@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/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@abraca/cli",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "CLI for Abracadabra — interact with CRDT document workspaces from the terminal",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/abracadabra-cli.cjs",
|
|
8
|
+
"module": "dist/abracadabra-cli.esm.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"abracadabra": "./dist/abracadabra-cli.esm.js"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
"source": {
|
|
18
|
+
"import": "./src/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"default": {
|
|
21
|
+
"import": "./dist/abracadabra-cli.esm.js",
|
|
22
|
+
"require": "./dist/abracadabra-cli.cjs",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@noble/ed25519": "^2.3.0",
|
|
32
|
+
"@noble/hashes": "^1.8.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@abraca/dabra": ">=1.0.0",
|
|
36
|
+
"y-protocols": "^1.0.6",
|
|
37
|
+
"yjs": "^13.6.8"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command interface and registry.
|
|
3
|
+
*/
|
|
4
|
+
import type { CLIConnection } from './connection.ts'
|
|
5
|
+
import type { ParsedArgs } from './parser.ts'
|
|
6
|
+
|
|
7
|
+
export interface Command {
|
|
8
|
+
name: string
|
|
9
|
+
aliases?: string[]
|
|
10
|
+
description: string
|
|
11
|
+
usage?: string
|
|
12
|
+
run(conn: CLIConnection | null, args: ParsedArgs): Promise<string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const commands: Command[] = []
|
|
16
|
+
|
|
17
|
+
export function registerCommand(cmd: Command): void {
|
|
18
|
+
commands.push(cmd)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getCommand(name: string): Command | null {
|
|
22
|
+
return commands.find(c =>
|
|
23
|
+
c.name === name || c.aliases?.includes(name)
|
|
24
|
+
) ?? null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getAllCommands(): Command[] {
|
|
28
|
+
return [...commands]
|
|
29
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Awareness commands: who, status, chat.
|
|
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 } from '../output.ts'
|
|
8
|
+
|
|
9
|
+
registerCommand({
|
|
10
|
+
name: 'who',
|
|
11
|
+
aliases: ['users', 'online'],
|
|
12
|
+
description: 'List connected users and their awareness state.',
|
|
13
|
+
usage: 'who [id=<docId>] [--format=json|tsv]',
|
|
14
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
15
|
+
if (!conn) return 'Not connected'
|
|
16
|
+
|
|
17
|
+
let awareness
|
|
18
|
+
if (args.params['id']) {
|
|
19
|
+
try {
|
|
20
|
+
const provider = await conn.getChildProvider(args.params['id'])
|
|
21
|
+
awareness = provider.awareness
|
|
22
|
+
} catch (error: any) {
|
|
23
|
+
return `Error: ${error.message}`
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const rootProvider = conn.rootProvider
|
|
27
|
+
if (!rootProvider) return 'Not connected'
|
|
28
|
+
awareness = rootProvider.awareness
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const states = awareness.getStates()
|
|
32
|
+
const selfId = awareness.clientID
|
|
33
|
+
const users: Array<{
|
|
34
|
+
name: string
|
|
35
|
+
status: string
|
|
36
|
+
docId: string
|
|
37
|
+
isYou: boolean
|
|
38
|
+
isAgent: boolean
|
|
39
|
+
}> = []
|
|
40
|
+
|
|
41
|
+
for (const [clientId, state] of states) {
|
|
42
|
+
const user = (state as Record<string, any>)['user']
|
|
43
|
+
if (!user) continue
|
|
44
|
+
users.push({
|
|
45
|
+
name: user.name ?? 'Unknown',
|
|
46
|
+
status: (state as Record<string, any>)['status'] ?? '—',
|
|
47
|
+
docId: (state as Record<string, any>)['docId'] ?? '—',
|
|
48
|
+
isYou: clientId === selfId,
|
|
49
|
+
isAgent: user.isAgent ?? false,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const format = getFormat(args, 'text')
|
|
54
|
+
|
|
55
|
+
if (format === 'json') {
|
|
56
|
+
return printJson(users)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (users.length === 0) {
|
|
60
|
+
return 'No users connected.'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rows = users.map(u => [
|
|
64
|
+
u.name + (u.isYou ? ' (you)' : '') + (u.isAgent ? ' 🤖' : ''),
|
|
65
|
+
u.status,
|
|
66
|
+
u.docId === '—' ? '—' : u.docId.slice(0, 8) + '…',
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
return printTable(rows, ['NAME', 'STATUS', 'DOCUMENT'])
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
registerCommand({
|
|
74
|
+
name: 'status',
|
|
75
|
+
description: 'Set your presence status.',
|
|
76
|
+
usage: 'status <text> | status --clear',
|
|
77
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
78
|
+
if (!conn) return 'Not connected'
|
|
79
|
+
|
|
80
|
+
const rootProvider = conn.rootProvider
|
|
81
|
+
if (!rootProvider) return 'Not connected'
|
|
82
|
+
|
|
83
|
+
if (args.flags.has('clear')) {
|
|
84
|
+
rootProvider.awareness.setLocalStateField('status', null)
|
|
85
|
+
return 'Status cleared.'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const status = args.params['text'] || args.positional[0]
|
|
89
|
+
if (!status) return 'Missing status text. Use: status <text> or status --clear'
|
|
90
|
+
|
|
91
|
+
rootProvider.awareness.setLocalStateField('status', status)
|
|
92
|
+
return `Status set to "${status}"`
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
registerCommand({
|
|
97
|
+
name: 'chat',
|
|
98
|
+
aliases: ['send'],
|
|
99
|
+
description: 'Send a chat message to a channel.',
|
|
100
|
+
usage: 'chat channel=<group:docId> text=<message>',
|
|
101
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
102
|
+
if (!conn) return 'Not connected'
|
|
103
|
+
|
|
104
|
+
const rootProvider = conn.rootProvider
|
|
105
|
+
if (!rootProvider) return 'Not connected'
|
|
106
|
+
|
|
107
|
+
const channel = args.params['channel']
|
|
108
|
+
if (!channel) return 'Missing required parameter: channel=<group:docId>'
|
|
109
|
+
|
|
110
|
+
const text = args.params['text'] || args.params['content'] || args.positional[0]
|
|
111
|
+
if (!text) return 'Missing required parameter: text=<message>'
|
|
112
|
+
|
|
113
|
+
rootProvider.sendStateless(JSON.stringify({
|
|
114
|
+
type: 'chat:send',
|
|
115
|
+
channel,
|
|
116
|
+
content: text,
|
|
117
|
+
sender_name: conn.displayName,
|
|
118
|
+
}))
|
|
119
|
+
|
|
120
|
+
return `Sent to ${channel}`
|
|
121
|
+
},
|
|
122
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content commands: append, prepend, wc, export.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'node:fs'
|
|
5
|
+
import * as path from 'node:path'
|
|
6
|
+
import { registerCommand } from '../command.ts'
|
|
7
|
+
import type { CLIConnection } from '../connection.ts'
|
|
8
|
+
import type { ParsedArgs } from '../parser.ts'
|
|
9
|
+
import { resolveDocument } from '../resolve.ts'
|
|
10
|
+
import { yjsToMarkdown } from '../converters/yjsToMarkdown.ts'
|
|
11
|
+
import { populateYDocFromMarkdown } from '../converters/markdownToYjs.ts'
|
|
12
|
+
|
|
13
|
+
registerCommand({
|
|
14
|
+
name: 'append',
|
|
15
|
+
description: 'Append content to a document.',
|
|
16
|
+
usage: 'append id=<docId> | name=<label> content=<text> [--inline]',
|
|
17
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
18
|
+
if (!conn) return 'Not connected'
|
|
19
|
+
|
|
20
|
+
const docId = resolveDocument(conn, args.params, args.positional)
|
|
21
|
+
if (!docId) return 'Document not found.'
|
|
22
|
+
|
|
23
|
+
const content = args.params['content']
|
|
24
|
+
if (!content) return 'Missing required parameter: content=<text>'
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const provider = await conn.getChildProvider(docId)
|
|
28
|
+
const fragment = provider.document.getXmlFragment('default')
|
|
29
|
+
const text = args.flags.has('inline')
|
|
30
|
+
? content.replace(/\\n/g, '\n').replace(/\\t/g, '\t')
|
|
31
|
+
: '\n' + content.replace(/\\n/g, '\n').replace(/\\t/g, '\t')
|
|
32
|
+
populateYDocFromMarkdown(fragment, text, '')
|
|
33
|
+
|
|
34
|
+
return `Appended to ${docId.slice(0, 8)}…`
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
return `Error: ${error.message}`
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
registerCommand({
|
|
42
|
+
name: 'prepend',
|
|
43
|
+
description: 'Prepend content to a document (after frontmatter).',
|
|
44
|
+
usage: 'prepend id=<docId> | name=<label> content=<text> [--inline]',
|
|
45
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
46
|
+
if (!conn) return 'Not connected'
|
|
47
|
+
|
|
48
|
+
const docId = resolveDocument(conn, args.params, args.positional)
|
|
49
|
+
if (!docId) return 'Document not found.'
|
|
50
|
+
|
|
51
|
+
const content = args.params['content']
|
|
52
|
+
if (!content) return 'Missing required parameter: content=<text>'
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const provider = await conn.getChildProvider(docId)
|
|
56
|
+
const doc = provider.document
|
|
57
|
+
const fragment = doc.getXmlFragment('default')
|
|
58
|
+
|
|
59
|
+
// Read existing, prepend, rewrite
|
|
60
|
+
const { markdown: existing } = yjsToMarkdown(fragment)
|
|
61
|
+
const text = content.replace(/\\n/g, '\n').replace(/\\t/g, '\t')
|
|
62
|
+
const combined = args.flags.has('inline')
|
|
63
|
+
? text + existing
|
|
64
|
+
: text + '\n' + existing
|
|
65
|
+
|
|
66
|
+
doc.transact(() => {
|
|
67
|
+
while (fragment.length > 0) {
|
|
68
|
+
fragment.delete(0)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
populateYDocFromMarkdown(fragment, combined, '')
|
|
72
|
+
|
|
73
|
+
return `Prepended to ${docId.slice(0, 8)}…`
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
return `Error: ${error.message}`
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
registerCommand({
|
|
81
|
+
name: 'wc',
|
|
82
|
+
aliases: ['wordcount'],
|
|
83
|
+
description: 'Count words and characters in a document.',
|
|
84
|
+
usage: 'wc id=<docId> | name=<label> [--words] [--characters]',
|
|
85
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
86
|
+
if (!conn) return 'Not connected'
|
|
87
|
+
|
|
88
|
+
const docId = resolveDocument(conn, args.params, args.positional)
|
|
89
|
+
if (!docId) return 'Document not found.'
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const provider = await conn.getChildProvider(docId)
|
|
93
|
+
const fragment = provider.document.getXmlFragment('default')
|
|
94
|
+
const { markdown } = yjsToMarkdown(fragment)
|
|
95
|
+
|
|
96
|
+
const words = markdown.split(/\s+/).filter(Boolean).length
|
|
97
|
+
const chars = markdown.length
|
|
98
|
+
|
|
99
|
+
if (args.flags.has('words')) return String(words)
|
|
100
|
+
if (args.flags.has('characters')) return String(chars)
|
|
101
|
+
|
|
102
|
+
return `Words: ${words}\nCharacters: ${chars}`
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
return `Error: ${error.message}`
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
registerCommand({
|
|
110
|
+
name: 'export',
|
|
111
|
+
description: 'Export a document as markdown to a local file.',
|
|
112
|
+
usage: 'export id=<docId> | name=<label> output=<filepath>',
|
|
113
|
+
async run(conn: CLIConnection | null, args: ParsedArgs): Promise<string> {
|
|
114
|
+
if (!conn) return 'Not connected'
|
|
115
|
+
|
|
116
|
+
const docId = resolveDocument(conn, args.params, args.positional)
|
|
117
|
+
if (!docId) return 'Document not found.'
|
|
118
|
+
|
|
119
|
+
const outputPath = args.params['output'] || args.params['to'] || args.params['path']
|
|
120
|
+
if (!outputPath) return 'Missing required parameter: output=<filepath>'
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const provider = await conn.getChildProvider(docId)
|
|
124
|
+
const fragment = provider.document.getXmlFragment('default')
|
|
125
|
+
const { title, markdown } = yjsToMarkdown(fragment)
|
|
126
|
+
|
|
127
|
+
const resolvedPath = path.resolve(outputPath)
|
|
128
|
+
fs.writeFileSync(resolvedPath, markdown, 'utf-8')
|
|
129
|
+
|
|
130
|
+
return `Exported "${title || 'document'}" to ${resolvedPath} (${Buffer.byteLength(markdown)} bytes)`
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
return `Error: ${error.message}`
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
})
|