run402 4.86.1 → 4.88.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.
- package/README.md +1 -1
- package/gitvault-surface.json +1 -1
- package/lib/deploy-v2.mjs +15 -1
- package/lib/image.mjs +57 -3
- package/lib/init.mjs +10 -4
- package/lib/next-actions.mjs +13 -0
- package/lib/status.mjs +97 -4
- package/lib/subdomains.mjs +36 -27
- package/lib/up.mjs +56 -16
- package/package.json +1 -1
- package/sdk/dist/credentials.d.ts +8 -0
- package/sdk/dist/credentials.d.ts.map +1 -1
- package/sdk/dist/errors.d.ts +1 -1
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/namespaces/ai.d.ts +18 -4
- package/sdk/dist/namespaces/ai.d.ts.map +1 -1
- package/sdk/dist/namespaces/ai.js +129 -9
- package/sdk/dist/namespaces/ai.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +45 -5
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +34 -0
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/namespaces/subdomains.d.ts +17 -4
- package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
- package/sdk/dist/namespaces/subdomains.js +14 -5
- package/sdk/dist/namespaces/subdomains.js.map +1 -1
- package/sdk/dist/node/actions-node.d.ts +13 -0
- package/sdk/dist/node/actions-node.d.ts.map +1 -1
- package/sdk/dist/node/actions-node.js +38 -0
- package/sdk/dist/node/actions-node.js.map +1 -1
- package/sdk/dist/node/credentials.d.ts +8 -0
- package/sdk/dist/node/credentials.d.ts.map +1 -1
- package/sdk/dist/node/credentials.js +13 -1
- package/sdk/dist/node/credentials.js.map +1 -1
- package/sdk/dist/node/deploy-manifest.d.ts +9 -1
- package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
- package/sdk/dist/node/deploy-manifest.js +26 -4
- package/sdk/dist/node/deploy-manifest.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +22 -0
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +29 -1
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.js +12 -4
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
- package/sdk/dist/type-contract.d.ts.map +1 -1
- package/sdk/dist/type-contract.js +5 -0
- package/sdk/dist/type-contract.js.map +1 -1
package/README.md
CHANGED
|
@@ -249,7 +249,7 @@ import { db, adminDb, auth, email, ai } from "@run402/functions";
|
|
|
249
249
|
|
|
250
250
|
### Same-origin web routes
|
|
251
251
|
|
|
252
|
-
`run402 deploy apply` accepts `site.public_paths` for clean static browser URLs and `routes.replace` for function ingress or exact method-aware static aliases. Release asset paths such as `events.html` are distinct from public paths such as `/events`: prefer `{ "site": { "replace": { "events.html": { "data": "<h1>Events</h1>" } }, "public_paths": { "mode": "explicit", "replace": { "/events": { "asset": "events.html" } } } } }` for ordinary clean static URLs. In explicit mode `/events.html` is not public unless separately declared; `mode: "implicit"` restores filename-derived reachability and can widen access. `routes.replace` is an array of route entries, not a path-keyed map. Use exact `/admin` plus final-wildcard `/admin/*` for a routed section root, narrow `/api/*` methods such as `["GET","POST","OPTIONS"]`, POST-only `/login` function routes, and exact static route targets like `{ "pattern": "/events", "methods": ["GET","HEAD"], "target": { "type": "static", "file": "events.html" } }` only for route-table alias behavior. Static route `file` is a release asset path, not a public path, URL, CAS hash, rewrite, or redirect. Recipe — static home page + SPA shell: a root alias `{ "pattern": "/", "target": { "type": "static", "file": "home.html" } }` (with `home.html` at the site root) serves real static bytes at `GET /` (`route_static_alias`) while unmatched app routes keep the `index.html` shell (`spa_fallback`); expect non-blocking `STATIC_ALIAS_SHADOWS_STATIC_PATH` (warn) / `STATIC_ALIAS_DUPLICATE_CANONICAL_URL` (info) plan lints and verify with `run402 deploy resolve --url https://<your-site>/ --method GET`. Routed functions use Node 22 Fetch Request -> Response; `req.url` is the full public URL on managed subdomains, deployment hosts, and verified custom domains. Direct `/functions/v1/:name` remains API-key protected. Known resolve literals include `host_missing`, `manifest_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `none`, `static_exact`, `static_index`, `spa_fallback`, `spa_fallback_missing`, `route_function`, `route_static_alias`, and `route_method_miss`; known `authorization_result` values include `authorized`, `not_public`, `not_applicable`, `manifest_missing`, `target_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `missing_cas_object`, `unfinalized_or_deleting_cas_object`, `size_mismatch`, and `unauthorized_cas_object`. Known `fallback_state` values include `active_release_missing`, `unsupported_manifest_version`, and `negative_cache_hit`; preserve unknown future strings. Release inventories may include `static_public_paths` with `public_path`, `asset_path`, `reachability_authority`, and `direct`; resolve may return the same reachability fields plus `authorization_result`, `cas_object`, `response_variant`, and route/static fields like `allow`, `route_pattern`, `target_type`, `target_name`, and `target_file`. Static route target warnings include `STATIC_ALIAS_SHADOWS_STATIC_PATH`, `STATIC_ALIAS_RELATIVE_ASSET_RISK`, `STATIC_ALIAS_DUPLICATE_CANONICAL_URL`, `STATIC_ALIAS_EXTENSIONLESS_NON_HTML`, and `STATIC_ALIAS_TABLE_NEAR_LIMIT`; inspect active routes, `static_public_paths`, and the backing `asset_path`. Runtime route failure codes to branch on: `ROUTE_MANIFEST_LOAD_FAILED`, `ROUTED_INVOKE_WORKER_SECRET_MISSING`, `ROUTED_INVOKE_AUTH_FAILED`, `ROUTED_ROUTE_STALE`, `ROUTE_METHOD_NOT_ALLOWED`, and `ROUTED_RESPONSE_TOO_LARGE`.
|
|
252
|
+
`run402 deploy apply` accepts `site.public_paths` for clean static browser URLs and `routes.replace` for function ingress or exact method-aware static aliases. Release asset paths such as `events.html` are distinct from public paths such as `/events`: prefer `{ "site": { "replace": { "events.html": { "data": "<h1>Events</h1>" } }, "public_paths": { "mode": "explicit", "replace": { "/events": { "asset": "events.html" } } } } }` for ordinary clean static URLs. In explicit mode `/events.html` is not public unless separately declared; `mode: "implicit"` restores filename-derived reachability and can widen access. `site.embedding.frame_ancestors` (catalog keys, e.g. `["localhost"]`, never raw origins) lets the named embedders put the site in an iframe: the gateway sends `Content-Security-Policy: frame-ancestors http://localhost:* http://127.0.0.1:*` and drops `X-Frame-Options` on every response of the host; omitted on a later apply carries the previous declaration forward, `null` returns to deny, unknown keys fail `INVALID_SPEC` naming the valid ones, and `/_run402/config.json` discloses the effective policy cross-origin so an embedder can check before framing. `routes.replace` is an array of route entries, not a path-keyed map. Use exact `/admin` plus final-wildcard `/admin/*` for a routed section root, narrow `/api/*` methods such as `["GET","POST","OPTIONS"]`, POST-only `/login` function routes, and exact static route targets like `{ "pattern": "/events", "methods": ["GET","HEAD"], "target": { "type": "static", "file": "events.html" } }` only for route-table alias behavior. Static route `file` is a release asset path, not a public path, URL, CAS hash, rewrite, or redirect. Recipe — static home page + SPA shell: a root alias `{ "pattern": "/", "target": { "type": "static", "file": "home.html" } }` (with `home.html` at the site root) serves real static bytes at `GET /` (`route_static_alias`) while unmatched app routes keep the `index.html` shell (`spa_fallback`); expect non-blocking `STATIC_ALIAS_SHADOWS_STATIC_PATH` (warn) / `STATIC_ALIAS_DUPLICATE_CANONICAL_URL` (info) plan lints and verify with `run402 deploy resolve --url https://<your-site>/ --method GET`. Routed functions use Node 22 Fetch Request -> Response; `req.url` is the full public URL on managed subdomains, deployment hosts, and verified custom domains. Direct `/functions/v1/:name` remains API-key protected. Known resolve literals include `host_missing`, `manifest_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `none`, `static_exact`, `static_index`, `spa_fallback`, `spa_fallback_missing`, `route_function`, `route_static_alias`, and `route_method_miss`; known `authorization_result` values include `authorized`, `not_public`, `not_applicable`, `manifest_missing`, `target_missing`, `active_release_missing`, `unsupported_manifest_version`, `path_error`, `missing_cas_object`, `unfinalized_or_deleting_cas_object`, `size_mismatch`, and `unauthorized_cas_object`. Known `fallback_state` values include `active_release_missing`, `unsupported_manifest_version`, and `negative_cache_hit`; preserve unknown future strings. Release inventories may include `static_public_paths` with `public_path`, `asset_path`, `reachability_authority`, and `direct`; resolve may return the same reachability fields plus `authorization_result`, `cas_object`, `response_variant`, and route/static fields like `allow`, `route_pattern`, `target_type`, `target_name`, and `target_file`. Static route target warnings include `STATIC_ALIAS_SHADOWS_STATIC_PATH`, `STATIC_ALIAS_RELATIVE_ASSET_RISK`, `STATIC_ALIAS_DUPLICATE_CANONICAL_URL`, `STATIC_ALIAS_EXTENSIONLESS_NON_HTML`, and `STATIC_ALIAS_TABLE_NEAR_LIMIT`; inspect active routes, `static_public_paths`, and the backing `asset_path`. Runtime route failure codes to branch on: `ROUTE_MANIFEST_LOAD_FAILED`, `ROUTED_INVOKE_WORKER_SECRET_MISSING`, `ROUTED_INVOKE_AUTH_FAILED`, `ROUTED_ROUTE_STALE`, `ROUTE_METHOD_NOT_ALLOWED`, and `ROUTED_RESPONSE_TOO_LARGE`.
|
|
253
253
|
|
|
254
254
|
### Secrets
|
|
255
255
|
|
package/gitvault-surface.json
CHANGED
package/lib/deploy-v2.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
} from "#sdk/node";
|
|
41
41
|
import { getSdk } from "./sdk.mjs";
|
|
42
42
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
43
|
-
import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId, isCoreApiTarget } from "./config.mjs";
|
|
43
|
+
import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId, isCoreApiTarget, updateProject } from "./config.mjs";
|
|
44
44
|
import { delegateTokenFromEnv } from "#sdk/node";
|
|
45
45
|
import { flagValue, normalizeArgv } from "./argparse.mjs";
|
|
46
46
|
import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
|
|
@@ -1050,6 +1050,16 @@ export function collectManifestSourceFiles(spec, baseDir) {
|
|
|
1050
1050
|
return [...out];
|
|
1051
1051
|
}
|
|
1052
1052
|
|
|
1053
|
+
/**
|
|
1054
|
+
* Persist an activated release's `deployment_id` into the local keystore.
|
|
1055
|
+
* `run402 subdomains claim` reads it as an optimization only — the gateway
|
|
1056
|
+
* binds the live release without it — so this must never throw.
|
|
1057
|
+
*/
|
|
1058
|
+
export function rememberLastDeployment(projectId, deploymentId) {
|
|
1059
|
+
if (!projectId || typeof deploymentId !== "string" || !deploymentId) return;
|
|
1060
|
+
try { updateProject(projectId, { last_deployment_id: deploymentId }); } catch { /* best-effort cache */ }
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1053
1063
|
async function applyCmd(args) {
|
|
1054
1064
|
const opts = parseApplyArgs(args);
|
|
1055
1065
|
const { source, error: sourceError } = resolveApplySource(opts, hasStdinSource());
|
|
@@ -1296,6 +1306,10 @@ async function applyCmd(args) {
|
|
|
1296
1306
|
onCommitLine: (line) => { if (!opts.quiet) process.stderr.write(`${line}\n`); },
|
|
1297
1307
|
}),
|
|
1298
1308
|
);
|
|
1309
|
+
// Cache the activated deployment id so `run402 subdomains claim` can
|
|
1310
|
+
// pass it as an optimization (the gateway defaults to the live release
|
|
1311
|
+
// without it). Best-effort: a keystore hiccup never fails the deploy.
|
|
1312
|
+
rememberLastDeployment(releaseSpec.project, outcome.deploy?.urls?.deployment_id);
|
|
1299
1313
|
if (!outcome.gitvault) {
|
|
1300
1314
|
console.log(JSON.stringify({ ...outcome.deploy, stats: sdkStats(sdk) }, null, 2));
|
|
1301
1315
|
printVerboseStats(opts.verbose, sdk);
|
package/lib/image.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { writeFileSync } from "fs";
|
|
|
2
2
|
import { getSdk } from "./sdk.mjs";
|
|
3
3
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
4
4
|
import { assertAllowedValue, assertKnownFlags, flagValue, normalizeArgv, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
|
|
5
|
+
import { resolveOrg } from "./org-context.mjs";
|
|
6
|
+
import { nextAction } from "./next-actions.mjs";
|
|
5
7
|
|
|
6
8
|
const HELP = `run402 image — Generate AI images via x402 micropayments
|
|
7
9
|
|
|
@@ -13,7 +15,9 @@ Options:
|
|
|
13
15
|
--output <file> Save image to file (e.g. output.png)
|
|
14
16
|
If omitted, returns base64 JSON to stdout
|
|
15
17
|
--org <org_id> The paying organization when the wallet belongs to more
|
|
16
|
-
than one (Lightning rail); x402 ignores it
|
|
18
|
+
than one (Lightning rail); x402 ignores it. Omitted, it
|
|
19
|
+
is the current org (RUN402_ORG, the .run402.json binding,
|
|
20
|
+
run402 org use, or the active project's owning org)
|
|
17
21
|
--help, -h Show this help message
|
|
18
22
|
|
|
19
23
|
Examples:
|
|
@@ -47,7 +51,9 @@ Options:
|
|
|
47
51
|
--output <file> Save image to file (e.g. output.png). If omitted,
|
|
48
52
|
returns base64 JSON to stdout.
|
|
49
53
|
--org <org_id> The paying organization when the wallet belongs to more
|
|
50
|
-
than one (Lightning rail); x402 ignores it
|
|
54
|
+
than one (Lightning rail); x402 ignores it. Omitted, it
|
|
55
|
+
is the current org (RUN402_ORG, the .run402.json binding,
|
|
56
|
+
run402 org use, or the active project's owning org)
|
|
51
57
|
|
|
52
58
|
Notes:
|
|
53
59
|
- Requires a funded allowance (run402 allowance create && run402 allowance fund)
|
|
@@ -104,8 +110,17 @@ export async function run(sub, args) {
|
|
|
104
110
|
}
|
|
105
111
|
assertAllowedValue(opts.aspect, ["square", "landscape", "portrait"], "--aspect");
|
|
106
112
|
|
|
113
|
+
// The paying organization rides the ONE shared org chain (--org, then
|
|
114
|
+
// RUN402_ORG / RUN402_ROOM / RUN402_PROJECT_ID, then the .run402.json
|
|
115
|
+
// binding, then `org use` / the active project's owning org). Only the
|
|
116
|
+
// Lightning rail reads it — a multi-org principal is refused
|
|
117
|
+
// ORGANIZATION_SELECTION_REQUIRED without one — and x402 ignores it, so
|
|
118
|
+
// resolving it is never a reason to refuse an x402 purchase: `optional`
|
|
119
|
+
// answers null rather than ORG_REQUIRED when the chain is empty.
|
|
120
|
+
const org = await resolveOrg({ org: opts.org }, { cmd: "image", optional: true });
|
|
121
|
+
|
|
107
122
|
try {
|
|
108
|
-
const data = await getSdk().ai.generateImage({ prompt: opts.prompt, aspect: opts.aspect, ...(
|
|
123
|
+
const data = await getSdk().ai.generateImage({ prompt: opts.prompt, aspect: opts.aspect, ...(org ? { orgId: org.orgId } : {}) });
|
|
109
124
|
if (opts.output) {
|
|
110
125
|
const buf = Buffer.from(data.image, "base64");
|
|
111
126
|
writeFileSync(opts.output, buf);
|
|
@@ -114,6 +129,45 @@ export async function run(sub, args) {
|
|
|
114
129
|
console.log(JSON.stringify({ aspect: data.aspect, content_type: data.content_type, image: data.image }));
|
|
115
130
|
}
|
|
116
131
|
} catch (err) {
|
|
132
|
+
reportOrgSelectionError(err, org);
|
|
117
133
|
reportSdkError(err);
|
|
118
134
|
}
|
|
119
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The Lightning rail's two org refusals, explained in the CLI's own words:
|
|
139
|
+
* `ORGANIZATION_SELECTION_REQUIRED` (no org named, and the SDK found no
|
|
140
|
+
* single local match among the gateway's candidates) and the 403 the
|
|
141
|
+
* gateway answers when the resolved org is one the principal cannot bill.
|
|
142
|
+
* Both name where the org came from and how to name another one.
|
|
143
|
+
*/
|
|
144
|
+
function reportOrgSelectionError(err, org) {
|
|
145
|
+
const code = err?.code;
|
|
146
|
+
const details = err?.details && typeof err.details === "object" ? err.details : {};
|
|
147
|
+
if (code === "ORGANIZATION_SELECTION_REQUIRED") {
|
|
148
|
+
const ids = Array.isArray(details.organization_ids) ? details.organization_ids : [];
|
|
149
|
+
fail({
|
|
150
|
+
code,
|
|
151
|
+
message: err.message,
|
|
152
|
+
hint:
|
|
153
|
+
`Pass --org <org_id> (one of: ${ids.join(", ") || "run402 org list"}) or select a current organization with ` +
|
|
154
|
+
"run402 org use <org_id>. Only the Lightning rail needs it; x402 ignores it.",
|
|
155
|
+
details: { ...details, organization_ids: ids, org_source: org?.source ?? null, org_source_detail: org?.sourceDetail ?? null },
|
|
156
|
+
next_actions: err.nextActions ?? [
|
|
157
|
+
nextAction("edit_request", { command: 'run402 image generate "<prompt>" --org <org_id>', why: "Name the paying organization on this call." }),
|
|
158
|
+
nextAction("edit_request", { command: "run402 org use <org_id>", why: "Select a current organization for this profile." }),
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (err?.status === 403 && org && details.org_id === org.orgId && org.source !== "flag") {
|
|
163
|
+
fail({
|
|
164
|
+
code: code ?? "FORBIDDEN",
|
|
165
|
+
message: err.message,
|
|
166
|
+
hint:
|
|
167
|
+
`The paying organization ${org.orgId} came from ${org.sourceDetail} (${org.source}) and this principal holds no billing role there. ` +
|
|
168
|
+
"Pass --org <org_id> for an organization you can bill, or run: run402 org use <org_id>",
|
|
169
|
+
details: { ...details, org_source: org.source, org_source_detail: org.sourceDetail },
|
|
170
|
+
next_actions: err.nextActions,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
package/lib/init.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readAllowance, saveAllowance, loadKeyStore, configDir, configureApiBase, getActiveProjectId } from "./config.mjs";
|
|
2
2
|
import { getSdk } from "./sdk.mjs";
|
|
3
3
|
import { fail } from "./sdk-errors.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { upDeployAction, deployAction } from "./next-actions.mjs";
|
|
5
5
|
import { resolveOwningOrgId } from "./org-context.mjs";
|
|
6
6
|
import { getActiveProfile } from "../core-dist/config.js";
|
|
7
7
|
import { readMeta } from "../core-dist/profiles.js";
|
|
@@ -77,7 +77,8 @@ Steps (idempotent when re-run with the same rail; pass --switch-rail to change r
|
|
|
77
77
|
3. Checks on-chain balance; requests faucet if zero
|
|
78
78
|
4. Shows current tier subscription status
|
|
79
79
|
5. Lists local project count
|
|
80
|
-
6. Suggests next step (tier
|
|
80
|
+
6. Suggests next step (run402 up -y when no tier is held yet — it subscribes
|
|
81
|
+
the prototype tier as part of the first deploy — or deploy apply)
|
|
81
82
|
|
|
82
83
|
Run this once to get started, or again to check your setup.
|
|
83
84
|
`;
|
|
@@ -730,10 +731,15 @@ export async function run(args = []) {
|
|
|
730
731
|
// string mirror of the first action's command (one spelling, surface-wide).
|
|
731
732
|
write("");
|
|
732
733
|
const tierMissing = !tierInfo || !tierInfo.tier || !tierInfo.active;
|
|
733
|
-
summary.next_actions = [tierMissing ?
|
|
734
|
+
summary.next_actions = [tierMissing ? upDeployAction() : deployAction()];
|
|
734
735
|
summary.next_step = summary.next_actions[0].command;
|
|
735
736
|
if (tierMissing) {
|
|
736
|
-
|
|
737
|
+
// `up -y` subscribes the prototype tier itself as part of the first
|
|
738
|
+
// deploy; `init` never buys the tier, so the one command that finishes
|
|
739
|
+
// the cold start is `up`, with `tier set` named as the standalone option.
|
|
740
|
+
write(" Next: run402 up -y");
|
|
741
|
+
write(" Deploy with run402 up -y — it subscribes the prototype tier (free on testnet) as part of the first deploy.");
|
|
742
|
+
write(" Or subscribe separately: run402 tier set prototype.");
|
|
737
743
|
} else {
|
|
738
744
|
write(" Ready to deploy. Run: run402 deploy apply --manifest app.json");
|
|
739
745
|
}
|
package/lib/next-actions.mjs
CHANGED
|
@@ -67,6 +67,19 @@ export function deployAction() {
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The cold-start deploy when the account holds no tier yet: `run402 up -y`
|
|
72
|
+
* subscribes the prototype tier itself as part of the first deploy, so `init`
|
|
73
|
+
* hands the agent ONE command instead of `tier set` + a separate deploy.
|
|
74
|
+
* `tier set prototype` stays named as the standalone alternative.
|
|
75
|
+
*/
|
|
76
|
+
export function upDeployAction() {
|
|
77
|
+
return nextAction("deploy", {
|
|
78
|
+
command: "run402 up -y",
|
|
79
|
+
why: "Deploy with run402 up -y — it subscribes the prototype tier (free on testnet) as part of the first deploy. Or subscribe separately: run402 tier set prototype.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
/**
|
|
71
84
|
* `repos create` (and `gitvault init`) on an org with no claimed slug: the
|
|
72
85
|
* response's `address: null` had no pointer to WHY, or to the named-addressing
|
package/lib/status.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
apiTargetKind,
|
|
8
8
|
} from "./config.mjs";
|
|
9
9
|
import { getSdk } from "./sdk.mjs";
|
|
10
|
+
import { fail } from "./sdk-errors.mjs";
|
|
10
11
|
import { assertKnownFlags, hasHelp, normalizeArgv } from "./argparse.mjs";
|
|
11
12
|
import { getActiveProfile } from "../core-dist/config.js";
|
|
12
13
|
import { readMeta } from "../core-dist/profiles.js";
|
|
@@ -14,7 +15,7 @@ import { readMeta } from "../core-dist/profiles.js";
|
|
|
14
15
|
const HELP = `run402 status — Show full organization state in one shot
|
|
15
16
|
|
|
16
17
|
Usage:
|
|
17
|
-
run402 status [--json]
|
|
18
|
+
run402 status [--json|--human]
|
|
18
19
|
|
|
19
20
|
Displays:
|
|
20
21
|
- Wallet identity (local_label, server_label, address)
|
|
@@ -26,6 +27,9 @@ Displays:
|
|
|
26
27
|
- Active API target
|
|
27
28
|
|
|
28
29
|
Output is JSON by default. --json is accepted as a compatibility no-op.
|
|
30
|
+
--human renders a compact human summary instead (wallet, API target, tier,
|
|
31
|
+
active project, balance, and the next action); it cannot be combined with
|
|
32
|
+
--json.
|
|
29
33
|
Run402 Cloud status requires an allowance; Core target status can still report
|
|
30
34
|
local project state without one.
|
|
31
35
|
`;
|
|
@@ -90,7 +94,16 @@ function normalizeProject(raw) {
|
|
|
90
94
|
export async function run(args = []) {
|
|
91
95
|
args = normalizeArgv(args);
|
|
92
96
|
if (hasHelp(args)) { console.log(HELP); process.exit(0); }
|
|
93
|
-
assertKnownFlags(args, ["--help", "-h", "--json"]);
|
|
97
|
+
assertKnownFlags(args, ["--help", "-h", "--json", "--human"]);
|
|
98
|
+
const human = args.includes("--human");
|
|
99
|
+
if (human && args.includes("--json")) {
|
|
100
|
+
fail({
|
|
101
|
+
code: "BAD_USAGE",
|
|
102
|
+
message: "--human cannot be combined with --json.",
|
|
103
|
+
details: { flags: args.filter((arg) => arg === "--human" || arg === "--json") },
|
|
104
|
+
hint: "JSON is the default; drop --json to keep it, or keep --human alone for the rendered view.",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
94
107
|
const allowance = readAllowance();
|
|
95
108
|
const target = {
|
|
96
109
|
api_base: apiBase(),
|
|
@@ -99,13 +112,18 @@ export async function run(args = []) {
|
|
|
99
112
|
};
|
|
100
113
|
if (!allowance) {
|
|
101
114
|
const store = loadKeyStore();
|
|
102
|
-
|
|
115
|
+
const bare = {
|
|
103
116
|
wallet: null,
|
|
104
117
|
target,
|
|
105
118
|
projects: Object.keys(store.projects).map(id => ({ project_id: id })),
|
|
106
119
|
active_project: getActiveProjectId() || null,
|
|
107
120
|
hint: target.kind === "core" ? "Run: run402 projects provision --name my-app" : "Run: run402 init",
|
|
108
|
-
}
|
|
121
|
+
};
|
|
122
|
+
if (human) {
|
|
123
|
+
console.log(formatStatusHuman(bare));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
console.log(JSON.stringify(bare));
|
|
109
127
|
return;
|
|
110
128
|
}
|
|
111
129
|
|
|
@@ -170,5 +188,80 @@ export async function run(args = []) {
|
|
|
170
188
|
target,
|
|
171
189
|
};
|
|
172
190
|
|
|
191
|
+
if (human) {
|
|
192
|
+
console.log(formatStatusHuman(result));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
173
195
|
console.log(JSON.stringify(result, null, 2));
|
|
174
196
|
}
|
|
197
|
+
|
|
198
|
+
function usdFromMicros(micros) {
|
|
199
|
+
if (typeof micros !== "number" || !Number.isFinite(micros)) return null;
|
|
200
|
+
return `$${(micros / 1_000_000).toFixed(2)}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The one next action for the human view, derived from the same state the
|
|
205
|
+
* JSON carries. Mirrors `init`: `run402 up -y` subscribes the prototype tier
|
|
206
|
+
* itself as part of the first deploy, so a tier-less account gets ONE command.
|
|
207
|
+
*/
|
|
208
|
+
function statusNextAction(result) {
|
|
209
|
+
if (!result.wallet) {
|
|
210
|
+
return result.hint?.replace(/^Run:\s*/, "") ?? "run402 init";
|
|
211
|
+
}
|
|
212
|
+
if (!result.tier) {
|
|
213
|
+
return "run402 up -y (subscribes the prototype tier, free on testnet, as part of the first deploy; or: run402 tier set prototype)";
|
|
214
|
+
}
|
|
215
|
+
if (!result.active_project) {
|
|
216
|
+
return "run402 up --name <name> -y (or select an existing project: run402 projects use <project_id>)";
|
|
217
|
+
}
|
|
218
|
+
return "run402 up -y (redeploys the active project from run402.json)";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Compact human rendering of the status payload — the same style as
|
|
223
|
+
* `run402 up --human`: one fact per line, the next action last. The JSON
|
|
224
|
+
* shape is untouched; this only formats it.
|
|
225
|
+
*/
|
|
226
|
+
export function formatStatusHuman(result) {
|
|
227
|
+
const lines = [];
|
|
228
|
+
if (!result.wallet) {
|
|
229
|
+
lines.push("Wallet: none (no allowance on this machine)");
|
|
230
|
+
} else {
|
|
231
|
+
const label = result.wallet.server_label
|
|
232
|
+
? `${result.wallet.local_label} (${result.wallet.server_label})`
|
|
233
|
+
: result.wallet.local_label;
|
|
234
|
+
lines.push(`Wallet: ${label} ${result.wallet.address}`);
|
|
235
|
+
if (result.rail) lines.push(`Rail: ${result.rail}`);
|
|
236
|
+
}
|
|
237
|
+
lines.push(`API: ${result.target.api_base} (${result.target.kind}, ${result.target.api_base_source})`);
|
|
238
|
+
if (result.wallet) {
|
|
239
|
+
if (result.tier) {
|
|
240
|
+
const lifecycle = result.organization_lifecycle_state ? `, org ${result.organization_lifecycle_state}` : "";
|
|
241
|
+
const expiry = result.lease_perpetual === true
|
|
242
|
+
? ", perpetual"
|
|
243
|
+
: (result.tier.expires ? `, expires ${result.tier.expires}` : "");
|
|
244
|
+
lines.push(`Tier: ${result.tier.name} (${result.tier.status}${lifecycle}${expiry})`);
|
|
245
|
+
} else {
|
|
246
|
+
lines.push("Tier: none");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const projects = Array.isArray(result.projects) ? result.projects : [];
|
|
250
|
+
if (result.active_project) {
|
|
251
|
+
const active = projects.find((p) => p?.project_id === result.active_project);
|
|
252
|
+
const site = active?.site_url ? ` ${active.site_url}` : "";
|
|
253
|
+
lines.push(`Project: ${result.active_project}${site}`);
|
|
254
|
+
} else {
|
|
255
|
+
lines.push(`Project: none active${projects.length ? ` (${projects.length} known)` : ""}`);
|
|
256
|
+
}
|
|
257
|
+
if (result.balances) {
|
|
258
|
+
const parts = [];
|
|
259
|
+
const onChain = usdFromMicros(result.balances.on_chain_usd_micros);
|
|
260
|
+
if (onChain) parts.push(`${onChain} ${result.balances.on_chain_token} on-chain`);
|
|
261
|
+
const credit = usdFromMicros(result.balances.prepaid_credit_usd_micros);
|
|
262
|
+
if (credit) parts.push(`${credit} prepaid credit`);
|
|
263
|
+
if (parts.length) lines.push(`Balance: ${parts.join(", ")}`);
|
|
264
|
+
}
|
|
265
|
+
lines.push(`Next: ${statusNextAction(result)}`);
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
package/lib/subdomains.mjs
CHANGED
|
@@ -2,7 +2,6 @@ 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, failUnknownSubcommand } from "./argparse.mjs";
|
|
5
|
-
import { cliCommandAction } from "./next-actions.mjs";
|
|
6
5
|
|
|
7
6
|
const HELP = `run402 subdomains — Manage custom subdomains
|
|
8
7
|
|
|
@@ -10,28 +9,30 @@ Usage:
|
|
|
10
9
|
run402 subdomains <subcommand> [args...]
|
|
11
10
|
|
|
12
11
|
Subcommands:
|
|
13
|
-
claim <name> [--project <id>] [--deployment <id>] Claim a subdomain
|
|
14
|
-
delete <name> --confirm [--project <id>]
|
|
15
|
-
list [--project <id>]
|
|
12
|
+
claim <name> [--project <id>] [--release <id> | --deployment <id>] Claim a subdomain
|
|
13
|
+
delete <name> --confirm [--project <id>] Release a subdomain. Requires --confirm.
|
|
14
|
+
list [--project <id>] List subdomains for a project
|
|
16
15
|
|
|
17
|
-
Options default to the active project
|
|
16
|
+
Options default to the active project. With neither --release nor
|
|
17
|
+
--deployment, claim binds the project's live release.
|
|
18
18
|
|
|
19
19
|
Examples:
|
|
20
20
|
run402 subdomains claim myapp
|
|
21
|
-
run402 subdomains claim myapp --
|
|
21
|
+
run402 subdomains claim myapp --release rel_abc123 --project prj_abc123
|
|
22
22
|
run402 subdomains delete myapp --confirm
|
|
23
23
|
run402 subdomains list
|
|
24
24
|
|
|
25
25
|
Notes:
|
|
26
26
|
- Subdomain names: 3-63 chars, lowercase alphanumeric + hyphens
|
|
27
|
-
- Creates <name>.run402.com pointing to the
|
|
27
|
+
- Creates <name>.run402.com pointing to the bound release
|
|
28
|
+
- Or declare it in the deploy manifest: "subdomains": { "set": ["myapp"] }
|
|
28
29
|
`;
|
|
29
30
|
|
|
30
31
|
const SUB_HELP = {
|
|
31
|
-
claim: `run402 subdomains claim — Claim a custom subdomain for a
|
|
32
|
+
claim: `run402 subdomains claim — Claim a custom subdomain for a release
|
|
32
33
|
|
|
33
34
|
Usage:
|
|
34
|
-
run402 subdomains claim <name> [--project <id>] [--deployment <id>]
|
|
35
|
+
run402 subdomains claim <name> [--project <id>] [--release <id> | --deployment <id>]
|
|
35
36
|
|
|
36
37
|
Arguments:
|
|
37
38
|
<name> Subdomain name (3-63 chars, lowercase alphanumeric +
|
|
@@ -39,15 +40,19 @@ Arguments:
|
|
|
39
40
|
|
|
40
41
|
Options:
|
|
41
42
|
--project <id> Project ID (defaults to the active project)
|
|
42
|
-
--
|
|
43
|
-
|
|
43
|
+
--release <id> Release ID (rel_...) to point at. With neither flag,
|
|
44
|
+
the project's live release is bound.
|
|
45
|
+
--deployment <id> Legacy deployment ID (dpl_...); rel_.../op_... ids are
|
|
46
|
+
accepted too.
|
|
44
47
|
|
|
45
48
|
Notes:
|
|
46
|
-
-
|
|
49
|
+
- With no flag the gateway binds the project's live (active) release; a
|
|
50
|
+
project with no live site answers 404.
|
|
51
|
+
- A deploy manifest can declare the same thing: "subdomains": { "set": ["<name>"] }
|
|
47
52
|
|
|
48
53
|
Examples:
|
|
49
54
|
run402 subdomains claim myapp
|
|
50
|
-
run402 subdomains claim myapp --
|
|
55
|
+
run402 subdomains claim myapp --release rel_abc123 --project prj_abc123
|
|
51
56
|
`,
|
|
52
57
|
list: `run402 subdomains list — List subdomains claimed by a project
|
|
53
58
|
|
|
@@ -82,19 +87,20 @@ Examples:
|
|
|
82
87
|
|
|
83
88
|
async function claim(args) {
|
|
84
89
|
const parsedArgs = normalizeArgv(args);
|
|
85
|
-
const valueFlags = ["--project", "--deployment"];
|
|
90
|
+
const valueFlags = ["--project", "--deployment", "--release"];
|
|
86
91
|
assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
|
|
87
92
|
const opts = {
|
|
88
93
|
project: flagValue(parsedArgs, "--project"),
|
|
89
94
|
deployment: flagValue(parsedArgs, "--deployment"),
|
|
95
|
+
release: flagValue(parsedArgs, "--release"),
|
|
90
96
|
};
|
|
91
|
-
let name
|
|
97
|
+
let name;
|
|
92
98
|
const positionals = positionalArgs(parsedArgs, valueFlags);
|
|
93
99
|
if (positionals.length > 1) {
|
|
94
100
|
fail({
|
|
95
101
|
code: "BAD_USAGE",
|
|
96
102
|
message: `Unexpected argument for subdomains claim: ${positionals[1]}`,
|
|
97
|
-
hint: "Use `run402 subdomains claim <name> --
|
|
103
|
+
hint: "Use `run402 subdomains claim <name> [--release <rel_id>]`.",
|
|
98
104
|
});
|
|
99
105
|
}
|
|
100
106
|
if (positionals.length === 1) {
|
|
@@ -104,23 +110,26 @@ async function claim(args) {
|
|
|
104
110
|
fail({
|
|
105
111
|
code: "BAD_USAGE",
|
|
106
112
|
message: "Missing <name>.",
|
|
107
|
-
hint: "run402 subdomains claim <name> [--project <id>] [--deployment <id>]",
|
|
113
|
+
hint: "run402 subdomains claim <name> [--project <id>] [--release <id> | --deployment <id>]",
|
|
108
114
|
});
|
|
109
115
|
}
|
|
110
|
-
|
|
111
|
-
const p = resolveProject(opts.project);
|
|
112
|
-
deploymentId = opts.deployment || deploymentId || p.last_deployment_id;
|
|
113
|
-
if (!deploymentId) {
|
|
116
|
+
if (opts.deployment && opts.release) {
|
|
114
117
|
fail({
|
|
115
|
-
code: "
|
|
116
|
-
message: "
|
|
117
|
-
hint: "
|
|
118
|
-
details: { project_id: projectId },
|
|
119
|
-
next_actions: [cliCommandAction("deploy", "run402 deploy apply", "Deploy a site first, then retry claiming the subdomain.")],
|
|
118
|
+
code: "BAD_USAGE",
|
|
119
|
+
message: "Pass either --release or --deployment, not both.",
|
|
120
|
+
hint: "run402 subdomains claim <name> [--release <id> | --deployment <id>]",
|
|
120
121
|
});
|
|
121
122
|
}
|
|
123
|
+
const projectId = resolveProjectId(opts.project);
|
|
124
|
+
const p = resolveProject(opts.project);
|
|
125
|
+
// With neither flag, the keystore's cached `last_deployment_id` is only an
|
|
126
|
+
// optimization; absent one, the gateway resolves the project's live
|
|
127
|
+
// release itself. Never fail client-side here — a unified-apply deploy
|
|
128
|
+
// may never have written the cache.
|
|
129
|
+
const deploymentId = opts.deployment || (opts.release ? undefined : p.last_deployment_id) || undefined;
|
|
130
|
+
const releaseId = opts.release || undefined;
|
|
122
131
|
try {
|
|
123
|
-
const data = await getSdk().subdomains.claim({ name, deploymentId, projectId });
|
|
132
|
+
const data = await getSdk().subdomains.claim({ name, deploymentId, releaseId, projectId });
|
|
124
133
|
console.log(JSON.stringify(data, null, 2));
|
|
125
134
|
} catch (err) {
|
|
126
135
|
reportSdkError(err);
|
package/lib/up.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { getSdk } from "./sdk.mjs";
|
|
|
6
6
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
7
7
|
import { assertKnownFlags, flagValue, normalizeArgv, positionalArgs } from "./argparse.mjs";
|
|
8
8
|
import { createUpdateCheckScheduler, emitUpdateNotice } from "./update-check.mjs";
|
|
9
|
-
import { allowanceAuthHeaders, isCoreApiTarget } from "./config.mjs";
|
|
9
|
+
import { allowanceAuthHeaders, isCoreApiTarget, updateProject } from "./config.mjs";
|
|
10
10
|
import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
|
|
11
11
|
|
|
12
12
|
const HELP = `run402 up — Provision/link/deploy the current app
|
|
@@ -372,6 +372,16 @@ export async function run(args = []) {
|
|
|
372
372
|
console.error(JSON.stringify(event));
|
|
373
373
|
},
|
|
374
374
|
});
|
|
375
|
+
// Cache the activated deployment id so `run402 subdomains claim` can
|
|
376
|
+
// pass it as an optimization (the gateway binds the live release
|
|
377
|
+
// without it). Best-effort: a keystore hiccup never fails `up`.
|
|
378
|
+
{
|
|
379
|
+
const deployedProject = result?.result?.project_id ?? result?.result?.deploy?.project_id ?? null;
|
|
380
|
+
const deploymentId = result?.result?.deploy?.urls?.deployment_id;
|
|
381
|
+
if (deployedProject && typeof deploymentId === "string" && deploymentId) {
|
|
382
|
+
try { updateProject(deployedProject, { last_deployment_id: deploymentId }); } catch { /* best-effort cache */ }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
375
385
|
// The remote scaffold + first push are best-effort ADDITIONS to an
|
|
376
386
|
// already-successful deploy: a git hiccup here must never flip an
|
|
377
387
|
// otherwise-successful `up` into a failure (the same non-fatal
|
|
@@ -384,7 +394,7 @@ export async function run(args = []) {
|
|
|
384
394
|
createdRepository = await gitInitIfNeeded(workDir);
|
|
385
395
|
result.result.repo = await composeRepoPushStep({ sdk, workDir, projectId, createdRepository, nested });
|
|
386
396
|
} catch (err) {
|
|
387
|
-
result.result.repo = { status: "failed", first_push: null, first_push_error: { code: err?.code ?? "GITVAULT_SETUP_FAILED", message: err?.message ?? String(err) } };
|
|
397
|
+
result.result.repo = { status: "failed", first_push: null, first_push_error: { code: err?.code ?? "GITVAULT_SETUP_FAILED", message: err?.message ?? String(err) }, ...repoNextActionsOf(err) };
|
|
388
398
|
}
|
|
389
399
|
}
|
|
390
400
|
}
|
|
@@ -405,9 +415,9 @@ export async function run(args = []) {
|
|
|
405
415
|
} else if (mode === "printSpec") {
|
|
406
416
|
console.log(JSON.stringify(result.result?.spec ?? null, null, 2));
|
|
407
417
|
} else if (human && result?.result?.app_result) {
|
|
408
|
-
console.log([formatAppUpHuman(result.result.app_result), formatRepoSkipLine(result)].filter(Boolean).join("\n"));
|
|
418
|
+
console.log([formatAppUpHuman(result.result.app_result), formatRepoSkipLine(result), formatRepoFirstPushErrorLine(result)].filter(Boolean).join("\n"));
|
|
409
419
|
} else if (human && shouldRenderHumanSuccess(result)) {
|
|
410
|
-
console.log([formatLegacyUpSuccess(result), formatRepoSkipLine(result)].filter(Boolean).join("\n"));
|
|
420
|
+
console.log([formatLegacyUpSuccess(result), formatRepoSkipLine(result), formatRepoFirstPushErrorLine(result)].filter(Boolean).join("\n"));
|
|
411
421
|
} else {
|
|
412
422
|
console.log(JSON.stringify(result, null, 2));
|
|
413
423
|
}
|
|
@@ -688,9 +698,6 @@ async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository,
|
|
|
688
698
|
// `run402 up --nested` (the caller's own spelling of the way out).
|
|
689
699
|
const scaffold = await scaffoldGitvaultRemote({ repoDir: workDir, projectId, orgId: orgId ?? undefined, createRepoIfMissing: false, nested, nestedCommand: "run402 up --nested" });
|
|
690
700
|
if (createdRepository && scaffold.gitvault) scaffold.gitvault.created_repository = true;
|
|
691
|
-
// A nested repository the scaffold itself just created is as fresh as one
|
|
692
|
-
// `up`'s own git init made: nothing committed, every file untracked.
|
|
693
|
-
const freshRepository = createdRepository || scaffold.gitvault?.created_repository === true;
|
|
694
701
|
const out = { ...scaffold, first_push: null, first_push_error: null };
|
|
695
702
|
if (scaffold.status !== "scaffolded") {
|
|
696
703
|
// Nothing was scaffolded (the app root is inside another repository, or
|
|
@@ -702,14 +709,22 @@ async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository,
|
|
|
702
709
|
out.first_push_error = { code: "GITVAULT_ORG_UNRESOLVED", message: `could not resolve the owning org for ${projectId} — the first push was skipped` };
|
|
703
710
|
return out;
|
|
704
711
|
}
|
|
712
|
+
// Read BEFORE the push decision: an unborn HEAD is what decides the lane.
|
|
713
|
+
out.local_git = await readLocalGitState(workDir);
|
|
714
|
+
// A nested repository the scaffold itself just created is as fresh as one
|
|
715
|
+
// `up`'s own git init made: nothing committed, every file untracked. So is
|
|
716
|
+
// a repository the agent `git init`ed itself and never committed to (an
|
|
717
|
+
// UNBORN HEAD): there is no clean tree to keep, only untracked files, and
|
|
718
|
+
// the strict lane would refuse SNAPSHOT_DIRTY_TREE on every first push.
|
|
719
|
+
const freshRepository = createdRepository || scaffold.gitvault?.created_repository === true || out.local_git.unborn === true;
|
|
705
720
|
try {
|
|
706
721
|
let vaultCreated = null;
|
|
707
|
-
// A repository
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
// first_push.snapshot);
|
|
712
|
-
// and its refusal is reported, never overridden.
|
|
722
|
+
// A fresh repository has, by definition, nothing committed yet — every
|
|
723
|
+
// file is untracked, so a clean-tree capture would refuse
|
|
724
|
+
// SNAPSHOT_DIRTY_TREE on every first push. Capture it as the synthetic
|
|
725
|
+
// first commit the dirty-tree lane produces (disclosed under
|
|
726
|
+
// first_push.snapshot); a repository WITH commits keeps the clean-tree
|
|
727
|
+
// rule and its refusal is reported, never overridden.
|
|
713
728
|
const pushed = await sdk.gitvault.push({
|
|
714
729
|
project_id: projectId,
|
|
715
730
|
org_id: orgId,
|
|
@@ -728,15 +743,26 @@ async function composeRepoPushStep({ sdk, workDir, projectId, createdRepository,
|
|
|
728
743
|
};
|
|
729
744
|
} catch (err) {
|
|
730
745
|
out.first_push_error = { code: err?.body?.code ?? err?.code ?? "GITVAULT_PUSH_FAILED", message: err?.message ?? String(err) };
|
|
746
|
+
Object.assign(out, repoNextActionsOf(err));
|
|
731
747
|
}
|
|
748
|
+
return out;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** `{ next_actions }` derived from a thrown SDK error's own advice, or nothing. */
|
|
752
|
+
function repoNextActionsOf(err) {
|
|
753
|
+
const actions = Array.isArray(err?.nextActions) ? err.nextActions : Array.isArray(err?.next_actions) ? err.next_actions : [];
|
|
754
|
+
return actions.length > 0 ? { next_actions: actions.map((a) => ({ type: a?.type, ...(a?.command ? { command: a.command } : {}), ...(a?.why ? { why: a.why } : {}) })) } : {};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Branch / HEAD / unborn / dirty of `workDir`, or `{ state: "unavailable" }`. Never throws. */
|
|
758
|
+
async function readLocalGitState(workDir) {
|
|
732
759
|
try {
|
|
733
760
|
const { hardenedGit } = await import("#sdk/node");
|
|
734
761
|
const read = async (args) => { try { return (await hardenedGit(workDir, args)).text().trim(); } catch { return null; } };
|
|
735
762
|
const head = await read(["rev-parse", "--verify", "HEAD"]);
|
|
736
763
|
const status = await read(["status", "--porcelain"]);
|
|
737
|
-
|
|
738
|
-
} catch {
|
|
739
|
-
return out;
|
|
764
|
+
return { branch: await read(["symbolic-ref", "--short", "HEAD"]), head, unborn: head === null, dirty: status === null ? null : status.length > 0 };
|
|
765
|
+
} catch { return { state: "unavailable" }; }
|
|
740
766
|
}
|
|
741
767
|
|
|
742
768
|
/**
|
|
@@ -833,6 +859,20 @@ function formatRepoSkipLine(result) {
|
|
|
833
859
|
return `Encrypted remote skipped: ${repo.reason ?? "skipped"}${where}.${remedy}`;
|
|
834
860
|
}
|
|
835
861
|
|
|
862
|
+
/**
|
|
863
|
+
* One line for the human summary when the first push into the encrypted
|
|
864
|
+
* remote failed: the error code and the first next action's command (or its
|
|
865
|
+
* advice) — the deploy itself succeeded, so this is a footnote, never an exit.
|
|
866
|
+
*/
|
|
867
|
+
function formatRepoFirstPushErrorLine(result) {
|
|
868
|
+
const repo = result?.result?.repo;
|
|
869
|
+
const error = repo?.first_push_error;
|
|
870
|
+
if (!error?.code) return null;
|
|
871
|
+
const first = (Array.isArray(repo.next_actions) ? repo.next_actions : []).find((action) => action?.command || action?.why);
|
|
872
|
+
const remedy = first?.command ? ` Next: ${first.command}` : first?.why ? ` ${first.why}` : "";
|
|
873
|
+
return `Encrypted remote first push failed: ${error.code}.${remedy}`;
|
|
874
|
+
}
|
|
875
|
+
|
|
836
876
|
function formatLegacyUpSuccess(result) {
|
|
837
877
|
const urls = result?.result?.deploy?.urls ?? {};
|
|
838
878
|
const origin = urls.site ?? urls.subdomain ?? urls.deployment;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run402",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.88.0",
|
|
4
4
|
"description": "CLI for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|