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/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 { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
22
23
|
import { getSdk } from "./sdk.mjs";
|
|
23
24
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
24
25
|
import {
|
|
@@ -69,8 +70,12 @@ Subcommands:
|
|
|
69
70
|
deploy — a vault-only project snapshots for months without one.
|
|
70
71
|
Against a project with no vault yet, this ALLOCATES one inline
|
|
71
72
|
(the six-stage creation, same as \`init\`) before publishing — one
|
|
72
|
-
command, no prior \`gitvault init\`.
|
|
73
|
-
|
|
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.
|
|
74
79
|
Before reporting a snapshot as landed the SDK compares finalization
|
|
75
80
|
receipts against the expected manifest and reads the admitted head
|
|
76
81
|
back from storage; a 200 alone is never enough. \`push\` is a
|
|
@@ -164,19 +169,36 @@ Examples:
|
|
|
164
169
|
* Resolve which vault to act on, plus the local git tree.
|
|
165
170
|
*
|
|
166
171
|
* `--repo` addresses the vault directly (the cold-restart path: an agent that
|
|
167
|
-
* knows its repo_id needs no project lookup). Otherwise the project
|
|
168
|
-
*
|
|
169
|
-
*
|
|
172
|
+
* knows its repo_id needs no project lookup). Otherwise the project targets,
|
|
173
|
+
* highest first: `--project` > the repo's own pin/remote > RUN402_PROJECT_ID
|
|
174
|
+
* > the active project (repo-first-onramp follow-up, kychee-com/run402#559 —
|
|
175
|
+
* see `gitvault-target.mjs`'s module doc for the full targeting order and
|
|
176
|
+
* why it exists: a stale active-project pointer used to silently outrank the
|
|
177
|
+
* repository this command is actually standing in).
|
|
170
178
|
*/
|
|
171
|
-
function vaultTarget(a) {
|
|
179
|
+
async function vaultTarget(a) {
|
|
172
180
|
const repoId = flagValue(a, "--repo");
|
|
173
181
|
const project = flagValue(a, "--project");
|
|
174
|
-
const
|
|
182
|
+
const repoDir = process.cwd();
|
|
183
|
+
const resolved = await resolveGitvaultTarget({
|
|
184
|
+
repoDir,
|
|
185
|
+
explicitProjectId: project ?? undefined,
|
|
186
|
+
explicitRepoId: repoId ?? undefined,
|
|
187
|
+
});
|
|
188
|
+
const target = { repo_dir: repoDir };
|
|
175
189
|
if (repoId != null) target.repo_id = repoId;
|
|
176
190
|
// Only demand a project when one is actually needed: `--repo` alone is a
|
|
177
|
-
// complete address, and requiring
|
|
178
|
-
//
|
|
179
|
-
if (repoId == null || project != null)
|
|
191
|
+
// complete address, and requiring one on top of it would make the
|
|
192
|
+
// cold-restart path fail for no reason.
|
|
193
|
+
if (repoId == null || project != null) {
|
|
194
|
+
if ("repo_id" in resolved && project == null) target.repo_id = resolved.repo_id;
|
|
195
|
+
// `resolveGitvaultTarget` reports its last (env/active) tier
|
|
196
|
+
// non-throwingly (`run402 doctor`'s call site needs that) — this call
|
|
197
|
+
// site is the one that historically failed closed with PROJECT_REQUIRED
|
|
198
|
+
// when nothing resolves anywhere, and still does: `resolveProjectId`
|
|
199
|
+
// re-derives the exact same env/active check and throws.
|
|
200
|
+
if ("project_id" in resolved) target.project_id = resolved.project_id ?? resolveProjectId(project);
|
|
201
|
+
}
|
|
180
202
|
return target;
|
|
181
203
|
}
|
|
182
204
|
|
|
@@ -333,7 +355,7 @@ async function policy(args) {
|
|
|
333
355
|
});
|
|
334
356
|
}
|
|
335
357
|
|
|
336
|
-
const target = vaultTarget(a);
|
|
358
|
+
const target = await vaultTarget(a);
|
|
337
359
|
try {
|
|
338
360
|
const sdk = getSdk();
|
|
339
361
|
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
@@ -359,7 +381,7 @@ async function status(args) {
|
|
|
359
381
|
requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
360
382
|
min: 0, max: 0, command: "run402 gitvault status", missing: "",
|
|
361
383
|
});
|
|
362
|
-
const target = vaultTarget(a);
|
|
384
|
+
const target = await vaultTarget(a);
|
|
363
385
|
if (a.includes("--refs")) target.refs = true;
|
|
364
386
|
try {
|
|
365
387
|
const s = await getSdk().gitvault.status(target);
|
|
@@ -370,6 +392,15 @@ async function status(args) {
|
|
|
370
392
|
if (s.remote) {
|
|
371
393
|
console.error(`remote '${s.remote.name}': ${s.remote.url}${s.remote.matches ? "" : " ← points at a DIFFERENT project than this status"}`);
|
|
372
394
|
}
|
|
395
|
+
// The id-pinning state (design D6, task 4.5): a slug-form remote pins
|
|
396
|
+
// repo_id in local git state the first time it resolves; id-form pins
|
|
397
|
+
// nothing (it needs no pin — see resolveGitvaultAddress's doc comment).
|
|
398
|
+
if (s.pinned) {
|
|
399
|
+
console.error(
|
|
400
|
+
`pinned: repo_id ${s.pinned.repo_id}` +
|
|
401
|
+
(s.pinned.resolved_from ? ` (resolved from run402::${s.pinned.resolved_from.org_slug}/${s.pinned.resolved_from.repo_name})` : ""),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
373
404
|
if (s.refs) {
|
|
374
405
|
const names = Object.keys(s.refs).sort();
|
|
375
406
|
console.error(names.length === 0 ? "refs: (none yet)" : `refs (${names.length}):`);
|
|
@@ -396,6 +427,33 @@ async function status(args) {
|
|
|
396
427
|
* only the dispatched SUBCOMMAND name changed (see `run()` below, where
|
|
397
428
|
* `gitvault push` survives one release as a deprecation-warning alias).
|
|
398
429
|
*/
|
|
430
|
+
/**
|
|
431
|
+
* D6 (repo-first-onramp task 4): when neither `--repo` nor `--project` was
|
|
432
|
+
* given explicitly, look at the local `run402`/`origin` remote (in that
|
|
433
|
+
* order, mirroring `scaffoldRemote`'s own naming) and, if it is a SLUG-form
|
|
434
|
+
* address (`run402::<org-slug>/<name>`), return the parsed address so
|
|
435
|
+
* `snapshot` can push-to-create through it — the same address-form
|
|
436
|
+
* resolution `git push` drives via the remote helper. `null` for an
|
|
437
|
+
* id-form remote, no remote at all, or an explicit `--repo`/`--project`.
|
|
438
|
+
*/
|
|
439
|
+
async function detectSlugFormRemote(a, repoDir) {
|
|
440
|
+
if (flagValue(a, "--repo") != null || flagValue(a, "--project") != null) return null;
|
|
441
|
+
const { hardenedGit } = await import("#sdk/node");
|
|
442
|
+
const { parseGitvaultRemoteUrl, gitvaultRemoteAddressForm } = await import("#sdk");
|
|
443
|
+
for (const name of ["run402", "origin"]) {
|
|
444
|
+
let url;
|
|
445
|
+
try {
|
|
446
|
+
url = (await hardenedGit(repoDir, ["remote", "get-url", name])).text().trim();
|
|
447
|
+
} catch {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (!url) continue;
|
|
451
|
+
const address = parseGitvaultRemoteUrl(url);
|
|
452
|
+
if (address && gitvaultRemoteAddressForm(address) === "slug") return address;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
399
457
|
async function snapshot(args) {
|
|
400
458
|
const a = normalizeArgv(args);
|
|
401
459
|
const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
|
|
@@ -404,14 +462,19 @@ async function snapshot(args) {
|
|
|
404
462
|
min: 0, max: 0, command: "run402 gitvault snapshot", missing: "",
|
|
405
463
|
});
|
|
406
464
|
const message = flagValue(a, "--message");
|
|
407
|
-
const
|
|
465
|
+
const repoDir = process.cwd();
|
|
466
|
+
const address = await detectSlugFormRemote(a, repoDir);
|
|
408
467
|
// D2: lazily allocate the vault on first push when there is a project to
|
|
409
468
|
// resolve the owning org from — the same resolution `gitvault init` uses.
|
|
410
469
|
// `--repo`-only addressing has nothing to create FROM (no project_id), so
|
|
411
|
-
// it is skipped there, matching `open()`'s own precedence.
|
|
412
|
-
|
|
470
|
+
// it is skipped there, matching `open()`'s own precedence. Skipped
|
|
471
|
+
// entirely for a slug-form remote (`address` above) — that resolves
|
|
472
|
+
// through the address, not a project_id, and needs no separate org_id.
|
|
473
|
+
const target = address ? { repo_dir: repoDir } : await vaultTarget(a);
|
|
474
|
+
const orgId = !address && target.project_id ? await resolveOwningOrgId(target.project_id) : null;
|
|
413
475
|
const opts = {
|
|
414
476
|
...target,
|
|
477
|
+
...(address ? { address } : {}),
|
|
415
478
|
...(orgId ? { org_id: orgId } : {}),
|
|
416
479
|
// The gitvault_commit line is progress, not payload: print it the moment
|
|
417
480
|
// the snapshot exists, well before the publication round-trips finish, so
|
|
@@ -450,7 +513,7 @@ async function compact(args) {
|
|
|
450
513
|
min: 0, max: 0, command: "run402 gitvault compact", missing: "",
|
|
451
514
|
});
|
|
452
515
|
try {
|
|
453
|
-
const result = await getSdk().gitvault.compact(vaultTarget(a));
|
|
516
|
+
const result = await getSdk().gitvault.compact(await vaultTarget(a));
|
|
454
517
|
console.log(JSON.stringify(result, null, 2));
|
|
455
518
|
console.error(
|
|
456
519
|
`checkpoint published at generation ${result.generation}: ` +
|
|
@@ -519,7 +582,7 @@ async function prune(args) {
|
|
|
519
582
|
hint: "Add --submit, or drop the flags to plan.",
|
|
520
583
|
});
|
|
521
584
|
}
|
|
522
|
-
const opts = vaultTarget(a);
|
|
585
|
+
const opts = await vaultTarget(a);
|
|
523
586
|
if (submitting) {
|
|
524
587
|
opts.submit = {
|
|
525
588
|
core: readJsonFile("--intent-core", corePath),
|
|
@@ -565,7 +628,7 @@ async function verify(args) {
|
|
|
565
628
|
requirePositionalCount(a, valueFlags, {
|
|
566
629
|
min: 0, max: 0, command: "run402 gitvault verify", missing: "",
|
|
567
630
|
});
|
|
568
|
-
const target = vaultTarget(a);
|
|
631
|
+
const target = await vaultTarget(a);
|
|
569
632
|
const budget = flagValue(a, "--budget");
|
|
570
633
|
if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
|
|
571
634
|
try {
|
package/lib/next-actions.mjs
CHANGED
|
@@ -66,3 +66,28 @@ export function deployAction() {
|
|
|
66
66
|
why: "Apply your release manifest to deploy.",
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `repos create` (and `gitvault init`) on an org with no claimed slug: the
|
|
72
|
+
* response's `address: null` had no pointer to WHY, or to the named-addressing
|
|
73
|
+
* feature at all (kychee-com/run402#560). One-time $1 fee, owner-only.
|
|
74
|
+
*/
|
|
75
|
+
export function claimOrgSlugAction() {
|
|
76
|
+
return nextAction("claim_org_slug", {
|
|
77
|
+
command: "run402 org slug <slug>",
|
|
78
|
+
why: "This organization has no claimed slug yet, so its repos have no run402::<slug>/<name> address. One-time $1, owner-only.",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The org already has a slug, but this project's address-form repo name was
|
|
84
|
+
* not claimed this time (a collision, or the best-effort claim failed for
|
|
85
|
+
* some other reason) — point at the explicit claim verb instead of leaving
|
|
86
|
+
* `address: null` unexplained.
|
|
87
|
+
*/
|
|
88
|
+
export function claimRepoNameAction(projectId) {
|
|
89
|
+
return nextAction("claim_repo_name", {
|
|
90
|
+
command: `run402 repos name <name> --project ${projectId}`,
|
|
91
|
+
why: "The owning organization has a slug, but this project has no claimed address-form name yet.",
|
|
92
|
+
});
|
|
93
|
+
}
|
package/lib/org.mjs
CHANGED
|
@@ -30,6 +30,7 @@ Usage:
|
|
|
30
30
|
run402 org get <org_id>
|
|
31
31
|
run402 org rename <org_id> --name <display_name> (or: --clear to remove the label)
|
|
32
32
|
run402 org payout-wallet <org_id> --wallet <wallet_address> (or: --clear to remove the explicit default)
|
|
33
|
+
run402 org slug <slug> [--org <org_id>]
|
|
33
34
|
run402 org whoami
|
|
34
35
|
run402 org use <org_id>
|
|
35
36
|
run402 org current
|
|
@@ -53,6 +54,9 @@ Subcommands:
|
|
|
53
54
|
list Orgs you are a member of
|
|
54
55
|
get Read one org (label + tier/lease + your role)
|
|
55
56
|
rename Set or clear an org's display label (owner-only)
|
|
57
|
+
slug Claim or rename the org's globally-unique, address-form slug
|
|
58
|
+
(owner-only). A genesis claim spends a one-time claim fee; a
|
|
59
|
+
rename releases the old slug into a ~90-day cooldown.
|
|
56
60
|
use Select the current org for this wallet profile
|
|
57
61
|
current Report the resolved current org and where it came from
|
|
58
62
|
clear Clear this wallet profile's org selection
|
|
@@ -120,6 +124,26 @@ Legacy (still supported):
|
|
|
120
124
|
|
|
121
125
|
Owner-only + step-up gated. Pass --clear (or an empty display_name) to remove
|
|
122
126
|
the label. Output includes the updated tier and lease timestamps.
|
|
127
|
+
`,
|
|
128
|
+
slug: `run402 org slug — claim or rename the org's address-form slug
|
|
129
|
+
|
|
130
|
+
Usage:
|
|
131
|
+
run402 org slug <slug> [--org <org_id>] [--idempotency-key <key>]
|
|
132
|
+
|
|
133
|
+
The slug is a globally-unique, claimable, address-form handle for the org
|
|
134
|
+
(repo-first-onramp design D6) — the <org-slug> half of a named repo address
|
|
135
|
+
run402::<org-slug>/<name>. Grammar: lowercase [a-z0-9-], no leading/trailing/
|
|
136
|
+
double hyphen, max 39 chars. Owner-only.
|
|
137
|
+
|
|
138
|
+
A genesis claim (the org had no prior slug) spends a one-time claim fee off
|
|
139
|
+
the org's balance. A rename is free but releases the OLD slug into a ~90-day
|
|
140
|
+
cooldown: it stops resolving, with a typed SLUG_RELEASED refusal naming the
|
|
141
|
+
new slug as successor — there is no redirect, so update every remote and
|
|
142
|
+
address that still names the old one.
|
|
143
|
+
|
|
144
|
+
This is a paid, side-effecting mutation and requires Idempotency-Key; the SDK
|
|
145
|
+
generates one automatically unless --idempotency-key is passed, so a retried
|
|
146
|
+
call after a dropped response can never double-bill.
|
|
123
147
|
`,
|
|
124
148
|
"payout-wallet": `run402 org payout-wallet — set or clear the tenant route payout wallet
|
|
125
149
|
|
|
@@ -413,6 +437,51 @@ async function payoutWallet(args) {
|
|
|
413
437
|
}
|
|
414
438
|
}
|
|
415
439
|
|
|
440
|
+
/**
|
|
441
|
+
* `run402 org slug <slug>` — claim or rename the org's address-form slug
|
|
442
|
+
* (repo-first-onramp design D6). Owner-only, and a genesis claim spends a
|
|
443
|
+
* one-time claim fee — this is a PAID, side-effecting mutation, so it
|
|
444
|
+
* requires `Idempotency-Key`; the SDK generates one client-side when
|
|
445
|
+
* `--idempotency-key` is omitted, so a retried call after a dropped response
|
|
446
|
+
* can never double-bill.
|
|
447
|
+
*/
|
|
448
|
+
async function slug(args) {
|
|
449
|
+
const a = normalizeArgv(args);
|
|
450
|
+
const valueFlags = ["--org", "--idempotency-key"];
|
|
451
|
+
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
452
|
+
const [newSlug] = requirePositionalCount(a, valueFlags, {
|
|
453
|
+
min: 1,
|
|
454
|
+
max: 1,
|
|
455
|
+
command: "run402 org slug <slug> [--org <org_id>]",
|
|
456
|
+
missing: "Missing <slug>.",
|
|
457
|
+
});
|
|
458
|
+
const org = await resolveOrg(a, { cmd: "org" });
|
|
459
|
+
if (!org) {
|
|
460
|
+
fail({
|
|
461
|
+
code: "ORG_UNRESOLVED",
|
|
462
|
+
message: "Could not resolve which organization to claim this slug for.",
|
|
463
|
+
hint: "Pass --org <org_id>, or select one first with `run402 org use <id>`.",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const idempotencyKey = flagValue(a, "--idempotency-key");
|
|
467
|
+
try {
|
|
468
|
+
const result = await getSdk().org(org.orgId).claimSlug(newSlug, idempotencyKey != null ? { idempotencyKey } : {});
|
|
469
|
+
console.log(JSON.stringify(result, null, 2));
|
|
470
|
+
if (result.created) {
|
|
471
|
+
console.error(`slug "${result.slug}" claimed for ${org.orgId} — a one-time claim fee was debited from the org's balance.`);
|
|
472
|
+
} else if (result.previous_slug && result.previous_slug !== result.slug) {
|
|
473
|
+
console.error(
|
|
474
|
+
`org ${org.orgId} renamed from "${result.previous_slug}" to "${result.slug}" — no fee. ` +
|
|
475
|
+
`"${result.previous_slug}" now enters its ~90-day release cooldown: it stops resolving with a typed SLUG_RELEASED refusal (naming "${result.slug}" as the successor), never a redirect. Update every remote and address that still names it.`,
|
|
476
|
+
);
|
|
477
|
+
} else {
|
|
478
|
+
console.error(`"${result.slug}" was already ${org.orgId}'s current slug — nothing changed, no fee.`);
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
reportSdkError(err);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
416
485
|
async function audit(args) {
|
|
417
486
|
const a = normalizeArgv(args);
|
|
418
487
|
const valueFlags = ["--limit", "--after", "--before"];
|
|
@@ -630,6 +699,7 @@ export async function run(sub, args) {
|
|
|
630
699
|
case "get": await get(args); break;
|
|
631
700
|
case "rename": await rename(args); break;
|
|
632
701
|
case "payout-wallet": await payoutWallet(args); break;
|
|
702
|
+
case "slug": await slug(args); break;
|
|
633
703
|
case "whoami": await whoami(args); break;
|
|
634
704
|
case "use": await use(args); break;
|
|
635
705
|
case "current": await current(args); break;
|
package/lib/repos.mjs
CHANGED
|
@@ -35,11 +35,12 @@
|
|
|
35
35
|
import { getSdk } from "./sdk.mjs";
|
|
36
36
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
37
37
|
import { withAutoApprove } from "./operator.mjs";
|
|
38
|
-
import { allowanceAuthHeaders, isCoreApiTarget } from "./config.mjs";
|
|
38
|
+
import { allowanceAuthHeaders, isCoreApiTarget, resolveProjectId } from "./config.mjs";
|
|
39
39
|
import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
|
|
40
40
|
import { resolveOrgId, resolveOwningOrgId } from "./org-context.mjs";
|
|
41
|
-
import { nextAction } from "./next-actions.mjs";
|
|
41
|
+
import { nextAction, claimOrgSlugAction, claimRepoNameAction } from "./next-actions.mjs";
|
|
42
42
|
import { printKeystoreLocation } from "./gitvault.mjs";
|
|
43
|
+
import { gitvaultRemoteUrlForRepo } from "#sdk";
|
|
43
44
|
import {
|
|
44
45
|
normalizeArgv,
|
|
45
46
|
hasHelp,
|
|
@@ -56,21 +57,30 @@ Usage:
|
|
|
56
57
|
run402 repos create <name> [--org <org_id>] [--dir <path>] [--tier <tier>]
|
|
57
58
|
run402 repos list [--org <org_id>]
|
|
58
59
|
run402 repos delete <project_id> [--force]
|
|
60
|
+
run402 repos name <name> [--project <id>]
|
|
59
61
|
|
|
60
62
|
Subcommands:
|
|
61
63
|
create Provision a project, ALLOCATE its vault (mints key material and a
|
|
62
64
|
one-shot recovery receipt), and scaffold the run402 remote —
|
|
63
65
|
origin when free, run402 when taken (D1). No deploy plan, no
|
|
64
66
|
release, nothing deployed: the vault-only track (design D8), for a
|
|
65
|
-
project that only ever hosts encrypted source.
|
|
67
|
+
project that only ever hosts encrypted source. When the owning org
|
|
68
|
+
has a slug (run402 org slug), also claims the project's address-
|
|
69
|
+
form repo name (best-effort — a name collision or missing slug
|
|
70
|
+
never fails the command) and prints the run402::<slug>/<name>
|
|
71
|
+
address (design D6).
|
|
66
72
|
list The organization's vault-bearing projects — those with an
|
|
67
73
|
allocated vault, whether or not they have ever deployed. Not
|
|
68
|
-
every project in the org; ones with no vault are omitted.
|
|
74
|
+
every project in the org; ones with no vault are omitted. Shows
|
|
75
|
+
the run402::<slug>/<name> address for a repo that has claimed one.
|
|
69
76
|
delete Delete the project and everything in it (database, functions,
|
|
70
77
|
subdomains, mailbox, secrets). REFUSES while the vault holds any
|
|
71
78
|
admitted generation unless --force is passed — this is
|
|
72
79
|
irreversible and destroys the vault's entire encrypted history
|
|
73
80
|
along with everything else.
|
|
81
|
+
name Claim or rename the project's per-org-unique, address-form name
|
|
82
|
+
(design D6) — the <name> half of run402::<org-slug>/<name>. No
|
|
83
|
+
fee, unlike the org slug. Same authority as renaming the project.
|
|
74
84
|
|
|
75
85
|
Options:
|
|
76
86
|
--org <org_id> create/list: the owning organization. create resolves it
|
|
@@ -88,6 +98,8 @@ Options:
|
|
|
88
98
|
derived from the name)
|
|
89
99
|
--force delete: proceed even though the vault holds generations
|
|
90
100
|
that would be permanently and irrecoverably lost
|
|
101
|
+
--project <id> name: project to claim the repo name for (default: the
|
|
102
|
+
active project)
|
|
91
103
|
--json No-op: stdout is already JSON.
|
|
92
104
|
|
|
93
105
|
There is no separate gitvault price: bytes count against the same
|
|
@@ -97,6 +109,7 @@ organization-pooled storage budget every project already has.
|
|
|
97
109
|
const CREATE_VALUE_FLAGS = ["--org", "--dir", "--tier", "--idempotency-key"];
|
|
98
110
|
const LIST_VALUE_FLAGS = ["--org"];
|
|
99
111
|
const DELETE_VALUE_FLAGS = ["--project"];
|
|
112
|
+
const NAME_VALUE_FLAGS = ["--project"];
|
|
100
113
|
|
|
101
114
|
function validateProjectName(name) {
|
|
102
115
|
if (name === "") {
|
|
@@ -123,6 +136,24 @@ function validateProjectName(name) {
|
|
|
123
136
|
}
|
|
124
137
|
}
|
|
125
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Best-effort slugify for the address-form repo name (design D6's grammar:
|
|
141
|
+
* lowercase [a-z0-9-], no leading/trailing/double hyphen, <=63 chars). The
|
|
142
|
+
* free-text project display name (`repos create <name>`'s positional) is
|
|
143
|
+
* NOT already in this charset, so `create` derives a candidate rather than
|
|
144
|
+
* sending the raw name straight to the claim route and failing on the first
|
|
145
|
+
* space or capital letter.
|
|
146
|
+
*/
|
|
147
|
+
function slugifyRepoName(name) {
|
|
148
|
+
return name
|
|
149
|
+
.toLowerCase()
|
|
150
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
151
|
+
.replace(/-+/g, "-")
|
|
152
|
+
.replace(/^-+|-+$/g, "")
|
|
153
|
+
.slice(0, 63)
|
|
154
|
+
.replace(/-+$/g, "");
|
|
155
|
+
}
|
|
156
|
+
|
|
126
157
|
async function create(args) {
|
|
127
158
|
const a = normalizeArgv(args);
|
|
128
159
|
assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--help", "-h"], CREATE_VALUE_FLAGS);
|
|
@@ -177,21 +208,63 @@ async function create(args) {
|
|
|
177
208
|
project_id: provisioned.project_id,
|
|
178
209
|
repo_dir: dir,
|
|
179
210
|
});
|
|
211
|
+
// Best-effort address-form name claim (design D6): when the owning org
|
|
212
|
+
// has a slug, name this repo so it is reachable as
|
|
213
|
+
// run402::<slug>/<name> too — never fails `create` itself. A collision,
|
|
214
|
+
// a missing slug, or any other refusal just means no address this time;
|
|
215
|
+
// `run402 repos name <name>` claims it explicitly later.
|
|
216
|
+
let address = null;
|
|
217
|
+
let orgSlug = null;
|
|
218
|
+
try {
|
|
219
|
+
const orgRecord = await getSdk().org(effectiveOrgId).get();
|
|
220
|
+
orgSlug = orgRecord.slug ?? null;
|
|
221
|
+
if (orgSlug) {
|
|
222
|
+
const candidate = slugifyRepoName(name);
|
|
223
|
+
if (candidate) {
|
|
224
|
+
const named = await getSdk().projects.setRepoName(provisioned.project_id, candidate);
|
|
225
|
+
address = gitvaultRemoteUrlForRepo(orgSlug, named.repo_name);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error(`repo name not claimed (non-fatal): ${err?.message ?? String(err)}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// `address: null` used to have no pointer to WHY, or to the
|
|
233
|
+
// named-addressing feature at all (kychee-com/run402#560): an agent
|
|
234
|
+
// reading the output had no path from "address is null" to
|
|
235
|
+
// `run402 org slug`/`run402 repos name`. One typed next_actions entry,
|
|
236
|
+
// pointing at whichever half is actually missing.
|
|
237
|
+
const nextActions = address
|
|
238
|
+
? []
|
|
239
|
+
: orgSlug
|
|
240
|
+
? [claimRepoNameAction(provisioned.project_id)]
|
|
241
|
+
: [claimOrgSlugAction()];
|
|
242
|
+
|
|
180
243
|
const out = {
|
|
181
244
|
project_id: provisioned.project_id,
|
|
182
245
|
repo_id: vault.repo_id,
|
|
246
|
+
address,
|
|
183
247
|
remote: vault.remote,
|
|
184
248
|
deduplicated: vault.deduplicated,
|
|
185
249
|
genesis_sha256: vault.genesis_sha256,
|
|
186
250
|
recovery_receipt: vault.recovery_receipt,
|
|
187
251
|
terminal_loss_statement: vault.terminal_loss_statement,
|
|
188
252
|
deployed: false,
|
|
253
|
+
next_actions: nextActions,
|
|
189
254
|
};
|
|
190
255
|
console.log(JSON.stringify(out, null, 2));
|
|
191
256
|
console.error(
|
|
192
257
|
`project ${provisioned.project_id} provisioned; vault ${vault.repo_id} ` +
|
|
193
258
|
(vault.deduplicated ? "already existed — nothing was re-allocated" : `allocated (genesis ${vault.genesis_sha256})`),
|
|
194
259
|
);
|
|
260
|
+
if (address) console.error(`address: ${address}`);
|
|
261
|
+
else if (!orgSlug) {
|
|
262
|
+
console.error(
|
|
263
|
+
"no named address yet — claim an org slug (run402 org slug <slug>, one-time $1) to get run402::<slug>/<name> addresses",
|
|
264
|
+
);
|
|
265
|
+
} else {
|
|
266
|
+
console.error(`no address claimed — run 'run402 repos name <name> --project ${provisioned.project_id}' to claim one`);
|
|
267
|
+
}
|
|
195
268
|
if (vault.remote) console.error(`remote '${vault.remote.name}' -> ${vault.remote.url} (${vault.remote.reason})`);
|
|
196
269
|
console.error("");
|
|
197
270
|
console.error(vault.terminal_loss_statement);
|
|
@@ -244,8 +317,21 @@ async function list(args) {
|
|
|
244
317
|
genesis_admitted_at: status.vault.genesis_admitted_at,
|
|
245
318
|
});
|
|
246
319
|
}
|
|
247
|
-
|
|
320
|
+
// The org's slug, when claimed (design D6) — printed so a human/agent can
|
|
321
|
+
// construct run402::<slug>/<name> addresses by hand. There is deliberately
|
|
322
|
+
// no per-project `address` field here yet: the gateway has no bulk (or
|
|
323
|
+
// even single) READ for a project's claimed repo_name today, only the
|
|
324
|
+
// WRITE route (`POST /projects/v1/:id/repo-name`) — adding one is gateway
|
|
325
|
+
// work, out of scope for this client-only change (see the final report).
|
|
326
|
+
let orgSlug = null;
|
|
327
|
+
try {
|
|
328
|
+
orgSlug = (await getSdk().org(orgId).get()).slug;
|
|
329
|
+
} catch {
|
|
330
|
+
// Best-effort — `list` must not fail over an org-slug lookup.
|
|
331
|
+
}
|
|
332
|
+
console.log(JSON.stringify({ org_id: orgId, org_slug: orgSlug, repos }, null, 2));
|
|
248
333
|
console.error(`${repos.length} vault-bearing project(s) of ${projects.length} total in this organization`);
|
|
334
|
+
if (orgSlug) console.error(`org slug: ${orgSlug} — a repo with a claimed address-form name is reachable at run402::${orgSlug}/<name>`);
|
|
249
335
|
}
|
|
250
336
|
|
|
251
337
|
async function del(args) {
|
|
@@ -297,6 +383,41 @@ async function del(args) {
|
|
|
297
383
|
}
|
|
298
384
|
}
|
|
299
385
|
|
|
386
|
+
/**
|
|
387
|
+
* `run402 repos name <name> [--project <id>]` — the explicit address-form
|
|
388
|
+
* claim (design D6, task 4.2): a project gets its per-org-unique `<name>`
|
|
389
|
+
* half of `run402::<org-slug>/<name>` either at push-to-create time or here.
|
|
390
|
+
*/
|
|
391
|
+
async function name(args) {
|
|
392
|
+
const a = normalizeArgv(args);
|
|
393
|
+
assertKnownFlags(a, [...NAME_VALUE_FLAGS, "--help", "-h"], NAME_VALUE_FLAGS);
|
|
394
|
+
const [repoName] = requirePositionalCount(a, NAME_VALUE_FLAGS, {
|
|
395
|
+
min: 1, max: 1, command: "run402 repos name <name> [--project <id>]", missing: "run402 repos name <name>: a name is required",
|
|
396
|
+
});
|
|
397
|
+
const projectId = resolveProjectId(flagValue(a, "--project"));
|
|
398
|
+
try {
|
|
399
|
+
const result = await getSdk().projects.setRepoName(projectId, repoName);
|
|
400
|
+
let address = null;
|
|
401
|
+
try {
|
|
402
|
+
const owningOrg = await resolveOwningOrgId(projectId);
|
|
403
|
+
const orgSlug = owningOrg ? (await getSdk().org(owningOrg).get()).slug : null;
|
|
404
|
+
if (orgSlug) address = gitvaultRemoteUrlForRepo(orgSlug, result.repo_name);
|
|
405
|
+
} catch {
|
|
406
|
+
// The claim itself already succeeded — a failed address-preview lookup is never fatal.
|
|
407
|
+
}
|
|
408
|
+
console.log(JSON.stringify({ ...result, address }, null, 2));
|
|
409
|
+
console.error(
|
|
410
|
+
result.previous_repo_name && result.previous_repo_name !== result.repo_name
|
|
411
|
+
? `renamed from "${result.previous_repo_name}" to "${result.repo_name}"`
|
|
412
|
+
: `name "${result.repo_name}" claimed for ${projectId}`,
|
|
413
|
+
);
|
|
414
|
+
if (address) console.error(`address: ${address}`);
|
|
415
|
+
else console.error("this org has no slug yet — claim one with `run402 org slug <slug>` to get a full run402::<slug>/<name> address");
|
|
416
|
+
} catch (err) {
|
|
417
|
+
reportSdkError(err);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
300
421
|
export async function run(sub, args) {
|
|
301
422
|
const argv = Array.isArray(args) ? args : [];
|
|
302
423
|
if (!sub || hasHelp([sub, ...argv])) {
|
|
@@ -316,6 +437,10 @@ export async function run(sub, args) {
|
|
|
316
437
|
await del(argv);
|
|
317
438
|
break;
|
|
318
439
|
}
|
|
440
|
+
case "name": {
|
|
441
|
+
await name(argv);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
319
444
|
default:
|
|
320
445
|
failUnknownSubcommand("repos", sub, {
|
|
321
446
|
hint: "Run `run402 repos --help` for usage.",
|
package/lib/up.mjs
CHANGED
|
@@ -585,14 +585,31 @@ function looksLikeGitRemoteUrl(source) {
|
|
|
585
585
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(source) || /^[^\s@]+@[^\s:]+:/.test(source);
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
-
/**
|
|
588
|
+
/**
|
|
589
|
+
* `git init` only when `dir` is not a repository yet. Returns whether it did.
|
|
590
|
+
*
|
|
591
|
+
* `-b main`, not whatever `init.defaultBranch` (or the pre-2.28 hardcoded
|
|
592
|
+
* `master`) happens to be — the docs teach `git push origin main`, and the
|
|
593
|
+
* gitvault remote helper's own dangling-HEAD hazard note (a first push of
|
|
594
|
+
* any OTHER branch leaves HEAD naming a ref that does not exist yet) is
|
|
595
|
+
* exactly what a mismatched default branch here would walk `up` straight
|
|
596
|
+
* into. `-b` needs git 2.28+ (2020); an older git falls back to the same
|
|
597
|
+
* result by a different route — `symbolic-ref` on a still-empty repository
|
|
598
|
+
* has no existing ref to disturb, so it is exactly as safe as `-b main`
|
|
599
|
+
* would have been. Mirrors `Gitvault.scaffoldRemote`'s identical fallback.
|
|
600
|
+
*/
|
|
589
601
|
async function gitInitIfNeeded(dir) {
|
|
590
602
|
const { hardenedGit } = await import("#sdk/node");
|
|
591
603
|
try {
|
|
592
604
|
await hardenedGit(dir, ["rev-parse", "--git-dir"]);
|
|
593
605
|
return false;
|
|
594
606
|
} catch {
|
|
595
|
-
|
|
607
|
+
try {
|
|
608
|
+
await hardenedGit(dir, ["init", "-q", "-b", "main", "."]);
|
|
609
|
+
} catch {
|
|
610
|
+
await hardenedGit(dir, ["init", "-q", "."]);
|
|
611
|
+
await hardenedGit(dir, ["symbolic-ref", "HEAD", "refs/heads/main"]);
|
|
612
|
+
}
|
|
596
613
|
return true;
|
|
597
614
|
}
|
|
598
615
|
}
|