@intelligo-dev/cli 1.0.0-beta.1 → 1.0.0-beta.13
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/NOTICE +6 -0
- package/README.md +63 -0
- package/dist/bin.js +85 -37
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts +22 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +61 -4
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create-flow.d.ts +34 -0
- package/dist/commands/create-flow.d.ts.map +1 -0
- package/dist/commands/create-flow.js +180 -0
- package/dist/commands/create-flow.js.map +1 -0
- package/dist/commands/create.d.ts +23 -6
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +45 -13
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.d.ts +14 -3
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +293 -37
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/migrate-check.d.ts +54 -1
- package/dist/commands/migrate-check.d.ts.map +1 -1
- package/dist/commands/migrate-check.js +101 -5
- package/dist/commands/migrate-check.js.map +1 -1
- package/dist/commands/migrate.d.ts +97 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +147 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/upgrade-check.d.ts +5 -5
- package/dist/commands/upgrade-check.d.ts.map +1 -1
- package/dist/commands/upgrade-check.js +19 -6
- package/dist/commands/upgrade-check.js.map +1 -1
- package/dist/env-files.d.ts +12 -0
- package/dist/env-files.d.ts.map +1 -0
- package/dist/env-files.js +29 -0
- package/dist/env-files.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +9 -15
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +5 -16
- package/dist/manifest.js.map +1 -1
- package/dist/migrations.d.ts +3 -6
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +3 -6
- package/dist/migrations.js.map +1 -1
- package/dist/model-catalogue.d.ts +14 -0
- package/dist/model-catalogue.d.ts.map +1 -0
- package/dist/model-catalogue.js +34 -0
- package/dist/model-catalogue.js.map +1 -0
- package/dist/registry-items.d.ts +56 -0
- package/dist/registry-items.d.ts.map +1 -0
- package/dist/registry-items.js +103 -0
- package/dist/registry-items.js.map +1 -0
- package/package.json +36 -10
- package/src/bin.ts +264 -0
- package/src/commands/add.ts +244 -0
- package/src/commands/create-flow.ts +228 -0
- package/src/commands/create.ts +139 -0
- package/src/commands/doctor.ts +487 -0
- package/src/commands/migrate-check.ts +257 -0
- package/src/commands/migrate.ts +234 -0
- package/src/commands/upgrade-check.ts +157 -0
- package/src/env-files.ts +31 -0
- package/src/index.ts +39 -0
- package/src/manifest.ts +93 -0
- package/src/migrations-dir.ts +25 -0
- package/src/migrations.ts +133 -0
- package/src/model-catalogue.ts +35 -0
- package/src/registry-items.ts +132 -0
- package/templates/admin-page/admin-page.tsx.tpl +2 -3
- package/templates/app-scaffold/assistant-route.ts.tpl +2 -2
- package/templates/app-scaffold/auth-route.ts.tpl +10 -0
- package/templates/app-scaffold/components.json.tpl +6 -2
- package/templates/app-scaffold/db-schema.ts.tpl +29 -0
- package/templates/app-scaffold/drizzle-journal.json.tpl +5 -0
- package/templates/app-scaffold/drizzle.config.ts.tpl +45 -0
- package/templates/app-scaffold/env.example.tpl +65 -1
- package/templates/app-scaffold/globals.css.tpl +172 -281
- package/templates/app-scaffold/i18n-request.ts.tpl +25 -0
- package/templates/app-scaffold/instrumentation.ts.tpl +21 -0
- package/templates/app-scaffold/intelligo.ts.tpl +104 -9
- package/templates/app-scaffold/layout.tsx.tpl +18 -1
- package/templates/app-scaffold/lib-utils.ts.tpl +1 -6
- package/templates/app-scaffold/next.config.mjs.tpl +2 -0
- package/templates/app-scaffold/package.json.tpl +31 -17
- package/templates/app-scaffold/page.tsx.tpl +16 -0
- package/templates/app-scaffold/plans.ts.tpl +32 -13
- package/templates/app-scaffold/{middleware.ts.tpl → proxy.ts.tpl} +6 -0
- package/templates/app-scaffold/sonner.tsx.tpl +42 -20
- package/templates/app-scaffold/stripe-webhook-route.ts.tpl +24 -0
- package/templates/app-scaffold/theme-provider.tsx.tpl +6 -4
- package/templates/app-scaffold/tsconfig.json.tpl +20 -4
- package/templates/app-scaffold/use-mobile.ts.tpl +19 -0
- package/templates/app-scaffold/workspace-bootstrap.ts.tpl +30 -0
- package/templates/maintenance/maintenance-route.ts.tpl +119 -0
- package/templates/manifest.json +57 -18
- package/templates/registry-items.json +116 -0
- package/templates/registry-requires.json +156 -0
- package/templates/usage-page/usage-page.tsx.tpl +23 -8
- package/templates/billing-page/billing-page.tsx.tpl +0 -72
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Your composition root
|
|
4
|
+
* Your composition root.
|
|
5
5
|
*
|
|
6
6
|
* This file is yours — Intelligo generated it once and will not
|
|
7
7
|
* overwrite it. It is the single place the application wires itself
|
|
@@ -14,18 +14,33 @@ import "server-only";
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
ensureBillingSettingsRow,
|
|
18
|
+
ensurePlanRows,
|
|
19
|
+
reserveQuota,
|
|
18
20
|
recordTokenUsage,
|
|
19
21
|
releaseReservation,
|
|
22
|
+
findSettlementByRequestId,
|
|
20
23
|
} from "@intelligo-dev/billing";
|
|
24
|
+
import { DEFAULT_MARGIN_BP } from "@intelligo-dev/executions/pricing";
|
|
21
25
|
import {
|
|
22
26
|
registerProductFeatures,
|
|
23
27
|
registerProductPlans,
|
|
28
|
+
registerTeamMemberLimits,
|
|
24
29
|
setDefaultProductSlug,
|
|
25
30
|
} from "@intelligo-dev/billing/plans";
|
|
26
|
-
import {
|
|
31
|
+
import { setWorkspaceCreatedHandler } from "@intelligo-dev/auth";
|
|
32
|
+
import { assertEnv } from "@intelligo-dev/core/env";
|
|
33
|
+
import { setRequestContextSource } from "@intelligo-dev/core/request-context";
|
|
34
|
+
import {
|
|
35
|
+
DEFAULT_MODELS,
|
|
36
|
+
createExecutions,
|
|
37
|
+
registerModels,
|
|
38
|
+
} from "@intelligo-dev/executions";
|
|
39
|
+
import { nextRequestContext } from "@intelligo-dev/next";
|
|
40
|
+
import { createLogger } from "@intelligo-dev/core/logger";
|
|
27
41
|
|
|
28
|
-
import { PLANS,
|
|
42
|
+
import { FEATURES, PLANS, TEAM_MEMBER_LIMITS } from "./plans";
|
|
43
|
+
import { onWorkspaceCreated } from "./workspace-bootstrap";
|
|
29
44
|
|
|
30
45
|
/** Identifies your product to the billing engine. */
|
|
31
46
|
export const PRODUCT_SLUG = "__APP_SLUG__";
|
|
@@ -38,15 +53,86 @@ export const CAPABILITIES = {
|
|
|
38
53
|
assistantMessage: "assistant.message",
|
|
39
54
|
} as const;
|
|
40
55
|
|
|
56
|
+
const log = createLogger("Intelligo");
|
|
57
|
+
|
|
41
58
|
let composed = false;
|
|
42
59
|
|
|
43
60
|
export function composeIntelligo(): void {
|
|
44
|
-
if (composed)
|
|
45
|
-
|
|
61
|
+
if (!composed) {
|
|
62
|
+
composed = true;
|
|
63
|
+
bind();
|
|
64
|
+
}
|
|
65
|
+
// Every call checks the seed, so a database that was unreachable at
|
|
66
|
+
// boot is seeded by the first request after it comes back.
|
|
67
|
+
seedIntelligo().catch((error: unknown) => {
|
|
68
|
+
log.error("Seeding the plan and billing-settings rows failed", {
|
|
69
|
+
error,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let seeding: Promise<void> | undefined;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Writes the rows the registrations imply, and resolves once they
|
|
78
|
+
* exist. Both writes are idempotent upserts, so processes that boot
|
|
79
|
+
* together are safe; callers share one promise, and a failure clears
|
|
80
|
+
* it so the next call tries again.
|
|
81
|
+
*/
|
|
82
|
+
export function seedIntelligo(): Promise<void> {
|
|
83
|
+
seeding ??= Promise.all([
|
|
84
|
+
// The plans table is a copy of the catalogue: a subscription row
|
|
85
|
+
// references its plan there, so the rows exist before the first
|
|
86
|
+
// checkout rather than being seeded by a migration.
|
|
87
|
+
ensurePlanRows(),
|
|
88
|
+
|
|
89
|
+
// What you bill in. Provider prices are USD, so a USD deployment
|
|
90
|
+
// converts at exactly 1.0; selling in another currency means stating
|
|
91
|
+
// its rate per USD here, in micros. There is no default rate — a
|
|
92
|
+
// framework that guesses one is inventing money. The row is seeded once;
|
|
93
|
+
// an existing row is left alone, because changing the currency under
|
|
94
|
+
// a ledger that holds balances is your decision, not a deploy's.
|
|
95
|
+
ensureBillingSettingsRow({
|
|
96
|
+
currency: "USD",
|
|
97
|
+
usdRateMicros: 1_000_000,
|
|
98
|
+
marginBp: DEFAULT_MARGIN_BP,
|
|
99
|
+
}),
|
|
100
|
+
]).then(
|
|
101
|
+
() => undefined,
|
|
102
|
+
(error: unknown) => {
|
|
103
|
+
seeding = undefined;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
return seeding;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function bind(): void {
|
|
111
|
+
// Fail on the first request rather than on the first query: a
|
|
112
|
+
// missing DATABASE_URL or auth secret is a configuration error, not
|
|
113
|
+
// something to discover deep inside a handler.
|
|
114
|
+
assertEnv();
|
|
115
|
+
|
|
116
|
+
// Where the framework reads the incoming request's headers from.
|
|
117
|
+
// Only this line knows the app is a Next.js one; `@intelligo-dev/auth`
|
|
118
|
+
// asks `@intelligo-dev/core/request-context` and stays usable from a
|
|
119
|
+
// worker or a test.
|
|
120
|
+
setRequestContextSource(nextRequestContext);
|
|
121
|
+
|
|
122
|
+
// What a new user's personal workspace starts with.
|
|
123
|
+
setWorkspaceCreatedHandler(onWorkspaceCreated);
|
|
46
124
|
|
|
47
125
|
setDefaultProductSlug(PRODUCT_SLUG);
|
|
48
126
|
registerProductPlans(PRODUCT_SLUG, PLANS);
|
|
49
127
|
registerProductFeatures(PRODUCT_SLUG, FEATURES);
|
|
128
|
+
registerTeamMemberLimits(PRODUCT_SLUG, TEAM_MEMBER_LIMITS);
|
|
129
|
+
|
|
130
|
+
// What each model costs. The framework ships a catalogue as data and
|
|
131
|
+
// registers none of it: an id with no registered price throws where
|
|
132
|
+
// the price is needed, rather than being guessed. Swap in your own
|
|
133
|
+
// contracted rates, or add a model the framework has never heard of,
|
|
134
|
+
// by passing your own array here.
|
|
135
|
+
registerModels(DEFAULT_MODELS);
|
|
50
136
|
}
|
|
51
137
|
|
|
52
138
|
export const executions = createExecutions({
|
|
@@ -54,17 +140,21 @@ export const executions = createExecutions({
|
|
|
54
140
|
// Passing requestId makes admission atomic: the worst-case cost is
|
|
55
141
|
// reserved in the same transaction that reads the balance, so
|
|
56
142
|
// concurrent requests cannot all pass.
|
|
57
|
-
const quota = await
|
|
143
|
+
const quota = await reserveQuota(workspaceId, { modelId: model, requestId });
|
|
58
144
|
return {
|
|
59
145
|
allowed: quota.allowed,
|
|
146
|
+
code: quota.code,
|
|
60
147
|
reason: quota.reason,
|
|
61
|
-
|
|
148
|
+
// The hold, as an amount with its currency.
|
|
149
|
+
estimated: quota.estimated,
|
|
62
150
|
usingTrialCredits: quota.usingTrialCredits,
|
|
63
151
|
};
|
|
64
152
|
},
|
|
65
153
|
|
|
66
154
|
async settleUsage(s) {
|
|
67
|
-
|
|
155
|
+
// Returns what was charged and which pool funded it; the lifecycle
|
|
156
|
+
// records the amount and its currency on the execution row.
|
|
157
|
+
return recordTokenUsage({
|
|
68
158
|
workspaceId: s.workspaceId,
|
|
69
159
|
userId: s.userId ?? "",
|
|
70
160
|
model: s.model ?? "unknown",
|
|
@@ -81,4 +171,9 @@ export const executions = createExecutions({
|
|
|
81
171
|
async releaseHold({ requestId }) {
|
|
82
172
|
await releaseReservation(requestId);
|
|
83
173
|
},
|
|
174
|
+
|
|
175
|
+
// Lets executions.reconcile() tell a settling row whose charge
|
|
176
|
+
// committed from one whose charge never happened.
|
|
177
|
+
findSettlement: ({ workspaceId, requestId }) =>
|
|
178
|
+
findSettlementByRequestId(workspaceId, requestId),
|
|
84
179
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
4
|
import { notFound } from "next/navigation";
|
|
4
5
|
import { NextIntlClientProvider } from "next-intl";
|
|
5
6
|
import { getMessages, getTranslations } from "next-intl/server";
|
|
@@ -10,6 +11,18 @@ import { ThemeProvider } from "@/components/shell/theme-provider";
|
|
|
10
11
|
import { Toaster } from "@/components/ui/sonner";
|
|
11
12
|
import { routing } from "@/i18n/routing";
|
|
12
13
|
|
|
14
|
+
// The theme names "Geist" in `--font-sans`; loading it here registers the
|
|
15
|
+
// faces under that family (and exposes the variables for a product's own
|
|
16
|
+
// use). Without it every surface falls back to the system face.
|
|
17
|
+
const geistSans = Geist({
|
|
18
|
+
subsets: ["latin", "latin-ext", "cyrillic"],
|
|
19
|
+
variable: "--font-geist-sans",
|
|
20
|
+
});
|
|
21
|
+
const geistMono = Geist_Mono({
|
|
22
|
+
subsets: ["latin", "latin-ext", "cyrillic"],
|
|
23
|
+
variable: "--font-geist-mono",
|
|
24
|
+
});
|
|
25
|
+
|
|
13
26
|
type Props = {
|
|
14
27
|
children: ReactNode;
|
|
15
28
|
params: Promise<{ locale: string }>;
|
|
@@ -39,7 +52,11 @@ export default async function RootLayout({ children, params }: Props) {
|
|
|
39
52
|
return (
|
|
40
53
|
// suppressHydrationWarning: next-themes sets the class attribute on
|
|
41
54
|
// <html> before hydration (see components/shell/theme-provider.tsx).
|
|
42
|
-
<html
|
|
55
|
+
<html
|
|
56
|
+
lang={locale}
|
|
57
|
+
className={`${geistSans.variable} ${geistMono.variable}`}
|
|
58
|
+
suppressHydrationWarning
|
|
59
|
+
>
|
|
43
60
|
<body>
|
|
44
61
|
<ThemeProvider>
|
|
45
62
|
<NextIntlClientProvider messages={messages}>
|
|
@@ -7,36 +7,50 @@
|
|
|
7
7
|
"dev": "next dev --turbopack --port 3000",
|
|
8
8
|
"build": "next build",
|
|
9
9
|
"start": "next start --port 3000",
|
|
10
|
-
"type-check": "tsc --noEmit"
|
|
10
|
+
"type-check": "tsc --noEmit",
|
|
11
|
+
"db:generate": "drizzle-kit generate",
|
|
12
|
+
"db:migrate": "intelligo migrate && drizzle-kit migrate",
|
|
13
|
+
"db:check": "intelligo migrate --check"
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
16
|
+
"@base-ui/react": "^1.8.0",
|
|
13
17
|
"@intelligo-dev/admin": "__INTELLIGO_DEP__",
|
|
14
18
|
"@intelligo-dev/audit": "__INTELLIGO_DEP__",
|
|
15
19
|
"@intelligo-dev/auth": "__INTELLIGO_DEP__",
|
|
16
20
|
"@intelligo-dev/billing": "__INTELLIGO_DEP__",
|
|
21
|
+
"@intelligo-dev/chat": "__INTELLIGO_DEP__",
|
|
17
22
|
"@intelligo-dev/core": "__INTELLIGO_DEP__",
|
|
18
23
|
"@intelligo-dev/executions": "__INTELLIGO_DEP__",
|
|
19
|
-
"@intelligo-dev/
|
|
20
|
-
"@
|
|
24
|
+
"@intelligo-dev/jobs": "__INTELLIGO_DEP__",
|
|
25
|
+
"@intelligo-dev/next": "__INTELLIGO_DEP__",
|
|
26
|
+
"ai": "^7.0.103",
|
|
27
|
+
"better-auth": "~1.6.33",
|
|
21
28
|
"class-variance-authority": "^0.7.1",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"next
|
|
29
|
+
"cn": "^0.3.0",
|
|
30
|
+
"drizzle-orm": "^0.45.2",
|
|
31
|
+
"lucide-react": "^1.46.0",
|
|
32
|
+
"next": "~16.3.5",
|
|
33
|
+
"next-intl": "^4.14.5",
|
|
26
34
|
"next-themes": "^0.4.6",
|
|
27
|
-
"react": "^19.
|
|
28
|
-
"react-dom": "^19.
|
|
35
|
+
"react": "^19.3.0",
|
|
36
|
+
"react-dom": "^19.3.0",
|
|
29
37
|
"server-only": "^0.0.1",
|
|
30
|
-
"sonner": "^2.0.
|
|
31
|
-
"
|
|
38
|
+
"sonner": "^2.0.8",
|
|
39
|
+
"stripe": "^22.6.2",
|
|
40
|
+
"zod": "^4.6.5"
|
|
32
41
|
},
|
|
33
42
|
"devDependencies": {
|
|
34
|
-
"@
|
|
35
|
-
"@
|
|
36
|
-
"@types/
|
|
37
|
-
"@types/react
|
|
38
|
-
"
|
|
43
|
+
"@intelligo-dev/cli": "__INTELLIGO_DEP__",
|
|
44
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
45
|
+
"@types/node": "^22.20.3",
|
|
46
|
+
"@types/react": "^19.3.0",
|
|
47
|
+
"@types/react-dom": "^19.3.0",
|
|
48
|
+
"dotenv": "^17.4.2",
|
|
49
|
+
"drizzle-kit": "^0.31.10",
|
|
50
|
+
"pg": "8.18.0",
|
|
51
|
+
"shadcn": "^4.21.0",
|
|
52
|
+
"tailwindcss": "^4.3.3",
|
|
39
53
|
"tw-animate-css": "^1.4.0",
|
|
40
|
-
"typescript": "^5.
|
|
54
|
+
"typescript": "^5.9.3"
|
|
41
55
|
}
|
|
42
56
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getAuthSession } from "@intelligo-dev/auth";
|
|
2
|
+
import { getLocale } from "next-intl/server";
|
|
3
|
+
|
|
4
|
+
import { redirect } from "@/i18n/navigation";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The root URL — a session decides where you land: the dashboard when
|
|
8
|
+
* signed in (the `dashboard` registry item), the login page otherwise
|
|
9
|
+
* (`auth-login`). Replace it with a landing page of your own whenever
|
|
10
|
+
* you like; it is yours.
|
|
11
|
+
*/
|
|
12
|
+
export default async function Home() {
|
|
13
|
+
const locale = await getLocale();
|
|
14
|
+
const session = await getAuthSession();
|
|
15
|
+
redirect({ href: session ? "/dashboard" : "/login", locale });
|
|
16
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Your plans and feature gates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The billing engine knows only the shape; the names, prices, and
|
|
5
|
+
* limits are yours, read through the registry your composition root
|
|
6
|
+
* populates. `monthlyAllowance` is what the credit engine enforces: an
|
|
7
|
+
* amount with its currency, which must be the one your composition root
|
|
8
|
+
* declares to `ensureBillingSettingsRow`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { fromMajor } from "@intelligo-dev/core/money";
|
|
11
12
|
import type { PlanConfig } from "@intelligo-dev/billing/plans";
|
|
12
13
|
|
|
13
14
|
export const PLANS: Record<string, PlanConfig> = {
|
|
@@ -15,29 +16,47 @@ export const PLANS: Record<string, PlanConfig> = {
|
|
|
15
16
|
name: "Free",
|
|
16
17
|
slug: "free",
|
|
17
18
|
description: "Get started",
|
|
18
|
-
descriptionMn: "",
|
|
19
19
|
priceOneTime: 0,
|
|
20
20
|
targetAudience: "Everyone",
|
|
21
21
|
aiModelLabel: "Base",
|
|
22
|
-
|
|
22
|
+
/** Half a dollar of model time a month — roughly a hundred cheap turns. */
|
|
23
|
+
monthlyAllowance: fromMajor(0.5, "USD"),
|
|
24
|
+
limits: {},
|
|
23
25
|
features: [],
|
|
24
|
-
featuresMn: [],
|
|
25
26
|
},
|
|
26
27
|
pro: {
|
|
27
28
|
name: "Pro",
|
|
28
29
|
slug: "pro",
|
|
29
30
|
description: "For daily use",
|
|
30
|
-
|
|
31
|
-
priceOneTime: 20_000,
|
|
31
|
+
priceOneTime: 20,
|
|
32
32
|
targetAudience: "Teams",
|
|
33
33
|
aiModelLabel: "Advanced",
|
|
34
|
-
|
|
34
|
+
/** $15 of model time inside a $20 plan. */
|
|
35
|
+
monthlyAllowance: fromMajor(15, "USD"),
|
|
36
|
+
limits: {},
|
|
35
37
|
features: [],
|
|
36
|
-
featuresMn: [],
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Which plans grant which feature. An unregistered feature is denied —
|
|
43
|
+
* a registry item whose `featureKey` is missing here returns 403 on
|
|
44
|
+
* every request, so add the key when you install the item.
|
|
45
|
+
*/
|
|
41
46
|
export const FEATURES: Record<string, readonly string[]> = {
|
|
42
47
|
assistant: ["free", "pro"],
|
|
48
|
+
// POST /api/chat (the `chat` registry item). Every plan, so a clean
|
|
49
|
+
// install can chat with no configuration; tighten to ["pro"] to put
|
|
50
|
+
// chat behind a paywall.
|
|
51
|
+
chat: ["free", "pro"],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Seats per plan: how many members a workspace on each plan may hold,
|
|
56
|
+
* the owner included. -1 is unlimited. A plan left out gets one seat,
|
|
57
|
+
* which refuses every invitation.
|
|
58
|
+
*/
|
|
59
|
+
export const TEAM_MEMBER_LIMITS: Record<string, number> = {
|
|
60
|
+
free: 3,
|
|
61
|
+
pro: 25,
|
|
43
62
|
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next 16 names this file `proxy.ts` (`middleware.ts` still resolves).
|
|
3
|
+
*
|
|
4
|
+
* Optimistic only: it redirects, and never touches the database. Every
|
|
5
|
+
* real check is server-side.
|
|
6
|
+
*/
|
|
1
7
|
import createIntlMiddleware from "next-intl/middleware";
|
|
2
8
|
|
|
3
9
|
import { routing } from "./i18n/routing";
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* `app/[locale]/layout.tsx`. Registry items call `toast(...)` from the
|
|
6
|
-
* `sonner` package and rely on this being mounted; without it their
|
|
7
|
-
* toasts silently never render.
|
|
8
|
-
*
|
|
9
|
-
* This is the stock shadcn `sonner` component; `pnpm dlx shadcn add
|
|
10
|
-
* sonner` would produce the same file at the same path.
|
|
11
|
-
*/
|
|
12
|
-
|
|
3
|
+
import { useTheme } from "next-themes";
|
|
4
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
|
13
5
|
import {
|
|
14
6
|
CircleCheckIcon,
|
|
15
7
|
InfoIcon,
|
|
16
|
-
Loader2Icon,
|
|
17
|
-
OctagonXIcon,
|
|
18
8
|
TriangleAlertIcon,
|
|
9
|
+
OctagonXIcon,
|
|
10
|
+
Loader2Icon,
|
|
11
|
+
XIcon,
|
|
19
12
|
} from "lucide-react";
|
|
20
|
-
import { useTheme } from "next-themes";
|
|
21
|
-
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
|
22
13
|
|
|
14
|
+
/*
|
|
15
|
+
* Sonner, drawn as a stack of rounded cards: each toast behind the front
|
|
16
|
+
* one peeks out a little lower and a little smaller, the status sits in a
|
|
17
|
+
* tinted round chip, and every move settles on a long ease-out that reads
|
|
18
|
+
* as a spring. Toasts are unstyled so the classes below own the surface;
|
|
19
|
+
* Sonner still owns stacking, swipe and timing, and its own
|
|
20
|
+
* prefers-reduced-motion rule still turns the motion off.
|
|
21
|
+
*/
|
|
23
22
|
const Toaster = ({ ...props }: ToasterProps) => {
|
|
24
23
|
const { theme = "system" } = useTheme();
|
|
25
24
|
|
|
@@ -27,21 +26,44 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|
|
27
26
|
<Sonner
|
|
28
27
|
theme={theme as ToasterProps["theme"]}
|
|
29
28
|
className="toaster group"
|
|
29
|
+
gap={10}
|
|
30
30
|
icons={{
|
|
31
|
-
success: <CircleCheckIcon className="size-4" />,
|
|
32
|
-
info: <InfoIcon className="size-4" />,
|
|
33
|
-
warning: <TriangleAlertIcon className="size-4" />,
|
|
34
|
-
error: <OctagonXIcon className="size-4" />,
|
|
35
|
-
loading: <Loader2Icon className="size-4 animate-spin" />,
|
|
31
|
+
success: <CircleCheckIcon className="size-4 text-success" />,
|
|
32
|
+
info: <InfoIcon className="size-4 text-info" />,
|
|
33
|
+
warning: <TriangleAlertIcon className="size-4 text-warning" />,
|
|
34
|
+
error: <OctagonXIcon className="size-4 text-destructive" />,
|
|
35
|
+
loading: <Loader2Icon className="size-4 animate-spin text-muted-foreground" />,
|
|
36
|
+
close: <XIcon className="size-3.5" />,
|
|
36
37
|
}}
|
|
37
38
|
style={
|
|
38
39
|
{
|
|
39
40
|
"--normal-bg": "var(--popover)",
|
|
40
41
|
"--normal-text": "var(--popover-foreground)",
|
|
41
42
|
"--normal-border": "var(--border)",
|
|
42
|
-
"--border-radius": "var(--radius)",
|
|
43
|
+
"--border-radius": "var(--radius-2xl)",
|
|
43
44
|
} as React.CSSProperties
|
|
44
45
|
}
|
|
46
|
+
toastOptions={{
|
|
47
|
+
unstyled: true,
|
|
48
|
+
style: {
|
|
49
|
+
transitionDuration: "450ms",
|
|
50
|
+
transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
51
|
+
},
|
|
52
|
+
classNames: {
|
|
53
|
+
toast:
|
|
54
|
+
"cn-toast group/toast flex w-(--width) items-center gap-3 rounded-2xl border border-border bg-popover/95 p-3 font-sans text-popover-foreground shadow-2xl backdrop-blur-xl outline-none focus-visible:ring-2 focus-visible:ring-ring has-[[data-description]]:items-start data-[expanded=false]:data-[front=false]:*:opacity-0",
|
|
55
|
+
icon: "relative grid size-7 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground group-data-[type=success]/toast:bg-success/10 group-data-[type=info]/toast:bg-info/10 group-data-[type=warning]/toast:bg-warning/10 group-data-[type=error]/toast:bg-destructive/10",
|
|
56
|
+
content: "flex min-w-0 flex-1 flex-col gap-0.5",
|
|
57
|
+
title: "text-sm leading-5 font-medium text-popover-foreground",
|
|
58
|
+
description: "line-clamp-2 text-xs leading-4 text-muted-foreground",
|
|
59
|
+
actionButton:
|
|
60
|
+
"inline-flex h-7 shrink-0 items-center rounded-full bg-primary px-3 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/88 focus-visible:ring-2 focus-visible:ring-ring outline-none",
|
|
61
|
+
cancelButton:
|
|
62
|
+
"inline-flex h-7 shrink-0 items-center rounded-full bg-muted px-3 text-xs font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring outline-none",
|
|
63
|
+
closeButton:
|
|
64
|
+
"order-last grid size-7 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring outline-none",
|
|
65
|
+
},
|
|
66
|
+
}}
|
|
45
67
|
{...props}
|
|
46
68
|
/>
|
|
47
69
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe webhook endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Point Stripe at POST /api/webhooks/stripe and set
|
|
5
|
+
* STRIPE_WEBHOOK_SECRET. The handler verifies the signature, records
|
|
6
|
+
* every event in `finance_events` before processing it, runs the
|
|
7
|
+
* built-in handlers (checkout completed, invoice paid / failed,
|
|
8
|
+
* subscription updated / deleted), and answers 500 on a handler error
|
|
9
|
+
* so Stripe retries — see `createStripeWebhookHandler` in
|
|
10
|
+
* `@intelligo-dev/billing` for the exact contract.
|
|
11
|
+
*
|
|
12
|
+
* Pass `onEvent` to react to event types the framework does not.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createStripeWebhookHandler } from "@intelligo-dev/billing";
|
|
16
|
+
|
|
17
|
+
import { composeIntelligo } from "@/lib/intelligo";
|
|
18
|
+
|
|
19
|
+
const handler = createStripeWebhookHandler();
|
|
20
|
+
|
|
21
|
+
export async function POST(request: Request) {
|
|
22
|
+
composeIntelligo();
|
|
23
|
+
return handler(request);
|
|
24
|
+
}
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Theme provider — mounted by `app/[locale]/layout.tsx` so any theme
|
|
5
5
|
* switcher (e.g. the app-shell item's user menu) actually works.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
8
|
-
* path; installing app-shell
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The `app-shell` registry item ships its own provider at this same
|
|
8
|
+
* path; installing app-shell (with --overwrite) replaces this starter
|
|
9
|
+
* copy, and the manifest will report the file as customized from then
|
|
10
|
+
* on — that is expected. Defaults: class-attribute strategy (Tailwind's
|
|
11
|
+
* `.dark` tokens), system preference honored, no transition flash on
|
|
12
|
+
* switch.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "../../tsconfig.base.json",
|
|
3
2
|
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"target": "ES2020",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"incremental": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
4
14
|
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
-
"jsx": "
|
|
15
|
+
"jsx": "react-jsx",
|
|
6
16
|
"noEmit": true,
|
|
7
17
|
"plugins": [{ "name": "next" }],
|
|
8
|
-
"paths": { "@/*": ["./*"] }
|
|
18
|
+
"paths": { "@/*": ["./*"] },
|
|
19
|
+
"allowJs": true
|
|
9
20
|
},
|
|
10
|
-
"include": [
|
|
21
|
+
"include": [
|
|
22
|
+
"**/*.ts",
|
|
23
|
+
"**/*.tsx",
|
|
24
|
+
".next/types/**/*.ts",
|
|
25
|
+
".next/dev/types/**/*.ts"
|
|
26
|
+
],
|
|
11
27
|
"exclude": ["node_modules", ".next"]
|
|
12
28
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
|
10
|
+
const onChange = () => {
|
|
11
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
12
|
+
}
|
|
13
|
+
mql.addEventListener("change", onChange)
|
|
14
|
+
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
|
15
|
+
return () => mql.removeEventListener("change", onChange)
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
return !!isMobile
|
|
19
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
|
|
3
|
+
import type { WorkspaceCreated } from "@intelligo-dev/auth";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs once, when a user's personal workspace is created — at signup,
|
|
7
|
+
* or the first time the app finds the user without one: the place for
|
|
8
|
+
* trial credits, referral bonuses or any other one-time provisioning.
|
|
9
|
+
* `lib/intelligo.ts` sets it with `setWorkspaceCreatedHandler`, so
|
|
10
|
+
* `@intelligo-dev/auth` never imports billing.
|
|
11
|
+
*
|
|
12
|
+
* A failure is logged and does not fail the signup.
|
|
13
|
+
*/
|
|
14
|
+
export async function onWorkspaceCreated(
|
|
15
|
+
_created: WorkspaceCreated
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
// Provisions nothing by default.
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Example: grant the registered trial with `@intelligo-dev/billing`
|
|
21
|
+
// (replace the export above):
|
|
22
|
+
//
|
|
23
|
+
// import { provisionTrialCredits } from "@intelligo-dev/billing";
|
|
24
|
+
//
|
|
25
|
+
// export async function onWorkspaceCreated({
|
|
26
|
+
// workspaceId,
|
|
27
|
+
// email,
|
|
28
|
+
// }: WorkspaceCreated): Promise<void> {
|
|
29
|
+
// await provisionTrialCredits({ workspaceId, email });
|
|
30
|
+
// }
|