@basaltkit/ai 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.js +12 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/make/make.d.ts +2 -0
- package/dist/make/make.js +12 -2
- package/dist/plan/context.d.ts +2 -2
- package/dist/plan/context.js +9 -2
- package/dist/plan/knowledge.d.ts +1 -1
- package/dist/plan/knowledge.js +8 -2
- package/dist/plan/packages.d.ts +33 -0
- package/dist/plan/packages.js +166 -0
- package/dist/plan/plan.js +1 -0
- package/dist/plan/render.d.ts +5 -1
- package/dist/plan/render.js +8 -2
- package/dist/plan/types.d.ts +3 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.js +1 -0
- package/package.json +2 -2
package/dist/commands.js
CHANGED
|
@@ -6,7 +6,7 @@ import { applyFixEdits, fixableIds, planFix, renderFixes } from './doctor/fixes.
|
|
|
6
6
|
import { createProvider, providerEnvFromProcess } from './provider/factory.js';
|
|
7
7
|
import { createPlan } from './plan/plan.js';
|
|
8
8
|
import { renderPlan } from './plan/render.js';
|
|
9
|
-
import { runMake } from './make/make.js';
|
|
9
|
+
import { packageSteps, runMake } from './make/make.js';
|
|
10
10
|
import { renderMakeResult } from './make/render.js';
|
|
11
11
|
import { runPrismaPush } from './make/schema.js';
|
|
12
12
|
import { verifyProject } from './make/verify.js';
|
|
@@ -39,7 +39,7 @@ export function aiCommands(options = {}) {
|
|
|
39
39
|
io.log('');
|
|
40
40
|
if (args.length > 0) {
|
|
41
41
|
io.log(`You asked: "${args.join(' ')}"`);
|
|
42
|
-
io.log(
|
|
42
|
+
io.log(`Plan it with: basalt ai:plan "${args.join(' ')}"`);
|
|
43
43
|
io.log('');
|
|
44
44
|
}
|
|
45
45
|
io.log('Available now:');
|
|
@@ -180,8 +180,17 @@ export function aiCommands(options = {}) {
|
|
|
180
180
|
io.error(error.message);
|
|
181
181
|
return 1;
|
|
182
182
|
}
|
|
183
|
-
renderPlan(plan, io);
|
|
183
|
+
renderPlan(plan, io, { footer: false });
|
|
184
184
|
io.log('');
|
|
185
|
+
// ai:make only scaffolds entities. A plan without any (a package to
|
|
186
|
+
// install or wire, config, docs) is complete as shown — say so before
|
|
187
|
+
// the confirmation prompt instead of failing after it.
|
|
188
|
+
if (plan.entities.length === 0) {
|
|
189
|
+
io.log(packageSteps(plan).length > 0
|
|
190
|
+
? 'Nothing to scaffold — this plan reuses ecosystem packages. Run the package steps above.'
|
|
191
|
+
: 'Nothing to scaffold — the steps above are applied by hand.');
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
185
194
|
// Agent review over the generated code — read-only, uses the same provider.
|
|
186
195
|
const agentReview = async (result) => {
|
|
187
196
|
if (flags['review'] !== true)
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ export { analyze, type AnalysisReport } from './analyze/run.js';
|
|
|
15
15
|
export { createPlan, parsePlan, type CreatePlanOptions } from './plan/plan.js';
|
|
16
16
|
export { buildPlanContext } from './plan/context.js';
|
|
17
17
|
export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
|
|
18
|
-
export {
|
|
18
|
+
export { ECOSYSTEM_PACKAGES, renderEcosystemCatalog, type EcosystemPackage } from './plan/packages.js';
|
|
19
|
+
export { renderPlan, type RenderPlanOptions } from './plan/render.js';
|
|
19
20
|
export type { ArchitecturePlan, PlanEntity, PlanField, PlanRelation, PlanStep, PlanStepKind, } from './plan/types.js';
|
|
20
21
|
export { runMake } from './make/make.js';
|
|
21
22
|
export { assertSafePlan, UnsafePlanError } from './plan/identifiers.js';
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { analyze } from './analyze/run.js';
|
|
|
21
21
|
export { createPlan, parsePlan } from './plan/plan.js';
|
|
22
22
|
export { buildPlanContext } from './plan/context.js';
|
|
23
23
|
export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
|
|
24
|
+
export { ECOSYSTEM_PACKAGES, renderEcosystemCatalog } from './plan/packages.js';
|
|
24
25
|
export { renderPlan } from './plan/render.js';
|
|
25
26
|
// Implementation (spec §4 Implement) — execute a plan via the generator + review gate.
|
|
26
27
|
export { runMake } from './make/make.js';
|
package/dist/make/make.d.ts
CHANGED
|
@@ -12,3 +12,5 @@ import type { MakeOptions, MakeResult } from './types.js';
|
|
|
12
12
|
* touches the database, never deletes.
|
|
13
13
|
*/
|
|
14
14
|
export declare function runMake(ctx: ProjectContext, plan: ArchitecturePlan, options?: MakeOptions): Promise<MakeResult>;
|
|
15
|
+
/** The plan's `package` steps as instructions — ai:make never installs dependencies itself. */
|
|
16
|
+
export declare function packageSteps(plan: ArchitecturePlan): string[];
|
package/dist/make/make.js
CHANGED
|
@@ -28,7 +28,10 @@ export async function runMake(ctx, plan, options = {}) {
|
|
|
28
28
|
assertSafePlan(plan);
|
|
29
29
|
const entities = plan.entities.filter((e) => e.name.trim() !== '');
|
|
30
30
|
if (entities.length === 0) {
|
|
31
|
-
|
|
31
|
+
const packages = packageSteps(plan);
|
|
32
|
+
throw new Error(packages.length > 0
|
|
33
|
+
? `ai:make — nothing to generate: the plan reuses ecosystem packages. ${packages.join(' ')}`
|
|
34
|
+
: 'ai:make — the plan has no entity to generate.');
|
|
32
35
|
}
|
|
33
36
|
throwIfAborted(options.signal);
|
|
34
37
|
const baseDir = options.baseDir ?? process.cwd();
|
|
@@ -222,7 +225,8 @@ function augmentFiles(files, entity, plan, ctx, opts) {
|
|
|
222
225
|
return { files: out, augmented, guarded, audited };
|
|
223
226
|
}
|
|
224
227
|
function buildFollowUps(plan, resources, schema, migration) {
|
|
225
|
-
|
|
228
|
+
// Packages first: the generated code may depend on them being wired.
|
|
229
|
+
const followUps = packageSteps(plan);
|
|
226
230
|
const prismaResources = resources.filter((r) => r.prisma);
|
|
227
231
|
if (prismaResources.length > 0) {
|
|
228
232
|
if (!schema?.found) {
|
|
@@ -257,6 +261,12 @@ function buildFollowUps(plan, resources, schema, migration) {
|
|
|
257
261
|
}
|
|
258
262
|
return followUps;
|
|
259
263
|
}
|
|
264
|
+
/** The plan's `package` steps as instructions — ai:make never installs dependencies itself. */
|
|
265
|
+
export function packageSteps(plan) {
|
|
266
|
+
return plan.steps
|
|
267
|
+
.filter((step) => step.kind === 'package')
|
|
268
|
+
.map((step) => `${step.title}${step.command ? ` — \`${step.command}\`` : ''}${step.detail ? `: ${step.detail}` : ''}`);
|
|
269
|
+
}
|
|
260
270
|
function reviewBuild(ctx, plan, resources, schema, migration) {
|
|
261
271
|
const items = [];
|
|
262
272
|
// Tenant isolation — the gate the spec cares most about.
|
package/dist/plan/context.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ProjectContext } from '../context/project.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Compact, relevant-only project summary for the planner (spec §18 Context
|
|
4
4
|
* Engineering — never ship the whole project to the model). Surfaces exactly what
|
|
5
|
-
* the Architect needs to ground a plan: enabled capabilities, existing models
|
|
6
|
-
* the generator's flags.
|
|
5
|
+
* the Architect needs to ground a plan: enabled capabilities, existing models,
|
|
6
|
+
* the ecosystem packages already installed and the generator's flags.
|
|
7
7
|
*/
|
|
8
8
|
export declare function buildPlanContext(ctx: ProjectContext): string;
|
package/dist/plan/context.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { ECOSYSTEM_PACKAGES } from './packages.js';
|
|
1
2
|
/**
|
|
2
3
|
* Compact, relevant-only project summary for the planner (spec §18 Context
|
|
3
4
|
* Engineering — never ship the whole project to the model). Surfaces exactly what
|
|
4
|
-
* the Architect needs to ground a plan: enabled capabilities, existing models
|
|
5
|
-
* the generator's flags.
|
|
5
|
+
* the Architect needs to ground a plan: enabled capabilities, existing models,
|
|
6
|
+
* the ecosystem packages already installed and the generator's flags.
|
|
6
7
|
*/
|
|
7
8
|
export function buildPlanContext(ctx) {
|
|
8
9
|
const s = ctx.stack;
|
|
@@ -25,12 +26,18 @@ export function buildPlanContext(ctx) {
|
|
|
25
26
|
const models = ctx.prisma && ctx.prisma.models.length > 0
|
|
26
27
|
? ctx.prisma.models.map((m) => `${m.name}${m.tenantScoped ? ' (tenantId)' : ''}`).join(', ')
|
|
27
28
|
: '(none)';
|
|
29
|
+
// Catalog packages only: the store/driver packages (-prisma, -s3…) are
|
|
30
|
+
// implementation detail the planner reaches through the catalog entry.
|
|
31
|
+
const catalog = new Set(ECOSYSTEM_PACKAGES.map((p) => p.name));
|
|
32
|
+
const installed = ctx.installed.filter((name) => catalog.has(name));
|
|
28
33
|
const lines = [
|
|
29
34
|
'PROJECT STACK:',
|
|
30
35
|
...caps.map((c) => `- ${c}`),
|
|
31
36
|
'',
|
|
32
37
|
`EXISTING PRISMA MODELS: ${models}`,
|
|
33
38
|
'',
|
|
39
|
+
`INSTALLED ECOSYSTEM PACKAGES: ${installed.join(', ') || '(none)'} — use these as they are; any other catalog package needs a "package" install step.`,
|
|
40
|
+
'',
|
|
34
41
|
'GENERATOR: `basalt make:resource <Name>` — flags: --prisma, --soft-delete, --no-register, --dir, --force.',
|
|
35
42
|
];
|
|
36
43
|
if (ctx.app)
|
package/dist/plan/knowledge.d.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
* This is deliberately prescriptive: the plan must reuse the generator and the
|
|
7
7
|
* platform plugins, never hand-roll parallel abstractions.
|
|
8
8
|
*/
|
|
9
|
-
export declare const BASALT_KNOWLEDGE
|
|
9
|
+
export declare const BASALT_KNOWLEDGE: string;
|
package/dist/plan/knowledge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderEcosystemCatalog } from './packages.js';
|
|
1
2
|
/**
|
|
2
3
|
* The Architect agent's system prompt — Basalt's conventions and official APIs,
|
|
3
4
|
* encoded so the model plans *with* the framework instead of inventing its own
|
|
@@ -10,6 +11,11 @@ export const BASALT_KNOWLEDGE = `You are the Architect agent for Basalt — a ba
|
|
|
10
11
|
|
|
11
12
|
ABSOLUTE RULES — always use the framework's official APIs and generators, never invent new ones:
|
|
12
13
|
|
|
14
|
+
0. ECOSYSTEM FIRST. Before planning any resource, check whether an official package already covers the capability (catalog below). If one does, use it: a "package" step whose "command" installs it (\`pnpm add <package>\`, plus its Prisma store when the app uses Prisma) and whose "detail" says how to wire its plugin/routes in src/app.ts — or, when the project already has it installed, a "package" step with no "command" that only wires/uses it. NEVER generate a resource, model or module that duplicates a package (no Comment model when @basaltkit/comments exists, no Notification table, no File/Attachment model, no Team/Invitation model, no AuditLog). Do NOT list a package-covered concept in "entities" — entities are only the domain models no package provides. Only generate from scratch what the catalog does not cover, and say so in "warnings" when a package covers it only partly.
|
|
15
|
+
|
|
16
|
+
ECOSYSTEM CATALOG:
|
|
17
|
+
${renderEcosystemCatalog()}
|
|
18
|
+
|
|
13
19
|
1. SCAFFOLDING. Create a resource with the generator, not by hand:
|
|
14
20
|
\`basalt make:resource <Name>\` — generates a full vertical: Zod schema, repository, service, DI plugin, typed routes and a test, all on-convention and auto-wired into src/app.ts.
|
|
15
21
|
Flags: --prisma (Prisma-backed repository + a schema.prisma model with id/createdAt/updatedAt), --soft-delete (adds deletedAt, a restore() method + route), --no-register (skip wiring), --dir=<path>, --force.
|
|
@@ -36,14 +42,14 @@ OUTPUT: Return ONLY a single JSON object (no prose, no markdown fences) matching
|
|
|
36
42
|
"relations": [{ "name": string, "model": string }] } // belongs-to: this model gets a <name>Id FK → <model>
|
|
37
43
|
],
|
|
38
44
|
"steps": [ // ordered implementation steps
|
|
39
|
-
{ "order": number, "title": string, "kind": "generator"|"schema"|"migration"|"service"|"routes"|"permissions"|"audit"|"test"|"docs"|"other", "detail": string, "command": string, "files": string[] }
|
|
45
|
+
{ "order": number, "title": string, "kind": "package"|"generator"|"schema"|"migration"|"service"|"routes"|"permissions"|"audit"|"test"|"docs"|"other", "detail": string, "command": string, "files": string[] }
|
|
40
46
|
],
|
|
41
47
|
"permissions": string[], // e.g. ["patients.view","patients.create",...]
|
|
42
48
|
"auditEvents": string[], // e.g. ["patient.created",...]
|
|
43
49
|
"tenantScoped": boolean, // does this feature involve tenant-owned data
|
|
44
50
|
"warnings": string[] // risks, decisions to confirm, missing info
|
|
45
51
|
}
|
|
46
|
-
For generator steps, put the exact command in "command" (e.g. "basalt make:resource Patient --prisma --soft-delete"). Omit "command"/"files" when not applicable.
|
|
52
|
+
For generator steps, put the exact command in "command" (e.g. "basalt make:resource Patient --prisma --soft-delete"); for package steps, the install command (e.g. "pnpm add @basaltkit/comments @basaltkit/comments-prisma"), or omit it when the package is already installed. Omit "command"/"files" when not applicable.
|
|
47
53
|
|
|
48
54
|
ENUMS: a field with a fixed set of values (a status/state) MUST set "enum" to those values — e.g. "estado (pago/pendente)" → { "name": "estado", "type": "String", "enum": ["pago", "pendente"] }. It becomes a validated z.enum([...]) (stored as a String column), never a free z.string().
|
|
49
55
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ecosystem catalog the Architect plans with: each official package, the
|
|
3
|
+
* capability it covers and the entry points an app wires. Without it the model
|
|
4
|
+
* only knew the generator and rebuilt features (comments, files, notifications…)
|
|
5
|
+
* as fresh resources instead of installing the package that already ships them.
|
|
6
|
+
*
|
|
7
|
+
* Kept in sync with `packages/*` by `tests/plan-packages.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export interface EcosystemPackage {
|
|
10
|
+
/** npm name, e.g. `@basaltkit/comments`. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** What it provides, in the words a request would use. */
|
|
13
|
+
covers: string;
|
|
14
|
+
/** The plugin factory wired in src/app.ts. */
|
|
15
|
+
plugin?: string;
|
|
16
|
+
/** Ready-made routes the app spreads into its route list. */
|
|
17
|
+
routes?: string;
|
|
18
|
+
/** The Prisma-backed store package, when the default store is in-memory. */
|
|
19
|
+
prisma?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The wiring as the package README shows it, one line — so a plan names the
|
|
22
|
+
* real options instead of guessing them (it once wrote
|
|
23
|
+
* `app.register(mailerPlugin, { provider: 'smtp' })`).
|
|
24
|
+
*/
|
|
25
|
+
example?: string;
|
|
26
|
+
/** The same wiring with the Prisma store, for apps on Prisma. */
|
|
27
|
+
prismaExample?: string;
|
|
28
|
+
/** The DI token a service resolves with `ctx().container.get(TOKEN)`. */
|
|
29
|
+
token?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare const ECOSYSTEM_PACKAGES: readonly EcosystemPackage[];
|
|
32
|
+
/** The catalog for the planner's system prompt: one entry per package, wiring indented under it. */
|
|
33
|
+
export declare function renderEcosystemCatalog(packages?: readonly EcosystemPackage[]): string;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export const ECOSYSTEM_PACKAGES = [
|
|
2
|
+
{
|
|
3
|
+
name: '@basaltkit/auth', covers: 'login, signup, JWT/sessions, password reset, MFA, OAuth, passkeys, API keys',
|
|
4
|
+
plugin: 'authPlugin', routes: 'authRoutes', prisma: '@basaltkit/auth-prisma', token: 'AUTH',
|
|
5
|
+
example: 'authPlugin({ users: new MemoryUserSource(), secret: env.AUTH_SECRET }) + fastifyPlugin({ routes: [...authRoutes()] })',
|
|
6
|
+
prismaExample: 'const s = prismaAuthStores(prisma); authPlugin({ secret: env.AUTH_SECRET, users: s.users, sessions: s.sessions, refreshTokens: s.refreshTokens, tokens: s.tokens, mfa: s.mfa })',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: '@basaltkit/permissions', covers: 'roles, permissions, policies, route guards (RBAC)',
|
|
10
|
+
plugin: 'permissionsPlugin', prisma: '@basaltkit/permissions-prisma', token: 'GATE',
|
|
11
|
+
example: 'permissionsPlugin({ store })',
|
|
12
|
+
prismaExample: 'const p = prismaAccessStore(prisma); permissionsPlugin({ store: p.store })',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: '@basaltkit/tenancy', covers: 'multi-tenancy: tenant resolution and tenant context',
|
|
16
|
+
plugin: 'tenancyPlugin', prisma: '@basaltkit/tenancy-prisma', token: 'TENANCY',
|
|
17
|
+
example: 'tenancyPlugin({ source, resolvers: [headerResolver()] })',
|
|
18
|
+
prismaExample: 'tenancyPlugin({ source: prismaTenantSource(prisma), resolvers: [subdomainResolver({ base: env.APP_DOMAIN })] })',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: '@basaltkit/teams', covers: 'team members, roles inside a tenant, email invitations',
|
|
22
|
+
plugin: 'teamsPlugin', routes: 'teamRoutes', prisma: '@basaltkit/teams-prisma', token: 'TEAMS',
|
|
23
|
+
example: 'teamsPlugin() + fastifyPlugin({ routes: [...teamRoutes()] })',
|
|
24
|
+
prismaExample: 'const t = prismaTeamsStores(prisma); teamsPlugin({ memberships: t.memberships, invitations: t.invitations })',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: '@basaltkit/comments', covers: 'comment threads on any resource, @mentions, resolve/reopen',
|
|
28
|
+
plugin: 'commentsPlugin', routes: 'commentRoutes', prisma: '@basaltkit/comments-prisma', token: 'COMMENTS',
|
|
29
|
+
example: 'commentsPlugin() + fastifyPlugin({ routes: [...commentRoutes()] })',
|
|
30
|
+
prismaExample: 'commentsPlugin({ store: prismaCommentsStore(prisma).store })',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: '@basaltkit/activity', covers: 'user-facing activity feed / history of changes',
|
|
34
|
+
plugin: 'activityPlugin', prisma: '@basaltkit/activity-prisma', token: 'ACTIVITY',
|
|
35
|
+
example: "activityPlugin(); then container.get(ACTIVITY).in('project').performedOn('project', id).log('published')",
|
|
36
|
+
prismaExample: 'activityPlugin({ store: prismaActivityStore(prisma).store })',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: '@basaltkit/audit', covers: 'append-only audit trail of state changes',
|
|
40
|
+
plugin: 'auditPlugin', prisma: '@basaltkit/audit-prisma', token: 'AUDIT',
|
|
41
|
+
example: 'eventsPlugin() + auditPlugin()',
|
|
42
|
+
prismaExample: 'auditPlugin({ store: prismaAuditStore(prisma).store })',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: '@basaltkit/notifications', covers: 'in-app and email notifications, per-user preferences',
|
|
46
|
+
plugin: 'notificationsPlugin', routes: 'inAppRoutes', prisma: '@basaltkit/notifications-prisma', token: 'NOTIFIER',
|
|
47
|
+
example: 'mailerPlugin({ driver: \'log\', from: env.MAIL_FROM }) + notificationsPlugin()',
|
|
48
|
+
prismaExample: 'notificationsPlugin({ inApp: prismaInAppStore(prisma).store, preferences: prismaPreferenceStore(prisma) })',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: '@basaltkit/files', covers: 'file uploads and attachments, quotas, signed URLs',
|
|
52
|
+
plugin: 'filesPlugin', routes: 'fileRoutes', prisma: '@basaltkit/files-prisma', token: 'FILES',
|
|
53
|
+
example: "storagePlugin({ disks: { uploads: { driver: 'local', root: './storage' } } }) + filesPlugin({ disk: 'uploads', validate: { maxSize: 5_000_000 } }) + fastifyPlugin({ routes: [...fileRoutes()] })",
|
|
54
|
+
prismaExample: "filesPlugin({ disk: 'uploads', store: prismaFilesStore(prisma).store })",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: '@basaltkit/files-versions', covers: 'document versions / revisions on top of files',
|
|
58
|
+
plugin: 'fileVersionsPlugin', token: 'FILE_VERSIONS',
|
|
59
|
+
example: 'fileVersionsPlugin({ store: prismaFileVersionsStore(prisma).store }) // store from @basaltkit/files-prisma/versions',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: '@basaltkit/storage', covers: 'disk/object storage (local, S3, Azure, GCS)',
|
|
63
|
+
plugin: 'storagePlugin', token: 'STORAGE',
|
|
64
|
+
example: "storagePlugin({ default: 'uploads', disks: { uploads: { driver: 'local', root: './storage' } } })",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: '@basaltkit/drives', covers: 'import from Google Drive, OneDrive/SharePoint, Dropbox',
|
|
68
|
+
plugin: 'drivesPlugin', token: 'DRIVES',
|
|
69
|
+
example: "drivesPlugin({ providers: [dropboxDrive({ clientId, clientSecret })], keys: [{ id: '2026-09', key: env.DRIVES_ENCRYPTION_KEY }], secret: env.APP_SECRET })",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: '@basaltkit/subscriptions', covers: 'billing: plans, subscriptions, trials, usage limits, payment gateways',
|
|
73
|
+
plugin: 'subscriptionsPlugin', prisma: '@basaltkit/subscriptions-prisma', token: 'SUBSCRIPTIONS',
|
|
74
|
+
example: "subscriptionsPlugin({ plans, fallbackPlan: 'free', gateway }) + routes billingRoutes({ successUrl, cancelUrl }), invoiceRoutes(); guard with meta: { subscribed: 'pro' } / { feature: 'api' }",
|
|
75
|
+
prismaExample: 'const s = prismaSubscriptionsStores(prisma); subscriptionsPlugin({ plans, store: s.store, usage: s.usage, webhooks: s.webhooks })',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: '@basaltkit/webhooks', covers: 'outbound webhooks with signing and retries',
|
|
79
|
+
plugin: 'webhooksPlugin', prisma: '@basaltkit/webhooks-prisma', token: 'WEBHOOKS',
|
|
80
|
+
example: "eventsPlugin() + webhooksPlugin({ secret: env.WEBHOOK_SECRET, events: ['invoice.*'] })",
|
|
81
|
+
prismaExample: 'webhooksPlugin({ store: prismaWebhookStore(prisma).store, secret: env.WEBHOOK_SECRET })',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: '@basaltkit/events', covers: 'typed domain events, listeners, transactional outbox',
|
|
85
|
+
plugin: 'eventsPlugin', prisma: '@basaltkit/events-prisma', token: 'EVENTS',
|
|
86
|
+
example: "eventsPlugin() + outboxPlugin({ store, captureEvents: ['invoice.*'], dispatch: async (entry) => { … } })",
|
|
87
|
+
prismaExample: 'outboxPlugin({ store: prismaOutboxStore(prisma, { claim: true }).store, dispatch })',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: '@basaltkit/queue', covers: 'background jobs (BullMQ, RabbitMQ, SQS, Kafka)',
|
|
91
|
+
plugin: 'queuePlugin', token: 'QUEUE',
|
|
92
|
+
example: 'queuePlugin({ jobs: [SendWelcomeEmail] }) // no connection = sync driver; bullmqQueuePlugin({ jobs, connection: env.REDIS_URL }) from @basaltkit/queue-bullmq',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: '@basaltkit/scheduler', covers: 'scheduled / recurring tasks (cron-like)',
|
|
96
|
+
plugin: 'schedulerPlugin', token: 'SCHEDULER',
|
|
97
|
+
example: "schedulerPlugin({ define: (schedule) => { schedule.call('backup', async () => { … }).daily().at('03:00') } })",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: '@basaltkit/mailer', covers: 'sending email (SMTP, Resend, SES, Mailgun)',
|
|
101
|
+
plugin: 'mailerPlugin', token: 'MAILER',
|
|
102
|
+
example: "mailerPlugin({ driver: 'log', from: env.MAIL_FROM }) // or driver: 'resend', resend: { apiKey }; SMTP: driver: smtpMailer({ url }) from @basaltkit/mailer-smtp; send with container.get(MAILER)",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: '@basaltkit/inbound-mail', covers: 'receiving email',
|
|
106
|
+
routes: 'inboundMailRoutes',
|
|
107
|
+
example: "fastifyPlugin({ routes: [...inboundMailRoutes({ driver: signedDriver({ secret: env.INBOUND_SECRET }), routes: [{ address: '{tenant}@in.example.com', handler }] })] })",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: '@basaltkit/realtime', covers: 'live updates over WebSocket/SSE, presence',
|
|
111
|
+
plugin: 'realtimePlugin', token: 'REALTIME',
|
|
112
|
+
example: "realtimePlugin({ bridge: [bridgeRule({ hook: 'note:created', channel: 'notes', event: 'created' })] })",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: '@basaltkit/search', covers: 'full-text search (Postgres, Elasticsearch, Meilisearch)',
|
|
116
|
+
plugin: 'searchPlugin', token: 'SEARCH',
|
|
117
|
+
example: "searchPlugin({ indexes: [defineIndex({ name: 'notes', fields: ['title', 'body'] })] })",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: '@basaltkit/cache', covers: 'caching with tags and tenant scoping',
|
|
121
|
+
plugin: 'cachePlugin', token: 'CACHE',
|
|
122
|
+
example: "cachePlugin({ driver: 'memory' }) // shared: driver: redisCache(env.REDIS_URL) from @basaltkit/cache-redis",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: '@basaltkit/flags', covers: 'feature flags and percentage rollouts',
|
|
126
|
+
plugin: 'flagsPlugin', token: 'FLAGS',
|
|
127
|
+
example: 'flagsPlugin(defineFlags({ newDashboard: { default: false, tenants: { acme: true } }, betaSearch: { default: false, rollout: 25 } }))',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: '@basaltkit/exports', covers: 'CSV/JSON/XLSX exports and CSV import',
|
|
131
|
+
plugin: 'exportsPlugin', token: 'EXPORTS',
|
|
132
|
+
example: "exportsPlugin({ formatters: [createCsvFormatter({ delimiter: ';' })] })",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: '@basaltkit/i18n', covers: 'translations, locale, number/date formatting',
|
|
136
|
+
plugin: 'i18nPlugin', token: 'I18N',
|
|
137
|
+
example: "i18nPlugin({ locales: { en, pt }, defaultLocale: 'en' })",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: '@basaltkit/backup', covers: 'PostgreSQL backups',
|
|
141
|
+
plugin: 'backupPlugin', token: 'BACKUP',
|
|
142
|
+
example: "storagePlugin({ disks: { backups: { driver: 'local', root: './backups', scope: null } } }) + backupPlugin({ connectionUrl: env.DATABASE_URL, disk: 'backups', retention: 7 })",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: '@basaltkit/mcp', covers: 'expose routes as MCP tools for AI agents',
|
|
146
|
+
plugin: 'mcpPlugin', token: 'MCP',
|
|
147
|
+
example: "mcpPlugin({ routes, serverInfo: { name: 'app', version: '1.0.0' } }) + fastifyPlugin({ routes: [...routes, ...mcpRoutes()] }); opt a route in with meta: { mcp: { name } }",
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
/** The catalog for the planner's system prompt: one entry per package, wiring indented under it. */
|
|
151
|
+
export function renderEcosystemCatalog(packages = ECOSYSTEM_PACKAGES) {
|
|
152
|
+
return packages
|
|
153
|
+
.map((p) => {
|
|
154
|
+
const lines = [`- ${p.name} — ${p.covers}`];
|
|
155
|
+
if (p.example)
|
|
156
|
+
lines.push(` wire: ${p.example}`);
|
|
157
|
+
if (p.prismaExample)
|
|
158
|
+
lines.push(` with Prisma (${p.prisma}): ${p.prismaExample}`);
|
|
159
|
+
else if (p.prisma)
|
|
160
|
+
lines.push(` Prisma store: ${p.prisma}`);
|
|
161
|
+
if (p.token)
|
|
162
|
+
lines.push(` use: ctx().container.get(${p.token})`);
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
})
|
|
165
|
+
.join('\n');
|
|
166
|
+
}
|
package/dist/plan/plan.js
CHANGED
package/dist/plan/render.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { LineWriter } from '../render.js';
|
|
2
2
|
import type { ArchitecturePlan } from './types.js';
|
|
3
|
+
export interface RenderPlanOptions {
|
|
4
|
+
/** Print the "what next" footer (default `true`); ai:make, which acts next itself, turns it off. */
|
|
5
|
+
footer?: boolean;
|
|
6
|
+
}
|
|
3
7
|
/** Render an {@link ArchitecturePlan} in the spec's `ARCHITECTURE PLAN` style. */
|
|
4
|
-
export declare function renderPlan(plan: ArchitecturePlan, io: LineWriter): void;
|
|
8
|
+
export declare function renderPlan(plan: ArchitecturePlan, io: LineWriter, options?: RenderPlanOptions): void;
|
package/dist/plan/render.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Render an {@link ArchitecturePlan} in the spec's `ARCHITECTURE PLAN` style. */
|
|
2
|
-
export function renderPlan(plan, io) {
|
|
2
|
+
export function renderPlan(plan, io, options = {}) {
|
|
3
3
|
io.log('ARCHITECTURE PLAN');
|
|
4
4
|
io.log('');
|
|
5
5
|
io.log(`Request: "${plan.request}"`);
|
|
@@ -49,7 +49,13 @@ export function renderPlan(plan, io) {
|
|
|
49
49
|
for (const warning of plan.warnings)
|
|
50
50
|
io.log(` - ${warning}`);
|
|
51
51
|
}
|
|
52
|
+
if (options.footer === false)
|
|
53
|
+
return;
|
|
52
54
|
io.log('');
|
|
53
55
|
io.log('This is a plan only — nothing was changed.');
|
|
54
|
-
|
|
56
|
+
// ai:make only scaffolds entities: suggesting it for a plan without any
|
|
57
|
+
// would end in "nothing to generate" after a confirmation prompt.
|
|
58
|
+
io.log(plan.entities.length > 0
|
|
59
|
+
? `Next: basalt ai:make "${plan.request}" implements it.`
|
|
60
|
+
: 'Nothing for ai:make to generate — the steps above are applied by hand.');
|
|
55
61
|
}
|
package/dist/plan/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export type PlanStepKind =
|
|
1
|
+
export type PlanStepKind =
|
|
2
|
+
/** Install and/or wire an official `@basaltkit/*` package instead of generating code. */
|
|
3
|
+
'package' | 'generator' | 'schema' | 'migration' | 'service' | 'routes' | 'permissions' | 'audit' | 'test' | 'docs' | 'other';
|
|
2
4
|
export interface PlanField {
|
|
3
5
|
name: string;
|
|
4
6
|
type: string;
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -230,6 +230,7 @@ export declare const PlanStepKindSchema: z.ZodEnum<{
|
|
|
230
230
|
generator: "generator";
|
|
231
231
|
migration: "migration";
|
|
232
232
|
other: "other";
|
|
233
|
+
package: "package";
|
|
233
234
|
permissions: "permissions";
|
|
234
235
|
routes: "routes";
|
|
235
236
|
schema: "schema";
|
|
@@ -245,6 +246,7 @@ export declare const PlanStepSchema: z.ZodObject<{
|
|
|
245
246
|
generator: "generator";
|
|
246
247
|
migration: "migration";
|
|
247
248
|
other: "other";
|
|
249
|
+
package: "package";
|
|
248
250
|
permissions: "permissions";
|
|
249
251
|
routes: "routes";
|
|
250
252
|
schema: "schema";
|
|
@@ -281,6 +283,7 @@ export declare const ArchitecturePlanSchema: z.ZodObject<{
|
|
|
281
283
|
generator: "generator";
|
|
282
284
|
migration: "migration";
|
|
283
285
|
other: "other";
|
|
286
|
+
package: "package";
|
|
284
287
|
permissions: "permissions";
|
|
285
288
|
routes: "routes";
|
|
286
289
|
schema: "schema";
|
package/dist/schema/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.5.0"
|
|
6
6
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"zod": "^4.6.7",
|
|
34
|
-
"@basaltkit/cli": "^1.7.
|
|
34
|
+
"@basaltkit/cli": "^1.7.1",
|
|
35
35
|
"@basaltkit/generator": "^1.7.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|