run402 3.5.3 → 3.5.5

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 (58) hide show
  1. package/cli.mjs +20 -0
  2. package/core-dist/config.js +86 -4
  3. package/lib/archives.mjs +55 -0
  4. package/lib/cache.mjs +10 -9
  5. package/lib/cloud.mjs +254 -0
  6. package/lib/config.mjs +27 -2
  7. package/lib/core.mjs +122 -0
  8. package/lib/deploy-v2.mjs +95 -67
  9. package/lib/doctor-source-scan.mjs +1 -1
  10. package/lib/functions.mjs +2 -1
  11. package/lib/init.mjs +106 -1
  12. package/lib/next-actions.mjs +30 -15
  13. package/lib/projects.mjs +13 -6
  14. package/lib/secrets.mjs +3 -2
  15. package/lib/status.mjs +25 -3
  16. package/lib/subdomains.mjs +2 -1
  17. package/package.json +1 -1
  18. package/sdk/core-dist/config.js +86 -4
  19. package/sdk/dist/credentials.d.ts +1 -1
  20. package/sdk/dist/credentials.d.ts.map +1 -1
  21. package/sdk/dist/index.d.ts +4 -0
  22. package/sdk/dist/index.d.ts.map +1 -1
  23. package/sdk/dist/index.js +4 -0
  24. package/sdk/dist/index.js.map +1 -1
  25. package/sdk/dist/namespaces/archives.d.ts +12 -0
  26. package/sdk/dist/namespaces/archives.d.ts.map +1 -0
  27. package/sdk/dist/namespaces/archives.js +202 -0
  28. package/sdk/dist/namespaces/archives.js.map +1 -0
  29. package/sdk/dist/namespaces/archives.types.d.ts +163 -0
  30. package/sdk/dist/namespaces/archives.types.d.ts.map +1 -0
  31. package/sdk/dist/namespaces/archives.types.js +2 -0
  32. package/sdk/dist/namespaces/archives.types.js.map +1 -0
  33. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  34. package/sdk/dist/namespaces/deploy.js +173 -15
  35. package/sdk/dist/namespaces/deploy.js.map +1 -1
  36. package/sdk/dist/namespaces/deploy.types.d.ts +16 -1
  37. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  39. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  40. package/sdk/dist/namespaces/projects.js +1 -0
  41. package/sdk/dist/namespaces/projects.js.map +1 -1
  42. package/sdk/dist/namespaces/projects.types.d.ts +7 -0
  43. package/sdk/dist/namespaces/projects.types.d.ts.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/deploy-manifest.d.ts.map +1 -1
  49. package/sdk/dist/node/deploy-manifest.js +15 -2
  50. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  51. package/sdk/dist/node/index.d.ts +4 -1
  52. package/sdk/dist/node/index.d.ts.map +1 -1
  53. package/sdk/dist/node/index.js +3 -0
  54. package/sdk/dist/node/index.js.map +1 -1
  55. package/sdk/dist/scoped.d.ts +12 -0
  56. package/sdk/dist/scoped.d.ts.map +1 -1
  57. package/sdk/dist/scoped.js +25 -0
  58. package/sdk/dist/scoped.js.map +1 -1
