run402 4.37.1 → 4.38.1
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 +3 -1
- package/core-dist/keystore.js +56 -4
- package/git-remote-run402.mjs +136 -14
- package/lib/command-manifest.mjs +7 -0
- package/lib/doctor.mjs +19 -6
- package/lib/gitvault-target.mjs +145 -0
- package/lib/gitvault.mjs +81 -18
- package/lib/next-actions.mjs +25 -0
- package/lib/org.mjs +70 -0
- package/lib/repos.mjs +130 -5
- package/lib/up.mjs +19 -2
- package/lib/wallet-context.mjs +88 -12
- package/package.json +1 -1
- package/sdk/core-dist/keystore.js +56 -4
- package/sdk/dist/errors.d.ts +6 -2
- 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/gitvault.crypto.d.ts +11 -2
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.js +11 -2
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +109 -3
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +161 -12
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/namespaces/org.d.ts +18 -1
- package/sdk/dist/namespaces/org.d.ts.map +1 -1
- package/sdk/dist/namespaces/org.js +38 -0
- package/sdk/dist/namespaces/org.js.map +1 -1
- package/sdk/dist/namespaces/org.types.d.ts +16 -0
- package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.d.ts +11 -1
- package/sdk/dist/namespaces/projects.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.js +16 -0
- package/sdk/dist/namespaces/projects.js.map +1 -1
- package/sdk/dist/namespaces/projects.types.d.ts +7 -0
- package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-address.d.ts +89 -0
- package/sdk/dist/node/gitvault-address.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-address.js +113 -0
- package/sdk/dist/node/gitvault-address.js.map +1 -0
- package/sdk/dist/node/gitvault-creation-journal.d.ts +81 -9
- package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-creation-journal.js +93 -17
- package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +13 -0
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +1 -0
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
- package/sdk/dist/node/gitvault-push-to-create.d.ts +75 -0
- package/sdk/dist/node/gitvault-push-to-create.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-push-to-create.js +132 -0
- package/sdk/dist/node/gitvault-push-to-create.js.map +1 -0
- package/sdk/dist/node/index.d.ts +6 -2
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +8 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/scoped.d.ts +2 -1
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +3 -0
- package/sdk/dist/scoped.js.map +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,9 @@ run402 repos list --org org_1a2b3c
|
|
|
65
65
|
run402 repos delete prj_xyz --force # refuses without --force while the vault holds generations
|
|
66
66
|
```
|
|
67
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.
|
|
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|name` 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
|
+
|
|
70
|
+
**Named addressing (design D6).** `run402 org slug <slug>` (owner-only, small one-time fee) claims an org's globally-unique, address-form slug, after which `run402::<slug>/<name>` addresses any repo under it — `git push` to a name that doesn't exist yet push-to-creates it. `run402 repos name <name> [--project <id>]` claims the per-org-unique `<name>` half explicitly (no fee); `repos create` claims one automatically, best-effort, when the org already has a slug. Also CLI/SDK-only — no MCP tool.
|
|
69
71
|
|
|
70
72
|
### Allowance
|
|
71
73
|
|
package/core-dist/keystore.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { getLegacyProjectsPath, getProjectCredentialsPath } from "./config.js";
|
|
5
5
|
import { clearActiveProjectId as clearProfileActiveProjectId, getActiveProjectId as getProfileActiveProjectId, recordMigration, setActiveProjectId as setProfileActiveProjectId, } from "./profile-state.js";
|
|
6
|
+
import { readAllowance } from "./allowance.js";
|
|
6
7
|
function withFileLock(path, fn, { retries = 200, delayMs = 20 } = {}) {
|
|
7
8
|
const lockDir = path + ".lock";
|
|
8
9
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -95,7 +96,11 @@ function migrateLegacyProjectsJson(targetPath) {
|
|
|
95
96
|
};
|
|
96
97
|
saveKeyStore(cache, targetPath);
|
|
97
98
|
if (legacy.active_project_id) {
|
|
98
|
-
|
|
99
|
+
// Scoped the same way every other write in this module now is (see
|
|
100
|
+
// `currentPrincipal`'s doc comment) — an unscoped write here is exactly
|
|
101
|
+
// what poisons the "unknown"-principal bucket for good on a machine that
|
|
102
|
+
// migrates before its wallet allowance exists yet.
|
|
103
|
+
setProfileActiveProjectId(legacy.active_project_id, undefined, { principal: currentPrincipal() });
|
|
99
104
|
}
|
|
100
105
|
recordMigration("projects_json_import", {
|
|
101
106
|
legacy_path: legacyPath,
|
|
@@ -165,12 +170,59 @@ export function removeProject(projectId, path) {
|
|
|
165
170
|
saveKeyStore(store, p);
|
|
166
171
|
});
|
|
167
172
|
if (!path)
|
|
168
|
-
clearProfileActiveProjectId(projectId);
|
|
173
|
+
clearProfileActiveProjectId(projectId, undefined, { principal: currentPrincipal() });
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The active-project scope's `principal`, derived from the CURRENT wallet's
|
|
177
|
+
* allowance — matching exactly what `NodeCredentialsProvider.setActiveProject`
|
|
178
|
+
* (sdk/src/node/credentials.ts) writes after `projects.provision` /
|
|
179
|
+
* `projects.use`. Without this, every reader here used the profile-state
|
|
180
|
+
* module's own default (empty) scope, which resolves to a FIXED
|
|
181
|
+
* "unknown"-principal bucket — the same bucket for every wallet in this
|
|
182
|
+
* profile.
|
|
183
|
+
*
|
|
184
|
+
* That mismatch is silent for a brand-new profile (the flat fallback in
|
|
185
|
+
* `profile-state.ts#getActiveProjectId` happens to agree), but once the
|
|
186
|
+
* "unknown" bucket is EVER populated by any principal-less write — the
|
|
187
|
+
* one-time legacy `projects.json` migration below, or any provision/`use`
|
|
188
|
+
* call made before this machine had a wallet allowance — it never gets
|
|
189
|
+
* updated again (real wallet operations write to the principal-keyed
|
|
190
|
+
* bucket, not "unknown") and PERMANENTLY shadows every later wallet-scoped
|
|
191
|
+
* activation for every caller that reads through this module: `resolveProjectId`
|
|
192
|
+
* / `resolveProject` (cli/lib/config.mjs), `projects current`, and every
|
|
193
|
+
* gitvault-verb target resolution. That is the root cause behind
|
|
194
|
+
* kychee-com/run402#559(a) — `repos create` DID call `projects.provision`,
|
|
195
|
+
* whose `creds.setActiveProject` correctly persisted the new project under
|
|
196
|
+
* the real wallet's scoped bucket AND the flat fallback, but every CLI read
|
|
197
|
+
* of "the active project" kept resolving the OLD "unknown"-bucket entry
|
|
198
|
+
* first and never fell through to the freshly-updated flat value.
|
|
199
|
+
*
|
|
200
|
+
* Best-effort: an unreadable/malformed allowance degrades to `null` (the
|
|
201
|
+
* same "unknown" bucket callers already tolerated before this fix), never a
|
|
202
|
+
* throw — this is a read-scoping concern, not an allowance-validity one.
|
|
203
|
+
*/
|
|
204
|
+
function currentPrincipal() {
|
|
205
|
+
try {
|
|
206
|
+
return readAllowance()?.address ?? null;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
169
211
|
}
|
|
170
212
|
export function getActiveProjectId(path) {
|
|
171
|
-
return getProfileActiveProjectId(path);
|
|
213
|
+
return getProfileActiveProjectId(path, { principal: currentPrincipal() });
|
|
172
214
|
}
|
|
173
215
|
export function setActiveProjectId(projectId, path) {
|
|
174
|
-
setProfileActiveProjectId(projectId, path);
|
|
216
|
+
setProfileActiveProjectId(projectId, path, { principal: currentPrincipal() });
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Scoped the same way as the getter/setter above — a caller reaching for
|
|
220
|
+
* `profile-state.ts`'s `clearActiveProjectId` directly (unscoped) clears only
|
|
221
|
+
* the "unknown"-principal bucket, leaving a real wallet's own scoped entry
|
|
222
|
+
* (set through `setActiveProjectId` above, or `NodeCredentialsProvider`)
|
|
223
|
+
* untouched and still resolvable.
|
|
224
|
+
*/
|
|
225
|
+
export function clearActiveProjectId(projectId, path) {
|
|
226
|
+
clearProfileActiveProjectId(projectId, path, { principal: currentPrincipal() });
|
|
175
227
|
}
|
|
176
228
|
//# sourceMappingURL=keystore.js.map
|
package/git-remote-run402.mjs
CHANGED
|
@@ -28,6 +28,21 @@
|
|
|
28
28
|
* completes the push. One command, no prior `gitvault init`. `git ls-remote`
|
|
29
29
|
* / `fetch` stay pure reads and allocate nothing.
|
|
30
30
|
*
|
|
31
|
+
* NAMED ADDRESSING + PUSH-TO-CREATE (repo-first-onramp task 4, design D6).
|
|
32
|
+
* `run402::<org>/<name>` admits TWO forms in the same slot — id-form
|
|
33
|
+
* (`org_id`/`prj_...`, unchanged: resolved via `r.gitvault.openOrCreate`
|
|
34
|
+
* above) and slug-form (`run402::<org-slug>/<name>`, e.g.
|
|
35
|
+
* `run402::acme/my-notes`) — discriminated by
|
|
36
|
+
* `gitvaultRemoteAddressForm`. A slug-form remote resolves through
|
|
37
|
+
* `r.gitvault.resolveOrCreateAddress`, which ALSO drives push-to-create on a
|
|
38
|
+
* miss (`push` only; `list`/`fetch` pass `allow_create: false`, same "reads
|
|
39
|
+
* never allocate" discipline as the id-form path) and PINS the resolved
|
|
40
|
+
* `repo_id` in this checkout's local git config the first time it resolves
|
|
41
|
+
* (task 4.5) — every later invocation on THIS checkout goes straight to the
|
|
42
|
+
* pinned id, skipping the address resolution round-trip entirely and
|
|
43
|
+
* surviving a later rename of either half. `SLUG_RELEASED` is never
|
|
44
|
+
* auto-followed: it refuses, naming the successor slug.
|
|
45
|
+
*
|
|
31
46
|
* WHICH REPOSITORY (the fail-closed rule). `process.cwd()` is NOT the
|
|
32
47
|
* repository. git identifies the repository with `GIT_DIR`, and during
|
|
33
48
|
* `git clone` cwd is the directory clone was RUN FROM — routinely some other,
|
|
@@ -68,7 +83,8 @@
|
|
|
68
83
|
|
|
69
84
|
import { createInterface } from "node:readline";
|
|
70
85
|
import { getSdk } from "./lib/sdk.mjs";
|
|
71
|
-
import {
|
|
86
|
+
import { resolveWalletCore, enforceWalletExistsCore, WalletSelectionError } from "./lib/wallet-context.mjs";
|
|
87
|
+
import { gitvaultRemoteAddressForm, gitvaultSlugReleasedInfo, parseGitvaultRemoteUrl } from "#sdk";
|
|
72
88
|
import { hardenedGit, resolveGitInvocationRepo } from "#sdk/node";
|
|
73
89
|
|
|
74
90
|
const out = (line) => process.stdout.write(`${line}\n`);
|
|
@@ -76,15 +92,91 @@ const out = (line) => process.stdout.write(`${line}\n`);
|
|
|
76
92
|
const endBlock = () => process.stdout.write("\n");
|
|
77
93
|
const note = (line) => process.stderr.write(`git-remote-run402: ${line}\n`);
|
|
78
94
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Wallet selection (kychee-com/run402#558). Before this, this file called
|
|
97
|
+
* `getSdk()` directly and ran NO wallet selection at all — a `.run402.json`
|
|
98
|
+
* binding, and even the global `wallets use` default, silently never
|
|
99
|
+
* reached it; only the `RUN402_WALLET` env layer worked, so a bound
|
|
100
|
+
* checkout's very next `git push run402 main` after a correctly-bound
|
|
101
|
+
* `run402 repos create` used the WRONG wallet's (usually empty) allowance.
|
|
102
|
+
*
|
|
103
|
+
* Shares `resolveWalletCore`/`enforceWalletExistsCore` with the CLI
|
|
104
|
+
* (`cli/lib/wallet-context.mjs`) — ONE implementation, minus the CLI's
|
|
105
|
+
* `--wallet` flag layer (this binary parses no argv flags at all). Resolved
|
|
106
|
+
* and applied (`process.env.RUN402_WALLET`) once per invocation, right
|
|
107
|
+
* before the first credential-touching call — never for `capabilities` /
|
|
108
|
+
* `option`, which touch neither credentials nor the network.
|
|
109
|
+
*
|
|
110
|
+
* WHICH DIRECTORY the binding walk starts from is NOT uniform, for the same
|
|
111
|
+
* fail-closed reason this file's own header explains for repository
|
|
112
|
+
* resolution: `list` needs no repository (a repository-free `git ls-remote`
|
|
113
|
+
* outside any checkout must keep working), so it walks from `process.cwd()`.
|
|
114
|
+
* `fetch`/`push` DO have a resolved repository by the time wallet selection
|
|
115
|
+
* runs (`requireRepo()` already succeeded) — walking from ITS directory
|
|
116
|
+
* rather than cwd is what makes `git clone` (cwd = wherever clone was RUN
|
|
117
|
+
* FROM, not the target repo) pick up a binding committed in the target
|
|
118
|
+
* repository, not whatever checkout happened to be current.
|
|
119
|
+
*/
|
|
120
|
+
let resolvedWallet = null;
|
|
121
|
+
|
|
122
|
+
function applyWalletForDir(dir) {
|
|
123
|
+
const resolved = resolveWalletCore({ env: process.env, cwd: dir });
|
|
124
|
+
enforceWalletExistsCore(resolved);
|
|
125
|
+
process.env.RUN402_WALLET = resolved.name;
|
|
126
|
+
resolvedWallet = resolved;
|
|
127
|
+
return resolved;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The resolved wallet's selection source, in the same words the CLI's own `--wallet` provenance line uses. `null` for the bare, unselected default. */
|
|
131
|
+
function walletSourceLabel(resolved) {
|
|
132
|
+
if (!resolved) return null;
|
|
133
|
+
if (resolved.source === "env") return "RUN402_WALLET";
|
|
134
|
+
if (resolved.source === "binding") return resolved.sourceDetail; // the .run402.json path
|
|
135
|
+
if (resolved.source === "config") return "wallets use";
|
|
136
|
+
return null; // "default" — nothing selected anything
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The allowance-missing/malformed family (`core/src/allowance.ts`'s own
|
|
141
|
+
* throws) all end with "Back up the file and run 'run402 init' to recreate
|
|
142
|
+
* it." — a remedy that assumes the resolved wallet is the one you meant.
|
|
143
|
+
* That is only true when NOTHING selected a wallet (the bare default); when
|
|
144
|
+
* an env var or a binding DID name one, the remedy is actively harmful —
|
|
145
|
+
* `run402 init` recreates the DEFAULT wallet's allowance, a DIFFERENT
|
|
146
|
+
* wallet than the one that was actually resolved and whose allowance is
|
|
147
|
+
* actually missing/broken (kychee-com/run402#558's second defect). Replace
|
|
148
|
+
* it with the resolved wallet's name and how selection works, so the fix is
|
|
149
|
+
* "correct the selection" rather than "recreate the wrong wallet".
|
|
150
|
+
*/
|
|
151
|
+
function enrichAllowanceError(message) {
|
|
152
|
+
if (!resolvedWallet || !/allowance\.json/.test(message)) return message;
|
|
153
|
+
const source = walletSourceLabel(resolvedWallet);
|
|
154
|
+
const stripped = message.replace(/\s*Back up the file and run 'run402 init' to recreate it\.?/, "").trim();
|
|
155
|
+
if (!source) {
|
|
156
|
+
// Genuinely the bare default wallet with no override anywhere — the
|
|
157
|
+
// original remedy already names the right target.
|
|
158
|
+
return `${stripped} Back up the file and run 'run402 init' to recreate it.`;
|
|
159
|
+
}
|
|
160
|
+
return (
|
|
161
|
+
`${stripped} Resolved wallet '${resolvedWallet.name}' via ${source} ` +
|
|
162
|
+
"(order: RUN402_WALLET env > .run402.json binding > 'wallets use' default > default). " +
|
|
163
|
+
`Wrong wallet? Fix selection instead. Right wallet, just no allowance yet? 'run402 wallets new ${resolvedWallet.name}'.`
|
|
164
|
+
);
|
|
82
165
|
}
|
|
83
166
|
|
|
84
167
|
function describeError(err) {
|
|
85
168
|
const code = err?.code ?? err?.body?.code ?? null;
|
|
86
|
-
const message = err?.message ?? err?.body?.message ?? String(err);
|
|
87
|
-
|
|
169
|
+
const message = enrichAllowanceError(err?.message ?? err?.body?.message ?? String(err));
|
|
170
|
+
// SLUG_RELEASED is never auto-followed — but the successor slug (design D6)
|
|
171
|
+
// is exactly the fact a human/agent reading stderr needs to act on it.
|
|
172
|
+
const released = gitvaultSlugReleasedInfo(err);
|
|
173
|
+
const suffix = released?.successor_slug ? ` (renamed to "${released.successor_slug}" — update the remote and re-run)` : "";
|
|
174
|
+
return oneLine(code ? `${code}: ${message}${suffix}` : message);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Protocol lines are single-line: collapse anything that could break framing. */
|
|
178
|
+
function oneLine(value) {
|
|
179
|
+
return String(value ?? "").replace(/\s+/g, " ").trim().slice(0, 400);
|
|
88
180
|
}
|
|
89
181
|
|
|
90
182
|
/**
|
|
@@ -132,7 +224,10 @@ async function main(argv) {
|
|
|
132
224
|
// `org_id` rides in the parsed address (`run402::<org_id>/<project_id>`),
|
|
133
225
|
// so it costs nothing extra to carry — it is exactly what D2's lazy
|
|
134
226
|
// creation needs to allocate an unresolved vault from `runPush` below, with
|
|
135
|
-
// no separate lookup.
|
|
227
|
+
// no separate lookup. Only meaningful for an ID-FORM address; a slug-form
|
|
228
|
+
// one resolves through `resolveOrCreateAddress` instead (below), which
|
|
229
|
+
// needs no separate org_id at all — the gateway resolves the slug itself.
|
|
230
|
+
const addressForm = gitvaultRemoteAddressForm(address);
|
|
136
231
|
const target = { project_id: address.project_id, org_id: address.org_id };
|
|
137
232
|
let verbosity = 1;
|
|
138
233
|
|
|
@@ -160,25 +255,42 @@ async function main(argv) {
|
|
|
160
255
|
note(`if you meant to restore this vault: git init --bare <dir> && git -C <dir> remote add run402 run402::${address.org_id}/${address.project_id} && git -C <dir> fetch run402 '+refs/heads/*:refs/heads/*'`);
|
|
161
256
|
}
|
|
162
257
|
|
|
163
|
-
/** Open the vault lazily — `capabilities` and `option` must never touch the network. */
|
|
164
|
-
const openVault = async (repoDir) => (await getSdk().gitvault.open(repoDir ? { ...target, repo_dir: repoDir } : target)).vault;
|
|
165
|
-
|
|
166
258
|
/** A 404/absent-vault refusal — the "nothing here yet" shape, never a genuine failure to mask. */
|
|
167
259
|
function isVaultNotFound(err) {
|
|
168
260
|
return err?.status === 404 || err?.code === "RESOURCE_NOT_FOUND" || err?.code === "ROUTE_NOT_FOUND";
|
|
169
261
|
}
|
|
170
262
|
|
|
171
263
|
/**
|
|
172
|
-
* Open the vault
|
|
173
|
-
*
|
|
174
|
-
*
|
|
264
|
+
* Open the vault lazily — `capabilities` and `option` must never touch the
|
|
265
|
+
* network. Dispatches on the address form (design D6): id-form is
|
|
266
|
+
* BYTE-IDENTICAL to before (`gitvault.open` with `{org_id, project_id}`);
|
|
267
|
+
* slug-form resolves (and, on the first successful resolution, PINS
|
|
268
|
+
* `repo_id` in local git state — task 4.5) through
|
|
269
|
+
* `gitvault.resolveOrCreateAddress` with `allow_create: false` — a read
|
|
270
|
+
* never allocates, same discipline the id-form path already had.
|
|
271
|
+
*/
|
|
272
|
+
const openVault = async (repoDir) => {
|
|
273
|
+
if (addressForm === "id") return (await getSdk().gitvault.open(repoDir ? { ...target, repo_dir: repoDir } : target)).vault;
|
|
274
|
+
const result = await getSdk().gitvault.resolveOrCreateAddress({ address, allow_create: false, ...(repoDir ? { repo_dir: repoDir } : {}) });
|
|
275
|
+
return result.handle.vault;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Open the vault, allocating it first when it does not exist yet (D2), and
|
|
280
|
+
* — for a SLUG-form address whose name does not resolve yet —
|
|
281
|
+
* PUSH-TO-CREATE it (design D6, task 4.4/4.5). Used ONLY by `runPush` —
|
|
282
|
+
* `list`/`fetch` stay pure reads and never create anything (see
|
|
283
|
+
* `runList`'s own not-found handling below).
|
|
175
284
|
*
|
|
176
285
|
* Prints the one-shot recovery receipt and the keystore path to stderr the
|
|
177
286
|
* moment allocation happens, per the client-surface spec: an agent reads
|
|
178
287
|
* stderr, and the receipt is worth exactly as many copies as get kept.
|
|
179
288
|
*/
|
|
180
289
|
async function openOrCreateVault(repoDir) {
|
|
181
|
-
const result =
|
|
290
|
+
const result =
|
|
291
|
+
addressForm === "id"
|
|
292
|
+
? await getSdk().gitvault.openOrCreate({ ...target, repo_dir: repoDir })
|
|
293
|
+
: await getSdk().gitvault.resolveOrCreateAddress({ address, repo_dir: repoDir, allow_create: true });
|
|
182
294
|
if (!result.found && result.created) {
|
|
183
295
|
note("");
|
|
184
296
|
note(`vault ${result.handle.repo_id} allocated (genesis ${result.created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
|
|
@@ -195,6 +307,10 @@ async function main(argv) {
|
|
|
195
307
|
}
|
|
196
308
|
|
|
197
309
|
async function runList() {
|
|
310
|
+
// `list` needs no repository (a repository-free `git ls-remote` outside
|
|
311
|
+
// any checkout must keep working) — the binding walk falls back to cwd,
|
|
312
|
+
// same as `capabilities`/`option`'s repository-free tier.
|
|
313
|
+
applyWalletForDir(process.cwd());
|
|
198
314
|
let state;
|
|
199
315
|
try {
|
|
200
316
|
state = await (await openVault()).materialize();
|
|
@@ -235,6 +351,10 @@ async function main(argv) {
|
|
|
235
351
|
repoRefusalNote(err);
|
|
236
352
|
return 1;
|
|
237
353
|
}
|
|
354
|
+
// The repository is resolved — walk the binding from ITS directory, not
|
|
355
|
+
// cwd (the "WHICH REPOSITORY" note above: during `git clone` cwd is
|
|
356
|
+
// wherever clone was run FROM, unrelated to the target repository).
|
|
357
|
+
applyWalletForDir(repoDir);
|
|
238
358
|
if (verbosity >= 1) note(`restoring the vault object database for ${batch.length} ref(s) into ${repoDir}`);
|
|
239
359
|
const restored = await getSdk().gitvault.restore({ ...target, repo_dir: repoDir, target_dir: repoDir });
|
|
240
360
|
if (verbosity >= 1) note(`restored generation ${restored.generation}`);
|
|
@@ -249,6 +369,8 @@ async function main(argv) {
|
|
|
249
369
|
// network: a push that names a ref this repository does not have must
|
|
250
370
|
// fail locally rather than after opening the vault.
|
|
251
371
|
const repoDir = await requireRepo();
|
|
372
|
+
// Same "walk from the resolved repository, not cwd" rule as `fetch`.
|
|
373
|
+
applyWalletForDir(repoDir);
|
|
252
374
|
const newOids = new Map();
|
|
253
375
|
for (const spec of specs) {
|
|
254
376
|
// A deletion carries an empty <src>. Everything else is resolved by
|
package/lib/command-manifest.mjs
CHANGED
|
@@ -197,6 +197,11 @@ export const COMMAND_MANIFEST = [
|
|
|
197
197
|
{ path: ["org", "get"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
|
|
198
198
|
{ path: ["org", "rename"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--name", "Gate"] },
|
|
199
199
|
{ path: ["org", "payout-wallet"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--wallet", "0x1111111111111111111111111111111111111111"] },
|
|
200
|
+
// repo-first-onramp task 4.1 (design D6). `--org` (not a positional org_id
|
|
201
|
+
// like rename/payout-wallet above) goes through resolveOrg's SHAPE
|
|
202
|
+
// validation (a real UUID) — "org_gate1" fails that locally, so this needs
|
|
203
|
+
// the same UUID-shaped fixture `org use` below already established.
|
|
204
|
+
{ path: ["org", "slug"], positionals: [p("slug")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["acme", "--org", "11111111-2222-3333-4444-555555555555"] },
|
|
200
205
|
{ path: ["org", "whoami"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
|
|
201
206
|
{ path: ["org", "audit"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
|
|
202
207
|
{ path: ["org", "use"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["11111111-2222-3333-4444-555555555555"] },
|
|
@@ -274,6 +279,8 @@ export const COMMAND_MANIFEST = [
|
|
|
274
279
|
{ 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
280
|
{ 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
281
|
{ path: ["repos", "delete"], positionals: [], projectScoped: true, legacyPositionalProject: true, minimalArgs: [], runStyle: "sub", skipBehavioral: "irreversibly deletes a project after reading its live vault generation count" },
|
|
282
|
+
// repo-first-onramp task 4.2 (design D6): explicit address-form name claim.
|
|
283
|
+
{ path: ["repos", "name"], positionals: [p("name")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["my-notes"], runStyle: "sub", skipBehavioral: "claims a per-org-unique repo name against a live project" },
|
|
277
284
|
{ path: ["errors"], positionals: [p("fingerprint_id", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "merged" },
|
|
278
285
|
|
|
279
286
|
// ── jobs ─────────────────────────────────────────────────────────────────
|
package/lib/doctor.mjs
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, statSync } from "node:fs";
|
|
15
|
-
import { configDir, readAllowance, loadKeyStore
|
|
15
|
+
import { configDir, readAllowance, loadKeyStore } from "./config.mjs";
|
|
16
|
+
import { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
16
17
|
import { getSdk } from "./sdk.mjs";
|
|
17
18
|
import {
|
|
18
19
|
resolveScanRoot,
|
|
@@ -408,9 +409,18 @@ export async function run(sub, args = []) {
|
|
|
408
409
|
// or `ok`, never a doctor failure. A vault-only project that has never
|
|
409
410
|
// deployed is a first-class shape (protocol D183), so its mere absence of a
|
|
410
411
|
// deploy raises nothing.
|
|
412
|
+
//
|
|
413
|
+
// TARGETING (repo-first-onramp follow-up, kychee-com/run402#559d): when
|
|
414
|
+
// cwd is a repository with its own pinned repo id or run402/origin remote,
|
|
415
|
+
// doctor now checks THAT vault, not the profile's active project — the
|
|
416
|
+
// same pin > remote > RUN402_PROJECT_ID env > active-project order every
|
|
417
|
+
// other gitvault verb follows (`gitvault-target.mjs`). Doctor has no
|
|
418
|
+
// `--project`/`--repo` flag of its own, so there is no explicit tier here.
|
|
411
419
|
{
|
|
412
|
-
const
|
|
413
|
-
|
|
420
|
+
const target = await resolveGitvaultTarget({ repoDir: process.cwd() });
|
|
421
|
+
const projectId = target.project_id ?? null;
|
|
422
|
+
const repoId = target.repo_id ?? null;
|
|
423
|
+
if (!projectId && !repoId) {
|
|
414
424
|
checks.push({
|
|
415
425
|
name: "gitvault",
|
|
416
426
|
status: "skipped",
|
|
@@ -418,9 +428,12 @@ export async function run(sub, args = []) {
|
|
|
418
428
|
});
|
|
419
429
|
} else {
|
|
420
430
|
try {
|
|
421
|
-
const gv = await getSdk().gitvault.status({
|
|
431
|
+
const gv = await getSdk().gitvault.status({
|
|
432
|
+
...(repoId ? { repo_id: repoId } : { project_id: projectId }),
|
|
433
|
+
repo_dir: process.cwd(),
|
|
434
|
+
});
|
|
422
435
|
const value = {
|
|
423
|
-
project_id: projectId,
|
|
436
|
+
project_id: gv.project_id ?? projectId,
|
|
424
437
|
repo_id: gv.repo_id,
|
|
425
438
|
vault: gv.vault === null ? null : "allocated",
|
|
426
439
|
gitvault_policy: gv.gitvault_policy,
|
|
@@ -446,7 +459,7 @@ export async function run(sub, args = []) {
|
|
|
446
459
|
gaps.push(`${gv.pending_overrides} unvaulted-override journal(s) are still open — run 'run402 gitvault push' to drain them`);
|
|
447
460
|
}
|
|
448
461
|
if (gv.remote && !gv.remote.matches) {
|
|
449
|
-
gaps.push(`the '${gv.remote.name}' git remote points at a different project than ${
|
|
462
|
+
gaps.push(`the '${gv.remote.name}' git remote points at a different project than ${value.project_id} (${gv.remote.url})`);
|
|
450
463
|
}
|
|
451
464
|
// Echoed exactly as the SDK reported them — including the
|
|
452
465
|
// doctor-persistent `grandfathered` advisory it owns.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "which vault does this repo mean" resolution for `run402 gitvault`
|
|
3
|
+
* and `run402 doctor`'s gitvault check (repo-first-onramp follow-up,
|
|
4
|
+
* kychee-com/run402#559's design gap): gitvault verbs were purely
|
|
5
|
+
* active-project-scoped even when run standing inside a repository that
|
|
6
|
+
* already names its own vault via a pinned repo id or a run402/origin
|
|
7
|
+
* remote — git muscle memory says a command run inside a repo acts on THAT
|
|
8
|
+
* repo, and a stale active-project pointer silently targeted a DIFFERENT
|
|
9
|
+
* one instead (GITVAULT_ACCESS_DENIED, or worse, a silently WRONG vault).
|
|
10
|
+
*
|
|
11
|
+
* Targeting order for a verb run standing inside a git repository, highest
|
|
12
|
+
* first:
|
|
13
|
+
* 1. an explicit --repo/--project flag (owned by each call site — this
|
|
14
|
+
* module supplies only the fallback chain beneath it, plus the
|
|
15
|
+
* mismatch warning against tier 3)
|
|
16
|
+
* 2. the 4.38.0 pin (`r402.repoId` in local git config) — addresses the
|
|
17
|
+
* vault by repo_id directly, no network read at all
|
|
18
|
+
* 3. the repo's run402/origin remote address — id-form is parsed
|
|
19
|
+
* directly out of the address string (free, no network); slug-form is
|
|
20
|
+
* resolved via the SDK (one read-only network call — `resolveAddress`,
|
|
21
|
+
* never a pin, never a push-to-create; this is a TARGETING read, not a
|
|
22
|
+
* publish)
|
|
23
|
+
* 4. RUN402_PROJECT_ID env
|
|
24
|
+
* 5. the profile's active project
|
|
25
|
+
*
|
|
26
|
+
* Outside a repository (or when repo detection itself fails), only tiers 4
|
|
27
|
+
* and 5 apply — unchanged from before this module existed.
|
|
28
|
+
*
|
|
29
|
+
* ARCHITECTURAL NOTE: this is CLI-edge policy (which flag/env/file wins),
|
|
30
|
+
* not gitvault protocol behavior — the same class of concern
|
|
31
|
+
* `wallet-context.mjs` owns for wallet selection. The protocol reads it
|
|
32
|
+
* composes (`readPinnedGitvaultRepo`, `resolveAddress`) already live once in
|
|
33
|
+
* the SDK; this module adds no protocol logic of its own.
|
|
34
|
+
*/
|
|
35
|
+
import { getActiveProjectId } from "./config.mjs";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Tiers 4/5 (RUN402_PROJECT_ID env, then the active project) — deliberately
|
|
39
|
+
* NON-throwing, unlike `config.mjs#resolveProjectId`. `run402 doctor`'s
|
|
40
|
+
* gitvault check needs to report "skipped" gracefully when nothing resolves
|
|
41
|
+
* anywhere (its pre-existing, tested behavior); a `fail()`-triggered
|
|
42
|
+
* `process.exit()` here would abort doctor's ENTIRE report, not just this
|
|
43
|
+
* one check. Callers that DO want the historical PROJECT_REQUIRED failure
|
|
44
|
+
* (gitvault.mjs's own verbs) get it by falling back to `resolveProjectId`
|
|
45
|
+
* themselves when this returns `null` — see `vaultTarget` in gitvault.mjs.
|
|
46
|
+
*/
|
|
47
|
+
function envOrActiveProjectId() {
|
|
48
|
+
return (process.env.RUN402_PROJECT_ID || "").trim() || getActiveProjectId() || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function isInsideGitRepo(repoDir) {
|
|
52
|
+
try {
|
|
53
|
+
const { hardenedGit } = await import("#sdk/node");
|
|
54
|
+
await hardenedGit(repoDir, ["rev-parse", "--git-dir"]);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The repo's OWN target, independent of any flag — the pin first, then the
|
|
63
|
+
* `run402`/`origin` remote (in that order, mirroring `scaffoldRemote`'s own
|
|
64
|
+
* naming precedence). `null` when the repo has neither, when this is not a
|
|
65
|
+
* repository at all, or (slug-form only) the remote fails to resolve over
|
|
66
|
+
* the network — a miss here is always ordinary, never thrown.
|
|
67
|
+
*/
|
|
68
|
+
export async function repoOwnGitvaultTarget(repoDir) {
|
|
69
|
+
if (!(await isInsideGitRepo(repoDir))) return null;
|
|
70
|
+
|
|
71
|
+
const { hardenedGit, readPinnedGitvaultRepo } = await import("#sdk/node");
|
|
72
|
+
const pinned = await readPinnedGitvaultRepo(repoDir);
|
|
73
|
+
if (pinned) return { repo_id: pinned.repo_id, project_id: null, source: "pin" };
|
|
74
|
+
|
|
75
|
+
const { parseGitvaultRemoteUrl, gitvaultRemoteAddressForm } = await import("#sdk");
|
|
76
|
+
for (const name of ["run402", "origin"]) {
|
|
77
|
+
let url;
|
|
78
|
+
try {
|
|
79
|
+
url = (await hardenedGit(repoDir, ["remote", "get-url", name])).text().trim();
|
|
80
|
+
} catch {
|
|
81
|
+
continue; // no such remote — try the other conventional name
|
|
82
|
+
}
|
|
83
|
+
if (!url) continue;
|
|
84
|
+
const address = parseGitvaultRemoteUrl(url);
|
|
85
|
+
if (!address) continue; // exists, but isn't a run402 address — try the other name
|
|
86
|
+
if (gitvaultRemoteAddressForm(address) === "id") {
|
|
87
|
+
// id-form already carries both halves in the address string — no
|
|
88
|
+
// network read needed at all.
|
|
89
|
+
return { repo_id: null, project_id: address.project_id, source: "remote", remote_name: name };
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const { getSdk } = await import("./sdk.mjs");
|
|
93
|
+
const resolved = await getSdk().gitvault.resolveAddress(address);
|
|
94
|
+
return { repo_id: resolved.repo_id, project_id: resolved.project_id, source: "remote", remote_name: name };
|
|
95
|
+
} catch {
|
|
96
|
+
// Offline, SLUG_RELEASED, not-found, ... — nothing to target from this
|
|
97
|
+
// rung; a caller falls through to env/active project.
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve `{ repo_id?, project_id? }` for a gitvault verb that addresses a
|
|
106
|
+
* vault the same way `--repo`/`--project` already do — pin beats remote
|
|
107
|
+
* beats env beats active project, an explicit flag beats all of them.
|
|
108
|
+
* `explicitProjectId`/`explicitRepoId` are the already-parsed flag values
|
|
109
|
+
* (`undefined`/`null` when absent — this module owns no flag parsing), and
|
|
110
|
+
* either, neither, or both may be set (mirroring `--repo`/`--project`
|
|
111
|
+
* together being valid on the CLI today). `warn` receives the one-line
|
|
112
|
+
* mismatch note when an explicit flag disagrees with the repo's OWN target;
|
|
113
|
+
* the flag still wins either way. The pin carries no project_id to compare
|
|
114
|
+
* for free, so only `--repo` is checked against it; only the remote tier
|
|
115
|
+
* carries a project_id for free (id-form) or resolves one (slug-form), so
|
|
116
|
+
* only `--project` is checked against it — matching the task's own wording
|
|
117
|
+
* ("a mismatch between an explicit flag and the repo's remote").
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveGitvaultTarget({
|
|
120
|
+
repoDir = process.cwd(),
|
|
121
|
+
explicitProjectId,
|
|
122
|
+
explicitRepoId,
|
|
123
|
+
warn = (line) => console.error(line),
|
|
124
|
+
} = {}) {
|
|
125
|
+
const needsOwn = explicitRepoId == null || explicitProjectId == null;
|
|
126
|
+
const own = needsOwn ? await repoOwnGitvaultTarget(repoDir) : null;
|
|
127
|
+
|
|
128
|
+
if (explicitRepoId != null && own?.source === "pin" && own.repo_id !== explicitRepoId) {
|
|
129
|
+
warn(`warning: --repo ${explicitRepoId} does not match this repo's pinned vault ${own.repo_id} — using --repo ${explicitRepoId}.`);
|
|
130
|
+
}
|
|
131
|
+
if (explicitProjectId != null && own?.source === "remote" && own.project_id && own.project_id !== explicitProjectId) {
|
|
132
|
+
warn(`warning: --project ${explicitProjectId} does not match this repo's '${own.remote_name}' remote project ${own.project_id} — using --project ${explicitProjectId}.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (explicitRepoId != null || explicitProjectId != null) {
|
|
136
|
+
const result = {};
|
|
137
|
+
if (explicitRepoId != null) result.repo_id = explicitRepoId;
|
|
138
|
+
if (explicitProjectId != null) result.project_id = explicitProjectId;
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (own?.source === "pin") return { repo_id: own.repo_id };
|
|
143
|
+
if (own?.source === "remote" && own.project_id) return { project_id: own.project_id };
|
|
144
|
+
return { project_id: envOrActiveProjectId() };
|
|
145
|
+
}
|