@dashai/cli 0.26.0 → 0.28.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';
@@ -178,16 +178,12 @@ function removeModuleEntry(slug) {
178
178
  function isLoggedIn() {
179
179
  return readSavedToken() !== null;
180
180
  }
181
- var FILE_NAME, DIR_NAME, DEFAULT_API_URL, KNOWN_API_URLS;
181
+ var FILE_NAME, DIR_NAME, DEFAULT_API_URL;
182
182
  var init_config = __esm({
183
183
  "src/lib/config.ts"() {
184
184
  FILE_NAME = "auth.json";
185
185
  DIR_NAME = "dashwise";
186
- DEFAULT_API_URL = "https://api.dashwise.dev";
187
- KNOWN_API_URLS = {
188
- dev: "https://api.dashwise.dev",
189
- prod: "https://api.dashwise.net"
190
- };
186
+ DEFAULT_API_URL = "https://api.dashwise.net";
191
187
  }
192
188
  });
193
189
 
@@ -283,7 +279,7 @@ async function apiRequest(method, path, body, options = {}) {
283
279
  function asEnvelope(v) {
284
280
  if (!v || typeof v !== "object") return null;
285
281
  const env = v;
286
- const out2 = {};
282
+ const out2 = { body: env };
287
283
  if (typeof env.code === "string") out2.code = env.code;
288
284
  else if (typeof env.error === "string" && CODE_SHAPE.test(env.error)) out2.code = env.error;
289
285
  if (typeof env.message === "string") out2.message = env.message;
@@ -557,6 +553,14 @@ async function getModuleBySlug(slug, options = {}) {
557
553
  function isInstallGated(r) {
558
554
  return r.install_gated === true;
559
555
  }
556
+ async function getUpgradePreview(installationId, version, options = {}) {
557
+ return apiRequest(
558
+ "GET",
559
+ `/api/installations/${installationId}/upgrade-preview?version=${encodeURIComponent(version)}`,
560
+ void 0,
561
+ options
562
+ );
563
+ }
560
564
  async function installModule(moduleId, body, options = {}) {
561
565
  return apiRequest(
562
566
  "POST",
@@ -640,6 +644,8 @@ var init_api_client = __esm({
640
644
  code;
641
645
  context;
642
646
  retriable;
647
+ /** The whole parsed error body, when the backend sent a JSON object. */
648
+ body;
643
649
  constructor(status, envelope, fallbackMessage) {
644
650
  const code2 = envelope?.code ?? codeFromStatus(status);
645
651
  const message = envelope?.message ?? fallbackMessage;
@@ -649,6 +655,7 @@ var init_api_client = __esm({
649
655
  this.code = code2;
650
656
  this.context = envelope?.context;
651
657
  this.retriable = envelope?.retriable ?? status >= 500;
658
+ this.body = envelope?.body;
652
659
  }
653
660
  };
654
661
  CODE_SHAPE = /^[A-Z][A-Z0-9_]*$/;
@@ -1187,16 +1194,16 @@ async function devCommand(options) {
1187
1194
  }
1188
1195
  const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
1189
1196
  try {
1190
- const session = await registerDevSession(apiUrl, apiToken, {
1197
+ const session2 = await registerDevSession(apiUrl, apiToken, {
1191
1198
  target_workspace_id: workspaceContext.id,
1192
1199
  callback_origins: callbackOrigins
1193
1200
  });
1194
- revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
1195
- devHandoffEnv = devSessionHandoffEnv(session);
1196
- info(` ${dim("dev session")} ${session.id}`);
1201
+ revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session2.id);
1202
+ devHandoffEnv = devSessionHandoffEnv(session2);
1203
+ info(` ${dim("dev session")} ${session2.id}`);
1197
1204
  info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
1198
1205
  info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
1199
- info(` ${dim("expires")} ${session.expires_at}`);
1206
+ info(` ${dim("expires")} ${session2.expires_at}`);
1200
1207
  info(
1201
1208
  ` ${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
1209
  );
@@ -1455,11 +1462,11 @@ async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
1455
1462
  success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
1456
1463
  return upgraded;
1457
1464
  }
1458
- function devSessionHandoffEnv(session) {
1459
- const secret = session.handoff_secret;
1465
+ function devSessionHandoffEnv(session2) {
1466
+ const secret = session2.handoff_secret;
1460
1467
  if (typeof secret !== "string" || secret.length === 0) return {};
1461
1468
  return {
1462
- DASHWISE_DEV_SESSION_ID: session.id,
1469
+ DASHWISE_DEV_SESSION_ID: session2.id,
1463
1470
  DASHWISE_DEV_HANDOFF_SECRET: secret
1464
1471
  };
1465
1472
  }
@@ -2742,6 +2749,7 @@ var init_custom = __esm({
2742
2749
  "@tailwindcss/postcss": "^4",
2743
2750
  "class-variance-authority": "^0.7.1",
2744
2751
  clsx: "^2.1.1",
2752
+ geist: "^1.7.2",
2745
2753
  "lucide-react": "^0.577.0",
2746
2754
  next: "^15.0.0",
2747
2755
  "next-themes": "^0.4.6",
@@ -7266,6 +7274,15 @@ var init_custom = __esm({
7266
7274
  integrity: "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==",
7267
7275
  license: "MIT"
7268
7276
  },
7277
+ "node_modules/geist": {
7278
+ version: "1.7.2",
7279
+ resolved: "https://registry.npmjs.org/geist/-/geist-1.7.2.tgz",
7280
+ integrity: "sha512-Gu5lDFa3pLRyoBlBPf0QIFHVdWAnpco7fS1bJm41jyLPFoguBgiubseUN2oLXMgqZ7uxAxDoXcHMhCY/fOTTgg==",
7281
+ license: "SIL OPEN FONT LICENSE",
7282
+ peerDependencies: {
7283
+ next: ">=13.2.0"
7284
+ }
7285
+ },
7269
7286
  "node_modules/gensync": {
7270
7287
  version: "1.0.0-beta.2",
7271
7288
  resolved: "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -10168,7 +10185,13 @@ function nextConfig() {
10168
10185
  // we DERIVE NEXT_PUBLIC_DASHWISE_API_URL here from DASHWISE_API_URL at
10169
10186
  // build/start time \u2014 you never set the NEXT_PUBLIC_ var yourself. If
10170
10187
  // you already have one set (e.g. a custom setup), it wins.
10171
- const apiUrl = process.env.DASHWISE_API_URL;
10188
+ const apiUrl = process.env.DASHWISE_API_URL ?? ${JSON.stringify(DEFAULT_API_URL)};
10189
+ // The browser-facing API target: an explicit NEXT_PUBLIC_ override wins (the
10190
+ // server may reach the API on an internal address). The embedding app is the
10191
+ // app next to THAT public target, never the internal one.
10192
+ const publicApiUrl = process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl;
10193
+ const appUrl = new URL(publicApiUrl);
10194
+ appUrl.hostname = appUrl.hostname.replace(/^api\\./, "app.");
10172
10195
 
10173
10196
  const nextConfig = {
10174
10197
  // Required for the platform-hosted (per-install Lambda) runtime: the
@@ -10179,8 +10202,7 @@ const nextConfig = {
10179
10202
  typescript: { ignoreBuildErrors: true },
10180
10203
  eslint: { ignoreDuringBuilds: true },
10181
10204
  env: {
10182
- NEXT_PUBLIC_DASHWISE_API_URL:
10183
- process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl,
10205
+ NEXT_PUBLIC_DASHWISE_API_URL: publicApiUrl,
10184
10206
  },
10185
10207
  // WHO MAY FRAME THIS MODULE.
10186
10208
  //
@@ -10196,7 +10218,7 @@ const nextConfig = {
10196
10218
  async headers() {
10197
10219
  const ancestors =
10198
10220
  process.env.DASHWISE_FRAME_ANCESTORS ??
10199
- "https://app.dashwise.dev https://app.dashwise.net";
10221
+ appUrl.origin;
10200
10222
  return [
10201
10223
  {
10202
10224
  source: '/:path*',
@@ -10526,12 +10548,34 @@ export const config = {
10526
10548
  }
10527
10549
  function appLayoutCustomTsx(opts) {
10528
10550
  return `import './globals.css';
10551
+ import { GeistSans } from 'geist/font/sans';
10552
+ import { GeistMono } from 'geist/font/mono';
10529
10553
  import { AuthProvider, HostRouteSync } from '@dashai/sdk/auth/client';
10530
10554
  import { PlatformProvider, PLATFORM_PREPAINT_SCRIPT } from '@dashai/sdk/platform';
10531
10555
  import { ThemeProvider } from '@/components/theme-provider';
10532
10556
  import { PlatformThemeSync } from '@/components/platform-theme-sync';
10533
10557
  import { getSession } from '@/lib/session';
10534
10558
 
10559
+ // The Dashwise app renders in Geist, and \`globals.css\` names that family at the
10560
+ // front of \`--font-sans\`. Loading it here is what makes that name resolve: drop
10561
+ // these two lines and the stack falls straight through to system-ui, so the
10562
+ // module reads as a different product the moment anyone looks at text.
10563
+ //
10564
+ // The \`geist\` package ships the font files and loads them through
10565
+ // \`next/font/local\`: no download at build time (a cloud sandbox may only
10566
+ // reach npm and Dashwise, L434) and no runtime request, so nothing to declare
10567
+ // in \`runtime.outbound_apis\`. Never switch to \`next/font/google\`: that
10568
+ // fetches from Google during \`next build\`. The \`.variable\` classes on
10569
+ // <html> are what make Next emit the @font-face rules at all; they also expose
10570
+ // \`var(--font-geist-sans)\` for components that want it directly.
10571
+ //
10572
+ // They sit on <html>, not <body>: \`--font-sans\` in globals.css is declared on
10573
+ // :root (= <html>) and reads \`var(--font-geist-sans)\`, and a custom property
10574
+ // resolves where it is declared. The family name next/font generates is not
10575
+ // "Geist", so naming the family literally would not find the loaded files.
10576
+ const geistSans = GeistSans;
10577
+ const geistMono = GeistMono;
10578
+
10535
10579
  export const metadata = {
10536
10580
  title: ${JSON.stringify(opts.name)},
10537
10581
  description: ${JSON.stringify(`Dashwise module: ${opts.slug}`)},
@@ -10556,7 +10600,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
10556
10600
  // renders inside the Dashwise app shell, where the header/nav/theme toggle
10557
10601
  // already exist. The pre-paint script below only ever UPGRADES it to
10558
10602
  // "standalone", so chrome can never flash on top of the app's own.
10559
- <html lang="en" data-dw-surface="embedded" suppressHydrationWarning>
10603
+ <html
10604
+ lang="en"
10605
+ data-dw-surface="embedded"
10606
+ className={\`\${geistSans.variable} \${geistMono.variable}\`}
10607
+ suppressHydrationWarning
10608
+ >
10560
10609
  <head>
10561
10610
  {/* Runs before first paint: stamps data-dw-surface on <html> and
10562
10611
  re-applies the cached workspace brand. Keeps \`.dw-standalone-only\`
@@ -11219,6 +11268,7 @@ function packageJsonCustom(opts) {
11219
11268
  // below that fails with "every import from @dashai/generated
11220
11269
  // throws", which points nowhere near the cause.
11221
11270
  "@dashai/sdk": SCAFFOLD_SDK_RANGE,
11271
+ geist: GEIST_DEP_RANGE,
11222
11272
  next: "^15.0.0",
11223
11273
  react: "^19.0.0",
11224
11274
  "react-dom": "^19.0.0",
@@ -11367,13 +11417,18 @@ function globalsCss() {
11367
11417
  --code-bg: oklch(0.14 0 0);
11368
11418
  --code-fg: oklch(0.97 0 0);
11369
11419
 
11420
+ /* Geist is loaded from the \`geist\` package in app/layout.tsx, which puts
11421
+ --font-geist-sans/--font-geist-mono on <html> (= :root), so they resolve
11422
+ here. Keep those classes on <html>: on <body> the var() below would be
11423
+ unresolved at :root and the whole stack would fall back silently. The
11424
+ second argument of var() keeps "Geist" as a fallback name. */
11370
11425
  --font-sans:
11371
- "Geist", "Geist Fallback", ui-sans-serif, system-ui, -apple-system,
11426
+ var(--font-geist-sans, "Geist"), ui-sans-serif, system-ui, -apple-system,
11372
11427
  BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial,
11373
11428
  "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
11374
11429
  "Segoe UI Symbol", "Noto Color Emoji";
11375
11430
  --font-mono:
11376
- "Geist Mono", "Geist Mono Fallback", ui-monospace, SFMono-Regular, Menlo,
11431
+ var(--font-geist-mono, "Geist Mono"), ui-monospace, SFMono-Regular, Menlo,
11377
11432
  Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
11378
11433
 
11379
11434
  --radius: 0.625rem;
@@ -11583,27 +11638,42 @@ import { Slot } from "radix-ui";
11583
11638
 
11584
11639
  import { cn } from "@/lib/utils";
11585
11640
 
11641
+ // A faithful copy of the Dashwise app's own button kit \u2014 only the \`cn\` import
11642
+ // path and the "use client" directive above differ. Variants your module does
11643
+ // not use yet are kept on purpose: divergence is what makes a vendored
11644
+ // design-system file drift out of sync with the app it is supposed to match.
11586
11645
  const buttonVariants = cva(
11587
- "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
11646
+ // Focus ring: full-opacity \`ring\` with a background-coloured offset.
11647
+ // The stock shadcn ring (\`ring-ring/50\` drawn directly on the button)
11648
+ // measures 2.30:1 against the page and 2.73:1 against a primary fill \u2014
11649
+ // below the 3:1 that WCAG 1.4.11 / 2.4.11 require of a focus indicator,
11650
+ // i.e. effectively invisible on the most-used control in any module.
11651
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
11588
11652
  {
11589
11653
  variants: {
11590
11654
  variant: {
11591
11655
  default: "bg-primary text-primary-foreground hover:bg-primary/90",
11592
11656
  destructive:
11593
- "bg-destructive text-white hover:bg-destructive/90",
11657
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
11594
11658
  outline:
11595
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
11659
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
11596
11660
  secondary:
11597
11661
  "bg-secondary text-secondary-foreground hover:bg-secondary/80",
11598
11662
  ghost:
11599
- "hover:bg-accent hover:text-accent-foreground",
11663
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
11600
11664
  link: "text-primary underline-offset-4 hover:underline",
11665
+ "destructive-soft":
11666
+ "bg-destructive/10 text-destructive border border-destructive/20 shadow-xs hover:bg-destructive/20",
11601
11667
  },
11602
11668
  size: {
11603
11669
  default: "h-9 px-4 py-2 has-[>svg]:px-3",
11670
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
11604
11671
  sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
11605
11672
  lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
11606
11673
  icon: "size-9",
11674
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
11675
+ "icon-sm": "size-8",
11676
+ "icon-lg": "size-10",
11607
11677
  },
11608
11678
  },
11609
11679
  defaultVariants: {
@@ -11627,6 +11697,8 @@ function Button({
11627
11697
  return (
11628
11698
  <Comp
11629
11699
  data-slot="button"
11700
+ data-variant={variant}
11701
+ data-size={size}
11630
11702
  className={cn(buttonVariants({ variant, size, className }))}
11631
11703
  {...props}
11632
11704
  />
@@ -12351,6 +12423,15 @@ the workspace's theme and branding.
12351
12423
  | corners / elevation | \`rounded-md\`/\`rounded-lg\`, \`shadow-xs\`\u2026\`shadow-md\` |
12352
12424
  | type | inherit \`--font-sans\`; ids and code use \`font-mono\` |
12353
12425
 
12426
+ **Don't re-declare the font.** \`app/layout.tsx\` loads Geist from the
12427
+ \`geist\` package and puts its variables on \`<html>\`; \`globals.css\` reads
12428
+ them with \`var(--font-geist-sans, "Geist")\`. Leave both alone. Never switch to
12429
+ \`next/font/google\` (it downloads from Google during \`next build\`, which fails
12430
+ in a cloud sandbox) and never move the variables to \`<body>\` (the var() at
12431
+ \`:root\` would be unresolved and text silently falls back to Tailwind's default
12432
+ sans). Same class of trap for \`--shadow-*\`, which must be mapped into
12433
+ \`@theme\` to be reachable by \`shadow-xs\`/\`shadow-sm\`.
12434
+
12354
12435
  **Theme and branding arrive from the host.** \`PlatformProvider\` +
12355
12436
  \`PlatformThemeSync\` (already wired in \`app/layout.tsx\`) follow the app's
12356
12437
  light/dark choice and apply the workspace's brand colour to \`--primary\` /
@@ -12853,9 +12934,7 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12853
12934
  person in plain words that the changes are saved and that Release in
12854
12935
  Dashwise includes them. Don't ask them to merge anything.
12855
12936
  - 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.
12937
+ - When the work is pushed, tell the person it is ready to release.
12859
12938
 
12860
12939
  ## Cloud workflow
12861
12940
 
@@ -12866,8 +12945,8 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
12866
12945
  - \`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
12946
  - Report what you changed, the pushed SHA and the checks you ran, including failures, in plain words.
12868
12947
  - 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.
12948
+ - 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.
12949
+ - 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
12950
 
12872
12951
  ## If you can't see the repository
12873
12952
 
@@ -12911,13 +12990,15 @@ function companyApiHost(apiUrl) {
12911
12990
  return null;
12912
12991
  }
12913
12992
  }
12993
+ function docsApiHost(apiUrl) {
12994
+ return companyApiHost(apiUrl) ?? new URL(DEFAULT_API_URL).host;
12995
+ }
12914
12996
  function apiHostLine(apiUrl) {
12915
- const host = companyApiHost(apiUrl);
12916
- return host ? `\`${host}\` (this company's Dashwise API; the company uses Dashwise at ${HOSTED_API_HOSTS[host]})` : "the company's Dashwise API host: `api.dashwise.net` if the person opens Dashwise at app.dashwise.net, `api.dashwise.dev` if at app.dashwise.dev. If you can't tell, ask them which address they use";
12997
+ const host = docsApiHost(apiUrl);
12998
+ return `\`${host}\` (this company's Dashwise API; the company uses Dashwise at ${HOSTED_API_HOSTS[host] ?? host})`;
12917
12999
  }
12918
13000
  function codexEnvironmentInstructions(apiUrl) {
12919
- const host = companyApiHost(apiUrl);
12920
- const apiUrlValue = host ? `https://${host}` : "https://<the company API host above>";
13001
+ const apiUrlValue = `https://${docsApiHost(apiUrl)}`;
12921
13002
  return `## Codex cloud environment
12922
13003
 
12923
13004
  When the person asks you to set up the Codex cloud environment for this
@@ -12964,14 +13045,13 @@ Publishing is their click.
12964
13045
  `;
12965
13046
  }
12966
13047
  function claudeCloudNote(apiUrl) {
12967
- const host = companyApiHost(apiUrl);
12968
13048
  return `## Claude cloud
12969
13049
 
12970
13050
  Claude Code on the web has no environment setup chat. It reads this repository
12971
13051
  and \`CLAUDE.md\`, and \`.claude/settings.json\` runs \`scripts/cloud-setup.sh\`
12972
13052
  when a cloud session starts. Node 22 or 24 both work. If
12973
13053
  the environment has a network setting, allow the same domains as for Codex:
12974
- ${CLOUD_ALLOWED_HOSTS.map((h) => `\`${h}\``).join(", ")} and ${host ? `\`${host}\`` : "the company's Dashwise API host (`api.dashwise.net` for app.dashwise.net, `api.dashwise.dev` for app.dashwise.dev)"}. No secrets.
13054
+ ${CLOUD_ALLOWED_HOSTS.map((h) => `\`${h}\``).join(", ")} and \`${docsApiHost(apiUrl)}\`. No secrets.
12975
13055
  `;
12976
13056
  }
12977
13057
  function agentsMd(opts) {
@@ -13215,15 +13295,17 @@ remain human actions. No Dashwise token is needed for
13215
13295
  repository code delivery.
13216
13296
  `;
13217
13297
  }
13218
- var NODE_ENGINES, DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX, CLOUD_ALLOWED_HOSTS, HOSTED_API_HOSTS;
13298
+ var NODE_ENGINES, DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, GEIST_DEP_RANGE, MODULE_SLUG_REGEX, CLOUD_ALLOWED_HOSTS, HOSTED_API_HOSTS;
13219
13299
  var init_scaffold = __esm({
13220
13300
  "src/lib/scaffold.ts"() {
13301
+ init_config();
13221
13302
  init_sdk_version();
13222
13303
  init_simple();
13223
13304
  init_custom();
13224
13305
  NODE_ENGINES = "22.x || 24.x";
13225
13306
  DEPLOY_TARGETS = ["railway", "vercel", "fly"];
13226
13307
  SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
13308
+ GEIST_DEP_RANGE = "^1.7.2";
13227
13309
  MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
13228
13310
  CLOUD_ALLOWED_HOSTS = [
13229
13311
  "registry.npmjs.org",
@@ -14056,6 +14138,339 @@ var init_diff_check = __esm({
14056
14138
  }
14057
14139
  });
14058
14140
 
14141
+ // src/lib/permission-hint.ts
14142
+ function str(v) {
14143
+ return typeof v === "string" && v.length > 0 ? v : void 0;
14144
+ }
14145
+ function num(v) {
14146
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
14147
+ }
14148
+ function featureFromPermission(permission) {
14149
+ const m = permission ? /^workspace\.feature\.([a-z0-9_]+)\.use$/.exec(permission) : null;
14150
+ return m?.[1];
14151
+ }
14152
+ function featureSubject(ctx) {
14153
+ const feature = str(ctx.feature) ?? featureFromPermission(str(ctx.permission));
14154
+ if (feature && FEATURE_SUBJECTS[feature]) return FEATURE_SUBJECTS[feature];
14155
+ return str(ctx.feature_label) ?? (feature ? `"${feature}"` : void 0);
14156
+ }
14157
+ function needsAdmin(what, module, ask) {
14158
+ const scope = module ? ` for ${module}` : "";
14159
+ return `${what}${scope} needs a Dashwise admin. Ask your Dashwise admin to ${ask}.`;
14160
+ }
14161
+ function refusalHint(code2, context, subject = {}) {
14162
+ const ctx = context ?? {};
14163
+ const module = subject.module ?? str(ctx.module_slug);
14164
+ const installationId = subject.installationId ?? num(ctx.installation_id);
14165
+ const installLabel = installationId !== void 0 ? ` (install #${installationId})` : "";
14166
+ const named = module ?? "this module";
14167
+ const reason = str(ctx.reason);
14168
+ if (code2 === "MODULE_CAPABILITY_DENIED" && reason === "authoring_access_required") {
14169
+ return {
14170
+ message: "You need authoring access to export development data; ask someone who manages module access.",
14171
+ needsGrant: true
14172
+ };
14173
+ }
14174
+ if (code2 === "RAW_DATA_NOT_ALLOWED") {
14175
+ if (reason === "raw_data_write_required") {
14176
+ return {
14177
+ message: "You don't have access to change this data. Ask a data list Admin for Editor access in the Data Hub Permissions tab.",
14178
+ needsGrant: true
14179
+ };
14180
+ }
14181
+ if (reason === "installation_access_required") {
14182
+ return {
14183
+ message: "You cannot open this installation with your current access; ask someone who manages installation access.",
14184
+ needsGrant: true
14185
+ };
14186
+ }
14187
+ return {
14188
+ message: "You don't have access to view this data. Ask a data list Admin for Viewer access in the Data Hub Permissions tab.",
14189
+ needsGrant: true
14190
+ };
14191
+ }
14192
+ if (code2 === "VALIDATION_ERROR" && reason === "module_context_required") {
14193
+ return {
14194
+ message: "Open the intended module project and select the company and installation.",
14195
+ needsGrant: false
14196
+ };
14197
+ }
14198
+ if (code2 === "VALIDATION_ERROR" && reason === "context_mismatch") {
14199
+ return {
14200
+ message: "The selected company and installation do not match; check your selection.",
14201
+ needsGrant: false
14202
+ };
14203
+ }
14204
+ if (code2 === "UNAUTHENTICATED" && reason === "module_api_credential_missing") {
14205
+ return {
14206
+ message: "No module API key is configured; configure an existing key for the intended installation.",
14207
+ needsGrant: false
14208
+ };
14209
+ }
14210
+ switch (code2) {
14211
+ case "DRAFT_INSTALLATION_MISSING":
14212
+ case "DEV_INSTALLATION_MISSING":
14213
+ return {
14214
+ message: "This module has no dev installation; ask a module builder to create one.",
14215
+ needsGrant: false
14216
+ };
14217
+ case "MANIFEST_MANAGED_BY_REPOSITORY":
14218
+ return {
14219
+ message: "This module's column rules are managed in module.json. Change human_edit and human_edit_reason there, then build and release the module.",
14220
+ needsGrant: false
14221
+ };
14222
+ case "NOT_FOUND":
14223
+ case "MODULE_NOT_FOUND":
14224
+ return { message: "Not found; check your selection or ask for access.", needsGrant: false };
14225
+ case "UNAUTHORIZED":
14226
+ case "UNAUTHENTICATED":
14227
+ case "TOKEN_EXPIRED":
14228
+ return { message: "Run dashwise login, then retry.", needsGrant: false };
14229
+ case "FEATURE_DISABLED":
14230
+ case "PERMISSION_DENIED": {
14231
+ const what = featureSubject(ctx);
14232
+ if (!what) {
14233
+ return {
14234
+ message: `You don't have permission to do this${module ? ` for ${module}` : ""}. Ask your Dashwise admin for access.`,
14235
+ needsGrant: true
14236
+ };
14237
+ }
14238
+ return { message: needsAdmin(what, module, "enable it"), needsGrant: true };
14239
+ }
14240
+ case "RUNTIME_OPTION_OPERATOR_ONLY": {
14241
+ const option = str(ctx.option);
14242
+ const what = (option && OPTION_SUBJECTS[option]) ?? (option ? `"${option}"` : "This runtime option");
14243
+ return { message: needsAdmin(what, module, "change it"), needsGrant: true };
14244
+ }
14245
+ case REBUILD_BUMP_OPERATOR_ONLY: {
14246
+ const bump = str(ctx.bump) ?? "minor";
14247
+ return {
14248
+ message: needsAdmin(`A rebuild with a ${bump} version bump`, module, "run it"),
14249
+ needsGrant: true
14250
+ };
14251
+ }
14252
+ case "IMAGE_TAG_EXISTS":
14253
+ return {
14254
+ message: `This rebuild of ${named} needs a higher version bump than patch. A rebuild with a minor version bump needs a Dashwise admin. Ask your Dashwise admin to run it.`,
14255
+ needsGrant: true
14256
+ };
14257
+ case "RUNTIME_IMAGE_NOT_ELIGIBLE": {
14258
+ const version = str(ctx.version);
14259
+ const target = installationId !== void 0 ? ` --rollout installs:${installationId}` : "";
14260
+ return {
14261
+ message: `${named}${version ? ` v${version}` : ""} was built before streaming support. Rebuild it and upgrade this install first: dashwise module rebuild${module ? ` ${module}` : ""}${target}`,
14262
+ needsGrant: false
14263
+ };
14264
+ }
14265
+ case "RUNTIME_SWAP_PENDING": {
14266
+ const version = str(ctx.version);
14267
+ return {
14268
+ message: `${named}${installLabel} is still finishing its update${version ? ` to v${version}` : ""}, so streaming cannot be turned on yet. Try again in a few minutes.`,
14269
+ needsGrant: false
14270
+ };
14271
+ }
14272
+ case "RUNTIME_OPTION_UNAVAILABLE": {
14273
+ const option = str(ctx.option);
14274
+ const what = (option && OPTION_SUBJECTS[option]) ?? "This runtime option";
14275
+ return { message: `${what} is switched off platform-wide right now.`, needsGrant: false };
14276
+ }
14277
+ case "RUNTIME_CHANGE_IN_PROGRESS": {
14278
+ const op = num(ctx.operation_id);
14279
+ return {
14280
+ message: `A runtime change is already being applied to ${named}${op !== void 0 ? ` (operation #${op})` : ""}. Wait for it, then check: dashwise module runtime${module ? ` ${module}` : ""}`,
14281
+ needsGrant: false
14282
+ };
14283
+ }
14284
+ case "BUILD_IN_PROGRESS": {
14285
+ const buildId = num(ctx.build_id);
14286
+ const status = str(ctx.status);
14287
+ const which = buildId !== void 0 ? ` (#${buildId}${status ? `, ${status}` : ""})` : "";
14288
+ return {
14289
+ message: `A build of ${named} is already running${which}. Wait for it to finish, then run the rebuild again.`,
14290
+ needsGrant: false
14291
+ };
14292
+ }
14293
+ case "REBUILD_SOURCE_NOT_FOUND":
14294
+ return {
14295
+ message: `${named} has no build from source to rebuild. Publish it first: dashwise module publish`,
14296
+ needsGrant: false
14297
+ };
14298
+ case "INSTALLATION_NOT_FOUND":
14299
+ return {
14300
+ message: `${installationId !== void 0 ? `Installation #${installationId}` : "The installation"} was not found, or you cannot see it.`,
14301
+ needsGrant: false
14302
+ };
14303
+ case "PUBLISH_RATE_LIMITED":
14304
+ case "PUBLISH_USER_RATE_LIMITED":
14305
+ return {
14306
+ message: `Too many builds of ${named} in the last hour. Try again later.`,
14307
+ needsGrant: false
14308
+ };
14309
+ // Install invitations (`dashwise module invites`, Südbau retro 6).
14310
+ case "AUTHOR_ADMIN_REQUIRED":
14311
+ return {
14312
+ message: `Only an admin of the company that owns ${named} can manage its install invitations. Ask that company's admin.`,
14313
+ needsGrant: true
14314
+ };
14315
+ case "COMPANY_ADMIN_REQUIRED":
14316
+ return {
14317
+ message: "Only an admin of this company can see or decline its install invitations. Ask your company's admin.",
14318
+ needsGrant: true
14319
+ };
14320
+ case "MODULE_PRIVATE_SHARE_FIRST":
14321
+ return {
14322
+ message: `${named} is still private. Share it with your company in Dashwise first, then invite another company.`,
14323
+ needsGrant: false
14324
+ };
14325
+ default:
14326
+ if (MODULE_RIGHT_CODES.has(code2)) {
14327
+ return {
14328
+ message: `You can't manage ${named}${installLabel}. Ask your Dashwise admin for access.`,
14329
+ needsGrant: true
14330
+ };
14331
+ }
14332
+ return null;
14333
+ }
14334
+ }
14335
+ function reportCliError(message, opts) {
14336
+ if (opts.json) {
14337
+ out(
14338
+ JSON.stringify({
14339
+ error: {
14340
+ code: opts.code ?? "CLI_ERROR",
14341
+ message: message.replace(ANSI, ""),
14342
+ status: null,
14343
+ context: null
14344
+ },
14345
+ hint: null,
14346
+ needs_grant: false
14347
+ })
14348
+ );
14349
+ } else {
14350
+ fail(message);
14351
+ }
14352
+ return 1;
14353
+ }
14354
+ function reportFailedOutcome(failure, hint, opts) {
14355
+ if (opts.json) {
14356
+ out(
14357
+ JSON.stringify({
14358
+ error: {
14359
+ code: failure.code,
14360
+ message: failure.message ?? null,
14361
+ status: null,
14362
+ context: null
14363
+ },
14364
+ hint: hint?.message ?? null,
14365
+ needs_grant: hint?.needsGrant ?? false,
14366
+ ...opts.extra
14367
+ })
14368
+ );
14369
+ } else {
14370
+ fail(hint?.message ?? opts.fallback);
14371
+ }
14372
+ return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
14373
+ }
14374
+ function isTransientPollError(err) {
14375
+ if (!(err instanceof ApiError)) return true;
14376
+ return err.status === 0 || err.status === 429 || err.status >= 500;
14377
+ }
14378
+ function isTypedRequestError(err) {
14379
+ if (err instanceof ApiError) return true;
14380
+ if (!(err instanceof Error)) return false;
14381
+ const candidate = err;
14382
+ return typeof candidate.code === "string" && typeof candidate.status === "number" && (candidate.context === void 0 || candidate.context !== null && typeof candidate.context === "object" && !Array.isArray(candidate.context));
14383
+ }
14384
+ function reportRefusal(err, subject, opts) {
14385
+ const writeJson = (message) => {
14386
+ if (opts.stderr) process.stderr.write(`${message}
14387
+ `);
14388
+ else out(message);
14389
+ };
14390
+ if (!isTypedRequestError(err)) {
14391
+ const message = err?.message ?? String(err);
14392
+ if (opts.json) {
14393
+ writeJson(
14394
+ JSON.stringify({
14395
+ error: {
14396
+ code: "CLI_ERROR",
14397
+ message: message.replace(ANSI, ""),
14398
+ status: null,
14399
+ context: null
14400
+ },
14401
+ hint: null,
14402
+ needs_grant: false
14403
+ })
14404
+ );
14405
+ } else {
14406
+ fail(`${opts.action} failed: ${message}`);
14407
+ }
14408
+ return 1;
14409
+ }
14410
+ const hint = err.status === 401 && err.context?.reason !== "module_api_credential_missing" ? {
14411
+ message: opts.moduleKey ? MODULE_KEY_REFUSED[opts.moduleKey] : "Your login has expired. Run: dashwise login",
14412
+ needsGrant: false
14413
+ } : refusalHint(err.code, err.context, subject);
14414
+ if (opts.json) {
14415
+ writeJson(
14416
+ JSON.stringify({
14417
+ error: {
14418
+ code: err.code,
14419
+ message: err.message,
14420
+ status: err.status,
14421
+ context: err.context ?? null
14422
+ },
14423
+ hint: hint?.message ?? null,
14424
+ needs_grant: hint?.needsGrant ?? false
14425
+ })
14426
+ );
14427
+ } else if (hint) {
14428
+ fail(
14429
+ `${hint.message} (${err.code})${err.context ? ` context=${JSON.stringify(err.context)}` : ""}`
14430
+ );
14431
+ } else {
14432
+ fail(
14433
+ `${opts.action} failed (${err.code}): ${err.message}${err.context ? ` context=${JSON.stringify(err.context)}` : ""}`
14434
+ );
14435
+ }
14436
+ return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
14437
+ }
14438
+ var EXIT_NEEDS_GRANT, EXIT_TIMED_OUT, REBUILD_BUMP_OPERATOR_ONLY, FEATURE_SUBJECTS, OPTION_SUBJECTS, MODULE_RIGHT_CODES, ANSI, MODULE_KEY_REFUSED;
14439
+ var init_permission_hint = __esm({
14440
+ "src/lib/permission-hint.ts"() {
14441
+ init_api_client();
14442
+ init_output();
14443
+ EXIT_NEEDS_GRANT = 3;
14444
+ EXIT_TIMED_OUT = 4;
14445
+ REBUILD_BUMP_OPERATOR_ONLY = "REBUILD_BUMP_OPERATOR_ONLY";
14446
+ FEATURE_SUBJECTS = {
14447
+ module_runtime_streaming: "Streaming",
14448
+ module_rebuild: "Rebuild",
14449
+ module_building: "Building modules",
14450
+ module_runtime_sizing: "Memory size"
14451
+ };
14452
+ OPTION_SUBJECTS = {
14453
+ streaming: "Streaming",
14454
+ memory_mb: "Memory size",
14455
+ reapply: "Re-applying the runtime",
14456
+ timeout: "The timeout",
14457
+ reserved_concurrency: "Reserved concurrency",
14458
+ log_retention: "Log retention"
14459
+ };
14460
+ MODULE_RIGHT_CODES = /* @__PURE__ */ new Set([
14461
+ "MODULE_CAPABILITY_DENIED",
14462
+ "PUBLISH_REQUIRES_ADMIN",
14463
+ "NOT_COPY_OWNER"
14464
+ ]);
14465
+ ANSI = /\x1b\[[0-9;]*m/g;
14466
+ MODULE_KEY_REFUSED = {
14467
+ env: "The module key in DASHWISE_API_KEY was refused (expired or revoked). Replace it with a current key, or unset it to use this module's saved key. dashwise login does not renew module keys.",
14468
+ env_file: "The module key in .env.local (DASHWISE_API_KEY) was refused (expired or revoked). Replace it, or remove that line to use this module's saved key. dashwise login does not renew module keys.",
14469
+ saved: `This module's saved key was refused (expired or revoked). In your CLI auth.json, set only modules[<slug>].apiKey to an empty string (""), keeping installationId and all other fields. Then run dashwise dev from this module's directory with DASHWISE_API_KEY unset to mint and save a replacement key for the same installation. dashwise login does not renew module keys.`
14470
+ };
14471
+ }
14472
+ });
14473
+
14059
14474
  // src/lib/manifest-reference-lint.ts
14060
14475
  function referenceFieldWarnings(manifest) {
14061
14476
  const tables = manifest?.owns?.tables;
@@ -14394,7 +14809,7 @@ async function destroyDevInstallCommand(options) {
14394
14809
  return 0;
14395
14810
  }
14396
14811
  if (!options.yes && process.stdout.isTTY) {
14397
- const choice = await confirm({
14812
+ const choice = await confirm$1({
14398
14813
  message: `Destroy dev install ${code(`${slug} (id=${entry.installationId})`)}? Tables and rows will be soft-deleted (recoverable for 30 days via dashboard trash).`
14399
14814
  });
14400
14815
  if (isCancel(choice) || !choice) {
@@ -14532,48 +14947,58 @@ __export(push_exports, {
14532
14947
  schemaPushCommand: () => schemaPushCommand
14533
14948
  });
14534
14949
  async function schemaPushCommand(options) {
14950
+ const report = (err) => reportRefusal(err, {}, { ...options, stderr: true, action: "Schema push" });
14951
+ const contextError = (message) => report(
14952
+ new ApiError(
14953
+ 0,
14954
+ {
14955
+ code: "VALIDATION_ERROR",
14956
+ message,
14957
+ context: { reason: "module_context_required" }
14958
+ },
14959
+ message
14960
+ )
14961
+ );
14535
14962
  const projectRoot = resolve(options.dir ?? process.cwd());
14536
14963
  let manifest;
14537
14964
  try {
14538
14965
  manifest = readManifestOrThrow(projectRoot);
14539
14966
  } catch (err) {
14540
- fail(err.message);
14541
- return 1;
14967
+ return contextError(err.message);
14542
14968
  }
14543
14969
  const slug = manifest.module?.slug;
14544
14970
  if (!slug) {
14545
- fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
14546
- return 1;
14971
+ return contextError("module.json has no module.slug.");
14547
14972
  }
14548
14973
  const cfg = readConfig();
14549
14974
  if (!cfg) {
14550
- fail(`Not logged in. Run ${code("dashwise login")} first.`);
14551
- return 1;
14975
+ return report(
14976
+ new ApiError(401, { code: "UNAUTHENTICATED", message: "Not logged in." }, "Not logged in.")
14977
+ );
14552
14978
  }
14553
14979
  const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
14554
14980
  let moduleId;
14555
14981
  try {
14556
14982
  moduleId = (await getModuleBySlug(slug, auth)).id;
14557
14983
  } catch (err) {
14558
- if (err instanceof ApiError && err.status === 404) {
14559
- fail(
14560
- `Module ${code(slug)} isn't registered on the backend yet. Run ${code("dashwise init")} (or ${code("dashwise module register")}) first.`
14561
- );
14562
- return 1;
14563
- }
14564
- fail(`Could not resolve module ${code(slug)}: ${err.message}`);
14565
- return 1;
14984
+ return reportRefusal(
14985
+ err,
14986
+ { module: slug },
14987
+ { ...options, stderr: true, action: "Resolve module" }
14988
+ );
14566
14989
  }
14567
14990
  const entry = readModuleEntry(slug);
14568
14991
  const dwkAuth = entry?.apiKey ? { baseUrl: resolveApiUrl(), auth: { token: entry.apiKey } } : null;
14569
14992
  let remote = null;
14570
14993
  let previewUnavailableReason = null;
14994
+ let previewError = null;
14571
14995
  if (!dwkAuth) {
14572
- previewUnavailableReason = `no dev-install ${code("dwk_")} key is cached for ${code(slug)} in auth.json`;
14996
+ previewUnavailableReason = `no dev-install dwk_ key is cached for ${slug} in auth.json`;
14573
14997
  } else {
14574
14998
  try {
14575
14999
  remote = (await getInstallationSchema(dwkAuth)).manifest;
14576
15000
  } catch (err) {
15001
+ previewError = err;
14577
15002
  const detail = err instanceof ApiError ? `${err.code} ${err.status}: ${err.message}` : err.message;
14578
15003
  previewUnavailableReason = `reading the dev-install manifest failed (${detail}) \u2014 the cached key may be expired or revoked`;
14579
15004
  }
@@ -14613,15 +15038,32 @@ async function schemaPushCommand(options) {
14613
15038
  }
14614
15039
  }
14615
15040
  } else {
14616
- warn(
14617
- `Cannot preview what this push would change: ${previewUnavailableReason}.
15041
+ if (!options.json) {
15042
+ warn(
15043
+ `Cannot preview what this push would change: ${previewUnavailableReason}.
14618
15044
  Without the preview there is no way to know whether the push REMOVES tables/fields (live DDL on the dev install).`
14619
- );
15045
+ );
15046
+ }
14620
15047
  if (!options.yes) {
14621
- fail(
14622
- `Refusing to push without the destructive-change preview. Re-run with ${code("--yes")} to push anyway (the backend still validates the apply).`
15048
+ if (previewError instanceof ApiError && [401, 403, 404].includes(previewError.status)) {
15049
+ return reportRefusal(
15050
+ previewError,
15051
+ { module: slug },
15052
+ { ...options, stderr: true, action: "Schema preview", moduleKey: "saved" }
15053
+ );
15054
+ }
15055
+ const message = `Refusing to push without the destructive-change preview (${previewUnavailableReason}). Re-run with --yes to push anyway (the backend still validates the apply).`;
15056
+ return report(
15057
+ new ApiError(
15058
+ 0,
15059
+ {
15060
+ code: "PREVIEW_UNAVAILABLE",
15061
+ message,
15062
+ context: { reason: dwkAuth ? "preview_read_failed" : "module_key_missing" }
15063
+ },
15064
+ message
15065
+ )
14623
15066
  );
14624
- return 1;
14625
15067
  }
14626
15068
  info(
14627
15069
  dim(
@@ -14638,7 +15080,7 @@ async function schemaPushCommand(options) {
14638
15080
  auth
14639
15081
  );
14640
15082
  } catch (err) {
14641
- return surfaceSchemaPushError(err, slug);
15083
+ return surfaceSchemaPushError(err, slug, options.json);
14642
15084
  }
14643
15085
  printApplySummary(applied);
14644
15086
  for (const w of referenceFieldWarnings(manifest)) warn(w.message);
@@ -14715,21 +15157,23 @@ function strProp(v, key) {
14715
15157
  }
14716
15158
  return void 0;
14717
15159
  }
14718
- function surfaceSchemaPushError(err, slug) {
15160
+ function surfaceSchemaPushError(err, slug, json) {
15161
+ if (json || !(err instanceof ApiError) || refusalHint(err.code, err.context, { module: slug }) || err.status === 401) {
15162
+ return reportRefusal(err, { module: slug }, { json, stderr: true, action: "Schema push" });
15163
+ }
15164
+ process.stderr.write(
15165
+ `(${err.code})${err.context ? ` context=${JSON.stringify(err.context)}` : ""}
15166
+ `
15167
+ );
14719
15168
  if (err instanceof ApiError) {
14720
15169
  switch (err.code) {
14721
15170
  case "MANIFEST_INVALID":
14722
15171
  fail(`Manifest rejected by the backend validator: ${err.message}`);
14723
15172
  for (const issue of extractIssues2(err.context)) {
14724
- out(` ${dim("-")} ${issue}`);
15173
+ process.stderr.write(` ${dim("-")} ${issue}
15174
+ `);
14725
15175
  }
14726
15176
  return 1;
14727
- case "DRAFT_INSTALLATION_MISSING":
14728
- fail(
14729
- `Module ${code(slug)} has no dev (local/sandbox) install in its owner workspace \u2014 production installs have NO draft plane, so there is nothing to push onto.
14730
- This happens after ${code("dashwise module install --replace-dev-install")}. Schema changes now ship as published versions: edit module.json, ${code("dashwise module publish --bump <patch|minor|major>")}, then ${code("dashwise module install")} to upgrade.`
14731
- );
14732
- return 1;
14733
15177
  case "UNIQUE_INDEX_BACKFILL_CONFLICT":
14734
15178
  fail(
14735
15179
  `Cannot create a UNIQUE index \u2014 existing live rows already violate it: ${err.message}
@@ -14750,14 +15194,15 @@ function surfaceSchemaPushError(err, slug) {
14750
15194
  case "FORBIDDEN":
14751
15195
  case "NOT_WORKSPACE_MEMBER":
14752
15196
  fail(
14753
- `Not authorized: pushing a draft manifest requires membership of the module's owner workspace.`
15197
+ `Not authorized: pushing a draft manifest requires membership of the module's owner company.`
14754
15198
  );
14755
15199
  return 1;
14756
15200
  default:
14757
15201
  if (err.status === 400) {
14758
15202
  fail(`Manifest rejected: ${err.message}`);
14759
15203
  for (const issue of extractIssues2(err.context)) {
14760
- out(` ${dim("-")} ${issue}`);
15204
+ process.stderr.write(` ${dim("-")} ${issue}
15205
+ `);
14761
15206
  }
14762
15207
  return 1;
14763
15208
  }
@@ -14789,6 +15234,7 @@ var init_push = __esm({
14789
15234
  init_config();
14790
15235
  init_manifest_io();
14791
15236
  init_manifest_merge();
15237
+ init_permission_hint();
14792
15238
  init_output();
14793
15239
  init_manifest_reference_lint();
14794
15240
  }
@@ -15965,29 +16411,11 @@ async function runDoctorChecks(dir, deps) {
15965
16411
  return checks;
15966
16412
  }
15967
16413
  function targetCheck(apiUrl) {
15968
- const source = process.env.DASHWISE_API_URL?.trim() ? "from DASHWISE_API_URL" : "from your saved login";
15969
- if (sameOrigin(apiUrl, KNOWN_API_URLS.prod)) {
15970
- return {
15971
- id: "target",
15972
- label: "Target",
15973
- status: "warn",
15974
- detail: `PRODUCTION (${apiUrl}, ${source}). Commands here change real company data.`,
15975
- fix: `To build against dev, run ${code(`dashwise login --api-url ${KNOWN_API_URLS.dev}`)} or set ${code(`DASHWISE_API_URL=${KNOWN_API_URLS.dev}`)}.`
15976
- };
15977
- }
15978
- if (sameOrigin(apiUrl, KNOWN_API_URLS.dev)) {
15979
- return {
15980
- id: "target",
15981
- label: "Target",
15982
- status: "ok",
15983
- detail: `dev (${apiUrl}, ${source})`
15984
- };
15985
- }
15986
16414
  return {
15987
16415
  id: "target",
15988
16416
  label: "Target",
15989
16417
  status: "ok",
15990
- detail: `custom (${apiUrl}, ${source})`
16418
+ detail: apiUrl
15991
16419
  };
15992
16420
  }
15993
16421
  async function connectionOnly(apiUrl) {
@@ -16168,13 +16596,6 @@ function sameRepo(a, b) {
16168
16596
  const ka = repoKey(a);
16169
16597
  return ka !== null && ka === repoKey(b);
16170
16598
  }
16171
- function sameOrigin(a, b) {
16172
- try {
16173
- return new URL(a).origin === new URL(b).origin;
16174
- } catch {
16175
- return false;
16176
- }
16177
- }
16178
16599
  function safeReadConfig2() {
16179
16600
  try {
16180
16601
  return readConfig();
@@ -16335,7 +16756,7 @@ function startMcpProxy(opts) {
16335
16756
  const reconnectMs = opts.reconnectMs ?? 2e3;
16336
16757
  const verbose = opts.verbose === true;
16337
16758
  const readOnly = opts.readOnly === true;
16338
- let session = opts.session;
16759
+ let session2 = opts.session;
16339
16760
  let sessionId = null;
16340
16761
  let protocolVersion = null;
16341
16762
  let readTools = null;
@@ -16364,7 +16785,7 @@ function startMcpProxy(opts) {
16364
16785
  parsed = JSON.parse(text4);
16365
16786
  } catch {
16366
16787
  log2(
16367
- `Discarded a non-JSON response from ${redactUrl(session.connectUrl)} (${source}); stdout carries MCP frames only.`
16788
+ `Discarded a non-JSON response from ${redactUrl(session2.connectUrl)} (${source}); stdout carries MCP frames only.`
16368
16789
  );
16369
16790
  trace(`discarded ${Buffer.byteLength(text4, "utf8")} bytes of non-JSON`);
16370
16791
  return;
@@ -16374,7 +16795,7 @@ function startMcpProxy(opts) {
16374
16795
  ) : parsed !== null && typeof parsed === "object" && "jsonrpc" in parsed;
16375
16796
  if (!looksJsonRpc) {
16376
16797
  log2(
16377
- `Discarded a JSON response from ${redactUrl(session.connectUrl)} that is not JSON-RPC (${source}).`
16798
+ `Discarded a JSON response from ${redactUrl(session2.connectUrl)} that is not JSON-RPC (${source}).`
16378
16799
  );
16379
16800
  return;
16380
16801
  }
@@ -16405,7 +16826,7 @@ function startMcpProxy(opts) {
16405
16826
  reauthAttempt = (async () => {
16406
16827
  log2("The session key expired or was rotated \u2014 re-authenticating once.");
16407
16828
  try {
16408
- session = await reauth();
16829
+ session2 = await reauth();
16409
16830
  sessionId = null;
16410
16831
  return true;
16411
16832
  } catch (err) {
@@ -16446,7 +16867,7 @@ function startMcpProxy(opts) {
16446
16867
  };
16447
16868
  const post = async (text4, isInitialize) => {
16448
16869
  if (stopped) return null;
16449
- return doFetch(session.connectUrl, {
16870
+ return doFetch(session2.connectUrl, {
16450
16871
  method: "POST",
16451
16872
  headers: requestHeaders(isInitialize),
16452
16873
  body: text4
@@ -16474,7 +16895,7 @@ function startMcpProxy(opts) {
16474
16895
  res = await post(text4, isInitialize);
16475
16896
  } catch (err) {
16476
16897
  const message = err.message;
16477
- log2(`Transport error talking to ${redactUrl(session.connectUrl)}: ${message}`);
16898
+ log2(`Transport error talking to ${redactUrl(session2.connectUrl)}: ${message}`);
16478
16899
  emit(transportErrorFrame(frame?.id ?? null, `Transport error: ${message}`));
16479
16900
  return { kind: "failed" };
16480
16901
  }
@@ -16498,7 +16919,7 @@ function startMcpProxy(opts) {
16498
16919
  const bodyText = await res.text();
16499
16920
  if (!res.ok) {
16500
16921
  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}`);
16922
+ log2(`Refused by ${redactUrl(session2.connectUrl)}: ${detail}`);
16502
16923
  emit(transportErrorFrame(frame?.id ?? null, `Dashwise MCP server: ${detail}`));
16503
16924
  if (res.status === 401) {
16504
16925
  exitCode = 1;
@@ -16585,7 +17006,7 @@ function startMcpProxy(opts) {
16585
17006
  streamAbort = new AbortController();
16586
17007
  let res;
16587
17008
  try {
16588
- res = await doFetch(session.connectUrl, {
17009
+ res = await doFetch(session2.connectUrl, {
16589
17010
  method: "GET",
16590
17011
  headers: {
16591
17012
  Accept: "text/event-stream",
@@ -16621,7 +17042,7 @@ function startMcpProxy(opts) {
16621
17042
  }
16622
17043
  streamUnavailable = true;
16623
17044
  log2(
16624
- `Server stream refused by ${redactUrl(session.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
17045
+ `Server stream refused by ${redactUrl(session2.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
16625
17046
  );
16626
17047
  emit(
16627
17048
  transportErrorFrame(
@@ -16898,9 +17319,9 @@ async function mcpCommand(options = {}) {
16898
17319
  return 1;
16899
17320
  }
16900
17321
  const label = options.endpointName ?? defaultEndpointLabel();
16901
- let session;
17322
+ let session2;
16902
17323
  try {
16903
- session = await openCliSession({
17324
+ session2 = await openCliSession({
16904
17325
  workspaceId: workspace2.id,
16905
17326
  label,
16906
17327
  ...scopes !== void 0 ? { scopes } : {}
@@ -16909,24 +17330,24 @@ async function mcpCommand(options = {}) {
16909
17330
  fail(describeSessionError(err, apiUrl, scopes));
16910
17331
  return 1;
16911
17332
  }
16912
- if (scopes !== void 0 && !Array.isArray(session.scopes)) {
16913
- await revokeSession(session.endpointId);
17333
+ if (scopes !== void 0 && !Array.isArray(session2.scopes)) {
17334
+ await revokeSession(session2.endpointId);
16914
17335
  fail(SCOPES_NOT_APPLIED);
16915
17336
  return 1;
16916
17337
  }
16917
17338
  info(
16918
- `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session.endpointId} (${session.name}).`
17339
+ `Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session2.endpointId} (${session2.name}).`
16919
17340
  );
16920
- info(`Session key expires ${dim(session.expiresAt)}.`);
16921
- info(describeScopes(session.scopes));
17341
+ info(`Session key expires ${dim(session2.expiresAt)}.`);
17342
+ info(describeScopes(session2.scopes));
16922
17343
  if (options.readOnly) {
16923
17344
  warn(
16924
17345
  "Read-only: write tools are refused by this proxy (a client-side guard, not a server-enforced scope \u2014 for that, use --scope)."
16925
17346
  );
16926
17347
  }
16927
- let ownedEndpointId = session.endpointId;
17348
+ let ownedEndpointId = session2.endpointId;
16928
17349
  const proxy = startMcpProxy({
16929
- session,
17350
+ session: session2,
16930
17351
  reauth: async () => {
16931
17352
  const fresh = await openCliSession({
16932
17353
  workspaceId: workspace2.id,
@@ -17939,7 +18360,9 @@ function convertModule(opts) {
17939
18360
  "app/layout.jsx",
17940
18361
  "src/app/layout.tsx"
17941
18362
  ].find((f) => existsSync(join(dir, f)));
18363
+ let layoutCreated = false;
17942
18364
  if (!layoutFile) {
18365
+ layoutCreated = true;
17943
18366
  writeIfMissing("app/layout.tsx", appLayoutCustomTsx(sopts));
17944
18367
  } else if (!readFileSync(join(dir, layoutFile), "utf-8").includes("AuthProvider")) {
17945
18368
  warnings.push(
@@ -17955,10 +18378,13 @@ function convertModule(opts) {
17955
18378
  `${nextConfigFile}: add \`output: 'standalone'\` \u2014 required for the module runtime bundle.`
17956
18379
  );
17957
18380
  }
17958
- if (!pkg.dependencies?.["@dashai/sdk"]) {
17959
- pkg.dependencies = { ...pkg.dependencies ?? {}, "@dashai/sdk": SDK_DEP_RANGE };
18381
+ const addDeps = {};
18382
+ if (!pkg.dependencies?.["@dashai/sdk"]) addDeps["@dashai/sdk"] = SDK_DEP_RANGE;
18383
+ if (layoutCreated && !pkg.dependencies?.geist) addDeps.geist = GEIST_DEP_RANGE;
18384
+ if (Object.keys(addDeps).length > 0) {
18385
+ pkg.dependencies = { ...pkg.dependencies ?? {}, ...addDeps };
17960
18386
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
17961
- created.push("package.json (added @dashai/sdk)");
18387
+ created.push(`package.json (added ${Object.keys(addDeps).join(", ")})`);
17962
18388
  }
17963
18389
  const outboundApis = scanOutboundHosts(dir);
17964
18390
  const runtimeBlockAdded = normalizeManifest(join(dir, "module.json"), {
@@ -18782,13 +19208,13 @@ init_api_client();
18782
19208
  function redactedConsumer(redacted) {
18783
19209
  switch (redacted) {
18784
19210
  case "private_copy":
18785
- return "someone's private copy";
19211
+ return "someone's private installation";
18786
19212
  case "other_team_copy":
18787
- return "another team's copy";
19213
+ return "another team's installation";
18788
19214
  case "development_copy":
18789
- return "a development copy you cannot see";
19215
+ return "a development installation you cannot see";
18790
19216
  default:
18791
- return "a copy you cannot see";
19217
+ return "an installation you cannot see";
18792
19218
  }
18793
19219
  }
18794
19220
  var WIDEN_WORDS = {
@@ -18806,7 +19232,7 @@ var WIDEN_WORDS = {
18806
19232
  function describeWidening(widening) {
18807
19233
  return widening.map((w) => WIDEN_WORDS[w] ?? w).join(", ");
18808
19234
  }
18809
- async function getUpgradePreview(installationId, version, options = {}) {
19235
+ async function getUpgradePreview2(installationId, version, options = {}) {
18810
19236
  return apiRequest(
18811
19237
  "GET",
18812
19238
  `/api/installations/${installationId}/upgrade-preview?version=${encodeURIComponent(version)}`,
@@ -18873,6 +19299,9 @@ function selectInstallsByKind(installs, moduleId) {
18873
19299
  function isRealRuntimeUrl(url) {
18874
19300
  return !!url && /^https:\/\//i.test(url);
18875
19301
  }
19302
+ function isRuntimeProvisioned(row) {
19303
+ return row.runtime_https ?? isRealRuntimeUrl(row.runtime_url);
19304
+ }
18876
19305
  function isDraftVersion(v) {
18877
19306
  return !v || v.includes("draft");
18878
19307
  }
@@ -18916,6 +19345,71 @@ async function resolveWorkspaceWithFallback(opts) {
18916
19345
  // src/commands/module/install.ts
18917
19346
  init_output();
18918
19347
  init_connect_status();
19348
+
19349
+ // src/lib/upgrade-plan.ts
19350
+ init_api_client();
19351
+ init_output();
19352
+ function upgradePlanOf(err) {
19353
+ if (!(err instanceof ApiError) || err.code !== "UPGRADE_CONFIRM_REQUIRED") {
19354
+ return null;
19355
+ }
19356
+ const body = err.body;
19357
+ if (!body || typeof body.drift_hash !== "string") return null;
19358
+ return {
19359
+ ...body,
19360
+ drift_hash: body.drift_hash,
19361
+ breaks_consumers: Array.isArray(body.breaks_consumers) ? body.breaks_consumers : []
19362
+ };
19363
+ }
19364
+ var REDACTED_LABEL = {
19365
+ private_copy: "someone's private installation",
19366
+ other_team_copy: "another team's installation",
19367
+ development_copy: "a development installation"
19368
+ };
19369
+ function consumerLabel(c2) {
19370
+ if (c2.redacted) return REDACTED_LABEL[c2.redacted] ?? "an installation you cannot see";
19371
+ const name = c2.module_name ?? c2.module_slug ?? "a module";
19372
+ const slug = c2.module_slug && c2.module_name ? ` (${c2.module_slug})` : "";
19373
+ const id = c2.installation_id != null ? ` #${c2.installation_id}` : "";
19374
+ return `${name}${slug}${id}`;
19375
+ }
19376
+ var count2 = (n, noun) => n == null ? "" : `, ${n} ${noun}${n === 1 ? "" : "s"}`;
19377
+ function renderUpgradePlan(slug, plan) {
19378
+ const n = plan.breaks_consumers.length;
19379
+ const to = plan.to_version ? ` to ${code(`v${plan.to_version}`)}` : "";
19380
+ if (n === 0) {
19381
+ info(`Upgrading ${code(slug)}${to} no longer disconnects any module that reads from it.`);
19382
+ } else {
19383
+ warn(
19384
+ `Upgrading ${code(slug)}${to} breaks ${n} module${n === 1 ? "" : "s"} that read from it. ${n === 1 ? "It is" : "They are"} disconnected until updated:`
19385
+ );
19386
+ for (const c2 of plan.breaks_consumers) info(` - ${consumerLabel(c2)}`);
19387
+ }
19388
+ const d = plan.drift?.details;
19389
+ const lines = [
19390
+ ...(d?.tables_to_remove ?? []).map(
19391
+ (t) => `remove table ${code(t.name)}${count2(t.rows, "row")}`
19392
+ ),
19393
+ ...(d?.fields_to_remove ?? []).map(
19394
+ (f) => `remove field ${code(`${f.table}.${f.name}`)}${count2(f.values, "value")}`
19395
+ ),
19396
+ ...(d?.fields_to_retype ?? []).map(
19397
+ (f) => `change field ${code(`${f.table}.${f.name}`)} from ${f.from} to ${f.to}${count2(f.values, "value")}`
19398
+ )
19399
+ ];
19400
+ if (lines.length > 0) {
19401
+ info("It also changes this installation:");
19402
+ for (const line of lines) info(` - ${line}`);
19403
+ }
19404
+ }
19405
+ function planLosesData(plan) {
19406
+ const drift = plan.drift;
19407
+ if (!drift) return true;
19408
+ const details = drift.details;
19409
+ return (details?.tables_to_remove?.some((t) => t.rows !== 0) ?? (drift.tables_to_remove?.length ?? 0) > 0) || (details?.fields_to_remove?.some((f) => f.values !== 0) ?? (drift.fields_to_remove?.length ?? 0) > 0) || (details?.fields_to_retype?.some((f) => f.values !== 0) ?? (drift.fields_to_retype?.length ?? 0) > 0);
19410
+ }
19411
+
19412
+ // src/commands/module/install.ts
18919
19413
  async function moduleInstallCommand(slugArg, options) {
18920
19414
  const slug = resolveSlug(slugArg, options.dir);
18921
19415
  if (!slug) {
@@ -18973,14 +19467,27 @@ async function moduleInstallCommand(slugArg, options) {
18973
19467
  const installs = await listWorkspaceInstallations(workspace2.id, auth);
18974
19468
  const byKind = selectInstallsByKind(installs, mod.id);
18975
19469
  const production = byKind.all.filter((row2) => !isDevInstallKind(row2.kind));
18976
- if (production.length > 1) {
18977
- fail(
18978
- "Several production installations exist for this module in this company. Select the installation to update in the app. Nothing was changed."
18979
- );
18980
- return 1;
18981
- }
18982
- existing = production[0];
18983
19470
  devInstall = byKind.dev;
19471
+ if (options.installation !== void 0) {
19472
+ existing = production.find((row2) => row2.id === options.installation);
19473
+ if (!existing) {
19474
+ fail(
19475
+ `Installation #${options.installation} is not a production installation of ${code(mod.slug)} you can see in this company. Nothing was changed.`
19476
+ );
19477
+ listInstallationCandidates(production);
19478
+ return 1;
19479
+ }
19480
+ } else {
19481
+ const candidates = production.filter((row2) => row2.held !== true);
19482
+ if (candidates.length > 1 || candidates.length === 0 && production.length > 0) {
19483
+ fail(
19484
+ candidates.length > 1 ? `Several production installations of ${code(mod.slug)} exist in this company. Choose one with ${code("--installation <id>")}. Nothing was changed.` : `The only production installations of ${code(mod.slug)} here are held. To update one, choose it with ${code("--installation <id>")}. Nothing was changed.`
19485
+ );
19486
+ listInstallationCandidates(production);
19487
+ return 1;
19488
+ }
19489
+ existing = candidates[0];
19490
+ }
18984
19491
  } catch (err) {
18985
19492
  fail(
18986
19493
  `Could not read workspace installations: ${err.message}`
@@ -19027,7 +19534,7 @@ async function moduleInstallCommand(slugArg, options) {
19027
19534
  );
19028
19535
  } else {
19029
19536
  info(
19030
- `Existing production install #${existing.id}${installedVersion2 ? ` at ${code(`v${installedVersion2}`)}` : ""} \u2014 this will upgrade it to ${code(`v${targetVersion}`)}.`
19537
+ `Existing production install ${describeInstallation(existing)}${installedVersion2 ? ` at ${code(`v${installedVersion2}`)}` : ""} \u2014 this will upgrade it to ${code(`v${targetVersion}`)}.`
19031
19538
  );
19032
19539
  }
19033
19540
  }
@@ -19035,15 +19542,19 @@ async function moduleInstallCommand(slugArg, options) {
19035
19542
  let lossy = false;
19036
19543
  let lossAccepted = false;
19037
19544
  if (existing) {
19038
- const rerun = rerunLine(mod.slug, workspace2.slug, targetVersion, options, devInstallToReplace !== void 0);
19545
+ const rerun = rerunLine(mod.slug, workspace2.slug, targetVersion, existing.id, options, devInstallToReplace !== void 0);
19039
19546
  const previewed = await previewUpgrade(existing.id, targetVersion, options, auth, rerun, devInstallToReplace?.id);
19040
19547
  if (typeof previewed === "number") return previewed;
19041
19548
  preview = previewed.preview;
19042
19549
  lossy = previewed.lossy;
19043
19550
  lossAccepted = previewed.accepted;
19044
19551
  }
19552
+ if (options.force && (preview?.breaks_consumers?.length ?? 0) > 0 && !options.yes && (!process.stdout.isTTY || !process.stdin.isTTY || options.json)) {
19553
+ fail(`Nothing was changed. Re-run with ${code("--force --yes")} to confirm the listed disconnections, or run in a terminal to be asked.`);
19554
+ return 1;
19555
+ }
19045
19556
  const askForLoss = lossy && !lossAccepted;
19046
- if (askForLoss || !options.yes && process.stdout.isTTY && !confirmedViaPicker) {
19557
+ if (askForLoss || !options.yes && process.stdout.isTTY && process.stdin.isTTY && !confirmedViaPicker) {
19047
19558
  const lossClause = lossy ? " It moves the data listed above to the Trash or converts it." : "";
19048
19559
  const message = devInstallToReplace ? isUpgrade ? `Destroy the dev install (revokes local-dev credentials) and upgrade ${code(mod.slug)} to ${code(`v${targetVersion}`)} in ${code(workspace2.slug)}?${lossClause}` : `Replace the dev install (revokes local-dev credentials) and install ${code(mod.slug)} ${code(`v${targetVersion}`)} into ${code(workspace2.slug)} as production?` : `${isUpgrade ? "Upgrade" : "Install"} ${code(mod.slug)} ${code(`v${targetVersion}`)} into workspace ${code(workspace2.slug)}?${lossClause}`;
19049
19560
  const answer = await select({
@@ -19070,26 +19581,63 @@ async function moduleInstallCommand(slugArg, options) {
19070
19581
  }
19071
19582
  info("Provisioning the runtime \u2014 this can take a minute\u2026");
19072
19583
  let result;
19073
- try {
19074
- result = await installModule(
19075
- mod.id,
19076
- {
19077
- workspaceId: workspace2.id,
19078
- version: targetVersion,
19079
- ...existing ? { installation_id: existing.id } : {},
19080
- ...options.force !== void 0 ? { force: options.force } : {},
19081
- // P5-08: confirms exactly the delta that was shown (409 DRIFT_CHANGED otherwise).
19082
- ...preview ? { drift_hash: preview.drift_hash } : {}
19083
- },
19084
- auth
19085
- );
19086
- } catch (err) {
19087
- return surfaceInstallError(err, mod.slug, workspace2.slug, devInstallReplaced);
19584
+ let confirmedPlan;
19585
+ for (let round = 0; ; round++) {
19586
+ try {
19587
+ result = await installModule(
19588
+ mod.id,
19589
+ {
19590
+ workspaceId: workspace2.id,
19591
+ // L420(3): always the resolved version and, for an upgrade, the
19592
+ // announced installation; after a confirm, the plan's version.
19593
+ version: confirmedPlan?.to_version ?? targetVersion,
19594
+ ...existing ? { installation_id: existing.id } : {},
19595
+ ...options.force !== void 0 ? { force: options.force } : {},
19596
+ ...confirmedPlan ? { drift_hash: confirmedPlan.drift_hash } : preview ? { drift_hash: preview.drift_hash } : {}
19597
+ },
19598
+ auth
19599
+ );
19600
+ break;
19601
+ } catch (err) {
19602
+ let plan = null;
19603
+ if (confirmedPlan && err instanceof ApiError && err.code === "DRIFT_CHANGED" && round < MAX_PLAN_ROUNDS) {
19604
+ warn(
19605
+ "What this upgrade changes has changed since it was shown. Showing the new plan."
19606
+ );
19607
+ plan = await refetchUpgradePlan(confirmedPlan, auth);
19608
+ if (!plan) {
19609
+ return surfaceInstallError(err, mod.slug, workspace2.slug, devInstallReplaced);
19610
+ }
19611
+ } else {
19612
+ plan = options.force && !confirmedPlan ? upgradePlanOf(err) : null;
19613
+ if (!plan || round >= MAX_PLAN_ROUNDS) {
19614
+ return surfaceInstallError(err, mod.slug, workspace2.slug, devInstallReplaced);
19615
+ }
19616
+ }
19617
+ if (!existing || plan.installation_id !== existing.id || plan.to_version !== targetVersion || !plan.drift_hash) {
19618
+ fail("The upgrade plan does not match the announced installation and version. Nothing was changed; check the installation in the app.");
19619
+ if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19620
+ return 1;
19621
+ }
19622
+ const confirmed = await confirmUpgradePlan(
19623
+ mod.slug,
19624
+ plan,
19625
+ options,
19626
+ preview?.drift_hash,
19627
+ rerunLine(mod.slug, workspace2.slug, targetVersion, existing.id, options, devInstallToReplace !== void 0)
19628
+ );
19629
+ if (confirmed !== 0) {
19630
+ if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19631
+ return confirmed;
19632
+ }
19633
+ confirmedPlan = plan;
19634
+ }
19088
19635
  }
19089
19636
  if (existing && (isInstallGated(result) || result.installation.id !== existing.id)) {
19090
19637
  fail(
19091
19638
  `The backend did not confirm an in-place update of installation #${existing.id}. Check its state in the app before retrying; no fallback install will be attempted.`
19092
19639
  );
19640
+ if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19093
19641
  return 1;
19094
19642
  }
19095
19643
  if (isInstallGated(result)) {
@@ -19119,7 +19667,7 @@ async function moduleInstallCommand(slugArg, options) {
19119
19667
  }
19120
19668
  const publicUrl = row?.public_url ?? null;
19121
19669
  const installedVersion = row?.module_version?.version ?? targetVersion;
19122
- const runtimeProvisioned = isRealRuntimeUrl(row?.runtime_url);
19670
+ const runtimeProvisioned = row ? isRuntimeProvisioned(row) : false;
19123
19671
  const readiness = await fetchUsesReadiness(result.installation.id, auth);
19124
19672
  const setupPending = readiness?.setupPending ?? false;
19125
19673
  if (options.json) {
@@ -19132,8 +19680,8 @@ async function moduleInstallCommand(slugArg, options) {
19132
19680
  public_url: publicUrl,
19133
19681
  verified,
19134
19682
  // P5-08: the confirmed preview; `null` for a fresh install (nothing to preview).
19135
- drift_hash: preview?.drift_hash ?? null,
19136
- data_loss: preview ? lossy : null,
19683
+ drift_hash: confirmedPlan?.drift_hash ?? preview?.drift_hash ?? null,
19684
+ data_loss: confirmedPlan ? planLosesData(confirmedPlan) : preview ? lossy : null,
19137
19685
  // `null` when the binding surface was unreachable; `[]` when the
19138
19686
  // module declares no slots. The two are not the same thing.
19139
19687
  uses: readiness === null ? null : usesInstallJson(readiness),
@@ -19232,7 +19780,7 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
19232
19780
  info(explanation);
19233
19781
  return "replace";
19234
19782
  }
19235
- if (process.stdout.isTTY && !options.yes) {
19783
+ if (process.stdout.isTTY && process.stdin.isTTY && !options.yes) {
19236
19784
  info(explanation);
19237
19785
  const answer = await select({
19238
19786
  message: `How should ${code(slug)} be installed into ${code(workspaceSlug)}?`,
@@ -19293,6 +19841,62 @@ async function replaceDevInstall(existing, slug, auth) {
19293
19841
  }
19294
19842
  return 0;
19295
19843
  }
19844
+ var MAX_PLAN_ROUNDS = 3;
19845
+ async function refetchUpgradePlan(previous, auth) {
19846
+ if (previous.installation_id === void 0 || previous.to_version === void 0) {
19847
+ return null;
19848
+ }
19849
+ try {
19850
+ const preview = await getUpgradePreview(
19851
+ previous.installation_id,
19852
+ previous.to_version,
19853
+ auth
19854
+ );
19855
+ return typeof preview.drift_hash === "string" ? {
19856
+ ...preview,
19857
+ breaks_consumers: Array.isArray(preview.breaks_consumers) ? preview.breaks_consumers : []
19858
+ } : null;
19859
+ } catch {
19860
+ return null;
19861
+ }
19862
+ }
19863
+ async function confirmUpgradePlan(slug, plan, options, acceptedPreviewHash, rerun) {
19864
+ renderUpgradePlan(slug, plan);
19865
+ const losesData = planLosesData(plan);
19866
+ const lossAccepted = plan.drift_hash === acceptedPreviewHash || options.acceptDataLoss === plan.drift_hash;
19867
+ if (losesData && !lossAccepted && (agentSessionMarker() || !process.stdout.isTTY || !process.stdin.isTTY || options.json)) {
19868
+ fail("This upgrade loses data. --yes does not accept data loss. Nothing was changed.");
19869
+ info(`To accept exactly the loss listed above, a person re-runs:
19870
+ ${rerun(plan.drift_hash)}`);
19871
+ return 1;
19872
+ }
19873
+ if (options.yes && (!losesData || lossAccepted)) {
19874
+ info(`${code("--yes")}: confirming this plan.`);
19875
+ return 0;
19876
+ }
19877
+ if (!process.stdout.isTTY || !process.stdin.isTTY || options.json) {
19878
+ fail(
19879
+ `Nothing was changed. Re-run with ${code("--force --yes")} to confirm this plan, or run it in a terminal to be asked.`
19880
+ );
19881
+ return 1;
19882
+ }
19883
+ const answer = await select({
19884
+ message: losesData ? `Upgrade ${code(slug)}, lose the data listed above and disconnect the listed modules?` : plan.breaks_consumers.length === 0 ? `Upgrade ${code(slug)} with these changes?` : `Disconnect ${plan.breaks_consumers.length === 1 ? "this module" : "these modules"} and upgrade ${code(slug)}?`,
19885
+ options: [
19886
+ {
19887
+ value: "go",
19888
+ label: losesData ? "Yes, upgrade and lose the data listed above" : plan.breaks_consumers.length === 0 ? "Yes, upgrade" : "Yes, upgrade and disconnect them"
19889
+ },
19890
+ { value: "cancel", label: "No, abort" }
19891
+ ],
19892
+ initialValue: "cancel"
19893
+ });
19894
+ if (isCancel(answer) || answer === "cancel") {
19895
+ fail("Aborted. Nothing was changed.");
19896
+ return 130;
19897
+ }
19898
+ return 0;
19899
+ }
19296
19900
  function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
19297
19901
  if (err instanceof ApiError) {
19298
19902
  switch (err.code) {
@@ -19317,8 +19921,8 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
19317
19921
  break;
19318
19922
  case "UPGRADE_BREAKS_CONSUMERS":
19319
19923
  fail(
19320
- `Upgrading ${code(slug)} would break modules that consume its current schema.
19321
- Review the breaking changes, then re-run with ${code("--force")} to upgrade anyway.`
19924
+ `Upgrading ${code(slug)} would break modules that read from it.
19925
+ Re-run with ${code("--force")} to see which ones and confirm the upgrade.`
19322
19926
  );
19323
19927
  break;
19324
19928
  case "FORBIDDEN":
@@ -19364,10 +19968,26 @@ function usesInstallJson(readiness) {
19364
19968
  }))
19365
19969
  };
19366
19970
  }
19971
+ var AUDIENCE = {
19972
+ company: "company",
19973
+ teams: "Team Spaces",
19974
+ people: "named people"
19975
+ };
19976
+ function describeInstallation(row) {
19977
+ const details = [
19978
+ row.reach?.kind ? AUDIENCE[row.reach.kind] ?? row.reach.kind : null,
19979
+ row.held ? "held" : null,
19980
+ row.public_url
19981
+ ].filter((part) => Boolean(part));
19982
+ return `#${row.id}${row.name ? ` "${row.name}"` : ""}${details.length ? ` (${details.join(", ")})` : ""}`;
19983
+ }
19984
+ function listInstallationCandidates(rows) {
19985
+ for (const row of rows) info(` ${describeInstallation(row)}`);
19986
+ }
19367
19987
  async function previewUpgrade(installationId, targetVersion, options, auth, rerun, devInstallToReplaceId) {
19368
19988
  let preview;
19369
19989
  try {
19370
- preview = await getUpgradePreview(installationId, targetVersion, auth);
19990
+ preview = await getUpgradePreview2(installationId, targetVersion, auth);
19371
19991
  } catch (err) {
19372
19992
  const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
19373
19993
  const checkNote = options.acceptDataLoss !== void 0 ? `, so ${code("--accept-data-loss")} cannot be checked` : "";
@@ -19391,7 +20011,7 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19391
20011
  const broken = preview.breaks_consumers ?? [];
19392
20012
  if (broken.length > 0) {
19393
20013
  const names = broken.map(
19394
- (c2) => c2.module_slug ? `${c2.module_slug}${c2.installation_id ? ` #${c2.installation_id}` : ""}` : redactedConsumer(c2.redacted)
20014
+ (c2) => !c2.redacted && c2.module_slug ? `${c2.module_slug}${c2.installation_id ? ` #${c2.installation_id}` : ""}` : redactedConsumer(c2.redacted)
19395
20015
  );
19396
20016
  warn(` Breaks ${broken.length} module${broken.length === 1 ? "" : "s"} that read${broken.length === 1 ? "s" : ""} this one: ${names.join(", ")}`);
19397
20017
  }
@@ -19401,7 +20021,7 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19401
20021
  }
19402
20022
  if (options.acceptDataLoss !== void 0 && options.acceptDataLoss !== preview.drift_hash) {
19403
20023
  fail(
19404
- `${code("--accept-data-loss")} does not match this upgrade's preview: the copy or the release changed since that hash was printed. Nothing was changed.`
20024
+ `${code("--accept-data-loss")} does not match this upgrade's preview: the installation or the release changed since that hash was printed. Nothing was changed.`
19405
20025
  );
19406
20026
  info(`To accept the change listed above, a person re-runs:
19407
20027
  ${rerun(preview.drift_hash)}`);
@@ -19411,7 +20031,7 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19411
20031
  if (lossy && !accepted) {
19412
20032
  const yesNote = options.yes ? ` ${code("--yes")} does not accept data loss.` : "";
19413
20033
  const marker = agentSessionMarker();
19414
- const why = marker ? `this is an agent session (${code(marker)} is set): a person decides` : !process.stdout.isTTY ? "there is no terminal to ask in" : null;
20034
+ const why = marker ? `this is an agent session (${code(marker)} is set): a person decides` : !process.stdout.isTTY || !process.stdin.isTTY || options.json ? "there is no terminal to ask in" : null;
19415
20035
  if (why) {
19416
20036
  const alsoDev = devInstallToReplaceId !== void 0 ? ` and destroys dev install #${devInstallToReplaceId} first` : "";
19417
20037
  fail(`This upgrade loses the data listed above${alsoDev}, and ${why}.${yesNote} Refused. Nothing was changed.`);
@@ -19422,12 +20042,13 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19422
20042
  }
19423
20043
  return { preview, lossy, accepted };
19424
20044
  }
19425
- function rerunLine(slug, workspaceSlug, targetVersion, options, replacingDevInstall) {
20045
+ function rerunLine(slug, workspaceSlug, targetVersion, installationId, options, replacingDevInstall) {
19426
20046
  return (hash) => [
19427
20047
  "dashwise module install",
19428
20048
  slug,
19429
20049
  `--workspace ${workspaceSlug}`,
19430
20050
  `--version ${targetVersion}`,
20051
+ `--installation ${installationId}`,
19431
20052
  ...options.force ? ["--force"] : [],
19432
20053
  ...replacingDevInstall ? ["--replace-dev-install"] : [],
19433
20054
  `--accept-data-loss ${hash}`
@@ -20117,270 +20738,61 @@ init_api_client();
20117
20738
  init_config();
20118
20739
  init_manifest_io();
20119
20740
  init_output();
20120
-
20121
- // src/lib/permission-hint.ts
20122
- init_api_client();
20123
- init_output();
20124
- var EXIT_NEEDS_GRANT = 3;
20125
- var EXIT_TIMED_OUT = 4;
20126
- var REBUILD_BUMP_OPERATOR_ONLY = "REBUILD_BUMP_OPERATOR_ONLY";
20127
- var FEATURE_SUBJECTS = {
20128
- module_runtime_streaming: "Streaming",
20129
- module_rebuild: "Rebuild",
20130
- module_building: "Building modules",
20131
- module_runtime_sizing: "Memory size"
20132
- };
20133
- var OPTION_SUBJECTS = {
20134
- streaming: "Streaming",
20135
- memory_mb: "Memory size",
20136
- reapply: "Re-applying the runtime",
20137
- timeout: "The timeout",
20138
- reserved_concurrency: "Reserved concurrency",
20139
- log_retention: "Log retention"
20140
- };
20141
- var MODULE_RIGHT_CODES = /* @__PURE__ */ new Set([
20142
- "MODULE_CAPABILITY_DENIED",
20143
- "PUBLISH_REQUIRES_ADMIN",
20144
- "NOT_COPY_OWNER"
20145
- ]);
20146
- function str(v) {
20147
- return typeof v === "string" && v.length > 0 ? v : void 0;
20148
- }
20149
- function num(v) {
20150
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
20151
- }
20152
- function featureFromPermission(permission) {
20153
- const m = permission ? /^workspace\.feature\.([a-z0-9_]+)\.use$/.exec(permission) : null;
20154
- return m?.[1];
20155
- }
20156
- function featureSubject(ctx) {
20157
- const feature = str(ctx.feature) ?? featureFromPermission(str(ctx.permission));
20158
- if (feature && FEATURE_SUBJECTS[feature]) return FEATURE_SUBJECTS[feature];
20159
- return str(ctx.feature_label) ?? (feature ? `"${feature}"` : void 0);
20160
- }
20161
- function needsAdmin(what, module, ask) {
20162
- const scope = module ? ` for ${module}` : "";
20163
- return `${what}${scope} needs a Dashwise admin. Ask your Dashwise admin to ${ask}.`;
20741
+ init_permission_hint();
20742
+ var FULL_SHA = /^[0-9a-f]{40}$/i;
20743
+ var positiveId = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
20744
+ function isRepositoryBuild(value, moduleId, buildId) {
20745
+ if (!value || typeof value !== "object") return false;
20746
+ const build = value;
20747
+ return positiveId(build.id) && build.module_id === moduleId && (buildId === void 0 || build.id === buildId) && build.source === "repo" && typeof build.commit_sha === "string" && FULL_SHA.test(build.commit_sha) && ["queued", "building", "succeeded", "failed"].includes(build.status ?? "");
20164
20748
  }
20165
- function refusalHint(code2, context, subject = {}) {
20166
- const ctx = context ?? {};
20167
- const module = subject.module ?? str(ctx.module_slug);
20168
- const installationId = subject.installationId ?? num(ctx.installation_id);
20169
- const installLabel = installationId !== void 0 ? ` (install #${installationId})` : "";
20170
- const named = module ?? "this module";
20171
- switch (code2) {
20172
- case "FEATURE_DISABLED":
20173
- case "PERMISSION_DENIED": {
20174
- const what = featureSubject(ctx);
20175
- if (!what) {
20176
- return {
20177
- message: `You don't have permission to do this${module ? ` for ${module}` : ""}. Ask your Dashwise admin for access.`,
20178
- needsGrant: true
20179
- };
20180
- }
20181
- return { message: needsAdmin(what, module, "enable it"), needsGrant: true };
20749
+ async function moduleReleaseCommand(slugArg, options) {
20750
+ const json = options.json;
20751
+ const usage2 = (message) => reportCliError(message, { json, code: "USAGE" });
20752
+ let slug = slugArg;
20753
+ if (!slug) {
20754
+ try {
20755
+ const localSlug = readManifest(options.dir)?.module?.slug;
20756
+ if (typeof localSlug === "string" && localSlug.length > 0) slug = localSlug;
20757
+ } catch {
20182
20758
  }
20183
- case "RUNTIME_OPTION_OPERATOR_ONLY": {
20184
- const option = str(ctx.option);
20185
- const what = (option && OPTION_SUBJECTS[option]) ?? (option ? `"${option}"` : "This runtime option");
20186
- return { message: needsAdmin(what, module, "change it"), needsGrant: true };
20759
+ }
20760
+ if (!slug) return usage2("Pass a module slug or run from a directory with a valid module.json.");
20761
+ const resuming = options.build !== void 0;
20762
+ if (resuming && !positiveId(options.build)) return usage2("--build expects a positive build id.");
20763
+ if (resuming && (options.commitSha !== void 0 || options.bump !== void 0 || options.noWait)) {
20764
+ return usage2("--build only reads an existing release; do not combine it with --commit-sha, --bump or --no-wait.");
20765
+ }
20766
+ if (!resuming && (!options.commitSha || !FULL_SHA.test(options.commitSha))) {
20767
+ return usage2("--commit-sha requires the full 40-character SHA already pushed to the linked repository.");
20768
+ }
20769
+ const bump = options.bump ?? "minor";
20770
+ if (!["patch", "minor", "major"].includes(bump)) return usage2("--bump expects patch, minor or major.");
20771
+ const timeoutMs = (options.timeoutSec ?? 360) * 1e3;
20772
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return usage2("--timeout expects a positive number of seconds.");
20773
+ const cfg = readConfig();
20774
+ if (!isLoggedIn() || !cfg) return reportCliError("Not logged in. Run `dashwise login` first.", { json });
20775
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
20776
+ let moduleId;
20777
+ try {
20778
+ const mod = await getModuleBySlug(slug, auth);
20779
+ moduleId = mod.id;
20780
+ slug = mod.slug;
20781
+ } catch (err) {
20782
+ return reportRefusal(err, { module: slug }, { json, action: "Resolving the module" });
20783
+ }
20784
+ if (!resuming && !options.yes) {
20785
+ if (json || !process.stdin.isTTY || !process.stdout.isTTY) {
20786
+ return usage2("Release creates a version. Pass --yes to confirm in non-interactive or --json mode.");
20187
20787
  }
20188
- case REBUILD_BUMP_OPERATOR_ONLY: {
20189
- const bump = str(ctx.bump) ?? "minor";
20190
- return {
20191
- message: needsAdmin(`A rebuild with a ${bump} version bump`, module, "run it"),
20192
- needsGrant: true
20193
- };
20194
- }
20195
- case "IMAGE_TAG_EXISTS":
20196
- return {
20197
- message: `This rebuild of ${named} needs a higher version bump than patch. A rebuild with a minor version bump needs a Dashwise admin. Ask your Dashwise admin to run it.`,
20198
- needsGrant: true
20199
- };
20200
- case "RUNTIME_IMAGE_NOT_ELIGIBLE": {
20201
- const version = str(ctx.version);
20202
- const target = installationId !== void 0 ? ` --rollout installs:${installationId}` : "";
20203
- return {
20204
- message: `${named}${version ? ` v${version}` : ""} was built before streaming support. Rebuild it and upgrade this install first: dashwise module rebuild${module ? ` ${module}` : ""}${target}`,
20205
- needsGrant: false
20206
- };
20207
- }
20208
- case "RUNTIME_SWAP_PENDING": {
20209
- const version = str(ctx.version);
20210
- return {
20211
- message: `${named}${installLabel} is still finishing its update${version ? ` to v${version}` : ""}, so streaming cannot be turned on yet. Try again in a few minutes.`,
20212
- needsGrant: false
20213
- };
20214
- }
20215
- case "RUNTIME_OPTION_UNAVAILABLE": {
20216
- const option = str(ctx.option);
20217
- const what = (option && OPTION_SUBJECTS[option]) ?? "This runtime option";
20218
- return { message: `${what} is switched off platform-wide right now.`, needsGrant: false };
20219
- }
20220
- case "RUNTIME_CHANGE_IN_PROGRESS": {
20221
- const op = num(ctx.operation_id);
20222
- return {
20223
- message: `A runtime change is already being applied to ${named}${op !== void 0 ? ` (operation #${op})` : ""}. Wait for it, then check: dashwise module runtime${module ? ` ${module}` : ""}`,
20224
- needsGrant: false
20225
- };
20226
- }
20227
- case "BUILD_IN_PROGRESS": {
20228
- const buildId = num(ctx.build_id);
20229
- const status = str(ctx.status);
20230
- const which = buildId !== void 0 ? ` (#${buildId}${status ? `, ${status}` : ""})` : "";
20231
- return {
20232
- message: `A build of ${named} is already running${which}. Wait for it to finish, then run the rebuild again.`,
20233
- needsGrant: false
20234
- };
20235
- }
20236
- case "REBUILD_SOURCE_NOT_FOUND":
20237
- return {
20238
- message: `${named} has no build from source to rebuild. Publish it first: dashwise module publish`,
20239
- needsGrant: false
20240
- };
20241
- case "INSTALLATION_NOT_FOUND":
20242
- return {
20243
- message: `${installationId !== void 0 ? `Installation #${installationId}` : "The installation"} was not found, or you cannot see it.`,
20244
- needsGrant: false
20245
- };
20246
- case "PUBLISH_RATE_LIMITED":
20247
- case "PUBLISH_USER_RATE_LIMITED":
20248
- return {
20249
- message: `Too many builds of ${named} in the last hour. Try again later.`,
20250
- needsGrant: false
20251
- };
20252
- default:
20253
- if (MODULE_RIGHT_CODES.has(code2)) {
20254
- return {
20255
- message: `You can't manage ${named}${installLabel}. Ask your Dashwise admin for access.`,
20256
- needsGrant: true
20257
- };
20258
- }
20259
- return null;
20260
- }
20261
- }
20262
- var ANSI = /\x1b\[[0-9;]*m/g;
20263
- function reportCliError(message, opts) {
20264
- if (opts.json) {
20265
- out(
20266
- JSON.stringify({
20267
- error: { code: opts.code ?? "CLI_ERROR", message: message.replace(ANSI, ""), status: null, context: null },
20268
- hint: null,
20269
- needs_grant: false
20270
- })
20271
- );
20272
- } else {
20273
- fail(message);
20274
- }
20275
- return 1;
20276
- }
20277
- function reportFailedOutcome(failure, hint, opts) {
20278
- if (opts.json) {
20279
- out(
20280
- JSON.stringify({
20281
- error: { code: failure.code, message: failure.message ?? null, status: null, context: null },
20282
- hint: hint?.message ?? null,
20283
- needs_grant: hint?.needsGrant ?? false,
20284
- ...opts.extra
20285
- })
20286
- );
20287
- } else {
20288
- fail(hint?.message ?? opts.fallback);
20289
- }
20290
- return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
20291
- }
20292
- function isTransientPollError(err) {
20293
- if (!(err instanceof ApiError)) return true;
20294
- return err.status === 0 || err.status === 429 || err.status >= 500;
20295
- }
20296
- function reportRefusal(err, subject, opts) {
20297
- if (!(err instanceof ApiError)) {
20298
- const message = err?.message ?? String(err);
20299
- if (opts.json) {
20300
- out(
20301
- JSON.stringify({
20302
- error: { code: "CLI_ERROR", message: message.replace(ANSI, ""), status: null, context: null },
20303
- hint: null,
20304
- needs_grant: false
20305
- })
20306
- );
20307
- } else {
20308
- fail(`${opts.action} failed: ${message}`);
20309
- }
20310
- return 1;
20311
- }
20312
- const hint = err.status === 401 ? { message: "Your login has expired. Run: dashwise login", needsGrant: false } : refusalHint(err.code, err.context, subject);
20313
- if (opts.json) {
20314
- out(
20315
- JSON.stringify({
20316
- error: { code: err.code, message: err.message, status: err.status, context: err.context ?? null },
20317
- hint: hint?.message ?? null,
20318
- needs_grant: hint?.needsGrant ?? false
20319
- })
20320
- );
20321
- } else if (hint) {
20322
- fail(hint.message);
20323
- } else {
20324
- fail(`${opts.action} failed (${err.code}): ${err.message}`);
20325
- }
20326
- return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
20327
- }
20328
-
20329
- // src/commands/module/release.ts
20330
- var FULL_SHA = /^[0-9a-f]{40}$/i;
20331
- var positiveId = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
20332
- function isRepositoryBuild(value, moduleId, buildId) {
20333
- if (!value || typeof value !== "object") return false;
20334
- const build = value;
20335
- return positiveId(build.id) && build.module_id === moduleId && (buildId === void 0 || build.id === buildId) && build.source === "repo" && typeof build.commit_sha === "string" && FULL_SHA.test(build.commit_sha) && ["queued", "building", "succeeded", "failed"].includes(build.status ?? "");
20336
- }
20337
- async function moduleReleaseCommand(slugArg, options) {
20338
- const json = options.json;
20339
- const usage2 = (message) => reportCliError(message, { json, code: "USAGE" });
20340
- let slug = slugArg;
20341
- if (!slug) {
20342
- try {
20343
- const localSlug = readManifest(options.dir)?.module?.slug;
20344
- if (typeof localSlug === "string" && localSlug.length > 0) slug = localSlug;
20345
- } catch {
20346
- }
20347
- }
20348
- if (!slug) return usage2("Pass a module slug or run from a directory with a valid module.json.");
20349
- const resuming = options.build !== void 0;
20350
- if (resuming && !positiveId(options.build)) return usage2("--build expects a positive build id.");
20351
- if (resuming && (options.commitSha !== void 0 || options.bump !== void 0 || options.noWait)) {
20352
- return usage2("--build only reads an existing release; do not combine it with --commit-sha, --bump or --no-wait.");
20353
- }
20354
- if (!resuming && (!options.commitSha || !FULL_SHA.test(options.commitSha))) {
20355
- return usage2("--commit-sha requires the full 40-character SHA already pushed to the linked repository.");
20356
- }
20357
- const bump = options.bump ?? "minor";
20358
- if (!["patch", "minor", "major"].includes(bump)) return usage2("--bump expects patch, minor or major.");
20359
- const timeoutMs = (options.timeoutSec ?? 360) * 1e3;
20360
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return usage2("--timeout expects a positive number of seconds.");
20361
- const cfg = readConfig();
20362
- if (!isLoggedIn() || !cfg) return reportCliError("Not logged in. Run `dashwise login` first.", { json });
20363
- const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
20364
- let moduleId;
20365
- try {
20366
- const mod = await getModuleBySlug(slug, auth);
20367
- moduleId = mod.id;
20368
- slug = mod.slug;
20369
- } catch (err) {
20370
- return reportRefusal(err, { module: slug }, { json, action: "Resolving the module" });
20371
- }
20372
- if (!resuming && !options.yes) {
20373
- if (json || !process.stdin.isTTY || !process.stdout.isTTY) {
20374
- return usage2("Release creates a version. Pass --yes to confirm in non-interactive or --json mode.");
20375
- }
20376
- const answer = await select({
20377
- message: `Release ${slug} at ${options.commitSha} with a ${bump} bump?`,
20378
- options: [{ value: "cancel", label: "Cancel" }, { value: "release", label: "Release this revision" }],
20379
- initialValue: "cancel"
20380
- });
20381
- if (isCancel(answer) || answer !== "release") {
20382
- reportCliError("Aborted.", { json });
20383
- return 130;
20788
+ const answer = await select({
20789
+ message: `Release ${slug} at ${options.commitSha} with a ${bump} bump?`,
20790
+ options: [{ value: "cancel", label: "Cancel" }, { value: "release", label: "Release this revision" }],
20791
+ initialValue: "cancel"
20792
+ });
20793
+ if (isCancel(answer) || answer !== "release") {
20794
+ reportCliError("Aborted.", { json });
20795
+ return 130;
20384
20796
  }
20385
20797
  }
20386
20798
  let buildId = options.build;
@@ -20575,7 +20987,7 @@ async function moduleStatusCommand(slugArg, options) {
20575
20987
  );
20576
20988
  return 0;
20577
20989
  }
20578
- const runtimeProvisioned = isRealRuntimeUrl(row.runtime_url);
20990
+ const runtimeProvisioned = isRuntimeProvisioned(row);
20579
20991
  const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
20580
20992
  const installedVersion = row.module_version?.version ?? null;
20581
20993
  const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
@@ -20592,7 +21004,9 @@ async function moduleStatusCommand(slugArg, options) {
20592
21004
  latest_version: latestVersion,
20593
21005
  update_available: updateAvailable,
20594
21006
  runtime_provisioned: runtimeProvisioned,
20595
- runtime_url: row.runtime_url,
21007
+ // The raw runtime endpoint is not printed any more: it reaches the
21008
+ // module's server code past the platform's doors. `public_url` is
21009
+ // the address to open.
20596
21010
  public_url: row.public_url,
20597
21011
  state,
20598
21012
  // C-09: per-slot data-plane readiness for THIS install. `null`
@@ -20618,7 +21032,7 @@ async function moduleStatusCommand(slugArg, options) {
20618
21032
  );
20619
21033
  kv(
20620
21034
  "Runtime:",
20621
- runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
21035
+ runtimeProvisioned ? "provisioned" : "not provisioned"
20622
21036
  );
20623
21037
  kv("Public URL:", row.public_url ?? dim("(none)"));
20624
21038
  kv(
@@ -20658,6 +21072,133 @@ function usesJson(readiness) {
20658
21072
  };
20659
21073
  }
20660
21074
 
21075
+ // src/commands/module/installations.ts
21076
+ init_api_client();
21077
+ init_config();
21078
+ init_output();
21079
+ var AUDIENCE2 = {
21080
+ company: "company",
21081
+ teams: "Team Spaces",
21082
+ people: "named people"
21083
+ };
21084
+ var givesRole = (roleKey) => typeof roleKey === "string" && roleKey !== "none";
21085
+ function governanceAudience(row) {
21086
+ if (!row.rules) return null;
21087
+ if (givesRole(row.rules.company)) return AUDIENCE2.company;
21088
+ if (row.rules.teams.some((t) => t.live === true && givesRole(t.role_key))) return AUDIENCE2.teams;
21089
+ return AUDIENCE2.people;
21090
+ }
21091
+ function toListEntry(row) {
21092
+ return {
21093
+ id: row.id,
21094
+ module_slug: row.module?.slug ?? null,
21095
+ name: row.name ?? row.module?.name ?? null,
21096
+ audience: row.reach?.kind ? AUDIENCE2[row.reach.kind] ?? row.reach.kind : null,
21097
+ version: row.module_version?.version ?? null,
21098
+ held: row.held === true,
21099
+ development: isDevInstallKind(row.kind),
21100
+ public_url: row.public_url,
21101
+ admin_view_only: false
21102
+ };
21103
+ }
21104
+ async function moduleInstallationsCommand(slug, options) {
21105
+ if (!isLoggedIn()) {
21106
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
21107
+ return 1;
21108
+ }
21109
+ const cfg = readConfig();
21110
+ if (!cfg) {
21111
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
21112
+ return 1;
21113
+ }
21114
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
21115
+ let workspace2;
21116
+ try {
21117
+ workspace2 = await resolveWorkspaceWithFallback({
21118
+ userFlag: options.workspace,
21119
+ cachedWorkspaceId: slug ? readModuleEntry(slug)?.workspaceId : void 0,
21120
+ configDefault: cfg.workspaceSlug,
21121
+ apiUrl: resolveApiUrl(),
21122
+ apiToken: cfg.token
21123
+ });
21124
+ } catch (err) {
21125
+ fail(`Company selection failed: ${err.message}`);
21126
+ return 1;
21127
+ }
21128
+ let rows;
21129
+ try {
21130
+ rows = await listWorkspaceInstallations(workspace2.id, auth);
21131
+ } catch (err) {
21132
+ fail(`Could not read the installations in ${code(workspace2.slug)}: ${err.message}`);
21133
+ return 1;
21134
+ }
21135
+ const governance = [];
21136
+ try {
21137
+ const govRows = await apiRequest(
21138
+ "GET",
21139
+ `/api/modules/workspaces/${workspace2.id}/installations/governance`,
21140
+ void 0,
21141
+ auth
21142
+ );
21143
+ const seen = new Set(rows.map((row) => row.id));
21144
+ const slugOf = new Map(rows.map((row) => [row.module_id, row.module?.slug ?? null]));
21145
+ let slugModuleId;
21146
+ if (slug) {
21147
+ slugModuleId = rows.find((row) => row.module?.slug === slug)?.module_id;
21148
+ if (slugModuleId === void 0) {
21149
+ try {
21150
+ slugModuleId = (await getModuleBySlug(slug, auth)).id;
21151
+ } catch {
21152
+ }
21153
+ }
21154
+ }
21155
+ for (const g of govRows) {
21156
+ if (g.trashed || seen.has(g.installation_id)) continue;
21157
+ if (slug && g.module_id !== slugModuleId) continue;
21158
+ governance.push({
21159
+ id: g.installation_id,
21160
+ module_slug: slugOf.get(g.module_id) ?? (slug && g.module_id === slugModuleId ? slug : null),
21161
+ name: g.name || null,
21162
+ audience: governanceAudience(g),
21163
+ version: null,
21164
+ held: g.held === true,
21165
+ development: null,
21166
+ public_url: null,
21167
+ admin_view_only: true
21168
+ });
21169
+ }
21170
+ } catch (err) {
21171
+ if (!(err instanceof ApiError && (err.status === 403 || err.status === 404))) {
21172
+ fail(`Could not read the company's full installation list: ${err.message}`);
21173
+ return 1;
21174
+ }
21175
+ }
21176
+ 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);
21177
+ if (options.json) {
21178
+ out(JSON.stringify({ workspace: workspace2.slug, installations: entries }));
21179
+ return 0;
21180
+ }
21181
+ if (entries.length === 0) {
21182
+ info(
21183
+ slug ? `No installations of ${code(slug)} you can see in ${code(workspace2.slug)}.` : `No installations you can see in ${code(workspace2.slug)}.`
21184
+ );
21185
+ return 0;
21186
+ }
21187
+ for (const e of entries) {
21188
+ const tags = [
21189
+ e.audience,
21190
+ e.version ? `v${e.version}` : null,
21191
+ e.held ? "held" : null,
21192
+ e.development ? "development" : null,
21193
+ e.admin_view_only ? "admin view, not open to you" : null
21194
+ ].filter(Boolean);
21195
+ out(
21196
+ `#${e.id} ${e.module_slug ?? "?"} ${e.name ? `"${e.name}"` : ""} ${dim(tags.join(", "))}${e.public_url ? ` ${e.public_url}` : ""}`.replace(/ {3,}/g, " ")
21197
+ );
21198
+ }
21199
+ return 0;
21200
+ }
21201
+
20661
21202
  // src/commands/module/uninstall.ts
20662
21203
  init_api_client();
20663
21204
  init_config();
@@ -20828,29 +21369,301 @@ function surfaceUninstallError(err, slug, workspaceSlug) {
20828
21369
  return 1;
20829
21370
  }
20830
21371
 
21372
+ // src/commands/module/invites.ts
21373
+ init_api_client();
21374
+ init_config();
21375
+ init_output();
21376
+ init_permission_hint();
21377
+ var positiveId2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
21378
+ function session(json) {
21379
+ const cfg = readConfig();
21380
+ if (!isLoggedIn() || !cfg) return reportCliError(`Not logged in. Run ${code("dashwise login")} first.`, { json });
21381
+ return {
21382
+ auth: { baseUrl: resolveApiUrl(), auth: { token: cfg.token } },
21383
+ token: cfg.token,
21384
+ workspaceSlug: cfg.workspaceSlug
21385
+ };
21386
+ }
21387
+ async function resolveModule(slug, s, json) {
21388
+ try {
21389
+ const mod = await getModuleBySlug(slug, s.auth);
21390
+ return { id: mod.id, slug: mod.slug };
21391
+ } catch (err) {
21392
+ return reportRefusal(err, { module: slug }, { json, action: "Resolving the module" });
21393
+ }
21394
+ }
21395
+ async function confirm(question, confirmLabel, opts) {
21396
+ if (opts.yes) return null;
21397
+ if (opts.json || !process.stdin.isTTY || !process.stdout.isTTY) {
21398
+ return reportCliError("This changes an invitation. Pass --yes to confirm in non-interactive or --json mode.", {
21399
+ json: opts.json,
21400
+ code: "USAGE"
21401
+ });
21402
+ }
21403
+ const answer = await select({
21404
+ message: question,
21405
+ options: [
21406
+ { value: "cancel", label: "Cancel" },
21407
+ { value: "go", label: confirmLabel }
21408
+ ],
21409
+ initialValue: "cancel"
21410
+ });
21411
+ if (isCancel(answer) || answer !== "go") {
21412
+ reportCliError("Aborted.", {});
21413
+ return 130;
21414
+ }
21415
+ return null;
21416
+ }
21417
+ function companyLabel(row) {
21418
+ return row.company_name ? `${row.company_name} (company #${row.workspace_id})` : `company #${row.workspace_id}`;
21419
+ }
21420
+ async function moduleInvitesListCommand(slug, options) {
21421
+ const { json } = options;
21422
+ const s = session(json);
21423
+ if (typeof s === "number") return s;
21424
+ const mod = await resolveModule(slug, s, json);
21425
+ if (typeof mod === "number") return mod;
21426
+ let rows;
21427
+ try {
21428
+ rows = await apiRequest("GET", `/api/modules/${mod.id}/invites`, void 0, s.auth);
21429
+ } catch (err) {
21430
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Reading the invitations" });
21431
+ }
21432
+ if (json) {
21433
+ out(JSON.stringify({ module: mod.slug, invites: rows }));
21434
+ return 0;
21435
+ }
21436
+ if (rows.length === 0) {
21437
+ info(`No open invitations for ${code(mod.slug)}.`);
21438
+ return 0;
21439
+ }
21440
+ for (const row of rows) {
21441
+ const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
21442
+ out(`#${row.id} ${companyLabel(row)} ${dim(row.invited_at.slice(0, 10))}${note}`);
21443
+ }
21444
+ return 0;
21445
+ }
21446
+ async function moduleInvitesSendCommand(slug, options) {
21447
+ const { json } = options;
21448
+ const usage2 = (message) => reportCliError(message, { json, code: "USAGE" });
21449
+ if (!options.to) return usage2("Pass --to <company id> to name the company to invite.");
21450
+ const s = session(json);
21451
+ if (typeof s === "number") return s;
21452
+ let target;
21453
+ const numeric = /^[1-9][0-9]*$/.test(options.to) ? Number(options.to) : void 0;
21454
+ let mine;
21455
+ try {
21456
+ mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
21457
+ } catch (err) {
21458
+ return reportRefusal(err, {}, { json, action: "Reading your companies" });
21459
+ }
21460
+ const own = mine.find((w) => numeric !== void 0 ? w.id === numeric : w.slug === options.to);
21461
+ if (own) {
21462
+ target = { id: own.id, label: `${own.name} (company #${own.id})` };
21463
+ } else if (positiveId2(numeric)) {
21464
+ target = { id: numeric, label: `company #${numeric}` };
21465
+ } else {
21466
+ return usage2(
21467
+ `${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).`
21468
+ );
21469
+ }
21470
+ const mod = await resolveModule(slug, s, json);
21471
+ if (typeof mod === "number") return mod;
21472
+ const stop = await confirm(
21473
+ [
21474
+ `Invite ${target.label} to install ${mod.slug}?`,
21475
+ `- Admins of that company can then see ${mod.slug} and install it.`,
21476
+ "- Their data stays theirs; you do not see it.",
21477
+ `- Your module's logic and its updates reach their installations.`,
21478
+ "- You see their company name only after one of their admins installs it or asks to.",
21479
+ `- You can withdraw the invitation at any time with ${code(`dashwise module invites revoke ${mod.slug} <invitation id>`)}.`
21480
+ ].join("\n"),
21481
+ "Send the invitation",
21482
+ options
21483
+ );
21484
+ if (stop !== null) return stop;
21485
+ let invite;
21486
+ try {
21487
+ const body = await apiRequest(
21488
+ "POST",
21489
+ `/api/modules/${mod.id}/invites`,
21490
+ { workspaceId: target.id, ...options.note !== void 0 ? { note: options.note } : {} },
21491
+ s.auth
21492
+ );
21493
+ invite = body.invite;
21494
+ } catch (err) {
21495
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Sending the invitation" });
21496
+ }
21497
+ if (json) out(JSON.stringify({ module: mod.slug, invite }));
21498
+ else success(`Invited ${target.label} to install ${mod.slug} (invitation #${invite.id}).`);
21499
+ return 0;
21500
+ }
21501
+ async function moduleInvitesRevokeCommand(slug, inviteId, options) {
21502
+ const { json } = options;
21503
+ if (!positiveId2(inviteId)) {
21504
+ return reportCliError(`Pass the invitation id. List them with ${code(`dashwise module invites list ${slug}`)}.`, {
21505
+ json,
21506
+ code: "USAGE"
21507
+ });
21508
+ }
21509
+ const s = session(json);
21510
+ if (typeof s === "number") return s;
21511
+ const mod = await resolveModule(slug, s, json);
21512
+ if (typeof mod === "number") return mod;
21513
+ const stop = await confirm(
21514
+ [
21515
+ `Withdraw invitation #${inviteId} for ${mod.slug}?`,
21516
+ `- Installations made with it stay, but they no longer receive updates of ${mod.slug}.`,
21517
+ "- No new installation can start from it."
21518
+ ].join("\n"),
21519
+ "Withdraw the invitation",
21520
+ options
21521
+ );
21522
+ if (stop !== null) return stop;
21523
+ try {
21524
+ await apiRequest("DELETE", `/api/modules/${mod.id}/invites/${inviteId}`, void 0, s.auth);
21525
+ } catch (err) {
21526
+ return reportRefusal(err, { module: mod.slug }, { json, action: "Withdrawing the invitation" });
21527
+ }
21528
+ if (json) out(JSON.stringify({ module: mod.slug, revoked: inviteId }));
21529
+ else success(`Withdrew invitation #${inviteId} for ${mod.slug}.`);
21530
+ return 0;
21531
+ }
21532
+ async function resolveOwnCompany(s, flag, opts) {
21533
+ const { json } = opts;
21534
+ let mine;
21535
+ try {
21536
+ mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
21537
+ } catch (err) {
21538
+ return reportRefusal(err, {}, { json, action: "Reading your companies" });
21539
+ }
21540
+ const list = mine.map((w) => `${w.slug} (company #${w.id})`).join(", ");
21541
+ const wanted = flag ?? s.workspaceSlug;
21542
+ if (wanted !== void 0) {
21543
+ const found = mine.find((w) => w.slug === wanted || String(w.id) === wanted);
21544
+ if (found) return found;
21545
+ return reportCliError(`${wanted} is not one of your companies. Your companies: ${list}.`, { json, code: "USAGE" });
21546
+ }
21547
+ if (mine.length === 1) return mine[0];
21548
+ if (mine.length === 0) return reportCliError("Your account belongs to no company yet.", { json });
21549
+ if (!opts.interactive || json || !process.stdin.isTTY || !process.stdout.isTTY) {
21550
+ return reportCliError(`Choose the company with --workspace <slug or id>. Your companies: ${list}.`, {
21551
+ json,
21552
+ code: "USAGE"
21553
+ });
21554
+ }
21555
+ const choice = await select({
21556
+ message: "Which company?",
21557
+ options: mine.map((w) => ({ value: String(w.id), label: w.name, hint: w.slug }))
21558
+ });
21559
+ const picked = isCancel(choice) ? void 0 : mine.find((w) => String(w.id) === choice);
21560
+ if (!picked) {
21561
+ reportCliError("Aborted.", {});
21562
+ return 130;
21563
+ }
21564
+ return picked;
21565
+ }
21566
+ async function moduleInvitesReceivedCommand(options) {
21567
+ const { json } = options;
21568
+ const s = session(json);
21569
+ if (typeof s === "number") return s;
21570
+ const company = await resolveOwnCompany(s, options.workspace, { json, interactive: true });
21571
+ if (typeof company === "number") return company;
21572
+ let rows;
21573
+ try {
21574
+ rows = await apiRequest(
21575
+ "GET",
21576
+ `/api/modules/workspaces/${company.id}/invites`,
21577
+ void 0,
21578
+ s.auth
21579
+ );
21580
+ } catch (err) {
21581
+ return reportRefusal(err, {}, { json, action: "Reading the invitations" });
21582
+ }
21583
+ if (json) {
21584
+ out(JSON.stringify({ workspace: company.slug, invites: rows }));
21585
+ return 0;
21586
+ }
21587
+ if (rows.length === 0) {
21588
+ info(`No open invitations for ${code(company.slug)}.`);
21589
+ return 0;
21590
+ }
21591
+ for (const row of rows) {
21592
+ const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
21593
+ out(
21594
+ `#${row.id} ${row.module.slug} ${dim(`v${row.module.current_version}`)} from ${row.from_company.name} ${dim(row.invited_at.slice(0, 10))}${note}`
21595
+ );
21596
+ }
21597
+ const inCompany = `--workspace ${company.slug}`;
21598
+ info(
21599
+ `Install one with ${code(`dashwise module install <slug> ${inCompany}`)}, or decline it with ${code(`dashwise module invites decline <id> ${inCompany}`)}.`
21600
+ );
21601
+ return 0;
21602
+ }
21603
+ async function moduleInvitesDeclineCommand(inviteId, options) {
21604
+ const { json } = options;
21605
+ if (!positiveId2(inviteId)) {
21606
+ return reportCliError(`Pass the invitation id. List them with ${code("dashwise module invites received")}.`, {
21607
+ json,
21608
+ code: "USAGE"
21609
+ });
21610
+ }
21611
+ const s = session(json);
21612
+ if (typeof s === "number") return s;
21613
+ const company = await resolveOwnCompany(s, options.workspace, { json, interactive: !options.yes });
21614
+ if (typeof company === "number") return company;
21615
+ const stop = await confirm(
21616
+ `Decline invitation #${inviteId} for ${company.slug}? The module's company may invite you again.`,
21617
+ "Decline the invitation",
21618
+ options
21619
+ );
21620
+ if (stop !== null) return stop;
21621
+ try {
21622
+ await apiRequest(
21623
+ "POST",
21624
+ `/api/modules/workspaces/${company.id}/invites/${inviteId}/decline`,
21625
+ void 0,
21626
+ s.auth
21627
+ );
21628
+ } catch (err) {
21629
+ return reportRefusal(err, {}, { json, action: "Declining the invitation" });
21630
+ }
21631
+ if (json) out(JSON.stringify({ workspace: company.slug, declined: inviteId }));
21632
+ else success(`Declined invitation #${inviteId}.`);
21633
+ return 0;
21634
+ }
21635
+
20831
21636
  // src/commands/module/pull-data.ts
20832
21637
  init_api_client();
21638
+ init_permission_hint();
20833
21639
  init_output();
20834
21640
  async function modulePullDataCommand(options) {
21641
+ const report = (err) => reportRefusal(err, {}, { ...options, stderr: true, action: "Pull data" });
21642
+ const contextError = (message) => report(
21643
+ new ApiError(
21644
+ 0,
21645
+ {
21646
+ code: "VALIDATION_ERROR",
21647
+ message,
21648
+ context: { reason: "module_context_required" }
21649
+ },
21650
+ message
21651
+ )
21652
+ );
20835
21653
  const projectRoot = resolve(options.dir ?? process.cwd());
20836
21654
  const manifestPath2 = join(projectRoot, "module.json");
20837
21655
  if (!existsSync(manifestPath2)) {
20838
- fail(
20839
- `No ${code("module.json")} found in ${code(projectRoot)}. Run \`dashwise module init <slug>\` first.`
20840
- );
20841
- return 1;
21656
+ return contextError(`No module.json found in ${projectRoot}.`);
20842
21657
  }
20843
21658
  let manifest;
20844
21659
  try {
20845
21660
  manifest = JSON.parse(readFileSync(manifestPath2, "utf-8"));
20846
21661
  } catch (err) {
20847
- fail(`Could not parse ${code(manifestPath2)}: ${err.message}`);
20848
- return 1;
21662
+ return contextError(`Could not parse module.json: ${err.message}`);
20849
21663
  }
20850
21664
  const slug = manifest.module?.slug;
20851
21665
  if (!slug) {
20852
- fail(`${code("module.json")} is missing ${code("module.slug")}.`);
20853
- return 1;
21666
+ return contextError("module.json is missing module.slug.");
20854
21667
  }
20855
21668
  let mod;
20856
21669
  try {
@@ -20859,16 +21672,7 @@ async function modulePullDataCommand(options) {
20859
21672
  `/api/modules/by-slug/${encodeURIComponent(slug)}`
20860
21673
  );
20861
21674
  } catch (err) {
20862
- if (err instanceof ApiError && err.status === 404) {
20863
- fail(`Module ${code(slug)} not found on the backend.`);
20864
- return 1;
20865
- }
20866
- if (err instanceof ApiError && err.status === 401) {
20867
- fail("Not logged in. Run `dashwise login` first.");
20868
- return 1;
20869
- }
20870
- fail(`Could not resolve module: ${err.message}`);
20871
- return 1;
21675
+ return report(err);
20872
21676
  }
20873
21677
  const limit = options.limit ?? 100;
20874
21678
  let res;
@@ -20878,18 +21682,15 @@ async function modulePullDataCommand(options) {
20878
21682
  `/api/modules/${mod.id}/own-data?limit=${limit}`
20879
21683
  );
20880
21684
  } catch (err) {
20881
- if (err instanceof ApiError && err.status === 400) {
20882
- fail(`Invalid request: ${err.message}`);
20883
- return 1;
20884
- }
20885
- fail(`Could not pull data: ${err.message}`);
20886
- return 1;
21685
+ return report(err);
20887
21686
  }
20888
21687
  const outPath = options.out ? resolve(options.out) : join(projectRoot, "dashwise-data.json");
20889
21688
  writeFileSync(outPath, JSON.stringify(res, null, 2) + "\n", "utf-8");
20890
21689
  let totalRows = 0;
20891
21690
  for (const t of res.tables) totalRows += t.rows.length;
20892
- success(`Wrote ${totalRows} row${totalRows === 1 ? "" : "s"} across ${res.tables.length} table${res.tables.length === 1 ? "" : "s"} to ${code(outPath)}`);
21691
+ success(
21692
+ `Wrote ${totalRows} row${totalRows === 1 ? "" : "s"} across ${res.tables.length} table${res.tables.length === 1 ? "" : "s"} to ${code(outPath)}`
21693
+ );
20893
21694
  for (const t of res.tables) {
20894
21695
  if (t.total_rows > t.rows.length) {
20895
21696
  warn(
@@ -20898,7 +21699,7 @@ async function modulePullDataCommand(options) {
20898
21699
  }
20899
21700
  }
20900
21701
  if (res.tables.length === 0) {
20901
- info(dim("No owned tables on this module. (Did you publish a manifest with `owns.tables`?)"));
21702
+ info(dim("No tables available."));
20902
21703
  }
20903
21704
  return 0;
20904
21705
  }
@@ -23509,16 +24310,43 @@ init_config();
23509
24310
  init_dotenv_local();
23510
24311
  init_manifest_io();
23511
24312
  init_output();
24313
+ init_api_client();
24314
+ init_permission_hint();
23512
24315
  init_install_target();
23513
- init_dev();
23514
24316
  async function queryCommand(options) {
23515
24317
  const projectRoot = resolve(options.dir ?? process.cwd());
23516
24318
  let manifestSlug;
23517
24319
  try {
23518
24320
  manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
23519
24321
  } catch (err) {
23520
- fail(err.message);
23521
- return 1;
24322
+ return reportRefusal(
24323
+ new ApiError(
24324
+ 0,
24325
+ {
24326
+ code: "VALIDATION_ERROR",
24327
+ message: err.message,
24328
+ context: { reason: "module_context_required" }
24329
+ },
24330
+ "Module context required."
24331
+ ),
24332
+ {},
24333
+ { ...options, stderr: true, action: "Query" }
24334
+ );
24335
+ }
24336
+ if (!manifestSlug) {
24337
+ return reportRefusal(
24338
+ new ApiError(
24339
+ 0,
24340
+ {
24341
+ code: "VALIDATION_ERROR",
24342
+ message: "module.json is missing module.slug.",
24343
+ context: { reason: "module_context_required" }
24344
+ },
24345
+ "Module context required."
24346
+ ),
24347
+ {},
24348
+ { ...options, stderr: true, action: "Query" }
24349
+ );
23522
24350
  }
23523
24351
  const generatedPath = join(
23524
24352
  projectRoot,
@@ -23532,8 +24360,10 @@ async function queryCommand(options) {
23532
24360
  );
23533
24361
  return 1;
23534
24362
  }
23535
- const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
24363
+ const keyFromProcessEnv = Boolean(process.env.DASHWISE_API_KEY);
23536
24364
  hydrateEnvFromDotenvLocal(projectRoot);
24365
+ let moduleKey = keyFromProcessEnv ? "env" : process.env.DASHWISE_API_KEY ? "env_file" : "saved";
24366
+ let keySource = keyFromProcessEnv ? "environment" : ".env.local";
23537
24367
  const cfg = (() => {
23538
24368
  try {
23539
24369
  return readConfig();
@@ -23542,46 +24372,47 @@ async function queryCommand(options) {
23542
24372
  }
23543
24373
  })();
23544
24374
  const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
23545
- let keySource = keyInShell ? "environment" : ".env.local";
23546
- let entry = manifestSlug ? readModuleEntry(manifestSlug) : null;
23547
- if (!process.env.DASHWISE_API_KEY && manifestSlug) {
24375
+ const entry = readModuleEntry(manifestSlug);
24376
+ if (!process.env.DASHWISE_API_KEY) {
23548
24377
  if (entry && options.install !== void 0 && entry.installationId !== options.install) {
23549
24378
  fail(
23550
- `No key for install #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to install #${entry.installationId}. Nothing was run.`
24379
+ `No key for installation #${options.install}: the cached ${code("dwk_")} key of ${code(manifestSlug)} belongs to installation #${entry.installationId}. Nothing was run.`
23551
24380
  );
23552
24381
  return 1;
23553
24382
  }
23554
- if (cfg) {
23555
- const healed = await resolveModuleApiKey(
23556
- manifestSlug,
23557
- entry,
23558
- apiUrl,
23559
- cfg.token
23560
- );
23561
- if (healed) entry = healed;
23562
- }
23563
24383
  if (entry?.apiKey) {
23564
24384
  process.env.DASHWISE_API_KEY = entry.apiKey;
24385
+ moduleKey = "saved";
23565
24386
  keySource = "cached";
23566
24387
  if (!process.env.DASHWISE_API_URL) {
23567
24388
  process.env.DASHWISE_API_URL = apiUrl;
23568
24389
  }
23569
24390
  }
23570
24391
  }
23571
- if (process.env.DASHWISE_API_KEY) {
23572
- const target = await nameKeyTarget({
23573
- apiKey: process.env.DASHWISE_API_KEY,
23574
- keySource,
23575
- entry,
23576
- cfg,
23577
- apiUrl: process.env.DASHWISE_API_URL ?? apiUrl,
23578
- ...options.install !== void 0 ? { install: options.install } : {}
23579
- });
23580
- if (target !== 0) return target;
23581
- } else if (options.install !== void 0) {
23582
- fail(`No ${code("dwk_")} key to check against install #${options.install}. Set ${code("DASHWISE_API_KEY")}. Nothing was run.`);
23583
- return 1;
24392
+ if (!process.env.DASHWISE_API_KEY) {
24393
+ return reportRefusal(
24394
+ new ApiError(
24395
+ 0,
24396
+ {
24397
+ code: "UNAUTHENTICATED",
24398
+ context: { reason: "module_api_credential_missing" },
24399
+ message: "No module API key is configured."
24400
+ },
24401
+ "No module API key is configured."
24402
+ ),
24403
+ {},
24404
+ { ...options, stderr: true, action: "Query" }
24405
+ );
23584
24406
  }
24407
+ const target = await nameKeyTarget({
24408
+ apiKey: process.env.DASHWISE_API_KEY,
24409
+ keySource,
24410
+ entry,
24411
+ cfg,
24412
+ apiUrl,
24413
+ ...options.install !== void 0 ? { install: options.install } : {}
24414
+ });
24415
+ if (target !== 0) return target;
23585
24416
  const modes = [
23586
24417
  options.inline !== void 0 ? "inline" : null,
23587
24418
  options.file !== void 0 ? "file" : null,
@@ -23611,7 +24442,7 @@ async function queryCommand(options) {
23611
24442
  const stream = options.stream === true;
23612
24443
  if (options.repl) return runRepl(generatedPath);
23613
24444
  if (options.file !== void 0)
23614
- return runFile(options.file, projectRoot, options.json === true, explain, stream);
24445
+ return runFile(options.file, projectRoot, options.json === true, explain, stream, moduleKey);
23615
24446
  if (options.inline !== void 0)
23616
24447
  return runInline(
23617
24448
  options.inline,
@@ -23619,11 +24450,12 @@ async function queryCommand(options) {
23619
24450
  generatedPath,
23620
24451
  options.json === true,
23621
24452
  explain,
23622
- stream
24453
+ stream,
24454
+ moduleKey
23623
24455
  );
23624
24456
  return 1;
23625
24457
  }
23626
- async function runInline(expression, projectRoot, generatedPath, jsonCompact, explain, stream) {
24458
+ async function runInline(expression, projectRoot, generatedPath, jsonCompact, explain, stream, moduleKey) {
23627
24459
  let entryUrl;
23628
24460
  try {
23629
24461
  entryUrl = generatedEntryUrl(generatedPath);
@@ -23644,7 +24476,7 @@ async function runInline(expression, projectRoot, generatedPath, jsonCompact, ex
23644
24476
  ].join("\n");
23645
24477
  writeFileSync(tmpFile, source, "utf-8");
23646
24478
  try {
23647
- return await importAndPrint(tmpFile, projectRoot, jsonCompact, explain, stream);
24479
+ return await importAndPrint(tmpFile, projectRoot, jsonCompact, explain, stream, moduleKey);
23648
24480
  } finally {
23649
24481
  try {
23650
24482
  rmSync(tmpDir, { recursive: true, force: true });
@@ -23652,7 +24484,7 @@ async function runInline(expression, projectRoot, generatedPath, jsonCompact, ex
23652
24484
  }
23653
24485
  }
23654
24486
  }
23655
- async function runFile(file, projectRoot, jsonCompact, explain, stream) {
24487
+ async function runFile(file, projectRoot, jsonCompact, explain, stream, moduleKey) {
23656
24488
  const abs = resolve(projectRoot, file);
23657
24489
  if (!existsSync(abs)) {
23658
24490
  fail(`File not found: ${code(abs)}`);
@@ -23664,7 +24496,7 @@ async function runFile(file, projectRoot, jsonCompact, explain, stream) {
23664
24496
  );
23665
24497
  return 1;
23666
24498
  }
23667
- return importAndPrint(abs, projectRoot, jsonCompact, explain, stream);
24499
+ return importAndPrint(abs, projectRoot, jsonCompact, explain, stream, moduleKey);
23668
24500
  }
23669
24501
  async function runRepl(generatedPath) {
23670
24502
  let generated;
@@ -23703,7 +24535,7 @@ async function runRepl(generatedPath) {
23703
24535
  });
23704
24536
  });
23705
24537
  }
23706
- async function importAndPrint(file, projectRoot, jsonCompact, explain, stream) {
24538
+ async function importAndPrint(file, projectRoot, jsonCompact, explain, stream, moduleKey) {
23707
24539
  const originalCwd = process.cwd();
23708
24540
  try {
23709
24541
  process.chdir(projectRoot);
@@ -23739,11 +24571,11 @@ async function importAndPrint(file, projectRoot, jsonCompact, explain, stream) {
23739
24571
  }
23740
24572
  return 0;
23741
24573
  } catch (err) {
23742
- fail(`Query failed: ${err.message}`);
24574
+ const exit = reportRefusal(err, {}, { json: jsonCompact, stderr: true, action: "Query", moduleKey });
23743
24575
  if (process.env.DASHWISE_DEBUG) {
23744
24576
  console.error(err);
23745
24577
  }
23746
- return 1;
24578
+ return exit;
23747
24579
  } finally {
23748
24580
  process.chdir(originalCwd);
23749
24581
  }
@@ -24444,6 +25276,7 @@ init_api_client();
24444
25276
  init_config();
24445
25277
  init_manifest_io();
24446
25278
  init_output();
25279
+ init_permission_hint();
24447
25280
  var PLATFORM_DEFAULTS = ["manifest", "on", "off"];
24448
25281
  var BUMPS = ["patch", "minor", "major"];
24449
25282
  function parseRollout(raw) {
@@ -24667,6 +25500,7 @@ init_api_client();
24667
25500
  init_config();
24668
25501
  init_manifest_io();
24669
25502
  init_output();
25503
+ init_permission_hint();
24670
25504
  var TERMINAL_OPERATION = /* @__PURE__ */ new Set(["succeeded", "failed", "superseded"]);
24671
25505
  async function moduleRuntimeShowCommand(slugArg, options) {
24672
25506
  const auth = authOrFail(options.json);
@@ -24775,7 +25609,7 @@ async function moduleRuntimeSetCommand(slugArg, options) {
24775
25609
  const slug = before.module.slug;
24776
25610
  const subject = { module: slug, installationId: before.installation_id };
24777
25611
  if (streaming !== void 0 && !options.yes && !options.json && process.stdin.isTTY) {
24778
- const ok = await confirm({
25612
+ const ok = await confirm$1({
24779
25613
  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
25614
  });
24781
25615
  if (isCancel(ok) || ok !== true) {
@@ -24963,10 +25797,11 @@ function sleep5(ms) {
24963
25797
 
24964
25798
  // src/program.ts
24965
25799
  init_output();
25800
+ init_permission_hint();
24966
25801
  var program = new Command();
24967
25802
  program.name("dashwise").description("Dashwise CLI \u2014 log in, author modules, publish to the marketplace.").version(getVersion(), "-v, --version", "Show the CLI version + exit").option(
24968
25803
  "--api-url <url>",
24969
- `Override Dashwise backend URL (default: $DASHWISE_API_URL, else your saved login, else ${KNOWN_API_URLS.dev}). Prod is ${KNOWN_API_URLS.prod}; pass a localhost URL to target a backend you are running yourself.`
25804
+ `Override Dashwise backend URL (default: $DASHWISE_API_URL, else your saved login, else ${DEFAULT_API_URL}).`
24970
25805
  );
24971
25806
  program.hook("preAction", () => {
24972
25807
  const apiUrl = program.opts().apiUrl?.trim();
@@ -24985,7 +25820,7 @@ program.command("login").description("Sign in via the device-code flow.").option
24985
25820
  process.exitCode = exit;
24986
25821
  });
24987
25822
  program.command("doctor").description(
24988
- "Check your setup: which Dashwise you target (dev or prod), login, connection, Node, CLI and SDK versions, and inside a module folder module.json, the repository link and the cloud setup files. Each item says what to do. Exits 1 when something fails."
25823
+ "Check your setup: target URL, login, connection, Node, CLI and SDK versions, and inside a module folder module.json, the repository link and the cloud setup files. Each item says what to do. Exits 1 when something fails."
24989
25824
  ).option("--dir <path>", "Module folder to check (default: current directory).").option("--json", "Print one JSON object { ok, checks } instead of the report.").action(async (cmdOpts) => {
24990
25825
  process.exitCode = await doctorCommand(
24991
25826
  {
@@ -25288,8 +26123,9 @@ moduleCmd.command("diff").description(
25288
26123
  });
25289
26124
  }
25290
26125
  );
25291
- moduleCmd.command("pull-data").description("Export the module's own-table rows to a local JSON file (for fixtures, local dev seeding).").option("--dir <path>", "Project root (default: current directory).").option("--limit <n>", "Max rows per table (1-1000, default 100).", parseIntOption).option("--out <path>", "Output file path (default: ./dashwise-data.json).").action(async (cmdOpts) => {
26126
+ moduleCmd.command("pull-data").description("Export the module's own-table rows to a local JSON file (for fixtures, local dev seeding).").option("--dir <path>", "Project root (default: current directory).").option("--limit <n>", "Max rows per table (1-1000, default 100).", parseIntOption).option("--out <path>", "Output file path (default: ./dashwise-data.json).").option("--json", "Emit structured errors on stderr.").action(async (cmdOpts) => {
25292
26127
  process.exitCode = await modulePullDataCommand({
26128
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25293
26129
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25294
26130
  ...cmdOpts.limit !== void 0 ? { limit: cmdOpts.limit } : {},
25295
26131
  ...cmdOpts.out !== void 0 ? { out: cmdOpts.out } : {}
@@ -25655,8 +26491,15 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
25655
26491
  }
25656
26492
  );
25657
26493
  moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description(
25658
- "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently: an upgrade first prints what it does to the copy's data (tables and fields it moves to the Trash or converts, with their counts), asks once, and sends the preview's drift_hash. A move that loses data is accepted only by a person: the one question in a terminal, or --accept-data-loss <drift_hash> with the hash the preview printed. --yes never accepts data loss; with no terminal, or inside an AI agent session, a lossy move is refused and the refusal prints the re-run line. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 \"deployed, setup pending\" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it."
25659
- ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--version <semver>", "Install a specific published version (default: latest).").option("--force", "Proceed even when the upgrade breaks consumer modules (UPGRADE_BREAKS_CONSUMERS).").option(
26494
+ "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently: an upgrade first prints what it does to the installation's data (tables and fields it moves to the Trash or converts, with their counts), asks once, and sends the preview's drift_hash. A move that loses data is accepted only by a person: the one question in a terminal, or --accept-data-loss <drift_hash> with the hash the preview printed. --yes never accepts data loss; with no terminal, or inside an AI agent session, a lossy move is refused and the refusal prints the re-run line. When only a dev install exists it asks whether to install alongside it (--keep-dev-install) or replace it (--replace-dev-install), rather than silently landing on the dev row. Exits 3 \u2014 \"deployed, setup pending\" \u2014 when a required `uses` slot is still unbound: the deploy succeeded but the module throws on its first data call until an admin binds it."
26495
+ ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--version <semver>", "Install a specific published version (default: latest).").option(
26496
+ "--force",
26497
+ "Upgrade even when it breaks consumer modules (UPGRADE_BREAKS_CONSUMERS): shows which modules it disconnects and what it changes, asks to confirm (--yes confirms disconnections, never data loss), then upgrades exactly that plan."
26498
+ ).option(
26499
+ "--installation <id>",
26500
+ "Update exactly this production installation (needed when several exist, or to update a held one).",
26501
+ parsePositiveIdOption
26502
+ ).option(
25660
26503
  "--keep-dev-install",
25661
26504
  "Install production ALONGSIDE the workspace's kind=local dev install, leaving local dev working. Non-destructive. Backends >= 2026-07-15 keep a separate live-install slot per kind group, so the two coexist."
25662
26505
  ).option(
@@ -25668,6 +26511,7 @@ moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description
25668
26511
  ).option("--json", "Emit the final result as machine-readable JSON.").action(
25669
26512
  async (slug, cmdOpts) => {
25670
26513
  process.exitCode = await moduleInstallCommand(slug, {
26514
+ ...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {},
25671
26515
  ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
25672
26516
  ...cmdOpts.version !== void 0 ? { version: cmdOpts.version } : {},
25673
26517
  ...cmdOpts.force !== void 0 ? { force: cmdOpts.force } : {},
@@ -25695,6 +26539,63 @@ moduleCmd.command("uninstall [slug]").description(
25695
26539
  });
25696
26540
  }
25697
26541
  );
26542
+ var invitesCmd = moduleCmd.command("invites").description(
26543
+ "Install invitations: send, list and withdraw them for your module, or see and decline the ones your company received."
26544
+ );
26545
+ var inviteIdArg = (value) => value !== void 0 && /^[1-9][0-9]*$/.test(value) ? Number(value) : void 0;
26546
+ invitesCmd.command("list <slug>").description("List the open install invitations for your module.").option("--json", "Emit machine-readable JSON.").action(async (slug, cmdOpts) => {
26547
+ process.exitCode = await moduleInvitesListCommand(slug, {
26548
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26549
+ });
26550
+ });
26551
+ invitesCmd.command("send <slug>").description(
26552
+ "Invite another company to install your module. Needs admin rights in the module's company. Asks before sending; pass --yes in scripts."
26553
+ ).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(
26554
+ async (slug, cmdOpts) => {
26555
+ process.exitCode = await moduleInvitesSendCommand(slug, {
26556
+ ...cmdOpts.to !== void 0 ? { to: cmdOpts.to } : {},
26557
+ ...cmdOpts.note !== void 0 ? { note: cmdOpts.note } : {},
26558
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26559
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26560
+ });
26561
+ }
26562
+ );
26563
+ invitesCmd.command("revoke <slug> <invite-id>").description(
26564
+ "Withdraw an install invitation. Installations made before stay. Asks first; pass --yes in scripts."
26565
+ ).option("-y, --yes", "Withdraw without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(async (slug, inviteId, cmdOpts) => {
26566
+ process.exitCode = await moduleInvitesRevokeCommand(slug, inviteIdArg(inviteId), {
26567
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26568
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26569
+ });
26570
+ });
26571
+ 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) => {
26572
+ process.exitCode = await moduleInvitesReceivedCommand({
26573
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26574
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26575
+ });
26576
+ });
26577
+ invitesCmd.command("decline <invite-id>").description(
26578
+ "Decline an install invitation your company received (company admins). Asks first; pass --yes in scripts."
26579
+ ).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(
26580
+ async (inviteId, cmdOpts) => {
26581
+ process.exitCode = await moduleInvitesDeclineCommand(inviteIdArg(inviteId), {
26582
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26583
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
26584
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26585
+ });
26586
+ }
26587
+ );
26588
+ moduleCmd.command("installations [slug]").description(
26589
+ "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."
26590
+ ).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(
26591
+ async (slug, cmdOpts) => {
26592
+ process.exitCode = await moduleInstallationsCommand(slug, {
26593
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26594
+ ...cmdOpts.dev !== void 0 ? { dev: cmdOpts.dev } : {},
26595
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
26596
+ });
26597
+ }
26598
+ );
25698
26599
  moduleCmd.command("status [slug]").description(
25699
26600
  "Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
25700
26601
  ).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(
@@ -25889,9 +26790,10 @@ var schemaCmd = program.command("schema").description(
25889
26790
  );
25890
26791
  schemaCmd.command("push").description(
25891
26792
  "Apply the LOCAL module.json to the dev install via PUT /api/modules/:id/draft/manifest (greenfield or delta). Destructive removals (tables/fields) are previewed and require confirmation unless --yes. Afterwards the canonical manifest is synced back with merge semantics (local-only rbac/indexes blocks survive)."
25892
- ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option("-y, --yes", "Skip the destructive-change confirmation (CI / scripted runs).").action(async (cmdOpts) => {
26793
+ ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option("-y, --yes", "Skip the destructive-change confirmation (CI / scripted runs).").option("--json", "Emit structured errors on stderr.").action(async (cmdOpts) => {
25893
26794
  const { schemaPushCommand: schemaPushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
25894
26795
  process.exitCode = await schemaPushCommand2({
26796
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25895
26797
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25896
26798
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
25897
26799
  });
@@ -25972,7 +26874,13 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
25972
26874
  }
25973
26875
  );
25974
26876
  function getVersion() {
25975
- return "0.26.0";
26877
+ return "0.28.0";
26878
+ }
26879
+ function parsePositiveIdOption(value) {
26880
+ if (!/^[1-9][0-9]*$/.test(value.trim())) {
26881
+ throw new Error(`Expected an installation id (a positive whole number), got '${value}'`);
26882
+ }
26883
+ return Number(value.trim());
25976
26884
  }
25977
26885
  function parseIntOption(value) {
25978
26886
  const n = Number.parseInt(value, 10);