package/lib/core.mjs ADDED
@@ -0,0 +1,122 @@
1
+ import { importArchiveToCore } from "#sdk/node";
2
+ import { fail, reportSdkError } from "./sdk-errors.mjs";
3
+ import { assertKnownFlags, flagValue, hasHelp, normalizeArgv, positionalArgs } from "./argparse.mjs";
4
+
5
+ const HELP = `run402 core — Local Run402 Core commands
6
+
7
+ Usage:
8
+ run402 core projects import <archive-path> --name <project-name> [options]
9
+
10
+ Options:
11
+ --name <name> New Core project name (default imported-project)
12
+ --env-file <path> Env file satisfying required archive secrets.
13
+ --secret KEY=VALUE Inline secret value; repeatable. Overrides --env-file.
14
+ --core-url <url> Core gateway URL (default RUN402_CORE_URL or http://127.0.0.1:4020)
15
+ --dry-run Verify and plan without creating a Core project.
16
+ --require-runnable Block import unless required secrets are supplied.
17
+ --json Emit final JSON.
18
+ --json-stream Emit NDJSON progress events and final result event.
19
+ `;
20
+
21
+ const FLAG_VALUES = ["--name", "--env-file", "--secret", "--core-url"];
22
+ const FLAGS = new Set([...FLAG_VALUES, "--dry-run", "--require-runnable", "--json", "--json-stream", "--help", "-h"]);
23
+
24
+ export async function run(sub, args = []) {
25
+ const all = [sub, ...args].filter(Boolean);
26
+ if (hasHelp(all)) {
27
+ console.log(HELP);
28
+ return;
29
+ }
30
+ if (sub !== "projects") {
31
+ fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> [options]" });
32
+ }
33
+ const action = args[0];
34
+ if (action === "import") return importProject(args.slice(1));
35
+ fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> [options]" });
36
+ }
37
+
38
+ async function importProject(rawArgs) {
39
+ const args = normalizeArgv(rawArgs);
40
+ assertKnownFlags(args, [...FLAGS], FLAG_VALUES);
41
+ const archivePath = positionalArgs(args, FLAG_VALUES)[0];
42
+ if (!archivePath) {
43
+ fail({ code: "BAD_USAGE", message: "Usage: run402 core projects import <archive-path> --name <project-name> [options]" });
44
+ }
45
+ const jsonStream = args.includes("--json-stream");
46
+ const secretValues = parseSecrets(args);
47
+ const name = flagValue(args, "--name") ?? "imported-project";
48
+ const coreUrl = flagValue(args, "--core-url") ?? undefined;
49
+ const startedEvent = {
50
+ event: "core_archive_import_started",
51
+ stage: "verify",
52
+ resource_type: "project_archive",
53
+ resource_id: archivePath,
54
+ status: "running",
55
+ completed_units: 0,
56
+ total_units: 1,
57
+ code: null,
58
+ message: "Verifying archive locally before Core import.",
59
+ next_action: { type: "none" },
60
+ retryable: false,
61
+ context: { archive_path: archivePath, core_url: coreUrl ?? null, project_name: name },
62
+ };
63
+ if (jsonStream) console.log(JSON.stringify(startedEvent));
64
+ try {
65
+ const result = await importArchiveToCore({
66
+ archivePath,
67
+ name,
68
+ coreUrl,
69
+ envFile: flagValue(args, "--env-file") ?? undefined,
70
+ secretValues,
71
+ dryRun: args.includes("--dry-run"),
72
+ requireRunnable: args.includes("--require-runnable"),
73
+ });
74
+ if (jsonStream) {
75
+ console.log(JSON.stringify({
76
+ event: "core_archive_import_complete",
77
+ stage: "complete",
78
+ resource_type: "project_archive",
79
+ resource_id: archivePath,
80
+ status: result.status,
81
+ completed_units: result.status === "imported" || result.status === "dry_run" ? 1 : 0,
82
+ total_units: 1,
83
+ code: firstDiagnosticCode(result),
84
+ message: `Core archive import status: ${result.status}`,
85
+ next_action: result.next_action,
86
+ retryable: result.status === "failed" && result.diagnostics.some((d) => d.retryable),
87
+ result,
88
+ }));
89
+ } else {
90
+ console.log(JSON.stringify({ ok: result.status === "imported" || result.status === "dry_run", import: result }, null, 2));
91
+ }
92
+ if (result.status !== "imported" && result.status !== "dry_run") process.exit(1);
93
+ } catch (err) {
94
+ reportSdkError(err);
95
+ }
96
+ }
97
+
98
+ function parseSecrets(args) {
99
+ const out = {};
100
+ for (let i = 0; i < args.length; i += 1) {
101
+ if (args[i] !== "--secret") continue;
102
+ const value = args[i + 1];
103
+ if (!value || value.startsWith("--")) {
104
+ fail({ code: "BAD_FLAG", message: "--secret requires KEY=VALUE" });
105
+ }
106
+ const eq = value.indexOf("=");
107
+ if (eq <= 0) {
108
+ fail({ code: "BAD_FLAG", message: "--secret requires KEY=VALUE", details: { value } });
109
+ }
110
+ const key = value.slice(0, eq);
111
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
112
+ fail({ code: "BAD_FLAG", message: `Invalid secret env var name: ${key}` });
113
+ }
114
+ out[key] = value.slice(eq + 1);
115
+ i += 1;
116
+ }
117
+ return out;
118
+ }
119
+
120
+ function firstDiagnosticCode(result) {
121
+ return result.diagnostics.find((d) => d.severity === "blocking")?.code ?? null;
122
+ }
package/lib/deploy-v2.mjs CHANGED
@@ -32,10 +32,11 @@ import {
32
32
  } from "#sdk/node";
