@m-kopa/launchpad-cli 0.36.0 → 0.37.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,24 @@ 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.37.0 — 2026-06-18
10
+
11
+ Feature (sp-grnted Phase 2, ADR 0028): **grant editors by email**, plus a
12
+ `list-editors` verb.
13
+
14
+ - `grant-editor` / `revoke-editor` now accept a **work email** as well as an id
15
+ (`launchpad grant-editor <slug> alice@m-kopa.com`). The email is resolved to
16
+ the recipient's Entra id via the bot's new `/users/resolve` endpoint — a
17
+ bounded, exact-match lookup within the platform's **existing** directory
18
+ consent (no new Graph permission). Members only: ambiguous emails are refused
19
+ (pass an id instead) and guest/external accounts are rejected.
20
+ - New `launchpad list-editors <slug>` — shows who has editor (BUILD) access.
21
+ - Both verbs now surface that **editor = BUILD rights (clone + deploy)** and that
22
+ browser VIEW access to the running app is a separate Entra-group grant from IT.
23
+
24
+ > Requires the portal-bot deploy that ships `/users/resolve`; until then the
25
+ > email path returns a clear resolver error. The id path is unaffected.
26
+
9
27
  ## 0.36.0 — 2026-06-18
10
28
 
11
29
  Feature (sp-rpt9kd, ADR 0029): **`launchpad bug` / `launchpad feature`** — file a
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.36.0";
22
+ var CLI_VERSION = "0.37.0";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -12422,8 +12422,9 @@ async function runEditor(action, args, io, deps) {
12422
12422
  const verb = action === "grant" ? "grant-editor" : "revoke-editor";
12423
12423
  const parsed = parseArgs14(args);
12424
12424
  if (parsed === null) {
12425
- io.err(`usage: launchpad ${verb} <slug> <id> [--yes] [--json]`);
12426
- io.err(" <id> is the recipient's value from `launchpad whoami --share`.");
12425
+ io.err(`usage: launchpad ${verb} <slug> <id|email> [--yes] [--json]`);
12426
+ io.err(" <id> is the recipient's value from `launchpad whoami --share`,");
12427
+ io.err(" or pass their @-email and it is resolved to their id.");
12427
12428
  return 64;
12428
12429
  }
12429
12430
  const { slug, target, yes, json } = parsed;
@@ -12433,15 +12434,21 @@ async function runEditor(action, args, io, deps) {
12433
12434
  }
12434
12435
  try {
12435
12436
  const cfg = deps.loadConfig();
12437
+ const resolved = await resolveTarget(verb, target, cfg, deps, io);
12438
+ if (resolved.kind === "error")
12439
+ return resolved.exit;
12440
+ const editorId = resolved.id;
12441
+ const label = resolved.label;
12436
12442
  const current = await deps.apiJson(cfg, {
12437
12443
  path: `/apps/${slug}`
12438
12444
  });
12439
- const alreadyEditor = current.record.editors.includes(target);
12445
+ const alreadyEditor = current.record.editors.includes(editorId);
12440
12446
  if (!yes) {
12441
12447
  const noop = action === "grant" && alreadyEditor || action === "revoke" && !alreadyEditor;
12448
+ const subject = resolved.resolvedFromEmail ? `${label} (id ${editorId})` : label;
12442
12449
  io.out(action === "grant" ? `About to grant editor access on "${slug}" to:
12443
- ${target}` : `About to revoke editor access on "${slug}" from:
12444
- ${target}`);
12450
+ ${subject}` : `About to revoke editor access on "${slug}" from:
12451
+ ${subject}`);
12445
12452
  if (noop) {
12446
12453
  io.out(action === "grant" ? " (already an editor — this will be a no-op)" : " (not currently an editor — this will be a no-op)");
12447
12454
  }
@@ -12451,7 +12458,7 @@ async function runEditor(action, args, io, deps) {
12451
12458
  return 0;
12452
12459
  }
12453
12460
  }
12454
- const result = await mutateWithRetry(action, slug, target, current.record.version, cfg, deps);
12461
+ const result = await mutateWithRetry(action, slug, editorId, current.record.version, cfg, deps);
12455
12462
  if (result.kind === "conflict") {
12456
12463
  io.err(`launchpad ${verb}: the app record for "${slug}" changed during the ${action} — re-run \`launchpad ${verb} ${slug} ${target}\`.`);
12457
12464
  return CONFLICT_EXIT;
@@ -12462,16 +12469,21 @@ async function runEditor(action, args, io, deps) {
12462
12469
  ok: true,
12463
12470
  slug,
12464
12471
  action,
12465
- editor: target,
12472
+ editor: editorId,
12473
+ input: target,
12466
12474
  changed,
12467
12475
  editors: result.record.editors
12468
12476
  }));
