@m-kopa/launchpad-cli 0.37.0 → 0.38.1

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.38.1 — 2026-06-19
10
+
11
+ Fix (sp-rpt9kd): `launchpad bug` / `launchpad feature` no longer truncate an
12
+ unquoted message to its first word. `launchpad bug this is a test bug report`
13
+ filed an issue titled just `this`, because the argument parser kept only the
14
+ first bare-word token. It now joins every positional token, so the whole
15
+ sentence is captured as the title/message. Quoted input (`launchpad bug "…"`)
16
+ was unaffected and still works. No interface change.
17
+
18
+ ## 0.38.0 — 2026-06-19
19
+
20
+ Feature (sp-cown4k, ADR 0030): **multi-owner co-ownership** — share full
21
+ ownership of an app.
22
+
23
+ - `add-owner <slug> <id|email>` / `remove-owner <slug> <id|email>` — any current
24
+ owner can add or remove **co-owners**. A co-owner has full, equal owner rights:
25
+ manage editors, manage co-owners, transfer, redeploy, and **destroy**. Add by
26
+ work email (resolved to an id, members-only) or by id. Mirrors `grant-editor`:
27
+ confirm-preview, hidden `If-Match` retry, fail-soft.
28
+ - `list-owners <slug>` — show the creator (the un-removable anchor) and the
29
+ co-owners.
30
+ - The original creator stays as the app's anchor and can't be evicted by a
31
+ co-owner (only displaced via transfer), so an app can never be orphaned.
32
+
33
+ > Requires the portal-bot deploy that ships `POST/DELETE /apps/<slug>/co-owners`;
34
+ > until then the verbs return a clear endpoint error.
35
+
9
36
  ## 0.37.0 — 2026-06-18
10
37
 
11
38
  Feature (sp-grnted Phase 2, ADR 0028): **grant editors by email**, plus 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.37.0";