33
33
  import { getSdk } from "./sdk.mjs";
34
34
  import { reportSdkError, fail } from "./sdk-errors.mjs";
35
- import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId } from "./config.mjs";
35
+ import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId, isCoreApiTarget } from "./config.mjs";
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
 
@@ -123,6 +124,11 @@ Routes:
123
124
  Routes activate atomically with the release. Direct /functions/v1/:name remains API-key protected.
124
125
  Runtime route failure codes: ROUTE_MANIFEST_LOAD_FAILED, ROUTED_INVOKE_WORKER_SECRET_MISSING, ROUTED_INVOKE_AUTH_FAILED, ROUTED_ROUTE_STALE, ROUTE_METHOD_NOT_ALLOWED, ROUTED_RESPONSE_TOO_LARGE.
125
126
 
127
+ Function capabilities:
128
+ functions.replace.<name>.capabilities is an array of runtime capability strings.
129
+ Framework adapters use it for contracts such as "astro.ssr.v1"; omit it for
130
+ ordinary user-authored functions unless a documented helper requires it.
131
+
126
132
  Internationalization (routed functions):
127
133
  "i18n": { "defaultLocale": "en", "locales": ["en", "es", "fr"], "detect": ["cookie:wl_locale", "accept-language"] }
128
134
  Omit i18n to carry forward from base release; pass "i18n": null to clear the slice on the new release.
@@ -663,7 +669,16 @@ async function mergeAstroReleaseSlice(spec, dirArg) {
663
669
  * the manifest normalizer is the authority on shape validity, not this
664
670
  * best-effort extractor.
665
671
  */
666
- function collectManifestSourceFiles(spec, baseDir) {
672
+ function isAstroSsrManifestFunction(entry) {
673
+ return (
674
+ entry &&
675
+ typeof entry === "object" &&
676
+ Array.isArray(entry.capabilities) &&
677
+ entry.capabilities.includes("astro.ssr.v1")
678
+ );
679
+ }
680
+
681
+ export function collectManifestSourceFiles(spec, baseDir) {
667
682
  const out = new Set();
668
683
  if (!spec || typeof spec !== "object") return [];
669
684
 
@@ -688,13 +703,20 @@ function collectManifestSourceFiles(spec, baseDir) {
688
703
  if (!map || typeof map !== "object") return;
689
704
  for (const entry of Object.values(map)) resolveEntryPath(entry);
690
705
  };
706
+ const eachFunctionValue = (map) => {
707
+ if (!map || typeof map !== "object") return;
708
+ for (const entry of Object.values(map)) {
709
+ if (isAstroSsrManifestFunction(entry)) continue;
710
+ resolveEntryPath(entry);
711
+ }
712
+ };
691
713
 
692
714
  const fns = spec.functions;
693
715
  if (fns && typeof fns === "object") {
694
- eachValue(fns.replace);
716
+ eachFunctionValue(fns.replace);
695
717
  if (fns.patch && typeof fns.patch === "object") {
696
- eachValue(fns.patch.put);
697
- eachValue(fns.patch.set);
718
+ eachFunctionValue(fns.patch.put);
719
+ eachFunctionValue(fns.patch.set);
698
720
  }
699
721
  }
700
722
 
@@ -895,9 +917,10 @@ async function applyCmd(args) {
895
917
  credentials: githubActionsCredentials({ projectId: releaseSpec.project, apiBase: API }),
896
918
  disablePaidFetch: true,
897
919
  };
898
- } else if (!loadLiveControlPlaneSession()) {
920
+ } else if (!isCoreApiTarget() && !loadLiveControlPlaneSession()) {
899
921
  // Aggressive early exit when no allowance is configured — unless a
900
- // wallet-less human is deploying via their operator (control-plane) session.
922
+ // wallet-less human is deploying via their operator (control-plane) session
923
+ // or the active target is a self-hosted Core Gateway.
901
924
  allowanceAuthHeaders("/apply/v1/plans");
902
925
  }
903
926
 
@@ -908,6 +931,7 @@ async function applyCmd(args) {
908
931
  idempotencyKey,
909
932
  allowWarnings: opts.allowWarnings,
910
933
  allowWarningCodes: opts.allowWarningCodes,
934
+ target: isCoreApiTarget() ? "core" : "cloud",
911
935
  }),
912
936
  );
