run402 4.97.0 → 4.98.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.
Files changed (163) hide show
  1. package/README.md +1 -1
  2. package/core-dist/binding-file.js +28 -1
  3. package/core-dist/wallet.js +3 -3
  4. package/lib/auth.mjs +1 -2
  5. package/lib/buzz-doctor.mjs +21 -806
  6. package/lib/buzz-doctor.test.mjs +7 -6
  7. package/lib/buzz.test.mjs +13 -2
  8. package/lib/doctor.mjs +72 -811
  9. package/lib/init.mjs +35 -576
  10. package/lib/org-context.mjs +39 -376
  11. package/lib/orgs.mjs +22 -106
  12. package/lib/projects.mjs +1 -2
  13. package/lib/repos.mjs +6 -6
  14. package/lib/rooms.mjs +1 -2
  15. package/lib/sdk-errors.mjs +26 -0
  16. package/lib/status.mjs +14 -154
  17. package/lib/up.mjs +1 -2
  18. package/lib/vault-scaffold.mjs +2 -3
  19. package/lib/vault-target.mjs +14 -141
  20. package/lib/wallet-context.mjs +61 -227
  21. package/lib/wallets.mjs +47 -310
  22. package/package.json +1 -2
  23. package/sdk/core-dist/binding-file.js +28 -1
  24. package/sdk/core-dist/wallet.js +3 -3
  25. package/sdk/dist/errors.d.ts +24 -2
  26. package/sdk/dist/errors.d.ts.map +1 -1
  27. package/sdk/dist/errors.js +44 -2
  28. package/sdk/dist/errors.js.map +1 -1
  29. package/sdk/dist/index.d.ts +18 -3
  30. package/sdk/dist/index.d.ts.map +1 -1
  31. package/sdk/dist/index.js +12 -1
  32. package/sdk/dist/index.js.map +1 -1
  33. package/sdk/dist/kernel.d.ts +34 -0
  34. package/sdk/dist/kernel.d.ts.map +1 -1
  35. package/sdk/dist/kernel.js +15 -1
  36. package/sdk/dist/kernel.js.map +1 -1
  37. package/sdk/dist/namespaces/admin.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/admin.js +8 -0
  39. package/sdk/dist/namespaces/admin.js.map +1 -1
  40. package/sdk/dist/namespaces/agent.d.ts +2 -2
  41. package/sdk/dist/namespaces/agent.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/agent.js +4 -0
  43. package/sdk/dist/namespaces/agent.js.map +1 -1
  44. package/sdk/dist/namespaces/agent.types.d.ts +5 -0
  45. package/sdk/dist/namespaces/agent.types.d.ts.map +1 -1
  46. package/sdk/dist/namespaces/apps.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/apps.js +2 -0
  48. package/sdk/dist/namespaces/apps.js.map +1 -1
  49. package/sdk/dist/namespaces/auth.d.ts.map +1 -1
  50. package/sdk/dist/namespaces/auth.js +9 -0
  51. package/sdk/dist/namespaces/auth.js.map +1 -1
  52. package/sdk/dist/namespaces/branches.d.ts.map +1 -1
  53. package/sdk/dist/namespaces/branches.js +2 -0
  54. package/sdk/dist/namespaces/branches.js.map +1 -1
  55. package/sdk/dist/namespaces/ci.d.ts.map +1 -1
  56. package/sdk/dist/namespaces/ci.js +2 -0
  57. package/sdk/dist/namespaces/ci.js.map +1 -1
  58. package/sdk/dist/namespaces/credentials.d.ts +0 -23
  59. package/sdk/dist/namespaces/credentials.d.ts.map +1 -1
  60. package/sdk/dist/namespaces/credentials.js +29 -0
  61. package/sdk/dist/namespaces/credentials.js.map +1 -1
  62. package/sdk/dist/namespaces/escalations.d.ts.map +1 -1
  63. package/sdk/dist/namespaces/escalations.js +2 -0
  64. package/sdk/dist/namespaces/escalations.js.map +1 -1
  65. package/sdk/dist/namespaces/grants.d.ts.map +1 -1
  66. package/sdk/dist/namespaces/grants.js +4 -0
  67. package/sdk/dist/namespaces/grants.js.map +1 -1
  68. package/sdk/dist/namespaces/live.d.ts +0 -16
  69. package/sdk/dist/namespaces/live.d.ts.map +1 -1
  70. package/sdk/dist/namespaces/live.js +19 -0
  71. package/sdk/dist/namespaces/live.js.map +1 -1
  72. package/sdk/dist/namespaces/me.d.ts.map +1 -1
  73. package/sdk/dist/namespaces/me.js +3 -0
  74. package/sdk/dist/namespaces/me.js.map +1 -1
  75. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  76. package/sdk/dist/namespaces/org.js +3 -0
  77. package/sdk/dist/namespaces/org.js.map +1 -1
  78. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  79. package/sdk/dist/namespaces/projects.js +5 -0
  80. package/sdk/dist/namespaces/projects.js.map +1 -1
  81. package/sdk/dist/namespaces/repos.d.ts.map +1 -1
  82. package/sdk/dist/namespaces/repos.js +5 -0
  83. package/sdk/dist/namespaces/repos.js.map +1 -1
  84. package/sdk/dist/namespaces/rooms.d.ts.map +1 -1
  85. package/sdk/dist/namespaces/rooms.js +3 -0
  86. package/sdk/dist/namespaces/rooms.js.map +1 -1
  87. package/sdk/dist/namespaces/session.d.ts.map +1 -1
  88. package/sdk/dist/namespaces/session.js +18 -0
  89. package/sdk/dist/namespaces/session.js.map +1 -1
  90. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  91. package/sdk/dist/namespaces/transfers.js +3 -0
  92. package/sdk/dist/namespaces/transfers.js.map +1 -1
  93. package/sdk/dist/namespaces/wallets.d.ts.map +1 -1
  94. package/sdk/dist/namespaces/wallets.js +2 -0
  95. package/sdk/dist/namespaces/wallets.js.map +1 -1
  96. package/sdk/dist/namespaces/write-approval.d.ts.map +1 -1
  97. package/sdk/dist/namespaces/write-approval.js +3 -0
  98. package/sdk/dist/namespaces/write-approval.js.map +1 -1
  99. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  100. package/sdk/dist/node/actions-node.js +3 -0
  101. package/sdk/dist/node/actions-node.js.map +1 -1
  102. package/sdk/dist/node/buzz-doctor-contract.d.ts +155 -0
  103. package/sdk/dist/node/buzz-doctor-contract.d.ts.map +1 -0
  104. package/sdk/dist/node/buzz-doctor-contract.js +385 -0
  105. package/sdk/dist/node/buzz-doctor-contract.js.map +1 -0
  106. package/sdk/dist/node/buzz-doctor.d.ts +155 -0
  107. package/sdk/dist/node/buzz-doctor.d.ts.map +1 -0
  108. package/sdk/dist/node/buzz-doctor.js +810 -0
  109. package/sdk/dist/node/buzz-doctor.js.map +1 -0
  110. package/sdk/dist/node/credentials.d.ts +9 -4
  111. package/sdk/dist/node/credentials.d.ts.map +1 -1
  112. package/sdk/dist/node/credentials.js +1 -1
  113. package/sdk/dist/node/credentials.js.map +1 -1
  114. package/sdk/dist/node/diagnostics.d.ts +45 -0
  115. package/sdk/dist/node/diagnostics.d.ts.map +1 -0
  116. package/sdk/dist/node/diagnostics.js +67 -0
  117. package/sdk/dist/node/diagnostics.js.map +1 -0
  118. package/sdk/dist/node/doctor.d.ts +101 -0
  119. package/sdk/dist/node/doctor.d.ts.map +1 -0
  120. package/sdk/dist/node/doctor.js +618 -0
  121. package/sdk/dist/node/doctor.js.map +1 -0
  122. package/sdk/dist/node/index.d.ts +57 -5
  123. package/sdk/dist/node/index.d.ts.map +1 -1
  124. package/sdk/dist/node/index.js +36 -1
  125. package/sdk/dist/node/index.js.map +1 -1
  126. package/sdk/dist/node/init.d.ts +137 -0
  127. package/sdk/dist/node/init.d.ts.map +1 -0
  128. package/sdk/dist/node/init.js +507 -0
  129. package/sdk/dist/node/init.js.map +1 -0
  130. package/sdk/dist/node/lightning-wallet.d.ts +61 -0
  131. package/sdk/dist/node/lightning-wallet.d.ts.map +1 -0
  132. package/sdk/dist/node/lightning-wallet.js +126 -0
  133. package/sdk/dist/node/lightning-wallet.js.map +1 -0
  134. package/sdk/dist/node/local-actions.d.ts +23 -0
  135. package/sdk/dist/node/local-actions.d.ts.map +1 -0
  136. package/sdk/dist/node/local-actions.js +38 -0
  137. package/sdk/dist/node/local-actions.js.map +1 -0
  138. package/sdk/dist/node/org-context.d.ts +169 -0
  139. package/sdk/dist/node/org-context.d.ts.map +1 -0
  140. package/sdk/dist/node/org-context.js +403 -0
  141. package/sdk/dist/node/org-context.js.map +1 -0
  142. package/sdk/dist/node/status.d.ts +62 -0
  143. package/sdk/dist/node/status.d.ts.map +1 -0
  144. package/sdk/dist/node/status.js +132 -0
  145. package/sdk/dist/node/status.js.map +1 -0
  146. package/sdk/dist/node/vault-target.d.ts +49 -0
  147. package/sdk/dist/node/vault-target.d.ts.map +1 -0
  148. package/sdk/dist/node/vault-target.js +109 -0
  149. package/sdk/dist/node/vault-target.js.map +1 -0
  150. package/sdk/dist/node/wallets.d.ts +242 -0
  151. package/sdk/dist/node/wallets.d.ts.map +1 -0
  152. package/sdk/dist/node/wallets.js +484 -0
  153. package/sdk/dist/node/wallets.js.map +1 -0
  154. package/sdk/dist/secret-gate.d.ts +377 -0
  155. package/sdk/dist/secret-gate.d.ts.map +1 -0
  156. package/sdk/dist/secret-gate.js +273 -0
  157. package/sdk/dist/secret-gate.js.map +1 -0
  158. package/vault-surface.json +1 -1
  159. package/fixtures/buzz-v0.5.2-cli-capabilities.json +0 -92
  160. package/fixtures/run402-buzz-doctor-v1-contract.json +0 -140
  161. package/lib/buzz-doctor-contract.mjs +0 -97
  162. package/lib/buzz-doctor-contract.test.mjs +0 -134
  163. package/lib/lightning-wallet.mjs +0 -115
