run402 4.36.1 → 4.37.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 +15 -0
- package/cli.mjs +6 -0
- package/git-remote-run402.mjs +69 -3
- package/lib/command-manifest.mjs +13 -1
- package/lib/gitvault-scaffold.mjs +73 -0
- package/lib/gitvault.mjs +72 -22
- package/lib/init.mjs +8 -3
- package/lib/projects.mjs +16 -0
- package/lib/repos.mjs +326 -0
- package/lib/up.mjs +167 -29
- package/package.json +1 -1
- package/sdk/dist/errors.d.ts +1 -1
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +1 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +9 -0
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +133 -16
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +207 -16
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/node/gitvault-apply.d.ts +18 -1
- package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-apply.js +38 -1
- package/sdk/dist/node/gitvault-apply.js.map +1 -1
- package/sdk/dist/node/gitvault-creation-journal.d.ts +22 -0
- package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-creation-journal.js +28 -0
- package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
- package/sdk/dist/node/gitvault-open-or-create.d.ts +59 -0
- package/sdk/dist/node/gitvault-open-or-create.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-open-or-create.js +62 -0
- package/sdk/dist/node/gitvault-open-or-create.js.map +1 -0
- package/sdk/dist/node/index.d.ts +3 -1
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +4 -1
- package/sdk/dist/node/index.js.map +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,21 @@ For app manifests with `verify.http[]`, `up` runs HTTP checks after deploy. Fres
|
|
|
52
52
|
|
|
53
53
|
For typed `run402.deploy.ts` configs, pass `--manifest` explicitly because TypeScript/JavaScript configs execute local code. Use `--check` for local-only import/normalize/file validation, `--print-spec` to inspect the normalized `ReleaseSpec`, `--plan` for a gateway-reviewed non-deploying plan, and `--require-plan <plan_id>` to apply only that reviewed intent. Warning flags are not used with `--require-plan`; the reviewed plan binds the exact warning/destructive set. Run402 Core skips Cloud allowance/tier prerequisites and fails closed when no Core project is selected.
|
|
54
54
|
|
|
55
|
+
### Vault-only repos (zero deploy ceremony)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
run402 init # once per machine
|
|
59
|
+
git remote add origin run402::<org_id>/<project_id>
|
|
60
|
+
git push -u origin main # allocates the vault on first push, publishes
|
|
61
|
+
|
|
62
|
+
# or, one call: provision + allocate + scaffold, nothing deployed
|
|
63
|
+
run402 repos create my-notes
|
|
64
|
+
run402 repos list --org org_1a2b3c
|
|
65
|
+
run402 repos delete prj_xyz --force # refuses without --force while the vault holds generations
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A hosted git remote, encrypted before it leaves the machine — no deploy, no manifest, no app. `origin` is claimed additively — an existing `origin` is never touched, the run402 remote falls back to `run402` instead. `repos create|list|delete` and `gitvault`'s mutating verbs (`init`, `snapshot`, `policy`, `compact`, `prune`) are CLI/OpenClaw-only by design — no MCP tool exists or will exist for them (one-shot recovery receipts, immutable generations, irreversible delete). See `run402 gitvault --help` and `run402 repos --help` for the full surface, and the CLI reference's `gitvault` / `repos` sections for the terminal-loss statement and the progressive backup warning.
|
|
69
|
+
|
|
55
70
|
### Allowance
|
|
56
71
|
|
|
57
72
|
```bash
|
package/cli.mjs
CHANGED
|
@@ -32,6 +32,7 @@ SET UP — get an agent funded and entitled
|
|
|
32
32
|
BUILD & SHIP — the app and everything it serves
|
|
33
33
|
up Provision/link/deploy the current app with SDK orchestration
|
|
34
34
|
projects Manage projects (provision, list, get, sql, delete)
|
|
35
|
+
repos Vault-only hosted encrypted repos, zero deploy ceremony (create, list, delete)
|
|
35
36
|
deploy Unified deploy operations (requires active tier)
|
|
36
37
|
apply Alias for deploy apply; supports --rehearse for migration rehearsal
|
|
37
38
|
functions Manage serverless functions (deploy, invoke, logs, list, delete)
|
|
@@ -221,6 +222,11 @@ switch (cmd) {
|
|
|
221
222
|
await run(sub, rest);
|
|
222
223
|
break;
|
|
223
224
|
}
|
|
225
|
+
case "repos": {
|
|
226
|
+
const { run } = await import("./lib/repos.mjs");
|
|
227
|
+
await run(sub, rest);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
224
230
|
case "apply": {
|
|
225
231
|
const { runDeployV2 } = await import("./lib/deploy-v2.mjs");
|
|
226
232
|
await runDeployV2("apply", [sub, ...rest].filter(Boolean));
|
package/git-remote-run402.mjs
CHANGED
|
@@ -19,6 +19,15 @@
|
|
|
19
19
|
* push [+]<src>:<dst> → publish one atomic ref transaction
|
|
20
20
|
* option <name> <value> → ok / unsupported, never a silent lie
|
|
21
21
|
*
|
|
22
|
+
* LAZY ALLOCATION ON FIRST PUSH (design D2). `list` never creates anything —
|
|
23
|
+
* an unallocated vault reports as an empty ref set, exactly what a fresh
|
|
24
|
+
* repository looks like — but `push` does: `git push origin main` against a
|
|
25
|
+
* project whose vault does not exist yet runs the six-stage creation journal
|
|
26
|
+
* inline (`r.gitvault.openOrCreate`, the SDK-owned primitive), prints the
|
|
27
|
+
* one-shot recovery receipt and the keystore path to stderr, and then
|
|
28
|
+
* completes the push. One command, no prior `gitvault init`. `git ls-remote`
|
|
29
|
+
* / `fetch` stay pure reads and allocate nothing.
|
|
30
|
+
*
|
|
22
31
|
* WHICH REPOSITORY (the fail-closed rule). `process.cwd()` is NOT the
|
|
23
32
|
* repository. git identifies the repository with `GIT_DIR`, and during
|
|
24
33
|
* `git clone` cwd is the directory clone was RUN FROM — routinely some other,
|
|
@@ -120,7 +129,11 @@ async function main(argv) {
|
|
|
120
129
|
return 1;
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
|
|
132
|
+
// `org_id` rides in the parsed address (`run402::<org_id>/<project_id>`),
|
|
133
|
+
// so it costs nothing extra to carry — it is exactly what D2's lazy
|
|
134
|
+
// creation needs to allocate an unresolved vault from `runPush` below, with
|
|
135
|
+
// no separate lookup.
|
|
136
|
+
const target = { project_id: address.project_id, org_id: address.org_id };
|
|
124
137
|
let verbosity = 1;
|
|
125
138
|
|
|
126
139
|
/**
|
|
@@ -150,8 +163,61 @@ async function main(argv) {
|
|
|
150
163
|
/** Open the vault lazily — `capabilities` and `option` must never touch the network. */
|
|
151
164
|
const openVault = async (repoDir) => (await getSdk().gitvault.open(repoDir ? { ...target, repo_dir: repoDir } : target)).vault;
|
|
152
165
|
|
|
166
|
+
/** A 404/absent-vault refusal — the "nothing here yet" shape, never a genuine failure to mask. */
|
|
167
|
+
function isVaultNotFound(err) {
|
|
168
|
+
return err?.status === 404 || err?.code === "RESOURCE_NOT_FOUND" || err?.code === "ROUTE_NOT_FOUND";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Open the vault, allocating it first when it does not exist yet (D2). Used
|
|
173
|
+
* ONLY by `runPush` — `list`/`fetch` stay pure reads and never create
|
|
174
|
+
* anything (see `runList`'s own not-found handling below).
|
|
175
|
+
*
|
|
176
|
+
* Prints the one-shot recovery receipt and the keystore path to stderr the
|
|
177
|
+
* moment allocation happens, per the client-surface spec: an agent reads
|
|
178
|
+
* stderr, and the receipt is worth exactly as many copies as get kept.
|
|
179
|
+
*/
|
|
180
|
+
async function openOrCreateVault(repoDir) {
|
|
181
|
+
const result = await getSdk().gitvault.openOrCreate({ ...target, repo_dir: repoDir });
|
|
182
|
+
if (!result.found && result.created) {
|
|
183
|
+
note("");
|
|
184
|
+
note(`vault ${result.handle.repo_id} allocated (genesis ${result.created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
|
|
185
|
+
note(JSON.stringify(result.created.recovery_receipt));
|
|
186
|
+
try {
|
|
187
|
+
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
188
|
+
note(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship`);
|
|
189
|
+
} catch {
|
|
190
|
+
// Never let a diagnostic line fail a push that already allocated successfully.
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const { GITVAULT_ALLOCATION_POLICY_ADVISORY } = await import("./lib/gitvault-scaffold.mjs");
|
|
194
|
+
note(GITVAULT_ALLOCATION_POLICY_ADVISORY);
|
|
195
|
+
} catch {
|
|
196
|
+
// Same rule: advisory only, never fatal.
|
|
197
|
+
}
|
|
198
|
+
note("");
|
|
199
|
+
}
|
|
200
|
+
return result.handle.vault;
|
|
201
|
+
}
|
|
202
|
+
|
|
153
203
|
async function runList() {
|
|
154
|
-
|
|
204
|
+
let state;
|
|
205
|
+
try {
|
|
206
|
+
state = await (await openVault()).materialize();
|
|
207
|
+
} catch (err) {
|
|
208
|
+
// An unallocated vault is not an error here: `list` is the read half of
|
|
209
|
+
// the protocol dance and must never create anything on its own (D2
|
|
210
|
+
// scopes lazy creation to `push`). Reporting it as an EMPTY ref set is
|
|
211
|
+
// exactly what a fresh repository looks like to git, and `push` still
|
|
212
|
+
// runs `list` first either way — this is what lets a first push land in
|
|
213
|
+
// one command instead of `list` failing the whole exchange before
|
|
214
|
+
// `push` ever gets a turn.
|
|
215
|
+
if (isVaultNotFound(err)) {
|
|
216
|
+
endBlock();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
155
221
|
const refs = state.refs ?? {};
|
|
156
222
|
for (const ref of Object.keys(refs).sort()) out(`${refs[ref]} ${ref}`);
|
|
157
223
|
const head = state.head_target;
|
|
@@ -198,7 +264,7 @@ async function main(argv) {
|
|
|
198
264
|
? null
|
|
199
265
|
: (await hardenedGit(repoDir, ["rev-parse", "--verify", "--end-of-options", spec.src])).text().trim());
|
|
200
266
|
}
|
|
201
|
-
const vault = await
|
|
267
|
+
const vault = await openOrCreateVault(repoDir);
|
|
202
268
|
const base = await vault.materialize();
|
|
203
269
|
const updates = [];
|
|
204
270
|
for (const spec of specs) {
|
package/lib/command-manifest.mjs
CHANGED
|
@@ -257,11 +257,23 @@ export const COMMAND_MANIFEST = [
|
|
|
257
257
|
// universal `{}` fetch mock or touch the gate's own checkout.
|
|
258
258
|
{ path: ["gitvault", "init"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "allocates a vault: mints key material on this machine and runs the six-stage creation journal" },
|
|
259
259
|
{ path: ["gitvault", "status"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the local principal keystore and the live vault record" },
|
|
260
|
-
{ path: ["gitvault", "
|
|
260
|
+
{ path: ["gitvault", "snapshot"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "captures the cwd git working tree and publishes a signed head" },
|
|
261
|
+
// D5 (repo-first-onramp task 2.5): `push` is a deprecation-warning alias
|
|
262
|
+
// for `snapshot`, retained for exactly one release — same structural
|
|
263
|
+
// shape, same skip reason, so it stays covered by this gate too.
|
|
264
|
+
{ path: ["gitvault", "push"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "deprecated alias for `snapshot` — captures the cwd git working tree and publishes a signed head" },
|
|
261
265
|
{ path: ["gitvault", "policy"], positionals: [p("gitvault_policy")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["required"], runStyle: "sub", skipBehavioral: "owner + step-up mutation of the live project's activation policy" },
|
|
262
266
|
{ path: ["gitvault", "compact"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "takes a maintenance lease and builds a checkpoint from the local repository" },
|
|
263
267
|
{ path: ["gitvault", "prune"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "materializes the live vault head to enumerate retention roots" },
|
|
264
268
|
{ path: ["gitvault", "verify"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "walks the live head chain against the keystore's authenticated pin" },
|
|
269
|
+
|
|
270
|
+
// ── repos (vault-only porcelain, repo-first-onramp D8, task 2.6) ────────
|
|
271
|
+
// `create` writes real git state into cwd and allocates a vault; `list`
|
|
272
|
+
// and `delete` read/mutate the live gitvault record — same structural-
|
|
273
|
+
// checks-only rationale as the gitvault family just above.
|
|
274
|
+
{ path: ["repos", "create"], positionals: [p("name")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "provisions a project, allocates a vault, and scaffolds a real git remote into cwd" },
|
|
275
|
+
{ path: ["repos", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "cross-references live projects against their live gitvault status, one call per project" },
|
|
276
|
+
{ path: ["repos", "delete"], positionals: [], projectScoped: true, legacyPositionalProject: true, minimalArgs: [], runStyle: "sub", skipBehavioral: "irreversibly deletes a project after reading its live vault generation count" },
|
|
265
277
|
{ path: ["errors"], positionals: [p("fingerprint_id", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
|
|
266
278
|
|
|
267
279
|
// ── jobs ─────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared git-remote-scaffold fold-in (repo-first-onramp D4, task 2.4).
|
|
3
|
+
*
|
|
4
|
+
* `run402 init`'s 5b block pioneered this pattern; this module is the same
|
|
5
|
+
* additive-only, non-fatal-in-every-branch scaffold factored out so
|
|
6
|
+
* `projects provision`, `run402 up`, and `run402 repos create` all report
|
|
7
|
+
* through the IDENTICAL `gitvault` / `gitvault_skipped` / `gitvault_error`
|
|
8
|
+
* summary keys `init` already uses, instead of three near-copies drifting
|
|
9
|
+
* apart. `init.mjs` itself is left untouched — its own scaffold block is
|
|
10
|
+
* shipped and tested; this module exists for the NEW callers task 2.4 adds.
|
|
11
|
+
*
|
|
12
|
+
* Never creates a repository on its own unless `createRepoIfMissing` is
|
|
13
|
+
* explicit (mirrors `init`'s `--git-remote` opt-in): a directory the caller
|
|
14
|
+
* did not ask to turn into a repository is left alone. `origin` is never
|
|
15
|
+
* touched or reclaimed (D1, via `gitvault.scaffoldRemote` itself). Every
|
|
16
|
+
* branch is non-fatal — a missing git, an unresolvable org, or an
|
|
17
|
+
* unreachable gateway all report and return, never throw.
|
|
18
|
+
*/
|
|
19
|
+
import { getSdk } from "./sdk.mjs";
|
|
20
|
+
import { resolveOwningOrgId } from "./org-context.mjs";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The interim allocation→policy advisory (repo-first-onramp D3, stated as an
|
|
24
|
+
* interim). TODAY the platform sets `gitvault_policy: required` at allocation
|
|
25
|
+
* — the decoupling (allocation leaving the policy unset) is design D3's
|
|
26
|
+
* gateway half and ships in a later release (task 3.1). Until it does, every
|
|
27
|
+
* client-side path that allocates a vault prints this line so the policy flip
|
|
28
|
+
* is never a silent side effect of a push or a create. Faithful over flattering:
|
|
29
|
+
* documenting the designed future as the present is the breach; this line is
|
|
30
|
+
* the truth. DELETE this export (and its four call sites) when the gateway
|
|
31
|
+
* decoupling ships.
|
|
32
|
+
*/
|
|
33
|
+
export const GITVAULT_ALLOCATION_POLICY_ADVISORY =
|
|
34
|
+
"note: allocation currently sets gitvault_policy to 'required' — from now on a deploy must present a vaulted capture (produced automatically by `run402 deploy apply` on any machine holding this keystore). Opt out with `run402 gitvault policy grandfathered --reason \"<why>\"`. A later release decouples allocation from the deploy gate (design D3).";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} options
|
|
38
|
+
* @param {string} [options.repoDir] Working tree to scaffold. Defaults to `process.cwd()`.
|
|
39
|
+
* @param {string} options.projectId The project the remote should point at.
|
|
40
|
+
* @param {string} [options.orgId] Explicit owning org. Resolved via `resolveOwningOrgId` when omitted.
|
|
41
|
+
* @param {boolean} [options.createRepoIfMissing] Opt into `git init`-ing `repoDir` when it is not a repository yet.
|
|
42
|
+
* @returns {Promise<{gitvault: object|null, gitvault_skipped?: string, gitvault_error?: {code: string, message: string}}>}
|
|
43
|
+
*/
|
|
44
|
+
export async function scaffoldGitvaultRemote({ repoDir = process.cwd(), projectId, orgId, createRepoIfMissing = false } = {}) {
|
|
45
|
+
const out = { gitvault: null };
|
|
46
|
+
try {
|
|
47
|
+
const { hardenedGit } = await import("#sdk/node");
|
|
48
|
+
let insideRepo = true;
|
|
49
|
+
try {
|
|
50
|
+
await hardenedGit(repoDir, ["rev-parse", "--git-dir"]);
|
|
51
|
+
} catch {
|
|
52
|
+
insideRepo = false;
|
|
53
|
+
}
|
|
54
|
+
if (!insideRepo && !createRepoIfMissing) {
|
|
55
|
+
out.gitvault_skipped = "not a git repository — re-run with --git-remote to create one and add the remote";
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
const resolvedOrgId = orgId ?? (await resolveOwningOrgId(projectId));
|
|
59
|
+
if (!resolvedOrgId) {
|
|
60
|
+
out.gitvault_skipped = `could not resolve the owning org for ${projectId} — the run402 remote was not added`;
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
const remote = await getSdk().gitvault.scaffoldRemote({ repo_dir: repoDir, org_id: resolvedOrgId, project_id: projectId });
|
|
64
|
+
// `allocated: false` is stated, not left to be inferred: this is local
|
|
65
|
+
// git only — no vault exists for the project yet (allocation happens
|
|
66
|
+
// lazily on first push, or explicitly via `run402 gitvault init`).
|
|
67
|
+
out.gitvault = { ...remote, allocated: false };
|
|
68
|
+
} catch (err) {
|
|
69
|
+
out.gitvault = null;
|
|
70
|
+
out.gitvault_error = { code: err?.body?.code ?? err?.code ?? "GITVAULT_SCAFFOLD_FAILED", message: err?.message ?? String(err) };
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
package/lib/gitvault.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
20
|
import { resolveProjectId } from "./config.mjs";
|
|
21
21
|
import { resolveOwningOrgId } from "./org-context.mjs";
|
|
22
|
+
import { GITVAULT_ALLOCATION_POLICY_ADVISORY } from "./gitvault-scaffold.mjs";
|
|
22
23
|
import { getSdk } from "./sdk.mjs";
|
|
23
24
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
24
25
|
import {
|
|
@@ -37,15 +38,15 @@ const COMMON_VALUE_FLAGS = ["--project", "--repo"];
|
|
|
37
38
|
export const HELP = `run402 gitvault — your source, encrypted before it leaves the machine
|
|
38
39
|
|
|
39
40
|
Usage:
|
|
40
|
-
run402 gitvault init
|
|
41
|
-
run402 gitvault status
|
|
42
|
-
run402 gitvault
|
|
43
|
-
run402 gitvault policy
|
|
44
|
-
|
|
45
|
-
run402 gitvault compact
|
|
46
|
-
run402 gitvault prune
|
|
47
|
-
|
|
48
|
-
run402 gitvault verify
|
|
41
|
+
run402 gitvault init [--project <id>] [--org <org_id>] [--git-remote] [--no-remote]
|
|
42
|
+
run402 gitvault status [--project <id>] [--repo <repo_id>] [--refs]
|
|
43
|
+
run402 gitvault snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint]
|
|
44
|
+
run402 gitvault policy <required|grandfathered> [--project <id>] [--repo <repo_id>]
|
|
45
|
+
[--reason <why>]
|
|
46
|
+
run402 gitvault compact [--project <id>] [--repo <repo_id>]
|
|
47
|
+
run402 gitvault prune [--project <id>] [--repo <repo_id>]
|
|
48
|
+
[--submit --intent-core <path> --verifier-receipt <path> [--wait]]
|
|
49
|
+
run402 gitvault verify [--project <id>] [--repo <repo_id>] [--budget <n>]
|
|
49
50
|
|
|
50
51
|
Subcommands:
|
|
51
52
|
init ALLOCATE the project's vault. This is the one step that mints key
|
|
@@ -65,11 +66,20 @@ Subcommands:
|
|
|
65
66
|
audited. \`grandfathered\` is the documented way out of a deploy
|
|
66
67
|
blocked by GITVAULT_CLIENT_UPGRADE_REQUIRED, and leaves a
|
|
67
68
|
doctor-persistent warning until the project returns to \`required\`.
|
|
68
|
-
|
|
69
|
-
deploy — a vault-only project
|
|
70
|
-
|
|
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\`. The one-shot recovery receipt
|
|
74
|
+
and keystore path print to stderr the moment that happens.
|
|
75
|
+
Before reporting a snapshot as landed the SDK compares finalization
|
|
71
76
|
receipts against the expected manifest and reads the admitted head
|
|
72
|
-
back from storage; a 200 alone is never enough.
|
|
77
|
+
back from storage; a 200 alone is never enough. \`push\` is a
|
|
78
|
+
deprecation-warning alias for one release — it will be removed
|
|
79
|
+
next release. Once \`gitvault\` was the only publish verb; \`git
|
|
80
|
+
push\` is now the actual publish path (via the remote helper),
|
|
81
|
+
so \`push\` here was renamed to name what it does: one verb per
|
|
82
|
+
operation.
|
|
73
83
|
compact Publish a checkpoint covering the canonical refs, every root
|
|
74
84
|
unexpired at the cutoff, and the HEAD target, under a maintenance
|
|
75
85
|
lease so a concurrent cycle cannot race it.
|
|
@@ -97,9 +107,9 @@ Options:
|
|
|
97
107
|
chain and advances the local materialized pin), which is
|
|
98
108
|
why plain \`status\` — an observation — does not do it.
|
|
99
109
|
--repo <repo_id> Address the vault directly by id, skipping project lookup
|
|
100
|
-
--message <text>
|
|
110
|
+
--message <text> snapshot: commit message for the synthetic commit a dirty tree
|
|
101
111
|
produces (a clean tree pushes HEAD itself, no message used)
|
|
102
|
-
--checkpoint
|
|
112
|
+
--checkpoint snapshot: force the checkpoint-bearing form regardless of delta size
|
|
103
113
|
--budget <n> verify: heads to verify in this call. The verified prefix is
|
|
104
114
|
persisted, so a budget-exceeded run resumes where it stopped
|
|
105
115
|
instead of restarting.
|
|
@@ -190,8 +200,12 @@ function printTerminalLoss(status) {
|
|
|
190
200
|
console.error("");
|
|
191
201
|
}
|
|
192
202
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Where the keystore lives — for verbs whose payload is not a `status`.
|
|
205
|
+
* Exported: `repos create` (repo-first-onramp task 2.6) prints the same
|
|
206
|
+
* line after allocating a vault, and must not restate this logic.
|
|
207
|
+
*/
|
|
208
|
+
export async function printKeystoreLocation() {
|
|
195
209
|
try {
|
|
196
210
|
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
197
211
|
console.error(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-keystore loss is terminal for vault history`);
|
|
@@ -268,8 +282,9 @@ async function init(args) {
|
|
|
268
282
|
? `vault ${result.repo_id} already existed — nothing was re-allocated and no new key material was minted`
|
|
269
283
|
: `allocated vault ${result.repo_id} (genesis ${result.genesis_sha256})`,
|
|
270
284
|
);
|
|
271
|
-
if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url}`);
|
|
285
|
+
if (result.remote) console.error(`remote '${result.remote.name}' -> ${result.remote.url} (${result.remote.reason})`);
|
|
272
286
|
if (remoteSkipped) console.error(`remote not added: ${remoteSkipped}`);
|
|
287
|
+
if (!result.deduplicated) console.error(GITVAULT_ALLOCATION_POLICY_ADVISORY);
|
|
273
288
|
// The recovery receipt is integrity data, not a secret, and it is worth
|
|
274
289
|
// exactly as much as the number of copies you keep. It is persisted into
|
|
275
290
|
// the keystore automatically; say where, because "keep many copies" is
|
|
@@ -376,20 +391,46 @@ async function status(args) {
|
|
|
376
391
|
}
|
|
377
392
|
}
|
|
378
393
|
|
|
379
|
-
|
|
394
|
+
/**
|
|
395
|
+
* D5 (repo-first-onramp task 2.5): one verb per operation. Renamed from
|
|
396
|
+
* `push` — `push` now means exactly one thing everywhere: `git push`. The
|
|
397
|
+
* capture lane keeps its old function name internally to minimize churn;
|
|
398
|
+
* only the dispatched SUBCOMMAND name changed (see `run()` below, where
|
|
399
|
+
* `gitvault push` survives one release as a deprecation-warning alias).
|
|
400
|
+
*/
|
|
401
|
+
async function snapshot(args) {
|
|
380
402
|
const a = normalizeArgv(args);
|
|
381
403
|
const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
|
|
382
404
|
assertKnownFlags(a, [...valueFlags, "--checkpoint", "--help", "-h"], valueFlags);
|
|
383
405
|
requirePositionalCount(a, valueFlags, {
|
|
384
|
-
min: 0, max: 0, command: "run402 gitvault
|
|
406
|
+
min: 0, max: 0, command: "run402 gitvault snapshot", missing: "",
|
|
385
407
|
});
|
|
386
408
|
const message = flagValue(a, "--message");
|
|
409
|
+
const target = vaultTarget(a);
|
|
410
|
+
// D2: lazily allocate the vault on first push when there is a project to
|
|
411
|
+
// resolve the owning org from — the same resolution `gitvault init` uses.
|
|
412
|
+
// `--repo`-only addressing has nothing to create FROM (no project_id), so
|
|
413
|
+
// it is skipped there, matching `open()`'s own precedence.
|
|
414
|
+
const orgId = target.project_id ? await resolveOwningOrgId(target.project_id) : null;
|
|
387
415
|
const opts = {
|
|
388
|
-
...
|
|
416
|
+
...target,
|
|
417
|
+
...(orgId ? { org_id: orgId } : {}),
|
|
389
418
|
// The gitvault_commit line is progress, not payload: print it the moment
|
|
390
419
|
// the snapshot exists, well before the publication round-trips finish, so
|
|
391
420
|
// a human watching a slow push sees what is being pushed.
|
|
392
421
|
onCommitLine: (line) => console.error(line),
|
|
422
|
+
// Fires synchronously, BEFORE the capture/publish that follows — printed
|
|
423
|
+
// here rather than deferred past `push()`'s return so the receipt is
|
|
424
|
+
// never lost if a later step in the SAME push fails after allocation
|
|
425
|
+
// already landed on the server.
|
|
426
|
+
onVaultCreated: async (created) => {
|
|
427
|
+
console.error("");
|
|
428
|
+
console.error(`vault allocated (genesis ${created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
|
|
429
|
+
console.error(JSON.stringify(created.recovery_receipt));
|
|
430
|
+
await printKeystoreLocation();
|
|
431
|
+
console.error(GITVAULT_ALLOCATION_POLICY_ADVISORY);
|
|
432
|
+
console.error("");
|
|
433
|
+
},
|
|
393
434
|
};
|
|
394
435
|
// The message rides on `snapshot`, which is what `captureSnapshot` reads —
|
|
395
436
|
// and, since 5.12b removed the dead top-level `push({ message })` field, is
|
|
@@ -558,8 +599,17 @@ export async function run(sub, args) {
|
|
|
558
599
|
await status(argv);
|
|
559
600
|
break;
|
|
560
601
|
}
|
|
602
|
+
case "snapshot": {
|
|
603
|
+
await snapshot(argv);
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
561
606
|
case "push": {
|
|
562
|
-
|
|
607
|
+
// D5: one verb per operation — "push" now means exactly one thing,
|
|
608
|
+
// `git push`. Retained as a deprecation-warning alias for ONE release
|
|
609
|
+
// (pre-launch, the benchmark gate prefers the rename now over an
|
|
610
|
+
// alias forever); it will be removed next release.
|
|
611
|
+
console.error("`run402 gitvault push` is deprecated and will be removed in the next release — use `run402 gitvault snapshot` instead.");
|
|
612
|
+
await snapshot(argv);
|
|
563
613
|
break;
|
|
564
614
|
}
|
|
565
615
|
case "compact": {
|
package/lib/init.mjs
CHANGED
|
@@ -557,7 +557,10 @@ export async function run(args = []) {
|
|
|
557
557
|
// Purely LOCAL git. No vault is allocated and no key material is written
|
|
558
558
|
// here — the spec is explicit that neither exists until first capture, so the
|
|
559
559
|
// cold-start path gains no prompt and no new failure mode. Allocation happens
|
|
560
|
-
// on the first `run402 gitvault
|
|
560
|
+
// on the first `git push origin <branch>` or `run402 gitvault snapshot` (D2,
|
|
561
|
+
// repo-first-onramp) — NOT on deploy: `applyWithGitvault` only ever reads an
|
|
562
|
+
// EXISTING vault's policy (D3) and never allocates one that does not exist,
|
|
563
|
+
// so a project with no vault deploys exactly as it always did.
|
|
561
564
|
//
|
|
562
565
|
// Adding the remote is the DEFAULT inside a repository that already exists,
|
|
563
566
|
// because it is pure addition: `origin` is never modified or claimed, no file
|
|
@@ -608,11 +611,13 @@ export async function run(args = []) {
|
|
|
608
611
|
if (remote.already_present && remote.existing_url !== remote.url) {
|
|
609
612
|
// Left exactly as it was. Name the URL that is actually in place
|
|
610
613
|
// rather than implying the remote now points at this project.
|
|
611
|
-
line("Gitvault", `remote '${remote.name}' already points at ${remote.existing_url} — left unchanged`);
|
|
614
|
+
line("Gitvault", `remote '${remote.name}' already points at ${remote.existing_url} — left unchanged (${remote.reason})`);
|
|
612
615
|
} else if (remote.already_present) {
|
|
613
616
|
line("Gitvault", `remote '${remote.name}' already set (${remote.url})`);
|
|
614
617
|
} else {
|
|
615
|
-
|
|
618
|
+
// D1: `origin` when it was free, `run402` when it was already
|
|
619
|
+
// taken by something else — `remote.reason` says which happened.
|
|
620
|
+
line("Gitvault", `${remote.created_repository ? "initialized a repository and added" : "added"} remote '${remote.name}' -> ${remote.url} (${remote.reason})`);
|
|
616
621
|
}
|
|
617
622
|
}
|
|
618
623
|
}
|
package/lib/projects.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { withAutoApprove } from "./operator.mjs";
|
|
|
6
6
|
import { getSdk } from "./sdk.mjs";
|
|
7
7
|
import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
|
|
8
8
|
import { stampOrgFromProject } from "./org-context.mjs";
|
|
9
|
+
import { scaffoldGitvaultRemote } from "./gitvault-scaffold.mjs";
|
|
9
10
|
import { assertKnownFlags, failBadProjectId, flagValue, hasHelp, normalizeArgv, positionalArgs, resolveProjectSelector, validateRegularFile, failUnknownSubcommand } from "./argparse.mjs";
|
|
10
11
|
|
|
11
12
|
const HELP = `run402 projects — Manage your deployed Run402 projects
|
|
@@ -299,6 +300,20 @@ async function quote() {
|
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
/**
|
|
304
|
+
* D4 (repo-first-onramp task 2.4): `projects provision` already knows the
|
|
305
|
+
* project and (usually) the org, and just set the active project —
|
|
306
|
+
* scaffolding the git remote here is a pure Anticipatory fold, zero extra
|
|
307
|
+
* round-trips beyond the one org lookup provision needed anyway when `--org`
|
|
308
|
+
* was not given explicitly. `provision` has no `--git-remote` opt-in, so a
|
|
309
|
+
* directory that is not already a repository is left untouched, exactly like
|
|
310
|
+
* `run402 init`'s own scaffold (shared via `gitvault-scaffold.mjs`, which
|
|
311
|
+
* emits the SAME `gitvault` / `gitvault_skipped` / `gitvault_error` keys).
|
|
312
|
+
*/
|
|
313
|
+
async function foldGitvaultScaffold(out, projectId, explicitOrgId) {
|
|
314
|
+
Object.assign(out, await scaffoldGitvaultRemote({ repoDir: process.cwd(), projectId, orgId: explicitOrgId }));
|
|
315
|
+
}
|
|
316
|
+
|
|
302
317
|
async function provision(args) {
|
|
303
318
|
const opts = { tier: "prototype", name: undefined, orgId: undefined, idempotencyKey: undefined };
|
|
304
319
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -373,6 +388,7 @@ async function provision(args) {
|
|
|
373
388
|
out.note = `active project changed: ${activeBefore} -> ${activeAfter}`;
|
|
374
389
|
out.previous_active_project_id = activeBefore;
|
|
375
390
|
}
|
|
391
|
+
await foldGitvaultScaffold(out, data.project_id, opts.orgId);
|
|
376
392
|
console.log(JSON.stringify(out, null, 2));
|
|
377
393
|
} catch (err) {
|
|
378
394
|
reportSdkError(err);
|