run402-mcp 3.7.4 → 3.7.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.
- package/README.md +4 -2
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/tools/app-up.d.ts +40 -0
- package/dist/tools/app-up.d.ts.map +1 -0
- package/dist/tools/app-up.js +48 -0
- package/dist/tools/app-up.js.map +1 -0
- package/dist/tools/create-mailbox.d.ts.map +1 -1
- package/dist/tools/create-mailbox.js +4 -1
- package/dist/tools/create-mailbox.js.map +1 -1
- package/dist/tools/get-mailbox.d.ts.map +1 -1
- package/dist/tools/get-mailbox.js +7 -0
- package/dist/tools/get-mailbox.js.map +1 -1
- package/dist/tools/list-mailboxes.d.ts.map +1 -1
- package/dist/tools/list-mailboxes.js +7 -0
- package/dist/tools/list-mailboxes.js.map +1 -1
- package/dist/tools/remove-sender-domain.js +1 -1
- package/dist/tools/remove-sender-domain.js.map +1 -1
- package/dist/tools/sender-domain-status.js +1 -1
- package/dist/tools/sender-domain-status.js.map +1 -1
- package/package.json +3 -2
- package/schemas/run402-app.v1.schema.json +304 -0
- package/sdk/dist/actions.d.ts +14 -1
- package/sdk/dist/actions.d.ts.map +1 -1
- package/sdk/dist/actions.js.map +1 -1
- package/sdk/dist/app-up.d.ts +271 -0
- package/sdk/dist/app-up.d.ts.map +1 -0
- package/sdk/dist/app-up.js +500 -0
- package/sdk/dist/app-up.js.map +1 -0
- package/sdk/dist/index.d.ts +1 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/email.d.ts +14 -2
- package/sdk/dist/namespaces/email.d.ts.map +1 -1
- package/sdk/dist/namespaces/email.js +4 -0
- package/sdk/dist/namespaces/email.js.map +1 -1
- package/sdk/dist/namespaces/projects.types.d.ts +1 -1
- package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/sender-domain.d.ts +1 -1
- package/sdk/dist/namespaces/sender-domain.d.ts.map +1 -1
- package/sdk/dist/namespaces/sender-domain.js +1 -1
- package/sdk/dist/namespaces/sender-domain.js.map +1 -1
- package/sdk/dist/node/actions-node.d.ts.map +1 -1
- package/sdk/dist/node/actions-node.js +417 -5
- package/sdk/dist/node/actions-node.js.map +1 -1
- package/sdk/dist/node/index.d.ts +1 -0
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +12 -3
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/target-profile.js +15 -3
- package/sdk/dist/node/target-profile.js.map +1 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
4
7
|
import { Run402Action } from "../actions.js";
|
|
8
|
+
import { RUN402_APP_MANIFEST_FILENAME, compileRun402AppInstallGraph, createRun402AppUpResult, } from "../app-up.js";
|
|
5
9
|
import { LocalError } from "../errors.js";
|
|
6
10
|
import { loadDeployManifest } from "./deploy-manifest.js";
|
|
7
11
|
const DEFAULT_BOOTSTRAP_TIER = "prototype";
|
|
8
12
|
const MANIFEST_CANDIDATES = [
|
|
13
|
+
RUN402_APP_MANIFEST_FILENAME,
|
|
9
14
|
"run402.deploy.json",
|
|
10
15
|
"app.json",
|
|
11
16
|
];
|
|
@@ -22,6 +27,7 @@ const TIER_RANK = {
|
|
|
22
27
|
hobby: 2,
|
|
23
28
|
team: 3,
|
|
24
29
|
};
|
|
30
|
+
const execFileAsync = promisify(execFile);
|
|
25
31
|
/**
|
|
26
32
|
* Node implementation of the action runner. The public CLI should treat this
|
|
27
33
|
* as the orchestration kernel and stay a flag parser / renderer.
|
|
@@ -126,9 +132,40 @@ export class NodeActions {
|
|
|
126
132
|
return run.result(result);
|
|
127
133
|
}
|
|
128
134
|
async #runUp(input, run) {
|
|
129
|
-
|
|
130
|
-
|
|
135
|
+
if (input.source && input.dir) {
|
|
136
|
+
throw run.error("Pass either an app source positional/repo URL or --dir, not both.", "RUN402_SOURCE_CONFLICT", { source: input.source, dir: input.dir });
|
|
137
|
+
}
|
|
138
|
+
const startedAt = new Date().toISOString();
|
|
139
|
+
const source = await this.#resolveUpSource(input, run);
|
|
140
|
+
const workspaceDir = source.workspaceDir;
|
|
141
|
+
const manifest = await this.#discoverAndValidateManifest(input, workspaceDir, source.metadata, run);
|
|
142
|
+
if (manifest.manifestKind === "app") {
|
|
143
|
+
const appResult = this.#planAppUpResult(input, manifest, run, {
|
|
144
|
+
startedAt,
|
|
145
|
+
status: "planned",
|
|
146
|
+
dryRun: run.dryRun || run.executionMode === "check" || run.executionMode === "printSpec",
|
|
147
|
+
projectId: input.projectId ?? (run.dryRun ? "prj_planned" : null),
|
|
148
|
+
});
|
|
149
|
+
if (run.executionMode === "check" || run.executionMode === "printSpec" || run.dryRun) {
|
|
150
|
+
return run.result({
|
|
151
|
+
project_id: input.projectId ?? "prj_planned",
|
|
152
|
+
manifest_path: manifest.manifestPath,
|
|
153
|
+
app_graph: manifest.appGraph,
|
|
154
|
+
app_result: appResult,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const blocked = this.#blockedAppUpResult(input, manifest, run, startedAt);
|
|
158
|
+
return run.result({
|
|
159
|
+
project_id: input.projectId ?? "prj_planned",
|
|
160
|
+
manifest_path: manifest.manifestPath,
|
|
161
|
+
app_graph: manifest.appGraph,
|
|
162
|
+
app_result: blocked,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
131
165
|
if (run.executionMode === "check" || run.executionMode === "printSpec") {
|
|
166
|
+
if (!manifest.releaseSpec) {
|
|
167
|
+
throw run.error("Internal error: release manifest did not produce a ReleaseSpec.", "RUN402_ACTION_INTERNAL");
|
|
168
|
+
}
|
|
132
169
|
return run.result({
|
|
133
170
|
project_id: manifest.releaseSpec.project,
|
|
134
171
|
manifest_path: manifest.manifestPath,
|
|
@@ -242,7 +279,7 @@ export class NodeActions {
|
|
|
242
279
|
deploy,
|
|
243
280
|
});
|
|
244
281
|
}
|
|
245
|
-
async #discoverAndValidateManifest(input, workspaceDir, run) {
|
|
282
|
+
async #discoverAndValidateManifest(input, workspaceDir, source, run) {
|
|
246
283
|
const step = run.addStep({
|
|
247
284
|
action: "deploy.discover",
|
|
248
285
|
description: "Discover and validate deploy manifest",
|
|
@@ -269,6 +306,38 @@ export class NodeActions {
|
|
|
269
306
|
}
|
|
270
307
|
throw run.error("No deploy manifest found. Add run402.deploy.json or app.json, or pass --manifest for executable configs.", "UP_MANIFEST_REQUIRED", { dir: workspaceDir, candidates: MANIFEST_CANDIDATES, executable_candidates: EXECUTABLE_MANIFEST_CANDIDATES });
|
|
271
308
|
}
|
|
309
|
+
if (basename(manifestPath) === RUN402_APP_MANIFEST_FILENAME) {
|
|
310
|
+
const loadedAppSpec = await loadRun402AppManifest(manifestPath);
|
|
311
|
+
const appSpec = input.buildMode
|
|
312
|
+
? {
|
|
313
|
+
...loadedAppSpec,
|
|
314
|
+
build: {
|
|
315
|
+
...(loadedAppSpec.build ?? {}),
|
|
316
|
+
mode: input.buildMode,
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
: loadedAppSpec;
|
|
320
|
+
const appGraph = await compileRun402AppInstallGraph(appSpec, {
|
|
321
|
+
source,
|
|
322
|
+
...(input.name ? { name: input.name } : {}),
|
|
323
|
+
...(input.idempotencyKey ?? run.rootIdempotencyKey
|
|
324
|
+
? { root_idempotency_key: input.idempotencyKey ?? run.rootIdempotencyKey }
|
|
325
|
+
: {}),
|
|
326
|
+
});
|
|
327
|
+
run.setState(step, "succeeded", {
|
|
328
|
+
manifest_kind: "app",
|
|
329
|
+
manifest_path: manifestPath,
|
|
330
|
+
app_id: appSpec.app.id,
|
|
331
|
+
graph_digest: appGraph.graph_digest,
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
manifestKind: "app",
|
|
335
|
+
manifestPath,
|
|
336
|
+
releaseSpec: null,
|
|
337
|
+
appGraph,
|
|
338
|
+
appSpec,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
272
341
|
const loaded = await loadDeployManifest(manifestPath, {
|
|
273
342
|
...(input.projectId
|
|
274
343
|
? { project: input.projectId }
|
|
@@ -283,6 +352,7 @@ export class NodeActions {
|
|
|
283
352
|
idempotency_key: loaded.idempotencyKey ?? null,
|
|
284
353
|
});
|
|
285
354
|
return {
|
|
355
|
+
manifestKind: "release",
|
|
286
356
|
manifestPath,
|
|
287
357
|
releaseSpec: loaded.spec,
|
|
288
358
|
idempotencyKey: loaded.idempotencyKey,
|
|
@@ -291,6 +361,162 @@ export class NodeActions {
|
|
|
291
361
|
: loaded.spec.project,
|
|
292
362
|
};
|
|
293
363
|
}
|
|
364
|
+
async #resolveUpSource(input, run) {
|
|
365
|
+
const baseDir = resolvePath(this.opts.cwd ?? process.cwd());
|
|
366
|
+
const rawSource = input.source ?? input.dir ?? ".";
|
|
367
|
+
const step = run.addStep({
|
|
368
|
+
action: "app.source.resolve",
|
|
369
|
+
description: "Resolve app source",
|
|
370
|
+
mutation: false,
|
|
371
|
+
auto: true,
|
|
372
|
+
details: { source: rawSource },
|
|
373
|
+
});
|
|
374
|
+
run.setState(step, "running");
|
|
375
|
+
if (isRepositoryUrl(rawSource)) {
|
|
376
|
+
const root = await mkdtemp(join(tmpdir(), "run402-app-source-"));
|
|
377
|
+
const checkout = join(root, "repo");
|
|
378
|
+
try {
|
|
379
|
+
await execFileAsync("git", ["clone", "--depth", "1", rawSource, checkout], {
|
|
380
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
381
|
+
});
|
|
382
|
+
const commit = await gitCommit(checkout);
|
|
383
|
+
run.setState(step, "succeeded", {
|
|
384
|
+
kind: "repo",
|
|
385
|
+
repo_url: rawSource,
|
|
386
|
+
commit: commit ?? null,
|
|
387
|
+
checkout,
|
|
388
|
+
});
|
|
389
|
+
return {
|
|
390
|
+
workspaceDir: checkout,
|
|
391
|
+
metadata: {
|
|
392
|
+
kind: "repo",
|
|
393
|
+
repo_url: rawSource,
|
|
394
|
+
...(commit ? { commit } : {}),
|
|
395
|
+
},
|
|
396
|
+
cleanupDir: root,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
await rm(root, { recursive: true, force: true });
|
|
401
|
+
throw run.error(`Failed to resolve repository source ${rawSource}.`, "RUN402_SOURCE_RESOLVE_FAILED", { source: rawSource, error: err instanceof Error ? err.message : String(err) });
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const workspaceDir = resolveMaybe(baseDir, rawSource);
|
|
405
|
+
const stat = await lstat(workspaceDir).catch(() => null);
|
|
406
|
+
if (!stat || !stat.isDirectory()) {
|
|
407
|
+
throw run.error("App source must be an existing directory or repository URL.", "RUN402_SOURCE_INVALID", { source: rawSource, resolved_path: workspaceDir });
|
|
408
|
+
}
|
|
409
|
+
const commit = await gitCommit(workspaceDir);
|
|
410
|
+
run.setState(step, "succeeded", {
|
|
411
|
+
kind: "local",
|
|
412
|
+
path: workspaceDir,
|
|
413
|
+
commit: commit ?? null,
|
|
414
|
+
});
|
|
415
|
+
return {
|
|
416
|
+
workspaceDir,
|
|
417
|
+
metadata: {
|
|
418
|
+
kind: "local",
|
|
419
|
+
path: workspaceDir,
|
|
420
|
+
...(commit ? { commit } : {}),
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
#planAppUpResult(input, manifest, run, opts) {
|
|
425
|
+
if (!manifest.appGraph) {
|
|
426
|
+
throw run.error("Internal error: app manifest did not produce an install graph.", "RUN402_ACTION_INTERNAL");
|
|
427
|
+
}
|
|
428
|
+
return createRun402AppUpResult({
|
|
429
|
+
graph: manifest.appGraph,
|
|
430
|
+
manifest_path: manifest.manifestPath,
|
|
431
|
+
status: opts.status,
|
|
432
|
+
started_at: opts.startedAt,
|
|
433
|
+
dry_run: opts.dryRun,
|
|
434
|
+
project_id: opts.projectId,
|
|
435
|
+
project_name: input.name ?? manifest.appSpec?.project.name ?? null,
|
|
436
|
+
public_origin: appPublicOrigin(input, manifest.appSpec),
|
|
437
|
+
approval_policy: {
|
|
438
|
+
yes: run.approval === "yes",
|
|
439
|
+
allow_prune: input.allowPrune === true,
|
|
440
|
+
max_spend_usd: input.maxSpendUsd ?? null,
|
|
441
|
+
build_mode: input.buildMode ?? manifest.appSpec?.build?.mode ?? null,
|
|
442
|
+
shell_build_approved: input.allowShellBuild === true,
|
|
443
|
+
},
|
|
444
|
+
diagnostics: opts.diagnostics,
|
|
445
|
+
next_actions: opts.nextActions,
|
|
446
|
+
blocked_node_id: opts.blockedNodeId,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
#blockedAppUpResult(input, manifest, run, startedAt) {
|
|
450
|
+
const block = this.#firstAppUpBlock(input, manifest, run);
|
|
451
|
+
return this.#planAppUpResult(input, manifest, run, {
|
|
452
|
+
startedAt,
|
|
453
|
+
status: "blocked",
|
|
454
|
+
dryRun: false,
|
|
455
|
+
projectId: input.projectId ?? null,
|
|
456
|
+
diagnostics: block.diagnostics,
|
|
457
|
+
nextActions: block.nextActions,
|
|
458
|
+
blockedNodeId: block.nodeId,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
#firstAppUpBlock(input, manifest, run) {
|
|
462
|
+
const nameBlock = missingRequiredName(input, manifest);
|
|
463
|
+
if (nameBlock)
|
|
464
|
+
return nameBlock;
|
|
465
|
+
const missingSecrets = missingRequiredSecrets(manifest.appSpec);
|
|
466
|
+
if (missingSecrets.length > 0) {
|
|
467
|
+
return {
|
|
468
|
+
nodeId: "secrets.ensure",
|
|
469
|
+
diagnostics: missingSecrets.map((secret) => ({
|
|
470
|
+
code: "MISSING_SECRET",
|
|
471
|
+
severity: "error",
|
|
472
|
+
node_id: "secrets.ensure",
|
|
473
|
+
field_path: `secrets.${secret.name}`,
|
|
474
|
+
message: [
|
|
475
|
+
`Required user secret ${secret.name} is missing from approved env source ${secret.sourceEnv}.`,
|
|
476
|
+
secret.description ? `Usage: ${secret.description}` : "",
|
|
477
|
+
].filter(Boolean).join(" "),
|
|
478
|
+
details: {
|
|
479
|
+
source_env: secret.sourceEnv,
|
|
480
|
+
...(secret.description ? { usage: secret.description } : {}),
|
|
481
|
+
},
|
|
482
|
+
})),
|
|
483
|
+
nextActions: missingSecrets.map((secret) => ({
|
|
484
|
+
type: "set_user_secret",
|
|
485
|
+
code: "MISSING_SECRET",
|
|
486
|
+
node_id: "secrets.ensure",
|
|
487
|
+
field_path: `secrets.${secret.name}`,
|
|
488
|
+
message: [
|
|
489
|
+
`Provide ${secret.sourceEnv} in the environment before retrying.`,
|
|
490
|
+
secret.description ? `Usage: ${secret.description}` : "",
|
|
491
|
+
].filter(Boolean).join(" "),
|
|
492
|
+
command: `${secret.sourceEnv}="<value>" run402 up --name <name> --yes`,
|
|
493
|
+
argv: ["run402", "secrets", "set", "<project_id>", secret.name, "--env", secret.sourceEnv],
|
|
494
|
+
})),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const spendBlock = spendApprovalBlock(input, run);
|
|
498
|
+
if (spendBlock)
|
|
499
|
+
return spendBlock;
|
|
500
|
+
const buildBlock = appBuildBlock(input, manifest);
|
|
501
|
+
if (buildBlock)
|
|
502
|
+
return buildBlock;
|
|
503
|
+
return {
|
|
504
|
+
nodeId: "project.ensure",
|
|
505
|
+
diagnostics: [{
|
|
506
|
+
code: "APP_INSTALL_STATE_UNAVAILABLE",
|
|
507
|
+
severity: "error",
|
|
508
|
+
node_id: "project.ensure",
|
|
509
|
+
message: "Server-side app installation state is required before app-aware up can mutate resources.",
|
|
510
|
+
}],
|
|
511
|
+
nextActions: [{
|
|
512
|
+
type: "wait_for_run402_support",
|
|
513
|
+
code: "APP_INSTALL_STATE_UNAVAILABLE",
|
|
514
|
+
node_id: "project.ensure",
|
|
515
|
+
message: "Use --check or --dry-run to inspect the graph until app installation endpoints are available.",
|
|
516
|
+
argv: ["run402", "up", "--check"],
|
|
517
|
+
}],
|
|
518
|
+
};
|
|
519
|
+
}
|
|
294
520
|
async #ensureAllowance(run, opts = { fund: false }) {
|
|
295
521
|
const status = await this.sdk.allowance.status();
|
|
296
522
|
if (status.configured) {
|
|
@@ -727,6 +953,174 @@ function withActionDetails(err, run) {
|
|
|
727
953
|
});
|
|
728
954
|
return err;
|
|
729
955
|
}
|
|
956
|
+
function isRepositoryUrl(value) {
|
|
957
|
+
return /^https?:\/\/.+/.test(value) || /^git@[^:]+:.+/.test(value) || /^ssh:\/\/.+/.test(value) || /^file:\/\/.+/.test(value);
|
|
958
|
+
}
|
|
959
|
+
async function gitCommit(dir) {
|
|
960
|
+
try {
|
|
961
|
+
const { stdout } = await execFileAsync("git", ["-C", dir, "rev-parse", "HEAD"], {
|
|
962
|
+
maxBuffer: 1024 * 1024,
|
|
963
|
+
});
|
|
964
|
+
const commit = String(stdout).trim();
|
|
965
|
+
return /^[0-9a-f]{40}$/i.test(commit) ? commit : undefined;
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
return undefined;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function appPublicOrigin(input, spec) {
|
|
972
|
+
const template = spec?.project.origin?.subdomain;
|
|
973
|
+
if (!template)
|
|
974
|
+
return null;
|
|
975
|
+
const name = input.name ?? spec?.project.name ?? null;
|
|
976
|
+
const subdomain = template.replace(/\$\{input\.name\}/g, name ?? "");
|
|
977
|
+
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(subdomain))
|
|
978
|
+
return null;
|
|
979
|
+
return `https://${subdomain}.run402.com`;
|
|
980
|
+
}
|
|
981
|
+
function spendApprovalBlock(input, run) {
|
|
982
|
+
const expectedPrototypeSpendUsd = 0.10;
|
|
983
|
+
if (input.maxSpendUsd !== undefined && input.maxSpendUsd < expectedPrototypeSpendUsd) {
|
|
984
|
+
return {
|
|
985
|
+
nodeId: "account.ensure",
|
|
986
|
+
diagnostics: [{
|
|
987
|
+
code: "SPEND_APPROVAL_REQUIRED",
|
|
988
|
+
severity: "error",
|
|
989
|
+
node_id: "account.ensure",
|
|
990
|
+
message: `The app install may need up to $${expectedPrototypeSpendUsd.toFixed(2)} for tier readiness, above --max-spend-usd ${input.maxSpendUsd}.`,
|
|
991
|
+
}],
|
|
992
|
+
nextActions: [{
|
|
993
|
+
type: "approve_spend",
|
|
994
|
+
code: "SPEND_APPROVAL_REQUIRED",
|
|
995
|
+
node_id: "account.ensure",
|
|
996
|
+
message: "Raise --max-spend-usd or preconfigure an active tier, then retry with approval.",
|
|
997
|
+
spend: {
|
|
998
|
+
amount_usd_micros: 100_000,
|
|
999
|
+
currency: "USD",
|
|
1000
|
+
description: "prototype tier readiness",
|
|
1001
|
+
},
|
|
1002
|
+
}],
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (run.approval !== "yes") {
|
|
1006
|
+
return {
|
|
1007
|
+
nodeId: "account.ensure",
|
|
1008
|
+
diagnostics: [{
|
|
1009
|
+
code: "SPEND_APPROVAL_REQUIRED",
|
|
1010
|
+
severity: "error",
|
|
1011
|
+
node_id: "account.ensure",
|
|
1012
|
+
message: "App-aware up needs explicit approval before it can perform spend-impacting readiness steps.",
|
|
1013
|
+
}],
|
|
1014
|
+
nextActions: [{
|
|
1015
|
+
type: "approve_spend",
|
|
1016
|
+
code: "SPEND_APPROVAL_REQUIRED",
|
|
1017
|
+
node_id: "account.ensure",
|
|
1018
|
+
message: "Retry with --yes and an appropriate --max-spend-usd cap, or run --dry-run to inspect the plan.",
|
|
1019
|
+
argv: ["run402", "up", "--yes", "--max-spend-usd", String(expectedPrototypeSpendUsd)],
|
|
1020
|
+
spend: {
|
|
1021
|
+
amount_usd_micros: 100_000,
|
|
1022
|
+
currency: "USD",
|
|
1023
|
+
description: "prototype tier readiness",
|
|
1024
|
+
},
|
|
1025
|
+
}],
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
function missingRequiredName(input, manifest) {
|
|
1031
|
+
const spec = manifest.appSpec;
|
|
1032
|
+
if (!spec || input.name || input.projectId || spec.project.id)
|
|
1033
|
+
return null;
|
|
1034
|
+
const needsName = spec.project.name?.includes("${input.name}") === true ||
|
|
1035
|
+
spec.project.origin?.subdomain?.includes("${input.name}") === true;
|
|
1036
|
+
if (!needsName)
|
|
1037
|
+
return null;
|
|
1038
|
+
return {
|
|
1039
|
+
nodeId: "project.ensure",
|
|
1040
|
+
diagnostics: [{
|
|
1041
|
+
code: "PROJECT_REQUIRED",
|
|
1042
|
+
severity: "error",
|
|
1043
|
+
node_id: "project.ensure",
|
|
1044
|
+
field_path: "project.name",
|
|
1045
|
+
message: "This app needs an instance name so Run402 can create the project, public subdomain, and project-scoped mail host.",
|
|
1046
|
+
details: {
|
|
1047
|
+
manifest_path: manifest.manifestPath,
|
|
1048
|
+
template: "${input.name}",
|
|
1049
|
+
},
|
|
1050
|
+
}],
|
|
1051
|
+
nextActions: [{
|
|
1052
|
+
type: "set_project_name",
|
|
1053
|
+
code: "PROJECT_REQUIRED",
|
|
1054
|
+
node_id: "project.ensure",
|
|
1055
|
+
field_path: "project.name",
|
|
1056
|
+
message: "Retry with --name, for example: run402 up --name kysigned3 --yes.",
|
|
1057
|
+
command: "run402 up --name kysigned3 --yes",
|
|
1058
|
+
argv: ["run402", "up", "--name", "kysigned3", "--yes"],
|
|
1059
|
+
}],
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function missingRequiredSecrets(spec) {
|
|
1063
|
+
const missing = [];
|
|
1064
|
+
for (const [name, secret] of Object.entries(spec?.secrets ?? {})) {
|
|
1065
|
+
if (secret.required !== true)
|
|
1066
|
+
continue;
|
|
1067
|
+
const sourceEnv = secret.source_env ?? name;
|
|
1068
|
+
if (process.env[sourceEnv])
|
|
1069
|
+
continue;
|
|
1070
|
+
missing.push({
|
|
1071
|
+
name,
|
|
1072
|
+
sourceEnv,
|
|
1073
|
+
...(secret.description ? { description: secret.description } : {}),
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return missing;
|
|
1077
|
+
}
|
|
1078
|
+
function appBuildBlock(input, manifest) {
|
|
1079
|
+
const build = manifest.appSpec?.build;
|
|
1080
|
+
if (!build)
|
|
1081
|
+
return null;
|
|
1082
|
+
const mode = input.buildMode ?? build.mode ?? "local";
|
|
1083
|
+
const nodeId = `build.${mode}`;
|
|
1084
|
+
const shellCommand = build.commands?.find((command) => command.shell);
|
|
1085
|
+
if (shellCommand && input.allowShellBuild !== true) {
|
|
1086
|
+
return {
|
|
1087
|
+
nodeId,
|
|
1088
|
+
diagnostics: [{
|
|
1089
|
+
code: "BUILD_APPROVAL_REQUIRED",
|
|
1090
|
+
severity: "error",
|
|
1091
|
+
node_id: nodeId,
|
|
1092
|
+
field_path: `build.commands.${shellCommand.id}.shell`,
|
|
1093
|
+
message: "Shell-string build commands require explicit shell build approval.",
|
|
1094
|
+
}],
|
|
1095
|
+
nextActions: [{
|
|
1096
|
+
type: "approve_shell_build",
|
|
1097
|
+
code: "BUILD_APPROVAL_REQUIRED",
|
|
1098
|
+
node_id: nodeId,
|
|
1099
|
+
field_path: `build.commands.${shellCommand.id}.shell`,
|
|
1100
|
+
message: "Review the shell command, then retry with --allow-shell-build or convert it to argv.",
|
|
1101
|
+
}],
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
if (mode === "remote" || mode === "sandbox") {
|
|
1105
|
+
return {
|
|
1106
|
+
nodeId,
|
|
1107
|
+
diagnostics: [{
|
|
1108
|
+
code: "REMOTE_BUILD_UNSUPPORTED",
|
|
1109
|
+
severity: "error",
|
|
1110
|
+
node_id: nodeId,
|
|
1111
|
+
message: `${mode} build mode is planned but not available in this SDK build yet.`,
|
|
1112
|
+
}],
|
|
1113
|
+
nextActions: [{
|
|
1114
|
+
type: "choose_build_mode",
|
|
1115
|
+
code: "REMOTE_BUILD_UNSUPPORTED",
|
|
1116
|
+
node_id: nodeId,
|
|
1117
|
+
message: "Retry with --build-mode local after preparing the local toolchain, or wait for Run402 remote build support.",
|
|
1118
|
+
argv: ["run402", "up", "--build-mode", "local", "--yes"],
|
|
1119
|
+
}],
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
return null;
|
|
1123
|
+
}
|
|
730
1124
|
async function findManifest(workspaceDir) {
|
|
731
1125
|
for (const candidate of MANIFEST_CANDIDATES) {
|
|
732
1126
|
const path = join(workspaceDir, candidate);
|
|
@@ -753,6 +1147,24 @@ async function findExecutableManifest(workspaceDir) {
|
|
|
753
1147
|
}
|
|
754
1148
|
return null;
|
|
755
1149
|
}
|
|
1150
|
+
async function loadRun402AppManifest(path) {
|
|
1151
|
+
let raw;
|
|
1152
|
+
try {
|
|
1153
|
+
raw = await readFile(path, "utf-8");
|
|
1154
|
+
}
|
|
1155
|
+
catch (err) {
|
|
1156
|
+
throw new LocalError(`Failed to read Run402 app manifest '${path}': ${err.message}`, "loading Run402 app manifest", err);
|
|
1157
|
+
}
|
|
1158
|
+
try {
|
|
1159
|
+
return JSON.parse(raw);
|
|
1160
|
+
}
|
|
1161
|
+
catch (err) {
|
|
1162
|
+
throw new LocalError(`Run402 app manifest is not valid JSON: ${err.message}`, "parsing Run402 app manifest", {
|
|
1163
|
+
code: "APP_SPEC_INVALID",
|
|
1164
|
+
details: { path },
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
756
1168
|
function reviewedPlanRequirement(mode) {
|
|
757
1169
|
return typeof mode === "object" && mode.kind === "applyReviewed"
|
|
758
1170
|
? { planId: mode.planId, ...(mode.planFingerprint ? { planFingerprint: mode.planFingerprint } : {}) }
|