@m-kopa/launchpad-cli 0.27.3 → 0.27.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/cli.js +516 -123
- package/dist/commands/deploy-flags.d.ts.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/editors.d.ts +16 -0
- package/dist/commands/editors.d.ts.map +1 -0
- package/dist/deploy/served-verify.d.ts +31 -0
- package/dist/deploy/served-verify.d.ts.map +1 -0
- package/dist/deploy/verify-deploy.d.ts +25 -0
- package/dist/deploy/verify-deploy.d.ts.map +1 -0
- package/dist/deploy/wait-for-deployment.d.ts +37 -0
- package/dist/deploy/wait-for-deployment.d.ts.map +1 -0
- 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 +1 -1
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +1 -1
- package/skills/launchpad-status/SKILL.md +1 -1
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.27.
|
|
22
|
+
var CLI_VERSION = "0.27.5";
|
|
23
23
|
|
|
24
24
|
// src/config.ts
|
|
25
25
|
import * as os from "node:os";
|
|
@@ -1525,7 +1525,7 @@ function describe10(e) {
|
|
|
1525
1525
|
}
|
|
1526
1526
|
|
|
1527
1527
|
// src/commands/deploy.ts
|
|
1528
|
-
import { existsSync as
|
|
1528
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1529
1529
|
import * as path8 from "node:path";
|
|
1530
1530
|
|
|
1531
1531
|
// src/bundle/orchestrate.ts
|
|
@@ -3616,9 +3616,212 @@ async function bundleAndDeploy(args) {
|
|
|
3616
3616
|
};
|
|
3617
3617
|
}
|
|
3618
3618
|
|
|
3619
|
+
// src/deploy/verify-deploy.ts
|
|
3620
|
+
import { readFileSync as readFileSync5, existsSync as existsSync3 } from "node:fs";
|
|
3621
|
+
import { join as join8 } from "node:path";
|
|
3622
|
+
|
|
3623
|
+
// src/deploy/deployment-status.ts
|
|
3624
|
+
async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
|
|
3625
|
+
let raw;
|
|
3626
|
+
try {
|
|
3627
|
+
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/exceptions` }, fetcher);
|
|
3628
|
+
} catch (e) {
|
|
3629
|
+
if (e instanceof NotFoundError)
|
|
3630
|
+
return null;
|
|
3631
|
+
throw e;
|
|
3632
|
+
}
|
|
3633
|
+
if (!Array.isArray(raw.exceptions))
|
|
3634
|
+
return null;
|
|
3635
|
+
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");
|
|
3636
|
+
}
|
|
3637
|
+
async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
3638
|
+
let raw;
|
|
3639
|
+
try {
|
|
3640
|
+
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/deployment-status` }, fetcher);
|
|
3641
|
+
} catch (e) {
|
|
3642
|
+
if (e instanceof NotFoundError)
|
|
3643
|
+
return null;
|
|
3644
|
+
throw e;
|
|
3645
|
+
}
|
|
3646
|
+
if (typeof raw.supported !== "boolean")
|
|
3647
|
+
return null;
|
|
3648
|
+
return {
|
|
3649
|
+
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
3650
|
+
supported: raw.supported,
|
|
3651
|
+
liveDeployment: raw.liveDeployment ?? null,
|
|
3652
|
+
lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
// src/deploy/wait-for-deployment.ts
|
|
3657
|
+
var DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
3658
|
+
function realWaitDeps(cfg) {
|
|
3659
|
+
return {
|
|
3660
|
+
fetchStatus: (slug) => fetchDeploymentStatus(cfg, slug),
|
|
3661
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
3662
|
+
now: () => Date.now()
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
async function waitForDeployment(args, deps) {
|
|
3666
|
+
const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
3667
|
+
const start = deps.now();
|
|
3668
|
+
for (;; ) {
|
|
3669
|
+
let status;
|
|
3670
|
+
try {
|
|
3671
|
+
status = await deps.fetchStatus(args.slug);
|
|
3672
|
+
} catch {
|
|
3673
|
+
return { kind: "unknown" };
|
|
3674
|
+
}
|
|
3675
|
+
if (status === null)
|
|
3676
|
+
return { kind: "unknown" };
|
|
3677
|
+
if (!status.supported)
|
|
3678
|
+
return { kind: "unsupported" };
|
|
3679
|
+
const live = status.liveDeployment;
|
|
3680
|
+
if (live) {
|
|
3681
|
+
if (live.buildStatus === "success") {
|
|
3682
|
+
return { kind: "success", deploymentUrl: live.url };
|
|
3683
|
+
}
|
|
3684
|
+
if (live.buildStatus === "failure") {
|
|
3685
|
+
return { kind: "failure", logExcerpt: live.logExcerpt, failedStage: live.failedStage };
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
if (deps.now() - start >= args.timeoutMs)
|
|
3689
|
+
return { kind: "pending" };
|
|
3690
|
+
await deps.sleep(pollIntervalMs);
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// src/deploy/served-verify.ts
|
|
3695
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3696
|
+
function sha256(bytes) {
|
|
3697
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
3698
|
+
}
|
|
3699
|
+
function toBytes(v) {
|
|
3700
|
+
return typeof v === "string" ? new TextEncoder().encode(v) : v;
|
|
3701
|
+
}
|
|
3702
|
+
async function verifyServed(args, deps = { fetch }) {
|
|
3703
|
+
let res;
|
|
3704
|
+
try {
|
|
3705
|
+
res = await deps.fetch(args.servedUrl, { method: "GET" });
|
|
3706
|
+
} catch (e) {
|
|
3707
|
+
return {
|
|
3708
|
+
kind: "unreachable",
|
|
3709
|
+
reason: `could not reach ${args.servedUrl}: ${e instanceof Error ? e.message : String(e)}`
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
if (!res.ok) {
|
|
3713
|
+
return {
|
|
3714
|
+
kind: "unreachable",
|
|
3715
|
+
status: res.status,
|
|
3716
|
+
reason: `served URL returned HTTP ${res.status}`
|
|
3717
|
+
};
|
|
3718
|
+
}
|
|
3719
|
+
if (args.appType !== "static") {
|
|
3720
|
+
return { kind: "match" };
|
|
3721
|
+
}
|
|
3722
|
+
let servedBytes;
|
|
3723
|
+
try {
|
|
3724
|
+
servedBytes = new Uint8Array(await res.arrayBuffer());
|
|
3725
|
+
} catch (e) {
|
|
3726
|
+
return {
|
|
3727
|
+
kind: "unreachable",
|
|
3728
|
+
reason: `could not read served body: ${e instanceof Error ? e.message : String(e)}`
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
const shippedBytes = toBytes(args.shippedEntrypoint);
|
|
3732
|
+
const servedHash = sha256(servedBytes);
|
|
3733
|
+
const shippedHash = sha256(shippedBytes);
|
|
3734
|
+
if (servedHash === shippedHash)
|
|
3735
|
+
return { kind: "match" };
|
|
3736
|
+
return {
|
|
3737
|
+
kind: "mismatch",
|
|
3738
|
+
servedHash,
|
|
3739
|
+
shippedHash,
|
|
3740
|
+
servedLen: servedBytes.byteLength,
|
|
3741
|
+
shippedLen: shippedBytes.byteLength
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
// src/deploy/verify-deploy.ts
|
|
3746
|
+
var VERIFY_MISMATCH_EXIT = 65;
|
|
3747
|
+
function readLocalEntrypoint(cwd) {
|
|
3748
|
+
for (const rel of ["index.html", join8("static", "index.html")]) {
|
|
3749
|
+
const p = join8(cwd, rel);
|
|
3750
|
+
if (existsSync3(p))
|
|
3751
|
+
return new Uint8Array(readFileSync5(p));
|
|
3752
|
+
}
|
|
3753
|
+
return null;
|
|
3754
|
+
}
|
|
3755
|
+
function realVerifyDeployDeps(cfg) {
|
|
3756
|
+
return {
|
|
3757
|
+
wait: waitForDeployment,
|
|
3758
|
+
verify: verifyServed,
|
|
3759
|
+
readLocalEntrypoint,
|
|
3760
|
+
waitDeps: realWaitDeps(cfg),
|
|
3761
|
+
verifyDeps: { fetch }
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
async function runDeployVerification(args, io, deps) {
|
|
3765
|
+
io.out("");
|
|
3766
|
+
io.out("Verifying the live deployment serves what you shipped …");
|
|
3767
|
+
const settle = await deps.wait({ slug: args.slug, timeoutMs: args.timeoutMs }, deps.waitDeps);
|
|
3768
|
+
switch (settle.kind) {
|
|
3769
|
+
case "unsupported":
|
|
3770
|
+
io.out(" (skipped — this app has no Pages deployment surface)");
|
|
3771
|
+
return 0;
|
|
3772
|
+
case "unknown":
|
|
3773
|
+
io.out(` (couldn't read deployment status — verify manually with \`launchpad status ${args.slug}\`)`);
|
|
3774
|
+
return 0;
|
|
3775
|
+
case "pending":
|
|
3776
|
+
io.out(`⏳ Build still in progress after ${Math.round(args.timeoutMs / 1000)}s — couldn't verify yet.`);
|
|
3777
|
+
io.out(` Re-check the outcome: \`launchpad status ${args.slug}\``);
|
|
3778
|
+
return 0;
|
|
3779
|
+
case "failure":
|
|
3780
|
+
io.err("✗ The Cloudflare Pages build FAILED — your content is NOT live.");
|
|
3781
|
+
if (settle.failedStage)
|
|
3782
|
+
io.err(` failed stage: ${settle.failedStage}`);
|
|
3783
|
+
if (settle.logExcerpt)
|
|
3784
|
+
io.err(` ${settle.logExcerpt}`);
|
|
3785
|
+
io.err(` Full logs: \`launchpad status ${args.slug}\``);
|
|
3786
|
+
return 1;
|
|
3787
|
+
case "success":
|
|
3788
|
+
return verifySuccess(args, settle.deploymentUrl, io, deps);
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
async function verifySuccess(args, deploymentUrl, io, deps) {
|
|
3792
|
+
if (deploymentUrl === null || deploymentUrl.length === 0) {
|
|
3793
|
+
io.out(" (build succeeded but no served URL was reported — verify manually)");
|
|
3794
|
+
return 0;
|
|
3795
|
+
}
|
|
3796
|
+
const entrypoint = deps.readLocalEntrypoint(args.cwd);
|
|
3797
|
+
if (entrypoint === null) {
|
|
3798
|
+
io.out(" (no local index.html found to compare against — skipped)");
|
|
3799
|
+
return 0;
|
|
3800
|
+
}
|
|
3801
|
+
const verdict = await deps.verify({ servedUrl: deploymentUrl, shippedEntrypoint: entrypoint, appType: args.appType }, deps.verifyDeps);
|
|
3802
|
+
switch (verdict.kind) {
|
|
3803
|
+
case "match":
|
|
3804
|
+
io.out("✓ Verified — the live deployment serves exactly what you shipped.");
|
|
3805
|
+
return 0;
|
|
3806
|
+
case "unreachable":
|
|
3807
|
+
io.out(` (couldn't reach the served URL to verify: ${verdict.reason}) — check manually`);
|
|
3808
|
+
return 0;
|
|
3809
|
+
case "mismatch":
|
|
3810
|
+
io.err("");
|
|
3811
|
+
io.err("✗ LIVE SERVES DIFFERENT CONTENT THAN YOU SHIPPED.");
|
|
3812
|
+
io.err(" Your deploy committed, but the served page does not match your local index.html.");
|
|
3813
|
+
io.err(" The app's build likely reads the entrypoint from a different path than your files");
|
|
3814
|
+
io.err(" landed — e.g. it serves `static/index.html` while your content is at the repo root");
|
|
3815
|
+
io.err(" (or vice-versa). This is the silent-revert class; your content did NOT go live.");
|
|
3816
|
+
io.err(` served ${verdict.servedLen}B (sha ${verdict.servedHash.slice(0, 12)}) ≠ shipped ${verdict.shippedLen}B (sha ${verdict.shippedHash.slice(0, 12)})`);
|
|
3817
|
+
io.err(` Check the app's Pages build config; see \`launchpad status ${args.slug}\`.`);
|
|
3818
|
+
return VERIFY_MISMATCH_EXIT;
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3619
3822
|
// src/commands/deploy.ts
|
|
3620
3823
|
import { parse as parseYaml5 } from "yaml";
|
|
3621
|
-
import { readFileSync as
|
|
3824
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3622
3825
|
|
|
3623
3826
|
// src/deploy/git-files.ts
|
|
3624
3827
|
import { spawn as spawn3 } from "node:child_process";
|
|
@@ -3894,6 +4097,19 @@ function parseDeployFlags(args) {
|
|
|
3894
4097
|
i += 2;
|
|
3895
4098
|
continue;
|
|
3896
4099
|
}
|
|
4100
|
+
if (a === "--no-verify") {
|
|
4101
|
+
i += 1;
|
|
4102
|
+
continue;
|
|
4103
|
+
}
|
|
4104
|
+
if (a.startsWith("--verify-timeout=")) {
|
|
4105
|
+
const v = a.slice("--verify-timeout=".length);
|
|
4106
|
+
const n = Number(v);
|
|
4107
|
+
if (!Number.isFinite(n) || n <= 0 || Math.floor(n) !== n) {
|
|
4108
|
+
return `invalid --verify-timeout "${v}" — expected positive integer seconds`;
|
|
4109
|
+
}
|
|
4110
|
+
i += 1;
|
|
4111
|
+
continue;
|
|
4112
|
+
}
|
|
3897
4113
|
return `unknown argument "${a}"`;
|
|
3898
4114
|
}
|
|
3899
4115
|
if (modeFlagsSeen > 1) {
|
|
@@ -4045,16 +4261,16 @@ function parseDeployFlags(args) {
|
|
|
4045
4261
|
}
|
|
4046
4262
|
|
|
4047
4263
|
// src/deploy/apply.ts
|
|
4048
|
-
import { existsSync as
|
|
4049
|
-
import { resolve as resolvePath, join as
|
|
4264
|
+
import { existsSync as existsSync4, rmSync } from "node:fs";
|
|
4265
|
+
import { resolve as resolvePath, join as join9 } from "node:path";
|
|
4050
4266
|
|
|
4051
4267
|
// src/manifest/load.ts
|
|
4052
|
-
import { readFileSync as
|
|
4268
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
4053
4269
|
import { parse as parseYaml3, YAMLParseError } from "yaml";
|
|
4054
4270
|
function loadManifest(path7) {
|
|
4055
4271
|
let raw;
|
|
4056
4272
|
try {
|
|
4057
|
-
raw =
|
|
4273
|
+
raw = readFileSync6(path7, "utf8");
|
|
4058
4274
|
} catch (err) {
|
|
4059
4275
|
const e = err;
|
|
4060
4276
|
if (e.code === "ENOENT") {
|
|
@@ -4276,8 +4492,8 @@ function sleep(ms) {
|
|
|
4276
4492
|
return new Promise((res) => setTimeout(res, ms));
|
|
4277
4493
|
}
|
|
4278
4494
|
function deletePinIfPresent(cfg, slug, io) {
|
|
4279
|
-
const pinPath =
|
|
4280
|
-
if (!
|
|
4495
|
+
const pinPath = join9(cfg.stateDir, slug, "group.json");
|
|
4496
|
+
if (!existsSync4(pinPath))
|
|
4281
4497
|
return;
|
|
4282
4498
|
try {
|
|
4283
4499
|
rmSync(pinPath, { force: true });
|
|
@@ -4710,7 +4926,7 @@ function handleNetworkError(e, io, slug, verb) {
|
|
|
4710
4926
|
|
|
4711
4927
|
// src/commands/infer-slug.ts
|
|
4712
4928
|
import * as path7 from "node:path";
|
|
4713
|
-
import { existsSync as
|
|
4929
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "node:fs";
|
|
4714
4930
|
import { parse as parseYaml4 } from "yaml";
|
|
4715
4931
|
var DIRNAME_RE = /^launchpad-app-([a-z0-9][a-z0-9-]*[a-z0-9])$/;
|
|
4716
4932
|
function inferSlugFromCwd(cwd) {
|
|
@@ -4730,10 +4946,10 @@ function resolveManifestSlug(parsed) {
|
|
|
4730
4946
|
return null;
|
|
4731
4947
|
}
|
|
4732
4948
|
function inferSlugFromManifestFile(manifestPath) {
|
|
4733
|
-
if (!
|
|
4949
|
+
if (!existsSync5(manifestPath))
|
|
4734
4950
|
return null;
|
|
4735
4951
|
try {
|
|
4736
|
-
return resolveManifestSlug(parseYaml4(
|
|
4952
|
+
return resolveManifestSlug(parseYaml4(readFileSync7(manifestPath, "utf8")));
|
|
4737
4953
|
} catch {
|
|
4738
4954
|
return null;
|
|
4739
4955
|
}
|
|
@@ -4800,7 +5016,7 @@ async function runDeploy(args, io) {
|
|
|
4800
5016
|
}
|
|
4801
5017
|
const cwd = process.cwd();
|
|
4802
5018
|
const manifestPath = path8.join(cwd, "launchpad.yaml");
|
|
4803
|
-
if (
|
|
5019
|
+
if (existsSync6(manifestPath)) {
|
|
4804
5020
|
const contentMode = flags.mode.kind === "content" ? flags.mode : { allowStale: false, rebaseOntoHead: false, overrideStale: null };
|
|
4805
5021
|
return runModelADeploy({
|
|
4806
5022
|
cwd,
|
|
@@ -4844,9 +5060,9 @@ async function runDeploy(args, io) {
|
|
|
4844
5060
|
}
|
|
4845
5061
|
let contentManifestYaml = null;
|
|
4846
5062
|
const contentManifestPath = path8.join(process.cwd(), "launchpad.yaml");
|
|
4847
|
-
if (
|
|
5063
|
+
if (existsSync6(contentManifestPath)) {
|
|
4848
5064
|
try {
|
|
4849
|
-
contentManifestYaml =
|
|
5065
|
+
contentManifestYaml = readFileSync8(contentManifestPath, "utf8");
|
|
4850
5066
|
} catch {
|
|
4851
5067
|
contentManifestYaml = null;
|
|
4852
5068
|
}
|
|
@@ -5055,17 +5271,31 @@ function surfaceStaleBlock(body, io) {
|
|
|
5055
5271
|
async function runModelADeploy(args) {
|
|
5056
5272
|
const { cwd, manifestPath, io } = args;
|
|
5057
5273
|
let slug;
|
|
5274
|
+
let appType = "static";
|
|
5058
5275
|
try {
|
|
5059
|
-
const
|
|
5276
|
+
const manifestObj = parseYaml5(readFileSync8(manifestPath, "utf8"));
|
|
5277
|
+
const metaSlug = resolveManifestSlug(manifestObj);
|
|
5060
5278
|
if (metaSlug === null) {
|
|
5061
5279
|
io.err(`launchpad deploy: launchpad.yaml is missing metadata.slug (v2) / metadata.name (v1). ` + `Run \`launchpad init\` again to regenerate the manifest.`);
|
|
5062
5280
|
return 64;
|
|
5063
5281
|
}
|
|
5064
5282
|
slug = metaSlug;
|
|
5283
|
+
const dtype = manifestObj?.deployment?.type;
|
|
5284
|
+
if (dtype === "static" || dtype === "react" || dtype === "react+api" || dtype === "container") {
|
|
5285
|
+
appType = dtype;
|
|
5286
|
+
}
|
|
5065
5287
|
} catch (e) {
|
|
5066
5288
|
io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe13(e)}`);
|
|
5067
5289
|
return 1;
|
|
5068
5290
|
}
|
|
5291
|
+
const noVerify = args.argv.includes("--no-verify");
|
|
5292
|
+
let verifyTimeoutMs = 180000;
|
|
5293
|
+
const vtFlag = args.argv.find((a) => a.startsWith("--verify-timeout="));
|
|
5294
|
+
if (vtFlag !== undefined) {
|
|
5295
|
+
const secs = Number.parseInt(vtFlag.slice("--verify-timeout=".length), 10);
|
|
5296
|
+
if (Number.isFinite(secs) && secs > 0)
|
|
5297
|
+
verifyTimeoutMs = secs * 1000;
|
|
5298
|
+
}
|
|
5069
5299
|
if (!SLUG_RE4.test(slug)) {
|
|
5070
5300
|
io.err(`launchpad deploy: invalid slug "${slug}" in manifest — expected ${SLUG_RE4.source}`);
|
|
5071
5301
|
return 64;
|
|
@@ -5174,11 +5404,11 @@ async function runModelADeploy(args) {
|
|
|
5174
5404
|
}
|
|
5175
5405
|
if (success.status === "provisioning_started") {
|
|
5176
5406
|
const submissionId = typeof success.submissionId === "string" ? success.submissionId : "(missing)";
|
|
5177
|
-
const
|
|
5407
|
+
const appType2 = typeof success.appType === "string" ? success.appType : "(missing)";
|
|
5178
5408
|
const message = typeof success.message === "string" ? success.message : "Bot accepted the upload and started provisioning.";
|
|
5179
5409
|
io.out(`✓ First-time deploy — provisioning workflow started for ${slug}`);
|
|
5180
5410
|
io.out(` submission: ${submissionId}`);
|
|
5181
|
-
io.out(` appType: ${
|
|
5411
|
+
io.out(` appType: ${appType2}`);
|
|
5182
5412
|
io.out("");
|
|
5183
5413
|
io.out(message);
|
|
5184
5414
|
io.out("");
|
|
@@ -5209,8 +5439,11 @@ async function runModelADeploy(args) {
|
|
|
5209
5439
|
surfaceDeployExtras(success, io, slug, args.allowStale);
|
|
5210
5440
|
io.out("");
|
|
5211
5441
|
io.out("Committed; build pending — Cloudflare Pages is building this deploy now.");
|
|
5212
|
-
|
|
5213
|
-
|
|
5442
|
+
if (noVerify) {
|
|
5443
|
+
io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
|
|
5444
|
+
return 0;
|
|
5445
|
+
}
|
|
5446
|
+
return runDeployVerification({ slug, cwd, appType, timeoutMs: verifyTimeoutMs }, io, realVerifyDeployDeps(cfg));
|
|
5214
5447
|
}
|
|
5215
5448
|
}
|
|
5216
5449
|
}
|
|
@@ -5406,7 +5639,7 @@ function describe14(e) {
|
|
|
5406
5639
|
}
|
|
5407
5640
|
|
|
5408
5641
|
// src/commands/generate.ts
|
|
5409
|
-
import { mkdirSync, readFileSync as
|
|
5642
|
+
import { mkdirSync, readFileSync as readFileSync9, writeFileSync } from "node:fs";
|
|
5410
5643
|
import { dirname as dirname5, resolve as resolve8, relative as relative3 } from "node:path";
|
|
5411
5644
|
var generateCommand = {
|
|
5412
5645
|
name: "generate",
|
|
@@ -5494,7 +5727,7 @@ function applyOne(artefact, path10, out, force) {
|
|
|
5494
5727
|
}
|
|
5495
5728
|
function readIfExists(path10) {
|
|
5496
5729
|
try {
|
|
5497
|
-
return { kind: "ok", content:
|
|
5730
|
+
return { kind: "ok", content: readFileSync9(path10, "utf8") };
|
|
5498
5731
|
} catch (err) {
|
|
5499
5732
|
const e = err;
|
|
5500
5733
|
if (e.code === "ENOENT")
|
|
@@ -5705,13 +5938,13 @@ function parseFlags(args) {
|
|
|
5705
5938
|
}
|
|
5706
5939
|
|
|
5707
5940
|
// src/groups/client.ts
|
|
5708
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
5709
|
-
import { dirname as dirname6, join as
|
|
5941
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5942
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
5710
5943
|
var CACHE_TTL_MS = 60 * 60 * 1000;
|
|
5711
5944
|
var CACHE_FILENAME = "groups.json";
|
|
5712
5945
|
async function fetchGroups(cfg, opts = {}) {
|
|
5713
5946
|
const now = opts.now ?? Date.now;
|
|
5714
|
-
const cachePath =
|
|
5947
|
+
const cachePath = join11(cfg.cacheDir, CACHE_FILENAME);
|
|
5715
5948
|
if (opts.forceRefresh !== true) {
|
|
5716
5949
|
const cached = readCache(cachePath);
|
|
5717
5950
|
if (cached !== null) {
|
|
@@ -5749,7 +5982,7 @@ async function fetchGroups(cfg, opts = {}) {
|
|
|
5749
5982
|
function readCache(path10) {
|
|
5750
5983
|
let raw;
|
|
5751
5984
|
try {
|
|
5752
|
-
raw =
|
|
5985
|
+
raw = readFileSync10(path10, "utf8");
|
|
5753
5986
|
} catch {
|
|
5754
5987
|
return null;
|
|
5755
5988
|
}
|
|
@@ -6250,17 +6483,17 @@ function describe18(e) {
|
|
|
6250
6483
|
|
|
6251
6484
|
// src/commands/init.ts
|
|
6252
6485
|
import { createInterface } from "node:readline/promises";
|
|
6253
|
-
import { existsSync as
|
|
6486
|
+
import { existsSync as existsSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "node:fs";
|
|
6254
6487
|
import { resolve as resolve9 } from "node:path";
|
|
6255
6488
|
import { stringify as yamlStringify } from "yaml";
|
|
6256
6489
|
|
|
6257
6490
|
// src/detect/index.ts
|
|
6258
|
-
import { existsSync as
|
|
6259
|
-
import { join as
|
|
6491
|
+
import { existsSync as existsSync7, readFileSync as readFileSync11, statSync } from "node:fs";
|
|
6492
|
+
import { join as join12 } from "node:path";
|
|
6260
6493
|
function detectAppShape(cwd) {
|
|
6261
|
-
const hasPackageJson =
|
|
6262
|
-
const hasAnyLockfile = LOCKFILES.some(({ file }) =>
|
|
6263
|
-
const hasViteConfig = VITE_CONFIG_NAMES.some((n) =>
|
|
6494
|
+
const hasPackageJson = existsSync7(join12(cwd, "package.json"));
|
|
6495
|
+
const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync7(join12(cwd, file)));
|
|
6496
|
+
const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync7(join12(cwd, n)));
|
|
6264
6497
|
if (!hasPackageJson && !hasAnyLockfile && !hasViteConfig) {
|
|
6265
6498
|
return {
|
|
6266
6499
|
kind: "not-applicable",
|
|
@@ -6306,7 +6539,7 @@ var LOCKFILES = [
|
|
|
6306
6539
|
{ file: "yarn.lock", pm: "yarn" }
|
|
6307
6540
|
];
|
|
6308
6541
|
function detectPackageManager(cwd) {
|
|
6309
|
-
const present = LOCKFILES.filter(({ file }) =>
|
|
6542
|
+
const present = LOCKFILES.filter(({ file }) => existsSync7(join12(cwd, file)));
|
|
6310
6543
|
if (present.length === 0) {
|
|
6311
6544
|
return {
|
|
6312
6545
|
kind: "ambiguous",
|
|
@@ -6328,8 +6561,8 @@ function detectPackageManager(cwd) {
|
|
|
6328
6561
|
}
|
|
6329
6562
|
function detectVitePresence(cwd) {
|
|
6330
6563
|
for (const name of VITE_CONFIG_NAMES) {
|
|
6331
|
-
const p =
|
|
6332
|
-
if (
|
|
6564
|
+
const p = join12(cwd, name);
|
|
6565
|
+
if (existsSync7(p)) {
|
|
6333
6566
|
return { kind: "ok", value: { path: p } };
|
|
6334
6567
|
}
|
|
6335
6568
|
}
|
|
@@ -6339,9 +6572,9 @@ function detectVitePresence(cwd) {
|
|
|
6339
6572
|
};
|
|
6340
6573
|
}
|
|
6341
6574
|
function detectAppType(cwd) {
|
|
6342
|
-
const fnDir =
|
|
6575
|
+
const fnDir = join12(cwd, "functions");
|
|
6343
6576
|
let hasFunctionsDir = false;
|
|
6344
|
-
if (
|
|
6577
|
+
if (existsSync7(fnDir)) {
|
|
6345
6578
|
try {
|
|
6346
6579
|
hasFunctionsDir = statSync(fnDir).isDirectory();
|
|
6347
6580
|
} catch {
|
|
@@ -6351,8 +6584,8 @@ function detectAppType(cwd) {
|
|
|
6351
6584
|
return { kind: "ok", value: hasFunctionsDir ? "react+api" : "react" };
|
|
6352
6585
|
}
|
|
6353
6586
|
function detectBuildCommand(cwd, pm) {
|
|
6354
|
-
const pkgJsonPath =
|
|
6355
|
-
if (!
|
|
6587
|
+
const pkgJsonPath = join12(cwd, "package.json");
|
|
6588
|
+
if (!existsSync7(pkgJsonPath)) {
|
|
6356
6589
|
return {
|
|
6357
6590
|
kind: "ambiguous",
|
|
6358
6591
|
reason: "no package.json at repo root. Run your package manager's `init` first."
|
|
@@ -6360,7 +6593,7 @@ function detectBuildCommand(cwd, pm) {
|
|
|
6360
6593
|
}
|
|
6361
6594
|
let pkgJson;
|
|
6362
6595
|
try {
|
|
6363
|
-
pkgJson = JSON.parse(
|
|
6596
|
+
pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf8"));
|
|
6364
6597
|
} catch (e) {
|
|
6365
6598
|
return {
|
|
6366
6599
|
kind: "ambiguous",
|
|
@@ -6395,7 +6628,7 @@ var OUT_DIR_REGEX = /\bbuild\s*:\s*\{[^{}]*?\boutDir\s*:\s*['"]([^'"]+)['"]/s;
|
|
|
6395
6628
|
function detectDestinationDir(cwd, vite) {
|
|
6396
6629
|
let text;
|
|
6397
6630
|
try {
|
|
6398
|
-
text =
|
|
6631
|
+
text = readFileSync11(vite.path, "utf8");
|
|
6399
6632
|
} catch (e) {
|
|
6400
6633
|
return {
|
|
6401
6634
|
kind: "ambiguous",
|
|
@@ -6425,7 +6658,7 @@ async function runInit(args, io, prompt) {
|
|
|
6425
6658
|
}
|
|
6426
6659
|
const { inputs, options } = parsed;
|
|
6427
6660
|
const outPath = resolve9(process.cwd(), options.out);
|
|
6428
|
-
if (
|
|
6661
|
+
if (existsSync8(outPath) && !options.force) {
|
|
6429
6662
|
io.err(`launchpad init: ${outPath} already exists`);
|
|
6430
6663
|
io.err("Pass --force to overwrite.");
|
|
6431
6664
|
return 64;
|
|
@@ -6843,8 +7076,8 @@ function renderYaml(manifest) {
|
|
|
6843
7076
|
}
|
|
6844
7077
|
function ensureGitignoreEntries(path10, entries) {
|
|
6845
7078
|
let current = "";
|
|
6846
|
-
if (
|
|
6847
|
-
current =
|
|
7079
|
+
if (existsSync8(path10)) {
|
|
7080
|
+
current = readFileSync12(path10, "utf8");
|
|
6848
7081
|
}
|
|
6849
7082
|
const lines = current.split(/\r?\n/);
|
|
6850
7083
|
const present = new Set(lines.map((l) => l.trim()));
|
|
@@ -7833,39 +8066,6 @@ async function fetchManifestStatus(cfg, slug, fetcher = fetch) {
|
|
|
7833
8066
|
return apiJson(cfg, { path: path12 }, fetcher);
|
|
7834
8067
|
}
|
|
7835
8068
|
|
|
7836
|
-
// src/deploy/deployment-status.ts
|
|
7837
|
-
async function fetchStandingExceptions(cfg, slug, fetcher = fetch) {
|
|
7838
|
-
let raw;
|
|
7839
|
-
try {
|
|
7840
|
-
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/exceptions` }, fetcher);
|
|
7841
|
-
} catch (e) {
|
|
7842
|
-
if (e instanceof NotFoundError)
|
|
7843
|
-
return null;
|
|
7844
|
-
throw e;
|
|
7845
|
-
}
|
|
7846
|
-
if (!Array.isArray(raw.exceptions))
|
|
7847
|
-
return null;
|
|
7848
|
-
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");
|
|
7849
|
-
}
|
|
7850
|
-
async function fetchDeploymentStatus(cfg, slug, fetcher = fetch) {
|
|
7851
|
-
let raw;
|
|
7852
|
-
try {
|
|
7853
|
-
raw = await apiJson(cfg, { path: `/apps/${encodeURIComponent(slug)}/deployment-status` }, fetcher);
|
|
7854
|
-
} catch (e) {
|
|
7855
|
-
if (e instanceof NotFoundError)
|
|
7856
|
-
return null;
|
|
7857
|
-
throw e;
|
|
7858
|
-
}
|
|
7859
|
-
if (typeof raw.supported !== "boolean")
|
|
7860
|
-
return null;
|
|
7861
|
-
return {
|
|
7862
|
-
slug: typeof raw.slug === "string" ? raw.slug : slug,
|
|
7863
|
-
supported: raw.supported,
|
|
7864
|
-
liveDeployment: raw.liveDeployment ?? null,
|
|
7865
|
-
lastSuccessfulDeployment: raw.lastSuccessfulDeployment ?? null
|
|
7866
|
-
};
|
|
7867
|
-
}
|
|
7868
|
-
|
|
7869
8069
|
// src/commands/pull.ts
|
|
7870
8070
|
var pullCommand = {
|
|
7871
8071
|
name: "pull",
|
|
@@ -8205,7 +8405,7 @@ function printUsage5(io) {
|
|
|
8205
8405
|
}
|
|
8206
8406
|
|
|
8207
8407
|
// src/commands/status.ts
|
|
8208
|
-
import { readFileSync as
|
|
8408
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
8209
8409
|
var statusCommand = {
|
|
8210
8410
|
name: "status",
|
|
8211
8411
|
summary: "show drift between local launchpad.yaml and deployed state",
|
|
@@ -8262,7 +8462,7 @@ async function runStatus(args, io) {
|
|
|
8262
8462
|
}
|
|
8263
8463
|
let localYaml = null;
|
|
8264
8464
|
try {
|
|
8265
|
-
localYaml =
|
|
8465
|
+
localYaml = readFileSync13(parsed.file, "utf8");
|
|
8266
8466
|
} catch (e) {
|
|
8267
8467
|
if (!isEnoent(e)) {
|
|
8268
8468
|
io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe25(e)}`);
|
|
@@ -8865,7 +9065,7 @@ function describe26(e) {
|
|
|
8865
9065
|
}
|
|
8866
9066
|
|
|
8867
9067
|
// src/deploy/rollback.ts
|
|
8868
|
-
import { existsSync as
|
|
9068
|
+
import { existsSync as existsSync9, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8869
9069
|
import { resolve as resolvePath2 } from "node:path";
|
|
8870
9070
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
8871
9071
|
|
|
@@ -8990,11 +9190,11 @@ async function runRollback(opts, io, deps = {}) {
|
|
|
8990
9190
|
}, io, applyDeps);
|
|
8991
9191
|
}
|
|
8992
9192
|
function readCurrentManifest(path13) {
|
|
8993
|
-
if (!
|
|
9193
|
+
if (!existsSync9(path13))
|
|
8994
9194
|
return null;
|
|
8995
9195
|
let raw;
|
|
8996
9196
|
try {
|
|
8997
|
-
raw =
|
|
9197
|
+
raw = readFileSync14(path13, "utf8");
|
|
8998
9198
|
} catch {
|
|
8999
9199
|
return null;
|
|
9000
9200
|
}
|
|
@@ -9150,7 +9350,7 @@ function parseArgs11(args) {
|
|
|
9150
9350
|
}
|
|
9151
9351
|
|
|
9152
9352
|
// src/secrets/push.ts
|
|
9153
|
-
import { existsSync as
|
|
9353
|
+
import { existsSync as existsSync10, readFileSync as readFileSync15 } from "node:fs";
|
|
9154
9354
|
import { resolve as resolvePath3 } from "node:path";
|
|
9155
9355
|
|
|
9156
9356
|
// src/secrets/env-parse.ts
|
|
@@ -9205,14 +9405,14 @@ async function runSecretsPush(opts, io, deps = {}) {
|
|
|
9205
9405
|
return 0;
|
|
9206
9406
|
}
|
|
9207
9407
|
const envPath = resolvePath3(process.cwd(), opts.env ?? ".env");
|
|
9208
|
-
if (!
|
|
9408
|
+
if (!existsSync10(envPath)) {
|
|
9209
9409
|
io.err(`launchpad secrets push: ${envPath}`);
|
|
9210
9410
|
io.err(" .env file not found. Run `launchpad secrets template` to scaffold one.");
|
|
9211
9411
|
return 2;
|
|
9212
9412
|
}
|
|
9213
9413
|
let envText;
|
|
9214
9414
|
try {
|
|
9215
|
-
envText =
|
|
9415
|
+
envText = readFileSync15(envPath, "utf8");
|
|
9216
9416
|
} catch (e) {
|
|
9217
9417
|
io.err(`launchpad secrets push: failed to read ${envPath}: ${describe28(e)}`);
|
|
9218
9418
|
return 2;
|
|
@@ -9484,7 +9684,7 @@ function renderManifestError5(result, io) {
|
|
|
9484
9684
|
}
|
|
9485
9685
|
|
|
9486
9686
|
// src/secrets/set.ts
|
|
9487
|
-
import { existsSync as
|
|
9687
|
+
import { existsSync as existsSync11, readFileSync as readFileSync16 } from "node:fs";
|
|
9488
9688
|
import { resolve as resolvePath5 } from "node:path";
|
|
9489
9689
|
import { parse as parseYaml6 } from "yaml";
|
|
9490
9690
|
var CELL_LABEL2 = {
|
|
@@ -9494,14 +9694,14 @@ var CELL_LABEL2 = {
|
|
|
9494
9694
|
};
|
|
9495
9695
|
function loadSet(fleetFile, setName, io) {
|
|
9496
9696
|
const path13 = resolvePath5(process.cwd(), fleetFile ?? "fleet-secret-sets.yaml");
|
|
9497
|
-
if (!
|
|
9697
|
+
if (!existsSync11(path13)) {
|
|
9498
9698
|
io.err(`✗ ${path13}`);
|
|
9499
9699
|
io.err(" fleet-secret-sets.yaml not found. Run from the platform repo root or pass --fleet-file.");
|
|
9500
9700
|
return 2;
|
|
9501
9701
|
}
|
|
9502
9702
|
let obj;
|
|
9503
9703
|
try {
|
|
9504
|
-
obj = parseYaml6(
|
|
9704
|
+
obj = parseYaml6(readFileSync16(path13, "utf8"));
|
|
9505
9705
|
} catch (e) {
|
|
9506
9706
|
io.err(`✗ ${path13}`);
|
|
9507
9707
|
io.err(` YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -9586,14 +9786,14 @@ async function runSecretsPushSet(opts, io, deps = {}) {
|
|
|
9586
9786
|
if (typeof set === "number")
|
|
9587
9787
|
return set;
|
|
9588
9788
|
const envPath = resolvePath5(process.cwd(), opts.env ?? ".env");
|
|
9589
|
-
if (!
|
|
9789
|
+
if (!existsSync11(envPath)) {
|
|
9590
9790
|
io.err(`launchpad secrets push --set: ${envPath} not found.`);
|
|
9591
9791
|
io.err(` Create a .env carrying the set's secrets: ${set.secrets.join(", ")}`);
|
|
9592
9792
|
return 2;
|
|
9593
9793
|
}
|
|
9594
9794
|
let envText;
|
|
9595
9795
|
try {
|
|
9596
|
-
envText =
|
|
9796
|
+
envText = readFileSync16(envPath, "utf8");
|
|
9597
9797
|
} catch (e) {
|
|
9598
9798
|
io.err(`launchpad secrets push --set: failed to read ${envPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
9599
9799
|
return 2;
|
|
@@ -9737,7 +9937,7 @@ function setPushExit(e) {
|
|
|
9737
9937
|
}
|
|
9738
9938
|
|
|
9739
9939
|
// src/commands/secrets-template.ts
|
|
9740
|
-
import { existsSync as
|
|
9940
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9741
9941
|
import { resolve as resolve11 } from "node:path";
|
|
9742
9942
|
async function runSecretsTemplate(args, io) {
|
|
9743
9943
|
const flags = parseFlags4(args);
|
|
@@ -9761,7 +9961,7 @@ async function runSecretsTemplate(args, io) {
|
|
|
9761
9961
|
return 0;
|
|
9762
9962
|
}
|
|
9763
9963
|
const outPath = resolve11(process.cwd(), flags.out);
|
|
9764
|
-
if (
|
|
9964
|
+
if (existsSync12(outPath) && !flags.force) {
|
|
9765
9965
|
io.err(`launchpad secrets template: ${outPath} already exists`);
|
|
9766
9966
|
io.err("Pass --force to overwrite, or --stdout to print without writing.");
|
|
9767
9967
|
return 64;
|
|
@@ -10049,8 +10249,8 @@ function printHelp2(io) {
|
|
|
10049
10249
|
|
|
10050
10250
|
// src/commands/skills.ts
|
|
10051
10251
|
import { fileURLToPath } from "node:url";
|
|
10052
|
-
import { dirname as dirname7, join as
|
|
10053
|
-
import { promises as fs6, existsSync as
|
|
10252
|
+
import { dirname as dirname7, join as join13, resolve as resolve12 } from "node:path";
|
|
10253
|
+
import { promises as fs6, existsSync as existsSync13 } from "node:fs";
|
|
10054
10254
|
import { homedir as homedir2 } from "node:os";
|
|
10055
10255
|
var BUNDLE_PREFIX = "launchpad-";
|
|
10056
10256
|
var BUNDLED_SKILLS = [
|
|
@@ -10111,7 +10311,7 @@ function printHelp3(io) {
|
|
|
10111
10311
|
}
|
|
10112
10312
|
function resolveInstallEnv() {
|
|
10113
10313
|
const bundleDir = process.env.LAUNCHPAD_SKILLS_BUNDLE_DIR ?? defaultBundleDir();
|
|
10114
|
-
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
10314
|
+
const userSkillsDir = process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join13(homedir2(), ".claude", "skills");
|
|
10115
10315
|
return { bundleDir, userSkillsDir };
|
|
10116
10316
|
}
|
|
10117
10317
|
function defaultBundleDir() {
|
|
@@ -10121,7 +10321,7 @@ function defaultBundleDir() {
|
|
|
10121
10321
|
resolve12(here, "..", "..", "skills")
|
|
10122
10322
|
];
|
|
10123
10323
|
for (const c of candidates) {
|
|
10124
|
-
if (
|
|
10324
|
+
if (existsSync13(join13(c, "launchpad-onboard", "SKILL.md"))) {
|
|
10125
10325
|
return c;
|
|
10126
10326
|
}
|
|
10127
10327
|
}
|
|
@@ -10142,12 +10342,12 @@ async function doInstall(io) {
|
|
|
10142
10342
|
}
|
|
10143
10343
|
let installed = 0;
|
|
10144
10344
|
for (const skill of BUNDLED_SKILLS) {
|
|
10145
|
-
const src =
|
|
10345
|
+
const src = join13(env.bundleDir, skill);
|
|
10146
10346
|
if (!await isDir(src)) {
|
|
10147
10347
|
io.err(`launchpad skills install: bundled skill "${skill}" missing from ${env.bundleDir} — package is incomplete.`);
|
|
10148
10348
|
return 1;
|
|
10149
10349
|
}
|
|
10150
|
-
const dest =
|
|
10350
|
+
const dest = join13(env.userSkillsDir, skill);
|
|
10151
10351
|
await fs6.rm(dest, { recursive: true, force: true });
|
|
10152
10352
|
await fs6.cp(src, dest, { recursive: true });
|
|
10153
10353
|
installed++;
|
|
@@ -10172,7 +10372,7 @@ async function doUninstall(io) {
|
|
|
10172
10372
|
continue;
|
|
10173
10373
|
if (!isBundleManaged(entry.name))
|
|
10174
10374
|
continue;
|
|
10175
|
-
const target =
|
|
10375
|
+
const target = join13(env.userSkillsDir, entry.name);
|
|
10176
10376
|
await fs6.rm(target, { recursive: true, force: true });
|
|
10177
10377
|
removed++;
|
|
10178
10378
|
io.out(`✗ removed ${target}`);
|
|
@@ -10195,7 +10395,7 @@ async function doList(io) {
|
|
|
10195
10395
|
}
|
|
10196
10396
|
const width = managedDirs.reduce((n, s) => Math.max(n, s.length), 0);
|
|
10197
10397
|
for (const name of managedDirs) {
|
|
10198
|
-
const skillFile =
|
|
10398
|
+
const skillFile = join13(env.userSkillsDir, name, "SKILL.md");
|
|
10199
10399
|
const version = await readVersion(skillFile);
|
|
10200
10400
|
io.out(` ${name.padEnd(width + 2)}${version ?? "(no version)"}`);
|
|
10201
10401
|
}
|
|
@@ -10230,13 +10430,13 @@ function describe29(e) {
|
|
|
10230
10430
|
import { execFile, spawn as spawn5 } from "node:child_process";
|
|
10231
10431
|
import { promisify } from "node:util";
|
|
10232
10432
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10233
|
-
import { dirname as dirname8, resolve as resolve13, relative as relative4, isAbsolute as isAbsolute2, join as
|
|
10433
|
+
import { dirname as dirname8, resolve as resolve13, relative as relative4, isAbsolute as isAbsolute2, join as join14 } from "node:path";
|
|
10234
10434
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
10235
|
-
import { readFileSync as
|
|
10435
|
+
import { readFileSync as readFileSync17, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
|
|
10236
10436
|
|
|
10237
10437
|
// src/commands/channel-auth.ts
|
|
10238
10438
|
import { createServer as createServer2 } from "node:http";
|
|
10239
|
-
import { createHash as
|
|
10439
|
+
import { createHash as createHash3, randomBytes as randomBytes5 } from "node:crypto";
|
|
10240
10440
|
var CHANNEL_BASE = "https://get.launchpad.m-kopa.us";
|
|
10241
10441
|
var CLI_AUTH_URL = `${CHANNEL_BASE}/__cli_auth`;
|
|
10242
10442
|
var CLI_TOKEN_URL = `${CHANNEL_BASE}/__cli_token`;
|
|
@@ -10247,7 +10447,7 @@ function base64url(b) {
|
|
|
10247
10447
|
}
|
|
10248
10448
|
function pkcePair() {
|
|
10249
10449
|
const verifier = base64url(randomBytes5(32));
|
|
10250
|
-
const challenge = base64url(
|
|
10450
|
+
const challenge = base64url(createHash3("sha256").update(verifier).digest());
|
|
10251
10451
|
return { verifier, challenge };
|
|
10252
10452
|
}
|
|
10253
10453
|
async function startLoopback(state, timeoutMs) {
|
|
@@ -10353,7 +10553,7 @@ var PKG = "@m-kopa/launchpad-cli";
|
|
|
10353
10553
|
var REGISTRY = "https://registry.npmjs.org";
|
|
10354
10554
|
var CHANNEL_VERSION_URL = "https://get.launchpad.m-kopa.us/version.json";
|
|
10355
10555
|
var CHANNEL_INSTALL_URL = "https://get.launchpad.m-kopa.us";
|
|
10356
|
-
var CHANNEL_MARKER =
|
|
10556
|
+
var CHANNEL_MARKER = join14(homedir3(), ".launchpad", "channel");
|
|
10357
10557
|
var EXIT_UPDATE_AVAILABLE = 10;
|
|
10358
10558
|
var UPGRADE_ARGS = {
|
|
10359
10559
|
npm: ["install", "-g", `${PKG}@latest`],
|
|
@@ -10463,8 +10663,8 @@ async function openSystemBrowser(url) {
|
|
|
10463
10663
|
await execFileAsync(opener, [url]);
|
|
10464
10664
|
}
|
|
10465
10665
|
async function runInstallerScript(script) {
|
|
10466
|
-
const dir = mkdtempSync(
|
|
10467
|
-
const file =
|
|
10666
|
+
const dir = mkdtempSync(join14(tmpdir(), "launchpad-update-"));
|
|
10667
|
+
const file = join14(dir, "install.sh");
|
|
10468
10668
|
try {
|
|
10469
10669
|
writeFileSync7(file, script, { mode: 448 });
|
|
10470
10670
|
return await new Promise((resolvePromise) => {
|
|
@@ -10488,7 +10688,7 @@ function resolveLatestVersion() {
|
|
|
10488
10688
|
}
|
|
10489
10689
|
function detectInstallChannel() {
|
|
10490
10690
|
try {
|
|
10491
|
-
return
|
|
10691
|
+
return readFileSync17(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
|
|
10492
10692
|
} catch {
|
|
10493
10693
|
return "github";
|
|
10494
10694
|
}
|
|
@@ -10695,7 +10895,7 @@ function printHelp4(io) {
|
|
|
10695
10895
|
}
|
|
10696
10896
|
|
|
10697
10897
|
// src/commands/validate.ts
|
|
10698
|
-
import { readFileSync as
|
|
10898
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
10699
10899
|
import { dirname as dirname9, resolve as resolve14 } from "node:path";
|
|
10700
10900
|
var validateCommand = {
|
|
10701
10901
|
name: "validate",
|
|
@@ -10729,7 +10929,7 @@ function checkBoundary(manifestPath, declared) {
|
|
|
10729
10929
|
}
|
|
10730
10930
|
let manifestYaml;
|
|
10731
10931
|
try {
|
|
10732
|
-
manifestYaml =
|
|
10932
|
+
manifestYaml = readFileSync18(manifestPath, "utf8");
|
|
10733
10933
|
} catch {
|
|
10734
10934
|
manifestYaml = null;
|
|
10735
10935
|
}
|
|
@@ -10993,7 +11193,11 @@ var whoamiCommand = {
|
|
|
10993
11193
|
summary: "show current session identity + role per app",
|
|
10994
11194
|
run: runWhoami
|
|
10995
11195
|
};
|
|
10996
|
-
async function runWhoami(
|
|
11196
|
+
async function runWhoami(args, io) {
|
|
11197
|
+
const wantShare = args.includes("--share") || args.includes("--id");
|
|
11198
|
+
if (wantShare) {
|
|
11199
|
+
return runShare(args, io);
|
|
11200
|
+
}
|
|
10997
11201
|
try {
|
|
10998
11202
|
const cfg = loadConfig();
|
|
10999
11203
|
const session = await readSession(cfg.sessionPath);
|
|
@@ -11036,28 +11240,215 @@ async function runWhoami(_args, io) {
|
|
|
11036
11240
|
return 1;
|
|
11037
11241
|
}
|
|
11038
11242
|
}
|
|
11243
|
+
async function runShare(args, io) {
|
|
11244
|
+
const json = args.includes("--json");
|
|
11245
|
+
try {
|
|
11246
|
+
const cfg = loadConfig();
|
|
11247
|
+
const session = await readSession(cfg.sessionPath);
|
|
11248
|
+
if (session === null) {
|
|
11249
|
+
io.err("No session — run `launchpad login`.");
|
|
11250
|
+
return 1;
|
|
11251
|
+
}
|
|
11252
|
+
let payload = null;
|
|
11253
|
+
try {
|
|
11254
|
+
payload = decodeJwtPayload(session.accessToken);
|
|
11255
|
+
} catch {}
|
|
11256
|
+
const rawSub = typeof payload?.sub === "string" ? payload.sub : null;
|
|
11257
|
+
const sub = rawSub === null ? null : rawSub.trim();
|
|
11258
|
+
if (sub === null || sub.length === 0) {
|
|
11259
|
+
io.err("Couldn't read your id from the session — run `launchpad login` again.");
|
|
11260
|
+
return 1;
|
|
11261
|
+
}
|
|
11262
|
+
io.out(json ? JSON.stringify({ sub }) : sub);
|
|
11263
|
+
return 0;
|
|
11264
|
+
} catch (e) {
|
|
11265
|
+
if (e instanceof UnauthenticatedError) {
|
|
11266
|
+
io.err(e.message);
|
|
11267
|
+
return 1;
|
|
11268
|
+
}
|
|
11269
|
+
io.err(`launchpad whoami --share failed: ${describe31(e)}`);
|
|
11270
|
+
return 1;
|
|
11271
|
+
}
|
|
11272
|
+
}
|
|
11039
11273
|
function describe31(e) {
|
|
11040
11274
|
return e instanceof Error ? e.message : String(e);
|
|
11041
11275
|
}
|
|
11042
11276
|
|
|
11277
|
+
// src/commands/editors.ts
|
|
11278
|
+
import { createInterface as createInterface4 } from "node:readline/promises";
|
|
11279
|
+
var CONFLICT_EXIT = 75;
|
|
11280
|
+
function realDeps() {
|
|
11281
|
+
return {
|
|
11282
|
+
loadConfig,
|
|
11283
|
+
apiJson,
|
|
11284
|
+
isTty: () => process.stdout.isTTY === true,
|
|
11285
|
+
prompt: async (question) => {
|
|
11286
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
11287
|
+
try {
|
|
11288
|
+
return (await rl.question(question)).trim();
|
|
11289
|
+
} finally {
|
|
11290
|
+
rl.close();
|
|
11291
|
+
}
|
|
11292
|
+
}
|
|
11293
|
+
};
|
|
11294
|
+
}
|
|
11295
|
+
function parseArgs13(args) {
|
|
11296
|
+
let slug;
|
|
11297
|
+
let target;
|
|
11298
|
+
let yes = false;
|
|
11299
|
+
let json = false;
|
|
11300
|
+
for (const a of args) {
|
|
11301
|
+
if (a === "--yes" || a === "-y")
|
|
11302
|
+
yes = true;
|
|
11303
|
+
else if (a === "--json")
|
|
11304
|
+
json = true;
|
|
11305
|
+
else if (slug === undefined)
|
|
11306
|
+
slug = a;
|
|
11307
|
+
else if (target === undefined)
|
|
11308
|
+
target = a;
|
|
11309
|
+
}
|
|
11310
|
+
if (slug === undefined || target === undefined)
|
|
11311
|
+
return null;
|
|
11312
|
+
return { slug, target, yes, json };
|
|
11313
|
+
}
|
|
11314
|
+
function isVersionMismatch(b) {
|
|
11315
|
+
return typeof b === "object" && b !== null && b.error === "version_mismatch" && typeof b.currentVersion === "number";
|
|
11316
|
+
}
|
|
11317
|
+
function makeEditorCommand(action, deps = realDeps()) {
|
|
11318
|
+
const name = action === "grant" ? "grant-editor" : "revoke-editor";
|
|
11319
|
+
return {
|
|
11320
|
+
name,
|
|
11321
|
+
summary: action === "grant" ? "grant a developer editor access to an app (by id; email in Phase 2)" : "revoke a developer's editor access to an app",
|
|
11322
|
+
run: (args, io) => runEditor(action, args, io, deps)
|
|
11323
|
+
};
|
|
11324
|
+
}
|
|
11325
|
+
async function runEditor(action, args, io, deps) {
|
|
11326
|
+
const verb = action === "grant" ? "grant-editor" : "revoke-editor";
|
|
11327
|
+
const parsed = parseArgs13(args);
|
|
11328
|
+
if (parsed === null) {
|
|
11329
|
+
io.err(`usage: launchpad ${verb} <slug> <id> [--yes] [--json]`);
|
|
11330
|
+
io.err(" <id> is the recipient's value from `launchpad whoami --share`.");
|
|
11331
|
+
return 64;
|
|
11332
|
+
}
|
|
11333
|
+
const { slug, target, yes, json } = parsed;
|
|
11334
|
+
if (!yes && !deps.isTty()) {
|
|
11335
|
+
io.err(`launchpad ${verb}: refusing to mutate "${slug}" without confirmation — pass --yes in non-interactive mode.`);
|
|
11336
|
+
return 64;
|
|
11337
|
+
}
|
|
11338
|
+
try {
|
|
11339
|
+
const cfg = deps.loadConfig();
|
|
11340
|
+
const current = await deps.apiJson(cfg, {
|
|
11341
|
+
path: `/apps/${slug}`
|
|
11342
|
+
});
|
|
11343
|
+
const alreadyEditor = current.record.editors.includes(target);
|
|
11344
|
+
if (!yes) {
|
|
11345
|
+
const noop = action === "grant" && alreadyEditor || action === "revoke" && !alreadyEditor;
|
|
11346
|
+
io.out(action === "grant" ? `About to grant editor access on "${slug}" to:
|
|
11347
|
+
${target}` : `About to revoke editor access on "${slug}" from:
|
|
11348
|
+
${target}`);
|
|
11349
|
+
if (noop) {
|
|
11350
|
+
io.out(action === "grant" ? " (already an editor — this will be a no-op)" : " (not currently an editor — this will be a no-op)");
|
|
11351
|
+
}
|
|
11352
|
+
const answer = (await deps.prompt("Proceed? [y/N] ")).toLowerCase();
|
|
11353
|
+
if (answer !== "y" && answer !== "yes") {
|
|
11354
|
+
io.out("Aborted — no change made.");
|
|
11355
|
+
return 0;
|
|
11356
|
+
}
|
|
11357
|
+
}
|
|
11358
|
+
const result = await mutateWithRetry(action, slug, target, current.record.version, cfg, deps);
|
|
11359
|
+
if (result.kind === "conflict") {
|
|
11360
|
+
io.err(`launchpad ${verb}: the app record for "${slug}" changed during the ${action} — re-run \`launchpad ${verb} ${slug} ${target}\`.`);
|
|
11361
|
+
return CONFLICT_EXIT;
|
|
11362
|
+
}
|
|
11363
|
+
const changed = result.record.version !== result.attemptedVersion;
|
|
11364
|
+
if (json) {
|
|
11365
|
+
io.out(JSON.stringify({
|
|
11366
|
+
ok: true,
|
|
11367
|
+
slug,
|
|
11368
|
+
action,
|
|
11369
|
+
editor: target,
|
|
11370
|
+
changed,
|
|
11371
|
+
editors: result.record.editors
|
|
11372
|
+
}));
|
|
11373
|
+
return 0;
|
|
11374
|
+
}
|
|
11375
|
+
if (!changed) {
|
|
11376
|
+
io.out(action === "grant" ? `${target} was already an editor on "${slug}" — no change.` : `${target} was not an editor on "${slug}" — no change.`);
|
|
11377
|
+
} else {
|
|
11378
|
+
io.out(action === "grant" ? `✓ ${target} is now an editor on "${slug}".` : `✓ ${target} is no longer an editor on "${slug}".`);
|
|
11379
|
+
}
|
|
11380
|
+
return 0;
|
|
11381
|
+
} catch (e) {
|
|
11382
|
+
if (e instanceof UnauthenticatedError) {
|
|
11383
|
+
io.err(e.message);
|
|
11384
|
+
return 1;
|
|
11385
|
+
}
|
|
11386
|
+
if (e instanceof ForbiddenError) {
|
|
11387
|
+
io.err(`launchpad ${verb}: not authorised — you must be the owner of "${slug}" to manage its editors.`);
|
|
11388
|
+
return 1;
|
|
11389
|
+
}
|
|
11390
|
+
if (e instanceof NotFoundError) {
|
|
11391
|
+
io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
|
|
11392
|
+
return 1;
|
|
11393
|
+
}
|
|
11394
|
+
io.err(`launchpad ${verb} failed: ${describe32(e)}`);
|
|
11395
|
+
return 1;
|
|
11396
|
+
}
|
|
11397
|
+
}
|
|
11398
|
+
async function mutateWithRetry(action, slug, target, startVersion, cfg, deps) {
|
|
11399
|
+
let version = startVersion;
|
|
11400
|
+
for (let attempt = 0;attempt < 2; attempt++) {
|
|
11401
|
+
const body = await sendMutation(action, slug, target, version, cfg, deps);
|
|
11402
|
+
if (isVersionMismatch(body)) {
|
|
11403
|
+
version = body.currentVersion;
|
|
11404
|
+
continue;
|
|
11405
|
+
}
|
|
11406
|
+
return { kind: "ok", record: body.record, attemptedVersion: version };
|
|
11407
|
+
}
|
|
11408
|
+
return { kind: "conflict" };
|
|
11409
|
+
}
|
|
11410
|
+
async function sendMutation(action, slug, target, version, cfg, deps) {
|
|
11411
|
+
const headers = { "if-match": String(version) };
|
|
11412
|
+
if (action === "grant") {
|
|
11413
|
+
return deps.apiJson(cfg, {
|
|
11414
|
+
method: "POST",
|
|
11415
|
+
path: `/apps/${slug}/editors`,
|
|
11416
|
+
jsonBody: { editor: target },
|
|
11417
|
+
headers,
|
|
11418
|
+
nonThrowingStatuses: [409]
|
|
11419
|
+
});
|
|
11420
|
+
}
|
|
11421
|
+
return deps.apiJson(cfg, {
|
|
11422
|
+
method: "DELETE",
|
|
11423
|
+
path: `/apps/${slug}/editors/${encodeURIComponent(target)}`,
|
|
11424
|
+
headers,
|
|
11425
|
+
nonThrowingStatuses: [409]
|
|
11426
|
+
});
|
|
11427
|
+
}
|
|
11428
|
+
function describe32(e) {
|
|
11429
|
+
return e instanceof Error ? e.message : String(e);
|
|
11430
|
+
}
|
|
11431
|
+
var grantEditorCommand = makeEditorCommand("grant");
|
|
11432
|
+
var revokeEditorCommand = makeEditorCommand("revoke");
|
|
11433
|
+
|
|
11043
11434
|
// src/update-notifier.ts
|
|
11044
11435
|
import { spawn as spawn6 } from "node:child_process";
|
|
11045
11436
|
import { homedir as homedir4 } from "node:os";
|
|
11046
|
-
import { join as
|
|
11437
|
+
import { join as join15 } from "node:path";
|
|
11047
11438
|
import {
|
|
11048
|
-
existsSync as
|
|
11439
|
+
existsSync as existsSync14,
|
|
11049
11440
|
mkdirSync as mkdirSync3,
|
|
11050
|
-
readFileSync as
|
|
11441
|
+
readFileSync as readFileSync19,
|
|
11051
11442
|
readdirSync as readdirSync2,
|
|
11052
11443
|
writeFileSync as writeFileSync8
|
|
11053
11444
|
} from "node:fs";
|
|
11054
11445
|
var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
|
|
11055
11446
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
11056
11447
|
var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
|
|
11057
|
-
var CACHE_FILE =
|
|
11448
|
+
var CACHE_FILE = join15(homedir4(), ".launchpad", "update-check.json");
|
|
11058
11449
|
function readCache2() {
|
|
11059
11450
|
try {
|
|
11060
|
-
const raw = JSON.parse(
|
|
11451
|
+
const raw = JSON.parse(readFileSync19(CACHE_FILE, "utf8"));
|
|
11061
11452
|
if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
|
|
11062
11453
|
const latest = raw.latest;
|
|
11063
11454
|
return {
|
|
@@ -11070,15 +11461,15 @@ function readCache2() {
|
|
|
11070
11461
|
}
|
|
11071
11462
|
function writeCache2(state) {
|
|
11072
11463
|
try {
|
|
11073
|
-
mkdirSync3(
|
|
11464
|
+
mkdirSync3(join15(homedir4(), ".launchpad"), { recursive: true });
|
|
11074
11465
|
writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
|
|
11075
11466
|
`, { mode: 384 });
|
|
11076
11467
|
} catch {}
|
|
11077
11468
|
}
|
|
11078
11469
|
var SKILL_PREFIX = "launchpad-";
|
|
11079
|
-
var SKILLS_HINT_MARKER =
|
|
11470
|
+
var SKILLS_HINT_MARKER = join15(homedir4(), ".launchpad", "skills-hint-shown");
|
|
11080
11471
|
function skillsTargetDir() {
|
|
11081
|
-
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ??
|
|
11472
|
+
return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir4(), ".claude", "skills");
|
|
11082
11473
|
}
|
|
11083
11474
|
function readInstalledSkills() {
|
|
11084
11475
|
try {
|
|
@@ -11086,14 +11477,14 @@ function readInstalledSkills() {
|
|
|
11086
11477
|
const bundle = readdirSync2(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
|
|
11087
11478
|
if (!bundle)
|
|
11088
11479
|
return { present: false, version: null };
|
|
11089
|
-
return { present: true, version: readSkillVersion(
|
|
11480
|
+
return { present: true, version: readSkillVersion(join15(dir, bundle.name, "SKILL.md")) };
|
|
11090
11481
|
} catch {
|
|
11091
11482
|
return { present: false, version: null };
|
|
11092
11483
|
}
|
|
11093
11484
|
}
|
|
11094
11485
|
function readSkillVersion(path13) {
|
|
11095
11486
|
try {
|
|
11096
|
-
const text =
|
|
11487
|
+
const text = readFileSync19(path13, "utf8");
|
|
11097
11488
|
const fenceEnd = text.indexOf(`
|
|
11098
11489
|
---`, 4);
|
|
11099
11490
|
const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
|
|
@@ -11105,14 +11496,14 @@ function readSkillVersion(path13) {
|
|
|
11105
11496
|
}
|
|
11106
11497
|
function absentHintShown() {
|
|
11107
11498
|
try {
|
|
11108
|
-
return
|
|
11499
|
+
return existsSync14(SKILLS_HINT_MARKER);
|
|
11109
11500
|
} catch {
|
|
11110
11501
|
return false;
|
|
11111
11502
|
}
|
|
11112
11503
|
}
|
|
11113
11504
|
function markAbsentHintShown() {
|
|
11114
11505
|
try {
|
|
11115
|
-
mkdirSync3(
|
|
11506
|
+
mkdirSync3(join15(homedir4(), ".launchpad"), { recursive: true });
|
|
11116
11507
|
writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
|
|
11117
11508
|
`, { mode: 384 });
|
|
11118
11509
|
} catch {}
|
|
@@ -11238,6 +11629,8 @@ var COMMANDS = [
|
|
|
11238
11629
|
rollbackCommand,
|
|
11239
11630
|
groupsCommand,
|
|
11240
11631
|
secretsCommand,
|
|
11632
|
+
grantEditorCommand,
|
|
11633
|
+
revokeEditorCommand,
|
|
11241
11634
|
refreshUpdateCacheCommand
|
|
11242
11635
|
];
|
|
11243
11636
|
async function dispatch(argv, io, commands = COMMANDS) {
|