@dashai/cli 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -428,6 +428,25 @@ async function getModuleBySlug(slug, options = {}) {
428
428
  options
429
429
  );
430
430
  }
431
+ function isInstallGated(r) {
432
+ return r.install_gated === true;
433
+ }
434
+ async function installModule(moduleId, body, options = {}) {
435
+ return apiRequest(
436
+ "POST",
437
+ `/api/modules/${moduleId}/install`,
438
+ body,
439
+ options
440
+ );
441
+ }
442
+ async function listWorkspaceInstallations(workspaceId, options = {}) {
443
+ return apiRequest(
444
+ "GET",
445
+ `/api/modules/workspaces/${workspaceId}/installations`,
446
+ void 0,
447
+ options
448
+ );
449
+ }
431
450
  async function applyDraftManifest(moduleId, manifest, options = {}) {
432
451
  return apiRequest(
433
452
  "PUT",
@@ -846,8 +865,14 @@ npm run dev:next # next dev on :3000 (terminal 2)
846
865
  - **\`module.json\`** \u2014 manifest (\`module_kind: 'custom'\`, \`runtime.kind: 'machine'\`).
847
866
  - **\`middleware.ts\`** \u2014 \`withAuth\` from \`@dashai/sdk/auth/middleware\`. Redirects unauthenticated requests through the DashWise SSO flow.
848
867
  - **\`app/layout.tsx\`** \u2014 wraps the tree in \`<AuthProvider>\` from \`@dashai/sdk/auth/client\`.
849
- - **\`app/page.tsx\`** \u2014 public landing (allowlisted in \`middleware.ts\`).
850
- - **\`app/dashboard/page.tsx\`** \u2014 Server Component requiring a valid session via \`getServerSession()\`.
868
+ - **\`app/page.tsx\`** \u2014 the app's home at \`/\` (auth-gated; zero-click for platform users via \`autoSignIn\`). **This is where you build your app.**
869
+ - **\`app/dashboard/page.tsx\`** \u2014 an additional example Server Component route, also session-gated via \`getServerSession()\`.
870
+
871
+ The design is yours: edit the theme tokens in \`app/globals.css\` (they ship
872
+ colored on purpose, as a starting point) and use any Tailwind colors or
873
+ shadcn blocks you like. A manual "Sign in" landing page is only for a
874
+ \`public_access_allowed\` module \u2014 re-add \`'/'\` to \`middleware.ts\` publicPaths
875
+ and drop the home's session guard to build one.
851
876
  - **\`app/api/auth/callback/route.ts\`** \u2014 exchanges the SSO one-time code for a session cookie (uses \`exchangeCode()\` from the SDK).
852
877
  - **\`app/api/auth/sign-in/route.ts\`** \u2014 one entry point that calls the backend's \`POST /api/module-api/sso/start-url\` (authenticated with your \`dwk_\` key) and 302s the browser to the returned \`redirect_url\`. Same path in dev + prod.
853
878
  - **\`app/api/auth/sign-out/route.ts\`** \u2014 clears the session cookie + tells the backend to revoke.
@@ -924,18 +949,28 @@ function middlewareTs() {
924
949
  import { withAuth } from '@dashai/sdk/auth/middleware';
925
950
 
926
951
  export default withAuth({
927
- // \`/sign-in-error\` is public so unauthenticated visitors can actually
928
- // see the polished SSO error page \u2014 otherwise a sign-in failure would
929
- // redirect-loop trying to re-authenticate.
930
- publicPaths: ['/', '/about', '/sign-in-error'],
952
+ // Only the SSO error page is public. The app's home lives at \`/\`
953
+ // (app/page.tsx) and is auth-gated like the rest of the app \u2014 a
954
+ // signed-in platform user still lands there with ZERO clicks because
955
+ // \`autoSignIn\` (below) completes the handshake silently. There is no
956
+ // public "sign in" wall in front of your app by default.
957
+ //
958
+ // \`/sign-in-error\` stays public so a failed sign-in can render its
959
+ // error page instead of redirect-looping back into re-authentication.
960
+ //
961
+ // Building a \`public_access_allowed\` module that wants an ANONYMOUS
962
+ // landing page (a marketing / manual "Sign in with DashWise" screen)?
963
+ // Re-add \`'/'\` here AND make app/page.tsx public (drop its
964
+ // getServerSession guard). See the README's "Design & home" note.
965
+ publicPaths: ['/sign-in-error'],
931
966
  loginPath: '/api/auth/sign-in',
932
967
  // Silent sign-in: when a signed-in DashWise user opens the module through
933
968
  // the platform (in-app embed or the module subdomain), the SSO handshake
934
- // completes with zero clicks \u2014 they land straight in the app, landing page
935
- // included. It never touches:
969
+ // completes with zero clicks \u2014 they land straight on the page they asked
970
+ // for, the home at \`/\` included. It never touches:
936
971
  // - anonymous visitors on public installs (no platform-session hint),
937
- // - local \`dashwise dev\` (no platform proxy \u2192 no hint header) \u2014 the
938
- // landing's "Sign in with DashWise" button stays your dev flow,
972
+ // - local \`dashwise dev\` (no platform proxy \u2192 no hint header) \u2014 a
973
+ // manual sign-in through /api/auth/sign-in stays your dev flow,
939
974
  // - anything within 60s of a failed silent attempt (no redirect loops).
940
975
  autoSignIn: true,
941
976
  });
@@ -977,84 +1012,118 @@ export default function RootLayout({ children }: { children: React.ReactNode })
977
1012
  `;
978
1013
  }
979
1014
  function appPageCustomTsx(opts) {
980
- return `// Public landing page \u2014 listed in middleware.ts publicPaths.
1015
+ return `// app/page.tsx \u2014 the app's REAL home, served at \`/\`. THIS IS YOUR APP.
1016
+ // Build it here.
1017
+ //
1018
+ // A signed-in DashWise user who opens this module through the platform
1019
+ // (in-app embed or the module subdomain) lands on THIS page with zero
1020
+ // clicks: the middleware's \`autoSignIn\` completes the SSO handshake
1021
+ // silently, so there is no sign-in wall in front of your app. The guard
1022
+ // below mirrors app/dashboard/page.tsx \u2014 no session \u2192 bounce through the
1023
+ // SSO flow and come right back to \`/\`.
981
1024
  //
982
- // NOTE: signed-in DashWise users usually never see this page \u2014 the
983
- // middleware's \`autoSignIn\` signs them in silently and they land on the
984
- // page they asked for. This landing serves anonymous visitors (public
985
- // installs), local \`dashwise dev\`, and the rare failed silent attempt \u2014
986
- // its "Sign in with DashWise" button is the manual fallback that kicks
987
- // off the same SSO flow (via /api/auth/sign-in). After signing in, the
988
- // user lands on /dashboard (Server Component, session-validated).
1025
+ // A public, anonymous sign-in landing page is only for a
1026
+ // \`public_access_allowed\` module that wants an unauthenticated entry
1027
+ // point. To build one, re-add \`'/'\` to middleware.ts publicPaths and drop
1028
+ // the getServerSession guard here (see the README's "Design & home" note).
989
1029
  //
990
- // Styling: shadcn/ui primitives (\`<Button>\`, \`<Card>\`) on Tailwind 4
991
- // theme tokens. Add more blocks via \`npx shadcn add @shadcn-studio/...\`
992
- // (see README's "Adding UI blocks" section).
1030
+ // The design is YOURS to own. These shadcn primitives sit on the theme
1031
+ // tokens in app/globals.css \u2014 edit the hues freely \u2014 and the full
1032
+ // Tailwind palette (bg-indigo-600, text-emerald-500, \u2026) is available too.
1033
+ // This colored starter look is a starting point, not a mandate.
993
1034
  import Link from 'next/link';
994
- import { LogIn, LayoutDashboard } from 'lucide-react';
1035
+ import { getServerSession } from '@dashai/sdk/auth/server';
1036
+ import { redirect } from 'next/navigation';
1037
+ import { ArrowRight, Table2, Database, PenLine } from 'lucide-react';
995
1038
  import { Button } from '@/components/ui/button';
996
1039
  import {
997
1040
  Card,
998
- CardContent,
999
1041
  CardDescription,
1000
1042
  CardHeader,
1001
1043
  CardTitle,
1002
1044
  } from '@/components/ui/card';
1003
1045
 
1004
- export default function Page() {
1046
+ // force-dynamic: this page calls getServerSession() at render, which
1047
+ // reads DASHWISE_API_URL/KEY. \`next build\` statically PRERENDERS pages by
1048
+ // default and runs with NO DashWise env, so the SDK would throw
1049
+ // "missing apiBaseUrl" and the platform's first publish would fail with
1050
+ // an opaque BUILD_FAILED. Opting into dynamic rendering skips prerender.
1051
+ export const dynamic = 'force-dynamic';
1052
+
1053
+ export default async function Home() {
1054
+ const session = await getServerSession();
1055
+ if (!session) {
1056
+ redirect('/api/auth/sign-in?return_to=/');
1057
+ }
1005
1058
  return (
1006
- <main className="flex min-h-dvh flex-col items-center justify-center gap-12 px-6 py-16">
1007
- <section className="flex max-w-2xl flex-col items-center gap-6 text-center">
1008
- <h1 className="text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
1059
+ <main className="mx-auto flex min-h-dvh max-w-4xl flex-col gap-10 px-6 py-12">
1060
+ {/* Primary-colored hero \u2014 the default look is colored on purpose,
1061
+ to signal the design is yours to change. */}
1062
+ <section className="flex flex-col gap-4 rounded-2xl bg-primary px-8 py-12 text-primary-foreground shadow-sm">
1063
+ <p className="text-sm font-medium uppercase tracking-wide opacity-80">
1009
1064
  ${escapeHtml(opts.name)}
1065
+ </p>
1066
+ <h1 className="text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
1067
+ Your app starts here.
1010
1068
  </h1>
1011
- <p className="text-balance text-lg text-muted-foreground">
1012
- A DashWise module. Sign in with your DashWise account to access the
1013
- dashboard and start using the workspace's data through the typed SDK.
1069
+ <p className="max-w-xl text-pretty text-lg opacity-90">
1070
+ Signed in as{' '}
1071
+ <span className="font-medium">{session.user.email}</span>. This is the
1072
+ real home of your module \u2014 edit{' '}
1073
+ <code className="rounded bg-primary-foreground/15 px-1.5 py-0.5 font-mono">
1074
+ app/page.tsx
1075
+ </code>{' '}
1076
+ to build it.
1014
1077
  </p>
1015
- <div className="flex flex-wrap items-center justify-center gap-3">
1016
- <Button asChild size="lg">
1017
- <Link href="/api/auth/sign-in?return_to=/dashboard">
1018
- <LogIn aria-hidden="true" />
1019
- Sign in with DashWise
1020
- </Link>
1021
- </Button>
1022
- <Button asChild variant="ghost" size="lg">
1078
+ <div className="flex flex-wrap gap-3 pt-2">
1079
+ <Button asChild size="lg" variant="secondary">
1023
1080
  <Link href="/dashboard">
1024
- <LayoutDashboard aria-hidden="true" />
1025
- Go to dashboard
1081
+ See the example route
1082
+ <ArrowRight aria-hidden="true" />
1026
1083
  </Link>
1027
1084
  </Button>
1028
1085
  </div>
1029
1086
  </section>
1030
1087
 
1031
- <Card className="w-full max-w-2xl">
1032
- <CardHeader>
1033
- <CardTitle>What you can do here</CardTitle>
1034
- <CardDescription>
1035
- This is a custom DashWise module \u2014 a full Next.js app authenticated
1036
- against the DashWise SSO. Edit \`app/page.tsx\` to replace this
1037
- landing page; everything else is yours to customize.
1038
- </CardDescription>
1039
- </CardHeader>
1040
- <CardContent className="text-sm text-muted-foreground">
1041
- <ul className="space-y-2">
1042
- <li>
1043
- Edit <code className="rounded bg-muted px-1.5 py-0.5">module.json</code>
1044
- {' '}to declare tables, dependencies, and runtime config.
1045
- </li>
1046
- <li>
1047
- Run <code className="rounded bg-muted px-1.5 py-0.5">dashwise dev</code>
1048
- {' '}for a one-command local loop with auth env vars auto-injected.
1049
- </li>
1050
- <li>
1051
- Import typed table clients from{' '}
1052
- <code className="rounded bg-muted px-1.5 py-0.5">@dashai/generated</code>
1053
- {' '}\u2014 the SDK regenerates on every manifest save.
1054
- </li>
1055
- </ul>
1056
- </CardContent>
1057
- </Card>
1088
+ {/* A few colored pointers to get moving. */}
1089
+ <section className="grid gap-4 sm:grid-cols-3">
1090
+ <Card className="border-l-4 border-l-chart-1">
1091
+ <CardHeader>
1092
+ <Table2 className="size-5 text-chart-1" aria-hidden="true" />
1093
+ <CardTitle className="text-base">Declare your data</CardTitle>
1094
+ <CardDescription>
1095
+ Add tables to{' '}
1096
+ <code className="rounded bg-muted px-1 py-0.5 font-mono">module.json</code>{' '}
1097
+ (or run{' '}
1098
+ <code className="rounded bg-muted px-1 py-0.5 font-mono">dashwise table create</code>).
1099
+ </CardDescription>
1100
+ </CardHeader>
1101
+ </Card>
1102
+ <Card className="border-l-4 border-l-chart-2">
1103
+ <CardHeader>
1104
+ <Database className="size-5 text-chart-2" aria-hidden="true" />
1105
+ <CardTitle className="text-base">Read it, typed</CardTitle>
1106
+ <CardDescription>
1107
+ Import{' '}
1108
+ <code className="rounded bg-muted px-1 py-0.5 font-mono">db</code>{' '}
1109
+ from{' '}
1110
+ <code className="rounded bg-muted px-1 py-0.5 font-mono">@dashai/generated</code>{' '}
1111
+ \u2014 regenerated on every manifest save.
1112
+ </CardDescription>
1113
+ </CardHeader>
1114
+ </Card>
1115
+ <Card className="border-l-4 border-l-chart-3">
1116
+ <CardHeader>
1117
+ <PenLine className="size-5 text-chart-3" aria-hidden="true" />
1118
+ <CardTitle className="text-base">Write via server actions</CardTitle>
1119
+ <CardDescription>
1120
+ Mutate data from Server Actions / route handlers so the scoped{' '}
1121
+ <code className="rounded bg-muted px-1 py-0.5 font-mono">dwk_</code>{' '}
1122
+ key stays server-side.
1123
+ </CardDescription>
1124
+ </CardHeader>
1125
+ </Card>
1126
+ </section>
1058
1127
  </main>
1059
1128
  );
1060
1129
  }
@@ -1074,18 +1143,25 @@ import {
1074
1143
  } from '@/components/ui/card';
1075
1144
 
1076
1145
  /**
1077
- * Auth-gated Server Component. The middleware ensures the user has
1078
- * a session cookie before this page renders; \`getServerSession()\`
1146
+ * An ADDITIONAL example route \u2014 your real home is app/page.tsx (\`/\`).
1147
+ * This \`/dashboard\` page is here to show a second auth-gated Server
1148
+ * Component: keep it, rename it, or delete it. The middleware ensures the
1149
+ * user has a session cookie before this page renders; \`getServerSession()\`
1079
1150
  * validates the cookie against the backend's \`/api/auth/sessions/me\`
1080
1151
  * endpoint, returning the decoded \`Session\` (or \`null\` if stale).
1081
1152
  *
1082
1153
  * On a successful render, this is the polished session card \u2014 replace
1083
1154
  * the contents with whatever your module actually needs to show.
1084
1155
  */
1156
+ // force-dynamic: same reason as app/page.tsx \u2014 this route calls
1157
+ // getServerSession() at render, which needs DASHWISE_API_URL/KEY. Static
1158
+ // prerender during \`next build\` runs with no env \u2192 BUILD_FAILED. Opt out.
1159
+ export const dynamic = 'force-dynamic';
1160
+
1085
1161
  export default async function Dashboard() {
1086
1162
  const session = await getServerSession();
1087
1163
  if (!session) {
1088
- redirect('/api/auth/sign-in?return_to=/dashboard');
1164
+ redirect('/api/auth/sign-in?return_to=/');
1089
1165
  }
1090
1166
  return (
1091
1167
  <main className="mx-auto flex min-h-dvh max-w-3xl flex-col gap-6 px-6 py-12">
@@ -1138,7 +1214,9 @@ export default async function Dashboard() {
1138
1214
  <CardHeader>
1139
1215
  <CardTitle>Next steps</CardTitle>
1140
1216
  <CardDescription>
1141
- Replace this dashboard with your module's actual UI.
1217
+ This is an extra example route \u2014 your real home is{' '}
1218
+ <code className="rounded bg-muted px-1.5 py-0.5">app/page.tsx</code>.
1219
+ Keep, rename, or delete this one.
1142
1220
  </CardDescription>
1143
1221
  </CardHeader>
1144
1222
  <CardContent className="space-y-2 text-sm text-muted-foreground">
@@ -1529,7 +1607,12 @@ export default config;
1529
1607
  `;
1530
1608
  }
1531
1609
  function globalsCss() {
1532
- return `@import "tailwindcss";
1610
+ return `/* These theme tokens are a STARTING POINT \u2014 change the hues freely.
1611
+ The default \`--primary\` is a violet/indigo and the \`--chart-*\` ramp is
1612
+ a 5-hue set; swap them for your brand. The full Tailwind color palette
1613
+ (bg-indigo-600, text-emerald-500, \u2026) is also available on top of these
1614
+ tokens. This is YOUR design to own. */
1615
+ @import "tailwindcss";
1533
1616
  @import "tw-animate-css";
1534
1617
  @import "shadcn/tailwind.css";
1535
1618
 
@@ -1542,20 +1625,33 @@ function globalsCss() {
1542
1625
  --card-foreground: oklch(0.14 0 0);
1543
1626
  --popover: oklch(1 0 0);
1544
1627
  --popover-foreground: oklch(0.14 0 0);
1545
- --primary: oklch(0.2 0 0);
1546
- --primary-foreground: oklch(0.99 0 0);
1547
- --secondary: oklch(0.97 0 0);
1548
- --secondary-foreground: oklch(0.2 0 0);
1549
- --muted: oklch(0.97 0 0);
1550
- --muted-foreground: oklch(0.56 0 0);
1551
- --accent: oklch(0.97 0 0);
1552
- --accent-foreground: oklch(0.2 0 0);
1628
+ /* Colored primary (violet/indigo, ~indigo-600). White foreground reads
1629
+ cleanly on it (~5:1). Change the hue (275) to re-brand. */
1630
+ --primary: oklch(0.55 0.22 275);
1631
+ --primary-foreground: oklch(0.985 0 0);
1632
+ /* secondary + muted stay near-neutral (clean UI) but carry a faint
1633
+ violet tint so nothing is pure grayscale. */
1634
+ --secondary: oklch(0.97 0.005 275);
1635
+ --secondary-foreground: oklch(0.25 0.02 275);
1636
+ --muted: oklch(0.97 0.005 275);
1637
+ --muted-foreground: oklch(0.55 0.02 275);
1638
+ /* accent = a soft, low-chroma wash of the primary hue. */
1639
+ --accent: oklch(0.96 0.03 275);
1640
+ --accent-foreground: oklch(0.4 0.1 275);
1553
1641
  --destructive: oklch(0.58 0.24 28.48);
1554
1642
  --border: oklch(0.92 0 0);
1555
1643
  --input: oklch(0.92 0 0);
1556
- --ring: oklch(0.71 0 0);
1644
+ --ring: oklch(0.55 0.2 275);
1557
1645
  --radius: 0.625rem;
1558
1646
 
1647
+ /* 5-hue chart ramp \u2014 distinct, readable hues for data viz + accents.
1648
+ Wired to \`bg-chart-1\` \u2026 \`text-chart-5\` via the @theme block below. */
1649
+ --chart-1: oklch(0.55 0.2 275);
1650
+ --chart-2: oklch(0.6 0.13 195);
1651
+ --chart-3: oklch(0.7 0.16 70);
1652
+ --chart-4: oklch(0.65 0.17 145);
1653
+ --chart-5: oklch(0.6 0.22 15);
1654
+
1559
1655
  --font-sans:
1560
1656
  ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
1561
1657
  "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
@@ -1571,18 +1667,37 @@ function globalsCss() {
1571
1667
  --card-foreground: oklch(0.99 0 0);
1572
1668
  --popover: oklch(0.2 0 0);
1573
1669
  --popover-foreground: oklch(0.99 0 0);
1574
- --primary: oklch(0.92 0 0);
1575
- --primary-foreground: oklch(0.2 0 0);
1576
- --secondary: oklch(0.27 0 0);
1670
+ /* Slightly lighter violet so it pops on the dark background; white
1671
+ foreground stays readable (~4.8:1). */
1672
+ --primary: oklch(0.58 0.21 275);
1673
+ --primary-foreground: oklch(0.985 0 0);
1674
+ --secondary: oklch(0.27 0.01 275);
1577
1675
  --secondary-foreground: oklch(0.99 0 0);
1578
- --muted: oklch(0.27 0 0);
1579
- --muted-foreground: oklch(0.71 0 0);
1580
- --accent: oklch(0.27 0 0);
1581
- --accent-foreground: oklch(0.99 0 0);
1676
+ --muted: oklch(0.27 0.01 275);
1677
+ --muted-foreground: oklch(0.71 0.02 275);
1678
+ --accent: oklch(0.3 0.04 275);
1679
+ --accent-foreground: oklch(0.97 0.01 275);
1582
1680
  --destructive: oklch(0.7 0.19 22.23);
1583
1681
  --border: oklch(0.27 0 0);
1584
1682
  --input: oklch(0.27 0 0);
1585
- --ring: oklch(0.56 0 0);
1683
+ --ring: oklch(0.58 0.19 275);
1684
+
1685
+ --chart-1: oklch(0.62 0.19 275);
1686
+ --chart-2: oklch(0.67 0.13 195);
1687
+ --chart-3: oklch(0.75 0.15 70);
1688
+ --chart-4: oklch(0.7 0.16 145);
1689
+ --chart-5: oklch(0.67 0.2 15);
1690
+ }
1691
+
1692
+ /* Wire the chart tokens into Tailwind's color namespace so \`bg-chart-1\`,
1693
+ \`text-chart-2\`, \`border-l-chart-3\`, \u2026 resolve. (\`@import "shadcn/tailwind.css"\`
1694
+ already maps the core tokens like \`--primary\`; this covers the ramp.) */
1695
+ @theme inline {
1696
+ --color-chart-1: var(--chart-1);
1697
+ --color-chart-2: var(--chart-2);
1698
+ --color-chart-3: var(--chart-3);
1699
+ --color-chart-4: var(--chart-4);
1700
+ --color-chart-5: var(--chart-5);
1586
1701
  }
1587
1702
 
1588
1703
  * {
@@ -1853,13 +1968,13 @@ export default async function SignInErrorPage({ searchParams }: SignInErrorPageP
1853
1968
  )}
1854
1969
  <div className="flex flex-wrap gap-2 pt-2">
1855
1970
  <Button asChild>
1856
- <Link href="/api/auth/sign-in?return_to=/dashboard">
1971
+ <Link href="/api/auth/sign-in?return_to=/">
1857
1972
  <RefreshCw aria-hidden="true" />
1858
1973
  Try again
1859
1974
  </Link>
1860
1975
  </Button>
1861
1976
  <Button asChild variant="ghost">
1862
- <Link href="/">Back to landing</Link>
1977
+ <Link href="/">Back to home</Link>
1863
1978
  </Button>
1864
1979
  </div>
1865
1980
  </CardContent>
@@ -1941,10 +2056,10 @@ dashwise field remove <table> <slug>
1941
2056
  dashwise field set-type <table> <slug> <type>
1942
2057
  \`\`\`
1943
2058
 
1944
- Allowed field types: \`text\`, \`long_text\`, \`number\`, \`boolean\`,
2059
+ Allowed field types (18): \`text\`, \`long_text\`, \`number\`, \`boolean\`,
1945
2060
  \`date\`, \`datetime\`, \`single_select\`, \`multi_select\`,
1946
2061
  \`link_row\`, \`file\`, \`multi_file\`, \`email\`, \`url\`,
1947
- \`phone\`, \`formula\`, \`ai\`.
2062
+ \`phone\`, \`formula\`, \`ai\`, \`uuid\`, \`jsonb\`.
1948
2063
 
1949
2064
  ## Read + write data via the generated SDK
1950
2065
 
@@ -1959,8 +2074,10 @@ import { db, qb } from '@dashai/generated';
1959
2074
  const rows = await db.<table>.list();
1960
2075
  await db.<table>.create({ /* fields */ });
1961
2076
 
1962
- // Query builder (joins, aggregates)
1963
- const stats = await qb.from('<table>').where({ /* ... */ }).execute();
2077
+ // Query builder \u2014 a per-table getter (NOT a \`.from()\` call); \`.where()\`
2078
+ // takes a single clause object (field \u2192 value or field \u2192 operator), and
2079
+ // \`.execute()\` runs it. Chain \`.orderBy()\` / \`.limit()\` / \`.count()\` etc.
2080
+ const open = await qb.<table>.where({ status: { equal: 'open' } }).execute();
1964
2081
  \`\`\`
1965
2082
 
1966
2083
  The data plane authenticates via the per-module scoped \`dwk_\` API key
@@ -2006,7 +2123,37 @@ In Server Components: \`await getServerSession()\` (from
2006
2123
  In Client Components: \`useSession()\` (from
2007
2124
  \`@dashai/sdk/auth/client\`), wrapped in \`<AuthProvider>\` (already
2008
2125
  in \`app/layout.tsx\`).
2009
-
2126
+ ${isCustom ? `
2127
+ ## Design & home
2128
+
2129
+ You OWN the UI \u2014 layout, components, and colors. Edit the theme tokens in
2130
+ \`app/globals.css\` freely (they're a starting point, not a mandate), reach
2131
+ for any Tailwind color (\`bg-indigo-600\`, \`text-emerald-500\`, \u2026), and pull
2132
+ in more shadcn blocks (see the README's "Adding UI blocks").
2133
+
2134
+ The app's **home is \`app/page.tsx\`, served at \`/\`** \u2014 build the real app
2135
+ there. It's auth-gated: a platform user lands on it zero-click (the
2136
+ middleware's \`autoSignIn\` signs them in silently), and \`app/dashboard/page.tsx\`
2137
+ is just an extra example route. A manual "Sign in with DashWise" landing
2138
+ page belongs ONLY to a \`public_access_allowed\` module that wants an
2139
+ anonymous entry point (re-add \`'/'\` to \`middleware.ts\` publicPaths + drop
2140
+ the home's session guard to build one).
2141
+
2142
+ ## Before you publish
2143
+
2144
+ Add \`export const dynamic = 'force-dynamic'\` to the top of ANY page that
2145
+ reads the session or data at render (calls \`getServerSession()\` or a
2146
+ \`dwk_\`-authenticated data call). Without it, \`next build\` statically
2147
+ prerenders the page with NO DashWise env vars, the SDK throws
2148
+ \`missing apiBaseUrl\`, and the platform's first publish fails with an
2149
+ opaque \`BUILD_FAILED\`. The scaffolded \`app/page.tsx\` + \`app/dashboard/page.tsx\`
2150
+ already have it \u2014 any new session-gated page you add needs it too.
2151
+
2152
+ A passing local \`npm run build\` is NOT proof: your \`.env.local\` supplies
2153
+ the env the platform build won't have, so it masks the failure. \`GET\` route
2154
+ handlers under \`app/api/*\` are dynamic by default in Next 15, so they don't
2155
+ hit this trap.
2156
+ ` : ""}
2010
2157
  ## Local dev lifecycle
2011
2158
 
2012
2159
  \`\`\`
@@ -5200,6 +5347,386 @@ async function moduleDiffCommand(options) {
5200
5347
  // src/bin.ts
5201
5348
  init_init();
5202
5349
 
5350
+ // src/commands/module/install.ts
5351
+ init_api_client();
5352
+ init_config();
5353
+ init_manifest_io();
5354
+
5355
+ // src/lib/module-lifecycle.ts
5356
+ init_api_client();
5357
+ init_output();
5358
+ init_workspaces();
5359
+ function isDevInstallKind(kind) {
5360
+ return kind === "local" || kind === "sandbox";
5361
+ }
5362
+ function isRealRuntimeUrl(url) {
5363
+ return !!url && /^https:\/\//i.test(url);
5364
+ }
5365
+ function isDraftVersion(v) {
5366
+ return !v || v.includes("draft");
5367
+ }
5368
+ function compareSemver(a, b) {
5369
+ const parse = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
5370
+ const pa = parse(a);
5371
+ const pb = parse(b);
5372
+ for (let i = 0; i < 3; i++) {
5373
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
5374
+ if (d !== 0) return d > 0 ? 1 : -1;
5375
+ }
5376
+ return 0;
5377
+ }
5378
+ function isUpdateAvailable(installedVersion, latestVersion) {
5379
+ return !!installedVersion && !!latestVersion && !isDraftVersion(installedVersion) && !isDraftVersion(latestVersion) && compareSemver(latestVersion, installedVersion) > 0;
5380
+ }
5381
+ async function resolveWorkspaceWithFallback(opts) {
5382
+ const fromCache = opts.userFlag === void 0 && opts.cachedWorkspaceId !== void 0;
5383
+ const flag = opts.userFlag ?? (fromCache ? String(opts.cachedWorkspaceId) : opts.configDefault);
5384
+ try {
5385
+ return await selectWorkspace({
5386
+ apiUrl: opts.apiUrl,
5387
+ apiToken: opts.apiToken,
5388
+ ...flag !== void 0 ? { flag } : {},
5389
+ interactive: true
5390
+ });
5391
+ } catch (err) {
5392
+ if (!fromCache || err instanceof ApiError) throw err;
5393
+ info(
5394
+ `Cached dev-install workspace ${code(String(opts.cachedWorkspaceId))} is no longer accessible \u2014 pick a workspace.`
5395
+ );
5396
+ return await selectWorkspace({
5397
+ apiUrl: opts.apiUrl,
5398
+ apiToken: opts.apiToken,
5399
+ ...opts.configDefault !== void 0 ? { flag: opts.configDefault } : {},
5400
+ interactive: true
5401
+ });
5402
+ }
5403
+ }
5404
+
5405
+ // src/commands/module/install.ts
5406
+ init_output();
5407
+ async function moduleInstallCommand(slugArg, options) {
5408
+ const slug = resolveSlug(slugArg, options.dir);
5409
+ if (!slug) {
5410
+ fail(
5411
+ `No module slug \u2014 pass one (${code("dashwise module install <slug>")}) or run from a directory with a valid module.json.`
5412
+ );
5413
+ return 1;
5414
+ }
5415
+ if (!isLoggedIn()) {
5416
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
5417
+ return 1;
5418
+ }
5419
+ const cfg = readConfig();
5420
+ if (!cfg) {
5421
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
5422
+ return 1;
5423
+ }
5424
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
5425
+ let mod;
5426
+ try {
5427
+ mod = await getModuleBySlug(slug, auth);
5428
+ } catch (err) {
5429
+ if (err instanceof ApiError && err.status === 404) {
5430
+ fail(
5431
+ `Module ${code(slug)} not found on the backend. Run ${code("dashwise module register")} + ${code("dashwise module publish")} first.`
5432
+ );
5433
+ return 1;
5434
+ }
5435
+ fail(`Could not resolve module ${code(slug)}: ${err.message}`);
5436
+ return 1;
5437
+ }
5438
+ if (!mod.current_version) {
5439
+ fail(
5440
+ `Module ${code(slug)} has no published version yet \u2014 nothing to install. Run ${code("dashwise module publish")} first.`
5441
+ );
5442
+ return 1;
5443
+ }
5444
+ const targetVersion = options.version ?? mod.current_version;
5445
+ let workspace2;
5446
+ try {
5447
+ workspace2 = await resolveWorkspaceWithFallback({
5448
+ userFlag: options.workspace,
5449
+ cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
5450
+ configDefault: cfg.workspaceSlug,
5451
+ apiUrl: resolveApiUrl(),
5452
+ apiToken: cfg.token
5453
+ });
5454
+ } catch (err) {
5455
+ fail(`Workspace selection failed: ${err.message}`);
5456
+ return 1;
5457
+ }
5458
+ let existing;
5459
+ try {
5460
+ const installs = await listWorkspaceInstallations(workspace2.id, auth);
5461
+ existing = installs.find((row2) => row2.module_id === mod.id);
5462
+ } catch (err) {
5463
+ fail(
5464
+ `Could not read workspace installations: ${err.message}`
5465
+ );
5466
+ return 1;
5467
+ }
5468
+ let isUpgrade = false;
5469
+ let devInstallToReplace;
5470
+ let confirmedViaPicker = false;
5471
+ if (existing && isDevInstallKind(existing.kind)) {
5472
+ const resolution = await resolveDevInstallCollision(
5473
+ existing,
5474
+ slug,
5475
+ workspace2.slug,
5476
+ options
5477
+ );
5478
+ if (typeof resolution === "number") return resolution;
5479
+ devInstallToReplace = existing;
5480
+ confirmedViaPicker = resolution === "replace-confirmed";
5481
+ existing = void 0;
5482
+ } else if (existing) {
5483
+ isUpgrade = true;
5484
+ const installedVersion2 = existing.module_version?.version;
5485
+ if (installedVersion2 && installedVersion2 === targetVersion) {
5486
+ info(
5487
+ `Already deployed at ${code(`v${installedVersion2}`)} \u2014 re-running is idempotent and repairs a failed provision.`
5488
+ );
5489
+ } else {
5490
+ info(
5491
+ `Existing production install #${existing.id}${installedVersion2 ? ` at ${code(`v${installedVersion2}`)}` : ""} \u2014 this will upgrade it to ${code(`v${targetVersion}`)}.`
5492
+ );
5493
+ }
5494
+ }
5495
+ if (!options.yes && process.stdout.isTTY && !confirmedViaPicker) {
5496
+ const message = devInstallToReplace ? `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)}?`;
5497
+ const answer = await select({
5498
+ message,
5499
+ options: [
5500
+ {
5501
+ value: "go",
5502
+ label: devInstallToReplace ? "Yes, replace the dev install + production-install" : isUpgrade ? "Yes, upgrade now" : "Yes, install now"
5503
+ },
5504
+ { value: "cancel", label: "No, abort" }
5505
+ ],
5506
+ initialValue: devInstallToReplace ? "cancel" : "go"
5507
+ });
5508
+ if (isCancel(answer) || answer === "cancel") {
5509
+ fail("Aborted. Nothing was changed.");
5510
+ return 130;
5511
+ }
5512
+ }
5513
+ let devInstallReplaced = false;
5514
+ if (devInstallToReplace) {
5515
+ const destroyed = await replaceDevInstall(devInstallToReplace, slug, auth);
5516
+ if (destroyed !== 0) return destroyed;
5517
+ devInstallReplaced = true;
5518
+ }
5519
+ info("Provisioning the runtime \u2014 this can take a minute\u2026");
5520
+ let result;
5521
+ try {
5522
+ result = await installModule(
5523
+ mod.id,
5524
+ {
5525
+ workspaceId: workspace2.id,
5526
+ ...options.version !== void 0 ? { version: options.version } : {},
5527
+ ...options.force !== void 0 ? { force: options.force } : {}
5528
+ },
5529
+ auth
5530
+ );
5531
+ } catch (err) {
5532
+ return surfaceInstallError(err, mod.slug, workspace2.slug, devInstallReplaced);
5533
+ }
5534
+ if (isInstallGated(result)) {
5535
+ if (options.json) {
5536
+ out(JSON.stringify(result));
5537
+ return 0;
5538
+ }
5539
+ success(
5540
+ `Install request ${code(`#${result.install_request.id}`)} queued (status: ${result.install_request.status}).`
5541
+ );
5542
+ info(
5543
+ `This module requires author approval before it installs. Once approved, check with ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)}.`
5544
+ );
5545
+ return 0;
5546
+ }
5547
+ let row;
5548
+ let verified = false;
5549
+ try {
5550
+ const after = await listWorkspaceInstallations(workspace2.id, auth);
5551
+ row = after.find((r) => r.module_id === mod.id);
5552
+ verified = row !== void 0;
5553
+ } catch {
5554
+ }
5555
+ const publicUrl = row?.public_url ?? null;
5556
+ const installedVersion = row?.module_version?.version ?? targetVersion;
5557
+ const runtimeProvisioned = isRealRuntimeUrl(row?.runtime_url);
5558
+ if (options.json) {
5559
+ out(
5560
+ JSON.stringify({
5561
+ installation_id: result.installation.id,
5562
+ kind: row?.kind ?? null,
5563
+ version: installedVersion,
5564
+ was_upgrade: result.was_upgrade,
5565
+ public_url: publicUrl,
5566
+ verified
5567
+ })
5568
+ );
5569
+ return 0;
5570
+ }
5571
+ if (!verified) {
5572
+ success(
5573
+ `Installed ${code(mod.slug)} ${code(`v${targetVersion}`)} (install ${code(`#${result.installation.id}`)}).`
5574
+ );
5575
+ info(
5576
+ `The verification read failed \u2014 run ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)} to see the deploy state and public URL.`
5577
+ );
5578
+ return 0;
5579
+ }
5580
+ if (runtimeProvisioned && publicUrl) {
5581
+ success(
5582
+ `Deployed ${code(mod.slug)} ${code(`v${installedVersion}`)} \u2014 live at ${code(publicUrl)}`
5583
+ );
5584
+ info(
5585
+ `Install ${code(`#${result.installation.id}`)} (kind=${row?.kind ?? "unknown"}) in workspace ${code(workspace2.slug)}${result.was_upgrade ? " \u2014 upgraded in place." : "."}`
5586
+ );
5587
+ } else if (runtimeProvisioned) {
5588
+ success(
5589
+ `Installed ${code(mod.slug)} ${code(`v${installedVersion}`)} (install ${code(`#${result.installation.id}`)}, kind=${row?.kind ?? "unknown"}).`
5590
+ );
5591
+ warn(
5592
+ `The runtime is provisioned, but this platform exposes no public host \u2014 there is no public URL to print. Ask the operator to configure the public module host; re-running install won't change this.`
5593
+ );
5594
+ } else {
5595
+ success(
5596
+ `Installed ${code(mod.slug)} ${code(`v${installedVersion}`)} (install ${code(`#${result.installation.id}`)}, kind=${row?.kind ?? "unknown"}).`
5597
+ );
5598
+ warn(
5599
+ `No public URL yet \u2014 runtime provisioning didn't complete. Re-running ${code("dashwise module install")} is idempotent and repairs a failed provision.`
5600
+ );
5601
+ }
5602
+ return 0;
5603
+ }
5604
+ function resolveSlug(slugArg, dir) {
5605
+ if (slugArg) return slugArg;
5606
+ try {
5607
+ const manifest = readManifest(dir);
5608
+ const slug = manifest?.module?.slug;
5609
+ return typeof slug === "string" && slug.length > 0 ? slug : void 0;
5610
+ } catch {
5611
+ return void 0;
5612
+ }
5613
+ }
5614
+ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options) {
5615
+ if (existing.kind === "sandbox") {
5616
+ fail(
5617
+ `Workspace ${code(workspaceSlug)} already has a ${code("kind=sandbox")} install of ${code(slug)} (id=${existing.id}), and only ONE install per (workspace, module) can exist.
5618
+ A plain install would upgrade that row in place \u2014 but ${code("kind")} is immutable, so it would stay sandbox and hidden from the app sidebar.
5619
+ The CLI can't destroy a sandbox install. Uninstall it from the dashboard first, or deploy with ${code("--workspace <other>")} to a separate workspace.`
5620
+ );
5621
+ return 1;
5622
+ }
5623
+ const explanation = `Workspace ${code(workspaceSlug)} already has the DEV install of ${code(slug)} (kind=local, id=${existing.id}) created by ${code("dashwise init")}.
5624
+ Only ONE install per (workspace, module) can exist, and ${code("kind")} is immutable: a plain install would upgrade that row in place \u2014 the runtime would provision, but it would STAY a dev install, hidden from the app sidebar.
5625
+ Options:
5626
+ - ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done. Note: this revokes the module's cached local-dev credentials, so local ${code("dashwise dev")} against this workspace needs a fresh ${code("dashwise init --force")} afterwards (which in turn cannot coexist with the production install here).
5627
+ - ${code("--workspace <other>")} \u2014 deploy to a separate test/client workspace and keep this dev install. Recommended for ongoing development.`;
5628
+ if (options.replaceDevInstall) return "replace";
5629
+ if (process.stdout.isTTY && !options.yes) {
5630
+ info(explanation);
5631
+ const answer = await select({
5632
+ message: `Replace the dev install of ${code(slug)} in ${code(workspaceSlug)} with a production install? (revokes local-dev credentials)`,
5633
+ options: [
5634
+ {
5635
+ value: "replace",
5636
+ label: "Replace \u2014 destroy the dev install, then production-install",
5637
+ hint: "local `dashwise dev` here will need `dashwise init --force` afterwards"
5638
+ },
5639
+ { value: "cancel", label: "Cancel \u2014 I will deploy to another workspace" }
5640
+ ],
5641
+ initialValue: "cancel"
5642
+ });
5643
+ if (isCancel(answer) || answer === "cancel") {
5644
+ fail("Aborted. Nothing was changed.");
5645
+ return 130;
5646
+ }
5647
+ return "replace-confirmed";
5648
+ }
5649
+ fail(explanation);
5650
+ return 1;
5651
+ }
5652
+ async function replaceDevInstall(existing, slug, auth) {
5653
+ info(`Destroying dev install ${code(`#${existing.id}`)} (kind=local)\u2026`);
5654
+ try {
5655
+ await destroyDevInstallation(existing.id, auth);
5656
+ } catch (err) {
5657
+ if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
5658
+ warn("Dev install already gone server-side \u2014 continuing.");
5659
+ } else if (err instanceof ApiError) {
5660
+ fail(
5661
+ `Could not destroy the dev install (${err.code} ${err.status}): ${err.message}`
5662
+ );
5663
+ return 1;
5664
+ } else {
5665
+ fail(`Could not destroy the dev install: ${err.message}`);
5666
+ return 1;
5667
+ }
5668
+ }
5669
+ const entry = readModuleEntry(slug);
5670
+ if (entry && entry.installationId === existing.id) {
5671
+ try {
5672
+ removeModuleEntry(slug);
5673
+ info(
5674
+ `Removed ${code(slug)} from auth.json \u2014 its local-dev credentials are revoked. Local ${code("dashwise dev")} needs ${code("dashwise init --force")} (ideally against a different workspace).`
5675
+ );
5676
+ } catch (err) {
5677
+ warn(
5678
+ `Dev install destroyed but auth.json cleanup failed: ${err.message}. Remove the ${code(`modules["${slug}"]`)} entry from ${code("~/.config/dashwise/auth.json")} manually.`
5679
+ );
5680
+ }
5681
+ }
5682
+ return 0;
5683
+ }
5684
+ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
5685
+ if (err instanceof ApiError) {
5686
+ switch (err.code) {
5687
+ case "INSTALL_NOT_INVITED":
5688
+ fail(
5689
+ `Module ${code(slug)} is invite-only and workspace ${code(workspaceSlug)} hasn't been invited.
5690
+ Ask the module's author to invite this workspace (or open the install gate), then retry.`
5691
+ );
5692
+ break;
5693
+ case "UPGRADE_BREAKS_CONSUMERS":
5694
+ fail(
5695
+ `Upgrading ${code(slug)} would break modules that consume its current schema.
5696
+ Review the breaking changes, then re-run with ${code("--force")} to upgrade anyway.`
5697
+ );
5698
+ break;
5699
+ case "FORBIDDEN":
5700
+ fail(
5701
+ `You must be an ADMIN of workspace ${code(workspaceSlug)} to install modules into it.`
5702
+ );
5703
+ break;
5704
+ case "UNAUTHENTICATED":
5705
+ fail(
5706
+ `Auth rejected by the backend \u2014 your CLI token may be stale. Re-run ${code("dashwise login")} and try again.`
5707
+ );
5708
+ break;
5709
+ default:
5710
+ if (err.status === 400 && /publish/i.test(err.message)) {
5711
+ fail(
5712
+ `${err.message}
5713
+ Run ${code("dashwise module publish")} first, then retry.`
5714
+ );
5715
+ } else {
5716
+ fail(`Install failed (${err.code || err.status}): ${err.message}`);
5717
+ }
5718
+ }
5719
+ } else {
5720
+ fail(`Install failed: ${err.message}`);
5721
+ }
5722
+ if (devInstallReplaced) {
5723
+ warn(
5724
+ `Note: the dev install was already removed (that is not rolled back). Re-run ${code("dashwise module install")} to complete the production install, or ${code("dashwise init --force")} to restore local dev.`
5725
+ );
5726
+ }
5727
+ return 1;
5728
+ }
5729
+
5203
5730
  // src/commands/module/list.ts
