run402-mcp 3.5.7 → 3.6.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 +7 -5
- package/package.json +1 -1
- package/sdk/README.md +20 -2
- package/sdk/dist/actions.d.ts +121 -0
- package/sdk/dist/actions.d.ts.map +1 -0
- package/sdk/dist/actions.js +10 -0
- package/sdk/dist/actions.js.map +1 -0
- package/sdk/dist/index.d.ts +2 -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/allowance.d.ts +5 -1
- package/sdk/dist/namespaces/allowance.d.ts.map +1 -1
- package/sdk/dist/namespaces/allowance.js +6 -2
- package/sdk/dist/namespaces/allowance.js.map +1 -1
- package/sdk/dist/node/actions-node.d.ts +24 -0
- package/sdk/dist/node/actions-node.d.ts.map +1 -0
- package/sdk/dist/node/actions-node.js +801 -0
- package/sdk/dist/node/actions-node.js.map +1 -0
- package/sdk/dist/node/index.d.ts +6 -0
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +27 -8
- package/sdk/dist/node/index.js.map +1 -1
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { Run402Action } from "../actions.js";
|
|
5
|
+
import { LocalError } from "../errors.js";
|
|
6
|
+
import { loadDeployManifest } from "./deploy-manifest.js";
|
|
7
|
+
const DEFAULT_BOOTSTRAP_TIER = "prototype";
|
|
8
|
+
const MANIFEST_CANDIDATES = [
|
|
9
|
+
"run402.deploy.json",
|
|
10
|
+
"app.json",
|
|
11
|
+
];
|
|
12
|
+
const TIER_RANK = {
|
|
13
|
+
prototype: 1,
|
|
14
|
+
hobby: 2,
|
|
15
|
+
team: 3,
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Node implementation of the action runner. The public CLI should treat this
|
|
19
|
+
* as the orchestration kernel and stay a flag parser / renderer.
|
|
20
|
+
*/
|
|
21
|
+
export class NodeActions {
|
|
22
|
+
sdk;
|
|
23
|
+
opts;
|
|
24
|
+
constructor(sdk, opts = {}) {
|
|
25
|
+
this.sdk = sdk;
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
}
|
|
28
|
+
async run(input, opts = {}) {
|
|
29
|
+
const run = new ActionRun(input, opts, this.#targetKind());
|
|
30
|
+
try {
|
|
31
|
+
switch (input.type) {
|
|
32
|
+
case Run402Action.ProjectsProvision:
|
|
33
|
+
return await this.#runProjectsProvision(input, run);
|
|
34
|
+
case Run402Action.TierSet:
|
|
35
|
+
return await this.#runTierSet(input, run);
|
|
36
|
+
case Run402Action.Up:
|
|
37
|
+
return await this.#runUp(input, run);
|
|
38
|
+
default:
|
|
39
|
+
assertNever(input);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
run.failLast(err);
|
|
44
|
+
if (err instanceof LocalError)
|
|
45
|
+
throw err;
|
|
46
|
+
throw withActionDetails(new LocalError(err instanceof Error ? err.message : String(err), "running Run402 action", {
|
|
47
|
+
cause: err,
|
|
48
|
+
code: "RUN402_ACTION_FAILED",
|
|
49
|
+
details: { action: input.type, steps: run.steps },
|
|
50
|
+
}), run);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async up(input = {}, opts) {
|
|
54
|
+
return this.run({ type: Run402Action.Up, ...input }, opts);
|
|
55
|
+
}
|
|
56
|
+
#targetKind() {
|
|
57
|
+
return this.opts.targetKind ?? "unknown";
|
|
58
|
+
}
|
|
59
|
+
async #runProjectsProvision(input, run) {
|
|
60
|
+
if (this.#targetKind() === "core") {
|
|
61
|
+
throw run.error("Project provisioning through the Cloud gateway is unavailable for a Run402 Core target.", "RUN402_CLOUD_ACTION_UNAVAILABLE", { action: input.type, target: this.#targetKind() });
|
|
62
|
+
}
|
|
63
|
+
const tier = input.tier ?? DEFAULT_BOOTSTRAP_TIER;
|
|
64
|
+
if (run.autoPrerequisites) {
|
|
65
|
+
await this.#ensureCloudTier(run, tier);
|
|
66
|
+
}
|
|
67
|
+
const idempotencyKey = input.idempotencyKey ?? run.rootIdempotencyKey;
|
|
68
|
+
const step = run.addStep({
|
|
69
|
+
action: Run402Action.ProjectsProvision,
|
|
70
|
+
description: "Provision Run402 project",
|
|
71
|
+
mutation: true,
|
|
72
|
+
auto: false,
|
|
73
|
+
details: {
|
|
74
|
+
tier,
|
|
75
|
+
name: input.name ?? null,
|
|
76
|
+
org_id: input.orgId ?? null,
|
|
77
|
+
idempotency_key: idempotencyKey ?? null,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
await run.approve(step, ["projects.provision"], "Provision a new Run402 Cloud project.");
|
|
81
|
+
if (run.dryRun) {
|
|
82
|
+
run.setState(step, "planned");
|
|
83
|
+
return run.result({});
|
|
84
|
+
}
|
|
85
|
+
run.setState(step, "running");
|
|
86
|
+
const result = await this.sdk.projects.provision({
|
|
87
|
+
tier,
|
|
88
|
+
name: input.name,
|
|
89
|
+
orgId: input.orgId,
|
|
90
|
+
idempotencyKey,
|
|
91
|
+
});
|
|
92
|
+
run.setState(step, "succeeded", { project_id: result.project_id });
|
|
93
|
+
return run.result(result);
|
|
94
|
+
}
|
|
95
|
+
async #runTierSet(input, run) {
|
|
96
|
+
if (this.#targetKind() === "core") {
|
|
97
|
+
throw run.error("Tier subscriptions are a Run402 Cloud capability and are skipped on Run402 Core targets.", "RUN402_CLOUD_ACTION_UNAVAILABLE", { action: input.type, target: this.#targetKind() });
|
|
98
|
+
}
|
|
99
|
+
if (run.autoPrerequisites) {
|
|
100
|
+
await this.#ensureAllowance(run);
|
|
101
|
+
}
|
|
102
|
+
const idempotencyKey = input.idempotencyKey ?? run.rootIdempotencyKey;
|
|
103
|
+
const step = run.addStep({
|
|
104
|
+
action: Run402Action.TierSet,
|
|
105
|
+
description: `Set Run402 tier to ${input.tier}`,
|
|
106
|
+
mutation: true,
|
|
107
|
+
auto: false,
|
|
108
|
+
details: { tier: input.tier, idempotency_key: idempotencyKey ?? null },
|
|
109
|
+
});
|
|
110
|
+
await run.approve(step, ["tier.set"], `Subscribe, renew, or upgrade the ${input.tier} tier.`);
|
|
111
|
+
if (run.dryRun) {
|
|
112
|
+
run.setState(step, "planned");
|
|
113
|
+
return run.result({});
|
|
114
|
+
}
|
|
115
|
+
run.setState(step, "running");
|
|
116
|
+
const result = await this.sdk.tier.set(input.tier, idempotencyKey ? { idempotencyKey } : {});
|
|
117
|
+
run.setState(step, "succeeded", { tier: result.tier, action: result.action });
|
|
118
|
+
return run.result(result);
|
|
119
|
+
}
|
|
120
|
+
async #runUp(input, run) {
|
|
121
|
+
const workspaceDir = resolvePath(this.opts.cwd ?? process.cwd(), input.dir ?? ".");
|
|
122
|
+
const manifest = await this.#discoverAndValidateManifest(input, workspaceDir, run);
|
|
123
|
+
if (this.#targetKind() !== "core") {
|
|
124
|
+
await this.#ensureCloudTier(run, input.tier ?? DEFAULT_BOOTSTRAP_TIER);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
run.skipStep({
|
|
128
|
+
action: Run402Action.TierSet,
|
|
129
|
+
description: "Skip Cloud allowance and tier prerequisites for Run402 Core",
|
|
130
|
+
mutation: false,
|
|
131
|
+
auto: true,
|
|
132
|
+
details: { target: this.#targetKind() },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const resolved = await this.#resolveProject(input, manifest, workspaceDir, run);
|
|
136
|
+
const normalized = await loadDeployManifest(manifest.manifestPath, {
|
|
137
|
+
project: resolved.projectId,
|
|
138
|
+
});
|
|
139
|
+
if (!hasDeployableContent(normalized.spec)) {
|
|
140
|
+
throw run.error("Deploy manifest contains no deployable sections.", "MANIFEST_EMPTY", { manifest_path: manifest.manifestPath });
|
|
141
|
+
}
|
|
142
|
+
const releaseSpec = normalized.spec;
|
|
143
|
+
if (resolved.shouldWriteLink) {
|
|
144
|
+
await this.#writeWorkspaceProjectLink(resolved.linkPath, {
|
|
145
|
+
schema_version: "run402.workspace-project.v1",
|
|
146
|
+
project_id: resolved.projectId,
|
|
147
|
+
...(input.name ? { name: input.name } : {}),
|
|
148
|
+
target: { kind: this.#targetKind() },
|
|
149
|
+
created_at: resolved.link?.created_at ?? new Date().toISOString(),
|
|
150
|
+
updated_at: new Date().toISOString(),
|
|
151
|
+
}, resolved.link, run);
|
|
152
|
+
}
|
|
153
|
+
const deployStep = run.addStep({
|
|
154
|
+
action: "deploy.apply",
|
|
155
|
+
description: "Apply deploy manifest",
|
|
156
|
+
mutation: true,
|
|
157
|
+
auto: false,
|
|
158
|
+
details: {
|
|
159
|
+
project_id: resolved.projectId,
|
|
160
|
+
manifest_path: manifest.manifestPath,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (run.dryRun) {
|
|
164
|
+
run.setState(deployStep, "planned");
|
|
165
|
+
return run.result({
|
|
166
|
+
project_id: resolved.projectId,
|
|
167
|
+
manifest_path: manifest.manifestPath,
|
|
168
|
+
...(resolved.shouldWriteLink ? { workspace_link_path: resolved.linkPath } : {}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
run.setState(deployStep, "running");
|
|
172
|
+
await this.#assertLocalProjectKeys(resolved.projectId, run);
|
|
173
|
+
const scoped = await this.sdk.project(resolved.projectId);
|
|
174
|
+
const deploy = await scoped.apply(releaseSpec, {
|
|
175
|
+
idempotencyKey: normalized.idempotencyKey ?? manifest.idempotencyKey ?? run.childKey("deploy.apply"),
|
|
176
|
+
allowWarnings: input.allowWarnings,
|
|
177
|
+
allowWarningCodes: input.allowWarningCodes,
|
|
178
|
+
target: this.#targetKind() === "core" ? "core" : "cloud",
|
|
179
|
+
onEvent: (event) => {
|
|
180
|
+
run.options.onEvent?.({
|
|
181
|
+
type: "action.step",
|
|
182
|
+
action: Run402Action.Up,
|
|
183
|
+
step: {
|
|
184
|
+
...deployStep,
|
|
185
|
+
details: { ...deployStep.details, deploy_event: event.type },
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
run.setState(deployStep, "succeeded", {
|
|
191
|
+
release_id: deploy.release_id,
|
|
192
|
+
operation_id: deploy.operation_id,
|
|
193
|
+
});
|
|
194
|
+
return run.result({
|
|
195
|
+
project_id: resolved.projectId,
|
|
196
|
+
manifest_path: manifest.manifestPath,
|
|
197
|
+
...(resolved.shouldWriteLink ? { workspace_link_path: resolved.linkPath } : {}),
|
|
198
|
+
deploy,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async #discoverAndValidateManifest(input, workspaceDir, run) {
|
|
202
|
+
const step = run.addStep({
|
|
203
|
+
action: "deploy.discover",
|
|
204
|
+
description: "Discover and validate deploy manifest",
|
|
205
|
+
mutation: false,
|
|
206
|
+
auto: true,
|
|
207
|
+
details: { dir: workspaceDir, manifest: input.manifest ?? null },
|
|
208
|
+
});
|
|
209
|
+
run.setState(step, "running");
|
|
210
|
+
const manifestPath = input.manifest
|
|
211
|
+
? resolveMaybe(workspaceDir, input.manifest)
|
|
212
|
+
: await findManifest(workspaceDir);
|
|
213
|
+
if (!manifestPath) {
|
|
214
|
+
throw run.error("No deploy manifest found. Add run402.deploy.json or app.json, or pass --manifest.", "UP_MANIFEST_REQUIRED", { dir: workspaceDir, candidates: MANIFEST_CANDIDATES });
|
|
215
|
+
}
|
|
216
|
+
const loaded = await loadDeployManifest(manifestPath, {
|
|
217
|
+
...(input.projectId
|
|
218
|
+
? { project: input.projectId }
|
|
219
|
+
: { defaultProject: "prj_up_preflight_placeholder" }),
|
|
220
|
+
});
|
|
221
|
+
if (!hasDeployableContent(loaded.spec)) {
|
|
222
|
+
throw run.error("Deploy manifest contains no deployable sections.", "MANIFEST_EMPTY", { manifest_path: manifestPath });
|
|
223
|
+
}
|
|
224
|
+
run.setState(step, "succeeded", {
|
|
225
|
+
manifest_path: manifestPath,
|
|
226
|
+
project_id: loaded.spec.project,
|
|
227
|
+
idempotency_key: loaded.idempotencyKey ?? null,
|
|
228
|
+
});
|
|
229
|
+
return {
|
|
230
|
+
manifestPath,
|
|
231
|
+
releaseSpec: loaded.spec,
|
|
232
|
+
idempotencyKey: loaded.idempotencyKey,
|
|
233
|
+
manifestProjectId: loaded.spec.project === "prj_up_preflight_placeholder"
|
|
234
|
+
? undefined
|
|
235
|
+
: loaded.spec.project,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async #ensureAllowance(run, opts = { fund: false }) {
|
|
239
|
+
const status = await this.sdk.allowance.status();
|
|
240
|
+
if (status.configured) {
|
|
241
|
+
run.skipStep({
|
|
242
|
+
action: "allowance.create",
|
|
243
|
+
description: "Local allowance already exists",
|
|
244
|
+
mutation: false,
|
|
245
|
+
auto: true,
|
|
246
|
+
details: { address: status.address, path: status.path ?? null },
|
|
247
|
+
});
|
|
248
|
+
if (!opts.fund)
|
|
249
|
+
return;
|
|
250
|
+
if (status.faucet_used) {
|
|
251
|
+
run.skipStep({
|
|
252
|
+
action: "allowance.faucet",
|
|
253
|
+
description: "Allowance faucet marker already present",
|
|
254
|
+
mutation: false,
|
|
255
|
+
auto: true,
|
|
256
|
+
details: { address: status.address, last_faucet: status.lastFaucet ?? null },
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const createStep = run.addStep({
|
|
263
|
+
action: "allowance.create",
|
|
264
|
+
description: "Create local allowance",
|
|
265
|
+
mutation: true,
|
|
266
|
+
auto: true,
|
|
267
|
+
});
|
|
268
|
+
await run.approve(createStep, ["allowance.create"], "Create a local allowance wallet.");
|
|
269
|
+
if (run.dryRun) {
|
|
270
|
+
run.setState(createStep, "planned");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
run.setState(createStep, "running");
|
|
274
|
+
const created = await this.sdk.allowance.create();
|
|
275
|
+
run.setState(createStep, "succeeded", {
|
|
276
|
+
address: created.address,
|
|
277
|
+
path: created.path ?? null,
|
|
278
|
+
});
|
|
279
|
+
if (!opts.fund)
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const faucetStep = run.addStep({
|
|
283
|
+
action: "allowance.faucet",
|
|
284
|
+
description: "Request testnet faucet funds for allowance",
|
|
285
|
+
mutation: true,
|
|
286
|
+
auto: true,
|
|
287
|
+
details: { idempotency_key: run.childKey("allowance.faucet") },
|
|
288
|
+
});
|
|
289
|
+
await run.approve(faucetStep, ["allowance.faucet"], "Request testnet USDC for the local allowance.");
|
|
290
|
+
if (run.dryRun) {
|
|
291
|
+
run.setState(faucetStep, "planned");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
run.setState(faucetStep, "running");
|
|
295
|
+
const faucet = await this.sdk.allowance.faucet({
|
|
296
|
+
idempotencyKey: run.childKey("allowance.faucet"),
|
|
297
|
+
});
|
|
298
|
+
run.setState(faucetStep, "succeeded", {
|
|
299
|
+
transaction_hash: faucet.transactionHash,
|
|
300
|
+
amount: faucet.amount,
|
|
301
|
+
network: faucet.network,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async #ensureCloudTier(run, desiredTier) {
|
|
305
|
+
await this.#ensureAllowance(run, { fund: false });
|
|
306
|
+
if (run.dryRun) {
|
|
307
|
+
const faucetStep = run.addStep({
|
|
308
|
+
action: "allowance.faucet",
|
|
309
|
+
description: "Request testnet faucet funds if tier payment is needed",
|
|
310
|
+
mutation: true,
|
|
311
|
+
auto: true,
|
|
312
|
+
});
|
|
313
|
+
await run.approve(faucetStep, ["allowance.faucet"], "Request testnet USDC if tier bootstrap needs payment.");
|
|
314
|
+
run.setState(faucetStep, "planned");
|
|
315
|
+
const step = run.addStep({
|
|
316
|
+
action: Run402Action.TierSet,
|
|
317
|
+
description: `Ensure active ${desiredTier} tier`,
|
|
318
|
+
mutation: true,
|
|
319
|
+
auto: true,
|
|
320
|
+
details: {
|
|
321
|
+
tier: desiredTier,
|
|
322
|
+
idempotency_key: run.childKey("tier.set"),
|
|
323
|
+
reason: "dry_run",
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
await run.approve(step, ["tier.set"], `Subscribe to ${desiredTier} if no active tier exists.`);
|
|
327
|
+
run.setState(step, "planned");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
let status;
|
|
331
|
+
try {
|
|
332
|
+
status = await this.sdk.tier.status();
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
status = null;
|
|
336
|
+
}
|
|
337
|
+
const current = normalizeTier(status?.tier);
|
|
338
|
+
if (status?.active && current && TIER_RANK[current] >= TIER_RANK[desiredTier]) {
|
|
339
|
+
run.skipStep({
|
|
340
|
+
action: Run402Action.TierSet,
|
|
341
|
+
description: "Active tier already satisfies bootstrap requirement",
|
|
342
|
+
mutation: false,
|
|
343
|
+
auto: true,
|
|
344
|
+
details: { current_tier: current, desired_tier: desiredTier },
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (status?.active && current && TIER_RANK[current] > TIER_RANK[desiredTier]) {
|
|
349
|
+
run.skipStep({
|
|
350
|
+
action: Run402Action.TierSet,
|
|
351
|
+
description: "Existing active tier is higher than bootstrap default",
|
|
352
|
+
mutation: false,
|
|
353
|
+
auto: true,
|
|
354
|
+
details: { current_tier: current, desired_tier: desiredTier },
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const idempotencyKey = run.childKey("tier.set");
|
|
359
|
+
await this.#ensureAllowance(run, { fund: true });
|
|
360
|
+
const step = run.addStep({
|
|
361
|
+
action: Run402Action.TierSet,
|
|
362
|
+
description: `Ensure active ${desiredTier} tier`,
|
|
363
|
+
mutation: true,
|
|
364
|
+
auto: true,
|
|
365
|
+
details: {
|
|
366
|
+
tier: desiredTier,
|
|
367
|
+
current_tier: current ?? null,
|
|
368
|
+
active: status?.active ?? null,
|
|
369
|
+
idempotency_key: idempotencyKey,
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
await run.approve(step, ["tier.set"], `Subscribe, renew, or upgrade to ${desiredTier}.`);
|
|
373
|
+
run.setState(step, "running");
|
|
374
|
+
const result = await this.sdk.tier.set(desiredTier, { idempotencyKey });
|
|
375
|
+
run.setState(step, "succeeded", { tier: result.tier, action: result.action });
|
|
376
|
+
}
|
|
377
|
+
async #resolveProject(input, manifest, workspaceDir, run) {
|
|
378
|
+
const linkPath = join(workspaceDir, ".run402", "project.json");
|
|
379
|
+
const link = await readWorkspaceProjectLink(linkPath);
|
|
380
|
+
const step = run.addStep({
|
|
381
|
+
action: "project.resolve",
|
|
382
|
+
description: "Resolve project for workspace",
|
|
383
|
+
mutation: false,
|
|
384
|
+
auto: true,
|
|
385
|
+
details: {
|
|
386
|
+
explicit_project_id: input.projectId ?? null,
|
|
387
|
+
linked_project_id: link?.project_id ?? null,
|
|
388
|
+
manifest_project_id: manifest.manifestProjectId ?? null,
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
run.setState(step, "running");
|
|
392
|
+
if (input.name && link?.name && input.name !== link.name) {
|
|
393
|
+
throw run.error("--name is only used when creating/linking a project and conflicts with the workspace link name.", "RUN402_NAME_PROJECT_CONFLICT", { name: input.name, linked_name: link.name, link_path: linkPath });
|
|
394
|
+
}
|
|
395
|
+
let projectId = null;
|
|
396
|
+
let source = null;
|
|
397
|
+
let shouldWriteLink = false;
|
|
398
|
+
if (input.projectId) {
|
|
399
|
+
projectId = input.projectId;
|
|
400
|
+
source = "explicit";
|
|
401
|
+
shouldWriteLink = !link || link.project_id !== projectId;
|
|
402
|
+
}
|
|
403
|
+
else if (link?.project_id) {
|
|
404
|
+
projectId = link.project_id;
|
|
405
|
+
source = "workspace_link";
|
|
406
|
+
}
|
|
407
|
+
else if (manifest.manifestProjectId) {
|
|
408
|
+
projectId = manifest.manifestProjectId;
|
|
409
|
+
source = "manifest";
|
|
410
|
+
}
|
|
411
|
+
if (projectId && manifest.manifestProjectId && manifest.manifestProjectId !== projectId) {
|
|
412
|
+
throw run.error(`Project conflict: resolved ${projectId} from ${source}, but manifest declares ${manifest.manifestProjectId}.`, "RUN402_PROJECT_CONFLICT", {
|
|
413
|
+
resolved_project_id: projectId,
|
|
414
|
+
resolved_source: source,
|
|
415
|
+
manifest_project_id: manifest.manifestProjectId,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (projectId) {
|
|
419
|
+
run.setState(step, "succeeded", { project_id: projectId, source, link_path: linkPath });
|
|
420
|
+
return { projectId, source: source ?? "explicit", link, linkPath, shouldWriteLink };
|
|
421
|
+
}
|
|
422
|
+
if (input.name) {
|
|
423
|
+
if (this.#targetKind() === "core") {
|
|
424
|
+
throw run.error("Run402 Core cannot provision a Cloud project during `up`. Pass --project or add project_id to the manifest.", "RUN402_CORE_PROJECT_REQUIRED", { target: this.#targetKind() });
|
|
425
|
+
}
|
|
426
|
+
const idempotencyKey = run.childKey("projects.provision");
|
|
427
|
+
const provisionStep = run.addStep({
|
|
428
|
+
action: Run402Action.ProjectsProvision,
|
|
429
|
+
description: "Provision project for workspace",
|
|
430
|
+
mutation: true,
|
|
431
|
+
auto: true,
|
|
432
|
+
details: {
|
|
433
|
+
name: input.name,
|
|
434
|
+
tier: input.tier ?? DEFAULT_BOOTSTRAP_TIER,
|
|
435
|
+
org_id: input.orgId ?? null,
|
|
436
|
+
idempotency_key: idempotencyKey,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
await run.approve(provisionStep, ["projects.provision"], `Provision project '${input.name}'.`);
|
|
440
|
+
if (run.dryRun) {
|
|
441
|
+
run.setState(provisionStep, "planned");
|
|
442
|
+
run.setState(step, "planned", { source: "created", link_path: linkPath });
|
|
443
|
+
return {
|
|
444
|
+
projectId: "prj_planned",
|
|
445
|
+
source: "created",
|
|
446
|
+
link,
|
|
447
|
+
linkPath,
|
|
448
|
+
shouldWriteLink: true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
run.setState(provisionStep, "running");
|
|
452
|
+
const provisioned = await this.sdk.projects.provision({
|
|
453
|
+
name: input.name,
|
|
454
|
+
tier: input.tier ?? DEFAULT_BOOTSTRAP_TIER,
|
|
455
|
+
orgId: input.orgId,
|
|
456
|
+
idempotencyKey,
|
|
457
|
+
});
|
|
458
|
+
run.setState(provisionStep, "succeeded", { project_id: provisioned.project_id });
|
|
459
|
+
run.setState(step, "succeeded", {
|
|
460
|
+
project_id: provisioned.project_id,
|
|
461
|
+
source: "created",
|
|
462
|
+
link_path: linkPath,
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
projectId: provisioned.project_id,
|
|
466
|
+
source: "created",
|
|
467
|
+
link,
|
|
468
|
+
linkPath,
|
|
469
|
+
shouldWriteLink: true,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const active = await this.sdk.projects.active();
|
|
473
|
+
if (active) {
|
|
474
|
+
const activeStep = run.addStep({
|
|
475
|
+
action: "project.resolve",
|
|
476
|
+
description: "Use active project for this workspace",
|
|
477
|
+
mutation: false,
|
|
478
|
+
auto: true,
|
|
479
|
+
details: { active_project_id: active },
|
|
480
|
+
});
|
|
481
|
+
await run.approve(activeStep, [], `Use active project ${active} and link this workspace to it.`);
|
|
482
|
+
run.setState(activeStep, run.dryRun ? "planned" : "succeeded", {
|
|
483
|
+
project_id: active,
|
|
484
|
+
source: "active",
|
|
485
|
+
});
|
|
486
|
+
run.setState(step, run.dryRun ? "planned" : "succeeded", {
|
|
487
|
+
project_id: active,
|
|
488
|
+
source: "active",
|
|
489
|
+
link_path: linkPath,
|
|
490
|
+
});
|
|
491
|
+
return {
|
|
492
|
+
projectId: active,
|
|
493
|
+
source: "active",
|
|
494
|
+
link,
|
|
495
|
+
linkPath,
|
|
496
|
+
shouldWriteLink: true,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
throw run.error("No project is configured for this workspace. Pass --project, add project_id to the manifest, or pass --name to create one.", "RUN402_PROJECT_REQUIRED", { link_path: linkPath, manifest_path: manifest.manifestPath });
|
|
500
|
+
}
|
|
501
|
+
async #writeWorkspaceProjectLink(path, link, expectedExisting, run) {
|
|
502
|
+
const step = run.addStep({
|
|
503
|
+
action: "workspace.link.write",
|
|
504
|
+
description: "Write workspace project link",
|
|
505
|
+
mutation: true,
|
|
506
|
+
auto: true,
|
|
507
|
+
details: { path, project_id: link.project_id, name: link.name ?? null },
|
|
508
|
+
});
|
|
509
|
+
await run.approve(step, ["workspace.link.write"], `Write ${shortPath(path)}.`);
|
|
510
|
+
if (run.dryRun) {
|
|
511
|
+
run.setState(step, "planned");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
run.setState(step, "running");
|
|
515
|
+
await assertNotSymlinkPath(path);
|
|
516
|
+
const current = await readWorkspaceProjectLink(path);
|
|
517
|
+
if (!workspaceProjectLinksEqual(current, expectedExisting ?? null)) {
|
|
518
|
+
throw run.error("Workspace project link changed while `up` was running; refusing to overwrite it.", "RUN402_WORKSPACE_LINK_CONFLICT", {
|
|
519
|
+
path,
|
|
520
|
+
expected_project_id: expectedExisting?.project_id ?? null,
|
|
521
|
+
current_project_id: current?.project_id ?? null,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
await mkdir(dirname(path), { recursive: true });
|
|
525
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
526
|
+
await writeFile(tmp, `${JSON.stringify(link, null, 2)}\n`, { mode: 0o600 });
|
|
527
|
+
try {
|
|
528
|
+
await rename(tmp, path);
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
await rm(tmp, { force: true });
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
534
|
+
run.setState(step, "succeeded", { path });
|
|
535
|
+
}
|
|
536
|
+
async #assertLocalProjectKeys(projectId, run) {
|
|
537
|
+
try {
|
|
538
|
+
await this.sdk.projects.keys(projectId);
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
throw withActionDetails(new LocalError(`Project ${projectId} is selected, but its keys are not available locally. Use a project in the local keystore or provision/link from this machine.`, "resolving project keys for up", {
|
|
542
|
+
cause: err,
|
|
543
|
+
code: "RUN402_PROJECT_KEYS_REQUIRED",
|
|
544
|
+
details: { project_id: projectId, steps: run.steps },
|
|
545
|
+
}), run);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
class ActionRun {
|
|
550
|
+
input;
|
|
551
|
+
options;
|
|
552
|
+
target;
|
|
553
|
+
steps = [];
|
|
554
|
+
dryRun;
|
|
555
|
+
autoPrerequisites;
|
|
556
|
+
approval;
|
|
557
|
+
constructor(input, options, target) {
|
|
558
|
+
this.input = input;
|
|
559
|
+
this.options = options;
|
|
560
|
+
this.target = target;
|
|
561
|
+
this.dryRun = options.dryRun === true;
|
|
562
|
+
this.autoPrerequisites = options.autoPrerequisites ?? input.type === Run402Action.Up;
|
|
563
|
+
this.approval = options.approval ?? "never";
|
|
564
|
+
}
|
|
565
|
+
get rootIdempotencyKey() {
|
|
566
|
+
return this.input.idempotencyKey ?? this.options.idempotencyKey;
|
|
567
|
+
}
|
|
568
|
+
childKey(scope) {
|
|
569
|
+
const root = this.rootIdempotencyKey ?? deriveStableRootKey(this.input);
|
|
570
|
+
if (!root) {
|
|
571
|
+
throw this.error(`Cannot derive an idempotency key for recursive mutation ${scope}.`, "RUN402_IDEMPOTENCY_REQUIRED", { scope, action: this.input.type });
|
|
572
|
+
}
|
|
573
|
+
return `action:${root}:${scope}`;
|
|
574
|
+
}
|
|
575
|
+
addStep(init) {
|
|
576
|
+
const step = {
|
|
577
|
+
id: `step_${String(this.steps.length + 1).padStart(2, "0")}`,
|
|
578
|
+
action: init.action,
|
|
579
|
+
description: init.description,
|
|
580
|
+
state: "planned",
|
|
581
|
+
mutation: init.mutation,
|
|
582
|
+
auto: init.auto,
|
|
583
|
+
...(init.details ? { details: init.details } : {}),
|
|
584
|
+
};
|
|
585
|
+
this.steps.push(step);
|
|
586
|
+
this.emit(step);
|
|
587
|
+
return step;
|
|
588
|
+
}
|
|
589
|
+
skipStep(init) {
|
|
590
|
+
const step = this.addStep({
|
|
591
|
+
...init,
|
|
592
|
+
mutation: init.mutation ?? false,
|
|
593
|
+
});
|
|
594
|
+
this.setState(step, "skipped", init.details);
|
|
595
|
+
return step;
|
|
596
|
+
}
|
|
597
|
+
setState(step, state, details) {
|
|
598
|
+
step.state = state;
|
|
599
|
+
if (details)
|
|
600
|
+
step.details = { ...(step.details ?? {}), ...details };
|
|
601
|
+
this.emit(step);
|
|
602
|
+
}
|
|
603
|
+
failLast(err) {
|
|
604
|
+
const step = [...this.steps].reverse().find((candidate) => candidate.state === "running" || candidate.state === "planned");
|
|
605
|
+
if (!step)
|
|
606
|
+
return;
|
|
607
|
+
this.setState(step, "failed", {
|
|
608
|
+
error: err instanceof Error ? err.message : String(err),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async approve(step, mutations, message) {
|
|
612
|
+
if (!step.auto)
|
|
613
|
+
return;
|
|
614
|
+
if (this.dryRun)
|
|
615
|
+
return;
|
|
616
|
+
if (!step.mutation && mutations.length === 0) {
|
|
617
|
+
if (this.approval === "never" && step.action === "project.resolve") {
|
|
618
|
+
throw this.error("Using the active project for `up` requires approval. Pass --project, create a workspace link, or run with -y / an interactive prompt.", "RUN402_APPROVAL_REQUIRED", { step, mutations });
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (this.approval === "yes")
|
|
623
|
+
return;
|
|
624
|
+
if (this.approval !== "never") {
|
|
625
|
+
const approved = await this.approval.approve({
|
|
626
|
+
action: this.input.type,
|
|
627
|
+
step,
|
|
628
|
+
message,
|
|
629
|
+
mutations,
|
|
630
|
+
});
|
|
631
|
+
if (approved)
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
this.setState(step, "blocked", { approval_required: true, mutations });
|
|
635
|
+
throw this.error(`${message} Approval is required before this action can continue.`, "RUN402_APPROVAL_REQUIRED", { step, mutations });
|
|
636
|
+
}
|
|
637
|
+
result(result) {
|
|
638
|
+
return {
|
|
639
|
+
action: this.input.type,
|
|
640
|
+
dry_run: this.dryRun,
|
|
641
|
+
target: this.target,
|
|
642
|
+
steps: this.steps,
|
|
643
|
+
result,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
error(message, code, details) {
|
|
647
|
+
return withActionDetails(new LocalError(message, "running Run402 action", {
|
|
648
|
+
code,
|
|
649
|
+
details: { action: this.input.type, ...(details ?? {}), steps: this.steps },
|
|
650
|
+
}), this);
|
|
651
|
+
}
|
|
652
|
+
emit(step) {
|
|
653
|
+
this.options.onEvent?.({
|
|
654
|
+
type: "action.step",
|
|
655
|
+
action: this.input.type,
|
|
656
|
+
step: { ...step, details: step.details ? { ...step.details } : undefined },
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function withActionDetails(err, run) {
|
|
661
|
+
Object.defineProperty(err, "steps", {
|
|
662
|
+
value: run.steps,
|
|
663
|
+
enumerable: true,
|
|
664
|
+
configurable: true,
|
|
665
|
+
});
|
|
666
|
+
return err;
|
|
667
|
+
}
|
|
668
|
+
async function findManifest(workspaceDir) {
|
|
669
|
+
for (const candidate of MANIFEST_CANDIDATES) {
|
|
670
|
+
const path = join(workspaceDir, candidate);
|
|
671
|
+
try {
|
|
672
|
+
await lstat(path);
|
|
673
|
+
return path;
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
// try next candidate
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
async function readWorkspaceProjectLink(path) {
|
|
682
|
+
let raw;
|
|
683
|
+
try {
|
|
684
|
+
raw = await readFile(path, "utf-8");
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
let parsed;
|
|
690
|
+
try {
|
|
691
|
+
parsed = JSON.parse(raw);
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
throw new LocalError(`Workspace project link is not valid JSON: ${err.message}`, "reading workspace project link", { code: "RUN402_WORKSPACE_LINK_INVALID", details: { path } });
|
|
695
|
+
}
|
|
696
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
697
|
+
throw new LocalError("Workspace project link must be a JSON object.", "reading workspace project link", { code: "RUN402_WORKSPACE_LINK_INVALID", details: { path } });
|
|
698
|
+
}
|
|
699
|
+
const record = parsed;
|
|
700
|
+
if (record.schema_version !== "run402.workspace-project.v1" ||
|
|
701
|
+
typeof record.project_id !== "string" ||
|
|
702
|
+
!record.project_id) {
|
|
703
|
+
throw new LocalError("Workspace project link must contain schema_version=run402.workspace-project.v1 and project_id.", "reading workspace project link", { code: "RUN402_WORKSPACE_LINK_INVALID", details: { path } });
|
|
704
|
+
}
|
|
705
|
+
return record;
|
|
706
|
+
}
|
|
707
|
+
async function assertNotSymlinkPath(path) {
|
|
708
|
+
const parts = path.split(/[\\/]+/);
|
|
709
|
+
let cursor = isAbsolute(path) ? "/" : process.cwd();
|
|
710
|
+
for (const part of parts) {
|
|
711
|
+
if (!part || part === ".")
|
|
712
|
+
continue;
|
|
713
|
+
cursor = cursor === "/" ? `/${part}` : join(cursor, part);
|
|
714
|
+
try {
|
|
715
|
+
const stat = await lstat(cursor);
|
|
716
|
+
if (stat.isSymbolicLink()) {
|
|
717
|
+
throw new LocalError(`Refusing to write workspace project link through symlink: ${cursor}`, "writing workspace project link", { code: "RUN402_WORKSPACE_LINK_SYMLINK", details: { path, symlink: cursor } });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
if (err.code === "ENOENT") {
|
|
722
|
+
try {
|
|
723
|
+
await lstat(dirname(cursor));
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
// parent also absent; mkdir will create it later
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
throw err;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
await mkdir(dirname(path), { recursive: true });
|
|
735
|
+
try {
|
|
736
|
+
await lstat(path);
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
if (err.code !== "ENOENT")
|
|
740
|
+
throw err;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function resolveMaybe(baseDir, path) {
|
|
744
|
+
return isAbsolute(path) ? path : resolvePath(baseDir, path);
|
|
745
|
+
}
|
|
746
|
+
function shortPath(path) {
|
|
747
|
+
const rel = relative(process.cwd(), path);
|
|
748
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
749
|
+
}
|
|
750
|
+
function normalizeTier(tier) {
|
|
751
|
+
return tier === "prototype" || tier === "hobby" || tier === "team" ? tier : null;
|
|
752
|
+
}
|
|
753
|
+
function workspaceProjectLinksEqual(a, b) {
|
|
754
|
+
if (a === null || b === null)
|
|
755
|
+
return a === b;
|
|
756
|
+
return (a.schema_version === b.schema_version &&
|
|
757
|
+
a.project_id === b.project_id &&
|
|
758
|
+
(a.name ?? null) === (b.name ?? null) &&
|
|
759
|
+
(a.target?.kind ?? null) === (b.target?.kind ?? null) &&
|
|
760
|
+
(a.target?.api_base ?? null) === (b.target?.api_base ?? null));
|
|
761
|
+
}
|
|
762
|
+
function deriveStableRootKey(input) {
|
|
763
|
+
if (input.type === Run402Action.ProjectsProvision && input.name) {
|
|
764
|
+
return `projects.provision:${input.name}`;
|
|
765
|
+
}
|
|
766
|
+
if (input.type === Run402Action.Up) {
|
|
767
|
+
const stable = JSON.stringify({
|
|
768
|
+
type: input.type,
|
|
769
|
+
dir: input.dir ?? ".",
|
|
770
|
+
manifest: input.manifest ?? null,
|
|
771
|
+
projectId: input.projectId ?? null,
|
|
772
|
+
name: input.name ?? null,
|
|
773
|
+
tier: input.tier ?? DEFAULT_BOOTSTRAP_TIER,
|
|
774
|
+
orgId: input.orgId ?? null,
|
|
775
|
+
});
|
|
776
|
+
return `up:${createHash("sha256").update(stable).digest("hex").slice(0, 24)}`;
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
function hasDeployableContent(spec) {
|
|
781
|
+
const meaningful = ["database", "site", "functions", "secrets", "subdomains", "routes", "checks", "i18n", "assets"];
|
|
782
|
+
return meaningful.some((key) => hasContent(spec[key]));
|
|
783
|
+
}
|
|
784
|
+
function hasContent(value) {
|
|
785
|
+
if (value === null)
|
|
786
|
+
return true;
|
|
787
|
+
if (value === undefined)
|
|
788
|
+
return false;
|
|
789
|
+
if (Array.isArray(value))
|
|
790
|
+
return value.length > 0;
|
|
791
|
+
if (typeof value === "object") {
|
|
792
|
+
return Object.values(value).some(hasContent);
|
|
793
|
+
}
|
|
794
|
+
if (typeof value === "string")
|
|
795
|
+
return value.length > 0;
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
function assertNever(value) {
|
|
799
|
+
throw new LocalError(`Unknown Run402 action ${value.type}`, "running Run402 action", { code: "RUN402_ACTION_UNKNOWN" });
|
|
800
|
+
}
|
|
801
|
+
//# sourceMappingURL=actions-node.js.map
|