run402 4.39.1 → 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/assets.mjs +19 -26
- package/lib/cdn.mjs +4 -6
- package/lib/command-manifest.mjs +56 -25
- package/lib/config.mjs +30 -0
- package/lib/deploy-v2.mjs +15 -0
- package/lib/doctor.mjs +36 -3
- package/lib/gitvault-scaffold.mjs +1 -1
- package/lib/gitvault.mjs +121 -819
- 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.crypto.d.ts +10 -0
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.js +10 -0
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +285 -5
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +369 -8
- 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-keystore.d.ts +11 -1
- package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-keystore.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror-backend.d.ts +88 -0
- package/sdk/dist/node/gitvault-mirror-backend.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-mirror-backend.js +374 -0
- package/sdk/dist/node/gitvault-mirror-backend.js.map +1 -0
- package/sdk/dist/node/gitvault-mirror-config.d.ts +53 -0
- package/sdk/dist/node/gitvault-mirror-config.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-mirror-config.js +112 -0
- package/sdk/dist/node/gitvault-mirror-config.js.map +1 -0
- package/sdk/dist/node/gitvault-mirror.d.ts +120 -0
- package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-mirror.js +464 -0
- package/sdk/dist/node/gitvault-mirror.js.map +1 -0
- package/sdk/dist/node/gitvault-publication.d.ts +153 -3
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +182 -13
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
- package/sdk/dist/node/gitvault-recover.d.ts +136 -0
- package/sdk/dist/node/gitvault-recover.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-recover.js +412 -0
- package/sdk/dist/node/gitvault-recover.js.map +1 -0
- package/sdk/dist/node/gitvault-snapshot.d.ts +8 -0
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.js +11 -0
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
package/lib/gitvault.mjs
CHANGED
|
@@ -1,815 +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 { resolveOwningOrgId } from "./org-context.mjs";
|
|
22
|
-
import { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
23
|
-
import { getSdk } from "./sdk.mjs";
|
|
24
|
-
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
25
|
-
import {
|
|
26
|
-
normalizeArgv,
|
|
27
|
-
hasHelp,
|
|
28
|
-
assertKnownFlags,
|
|
29
|
-
parseIntegerFlag,
|
|
30
|
-
flagValue,
|
|
31
|
-
requirePositionalCount,
|
|
32
|
-
failUnknownSubcommand,
|
|
33
|
-
} from "./argparse.mjs";
|
|
34
|
-
|
|
35
|
-
/** Value-taking flags every gitvault subcommand accepts. */
|
|
36
|
-
const COMMON_VALUE_FLAGS = ["--project", "--repo"];
|
|
20
|
+
import { fail } from "./sdk-errors.mjs";
|
|
21
|
+
import { hasHelp } from "./argparse.mjs";
|
|
37
22
|
|
|
38
|
-
export const HELP = `run402 gitvault —
|
|
23
|
+
export const HELP = `run402 gitvault — RETIRED (repo-surface-consolidation)
|
|
39
24
|
|
|
40
25
|
Usage:
|
|
41
|
-
run402 gitvault
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
policy Set the activation policy — \`required\` (a deploy must present a
|
|
65
|
-
vaulted capture) or \`grandfathered\` (it need not). Owner + step-up,
|
|
66
|
-
audited. \`grandfathered\` is the documented way out of a deploy
|
|
67
|
-
blocked by GITVAULT_CLIENT_UPGRADE_REQUIRED, and leaves a
|
|
68
|
-
doctor-persistent warning until the project returns to \`required\`.
|
|
69
|
-
snapshot Capture the working tree and publish it. This is NOT gated on a
|
|
70
|
-
deploy — a vault-only project snapshots for months without one.
|
|
71
|
-
Against a project with no vault yet, this ALLOCATES one inline
|
|
72
|
-
(the six-stage creation, same as \`init\`) before publishing — one
|
|
73
|
-
command, no prior \`gitvault init\`. When --repo/--project are
|
|
74
|
-
both omitted and the local run402/origin remote is a slug-form
|
|
75
|
-
address (run402::<org-slug>/<name>), PUSH-TO-CREATES through it
|
|
76
|
-
instead (design D6) — same as pushing that name with \`git\`. The
|
|
77
|
-
one-shot recovery receipt and keystore path print to stderr the
|
|
78
|
-
moment that happens.
|
|
79
|
-
Before reporting a snapshot as landed the SDK compares finalization
|
|
80
|
-
receipts against the expected manifest and reads the admitted head
|
|
81
|
-
back from storage; a 200 alone is never enough. \`push\` is a
|
|
82
|
-
deprecation-warning alias for one release — it will be removed
|
|
83
|
-
next release. Once \`gitvault\` was the only publish verb; \`git
|
|
84
|
-
push\` is now the actual publish path (via the remote helper),
|
|
85
|
-
so \`push\` here was renamed to name what it does: one verb per
|
|
86
|
-
operation. \`--dry-run\` (kychee-com/run402#565) previews it
|
|
87
|
-
instead: the same real local pipeline, publishing nothing and
|
|
88
|
-
never allocating.
|
|
89
|
-
compact Publish a checkpoint covering the canonical refs, every root
|
|
90
|
-
unexpired at the cutoff, and the HEAD target, under a maintenance
|
|
91
|
-
lease so a concurrent cycle cannot race it.
|
|
92
|
-
prune Plan a prune, and — with both verifier receipts — submit it.
|
|
93
|
-
Two phases, because the protocol is two-phase; see below.
|
|
94
|
-
verify Verify the head chain from the authenticated pin up to the newest
|
|
95
|
-
listed generation. Fails closed on a regression, a gap, or a
|
|
96
|
-
transition descriptor this client cannot validate.
|
|
97
|
-
|
|
98
|
-
Options:
|
|
99
|
-
--project <id> Project whose vault to act on (defaults to the active project)
|
|
100
|
-
--org <org_id> init: the owning organization (resolved from the project
|
|
101
|
-
when omitted)
|
|
102
|
-
--git-remote init: 'git init' the current directory when it is not a
|
|
103
|
-
repository yet, so the run402 remote can be added there.
|
|
104
|
-
Opt-in: creating a repository where you did not ask for one
|
|
105
|
-
is a bad surprise, so without it a non-repository directory
|
|
106
|
-
allocates the vault and adds no remote.
|
|
107
|
-
--no-remote init: allocate the vault only; touch no git configuration
|
|
108
|
-
--reason <why> policy: why the policy is changing — recorded in the audit
|
|
109
|
-
event. REQUIRED for \`grandfathered\`, which is a deliberate
|
|
110
|
-
weakening of the activation guarantee.
|
|
111
|
-
--refs status: also materialize and report the vault's ref map and
|
|
112
|
-
HEAD target. This is a VERIFICATION (it walks the head
|
|
113
|
-
chain and advances the local materialized pin), which is
|
|
114
|
-
why plain \`status\` — an observation — does not do it.
|
|
115
|
-
--human status: a five/six-line human summary on stdout instead of
|
|
116
|
-
the JSON dump (kychee-com/run402#569; explicit opt-in per
|
|
117
|
-
the cli-output-contract). Address, remote; HEAD + ref count
|
|
118
|
-
(needs --refs too — otherwise the line names the omission);
|
|
119
|
-
generations in decimal; storage bytes/object count (from
|
|
120
|
-
this SAME status() call — no extra network read); whether
|
|
121
|
-
THIS machine can decrypt, and the policy; standing warnings,
|
|
122
|
-
if any, verbatim (a live terminal-loss risk may be the sixth
|
|
123
|
-
line). Rejected together with --json. No effect on plain
|
|
124
|
-
\`status\`'s own output, which is unchanged.
|
|
125
|
-
--repo <repo_id> Address the vault directly by id, skipping project lookup
|
|
126
|
-
--message <text> snapshot: commit message for the synthetic commit a dirty tree
|
|
127
|
-
produces (a clean tree pushes HEAD itself, no message used)
|
|
128
|
-
--checkpoint snapshot: force the checkpoint-bearing form regardless of delta size
|
|
129
|
-
--dry-run snapshot: a REAL preview (kychee-com/run402#565) — runs the actual
|
|
130
|
-
local pipeline (capture, pack building, encryption sizing) and
|
|
131
|
-
reports objects, encrypted bytes, refs, and the generation it
|
|
132
|
-
would admit as. Publishes NOTHING, and never allocates a vault
|
|
133
|
-
that does not exist yet (reports allocation_needed instead).
|
|
134
|
-
--budget <n> verify: heads to verify in this call. The verified prefix is
|
|
135
|
-
persisted, so a budget-exceeded run resumes where it stopped
|
|
136
|
-
instead of restarting.
|
|
137
|
-
--submit prune: submit the planned intent. Requires --intent-core and
|
|
138
|
-
--verifier-receipt.
|
|
139
|
-
--intent-core <path>
|
|
140
|
-
prune: the plan's \`intent_core\`, saved verbatim from a prior
|
|
141
|
-
planning run. A rebuilt core carries a different nonce, so
|
|
142
|
-
the r402s-verify receipt would no longer bind to it.
|
|
143
|
-
--verifier-receipt <path>
|
|
144
|
-
prune: r402s-verify's \`verifier_receipt\` over that core.
|
|
145
|
-
--wait prune: poll the submitted intent until the control-plane-
|
|
146
|
-
signed completion appears, instead of returning immediately.
|
|
147
|
-
--json No-op: stdout is already JSON.
|
|
148
|
-
|
|
149
|
-
prune is TWO PHASES, because the protocol is:
|
|
150
|
-
1. \`run402 gitvault prune\` plans. It walks the verified chain, computes the GC
|
|
151
|
-
root set, subtracts it, and prints a SIGNED \`intent_core\` plus its
|
|
152
|
-
\`intent_core_sha256\`. Nothing is submitted and nothing is deleted.
|
|
153
|
-
2. Run \`r402s-verify\` against that core, then re-run with
|
|
154
|
-
\`--submit --intent-core <core.json> --verifier-receipt <receipt.json>\`.
|
|
155
|
-
The intent carries TWO receipts over the same core, one per implementation:
|
|
156
|
-
this CLI produces the \`run402-cli\` half by restoring the latest checkpoint
|
|
157
|
-
and recomputing its commitments, and \`r402s-verify\` produces the other.
|
|
158
|
-
A second receipt from this lineage would prove nothing, so it is never
|
|
159
|
-
synthesized here.
|
|
160
|
-
Only the control-plane-signed completion says what was deleted, and only its
|
|
161
|
-
\`deleted\` result means the bytes are gone — \`present_after_attempt\` is a
|
|
162
|
-
FAILED deletion, never counted as a success. There is deliberately no purge
|
|
163
|
-
verb in V0 at all. Retention is an operational promise of the platform, not a
|
|
164
|
-
cryptographic guarantee against it.
|
|
165
|
-
|
|
166
|
-
Expiry is permissive, by design:
|
|
167
|
-
A retention root whose \`effective_admitted_at\` this client cannot resolve is
|
|
168
|
-
RETAINED, and a compact that cannot obtain a retention-cutoff ticket keeps
|
|
169
|
-
every root. That costs storage, never history.
|
|
170
|
-
|
|
171
|
-
Terminal loss (protocol §0):
|
|
172
|
-
In V0-A, whole-machine or whole-keystore loss is terminal for vault history
|
|
173
|
-
until human envelopes ship. \`status\` prints the full statement verbatim on
|
|
174
|
-
stderr and carries it in its JSON — read it before you rely on this.
|
|
175
|
-
|
|
176
|
-
Examples:
|
|
177
|
-
run402 gitvault init
|
|
178
|
-
run402 gitvault status --refs
|
|
179
|
-
run402 gitvault status --human
|
|
180
|
-
run402 gitvault snapshot --message "wip: refactor the parser"
|
|
181
|
-
run402 gitvault snapshot --dry-run
|
|
182
|
-
run402 gitvault policy grandfathered --reason "migrating CI to a vaulted client"
|
|
183
|
-
run402 gitvault verify --budget 500
|
|
184
|
-
run402 gitvault prune --project prj_1a2b3c
|
|
185
|
-
# \`gitvault push\` still works as a deprecation-warning alias for \`snapshot\`
|
|
186
|
-
# 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.
|
|
187
49
|
`;
|
|
188
50
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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 }],
|
|
208
89
|
});
|
|
209
|
-
const target = { repo_dir: repoDir };
|
|
210
|
-
if (repoId != null) target.repo_id = repoId;
|
|
211
|
-
// Only demand a project when one is actually needed: `--repo` alone is a
|
|
212
|
-
// complete address, and requiring one on top of it would make the
|
|
213
|
-
// cold-restart path fail for no reason.
|
|
214
|
-
if (repoId == null || project != null) {
|
|
215
|
-
if ("repo_id" in resolved && project == null) target.repo_id = resolved.repo_id;
|
|
216
|
-
// `resolveGitvaultTarget` reports its last (env/active) tier
|
|
217
|
-
// non-throwingly (`run402 doctor`'s call site needs that) — this call
|
|
218
|
-
// site is the one that historically failed closed with PROJECT_REQUIRED
|
|
219
|
-
// when nothing resolves anywhere, and still does: `resolveProjectId`
|
|
220
|
-
// re-derives the exact same env/active check and throws.
|
|
221
|
-
if ("project_id" in resolved) target.project_id = resolved.project_id ?? resolveProjectId(project);
|
|
222
|
-
}
|
|
223
|
-
return target;
|
|
224
90
|
}
|
|
225
91
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
* (dogfood #1, finding D2) — a warning nobody can act on.
|
|
236
|
-
*/
|
|
237
|
-
function printTerminalLoss(status) {
|
|
238
|
-
console.error("");
|
|
239
|
-
console.error(status.terminal_loss_statement);
|
|
240
|
-
console.error(status.terminal_loss_detail);
|
|
241
|
-
console.error(`Back up this directory: ${status.keystore.root}`);
|
|
242
|
-
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
|
+
});
|
|
243
101
|
}
|
|
244
102
|
|
|
245
103
|
/**
|
|
246
|
-
* Where the keystore lives — for verbs whose payload is not a `
|
|
247
|
-
* Exported: `repos create`
|
|
248
|
-
*
|
|
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.
|
|
249
107
|
*/
|
|
250
108
|
export async function printKeystoreLocation() {
|
|
251
109
|
try {
|
|
252
110
|
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
253
|
-
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`);
|
|
254
112
|
} catch {
|
|
255
113
|
// Never let a diagnostic line fail a command that already succeeded.
|
|
256
114
|
}
|
|
257
115
|
}
|
|
258
116
|
|
|
259
|
-
/**
|
|
260
|
-
* `run402 gitvault init` — allocate the project's vault.
|
|
261
|
-
*
|
|
262
|
-
* WHY THIS EXISTS AS ITS OWN VERB (dogfood #1, finding A). Until it did, the
|
|
263
|
-
* only way to allocate was `sdk.gitvault.init()` through the vendored SDK:
|
|
264
|
-
* `gitvault status` pointed at `run402 init`, which scaffolds the remote and
|
|
265
|
-
* says so in a comment; `gitvault push` and `git push run402` both 404'd and
|
|
266
|
-
* handed the user a raw `POST /gitvault/v1/vaults`. A published CLI that can
|
|
267
|
-
* do everything except start is not a usable product.
|
|
268
|
-
*
|
|
269
|
-
* It stays SEPARATE from `run402 init` on purpose: this is the step that mints
|
|
270
|
-
* key material on this machine and emits a one-shot recovery receipt, and
|
|
271
|
-
* whole-keystore loss is terminal for vault history. That belongs to a command
|
|
272
|
-
* the user typed, not to a setup command's side effects.
|
|
273
|
-
*/
|
|
274
|
-
async function init(args) {
|
|
275
|
-
const a = normalizeArgv(args);
|
|
276
|
-
const valueFlags = ["--project", "--org"];
|
|
277
|
-
assertKnownFlags(a, [...valueFlags, "--git-remote", "--no-remote", "--help", "-h"], valueFlags);
|
|
278
|
-
requirePositionalCount(a, valueFlags, {
|
|
279
|
-
min: 0, max: 0, command: "run402 gitvault init", missing: "",
|
|
280
|
-
});
|
|
281
|
-
if (a.includes("--git-remote") && a.includes("--no-remote")) {
|
|
282
|
-
fail({
|
|
283
|
-
code: "BAD_USAGE",
|
|
284
|
-
message: "--git-remote and --no-remote contradict each other.",
|
|
285
|
-
hint: "--git-remote creates a repository to add the remote to; --no-remote touches no git configuration at all.",
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
const projectId = resolveProjectId(flagValue(a, "--project"));
|
|
289
|
-
const orgId = flagValue(a, "--org") ?? await resolveOwningOrgId(projectId);
|
|
290
|
-
if (!orgId) {
|
|
291
|
-
fail({
|
|
292
|
-
code: "ORG_UNRESOLVED",
|
|
293
|
-
message: `Could not resolve the organization that owns ${projectId}.`,
|
|
294
|
-
hint: "Pass --org <org_id>, or check that this wallet can see the project (`run402 projects list`).",
|
|
295
|
-
details: { project_id: projectId },
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Whether to touch git at all. Mirrors `run402 init`: adding a remote inside
|
|
300
|
-
// an EXISTING repository is pure addition and is the default; CREATING a
|
|
301
|
-
// repository is opt-in, because a vault can be allocated from anywhere and
|
|
302
|
-
// `git init`-ing whatever directory you happened to be in is a bad surprise.
|
|
303
|
-
let scaffold = !a.includes("--no-remote");
|
|
304
|
-
let remoteSkipped = null;
|
|
305
|
-
if (scaffold && !a.includes("--git-remote")) {
|
|
306
|
-
const { hardenedGit } = await import("#sdk/node");
|
|
307
|
-
try {
|
|
308
|
-
await hardenedGit(process.cwd(), ["rev-parse", "--git-dir"]);
|
|
309
|
-
} catch {
|
|
310
|
-
scaffold = false;
|
|
311
|
-
remoteSkipped = "not a git repository — the vault was allocated; re-run with --git-remote to create one and add the remote";
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
const result = await getSdk().gitvault.init({
|
|
317
|
-
org_id: orgId,
|
|
318
|
-
project_id: projectId,
|
|
319
|
-
...(scaffold ? { repo_dir: process.cwd() } : { scaffold_git: false }),
|
|
320
|
-
});
|
|
321
|
-
console.log(JSON.stringify(remoteSkipped ? { ...result, remote_skipped: remoteSkipped } : result, null, 2));
|
|
322
|
-
console.error(
|
|
323
|
-
result.deduplicated
|
|
324
|
-
? `vault ${result.repo_id} already existed — nothing was re-allocated and no new key material was minted`
|
|
325
|
-
: `allocated vault ${result.repo_id} (genesis ${result.genesis_sha256})`,
|
|
326
|
-
);
|
|
327
|
-
if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url} (${result.remote.reason})`);
|
|
328
|
-
if (remoteSkipped) console.error(`remote not added: ${remoteSkipped}`);
|
|
329
|
-
// The recovery receipt is integrity data, not a secret, and it is worth
|
|
330
|
-
// exactly as much as the number of copies you keep. It is persisted into
|
|
331
|
-
// the keystore automatically; say where, because "keep many copies" is
|
|
332
|
-
// advice nobody can act on without a path.
|
|
333
|
-
console.error("");
|
|
334
|
-
console.error(result.terminal_loss_statement);
|
|
335
|
-
await printKeystoreLocation();
|
|
336
|
-
console.error("");
|
|
337
|
-
} catch (err) {
|
|
338
|
-
reportSdkError(err);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* `run402 gitvault policy <required|grandfathered>` — the activation gate.
|
|
344
|
-
*
|
|
345
|
-
* The gateway's own `GITVAULT_CLIENT_UPGRADE_REQUIRED` envelope names
|
|
346
|
-
* `run402 gitvault policy grandfathered --reason <why>` as the second way out
|
|
347
|
-
* of a blocked deploy. Until this verb existed, running exactly what the
|
|
348
|
-
* platform told you to run returned UNKNOWN_SUBCOMMAND, so a user could
|
|
349
|
-
* allocate themselves into a blocked-deploy state with no way back.
|
|
350
|
-
*/
|
|
351
|
-
async function policy(args) {
|
|
352
|
-
const a = normalizeArgv(args);
|
|
353
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--reason"];
|
|
354
|
-
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
355
|
-
const [requested] = requirePositionalCount(a, valueFlags, {
|
|
356
|
-
min: 1, max: 1, command: "run402 gitvault policy <required|grandfathered>",
|
|
357
|
-
missing: "Missing <policy>. Expected `required` or `grandfathered`.",
|
|
358
|
-
});
|
|
359
|
-
if (requested !== "required" && requested !== "grandfathered") {
|
|
360
|
-
fail({
|
|
361
|
-
code: "BAD_USAGE",
|
|
362
|
-
message: `Unknown policy: ${requested}.`,
|
|
363
|
-
hint: "Expected `required` (a deploy must present a vaulted capture) or `grandfathered` (it need not).",
|
|
364
|
-
details: { policy: requested, known_policies: ["required", "grandfathered"] },
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
const reason = flagValue(a, "--reason");
|
|
368
|
-
// Required only for the weakening direction. Returning to `required` is
|
|
369
|
-
// restoring the default and needs no justification; leaving it does.
|
|
370
|
-
if (requested === "grandfathered" && (reason == null || reason.trim() === "")) {
|
|
371
|
-
fail({
|
|
372
|
-
code: "BAD_USAGE",
|
|
373
|
-
message: "`grandfathered` needs --reason <why>.",
|
|
374
|
-
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\".",
|
|
375
|
-
details: { policy: requested },
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const target = await vaultTarget(a);
|
|
380
|
-
try {
|
|
381
|
-
const sdk = getSdk();
|
|
382
|
-
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
383
|
-
const result = await sdk.gitvault.setPolicy(repoId, {
|
|
384
|
-
gitvault_policy: requested,
|
|
385
|
-
...(reason != null ? { reason } : {}),
|
|
386
|
-
});
|
|
387
|
-
console.log(JSON.stringify({ repo_id: repoId, ...result }, null, 2));
|
|
388
|
-
console.error(
|
|
389
|
-
result.changed
|
|
390
|
-
? `gitvault_policy is now ${result.gitvault_policy} (version ${result.gitvault_policy_version})`
|
|
391
|
-
: `gitvault_policy was already ${result.gitvault_policy} — nothing changed`,
|
|
392
|
-
);
|
|
393
|
-
for (const w of result.warnings ?? []) console.error(`warning (${w.kind}): ${w.message}`);
|
|
394
|
-
} catch (err) {
|
|
395
|
-
reportSdkError(err);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* The vault's address in the form a human would actually type it: named
|
|
401
|
-
* (`run402::<org-slug>/<name>`) when this checkout's local pin resolved from
|
|
402
|
-
* one — an id-form pin buys nothing and is never written (see
|
|
403
|
-
* `gitvault-address.ts`'s own doc comment), so a non-null `s.pinned` always
|
|
404
|
-
* carries `resolved_from` — else id-form (`run402::<org_id>/<project_id>`),
|
|
405
|
-
* falling back to whichever of project_id/repo_id is known when the vault
|
|
406
|
-
* record itself is unavailable.
|
|
407
|
-
*/
|
|
408
|
-
function formatGitvaultAddress(s) {
|
|
409
|
-
if (s.pinned?.resolved_from) {
|
|
410
|
-
return `run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name}`;
|
|
411
|
-
}
|
|
412
|
-
const orgId = s.vault?.org_id ?? null;
|
|
413
|
-
const projectId = s.project_id ?? s.vault?.project_id ?? null;
|
|
414
|
-
if (orgId && projectId) return `run402::${orgId}/${projectId}`;
|
|
415
|
-
if (projectId) return projectId;
|
|
416
|
-
if (s.repo_id) return `repo ${s.repo_id}`;
|
|
417
|
-
return "(unresolved)";
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* `run402 gitvault status --human` (kychee-com/run402#569) — the five-liner:
|
|
422
|
-
* "status --refs is an admission-debugging protocol dump; the human question
|
|
423
|
-
* is five lines — remote URL, branch/HEAD, generation, bytes,
|
|
424
|
-
* can-this-machine-decrypt." Renders from `s` alone — the SAME status() call
|
|
425
|
-
* the JSON path already made, so `--human` costs no extra network read.
|
|
426
|
-
*
|
|
427
|
-
* Generations render DECIMAL, not the wire's 16-hex-digit form — a hex
|
|
428
|
-
* generation is a protocol detail, not something a human reads at a glance.
|
|
429
|
-
*
|
|
430
|
-
* The HEAD/ref-count line needs the vault's OWN ref map, which `status`
|
|
431
|
-
* fetches only when `--refs` is ALSO passed (materializing is a verification
|
|
432
|
-
* that advances local state — `status` alone stays a pure observation, see
|
|
433
|
-
* that option's own doc comment). Composing `--human --refs` gets the full
|
|
434
|
-
* line; `--human` alone names the omission rather than guessing from the
|
|
435
|
-
* local git checkout, which could easily disagree with what the vault holds.
|
|
436
|
-
*
|
|
437
|
-
* Warnings — including the progressive terminal-loss risk warning — are
|
|
438
|
-
* echoed EXACTLY as the SDK reported them (never reworded) and become an
|
|
439
|
-
* optional sixth line, present only when `s.warnings` is non-empty. Without
|
|
440
|
-
* `--human`, `run402 gitvault status` is unchanged: it always prints the
|
|
441
|
-
* FULL terminal-loss statement verbatim on stderr regardless of warnings;
|
|
442
|
-
* this compact view surfaces it only when it is actually live advice.
|
|
443
|
-
*/
|
|
444
|
-
async function formatGitvaultHuman(s) {
|
|
445
|
-
const lines = [];
|
|
446
|
-
const remotePart = s.remote
|
|
447
|
-
? ` (remote '${s.remote.name}'${s.remote.matches ? "" : " — points at a DIFFERENT project"})`
|
|
448
|
-
: " (no local remote)";
|
|
449
|
-
lines.push(`Address: ${formatGitvaultAddress(s)}${remotePart}`);
|
|
450
|
-
|
|
451
|
-
if (!s.vault) {
|
|
452
|
-
// A normal shape (protocol D183) — no vault allocated for this project
|
|
453
|
-
// yet. Nothing below this line is knowable, so it is not fabricated.
|
|
454
|
-
lines.push("Vault: not allocated yet for this project — run 'run402 gitvault init' to allocate one.");
|
|
455
|
-
if (s.warnings.length > 0) lines.push(`Warnings: ${s.warnings.map((w) => w.message).join(" ")}`);
|
|
456
|
-
return lines.join("\n");
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (s.refs) {
|
|
460
|
-
const count = Object.keys(s.refs).length;
|
|
461
|
-
const head = !s.head_target
|
|
462
|
-
? "(none yet)"
|
|
463
|
-
: s.head_target.kind === "symref"
|
|
464
|
-
? s.head_target.ref
|
|
465
|
-
: `detached @ ${s.head_target.oid}`;
|
|
466
|
-
lines.push(`HEAD: ${head} (${count} ref${count === 1 ? "" : "s"})`);
|
|
467
|
-
} else {
|
|
468
|
-
lines.push("HEAD: (not materialized — pass --refs to see HEAD/ref count)");
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const { generationToBigInt } = await import("#sdk/node");
|
|
472
|
-
const decimal = (g) => (g ? generationToBigInt(g).toString() : "none");
|
|
473
|
-
lines.push(`Generations: authenticated ${decimal(s.pins.highest_authenticated)}, materialized ${decimal(s.pins.highest_materialized)}`);
|
|
474
|
-
|
|
475
|
-
// Bytes + object count — pulled from the vault record `status()` ALREADY
|
|
476
|
-
// fetched (no new network read, per the ask). `objects` is per-object-kind
|
|
477
|
-
// counts; summed for one number a human can glance at.
|
|
478
|
-
const storage = s.vault.storage;
|
|
479
|
-
const objectCount = storage?.objects ? Object.values(storage.objects).reduce((sum, n) => sum + Number(n), 0) : null;
|
|
480
|
-
lines.push(storage ? `Storage: ${storage.source_bytes} byte(s)${objectCount != null ? ` across ${objectCount} object(s)` : ""}` : "Storage: unknown");
|
|
481
|
-
|
|
482
|
-
const decryptPart = !s.keystore.holds_repo_key
|
|
483
|
-
? "CANNOT decrypt (no key in this machine's keystore)"
|
|
484
|
-
: s.keystore.can_sign
|
|
485
|
-
? "can decrypt and publish"
|
|
486
|
-
: "can decrypt (read-only — no signing key)";
|
|
487
|
-
lines.push(`This machine: ${decryptPart}. Policy: ${s.gitvault_policy ?? "(none)"}`);
|
|
488
|
-
|
|
489
|
-
if (s.warnings.length > 0) lines.push(`Warnings: ${s.warnings.map((w) => w.message).join(" ")}`);
|
|
490
|
-
|
|
491
|
-
return lines.join("\n");
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async function status(args) {
|
|
495
|
-
const a = normalizeArgv(args);
|
|
496
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--refs", "--human", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
497
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
498
|
-
min: 0, max: 0, command: "run402 gitvault status", missing: "",
|
|
499
|
-
});
|
|
500
|
-
// kychee-com/run402#569 — an explicit opt-in per the cli-output-contract
|
|
501
|
-
// (openspec/specs/cli-output-contract/spec.md: raw/human stdout REQUIRES
|
|
502
|
-
// one), the same shape `run402 up`'s own `--human` already uses. Without
|
|
503
|
-
// it, behavior is byte-identical to before this flag existed.
|
|
504
|
-
const human = a.includes("--human");
|
|
505
|
-
if (human && a.includes("--json")) {
|
|
506
|
-
fail({
|
|
507
|
-
code: "BAD_USAGE",
|
|
508
|
-
message: "--human cannot be combined with --json.",
|
|
509
|
-
details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") },
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
const target = await vaultTarget(a);
|
|
513
|
-
if (a.includes("--refs")) target.refs = true;
|
|
514
|
-
try {
|
|
515
|
-
const s = await getSdk().gitvault.status(target);
|
|
516
|
-
if (human) {
|
|
517
|
-
// The human view REPLACES the JSON dump — it is the sanctioned
|
|
518
|
-
// exception the CLI-wide `--json` no-op convention already carves out
|
|
519
|
-
// for a command's OWN `--human` flag (see argparse.mjs's header
|
|
520
|
-
// comment). No new network read: everything below is already present
|
|
521
|
-
// on `s`, the SAME status() call the JSON path made.
|
|
522
|
-
console.log(await formatGitvaultHuman(s));
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
console.log(JSON.stringify(s, null, 2));
|
|
526
|
-
printTerminalLoss(s);
|
|
527
|
-
// Two facts the user otherwise has to leave the CLI for: which vault this
|
|
528
|
-
// checkout is wired to, and what the control plane says is in it.
|
|
529
|
-
if (s.remote) {
|
|
530
|
-
// `matches` is a TRI-STATE (kychee-com/run402#562): `false` is a real
|
|
531
|
-
// mismatch; `null` only means a slug-form remote has not resolved on
|
|
532
|
-
// this machine yet — that is NOT evidence of anything wrong, so it
|
|
533
|
-
// gets a neutral note, never the mismatch warning.
|
|
534
|
-
const suffix =
|
|
535
|
-
s.remote.matches === false ? " ← points at a DIFFERENT project than this status"
|
|
536
|
-
: s.remote.matches === null ? ` (${s.remote.reason})`
|
|
537
|
-
: "";
|
|
538
|
-
console.error(`remote '${s.remote.name}': ${s.remote.url}${suffix}`);
|
|
539
|
-
}
|
|
540
|
-
// The id-pinning state (design D6, task 4.5): a slug-form remote pins
|
|
541
|
-
// repo_id in local git state the first time it resolves; id-form pins
|
|
542
|
-
// nothing (it needs no pin — see resolveGitvaultAddress's doc comment).
|
|
543
|
-
if (s.pinned) {
|
|
544
|
-
console.error(
|
|
545
|
-
`pinned: repo_id ${s.pinned.repo_id}` +
|
|
546
|
-
(s.pinned.resolved_from ? ` (resolved from run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name})` : ""),
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
if (s.refs) {
|
|
550
|
-
const names = Object.keys(s.refs).sort();
|
|
551
|
-
console.error(names.length === 0 ? "refs: (none yet)" : `refs (${names.length}):`);
|
|
552
|
-
for (const ref of names) console.error(` ${s.refs[ref]} ${ref}`);
|
|
553
|
-
if (s.head_target) {
|
|
554
|
-
console.error(s.head_target.kind === "symref" ? ` HEAD -> ${s.head_target.ref}` : ` HEAD ${s.head_target.oid} (detached)`);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
// Advisories are echoed EXACTLY as the SDK reported them. Nothing is
|
|
558
|
-
// synthesized here — in particular a project that has never deployed gets
|
|
559
|
-
// no deploy-related warning, because a vault-only project is a first-class
|
|
560
|
-
// shape (protocol D183), not a half-configured deploy.
|
|
561
|
-
for (const w of s.warnings) console.error(`warning (${w.kind}): ${w.message}`);
|
|
562
|
-
for (const n of s.next_actions) console.error(`next: ${n.action}${n.command ? ` — ${n.command}` : ""}`);
|
|
563
|
-
} catch (err) {
|
|
564
|
-
reportSdkError(err);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* D5 (repo-first-onramp task 2.5): one verb per operation. Renamed from
|
|
570
|
-
* `push` — `push` now means exactly one thing everywhere: `git push`. The
|
|
571
|
-
* capture lane keeps its old function name internally to minimize churn;
|
|
572
|
-
* only the dispatched SUBCOMMAND name changed (see `run()` below, where
|
|
573
|
-
* `gitvault push` survives one release as a deprecation-warning alias).
|
|
574
|
-
*/
|
|
575
|
-
/**
|
|
576
|
-
* D6 (repo-first-onramp task 4): when neither `--repo` nor `--project` was
|
|
577
|
-
* given explicitly, look at the local `run402`/`origin` remote (in that
|
|
578
|
-
* order, mirroring `scaffoldRemote`'s own naming) and, if it is a SLUG-form
|
|
579
|
-
* address (`run402::<org-slug>/<name>`), return the parsed address so
|
|
580
|
-
* `snapshot` can push-to-create through it — the same address-form
|
|
581
|
-
* resolution `git push` drives via the remote helper. `null` for an
|
|
582
|
-
* id-form remote, no remote at all, or an explicit `--repo`/`--project`.
|
|
583
|
-
*/
|
|
584
|
-
async function detectSlugFormRemote(a, repoDir) {
|
|
585
|
-
if (flagValue(a, "--repo") != null || flagValue(a, "--project") != null) return null;
|
|
586
|
-
const { hardenedGit } = await import("#sdk/node");
|
|
587
|
-
const { parseGitvaultRemoteUrl, gitvaultRemoteAddressForm } = await import("#sdk");
|
|
588
|
-
for (const name of ["run402", "origin"]) {
|
|
589
|
-
let url;
|
|
590
|
-
try {
|
|
591
|
-
url = (await hardenedGit(repoDir, ["remote", "get-url", name])).text().trim();
|
|
592
|
-
} catch {
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
if (!url) continue;
|
|
596
|
-
const address = parseGitvaultRemoteUrl(url);
|
|
597
|
-
if (address && gitvaultRemoteAddressForm(address) === "slug") return address;
|
|
598
|
-
}
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async function snapshot(args) {
|
|
603
|
-
const a = normalizeArgv(args);
|
|
604
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
|
|
605
|
-
assertKnownFlags(a, [...valueFlags, "--checkpoint", "--dry-run", "--help", "-h"], valueFlags);
|
|
606
|
-
requirePositionalCount(a, valueFlags, {
|
|
607
|
-
min: 0, max: 0, command: "run402 gitvault snapshot", missing: "",
|
|
608
|
-
});
|
|
609
|
-
const dryRun = a.includes("--dry-run");
|
|
610
|
-
const message = flagValue(a, "--message");
|
|
611
|
-
const repoDir = process.cwd();
|
|
612
|
-
const address = await detectSlugFormRemote(a, repoDir);
|
|
613
|
-
// D2: lazily allocate the vault on first push when there is a project to
|
|
614
|
-
// resolve the owning org from — the same resolution `gitvault init` uses.
|
|
615
|
-
// `--repo`-only addressing has nothing to create FROM (no project_id), so
|
|
616
|
-
// it is skipped there, matching `open()`'s own precedence. Skipped
|
|
617
|
-
// entirely for a slug-form remote (`address` above) — that resolves
|
|
618
|
-
// through the address, not a project_id, and needs no separate org_id.
|
|
619
|
-
//
|
|
620
|
-
// Skipped ENTIRELY for --dry-run (kychee-com/run402#565): org resolution
|
|
621
|
-
// exists only to feed lazy allocation, and a dry run never allocates — the
|
|
622
|
-
// read would cost a network round-trip for a fact `planPush` never uses.
|
|
623
|
-
const target = address ? { repo_dir: repoDir } : await vaultTarget(a);
|
|
624
|
-
const orgId = !address && !dryRun && target.project_id ? await resolveOwningOrgId(target.project_id) : null;
|
|
625
|
-
const opts = {
|
|
626
|
-
...target,
|
|
627
|
-
...(address ? { address } : {}),
|
|
628
|
-
...(orgId ? { org_id: orgId } : {}),
|
|
629
|
-
// The gitvault_commit line is progress, not payload: print it the moment
|
|
630
|
-
// the snapshot exists, well before the publication round-trips finish, so
|
|
631
|
-
// a human watching a slow push sees what is being pushed. Fires for a
|
|
632
|
-
// dry run too — the capture itself is real, local work.
|
|
633
|
-
onCommitLine: (line) => console.error(line),
|
|
634
|
-
// Fires synchronously, BEFORE the capture/publish that follows — printed
|
|
635
|
-
// here rather than deferred past `push()`'s return so the receipt is
|
|
636
|
-
// never lost if a later step in the SAME push fails after allocation
|
|
637
|
-
// already landed on the server. Never fires for --dry-run: `planPush`
|
|
638
|
-
// never allocates, so this callback is simply unused there.
|
|
639
|
-
onVaultCreated: async (created) => {
|
|
640
|
-
console.error("");
|
|
641
|
-
console.error(`vault allocated (genesis ${created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
|
|
642
|
-
console.error(JSON.stringify(created.recovery_receipt));
|
|
643
|
-
await printKeystoreLocation();
|
|
644
|
-
console.error("");
|
|
645
|
-
},
|
|
646
|
-
};
|
|
647
|
-
// The message rides on `snapshot`, which is what `captureSnapshot` reads —
|
|
648
|
-
// and, since 5.12b removed the dead top-level `push({ message })` field, is
|
|
649
|
-
// the ONE place it can ride.
|
|
650
|
-
if (message != null) opts.snapshot = { message };
|
|
651
|
-
if (a.includes("--checkpoint")) opts.checkpoint = true;
|
|
652
|
-
try {
|
|
653
|
-
if (dryRun) {
|
|
654
|
-
// kychee-com/run402#565: a REAL dry run — the same local pipeline
|
|
655
|
-
// `push` runs (capture, pack building, encryption sizing), stopping
|
|
656
|
-
// before the two network mutations. Nothing is published; the JSON
|
|
657
|
-
// report is the entire contract, so it goes on stdout like every other
|
|
658
|
-
// gitvault verb's payload.
|
|
659
|
-
const plan = await getSdk().gitvault.planPush(opts);
|
|
660
|
-
console.log(JSON.stringify(plan, null, 2));
|
|
661
|
-
if (plan.allocation_needed) {
|
|
662
|
-
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");
|
|
663
|
-
} else {
|
|
664
|
-
console.error(
|
|
665
|
-
`dry-run: would publish generation ${plan.would_admit_generation} (${plan.would_admit_generation_decimal}, ${plan.form}) — ` +
|
|
666
|
-
`${plan.object_count} object(s), ${plan.encrypted_bytes} encrypted byte(s) (${plan.raw_bytes} raw)`,
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
const result = await getSdk().gitvault.push(opts);
|
|
672
|
-
console.log(JSON.stringify(result, null, 2));
|
|
673
|
-
console.error(`published generation ${result.generation} (${result.form})`);
|
|
674
|
-
} catch (err) {
|
|
675
|
-
reportSdkError(err);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async function compact(args) {
|
|
680
|
-
const a = normalizeArgv(args);
|
|
681
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
682
|
-
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
683
|
-
min: 0, max: 0, command: "run402 gitvault compact", missing: "",
|
|
684
|
-
});
|
|
685
|
-
try {
|
|
686
|
-
const result = await getSdk().gitvault.compact(await vaultTarget(a));
|
|
687
|
-
console.log(JSON.stringify(result, null, 2));
|
|
688
|
-
console.error(
|
|
689
|
-
`checkpoint published at generation ${result.generation}: ` +
|
|
690
|
-
`${result.covered_refs} ref(s), ${result.covered_roots} retention root(s).`,
|
|
691
|
-
);
|
|
692
|
-
if (!result.cutoff_bound) {
|
|
693
|
-
// Say what actually happened rather than reporting a clean compaction:
|
|
694
|
-
// without a ticket no root can leave the map, so this run reclaimed
|
|
695
|
-
// nothing from expiry.
|
|
696
|
-
console.error(
|
|
697
|
-
"no retention-cutoff ticket was obtained, so roots were RETAINED — expiry is permissive. " +
|
|
698
|
-
"The checkpoint published, but no expired root left the map; re-run compact once the ticket route answers.",
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
} catch (err) {
|
|
702
|
-
reportSdkError(err);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/** Read a protocol object a prior planning run (or r402s-verify) wrote to disk. */
|
|
707
|
-
function readJsonFile(flag, path) {
|
|
708
|
-
let text;
|
|
709
|
-
try {
|
|
710
|
-
text = readFileSync(path, "utf8");
|
|
711
|
-
} catch (err) {
|
|
712
|
-
fail({
|
|
713
|
-
code: "BAD_USAGE",
|
|
714
|
-
message: `${flag} ${path} could not be read: ${err?.message ?? String(err)}`,
|
|
715
|
-
hint: "Point it at the file a prior `run402 gitvault prune` (or r402s-verify) wrote.",
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
try {
|
|
719
|
-
return JSON.parse(text);
|
|
720
|
-
} catch (err) {
|
|
721
|
-
fail({
|
|
722
|
-
code: "BAD_USAGE",
|
|
723
|
-
message: `${flag} ${path} is not valid JSON: ${err?.message ?? String(err)}`,
|
|
724
|
-
hint: "Pass the file verbatim; do not reformat or re-serialize it.",
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
async function prune(args) {
|
|
730
|
-
const a = normalizeArgv(args);
|
|
731
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--intent-core", "--verifier-receipt"];
|
|
732
|
-
assertKnownFlags(a, [...valueFlags, "--submit", "--wait", "--help", "-h"], valueFlags);
|
|
733
|
-
requirePositionalCount(a, valueFlags, {
|
|
734
|
-
min: 0, max: 0, command: "run402 gitvault prune", missing: "",
|
|
735
|
-
});
|
|
736
|
-
const submitting = a.includes("--submit");
|
|
737
|
-
const corePath = flagValue(a, "--intent-core");
|
|
738
|
-
const receiptPath = flagValue(a, "--verifier-receipt");
|
|
739
|
-
// Refuse the half-specified submit here rather than planning and silently
|
|
740
|
-
// discarding the flags — an agent that typed --submit meant to submit.
|
|
741
|
-
if (submitting && (corePath == null || receiptPath == null)) {
|
|
742
|
-
fail({
|
|
743
|
-
code: "BAD_USAGE",
|
|
744
|
-
message: "run402 gitvault prune --submit needs both --intent-core and --verifier-receipt.",
|
|
745
|
-
hint: "Plan first (`run402 gitvault prune`), save its `intent_core`, run r402s-verify against it, then submit both.",
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
if (!submitting && (corePath != null || receiptPath != null)) {
|
|
749
|
-
fail({
|
|
750
|
-
code: "BAD_USAGE",
|
|
751
|
-
message: "--intent-core / --verifier-receipt only apply with --submit.",
|
|
752
|
-
hint: "Add --submit, or drop the flags to plan.",
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
const opts = await vaultTarget(a);
|
|
756
|
-
if (submitting) {
|
|
757
|
-
opts.submit = {
|
|
758
|
-
core: readJsonFile("--intent-core", corePath),
|
|
759
|
-
verifier_receipt: readJsonFile("--verifier-receipt", receiptPath),
|
|
760
|
-
};
|
|
761
|
-
if (a.includes("--wait")) opts.submit.wait = {};
|
|
762
|
-
}
|
|
763
|
-
try {
|
|
764
|
-
const result = await getSdk().gitvault.prune(opts);
|
|
765
|
-
console.log(JSON.stringify(result, null, 2));
|
|
766
|
-
// Never imply a deletion. State what actually happened, then reproduce the
|
|
767
|
-
// SDK's own note verbatim rather than summarizing it.
|
|
768
|
-
if (!result.submitted) {
|
|
769
|
-
console.error(
|
|
770
|
-
result.blocked_reason
|
|
771
|
-
? `planned — nothing to submit: ${result.blocked_reason}`
|
|
772
|
-
: `planned — nothing submitted. ${result.object_candidates.length} object(s) proposed for deletion` +
|
|
773
|
-
`${result.deferred_object_count > 0 ? ` (${result.deferred_object_count} more deferred to a later intent)` : ""}` +
|
|
774
|
-
`; ${result.eligible_count} retention root(s) past their window, ${result.retained_count} retained.`,
|
|
775
|
-
);
|
|
776
|
-
if (result.intent_core_sha256) {
|
|
777
|
-
console.error(`intent_core_sha256: ${result.intent_core_sha256} — run r402s-verify against this core, then re-run with --submit.`);
|
|
778
|
-
}
|
|
779
|
-
} else if (result.confirmation?.outcome) {
|
|
780
|
-
console.error(
|
|
781
|
-
`submitted — the signed completion reports ${result.confirmation.deleted.length} deleted, ` +
|
|
782
|
-
`${result.confirmation.present.length} still present` +
|
|
783
|
-
`${result.confirmation.unadjudicated.length > 0 ? `, ${result.confirmation.unadjudicated.length} unadjudicated` : ""}.`,
|
|
784
|
-
);
|
|
785
|
-
} else {
|
|
786
|
-
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.");
|
|
787
|
-
}
|
|
788
|
-
console.error(result.note);
|
|
789
|
-
} catch (err) {
|
|
790
|
-
reportSdkError(err);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
async function verify(args) {
|
|
795
|
-
const a = normalizeArgv(args);
|
|
796
|
-
const valueFlags = [...COMMON_VALUE_FLAGS, "--budget"];
|
|
797
|
-
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
798
|
-
requirePositionalCount(a, valueFlags, {
|
|
799
|
-
min: 0, max: 0, command: "run402 gitvault verify", missing: "",
|
|
800
|
-
});
|
|
801
|
-
const target = await vaultTarget(a);
|
|
802
|
-
const budget = flagValue(a, "--budget");
|
|
803
|
-
if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
|
|
804
|
-
try {
|
|
805
|
-
const state = await getSdk().gitvault.verify(target);
|
|
806
|
-
console.log(JSON.stringify(state, null, 2));
|
|
807
|
-
console.error(`verified through generation ${state.generation}`);
|
|
808
|
-
} catch (err) {
|
|
809
|
-
reportSdkError(err);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
117
|
export async function run(sub, args) {
|
|
814
118
|
const argv = Array.isArray(args) ? args : [];
|
|
815
119
|
if (!sub || hasHelp([sub, ...argv])) {
|
|
@@ -817,46 +121,44 @@ export async function run(sub, args) {
|
|
|
817
121
|
process.exit(0);
|
|
818
122
|
}
|
|
819
123
|
switch (sub) {
|
|
820
|
-
case "init":
|
|
821
|
-
|
|
124
|
+
case "init":
|
|
125
|
+
movedResponse("init");
|
|
126
|
+
break;
|
|
127
|
+
case "status":
|
|
128
|
+
movedResponse("status");
|
|
129
|
+
break;
|
|
130
|
+
case "snapshot":
|
|
131
|
+
movedResponse("snapshot");
|
|
132
|
+
break;
|
|
133
|
+
case "policy":
|
|
134
|
+
movedResponse("policy");
|
|
822
135
|
break;
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
await policy(argv);
|
|
136
|
+
case "compact":
|
|
137
|
+
movedResponse("compact");
|
|
826
138
|
break;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
await status(argv);
|
|
139
|
+
case "prune":
|
|
140
|
+
movedResponse("prune");
|
|
830
141
|
break;
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
await snapshot(argv);
|
|
142
|
+
case "verify":
|
|
143
|
+
movedResponse("verify");
|
|
834
144
|
break;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
// D5: one verb per operation — "push" now means exactly one thing,
|
|
838
|
-
// `git push`. Retained as a deprecation-warning alias for ONE release
|
|
839
|
-
// (pre-launch, the benchmark gate prefers the rename now over an
|
|
840
|
-
// alias forever); it will be removed next release.
|
|
841
|
-
console.error("`run402 gitvault push` is deprecated and will be removed in the next release — use `run402 gitvault snapshot` instead.");
|
|
842
|
-
await snapshot(argv);
|
|
145
|
+
case "mirror":
|
|
146
|
+
movedResponse("mirror");
|
|
843
147
|
break;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
await compact(argv);
|
|
148
|
+
case "recover":
|
|
149
|
+
movedResponse("recover");
|
|
847
150
|
break;
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
await prune(argv);
|
|
151
|
+
case "push":
|
|
152
|
+
removedResponse("push");
|
|
851
153
|
break;
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
await verify(argv);
|
|
154
|
+
case "reconcile":
|
|
155
|
+
removedResponse("reconcile");
|
|
855
156
|
break;
|
|
856
|
-
}
|
|
857
157
|
default:
|
|
858
|
-
|
|
859
|
-
|
|
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.",
|
|
860
162
|
});
|
|
861
163
|
}
|
|
862
164
|
}
|