@m-kopa/launchpad-cli 0.24.0 → 0.26.1
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 +92 -0
- package/dist/bundle/upload.d.ts +34 -2
- package/dist/bundle/upload.d.ts.map +1 -1
- package/dist/cli.js +585 -266
- package/dist/commands/deploy-flags.d.ts +6 -0
- package/dist/commands/deploy-flags.d.ts.map +1 -1
- package/dist/commands/deploy.d.ts +10 -13
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/destroy.d.ts +1 -1
- package/dist/commands/destroy.d.ts.map +1 -1
- package/dist/commands/envvars.d.ts +2 -2
- package/dist/commands/envvars.d.ts.map +1 -1
- package/dist/commands/infer-slug.d.ts +37 -0
- package/dist/commands/infer-slug.d.ts.map +1 -0
- package/dist/commands/logs.d.ts +2 -2
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/merge.d.ts +2 -2
- package/dist/commands/merge.d.ts.map +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.d.ts.map +1 -1
- package/dist/commands/recover.d.ts +12 -0
- package/dist/commands/recover.d.ts.map +1 -0
- package/dist/commands/review.d.ts +2 -2
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/status.d.ts +18 -4
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/deploy/apply.d.ts +5 -2
- package/dist/deploy/apply.d.ts.map +1 -1
- package/dist/deploy/deployment-status.d.ts +18 -0
- package/dist/deploy/deployment-status.d.ts.map +1 -1
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/dispatcher.d.ts.map +1 -1
- 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 +6 -4
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +13 -6
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ 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.26.1";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -1219,8 +1219,8 @@ function describe9(e) {
|
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
1221
|
// src/commands/deploy.ts
|
|
1222
|
-
import { existsSync as
|
|
1223
|
-
import * as
|
|
1222
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1223
|
+
import * as path7 from "node:path";
|
|
1224
1224
|
|
|
1225
1225
|
// src/bundle/orchestrate.ts
|
|
1226
1226
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
@@ -1432,6 +1432,9 @@ async function uploadBundle(cfg, slug, manifestYaml, bundleTarGz, workerArtifact
|
|
|
1432
1432
|
if (res.status === 202) {
|
|
1433
1433
|
return { kind: "ok", response: body };
|
|
1434
1434
|
}
|
|
1435
|
+
if (res.status === 200 && typeof body === "object" && body !== null && body.outcome === "nothing-to-deploy") {
|
|
1436
|
+
return { kind: "ok", response: body };
|
|
1437
|
+
}
|
|
1435
1438
|
return {
|
|
1436
1439
|
kind: "error",
|
|
1437
1440
|
status: res.status,
|
|
@@ -3303,8 +3306,8 @@ async function bundleAndDeploy(args) {
|
|
|
3303
3306
|
}
|
|
3304
3307
|
|
|
3305
3308
|
// src/commands/deploy.ts
|
|
3306
|
-
import { parse as
|
|
3307
|
-
import { readFileSync as
|
|
3309
|
+
import { parse as parseYaml5 } from "yaml";
|
|
3310
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
3308
3311
|
|
|
3309
3312
|
// src/deploy/git-files.ts
|
|
3310
3313
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -3380,6 +3383,7 @@ function parseDeployFlags(args) {
|
|
|
3380
3383
|
let yes = false;
|
|
3381
3384
|
let resumePrNumber = null;
|
|
3382
3385
|
let timeoutMinutes = null;
|
|
3386
|
+
let atSha = null;
|
|
3383
3387
|
let modeFlagsSeen = 0;
|
|
3384
3388
|
let i = 0;
|
|
3385
3389
|
while (i < args.length) {
|
|
@@ -3532,6 +3536,17 @@ function parseDeployFlags(args) {
|
|
|
3532
3536
|
i += 2;
|
|
3533
3537
|
continue;
|
|
3534
3538
|
}
|
|
3539
|
+
if (a === "--at") {
|
|
3540
|
+
const v = args[i + 1];
|
|
3541
|
+
if (v === undefined)
|
|
3542
|
+
return `missing value for ${a}`;
|
|
3543
|
+
if (!/^[0-9a-f]{40}$/.test(v)) {
|
|
3544
|
+
return `invalid --at "${v}" — expected a 40-char lowercase hex git SHA from the MANAGED repo`;
|
|
3545
|
+
}
|
|
3546
|
+
atSha = v;
|
|
3547
|
+
i += 2;
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3535
3550
|
if (a === "--resume-pr") {
|
|
3536
3551
|
const v = args[i + 1];
|
|
3537
3552
|
if (v === undefined)
|
|
@@ -3601,6 +3616,7 @@ function parseDeployFlags(args) {
|
|
|
3601
3616
|
rePin,
|
|
3602
3617
|
yes,
|
|
3603
3618
|
resumePrNumber,
|
|
3619
|
+
atSha,
|
|
3604
3620
|
timeoutMinutes
|
|
3605
3621
|
},
|
|
3606
3622
|
message: null,
|
|
@@ -3610,6 +3626,9 @@ function parseDeployFlags(args) {
|
|
|
3610
3626
|
if (resumePrNumber !== null) {
|
|
3611
3627
|
return "--resume-pr is only valid with --apply";
|
|
3612
3628
|
}
|
|
3629
|
+
if (atSha !== null) {
|
|
3630
|
+
return "--at is only valid with --apply";
|
|
3631
|
+
}
|
|
3613
3632
|
if (timeoutMinutes !== null) {
|
|
3614
3633
|
return "--timeout-minutes is only valid with --apply";
|
|
3615
3634
|
}
|
|
@@ -3687,7 +3706,6 @@ function parseDeployFlags(args) {
|
|
|
3687
3706
|
// src/deploy/apply.ts
|
|
3688
3707
|
import { existsSync as existsSync3, rmSync } from "node:fs";
|
|
3689
3708
|
import { resolve as resolvePath, join as join7 } from "node:path";
|
|
3690
|
-
import { execFileSync } from "node:child_process";
|
|
3691
3709
|
|
|
3692
3710
|
// src/manifest/load.ts
|
|
3693
3711
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
@@ -3780,29 +3798,23 @@ async function runDeployApply(opts, io, deps = {}) {
|
|
|
3780
3798
|
io.err(`launchpad deploy --apply: manifest's metadata.name '${slug}' is not a valid slug`);
|
|
3781
3799
|
return 1;
|
|
3782
3800
|
}
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
} catch (e) {
|
|
3787
|
-
io.err(`launchpad deploy --apply: failed to resolve git HEAD in ${process.cwd()}: ${describe10(e)}`);
|
|
3788
|
-
io.err(" Apply needs a clean commit so portal-bot can fetch launchpad.yaml at that sha.");
|
|
3789
|
-
return 2;
|
|
3790
|
-
}
|
|
3791
|
-
if (!MANIFEST_SHA_REGEX.test(manifestSha)) {
|
|
3792
|
-
io.err(`launchpad deploy --apply: git HEAD returned an unexpected sha shape: '${manifestSha}'`);
|
|
3793
|
-
return 2;
|
|
3801
|
+
if (opts.atSha !== undefined && !MANIFEST_SHA_REGEX.test(opts.atSha)) {
|
|
3802
|
+
io.err(`launchpad deploy --apply: --at expects a 40-char lowercase hex git SHA, got '${opts.atSha}'`);
|
|
3803
|
+
return 64;
|
|
3794
3804
|
}
|
|
3795
|
-
io.out(`Planning apply for ${slug} @ ${
|
|
3805
|
+
io.out(opts.atSha !== undefined ? `Planning apply for ${slug} @ ${opts.atSha.slice(0, 7)} (managed repo) …` : `Planning apply for ${slug} (bot resolves managed main HEAD) …`);
|
|
3796
3806
|
let plan;
|
|
3797
3807
|
try {
|
|
3798
3808
|
plan = await apiJson(cfg, {
|
|
3799
3809
|
method: "POST",
|
|
3800
3810
|
path: `/apps/${slug}/manifest/plan`,
|
|
3801
|
-
jsonBody: { manifestSha }
|
|
3811
|
+
jsonBody: opts.atSha !== undefined ? { manifestSha: opts.atSha } : {}
|
|
3802
3812
|
}, fetcher);
|
|
3803
3813
|
} catch (e) {
|
|
3804
3814
|
return mapHttpError(e, slug, io);
|
|
3805
3815
|
}
|
|
3816
|
+
const manifestSha = plan.manifestSha;
|
|
3817
|
+
io.out(`Manifest pinned @ ${manifestSha.slice(0, 7)} (managed repo provenance).`);
|
|
3806
3818
|
for (const w of plan.warnings)
|
|
3807
3819
|
io.err(`! ${w}`);
|
|
3808
3820
|
io.out("");
|
|
@@ -3922,14 +3934,6 @@ function sleep(ms) {
|
|
|
3922
3934
|
return Promise.resolve();
|
|
3923
3935
|
return new Promise((res) => setTimeout(res, ms));
|
|
3924
3936
|
}
|
|
3925
|
-
function defaultGitHeadSha(cwd) {
|
|
3926
|
-
const out = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
3927
|
-
cwd,
|
|
3928
|
-
encoding: "utf8",
|
|
3929
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3930
|
-
});
|
|
3931
|
-
return out.trim();
|
|
3932
|
-
}
|
|
3933
3937
|
function deletePinIfPresent(cfg, slug, io) {
|
|
3934
3938
|
const pinPath = join7(cfg.stateDir, slug, "group.json");
|
|
3935
3939
|
if (!existsSync3(pinPath))
|
|
@@ -4012,7 +4016,7 @@ function renderManifestError(loaded, io) {
|
|
|
4012
4016
|
}
|
|
4013
4017
|
|
|
4014
4018
|
// src/deploy/dry-run.ts
|
|
4015
|
-
import { execFileSync
|
|
4019
|
+
import { execFileSync } from "node:child_process";
|
|
4016
4020
|
import { resolve as resolve5 } from "node:path";
|
|
4017
4021
|
var MANIFEST_SHA_REGEX2 = /^[0-9a-f]{40}$/;
|
|
4018
4022
|
async function runDeployDryRun(opts, io, deps = {}) {
|
|
@@ -4026,7 +4030,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
|
|
|
4026
4030
|
const slug = loaded.manifest.metadata.name;
|
|
4027
4031
|
let manifestSha;
|
|
4028
4032
|
try {
|
|
4029
|
-
manifestSha = (deps.gitHeadSha ??
|
|
4033
|
+
manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
|
|
4030
4034
|
} catch (e) {
|
|
4031
4035
|
const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe11(e)}`;
|
|
4032
4036
|
if (opts.json) {
|
|
@@ -4069,8 +4073,8 @@ async function runDeployDryRun(opts, io, deps = {}) {
|
|
|
4069
4073
|
}
|
|
4070
4074
|
return 0;
|
|
4071
4075
|
}
|
|
4072
|
-
function
|
|
4073
|
-
const out =
|
|
4076
|
+
function defaultGitHeadSha(cwd) {
|
|
4077
|
+
const out = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
4074
4078
|
cwd,
|
|
4075
4079
|
encoding: "utf8",
|
|
4076
4080
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4363,6 +4367,46 @@ function handleNetworkError(e, io, slug, verb) {
|
|
|
4363
4367
|
return 1;
|
|
4364
4368
|
}
|
|
4365
4369
|
|
|
4370
|
+
// src/commands/infer-slug.ts
|
|
4371
|
+
import * as path6 from "node:path";
|
|
4372
|
+
import { existsSync as existsSync4, readFileSync as readFileSync6 } from "node:fs";
|
|
4373
|
+
import { parse as parseYaml4 } from "yaml";
|
|
4374
|
+
var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4375
|
+
function inferSlugFromCwd(cwd) {
|
|
4376
|
+
const base = path6.basename(cwd);
|
|
4377
|
+
const m = base.match(DIRNAME_RE);
|
|
4378
|
+
return m === null ? null : m[1];
|
|
4379
|
+
}
|
|
4380
|
+
function resolveManifestSlug(parsed) {
|
|
4381
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.metadata !== "object" || parsed.metadata === null) {
|
|
4382
|
+
return null;
|
|
4383
|
+
}
|
|
4384
|
+
const meta = parsed.metadata;
|
|
4385
|
+
if (typeof meta.slug === "string")
|
|
4386
|
+
return meta.slug;
|
|
4387
|
+
if (typeof meta.name === "string")
|
|
4388
|
+
return meta.name;
|
|
4389
|
+
return null;
|
|
4390
|
+
}
|
|
4391
|
+
function inferSlugFromManifestFile(manifestPath) {
|
|
4392
|
+
if (!existsSync4(manifestPath))
|
|
4393
|
+
return null;
|
|
4394
|
+
try {
|
|
4395
|
+
return resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
|
|
4396
|
+
} catch {
|
|
4397
|
+
return null;
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
function inferSlug(opts) {
|
|
4401
|
+
const manifestPath = path6.resolve(opts.cwd, opts.file ?? "launchpad.yaml");
|
|
4402
|
+
const fromManifest = inferSlugFromManifestFile(manifestPath);
|
|
4403
|
+
const fromDir = inferSlugFromCwd(opts.cwd);
|
|
4404
|
+
if (fromManifest !== null && fromDir !== null && fromManifest !== fromDir) {
|
|
4405
|
+
opts.warn?.(`note: directory name suggests "${fromDir}" but ${path6.basename(manifestPath)} ` + `declares "${fromManifest}" — using the manifest. ` + `(Pass <slug> or --slug to override.)`);
|
|
4406
|
+
}
|
|
4407
|
+
return fromManifest ?? fromDir;
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4366
4410
|
// src/commands/deploy.ts
|
|
4367
4411
|
var deployCommand = {
|
|
4368
4412
|
name: "deploy",
|
|
@@ -4370,27 +4414,17 @@ var deployCommand = {
|
|
|
4370
4414
|
run: runDeploy
|
|
4371
4415
|
};
|
|
4372
4416
|
var SLUG_RE4 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
4373
|
-
var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4374
4417
|
async function runDeploy(args, io) {
|
|
4375
4418
|
const flags = parseDeployFlags(args);
|
|
4376
4419
|
if (typeof flags === "string") {
|
|
4377
4420
|
io.err(`launchpad deploy: ${flags}`);
|
|
4421
|
+
const isFlagMisuse = flags.includes("is only valid with") || flags.includes("does not accept") || flags.includes("mutually exclusive");
|
|
4422
|
+
if (isFlagMisuse) {
|
|
4423
|
+
io.err(" (run `launchpad deploy` with no flags from your app directory for a content deploy)");
|
|
4424
|
+
return 64;
|
|
4425
|
+
}
|
|
4378
4426
|
io.err("");
|
|
4379
|
-
io.err(
|
|
4380
|
-
` + " (slug defaults to the current directory's `launchpad-app-<slug>` suffix)\n" + `
|
|
4381
|
-
` + `M-892 modes:
|
|
4382
|
-
` + ` launchpad deploy --resume <slug>
|
|
4383
|
-
` + ` launchpad deploy --abandon <slug>
|
|
4384
|
-
` + ` launchpad deploy --new --slug <slug> --display-name <name>
|
|
4385
|
-
` + ` --app-type <${APP_TYPES.join("|")}>
|
|
4386
|
-
` + ` --allowed-group <G_KEY> [--allowed-group ...]
|
|
4387
|
-
` + `
|
|
4388
|
-
` + `Scope 6 dry-run (manifest-driven preview, read-only):
|
|
4389
|
-
` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
|
|
4390
|
-
` + `
|
|
4391
|
-
` + `Scope 6 apply (manifest-driven, writes TF + runs terraform apply):
|
|
4392
|
-
` + ` launchpad deploy --apply --platform-repo <path>
|
|
4393
|
-
` + " [--file <manifest>] [--re-pin] [--yes]");
|
|
4427
|
+
io.err(deployUsage());
|
|
4394
4428
|
return 64;
|
|
4395
4429
|
}
|
|
4396
4430
|
if (flags.mode.kind === "dry-run") {
|
|
@@ -4402,6 +4436,7 @@ async function runDeploy(args, io) {
|
|
|
4402
4436
|
platformRepo: flags.mode.platformRepo,
|
|
4403
4437
|
rePin: flags.mode.rePin,
|
|
4404
4438
|
yes: flags.mode.yes,
|
|
4439
|
+
...flags.mode.atSha !== null ? { atSha: flags.mode.atSha } : {},
|
|
4405
4440
|
...flags.mode.resumePrNumber !== null ? { resumePrNumber: flags.mode.resumePrNumber } : {},
|
|
4406
4441
|
...flags.mode.timeoutMinutes !== null ? { timeoutMinutes: flags.mode.timeoutMinutes } : {}
|
|
4407
4442
|
}, io);
|
|
@@ -4423,8 +4458,8 @@ async function runDeploy(args, io) {
|
|
|
4423
4458
|
}, io);
|
|
4424
4459
|
}
|
|
4425
4460
|
const cwd = process.cwd();
|
|
4426
|
-
const manifestPath =
|
|
4427
|
-
if (
|
|
4461
|
+
const manifestPath = path7.join(cwd, "launchpad.yaml");
|
|
4462
|
+
if (existsSync5(manifestPath)) {
|
|
4428
4463
|
return runModelADeploy({ cwd, manifestPath, argv: args, io });
|
|
4429
4464
|
}
|
|
4430
4465
|
const parsed = parseArgs2(args);
|
|
@@ -4436,9 +4471,9 @@ async function runDeploy(args, io) {
|
|
|
4436
4471
|
if (parsed.slug !== null) {
|
|
4437
4472
|
slug = parsed.slug;
|
|
4438
4473
|
} else {
|
|
4439
|
-
const inferred =
|
|
4474
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
4440
4475
|
if (inferred === null) {
|
|
4441
|
-
io.err(`launchpad deploy: could not infer slug from cwd (${
|
|
4476
|
+
io.err(`launchpad deploy: could not infer slug from cwd (${path7.basename(process.cwd())});
|
|
4442
4477
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
4443
4478
|
return 64;
|
|
4444
4479
|
}
|
|
@@ -4458,10 +4493,10 @@ async function runDeploy(args, io) {
|
|
|
4458
4493
|
return 1;
|
|
4459
4494
|
}
|
|
4460
4495
|
let contentManifestYaml = null;
|
|
4461
|
-
const contentManifestPath =
|
|
4462
|
-
if (
|
|
4496
|
+
const contentManifestPath = path7.join(process.cwd(), "launchpad.yaml");
|
|
4497
|
+
if (existsSync5(contentManifestPath)) {
|
|
4463
4498
|
try {
|
|
4464
|
-
contentManifestYaml =
|
|
4499
|
+
contentManifestYaml = readFileSync7(contentManifestPath, "utf8");
|
|
4465
4500
|
} catch {
|
|
4466
4501
|
contentManifestYaml = null;
|
|
4467
4502
|
}
|
|
@@ -4539,6 +4574,24 @@ async function runDeploy(args, io) {
|
|
|
4539
4574
|
return 1;
|
|
4540
4575
|
}
|
|
4541
4576
|
}
|
|
4577
|
+
function deployUsage() {
|
|
4578
|
+
return `usage: launchpad deploy [--message <text>] [--slug <slug>]
|
|
4579
|
+
` + ` (slug comes from launchpad.yaml at cwd when present — Model A —
|
|
4580
|
+
` + " else from the current directory's `launchpad-app-<slug>` suffix)\n" + `
|
|
4581
|
+
` + `provisioning modes:
|
|
4582
|
+
` + ` launchpad deploy --resume <slug>
|
|
4583
|
+
` + ` launchpad deploy --abandon <slug>
|
|
4584
|
+
` + ` launchpad deploy --new --slug <slug> --display-name <name>
|
|
4585
|
+
` + ` --app-type <${APP_TYPES.join("|")}>
|
|
4586
|
+
` + ` --allowed-group <G_KEY> [--allowed-group ...]
|
|
4587
|
+
` + `
|
|
4588
|
+
` + `manifest-driven dry-run (read-only preview):
|
|
4589
|
+
` + ` launchpad deploy --dry-run [--file <manifest>] [--json]
|
|
4590
|
+
` + `
|
|
4591
|
+
` + `manifest-driven apply (runs server-side via portal-bot):
|
|
4592
|
+
` + ` launchpad deploy --apply [--file <manifest>] [--at <sha>] [--re-pin]
|
|
4593
|
+
` + " [--yes] [--resume-pr <n>] [--timeout-minutes <n>]";
|
|
4594
|
+
}
|
|
4542
4595
|
function parseArgs2(args) {
|
|
4543
4596
|
let message = null;
|
|
4544
4597
|
let slug = null;
|
|
@@ -4565,11 +4618,6 @@ function parseArgs2(args) {
|
|
|
4565
4618
|
}
|
|
4566
4619
|
return { slug, message };
|
|
4567
4620
|
}
|
|
4568
|
-
function inferSlugFromCwd(cwd) {
|
|
4569
|
-
const base = path6.basename(cwd);
|
|
4570
|
-
const m = base.match(DIRNAME_RE);
|
|
4571
|
-
return m === null ? null : m[1];
|
|
4572
|
-
}
|
|
4573
4621
|
function makeBytesFetcher(bytes, pathSuffix) {
|
|
4574
4622
|
return async (input, init) => {
|
|
4575
4623
|
const targetUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
@@ -4597,22 +4645,33 @@ function formatBytes2(n) {
|
|
|
4597
4645
|
function describe12(e) {
|
|
4598
4646
|
return e instanceof Error ? e.message : String(e);
|
|
4599
4647
|
}
|
|
4600
|
-
function
|
|
4601
|
-
if (
|
|
4602
|
-
|
|
4648
|
+
function surfaceDeployExtras(body, io, slug) {
|
|
4649
|
+
if (body.boundary_stripped !== undefined && body.boundary_stripped.length > 0) {
|
|
4650
|
+
io.err(`warning: the bot stripped ${body.boundary_stripped.length} never-shippable file(s) server-side:`);
|
|
4651
|
+
for (const p of body.boundary_stripped.slice(0, 10)) {
|
|
4652
|
+
io.err(` - ${p}`);
|
|
4653
|
+
}
|
|
4654
|
+
if (body.boundary_stripped.length > 10) {
|
|
4655
|
+
io.err(` … and ${body.boundary_stripped.length - 10} more`);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
const se = body.standing_exceptions;
|
|
4659
|
+
if (se !== undefined && se.count > 0) {
|
|
4660
|
+
io.out(` ${se.count} standing policy exception(s) observed in content already live on main (non-blocking):`);
|
|
4661
|
+
for (const e of se.entries.slice(0, 5)) {
|
|
4662
|
+
io.out(` - ${e.path} [${e.rule}]`);
|
|
4663
|
+
}
|
|
4664
|
+
if (se.entries.length > 5) {
|
|
4665
|
+
io.out(` … and ${se.entries.length - 5} more`);
|
|
4666
|
+
}
|
|
4667
|
+
io.out(` Full list: \`launchpad status ${slug}\`.`);
|
|
4603
4668
|
}
|
|
4604
|
-
const meta = parsed.metadata;
|
|
4605
|
-
if (typeof meta.slug === "string")
|
|
4606
|
-
return meta.slug;
|
|
4607
|
-
if (typeof meta.name === "string")
|
|
4608
|
-
return meta.name;
|
|
4609
|
-
return null;
|
|
4610
4669
|
}
|
|
4611
4670
|
async function runModelADeploy(args) {
|
|
4612
4671
|
const { cwd, manifestPath, io } = args;
|
|
4613
4672
|
let slug;
|
|
4614
4673
|
try {
|
|
4615
|
-
const metaSlug = resolveManifestSlug(
|
|
4674
|
+
const metaSlug = resolveManifestSlug(parseYaml5(readFileSync7(manifestPath, "utf8")));
|
|
4616
4675
|
if (metaSlug === null) {
|
|
4617
4676
|
io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
|
|
4618
4677
|
return 64;
|
|
@@ -4688,16 +4747,30 @@ async function runModelADeploy(args) {
|
|
|
4688
4747
|
io.err(` - ${String(f.path)} [${String(f.rule)}]`);
|
|
4689
4748
|
}
|
|
4690
4749
|
}
|
|
4750
|
+
if (errorCode === "bundle_policy_violation" || errorCode === "app_boundary_violation" || errorCode === "bad_build_command") {
|
|
4751
|
+
io.err("");
|
|
4752
|
+
io.err(" Nothing was committed or claimed by this attempt — fix the");
|
|
4753
|
+
io.err(" listed file(s) and re-run `launchpad deploy`; the retry is clean.");
|
|
4754
|
+
}
|
|
4691
4755
|
return 1;
|
|
4692
4756
|
}
|
|
4693
4757
|
case "ok": {
|
|
4694
4758
|
for (const w of result.boundaryWarnings) {
|
|
4695
4759
|
io.err(`warning: ${w}`);
|
|
4696
4760
|
}
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4761
|
+
const success = result.result;
|
|
4762
|
+
if ("outcome" in success) {
|
|
4763
|
+
io.out("Nothing to deploy — your app is already live at this content.");
|
|
4764
|
+
if (typeof success.head_sha === "string") {
|
|
4765
|
+
io.out(` (managed main @ ${success.head_sha.slice(0, 8)} matches the bundle)`);
|
|
4766
|
+
}
|
|
4767
|
+
surfaceDeployExtras(success, io, slug);
|
|
4768
|
+
return 0;
|
|
4769
|
+
}
|
|
4770
|
+
if (success.status === "provisioning_started") {
|
|
4771
|
+
const submissionId = typeof success.submissionId === "string" ? success.submissionId : "(missing)";
|
|
4772
|
+
const appType = typeof success.appType === "string" ? success.appType : "(missing)";
|
|
4773
|
+
const message = typeof success.message === "string" ? success.message : "Bot accepted the upload and started provisioning.";
|
|
4701
4774
|
io.out(`✓ First-time deploy — provisioning workflow started for ${slug}`);
|
|
4702
4775
|
io.out(` submission: ${submissionId}`);
|
|
4703
4776
|
io.out(` appType: ${appType}`);
|
|
@@ -4709,12 +4782,12 @@ async function runModelADeploy(args) {
|
|
|
4709
4782
|
io.out(` launchpad deploy # re-run once lifecycle is "live"`);
|
|
4710
4783
|
return 0;
|
|
4711
4784
|
}
|
|
4712
|
-
if (typeof
|
|
4785
|
+
if (typeof success.commit_sha !== "string" || typeof success.repo !== "string") {
|
|
4713
4786
|
io.err(`launchpad deploy: bot returned an unexpected 202 body — missing commit_sha or repo.`);
|
|
4714
|
-
io.err(` got: ${JSON.stringify(
|
|
4787
|
+
io.err(` got: ${JSON.stringify(success)}`);
|
|
4715
4788
|
return 1;
|
|
4716
4789
|
}
|
|
4717
|
-
io.out(`✓ Bundle accepted — committed as ${
|
|
4790
|
+
io.out(`✓ Bundle accepted — committed as ${success.commit_sha.slice(0, 8)} on ${success.repo}`);
|
|
4718
4791
|
io.out(` ${result.fileCount} files (${formatBytes2(result.compressedBytes)} gzipped)`);
|
|
4719
4792
|
if (result.workerScript !== null) {
|
|
4720
4793
|
io.out(` cron Worker '${result.workerScript}' bundled + shipped (the bot deploys it after the commit)`);
|
|
@@ -4722,9 +4795,10 @@ async function runModelADeploy(args) {
|
|
|
4722
4795
|
if (result.walk.skipped.length > 0) {
|
|
4723
4796
|
io.out(` ${result.walk.skipped.length} entries skipped (.gitignore / default-ignore / symlink)`);
|
|
4724
4797
|
}
|
|
4725
|
-
if (typeof
|
|
4726
|
-
io.out(` ${
|
|
4798
|
+
if (typeof success.message === "string") {
|
|
4799
|
+
io.out(` ${success.message}`);
|
|
4727
4800
|
}
|
|
4801
|
+
surfaceDeployExtras(success, io, slug);
|
|
4728
4802
|
io.out("");
|
|
4729
4803
|
io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
|
|
4730
4804
|
io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
|
|
@@ -4734,14 +4808,13 @@ async function runModelADeploy(args) {
|
|
|
4734
4808
|
}
|
|
4735
4809
|
|
|
4736
4810
|
// src/commands/envvars.ts
|
|
4737
|
-
import * as
|
|
4811
|
+
import * as path8 from "node:path";
|
|
4738
4812
|
var envvarsCommand = {
|
|
4739
4813
|
name: "envvars",
|
|
4740
4814
|
summary: "list / set / remove production env vars (slug-scoped)",
|
|
4741
4815
|
run: runEnvvars
|
|
4742
4816
|
};
|
|
4743
4817
|
var SLUG_RE5 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
4744
|
-
var DIRNAME_RE2 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4745
4818
|
var ENV_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
4746
4819
|
async function runEnvvars(args, io) {
|
|
4747
4820
|
const parsed = parseArgs3(args);
|
|
@@ -4753,9 +4826,9 @@ async function runEnvvars(args, io) {
|
|
|
4753
4826
|
if (parsed.slug !== null) {
|
|
4754
4827
|
slug = parsed.slug;
|
|
4755
4828
|
} else {
|
|
4756
|
-
const inferred =
|
|
4829
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
4757
4830
|
if (inferred === null) {
|
|
4758
|
-
io.err(`launchpad envvars: could not infer slug from cwd (${
|
|
4831
|
+
io.err(`launchpad envvars: could not infer slug from cwd (${path8.basename(process.cwd())});
|
|
4759
4832
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
4760
4833
|
return 64;
|
|
4761
4834
|
}
|
|
@@ -4901,11 +4974,6 @@ function parseArgs3(args) {
|
|
|
4901
4974
|
}
|
|
4902
4975
|
return null;
|
|
4903
4976
|
}
|
|
4904
|
-
function inferSlugFromCwd2(cwd) {
|
|
4905
|
-
const base = path7.basename(cwd);
|
|
4906
|
-
const m = base.match(DIRNAME_RE2);
|
|
4907
|
-
return m === null ? null : m[1];
|
|
4908
|
-
}
|
|
4909
4977
|
function renderList(envVars, io) {
|
|
4910
4978
|
if (envVars.length === 0) {
|
|
4911
4979
|
io.out("(no env vars set)");
|
|
@@ -4930,8 +4998,8 @@ function describe13(e) {
|
|
|
4930
4998
|
}
|
|
4931
4999
|
|
|
4932
5000
|
// src/commands/generate.ts
|
|
4933
|
-
import { mkdirSync, readFileSync as
|
|
4934
|
-
import { dirname as dirname4, resolve as
|
|
5001
|
+
import { mkdirSync, readFileSync as readFileSync8, writeFileSync } from "node:fs";
|
|
5002
|
+
import { dirname as dirname4, resolve as resolve7, relative as relative3 } from "node:path";
|
|
4935
5003
|
var generateCommand = {
|
|
4936
5004
|
name: "generate",
|
|
4937
5005
|
summary: "emit derived artefacts (wrangler.toml, deploy.yml) from launchpad.yaml",
|
|
@@ -4944,14 +5012,14 @@ async function runGenerate(args, io) {
|
|
|
4944
5012
|
io.err("Usage: launchpad generate [--file <path>] [--dry-run] [--force] [--json]");
|
|
4945
5013
|
return 64;
|
|
4946
5014
|
}
|
|
4947
|
-
const manifestPath =
|
|
5015
|
+
const manifestPath = resolve7(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
4948
5016
|
const result = loadManifest(manifestPath);
|
|
4949
5017
|
if (result.kind !== "ok") {
|
|
4950
5018
|
return flags.json ? renderManifestErrorJson(result, io) : renderManifestErrorHuman(result, io);
|
|
4951
5019
|
}
|
|
4952
5020
|
const appRoot = dirname4(manifestPath);
|
|
4953
|
-
const wranglerPath =
|
|
4954
|
-
const workflowPath =
|
|
5021
|
+
const wranglerPath = resolve7(appRoot, "container", "wrangler.toml");
|
|
5022
|
+
const workflowPath = resolve7(appRoot, ".github", "workflows", "deploy.yml");
|
|
4955
5023
|
const wranglerOut = generateWranglerToml(result.manifest);
|
|
4956
5024
|
const workflowOut = generateGithubDeployWorkflow(result.manifest);
|
|
4957
5025
|
if (flags.dryRun) {
|
|
@@ -4966,7 +5034,7 @@ async function runGenerate(args, io) {
|
|
|
4966
5034
|
];
|
|
4967
5035
|
return flags.json ? renderApplyJson(reports, appRoot, io) : renderApplyHuman(reports, appRoot, io);
|
|
4968
5036
|
}
|
|
4969
|
-
function applyOne(artefact,
|
|
5037
|
+
function applyOne(artefact, path9, out, force) {
|
|
4970
5038
|
if (out.kind === "not-applicable") {
|
|
4971
5039
|
return {
|
|
4972
5040
|
artefact,
|
|
@@ -4974,32 +5042,32 @@ function applyOne(artefact, path8, out, force) {
|
|
|
4974
5042
|
warnings: []
|
|
4975
5043
|
};
|
|
4976
5044
|
}
|
|
4977
|
-
const existing = readIfExists(
|
|
5045
|
+
const existing = readIfExists(path9);
|
|
4978
5046
|
if (existing.kind === "read-error") {
|
|
4979
5047
|
return {
|
|
4980
5048
|
artefact,
|
|
4981
|
-
action: { kind: "write-error", path:
|
|
5049
|
+
action: { kind: "write-error", path: path9, message: existing.message },
|
|
4982
5050
|
warnings: out.warnings
|
|
4983
5051
|
};
|
|
4984
5052
|
}
|
|
4985
5053
|
if (existing.kind === "ok" && existing.content === out.content) {
|
|
4986
|
-
return { artefact, action: { kind: "unchanged", path:
|
|
5054
|
+
return { artefact, action: { kind: "unchanged", path: path9 }, warnings: out.warnings };
|
|
4987
5055
|
}
|
|
4988
5056
|
if (existing.kind === "ok" && existing.content !== out.content && !force) {
|
|
4989
5057
|
return {
|
|
4990
5058
|
artefact,
|
|
4991
|
-
action: { kind: "would-overwrite", path:
|
|
5059
|
+
action: { kind: "would-overwrite", path: path9 },
|
|
4992
5060
|
warnings: out.warnings
|
|
4993
5061
|
};
|
|
4994
5062
|
}
|
|
4995
5063
|
try {
|
|
4996
|
-
mkdirSync(dirname4(
|
|
4997
|
-
writeFileSync(
|
|
5064
|
+
mkdirSync(dirname4(path9), { recursive: true });
|
|
5065
|
+
writeFileSync(path9, out.content, "utf8");
|
|
4998
5066
|
return {
|
|
4999
5067
|
artefact,
|
|
5000
5068
|
action: {
|
|
5001
5069
|
kind: "written",
|
|
5002
|
-
path:
|
|
5070
|
+
path: path9,
|
|
5003
5071
|
bytes: Buffer.byteLength(out.content, "utf8")
|
|
5004
5072
|
},
|
|
5005
5073
|
warnings: out.warnings
|
|
@@ -5009,16 +5077,16 @@ function applyOne(artefact, path8, out, force) {
|
|
|
5009
5077
|
artefact,
|
|
5010
5078
|
action: {
|
|
5011
5079
|
kind: "write-error",
|
|
5012
|
-
path:
|
|
5080
|
+
path: path9,
|
|
5013
5081
|
message: err.message ?? String(err)
|
|
5014
5082
|
},
|
|
5015
5083
|
warnings: out.warnings
|
|
5016
5084
|
};
|
|
5017
5085
|
}
|
|
5018
5086
|
}
|
|
5019
|
-
function readIfExists(
|
|
5087
|
+
function readIfExists(path9) {
|
|
5020
5088
|
try {
|
|
5021
|
-
return { kind: "ok", content:
|
|
5089
|
+
return { kind: "ok", content: readFileSync8(path9, "utf8") };
|
|
5022
5090
|
} catch (err) {
|
|
5023
5091
|
const e = err;
|
|
5024
5092
|
if (e.code === "ENOENT")
|
|
@@ -5229,7 +5297,7 @@ function parseFlags(args) {
|
|
|
5229
5297
|
}
|
|
5230
5298
|
|
|
5231
5299
|
// src/groups/client.ts
|
|
5232
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
5300
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5233
5301
|
import { dirname as dirname5, join as join9 } from "node:path";
|
|
5234
5302
|
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
5235
5303
|
var CACHE_FILENAME = "groups.json";
|
|
@@ -5270,10 +5338,10 @@ async function fetchGroups(cfg, opts = {}) {
|
|
|
5270
5338
|
writeCache(cachePath, { fetchedAt, groups });
|
|
5271
5339
|
return { kind: "ok", source: "fresh", fetchedAt, groups };
|
|
5272
5340
|
}
|
|
5273
|
-
function readCache(
|
|
5341
|
+
function readCache(path9) {
|
|
5274
5342
|
let raw;
|
|
5275
5343
|
try {
|
|
5276
|
-
raw =
|
|
5344
|
+
raw = readFileSync9(path9, "utf8");
|
|
5277
5345
|
} catch {
|
|
5278
5346
|
return null;
|
|
5279
5347
|
}
|
|
@@ -5301,10 +5369,10 @@ function isEntraGroup(value) {
|
|
|
5301
5369
|
const g = value;
|
|
5302
5370
|
return typeof g.id === "string" && typeof g.displayName === "string" && (typeof g.mailNickname === "string" || g.mailNickname === null);
|
|
5303
5371
|
}
|
|
5304
|
-
function writeCache(
|
|
5372
|
+
function writeCache(path9, envelope) {
|
|
5305
5373
|
try {
|
|
5306
|
-
mkdirSync2(dirname5(
|
|
5307
|
-
writeFileSync2(
|
|
5374
|
+
mkdirSync2(dirname5(path9), { recursive: true });
|
|
5375
|
+
writeFileSync2(path9, JSON.stringify(envelope), "utf8");
|
|
5308
5376
|
} catch {}
|
|
5309
5377
|
}
|
|
5310
5378
|
function describe14(e) {
|
|
@@ -5774,17 +5842,17 @@ function describe17(e) {
|
|
|
5774
5842
|
|
|
5775
5843
|
// src/commands/init.ts
|
|
5776
5844
|
import { createInterface } from "node:readline/promises";
|
|
5777
|
-
import { existsSync as
|
|
5778
|
-
import { resolve as
|
|
5845
|
+
import { existsSync as existsSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5846
|
+
import { resolve as resolve8 } from "node:path";
|
|
5779
5847
|
import { stringify as yamlStringify } from "yaml";
|
|
5780
5848
|
|
|
5781
5849
|
// src/detect/index.ts
|
|
5782
|
-
import { existsSync as
|
|
5850
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, statSync } from "node:fs";
|
|
5783
5851
|
import { join as join10 } from "node:path";
|
|
5784
5852
|
function detectAppShape(cwd) {
|
|
5785
|
-
const hasPackageJson =
|
|
5786
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) =>
|
|
5787
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) =>
|
|
5853
|
+
const hasPackageJson = existsSync6(join10(cwd, "package.json"));
|
|
5854
|
+
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync6(join10(cwd, file)));
|
|
5855
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync6(join10(cwd, n)));
|
|
5788
5856
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
5789
5857
|
return {
|
|
5790
5858
|
kind: "not-applicable",
|
|
@@ -5830,7 +5898,7 @@ var LOCKFILES = [
|
|
|
5830
5898
|
{ file: "yarn.lock", pm: "yarn" }
|
|
5831
5899
|
];
|
|
5832
5900
|
function detectPackageManager(cwd) {
|
|
5833
|
-
const present = LOCKFILES.filter(({ file }) =>
|
|
5901
|
+
const present = LOCKFILES.filter(({ file }) => existsSync6(join10(cwd, file)));
|
|
5834
5902
|
if (present.length === 0) {
|
|
5835
5903
|
return {
|
|
5836
5904
|
kind: "ambiguous",
|
|
@@ -5853,7 +5921,7 @@ function detectPackageManager(cwd) {
|
|
|
5853
5921
|
function detectVitePresence(cwd) {
|
|
5854
5922
|
for (const name of VITE_CONFIG_NAMES) {
|
|
5855
5923
|
const p = join10(cwd, name);
|
|
5856
|
-
if (
|
|
5924
|
+
if (existsSync6(p)) {
|
|
5857
5925
|
return { kind: "ok", value: { path: p } };
|
|
5858
5926
|
}
|
|
5859
5927
|
}
|
|
@@ -5865,7 +5933,7 @@ function detectVitePresence(cwd) {
|
|
|
5865
5933
|
function detectAppType(cwd) {
|
|
5866
5934
|
const fnDir = join10(cwd, "functions");
|
|
5867
5935
|
let hasFunctionsDir = false;
|
|
5868
|
-
if (
|
|
5936
|
+
if (existsSync6(fnDir)) {
|
|
5869
5937
|
try {
|
|
5870
5938
|
hasFunctionsDir = statSync(fnDir).isDirectory();
|
|
5871
5939
|
} catch {
|
|
@@ -5876,7 +5944,7 @@ function detectAppType(cwd) {
|
|
|
5876
5944
|
}
|
|
5877
5945
|
function detectBuildCommand(cwd, pm) {
|
|
5878
5946
|
const pkgJsonPath = join10(cwd, "package.json");
|
|
5879
|
-
if (!
|
|
5947
|
+
if (!existsSync6(pkgJsonPath)) {
|
|
5880
5948
|
return {
|
|
5881
5949
|
kind: "ambiguous",
|
|
5882
5950
|
reason: "no package.json at repo root. Run your package manager's `init` first."
|
|
@@ -5884,7 +5952,7 @@ function detectBuildCommand(cwd, pm) {
|
|
|
5884
5952
|
}
|
|
5885
5953
|
let pkgJson;
|
|
5886
5954
|
try {
|
|
5887
|
-
pkgJson = JSON.parse(
|
|
5955
|
+
pkgJson = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
|
|
5888
5956
|
} catch (e) {
|
|
5889
5957
|
return {
|
|
5890
5958
|
kind: "ambiguous",
|
|
@@ -5919,7 +5987,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
|
|
|
5919
5987
|
function detectDestinationDir(cwd, vite) {
|
|
5920
5988
|
let text;
|
|
5921
5989
|
try {
|
|
5922
|
-
text =
|
|
5990
|
+
text = readFileSync10(vite.path, "utf8");
|
|
5923
5991
|
} catch (e) {
|
|
5924
5992
|
return {
|
|
5925
5993
|
kind: "ambiguous",
|
|
@@ -5948,8 +6016,8 @@ async function runInit(args, io, prompt) {
|
|
|
5948
6016
|
return 64;
|
|
5949
6017
|
}
|
|
5950
6018
|
const { inputs, options } = parsed;
|
|
5951
|
-
const outPath =
|
|
5952
|
-
if (
|
|
6019
|
+
const outPath = resolve8(process.cwd(), options.out);
|
|
6020
|
+
if (existsSync7(outPath) && !options.force) {
|
|
5953
6021
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
5954
6022
|
io.err("Pass --force to overwrite.");
|
|
5955
6023
|
return 64;
|
|
@@ -5996,7 +6064,7 @@ async function runInit(args, io, prompt) {
|
|
|
5996
6064
|
}
|
|
5997
6065
|
io.out(`✓ wrote ${outPath}`);
|
|
5998
6066
|
if (options.gitignore) {
|
|
5999
|
-
const gitignorePath =
|
|
6067
|
+
const gitignorePath = resolve8(process.cwd(), ".gitignore");
|
|
6000
6068
|
try {
|
|
6001
6069
|
const changed = ensureGitignoreEntries(gitignorePath, [".env", ".env.local"]);
|
|
6002
6070
|
if (changed.length > 0) {
|
|
@@ -6365,10 +6433,10 @@ function buildManifest(inputs, detected) {
|
|
|
6365
6433
|
function renderYaml(manifest) {
|
|
6366
6434
|
return yamlStringify(manifest, { lineWidth: 0 });
|
|
6367
6435
|
}
|
|
6368
|
-
function ensureGitignoreEntries(
|
|
6436
|
+
function ensureGitignoreEntries(path9, entries) {
|
|
6369
6437
|
let current = "";
|
|
6370
|
-
if (
|
|
6371
|
-
current =
|
|
6438
|
+
if (existsSync7(path9)) {
|
|
6439
|
+
current = readFileSync11(path9, "utf8");
|
|
6372
6440
|
}
|
|
6373
6441
|
const lines = current.split(/\r?\n/);
|
|
6374
6442
|
const present = new Set(lines.map((l) => l.trim()));
|
|
@@ -6386,7 +6454,7 @@ function ensureGitignoreEntries(path8, entries) {
|
|
|
6386
6454
|
}
|
|
6387
6455
|
}
|
|
6388
6456
|
if (added.length > 0) {
|
|
6389
|
-
writeFileSync3(
|
|
6457
|
+
writeFileSync3(path9, out, { encoding: "utf8" });
|
|
6390
6458
|
}
|
|
6391
6459
|
return added;
|
|
6392
6460
|
}
|
|
@@ -6458,14 +6526,13 @@ function describe19(e) {
|
|
|
6458
6526
|
}
|
|
6459
6527
|
|
|
6460
6528
|
// src/commands/logs.ts
|
|
6461
|
-
import * as
|
|
6529
|
+
import * as path9 from "node:path";
|
|
6462
6530
|
var logsCommand = {
|
|
6463
6531
|
name: "logs",
|
|
6464
6532
|
summary: "show recent Pages deployment history (slug-scoped)",
|
|
6465
6533
|
run: runLogs
|
|
6466
6534
|
};
|
|
6467
6535
|
var SLUG_RE6 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
6468
|
-
var DIRNAME_RE3 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
6469
6536
|
var DEFAULT_LINES = 10;
|
|
6470
6537
|
var MAX_LINES = 25;
|
|
6471
6538
|
async function runLogs(args, io) {
|
|
@@ -6479,9 +6546,9 @@ async function runLogs(args, io) {
|
|
|
6479
6546
|
if (parsed.slug !== null) {
|
|
6480
6547
|
slug = parsed.slug;
|
|
6481
6548
|
} else {
|
|
6482
|
-
const inferred =
|
|
6549
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
6483
6550
|
if (inferred === null) {
|
|
6484
|
-
io.err(`launchpad logs: could not infer slug from cwd (${
|
|
6551
|
+
io.err(`launchpad logs: could not infer slug from cwd (${path9.basename(process.cwd())});
|
|
6485
6552
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
6486
6553
|
return 64;
|
|
6487
6554
|
}
|
|
@@ -6559,11 +6626,6 @@ function parseArgs5(args) {
|
|
|
6559
6626
|
}
|
|
6560
6627
|
return { slug, lines };
|
|
6561
6628
|
}
|
|
6562
|
-
function inferSlugFromCwd3(cwd) {
|
|
6563
|
-
const base = path8.basename(cwd);
|
|
6564
|
-
const m = base.match(DIRNAME_RE3);
|
|
6565
|
-
return m === null ? null : m[1];
|
|
6566
|
-
}
|
|
6567
6629
|
function renderDeployments(deployments, io) {
|
|
6568
6630
|
if (deployments.length === 0) {
|
|
6569
6631
|
io.out("(no deployments yet)");
|
|
@@ -6614,14 +6676,13 @@ function describe20(e) {
|
|
|
6614
6676
|
}
|
|
6615
6677
|
|
|
6616
6678
|
// src/commands/merge.ts
|
|
6617
|
-
import * as
|
|
6679
|
+
import * as path10 from "node:path";
|
|
6618
6680
|
var mergeCommand = {
|
|
6619
6681
|
name: "merge",
|
|
6620
6682
|
summary: "squash-merge a review-passed PR (slug-scoped)",
|
|
6621
6683
|
run: runMerge
|
|
6622
6684
|
};
|
|
6623
6685
|
var SLUG_RE7 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
6624
|
-
var DIRNAME_RE4 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
6625
6686
|
var HANDLED_STATUSES = [409, 422, 502, 503];
|
|
6626
6687
|
async function runMerge(args, io) {
|
|
6627
6688
|
const parsed = parseArgs6(args);
|
|
@@ -6634,9 +6695,9 @@ async function runMerge(args, io) {
|
|
|
6634
6695
|
if (parsed.slug !== null) {
|
|
6635
6696
|
slug = parsed.slug;
|
|
6636
6697
|
} else {
|
|
6637
|
-
const inferred =
|
|
6698
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
6638
6699
|
if (inferred === null) {
|
|
6639
|
-
io.err(`launchpad merge: could not infer slug from cwd (${
|
|
6700
|
+
io.err(`launchpad merge: could not infer slug from cwd (${path10.basename(process.cwd())});
|
|
6640
6701
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
6641
6702
|
return 64;
|
|
6642
6703
|
}
|
|
@@ -6733,11 +6794,6 @@ function parseArgs6(args) {
|
|
|
6733
6794
|
return null;
|
|
6734
6795
|
return { slug, prNumber };
|
|
6735
6796
|
}
|
|
6736
|
-
function inferSlugFromCwd4(cwd) {
|
|
6737
|
-
const base = path9.basename(cwd);
|
|
6738
|
-
const m = base.match(DIRNAME_RE4);
|
|
6739
|
-
return m === null ? null : m[1];
|
|
6740
|
-
}
|
|
6741
6797
|
function renderBotError(status, env, io, prNumber = null) {
|
|
6742
6798
|
const code = env?.error ?? "unknown";
|
|
6743
6799
|
const detail = env?.message;
|
|
@@ -6793,7 +6849,7 @@ function describe21(e) {
|
|
|
6793
6849
|
}
|
|
6794
6850
|
|
|
6795
6851
|
// src/commands/plan.ts
|
|
6796
|
-
import { resolve as
|
|
6852
|
+
import { resolve as resolve9 } from "node:path";
|
|
6797
6853
|
var planCommand = {
|
|
6798
6854
|
name: "plan",
|
|
6799
6855
|
summary: "summarise what the manifest would deploy (offline)",
|
|
@@ -6806,7 +6862,7 @@ async function runPlan(args, io) {
|
|
|
6806
6862
|
io.err("Usage: launchpad plan [--file <path>] [--json]");
|
|
6807
6863
|
return 64;
|
|
6808
6864
|
}
|
|
6809
|
-
const manifestPath =
|
|
6865
|
+
const manifestPath = resolve9(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
6810
6866
|
const result = loadManifest(manifestPath);
|
|
6811
6867
|
return flags.json ? renderJson(result, io) : renderHuman(result, io);
|
|
6812
6868
|
}
|
|
@@ -6989,7 +7045,7 @@ var SLUG_RE8 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
|
6989
7045
|
var SLUG_MIN_LENGTH = 3;
|
|
6990
7046
|
var SLUG_MAX_LENGTH = 58;
|
|
6991
7047
|
async function runDestroy(args, io, prompt, isTty) {
|
|
6992
|
-
const parsed = parseArgs7(args);
|
|
7048
|
+
const parsed = parseArgs7(args, process.cwd(), (l) => io.err(l));
|
|
6993
7049
|
if (typeof parsed === "string") {
|
|
6994
7050
|
io.err(`launchpad destroy: ${parsed}`);
|
|
6995
7051
|
printUsage3(io);
|
|
@@ -7044,7 +7100,7 @@ async function runDestroy(args, io, prompt, isTty) {
|
|
|
7044
7100
|
return 1;
|
|
7045
7101
|
}
|
|
7046
7102
|
}
|
|
7047
|
-
function parseArgs7(args, cwd = process.cwd()) {
|
|
7103
|
+
function parseArgs7(args, cwd = process.cwd(), warn) {
|
|
7048
7104
|
let slug = null;
|
|
7049
7105
|
let confirmSlug = null;
|
|
7050
7106
|
let yes = false;
|
|
@@ -7091,7 +7147,7 @@ function parseArgs7(args, cwd = process.cwd()) {
|
|
|
7091
7147
|
i += 1;
|
|
7092
7148
|
}
|
|
7093
7149
|
if (slug === null) {
|
|
7094
|
-
const inferred =
|
|
7150
|
+
const inferred = inferSlug({ cwd, warn });
|
|
7095
7151
|
if (inferred === null) {
|
|
7096
7152
|
return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
7097
7153
|
}
|
|
@@ -7251,8 +7307,8 @@ import { stringify as stringifyYaml } from "yaml";
|
|
|
7251
7307
|
|
|
7252
7308
|
// src/deploy/manifest-state.ts
|
|
7253
7309
|
async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
7254
|
-
const
|
|
7255
|
-
const raw = await apiJson(cfg, { path:
|
|
7310
|
+
const path11 = opts.includeManifest === true ? `/apps/${encodeURIComponent(slug)}/manifest/state?include=manifest` : `/apps/${encodeURIComponent(slug)}/manifest/state`;
|
|
7311
|
+
const raw = await apiJson(cfg, { path: path11 }, fetcher);
|
|
7256
7312
|
return {
|
|
7257
7313
|
slug: raw.slug,
|
|
7258
7314
|
hasAppFile: raw.hasAppFile,
|
|
@@ -7265,11 +7321,24 @@ async function fetchManifestState(cfg, slug, opts = {}, fetcher = fetch) {
|
|
|
7265
7321
|
|
|
7266
7322
|
// src/deploy/manifest-status.ts
|
|
7267
7323
|
async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
7268
|
-
const
|
|
7269
|
-
return apiJson(cfg, { path:
|
|
7324
|
+
const path11 = `/apps/${encodeURIComponent(slug)}/manifest/status`;
|
|
7325
|
+
return apiJson(cfg, { path: path11 }, fetcher);
|
|
7270
7326
|
}
|
|
7271
7327
|
|
|
7272
7328
|
// src/deploy/deployment-status.ts
|
|
7329
|
+
async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
|
|
7330
|
+
let raw;
|
|
7331
|
+
try {
|
|
7332
|
+
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/exceptions` }, fetcher);
|
|
7333
|
+
} catch (e) {
|
|
7334
|
+
if (e instanceof NotFoundError)
|
|
7335
|
+
return null;
|
|
7336
|
+
throw e;
|
|
7337
|
+
}
|
|
7338
|
+
if (!Array.isArray(raw.exceptions))
|
|
7339
|
+
return null;
|
|
7340
|
+
return raw.exceptions.filter((e) => typeof e === "object" && e !== null && typeof e.path === "string" && typeof e.rule === "string" && typeof e.detectedAt === "string" && typeof e.deployRef === "string");
|
|
7341
|
+
}
|
|
7273
7342
|
async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
7274
7343
|
let raw;
|
|
7275
7344
|
try {
|
|
@@ -7297,7 +7366,7 @@ var pullCommand = {
|
|
|
7297
7366
|
};
|
|
7298
7367
|
var SLUG_RE9 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7299
7368
|
async function runPull(args, io) {
|
|
7300
|
-
const parsed = parseArgs8(args);
|
|
7369
|
+
const parsed = parseArgs8(args, process.cwd(), (l) => io.err(l));
|
|
7301
7370
|
if (typeof parsed === "string") {
|
|
7302
7371
|
io.err(`launchpad pull: ${parsed}`);
|
|
7303
7372
|
printUsage4(io);
|
|
@@ -7383,7 +7452,7 @@ async function fetchLiveDeploymentBestEffort(cfg, slug) {
|
|
|
7383
7452
|
return null;
|
|
7384
7453
|
}
|
|
7385
7454
|
}
|
|
7386
|
-
function parseArgs8(args, cwd = process.cwd()) {
|
|
7455
|
+
function parseArgs8(args, cwd = process.cwd(), warn) {
|
|
7387
7456
|
let slug = null;
|
|
7388
7457
|
let out = null;
|
|
7389
7458
|
let status = false;
|
|
@@ -7424,7 +7493,7 @@ function parseArgs8(args, cwd = process.cwd()) {
|
|
|
7424
7493
|
i += 1;
|
|
7425
7494
|
}
|
|
7426
7495
|
if (slug === null) {
|
|
7427
|
-
const inferred =
|
|
7496
|
+
const inferred = inferSlug({ cwd, warn });
|
|
7428
7497
|
if (inferred === null) {
|
|
7429
7498
|
return `slug not provided + cannot infer from cwd. ` + `Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
7430
7499
|
}
|
|
@@ -7442,8 +7511,9 @@ function printUsage4(io) {
|
|
|
7442
7511
|
" Reads the deployed launchpad.yaml for an app via the bot.",
|
|
7443
7512
|
" No local platform-repo or terraform required.",
|
|
7444
7513
|
"",
|
|
7445
|
-
"
|
|
7446
|
-
"
|
|
7514
|
+
" With no slug, it is inferred from the local launchpad.yaml's",
|
|
7515
|
+
" declared slug first, then from a launchpad-app-<slug>/",
|
|
7516
|
+
" directory name. Explicit --slug or positional override.",
|
|
7447
7517
|
"",
|
|
7448
7518
|
" --status read the role-redacted status block (what your",
|
|
7449
7519
|
" role may see) instead of the spec manifest.",
|
|
@@ -7456,14 +7526,184 @@ function describe23(e) {
|
|
|
7456
7526
|
return e instanceof Error ? e.message : String(e);
|
|
7457
7527
|
}
|
|
7458
7528
|
|
|
7529
|
+
// src/commands/recover.ts
|
|
7530
|
+
var recoverCommand = {
|
|
7531
|
+
name: "recover",
|
|
7532
|
+
summary: "repair a terminal-failed app record by reconciling against live state",
|
|
7533
|
+
run: runRecover
|
|
7534
|
+
};
|
|
7535
|
+
var SLUG_RE10 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7536
|
+
async function runRecover(args, io) {
|
|
7537
|
+
const parsed = parseRecoverArgs(args, process.cwd(), (l) => io.err(l));
|
|
7538
|
+
if (typeof parsed === "string") {
|
|
7539
|
+
io.err(`launchpad recover: ${parsed}`);
|
|
7540
|
+
printUsage5(io);
|
|
7541
|
+
return 64;
|
|
7542
|
+
}
|
|
7543
|
+
const cfg = loadConfig();
|
|
7544
|
+
try {
|
|
7545
|
+
io.out(`Reconciling "${parsed.slug}" against live Cloudflare state …`);
|
|
7546
|
+
const res = await apiRaw(cfg, {
|
|
7547
|
+
method: "POST",
|
|
7548
|
+
path: `/apps/${parsed.slug}/recover`,
|
|
7549
|
+
jsonBody: undefined,
|
|
7550
|
+
nonThrowingStatuses: [409, 503]
|
|
7551
|
+
});
|
|
7552
|
+
const body = await res.json().catch(() => null);
|
|
7553
|
+
if (parsed.json) {
|
|
7554
|
+
io.out(JSON.stringify({ httpStatus: res.status, ...body ?? {} }, null, 2));
|
|
7555
|
+
return res.status === 200 ? 0 : 1;
|
|
7556
|
+
}
|
|
7557
|
+
if (res.status === 200 && body !== null && "outcome" in body) {
|
|
7558
|
+
renderSuccess2(body, io);
|
|
7559
|
+
return 0;
|
|
7560
|
+
}
|
|
7561
|
+
if (res.status === 503) {
|
|
7562
|
+
const msg = body !== null && "message" in body && typeof body.message === "string" ? body.message : "live state unavailable — nothing was changed; retry shortly.";
|
|
7563
|
+
io.err(`launchpad recover: ${msg}`);
|
|
7564
|
+
return 1;
|
|
7565
|
+
}
|
|
7566
|
+
if (res.status === 409 && body !== null && "error" in body) {
|
|
7567
|
+
renderRefusal(parsed.slug, body, io);
|
|
7568
|
+
return 1;
|
|
7569
|
+
}
|
|
7570
|
+
io.err(`launchpad recover: bot returned an unexpected HTTP ${res.status} response.`);
|
|
7571
|
+
return 1;
|
|
7572
|
+
} catch (e) {
|
|
7573
|
+
return mapError(e, parsed.slug, io);
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
function renderSuccess2(body, io) {
|
|
7577
|
+
if (body.outcome === "noop_already_healthy") {
|
|
7578
|
+
io.out(`${body.slug}: already healthy — nothing to recover.`);
|
|
7579
|
+
io.out(` ${body.message}`);
|
|
7580
|
+
return;
|
|
7581
|
+
}
|
|
7582
|
+
io.out(`${body.slug}: REPAIRED — registry record reconciled to live.`);
|
|
7583
|
+
if (body.before !== undefined) {
|
|
7584
|
+
io.out(` before: ${body.before.lifecycle}` + (body.before.reason !== null ? ` (${body.before.reason})` : ""));
|
|
7585
|
+
}
|
|
7586
|
+
if (body.after !== undefined) {
|
|
7587
|
+
io.out(` after: ${body.after.lifecycle}`);
|
|
7588
|
+
}
|
|
7589
|
+
const checked = body.checked;
|
|
7590
|
+
if (checked !== undefined) {
|
|
7591
|
+
io.out(` verified: Pages project "${checked.pagesProject}" exists` + (checked.latestDeployment !== null ? `; latest production deployment ${checked.latestDeployment.id} ` + `(${checked.latestDeployment.buildStatus}, ${checked.latestDeployment.createdOn})` : ""));
|
|
7592
|
+
if (checked.olderContentServing) {
|
|
7593
|
+
io.out(" note: the LATEST build failed — an older successful deployment is what's serving.");
|
|
7594
|
+
}
|
|
7595
|
+
}
|
|
7596
|
+
io.out("");
|
|
7597
|
+
io.out(`Run \`launchpad status ${body.slug}\` — it now reports the live deployment truth.`);
|
|
7598
|
+
}
|
|
7599
|
+
function renderRefusal(slug, body, io) {
|
|
7600
|
+
io.err(`launchpad recover: refused — "${slug}" was NOT repaired.`);
|
|
7601
|
+
if (typeof body.message === "string") {
|
|
7602
|
+
io.err(` ${body.message}`);
|
|
7603
|
+
}
|
|
7604
|
+
const checked = body.checked;
|
|
7605
|
+
if (checked !== undefined) {
|
|
7606
|
+
io.err(" checked:");
|
|
7607
|
+
io.err(` Pages project "${checked.pagesProject}": ${checked.projectExists ? "exists" : "MISSING"}`);
|
|
7608
|
+
if (checked.latestDeployment !== null) {
|
|
7609
|
+
io.err(` latest production deployment: ${checked.latestDeployment.id} (${checked.latestDeployment.buildStatus})`);
|
|
7610
|
+
} else if (checked.projectExists) {
|
|
7611
|
+
io.err(" latest production deployment: none");
|
|
7612
|
+
}
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7615
|
+
function mapError(e, slug, io) {
|
|
7616
|
+
if (e instanceof UnauthenticatedError) {
|
|
7617
|
+
io.err(`launchpad recover: ${e.message}`);
|
|
7618
|
+
io.err(" session expired, run `launchpad login`");
|
|
7619
|
+
return 1;
|
|
7620
|
+
}
|
|
7621
|
+
if (e instanceof ForbiddenError) {
|
|
7622
|
+
io.err(`launchpad recover: not authorised for app "${slug}" (you must be an owner or editor).`);
|
|
7623
|
+
return 1;
|
|
7624
|
+
}
|
|
7625
|
+
if (e instanceof NotFoundError) {
|
|
7626
|
+
io.err(`launchpad recover: app "${slug}" not found.`);
|
|
7627
|
+
return 1;
|
|
7628
|
+
}
|
|
7629
|
+
if (e instanceof ApiError || e instanceof TransportError) {
|
|
7630
|
+
io.err(`launchpad recover: ${e.message}`);
|
|
7631
|
+
return 1;
|
|
7632
|
+
}
|
|
7633
|
+
io.err(`launchpad recover failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
7634
|
+
return 1;
|
|
7635
|
+
}
|
|
7636
|
+
function parseRecoverArgs(args, cwd = process.cwd(), warn) {
|
|
7637
|
+
let slug = null;
|
|
7638
|
+
let json = false;
|
|
7639
|
+
let i = 0;
|
|
7640
|
+
while (i < args.length) {
|
|
7641
|
+
const a = args[i] ?? "";
|
|
7642
|
+
if (a === "--slug") {
|
|
7643
|
+
const v = args[i + 1];
|
|
7644
|
+
if (v === undefined)
|
|
7645
|
+
return "missing value for --slug";
|
|
7646
|
+
if (slug !== null)
|
|
7647
|
+
return "cannot mix --slug with positional slug (or pass --slug twice)";
|
|
7648
|
+
slug = v;
|
|
7649
|
+
i += 2;
|
|
7650
|
+
continue;
|
|
7651
|
+
}
|
|
7652
|
+
if (a === "--json") {
|
|
7653
|
+
json = true;
|
|
7654
|
+
i += 1;
|
|
7655
|
+
continue;
|
|
7656
|
+
}
|
|
7657
|
+
if (a.startsWith("--")) {
|
|
7658
|
+
return `unknown flag "${a}"`;
|
|
7659
|
+
}
|
|
7660
|
+
if (slug !== null) {
|
|
7661
|
+
return "cannot mix --slug with positional slug (or pass two positional slugs)";
|
|
7662
|
+
}
|
|
7663
|
+
slug = a;
|
|
7664
|
+
i += 1;
|
|
7665
|
+
}
|
|
7666
|
+
if (slug === null) {
|
|
7667
|
+
slug = inferSlug({ cwd, warn });
|
|
7668
|
+
}
|
|
7669
|
+
if (slug === null) {
|
|
7670
|
+
return "slug not provided + cannot infer from cwd. " + "Pass <slug> or --slug <slug>, cd into launchpad-app-<slug>/, or run from a directory with launchpad.yaml.";
|
|
7671
|
+
}
|
|
7672
|
+
if (!SLUG_RE10.test(slug)) {
|
|
7673
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE10.source}`;
|
|
7674
|
+
}
|
|
7675
|
+
return { slug, json };
|
|
7676
|
+
}
|
|
7677
|
+
function printUsage5(io) {
|
|
7678
|
+
io.err([
|
|
7679
|
+
"usage: launchpad recover [<slug>] [--slug <slug>] [--json]",
|
|
7680
|
+
"",
|
|
7681
|
+
" Repair an app whose registry record is stuck at a terminal",
|
|
7682
|
+
" provisioning failure although the app is actually live (e.g. a",
|
|
7683
|
+
" since-fixed platform bug failed the record after content shipped).",
|
|
7684
|
+
"",
|
|
7685
|
+
" The bot verifies LIVE Cloudflare state first: the record is only",
|
|
7686
|
+
" repaired when the Pages project exists and a successful production",
|
|
7687
|
+
" deployment is serving. A not-live app is refused with what was",
|
|
7688
|
+
" checked — recover never fabricates a live state.",
|
|
7689
|
+
"",
|
|
7690
|
+
" Recovering an already-healthy app is a no-op success.",
|
|
7691
|
+
"",
|
|
7692
|
+
"Flags:",
|
|
7693
|
+
" --slug <slug> override cwd inference.",
|
|
7694
|
+
" --json emit machine-readable JSON to stdout."
|
|
7695
|
+
].join(`
|
|
7696
|
+
`));
|
|
7697
|
+
}
|
|
7698
|
+
|
|
7459
7699
|
// src/commands/status.ts
|
|
7460
|
-
import { readFileSync as
|
|
7700
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
7461
7701
|
var statusCommand = {
|
|
7462
7702
|
name: "status",
|
|
7463
7703
|
summary: "show drift between local launchpad.yaml and deployed state",
|
|
7464
7704
|
run: runStatus
|
|
7465
7705
|
};
|
|
7466
|
-
var
|
|
7706
|
+
var SLUG_RE11 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7467
7707
|
async function fetchLifecycle(cfg, slug) {
|
|
7468
7708
|
try {
|
|
7469
7709
|
return await apiJson(cfg, { path: `/apps/${slug}/lifecycle` });
|
|
@@ -7495,10 +7735,10 @@ function mapBotError(e, slug, io) {
|
|
|
7495
7735
|
return 2;
|
|
7496
7736
|
}
|
|
7497
7737
|
async function runStatus(args, io) {
|
|
7498
|
-
const parsed = parseArgs9(args);
|
|
7738
|
+
const parsed = parseArgs9(args, process.cwd(), (l) => io.err(l));
|
|
7499
7739
|
if (typeof parsed === "string") {
|
|
7500
7740
|
io.err(`launchpad status: ${parsed}`);
|
|
7501
|
-
|
|
7741
|
+
printUsage6(io);
|
|
7502
7742
|
return 64;
|
|
7503
7743
|
}
|
|
7504
7744
|
const cfg = loadConfig();
|
|
@@ -7512,34 +7752,36 @@ async function runStatus(args, io) {
|
|
|
7512
7752
|
emit3(lifecycleOutput(parsed.slug, lifecycle), parsed.json, io);
|
|
7513
7753
|
return 0;
|
|
7514
7754
|
}
|
|
7515
|
-
let localYaml;
|
|
7755
|
+
let localYaml = null;
|
|
7516
7756
|
try {
|
|
7517
|
-
localYaml =
|
|
7757
|
+
localYaml = readFileSync12(parsed.file, "utf8");
|
|
7518
7758
|
} catch (e) {
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7759
|
+
if (!isEnoent(e)) {
|
|
7760
|
+
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
|
|
7761
|
+
return 2;
|
|
7522
7762
|
}
|
|
7523
|
-
return 2;
|
|
7524
|
-
}
|
|
7525
|
-
let localObj;
|
|
7526
|
-
try {
|
|
7527
|
-
const { parse: parseYaml5 } = await import("yaml");
|
|
7528
|
-
localObj = parseYaml5(localYaml);
|
|
7529
|
-
} catch (e) {
|
|
7530
|
-
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
|
|
7531
|
-
return 2;
|
|
7532
7763
|
}
|
|
7533
|
-
|
|
7534
|
-
if (
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7764
|
+
let local = null;
|
|
7765
|
+
if (localYaml !== null) {
|
|
7766
|
+
let localObj;
|
|
7767
|
+
try {
|
|
7768
|
+
const { parse: parseYaml6 } = await import("yaml");
|
|
7769
|
+
localObj = parseYaml6(localYaml);
|
|
7770
|
+
} catch (e) {
|
|
7771
|
+
io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
|
|
7772
|
+
return 2;
|
|
7538
7773
|
}
|
|
7539
|
-
|
|
7774
|
+
const localParse = parseManifest(localObj);
|
|
7775
|
+
if (localParse.kind !== "ok") {
|
|
7776
|
+
io.err(`launchpad status: ${parsed.file} failed schema validation:`);
|
|
7777
|
+
for (const issue of localParse.issues) {
|
|
7778
|
+
io.err(` - ${issue.path}: ${issue.message}`);
|
|
7779
|
+
}
|
|
7780
|
+
return 2;
|
|
7781
|
+
}
|
|
7782
|
+
local = localParse.manifest;
|
|
7783
|
+
warnSecretShape(local.production_env, io);
|
|
7540
7784
|
}
|
|
7541
|
-
const local = localParse.manifest;
|
|
7542
|
-
warnSecretShape(local.production_env, io);
|
|
7543
7785
|
let state;
|
|
7544
7786
|
try {
|
|
7545
7787
|
state = await fetchManifestState(cfg, parsed.slug, { includeManifest: true });
|
|
@@ -7557,6 +7799,15 @@ async function runStatus(args, io) {
|
|
|
7557
7799
|
}
|
|
7558
7800
|
io.err(`launchpad status: live deployment state unavailable (${describe24(e)}) — ` + `the report below is from the platform manifest view only.`);
|
|
7559
7801
|
}
|
|
7802
|
+
let standingExceptions = null;
|
|
7803
|
+
try {
|
|
7804
|
+
standingExceptions = await fetchStandingExceptions(cfg, parsed.slug);
|
|
7805
|
+
} catch (e) {
|
|
7806
|
+
if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
|
|
7807
|
+
return mapBotError(e, parsed.slug, io);
|
|
7808
|
+
}
|
|
7809
|
+
io.err(`launchpad status: standing-exception inventory unavailable (${describe24(e)}).`);
|
|
7810
|
+
}
|
|
7560
7811
|
if (state.manifestYaml === null || state.manifestYaml === undefined) {
|
|
7561
7812
|
const live = deployment?.liveDeployment ?? null;
|
|
7562
7813
|
const contentIsLive = live !== null && (live.buildStatus === "success" || deployment?.lastSuccessfulDeployment != null);
|
|
@@ -7564,21 +7815,47 @@ async function runStatus(args, io) {
|
|
|
7564
7815
|
const out = {
|
|
7565
7816
|
state: contentIsLive ? "live_content_untracked" : liveButEmpty ? "live_no_content" : "no_deployed_manifest",
|
|
7566
7817
|
slug: parsed.slug,
|
|
7818
|
+
...local === null ? { drift: null } : {},
|
|
7819
|
+
deployedSha: state.lastAppliedManifestSha,
|
|
7820
|
+
headSha: state.appRepoHeadSha,
|
|
7821
|
+
hasOpenPr: state.openPr !== null,
|
|
7822
|
+
openPrNumber: state.openPr?.number ?? null,
|
|
7823
|
+
driftFields: [],
|
|
7824
|
+
driftDetails: [],
|
|
7825
|
+
...deploymentKnown ? { deployment } : {},
|
|
7826
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
7827
|
+
};
|
|
7828
|
+
emit3(out, parsed.json, io);
|
|
7829
|
+
return 0;
|
|
7830
|
+
}
|
|
7831
|
+
if (local === null) {
|
|
7832
|
+
const out = {
|
|
7833
|
+
state: "live_drift_unknown",
|
|
7834
|
+
slug: parsed.slug,
|
|
7835
|
+
drift: null,
|
|
7567
7836
|
deployedSha: state.lastAppliedManifestSha,
|
|
7568
7837
|
headSha: state.appRepoHeadSha,
|
|
7569
7838
|
hasOpenPr: state.openPr !== null,
|
|
7570
7839
|
openPrNumber: state.openPr?.number ?? null,
|
|
7571
7840
|
driftFields: [],
|
|
7572
7841
|
driftDetails: [],
|
|
7573
|
-
...deploymentKnown ? { deployment } : {}
|
|
7842
|
+
...deploymentKnown ? { deployment } : {},
|
|
7843
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
7574
7844
|
};
|
|
7575
7845
|
emit3(out, parsed.json, io);
|
|
7846
|
+
if (parsed.strict) {
|
|
7847
|
+
if (deployment?.liveDeployment?.buildStatus === "failure") {
|
|
7848
|
+
io.err(`launchpad status: --strict: live build FAILED ` + `(drift not evaluated — no local launchpad.yaml here).`);
|
|
7849
|
+
return 1;
|
|
7850
|
+
}
|
|
7851
|
+
io.err(`launchpad status: --strict: drift not evaluated — no local ` + `launchpad.yaml here; live state looks healthy, exiting 0.`);
|
|
7852
|
+
}
|
|
7576
7853
|
return 0;
|
|
7577
7854
|
}
|
|
7578
7855
|
let deployedObj;
|
|
7579
7856
|
try {
|
|
7580
|
-
const { parse:
|
|
7581
|
-
deployedObj =
|
|
7857
|
+
const { parse: parseYaml6 } = await import("yaml");
|
|
7858
|
+
deployedObj = parseYaml6(state.manifestYaml);
|
|
7582
7859
|
} catch (e) {
|
|
7583
7860
|
io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
|
|
7584
7861
|
return 2;
|
|
@@ -7602,7 +7879,8 @@ async function runStatus(args, io) {
|
|
|
7602
7879
|
openPrNumber: state.openPr?.number ?? null,
|
|
7603
7880
|
driftFields: drift.map((d) => d.path),
|
|
7604
7881
|
driftDetails: drift,
|
|
7605
|
-
...deploymentKnown ? { deployment } : {}
|
|
7882
|
+
...deploymentKnown ? { deployment } : {},
|
|
7883
|
+
...standingExceptions !== null ? { standingExceptions } : {}
|
|
7606
7884
|
};
|
|
7607
7885
|
emit3(result, parsed.json, io);
|
|
7608
7886
|
if (result.state === "drift" && parsed.strict) {
|
|
@@ -7612,17 +7890,17 @@ async function runStatus(args, io) {
|
|
|
7612
7890
|
}
|
|
7613
7891
|
function computeDrift(local, deployed) {
|
|
7614
7892
|
const diffs = [];
|
|
7615
|
-
const cmp = (
|
|
7893
|
+
const cmp = (path11, l, d) => {
|
|
7616
7894
|
const li = l ?? null;
|
|
7617
7895
|
const di = d ?? null;
|
|
7618
7896
|
if (typeof li === "object" || typeof di === "object") {
|
|
7619
7897
|
if (JSON.stringify(li) !== JSON.stringify(di)) {
|
|
7620
|
-
diffs.push({ path:
|
|
7898
|
+
diffs.push({ path: path11, local: l, deployed: d });
|
|
7621
7899
|
}
|
|
7622
7900
|
return;
|
|
7623
7901
|
}
|
|
7624
7902
|
if (li !== di) {
|
|
7625
|
-
diffs.push({ path:
|
|
7903
|
+
diffs.push({ path: path11, local: l, deployed: d });
|
|
7626
7904
|
}
|
|
7627
7905
|
};
|
|
7628
7906
|
cmp("metadata.name", local.metadata.name, deployed.metadata.name);
|
|
@@ -7687,23 +7965,37 @@ function emit3(out, asJson, io) {
|
|
|
7687
7965
|
return;
|
|
7688
7966
|
case "live_no_content":
|
|
7689
7967
|
io.out(`${out.slug}: live — no content deployed yet. Run \`launchpad deploy\`.`);
|
|
7968
|
+
surfaceNoLocalManifestNote(out, io);
|
|
7690
7969
|
surfaceHeadVsDeployed(out, io);
|
|
7691
7970
|
surfaceDeployment(out, io);
|
|
7971
|
+
surfaceExceptions(out, io);
|
|
7692
7972
|
return;
|
|
7693
7973
|
case "no_deployed_manifest":
|
|
7694
7974
|
io.out(`${out.slug}: no deployed manifest yet — run \`launchpad deploy\`.`);
|
|
7975
|
+
surfaceNoLocalManifestNote(out, io);
|
|
7695
7976
|
surfaceHeadVsDeployed(out, io);
|
|
7696
7977
|
surfaceDeployment(out, io);
|
|
7978
|
+
surfaceExceptions(out, io);
|
|
7697
7979
|
return;
|
|
7698
7980
|
case "live_content_untracked":
|
|
7699
7981
|
io.out(`${out.slug}: live — content deployed via ` + `${triggerLabel(out.deployment?.liveDeployment?.trigger)} (no platform-tracked manifest; this app deploys outside \`launchpad deploy\`).`);
|
|
7982
|
+
surfaceNoLocalManifestNote(out, io);
|
|
7700
7983
|
surfaceHeadVsDeployed(out, io);
|
|
7701
7984
|
surfaceDeployment(out, io);
|
|
7985
|
+
surfaceExceptions(out, io);
|
|
7986
|
+
return;
|
|
7987
|
+
case "live_drift_unknown":
|
|
7988
|
+
io.out(`${out.slug}: live` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
7989
|
+
surfaceNoLocalManifestNote(out, io);
|
|
7990
|
+
surfaceHeadVsDeployed(out, io);
|
|
7991
|
+
surfaceDeployment(out, io);
|
|
7992
|
+
surfaceExceptions(out, io);
|
|
7702
7993
|
return;
|
|
7703
7994
|
case "in_sync":
|
|
7704
7995
|
io.out(`${out.slug}: live, in sync` + (out.deployedSha ? ` (content @ ${out.deployedSha.slice(0, 7)})` : ""));
|
|
7705
7996
|
surfaceHeadVsDeployed(out, io);
|
|
7706
7997
|
surfaceDeployment(out, io);
|
|
7998
|
+
surfaceExceptions(out, io);
|
|
7707
7999
|
return;
|
|
7708
8000
|
case "drift":
|
|
7709
8001
|
io.out(`${out.slug}: live, drift: ${out.driftFields.join(", ")}`);
|
|
@@ -7714,9 +8006,15 @@ function emit3(out, asJson, io) {
|
|
|
7714
8006
|
}
|
|
7715
8007
|
surfaceHeadVsDeployed(out, io);
|
|
7716
8008
|
surfaceDeployment(out, io);
|
|
8009
|
+
surfaceExceptions(out, io);
|
|
7717
8010
|
return;
|
|
7718
8011
|
}
|
|
7719
8012
|
}
|
|
8013
|
+
function surfaceNoLocalManifestNote(out, io) {
|
|
8014
|
+
if (out.drift !== null)
|
|
8015
|
+
return;
|
|
8016
|
+
io.out(" no local launchpad.yaml here — drift not checked " + "(cd into the app directory or pass --file to compare)");
|
|
8017
|
+
}
|
|
7720
8018
|
function triggerLabel(trigger) {
|
|
7721
8019
|
if (trigger === "git-push")
|
|
7722
8020
|
return "git push";
|
|
@@ -7763,6 +8061,16 @@ function surfaceDeployment(out, io) {
|
|
|
7763
8061
|
}
|
|
7764
8062
|
}
|
|
7765
8063
|
}
|
|
8064
|
+
function surfaceExceptions(out, io) {
|
|
8065
|
+
const exceptions = out.standingExceptions;
|
|
8066
|
+
if (exceptions === undefined || exceptions.length === 0)
|
|
8067
|
+
return;
|
|
8068
|
+
io.out(` standing exceptions: ${exceptions.length} policy violation(s) in content already live on main (non-blocking):`);
|
|
8069
|
+
for (const e of exceptions) {
|
|
8070
|
+
io.out(` - ${e.path} [${e.rule}]`);
|
|
8071
|
+
}
|
|
8072
|
+
io.out(" these never block a deploy that doesn't change them, but they are never grandfathered silently — clean them up in a future deploy.");
|
|
8073
|
+
}
|
|
7766
8074
|
function surfaceHeadVsDeployed(out, io) {
|
|
7767
8075
|
if (out.headSha !== null && out.deployedSha !== null && out.headSha !== out.deployedSha) {
|
|
7768
8076
|
const short = (s) => s.slice(0, 7);
|
|
@@ -7794,7 +8102,7 @@ function warnSecretShape(env, io) {
|
|
|
7794
8102
|
}
|
|
7795
8103
|
}
|
|
7796
8104
|
}
|
|
7797
|
-
function parseArgs9(args, cwd = process.cwd()) {
|
|
8105
|
+
function parseArgs9(args, cwd = process.cwd(), warn) {
|
|
7798
8106
|
let slug = null;
|
|
7799
8107
|
let file = "./launchpad.yaml";
|
|
7800
8108
|
let json = false;
|
|
@@ -7841,54 +8149,68 @@ function parseArgs9(args, cwd = process.cwd()) {
|
|
|
7841
8149
|
i += 1;
|
|
7842
8150
|
}
|
|
7843
8151
|
if (slug === null) {
|
|
7844
|
-
const inferred =
|
|
8152
|
+
const inferred = inferSlug({ cwd, file, warn });
|
|
7845
8153
|
if (inferred === null) {
|
|
7846
8154
|
return `slug not provided + cannot infer from cwd. Pass <slug> or --slug <slug>, or cd into a directory named launchpad-app-<slug>.`;
|
|
7847
8155
|
}
|
|
7848
8156
|
slug = inferred;
|
|
7849
8157
|
}
|
|
7850
|
-
if (!
|
|
7851
|
-
return `invalid slug "${slug}" — expected ${
|
|
8158
|
+
if (!SLUG_RE11.test(slug)) {
|
|
8159
|
+
return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
|
|
7852
8160
|
}
|
|
7853
8161
|
return { slug, file, json, strict };
|
|
7854
8162
|
}
|
|
7855
|
-
function
|
|
8163
|
+
function printUsage6(io) {
|
|
7856
8164
|
io.err([
|
|
7857
8165
|
"usage: launchpad status [<slug>] [--slug <slug>] [--file <path>] [--json] [--strict]",
|
|
7858
8166
|
"",
|
|
7859
8167
|
" Compare local launchpad.yaml against the deployed state.",
|
|
7860
8168
|
" No local platform-repo or terraform required.",
|
|
7861
8169
|
"",
|
|
7862
|
-
"
|
|
7863
|
-
"
|
|
8170
|
+
" With no explicit slug, it is inferred from (in order): the",
|
|
8171
|
+
" local manifest's declared slug (./launchpad.yaml or --file),",
|
|
8172
|
+
" then a launchpad-app-<slug>/ directory name. An explicit",
|
|
8173
|
+
" --slug or positional always overrides; when the manifest and",
|
|
8174
|
+
" the directory name disagree, the manifest wins (with a note).",
|
|
8175
|
+
"",
|
|
8176
|
+
" With a known slug but NO local manifest, status degrades to",
|
|
8177
|
+
" the live-truth-only view (lifecycle + deployment; drift not",
|
|
8178
|
+
" checked) and exits 0.",
|
|
7864
8179
|
"",
|
|
7865
8180
|
"Flags:",
|
|
7866
8181
|
" --file <path> local manifest path (default: ./launchpad.yaml).",
|
|
7867
|
-
"
|
|
7868
|
-
"
|
|
8182
|
+
" Also consulted for slug inference; --slug or a",
|
|
8183
|
+
" positional slug still overrides.",
|
|
8184
|
+
" --slug <slug> override inference.",
|
|
7869
8185
|
" --json emit machine-readable JSON to stdout.",
|
|
7870
8186
|
" --strict exit 1 on drift. Default is report-only (exit 0).",
|
|
8187
|
+
" With no local manifest, drift can't be evaluated:",
|
|
8188
|
+
" exit 0 unless the live build itself failed.",
|
|
7871
8189
|
"",
|
|
7872
8190
|
"Exit codes:",
|
|
7873
|
-
" 0 = in sync, OR drift in default (report-only) mode
|
|
7874
|
-
"
|
|
7875
|
-
"
|
|
8191
|
+
" 0 = in sync, OR drift in default (report-only) mode, OR the",
|
|
8192
|
+
" live-truth-only view (no local manifest).",
|
|
8193
|
+
" 1 = drift, when --strict is set (or a failed live build under",
|
|
8194
|
+
" --strict with no local manifest).",
|
|
8195
|
+
" 2 = error (network, auth, unreadable/invalid local manifest, etc.)."
|
|
7876
8196
|
].join(`
|
|
7877
8197
|
`));
|
|
7878
8198
|
}
|
|
7879
8199
|
function describe24(e) {
|
|
7880
8200
|
return e instanceof Error ? e.message : String(e);
|
|
7881
8201
|
}
|
|
8202
|
+
function isEnoent(e) {
|
|
8203
|
+
return typeof e === "object" && e !== null && e.code === "ENOENT";
|
|
8204
|
+
}
|
|
7882
8205
|
|
|
7883
8206
|
// src/commands/review.ts
|
|
7884
|
-
import * as
|
|
8207
|
+
import * as path11 from "node:path";
|
|
7885
8208
|
var reviewCommand = {
|
|
7886
8209
|
name: "review",
|
|
7887
8210
|
summary: "show the review state for a PR (slug-scoped)",
|
|
7888
8211
|
run: runReview
|
|
7889
8212
|
};
|
|
7890
|
-
var
|
|
7891
|
-
var DIRNAME_RE5 = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
8213
|
+
var SLUG_RE12 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
7892
8214
|
async function runReview(args, io) {
|
|
7893
8215
|
const parsed = parseArgs10(args);
|
|
7894
8216
|
if (parsed === null) {
|
|
@@ -7900,16 +8222,16 @@ async function runReview(args, io) {
|
|
|
7900
8222
|
if (parsed.slug !== null) {
|
|
7901
8223
|
slug = parsed.slug;
|
|
7902
8224
|
} else {
|
|
7903
|
-
const inferred =
|
|
8225
|
+
const inferred = inferSlug({ cwd: process.cwd(), warn: (l) => io.err(l) });
|
|
7904
8226
|
if (inferred === null) {
|
|
7905
|
-
io.err(`launchpad review: could not infer slug from cwd (${
|
|
8227
|
+
io.err(`launchpad review: could not infer slug from cwd (${path11.basename(process.cwd())});
|
|
7906
8228
|
` + ` pass --slug <slug>, or cd into a directory named launchpad-app-<slug>.`);
|
|
7907
8229
|
return 64;
|
|
7908
8230
|
}
|
|
7909
8231
|
slug = inferred;
|
|
7910
8232
|
}
|
|
7911
|
-
if (!
|
|
7912
|
-
io.err(`launchpad review: invalid slug "${slug}" — expected ${
|
|
8233
|
+
if (!SLUG_RE12.test(slug)) {
|
|
8234
|
+
io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE12.source}`);
|
|
7913
8235
|
return 64;
|
|
7914
8236
|
}
|
|
7915
8237
|
try {
|
|
@@ -7984,11 +8306,6 @@ function parseArgs10(args) {
|
|
|
7984
8306
|
}
|
|
7985
8307
|
return { slug, prNumber };
|
|
7986
8308
|
}
|
|
7987
|
-
function inferSlugFromCwd5(cwd) {
|
|
7988
|
-
const base = path10.basename(cwd);
|
|
7989
|
-
const m = base.match(DIRNAME_RE5);
|
|
7990
|
-
return m === null ? null : m[1];
|
|
7991
|
-
}
|
|
7992
8309
|
function renderReview(r, io) {
|
|
7993
8310
|
const rv = r.review;
|
|
7994
8311
|
io.out(`Review of PR #${rv.prNumber} (${rv.slug})`);
|
|
@@ -8040,7 +8357,7 @@ function describe25(e) {
|
|
|
8040
8357
|
}
|
|
8041
8358
|
|
|
8042
8359
|
// src/deploy/rollback.ts
|
|
8043
|
-
import { existsSync as
|
|
8360
|
+
import { existsSync as existsSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8044
8361
|
import { resolve as resolvePath2 } from "node:path";
|
|
8045
8362
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
8046
8363
|
|
|
@@ -8160,19 +8477,20 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
8160
8477
|
file: opts.file,
|
|
8161
8478
|
platformRepo: null,
|
|
8162
8479
|
rePin: false,
|
|
8480
|
+
atSha: verifiedSha,
|
|
8163
8481
|
yes: true
|
|
8164
8482
|
}, io, applyDeps);
|
|
8165
8483
|
}
|
|
8166
|
-
function readCurrentManifest(
|
|
8167
|
-
if (!
|
|
8484
|
+
function readCurrentManifest(path12) {
|
|
8485
|
+
if (!existsSync8(path12))
|
|
8168
8486
|
return null;
|
|
8169
8487
|
let raw;
|
|
8170
8488
|
try {
|
|
8171
|
-
raw =
|
|
8489
|
+
raw = readFileSync13(path12, "utf8");
|
|
8172
8490
|
} catch {
|
|
8173
8491
|
return null;
|
|
8174
8492
|
}
|
|
8175
|
-
const parsed = parseManifest2(raw,
|
|
8493
|
+
const parsed = parseManifest2(raw, path12);
|
|
8176
8494
|
if (parsed.kind !== "ok")
|
|
8177
8495
|
return null;
|
|
8178
8496
|
return summarise(parsed.manifest);
|
|
@@ -8324,7 +8642,7 @@ function parseArgs11(args) {
|
|
|
8324
8642
|
}
|
|
8325
8643
|
|
|
8326
8644
|
// src/secrets/push.ts
|
|
8327
|
-
import { existsSync as
|
|
8645
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14 } from "node:fs";
|
|
8328
8646
|
import { resolve as resolvePath3 } from "node:path";
|
|
8329
8647
|
|
|
8330
8648
|
// src/secrets/env-parse.ts
|
|
@@ -8379,14 +8697,14 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
8379
8697
|
return 0;
|
|
8380
8698
|
}
|
|
8381
8699
|
const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
|
|
8382
|
-
if (!
|
|
8700
|
+
if (!existsSync9(envPath)) {
|
|
8383
8701
|
io.err(`launchpad secrets push: ${envPath}`);
|
|
8384
8702
|
io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
|
|
8385
8703
|
return 2;
|
|
8386
8704
|
}
|
|
8387
8705
|
let envText;
|
|
8388
8706
|
try {
|
|
8389
|
-
envText =
|
|
8707
|
+
envText = readFileSync14(envPath, "utf8");
|
|
8390
8708
|
} catch (e) {
|
|
8391
8709
|
io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
|
|
8392
8710
|
return 2;
|
|
@@ -8658,32 +8976,32 @@ function renderManifestError5(result, io) {
|
|
|
8658
8976
|
}
|
|
8659
8977
|
|
|
8660
8978
|
// src/secrets/set.ts
|
|
8661
|
-
import { existsSync as
|
|
8979
|
+
import { existsSync as existsSync10, readFileSync as readFileSync15 } from "node:fs";
|
|
8662
8980
|
import { resolve as resolvePath5 } from "node:path";
|
|
8663
|
-
import { parse as
|
|
8981
|
+
import { parse as parseYaml6 } from "yaml";
|
|
8664
8982
|
var CELL_LABEL2 = {
|
|
8665
8983
|
present: "PRESENT",
|
|
8666
8984
|
missing: "MISSING",
|
|
8667
8985
|
not_deployed: "NOT_DEPLOYED"
|
|
8668
8986
|
};
|
|
8669
8987
|
function loadSet(fleetFile, setName, io) {
|
|
8670
|
-
const
|
|
8671
|
-
if (!
|
|
8672
|
-
io.err(`✗ ${
|
|
8988
|
+
const path12 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
8989
|
+
if (!existsSync10(path12)) {
|
|
8990
|
+
io.err(`✗ ${path12}`);
|
|
8673
8991
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
8674
8992
|
return 2;
|
|
8675
8993
|
}
|
|
8676
8994
|
let obj;
|
|
8677
8995
|
try {
|
|
8678
|
-
obj =
|
|
8996
|
+
obj = parseYaml6(readFileSync15(path12, "utf8"));
|
|
8679
8997
|
} catch (e) {
|
|
8680
|
-
io.err(`✗ ${
|
|
8998
|
+
io.err(`✗ ${path12}`);
|
|
8681
8999
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
8682
9000
|
return 1;
|
|
8683
9001
|
}
|
|
8684
9002
|
const parsed = parseFleetSecretSets(obj);
|
|
8685
9003
|
if (!parsed.ok) {
|
|
8686
|
-
io.err(`✗ ${
|
|
9004
|
+
io.err(`✗ ${path12}`);
|
|
8687
9005
|
io.err(` ${parsed.issues.length} schema issue(s):`);
|
|
8688
9006
|
for (const i of parsed.issues)
|
|
8689
9007
|
io.err(` ${i.path}: ${i.message}`);
|
|
@@ -8692,7 +9010,7 @@ function loadSet(fleetFile, setName, io) {
|
|
|
8692
9010
|
const set = parsed.manifest.secretSets.find((s) => s.name === setName);
|
|
8693
9011
|
if (set === undefined) {
|
|
8694
9012
|
const names = parsed.manifest.secretSets.map((s) => s.name).join(", ");
|
|
8695
|
-
io.err(`✗ secret-set "${setName}" not found in ${
|
|
9013
|
+
io.err(`✗ secret-set "${setName}" not found in ${path12}`);
|
|
8696
9014
|
io.err(` declared sets: ${names}`);
|
|
8697
9015
|
return 1;
|
|
8698
9016
|
}
|
|
@@ -8760,14 +9078,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
|
|
|
8760
9078
|
if (typeof set === "number")
|
|
8761
9079
|
return set;
|
|
8762
9080
|
const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
|
|
8763
|
-
if (!
|
|
9081
|
+
if (!existsSync10(envPath)) {
|
|
8764
9082
|
io.err(`launchpad secrets push --set: ${envPath} not found.`);
|
|
8765
9083
|
io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
|
|
8766
9084
|
return 2;
|
|
8767
9085
|
}
|
|
8768
9086
|
let envText;
|
|
8769
9087
|
try {
|
|
8770
|
-
envText =
|
|
9088
|
+
envText = readFileSync15(envPath, "utf8");
|
|
8771
9089
|
} catch (e) {
|
|
8772
9090
|
io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
8773
9091
|
return 2;
|
|
@@ -8911,8 +9229,8 @@ function setPushExit(e) {
|
|
|
8911
9229
|
}
|
|
8912
9230
|
|
|
8913
9231
|
// src/commands/secrets-template.ts
|
|
8914
|
-
import { existsSync as
|
|
8915
|
-
import { resolve as
|
|
9232
|
+
import { existsSync as existsSync11, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9233
|
+
import { resolve as resolve10 } from "node:path";
|
|
8916
9234
|
async function runSecretsTemplate(args, io) {
|
|
8917
9235
|
const flags = parseFlags4(args);
|
|
8918
9236
|
if (flags.kind === "usage-error") {
|
|
@@ -8920,7 +9238,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
8920
9238
|
io.err("Usage: launchpad secrets template [--file <path>] [--out <path>] " + "[--stdout] [--force] [--include-platform-managed]");
|
|
8921
9239
|
return 64;
|
|
8922
9240
|
}
|
|
8923
|
-
const manifestPath =
|
|
9241
|
+
const manifestPath = resolve10(process.cwd(), flags.file ?? "launchpad.yaml");
|
|
8924
9242
|
const result = loadManifest(manifestPath);
|
|
8925
9243
|
const renderResult = renderManifest(result, io);
|
|
8926
9244
|
if (renderResult.kind !== "ok") {
|
|
@@ -8934,8 +9252,8 @@ async function runSecretsTemplate(args, io) {
|
|
|
8934
9252
|
}
|
|
8935
9253
|
return 0;
|
|
8936
9254
|
}
|
|
8937
|
-
const outPath =
|
|
8938
|
-
if (
|
|
9255
|
+
const outPath = resolve10(process.cwd(), flags.out);
|
|
9256
|
+
if (existsSync11(outPath) && !flags.force) {
|
|
8939
9257
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
8940
9258
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
8941
9259
|
return 64;
|
|
@@ -9223,8 +9541,8 @@ function printHelp2(io) {
|
|
|
9223
9541
|
|
|
9224
9542
|
// src/commands/skills.ts
|
|
9225
9543
|
import { fileURLToPath } from "node:url";
|
|
9226
|
-
import { dirname as dirname6, join as join11, resolve as
|
|
9227
|
-
import { promises as fs5, existsSync as
|
|
9544
|
+
import { dirname as dirname6, join as join11, resolve as resolve11 } from "node:path";
|
|
9545
|
+
import { promises as fs5, existsSync as existsSync12 } from "node:fs";
|
|
9228
9546
|
import { homedir as homedir2 } from "node:os";
|
|
9229
9547
|
var BUNDLE_PREFIX = "launchpad-";
|
|
9230
9548
|
var BUNDLED_SKILLS = [
|
|
@@ -9291,11 +9609,11 @@ function resolveInstallEnv() {
|
|
|
9291
9609
|
function defaultBundleDir() {
|
|
9292
9610
|
const here = dirname6(fileURLToPath(import.meta.url));
|
|
9293
9611
|
const candidates = [
|
|
9294
|
-
|
|
9295
|
-
|
|
9612
|
+
resolve11(here, "..", "skills"),
|
|
9613
|
+
resolve11(here, "..", "..", "skills")
|
|
9296
9614
|
];
|
|
9297
9615
|
for (const c of candidates) {
|
|
9298
|
-
if (
|
|
9616
|
+
if (existsSync12(join11(c, "launchpad-onboard", "SKILL.md"))) {
|
|
9299
9617
|
return c;
|
|
9300
9618
|
}
|
|
9301
9619
|
}
|
|
@@ -9375,10 +9693,10 @@ async function doList(io) {
|
|
|
9375
9693
|
}
|
|
9376
9694
|
return 0;
|
|
9377
9695
|
}
|
|
9378
|
-
async function readVersion(
|
|
9696
|
+
async function readVersion(path12) {
|
|
9379
9697
|
let text;
|
|
9380
9698
|
try {
|
|
9381
|
-
text = await fs5.readFile(
|
|
9699
|
+
text = await fs5.readFile(path12, "utf8");
|
|
9382
9700
|
} catch {
|
|
9383
9701
|
return null;
|
|
9384
9702
|
}
|
|
@@ -9388,9 +9706,9 @@ async function readVersion(path11) {
|
|
|
9388
9706
|
const m = /^version:\s*(.+?)\s*$/m.exec(front);
|
|
9389
9707
|
return m === null ? null : m[1] ?? null;
|
|
9390
9708
|
}
|
|
9391
|
-
async function isDir(
|
|
9709
|
+
async function isDir(path12) {
|
|
9392
9710
|
try {
|
|
9393
|
-
const stat = await fs5.stat(
|
|
9711
|
+
const stat = await fs5.stat(path12);
|
|
9394
9712
|
return stat.isDirectory();
|
|
9395
9713
|
} catch {
|
|
9396
9714
|
return false;
|
|
@@ -9404,9 +9722,9 @@ function describe28(e) {
|
|
|
9404
9722
|
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
9405
9723
|
import { promisify } from "node:util";
|
|
9406
9724
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9407
|
-
import { dirname as dirname7, resolve as
|
|
9725
|
+
import { dirname as dirname7, resolve as resolve12, relative as relative4, isAbsolute as isAbsolute2, join as join12 } from "node:path";
|
|
9408
9726
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
9409
|
-
import { readFileSync as
|
|
9727
|
+
import { readFileSync as readFileSync16, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
9410
9728
|
|
|
9411
9729
|
// src/commands/channel-auth.ts
|
|
9412
9730
|
import { createServer as createServer2 } from "node:http";
|
|
@@ -9449,9 +9767,9 @@ async function startLoopback(state, timeoutMs) {
|
|
|
9449
9767
|
resolveCode(code);
|
|
9450
9768
|
}
|
|
9451
9769
|
});
|
|
9452
|
-
const bound = await new Promise((
|
|
9453
|
-
server.once("error", () =>
|
|
9454
|
-
server.listen(0, "127.0.0.1", () =>
|
|
9770
|
+
const bound = await new Promise((resolve12) => {
|
|
9771
|
+
server.once("error", () => resolve12(false));
|
|
9772
|
+
server.listen(0, "127.0.0.1", () => resolve12(true));
|
|
9455
9773
|
});
|
|
9456
9774
|
if (!bound)
|
|
9457
9775
|
return null;
|
|
@@ -9635,7 +9953,7 @@ function resolveLatestVersion() {
|
|
|
9635
9953
|
}
|
|
9636
9954
|
function detectInstallChannel() {
|
|
9637
9955
|
try {
|
|
9638
|
-
return
|
|
9956
|
+
return readFileSync16(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
|
|
9639
9957
|
} catch {
|
|
9640
9958
|
return "github";
|
|
9641
9959
|
}
|
|
@@ -9698,7 +10016,7 @@ function errStderr(e) {
|
|
|
9698
10016
|
return "";
|
|
9699
10017
|
}
|
|
9700
10018
|
async function detectPackageManager2() {
|
|
9701
|
-
const pkgRoot =
|
|
10019
|
+
const pkgRoot = resolve12(dirname7(fileURLToPath2(import.meta.url)), "..", "..");
|
|
9702
10020
|
const candidates = [];
|
|
9703
10021
|
const npmRoot = await pmRoot("npm");
|
|
9704
10022
|
if (npmRoot !== null)
|
|
@@ -9706,7 +10024,7 @@ async function detectPackageManager2() {
|
|
|
9706
10024
|
const pnpmRoot = await pmRoot("pnpm");
|
|
9707
10025
|
if (pnpmRoot !== null)
|
|
9708
10026
|
candidates.push(["pnpm", pnpmRoot]);
|
|
9709
|
-
candidates.push(["bun",
|
|
10027
|
+
candidates.push(["bun", resolve12(homedir3(), ".bun/install/global/node_modules")]);
|
|
9710
10028
|
const matches = candidates.filter(([, root]) => pathContains(root, pkgRoot));
|
|
9711
10029
|
return matches.length === 1 ? matches[0][0] : null;
|
|
9712
10030
|
}
|
|
@@ -9842,8 +10160,8 @@ function printHelp4(io) {
|
|
|
9842
10160
|
}
|
|
9843
10161
|
|
|
9844
10162
|
// src/commands/validate.ts
|
|
9845
|
-
import { readFileSync as
|
|
9846
|
-
import { dirname as dirname8, resolve as
|
|
10163
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
10164
|
+
import { dirname as dirname8, resolve as resolve13 } from "node:path";
|
|
9847
10165
|
var validateCommand = {
|
|
9848
10166
|
name: "validate",
|
|
9849
10167
|
summary: "validate launchpad.yaml against the v1alpha1 schema",
|
|
@@ -9857,12 +10175,12 @@ async function runValidate(args, io) {
|
|
|
9857
10175
|
return 64;
|
|
9858
10176
|
}
|
|
9859
10177
|
const { file, json, strictGroups } = parseResult;
|
|
9860
|
-
const
|
|
9861
|
-
const result = loadManifest(
|
|
10178
|
+
const path12 = resolve13(process.cwd(), file ?? "launchpad.yaml");
|
|
10179
|
+
const result = loadManifest(path12);
|
|
9862
10180
|
if (result.kind !== "ok") {
|
|
9863
10181
|
return json ? renderJsonError(result, io) : renderHumanError(result, io);
|
|
9864
10182
|
}
|
|
9865
|
-
const boundary = checkBoundary(
|
|
10183
|
+
const boundary = checkBoundary(path12, result.manifest.app !== undefined);
|
|
9866
10184
|
const groupCheck = strictGroups ? await checkGroups(allowedEntraGroups(result.manifest.access)) : { kind: "skipped" };
|
|
9867
10185
|
return json ? renderJsonOk(result, groupCheck, boundary, io) : renderHumanOk(result, groupCheck, boundary, io);
|
|
9868
10186
|
}
|
|
@@ -9876,7 +10194,7 @@ function checkBoundary(manifestPath, declared) {
|
|
|
9876
10194
|
}
|
|
9877
10195
|
let manifestYaml;
|
|
9878
10196
|
try {
|
|
9879
|
-
manifestYaml =
|
|
10197
|
+
manifestYaml = readFileSync17(manifestPath, "utf8");
|
|
9880
10198
|
} catch {
|
|
9881
10199
|
manifestYaml = null;
|
|
9882
10200
|
}
|
|
@@ -10191,14 +10509,14 @@ function describe30(e) {
|
|
|
10191
10509
|
import { spawn as spawn6 } from "node:child_process";
|
|
10192
10510
|
import { homedir as homedir4 } from "node:os";
|
|
10193
10511
|
import { join as join13 } from "node:path";
|
|
10194
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
10512
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "node:fs";
|
|
10195
10513
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
10196
10514
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10197
10515
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
10198
10516
|
var CACHE_FILE = join13(homedir4(), ".launchpad", "update-check.json");
|
|
10199
10517
|
function readCache2() {
|
|
10200
10518
|
try {
|
|
10201
|
-
const raw = JSON.parse(
|
|
10519
|
+
const raw = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
|
|
10202
10520
|
if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
|
|
10203
10521
|
const latest = raw.latest;
|
|
10204
10522
|
return {
|
|
@@ -10314,6 +10632,7 @@ var COMMANDS = [
|
|
|
10314
10632
|
generateCommand,
|
|
10315
10633
|
pullCommand,
|
|
10316
10634
|
statusCommand,
|
|
10635
|
+
recoverCommand,
|
|
10317
10636
|
destroyCommand,
|
|
10318
10637
|
rollbackCommand,
|
|
10319
10638
|
groupsCommand,
|