run402 4.40.0 → 4.41.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/README.md +7 -4
- package/cli.mjs +10 -2
- package/lib/command-manifest.mjs +56 -45
- package/lib/doctor.mjs +4 -4
- package/lib/gitvault-scaffold.mjs +1 -1
- package/lib/gitvault.mjs +118 -1163
- package/lib/init.mjs +2 -2
- package/lib/repos.mjs +1237 -248
- package/package.json +1 -1
- package/sdk/dist/errors.d.ts +5 -2
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +154 -11
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +154 -16
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/node/gitvault-apply.d.ts +1 -1
- package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-apply.js +6 -6
- package/sdk/dist/node/gitvault-apply.js.map +1 -1
- package/sdk/dist/node/gitvault-creation-journal.js +1 -1
- package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
- package/sdk/dist/node/gitvault-deploy.js +2 -2
- package/sdk/dist/node/gitvault-deploy.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror-backend.js +1 -1
- package/sdk/dist/node/gitvault-mirror-backend.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror.js +1 -1
- package/sdk/dist/node/gitvault-mirror.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +22 -3
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +37 -12
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
package/lib/gitvault.mjs
CHANGED
|
@@ -1,1150 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `run402 gitvault` —
|
|
2
|
+
* `run402 gitvault` — RETIRED (repo-surface-consolidation, design D7).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* The 19-command dispatcher this module used to hold is gone. Every old
|
|
5
|
+
* spelling now answers a proper stderr JSON error envelope: typed
|
|
6
|
+
* `next_actions`, non-zero exit, EMPTY stdout — never silence, never new
|
|
7
|
+
* behavior. Renamed verbs answer `COMMAND_MOVED` naming their `repos`
|
|
8
|
+
* successor; verbs with no behavioral successor answer `COMMAND_REMOVED`
|
|
9
|
+
* (never a `COMMAND_MOVED` that would lie about equivalence — design
|
|
10
|
+
* D7/D10). The freed spellings are RESERVED for one release, never
|
|
11
|
+
* re-pointed at different behavior — the `rooms`→`messages` precedent.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* `gitvault` itself survives only as protocol/infrastructure language: the
|
|
14
|
+
* SDK keeps `r.gitvault.*` (design D1 — "gitvault is what the thing IS;
|
|
15
|
+
* repos is what the user HAS"). This module's one surviving export,
|
|
16
|
+
* `printKeystoreLocation`, is a shared print helper `cli/lib/repos.mjs`
|
|
17
|
+
* still composes — moving it there instead would make repos.mjs restate
|
|
18
|
+
* logic this file already owns for zero benefit.
|
|
18
19
|
*/
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { resolveProjectId } from "./config.mjs";
|
|
22
|
-
import { resolveOwningOrgId } from "./org-context.mjs";
|
|
23
|
-
import { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
24
|
-
import { getSdk } from "./sdk.mjs";
|
|
25
|
-
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
26
|
-
import {
|
|
27
|
-
normalizeArgv,
|
|
28
|
-
hasHelp,
|
|
29
|
-
assertKnownFlags,
|
|
30
|
-
parseIntegerFlag,
|
|
31
|
-
flagValue,
|
|
32
|
-
requirePositionalCount,
|
|
33
|
-
failUnknownSubcommand,
|
|
34
|
-
} from "./argparse.mjs";
|
|
35
|
-
|
|
36
|
-
/** Value-taking flags every gitvault subcommand accepts. */
|
|
37
|
-
const COMMON_VALUE_FLAGS = ["--project", "--repo"];
|
|
20
|
+
import { fail } from "./sdk-errors.mjs";
|
|
21
|
+
import { hasHelp } from "./argparse.mjs";
|
|
38
22
|
|
|
39
|
-
export const HELP = `run402 gitvault —
|
|
23
|
+
export const HELP = `run402 gitvault — RETIRED (repo-surface-consolidation)
|
|
40
24
|
|
|
41
25
|
Usage:
|
|
42
|
-
run402 gitvault
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
only adds the git remote). Idempotent: an existing vault is
|
|
66
|
-
reported with \`deduplicated: true\` and nothing is re-minted. Adds
|
|
67
|
-
the \`run402\` remote too when the current directory is already a
|
|
68
|
-
repository.
|
|
69
|
-
status What this machine and the control plane each believe about the
|
|
70
|
-
vault: allocation, policy, whether this keystore can sign, the
|
|
71
|
-
authenticated and materialized pins, any pending
|
|
72
|
-
unvaulted-override journals, and where the keystore lives. Never
|
|
73
|
-
reports key material.
|
|
74
|
-
policy Set the activation policy — \`required\` (a deploy must present a
|
|
75
|
-
vaulted capture) or \`grandfathered\` (it need not). Owner + step-up,
|
|
76
|
-
audited. \`grandfathered\` is the documented way out of a deploy
|
|
77
|
-
blocked by GITVAULT_CLIENT_UPGRADE_REQUIRED, and leaves a
|
|
78
|
-
doctor-persistent warning until the project returns to \`required\`.
|
|
79
|
-
snapshot Capture the working tree and publish it. This is NOT gated on a
|
|
80
|
-
deploy — a vault-only project snapshots for months without one.
|
|
81
|
-
Against a project with no vault yet, this ALLOCATES one inline
|
|
82
|
-
(the six-stage creation, same as \`init\`) before publishing — one
|
|
83
|
-
command, no prior \`gitvault init\`. When --repo/--project are
|
|
84
|
-
both omitted and the local run402/origin remote is a slug-form
|
|
85
|
-
address (run402::<org-slug>/<name>), PUSH-TO-CREATES through it
|
|
86
|
-
instead (design D6) — same as pushing that name with \`git\`. The
|
|
87
|
-
one-shot recovery receipt and keystore path print to stderr the
|
|
88
|
-
moment that happens.
|
|
89
|
-
Before reporting a snapshot as landed the SDK compares finalization
|
|
90
|
-
receipts against the expected manifest and reads the admitted head
|
|
91
|
-
back from storage; a 200 alone is never enough. \`push\` is a
|
|
92
|
-
deprecation-warning alias for one release — it will be removed
|
|
93
|
-
next release. Once \`gitvault\` was the only publish verb; \`git
|
|
94
|
-
push\` is now the actual publish path (via the remote helper),
|
|
95
|
-
so \`push\` here was renamed to name what it does: one verb per
|
|
96
|
-
operation. \`--dry-run\` (kychee-com/run402#565) previews it
|
|
97
|
-
instead: the same real local pipeline, publishing nothing and
|
|
98
|
-
never allocating.
|
|
99
|
-
compact Publish a checkpoint covering the canonical refs, every root
|
|
100
|
-
unexpired at the cutoff, and the HEAD target, under a maintenance
|
|
101
|
-
lease so a concurrent cycle cannot race it.
|
|
102
|
-
prune Plan a prune, and — with both verifier receipts — submit it.
|
|
103
|
-
Two phases, because the protocol is two-phase; see below.
|
|
104
|
-
verify Verify the head chain from the authenticated pin up to the newest
|
|
105
|
-
listed generation. Fails closed on a regression, a gap, or a
|
|
106
|
-
transition descriptor this client cannot validate.
|
|
107
|
-
reconcile Wrap this vault's current epoch key to every org member who has
|
|
108
|
-
published an encryption key but has no envelope on this vault
|
|
109
|
-
yet (gitvault-human-envelopes task 4.1's ADD-path workaround).
|
|
110
|
-
\`snapshot\`/\`push\` already run this themselves, best-effort,
|
|
111
|
-
after every successful publish — run it explicitly at session
|
|
112
|
-
start, or to see the per-recipient breakdown on its own. Pins
|
|
113
|
-
each recipient's fingerprint on first wrap (TOFU); a directory
|
|
114
|
-
fingerprint that no longer matches its pin is refused, never
|
|
115
|
-
silently re-wrapped — reported loudly, one recipient at a time.
|
|
116
|
-
THIS IS A WORKAROUND, not full epoch rotation: a newly-wrapped
|
|
117
|
-
member gets this vault's ENTIRE history under V0's single fixed
|
|
118
|
-
epoch, not \"from here forward\" — the org directory route also
|
|
119
|
-
does not yet publish the raw key bytes this needs, so every
|
|
120
|
-
recipient reports \`skipped\` (reason \`missing_public_key\`)
|
|
121
|
-
until the gateway ships that field.
|
|
122
|
-
mirror The exit ramp (gitvault-mirror-and-recover): a client-side,
|
|
123
|
-
customer-owned ciphertext mirror. run402 never holds a
|
|
124
|
-
credential to it. \`set\` configures the destination (config
|
|
125
|
-
lives beside the keystore, never in run402.config.json, never a
|
|
126
|
-
raw secret); \`remove\` drops the config only — it NEVER touches
|
|
127
|
-
the mirror's own bytes; \`status\` reports whether the mirror is
|
|
128
|
-
current against the live vault; \`sync\` lists+diffs+copies what
|
|
129
|
-
the mirror is missing (idempotent, resumable); \`verify\` is the
|
|
130
|
-
KEYLESS integrity probe — discovery + chain verification +
|
|
131
|
-
absence adjudication, never decryption. Every deploy/snapshot
|
|
132
|
-
dual-pushes to a configured mirror automatically; a mirror
|
|
133
|
-
failure NEVER blocks the deploy — it is reported as a separate
|
|
134
|
-
\`mirror_push\` field.
|
|
135
|
-
recover \`r402s-recover\`: rebuild a working git repository straight from
|
|
136
|
-
a mirrored prefix, with NO SERVER INVOLVED. Proves this mirror's
|
|
137
|
-
validity, never freshness — read both honesty statements in the
|
|
138
|
-
output before relying on the result.
|
|
139
|
-
|
|
140
|
-
Options:
|
|
141
|
-
--project <id> Project whose vault to act on (defaults to the active project)
|
|
142
|
-
--org <org_id> init: the owning organization (resolved from the project
|
|
143
|
-
when omitted)
|
|
144
|
-
--git-remote init: 'git init' the current directory when it is not a
|
|
145
|
-
repository yet, so the run402 remote can be added there.
|
|
146
|
-
Opt-in: creating a repository where you did not ask for one
|
|
147
|
-
is a bad surprise, so without it a non-repository directory
|
|
148
|
-
allocates the vault and adds no remote.
|
|
149
|
-
--no-remote init: allocate the vault only; touch no git configuration
|
|
150
|
-
--reason <why> policy: why the policy is changing — recorded in the audit
|
|
151
|
-
event. REQUIRED for \`grandfathered\`, which is a deliberate
|
|
152
|
-
weakening of the activation guarantee.
|
|
153
|
-
--refs status: also materialize and report the vault's ref map and
|
|
154
|
-
HEAD target. This is a VERIFICATION (it walks the head
|
|
155
|
-
chain and advances the local materialized pin), which is
|
|
156
|
-
why plain \`status\` — an observation — does not do it.
|
|
157
|
-
--human status: a five/six-line human summary on stdout instead of
|
|
158
|
-
the JSON dump (kychee-com/run402#569; explicit opt-in per
|
|
159
|
-
the cli-output-contract). Address, remote; HEAD + ref count
|
|
160
|
-
(needs --refs too — otherwise the line names the omission);
|
|
161
|
-
generations in decimal; storage bytes/object count (from
|
|
162
|
-
this SAME status() call — no extra network read); whether
|
|
163
|
-
THIS machine can decrypt, and the policy; standing warnings,
|
|
164
|
-
if any, verbatim (a live terminal-loss risk may be the sixth
|
|
165
|
-
line). Rejected together with --json. No effect on plain
|
|
166
|
-
\`status\`'s own output, which is unchanged.
|
|
167
|
-
--repo <repo_id> Address the vault directly by id, skipping project lookup
|
|
168
|
-
--message <text> snapshot: commit message for the synthetic commit a dirty tree
|
|
169
|
-
produces (a clean tree pushes HEAD itself, no message used)
|
|
170
|
-
--checkpoint snapshot: force the checkpoint-bearing form regardless of delta size
|
|
171
|
-
--dry-run snapshot: a REAL preview (kychee-com/run402#565) — runs the actual
|
|
172
|
-
local pipeline (capture, pack building, encryption sizing) and
|
|
173
|
-
reports objects, encrypted bytes, refs, and the generation it
|
|
174
|
-
would admit as. Publishes NOTHING, and never allocates a vault
|
|
175
|
-
that does not exist yet (reports allocation_needed instead).
|
|
176
|
-
--budget <n> verify: heads to verify in this call. The verified prefix is
|
|
177
|
-
persisted, so a budget-exceeded run resumes where it stopped
|
|
178
|
-
instead of restarting.
|
|
179
|
-
--submit prune: submit the planned intent. Requires --intent-core and
|
|
180
|
-
--verifier-receipt.
|
|
181
|
-
--intent-core <path>
|
|
182
|
-
prune: the plan's \`intent_core\`, saved verbatim from a prior
|
|
183
|
-
planning run. A rebuilt core carries a different nonce, so
|
|
184
|
-
the r402s-verify receipt would no longer bind to it.
|
|
185
|
-
--verifier-receipt <path>
|
|
186
|
-
prune: r402s-verify's \`verifier_receipt\` over that core.
|
|
187
|
-
--wait prune: poll the submitted intent until the control-plane-
|
|
188
|
-
signed completion appears, instead of returning immediately.
|
|
189
|
-
--profile <name> mirror set / recover: the AWS credential profile name for an
|
|
190
|
-
s3:// destination (read from ~/.aws/credentials at USE time —
|
|
191
|
-
never stored). Mutually exclusive with --ambient.
|
|
192
|
-
--ambient mirror set / recover: use the ambient AWS_ACCESS_KEY_ID /
|
|
193
|
-
AWS_SECRET_ACCESS_KEY environment chain instead of a profile.
|
|
194
|
-
--region <r> mirror set / recover: AWS region for an s3:// destination
|
|
195
|
-
(defaults to AWS_REGION / AWS_DEFAULT_REGION).
|
|
196
|
-
--endpoint <url> mirror set / recover: an S3-compatible endpoint override.
|
|
197
|
-
--out <dir> recover: where to materialize the recovered repository.
|
|
198
|
-
--json No-op: stdout is already JSON.
|
|
199
|
-
|
|
200
|
-
prune is TWO PHASES, because the protocol is:
|
|
201
|
-
1. \`run402 gitvault prune\` plans. It walks the verified chain, computes the GC
|
|
202
|
-
root set, subtracts it, and prints a SIGNED \`intent_core\` plus its
|
|
203
|
-
\`intent_core_sha256\`. Nothing is submitted and nothing is deleted.
|
|
204
|
-
2. Run \`r402s-verify\` against that core, then re-run with
|
|
205
|
-
\`--submit --intent-core <core.json> --verifier-receipt <receipt.json>\`.
|
|
206
|
-
The intent carries TWO receipts over the same core, one per implementation:
|
|
207
|
-
this CLI produces the \`run402-cli\` half by restoring the latest checkpoint
|
|
208
|
-
and recomputing its commitments, and \`r402s-verify\` produces the other.
|
|
209
|
-
A second receipt from this lineage would prove nothing, so it is never
|
|
210
|
-
synthesized here.
|
|
211
|
-
Only the control-plane-signed completion says what was deleted, and only its
|
|
212
|
-
\`deleted\` result means the bytes are gone — \`present_after_attempt\` is a
|
|
213
|
-
FAILED deletion, never counted as a success. There is deliberately no purge
|
|
214
|
-
verb in V0 at all. Retention is an operational promise of the platform, not a
|
|
215
|
-
cryptographic guarantee against it.
|
|
216
|
-
|
|
217
|
-
Expiry is permissive, by design:
|
|
218
|
-
A retention root whose \`effective_admitted_at\` this client cannot resolve is
|
|
219
|
-
RETAINED, and a compact that cannot obtain a retention-cutoff ticket keeps
|
|
220
|
-
every root. That costs storage, never history.
|
|
221
|
-
|
|
222
|
-
Terminal loss (protocol §0):
|
|
223
|
-
In V0-A, whole-machine or whole-keystore loss is terminal for vault history
|
|
224
|
-
until human envelopes ship. \`status\` prints the full statement verbatim on
|
|
225
|
-
stderr and carries it in its JSON — read it before you rely on this.
|
|
226
|
-
|
|
227
|
-
Examples:
|
|
228
|
-
run402 gitvault init
|
|
229
|
-
run402 gitvault status --refs
|
|
230
|
-
run402 gitvault status --human
|
|
231
|
-
run402 gitvault snapshot --message "wip: refactor the parser"
|
|
232
|
-
run402 gitvault snapshot --dry-run
|
|
233
|
-
run402 gitvault policy grandfathered --reason "migrating CI to a vaulted client"
|
|
234
|
-
run402 gitvault verify --budget 500
|
|
235
|
-
run402 gitvault reconcile
|
|
236
|
-
run402 gitvault prune --project prj_1a2b3c
|
|
237
|
-
run402 gitvault mirror set s3://acme-vault-mirror --profile acme
|
|
238
|
-
run402 gitvault mirror sync
|
|
239
|
-
run402 gitvault mirror status
|
|
240
|
-
run402 gitvault mirror verify
|
|
241
|
-
run402 gitvault recover s3://acme-vault-mirror --out ./restored
|
|
242
|
-
# \`gitvault push\` still works as a deprecation-warning alias for \`snapshot\`
|
|
243
|
-
# for one release; it will be removed next release.
|
|
26
|
+
run402 gitvault <anything> — answers a structured error naming its repos/git successor; see below.
|
|
27
|
+
|
|
28
|
+
Every "run402 gitvault <verb>" spelling has moved to "run402 repos <verb>",
|
|
29
|
+
except "reconcile" and "push", which are REMOVED outright (see below). Each
|
|
30
|
+
one answers a structured COMMAND_MOVED/COMMAND_REMOVED error for exactly one
|
|
31
|
+
release; after that the spelling is reserved and answers nothing at all.
|
|
32
|
+
|
|
33
|
+
Moved:
|
|
34
|
+
gitvault init -> repos create --project <id> (or repos create <name> for a NEW project)
|
|
35
|
+
gitvault status -> repos view
|
|
36
|
+
gitvault snapshot -> repos snapshot
|
|
37
|
+
gitvault policy -> repos policy
|
|
38
|
+
gitvault compact -> repos gc
|
|
39
|
+
gitvault prune -> repos gc
|
|
40
|
+
gitvault verify -> repos fsck
|
|
41
|
+
gitvault mirror -> repos mirror
|
|
42
|
+
gitvault recover -> repos recover
|
|
43
|
+
|
|
44
|
+
Removed, no successor:
|
|
45
|
+
gitvault push -> git push (its one-release alias window is over); repos snapshot is the capture lane
|
|
46
|
+
gitvault reconcile -> repos access (read-only inspection; the workaround it approximated is gone, not renamed)
|
|
47
|
+
|
|
48
|
+
Run \`run402 repos --help\` for the current surface.
|
|
244
49
|
`;
|
|
245
50
|
|
|
246
|
-
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
51
|
+
/** Renamed verbs: `gitvault <key>` -> the exact `repos` command that means the same thing now. */
|
|
52
|
+
const MOVED = {
|
|
53
|
+
init: { command: "run402 repos create --project <id>", why: "repos create absorbs allocation — pass --project to adopt an existing project, or a name to provision a new one." },
|
|
54
|
+
status: { command: "run402 repos view", why: "repos view is the side-effect-free repo inspection command; it never materializes refs the way status --refs used to." },
|
|
55
|
+
snapshot: { command: "run402 repos snapshot", why: "same verb, new noun." },
|
|
56
|
+
policy: { command: "run402 repos policy", why: "same verb, new noun." },
|
|
57
|
+
compact: { command: "run402 repos gc", why: "gc is git gc's own two halves — checkpoint publication and prune planning — in one verb." },
|
|
58
|
+
prune: { command: "run402 repos gc", why: "gc is git gc's own two halves — checkpoint publication and prune planning — in one verb." },
|
|
59
|
+
verify: { command: "run402 repos fsck", why: "fsck is git fsck's own job: walk the object graph and fail closed on corruption." },
|
|
60
|
+
mirror: { command: "run402 repos mirror", why: "one flag-driven verb replaces the five mirror subcommands (set/remove/status/sync/verify)." },
|
|
61
|
+
recover: { command: "run402 repos recover", why: "same verb, new noun." },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Verbs with NO behavioral successor — a lying COMMAND_MOVED would be worse than an honest COMMAND_REMOVED. */
|
|
65
|
+
const REMOVED = {
|
|
66
|
+
push: {
|
|
67
|
+
message: "`run402 gitvault push` was a deprecation-warning alias for exactly one release, and that release is over.",
|
|
68
|
+
hint: "`push` now means exactly one thing everywhere: `git push`. The capture lane is `run402 repos snapshot`.",
|
|
69
|
+
next_actions: [
|
|
70
|
+
{ type: "use_moved_command", command: "git push", why: "Publish your branches and tags the ordinary way." },
|
|
71
|
+
{ type: "use_moved_command", command: "run402 repos snapshot", why: "The capture lane: publish the protocol-owned deploy ref outside a deploy." },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
reconcile: {
|
|
75
|
+
message: "`run402 gitvault reconcile` is removed. It was a workaround — its own help text said so — a newly-wrapped member got the vault's entire history under a single fixed epoch, not real epoch rotation, and a temporary mechanism does not get a permanent verb.",
|
|
76
|
+
hint: "`run402 repos access` reports what the read surface has today (recipients, coverage, this machine's local TOFU pins). `repos access repair` will replace the mutating half once real epoch rotation ships — it does not exist yet either.",
|
|
77
|
+
next_actions: [{ type: "use_moved_command", command: "run402 repos access", why: "Inspect recipients and coverage — there is no equivalent mutating successor yet." }],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function movedResponse(sub) {
|
|
82
|
+
const m = MOVED[sub];
|
|
83
|
+
fail({
|
|
84
|
+
code: "COMMAND_MOVED",
|
|
85
|
+
message: `run402 gitvault ${sub} moved to ${m.command}.`,
|
|
86
|
+
hint: m.why,
|
|
87
|
+
details: { was: `gitvault ${sub}`, now: m.command },
|
|
88
|
+
next_actions: [{ type: "use_moved_command", command: m.command, why: m.why }],
|
|
265
89
|
});
|
|
266
|
-
const target = { repo_dir: repoDir };
|
|
267
|
-
if (repoId != null) target.repo_id = repoId;
|
|
268
|
-
// Only demand a project when one is actually needed: `--repo` alone is a
|
|
269
|
-
// complete address, and requiring one on top of it would make the
|
|
270
|
-
// cold-restart path fail for no reason.
|
|
271
|
-
if (repoId == null || project != null) {
|
|
272
|
-
if ("repo_id" in resolved && project == null) target.repo_id = resolved.repo_id;
|
|
273
|
-
// `resolveGitvaultTarget` reports its last (env/active) tier
|
|
274
|
-
// non-throwingly (`run402 doctor`'s call site needs that) — this call
|
|
275
|
-
// site is the one that historically failed closed with PROJECT_REQUIRED
|
|
276
|
-
// when nothing resolves anywhere, and still does: `resolveProjectId`
|
|
277
|
-
// re-derives the exact same env/active check and throws.
|
|
278
|
-
if ("project_id" in resolved) target.project_id = resolved.project_id ?? resolveProjectId(project);
|
|
279
|
-
}
|
|
280
|
-
return target;
|
|
281
90
|
}
|
|
282
91
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
* (dogfood #1, finding D2) — a warning nobody can act on.
|
|
293
|
-
*/
|
|
294
|
-
function printTerminalLoss(status) {
|
|
295
|
-
console.error("");
|
|
296
|
-
console.error(status.terminal_loss_statement);
|
|
297
|
-
console.error(status.terminal_loss_detail);
|
|
298
|
-
console.error(`Back up this directory: ${status.keystore.root}`);
|
|
299
|
-
console.error("");
|
|
92
|
+
function removedResponse(sub) {
|
|
93
|
+
const r = REMOVED[sub];
|
|
94
|
+
fail({
|
|
95
|
+
code: "COMMAND_REMOVED",
|
|
96
|
+
message: r.message,
|
|
97
|
+
hint: r.hint,
|
|
98
|
+
details: { was: `gitvault ${sub}` },
|
|
99
|
+
next_actions: r.next_actions,
|
|
100
|
+
});
|
|
300
101
|
}
|
|
301
102
|
|
|
302
103
|
/**
|
|
303
|
-
* Where the keystore lives — for verbs whose payload is not a `
|
|
304
|
-
* Exported: `repos create`
|
|
305
|
-
*
|
|
104
|
+
* Where the keystore lives — for verbs whose payload is not a `view`.
|
|
105
|
+
* Exported: `repos create` and `repos snapshot` print the same line after
|
|
106
|
+
* allocating/publishing, and must not restate this logic.
|
|
306
107
|
*/
|
|
307
108
|
export async function printKeystoreLocation() {
|
|
308
109
|
try {
|
|
309
110
|
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
310
|
-
console.error(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-keystore loss is terminal for
|
|
111
|
+
console.error(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-keystore loss is terminal for repo history`);
|
|
311
112
|
} catch {
|
|
312
113
|
// Never let a diagnostic line fail a command that already succeeded.
|
|
313
114
|
}
|
|
314
115
|
}
|
|
315
116
|
|
|
316
|
-
/**
|
|
317
|
-
* `run402 gitvault init` — allocate the project's vault.
|
|
318
|
-
*
|
|
319
|
-
* WHY THIS EXISTS AS ITS OWN VERB (dogfood #1, finding A). Until it did, the
|
|
320
|
-
* only way to allocate was `sdk.gitvault.init()` through the vendored SDK:
|
|
321
|
-
* `gitvault status` pointed at `run402 init`, which scaffolds the remote and
|
|
322
|
-
* says so in a comment; `gitvault push` and `git push run402` both 404'd and
|
|
323
|
-
* handed the user a raw `POST /gitvault/v1/vaults`. A published CLI that can
|
|
324
|
-
* do everything except start is not a usable product.
|
|
325
|
-
*
|
|
326
|
-
* It stays SEPARATE from `run402 init` on purpose: this is the step that mints
|
|
327
|
-
* key material on this machine and emits a one-shot recovery receipt, and
|
|
328
|
-
* whole-keystore loss is terminal for vault history. That belongs to a command
|
|
329
|
-
* the user typed, not to a setup command's side effects.
|
|
330
|
-
*/
|
|
331
|
-
async function init(args) {
|
|
332
|
-
const a = normalizeArgv(args);
|
|
333
|
-
const valueFlags = ["--project", "--org"];
|
|
334
|
-
assertKnownFlags(a, [...valueFlags, "--git-remote", "--no-remote", "--help", "-h"], valueFlags);
|
|
335
|
-
requirePositionalCount(a, valueFlags, {
|
|
336
|
-
min: 0, max: 0, command: "run402 gitvault init", missing: "",
|
|
337
|
-
});
|
|
338
|
-
if (a.includes("--git-remote") && a.includes("--no-remote")) {
|
|
339
|
-
fail({
|
|
340
|
-
code: "BAD_USAGE",
|
|
341
|
-
message: "--git-remote and --no-remote contradict each other.",
|
|
342
|
-
hint: "--git-remote creates a repository to add the remote to; --no-remote touches no git configuration at all.",
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
const projectId = resolveProjectId(flagValue(a, "--project"));
|
|
346
|
-
const orgId = flagValue(a, "--org") ?? await resolveOwningOrgId(projectId);
|
|
347
|
-
if (!orgId) {
|
|
348
|
-
fail({
|
|
349
|
-
code: "ORG_UNRESOLVED",
|
|
350
|
-
message: `Could not resolve the organization that owns ${projectId}.`,
|
|
351
|
-
hint: "Pass --org <org_id>, or check that this wallet can see the project (`run402 projects list`).",
|
|
352
|
-
details: { project_id: projectId },
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Whether to touch git at all. Mirrors `run402 init`: adding a remote inside
|
|
357
|
-
// an EXISTING repository is pure addition and is the default; CREATING a
|
|
358
|
-
// repository is opt-in, because a vault can be allocated from anywhere and
|
|
359
|
-
// `git init`-ing whatever directory you happened to be in is a bad surprise.
|
|
360
|
-
let scaffold = !a.includes("--no-remote");
|
|
361
|
-
let remoteSkipped = null;
|
|
362
|
-
if (scaffold && !a.includes("--git-remote")) {
|
|
363
|
-
const { hardenedGit } = await import("#sdk/node");
|
|
364
|
-
try {
|
|
365
|
-
await hardenedGit(process.cwd(), ["rev-parse", "--git-dir"]);
|
|
366
|
-
} catch {
|
|
367
|
-
scaffold = false;
|
|
368
|
-
remoteSkipped = "not a git repository — the vault was allocated; re-run with --git-remote to create one and add the remote";
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
try {
|
|
373
|
-
const result = await getSdk().gitvault.init({
|
|
374
|
-
org_id: orgId,
|
|
375
|
-
project_id: projectId,
|
|
376
|
-
...(scaffold ? { repo_dir: process.cwd() } : { scaffold_git: false }),
|
|
377
|
-
});
|
|
378
|
-
console.log(JSON.stringify(remoteSkipped ? { ...result, remote_skipped: remoteSkipped } : result, null, 2));
|
|
379
|
-
console.error(
|
|
380
|
-
result.deduplicated
|
|
381
|
-
? `vault ${result.repo_id} already existed — nothing was re-allocated and no new key material was minted`
|
|
382
|
-
: `allocated vault ${result.repo_id} (genesis ${result.genesis_sha256})`,
|
|
383
|
-
);
|
|
384
|
-
if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url} (${result.remote.reason})`);
|
|
385
|
-
if (remoteSkipped) console.error(`remote not added: ${remoteSkipped}`);
|
|
386
|
-
// The recovery receipt is integrity data, not a secret, and it is worth
|
|
387
|
-
// exactly as much as the number of copies you keep. It is persisted into
|
|
388
|
-
// the keystore automatically; say where, because "keep many copies" is
|
|
389
|
-
// advice nobody can act on without a path.
|
|
390
|
-
console.error("");
|
|
391
|
-
console.error(result.terminal_loss_statement);
|
|
392
|
-
await printKeystoreLocation();
|
|
393
|
-
console.error("");
|
|
394
|
-
} catch (err) {
|
|
395
|
-
reportSdkError(err);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* `run402 gitvault policy <required|grandfathered>` — the activation gate.
|
|
401
|
-
*
|
|
402
|
-
* The gateway's own `GITVAULT_CLIENT_UPGRADE_REQUIRED` envelope names
|
|
403
|
-
* `run402 gitvault policy grandfathered --reason <why>` as the second way out
|
|
404
|
-
* of a blocked deploy. Until this verb existed, running exactly what the
|
|
405
|
-
* platform told you to run returned UNKNOWN_SUBCOMMAND, so a user could
|
|
406
|
-
* allocate themselves into a blocked-deploy state with no way back.
|
|
407
|
-
*/
|
|
408
|
-
async function policy(args) {
|
|
409
|
-
const a = normalizeArgv(args);
|
|
410
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--reason"];
|
|
411
|
-
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
412
|
-
const [requested] = requirePositionalCount(a, valueFlags, {
|
|
413
|
-
min: 1, max: 1, command: "run402 gitvault policy <required|grandfathered>",
|
|
414
|
-
missing: "Missing <policy>. Expected `required` or `grandfathered`.",
|
|
415
|
-
});
|
|
416
|
-
if (requested !== "required" && requested !== "grandfathered") {
|
|
417
|
-
fail({
|
|
418
|
-
code: "BAD_USAGE",
|
|
419
|
-
message: `Unknown policy: ${requested}.`,
|
|
420
|
-
hint: "Expected `required` (a deploy must present a vaulted capture) or `grandfathered` (it need not).",
|
|
421
|
-
details: { policy: requested, known_policies: ["required", "grandfathered"] },
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
const reason = flagValue(a, "--reason");
|
|
425
|
-
// Required only for the weakening direction. Returning to `required` is
|
|
426
|
-
// restoring the default and needs no justification; leaving it does.
|
|
427
|
-
if (requested === "grandfathered" && (reason == null || reason.trim() === "")) {
|
|
428
|
-
fail({
|
|
429
|
-
code: "BAD_USAGE",
|
|
430
|
-
message: "`grandfathered` needs --reason <why>.",
|
|
431
|
-
hint: "It weakens the activation guarantee for this project and is recorded in the audit event. Say why, e.g. --reason \"migrating CI to a vaulted client\".",
|
|
432
|
-
details: { policy: requested },
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const target = await vaultTarget(a);
|
|
437
|
-
try {
|
|
438
|
-
const sdk = getSdk();
|
|
439
|
-
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
440
|
-
const result = await sdk.gitvault.setPolicy(repoId, {
|
|
441
|
-
gitvault_policy: requested,
|
|
442
|
-
...(reason != null ? { reason } : {}),
|
|
443
|
-
});
|
|
444
|
-
console.log(JSON.stringify({ repo_id: repoId, ...result }, null, 2));
|
|
445
|
-
console.error(
|
|
446
|
-
result.changed
|
|
447
|
-
? `gitvault_policy is now ${result.gitvault_policy} (version ${result.gitvault_policy_version})`
|
|
448
|
-
: `gitvault_policy was already ${result.gitvault_policy} — nothing changed`,
|
|
449
|
-
);
|
|
450
|
-
for (const w of result.warnings ?? []) console.error(`warning (${w.kind}): ${w.message}`);
|
|
451
|
-
} catch (err) {
|
|
452
|
-
reportSdkError(err);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* The vault's address in the form a human would actually type it: named
|
|
458
|
-
* (`run402::<org-slug>/<name>`) when this checkout's local pin resolved from
|
|
459
|
-
* one — an id-form pin buys nothing and is never written (see
|
|
460
|
-
* `gitvault-address.ts`'s own doc comment), so a non-null `s.pinned` always
|
|
461
|
-
* carries `resolved_from` — else id-form (`run402::<org_id>/<project_id>`),
|
|
462
|
-
* falling back to whichever of project_id/repo_id is known when the vault
|
|
463
|
-
* record itself is unavailable.
|
|
464
|
-
*/
|
|
465
|
-
function formatGitvaultAddress(s) {
|
|
466
|
-
if (s.pinned?.resolved_from) {
|
|
467
|
-
return `run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name}`;
|
|
468
|
-
}
|
|
469
|
-
const orgId = s.vault?.org_id ?? null;
|
|
470
|
-
const projectId = s.project_id ?? s.vault?.project_id ?? null;
|
|
471
|
-
if (orgId && projectId) return `run402::${orgId}/${projectId}`;
|
|
472
|
-
if (projectId) return projectId;
|
|
473
|
-
if (s.repo_id) return `repo ${s.repo_id}`;
|
|
474
|
-
return "(unresolved)";
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* `run402 gitvault status --human` (kychee-com/run402#569) — the five-liner:
|
|
479
|
-
* "status --refs is an admission-debugging protocol dump; the human question
|
|
480
|
-
* is five lines — remote URL, branch/HEAD, generation, bytes,
|
|
481
|
-
* can-this-machine-decrypt." Renders from `s` alone — the SAME status() call
|
|
482
|
-
* the JSON path already made, so `--human` costs no extra network read.
|
|
483
|
-
*
|
|
484
|
-
* Generations render DECIMAL, not the wire's 16-hex-digit form — a hex
|
|
485
|
-
* generation is a protocol detail, not something a human reads at a glance.
|
|
486
|
-
*
|
|
487
|
-
* The HEAD/ref-count line needs the vault's OWN ref map, which `status`
|
|
488
|
-
* fetches only when `--refs` is ALSO passed (materializing is a verification
|
|
489
|
-
* that advances local state — `status` alone stays a pure observation, see
|
|
490
|
-
* that option's own doc comment). Composing `--human --refs` gets the full
|
|
491
|
-
* line; `--human` alone names the omission rather than guessing from the
|
|
492
|
-
* local git checkout, which could easily disagree with what the vault holds.
|
|
493
|
-
*
|
|
494
|
-
* Warnings — including the progressive terminal-loss risk warning — are
|
|
495
|
-
* echoed EXACTLY as the SDK reported them (never reworded) and become an
|
|
496
|
-
* optional sixth line, present only when `s.warnings` is non-empty. Without
|
|
497
|
-
* `--human`, `run402 gitvault status` is unchanged: it always prints the
|
|
498
|
-
* FULL terminal-loss statement verbatim on stderr regardless of warnings;
|
|
499
|
-
* this compact view surfaces it only when it is actually live advice.
|
|
500
|
-
*/
|
|
501
|
-
async function formatGitvaultHuman(s) {
|
|
502
|
-
const lines = [];
|
|
503
|
-
const remotePart = s.remote
|
|
504
|
-
? ` (remote '${s.remote.name}'${s.remote.matches ? "" : " — points at a DIFFERENT project"})`
|
|
505
|
-
: " (no local remote)";
|
|
506
|
-
lines.push(`Address: ${formatGitvaultAddress(s)}${remotePart}`);
|
|
507
|
-
|
|
508
|
-
if (!s.vault) {
|
|
509
|
-
// A normal shape (protocol D183) — no vault allocated for this project
|
|
510
|
-
// yet. Nothing below this line is knowable, so it is not fabricated.
|
|
511
|
-
lines.push("Vault: not allocated yet for this project — run 'run402 gitvault init' to allocate one.");
|
|
512
|
-
if (s.warnings.length > 0) lines.push(`Warnings: ${s.warnings.map((w) => w.message).join(" ")}`);
|
|
513
|
-
return lines.join("\n");
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
if (s.refs) {
|
|
517
|
-
const count = Object.keys(s.refs).length;
|
|
518
|
-
const head = !s.head_target
|
|
519
|
-
? "(none yet)"
|
|
520
|
-
: s.head_target.kind === "symref"
|
|
521
|
-
? s.head_target.ref
|
|
522
|
-
: `detached @ ${s.head_target.oid}`;
|
|
523
|
-
lines.push(`HEAD: ${head} (${count} ref${count === 1 ? "" : "s"})`);
|
|
524
|
-
} else {
|
|
525
|
-
lines.push("HEAD: (not materialized — pass --refs to see HEAD/ref count)");
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const { generationToBigInt } = await import("#sdk/node");
|
|
529
|
-
const decimal = (g) => (g ? generationToBigInt(g).toString() : "none");
|
|
530
|
-
lines.push(`Generations: authenticated ${decimal(s.pins.highest_authenticated)}, materialized ${decimal(s.pins.highest_materialized)}`);
|
|
531
|
-
|
|
532
|
-
// Bytes + object count — pulled from the vault record `status()` ALREADY
|
|
533
|
-
// fetched (no new network read, per the ask). `objects` is per-object-kind
|
|
534
|
-
// counts; summed for one number a human can glance at.
|
|
535
|
-
const storage = s.vault.storage;
|
|
536
|
-
const objectCount = storage?.objects ? Object.values(storage.objects).reduce((sum, n) => sum + Number(n), 0) : null;
|
|
537
|
-
lines.push(storage ? `Storage: ${storage.source_bytes} byte(s)${objectCount != null ? ` across ${objectCount} object(s)` : ""}` : "Storage: unknown");
|
|
538
|
-
|
|
539
|
-
const decryptPart = !s.keystore.holds_repo_key
|
|
540
|
-
? "CANNOT decrypt (no key in this machine's keystore)"
|
|
541
|
-
: s.keystore.can_sign
|
|
542
|
-
? "can decrypt and publish"
|
|
543
|
-
: "can decrypt (read-only — no signing key)";
|
|
544
|
-
lines.push(`This machine: ${decryptPart}. Policy: ${s.gitvault_policy ?? "(none)"}`);
|
|
545
|
-
|
|
546
|
-
if (s.warnings.length > 0) lines.push(`Warnings: ${s.warnings.map((w) => w.message).join(" ")}`);
|
|
547
|
-
|
|
548
|
-
return lines.join("\n");
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
async function status(args) {
|
|
552
|
-
const a = normalizeArgv(args);
|
|
553
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--refs", "--human", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
554
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
555
|
-
min: 0, max: 0, command: "run402 gitvault status", missing: "",
|
|
556
|
-
});
|
|
557
|
-
// kychee-com/run402#569 — an explicit opt-in per the cli-output-contract
|
|
558
|
-
// (openspec/specs/cli-output-contract/spec.md: raw/human stdout REQUIRES
|
|
559
|
-
// one), the same shape `run402 up`'s own `--human` already uses. Without
|
|
560
|
-
// it, behavior is byte-identical to before this flag existed.
|
|
561
|
-
const human = a.includes("--human");
|
|
562
|
-
if (human && a.includes("--json")) {
|
|
563
|
-
fail({
|
|
564
|
-
code: "BAD_USAGE",
|
|
565
|
-
message: "--human cannot be combined with --json.",
|
|
566
|
-
details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") },
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
const target = await vaultTarget(a);
|
|
570
|
-
if (a.includes("--refs")) target.refs = true;
|
|
571
|
-
try {
|
|
572
|
-
const s = await getSdk().gitvault.status(target);
|
|
573
|
-
if (human) {
|
|
574
|
-
// The human view REPLACES the JSON dump — it is the sanctioned
|
|
575
|
-
// exception the CLI-wide `--json` no-op convention already carves out
|
|
576
|
-
// for a command's OWN `--human` flag (see argparse.mjs's header
|
|
577
|
-
// comment). No new network read: everything below is already present
|
|
578
|
-
// on `s`, the SAME status() call the JSON path made.
|
|
579
|
-
console.log(await formatGitvaultHuman(s));
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
console.log(JSON.stringify(s, null, 2));
|
|
583
|
-
printTerminalLoss(s);
|
|
584
|
-
// Two facts the user otherwise has to leave the CLI for: which vault this
|
|
585
|
-
// checkout is wired to, and what the control plane says is in it.
|
|
586
|
-
if (s.remote) {
|
|
587
|
-
// `matches` is a TRI-STATE (kychee-com/run402#562): `false` is a real
|
|
588
|
-
// mismatch; `null` only means a slug-form remote has not resolved on
|
|
589
|
-
// this machine yet — that is NOT evidence of anything wrong, so it
|
|
590
|
-
// gets a neutral note, never the mismatch warning.
|
|
591
|
-
const suffix =
|
|
592
|
-
s.remote.matches === false ? " ← points at a DIFFERENT project than this status"
|
|
593
|
-
: s.remote.matches === null ? ` (${s.remote.reason})`
|
|
594
|
-
: "";
|
|
595
|
-
console.error(`remote '${s.remote.name}': ${s.remote.url}${suffix}`);
|
|
596
|
-
}
|
|
597
|
-
// The id-pinning state (design D6, task 4.5): a slug-form remote pins
|
|
598
|
-
// repo_id in local git state the first time it resolves; id-form pins
|
|
599
|
-
// nothing (it needs no pin — see resolveGitvaultAddress's doc comment).
|
|
600
|
-
if (s.pinned) {
|
|
601
|
-
console.error(
|
|
602
|
-
`pinned: repo_id ${s.pinned.repo_id}` +
|
|
603
|
-
(s.pinned.resolved_from ? ` (resolved from run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name})` : ""),
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
if (s.refs) {
|
|
607
|
-
const names = Object.keys(s.refs).sort();
|
|
608
|
-
console.error(names.length === 0 ? "refs: (none yet)" : `refs (${names.length}):`);
|
|
609
|
-
for (const ref of names) console.error(` ${s.refs[ref]} ${ref}`);
|
|
610
|
-
if (s.head_target) {
|
|
611
|
-
console.error(s.head_target.kind === "symref" ? ` HEAD -> ${s.head_target.ref}` : ` HEAD ${s.head_target.oid} (detached)`);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
// Advisories are echoed EXACTLY as the SDK reported them. Nothing is
|
|
615
|
-
// synthesized here — in particular a project that has never deployed gets
|
|
616
|
-
// no deploy-related warning, because a vault-only project is a first-class
|
|
617
|
-
// shape (protocol D183), not a half-configured deploy.
|
|
618
|
-
for (const w of s.warnings) console.error(`warning (${w.kind}): ${w.message}`);
|
|
619
|
-
for (const n of s.next_actions) console.error(`next: ${n.action}${n.command ? ` — ${n.command}` : ""}`);
|
|
620
|
-
} catch (err) {
|
|
621
|
-
reportSdkError(err);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* D5 (repo-first-onramp task 2.5): one verb per operation. Renamed from
|
|
627
|
-
* `push` — `push` now means exactly one thing everywhere: `git push`. The
|
|
628
|
-
* capture lane keeps its old function name internally to minimize churn;
|
|
629
|
-
* only the dispatched SUBCOMMAND name changed (see `run()` below, where
|
|
630
|
-
* `gitvault push` survives one release as a deprecation-warning alias).
|
|
631
|
-
*/
|
|
632
|
-
/**
|
|
633
|
-
* D6 (repo-first-onramp task 4): when neither `--repo` nor `--project` was
|
|
634
|
-
* given explicitly, look at the local `run402`/`origin` remote (in that
|
|
635
|
-
* order, mirroring `scaffoldRemote`'s own naming) and, if it is a SLUG-form
|
|
636
|
-
* address (`run402::<org-slug>/<name>`), return the parsed address so
|
|
637
|
-
* `snapshot` can push-to-create through it — the same address-form
|
|
638
|
-
* resolution `git push` drives via the remote helper. `null` for an
|
|
639
|
-
* id-form remote, no remote at all, or an explicit `--repo`/`--project`.
|
|
640
|
-
*/
|
|
641
|
-
async function detectSlugFormRemote(a, repoDir) {
|
|
642
|
-
if (flagValue(a, "--repo") != null || flagValue(a, "--project") != null) return null;
|
|
643
|
-
const { hardenedGit } = await import("#sdk/node");
|
|
644
|
-
const { parseGitvaultRemoteUrl, gitvaultRemoteAddressForm } = await import("#sdk");
|
|
645
|
-
for (const name of ["run402", "origin"]) {
|
|
646
|
-
let url;
|
|
647
|
-
try {
|
|
648
|
-
url = (await hardenedGit(repoDir, ["remote", "get-url", name])).text().trim();
|
|
649
|
-
} catch {
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
if (!url) continue;
|
|
653
|
-
const address = parseGitvaultRemoteUrl(url);
|
|
654
|
-
if (address && gitvaultRemoteAddressForm(address) === "slug") return address;
|
|
655
|
-
}
|
|
656
|
-
return null;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
async function snapshot(args) {
|
|
660
|
-
const a = normalizeArgv(args);
|
|
661
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
|
|
662
|
-
assertKnownFlags(a, [...valueFlags, "--checkpoint", "--dry-run", "--help", "-h"], valueFlags);
|
|
663
|
-
requirePositionalCount(a, valueFlags, {
|
|
664
|
-
min: 0, max: 0, command: "run402 gitvault snapshot", missing: "",
|
|
665
|
-
});
|
|
666
|
-
const dryRun = a.includes("--dry-run");
|
|
667
|
-
const message = flagValue(a, "--message");
|
|
668
|
-
const repoDir = process.cwd();
|
|
669
|
-
const address = await detectSlugFormRemote(a, repoDir);
|
|
670
|
-
// D2: lazily allocate the vault on first push when there is a project to
|
|
671
|
-
// resolve the owning org from — the same resolution `gitvault init` uses.
|
|
672
|
-
// `--repo`-only addressing has nothing to create FROM (no project_id), so
|
|
673
|
-
// it is skipped there, matching `open()`'s own precedence. Skipped
|
|
674
|
-
// entirely for a slug-form remote (`address` above) — that resolves
|
|
675
|
-
// through the address, not a project_id, and needs no separate org_id.
|
|
676
|
-
//
|
|
677
|
-
// Skipped ENTIRELY for --dry-run (kychee-com/run402#565): org resolution
|
|
678
|
-
// exists only to feed lazy allocation, and a dry run never allocates — the
|
|
679
|
-
// read would cost a network round-trip for a fact `planPush` never uses.
|
|
680
|
-
const target = address ? { repo_dir: repoDir } : await vaultTarget(a);
|
|
681
|
-
const orgId = !address && !dryRun && target.project_id ? await resolveOwningOrgId(target.project_id) : null;
|
|
682
|
-
const opts = {
|
|
683
|
-
...target,
|
|
684
|
-
...(address ? { address } : {}),
|
|
685
|
-
...(orgId ? { org_id: orgId } : {}),
|
|
686
|
-
// The gitvault_commit line is progress, not payload: print it the moment
|
|
687
|
-
// the snapshot exists, well before the publication round-trips finish, so
|
|
688
|
-
// a human watching a slow push sees what is being pushed. Fires for a
|
|
689
|
-
// dry run too — the capture itself is real, local work.
|
|
690
|
-
onCommitLine: (line) => console.error(line),
|
|
691
|
-
// Fires synchronously, BEFORE the capture/publish that follows — printed
|
|
692
|
-
// here rather than deferred past `push()`'s return so the receipt is
|
|
693
|
-
// never lost if a later step in the SAME push fails after allocation
|
|
694
|
-
// already landed on the server. Never fires for --dry-run: `planPush`
|
|
695
|
-
// never allocates, so this callback is simply unused there.
|
|
696
|
-
onVaultCreated: async (created) => {
|
|
697
|
-
console.error("");
|
|
698
|
-
console.error(`vault allocated (genesis ${created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
|
|
699
|
-
console.error(JSON.stringify(created.recovery_receipt));
|
|
700
|
-
await printKeystoreLocation();
|
|
701
|
-
console.error("");
|
|
702
|
-
},
|
|
703
|
-
};
|
|
704
|
-
// The message rides on `snapshot`, which is what `captureSnapshot` reads —
|
|
705
|
-
// and, since 5.12b removed the dead top-level `push({ message })` field, is
|
|
706
|
-
// the ONE place it can ride.
|
|
707
|
-
if (message != null) opts.snapshot = { message };
|
|
708
|
-
if (a.includes("--checkpoint")) opts.checkpoint = true;
|
|
709
|
-
try {
|
|
710
|
-
if (dryRun) {
|
|
711
|
-
// kychee-com/run402#565: a REAL dry run — the same local pipeline
|
|
712
|
-
// `push` runs (capture, pack building, encryption sizing), stopping
|
|
713
|
-
// before the two network mutations. Nothing is published; the JSON
|
|
714
|
-
// report is the entire contract, so it goes on stdout like every other
|
|
715
|
-
// gitvault verb's payload.
|
|
716
|
-
const plan = await getSdk().gitvault.planPush(opts);
|
|
717
|
-
console.log(JSON.stringify(plan, null, 2));
|
|
718
|
-
if (plan.allocation_needed) {
|
|
719
|
-
console.error("dry-run: no vault allocated for this project yet — a real snapshot would allocate one first; object/byte sizing is not knowable until then");
|
|
720
|
-
} else {
|
|
721
|
-
console.error(
|
|
722
|
-
`dry-run: would publish generation ${plan.would_admit_generation} (${plan.would_admit_generation_decimal}, ${plan.form}) — ` +
|
|
723
|
-
`${plan.object_count} object(s), ${plan.encrypted_bytes} encrypted byte(s) (${plan.raw_bytes} raw)`,
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
const result = await getSdk().gitvault.push(opts);
|
|
729
|
-
console.log(JSON.stringify(result, null, 2));
|
|
730
|
-
console.error(`published generation ${result.generation} (${result.form})`);
|
|
731
|
-
// Design D6: the mirror result is reported BESIDE the vault outcome
|
|
732
|
-
// above, on its own line — a mirror failure never blocked the publish.
|
|
733
|
-
if (result.mirror_push?.outcome === "pushed") {
|
|
734
|
-
console.error(`mirror: pushed generation ${result.generation} (${result.mirror_push.summary?.objects_copied ?? 0} object(s) copied)`);
|
|
735
|
-
} else if (result.mirror_push?.outcome === "failed") {
|
|
736
|
-
console.error(`mirror: dual-push FAILED (deploy is unaffected) — ${result.mirror_push.error ?? "see mirror_push.summary.errors"}`);
|
|
737
|
-
}
|
|
738
|
-
} catch (err) {
|
|
739
|
-
reportSdkError(err);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
async function compact(args) {
|
|
744
|
-
const a = normalizeArgv(args);
|
|
745
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
746
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
747
|
-
min: 0, max: 0, command: "run402 gitvault compact", missing: "",
|
|
748
|
-
});
|
|
749
|
-
try {
|
|
750
|
-
const result = await getSdk().gitvault.compact(await vaultTarget(a));
|
|
751
|
-
console.log(JSON.stringify(result, null, 2));
|
|
752
|
-
console.error(
|
|
753
|
-
`checkpoint published at generation ${result.generation}: ` +
|
|
754
|
-
`${result.covered_refs} ref(s), ${result.covered_roots} retention root(s).`,
|
|
755
|
-
);
|
|
756
|
-
if (!result.cutoff_bound) {
|
|
757
|
-
// Say what actually happened rather than reporting a clean compaction:
|
|
758
|
-
// without a ticket no root can leave the map, so this run reclaimed
|
|
759
|
-
// nothing from expiry.
|
|
760
|
-
console.error(
|
|
761
|
-
"no retention-cutoff ticket was obtained, so roots were RETAINED — expiry is permissive. " +
|
|
762
|
-
"The checkpoint published, but no expired root left the map; re-run compact once the ticket route answers.",
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
} catch (err) {
|
|
766
|
-
reportSdkError(err);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/** Read a protocol object a prior planning run (or r402s-verify) wrote to disk. */
|
|
771
|
-
function readJsonFile(flag, path) {
|
|
772
|
-
let text;
|
|
773
|
-
try {
|
|
774
|
-
text = readFileSync(path, "utf8");
|
|
775
|
-
} catch (err) {
|
|
776
|
-
fail({
|
|
777
|
-
code: "BAD_USAGE",
|
|
778
|
-
message: `${flag} ${path} could not be read: ${err?.message ?? String(err)}`,
|
|
779
|
-
hint: "Point it at the file a prior `run402 gitvault prune` (or r402s-verify) wrote.",
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
try {
|
|
783
|
-
return JSON.parse(text);
|
|
784
|
-
} catch (err) {
|
|
785
|
-
fail({
|
|
786
|
-
code: "BAD_USAGE",
|
|
787
|
-
message: `${flag} ${path} is not valid JSON: ${err?.message ?? String(err)}`,
|
|
788
|
-
hint: "Pass the file verbatim; do not reformat or re-serialize it.",
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
async function prune(args) {
|
|
794
|
-
const a = normalizeArgv(args);
|
|
795
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--intent-core", "--verifier-receipt"];
|
|
796
|
-
assertKnownFlags(a, [...valueFlags, "--submit", "--wait", "--help", "-h"], valueFlags);
|
|
797
|
-
requirePositionalCount(a, valueFlags, {
|
|
798
|
-
min: 0, max: 0, command: "run402 gitvault prune", missing: "",
|
|
799
|
-
});
|
|
800
|
-
const submitting = a.includes("--submit");
|
|
801
|
-
const corePath = flagValue(a, "--intent-core");
|
|
802
|
-
const receiptPath = flagValue(a, "--verifier-receipt");
|
|
803
|
-
// Refuse the half-specified submit here rather than planning and silently
|
|
804
|
-
// discarding the flags — an agent that typed --submit meant to submit.
|
|
805
|
-
if (submitting && (corePath == null || receiptPath == null)) {
|
|
806
|
-
fail({
|
|
807
|
-
code: "BAD_USAGE",
|
|
808
|
-
message: "run402 gitvault prune --submit needs both --intent-core and --verifier-receipt.",
|
|
809
|
-
hint: "Plan first (`run402 gitvault prune`), save its `intent_core`, run r402s-verify against it, then submit both.",
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
if (!submitting && (corePath != null || receiptPath != null)) {
|
|
813
|
-
fail({
|
|
814
|
-
code: "BAD_USAGE",
|
|
815
|
-
message: "--intent-core / --verifier-receipt only apply with --submit.",
|
|
816
|
-
hint: "Add --submit, or drop the flags to plan.",
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
const opts = await vaultTarget(a);
|
|
820
|
-
if (submitting) {
|
|
821
|
-
opts.submit = {
|
|
822
|
-
core: readJsonFile("--intent-core", corePath),
|
|
823
|
-
verifier_receipt: readJsonFile("--verifier-receipt", receiptPath),
|
|
824
|
-
};
|
|
825
|
-
if (a.includes("--wait")) opts.submit.wait = {};
|
|
826
|
-
}
|
|
827
|
-
try {
|
|
828
|
-
const result = await getSdk().gitvault.prune(opts);
|
|
829
|
-
console.log(JSON.stringify(result, null, 2));
|
|
830
|
-
// Never imply a deletion. State what actually happened, then reproduce the
|
|
831
|
-
// SDK's own note verbatim rather than summarizing it.
|
|
832
|
-
if (!result.submitted) {
|
|
833
|
-
console.error(
|
|
834
|
-
result.blocked_reason
|
|
835
|
-
? `planned — nothing to submit: ${result.blocked_reason}`
|
|
836
|
-
: `planned — nothing submitted. ${result.object_candidates.length} object(s) proposed for deletion` +
|
|
837
|
-
`${result.deferred_object_count > 0 ? ` (${result.deferred_object_count} more deferred to a later intent)` : ""}` +
|
|
838
|
-
`; ${result.eligible_count} retention root(s) past their window, ${result.retained_count} retained.`,
|
|
839
|
-
);
|
|
840
|
-
if (result.intent_core_sha256) {
|
|
841
|
-
console.error(`intent_core_sha256: ${result.intent_core_sha256} — run r402s-verify against this core, then re-run with --submit.`);
|
|
842
|
-
}
|
|
843
|
-
} else if (result.confirmation?.outcome) {
|
|
844
|
-
console.error(
|
|
845
|
-
`submitted — the signed completion reports ${result.confirmation.deleted.length} deleted, ` +
|
|
846
|
-
`${result.confirmation.present.length} still present` +
|
|
847
|
-
`${result.confirmation.unadjudicated.length > 0 ? `, ${result.confirmation.unadjudicated.length} unadjudicated` : ""}.`,
|
|
848
|
-
);
|
|
849
|
-
} else {
|
|
850
|
-
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.");
|
|
851
|
-
}
|
|
852
|
-
console.error(result.note);
|
|
853
|
-
} catch (err) {
|
|
854
|
-
reportSdkError(err);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
async function verify(args) {
|
|
859
|
-
const a = normalizeArgv(args);
|
|
860
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--budget"];
|
|
861
|
-
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
862
|
-
requirePositionalCount(a, valueFlags, {
|
|
863
|
-
min: 0, max: 0, command: "run402 gitvault verify", missing: "",
|
|
864
|
-
});
|
|
865
|
-
const target = await vaultTarget(a);
|
|
866
|
-
const budget = flagValue(a, "--budget");
|
|
867
|
-
if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
|
|
868
|
-
try {
|
|
869
|
-
const state = await getSdk().gitvault.verify(target);
|
|
870
|
-
console.log(JSON.stringify(state, null, 2));
|
|
871
|
-
console.error(`verified through generation ${state.generation}`);
|
|
872
|
-
} catch (err) {
|
|
873
|
-
reportSdkError(err);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/**
|
|
878
|
-
* `run402 gitvault reconcile` — gitvault-human-envelopes task 4.1's ADD-path
|
|
879
|
-
* workaround (design D5's "session start" hook; `push`/`snapshot` also runs
|
|
880
|
-
* this itself, best-effort, on the "deploy time" hook — see its
|
|
881
|
-
* `reconcile_recipients` field).
|
|
882
|
-
*
|
|
883
|
-
* Wraps this vault's current epoch key to every org member who has published
|
|
884
|
-
* an encryption key but has no envelope on this vault yet. This is the
|
|
885
|
-
* WORKAROUND shape, not full epoch rotation (blocked on a protocol
|
|
886
|
-
* revision) — see `Gitvault.reconcileEnvelopeRecipients`'s doc comment for
|
|
887
|
-
* the honest scope: a newly-wrapped member gets this vault's ENTIRE history
|
|
888
|
-
* under V0's single epoch, not "from here forward."
|
|
889
|
-
*/
|
|
890
|
-
async function reconcile(args) {
|
|
891
|
-
const a = normalizeArgv(args);
|
|
892
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
893
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
894
|
-
min: 0, max: 0, command: "run402 gitvault reconcile", missing: "",
|
|
895
|
-
});
|
|
896
|
-
const target = await vaultTarget(a);
|
|
897
|
-
try {
|
|
898
|
-
const result = await getSdk().gitvault.reconcileEnvelopeRecipients(target);
|
|
899
|
-
console.log(JSON.stringify(result, null, 2));
|
|
900
|
-
if (result.wrapped.length > 0) {
|
|
901
|
-
console.error(`wrapped ${result.wrapped.length} new recipient(s) at epoch ${result.epoch}`);
|
|
902
|
-
}
|
|
903
|
-
if (result.already_covered.length > 0) {
|
|
904
|
-
console.error(`${result.already_covered.length} recipient(s) already covered`);
|
|
905
|
-
}
|
|
906
|
-
for (const s of result.skipped) {
|
|
907
|
-
if (s.reason === "pinned_key_mismatch") {
|
|
908
|
-
console.error(
|
|
909
|
-
`SKIPPED ${s.principal_id}: the org directory's key changed (was ${s.details?.pinned_fingerprint}, now ${s.details?.directory_fingerprint}) — this repo will not silently re-wrap under it; investigate before re-running`,
|
|
910
|
-
);
|
|
911
|
-
} else if (s.reason === "missing_public_key") {
|
|
912
|
-
console.error(`skipped ${s.principal_id}: the org directory has no public key on record yet for ${s.ek_fingerprint}`);
|
|
913
|
-
} else {
|
|
914
|
-
console.error(`skipped ${s.principal_id} (${s.ek_fingerprint}): ${s.reason}${s.details ? ` ${JSON.stringify(s.details)}` : ""}`);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
if (result.wrapped.length === 0 && result.skipped.length === 0 && result.already_covered.length === 0) {
|
|
918
|
-
console.error("nothing to reconcile — the org directory has no envelope-capable members yet");
|
|
919
|
-
}
|
|
920
|
-
} catch (err) {
|
|
921
|
-
reportSdkError(err);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// ─── mirror (gitvault-mirror-and-recover) ─────────────────────────────────
|
|
926
|
-
|
|
927
|
-
const LARGE_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* docs/agent-response-design.md's CLI pipe-contract row: stdout ALWAYS keeps
|
|
931
|
-
* the full JSON (never truncated), and when a result is large it is ALSO
|
|
932
|
-
* written to a private 0600 file with a one-line stderr breadcrumb naming the
|
|
933
|
-
* path. Best-effort — a spill failure never changes what the command reports.
|
|
934
|
-
*/
|
|
935
|
-
async function spillIfLarge(repoId, verb, payload) {
|
|
936
|
-
const json = JSON.stringify(payload, null, 2);
|
|
937
|
-
if (Buffer.byteLength(json, "utf8") <= LARGE_OUTPUT_THRESHOLD_BYTES) return;
|
|
938
|
-
try {
|
|
939
|
-
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
940
|
-
const dir = join(getGitvaultKeystoreRoot(), "reports");
|
|
941
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
942
|
-
const path = join(dir, `${verb}-${repoId ?? "unknown"}-${Date.now()}.json`);
|
|
943
|
-
writeFileSync(path, json, { mode: 0o600 });
|
|
944
|
-
console.error(`(this result is large — the full JSON was also written to ${path})`);
|
|
945
|
-
} catch {
|
|
946
|
-
// best-effort only; stdout already carries the full result regardless
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/** Design D8: both honesty statements, verbatim, wherever mirror status or recovery success is shown. */
|
|
951
|
-
function printMirrorHonesty(result) {
|
|
952
|
-
if (result.validity_not_freshness) console.error(result.validity_not_freshness);
|
|
953
|
-
if (result.keystore_still_required) console.error(result.keystore_still_required);
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
function resolveMirrorCredential(a) {
|
|
957
|
-
const profile = flagValue(a, "--profile");
|
|
958
|
-
const ambient = a.includes("--ambient");
|
|
959
|
-
if (profile != null && ambient) {
|
|
960
|
-
fail({
|
|
961
|
-
code: "BAD_USAGE",
|
|
962
|
-
message: "--profile and --ambient contradict each other.",
|
|
963
|
-
hint: "Pick one credential source for the s3:// destination.",
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
if (profile != null) return { kind: "profile", profile };
|
|
967
|
-
if (ambient) return { kind: "ambient" };
|
|
968
|
-
return undefined;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function formatMirrorDestination(destination) {
|
|
972
|
-
if (!destination) return "(none)";
|
|
973
|
-
return destination.kind === "s3" ? `s3://${destination.bucket}/${destination.prefix}` : destination.path;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
async function mirrorSetCmd(args) {
|
|
977
|
-
const a = normalizeArgv(args);
|
|
978
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--profile", "--region", "--endpoint"];
|
|
979
|
-
assertKnownFlags(a, [...valueFlags, "--ambient", "--help", "-h"], valueFlags);
|
|
980
|
-
const [destination] = requirePositionalCount(a, valueFlags, {
|
|
981
|
-
min: 1, max: 1, command: "run402 gitvault mirror set <destination>",
|
|
982
|
-
missing: "Missing <destination>. Expected s3://<bucket>[/<prefix>] or a directory path.",
|
|
983
|
-
});
|
|
984
|
-
const credential = resolveMirrorCredential(a);
|
|
985
|
-
const region = flagValue(a, "--region");
|
|
986
|
-
const endpoint = flagValue(a, "--endpoint");
|
|
987
|
-
const target = await vaultTarget(a);
|
|
988
|
-
try {
|
|
989
|
-
const result = await getSdk().gitvault.mirrorSet({
|
|
990
|
-
...target,
|
|
991
|
-
destination_url: destination,
|
|
992
|
-
...(credential ? { credential } : {}),
|
|
993
|
-
...(region != null ? { region } : {}),
|
|
994
|
-
...(endpoint != null ? { endpoint } : {}),
|
|
995
|
-
});
|
|
996
|
-
console.log(JSON.stringify(result, null, 2));
|
|
997
|
-
console.error(`mirror configured for ${result.repo_id} -> ${formatMirrorDestination(result.destination)}`);
|
|
998
|
-
console.error("run `run402 gitvault mirror sync` to backfill it now, then deploys will dual-push automatically.");
|
|
999
|
-
} catch (err) {
|
|
1000
|
-
reportSdkError(err);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
async function mirrorRemoveCmd(args) {
|
|
1005
|
-
const a = normalizeArgv(args);
|
|
1006
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1007
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 gitvault mirror remove", missing: "" });
|
|
1008
|
-
const target = await vaultTarget(a);
|
|
1009
|
-
try {
|
|
1010
|
-
const result = await getSdk().gitvault.mirrorRemove(target);
|
|
1011
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1012
|
-
console.error(
|
|
1013
|
-
result.removed
|
|
1014
|
-
? `mirror config removed for ${result.repo_id} — the mirror's OWN bytes were not touched`
|
|
1015
|
-
: `no mirror was configured for ${result.repo_id} — nothing to remove`,
|
|
1016
|
-
);
|
|
1017
|
-
} catch (err) {
|
|
1018
|
-
reportSdkError(err);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
async function mirrorStatusCmd(args) {
|
|
1023
|
-
const a = normalizeArgv(args);
|
|
1024
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1025
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 gitvault mirror status", missing: "" });
|
|
1026
|
-
const target = await vaultTarget(a);
|
|
1027
|
-
try {
|
|
1028
|
-
const result = await getSdk().gitvault.mirrorStatus(target);
|
|
1029
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1030
|
-
if (!result.configured) {
|
|
1031
|
-
console.error(`no mirror configured for ${result.repo_id}. Configure one: run402 gitvault mirror set <destination>`);
|
|
1032
|
-
} else {
|
|
1033
|
-
const currency = result.is_current === true ? "current" : result.is_current === false ? `STALE — ${result.closing_command}` : "unknown (mirror unreachable or vault unread)";
|
|
1034
|
-
console.error(`mirror ${result.destination}: mirrored generation ${result.mirrored_generation ?? "(none)"}, vault newest ${result.newest_generation ?? "(none)"} — ${currency}`);
|
|
1035
|
-
}
|
|
1036
|
-
printMirrorHonesty(result);
|
|
1037
|
-
} catch (err) {
|
|
1038
|
-
reportSdkError(err);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
async function mirrorSyncCmd(args) {
|
|
1043
|
-
const a = normalizeArgv(args);
|
|
1044
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1045
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 gitvault mirror sync", missing: "" });
|
|
1046
|
-
const target = await vaultTarget(a);
|
|
1047
|
-
try {
|
|
1048
|
-
const result = await getSdk().gitvault.mirrorSync(target);
|
|
1049
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1050
|
-
await spillIfLarge(result.repo_id, "mirror-sync", result);
|
|
1051
|
-
console.error(
|
|
1052
|
-
`mirror sync for ${result.repo_id}: ${result.objects_copied} copied, ${result.objects_already_present} already present` +
|
|
1053
|
-
`${result.objects_skipped_foreign_recipient > 0 ? `, ${result.objects_skipped_foreign_recipient} skipped (envelopes for other recipients — expected)` : ""}` +
|
|
1054
|
-
`${result.objects_failed > 0 ? `, ${result.objects_failed} FAILED` : ""} (${result.bytes_copied} byte(s) copied this run).`,
|
|
1055
|
-
);
|
|
1056
|
-
for (const e of result.errors) console.error(` failed: ${e.key} — ${e.error}`);
|
|
1057
|
-
printMirrorHonesty(result);
|
|
1058
|
-
} catch (err) {
|
|
1059
|
-
reportSdkError(err);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
async function mirrorVerifyCmd(args) {
|
|
1064
|
-
const a = normalizeArgv(args);
|
|
1065
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1066
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 gitvault mirror verify", missing: "" });
|
|
1067
|
-
const target = await vaultTarget(a);
|
|
1068
|
-
try {
|
|
1069
|
-
const result = await getSdk().gitvault.mirrorVerify(target);
|
|
1070
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1071
|
-
await spillIfLarge(result.repo_id, "mirror-verify", result);
|
|
1072
|
-
console.error(`mirror keyless verify for ${result.repo_id}: recoverable generation ${result.recovered_generation}${result.chain_break ? ` (chain break at ${result.chain_break.generation}: ${result.chain_break.reason})` : ""}.`);
|
|
1073
|
-
if (result.data_loss_detected) {
|
|
1074
|
-
console.error(`DATA LOSS DETECTED: ${result.absences.filter((x) => x.adjudication === "unexplained_absence").length} object(s) are unexplained absences.`);
|
|
1075
|
-
}
|
|
1076
|
-
printMirrorHonesty(result);
|
|
1077
|
-
} catch (err) {
|
|
1078
|
-
reportSdkError(err);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
async function mirror(args) {
|
|
1083
|
-
const a = normalizeArgv(args);
|
|
1084
|
-
if (!a[0] || hasHelp([a[0], ...a.slice(1)])) {
|
|
1085
|
-
console.log(HELP);
|
|
1086
|
-
process.exit(0);
|
|
1087
|
-
}
|
|
1088
|
-
const action = a[0];
|
|
1089
|
-
const rest = a.slice(1);
|
|
1090
|
-
// Deliberately if/else, not a switch statement (`policy()`'s own
|
|
1091
|
-
// precedent above): sync.test.ts's CLI-command scanner regexes switch
|
|
1092
|
-
// labels FLATLY across the whole file with no nesting awareness, so a
|
|
1093
|
-
// nested switch here would misattribute "set"/"remove"/"sync" as bogus
|
|
1094
|
-
// top-level `gitvault:set` etc. commands. `gitvault mirror <action>` is
|
|
1095
|
-
// ONE compound verb (SURFACE's `gitvault_mirror` row), not five leaf
|
|
1096
|
-
// commands.
|
|
1097
|
-
if (action === "set") return mirrorSetCmd(rest);
|
|
1098
|
-
if (action === "remove") return mirrorRemoveCmd(rest);
|
|
1099
|
-
if (action === "status") return mirrorStatusCmd(rest);
|
|
1100
|
-
if (action === "sync") return mirrorSyncCmd(rest);
|
|
1101
|
-
if (action === "verify") return mirrorVerifyCmd(rest);
|
|
1102
|
-
failUnknownSubcommand("gitvault mirror", action, {
|
|
1103
|
-
hint: "Run `run402 gitvault mirror --help` for usage.",
|
|
1104
|
-
extraSubcommands: ["set", "remove", "status", "sync", "verify"],
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
async function recover(args) {
|
|
1109
|
-
const a = normalizeArgv(args);
|
|
1110
|
-
const valueFlags = ["--out", "--repo", "--profile", "--region", "--endpoint"];
|
|
1111
|
-
assertKnownFlags(a, [...valueFlags, "--ambient", "--help", "-h"], valueFlags);
|
|
1112
|
-
const [source] = requirePositionalCount(a, valueFlags, {
|
|
1113
|
-
min: 1, max: 1, command: "run402 gitvault recover <source> --out <dir>",
|
|
1114
|
-
missing: "Missing <source>. Expected s3://<bucket>[/<prefix>] or a directory path.",
|
|
1115
|
-
});
|
|
1116
|
-
const outDir = flagValue(a, "--out");
|
|
1117
|
-
if (outDir == null) {
|
|
1118
|
-
fail({
|
|
1119
|
-
code: "BAD_USAGE",
|
|
1120
|
-
message: "run402 gitvault recover needs --out <dir>.",
|
|
1121
|
-
hint: "Where to materialize the recovered repository, e.g. --out ./restored",
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
const credential = resolveMirrorCredential(a);
|
|
1125
|
-
const repoId = flagValue(a, "--repo");
|
|
1126
|
-
const region = flagValue(a, "--region");
|
|
1127
|
-
const endpoint = flagValue(a, "--endpoint");
|
|
1128
|
-
try {
|
|
1129
|
-
const result = await getSdk().gitvault.recover({
|
|
1130
|
-
source, out_dir: outDir,
|
|
1131
|
-
...(repoId != null ? { repo_id: repoId } : {}),
|
|
1132
|
-
...(credential ? { credential } : {}),
|
|
1133
|
-
...(region != null ? { region } : {}),
|
|
1134
|
-
...(endpoint != null ? { endpoint } : {}),
|
|
1135
|
-
});
|
|
1136
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1137
|
-
await spillIfLarge(result.repo_id, "recover", result);
|
|
1138
|
-
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)` : "") + ".");
|
|
1139
|
-
if (result.data_loss_detected) {
|
|
1140
|
-
console.error(`DATA LOSS DETECTED: ${result.absences.filter((x) => x.adjudication === "unexplained_absence").length} object(s) are unexplained absences — see "absences" in the result above.`);
|
|
1141
|
-
}
|
|
1142
|
-
printMirrorHonesty(result);
|
|
1143
|
-
} catch (err) {
|
|
1144
|
-
reportSdkError(err);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
117
|
export async function run(sub, args) {
|
|
1149
118
|
const argv = Array.isArray(args) ? args : [];
|
|
1150
119
|
if (!sub || hasHelp([sub, ...argv])) {
|
|
@@ -1152,58 +121,44 @@ export async function run(sub, args) {
|
|
|
1152
121
|
process.exit(0);
|
|
1153
122
|
}
|
|
1154
123
|
switch (sub) {
|
|
1155
|
-
case "init":
|
|
1156
|
-
|
|
124
|
+
case "init":
|
|
125
|
+
movedResponse("init");
|
|
1157
126
|
break;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
await policy(argv);
|
|
127
|
+
case "status":
|
|
128
|
+
movedResponse("status");
|
|
1161
129
|
break;
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
await status(argv);
|
|
130
|
+
case "snapshot":
|
|
131
|
+
movedResponse("snapshot");
|
|
1165
132
|
break;
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
await snapshot(argv);
|
|
133
|
+
case "policy":
|
|
134
|
+
movedResponse("policy");
|
|
1169
135
|
break;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
// D5: one verb per operation — "push" now means exactly one thing,
|
|
1173
|
-
// `git push`. Retained as a deprecation-warning alias for ONE release
|
|
1174
|
-
// (pre-launch, the benchmark gate prefers the rename now over an
|
|
1175
|
-
// alias forever); it will be removed next release.
|
|
1176
|
-
console.error("`run402 gitvault push` is deprecated and will be removed in the next release — use `run402 gitvault snapshot` instead.");
|
|
1177
|
-
await snapshot(argv);
|
|
136
|
+
case "compact":
|
|
137
|
+
movedResponse("compact");
|
|
1178
138
|
break;
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
await compact(argv);
|
|
139
|
+
case "prune":
|
|
140
|
+
movedResponse("prune");
|
|
1182
141
|
break;
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
await prune(argv);
|
|
142
|
+
case "verify":
|
|
143
|
+
movedResponse("verify");
|
|
1186
144
|
break;
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
await verify(argv);
|
|
145
|
+
case "mirror":
|
|
146
|
+
movedResponse("mirror");
|
|
1190
147
|
break;
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
await reconcile(argv);
|
|
148
|
+
case "recover":
|
|
149
|
+
movedResponse("recover");
|
|
1194
150
|
break;
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
await mirror(argv);
|
|
151
|
+
case "push":
|
|
152
|
+
removedResponse("push");
|
|
1198
153
|
break;
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
await recover(argv);
|
|
154
|
+
case "reconcile":
|
|
155
|
+
removedResponse("reconcile");
|
|
1202
156
|
break;
|
|
1203
|
-
}
|
|
1204
157
|
default:
|
|
1205
|
-
|
|
1206
|
-
|
|
158
|
+
fail({
|
|
159
|
+
code: "UNKNOWN_SUBCOMMAND",
|
|
160
|
+
message: `run402 gitvault ${sub}: unknown, and gitvault itself is retired.`,
|
|
161
|
+
hint: "Run `run402 repos --help` for the current surface.",
|
|
1207
162
|
});
|
|
1208
163
|
}
|
|
1209
164
|
}
|