run402 3.5.2 → 3.5.4

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 (55) hide show
  1. package/cli.mjs +20 -0
  2. package/lib/archives.mjs +55 -0
  3. package/lib/cache.mjs +10 -9
  4. package/lib/cloud.mjs +254 -0
  5. package/lib/config.mjs +6 -0
  6. package/lib/core.mjs +122 -0
  7. package/lib/deploy-v2.mjs +65 -60
  8. package/lib/doctor-source-scan.mjs +1 -1
  9. package/lib/functions.mjs +2 -1
  10. package/lib/init.mjs +8 -6
  11. package/lib/next-actions.mjs +61 -0
  12. package/lib/projects.mjs +16 -3
  13. package/lib/secrets.mjs +3 -2
  14. package/lib/subdomains.mjs +2 -1
  15. package/lib/tier.mjs +16 -5
  16. package/package.json +1 -1
  17. package/sdk/dist/credentials.d.ts +1 -1
  18. package/sdk/dist/credentials.d.ts.map +1 -1
  19. package/sdk/dist/errors.d.ts +25 -2
  20. package/sdk/dist/errors.d.ts.map +1 -1
  21. package/sdk/dist/errors.js +31 -2
  22. package/sdk/dist/errors.js.map +1 -1
  23. package/sdk/dist/index.d.ts +5 -1
  24. package/sdk/dist/index.d.ts.map +1 -1
  25. package/sdk/dist/index.js +4 -0
  26. package/sdk/dist/index.js.map +1 -1
  27. package/sdk/dist/namespaces/archives.d.ts +12 -0
  28. package/sdk/dist/namespaces/archives.d.ts.map +1 -0
  29. package/sdk/dist/namespaces/archives.js +202 -0
  30. package/sdk/dist/namespaces/archives.js.map +1 -0
  31. package/sdk/dist/namespaces/archives.types.d.ts +163 -0
  32. package/sdk/dist/namespaces/archives.types.d.ts.map +1 -0
  33. package/sdk/dist/namespaces/archives.types.js +2 -0
  34. package/sdk/dist/namespaces/archives.types.js.map +1 -0
  35. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  36. package/sdk/dist/namespaces/projects.js +3 -0
  37. package/sdk/dist/namespaces/projects.js.map +1 -1
  38. package/sdk/dist/namespaces/projects.types.d.ts +9 -0
  39. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  40. package/sdk/dist/namespaces/tier.d.ts +12 -1
  41. package/sdk/dist/namespaces/tier.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/tier.js +4 -1
  43. package/sdk/dist/namespaces/tier.js.map +1 -1
  44. package/sdk/dist/node/archives-node.d.ts +14 -0
  45. package/sdk/dist/node/archives-node.d.ts.map +1 -0
  46. package/sdk/dist/node/archives-node.js +715 -0
  47. package/sdk/dist/node/archives-node.js.map +1 -0
  48. package/sdk/dist/node/index.d.ts +4 -1
  49. package/sdk/dist/node/index.d.ts.map +1 -1
  50. package/sdk/dist/node/index.js +3 -0
  51. package/sdk/dist/node/index.js.map +1 -1
  52. package/sdk/dist/scoped.d.ts +12 -0
  53. package/sdk/dist/scoped.d.ts.map +1 -1
  54. package/sdk/dist/scoped.js +25 -0
  55. package/sdk/dist/scoped.js.map +1 -1
package/lib/deploy-v2.mjs CHANGED
@@ -36,6 +36,7 @@ import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId } from
36
36
  import { normalizeArgv } from "./argparse.mjs";
37
37
  import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
38
38
  import { withAutoApprove } from "./operator.mjs";
39
+ import { editRequestAction, nextAction, retryAction } from "./next-actions.mjs";
39
40
 
40
41
  const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
41
42
 
