primitive-admin 1.1.0-alpha.83 → 1.1.0-alpha.85
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 +43 -1
- package/assets/skill/skills/primitive-platform/SKILL.md +105 -675
- package/dist/src/commands/admins.js +29 -32
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +2 -1
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +12 -5
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +2 -1
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +26 -10
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +2 -1
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +88 -18
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/connections.js +8 -9
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +2 -1
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.js +3 -2
- package/dist/src/commands/database-type-configs.js.map +1 -1
- package/dist/src/commands/databases.js +26 -10
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +38 -0
- package/dist/src/commands/documents.js +258 -64
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +2 -1
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.js +11 -1
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/feature-flags.js +2 -1
- package/dist/src/commands/feature-flags.js.map +1 -1
- package/dist/src/commands/functions.js +3 -2
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/group-type-configs.js +2 -1
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +29 -8
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.js +17 -11
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/integrations.js +26 -9
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/locks.js +2 -1
- package/dist/src/commands/locks.js.map +1 -1
- package/dist/src/commands/metadata-category-configs.js +2 -1
- package/dist/src/commands/metadata-category-configs.js.map +1 -1
- package/dist/src/commands/metadata.js +8 -1
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +28 -11
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.js +2 -1
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.js +15 -6
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/secrets.js +2 -1
- package/dist/src/commands/secrets.js.map +1 -1
- package/dist/src/commands/sessions.js +10 -9
- package/dist/src/commands/sessions.js.map +1 -1
- package/dist/src/commands/sync.d.ts +64 -0
- package/dist/src/commands/sync.js +347 -17
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +2 -1
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +16 -9
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +2 -1
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/waitlist.js +2 -1
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.js +17 -10
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.js +36 -18
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +111 -1
- package/dist/src/lib/api-client.js +179 -66
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +29 -7
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-payload.js +27 -2
- package/dist/src/lib/config-payload.js.map +1 -1
- package/dist/src/lib/config-surface.d.ts +2 -1
- package/dist/src/lib/config-surface.js +50 -3
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.js +8 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
- package/dist/src/lib/document-ingest-rows.d.ts +137 -0
- package/dist/src/lib/document-ingest-rows.js +181 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +25 -2
- package/dist/src/lib/document-ingest.js +184 -31
- package/dist/src/lib/document-ingest.js.map +1 -1
- package/dist/src/lib/function-db-types.d.ts +1 -1
- package/dist/src/lib/function-db-types.js +1 -1
- package/dist/src/lib/function-document-types.js +23 -2
- package/dist/src/lib/function-document-types.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +182 -1
- package/dist/src/lib/generated-config-surfaces.js +441 -2
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/list-options.d.ts +68 -0
- package/dist/src/lib/list-options.js +89 -0
- package/dist/src/lib/list-options.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +16 -0
- package/dist/src/lib/log-inspection.js +23 -0
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/paginate.d.ts +15 -0
- package/dist/src/lib/paginate.js +17 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/resolve-owner.d.ts +19 -0
- package/dist/src/lib/resolve-owner.js +20 -0
- package/dist/src/lib/resolve-owner.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +49 -3
- package/dist/src/lib/skill-installer.js +275 -100
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshot-build.d.ts +50 -0
- package/dist/src/lib/snapshot-build.js +111 -0
- package/dist/src/lib/snapshot-build.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { snapshotBuildRows } from "../lib/snapshot-build-rows.js";
|
|
2
|
+
import { driveSnapshotBuild } from "../lib/snapshot-build.js";
|
|
3
|
+
import { ingestClientRows, ingestSessionRows, } from "../lib/document-ingest-rows.js";
|
|
2
4
|
import { ApiClient } from "../lib/api-client.js";
|
|
3
5
|
import { resolveAppId } from "../lib/config.js";
|
|
4
6
|
import { success, error, info, formatTable, formatId, formatDate, json, keyValue, result as printResult, warn, flushOutput, } from "../lib/output.js";
|
|
5
7
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
8
|
+
import { resolveOwnerUserId } from "../lib/resolve-owner.js";
|
|
9
|
+
import { pageCursorOption, pageLimitOption, parsePageLimit, printEmptyPage, printPageHint, } from "../lib/list-options.js";
|
|
10
|
+
import { normalizeCliListEnvelope, wholeListEnvelope, } from "../lib/paginate.js";
|
|
6
11
|
import { parseDataOption } from "../lib/data-input.js";
|
|
7
12
|
import { parseFilterOptions } from "../lib/record-filter.js";
|
|
8
13
|
import { buildPermissionsExport } from "../lib/document-export-permissions.js";
|
|
@@ -18,37 +23,16 @@ import { ulid } from "ulid";
|
|
|
18
23
|
import * as fs from "fs";
|
|
19
24
|
import * as path from "path";
|
|
20
25
|
/**
|
|
21
|
-
* One bulk-load session as rows (#3434, #3435).
|
|
26
|
+
* One bulk-load session as rows (#3434, #3435, #3598).
|
|
22
27
|
*
|
|
23
28
|
* Shared by `documents ingests get` and by `documents ingest`'s own terminal
|
|
24
|
-
* report, so an operator reads the same thing however they arrived at it.
|
|
29
|
+
* report, so an operator reads the same thing however they arrived at it. The
|
|
30
|
+
* rows themselves are decided in `document-ingest-rows`, where they can be
|
|
31
|
+
* checked without a live server holding a session in the right state.
|
|
25
32
|
*/
|
|
26
33
|
function printIngestSession(session) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
printResult("State", session.state);
|
|
30
|
-
printResult("Created by", session.createdBy);
|
|
31
|
-
printResult("Created", formatDate(session.createdAt));
|
|
32
|
-
printResult("Updated", formatDate(session.updatedAt));
|
|
33
|
-
printResult("Completed", session.completedAt ? formatDate(session.completedAt) : "—");
|
|
34
|
-
printResult("Chunks", session.chunks);
|
|
35
|
-
printResult("Rows", session.rows);
|
|
36
|
-
printResult("Bytes", session.bytes);
|
|
37
|
-
const progress = session.progress ?? {};
|
|
38
|
-
printResult("Progress", `copied ${progress.rowsCopied ?? 0} rows, applied ` +
|
|
39
|
-
`${progress.chunksApplied ?? 0} chunks / ${progress.rowsApplied ?? 0} rows, ` +
|
|
40
|
-
`reconciled ${progress.recordsReconciled ?? 0} records`);
|
|
41
|
-
printResult("Copy point", session.copyStartedAt ? formatDate(session.copyStartedAt) : "—");
|
|
42
|
-
printResult("Discontinuity epoch", session.discontinuityEpoch ?? "—");
|
|
43
|
-
printResult("Build", session.buildId ?? "—");
|
|
44
|
-
printResult("Expires", formatDate(session.expiresAt));
|
|
45
|
-
// Why it stopped is the reason an operator opened this at all.
|
|
46
|
-
if (session.failure) {
|
|
47
|
-
printResult("Failure", session.failure.code);
|
|
48
|
-
printResult("Reason", session.failure.reason);
|
|
49
|
-
for (const row of session.failure.rows ?? []) {
|
|
50
|
-
printResult(" Row", typeof row === "string" ? row : `${row.model} ${row.id}`);
|
|
51
|
-
}
|
|
34
|
+
for (const [label, value] of ingestSessionRows(session)) {
|
|
35
|
+
printResult(label, value);
|
|
52
36
|
}
|
|
53
37
|
}
|
|
54
38
|
/**
|
|
@@ -66,22 +50,85 @@ function printDocumentFormat(doc) {
|
|
|
66
50
|
}
|
|
67
51
|
}
|
|
68
52
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
53
|
+
* What `documents records bulk` reports when the write is refused (#3619).
|
|
54
|
+
*
|
|
55
|
+
* The command used to catch every failure with `error(err.message)` while
|
|
56
|
+
* `ApiError` carried the code and the status on separate fields, so the stable
|
|
57
|
+
* code the server answers never reached the output and `--json` handled
|
|
58
|
+
* successes only. An operator driving a multi-hour load had to read human text
|
|
59
|
+
* to tell "ask again" from "somebody look at the log" (finding 3619-SO-04).
|
|
60
|
+
*
|
|
61
|
+
* The status is read from `statusCode` FIRST: that is where
|
|
62
|
+
* `cli/src/lib/api-client.ts`'s `ApiError` keeps it, and a reader that asks for
|
|
63
|
+
* `status` alone sees `undefined` on every error the client raises and prints
|
|
64
|
+
* nothing at all — #3597 paid for that once already. `status` is the fallback,
|
|
65
|
+
* for an error that came from somewhere else.
|
|
66
|
+
*
|
|
67
|
+
* Its own function so both surfaces are composed once and can be graded
|
|
68
|
+
* without spawning a process: the human line and the `--json` envelope must
|
|
69
|
+
* agree about which failure happened.
|
|
75
70
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
export function describeBulkFailure(err) {
|
|
72
|
+
const message = (typeof err?.message === "string" && err.message) || String(err);
|
|
73
|
+
const code = typeof err?.code === "string" && err.code ? err.code : undefined;
|
|
74
|
+
const raw = err?.statusCode ?? err?.status;
|
|
75
|
+
const status = Number.isFinite(Number(raw)) ? Number(raw) : undefined;
|
|
76
|
+
return {
|
|
77
|
+
line: `${message}${code ? ` [${code}]` : ""}${status !== undefined ? ` (${status})` : ""}`,
|
|
78
|
+
envelope: {
|
|
79
|
+
ok: false,
|
|
80
|
+
...(code ? { code } : {}),
|
|
81
|
+
...(status !== undefined ? { status } : {}),
|
|
82
|
+
error: message,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A document's tags as a list, from whatever the wire handed back (#3644).
|
|
88
|
+
*
|
|
89
|
+
* `tags` is absent when the document has none — the server's presence rule,
|
|
90
|
+
* shared by the app API's list shape and the admin inventory — so every
|
|
91
|
+
* reader here has to cope with `undefined`. A stray non-array (an older
|
|
92
|
+
* server, a hand-edited fixture) is read as "no tags" rather than printed
|
|
93
|
+
* raw.
|
|
94
|
+
*/
|
|
95
|
+
function documentTagList(tags) {
|
|
96
|
+
return Array.isArray(tags)
|
|
97
|
+
? tags.filter((tag) => typeof tag === "string" && tag !== "")
|
|
98
|
+
: [];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Tag text a terminal prints rather than obeys (#3644).
|
|
102
|
+
*
|
|
103
|
+
* A tag is app-user text. The write-side validator
|
|
104
|
+
* (`validateTag` in `src/app-api/controllers/documents-controller.ts`) trims
|
|
105
|
+
* it, caps it at 48 characters and refuses dynamo-bao's key separator, but
|
|
106
|
+
* every other byte survives — including the escape character. Printed raw
|
|
107
|
+
* into an operator's terminal, a 15-character tag such as
|
|
108
|
+
* `\x1b[2J\x1b[Hrestored` clears the screen and homes the cursor while
|
|
109
|
+
* the listing is being written, so a member with write access to one document
|
|
110
|
+
* could hide or falsify what `documents list` and `documents get` appear to
|
|
111
|
+
* report about the rest. The C0 range, DEL and the C1 range are therefore
|
|
112
|
+
* rendered as their `\uXXXX` source escapes: readable, one line and one cell
|
|
113
|
+
* per tag, and inert. Ordinary text — accents, CJK, emoji — is untouched.
|
|
114
|
+
*/
|
|
115
|
+
function escapeForTerminal(tag) {
|
|
116
|
+
return tag.replace(/[\x00-\x1f\x7f-\x9f]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A document's tags as printable text, in the order the wire reported them.
|
|
120
|
+
*
|
|
121
|
+
* Only the plain-text surfaces come through here: `--json` on both commands
|
|
122
|
+
* returns the server's object untouched, so a migration diffing tags against
|
|
123
|
+
* what it wrote still reads the stored bytes.
|
|
124
|
+
*/
|
|
125
|
+
export function displayTags(tags) {
|
|
126
|
+
return documentTagList(tags).map(escapeForTerminal);
|
|
127
|
+
}
|
|
128
|
+
/** The tags cell of a documents table: comma-joined, or `-` when there are none. */
|
|
129
|
+
export function formatTags(tags) {
|
|
130
|
+
const list = displayTags(tags);
|
|
131
|
+
return list.length > 0 ? list.join(", ") : "-";
|
|
85
132
|
}
|
|
86
133
|
export function registerDocumentsCommands(program) {
|
|
87
134
|
const documents = program
|
|
@@ -119,26 +166,38 @@ Examples:
|
|
|
119
166
|
.description("List a user's documents")
|
|
120
167
|
.requiredOption("--user-id <id>", "User ID whose documents to list")
|
|
121
168
|
.option("--app <app-id>", "App ID")
|
|
169
|
+
.addOption(pageLimitOption())
|
|
170
|
+
.addOption(pageCursorOption())
|
|
122
171
|
.option("--json", "Output as JSON")
|
|
123
172
|
.action(async (options) => {
|
|
173
|
+
const limit = parsePageLimit(options.limit);
|
|
124
174
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
125
175
|
const client = new ApiClient();
|
|
126
176
|
try {
|
|
127
|
-
const
|
|
177
|
+
const page = normalizeCliListEnvelope(await client.listAdminDocumentsPage(resolvedAppId, options.userId, {
|
|
178
|
+
limit,
|
|
179
|
+
cursor: options.cursor,
|
|
180
|
+
}));
|
|
181
|
+
const list = page.items;
|
|
128
182
|
if (options.json) {
|
|
129
|
-
json(
|
|
183
|
+
json(page);
|
|
130
184
|
return;
|
|
131
185
|
}
|
|
132
186
|
if (list.length === 0) {
|
|
133
|
-
|
|
187
|
+
printEmptyPage(page, "documents");
|
|
134
188
|
return;
|
|
135
189
|
}
|
|
136
190
|
console.log(formatTable(list, [
|
|
137
191
|
{ header: "DOCUMENT_ID", key: "documentId", format: formatId },
|
|
138
192
|
{ header: "TITLE", key: "title", flex: true },
|
|
193
|
+
// #3644 — the tags the route now reports. An app migration that
|
|
194
|
+
// regenerates tags on import verifies them from here; a document
|
|
195
|
+
// with none shows `-` rather than an empty cell.
|
|
196
|
+
{ header: "TAGS", key: "tags", format: formatTags },
|
|
139
197
|
{ header: "PERMISSION", key: "permission" },
|
|
140
198
|
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
141
199
|
]));
|
|
200
|
+
printPageHint(page);
|
|
142
201
|
}
|
|
143
202
|
catch (err) {
|
|
144
203
|
error(err.message);
|
|
@@ -167,6 +226,15 @@ Examples:
|
|
|
167
226
|
keyValue("Created", formatDate(doc.createdAt));
|
|
168
227
|
keyValue("Modified", formatDate(doc.modifiedAt ?? doc.lastModified));
|
|
169
228
|
printDocumentFormat(doc);
|
|
229
|
+
// #3644 — tags, when the document has any. The endpoint has returned
|
|
230
|
+
// them since #3096 but only `--json` showed them, so a migration
|
|
231
|
+
// verifying restored tags had to parse JSON to see its own work.
|
|
232
|
+
// Printed only when present, by the same rule the response uses: an
|
|
233
|
+
// untagged document's output is unchanged, and escaped the same way
|
|
234
|
+
// the table's cell is so a crafted tag cannot drive the terminal.
|
|
235
|
+
const tags = displayTags(doc.tags);
|
|
236
|
+
if (tags.length > 0)
|
|
237
|
+
keyValue("Tags", tags.join(", "));
|
|
170
238
|
// `GET documents/:id` reports the caller's own access, not the
|
|
171
239
|
// document's grant/alias/collection inventory — printing counts for
|
|
172
240
|
// fields the endpoint never returns read as a definitive "0" for
|
|
@@ -320,7 +388,7 @@ Permissions (enforced by the server, not the CLI):
|
|
|
320
388
|
const result = await client.listDocumentPermissions(resolvedAppId, documentId);
|
|
321
389
|
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
322
390
|
if (options.json) {
|
|
323
|
-
json(list);
|
|
391
|
+
json(wholeListEnvelope(list));
|
|
324
392
|
return;
|
|
325
393
|
}
|
|
326
394
|
if (list.length === 0) {
|
|
@@ -868,7 +936,11 @@ Permissions (enforced by the server, not the CLI):
|
|
|
868
936
|
`${result.added.length} added, ${result.updated.length} updated, ${result.deleted} deleted.`);
|
|
869
937
|
}
|
|
870
938
|
catch (err) {
|
|
871
|
-
|
|
939
|
+
const report = describeBulkFailure(err);
|
|
940
|
+
if (options.json)
|
|
941
|
+
json(report.envelope);
|
|
942
|
+
else
|
|
943
|
+
error(report.line);
|
|
872
944
|
process.exit(1);
|
|
873
945
|
}
|
|
874
946
|
});
|
|
@@ -983,7 +1055,13 @@ Permissions (enforced by the server, not the CLI):
|
|
|
983
1055
|
for (const ignored of input.ignored) {
|
|
984
1056
|
info(`Ignoring ${ignored}: a bulk load reads the snapshot only.`);
|
|
985
1057
|
}
|
|
1058
|
+
// #3598 — reading, checking, sorting, cutting and gzipping is this
|
|
1059
|
+
// side's biggest phase on a large input, and it used to be invisible
|
|
1060
|
+
// inside one wall-clock number that also covered the platform's work.
|
|
1061
|
+
const buildStartedAt = Date.now();
|
|
986
1062
|
plan = await buildIngestArtifact({ input, schema });
|
|
1063
|
+
const buildMs = Date.now() - buildStartedAt;
|
|
1064
|
+
info(`Built artifact in ${(buildMs / 1000).toFixed(1)}s.`);
|
|
987
1065
|
for (const line of summariseIngestPlan(plan))
|
|
988
1066
|
info(line);
|
|
989
1067
|
assertIngestConfirmable({
|
|
@@ -1028,14 +1106,27 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1028
1106
|
}
|
|
1029
1107
|
if (outcome.message)
|
|
1030
1108
|
error(outcome.message);
|
|
1109
|
+
// #3598 — the two sides, side by side. The session's own `timings`
|
|
1110
|
+
// say where the platform's share went; `client` says what this
|
|
1111
|
+
// process spent before and around it, so a slow load can be blamed on
|
|
1112
|
+
// the side that was actually slow.
|
|
1113
|
+
const clientTimings = { buildMs, ...outcome.timings };
|
|
1031
1114
|
if (options.json) {
|
|
1032
|
-
json(
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1115
|
+
json({
|
|
1116
|
+
...(outcome.session ?? { sessionId: outcome.sessionId }),
|
|
1117
|
+
client: clientTimings,
|
|
1118
|
+
});
|
|
1036
1119
|
}
|
|
1037
|
-
else
|
|
1038
|
-
|
|
1120
|
+
else {
|
|
1121
|
+
if (outcome.session) {
|
|
1122
|
+
printIngestSession(outcome.session);
|
|
1123
|
+
}
|
|
1124
|
+
else if (outcome.sessionId) {
|
|
1125
|
+
printResult("Session", outcome.sessionId);
|
|
1126
|
+
}
|
|
1127
|
+
for (const [label, value] of ingestClientRows(clientTimings)) {
|
|
1128
|
+
printResult(label, value);
|
|
1129
|
+
}
|
|
1039
1130
|
}
|
|
1040
1131
|
return outcome.exitCode;
|
|
1041
1132
|
};
|
|
@@ -1071,13 +1162,14 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1071
1162
|
.option("--cursor <cursor>", "Continue from a previous page's nextCursor")
|
|
1072
1163
|
.option("--json", "Output as JSON")
|
|
1073
1164
|
.action(async (documentId, options) => {
|
|
1165
|
+
const limit = parsePageLimit(options.limit);
|
|
1074
1166
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1075
1167
|
const client = new ApiClient();
|
|
1076
1168
|
try {
|
|
1077
|
-
const page = await client.listDocumentIngests(resolvedAppId, documentId, {
|
|
1078
|
-
limit
|
|
1169
|
+
const page = normalizeCliListEnvelope(await client.listDocumentIngests(resolvedAppId, documentId, {
|
|
1170
|
+
limit,
|
|
1079
1171
|
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
1080
|
-
});
|
|
1172
|
+
}));
|
|
1081
1173
|
if (options.json) {
|
|
1082
1174
|
json(page);
|
|
1083
1175
|
return;
|
|
@@ -1145,13 +1237,14 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1145
1237
|
.option("--cursor <cursor>", "Continue from a previous page's nextCursor")
|
|
1146
1238
|
.option("--json", "Output as JSON")
|
|
1147
1239
|
.action(async (documentId, options) => {
|
|
1240
|
+
const limit = parsePageLimit(options.limit);
|
|
1148
1241
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1149
1242
|
const client = new ApiClient();
|
|
1150
1243
|
try {
|
|
1151
|
-
const page = await client.listDocumentSnapshots(resolvedAppId, documentId, {
|
|
1152
|
-
limit
|
|
1244
|
+
const page = normalizeCliListEnvelope(await client.listDocumentSnapshots(resolvedAppId, documentId, {
|
|
1245
|
+
limit,
|
|
1153
1246
|
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
1154
|
-
});
|
|
1247
|
+
}));
|
|
1155
1248
|
if (options.json) {
|
|
1156
1249
|
json(page);
|
|
1157
1250
|
return;
|
|
@@ -1216,6 +1309,96 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1216
1309
|
process.exit(1);
|
|
1217
1310
|
}
|
|
1218
1311
|
});
|
|
1312
|
+
// #3666, criterion 7 — the one ACTION verb in this group: snapshot the
|
|
1313
|
+
// document now instead of waiting for the size or age trigger. Its exit
|
|
1314
|
+
// codes are `documents ingest`'s, as `docs/cli-design.md` records them, and
|
|
1315
|
+
// for the same reason: giving up on WATCHING a build is not the build
|
|
1316
|
+
// failing.
|
|
1317
|
+
//
|
|
1318
|
+
// Not destructive, so it does not confirm: a seal rotates the overlay the
|
|
1319
|
+
// room is writing to and loses nothing, which is what the size trigger does
|
|
1320
|
+
// on its own several times a day.
|
|
1321
|
+
snapshots
|
|
1322
|
+
.command("build")
|
|
1323
|
+
.description("Snapshot a large document now: seal the open epoch and build a base")
|
|
1324
|
+
.argument("<document-id>", "Document ID")
|
|
1325
|
+
.option("--app <app-id>", "App ID")
|
|
1326
|
+
.option("--wait", "Poll until the build has verified or failed")
|
|
1327
|
+
.option("--timeout <seconds>", "Give up waiting after this many seconds")
|
|
1328
|
+
.option("--json", "Output as JSON")
|
|
1329
|
+
.action(async (documentId, options) => {
|
|
1330
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1331
|
+
const client = new ApiClient();
|
|
1332
|
+
const run = async () => {
|
|
1333
|
+
// Ctrl-C while polling leaves the build running — by then the room is
|
|
1334
|
+
// already building it and giving up on watching is not giving up on
|
|
1335
|
+
// the build — so the signal is handed to the driver rather than
|
|
1336
|
+
// killing the process.
|
|
1337
|
+
const interrupt = new AbortController();
|
|
1338
|
+
const onSignal = () => interrupt.abort();
|
|
1339
|
+
process.on("SIGINT", onSignal);
|
|
1340
|
+
let outcome;
|
|
1341
|
+
try {
|
|
1342
|
+
outcome = await driveSnapshotBuild({
|
|
1343
|
+
client: {
|
|
1344
|
+
requestSnapshot: (id) => client.requestDocumentSnapshot(resolvedAppId, id),
|
|
1345
|
+
getSnapshot: (id, buildId) => client.getDocumentSnapshot(resolvedAppId, id, buildId),
|
|
1346
|
+
},
|
|
1347
|
+
documentId,
|
|
1348
|
+
wait: Boolean(options.wait),
|
|
1349
|
+
timeoutMs: options.timeout ? Number(options.timeout) * 1000 : null,
|
|
1350
|
+
signal: interrupt.signal,
|
|
1351
|
+
onProgress: (line) => info(line),
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
finally {
|
|
1355
|
+
process.off("SIGINT", onSignal);
|
|
1356
|
+
}
|
|
1357
|
+
if (outcome.message)
|
|
1358
|
+
error(outcome.message);
|
|
1359
|
+
if (options.json) {
|
|
1360
|
+
// The route's answer as received, with the build the wait settled
|
|
1361
|
+
// on beside it when there was one — so a script branches on the
|
|
1362
|
+
// verdict rather than on an exit code alone.
|
|
1363
|
+
json(outcome.build ? { ...outcome.response, build: outcome.build } : outcome.response);
|
|
1364
|
+
return outcome.exitCode;
|
|
1365
|
+
}
|
|
1366
|
+
if (outcome.response?.sealed === false) {
|
|
1367
|
+
printResult("Sealed", "no — the open epoch carried nothing");
|
|
1368
|
+
printResult("Reason", outcome.response.reason);
|
|
1369
|
+
printResult("Covering build", outcome.response.coveringBuildId ?? "—");
|
|
1370
|
+
return outcome.exitCode;
|
|
1371
|
+
}
|
|
1372
|
+
printResult("Sealed epoch", outcome.response.sealedEpoch);
|
|
1373
|
+
printResult("Open epoch", outcome.response.nextEpoch);
|
|
1374
|
+
printResult("Build", outcome.response.buildId);
|
|
1375
|
+
if (outcome.build) {
|
|
1376
|
+
for (const [label, value] of snapshotBuildRows(outcome.build)) {
|
|
1377
|
+
printResult(label, value);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return outcome.exitCode;
|
|
1381
|
+
};
|
|
1382
|
+
let code = 1;
|
|
1383
|
+
try {
|
|
1384
|
+
code = await run();
|
|
1385
|
+
}
|
|
1386
|
+
catch (err) {
|
|
1387
|
+
// The server's own sentence, plus the code a script branches on
|
|
1388
|
+
// (#3403) — a refused loop that only says "too young to seal" leaves
|
|
1389
|
+
// a caller parsing prose to tell it from a permission refusal.
|
|
1390
|
+
error(err.code ? `${err.message} (${err.code})` : err.message);
|
|
1391
|
+
const retryAfterMs = err.details?.retryAfterMs;
|
|
1392
|
+
if (typeof retryAfterMs === "number") {
|
|
1393
|
+
error(`Ask again in ${Math.ceil(retryAfterMs / 1000)}s.`);
|
|
1394
|
+
}
|
|
1395
|
+
code = 1;
|
|
1396
|
+
}
|
|
1397
|
+
// The result has to reach the pipe before the process ends: stdout to a
|
|
1398
|
+
// pipe is asynchronous and `process.exit` does not wait for it.
|
|
1399
|
+
await flushOutput();
|
|
1400
|
+
process.exit(code);
|
|
1401
|
+
});
|
|
1219
1402
|
// #3433, criterion 10 — the operator audit: `snapshot ⊕ overlays == table`,
|
|
1220
1403
|
// recomputed on this side with the CLIENT's loader and fold and compared
|
|
1221
1404
|
// with the authoritative table page by page. The one check that can catch a
|
|
@@ -1288,9 +1471,16 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1288
1471
|
documents
|
|
1289
1472
|
.command("transfer-owner")
|
|
1290
1473
|
.description("Transfer document ownership to another user")
|
|
1474
|
+
// Every positional is declared optional and the action decides which is
|
|
1475
|
+
// which (#3645). A required positional AFTER an optional one makes the
|
|
1476
|
+
// optional one mandatory: commander binds the first value it sees to
|
|
1477
|
+
// `[app-id]` and then reports the required argument missing, so
|
|
1478
|
+
// `transfer-owner <document-id> <new-owner-id>` never reached the action.
|
|
1479
|
+
// What each slot actually requires is enforced below, where the shift is
|
|
1480
|
+
// already known.
|
|
1291
1481
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1292
|
-
.argument("
|
|
1293
|
-
.argument("
|
|
1482
|
+
.argument("[document-id]", "Document ID (required)")
|
|
1483
|
+
.argument("[new-owner-id]", "User ID of the new owner (required)")
|
|
1294
1484
|
.option("--app <app-id>", "App ID")
|
|
1295
1485
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
1296
1486
|
.option("--json", "Output as JSON")
|
|
@@ -1362,7 +1552,7 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1362
1552
|
const result = await client.listDocumentGroupPermissions(resolvedAppId, documentId);
|
|
1363
1553
|
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
1364
1554
|
if (options.json) {
|
|
1365
|
-
json(list);
|
|
1555
|
+
json(wholeListEnvelope(list));
|
|
1366
1556
|
return;
|
|
1367
1557
|
}
|
|
1368
1558
|
if (list.length === 0) {
|
|
@@ -1492,8 +1682,11 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1492
1682
|
documents
|
|
1493
1683
|
.command("export")
|
|
1494
1684
|
.description("Export a document (Yjs state, blobs, permissions, aliases)")
|
|
1685
|
+
// Optional in commander, required in the action (#3645) — see
|
|
1686
|
+
// `transfer-owner` above for why a `<...>` here would make `[app-id]`
|
|
1687
|
+
// mandatory.
|
|
1495
1688
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1496
|
-
.argument("
|
|
1689
|
+
.argument("[document-id]", "Document ID to export (required)")
|
|
1497
1690
|
.option("--app <app-id>", "App ID")
|
|
1498
1691
|
.option("--output <dir>", "Output directory", "./primitive-export")
|
|
1499
1692
|
.option("--no-blobs", "Skip blob data")
|
|
@@ -1604,8 +1797,9 @@ Permissions (enforced by the server, not the CLI):
|
|
|
1604
1797
|
documents
|
|
1605
1798
|
.command("import")
|
|
1606
1799
|
.description("Import documents from an export directory")
|
|
1800
|
+
// Optional in commander, required in the action (#3645).
|
|
1607
1801
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1608
|
-
.argument("
|
|
1802
|
+
.argument("[path]", "Path to export directory or single document directory (required)")
|
|
1609
1803
|
.option("--app <app-id>", "App ID")
|
|
1610
1804
|
.option("--aliases <mode>", "Alias conflict handling: overwrite or skip", "skip")
|
|
1611
1805
|
// `--overwrite` has never replaced a document: the import path issues no
|