5204
5731
  init_api_client();
5205
5732
  init_config();
@@ -5668,6 +6195,140 @@ function formatBytes2(n) {
5668
6195
  return `${(n / 1024 / 1024).toFixed(1)}MB`;
5669
6196
  }
5670
6197
 
6198
+ // src/commands/module/status.ts
6199
+ init_api_client();
6200
+ init_config();
6201
+ init_manifest_io();
6202
+ init_output();
6203
+ async function moduleStatusCommand(slugArg, options) {
6204
+ let slug = slugArg;
6205
+ if (!slug) {
6206
+ try {
6207
+ const manifest = readManifest(options.dir);
6208
+ const fromManifest = manifest?.module?.slug;
6209
+ if (typeof fromManifest === "string" && fromManifest.length > 0) {
6210
+ slug = fromManifest;
6211
+ }
6212
+ } catch {
6213
+ }
6214
+ }
6215
+ if (!slug) {
6216
+ fail(
6217
+ `No module slug \u2014 pass one (${code("dashwise module status <slug>")}) or run from a directory with a valid module.json.`
6218
+ );
6219
+ return 1;
6220
+ }
6221
+ if (!isLoggedIn()) {
6222
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
6223
+ return 1;
6224
+ }
6225
+ const cfg = readConfig();
6226
+ if (!cfg) {
6227
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
6228
+ return 1;
6229
+ }
6230
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
6231
+ let mod;
6232
+ try {
6233
+ mod = await getModuleBySlug(slug, auth);
6234
+ } catch (err) {
6235
+ if (err instanceof ApiError && err.status === 404) {
6236
+ fail(`Module ${code(slug)} not found on the backend.`);
6237
+ return 1;
6238
+ }
6239
+ fail(`Could not resolve module ${code(slug)}: ${err.message}`);
6240
+ return 1;
6241
+ }
6242
+ let workspace2;
6243
+ try {
6244
+ workspace2 = await resolveWorkspaceWithFallback({
6245
+ userFlag: options.workspace,
6246
+ cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
6247
+ configDefault: cfg.workspaceSlug,
6248
+ apiUrl: resolveApiUrl(),
6249
+ apiToken: cfg.token
6250
+ });
6251
+ } catch (err) {
6252
+ fail(`Workspace selection failed: ${err.message}`);
6253
+ return 1;
6254
+ }
6255
+ let row;
6256
+ try {
6257
+ const installs = await listWorkspaceInstallations(workspace2.id, auth);
6258
+ row = installs.find((r) => r.module_id === mod.id);
6259
+ } catch (err) {
6260
+ fail(`Could not read workspace installations: ${err.message}`);
6261
+ return 1;
6262
+ }
6263
+ const latestVersion = row?.module?.current_version ?? mod.current_version;
6264
+ if (!row) {
6265
+ if (options.json) {
6266
+ out(
6267
+ JSON.stringify({
6268
+ installed: false,
6269
+ module: mod.slug,
6270
+ workspace: { id: workspace2.id, slug: workspace2.slug },
6271
+ latest_version: latestVersion
6272
+ })
6273
+ );
6274
+ return 0;
6275
+ }
6276
+ info(
6277
+ `${code(mod.slug)} is not installed in workspace ${code(workspace2.slug)}.` + (latestVersion ? ` Deploy it with ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}.` : ` It has no published version yet \u2014 run ${code("dashwise module publish")} first.`)
6278
+ );
6279
+ return 0;
6280
+ }
6281
+ const runtimeProvisioned = isRealRuntimeUrl(row.runtime_url);
6282
+ const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
6283
+ const installedVersion = row.module_version?.version ?? null;
6284
+ const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
6285
+ if (options.json) {
6286
+ out(
6287
+ JSON.stringify({
6288
+ installed: true,
6289
+ module: mod.slug,
6290
+ workspace: { id: workspace2.id, slug: workspace2.slug },
6291
+ installation_id: row.id,
6292
+ kind: row.kind ?? null,
6293
+ version: installedVersion,
6294
+ latest_version: latestVersion,
6295
+ update_available: updateAvailable,
6296
+ runtime_provisioned: runtimeProvisioned,
6297
+ runtime_url: row.runtime_url,
6298
+ public_url: row.public_url,
6299
+ state
6300
+ })
6301
+ );
6302
+ return 0;
6303
+ }
6304
+ const stateLabel = state === "live" ? "Live" : state === "deployed" ? "Deployed" : "Not deployed \u2014 runtime not provisioned";
6305
+ out("");
6306
+ kv("Module:", `${mod.slug} ${dim(`(id ${mod.id})`)}`);
6307
+ kv("Workspace:", `${workspace2.slug} ${dim(`(id ${workspace2.id})`)}`);
6308
+ kv("Install:", `#${row.id} ${dim(`(kind=${row.kind ?? "unknown"})`)}`);
6309
+ kv(
6310
+ "Version:",
6311
+ `${installedVersion ?? dim("(unknown)")}${updateAvailable ? ` ${code(`update available: v${latestVersion} \u2014 run \`dashwise module install ${mod.slug}\``)}` : latestVersion ? ` ${dim(`(latest published: v${latestVersion})`)}` : ""}`
6312
+ );
6313
+ kv(
6314
+ "Runtime:",
6315
+ runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
6316
+ );
6317
+ kv("Public URL:", row.public_url ?? dim("(none)"));
6318
+ kv("State:", stateLabel);
6319
+ out("");
6320
+ if (row.kind === "local") {
6321
+ info(
6322
+ `This is the ${code("kind=local")} dev install \u2014 hidden from the app sidebar. A production deploy needs ${code("dashwise module install --replace-dev-install")} (or ${code("--workspace <other>")}).`
6323
+ );
6324
+ } else if (state === "not_deployed") {
6325
+ info(
6326
+ `Re-run ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)} to (re-)provision the runtime \u2014 the install POST is idempotent.`
6327
+ );
6328
+ }
6329
+ return 0;
6330
+ }
6331
+
5671
6332
  // src/commands/module/pull-data.ts