913
937
  console.log(JSON.stringify(result, null, 2));
@@ -939,66 +963,66 @@ const CI_DEPLOY_ERROR_GUIDANCE = {
939
963
  invalid_token: {
940
964
  hint: "Ensure the workflow has permissions: id-token: write and is running in the repository/branch linked with run402 ci link github.",
941
965
  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.",
966
+ nextAction("edit_request", { why: "Check the workflow permissions block includes id-token: write." }),
967
+ editRequestAction("run402 ci link github", "Re-link if the repository, branch, or environment changed."),
944
968
  ],
945
969
  },
946
970
  access_denied: {
947
971
  hint: "The OIDC token was valid, but no active Run402 CI binding allowed this workflow.",
948
972
  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.",
973
+ editRequestAction("run402 ci list --project <prj_...>", "Inspect active CI bindings for this project."),
974
+ editRequestAction("run402 ci link github", "Link this repository, branch, and environment to Run402."),
951
975
  ],
952
976
  },
953
977
  binding_revoked: {
954
978
  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
979
  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.",
980
+ editRequestAction("run402 ci link github", "Re-create the CI binding from this repository."),
981
+ nextAction("edit_request", { why: "Do not run run402 ci set-asset-scopes; it returns 409 on a revoked binding." }),
982
+ editRequestAction("run402 ci list --project <prj_...>", "Confirm the binding state locally."),
959
983
  ],
960
984
  },
961
985
  event_not_allowed: {
962
986
  hint: "This binding only allows push and workflow_dispatch events in v1.",
963
987
  next_actions: [
964
- "Trigger the workflow with push or workflow_dispatch.",
965
- "Create a separate follow-up design before enabling PR deploy events.",
988
+ nextAction("retry", { why: "Trigger the workflow with push or workflow_dispatch." }),
989
+ nextAction("edit_request", { why: "Create a separate follow-up design before enabling PR deploy events." }),
966
990
  ],
967
991
  },
968
992
  repository_id_mismatch: {
969
993
  hint: "The GitHub repository id in the OIDC token does not match the linked binding.",
970
994
  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.",
995
+ editRequestAction("run402 ci link github", "Re-link from the current repository."),
996
+ editRequestAction("run402 ci link github --repository-id <id>", "Pass the numeric GitHub repository id if automatic lookup fails."),
973
997
  ],
974
998
  },
975
999
  forbidden_spec_field: {
976
1000
  hint: "CI deploys can deploy site/functions/database content and route declarations only when the binding includes covering route scopes.",
977
1001
  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.",
1002
+ nextAction("edit_request", { why: "Remove forbidden fields such as secrets, subdomains, or checks from the CI manifest." }),
1003
+ nextAction("edit_request", { why: "Keep the normalized manifest small enough to avoid manifest_ref." }),
980
1004
  ],
981
1005
  },
982
1006
  CI_ROUTE_SCOPE_DENIED: {
983
1007
  hint: "This CI binding does not cover one or more route declarations in the deploy manifest.",
984
1008
  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.",
1009
+ editRequestAction("run402 ci link github --route-scope <pattern>", "Add a CI route scope for every exact or prefix path the workflow may deploy."),
1010
+ nextAction("edit_request", { why: "Use exact scopes such as --route-scope /admin or prefix scopes such as --route-scope /api/*." }),
1011
+ editRequestAction("run402 deploy apply", "Run route changes locally when they are outside the CI delegation."),
988
1012
  ],