22
+ var CLI_VERSION = "0.38.1";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
@@ -12372,6 +12372,39 @@ function describe31(e) {
12372
12372
 
12373
12373
  // src/commands/editors.ts
12374
12374
  import { createInterface as createInterface5 } from "node:readline/promises";
12375
+
12376
+ // src/commands/resolve-target.ts
12377
+ async function resolveTarget(verb, target, cfg, deps, io) {
12378
+ if (!target.includes("@")) {
12379
+ return { kind: "id", id: target, label: target, resolvedFromEmail: false };
12380
+ }
12381
+ const body = await deps.apiJson(cfg, {
12382
+ path: `/users/resolve?email=${encodeURIComponent(target)}`,
12383
+ nonThrowingStatuses: [400, 403, 404, 409, 502, 503]
12384
+ });
12385
+ if (typeof body.oid === "string" && body.oid.length > 0) {
12386
+ return { kind: "id", id: body.oid, label: target, resolvedFromEmail: true };
12387
+ }
12388
+ io.err(resolveErrorMessage(verb, target, body));
12389
+ return { kind: "error", exit: 1 };
12390
+ }
12391
+ function resolveErrorMessage(verb, email, body) {
12392
+ const fallback = `pass their id from \`launchpad whoami --share\` instead.`;
12393
+ switch (body.error) {
12394
+ case "user_not_found":
12395
+ return `launchpad ${verb}: no Entra user matches "${email}" — check the address, or ${fallback}`;
12396
+ case "ambiguous_email":
12397
+ return `launchpad ${verb}: "${email}" matches more than one user — ${fallback}`;
12398
+ case "guest_unsupported":
12399
+ return `launchpad ${verb}: "${email}" is a guest/external user — grant-by-email is members-only; ${fallback}`;
12400
+ case "bad_request":
12401
+ return `launchpad ${verb}: "${email}" is not a valid email address.`;
12402
+ default:
12403
+ return `launchpad ${verb}: could not resolve "${email}"${body.message ? `: ${body.message}` : ""} — ${fallback}`;
12404
+ }
12405
+ }
12406
+
12407
+ // src/commands/editors.ts
12375
12408
  var CONFLICT_EXIT = 75;
12376
12409
  function realDeps() {
12377
12410
  return {
@@ -12536,35 +12569,6 @@ async function sendMutation(action, slug, target, version, cfg, deps) {
12536
12569
  function describe32(e) {
12537
12570
  return e instanceof Error ? e.message : String(e);
12538
12571
  }
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
- }
12568
12572
  var grantEditorCommand = makeEditorCommand("grant");
12569
12573
  var revokeEditorCommand = makeEditorCommand("revoke");
12570
12574
 
@@ -12639,6 +12643,247 @@ function makeListEditorsCommand(deps = realDeps2()) {
12639
12643
  }
12640
12644
  var listEditorsCommand = makeListEditorsCommand();
12641
12645
 
12646
+ // src/commands/owners.ts
12647
+ import { createInterface as createInterface6 } from "node:readline/promises";
12648
+ var CONFLICT_EXIT2 = 75;
12649
+ function realDeps3() {
12650
+ return {
12651
+ loadConfig,
12652
+ apiJson,
12653
+ isTty: () => process.stdout.isTTY === true,
12654
+ prompt: async (question) => {
12655
+ const rl = createInterface6({ input: process.stdin, output: process.stdout });
12656
+ try {
12657
+ return (await rl.question(question)).trim();
12658
+ } finally {
12659
+ rl.close();
12660
+ }
12661
+ }
12662
+ };
12663
+ }
12664
+ function parseArgs16(args) {
12665
+ let slug;
12666
+ let target;
12667
+ let yes = false;
12668
+ let json = false;
12669
+ for (const a of args) {
12670
+ if (a === "--yes" || a === "-y")
12671
+ yes = true;
12672
+ else if (a === "--json")
12673
+ json = true;
12674
+ else if (slug === undefined)
12675
+ slug = a;
12676
+ else if (target === undefined)
12677
+ target = a;
12678
+ }
12679
+ if (slug === undefined || target === undefined)
12680
+ return null;
12681
+ return { slug, target, yes, json };
12682
+ }
12683
+ function isVersionMismatch2(b) {
12684
+ return typeof b === "object" && b !== null && b.error === "version_mismatch" && typeof b.currentVersion === "number";
12685
+ }
12686
+ function makeOwnerCommand(action, deps = realDeps3()) {
12687
+ const name = action === "add" ? "add-owner" : "remove-owner";
12688
+ return {
12689
+ name,
12690
+ summary: action === "add" ? "add a co-owner with full owner rights to an app (by id or email)" : "remove a co-owner from an app",
12691
+ run: (args, io) => runOwner(action, args, io, deps)
12692
+ };
12693
+ }
12694
+ async function runOwner(action, args, io, deps) {
12695
+ const verb = action === "add" ? "add-owner" : "remove-owner";
12696
+ const parsed = parseArgs16(args);
12697
+ if (parsed === null) {
12698
+ io.err(`usage: launchpad ${verb} <slug> <id|email> [--yes] [--json]`);
12699
+ io.err(" <id> is the recipient's value from `launchpad whoami --share`,");
12700
+ io.err(" or pass their @-email and it is resolved to their id.");
12701
+ return 64;
12702
+ }
12703
+ const { slug, target, yes, json } = parsed;
12704
+ if (!yes && !deps.isTty()) {
12705
+ io.err(`launchpad ${verb}: refusing to mutate "${slug}" without confirmation — pass --yes in non-interactive mode.`);
12706
+ return 64;
12707
+ }
12708
+ try {
12709
+ const cfg = deps.loadConfig();
12710
+ const resolved = await resolveTarget(verb, target, cfg, deps, io);
12711
+ if (resolved.kind === "error")
12712
+ return resolved.exit;
12713
+ const ownerId = resolved.id;
12714
+ const label = resolved.label;
12715
+ const current = await deps.apiJson(cfg, {
12716
+ path: `/apps/${slug}`
12717
+ });
12718
+ const alreadyOwner = current.record.owner === ownerId || current.record.coOwners.includes(ownerId);
12719
+ if (!yes) {
12720
+ const noop = action === "add" && alreadyOwner || action === "remove" && !current.record.coOwners.includes(ownerId);
12721
+ const subject = resolved.resolvedFromEmail ? `${label} (id ${ownerId})` : label;
12722
+ io.out(action === "add" ? `About to add a co-owner on "${slug}":
12723
+ ${subject}` : `About to remove a co-owner on "${slug}":
12724
+ ${subject}`);
12725
+ if (action === "add") {
12726
+ io.out(" A co-owner gets FULL owner rights (manage editors, owners,");
12727
+ io.out(" transfer, redeploy, AND destroy).");
12728
+ }
12729
+ if (noop) {
12730
+ io.out(action === "add" ? current.record.owner === ownerId ? " (already the owner — this will be a no-op)" : " (already a co-owner — this will be a no-op)" : " (not currently a co-owner — this will be a no-op)");
12731
+ }
12732
+ const answer = (await deps.prompt("Proceed? [y/N] ")).toLowerCase();
12733
+ if (answer !== "y" && answer !== "yes") {
12734
+ io.out("Aborted — no change made.");
12735
+ return 0;
12736
+ }
12737
+ }
12738
+ const result = await mutateWithRetry2(action, slug, ownerId, current.record.version, cfg, deps);
12739
+ if (result.kind === "conflict") {
12740
+ io.err(`launchpad ${verb}: the app record for "${slug}" changed during the ${action} — re-run \`launchpad ${verb} ${slug} ${target}\`.`);
12741
+ return CONFLICT_EXIT2;
12742
+ }
12743
+ const changed = result.record.version !== result.attemptedVersion;
12744
+ if (json) {
12745
+ io.out(JSON.stringify({
12746
+ ok: true,
12747
+ slug,
12748
+ action,
12749
+ coOwner: ownerId,
12750
+ input: target,
12751
+ changed,
12752
+ coOwners: result.record.coOwners
12753
+ }));
12754
+ return 0;
12755
+ }
12756
+ if (!changed) {
12757
+ io.out(action === "add" ? `${label} was already an owner of "${slug}" — no change.` : `${label} was not a co-owner of "${slug}" — no change.`);
12758
+ } else {
12759
+ io.out(action === "add" ? `✓ ${label} is now a co-owner of "${slug}" (full owner rights).` : `✓ ${label} is no longer a co-owner of "${slug}".`);
12760
+ }
12761
+ return 0;
12762
+ } catch (e) {
12763
+ if (e instanceof UnauthenticatedError) {
12764
+ io.err(e.message);
12765
+ return 1;
12766
+ }
12767
+ if (e instanceof ForbiddenError) {
12768
+ io.err(`launchpad ${verb}: not authorised — you must be an owner of "${slug}" to manage its owners.`);
12769
+ return 1;
12770
+ }
12771
+ if (e instanceof NotFoundError) {
12772
+ io.err(`launchpad ${verb}: no app "${slug}" — check the slug with \`launchpad apps\`.`);
12773
+ return 1;
12774
+ }
12775
+ io.err(`launchpad ${verb} failed: ${describe33(e)}`);
12776
+ return 1;
12777
+ }
12778
+ }
12779
+ async function mutateWithRetry2(action, slug, target, startVersion, cfg, deps) {
12780
+ let version = startVersion;
12781
+ for (let attempt = 0;attempt < 2; attempt++) {
12782
+ const body = await sendMutation2(action, slug, target, version, cfg, deps);
12783
+ if (isVersionMismatch2(body)) {
12784
+ version = body.currentVersion;
12785
+ continue;
12786
+ }
12787
+ return { kind: "ok", record: body.record, attemptedVersion: version };
12788
+ }
12789
+ return { kind: "conflict" };
12790
+ }
12791
+ async function sendMutation2(action, slug, target, version, cfg, deps) {
12792
+ const headers = { "if-match": String(version) };
12793
+ if (action === "add") {
12794
+ return deps.apiJson(cfg, {
12795
+ method: "POST",
12796
+ path: `/apps/${slug}/co-owners`,
12797
+ jsonBody: { coOwner: target },
12798
+ headers,
12799
+ nonThrowingStatuses: [409]
12800
+ });
12801
+ }
12802
+ return deps.apiJson(cfg, {
12803
+ method: "DELETE",
12804
+ path: `/apps/${slug}/co-owners/${encodeURIComponent(target)}`,
12805
+ headers,
12806
+ nonThrowingStatuses: [409]
12807
+ });
12808
+ }
12809
+ function describe33(e) {
12810
+ return e instanceof Error ? e.message : String(e);
12811
+ }
12812
+ var addOwnerCommand = makeOwnerCommand("add");
12813
+ var removeOwnerCommand = makeOwnerCommand("remove");
12814
+
12815
+ // src/commands/list-owners.ts
12816
+ function realDeps4() {
12817
+ return { loadConfig, apiJson };
12818
+ }
12819
+ function parseArgs17(args) {
12820
+ let slug;
12821
+ let json = false;
12822
+ for (const a of args) {
12823
+ if (a === "--json")
12824
+ json = true;
12825
+ else if (slug === undefined)
12826
+ slug = a;
12827
+ }
12828
+ if (slug === undefined)
12829
+ return null;
12830
+ return { slug, json };
12831
+ }
12832
+ async function runListOwners(args, io, deps) {
12833
+ const parsed = parseArgs17(args);
12834
+ if (parsed === null) {
12835
+ io.err("usage: launchpad list-owners <slug> [--json]");
12836
+ return 64;
12837
+ }
12838
+ const { slug, json } = parsed;
12839
+ try {
12840
+ const cfg = deps.loadConfig();
12841
+ const current = await deps.apiJson(cfg, {
12842
+ path: `/apps/${slug}`
12843
+ });
12844
+ const { owner, coOwners } = current.record;
12845
+ if (json) {
12846
+ io.out(JSON.stringify({ slug, owner, coOwners }));
12847
+ return 0;
12848
+ }
12849
+ io.out(`Owners of "${slug}":`);
12850
+ io.out(` ${owner} (creator)`);
12851
+ if (coOwners.length === 0) {
12852
+ io.out(" (no co-owners — only the creator has owner rights)");
12853
+ } else {
12854
+ for (const c of coOwners)
12855
+ io.out(` ${c} (co-owner)`);
12856
+ }
12857
+ io.out("");
12858
+ io.out("Every owner has full rights (manage editors + owners, transfer, redeploy,");
12859
+ io.out("destroy). The creator is the un-removable anchor (displace via transfer).");
12860
+ return 0;
12861
+ } catch (e) {
12862
+ if (e instanceof UnauthenticatedError) {
12863
+ io.err(e.message);
12864
+ return 1;
12865
+ }
12866
+ if (e instanceof ForbiddenError) {
12867
+ io.err(`launchpad list-owners: not authorised to view owners on "${slug}".`);
12868
+ return 1;
12869
+ }
12870
+ if (e instanceof NotFoundError) {
12871
+ io.err(`launchpad list-owners: no app "${slug}" — check the slug with \`launchpad apps\`.`);
12872
+ return 1;
12873
+ }
12874
+ io.err(`launchpad list-owners failed: ${e instanceof Error ? e.message : String(e)}`);
12875
+ return 1;
12876
+ }
12877
+ }
12878
+ function makeListOwnersCommand(deps = realDeps4()) {
12879
+ return {
12880
+ name: "list-owners",
12881
+ summary: "list the owners (creator + co-owners) of an app",
12882
+ run: (args, io) => runListOwners(args, io, deps)
12883
+ };
12884
+ }
12885
+ var listOwnersCommand = makeListOwnersCommand();
12886
+
12642
12887
  // src/update-notifier.ts
12643
12888
  import { spawn as spawn6 } from "node:child_process";
12644
12889
  import { homedir as homedir4 } from "node:os";
@@ -13038,7 +13283,7 @@ var emitTelemetryCommand = {
13038
13283
  };
13039
13284
 
13040
13285
  // src/commands/report.ts
13041
- import { createInterface as createInterface6 } from "node:readline/promises";
13286
+ import { createInterface as createInterface7 } from "node:readline/promises";
13042
13287
  import { platform } from "node:os";
13043
13288
 
13044
13289
  // src/report/breadcrumb.ts
@@ -13071,13 +13316,13 @@ function readBreadcrumb() {
13071
13316
  }
13072
13317
 
13073
13318
  // src/commands/report.ts
13074
- function realDeps3() {
13319
+ function realDeps5() {
13075
13320
  return {
13076
13321
  loadConfig,
13077
13322
  apiJson,
13078
13323
  isTty: () => process.stdout.isTTY === true && process.stdin.isTTY === true,
13079
13324
  prompt: async (question) => {
13080
- const rl = createInterface6({ input: process.stdin, output: process.stdout });
13325
+ const rl = createInterface7({ input: process.stdin, output: process.stdout });
13081
13326
  try {
13082
13327
  return (await rl.question(question)).trim();
13083
13328
  } finally {
@@ -13090,10 +13335,10 @@ function realDeps3() {
13090
13335
  readBreadcrumb
13091
13336
  };
13092
13337
  }
13093
- function parseArgs16(args) {
13338
+ function parseArgs18(args) {
13094
13339
  let title;
13095
13340
  let message;
13096
- let positional;
13341
+ const positionalParts = [];
13097
13342
  let json = false;
13098
13343
  for (let i = 0;i < args.length; i++) {
13099
13344
  const a = args[i];
@@ -13111,16 +13356,17 @@ function parseArgs16(args) {
13111
13356
  message = v;
13112
13357
  i++;
13113
13358
  }
13114
- } else if (a !== undefined && !a.startsWith("-") && positional === undefined) {
13115
- positional = a;
13359
+ } else if (a !== undefined && !a.startsWith("-")) {
13360
+ positionalParts.push(a);
13116
13361
  }
13117
13362
  }
13363
+ const positional = positionalParts.length > 0 ? positionalParts.join(" ") : undefined;
13118
13364
  return { title, message, positional, json };
13119
13365
  }
13120
13366
  var NOUN = { bug: "bug report", feature: "feature request" };
13121
13367
  async function runReport(kind, args, io, deps) {
13122
13368
  const verb = kind;
13123
- const parsed = parseArgs16(args);
13369
+ const parsed = parseArgs18(args);
13124
13370
  let message = parsed.message ?? parsed.positional ?? "";
13125
13371
  let title = parsed.title ?? "";
13126
13372
  if (message === "" && deps.isTty()) {
@@ -13202,7 +13448,7 @@ function buildContext(deps) {
13202
13448
  ctx.appSlug = slug;
13203
13449
  return ctx;
13204
13450
  }
13205
- function makeReportCommand(kind, deps = realDeps3()) {
13451
+ function makeReportCommand(kind, deps = realDeps5()) {
13206
13452
  return {
13207
13453
  name: kind,
13208
13454
  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)",
@@ -13241,6 +13487,9 @@ var COMMANDS = [
13241
13487
  grantEditorCommand,
13242
13488
  revokeEditorCommand,
13243
13489
  listEditorsCommand,
13490
+ addOwnerCommand,
13491
+ removeOwnerCommand,
13492
+ listOwnersCommand,
13244
13493
  makeReportCommand("bug"),
13245
13494
  makeReportCommand("feature"),
13246
13495
  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;AAuQD,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":"AAwBA,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;AAwMD,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 ListOwnersDeps {
5
+ readonly loadConfig: () => CliConfig;
6
+ readonly apiJson: <T>(cfg: CliConfig, opts: ApiRequestOptions) => Promise<T>;
7
+ }
8
+ export declare function makeListOwnersCommand(deps?: ListOwnersDeps): Command;
9
+ export declare const listOwnersCommand: Command;
10
+ //# sourceMappingURL=list-owners.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-owners.d.ts","sourceRoot":"","sources":["../../src/commands/list-owners.ts"],"names":[],"mappings":"AAcA,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,cAAc;IAC7B,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;AA4FD,wBAAgB,qBAAqB,CACnC,IAAI,GAAE,cAA2B,GAChC,OAAO,CAMT;AAED,eAAO,MAAM,iBAAiB,EAAE,OAAiC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { CliConfig } from "../config.js";
2
+ import type { ApiRequestOptions } from "../http/api-client.js";
3
+ import type { Command } from "../dispatcher.js";
4
+ /** EX_TEMPFAIL — a concurrent edit won the race; the action is safe to retry. */
5
+ export declare const CONFLICT_EXIT = 75;
6
+ export type OwnerAction = "add" | "remove";
7
+ export interface OwnerDeps {
8
+ readonly loadConfig: () => CliConfig;
9
+ readonly apiJson: <T>(cfg: CliConfig, opts: ApiRequestOptions) => Promise<T>;
10
+ readonly isTty: () => boolean;
11
+ readonly prompt: (question: string) => Promise<string>;
12
+ }
13
+ export declare function makeOwnerCommand(action: OwnerAction, deps?: OwnerDeps): Command;
14
+ export declare const addOwnerCommand: Command;
15
+ export declare const removeOwnerCommand: Command;
16
+ //# sourceMappingURL=owners.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owners.d.ts","sourceRoot":"","sources":["../../src/commands/owners.ts"],"names":[],"mappings":"AAwBA,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,WAAW,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,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;AA+DD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,GAAE,SAAsB,GAAG,OAAO,CAU3F;AAoMD,eAAO,MAAM,eAAe,EAAE,OAAiC,CAAC;AAChE,eAAO,MAAM,kBAAkB,EAAE,OAAoC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/commands/report.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,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,MAAM,UAAU,GAAG,KAAK,GAAG,SAAS,CAAC;AAE3C,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;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,OAAO,cAAc,CAAC;CAChD;AA0KD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,GAAE,UAAuB,GAAG,OAAO,CAS1F"}
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/commands/report.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,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,MAAM,UAAU,GAAG,KAAK,GAAG,SAAS,CAAC;AAE3C,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;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,OAAO,cAAc,CAAC;CAChD;AA+KD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,GAAE,UAAuB,GAAG,OAAO,CAS1F"}
@@ -0,0 +1,26 @@
1
+ import type { CliConfig } from "../config.js";
2
+ import type { ApiRequestOptions } from "../http/api-client.js";
3
+ import type { CliIo, ExitCode } from "../dispatcher.js";
4
+ /** Minimal dependency surface the resolver needs (a subset of the
5
+ * per-command Deps objects, which all carry `apiJson`). */
6
+ export interface ResolveDeps {
7
+ readonly apiJson: <T>(cfg: CliConfig, opts: ApiRequestOptions) => Promise<T>;
8
+ }
9
+ export type ResolveOutcome = {
10
+ kind: "id";
11
+ id: string;
12
+ label: string;
13
+ resolvedFromEmail: boolean;
14
+ } | {
15
+ kind: "error";
16
+ exit: ExitCode;
17
+ };
18
+ export interface ResolveResponse {
19
+ readonly oid?: string;
20
+ readonly userPrincipalName?: string;
21
+ readonly error?: string;
22
+ readonly message?: string;
23
+ }
24
+ export declare function resolveTarget(verb: string, target: string, cfg: CliConfig, deps: ResolveDeps, io: Pick<CliIo, "err">): Promise<ResolveOutcome>;
25
+ export declare function resolveErrorMessage(verb: string, email: string, body: ResolveResponse): string;
26
+ //# sourceMappingURL=resolve-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-target.d.ts","sourceRoot":"","sources":["../../src/commands/resolve-target.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAExD;4DAC4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC9E;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEtC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,WAAW,EACjB,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,GACrB,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,eAAe,GACpB,MAAM,CAcR"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AA0DA,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,EAyCtC,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.37.0";
1
+ export declare const CLI_VERSION = "0.38.1";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@m-kopa/launchpad-cli",
3
- "version": "0.37.0",
4
- "description": "Launchpad CLI \u2014 clone / deploy / review / merge against Launchpad-managed apps. Talks to the portal-bot endpoints (SCOPE-M-760 / T4).",
3
+ "version": "0.38.1",
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": {
7
7
  "launchpad": "./dist/cli.js"
@@ -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.37.0
4
+ version: 0.38.1
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.37.0
4
+ version: 0.38.1
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.37.0
4
+ version: 0.38.1
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.37.0
4
+ version: 0.38.1
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.37.0
4
+ version: 0.38.1
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.37.0
4
+ version: 0.38.1
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.37.0
4
+ version: 0.38.1
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.37.0
4
+ version: 0.38.1
5
5
  ---
6
6
 
7
7
  <!-- BEGIN shell-contract (managed by scripts/sync-skill-contract.sh — edit skills/_partials/shell-contract.md) -->