@@ -939,66 +940,66 @@ const CI_DEPLOY_ERROR_GUIDANCE = {
939
940
  invalid_token: {
940
941
  hint: "Ensure the workflow has permissions: id-token: write and is running in the repository/branch linked with run402 ci link github.",
941
942
  next_actions: [
942
- "Check the workflow permissions block includes id-token: write.",
943
- "Re-run run402 ci link github if the repository, branch, or environment changed.",
943
+ nextAction("edit_request", { why: "Check the workflow permissions block includes id-token: write." }),
944
+ editRequestAction("run402 ci link github", "Re-link if the repository, branch, or environment changed."),
944
945
  ],
945
946
  },
946
947
  access_denied: {
947
948
  hint: "The OIDC token was valid, but no active Run402 CI binding allowed this workflow.",
948
949
  next_actions: [
949
- "Run run402 ci list --project <prj_...> locally to inspect bindings.",
950
- "Run run402 ci link github again for this repository/branch/environment.",
950
+ editRequestAction("run402 ci list --project <prj_...>", "Inspect active CI bindings for this project."),
951
+ editRequestAction("run402 ci link github", "Link this repository, branch, and environment to Run402."),
951
952
  ],
952
953
  },
953
954
  binding_revoked: {
954
955
  hint: "A matching CI binding existed but was revoked — most often because the project was transferred or handed to a new owner, which suspends the prior org's CI bindings.",
955
956
  next_actions: [
956
- "Run run402 ci link github again from this repository to re-create the binding.",
957
- "Do not run run402 ci set-asset-scopes it returns 409 on a revoked binding.",
958
- "Run run402 ci list --project <prj_...> locally to confirm the binding state.",
957
+ editRequestAction("run402 ci link github", "Re-create the CI binding from this repository."),
958
+ nextAction("edit_request", { why: "Do not run run402 ci set-asset-scopes; it returns 409 on a revoked binding." }),
959
+ editRequestAction("run402 ci list --project <prj_...>", "Confirm the binding state locally."),
959
960
  ],
960
961
  },
961
962
  event_not_allowed: {
962
963
  hint: "This binding only allows push and workflow_dispatch events in v1.",
963
964
  next_actions: [
964
- "Trigger the workflow with push or workflow_dispatch.",
965
- "Create a separate follow-up design before enabling PR deploy events.",
965
+ nextAction("retry", { why: "Trigger the workflow with push or workflow_dispatch." }),
966
+ nextAction("edit_request", { why: "Create a separate follow-up design before enabling PR deploy events." }),
966
967
  ],
967
968
  },
968
969
  repository_id_mismatch: {
969
970
  hint: "The GitHub repository id in the OIDC token does not match the linked binding.",
970
971
  next_actions: [
971
- "Run run402 ci link github again from the current repository.",
972
- "If automatic lookup fails, pass --repository-id with the numeric GitHub repository id.",
972
+ editRequestAction("run402 ci link github", "Re-link from the current repository."),
973
+ editRequestAction("run402 ci link github --repository-id <id>", "Pass the numeric GitHub repository id if automatic lookup fails."),
973
974
  ],
974
975
  },
975
976
  forbidden_spec_field: {
976
977
  hint: "CI deploys can deploy site/functions/database content and route declarations only when the binding includes covering route scopes.",
977
978
  next_actions: [
978
- "Remove forbidden fields such as secrets, subdomains, or checks from the CI manifest.",
979
- "Keep the normalized manifest small enough to avoid manifest_ref.",
979
+ nextAction("edit_request", { why: "Remove forbidden fields such as secrets, subdomains, or checks from the CI manifest." }),
980
+ nextAction("edit_request", { why: "Keep the normalized manifest small enough to avoid manifest_ref." }),
980
981
  ],
981
982
  },
982
983
  CI_ROUTE_SCOPE_DENIED: {
983
984
  hint: "This CI binding does not cover one or more route declarations in the deploy manifest.",
984
985
  next_actions: [
985
- "Re-run run402 ci link github with --route-scope for every exact or prefix path the workflow may deploy.",
986
- "Use exact scopes such as --route-scope /admin or prefix scopes such as --route-scope /api/*.",
987
- "Run the deploy locally with run402 deploy apply for route changes outside the CI delegation.",
986
+ editRequestAction("run402 ci link github --route-scope <pattern>", "Add a CI route scope for every exact or prefix path the workflow may deploy."),
987
+ nextAction("edit_request", { why: "Use exact scopes such as --route-scope /admin or prefix scopes such as --route-scope /api/*." }),
988
+ editRequestAction("run402 deploy apply", "Run route changes locally when they are outside the CI delegation."),
988
989
  ],
989
990
  },
990
991
  forbidden_plan: {
991
992
  hint: "The gateway rejected this deploy plan for CI. Keep CI deploys to the allowed resources and re-link if policy changed.",
992
993
  next_actions: [
993
- "Inspect the gateway error details for the rejected resource.",
994
- "Run the deploy locally with run402 deploy apply for operations outside the CI allowlist.",
994
+ nextAction("edit_request", { why: "Inspect the gateway error details for the rejected resource." }),
995
+ editRequestAction("run402 deploy apply", "Run operations outside the CI allowlist locally."),
995
996
  ],
996
997
  },
997
998
  payment_required: {
998
999
  hint: "The project tier or payment state does not allow this CI deploy.",
999
1000
  next_actions: [
1000
- "Run run402 tier status --project <prj_...> locally.",
1001
- "Renew or upgrade the project tier, then re-run the workflow.",
1001
+ editRequestAction("run402 tier status --project <prj_...>", "Inspect project tier and payment state locally."),
1002
+ editRequestAction("run402 tier set <tier>", "Renew or upgrade the project tier, then re-run the workflow."),
1002
1003
  ],
1003
1004
  },
1004
1005
  };
