@dashai/cli 0.26.0 → 0.27.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/dist/bin.js CHANGED
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, read
4
4
  import { hostname, tmpdir, platform, homedir } from 'os';
5
5
  import { resolve, join, relative, dirname, basename, win32, sep } from 'path';
6
6
  import { execFileSync, spawn, spawnSync } from 'child_process';
7
- import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm, multiselect } from '@clack/prompts';
7
+ import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm as confirm$1, multiselect } from '@clack/prompts';
8
8
  import { Command, Option } from 'commander';
9
9
  import open from 'open';
10
10
  import AdmZip from 'adm-zip';
@@ -1187,16 +1187,16 @@ async function devCommand(options) {
1187
1187
  }
1188
1188
  const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
1189
1189
  try {
1190
- const session = await registerDevSession(apiUrl, apiToken, {
1190
+ const session2 = await registerDevSession(apiUrl, apiToken, {
1191
1191
  target_workspace_id: workspaceContext.id,
1192
1192
  callback_origins: callbackOrigins
1193
1193
  });
1194
- revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
1195
- devHandoffEnv = devSessionHandoffEnv(session);
1196
- info(` ${dim("dev session")} ${session.id}`);
1194
+ revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session2.id);
1195
+ devHandoffEnv = devSessionHandoffEnv(session2);
1196
+ info(` ${dim("dev session")} ${session2.id}`);
1197
1197
  info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
1198
1198
  info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
1199
- info(` ${dim("expires")} ${session.expires_at}`);
1199
+ info(` ${dim("expires")} ${session2.expires_at}`);
1200
1200
  info(
1201
1201
  ` ${dim("sign-in")} ${Object.keys(devHandoffEnv).length > 0 ? "zero-click (server-side dev handoff armed)" : "browser SSO (backend issued no handoff secret \u2014 pre-2026-07-15 backend)"}`
1202
1202
  );
@@ -1455,11 +1455,11 @@ async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
1455
1455
  success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
1456
1456
  return upgraded;
1457
1457
  }
1458
- function devSessionHandoffEnv(session) {
1459
- const secret = session.handoff_secret;
1458
+ function devSessionHandoffEnv(session2) {
1459
+ const secret = session2.handoff_secret;
1460
1460
  if (typeof secret !== "string" || secret.length === 0) return {};
1461
1461
  return {
1462
- DASHWISE_DEV_SESSION_ID: session.id,
1462
+ DASHWISE_DEV_SESSION_ID: session2.id,
1463
1463
  DASHWISE_DEV_HANDOFF_SECRET: secret
1464
1464
  };
1465
1465
  }
@@ -12853,9 +12853,7 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12853
12853
  person in plain words that the changes are saved and that Release in
12854
12854
  Dashwise includes them. Don't ask them to merge anything.
12855
12855
  - Never force-push and never rewrite the default branch's history.
12856
- - The person only confirms Release and Take live in Dashwise. When the work is
12857
- pushed, tell them it is ready: in Dashwise open the module, click Release,
12858
- then Take live.
12856
+ - When the work is pushed, tell the person it is ready to release.
12859
12857
 
12860
12858
  ## Cloud workflow
12861
12859
 
@@ -12866,8 +12864,8 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12866
12864
  - \`npm run smoke:production\` starts the built app on a local loopback port. If a cloud sandbox refuses that port, rerun the command with the network permission; never skip the check.
12867
12865
  - Report what you changed, the pushed SHA and the checks you ran, including failures, in plain words.
12868
12866
  - Never print, persist or commit secrets. Repository code delivery needs no Dashwise token. Do not log in or access company data for setup or checks; see "Dashwise login" for full development.
12869
- - Never automatically release, publish, deploy or take an installation live. Release and Live require an explicit human action.
12870
- - Even when the person says you may release, you can't do it for them: tell them in one sentence where to click (in Dashwise open the module, click Release, then Take live). Never ask for a \`dashwise login\` or a token just to release.
12867
+ - Before releasing, installing/updating an installation or taking live, propose the exact action in the chat and wait for the person to accept or change it; never do it on your own.
12868
+ - When the person accepts, do exactly that action if your Dashwise connection allows it; if Dashwise refuses, tell them what it said (for example a permission to turn on in My Account). If you have no way to do it, tell them in one sentence where to click (in Dashwise open the module, click Release, then Take live). Never ask for a \`dashwise login\` or a token just to release.
12871
12869
 
12872
12870
  ## If you can't see the repository
12873
12871
 
@@ -14394,7 +14392,7 @@ async function destroyDevInstallCommand(options) {
14394
14392
  return 0;
14395
14393
  }
14396
14394
  if (!options.yes && process.stdout.isTTY) {
14397
- const choice = await confirm({
14395
+ const choice = await confirm$1({
14398
14396
  message: `Destroy dev install ${code(`${slug} (id=${entry.installationId})`)}? Tables and rows will be soft-deleted (recoverable for 30 days via dashboard trash).`
14399
14397
  });
14400
14398
  if (isCancel(choice) || !choice) {
@@ -16335,7 +16333,7 @@ function startMcpProxy(opts) {
16335
16333
  const reconnectMs = opts.reconnectMs ?? 2e3;
16336
16334
  const verbose = opts.verbose === true;
16337
16335
  const readOnly = opts.readOnly === true;
16338
- let session = opts.session;
16336
+ let session2 = opts.session;
16339
16337
  let sessionId = null;
16340
16338
  let protocolVersion = null;
16341
16339
  let readTools = null;
@@ -16364,7 +16362,7 @@ function startMcpProxy(opts) {
16364
16362
  parsed = JSON.parse(text4);
16365
16363
  } catch {
16366
16364
  log2(
16367
- `Discarded a non-JSON response from ${redactUrl(session.connectUrl)} (${source}); stdout carries MCP frames only.`
16365
+ `Discarded a non-JSON response from ${redactUrl(session2.connectUrl)} (${source}); stdout carries MCP frames only.`
16368
16366
  );
16369
16367
  trace(`discarded ${Buffer.byteLength(text4, "utf8")} bytes of non-JSON`);
16370
16368
  return;
@@ -16374,7 +16372,7 @@ function startMcpProxy(opts) {
16374
16372
  ) : parsed !== null && typeof parsed === "object" && "jsonrpc" in parsed;
16375
16373
  if (!looksJsonRpc) {
16376
16374
  log2(
16377
- `Discarded a JSON response from ${redactUrl(session.connectUrl)} that is not JSON-RPC (${source}).`
16375
+ `Discarded a JSON response from ${redactUrl(session2.connectUrl)} that is not JSON-RPC (${source}).`
16378
16376
  );
16379
16377
  return;
16380
16378
  }
@@ -16405,7 +16403,7 @@ function startMcpProxy(opts) {
16405
16403
  reauthAttempt = (async () => {
16406
16404
  log2("The session key expired or was rotated \u2014 re-authenticating once.");
16407
16405
  try {
16408
- session = await reauth();
16406
+ session2 = await reauth();
16409
16407
  sessionId = null;
16410
16408
  return true;
16411
16409
  } catch (err) {
@@ -16446,7 +16444,7 @@ function startMcpProxy(opts) {
16446
16444
  };
16447
16445
  const post = async (text4, isInitialize) => {
16448
16446
  if (stopped) return null;
16449
- return doFetch(session.connectUrl, {
16447
+ return doFetch(session2.connectUrl, {
16450
16448
  method: "POST",
16451
16449
  headers: requestHeaders(isInitialize),
16452
16450
  body: text4
@@ -16474,7 +16472,7 @@ function startMcpProxy(opts) {
16474
16472
  res = await post(text4, isInitialize);
16475
16473
  } catch (err) {
16476
16474
  const message = err.message;
16477
- log2(`Transport error talking to ${redactUrl(session.connectUrl)}: ${message}`);
16475
+ log2(`Transport error talking to ${redactUrl(session2.connectUrl)}: ${message}`);
16478
16476
  emit(transportErrorFrame(frame?.id ?? null, `Transport error: ${message}`));
16479
16477
  return { kind: "failed" };
16480
16478
  }
@@ -16498,7 +16496,7 @@ function startMcpProxy(opts) {
16498
16496
  const bodyText = await res.text();
16499
16497
  if (!res.ok) {
16500
16498
  const detail = res.status === 401 ? "the endpoint key is no longer valid (expired, rotated by another `dashwise mcp`, or revoked)" : `HTTP ${res.status}`;
16501
- log2(`Refused by ${redactUrl(session.connectUrl)}: ${detail}`);
16499
+ log2(`Refused by ${redactUrl(session2.connectUrl)}: ${detail}`);
16502
16500
  emit(transportErrorFrame(frame?.id ?? null, `Dashwise MCP server: ${detail}`));
16503
16501
  if (res.status === 401) {
16504
16502
  exitCode = 1;
@@ -16585,7 +16583,7 @@ function startMcpProxy(opts) {
16585
16583
  streamAbort = new AbortController();
16586
16584
  let res;
16587
16585
  try {
16588
- res = await doFetch(session.connectUrl, {
16586
+ res = await doFetch(session2.connectUrl, {
16589
16587
  method: "GET",
16590
16588
  headers: {
16591
16589
  Accept: "text/event-stream",
@@ -16621,7 +16619,7 @@ function startMcpProxy(opts) {
16621
16619
  }
16622
16620
  streamUnavailable = true;
16623
16621
  log2(
16624
- `Server stream refused by ${redactUrl(session.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
16622
+ `Server stream refused by ${redactUrl(session2.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
16625
16623
  );
16626
16624
  emit(
16627
16625
  transportErrorFrame(
@@ -16898,9 +16896,9 @@ async function mcpCommand(options = {}) {
16898
16896
  return 1;
16899
16897
  }
16900
16898
  const label = options.endpointName ?? defaultEndpointLabel();
16901
- let session;
16899
+ let session2;
16902
16900
  try {
16903
- session = await openCliSession({
16901
+ session2 = await openCliSession({
16904
16902
  workspaceId: workspace2.id,
16905
16903
  label,
16906
16904
  ...scopes !== void 0 ? { scopes } : {}
@@ -16909,24 +16907,24 @@ async function mcpCommand(options = {}) {
16909
16907
  fail(describeSessionError(err, apiUrl, scopes));
16910
16908
  return 1;
16911
16909
  }
16912
- if (scopes !== void 0 && !Array.isArray(session.scopes)) {
16913
- await revokeSession(session.endpointId);
16910
+ if (scopes !== void 0 && !Array.isArray(session2.scopes)) {
16911
+ await revokeSession(session2.endpointId);
16914
16912
  fail(SCOPES_NOT_APPLIED);
16915
16913
  return 1;
16916
16914
  }
16917
16915
  info(
16918
- `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session.endpointId} (${session.name}).`
16916
+ `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session2.endpointId} (${session2.name}).`
16919
16917
  );
16920
- info(`Session key expires ${dim(session.expiresAt)}.`);
16921
- info(describeScopes(session.scopes));
16918
+ info(`Session key expires ${dim(session2.expiresAt)}.`);
16919
+ info(describeScopes(session2.scopes));
16922
16920
  if (options.readOnly) {
16923
16921
  warn(
16924
16922
  "Read-only: write tools are refused by this proxy (a client-side guard, not a server-enforced scope \u2014 for that, use --scope)."
16925
16923
  );
16926
16924
  }
16927
- let ownedEndpointId = session.endpointId;
16925
+ let ownedEndpointId = session2.endpointId;
16928
16926
  const proxy = startMcpProxy({
16929
- session,
16927
+ session: session2,
16930
16928
  reauth: async () => {
16931
16929
  const fresh = await openCliSession({
16932
16930
  workspaceId: workspace2.id,
@@ -20249,6 +20247,22 @@ function refusalHint(code2, context, subject = {}) {
20249
20247
  message: `Too many builds of ${named} in the last hour. Try again later.`,
20250
20248
  needsGrant: false
20251
20249
  };
20250
+ // Install invitations (`dashwise module invites`, Südbau retro 6).
20251
+ case "AUTHOR_ADMIN_REQUIRED":
20252
+ return {
20253
+ message: `Only an admin of the company that owns ${named} can manage its install invitations. Ask that company's admin.`,
20254
+ needsGrant: true
20255
+ };
20256
+ case "COMPANY_ADMIN_REQUIRED":
20257
+ return {
20258
+ message: "Only an admin of this company can see or decline its install invitations. Ask your company's admin.",
20259
+ needsGrant: true
20260
+ };
20261
+ case "MODULE_PRIVATE_SHARE_FIRST":
20262
+ return {
20263
+ message: `${named} is still private. Share it with your company in Dashwise first, then invite another company.`,
20264
+ needsGrant: false
20265
+ };
20252
20266
  default:
20253
20267
  if (MODULE_RIGHT_CODES.has(code2)) {
20254
20268
  return {
@@ -20658,6 +20672,133 @@ function usesJson(readiness) {
20658
20672
  };
20659
20673
  }
20660
20674
 
20675
+ // src/commands/module/installations.ts
20676
+ init_api_client();
20677
+ init_config();
20678
+ init_output();
20679
+ var AUDIENCE = {
20680
+ company: "company",
20681
+ teams: "Team Spaces",
20682
+ people: "named people"
20683
+ };
20684
+ var givesRole = (roleKey) => typeof roleKey === "string" && roleKey !== "none";
20685
+ function governanceAudience(row) {
20686
+ if (!row.rules) return null;
20687
+ if (givesRole(row.rules.company)) return AUDIENCE.company;
20688
+ if (row.rules.teams.some((t) => t.live === true && givesRole(t.role_key))) return AUDIENCE.teams;
20689
+ return AUDIENCE.people;
20690
+ }
20691
+ function toListEntry(row) {
20692
+ return {
20693
+ id: row.id,
20694
+ module_slug: row.module?.slug ?? null,
20695
+ name: row.name ?? row.module?.name ?? null,
20696
+ audience: row.reach?.kind ? AUDIENCE[row.reach.kind] ?? row.reach.kind : null,
20697
+ version: row.module_version?.version ?? null,
20698
+ held: row.held === true,
20699
+ development: isDevInstallKind(row.kind),
20700
+ public_url: row.public_url,
20701
+ admin_view_only: false
20702
+ };
20703
+ }
20704
+ async function moduleInstallationsCommand(slug, options) {
20705
+ if (!isLoggedIn()) {
20706
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
20707
+ return 1;
20708
+ }
20709
+ const cfg = readConfig();
20710
+ if (!cfg) {
20711
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
20712
+ return 1;
20713
+ }
20714
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
20715
+ let workspace2;
20716
+ try {
20717
+ workspace2 = await resolveWorkspaceWithFallback({
20718
+ userFlag: options.workspace,
20719
+ cachedWorkspaceId: slug ? readModuleEntry(slug)?.workspaceId : void 0,
20720
+ configDefault: cfg.workspaceSlug,
20721
+ apiUrl: resolveApiUrl(),
20722
+ apiToken: cfg.token
20723
+ });
20724
+ } catch (err) {
20725
+ fail(`Company selection failed: ${err.message}`);
20726
+ return 1;
20727
+ }
20728
+ let rows;
20729
+ try {
20730
+ rows = await listWorkspaceInstallations(workspace2.id, auth);
20731
+ } catch (err) {
20732
+ fail(`Could not read the installations in ${code(workspace2.slug)}: ${err.message}`);
20733
+ return 1;
20734
+ }
20735
+ const governance = [];
20736
+ try {
20737
+ const govRows = await apiRequest(
20738
+ "GET",
20739
+ `/api/modules/workspaces/${workspace2.id}/installations/governance`,
20740
+ void 0,
20741
+ auth
20742
+ );
20743
+ const seen = new Set(rows.map((row) => row.id));
20744
+ const slugOf = new Map(rows.map((row) => [row.module_id, row.module?.slug ?? null]));
20745
+ let slugModuleId;
20746
+ if (slug) {
20747
+ slugModuleId = rows.find((row) => row.module?.slug === slug)?.module_id;
20748
+ if (slugModuleId === void 0) {
20749
+ try {
20750
+ slugModuleId = (await getModuleBySlug(slug, auth)).id;
20751
+ } catch {
20752
+ }
20753
+ }
20754
+ }
20755
+ for (const g of govRows) {
20756
+ if (g.trashed || seen.has(g.installation_id)) continue;
20757
+ if (slug && g.module_id !== slugModuleId) continue;
20758
+ governance.push({
20759
+ id: g.installation_id,
20760
+ module_slug: slugOf.get(g.module_id) ?? (slug && g.module_id === slugModuleId ? slug : null),
20761
+ name: g.name || null,
20762
+ audience: governanceAudience(g),
20763
+ version: null,
20764
+ held: g.held === true,
20765
+ development: null,
20766
+ public_url: null,
20767
+ admin_view_only: true
20768
+ });
20769
+ }
20770
+ } catch (err) {
20771
+ if (!(err instanceof ApiError && (err.status === 403 || err.status === 404))) {
20772
+ fail(`Could not read the company's full installation list: ${err.message}`);
20773
+ return 1;
20774
+ }
20775
+ }
20776
+ const entries = rows.filter((row) => !slug || row.module?.slug === slug).map(toListEntry).concat(governance).filter((entry) => options.dev || !entry.development).sort((a, b) => (a.module_slug ?? "").localeCompare(b.module_slug ?? "") || a.id - b.id);
20777
+ if (options.json) {
20778
+ out(JSON.stringify({ workspace: workspace2.slug, installations: entries }));
20779
+ return 0;
20780
+ }
20781
+ if (entries.length === 0) {
20782
+ info(
20783
+ slug ? `No installations of ${code(slug)} you can see in ${code(workspace2.slug)}.` : `No installations you can see in ${code(workspace2.slug)}.`
20784
+ );
20785
+ return 0;
20786
+ }
20787
+ for (const e of entries) {
20788
+ const tags = [
20789
+ e.audience,
20790
+ e.version ? `v${e.version}` : null,
20791
+ e.held ? "held" : null,
20792
+ e.development ? "development" : null,
20793
+ e.admin_view_only ? "admin view, not open to you" : null
20794
+ ].filter(Boolean);
20795
+ out(
20796
+ `#${e.id} ${e.module_slug ?? "?"} ${e.name ? `"${e.name}"` : ""} ${dim(tags.join(", "))}${e.public_url ? ` ${e.public_url}` : ""}`.replace(/ {3,}/g, " ")
20797
+ );
20798
+ }
20799
+ return 0;
20800
+ }
20801
+
20661
20802
  // src/commands/module/uninstall.ts
20662
20803
  init_api_client();
20663
20804
  init_config();
@@ -20828,6 +20969,269 @@ function surfaceUninstallError(err, slug, workspaceSlug) {
20828
20969
  return 1;
20829
20970
  }
20830
20971
 
20972
+ // src/commands/module/invites.ts
20973
+ init_api_client();
20974
+ init_config();
20975
+ init_output();
20976
+ var positiveId2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
20977
+ function session(json) {
20978
+ const cfg = readConfig();
20979
+ if (!isLoggedIn() || !cfg) return reportCliError(`Not logged in. Run ${code("dashwise login")} first.`, { json });
20980
+ return {
20981
+ auth: { baseUrl: resolveApiUrl(), auth: { token: cfg.token } },
20982
+ token: cfg.token,
20983
+ workspaceSlug: cfg.workspaceSlug
20984
+ };
20985
+ }
20986
+ async function resolveModule(slug, s, json) {
20987
+ try {
20988
+ const mod = await getModuleBySlug(slug, s.auth);
20989
+ return { id: mod.id, slug: mod.slug };
20990
+ } catch (err) {
20991
+ return reportRefusal(err, { module: slug }, { json, action: "Resolving the module" });
20992
+ }
20993
+ }
20994
+ async function confirm(question, confirmLabel, opts) {
20995
+ if (opts.yes) return null;
20996
+ if (opts.json || !process.stdin.isTTY || !process.stdout.isTTY) {
20997
+ return reportCliError("This changes an invitation. Pass --yes to confirm in non-interactive or --json mode.", {
20998
+ json: opts.json,
20999
+ code: "USAGE"
21000
+ });
21001
+ }
21002
+ const answer = await select({
21003
+ message: question,
21004
+ options: [
21005
+ { value: "cancel", label: "Cancel" },
21006
+ { value: "go", label: confirmLabel }
21007
+ ],
21008
+ initialValue: "cancel"
21009
+ });
21010
+ if (isCancel(answer) || answer !== "go") {
21011
+ reportCliError("Aborted.", {});
21012
+ return 130;
21013
+ }
21014
+ return null;
21015
+ }
21016
+ function companyLabel(row) {
21017
+ return row.company_name ? `${row.company_name} (company #${row.workspace_id})` : `company #${row.workspace_id}`;
21018
+ }
21019
+ async function moduleInvitesListCommand(slug, options) {
21020
+ const { json } = options;
21021
+ const s = session(json);
21022
+ if (typeof s === "number") return s;
21023
+ const mod = await resolveModule(slug, s, json);
21024
+ if (typeof mod === "number") return mod;
21025
+ let rows;
21026
+ try {
21027
+ rows = await apiRequest("GET", `/api/modules/${mod.id}/invites`, void 0, s.auth);
21028
+ } catch (err) {
21029
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Reading the invitations" });
21030
+ }
21031
+ if (json) {
21032
+ out(JSON.stringify({ module: mod.slug, invites: rows }));
21033
+ return 0;
21034
+ }
21035
+ if (rows.length === 0) {
21036
+ info(`No open invitations for ${code(mod.slug)}.`);
21037
+ return 0;
21038
+ }
21039
+ for (const row of rows) {
21040
+ const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
21041
+ out(`#${row.id} ${companyLabel(row)} ${dim(row.invited_at.slice(0, 10))}${note}`);
21042
+ }
21043
+ return 0;
21044
+ }
21045
+ async function moduleInvitesSendCommand(slug, options) {
21046
+ const { json } = options;
21047
+ const usage2 = (message) => reportCliError(message, { json, code: "USAGE" });
21048
+ if (!options.to) return usage2("Pass --to <company id> to name the company to invite.");
21049
+ const s = session(json);
21050
+ if (typeof s === "number") return s;
21051
+ let target;
21052
+ const numeric = /^[1-9][0-9]*$/.test(options.to) ? Number(options.to) : void 0;
21053
+ let mine;
21054
+ try {
21055
+ mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
21056
+ } catch (err) {
21057
+ return reportRefusal(err, {}, { json, action: "Reading your companies" });
21058
+ }
21059
+ const own = mine.find((w) => numeric !== void 0 ? w.id === numeric : w.slug === options.to);
21060
+ if (own) {
21061
+ target = { id: own.id, label: `${own.name} (company #${own.id})` };
21062
+ } else if (positiveId2(numeric)) {
21063
+ target = { id: numeric, label: `company #${numeric}` };
21064
+ } else {
21065
+ return usage2(
21066
+ `${options.to} is not one of your companies. For another company, pass its company id (an admin of that company finds it in its settings).`
21067
+ );
21068
+ }
21069
+ const mod = await resolveModule(slug, s, json);
21070
+ if (typeof mod === "number") return mod;
21071
+ const stop = await confirm(
21072
+ [
21073
+ `Invite ${target.label} to install ${mod.slug}?`,
21074
+ `- Admins of that company can then see ${mod.slug} and install it.`,
21075
+ "- Their data stays theirs; you do not see it.",
21076
+ `- Your module's logic and its updates reach their installations.`,
21077
+ "- You see their company name only after one of their admins installs it or asks to.",
21078
+ `- You can withdraw the invitation at any time with ${code(`dashwise module invites revoke ${mod.slug} <invitation id>`)}.`
21079
+ ].join("\n"),
21080
+ "Send the invitation",
21081
+ options
21082
+ );
21083
+ if (stop !== null) return stop;
21084
+ let invite;
21085
+ try {
21086
+ const body = await apiRequest(
21087
+ "POST",
21088
+ `/api/modules/${mod.id}/invites`,
21089
+ { workspaceId: target.id, ...options.note !== void 0 ? { note: options.note } : {} },
21090
+ s.auth
21091
+ );
21092
+ invite = body.invite;
21093
+ } catch (err) {
21094
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Sending the invitation" });
21095
+ }
21096
+ if (json) out(JSON.stringify({ module: mod.slug, invite }));
21097
+ else success(`Invited ${target.label} to install ${mod.slug} (invitation #${invite.id}).`);
21098
+ return 0;
21099
+ }
21100
+ async function moduleInvitesRevokeCommand(slug, inviteId, options) {
21101
+ const { json } = options;
21102
+ if (!positiveId2(inviteId)) {
21103
+ return reportCliError(`Pass the invitation id. List them with ${code(`dashwise module invites list ${slug}`)}.`, {
21104
+ json,
21105
+ code: "USAGE"
21106
+ });
21107
+ }
21108
+ const s = session(json);
21109
+ if (typeof s === "number") return s;
21110
+ const mod = await resolveModule(slug, s, json);
21111
+ if (typeof mod === "number") return mod;
21112
+ const stop = await confirm(
21113
+ [
21114
+ `Withdraw invitation #${inviteId} for ${mod.slug}?`,
21115
+ `- Installations made with it stay, but they no longer receive updates of ${mod.slug}.`,
21116
+ "- No new installation can start from it."
21117
+ ].join("\n"),
21118
+ "Withdraw the invitation",
21119
+ options
21120
+ );
21121
+ if (stop !== null) return stop;
21122
+ try {
21123
+ await apiRequest("DELETE", `/api/modules/${mod.id}/invites/${inviteId}`, void 0, s.auth);
21124
+ } catch (err) {
21125
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Withdrawing the invitation" });
21126
+ }
21127
+ if (json) out(JSON.stringify({ module: mod.slug, revoked: inviteId }));
21128
+ else success(`Withdrew invitation #${inviteId} for ${mod.slug}.`);
21129
+ return 0;
21130
+ }
21131
+ async function resolveOwnCompany(s, flag, opts) {
21132
+ const { json } = opts;
21133
+ let mine;
21134
+ try {
21135
+ mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
21136
+ } catch (err) {
21137
+ return reportRefusal(err, {}, { json, action: "Reading your companies" });
21138
+ }
21139
+ const list = mine.map((w) => `${w.slug} (company #${w.id})`).join(", ");
21140
+ const wanted = flag ?? s.workspaceSlug;
21141
+ if (wanted !== void 0) {
21142
+ const found = mine.find((w) => w.slug === wanted || String(w.id) === wanted);
21143
+ if (found) return found;
21144
+ return reportCliError(`${wanted} is not one of your companies. Your companies: ${list}.`, { json, code: "USAGE" });
21145
+ }
21146
+ if (mine.length === 1) return mine[0];
21147
+ if (mine.length === 0) return reportCliError("Your account belongs to no company yet.", { json });
21148
+ if (!opts.interactive || json || !process.stdin.isTTY || !process.stdout.isTTY) {
21149
+ return reportCliError(`Choose the company with --workspace <slug or id>. Your companies: ${list}.`, {
21150
+ json,
21151
+ code: "USAGE"
21152
+ });
21153
+ }
21154
+ const choice = await select({
21155
+ message: "Which company?",
21156
+ options: mine.map((w) => ({ value: String(w.id), label: w.name, hint: w.slug }))
21157
+ });
21158
+ const picked = isCancel(choice) ? void 0 : mine.find((w) => String(w.id) === choice);
21159
+ if (!picked) {
21160
+ reportCliError("Aborted.", {});
21161
+ return 130;
21162
+ }
21163
+ return picked;
21164
+ }
21165
+ async function moduleInvitesReceivedCommand(options) {
21166
+ const { json } = options;
21167
+ const s = session(json);
21168
+ if (typeof s === "number") return s;
21169
+ const company = await resolveOwnCompany(s, options.workspace, { json, interactive: true });
21170
+ if (typeof company === "number") return company;
21171
+ let rows;
21172
+ try {
21173
+ rows = await apiRequest(
21174
+ "GET",
21175
+ `/api/modules/workspaces/${company.id}/invites`,
21176
+ void 0,
21177
+ s.auth
21178
+ );
21179
+ } catch (err) {
21180
+ return reportRefusal(err, {}, { json, action: "Reading the invitations" });
21181
+ }
21182
+ if (json) {
21183
+ out(JSON.stringify({ workspace: company.slug, invites: rows }));
21184
+ return 0;
21185
+ }
21186
+ if (rows.length === 0) {
21187
+ info(`No open invitations for ${code(company.slug)}.`);
21188
+ return 0;
21189
+ }
21190
+ for (const row of rows) {
21191
+ const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
21192
+ out(
21193
+ `#${row.id} ${row.module.slug} ${dim(`v${row.module.current_version}`)} from ${row.from_company.name} ${dim(row.invited_at.slice(0, 10))}${note}`
21194
+ );
21195
+ }
21196
+ const inCompany = `--workspace ${company.slug}`;
21197
+ info(
21198
+ `Install one with ${code(`dashwise module install <slug> ${inCompany}`)}, or decline it with ${code(`dashwise module invites decline <id> ${inCompany}`)}.`
21199
+ );
21200
+ return 0;
21201
+ }
21202
+ async function moduleInvitesDeclineCommand(inviteId, options) {
21203
+ const { json } = options;
21204
+ if (!positiveId2(inviteId)) {
21205
+ return reportCliError(`Pass the invitation id. List them with ${code("dashwise module invites received")}.`, {
21206
+ json,
21207
+ code: "USAGE"
21208
+ });
21209
+ }
21210
+ const s = session(json);
21211
+ if (typeof s === "number") return s;
21212
+ const company = await resolveOwnCompany(s, options.workspace, { json, interactive: !options.yes });
21213
+ if (typeof company === "number") return company;
21214
+ const stop = await confirm(
21215
+ `Decline invitation #${inviteId} for ${company.slug}? The module's company may invite you again.`,
21216
+ "Decline the invitation",
21217
+ options
21218
+ );
21219
+ if (stop !== null) return stop;
21220
+ try {
21221
+ await apiRequest(
21222
+ "POST",
21223
+ `/api/modules/workspaces/${company.id}/invites/${inviteId}/decline`,
21224
+ void 0,
21225
+ s.auth
21226
+ );
21227
+ } catch (err) {
21228
+ return reportRefusal(err, {}, { json, action: "Declining the invitation" });
21229
+ }
21230
+ if (json) out(JSON.stringify({ workspace: company.slug, declined: inviteId }));
21231
+ else success(`Declined invitation #${inviteId}.`);
21232
+ return 0;
21233
+ }
21234
+
20831
21235
  // src/commands/module/pull-data.ts
20832
21236
  init_api_client();
20833
21237
  init_output();
@@ -24775,7 +25179,7 @@ async function moduleRuntimeSetCommand(slugArg, options) {
24775
25179
  const slug = before.module.slug;
24776
25180
  const subject = { module: slug, installationId: before.installation_id };
24777
25181
  if (streaming !== void 0 && !options.yes && !options.json && process.stdin.isTTY) {
24778
- const ok = await confirm({
25182
+ const ok = await confirm$1({
24779
25183
  message: `Turn streaming ${streaming === "RESPONSE_STREAM" ? "on" : "off"} for ${slug} (install #${before.installation_id})? The switch takes about a minute, and pages may come back empty while it runs.`
24780
25184
  });
24781
25185
  if (isCancel(ok) || ok !== true) {
@@ -25695,6 +26099,63 @@ moduleCmd.command("uninstall [slug]").description(
25695
26099
  });
25696
26100
  }
25697
26101
  );
26102
+ var invitesCmd = moduleCmd.command("invites").description(
26103
+ "Install invitations: send, list and withdraw them for your module, or see and decline the ones your company received."
26104
+ );
26105
+ var inviteIdArg = (value) => value !== void 0 && /^[1-9][0-9]*$/.test(value) ? Number(value) : void 0;
26106
+ invitesCmd.command("list <slug>").description("List the open install invitations for your module.").option("--json", "Emit machine-readable JSON.").action(async (slug, cmdOpts) => {
26107
+ process.exitCode = await moduleInvitesListCommand(slug, {
26108
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26109
+ });
26110
+ });
26111
+ invitesCmd.command("send <slug>").description(
26112
+ "Invite another company to install your module. Needs admin rights in the module's company. Asks before sending; pass --yes in scripts."
26113
+ ).option("--to <company>", "The company id to invite, or the slug of a company you belong to.").option("--note <text>", "A short note shown to the invited company (max 1000 characters).").option("-y, --yes", "Send without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(
26114
+ async (slug, cmdOpts) => {
26115
+ process.exitCode = await moduleInvitesSendCommand(slug, {
26116
+ ...cmdOpts.to !== void 0 ? { to: cmdOpts.to } : {},
26117
+ ...cmdOpts.note !== void 0 ? { note: cmdOpts.note } : {},
26118
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26119
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26120
+ });
26121
+ }
26122
+ );
26123
+ invitesCmd.command("revoke <slug> <invite-id>").description(
26124
+ "Withdraw an install invitation. Installations made before stay. Asks first; pass --yes in scripts."
26125
+ ).option("-y, --yes", "Withdraw without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(async (slug, inviteId, cmdOpts) => {
26126
+ process.exitCode = await moduleInvitesRevokeCommand(slug, inviteIdArg(inviteId), {
26127
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26128
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26129
+ });
26130
+ });
26131
+ invitesCmd.command("received").description("List the open install invitations your company received (company admins).").option("--workspace <slug-or-id>", "Company (default: config default or interactive picker).").option("--json", "Emit machine-readable JSON.").action(async (cmdOpts) => {
26132
+ process.exitCode = await moduleInvitesReceivedCommand({
26133
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26134
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26135
+ });
26136
+ });
26137
+ invitesCmd.command("decline <invite-id>").description(
26138
+ "Decline an install invitation your company received (company admins). Asks first; pass --yes in scripts."
26139
+ ).option("--workspace <slug-or-id>", "Company (default: config default or interactive picker).").option("-y, --yes", "Decline without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(
26140
+ async (inviteId, cmdOpts) => {
26141
+ process.exitCode = await moduleInvitesDeclineCommand(inviteIdArg(inviteId), {
26142
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26143
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26144
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26145
+ });
26146
+ }
26147
+ );
26148
+ moduleCmd.command("installations [slug]").description(
26149
+ "List the installations in a company that you can see: id, module, name, audience, version, held state and public address. With a slug, only that module. Read-only."
26150
+ ).option("--workspace <slug-or-id>", "Company (default: cached module company, config default, or interactive picker).").option("--dev", "Include development installations.").option("--json", "Emit machine-readable JSON.").action(
26151
+ async (slug, cmdOpts) => {
26152
+ process.exitCode = await moduleInstallationsCommand(slug, {
26153
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26154
+ ...cmdOpts.dev !== void 0 ? { dev: cmdOpts.dev } : {},
26155
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26156
+ });
26157
+ }
26158
+ );
25698
26159
  moduleCmd.command("status [slug]").description(
25699
26160
  "Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
25700
26161
  ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--json", "Emit machine-readable JSON.").action(
@@ -25972,7 +26433,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
25972
26433
  }
25973
26434
  );
25974
26435
  function getVersion() {
25975
- return "0.26.0";
26436
+ return "0.27.0";
25976
26437
  }
25977
26438
  function parseIntOption(value) {
25978
26439
  const n = Number.parseInt(value, 10);