@dashai/cli 0.27.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -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_]*$/;
@@ -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\` /
@@ -12909,13 +12990,15 @@ function companyApiHost(apiUrl) {
12909
12990
  return null;
12910
12991
  }
12911
12992
  }
12993
+ function docsApiHost(apiUrl) {
12994
+ return companyApiHost(apiUrl) ?? new URL(DEFAULT_API_URL).host;
12995
+ }
12912
12996
  function apiHostLine(apiUrl) {
12913
- const host = companyApiHost(apiUrl);
12914
- 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})`;
12915
12999
  }
12916
13000
  function codexEnvironmentInstructions(apiUrl) {
12917
- const host = companyApiHost(apiUrl);
12918
- const apiUrlValue = host ? `https://${host}` : "https://<the company API host above>";
13001
+ const apiUrlValue = `https://${docsApiHost(apiUrl)}`;
12919
13002
  return `## Codex cloud environment
12920
13003
 
12921
13004
  When the person asks you to set up the Codex cloud environment for this
@@ -12962,14 +13045,13 @@ Publishing is their click.
12962
13045
  `;
12963
13046
  }
12964
13047
  function claudeCloudNote(apiUrl) {
12965
- const host = companyApiHost(apiUrl);
12966
13048
  return `## Claude cloud
12967
13049
 
12968
13050
  Claude Code on the web has no environment setup chat. It reads this repository
12969
13051
  and \`CLAUDE.md\`, and \`.claude/settings.json\` runs \`scripts/cloud-setup.sh\`
12970
13052
  when a cloud session starts. Node 22 or 24 both work. If
12971
13053
  the environment has a network setting, allow the same domains as for Codex:
12972
- ${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.
12973
13055
  `;
12974
13056
  }
12975
13057
  function agentsMd(opts) {
@@ -13213,15 +13295,17 @@ remain human actions. No Dashwise token is needed for
13213
13295
  repository code delivery.
13214
13296
  `;
13215
13297
  }
13216
- 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;
13217
13299
  var init_scaffold = __esm({
13218
13300
  "src/lib/scaffold.ts"() {
13301
+ init_config();
13219
13302
  init_sdk_version();
13220
13303
  init_simple();
13221
13304
  init_custom();
13222
13305
  NODE_ENGINES = "22.x || 24.x";
13223
13306
  DEPLOY_TARGETS = ["railway", "vercel", "fly"];
13224
13307
  SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
13308
+ GEIST_DEP_RANGE = "^1.7.2";
13225
13309
  MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
13226
13310
  CLOUD_ALLOWED_HOSTS = [
13227
13311
  "registry.npmjs.org",
@@ -14054,214 +14138,547 @@ var init_diff_check = __esm({
14054
14138
  }
14055
14139
  });
14056
14140
 
14057
- // src/lib/manifest-reference-lint.ts
14058
- function referenceFieldWarnings(manifest) {
14059
- const tables = manifest?.owns?.tables;
14060
- if (!Array.isArray(tables)) return [];
14061
- const warnings = [];
14062
- for (const table of tables) {
14063
- if (typeof table?.slug !== "string" || !Array.isArray(table.fields)) continue;
14064
- for (const field of table.fields) {
14065
- if (typeof field?.slug !== "string" || typeof field.type !== "string") continue;
14066
- if (field.slug === "id" || !REFERENCE_NAME.test(field.slug)) continue;
14067
- if (!PLAIN_TYPES.has(field.type)) continue;
14068
- warnings.push({
14069
- table: table.slug,
14070
- field: field.slug,
14071
- type: field.type,
14072
- message: `${table.slug}.${field.slug} is a ${field.type} field named like a reference. If it points at a row in Dashwise, use a link_row field: plain ids break silently when data is copied to another installation. Ignore this for ids from outside systems.`
14073
- });
14074
- }
14075
- }
14076
- return warnings;
14077
- }
14078
- var REFERENCE_NAME, PLAIN_TYPES;
14079
- var init_manifest_reference_lint = __esm({
14080
- "src/lib/manifest-reference-lint.ts"() {
14081
- REFERENCE_NAME = /(?:_id|_ref|Id|Ref)$/;
14082
- PLAIN_TYPES = /* @__PURE__ */ new Set(["number", "text", "autonumber"]);
14083
- }
14084
- });
14085
- function loadSchemaCommandContext(opts) {
14086
- const projectRoot = resolve(opts.dir ?? process.cwd());
14087
- const manifestPath2 = join(projectRoot, "module.json");
14088
- const manifest = readManifestOrThrow(projectRoot);
14089
- const manifestRawBytes = readFileSync(manifestPath2, "utf-8");
14090
- const slug = manifest.module?.slug;
14091
- if (!slug) {
14092
- throw new Error(
14093
- `module.json at ${manifestPath2} has no module.slug \u2014 the manifest is malformed.`
14094
- );
14095
- }
14096
- const cfg = readConfig();
14097
- if (!cfg) {
14098
- throw new Error("Not logged in. Run `dashwise login` first.");
14099
- }
14100
- const entry = readModuleEntry(slug);
14101
- if (!entry) {
14102
- throw new Error(
14103
- `No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
14104
- );
14105
- }
14106
- const token = resolveSchemaAuthToken(entry, slug);
14107
- return {
14108
- projectRoot,
14109
- manifestPath: manifestPath2,
14110
- manifest,
14111
- manifestRawBytes,
14112
- slug,
14113
- cfg,
14114
- entry,
14115
- requestOptions: {
14116
- baseUrl: resolveApiUrl(),
14117
- auth: { token }
14118
- }
14119
- };
14120
- }
14121
- function resolveSchemaAuthToken(entry, slug) {
14122
- if (entry.apiKey) return entry.apiKey;
14123
- throw new Error(
14124
- `The cached credential for "${slug}" is a legacy runtime token, which the schema endpoints no longer accept. Run \`dashwise dev\` once (it mints a dwk_ API key and updates your config), or \`dashwise init ${slug}\` to re-provision (existing files are preserved).`
14125
- );
14141
+ // src/lib/permission-hint.ts
14142
+ function str(v) {
14143
+ return typeof v === "string" && v.length > 0 ? v : void 0;
14126
14144
  }
14127
- var init_schema_command_context = __esm({
14128
- "src/lib/schema-command-context.ts"() {
14129
- init_config();
14130
- init_manifest_io();
14131
- }
14132
- });
14133
- function hydrateEnvFromDotenvLocal(projectRoot) {
14134
- const envFile = join(projectRoot, ".env.local");
14135
- if (!existsSync(envFile)) return;
14136
- const raw = readFileSync(envFile, "utf-8");
14137
- for (const line of raw.split(/\r?\n/)) {
14138
- const trimmed = line.trim();
14139
- if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
14140
- const eq = trimmed.indexOf("=");
14141
- if (eq <= 0) continue;
14142
- const key = trimmed.slice(0, eq).trim();
14143
- let value = trimmed.slice(eq + 1).trim();
14144
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
14145
- value = value.slice(1, -1);
14146
- }
14147
- if (process.env[key] === void 0) {
14148
- process.env[key] = value;
14149
- }
14150
- }
14145
+ function num(v) {
14146
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
14151
14147
  }
14152
- var init_dotenv_local = __esm({
14153
- "src/lib/dotenv-local.ts"() {
14154
- }
14155
- });
14156
-
14157
- // src/lib/install-target.ts
14158
- function keyDisplayPrefix(apiKey) {
14159
- if (!apiKey.startsWith(KEY_SCHEME)) return null;
14160
- const prefix = apiKey.slice(KEY_SCHEME.length, KEY_SCHEME.length + KEY_PREFIX_CHARS);
14161
- return prefix.length === KEY_PREFIX_CHARS ? prefix : null;
14148
+ function featureFromPermission(permission) {
14149
+ const m = permission ? /^workspace\.feature\.([a-z0-9_]+)\.use$/.exec(permission) : null;
14150
+ return m?.[1];
14162
14151
  }
14163
- async function keyBelongsToInstall(apiKey, installationId, options, now = /* @__PURE__ */ new Date()) {
14164
- const prefix = keyDisplayPrefix(apiKey);
14165
- if (prefix === null) return false;
14166
- const { api_keys } = await listApiKeys(installationId, options);
14167
- return api_keys.some(
14168
- (k) => k.installation_id === installationId && k.key_prefix === prefix && (k.status === void 0 || k.status === "active") && k.revoked_at === null && (k.expires_at === null || new Date(k.expires_at) > now)
14169
- );
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);
14170
14156
  }
14171
- function formatInstallTarget(id, detail) {
14172
- return detail ? `\u2192 install #${id} (${detail})` : `\u2192 install #${id}`;
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}.`;
14173
14160
  }
14174
- async function nameKeyTarget(args) {
14175
- const { apiKey, keySource, entry, cfg, apiUrl, install } = args;
14176
- const request = cfg ? { baseUrl: apiUrl, auth: { token: cfg.token }, signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) } : null;
14177
- if (install === void 0) {
14178
- if (keySource !== "cached" || !entry) {
14179
- info(
14180
- `\u2192 install not checked: ${code("DASHWISE_API_KEY")} comes from ${keySource === "environment" ? "the environment" : code(".env.local")}. Pass ${code("--install <id>")} to name its target and refuse a mismatch.`
14181
- );
14182
- return 0;
14183
- }
14184
- if (!request || entry.workspaceId === void 0) {
14185
- info(formatInstallTarget(entry.installationId, "the cached dev install"));
14186
- return 0;
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
+ };
14187
14180
  }
14188
- try {
14189
- const workspaces = await apiRequest("GET", "/api/workspaces", void 0, request);
14190
- const ws = workspaces.find((w) => w.id === entry.workspaceId);
14191
- info(formatInstallTarget(entry.installationId, `${ws ? ws.slug : `workspace #${entry.workspaceId}`}, local`));
14192
- } catch {
14193
- info(formatInstallTarget(entry.installationId));
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
+ };
14194
14186
  }
14195
- return 0;
14196
- }
14197
- const source = keySource === "cached" ? "the cached key" : `${code("DASHWISE_API_KEY")} (from ${keySource === "environment" ? "the environment" : code(".env.local")})`;
14198
- if (!request) {
14199
- fail(
14200
- `${code("--install")} looks the install up with your CLI login, and there is none. Run ${code("dashwise login")} first. Nothing was run.`
14201
- );
14202
- return 1;
14203
- }
14204
- let belongs;
14205
- try {
14206
- belongs = await keyBelongsToInstall(apiKey, install, request);
14207
- } catch (err) {
14208
- fail(
14209
- `Could not read the keys of install #${install} to confirm ${source} (${describe(err)}). Refusing to run against an unconfirmed install. Nothing was run.`
14210
- );
14211
- return 1;
14212
- }
14213
- const cachedDev = entry?.installationId === install ? "the cached dev install, " : "";
14214
- if (!belongs) {
14215
- const other = entry && entry.installationId !== install && entry.apiKey === apiKey ? ` It is the cached key of install #${entry.installationId}.` : "";
14216
- fail(`${source} is not a live key of install #${install}.${other} Nothing was run.`);
14217
- info(
14218
- `Use a key of install #${install} (mint one with ${code(`dashwise api-keys create <name> --install ${install}`)}), or drop ${code("--install")} to run against the key's own install.`
14219
- );
14220
- return 1;
14221
- }
14222
- info(formatInstallTarget(install, `${cachedDev}key checked`));
14223
- return 0;
14224
- }
14225
- function describe(err) {
14226
- if (err instanceof ApiError) return `${err.code || err.status} ${err.message}`;
14227
- return err.message;
14228
- }
14229
- var KEY_PREFIX_CHARS, KEY_SCHEME, LOOKUP_TIMEOUT_MS;
14230
- var init_install_target = __esm({
14231
- "src/lib/install-target.ts"() {
14232
- init_api_client();
14233
- init_output();
14234
- KEY_PREFIX_CHARS = 8;
14235
- KEY_SCHEME = "dwk_";
14236
- LOOKUP_TIMEOUT_MS = 1500;
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
+ };
14237
14191
  }
14238
- });
14239
-
14240
- // src/commands/run.ts
14241
- var run_exports = {};
14242
- __export(run_exports, {
14243
- runCommand: () => runCommand
14244
- });
14245
- async function runCommand(cmdArgs, options) {
14246
- if (!cmdArgs || cmdArgs.length === 0) {
14247
- fail(
14248
- `Usage: ${code("dashwise run -- <command\u2026>")} (e.g. ${code("dashwise run -- node seed.mjs")})`
14249
- );
14250
- return 1;
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
+ };
14251
14197
  }