@@ -1032,18 +1033,22 @@ function enhanceDeployWarningError(err) {
1032
1033
  : code
1033
1034
  ? [code]
1034
1035
  : [];
1035
- const allowWarningAction = unacknowledgedCodes.length === 1
1036
- ? `Retry with \`--allow-warning ${unacknowledgedCodes[0]}\` only after reviewing that warning.`
1037
- : unacknowledgedCodes.length > 1
1038
- ? `Retry with one \`--allow-warning <code>\` per reviewed warning code: ${unacknowledgedCodes.join(", ")}.`
1039
- : "Retry with `--allow-warning <code>` only after reviewing the warning.";
1036
+ const allowWarningCommand = unacknowledgedCodes.length > 0
1037
+ ? `run402 deploy apply ${unacknowledgedCodes.map((warningCode) => `--allow-warning ${warningCode}`).join(" ")}`
1038
+ : "run402 deploy apply --allow-warning <code>";
1039
+ const allowWarningAction = editRequestAction(
1040
+ allowWarningCommand,
1041
+ unacknowledgedCodes.length > 0
1042
+ ? `Retry only after reviewing warning code${unacknowledgedCodes.length === 1 ? "" : "s"}: ${unacknowledgedCodes.join(", ")}.`
1043
+ : "Retry only after reviewing the warning code.",
1044
+ );
1040
1045
  const defaultNextActions = [
1041
1046
  ...(affected.length > 0
1042
- ? [`Set or inspect affected secrets: ${Array.from(new Set(affected)).join(", ")}`]
1047
+ ? [editRequestAction("run402 secrets set <project> <KEY> --stdin", `Set or inspect affected secrets: ${Array.from(new Set(affected)).join(", ")}`)]
1043
1048
  : []),
1044
- "Retry `run402 deploy apply` after resolving warnings.",
1049
+ retryAction("run402 deploy apply", "Retry after resolving warnings."),
1045
1050
  allowWarningAction,
1046
- "Use `--allow-warnings` only when every warning was explicitly reviewed.",
1051
+ editRequestAction("run402 deploy apply --allow-warnings", "Use only when every warning was explicitly reviewed."),
1047
1052
  ];
1048
1053
  enhanced.body = {
1049
1054
  ...existingBody,
@@ -1066,98 +1071,98 @@ const ROUTE_WARNING_GUIDANCE = {
1066
1071
  PUBLIC_ROUTED_FUNCTION: {
1067
1072
  hint: "A deploy route makes a function public same-origin browser ingress; direct /functions/v1/:name remains API-key protected.",
1068
1073
  next_actions: [
1069
- "Review application auth and authorization in the routed function.",
1070
- "Add CSRF protection for cookie-authenticated POST/PUT/PATCH/DELETE routes.",
1071
- "Implement CORS and OPTIONS explicitly when cross-origin callers are intended.",
1072
- "Retry with --allow-warnings only after the public ingress review is intentional.",
1074
+ nextAction("edit_request", { why: "Review application auth and authorization in the routed function." }),
1075
+ nextAction("edit_request", { why: "Add CSRF protection for cookie-authenticated POST/PUT/PATCH/DELETE routes." }),
1076
+ nextAction("edit_request", { why: "Implement CORS and OPTIONS explicitly when cross-origin callers are intended." }),
1077
+ retryAction("run402 deploy apply --allow-warnings", "Retry only after the public ingress review is intentional."),
1073
1078
  ],
1074
1079
  },
1075
1080
  ROUTE_TARGET_CARRIED_FORWARD: {
1076
1081
  hint: "A carried-forward route still points at a base-release function target.",
1077
1082
  next_actions: [
1078
- "Inspect the active release with run402 deploy release active.",
1079
- "Deploy routes.replace if the target should change in this release.",
1083
+ editRequestAction("run402 deploy release active", "Inspect the active release."),
1084
+ editRequestAction("run402 deploy apply --manifest <path>", "Deploy routes.replace if the target should change in this release."),
1080
1085
  ],
1081
1086
  },
1082
1087
  ROUTE_SHADOWS_STATIC_PATH: {
1083
1088
  hint: "A dynamic route shadows a static site path.",
1084
1089
  next_actions: [
1085
- "Inspect the warning details for affected static paths.",
1086
- "Inspect live routes with run402 deploy release active.",
1087
- "Retry with --allow-warnings only when dynamic shadowing is intentional.",
1090
+ nextAction("edit_request", { why: "Inspect the warning details for affected static paths." }),
1091
+ editRequestAction("run402 deploy release active", "Inspect live routes."),
1092
+ retryAction("run402 deploy apply --allow-warnings", "Retry only when dynamic shadowing is intentional."),
1088
1093
  ],
1089
1094
  },
1090
1095
  WILDCARD_ROUTE_SHADOWS_STATIC_PATHS: {
1091
1096
  hint: "A prefix wildcard route shadows one or more static site paths.",
1092
1097
  next_actions: [
1093
- "Review affected route/static path details.",
1094
- "Split exact routes or move static paths if the shadowing is accidental.",
1095
- "Retry with --allow-warnings only when the wildcard shadowing is intentional.",
1098
+ nextAction("edit_request", { why: "Review affected route/static path details." }),
1099
+ nextAction("edit_request", { why: "Split exact routes or move static paths if the shadowing is accidental." }),
1100
+ retryAction("run402 deploy apply --allow-warnings", "Retry only when the wildcard shadowing is intentional."),
1096
1101
  ],
1097
1102
  },
1098
1103
  METHOD_SPECIFIC_ROUTE_ALLOWS_GET_STATIC_FALLBACK: {
1099
1104
  hint: "A method-specific route allows static fallback for unmatched methods such as GET.",
1100
1105
  next_actions: [
1101
- "Confirm that static fallback for GET/HEAD is intended.",
1102
- "Add method coverage or static files deliberately.",
1106
+ nextAction("edit_request", { why: "Confirm that static fallback for GET/HEAD is intended." }),
1107
+ nextAction("edit_request", { why: "Add method coverage or static files deliberately." }),
1103
1108
  ],
1104
1109
  },
1105
1110
  WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS: {
1106
1111
  hint: "A wildcard function route only allows GET/HEAD, so POST/PUT/PATCH/DELETE paths under that prefix will be rejected before the function runs.",
1107
1112
  next_actions: [
1108
- "Add the mutation methods the routed function supports, such as POST.",
1109
- "Omit methods to allow every supported method when the route is an API surface.",
1110
- "Set acknowledge_readonly: true on an intentionally read-only GET/HEAD wildcard function route.",
1111
- "Use --allow-warning WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS only as a reviewed CLI escape hatch.",
1113
+ nextAction("edit_request", { why: "Add the mutation methods the routed function supports, such as POST." }),
1114
+ nextAction("edit_request", { why: "Omit methods to allow every supported method when the route is an API surface." }),
1115
+ nextAction("edit_request", { why: "Set acknowledge_readonly: true on an intentionally read-only GET/HEAD wildcard function route." }),
1116
+ retryAction("run402 deploy apply --allow-warning WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS", "Use only as a reviewed CLI escape hatch."),
1112
1117
  ],
1113
1118
  },
1114
1119
  ROUTE_TABLE_NEAR_LIMIT: {
1115
1120
  hint: "The route table is near the gateway/project limit.",
1116
1121
  next_actions: [
1117
- "Consolidate prefix routes where possible.",
1118
- "Remove stale route entries before adding more.",
1122
+ nextAction("edit_request", { why: "Consolidate prefix routes where possible." }),
1123
+ nextAction("edit_request", { why: "Remove stale route entries before adding more." }),
1119
1124
  ],
1120
1125
  },
1121
1126
  ROUTES_NOT_ENABLED: {
1122
1127
  hint: "Deploy-v2 web routes are not enabled for this project or environment; direct /functions/v1/:name remains protected and is not a browser-route substitute.",
1123
1128
  next_actions: [
1124
- "Deploy without the routes resource, or request route enablement for this project/environment.",
1125
- "Keep direct function invocation API-key protected; do not substitute it for same-origin browser routes.",
1129
+ nextAction("edit_request", { why: "Deploy without the routes resource, or request route enablement for this project/environment." }),
1130
+ nextAction("edit_request", { why: "Keep direct function invocation API-key protected; do not substitute it for same-origin browser routes." }),
1126
1131
  ],
1127
1132
  },
1128
1133
  STATIC_ALIAS_SHADOWS_STATIC_PATH: {
1129
1134
  hint: "A static route target shadows a direct static path at the same public URL.",
1130
1135
  next_actions: [
1131
- "Inspect the route pattern, target.file, and direct static path.",
1132
- "Confirm only when the static route target is intentional.",
1136
+ nextAction("edit_request", { why: "Inspect the route pattern, target.file, and direct static path." }),
1137
+ nextAction("edit_request", { why: "Confirm only when the static route target is intentional." }),
1133
1138
  ],
1134
1139
  },
1135
1140
  STATIC_ALIAS_RELATIVE_ASSET_RISK: {
1136
1141
  hint: "Relative asset URLs inside the target HTML may resolve differently at the static route target URL.",
1137
1142
  next_actions: [
1138
- "Inspect the target HTML for relative asset references.",
1139
- "Use absolute asset URLs or confirm only when the alternate URL is intentional.",
1143
+ nextAction("edit_request", { why: "Inspect the target HTML for relative asset references." }),
1144
+ nextAction("edit_request", { why: "Use absolute asset URLs or confirm only when the alternate URL is intentional." }),
1140
1145
  ],
1141
1146
  },
1142
1147
  STATIC_ALIAS_DUPLICATE_CANONICAL_URL: {
1143
1148
  hint: "Both the static route target URL and the target file URL may be publicly reachable.",
1144
1149
  next_actions: [
1145
- "Decide which URL should be canonical.",
1146
- "Update links/canonical tags or accept the duplicate public URL intentionally.",
1150
+ nextAction("edit_request", { why: "Decide which URL should be canonical." }),
1151
+ nextAction("edit_request", { why: "Update links/canonical tags or accept the duplicate public URL intentionally." }),
1147
1152
  ],
1148
1153
  },
1149
1154
  STATIC_ALIAS_EXTENSIONLESS_NON_HTML: {
1150
1155
  hint: "An extensionless static route target points at a non-HTML file.",
1151
1156
  next_actions: [
1152
- "Check that the extensionless route is meant to serve that content type.",
1153
- "Prefer extensionless static route targets for HTML pages.",
1157
+ nextAction("edit_request", { why: "Check that the extensionless route is meant to serve that content type." }),
1158
+ nextAction("edit_request", { why: "Prefer extensionless static route targets for HTML pages." }),
1154
1159
  ],
1155
1160
  },
1156
1161
  STATIC_ALIAS_TABLE_NEAR_LIMIT: {
1157
1162
  hint: "Static route targets count toward the route table limit.",
1158
1163
  next_actions: [
1159
- "Consolidate manual static route targets where possible.",
1160
- "Avoid one route entry per page for large sites until framework-scale Web Output support exists.",
1164
+ nextAction("edit_request", { why: "Consolidate manual static route targets where possible." }),
1165
+ nextAction("edit_request", { why: "Avoid one route entry per page for large sites until framework-scale Web Output support exists." }),
1161
1166
  ],
1162
1167
  },
1163
1168
  };
@@ -29,7 +29,7 @@
29
29
  * summary. Wired into `run402 deploy` pre-flight as a deploy-failing
30
30
  * gate for the error severities; non-blocking for warning severities.
31
31
  *
32
- * @see openspec/changes/auth-aware-ssr/specs/functions-sdk-auth-model
32
+ * @see the auth-aware-ssr OpenSpec change (functions-sdk-auth-model)
33
33
  */
34
34
 
35
35
  import { readdirSync, readFileSync, statSync } from "node:fs";
package/lib/functions.mjs CHANGED
@@ -3,6 +3,7 @@ import { findProject, API } from "./config.mjs";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail } from "./sdk-errors.mjs";
5
5
  import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, validateRegularFile } from "./argparse.mjs";
