@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
package/src/parser.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal argument parser for Abracadabra CLI.
|
|
3
|
+
*
|
|
4
|
+
* Syntax:
|
|
5
|
+
* abracadabra [--flag] <command>[:<subcommand>] [key=value ...] [--flag2]
|
|
6
|
+
* abracadabra [--flag] <command>[:<subcommand>] [key="value with spaces"] [--format=json]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ParsedArgs {
|
|
10
|
+
/** The command name, e.g. "tree", "spaces:switch", "read" */
|
|
11
|
+
command: string
|
|
12
|
+
/** Key-value parameters, e.g. { name: "My Doc", type: "doc" } */
|
|
13
|
+
params: Record<string, string>
|
|
14
|
+
/** Boolean flags, e.g. { total: true, verbose: true } */
|
|
15
|
+
flags: Set<string>
|
|
16
|
+
/** Positional arguments (non-flag, non-key=value, after the command) */
|
|
17
|
+
positional: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse CLI arguments into a structured object.
|
|
22
|
+
* @param argv Raw process.argv (includes node path and script path)
|
|
23
|
+
*/
|
|
24
|
+
export function parseArgs(argv: string[]): ParsedArgs {
|
|
25
|
+
const args = argv.slice(2) // skip node + script
|
|
26
|
+
|
|
27
|
+
const result: ParsedArgs = {
|
|
28
|
+
command: 'help',
|
|
29
|
+
params: {},
|
|
30
|
+
flags: new Set(),
|
|
31
|
+
positional: [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let commandFound = false
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
const arg = args[i]
|
|
38
|
+
|
|
39
|
+
// --flag or --key=value
|
|
40
|
+
if (arg.startsWith('--')) {
|
|
41
|
+
const stripped = arg.slice(2)
|
|
42
|
+
const eqIdx = stripped.indexOf('=')
|
|
43
|
+
if (eqIdx !== -1) {
|
|
44
|
+
// --format=json style
|
|
45
|
+
const key = stripped.slice(0, eqIdx)
|
|
46
|
+
const value = stripped.slice(eqIdx + 1)
|
|
47
|
+
result.params[key] = value
|
|
48
|
+
} else {
|
|
49
|
+
result.flags.add(stripped)
|
|
50
|
+
}
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// key=value pair
|
|
55
|
+
const eqIdx = arg.indexOf('=')
|
|
56
|
+
if (eqIdx !== -1 && eqIdx > 0) {
|
|
57
|
+
const key = arg.slice(0, eqIdx)
|
|
58
|
+
let value = arg.slice(eqIdx + 1)
|
|
59
|
+
// Strip surrounding quotes if present
|
|
60
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
61
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
62
|
+
value = value.slice(1, -1)
|
|
63
|
+
}
|
|
64
|
+
result.params[key] = value
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// First bare word is the command
|
|
69
|
+
if (!commandFound) {
|
|
70
|
+
result.command = arg
|
|
71
|
+
commandFound = true
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Subsequent bare words are positional
|
|
76
|
+
result.positional.push(arg)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
}
|
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document resolution utilities for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Resolves documents by id, name (label), or path (label chain from root).
|
|
5
|
+
* Similar to Obsidian's file= / path= resolution.
|
|
6
|
+
*/
|
|
7
|
+
import * as Y from 'yjs'
|
|
8
|
+
import type { CLIConnection } from './connection.ts'
|
|
9
|
+
|
|
10
|
+
export interface TreeEntry {
|
|
11
|
+
id: string
|
|
12
|
+
label: string
|
|
13
|
+
parentId: string | null
|
|
14
|
+
order: number
|
|
15
|
+
type?: string
|
|
16
|
+
meta?: Record<string, unknown>
|
|
17
|
+
createdAt?: number
|
|
18
|
+
updatedAt?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Safely read a tree map value, converting Y.Map to plain object if needed. */
|
|
22
|
+
function toPlain(val: any): any {
|
|
23
|
+
return val instanceof Y.Map ? val.toJSON() : val
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Read all tree entries from the Y.Map. */
|
|
27
|
+
export function readEntries(treeMap: Y.Map<any>): TreeEntry[] {
|
|
28
|
+
const entries: TreeEntry[] = []
|
|
29
|
+
treeMap.forEach((raw: any, id: string) => {
|
|
30
|
+
const value = toPlain(raw)
|
|
31
|
+
if (typeof value !== 'object' || value === null) return
|
|
32
|
+
entries.push({
|
|
33
|
+
id,
|
|
34
|
+
label: value.label || 'Untitled',
|
|
35
|
+
parentId: value.parentId ?? null,
|
|
36
|
+
order: value.order ?? 0,
|
|
37
|
+
type: value.type,
|
|
38
|
+
meta: value.meta,
|
|
39
|
+
createdAt: value.createdAt,
|
|
40
|
+
updatedAt: value.updatedAt,
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
return entries
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Get direct children of a parent, sorted by order. */
|
|
47
|
+
export function childrenOf(entries: TreeEntry[], parentId: string | null): TreeEntry[] {
|
|
48
|
+
return entries
|
|
49
|
+
.filter(e => e.parentId === parentId)
|
|
50
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Get all descendants of an entry. */
|
|
54
|
+
export function descendantsOf(entries: TreeEntry[], id: string | null): TreeEntry[] {
|
|
55
|
+
const result: TreeEntry[] = []
|
|
56
|
+
const visited = new Set<string>()
|
|
57
|
+
function collect(pid: string) {
|
|
58
|
+
if (visited.has(pid)) return
|
|
59
|
+
visited.add(pid)
|
|
60
|
+
for (const child of childrenOf(entries, pid)) {
|
|
61
|
+
result.push(child)
|
|
62
|
+
collect(child.id)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
collect(id!)
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build a nested tree structure for display. */
|
|
70
|
+
export function buildTree(entries: TreeEntry[], rootId: string | null, maxDepth: number, currentDepth = 0, visited = new Set<string>()): TreeDisplayNode[] {
|
|
71
|
+
if (maxDepth >= 0 && currentDepth >= maxDepth) return []
|
|
72
|
+
const children = childrenOf(entries, rootId)
|
|
73
|
+
return children.filter(e => !visited.has(e.id)).map(entry => {
|
|
74
|
+
const next = new Set(visited)
|
|
75
|
+
next.add(entry.id)
|
|
76
|
+
return {
|
|
77
|
+
id: entry.id,
|
|
78
|
+
label: entry.label,
|
|
79
|
+
type: entry.type,
|
|
80
|
+
meta: entry.meta,
|
|
81
|
+
children: buildTree(entries, entry.id, maxDepth, currentDepth + 1, next),
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TreeDisplayNode {
|
|
87
|
+
id: string
|
|
88
|
+
label: string
|
|
89
|
+
type?: string
|
|
90
|
+
meta?: Record<string, unknown>
|
|
91
|
+
children: TreeDisplayNode[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Normalize a document ID so the hub/root doc ID is treated as the tree root (null).
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeRootId(id: string | null | undefined, conn: CLIConnection): string | null {
|
|
98
|
+
if (id == null) return null
|
|
99
|
+
return id === conn.rootDocId ? null : id
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a document from parsed args.
|
|
104
|
+
*
|
|
105
|
+
* Resolution order:
|
|
106
|
+
* 1. id=<uuid> — Direct document ID
|
|
107
|
+
* 2. name=<label> — Case-insensitive label match (first match wins)
|
|
108
|
+
* 3. path=<a/b/c> — Resolve by label path from root
|
|
109
|
+
* 4. First positional argument — tries name resolution
|
|
110
|
+
*
|
|
111
|
+
* Returns the document ID or null if not found.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveDocument(
|
|
114
|
+
conn: CLIConnection,
|
|
115
|
+
params: Record<string, string>,
|
|
116
|
+
positional: string[],
|
|
117
|
+
): string | null {
|
|
118
|
+
const treeMap = conn.getTreeMap()
|
|
119
|
+
if (!treeMap) return null
|
|
120
|
+
|
|
121
|
+
const entries = readEntries(treeMap)
|
|
122
|
+
|
|
123
|
+
// 1. Direct ID
|
|
124
|
+
if (params['id']) {
|
|
125
|
+
const entry = entries.find(e => e.id === params['id'])
|
|
126
|
+
return entry ? entry.id : null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 2. Name resolution (case-insensitive label match)
|
|
130
|
+
const name = params['name'] || params['file'] || positional[0]
|
|
131
|
+
if (name) {
|
|
132
|
+
const lower = name.toLowerCase()
|
|
133
|
+
const match = entries.find(e => e.label.toLowerCase() === lower)
|
|
134
|
+
if (match) return match.id
|
|
135
|
+
|
|
136
|
+
// Substring match as fallback
|
|
137
|
+
const substring = entries.find(e => e.label.toLowerCase().includes(lower))
|
|
138
|
+
if (substring) return substring.id
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 3. Path resolution (slash-separated label chain)
|
|
142
|
+
if (params['path']) {
|
|
143
|
+
const segments = params['path'].split('/').map(s => s.trim()).filter(Boolean)
|
|
144
|
+
let currentParent: string | null = normalizeRootId(conn.rootDocId, conn)
|
|
145
|
+
|
|
146
|
+
for (const segment of segments) {
|
|
147
|
+
const lower = segment.toLowerCase()
|
|
148
|
+
const children = childrenOf(entries, currentParent)
|
|
149
|
+
const match = children.find(c => c.label.toLowerCase() === lower)
|
|
150
|
+
if (!match) return null
|
|
151
|
+
currentParent = match.id
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return currentParent
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Get the ancestor path labels for a document. */
|
|
161
|
+
export function getAncestorPath(entries: TreeEntry[], docId: string): string[] {
|
|
162
|
+
const byId = new Map(entries.map(e => [e.id, e]))
|
|
163
|
+
const path: string[] = []
|
|
164
|
+
let current = byId.get(docId)?.parentId
|
|
165
|
+
const visited = new Set<string>()
|
|
166
|
+
while (current && !visited.has(current)) {
|
|
167
|
+
visited.add(current)
|
|
168
|
+
const parent = byId.get(current)
|
|
169
|
+
if (!parent) break
|
|
170
|
+
path.unshift(parent.label)
|
|
171
|
+
current = parent.parentId
|
|
172
|
+
}
|
|
173
|
+
return path
|
|
174
|
+
}
|