@m-kopa/launchpad-cli 0.27.0 → 0.27.2

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,46 @@ 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.27.2 — 2026-06-15
10
+
11
+ Fix: `launchpad login --help` and `launchpad logout --help` (and `-h`)
12
+ now print usage and exit without side effects. Previously both verbs
13
+ ignored their arguments and ran the real flow, so asking for help
14
+ actually signed you in / revoked + cleared your session. The flags are
15
+ now documented on each command's docs page.
16
+
17
+ ## 0.27.1 — 2026-06-12
18
+
19
+ Bundled-skills refresh (sp-s4k9wm) — no CLI code changes. Every
20
+ `launchpad-*` skill audited claim-by-claim against shipped behaviour
21
+ (141 claims: 43 corrected, 13 coverage gaps filled):
22
+
23
+ - **Auth guidance** now matches the 0.27.0 gateway login: gateway-first
24
+ sign-in, the ≤ 0.26.x `launchpad update && launchpad login` recovery,
25
+ silent rotating-refresh session model (the "~24 h Cf Access session"
26
+ framing is gone).
27
+ - **`launchpad-content-pr`** reworked to the real deploy model:
28
+ subsequent deploys commit directly to the app repo's `main` (no
29
+ content PR), the CLI returns at the bot's 202 ack, and verification
30
+ is its own `launchpad status` step. The fictional "stack-fit
31
+ pre-flight" is replaced by the real gates (bundle policy, secret
32
+ scan, build-command allowlist — delta-judged per ADR 0025).
33
+ - **`launchpad-deploy`** uses `launchpad init`'s real flag vocabulary,
34
+ documents group displayName|UUID resolution, pages-tier D1
35
+ (`d1_binding`) auto-provisioning incl. the empty-DB gotcha, and the
36
+ real server-side gate set.
37
+ - **`launchpad-deploy-status`** carries the full stage taxonomy
38
+ (`content_seeded`, the `tf_env_*` trio) and folds in
39
+ `launchpad recover` for terminal-failed-but-serving apps.
40
+ - **`launchpad-status`** documents the full lifecycle/live-truth state
41
+ union and the live Pages build-outcome rendering.
42
+ - **`launchpad-destroy`** documents the per-app-workspace dispatch
43
+ teardown (no destroy PR on that path) and that the app's D1 database
44
+ is never dropped.
45
+
46
+ Run `launchpad skills update` after upgrading to pick up the refreshed
47
+ bundle.
48
+
9
49
  ## 0.27.0 — 2026-06-12
10
50
 
11
51
  `launchpad login` moves onto the platform's auth gateway (sp-cli7kq
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/version.ts
22
- var CLI_VERSION = "0.27.0";
22
+ var CLI_VERSION = "0.27.2";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -6724,7 +6724,11 @@ function makeLoginCommand(deps = REAL_DEPS) {
6724
6724
  run: (args, io) => runLogin(args, io, deps)
6725
6725
  };
6726
6726
  }