6
+ import { cliCommandAction } from "./next-actions.mjs";
6
7
 
7
8
  const FUNCTION_LOG_REQUEST_ID_RE = /^req_[A-Za-z0-9_-]{4,128}$/;
8
9
  const ISO_DATE_TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
@@ -541,7 +542,7 @@ async function rebuild(projectId, args = []) {
541
542
  code: "CANNOT_REBUILD_UNLOCKED_DEPS",
542
543
  message: err.body.error || err.body.message || "Function was deployed before dependency locking.",
543
544
  hint: UNLOCKED_DEPS_HINT,
544
- next_actions: [`run402 functions deploy ${projectId} ${name} --file <file>`],
545
+ next_actions: [cliCommandAction("deploy", `run402 functions deploy ${projectId} ${name} --file <file>`, "Redeploy the function once so Run402 can lock dependencies for future rebuilds.")],
545
546
  retryable: false,
546
547
  safe_to_retry: false,
547
548
  });
package/lib/init.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readAllowance, saveAllowance, loadKeyStore, configDir } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { fail } from "./sdk-errors.mjs";
4
+ import { setTierAction, deployAction } from "./next-actions.mjs";
4
5
  import { getActiveProfile } from "../core-dist/config.js";
5
6
  import { readMeta } from "../core-dist/profiles.js";
