This document outlines the plan to unify all CLI commands with consistent patterns, structured responses, and shared utilities. It builds on the path integrity and UI-CLI mapping concepts from VALID_PATHS.md.
- Current Command Inventory
- Identified Issues
- Unified Architecture
- Shared Utilities
- Command Result Standard
- Command-by-Command Migration
- Implementation Phases
| Command | Subcommands | Current Return | Uses resolvePath | Uses Store |
|---|---|---|---|---|
pwd |
- | void | No | Read currentUrl |
cd |
- | void | Yes (local copy) | Read row |
ls |
- | void | Yes (local copy) | Read table |
| Command | Subcommands | Current Return | Uses resolvePath | Uses Pod |
|---|---|---|---|---|
cat |
- | void | Yes (local copy) | No (store read) |
touch |
- | Promise | Yes (local copy) | Yes (PUT) |
mkdir |
- | Promise | Yes (local copy) | Yes (PUT) |
rm |
- | Promise | Yes (local copy) | Yes (DELETE) |
| Command | Subcommands | Current Return | Uses resolvePath |
|---|---|---|---|
file |
info, set-author, set-title, set-description | void | Yes (local copy) |
| Command | Subcommands | Current Return | Platform-specific |
|---|---|---|---|
export |
- | Promise | Yes (Node vs Browser) |
import |
- | void | Yes (UI-only in browser) |
| Command | Subcommands | Shared Utilities |
|---|---|---|
persona |
list, create, show, edit, delete, set-default, set-inbox, set-typeindex | findPersonaId (local) |
contact |
list, add, show, edit, delete, search, link | findContactId (local), findPersonaId (duplicated) |
group |
list, create, show, edit, delete, add-member, remove-member, list-members | findGroupId (local), findContactId (duplicated) |
| Command | Subcommands | Current Return |
|---|---|---|
config |
list, get, set, reset | void |
| Command | Subcommands | Current Return |
|---|---|---|
typeindex |
list, show, register, unregister | void |
| Command | File | Purpose |
|---|---|---|
help |
help.tsx | Show available commands |
clear |
clear.tsx | Clear terminal output |
exit |
exit.tsx | Exit CLI (Node only) |
resolvePath function - Duplicated in 3 files with identical implementation:
src/cli/commands/navigation.tsx:21-39src/cli/commands/files.tsx:8-19src/cli/commands/file.tsx:12-23
findPersonaId function - Duplicated in 2 files:
src/cli/commands/persona.tsx:515-542src/cli/commands/contact.tsx:577-601
findContactId function - Duplicated in 2 files:
src/cli/commands/contact.tsx:545-572src/cli/commands/group.tsx:694-718
Commands return different types:
- Most return
void - Async commands return
Promise<void> - No structured result for programmatic consumption
- No way to distinguish success from failure programmatically
- All commands output React components directly
- No
--jsonflag for machine-readable output - UI cannot consume command results programmatically
- Some commands throw errors
- Some commands output errors and return
- No standard error codes or messages
- Path validation happens inline in each command
- No pre-validation hook
- Validation logic differs between commands
- Some commands use
parseCliArgs() - Some commands access
argsdirectly - Option naming conventions vary
src/cli/
├── types.ts # Core types (enhanced)
├── registry.tsx # Command registry
├── executor.ts # NEW: Command executor with result handling
├── parse-args.ts # Argument parsing (enhanced)
├── path.ts # NEW: Centralized path utilities
├── entity-lookup.ts # NEW: Centralized entity lookup
├── commands/
│ ├── index.ts # Command exports
│ ├── navigation.tsx # pwd, cd, ls
│ ├── files.tsx # cat, touch, mkdir, rm
│ ├── file.tsx # file info/set-*
│ ├── data.tsx # export, import
│ ├── persona.tsx # persona *
│ ├── contact.tsx # contact *
│ ├── group.tsx # group *
│ ├── config.tsx # config *
│ ├── typeindex.tsx # typeindex *
│ ├── help.tsx # help
│ ├── clear.tsx # clear
│ └── exit.tsx # exit
└── hooks/
├── useCliExecutor.ts # NEW: React hook for UI integration
└── useCliContext.ts # NEW: Context provider hook
// src/cli/types.ts (enhanced)
/**
* Standardized command result
*/
export interface CommandResult<T = unknown> {
success: boolean;
data?: T;
message?: string;
error?: CommandError;
}
export interface CommandError {
code: ErrorCode;
message: string;
details?: unknown;
}
export type ErrorCode =
// Path errors
| 'INVALID_PATH'
| 'PATH_NOT_FOUND'
| 'NOT_A_DIRECTORY'
| 'NOT_A_FILE'
| 'ALREADY_EXISTS'
| 'PARENT_NOT_FOUND'
| 'DIRECTORY_NOT_EMPTY'
| 'ESCAPE_ATTEMPT'
// Entity errors
| 'ENTITY_NOT_FOUND'
| 'DUPLICATE_ENTITY'
| 'INVALID_ENTITY'
// Argument errors
| 'MISSING_ARGUMENT'
| 'INVALID_ARGUMENT'
| 'UNKNOWN_SUBCOMMAND'
// Operation errors
| 'OPERATION_FAILED'
| 'NOT_SUPPORTED'
| 'PERMISSION_DENIED';
/**
* Enhanced command definition
*/
export interface Command {
name: string;
description: string;
usage: string;
/**
* Execute the command
* @returns CommandResult for programmatic use, or void for output-only commands
*/
execute: (
args: string[],
context: CliContext,
options?: CommandOptions
) => CommandResult | Promise<CommandResult> | void | Promise<void>;
/**
* Optional: Validate arguments before execution
*/
validate?: (args: string[], context: CliContext) => CommandError | null;
/**
* Optional: Command supports JSON output mode
*/
supportsJson?: boolean;
/**
* Optional: Subcommands for compound commands
*/
subcommands?: Record<string, SubcommandDef>;
}
export interface SubcommandDef {
description: string;
usage: string;
execute: (
args: string[],
context: CliContext,
options?: CommandOptions
) => CommandResult | Promise<CommandResult> | void | Promise<void>;
}
export interface CommandOptions {
/** Suppress terminal output */
silent?: boolean;
/** Return structured data instead of rendering */
json?: boolean;
}See VALID_PATHS.md Section 1 for full specification. Key exports:
export function resolvePath(currentUrl: string, path: string, baseUrl: string): ResolveResult;
export function validateName(name: string): PathError | null;
export function ensureTrailingSlash(url: string): string;
export function removeTrailingSlash(url: string): string;
export function getParentUrl(url: string, baseUrl: string): string;
export function getSegments(url: string, baseUrl: string): string[];
export function decodeSegment(segment: string): string;
export function encodeSegment(name: string): string;
export function isContainer(url: string): boolean;
export function isDescendantOf(url: string, ancestorUrl: string): boolean;Consolidate all entity lookup functions:
import type { Store } from 'tinybase';
import { STORE_TABLES } from '../storeLayout';
import { FOAF, VCARD } from '@inrupt/vocab-common-rdf';
export interface LookupResult {
found: true;
id: string;
}
export interface LookupNotFound {
found: false;
}
export type EntityLookupResult = LookupResult | LookupNotFound;
/**
* Find a persona by ID, partial ID, or name
*/
export function findPersona(store: Store, query: string): EntityLookupResult {
const personas = store.getTable(STORE_TABLES.PERSONAS) || {};
// Exact match
if (personas[query]) {
return { found: true, id: query };
}
// Short ID match (UUID part without #me)
for (const id of Object.keys(personas)) {
const shortId = id.split('/').pop()?.replace('#me', '') || '';
if (shortId === query || shortId.startsWith(query)) {
return { found: true, id };
}
}
// Name match (case-insensitive)
const queryLower = query.toLowerCase();
for (const [id, record] of Object.entries(personas)) {
const name = ((record as Record<string, unknown>)[FOAF.name] as string || '').toLowerCase();
if (name === queryLower || name.includes(queryLower)) {
return { found: true, id };
}
}
return { found: false };
}
/**
* Find a contact by ID, partial ID, or name
*/
export function findContact(store: Store, query: string): EntityLookupResult {
const contacts = store.getTable(STORE_TABLES.CONTACTS) || {};
// Exact match
if (contacts[query]) {
return { found: true, id: query };
}
// Short ID match (after #)
for (const id of Object.keys(contacts)) {
const shortId = id.split('#').pop() || '';
if (shortId === query || shortId.startsWith(query)) {
return { found: true, id };
}
}
// Name match (case-insensitive)
const queryLower = query.toLowerCase();
for (const [id, record] of Object.entries(contacts)) {
const name = ((record as Record<string, unknown>)[VCARD.fn] as string || '').toLowerCase();
if (name === queryLower || name.includes(queryLower)) {
return { found: true, id };
}
}
return { found: false };
}
/**
* Find a group by ID, partial ID, or name
*/
export function findGroup(store: Store, query: string): EntityLookupResult {
const groups = store.getTable(STORE_TABLES.GROUPS) || {};
// Exact match
if (groups[query]) {
return { found: true, id: query };
}
// Slug match (after /groups/)
for (const id of Object.keys(groups)) {
const slug = id.split('/groups/').pop()?.split('#')[0] || '';
if (slug === query || slug.startsWith(query)) {
return { found: true, id };
}
}
// Name match (case-insensitive)
const queryLower = query.toLowerCase();
for (const [id, record] of Object.entries(groups)) {
const name = ((record as Record<string, unknown>)[VCARD.fn] as string || '').toLowerCase();
if (name === queryLower || name.includes(queryLower)) {
return { found: true, id };
}
}
return { found: false };
}
/**
* Generic entity lookup
*/
export function findEntity(
store: Store,
table: string,
query: string,
nameKey: string,
idExtractor: (id: string) => string
): EntityLookupResult {
const entities = store.getTable(table) || {};
if (entities[query]) {
return { found: true, id: query };
}
for (const id of Object.keys(entities)) {
const shortId = idExtractor(id);
if (shortId === query || shortId.startsWith(query)) {
return { found: true, id };
}
}
const queryLower = query.toLowerCase();
for (const [id, record] of Object.entries(entities)) {
const name = ((record as Record<string, unknown>)[nameKey] as string || '').toLowerCase();
if (name === queryLower || name.includes(queryLower)) {
return { found: true, id };
}
}
return { found: false };
}import React from 'react';
import { Text } from 'ink';
import type { Command, CliContext, CommandResult, CommandOptions, CommandError } from './types';
import { commands } from './registry';
/**
* Execute a command and return structured result
*/
export async function executeCommand(
input: string,
context: CliContext,
options?: CommandOptions
): Promise<CommandResult> {
const trimmed = input.trim();
if (!trimmed) {
return { success: true, message: '' };
}
const [cmdName, ...args] = trimmed.split(/\s+/);
const command = commands[cmdName.toLowerCase()];
if (!command) {
const error: CommandError = {
code: 'INVALID_ARGUMENT',
message: `Unknown command: ${cmdName}`,
};
if (!options?.silent) {
context.addOutput(
<Text color="red">Unknown command: {cmdName}. Type "help" for available commands.</Text>,
'error'
);
}
return { success: false, error };
}
// Run validation if defined
if (command.validate) {
const validationError = command.validate(args, context);
if (validationError) {
if (!options?.silent) {
context.addOutput(
<Text color="red">{validationError.message}</Text>,
'error'
);
}
return { success: false, error: validationError };
}
}
try {
const result = await command.execute(args, { ...context, commands }, options);
// If command returns a result, use it
if (result && typeof result === 'object' && 'success' in result) {
return result;
}
// Otherwise assume success
return { success: true };
} catch (err) {
const error: CommandError = {
code: 'OPERATION_FAILED',
message: err instanceof Error ? err.message : String(err),
};
if (!options?.silent) {
context.addOutput(
<Text color="red">Error: {error.message}</Text>,
'error'
);
}
return { success: false, error };
}
}
/**
* Execute a command by name with arguments
*/
export async function exec(
command: string,
args: string[],
context: CliContext,
options?: CommandOptions
): Promise<CommandResult> {
const input = [command, ...args].join(' ');
return executeCommand(input, context, options);
}// Add to existing parse-args.ts
/**
* Check if --json flag is present
*/
export const hasJsonFlag = (options: Record<string, string | boolean>): boolean => {
return options['json'] === true || options['j'] === true;
};
/**
* Get required positional argument or return error
*/
export const getRequiredArg = (
positional: string[],
index: number,
name: string
): string | CommandError => {
if (index >= positional.length || !positional[index]) {
return {
code: 'MISSING_ARGUMENT',
message: `Missing required argument: ${name}`,
};
}
return positional[index];
};
/**
* Validate subcommand
*/
export const validateSubcommand = (
subcommand: string | undefined,
validSubcommands: string[],
commandName: string
): CommandError | null => {
if (!subcommand) {
return {
code: 'MISSING_ARGUMENT',
message: `${commandName}: missing subcommand`,
};
}
if (!validSubcommands.includes(subcommand.toLowerCase())) {
return {
code: 'UNKNOWN_SUBCOMMAND',
message: `${commandName}: unknown subcommand: ${subcommand}`,
};
}
return null;
};// pwd
{ success: true, data: { url: string } }
// cd
{ success: true, data: { url: string, previousUrl: string } }
// ls
{ success: true, data: { url: string, children: ResourceInfo[] } }
interface ResourceInfo {
url: string;
name: string;
type: 'Container' | 'Resource';
contentType?: string;
size?: number;
updated?: string;
}// cat
{ success: true, data: { url: string, content: string, contentType: string } }
// touch
{ success: true, data: { url: string, created: true } }
// mkdir
{ success: true, data: { url: string, created: true } }
// rm
{ success: true, data: { url: string, deleted: true } }// persona/contact/group list
{ success: true, data: { count: number, items: EntitySummary[] } }
// persona/contact/group create
{ success: true, data: { id: string, name: string } }
// persona/contact/group show
{ success: true, data: EntityDetails }
// persona/contact/group edit
{ success: true, data: { id: string, updated: string[] } }
// persona/contact/group delete
{ success: true, data: { id: string, name: string } }// config list
{ success: true, data: { settings: SettingInfo[] } }
// config get
{ success: true, data: { key: string, value: unknown, isDefault: boolean } }
// config set
{ success: true, data: { key: string, value: unknown, previousValue: unknown } }
// config reset
{ success: true, data: { key?: string, resetAll: boolean } }Current:
execute: (_args, context) => {
context.addOutput(<Text color="cyan">{context.currentUrl}</Text>);
}Unified:
execute: (_args, context, options) => {
const result = { success: true, data: { url: context.currentUrl } };
if (options?.json) {
context.addOutput(<Text>{JSON.stringify(result.data)}</Text>);
} else if (!options?.silent) {
context.addOutput(<Text color="cyan">{context.currentUrl}</Text>);
}
return result;
}Current: Uses local resolvePath, handles trailing slash logic inline.
Unified:
import { resolvePath } from '../path';
execute: (args, context, options) => {
const { currentUrl, setCurrentUrl, baseUrl, store } = context;
const path = args[0];
const previousUrl = currentUrl;
// No path = go to root
if (!path) {
setCurrentUrl(baseUrl);
return { success: true, data: { url: baseUrl, previousUrl } };
}
// Resolve path
const resolved = resolvePath(currentUrl, path, baseUrl);
if (!resolved.valid) {
if (!options?.silent) {
context.addOutput(<Text color="red">cd: {resolved.error}</Text>, 'error');
}
return { success: false, error: { code: resolved.code, message: resolved.error } };
}
// Ensure it's a container
let targetUrl = resolved.url;
if (!targetUrl.endsWith('/')) {
targetUrl = targetUrl + '/';
}
// Check exists
if (!store.hasRow('resources', targetUrl)) {
const error = { code: 'PATH_NOT_FOUND', message: `cd: no such directory: ${path}` };
if (!options?.silent) {
context.addOutput(<Text color="red">{error.message}</Text>, 'error');
}
return { success: false, error };
}
// Check is container
const row = store.getRow('resources', targetUrl);
if (row.type !== 'Container') {
const error = { code: 'NOT_A_DIRECTORY', message: `cd: not a directory: ${path}` };
if (!options?.silent) {
context.addOutput(<Text color="red">{error.message}</Text>, 'error');
}
return { success: false, error };
}
setCurrentUrl(targetUrl);
return { success: true, data: { url: targetUrl, previousUrl } };
}Current: Outputs React component with children list.
Unified:
import { resolvePath } from '../path';
supportsJson: true,
execute: (args, context, options) => {
const { currentUrl, baseUrl, store, addOutput } = context;
const { positional, options: cmdOptions } = parseCliArgs(args);
const path = positional[0];
const jsonMode = options?.json || hasJsonFlag(cmdOptions);
const targetUrl = path ? resolvePath(currentUrl, path, baseUrl) : { valid: true, url: currentUrl };
if (!targetUrl.valid) {
return { success: false, error: { code: targetUrl.code, message: targetUrl.error } };
}
const row = store.getRow('resources', targetUrl.url);
if (!row) {
return {
success: false,
error: { code: 'PATH_NOT_FOUND', message: `ls: no such file or directory: ${path || targetUrl.url}` }
};
}
// Build children list
const children: ResourceInfo[] = [];
if (row.type === 'Container') {
const allRows = store.getTable('resources') || {};
Object.entries(allRows)
.filter(([, r]) => r.parentId === targetUrl.url)
.sort(([urlA, rowA], [urlB, rowB]) => {
if (rowA.type === 'Container' && rowB.type !== 'Container') return -1;
if (rowA.type !== 'Container' && rowB.type === 'Container') return 1;
return urlA.localeCompare(urlB);
})
.forEach(([url, r]) => {
children.push({
url,
name: decodeSegment(url.split('/').filter(Boolean).pop() || ''),
type: r.type as 'Container' | 'Resource',
contentType: r.contentType,
updated: r.updated,
});
});
} else {
// Single file
children.push({
url: targetUrl.url,
name: decodeSegment(targetUrl.url.split('/').filter(Boolean).pop() || ''),
type: 'Resource',
contentType: row.contentType,
updated: row.updated,
});
}
const result = { success: true, data: { url: targetUrl.url, children } };
if (jsonMode) {
addOutput(<Text>{JSON.stringify(result.data, null, 2)}</Text>);
} else if (!options?.silent) {
// Existing React output...
}
return result;
}Enhanced with --base64 support for binary files:
import { resolvePath, validateName } from '../path';
supportsJson: true,
execute: async (args, context, options) => {
const { currentUrl, baseUrl, pod, store, addOutput } = context;
const { positional, options: cmdOptions } = parseCliArgs(args);
const filename = positional[0];
if (!filename) {
return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'touch: missing file operand' } };
}
// Validate filename
const nameError = validateName(filename);
if (nameError) {
return { success: false, error: { code: nameError.code, message: `touch: ${nameError.error}` } };
}
// Check current location is container
const row = store.getRow('resources', currentUrl);
if (!row || row.type !== 'Container') {
return { success: false, error: { code: 'NOT_A_DIRECTORY', message: 'touch: current location is not a directory' } };
}
const content = getOptionString(cmdOptions, 'content') || '';
const contentType = getOptionString(cmdOptions, 'type') || 'text/plain';
const isBase64 = getOptionBoolean(cmdOptions, 'base64');
const resolved = resolvePath(currentUrl, filename, baseUrl);
if (!resolved.valid) {
return { success: false, error: { code: resolved.code, message: `touch: ${resolved.error}` } };
}
const result = await pod.handleRequest(resolved.url, {
method: 'PUT',
body: content,
headers: {
'Content-Type': contentType,
...(isBase64 ? { 'Content-Transfer-Encoding': 'base64' } : {}),
},
});
if (result.status === 201) {
const successResult = { success: true, data: { url: resolved.url, created: true } };
if (!options?.silent) {
addOutput(<Text color="green">Created: {filename}</Text>, 'success');
}
return successResult;
}
return {
success: false,
error: { code: 'OPERATION_FAILED', message: `touch: failed to create ${filename}: ${result.body}` }
};
}All entity commands (persona, contact, group) follow a similar pattern. Here's the unified template:
import { findPersona, findContact, findGroup } from '../entity-lookup';
export const personaCommand: Command = {
name: 'persona',
description: 'Manage identity personas',
usage: 'persona <subcommand> [args]',
supportsJson: true,
subcommands: {
list: {
description: 'List all personas',
usage: 'persona list [--json]',
execute: (args, context, options) => {
const { store, addOutput } = context;
const { options: cmdOptions } = parseCliArgs(args);
const jsonMode = options?.json || hasJsonFlag(cmdOptions);
const personas = store.getTable(STORE_TABLES.PERSONAS) || {};
const items = Object.entries(personas).map(([id, record]) => ({
id,
name: getPersonaName(record),
isDefault: store.getValue('defaultPersonaId') === id,
}));
const result = { success: true, data: { count: items.length, items } };
if (jsonMode) {
addOutput(<Text>{JSON.stringify(result.data, null, 2)}</Text>);
} else if (!options?.silent) {
// Existing React rendering...
}
return result;
},
},
create: {
description: 'Create a new persona',
usage: 'persona create <name> [--nickname=...] [--email=...]',
execute: (args, context, options) => {
// ... implementation
},
},
show: {
description: 'Show persona details',
usage: 'persona show <id> [--full] [--json]',
execute: (args, context, options) => {
const { store, addOutput } = context;
const { positional, options: cmdOptions } = parseCliArgs(args);
const idArg = positional[0];
if (!idArg) {
return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'persona show: missing id' } };
}
const lookup = findPersona(store, idArg);
if (!lookup.found) {
return { success: false, error: { code: 'ENTITY_NOT_FOUND', message: `Persona not found: ${idArg}` } };
}
const persona = store.getRow(STORE_TABLES.PERSONAS, lookup.id);
// ... build result and output
},
},
// ... other subcommands
},
execute: (args, context, options) => {
const subcommand = args[0]?.toLowerCase();
const subArgs = args.slice(1);
if (!subcommand || subcommand === 'help') {
context.addOutput(<Text>{personaCommand.usage}</Text>);
return { success: true };
}
const handler = personaCommand.subcommands?.[subcommand];
if (!handler) {
return {
success: false,
error: { code: 'UNKNOWN_SUBCOMMAND', message: `persona: unknown subcommand: ${subcommand}` }
};
}
return handler.execute(subArgs, context, options);
},
};-
Create shared utilities:
-
src/cli/path.ts- Path resolution and validation -
src/cli/entity-lookup.ts- Entity lookup functions -
src/cli/executor.ts- Command executor
-
-
Enhance types:
- Add
CommandResultinterface totypes.ts - Add
CommandErrorand error codes - Add
CommandOptionsinterface - Add
supportsJsonto Command interface
- Add
-
Enhance argument parsing:
- Add
hasJsonFlag()helper - Add
getRequiredArg()helper - Add
validateSubcommand()helper
- Add
-
Migrate navigation commands:
- Update
pwdwith result type - Update
cdto use centralized path module - Update
lswith --json support
- Update
-
Add tests:
- Path module unit tests
- Navigation command tests
-
Migrate file operation commands:
- Update
catwith result type and --json - Update
touchwith --base64 support - Update
mkdirwith result type - Update
rmwith --recursive and --force
- Update
-
Migrate file metadata commands:
- Update
file infowith --json - Update
file set-*commands
- Update
-
Migrate persona command:
- Use centralized
findPersona() - Add --json to all subcommands
- Standardize result types
- Use centralized
-
Migrate contact command:
- Use centralized
findContact()andfindPersona() - Add --json to all subcommands
- Standardize result types
- Use centralized
-
Migrate group command:
- Use centralized
findGroup()andfindContact() - Add --json to all subcommands
- Standardize result types
- Use centralized
-
Migrate config commands:
- Add --json support
- Standardize result types
-
Migrate typeindex commands:
- Add --json support
- Standardize result types
-
Migrate data commands:
- Standardize export result
- Plan import implementation
-
Create React hooks:
-
useCliExecutorhook -
useCliContextprovider
-
-
Update App.tsx:
- Replace direct pod calls with CLI executor
- Wire up dialogs to command responses
-
Integration tests:
- E2E tests for UI-CLI flow
- Verify result consumption
After unification, all commands marked with supportsJson: true will accept --json flag:
| Command | Subcommands with --json |
|---|---|
pwd |
(direct) |
ls |
(direct) |
cat |
(direct) |
touch |
(direct) |
mkdir |
(direct) |
rm |
(direct) |
file |
info |
persona |
list, show, create, edit, delete |
contact |
list, show, add, edit, delete, search |
group |
list, show, create, edit, delete, list-members |
config |
list, get, set, reset |
typeindex |
list, show |
| Code | Description | Commands |
|---|---|---|
INVALID_PATH |
Path contains invalid characters | cd, ls, cat, touch, mkdir, rm |
PATH_NOT_FOUND |
Target path does not exist | cd, ls, cat, rm |
NOT_A_DIRECTORY |
Expected directory, got file | cd, ls, touch, mkdir |
NOT_A_FILE |
Expected file, got directory | cat |
ALREADY_EXISTS |
Resource already exists | touch, mkdir |
PARENT_NOT_FOUND |
Parent container missing | touch, mkdir |
DIRECTORY_NOT_EMPTY |
Cannot delete non-empty dir | rm |
ESCAPE_ATTEMPT |
Path tries to escape root | cd, ls, touch, mkdir, rm |
ENTITY_NOT_FOUND |
Persona/contact/group not found | persona, contact, group |
DUPLICATE_ENTITY |
Entity with same ID exists | persona create, contact add |
INVALID_ENTITY |
Entity data validation failed | persona, contact, group |
MISSING_ARGUMENT |
Required argument not provided | All commands |
INVALID_ARGUMENT |
Argument value is invalid | All commands |
UNKNOWN_SUBCOMMAND |
Subcommand not recognized | file, persona, contact, group, config, typeindex |
OPERATION_FAILED |
Operation failed unexpectedly | All async commands |
NOT_SUPPORTED |
Feature not supported in env | import (browser), exit (browser) |
PERMISSION_DENIED |
Operation not allowed | rm (root), future ACL |