@m-kopa/launchpad-cli 0.32.2 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
7
7
  pre-1.0 minor bumps may carry breaking changes per ADR 0005.
8
8
 
9
+ ## 0.33.0 — 2026-06-17
10
+
11
+ New verb: **`launchpad watch [<slug>]`** (and `launchpad status <slug> --watch`) —
12
+ watch an app provision to live in real time (sp-wtch7q). Run it in a second
13
+ terminal after a first `launchpad deploy` instead of re-running `launchpad status`
14
+ by hand.
15
+
16
+ - **Live phase pipeline.** The 15 provisioning stages are grouped into six phases
17
+ (Repo · Build · Infra · Certificate · Access · Verify) with a spinner on the
18
+ active phase, `stage N/15`, and **client-side** per-phase timers (the lifecycle
19
+ exposes only the current stage, so timings are measured from observed transitions
20
+ and omitted — never faked — for stages that completed before the watch attached).
21
+ - **Graceful terminal frames.** On live, the app URL front and centre; on failure,
22
+ the failing stage + reason + the exact recovery command. `Ctrl-C` stops *watching*
23
+ only — provisioning continues.
24
+ - **Capability-tiered, cross-platform.** A terminal probe selects the richest safe
25
+ renderer — inline live-redraw → ASCII-glyph fallback → append-only → plain — and
26
+ **degrades, never assumes** (Windows legacy code pages get ASCII, not garbage).
27
+ Piped / CI / non-TTY output is plain, escape-free text. The platform's **first
28
+ Windows CI matrix** (windows-latest × Git-Bash/PowerShell/cmd) runs the watcher's
29
+ degraded tiers.
30
+ - **Safe by construction.** Every bot-supplied string is escape-sanitised before it
31
+ reaches the terminal (no terminal-escape injection). **Zero new runtime deps** —
32
+ the ANSI is hand-rolled, now guarded by an assertive zero-runtime-dep CI check.
33
+ - A richer full-screen ("alt-screen") mode is planned as a later opt-in experimental
34
+ variant; this release ships the inline renderer.
35
+
9
36
  ## 0.32.2 — 2026-06-17
10
37
 
