@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 +789 -99
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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
|
|
850
|
-
- **\`app/dashboard/page.tsx\`** \u2014 Server Component
|
|
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
|
-
//
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
|
|
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
|
|
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
|
|
938
|
-
//
|
|
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 `//
|
|
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
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
//
|
|
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
|
-
//
|
|
991
|
-
//
|
|
992
|
-
// (
|
|
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 {
|
|
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
|
-
|
|
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
|
|
1007
|
-
|
|
1008
|
-
|
|
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-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
|
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
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
<
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
*
|
|
1078
|
-
*
|
|
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=/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
--
|
|
1548
|
-
--
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
--
|
|
1552
|
-
--
|
|
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.
|
|
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
|
-
|
|
1575
|
-
|
|
1576
|
-
--
|
|
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
|
|
1579
|
-
--muted-foreground: oklch(0.71 0
|
|
1580
|
-
--accent: oklch(0.
|
|
1581
|
-
--accent-foreground: oklch(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.
|
|
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=/
|
|
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
|
|
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 (
|
|
1963
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
10074
|
+
return "0.14.0";
|
|
9385
10075
|
}
|
|
9386
10076
|
function parseIntOption(value) {
|
|
9387
10077
|
const n = Number.parseInt(value, 10);
|