@m-kopa/launchpad-cli 0.27.4 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.4";
22
+ var CLI_VERSION = "0.28.0";
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 existsSync5 } from "node:fs";
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 readFileSync7 } from "node:fs";
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 existsSync3, rmSync } from "node:fs";
4049
- import { resolve as resolvePath, join as join8 } from "node:path";
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 readFileSync5 } from "node:fs";
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 = readFileSync5(path7, "utf8");
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 = join8(cfg.stateDir, slug, "group.json");
4280
- if (!existsSync3(pinPath))
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 existsSync4, readFileSync as readFileSync6 } from "node:fs";
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 (!existsSync4(manifestPath))
4949
+ if (!existsSync5(manifestPath))
4734
4950
  return null;
4735
4951
  try {
4736
- return resolveManifestSlug(parseYaml4(readFileSync6(manifestPath, "utf8")));
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 (existsSync5(manifestPath)) {
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 (existsSync5(contentManifestPath)) {
5063
+ if (existsSync6(contentManifestPath)) {
4848
5064
  try {
4849
- contentManifestYaml = readFileSync7(contentManifestPath, "utf8");
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 metaSlug = resolveManifestSlug(parseYaml5(readFileSync7(manifestPath, "utf8")));
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 appType = typeof success.appType === "string" ? success.appType : "(missing)";
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: ${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
- io.out(`Run \`launchpad status ${slug}\` to confirm the build outcome (success / failure + log excerpt).`);
5213
- return 0;
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 readFileSync8, writeFileSync } from "node:fs";
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: readFileSync8(path10, "utf8") };
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 readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
5709
- import { dirname as dirname6, join as join10 } from "node:path";
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 = join10(cfg.cacheDir, CACHE_FILENAME);
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 = readFileSync9(path10, "utf8");
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 existsSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync3 } from "node:fs";
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 existsSync6, readFileSync as readFileSync10, statSync } from "node:fs";
6259
- import { join as join11 } from "node:path";
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 = existsSync6(join11(cwd, "package.json"));
6262
- const hasAnyLockfile = LOCKFILES.some(({ file }) => existsSync6(join11(cwd, file)));
6263
- const hasViteConfig = VITE_CONFIG_NAMES.some((n) => existsSync6(join11(cwd, 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 }) => existsSync6(join11(cwd, 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 = join11(cwd, name);
6332
- if (existsSync6(p)) {
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 = join11(cwd, "functions");
6575
+ const fnDir = join12(cwd, "functions");
6343
6576
  let hasFunctionsDir = false;
6344
- if (existsSync6(fnDir)) {
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 = join11(cwd, "package.json");
6355
- if (!existsSync6(pkgJsonPath)) {
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(readFileSync10(pkgJsonPath, "utf8"));
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 = readFileSync10(vite.path, "utf8");
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 (existsSync7(outPath) && !options.force) {
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 (existsSync7(path10)) {
6847
- current = readFileSync11(path10, "utf8");
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 readFileSync12 } from "node:fs";
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 = readFileSync12(parsed.file, "utf8");
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 existsSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync5 } from "node:fs";
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 (!existsSync8(path13))
9193
+ if (!existsSync9(path13))
8994
9194
  return null;
8995
9195
  let raw;
8996
9196
  try {
8997
- raw = readFileSync13(path13, "utf8");
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 existsSync9, readFileSync as readFileSync14 } from "node:fs";
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 (!existsSync9(envPath)) {
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 = readFileSync14(envPath, "utf8");
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 existsSync10, readFileSync as readFileSync15 } from "node:fs";
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 (!existsSync10(path13)) {
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(readFileSync15(path13, "utf8"));
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 (!existsSync10(envPath)) {
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 = readFileSync15(envPath, "utf8");
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 existsSync11, writeFileSync as writeFileSync6 } from "node:fs";
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 (existsSync11(outPath) && !flags.force) {
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 join12, resolve as resolve12 } from "node:path";
10053
- import { promises as fs6, existsSync as existsSync12 } from "node:fs";
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 ?? join12(homedir2(), ".claude", "skills");
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 (existsSync12(join12(c, "launchpad-onboard", "SKILL.md"))) {
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 = join12(env.bundleDir, skill);
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 = join12(env.userSkillsDir, skill);
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 = join12(env.userSkillsDir, entry.name);
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 = join12(env.userSkillsDir, name, "SKILL.md");
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 join13 } from "node:path";
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 readFileSync16, mkdtempSync, writeFileSync as writeFileSync7, rmSync as rmSync2 } from "node:fs";
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 createHash2, randomBytes as randomBytes5 } from "node:crypto";
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(createHash2("sha256").update(verifier).digest());
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 = join13(homedir3(), ".launchpad", "channel");
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(join13(tmpdir(), "launchpad-update-"));
10467
- const file = join13(dir, "install.sh");
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 readFileSync16(CHANNEL_MARKER, "utf8").trim() === "platform" ? "platform" : "github";
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 readFileSync17 } from "node:fs";
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 = readFileSync17(manifestPath, "utf8");
10932
+ manifestYaml = readFileSync18(manifestPath, "utf8");
10733
10933
  } catch {
10734
10934
  manifestYaml = null;
10735
10935
  }
@@ -11234,21 +11434,21 @@ var revokeEditorCommand = makeEditorCommand("revoke");
11234
11434
  // src/update-notifier.ts
11235
11435
  import { spawn as spawn6 } from "node:child_process";
11236
11436
  import { homedir as homedir4 } from "node:os";
11237
- import { join as join14 } from "node:path";
11437
+ import { join as join15 } from "node:path";
11238
11438
  import {
11239
- existsSync as existsSync13,
11439
+ existsSync as existsSync14,
11240
11440
  mkdirSync as mkdirSync3,
11241
- readFileSync as readFileSync18,
11441
+ readFileSync as readFileSync19,
11242
11442
  readdirSync as readdirSync2,
11243
11443
  writeFileSync as writeFileSync8
11244
11444
  } from "node:fs";
11245
11445
  var INTERNAL_REFRESH_VERB = "__refresh-update-cache";
11246
11446
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
11247
11447
  var OPT_OUT_ENV = "LAUNCHPAD_NO_UPDATE_NOTIFIER";
11248
- var CACHE_FILE = join14(homedir4(), ".launchpad", "update-check.json");
11448
+ var CACHE_FILE = join15(homedir4(), ".launchpad", "update-check.json");
11249
11449
  function readCache2() {
11250
11450
  try {
11251
- const raw = JSON.parse(readFileSync18(CACHE_FILE, "utf8"));
11451
+ const raw = JSON.parse(readFileSync19(CACHE_FILE, "utf8"));
11252
11452
  if (typeof raw === "object" && raw !== null && typeof raw.checkedAt === "number") {
11253
11453
  const latest = raw.latest;
11254
11454
  return {
@@ -11261,15 +11461,15 @@ function readCache2() {
11261
11461
  }
11262
11462
  function writeCache2(state) {
11263
11463
  try {
11264
- mkdirSync3(join14(homedir4(), ".launchpad"), { recursive: true });
11464
+ mkdirSync3(join15(homedir4(), ".launchpad"), { recursive: true });
11265
11465
  writeFileSync8(CACHE_FILE, `${JSON.stringify(state)}
11266
11466
  `, { mode: 384 });
11267
11467
  } catch {}
11268
11468
  }
11269
11469
  var SKILL_PREFIX = "launchpad-";
11270
- var SKILLS_HINT_MARKER = join14(homedir4(), ".launchpad", "skills-hint-shown");
11470
+ var SKILLS_HINT_MARKER = join15(homedir4(), ".launchpad", "skills-hint-shown");
11271
11471
  function skillsTargetDir() {
11272
- return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join14(homedir4(), ".claude", "skills");
11472
+ return process.env.LAUNCHPAD_SKILLS_TARGET_DIR ?? join15(homedir4(), ".claude", "skills");
11273
11473
  }
11274
11474
  function readInstalledSkills() {
11275
11475
  try {
@@ -11277,14 +11477,14 @@ function readInstalledSkills() {
11277
11477
  const bundle = readdirSync2(dir, { withFileTypes: true }).find((e) => e.isDirectory() && e.name.startsWith(SKILL_PREFIX));
11278
11478
  if (!bundle)
11279
11479
  return { present: false, version: null };
11280
- return { present: true, version: readSkillVersion(join14(dir, bundle.name, "SKILL.md")) };
11480
+ return { present: true, version: readSkillVersion(join15(dir, bundle.name, "SKILL.md")) };
11281
11481
  } catch {
11282
11482
  return { present: false, version: null };
11283
11483
  }
11284
11484
  }
11285
11485
  function readSkillVersion(path13) {
11286
11486
  try {
11287
- const text = readFileSync18(path13, "utf8");
11487
+ const text = readFileSync19(path13, "utf8");
11288
11488
  const fenceEnd = text.indexOf(`
11289
11489
  ---`, 4);
11290
11490
  const front = fenceEnd === -1 ? text.slice(0, 1024) : text.slice(0, fenceEnd);
@@ -11296,14 +11496,14 @@ function readSkillVersion(path13) {
11296
11496
  }
11297
11497
  function absentHintShown() {
11298
11498
  try {
11299
- return existsSync13(SKILLS_HINT_MARKER);
11499
+ return existsSync14(SKILLS_HINT_MARKER);
11300
11500
  } catch {
11301
11501
  return false;
11302
11502
  }
11303
11503
  }
11304
11504
  function markAbsentHintShown() {
11305
11505
  try {
11306
- mkdirSync3(join14(homedir4(), ".launchpad"), { recursive: true });
11506
+ mkdirSync3(join15(homedir4(), ".launchpad"), { recursive: true });
11307
11507
  writeFileSync8(SKILLS_HINT_MARKER, `${Date.now()}
11308
11508
  `, { mode: 384 });
11309
11509
  } catch {}
@@ -11403,6 +11603,234 @@ var refreshUpdateCacheCommand = {
11403
11603
  }
11404
11604
  };
11405
11605
 
11606
+ // src/telemetry.ts
11607
+ import { spawn as spawn7 } from "node:child_process";
11608
+ import { homedir as homedir5 } from "node:os";
11609
+ import { join as join16 } from "node:path";
11610
+ import { existsSync as existsSync15, mkdirSync as mkdirSync4, readFileSync as readFileSync20, writeFileSync as writeFileSync9 } from "node:fs";
11611
+ import { randomUUID } from "node:crypto";
11612
+ var INTERNAL_EMIT_VERB = "__emit-telemetry";
11613
+ var POSTHOG_HOST = "https://us.i.posthog.com";
11614
+ var POSTHOG_PROJECT_KEY = "phc_D4FKJnWb7wWGCWs8NoTpQ2xFGFd4jQt698pQ68BrdcXt";
11615
+ var CAPTURE_PATH = "/i/v0/e/";
11616
+ var SEND_TIMEOUT_MS = 3000;
11617
+ var APP = "launchpad-cli";
11618
+ var DEVICE_ID_FILE = join16(homedir5(), ".launchpad", "telemetry-id.json");
11619
+ var FIRST_RUN_MARKER = join16(homedir5(), ".launchpad", "telemetry-notice-shown");
11620
+ function buildEvent(ctx, identity, nowMs) {
11621
+ const properties = {
11622
+ app: APP,
11623
+ command: ctx.command,
11624
+ exit_code: ctx.exitCode,
11625
+ duration_ms: ctx.durationMs,
11626
+ cli_version: CLI_VERSION,
11627
+ os: process.platform,
11628
+ node_version: process.version,
11629
+ ci: ctx.ci,
11630
+ environment: ctx.environment
11631
+ };
11632
+ if (!identity.identified)
11633
+ properties.$process_person_profile = false;
11634
+ return {
11635
+ api_key: POSTHOG_PROJECT_KEY,
11636
+ event: "command_run",
11637
+ distinct_id: identity.distinctId,
11638
+ properties,
11639
+ timestamp: new Date(nowMs).toISOString()
11640
+ };
11641
+ }
11642
+ function emittingCommand(argv) {
11643
+ const verb = argv[0];
11644
+ if (verb === undefined)
11645
+ return null;
11646
+ if (verb === "--help" || verb === "-h")
11647
+ return null;
11648
+ if (verb === "--version" || verb === "-v")
11649
+ return null;
11650
+ if (verb === INTERNAL_EMIT_VERB || verb === INTERNAL_REFRESH_VERB)
11651
+ return null;
11652
+ return verb;
11653
+ }
11654
+ function environmentOf(argv, stderrIsTTY) {
11655
+ if (argv.includes("--json"))
11656
+ return "json";
11657
+ return stderrIsTTY ? "tty" : "non-tty";
11658
+ }
11659
+ async function resolveIdentity(deps) {
11660
+ try {
11661
+ const session = await deps.readSession();
11662
+ if (session && typeof session.accessToken === "string") {
11663
+ let payload = null;
11664
+ try {
11665
+ payload = decodeJwtPayload(session.accessToken);
11666
+ } catch {
11667
+ payload = null;
11668
+ }
11669
+ const id = typeof payload?.email === "string" && payload.email ? payload.email : typeof payload?.sub === "string" && payload.sub ? payload.sub : null;
11670
+ if (id)
11671
+ return { distinctId: id, identified: true };
11672
+ }
11673
+ } catch {}
11674
+ return { distinctId: deps.deviceId(), identified: false };
11675
+ }
11676
+ async function resolveIdentityReal() {
11677
+ return resolveIdentity({
11678
+ readSession: async () => {
11679
+ const cfg = loadConfig();
11680
+ return readSession(cfg.sessionPath);
11681
+ },
11682
+ deviceId: getOrCreateDeviceId
11683
+ });
11684
+ }
11685
+ function getOrCreateDeviceId() {
11686
+ try {
11687
+ const raw = JSON.parse(readFileSync20(DEVICE_ID_FILE, "utf8"));
11688
+ const id2 = raw?.deviceId;
11689
+ if (typeof id2 === "string" && id2)
11690
+ return id2;
11691
+ } catch {}
11692
+ const id = randomUUID();
11693
+ try {
11694
+ mkdirSync4(join16(homedir5(), ".launchpad"), { recursive: true });
11695
+ writeFileSync9(DEVICE_ID_FILE, `${JSON.stringify({ deviceId: id })}
11696
+ `, {
11697
+ mode: 384
11698
+ });
11699
+ } catch {}
11700
+ return id;
11701
+ }
11702
+ async function postCapture(event, fetcher = fetch) {
11703
+ const controller = new AbortController;
11704
+ const timer = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
11705
+ try {
11706
+ await fetcher(`${POSTHOG_HOST}${CAPTURE_PATH}`, {
11707
+ method: "POST",
11708
+ headers: {
11709
+ "content-type": "application/json",
11710
+ "user-agent": `launchpad-cli/${CLI_VERSION}`
11711
+ },
11712
+ body: JSON.stringify(event),
11713
+ signal: controller.signal
11714
+ });
11715
+ } catch {} finally {
11716
+ clearTimeout(timer);
11717
+ }
11718
+ }
11719
+ function recordAfterCommand(argv, exitCode, durationMs, deps) {
11720
+ const command = emittingCommand(argv);
11721
+ if (command === null)
11722
+ return;
11723
+ deps.emit({
11724
+ command,
11725
+ exitCode,
11726
+ durationMs,
11727
+ ci: deps.ci,
11728
+ environment: environmentOf(argv, deps.stderrIsTTY)
11729
+ });
11730
+ }
11731
+ function recordAfterCommandReal(argv, exitCode, durationMs, cliPath = process.argv[1]) {
11732
+ try {
11733
+ recordAfterCommand(argv, exitCode, durationMs, {
11734
+ emit: (ctx) => spawnDetachedEmit(cliPath, ctx),
11735
+ ci: Boolean(process.env.CI),
11736
+ stderrIsTTY: Boolean(process.stderr.isTTY)
11737
+ });
11738
+ } catch {}
11739
+ }
11740
+ function spawnDetachedEmit(cliPath, ctx) {
11741
+ if (!cliPath)
11742
+ return;
11743
+ try {
11744
+ const child = spawn7(process.execPath, [
11745
+ cliPath,
11746
+ INTERNAL_EMIT_VERB,
11747
+ "--command",
11748
+ ctx.command,
11749
+ "--exit",
11750
+ String(ctx.exitCode),
11751
+ "--duration",
11752
+ String(ctx.durationMs),
11753
+ "--environment",
11754
+ ctx.environment,
11755
+ ...ctx.ci ? ["--ci"] : []
11756
+ ], { detached: true, stdio: "ignore" });
11757
+ child.unref();
11758
+ } catch {}
11759
+ }
11760
+ function telemetryNoticeLine() {
11761
+ return "ℹ launchpad records anonymous + signed-in usage analytics (command, CLI version, OS, outcome — never your arguments, paths, or file contents) to improve the platform. Internal staff tooling. More: https://get.launchpad.m-kopa.us/docs/.";
11762
+ }
11763
+ function firstRunNoticeShown() {
11764
+ try {
11765
+ return existsSync15(FIRST_RUN_MARKER);
11766
+ } catch {
11767
+ return false;
11768
+ }
11769
+ }
11770
+ function markFirstRunNotice() {
11771
+ try {
11772
+ mkdirSync4(join16(homedir5(), ".launchpad"), { recursive: true });
11773
+ writeFileSync9(FIRST_RUN_MARKER, `${Date.now()}
11774
+ `, { mode: 384 });
11775
+ } catch {}
11776
+ }
11777
+ function maybeFirstRunNotice(io, argv, deps) {
11778
+ if (!deps.stderrIsTTY)
11779
+ return;
11780
+ if (argv.includes("--json"))
11781
+ return;
11782
+ if (emittingCommand(argv) === null)
11783
+ return;
11784
+ if (deps.shown())
11785
+ return;
11786
+ io.err(telemetryNoticeLine());
11787
+ deps.mark();
11788
+ }
11789
+ function maybeFirstRunNoticeReal(io, argv) {
11790
+ try {
11791
+ maybeFirstRunNotice(io, argv, {
11792
+ stderrIsTTY: Boolean(process.stderr.isTTY),
11793
+ shown: firstRunNoticeShown,
11794
+ mark: markFirstRunNotice
11795
+ });
11796
+ } catch {}
11797
+ }
11798
+ function parseEmitArgs(args) {
11799
+ const get = (flag) => {
11800
+ const i = args.indexOf(flag);
11801
+ return i >= 0 ? args[i + 1] : undefined;
11802
+ };
11803
+ const command = get("--command");
11804
+ const exit = get("--exit");
11805
+ const duration = get("--duration");
11806
+ const environment = get("--environment");
11807
+ if (!command || exit === undefined || duration === undefined)
11808
+ return null;
11809
+ const env = environment === "json" || environment === "non-tty" ? environment : "tty";
11810
+ return {
11811
+ command,
11812
+ exitCode: Number.parseInt(exit, 10) || 0,
11813
+ durationMs: Number.parseInt(duration, 10) || 0,
11814
+ ci: args.includes("--ci"),
11815
+ environment: env
11816
+ };
11817
+ }
11818
+ var emitTelemetryCommand = {
11819
+ name: INTERNAL_EMIT_VERB,
11820
+ summary: "(internal) send a usage-telemetry event",
11821
+ hidden: true,
11822
+ run: async (args) => {
11823
+ try {
11824
+ const ctx = parseEmitArgs(args);
11825
+ if (ctx) {
11826
+ const identity = await resolveIdentityReal();
11827
+ await postCapture(buildEvent(ctx, identity, Date.now()));
11828
+ }
11829
+ } catch {}
11830
+ return 0;
11831
+ }
11832
+ };
11833
+
11406
11834
  // src/dispatcher.ts
11407
11835
  var COMMANDS = [
11408
11836
  loginCommand,
@@ -11431,7 +11859,8 @@ var COMMANDS = [
11431
11859
  secretsCommand,
11432
11860
  grantEditorCommand,
11433
11861
  revokeEditorCommand,
11434
- refreshUpdateCacheCommand
11862
+ refreshUpdateCacheCommand,
11863
+ emitTelemetryCommand
11435
11864
  ];
11436
11865
  async function dispatch(argv, io, commands = COMMANDS) {
11437
11866
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
@@ -11492,12 +11921,19 @@ var io = {
11492
11921
  }
11493
11922
  };
11494
11923
  var args = process.argv.slice(2);
11924
+ var startedAt = Date.now();
11495
11925
  dispatch(args, io).then((code) => {
11926
+ const elapsedMs = Date.now() - startedAt;
11496
11927
  process.exitCode = code;
11497
11928
  notifyAfterCommand(io, args);
11929
+ maybeFirstRunNoticeReal(io, args);
11930
+ recordAfterCommandReal(args, code, elapsedMs);
11498
11931
  }).catch((err) => {
11932
+ const elapsedMs = Date.now() - startedAt;
11499
11933
  const msg = err instanceof Error ? err.message : String(err);
11500
11934
  process.stderr.write(`launchpad: unexpected error: ${msg}
11501
11935
  `);
11502
11936
  process.exitCode = 70;
11937
+ maybeFirstRunNoticeReal(io, args);
11938
+ recordAfterCommandReal(args, 70, elapsedMs);
11503
11939
  });