6
7
  import { mkdirSync } from "fs";
@@ -95,6 +96,7 @@ export async function run(args = []) {
95
96
  balances: null,
96
97
  tier: null,
97
98
  projects_saved: 0,
99
+ next_actions: [],
98
100
  next_step: null,
99
101
  };
100
102
 
@@ -268,18 +270,18 @@ export async function run(args = []) {
268
270
  summary.projects_saved = Object.keys(store.projects).length;
269
271
  line("Projects", `${summary.projects_saved} saved`);
270
272
 
271
- // 6. Next step
273
+ // 6. Next step — canonical typed action(s); `next_step` is the back-compat
274
+ // string mirror of the first action's command (one spelling, surface-wide).
272
275
  write("");
273
- const nextStep = (!tierInfo || !tierInfo.tier || !tierInfo.active)
274
- ? "run402 tier set prototype"
275
- : "run402 deploy apply --manifest app.json";
276
- if (!tierInfo || !tierInfo.tier || !tierInfo.active) {
276
+ const tierMissing = !tierInfo || !tierInfo.tier || !tierInfo.active;
277
+ summary.next_actions = [tierMissing ? setTierAction("prototype") : deployAction()];
278
+ summary.next_step = summary.next_actions[0].command;
279
+ if (tierMissing) {
277
280
  write(" Next: run402 tier set prototype");
278
281
  } else {
279
282
  write(" Ready to deploy. Run: run402 deploy apply --manifest app.json");
280
283
  }
281
284
  write("");
282
- summary.next_step = nextStep;
283
285
 
284
286
  console.log(JSON.stringify(summary, null, 2));
285
287
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Canonical bootstrap `next_actions` entries
3
+ * (change: keep-agent-in-loop-on-cold-start).
4
+ *
5
+ * One spelling, surface-wide: each cold-start wall hands the agent a typed
6
+ * action whose `command` is the literal CLI invocation. The shape mirrors the
7
+ * gateway's `next_actions` (`{ type, why, ... }`) extended with `command` for
8
+ * CLI-resolvable, client-side actions — the same shape already used by
9
+ * `cli/lib/email.mjs`. The `type` values match the SDK `NextActionType` union.
10
+ *
11
+ * Use these helpers whenever the CLI itself authors next actions. Gateway/SDK
12
+ * `next_actions` remain the lower-layer source of truth and should pass through.
13
+ */
14
+
15
+ export function nextAction(type, fields = {}) {
16
+ const action = { type };
17
+ for (const key of ["command", "method", "path", "auth", "why"]) {
18
+ if (fields[key] !== undefined) action[key] = fields[key];
19
+ }
20
+ return action;
21
+ }
22
+
23
+ export function cliCommandAction(type, command, why) {
24
+ return nextAction(type, { command, why });
25
+ }
26
+
27
+ export function editRequestAction(command, why) {
28
+ return cliCommandAction("edit_request", command, why);
29
+ }
30
+
31
+ export function retryAction(command, why) {
32
+ return cliCommandAction("retry", command, why);
33
+ }
34
+
35
+ export function initializeWalletAction() {
36
+ return nextAction("initialize_wallet", {
37
+ command: "run402 init",
38
+ why: "Create and fund an agent allowance, then retry.",
39
+ });
40
+ }
41
+
42
+ export function createProjectAction() {
43
+ return nextAction("create_project", {
44
+ command: "run402 projects provision",
45
+ why: "Provision a project to act on, then retry.",
46
+ });
47
+ }
48
+
49
+ export function setTierAction(tier = "prototype") {
50
+ return nextAction("renew_tier", {
51
+ command: `run402 tier set ${tier}`,
52
+ why: "Subscribe the account to a tier (free on testnet), then retry.",
53
+ });
54
+ }
55
+
56
+ export function deployAction() {
57
+ return nextAction("deploy", {
58
+ command: "run402 deploy apply --manifest app.json",
59
+ why: "Apply your release manifest to deploy.",
60
+ });
61
+ }
package/lib/projects.mjs CHANGED
@@ -149,13 +149,16 @@ Examples:
149
149
  provision: `run402 projects provision — Provision a new Postgres project
150
150
 
151
151
  Usage:
152
- run402 projects provision [--tier <tier>] [--name <name>] [--org <id>]
152
+ run402 projects provision [--tier <tier>] [--name <name>] [--org <id>] [--idempotency-key <key>]
153
153
 
154
154
  Options:
155
155
  --tier <tier> Tier for the new project (default: prototype)
156
156
  --name <name> Human-readable name for the project
157
157
  --org <id> Provision into an EXISTING org (needs developer+ on it).
158
158
  Omit for the cold-start path. Tier is org-governed.
159
+ --idempotency-key <key> Retry-safe key: re-running with the same key returns
160
+ the existing project instead of duplicating it. Auto-derived
161
+ from --name when omitted; an unnamed provision stays un-keyed.
159
162
 
160
163
  Notes:
161
164
  - Payment is automatic via x402; requires a funded allowance
@@ -257,7 +260,7 @@ async function quote() {
257
260
  }
258
261
 
259
262
  async function provision(args) {
260
- const opts = { tier: "prototype", name: undefined, orgId: undefined };
263
+ const opts = { tier: "prototype", name: undefined, orgId: undefined, idempotencyKey: undefined };
261
264
  for (let i = 0; i < args.length; i++) {
262
265
  if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
263
266
  // Use !== undefined so an empty-string value is captured (and rejected
@@ -267,6 +270,16 @@ async function provision(args) {
267
270
  // Omitted = cold-start. Tier is org-governed, so --tier is irrelevant here
268
271
  // (the gateway ignores a client-supplied tier in all cases).
269
272
  if (args[i] === "--org" && args[i + 1] !== undefined) opts.orgId = args[++i];
273
+ // Retry-safety: re-running provision (the agent's natural mode after a
274
+ // crash/fresh session) must not duplicate-bill a project.
275
+ if (args[i] === "--idempotency-key" && args[i + 1] !== undefined) opts.idempotencyKey = args[++i];
276
+ }
277
+ // Auto-derive a stable key from --name when none was supplied: a named
278
+ // project is a stable intent, so re-running `provision --name X` collapses
279
+ // onto the same project. An explicit --idempotency-key always wins; an
280
+ // unnamed provision stays un-keyed (each call is a new project on purpose).
281
+ if (opts.idempotencyKey === undefined && opts.name) {
282
+ opts.idempotencyKey = `provision:${opts.name}`;
270
283
  }
271
284
  if (opts.orgId === "") {
272
285
  fail({
@@ -312,7 +325,7 @@ async function provision(args) {
312
325
  const activeBefore = getActiveProjectId();
313
326
  try {
314
327
  const data = await withAutoApprove(() =>
315
- getSdk().projects.provision({ tier: opts.tier, name: opts.name, orgId: opts.orgId }),
328
+ getSdk().projects.provision({ tier: opts.tier, name: opts.name, orgId: opts.orgId, idempotencyKey: opts.idempotencyKey }),
316
329
  );
317
330
  const activeAfter = getActiveProjectId();
318
331
  const out = { ...data };
package/lib/secrets.mjs CHANGED
@@ -2,6 +2,7 @@ import { readFileSync } from "fs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
  import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs, validateRegularFile } from "./argparse.mjs";
5
+ import { editRequestAction } from "./next-actions.mjs";
5
6
 
6
7
  const HELP = `run402 secrets — Manage project secrets
7
8
 
@@ -163,8 +164,8 @@ function failMissingStdin() {
163
164
  message: "Missing secret value on stdin.",
164
165
  hint: "Pipe a value, use --file <path>, or provide an inline value only when shell history exposure is acceptable.",
165
166
  next_actions: [
166
- "printf %s \"$VALUE\" | run402 secrets set <project> <KEY> --stdin",
167
- "run402 secrets set <project> <KEY> --file <path>",
167
+ editRequestAction("printf %s \"$VALUE\" | run402 secrets set <project> <KEY> --stdin", "Pipe the secret value through stdin."),
168
+ editRequestAction("run402 secrets set <project> <KEY> --file <path>", "Read the secret value from a local file."),
168
169
  ],
169
170
  });
170
171
  }