package/lib/init.mjs CHANGED
@@ -1,17 +1,14 @@
1
- import { readWallet, saveWallet, loadKeyStore, configDir, configureApiBase, getActiveProjectId } from "./config.mjs";
1
+ /**
2
+ * run402 init — the CLI edge of `r.init()` (`@run402/sdk/node`), which sets up
3
+ * the active profile (wallet, rail, funding, voucher, Lightning wallet, tier,
4
+ * projects, vault remote, organization, next step) or, with `--api-base`,
5
+ * configures a Run402 Core target. This module parses argv, refuses the flag
6
+ * combinations that cannot mean anything, streams the progress lines to
7
+ * stderr, and prints the summary. `run402 init astro` routes to the Astro
8
+ * scaffolder.
9
+ */
2
10
  import { getSdk } from "./sdk.mjs";
3
- import { fail } from "./sdk-errors.mjs";
4
- import { upDeployAction, deployAction } from "./next-actions.mjs";
5
- import { resolveOwningOrgId } from "./org-context.mjs";
6
- import { getActiveProfile } from "../core-dist/config.js";
7
- import { readMeta } from "../core-dist/profiles.js";
8
- import { mkdirSync } from "fs";
9
- import { fundingRecovery, fundingBlocksBootstrap } from "#sdk/node";
10
-
11
- const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
12
- const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
13
- const PATH_USD = "0x20c0000000000000000000000000000000000000";
14
- const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
11
+ import { fail, reportLocalOrSdkError } from "./sdk-errors.mjs";
15
12
 