6727
- async function runLogin(_args, io, deps) {
6727
+ async function runLogin(args, io, deps) {
6728
+ if (args.includes("--help") || args.includes("-h")) {
6729
+ printLoginHelp(io);
6730
+ return 0;
6731
+ }
6728
6732
  try {
6729
6733
  const cfg = loadConfig();
6730
6734
  if (cfg.authLegacy) {
@@ -6775,6 +6779,27 @@ async function runLegacyLogin(io, botUrl, sessionPath, deps) {
6775
6779
  io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
6776
6780
  return 0;
6777
6781
  }
6782
+ function printLoginHelp(io) {
6783
+ io.out("launchpad login — authenticate and store a session.");
6784
+ io.out("");
6785
+ io.out("Usage:");
6786
+ io.out(" launchpad login Sign in via the browser and store a session");
6787
+ io.out("");
6788
+ io.out("Opens your browser to sign in with your M-KOPA Microsoft account");
6789
+ io.out("through the Launchpad auth gateway (loopback PKCE), then writes the");
6790
+ io.out("session to ~/.launchpad/session.json. The short-lived access token");
6791
+ io.out("refreshes silently as you use the CLI.");
6792
+ io.out("");
6793
+ io.out("Environment:");
6794
+ io.out(" LAUNCHPAD_AUTH_LEGACY=1 Force the legacy Cloudflare Access flow");
6795
+ io.out(" (deprecated; removed when the dual-auth");
6796
+ io.out(" window closes)");
6797
+ io.out(" LAUNCHPAD_AUTH_GATEWAY_URL Override the gateway base URL (testing)");
6798
+ io.out("");
6799
+ io.out("Exit codes:");
6800
+ io.out(" 0 signed in / session stored");
6801
+ io.out(" 1 login failed (network, browser, or gateway error)");
6802
+ }
6778
6803
  function describe19(e) {
6779
6804
  return e instanceof Error ? e.message : String(e);
6780
6805
  }
@@ -6789,7 +6814,11 @@ function makeLogoutCommand(deps = REAL_DEPS2) {
6789
6814
  run: (args, io) => runLogout(args, io, deps)
6790
6815
  };
6791
6816
  }
6792
- async function runLogout(_args, io, deps) {
6817
+ async function runLogout(args, io, deps) {
6818
+ if (args.includes("--help") || args.includes("-h")) {
6819
+ printLogoutHelp(io);
6820
+ return 0;
6821
+ }
6793
6822
  try {
6794
6823
  const cfg = loadConfig();
6795
6824
  let session = null;
@@ -6819,6 +6848,22 @@ async function runLogout(_args, io, deps) {
6819
6848
  return 1;
6820
6849
  }
6821
6850
  }
6851
+ function printLogoutHelp(io) {
6852
+ io.out("launchpad logout — revoke the session server-side and clear it locally.");
6853
+ io.out("");
6854
+ io.out("Usage:");
6855
+ io.out(" launchpad logout Revoke server-side, then clear the local session");
6856
+ io.out("");
6857
+ io.out("Gateway (v2) sessions are revoked server-side (the refresh token dies");
6858
+ io.out("immediately; any in-flight access token expires within ~15 minutes),");
6859
+ io.out("then the local ~/.launchpad/session.json is cleared. Logout also works");
6860
+ io.out("offline: if the gateway is unreachable the local session is cleared");
6861
+ io.out("anyway with a warning, and the exit code stays 0.");
6862
+ io.out("");
6863
+ io.out("Exit codes:");
6864
+ io.out(" 0 logged out (or already logged out)");
6865
+ io.out(" 1 could not clear the local session (file IO / config error)");
6866
+ }
6822
6867
  function describe20(e) {
6823
6868
  return e instanceof Error ? e.message : String(e);
6824
6869
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.27.0";
1
+ export declare const CLI_VERSION = "0.27.2";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m-kopa/launchpad-cli",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
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": {
@@ -28,7 +28,7 @@
28
28
  "homepage": "https://github.com/M-KOPA/launchpad-platform/tree/main/packages/launchpad-cli#readme",
29
29
  "license": "UNLICENSED",
30
30
  "dependencies": {
31
- "esbuild": "0.27.3",
31
+ "esbuild": "0.28.1",
32
32
  "yaml": "2.9.0",
33
33
  "zod": "4.4.3"
34
34
  },
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: launchpad-content-pr
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) and the stack-fit pre-flight that the bot enforces server-side. 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.27.0
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.27.2
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->
@@ -33,17 +33,23 @@ esac
33
33
  Push a content change to an already-provisioned Launchpad app, then
34
34
  verify it shipped.
35
35
 
36
+ The slash-command name is historical: subsequent deploys do **not**
37
+ open a content PR any more. The bot commits the bundle **directly to
38
+ the app repo's `main`** and Cloudflare Pages builds from that commit
39
+ asynchronously — so "ship" and "verify" are two separate steps, and
40
+ this skill covers both.
41
+
36
42
  Under Model A the first deploy and the first content are the same
37
43
  event — `launchpad init` + `launchpad deploy` from the user's CWD
38
- opens a PR that contains their working tree, the bot waits for the
39
- Cloudflare Pages deployment to come up green (`deployment_verified`
40
- lifecycle gate), and the lifecycle flips to `live`. There is no
41
- separate "now push content" step.
44
+ stages the working tree with the provisioning run: the workflow's
45
+ `content_seeded` stage commits it straight onto the app repo's
46
+ `main`, the bot waits for the Cloudflare Pages deployment to come up
47
+ green (`deployment_verified`), and the lifecycle flips to `live`.
48
+ There is no separate "now push content" step.
42
49
 
43
50
  This skill is therefore the **iteration** companion to
44
51
  `/launchpad-deploy`: once an app is live, how do you ship the next
45
- change? It is also the canonical place to find the stack-fit
46
- pre-flight (the rules the bot enforces server-side on every bundle).
52
+ change and how do you know it's actually serving.
47
53
 
48
54
  ## Pre-flight
49
55
 
@@ -65,7 +71,10 @@ Look for the slug in the `live` lifecycle bucket. If it's in
65
71
  `provisioning` / `failed` / `destroying` / `destroyed`, the
66
72
  iteration loop is not the right tool — route to
67
73
  `/launchpad-deploy-status` (in-flight) or `/launchpad-destroy`
68
- (teardown) instead.
74
+ (teardown) instead. If it shows `failed` but the app is actually
75
+ serving (e.g. a verification timed out after the real deploy
76
+ succeeded), `launchpad recover <slug>` reconciles the record against
77
+ live Cloudflare state.
69
78
 
70
79
  ## Edit and ship
71
80
 
@@ -75,92 +84,84 @@ The iteration loop is two verbs:
75
84
  # 1. Edit your working tree.
76
85
 
77
86
  # 2. Ship it.
78
- launchpad deploy --message "<one-line description>"
87
+ launchpad deploy
79
88
  ```
80
89
 
81
- The CLI bundles the CWD (using `git ls-files -co --exclude-standard`
82
- where available; falling back to a pure-FS walker honouring
83
- `.gitignore` and a default-ignore set), gzips it, uploads to the
84
- bot, and the bot opens a content PR on `launchpad-app-<slug>`.
85
- Cloudflare Pages auto-deploys on merge to `main`; the bot waits for
86
- the deploy to come up green via the `deployment_verified` lifecycle
87
- gate before reporting `done`.
88
-
89
- Common flags:
90
-
91
- - **`--slug <slug>`** explicit override. Defaults to the slug from
92
- `./launchpad.yaml`, or the cwd-name parse if you're in a
93
- `launchpad-app-<slug>/` clone.
94
- - **`--message <text>`** threaded as the PR description. Useful
95
- for change logs and audit trails.
96
- - **`--file <path>`** — point at a non-default manifest path.
90
+ What happens:
91
+
92
+ - The CLI bundles the CWD with a **pure-FS walker** (no `git`
93
+ needed) honouring `.gitignore` plus a default-ignore set; symlinks
94
+ are never followed. The bundle is gzipped and uploaded to the bot.
95
+ - The bot runs its server-side gates (next section), then commits
96
+ the bundle **directly to `main`** on `launchpad-app-<slug>` via
97
+ the Git Data API — you are the commit author, the bot is the
98
+ committer. No PR, no merge step.
99
+ - The CLI returns at the bot's 202 ack — `✓ Bundle accepted —
100
+ committed as <sha>`, then "Committed; build pending". It does
101
+ **not** wait for the Pages build: a successful deploy is **not** a
102
+ live app yet — the build runs asynchronously and can fail after
103
+ the commit lands. Always confirm with `launchpad status`.
104
+
105
+ Flags worth knowing:
106
+
107
+ - The slug comes from `./launchpad.yaml` (`metadata.slug` /
108
+ `metadata.name`). On this path `--slug` does **not** override it —
109
+ it only overrides directory-name inference on the legacy clone
110
+ flow (no local manifest).
111
+ - **`--message`** is sent as a forward-compat header on the legacy
112
+ path only, and **the bot currently ignores it**. Don't rely on it
113
+ for change logs or audit trails.
114
+ - **`--file`** is valid only with `--dry-run` / `--apply`
115
+ (manifest-driven modes), not with a content deploy.
97
116
 
98
117
  Exit codes:
99
118
 
100
- - **0** — content PR opened + Cloudflare Pages deployment verified.
101
- - **non-zero** see `/launchpad-deploy-status <slug>` for the
102
- failure reason.
103
-
104
- ## Stack-fit what the bot enforces server-side
105
-
106
- The bot rejects bundles that contain shapes the Cloudflare Pages +
107
- Workers runtime cannot host. The validation runs on every
108
- `launchpad deploy`; you do not need to pre-flight it locally, but
109
- **knowing what's enforced saves the round-trip when a bundle is
110
- going to fail**.
111
-
112
- The accepted Launchpad stack is the single source of truth in:
113
-
114
- - `launchpad-platform/ARCHITECTURE.md § Tech Stack`
115
- - `launchpad-platform/PATTERNS.md § Approved Libraries`
116
- - `launchpad-platform/ANTI-PATTERNS.md`
117
-
118
- ### Forbidden runtime dependencies
119
-
120
- The bot rejects bundles whose `package.json` declares any of these
121
- runtime dependenciesthey do not work in the Workers runtime
122
- without a non-trivial port:
123
-
124
- | Forbidden | Replacement on Launchpad |
125
- |---|---|
126
- | `fastify` / `express` / `koa` / `hapi` / `@nestjs/*` | `hono` mounted at `functions/api/[[path]].ts` — the only server framework (PATTERNS.md). |
127
- | `better-sqlite3` / native `sqlite3` | Cloudflare D1 binding (`wrangler.toml [[d1_databases]]`). |
128
- | `pg` / `mysql2` / `mongodb` / `mongoose` / `redis` / `ioredis` | Neon (Postgres over HTTP) or another HTTP-driver backend. Native TCP drivers do not work in the Workers runtime. |
129
- | `dotenv` | `c.env.*` bindings; `wrangler secret put` (or `launchpad envvars` / `launchpad secrets push`) for secrets. |
130
- | `pm2` / `forever` / `nodemon` | Cloudflare Cron Triggers (`wrangler.toml [triggers] crons`). |
131
-
132
- ### Forbidden top-level files
133
-
134
- The bot rejects bundles whose root contains any of these (on non-
135
- container app-types):
136
-
137
- ```
138
- Dockerfile · docker-compose.yml · docker-compose.yaml · nginx.conf
139
- pm2.config.js · ecosystem.config.js · Procfile
140
- ```
141
-
142
- Their presence is the smoking gun that the app was copied from a
143
- long-running-server stack without removing the legacy infra.
144
-
145
- ### Forbidden source patterns
146
-
147
- The bot's secret-scan + build-command policy rejects bundles where
148
- the source tree contains:
149
-
150
- - `process.env.X` / `process.env['X']` reads — switch to `c.env.*`
151
- bindings.
152
- - `setInterval` / `setTimeout` daemons — Workers do not run between
153
- requests; use Cron Triggers for periodic work.
154
- - High-signal secret patterns (AWS access keys, GitHub PATs / OAuth
155
- / app tokens, Slack tokens, SSH/RSA/EC/PGP keys, generic
156
- `api_key` shapes). These never belong in a bundle — push them
157
- through `launchpad secrets push` instead.
158
-
159
- ### `react+api` requires `nodejs_compat`
160
-
161
- The bot checks that `wrangler.toml` declares
162
- `compatibility_flags = ["nodejs_compat"]` for any `react+api` app
163
- (ADR-0011 carve-out).
119
+ - **0** — bundle accepted and committed (or "nothing to deploy"
120
+ when `main` already matches the bundle). This is **not** proof the
121
+ app is live — verify with `launchpad status`.
122
+ - **non-zero** — gate rejections are rendered with per-file detail
123
+ on stderr; fix the bundle and re-run. For provisioning-phase
124
+ failures see `/launchpad-deploy-status <slug>`.
125
+
126
+ ## What the bot enforces on every bundle
127
+
128
+ The canonical gate list and caps live in `/launchpad-deploy
129
+ § Constants (single source of truth)`. In user-visible terms, every
130
+ upload passes:
131
+
132
+ - **Bundle policy** — file-count / bundle-size / per-file-size caps,
133
+ symlink and path-traversal/absolute-path rejection, and auth-file
134
+ rules (`.github/workflows`, CODEOWNERS, `.npmrc`/`.yarnrc`
135
+ carrying auth tokens, `.git/`).
136
+ - **Secret scan** — high-signal secret patterns (AWS access keys,
137
+ GitHub PATs / OAuth / app tokens, Slack tokens, SSH/RSA/EC/PGP
138
+ private keys, generic `api_key` shapes). These never belong in a
139
+ bundle push them through `launchpad secrets push` instead.
140
+ - **Build-command allowlist** `build.command` must match the
141
+ bot's allowlist.
142
+ - **App boundary (CLI-side, before upload)** — files outside
143
+ `app.root` / `app.include` are stripped with a warning, and
144
+ key/cert-shaped files (`*.pem`, `*.key`, `id_rsa*`, `*.p12`) are
145
+ denied client-side.
146
+
147
+ Gates are **delta-judged** (ADR 0025): they evaluate what your
148
+ deploy *changes* relative to `main`, not everything the workspace
149
+ contains. A pre-existing violation already live on `main` becomes a
150
+ non-blocking **standing exception** — recorded by the bot and
151
+ surfaced by both `launchpad deploy` and `launchpad status` (the bot
152
+ serves the inventory at `GET /apps/<slug>/exceptions`). So "my old
153
+ violation didn't block this deploy" is by design, not a missed gate.
154
+
155
+ There is **no** server-side scan of `package.json` dependencies,
156
+ top-level files, or source patterns. Express/Koa-style servers,
157
+ native DB drivers (`pg`, `better-sqlite3`, …), `dotenv`, and
158
+ pm2/Docker artefacts are not rejected at deploy time — they simply
159
+ won't run on the Cloudflare Pages + Workers runtime and fail at
160
+ build or runtime instead. Likewise `react+api`'s
161
+ `compatibility_flags = ["nodejs_compat"]` in `wrangler.toml`
162
+ (ADR-0011) is **required but not validated** — a missing flag fails
163
+ at runtime. Treat the stack-constraints table in `/launchpad-deploy`
164
+ as advisory architecture guidance, not an enforced gate.
164
165
 
165
166
  ### Verifying locally before you ship
166
167
 
@@ -172,34 +173,47 @@ launchpad validate
172
173
  launchpad plan
173
174
  ```
174
175
 
175
- Neither verb talks to the bot. Catch what you can locally; the bot
176
- catches the rest server-side.
176
+ Both are offline by default (no bot). `launchpad validate
177
+ --strict-groups` opts in to an online check that resolves
178
+ `access.allowed_entra_group` (needs a session).
177
179
 
178
- ## Verify after merge
180
+ ## Verify the deploy
179
181
 
180
182
  ```bash
181
183
  launchpad status <slug>
182
184
  ```
183
185
 
184
- Three possible states (see `/launchpad-status` for the canonical
185
- reference):
186
-
187
- - **`in sync`** local matches deployed; the deploy landed and
188
- `deployment_verified` is green. You're done.
189
- - **`drift: <fields>`** — your local manifest diverges from what was
186
+ `launchpad status` reports manifest drift **and** the live
187
+ Cloudflare Pages build truth — last deployment, what triggered it,
188
+ build outcome, and a failure-log excerpt when the build broke. (See
189
+ `/launchpad-status` for the full state list.) What you're looking
190
+ for after a deploy:
191
+
192
+ - **`live, in sync`** with `last deployment: build success` — your
193
+ commit landed and the build is serving. You're done.
194
+ - **`last deployment: build FAILED at stage "<stage>"`** (+ log
195
+ excerpt) — the commit landed but the build broke; the previous
196
+ successful deployment is still serving. Fix the cause and re-run
197
+ `launchpad deploy`.
198
+ - **`build IN PROGRESS`** — re-run `launchpad status` shortly to
199
+ confirm the outcome.
200
+ - **`drift: <fields>`** — your local manifest diverges from what's
190
201
  deployed. Either re-run `launchpad deploy` to ship the local, or
191
202
  `launchpad pull <slug> --out launchpad.yaml` to bring the local
192
203
  into line with deployed.
193
- - **`no deployed manifest yet`** the deploy is still in flight or
194
- failed. Re-check in a minute, or run `/launchpad-deploy-status
195
- <slug>`.
204
+ - **provisioning / failed / destroy states** route to
205
+ `/launchpad-deploy-status` or `/launchpad-destroy`.
206
+
207
+ Do not declare a change shipped until `status` shows the build
208
+ outcome for your commit.
196
209
 
197
210
  You can also point a browser at `https://<slug>.launchpad.m-kopa.us`
198
- once status reports `in sync`. An SSO gate sits in front of the app:
199
- for a gateway-fronted app (the default, `auth: gateway`) expect a
200
- redirect to the Entra-OIDC gateway (Microsoft sign-in) on the first
201
- request; for an `auth: access` app expect a redirect to
202
- `*.cloudflareaccess.com`. Either way you land on your app after sign-in.
211
+ once status reports the build green. An SSO gate sits in front of
212
+ the app: for a gateway-fronted app (the default, `auth: gateway`)
213
+ expect a redirect to the Entra-OIDC gateway (Microsoft sign-in) on
214
+ the first request; for an `auth: access` app expect a redirect to
215
+ `*.cloudflareaccess.com`. Either way you land on your app after
216
+ sign-in.
203
217
 
204
218
  If the URL serves the wrong thing:
205
219
 
@@ -218,16 +232,23 @@ If the URL serves the wrong thing:
218
232
  Once you've shipped first content, the daily-use verbs are:
219
233
 
220
234
  - **`launchpad status`** (`/launchpad-status`) — is my local
221
- `launchpad.yaml` in sync with what's deployed?
235
+ `launchpad.yaml` in sync with what's deployed, and did the last
236
+ build succeed?
222
237
  - **`launchpad pull <slug>`** (`/launchpad-status`) — read the
223
238
  currently-deployed `launchpad.yaml`.
224
- - **`launchpad deploy`** — package the working tree + open an
225
- update PR via the bot.
239
+ - **`launchpad deploy`** — bundle the working tree; the bot commits
240
+ it directly to the app repo's `main`.
226
241
  - **`launchpad envvars`** — list / set / remove non-secret
227
242
  production env vars.
228
- - **`launchpad secrets push`** — push secrets (never via git).
229
- - **`launchpad logs <slug>`** — recent Cloudflare Pages deployment
230
- history.
243
+ - **`launchpad secrets template`** — emit `.env.example` from the
244
+ manifest's secret bindings.
245
+ - **`launchpad secrets push`** — push secret values from `.env`
246
+ (never via git).
247
+ - **`launchpad secrets status`** — names-only PRESENT/MISSING audit
248
+ per binding.
249
+ - **`launchpad logs --slug <slug>`** — recent Cloudflare Pages
250
+ deployment history (default 10 entries, max 25 via `--lines`).
251
+ Note: the slug is flag-only, not positional.
231
252
  - **`launchpad rollback`** — revert manifest to a prior git SHA +
232
253
  re-apply.
233
254
 
@@ -237,17 +258,18 @@ Once you've shipped first content, the daily-use verbs are:
237
258
  skill. Every step is a `launchpad` verb. External users without
238
259
  M-KOPA GitHub access need this skill to work end-to-end on the
239
260
  CLI alone.
240
- - Do **not** open content PRs by hand against the app repo — the
241
- bot's bootstrap ruleset gates auto-merge, and a hand-rolled PR
242
- bypasses the bundle scan, secret-scan, and build-command policy.
243
- Use `launchpad deploy`.
244
- - Do **not** treat the stack-fit pre-flight as something the user
245
- re-implements locally. The bot enforces it server-side on every
246
- bundle; the playbook describes what's enforced, not how to
247
- re-implement it.
248
- - Do **not** force-merge if the bot's PR checks are red. The
249
- bundle-policy / secret-scan / build-command checks are detecting
250
- real things; surface the failure verbatim and let the user fix
261
+ - Do **not** push commits or open PRs by hand against the app repo.
262
+ The bot commits through a ruleset bypass it alone holds, and a
263
+ hand-rolled change bypasses the bundle policy, secret-scan,
264
+ build-command gates, and the standing-exception ledger. Use
265
+ `launchpad deploy`.
266
+ - Do **not** treat exit 0 from `launchpad deploy` as "live". The
267
+ commit landed, but the Pages build runs asynchronously and can
268
+ fail afterwards — always verify with `launchpad status`.
269
+ - Do **not** re-implement the server-side gates locally.
270
+ `launchpad validate` plus the CLI's own app-boundary pre-flight
271
+ cover the local half; the bot enforces the rest server-side and
272
+ renders violations verbatim — surface them and let the user fix
251
273
  the bundle.
252
274
  - Do **not** edit `launchpad.yaml`'s `production_env:` block to
253
275
  contain secret values. That block is non-secret by contract;