12469
12477
  return 0;
12470
12478
  }
12471
12479
  if (!changed) {
12472
- io.out(action === "grant" ? `${target} was already an editor on "${slug}" — no change.` : `${target} was not an editor on "${slug}" — no change.`);
12480
+ io.out(action === "grant" ? `${label} was already an editor on "${slug}" — no change.` : `${label} was not an editor on "${slug}" — no change.`);
12473
12481
  } else {
12474
- io.out(action === "grant" ? `✓ ${target} is now an editor on "${slug}".` : `✓ ${target} is no longer an editor on "${slug}".`);
12482
+ io.out(action === "grant" ? `✓ ${label} is now an editor on "${slug}".` : `✓ ${label} is no longer an editor on "${slug}".`);
12483
+ }
12484
+ if (action === "grant") {
12485
+ io.out(" Note: editor access = BUILD rights (clone + deploy). Opening the");
12486
+ io.out(" running app in a browser is a separate Entra-group grant from IT.");
12475
12487
  }
12476
12488
  return 0;
12477
12489
  } catch (e) {
@@ -12524,9 +12536,109 @@ async function sendMutation(action, slug, target, version, cfg, deps) {
12524
12536
  function describe32(e) {
12525
12537
  return e instanceof Error ? e.message : String(e);
12526
12538
  }
12539
+ async function resolveTarget(verb, target, cfg, deps, io) {
12540
+ if (!target.includes("@")) {
12541
+ return { kind: "id", id: target, label: target, resolvedFromEmail: false };
12542
+ }
12543
+ const body = await deps.apiJson(cfg, {
12544
+ path: `/users/resolve?email=${encodeURIComponent(target)}`,
12545
+ nonThrowingStatuses: [400, 403, 404, 409, 502, 503]
12546
+ });
12547
+ if (typeof body.oid === "string" && body.oid.length > 0) {
12548
+ return { kind: "id", id: body.oid, label: target, resolvedFromEmail: true };
12549
+ }
12550
+ io.err(resolveErrorMessage(verb, target, body));
12551
+ return { kind: "error", exit: 1 };
12552
+ }
12553
+ function resolveErrorMessage(verb, email, body) {
12554
+ const fallback = `pass their id from \`launchpad whoami --share\` instead.`;
12555
+ switch (body.error) {
12556
+ case "user_not_found":
12557
+ return `launchpad ${verb}: no Entra user matches "${email}" — check the address, or ${fallback}`;
12558
+ case "ambiguous_email":
12559
+ return `launchpad ${verb}: "${email}" matches more than one user — ${fallback}`;
12560
+ case "guest_unsupported":
12561
+ return `launchpad ${verb}: "${email}" is a guest/external user — grant-by-email is members-only; ${fallback}`;
12562
+ case "bad_request":
12563
+ return `launchpad ${verb}: "${email}" is not a valid email address.`;
12564
+ default:
12565
+ return `launchpad ${verb}: could not resolve "${email}"${body.message ? `: ${body.message}` : ""} — ${fallback}`;
12566
+ }
12567
+ }
12527
12568
  var grantEditorCommand = makeEditorCommand("grant");
12528
12569
  var revokeEditorCommand = makeEditorCommand("revoke");
12529
12570
 
12571
+ // src/commands/list-editors.ts
12572
+ function realDeps2() {
12573
+ return { loadConfig, apiJson };
12574
+ }
12575
+ function parseArgs15(args) {
12576
+ let slug;
12577
+ let json = false;
12578
+ for (const a of args) {
12579
+ if (a === "--json")
12580
+ json = true;
12581
+ else if (slug === undefined)
12582
+ slug = a;
12583
+ }
12584
+ if (slug === undefined)
12585
+ return null;
12586
+ return { slug, json };
12587
+ }
12588
+ async function runListEditors(args, io, deps) {
12589
+ const parsed = parseArgs15(args);
12590
+ if (parsed === null) {
12591
+ io.err("usage: launchpad list-editors <slug> [--json]");
12592
+ return 64;
12593
+ }
12594
+ const { slug, json } = parsed;
12595
+ try {
12596
+ const cfg = deps.loadConfig();
12597
+ const current = await deps.apiJson(cfg, {
12598
+ path: `/apps/${slug}`
12599
+ });
12600
+ const editors = current.record.editors;
12601
+ if (json) {
12602
+ io.out(JSON.stringify({ slug, editors }));
12603
+ return 0;
12604
+ }
12605
+ if (editors.length === 0) {
12606
+ io.out(`No editors on "${slug}" — only the owner has BUILD access.`);
12607
+ } else {
12608
+ io.out(`Editors on "${slug}" (${editors.length}):`);
12609
+ for (const e of editors)
12610
+ io.out(` ${e}`);
12611
+ }
12612
+ io.out("");
12613
+ io.out("Editor access = BUILD rights (clone + deploy). Browser VIEW access to");
12614
+ io.out("the running app is a separate Entra-group grant from IT.");
12615
+ return 0;
12616
+ } catch (e) {
12617
+ if (e instanceof UnauthenticatedError) {
12618
+ io.err(e.message);
12619
+ return 1;
12620
+ }
12621
+ if (e instanceof ForbiddenError) {
12622
+ io.err(`launchpad list-editors: not authorised to view editors on "${slug}".`);
12623
+ return 1;
12624
+ }
12625
+ if (e instanceof NotFoundError) {
12626
+ io.err(`launchpad list-editors: no app "${slug}" — check the slug with \`launchpad apps\`.`);
12627
+ return 1;
12628
+ }
12629
+ io.err(`launchpad list-editors failed: ${e instanceof Error ? e.message : String(e)}`);
12630
+ return 1;
12631
+ }
12632
+ }
12633
+ function makeListEditorsCommand(deps = realDeps2()) {
12634
+ return {
12635
+ name: "list-editors",
12636
+ summary: "list the developers with editor (BUILD) access to an app",
12637
+ run: (args, io) => runListEditors(args, io, deps)
12638
+ };
12639
+ }
12640
+ var listEditorsCommand = makeListEditorsCommand();
12641
+
12530
12642
  // src/update-notifier.ts
12531
12643
  import { spawn as spawn6 } from "node:child_process";
12532
12644
  import { homedir as homedir4 } from "node:os";
@@ -12959,7 +13071,7 @@ function readBreadcrumb() {
12959
13071
  }
12960
13072
 
12961
13073
  // src/commands/report.ts
12962
- function realDeps2() {
13074
+ function realDeps3() {
12963
13075
  return {
12964
13076
  loadConfig,
12965
13077
  apiJson,
@@ -12978,7 +13090,7 @@ function realDeps2() {
12978
13090
  readBreadcrumb
12979
13091
  };
12980
13092
  }
12981
- function parseArgs15(args) {
13093
+ function parseArgs16(args) {
12982
13094
  let title;
12983
13095
  let message;
12984
13096
  let positional;
@@ -13008,7 +13120,7 @@ function parseArgs15(args) {
13008
13120
  var NOUN = { bug: "bug report", feature: "feature request" };
13009
13121
  async function runReport(kind, args, io, deps) {
13010
13122
  const verb = kind;
13011
- const parsed = parseArgs15(args);
13123
+ const parsed = parseArgs16(args);
13012
13124
  let message = parsed.message ?? parsed.positional ?? "";
13013
13125
  let title = parsed.title ?? "";
13014
13126
  if (message === "" && deps.isTty()) {
@@ -13090,7 +13202,7 @@ function buildContext(deps) {
13090
13202
  ctx.appSlug = slug;
13091
13203
  return ctx;
13092
13204
  }
13093
- function makeReportCommand(kind, deps = realDeps2()) {
13205
+ function makeReportCommand(kind, deps = realDeps3()) {
13094
13206
  return {
13095
13207
  name: kind,
13096
13208
  summary: kind === "bug" ? "file a bug report (lands in the team's Linear inbox)" : "file a feature request (lands in the team's Linear inbox)",
@@ -13128,6 +13240,7 @@ var COMMANDS = [
13128
13240
  secretsCommand,
13129
13241
  grantEditorCommand,
13130
13242
  revokeEditorCommand,
13243
+ listEditorsCommand,
13131
13244
  makeReportCommand("bug"),
13132
13245
  makeReportCommand("feature"),
13133
13246
  refreshUpdateCacheCommand,
@@ -1 +1 @@
1
- {"version":3,"file":"editors.d.ts","sourceRoot":"","sources":["../../src/commands/editors.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAEjE,iFAAiF;AACjF,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACxD;AA8DD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,GAAE,UAAuB,GAAG,OAAO,CAU9F;AAgLD,eAAO,MAAM,kBAAkB,EAAE,OAAoC,CAAC;AACtE,eAAO,MAAM,mBAAmB,EAAE,OAAqC,CAAC"}
1
+ {"version":3,"file":"editors.d.ts","sourceRoot":"","sources":["../../src/commands/editors.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAEjE,iFAAiF;AACjF,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7E,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACxD;AA8DD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,GAAE,UAAuB,GAAG,OAAO,CAU9F;AAuQD,eAAO,MAAM,kBAAkB,EAAE,OAAoC,CAAC;AACtE,eAAO,MAAM,mBAAmB,EAAE,OAAqC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { CliConfig } from "../config.js";
2
+ import type { ApiRequestOptions } from "../http/api-client.js";
3
+ import type { Command } from "../dispatcher.js";
4
+ export interface ListEditorsDeps {
5
+ readonly loadConfig: () => CliConfig;
6
+ readonly apiJson: <T>(cfg: CliConfig, opts: ApiRequestOptions) => Promise<T>;
7
+ }
8
+ export declare function makeListEditorsCommand(deps?: ListEditorsDeps): Command;
9
+ export declare const listEditorsCommand: Command;
10
+ //# sourceMappingURL=list-editors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-editors.d.ts","sourceRoot":"","sources":["../../src/commands/list-editors.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAS,OAAO,EAAY,MAAM,kBAAkB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC9E;AAyFD,wBAAgB,sBAAsB,CACpC,IAAI,GAAE,eAA4B,GACjC,OAAO,CAMT;AAED,eAAO,MAAM,kBAAkB,EAAE,OAAkC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AAuDA,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,EAqCtC,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,CA0BnB;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":"AAwDA,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,EAsCtC,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,CA0BnB;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.36.0";
1
+ export declare const CLI_VERSION = "0.37.0";
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.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Launchpad CLI \u2014 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": {
@@ -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.36.0
4
+ version: 0.37.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.36.0
4
+ version: 0.37.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-status
3
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.36.0
4
+ version: 0.37.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-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.36.0
4
+ version: 0.37.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.36.0
4
+ version: 0.37.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.36.0
4
+ version: 0.37.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-report
3
3
  description: File a bug report or feature request to the Launchpad team's tracker from the CLI. Use when someone reports something broken, hits an error in a launchpad command, or wishes a feature existed — e.g. "this is broken", "report a bug", "can you file that", "I wish launchpad could…", "/launchpad-bug", "/launchpad-feature". Always confirm and show exactly what you'll send before filing; never file silently.
4
- version: 0.36.0
4
+ version: 0.37.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.36.0
4
+ version: 0.37.0
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->