16
13
  const HELP = `run402 init — Set up wallet, funding, and check tier status
17
14
 
@@ -84,8 +81,6 @@ Steps (idempotent when re-run with the same rail; pass --switch-rail to change r
84
81
  Run this once to get started, or again to check your setup.
85
82
  `;
86
83
 
87
- function short(addr) { return addr.slice(0, 6) + "..." + addr.slice(-4); }
88
-
89
84
  function parseApiBaseFlag(args) {
90
85
  for (let i = 0; i < args.length; i++) {
91
86
  const arg = args[i];
@@ -183,70 +178,9 @@ function parseGitRemoteFlag(args) {
183
178
  return { value: true, args: [...args.slice(0, idx), ...args.slice(idx + 1)] };
184
179
  }
185
180
 
186
- function sameOrigin(a, b) {
187
- try {
188
- return new URL(a).origin === new URL(b).origin;
189
- } catch {
190
- return false;
191
- }
192
- }
193
-
194
- async function detectTarget(apiBase) {
195
- try {
196
- const health = await getSdk({ apiBase, disablePaidFetch: true, authMode: "none" }).service.health();
197
- const kind = health && typeof health === "object" && health.mode === "core"
198
- ? "core"
199
- : sameOrigin(apiBase, "https://api.run402.com") ? "cloud" : "core";
200
- return {
201
- kind,
202
- health_status: typeof health?.status === "string" ? health.status : "ok",
203
- };
204
- } catch (err) {
205
- return {
206
- kind: sameOrigin(apiBase, "https://api.run402.com") ? "cloud" : "core",
207
- health_error: errorMessage(err),
208
- };
209
- }
210
- }
211
-
212
- function errorMessage(err) {
213
- if (err?.body && typeof err.body === "object") return err.body.message || err.body.error || err.message;
214
- return err?.message || String(err);
215
- }
216
-
217
- /**
218
- * Which project the vault scaffold acts on — and why it may act on none.
219
- *
220
- * The CLI-wide resolution order (`RUN402_PROJECT_ID`, then the active
221
- * project), not `getActiveProjectId()` alone. Reading only the active project
222
- * meant an agent that had exported `RUN402_PROJECT_ID` — the normal way to
223
- * address a project without mutating machine state — got a silent no-op: no
224
- * remote, no allocation, and no `vault` key in the summary at all, with
225
- * nothing anywhere saying `run402 projects use` was a prerequisite
226
- * (dogfood #1, finding A). A command that exits 0 having done nothing is worse
227
- * than an error, so the no-project case names itself.
228
- *
229
- * Exported so the decision is testable on its own: it is one expression, and
230
- * it was wrong in two ways at once.
231
- */
232
- export function resolveScaffoldProject(env = process.env, activeProjectId = getActiveProjectId()) {
233
- const fromEnv = typeof env.RUN402_PROJECT_ID === "string" ? env.RUN402_PROJECT_ID.trim() : "";
234
- const projectId = fromEnv || activeProjectId || null;
235
- if (projectId) return { projectId, skipped: null };
236
- return {
237
- projectId: null,
238
- skipped: "no project is selected, so there is nothing to point a run402 remote at — run `run402 projects use <project_id>` (or set RUN402_PROJECT_ID), then re-run `run402 init`",
239
- };
240
- }
241
-
242
181
  export async function run(args = []) {
243
- // Capability `astro-ssr-runtime` (v1.52): scaffold an Astro project.
244
- // Sub-routes when first positional is 'astro'. Handle BEFORE the
245
- // outer --help check so `run402 init astro --help` shows the astro
246
- // scaffolder's help, not the rail-setup help. The rest of init's
247
- // payment-rail setup is intentionally orthogonal — agents typically
248
- // run `run402 init astro <dir>` to scaffold AND `run402 init` once
249
- // to set up wallet / tier.
182
+ // `run402 init astro <dir>` scaffolds an Astro project; handled BEFORE the
183
+ // --help check so `run402 init astro --help` shows the scaffolder's help.
250
184
  if (args[0] === "astro") {
251
185
  const { runInitAstro } = await import("./init-astro.mjs");
252
186
  await runInitAstro(args.slice(1));
@@ -255,9 +189,8 @@ export async function run(args = []) {
255
189
 
256
190
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
257
191
 
258
- // Strip --voucher before anything else parses argv, so the rail/positional
259
- // logic below never sees it (an unrecognized token there would be read as a
260
- // rail name).
192
+ // Strip the value flags before the rail positional is read, so an
193
+ // unrecognized token is never taken for a rail name.
261
194
  const parsedVoucher = parseVoucherFlag(args);
262
195
  args = parsedVoucher.args;
263
196
  const voucherCode = parsedVoucher.value;
@@ -266,9 +199,6 @@ export async function run(args = []) {
266
199
  args = parsedGitRemote.args;
267
200
  const scaffoldGitRemote = parsedGitRemote.value;
268
201
 
269
- // principal-display-name (first-deploy-agent-dx): `--name <name>` sets this
270
- // principal's display name once (PATCH /agent/v1/me). Stripped here so the
271
- // rail/positional logic never sees it.
272
202
  const parsedName = parseValueFlag(args, "--name");
273
203
  args = parsedName.args;
274
204
  const displayName = parsedName.value;
@@ -276,7 +206,9 @@ export async function run(args = []) {
276
206
  fail({ code: "BAD_USAGE", message: "--name requires a non-empty value.", details: { flag: "--name" } });
277
207
  }
278
208
 
209
+ const onLine = (line) => console.error(line);
279
210
  const parsedApiBase = parseApiBaseFlag(args);
211
+ let summary;
280
212
  if (parsedApiBase.value) {
281
213
  if (parsedApiBase.args.some((arg) => typeof arg === "string" && !arg.startsWith("--"))) {
282
214
  fail({
@@ -285,10 +217,8 @@ export async function run(args = []) {
285
217
  hint: "Run `run402 init --api-base=http://my-core:4020` for Core, or `run402 init` for Run402 Cloud.",
286
218
  });
287
219
  }
288
- // This branch configures a Core/API target and returns without setting up
289
- // a wallet — there is no organization here to credit, and promo
290
- // vouchers are a Run402 Cloud concept. Say so instead of accepting the
291
- // flag and silently dropping it.
220
+ // The target branch touches no wallet and no project: a voucher has no
221
+ // organization to credit and a git scaffold has nothing to point at.
292
222
  if (voucherCode) {
293
223
  fail({
294
224
  code: "BAD_USAGE",
@@ -296,9 +226,6 @@ export async function run(args = []) {
296
226
  hint: "Configure the target first (`run402 init --api-base=…`), then redeem against Run402 Cloud with `run402 redeem <code>`.",
297
227
  });
298
228
  }
299
- // Same reasoning as --voucher: this branch configures a target and returns
300
- // without an active project, so a git scaffold has nothing to point at.
301
- // Say so rather than accepting the flag and silently dropping it.
302
229
  if (scaffoldGitRemote) {
303
230
  fail({
304
231
  code: "BAD_USAGE",
@@ -306,497 +233,29 @@ export async function run(args = []) {
306
233
  hint: "Configure the target first (`run402 init --api-base=…`), provision a project, then run `run402 init --git-remote` from the project directory.",
307
234
  });
308
235
  }
309
- const CONFIG_DIR = configDir();
310
- const detected = await detectTarget(parsedApiBase.value);
311
- const config = configureApiBase(parsedApiBase.value, {
312
- target_kind: detected.kind,
313
- ...(detected.health_status ? { health_status: detected.health_status } : {}),
314
- ...(detected.health_error ? { health_error: detected.health_error } : {}),
315
- });
316
- mkdirSync(CONFIG_DIR, { recursive: true });
317
- console.error("");
318
- console.error(` ${"Config".padEnd(10)} ${CONFIG_DIR}`);
319
- console.error(` ${"API base".padEnd(10)} ${config.api_base}`);
320
- console.error(` ${"Target".padEnd(10)} ${config.target_kind}`);
321
- if (detected.health_status) console.error(` ${"Health".padEnd(10)} ${detected.health_status}`);
322
- if (detected.health_error) console.error(` ${"Health".padEnd(10)} ${detected.health_error}`);
323
- console.error("");
324
- const summary = {
325
- config_dir: CONFIG_DIR,
326
- api_base: config.api_base,
327
- api_base_source: "profile",
328
- target: {
329
- kind: config.target_kind,
330
- ...(config.health_status ? { health_status: config.health_status } : {}),
331
- ...(config.health_error ? { health_error: config.health_error } : {}),
332
- },
333
- payment_required: config.target_kind === "cloud",
334
- next_actions: [{
335
- type: "create_project",
336
- command: 'run402 projects provision --name "my-app"',
337
- }],
338
- next_step: 'run402 projects provision --name "my-app"',
339
- };
340
- console.log(JSON.stringify(summary, null, 2));
341
- return;
342
- }
343
-
344
- // Resolve once for this invocation — reflects the active wallet/profile that
345
- // cli.mjs published to RUN402_WALLET before this module loaded.
346
- const CONFIG_DIR = configDir();
347
-
348
- const isMpp = args[0] === "mpp";
349
- const isLightning = args[0] === "lightning";
350
- const requestedRail = isLightning ? "lightning" : isMpp ? "mpp" : "x402";
351
- const switchRailConfirmed = args.includes("--switch-rail");
352
-
353
- const existingWallet = readWallet();
354
- if (existingWallet?.rail && existingWallet.rail !== requestedRail && !switchRailConfirmed) {
355
- fail({
356
- code: "RAIL_SWITCH_REQUIRES_CONFIRM",
357
- message: `Already on rail '${existingWallet.rail}'. Pass --switch-rail to switch to '${requestedRail}'.`,
358
- details: { current_rail: existingWallet.rail, requested_rail: requestedRail },
359
- });
360
- }
361
-
362
- // Human-readable progress lines go to stderr so stdout stays JSON-clean for
363
- // agents. Final structured summary emits to stdout at the end.
364
- const write = (s) => console.error(s);
365
- const line = (label, value) => write(` ${label.padEnd(10)} ${value}`);
366
- const summary = {
367
- config_dir: CONFIG_DIR,
368
- wallet: null,
369
- rail: null,
370
- network: null,
371
- balances: null,
372
- // Present (null or an object) only when --voucher was passed, so its
373
- // absence means "no code was offered" rather than "a code silently
374
- // vanished". `voucher_error` appears alongside a null `voucher`.
375
- ...(voucherCode ? { voucher: null } : {}),
376
- tier: null,
377
- projects_saved: 0,
378
- /** The project init acted on — the input to the vault scaffold decision. */
379
- active_project_id: null,
380
- next_actions: [],
381
- next_step: null,
382
- };
383
-
384
- write("");
385
-
386
- // 1. Config directory
387
- mkdirSync(CONFIG_DIR, { recursive: true });
388
- line("Config", CONFIG_DIR);
389
-
390
- // 2. Wallet
391
- let localWallet = existingWallet;
392
- const previousRail = localWallet?.rail;
393
- if (!localWallet) {
394
- const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
395
- const privateKey = generatePrivateKey();
396
- const account = privateKeyToAccount(privateKey);
397
- localWallet = { address: account.address, privateKey, created: new Date().toISOString(), funded: false, rail: requestedRail };
398
- saveWallet(localWallet);
399
- line("Wallet", `${short(localWallet.address)} (created)`);
400
- } else {
401
- // Update rail if switching (a wallet leaving Lightning keeps its pairing on disk until revoked).
402
- if (localWallet.rail !== requestedRail) {
403
- localWallet = { ...localWallet, rail: requestedRail };
404
- saveWallet(localWallet);
405
- }
406
- line("Wallet", short(localWallet.address));
407
- }
408
-
409
- const walletName = getActiveProfile();
410
- const walletMeta = readMeta(walletName);
411
- summary.wallet = { local_label: walletName, server_label: walletMeta?.label ?? null, address: localWallet.address };
412
- summary.network = isLightning ? "bitcoin-mainnet" : isMpp ? "tempo-moderato" : "base-sepolia";
413
- summary.rail = requestedRail;
414
-
415
- line("Network", isLightning ? "Bitcoin mainnet (Lightning) + Base Sepolia fallback" : isMpp ? "Tempo Moderato (testnet)" : "Base Sepolia (testnet)");
416
- line("Rail", requestedRail);
417
-
418
- // 3. Balance — check on-chain, faucet if zero
419
- let balance = 0;
420
- let fundingError;
421
- let fundingPending = false;
422
-
423
- if (isMpp) {
424
- // Tempo Moderato: read pathUSD balance
425
- const { createPublicClient, http, defineChain } = await import("viem");
426
- const tempoModerato = defineChain({
427
- id: 42431,
428
- name: "Tempo Moderato",
429
- nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
430
- rpcUrls: { default: { http: [TEMPO_RPC] } },
431
- });
432
- const client = createPublicClient({ chain: tempoModerato, transport: http() });
433
-
434
- try {
435
- const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [localWallet.address] });
436
- balance = Number(raw);
437
- } catch {}
438
-
439
- if (balance === 0) {
440
- line("Balance", "0 pathUSD — requesting Tempo faucet...");
441
- try {
442
- const res = await fetch(TEMPO_RPC, {
443
- method: "POST",
444
- headers: { "Content-Type": "application/json" },
445
- body: JSON.stringify({ jsonrpc: "2.0", method: "tempo_fundAddress", params: [localWallet.address], id: 1 }),
446
- });
447
- const data = await res.json();
448
- if (data.result) {
449
- // Tempo faucet is "instant" on-chain, but the client RPC read can be
450
- // racy relative to faucet settlement — poll up to 30s, mirroring
451
- // the x402 path below.
452
- for (let i = 0; i < 30; i++) {
453
- await new Promise(r => setTimeout(r, 1000));
454
- try {
455
- const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [localWallet.address] });
456
- balance = Number(raw);
457
- if (balance > 0) break;
458
- } catch {}
459
- }
460
- saveWallet({ ...localWallet, funded: true, lastFaucet: new Date().toISOString() });
461
- if (balance > 0) {
462
- line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD (funded)`);
463
- } else {
464
- fundingPending = true;
465
- line("Balance", "faucet sent — not yet confirmed on-chain");
466
- }
467
- } else {
468
- fundingError = data.error ?? new Error("Faucet failed");
469
- line("Balance", `faucet failed: ${data.error?.message || "unknown error"}`);
470
- }
471
- } catch (err) {
472
- fundingError = err;
473
- line("Balance", `faucet error: ${err.message}`);
474
- }
475
- } else {
476
- line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD`);
477
- }
478
- } else {
479
- // Base Sepolia: read USDC balance (existing behavior)
480
- const { createPublicClient, http } = await import("viem");
481
- const { baseSepolia } = await import("viem/chains");
482
- const client = createPublicClient({ chain: baseSepolia, transport: http() });
483
-
484
- try {
485
- const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [localWallet.address] });
486
- balance = Number(raw);
487
- } catch {}
488
-
489
- if (balance === 0) {
490
- line("Balance", "0 USDC — requesting faucet...");
491
- try {
492
- await getSdk().wallets.faucet(localWallet.address);
493
- // Poll for up to 30s
494
- for (let i = 0; i < 30; i++) {
495
- await new Promise(r => setTimeout(r, 1000));
496
- try {
497
- const raw = await client.readContract({ address: USDC_SEPOLIA, abi: USDC_ABI, functionName: "balanceOf", args: [localWallet.address] });
498
- balance = Number(raw);
499
- if (balance > 0) break;
500
- } catch {}
501
- }
502
- saveWallet({ ...localWallet, funded: true, lastFaucet: new Date().toISOString() });
503
- if (balance > 0) {
504
- line("Balance", `${(balance / 1e6).toFixed(2)} USDC (funded)`);
505
- } else {
506
- fundingPending = true;
507
- line("Balance", "faucet sent — not yet confirmed on-chain");
508
- }
509
- } catch (err) {
510
- fundingError = err;
511
- line("Balance", `faucet failed: ${errorMessage(err)}`);
512
- }
513
- } else {
514
- line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
515
- }
516
- }
517
-
518
- // 3b. Promo code, when one was handed to us.
519
- //
520
- // This runs BEFORE the balance read below so the redeemed amount shows up in
521
- // `allowance_usd_micros` without a second round-trip, and AFTER the
522
- // wallet exists so the redemption authenticates as this agent.
523
- //
524
- // NOTHING here may fail init. An advertised gift that dead-ends a build is
525
- // worse than no gift at all: the agent was told to run one command, and that
526
- // command must still leave it set up and able to work. Every failure — bad
527
- // code, expired, already used by someone else, org at its ceiling, network
528
- // down, gateway too old to know the route — warns on stderr, records
529
- // `voucher_error` in the JSON summary, and lets setup finish.
530
- if (voucherCode) {
531
236
  try {
532
- const redemption = await getSdk().vouchers.redeem(voucherCode);
533
- const redeemed = (redemption.amount_usd_micros / 1_000_000).toFixed(2);
534
- line(
535
- "Voucher",
536
- redemption.already_redeemed
537
- ? `$${redeemed} already added to the allowance (not added twice)`
538
- : `$${redeemed} added to the allowance`,
539
- );
540
- summary.voucher = {
541
- voucher_id: redemption.voucher_id,
542
- amount_usd_micros: redemption.amount_usd_micros,
543
- already_redeemed: redemption.already_redeemed,
544
- next_actions: Array.isArray(redemption.next_actions) ? redemption.next_actions : [],
545
- };
546
- // Relate the gift to what it buys, in the gateway's own words: its
547
- // `set_tier` next action names the largest tier the allowance covers, and
548
- // that purchase settles from the allowance — no wallet funding step between.
549
- const covers = summary.voucher.next_actions.find((a) => a?.type === "set_tier" && typeof a.cli === "string");
550
- if (covers) {
551
- const tierName = covers.highest_affordable_tier ?? covers.cli.replace(/^run402 tier set\s+/, "");
552
- line("Allowance", `$${redeemed} covers ${tierName} — ${covers.cli} (settles from the allowance; no wallet funds needed)`);
553
- }
237
+ summary = await getSdk().init({ apiBase: parsedApiBase.value, onLine });
554
238
  } catch (err) {
555
- // Faithful: name what failed and keep going. `voucher_error` is a
556
- // first-class summary field, not an omission the caller has to infer.
557
- const reason = errorMessage(err);
558
- line("Voucher", `not applied: ${reason}`);
559
- summary.voucher = null;
560
- summary.voucher_error = {
561
- code: err?.body?.code ?? err?.code ?? "VOUCHER_REDEEM_FAILED",
562
- message: reason,
563
- };
564
- }
565
- }
566
-
567
- // Balances mirror `run402 status`: the on-chain wallet figure above plus the
568
- // organization's Run402-held allowance (rail-independent). The allowance is
569
- // fetched best-effort so a billing read failure never blocks setup.
570
- const billing = await getSdk().billing.checkBalance(localWallet.address).catch(() => null);
571
- const hasBilling = billing && billing.exists !== false;
572
- summary.balances = {
573
- on_chain_usd_micros: balance,
574
- on_chain_token: isMpp ? "pathUSD" : "USDC",
575
- allowance_usd_micros: hasBilling ? billing.allowance_usd_micros : null,
576
- held_usd_micros: hasBilling ? (billing.held_usd_micros ?? 0) : null,
577
- };
578
-
579
- // Show note if switching rails
580
- if (previousRail && previousRail !== (isMpp ? "mpp" : "x402")) {
581
- const prev = previousRail === "mpp" ? "Tempo pathUSD" : "Base Sepolia USDC";
582
- line("Note", `Switched from ${previousRail} — ${prev} balance still available if you switch back`);
583
- }
584
-
585
- // 3b. The Lightning wallet (mpp-lightning-over-nwc): a budgeted wallet
586
- // on Run402's Hub, minted by the platform, its pairing kept beside the
587
- // Base key. Never printed. A Hub that is not configured leaves the rail
588
- // on Lightning with x402 as the live fallback, and says so.
589
- summary.lightning = null;
590
- if (isLightning) {
591
- try {
592
- const { ensureLightningWallet, describeLightning, readLightningBalance } = await import("./lightning-wallet.mjs");
593
- const result = await ensureLightningWallet();
594
- localWallet = result.localWallet;
595
- const settled = result.outcome === "stored" || result.outcome === "present";
596
- const balance = settled ? await readLightningBalance(localWallet) : null;
597
- summary.lightning = { ...(describeLightning(localWallet, result.wallet, balance) ?? {}), outcome: result.outcome };
598
- if (result.outcome === "stored") {
599
- line("Lightning", `wallet minted on Run402's Hub (${result.wallet.budget_sats} sats budget, ${result.wallet.starter_sats} starter)`);
600
- } else if (result.outcome === "present") {
601
- line("Lightning", `wallet ${localWallet.lightning.wallet_id}${balance ? ` — ${balance.balance_sats} sats` : ""}`);
602
- } else if (result.outcome === "minting") {
603
- line("Lightning", "the platform is still minting the wallet — rerun `run402 init lightning` in a few seconds");
604
- } else if (result.outcome === "unavailable") {
605
- line("Lightning", "no Hub on this gateway — paying over x402 until it is back");
606
- } else if (result.outcome === "pairing_lost") {
607
- line("Lightning", "wallet is active but its pairing was handed to another machine — `run402 wallets lightning revoke`, then init again");
608
- } else {
609
- line("Lightning", `wallet is ${result.outcome}`);
610
- }
611
- } catch (err) {
612
- summary.lightning = { outcome: "error", code: err?.body?.code ?? err?.code ?? "LIGHTNING_WALLET_FAILED", message: err?.message ?? String(err) };
613
- line("Lightning", `wallet setup failed: ${err?.message ?? String(err)} — paying over x402 until it is fixed`);
614
- }
615
- }
616
-
617
- // 4. Tier status
618
- const store = loadKeyStore();
619
- let tierInfo = null;
620
- try {
621
- tierInfo = await getSdk().tier.status();
622
- } catch {}
623
-
624
- if (tierInfo && tierInfo.tier && tierInfo.active) {
625
- const expiry = tierInfo.lease_expires_at ? tierInfo.lease_expires_at.split("T")[0] : "unknown";
626
- line("Tier", `${tierInfo.tier} (expires ${expiry})`);
627
- summary.tier = { name: tierInfo.tier, expires: tierInfo.lease_expires_at || null };
628
- } else {
629
- line("Tier", "(none)");
630
- summary.tier = null;
631
- }
632
-
633
- // 5. Projects — count locally saved project entries. Note: "saved" (not
634
- // "active") — these are all projects in the keystore, regardless of whether
635
- // the server considers them active. A project selected with
636
- // `run402 projects use` but never provisioned FROM this machine holds no
637
- // local keys, so it is legitimately not counted here; `active_project_id`
638
- // below is the field that says which project init actually acted on, and
639
- // reading `projects_saved: 0` as "init did nothing" is what made the
640
- // vault skip look silent (dogfood #1, finding A).
641
- summary.projects_saved = Object.keys(store.projects).length;
642
- line("Projects", `${summary.projects_saved} saved`);
643
-
644
- // 5b. vault git remote (vault-client-surface, task 5.7).
645
- //
646
- // Purely LOCAL git. No vault is allocated and no key material is written
647
- // here — the spec is explicit that neither exists until first capture, so the
648
- // cold-start path gains no prompt and no new failure mode. Allocation happens
649
- // on the first `git push origin <branch>` or `run402 repos capture` (D2,
650
- // repo-first-onramp) — NOT on deploy: `applyWithVault` only ever reads an
651
- // EXISTING vault's policy (D3) and never allocates one that does not exist,
652
- // so a project with no vault deploys exactly as it always did.
653
- //
654
- // Adding the remote is the DEFAULT inside a repository that already exists,
655
- // because it is pure addition: `origin` is never modified or claimed, no file
656
- // is created, nothing is rewritten. CREATING a repository is NOT the default
657
- // — `run402 init` is routinely run outside a project directory, and
658
- // `git init`-ing whatever directory the user happened to be in would be a
659
- // genuinely bad surprise. `--git-remote` opts into that one step.
660
- //
661
- // NON-FATAL in every branch: a missing git, a directory that is not a
662
- // repository, an unreachable gateway, or a `run402` remote already pointing
663
- // somewhere else must warn and let setup finish.
664
- const { projectId: activeProjectId, skipped: noProjectSkip } = resolveScaffoldProject();
665
- summary.active_project_id = activeProjectId;
666
- if (noProjectSkip) {
667
- summary.vault = null;
668
- summary.vault_skipped = noProjectSkip;
669
- line("Vault", "skipped — no project selected (run402 projects use <project_id>)");
670
- } else {
671
- summary.vault = null;
672
- try {
673
- // Dynamic import: the scaffold is the only thing here that needs the
674
- // Node SDK's hardened git runner, and a top-level import would drag it
675
- // into every init invocation (and every test that mocks ./sdk.mjs).
676
- const { hardenedGit } = await import("#sdk/node");
677
- let insideRepo = true;
678
- try {
679
- await hardenedGit(process.cwd(), ["rev-parse", "--git-dir"]);
680
- } catch {
681
- insideRepo = false;
682
- }
683
- if (!insideRepo && !scaffoldGitRemote) {
684
- summary.vault_skipped = "not a git repository — re-run with --git-remote to create one and add the remote";
685
- line("Vault", "skipped — not a git repository (--git-remote creates one)");
686
- } else {
687
- const orgId = await resolveOwningOrgId(activeProjectId);
688
- if (!orgId) {
689
- summary.vault_skipped = `could not resolve the owning org for ${activeProjectId} — the run402 remote was not added`;
690
- line("Vault", "skipped — owning org unresolved");
691
- } else {
692
- const remote = await getSdk().repos.scaffoldRemote({
693
- repo_dir: process.cwd(),
694
- org_id: orgId,
695
- project_id: activeProjectId,
696
- });
697
- // `allocated: false` is stated, not left to be inferred: this was
698
- // local git only, and no vault exists for the project yet.
699
- summary.vault = { ...remote, allocated: false };
700
- if (remote.already_present && remote.existing_url !== remote.url) {
701
- // Left exactly as it was. Name the URL that is actually in place
702
- // rather than implying the remote now points at this project.
703
- line("Vault", `remote '${remote.name}' already points at ${remote.existing_url} — left unchanged (${remote.reason})`);
704
- } else if (remote.already_present) {
705
- line("Vault", `remote '${remote.name}' already set (${remote.url})`);
706
- } else {
707
- // D1: `origin` when it was free, `run402` when it was already
708
- // taken by something else — `remote.reason` says which happened.
709
- line("Vault", `${remote.created_repository ? "initialized a repository and added" : "added"} remote '${remote.name}' -> ${remote.url} (${remote.reason})`);
710
- }
711
- }
712
- }
713
- } catch (err) {
714
- const reason = errorMessage(err);
715
- summary.vault = null;
716
- summary.vault_error = {
717
- code: err?.body?.code ?? err?.code ?? "VAULT_SCAFFOLD_FAILED",
718
- message: reason,
719
- };
720
- line("Vault", `remote not added: ${reason}`);
239
+ reportLocalOrSdkError(err);
240
+ return;
721
241
  }
242
+ console.log(JSON.stringify(summary, null, 2));
243
+ return;
722
244
  }
723
245
 
724
- // 5b. The org. `init` already materializes it (the tier read above
725
- // authenticates, which provisions the wallet's org-of-one), so NOT reporting
726
- // it forced every agent that wanted to coordinate to go find it with a second
727
- // command and copy a UUID by hand. It is an identifier, not a credential.
246
+ const rail = args[0] === "lightning" ? "lightning" : args[0] === "mpp" ? "mpp" : "x402";
728
247
  try {
729
- const orgs = await getSdk().orgs.list();
730
- const rows = Array.isArray(orgs) ? orgs : (orgs?.orgs ?? []);
731
- summary.orgs = rows.map((o) => ({
732
- org_id: o.org_id,
733
- display_name: o.display_name ?? null,
734
- role: o.role ?? null,
735
- }));
736
- if (rows.length === 1) {
737
- line("Org", `${rows[0].org_id}${rows[0].display_name ? ` (${rows[0].display_name})` : ""}`);
738
- } else if (rows.length > 1) {
739
- line("Org", `${rows.length} organizations — run402 orgs list`);
740
- }
741
- } catch {
742
- // Best-effort, exactly like the billing read: a listing failure must never
743
- // fail setup.
744
- summary.orgs = null;
745
- }
746
-
747
- // 6. Next step — canonical typed action(s); `next_step` is the back-compat
748
- // string mirror of the first action's command (one spelling, surface-wide).
749
- write("");
750
- const tierMissing = !tierInfo || !tierInfo.tier || !tierInfo.active;
751
- summary.funding = fundingRecovery(fundingError, fundingPending);
752
- const fundingBlocked = fundingBlocksBootstrap(summary.funding, {
753
- activeTier: !tierMissing, onChainBalance: balance,
754
- allowanceBalance: summary.balances.allowance_usd_micros,
755
- lightningBalance: summary.lightning?.balance_sats,
756
- });
757
- summary.next_actions = [tierMissing ? upDeployAction() : deployAction()];
758
- if (fundingBlocked) {
759
- const quotedWallet = `'${walletName.replaceAll("'", "'\\''")}'`;
760
- summary.next_actions = summary.funding.next_actions.map(action => ({ ...action,
761
- ...(["retry", "check_balance"].includes(action.type) ? {
762
- command: `run402 --wallet ${quotedWallet} wallets ${action.type === "retry" ? "fund" : "balance"}`,
763
- } : {}),
764
- }));
765
- }
766
- summary.next_step = summary.next_actions[0].command ?? null;
767
- if (fundingBlocked) {
768
- line("Funding", summary.funding.status);
769
- write(` ${summary.next_actions[0].why}`);
770
- if (summary.next_step) write(` Next: ${summary.next_step}`);
771
- } else if (tierMissing) {
772
- // `up -y` sets the prototype tier itself as part of the first
773
- // deploy; `init` never buys the tier, so the one command that finishes
774
- // the cold start is `up`, with `tier set` named as the standalone option.
775
- const allowanceCovers = summary.voucher?.next_actions?.find((a) => a?.type === "set_tier" && typeof a.cli === "string");
776
- write(" Next: run402 up -y");
777
- write(" Deploy with run402 up -y — it sets the prototype tier (free on testnet) as part of the first deploy.");
778
- if (allowanceCovers) {
779
- write(` Your allowance covers ${allowanceCovers.highest_affordable_tier ?? "a larger tier"}: ${allowanceCovers.cli} — then run402 up -y.`);
780
- } else {
781
- write(" Or set it separately: run402 tier set prototype.");
782
- }
783
- } else {
784
- write(" Ready to deploy. Run: run402 deploy --manifest app.json");
785
- }
786
- write("");
787
-
788
- // 5c. Display name (principal-display-name). Best-effort: a hiccup never
789
- // fails init — the summary says what happened.
790
- if (displayName !== undefined) {
791
- try {
792
- const me = await getSdk().orgs.setDisplayName(displayName.trim());
793
- summary.display_name = me?.principal?.display_name ?? displayName.trim();
794
- line("Name", summary.display_name);
795
- } catch (err) {
796
- summary.display_name = null;
797
- summary.display_name_error = { code: err?.body?.code ?? err?.code ?? "DISPLAY_NAME_FAILED", message: err?.message ?? String(err) };
798
- }
248
+ summary = await getSdk().init({
249
+ rail,
250
+ switchRail: args.includes("--switch-rail"),
251
+ voucher: voucherCode,
252
+ gitRemote: scaffoldGitRemote,
253
+ ...(displayName !== undefined ? { name: displayName } : {}),
254
+ onLine,
255
+ });
256
+ } catch (err) {
257
+ reportLocalOrSdkError(err);
258
+ return;
799
259
  }
800
-
801
260
  console.log(JSON.stringify(summary, null, 2));
802
261
  }