@m-kopa/launchpad-cli 0.54.1 → 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 +17 -0
- package/dist/cli.js +1069 -497
- 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/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,
|
|
@@ -3469,6 +3472,19 @@ var ManifestV2Schema = z2.object({
|
|
|
3469
3472
|
spec: SpecSchema,
|
|
3470
3473
|
status: StatusSchema.optional()
|
|
3471
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
|
+
}
|
|
3472
3488
|
// ../launchpad-engine/dist/secrets-audit.js
|
|
3473
3489
|
function auditSecretPresence(bindings, surfaces) {
|
|
3474
3490
|
const byId = new Map(surfaces.map((s) => [s.id, s]));
|
|
@@ -3588,298 +3604,569 @@ function deriveResources(m) {
|
|
|
3588
3604
|
}
|
|
3589
3605
|
return items;
|
|
3590
3606
|
}
|
|
3591
|
-
// ../launchpad-engine/dist/
|
|
3592
|
-
var
|
|
3593
|
-
var
|
|
3594
|
-
|
|
3595
|
-
|
|
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") {
|
|
3596
3623
|
return {
|
|
3597
|
-
kind: "
|
|
3598
|
-
|
|
3624
|
+
kind: "supported",
|
|
3625
|
+
manifestValid: true,
|
|
3626
|
+
runtimeSupported: true,
|
|
3627
|
+
runtime: "pages"
|
|
3599
3628
|
};
|
|
3600
3629
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
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") {
|
|
3603
3690
|
return {
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3691
|
+
type: manifest.spec.appType,
|
|
3692
|
+
appName: manifest.metadata.slug,
|
|
3693
|
+
containers: manifest.spec.containers ?? []
|
|
3607
3694
|
};
|
|
3608
3695
|
}
|
|
3609
|
-
return {
|
|
3696
|
+
return {
|
|
3697
|
+
type: manifest.deployment.type,
|
|
3698
|
+
appName: manifest.metadata.name,
|
|
3699
|
+
containers: manifest.containers ?? []
|
|
3700
|
+
};
|
|
3610
3701
|
}
|
|
3611
|
-
function
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
# Source: launchpad.yaml (apiVersion: ${manifest.apiVersion})
|
|
3615
|
-
# App: ${manifest.metadata.name}
|
|
3616
|
-
#
|
|
3617
|
-
# Regenerate with \`launchpad generate\` (\`launchpad deploy\` also
|
|
3618
|
-
# re-emits before applying). Manual edits are overwritten on the
|
|
3619
|
-
# next regeneration. Manifest is the source of truth — change
|
|
3620
|
-
# ${manifest.metadata.name}/launchpad.yaml instead.`;
|
|
3621
|
-
lines.push(header);
|
|
3622
|
-
lines.push("");
|
|
3623
|
-
lines.push(`name = "${escapeTomlString(manifest.metadata.name)}"`);
|
|
3624
|
-
lines.push(`main = "worker/index.ts"`);
|
|
3625
|
-
lines.push(`compatibility_date = "${DEFAULT_COMPATIBILITY_DATE}"`);
|
|
3626
|
-
lines.push(`compatibility_flags = [${DEFAULT_COMPATIBILITY_FLAGS.map((f) => `"${escapeTomlString(f)}"`).join(", ")}]`);
|
|
3627
|
-
lines.push("");
|
|
3628
|
-
lines.push("# Workers Logs (PATTERNS.md: non-optional for container apps).");
|
|
3629
|
-
lines.push("[observability]");
|
|
3630
|
-
lines.push("enabled = true");
|
|
3631
|
-
lines.push("head_sampling_rate = 1.0");
|
|
3632
|
-
lines.push("");
|
|
3633
|
-
lines.push("# Platform-managed runtime config. Populated by the deploy");
|
|
3634
|
-
lines.push("# workflow via `wrangler deploy --var KEY:VALUE`. Secrets");
|
|
3635
|
-
lines.push("# (declared in launchpad.yaml secrets.bindings[]) go through");
|
|
3636
|
-
lines.push("# `wrangler secret put` and are NOT here.");
|
|
3637
|
-
lines.push("[vars]");
|
|
3638
|
-
lines.push(`CF_ACCESS_AUD = ""`);
|
|
3639
|
-
lines.push(`CF_ACCESS_TEAM_DOMAIN = ""`);
|
|
3640
|
-
lines.push("");
|
|
3641
|
-
for (const c of containers) {
|
|
3642
|
-
const className = containerClassName(c.name);
|
|
3643
|
-
const maxInstances = c.max_instances ?? 1;
|
|
3644
|
-
lines.push("[[containers]]");
|
|
3645
|
-
lines.push(`class_name = "${className}"`);
|
|
3646
|
-
lines.push(`image = "${escapeTomlString(c.image)}"`);
|
|
3647
|
-
lines.push(`max_instances = ${maxInstances}`);
|
|
3648
|
-
lines.push("");
|
|
3702
|
+
function validImagePath(path7) {
|
|
3703
|
+
if (byteLength(path7) > MAX_PATH_BYTES || !path7.startsWith("./") || path7.includes("\\") || CONTROL_CHARACTER.test(path7)) {
|
|
3704
|
+
return false;
|
|
3649
3705
|
}
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
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;
|
|
3657
3712
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
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
|
+
];
|
|
3668
3932
|
return lines.join(`
|
|
3669
3933
|
`);
|
|
3670
3934
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
function
|
|
3675
|
-
|
|
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: [] };
|
|
3676
3954
|
}
|
|
3677
|
-
function
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
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
|
+
`);
|
|
3681
3991
|
}
|
|
3682
|
-
function escapeTomlString(
|
|
3683
|
-
return
|
|
3992
|
+
function escapeTomlString(value) {
|
|
3993
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
3684
3994
|
}
|
|
3685
3995
|
// ../launchpad-engine/dist/github-workflow.js
|
|
3686
|
-
var
|
|
3687
|
-
var DEFAULT_BUN_VERSION = "1.3.4";
|
|
3996
|
+
var DEFAULT_NODE_VERSION = "22";
|
|
3688
3997
|
var ACTIONS_CHECKOUT_SHA = "de0fac2e4500dabe0009e67214ff5f5447ce83dd";
|
|
3689
|
-
var
|
|
3998
|
+
var ACTIONS_SETUP_NODE_SHA = "48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e";
|
|
3690
3999
|
function generateGithubDeployWorkflow(manifest) {
|
|
3691
|
-
|
|
4000
|
+
const result = validateDeploymentCapability(manifest);
|
|
4001
|
+
if (result.kind === "unsupported") {
|
|
3692
4002
|
return {
|
|
3693
|
-
kind: "
|
|
3694
|
-
reason:
|
|
4003
|
+
kind: "unsupported",
|
|
4004
|
+
reason: result.reason,
|
|
4005
|
+
message: result.message
|
|
3695
4006
|
};
|
|
3696
4007
|
}
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
4008
|
+
if (result.runtime !== "container") {
|
|
4009
|
+
return {
|
|
4010
|
+
kind: "not-applicable",
|
|
4011
|
+
reason: "pages runtimes do not use the container deploy workflow"
|
|
4012
|
+
};
|
|
3701
4013
|
}
|
|
3702
|
-
return {
|
|
3703
|
-
kind: "ok",
|
|
3704
|
-
content: emit2(manifest, bindings),
|
|
3705
|
-
warnings
|
|
3706
|
-
};
|
|
4014
|
+
return { kind: "ok", content: emit3(result.capability), warnings: [] };
|
|
3707
4015
|
}
|
|
3708
|
-
function
|
|
3709
|
-
const
|
|
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
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
lines.push(` set -euo pipefail`);
|
|
3848
|
-
lines.push(` URL="${workerUrl}/api/health"`);
|
|
3849
|
-
lines.push(` code=$(curl -sS -o /dev/null -w "%{http_code}" "$URL")`);
|
|
3850
|
-
lines.push(` case "$code" in`);
|
|
3851
|
-
lines.push(` 302|401)`);
|
|
3852
|
-
lines.push(` echo "backend auth-gate ok: HTTP $code on unauthenticated request to /api/health."`);
|
|
3853
|
-
lines.push(` ;;`);
|
|
3854
|
-
lines.push(` *)`);
|
|
3855
|
-
lines.push(` echo "backend auth-gate regression: expected 302 or 401 on /api/health, got $code" >&2`);
|
|
3856
|
-
lines.push(` curl -sSI "$URL" >&2 || true`);
|
|
3857
|
-
lines.push(` exit 1`);
|
|
3858
|
-
lines.push(` ;;`);
|
|
3859
|
-
lines.push(` esac`);
|
|
3860
|
-
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
|
+
];
|
|
3861
4155
|
return lines.join(`
|
|
3862
4156
|
`);
|
|
3863
4157
|
}
|
|
3864
|
-
function
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
lines.push(` run: |`);
|
|
3877
|
-
lines.push(` if [ -z "\${${name}:-}" ]; then`);
|
|
3878
|
-
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."`);
|
|
3879
|
-
lines.push(` exit 0`);
|
|
3880
|
-
lines.push(` fi`);
|
|
3881
|
-
lines.push(` printf '%s' "$${name}" | bunx wrangler secret put ${name}`);
|
|
3882
|
-
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
|
+
]);
|
|
3883
4170
|
}
|
|
3884
4171
|
// ../launchpad-engine/dist/per-app-workspace.js
|
|
3885
4172
|
var CF_ACCOUNT_ID = "c07cb17c9f742e8144c4828b3693ca65";
|
|
@@ -4231,6 +4518,21 @@ async function bundleAndDeploy(args) {
|
|
|
4231
4518
|
import { readFileSync as readFileSync8, existsSync as existsSync4 } from "node:fs";
|
|
4232
4519
|
import { join as join9 } from "node:path";
|
|
4233
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
|
+
|
|
4234
4536
|
// src/deploy/deployment-status.ts
|
|
4235
4537
|
async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
|
|
4236
4538
|
let raw;
|
|
@@ -4259,11 +4561,39 @@ async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
|
4259
4561
|
return {
|
|
4260
4562
|
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
4261
4563
|
supported: raw.supported,
|
|
4262
|
-
liveDeployment: raw.liveDeployment ?? null,
|
|
4263
|
-
lastSuccessfulDeployment: raw.lastSuccessfulDeployment
|
|
4564
|
+
liveDeployment: scrubLiveDeployment(raw.liveDeployment ?? null),
|
|
4565
|
+
lastSuccessfulDeployment: projectLastSuccessful(raw.lastSuccessfulDeployment),
|
|
4264
4566
|
...raw.reconciledToLive === true ? { reconciledToLive: true } : {}
|
|
4265
4567
|
};
|
|
4266
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
|
+
}
|
|
4267
4597
|
|
|
4268
4598
|
// src/deploy/commit-match-settle.ts
|
|
4269
4599
|
var DEFAULT_REGISTRATION_BOUND_MS = 120000;
|
|
@@ -4429,21 +4759,6 @@ async function verifyServed(args, deps = { fetch }) {
|
|
|
4429
4759
|
};
|
|
4430
4760
|
}
|
|
4431
4761
|
|
|
4432
|
-
// src/deploy/redact-diagnostic.ts
|
|
4433
|
-
var MAX_DIAGNOSTIC_LEN = 600;
|
|
4434
|
-
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;
|
|
4435
|
-
var LONG_TOKEN = /[A-Za-z0-9_\-+/.]{28,}={0,2}/g;
|
|
4436
|
-
function redactDiagnostic(text, maxLen = MAX_DIAGNOSTIC_LEN) {
|
|
4437
|
-
if (text === undefined || text === null)
|
|
4438
|
-
return "";
|
|
4439
|
-
let t = text.replace(SECRET_KV, (_m, key, sep2) => `${key}${sep2}***`);
|
|
4440
|
-
t = t.replace(LONG_TOKEN, "***");
|
|
4441
|
-
t = t.trim();
|
|
4442
|
-
if (t.length > maxLen)
|
|
4443
|
-
t = `${t.slice(0, maxLen - 1)}…`;
|
|
4444
|
-
return t;
|
|
4445
|
-
}
|
|
4446
|
-
|
|
4447
4762
|
// src/commands/status.ts
|
|
4448
4763
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4449
4764
|
|
|
@@ -5304,7 +5619,29 @@ async function runStatus(args, io) {
|
|
|
5304
5619
|
return mapBotError(e, parsed.slug, io);
|
|
5305
5620
|
}
|
|
5306
5621
|
if (lifecycle !== null && lifecycle.state !== "live") {
|
|
5307
|
-
|
|
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);
|
|
5308
5645
|
return 0;
|
|
5309
5646
|
}
|
|
5310
5647
|
let localYaml = null;
|
|
@@ -5352,7 +5689,7 @@ async function runStatus(args, io) {
|
|
|
5352
5689
|
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
5353
5690
|
return mapBotError(e, parsed.slug, io);
|
|
5354
5691
|
}
|
|
5355
|
-
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.`);
|
|
5356
5693
|
}
|
|
5357
5694
|
let standingExceptions = null;
|
|
5358
5695
|
try {
|
|
@@ -5381,7 +5718,7 @@ async function runStatus(args, io) {
|
|
|
5381
5718
|
...deploymentKnown ? { deployment } : {},
|
|
5382
5719
|
...standingExceptions !== null ? { standingExceptions } : {}
|
|
5383
5720
|
};
|
|
5384
|
-
|
|
5721
|
+
emit4(out, parsed.json, io);
|
|
5385
5722
|
return 0;
|
|
5386
5723
|
}
|
|
5387
5724
|
if (local === null) {
|
|
@@ -5398,7 +5735,7 @@ async function runStatus(args, io) {
|
|
|
5398
5735
|
...deploymentKnown ? { deployment } : {},
|
|
5399
5736
|
...standingExceptions !== null ? { standingExceptions } : {}
|
|
5400
5737
|
};
|
|
5401
|
-
|
|
5738
|
+
emit4(out, parsed.json, io);
|
|
5402
5739
|
if (parsed.strict) {
|
|
5403
5740
|
if (deployment?.liveDeployment?.buildStatus === "failure") {
|
|
5404
5741
|
io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
|
|
@@ -5481,7 +5818,7 @@ async function runStatus(args, io) {
|
|
|
5481
5818
|
...accessDrift !== undefined ? { accessDrift } : {},
|
|
5482
5819
|
...groupIssues.length === 0 ? {} : { groupIssues }
|
|
5483
5820
|
};
|
|
5484
|
-
|
|
5821
|
+
emit4(result, parsed.json, io);
|
|
5485
5822
|
if (parsed.strict && (result.state === "drift" || accessDrift?.inSync === false || groupIssues.length > 0)) {
|
|
5486
5823
|
return 1;
|
|
5487
5824
|
}
|
|
@@ -5677,7 +6014,7 @@ function lifecycleOutput(slug, lc) {
|
|
|
5677
6014
|
driftDetails: []
|
|
5678
6015
|
};
|
|
5679
6016
|
}
|
|
5680
|
-
function
|
|
6017
|
+
function emit4(out, asJson, io) {
|
|
5681
6018
|
if (asJson) {
|
|
5682
6019
|
io.out(JSON.stringify(out, null, 2));
|
|
5683
6020
|
return;
|
|
@@ -5689,12 +6026,16 @@ function emit3(out, asJson, io) {
|
|
|
5689
6026
|
return;
|
|
5690
6027
|
case "provisioning_failed":
|
|
5691
6028
|
io.out(`${out.slug}: provisioning FAILED at stage ${out.stage ?? "unknown"}` + (out.failedReason ? `: ${out.failedReason}` : ""));
|
|
5692
|
-
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)) {
|
|
5693
6033
|
io.out(` the app may already be live — run \`launchpad recover ${out.slug}\` to reconcile the record;`);
|
|
5694
6034
|
io.out(` if it is not serving, the same command starts the stage-dependent recovery.`);
|
|
5695
6035
|
} else {
|
|
5696
6036
|
io.out(` run \`launchpad recover ${out.slug}\` to start the stage-dependent recovery.`);
|
|
5697
6037
|
}
|
|
6038
|
+
surfaceFailedLifecycleDeployment(out, io);
|
|
5698
6039
|
return;
|
|
5699
6040
|
case "destroying":
|
|
5700
6041
|
io.out(`${out.slug}: destroying…`);
|
|
@@ -5791,6 +6132,20 @@ function triggerLabel(trigger) {
|
|
|
5791
6132
|
return "launchpad deploy (bot)";
|
|
5792
6133
|
return "an unknown mechanism";
|
|
5793
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
|
+
}
|
|
5794
6149
|
function surfaceDeployment(out, io) {
|
|
5795
6150
|
const dep = out.deployment;
|
|
5796
6151
|
if (dep === undefined || dep === null || !dep.supported)
|
|
@@ -5807,15 +6162,15 @@ function surfaceDeployment(out, io) {
|
|
|
5807
6162
|
const via = triggerLabel(live.trigger);
|
|
5808
6163
|
switch (live.buildStatus) {
|
|
5809
6164
|
case "success":
|
|
5810
|
-
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}]`);
|
|
5811
6166
|
return;
|
|
5812
6167
|
case "in_progress":
|
|
5813
|
-
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}]`);
|
|
5814
6169
|
io.out(" re-run `launchpad status` shortly to confirm the build outcome.");
|
|
5815
6170
|
return;
|
|
5816
6171
|
case "failure": {
|
|
5817
6172
|
const stage = live.failedStage !== undefined ? ` at stage "${live.failedStage}"` : "";
|
|
5818
|
-
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}]`);
|
|
5819
6174
|
if (live.logExcerpt !== undefined && live.logExcerpt.length > 0) {
|
|
5820
6175
|
io.out(" build log (excerpt):");
|
|
5821
6176
|
for (const line of live.logExcerpt.split(`
|
|
@@ -7718,6 +8073,81 @@ function handleNetworkError(e, io, slug, verb) {
|
|
|
7718
8073
|
return 1;
|
|
7719
8074
|
}
|
|
7720
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
|
+
|
|
7721
8151
|
// src/commands/deploy.ts
|
|
7722
8152
|
var deployCommand = {
|
|
7723
8153
|
name: "deploy",
|
|
@@ -7904,7 +8334,7 @@ async function runDeploy(args, io) {
|
|
|
7904
8334
|
io.err(`launchpad deploy: ${e.message}`);
|
|
7905
8335
|
return 1;
|
|
7906
8336
|
}
|
|
7907
|
-
io.err(`launchpad deploy failed: ${
|
|
8337
|
+
io.err(`launchpad deploy failed: ${describe16(e)}`);
|
|
7908
8338
|
return 1;
|
|
7909
8339
|
}
|
|
7910
8340
|
}
|
|
@@ -7975,7 +8405,7 @@ function formatBytes2(n) {
|
|
|
7975
8405
|
return `${(n / 1024).toFixed(1)}KB`;
|
|
7976
8406
|
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
7977
8407
|
}
|
|
7978
|
-
function
|
|
8408
|
+
function describe16(e) {
|
|
7979
8409
|
return e instanceof Error ? e.message : String(e);
|
|
7980
8410
|
}
|
|
7981
8411
|
function surfaceDeployExtras(body, io, slug, allowStale = false) {
|
|
@@ -8047,11 +8477,11 @@ function surfaceStaleBlock(body, io) {
|
|
|
8047
8477
|
}
|
|
8048
8478
|
var MANIFEST_DIFF_READ_TIMEOUT_MS = 5000;
|
|
8049
8479
|
function withTimeout(p, ms) {
|
|
8050
|
-
return new Promise((
|
|
8480
|
+
return new Promise((resolve10, reject) => {
|
|
8051
8481
|
const timer = setTimeout(() => reject(new Error(`manifest read timed out after ${ms}ms`)), ms);
|
|
8052
8482
|
p.then((v) => {
|
|
8053
8483
|
clearTimeout(timer);
|
|
8054
|
-
|
|
8484
|
+
resolve10(v);
|
|
8055
8485
|
}, (e) => {
|
|
8056
8486
|
clearTimeout(timer);
|
|
8057
8487
|
reject(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -8098,7 +8528,7 @@ async function advanceBaseShaMarker(cwd, sha, io) {
|
|
|
8098
8528
|
return;
|
|
8099
8529
|
await writeBaseShaMarker(cwd, sha);
|
|
8100
8530
|
} catch (e) {
|
|
8101
|
-
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.`);
|
|
8102
8532
|
}
|
|
8103
8533
|
}
|
|
8104
8534
|
async function runModelADeploy(args) {
|
|
@@ -8113,12 +8543,30 @@ async function runModelADeploy(args) {
|
|
|
8113
8543
|
return 64;
|
|
8114
8544
|
}
|
|
8115
8545
|
slug = metaSlug;
|
|
8116
|
-
const
|
|
8117
|
-
|
|
8118
|
-
|
|
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
|
+
}
|
|
8119
8567
|
}
|
|
8120
8568
|
} catch (e) {
|
|
8121
|
-
io.err(`launchpad deploy: failed to read ${manifestPath}: ${
|
|
8569
|
+
io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe16(e)}`);
|
|
8122
8570
|
return 1;
|
|
8123
8571
|
}
|
|
8124
8572
|
const noVerify = args.argv.includes("--no-verify");
|
|
@@ -8148,7 +8596,7 @@ async function runModelADeploy(args) {
|
|
|
8148
8596
|
try {
|
|
8149
8597
|
cfg = loadConfig();
|
|
8150
8598
|
} catch (e) {
|
|
8151
|
-
io.err(`launchpad deploy: ${
|
|
8599
|
+
io.err(`launchpad deploy: ${describe16(e)}`);
|
|
8152
8600
|
return 1;
|
|
8153
8601
|
}
|
|
8154
8602
|
try {
|
|
@@ -8198,7 +8646,7 @@ async function runModelADeploy(args) {
|
|
|
8198
8646
|
io.err(" run `launchpad login` to refresh your session.");
|
|
8199
8647
|
return 1;
|
|
8200
8648
|
}
|
|
8201
|
-
io.err(`launchpad deploy: unexpected error: ${
|
|
8649
|
+
io.err(`launchpad deploy: unexpected error: ${describe16(e)}`);
|
|
8202
8650
|
return 1;
|
|
8203
8651
|
}
|
|
8204
8652
|
switch (result.kind) {
|
|
@@ -8673,7 +9121,7 @@ async function runEnvvars(args, io) {
|
|
|
8673
9121
|
io.err(`launchpad envvars: ${e.message}`);
|
|
8674
9122
|
return 1;
|
|
8675
9123
|
}
|
|
8676
|
-
io.err(`launchpad envvars failed: ${
|
|
9124
|
+
io.err(`launchpad envvars failed: ${describe17(e)}`);
|
|
8677
9125
|
return 1;
|
|
8678
9126
|
}
|
|
8679
9127
|
}
|
|
@@ -8770,16 +9218,16 @@ function renderList(envVars, io) {
|
|
|
8770
9218
|
io.out(fmt(row));
|
|
8771
9219
|
}
|
|
8772
9220
|
}
|
|
8773
|
-
function
|
|
9221
|
+
function describe17(e) {
|
|
8774
9222
|
return e instanceof Error ? e.message : String(e);
|
|
8775
9223
|
}
|
|
8776
9224
|
|
|
8777
9225
|
// src/commands/generate.ts
|
|
8778
9226
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8779
|
-
import { dirname as
|
|
9227
|
+
import { dirname as dirname7, resolve as resolve10, relative as relative4 } from "node:path";
|
|
8780
9228
|
var generateCommand = {
|
|
8781
9229
|
name: "generate",
|
|
8782
|
-
summary: "emit derived artefacts
|
|
9230
|
+
summary: "emit derived runtime artefacts from launchpad.yaml",
|
|
8783
9231
|
run: runGenerate
|
|
8784
9232
|
};
|
|
8785
9233
|
async function runGenerate(args, io) {
|
|
@@ -8789,33 +9237,50 @@ async function runGenerate(args, io) {
|
|
|
8789
9237
|
io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
|
|
8790
9238
|
return 64;
|
|
8791
9239
|
}
|
|
8792
|
-
const manifestPath =
|
|
9240
|
+
const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
8793
9241
|
const result = loadManifest(manifestPath);
|
|
8794
9242
|
if (result.kind !== "ok") {
|
|
8795
9243
|
return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
|
|
8796
9244
|
}
|
|
8797
|
-
const
|
|
8798
|
-
|
|
8799
|
-
|
|
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");
|
|
8800
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;
|
|
8801
9261
|
const workflowOut = generateGithubDeployWorkflow(result.manifest);
|
|
8802
9262
|
if (flags.dryRun) {
|
|
8803
9263
|
return renderDryRun([
|
|
8804
9264
|
{ artefact: "wrangler.toml", path: wranglerPath, result: wranglerOut },
|
|
9265
|
+
{ artefact: "worker/index.ts", path: workerPath, result: workerOut },
|
|
8805
9266
|
{ artefact: "deploy.yml", path: workflowPath, result: workflowOut }
|
|
8806
9267
|
], appRoot, flags.json, io);
|
|
8807
9268
|
}
|
|
8808
9269
|
const reports = [
|
|
8809
9270
|
applyOne("wrangler.toml", wranglerPath, wranglerOut, flags.force),
|
|
9271
|
+
applyOne("worker/index.ts", workerPath, workerOut, flags.force),
|
|
8810
9272
|
applyOne("deploy.yml", workflowPath, workflowOut, flags.force)
|
|
8811
9273
|
];
|
|
8812
9274
|
return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
|
|
8813
9275
|
}
|
|
8814
9276
|
function applyOne(artefact, path10, out, force) {
|
|
8815
|
-
if (out.kind === "not-applicable") {
|
|
9277
|
+
if (out.kind === "not-applicable" || out.kind === "unsupported") {
|
|
8816
9278
|
return {
|
|
8817
9279
|
artefact,
|
|
8818
|
-
action: {
|
|
9280
|
+
action: {
|
|
9281
|
+
kind: "not-applicable",
|
|
9282
|
+
reason: out.kind === "unsupported" ? `${out.reason}: ${out.message}` : out.reason
|
|
9283
|
+
},
|
|
8819
9284
|
warnings: []
|
|
8820
9285
|
};
|
|
8821
9286
|
}
|
|
@@ -8838,7 +9303,7 @@ function applyOne(artefact, path10, out, force) {
|
|
|
8838
9303
|
};
|
|
8839
9304
|
}
|
|
8840
9305
|
try {
|
|
8841
|
-
mkdirSync2(
|
|
9306
|
+
mkdirSync2(dirname7(path10), { recursive: true });
|
|
8842
9307
|
writeFileSync2(path10, out.content, "utf8");
|
|
8843
9308
|
return {
|
|
8844
9309
|
artefact,
|
|
@@ -8874,12 +9339,12 @@ function readIfExists(path10) {
|
|
|
8874
9339
|
function renderDryRun(inputs, appRoot, json, io) {
|
|
8875
9340
|
if (json) {
|
|
8876
9341
|
const files = inputs.map((i) => {
|
|
8877
|
-
const previewPath = i.preview === undefined ?
|
|
8878
|
-
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") {
|
|
8879
9344
|
return {
|
|
8880
9345
|
artefact: i.artefact,
|
|
8881
9346
|
kind: "not-applicable",
|
|
8882
|
-
reason: i.result.reason,
|
|
9347
|
+
reason: i.result.kind === "unsupported" ? `${i.result.reason}: ${i.result.message}` : i.result.reason,
|
|
8883
9348
|
...i.preview ? { preview: true, destination: i.preview.destination } : {}
|
|
8884
9349
|
};
|
|
8885
9350
|
}
|
|
@@ -8896,12 +9361,13 @@ function renderDryRun(inputs, appRoot, json, io) {
|
|
|
8896
9361
|
return 0;
|
|
8897
9362
|
}
|
|
8898
9363
|
for (const i of inputs) {
|
|
8899
|
-
if (i.result.kind === "not-applicable") {
|
|
8900
|
-
|
|
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}`);
|
|
8901
9367
|
io.out("");
|
|
8902
9368
|
continue;
|
|
8903
9369
|
}
|
|
8904
|
-
const headerPath = i.preview === undefined ?
|
|
9370
|
+
const headerPath = i.preview === undefined ? relative4(appRoot, i.path) : i.path;
|
|
8905
9371
|
const header = i.preview === undefined ? `--- ${headerPath} ---` : `--- [preview, destination = ${i.preview.destination}] ${headerPath} ---`;
|
|
8906
9372
|
io.out(header);
|
|
8907
9373
|
io.out(i.result.content);
|
|
@@ -8920,20 +9386,20 @@ function renderApplyHuman(reports, appRoot, io) {
|
|
|
8920
9386
|
const a = r.action;
|
|
8921
9387
|
switch (a.kind) {
|
|
8922
9388
|
case "written":
|
|
8923
|
-
io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${
|
|
9389
|
+
io.out(`✓ ${r.artefact} — wrote ${a.bytes} bytes to ${relative4(appRoot, a.path)}`);
|
|
8924
9390
|
break;
|
|
8925
9391
|
case "unchanged":
|
|
8926
|
-
io.out(`= ${r.artefact} — unchanged (${
|
|
9392
|
+
io.out(`= ${r.artefact} — unchanged (${relative4(appRoot, a.path)})`);
|
|
8927
9393
|
break;
|
|
8928
9394
|
case "would-overwrite":
|
|
8929
|
-
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`);
|
|
8930
9396
|
wouldOverwrite++;
|
|
8931
9397
|
break;
|
|
8932
9398
|
case "not-applicable":
|
|
8933
9399
|
io.out(`- ${r.artefact} — skipped (${a.reason})`);
|
|
8934
9400
|
break;
|
|
8935
9401
|
case "write-error":
|
|
8936
|
-
io.err(`✗ ${r.artefact} — ${a.message} (${
|
|
9402
|
+
io.err(`✗ ${r.artefact} — ${a.message} (${relative4(appRoot, a.path)})`);
|
|
8937
9403
|
writeError++;
|
|
8938
9404
|
break;
|
|
8939
9405
|
}
|
|
@@ -8952,17 +9418,17 @@ function renderApplyJson(reports, appRoot, io) {
|
|
|
8952
9418
|
const base = { artefact: r.artefact, warnings: r.warnings };
|
|
8953
9419
|
switch (a.kind) {
|
|
8954
9420
|
case "written":
|
|
8955
|
-
return { ...base, kind: a.kind, path:
|
|
9421
|
+
return { ...base, kind: a.kind, path: relative4(appRoot, a.path), bytes: a.bytes };
|
|
8956
9422
|
case "unchanged":
|
|
8957
9423
|
case "would-overwrite":
|
|
8958
|
-
return { ...base, kind: a.kind, path:
|
|
9424
|
+
return { ...base, kind: a.kind, path: relative4(appRoot, a.path) };
|
|
8959
9425
|
case "not-applicable":
|
|
8960
9426
|
return { ...base, kind: a.kind, reason: a.reason };
|
|
8961
9427
|
case "write-error":
|
|
8962
9428
|
return {
|
|
8963
9429
|
...base,
|
|
8964
9430
|
kind: a.kind,
|
|
8965
|
-
path:
|
|
9431
|
+
path: relative4(appRoot, a.path),
|
|
8966
9432
|
message: a.message
|
|
8967
9433
|
};
|
|
8968
9434
|
}
|
|
@@ -9086,13 +9552,13 @@ function decodeJwtPayload(token) {
|
|
|
9086
9552
|
try {
|
|
9087
9553
|
json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
|
|
9088
9554
|
} catch (e) {
|
|
9089
|
-
throw new JwtParseError(`could not base64-decode JWT payload: ${
|
|
9555
|
+
throw new JwtParseError(`could not base64-decode JWT payload: ${describe18(e)}`);
|
|
9090
9556
|
}
|
|
9091
9557
|
let parsed;
|
|
9092
9558
|
try {
|
|
9093
9559
|
parsed = JSON.parse(json);
|
|
9094
9560
|
} catch (e) {
|
|
9095
|
-
throw new JwtParseError(`JWT payload is not JSON: ${
|
|
9561
|
+
throw new JwtParseError(`JWT payload is not JSON: ${describe18(e)}`);
|
|
9096
9562
|
}
|
|
9097
9563
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
9098
9564
|
throw new JwtParseError(`JWT payload is not an object`);
|
|
@@ -9103,7 +9569,7 @@ function b64UrlToB64(s) {
|
|
|
9103
9569
|
const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
|
|
9104
9570
|
return padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
9105
9571
|
}
|
|
9106
|
-
function
|
|
9572
|
+
function describe18(e) {
|
|
9107
9573
|
return e instanceof Error ? e.message : String(e);
|
|
9108
9574
|
}
|
|
9109
9575
|
|
|
@@ -9130,7 +9596,7 @@ async function runGroupsWhoami(args, io) {
|
|
|
9130
9596
|
try {
|
|
9131
9597
|
payload = decodeJwtPayload(session.accessToken);
|
|
9132
9598
|
} catch (e) {
|
|
9133
|
-
const message = e instanceof JwtParseError ? e.message :
|
|
9599
|
+
const message = e instanceof JwtParseError ? e.message : describe19(e);
|
|
9134
9600
|
if (json) {
|
|
9135
9601
|
io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
|
|
9136
9602
|
} else {
|
|
@@ -9211,7 +9677,7 @@ function parseFlags2(args) {
|
|
|
9211
9677
|
}
|
|
9212
9678
|
return { kind: "ok", json };
|
|
9213
9679
|
}
|
|
9214
|
-
function
|
|
9680
|
+
function describe19(e) {
|
|
9215
9681
|
return e instanceof Error ? e.message : String(e);
|
|
9216
9682
|
}
|
|
9217
9683
|
|
|
@@ -9314,7 +9780,7 @@ async function loadGroups(io, refresh, json) {
|
|
|
9314
9780
|
}
|
|
9315
9781
|
return { kind: "error", code: 3 };
|
|
9316
9782
|
}
|
|
9317
|
-
renderFetchError(io,
|
|
9783
|
+
renderFetchError(io, describe20(e), json);
|
|
9318
9784
|
return { kind: "error", code: 2 };
|
|
9319
9785
|
}
|
|
9320
9786
|
}
|
|
@@ -9351,7 +9817,7 @@ async function lookupTenantWide(key) {
|
|
|
9351
9817
|
return { kind: "unauthenticated", message: e.message };
|
|
9352
9818
|
if (e instanceof ForbiddenError)
|
|
9353
9819
|
return { kind: "forbidden", message: e.message };
|
|
9354
|
-
return { kind: "error", message:
|
|
9820
|
+
return { kind: "error", message: describe20(e) };
|
|
9355
9821
|
}
|
|
9356
9822
|
}
|
|
9357
9823
|
function renderLookupFailure(io, json, hit) {
|
|
@@ -9497,9 +9963,9 @@ async function runShow(args, io) {
|
|
|
9497
9963
|
}
|
|
9498
9964
|
const key = parsed.flags.positional[0];
|
|
9499
9965
|
const hit = await lookupTenantWide(key);
|
|
9500
|
-
const
|
|
9501
|
-
if (
|
|
9502
|
-
return
|
|
9966
|
+
const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
|
|
9967
|
+
if (failure2 !== null)
|
|
9968
|
+
return failure2;
|
|
9503
9969
|
if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
|
|
9504
9970
|
return 2;
|
|
9505
9971
|
}
|
|
@@ -9546,9 +10012,9 @@ async function runResolve(args, io) {
|
|
|
9546
10012
|
}
|
|
9547
10013
|
const key = parsed.flags.positional[0];
|
|
9548
10014
|
const hit = await lookupTenantWide(key);
|
|
9549
|
-
const
|
|
9550
|
-
if (
|
|
9551
|
-
return
|
|
10015
|
+
const failure2 = renderLookupFailure(io, parsed.flags.json, hit);
|
|
10016
|
+
if (failure2 !== null)
|
|
10017
|
+
return failure2;
|
|
9552
10018
|
if (hit.kind !== "found" && hit.kind !== "ambiguous" && hit.kind !== "not-found") {
|
|
9553
10019
|
return 2;
|
|
9554
10020
|
}
|
|
@@ -9595,23 +10061,23 @@ function renderTable2(io, groups) {
|
|
|
9595
10061
|
for (const r of rows)
|
|
9596
10062
|
io.out(fmt(r));
|
|
9597
10063
|
}
|
|
9598
|
-
function
|
|
10064
|
+
function describe20(e) {
|
|
9599
10065
|
return e instanceof Error ? e.message : String(e);
|
|
9600
10066
|
}
|
|
9601
10067
|
|
|
9602
10068
|
// src/commands/init.ts
|
|
9603
10069
|
import { createInterface } from "node:readline/promises";
|
|
9604
10070
|
import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "node:fs";
|
|
9605
|
-
import { dirname as
|
|
10071
|
+
import { dirname as dirname8, resolve as resolve11 } from "node:path";
|
|
9606
10072
|
import { stringify as yamlStringify } from "yaml";
|
|
9607
10073
|
|
|
9608
10074
|
// src/detect/index.ts
|
|
9609
10075
|
import { existsSync as existsSync9, readFileSync as readFileSync13, statSync } from "node:fs";
|
|
9610
|
-
import { join as
|
|
10076
|
+
import { join as join13 } from "node:path";
|
|
9611
10077
|
function detectAppShape(cwd) {
|
|
9612
|
-
const hasPackageJson = existsSync9(
|
|
9613
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync9(
|
|
9614
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync9(
|
|
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)));
|
|
9615
10081
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
9616
10082
|
return {
|
|
9617
10083
|
kind: "not-applicable",
|
|
@@ -9657,7 +10123,7 @@ var LOCKFILES = [
|
|
|
9657
10123
|
{ file: "yarn.lock", pm: "yarn" }
|
|
9658
10124
|
];
|
|
9659
10125
|
function detectPackageManager(cwd) {
|
|
9660
|
-
const present = LOCKFILES.filter(({ file }) => existsSync9(
|
|
10126
|
+
const present = LOCKFILES.filter(({ file }) => existsSync9(join13(cwd, file)));
|
|
9661
10127
|
if (present.length === 0) {
|
|
9662
10128
|
return {
|
|
9663
10129
|
kind: "ambiguous",
|
|
@@ -9679,7 +10145,7 @@ function detectPackageManager(cwd) {
|
|
|
9679
10145
|
}
|
|
9680
10146
|
function detectVitePresence(cwd) {
|
|
9681
10147
|
for (const name of VITE_CONFIG_NAMES) {
|
|
9682
|
-
const p =
|
|
10148
|
+
const p = join13(cwd, name);
|
|
9683
10149
|
if (existsSync9(p)) {
|
|
9684
10150
|
return { kind: "ok", value: { path: p } };
|
|
9685
10151
|
}
|
|
@@ -9690,7 +10156,7 @@ function detectVitePresence(cwd) {
|
|
|
9690
10156
|
};
|
|
9691
10157
|
}
|
|
9692
10158
|
function detectAppType(cwd) {
|
|
9693
|
-
const fnDir =
|
|
10159
|
+
const fnDir = join13(cwd, "functions");
|
|
9694
10160
|
let hasFunctionsDir = false;
|
|
9695
10161
|
if (existsSync9(fnDir)) {
|
|
9696
10162
|
try {
|
|
@@ -9702,7 +10168,7 @@ function detectAppType(cwd) {
|
|
|
9702
10168
|
return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
|
|
9703
10169
|
}
|
|
9704
10170
|
function detectBuildCommand(cwd, pm) {
|
|
9705
|
-
const pkgJsonPath =
|
|
10171
|
+
const pkgJsonPath = join13(cwd, "package.json");
|
|
9706
10172
|
if (!existsSync9(pkgJsonPath)) {
|
|
9707
10173
|
return {
|
|
9708
10174
|
kind: "ambiguous",
|
|
@@ -9775,7 +10241,7 @@ async function runInit(args, io, prompt) {
|
|
|
9775
10241
|
return 64;
|
|
9776
10242
|
}
|
|
9777
10243
|
const { inputs, options } = parsed;
|
|
9778
|
-
const outPath =
|
|
10244
|
+
const outPath = resolve11(process.cwd(), options.out);
|
|
9779
10245
|
if (existsSync10(outPath) && !options.force) {
|
|
9780
10246
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
9781
10247
|
io.err("Pass --force to overwrite.");
|
|
@@ -9823,7 +10289,7 @@ async function runInit(args, io, prompt) {
|
|
|
9823
10289
|
}
|
|
9824
10290
|
io.out(`✓ wrote ${outPath}`);
|
|
9825
10291
|
if (options.gitignore) {
|
|
9826
|
-
const gitignorePath =
|
|
10292
|
+
const gitignorePath = resolve11(process.cwd(), ".gitignore");
|
|
9827
10293
|
try {
|
|
9828
10294
|
const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
|
|
9829
10295
|
if (changed.length > 0) {
|
|
@@ -9838,8 +10304,8 @@ async function runInit(args, io, prompt) {
|
|
|
9838
10304
|
}
|
|
9839
10305
|
}
|
|
9840
10306
|
if (fillResult.inputs.type === "static") {
|
|
9841
|
-
const serveDir =
|
|
9842
|
-
const indexPath =
|
|
10307
|
+
const serveDir = resolve11(dirname8(outPath), STATIC_SERVE_DIR);
|
|
10308
|
+
const indexPath = resolve11(serveDir, "index.html");
|
|
9843
10309
|
if (!existsSync10(indexPath)) {
|
|
9844
10310
|
try {
|
|
9845
10311
|
mkdirSync3(serveDir, { recursive: true });
|
|
@@ -10306,13 +10772,13 @@ async function runLogin(args, io, deps) {
|
|
|
10306
10772
|
io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
|
|
10307
10773
|
return 0;
|
|
10308
10774
|
} catch (e) {
|
|
10309
|
-
io.err(`Gateway login failed: ${
|
|
10775
|
+
io.err(`Gateway login failed: ${describe21(e)}`);
|
|
10310
10776
|
io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
|
|
10311
10777
|
io.err("");
|
|
10312
10778
|
return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
|
|
10313
10779
|
}
|
|
10314
10780
|
} catch (e) {
|
|
10315
|
-
io.err(`launchpad login failed: ${
|
|
10781
|
+
io.err(`launchpad login failed: ${describe21(e)}`);
|
|
10316
10782
|
return 1;
|
|
10317
10783
|
}
|
|
10318
10784
|
}
|
|
@@ -10354,7 +10820,7 @@ function printLoginHelp(io) {
|
|
|
10354
10820
|
io.out(" 0 signed in / session stored");
|
|
10355
10821
|
io.out(" 1 login failed (network, browser, or gateway error)");
|
|
10356
10822
|
}
|
|
10357
|
-
function
|
|
10823
|
+
function describe21(e) {
|
|
10358
10824
|
return e instanceof Error ? e.message : String(e);
|
|
10359
10825
|
}
|
|
10360
10826
|
|
|
@@ -10391,14 +10857,14 @@ async function runLogout(args, io, deps) {
|
|
|
10391
10857
|
});
|
|
10392
10858
|
io.out("Server-side session revoked.");
|
|
10393
10859
|
} catch (e) {
|
|
10394
|
-
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.`);
|
|
10395
10861
|
}
|
|
10396
10862
|
}
|
|
10397
10863
|
const had = await clearSession(cfg.sessionPath);
|
|
10398
10864
|
io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
|
|
10399
10865
|
return 0;
|
|
10400
10866
|
} catch (e) {
|
|
10401
|
-
io.err(`launchpad logout failed: ${
|
|
10867
|
+
io.err(`launchpad logout failed: ${describe22(e)}`);
|
|
10402
10868
|
return 1;
|
|
10403
10869
|
}
|
|
10404
10870
|
}
|
|
@@ -10418,7 +10884,7 @@ function printLogoutHelp(io) {
|
|
|
10418
10884
|
io.out(" 0 logged out (or already logged out)");
|
|
10419
10885
|
io.out(" 1 could not clear the local session (file IO / config error)");
|
|
10420
10886
|
}
|
|
10421
|
-
function
|
|
10887
|
+
function describe22(e) {
|
|
10422
10888
|
return e instanceof Error ? e.message : String(e);
|
|
10423
10889
|
}
|
|
10424
10890
|
|
|
@@ -10488,7 +10954,7 @@ async function runLogs(args, io) {
|
|
|
10488
10954
|
io.err(`launchpad logs: ${e.message}`);
|
|
10489
10955
|
return 1;
|
|
10490
10956
|
}
|
|
10491
|
-
io.err(`launchpad logs failed: ${
|
|
10957
|
+
io.err(`launchpad logs failed: ${describe23(e)}`);
|
|
10492
10958
|
return 1;
|
|
10493
10959
|
}
|
|
10494
10960
|
}
|
|
@@ -10568,7 +11034,7 @@ function formatRelative2(iso) {
|
|
|
10568
11034
|
return `${hr}h ago`;
|
|
10569
11035
|
return `${Math.floor(hr / 24)}d ago`;
|
|
10570
11036
|
}
|
|
10571
|
-
function
|
|
11037
|
+
function describe23(e) {
|
|
10572
11038
|
return e instanceof Error ? e.message : String(e);
|
|
10573
11039
|
}
|
|
10574
11040
|
|
|
@@ -10617,7 +11083,7 @@ async function runMerge(args, io) {
|
|
|
10617
11083
|
try {
|
|
10618
11084
|
body = await res.json();
|
|
10619
11085
|
} catch (e) {
|
|
10620
|
-
io.err(`launchpad merge: bot returned 2xx with malformed body: ${
|
|
11086
|
+
io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe24(e)}`);
|
|
10621
11087
|
return 1;
|
|
10622
11088
|
}
|
|
10623
11089
|
io.out("");
|
|
@@ -10657,7 +11123,7 @@ async function runMerge(args, io) {
|
|
|
10657
11123
|
io.err(`launchpad merge: ${e.message}`);
|
|
10658
11124
|
return 1;
|
|
10659
11125
|
}
|
|
10660
|
-
io.err(`launchpad merge failed: ${
|
|
11126
|
+
io.err(`launchpad merge failed: ${describe24(e)}`);
|
|
10661
11127
|
return 1;
|
|
10662
11128
|
}
|
|
10663
11129
|
}
|
|
@@ -10756,12 +11222,12 @@ function renderBotError(status, env, io, prNumber = null) {
|
|
|
10756
11222
|
io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
|
|
10757
11223
|
}
|
|
10758
11224
|
}
|
|
10759
|
-
function
|
|
11225
|
+
function describe24(e) {
|
|
10760
11226
|
return e instanceof Error ? e.message : String(e);
|
|
10761
11227
|
}
|
|
10762
11228
|
|
|
10763
11229
|
// src/commands/plan.ts
|
|
10764
|
-
import { resolve as
|
|
11230
|
+
import { resolve as resolve12 } from "node:path";
|
|
10765
11231
|
var planCommand = {
|
|
10766
11232
|
name: "plan",
|
|
10767
11233
|
summary: "summarise what the manifest would deploy (offline)",
|
|
@@ -10774,8 +11240,19 @@ async function runPlan(args, io) {
|
|
|
10774
11240
|
io.err("Usage: launchpad plan [--file <path>] [--json]");
|
|
10775
11241
|
return 64;
|
|
10776
11242
|
}
|
|
10777
|
-
const manifestPath =
|
|
11243
|
+
const manifestPath = resolve12(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
10778
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
|
+
}
|
|
10779
11256
|
return flags.json ? renderJson(result, io) : renderHuman(result, io);
|
|
10780
11257
|
}
|
|
10781
11258
|
function renderJson(result, io) {
|
|
@@ -11008,7 +11485,7 @@ async function runDestroy(args, io, prompt, isTty) {
|
|
|
11008
11485
|
io.err(`launchpad destroy: ${e.message}`);
|
|
11009
11486
|
return 1;
|
|
11010
11487
|
}
|
|
11011
|
-
io.err(`launchpad destroy failed: ${
|
|
11488
|
+
io.err(`launchpad destroy failed: ${describe25(e)}`);
|
|
11012
11489
|
return 1;
|
|
11013
11490
|
}
|
|
11014
11491
|
}
|
|
@@ -11208,7 +11685,7 @@ function printUsage4(io) {
|
|
|
11208
11685
|
].join(`
|
|
11209
11686
|
`));
|
|
11210
11687
|
}
|
|
11211
|
-
function
|
|
11688
|
+
function describe25(e) {
|
|
11212
11689
|
return e instanceof Error ? e.message : String(e);
|
|
11213
11690
|
}
|
|
11214
11691
|
|
|
@@ -11340,22 +11817,22 @@ async function treesEqual(cwd, tracked, headDir, headPaths) {
|
|
|
11340
11817
|
if (tracked.length !== headPaths.length)
|
|
11341
11818
|
return false;
|
|
11342
11819
|
const expected = new Set(headPaths);
|
|
11343
|
-
for (const
|
|
11344
|
-
if (!expected.has(
|
|
11820
|
+
for (const relative6 of tracked) {
|
|
11821
|
+
if (!expected.has(relative6))
|
|
11345
11822
|
return false;
|
|
11346
|
-
if (!equal(await readEntry(path12.resolve(cwd,
|
|
11823
|
+
if (!equal(await readEntry(path12.resolve(cwd, relative6)), await readEntry(path12.resolve(headDir, relative6)))) {
|
|
11347
11824
|
return false;
|
|
11348
11825
|
}
|
|
11349
11826
|
}
|
|
11350
11827
|
return true;
|
|
11351
11828
|
}
|
|
11352
|
-
async function listFiles(root,
|
|
11829
|
+
async function listFiles(root, relative6 = "") {
|
|
11353
11830
|
const files = [];
|
|
11354
|
-
const entries = await fs6.readdir(path12.resolve(root,
|
|
11831
|
+
const entries = await fs6.readdir(path12.resolve(root, relative6), {
|
|
11355
11832
|
withFileTypes: true
|
|
11356
11833
|
});
|
|
11357
11834
|
for (const entry of entries) {
|
|
11358
|
-
const child =
|
|
11835
|
+
const child = relative6.length === 0 ? entry.name : `${relative6}/${entry.name}`;
|
|
11359
11836
|
if (entry.isDirectory())
|
|
11360
11837
|
files.push(...await listFiles(root, child));
|
|
11361
11838
|
else if (entry.isFile())
|
|
@@ -11364,12 +11841,12 @@ async function listFiles(root, relative5 = "") {
|
|
|
11364
11841
|
return files.sort();
|
|
11365
11842
|
}
|
|
11366
11843
|
function gitOutput(cwd, args) {
|
|
11367
|
-
return new Promise((
|
|
11844
|
+
return new Promise((resolve14, reject) => {
|
|
11368
11845
|
execFile("git", [...args], { cwd, encoding: "utf8" }, (error, stdout) => {
|
|
11369
11846
|
if (error !== null)
|
|
11370
11847
|
reject(error);
|
|
11371
11848
|
else
|
|
11372
|
-
|
|
11849
|
+
resolve14(stdout);
|
|
11373
11850
|
});
|
|
11374
11851
|
});
|
|
11375
11852
|
}
|
|
@@ -11378,7 +11855,7 @@ function splitNull(value) {
|
|
|
11378
11855
|
}
|
|
11379
11856
|
async function reconcile(args) {
|
|
11380
11857
|
const removals = await cleanAncestorRemovals(args);
|
|
11381
|
-
const planned = await Promise.all(args.paths.map((
|
|
11858
|
+
const planned = await Promise.all(args.paths.map((relative6) => planPath(args, relative6, removals)));
|
|
11382
11859
|
const conflicts = planned.flatMap((entry) => entry.conflict === undefined ? [] : [entry.conflict]);
|
|
11383
11860
|
if (conflicts.length > 0) {
|
|
11384
11861
|
const report = {
|
|
@@ -11422,55 +11899,55 @@ async function reconcile(args) {
|
|
|
11422
11899
|
}
|
|
11423
11900
|
async function cleanAncestorRemovals(args) {
|
|
11424
11901
|
const removals = new Set;
|
|
11425
|
-
for (const
|
|
11426
|
-
assertSafePath(
|
|
11427
|
-
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);
|
|
11428
11905
|
if (checked.kind === "blocked")
|
|
11429
11906
|
continue;
|
|
11430
|
-
const base = await readEntry(path12.resolve(args.baseDir,
|
|
11907
|
+
const base = await readEntry(path12.resolve(args.baseDir, relative6));
|
|
11431
11908
|
const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
|
|
11432
11909
|
if (local.kind !== "missing" && cleanLocal(base, local))
|
|
11433
|
-
removals.add(
|
|
11910
|
+
removals.add(relative6);
|
|
11434
11911
|
}
|
|
11435
11912
|
return removals;
|
|
11436
11913
|
}
|
|
11437
|
-
async function planPath(args,
|
|
11438
|
-
assertSafePath(
|
|
11439
|
-
const checked = await checkParents(args.cwd,
|
|
11440
|
-
const base = await readEntry(path12.resolve(args.baseDir,
|
|
11441
|
-
const upstreamDeleted = args.compare.deleted.includes(
|
|
11442
|
-
const head = upstreamDeleted ? { kind: "missing" } : await readEntry(path12.resolve(args.headDir,
|
|
11443
|
-
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);
|
|
11444
11921
|
if (checked.kind === "blocked") {
|
|
11445
|
-
return conflictPlan(
|
|
11922
|
+
return conflictPlan(relative6, checked.target, { kind: "missing" }, head, upstreamDeleted, checked.reason);
|
|
11446
11923
|
}
|
|
11447
11924
|
const local = checked.projectedMissing ? { kind: "missing" } : await readEntry(checked.target);
|
|
11448
|
-
if (
|
|
11449
|
-
return conflictPlan(
|
|
11925
|
+
if (unsupported2) {
|
|
11926
|
+
return conflictPlan(relative6, checked.target, local, head, false, "upstream or baseline path uses an unsupported Git entry type");
|
|
11450
11927
|
}
|
|
11451
11928
|
if (alreadyResolved(local, head, upstreamDeleted)) {
|
|
11452
|
-
return { relative:
|
|
11929
|
+
return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: false };
|
|
11453
11930
|
}
|
|
11454
11931
|
if (unsupportedHead(head, upstreamDeleted)) {
|
|
11455
|
-
return conflictPlan(
|
|
11932
|
+
return conflictPlan(relative6, checked.target, local, head, true, "unsupported upstream file type");
|
|
11456
11933
|
}
|
|
11457
11934
|
if (cleanLocal(base, local)) {
|
|
11458
|
-
return { relative:
|
|
11935
|
+
return { relative: relative6, target: checked.target, local, head, upstreamDeleted, apply: true };
|
|
11459
11936
|
}
|
|
11460
|
-
return conflictPlan(
|
|
11937
|
+
return conflictPlan(relative6, checked.target, local, head, upstreamDeleted, conflictReason(base, local, upstreamDeleted));
|
|
11461
11938
|
}
|
|
11462
|
-
function conflictPlan(
|
|
11939
|
+
function conflictPlan(relative6, target, local, head, upstreamDeleted, reason) {
|
|
11463
11940
|
const suffix = upstreamDeleted ? ".upstream-deleted" : ".upstream";
|
|
11464
|
-
const digest = createHash3("sha256").update(
|
|
11941
|
+
const digest = createHash3("sha256").update(relative6).digest("hex");
|
|
11465
11942
|
return {
|
|
11466
|
-
relative:
|
|
11943
|
+
relative: relative6,
|
|
11467
11944
|
target,
|
|
11468
11945
|
local,
|
|
11469
11946
|
head,
|
|
11470
11947
|
upstreamDeleted,
|
|
11471
11948
|
apply: false,
|
|
11472
11949
|
conflict: {
|
|
11473
|
-
path:
|
|
11950
|
+
path: relative6,
|
|
11474
11951
|
reason,
|
|
11475
11952
|
artifact: `${CONFLICT_DIR}/${digest}${suffix}`,
|
|
11476
11953
|
upstreamDeleted
|
|
@@ -11571,13 +12048,13 @@ function equal(left, right) {
|
|
|
11571
12048
|
return false;
|
|
11572
12049
|
return left.bytes.every((byte, index) => byte === right.bytes[index]);
|
|
11573
12050
|
}
|
|
11574
|
-
async function writeAtomic(cwd,
|
|
11575
|
-
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);
|
|
11576
12053
|
if (checked.kind === "blocked")
|
|
11577
12054
|
throw new Error(checked.reason);
|
|
11578
12055
|
const file = checked.target;
|
|
11579
12056
|
await fs6.mkdir(path12.dirname(file), { recursive: true });
|
|
11580
|
-
const rechecked = await checkParents(cwd,
|
|
12057
|
+
const rechecked = await checkParents(cwd, relative6, new Set, allowMetadata);
|
|
11581
12058
|
if (rechecked.kind === "blocked")
|
|
11582
12059
|
throw new Error(rechecked.reason);
|
|
11583
12060
|
const temporary = `${file}.launchpad-tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
@@ -11592,8 +12069,8 @@ async function writeAtomic(cwd, relative5, bytes, mode, allowMetadata = false) {
|
|
|
11592
12069
|
throw error;
|
|
11593
12070
|
}
|
|
11594
12071
|
}
|
|
11595
|
-
async function writeJsonAtomic(cwd,
|
|
11596
|
-
await writeAtomic(cwd,
|
|
12072
|
+
async function writeJsonAtomic(cwd, relative6, value) {
|
|
12073
|
+
await writeAtomic(cwd, relative6, new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
|
|
11597
12074
|
`), 384, true);
|
|
11598
12075
|
}
|
|
11599
12076
|
async function resetConflictArtifacts(cwd) {
|
|
@@ -11609,37 +12086,37 @@ async function cleanupConflictState(cwd) {
|
|
|
11609
12086
|
throw new Error(report.reason);
|
|
11610
12087
|
await fs6.rm(report.target, { force: true });
|
|
11611
12088
|
}
|
|
11612
|
-
function assertSafePath(
|
|
11613
|
-
if (!isSafeSourcePath(
|
|
11614
|
-
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}`);
|
|
11615
12092
|
}
|
|
11616
12093
|
}
|
|
11617
|
-
function isSafeSourcePath(
|
|
11618
|
-
return isSafeRelativePath(
|
|
12094
|
+
function isSafeSourcePath(relative6) {
|
|
12095
|
+
return isSafeRelativePath(relative6) && !relative6.startsWith(`${MARKER_DIR}/`);
|
|
11619
12096
|
}
|
|
11620
|
-
function isSafeRelativePath(
|
|
11621
|
-
const normalized = path12.posix.normalize(
|
|
11622
|
-
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("../");
|
|
11623
12100
|
}
|
|
11624
|
-
function resolveInsideClone(cwd,
|
|
12101
|
+
function resolveInsideClone(cwd, relative6) {
|
|
11625
12102
|
const root = path12.resolve(cwd);
|
|
11626
|
-
const target = path12.resolve(root,
|
|
12103
|
+
const target = path12.resolve(root, relative6);
|
|
11627
12104
|
const fromRoot = path12.relative(root, target);
|
|
11628
12105
|
if (fromRoot.length === 0 || path12.isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${path12.sep}`)) {
|
|
11629
|
-
throw new Error(`unsafe source path returned by bot: ${
|
|
12106
|
+
throw new Error(`unsafe source path returned by bot: ${relative6}`);
|
|
11630
12107
|
}
|
|
11631
12108
|
return target;
|
|
11632
12109
|
}
|
|
11633
|
-
async function checkParents(cwd,
|
|
12110
|
+
async function checkParents(cwd, relative6, projectedRemovals, allowMetadata = false) {
|
|
11634
12111
|
if (allowMetadata) {
|
|
11635
|
-
if (!isSafeRelativePath(
|
|
11636
|
-
throw new Error(`unsafe metadata path: ${
|
|
12112
|
+
if (!isSafeRelativePath(relative6)) {
|
|
12113
|
+
throw new Error(`unsafe metadata path: ${relative6}`);
|
|
11637
12114
|
}
|
|
11638
12115
|
} else {
|
|
11639
|
-
assertSafePath(
|
|
12116
|
+
assertSafePath(relative6);
|
|
11640
12117
|
}
|
|
11641
|
-
const target = resolveInsideClone(cwd,
|
|
11642
|
-
const components =
|
|
12118
|
+
const target = resolveInsideClone(cwd, relative6);
|
|
12119
|
+
const components = relative6.split("/");
|
|
11643
12120
|
let current = path12.resolve(cwd);
|
|
11644
12121
|
for (let index = 0;index < components.length - 1; index += 1) {
|
|
11645
12122
|
current = path12.join(current, components[index]);
|
|
@@ -11778,7 +12255,7 @@ async function runPull(args, io) {
|
|
|
11778
12255
|
io.err(`launchpad pull: ${e.message}`);
|
|
11779
12256
|
return 1;
|
|
11780
12257
|
}
|
|
11781
|
-
io.err(`launchpad pull failed: ${
|
|
12258
|
+
io.err(`launchpad pull failed: ${describe26(e)}`);
|
|
11782
12259
|
return 1;
|
|
11783
12260
|
}
|
|
11784
12261
|
}
|
|
@@ -11886,7 +12363,7 @@ function printUsage5(io) {
|
|
|
11886
12363
|
].join(`
|
|
11887
12364
|
`));
|
|
11888
12365
|
}
|
|
11889
|
-
function
|
|
12366
|
+
function describe26(e) {
|
|
11890
12367
|
return e instanceof Error ? e.message : String(e);
|
|
11891
12368
|
}
|
|
11892
12369
|
|
|
@@ -12221,7 +12698,7 @@ async function runReview(args, io) {
|
|
|
12221
12698
|
io.err(`launchpad review: ${e.message}`);
|
|
12222
12699
|
return 1;
|
|
12223
12700
|
}
|
|
12224
|
-
io.err(`launchpad review failed: ${
|
|
12701
|
+
io.err(`launchpad review failed: ${describe27(e)}`);
|
|
12225
12702
|
return 1;
|
|
12226
12703
|
}
|
|
12227
12704
|
}
|
|
@@ -12299,7 +12776,7 @@ function severityRank(s) {
|
|
|
12299
12776
|
return 3;
|
|
12300
12777
|
}
|
|
12301
12778
|
}
|
|
12302
|
-
function
|
|
12779
|
+
function describe27(e) {
|
|
12303
12780
|
return e instanceof Error ? e.message : String(e);
|
|
12304
12781
|
}
|
|
12305
12782
|
|
|
@@ -12359,7 +12836,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
12359
12836
|
cwd: process.cwd()
|
|
12360
12837
|
});
|
|
12361
12838
|
} catch (e) {
|
|
12362
|
-
io.err(`launchpad rollback: git rev-parse failed to start: ${
|
|
12839
|
+
io.err(`launchpad rollback: git rev-parse failed to start: ${describe28(e)}`);
|
|
12363
12840
|
return 2;
|
|
12364
12841
|
}
|
|
12365
12842
|
if (revParse.exitCode !== 0) {
|
|
@@ -12374,7 +12851,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
12374
12851
|
try {
|
|
12375
12852
|
show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
|
|
12376
12853
|
} catch (e) {
|
|
12377
|
-
io.err(`launchpad rollback: git show failed to start: ${
|
|
12854
|
+
io.err(`launchpad rollback: git show failed to start: ${describe28(e)}`);
|
|
12378
12855
|
return 2;
|
|
12379
12856
|
}
|
|
12380
12857
|
if (show.exitCode !== 0) {
|
|
@@ -12408,7 +12885,7 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
12408
12885
|
try {
|
|
12409
12886
|
writeFileSync5(manifestPath, historicalYaml, "utf8");
|
|
12410
12887
|
} catch (e) {
|
|
12411
|
-
io.err(`launchpad rollback: failed to write ${manifestPath}: ${
|
|
12888
|
+
io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe28(e)}`);
|
|
12412
12889
|
return 2;
|
|
12413
12890
|
}
|
|
12414
12891
|
io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
|
|
@@ -12490,7 +12967,7 @@ async function defaultPrompt4(question) {
|
|
|
12490
12967
|
rl.close();
|
|
12491
12968
|
}
|
|
12492
12969
|
}
|
|
12493
|
-
function
|
|
12970
|
+
function describe28(e) {
|
|
12494
12971
|
return e instanceof Error ? e.message : String(e);
|
|
12495
12972
|
}
|
|
12496
12973
|
function renderManifestError3(result, io) {
|
|
@@ -12595,7 +13072,7 @@ import { createInterface as createInterface4 } from "node:readline/promises";
|
|
|
12595
13072
|
import { stdin as input2, stdout as output2 } from "node:process";
|
|
12596
13073
|
|
|
12597
13074
|
// src/secrets/env-parse.ts
|
|
12598
|
-
var
|
|
13075
|
+
var ENV_NAME2 = /^[A-Z][A-Z0-9_]*$/;
|
|
12599
13076
|
var LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
|
|
12600
13077
|
function parseEnv(text) {
|
|
12601
13078
|
const entries = new Map;
|
|
@@ -12620,7 +13097,7 @@ function parseEnv(text) {
|
|
|
12620
13097
|
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
12621
13098
|
value = value.slice(1, -1);
|
|
12622
13099
|
}
|
|
12623
|
-
if (!
|
|
13100
|
+
if (!ENV_NAME2.test(key)) {
|
|
12624
13101
|
invalidNames.push(key);
|
|
12625
13102
|
continue;
|
|
12626
13103
|
}
|
|
@@ -12655,7 +13132,7 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
12655
13132
|
try {
|
|
12656
13133
|
envText = readFileSync16(envPath, "utf8");
|
|
12657
13134
|
} catch (e) {
|
|
12658
|
-
io.err(`launchpad secrets push: failed to read ${envPath}: ${
|
|
13135
|
+
io.err(`launchpad secrets push: failed to read ${envPath}: ${describe29(e)}`);
|
|
12659
13136
|
return 2;
|
|
12660
13137
|
}
|
|
12661
13138
|
const parsed = parseEnv(envText);
|
|
@@ -12839,10 +13316,10 @@ function mapPushError(e, slug, name, io) {
|
|
|
12839
13316
|
io.err(`✗ secrets push ${name}: ${e.message}`);
|
|
12840
13317
|
return 1;
|
|
12841
13318
|
}
|
|
12842
|
-
io.err(`✗ secrets push ${name} failed: ${
|
|
13319
|
+
io.err(`✗ secrets push ${name} failed: ${describe29(e)}`);
|
|
12843
13320
|
return 2;
|
|
12844
13321
|
}
|
|
12845
|
-
function
|
|
13322
|
+
function describe29(e) {
|
|
12846
13323
|
return e instanceof Error ? e.message : String(e);
|
|
12847
13324
|
}
|
|
12848
13325
|
function renderManifestError4(result, io) {
|
|
@@ -13232,7 +13709,7 @@ function setPushExit(e) {
|
|
|
13232
13709
|
|
|
13233
13710
|
// src/commands/secrets-template.ts
|
|
13234
13711
|
import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "node:fs";
|
|
13235
|
-
import { resolve as
|
|
13712
|
+
import { resolve as resolve14 } from "node:path";
|
|
13236
13713
|
async function runSecretsTemplate(args, io) {
|
|
13237
13714
|
const flags = parseFlags4(args);
|
|
13238
13715
|
if (flags.kind === "usage-error") {
|
|
@@ -13240,7 +13717,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
13240
13717
|
io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
|
|
13241
13718
|
return 64;
|
|
13242
13719
|
}
|
|
13243
|
-
const manifestPath =
|
|
13720
|
+
const manifestPath = resolve14(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
13244
13721
|
const result = loadManifest(manifestPath);
|
|
13245
13722
|
const renderResult = renderManifest(result, io);
|
|
13246
13723
|
if (renderResult.kind !== "ok") {
|
|
@@ -13254,7 +13731,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
13254
13731
|
}
|
|
13255
13732
|
return 0;
|
|
13256
13733
|
}
|
|
13257
|
-
const outPath =
|
|
13734
|
+
const outPath = resolve14(process.cwd(), flags.out);
|
|
13258
13735
|
if (existsSync14(outPath) && !flags.force) {
|
|
13259
13736
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
13260
13737
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
@@ -13551,16 +14028,16 @@ function printHelp3(io) {
|
|
|
13551
14028
|
|
|
13552
14029
|
// src/commands/skills.ts
|
|
13553
14030
|
import { fileURLToPath } from "node:url";
|
|
13554
|
-
import { dirname as
|
|
14031
|
+
import { dirname as dirname10, join as join16, resolve as resolve15 } from "node:path";
|
|
13555
14032
|
import { promises as fs7, existsSync as existsSync15 } from "node:fs";
|
|
13556
14033
|
|
|
13557
14034
|
// src/skills-bundle.ts
|
|
13558
14035
|
import { homedir as homedir2 } from "node:os";
|
|
13559
|
-
import { join as
|
|
14036
|
+
import { join as join15 } from "node:path";
|
|
13560
14037
|
import { readFileSync as readFileSync18, readdirSync as readdirSync2 } from "node:fs";
|
|
13561
14038
|
var SKILL_PREFIX = "launchpad-";
|
|
13562
14039
|
function skillsTargetDir() {
|
|
13563
|
-
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
14040
|
+
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir2(), ".claude", "skills");
|
|
13564
14041
|
}
|
|
13565
14042
|
function readInstalledSkills() {
|
|
13566
14043
|
try {
|
|
@@ -13570,7 +14047,7 @@ function readInstalledSkills() {
|
|
|
13570
14047
|
return { present: false, version: null };
|
|
13571
14048
|
return {
|
|
13572
14049
|
present: true,
|
|
13573
|
-
version: readSkillVersion(
|
|
14050
|
+
version: readSkillVersion(join15(dir, bundle.name, "SKILL.md"))
|
|
13574
14051
|
};
|
|
13575
14052
|
} catch {
|
|
13576
14053
|
return { present: false, version: null };
|
|
@@ -13632,7 +14109,7 @@ async function runSkills(args, io) {
|
|
|
13632
14109
|
return 64;
|
|
13633
14110
|
}
|
|
13634
14111
|
} catch (e) {
|
|
13635
|
-
io.err(`launchpad skills ${action}: ${
|
|
14112
|
+
io.err(`launchpad skills ${action}: ${describe30(e)}`);
|
|
13636
14113
|
return 1;
|
|
13637
14114
|
}
|
|
13638
14115
|
}
|
|
@@ -13655,13 +14132,13 @@ function resolveInstallEnv() {
|
|
|
13655
14132
|
return { bundleDir, userSkillsDir };
|
|
13656
14133
|
}
|
|
13657
14134
|
function defaultBundleDir() {
|
|
13658
|
-
const here =
|
|
14135
|
+
const here = dirname10(fileURLToPath(import.meta.url));
|
|
13659
14136
|
const candidates = [
|
|
13660
|
-
|
|
13661
|
-
|
|
14137
|
+
resolve15(here, "..", "skills"),
|
|
14138
|
+
resolve15(here, "..", "..", "skills")
|
|
13662
14139
|
];
|
|
13663
14140
|
for (const c of candidates) {
|
|
13664
|
-
if (existsSync15(
|
|
14141
|
+
if (existsSync15(join16(c, "launchpad-onboard", "SKILL.md"))) {
|
|
13665
14142
|
return c;
|
|
13666
14143
|
}
|
|
13667
14144
|
}
|
|
@@ -13682,12 +14159,12 @@ async function doInstall(io) {
|
|
|
13682
14159
|
}
|
|
13683
14160
|
let installed = 0;
|
|
13684
14161
|
for (const skill of BUNDLED_SKILLS) {
|
|
13685
|
-
const src =
|
|
14162
|
+
const src = join16(env.bundleDir, skill);
|
|
13686
14163
|
if (!await isDir(src)) {
|
|
13687
14164
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
13688
14165
|
return 1;
|
|
13689
14166
|
}
|
|
13690
|
-
const dest =
|
|
14167
|
+
const dest = join16(env.userSkillsDir, skill);
|
|
13691
14168
|
await fs7.rm(dest, { recursive: true, force: true });
|
|
13692
14169
|
await fs7.cp(src, dest, { recursive: true });
|
|
13693
14170
|
installed++;
|
|
@@ -13712,7 +14189,7 @@ async function doUninstall(io) {
|
|
|
13712
14189
|
continue;
|
|
13713
14190
|
if (!isBundleManaged(entry.name))
|
|
13714
14191
|
continue;
|
|
13715
|
-
const target =
|
|
14192
|
+
const target = join16(env.userSkillsDir, entry.name);
|
|
13716
14193
|
await fs7.rm(target, { recursive: true, force: true });
|
|
13717
14194
|
removed++;
|
|
13718
14195
|
io.out(`✗ removed ${target}`);
|
|
@@ -13735,7 +14212,7 @@ async function doList(io) {
|
|
|
13735
14212
|
}
|
|
13736
14213
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
13737
14214
|
for (const name of managedDirs) {
|
|
13738
|
-
const skillFile =
|
|
14215
|
+
const skillFile = join16(env.userSkillsDir, name, "SKILL.md");
|
|
13739
14216
|
const version = await readVersion(skillFile);
|
|
13740
14217
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
13741
14218
|
}
|
|
@@ -13762,7 +14239,7 @@ async function isDir(path14) {
|
|
|
13762
14239
|
return false;
|
|
13763
14240
|
}
|
|
13764
14241
|
}
|
|
13765
|
-
function
|
|
14242
|
+
function describe30(e) {
|
|
13766
14243
|
return e instanceof Error ? e.message : String(e);
|
|
13767
14244
|
}
|
|
13768
14245
|
|
|
@@ -13770,7 +14247,7 @@ function describe29(e) {
|
|
|
13770
14247
|
import { execFile as execFile2, spawn as spawn5 } from "node:child_process";
|
|
13771
14248
|
import { promisify } from "node:util";
|
|
13772
14249
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
13773
|
-
import { dirname as
|
|
14250
|
+
import { dirname as dirname11, resolve as resolve16, relative as relative6, isAbsolute as isAbsolute3, join as join17 } from "node:path";
|
|
13774
14251
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
13775
14252
|
import { readFileSync as readFileSync19, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
13776
14253
|
|
|
@@ -13815,9 +14292,9 @@ async function startLoopback(state, timeoutMs) {
|
|
|
13815
14292
|
resolveCode(code);
|
|
13816
14293
|
}
|
|
13817
14294
|
});
|
|
13818
|
-
const bound = await new Promise((
|
|
13819
|
-
server.once("error", () =>
|
|
13820
|
-
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));
|
|
13821
14298
|
});
|
|
13822
14299
|
if (!bound)
|
|
13823
14300
|
return null;
|
|
@@ -13893,7 +14370,7 @@ var PKG = "@m-kopa/launchpad-cli";
|
|
|
13893
14370
|
var REGISTRY = "https://registry.npmjs.org";
|
|
13894
14371
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
13895
14372
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
13896
|
-
var CHANNEL_MARKER =
|
|
14373
|
+
var CHANNEL_MARKER = join17(homedir3(), ".launchpad", "channel");
|
|
13897
14374
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
13898
14375
|
var UPGRADE_ARGS = {
|
|
13899
14376
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -14022,8 +14499,8 @@ async function openSystemBrowser(url) {
|
|
|
14022
14499
|
await execFileAsync(opener, [url]);
|
|
14023
14500
|
}
|
|
14024
14501
|
async function runInstallerScript(script) {
|
|
14025
|
-
const dir = mkdtempSync(
|
|
14026
|
-
const file =
|
|
14502
|
+
const dir = mkdtempSync(join17(tmpdir(), "launchpad-update-"));
|
|
14503
|
+
const file = join17(dir, "install.sh");
|
|
14027
14504
|
try {
|
|
14028
14505
|
writeFileSync7(file, script, { mode: 448 });
|
|
14029
14506
|
return await new Promise((resolvePromise) => {
|
|
@@ -14110,7 +14587,7 @@ function errStderr(e) {
|
|
|
14110
14587
|
return "";
|
|
14111
14588
|
}
|
|
14112
14589
|
async function detectPackageManager2() {
|
|
14113
|
-
const pkgRoot =
|
|
14590
|
+
const pkgRoot = resolve16(dirname11(fileURLToPath2(import.meta.url)), "..", "..");
|
|
14114
14591
|
const candidates = [];
|
|
14115
14592
|
const npmRoot = await pmRoot("npm");
|
|
14116
14593
|
if (npmRoot !== null)
|
|
@@ -14118,7 +14595,7 @@ async function detectPackageManager2() {
|
|
|
14118
14595
|
const pnpmRoot = await pmRoot("pnpm");
|
|
14119
14596
|
if (pnpmRoot !== null)
|
|
14120
14597
|
candidates.push(["pnpm", pnpmRoot]);
|
|
14121
|
-
candidates.push(["bun",
|
|
14598
|
+
candidates.push(["bun", resolve16(homedir3(), ".bun/install/global/node_modules")]);
|
|
14122
14599
|
const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
|
|
14123
14600
|
return matches.length === 1 ? matches[0][0] : null;
|
|
14124
14601
|
}
|
|
@@ -14132,7 +14609,7 @@ async function pmRoot(pm) {
|
|
|
14132
14609
|
}
|
|
14133
14610
|
}
|
|
14134
14611
|
function pathContains(root, child) {
|
|
14135
|
-
const rel =
|
|
14612
|
+
const rel = relative6(root, child);
|
|
14136
14613
|
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
14137
14614
|
}
|
|
14138
14615
|
function runUpgrade(pm) {
|
|
@@ -14256,7 +14733,7 @@ function printHelp5(io) {
|
|
|
14256
14733
|
|
|
14257
14734
|
// src/commands/validate.ts
|
|
14258
14735
|
import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
14259
|
-
import { dirname as
|
|
14736
|
+
import { dirname as dirname12, resolve as resolve17 } from "node:path";
|
|
14260
14737
|
var validateCommand = {
|
|
14261
14738
|
name: "validate",
|
|
14262
14739
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -14270,11 +14747,21 @@ async function runValidate(args, io) {
|
|
|
14270
14747
|
return 64;
|
|
14271
14748
|
}
|
|
14272
14749
|
const { file, json, strictGroups } = parseResult;
|
|
14273
|
-
const path14 =
|
|
14750
|
+
const path14 = resolve17(process.cwd(), file ?? "launchpad.yaml");
|
|
14274
14751
|
const result = loadManifest(path14);
|
|
14275
14752
|
if (result.kind !== "ok") {
|
|
14276
14753
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
14277
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;
|
|
14278
14765
|
const boundary = checkBoundary(path14, result.manifest.app !== undefined);
|
|
14279
14766
|
const serveErrors = checkStaticServeDir(path14, result.manifest);
|
|
14280
14767
|
if (serveErrors.length > 0) {
|
|
@@ -14284,7 +14771,7 @@ async function runValidate(args, io) {
|
|
|
14284
14771
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
14285
14772
|
}
|
|
14286
14773
|
function checkBoundary(manifestPath, declared) {
|
|
14287
|
-
const dir =
|
|
14774
|
+
const dir = dirname12(manifestPath);
|
|
14288
14775
|
let files;
|
|
14289
14776
|
try {
|
|
14290
14777
|
files = walkCwd(dir).files;
|
|
@@ -14316,8 +14803,8 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
14316
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.`
|
|
14317
14804
|
];
|
|
14318
14805
|
}
|
|
14319
|
-
const dir =
|
|
14320
|
-
const abs =
|
|
14806
|
+
const dir = dirname12(manifestPath);
|
|
14807
|
+
const abs = resolve17(dir, serveDir);
|
|
14321
14808
|
if (!existsSync16(abs)) {
|
|
14322
14809
|
return [
|
|
14323
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).`
|
|
@@ -14327,7 +14814,7 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
14327
14814
|
try {
|
|
14328
14815
|
isDir2 = statSync2(abs).isDirectory();
|
|
14329
14816
|
} catch (e) {
|
|
14330
|
-
return [`failed to inspect static served directory '${serveDir}/': ${
|
|
14817
|
+
return [`failed to inspect static served directory '${serveDir}/': ${describe31(e)}`];
|
|
14331
14818
|
}
|
|
14332
14819
|
if (!isDir2) {
|
|
14333
14820
|
return [`static app serves '${serveDir}/' but that path is not a directory.`];
|
|
@@ -14336,14 +14823,14 @@ function checkStaticServeDir(manifestPath, manifest) {
|
|
|
14336
14823
|
try {
|
|
14337
14824
|
entries = readdirSync3(abs);
|
|
14338
14825
|
} catch (e) {
|
|
14339
|
-
return [`failed to read static served directory '${serveDir}/': ${
|
|
14826
|
+
return [`failed to read static served directory '${serveDir}/': ${describe31(e)}`];
|
|
14340
14827
|
}
|
|
14341
14828
|
if (entries.length === 0) {
|
|
14342
14829
|
return [
|
|
14343
14830
|
`static app's served directory '${serveDir}/' is empty — the deploy would serve an empty site.`
|
|
14344
14831
|
];
|
|
14345
14832
|
}
|
|
14346
|
-
if (!existsSync16(
|
|
14833
|
+
if (!existsSync16(resolve17(abs, "index.html"))) {
|
|
14347
14834
|
return [
|
|
14348
14835
|
`static app's served directory '${serveDir}/' has no index.html — the deploy would serve a directory with no entrypoint.`
|
|
14349
14836
|
];
|
|
@@ -14391,7 +14878,7 @@ async function checkGroup(allowedGroup, cfg) {
|
|
|
14391
14878
|
if (e instanceof ForbiddenError) {
|
|
14392
14879
|
return { kind: "forbidden", message: e.message };
|
|
14393
14880
|
}
|
|
14394
|
-
return { kind: "network", message:
|
|
14881
|
+
return { kind: "network", message: describe31(e) };
|
|
14395
14882
|
}
|
|
14396
14883
|
}
|
|
14397
14884
|
async function checkGroups(groups) {
|
|
@@ -14488,6 +14975,8 @@ function renderHumanError(result, io) {
|
|
|
14488
14975
|
}
|
|
14489
14976
|
function renderJsonOk(result, group, boundary, io) {
|
|
14490
14977
|
const base = {
|
|
14978
|
+
manifest_valid: true,
|
|
14979
|
+
runtime_supported: true,
|
|
14491
14980
|
name: result.manifest.metadata.name,
|
|
14492
14981
|
deploymentType: result.manifest.deployment.type,
|
|
14493
14982
|
appBoundary: {
|
|
@@ -14604,7 +15093,7 @@ function parseArgs13(args) {
|
|
|
14604
15093
|
}
|
|
14605
15094
|
return { kind: "ok", file, json, strictGroups };
|
|
14606
15095
|
}
|
|
14607
|
-
function
|
|
15096
|
+
function describe31(e) {
|
|
14608
15097
|
return e instanceof Error ? e.message : String(e);
|
|
14609
15098
|
}
|
|
14610
15099
|
|
|
@@ -14657,7 +15146,7 @@ async function runWhoami(args, io) {
|
|
|
14657
15146
|
io.err(e.message);
|
|
14658
15147
|
return 1;
|
|
14659
15148
|
}
|
|
14660
|
-
io.err(`launchpad whoami failed: ${
|
|
15149
|
+
io.err(`launchpad whoami failed: ${describe32(e)}`);
|
|
14661
15150
|
return 1;
|
|
14662
15151
|
}
|
|
14663
15152
|
}
|
|
@@ -14687,11 +15176,11 @@ async function runShare(args, io) {
|
|
|
14687
15176
|
io.err(e.message);
|
|
14688
15177
|
return 1;
|
|
14689
15178
|
}
|
|
14690
|
-
io.err(`launchpad whoami --share failed: ${
|
|
15179
|
+
io.err(`launchpad whoami --share failed: ${describe32(e)}`);
|
|
14691
15180
|
return 1;
|
|
14692
15181
|
}
|
|
14693
15182
|
}
|
|
14694
|
-
function
|
|
15183
|
+
function describe32(e) {
|
|
14695
15184
|
return e instanceof Error ? e.message : String(e);
|
|
14696
15185
|
}
|
|
14697
15186
|
|
|
@@ -14857,7 +15346,7 @@ async function runEditor(action, args, io, deps) {
|
|
|
14857
15346
|
io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
|
|
14858
15347
|
return 1;
|
|
14859
15348
|
}
|
|
14860
|
-
io.err(`launchpad ${verb} failed: ${
|
|
15349
|
+
io.err(`launchpad ${verb} failed: ${describe33(e)}`);
|
|
14861
15350
|
return 1;
|
|
14862
15351
|
}
|
|
14863
15352
|
}
|
|
@@ -14891,7 +15380,7 @@ async function sendMutation(action, slug, target, version, cfg, deps) {
|
|
|
14891
15380
|
nonThrowingStatuses: [409]
|
|
14892
15381
|
});
|
|
14893
15382
|
}
|
|
14894
|
-
function
|
|
15383
|
+
function describe33(e) {
|
|
14895
15384
|
return e instanceof Error ? e.message : String(e);
|
|
14896
15385
|
}
|
|
14897
15386
|
var grantEditorCommand = makeEditorCommand("grant");
|
|
@@ -15097,7 +15586,7 @@ async function runOwner(action, args, io, deps) {
|
|
|
15097
15586
|
io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
|
|
15098
15587
|
return 1;
|
|
15099
15588
|
}
|
|
15100
|
-
io.err(`launchpad ${verb} failed: ${
|
|
15589
|
+
io.err(`launchpad ${verb} failed: ${describe34(e)}`);
|
|
15101
15590
|
return 1;
|
|
15102
15591
|
}
|
|
15103
15592
|
}
|
|
@@ -15131,12 +15620,92 @@ async function sendMutation2(action, slug, target, version, cfg, deps) {
|
|
|
15131
15620
|
nonThrowingStatuses: [409]
|
|
15132
15621
|
});
|
|
15133
15622
|
}
|
|
15134
|
-
function
|
|
15623
|
+
function describe34(e) {
|
|
15135
15624
|
return e instanceof Error ? e.message : String(e);
|
|
15136
15625
|
}
|
|
15137
15626
|
var addOwnerCommand = makeOwnerCommand("add");
|
|
15138
15627
|
var removeOwnerCommand = makeOwnerCommand("remove");
|
|
15139
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
|
+
|
|
15140
15709
|
// src/commands/list-owners.ts
|
|
15141
15710
|
function realDeps4() {
|
|
15142
15711
|
return { loadConfig, apiJson };
|
|
@@ -15212,7 +15781,7 @@ var listOwnersCommand = makeListOwnersCommand();
|
|
|
15212
15781
|
// src/update-notifier.ts
|
|
15213
15782
|
import { spawn as spawn6 } from "node:child_process";
|
|
15214
15783
|
import { homedir as homedir4 } from "node:os";
|
|
15215
|
-
import { join as
|
|
15784
|
+
import { join as join18 } from "node:path";
|
|
15216
15785
|
import {
|
|
15217
15786
|
existsSync as existsSync17,
|
|
15218
15787
|
mkdirSync as mkdirSync4,
|
|
@@ -15222,7 +15791,7 @@ import {
|
|
|
15222
15791
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
15223
15792
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
15224
15793
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
15225
|
-
var CACHE_FILE =
|
|
15794
|
+
var CACHE_FILE = join18(homedir4(), ".launchpad", "update-check.json");
|
|
15226
15795
|
function readCache2() {
|
|
15227
15796
|
try {
|
|
15228
15797
|
const raw = JSON.parse(readFileSync21(CACHE_FILE, "utf8"));
|
|
@@ -15238,12 +15807,12 @@ function readCache2() {
|
|
|
15238
15807
|
}
|
|
15239
15808
|
function writeCache2(state) {
|
|
15240
15809
|
try {
|
|
15241
|
-
mkdirSync4(
|
|
15810
|
+
mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
|
|
15242
15811
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
15243
15812
|
`, { mode: 384 });
|
|
15244
15813
|
} catch {}
|
|
15245
15814
|
}
|
|
15246
|
-
var SKILLS_HINT_MARKER =
|
|
15815
|
+
var SKILLS_HINT_MARKER = join18(homedir4(), ".launchpad", "skills-hint-shown");
|
|
15247
15816
|
function absentHintShown() {
|
|
15248
15817
|
try {
|
|
15249
15818
|
return existsSync17(SKILLS_HINT_MARKER);
|
|
@@ -15253,7 +15822,7 @@ function absentHintShown() {
|
|
|
15253
15822
|
}
|
|
15254
15823
|
function markAbsentHintShown() {
|
|
15255
15824
|
try {
|
|
15256
|
-
mkdirSync4(
|
|
15825
|
+
mkdirSync4(join18(homedir4(), ".launchpad"), { recursive: true });
|
|
15257
15826
|
writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
|
|
15258
15827
|
`, { mode: 384 });
|
|
15259
15828
|
} catch {}
|
|
@@ -15356,7 +15925,7 @@ var refreshUpdateCacheCommand = {
|
|
|
15356
15925
|
// src/telemetry.ts
|
|
15357
15926
|
import { spawn as spawn7 } from "node:child_process";
|
|
15358
15927
|
import { homedir as homedir5 } from "node:os";
|
|
15359
|
-
import { join as
|
|
15928
|
+
import { join as join19 } from "node:path";
|
|
15360
15929
|
import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync9 } from "node:fs";
|
|
15361
15930
|
import { randomUUID } from "node:crypto";
|
|
15362
15931
|
var INTERNAL_EMIT_VERB = "__emit-telemetry";
|
|
@@ -15365,8 +15934,8 @@ var POSTHOG_PROJECT_KEY = "phc_CYaCuETanWc36TMTiB7cdkdnmPPhFZCkDGmWGLaNkXnb";
|
|
|
15365
15934
|
var CAPTURE_PATH = "/i/v0/e/";
|
|
15366
15935
|
var SEND_TIMEOUT_MS = 3000;
|
|
15367
15936
|
var APP = "launchpad-cli";
|
|
15368
|
-
var DEVICE_ID_FILE =
|
|
15369
|
-
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");
|
|
15370
15939
|
function buildEvent(ctx, identity, nowMs) {
|
|
15371
15940
|
const properties = {
|
|
15372
15941
|
app: APP,
|
|
@@ -15440,7 +16009,7 @@ function getOrCreateDeviceId() {
|
|
|
15440
16009
|
} catch {}
|
|
15441
16010
|
const id = randomUUID();
|
|
15442
16011
|
try {
|
|
15443
|
-
mkdirSync5(
|
|
16012
|
+
mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
|
|
15444
16013
|
writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
|
|
15445
16014
|
`, {
|
|
15446
16015
|
mode: 384
|
|
@@ -15518,7 +16087,7 @@ function firstRunNoticeShown() {
|
|
|
15518
16087
|
}
|
|
15519
16088
|
function markFirstRunNotice() {
|
|
15520
16089
|
try {
|
|
15521
|
-
mkdirSync5(
|
|
16090
|
+
mkdirSync5(join19(homedir5(), ".launchpad"), { recursive: true });
|
|
15522
16091
|
writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
|
|
15523
16092
|
`, { mode: 384 });
|
|
15524
16093
|
} catch {}
|
|
@@ -15586,14 +16155,14 @@ import { platform } from "node:os";
|
|
|
15586
16155
|
// src/report/breadcrumb.ts
|
|
15587
16156
|
import { writeFileSync as writeFileSync10, readFileSync as readFileSync23, mkdirSync as mkdirSync6 } from "node:fs";
|
|
15588
16157
|
import { homedir as homedir6 } from "node:os";
|
|
15589
|
-
import { join as
|
|
16158
|
+
import { join as join20, dirname as dirname13 } from "node:path";
|
|
15590
16159
|
function breadcrumbPath() {
|
|
15591
|
-
return
|
|
16160
|
+
return join20(homedir6(), ".launchpad", "last-run.json");
|
|
15592
16161
|
}
|
|
15593
16162
|
function writeBreadcrumb(verb, exit) {
|
|
15594
16163
|
try {
|
|
15595
16164
|
const p = breadcrumbPath();
|
|
15596
|
-
mkdirSync6(
|
|
16165
|
+
mkdirSync6(dirname13(p), { recursive: true });
|
|
15597
16166
|
writeFileSync10(p, JSON.stringify({ verb, exit, ts: Date.now() }), "utf8");
|
|
15598
16167
|
} catch {}
|
|
15599
16168
|
}
|
|
@@ -15793,6 +16362,7 @@ var COMMANDS = [
|
|
|
15793
16362
|
addOwnerCommand,
|
|
15794
16363
|
removeOwnerCommand,
|
|
15795
16364
|
listOwnersCommand,
|
|
16365
|
+
adminCommand,
|
|
15796
16366
|
makeReportCommand("bug"),
|
|
15797
16367
|
makeReportCommand("feature"),
|
|
15798
16368
|
refreshUpdateCacheCommand,
|
|
@@ -15800,7 +16370,7 @@ var COMMANDS = [
|
|
|
15800
16370
|
];
|
|
15801
16371
|
async function dispatch(argv, io, commands = COMMANDS, deps = {}) {
|
|
15802
16372
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
15803
|
-
|
|
16373
|
+
printHelp7(io, commands);
|
|
15804
16374
|
return 0;
|
|
15805
16375
|
}
|
|
15806
16376
|
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
@@ -15863,9 +16433,11 @@ function requiresCliFloorPreflight(verb, args) {
|
|
|
15863
16433
|
return args[0] === "set" || args[0] === "rm";
|
|
15864
16434
|
if (verb === "secrets")
|
|
15865
16435
|
return args[0] === "push";
|
|
16436
|
+
if (verb === "admin")
|
|
16437
|
+
return args[0] === "status-revoke";
|
|
15866
16438
|
return false;
|
|
15867
16439
|
}
|
|
15868
|
-
function
|
|
16440
|
+
function printHelp7(io, commands) {
|
|
15869
16441
|
io.out("launchpad — manage Launchpad-deployed apps from the command line.");
|
|
15870
16442
|
io.out("");
|
|
15871
16443
|
io.out("Usage:");
|