@corvio/cli 0.1.0-beta.3 → 0.1.0-beta.5
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/README.md +9 -2
- package/dist/cli.js +982 -35
- package/dist/core.js +475 -61
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { lstat, mkdir, readFile, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
|
|
3
8
|
import {
|
|
4
9
|
ApiClient,
|
|
5
10
|
CliError,
|
|
6
11
|
EXIT,
|
|
12
|
+
assertKnownOptions,
|
|
13
|
+
assertPositionalCount,
|
|
14
|
+
clearWorkspaceContext,
|
|
7
15
|
clearWorkspaceCredential,
|
|
8
16
|
idempotencyKey,
|
|
9
17
|
listOption,
|
|
@@ -12,44 +20,214 @@ import {
|
|
|
12
20
|
openBrowser,
|
|
13
21
|
option,
|
|
14
22
|
readTextFile,
|
|
23
|
+
readWorkspaceContext,
|
|
15
24
|
readWorkspaceCredential,
|
|
16
25
|
requireConfirmation,
|
|
26
|
+
resolveNpmUpdateStatus,
|
|
17
27
|
runMain,
|
|
18
28
|
sleep,
|
|
29
|
+
storeWorkspaceContext,
|
|
19
30
|
storeWorkspaceCredential,
|
|
20
31
|
writeResult,
|
|
32
|
+
writeBinaryFile,
|
|
21
33
|
writeTextFile,
|
|
22
34
|
} from "./core.js";
|
|
23
35
|
|
|
24
|
-
const VERSION = "0.1.0-beta.
|
|
36
|
+
const VERSION = "0.1.0-beta.5";
|
|
25
37
|
const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
|
|
26
38
|
const DEFAULT_APP_BASE = "https://corvio.ai";
|
|
27
39
|
const USER_AGENT = `corvio-cli/${VERSION}`;
|
|
40
|
+
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
41
|
+
const MAX_SYNC_FILES = 2_000;
|
|
42
|
+
const MAX_MARKDOWN_CHARACTERS = 120_000;
|
|
43
|
+
const GLOBAL_OPTIONS = Object.freeze([
|
|
44
|
+
"api-base",
|
|
45
|
+
"workspace",
|
|
46
|
+
"workspace-id",
|
|
47
|
+
"json",
|
|
48
|
+
"no-input",
|
|
49
|
+
"yes",
|
|
50
|
+
"help",
|
|
51
|
+
"version",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const COMMAND_OPTIONS = Object.freeze({
|
|
55
|
+
"auth:login": ["force"],
|
|
56
|
+
"auth:status": [],
|
|
57
|
+
"auth:logout": [],
|
|
58
|
+
"workspaces:list": [],
|
|
59
|
+
"workspaces:current": [],
|
|
60
|
+
"workspaces:use": ["id"],
|
|
61
|
+
"workspaces:create": ["name", "kind", "description", "idempotency-key"],
|
|
62
|
+
"capabilities:": [],
|
|
63
|
+
"ask:": ["prompt", "conversation-id", "sources", "allow-actions", "idempotency-key"],
|
|
64
|
+
"questions:list": ["limit", "cursor", "conversation-id"],
|
|
65
|
+
"questions:get": ["id"],
|
|
66
|
+
"conversations:list": ["limit", "cursor"],
|
|
67
|
+
"conversations:get": ["id"],
|
|
68
|
+
"search:": ["query", "limit", "page-id", "node-id"],
|
|
69
|
+
"docs:list": ["limit", "cursor"],
|
|
70
|
+
"docs:get": ["id", "output"],
|
|
71
|
+
"docs:create": ["title", "file", "markdown", "parent-node-id", "idempotency-key"],
|
|
72
|
+
"docs:update": ["id", "title", "file", "markdown", "expected-revision"],
|
|
73
|
+
"docs:move": ["id", "parent-node-id", "before-node-id", "after-node-id", "idempotency-key"],
|
|
74
|
+
"docs:archive": ["id"],
|
|
75
|
+
"docs:share": ["id", "display-name"],
|
|
76
|
+
"docs:open": ["id"],
|
|
77
|
+
"projects:create": ["title", "idempotency-key"],
|
|
78
|
+
"files:list": ["limit"],
|
|
79
|
+
"files:get": ["id"],
|
|
80
|
+
"files:download": ["id", "output"],
|
|
81
|
+
"files:upload": ["file", "mime-type"],
|
|
82
|
+
"files:organize": ["id", "instruction", "target-root-node-id"],
|
|
83
|
+
"files:open": ["id"],
|
|
84
|
+
"sync:init": ["root-node-id", "dir", "name", "force"],
|
|
85
|
+
"sync:status": ["dir"],
|
|
86
|
+
"sync:plan": ["dir"],
|
|
87
|
+
"sync:pull": ["dir"],
|
|
88
|
+
"sync:push": ["dir"],
|
|
89
|
+
"sync:resolve": ["dir", "conflict-id", "strategy"],
|
|
90
|
+
"update:check": [],
|
|
91
|
+
});
|
|
28
92
|
|
|
29
93
|
const HELP = `Corvio Workspace CLI ${VERSION}
|
|
30
94
|
|
|
31
95
|
Usage:
|
|
32
96
|
corvio auth login|status|logout
|
|
97
|
+
corvio workspaces list|use|current|create [id]
|
|
33
98
|
corvio capabilities
|
|
34
99
|
corvio ask --prompt <text> [--conversation-id <id>] [--allow-actions]
|
|
35
100
|
corvio questions list|get [id]
|
|
36
101
|
corvio conversations list|get [id]
|
|
37
102
|
corvio search <query>
|
|
38
|
-
corvio docs list|get|create|update|archive|share|open [id]
|
|
103
|
+
corvio docs list|get|create|update|move|archive|share|open [id]
|
|
104
|
+
corvio projects create --title <title>
|
|
105
|
+
corvio files list|get|download|upload|organize|open [id]
|
|
106
|
+
corvio sync init|status|plan|pull|push|resolve
|
|
107
|
+
corvio update check
|
|
39
108
|
|
|
40
109
|
Global options:
|
|
41
|
-
--api-base <url> --json --no-input --yes --help --version
|
|
110
|
+
--api-base <url> --workspace <id> --json --no-input --yes --help --version
|
|
42
111
|
|
|
43
112
|
Authentication:
|
|
44
|
-
CORVIO_API_KEY=
|
|
113
|
+
CORVIO_API_KEY=cvu_... takes precedence over the stored device-link login.
|
|
114
|
+
Legacy Workspace-bound cvk_ keys remain supported.
|
|
115
|
+
Select the target with 'corvio workspaces use <id>', --workspace, or CORVIO_WORKSPACE_ID.
|
|
45
116
|
Run 'corvio auth login' when no environment key is configured.
|
|
46
117
|
`;
|
|
47
118
|
|
|
119
|
+
const COMMAND_HELP = Object.freeze({
|
|
120
|
+
auth: `Usage: corvio auth login|status|logout\n\nAuthenticate with a user-level device link, inspect the current principal, or revoke the stored credential.`,
|
|
121
|
+
workspaces: `Usage:\n corvio workspaces list\n corvio workspaces use <workspace-id>\n corvio workspaces current\n corvio workspaces create --name <name> [--kind personal|team]\n\nWorkspace status and blocked capability reasons are returned with each item.`,
|
|
122
|
+
capabilities: `Usage: corvio capabilities [--json]\n\nShow live API resources, principal scopes, selected Workspace status, and CLI compatibility.`,
|
|
123
|
+
ask: `Usage: corvio ask --prompt <text> [--conversation-id <id>] [--sources workspace,web,memory] [--allow-actions] [--idempotency-key <key>]`,
|
|
124
|
+
questions: `Usage:\n corvio questions list [--limit <n>] [--cursor <cursor>] [--conversation-id <id>]\n corvio questions get <question-id>`,
|
|
125
|
+
conversations: `Usage:\n corvio conversations list [--limit <n>] [--cursor <cursor>]\n corvio conversations get <conversation-id>`,
|
|
126
|
+
search: `Usage: corvio search <query> [--limit <n>] [--page-id <id>] [--node-id <id>]`,
|
|
127
|
+
docs: `Usage:\n corvio docs list [--limit <n>] [--cursor <cursor>]\n corvio docs get <id> [--output <path>]\n corvio docs create --title <title> [--file <path>|--markdown <text>] [--parent-node-id <id>] [--idempotency-key <key>]\n corvio docs update <id> [--title <title>] [--file <path>|--markdown <text>] [--expected-revision <n>]\n corvio docs move <id> --parent-node-id <id>\n corvio docs archive <id> --yes\n corvio docs share <id> --yes [--display-name <name>]\n corvio docs open <id>`,
|
|
128
|
+
projects: `Usage: corvio projects create --title <title> [--idempotency-key <key>]\n\nProjects are the user-visible folder/container owner in the Corvio Docs tree. 'corvio folders' is an alias.`,
|
|
129
|
+
files: `Usage:\n corvio files list [--limit <n>]\n corvio files get <id>\n corvio files download <id> [--output <path>]\n corvio files upload --file <path> [--mime-type <type>]\n corvio files organize <id> --instruction <text> --yes [--target-root-node-id <id>]\n corvio files open <id>`,
|
|
130
|
+
sync: `Usage:\n corvio sync init --root-node-id <id> [--dir <path>] [--name <name>]\n corvio sync status|plan|pull [--dir <path>]\n corvio sync push --yes [--dir <path>]\n corvio sync resolve --conflict-id <id> --strategy use-remote|keep-local --yes [--dir <path>]`,
|
|
131
|
+
update: `Usage: corvio update check [--json]\n\nCompare this executable with the reviewed API policy and the npm beta dist-tag.`,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function contextualHelp(command, action) {
|
|
135
|
+
const normalizedCommand = command === "workspace" ? "workspaces" : command === "folders" ? "projects" : command;
|
|
136
|
+
const commandHelp = COMMAND_HELP[normalizedCommand];
|
|
137
|
+
if (!commandHelp) return HELP.trimEnd();
|
|
138
|
+
if (!action) return commandHelp;
|
|
139
|
+
const matchingLine = commandHelp
|
|
140
|
+
.split("\n")
|
|
141
|
+
.find((line) => line.trim().startsWith(`corvio ${normalizedCommand} ${action}`));
|
|
142
|
+
return matchingLine ? `Usage:\n ${matchingLine.trim()}\n\n${commandHelp}` : commandHelp;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeInvocation(command, action) {
|
|
146
|
+
const normalizedCommand = command === "workspace" ? "workspaces" : command === "folders" ? "projects" : command;
|
|
147
|
+
const defaultActions = {
|
|
148
|
+
workspaces: "list",
|
|
149
|
+
questions: "list",
|
|
150
|
+
conversations: "list",
|
|
151
|
+
docs: "list",
|
|
152
|
+
projects: "create",
|
|
153
|
+
files: "list",
|
|
154
|
+
sync: "status",
|
|
155
|
+
update: "check",
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
command: normalizedCommand,
|
|
159
|
+
action: ["ask", "search", "capabilities"].includes(normalizedCommand)
|
|
160
|
+
? ""
|
|
161
|
+
: action || defaultActions[normalizedCommand] || "",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function validateInvocation(options, positionals) {
|
|
166
|
+
const [rawCommand, rawAction] = positionals;
|
|
167
|
+
if (!rawCommand || rawCommand === "help" || rawCommand === "version" || options.help || options.version) return;
|
|
168
|
+
const { command, action } = normalizeInvocation(rawCommand, rawAction);
|
|
169
|
+
const schema = COMMAND_OPTIONS[`${command}:${action}`];
|
|
170
|
+
if (!schema) throw new CliError(`Unknown command: ${positionals.join(" ")}`, { code: "unknown_command" });
|
|
171
|
+
assertKnownOptions(options, [...GLOBAL_OPTIONS, ...schema]);
|
|
172
|
+
if (command !== "ask" && command !== "search") {
|
|
173
|
+
const actionsWithId = new Set([
|
|
174
|
+
"workspaces:use",
|
|
175
|
+
"questions:get",
|
|
176
|
+
"conversations:get",
|
|
177
|
+
"docs:get",
|
|
178
|
+
"docs:update",
|
|
179
|
+
"docs:move",
|
|
180
|
+
"docs:archive",
|
|
181
|
+
"docs:share",
|
|
182
|
+
"docs:open",
|
|
183
|
+
"files:get",
|
|
184
|
+
"files:download",
|
|
185
|
+
"files:organize",
|
|
186
|
+
"files:open",
|
|
187
|
+
]);
|
|
188
|
+
assertPositionalCount(positionals, actionsWithId.has(`${command}:${action}`) ? 3 : command === "capabilities" ? 1 : 2);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function rejectTogether(options, left, right) {
|
|
193
|
+
if (options[left] !== undefined && options[right] !== undefined) {
|
|
194
|
+
throw new CliError(`Options --${left} and --${right} cannot be used together.`, {
|
|
195
|
+
code: "mutually_exclusive_options",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
48
200
|
function baseUrl(options) {
|
|
49
201
|
return normalizeApiBase(options["api-base"] || process.env.CORVIO_API_BASE, DEFAULT_API_BASE);
|
|
50
202
|
}
|
|
51
203
|
|
|
52
|
-
|
|
204
|
+
function resourcePath(value) {
|
|
205
|
+
return encodeURIComponent(String(value));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function safeRemoteFileName(value, fallback) {
|
|
209
|
+
const normalized = String(value || "").replaceAll("\\", "/");
|
|
210
|
+
const name = basename(normalized).trim();
|
|
211
|
+
return name && name !== "." && name !== ".." ? name : fallback;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function resolveWorkspaceSelection(options, credential) {
|
|
215
|
+
const explicit = String(options.workspace || options["workspace-id"] || "").trim();
|
|
216
|
+
if (explicit) return { workspaceId: explicit, workspaceName: null, source: "option" };
|
|
217
|
+
const legacyWorkspaceId = String(credential?.metadata?.workspace_id || "").trim();
|
|
218
|
+
if (legacyWorkspaceId) {
|
|
219
|
+
return {
|
|
220
|
+
workspaceId: legacyWorkspaceId,
|
|
221
|
+
workspaceName: String(credential?.metadata?.workspace_name || "").trim() || null,
|
|
222
|
+
source: "legacy_credential",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const context = await readWorkspaceContext();
|
|
226
|
+
if (context?.workspaceId) return context;
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function client(options, { auth = true, workspace = "required" } = {}) {
|
|
53
231
|
const credential = auth ? await readWorkspaceCredential() : null;
|
|
54
232
|
if (auth && !credential) {
|
|
55
233
|
throw new CliError("No Corvio credential found. Set CORVIO_API_KEY or run `corvio auth login`.", {
|
|
@@ -57,16 +235,31 @@ async function client(options, { auth = true } = {}) {
|
|
|
57
235
|
exitCode: EXIT.AUTH,
|
|
58
236
|
});
|
|
59
237
|
}
|
|
238
|
+
const selection = auth && workspace !== "none"
|
|
239
|
+
? await resolveWorkspaceSelection(options, credential)
|
|
240
|
+
: null;
|
|
241
|
+
if (auth && workspace === "required" && credential?.key?.startsWith("cvu_") && !selection?.workspaceId) {
|
|
242
|
+
throw new CliError(
|
|
243
|
+
"No Corvio Workspace selected. Run `corvio workspaces list` then `corvio workspaces use <id>`.",
|
|
244
|
+
{ code: "workspace_selection_required", exitCode: EXIT.AUTH },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
60
247
|
return {
|
|
61
|
-
api: new ApiClient({
|
|
248
|
+
api: new ApiClient({
|
|
249
|
+
baseUrl: baseUrl(options),
|
|
250
|
+
credential: credential?.key,
|
|
251
|
+
workspaceId: selection?.workspaceId,
|
|
252
|
+
userAgent: USER_AGENT,
|
|
253
|
+
}),
|
|
62
254
|
credential,
|
|
255
|
+
selection,
|
|
63
256
|
};
|
|
64
257
|
}
|
|
65
258
|
|
|
66
259
|
async function authLogin(options) {
|
|
67
260
|
const current = await readWorkspaceCredential();
|
|
68
261
|
if (current?.source === "environment" && !options.force) {
|
|
69
|
-
const
|
|
262
|
+
const api = new ApiClient({ baseUrl: baseUrl(options), credential: current.key, userAgent: USER_AGENT });
|
|
70
263
|
const identity = await api.request("/me");
|
|
71
264
|
writeResult({ authenticated: true, source: "environment", identity }, { json: options.json });
|
|
72
265
|
return;
|
|
@@ -85,6 +278,11 @@ async function authLogin(options) {
|
|
|
85
278
|
"documents:read",
|
|
86
279
|
"documents:write",
|
|
87
280
|
"documents:share",
|
|
281
|
+
"assets:read",
|
|
282
|
+
"assets:write",
|
|
283
|
+
"sync:read",
|
|
284
|
+
"sync:write",
|
|
285
|
+
"workspaces:write",
|
|
88
286
|
],
|
|
89
287
|
},
|
|
90
288
|
});
|
|
@@ -127,7 +325,9 @@ async function authLogin(options) {
|
|
|
127
325
|
authenticated: true,
|
|
128
326
|
source: "device_link",
|
|
129
327
|
profile: stored.profile,
|
|
328
|
+
user: identity.user,
|
|
130
329
|
workspace: identity.workspace,
|
|
330
|
+
workspaces: identity.workspaces || [],
|
|
131
331
|
scopes: identity.api_key?.scopes || [],
|
|
132
332
|
}, { json: options.json });
|
|
133
333
|
}
|
|
@@ -138,7 +338,12 @@ async function authStatus(options) {
|
|
|
138
338
|
writeResult({ authenticated: false, sources: ["CORVIO_API_KEY", "device_link"] }, { json: options.json });
|
|
139
339
|
return;
|
|
140
340
|
}
|
|
141
|
-
const
|
|
341
|
+
const selection = await resolveWorkspaceSelection(options, credential);
|
|
342
|
+
const api = new ApiClient({
|
|
343
|
+
baseUrl: baseUrl(options),
|
|
344
|
+
credential: credential.key,
|
|
345
|
+
userAgent: USER_AGENT,
|
|
346
|
+
});
|
|
142
347
|
const identity = await api.request("/me");
|
|
143
348
|
writeResult({
|
|
144
349
|
authenticated: true,
|
|
@@ -146,6 +351,8 @@ async function authStatus(options) {
|
|
|
146
351
|
profile: credential.profile,
|
|
147
352
|
user: identity.user,
|
|
148
353
|
workspace: identity.workspace,
|
|
354
|
+
workspaces: identity.workspaces || [],
|
|
355
|
+
selection,
|
|
149
356
|
api_key: identity.api_key,
|
|
150
357
|
}, { json: options.json });
|
|
151
358
|
}
|
|
@@ -165,24 +372,104 @@ async function authLogout(options) {
|
|
|
165
372
|
const api = new ApiClient({ baseUrl: baseUrl(options), credential: credential.key, userAgent: USER_AGENT });
|
|
166
373
|
await api.request("/auth/credentials/current:revoke", { method: "POST" });
|
|
167
374
|
await clearWorkspaceCredential();
|
|
375
|
+
await clearWorkspaceContext();
|
|
168
376
|
writeResult({ authenticated: false, revoked: true, profile: credential.profile }, { json: options.json });
|
|
169
377
|
}
|
|
170
378
|
|
|
171
379
|
async function capabilities(options) {
|
|
172
|
-
const { api, credential } = await client(options);
|
|
380
|
+
const { api, credential, selection } = await client(options, { workspace: "optional" });
|
|
173
381
|
const [index, identity] = await Promise.all([api.request("", { auth: false }), api.request("/me")]);
|
|
174
382
|
writeResult({
|
|
175
383
|
cli_version: VERSION,
|
|
176
384
|
auth_source: credential.source,
|
|
385
|
+
workspace_selection: selection,
|
|
177
386
|
api: index,
|
|
178
387
|
principal: identity,
|
|
179
|
-
|
|
180
|
-
files: "Public Workspace Asset upload/organization is not advertised by /v1 yet.",
|
|
181
|
-
sync: "Public Local Sync adapters are not advertised by /v1 yet.",
|
|
182
|
-
},
|
|
388
|
+
update: await buildUpdateStatus(index, { allowRegistryFailure: true }),
|
|
183
389
|
}, { json: options.json });
|
|
184
390
|
}
|
|
185
391
|
|
|
392
|
+
async function workspaces(options, action, id) {
|
|
393
|
+
const credential = await readWorkspaceCredential();
|
|
394
|
+
if (!credential) {
|
|
395
|
+
throw new CliError("No Corvio credential found. Run `corvio auth login`.", {
|
|
396
|
+
code: "auth_required",
|
|
397
|
+
exitCode: EXIT.AUTH,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (action === "list") {
|
|
401
|
+
const api = new ApiClient({ baseUrl: baseUrl(options), credential: credential.key, userAgent: USER_AGENT });
|
|
402
|
+
if (credential.key.startsWith("cvk_")) {
|
|
403
|
+
const identity = await api.request("/me");
|
|
404
|
+
return writeResult({ data: identity.workspace ? [identity.workspace] : [], legacy_bound: true }, { json: options.json });
|
|
405
|
+
}
|
|
406
|
+
return writeResult(await api.request("/workspaces"), { json: options.json });
|
|
407
|
+
}
|
|
408
|
+
const selection = await resolveWorkspaceSelection(options, credential);
|
|
409
|
+
if (action === "current") {
|
|
410
|
+
if (!selection?.workspaceId) {
|
|
411
|
+
if (credential.key.startsWith("cvk_")) {
|
|
412
|
+
const api = new ApiClient({ baseUrl: baseUrl(options), credential: credential.key, userAgent: USER_AGENT });
|
|
413
|
+
const identity = await api.request("/me");
|
|
414
|
+
return writeResult(
|
|
415
|
+
{ selected: Boolean(identity.workspace), workspace: identity.workspace, source: "legacy_credential" },
|
|
416
|
+
{ json: options.json },
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return writeResult({ selected: false, workspace: null }, { json: options.json });
|
|
420
|
+
}
|
|
421
|
+
const api = new ApiClient({
|
|
422
|
+
baseUrl: baseUrl(options),
|
|
423
|
+
credential: credential.key,
|
|
424
|
+
workspaceId: selection.workspaceId,
|
|
425
|
+
userAgent: USER_AGENT,
|
|
426
|
+
});
|
|
427
|
+
const identity = await api.request("/me");
|
|
428
|
+
return writeResult({ selected: true, workspace: identity.workspace, source: selection.source }, { json: options.json });
|
|
429
|
+
}
|
|
430
|
+
if (action === "use") {
|
|
431
|
+
const workspaceId = String(id || options.id || options.workspace || options["workspace-id"] || "").trim();
|
|
432
|
+
if (!workspaceId) throw new CliError("Provide a Workspace ID.", { code: "workspace_selection_required" });
|
|
433
|
+
const api = new ApiClient({
|
|
434
|
+
baseUrl: baseUrl(options),
|
|
435
|
+
credential: credential.key,
|
|
436
|
+
workspaceId,
|
|
437
|
+
userAgent: USER_AGENT,
|
|
438
|
+
});
|
|
439
|
+
const identity = await api.request("/me");
|
|
440
|
+
if (!identity.workspace?.id) {
|
|
441
|
+
throw new CliError("Corvio did not resolve the selected Workspace.", { code: "workspace_selection_failed" });
|
|
442
|
+
}
|
|
443
|
+
await storeWorkspaceContext({ workspaceId: identity.workspace.id, workspaceName: identity.workspace.name });
|
|
444
|
+
return writeResult({ selected: true, workspace: identity.workspace }, { json: options.json });
|
|
445
|
+
}
|
|
446
|
+
if (action === "create") {
|
|
447
|
+
if (!credential.key.startsWith("cvu_")) {
|
|
448
|
+
throw new CliError("Creating a Workspace requires a user-level cvu_ credential.", {
|
|
449
|
+
code: "user_api_key_required",
|
|
450
|
+
exitCode: EXIT.AUTH,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const api = new ApiClient({ baseUrl: baseUrl(options), credential: credential.key, userAgent: USER_AGENT });
|
|
454
|
+
const kind = String(options.kind || "personal");
|
|
455
|
+
if (!new Set(["personal", "team"]).has(kind)) {
|
|
456
|
+
throw new CliError("--kind must be personal or team.", { code: "workspace_kind_invalid" });
|
|
457
|
+
}
|
|
458
|
+
const result = await api.request("/workspaces", {
|
|
459
|
+
method: "POST",
|
|
460
|
+
headers: { "idempotency-key": idempotencyKey(options) },
|
|
461
|
+
body: {
|
|
462
|
+
name: option(options, "name", { required: true }),
|
|
463
|
+
kind,
|
|
464
|
+
description: options.description || null,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
await storeWorkspaceContext({ workspaceId: result.id, workspaceName: result.name });
|
|
468
|
+
return writeResult({ created: true, selected: true, workspace: result }, { json: options.json });
|
|
469
|
+
}
|
|
470
|
+
throw new CliError("workspaces action must be list, use, current, or create.");
|
|
471
|
+
}
|
|
472
|
+
|
|
186
473
|
async function ask(options, positionals) {
|
|
187
474
|
const prompt = String(options.prompt || positionals.join(" ")).trim();
|
|
188
475
|
if (!prompt) throw new CliError("Provide --prompt <text> or a positional prompt.");
|
|
@@ -196,13 +483,14 @@ async function ask(options, positionals) {
|
|
|
196
483
|
mode: options["allow-actions"] ? "allow_actions" : "answer_only",
|
|
197
484
|
sources: listOption(options, "sources", ["workspace", "web", "memory"]),
|
|
198
485
|
},
|
|
486
|
+
timeoutMs: 330_000,
|
|
199
487
|
});
|
|
200
488
|
writeResult(result, { json: options.json });
|
|
201
489
|
}
|
|
202
490
|
|
|
203
491
|
function pageQuery(options) {
|
|
204
492
|
return {
|
|
205
|
-
limit: numberOption(options, "limit", 20),
|
|
493
|
+
limit: numberOption(options, "limit", 20, { maximum: 100 }),
|
|
206
494
|
cursor: options.cursor,
|
|
207
495
|
conversation_id: options["conversation-id"],
|
|
208
496
|
};
|
|
@@ -212,7 +500,7 @@ async function questions(options, action, id) {
|
|
|
212
500
|
const { api } = await client(options);
|
|
213
501
|
const result = action === "list"
|
|
214
502
|
? await api.request("/questions", { query: pageQuery(options) })
|
|
215
|
-
: await api.request(`/questions/${id || option(options, "id", { required: true })}`);
|
|
503
|
+
: await api.request(`/questions/${resourcePath(id || option(options, "id", { required: true }))}`);
|
|
216
504
|
writeResult(result, { json: options.json });
|
|
217
505
|
}
|
|
218
506
|
|
|
@@ -220,7 +508,7 @@ async function conversations(options, action, id) {
|
|
|
220
508
|
const { api } = await client(options);
|
|
221
509
|
const result = action === "list"
|
|
222
510
|
? await api.request("/conversations", { query: pageQuery(options) })
|
|
223
|
-
: await api.request(`/conversations/${id || option(options, "id", { required: true })}`);
|
|
511
|
+
: await api.request(`/conversations/${resourcePath(id || option(options, "id", { required: true }))}`);
|
|
224
512
|
writeResult(result, { json: options.json });
|
|
225
513
|
}
|
|
226
514
|
|
|
@@ -230,9 +518,10 @@ async function search(options, positionals) {
|
|
|
230
518
|
const { api } = await client(options);
|
|
231
519
|
const result = await api.request("/search", {
|
|
232
520
|
method: "POST",
|
|
521
|
+
retrySafe: true,
|
|
233
522
|
body: {
|
|
234
523
|
query,
|
|
235
|
-
limit: numberOption(options, "limit", 10),
|
|
524
|
+
limit: numberOption(options, "limit", 10, { maximum: 50 }),
|
|
236
525
|
page_id: options["page-id"] || null,
|
|
237
526
|
node_id: options["node-id"] || null,
|
|
238
527
|
},
|
|
@@ -247,12 +536,13 @@ async function docs(options, action, id) {
|
|
|
247
536
|
if (action === "list") {
|
|
248
537
|
result = await api.request("/documents", { query: pageQuery(options) });
|
|
249
538
|
} else if (action === "get") {
|
|
250
|
-
result = await api.request(`/documents/${documentId || option(options, "id", { required: true })}`);
|
|
539
|
+
result = await api.request(`/documents/${resourcePath(documentId || option(options, "id", { required: true }))}`);
|
|
251
540
|
if (options.output) {
|
|
252
541
|
await writeTextFile(options.output, String(result.markdown || ""));
|
|
253
542
|
result = { ...result, output: options.output };
|
|
254
543
|
}
|
|
255
544
|
} else if (action === "create") {
|
|
545
|
+
rejectTogether(options, "file", "markdown");
|
|
256
546
|
const markdown = options.file ? await readTextFile(options.file) : String(options.markdown || "");
|
|
257
547
|
result = await api.request("/documents", {
|
|
258
548
|
method: "POST",
|
|
@@ -265,58 +555,715 @@ async function docs(options, action, id) {
|
|
|
265
555
|
},
|
|
266
556
|
});
|
|
267
557
|
} else if (action === "update") {
|
|
558
|
+
rejectTogether(options, "file", "markdown");
|
|
268
559
|
const markdown = options.file ? await readTextFile(options.file) : options.markdown;
|
|
269
|
-
|
|
560
|
+
if (options.title === undefined && markdown === undefined) {
|
|
561
|
+
throw new CliError("Document update requires --title, --file, or --markdown.", {
|
|
562
|
+
code: "document_update_empty",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const targetId = documentId || option(options, "id", { required: true });
|
|
566
|
+
let expectedRevision;
|
|
567
|
+
if (options["expected-revision"] !== undefined) {
|
|
568
|
+
expectedRevision = numberOption(options, "expected-revision", undefined, { minimum: 0 });
|
|
569
|
+
} else {
|
|
570
|
+
const current = await api.request(`/documents/${resourcePath(targetId)}`);
|
|
571
|
+
if (!Number.isSafeInteger(current?.content_revision) || current.content_revision < 0) {
|
|
572
|
+
throw new CliError("Corvio did not return a valid document content revision.", {
|
|
573
|
+
code: "document_revision_missing",
|
|
574
|
+
exitCode: EXIT.REMOTE,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
expectedRevision = current.content_revision;
|
|
578
|
+
}
|
|
579
|
+
result = await api.request(`/documents/${resourcePath(targetId)}`, {
|
|
270
580
|
method: "PUT",
|
|
271
581
|
body: {
|
|
272
|
-
...(options.title ? { title: options.title } : {}),
|
|
582
|
+
...(options.title !== undefined ? { title: options.title } : {}),
|
|
273
583
|
...(markdown !== undefined ? { markdown } : {}),
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
584
|
+
expected_content_revision: expectedRevision,
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
} else if (action === "move") {
|
|
588
|
+
rejectTogether(options, "before-node-id", "after-node-id");
|
|
589
|
+
const current = await api.request(`/documents/${resourcePath(documentId || option(options, "id", { required: true }))}`);
|
|
590
|
+
result = await api.request(`/nodes/${resourcePath(current.node_id)}:move`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "idempotency-key": idempotencyKey(options) },
|
|
593
|
+
body: {
|
|
594
|
+
parent_node_id: option(options, "parent-node-id", { required: true }),
|
|
595
|
+
before_node_id: options["before-node-id"] || null,
|
|
596
|
+
after_node_id: options["after-node-id"] || null,
|
|
277
597
|
},
|
|
278
598
|
});
|
|
279
599
|
} else if (action === "archive") {
|
|
280
600
|
requireConfirmation(options, "Archiving a document");
|
|
281
|
-
result = await api.request(`/documents/${documentId || option(options, "id", { required: true })}`, { method: "DELETE" });
|
|
601
|
+
result = await api.request(`/documents/${resourcePath(documentId || option(options, "id", { required: true }))}`, { method: "DELETE" });
|
|
282
602
|
} else if (action === "share") {
|
|
283
603
|
requireConfirmation(options, "Creating a document share link");
|
|
284
|
-
result = await api.request(`/documents/${documentId || option(options, "id", { required: true })}/shares`, {
|
|
604
|
+
result = await api.request(`/documents/${resourcePath(documentId || option(options, "id", { required: true }))}/shares`, {
|
|
285
605
|
method: "POST",
|
|
606
|
+
retrySafe: true,
|
|
286
607
|
body: { public_display_name: options["display-name"] || null },
|
|
287
608
|
});
|
|
288
609
|
} else if (action === "open") {
|
|
289
|
-
const
|
|
610
|
+
const targetId = documentId || option(options, "id", { required: true });
|
|
611
|
+
const target = `${String(process.env.CORVIO_APP_BASE || DEFAULT_APP_BASE).replace(/\/+$/, "")}/app/d/${resourcePath(targetId)}`;
|
|
290
612
|
const opened = options["no-input"] ? false : await openBrowser(target);
|
|
291
|
-
result = { document_id:
|
|
613
|
+
result = { document_id: targetId, url: target, opened };
|
|
614
|
+
} else {
|
|
615
|
+
throw new CliError("docs action must be list, get, create, update, move, archive, share, or open.");
|
|
616
|
+
}
|
|
617
|
+
writeResult(result, { json: options.json });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function projects(options, action) {
|
|
621
|
+
if (action !== "create") {
|
|
622
|
+
throw new CliError("projects action must be create.");
|
|
623
|
+
}
|
|
624
|
+
const { api } = await client(options);
|
|
625
|
+
const result = await api.request("/projects", {
|
|
626
|
+
method: "POST",
|
|
627
|
+
headers: { "idempotency-key": idempotencyKey(options) },
|
|
628
|
+
body: { title: option(options, "title", { required: true }) },
|
|
629
|
+
});
|
|
630
|
+
writeResult(result, { json: options.json });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function buildUpdateStatus(index, { allowRegistryFailure = false } = {}) {
|
|
634
|
+
return resolveNpmUpdateStatus({
|
|
635
|
+
currentVersion: VERSION,
|
|
636
|
+
policy: index?.distribution?.workspace_cli || {
|
|
637
|
+
package: "@corvio/cli",
|
|
638
|
+
dist_tag: "beta",
|
|
639
|
+
},
|
|
640
|
+
userAgent: USER_AGENT,
|
|
641
|
+
allowRegistryFailure,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function updateCachePath() {
|
|
646
|
+
const root = process.env.CORVIO_CONFIG_DIR
|
|
647
|
+
? resolve(process.env.CORVIO_CONFIG_DIR)
|
|
648
|
+
: process.env.XDG_CACHE_HOME
|
|
649
|
+
? join(resolve(process.env.XDG_CACHE_HOME), "corvio")
|
|
650
|
+
: join(homedir(), ".cache", "corvio");
|
|
651
|
+
return join(root, "update-check.json");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function readUpdateCache() {
|
|
655
|
+
try {
|
|
656
|
+
return JSON.parse(await readFile(updateCachePath(), "utf8"));
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function writeUpdateCache(payload) {
|
|
663
|
+
const target = updateCachePath();
|
|
664
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
665
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
666
|
+
await writeFile(temporary, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
667
|
+
await rename(temporary, target);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function updateCheck(options) {
|
|
671
|
+
const { api } = await client(options, { auth: false });
|
|
672
|
+
const index = await api.request("", { auth: false });
|
|
673
|
+
const result = await buildUpdateStatus(index);
|
|
674
|
+
await writeUpdateCache({ checked_at: new Date().toISOString(), result }).catch(() => {});
|
|
675
|
+
writeResult(result, { json: options.json });
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function maybeNotifyUpdate(options) {
|
|
679
|
+
if (
|
|
680
|
+
options.json
|
|
681
|
+
|| options["no-input"]
|
|
682
|
+
|| process.env.CI
|
|
683
|
+
|| process.env.CORVIO_DISABLE_UPDATE_CHECK === "1"
|
|
684
|
+
) return;
|
|
685
|
+
const cache = await readUpdateCache();
|
|
686
|
+
const checkedAt = Date.parse(String(cache?.checked_at || ""));
|
|
687
|
+
let result = cache?.result;
|
|
688
|
+
if (!Number.isFinite(checkedAt) || Date.now() - checkedAt > 24 * 60 * 60 * 1000) {
|
|
689
|
+
try {
|
|
690
|
+
const { api } = await client(options, { auth: false });
|
|
691
|
+
const index = await api.request("", { auth: false });
|
|
692
|
+
result = await buildUpdateStatus(index, { allowRegistryFailure: true });
|
|
693
|
+
await writeUpdateCache({ checked_at: new Date().toISOString(), result }).catch(() => {});
|
|
694
|
+
} catch {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (result?.update_available) {
|
|
699
|
+
process.stderr.write(
|
|
700
|
+
`Update available: @corvio/cli ${result.current_version} → ${result.latest_published_version}. `
|
|
701
|
+
+ `Run: ${result.install_command}\n`,
|
|
702
|
+
);
|
|
703
|
+
} else if (result?.supported === false) {
|
|
704
|
+
process.stderr.write(
|
|
705
|
+
`This Corvio CLI (${VERSION}) is no longer supported. Run: ${result.install_command}\n`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function mimeTypeFor(path) {
|
|
711
|
+
const extension = extname(String(path)).toLowerCase();
|
|
712
|
+
return ({
|
|
713
|
+
".md": "text/markdown",
|
|
714
|
+
".txt": "text/plain",
|
|
715
|
+
".csv": "text/csv",
|
|
716
|
+
".json": "application/json",
|
|
717
|
+
".pdf": "application/pdf",
|
|
718
|
+
".png": "image/png",
|
|
719
|
+
".jpg": "image/jpeg",
|
|
720
|
+
".jpeg": "image/jpeg",
|
|
721
|
+
".gif": "image/gif",
|
|
722
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
723
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
724
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
725
|
+
})[extension] || "application/octet-stream";
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function files(options, action, id) {
|
|
729
|
+
const { api } = await client(options);
|
|
730
|
+
const assetId = id || options.id;
|
|
731
|
+
let result;
|
|
732
|
+
if (action === "list") {
|
|
733
|
+
result = await api.request("/files", { query: { limit: numberOption(options, "limit", 50, { maximum: 100 }) } });
|
|
734
|
+
} else if (action === "get") {
|
|
735
|
+
result = await api.request(`/files/${resourcePath(assetId || option(options, "id", { required: true }))}`);
|
|
736
|
+
} else if (action === "download") {
|
|
737
|
+
const targetId = assetId || option(options, "id", { required: true });
|
|
738
|
+
const metadata = await api.request(`/files/${resourcePath(targetId)}`);
|
|
739
|
+
const bytes = await api.requestBytes(`/files/${resourcePath(targetId)}/content`);
|
|
740
|
+
if (metadata.content_sha256 && sha256(bytes) !== metadata.content_sha256) {
|
|
741
|
+
throw new CliError("Downloaded file content did not match Corvio's SHA-256 receipt.", {
|
|
742
|
+
code: "download_integrity_mismatch",
|
|
743
|
+
exitCode: EXIT.REMOTE,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const output = options.output || safeRemoteFileName(metadata.original_file_name, `${targetId}.bin`);
|
|
747
|
+
const written = await writeBinaryFile(output, bytes);
|
|
748
|
+
result = { ...metadata, output: written };
|
|
749
|
+
} else if (action === "upload") {
|
|
750
|
+
const input = resolve(String(option(options, "file", { required: true })));
|
|
751
|
+
const info = await stat(input);
|
|
752
|
+
if (!info.isFile()) throw new CliError("--file must point to a regular file.", { code: "upload_file_invalid" });
|
|
753
|
+
if (info.size < 1 || info.size > MAX_UPLOAD_BYTES) {
|
|
754
|
+
throw new CliError(`--file must be between 1 byte and ${MAX_UPLOAD_BYTES} bytes.`, {
|
|
755
|
+
code: "upload_file_size_invalid",
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
const bytes = await readFile(input);
|
|
759
|
+
if (bytes.length < 1 || bytes.length > MAX_UPLOAD_BYTES) {
|
|
760
|
+
throw new CliError(`--file must be between 1 byte and ${MAX_UPLOAD_BYTES} bytes.`, {
|
|
761
|
+
code: "upload_file_size_invalid",
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const fileName = basename(input);
|
|
765
|
+
const mimeType = String(options["mime-type"] || mimeTypeFor(input));
|
|
766
|
+
const initialized = await api.request("/files/uploads", {
|
|
767
|
+
method: "POST",
|
|
768
|
+
body: { file_name: fileName, mime_type: mimeType, size_bytes: bytes.length },
|
|
769
|
+
});
|
|
770
|
+
await api.putUrl(initialized.upload_url, bytes, {
|
|
771
|
+
headers: initialized.upload_headers || {},
|
|
772
|
+
timeoutMs: 5 * 60_000,
|
|
773
|
+
});
|
|
774
|
+
result = await api.request("/files/uploads:finalize", {
|
|
775
|
+
method: "POST",
|
|
776
|
+
retrySafe: true,
|
|
777
|
+
body: {
|
|
778
|
+
upload_id: initialized.upload_id,
|
|
779
|
+
blob_name: initialized.blob_name,
|
|
780
|
+
file_name: fileName,
|
|
781
|
+
mime_type: mimeType,
|
|
782
|
+
size_bytes: bytes.length,
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
if (result.content_sha256 && result.content_sha256 !== sha256(bytes)) {
|
|
786
|
+
throw new CliError("Uploaded file content did not match Corvio's SHA-256 receipt.", {
|
|
787
|
+
code: "upload_integrity_mismatch",
|
|
788
|
+
exitCode: EXIT.REMOTE,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
} else if (action === "organize") {
|
|
792
|
+
requireConfirmation(options, "Starting a file organization run");
|
|
793
|
+
result = await api.request(`/files/${resourcePath(assetId || option(options, "id", { required: true }))}:organize`, {
|
|
794
|
+
method: "POST",
|
|
795
|
+
body: {
|
|
796
|
+
instruction: option(options, "instruction", { required: true }),
|
|
797
|
+
target_root_node_id: options["target-root-node-id"] || null,
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
} else if (action === "open") {
|
|
801
|
+
const targetId = assetId || option(options, "id", { required: true });
|
|
802
|
+
const target = `${String(process.env.CORVIO_APP_BASE || DEFAULT_APP_BASE).replace(/\/+$/, "")}/app/a/${resourcePath(targetId)}`;
|
|
803
|
+
result = { file_id: targetId, url: target, opened: options["no-input"] ? false : await openBrowser(target) };
|
|
292
804
|
} else {
|
|
293
|
-
throw new CliError("
|
|
805
|
+
throw new CliError("files action must be list, get, download, upload, organize, or open.");
|
|
294
806
|
}
|
|
295
807
|
writeResult(result, { json: options.json });
|
|
296
808
|
}
|
|
297
809
|
|
|
810
|
+
function sha256(content) {
|
|
811
|
+
return createHash("sha256").update(content).digest("hex");
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function syncRoot(options) {
|
|
815
|
+
return resolve(String(options.dir || process.cwd()));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function syncStatePath(root) {
|
|
819
|
+
return join(root, ".corvio", "state.json");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function readSyncState(options) {
|
|
823
|
+
const root = syncRoot(options);
|
|
824
|
+
let state;
|
|
825
|
+
try {
|
|
826
|
+
state = JSON.parse(await readFile(syncStatePath(root), "utf8"));
|
|
827
|
+
} catch (cause) {
|
|
828
|
+
if (cause?.code === "ENOENT") {
|
|
829
|
+
throw new CliError("This directory is not initialized. Run `corvio sync init --root-node-id <id>`.", {
|
|
830
|
+
code: "sync_not_initialized",
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
throw new CliError(".corvio/state.json is invalid.", { code: "sync_state_invalid" });
|
|
834
|
+
}
|
|
835
|
+
if (state.version !== 1 || !state.source_id) {
|
|
836
|
+
throw new CliError(".corvio/state.json has an unsupported format.", { code: "sync_state_invalid" });
|
|
837
|
+
}
|
|
838
|
+
const requestedBase = baseUrl(options);
|
|
839
|
+
if (state.api_base && state.api_base !== requestedBase) {
|
|
840
|
+
throw new CliError(
|
|
841
|
+
`This directory belongs to ${state.api_base}; use the matching --api-base or reinitialize it.`,
|
|
842
|
+
{ code: "sync_api_base_mismatch" },
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
return { root, state };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function writeSyncState(root, state) {
|
|
849
|
+
const target = syncStatePath(root);
|
|
850
|
+
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
|
|
851
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
852
|
+
await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
853
|
+
await rename(temporary, target);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function localSyncPath(root, relativePath) {
|
|
857
|
+
if (String(relativePath).includes("\0")) {
|
|
858
|
+
throw new CliError("Unsafe sync path returned by Corvio.", { code: "sync_path_unsafe" });
|
|
859
|
+
}
|
|
860
|
+
const parts = String(relativePath).replaceAll("\\", "/").split("/").filter(Boolean);
|
|
861
|
+
if (parts[0] !== "corvio_docs" || parts.some((part) => part === "." || part === "..")) {
|
|
862
|
+
throw new CliError(`Unsafe sync path returned by Corvio: ${relativePath}`, { code: "sync_path_unsafe" });
|
|
863
|
+
}
|
|
864
|
+
const target = resolve(root, ...parts);
|
|
865
|
+
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
866
|
+
throw new CliError(`Unsafe sync path returned by Corvio: ${relativePath}`, { code: "sync_path_unsafe" });
|
|
867
|
+
}
|
|
868
|
+
return target;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function assertNoSyncSymlink(root, target) {
|
|
872
|
+
const rootInfo = await lstat(root);
|
|
873
|
+
if (rootInfo.isSymbolicLink()) {
|
|
874
|
+
throw new CliError(`Sync root must not be a symbolic link: ${root}`, {
|
|
875
|
+
code: "sync_symlink_unsafe",
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
const relativeTarget = relative(root, target);
|
|
879
|
+
if (relativeTarget === "" || relativeTarget.startsWith(`..${sep}`) || relativeTarget === "..") {
|
|
880
|
+
throw new CliError(`Unsafe local sync target: ${target}`, { code: "sync_path_unsafe" });
|
|
881
|
+
}
|
|
882
|
+
let current = root;
|
|
883
|
+
for (const part of relativeTarget.split(sep).filter(Boolean)) {
|
|
884
|
+
current = join(current, part);
|
|
885
|
+
try {
|
|
886
|
+
const info = await lstat(current);
|
|
887
|
+
if (info.isSymbolicLink()) {
|
|
888
|
+
throw new CliError(`Sync path crosses a symbolic link: ${current}`, {
|
|
889
|
+
code: "sync_symlink_unsafe",
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (error?.code === "ENOENT") return;
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function verifiedRemoteMarkdown(item) {
|
|
900
|
+
const markdown = String(item?.markdown || "");
|
|
901
|
+
if (markdown.length > MAX_MARKDOWN_CHARACTERS) {
|
|
902
|
+
throw new CliError("Remote Markdown exceeds the supported sync size.", {
|
|
903
|
+
code: "sync_markdown_too_large",
|
|
904
|
+
exitCode: EXIT.REMOTE,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
const contentHash = sha256(Buffer.from(markdown, "utf8"));
|
|
908
|
+
if (contentHash !== item?.content_sha256) {
|
|
909
|
+
throw new CliError("Remote Markdown did not match Corvio's SHA-256 receipt.", {
|
|
910
|
+
code: "sync_integrity_mismatch",
|
|
911
|
+
exitCode: EXIT.REMOTE,
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
return { markdown, contentHash };
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function scanMarkdown(root) {
|
|
918
|
+
const mirror = join(root, "corvio_docs");
|
|
919
|
+
await mkdir(mirror, { recursive: true });
|
|
920
|
+
await assertNoSyncSymlink(root, mirror);
|
|
921
|
+
const manifest = [];
|
|
922
|
+
async function walk(directory) {
|
|
923
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
924
|
+
const path = join(directory, entry.name);
|
|
925
|
+
if (entry.isSymbolicLink()) continue;
|
|
926
|
+
if (entry.isDirectory()) {
|
|
927
|
+
await walk(path);
|
|
928
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
929
|
+
const bytes = await readFile(path);
|
|
930
|
+
const info = await lstat(path);
|
|
931
|
+
manifest.push({
|
|
932
|
+
relative_path: relative(root, path).split(sep).join("/"),
|
|
933
|
+
content_sha256: sha256(bytes),
|
|
934
|
+
size_bytes: bytes.length,
|
|
935
|
+
mtime_ms: Math.trunc(info.mtimeMs),
|
|
936
|
+
});
|
|
937
|
+
if (manifest.length > MAX_SYNC_FILES) {
|
|
938
|
+
throw new CliError(`Sync supports at most ${MAX_SYNC_FILES} Markdown files per source.`, {
|
|
939
|
+
code: "sync_file_limit_exceeded",
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
await walk(mirror);
|
|
946
|
+
manifest.sort((left, right) => left.relative_path.localeCompare(right.relative_path));
|
|
947
|
+
return manifest;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function syncManifest(root, state) {
|
|
951
|
+
const manifest = await scanMarkdown(root);
|
|
952
|
+
const mappings = Object.entries(state.mappings || {}).map(([pageId, mapping]) => ({ pageId, ...mapping }));
|
|
953
|
+
const pageByPath = new Map(mappings.map((mapping) => [mapping.relative_path, mapping.pageId]));
|
|
954
|
+
const presentPaths = new Set(manifest.map((item) => item.relative_path));
|
|
955
|
+
for (const item of manifest) {
|
|
956
|
+
const pageId = pageByPath.get(item.relative_path);
|
|
957
|
+
if (pageId) item.page_id = pageId;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Treat a local move as the same Page only when the last-common hash gives
|
|
961
|
+
// one unambiguous missing-old-path -> new-path match. Duplicate content
|
|
962
|
+
// stays unbound and the server reports separate missing/new actions.
|
|
963
|
+
const unboundByHash = new Map();
|
|
964
|
+
for (const item of manifest.filter((candidate) => !candidate.page_id)) {
|
|
965
|
+
const values = unboundByHash.get(item.content_sha256) || [];
|
|
966
|
+
values.push(item);
|
|
967
|
+
unboundByHash.set(item.content_sha256, values);
|
|
968
|
+
}
|
|
969
|
+
const missingByHash = new Map();
|
|
970
|
+
for (const mapping of mappings.filter((candidate) => !presentPaths.has(candidate.relative_path))) {
|
|
971
|
+
const values = missingByHash.get(mapping.content_sha256) || [];
|
|
972
|
+
values.push(mapping);
|
|
973
|
+
missingByHash.set(mapping.content_sha256, values);
|
|
974
|
+
}
|
|
975
|
+
for (const [hash, items] of unboundByHash.entries()) {
|
|
976
|
+
const candidates = missingByHash.get(hash) || [];
|
|
977
|
+
if (items.length === 1 && candidates.length === 1) items[0].page_id = candidates[0].pageId;
|
|
978
|
+
}
|
|
979
|
+
return manifest;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async function syncPlan(api, state, root) {
|
|
983
|
+
return api.request("/sync/plan", {
|
|
984
|
+
method: "POST",
|
|
985
|
+
retrySafe: true,
|
|
986
|
+
body: { source_id: state.source_id, local: await syncManifest(root, state) },
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function syncCommand(options, action) {
|
|
991
|
+
const { api, selection } = await client(options);
|
|
992
|
+
if (action === "init") {
|
|
993
|
+
const root = syncRoot(options);
|
|
994
|
+
await mkdir(root, { recursive: true });
|
|
995
|
+
if (!options.force) {
|
|
996
|
+
try {
|
|
997
|
+
await readFile(syncStatePath(root), "utf8");
|
|
998
|
+
throw new CliError("This directory is already initialized; pass --force to bind it again.", {
|
|
999
|
+
code: "sync_already_initialized",
|
|
1000
|
+
});
|
|
1001
|
+
} catch (cause) {
|
|
1002
|
+
if (cause instanceof CliError) throw cause;
|
|
1003
|
+
if (cause?.code !== "ENOENT") throw cause;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const clientId = `corvio-cli-${randomUUID()}`;
|
|
1007
|
+
const source = await api.request("/sync/sources", {
|
|
1008
|
+
method: "POST",
|
|
1009
|
+
body: {
|
|
1010
|
+
root_node_id: option(options, "root-node-id", { required: true }),
|
|
1011
|
+
folder_name: String(options.name || basename(root)),
|
|
1012
|
+
client_id: clientId,
|
|
1013
|
+
},
|
|
1014
|
+
});
|
|
1015
|
+
const state = {
|
|
1016
|
+
version: 1,
|
|
1017
|
+
source_id: source.id,
|
|
1018
|
+
root_node_id: source.root_node_id,
|
|
1019
|
+
workspace_id: source.workspace_id || selection?.workspaceId || null,
|
|
1020
|
+
client_id: clientId,
|
|
1021
|
+
api_base: baseUrl(options),
|
|
1022
|
+
mirror_root_dir: source.mirror_root_dir || "corvio_docs",
|
|
1023
|
+
mappings: {},
|
|
1024
|
+
initialized_at: new Date().toISOString(),
|
|
1025
|
+
};
|
|
1026
|
+
await mkdir(join(root, state.mirror_root_dir), { recursive: true });
|
|
1027
|
+
await writeSyncState(root, state);
|
|
1028
|
+
return writeResult({ source, directory: root, state_file: syncStatePath(root) }, { json: options.json });
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const { root, state } = await readSyncState(options);
|
|
1032
|
+
if (state.workspace_id && selection?.workspaceId && state.workspace_id !== selection.workspaceId) {
|
|
1033
|
+
throw new CliError(
|
|
1034
|
+
`This sync directory belongs to Workspace ${state.workspace_id}; select it before syncing.`,
|
|
1035
|
+
{ code: "sync_workspace_mismatch" },
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
if (action === "status") {
|
|
1039
|
+
const remote = await api.request(`/sync/sources/${resourcePath(state.source_id)}`);
|
|
1040
|
+
return writeResult({ ...remote, directory: root, state_file: syncStatePath(root) }, { json: options.json });
|
|
1041
|
+
}
|
|
1042
|
+
if (action === "plan") {
|
|
1043
|
+
return writeResult(await syncPlan(api, state, root), { json: options.json });
|
|
1044
|
+
}
|
|
1045
|
+
if (action === "pull") {
|
|
1046
|
+
const plan = await syncPlan(api, state, root);
|
|
1047
|
+
const conflicts = plan.actions.filter((item) => item.action === "conflict");
|
|
1048
|
+
if (conflicts.length) {
|
|
1049
|
+
throw new CliError("Sync has conflicts. Run `corvio sync plan --json` and resolve each conflict.", {
|
|
1050
|
+
code: "sync_conflict",
|
|
1051
|
+
exitCode: EXIT.CONFLICT,
|
|
1052
|
+
details: { conflicts },
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const committed = [];
|
|
1056
|
+
const mappings = { ...(state.mappings || {}) };
|
|
1057
|
+
for (const actionItem of plan.actions.filter((item) => item.action === "pull")) {
|
|
1058
|
+
const item = actionItem.remote;
|
|
1059
|
+
if (!item) continue;
|
|
1060
|
+
const target = localSyncPath(root, item.relative_path);
|
|
1061
|
+
await assertNoSyncSymlink(root, target);
|
|
1062
|
+
const verified = verifiedRemoteMarkdown(item);
|
|
1063
|
+
const previous = mappings[item.page_id];
|
|
1064
|
+
if (previous?.relative_path && previous.relative_path !== item.relative_path) {
|
|
1065
|
+
const oldTarget = localSyncPath(root, previous.relative_path);
|
|
1066
|
+
await assertNoSyncSymlink(root, oldTarget);
|
|
1067
|
+
try {
|
|
1068
|
+
const oldBytes = await readFile(oldTarget);
|
|
1069
|
+
if (sha256(oldBytes) === previous.content_sha256) await unlink(oldTarget);
|
|
1070
|
+
} catch (cause) {
|
|
1071
|
+
if (cause?.code !== "ENOENT") throw cause;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
await writeTextFile(target, verified.markdown);
|
|
1075
|
+
const info = await stat(target);
|
|
1076
|
+
committed.push({
|
|
1077
|
+
page_id: item.page_id,
|
|
1078
|
+
node_id: item.node_id,
|
|
1079
|
+
relative_path: item.relative_path,
|
|
1080
|
+
content_sha256: verified.contentHash,
|
|
1081
|
+
mtime_ms: Math.trunc(info.mtimeMs),
|
|
1082
|
+
});
|
|
1083
|
+
mappings[item.page_id] = {
|
|
1084
|
+
node_id: item.node_id,
|
|
1085
|
+
relative_path: item.relative_path,
|
|
1086
|
+
content_sha256: item.content_sha256,
|
|
1087
|
+
content_revision: item.content_revision,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
if (committed.length) {
|
|
1091
|
+
await api.request("/sync/commit", {
|
|
1092
|
+
method: "POST",
|
|
1093
|
+
body: { source_id: state.source_id, items: committed },
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
state.mappings = mappings;
|
|
1097
|
+
state.last_pull_at = new Date().toISOString();
|
|
1098
|
+
await writeSyncState(root, state);
|
|
1099
|
+
return writeResult({
|
|
1100
|
+
pulled: committed.length,
|
|
1101
|
+
unresolved_local_missing: plan.actions.filter((item) => item.action === "local_missing"),
|
|
1102
|
+
directory: root,
|
|
1103
|
+
}, { json: options.json });
|
|
1104
|
+
}
|
|
1105
|
+
if (action === "push") {
|
|
1106
|
+
requireConfirmation(options, "Pushing local Markdown changes to Corvio");
|
|
1107
|
+
const plan = await syncPlan(api, state, root);
|
|
1108
|
+
const conflicts = plan.actions.filter((item) => item.action === "conflict");
|
|
1109
|
+
if (conflicts.length) {
|
|
1110
|
+
throw new CliError("Sync has conflicts. Resolve them before pushing.", {
|
|
1111
|
+
code: "sync_conflict",
|
|
1112
|
+
exitCode: EXIT.CONFLICT,
|
|
1113
|
+
details: { conflicts },
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
const results = [];
|
|
1117
|
+
const mappings = { ...(state.mappings || {}) };
|
|
1118
|
+
for (const actionItem of plan.actions.filter((item) => item.action === "push")) {
|
|
1119
|
+
const path = localSyncPath(root, actionItem.relative_path);
|
|
1120
|
+
await assertNoSyncSymlink(root, path);
|
|
1121
|
+
const bytes = await readFile(path);
|
|
1122
|
+
const markdown = bytes.toString("utf8");
|
|
1123
|
+
if (markdown.length > MAX_MARKDOWN_CHARACTERS) {
|
|
1124
|
+
throw new CliError(`Markdown exceeds ${MAX_MARKDOWN_CHARACTERS} characters: ${actionItem.relative_path}`, {
|
|
1125
|
+
code: "sync_markdown_too_large",
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
const info = await stat(path);
|
|
1129
|
+
const previous = actionItem.page_id ? mappings[actionItem.page_id] : null;
|
|
1130
|
+
const result = await api.request("/sync/push", {
|
|
1131
|
+
method: "POST",
|
|
1132
|
+
body: {
|
|
1133
|
+
source_id: state.source_id,
|
|
1134
|
+
relative_path: actionItem.relative_path,
|
|
1135
|
+
markdown,
|
|
1136
|
+
content_sha256: sha256(bytes),
|
|
1137
|
+
mtime_ms: Math.trunc(info.mtimeMs),
|
|
1138
|
+
page_id: actionItem.page_id || null,
|
|
1139
|
+
expected_content_revision: previous?.content_revision ?? null,
|
|
1140
|
+
},
|
|
1141
|
+
});
|
|
1142
|
+
results.push(result);
|
|
1143
|
+
mappings[result.item.page_id] = {
|
|
1144
|
+
node_id: result.item.node_id,
|
|
1145
|
+
relative_path: result.item.relative_path,
|
|
1146
|
+
content_sha256: result.item.content_sha256,
|
|
1147
|
+
content_revision: result.item.content_revision,
|
|
1148
|
+
};
|
|
1149
|
+
state.mappings = mappings;
|
|
1150
|
+
state.last_push_at = new Date().toISOString();
|
|
1151
|
+
await writeSyncState(root, state);
|
|
1152
|
+
}
|
|
1153
|
+
state.mappings = mappings;
|
|
1154
|
+
state.last_push_at = new Date().toISOString();
|
|
1155
|
+
await writeSyncState(root, state);
|
|
1156
|
+
return writeResult({
|
|
1157
|
+
pushed: results.length,
|
|
1158
|
+
results,
|
|
1159
|
+
unresolved_local_missing: plan.actions.filter((item) => item.action === "local_missing"),
|
|
1160
|
+
}, { json: options.json });
|
|
1161
|
+
}
|
|
1162
|
+
if (action === "resolve") {
|
|
1163
|
+
requireConfirmation(options, "Resolving a sync conflict");
|
|
1164
|
+
const conflictId = option(options, "conflict-id", { required: true });
|
|
1165
|
+
const strategy = option(options, "strategy", { required: true });
|
|
1166
|
+
const conflict = await api.request(`/sync/conflicts/${resourcePath(conflictId)}`);
|
|
1167
|
+
if (strategy === "use-remote") {
|
|
1168
|
+
if (!conflict.remote) throw new CliError("The remote document is no longer available.", { code: "sync_remote_missing" });
|
|
1169
|
+
const item = conflict.remote;
|
|
1170
|
+
const target = localSyncPath(root, item.relative_path);
|
|
1171
|
+
await assertNoSyncSymlink(root, target);
|
|
1172
|
+
const verified = verifiedRemoteMarkdown(item);
|
|
1173
|
+
await writeTextFile(target, verified.markdown);
|
|
1174
|
+
const info = await stat(target);
|
|
1175
|
+
await api.request("/sync/commit", {
|
|
1176
|
+
method: "POST",
|
|
1177
|
+
body: {
|
|
1178
|
+
source_id: state.source_id,
|
|
1179
|
+
items: [{
|
|
1180
|
+
page_id: item.page_id,
|
|
1181
|
+
node_id: item.node_id,
|
|
1182
|
+
relative_path: item.relative_path,
|
|
1183
|
+
content_sha256: verified.contentHash,
|
|
1184
|
+
mtime_ms: Math.trunc(info.mtimeMs),
|
|
1185
|
+
}],
|
|
1186
|
+
},
|
|
1187
|
+
});
|
|
1188
|
+
const result = await api.request(`/sync/conflicts/${resourcePath(conflictId)}:resolve`, {
|
|
1189
|
+
method: "POST",
|
|
1190
|
+
body: { strategy: "use_remote" },
|
|
1191
|
+
});
|
|
1192
|
+
state.mappings[item.page_id] = {
|
|
1193
|
+
node_id: item.node_id,
|
|
1194
|
+
relative_path: item.relative_path,
|
|
1195
|
+
content_sha256: item.content_sha256,
|
|
1196
|
+
content_revision: item.content_revision,
|
|
1197
|
+
};
|
|
1198
|
+
await writeSyncState(root, state);
|
|
1199
|
+
return writeResult(result, { json: options.json });
|
|
1200
|
+
}
|
|
1201
|
+
if (strategy === "keep-local") {
|
|
1202
|
+
const target = localSyncPath(root, conflict.relative_path);
|
|
1203
|
+
await assertNoSyncSymlink(root, target);
|
|
1204
|
+
const bytes = await readFile(target);
|
|
1205
|
+
const markdown = bytes.toString("utf8");
|
|
1206
|
+
if (markdown.length > MAX_MARKDOWN_CHARACTERS) {
|
|
1207
|
+
throw new CliError(`Markdown exceeds ${MAX_MARKDOWN_CHARACTERS} characters: ${conflict.relative_path}`, {
|
|
1208
|
+
code: "sync_markdown_too_large",
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
const info = await stat(target);
|
|
1212
|
+
const result = await api.request("/sync/push", {
|
|
1213
|
+
method: "POST",
|
|
1214
|
+
body: {
|
|
1215
|
+
source_id: state.source_id,
|
|
1216
|
+
relative_path: conflict.relative_path,
|
|
1217
|
+
markdown,
|
|
1218
|
+
content_sha256: sha256(bytes),
|
|
1219
|
+
mtime_ms: Math.trunc(info.mtimeMs),
|
|
1220
|
+
page_id: conflict.remote?.page_id || null,
|
|
1221
|
+
conflict_id: conflictId,
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
state.mappings[result.item.page_id] = {
|
|
1225
|
+
node_id: result.item.node_id,
|
|
1226
|
+
relative_path: result.item.relative_path,
|
|
1227
|
+
content_sha256: result.item.content_sha256,
|
|
1228
|
+
content_revision: result.item.content_revision,
|
|
1229
|
+
};
|
|
1230
|
+
await writeSyncState(root, state);
|
|
1231
|
+
return writeResult(result, { json: options.json });
|
|
1232
|
+
}
|
|
1233
|
+
throw new CliError("--strategy must be use-remote or keep-local.", { code: "sync_strategy_invalid" });
|
|
1234
|
+
}
|
|
1235
|
+
throw new CliError("sync action must be init, status, plan, pull, push, or resolve.");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
298
1238
|
async function main({ options, positionals }) {
|
|
299
1239
|
const [command, action, id, ...rest] = positionals;
|
|
300
1240
|
if (options.version || command === "version") return writeResult(VERSION, { json: options.json });
|
|
301
|
-
if (options.help || !command || command === "help")
|
|
1241
|
+
if (options.help || !command || command === "help") {
|
|
1242
|
+
const helpCommand = command === "help" ? action : command;
|
|
1243
|
+
const helpAction = command === "help" ? id : action;
|
|
1244
|
+
return writeResult(contextualHelp(helpCommand, helpAction), { json: false });
|
|
1245
|
+
}
|
|
1246
|
+
validateInvocation(options, positionals);
|
|
1247
|
+
if (command !== "update") await maybeNotifyUpdate(options);
|
|
302
1248
|
if (command === "auth") {
|
|
303
1249
|
if (action === "login") return authLogin(options);
|
|
304
1250
|
if (action === "status") return authStatus(options);
|
|
305
1251
|
if (action === "logout") return authLogout(options);
|
|
306
1252
|
throw new CliError("auth action must be login, status, or logout.");
|
|
307
1253
|
}
|
|
1254
|
+
if (command === "workspaces" || command === "workspace") {
|
|
1255
|
+
return workspaces(options, action || "list", id);
|
|
1256
|
+
}
|
|
308
1257
|
if (command === "capabilities") return capabilities(options);
|
|
309
1258
|
if (command === "ask") return ask(options, [action, id, ...rest].filter(Boolean));
|
|
310
1259
|
if (command === "questions") return questions(options, action || "list", id);
|
|
311
1260
|
if (command === "conversations") return conversations(options, action || "list", id);
|
|
312
1261
|
if (command === "search") return search(options, [action, id, ...rest].filter(Boolean));
|
|
313
1262
|
if (command === "docs") return docs(options, action || "list", id);
|
|
314
|
-
if (command === "
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
}
|
|
1263
|
+
if (command === "projects" || command === "folders") return projects(options, action || "create");
|
|
1264
|
+
if (command === "files") return files(options, action || "list", id);
|
|
1265
|
+
if (command === "sync") return syncCommand(options, action || "status");
|
|
1266
|
+
if (command === "update" && (action || "check") === "check") return updateCheck(options);
|
|
320
1267
|
throw new CliError(`Unknown command: ${command}`);
|
|
321
1268
|
}
|
|
322
1269
|
|