5672
6333
  init_api_client();
5673
6334
  init_output();
@@ -6289,7 +6950,7 @@ function rangeSatisfied(version, range) {
6289
6950
  const op = opMatch[1] ?? "=";
6290
6951
  const target = parseSemver(opMatch[2]);
6291
6952
  if (!target) return false;
6292
- const cmp = compareSemver(vParts, target);
6953
+ const cmp = compareSemver2(vParts, target);
6293
6954
  switch (op) {
6294
6955
  case "=":
6295
6956
  return cmp === 0;
@@ -6318,7 +6979,7 @@ function parseSemver(s) {
6318
6979
  if (!m) return null;
6319
6980
  return [Number(m[1]), Number(m[2]), Number(m[3])];
6320
6981
  }
6321
- function compareSemver(a, b) {
6982
+ function compareSemver2(a, b) {
6322
6983
  for (let i = 0; i < 3; i++) {
6323
6984
  if (a[i] !== b[i]) return a[i] - b[i];
6324
6985
  }
@@ -9204,6 +9865,35 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
9204
9865
  });
9205
9866
  }
9206
9867
  );
9868
+ moduleCmd.command("install [slug]").alias("deploy").description(
9869
+ "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently. Refuses to silently land on a dev install: use --replace-dev-install or --workspace <other>."
9870
+ ).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(
9871
+ "--replace-dev-install",
9872
+ "Destroy the workspace's kind=local dev install first, then fresh production install. Revokes the module's cached local-dev credentials (re-run `dashwise init --force` for local dev afterwards)."
9873
+ ).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option("--json", "Emit the final result as machine-readable JSON.").action(
9874
+ async (slug, cmdOpts) => {
9875
+ process.exitCode = await moduleInstallCommand(slug, {
9876
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
9877
+ ...cmdOpts.version !== void 0 ? { version: cmdOpts.version } : {},
9878
+ ...cmdOpts.force !== void 0 ? { force: cmdOpts.force } : {},
9879
+ ...cmdOpts.replaceDevInstall !== void 0 ? { replaceDevInstall: cmdOpts.replaceDevInstall } : {},
9880
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
9881
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
9882
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
9883
+ });
9884
+ }
9885
+ );
9886
+ moduleCmd.command("status [slug]").description(
9887
+ "Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
9888
+ ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--json", "Emit machine-readable JSON.").action(
9889
+ async (slug, cmdOpts) => {
9890
+ process.exitCode = await moduleStatusCommand(slug, {
9891
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
9892
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
9893
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
9894
+ });
9895
+ }
9896
+ );
9207
9897
  moduleCmd.command("query-log").description(
9208
9898
  "Read the per-module SDK query log. Default: 50 most recent entries. `--slow` filters to duration_ms > 500ms; `--since 1h` narrows by time; `--tail` streams new entries every 5s."
9209
9899
  ).option(
@@ -9381,7 +10071,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
9381
10071
  }
9382
10072
  })();
9383
10073
  function getVersion() {
9384
- return "0.12.1";
10074
+ return "0.14.0";
9385
10075
  }
9386
10076
  function parseIntOption(value) {
9387
10077
  const n = Number.parseInt(value, 10);