@@ -2,6 +2,7 @@ import { resolveProject, resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
  import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs } from "./argparse.mjs";
5
+ import { cliCommandAction } from "./next-actions.mjs";
5
6
 
6
7
  const HELP = `run402 subdomains — Manage custom subdomains
7
8
 
@@ -115,7 +116,7 @@ async function claim(args) {
115
116
  message: "no deployment_id specified and no recent deployment found.",
116
117
  hint: "Deploy a site first or pass --deployment <id>.",
117
118
  details: { project_id: projectId },
118
- next_actions: [{ action: "deploy_site_first" }],
119
+ next_actions: [cliCommandAction("deploy", "run402 deploy apply", "Deploy a site first, then retry claiming the subdomain.")],
119
120
  });
120
121
  }
121
122
  try {
package/lib/tier.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
2
  import { reportSdkError, fail } from "./sdk-errors.mjs";
3
- import { assertKnownFlags, normalizeArgv, positionalArgs } from "./argparse.mjs";
3
+ import { assertKnownFlags, normalizeArgv, positionalArgs, flagValue } from "./argparse.mjs";
4
+ import { setTierAction } from "./next-actions.mjs";
4
5
 
5
6
  const HELP = `run402 tier — Manage your Run402 tier subscription
6
7
 
@@ -50,11 +51,16 @@ Examples:
50
51
  set: `run402 tier set — Subscribe, renew, or upgrade your tier
51
52
 
52
53
  Usage:
53
- run402 tier set <tier>
54
+ run402 tier set <tier> [--idempotency-key <key>]
54
55
 
55
56
  Arguments:
56
57
  <tier> One of: prototype, hobby, team
57
58
 
59
+ Options:
60
+ --idempotency-key <key> Retry-safe key: re-running the same subscribe/renew
61
+ intent with this key does not double-charge. Use a
62
+ fresh key for a deliberate second renewal.
63
+
58
64
  Tiers:
59
65
  prototype $0.10/7d (free with testnet faucet)
60
66
  hobby $5/30d
@@ -101,8 +107,8 @@ async function status(args = []) {
101
107
 
102
108
  async function set(args = []) {
103
109
  const parsedArgs = normalizeArgv(args);
104
- assertKnownFlags(parsedArgs, ["--help", "-h"]);
105
- const positionals = positionalArgs(parsedArgs);
110
+ assertKnownFlags(parsedArgs, ["--help", "-h"], ["--idempotency-key"]);
111
+ const positionals = positionalArgs(parsedArgs, ["--idempotency-key"]);
106
112
  if (positionals.length > 1) {
107
113
  fail({
108
114
  code: "BAD_USAGE",
@@ -116,11 +122,16 @@ async function set(args = []) {
116
122
  code: "BAD_USAGE",
117
123
  message: "Missing <tier>.",
118
124
  hint: "run402 tier set <prototype|hobby|team>",
125
+ next_actions: [setTierAction()],
119
126
  });
120
127
  }
128
+ // Caller-supplied idempotency key makes a retried subscribe/renew safe from
129
+ // double-charge. Not auto-derived: the SDK cannot tell a retry from a new
130
+ // renewal intent (that boundary is the caller's).
131
+ const idempotencyKey = flagValue(parsedArgs, "--idempotency-key") ?? undefined;
121
132
  try {
122
133
  const sdk = getSdk();
123
- const data = await sdk.tier.set(tierName);
134
+ const data = await sdk.tier.set(tierName, idempotencyKey ? { idempotencyKey } : {});
124
135
  let statusAfter = null;
125
136
  try {
126
137
  statusAfter = await sdk.tier.status();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "3.5.2",
3
+ "version": "3.5.4",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,7 @@ export interface WalletIdentity {
46
46
  * Gateway write-auth (operator-approval) capabilities. Each is scoped to a
47
47
  * target: `org.project.create` → an org, the others → a project.
48
48
  */
49
- export type WriteAuthCapability = "org.project.create" | "project.deploy" | "project.secret.write";
49
+ export type WriteAuthCapability = "org.project.create" | "project.deploy" | "project.secret.write" | "project.archives.export";
50
50
  /** A write-capability target: an org (for `org.project.create`) or a project. */
51
51
  export interface WriteAuthTarget {
52
52
  org_id?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAC3B,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,CAAC;AAE3B,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,+BAA+B;IAC/B,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;OASG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAE1F;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEpD;;;OAGG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D,kGAAkG;IAClG,aAAa,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvE,oEAAoE;IACpE,aAAa,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C,kEAAkE;IAClE,gBAAgB,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7C,oEAAoE;IACpE,gBAAgB,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE5C,gFAAgF;IAChF,aAAa,CAAC,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAEhD,6CAA6C;IAC7C,aAAa,CAAC,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnD,oHAAoH;IACpH,eAAe,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAE3C,6FAA6F;IAC7F,gBAAgB,CAAC,IAAI,MAAM,CAAC;IAE5B;;;;OAIG;IACH,iBAAiB,CAAC,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACtD"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAC3B,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,GACtB,yBAAyB,CAAC;AAE9B,iFAAiF;AACjF,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,+BAA+B;IAC/B,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;OASG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAE1F;;;OAGG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEpD;;;OAGG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D,kGAAkG;IAClG,aAAa,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvE,oEAAoE;IACpE,aAAa,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C,kEAAkE;IAClE,gBAAgB,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7C,oEAAoE;IACpE,gBAAgB,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAE5C,gFAAgF;IAChF,aAAa,CAAC,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAEhD,6CAA6C;IAC7C,aAAa,CAAC,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnD,oHAAoH;IACpH,eAAe,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAE3C,6FAA6F;IAC7F,gBAAgB,CAAC,IAAI,MAAM,CAAC;IAE5B;;;;OAIG;IACH,iBAAiB,CAAC,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACtD"}
@@ -58,8 +58,8 @@ export declare abstract class Run402Error extends Error {
58
58
  readonly traceId?: string;
59
59
  /** Canonical structured context. Preserved by reference from the response body. */
60
60
  readonly details?: unknown;
61
- /** Advisory next actions from the gateway. Rendering them must not execute them. */
62
- readonly nextActions?: unknown[];
61
+ /** Advisory next actions (gateway-authored or SDK-synthesized). Rendering them must not execute them. */
62
+ readonly nextActions?: NextAction[];
63
63
  /**
64
64
  * Quota-denial scope (v1.46+). `"organization"` for pooled organization
65
65
  * denials; `"project"` for the orphan fallback (project whose organization
@@ -252,6 +252,29 @@ export declare class TransferFreezeError extends Run402Error {
252
252
  readonly projectId: string | null;
253
253
  constructor(message: string, status: number, body: unknown, context: string);
254
254
  }
255
+ /**
256
+ * Known `type` values for a {@link NextAction}. The gateway set (style.md
257
+ * §Errors) extended with the client-side bootstrap verbs `create_project` and
258
+ * `initialize_wallet`, plus `operator_approve` (synthesized for WRITE_AUTH).
259
+ * Tolerates unknown future gateway types via the `(string & {})` fallback.
260
+ */
261
+ export type NextActionType = "retry" | "authenticate" | "submit_payment" | "renew_tier" | "check_usage" | "resume_deploy" | "edit_request" | "edit_migration" | "create_project" | "initialize_wallet" | "deploy" | "operator_approve" | "contact_support";
262
+ /**
263
+ * A single advisory "what to do next" entry. Mirrors the gateway's
264
+ * `next_actions[]` shape (`{ type, method?, path?, auth?, why? }`) extended with
265
+ * `command` — the literal CLI invocation — for client-side, CLI-resolvable
266
+ * actions. Rendering an action must never execute it.
267
+ */
268
+ export interface NextAction {
269
+ type: NextActionType | (string & {});
270
+ /** Literal CLI invocation for client-side, CLI-resolvable actions. */
271
+ command?: string;
272
+ method?: string;
273
+ path?: string;
274
+ auth?: string;
275
+ why?: string;
276
+ [key: string]: unknown;
277
+ }
255
278
  /**
256
279
  * HTTP 403 `STEP_UP_REQUIRED` — the gateway requires a fresh, same-client
257
280
  * step-up (a recent `passkey` AMR) before this high-stakes control-plane