run402 3.8.0 → 3.8.2

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 CHANGED
@@ -7,13 +7,13 @@ For the full CLI reference (every flag, every subcommand) see **<https://docs.ru
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- npm install -g run402
10
+ npm install -g run402@latest
11
11
  ```
12
12
 
13
13
  Or run without installing:
14
14
 
15
15
  ```bash
16
- npx run402 <command>
16
+ npx -y run402@latest <command>
17
17
  ```
18
18
 
19
19
  ## 30-second start
@@ -29,6 +29,8 @@ That's a real Postgres database + a deployed static site, paid for autonomously
29
29
 
30
30
  Every command prints **JSON to stdout**, **JSON errors to stderr**, and exits **0 on success / 1 on failure**. Designed for shells, scripts, and agent loops — pipe everything to `jq`.
31
31
 
32
+ Stale CLI notices are advisory and never pollute success stdout. When a cached npm check says a newer `run402` exists, normal commands may emit a structured `cli.update_available` JSON object on stderr, or an NDJSON event in `--json-stream`. Run `run402 doctor --refresh` for the explicit live check; it reports whether this invocation is local, global, npx/npm exec, pnpm/yarn/bun, or custom, and includes an `upgrade_client` action with both `command` and `argv`.
33
+
32
34
  ## Common commands
33
35
 
34
36
  ### One-command app deploy
package/lib/deploy-v2.mjs CHANGED
@@ -38,6 +38,7 @@ import { normalizeArgv } from "./argparse.mjs";
38
38
  import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
39
39
  import { withAutoApprove } from "./operator.mjs";
40
40
  import { editRequestAction, nextAction, retryAction } from "./next-actions.mjs";
41
+ import { createUpdateCheckScheduler, emitUpdateNotice } from "./update-check.mjs";
41
42
 
42
43
  const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
43
44
 
@@ -444,6 +445,7 @@ function parsePromoteArgs(args) {
444
445
 
445
446
  async function promoteCmd(args) {
446
447
  const opts = parsePromoteArgs(args);
448
+ emitDeployUpdateNotice("promote", args, { quiet: opts.quiet });
447
449
  const projectId = opts.project ?? resolveProjectId(null);
448
450
 
449
451
  // Preserve the aggressive early-exit when no allowance is configured
@@ -500,6 +502,13 @@ function makeStderrEventWriter(quiet) {
500
502
  };
501
503
  }
502
504
 
505
+ function emitDeployUpdateNotice(subcommand, args, { quiet = false } = {}) {
506
+ const scheduler = createUpdateCheckScheduler({
507
+ command: ["run402", "deploy", subcommand, ...args],
508
+ });
509
+ emitUpdateNotice(scheduler.cachedNotice, { quiet });
510
+ }
511
+
503
512
  function parseApplyArgs(args) {
504
513
  const opts = {
505
514
  manifest: null,
@@ -787,6 +796,7 @@ async function applyCmd(args) {
787
796
  const opts = parseApplyArgs(args);
788
797
  const { source, error: sourceError } = resolveApplySource(opts, hasStdinSource());
789
798
  if (sourceError) fail(sourceError);
799
+ emitDeployUpdateNotice("apply", args, { quiet: opts.quiet });
790
800
 
791
801
  let raw;
792
802
  let manifestPath = null;
@@ -1377,6 +1387,7 @@ async function resumeCmd(args) {
1377
1387
  project: parsed.flags["--project"] ?? null,
1378
1388
  quiet: Boolean(parsed.flags["--quiet"]),
1379
1389
  };
1390
+ emitDeployUpdateNotice("resume", args, { quiet: opts.quiet });
1380
1391
  const project = resolveProjectId(opts.project);
1381
1392
 
1382
1393
  allowanceAuthHeaders("/apply/v1/operations");
package/lib/doctor.mjs CHANGED
@@ -19,22 +19,25 @@ import {
19
19
  scanSourceTree,
20
20
  SCAN_SEVERITY,
21
21
  } from "./doctor-source-scan.mjs";
22
+ import { doctorUpdateCheck } from "./update-check.mjs";
22
23
 
23
24
  const HELP = `run402 doctor — Health and config diagnostics
24
25
 
25
26
  Usage:
26
- run402 doctor [--verbose] [--no-scan] [--scan-dir <D>]
27
+ run402 doctor [--verbose] [--refresh] [--no-scan] [--scan-dir <D>]
27
28
 
28
29
  Output:
29
30
  Stdout is a JSON report { ok, checks: [{ name, status, value?, hint?, message? }] }.
30
31
 
31
32
  Options:
32
33
  --verbose Include extra detail (timing, error messages)
34
+ --refresh Wait for a bounded live npm version check for the run402 CLI
33
35
  --no-scan Skip the source-tree scan (config / health checks only)
