@abraca/cli 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abracadabra-cli.cjs +3301 -0
- package/dist/abracadabra-cli.cjs.map +1 -0
- package/dist/abracadabra-cli.esm.js +3272 -0
- package/dist/abracadabra-cli.esm.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/package.json +39 -0
- package/src/command.ts +29 -0
- package/src/commands/awareness.ts +122 -0
- package/src/commands/content.ts +135 -0
- package/src/commands/documents.ts +419 -0
- package/src/commands/files.ts +117 -0
- package/src/commands/help.ts +72 -0
- package/src/commands/meta.ts +149 -0
- package/src/commands/permissions.ts +94 -0
- package/src/commands/spaces.ts +100 -0
- package/src/commands/tree.ts +158 -0
- package/src/connection.ts +321 -0
- package/src/converters/index.ts +10 -0
- package/src/converters/markdownToYjs.ts +3 -0
- package/src/converters/types.ts +3 -0
- package/src/converters/yjsToMarkdown.ts +3 -0
- package/src/crypto.ts +69 -0
- package/src/index.ts +114 -0
- package/src/output.ts +109 -0
- package/src/parser.ts +80 -0
- package/src/resolve.ts +174 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata commands: meta, tags.
|
|
3
|
+
*/
|
|
4
|
+
import * as Y from 'yjs'
|
|
5
|
+
import { registerCommand } from '../command.ts'
|
|
6
|
+
import type { CLIConnection } from '../connection.ts'
|
|
7
|
+
import type { ParsedArgs } from '../parser.ts'
|
|
8
|
+
import { readEntries, resolveDocument } from '../resolve.ts'
|
|
9
|
+
import { printJson, printTable, getFormat } from '../output.ts'
|
|
10
|
+
|
|
11
|
+
/** Safely read a tree map value. */
|
|
12
|
+
function toPlain(val: any): any {
|
|
13
|
+
return val instanceof Y.Map ? val.toJSON() : val
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
registerCommand({
|
|
17
|
+
name: 'meta',
|
|
18
|
+
aliases: ['metadata'],
|
|
19
|
+
description: 'Get or set document metadata.',
|
|
20
|
+
usage: 'meta id=<docId> | name=<label> [key=value ...] [--format=json]',
|
|
21
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
22
|
+
if (!conn) return 'Not connected'
|
|
23
|
+
|
|
24
|
+
const docId = resolveDocument(conn, args.params, args.positional)
|
|
25
|
+
if (!docId) return 'Document not found.'
|
|
26
|
+
|
|
27
|
+
const treeMap = conn.getTreeMap()
|
|
28
|
+
if (!treeMap) return 'Not connected'
|
|
29
|
+
|
|
30
|
+
const raw = treeMap.get(docId)
|
|
31
|
+
if (!raw) return `Document ${docId} not found.`
|
|
32
|
+
|
|
33
|
+
const entry = toPlain(raw)
|
|
34
|
+
|
|
35
|
+
// Collect meta updates from remaining params
|
|
36
|
+
const metaKeys = new Set(['icon', 'color', 'priority', 'status', 'checked', 'rating',
|
|
37
|
+
'tags', 'dateStart', 'dateEnd', 'datetimeStart', 'datetimeEnd', 'allDay',
|
|
38
|
+
'url', 'email', 'phone', 'number', 'unit', 'subtitle', 'note',
|
|
39
|
+
'taskProgress', 'coverUploadId', 'geoType', 'geoLat', 'geoLng',
|
|
40
|
+
'deskX', 'deskY', 'deskMode', 'spShape', 'spColor', 'chartType',
|
|
41
|
+
'kanbanColumnWidth', 'galleryColumns', 'calendarView', 'tableMode',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
const updates: Record<string, unknown> = {}
|
|
45
|
+
let hasUpdates = false
|
|
46
|
+
|
|
47
|
+
for (const [key, value] of Object.entries(args.params)) {
|
|
48
|
+
if (metaKeys.has(key)) {
|
|
49
|
+
// Parse special types
|
|
50
|
+
if (value === 'null') {
|
|
51
|
+
updates[key] = null
|
|
52
|
+
} else if (value === 'true') {
|
|
53
|
+
updates[key] = true
|
|
54
|
+
} else if (value === 'false') {
|
|
55
|
+
updates[key] = false
|
|
56
|
+
} else if (['priority', 'rating', 'number', 'taskProgress', 'geoLat', 'geoLng',
|
|
57
|
+
'deskX', 'deskY', 'galleryColumns'].includes(key)) {
|
|
58
|
+
updates[key] = parseFloat(value)
|
|
59
|
+
} else if (key === 'tags') {
|
|
60
|
+
updates[key] = value.split(',').map(s => s.trim())
|
|
61
|
+
} else {
|
|
62
|
+
updates[key] = value
|
|
63
|
+
}
|
|
64
|
+
hasUpdates = true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (hasUpdates) {
|
|
69
|
+
// Write mode: merge updates into existing meta
|
|
70
|
+
treeMap.set(docId, {
|
|
71
|
+
...entry,
|
|
72
|
+
meta: { ...(entry.meta ?? {}), ...updates },
|
|
73
|
+
updatedAt: Date.now(),
|
|
74
|
+
})
|
|
75
|
+
return `Metadata updated for ${docId.slice(0, 8)}…`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Read mode: display current metadata
|
|
79
|
+
const meta = entry.meta ?? {}
|
|
80
|
+
if (args.params['format'] === 'json') {
|
|
81
|
+
return printJson({ id: docId, label: entry.label, type: entry.type, meta })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Object.keys(meta).length === 0) {
|
|
85
|
+
return `Document "${entry.label}" has no metadata.`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const lines = Object.entries(meta).map(([k, v]) =>
|
|
89
|
+
`${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`
|
|
90
|
+
)
|
|
91
|
+
return lines.join('\n')
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
registerCommand({
|
|
96
|
+
name: 'tags',
|
|
97
|
+
description: 'List tags aggregated from document metadata.',
|
|
98
|
+
usage: 'tags [id=<docId>] [--counts] [--total] [--format=json|tsv]',
|
|
99
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
100
|
+
if (!conn) return 'Not connected'
|
|
101
|
+
const treeMap = conn.getTreeMap()
|
|
102
|
+
if (!treeMap) return 'Not connected'
|
|
103
|
+
|
|
104
|
+
// If a document is specified, show its tags
|
|
105
|
+
const docId = resolveDocument(conn, args.params, args.positional)
|
|
106
|
+
if (docId) {
|
|
107
|
+
const raw = treeMap.get(docId)
|
|
108
|
+
if (!raw) return `Document ${docId} not found.`
|
|
109
|
+
const entry = toPlain(raw)
|
|
110
|
+
const tags: string[] = entry.meta?.tags ?? []
|
|
111
|
+
if (tags.length === 0) return 'No tags.'
|
|
112
|
+
return tags.join('\n')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Aggregate tags from all documents
|
|
116
|
+
const entries = readEntries(treeMap)
|
|
117
|
+
const tagCounts = new Map<string, number>()
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const tags: string[] = (entry.meta as any)?.tags ?? []
|
|
121
|
+
for (const tag of tags) {
|
|
122
|
+
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (tagCounts.size === 0) return 'No tags found.'
|
|
127
|
+
|
|
128
|
+
if (args.flags.has('total')) {
|
|
129
|
+
return String(tagCounts.size)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sorted = [...tagCounts.entries()].sort((a, b) => {
|
|
133
|
+
if (args.params['sort'] === 'count') return b[1] - a[1]
|
|
134
|
+
return a[0].localeCompare(b[0])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const format = getFormat(args, 'text')
|
|
138
|
+
|
|
139
|
+
if (format === 'json') {
|
|
140
|
+
return printJson(sorted.map(([tag, count]) => ({ tag, count })))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (args.flags.has('counts')) {
|
|
144
|
+
return sorted.map(([tag, count]) => `${tag}\t${count}`).join('\n')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return sorted.map(([tag]) => tag).join('\n')
|
|
148
|
+
},
|
|
149
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permission commands: permissions.
|
|
3
|
+
*/
|
|
4
|
+
import { registerCommand } from '../command.ts'
|
|
5
|
+
import type { CLIConnection } from '../connection.ts'
|
|
6
|
+
import type { ParsedArgs } from '../parser.ts'
|
|
7
|
+
import { resolveDocument } from '../resolve.ts'
|
|
8
|
+
import { printJson, printTable, getFormat } from '../output.ts'
|
|
9
|
+
|
|
10
|
+
registerCommand({
|
|
11
|
+
name: 'permissions',
|
|
12
|
+
aliases: ['perms'],
|
|
13
|
+
description: 'List or set document permissions.',
|
|
14
|
+
usage: 'permissions id=<docId> [user=<userId> role=<role>] [--effective] [--format=json|tsv]',
|
|
15
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
16
|
+
if (!conn) return 'Not connected'
|
|
17
|
+
|
|
18
|
+
const docId = resolveDocument(conn, args.params, args.positional)
|
|
19
|
+
if (!docId) return 'Document not found.'
|
|
20
|
+
|
|
21
|
+
// Set permission mode
|
|
22
|
+
const targetUser = args.params['user'] || args.params['user_id']
|
|
23
|
+
const targetRole = args.params['role']
|
|
24
|
+
|
|
25
|
+
if (targetUser && targetRole) {
|
|
26
|
+
try {
|
|
27
|
+
await conn.client.setPermission(docId, {
|
|
28
|
+
user_id: targetUser,
|
|
29
|
+
role: targetRole as 'owner' | 'editor' | 'viewer' | 'observer',
|
|
30
|
+
})
|
|
31
|
+
return `Set ${targetUser.slice(0, 12)}… → ${targetRole} on ${docId.slice(0, 8)}…`
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
return `Error: ${error.message}`
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Remove permission mode
|
|
38
|
+
if (targetUser && args.flags.has('remove')) {
|
|
39
|
+
try {
|
|
40
|
+
await conn.client.removePermission(docId, { user_id: targetUser })
|
|
41
|
+
return `Removed permissions for ${targetUser.slice(0, 12)}… on ${docId.slice(0, 8)}…`
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
return `Error: ${error.message}`
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// List permissions mode
|
|
48
|
+
try {
|
|
49
|
+
const format = getFormat(args, 'text')
|
|
50
|
+
|
|
51
|
+
if (args.flags.has('effective')) {
|
|
52
|
+
const result = await conn.client.listEffectivePermissions(docId)
|
|
53
|
+
|
|
54
|
+
if (format === 'json') {
|
|
55
|
+
return printJson(result)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (result.permissions.length === 0) {
|
|
59
|
+
return `Default role: ${result.default_role}\nNo explicit permissions.`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const rows = result.permissions.map(p => [
|
|
63
|
+
p.display_name || p.username,
|
|
64
|
+
p.role,
|
|
65
|
+
p.source,
|
|
66
|
+
p.inherited_from_doc_id ? p.inherited_from_doc_id.slice(0, 8) + '…' : '—',
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
return `Default role: ${result.default_role}\n\n` +
|
|
70
|
+
printTable(rows, ['USER', 'ROLE', 'SOURCE', 'INHERITED FROM'])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const perms = await conn.client.listPermissions(docId)
|
|
74
|
+
|
|
75
|
+
if (format === 'json') {
|
|
76
|
+
return printJson(perms)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (perms.length === 0) {
|
|
80
|
+
return 'No explicit permissions set.'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rows = perms.map(p => [
|
|
84
|
+
p.display_name || p.username,
|
|
85
|
+
p.role,
|
|
86
|
+
p.user_id.slice(0, 12) + '…',
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
return printTable(rows, ['USER', 'ROLE', 'USER ID'])
|
|
90
|
+
} catch (error: any) {
|
|
91
|
+
return `Error: ${error.message}`
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spaces, spaces:switch, info commands.
|
|
3
|
+
*/
|
|
4
|
+
import { registerCommand } from '../command.ts'
|
|
5
|
+
import type { CLIConnection } from '../connection.ts'
|
|
6
|
+
import type { ParsedArgs } from '../parser.ts'
|
|
7
|
+
import { printJson, printTable, getFormat, pad } from '../output.ts'
|
|
8
|
+
import { readEntries, childrenOf } from '../resolve.ts'
|
|
9
|
+
|
|
10
|
+
registerCommand({
|
|
11
|
+
name: 'info',
|
|
12
|
+
description: 'Show server and space info.',
|
|
13
|
+
async run(conn: CLIConnection | null): Promise<string> {
|
|
14
|
+
if (!conn) return 'Not connected'
|
|
15
|
+
const si = conn.serverInfo
|
|
16
|
+
if (!si) return 'No server info available'
|
|
17
|
+
|
|
18
|
+
const treeMap = conn.getTreeMap()
|
|
19
|
+
const docCount = treeMap ? readEntries(treeMap).length : 0
|
|
20
|
+
|
|
21
|
+
const rootProvider = conn.rootProvider
|
|
22
|
+
const onlineUsers = rootProvider
|
|
23
|
+
? Array.from(rootProvider.awareness.getStates().values()).filter((s: any) => s.user).length
|
|
24
|
+
: 0
|
|
25
|
+
|
|
26
|
+
const lines = [
|
|
27
|
+
`Server: ${si.name ?? '—'}`,
|
|
28
|
+
`URL: ${conn.config.url}`,
|
|
29
|
+
`Version: ${si.version ?? '—'}`,
|
|
30
|
+
`Protocol: ${si.protocol_version ?? '—'}`,
|
|
31
|
+
`Hub Doc: ${conn.rootDocId ?? '—'}`,
|
|
32
|
+
`Auth: ${si.auth_methods?.join(', ') ?? '—'}`,
|
|
33
|
+
`Registration:${si.registration_allowed ? ' open' : ' closed'}${si.invite_only ? ' (invite only)' : ''}`,
|
|
34
|
+
`Documents: ${docCount}`,
|
|
35
|
+
`Users: ${onlineUsers} online`,
|
|
36
|
+
]
|
|
37
|
+
return lines.join('\n')
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
registerCommand({
|
|
42
|
+
name: 'spaces',
|
|
43
|
+
description: 'List available spaces/root documents.',
|
|
44
|
+
usage: 'spaces [--format=json|tsv]',
|
|
45
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
46
|
+
if (!conn) return 'Not connected'
|
|
47
|
+
const spaces = conn.spaces
|
|
48
|
+
if (!spaces.length) return 'No spaces available.'
|
|
49
|
+
|
|
50
|
+
const format = getFormat(args, 'text')
|
|
51
|
+
const active = conn.rootDocId
|
|
52
|
+
|
|
53
|
+
if (format === 'json') {
|
|
54
|
+
return printJson(spaces.map(s => ({ ...s, active: s.doc_id === active })))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rows = spaces.map(s => [
|
|
58
|
+
s.doc_id === active ? '▸' : ' ',
|
|
59
|
+
s.doc_id.slice(0, 8) + '…',
|
|
60
|
+
s.name,
|
|
61
|
+
s.is_hub ? 'hub' : '',
|
|
62
|
+
s.visibility,
|
|
63
|
+
])
|
|
64
|
+
|
|
65
|
+
return printTable(rows, ['', 'ID', 'NAME', 'HUB', 'VISIBILITY'])
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
registerCommand({
|
|
70
|
+
name: 'spaces:switch',
|
|
71
|
+
aliases: ['switch'],
|
|
72
|
+
description: 'Switch the active space.',
|
|
73
|
+
usage: 'spaces:switch name=<name> | id=<docId>',
|
|
74
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
75
|
+
if (!conn) return 'Not connected'
|
|
76
|
+
|
|
77
|
+
const targetName = args.params['name'] || args.positional[0]
|
|
78
|
+
const targetId = args.params['id']
|
|
79
|
+
|
|
80
|
+
let docId: string | null = null
|
|
81
|
+
|
|
82
|
+
if (targetId) {
|
|
83
|
+
docId = targetId
|
|
84
|
+
} else if (targetName) {
|
|
85
|
+
const lower = targetName.toLowerCase()
|
|
86
|
+
const space = conn.spaces.find(s =>
|
|
87
|
+
s.name.toLowerCase() === lower || s.doc_id === targetName
|
|
88
|
+
)
|
|
89
|
+
if (space) docId = space.doc_id
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!docId) {
|
|
93
|
+
return 'Space not found. Use "abracadabra spaces" to list available spaces.'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await conn.switchSpace(docId)
|
|
97
|
+
const space = conn.spaces.find(s => s.doc_id === docId)
|
|
98
|
+
return `Switched to space "${space?.name ?? docId}"`
|
|
99
|
+
},
|
|
100
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tree, ls, search commands — navigate and explore the document tree.
|
|
3
|
+
*/
|
|
4
|
+
import { registerCommand } from '../command.ts'
|
|
5
|
+
import type { CLIConnection } from '../connection.ts'
|
|
6
|
+
import type { ParsedArgs } from '../parser.ts'
|
|
7
|
+
import {
|
|
8
|
+
readEntries,
|
|
9
|
+
childrenOf,
|
|
10
|
+
buildTree,
|
|
11
|
+
normalizeRootId,
|
|
12
|
+
resolveDocument,
|
|
13
|
+
getAncestorPath,
|
|
14
|
+
type TreeDisplayNode,
|
|
15
|
+
} from '../resolve.ts'
|
|
16
|
+
import { printJson, printTable, printTree, getFormat, relativeTime, type TreeNode } from '../output.ts'
|
|
17
|
+
|
|
18
|
+
function toTreeNodes(nodes: TreeDisplayNode[]): TreeNode[] {
|
|
19
|
+
return nodes.map(n => ({
|
|
20
|
+
label: n.label,
|
|
21
|
+
type: n.type,
|
|
22
|
+
children: n.children.length > 0 ? toTreeNodes(n.children) : undefined,
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
registerCommand({
|
|
27
|
+
name: 'tree',
|
|
28
|
+
aliases: ['t'],
|
|
29
|
+
description: 'Show the full document tree hierarchy.',
|
|
30
|
+
usage: 'tree [id=<docId>] [depth=<n>] [--format=json|tree]',
|
|
31
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
32
|
+
if (!conn) return 'Not connected'
|
|
33
|
+
const treeMap = conn.getTreeMap()
|
|
34
|
+
if (!treeMap) return 'Not connected'
|
|
35
|
+
|
|
36
|
+
const rootId = normalizeRootId(
|
|
37
|
+
args.params['id'] || args.positional[0] || conn.rootDocId,
|
|
38
|
+
conn
|
|
39
|
+
)
|
|
40
|
+
const maxDepth = args.params['depth'] ? parseInt(args.params['depth'], 10) : -1
|
|
41
|
+
const format = getFormat(args, 'tree')
|
|
42
|
+
|
|
43
|
+
const entries = readEntries(treeMap)
|
|
44
|
+
const tree = buildTree(entries, rootId, maxDepth)
|
|
45
|
+
|
|
46
|
+
if (format === 'json') {
|
|
47
|
+
return printJson(tree)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (tree.length === 0) {
|
|
51
|
+
return '(empty tree)'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find root label for header
|
|
55
|
+
const hubSpace = conn.spaces.find(s => s.doc_id === conn.rootDocId)
|
|
56
|
+
const rootLabel = hubSpace?.name ?? conn.rootDocId ?? 'Workspace'
|
|
57
|
+
|
|
58
|
+
return rootLabel + '\n' + printTree(toTreeNodes(tree))
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
registerCommand({
|
|
63
|
+
name: 'ls',
|
|
64
|
+
aliases: ['list', 'l'],
|
|
65
|
+
description: 'List direct children of a document (defaults to root).',
|
|
66
|
+
usage: 'ls [id=<parentId>] [name=<parentName>] [--format=json|tsv] [--total]',
|
|
67
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
68
|
+
if (!conn) return 'Not connected'
|
|
69
|
+
const treeMap = conn.getTreeMap()
|
|
70
|
+
if (!treeMap) return 'Not connected'
|
|
71
|
+
|
|
72
|
+
let parentId: string | null = null
|
|
73
|
+
if (args.params['id'] || args.params['name'] || args.params['path'] || args.positional[0]) {
|
|
74
|
+
parentId = resolveDocument(conn, args.params, args.positional)
|
|
75
|
+
if (!parentId && (args.params['id'] || args.params['name'] || args.params['path'])) {
|
|
76
|
+
return 'Document not found.'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const targetId = normalizeRootId(parentId || conn.rootDocId, conn)
|
|
81
|
+
const entries = readEntries(treeMap)
|
|
82
|
+
const children = childrenOf(entries, targetId)
|
|
83
|
+
|
|
84
|
+
if (args.flags.has('total')) {
|
|
85
|
+
return String(children.length)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const format = getFormat(args, 'text')
|
|
89
|
+
|
|
90
|
+
if (format === 'json') {
|
|
91
|
+
return printJson(children)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (children.length === 0) {
|
|
95
|
+
return '(no children)'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rows = children.map(c => [
|
|
99
|
+
c.id.slice(0, 8) + '…',
|
|
100
|
+
c.label,
|
|
101
|
+
c.type ?? '—',
|
|
102
|
+
relativeTime(c.updatedAt),
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
return printTable(rows, ['ID', 'LABEL', 'TYPE', 'UPDATED'])
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
registerCommand({
|
|
110
|
+
name: 'search',
|
|
111
|
+
aliases: ['find', 's'],
|
|
112
|
+
description: 'Search documents by label across the tree.',
|
|
113
|
+
usage: 'search query=<text> [id=<rootId>] [--format=json|tsv] [--total]',
|
|
114
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
115
|
+
if (!conn) return 'Not connected'
|
|
116
|
+
const treeMap = conn.getTreeMap()
|
|
117
|
+
if (!treeMap) return 'Not connected'
|
|
118
|
+
|
|
119
|
+
const query = args.params['query'] || args.positional[0]
|
|
120
|
+
if (!query) return 'Missing required parameter: query=<text>'
|
|
121
|
+
|
|
122
|
+
const entries = readEntries(treeMap)
|
|
123
|
+
const lowerQuery = query.toLowerCase()
|
|
124
|
+
|
|
125
|
+
const matches = entries.filter(e =>
|
|
126
|
+
e.label.toLowerCase().includes(lowerQuery)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (args.flags.has('total')) {
|
|
130
|
+
return String(matches.length)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const format = getFormat(args, 'text')
|
|
134
|
+
|
|
135
|
+
if (matches.length === 0) {
|
|
136
|
+
return `No documents found matching "${query}".`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const results = matches.map(entry => ({
|
|
140
|
+
id: entry.id,
|
|
141
|
+
label: entry.label,
|
|
142
|
+
type: entry.type,
|
|
143
|
+
path: getAncestorPath(entries, entry.id),
|
|
144
|
+
}))
|
|
145
|
+
|
|
146
|
+
if (format === 'json') {
|
|
147
|
+
return printJson(results)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const rows = results.map(r => [
|
|
151
|
+
r.path.length > 0 ? r.path.join(' / ') + ' / ' + r.label : r.label,
|
|
152
|
+
r.label,
|
|
153
|
+
r.type ?? '—',
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
return printTable(rows, ['PATH', 'LABEL', 'TYPE'])
|
|
157
|
+
},
|
|
158
|
+
})
|