989
1013
  },
990
1014
  forbidden_plan: {
991
1015
  hint: "The gateway rejected this deploy plan for CI. Keep CI deploys to the allowed resources and re-link if policy changed.",
992
1016
  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.",
1017
+ nextAction("edit_request", { why: "Inspect the gateway error details for the rejected resource." }),
1018
+ editRequestAction("run402 deploy apply", "Run operations outside the CI allowlist locally."),
995
1019
  ],
996
1020
  },
997
1021
  payment_required: {
998
1022
  hint: "The project tier or payment state does not allow this CI deploy.",
999
1023
  next_actions: [
1000
- "Run run402 tier status --project <prj_...> locally.",
1001
- "Renew or upgrade the project tier, then re-run the workflow.",
1024
+ editRequestAction("run402 tier status --project <prj_...>", "Inspect project tier and payment state locally."),
1025
+ editRequestAction("run402 tier set <tier>", "Renew or upgrade the project tier, then re-run the workflow."),
1002
1026
  ],
1003
1027
  },
1004
1028
  };
@@ -1032,18 +1056,22 @@ function enhanceDeployWarningError(err) {
1032
1056
  : code
1033
1057
  ? [code]
1034
1058
  : [];
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.";
1059
+ const allowWarningCommand = unacknowledgedCodes.length > 0
1060
+ ? `run402 deploy apply ${unacknowledgedCodes.map((warningCode) => `--allow-warning ${warningCode}`).join(" ")}`
1061
+ : "run402 deploy apply --allow-warning <code>";
1062
+ const allowWarningAction = editRequestAction(
1063
+ allowWarningCommand,
1064
+ unacknowledgedCodes.length > 0
1065
+ ? `Retry only after reviewing warning code${unacknowledgedCodes.length === 1 ? "" : "s"}: ${unacknowledgedCodes.join(", ")}.`
1066
+ : "Retry only after reviewing the warning code.",
1067
+ );
1040
1068
  const defaultNextActions = [
1041
1069
  ...(affected.length > 0
1042
- ? [`Set or inspect affected secrets: ${Array.from(new Set(affected)).join(", ")}`]
1070
+ ? [editRequestAction("run402 secrets set <project> <KEY> --stdin", `Set or inspect affected secrets: ${Array.from(new Set(affected)).join(", ")}`)]
1043
1071
  : []),
1044
- "Retry `run402 deploy apply` after resolving warnings.",
1072
+ retryAction("run402 deploy apply", "Retry after resolving warnings."),
1045
1073
  allowWarningAction,
1046
- "Use `--allow-warnings` only when every warning was explicitly reviewed.",
1074
+ editRequestAction("run402 deploy apply --allow-warnings", "Use only when every warning was explicitly reviewed."),
1047
1075
  ];
1048
1076
  enhanced.body = {
1049
1077
  ...existingBody,
@@ -1066,98 +1094,98 @@ const ROUTE_WARNING_GUIDANCE = {
1066
1094
  PUBLIC_ROUTED_FUNCTION: {
1067
1095
  hint: "A deploy route makes a function public same-origin browser ingress; direct /functions/v1/:name remains API-key protected.",
1068
1096
  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.",
1097
+ nextAction("edit_request", { why: "Review application auth and authorization in the routed function." }),
1098
+ nextAction("edit_request", { why: "Add CSRF protection for cookie-authenticated POST/PUT/PATCH/DELETE routes." }),
1099
+ nextAction("edit_request", { why: "Implement CORS and OPTIONS explicitly when cross-origin callers are intended." }),
1100
+ retryAction("run402 deploy apply --allow-warnings", "Retry only after the public ingress review is intentional."),
1073
1101
  ],
1074
1102
  },
1075
1103
  ROUTE_TARGET_CARRIED_FORWARD: {
1076
1104
  hint: "A carried-forward route still points at a base-release function target.",
1077
1105
  next_actions: [
1078
- "Inspect the active release with run402 deploy release active.",
1079
- "Deploy routes.replace if the target should change in this release.",
1106
+ editRequestAction("run402 deploy release active", "Inspect the active release."),
1107
+ editRequestAction("run402 deploy apply --manifest <path>", "Deploy routes.replace if the target should change in this release."),
1080
1108
  ],
1081
1109
  },
1082
1110
  ROUTE_SHADOWS_STATIC_PATH: {
1083
1111
  hint: "A dynamic route shadows a static site path.",
1084
1112
  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.",
1113
+ nextAction("edit_request", { why: "Inspect the warning details for affected static paths." }),
1114
+ editRequestAction("run402 deploy release active", "Inspect live routes."),
1115
+ retryAction("run402 deploy apply --allow-warnings", "Retry only when dynamic shadowing is intentional."),
1088
1116
  ],
1089
1117
  },
1090
1118
  WILDCARD_ROUTE_SHADOWS_STATIC_PATHS: {
1091
1119
  hint: "A prefix wildcard route shadows one or more static site paths.",
1092
1120
  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.",
1121
+ nextAction("edit_request", { why: "Review affected route/static path details." }),
1122
+ nextAction("edit_request", { why: "Split exact routes or move static paths if the shadowing is accidental." }),
1123
+ retryAction("run402 deploy apply --allow-warnings", "Retry only when the wildcard shadowing is intentional."),
1096
1124
  ],
1097
1125
  },