14252
- if (options.install !== void 0 && (!Number.isInteger(options.install) || options.install <= 0)) {
14253
- fail(`--install expects a positive integer installation id (got "${options.install}").`);
14254
- return 1;
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
+ };
14255
14203
  }
14256
- const projectRoot = resolve(options.dir ?? process.cwd());
14257
- let manifestSlug;
14258
- try {
14259
- manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
14260
- } catch (err) {
14261
- fail(err.message);
14262
- return 1;
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
+ };
14263
14209
  }
14264
- if (!manifestSlug) {
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
+
14474
+ // src/lib/manifest-reference-lint.ts
14475
+ function referenceFieldWarnings(manifest) {
14476
+ const tables = manifest?.owns?.tables;
14477
+ if (!Array.isArray(tables)) return [];
14478
+ const warnings = [];
14479
+ for (const table of tables) {
14480
+ if (typeof table?.slug !== "string" || !Array.isArray(table.fields)) continue;
14481
+ for (const field of table.fields) {
14482
+ if (typeof field?.slug !== "string" || typeof field.type !== "string") continue;
14483
+ if (field.slug === "id" || !REFERENCE_NAME.test(field.slug)) continue;
14484
+ if (!PLAIN_TYPES.has(field.type)) continue;
14485
+ warnings.push({
14486
+ table: table.slug,
14487
+ field: field.slug,
14488
+ type: field.type,
14489
+ message: `${table.slug}.${field.slug} is a ${field.type} field named like a reference. If it points at a row in Dashwise, use a link_row field: plain ids break silently when data is copied to another installation. Ignore this for ids from outside systems.`
14490
+ });
14491
+ }
14492
+ }
14493
+ return warnings;
14494
+ }
14495
+ var REFERENCE_NAME, PLAIN_TYPES;
14496
+ var init_manifest_reference_lint = __esm({
14497
+ "src/lib/manifest-reference-lint.ts"() {
14498
+ REFERENCE_NAME = /(?:_id|_ref|Id|Ref)$/;
14499
+ PLAIN_TYPES = /* @__PURE__ */ new Set(["number", "text", "autonumber"]);
14500
+ }
14501
+ });
14502
+ function loadSchemaCommandContext(opts) {
14503
+ const projectRoot = resolve(opts.dir ?? process.cwd());
14504
+ const manifestPath2 = join(projectRoot, "module.json");
14505
+ const manifest = readManifestOrThrow(projectRoot);
14506
+ const manifestRawBytes = readFileSync(manifestPath2, "utf-8");
14507
+ const slug = manifest.module?.slug;
14508
+ if (!slug) {
14509
+ throw new Error(
14510
+ `module.json at ${manifestPath2} has no module.slug \u2014 the manifest is malformed.`
14511
+ );
14512
+ }
14513
+ const cfg = readConfig();
14514
+ if (!cfg) {
14515
+ throw new Error("Not logged in. Run `dashwise login` first.");
14516
+ }
14517
+ const entry = readModuleEntry(slug);
14518
+ if (!entry) {
14519
+ throw new Error(
14520
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug}\` to provision one (existing files are preserved).`
14521
+ );
14522
+ }
14523
+ const token = resolveSchemaAuthToken(entry, slug);
14524
+ return {
14525
+ projectRoot,
14526
+ manifestPath: manifestPath2,
14527
+ manifest,
14528
+ manifestRawBytes,
14529
+ slug,
14530
+ cfg,
14531
+ entry,
14532
+ requestOptions: {
14533
+ baseUrl: resolveApiUrl(),
14534
+ auth: { token }
14535
+ }
14536
+ };
14537
+ }
14538
+ function resolveSchemaAuthToken(entry, slug) {
14539
+ if (entry.apiKey) return entry.apiKey;
14540
+ throw new Error(
14541
+ `The cached credential for "${slug}" is a legacy runtime token, which the schema endpoints no longer accept. Run \`dashwise dev\` once (it mints a dwk_ API key and updates your config), or \`dashwise init ${slug}\` to re-provision (existing files are preserved).`
14542
+ );
14543
+ }
14544
+ var init_schema_command_context = __esm({
14545
+ "src/lib/schema-command-context.ts"() {
14546
+ init_config();
14547
+ init_manifest_io();
14548
+ }
14549
+ });
14550
+ function hydrateEnvFromDotenvLocal(projectRoot) {
14551
+ const envFile = join(projectRoot, ".env.local");
14552
+ if (!existsSync(envFile)) return;
14553
+ const raw = readFileSync(envFile, "utf-8");
14554
+ for (const line of raw.split(/\r?\n/)) {
14555
+ const trimmed = line.trim();
14556
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
14557
+ const eq = trimmed.indexOf("=");
14558
+ if (eq <= 0) continue;
14559
+ const key = trimmed.slice(0, eq).trim();
14560
+ let value = trimmed.slice(eq + 1).trim();
14561
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
14562
+ value = value.slice(1, -1);
14563
+ }
14564
+ if (process.env[key] === void 0) {
14565
+ process.env[key] = value;
14566
+ }
14567
+ }
14568
+ }
14569
+ var init_dotenv_local = __esm({
14570
+ "src/lib/dotenv-local.ts"() {
14571
+ }
14572
+ });
14573
+
14574
+ // src/lib/install-target.ts
14575
+ function keyDisplayPrefix(apiKey) {
14576
+ if (!apiKey.startsWith(KEY_SCHEME)) return null;
14577
+ const prefix = apiKey.slice(KEY_SCHEME.length, KEY_SCHEME.length + KEY_PREFIX_CHARS);
14578
+ return prefix.length === KEY_PREFIX_CHARS ? prefix : null;
14579
+ }
14580
+ async function keyBelongsToInstall(apiKey, installationId, options, now = /* @__PURE__ */ new Date()) {
14581
+ const prefix = keyDisplayPrefix(apiKey);
14582
+ if (prefix === null) return false;
14583
+ const { api_keys } = await listApiKeys(installationId, options);
14584
+ return api_keys.some(
14585
+ (k) => k.installation_id === installationId && k.key_prefix === prefix && (k.status === void 0 || k.status === "active") && k.revoked_at === null && (k.expires_at === null || new Date(k.expires_at) > now)
14586
+ );
14587
+ }
14588
+ function formatInstallTarget(id, detail) {
14589
+ return detail ? `\u2192 install #${id} (${detail})` : `\u2192 install #${id}`;
14590
+ }
14591
+ async function nameKeyTarget(args) {
14592
+ const { apiKey, keySource, entry, cfg, apiUrl, install } = args;
14593
+ const request = cfg ? { baseUrl: apiUrl, auth: { token: cfg.token }, signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) } : null;
14594
+ if (install === void 0) {
14595
+ if (keySource !== "cached" || !entry) {
14596
+ info(
14597
+ `\u2192 install not checked: ${code("DASHWISE_API_KEY")} comes from ${keySource === "environment" ? "the environment" : code(".env.local")}. Pass ${code("--install <id>")} to name its target and refuse a mismatch.`
14598
+ );
14599
+ return 0;
14600
+ }
14601
+ if (!request || entry.workspaceId === void 0) {
14602
+ info(formatInstallTarget(entry.installationId, "the cached dev install"));
14603
+ return 0;
14604
+ }
14605
+ try {
14606
+ const workspaces = await apiRequest("GET", "/api/workspaces", void 0, request);
14607
+ const ws = workspaces.find((w) => w.id === entry.workspaceId);
14608
+ info(formatInstallTarget(entry.installationId, `${ws ? ws.slug : `workspace #${entry.workspaceId}`}, local`));
14609
+ } catch {
14610
+ info(formatInstallTarget(entry.installationId));
14611
+ }
14612
+ return 0;
14613
+ }
14614
+ const source = keySource === "cached" ? "the cached key" : `${code("DASHWISE_API_KEY")} (from ${keySource === "environment" ? "the environment" : code(".env.local")})`;
14615
+ if (!request) {
14616
+ fail(
14617
+ `${code("--install")} looks the install up with your CLI login, and there is none. Run ${code("dashwise login")} first. Nothing was run.`
14618
+ );
14619
+ return 1;
14620
+ }
14621
+ let belongs;
14622
+ try {
14623
+ belongs = await keyBelongsToInstall(apiKey, install, request);
14624
+ } catch (err) {
14625
+ fail(
14626
+ `Could not read the keys of install #${install} to confirm ${source} (${describe(err)}). Refusing to run against an unconfirmed install. Nothing was run.`
14627
+ );
14628
+ return 1;
14629
+ }
14630
+ const cachedDev = entry?.installationId === install ? "the cached dev install, " : "";
14631
+ if (!belongs) {
14632
+ const other = entry && entry.installationId !== install && entry.apiKey === apiKey ? ` It is the cached key of install #${entry.installationId}.` : "";
14633
+ fail(`${source} is not a live key of install #${install}.${other} Nothing was run.`);
14634
+ info(
14635
+ `Use a key of install #${install} (mint one with ${code(`dashwise api-keys create <name> --install ${install}`)}), or drop ${code("--install")} to run against the key's own install.`
14636
+ );
14637
+ return 1;
14638
+ }
14639
+ info(formatInstallTarget(install, `${cachedDev}key checked`));
14640
+ return 0;
14641
+ }
14642
+ function describe(err) {
14643
+ if (err instanceof ApiError) return `${err.code || err.status} ${err.message}`;
14644
+ return err.message;
14645
+ }
14646
+ var KEY_PREFIX_CHARS, KEY_SCHEME, LOOKUP_TIMEOUT_MS;
14647
+ var init_install_target = __esm({
14648
+ "src/lib/install-target.ts"() {
14649
+ init_api_client();
14650
+ init_output();
14651
+ KEY_PREFIX_CHARS = 8;
14652
+ KEY_SCHEME = "dwk_";
14653
+ LOOKUP_TIMEOUT_MS = 1500;
14654
+ }
14655
+ });
14656
+
14657
+ // src/commands/run.ts
14658
+ var run_exports = {};
14659
+ __export(run_exports, {
14660
+ runCommand: () => runCommand
14661
+ });
14662
+ async function runCommand(cmdArgs, options) {
14663
+ if (!cmdArgs || cmdArgs.length === 0) {
14664
+ fail(
14665
+ `Usage: ${code("dashwise run -- <command\u2026>")} (e.g. ${code("dashwise run -- node seed.mjs")})`
14666
+ );
14667
+ return 1;
14668
+ }
14669
+ if (options.install !== void 0 && (!Number.isInteger(options.install) || options.install <= 0)) {
14670
+ fail(`--install expects a positive integer installation id (got "${options.install}").`);
14671
+ return 1;
14672
+ }
14673
+ const projectRoot = resolve(options.dir ?? process.cwd());
14674
+ let manifestSlug;
14675
+ try {
14676
+ manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
14677
+ } catch (err) {
14678
+ fail(err.message);
14679
+ return 1;
14680
+ }
14681
+ if (!manifestSlug) {
14265
14682
  fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
14266
14683
  return 1;
14267
14684
  }
@@ -14530,48 +14947,58 @@ __export(push_exports, {
14530
14947
  schemaPushCommand: () => schemaPushCommand
14531
14948
  });
14532
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
+ );
14533
14962
  const projectRoot = resolve(options.dir ?? process.cwd());
14534
14963
  let manifest;
14535
14964
  try {
14536
14965
  manifest = readManifestOrThrow(projectRoot);
14537
14966
  } catch (err) {
14538
- fail(err.message);
14539
- return 1;
14967
+ return contextError(err.message);
14540
14968
  }
14541
14969
  const slug = manifest.module?.slug;
14542
14970
  if (!slug) {
14543
- fail(`module.json has no ${code("module.slug")} \u2014 the manifest is malformed.`);
14544
- return 1;
14971
+ return contextError("module.json has no module.slug.");
14545
14972
  }
14546
14973
  const cfg = readConfig();
14547
14974
  if (!cfg) {
14548
- fail(`Not logged in. Run ${code("dashwise login")} first.`);
14549
- return 1;
14975
+ return report(
14976
+ new ApiError(401, { code: "UNAUTHENTICATED", message: "Not logged in." }, "Not logged in.")
14977
+ );
14550
14978
  }
14551
14979
  const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
14552
14980
  let moduleId;
14553
14981
  try {
14554
14982
  moduleId = (await getModuleBySlug(slug, auth)).id;
14555
14983
  } catch (err) {
14556
- if (err instanceof ApiError && err.status === 404) {
14557
- fail(
14558
- `Module ${code(slug)} isn't registered on the backend yet. Run ${code("dashwise init")} (or ${code("dashwise module register")}) first.`
14559
- );
14560
- return 1;
14561
- }
14562
- fail(`Could not resolve module ${code(slug)}: ${err.message}`);
14563
- return 1;
14984
+ return reportRefusal(
14985
+ err,
14986
+ { module: slug },
14987
+ { ...options, stderr: true, action: "Resolve module" }
14988
+ );
14564
14989
  }
14565
14990
  const entry = readModuleEntry(slug);
14566
14991
  const dwkAuth = entry?.apiKey ? { baseUrl: resolveApiUrl(), auth: { token: entry.apiKey } } : null;
14567
14992
  let remote = null;
14568
14993
  let previewUnavailableReason = null;
14994
+ let previewError = null;
14569
14995
  if (!dwkAuth) {
14570
- 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`;
14571
14997
  } else {
14572
14998
  try {
14573
14999
  remote = (await getInstallationSchema(dwkAuth)).manifest;
14574
15000
  } catch (err) {
15001
+ previewError = err;
14575
15002
  const detail = err instanceof ApiError ? `${err.code} ${err.status}: ${err.message}` : err.message;
14576
15003
  previewUnavailableReason = `reading the dev-install manifest failed (${detail}) \u2014 the cached key may be expired or revoked`;
14577
15004
  }
@@ -14611,15 +15038,32 @@ async function schemaPushCommand(options) {
14611
15038
  }
14612
15039
  }
14613
15040
  } else {
14614
- warn(
14615
- `Cannot preview what this push would change: ${previewUnavailableReason}.
15041
+ if (!options.json) {
15042
+ warn(
15043
+ `Cannot preview what this push would change: ${previewUnavailableReason}.
14616
15044
  Without the preview there is no way to know whether the push REMOVES tables/fields (live DDL on the dev install).`
14617
- );
15045
+ );
15046
+ }
14618
15047
  if (!options.yes) {
14619
- fail(
14620
- `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
+ )
14621
15066
  );
14622
- return 1;
14623
15067
  }
14624
15068
  info(
14625
15069
  dim(
@@ -14636,7 +15080,7 @@ async function schemaPushCommand(options) {
14636
15080
  auth
14637
15081
  );
14638
15082
  } catch (err) {
14639
- return surfaceSchemaPushError(err, slug);
15083
+ return surfaceSchemaPushError(err, slug, options.json);
14640
15084
  }
14641
15085
  printApplySummary(applied);
14642
15086
  for (const w of referenceFieldWarnings(manifest)) warn(w.message);
@@ -14713,21 +15157,23 @@ function strProp(v, key) {
14713
15157
  }
14714
15158
  return void 0;
14715
15159
  }
14716
- 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
+ );
14717
15168
  if (err instanceof ApiError) {
14718
15169
  switch (err.code) {
14719
15170
  case "MANIFEST_INVALID":
14720
15171
  fail(`Manifest rejected by the backend validator: ${err.message}`);
14721
15172
  for (const issue of extractIssues2(err.context)) {
14722
- out(` ${dim("-")} ${issue}`);
15173
+ process.stderr.write(` ${dim("-")} ${issue}
15174
+ `);
14723
15175
  }
14724
15176
  return 1;
14725
- case "DRAFT_INSTALLATION_MISSING":
14726
- fail(
14727
- `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.
14728
- 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.`
14729
- );
14730
- return 1;
14731
15177
  case "UNIQUE_INDEX_BACKFILL_CONFLICT":
14732
15178
  fail(
14733
15179
  `Cannot create a UNIQUE index \u2014 existing live rows already violate it: ${err.message}
@@ -14748,14 +15194,15 @@ function surfaceSchemaPushError(err, slug) {
14748
15194
  case "FORBIDDEN":
14749
15195
  case "NOT_WORKSPACE_MEMBER":
14750
15196
  fail(
14751
- `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.`
14752
15198
  );
14753
15199
  return 1;
14754
15200
  default:
14755
15201
  if (err.status === 400) {
14756
15202
  fail(`Manifest rejected: ${err.message}`);
14757
15203
  for (const issue of extractIssues2(err.context)) {
14758
- out(` ${dim("-")} ${issue}`);
15204
+ process.stderr.write(` ${dim("-")} ${issue}
15205
+ `);
14759
15206
  }
14760
15207
  return 1;
14761
15208
  }
@@ -14787,6 +15234,7 @@ var init_push = __esm({
14787
15234
  init_config();
14788
15235
  init_manifest_io();
14789
15236
  init_manifest_merge();
15237
+ init_permission_hint();
14790
15238
  init_output();
14791
15239
  init_manifest_reference_lint();
14792
15240
  }
@@ -15963,29 +16411,11 @@ async function runDoctorChecks(dir, deps) {
15963
16411
  return checks;
15964
16412
  }
15965
16413
  function targetCheck(apiUrl) {
15966
- const source = process.env.DASHWISE_API_URL?.trim() ? "from DASHWISE_API_URL" : "from your saved login";
15967
- if (sameOrigin(apiUrl, KNOWN_API_URLS.prod)) {
15968
- return {
15969
- id: "target",
15970
- label: "Target",
15971
- status: "warn",
15972
- detail: `PRODUCTION (${apiUrl}, ${source}). Commands here change real company data.`,
15973
- 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}`)}.`
15974
- };
15975
- }
15976
- if (sameOrigin(apiUrl, KNOWN_API_URLS.dev)) {
15977
- return {
15978
- id: "target",
15979
- label: "Target",
15980
- status: "ok",
15981
- detail: `dev (${apiUrl}, ${source})`
15982
- };
15983
- }
15984
16414
  return {
15985
16415
  id: "target",
15986
16416
  label: "Target",
15987
16417
  status: "ok",
15988
- detail: `custom (${apiUrl}, ${source})`
16418
+ detail: apiUrl
15989
16419
  };
15990
16420
  }
15991
16421
  async function connectionOnly(apiUrl) {
@@ -16166,13 +16596,6 @@ function sameRepo(a, b) {
16166
16596
  const ka = repoKey(a);
16167
16597
  return ka !== null && ka === repoKey(b);
16168
16598
  }
16169
- function sameOrigin(a, b) {
16170
- try {
16171
- return new URL(a).origin === new URL(b).origin;
16172
- } catch {
16173
- return false;
16174
- }
16175
- }
16176
16599
  function safeReadConfig2() {
16177
16600
  try {
16178
16601
  return readConfig();
@@ -17937,7 +18360,9 @@ function convertModule(opts) {
17937
18360
  "app/layout.jsx",
17938
18361
  "src/app/layout.tsx"
17939
18362
  ].find((f) => existsSync(join(dir, f)));
18363
+ let layoutCreated = false;
17940
18364
  if (!layoutFile) {
18365
+ layoutCreated = true;
17941
18366
  writeIfMissing("app/layout.tsx", appLayoutCustomTsx(sopts));
17942
18367
  } else if (!readFileSync(join(dir, layoutFile), "utf-8").includes("AuthProvider")) {
17943
18368
  warnings.push(
@@ -17953,10 +18378,13 @@ function convertModule(opts) {
17953
18378
  `${nextConfigFile}: add \`output: 'standalone'\` \u2014 required for the module runtime bundle.`
17954
18379
  );
17955
18380
  }
17956
- if (!pkg.dependencies?.["@dashai/sdk"]) {
17957
- 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 };
17958
18386
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
17959
- created.push("package.json (added @dashai/sdk)");
18387
+ created.push(`package.json (added ${Object.keys(addDeps).join(", ")})`);
17960
18388
  }
17961
18389
  const outboundApis = scanOutboundHosts(dir);
17962
18390
  const runtimeBlockAdded = normalizeManifest(join(dir, "module.json"), {
@@ -18780,13 +19208,13 @@ init_api_client();
18780
19208
  function redactedConsumer(redacted) {
18781
19209
  switch (redacted) {
18782
19210
  case "private_copy":
18783
- return "someone's private copy";
19211
+ return "someone's private installation";
18784
19212
  case "other_team_copy":
18785
- return "another team's copy";
19213
+ return "another team's installation";
18786
19214
  case "development_copy":
18787
- return "a development copy you cannot see";
19215
+ return "a development installation you cannot see";
18788
19216
  default:
18789
- return "a copy you cannot see";
19217
+ return "an installation you cannot see";
18790
19218
  }
18791
19219
  }
18792
19220
  var WIDEN_WORDS = {
@@ -18804,7 +19232,7 @@ var WIDEN_WORDS = {
18804
19232
  function describeWidening(widening) {
18805
19233
  return widening.map((w) => WIDEN_WORDS[w] ?? w).join(", ");
18806
19234
  }
18807
- async function getUpgradePreview(installationId, version, options = {}) {
19235
+ async function getUpgradePreview2(installationId, version, options = {}) {
18808
19236
  return apiRequest(
18809
19237
  "GET",
18810
19238
  `/api/installations/${installationId}/upgrade-preview?version=${encodeURIComponent(version)}`,
@@ -18871,6 +19299,9 @@ function selectInstallsByKind(installs, moduleId) {
18871
19299
  function isRealRuntimeUrl(url) {
18872
19300
  return !!url && /^https:\/\//i.test(url);
18873
19301
  }
19302
+ function isRuntimeProvisioned(row) {
19303
+ return row.runtime_https ?? isRealRuntimeUrl(row.runtime_url);
19304
+ }
18874
19305
  function isDraftVersion(v) {
18875
19306
  return !v || v.includes("draft");
18876
19307
  }
@@ -18912,8 +19343,73 @@ async function resolveWorkspaceWithFallback(opts) {
18912
19343
  }
18913
19344
 
18914
19345
  // src/commands/module/install.ts
18915
- init_output();
18916
- init_connect_status();
19346
+ init_output();
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
18917
19413
  async function moduleInstallCommand(slugArg, options) {
18918
19414
  const slug = resolveSlug(slugArg, options.dir);
18919
19415
  if (!slug) {
@@ -18971,14 +19467,27 @@ async function moduleInstallCommand(slugArg, options) {
18971
19467
  const installs = await listWorkspaceInstallations(workspace2.id, auth);
18972
19468
  const byKind = selectInstallsByKind(installs, mod.id);
18973
19469
  const production = byKind.all.filter((row2) => !isDevInstallKind(row2.kind));
18974
- if (production.length > 1) {
18975
- fail(
18976
- "Several production installations exist for this module in this company. Select the installation to update in the app. Nothing was changed."
18977
- );
18978
- return 1;
18979
- }
18980
- existing = production[0];
18981
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
+ }
18982
19491
  } catch (err) {
18983
19492
  fail(
18984
19493
  `Could not read workspace installations: ${err.message}`
@@ -19025,7 +19534,7 @@ async function moduleInstallCommand(slugArg, options) {
19025
19534
  );
19026
19535
  } else {
19027
19536
  info(
19028
- `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}`)}.`
19029
19538
  );
19030
19539
  }
19031
19540
  }
@@ -19033,15 +19542,19 @@ async function moduleInstallCommand(slugArg, options) {
19033
19542
  let lossy = false;
19034
19543
  let lossAccepted = false;
19035
19544
  if (existing) {
19036
- 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);
19037
19546
  const previewed = await previewUpgrade(existing.id, targetVersion, options, auth, rerun, devInstallToReplace?.id);
19038
19547
  if (typeof previewed === "number") return previewed;
19039
19548
  preview = previewed.preview;
19040
19549
  lossy = previewed.lossy;
19041
19550
  lossAccepted = previewed.accepted;
19042
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
+ }
19043
19556
  const askForLoss = lossy && !lossAccepted;
19044
- if (askForLoss || !options.yes && process.stdout.isTTY && !confirmedViaPicker) {
19557
+ if (askForLoss || !options.yes && process.stdout.isTTY && process.stdin.isTTY && !confirmedViaPicker) {
19045
19558
  const lossClause = lossy ? " It moves the data listed above to the Trash or converts it." : "";
19046
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}`;
19047
19560
  const answer = await select({
@@ -19068,26 +19581,63 @@ async function moduleInstallCommand(slugArg, options) {
19068
19581
  }
19069
19582
  info("Provisioning the runtime \u2014 this can take a minute\u2026");
19070
19583
  let result;
19071
- try {
19072
- result = await installModule(
19073
- mod.id,
19074
- {
19075
- workspaceId: workspace2.id,
19076
- version: targetVersion,
19077
- ...existing ? { installation_id: existing.id } : {},
19078
- ...options.force !== void 0 ? { force: options.force } : {},
19079
- // P5-08: confirms exactly the delta that was shown (409 DRIFT_CHANGED otherwise).
19080
- ...preview ? { drift_hash: preview.drift_hash } : {}
19081
- },
19082
- auth
19083
- );
19084
- } catch (err) {
19085
- 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
+ }
19086
19635
  }
19087
19636
  if (existing && (isInstallGated(result) || result.installation.id !== existing.id)) {
19088
19637
  fail(
19089
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.`
19090
19639
  );
19640
+ if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19091
19641
  return 1;
19092
19642
  }
19093
19643
  if (isInstallGated(result)) {
@@ -19117,7 +19667,7 @@ async function moduleInstallCommand(slugArg, options) {
19117
19667
  }
19118
19668
  const publicUrl = row?.public_url ?? null;
19119
19669
  const installedVersion = row?.module_version?.version ?? targetVersion;
19120
- const runtimeProvisioned = isRealRuntimeUrl(row?.runtime_url);
19670
+ const runtimeProvisioned = row ? isRuntimeProvisioned(row) : false;
19121
19671
  const readiness = await fetchUsesReadiness(result.installation.id, auth);
19122
19672
  const setupPending = readiness?.setupPending ?? false;
19123
19673
  if (options.json) {
@@ -19130,8 +19680,8 @@ async function moduleInstallCommand(slugArg, options) {
19130
19680
  public_url: publicUrl,
19131
19681
  verified,
19132
19682
  // P5-08: the confirmed preview; `null` for a fresh install (nothing to preview).
19133
- drift_hash: preview?.drift_hash ?? null,
19134
- data_loss: preview ? lossy : null,
19683
+ drift_hash: confirmedPlan?.drift_hash ?? preview?.drift_hash ?? null,
19684
+ data_loss: confirmedPlan ? planLosesData(confirmedPlan) : preview ? lossy : null,
19135
19685
  // `null` when the binding surface was unreachable; `[]` when the
19136
19686
  // module declares no slots. The two are not the same thing.
19137
19687
  uses: readiness === null ? null : usesInstallJson(readiness),
@@ -19230,7 +19780,7 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
19230
19780
  info(explanation);
19231
19781
  return "replace";
19232
19782
  }
19233
- if (process.stdout.isTTY && !options.yes) {
19783
+ if (process.stdout.isTTY && process.stdin.isTTY && !options.yes) {
19234
19784
  info(explanation);
19235
19785
  const answer = await select({
19236
19786
  message: `How should ${code(slug)} be installed into ${code(workspaceSlug)}?`,
@@ -19291,6 +19841,62 @@ async function replaceDevInstall(existing, slug, auth) {
19291
19841
  }
19292
19842
  return 0;
19293
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
+ }
19294
19900
  function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
19295
19901
  if (err instanceof ApiError) {
19296
19902
  switch (err.code) {
@@ -19315,8 +19921,8 @@ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
19315
19921
  break;
19316
19922
  case "UPGRADE_BREAKS_CONSUMERS":
19317
19923
  fail(
19318
- `Upgrading ${code(slug)} would break modules that consume its current schema.
19319
- 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.`
19320
19926
  );
19321
19927
  break;
19322
19928
  case "FORBIDDEN":
@@ -19362,10 +19968,26 @@ function usesInstallJson(readiness) {
19362
19968
  }))
19363
19969
  };
19364
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
+ }
19365
19987
  async function previewUpgrade(installationId, targetVersion, options, auth, rerun, devInstallToReplaceId) {
19366
19988
  let preview;
19367
19989
  try {
19368
- preview = await getUpgradePreview(installationId, targetVersion, auth);
19990
+ preview = await getUpgradePreview2(installationId, targetVersion, auth);
19369
19991
  } catch (err) {
19370
19992
  const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
19371
19993
  const checkNote = options.acceptDataLoss !== void 0 ? `, so ${code("--accept-data-loss")} cannot be checked` : "";
@@ -19389,7 +20011,7 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19389
20011
  const broken = preview.breaks_consumers ?? [];
19390
20012
  if (broken.length > 0) {
19391
20013
  const names = broken.map(
19392
- (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)
19393
20015
  );
19394
20016
  warn(` Breaks ${broken.length} module${broken.length === 1 ? "" : "s"} that read${broken.length === 1 ? "s" : ""} this one: ${names.join(", ")}`);
19395
20017
  }
@@ -19399,7 +20021,7 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19399
20021
  }
19400
20022
  if (options.acceptDataLoss !== void 0 && options.acceptDataLoss !== preview.drift_hash) {
19401
20023
  fail(
19402
- `${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.`
19403
20025
  );
19404
20026
  info(`To accept the change listed above, a person re-runs:
19405
20027
  ${rerun(preview.drift_hash)}`);
@@ -19409,7 +20031,7 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19409
20031
  if (lossy && !accepted) {
19410
20032
  const yesNote = options.yes ? ` ${code("--yes")} does not accept data loss.` : "";
19411
20033
  const marker = agentSessionMarker();
19412
- 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;
19413
20035
  if (why) {
19414
20036
  const alsoDev = devInstallToReplaceId !== void 0 ? ` and destroys dev install #${devInstallToReplaceId} first` : "";
19415
20037
  fail(`This upgrade loses the data listed above${alsoDev}, and ${why}.${yesNote} Refused. Nothing was changed.`);
@@ -19420,12 +20042,13 @@ async function previewUpgrade(installationId, targetVersion, options, auth, reru
19420
20042
  }
19421
20043
  return { preview, lossy, accepted };
19422
20044
  }
19423
- function rerunLine(slug, workspaceSlug, targetVersion, options, replacingDevInstall) {
20045
+ function rerunLine(slug, workspaceSlug, targetVersion, installationId, options, replacingDevInstall) {
19424
20046
  return (hash) => [
19425
20047
  "dashwise module install",
19426
20048
  slug,
19427
20049
  `--workspace ${workspaceSlug}`,
19428
20050
  `--version ${targetVersion}`,
20051
+ `--installation ${installationId}`,
19429
20052
  ...options.force ? ["--force"] : [],
19430
20053
  ...replacingDevInstall ? ["--replace-dev-install"] : [],
19431
20054
  `--accept-data-loss ${hash}`
@@ -20023,324 +20646,99 @@ async function modulePublishCommand(options) {
20023
20646
  success(`Build ${code(`#${enq.build_id}`)} queued for ${code(mod.slug)}.`);
20024
20647
  info(`Not waiting (${code("--no-wait")}). Re-run publish or poll the build to check.`);
20025
20648
  if (options.json) out(JSON.stringify(enq));
20026
- return 0;
20027
- }
20028
- if (!options.json) {
20029
- info(`Build ${code(`#${enq.build_id}`)} queued \u2014 building on the platform\u2026`);
20030
- }
20031
- return pollBuild(enq.build_id, enq.module_id, mod.slug, options);
20032
- }
20033
- const result = body;
20034
- if (!result) {
20035
- fail("Publish succeeded but the response body was empty or malformed.");
20036
- return 1;
20037
- }
20038
- if (options.json) {
20039
- out(JSON.stringify(result));
20040
- return 0;
20041
- }
20042
- success(`Published ${code(mod.slug)} ${code(`v${result.version}`)}`);
20043
- info(`Bundle: ${code(result.bundle_url)}`);
20044
- info(`Hash: ${dim(result.bundle_hash)}`);
20045
- info(`Size: ${formatBytes2(result.bytes_uploaded)}`);
20046
- if (result.warnings.length > 0) {
20047
- warn(`${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}:`);
20048
- for (const w of result.warnings) {
20049
- out(` ${dim("\u2022")} ${w}`);
20050
- }
20051
- }
20052
- if (process.stdout.isTTY) outro("Done.");
20053
- return 0;
20054
- }
20055
- async function pollBuild(buildId, moduleId, slug, options) {
20056
- const timeoutMs = (options.timeoutSec ?? 360) * 1e3;
20057
- const intervalMs = options.pollIntervalMs ?? 4e3;
20058
- const deadline = Date.now() + timeoutMs;
20059
- let lastStatus = "";
20060
- while (Date.now() < deadline) {
20061
- let build;
20062
- try {
20063
- build = await apiRequest(
20064
- "GET",
20065
- `/api/modules/${moduleId}/builds/${buildId}`
20066
- );
20067
- } catch {
20068
- await sleep(intervalMs);
20069
- continue;
20070
- }
20071
- if (!options.json && build.status !== lastStatus) {
20072
- info(`Build ${code(`#${buildId}`)}: ${build.status}`);
20073
- lastStatus = build.status;
20074
- }
20075
- if (build.status === "succeeded") {
20076
- if (options.json) {
20077
- out(JSON.stringify(build));
20078
- } else {
20079
- success(`Published ${code(slug)} ${code(`v${build.version ?? "?"}`)}`);
20080
- info(`Build: ${dim(`#${buildId}`)}`);
20081
- if (process.stdout.isTTY) outro("Done.");
20082
- }
20083
- return 0;
20084
- }
20085
- if (build.status === "failed") {
20086
- if (options.json) {
20087
- out(JSON.stringify(build));
20088
- } else {
20089
- fail(
20090
- `Build ${code(`#${buildId}`)} failed (${build.error_code ?? "BUILD_FAILED"}): ${build.error_message ?? "(no detail)"}`
20091
- );
20092
- if (build.log_url) info(`Logs: ${code(build.log_url)}`);
20093
- info(`Re-run ${code("dashwise module publish")} to try again.`);
20094
- }
20095
- return 1;
20096
- }
20097
- await sleep(intervalMs);
20098
- }
20099
- warn(
20100
- `Still building after ${options.timeoutSec ?? 360}s \u2014 the build is likely still running. Re-run ${code("dashwise module publish")} to check, or inspect build ${code(`#${buildId}`)}.`
20101
- );
20102
- return 0;
20103
- }
20104
- function sleep(ms) {
20105
- return new Promise((r) => setTimeout(r, ms));
20106
- }
20107
- function formatBytes2(n) {
20108
- if (n < 1024) return `${n}B`;
20109
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
20110
- return `${(n / 1024 / 1024).toFixed(1)}MB`;
20111
- }
20112
-
20113
- // src/commands/module/release.ts
20114
- init_api_client();
20115
- init_config();
20116
- init_manifest_io();
20117
- init_output();
20118
-
20119
- // src/lib/permission-hint.ts
20120
- init_api_client();
20121
- init_output();
20122
- var EXIT_NEEDS_GRANT = 3;
20123
- var EXIT_TIMED_OUT = 4;
20124
- var REBUILD_BUMP_OPERATOR_ONLY = "REBUILD_BUMP_OPERATOR_ONLY";
20125
- var FEATURE_SUBJECTS = {
20126
- module_runtime_streaming: "Streaming",
20127
- module_rebuild: "Rebuild",
20128
- module_building: "Building modules",
20129
- module_runtime_sizing: "Memory size"
20130
- };
20131
- var OPTION_SUBJECTS = {
20132
- streaming: "Streaming",
20133
- memory_mb: "Memory size",
20134
- reapply: "Re-applying the runtime",
20135
- timeout: "The timeout",
20136
- reserved_concurrency: "Reserved concurrency",
20137
- log_retention: "Log retention"
20138
- };
20139
- var MODULE_RIGHT_CODES = /* @__PURE__ */ new Set([
20140
- "MODULE_CAPABILITY_DENIED",
20141
- "PUBLISH_REQUIRES_ADMIN",
20142
- "NOT_COPY_OWNER"
20143
- ]);
20144
- function str(v) {
20145
- return typeof v === "string" && v.length > 0 ? v : void 0;
20146
- }
20147
- function num(v) {
20148
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
20149
- }
20150
- function featureFromPermission(permission) {
20151
- const m = permission ? /^workspace\.feature\.([a-z0-9_]+)\.use$/.exec(permission) : null;
20152
- return m?.[1];
20153
- }
20154
- function featureSubject(ctx) {
20155
- const feature = str(ctx.feature) ?? featureFromPermission(str(ctx.permission));
20156
- if (feature && FEATURE_SUBJECTS[feature]) return FEATURE_SUBJECTS[feature];
20157
- return str(ctx.feature_label) ?? (feature ? `"${feature}"` : void 0);
20158
- }
20159
- function needsAdmin(what, module, ask) {
20160
- const scope = module ? ` for ${module}` : "";
20161
- return `${what}${scope} needs a Dashwise admin. Ask your Dashwise admin to ${ask}.`;
20162
- }
20163
- function refusalHint(code2, context, subject = {}) {
20164
- const ctx = context ?? {};
20165
- const module = subject.module ?? str(ctx.module_slug);
20166
- const installationId = subject.installationId ?? num(ctx.installation_id);
20167
- const installLabel = installationId !== void 0 ? ` (install #${installationId})` : "";
20168
- const named = module ?? "this module";
20169
- switch (code2) {
20170
- case "FEATURE_DISABLED":
20171
- case "PERMISSION_DENIED": {
20172
- const what = featureSubject(ctx);
20173
- if (!what) {
20174
- return {
20175
- message: `You don't have permission to do this${module ? ` for ${module}` : ""}. Ask your Dashwise admin for access.`,
20176
- needsGrant: true
20177
- };
20178
- }
20179
- return { message: needsAdmin(what, module, "enable it"), needsGrant: true };
20180
- }
20181
- case "RUNTIME_OPTION_OPERATOR_ONLY": {
20182
- const option = str(ctx.option);
20183
- const what = (option && OPTION_SUBJECTS[option]) ?? (option ? `"${option}"` : "This runtime option");
20184
- return { message: needsAdmin(what, module, "change it"), needsGrant: true };
20185
- }
20186
- case REBUILD_BUMP_OPERATOR_ONLY: {
20187
- const bump = str(ctx.bump) ?? "minor";
20188
- return {
20189
- message: needsAdmin(`A rebuild with a ${bump} version bump`, module, "run it"),
20190
- needsGrant: true
20191
- };
20192
- }
20193
- case "IMAGE_TAG_EXISTS":
20194
- return {
20195
- 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.`,
20196
- needsGrant: true
20197
- };
20198
- case "RUNTIME_IMAGE_NOT_ELIGIBLE": {
20199
- const version = str(ctx.version);
20200
- const target = installationId !== void 0 ? ` --rollout installs:${installationId}` : "";
20201
- return {
20202
- message: `${named}${version ? ` v${version}` : ""} was built before streaming support. Rebuild it and upgrade this install first: dashwise module rebuild${module ? ` ${module}` : ""}${target}`,
20203
- needsGrant: false
20204
- };
20205
- }
20206
- case "RUNTIME_SWAP_PENDING": {
20207
- const version = str(ctx.version);
20208
- return {
20209
- 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.`,
20210
- needsGrant: false
20211
- };
20212
- }
20213
- case "RUNTIME_OPTION_UNAVAILABLE": {
20214
- const option = str(ctx.option);
20215
- const what = (option && OPTION_SUBJECTS[option]) ?? "This runtime option";
20216
- return { message: `${what} is switched off platform-wide right now.`, needsGrant: false };
20217
- }
20218
- case "RUNTIME_CHANGE_IN_PROGRESS": {
20219
- const op = num(ctx.operation_id);
20220
- return {
20221
- 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}` : ""}`,
20222
- needsGrant: false
20223
- };
20649
+ return 0;
20224
20650
  }
20225
- case "BUILD_IN_PROGRESS": {
20226
- const buildId = num(ctx.build_id);
20227
- const status = str(ctx.status);
20228
- const which = buildId !== void 0 ? ` (#${buildId}${status ? `, ${status}` : ""})` : "";
20229
- return {
20230
- message: `A build of ${named} is already running${which}. Wait for it to finish, then run the rebuild again.`,
20231
- needsGrant: false
20232
- };
20651
+ if (!options.json) {
20652
+ info(`Build ${code(`#${enq.build_id}`)} queued \u2014 building on the platform\u2026`);
20233
20653
  }
20234
- case "REBUILD_SOURCE_NOT_FOUND":
20235
- return {
20236
- message: `${named} has no build from source to rebuild. Publish it first: dashwise module publish`,
20237
- needsGrant: false
20238
- };
20239
- case "INSTALLATION_NOT_FOUND":
20240
- return {
20241
- message: `${installationId !== void 0 ? `Installation #${installationId}` : "The installation"} was not found, or you cannot see it.`,
20242
- needsGrant: false
20243
- };
20244
- case "PUBLISH_RATE_LIMITED":
20245
- case "PUBLISH_USER_RATE_LIMITED":
20246
- return {
20247
- message: `Too many builds of ${named} in the last hour. Try again later.`,
20248
- needsGrant: false
20249
- };
20250
- // Install invitations (`dashwise module invites`, Südbau retro 6).
20251
- case "AUTHOR_ADMIN_REQUIRED":
20252
- return {
20253
- message: `Only an admin of the company that owns ${named} can manage its install invitations. Ask that company's admin.`,
20254
- needsGrant: true
20255
- };
20256
- case "COMPANY_ADMIN_REQUIRED":
20257
- return {
20258
- message: "Only an admin of this company can see or decline its install invitations. Ask your company's admin.",
20259
- needsGrant: true
20260
- };
20261
- case "MODULE_PRIVATE_SHARE_FIRST":
20262
- return {
20263
- message: `${named} is still private. Share it with your company in Dashwise first, then invite another company.`,
20264
- needsGrant: false
20265
- };
20266
- default:
20267
- if (MODULE_RIGHT_CODES.has(code2)) {
20268
- return {
20269
- message: `You can't manage ${named}${installLabel}. Ask your Dashwise admin for access.`,
20270
- needsGrant: true
20271
- };
20272
- }
20273
- return null;
20654
+ return pollBuild(enq.build_id, enq.module_id, mod.slug, options);
20274
20655
  }
20275
- }
20276
- var ANSI = /\x1b\[[0-9;]*m/g;
20277
- function reportCliError(message, opts) {
20278
- if (opts.json) {
20279
- out(
20280
- JSON.stringify({
20281
- error: { code: opts.code ?? "CLI_ERROR", message: message.replace(ANSI, ""), status: null, context: null },
20282
- hint: null,
20283
- needs_grant: false
20284
- })
20285
- );
20286
- } else {
20287
- fail(message);
20656
+ const result = body;
20657
+ if (!result) {
20658
+ fail("Publish succeeded but the response body was empty or malformed.");
20659
+ return 1;
20288
20660
  }
20289
- return 1;
20290
- }
20291
- function reportFailedOutcome(failure, hint, opts) {
20292
- if (opts.json) {
20293
- out(
20294
- JSON.stringify({
20295
- error: { code: failure.code, message: failure.message ?? null, status: null, context: null },
20296
- hint: hint?.message ?? null,
20297
- needs_grant: hint?.needsGrant ?? false,
20298
- ...opts.extra
20299
- })
20300
- );
20301
- } else {
20302
- fail(hint?.message ?? opts.fallback);
20661
+ if (options.json) {
20662
+ out(JSON.stringify(result));
20663
+ return 0;
20303
20664
  }
20304
- return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
20305
- }
20306
- function isTransientPollError(err) {
20307
- if (!(err instanceof ApiError)) return true;
20308
- return err.status === 0 || err.status === 429 || err.status >= 500;
20665
+ success(`Published ${code(mod.slug)} ${code(`v${result.version}`)}`);
20666
+ info(`Bundle: ${code(result.bundle_url)}`);
20667
+ info(`Hash: ${dim(result.bundle_hash)}`);
20668
+ info(`Size: ${formatBytes2(result.bytes_uploaded)}`);
20669
+ if (result.warnings.length > 0) {
20670
+ warn(`${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}:`);
20671
+ for (const w of result.warnings) {
20672
+ out(` ${dim("\u2022")} ${w}`);
20673
+ }
20674
+ }
20675
+ if (process.stdout.isTTY) outro("Done.");
20676
+ return 0;
20309
20677
  }
20310
- function reportRefusal(err, subject, opts) {
20311
- if (!(err instanceof ApiError)) {
20312
- const message = err?.message ?? String(err);
20313
- if (opts.json) {
20314
- out(
20315
- JSON.stringify({
20316
- error: { code: "CLI_ERROR", message: message.replace(ANSI, ""), status: null, context: null },
20317
- hint: null,
20318
- needs_grant: false
20319
- })
20678
+ async function pollBuild(buildId, moduleId, slug, options) {
20679
+ const timeoutMs = (options.timeoutSec ?? 360) * 1e3;
20680
+ const intervalMs = options.pollIntervalMs ?? 4e3;
20681
+ const deadline = Date.now() + timeoutMs;
20682
+ let lastStatus = "";
20683
+ while (Date.now() < deadline) {
20684
+ let build;
20685
+ try {
20686
+ build = await apiRequest(
20687
+ "GET",
20688
+ `/api/modules/${moduleId}/builds/${buildId}`
20320
20689
  );
20321
- } else {
20322
- fail(`${opts.action} failed: ${message}`);
20690
+ } catch {
20691
+ await sleep(intervalMs);
20692
+ continue;
20323
20693
  }
20324
- return 1;
20325
- }
20326
- const hint = err.status === 401 ? { message: "Your login has expired. Run: dashwise login", needsGrant: false } : refusalHint(err.code, err.context, subject);
20327
- if (opts.json) {
20328
- out(
20329
- JSON.stringify({
20330
- error: { code: err.code, message: err.message, status: err.status, context: err.context ?? null },
20331
- hint: hint?.message ?? null,
20332
- needs_grant: hint?.needsGrant ?? false
20333
- })
20334
- );
20335
- } else if (hint) {
20336
- fail(hint.message);
20337
- } else {
20338
- fail(`${opts.action} failed (${err.code}): ${err.message}`);
20694
+ if (!options.json && build.status !== lastStatus) {
20695
+ info(`Build ${code(`#${buildId}`)}: ${build.status}`);
20696
+ lastStatus = build.status;
20697
+ }
20698
+ if (build.status === "succeeded") {
20699
+ if (options.json) {
20700
+ out(JSON.stringify(build));
20701
+ } else {
20702
+ success(`Published ${code(slug)} ${code(`v${build.version ?? "?"}`)}`);
20703
+ info(`Build: ${dim(`#${buildId}`)}`);
20704
+ if (process.stdout.isTTY) outro("Done.");
20705
+ }
20706
+ return 0;
20707
+ }
20708
+ if (build.status === "failed") {
20709
+ if (options.json) {
20710
+ out(JSON.stringify(build));
20711
+ } else {
20712
+ fail(
20713
+ `Build ${code(`#${buildId}`)} failed (${build.error_code ?? "BUILD_FAILED"}): ${build.error_message ?? "(no detail)"}`
20714
+ );
20715
+ if (build.log_url) info(`Logs: ${code(build.log_url)}`);
20716
+ info(`Re-run ${code("dashwise module publish")} to try again.`);
20717
+ }
20718
+ return 1;
20719
+ }
20720
+ await sleep(intervalMs);
20339
20721
  }
20340
- return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
20722
+ warn(
20723
+ `Still building after ${options.timeoutSec ?? 360}s \u2014 the build is likely still running. Re-run ${code("dashwise module publish")} to check, or inspect build ${code(`#${buildId}`)}.`
20724
+ );
20725
+ return 0;
20726
+ }
20727
+ function sleep(ms) {
20728
+ return new Promise((r) => setTimeout(r, ms));
20729
+ }
20730
+ function formatBytes2(n) {
20731
+ if (n < 1024) return `${n}B`;
20732
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
20733
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
20341
20734
  }
20342
20735
 
20343
20736
  // src/commands/module/release.ts
20737
+ init_api_client();
20738
+ init_config();
20739
+ init_manifest_io();
20740
+ init_output();
20741
+ init_permission_hint();
20344
20742
  var FULL_SHA = /^[0-9a-f]{40}$/i;
20345
20743
  var positiveId = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
20346
20744
  function isRepositoryBuild(value, moduleId, buildId) {
@@ -20589,7 +20987,7 @@ async function moduleStatusCommand(slugArg, options) {
20589
20987
  );
20590
20988
  return 0;
20591
20989
  }
20592
- const runtimeProvisioned = isRealRuntimeUrl(row.runtime_url);
20990
+ const runtimeProvisioned = isRuntimeProvisioned(row);
20593
20991
  const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
20594
20992
  const installedVersion = row.module_version?.version ?? null;
20595
20993
  const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
@@ -20606,7 +21004,9 @@ async function moduleStatusCommand(slugArg, options) {
20606
21004
  latest_version: latestVersion,
20607
21005
  update_available: updateAvailable,
20608
21006
  runtime_provisioned: runtimeProvisioned,
20609
- 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.
20610
21010
  public_url: row.public_url,
20611
21011
  state,
20612
21012
  // C-09: per-slot data-plane readiness for THIS install. `null`
@@ -20632,7 +21032,7 @@ async function moduleStatusCommand(slugArg, options) {
20632
21032
  );
20633
21033
  kv(
20634
21034
  "Runtime:",
20635
- runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
21035
+ runtimeProvisioned ? "provisioned" : "not provisioned"
20636
21036
  );
20637
21037
  kv("Public URL:", row.public_url ?? dim("(none)"));
20638
21038
  kv(
@@ -20676,7 +21076,7 @@ function usesJson(readiness) {
20676
21076
  init_api_client();
20677
21077
  init_config();
20678
21078
  init_output();
20679
- var AUDIENCE = {
21079
+ var AUDIENCE2 = {
20680
21080
  company: "company",
20681
21081
  teams: "Team Spaces",
20682
21082
  people: "named people"
@@ -20684,16 +21084,16 @@ var AUDIENCE = {
20684
21084
  var givesRole = (roleKey) => typeof roleKey === "string" && roleKey !== "none";
20685
21085
  function governanceAudience(row) {
20686
21086
  if (!row.rules) return null;
20687
- if (givesRole(row.rules.company)) return AUDIENCE.company;
20688
- if (row.rules.teams.some((t) => t.live === true && givesRole(t.role_key))) return AUDIENCE.teams;
20689
- return AUDIENCE.people;
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;
20690
21090
  }
20691
21091
  function toListEntry(row) {
20692
21092
  return {
20693
21093
  id: row.id,
20694
21094
  module_slug: row.module?.slug ?? null,
20695
21095
  name: row.name ?? row.module?.name ?? null,
20696
- audience: row.reach?.kind ? AUDIENCE[row.reach.kind] ?? row.reach.kind : null,
21096
+ audience: row.reach?.kind ? AUDIENCE2[row.reach.kind] ?? row.reach.kind : null,
20697
21097
  version: row.module_version?.version ?? null,
20698
21098
  held: row.held === true,
20699
21099
  development: isDevInstallKind(row.kind),
@@ -20973,6 +21373,7 @@ function surfaceUninstallError(err, slug, workspaceSlug) {
20973
21373
  init_api_client();
20974
21374
  init_config();
20975
21375
  init_output();
21376
+ init_permission_hint();
20976
21377
  var positiveId2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
20977
21378
  function session(json) {
20978
21379
  const cfg = readConfig();
@@ -21234,27 +21635,35 @@ async function moduleInvitesDeclineCommand(inviteId, options) {
21234
21635
 
21235
21636
  // src/commands/module/pull-data.ts
21236
21637
  init_api_client();
21638
+ init_permission_hint();
21237
21639
  init_output();
21238
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
+ );
21239
21653
  const projectRoot = resolve(options.dir ?? process.cwd());
21240
21654
  const manifestPath2 = join(projectRoot, "module.json");
21241
21655
  if (!existsSync(manifestPath2)) {
21242
- fail(
21243
- `No ${code("module.json")} found in ${code(projectRoot)}. Run \`dashwise module init <slug>\` first.`
21244
- );
21245
- return 1;
21656
+ return contextError(`No module.json found in ${projectRoot}.`);
21246
21657
  }
21247
21658
  let manifest;
21248
21659
  try {
21249
21660
  manifest = JSON.parse(readFileSync(manifestPath2, "utf-8"));
21250
21661
  } catch (err) {
21251
- fail(`Could not parse ${code(manifestPath2)}: ${err.message}`);
21252
- return 1;
21662
+ return contextError(`Could not parse module.json: ${err.message}`);
21253
21663
  }
21254
21664
  const slug = manifest.module?.slug;
21255
21665
  if (!slug) {
21256
- fail(`${code("module.json")} is missing ${code("module.slug")}.`);
21257
- return 1;
21666
+ return contextError("module.json is missing module.slug.");
21258
21667
  }
21259
21668
  let mod;
21260
21669
  try {
@@ -21263,16 +21672,7 @@ async function modulePullDataCommand(options) {
21263
21672
  `/api/modules/by-slug/${encodeURIComponent(slug)}`
21264
21673
  );
21265
21674
  } catch (err) {
21266
- if (err instanceof ApiError && err.status === 404) {
21267
- fail(`Module ${code(slug)} not found on the backend.`);
21268
- return 1;
21269
- }
21270
- if (err instanceof ApiError && err.status === 401) {
21271
- fail("Not logged in. Run `dashwise login` first.");
21272
- return 1;
21273
- }
21274
- fail(`Could not resolve module: ${err.message}`);
21275
- return 1;
21675
+ return report(err);
21276
21676
  }
21277
21677
  const limit = options.limit ?? 100;
21278
21678
  let res;
@@ -21282,18 +21682,15 @@ async function modulePullDataCommand(options) {
21282
21682
  `/api/modules/${mod.id}/own-data?limit=${limit}`
21283
21683
  );
21284
21684
  } catch (err) {
21285
- if (err instanceof ApiError && err.status === 400) {
21286
- fail(`Invalid request: ${err.message}`);
21287
- return 1;
21288
- }
21289
- fail(`Could not pull data: ${err.message}`);
21290
- return 1;
21685
+ return report(err);
21291
21686
  }
21292
21687
  const outPath = options.out ? resolve(options.out) : join(projectRoot, "dashwise-data.json");
21293
21688
  writeFileSync(outPath, JSON.stringify(res, null, 2) + "\n", "utf-8");
21294
21689
  let totalRows = 0;
21295
21690
  for (const t of res.tables) totalRows += t.rows.length;
21296
- 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
+ );
21297
21694
  for (const t of res.tables) {
21298
21695
  if (t.total_rows > t.rows.length) {
21299
21696
  warn(
@@ -21302,7 +21699,7 @@ async function modulePullDataCommand(options) {
21302
21699
  }
21303
21700
  }
21304
21701
  if (res.tables.length === 0) {
21305
- info(dim("No owned tables on this module. (Did you publish a manifest with `owns.tables`?)"));
21702
+ info(dim("No tables available."));
21306
21703
  }
21307
21704
  return 0;
21308
21705
  }
@@ -23913,16 +24310,43 @@ init_config();
23913
24310
  init_dotenv_local();
23914
24311
  init_manifest_io();
23915
24312
  init_output();
24313
+ init_api_client();
24314
+ init_permission_hint();
23916
24315
  init_install_target();
23917
- init_dev();
23918
24316
  async function queryCommand(options) {
23919
24317
  const projectRoot = resolve(options.dir ?? process.cwd());
23920
24318
  let manifestSlug;
23921
24319
  try {
23922
24320
  manifestSlug = readManifestOrThrow(projectRoot).module?.slug;
23923
24321
  } catch (err) {
23924
- fail(err.message);
23925
- 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
+ );
23926
24350
  }
23927
24351
  const generatedPath = join(
23928
24352
  projectRoot,
@@ -23936,8 +24360,10 @@ async function queryCommand(options) {
23936
24360
  );
23937
24361
  return 1;
23938
24362
  }
23939
- const keyInShell = process.env.DASHWISE_API_KEY !== void 0;
24363
+ const keyFromProcessEnv = Boolean(process.env.DASHWISE_API_KEY);
23940
24364
  hydrateEnvFromDotenvLocal(projectRoot);
24365
+ let moduleKey = keyFromProcessEnv ? "env" : process.env.DASHWISE_API_KEY ? "env_file" : "saved";
24366
+ let keySource = keyFromProcessEnv ? "environment" : ".env.local";
23941
24367
  const cfg = (() => {
23942
24368
  try {
23943
24369
  return readConfig();
@@ -23946,46 +24372,47 @@ async function queryCommand(options) {
23946
24372
  }
23947
24373
  })();
23948
24374
  const apiUrl = process.env.DASHWISE_API_URL ?? cfg?.apiUrl ?? "http://localhost:3000";
23949
- let keySource = keyInShell ? "environment" : ".env.local";
23950
- let entry = manifestSlug ? readModuleEntry(manifestSlug) : null;
23951
- if (!process.env.DASHWISE_API_KEY && manifestSlug) {
24375
+ const entry = readModuleEntry(manifestSlug);
24376
+ if (!process.env.DASHWISE_API_KEY) {
23952
24377
  if (entry && options.install !== void 0 && entry.installationId !== options.install) {
23953
24378
  fail(
23954
- `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.`
23955
24380
  );
23956
24381
  return 1;
23957
24382
  }
23958
- if (cfg) {
23959
- const healed = await resolveModuleApiKey(
23960
- manifestSlug,
23961
- entry,
23962
- apiUrl,
23963
- cfg.token
23964
- );
23965
- if (healed) entry = healed;
23966
- }
23967
24383
  if (entry?.apiKey) {
23968
24384
  process.env.DASHWISE_API_KEY = entry.apiKey;
24385
+ moduleKey = "saved";
23969
24386
  keySource = "cached";
23970
24387
  if (!process.env.DASHWISE_API_URL) {
23971
24388
  process.env.DASHWISE_API_URL = apiUrl;
23972
24389
  }
23973
24390
  }
23974
24391
  }
23975
- if (process.env.DASHWISE_API_KEY) {
23976
- const target = await nameKeyTarget({
23977
- apiKey: process.env.DASHWISE_API_KEY,
23978
- keySource,
23979
- entry,
23980
- cfg,
23981
- apiUrl: process.env.DASHWISE_API_URL ?? apiUrl,
23982
- ...options.install !== void 0 ? { install: options.install } : {}
23983
- });
23984
- if (target !== 0) return target;
23985
- } else if (options.install !== void 0) {
23986
- fail(`No ${code("dwk_")} key to check against install #${options.install}. Set ${code("DASHWISE_API_KEY")}. Nothing was run.`);
23987
- 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
+ );
23988
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;
23989
24416
  const modes = [
23990
24417
  options.inline !== void 0 ? "inline" : null,
23991
24418
  options.file !== void 0 ? "file" : null,
@@ -24015,7 +24442,7 @@ async function queryCommand(options) {
24015
24442
  const stream = options.stream === true;
24016
24443
  if (options.repl) return runRepl(generatedPath);
24017
24444
  if (options.file !== void 0)
24018
- return runFile(options.file, projectRoot, options.json === true, explain, stream);
24445
+ return runFile(options.file, projectRoot, options.json === true, explain, stream, moduleKey);
24019
24446
  if (options.inline !== void 0)
24020
24447
  return runInline(
24021
24448
  options.inline,
@@ -24023,11 +24450,12 @@ async function queryCommand(options) {
24023
24450
  generatedPath,
24024
24451
  options.json === true,
24025
24452
  explain,
24026
- stream
24453
+ stream,
24454
+ moduleKey
24027
24455
  );
24028
24456
  return 1;
24029
24457
  }
24030
- async function runInline(expression, projectRoot, generatedPath, jsonCompact, explain, stream) {
24458
+ async function runInline(expression, projectRoot, generatedPath, jsonCompact, explain, stream, moduleKey) {
24031
24459
  let entryUrl;
24032
24460
  try {
24033
24461
  entryUrl = generatedEntryUrl(generatedPath);
@@ -24048,7 +24476,7 @@ async function runInline(expression, projectRoot, generatedPath, jsonCompact, ex
24048
24476
  ].join("\n");
24049
24477
  writeFileSync(tmpFile, source, "utf-8");
24050
24478
  try {
24051
- return await importAndPrint(tmpFile, projectRoot, jsonCompact, explain, stream);
24479
+ return await importAndPrint(tmpFile, projectRoot, jsonCompact, explain, stream, moduleKey);
24052
24480
  } finally {
24053
24481
  try {
24054
24482
  rmSync(tmpDir, { recursive: true, force: true });
@@ -24056,7 +24484,7 @@ async function runInline(expression, projectRoot, generatedPath, jsonCompact, ex
24056
24484
  }
24057
24485
  }
24058
24486
  }
24059
- async function runFile(file, projectRoot, jsonCompact, explain, stream) {
24487
+ async function runFile(file, projectRoot, jsonCompact, explain, stream, moduleKey) {
24060
24488
  const abs = resolve(projectRoot, file);
24061
24489
  if (!existsSync(abs)) {
24062
24490
  fail(`File not found: ${code(abs)}`);
@@ -24068,7 +24496,7 @@ async function runFile(file, projectRoot, jsonCompact, explain, stream) {
24068
24496
  );
24069
24497
  return 1;
24070
24498
  }
24071
- return importAndPrint(abs, projectRoot, jsonCompact, explain, stream);
24499
+ return importAndPrint(abs, projectRoot, jsonCompact, explain, stream, moduleKey);
24072
24500
  }
24073
24501
  async function runRepl(generatedPath) {
24074
24502
  let generated;
@@ -24107,7 +24535,7 @@ async function runRepl(generatedPath) {
24107
24535
  });
24108
24536
  });
24109
24537
  }
24110
- async function importAndPrint(file, projectRoot, jsonCompact, explain, stream) {
24538
+ async function importAndPrint(file, projectRoot, jsonCompact, explain, stream, moduleKey) {
24111
24539
  const originalCwd = process.cwd();
24112
24540
  try {
24113
24541
  process.chdir(projectRoot);
@@ -24143,11 +24571,11 @@ async function importAndPrint(file, projectRoot, jsonCompact, explain, stream) {
24143
24571
  }
24144
24572
  return 0;
24145
24573
  } catch (err) {
24146
- fail(`Query failed: ${err.message}`);
24574
+ const exit = reportRefusal(err, {}, { json: jsonCompact, stderr: true, action: "Query", moduleKey });
24147
24575
  if (process.env.DASHWISE_DEBUG) {
24148
24576
  console.error(err);
24149
24577
  }
24150
- return 1;
24578
+ return exit;
24151
24579
  } finally {
24152
24580
  process.chdir(originalCwd);
24153
24581
  }
@@ -24848,6 +25276,7 @@ init_api_client();
24848
25276
  init_config();
24849
25277
  init_manifest_io();
24850
25278
  init_output();
25279
+ init_permission_hint();
24851
25280
  var PLATFORM_DEFAULTS = ["manifest", "on", "off"];
24852
25281
  var BUMPS = ["patch", "minor", "major"];
24853
25282
  function parseRollout(raw) {
@@ -25071,6 +25500,7 @@ init_api_client();
25071
25500
  init_config();
25072
25501
  init_manifest_io();
25073
25502
  init_output();
25503
+ init_permission_hint();
25074
25504
  var TERMINAL_OPERATION = /* @__PURE__ */ new Set(["succeeded", "failed", "superseded"]);
25075
25505
  async function moduleRuntimeShowCommand(slugArg, options) {
25076
25506
  const auth = authOrFail(options.json);
@@ -25367,10 +25797,11 @@ function sleep5(ms) {
25367
25797
 
25368
25798
  // src/program.ts
25369
25799
  init_output();
25800
+ init_permission_hint();
25370
25801
  var program = new Command();
25371
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(
25372
25803
  "--api-url <url>",
25373
- `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}).`
25374
25805
  );
25375
25806
  program.hook("preAction", () => {
25376
25807
  const apiUrl = program.opts().apiUrl?.trim();
@@ -25389,7 +25820,7 @@ program.command("login").description("Sign in via the device-code flow.").option
25389
25820
  process.exitCode = exit;
25390
25821
  });
25391
25822
  program.command("doctor").description(
25392
- "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."
25393
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) => {
25394
25825
  process.exitCode = await doctorCommand(
25395
25826
  {
@@ -25692,8 +26123,9 @@ moduleCmd.command("diff").description(
25692
26123
  });
25693
26124
  }
25694
26125
  );
25695
- 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) => {
25696
26127
  process.exitCode = await modulePullDataCommand({
26128
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
25697
26129
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
25698
26130
  ...cmdOpts.limit !== void 0 ? { limit: cmdOpts.limit } : {},
25699
26131
  ...cmdOpts.out !== void 0 ? { out: cmdOpts.out } : {}
@@ -26059,8 +26491,15 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
26059
26491
  }
26060
26492
  );
26061
26493
  moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description(
26062
- "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."
26063
- ).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(
26064
26503
  "--keep-dev-install",
26065
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."
26066
26505
  ).option(
@@ -26072,6 +26511,7 @@ moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description
26072
26511
  ).option("--json", "Emit the final result as machine-readable JSON.").action(
26073
26512
  async (slug, cmdOpts) => {
26074
26513
  process.exitCode = await moduleInstallCommand(slug, {
26514
+ ...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {},
26075
26515
  ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
26076
26516
  ...cmdOpts.version !== void 0 ? { version: cmdOpts.version } : {},
26077
26517
  ...cmdOpts.force !== void 0 ? { force: cmdOpts.force } : {},
@@ -26350,9 +26790,10 @@ var schemaCmd = program.command("schema").description(
26350
26790
  );
26351
26791
  schemaCmd.command("push").description(
26352
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)."
26353
- ).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) => {
26354
26794
  const { schemaPushCommand: schemaPushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
26355
26795
  process.exitCode = await schemaPushCommand2({
26796
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
26356
26797
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
26357
26798
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
26358
26799
  });
@@ -26433,7 +26874,13 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
26433
26874
  }
26434
26875
  );
26435
26876
  function getVersion() {
26436
- return "0.27.0";
26877
+ return "0.28.1";
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());
26437
26884
  }
26438
26885
  function parseIntOption(value) {
26439
26886
  const n = Number.parseInt(value, 10);