@m-kopa/launchpad-cli 0.54.0 → 0.55.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/CHANGELOG.md +25 -0
- package/dist/cli.js +1150 -534
- package/dist/commands/admin.d.ts +10 -0
- package/dist/commands/admin.d.ts.map +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deploy/deployment-status.d.ts.map +1 -1
- package/dist/deploy/git-files.d.ts +7 -11
- package/dist/deploy/git-files.d.ts.map +1 -1
- package/dist/deploy/redact-diagnostic.d.ts.map +1 -1
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/manifest/container-filesystem.d.ts +19 -0
- package/dist/manifest/container-filesystem.d.ts.map +1 -0
- package/dist/manifest/runtime-capability.d.ts +22 -0
- package/dist/manifest/runtime-capability.d.ts.map +1 -0
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/skills/launchpad-content-pr/SKILL.md +1 -1
- package/skills/launchpad-deploy/SKILL.md +1 -1
- package/skills/launchpad-deploy-status/SKILL.md +1 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-report/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -19,18 +19,20 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
20
|
|
|
21
21
|
// src/version.ts
|
|
22
|
-
var CLI_VERSION = "0.
|
|
22
|
+
var CLI_VERSION = "0.55.0";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
26
26
|
import * as path from "node:path";
|
|
27
27
|
var DEFAULT_BOT_URL = "https://launchpad-portal-bot.mkopa-launchpad.workers.dev";
|
|
28
28
|
var DEFAULT_AUTH_GATEWAY_URL = "https://auth.launchpad.m-kopa.us";
|
|
29
|
+
var DEFAULT_STATUS_PAGE_HOST = "status.launchpad.m-kopa.us";
|
|
29
30
|
function loadConfig(env = process.env) {
|
|
30
31
|
const rawBotUrl = env.LAUNCHPAD_BOT_URL ?? DEFAULT_BOT_URL;
|
|
31
32
|
const botUrl = rawBotUrl.replace(/\/+$/, "");
|
|
32
33
|
const rawGatewayUrl = env.LAUNCHPAD_AUTH_GATEWAY_URL ?? DEFAULT_AUTH_GATEWAY_URL;
|
|
33
34
|
const authGatewayUrl = rawGatewayUrl.replace(/\/+$/, "");
|
|
35
|
+
const statusPageHost = (env.LAUNCHPAD_STATUS_PAGE_HOST ?? DEFAULT_STATUS_PAGE_HOST).trim().toLowerCase();
|
|
34
36
|
const authLegacy = env.LAUNCHPAD_AUTH_LEGACY === "1";
|
|
35
37
|
const sessionPath = env.LAUNCHPAD_SESSION_PATH ?? path.join(os.homedir(), ".launchpad", "session.json");
|
|
36
38
|
const cacheDir = env.LAUNCHPAD_CACHE_DIR ?? path.join(os.homedir(), ".launchpad", "cache");
|
|
@@ -40,6 +42,7 @@ function loadConfig(env = process.env) {
|
|
|
40
42
|
return {
|
|
41
43
|
botUrl,
|
|
42
44
|
authGatewayUrl,
|
|
45
|
+
statusPageHost,
|
|
43
46
|
authLegacy,
|
|
44
47
|
sessionPath,
|
|
45
48
|
cacheDir,
|
|
@@ -1867,7 +1870,7 @@ function describe10(e) {
|
|
|
1867
1870
|
}
|
|
1868
1871
|
|
|
1869
1872
|
// src/commands/deploy.ts
|
|
1870
|
-
import { existsSync as
|
|
1873
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1871
1874
|
import * as path8 from "node:path";
|
|
1872
1875
|
|
|
1873
1876
|
// src/bundle/orchestrate.ts
|
|
@@ -2657,6 +2660,7 @@ var ManifestSchema = z.object({
|
|
|
2657
2660
|
app: AppBoundarySchema.optional(),
|
|
2658
2661
|
verify: VerifySchema.optional(),
|
|
2659
2662
|
confine_origin: z.boolean().optional(),
|
|
2663
|
+
origin: z.enum(["pages", "dispatch"]).optional(),
|
|
2660
2664
|
catalogue: z.object({ listed: z.boolean().default(true) }).strict().optional(),
|
|
2661
2665
|
sensitive: z.boolean().optional()
|
|
2662
2666
|
}).strict().superRefine((m, ctx) => {
|
|
@@ -3047,7 +3051,9 @@ var PLATFORM_SEEDED_GITHUB_PATHS = new Set([
|
|
|
3047
3051
|
".github/scripts/parse-tsc.mjs",
|
|
3048
3052
|
".github/scripts/parse-vitest.mjs",
|
|
3049
3053
|
".github/scripts/parse-gitleaks.mjs",
|
|
3050
|
-
".github/scripts/parse-bun-audit.mjs"
|
|
3054
|
+
".github/scripts/parse-bun-audit.mjs",
|
|
3055
|
+
".github/scripts/parse-html-validate.mjs",
|
|
3056
|
+
".github/scripts/parse-hadolint.mjs"
|
|
3051
3057
|
]);
|
|
3052
3058
|
function checkDenyList(path7) {
|
|
3053
3059
|
const segments = path7.split("/");
|
|
@@ -3290,6 +3296,7 @@ var SpecSchema = z2.object({
|
|
|
3290
3296
|
app: AppBoundarySchema.optional(),
|
|
3291
3297
|
verify: VerifySchema.optional(),
|
|
3292
3298
|
confine_origin: z2.boolean().optional(),
|
|
3299
|
+
origin: z2.enum(["pages", "dispatch"]).optional(),
|
|
3293
3300
|
sensitive: z2.boolean().optional(),
|
|
3294
3301
|
catalogue: z2.object({ listed: z2.boolean().default(true) }).strict().optional(),
|
|
3295
3302
|
env_vars: z2.record(z2.string().regex(ENV_NAME_REGEX, "env_vars keys must be UPPER_SNAKE_CASE"), EnvVarSchema).optional(),
|
|
@@ -3335,6 +3342,43 @@ var SpecSchema = z2.object({
|
|
|
3335
3342
|
});
|
|
3336
3343
|
}
|
|
3337
3344
|
}
|
|
3345
|
+
if (s.origin === "dispatch") {
|
|
3346
|
+
if (isContainer) {
|
|
3347
|
+
ctx.addIssue({
|
|
3348
|
+
code: z2.ZodIssueCode.custom,
|
|
3349
|
+
path: ["origin"],
|
|
3350
|
+
message: "origin: dispatch is not available for container apps — they run on the container runtime (ADR 0015/0016), not in a Workers dispatch namespace."
|
|
3351
|
+
});
|
|
3352
|
+
}
|
|
3353
|
+
if ((s.auth ?? "access") !== "gateway") {
|
|
3354
|
+
ctx.addIssue({
|
|
3355
|
+
code: z2.ZodIssueCode.custom,
|
|
3356
|
+
path: ["origin"],
|
|
3357
|
+
message: "origin: dispatch requires an explicit auth: gateway (auth defaults to 'access' when omitted). A dispatch-namespace Worker has no public hostname for Cloudflare Access to gate, so the gateway must be the sole gate."
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
if (s.confine_origin !== undefined) {
|
|
3361
|
+
ctx.addIssue({
|
|
3362
|
+
code: z2.ZodIssueCode.custom,
|
|
3363
|
+
path: ["confine_origin"],
|
|
3364
|
+
message: "confine_origin has no meaning with origin: dispatch — there is no public pages.dev apex to confine. Remove it."
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
if (s.env_vars !== undefined) {
|
|
3368
|
+
ctx.addIssue({
|
|
3369
|
+
code: z2.ZodIssueCode.custom,
|
|
3370
|
+
path: ["env_vars"],
|
|
3371
|
+
message: "env_vars are not yet supported with origin: dispatch — a dispatch workspace has no Pages module to apply them, and accepting them here would silently drop them. Worker-level vars land with the v2 deploy path."
|
|
3372
|
+
});
|
|
3373
|
+
}
|
|
3374
|
+
if (s.build !== undefined) {
|
|
3375
|
+
ctx.addIssue({
|
|
3376
|
+
code: z2.ZodIssueCode.custom,
|
|
3377
|
+
path: ["build"],
|
|
3378
|
+
message: "build is not yet supported with origin: dispatch — a dispatch workspace has no Pages module to carry it, and the v2 deploy path that will consume it is not shipped yet. Accepting it here would silently drop it."
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3338
3382
|
if (!isContainer && s.containers !== undefined) {
|
|
3339
3383
|
ctx.addIssue({
|
|
3340
3384
|
code: z2.ZodIssueCode.custom,
|
|
@@ -3428,6 +3472,19 @@ var ManifestV2Schema = z2.object({
|
|
|
3428
3472
|
spec: SpecSchema,
|
|
3429
3473
|
status: StatusSchema.optional()
|
|
3430
3474
|
}).strict();
|
|
3475
|
+
function parseManifestV2(input) {
|
|
3476
|
+
const result = ManifestV2Schema.safeParse(input);
|
|
3477
|
+
if (result.success) {
|
|
3478
|
+
return { kind: "ok", manifest: result.data };
|
|
3479
|
+
}
|
|
3480
|
+
return {
|
|
3481
|
+
kind: "schema-error",
|
|
3482
|
+
issues: result.error.issues.map((issue) => ({
|
|
3483
|
+
path: issue.path.map(String).join(".") || "(root)",
|
|
3484
|
+
message: issue.message
|
|
3485
|
+
}))
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3431
3488
|
// ../launchpad-engine/dist/secrets-audit.js
|
|
3432
3489
|
function auditSecretPresence(bindings, surfaces) {
|
|
3433
3490
|
const byId = new Map(surfaces.map((s) => [s.id, s]));
|
|
@@ -3547,298 +3604,569 @@ function deriveResources(m) {
|
|
|
3547
3604
|
}
|
|
3548
3605
|
return items;
|
|
3549
3606
|
}
|
|
3550
|
-
// ../launchpad-engine/dist/
|
|
3551
|
-
var
|
|
3552
|
-
var
|
|
3553
|
-
|
|
3554
|
-
|
|
3607
|
+
// ../launchpad-engine/dist/container-runtime-capability.js
|
|
3608
|
+
var MAX_PATH_BYTES = 256;
|
|
3609
|
+
var MAX_ENV_NAMES = 64;
|
|
3610
|
+
var MAX_ENV_NAME_BYTES = 64;
|
|
3611
|
+
var CONTAINER_NAME = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
3612
|
+
var ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
3613
|
+
var IMAGE_SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
3614
|
+
var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
3615
|
+
var RESERVED_ENV_PREFIXES = ["LAUNCHPAD_", "CF_ACCESS_", "CLOUDFLARE_"];
|
|
3616
|
+
var RESERVED_ENV_NAMES = new Set([
|
|
3617
|
+
"BINDINGS_PROXY_SECRET",
|
|
3618
|
+
"BINDINGS_PROXY_URL"
|
|
3619
|
+
]);
|
|
3620
|
+
function validateDeploymentCapability(manifest) {
|
|
3621
|
+
const view = deploymentView(manifest);
|
|
3622
|
+
if (view.type !== "container") {
|
|
3555
3623
|
return {
|
|
3556
|
-
kind: "
|
|
3557
|
-
|
|
3624
|
+
kind: "supported",
|
|
3625
|
+
manifestValid: true,
|
|
3626
|
+
runtimeSupported: true,
|
|
3627
|
+
runtime: "pages"
|
|
3558
3628
|
};
|
|
3559
3629
|
}
|
|
3560
|
-
|
|
3561
|
-
|
|
3630
|
+
if (view.containers.length !== 1) {
|
|
3631
|
+
return unsupported("unsupported_container_topology", "containers", `the container runtime supports exactly one container; found ${view.containers.length}`);
|
|
3632
|
+
}
|
|
3633
|
+
const container = view.containers[0];
|
|
3634
|
+
if (byteLength(container.name) > 32 || container.name.length < 2 || !CONTAINER_NAME.test(container.name)) {
|
|
3635
|
+
return unsupported("invalid_container_name", "containers.0.name", "container name must be a 2-32 character lowercase slug");
|
|
3636
|
+
}
|
|
3637
|
+
if (container.port < 1024 || container.port > 65535) {
|
|
3638
|
+
return unsupported("unsupported_container_port", "containers.0.port", "container port must be between 1024 and 65535");
|
|
3639
|
+
}
|
|
3640
|
+
if (!validImagePath(container.image)) {
|
|
3641
|
+
return unsupported("invalid_container_image", "containers.0.image", "container image must be a contained repository-relative Dockerfile path beginning ./");
|
|
3642
|
+
}
|
|
3643
|
+
if (!validHealthcheck(container.healthcheck)) {
|
|
3644
|
+
return unsupported("invalid_container_healthcheck", "containers.0.healthcheck", "container healthcheck must be a simple absolute path without traversal, query, or fragment");
|
|
3645
|
+
}
|
|
3646
|
+
const environmentNames = container.env ?? [];
|
|
3647
|
+
if (environmentNames.length > MAX_ENV_NAMES) {
|
|
3648
|
+
return unsupported("too_many_container_environment_names", "containers.0.env", `container env may contain at most ${MAX_ENV_NAMES} names`);
|
|
3649
|
+
}
|
|
3650
|
+
const seen = new Set;
|
|
3651
|
+
const bindingName = containerBindingName(container.name);
|
|
3652
|
+
for (let index = 0;index < environmentNames.length; index += 1) {
|
|
3653
|
+
const name = environmentNames[index];
|
|
3654
|
+
const path7 = `containers.0.env.${index}`;
|
|
3655
|
+
if (byteLength(name) > MAX_ENV_NAME_BYTES || !ENV_NAME.test(name)) {
|
|
3656
|
+
return unsupported("invalid_container_environment_name", path7, "container environment names must be at most 64 bytes and use UPPER_SNAKE_CASE");
|
|
3657
|
+
}
|
|
3658
|
+
if (seen.has(name)) {
|
|
3659
|
+
return unsupported("duplicate_container_environment_name", path7, `container environment name '${name}' is duplicated`);
|
|
3660
|
+
}
|
|
3661
|
+
seen.add(name);
|
|
3662
|
+
if (RESERVED_ENV_NAMES.has(name) || RESERVED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)) || name === bindingName) {
|
|
3663
|
+
return unsupported("reserved_container_environment_name", path7, `container environment name '${name}' is owned by the Launchpad runtime`);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
return {
|
|
3667
|
+
kind: "supported",
|
|
3668
|
+
manifestValid: true,
|
|
3669
|
+
runtimeSupported: true,
|
|
3670
|
+
runtime: "container",
|
|
3671
|
+
capability: {
|
|
3672
|
+
appName: view.appName,
|
|
3673
|
+
apiVersion: manifest.apiVersion,
|
|
3674
|
+
container,
|
|
3675
|
+
className: containerClassName(view.appName),
|
|
3676
|
+
bindingName,
|
|
3677
|
+
dockerfilePathFromContainerDirectory: `../${container.image.slice(2)}`,
|
|
3678
|
+
environmentNames
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
}
|
|
3682
|
+
function containerClassName(containerName) {
|
|
3683
|
+
return `AppContainer${containerName.split("-").map(capitalise).join("")}`;
|
|
3684
|
+
}
|
|
3685
|
+
function containerBindingName(containerName) {
|
|
3686
|
+
return `APP_CONTAINER_${containerName.split("-").map((part) => part.toUpperCase()).join("_")}`;
|
|
3687
|
+
}
|
|
3688
|
+
function deploymentView(manifest) {
|
|
3689
|
+
if (manifest.apiVersion === "launchpad.m-kopa.us/v2") {
|
|
3562
3690
|
return {
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3691
|
+
type: manifest.spec.appType,
|
|
3692
|
+
appName: manifest.metadata.slug,
|
|
3693
|
+
containers: manifest.spec.containers ?? []
|
|
3566
3694
|
};
|
|
3567
3695
|
}
|
|
3568
|
-
return {
|
|
3696
|
+
return {
|
|
3697
|
+
type: manifest.deployment.type,
|
|
3698
|
+
appName: manifest.metadata.name,
|
|
3699
|
+
containers: manifest.containers ?? []
|
|
3700
|
+
};
|
|
3569
3701
|
}
|
|
3570
|
-
function
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
# Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})
|
|
3574
|
-
# App: ${manifest.metadata.name}
|
|
3575
|
-
#
|
|
3576
|
-
# Regenerate with \`launchpad generate\` (\`launchpad deploy\` also
|
|
3577
|
-
# re-emits before applying). Manual edits are overwritten on the
|
|
3578
|
-
# next regeneration. Manifest is the source of truth — change
|
|
3579
|
-
# ${manifest.metadata.name}/launchpad.yaml instead.`;
|
|
3580
|
-
lines.push(header);
|
|
3581
|
-
lines.push("");
|
|
3582
|
-
lines.push(`name = "${escapeTomlString(manifest.metadata.name)}"`);
|
|
3583
|
-
lines.push(`main = "worker/index.ts"`);
|
|
3584
|
-
lines.push(`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`);
|
|
3585
|
-
lines.push(`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((f) => `"${escapeTomlString(f)}"`).join(", ")}]`);
|
|
3586
|
-
lines.push("");
|
|
3587
|
-
lines.push("# Workers Logs (PATTERNS.md: non-optional for container apps).");
|
|
3588
|
-
lines.push("[observability]");
|
|
3589
|
-
lines.push("enabled = true");
|
|
3590
|
-
lines.push("head_sampling_rate = 1.0");
|
|
3591
|
-
lines.push("");
|
|
3592
|
-
lines.push("# Platform-managed runtime config. Populated by the deploy");
|
|
3593
|
-
lines.push("# workflow via `wrangler deploy --var KEY:VALUE`. Secrets");
|
|
3594
|
-
lines.push("# (declared in launchpad.yaml secrets.bindings[]) go through");
|
|
3595
|
-
lines.push("# `wrangler secret put` and are NOT here.");
|
|
3596
|
-
lines.push("[vars]");
|
|
3597
|
-
lines.push(`CF_ACCESS_AUD = ""`);
|
|
3598
|
-
lines.push(`CF_ACCESS_TEAM_DOMAIN = ""`);
|
|
3599
|
-
lines.push("");
|
|
3600
|
-
for (const c of containers) {
|
|
3601
|
-
const className = containerClassName(c.name);
|
|
3602
|
-
const maxInstances = c.max_instances ?? 1;
|
|
3603
|
-
lines.push("[[containers]]");
|
|
3604
|
-
lines.push(`class_name = "${className}"`);
|
|
3605
|
-
lines.push(`image = "${escapeTomlString(c.image)}"`);
|
|
3606
|
-
lines.push(`max_instances = ${maxInstances}`);
|
|
3607
|
-
lines.push("");
|
|
3702
|
+
function validImagePath(path7) {
|
|
3703
|
+
if (byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("./") || path7.includes("\\") || CONTROL_CHARACTER.test(path7)) {
|
|
3704
|
+
return false;
|
|
3608
3705
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
lines.push("");
|
|
3706
|
+
const segments = path7.slice(2).split("/");
|
|
3707
|
+
return segments.length > 0 && segments.every((segment) => segment.length > 0 && segment !== "." && segment !== ".." && IMAGE_SEGMENT.test(segment));
|
|
3708
|
+
}
|
|
3709
|
+
function validHealthcheck(path7) {
|
|
3710
|
+
if (byteLength(path7) === 0 || byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("/") || path7.includes("\\") || path7.includes("?") || path7.includes("#") || CONTROL_CHARACTER.test(path7)) {
|
|
3711
|
+
return false;
|
|
3616
3712
|
}
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3713
|
+
const segments = path7.slice(1).split("/");
|
|
3714
|
+
return segments.length > 0 && segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
3715
|
+
}
|
|
3716
|
+
function byteLength(value) {
|
|
3717
|
+
return new TextEncoder().encode(value).byteLength;
|
|
3718
|
+
}
|
|
3719
|
+
function capitalise(value) {
|
|
3720
|
+
if (value.length === 0)
|
|
3721
|
+
return value;
|
|
3722
|
+
return value[0].toUpperCase() + value.slice(1);
|
|
3723
|
+
}
|
|
3724
|
+
function unsupported(reason, path7, message) {
|
|
3725
|
+
return {
|
|
3726
|
+
kind: "unsupported",
|
|
3727
|
+
manifestValid: true,
|
|
3728
|
+
runtimeSupported: false,
|
|
3729
|
+
reason,
|
|
3730
|
+
path: path7,
|
|
3731
|
+
message
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3734
|
+
// ../launchpad-engine/dist/container-worker.js
|
|
3735
|
+
function generateContainerWorker(manifest) {
|
|
3736
|
+
const result = validateDeploymentCapability(manifest);
|
|
3737
|
+
if (result.kind === "unsupported") {
|
|
3738
|
+
return {
|
|
3739
|
+
kind: "unsupported",
|
|
3740
|
+
reason: result.reason,
|
|
3741
|
+
message: result.message
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
if (result.runtime !== "container") {
|
|
3745
|
+
return {
|
|
3746
|
+
kind: "not-applicable",
|
|
3747
|
+
reason: "pages runtimes do not use the generated container Worker"
|
|
3748
|
+
};
|
|
3749
|
+
}
|
|
3750
|
+
return { kind: "ok", content: emit(result.capability) };
|
|
3751
|
+
}
|
|
3752
|
+
function emit(capability) {
|
|
3753
|
+
const app = JSON.stringify(capability.appName);
|
|
3754
|
+
const className = capability.className;
|
|
3755
|
+
const bindingName = capability.bindingName;
|
|
3756
|
+
const environmentFields = capability.environmentNames.map((name) => ` ${name}: string;`);
|
|
3757
|
+
const environmentValues = capability.environmentNames.map((name) => ` ${name}: this.env.${name} ?? "",`);
|
|
3758
|
+
const lines = [
|
|
3759
|
+
"/**",
|
|
3760
|
+
" * Auto-generated by @m-kopa/launchpad-engine.",
|
|
3761
|
+
" * Source: launchpad.yaml. Do not edit by hand.",
|
|
3762
|
+
" */",
|
|
3763
|
+
'import { Container } from "@cloudflare/containers";',
|
|
3764
|
+
"import {",
|
|
3765
|
+
" AccessJwtVerificationError,",
|
|
3766
|
+
" createJwksCache,",
|
|
3767
|
+
" parseAccessJwtFromHeaders,",
|
|
3768
|
+
" verifyAccessJwt,",
|
|
3769
|
+
" type User,",
|
|
3770
|
+
'} from "@m-kopa/platform-auth";',
|
|
3771
|
+
"",
|
|
3772
|
+
"type Env = {",
|
|
3773
|
+
` ${bindingName}: DurableObjectNamespace<${className}>;`,
|
|
3774
|
+
" BINDINGS_PROXY_SECRET: string;",
|
|
3775
|
+
" CF_ACCESS_AUD: string;",
|
|
3776
|
+
" CF_ACCESS_TEAM_DOMAIN: string;",
|
|
3777
|
+
" LAUNCHPAD_DEPLOYMENT_SHA: string;",
|
|
3778
|
+
...environmentFields,
|
|
3779
|
+
"};",
|
|
3780
|
+
"",
|
|
3781
|
+
`export class ${className} extends Container<Env> {`,
|
|
3782
|
+
` defaultPort = ${capability.container.port};`,
|
|
3783
|
+
' sleepAfter = "5m";',
|
|
3784
|
+
...environmentValues.length === 0 ? [] : ["", " envVars = {", ...environmentValues, " };"],
|
|
3785
|
+
"}",
|
|
3786
|
+
"",
|
|
3787
|
+
"let jwksCache: ReturnType<typeof createJwksCache> | null = null;",
|
|
3788
|
+
"",
|
|
3789
|
+
"function jwks(env: Env): ReturnType<typeof createJwksCache> {",
|
|
3790
|
+
" if (jwksCache === null) {",
|
|
3791
|
+
" jwksCache = createJwksCache(",
|
|
3792
|
+
" `https://${env.CF_ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`,",
|
|
3793
|
+
" );",
|
|
3794
|
+
" }",
|
|
3795
|
+
" return jwksCache;",
|
|
3796
|
+
"}",
|
|
3797
|
+
"",
|
|
3798
|
+
"function unauthenticated(code: string): Response {",
|
|
3799
|
+
" return Response.json(",
|
|
3800
|
+
' { error: "unauthenticated", code },',
|
|
3801
|
+
' { status: 401, headers: { "cache-control": "no-store" } },',
|
|
3802
|
+
" );",
|
|
3803
|
+
"}",
|
|
3804
|
+
"",
|
|
3805
|
+
"async function secretsEqual(left: string, right: string): Promise<boolean> {",
|
|
3806
|
+
" const encoder = new TextEncoder();",
|
|
3807
|
+
" const [leftDigest, rightDigest] = await Promise.all([",
|
|
3808
|
+
' crypto.subtle.digest("SHA-256", encoder.encode(left)),',
|
|
3809
|
+
' crypto.subtle.digest("SHA-256", encoder.encode(right)),',
|
|
3810
|
+
" ]);",
|
|
3811
|
+
" const leftBytes = new Uint8Array(leftDigest);",
|
|
3812
|
+
" const rightBytes = new Uint8Array(rightDigest);",
|
|
3813
|
+
" let difference = 0;",
|
|
3814
|
+
" for (let index = 0; index < leftBytes.length; index += 1) {",
|
|
3815
|
+
" difference |= leftBytes[index]! ^ rightBytes[index]!;",
|
|
3816
|
+
" }",
|
|
3817
|
+
" return difference === 0;",
|
|
3818
|
+
"}",
|
|
3819
|
+
"",
|
|
3820
|
+
"async function authenticate(request: Request, env: Env): Promise<User | Response> {",
|
|
3821
|
+
" if (!env.CF_ACCESS_TEAM_DOMAIN || !env.CF_ACCESS_AUD) {",
|
|
3822
|
+
' return unauthenticated("auth_unconfigured");',
|
|
3823
|
+
" }",
|
|
3824
|
+
" const token = parseAccessJwtFromHeaders(request.headers);",
|
|
3825
|
+
" if (token === null) {",
|
|
3826
|
+
' return unauthenticated("access_jwt_missing");',
|
|
3827
|
+
" }",
|
|
3828
|
+
" try {",
|
|
3829
|
+
" return await verifyAccessJwt(token, {",
|
|
3830
|
+
" teamDomain: `https://${env.CF_ACCESS_TEAM_DOMAIN}`,",
|
|
3831
|
+
" audience: env.CF_ACCESS_AUD,",
|
|
3832
|
+
" jwks: jwks(env),",
|
|
3833
|
+
" clockTolerance: 30,",
|
|
3834
|
+
" });",
|
|
3835
|
+
" } catch (error) {",
|
|
3836
|
+
" if (error instanceof AccessJwtVerificationError) {",
|
|
3837
|
+
" return unauthenticated(error.code);",
|
|
3838
|
+
" }",
|
|
3839
|
+
' console.error(JSON.stringify({ event: "container_runtime_error", kind: "access_jwt_verification_failed" }));',
|
|
3840
|
+
' return unauthenticated("access_jwt_verification_failed");',
|
|
3841
|
+
" }",
|
|
3842
|
+
"}",
|
|
3843
|
+
"",
|
|
3844
|
+
"function forwardedRequest(",
|
|
3845
|
+
" request: Request,",
|
|
3846
|
+
" user: User,",
|
|
3847
|
+
" env: Env,",
|
|
3848
|
+
" requestId: string,",
|
|
3849
|
+
"): Request {",
|
|
3850
|
+
" const url = new URL(request.url);",
|
|
3851
|
+
" const headers = new Headers(request.headers);",
|
|
3852
|
+
" for (const name of Array.from(headers.keys())) {",
|
|
3853
|
+
" const lower = name.toLowerCase();",
|
|
3854
|
+
" if (",
|
|
3855
|
+
' lower.startsWith("cf-access-") ||',
|
|
3856
|
+
' lower.startsWith("x-user-") ||',
|
|
3857
|
+
' lower.startsWith("x-bindings-proxy-") ||',
|
|
3858
|
+
' lower === "x-request-id"',
|
|
3859
|
+
" ) {",
|
|
3860
|
+
" headers.delete(name);",
|
|
3861
|
+
" }",
|
|
3862
|
+
" }",
|
|
3863
|
+
' headers.set("X-User-Sub", user.sub);',
|
|
3864
|
+
' headers.set("X-User-Email", user.preferredUsername);',
|
|
3865
|
+
" if (user.groups.length > 0) {",
|
|
3866
|
+
' headers.set("X-User-Groups", user.groups.join(","));',
|
|
3867
|
+
" }",
|
|
3868
|
+
' headers.set("X-Request-ID", requestId);',
|
|
3869
|
+
' headers.set("X-Bindings-Proxy-Secret", env.BINDINGS_PROXY_SECRET);',
|
|
3870
|
+
' headers.set("X-Bindings-Proxy-Url", `${url.origin}/__platform/bindings`);',
|
|
3871
|
+
" return new Request(url.toString(), {",
|
|
3872
|
+
" method: request.method,",
|
|
3873
|
+
" headers,",
|
|
3874
|
+
' body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,',
|
|
3875
|
+
" redirect: request.redirect,",
|
|
3876
|
+
" });",
|
|
3877
|
+
"}",
|
|
3878
|
+
"",
|
|
3879
|
+
"function runtimeEvent(",
|
|
3880
|
+
" env: Env,",
|
|
3881
|
+
" requestId: string,",
|
|
3882
|
+
" status: number,",
|
|
3883
|
+
" kind: string,",
|
|
3884
|
+
"): void {",
|
|
3885
|
+
" console.log(JSON.stringify({",
|
|
3886
|
+
' event: "container_runtime_dispatch",',
|
|
3887
|
+
` slug: ${app},`,
|
|
3888
|
+
" deployment_sha: env.LAUNCHPAD_DEPLOYMENT_SHA,",
|
|
3889
|
+
" request_id: requestId,",
|
|
3890
|
+
` class_name: ${JSON.stringify(className)},`,
|
|
3891
|
+
` binding_name: ${JSON.stringify(bindingName)},`,
|
|
3892
|
+
" status,",
|
|
3893
|
+
" kind,",
|
|
3894
|
+
" }));",
|
|
3895
|
+
"}",
|
|
3896
|
+
"",
|
|
3897
|
+
"export default {",
|
|
3898
|
+
" async fetch(request: Request, env: Env): Promise<Response> {",
|
|
3899
|
+
" const url = new URL(request.url);",
|
|
3900
|
+
" const requestId = crypto.randomUUID();",
|
|
3901
|
+
' if (request.method === "GET" && url.pathname === "/health") {',
|
|
3902
|
+
' const response = Response.json({ ok: true, role: "worker" });',
|
|
3903
|
+
' runtimeEvent(env, requestId, response.status, "worker_health");',
|
|
3904
|
+
" return response;",
|
|
3905
|
+
" }",
|
|
3906
|
+
' if (url.pathname === "/__platform/bindings" || url.pathname.startsWith("/__platform/bindings/")) {',
|
|
3907
|
+
' const presented = request.headers.get("x-bindings-proxy-secret");',
|
|
3908
|
+
" if (!env.BINDINGS_PROXY_SECRET || !presented || !(await secretsEqual(presented, env.BINDINGS_PROXY_SECRET))) {",
|
|
3909
|
+
' const response = Response.json({ error: "forbidden" }, { status: 403 });',
|
|
3910
|
+
' runtimeEvent(env, requestId, response.status, "bindings_forbidden");',
|
|
3911
|
+
" return response;",
|
|
3912
|
+
" }",
|
|
3913
|
+
' const response = Response.json({ error: "unknown_binding_endpoint" }, { status: 404 });',
|
|
3914
|
+
' runtimeEvent(env, requestId, response.status, "bindings_unknown");',
|
|
3915
|
+
" return response;",
|
|
3916
|
+
" }",
|
|
3917
|
+
" const identity = await authenticate(request, env);",
|
|
3918
|
+
" if (identity instanceof Response) {",
|
|
3919
|
+
' runtimeEvent(env, requestId, identity.status, "authentication_failed");',
|
|
3920
|
+
" return identity;",
|
|
3921
|
+
" }",
|
|
3922
|
+
` const id = env.${bindingName}.idFromName("singleton");`,
|
|
3923
|
+
` const response = await env.${bindingName}.get(id).fetch(`,
|
|
3924
|
+
" forwardedRequest(request, identity, env, requestId),",
|
|
3925
|
+
" );",
|
|
3926
|
+
' runtimeEvent(env, requestId, response.status, "container_response");',
|
|
3927
|
+
" return response;",
|
|
3928
|
+
" },",
|
|
3929
|
+
"};",
|
|
3930
|
+
""
|
|
3931
|
+
];
|
|
3627
3932
|
return lines.join(`
|
|
3628
3933
|
`);
|
|
3629
3934
|
}
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
function
|
|
3634
|
-
|
|
3935
|
+
// ../launchpad-engine/dist/wrangler.js
|
|
3936
|
+
var DEFAULT_COMPATIBILITY_DATE = "2026-05-01";
|
|
3937
|
+
var DEFAULT_COMPATIBILITY_FLAGS = ["nodejs_compat"];
|
|
3938
|
+
function generateWranglerToml(manifest) {
|
|
3939
|
+
const result = validateDeploymentCapability(manifest);
|
|
3940
|
+
if (result.kind === "unsupported") {
|
|
3941
|
+
return {
|
|
3942
|
+
kind: "unsupported",
|
|
3943
|
+
reason: result.reason,
|
|
3944
|
+
message: result.message
|
|
3945
|
+
};
|
|
3946
|
+
}
|
|
3947
|
+
if (result.runtime !== "container") {
|
|
3948
|
+
return {
|
|
3949
|
+
kind: "not-applicable",
|
|
3950
|
+
reason: "pages runtimes do not use container/wrangler.toml"
|
|
3951
|
+
};
|
|
3952
|
+
}
|
|
3953
|
+
return { kind: "ok", content: emit2(result.capability), warnings: [] };
|
|
3635
3954
|
}
|
|
3636
|
-
function
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3955
|
+
function emit2(capability) {
|
|
3956
|
+
const maxInstances = capability.container.max_instances ?? 1;
|
|
3957
|
+
return [
|
|
3958
|
+
"# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
|
|
3959
|
+
`# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
|
|
3960
|
+
`# App: ${capability.appName}`,
|
|
3961
|
+
"",
|
|
3962
|
+
`name = "${escapeTomlString(capability.appName)}"`,
|
|
3963
|
+
'main = "worker/index.ts"',
|
|
3964
|
+
`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`,
|
|
3965
|
+
`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((flag) => `"${flag}"`).join(", ")}]`,
|
|
3966
|
+
"",
|
|
3967
|
+
"[observability]",
|
|
3968
|
+
"enabled = true",
|
|
3969
|
+
"head_sampling_rate = 1.0",
|
|
3970
|
+
"",
|
|
3971
|
+
"[vars]",
|
|
3972
|
+
'CF_ACCESS_AUD = ""',
|
|
3973
|
+
'CF_ACCESS_TEAM_DOMAIN = ""',
|
|
3974
|
+
'LAUNCHPAD_DEPLOYMENT_SHA = ""',
|
|
3975
|
+
"",
|
|
3976
|
+
"[[containers]]",
|
|
3977
|
+
`class_name = "${capability.className}"`,
|
|
3978
|
+
`image = "${escapeTomlString(capability.dockerfilePathFromContainerDirectory)}"`,
|
|
3979
|
+
`max_instances = ${maxInstances}`,
|
|
3980
|
+
"",
|
|
3981
|
+
"[[durable_objects.bindings]]",
|
|
3982
|
+
`name = "${capability.bindingName}"`,
|
|
3983
|
+
`class_name = "${capability.className}"`,
|
|
3984
|
+
"",
|
|
3985
|
+
"[[migrations]]",
|
|
3986
|
+
'tag = "v1"',
|
|
3987
|
+
`new_sqlite_classes = ["${capability.className}"]`,
|
|
3988
|
+
""
|
|
3989
|
+
].join(`
|
|
3990
|
+
`);
|
|
3640
3991
|
}
|
|
3641
|
-
function escapeTomlString(
|
|
3642
|
-
return
|
|
3992
|
+
function escapeTomlString(value) {
|
|
3993
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
3643
3994
|
}
|
|
3644
3995
|
// ../launchpad-engine/dist/github-workflow.js
|
|
3645
|
-
var
|
|
3646
|
-
var DEFAULT_BUN_VERSION = "1.3.4";
|
|
3996
|
+
var DEFAULT_NODE_VERSION = "22";
|
|
3647
3997
|
var ACTIONS_CHECKOUT_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
|
|
3648
|
-
var
|
|
3998
|
+
var ACTIONS_SETUP_NODE_SHA = "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e";
|
|
3649
3999
|
function generateGithubDeployWorkflow(manifest) {
|
|
3650
|
-
|
|
4000
|
+
const result = validateDeploymentCapability(manifest);
|
|
4001
|
+
if (result.kind === "unsupported") {
|
|
3651
4002
|
return {
|
|
3652
|
-
kind: "
|
|
3653
|
-
reason:
|
|
4003
|
+
kind: "unsupported",
|
|
4004
|
+
reason: result.reason,
|
|
4005
|
+
message: result.message
|
|
3654
4006
|
};
|
|
3655
4007
|
}
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
4008
|
+
if (result.runtime !== "container") {
|
|
4009
|
+
return {
|
|
4010
|
+
kind: "not-applicable",
|
|
4011
|
+
reason: "pages runtimes do not use the container deploy workflow"
|
|
4012
|
+
};
|
|
3660
4013
|
}
|
|
3661
|
-
return {
|
|
3662
|
-
kind: "ok",
|
|
3663
|
-
content: emit2(manifest, bindings),
|
|
3664
|
-
warnings
|
|
3665
|
-
};
|
|
4014
|
+
return { kind: "ok", content: emit3(result.capability), warnings: [] };
|
|
3666
4015
|
}
|
|
3667
|
-
function
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
lines.push(` set -euo pipefail`);
|
|
3807
|
-
lines.push(` URL="${workerUrl}/api/health"`);
|
|
3808
|
-
lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
|
|
3809
|
-
lines.push(` case "$code" in`);
|
|
3810
|
-
lines.push(` 302|401)`);
|
|
3811
|
-
lines.push(` echo "backend auth-gate ok: HTTP $code on unauthenticated request to /api/health."`);
|
|
3812
|
-
lines.push(` ;;`);
|
|
3813
|
-
lines.push(` *)`);
|
|
3814
|
-
lines.push(` echo "backend auth-gate regression: expected 302 or 401 on /api/health, got $code" >&2`);
|
|
3815
|
-
lines.push(` curl -sSI "$URL" >&2 || true`);
|
|
3816
|
-
lines.push(` exit 1`);
|
|
3817
|
-
lines.push(` ;;`);
|
|
3818
|
-
lines.push(` esac`);
|
|
3819
|
-
lines.push("");
|
|
4016
|
+
function emit3(capability) {
|
|
4017
|
+
const lines = [
|
|
4018
|
+
"# Auto-generated by @m-kopa/launchpad-engine. Do not edit by hand.",
|
|
4019
|
+
`# Source: launchpad.yaml (apiVersion: ${capability.apiVersion})`,
|
|
4020
|
+
`# App: ${capability.appName}`,
|
|
4021
|
+
"",
|
|
4022
|
+
"name: deploy",
|
|
4023
|
+
"",
|
|
4024
|
+
"on:",
|
|
4025
|
+
" push:",
|
|
4026
|
+
" branches:",
|
|
4027
|
+
" - main",
|
|
4028
|
+
" workflow_dispatch:",
|
|
4029
|
+
"",
|
|
4030
|
+
"permissions:",
|
|
4031
|
+
" contents: read",
|
|
4032
|
+
"",
|
|
4033
|
+
"concurrency:",
|
|
4034
|
+
" group: deploy-${{ github.workflow }}-${{ github.sha }}",
|
|
4035
|
+
" cancel-in-progress: false",
|
|
4036
|
+
"",
|
|
4037
|
+
"jobs:",
|
|
4038
|
+
" authorize:",
|
|
4039
|
+
" name: authorize exact run attempt",
|
|
4040
|
+
" runs-on: ubuntu-latest",
|
|
4041
|
+
" steps:",
|
|
4042
|
+
` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`,
|
|
4043
|
+
" with:",
|
|
4044
|
+
" persist-credentials: false",
|
|
4045
|
+
"",
|
|
4046
|
+
" - name: Verify protected runtime authorization",
|
|
4047
|
+
" env:",
|
|
4048
|
+
" GH_TOKEN: ${{ github.token }}",
|
|
4049
|
+
" run: |",
|
|
4050
|
+
" set -euo pipefail",
|
|
4051
|
+
" record=$(mktemp)",
|
|
4052
|
+
` trap 'rm -f "$record"' EXIT`,
|
|
4053
|
+
' api="${{ github.api_url }}/repos/${{ github.repository }}/contents/.launchpad/runtime-control/${GITHUB_SHA}.json?ref=launchpad-runtime-control"',
|
|
4054
|
+
" for attempt in $(seq 1 60); do",
|
|
4055
|
+
" if curl --fail --silent --show-error \\",
|
|
4056
|
+
' -H "Authorization: Bearer ${GH_TOKEN}" \\',
|
|
4057
|
+
' -H "Accept: application/vnd.github.raw+json" \\',
|
|
4058
|
+
' -H "X-GitHub-Api-Version: 2022-11-28" \\',
|
|
4059
|
+
' "$api" > "$record"; then',
|
|
4060
|
+
" break",
|
|
4061
|
+
" fi",
|
|
4062
|
+
' if [ "$attempt" -eq 60 ]; then',
|
|
4063
|
+
' echo "No exact-run authorization was published" >&2',
|
|
4064
|
+
" exit 1",
|
|
4065
|
+
" fi",
|
|
4066
|
+
" sleep 5",
|
|
4067
|
+
" done",
|
|
4068
|
+
" jq -e '",
|
|
4069
|
+
' (.schema == "launchpad-runtime-control/v1") and',
|
|
4070
|
+
' (.attempt_id | type == "string" and length > 0) and',
|
|
4071
|
+
' (.lease_digest | test("^[0-9a-f]{64}$")) and',
|
|
4072
|
+
" (.commit_sha == env.GITHUB_SHA) and",
|
|
4073
|
+
" (.run_id == env.GITHUB_RUN_ID) and",
|
|
4074
|
+
" (.run_attempt == env.GITHUB_RUN_ATTEMPT) and",
|
|
4075
|
+
' (.expires_at_epoch | type == "number" and . > now)',
|
|
4076
|
+
` ' "$record" >/dev/null`,
|
|
4077
|
+
" wrangler_sha=$(sha256sum container/wrangler.toml | cut -d' ' -f1)",
|
|
4078
|
+
" worker_sha=$(sha256sum container/worker/index.ts | cut -d' ' -f1)",
|
|
4079
|
+
" workflow_sha=$(sha256sum .github/workflows/deploy.yml | cut -d' ' -f1)",
|
|
4080
|
+
" jq -e \\",
|
|
4081
|
+
' --arg wrangler "$wrangler_sha" \\',
|
|
4082
|
+
' --arg worker "$worker_sha" \\',
|
|
4083
|
+
' --arg workflow "$workflow_sha" \\',
|
|
4084
|
+
` '.artifact_digests["container/wrangler.toml"] == $wrangler and`,
|
|
4085
|
+
' .artifact_digests["container/worker/index.ts"] == $worker and',
|
|
4086
|
+
` .artifact_digests[".github/workflows/deploy.yml"] == $workflow' \\`,
|
|
4087
|
+
' "$record" >/dev/null',
|
|
4088
|
+
"",
|
|
4089
|
+
" deploy:",
|
|
4090
|
+
" name: deploy admitted runtime",
|
|
4091
|
+
" needs: authorize",
|
|
4092
|
+
" runs-on: ubuntu-latest",
|
|
4093
|
+
" environment: ${{ vars.LAUNCHPAD_APP_RUNTIME || 'production' }}",
|
|
4094
|
+
" steps:",
|
|
4095
|
+
` - uses: actions/checkout@${ACTIONS_CHECKOUT_SHA} # v6.0.2`,
|
|
4096
|
+
" with:",
|
|
4097
|
+
" persist-credentials: false",
|
|
4098
|
+
"",
|
|
4099
|
+
` - uses: actions/setup-node@${ACTIONS_SETUP_NODE_SHA} # v6.4.0`,
|
|
4100
|
+
" with:",
|
|
4101
|
+
` node-version: "${DEFAULT_NODE_VERSION}"`,
|
|
4102
|
+
" cache: npm",
|
|
4103
|
+
" cache-dependency-path: |",
|
|
4104
|
+
" container/package-lock.json",
|
|
4105
|
+
" container/backend/package-lock.json",
|
|
4106
|
+
"",
|
|
4107
|
+
" - name: Install frozen Worker dependencies",
|
|
4108
|
+
" working-directory: container",
|
|
4109
|
+
" run: npm ci --ignore-scripts",
|
|
4110
|
+
"",
|
|
4111
|
+
" - name: Install frozen workload dependencies",
|
|
4112
|
+
" working-directory: container/backend",
|
|
4113
|
+
" run: npm ci --ignore-scripts",
|
|
4114
|
+
"",
|
|
4115
|
+
" - name: Verify required deployment inputs",
|
|
4116
|
+
" env:",
|
|
4117
|
+
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4118
|
+
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4119
|
+
" CF_ACCESS_AUD: ${{ vars.CF_ACCESS_AUD }}",
|
|
4120
|
+
" CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
|
|
4121
|
+
" BINDINGS_PROXY_SECRET: ${{ secrets.BINDINGS_PROXY_SECRET }}",
|
|
4122
|
+
" CF_ACCESS_AUD: ${{ vars.CF_ACCESS_AUD }}",
|
|
4123
|
+
" CF_ACCESS_TEAM_DOMAIN: ${{ vars.CF_ACCESS_TEAM_DOMAIN }}",
|
|
4124
|
+
...capability.environmentNames.map((name) => ` ${name}: \${{ secrets.${name} }}`),
|
|
4125
|
+
" run: |",
|
|
4126
|
+
" set -euo pipefail",
|
|
4127
|
+
" for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID BINDINGS_PROXY_SECRET CF_ACCESS_AUD CF_ACCESS_TEAM_DOMAIN; do",
|
|
4128
|
+
' if [ -z "${!name:-}" ]; then',
|
|
4129
|
+
' echo "Required deployment input $name is empty" >&2',
|
|
4130
|
+
" exit 1",
|
|
4131
|
+
" fi",
|
|
4132
|
+
" done",
|
|
4133
|
+
...capability.environmentNames.flatMap((name) => [
|
|
4134
|
+
` if [ -z "\${${name}:-}" ]; then`,
|
|
4135
|
+
` echo "Required container secret ${name} is empty" >&2`,
|
|
4136
|
+
" exit 1",
|
|
4137
|
+
" fi"
|
|
4138
|
+
]),
|
|
4139
|
+
"",
|
|
4140
|
+
" - name: Deploy Worker and container image",
|
|
4141
|
+
" working-directory: container",
|
|
4142
|
+
" env:",
|
|
4143
|
+
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4144
|
+
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4145
|
+
" run: |",
|
|
4146
|
+
" npx --no-install wrangler deploy \\",
|
|
4147
|
+
' --message "launchpad:${GITHUB_SHA}" \\',
|
|
4148
|
+
' --tag "launchpad-deployment-sha:${GITHUB_SHA}" \\',
|
|
4149
|
+
' --var "CF_ACCESS_AUD:${CF_ACCESS_AUD}" \\',
|
|
4150
|
+
' --var "CF_ACCESS_TEAM_DOMAIN:${CF_ACCESS_TEAM_DOMAIN}" \\',
|
|
4151
|
+
' --var "LAUNCHPAD_DEPLOYMENT_SHA:${GITHUB_SHA}"',
|
|
4152
|
+
"",
|
|
4153
|
+
...secretSteps(capability)
|
|
4154
|
+
];
|
|
3820
4155
|
return lines.join(`
|
|
3821
4156
|
`);
|
|
3822
4157
|
}
|
|
3823
|
-
function
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
lines.push(` run: |`);
|
|
3836
|
-
lines.push(` if [ -z "\${${name}:-}" ]; then`);
|
|
3837
|
-
lines.push(` echo "::warning::${name} GH Actions secret is unset — Worker will be missing this binding until the secret is provisioned + this workflow is rerun."`);
|
|
3838
|
-
lines.push(` exit 0`);
|
|
3839
|
-
lines.push(` fi`);
|
|
3840
|
-
lines.push(` printf '%s' "$${name}" | bunx wrangler secret put ${name}`);
|
|
3841
|
-
lines.push("");
|
|
4158
|
+
function secretSteps(capability) {
|
|
4159
|
+
return ["BINDINGS_PROXY_SECRET", ...capability.environmentNames].flatMap((name) => [
|
|
4160
|
+
` - name: Push ${name}`,
|
|
4161
|
+
" working-directory: container",
|
|
4162
|
+
" env:",
|
|
4163
|
+
" CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}",
|
|
4164
|
+
" CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}",
|
|
4165
|
+
` ${name}: \${{ secrets.${name} }}`,
|
|
4166
|
+
" run: |",
|
|
4167
|
+
` printf '%s' "$${name}" | npx --no-install wrangler secret put ${name}`,
|
|
4168
|
+
""
|
|
4169
|
+
]);
|
|
3842
4170
|
}
|
|
3843
4171
|
// ../launchpad-engine/dist/per-app-workspace.js
|
|
3844
4172
|
var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
@@ -4190,6 +4518,21 @@ async function bundleAndDeploy(args) {
|
|
|
4190
4518
|
import { readFileSync as readFileSync8, existsSync as existsSync4 } from "node:fs";
|
|
4191
4519
|
import { join as join9 } from "node:path";
|
|
4192
4520
|
|
|
4521
|
+
// src/deploy/redact-diagnostic.ts
|
|
4522
|
+
var MAX_DIAGNOSTIC_LEN = 600;
|
|
4523
|
+
var SECRET_KV = /(["']?)(\b[A-Za-z0-9_-]*(?:secrets?|token|api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session)\b)\1(\s*[=:]\s*|\s+)(?:"[^"]*"|'[^']*'|\S+)/gi;
|
|
4524
|
+
var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
|
|
4525
|
+
function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
|
|
4526
|
+
if (text === undefined || text === null)
|
|
4527
|
+
return "";
|
|
4528
|
+
let t = text.replace(SECRET_KV, (_match, quote, key, separator) => `${quote}${key}${quote}${separator}***`);
|
|
4529
|
+
t = t.replace(LONG_TOKEN, "***");
|
|
4530
|
+
t = t.trim();
|
|
4531
|
+
if (t.length > maxLen)
|
|
4532
|
+
t = `${t.slice(0, maxLen - 1)}…`;
|
|
4533
|
+
return t;
|
|
4534
|
+
}
|
|
4535
|
+
|
|
4193
4536
|
// src/deploy/deployment-status.ts
|
|
4194
4537
|
async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
|
|
4195
4538
|
let raw;
|
|
@@ -4218,11 +4561,39 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
|
4218
4561
|
return {
|
|
4219
4562
|
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
4220
4563
|
supported: raw.supported,
|
|
4221
|
-
liveDeployment: raw.liveDeployment ?? null,
|
|
4222
|
-
lastSuccessfulDeployment: raw.lastSuccessfulDeployment
|
|
4564
|
+
liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
|
|
4565
|
+
lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
|
|
4223
4566
|
...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
|
|
4224
4567
|
};
|
|
4225
4568
|
}
|
|
4569
|
+
function projectLastSuccessful(value) {
|
|
4570
|
+
if (value === null || typeof value !== "object")
|
|
4571
|
+
return null;
|
|
4572
|
+
const raw = value;
|
|
4573
|
+
if (typeof raw.id !== "string" || typeof raw.createdOn !== "string") {
|
|
4574
|
+
return null;
|
|
4575
|
+
}
|
|
4576
|
+
return {
|
|
4577
|
+
id: raw.id,
|
|
4578
|
+
createdOn: raw.createdOn,
|
|
4579
|
+
url: typeof raw.url === "string" ? raw.url : null
|
|
4580
|
+
};
|
|
4581
|
+
}
|
|
4582
|
+
function scrubLiveDeployment(deployment) {
|
|
4583
|
+
if (deployment === null)
|
|
4584
|
+
return null;
|
|
4585
|
+
return {
|
|
4586
|
+
...deployment,
|
|
4587
|
+
...deployment.commit === undefined ? {} : {
|
|
4588
|
+
commit: {
|
|
4589
|
+
...deployment.commit,
|
|
4590
|
+
message: redactDiagnostic(deployment.commit.message)
|
|
4591
|
+
}
|
|
4592
|
+
},
|
|
4593
|
+
...deployment.failedStage === undefined ? {} : { failedStage: redactDiagnostic(deployment.failedStage) },
|
|
4594
|
+
...deployment.logExcerpt === undefined ? {} : { logExcerpt: redactDiagnostic(deployment.logExcerpt) }
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4226
4597
|
|
|
4227
4598
|
// src/deploy/commit-match-settle.ts
|
|
4228
4599
|
var DEFAULT_REGISTRATION_BOUND_MS = 120000;
|
|
@@ -4388,21 +4759,6 @@ async function verifyServed(args, deps = { fetch }) {
|
|
|
4388
4759
|
};
|
|
4389
4760
|
}
|
|
4390
4761
|
|
|
4391
|
-
// src/deploy/redact-diagnostic.ts
|
|
4392
|
-
var MAX_DIAGNOSTIC_LEN = 600;
|
|
4393
|
-
var SECRET_KV = /\b(secret|secrets?|token|api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|password|passwd|pwd|authorization|auth[_-]?token|bearer|client[_-]?secret|session)\b(\s*[=:]\s*|\s+)(\S+)/gi;
|
|
4394
|
-
var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
|
|
4395
|
-
function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
|
|
4396
|
-
if (text === undefined || text === null)
|
|
4397
|
-
return "";
|
|
4398
|
-
let t = text.replace(SECRET_KV, (_m, key, sep2) => `${key}${sep2}***`);
|
|
4399
|
-
t = t.replace(LONG_TOKEN, "***");
|
|
4400
|
-
t = t.trim();
|
|
4401
|
-
if (t.length > maxLen)
|
|
4402
|
-
t = `${t.slice(0, maxLen - 1)}…`;
|
|
4403
|
-
return t;
|
|
4404
|
-
}
|
|
4405
|
-
|
|
4406
4762
|
// src/commands/status.ts
|
|
4407
4763
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4408
4764
|
|
|
@@ -5263,7 +5619,29 @@ async function runStatus(args, io) {
|
|
|
5263
5619
|
return mapBotError(e, parsed.slug, io);
|
|
5264
5620
|
}
|
|
5265
5621
|
if (lifecycle !== null && lifecycle.state !== "live") {
|
|
5266
|
-
|
|
5622
|
+
let output = lifecycleOutput(parsed.slug, lifecycle);
|
|
5623
|
+
if (lifecycle.state === "failed") {
|
|
5624
|
+
try {
|
|
5625
|
+
const deployment2 = await fetchDeploymentStatus(cfg, parsed.slug);
|
|
5626
|
+
output = {
|
|
5627
|
+
...output,
|
|
5628
|
+
deployment: deployment2,
|
|
5629
|
+
...deployment2 === null ? {
|
|
5630
|
+
deploymentUnavailableReason: "the deployment-status endpoint is unavailable or returned an unsupported response"
|
|
5631
|
+
} : {}
|
|
5632
|
+
};
|
|
5633
|
+
} catch (e) {
|
|
5634
|
+
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
5635
|
+
return mapBotError(e, parsed.slug, io);
|
|
5636
|
+
}
|
|
5637
|
+
output = {
|
|
5638
|
+
...output,
|
|
5639
|
+
deployment: null,
|
|
5640
|
+
deploymentUnavailableReason: redactDiagnostic(describe12(e))
|
|
5641
|
+
};
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
emit4(output, parsed.json, io);
|
|
5267
5645
|
return 0;
|
|
5268
5646
|
}
|
|
5269
5647
|
let localYaml = null;
|
|
@@ -5311,7 +5689,7 @@ async function runStatus(args, io) {
|
|
|
5311
5689
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
5312
5690
|
return mapBotError(e, parsed.slug, io);
|
|
5313
5691
|
}
|
|
5314
|
-
io.err(`launchpad status: live deployment state unavailable (${describe12(e)}) — ` + `the report below is from the platform manifest view only.`);
|
|
5692
|
+
io.err(`launchpad status: live deployment state unavailable (${redactDiagnostic(describe12(e))}) — ` + `the report below is from the platform manifest view only.`);
|
|
5315
5693
|
}
|
|
5316
5694
|
let standingExceptions = null;
|
|
5317
5695
|
try {
|
|
@@ -5340,7 +5718,7 @@ async function runStatus(args, io) {
|
|
|
5340
5718
|
...deploymentKnown ? { deployment } : {},
|
|
5341
5719
|
...standingExceptions !== null ? { standingExceptions } : {}
|
|
5342
5720
|
};
|
|
5343
|
-
|
|
5721
|
+
emit4(out, parsed.json, io);
|
|
5344
5722
|
return 0;
|
|
5345
5723
|
}
|
|
5346
5724
|
if (local === null) {
|
|
@@ -5357,7 +5735,7 @@ async function runStatus(args, io) {
|
|
|
5357
5735
|
...deploymentKnown ? { deployment } : {},
|
|
5358
5736
|
...standingExceptions !== null ? { standingExceptions } : {}
|
|
5359
5737
|
};
|
|
5360
|
-
|
|
5738
|
+
emit4(out, parsed.json, io);
|
|
5361
5739
|
if (parsed.strict) {
|
|
5362
5740
|
if (deployment?.liveDeployment?.buildStatus === "failure") {
|
|
5363
5741
|
io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
|
|
@@ -5440,7 +5818,7 @@ async function runStatus(args, io) {
|
|
|
5440
5818
|
...accessDrift !== undefined ? { accessDrift } : {},
|
|
5441
5819
|
...groupIssues.length === 0 ? {} : { groupIssues }
|
|
5442
5820
|
};
|
|
5443
|
-
|
|
5821
|
+
emit4(result, parsed.json, io);
|
|
5444
5822
|
if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false || groupIssues.length > 0)) {
|
|
5445
5823
|
return 1;
|
|
5446
5824
|
}
|
|
@@ -5636,7 +6014,7 @@ function lifecycleOutput(slug, lc) {
|
|
|
5636
6014
|
driftDetails: []
|
|
5637
6015
|
};
|
|
5638
6016
|
}
|
|
5639
|
-
function
|
|
6017
|
+
function emit4(out, asJson, io) {
|
|
5640
6018
|
if (asJson) {
|
|
5641
6019
|
io.out(JSON.stringify(out, null, 2));
|
|
5642
6020
|
return;
|
|
@@ -5648,12 +6026,16 @@ function emit3(out, asJson, io) {
|
|
|
5648
6026
|
return;
|
|
5649
6027
|
case "provisioning_failed":
|
|
5650
6028
|
io.out(`${out.slug}: provisioning FAILED at stage ${out.stage ?? "unknown"}` + (out.failedReason ? `: ${out.failedReason}` : ""));
|
|
5651
|
-
if (
|
|
6029
|
+
if (out.deployment?.reconciledToLive === true) {
|
|
6030
|
+
io.out(` that lifecycle state is stale: this read verified the live build and reconciled the record failed→live.`);
|
|
6031
|
+
io.out(` no action needed — re-run \`launchpad status ${out.slug}\` to see the reconciled state.`);
|
|
6032
|
+
} else if (isLikelyLiveFailureStage(out.stage)) {
|
|
5652
6033
|
io.out(` the app may already be live — run \`launchpad recover ${out.slug}\` to reconcile the record;`);
|
|
5653
6034
|
io.out(` if it is not serving, the same command starts the stage-dependent recovery.`);
|
|
5654
6035
|
} else {
|
|
5655
6036
|
io.out(` run \`launchpad recover ${out.slug}\` to start the stage-dependent recovery.`);
|
|
5656
6037
|
}
|
|
6038
|
+
surfaceFailedLifecycleDeployment(out, io);
|
|
5657
6039
|
return;
|
|
5658
6040
|
case "destroying":
|
|
5659
6041
|
io.out(`${out.slug}: destroying…`);
|
|
@@ -5750,6 +6132,20 @@ function triggerLabel(trigger) {
|
|
|
5750
6132
|
return "launchpad deploy (bot)";
|
|
5751
6133
|
return "an unknown mechanism";
|
|
5752
6134
|
}
|
|
6135
|
+
function surfaceFailedLifecycleDeployment(out, io) {
|
|
6136
|
+
if (out.deploymentUnavailableReason !== undefined) {
|
|
6137
|
+
io.out(` latest deployment evidence unavailable: ${out.deploymentUnavailableReason}`);
|
|
6138
|
+
return;
|
|
6139
|
+
}
|
|
6140
|
+
if (out.deployment === undefined || out.deployment === null)
|
|
6141
|
+
return;
|
|
6142
|
+
if (!out.deployment.supported) {
|
|
6143
|
+
io.out(" latest deployment evidence: not applicable because this app has no Pages deployment surface.");
|
|
6144
|
+
return;
|
|
6145
|
+
}
|
|
6146
|
+
io.out(" latest deployment evidence (may not be the cause of this lifecycle failure):");
|
|
6147
|
+
surfaceDeployment(out, io);
|
|
6148
|
+
}
|
|
5753
6149
|
function surfaceDeployment(out, io) {
|
|
5754
6150
|
const dep = out.deployment;
|
|
5755
6151
|
if (dep === undefined || dep === null || !dep.supported)
|
|
@@ -5766,15 +6162,15 @@ function surfaceDeployment(out, io) {
|
|
|
5766
6162
|
const via = triggerLabel(live.trigger);
|
|
5767
6163
|
switch (live.buildStatus) {
|
|
5768
6164
|
case "success":
|
|
5769
|
-
io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn}`);
|
|
6165
|
+
io.out(` last deployment: build success (${via}${commit}) at ${live.createdOn} [deployment ${live.id}]`);
|
|
5770
6166
|
return;
|
|
5771
6167
|
case "in_progress":
|
|
5772
|
-
io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn})`);
|
|
6168
|
+
io.out(` last deployment: build IN PROGRESS (${via}${commit}, started ${live.createdOn}) [deployment ${live.id}]`);
|
|
5773
6169
|
io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
|
|
5774
6170
|
return;
|
|
5775
6171
|
case "failure": {
|
|
5776
6172
|
const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
|
|
5777
|
-
io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn}`);
|
|
6173
|
+
io.out(` last deployment: build FAILED${stage} (${via}${commit}) at ${live.createdOn} [deployment ${live.id}]`);
|
|
5778
6174
|
if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
|
|
5779
6175
|
io.out(" build log (excerpt):");
|
|
5780
6176
|
for (const line of live.logExcerpt.split(`
|
|
@@ -6185,6 +6581,8 @@ import { readFileSync as readFileSync10 } from "node:fs";
|
|
|
6185
6581
|
|
|
6186
6582
|
// src/deploy/git-files.ts
|
|
6187
6583
|
import { spawn as spawn3 } from "node:child_process";
|
|
6584
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
6585
|
+
import { resolve as resolve7 } from "node:path";
|
|
6188
6586
|
|
|
6189
6587
|
class GitFilesError extends Error {
|
|
6190
6588
|
code = "git_files_error";
|
|
@@ -6202,6 +6600,7 @@ var PROTECTED_PATHS = new Set([
|
|
|
6202
6600
|
]);
|
|
6203
6601
|
async function listDeployFiles(opts) {
|
|
6204
6602
|
const sp = opts.spawner ?? spawn3;
|
|
6603
|
+
const exists = opts.exists ?? existsSync5;
|
|
6205
6604
|
const stdout = await runGit2(sp, opts.cwd, [
|
|
6206
6605
|
"ls-files",
|
|
6207
6606
|
"-c",
|
|
@@ -6209,11 +6608,11 @@ async function listDeployFiles(opts) {
|
|
|
6209
6608
|
"--exclude-standard",
|
|
6210
6609
|
"-z"
|
|
6211
6610
|
]);
|
|
6212
|
-
const paths = stdout.split("\x00").filter((p) => p.length > 0 && !PROTECTED_PATHS.has(p));
|
|
6611
|
+
const paths = stdout.split("\x00").filter((p) => p.length > 0 && exists(resolve7(opts.cwd, p)) && !PROTECTED_PATHS.has(p));
|
|
6213
6612
|
return [...paths].sort();
|
|
6214
6613
|
}
|
|
6215
6614
|
function runGit2(sp, cwd, args) {
|
|
6216
|
-
return new Promise((
|
|
6615
|
+
return new Promise((resolve8, reject) => {
|
|
6217
6616
|
const spawnOpts = { cwd, stdio: ["ignore", "pipe", "pipe"] };
|
|
6218
6617
|
const child = sp("git", [...args], spawnOpts);
|
|
6219
6618
|
let stdout = "";
|
|
@@ -6229,7 +6628,7 @@ function runGit2(sp, cwd, args) {
|
|
|
6229
6628
|
});
|
|
6230
6629
|
child.once("close", (code) => {
|
|
6231
6630
|
if (code === 0) {
|
|
6232
|
-
|
|
6631
|
+
resolve8(stdout);
|
|
6233
6632
|
return;
|
|
6234
6633
|
}
|
|
6235
6634
|
reject(new GitFilesError(`\`git ${args.join(" ")}\` exited ${code}: ${stderr.trim()}`));
|
|
@@ -6806,7 +7205,7 @@ function parseDeployFlags(args) {
|
|
|
6806
7205
|
}
|
|
6807
7206
|
|
|
6808
7207
|
// src/deploy/apply.ts
|
|
6809
|
-
import { existsSync as
|
|
7208
|
+
import { existsSync as existsSync6, rmSync } from "node:fs";
|
|
6810
7209
|
import { resolve as resolvePath, join as join10 } from "node:path";
|
|
6811
7210
|
|
|
6812
7211
|
// src/manifest/load.ts
|
|
@@ -7173,7 +7572,7 @@ function sleep(ms) {
|
|
|
7173
7572
|
}
|
|
7174
7573
|
function deletePinIfPresent(cfg, slug, io) {
|
|
7175
7574
|
const pinPath = join10(cfg.stateDir, slug, "group.json");
|
|
7176
|
-
if (!
|
|
7575
|
+
if (!existsSync6(pinPath))
|
|
7177
7576
|
return;
|
|
7178
7577
|
try {
|
|
7179
7578
|
rmSync(pinPath, { force: true });
|
|
@@ -7311,12 +7710,12 @@ async function handleAccessDrift(params) {
|
|
|
7311
7710
|
|
|
7312
7711
|
// src/deploy/dry-run.ts
|
|
7313
7712
|
import { execFileSync } from "node:child_process";
|
|
7314
|
-
import { resolve as
|
|
7713
|
+
import { resolve as resolve8 } from "node:path";
|
|
7315
7714
|
var MANIFEST_SHA_REGEX2 = /^[0-9a-f]{40}$/;
|
|
7316
7715
|
async function runDeployDryRun(opts, io, deps = {}) {
|
|
7317
7716
|
const cfg = deps.config ?? loadConfig();
|
|
7318
7717
|
const fetcher = deps.fetcher ?? fetch;
|
|
7319
|
-
const manifestPath =
|
|
7718
|
+
const manifestPath = resolve8(process.cwd(), opts.file ?? "launchpad.yaml");
|
|
7320
7719
|
const loaded = loadManifest(manifestPath);
|
|
7321
7720
|
if (loaded.kind !== "ok") {
|
|
7322
7721
|
return renderManifestError2(loaded, opts.json, io);
|
|
@@ -7674,6 +8073,81 @@ function handleNetworkError(e, io, slug, verb) {
|
|
|
7674
8073
|
return 1;
|
|
7675
8074
|
}
|
|
7676
8075
|
|
|
8076
|
+
// src/manifest/container-filesystem.ts
|
|
8077
|
+
import { lstatSync as lstatSync3, realpathSync } from "node:fs";
|
|
8078
|
+
import { dirname as dirname6, join as join11, relative as relative3, resolve as resolve9, sep as sep2 } from "node:path";
|
|
8079
|
+
function validateContainerFilesystem(manifestPath, capability) {
|
|
8080
|
+
const root = realpathSync(dirname6(manifestPath));
|
|
8081
|
+
const segments = capability.container.image.slice(2).split("/");
|
|
8082
|
+
let current = root;
|
|
8083
|
+
for (const segment of segments) {
|
|
8084
|
+
current = join11(current, segment);
|
|
8085
|
+
let stat;
|
|
8086
|
+
try {
|
|
8087
|
+
stat = lstatSync3(current);
|
|
8088
|
+
} catch (error) {
|
|
8089
|
+
return failure("container_image_unreadable", relative3(root, current), `cannot inspect container image path: ${describe15(error)}`);
|
|
8090
|
+
}
|
|
8091
|
+
if (stat.isSymbolicLink()) {
|
|
8092
|
+
return failure("container_image_symlink", relative3(root, current), "container image path must not contain symbolic links");
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
let finalPath;
|
|
8096
|
+
try {
|
|
8097
|
+
finalPath = realpathSync(current);
|
|
8098
|
+
} catch (error) {
|
|
8099
|
+
return failure("container_image_unreadable", relative3(root, current), `cannot resolve container image path: ${describe15(error)}`);
|
|
8100
|
+
}
|
|
8101
|
+
const rootPrefix = resolve9(root) + sep2;
|
|
8102
|
+
if (!resolve9(finalPath).startsWith(rootPrefix)) {
|
|
8103
|
+
return failure("container_image_outside_repository", capability.container.image, "container image resolves outside the app repository");
|
|
8104
|
+
}
|
|
8105
|
+
if (!lstatSync3(finalPath).isFile()) {
|
|
8106
|
+
return failure("container_image_not_regular_file", capability.container.image, "container image must resolve to a regular Dockerfile");
|
|
8107
|
+
}
|
|
8108
|
+
return { kind: "ok", dockerfilePath: finalPath };
|
|
8109
|
+
}
|
|
8110
|
+
function failure(reason, path8, message) {
|
|
8111
|
+
return { kind: "error", reason, path: path8, message };
|
|
8112
|
+
}
|
|
8113
|
+
function describe15(error) {
|
|
8114
|
+
return error instanceof Error ? error.message : String(error);
|
|
8115
|
+
}
|
|
8116
|
+
|
|
8117
|
+
// src/manifest/runtime-capability.ts
|
|
8118
|
+
function validateCliRuntimeCapability(manifestPath, manifest) {
|
|
8119
|
+
const result = validateDeploymentCapability(manifest);
|
|
8120
|
+
if (result.kind === "unsupported" || result.runtime === "pages") {
|
|
8121
|
+
return result;
|
|
8122
|
+
}
|
|
8123
|
+
const filesystem = validateContainerFilesystem(manifestPath, result.capability);
|
|
8124
|
+
return filesystem.kind === "error" ? filesystem : result;
|
|
8125
|
+
}
|
|
8126
|
+
function rejectUnsupportedCliRuntime(args) {
|
|
8127
|
+
const result = validateCliRuntimeCapability(args.manifestPath, args.manifest);
|
|
8128
|
+
if (result.kind !== "unsupported" && result.kind !== "error")
|
|
8129
|
+
return null;
|
|
8130
|
+
if (args.json) {
|
|
8131
|
+
args.io.out(JSON.stringify({
|
|
8132
|
+
ok: false,
|
|
8133
|
+
manifest_valid: true,
|
|
8134
|
+
runtime_supported: false,
|
|
8135
|
+
reason: result.reason,
|
|
8136
|
+
path: result.path,
|
|
8137
|
+
message: result.message
|
|
8138
|
+
}));
|
|
8139
|
+
return 1;
|
|
8140
|
+
}
|
|
8141
|
+
if (args.structurallyValidName !== undefined) {
|
|
8142
|
+
args.io.out(`✓ ${args.structurallyValidName} - manifest is structurally valid`);
|
|
8143
|
+
args.io.err(`✗ runtime unsupported: ${result.reason}`);
|
|
8144
|
+
} else {
|
|
8145
|
+
args.io.err(`launchpad ${args.command}: runtime unsupported (${result.reason})`);
|
|
8146
|
+
}
|
|
8147
|
+
args.io.err(` ${result.path}: ${result.message}`);
|
|
8148
|
+
return 1;
|
|
8149
|
+
}
|
|
8150
|
+
|
|
7677
8151
|
// src/commands/deploy.ts
|
|
7678
8152
|
var deployCommand = {
|
|
7679
8153
|
name: "deploy",
|
|
@@ -7726,7 +8200,7 @@ async function runDeploy(args, io) {
|
|
|
7726
8200
|
}
|
|
7727
8201
|
const cwd = process.cwd();
|
|
7728
8202
|
const manifestPath = path8.join(cwd, "launchpad.yaml");
|
|
7729
|
-
if (
|
|
8203
|
+
if (existsSync7(manifestPath)) {
|
|
7730
8204
|
const contentMode = flags.mode.kind === "content" ? flags.mode : {
|
|
7731
8205
|
allowStale: false,
|
|
7732
8206
|
rebaseOntoHead: false,
|
|
@@ -7780,7 +8254,7 @@ async function runDeploy(args, io) {
|
|
|
7780
8254
|
}
|
|
7781
8255
|
let contentManifestYaml = null;
|
|
7782
8256
|
const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
|
|
7783
|
-
if (
|
|
8257
|
+
if (existsSync7(contentManifestPath)) {
|
|
7784
8258
|
try {
|
|
7785
8259
|
contentManifestYaml = readFileSync10(contentManifestPath, "utf8");
|
|
7786
8260
|
} catch {
|
|
@@ -7860,7 +8334,7 @@ async function runDeploy(args, io) {
|
|
|
7860
8334
|
io.err(`launchpad deploy: ${e.message}`);
|
|
7861
8335
|
return 1;
|
|
7862
8336
|
}
|
|
7863
|
-
io.err(`launchpad deploy failed: ${
|
|
8337
|
+
io.err(`launchpad deploy failed: ${describe16(e)}`);
|
|
7864
8338
|
return 1;
|
|
7865
8339
|
}
|
|
7866
8340
|
}
|
|
@@ -7931,7 +8405,7 @@ function formatBytes2(n) {
|
|
|
7931
8405
|
return `${(n / 1024).toFixed(1)}KB`;
|
|
7932
8406
|
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
7933
8407
|
}
|
|
7934
|
-
function
|
|
8408
|
+
function describe16(e) {
|
|
7935
8409
|
return e instanceof Error ? e.message : String(e);
|
|
7936
8410
|
}
|
|
7937
8411
|
function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
@@ -8003,11 +8477,11 @@ function surfaceStaleBlock(body, io) {
|
|
|
8003
8477
|
}
|
|
8004
8478
|
var MANIFEST_DIFF_READ_TIMEOUT_MS = 5000;
|
|
8005
8479
|
function withTimeout(p, ms) {
|
|
8006
|
-
return new Promise((
|
|
8480
|
+
return new Promise((resolve10, reject) => {
|
|
8007
8481
|
const timer = setTimeout(() => reject(new Error(`manifest read timed out after ${ms}ms`)), ms);
|
|
8008
8482
|
p.then((v) => {
|
|
8009
8483
|
clearTimeout(timer);
|
|
8010
|
-
|
|
8484
|
+
resolve10(v);
|
|
8011
8485
|
}, (e) => {
|
|
8012
8486
|
clearTimeout(timer);
|
|
8013
8487
|
reject(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -8054,7 +8528,7 @@ async function advanceBaseShaMarker(cwd, sha, io) {
|
|
|
8054
8528
|
return;
|
|
8055
8529
|
await writeBaseShaMarker(cwd, sha);
|
|
8056
8530
|
} catch (e) {
|
|
8057
|
-
io.err(`warning: couldn't update the local base-sha marker (${
|
|
8531
|
+
io.err(`warning: couldn't update the local base-sha marker (${describe16(e)}); ` + `a future deploy may re-warn about staleness. Harmless — your deploy landed.`);
|
|
8058
8532
|
}
|
|
8059
8533
|
}
|
|
8060
8534
|
async function runModelADeploy(args) {
|
|
@@ -8069,12 +8543,30 @@ async function runModelADeploy(args) {
|
|
|
8069
8543
|
return 64;
|
|
8070
8544
|
}
|
|
8071
8545
|
slug = metaSlug;
|
|
8072
|
-
const
|
|
8073
|
-
|
|
8074
|
-
|
|
8546
|
+
const raw = manifestObj;
|
|
8547
|
+
const isV2 = raw?.apiVersion === "launchpad.m-kopa.us/v2";
|
|
8548
|
+
const deploymentType = isV2 ? raw?.spec?.appType : raw?.deployment?.type;
|
|
8549
|
+
if (deploymentType === "static" || deploymentType === "react" || deploymentType === "react+api" || deploymentType === "container") {
|
|
8550
|
+
appType = deploymentType;
|
|
8551
|
+
}
|
|
8552
|
+
if (deploymentType === "container") {
|
|
8553
|
+
const parsed = isV2 ? parseManifestV2(manifestObj) : parseManifest(manifestObj);
|
|
8554
|
+
if (parsed.kind !== "ok") {
|
|
8555
|
+
io.err("launchpad deploy: manifest is structurally invalid:");
|
|
8556
|
+
for (const issue of parsed.issues) {
|
|
8557
|
+
io.err(` ${issue.path}: ${issue.message}`);
|
|
8558
|
+
}
|
|
8559
|
+
return 1;
|
|
8560
|
+
}
|
|
8561
|
+
const capability = validateCliRuntimeCapability(manifestPath, parsed.manifest);
|
|
8562
|
+
if (capability.kind === "unsupported" || capability.kind === "error") {
|
|
8563
|
+
io.err(`launchpad deploy: runtime unsupported (${capability.reason})`);
|
|
8564
|
+
io.err(` ${capability.path}: ${capability.message}`);
|
|
8565
|
+
return 1;
|
|
8566
|
+
}
|
|
8075
8567
|
}
|
|
8076
8568
|
} catch (e) {
|
|
8077
|
-
io.err(`launchpad deploy: failed to read ${manifestPath}: ${
|
|
8569
|
+
io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe16(e)}`);
|
|
8078
8570
|
return 1;
|
|
8079
8571
|
}
|
|
8080
8572
|
const noVerify = args.argv.includes("--no-verify");
|
|
@@ -8104,7 +8596,7 @@ async function runModelADeploy(args) {
|
|
|
8104
8596
|
try {
|
|
8105
8597
|
cfg = loadConfig();
|
|
8106
8598
|
} catch (e) {
|
|
8107
|
-
io.err(`launchpad deploy: ${
|
|
8599
|
+
io.err(`launchpad deploy: ${describe16(e)}`);
|
|
8108
8600
|
return 1;
|
|
8109
8601
|
}
|
|
8110
8602
|
try {
|
|
@@ -8154,7 +8646,7 @@ async function runModelADeploy(args) {
|
|
|
8154
8646
|
io.err(" run `launchpad login` to refresh your session.");
|
|
8155
8647
|
return 1;
|
|
8156
8648
|
}
|
|
8157
|
-
io.err(`launchpad deploy: unexpected error: ${
|
|
8649
|
+
io.err(`launchpad deploy: unexpected error: ${describe16(e)}`);
|
|
8158
8650
|
return 1;
|
|
8159
8651
|
}
|
|
8160
8652
|
switch (result.kind) {
|
|
@@ -8366,7 +8858,7 @@ function pendingReconcileOutcome(body) {
|
|
|
8366
8858
|
}
|
|
8367
8859
|
|
|
8368
8860
|
// src/commands/redeploy.ts
|
|
8369
|
-
import { existsSync as
|
|
8861
|
+
import { existsSync as existsSync8, readFileSync as readFileSync11 } from "node:fs";
|
|
8370
8862
|
import { resolve as resolvePath2 } from "node:path";
|
|
8371
8863
|
import { parse as parseYaml7 } from "yaml";
|
|
8372
8864
|
var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
@@ -8444,7 +8936,7 @@ async function runRedeploy(opts, io, deps = {}) {
|
|
|
8444
8936
|
}
|
|
8445
8937
|
function readLocalAppType(cwd, file) {
|
|
8446
8938
|
const manifestPath = resolvePath2(cwd, file ?? "launchpad.yaml");
|
|
8447
|
-
if (!
|
|
8939
|
+
if (!existsSync8(manifestPath))
|
|
8448
8940
|
return null;
|
|
8449
8941
|
try {
|
|
8450
8942
|
const obj = parseYaml7(readFileSync11(manifestPath, "utf8"));
|
|
@@ -8629,7 +9121,7 @@ async function runEnvvars(args, io) {
|
|
|
8629
9121
|
io.err(`launchpad envvars: ${e.message}`);
|
|
8630
9122
|
return 1;
|
|
8631
9123
|
}
|
|
8632
|
-
io.err(`launchpad envvars failed: ${
|
|
9124
|
+
io.err(`launchpad envvars failed: ${describe17(e)}`);
|
|
8633
9125
|
return 1;
|
|
8634
9126
|
}
|
|
8635
9127
|
}
|
|
@@ -8726,16 +9218,16 @@ function renderList(envVars, io) {
|
|
|
8726
9218
|
io.out(fmt(row));
|
|
8727
9219
|
}
|
|
8728
9220
|
}
|
|
8729
|
-
function
|
|
9221
|
+
function describe17(e) {
|
|
8730
9222
|
return e instanceof Error ? e.message : String(e);
|
|
8731
9223
|
}
|
|
8732
9224
|
|
|
8733
9225
|
// src/commands/generate.ts
|
|
8734
9226
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8735
|
-
import { dirname as
|
|
9227
|
+
import { dirname as dirname7, resolve as resolve10, relative as relative4 } from "node:path";
|
|
8736
9228
|
var generateCommand = {
|
|
8737
9229
|
name: "generate",
|
|
8738
|
-
summary: "emit derived artefacts
|
|
9230
|
+
summary: "emit derived runtime artefacts from launchpad.yaml",
|
|
8739
9231
|
run: runGenerate
|
|
8740
9232
|
};
|
|
8741
9233
|
async function runGenerate(args, io) {
|
|
@@ -8745,33 +9237,50 @@ async function runGenerate(args, io) {
|
|
|
8745
9237
|
io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
|
|
8746
9238
|
return 64;
|
|
8747
9239
|
}
|
|
8748
|
-
const manifestPath =
|
|
9240
|
+
const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
8749
9241
|
const result = loadManifest(manifestPath);
|
|
8750
9242
|
if (result.kind !== "ok") {
|
|
8751
9243
|
return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
|
|
8752
9244
|
}
|
|
8753
|
-
const
|
|
8754
|
-
|
|
8755
|
-
|
|
9245
|
+
const unsupported2 = rejectUnsupportedCliRuntime({
|
|
9246
|
+
command: "generate",
|
|
9247
|
+
manifestPath,
|
|
9248
|
+
manifest: result.manifest,
|
|
9249
|
+
io,
|
|
9250
|
+
json: flags.json
|
|
9251
|
+
});
|
|
9252
|
+
if (unsupported2 !== null)
|
|
9253
|
+
return unsupported2;
|
|
9254
|
+
const appRoot = dirname7(manifestPath);
|
|
9255
|
+
const wranglerPath = resolve10(appRoot, "container", "wrangler.toml");
|
|
9256
|
+
const workerPath = resolve10(appRoot, "container", "worker", "index.ts");
|
|
9257
|
+
const workflowPath = resolve10(appRoot, ".github", "workflows", "deploy.yml");
|
|
8756
9258
|
const wranglerOut = generateWranglerToml(result.manifest);
|
|
9259
|
+
const workerRaw = generateContainerWorker(result.manifest);
|
|
9260
|
+
const workerOut = workerRaw.kind === "ok" ? { kind: "ok", content: workerRaw.content, warnings: [] } : workerRaw;
|
|
8757
9261
|
const workflowOut = generateGithubDeployWorkflow(result.manifest);
|
|
8758
9262
|
if (flags.dryRun) {
|
|
8759
9263
|
return renderDryRun([
|
|
8760
9264
|
{ artefact: "wrangler.toml", path: wranglerPath, result: wranglerOut },
|
|
9265
|
+
{ artefact: "worker/index.ts", path: workerPath, result: workerOut },
|
|
8761
9266
|
{ artefact: "deploy.yml", path: workflowPath, result: workflowOut }
|
|
8762
9267
|
], appRoot, flags.json, io);
|
|
8763
9268
|
}
|
|
8764
9269
|
const reports = [
|
|
8765
9270
|
applyOne("wrangler.toml", wranglerPath, wranglerOut, flags.force),
|
|
9271
|
+
applyOne("worker/index.ts", workerPath, workerOut, flags.force),
|
|
8766
9272
|
applyOne("deploy.yml", workflowPath, workflowOut, flags.force)
|
|
8767
9273
|
];
|
|
8768
9274
|
return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
|
|
8769
9275
|
}
|
|
8770
9276
|
function applyOne(artefact, path10, out, force) {
|
|
8771
|
-
if (out.kind === "not-applicable") {
|
|
9277
|
+
if (out.kind === "not-applicable" || out.kind === "unsupported") {
|
|
8772
9278
|
return {
|
|
8773
9279
|
artefact,
|
|
8774
|
-
action: {
|
|
9280
|
+
action: {
|
|
9281
|
+
kind: "not-applicable",
|
|
9282
|
+
reason: out.kind === "unsupported" ? `${out.reason}: ${out.message}` : out.reason
|
|
9283
|
+
},
|
|
8775
9284
|
warnings: []
|
|
8776
9285
|
};
|
|
8777
9286
|
}
|
|
@@ -8794,7 +9303,7 @@ function applyOne(artefact, path10, out, force) {
|
|
|
8794
9303
|
};
|
|
8795
9304
|
}
|
|
8796
9305
|
try {
|
|
8797
|
-
mkdirSync2(
|
|
9306
|
+
mkdirSync2(dirname7(path10), { recursive: true });
|
|
8798
9307
|
writeFileSync2(path10, out.content, "utf8");
|
|
8799
9308
|
return {
|
|
8800
9309
|
artefact,
|
|
@@ -8830,12 +9339,12 @@ function readIfExists(path10) {
|
|
|
8830
9339
|
function renderDryRun(inputs, appRoot, json, io) {
|
|
8831
9340
|
if (json) {
|
|
8832
9341
|
const files = inputs.map((i) => {
|
|
8833
|
-
const previewPath = i.preview === undefined ?
|
|
8834
|
-
if (i.result.kind === "not-applicable") {
|
|
9342
|
+
const previewPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
|
|
9343
|
+
if (i.result.kind === "not-applicable" || i.result.kind === "unsupported") {
|
|
8835
9344
|
return {
|
|
8836
9345
|
artefact: i.artefact,
|
|
8837
9346
|
kind: "not-applicable",
|
|
8838
|
-
reason: i.result.reason,
|
|
9347
|
+
reason: i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason,
|
|
8839
9348
|
...i.preview ? { preview: true, destination: i.preview.destination } : {}
|
|
8840
9349
|
};
|
|
8841
9350
|
}
|
|
@@ -8852,12 +9361,13 @@ function renderDryRun(inputs, appRoot, json, io) {
|
|
|
8852
9361
|
return 0;
|
|
8853
9362
|
}
|
|
8854
9363
|
for (const i of inputs) {
|
|
8855
|
-
if (i.result.kind === "not-applicable") {
|
|
8856
|
-
|
|
9364
|
+
if (i.result.kind === "not-applicable" || i.result.kind === "unsupported") {
|
|
9365
|
+
const reason = i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason;
|
|
9366
|
+
io.out(`# ${i.artefact}: skipped - ${reason}`);
|
|
8857
9367
|
io.out("");
|
|
8858
9368
|
continue;
|
|
8859
9369
|
}
|
|
8860
|
-
const headerPath = i.preview === undefined ?
|
|
9370
|
+
const headerPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
|
|
8861
9371
|
const header = i.preview === undefined ? `--- ${headerPath} ---` : `--- [preview, destination = ${i.preview.destination}] ${headerPath} ---`;
|
|
8862
9372
|
io.out(header);
|
|
8863
9373
|
io.out(i.result.content);
|
|
@@ -8876,20 +9386,20 @@ function renderApplyHuman(reports, appRoot, io) {
|
|
|
8876
9386
|
const a = r.action;
|
|
8877
9387
|
switch (a.kind) {
|
|
8878
9388
|
case "written":
|
|
8879
|
-
io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${
|
|
9389
|
+
io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${relative4(appRoot, a.path)}`);
|
|
8880
9390
|
break;
|
|
8881
9391
|
case "unchanged":
|
|
8882
|
-
io.out(`= ${r.artefact} — unchanged (${
|
|
9392
|
+
io.out(`= ${r.artefact} — unchanged (${relative4(appRoot, a.path)})`);
|
|
8883
9393
|
break;
|
|
8884
9394
|
case "would-overwrite":
|
|
8885
|
-
io.err(`! ${r.artefact} — exists with different content (${
|
|
9395
|
+
io.err(`! ${r.artefact} — exists with different content (${relative4(appRoot, a.path)}); re-run with --force to overwrite`);
|
|
8886
9396
|
wouldOverwrite++;
|
|
8887
9397
|
break;
|
|
8888
9398
|
case "not-applicable":
|
|
8889
9399
|
io.out(`- ${r.artefact} — skipped (${a.reason})`);
|
|
8890
9400
|
break;
|
|
8891
9401
|
case "write-error":
|
|
8892
|
-
io.err(`✗ ${r.artefact} — ${a.message} (${
|
|
9402
|
+
io.err(`✗ ${r.artefact} — ${a.message} (${relative4(appRoot, a.path)})`);
|
|
8893
9403
|
writeError++;
|
|
8894
9404
|
break;
|
|
8895
9405
|
}
|
|
@@ -8908,17 +9418,17 @@ function renderApplyJson(reports, appRoot, io) {
|
|
|
8908
9418
|
const base = { artefact: r.artefact, warnings: r.warnings };
|
|
8909
9419
|
switch (a.kind) {
|
|
8910
9420
|
case "written":
|
|
8911
|
-
return { ...base, kind: a.kind, path:
|
|
9421
|
+
return { ...base, kind: a.kind, path: relative4(appRoot, a.path), bytes: a.bytes };
|
|
8912
9422
|
case "unchanged":
|
|
8913
9423
|
case "would-overwrite":
|
|
8914
|
-
return { ...base, kind: a.kind, path:
|
|
9424
|
+
return { ...base, kind: a.kind, path: relative4(appRoot, a.path) };
|
|
8915
9425
|
case "not-applicable":
|
|
8916
9426
|
return { ...base, kind: a.kind, reason: a.reason };
|
|
8917
9427
|
case "write-error":
|
|
8918
9428
|
return {
|
|
8919
9429
|
...base,
|
|
8920
9430
|
kind: a.kind,
|
|
8921
|
-
path:
|
|
9431
|
+
path: relative4(appRoot, a.path),
|
|
8922
9432
|
message: a.message
|
|
8923
9433
|
};
|
|
8924
9434
|
}
|
|
@@ -9042,13 +9552,13 @@ function decodeJwtPayload(token) {
|
|
|
9042
9552
|
try {
|
|
9043
9553
|
json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
|
|
9044
9554
|
} catch (e) {
|
|
9045
|
-
throw new JwtParseError(`could not base64-decode JWT payload: ${
|
|
9555
|
+
throw new JwtParseError(`could not base64-decode JWT payload: ${describe18(e)}`);
|
|
9046
9556
|
}
|
|
9047
9557
|
let parsed;
|
|
9048
9558
|
try {
|
|
9049
9559
|
parsed = JSON.parse(json);
|
|
9050
9560
|
} catch (e) {
|
|
9051
|
-
throw new JwtParseError(`JWT payload is not JSON: ${
|
|
9561
|
+
throw new JwtParseError(`JWT payload is not JSON: ${describe18(e)}`);
|
|
9052
9562
|
}
|
|
9053
9563
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9054
9564
|
throw new JwtParseError(`JWT payload is not an object`);
|
|
@@ -9059,7 +9569,7 @@ function b64UrlToB64(s) {
|
|
|
9059
9569
|
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
9060
9570
|
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
9061
9571
|
}
|
|
9062
|
-
function
|
|
9572
|
+
function describe18(e) {
|
|
9063
9573
|
return e instanceof Error ? e.message : String(e);
|
|
9064
9574
|
}
|
|
9065
9575
|
|
|
@@ -9086,7 +9596,7 @@ async function runGroupsWhoami(args, io) {
|
|
|
9086
9596
|
try {
|
|
9087
9597
|
payload = decodeJwtPayload(session.accessToken);
|
|
9088
9598
|
} catch (e) {
|
|
9089
|
-
const message = e instanceof JwtParseError ? e.message :
|
|
9599
|
+
const message = e instanceof JwtParseError ? e.message : describe19(e);
|
|
9090
9600
|
if (json) {
|
|
9091
9601
|
io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
|
|
9092
9602
|
} else {
|
|
@@ -9167,7 +9677,7 @@ function parseFlags2(args) {
|
|
|
9167
9677
|
}
|
|
9168
9678
|
return { kind: "ok", json };
|
|
9169
9679
|
}
|
|
9170
|
-
function
|
|
9680
|
+
function describe19(e) {
|
|
9171
9681
|
return e instanceof Error ? e.message : String(e);
|
|
9172
9682
|
}
|
|
9173
9683
|
|
|
@@ -9270,7 +9780,7 @@ async function loadGroups(io, refresh, json) {
|
|
|
9270
9780
|
}
|
|
9271
9781
|
return { kind: "error", code: 3 };
|
|
9272
9782
|
}
|
|
9273
|
-
renderFetchError(io,
|
|
9783
|
+
renderFetchError(io, describe20(e), json);
|
|
9274
9784
|
return { kind: "error", code: 2 };
|
|
9275
9785
|
}
|
|
9276
9786
|
}
|
|
@@ -9307,7 +9817,7 @@ async function lookupTenantWide(key) {
|
|
|
9307
9817
|
return { kind: "unauthenticated", message: e.message };
|
|
9308
9818
|
if (e instanceof ForbiddenError)
|
|
9309
9819
|
return { kind: "forbidden", message: e.message };
|
|
9310
|
-
return { kind: "error", message:
|
|
9820
|
+
return { kind: "error", message: describe20(e) };
|
|
9311
9821
|
}
|
|
9312
9822
|
}
|
|
9313
9823
|
function renderLookupFailure(io, json, hit) {
|
|
@@ -9453,9 +9963,9 @@ async function runShow(args, io) {
|
|
|
9453
9963
|
}
|
|
9454
9964
|
const key = parsed.flags.positional[0];
|
|
9455
9965
|
const hit = await lookupTenantWide(key);
|
|
9456
|
-
const
|
|
9457
|
-
if (
|
|
9458
|
-
return
|
|
9966
|
+
const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
|
|
9967
|
+
if (failure2 !== null)
|
|
9968
|
+
return failure2;
|
|
9459
9969
|
if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
|
|
9460
9970
|
return 2;
|
|
9461
9971
|
}
|
|
@@ -9502,9 +10012,9 @@ async function runResolve(args, io) {
|
|
|
9502
10012
|
}
|
|
9503
10013
|
const key = parsed.flags.positional[0];
|
|
9504
10014
|
const hit = await lookupTenantWide(key);
|
|
9505
|
-
const
|
|
9506
|
-
if (
|
|
9507
|
-
return
|
|
10015
|
+
const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
|
|
10016
|
+
if (failure2 !== null)
|
|
10017
|
+
return failure2;
|
|
9508
10018
|
if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
|
|
9509
10019
|
return 2;
|
|
9510
10020
|
}
|
|
@@ -9551,23 +10061,23 @@ function renderTable2(io, groups) {
|
|
|
9551
10061
|
for (const r of rows)
|
|
9552
10062
|
io.out(fmt(r));
|
|
9553
10063
|
}
|
|
9554
|
-
function
|
|
10064
|
+
function describe20(e) {
|
|
9555
10065
|
return e instanceof Error ? e.message : String(e);
|
|
9556
10066
|
}
|
|
9557
10067
|
|
|
9558
10068
|
// src/commands/init.ts
|
|
9559
10069
|
import { createInterface } from "node:readline/promises";
|
|
9560
|
-
import { existsSync as
|
|
9561
|
-
import { dirname as
|
|
10070
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
|
|
10071
|
+
import { dirname as dirname8, resolve as resolve11 } from "node:path";
|
|
9562
10072
|
import { stringify as yamlStringify } from "yaml";
|
|
9563
10073
|
|
|
9564
10074
|
// src/detect/index.ts
|
|
9565
|
-
import { existsSync as
|
|
9566
|
-
import { join as
|
|
10075
|
+
import { existsSync as existsSync9, readFileSync as readFileSync13, statSync } from "node:fs";
|
|
10076
|
+
import { join as join13 } from "node:path";
|
|
9567
10077
|
function detectAppShape(cwd) {
|
|
9568
|
-
const hasPackageJson =
|
|
9569
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) =>
|
|
9570
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) =>
|
|
10078
|
+
const hasPackageJson = existsSync9(join13(cwd, "package.json"));
|
|
10079
|
+
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(join13(cwd, file)));
|
|
10080
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(join13(cwd, n)));
|
|
9571
10081
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
9572
10082
|
return {
|
|
9573
10083
|
kind: "not-applicable",
|
|
@@ -9613,7 +10123,7 @@ var LOCKFILES = [
|
|
|
9613
10123
|
{ file: "yarn.lock", pm: "yarn" }
|
|
9614
10124
|
];
|
|
9615
10125
|
function detectPackageManager(cwd) {
|
|
9616
|
-
const present = LOCKFILES.filter(({ file }) =>
|
|
10126
|
+
const present = LOCKFILES.filter(({ file }) => existsSync9(join13(cwd, file)));
|
|
9617
10127
|
if (present.length === 0) {
|
|
9618
10128
|
return {
|
|
9619
10129
|
kind: "ambiguous",
|
|
@@ -9635,8 +10145,8 @@ function detectPackageManager(cwd) {
|
|
|
9635
10145
|
}
|
|
9636
10146
|
function detectVitePresence(cwd) {
|
|
9637
10147
|
for (const name of VITE_CONFIG_NAMES) {
|
|
9638
|
-
const p =
|
|
9639
|
-
if (
|
|
10148
|
+
const p = join13(cwd, name);
|
|
10149
|
+
if (existsSync9(p)) {
|
|
9640
10150
|
return { kind: "ok", value: { path: p } };
|
|
9641
10151
|
}
|
|
9642
10152
|
}
|
|
@@ -9646,9 +10156,9 @@ function detectVitePresence(cwd) {
|
|
|
9646
10156
|
};
|
|
9647
10157
|
}
|
|
9648
10158
|
function detectAppType(cwd) {
|
|
9649
|
-
const fnDir =
|
|
10159
|
+
const fnDir = join13(cwd, "functions");
|
|
9650
10160
|
let hasFunctionsDir = false;
|
|
9651
|
-
if (
|
|
10161
|
+
if (existsSync9(fnDir)) {
|
|
9652
10162
|
try {
|
|
9653
10163
|
hasFunctionsDir = statSync(fnDir).isDirectory();
|
|
9654
10164
|
} catch {
|
|
@@ -9658,8 +10168,8 @@ function detectAppType(cwd) {
|
|
|
9658
10168
|
return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
|
|
9659
10169
|
}
|
|
9660
10170
|
function detectBuildCommand(cwd, pm) {
|
|
9661
|
-
const pkgJsonPath =
|
|
9662
|
-
if (!
|
|
10171
|
+
const pkgJsonPath = join13(cwd, "package.json");
|
|
10172
|
+
if (!existsSync9(pkgJsonPath)) {
|
|
9663
10173
|
return {
|
|
9664
10174
|
kind: "ambiguous",
|
|
9665
10175
|
reason: "no package.json at repo root. Run your package manager's `init` first."
|
|
@@ -9731,8 +10241,8 @@ async function runInit(args, io, prompt) {
|
|
|
9731
10241
|
return 64;
|
|
9732
10242
|
}
|
|
9733
10243
|
const { inputs, options } = parsed;
|
|
9734
|
-
const outPath =
|
|
9735
|
-
if (
|
|
10244
|
+
const outPath = resolve11(process.cwd(), options.out);
|
|
10245
|
+
if (existsSync10(outPath) && !options.force) {
|
|
9736
10246
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
9737
10247
|
io.err("Pass --force to overwrite.");
|
|
9738
10248
|
return 64;
|
|
@@ -9779,7 +10289,7 @@ async function runInit(args, io, prompt) {
|
|
|
9779
10289
|
}
|
|
9780
10290
|
io.out(`✓ wrote ${outPath}`);
|
|
9781
10291
|
if (options.gitignore) {
|
|
9782
|
-
const gitignorePath =
|
|
10292
|
+
const gitignorePath = resolve11(process.cwd(), ".gitignore");
|
|
9783
10293
|
try {
|
|
9784
10294
|
const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
|
|
9785
10295
|
if (changed.length > 0) {
|
|
@@ -9794,9 +10304,9 @@ async function runInit(args, io, prompt) {
|
|
|
9794
10304
|
}
|
|
9795
10305
|
}
|
|
9796
10306
|
if (fillResult.inputs.type === "static") {
|
|
9797
|
-
const serveDir =
|
|
9798
|
-
const indexPath =
|
|
9799
|
-
if (!
|
|
10307
|
+
const serveDir = resolve11(dirname8(outPath), STATIC_SERVE_DIR);
|
|
10308
|
+
const indexPath = resolve11(serveDir, "index.html");
|
|
10309
|
+
if (!existsSync10(indexPath)) {
|
|
9800
10310
|
try {
|
|
9801
10311
|
mkdirSync3(serveDir, { recursive: true });
|
|
9802
10312
|
writeFileSync3(indexPath, starterIndexHtml(fillResult.inputs.name), {
|
|
@@ -10188,7 +10698,7 @@ function renderYaml(manifest) {
|
|
|
10188
10698
|
}
|
|
10189
10699
|
function ensureGitignoreEntries(path10, entries) {
|
|
10190
10700
|
let current = "";
|
|
10191
|
-
if (
|
|
10701
|
+
if (existsSync10(path10)) {
|
|
10192
10702
|
current = readFileSync14(path10, "utf8");
|
|
10193
10703
|
}
|
|
10194
10704
|
const lines = current.split(/\r?\n/);
|
|
@@ -10262,13 +10772,13 @@ async function runLogin(args, io, deps) {
|
|
|
10262
10772
|
io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
|
|
10263
10773
|
return 0;
|
|
10264
10774
|
} catch (e) {
|
|
10265
|
-
io.err(`Gateway login failed: ${
|
|
10775
|
+
io.err(`Gateway login failed: ${describe21(e)}`);
|
|
10266
10776
|
io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
|
|
10267
10777
|
io.err("");
|
|
10268
10778
|
return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
|
|
10269
10779
|
}
|
|
10270
10780
|
} catch (e) {
|
|
10271
|
-
io.err(`launchpad login failed: ${
|
|
10781
|
+
io.err(`launchpad login failed: ${describe21(e)}`);
|
|
10272
10782
|
return 1;
|
|
10273
10783
|
}
|
|
10274
10784
|
}
|
|
@@ -10310,7 +10820,7 @@ function printLoginHelp(io) {
|
|
|
10310
10820
|
io.out(" 0 signed in / session stored");
|
|
10311
10821
|
io.out(" 1 login failed (network, browser, or gateway error)");
|
|
10312
10822
|
}
|
|
10313
|
-
function
|
|
10823
|
+
function describe21(e) {
|
|
10314
10824
|
return e instanceof Error ? e.message : String(e);
|
|
10315
10825
|
}
|
|
10316
10826
|
|
|
@@ -10347,14 +10857,14 @@ async function runLogout(args, io, deps) {
|
|
|
10347
10857
|
});
|
|
10348
10858
|
io.out("Server-side session revoked.");
|
|
10349
10859
|
} catch (e) {
|
|
10350
|
-
io.err(`warning: could not revoke the session server-side (${
|
|
10860
|
+
io.err(`warning: could not revoke the session server-side (${describe22(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
|
|
10351
10861
|
}
|
|
10352
10862
|
}
|
|
10353
10863
|
const had = await clearSession(cfg.sessionPath);
|
|
10354
10864
|
io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
|
|
10355
10865
|
return 0;
|
|
10356
10866
|
} catch (e) {
|
|
10357
|
-
io.err(`launchpad logout failed: ${
|
|
10867
|
+
io.err(`launchpad logout failed: ${describe22(e)}`);
|
|
10358
10868
|
return 1;
|
|
10359
10869
|
}
|
|
10360
10870
|
}
|
|
@@ -10374,7 +10884,7 @@ function printLogoutHelp(io) {
|
|
|
10374
10884
|
io.out(" 0 logged out (or already logged out)");
|
|
10375
10885
|
io.out(" 1 could not clear the local session (file IO / config error)");
|
|
10376
10886
|
}
|
|
10377
|
-
function
|
|
10887
|
+
function describe22(e) {
|
|
10378
10888
|
return e instanceof Error ? e.message : String(e);
|
|
10379
10889
|
}
|
|
10380
10890
|
|
|
@@ -10444,7 +10954,7 @@ async function runLogs(args, io) {
|
|
|
10444
10954
|
io.err(`launchpad logs: ${e.message}`);
|
|
10445
10955
|
return 1;
|
|
10446
10956
|
}
|
|
10447
|
-
io.err(`launchpad logs failed: ${
|
|
10957
|
+
io.err(`launchpad logs failed: ${describe23(e)}`);
|
|
10448
10958
|
return 1;
|
|
10449
10959
|
}
|
|
10450
10960
|
}
|
|
@@ -10524,7 +11034,7 @@ function formatRelative2(iso) {
|
|
|
10524
11034
|
return `${hr}h ago`;
|
|
10525
11035
|
return `${Math.floor(hr / 24)}d ago`;
|
|
10526
11036
|
}
|
|
10527
|
-
function
|
|
11037
|
+
function describe23(e) {
|
|
10528
11038
|
return e instanceof Error ? e.message : String(e);
|
|
10529
11039
|
}
|
|
10530
11040
|
|
|
@@ -10573,7 +11083,7 @@ async function runMerge(args, io) {
|
|
|
10573
11083
|
try {
|
|
10574
11084
|
body = await res.json();
|
|
10575
11085
|
} catch (e) {
|
|
10576
|
-
io.err(`launchpad merge: bot returned 2xx with malformed body: ${
|
|
11086
|
+
io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe24(e)}`);
|
|
10577
11087
|
return 1;
|
|
10578
11088
|
}
|
|
10579
11089
|
io.out("");
|
|
@@ -10613,7 +11123,7 @@ async function runMerge(args, io) {
|
|
|
10613
11123
|
io.err(`launchpad merge: ${e.message}`);
|
|
10614
11124
|
return 1;
|
|
10615
11125
|
}
|
|
10616
|
-
io.err(`launchpad merge failed: ${
|
|
11126
|
+
io.err(`launchpad merge failed: ${describe24(e)}`);
|
|
10617
11127
|
return 1;
|
|
10618
11128
|
}
|
|
10619
11129
|
}
|
|
@@ -10712,12 +11222,12 @@ function renderBotError(status, env, io, prNumber = null) {
|
|
|
10712
11222
|
io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
|
|
10713
11223
|
}
|
|
10714
11224
|
}
|
|
10715
|
-
function
|
|
11225
|
+
function describe24(e) {
|
|
10716
11226
|
return e instanceof Error ? e.message : String(e);
|
|
10717
11227
|
}
|
|
10718
11228
|
|
|
10719
11229
|
// src/commands/plan.ts
|
|
10720
|
-
import { resolve as
|
|
11230
|
+
import { resolve as resolve12 } from "node:path";
|
|
10721
11231
|
var planCommand = {
|
|
10722
11232
|
name: "plan",
|
|
10723
11233
|
summary: "summarise what the manifest would deploy (offline)",
|
|
@@ -10730,8 +11240,19 @@ async function runPlan(args, io) {
|
|
|
10730
11240
|
io.err("Usage: launchpad plan [--file <path>] [--json]");
|
|
10731
11241
|
return 64;
|
|
10732
11242
|
}
|
|
10733
|
-
const manifestPath =
|
|
11243
|
+
const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
10734
11244
|
const result = loadManifest(manifestPath);
|
|
11245
|
+
if (result.kind === "ok") {
|
|
11246
|
+
const unsupported2 = rejectUnsupportedCliRuntime({
|
|
11247
|
+
command: "plan",
|
|
11248
|
+
manifestPath,
|
|
11249
|
+
manifest: result.manifest,
|
|
11250
|
+
io,
|
|
11251
|
+
json: flags.json
|
|
11252
|
+
});
|
|
11253
|
+
if (unsupported2 !== null)
|
|
11254
|
+
return unsupported2;
|
|
11255
|
+
}
|
|
10735
11256
|
return flags.json ? renderJson(result, io) : renderHuman(result, io);
|
|
10736
11257
|
}
|
|
10737
11258
|
function renderJson(result, io) {
|
|
@@ -10964,7 +11485,7 @@ async function runDestroy(args, io, prompt, isTty) {
|
|
|
10964
11485
|
io.err(`launchpad destroy: ${e.message}`);
|
|
10965
11486
|
return 1;
|
|
10966
11487
|
}
|
|
10967
|
-
io.err(`launchpad destroy failed: ${
|
|
11488
|
+
io.err(`launchpad destroy failed: ${describe25(e)}`);
|
|
10968
11489
|
return 1;
|
|
10969
11490
|
}
|
|
10970
11491
|
}
|
|
@@ -11164,7 +11685,7 @@ function printUsage4(io) {
|
|
|
11164
11685
|
].join(`
|
|
11165
11686
|
`));
|
|
11166
11687
|
}
|
|
11167
|
-
function
|
|
11688
|
+
function describe25(e) {
|
|
11168
11689
|
return e instanceof Error ? e.message : String(e);
|
|
11169
11690
|
}
|
|
11170
11691
|
|
|
@@ -11296,22 +11817,22 @@ async function treesEqual(cwd, tracked, headDir, headPaths) {
|
|
|
11296
11817
|
if (tracked.length !== headPaths.length)
|
|
11297
11818
|
return false;
|
|
11298
11819
|
const expected = new Set(headPaths);
|
|
11299
|
-
for (const
|
|
11300
|
-
if (!expected.has(
|
|
11820
|
+
for (const relative6 of tracked) {
|
|
11821
|
+
if (!expected.has(relative6))
|
|
11301
11822
|
return false;
|
|
11302
|
-
if (!equal(await readEntry(path12.resolve(cwd,
|
|
11823
|
+
if (!equal(await readEntry(path12.resolve(cwd, relative6)), await readEntry(path12.resolve(headDir, relative6)))) {
|
|
11303
11824
|
return false;
|
|
11304
11825
|
}
|
|
11305
11826
|
}
|
|
11306
11827
|
return true;
|
|
11307
11828
|
}
|
|
11308
|
-
async function listFiles(root,
|
|
11829
|
+
async function listFiles(root, relative6 = "") {
|
|
11309
11830
|
const files = [];
|
|
11310
|
-
const entries = await fs6.readdir(path12.resolve(root,
|
|
11831
|
+
const entries = await fs6.readdir(path12.resolve(root, relative6), {
|
|
11311
11832
|
withFileTypes: true
|
|
11312
11833
|
});
|
|
11313
11834
|
for (const entry of entries) {
|
|
11314
|
-
const child =
|
|
11835
|
+
const child = relative6.length === 0 ? entry.name : `${relative6}/${entry.name}`;
|
|
11315
11836
|
if (entry.isDirectory())
|
|
11316
11837
|
files.push(...await listFiles(root, child));
|
|
11317
11838
|
else if (entry.isFile())
|
|
@@ -11320,12 +11841,12 @@ async function listFiles(root, relative5 = "") {
|
|
|
11320
11841
|
return files.sort();
|
|
11321
11842
|
}
|
|
11322
11843
|
function gitOutput(cwd, args) {
|
|
11323
|
-
return new Promise((
|
|
11844
|
+
return new Promise((resolve14, reject) => {
|
|
11324
11845
|
execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
|
|
11325
11846
|
if (error !== null)
|
|
11326
11847
|
reject(error);
|
|
11327
11848
|
else
|
|
11328
|
-
|
|
11849
|
+
resolve14(stdout);
|
|
11329
11850
|
});
|
|
11330
11851
|
});
|
|
11331
11852
|
}
|
|
@@ -11334,7 +11855,7 @@ function splitNull(value) {
|
|
|
11334
11855
|
}
|
|
11335
11856
|
async function reconcile(args) {
|
|
11336
11857
|
const removals = await cleanAncestorRemovals(args);
|
|
11337
|
-
const planned = await Promise.all(args.paths.map((
|
|
11858
|
+
const planned = await Promise.all(args.paths.map((relative6) => planPath(args, relative6, removals)));
|
|
11338
11859
|
const conflicts = planned.flatMap((entry) => entry.conflict === undefined ? [] : [entry.conflict]);
|
|
11339
11860
|
if (conflicts.length > 0) {
|
|
11340
11861
|
const report = {
|
|
@@ -11378,55 +11899,55 @@ async function reconcile(args) {
|
|
|
11378
11899
|
}
|
|
11379
11900
|
async function cleanAncestorRemovals(args) {
|
|
11380
11901
|
const removals = new Set;
|
|
11381
|
-
for (const
|
|
11382
|
-
assertSafePath(
|
|
11383
|
-
const checked = await checkParents(args.cwd,
|
|
11902
|
+
for (const relative6 of args.compare.deleted) {
|
|
11903
|
+
assertSafePath(relative6);
|
|
11904
|
+
const checked = await checkParents(args.cwd, relative6, removals);
|
|
11384
11905
|
if (checked.kind === "blocked")
|
|
11385
11906
|
continue;
|
|
11386
|
-
const base = await readEntry(path12.resolve(args.baseDir,
|
|
11907
|
+
const base = await readEntry(path12.resolve(args.baseDir, relative6));
|
|
11387
11908
|
const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
|
|
11388
11909
|
if (local.kind !== "missing" && cleanLocal(base, local))
|
|
11389
|
-
removals.add(
|
|
11910
|
+
removals.add(relative6);
|
|
11390
11911
|
}
|
|
11391
11912
|
return removals;
|
|
11392
11913
|
}
|
|
11393
|
-
async function planPath(args,
|
|
11394
|
-
assertSafePath(
|
|
11395
|
-
const checked = await checkParents(args.cwd,
|
|
11396
|
-
const base = await readEntry(path12.resolve(args.baseDir,
|
|
11397
|
-
const upstreamDeleted = args.compare.deleted.includes(
|
|
11398
|
-
const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir,
|
|
11399
|
-
const
|
|
11914
|
+
async function planPath(args, relative6, removals) {
|
|
11915
|
+
assertSafePath(relative6);
|
|
11916
|
+
const checked = await checkParents(args.cwd, relative6, removals);
|
|
11917
|
+
const base = await readEntry(path12.resolve(args.baseDir, relative6));
|
|
11918
|
+
const upstreamDeleted = args.compare.deleted.includes(relative6);
|
|
11919
|
+
const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir, relative6));
|
|
11920
|
+
const unsupported2 = (args.compare.unsupported ?? []).includes(relative6);
|
|
11400
11921
|
if (checked.kind === "blocked") {
|
|
11401
|
-
return conflictPlan(
|
|
11922
|
+
return conflictPlan(relative6, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
|
|
11402
11923
|
}
|
|
11403
11924
|
const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
|
|
11404
|
-
if (
|
|
11405
|
-
return conflictPlan(
|
|
11925
|
+
if (unsupported2) {
|
|
11926
|
+
return conflictPlan(relative6, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
|
|
11406
11927
|
}
|
|
11407
11928
|
if (alreadyResolved(local, head, upstreamDeleted)) {
|
|
11408
|
-
return { relative:
|
|
11929
|
+
return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: false };
|
|
11409
11930
|
}
|
|
11410
11931
|
if (unsupportedHead(head, upstreamDeleted)) {
|
|
11411
|
-
return conflictPlan(
|
|
11932
|
+
return conflictPlan(relative6, checked.target, local, head, true, "unsupported upstream file type");
|
|
11412
11933
|
}
|
|
11413
11934
|
if (cleanLocal(base, local)) {
|
|
11414
|
-
return { relative:
|
|
11935
|
+
return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: true };
|
|
11415
11936
|
}
|
|
11416
|
-
return conflictPlan(
|
|
11937
|
+
return conflictPlan(relative6, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
|
|
11417
11938
|
}
|
|
11418
|
-
function conflictPlan(
|
|
11939
|
+
function conflictPlan(relative6, target, local, head, upstreamDeleted, reason) {
|
|
11419
11940
|
const suffix = upstreamDeleted ? ".upstream-deleted" : ".upstream";
|
|
11420
|
-
const digest = createHash3("sha256").update(
|
|
11941
|
+
const digest = createHash3("sha256").update(relative6).digest("hex");
|
|
11421
11942
|
return {
|
|
11422
|
-
relative:
|
|
11943
|
+
relative: relative6,
|
|
11423
11944
|
target,
|
|
11424
11945
|
local,
|
|
11425
11946
|
head,
|
|
11426
11947
|
upstreamDeleted,
|
|
11427
11948
|
apply: false,
|
|
11428
11949
|
conflict: {
|
|
11429
|
-
path:
|
|
11950
|
+
path: relative6,
|
|
11430
11951
|
reason,
|
|
11431
11952
|
artifact: `${CONFLICT_DIR}/${digest}${suffix}`,
|
|
11432
11953
|
upstreamDeleted
|
|
@@ -11527,13 +12048,13 @@ function equal(left, right) {
|
|
|
11527
12048
|
return false;
|
|
11528
12049
|
return left.bytes.every((byte, index) => byte === right.bytes[index]);
|
|
11529
12050
|
}
|
|
11530
|
-
async function writeAtomic(cwd,
|
|
11531
|
-
const checked = await checkParents(cwd,
|
|
12051
|
+
async function writeAtomic(cwd, relative6, bytes, mode, allowMetadata = false) {
|
|
12052
|
+
const checked = await checkParents(cwd, relative6, new Set, allowMetadata);
|
|
11532
12053
|
if (checked.kind === "blocked")
|
|
11533
12054
|
throw new Error(checked.reason);
|
|
11534
12055
|
const file = checked.target;
|
|
11535
12056
|
await fs6.mkdir(path12.dirname(file), { recursive: true });
|
|
11536
|
-
const rechecked = await checkParents(cwd,
|
|
12057
|
+
const rechecked = await checkParents(cwd, relative6, new Set, allowMetadata);
|
|
11537
12058
|
if (rechecked.kind === "blocked")
|
|
11538
12059
|
throw new Error(rechecked.reason);
|
|
11539
12060
|
const temporary = `${file}.launchpad-tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
@@ -11548,8 +12069,8 @@ async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
|
|
|
11548
12069
|
throw error;
|
|
11549
12070
|
}
|
|
11550
12071
|
}
|
|
11551
|
-
async function writeJsonAtomic(cwd,
|
|
11552
|
-
await writeAtomic(cwd,
|
|
12072
|
+
async function writeJsonAtomic(cwd, relative6, value) {
|
|
12073
|
+
await writeAtomic(cwd, relative6, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
|
|
11553
12074
|
`), 384, true);
|
|
11554
12075
|
}
|
|
11555
12076
|
async function resetConflictArtifacts(cwd) {
|
|
@@ -11565,37 +12086,37 @@ async function cleanupConflictState(cwd) {
|
|
|
11565
12086
|
throw new Error(report.reason);
|
|
11566
12087
|
await fs6.rm(report.target, { force: true });
|
|
11567
12088
|
}
|
|
11568
|
-
function assertSafePath(
|
|
11569
|
-
if (!isSafeSourcePath(
|
|
11570
|
-
throw new Error(`unsafe source path returned by bot: ${
|
|
12089
|
+
function assertSafePath(relative6) {
|
|
12090
|
+
if (!isSafeSourcePath(relative6)) {
|
|
12091
|
+
throw new Error(`unsafe source path returned by bot: ${relative6}`);
|
|
11571
12092
|
}
|
|
11572
12093
|
}
|
|
11573
|
-
function isSafeSourcePath(
|
|
11574
|
-
return isSafeRelativePath(
|
|
12094
|
+
function isSafeSourcePath(relative6) {
|
|
12095
|
+
return isSafeRelativePath(relative6) && !relative6.startsWith(`${MARKER_DIR}/`);
|
|
11575
12096
|
}
|
|
11576
|
-
function isSafeRelativePath(
|
|
11577
|
-
const normalized = path12.posix.normalize(
|
|
11578
|
-
return
|
|
12097
|
+
function isSafeRelativePath(relative6) {
|
|
12098
|
+
const normalized = path12.posix.normalize(relative6);
|
|
12099
|
+
return relative6.length > 0 && !relative6.includes("\\") && !path12.isAbsolute(relative6) && normalized === relative6 && normalized !== ".." && !normalized.startsWith("../");
|
|
11579
12100
|
}
|
|
11580
|
-
function resolveInsideClone(cwd,
|
|
12101
|
+
function resolveInsideClone(cwd, relative6) {
|
|
11581
12102
|
const root = path12.resolve(cwd);
|
|
11582
|
-
const target = path12.resolve(root,
|
|
12103
|
+
const target = path12.resolve(root, relative6);
|
|
11583
12104
|
const fromRoot = path12.relative(root, target);
|
|
11584
12105
|
if (fromRoot.length === 0 || path12.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${path12.sep}`)) {
|
|
11585
|
-
throw new Error(`unsafe source path returned by bot: ${
|
|
12106
|
+
throw new Error(`unsafe source path returned by bot: ${relative6}`);
|
|
11586
12107
|
}
|
|
11587
12108
|
return target;
|
|
11588
12109
|
}
|
|
11589
|
-
async function checkParents(cwd,
|
|
12110
|
+
async function checkParents(cwd, relative6, projectedRemovals, allowMetadata = false) {
|
|
11590
12111
|
if (allowMetadata) {
|
|
11591
|
-
if (!isSafeRelativePath(
|
|
11592
|
-
throw new Error(`unsafe metadata path: ${
|
|
12112
|
+
if (!isSafeRelativePath(relative6)) {
|
|
12113
|
+
throw new Error(`unsafe metadata path: ${relative6}`);
|
|
11593
12114
|
}
|
|
11594
12115
|
} else {
|
|
11595
|
-
assertSafePath(
|
|
12116
|
+
assertSafePath(relative6);
|
|
11596
12117
|
}
|
|
11597
|
-
const target = resolveInsideClone(cwd,
|
|
11598
|
-
const components =
|
|
12118
|
+
const target = resolveInsideClone(cwd, relative6);
|
|
12119
|
+
const components = relative6.split("/");
|
|
11599
12120
|
let current = path12.resolve(cwd);
|
|
11600
12121
|
for (let index = 0;index < components.length - 1; index += 1) {
|
|
11601
12122
|
current = path12.join(current, components[index]);
|
|
@@ -11734,7 +12255,7 @@ async function runPull(args, io) {
|
|
|
11734
12255
|
io.err(`launchpad pull: ${e.message}`);
|
|
11735
12256
|
return 1;
|
|
11736
12257
|
}
|
|
11737
|
-
io.err(`launchpad pull failed: ${
|
|
12258
|
+
io.err(`launchpad pull failed: ${describe26(e)}`);
|
|
11738
12259
|
return 1;
|
|
11739
12260
|
}
|
|
11740
12261
|
}
|
|
@@ -11842,7 +12363,7 @@ function printUsage5(io) {
|
|
|
11842
12363
|
].join(`
|
|
11843
12364
|
`));
|
|
11844
12365
|
}
|
|
11845
|
-
function
|
|
12366
|
+
function describe26(e) {
|
|
11846
12367
|
return e instanceof Error ? e.message : String(e);
|
|
11847
12368
|
}
|
|
11848
12369
|
|
|
@@ -12177,7 +12698,7 @@ async function runReview(args, io) {
|
|
|
12177
12698
|
io.err(`launchpad review: ${e.message}`);
|
|
12178
12699
|
return 1;
|
|
12179
12700
|
}
|
|
12180
|
-
io.err(`launchpad review failed: ${
|
|
12701
|
+
io.err(`launchpad review failed: ${describe27(e)}`);
|
|
12181
12702
|
return 1;
|
|
12182
12703
|
}
|
|
12183
12704
|
}
|
|
@@ -12255,12 +12776,12 @@ function severityRank(s) {
|
|
|
12255
12776
|
return 3;
|
|
12256
12777
|
}
|
|
12257
12778
|
}
|
|
12258
|
-
function
|
|
12779
|
+
function describe27(e) {
|
|
12259
12780
|
return e instanceof Error ? e.message : String(e);
|
|
12260
12781
|
}
|
|
12261
12782
|
|
|
12262
12783
|
// src/deploy/rollback.ts
|
|
12263
|
-
import { existsSync as
|
|
12784
|
+
import { existsSync as existsSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "node:fs";
|
|
12264
12785
|
import { resolve as resolvePath3 } from "node:path";
|
|
12265
12786
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
12266
12787
|
|
|
@@ -12315,7 +12836,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
12315
12836
|
cwd: process.cwd()
|
|
12316
12837
|
});
|
|
12317
12838
|
} catch (e) {
|
|
12318
|
-
io.err(`launchpad rollback: git rev-parse failed to start: ${
|
|
12839
|
+
io.err(`launchpad rollback: git rev-parse failed to start: ${describe28(e)}`);
|
|
12319
12840
|
return 2;
|
|
12320
12841
|
}
|
|
12321
12842
|
if (revParse.exitCode !== 0) {
|
|
@@ -12330,7 +12851,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
12330
12851
|
try {
|
|
12331
12852
|
show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
|
|
12332
12853
|
} catch (e) {
|
|
12333
|
-
io.err(`launchpad rollback: git show failed to start: ${
|
|
12854
|
+
io.err(`launchpad rollback: git show failed to start: ${describe28(e)}`);
|
|
12334
12855
|
return 2;
|
|
12335
12856
|
}
|
|
12336
12857
|
if (show.exitCode !== 0) {
|
|
@@ -12364,7 +12885,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
12364
12885
|
try {
|
|
12365
12886
|
writeFileSync5(manifestPath, historicalYaml, "utf8");
|
|
12366
12887
|
} catch (e) {
|
|
12367
|
-
io.err(`launchpad rollback: failed to write ${manifestPath}: ${
|
|
12888
|
+
io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe28(e)}`);
|
|
12368
12889
|
return 2;
|
|
12369
12890
|
}
|
|
12370
12891
|
io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
|
|
@@ -12385,7 +12906,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
12385
12906
|
}, io, applyDeps);
|
|
12386
12907
|
}
|
|
12387
12908
|
function readCurrentManifest(path14) {
|
|
12388
|
-
if (!
|
|
12909
|
+
if (!existsSync11(path14))
|
|
12389
12910
|
return null;
|
|
12390
12911
|
let raw;
|
|
12391
12912
|
try {
|
|
@@ -12446,7 +12967,7 @@ async function defaultPrompt4(question) {
|
|
|
12446
12967
|
rl.close();
|
|
12447
12968
|
}
|
|
12448
12969
|
}
|
|
12449
|
-
function
|
|
12970
|
+
function describe28(e) {
|
|
12450
12971
|
return e instanceof Error ? e.message : String(e);
|
|
12451
12972
|
}
|
|
12452
12973
|
function renderManifestError3(result, io) {
|
|
@@ -12545,13 +13066,13 @@ function parseArgs12(args) {
|
|
|
12545
13066
|
}
|
|
12546
13067
|
|
|
12547
13068
|
// src/secrets/push.ts
|
|
12548
|
-
import { existsSync as
|
|
13069
|
+
import { existsSync as existsSync12, readFileSync as readFileSync16 } from "node:fs";
|
|
12549
13070
|
import { resolve as resolvePath4 } from "node:path";
|
|
12550
13071
|
import { createInterface as createInterface4 } from "node:readline/promises";
|
|
12551
13072
|
import { stdin as input2, stdout as output2 } from "node:process";
|
|
12552
13073
|
|
|
12553
13074
|
// src/secrets/env-parse.ts
|
|
12554
|
-
var
|
|
13075
|
+
var ENV_NAME2 = /^[A-Z][A-Z0-9_]*$/;
|
|
12555
13076
|
var LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
12556
13077
|
function parseEnv(text) {
|
|
12557
13078
|
const entries = new Map;
|
|
@@ -12576,7 +13097,7 @@ function parseEnv(text) {
|
|
|
12576
13097
|
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
12577
13098
|
value = value.slice(1, -1);
|
|
12578
13099
|
}
|
|
12579
|
-
if (!
|
|
13100
|
+
if (!ENV_NAME2.test(key)) {
|
|
12580
13101
|
invalidNames.push(key);
|
|
12581
13102
|
continue;
|
|
12582
13103
|
}
|
|
@@ -12602,7 +13123,7 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
12602
13123
|
return 0;
|
|
12603
13124
|
}
|
|
12604
13125
|
const envPath = resolvePath4(process.cwd(), opts.env ?? ".env");
|
|
12605
|
-
if (!
|
|
13126
|
+
if (!existsSync12(envPath)) {
|
|
12606
13127
|
io.err(`launchpad secrets push: ${envPath}`);
|
|
12607
13128
|
io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
|
|
12608
13129
|
return 2;
|
|
@@ -12611,7 +13132,7 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
12611
13132
|
try {
|
|
12612
13133
|
envText = readFileSync16(envPath, "utf8");
|
|
12613
13134
|
} catch (e) {
|
|
12614
|
-
io.err(`launchpad secrets push: failed to read ${envPath}: ${
|
|
13135
|
+
io.err(`launchpad secrets push: failed to read ${envPath}: ${describe29(e)}`);
|
|
12615
13136
|
return 2;
|
|
12616
13137
|
}
|
|
12617
13138
|
const parsed = parseEnv(envText);
|
|
@@ -12795,10 +13316,10 @@ function mapPushError(e, slug, name, io) {
|
|
|
12795
13316
|
io.err(`✗ secrets push ${name}: ${e.message}`);
|
|
12796
13317
|
return 1;
|
|
12797
13318
|
}
|
|
12798
|
-
io.err(`✗ secrets push ${name} failed: ${
|
|
13319
|
+
io.err(`✗ secrets push ${name} failed: ${describe29(e)}`);
|
|
12799
13320
|
return 2;
|
|
12800
13321
|
}
|
|
12801
|
-
function
|
|
13322
|
+
function describe29(e) {
|
|
12802
13323
|
return e instanceof Error ? e.message : String(e);
|
|
12803
13324
|
}
|
|
12804
13325
|
function renderManifestError4(result, io) {
|
|
@@ -12934,7 +13455,7 @@ function renderManifestError5(result, io) {
|
|
|
12934
13455
|
}
|
|
12935
13456
|
|
|
12936
13457
|
// src/secrets/set.ts
|
|
12937
|
-
import { existsSync as
|
|
13458
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "node:fs";
|
|
12938
13459
|
import { resolve as resolvePath6 } from "node:path";
|
|
12939
13460
|
import { parse as parseYaml8 } from "yaml";
|
|
12940
13461
|
var CELL_LABEL2 = {
|
|
@@ -12944,7 +13465,7 @@ var CELL_LABEL2 = {
|
|
|
12944
13465
|
};
|
|
12945
13466
|
function loadSet(fleetFile, setName, io) {
|
|
12946
13467
|
const path14 = resolvePath6(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
12947
|
-
if (!
|
|
13468
|
+
if (!existsSync13(path14)) {
|
|
12948
13469
|
io.err(`✗ ${path14}`);
|
|
12949
13470
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
12950
13471
|
return 2;
|
|
@@ -13036,7 +13557,7 @@ async function runSecretsPushSet(opts, io, deps = {}) {
|
|
|
13036
13557
|
if (typeof set === "number")
|
|
13037
13558
|
return set;
|
|
13038
13559
|
const envPath = resolvePath6(process.cwd(), opts.env ?? ".env");
|
|
13039
|
-
if (!
|
|
13560
|
+
if (!existsSync13(envPath)) {
|
|
13040
13561
|
io.err(`launchpad secrets push --set: ${envPath} not found.`);
|
|
13041
13562
|
io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
|
|
13042
13563
|
return 2;
|
|
@@ -13187,8 +13708,8 @@ function setPushExit(e) {
|
|
|
13187
13708
|
}
|
|
13188
13709
|
|
|
13189
13710
|
// src/commands/secrets-template.ts
|
|
13190
|
-
import { existsSync as
|
|
13191
|
-
import { resolve as
|
|
13711
|
+
import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "node:fs";
|
|
13712
|
+
import { resolve as resolve14 } from "node:path";
|
|
13192
13713
|
async function runSecretsTemplate(args, io) {
|
|
13193
13714
|
const flags = parseFlags4(args);
|
|
13194
13715
|
if (flags.kind === "usage-error") {
|
|
@@ -13196,7 +13717,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
13196
13717
|
io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
|
|
13197
13718
|
return 64;
|
|
13198
13719
|
}
|
|
13199
|
-
const manifestPath =
|
|
13720
|
+
const manifestPath = resolve14(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
13200
13721
|
const result = loadManifest(manifestPath);
|
|
13201
13722
|
const renderResult = renderManifest(result, io);
|
|
13202
13723
|
if (renderResult.kind !== "ok") {
|
|
@@ -13210,8 +13731,8 @@ async function runSecretsTemplate(args, io) {
|
|
|
13210
13731
|
}
|
|
13211
13732
|
return 0;
|
|
13212
13733
|
}
|
|
13213
|
-
const outPath =
|
|
13214
|
-
if (
|
|
13734
|
+
const outPath = resolve14(process.cwd(), flags.out);
|
|
13735
|
+
if (existsSync14(outPath) && !flags.force) {
|
|
13215
13736
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
13216
13737
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
13217
13738
|
return 64;
|
|
@@ -13507,16 +14028,16 @@ function printHelp3(io) {
|
|
|
13507
14028
|
|
|
13508
14029
|
// src/commands/skills.ts
|
|
13509
14030
|
import { fileURLToPath } from "node:url";
|
|
13510
|
-
import { dirname as
|
|
13511
|
-
import { promises as fs7, existsSync as
|
|
14031
|
+
import { dirname as dirname10, join as join16, resolve as resolve15 } from "node:path";
|
|
14032
|
+
import { promises as fs7, existsSync as existsSync15 } from "node:fs";
|
|
13512
14033
|
|
|
13513
14034
|
// src/skills-bundle.ts
|
|
13514
14035
|
import { homedir as homedir2 } from "node:os";
|
|
13515
|
-
import { join as
|
|
14036
|
+
import { join as join15 } from "node:path";
|
|
13516
14037
|
import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
|
|
13517
14038
|
var SKILL_PREFIX = "launchpad-";
|
|
13518
14039
|
function skillsTargetDir() {
|
|
13519
|
-
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
14040
|
+
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir2(), ".claude", "skills");
|
|
13520
14041
|
}
|
|
13521
14042
|
function readInstalledSkills() {
|
|
13522
14043
|
try {
|
|
@@ -13526,7 +14047,7 @@ function readInstalledSkills() {
|
|
|
13526
14047
|
return { present: false, version: null };
|
|
13527
14048
|
return {
|
|
13528
14049
|
present: true,
|
|
13529
|
-
version: readSkillVersion(
|
|
14050
|
+
version: readSkillVersion(join15(dir, bundle.name, "SKILL.md"))
|
|
13530
14051
|
};
|
|
13531
14052
|
} catch {
|
|
13532
14053
|
return { present: false, version: null };
|
|
@@ -13588,7 +14109,7 @@ async function runSkills(args, io) {
|
|
|
13588
14109
|
return 64;
|
|
13589
14110
|
}
|
|
13590
14111
|
} catch (e) {
|
|
13591
|
-
io.err(`launchpad skills ${action}: ${
|
|
14112
|
+
io.err(`launchpad skills ${action}: ${describe30(e)}`);
|
|
13592
14113
|
return 1;
|
|
13593
14114
|
}
|
|
13594
14115
|
}
|
|
@@ -13611,13 +14132,13 @@ function resolveInstallEnv() {
|
|
|
13611
14132
|
return { bundleDir, userSkillsDir };
|
|
13612
14133
|
}
|
|
13613
14134
|
function defaultBundleDir() {
|
|
13614
|
-
const here =
|
|
14135
|
+
const here = dirname10(fileURLToPath(import.meta.url));
|
|
13615
14136
|
const candidates = [
|
|
13616
|
-
|
|
13617
|
-
|
|
14137
|
+
resolve15(here, "..", "skills"),
|
|
14138
|
+
resolve15(here, "..", "..", "skills")
|
|
13618
14139
|
];
|
|
13619
14140
|
for (const c of candidates) {
|
|
13620
|
-
if (
|
|
14141
|
+
if (existsSync15(join16(c, "launchpad-onboard", "SKILL.md"))) {
|
|
13621
14142
|
return c;
|
|
13622
14143
|
}
|
|
13623
14144
|
}
|
|
@@ -13638,12 +14159,12 @@ async function doInstall(io) {
|
|
|
13638
14159
|
}
|
|
13639
14160
|
let installed = 0;
|
|
13640
14161
|
for (const skill of BUNDLED_SKILLS) {
|
|
13641
|
-
const src =
|
|
14162
|
+
const src = join16(env.bundleDir, skill);
|
|
13642
14163
|
if (!await isDir(src)) {
|
|
13643
14164
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
13644
14165
|
return 1;
|
|
13645
14166
|
}
|
|
13646
|
-
const dest =
|
|
14167
|
+
const dest = join16(env.userSkillsDir, skill);
|
|
13647
14168
|
await fs7.rm(dest, { recursive: true, force: true });
|
|
13648
14169
|
await fs7.cp(src, dest, { recursive: true });
|
|
13649
14170
|
installed++;
|
|
@@ -13668,7 +14189,7 @@ async function doUninstall(io) {
|
|
|
13668
14189
|
continue;
|
|
13669
14190
|
if (!isBundleManaged(entry.name))
|
|
13670
14191
|
continue;
|
|
13671
|
-
const target =
|
|
14192
|
+
const target = join16(env.userSkillsDir, entry.name);
|
|
13672
14193
|
await fs7.rm(target, { recursive: true, force: true });
|
|
13673
14194
|
removed++;
|
|
13674
14195
|
io.out(`✗ removed ${target}`);
|
|
@@ -13691,7 +14212,7 @@ async function doList(io) {
|
|
|
13691
14212
|
}
|
|
13692
14213
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
13693
14214
|
for (const name of managedDirs) {
|
|
13694
|
-
const skillFile =
|
|
14215
|
+
const skillFile = join16(env.userSkillsDir, name, "SKILL.md");
|
|
13695
14216
|
const version = await readVersion(skillFile);
|
|
13696
14217
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
13697
14218
|
}
|
|
@@ -13718,7 +14239,7 @@ async function isDir(path14) {
|
|
|
13718
14239
|
return false;
|
|
13719
14240
|
}
|
|
13720
14241
|
}
|
|
13721
|
-
function
|
|
14242
|
+
function describe30(e) {
|
|
13722
14243
|
return e instanceof Error ? e.message : String(e);
|
|
13723
14244
|
}
|
|
13724
14245
|
|
|
@@ -13726,7 +14247,7 @@ function describe29(e) {
|
|
|
13726
14247
|
import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
|
|
13727
14248
|
import { promisify } from "node:util";
|
|
13728
14249
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
13729
|
-
import { dirname as
|
|
14250
|
+
import { dirname as dirname11, resolve as resolve16, relative as relative6, isAbsolute as isAbsolute3, join as join17 } from "node:path";
|
|
13730
14251
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
13731
14252
|
import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
13732
14253
|
|
|
@@ -13771,9 +14292,9 @@ async function startLoopback(state, timeoutMs) {
|
|
|
13771
14292
|
resolveCode(code);
|
|
13772
14293
|
}
|
|
13773
14294
|
});
|
|
13774
|
-
const bound = await new Promise((
|
|
13775
|
-
server.once("error", () =>
|
|
13776
|
-
server.listen(0, "127.0.0.1", () =>
|
|
14295
|
+
const bound = await new Promise((resolve16) => {
|
|
14296
|
+
server.once("error", () => resolve16(false));
|
|
14297
|
+
server.listen(0, "127.0.0.1", () => resolve16(true));
|
|
13777
14298
|
});
|
|
13778
14299
|
if (!bound)
|
|
13779
14300
|
return null;
|
|
@@ -13849,7 +14370,7 @@ var PKG = "@m-kopa/launchpad-cli";
|
|
|
13849
14370
|
var REGISTRY = "https://registry.npmjs.org";
|
|
13850
14371
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
13851
14372
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
13852
|
-
var CHANNEL_MARKER =
|
|
14373
|
+
var CHANNEL_MARKER = join17(homedir3(), ".launchpad", "channel");
|
|
13853
14374
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
13854
14375
|
var UPGRADE_ARGS = {
|
|
13855
14376
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -13978,8 +14499,8 @@ async function openSystemBrowser(url) {
|
|
|
13978
14499
|
await execFileAsync(opener, [url]);
|
|
13979
14500
|
}
|
|
13980
14501
|
async function runInstallerScript(script) {
|
|
13981
|
-
const dir = mkdtempSync(
|
|
13982
|
-
const file =
|
|
14502
|
+
const dir = mkdtempSync(join17(tmpdir(), "launchpad-update-"));
|
|
14503
|
+
const file = join17(dir, "install.sh");
|
|
13983
14504
|
try {
|
|
13984
14505
|
writeFileSync7(file, script, { mode: 448 });
|
|
13985
14506
|
return await new Promise((resolvePromise) => {
|
|
@@ -14066,7 +14587,7 @@ function errStderr(e) {
|
|
|
14066
14587
|
return "";
|
|
14067
14588
|
}
|
|
14068
14589
|
async function detectPackageManager2() {
|
|
14069
|
-
const pkgRoot =
|
|
14590
|
+
const pkgRoot = resolve16(dirname11(fileURLToPath2(import.meta.url)), "..", "..");
|
|
14070
14591
|
const candidates = [];
|
|
14071
14592
|
const npmRoot = await pmRoot("npm");
|
|
14072
14593
|
if (npmRoot !== null)
|
|
@@ -14074,7 +14595,7 @@ async function detectPackageManager2() {
|
|
|
14074
14595
|
const pnpmRoot = await pmRoot("pnpm");
|
|
14075
14596
|
if (pnpmRoot !== null)
|
|
14076
14597
|
candidates.push(["pnpm", pnpmRoot]);
|
|
14077
|
-
candidates.push(["bun",
|
|
14598
|
+
candidates.push(["bun", resolve16(homedir3(), ".bun/install/global/node_modules")]);
|
|
14078
14599
|
const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
|
|
14079
14600
|
return matches.length === 1 ? matches[0][0] : null;
|
|
14080
14601
|
}
|
|
@@ -14088,7 +14609,7 @@ async function pmRoot(pm) {
|
|
|
14088
14609
|
}
|
|
14089
14610
|
}
|
|
14090
14611
|
function pathContains(root, child) {
|
|
14091
|
-
const rel =
|
|
14612
|
+
const rel = relative6(root, child);
|
|
14092
14613
|
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
14093
14614
|
}
|
|
14094
14615
|
function runUpgrade(pm) {
|
|
@@ -14211,8 +14732,8 @@ function printHelp5(io) {
|
|
|
14211
14732
|
}
|
|
14212
14733
|
|
|
14213
14734
|
// src/commands/validate.ts
|
|
14214
|
-
import { existsSync as
|
|
14215
|
-
import { dirname as
|
|
14735
|
+
import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
14736
|
+
import { dirname as dirname12, resolve as resolve17 } from "node:path";
|
|
14216
14737
|
var validateCommand = {
|
|
14217
14738
|
name: "validate",
|
|
14218
14739
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -14226,11 +14747,21 @@ async function runValidate(args, io) {
|
|
|
14226
14747
|
return 64;
|
|
14227
14748
|
}
|
|
14228
14749
|
const { file, json, strictGroups } = parseResult;
|
|
14229
|
-
const path14 =
|
|
14750
|
+
const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
|
|
14230
14751
|
const result = loadManifest(path14);
|
|
14231
14752
|
if (result.kind !== "ok") {
|
|
14232
14753
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
14233
14754
|
}
|
|
14755
|
+
const unsupported2 = rejectUnsupportedCliRuntime({
|
|
14756
|
+
command: "validate",
|
|
14757
|
+
manifestPath: path14,
|
|
14758
|
+
manifest: result.manifest,
|
|
14759
|
+
io,
|
|
14760
|
+
json,
|
|
14761
|
+
structurallyValidName: result.manifest.metadata.name
|
|
14762
|
+
});
|
|
14763
|
+
if (unsupported2 !== null)
|
|
14764
|
+
return unsupported2;
|
|
14234
14765
|
const boundary = checkBoundary(path14, result.manifest.app !== undefined);
|
|
14235
14766
|
const serveErrors = checkStaticServeDir(path14, result.manifest);
|
|
14236
14767
|
if (serveErrors.length > 0) {
|
|
@@ -14240,7 +14771,7 @@ async function runValidate(args, io) {
|
|
|
14240
14771
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
14241
14772
|
}
|
|
14242
14773
|
function checkBoundary(manifestPath, declared) {
|
|
14243
|
-
const dir =
|
|
14774
|
+
const dir = dirname12(manifestPath);
|
|
14244
14775
|
let files;
|
|
14245
14776
|
try {
|
|
14246
14777
|
files = walkCwd(dir).files;
|
|
@@ -14272,9 +14803,9 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
14272
14803
|
`static app destination_dir '${serveDir}' is unsafe — use a confined relative subdirectory (e.g. '${STATIC_SERVE_DIR}'). ` + `Serving '.' or an absolute/parent path would publish files outside your app's content.`
|
|
14273
14804
|
];
|
|
14274
14805
|
}
|
|
14275
|
-
const dir =
|
|
14276
|
-
const abs =
|
|
14277
|
-
if (!
|
|
14806
|
+
const dir = dirname12(manifestPath);
|
|
14807
|
+
const abs = resolve17(dir, serveDir);
|
|
14808
|
+
if (!existsSync16(abs)) {
|
|
14278
14809
|
return [
|
|
14279
14810
|
`static app serves '${serveDir}/' but that directory does not exist — nothing would be served. ` + `Create ${serveDir}/index.html (or run \`launchpad init\` to scaffold it).`
|
|
14280
14811
|
];
|
|
@@ -14283,7 +14814,7 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
14283
14814
|
try {
|
|
14284
14815
|
isDir2 = statSync2(abs).isDirectory();
|
|
14285
14816
|
} catch (e) {
|
|
14286
|
-
return [`failed to inspect static served directory '${serveDir}/': ${
|
|
14817
|
+
return [`failed to inspect static served directory '${serveDir}/': ${describe31(e)}`];
|
|
14287
14818
|
}
|
|
14288
14819
|
if (!isDir2) {
|
|
14289
14820
|
return [`static app serves '${serveDir}/' but that path is not a directory.`];
|
|
@@ -14292,14 +14823,14 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
14292
14823
|
try {
|
|
14293
14824
|
entries = readdirSync3(abs);
|
|
14294
14825
|
} catch (e) {
|
|
14295
|
-
return [`failed to read static served directory '${serveDir}/': ${
|
|
14826
|
+
return [`failed to read static served directory '${serveDir}/': ${describe31(e)}`];
|
|
14296
14827
|
}
|
|
14297
14828
|
if (entries.length === 0) {
|
|
14298
14829
|
return [
|
|
14299
14830
|
`static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
|
|
14300
14831
|
];
|
|
14301
14832
|
}
|
|
14302
|
-
if (!
|
|
14833
|
+
if (!existsSync16(resolve17(abs, "index.html"))) {
|
|
14303
14834
|
return [
|
|
14304
14835
|
`static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
|
|
14305
14836
|
];
|
|
@@ -14347,7 +14878,7 @@ async function checkGroup(allowedGroup, cfg) {
|
|
|
14347
14878
|
if (e instanceof ForbiddenError) {
|
|
14348
14879
|
return { kind: "forbidden", message: e.message };
|
|
14349
14880
|
}
|
|
14350
|
-
return { kind: "network", message:
|
|
14881
|
+
return { kind: "network", message: describe31(e) };
|
|
14351
14882
|
}
|
|
14352
14883
|
}
|
|
14353
14884
|
async function checkGroups(groups) {
|
|
@@ -14444,6 +14975,8 @@ function renderHumanError(result, io) {
|
|
|
14444
14975
|
}
|
|
14445
14976
|
function renderJsonOk(result, group, boundary, io) {
|
|
14446
14977
|
const base = {
|
|
14978
|
+
manifest_valid: true,
|
|
14979
|
+
runtime_supported: true,
|
|
14447
14980
|
name: result.manifest.metadata.name,
|
|
14448
14981
|
deploymentType: result.manifest.deployment.type,
|
|
14449
14982
|
appBoundary: {
|
|
@@ -14560,7 +15093,7 @@ function parseArgs13(args) {
|
|
|
14560
15093
|
}
|
|
14561
15094
|
return { kind: "ok", file, json, strictGroups };
|
|
14562
15095
|
}
|
|
14563
|
-
function
|
|
15096
|
+
function describe31(e) {
|
|
14564
15097
|
return e instanceof Error ? e.message : String(e);
|
|
14565
15098
|
}
|
|
14566
15099
|
|
|
@@ -14613,7 +15146,7 @@ async function runWhoami(args, io) {
|
|
|
14613
15146
|
io.err(e.message);
|
|
14614
15147
|
return 1;
|
|
14615
15148
|
}
|
|
14616
|
-
io.err(`launchpad whoami failed: ${
|
|
15149
|
+
io.err(`launchpad whoami failed: ${describe32(e)}`);
|
|
14617
15150
|
return 1;
|
|
14618
15151
|
}
|
|
14619
15152
|
}
|
|
@@ -14643,11 +15176,11 @@ async function runShare(args, io) {
|
|
|
14643
15176
|
io.err(e.message);
|
|
14644
15177
|
return 1;
|
|
14645
15178
|
}
|
|
14646
|
-
io.err(`launchpad whoami --share failed: ${
|
|
15179
|
+
io.err(`launchpad whoami --share failed: ${describe32(e)}`);
|
|
14647
15180
|
return 1;
|
|
14648
15181
|
}
|
|
14649
15182
|
}
|
|
14650
|
-
function
|
|
15183
|
+
function describe32(e) {
|
|
14651
15184
|
return e instanceof Error ? e.message : String(e);
|
|
14652
15185
|
}
|
|
14653
15186
|
|
|
@@ -14813,7 +15346,7 @@ async function runEditor(action, args, io, deps) {
|
|
|
14813
15346
|
io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
|
|
14814
15347
|
return 1;
|
|
14815
15348
|
}
|
|
14816
|
-
io.err(`launchpad ${verb} failed: ${
|
|
15349
|
+
io.err(`launchpad ${verb} failed: ${describe33(e)}`);
|
|
14817
15350
|
return 1;
|
|
14818
15351
|
}
|
|
14819
15352
|
}
|
|
@@ -14847,7 +15380,7 @@ async function sendMutation(action, slug, target, version, cfg, deps) {
|
|
|
14847
15380
|
nonThrowingStatuses: [409]
|
|
14848
15381
|
});
|
|
14849
15382
|
}
|
|
14850
|
-
function
|
|
15383
|
+
function describe33(e) {
|
|
14851
15384
|
return e instanceof Error ? e.message : String(e);
|
|
14852
15385
|
}
|
|
14853
15386
|
var grantEditorCommand = makeEditorCommand("grant");
|
|
@@ -15053,7 +15586,7 @@ async function runOwner(action, args, io, deps) {
|
|
|
15053
15586
|
io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
|
|
15054
15587
|
return 1;
|
|
15055
15588
|
}
|
|
15056
|
-
io.err(`launchpad ${verb} failed: ${
|
|
15589
|
+
io.err(`launchpad ${verb} failed: ${describe34(e)}`);
|
|
15057
15590
|
return 1;
|
|
15058
15591
|
}
|
|
15059
15592
|
}
|
|
@@ -15087,12 +15620,92 @@ async function sendMutation2(action, slug, target, version, cfg, deps) {
|
|
|
15087
15620
|
nonThrowingStatuses: [409]
|
|
15088
15621
|
});
|
|
15089
15622
|
}
|
|
15090
|
-
function
|
|
15623
|
+
function describe34(e) {
|
|
15091
15624
|
return e instanceof Error ? e.message : String(e);
|
|
15092
15625
|
}
|
|
15093
15626
|
var addOwnerCommand = makeOwnerCommand("add");
|
|
15094
15627
|
var removeOwnerCommand = makeOwnerCommand("remove");
|
|
15095
15628
|
|
|
15629
|
+
// src/commands/admin.ts
|
|
15630
|
+
var REAL_DEPS3 = { loadConfig, apiJson };
|
|
15631
|
+
var adminCommand = makeAdminCommand();
|
|
15632
|
+
function makeAdminCommand(deps = REAL_DEPS3) {
|
|
15633
|
+
return {
|
|
15634
|
+
name: "admin",
|
|
15635
|
+
summary: "run authenticated platform-operator actions",
|
|
15636
|
+
run: (args, io) => runAdmin(args, io, deps)
|
|
15637
|
+
};
|
|
15638
|
+
}
|
|
15639
|
+
function statusPageUrl(value, allowedHost) {
|
|
15640
|
+
try {
|
|
15641
|
+
const url = new URL(value);
|
|
15642
|
+
if (url.protocol !== "https:")
|
|
15643
|
+
return null;
|
|
15644
|
+
if (url.hostname !== allowedHost)
|
|
15645
|
+
return null;
|
|
15646
|
+
if (!/^\/d\/[^/]+$/u.test(url.pathname))
|
|
15647
|
+
return null;
|
|
15648
|
+
if (url.username !== "" || url.password !== "")
|
|
15649
|
+
return null;
|
|
15650
|
+
if (url.search !== "" || url.hash !== "")
|
|
15651
|
+
return null;
|
|
15652
|
+
return url.toString();
|
|
15653
|
+
} catch {
|
|
15654
|
+
return null;
|
|
15655
|
+
}
|
|
15656
|
+
}
|
|
15657
|
+
async function runAdmin(args, io, deps) {
|
|
15658
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
15659
|
+
printHelp6(io);
|
|
15660
|
+
return 0;
|
|
15661
|
+
}
|
|
15662
|
+
const config = deps.loadConfig();
|
|
15663
|
+
const parsed = parseStatusRevokeArgs(args, config.statusPageHost ?? DEFAULT_STATUS_PAGE_HOST);
|
|
15664
|
+
if (parsed.kind === "unknown") {
|
|
15665
|
+
io.err(`launchpad admin: unknown subcommand '${parsed.subcommand}'`);
|
|
15666
|
+
printHelp6(io);
|
|
15667
|
+
return 64;
|
|
15668
|
+
}
|
|
15669
|
+
if (parsed.kind === "invalid") {
|
|
15670
|
+
io.err("launchpad admin: expected `status-revoke <status-url>`");
|
|
15671
|
+
printHelp6(io);
|
|
15672
|
+
return 64;
|
|
15673
|
+
}
|
|
15674
|
+
return revokeStatusPage(parsed.url, config, io, deps);
|
|
15675
|
+
}
|
|
15676
|
+
function parseStatusRevokeArgs(args, allowedHost) {
|
|
15677
|
+
if (args[0] !== undefined && args[0] !== "status-revoke") {
|
|
15678
|
+
return { kind: "unknown", subcommand: args[0] };
|
|
15679
|
+
}
|
|
15680
|
+
if (args.length !== 2)
|
|
15681
|
+
return { kind: "invalid" };
|
|
15682
|
+
const url = statusPageUrl(args[1], allowedHost);
|
|
15683
|
+
return url === null ? { kind: "invalid" } : { kind: "ok", url };
|
|
15684
|
+
}
|
|
15685
|
+
async function revokeStatusPage(url, config, io, deps) {
|
|
15686
|
+
try {
|
|
15687
|
+
const response = await deps.apiJson(config, {
|
|
15688
|
+
method: "POST",
|
|
15689
|
+
path: "/status-page/revoke",
|
|
15690
|
+
jsonBody: { url }
|
|
15691
|
+
});
|
|
15692
|
+
io.out(statusRevokeMessage(response));
|
|
15693
|
+
return 0;
|
|
15694
|
+
} catch (error) {
|
|
15695
|
+
io.err(`launchpad admin status-revoke: ${error instanceof Error ? error.message : String(error)}`);
|
|
15696
|
+
return 1;
|
|
15697
|
+
}
|
|
15698
|
+
}
|
|
15699
|
+
function statusRevokeMessage(response) {
|
|
15700
|
+
return response.revoked ? "Status page revoked." : "Status page was already absent.";
|
|
15701
|
+
}
|
|
15702
|
+
function printHelp6(io) {
|
|
15703
|
+
io.out("launchpad admin - authenticated platform-operator actions.");
|
|
15704
|
+
io.out("");
|
|
15705
|
+
io.out("Usage:");
|
|
15706
|
+
io.out(" launchpad admin status-revoke <status-url>");
|
|
15707
|
+
}
|
|
15708
|
+
|
|
15096
15709
|
// src/commands/list-owners.ts
|
|
15097
15710
|
function realDeps4() {
|
|
15098
15711
|
return { loadConfig, apiJson };
|
|
@@ -15168,9 +15781,9 @@ var listOwnersCommand = makeListOwnersCommand();
|
|
|
15168
15781
|
// src/update-notifier.ts
|
|
15169
15782
|
import { spawn as spawn6 } from "node:child_process";
|
|
15170
15783
|
import { homedir as homedir4 } from "node:os";
|
|
15171
|
-
import { join as
|
|
15784
|
+
import { join as join18 } from "node:path";
|
|
15172
15785
|
import {
|
|
15173
|
-
existsSync as
|
|
15786
|
+
existsSync as existsSync17,
|
|
15174
15787
|
mkdirSync as mkdirSync4,
|
|
15175
15788
|
readFileSync as readFileSync21,
|
|
15176
15789
|
writeFileSync as writeFileSync8
|
|
@@ -15178,7 +15791,7 @@ import {
|
|
|
15178
15791
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
15179
15792
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
15180
15793
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
15181
|
-
var CACHE_FILE =
|
|
15794
|
+
var CACHE_FILE = join18(homedir4(), ".launchpad", "update-check.json");
|
|
15182
15795
|
function readCache2() {
|
|
15183
15796
|
try {
|
|
15184
15797
|
const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
|
|
@@ -15194,22 +15807,22 @@ function readCache2() {
|
|
|
15194
15807
|
}
|
|
15195
15808
|
function writeCache2(state) {
|
|
15196
15809
|
try {
|
|
15197
|
-
mkdirSync4(
|
|
15810
|
+
mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
|
|
15198
15811
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
15199
15812
|
`, { mode: 384 });
|
|
15200
15813
|
} catch {}
|
|
15201
15814
|
}
|
|
15202
|
-
var SKILLS_HINT_MARKER =
|
|
15815
|
+
var SKILLS_HINT_MARKER = join18(homedir4(), ".launchpad", "skills-hint-shown");
|
|
15203
15816
|
function absentHintShown() {
|
|
15204
15817
|
try {
|
|
15205
|
-
return
|
|
15818
|
+
return existsSync17(SKILLS_HINT_MARKER);
|
|
15206
15819
|
} catch {
|
|
15207
15820
|
return false;
|
|
15208
15821
|
}
|
|
15209
15822
|
}
|
|
15210
15823
|
function markAbsentHintShown() {
|
|
15211
15824
|
try {
|
|
15212
|
-
mkdirSync4(
|
|
15825
|
+
mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
|
|
15213
15826
|
writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
|
|
15214
15827
|
`, { mode: 384 });
|
|
15215
15828
|
} catch {}
|
|
@@ -15312,8 +15925,8 @@ var refreshUpdateCacheCommand = {
|
|
|
15312
15925
|
// src/telemetry.ts
|
|
15313
15926
|
import { spawn as spawn7 } from "node:child_process";
|
|
15314
15927
|
import { homedir as homedir5 } from "node:os";
|
|
15315
|
-
import { join as
|
|
15316
|
-
import { existsSync as
|
|
15928
|
+
import { join as join19 } from "node:path";
|
|
15929
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
|
|
15317
15930
|
import { randomUUID } from "node:crypto";
|
|
15318
15931
|
var INTERNAL_EMIT_VERB = "__emit-telemetry";
|
|
15319
15932
|
var POSTHOG_HOST = "https://us.i.posthog.com";
|
|
@@ -15321,8 +15934,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
|
|
|
15321
15934
|
var CAPTURE_PATH = "/i/v0/e/";
|
|
15322
15935
|
var SEND_TIMEOUT_MS = 3000;
|
|
15323
15936
|
var APP = "launchpad-cli";
|
|
15324
|
-
var DEVICE_ID_FILE =
|
|
15325
|
-
var FIRST_RUN_MARKER =
|
|
15937
|
+
var DEVICE_ID_FILE = join19(homedir5(), ".launchpad", "telemetry-id.json");
|
|
15938
|
+
var FIRST_RUN_MARKER = join19(homedir5(), ".launchpad", "telemetry-notice-shown");
|
|
15326
15939
|
function buildEvent(ctx, identity, nowMs) {
|
|
15327
15940
|
const properties = {
|
|
15328
15941
|
app: APP,
|
|
@@ -15396,7 +16009,7 @@ function getOrCreateDeviceId() {
|
|
|
15396
16009
|
} catch {}
|
|
15397
16010
|
const id = randomUUID();
|
|
15398
16011
|
try {
|
|
15399
|
-
mkdirSync5(
|
|
16012
|
+
mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
|
|
15400
16013
|
writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
|
|
15401
16014
|
`, {
|
|
15402
16015
|
mode: 384
|
|
@@ -15467,14 +16080,14 @@ function telemetryNoticeLine() {
|
|
|
15467
16080
|
}
|
|
15468
16081
|
function firstRunNoticeShown() {
|
|
15469
16082
|
try {
|
|
15470
|
-
return
|
|
16083
|
+
return existsSync18(FIRST_RUN_MARKER);
|
|
15471
16084
|
} catch {
|
|
15472
16085
|
return false;
|
|
15473
16086
|
}
|
|
15474
16087
|
}
|
|
15475
16088
|
function markFirstRunNotice() {
|
|
15476
16089
|
try {
|
|
15477
|
-
mkdirSync5(
|
|
16090
|
+
mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
|
|
15478
16091
|
writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
|
|
15479
16092
|
`, { mode: 384 });
|
|
15480
16093
|
} catch {}
|
|
@@ -15542,14 +16155,14 @@ import { platform } from "node:os";
|
|
|
15542
16155
|
// src/report/breadcrumb.ts
|
|
15543
16156
|
import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
|
|
15544
16157
|
import { homedir as homedir6 } from "node:os";
|
|
15545
|
-
import { join as
|
|
16158
|
+
import { join as join20, dirname as dirname13 } from "node:path";
|
|
15546
16159
|
function breadcrumbPath() {
|
|
15547
|
-
return
|
|
16160
|
+
return join20(homedir6(), ".launchpad", "last-run.json");
|
|
15548
16161
|
}
|
|
15549
16162
|
function writeBreadcrumb(verb, exit) {
|
|
15550
16163
|
try {
|
|
15551
16164
|
const p = breadcrumbPath();
|
|
15552
|
-
mkdirSync6(
|
|
16165
|
+
mkdirSync6(dirname13(p), { recursive: true });
|
|
15553
16166
|
writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
|
|
15554
16167
|
} catch {}
|
|
15555
16168
|
}
|
|
@@ -15749,6 +16362,7 @@ var COMMANDS = [
|
|
|
15749
16362
|
addOwnerCommand,
|
|
15750
16363
|
removeOwnerCommand,
|
|
15751
16364
|
listOwnersCommand,
|
|
16365
|
+
adminCommand,
|
|
15752
16366
|
makeReportCommand("bug"),
|
|
15753
16367
|
makeReportCommand("feature"),
|
|
15754
16368
|
refreshUpdateCacheCommand,
|
|
@@ -15756,7 +16370,7 @@ var COMMANDS = [
|
|
|
15756
16370
|
];
|
|
15757
16371
|
async function dispatch(argv, io, commands = COMMANDS, deps = {}) {
|
|
15758
16372
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
15759
|
-
|
|
16373
|
+
printHelp7(io, commands);
|
|
15760
16374
|
return 0;
|
|
15761
16375
|
}
|
|
15762
16376
|
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
@@ -15819,9 +16433,11 @@ function requiresCliFloorPreflight(verb, args) {
|
|
|
15819
16433
|
return args[0] === "set" || args[0] === "rm";
|
|
15820
16434
|
if (verb === "secrets")
|
|
15821
16435
|
return args[0] === "push";
|
|
16436
|
+
if (verb === "admin")
|
|
16437
|
+
return args[0] === "status-revoke";
|
|
15822
16438
|
return false;
|
|
15823
16439
|
}
|
|
15824
|
-
function
|
|
16440
|
+
function printHelp7(io, commands) {
|
|
15825
16441
|
io.out("launchpad — manage Launchpad-deployed apps from the command line.");
|
|
15826
16442
|
io.out("");
|
|
15827
16443
|
io.out("Usage:");
|