1098
1126
  METHOD_SPECIFIC_ROUTE_ALLOWS_GET_STATIC_FALLBACK: {
1099
1127
  hint: "A method-specific route allows static fallback for unmatched methods such as GET.",
1100
1128
  next_actions: [
1101
- "Confirm that static fallback for GET/HEAD is intended.",
1102
- "Add method coverage or static files deliberately.",
1129
+ nextAction("edit_request", { why: "Confirm that static fallback for GET/HEAD is intended." }),
1130
+ nextAction("edit_request", { why: "Add method coverage or static files deliberately." }),
1103
1131
  ],
1104
1132
  },
1105
1133
  WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS: {
1106
1134
  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
1135
  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.",
1136
+ nextAction("edit_request", { why: "Add the mutation methods the routed function supports, such as POST." }),
1137
+ nextAction("edit_request", { why: "Omit methods to allow every supported method when the route is an API surface." }),
1138
+ nextAction("edit_request", { why: "Set acknowledge_readonly: true on an intentionally read-only GET/HEAD wildcard function route." }),
1139
+ retryAction("run402 deploy apply --allow-warning WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODS", "Use only as a reviewed CLI escape hatch."),
1112
1140
  ],
1113
1141
  },
1114
1142
  ROUTE_TABLE_NEAR_LIMIT: {
1115
1143
  hint: "The route table is near the gateway/project limit.",
1116
1144
  next_actions: [
1117
- "Consolidate prefix routes where possible.",
1118
- "Remove stale route entries before adding more.",
1145
+ nextAction("edit_request", { why: "Consolidate prefix routes where possible." }),
1146
+ nextAction("edit_request", { why: "Remove stale route entries before adding more." }),
1119
1147
  ],
1120
1148
  },
1121
1149
  ROUTES_NOT_ENABLED: {
1122
1150
  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
1151
  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.",
1152
+ nextAction("edit_request", { why: "Deploy without the routes resource, or request route enablement for this project/environment." }),
1153
+ nextAction("edit_request", { why: "Keep direct function invocation API-key protected; do not substitute it for same-origin browser routes." }),
1126
1154
  ],
1127
1155
  },
1128
1156
  STATIC_ALIAS_SHADOWS_STATIC_PATH: {
1129
1157
  hint: "A static route target shadows a direct static path at the same public URL.",
1130
1158
  next_actions: [
1131
- "Inspect the route pattern, target.file, and direct static path.",
1132
- "Confirm only when the static route target is intentional.",
1159
+ nextAction("edit_request", { why: "Inspect the route pattern, target.file, and direct static path." }),
1160
+ nextAction("edit_request", { why: "Confirm only when the static route target is intentional." }),
1133
1161
  ],
1134
1162
  },