34
36
  --scan-dir D Scan a custom directory instead of \`<cwd>/src\`
35
37
 
36
38
  Checks performed:
37
39
  - Config directory exists and is writable
40
+ - Installed run402 CLI version and update guidance
38
41
  - Allowance is configured and on a valid rail (x402 / mpp)
39
42
  - Keystore has at least one wallet
40
43
  - API_BASE is reachable (network check via /health)
@@ -60,6 +63,7 @@ export async function run(sub, args = []) {
60
63
  return;
61
64
  }
62
65
  const verbose = all.includes("--verbose");
66
+ const refresh = all.includes("--refresh");
63
67
  const skipScan = all.includes("--no-scan");
64
68
  const scanDirArgIdx = all.indexOf("--scan-dir");
65
69
  const scanDirOverride = scanDirArgIdx >= 0 ? all[scanDirArgIdx + 1] : null;
@@ -87,6 +91,18 @@ export async function run(sub, args = []) {
87
91
  });
88
92
  }
89
93
 
94
+ // 1b. CLI version/update state. This is advisory: stale or unknown version
95
+ // state should help the user, not hide the rest of doctor.
96
+ try {
97
+ checks.push(await doctorUpdateCheck({ refresh }));
98
+ } catch (err) {
99
+ checks.push({
100
+ name: "cli_update",
101
+ status: "unknown",
102
+ message: err instanceof Error ? err.message : String(err),
103
+ });
104
+ }
105
+
90
106
  // 2. Allowance.
91
107
  try {
92
108
  const allowance = readAllowance();
@@ -324,7 +340,7 @@ export async function run(sub, args = []) {
324
340
  // 'warning' counts as ok for exit-code purposes — gaps are surfaced in
325
341
  // output but don't fail the doctor. Only hard 'error' / 'missing' /
326
342
  // 'empty' fail.
327
- const allOk = checks.every((c) => c.status === "ok" || c.status === "warning" || c.status === "skipped");
343
+ const allOk = checks.every((c) => c.status === "ok" || c.status === "warning" || c.status === "skipped" || c.status === "unknown");
328
344
 
329
345
  console.log(JSON.stringify({ ok: allOk, checks }, null, 2));
330
346
  process.exit(allOk ? 0 : 1);
package/lib/up.mjs CHANGED
@@ -3,6 +3,7 @@ import { stdin as input, stderr as output } from "node:process";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail } from "./sdk-errors.mjs";
5
5
  import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs } from "./argparse.mjs";
6
+ import { createUpdateCheckScheduler, emitUpdateNotice } from "./update-check.mjs";
6
7
 
7
8
  const HELP = `run402 up — Provision/link/deploy the current app
8
9
 
@@ -44,6 +45,11 @@ Options:
44
45
  --json-stream Emit NDJSON progress events on stdout and a final result event.
45
46
  --quiet Suppress action progress events on stderr.
46
47
 
48
+ Update notices:
49
+ Stale CLI notices are advisory and never change the result payload or exit
50
+ code. Non-streaming notices are JSON on stderr; --json-stream emits
51
+ cli.update_available as an NDJSON event.
52
+
47
53
  Project resolution:
48
54
  explicit --project > .run402/project.json > manifest project_id > approved
49
55
  project creation from --name > approved active-project fallback.
@@ -184,6 +190,10 @@ export async function run(args = []) {
184
190
  });
185
191
  }
186
192
  const allowWarningCodes = collectRepeatedValues(parsed, "--allow-warning");
193
+ const updateScheduler = createUpdateCheckScheduler({
194
+ command: ["run402", "up", ...parsed],
195
+ });
196
+ emitUpdateNotice(updateScheduler.cachedNotice, { jsonStream, quiet });
187
197
 
188
198
  try {
189
199
  const sdk = getSdk();
@@ -224,11 +234,20 @@ export async function run(args = []) {
224
234
  } else {
225
235
  console.log(JSON.stringify(result, null, 2));
226
236
  }
237
+ if (shouldExitNonZeroForUpResult(result)) {
238
+ process.exitCode = 1;
239
+ }
227
240
  } catch (err) {
228
241
  reportSdkError(err);
229
242
  }
230
243
  }
231
244
 
245
+ export function shouldExitNonZeroForUpResult(result) {
246
+ return result?.action === "up" &&
247
+ result?.mode === "apply" &&
248
+ result?.result?.app_result?.status === "deployed_unverified";
249
+ }
250
+
232
251
  function parseExecutionMode(args) {
233
252
  const modes = [];
234
253
  if (args.includes("--check")) modes.push("--check");
@@ -0,0 +1,29 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { shouldExitNonZeroForUpResult } from "./up.mjs";
4
+
5
+ test("run402 up exits nonzero for deployed-but-unverified app results", () => {
6
+ assert.equal(shouldExitNonZeroForUpResult({
7
+ action: "up",
8
+ mode: "apply",
9
+ result: {
10
+ app_result: { status: "deployed_unverified" },
11
+ },
12
+ }), true);
13
+
14
+ assert.equal(shouldExitNonZeroForUpResult({
15
+ action: "up",
16
+ mode: "apply",
17
+ result: {
18
+ app_result: { status: "succeeded" },
19
+ },
20
+ }), false);
21
+
22
+ assert.equal(shouldExitNonZeroForUpResult({
23
+ action: "up",
24
+ mode: "check",
25
+ result: {
26
+ app_result: { status: "deployed_unverified" },
27
+ },
28
+ }), false);
29
+ });