@m-kopa/launchpad-cli 0.32.1 → 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 +49 -0
- package/dist/cli.js +649 -5
- package/dist/commands/status.d.ts +24 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/watch.d.ts +9 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/dispatcher.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/watch/capabilities.d.ts +23 -0
- package/dist/watch/capabilities.d.ts.map +1 -0
- package/dist/watch/engine.d.ts +35 -0
- package/dist/watch/engine.d.ts.map +1 -0
- package/dist/watch/index.d.ts +19 -0
- package/dist/watch/index.d.ts.map +1 -0
- package/dist/watch/model.d.ts +69 -0
- package/dist/watch/model.d.ts.map +1 -0
- package/dist/watch/phases.d.ts +20 -0
- package/dist/watch/phases.d.ts.map +1 -0
- package/dist/watch/render.d.ts +55 -0
- package/dist/watch/render.d.ts.map +1 -0
- package/dist/watch/sanitize.d.ts +14 -0
- package/dist/watch/sanitize.d.ts.map +1 -0
- package/package.json +2 -1
- package/skills/launchpad-content-pr/SKILL.md +25 -13
- package/skills/launchpad-deploy/SKILL.md +8 -5
- package/skills/launchpad-deploy-status/SKILL.md +34 -5
- package/skills/launchpad-destroy/SKILL.md +1 -1
- package/skills/launchpad-identity/SKILL.md +1 -1
- package/skills/launchpad-onboard/SKILL.md +4 -4
- package/skills/launchpad-status/SKILL.md +3 -3
package/dist/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
|
|
|
19
19
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
20
|
|
|
21
21
|
// src/version.ts
|
|
22
|
-
var CLI_VERSION = "0.
|
|
22
|
+
var CLI_VERSION = "0.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
|
|
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 (!
|
|
8942
|
-
io.err(`launchpad review: invalid slug "${slug}" — expected ${
|
|
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,
|