1135
1163
  STATIC_ALIAS_RELATIVE_ASSET_RISK: {
1136
1164
  hint: "Relative asset URLs inside the target HTML may resolve differently at the static route target URL.",
1137
1165
  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.",
1166
+ nextAction("edit_request", { why: "Inspect the target HTML for relative asset references." }),
1167
+ nextAction("edit_request", { why: "Use absolute asset URLs or confirm only when the alternate URL is intentional." }),
1140
1168
  ],
1141
1169
  },
1142
1170
  STATIC_ALIAS_DUPLICATE_CANONICAL_URL: {
1143
1171
  hint: "Both the static route target URL and the target file URL may be publicly reachable.",
1144
1172
  next_actions: [
1145
- "Decide which URL should be canonical.",
1146
- "Update links/canonical tags or accept the duplicate public URL intentionally.",
1173
+ nextAction("edit_request", { why: "Decide which URL should be canonical." }),
1174
+ nextAction("edit_request", { why: "Update links/canonical tags or accept the duplicate public URL intentionally." }),
1147
1175
  ],
1148
1176
  },
1149
1177
  STATIC_ALIAS_EXTENSIONLESS_NON_HTML: {
1150
1178
  hint: "An extensionless static route target points at a non-HTML file.",
1151
1179
  next_actions: [
1152
- "Check that the extensionless route is meant to serve that content type.",
1153
- "Prefer extensionless static route targets for HTML pages.",
1180
+ nextAction("edit_request", { why: "Check that the extensionless route is meant to serve that content type." }),
1181
+ nextAction("edit_request", { why: "Prefer extensionless static route targets for HTML pages." }),
1154
1182
  ],
1155
1183
  },
1156
1184
  STATIC_ALIAS_TABLE_NEAR_LIMIT: {
1157
1185
  hint: "Static route targets count toward the route table limit.",
1158
1186
  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.",
1187
+ nextAction("edit_request", { why: "Consolidate manual static route targets where possible." }),
1188
+ nextAction("edit_request", { why: "Avoid one route entry per page for large sites until framework-scale Web Output support exists." }),
1161
1189
  ],
1162
1190
  },
1163
1191
  };
@@ -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,4 +1,4 @@
1
- import { readAllowance, saveAllowance, loadKeyStore, configDir } from "./config.mjs";
1
+ import { readAllowance, saveAllowance, loadKeyStore, configDir, configureApiBase } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { fail } from "./sdk-errors.mjs";
4
4
  import { setTierAction, deployAction } from "./next-actions.mjs";