11
38
  Docs: skill-accuracy sweep. Audited all bundled skills against the live
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.32.2";
22
+ var CLI_VERSION = "0.33.0";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -8406,6 +8406,593 @@ function printUsage5(io) {
8406
8406
 
8407
8407
  // src/commands/status.ts
8408
8408
  import { readFileSync as readFileSync13 } from "node:fs";
8409
+
8410
+ // src/watch/capabilities.ts
8411
+ function isUtf8(env) {
8412
+ const v = `${env.LC_ALL ?? ""}|${env.LC_CTYPE ?? ""}|${env.LANG ?? ""}`.toLowerCase();
8413
+ return v.includes("utf-8") || v.includes("utf8");
8414
+ }
8415
+ function knownModernTerminal(env) {
8416
+ if (env.WT_SESSION)
8417
+ return true;
8418
+ if (env.ConEmuANSI === "ON")
8419
+ return true;
8420
+ const prog = (env.TERM_PROGRAM ?? "").toLowerCase();
8421
+ return prog === "iterm.app" || prog === "apple_terminal" || prog === "vscode" || prog === "ghostty" || prog === "wezterm";
8422
+ }
8423
+ function probeCapabilities(p) {
8424
+ const isTTY = p.isTTY === true;
8425
+ if (!isTTY)
8426
+ return { isTTY: false, ansi: false, unicode: false };
8427
+ const term = (p.env.TERM ?? "").toLowerCase();
8428
+ const dumb = term === "dumb";
8429
+ const ansi = !dumb && (p.platform !== "win32" || knownModernTerminal(p.env) || term.startsWith("xterm"));
8430
+ const unicode = isUtf8(p.env) || knownModernTerminal(p.env) || p.platform === "darwin";
8431
+ return { isTTY, ansi, unicode };
8432
+ }
8433
+ function selectTier(caps) {
8434
+ if (!caps.isTTY)
8435
+ return "plain";
8436
+ if (!caps.ansi)
8437
+ return "append";
8438
+ return caps.unicode ? "inline" : "ascii";
8439
+ }
8440
+ function detectTier(stream = process.stdout) {
8441
+ const caps = probeCapabilities({
8442
+ platform: process.platform,
8443
+ env: process.env,
8444
+ isTTY: stream.isTTY === true
8445
+ });
8446
+ return { caps, tier: selectTier(caps) };
8447
+ }
8448
+
8449
+ // src/watch/phases.ts
8450
+ var PHASE_NAMES = [
8451
+ "Repo",
8452
+ "Build",
8453
+ "Infra",
8454
+ "Certificate",
8455
+ "Access",
8456
+ "Verify"
8457
+ ];
8458
+ var STAGE_ORDER = [
8459
+ "pending",
8460
+ "repo_created",
8461
+ "bootstrap_pr_opened",
8462
+ "bootstrap_pr_merged",
8463
+ "content_seeded",
8464
+ "tf_pr_opened",
8465
+ "tf_pr_merged",
8466
+ "tf_applied",
8467
+ "cert_active",
8468
+ "policy_attached",
8469
+ "tf_env_pr_opened",
8470
+ "tf_env_pr_merged",
8471
+ "tf_env_applied",
8472
+ "ready_for_content",
8473
+ "deployment_verified"
8474
+ ];
8475
+ var TOTAL_STAGES = STAGE_ORDER.length;
8476
+ var STAGE_TO_PHASE = {
8477
+ pending: "Repo",
8478
+ repo_created: "Repo",
8479
+ bootstrap_pr_opened: "Repo",
8480
+ bootstrap_pr_merged: "Repo",
8481
+ content_seeded: "Build",
8482
+ tf_pr_opened: "Infra",
8483
+ tf_pr_merged: "Infra",
8484
+ tf_applied: "Infra",
8485
+ cert_active: "Certificate",
8486
+ policy_attached: "Access",
8487
+ tf_env_pr_opened: "Access",
8488
+ tf_env_pr_merged: "Access",
8489
+ tf_env_applied: "Access",
8490
+ ready_for_content: "Verify",
8491
+ deployment_verified: "Verify"
8492
+ };
8493
+ var FAILURE_STAGE_TO_PHASE = {
8494
+ validator_rejected: "Repo",
8495
+ bot_pr_ci_failed: "Infra",
8496
+ tf_apply_failed: "Infra",
8497
+ abandoned: "Repo",
8498
+ failed: "Verify"
8499
+ };
8500
+ function ordinalOfStage(stage) {
8501
+ const i = STAGE_ORDER.indexOf(stage);
8502
+ return i < 0 ? null : i + 1;
8503
+ }
8504
+ function phaseOfStage(stage) {
8505
+ return STAGE_TO_PHASE[stage] ?? FAILURE_STAGE_TO_PHASE[stage] ?? null;
8506
+ }
8507
+ function phaseIndex(phase) {
8508
+ return PHASE_NAMES.indexOf(phase);
8509
+ }
8510
+
8511
+ // src/watch/model.ts
8512
+ class WatchModel {
8513
+ slug;
8514
+ startedAtMs = null;
8515
+ stageFirstSeen = new Map;
8516
+ observedOrder = [];
8517
+ currentStage = null;
8518
+ terminal = null;
8519
+ failure = null;
8520
+ liveUrl = null;
8521
+ constructor(slug) {
8522
+ this.slug = slug;
8523
+ }
8524
+ ingest(view, nowMs) {
8525
+ if (this.startedAtMs === null)
8526
+ this.startedAtMs = nowMs;
8527
+ if (view.stage && !this.stageFirstSeen.has(view.stage)) {
8528
+ this.stageFirstSeen.set(view.stage, nowMs);
8529
+ this.observedOrder.push(view.stage);
8530
+ }
8531
+ if (view.stage)
8532
+ this.currentStage = view.stage;
8533
+ if (view.liveUrl)
8534
+ this.liveUrl = view.liveUrl;
8535
+ if (view.state === "live") {
8536
+ this.terminal = "live";
8537
+ } else if (view.state === "failed") {
8538
+ this.terminal = "failed";
8539
+ this.failure = view.failedAt ?? this.failure;
8540
+ if (view.failedAt?.stage)
8541
+ this.currentStage = view.failedAt.stage;
8542
+ }
8543
+ }
8544
+ isTerminal() {
8545
+ return this.terminal !== null;
8546
+ }
8547
+ snapshot(nowMs) {
8548
+ const started = this.startedAtMs ?? nowMs;
8549
+ const activePhase = this.currentStage !== null ? phaseOfStage(this.currentStage) : null;
8550
+ const activeIdx = activePhase !== null ? phaseIndex(activePhase) : -1;
8551
+ const phases = PHASE_NAMES.map((name) => {
8552
+ const idx = phaseIndex(name);
8553
+ let status;
8554
+ if (this.terminal === "live") {
8555
+ status = "done";
8556
+ } else if (this.terminal === "failed") {
8557
+ if (idx < activeIdx)
8558
+ status = "done";
8559
+ else if (idx === activeIdx)
8560
+ status = "failed";
8561
+ else
8562
+ status = "pending";
8563
+ } else if (activeIdx < 0) {
8564
+ status = "pending";
8565
+ } else if (idx < activeIdx) {
8566
+ status = "done";
8567
+ } else if (idx === activeIdx) {
8568
+ status = "active";
8569
+ } else {
8570
+ status = "pending";
8571
+ }
8572
+ return { name, status, elapsedMs: this.phaseElapsed(name, nowMs) };
8573
+ });
8574
+ const stageOrdinal = this.terminal === "live" ? TOTAL_STAGES : this.currentStage !== null ? ordinalOfStage(this.currentStage) : null;
8575
+ return {
8576
+ slug: this.slug,
8577
+ phases,
8578
+ currentStage: this.currentStage,
8579
+ stageOrdinal,
8580
+ totalStages: TOTAL_STAGES,
8581
+ terminal: this.terminal,
8582
+ failure: this.failure ? {
8583
+ stage: this.failure.stage,
8584
+ message: this.failure.message,
8585
+ retryable: this.failure.retryable
8586
+ } : null,
8587
+ liveUrl: this.liveUrl,
8588
+ startedAtMs: started,
8589
+ elapsedTotalMs: Math.max(0, nowMs - started)
8590
+ };
8591
+ }
8592
+ phaseElapsed(phase, nowMs) {
8593
+ let startMs = null;
8594
+ for (const stage of this.observedOrder) {
8595
+ if (phaseOfStage(stage) === phase) {
8596
+ const seen = this.stageFirstSeen.get(stage);
8597
+ if (seen !== undefined && (startMs === null || seen < startMs)) {
8598
+ startMs = seen;
8599
+ }
8600
+ }
8601
+ }
8602
+ if (startMs === null)
8603
+ return null;
8604
+ const targetIdx = phaseIndex(phase);
8605
+ let endMs = null;
8606
+ for (const stage of this.observedOrder) {
8607
+ const p = phaseOfStage(stage);
8608
+ if (p !== null && phaseIndex(p) > targetIdx) {
8609
+ const seen = this.stageFirstSeen.get(stage);
8610
+ if (seen !== undefined && (endMs === null || seen < endMs)) {
8611
+ endMs = seen;
8612
+ }
8613
+ }
8614
+ }
8615
+ if (endMs === null)
8616
+ endMs = nowMs;
8617
+ return Math.max(0, endMs - startMs);
8618
+ }
8619
+ }
8620
+
8621
+ // src/watch/engine.ts
8622
+ var DEFAULTS = {
8623
+ pollIntervalMs: 2000,
8624
+ backoffFactor: 1.5,
8625
+ maxBackoffMs: 30000,
8626
+ maxDurationMs: 20 * 60 * 1000
8627
+ };
8628
+ async function runWatchLoop(slug, deps, options = {}) {
8629
+ const opts = { ...DEFAULTS, ...options };
8630
+ const model = new WatchModel(slug);
8631
+ const startedAt = deps.now();
8632
+ let backoffMs = opts.pollIntervalMs;
8633
+ let lastSnapshot = null;
8634
+ while (true) {
8635
+ if (deps.signal.aborted)
8636
+ return { outcome: "interrupted", snapshot: lastSnapshot };
8637
+ let nextDelay = opts.pollIntervalMs;
8638
+ try {
8639
+ const view = await deps.fetchLifecycle();
8640
+ if (view === null) {
8641
+ return { outcome: "not_found", snapshot: lastSnapshot };
8642
+ }
8643
+ model.ingest(view, deps.now());
8644
+ lastSnapshot = model.snapshot(deps.now());
8645
+ if (model.isTerminal()) {
8646
+ deps.onTerminal(lastSnapshot);
8647
+ return {
8648
+ outcome: lastSnapshot.terminal === "live" ? "live" : "failed",
8649
+ snapshot: lastSnapshot
8650
+ };
8651
+ }
8652
+ deps.onTick(lastSnapshot);
8653
+ backoffMs = opts.pollIntervalMs;
8654
+ } catch (error) {
8655
+ backoffMs = Math.min(backoffMs * opts.backoffFactor, opts.maxBackoffMs);
8656
+ nextDelay = backoffMs;
8657
+ deps.onPollError?.(error, nextDelay);
8658
+ }
8659
+ if (deps.signal.aborted)
8660
+ return { outcome: "interrupted", snapshot: lastSnapshot };
8661
+ if (deps.now() - startedAt >= opts.maxDurationMs) {
8662
+ return { outcome: "timeout", snapshot: lastSnapshot };
8663
+ }
8664
+ await deps.sleep(nextDelay, deps.signal);
8665
+ }
8666
+ }
8667
+ function makeRealSleep() {
8668
+ return (ms, signal) => new Promise((resolve11) => {
8669
+ if (signal.aborted)
8670
+ return resolve11();
8671
+ const t = setTimeout(resolve11, ms);
8672
+ signal.addEventListener("abort", () => {
8673
+ clearTimeout(t);
8674
+ resolve11();
8675
+ }, { once: true });
8676
+ });
8677
+ }
8678
+
8679
+ // src/watch/sanitize.ts
8680
+ function isControlCodePoint(cp) {
8681
+ return cp <= 31 || cp >= 127 && cp <= 159;
8682
+ }
8683
+ function sanitizeForTerminal(input2) {
8684
+ let out = "";
8685
+ for (const ch of input2) {
8686
+ const cp = ch.codePointAt(0);
8687
+ if (cp !== undefined && !isControlCodePoint(cp))
8688
+ out += ch;
8689
+ }
8690
+ return out;
8691
+ }
8692
+ function sanitizeField(input2, max = 160) {
8693
+ if (input2 == null)
8694
+ return "";
8695
+ const clean = sanitizeForTerminal(input2);
8696
+ if (clean.length <= max)
8697
+ return clean;
8698
+ return clean.slice(0, Math.max(0, max - 1)) + "…";
8699
+ }
8700
+
8701
+ // src/watch/render.ts
8702
+ var GLYPHS = {
8703
+ unicode: {
8704
+ done: "✓",
8705
+ pending: "○",
8706
+ fail: "✗",
8707
+ active: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
8708
+ sep: "·",
8709
+ arrow: "→",
8710
+ dash: "—"
8711
+ },
8712
+ ascii: {
8713
+ done: "v",
8714
+ pending: ".",
8715
+ fail: "x",
8716
+ active: ["|", "/", "-", "\\"],
8717
+ sep: "-",
8718
+ arrow: "->",
8719
+ dash: "-"
8720
+ }
8721
+ };
8722
+ var ESC = "\x1B";
8723
+ var CURSOR_HIDE = `${ESC}[?25l`;
8724
+ var CURSOR_SHOW = `${ESC}[?25h`;
8725
+ var cursorUp = (n) => n > 0 ? `${ESC}[${n}A` : "";
8726
+ var CLEAR_FROM_CURSOR = `${ESC}[0J`;
8727
+ function styleForTier(tier) {
8728
+ return tier === "inline" ? "unicode" : "ascii";
8729
+ }
8730
+ function launchpadUrl(slug) {
8731
+ return `https://${sanitizeField(slug, 80)}.launchpad.m-kopa.us`;
8732
+ }
8733
+ function formatDuration(ms, dash = "—") {
8734
+ if (ms == null)
8735
+ return dash;
8736
+ const total = Math.floor(ms / 1000);
8737
+ const m = Math.floor(total / 60);
8738
+ const s = total % 60;
8739
+ return `${m}:${String(s).padStart(2, "0")}`;
8740
+ }
8741
+ function phaseGlyph(g, status, spinnerFrame) {
8742
+ switch (status) {
8743
+ case "done":
8744
+ return g.done;
8745
+ case "failed":
8746
+ return g.fail;
8747
+ case "active":
8748
+ return g.active[spinnerFrame % g.active.length] ?? g.active[0];
8749
+ default:
8750
+ return g.pending;
8751
+ }
8752
+ }
8753
+ function headerSpinner(g, spinnerFrame) {
8754
+ return g.active[spinnerFrame % g.active.length] ?? g.active[0];
8755
+ }
8756
+ var PAD = 12;
8757
+ function renderProvisioningFrame(s, opts) {
8758
+ const g = GLYPHS[opts.style];
8759
+ const slug = sanitizeField(s.slug, 80);
8760
+ const lines = [];
8761
+ lines.push(` launchpad ${g.sep} provisioning ${slug} ${headerSpinner(g, opts.spinnerFrame)} ${formatDuration(s.elapsedTotalMs, g.dash)}`);
8762
+ lines.push("");
8763
+ for (const p of s.phases) {
8764
+ const glyph = phaseGlyph(g, p.status, opts.spinnerFrame);
8765
+ const name = p.name.padEnd(PAD);
8766
+ const detail = p.status === "active" ? "..." : p.status === "done" ? formatDuration(p.elapsedMs, g.dash) : "";
8767
+ lines.push(` ${glyph} ${name} ${detail}`);
8768
+ }
8769
+ lines.push("");
8770
+ const stage = s.currentStage ? sanitizeField(s.currentStage, 40) : g.dash;
8771
+ const ord = s.stageOrdinal ?? "?";
8772
+ lines.push(` stage ${ord}/${s.totalStages} ${g.sep} ${stage}`);
8773
+ lines.push(` Ctrl-C to stop watching ${g.sep} provisioning continues`);
8774
+ return lines.join(`
8775
+ `);
8776
+ }
8777
+ function renderAppendLine(s, opts) {
8778
+ const g = GLYPHS[opts.style];
8779
+ const active = s.phases.find((p) => p.status === "active");
8780
+ const phase = active ? active.name : s.terminal === "live" ? "live" : "...";
8781
+ const stage = s.currentStage ? sanitizeField(s.currentStage, 40) : g.dash;
8782
+ return ` ${headerSpinner(g, opts.spinnerFrame)} ${phase} ${g.sep} stage ${s.stageOrdinal ?? "?"}/${s.totalStages} ${g.sep} ${stage} ${g.sep} ${formatDuration(s.elapsedTotalMs, g.dash)}`;
8783
+ }
8784
+ function renderPlainLine(s) {
8785
+ const slug = sanitizeField(s.slug, 80);
8786
+ if (s.terminal === "live") {
8787
+ const url = s.liveUrl ? sanitizeField(s.liveUrl, 120) : launchpadUrl(s.slug);
8788
+ return `${slug}: live -> ${url}`;
8789
+ }
8790
+ if (s.terminal === "failed") {
8791
+ const stage2 = s.failure ? sanitizeField(s.failure.stage, 40) : "unknown";
8792
+ const reason = s.failure ? sanitizeField(s.failure.message, 200) : "";
8793
+ return `${slug}: failed at ${stage2}${reason ? ` - ${reason}` : ""}`;
8794
+ }
8795
+ const stage = s.currentStage ? sanitizeField(s.currentStage, 40) : "pending";
8796
+ return `${slug}: provisioning (stage: ${stage}) [${s.stageOrdinal ?? "?"}/${s.totalStages}]`;
8797
+ }
8798
+ function renderLiveFrame(s, opts) {
8799
+ const g = GLYPHS[opts.style];
8800
+ const slug = sanitizeField(s.slug, 80);
8801
+ const url = s.liveUrl ? sanitizeField(s.liveUrl, 120) : launchpadUrl(s.slug);
8802
+ const row = s.phases.map((p) => `${p.name} ${g.done}`).join(" ");
8803
+ return [
8804
+ ` launchpad ${g.sep} ${slug} is LIVE`,
8805
+ "",
8806
+ ` ${row}`,
8807
+ "",
8808
+ ` Live ${g.arrow} ${url}`
8809
+ ].join(`
8810
+ `);
8811
+ }
8812
+ function renderFailedFrame(s, opts) {
8813
+ const g = GLYPHS[opts.style];
8814
+ const slug = sanitizeField(s.slug, 80);
8815
+ const row = s.phases.map((p) => `${p.name} ${p.status === "failed" ? g.fail : p.status === "done" ? g.done : g.pending}`).join(" ");
8816
+ const stage = s.failure ? sanitizeField(s.failure.stage, 40) : "—";
8817
+ const reason = s.failure ? sanitizeField(s.failure.message, 200) : "unknown";
8818
+ const recovery = s.failure?.retryable === false ? "recover" : "deploy --resume";
8819
+ return [
8820
+ ` launchpad ${g.sep} ${slug} ${g.dash} provisioning failed`,
8821
+ "",
8822
+ ` ${row}`,
8823
+ "",
8824
+ ` ${g.fail} ${stage}: ${reason}`,
8825
+ ` Next ${g.arrow} launchpad ${recovery} ${slug}`
8826
+ ].join(`
8827
+ `);
8828
+ }
8829
+
8830
+ class Renderer {
8831
+ write;
8832
+ tier;
8833
+ prevLines = 0;
8834
+ spinnerFrame = 0;
8835
+ cursorHidden = false;
8836
+ style;
8837
+ constructor(write, tier) {
8838
+ this.write = write;
8839
+ this.tier = tier;
8840
+ this.style = styleForTier(tier);
8841
+ }
8842
+ tick(s) {
8843
+ if (this.tier === "plain")
8844
+ return;
8845
+ this.spinnerFrame += 1;
8846
+ if (this.tier === "append") {
8847
+ this.write(renderAppendLine(s, { style: this.style, spinnerFrame: this.spinnerFrame }) + `
8848
+ `);
8849
+ return;
8850
+ }
8851
+ if (!this.cursorHidden) {
8852
+ this.write(CURSOR_HIDE);
8853
+ this.cursorHidden = true;
8854
+ }
8855
+ this.redraw(renderProvisioningFrame(s, { style: this.style, spinnerFrame: this.spinnerFrame }));
8856
+ }
8857
+ terminal(s) {
8858
+ if (this.tier === "plain")
8859
+ return;
8860
+ const frame = s.terminal === "live" ? renderLiveFrame(s, { style: this.style }) : renderFailedFrame(s, { style: this.style });
8861
+ if (this.tier === "append") {
8862
+ this.write(frame + `
8863
+ `);
8864
+ } else {
8865
+ this.redraw(frame);
8866
+ }
8867
+ this.restoreCursor();
8868
+ }
8869
+ restoreCursor() {
8870
+ if (this.cursorHidden) {
8871
+ this.write(CURSOR_SHOW);
8872
+ this.cursorHidden = false;
8873
+ }
8874
+ }
8875
+ redraw(body) {
8876
+ const out = (this.prevLines > 0 ? cursorUp(this.prevLines) + CLEAR_FROM_CURSOR : "") + body + `
8877
+ `;
8878
+ this.write(out);
8879
+ this.prevLines = body.split(`
8880
+ `).length;
8881
+ }
8882
+ }
8883
+
8884
+ // src/watch/index.ts
8885
+ async function fetchLifecycleView(cfg, slug) {
8886
+ try {
8887
+ return await apiJson(cfg, {
8888
+ path: `/apps/${slug}/lifecycle`
8889
+ });
8890
+ } catch (e) {
8891
+ if (e instanceof NotFoundError)
8892
+ return null;
8893
+ throw e;
8894
+ }
8895
+ }
8896
+ function mapWatchError(e, slug, io) {
8897
+ if (e instanceof UnauthenticatedError) {
8898
+ io.err(`launchpad watch: ${e.message}`);
8899
+ io.err(" session expired, run `launchpad login`");
8900
+ return 2;
8901
+ }
8902
+ if (e instanceof ForbiddenError) {
8903
+ io.err(`launchpad watch: not authorised for app "${slug}" (owner or editor required)`);
8904
+ return 2;
8905
+ }
8906
+ if (e instanceof NotFoundError) {
8907
+ io.err(`launchpad watch: app "${slug}" not found`);
8908
+ return 2;
8909
+ }
8910
+ if (e instanceof ApiError || e instanceof TransportError) {
8911
+ io.err(`launchpad watch: ${e.message}`);
8912
+ return 2;
8913
+ }
8914
+ io.err(`launchpad watch failed: ${e instanceof Error ? e.message : String(e)}`);
8915
+ return 2;
8916
+ }
8917
+ function defaultSignalRegistrar(handler) {
8918
+ process.on("SIGINT", handler);
8919
+ process.on("SIGTERM", handler);
8920
+ return () => {
8921
+ process.off("SIGINT", handler);
8922
+ process.off("SIGTERM", handler);
8923
+ };
8924
+ }
8925
+ async function runWatch(slug, io, deps = {}) {
8926
+ const cfg = loadConfig();
8927
+ const now = deps.now ?? (() => Date.now());
8928
+ const stdout = deps.stdout ?? process.stdout;
8929
+ let first;
8930
+ try {
8931
+ first = await fetchLifecycleView(cfg, slug);
8932
+ } catch (e) {
8933
+ return mapWatchError(e, slug, io);
8934
+ }
8935
+ if (first === null) {
8936
+ io.err(`launchpad watch: no app "${slug}" to watch (unknown slug, or first deploy not started)`);
8937
+ return 1;
8938
+ }
8939
+ if (first.state === "destroyed" || first.state === "destroying" || first.state === "destroy_failed") {
8940
+ io.out(`${slug}: ${first.state} — nothing to watch.`);
8941
+ return 0;
8942
+ }
8943
+ const { tier } = detectTier(stdout);
8944
+ const renderer = new Renderer((s) => stdout.write(s), tier);
8945
+ const controller = new AbortController;
8946
+ const registrar = deps.onSignal ?? defaultSignalRegistrar;
8947
+ const unregister = registrar(() => {
8948
+ controller.abort();
8949
+ renderer.restoreCursor();
8950
+ });
8951
+ const onTick = (s) => {
8952
+ if (tier === "plain")
8953
+ io.out(renderPlainLine(s));
8954
+ else
8955
+ renderer.tick(s);
8956
+ };
8957
+ const onTerminal = (s) => {
8958
+ if (tier === "plain")
8959
+ io.out(renderPlainLine(s));
8960
+ else
8961
+ renderer.terminal(s);
8962
+ };
8963
+ try {
8964
+ const res = await runWatchLoop(slug, {
8965
+ fetchLifecycle: () => fetchLifecycleView(cfg, slug),
8966
+ now,
8967
+ sleep: makeRealSleep(),
8968
+ signal: controller.signal,
8969
+ onTick,
8970
+ onTerminal
8971
+ }, deps.engineOptions);
8972
+ switch (res.outcome) {
8973
+ case "live":
8974
+ return 0;
8975
+ case "failed":
8976
+ return 1;
8977
+ case "not_found":
8978
+ io.err(`launchpad watch: app "${slug}" is no longer known to the platform`);
8979
+ return 1;
8980
+ case "interrupted":
8981
+ io.err(`
8982
+ stopped watching ${slug} — provisioning continues. Re-run \`launchpad watch ${slug}\`.`);
8983
+ return 130;
8984
+ case "timeout":
8985
+ io.err(`
8986
+ still provisioning after the watch window — re-run \`launchpad watch ${slug}\` or check \`launchpad status ${slug}\`.`);
8987
+ return 0;
8988
+ }
8989
+ } finally {
8990
+ unregister();
8991
+ renderer.restoreCursor();
8992
+ }
8993
+ }
8994
+
8995
+ // src/commands/status.ts
8409
8996
  var statusCommand = {
8410
8997
  name: "status",
8411
8998
  summary: "show drift between local launchpad.yaml and deployed state",
@@ -8449,6 +9036,9 @@ async function runStatus(args, io) {
8449
9036
  printUsage6(io);
8450
9037
  return 64;
8451
9038
  }
9039
+ if (parsed.watch) {
9040
+ return runWatch(parsed.slug, io);
9041
+ }
8452
9042
  const cfg = loadConfig();
8453
9043
  let lifecycle;
8454
9044
  try {
@@ -8815,6 +9405,7 @@ function parseArgs9(args, cwd = process.cwd(), warn) {
8815
9405
  let file = "./launchpad.yaml";
8816
9406
  let json = false;
8817
9407
  let strict = false;
9408
+ let watch = false;
8818
9409
  let i = 0;
8819
9410
  while (i < args.length) {
8820
9411
  const a = args[i] ?? "";
@@ -8847,6 +9438,11 @@ function parseArgs9(args, cwd = process.cwd(), warn) {
8847
9438
  i += 1;
8848
9439
  continue;
8849
9440
  }
9441
+ if (a === "--watch") {
9442
+ watch = true;
9443
+ i += 1;
9444
+ continue;
9445
+ }
8850
9446
  if (a.startsWith("--")) {
8851
9447
  return `unknown flag "${a}"`;
8852
9448
  }
@@ -8866,7 +9462,7 @@ function parseArgs9(args, cwd = process.cwd(), warn) {
8866
9462
  if (!SLUG_RE11.test(slug)) {
8867
9463
  return `invalid slug "${slug}" — expected ${SLUG_RE11.source}`;
8868
9464
  }
8869
- return { slug, file, json, strict };
9465
+ return { slug, file, json, strict, watch };
8870
9466
  }
8871
9467
  function printUsage6(io) {
8872
9468
  io.err([
@@ -8911,6 +9507,53 @@ function isEnoent(e) {
8911
9507
  return typeof e === "object" && e !== null && e.code === "ENOENT";
8912
9508
  }
8913
9509
 
9510
+ // src/commands/watch.ts
9511
+ var SLUG_RE12 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
9512
+ function printUsage7(io) {
9513
+ io.err("usage: launchpad watch [<slug>] [--slug <slug>] [--file <path>]");
9514
+ io.err(" Watch an app provision to live in real time.");
9515
+ }
9516
+ function resolveWatchSlug(args, cwd, warn) {
9517
+ let explicit;
9518
+ let file;
9519
+ for (let i = 0;i < args.length; i += 1) {
9520
+ const a = args[i];
9521
+ if (a === "--slug") {
9522
+ explicit = args[++i];
9523
+ } else if (a === "--file") {
9524
+ file = args[++i];
9525
+ } else if (a === "--watch") {} else if (a.startsWith("-")) {
9526
+ return { error: `unknown flag: ${a}` };
9527
+ } else if (explicit === undefined) {
9528
+ explicit = a;
9529
+ } else {
9530
+ return { error: `unexpected argument: ${a}` };
9531
+ }
9532
+ }
9533
+ const slug = explicit ?? inferSlug({ cwd, file, warn }) ?? undefined;
9534
+ if (slug === undefined) {
9535
+ return { error: "no slug given and none could be inferred from the current directory" };
9536
+ }
9537
+ if (!SLUG_RE12.test(slug)) {
9538
+ return { error: `invalid slug: "${slug}"` };
9539
+ }
9540
+ return { slug };
9541
+ }
9542
+ async function runWatchCommand(args, io) {
9543
+ const resolved = resolveWatchSlug(args, process.cwd(), (l) => io.err(l));
9544
+ if ("error" in resolved) {
9545
+ io.err(`launchpad watch: ${resolved.error}`);
9546
+ printUsage7(io);
9547
+ return 64;
9548
+ }
9549
+ return runWatch(resolved.slug, io);
9550
+ }
9551
+ var watchCommand = {
9552
+ name: "watch",
9553
+ summary: "watch an app provision to live in real time",
9554
+ run: runWatchCommand
9555
+ };
9556
+
8914
9557
  // src/commands/review.ts
8915
9558
  import * as path12 from "node:path";
8916
9559
  var reviewCommand = {
@@ -8918,7 +9561,7 @@ var reviewCommand = {
8918
9561
  summary: "show the review state for a PR (slug-scoped)",
8919
9562
  run: runReview
8920
9563
  };
8921
- var SLUG_RE12 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
9564
+ var SLUG_RE13 = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
8922
9565
  async function runReview(args, io) {
8923
9566
  const parsed = parseArgs10(args);
8924
9567
  if (parsed === null) {
@@ -8938,8 +9581,8 @@ async function runReview(args, io) {
8938
9581
  }
8939
9582
  slug = inferred;
8940
9583
  }
8941
- if (!SLUG_RE12.test(slug)) {
8942
- io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE12.source}`);
9584
+ if (!SLUG_RE13.test(slug)) {
9585
+ io.err(`launchpad review: invalid slug "${slug}" — expected ${SLUG_RE13.source}`);
8943
9586
  return 64;
8944
9587
  }
8945
9588
  try {
@@ -11851,6 +12494,7 @@ var COMMANDS = [
11851
12494
  generateCommand,
11852
12495
  pullCommand,
11853
12496
  statusCommand,
12497
+ watchCommand,
11854
12498
  recoverCommand,
11855
12499
  destroyCommand,
11856
12500
  rollbackCommand,
@@ -1,3 +1,4 @@
1
+ import { type CliConfig } from "../config.js";
1
2
  import { type DeploymentStatusView, type StandingExceptionView } from "../deploy/deployment-status.js";
2
3
  import { type Manifest } from "@m-kopa/launchpad-engine";
3
4
  import type { Command } from "../dispatcher.js";
@@ -7,6 +8,24 @@ interface StatusArgs {
7
8
  readonly file: string;
8
9
  readonly json: boolean;
9
10
  readonly strict: boolean;
11
+ /** `--watch`: delegate to the live `launchpad watch` renderer (sp-wtch7q). */
12
+ readonly watch: boolean;
13
+ }
14
+ /** The bot's `/apps/<slug>/lifecycle` response (sp-st5hw9) — mirrors the
15
+ * bot-side `LifecycleResponse`. Lets status show provisioning progress + a
16
+ * clear failed state instead of the misleading "no deployed manifest yet". */
17
+ interface LifecycleView {
18
+ readonly slug: string;
19
+ readonly state: "provisioning" | "live" | "failed" | "destroying" | "destroyed" | "destroy_failed";
20
+ readonly stage?: string;
21
+ readonly submissionId?: string;
22
+ readonly failedAt?: {
23
+ readonly stage: string;
24
+ readonly message: string;
25
+ readonly retryable: boolean;
26
+ };
27
+ readonly liveUrl?: string;
28
+ readonly lifecycleReason?: string;
10
29
  }
11
30
  /** What status reports, in machine-readable form for downstream
12
31
  * consumers (M-1187 destroy, M-1189 update-skill). The lifecycle states
@@ -56,6 +75,11 @@ export interface StatusJsonOutput {
56
75
  * Absent when the bot pre-dates the endpoint. */
57
76
  readonly standingExceptions?: readonly StandingExceptionView[];
58
77
  }
78
+ /** Fetch the app's provisioning lifecycle (sp-st5hw9). Returns null when the
79
+ * bot doesn't recognise the slug OR predates the endpoint — BOTH surface as a
80
+ * 404, so the caller degrades to the manifest-diff path (which raises a real
81
+ * not-found itself for a truly-unknown slug). Auth / forbidden propagate. */
82
+ export declare function fetchLifecycle(cfg: CliConfig, slug: string): Promise<LifecycleView | null>;
59
83
  /**
60
84
  * Compute drift over the v1 closed field set (AC5).
61
85
  *
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AA6CA,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AASxC,OAAO,EAAqC,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAEjE,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAEF,UAAU,UAAU;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAuBD;;;uEAGuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EACV,cAAc,GACd,qBAAqB,GACrB,iBAAiB;IACnB;;wDAEoD;OAClD,wBAAwB,GACxB,SAAS,GACT,OAAO;IACT;;oEAEgE;OAC9D,oBAAoB,GACpB,sBAAsB,GACtB,YAAY,GACZ,WAAW,GACX,gBAAgB,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;sEAGkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sDAAsD;IACtD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;QACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC,CAAC;IACH;;;wCAGoC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAClD;;sDAEkD;IAClD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAChE;AAuRD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,QAAQ,GACjB,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CA2DpE;AAuRD;0BAC0B;AAC1B,wBAAgB,SAAS,CACvB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,GAAE,MAAsB,EAC3B,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC5B,UAAU,GAAG,MAAM,CAqErB"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AA0CA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAG1D,OAAO,EAGL,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AASxC,OAAO,EAAqC,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAE5F,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAEjE,eAAO,MAAM,aAAa,EAAE,OAI3B,CAAC;AAEF,UAAU,UAAU;IAClB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAID;;+EAE+E;AAC/E,UAAU,aAAa;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EACV,cAAc,GACd,MAAM,GACN,QAAQ,GACR,YAAY,GACZ,WAAW,GACX,gBAAgB,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IACtG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;uEAGuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EACV,cAAc,GACd,qBAAqB,GACrB,iBAAiB;IACnB;;wDAEoD;OAClD,wBAAwB,GACxB,SAAS,GACT,OAAO;IACT;;oEAEgE;OAC9D,oBAAoB,GACpB,sBAAsB,GACtB,YAAY,GACZ,WAAW,GACX,gBAAgB,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;sEAGkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sDAAsD;IACtD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,oFAAoF;IACpF,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;QACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;KAC5B,CAAC,CAAC;IACH;;;wCAGoC;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAClD;;sDAEkD;IAClD,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAChE;AAED;;;8EAG8E;AAC9E,wBAAsB,cAAc,CAClC,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAO/B;AA4QD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,QAAQ,GACjB,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,CA2DpE;AAuRD;0BAC0B;AAC1B,wBAAgB,SAAS,CACvB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,GAAE,MAAsB,EAC3B,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC5B,UAAU,GAAG,MAAM,CA2ErB"}
@@ -0,0 +1,9 @@
1
+ import type { Command } from "../dispatcher.js";
2
+ /** Resolve the slug from args or the cwd, or return an error string. */
3
+ export declare function resolveWatchSlug(args: readonly string[], cwd: string, warn: (line: string) => void): {
4
+ slug: string;
5
+ } | {
6
+ error: string;
7
+ };
8
+ export declare const watchCommand: Command;
9
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/commands/watch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAmB,MAAM,kBAAkB,CAAC;AAWjE,wEAAwE;AACxE,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CA2BtC;AAYD,eAAO,MAAM,YAAY,EAAE,OAI1B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AAmDA,MAAM,WAAW,KAAK;IACpB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAE9B,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxE;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,OAAO,EAiCtC,CAAC;AAEF;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,QAAQ,GAAE,SAAS,OAAO,EAAa,GACtC,OAAO,CAAC,QAAQ,CAAC,CAmBnB;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAyBvE"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AAoDA,MAAM,WAAW,KAAK;IACpB,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAE9B,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxE;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,OAAO,EAkCtC,CAAC;AAEF;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,KAAK,EACT,QAAQ,GAAE,SAAS,OAAO,EAAa,GACtC,OAAO,CAAC,QAAQ,CAAC,CAmBnB;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAyBvE"}
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.32.2";
1
+ export declare const CLI_VERSION = "0.33.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,23 @@
1
+ export type RenderTier = "inline" | "ascii" | "append" | "plain";
2
+ export interface TerminalCaps {
3
+ readonly isTTY: boolean;
4
+ /** Cursor-control / VT escape sequences are safe to emit. */
5
+ readonly ansi: boolean;
6
+ /** Unicode box/braille glyphs render correctly. */
7
+ readonly unicode: boolean;
8
+ }
9
+ export interface ProbeEnv {
10
+ readonly platform: NodeJS.Platform;
11
+ readonly env: NodeJS.ProcessEnv;
12
+ readonly isTTY: boolean;
13
+ }
14
+ export declare function probeCapabilities(p: ProbeEnv): TerminalCaps;
15
+ export declare function selectTier(caps: TerminalCaps): RenderTier;
16
+ /** Convenience: probe the live process + pick a tier. */
17
+ export declare function detectTier(stream?: {
18
+ isTTY?: boolean;
19
+ }): {
20
+ caps: TerminalCaps;
21
+ tier: RenderTier;
22
+ };
23
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/watch/capabilities.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IACnC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAqBD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,QAAQ,GAAG,YAAY,CAsB3D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,UAAU,CAIzD;AAED,yDAAyD;AACzD,wBAAgB,UAAU,CAAC,MAAM,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAmB,GAAG;IACxE,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;CAClB,CAOA"}
@@ -0,0 +1,35 @@
1
+ import { type LifecycleSnapshot, type WatchSnapshot } from "./model.js";
2
+ export type WatchOutcome = "live" | "failed" | "timeout" | "interrupted" | "not_found";
3
+ export interface WatchEngineDeps {
4
+ /** One poll. Resolves to the lifecycle, or null when the slug is
5
+ * unknown to the bot (terminal: nothing to watch). Throws on transient
6
+ * / network / 5xx errors (the loop backs off and retries). */
7
+ readonly fetchLifecycle: () => Promise<LifecycleSnapshot | null>;
8
+ readonly now: () => number;
9
+ /** Resolves after `ms`, or early if `signal` aborts. */
10
+ readonly sleep: (ms: number, signal: AbortSignal) => Promise<void>;
11
+ /** Aborts the watch (wired to SIGINT/SIGTERM by the caller). */
12
+ readonly signal: AbortSignal;
13
+ /** Called every poll with the latest snapshot (the live frame). */
14
+ readonly onTick: (snapshot: WatchSnapshot) => void;
15
+ /** Called once on a terminal state (live/failed) with the final frame. */
16
+ readonly onTerminal: (snapshot: WatchSnapshot) => void;
17
+ /** Non-fatal: a transient poll error the loop is retrying. */
18
+ readonly onPollError?: (error: unknown, nextRetryMs: number) => void;
19
+ }
20
+ export interface WatchEngineOptions {
21
+ readonly pollIntervalMs?: number;
22
+ readonly backoffFactor?: number;
23
+ readonly maxBackoffMs?: number;
24
+ readonly maxDurationMs?: number;
25
+ }
26
+ export interface WatchResult {
27
+ readonly outcome: WatchOutcome;
28
+ /** The last snapshot rendered (null only if the very first poll said
29
+ * the slug is unknown). */
30
+ readonly snapshot: WatchSnapshot | null;
31
+ }
32
+ export declare function runWatchLoop(slug: string, deps: WatchEngineDeps, options?: WatchEngineOptions): Promise<WatchResult>;
33
+ /** Real interruptible sleep (production). */
34
+ export declare function makeRealSleep(): (ms: number, signal: AbortSignal) => Promise<void>;
35
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/watch/engine.ts"],"names":[],"mappings":"AAQA,OAAO,EAAc,KAAK,iBAAiB,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpF,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,aAAa,GACb,WAAW,CAAC;AAEhB,MAAM,WAAW,eAAe;IAC9B;;mEAE+D;IAC/D,QAAQ,CAAC,cAAc,EAAE,MAAM,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACjE,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;IACnD,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;IACvD,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACtE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AASD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B;gCAC4B;IAC5B,QAAQ,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;CACzC;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,eAAe,EACrB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CA2CtB;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAclF"}
@@ -0,0 +1,19 @@
1
+ import type { CliIo, ExitCode } from "../dispatcher.js";
2
+ import { type WatchEngineOptions } from "./engine.js";
3
+ export interface RunWatchDeps {
4
+ readonly now?: () => number;
5
+ readonly stdout?: {
6
+ isTTY?: boolean;
7
+ write: (s: string) => void;
8
+ };
9
+ /** Test seam for signal registration; defaults to process. */
10
+ readonly onSignal?: (handler: () => void) => () => void;
11
+ readonly engineOptions?: WatchEngineOptions;
12
+ }
13
+ /**
14
+ * Watch a slug provision to live. Returns an exit code:
15
+ * 0 live (or timed-out-but-still-going); 1 failed / not-found;
16
+ * 2 auth/transport; 130 interrupted (Ctrl-C).
17
+ */
18
+ export declare function runWatch(slug: string, io: CliIo, deps?: RunWatchDeps): Promise<ExitCode>;
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/watch/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAA+B,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA6CnF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IAClE,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IACxD,QAAQ,CAAC,aAAa,CAAC,EAAE,kBAAkB,CAAC;CAC7C;AAWD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA2ElG"}
@@ -0,0 +1,69 @@
1
+ import { type PhaseName } from "./phases.js";
2
+ /** The subset of the bot's LifecycleView the watcher consumes. */
3
+ export interface LifecycleSnapshot {
4
+ readonly state: "provisioning" | "live" | "failed" | "destroying" | "destroyed" | "destroy_failed";
5
+ readonly stage?: string;
6
+ readonly failedAt?: {
7
+ readonly stage: string;
8
+ readonly message: string;
9
+ readonly retryable: boolean;
10
+ };
11
+ readonly liveUrl?: string;
12
+ }
13
+ export type PhaseStatus = "pending" | "active" | "done" | "failed";
14
+ export interface PhaseView {
15
+ readonly name: PhaseName;
16
+ readonly status: PhaseStatus;
17
+ /** Observed client-side elapsed for this phase, or null when unknown
18
+ * (the phase completed before the watch attached, or hasn't started). */
19
+ readonly elapsedMs: number | null;
20
+ }
21
+ export type Terminal = "live" | "failed" | null;
22
+ export interface WatchSnapshot {
23
+ readonly slug: string;
24
+ readonly phases: readonly PhaseView[];
25
+ readonly currentStage: string | null;
26
+ /** N in "stage N/15" (15 when live). */
27
+ readonly stageOrdinal: number | null;
28
+ readonly totalStages: number;
29
+ readonly terminal: Terminal;
30
+ readonly failure: {
31
+ readonly stage: string;
32
+ readonly message: string;
33
+ readonly retryable: boolean;
34
+ } | null;
35
+ readonly liveUrl: string | null;
36
+ readonly startedAtMs: number;
37
+ readonly elapsedTotalMs: number;
38
+ }
39
+ /**
40
+ * Accumulates lifecycle observations into a renderable snapshot. One
41
+ * instance per watch session; `ingest` is called once per poll tick.
42
+ */
43
+ export declare class WatchModel {
44
+ private readonly slug;
45
+ private startedAtMs;
46
+ /** First time each stage was observed (client-side). */
47
+ private readonly stageFirstSeen;
48
+ /** Order in which stages were first observed. */
49
+ private readonly observedOrder;
50
+ private currentStage;
51
+ private terminal;
52
+ private failure;
53
+ private liveUrl;
54
+ constructor(slug: string);
55
+ /** Fold one poll result into the model. */
56
+ ingest(view: LifecycleSnapshot, nowMs: number): void;
57
+ /** True once a terminal (live/failed) state has been observed. */
58
+ isTerminal(): boolean;
59
+ /** Render the current snapshot as of `nowMs`. */
60
+ snapshot(nowMs: number): WatchSnapshot;
61
+ /**
62
+ * Observed elapsed for a phase: from the first observed stage in the
63
+ * phase to the first observed stage of a LATER phase (or `nowMs` if the
64
+ * phase is still the active one). null when we never observed the
65
+ * phase's start (it completed before the watch attached).
66
+ */
67
+ private phaseElapsed;
68
+ }
69
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/watch/model.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,SAAS,EAKf,MAAM,aAAa,CAAC;AAErB,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EACV,cAAc,GACd,MAAM,GACN,QAAQ,GACR,YAAY,GACZ,WAAW,GACX,gBAAgB,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B;8EAC0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEhD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,wCAAwC;IACxC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;KAC7B,GAAG,IAAI,CAAC;IACT,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;GAGG;AACH,qBAAa,UAAU;IAWT,OAAO,CAAC,QAAQ,CAAC,IAAI;IAVjC,OAAO,CAAC,WAAW,CAAuB;IAC1C,wDAAwD;IACxD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,iDAAiD;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,OAAO,CAA8C;IAC7D,OAAO,CAAC,OAAO,CAAuB;gBAET,IAAI,EAAE,MAAM;IAEzC,2CAA2C;IAC3C,MAAM,CAAC,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAmBpD,kEAAkE;IAClE,UAAU,IAAI,OAAO;IAIrB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa;IAsDtC;;;;;OAKG;IACH,OAAO,CAAC,YAAY;CA4BrB"}
@@ -0,0 +1,20 @@
1
+ /** The six phases, in display order. */
2
+ export declare const PHASE_NAMES: readonly ["Repo", "Build", "Infra", "Certificate", "Access", "Verify"];
3
+ export type PhaseName = (typeof PHASE_NAMES)[number];
4
+ /**
5
+ * The 15 working stages in happy-path order. `done` is terminal and is
6
+ * NOT a working stage — reaching it means the Verify phase is complete
7
+ * (the app is live). `stage N/15` uses a stage's index in this array.
8
+ */
9
+ export declare const STAGE_ORDER: readonly ["pending", "repo_created", "bootstrap_pr_opened", "bootstrap_pr_merged", "content_seeded", "tf_pr_opened", "tf_pr_merged", "tf_applied", "cert_active", "policy_attached", "tf_env_pr_opened", "tf_env_pr_merged", "tf_env_applied", "ready_for_content", "deployment_verified"];
10
+ export declare const TOTAL_STAGES: 15;
11
+ /** 1-based ordinal of a working stage, or null if not a working stage. */
12
+ export declare function ordinalOfStage(stage: string): number | null;
13
+ /**
14
+ * The phase a stage belongs to. Returns null for an unrecognised stage
15
+ * (the model then keeps the last-known active phase rather than guessing).
16
+ */
17
+ export declare function phaseOfStage(stage: string): PhaseName | null;
18
+ /** Index of a phase in PHASE_NAMES (display/ordering), or -1. */
19
+ export declare function phaseIndex(phase: PhaseName): number;
20
+ //# sourceMappingURL=phases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phases.d.ts","sourceRoot":"","sources":["../../src/watch/phases.ts"],"names":[],"mappings":"AAWA,wCAAwC;AACxC,eAAO,MAAM,WAAW,wEAOd,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,WAAW,4RAgBd,CAAC;AAEX,eAAO,MAAM,YAAY,IAAqB,CAAC;AAmC/C,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG3D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAE5D;AAED,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,55 @@
1
+ import type { RenderTier } from "./capabilities.js";
2
+ import type { WatchSnapshot } from "./model.js";
3
+ type GlyphStyle = "unicode" | "ascii";
4
+ export declare const CURSOR_HIDE = "\u001B[?25l";
5
+ export declare const CURSOR_SHOW = "\u001B[?25h";
6
+ export declare function styleForTier(tier: RenderTier): GlyphStyle;
7
+ export declare function launchpadUrl(slug: string): string;
8
+ /** ms → "M:SS" (or `dash` when unknown/null). */
9
+ export declare function formatDuration(ms: number | null, dash?: string): string;
10
+ /** The live provisioning frame (multi-line) for inline/ascii tiers. */
11
+ export declare function renderProvisioningFrame(s: WatchSnapshot, opts: {
12
+ style: GlyphStyle;
13
+ spinnerFrame: number;
14
+ }): string;
15
+ /** One-line compact update for the append-only tier. */
16
+ export declare function renderAppendLine(s: WatchSnapshot, opts: {
17
+ style: GlyphStyle;
18
+ spinnerFrame: number;
19
+ }): string;
20
+ /**
21
+ * Plain, ASCII-only, escape-free line for the non-TTY tier (pipes / CI).
22
+ * Mirrors the existing `launchpad status` one-liner style so piped output
23
+ * stays familiar and never carries control sequences.
24
+ */
25
+ export declare function renderPlainLine(s: WatchSnapshot): string;
26
+ /** Final LIVE frame. */
27
+ export declare function renderLiveFrame(s: WatchSnapshot, opts: {
28
+ style: GlyphStyle;
29
+ }): string;
30
+ /** Final FAILED frame. */
31
+ export declare function renderFailedFrame(s: WatchSnapshot, opts: {
32
+ style: GlyphStyle;
33
+ }): string;
34
+ /**
35
+ * Owns the cursor mechanics for the redraw tiers. `write` receives raw
36
+ * strings (the renderer's own trusted ANSI + already-sanitised content).
37
+ */
38
+ export declare class Renderer {
39
+ private readonly write;
40
+ private readonly tier;
41
+ private prevLines;
42
+ private spinnerFrame;
43
+ private cursorHidden;
44
+ private readonly style;
45
+ constructor(write: (s: string) => void, tier: RenderTier);
46
+ /** Per-poll live frame. No-op for the plain tier (caller handles it). */
47
+ tick(s: WatchSnapshot): void;
48
+ /** Final frame; settles the view (no spinner) and restores the cursor. */
49
+ terminal(s: WatchSnapshot): void;
50
+ /** Restore the terminal on any exit path (also called from signal handlers). */
51
+ restoreCursor(): void;
52
+ private redraw;
53
+ }
54
+ export {};
55
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/watch/render.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,YAAY,CAAC;AAE7D,KAAK,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC;AAoCtC,eAAO,MAAM,WAAW,gBAAgB,CAAC;AACzC,eAAO,MAAM,WAAW,gBAAgB,CAAC;AAIzC,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAEzD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,SAAM,GAAG,MAAM,CAMpE;AAqBD,uEAAuE;AACvE,wBAAgB,uBAAuB,CACrC,CAAC,EAAE,aAAa,EAChB,IAAI,EAAE;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAChD,MAAM,CAkBR;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,aAAa,EAChB,IAAI,EAAE;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAChD,MAAM,CAMR;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,CAaxD;AAED,wBAAwB;AACxB,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG,MAAM,CAYrF;AAED,0BAA0B;AAC1B,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE;IAAE,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG,MAAM,CAiBvF;AAED;;;GAGG;AACH,qBAAa,QAAQ;IAOjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPvB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;gBAGhB,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,EAC1B,IAAI,EAAE,UAAU;IAKnC,yEAAyE;IACzE,IAAI,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI;IAc5B,0EAA0E;IAC1E,QAAQ,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI;IAchC,gFAAgF;IAChF,aAAa,IAAI,IAAI;IAOrB,OAAO,CAAC,MAAM;CAKf"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Strip all control characters from an untrusted string so nothing it
3
+ * contains can be interpreted as a terminal control sequence. Printable
4
+ * text (incl. full Unicode) is preserved; an ESC that would introduce a
5
+ * CSI/OSC sequence is removed, so the rest renders as inert literal text.
6
+ */
7
+ export declare function sanitizeForTerminal(input: string): string;
8
+ /**
9
+ * Sanitise + bound a value to a single line of at most `max` glyphs, so a
10
+ * pathologically long bot string can't blow the layout. Adds an ellipsis
11
+ * when truncated.
12
+ */
13
+ export declare function sanitizeField(input: string | null | undefined, max?: number): string;
14
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../../src/watch/sanitize.ts"],"names":[],"mappings":"AAkBA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,SAAM,GAAG,MAAM,CAKjF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m-kopa/launchpad-cli",
3
- "version": "0.32.2",
3
+ "version": "0.33.0",
4
4
  "description": "Launchpad CLI — clone / deploy / review / merge against Launchpad-managed apps. Talks to the portal-bot endpoints (SCOPE-M-760 / T4).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,6 +50,7 @@
50
50
  "lint": "eslint src tests",
51
51
  "typecheck": "tsc --noEmit",
52
52
  "check:version-sync": "bash scripts/check-version-sync.sh",
53
+ "check:runtime-deps": "node scripts/check-runtime-deps.mjs",
53
54
  "check:installer-syntax": "bash scripts/check-installer-syntax.sh",
54
55
  "check:skill-contract": "bash scripts/sync-skill-contract.sh --check",
55
56
  "check:skill-bash-dialect": "bash scripts/check-skill-bash-dialect.sh",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-content-pr
3
3
  description: Push a content change to a Launchpad app via `launchpad deploy` and verify it shipped via `launchpad status`. Covers the post-first-deploy iteration loop (edit → deploy → verify) — subsequent deploys commit directly to the app repo's main and the Pages build runs asynchronously, so verification is its own step. Use when someone says "push a content change", "ship an update", "/launchpad-content-pr", "verify my deploy", or after `/launchpad-deploy` reports `done` and they want to follow up with an edit.
4
- version: 0.32.2
4
+ version: 0.33.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-deploy
3
3
  description: Walk a Launchpad user through deploying an app from their local working directory (Model A — `launchpad init` + `launchpad deploy`). Wraps the CLI verbs end-to-end: detects the app shape, scaffolds `launchpad.yaml`, resolves the allowed Entra group via `launchpad groups`, bundles the CWD via `launchpad deploy`, and watches the rollout via `launchpad status`. Use when someone says "deploy a new app", "ship my app to Launchpad", "/launchpad-deploy", "I have an app locally — get it on Launchpad", or any variant. Resume/abandon for legacy in-flight provisioning is at the bottom.
4
- version: 0.32.2
4
+ version: 0.33.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -271,8 +271,10 @@ fresh slug auto-provisions:
271
271
  5–10 minutes. **Your bundle ships with the provisioning run** — when
272
272
  lifecycle reaches `live`, this deploy's content is what's serving.
273
273
  **No second deploy needed**; re-deploying is only for the rare case
274
- where the app comes up live *without* your content. Watch with
275
- `launchpad status <slug>`.
274
+ where the app comes up live *without* your content. **Watch it go
275
+ live in real time with `launchpad watch <slug>`** (a self-updating
276
+ view of the provisioning pipeline that exits when the app is live or
277
+ failed); `launchpad status <slug>` is the one-shot equivalent.
276
278
  - **Subsequent deploys** (slug already live): the bot extracts the
277
279
  tarball, runs the ingest gates (see below), commits the bundle
278
280
  straight onto `main` of `launchpad-app-<slug>` via the GitHub App
@@ -314,8 +316,9 @@ re-trying.
314
316
 
315
317
  The CLI exits as soon as the bot acknowledges the upload (202). For
316
318
  subsequent deploys it prints the commit short-SHA + repo; for first
317
- deploys it prints provisioning guidance. Use `launchpad status
318
- <slug>` to watch lifecycle progress to its terminal state.
319
+ deploys it prints provisioning guidance. Run `launchpad watch <slug>`
320
+ to watch lifecycle progress live to its terminal state (or
321
+ `launchpad status <slug>` for a one-shot check).
319
322
 
320
323
  Flags — there is nothing useful to pass on the Model A path:
321
324
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-deploy-status
3
- description: Show the current provisioning stage + failure reason for a Launchpad app via `launchpad status` (Model A drift + deployment_verified) and `launchpad apps` (lifecycle bucket). Renders the M-892 stage trace for in-flight provisioning, and is the canonical home for `launchpad recover` (repair a terminal-failed app record that is actually live). Use when someone says "what's the status of demo-X", "/launchpad-deploy-status", "is my deploy stuck", "my app says failed but it's serving", or after `/launchpad-deploy` reports a non-`done` terminal stage.
4
- version: 0.32.2
3
+ description: Show the current provisioning stage + failure reason for a Launchpad app via `launchpad status` (Model A drift + deployment_verified) and `launchpad apps` (lifecycle bucket), or watch provisioning live with `launchpad watch`. Renders the M-892 stage trace for in-flight provisioning, and is the canonical home for `launchpad recover` (repair a terminal-failed app record that is actually live). Use when someone says "what's the status of demo-X", "/launchpad-deploy-status", "is my deploy stuck", "watch my deploy go live", "watch provisioning", "my app says failed but it's serving", or after `/launchpad-deploy` reports a non-`done` terminal stage.
4
+ version: 0.33.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -56,6 +56,7 @@ inference source resolves.
56
56
 
57
57
  | Question | Verb |
58
58
  |---|---|
59
+ | Watch a first deploy provision to live in real time | `launchpad watch <slug>` — see § Watch below |
59
60
  | Is my local manifest in sync with what's deployed? | `launchpad status <slug>` |
60
61
  | What lifecycle bucket is the app in? | `launchpad apps` |
61
62
  | What was actually deployed? | `launchpad pull <slug>` |
@@ -63,8 +64,34 @@ inference source resolves.
63
64
  | What broke the most recent deploy? | `launchpad status <slug> --json` + the bot's PR check trail |
64
65
  | It says `failed` but the app is live in a browser | `launchpad recover <slug>` — see § Recover below |
65
66
 
66
- The Model A default is `launchpad status <slug>`. The other verbs
67
- are specialisations.
67
+ The Model A default is `launchpad status <slug>` (a one-shot read).
68
+ For an in-flight first deploy, `launchpad watch <slug>` is the live
69
+ view. The other verbs are specialisations.
70
+
71
+ ## Watch — live provisioning (`launchpad watch`)
72
+
73
+ For a first deploy still provisioning, `launchpad watch <slug>` (alias:
74
+ `launchpad status <slug> --watch`) renders the pipeline live and updates
75
+ in place until the app is **live** or **failed** — no manual re-running:
76
+
77
+ ```bash
78
+ launchpad watch <slug>
79
+ ```
80
+
81
+ - The 15 workflow stages are grouped into six phases (Repo · Build ·
82
+ Infra · Certificate · Access · Verify) with a spinner on the active
83
+ phase and a `stage N/15` counter. Slug inference matches
84
+ `launchpad status` (manifest slug, then the `launchpad-app-<slug>/`
85
+ dirname).
86
+ - On **live** it settles to a final frame with the app URL; on
87
+ **failed** it shows the failing stage, the reason, and the exact
88
+ recovery command (`launchpad deploy --resume <slug>`, or
89
+ `launchpad recover <slug>` for a terminal-failed-but-live record —
90
+ see § Recover). Exit `0` live, `1` failed, `130` on `Ctrl-C`.
91
+ - `Ctrl-C` stops **watching** only — it never affects provisioning.
92
+ - It adapts to the terminal (full glyphs → ASCII → plain when piped/CI),
93
+ so it is safe to run anywhere. It is **read-only** — it polls the same
94
+ lifecycle `launchpad status` reads, and makes no change.
68
95
 
69
96
  ## Standard (Model A) status
70
97
 
@@ -77,6 +104,8 @@ the canonical reference). Lifecycle-shaped states:
77
104
 
78
105
  - **`provisioning`** — first deploy still in flight. The live
79
106
  workflow stage is shown inline (`stage: …`); see § Stage taxonomy.
107
+ To watch it advance to its terminal state live, use
108
+ `launchpad watch <slug>` (§ Watch).
80
109
  - **`provisioning_failed`** — provisioning failed; the failing stage
81
110
  and reason are shown inline. If the app is actually live, see
82
111
  § Recover.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-destroy
3
3
  description: Tear down a Launchpad app end-to-end via `launchpad destroy` — Cloudflare Pages project, edge-auth wiring (gateway KV/audience entries, or the Access app for `auth: access` apps), custom hostname, platform-repo TF, and the app repo (archive-renamed). Owner-only verb with a two-step destructive confirmation. Use when someone says "destroy this app", "/launchpad-destroy", "tear down `<slug>`", "delete the app", or asks to clean up a smoke-test / orphan / retired app.
4
- version: 0.32.2
4
+ version: 0.33.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-identity
3
3
  description: Teach an app author how to use the signed-in user's identity inside a Launchpad app — read the gateway-forwarded X-Launchpad-User-Assertion in a Pages Function, VERIFY it with @m-kopa/platform-auth (fail-closed), and show who's logged in (sub/email/name). Use when someone says "who is logged in", "show the current user", "get the user's email in my app", "auth in my launchpad app", "read the user identity", "/launchpad-identity", or is wiring up an /api/me for a gateway-fronted app.
4
- version: 0.32.2
4
+ version: 0.33.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-onboard
3
3
  description: One-time setup for the Launchpad CLI + Claude Code skill bundle. Verifies the `launchpad` CLI is installed and current, runs `launchpad whoami` to confirm the session is fresh, and checks the bundled skills are installed and in lock-step with the CLI. Idempotent — safe to re-run any time. Use when someone says "set me up for Launchpad", "I just got a new machine and want to use Launchpad", "/launchpad-onboard", or any of the other launchpad-* skills fails on a prereq check.
4
- version: 0.32.2
4
+ version: 0.33.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-status
3
3
  description: Show whether a Launchpad app's local launchpad.yaml matches what's deployed, and read the deployed manifest. Wraps `launchpad pull` (fetch deployed YAML) and `launchpad status` (drift report). Use when someone says "is my app in sync", "what's deployed", "show drift", "/launchpad-status", "/launchpad-pull", or after `launchpad deploy` to verify the change landed.
4
- version: 0.32.2
4
+ version: 0.33.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->