run402 4.42.0 → 4.44.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/lib/command-manifest.mjs +6 -3
- package/lib/deploy-v2.mjs +27 -4
- package/lib/next-actions.mjs +5 -2
- package/lib/repos.mjs +571 -123
- package/lib/stats.mjs +57 -0
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +10 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +11 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/kernel.d.ts +31 -0
- package/sdk/dist/kernel.d.ts.map +1 -1
- package/sdk/dist/kernel.js +116 -3
- package/sdk/dist/kernel.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.d.ts +178 -5
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.js +266 -7
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +103 -1
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +175 -7
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.types.d.ts +184 -9
- package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.types.js +2 -1
- package/sdk/dist/namespaces/gitvault.types.js.map +1 -1
- package/sdk/dist/node/gitvault-apply.d.ts +8 -0
- package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-apply.js +1 -0
- package/sdk/dist/node/gitvault-apply.js.map +1 -1
- package/sdk/dist/node/gitvault-deploy.d.ts +13 -0
- package/sdk/dist/node/gitvault-deploy.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-deploy.js +28 -0
- package/sdk/dist/node/gitvault-deploy.js.map +1 -1
- package/sdk/dist/node/gitvault-keystore.d.ts +54 -4
- package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-keystore.js +20 -0
- package/sdk/dist/node/gitvault-keystore.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +328 -4
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +560 -14
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.d.ts +40 -7
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.js +67 -19
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
package/lib/repos.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
|
33
33
|
import { nextAction, claimOrgSlugAction, claimRepoNameAction } from "./next-actions.mjs";
|
|
34
34
|
import { printKeystoreLocation } from "./gitvault.mjs";
|
|
35
35
|
import { gitvaultRemoteUrlForRepo } from "#sdk";
|
|
36
|
+
import { sdkStats, printVerboseStats, isVerbose } from "./stats.mjs";
|
|
36
37
|
import {
|
|
37
38
|
normalizeArgv,
|
|
38
39
|
hasHelp,
|
|
@@ -55,28 +56,34 @@ Usage:
|
|
|
55
56
|
Common:
|
|
56
57
|
run402 repos create [name] [--org <org_id>] [--dir <path>] [--tier <tier>] [--project <id>]
|
|
57
58
|
run402 repos view [--project <id>] [--repo <repo_id>] [--human]
|
|
58
|
-
run402 repos list [--org <org_id>]
|
|
59
|
+
run402 repos list [--org <org_id>] [--human]
|
|
59
60
|
|
|
60
61
|
Then plain git, forever:
|
|
61
62
|
git push
|
|
62
63
|
git clone run402::<org>/<repo>
|
|
63
64
|
|
|
64
65
|
Occasional:
|
|
65
|
-
run402 repos snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint] [--dry-run]
|
|
66
|
+
run402 repos snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint] [--dry-run] [--allow-dirty] [--manifest-out <path>]
|
|
66
67
|
run402 repos mirror [<destination>] [--off] [--backfill] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>] [--project <id>] [--repo <repo_id>]
|
|
67
|
-
run402 repos recover <source> --out <dir> [--repo <repo_id>] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>]
|
|
68
|
+
run402 repos recover <source> --out <dir> [--repo <repo_id>] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>] [--human]
|
|
68
69
|
|
|
69
70
|
Lifecycle:
|
|
70
71
|
run402 repos rename <new_name> [--repo <repo_id> | --project <project_id>]
|
|
71
72
|
run402 repos delete [--project <id>] [--repo <repo_id>] [--force]
|
|
72
73
|
|
|
73
74
|
Maintenance:
|
|
74
|
-
run402 repos fsck [--project <id>] [--repo <repo_id>] [--mirror] [--budget <n>] [--no-write]
|
|
75
|
+
run402 repos fsck [--project <id>] [--repo <repo_id>] [--mirror] [--budget <n>] [--no-write] [--human]
|
|
75
76
|
run402 repos gc [--project <id>] [--repo <repo_id>] [--submit --intent-core <path> --verifier-receipt <path> [--wait]]
|
|
76
|
-
run402 repos access [--project <id>] [--repo <repo_id>]
|
|
77
|
-
run402 repos access repair [--project <id>] [--repo <repo_id>]
|
|
77
|
+
run402 repos access [--project <id>] [--repo <repo_id>] [--human]
|
|
78
|
+
run402 repos access repair [--project <id>] [--repo <repo_id>] --recipient-state-version <n> --recipient-revocation-version <n>
|
|
79
|
+
run402 repos access revoke-key <principal_id> [--project <id>] [--repo <repo_id>]
|
|
80
|
+
run402 repos access declare-exposure [--project <id>] [--repo <repo_id>]
|
|
78
81
|
run402 repos policy <required|grandfathered> [--project <id>] [--repo <repo_id>] [--reason <why>]
|
|
79
82
|
|
|
83
|
+
Every verb above also accepts -v/--verbose (a stderr summary line of request
|
|
84
|
+
stats — round trips, wire time, bytes — coexists with --human) and always
|
|
85
|
+
carries a \`stats\` block in its JSON result.
|
|
86
|
+
|
|
80
87
|
Subcommands:
|
|
81
88
|
create Provision (or, with --project, ADOPT an existing project), ALLOCATE
|
|
82
89
|
its vault (mints key material and, on first allocation, a one-shot
|
|
@@ -101,7 +108,8 @@ Subcommands:
|
|
|
101
108
|
vaults-by-org read when the gateway has it (one round trip);
|
|
102
109
|
gracefully falls back to the older per-project walk when it
|
|
103
110
|
404s. Not every project in the org — ones with no vault are
|
|
104
|
-
omitted.
|
|
111
|
+
omitted. \`--human\` renders a compact roster (address,
|
|
112
|
+
generation, bytes, policy) instead of JSON.
|
|
105
113
|
rename Claim or rename the repo's per-org-unique, address-form name
|
|
106
114
|
(the <name> half of run402::<org-slug>/<name>). Address by
|
|
107
115
|
--repo or --project (not both).
|
|
@@ -120,6 +128,21 @@ Subcommands:
|
|
|
120
128
|
publishing. Push-to-creates through a slug-form remote
|
|
121
129
|
(run402::<org-slug>/<name>) the same way \`git push\` does.
|
|
122
130
|
\`--dry-run\` previews the real local pipeline without publishing.
|
|
131
|
+
A DIRTY tree (modified/staged tracked paths, or untracked-not-
|
|
132
|
+
ignored paths) REFUSES by default (SNAPSHOT_DIRTY_TREE, before any
|
|
133
|
+
object is created) — commit and retry, or pass \`--allow-dirty\` to
|
|
134
|
+
capture it as-is; the result then discloses exactly what was
|
|
135
|
+
swept in (modified_captured / untracked_captured), printed to
|
|
136
|
+
stderr too. \`--dry-run\` surfaces the same refusal.
|
|
137
|
+
Both \`--dry-run\` and a real snapshot print a SUMMARY by default —
|
|
138
|
+
file counts (files_total/files_changed/files_new), total/delta
|
|
139
|
+
bytes, and up to 200 changed/new paths (changed_more names any
|
|
140
|
+
overflow) — never the full captured-file inventory, which can run
|
|
141
|
+
to thousands of entries on a real repo. \`--manifest-out <path>\`
|
|
142
|
+
writes the complete inventory to a file (the result's
|
|
143
|
+
manifest_path names it); \`-v\`/\`--verbose\` inlines the full
|
|
144
|
+
inventory directly in the JSON (in addition to its usual stderr
|
|
145
|
+
stats line, not instead of it).
|
|
123
146
|
mirror ONE flag-driven verb for the client-side, customer-
|
|
124
147
|
owned ciphertext mirror — run402 never holds a credential to it.
|
|
125
148
|
No argument: READ the configured destination + a keyless
|
|
@@ -131,12 +154,16 @@ Subcommands:
|
|
|
131
154
|
mirror that fell behind). Exactly one of these per call. Mirror
|
|
132
155
|
state also renders inside \`repos view\`; mirror INTEGRITY inside
|
|
133
156
|
\`repos fsck --mirror\`.
|
|
134
|
-
recover \`r402s-recover\`: rebuild a
|
|
135
|
-
a mirrored prefix, with NO SERVER INVOLVED —
|
|
136
|
-
path (normal retrieval is plain \`git clone
|
|
137
|
-
no \`repos clone\` verb exists).
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
recover \`r402s-recover\`: rebuild a BARE recovery repository (no working
|
|
158
|
+
files) straight from a mirrored prefix, with NO SERVER INVOLVED —
|
|
159
|
+
the offline disaster path (normal retrieval is plain \`git clone
|
|
160
|
+
run402::<org>/<repo>\`, no \`repos clone\` verb exists). The result's
|
|
161
|
+
\`layout\` is \`"bare"\` and its \`next_actions\` print the exact
|
|
162
|
+
\`git clone <out_dir> <out_dir>-worktree\` to run for a working
|
|
163
|
+
tree — recover itself never checks files out. Proves this
|
|
164
|
+
mirror's validity, never freshness — read both honesty statements
|
|
165
|
+
before relying on the result. \`--human\` renders a short summary
|
|
166
|
+
instead of JSON.
|
|
140
167
|
fsck Walks the head chain AND materializes
|
|
141
168
|
the ref map, advancing BOTH
|
|
142
169
|
local trust pins — reported EXPLICITLY as local_state_changed +
|
|
@@ -147,6 +174,7 @@ Subcommands:
|
|
|
147
174
|
normal writing mode, so a budget-exceeded run resumes). \`--mirror\`
|
|
148
175
|
additionally runs the keyless mirror integrity probe — it proves
|
|
149
176
|
the mirror's VALIDITY, never its FRESHNESS, and says so.
|
|
177
|
+
\`--human\` renders a short summary instead of JSON.
|
|
150
178
|
gc \`git gc\`'s own two halves — checkpoint publication (compact) and
|
|
151
179
|
prune planning — in one verb, NOT described as "exactly git gc":
|
|
152
180
|
the deletion ceremony is stricter. Plans and checkpoints by
|
|
@@ -163,20 +191,41 @@ Subcommands:
|
|
|
163
191
|
pending_removal, from the gateway's desired-recipient-state
|
|
164
192
|
substrate), and (best-effort, this machine only) each principal's
|
|
165
193
|
local TOFU pin. stale_access names removed members whose access
|
|
166
|
-
|
|
167
|
-
not enforcement, until
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
194
|
+
has NOT yet been rotated away — pending_removal is honest
|
|
195
|
+
bookkeeping, not enforcement, until \`access repair\`/\`revoke-key\`
|
|
196
|
+
actually rotates. history_scope (which epochs each recipient can
|
|
197
|
+
read) is not reported by this read — see the \`gap\` field. An
|
|
198
|
+
enrolled teammate's key envelope is wrapped AUTOMATICALLY — no
|
|
199
|
+
manual step — by the next \`git push\` or \`repos snapshot\` any
|
|
200
|
+
key-holding client runs (best-effort, non-blocking; the retired
|
|
201
|
+
\`gitvault reconcile\` verb did this by hand and is REMOVED).
|
|
202
|
+
\`--human\` renders a compact roster instead of JSON (the read
|
|
203
|
+
form only — repair/revoke-key/declare-exposure stay JSON-only).
|
|
173
204
|
access repair
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
Epoch rotation (D193-D203, rev 42) with reason:"elective_rekey" —
|
|
206
|
+
re-keys this vault's CURRENT epoch away from every stale_access
|
|
207
|
+
principal at once, and clears a pre-existing vault's one-time
|
|
208
|
+
migration requirement. \`reconcile\`, the workaround this
|
|
209
|
+
replaces, is REMOVED (it never wrapped a key correctly-scoped to
|
|
210
|
+
"from here forward" — this does). Needs
|
|
211
|
+
--recipient-state-version and --recipient-revocation-version:
|
|
212
|
+
the gateway exposes no read route for these two counters outside
|
|
213
|
+
\`revoke-key\`'s own response, so this verb needs them supplied
|
|
214
|
+
explicitly today — refuses cleanly, naming exactly this, when
|
|
215
|
+
omitted. Owner + step-up.
|
|
216
|
+
access revoke-key <principal_id>
|
|
217
|
+
The ONE fully self-contained rotation entry point: declares
|
|
218
|
+
reason:"recipient_key_revoked" for one principal and rotates off
|
|
219
|
+
that declaration's OWN returned counters — no flags needed.
|
|
220
|
+
Owner + step-up. The rekey remedy for "this specific principal's
|
|
221
|
+
key should no longer be trusted."
|
|
222
|
+
access declare-exposure
|
|
223
|
+
Declares reason:"epoch_secret_exposed" for THIS vault
|
|
224
|
+
(vault-scoped, not org-wide) — the rekey remedy for a leaked
|
|
225
|
+
K_repo/K_e. The declaration itself lands immediately; the
|
|
226
|
+
follow-up rotation it authorizes is not auto-run (same counter
|
|
227
|
+
gap as \`access repair\`) — this prints exactly what to do next.
|
|
228
|
+
Owner + step-up.
|
|
180
229
|
policy Set the activation policy — \`required\` (a deploy must present a
|
|
181
230
|
vaulted capture) or \`grandfathered\` (it need not). Owner +
|
|
182
231
|
step-up, audited. \`grandfathered\` is the documented way out of a
|
|
@@ -195,9 +244,17 @@ Options:
|
|
|
195
244
|
--idempotency-key <key>
|
|
196
245
|
create: re-running with the same key resolves to the
|
|
197
246
|
same project instead of creating a second one — new
|
|
198
|
-
projects only (default: derived from the name)
|
|
199
|
-
|
|
200
|
-
|
|
247
|
+
projects only (default: derived from the name).
|
|
248
|
+
access repair/revoke-key: the rotation attempt's OWN
|
|
249
|
+
client_idempotency_key (32-hex) — default: a fresh
|
|
250
|
+
CSPRNG value each call, never resumed across processes.
|
|
251
|
+
--recipient-state-version <n>
|
|
252
|
+
--recipient-revocation-version <n>
|
|
253
|
+
access repair: the D194 frozen watermark pair this
|
|
254
|
+
rotation attempt is fenced against. Required — see
|
|
255
|
+
\`run402 repos access repair --help\` for why.
|
|
256
|
+
--human view/list/access/fsck/recover: a short summary on stdout
|
|
257
|
+
instead of the JSON dump. Rejected together with --json.
|
|
201
258
|
--force delete: proceed even though the repo holds generations
|
|
202
259
|
that would be permanently and irrecoverably lost. Never
|
|
203
260
|
overrides the non-repo-infrastructure refusal.
|
|
@@ -205,7 +262,19 @@ Options:
|
|
|
205
262
|
tree produces (a clean tree pushes HEAD itself, unused)
|
|
206
263
|
--checkpoint snapshot: force the checkpoint-bearing form regardless of delta size
|
|
207
264
|
--dry-run snapshot: a REAL preview — runs the actual local pipeline
|
|
208
|
-
and reports what would publish. Publishes nothing.
|
|
265
|
+
and reports what would publish. Publishes nothing. A
|
|
266
|
+
dirty tree still refuses SNAPSHOT_DIRTY_TREE here (a
|
|
267
|
+
preview that hid the refusal would lie).
|
|
268
|
+
--allow-dirty snapshot: capture a dirty tree as-is instead of refusing.
|
|
269
|
+
The result discloses exactly what was swept in
|
|
270
|
+
(modified_captured / untracked_captured) — even this
|
|
271
|
+
override never captures silently.
|
|
272
|
+
--manifest-out <path>
|
|
273
|
+
snapshot: write the complete captured-file inventory
|
|
274
|
+
(the full JSON the SDK returned, untouched) to a private
|
|
275
|
+
0600 file instead of stdout's default summary. The
|
|
276
|
+
printed result's manifest_path names it. Composes with
|
|
277
|
+
--dry-run and with -v/--verbose.
|
|
209
278
|
--off mirror: remove the configured destination (config only)
|
|
210
279
|
--backfill mirror: copy every object the configured mirror is missing
|
|
211
280
|
--profile <name> mirror / recover: the AWS credential profile name for an
|
|
@@ -234,6 +303,13 @@ Options:
|
|
|
234
303
|
signed completion appears, instead of returning immediately
|
|
235
304
|
--reason <why> policy: why the policy is changing — recorded in the
|
|
236
305
|
audit event. REQUIRED for \`grandfathered\`.
|
|
306
|
+
-v, --verbose Print one stderr summary line of this call's request
|
|
307
|
+
stats (round trips, wire time, bytes). Coexists with
|
|
308
|
+
--human. The JSON result always carries a \`stats\` block
|
|
309
|
+
regardless of this flag. On \`snapshot\`/\`snapshot
|
|
310
|
+
--dry-run\`, ALSO inlines the full captured-file
|
|
311
|
+
inventory in stdout's JSON (composes with the stats
|
|
312
|
+
line — both happen, not one or the other).
|
|
237
313
|
--json No-op: stdout is already JSON.
|
|
238
314
|
|
|
239
315
|
Terminal loss (protocol §0):
|
|
@@ -247,15 +323,17 @@ Examples:
|
|
|
247
323
|
run402 repos create --project prj_1a2b3c # allocate for an existing project
|
|
248
324
|
git push -u origin HEAD # the printed next_action, verbatim
|
|
249
325
|
run402 repos view --human
|
|
250
|
-
run402 repos list --org org_1a2b3c
|
|
326
|
+
run402 repos list --org org_1a2b3c --human
|
|
251
327
|
run402 repos rename my-notes --project prj_1a2b3c
|
|
252
328
|
run402 repos snapshot --dry-run
|
|
329
|
+
run402 repos snapshot --dry-run --manifest-out /tmp/snapshot-plan.json
|
|
330
|
+
run402 repos snapshot --allow-dirty
|
|
253
331
|
run402 repos mirror s3://acme-vault-mirror --profile acme
|
|
254
332
|
run402 repos mirror --backfill
|
|
255
|
-
run402 repos fsck --mirror
|
|
333
|
+
run402 repos fsck --mirror --human
|
|
256
334
|
run402 repos gc
|
|
257
|
-
run402 repos access
|
|
258
|
-
run402 repos recover s3://acme-vault-mirror --out ./restored
|
|
335
|
+
run402 repos access --human
|
|
336
|
+
run402 repos recover s3://acme-vault-mirror --out ./restored --human
|
|
259
337
|
run402 repos delete --project prj_xyz --force
|
|
260
338
|
`;
|
|
261
339
|
|
|
@@ -305,6 +383,19 @@ function printTerminalLoss(status) {
|
|
|
305
383
|
console.error("");
|
|
306
384
|
}
|
|
307
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Print a verb's JSON result with the always-on `stats` block (Observability:
|
|
388
|
+
* RUN402_TRACE + always-on stats + -v). `sdk.stats()` reflects only calls
|
|
389
|
+
* made through THIS `sdk` instance — every verb below resolves one `sdk =
|
|
390
|
+
* getSdk()` and reuses it for its own direct calls so the count is accurate
|
|
391
|
+
* for the work this function did; calls a shared cross-cutting helper
|
|
392
|
+
* (org/wallet resolution) makes through its own internal instance are not
|
|
393
|
+
* reflected (see `cli/lib/stats.mjs`'s doc comment).
|
|
394
|
+
*/
|
|
395
|
+
function printJson(sdk, payload) {
|
|
396
|
+
console.log(JSON.stringify({ ...payload, stats: sdkStats(sdk) }, null, 2));
|
|
397
|
+
}
|
|
398
|
+
|
|
308
399
|
const LARGE_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
309
400
|
|
|
310
401
|
/**
|
|
@@ -507,17 +598,17 @@ async function inferRepoName(dir) {
|
|
|
507
598
|
|
|
508
599
|
const CREATE_VALUE_FLAGS = ["--org", "--dir", "--tier", "--idempotency-key", "--project"];
|
|
509
600
|
|
|
510
|
-
async function printCreateResult({ projectId, vault, adopted, name }) {
|
|
601
|
+
async function printCreateResult({ sdk, projectId, vault, adopted, name, verboseArgv }) {
|
|
511
602
|
let address = null;
|
|
512
603
|
let orgSlug = null;
|
|
513
604
|
try {
|
|
514
605
|
const owningOrg = await resolveOwningOrgId(projectId);
|
|
515
|
-
const orgRecord = owningOrg ? await
|
|
606
|
+
const orgRecord = owningOrg ? await sdk.org(owningOrg).get() : null;
|
|
516
607
|
orgSlug = orgRecord?.slug ?? null;
|
|
517
608
|
if (orgSlug && name) {
|
|
518
609
|
const candidate = slugifyRepoName(name);
|
|
519
610
|
if (candidate) {
|
|
520
|
-
const named = await
|
|
611
|
+
const named = await sdk.projects.setRepoName(projectId, candidate);
|
|
521
612
|
address = gitvaultRemoteUrlForRepo(orgSlug, named.repo_name);
|
|
522
613
|
}
|
|
523
614
|
}
|
|
@@ -546,13 +637,13 @@ async function printCreateResult({ projectId, vault, adopted, name }) {
|
|
|
546
637
|
deployed: false,
|
|
547
638
|
next_actions: nextActions,
|
|
548
639
|
};
|
|
549
|
-
|
|
640
|
+
printJson(sdk, out);
|
|
550
641
|
console.error(
|
|
551
642
|
`project ${projectId} ${adopted ? "adopted" : "provisioned"}; repo ${vault.repo_id} ` +
|
|
552
643
|
(vault.deduplicated ? "already existed — nothing was re-allocated" : `allocated (genesis ${vault.genesis_sha256})`),
|
|
553
644
|
);
|
|
554
645
|
if (address) console.error(`address: ${address}`);
|
|
555
|
-
else if (!orgSlug) console.error("no named address yet — claim an org slug (run402 org slug <slug
|
|
646
|
+
else if (!orgSlug) console.error("no named address yet — claim an org slug (run402 org slug <slug>) to get run402::<slug>/<name> addresses");
|
|
556
647
|
else console.error(`no address claimed — run 'run402 repos rename <name> --project ${projectId}' to claim one`);
|
|
557
648
|
if (vault.remote) console.error(`remote '${vault.remote.name}' -> ${vault.remote.url} (${vault.remote.reason})`);
|
|
558
649
|
if (pushAction) console.error(`next: ${pushAction.command}`);
|
|
@@ -561,9 +652,11 @@ async function printCreateResult({ projectId, vault, adopted, name }) {
|
|
|
561
652
|
await printKeystoreLocation();
|
|
562
653
|
console.error("");
|
|
563
654
|
console.error("nothing was deployed — this is a vault-only repo. Deploy later with `run402 deploy apply`, or never.");
|
|
655
|
+
printVerboseStats(verboseArgv, sdk);
|
|
564
656
|
}
|
|
565
657
|
|
|
566
658
|
async function createAdopt(projectId, dir, a) {
|
|
659
|
+
const sdk = getSdk();
|
|
567
660
|
const orgId = flagValue(a, "--org") ?? await resolveOwningOrgId(projectId);
|
|
568
661
|
if (!orgId) {
|
|
569
662
|
fail({
|
|
@@ -574,14 +667,15 @@ async function createAdopt(projectId, dir, a) {
|
|
|
574
667
|
});
|
|
575
668
|
}
|
|
576
669
|
try {
|
|
577
|
-
const vault = await
|
|
578
|
-
await printCreateResult({ projectId, vault, adopted: true, name: null });
|
|
670
|
+
const vault = await sdk.gitvault.init({ org_id: orgId, project_id: projectId, repo_dir: dir });
|
|
671
|
+
await printCreateResult({ sdk, projectId, vault, adopted: true, name: null, verboseArgv: a });
|
|
579
672
|
} catch (err) {
|
|
580
673
|
reportSdkError(err);
|
|
581
674
|
}
|
|
582
675
|
}
|
|
583
676
|
|
|
584
677
|
async function createProvision(name, dir, a) {
|
|
678
|
+
const sdk = getSdk();
|
|
585
679
|
const tier = flagValue(a, "--tier") ?? "prototype";
|
|
586
680
|
const idempotencyKey = flagValue(a, "--idempotency-key") ?? `repos-create:${name}`;
|
|
587
681
|
// `optional: true` — a fresh wallet with no org yet is the cold-start path
|
|
@@ -593,7 +687,7 @@ async function createProvision(name, dir, a) {
|
|
|
593
687
|
let provisioned;
|
|
594
688
|
try {
|
|
595
689
|
provisioned = await withAutoApprove(() =>
|
|
596
|
-
|
|
690
|
+
sdk.projects.provision({ tier, name, ...(orgId ? { orgId } : {}), idempotencyKey }),
|
|
597
691
|
);
|
|
598
692
|
} catch (err) {
|
|
599
693
|
reportSdkError(err);
|
|
@@ -612,8 +706,8 @@ async function createProvision(name, dir, a) {
|
|
|
612
706
|
}
|
|
613
707
|
|
|
614
708
|
try {
|
|
615
|
-
const vault = await
|
|
616
|
-
await printCreateResult({ projectId: provisioned.project_id, vault, adopted: false, name });
|
|
709
|
+
const vault = await sdk.gitvault.init({ org_id: effectiveOrgId, project_id: provisioned.project_id, repo_dir: dir });
|
|
710
|
+
await printCreateResult({ sdk, projectId: provisioned.project_id, vault, adopted: false, name, verboseArgv: a });
|
|
617
711
|
} catch (err) {
|
|
618
712
|
reportSdkError(err);
|
|
619
713
|
}
|
|
@@ -621,7 +715,7 @@ async function createProvision(name, dir, a) {
|
|
|
621
715
|
|
|
622
716
|
async function create(args) {
|
|
623
717
|
const a = normalizeArgv(args);
|
|
624
|
-
assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--help", "-h"], CREATE_VALUE_FLAGS);
|
|
718
|
+
assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--help", "-h", "-v", "--verbose"], CREATE_VALUE_FLAGS);
|
|
625
719
|
const positionals = requirePositionalCount(a, CREATE_VALUE_FLAGS, {
|
|
626
720
|
min: 0, max: 1, command: "run402 repos create [name]", missing: "",
|
|
627
721
|
});
|
|
@@ -655,8 +749,8 @@ async function create(args) {
|
|
|
655
749
|
// ─── list ───────────────────────────────────────────────────────────────────
|
|
656
750
|
|
|
657
751
|
/** The FROZEN bulk-read shape (task 2.4) — one round trip. */
|
|
658
|
-
async function listViaBulkRead(orgId) {
|
|
659
|
-
const result = await
|
|
752
|
+
async function listViaBulkRead(sdk, orgId) {
|
|
753
|
+
const result = await sdk.gitvault.listByOrg(orgId);
|
|
660
754
|
return Array.isArray(result.vaults) ? result.vaults : [];
|
|
661
755
|
}
|
|
662
756
|
|
|
@@ -667,14 +761,14 @@ async function listViaBulkRead(orgId) {
|
|
|
667
761
|
* function once the bulk route has shipped long enough that no gateway
|
|
668
762
|
* still 404s it.
|
|
669
763
|
*/
|
|
670
|
-
async function listViaFallback(orgId) {
|
|
671
|
-
const result = await
|
|
764
|
+
async function listViaFallback(sdk, orgId) {
|
|
765
|
+
const result = await sdk.projects.list({ org: orgId });
|
|
672
766
|
const projects = Array.isArray(result.projects) ? result.projects : [];
|
|
673
767
|
const repos = [];
|
|
674
768
|
for (const p of projects) {
|
|
675
769
|
let status;
|
|
676
770
|
try {
|
|
677
|
-
status = await
|
|
771
|
+
status = await sdk.gitvault.status({ project_id: p.id });
|
|
678
772
|
} catch {
|
|
679
773
|
continue;
|
|
680
774
|
}
|
|
@@ -695,21 +789,38 @@ async function listViaFallback(orgId) {
|
|
|
695
789
|
return repos;
|
|
696
790
|
}
|
|
697
791
|
|
|
792
|
+
/** `repos list --human`: a compact roster — one line per repo (address, generation, bytes, policy). */
|
|
793
|
+
async function formatRepoListHuman(orgSlug, repos) {
|
|
794
|
+
if (repos.length === 0) return "(no vault-bearing repos in this organization)";
|
|
795
|
+
const { generationToBigInt } = await import("#sdk/node");
|
|
796
|
+
const decimal = (g) => (g ? generationToBigInt(g).toString() : "none");
|
|
797
|
+
const lines = repos.map((r) => {
|
|
798
|
+
const address = orgSlug && r.repo_name ? `run402::${orgSlug}/${r.repo_name}` : (r.repo_name ?? r.project_id);
|
|
799
|
+
return `${address} gen=${decimal(r.newest_generation)} ${r.source_bytes} byte(s) policy=${r.gitvault_policy ?? "(none)"} (${r.repo_id})`;
|
|
800
|
+
});
|
|
801
|
+
return lines.join("\n");
|
|
802
|
+
}
|
|
803
|
+
|
|
698
804
|
async function list(args) {
|
|
699
805
|
const a = normalizeArgv(args);
|
|
700
|
-
assertKnownFlags(a, ["--org", "--help", "-h"], ["--org"]);
|
|
806
|
+
assertKnownFlags(a, ["--org", "--human", "-v", "--verbose", "--help", "-h"], ["--org"]);
|
|
701
807
|
requirePositionalCount(a, ["--org"], { min: 0, max: 0, command: "run402 repos list", missing: "" });
|
|
808
|
+
const human = a.includes("--human");
|
|
809
|
+
if (human && a.includes("--json")) {
|
|
810
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
811
|
+
}
|
|
812
|
+
const sdk = getSdk();
|
|
702
813
|
const orgId = await resolveOrgId(a, { cmd: "repos" });
|
|
703
814
|
|
|
704
815
|
let repos;
|
|
705
816
|
let usedFallback = false;
|
|
706
817
|
try {
|
|
707
|
-
repos = await listViaBulkRead(orgId);
|
|
818
|
+
repos = await listViaBulkRead(sdk, orgId);
|
|
708
819
|
} catch (err) {
|
|
709
820
|
if (err?.status === 404) {
|
|
710
821
|
usedFallback = true;
|
|
711
822
|
try {
|
|
712
|
-
repos = await listViaFallback(orgId);
|
|
823
|
+
repos = await listViaFallback(sdk, orgId);
|
|
713
824
|
} catch (fallbackErr) {
|
|
714
825
|
reportSdkError(fallbackErr);
|
|
715
826
|
return;
|
|
@@ -723,42 +834,50 @@ async function list(args) {
|
|
|
723
834
|
let orgSlug = repos.find((r) => r.org_slug)?.org_slug ?? null;
|
|
724
835
|
if (orgSlug == null) {
|
|
725
836
|
try {
|
|
726
|
-
orgSlug = (await
|
|
837
|
+
orgSlug = (await sdk.org(orgId).get()).slug;
|
|
727
838
|
} catch {
|
|
728
839
|
// best-effort — `list` must not fail over an org-slug lookup
|
|
729
840
|
}
|
|
730
841
|
}
|
|
731
842
|
|
|
732
|
-
|
|
843
|
+
if (human) {
|
|
844
|
+
console.log(await formatRepoListHuman(orgSlug, repos));
|
|
845
|
+
printVerboseStats(a, sdk);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
printJson(sdk, { org_id: orgId, org_slug: orgSlug, repos });
|
|
733
849
|
console.error(`${repos.length} vault-bearing project(s) in this organization${usedFallback ? " (per-project fallback read — the bulk vaults-by-org route is not live on this gateway yet)" : ""}`);
|
|
734
850
|
if (orgSlug) console.error(`org slug: ${orgSlug} — a repo with a claimed address-form name is reachable at run402::${orgSlug}/<name>`);
|
|
851
|
+
printVerboseStats(a, sdk);
|
|
735
852
|
}
|
|
736
853
|
|
|
737
854
|
// ─── view ───────────────────────────────────────────────────────────────────
|
|
738
855
|
|
|
739
856
|
async function view(args) {
|
|
740
857
|
const a = normalizeArgv(args);
|
|
741
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
858
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
742
859
|
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos view", missing: "" });
|
|
743
860
|
const human = a.includes("--human");
|
|
744
861
|
if (human && a.includes("--json")) {
|
|
745
862
|
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
746
863
|
}
|
|
864
|
+
const sdk = getSdk();
|
|
747
865
|
const target = await vaultTarget(a);
|
|
748
866
|
try {
|
|
749
867
|
// Design D3: `view` NEVER passes `refs: true` — it is side-effect-free
|
|
750
868
|
// by construction, not by convention. Materialization belongs to `fsck`.
|
|
751
|
-
const s = await
|
|
869
|
+
const s = await sdk.gitvault.status(target);
|
|
752
870
|
let mirror = null;
|
|
753
871
|
if (s.repo_id) {
|
|
754
872
|
try {
|
|
755
|
-
mirror = await
|
|
873
|
+
mirror = await sdk.gitvault.mirrorStatus({ ...target, repo_id: s.repo_id });
|
|
756
874
|
} catch {
|
|
757
875
|
// best-effort — a mirror read failure never fails `view`
|
|
758
876
|
}
|
|
759
877
|
}
|
|
760
878
|
if (human) {
|
|
761
879
|
console.log(await formatRepoHuman(s, mirror));
|
|
880
|
+
printVerboseStats(a, sdk);
|
|
762
881
|
return;
|
|
763
882
|
}
|
|
764
883
|
const verifyRefsAction = nextAction("verify_refs", { command: "run402 repos fsck", why: "Walk the signed chain and materialize verified refs." });
|
|
@@ -769,7 +888,7 @@ async function view(args) {
|
|
|
769
888
|
mirror,
|
|
770
889
|
next_actions: combinedNextActions,
|
|
771
890
|
};
|
|
772
|
-
|
|
891
|
+
printJson(sdk, out);
|
|
773
892
|
printTerminalLoss(s);
|
|
774
893
|
if (s.remote) {
|
|
775
894
|
const suffix =
|
|
@@ -787,6 +906,7 @@ async function view(args) {
|
|
|
787
906
|
}
|
|
788
907
|
for (const w of s.warnings) console.error(`warning (${w.kind}): ${w.message}`);
|
|
789
908
|
for (const n of combinedNextActions) console.error(`next: ${n.why ?? n.action ?? n.type}${n.command ? ` — ${n.command}` : ""}`);
|
|
909
|
+
printVerboseStats(a, sdk);
|
|
790
910
|
} catch (err) {
|
|
791
911
|
reportSdkError(err);
|
|
792
912
|
}
|
|
@@ -796,11 +916,12 @@ async function view(args) {
|
|
|
796
916
|
|
|
797
917
|
async function rename(args) {
|
|
798
918
|
const a = normalizeArgv(args);
|
|
799
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
919
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
800
920
|
const [repoName] = requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
801
921
|
min: 1, max: 1, command: "run402 repos rename <new_name> [--repo <repo_id> | --project <project_id>]",
|
|
802
922
|
missing: "run402 repos rename <new_name>: a new name is required",
|
|
803
923
|
});
|
|
924
|
+
const sdk = getSdk();
|
|
804
925
|
const repoFlag = flagValue(a, "--repo");
|
|
805
926
|
const projectFlag = flagValue(a, "--project");
|
|
806
927
|
if (repoFlag != null && projectFlag != null) {
|
|
@@ -809,7 +930,7 @@ async function rename(args) {
|
|
|
809
930
|
let projectId;
|
|
810
931
|
if (repoFlag != null) {
|
|
811
932
|
try {
|
|
812
|
-
projectId = (await
|
|
933
|
+
projectId = (await sdk.gitvault.get(repoFlag)).project_id;
|
|
813
934
|
} catch (err) {
|
|
814
935
|
reportSdkError(err);
|
|
815
936
|
return;
|
|
@@ -818,16 +939,16 @@ async function rename(args) {
|
|
|
818
939
|
projectId = resolveProjectId(projectFlag);
|
|
819
940
|
}
|
|
820
941
|
try {
|
|
821
|
-
const result = await
|
|
942
|
+
const result = await sdk.projects.setRepoName(projectId, repoName);
|
|
822
943
|
let address = null;
|
|
823
944
|
try {
|
|
824
945
|
const owningOrg = await resolveOwningOrgId(projectId);
|
|
825
|
-
const orgSlug = owningOrg ? (await
|
|
946
|
+
const orgSlug = owningOrg ? (await sdk.org(owningOrg).get()).slug : null;
|
|
826
947
|
if (orgSlug) address = gitvaultRemoteUrlForRepo(orgSlug, result.repo_name);
|
|
827
948
|
} catch {
|
|
828
949
|
// The claim itself already succeeded — a failed address-preview lookup is never fatal.
|
|
829
950
|
}
|
|
830
|
-
|
|
951
|
+
printJson(sdk, { ...result, address });
|
|
831
952
|
console.error(
|
|
832
953
|
result.previous_repo_name && result.previous_repo_name !== result.repo_name
|
|
833
954
|
? `renamed from "${result.previous_repo_name}" to "${result.repo_name}"`
|
|
@@ -835,6 +956,7 @@ async function rename(args) {
|
|
|
835
956
|
);
|
|
836
957
|
if (address) console.error(`address: ${address}`);
|
|
837
958
|
else console.error("this org has no slug yet — claim one with `run402 org slug <slug>` to get a full run402::<slug>/<name> address");
|
|
959
|
+
printVerboseStats(a, sdk);
|
|
838
960
|
} catch (err) {
|
|
839
961
|
reportSdkError(err);
|
|
840
962
|
}
|
|
@@ -863,22 +985,22 @@ async function checkResource(read, resourceName, countOf) {
|
|
|
863
985
|
* that fails for a reason OTHER than "genuinely absent" (404) is reported
|
|
864
986
|
* `unknown` and REFUSES delete too — D9 never guesses its way to yes.
|
|
865
987
|
*/
|
|
866
|
-
async function checkNonRepoResources(projectId) {
|
|
988
|
+
async function checkNonRepoResources(sdk, projectId) {
|
|
867
989
|
const refused = [];
|
|
868
990
|
try {
|
|
869
|
-
const detail = await
|
|
991
|
+
const detail = await sdk.projects.get(projectId);
|
|
870
992
|
if (Array.isArray(detail.mailbox) && detail.mailbox.length > 0) refused.push({ resource: "mailbox", status: "present", count: detail.mailbox.length });
|
|
871
993
|
if (Array.isArray(detail.custom_domains) && detail.custom_domains.length > 0) refused.push({ resource: "custom_domains", status: "present", count: detail.custom_domains.length });
|
|
872
994
|
} catch (err) {
|
|
873
995
|
refused.push({ resource: "project_detail", status: "unknown", reason: err?.message ?? String(err) });
|
|
874
996
|
}
|
|
875
|
-
const schema = await checkResource(() =>
|
|
997
|
+
const schema = await checkResource(() => sdk.projects.getSchema(projectId), "database_schema", (s) => (Array.isArray(s?.tables) ? s.tables.length : 0));
|
|
876
998
|
if (schema) refused.push(schema);
|
|
877
|
-
const functions = await checkResource(() =>
|
|
999
|
+
const functions = await checkResource(() => sdk.functions.list(projectId), "functions", (r) => (Array.isArray(r?.functions) ? r.functions.length : 0));
|
|
878
1000
|
if (functions) refused.push(functions);
|
|
879
|
-
const secrets = await checkResource(() =>
|
|
1001
|
+
const secrets = await checkResource(() => sdk.secrets.list(projectId), "secrets", (r) => (Array.isArray(r?.secrets) ? r.secrets.length : 0));
|
|
880
1002
|
if (secrets) refused.push(secrets);
|
|
881
|
-
const subdomains = await checkResource(() =>
|
|
1003
|
+
const subdomains = await checkResource(() => sdk.subdomains.list(projectId), "subdomains", (r) => (Array.isArray(r) ? r.length : 0));
|
|
882
1004
|
if (subdomains) refused.push(subdomains);
|
|
883
1005
|
return refused;
|
|
884
1006
|
}
|
|
@@ -893,7 +1015,8 @@ function stripFlag(args, flag) {
|
|
|
893
1015
|
|
|
894
1016
|
async function del(args) {
|
|
895
1017
|
const a = normalizeArgv(args);
|
|
896
|
-
assertKnownFlags(a, ["--project", "--repo", "--force", "--help", "-h"], ["--project", "--repo"]);
|
|
1018
|
+
assertKnownFlags(a, ["--project", "--repo", "--force", "-v", "--verbose", "--help", "-h"], ["--project", "--repo"]);
|
|
1019
|
+
const sdk = getSdk();
|
|
897
1020
|
const repoFlag = flagValue(a, "--repo");
|
|
898
1021
|
let projectId;
|
|
899
1022
|
let rest;
|
|
@@ -902,7 +1025,7 @@ async function del(args) {
|
|
|
902
1025
|
fail({ code: "BAD_USAGE", message: "pass --repo or --project, not both." });
|
|
903
1026
|
}
|
|
904
1027
|
try {
|
|
905
|
-
projectId = (await
|
|
1028
|
+
projectId = (await sdk.gitvault.get(repoFlag)).project_id;
|
|
906
1029
|
} catch (err) {
|
|
907
1030
|
reportSdkError(err);
|
|
908
1031
|
return;
|
|
@@ -916,7 +1039,7 @@ async function del(args) {
|
|
|
916
1039
|
|
|
917
1040
|
let status;
|
|
918
1041
|
try {
|
|
919
|
-
status = await
|
|
1042
|
+
status = await sdk.gitvault.status({ project_id: projectId });
|
|
920
1043
|
} catch (err) {
|
|
921
1044
|
reportSdkError(err);
|
|
922
1045
|
return;
|
|
@@ -927,7 +1050,7 @@ async function del(args) {
|
|
|
927
1050
|
|
|
928
1051
|
// D9, checked FIRST and unconditionally: --force below overrides only the
|
|
929
1052
|
// vault-history confirmation, never this refusal.
|
|
930
|
-
const refusedResources = await checkNonRepoResources(projectId);
|
|
1053
|
+
const refusedResources = await checkNonRepoResources(sdk, projectId);
|
|
931
1054
|
if (refusedResources.length > 0) {
|
|
932
1055
|
fail({
|
|
933
1056
|
code: "PROJECT_HAS_NON_REPO_RESOURCES",
|
|
@@ -957,13 +1080,14 @@ async function del(args) {
|
|
|
957
1080
|
}
|
|
958
1081
|
|
|
959
1082
|
try {
|
|
960
|
-
await
|
|
961
|
-
|
|
1083
|
+
await sdk.projects.delete(projectId);
|
|
1084
|
+
printJson(sdk, {
|
|
962
1085
|
project_id: projectId,
|
|
963
1086
|
deleted: true,
|
|
964
1087
|
deleted_resources: ["project", ...(vault ? ["vault_history"] : [])],
|
|
965
1088
|
vault: vault ? { repo_id: status.repo_id, admitted_generations: admittedGenerations, source_bytes: sourceBytes } : null,
|
|
966
|
-
}
|
|
1089
|
+
});
|
|
1090
|
+
printVerboseStats(a, sdk);
|
|
967
1091
|
} catch (err) {
|
|
968
1092
|
reportSdkError(err);
|
|
969
1093
|
}
|
|
@@ -971,7 +1095,7 @@ async function del(args) {
|
|
|
971
1095
|
|
|
972
1096
|
// ─── snapshot ───────────────────────────────────────────────────────────────
|
|
973
1097
|
|
|
974
|
-
const SNAPSHOT_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--message"];
|
|
1098
|
+
const SNAPSHOT_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--message", "--manifest-out"];
|
|
975
1099
|
|
|
976
1100
|
/**
|
|
977
1101
|
* When neither `--repo` nor `--project` was given explicitly, look at the
|
|
@@ -998,12 +1122,94 @@ async function detectSlugFormRemote(a, repoDir) {
|
|
|
998
1122
|
return null;
|
|
999
1123
|
}
|
|
1000
1124
|
|
|
1125
|
+
/**
|
|
1126
|
+
* Dirty-tree disclosure (help people not make mistakes): even an explicit
|
|
1127
|
+
* `--allow-dirty` override never captures silently — every modified/staged
|
|
1128
|
+
* tracked path and every untracked-not-ignored path that got swept into the
|
|
1129
|
+
* capture is named on stderr, one per line.
|
|
1130
|
+
*/
|
|
1131
|
+
function printDirtyDisclosure(snapshot) {
|
|
1132
|
+
if (!snapshot) return;
|
|
1133
|
+
for (const p of snapshot.modified_captured ?? []) console.error(`captured (modified): ${p}`);
|
|
1134
|
+
for (const p of snapshot.untracked_captured ?? []) console.error(`captured (untracked): ${p}`);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** How many `changed_paths` entries {@link summarizeSnapshotPayload} inlines before capping. */
|
|
1138
|
+
const SNAPSHOT_CHANGED_PATHS_CAP = 200;
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* item 1 (dogfood): `snapshot.captured` — the SDK's full captured-file
|
|
1142
|
+
* inventory (every tracked + untracked-not-ignored path in the repo, always
|
|
1143
|
+
* populated regardless of how small the actual push delta is) — is a
|
|
1144
|
+
* multi-thousand-line flood on a real repo, even when what actually
|
|
1145
|
+
* publishes is a handful of kilobytes. The SDK keeps returning it in full
|
|
1146
|
+
* (thin-shim law: other SDK consumers may want it) — this reshapes ONLY the
|
|
1147
|
+
* CLI's own stdout, by default:
|
|
1148
|
+
*
|
|
1149
|
+
* - `files_total` / `files_changed` / `files_new` — counts. `files_total`
|
|
1150
|
+
* is `captured.length`; `files_changed`/`files_new` are
|
|
1151
|
+
* `modified_captured`/`untracked_captured` — the ONLY per-path drift
|
|
1152
|
+
* this data distinguishes (the `--allow-dirty` sweep-in disclosure).
|
|
1153
|
+
* On the common clean-tree path both are empty, so `changed_paths` is
|
|
1154
|
+
* too — there is no `files_deleted` here, because `captured` only
|
|
1155
|
+
* lists paths PRESENT on disk today; nothing in this data names which
|
|
1156
|
+
* paths a plain clean push's new commits touched.
|
|
1157
|
+
* - `changed_paths` — `modified_captured` ∪ `untracked_captured`,
|
|
1158
|
+
* sorted, capped at `SNAPSHOT_CHANGED_PATHS_CAP`; `changed_more` names
|
|
1159
|
+
* the overflow explicitly rather than truncating silently.
|
|
1160
|
+
* - `snapshot.captured` / `.paths` / `.modified_captured` /
|
|
1161
|
+
* `.untracked_captured` are dropped from the default `snapshot` object
|
|
1162
|
+
* (its other scalar fields — kind, oid, tree_oid, head, head_oid,
|
|
1163
|
+
* captured_digest, top_level, global_excludes_path — stay). `verbose`
|
|
1164
|
+
* restores them (composes with the summary fields, does not replace
|
|
1165
|
+
* them) — the `-v`/`--verbose` flag already means "print a stats
|
|
1166
|
+
* line"; on `snapshot --dry-run`/`snapshot` it ALSO inlines the full
|
|
1167
|
+
* inventory.
|
|
1168
|
+
* - `manifest_path` is `null` unless `--manifest-out <path>` wrote the
|
|
1169
|
+
* COMPLETE, untouched payload to that file — see `writeManifestOut`.
|
|
1170
|
+
*/
|
|
1171
|
+
function summarizeSnapshotPayload(payload, { verbose = false, manifestPath = null } = {}) {
|
|
1172
|
+
const out = { ...payload, manifest_path: manifestPath };
|
|
1173
|
+
const snapshot = payload.snapshot;
|
|
1174
|
+
if (!snapshot) return out;
|
|
1175
|
+
const modified = snapshot.modified_captured ?? [];
|
|
1176
|
+
const untracked = snapshot.untracked_captured ?? [];
|
|
1177
|
+
const changedAll = [...modified, ...untracked].sort();
|
|
1178
|
+
const changedPaths = changedAll.slice(0, SNAPSHOT_CHANGED_PATHS_CAP);
|
|
1179
|
+
out.files_total = Array.isArray(snapshot.captured) ? snapshot.captured.length : 0;
|
|
1180
|
+
out.files_changed = modified.length;
|
|
1181
|
+
out.files_new = untracked.length;
|
|
1182
|
+
out.changed_paths = changedPaths;
|
|
1183
|
+
out.changed_more = changedAll.length - changedPaths.length;
|
|
1184
|
+
if (!verbose) {
|
|
1185
|
+
const { captured, paths, modified_captured, untracked_captured, ...trimmedSnapshot } = snapshot;
|
|
1186
|
+
out.snapshot = trimmedSnapshot;
|
|
1187
|
+
}
|
|
1188
|
+
return out;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/** `--manifest-out <path>`: write the COMPLETE, untouched plan/push payload — the full captured-file inventory included — to a private 0600 file. */
|
|
1192
|
+
function writeManifestOut(path, payload) {
|
|
1193
|
+
try {
|
|
1194
|
+
writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
fail({
|
|
1197
|
+
code: "MANIFEST_OUT_WRITE_FAILED",
|
|
1198
|
+
message: `could not write the full snapshot inventory to ${path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
1199
|
+
hint: "Check that the path is writable and its parent directory exists.",
|
|
1200
|
+
details: { path },
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1001
1205
|
async function snapshot(args) {
|
|
1002
1206
|
const a = normalizeArgv(args);
|
|
1003
|
-
assertKnownFlags(a, [...SNAPSHOT_VALUE_FLAGS, "--checkpoint", "--dry-run", "--help", "-h"], SNAPSHOT_VALUE_FLAGS);
|
|
1207
|
+
assertKnownFlags(a, [...SNAPSHOT_VALUE_FLAGS, "--checkpoint", "--dry-run", "--allow-dirty", "-v", "--verbose", "--help", "-h"], SNAPSHOT_VALUE_FLAGS);
|
|
1004
1208
|
requirePositionalCount(a, SNAPSHOT_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos snapshot", missing: "" });
|
|
1209
|
+
const sdk = getSdk();
|
|
1005
1210
|
const dryRun = a.includes("--dry-run");
|
|
1006
1211
|
const message = flagValue(a, "--message");
|
|
1212
|
+
const allowDirty = a.includes("--allow-dirty");
|
|
1007
1213
|
const repoDir = process.cwd();
|
|
1008
1214
|
const address = await detectSlugFormRemote(a, repoDir);
|
|
1009
1215
|
const target = address ? { repo_dir: repoDir } : await vaultTarget(a);
|
|
@@ -1021,12 +1227,18 @@ async function snapshot(args) {
|
|
|
1021
1227
|
console.error("");
|
|
1022
1228
|
},
|
|
1023
1229
|
};
|
|
1024
|
-
|
|
1230
|
+
const snapshotOpts = {};
|
|
1231
|
+
if (message != null) snapshotOpts.message = message;
|
|
1232
|
+
if (allowDirty) snapshotOpts.allowDirty = true;
|
|
1233
|
+
if (Object.keys(snapshotOpts).length > 0) opts.snapshot = snapshotOpts;
|
|
1025
1234
|
if (a.includes("--checkpoint")) opts.checkpoint = true;
|
|
1235
|
+
const manifestOutPath = flagValue(a, "--manifest-out");
|
|
1236
|
+
const verbose = isVerbose(a);
|
|
1026
1237
|
try {
|
|
1027
1238
|
if (dryRun) {
|
|
1028
|
-
const plan = await
|
|
1029
|
-
|
|
1239
|
+
const plan = await sdk.gitvault.planPush(opts);
|
|
1240
|
+
if (manifestOutPath != null) writeManifestOut(manifestOutPath, plan);
|
|
1241
|
+
printJson(sdk, summarizeSnapshotPayload(plan, { verbose, manifestPath: manifestOutPath }));
|
|
1030
1242
|
if (plan.allocation_needed) {
|
|
1031
1243
|
console.error("dry-run: no repo allocated for this project yet — a real snapshot would allocate one first; object/byte sizing is not knowable until then");
|
|
1032
1244
|
} else {
|
|
@@ -1035,16 +1247,21 @@ async function snapshot(args) {
|
|
|
1035
1247
|
`${plan.object_count} object(s), ${plan.encrypted_bytes} encrypted byte(s) (${plan.raw_bytes} raw)`,
|
|
1036
1248
|
);
|
|
1037
1249
|
}
|
|
1250
|
+
printDirtyDisclosure(plan.snapshot);
|
|
1251
|
+
printVerboseStats(a, sdk);
|
|
1038
1252
|
return;
|
|
1039
1253
|
}
|
|
1040
|
-
const result = await
|
|
1041
|
-
|
|
1254
|
+
const result = await sdk.gitvault.push(opts);
|
|
1255
|
+
if (manifestOutPath != null) writeManifestOut(manifestOutPath, result);
|
|
1256
|
+
printJson(sdk, summarizeSnapshotPayload(result, { verbose, manifestPath: manifestOutPath }));
|
|
1042
1257
|
console.error(`published generation ${result.generation} (${result.form})`);
|
|
1043
1258
|
if (result.mirror_push?.outcome === "pushed") {
|
|
1044
1259
|
console.error(`mirror: pushed generation ${result.generation} (${result.mirror_push.summary?.objects_copied ?? 0} object(s) copied)`);
|
|
1045
1260
|
} else if (result.mirror_push?.outcome === "failed") {
|
|
1046
1261
|
console.error(`mirror: dual-push FAILED (deploy is unaffected) — ${result.mirror_push.error ?? "see mirror_push.summary.errors"}`);
|
|
1047
1262
|
}
|
|
1263
|
+
printDirtyDisclosure(result.snapshot);
|
|
1264
|
+
printVerboseStats(a, sdk);
|
|
1048
1265
|
} catch (err) {
|
|
1049
1266
|
reportSdkError(err);
|
|
1050
1267
|
}
|
|
@@ -1055,7 +1272,7 @@ async function snapshot(args) {
|
|
|
1055
1272
|
async function policy(args) {
|
|
1056
1273
|
const a = normalizeArgv(args);
|
|
1057
1274
|
const valueFlags = [...COMMON_VALUE_FLAGS, "--reason"];
|
|
1058
|
-
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
1275
|
+
assertKnownFlags(a, [...valueFlags, "-v", "--verbose", "--help", "-h"], valueFlags);
|
|
1059
1276
|
const [requested] = requirePositionalCount(a, valueFlags, {
|
|
1060
1277
|
min: 1, max: 1, command: "run402 repos policy <required|grandfathered>",
|
|
1061
1278
|
missing: "Missing <policy>. Expected `required` or `grandfathered`.",
|
|
@@ -1083,13 +1300,14 @@ async function policy(args) {
|
|
|
1083
1300
|
const sdk = getSdk();
|
|
1084
1301
|
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
1085
1302
|
const result = await sdk.gitvault.setPolicy(repoId, { gitvault_policy: requested, ...(reason != null ? { reason } : {}) });
|
|
1086
|
-
|
|
1303
|
+
printJson(sdk, { repo_id: repoId, ...result });
|
|
1087
1304
|
console.error(
|
|
1088
1305
|
result.changed
|
|
1089
1306
|
? `gitvault_policy is now ${result.gitvault_policy} (version ${result.gitvault_policy_version})`
|
|
1090
1307
|
: `gitvault_policy was already ${result.gitvault_policy} — nothing changed`,
|
|
1091
1308
|
);
|
|
1092
1309
|
for (const w of result.warnings ?? []) console.error(`warning (${w.kind}): ${w.message}`);
|
|
1310
|
+
printVerboseStats(a, sdk);
|
|
1093
1311
|
} catch (err) {
|
|
1094
1312
|
reportSdkError(err);
|
|
1095
1313
|
}
|
|
@@ -1099,10 +1317,11 @@ async function policy(args) {
|
|
|
1099
1317
|
|
|
1100
1318
|
const MIRROR_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--profile", "--region", "--endpoint"];
|
|
1101
1319
|
|
|
1102
|
-
async function mirrorRead(target) {
|
|
1320
|
+
async function mirrorRead(target, a) {
|
|
1321
|
+
const sdk = getSdk();
|
|
1103
1322
|
try {
|
|
1104
|
-
const result = await
|
|
1105
|
-
|
|
1323
|
+
const result = await sdk.gitvault.mirrorStatus(target);
|
|
1324
|
+
printJson(sdk, result);
|
|
1106
1325
|
if (!result.configured) {
|
|
1107
1326
|
console.error(`no mirror configured for ${result.repo_id}. Configure one: run402 repos mirror <destination>`);
|
|
1108
1327
|
} else {
|
|
@@ -1110,49 +1329,55 @@ async function mirrorRead(target) {
|
|
|
1110
1329
|
console.error(`mirror ${result.destination}: mirrored generation ${result.mirrored_generation ?? "(none)"}, vault newest ${result.newest_generation ?? "(none)"} — ${currency}`);
|
|
1111
1330
|
}
|
|
1112
1331
|
printMirrorHonesty(result);
|
|
1332
|
+
printVerboseStats(a, sdk);
|
|
1113
1333
|
} catch (err) {
|
|
1114
1334
|
reportSdkError(err);
|
|
1115
1335
|
}
|
|
1116
1336
|
}
|
|
1117
1337
|
|
|
1118
1338
|
async function mirrorSet(target, destination, a) {
|
|
1339
|
+
const sdk = getSdk();
|
|
1119
1340
|
const credential = resolveMirrorCredential(a);
|
|
1120
1341
|
const region = flagValue(a, "--region");
|
|
1121
1342
|
const endpoint = flagValue(a, "--endpoint");
|
|
1122
1343
|
try {
|
|
1123
|
-
const result = await
|
|
1344
|
+
const result = await sdk.gitvault.mirrorSet({
|
|
1124
1345
|
...target,
|
|
1125
1346
|
destination_url: destination,
|
|
1126
1347
|
...(credential ? { credential } : {}),
|
|
1127
1348
|
...(region != null ? { region } : {}),
|
|
1128
1349
|
...(endpoint != null ? { endpoint } : {}),
|
|
1129
1350
|
});
|
|
1130
|
-
|
|
1351
|
+
printJson(sdk, result);
|
|
1131
1352
|
console.error(`mirror configured for ${result.repo_id} -> ${formatMirrorDestination(result.destination)}`);
|
|
1132
1353
|
console.error("run `run402 repos mirror --backfill` to catch it up now, then every publish dual-pushes automatically.");
|
|
1354
|
+
printVerboseStats(a, sdk);
|
|
1133
1355
|
} catch (err) {
|
|
1134
1356
|
reportSdkError(err);
|
|
1135
1357
|
}
|
|
1136
1358
|
}
|
|
1137
1359
|
|
|
1138
|
-
async function mirrorOff(target) {
|
|
1360
|
+
async function mirrorOff(target, a) {
|
|
1361
|
+
const sdk = getSdk();
|
|
1139
1362
|
try {
|
|
1140
|
-
const result = await
|
|
1141
|
-
|
|
1363
|
+
const result = await sdk.gitvault.mirrorRemove(target);
|
|
1364
|
+
printJson(sdk, result);
|
|
1142
1365
|
console.error(
|
|
1143
1366
|
result.removed
|
|
1144
1367
|
? `mirror config removed for ${result.repo_id} — the mirror's OWN bytes were not touched`
|
|
1145
1368
|
: `no mirror was configured for ${result.repo_id} — nothing to remove`,
|
|
1146
1369
|
);
|
|
1370
|
+
printVerboseStats(a, sdk);
|
|
1147
1371
|
} catch (err) {
|
|
1148
1372
|
reportSdkError(err);
|
|
1149
1373
|
}
|
|
1150
1374
|
}
|
|
1151
1375
|
|
|
1152
|
-
async function mirrorBackfill(target) {
|
|
1376
|
+
async function mirrorBackfill(target, a) {
|
|
1377
|
+
const sdk = getSdk();
|
|
1153
1378
|
try {
|
|
1154
|
-
const result = await
|
|
1155
|
-
|
|
1379
|
+
const result = await sdk.gitvault.mirrorSync(target);
|
|
1380
|
+
printJson(sdk, result);
|
|
1156
1381
|
await spillIfLarge(result.repo_id, "mirror-backfill", result);
|
|
1157
1382
|
console.error(
|
|
1158
1383
|
`mirror backfill for ${result.repo_id}: ${result.objects_copied} copied, ${result.objects_already_present} already present` +
|
|
@@ -1161,6 +1386,7 @@ async function mirrorBackfill(target) {
|
|
|
1161
1386
|
);
|
|
1162
1387
|
for (const e of result.errors) console.error(` failed: ${e.key} — ${e.error}`);
|
|
1163
1388
|
printMirrorHonesty(result);
|
|
1389
|
+
printVerboseStats(a, sdk);
|
|
1164
1390
|
} catch (err) {
|
|
1165
1391
|
reportSdkError(err);
|
|
1166
1392
|
}
|
|
@@ -1168,7 +1394,7 @@ async function mirrorBackfill(target) {
|
|
|
1168
1394
|
|
|
1169
1395
|
async function mirror(args) {
|
|
1170
1396
|
const a = normalizeArgv(args);
|
|
1171
|
-
assertKnownFlags(a, [...MIRROR_VALUE_FLAGS, "--off", "--backfill", "--ambient", "--help", "-h"], MIRROR_VALUE_FLAGS);
|
|
1397
|
+
assertKnownFlags(a, [...MIRROR_VALUE_FLAGS, "--off", "--backfill", "--ambient", "-v", "--verbose", "--help", "-h"], MIRROR_VALUE_FLAGS);
|
|
1172
1398
|
const positionals = requirePositionalCount(a, MIRROR_VALUE_FLAGS, {
|
|
1173
1399
|
min: 0, max: 1, command: "run402 repos mirror [<destination>]", missing: "",
|
|
1174
1400
|
});
|
|
@@ -1185,26 +1411,57 @@ async function mirror(args) {
|
|
|
1185
1411
|
}
|
|
1186
1412
|
const target = await vaultTarget(a);
|
|
1187
1413
|
if (destination != null) return mirrorSet(target, destination, a);
|
|
1188
|
-
if (off) return mirrorOff(target);
|
|
1189
|
-
if (backfill) return mirrorBackfill(target);
|
|
1190
|
-
return mirrorRead(target);
|
|
1414
|
+
if (off) return mirrorOff(target, a);
|
|
1415
|
+
if (backfill) return mirrorBackfill(target, a);
|
|
1416
|
+
return mirrorRead(target, a);
|
|
1191
1417
|
}
|
|
1192
1418
|
|
|
1193
1419
|
// ─── fsck (verify the head chain + materialize refs) ──────────────────
|
|
1194
1420
|
|
|
1421
|
+
/** `repos fsck --human`: the same verdict the stderr lines already carry, condensed into one block. */
|
|
1422
|
+
function formatFsckHuman(result, mirrorRequested) {
|
|
1423
|
+
const lines = [`Repo: ${result.repo_id}`];
|
|
1424
|
+
lines.push(
|
|
1425
|
+
!result.write
|
|
1426
|
+
? `Verified through generation ${result.verified_to_generation} — audit mode, nothing local was persisted.`
|
|
1427
|
+
: result.local_state_changed
|
|
1428
|
+
? `Verified through generation ${result.verified_to_generation} — local pin advanced from ${result.pin_before.highest_authenticated ?? "genesis"} to ${result.pin_after.highest_authenticated}.`
|
|
1429
|
+
: `Verified through generation ${result.verified_to_generation} — already at the newest verified generation.`,
|
|
1430
|
+
);
|
|
1431
|
+
if (mirrorRequested && result.mirror) {
|
|
1432
|
+
lines.push(
|
|
1433
|
+
`Mirror: recoverable generation ${result.mirror.recovered_generation}` +
|
|
1434
|
+
(result.mirror.chain_break ? ` (chain break at ${result.mirror.chain_break.generation}: ${result.mirror.chain_break.reason})` : "") +
|
|
1435
|
+
(result.mirror.data_loss_detected ? ` — DATA LOSS DETECTED (${result.mirror.absences.filter((x) => x.adjudication === "unexplained_absence").length} unexplained absence(s))` : ""),
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
return lines.join("\n");
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1195
1441
|
async function fsck(args) {
|
|
1196
1442
|
const a = normalizeArgv(args);
|
|
1197
1443
|
const valueFlags = [...COMMON_VALUE_FLAGS, "--budget"];
|
|
1198
|
-
assertKnownFlags(a, [...valueFlags, "--mirror", "--no-write", "--help", "-h"], valueFlags);
|
|
1444
|
+
assertKnownFlags(a, [...valueFlags, "--mirror", "--no-write", "--human", "-v", "--verbose", "--help", "-h"], valueFlags);
|
|
1199
1445
|
requirePositionalCount(a, valueFlags, { min: 0, max: 0, command: "run402 repos fsck", missing: "" });
|
|
1446
|
+
const human = a.includes("--human");
|
|
1447
|
+
if (human && a.includes("--json")) {
|
|
1448
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
1449
|
+
}
|
|
1450
|
+
const sdk = getSdk();
|
|
1200
1451
|
const target = await vaultTarget(a);
|
|
1201
1452
|
const budget = flagValue(a, "--budget");
|
|
1202
1453
|
if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
|
|
1203
1454
|
const write = !a.includes("--no-write");
|
|
1204
1455
|
const mirrorRequested = a.includes("--mirror");
|
|
1205
1456
|
try {
|
|
1206
|
-
const result = await
|
|
1207
|
-
|
|
1457
|
+
const result = await sdk.gitvault.fsck({ ...target, write, mirror: mirrorRequested });
|
|
1458
|
+
if (human) {
|
|
1459
|
+
console.log(formatFsckHuman(result, mirrorRequested));
|
|
1460
|
+
if (mirrorRequested && result.mirror) printMirrorHonesty(result.mirror);
|
|
1461
|
+
printVerboseStats(a, sdk);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
printJson(sdk, result);
|
|
1208
1465
|
await spillIfLarge(result.repo_id, "fsck", result);
|
|
1209
1466
|
if (!write) {
|
|
1210
1467
|
console.error(`--no-write: verified through generation ${result.verified_to_generation} — nothing local was persisted (pin_before === pin_after).`);
|
|
@@ -1220,6 +1477,7 @@ async function fsck(args) {
|
|
|
1220
1477
|
}
|
|
1221
1478
|
printMirrorHonesty(result.mirror);
|
|
1222
1479
|
}
|
|
1480
|
+
printVerboseStats(a, sdk);
|
|
1223
1481
|
} catch (err) {
|
|
1224
1482
|
reportSdkError(err);
|
|
1225
1483
|
}
|
|
@@ -1231,8 +1489,9 @@ const GC_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--intent-core", "--verifier-rece
|
|
|
1231
1489
|
|
|
1232
1490
|
async function gc(args) {
|
|
1233
1491
|
const a = normalizeArgv(args);
|
|
1234
|
-
assertKnownFlags(a, [...GC_VALUE_FLAGS, "--submit", "--wait", "--help", "-h"], GC_VALUE_FLAGS);
|
|
1492
|
+
assertKnownFlags(a, [...GC_VALUE_FLAGS, "--submit", "--wait", "-v", "--verbose", "--help", "-h"], GC_VALUE_FLAGS);
|
|
1235
1493
|
requirePositionalCount(a, GC_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos gc", missing: "" });
|
|
1494
|
+
const sdk = getSdk();
|
|
1236
1495
|
const submitting = a.includes("--submit");
|
|
1237
1496
|
const corePath = flagValue(a, "--intent-core");
|
|
1238
1497
|
const receiptPath = flagValue(a, "--verifier-receipt");
|
|
@@ -1252,9 +1511,9 @@ async function gc(args) {
|
|
|
1252
1511
|
if (submitting) {
|
|
1253
1512
|
const opts = { ...target, submit: { core: readJsonFile("--intent-core", corePath), verifier_receipt: readJsonFile("--verifier-receipt", receiptPath) } };
|
|
1254
1513
|
if (a.includes("--wait")) opts.submit.wait = {};
|
|
1255
|
-
const prune = await
|
|
1514
|
+
const prune = await sdk.gitvault.prune(opts);
|
|
1256
1515
|
const out = { phase: "submitted", prune };
|
|
1257
|
-
|
|
1516
|
+
printJson(sdk, out);
|
|
1258
1517
|
if (prune.confirmation?.outcome) {
|
|
1259
1518
|
console.error(
|
|
1260
1519
|
`submitted — the signed completion reports ${prune.confirmation.deleted.length} deleted, ` +
|
|
@@ -1265,11 +1524,12 @@ async function gc(args) {
|
|
|
1265
1524
|
console.error("submitted — no completion yet. Nothing is deleted until the control-plane-signed completion says so; re-run with --wait or poll the intent.");
|
|
1266
1525
|
}
|
|
1267
1526
|
console.error(prune.note);
|
|
1527
|
+
printVerboseStats(a, sdk);
|
|
1268
1528
|
return;
|
|
1269
1529
|
}
|
|
1270
1530
|
|
|
1271
|
-
const checkpoint = await
|
|
1272
|
-
const prune = await
|
|
1531
|
+
const checkpoint = await sdk.gitvault.compact(target);
|
|
1532
|
+
const prune = await sdk.gitvault.prune(target);
|
|
1273
1533
|
const nextActions = [];
|
|
1274
1534
|
if (!prune.blocked_reason && prune.object_candidates.length > 0) {
|
|
1275
1535
|
// Additive fields beyond the CLI's usual {type, command, why}: the
|
|
@@ -1285,7 +1545,7 @@ async function gc(args) {
|
|
|
1285
1545
|
});
|
|
1286
1546
|
}
|
|
1287
1547
|
const out = { phase: "planned", checkpoint, prune, next_actions: nextActions };
|
|
1288
|
-
|
|
1548
|
+
printJson(sdk, out);
|
|
1289
1549
|
console.error(`checkpoint published at generation ${checkpoint.generation}: ${checkpoint.covered_refs} ref(s), ${checkpoint.covered_roots} retention root(s).`);
|
|
1290
1550
|
if (!checkpoint.cutoff_bound) {
|
|
1291
1551
|
console.error("no retention-cutoff ticket was obtained, so roots were RETAINED — expiry is permissive. The checkpoint published, but no expired root left the map.");
|
|
@@ -1303,6 +1563,7 @@ async function gc(args) {
|
|
|
1303
1563
|
}
|
|
1304
1564
|
}
|
|
1305
1565
|
console.error("`gc` is NOT \"exactly git gc\" — the deletion ceremony is stricter: nothing is removed until a control-plane-signed completion confirms it.");
|
|
1566
|
+
printVerboseStats(a, sdk);
|
|
1306
1567
|
} catch (err) {
|
|
1307
1568
|
reportSdkError(err);
|
|
1308
1569
|
}
|
|
@@ -1310,14 +1571,40 @@ async function gc(args) {
|
|
|
1310
1571
|
|
|
1311
1572
|
// ─── access (read-only; repair gated) ──────────────────────
|
|
1312
1573
|
|
|
1574
|
+
/** `repos access --human`: a compact roster of directory recipients and their coverage. */
|
|
1575
|
+
function formatAccessHuman(result) {
|
|
1576
|
+
const lines = [`Repo: ${result.repo_id}`];
|
|
1577
|
+
lines.push(`Recipients: ${result.recipients.length} directory, ${result.recipients.filter((r) => r.covered).length} covered`);
|
|
1578
|
+
for (const r of result.recipients) {
|
|
1579
|
+
lines.push(` ${r.covered ? "covered" : "NOT covered"} ${r.display_name ?? r.principal_id}${r.envelope_state ? ` (${r.envelope_state})` : ""}`);
|
|
1580
|
+
}
|
|
1581
|
+
if (result.this_keystore) lines.push(`This machine's own keystore also covers (writing principal, not in org directory): ${result.this_keystore.fingerprint}`);
|
|
1582
|
+
if (result.unmatched_covered_fingerprints.length > 0) lines.push(`Orphaned/external coverage: ${result.unmatched_covered_fingerprints.join(", ")}`);
|
|
1583
|
+
if (Array.isArray(result.stale_access) && result.stale_access.length > 0) {
|
|
1584
|
+
lines.push(`Stale access (removed members that still decrypt): ${result.stale_access.map((s) => s.display_name ?? s.principal_id).join(", ")}`);
|
|
1585
|
+
}
|
|
1586
|
+
lines.push(result.gap);
|
|
1587
|
+
return lines.join("\n");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1313
1590
|
async function accessRead(args) {
|
|
1314
1591
|
const a = normalizeArgv(args);
|
|
1315
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1592
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1316
1593
|
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access", missing: "" });
|
|
1594
|
+
const human = a.includes("--human");
|
|
1595
|
+
if (human && a.includes("--json")) {
|
|
1596
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
1597
|
+
}
|
|
1598
|
+
const sdk = getSdk();
|
|
1317
1599
|
const target = await vaultTarget(a);
|
|
1318
1600
|
try {
|
|
1319
|
-
const result = await
|
|
1320
|
-
|
|
1601
|
+
const result = await sdk.gitvault.access(target);
|
|
1602
|
+
if (human) {
|
|
1603
|
+
console.log(formatAccessHuman(result));
|
|
1604
|
+
printVerboseStats(a, sdk);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
printJson(sdk, result);
|
|
1321
1608
|
await spillIfLarge(result.repo_id, "access", result);
|
|
1322
1609
|
console.error(`${result.recipients.length} directory recipient(s), ${result.recipients.filter((r) => r.covered).length} covered on this repo.`);
|
|
1323
1610
|
if (result.this_keystore) {
|
|
@@ -1331,35 +1618,183 @@ async function accessRead(args) {
|
|
|
1331
1618
|
console.error(`${result.stale_access.length} removed member(s) STILL decrypt this vault (not yet revocable — no epoch rotation in v0): ${names}`);
|
|
1332
1619
|
}
|
|
1333
1620
|
console.error(result.gap);
|
|
1621
|
+
printVerboseStats(a, sdk);
|
|
1334
1622
|
} catch (err) {
|
|
1335
1623
|
reportSdkError(err);
|
|
1336
1624
|
}
|
|
1337
1625
|
}
|
|
1338
1626
|
|
|
1627
|
+
const ROTATION_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--recipient-state-version", "--recipient-revocation-version", "--idempotency-key"];
|
|
1628
|
+
|
|
1629
|
+
/**
|
|
1630
|
+
* `run402 repos access repair` (D193-D203, rev 42) — a general re-key of
|
|
1631
|
+
* this vault's CURRENT epoch, dropping every principal in `stale_access`
|
|
1632
|
+
* (`pending_removal`, still covered) and clearing a pre-existing vault's
|
|
1633
|
+
* one-time migration requirement. Drives `rotateEpoch({reason:"elective_rekey"})`.
|
|
1634
|
+
*
|
|
1635
|
+
* `--recipient-state-version`/`--recipient-revocation-version` are the D194
|
|
1636
|
+
* frozen watermarks this attempt must be fenced against. They are NOT
|
|
1637
|
+
* discovered automatically here: the live gateway exposes NO general read
|
|
1638
|
+
* route for `internal.gitvault_recipient_state_counters` outside the
|
|
1639
|
+
* `key-revocation` declare route's own response (see
|
|
1640
|
+
* `GitvaultVault.rotateEpoch`'s doc comment, `sdk/src/node/gitvault-
|
|
1641
|
+
* publication.ts`, for the confirmed source-level finding). Until that
|
|
1642
|
+
* route ships, this verb needs the pair supplied explicitly — refusing
|
|
1643
|
+
* cleanly and naming exactly this when they are omitted, rather than
|
|
1644
|
+
* guessing and either failing opaquely or (worse) never converging.
|
|
1645
|
+
*
|
|
1646
|
+
* **`elective_rekey` refuses ANY exclusion** (`EPOCH_ROTATION_INCOMPLETE_ENROLLMENT`
|
|
1647
|
+
* on even one keyless/unconfirmed desired principal) — so a pending
|
|
1648
|
+
* `/confirm`/`/repin` receipt does NOT help here: folding it into THIS
|
|
1649
|
+
* rotation's `pending_confirmations` still leaves that principal
|
|
1650
|
+
* `excluded_unconfirmed` for THIS rotation (D196 — same-head manifest
|
|
1651
|
+
* updates never self-authorize), which `elective_rekey`'s own
|
|
1652
|
+
* completeness check then refuses on. If a directory principal is
|
|
1653
|
+
* unconfirmed when this vault needs to clear its migration requirement,
|
|
1654
|
+
* use `run402 repos access revoke-key`/`declare-exposure` instead (an
|
|
1655
|
+
* urgent reason, which admits with a nonempty partial target set) and
|
|
1656
|
+
* fold the pending receipt into THAT rotation.
|
|
1657
|
+
*/
|
|
1339
1658
|
async function accessRepair(args) {
|
|
1340
1659
|
const a = normalizeArgv(args);
|
|
1341
|
-
assertKnownFlags(a, [...
|
|
1342
|
-
requirePositionalCount(a,
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1660
|
+
assertKnownFlags(a, [...ROTATION_VALUE_FLAGS, "-v", "--verbose", "--help", "-h"], ROTATION_VALUE_FLAGS);
|
|
1661
|
+
requirePositionalCount(a, ROTATION_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access repair", missing: "" });
|
|
1662
|
+
const recipientStateVersion = flagValue(a, "--recipient-state-version");
|
|
1663
|
+
const recipientRevocationVersion = flagValue(a, "--recipient-revocation-version");
|
|
1664
|
+
if (recipientStateVersion == null || recipientRevocationVersion == null) {
|
|
1665
|
+
fail({
|
|
1666
|
+
code: "ROTATION_COUNTERS_REQUIRED",
|
|
1667
|
+
message: "`run402 repos access repair` needs --recipient-state-version and --recipient-revocation-version — the gateway does not yet expose a read route for these two counters outside the key-revocation declare route.",
|
|
1668
|
+
hint: "If you know a specific principal whose key should be revoked, use `run402 repos access revoke-key <principal_id>` instead — it is fully self-contained (no flags needed). `access repair` is the general re-key for clearing stale_access / a first-ever migration and needs these two values from platform staff or direct DB access until a gateway read route ships.",
|
|
1669
|
+
next_actions: [nextAction("edit_request", { command: "run402 repos access revoke-key <principal_id>", why: "the ONE fully self-contained rotation entry point today — no counters needed" })],
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
const sdk = getSdk();
|
|
1673
|
+
const target = await vaultTarget(a);
|
|
1674
|
+
try {
|
|
1675
|
+
const result = await sdk.gitvault.rotateEpoch({
|
|
1676
|
+
...target,
|
|
1677
|
+
reason: "elective_rekey",
|
|
1678
|
+
recipient_state_version: recipientStateVersion,
|
|
1679
|
+
recipient_revocation_version: recipientRevocationVersion,
|
|
1680
|
+
...(flagValue(a, "--idempotency-key") != null ? { client_idempotency_key: flagValue(a, "--idempotency-key") } : {}),
|
|
1681
|
+
});
|
|
1682
|
+
printJson(sdk, result);
|
|
1683
|
+
await spillIfLarge(result.rotation_id, "access-repair", result);
|
|
1684
|
+
console.error(`rotated to epoch ${result.new_epoch} at generation ${result.generation}: ${result.included.length} recipient(s) included, ${result.excluded_keyless_principal_ids.length} keyless, ${result.excluded_unconfirmed_principal_ids.length} unconfirmed.`);
|
|
1685
|
+
console.error(`self_check: ${result.self_check}${result.self_check === "not_a_recipient" ? " (this machine's own principal is not itself a vault recipient — nothing to self-verify)" : " (this machine's own opened envelope reproduced the committed epoch key)"}.`);
|
|
1686
|
+
printVerboseStats(a, sdk);
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
reportSdkError(err);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* `run402 repos access revoke-key <principal_id>` (D199) — the ONE fully
|
|
1694
|
+
* self-contained rotation entry point: declares
|
|
1695
|
+
* `reason:"recipient_key_revoked"` for `principal_id` (owner + step-up)
|
|
1696
|
+
* and drives the rotation off that declaration's OWN returned counters.
|
|
1697
|
+
* No flags needed — this is the reason value with a real, working
|
|
1698
|
+
* gateway-side counter read.
|
|
1699
|
+
*/
|
|
1700
|
+
async function accessRevokeKey(args) {
|
|
1701
|
+
const a = normalizeArgv(args);
|
|
1702
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--idempotency-key", "-v", "--verbose", "--help", "-h"], [...COMMON_VALUE_FLAGS, "--idempotency-key"]);
|
|
1703
|
+
const [principalId] = requirePositionalCount(a, [...COMMON_VALUE_FLAGS, "--idempotency-key"], {
|
|
1704
|
+
min: 1, max: 1, command: "run402 repos access revoke-key <principal_id>",
|
|
1705
|
+
missing: "Missing <principal_id>. This is the principal whose current key should no longer be trusted — the next rotation excludes them from the new epoch.",
|
|
1348
1706
|
});
|
|
1707
|
+
const sdk = getSdk();
|
|
1708
|
+
const target = await vaultTarget(a);
|
|
1709
|
+
try {
|
|
1710
|
+
const result = await sdk.gitvault.rotateEpochForKeyRevocation(principalId, {
|
|
1711
|
+
...target,
|
|
1712
|
+
...(flagValue(a, "--idempotency-key") != null ? { client_idempotency_key: flagValue(a, "--idempotency-key") } : {}),
|
|
1713
|
+
});
|
|
1714
|
+
printJson(sdk, result);
|
|
1715
|
+
await spillIfLarge(result.rotation_id, "access-revoke-key", result);
|
|
1716
|
+
console.error(`declared ${principalId}'s key revoked and rotated to epoch ${result.new_epoch} at generation ${result.generation}: ${result.included.length} recipient(s) included going forward.`);
|
|
1717
|
+
console.error(`self_check: ${result.self_check}.`);
|
|
1718
|
+
printVerboseStats(a, sdk);
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
reportSdkError(err);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* `run402 repos access declare-exposure` (D199) — declares
|
|
1726
|
+
* `reason:"epoch_secret_exposed"` admissible for THIS vault (owner +
|
|
1727
|
+
* step-up), vault-scoped (one vault's leaked key is not evidence any
|
|
1728
|
+
* sibling vault is compromised). The DECLARATION itself is real and
|
|
1729
|
+
* self-contained; the FOLLOW-UP rotation it authorizes is NOT auto-run
|
|
1730
|
+
* here, because — same confirmed gap as `access repair` — the D194
|
|
1731
|
+
* counters it must be fenced against have no client-visible read for this
|
|
1732
|
+
* reason value either. This is the rekey remedy the exposed-key incident
|
|
1733
|
+
* needs: declare here, then rotate (via `--recipient-state-version`/
|
|
1734
|
+
* `--recipient-revocation-version` once known, e.g. from platform staff).
|
|
1735
|
+
*
|
|
1736
|
+
* **If a `/confirm`/`/repin` receipt is already pending** (a directory
|
|
1737
|
+
* principal was confirmed BEFORE this declaration, or gets confirmed while
|
|
1738
|
+
* the rotation is outstanding), do NOT call `publishPinManifestUpdate`
|
|
1739
|
+
* separately — that call is itself an ORDINARY admission and is itself
|
|
1740
|
+
* refused `EPOCH_ROTATION_REQUIRED` for as long as this declaration stays
|
|
1741
|
+
* outstanding (reproduced live in production 2026-08-27). Pass the receipt
|
|
1742
|
+
* to `r.gitvault.rotateEpoch({..., pending_confirmations: [{principal_id,
|
|
1743
|
+
* ek_fingerprint, receipt}]})` instead — it rides the SAME head as the
|
|
1744
|
+
* rotation this declaration requires, publishing durably without needing a
|
|
1745
|
+
* second, separately-gated admission. See `GitvaultVault.rotateEpoch`'s
|
|
1746
|
+
* doc comment for what this does NOT do: the folded principal is still
|
|
1747
|
+
* excluded from THIS rotation's own envelope set (D196) and becomes
|
|
1748
|
+
* eligible starting at the NEXT rotation.
|
|
1749
|
+
*/
|
|
1750
|
+
async function accessDeclareExposure(args) {
|
|
1751
|
+
const a = normalizeArgv(args);
|
|
1752
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1753
|
+
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access declare-exposure", missing: "" });
|
|
1754
|
+
const target = await vaultTarget(a);
|
|
1755
|
+
try {
|
|
1756
|
+
const sdk = getSdk();
|
|
1757
|
+
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
1758
|
+
const result = await sdk.gitvault.declareEpochSecretExposed(repoId);
|
|
1759
|
+
printJson(sdk, result);
|
|
1760
|
+
console.error(`declared epoch_secret_exposed for ${repoId} (epoch_secret_exposure_version now ${result.epoch_secret_exposure_version}).`);
|
|
1761
|
+
console.error("THIS DECLARATION DOES NOT ROTATE THE VAULT BY ITSELF — the next ordinary push now refuses EPOCH_ROTATION_REQUIRED until a rotate_epoch with reason:\"epoch_secret_exposed\" commits.");
|
|
1762
|
+
console.error("submit that rotation via r.gitvault.rotateEpoch({repo_id, reason: \"epoch_secret_exposed\", recipient_state_version, recipient_revocation_version}) once you have the two counter values (no CLI shortcut exists for this reason yet — see `run402 repos access repair --help`).");
|
|
1763
|
+
console.error("if a /confirm or /repin receipt is already pending for a directory principal, do NOT publish it separately (publishPinManifestUpdate is itself gated the same way) — pass it as rotateEpoch's pending_confirmations instead so it rides the SAME head as this rotation.");
|
|
1764
|
+
printVerboseStats(a, sdk);
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
reportSdkError(err);
|
|
1767
|
+
}
|
|
1349
1768
|
}
|
|
1350
1769
|
|
|
1351
1770
|
async function access(args) {
|
|
1352
1771
|
const a = normalizeArgv(args);
|
|
1353
1772
|
if (a[0] === "repair") return accessRepair(a.slice(1));
|
|
1773
|
+
if (a[0] === "revoke-key") return accessRevokeKey(a.slice(1));
|
|
1774
|
+
if (a[0] === "declare-exposure") return accessDeclareExposure(a.slice(1));
|
|
1354
1775
|
return accessRead(a);
|
|
1355
1776
|
}
|
|
1356
1777
|
|
|
1357
1778
|
// ─── recover ─────────────────────
|
|
1358
1779
|
|
|
1780
|
+
/** `repos recover --human`: the same verdict the stderr lines already carry, condensed into one block. */
|
|
1781
|
+
function formatRecoverHuman(result, outDir) {
|
|
1782
|
+
const lines = [
|
|
1783
|
+
`Repo: ${result.repo_id}`,
|
|
1784
|
+
`Recovered generation ${result.recovered_generation} into ${outDir}` +
|
|
1785
|
+
(result.chain_break ? ` (chain break at ${result.chain_break.generation} — fell back to the newest fully-verified generation)` : "") + ".",
|
|
1786
|
+
];
|
|
1787
|
+
if (result.data_loss_detected) {
|
|
1788
|
+
lines.push(`DATA LOSS DETECTED: ${result.absences.filter((x) => x.adjudication === "unexplained_absence").length} object(s) are unexplained absences.`);
|
|
1789
|
+
}
|
|
1790
|
+
lines.push(`Layout: ${result.layout}` + (result.layout === "bare" ? " (no working files — not a failed recovery)" : ""));
|
|
1791
|
+
return lines.join("\n");
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1359
1794
|
async function recover(args) {
|
|
1360
1795
|
const a = normalizeArgv(args);
|
|
1361
1796
|
const valueFlags = ["--out", "--repo", "--profile", "--region", "--endpoint"];
|
|
1362
|
-
assertKnownFlags(a, [...valueFlags, "--ambient", "--help", "-h"], valueFlags);
|
|
1797
|
+
assertKnownFlags(a, [...valueFlags, "--ambient", "--human", "-v", "--verbose", "--help", "-h"], valueFlags);
|
|
1363
1798
|
const [source] = requirePositionalCount(a, valueFlags, {
|
|
1364
1799
|
min: 1, max: 1, command: "run402 repos recover <source> --out <dir>",
|
|
1365
1800
|
missing: "Missing <source>. Expected s3://<bucket>[/<prefix>] or a directory path.",
|
|
@@ -1368,19 +1803,31 @@ async function recover(args) {
|
|
|
1368
1803
|
if (outDir == null) {
|
|
1369
1804
|
fail({ code: "BAD_USAGE", message: "run402 repos recover needs --out <dir>.", hint: "Where to materialize the recovered repository, e.g. --out ./restored" });
|
|
1370
1805
|
}
|
|
1806
|
+
const human = a.includes("--human");
|
|
1807
|
+
if (human && a.includes("--json")) {
|
|
1808
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
1809
|
+
}
|
|
1810
|
+
const sdk = getSdk();
|
|
1371
1811
|
const credential = resolveMirrorCredential(a);
|
|
1372
1812
|
const repoId = flagValue(a, "--repo");
|
|
1373
1813
|
const region = flagValue(a, "--region");
|
|
1374
1814
|
const endpoint = flagValue(a, "--endpoint");
|
|
1375
1815
|
try {
|
|
1376
|
-
const result = await
|
|
1816
|
+
const result = await sdk.gitvault.recover({
|
|
1377
1817
|
source, out_dir: outDir,
|
|
1378
1818
|
...(repoId != null ? { repo_id: repoId } : {}),
|
|
1379
1819
|
...(credential ? { credential } : {}),
|
|
1380
1820
|
...(region != null ? { region } : {}),
|
|
1381
1821
|
...(endpoint != null ? { endpoint } : {}),
|
|
1382
1822
|
});
|
|
1383
|
-
|
|
1823
|
+
if (human) {
|
|
1824
|
+
console.log(formatRecoverHuman(result, outDir));
|
|
1825
|
+
if (result.layout === "bare") for (const n of result.next_actions ?? []) console.error(`next: ${n.action} — ${n.command}`);
|
|
1826
|
+
printMirrorHonesty(result);
|
|
1827
|
+
printVerboseStats(a, sdk);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
printJson(sdk, result);
|
|
1384
1831
|
await spillIfLarge(result.repo_id, "recover", result);
|
|
1385
1832
|
console.error(`recovered generation ${result.recovered_generation} for ${result.repo_id} into ${outDir}` + (result.chain_break ? ` (chain break at ${result.chain_break.generation} — fell back to the newest fully-verified generation)` : "") + ".");
|
|
1386
1833
|
if (result.data_loss_detected) {
|
|
@@ -1391,6 +1838,7 @@ async function recover(args) {
|
|
|
1391
1838
|
for (const n of result.next_actions ?? []) console.error(`next: ${n.action} — ${n.command}`);
|
|
1392
1839
|
}
|
|
1393
1840
|
printMirrorHonesty(result);
|
|
1841
|
+
printVerboseStats(a, sdk);
|
|
1394
1842
|
} catch (err) {
|
|
1395
1843
|
reportSdkError(err);
|
|
1396
1844
|
}
|