@@ -15,6 +15,9 @@ const HELP = `run402 init — Set up allowance, funding, and check tier status
15
15
 
16
16
  Usage:
17
17
  run402 init Set up with x402 (Base Sepolia) — default
18
+ run402 init --api-base <url>
19
+ Configure a Run402 Core/API target for the active
20
+ profile without setting up Cloud payment.
18
21
  run402 init mpp Set up with MPP (Tempo Moderato)
19
22
  run402 init <rail> --switch-rail
20
23
  Switch the persisted payment rail to <rail>.
@@ -23,6 +26,9 @@ Usage:
23
26
  silently flipping billing networks.
24
27
 
25
28
  Options:
29
+ --api-base <url> Configure the active profile to use this API base. Use this
30
+ for a self-hosted Run402 Core Gateway, e.g.
31
+ http://my-core:4020.
26
32
  --switch-rail Confirm switching the persisted payment rail. Re-running
27
33
  init with the SAME rail as the existing allowance is always
28
34
  idempotent and does not need this flag.
@@ -46,6 +52,61 @@ Run this once to get started, or again to check your setup.
46
52
 
47
53
  function short(addr) { return addr.slice(0, 6) + "..." + addr.slice(-4); }
48
54
 
55
+ function parseApiBaseFlag(args) {
56
+ for (let i = 0; i < args.length; i++) {
57
+ const arg = args[i];
58
+ if (arg === "--api-base") {
59
+ const value = args[i + 1];
60
+ if (value === undefined || String(value).startsWith("--")) {
61
+ fail({
62
+ code: "BAD_USAGE",
63
+ message: "--api-base requires a value.",
64
+ details: { flag: "--api-base" },
65
+ });
66
+ }
67
+ return { value, args: [...args.slice(0, i), ...args.slice(i + 2)] };
68
+ }
69
+ if (typeof arg === "string" && arg.startsWith("--api-base=")) {
70
+ const value = arg.slice("--api-base=".length);
71
+ if (!value) {
72
+ fail({
73
+ code: "BAD_USAGE",
74
+ message: "--api-base requires a non-empty value.",
75
+ details: { flag: "--api-base" },
76
+ });
77
+ }
78
+ return { value, args: [...args.slice(0, i), ...args.slice(i + 1)] };
79
+ }
80
+ }
81
+ return { value: null, args };
82
+ }
83
+
84
+ function sameOrigin(a, b) {
85
+ try {
86
+ return new URL(a).origin === new URL(b).origin;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ async function detectTarget(apiBase) {
93
+ try {
94
+ const health = await getSdk({ apiBase, disablePaidFetch: true, authMode: "none" }).service.health();
95
+ const kind = health && typeof health === "object" && health.mode === "core"
96
+ ? "core"
97
+ : sameOrigin(apiBase, "https://api.run402.com") ? "cloud" : "core";
98
+ return {
99
+ kind,
100
+ health_status: typeof health?.status === "string" ? health.status : "ok",
101
+ };
102
+ } catch (err) {
103
+ return {
104
+ kind: sameOrigin(apiBase, "https://api.run402.com") ? "cloud" : "core",
105
+ health_error: errorMessage(err),
106
+ };
107
+ }
108
+ }
109
+
49
110
  function errorMessage(err) {
50
111
  if (err?.body && typeof err.body === "object") return err.body.message || err.body.error || err.message;
51
112
  return err?.message || String(err);
@@ -67,6 +128,50 @@ export async function run(args = []) {
67
128
 
68
129
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
69
130
 
131
+ const parsedApiBase = parseApiBaseFlag(args);
132
+ if (parsedApiBase.value) {
133
+ if (parsedApiBase.args.some((arg) => typeof arg === "string" && !arg.startsWith("--"))) {
134
+ fail({
135
+ code: "BAD_USAGE",
136
+ message: "run402 init --api-base cannot be combined with a payment rail.",
137
+ hint: "Run `run402 init --api-base=http://my-core:4020` for Core, or `run402 init` for Run402 Cloud.",
138
+ });
139
+ }
140
+ const CONFIG_DIR = configDir();
141
+ const detected = await detectTarget(parsedApiBase.value);
142
+ const config = configureApiBase(parsedApiBase.value, {
143
+ target_kind: detected.kind,
144
+ ...(detected.health_status ? { health_status: detected.health_status } : {}),
145
+ ...(detected.health_error ? { health_error: detected.health_error } : {}),
146
+ });
147
+ mkdirSync(CONFIG_DIR, { recursive: true });
148
+ console.error("");
149
+ console.error(` ${"Config".padEnd(10)} ${CONFIG_DIR}`);
150
+ console.error(` ${"API base".padEnd(10)} ${config.api_base}`);
151
+ console.error(` ${"Target".padEnd(10)} ${config.target_kind}`);
152
+ if (detected.health_status) console.error(` ${"Health".padEnd(10)} ${detected.health_status}`);
153
+ if (detected.health_error) console.error(` ${"Health".padEnd(10)} ${detected.health_error}`);
154
+ console.error("");
155
+ const summary = {
156
+ config_dir: CONFIG_DIR,
157
+ api_base: config.api_base,
158
+ api_base_source: "profile",
159
+ target: {
160
+ kind: config.target_kind,
161
+ ...(config.health_status ? { health_status: config.health_status } : {}),
162
+ ...(config.health_error ? { health_error: config.health_error } : {}),
163
+ },
164
+ payment_required: config.target_kind === "cloud",
165
+ next_actions: [{
166
+ type: "create_project",
167
+ command: 'run402 projects provision --name "my-app"',
168
+ }],
169
+ next_step: 'run402 projects provision --name "my-app"',
170
+ };
171
+ console.log(JSON.stringify(summary, null, 2));
172
+ return;
173
+ }
174
+
70
175
  // Resolve once for this invocation — reflects the active wallet/profile that
71
176
  // cli.mjs published to RUN402_WALLET before this module loaded.
72
177
  const CONFIG_DIR = configDir();