@basaltkit/ai 1.6.0 → 1.6.2
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 -8
- package/dist/context/project.d.ts +9 -0
- package/dist/context/project.js +10 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/make/make.d.ts +1 -1
- package/dist/make/make.js +4 -1
- package/dist/plan/knowledge.js +3 -1
- package/dist/plan/packages.d.ts +11 -1
- package/dist/plan/packages.js +158 -31
- package/dist/plan/plan.d.ts +10 -0
- package/dist/plan/plan.js +38 -0
- package/dist/plan/render.d.ts +5 -1
- package/dist/plan/render.js +9 -2
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -4,7 +4,7 @@ import { detectProject, nodeReader } from './context/project.js';
|
|
|
4
4
|
import { hasErrors, runDoctor } from './doctor/run.js';
|
|
5
5
|
import { applyFixEdits, fixableIds, planFix, renderFixes } from './doctor/fixes.js';
|
|
6
6
|
import { createProvider, providerEnvFromProcess } from './provider/factory.js';
|
|
7
|
-
import { createPlan } from './plan/plan.js';
|
|
7
|
+
import { createPlan, withGeneratorEntities } from './plan/plan.js';
|
|
8
8
|
import { renderPlan } from './plan/render.js';
|
|
9
9
|
import { packageSteps, runMake } from './make/make.js';
|
|
10
10
|
import { renderMakeResult } from './make/render.js';
|
|
@@ -140,7 +140,7 @@ export function aiCommands(options = {}) {
|
|
|
140
140
|
io.log('Planning...');
|
|
141
141
|
io.log('');
|
|
142
142
|
try {
|
|
143
|
-
const plan = await createPlan(provider, ctx, request);
|
|
143
|
+
const plan = withGeneratorEntities(await createPlan(provider, ctx, request), ctx);
|
|
144
144
|
renderPlan(plan, io);
|
|
145
145
|
return 0;
|
|
146
146
|
}
|
|
@@ -174,18 +174,22 @@ export function aiCommands(options = {}) {
|
|
|
174
174
|
io.log('');
|
|
175
175
|
let plan;
|
|
176
176
|
try {
|
|
177
|
-
|
|
177
|
+
// Shown and built with an entity for every generator step.
|
|
178
|
+
plan = withGeneratorEntities(await createPlan(provider, ctx, request), ctx);
|
|
178
179
|
}
|
|
179
180
|
catch (error) {
|
|
180
181
|
io.error(error.message);
|
|
181
182
|
return 1;
|
|
182
183
|
}
|
|
183
|
-
renderPlan(plan, io);
|
|
184
|
+
renderPlan(plan, io, { footer: false });
|
|
184
185
|
io.log('');
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
// ai:make only scaffolds entities. A plan without any (a package to
|
|
187
|
+
// install or wire, config, docs) is complete as shown — say so before
|
|
188
|
+
// the confirmation prompt instead of failing after it.
|
|
189
|
+
if (plan.entities.length === 0) {
|
|
190
|
+
io.log(packageSteps(plan).length > 0
|
|
191
|
+
? 'Nothing to scaffold — this plan reuses ecosystem packages. Run the package steps above.'
|
|
192
|
+
: 'Nothing to scaffold — the steps above are applied by hand.');
|
|
189
193
|
return 0;
|
|
190
194
|
}
|
|
191
195
|
// Agent review over the generated code — read-only, uses the same provider.
|
|
@@ -35,9 +35,18 @@ export interface DetectedStack {
|
|
|
35
35
|
logger: boolean;
|
|
36
36
|
ai: boolean;
|
|
37
37
|
}
|
|
38
|
+
/** A model column as declared: `type` keeps Prisma's spelling minus `?`/`[]`. */
|
|
39
|
+
export interface PrismaColumn {
|
|
40
|
+
name: string;
|
|
41
|
+
type: string;
|
|
42
|
+
optional: boolean;
|
|
43
|
+
list: boolean;
|
|
44
|
+
}
|
|
38
45
|
export interface PrismaModel {
|
|
39
46
|
name: string;
|
|
40
47
|
fields: string[];
|
|
48
|
+
/** The same fields with their types, so a resource can be generated for an existing model. */
|
|
49
|
+
columns?: PrismaColumn[];
|
|
41
50
|
/** True when the model carries a `tenantId` column (tenant-scoped). */
|
|
42
51
|
tenantScoped: boolean;
|
|
43
52
|
/** The `@@map("…")` table name, when the model declares one. */
|
package/dist/context/project.js
CHANGED
|
@@ -506,12 +506,17 @@ function parseModels(schema) {
|
|
|
506
506
|
const body = match[2];
|
|
507
507
|
if (!name || body === undefined)
|
|
508
508
|
continue;
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
509
|
+
const columns = [];
|
|
510
|
+
for (const line of body.split('\n')) {
|
|
511
|
+
const [field, rawType] = line.trim().split(/\s+/);
|
|
512
|
+
if (!field || field.startsWith('@') || field.startsWith('//'))
|
|
513
|
+
continue;
|
|
514
|
+
const type = (rawType ?? '').replace(/[?[\]]/g, '');
|
|
515
|
+
columns.push({ name: field, type, optional: rawType?.endsWith('?') === true, list: rawType?.endsWith('[]') === true });
|
|
516
|
+
}
|
|
517
|
+
const fields = columns.map((c) => c.name);
|
|
513
518
|
const table = /@@map\(\s*"([^"]+)"\s*\)/.exec(body)?.[1];
|
|
514
|
-
models.push({ name, fields, tenantScoped: fields.includes('tenantId'), ...(table ? { table } : {}) });
|
|
519
|
+
models.push({ name, fields, columns, tenantScoped: fields.includes('tenantId'), ...(table ? { table } : {}) });
|
|
515
520
|
}
|
|
516
521
|
return models;
|
|
517
522
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,17 +6,17 @@ export { fetchWithRetry, type RetryOptions } from './provider/http.js';
|
|
|
6
6
|
export { globalSseFetch, parseSseContent, type SseFetch, type SseResponse } from './provider/sse.js';
|
|
7
7
|
export { createProvider, providerEnvFrom, providerEnvFromProcess, type CreateProviderOptions, type ProviderEnv, type ProviderName, } from './provider/factory.js';
|
|
8
8
|
export { runGeneration, generateText, withAbort, throwIfAborted, abortError, isAbortError, type OnProgress, type WorkflowProgress, type WorkflowRunOptions, } from './generate.js';
|
|
9
|
-
export { detectProject, nodeReader, memoryReader, type AppFileInfo, type CentralPlatformRouteHint, type DefaultMemoryStore, type DetectedStack, type EnvFileInfo, type FrameworkSchema, type MigrationsInfo, type PrismaInfo, type PrismaModel, type ProjectContext, type ProjectReader, type ServerFileInfo, type SourceHint, type SourceHints, } from './context/project.js';
|
|
9
|
+
export { detectProject, nodeReader, memoryReader, type AppFileInfo, type CentralPlatformRouteHint, type DefaultMemoryStore, type DetectedStack, type EnvFileInfo, type FrameworkSchema, type MigrationsInfo, type PrismaInfo, type PrismaColumn, type PrismaModel, type ProjectContext, type ProjectReader, type ServerFileInfo, type SourceHint, type SourceHints, } from './context/project.js';
|
|
10
10
|
export { defineRule, type Diagnostic, type DiagnosticCategory, type DoctorRule, type Severity, } from './doctor/types.js';
|
|
11
11
|
export { DEFAULT_RULES } from './doctor/rules.js';
|
|
12
12
|
export { hasErrors, runDoctor } from './doctor/run.js';
|
|
13
13
|
export { applyFixEdits, fixableIds, planFix, renderFixes, FIXES, type FileEdit, type FixApplier, type FixOutcome, type FixStatus, } from './doctor/fixes.js';
|
|
14
14
|
export { analyze, type AnalysisReport } from './analyze/run.js';
|
|
15
|
-
export { createPlan, parsePlan, type CreatePlanOptions } from './plan/plan.js';
|
|
15
|
+
export { createPlan, generatorTargets, parsePlan, withGeneratorEntities, type CreatePlanOptions } from './plan/plan.js';
|
|
16
16
|
export { buildPlanContext } from './plan/context.js';
|
|
17
17
|
export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
|
|
18
18
|
export { ECOSYSTEM_PACKAGES, renderEcosystemCatalog, type EcosystemPackage } from './plan/packages.js';
|
|
19
|
-
export { renderPlan } from './plan/render.js';
|
|
19
|
+
export { renderPlan, type RenderPlanOptions } from './plan/render.js';
|
|
20
20
|
export type { ArchitecturePlan, PlanEntity, PlanField, PlanRelation, PlanStep, PlanStepKind, } from './plan/types.js';
|
|
21
21
|
export { runMake } from './make/make.js';
|
|
22
22
|
export { assertSafePlan, UnsafePlanError } from './plan/identifiers.js';
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export { applyFixEdits, fixableIds, planFix, renderFixes, FIXES, } from './docto
|
|
|
18
18
|
// Analysis report (spec §5, Analyze).
|
|
19
19
|
export { analyze } from './analyze/run.js';
|
|
20
20
|
// Planning (spec §3, Plan mode) — natural language → architecture plan.
|
|
21
|
-
export { createPlan, parsePlan } from './plan/plan.js';
|
|
21
|
+
export { createPlan, generatorTargets, parsePlan, withGeneratorEntities } from './plan/plan.js';
|
|
22
22
|
export { buildPlanContext } from './plan/context.js';
|
|
23
23
|
export { BASALT_KNOWLEDGE } from './plan/knowledge.js';
|
|
24
24
|
export { ECOSYSTEM_PACKAGES, renderEcosystemCatalog } from './plan/packages.js';
|
package/dist/make/make.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ import type { MakeOptions, MakeResult } from './types.js';
|
|
|
11
11
|
* decided. No destructive operations — never overwrites without `force`, never
|
|
12
12
|
* touches the database, never deletes.
|
|
13
13
|
*/
|
|
14
|
-
export declare function runMake(ctx: ProjectContext,
|
|
14
|
+
export declare function runMake(ctx: ProjectContext, planned: ArchitecturePlan, options?: MakeOptions): Promise<MakeResult>;
|
|
15
15
|
/** The plan's `package` steps as instructions — ai:make never installs dependencies itself. */
|
|
16
16
|
export declare function packageSteps(plan: ArchitecturePlan): string[];
|
package/dist/make/make.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { unifiedDiff } from './diff.js';
|
|
7
7
|
import { domainFields, injectPrismaFields, injectZodFields } from './fields.js';
|
|
8
8
|
import { assertSafePlan } from '../plan/identifiers.js';
|
|
9
|
+
import { withGeneratorEntities } from '../plan/plan.js';
|
|
9
10
|
import { externalRelationTargets, injectPrismaRelations, inverseRelationLines, relationFieldLines, relationForeignKeys, } from './relations.js';
|
|
10
11
|
import { injectOpenApiMeta } from './openapi.js';
|
|
11
12
|
import { renderPermissionsFile } from './permissions.js';
|
|
@@ -22,7 +23,9 @@ import { injectAuditPlugin, injectAuditService, injectPermissionGuards } from '.
|
|
|
22
23
|
* decided. No destructive operations — never overwrites without `force`, never
|
|
23
24
|
* touches the database, never deletes.
|
|
24
25
|
*/
|
|
25
|
-
export async function runMake(ctx,
|
|
26
|
+
export async function runMake(ctx, planned, options = {}) {
|
|
27
|
+
// Generator steps whose entity the model left out still get built.
|
|
28
|
+
const plan = withGeneratorEntities(planned, ctx);
|
|
26
29
|
// The plan is untrusted (LLM / MCP client): refuse any name that could
|
|
27
30
|
// inject code into the generated sources before generating anything.
|
|
28
31
|
assertSafePlan(plan);
|
package/dist/plan/knowledge.js
CHANGED
|
@@ -11,7 +11,7 @@ export const BASALT_KNOWLEDGE = `You are the Architect agent for Basalt — a ba
|
|
|
11
11
|
|
|
12
12
|
ABSOLUTE RULES — always use the framework's official APIs and generators, never invent new ones:
|
|
13
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 step 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.
|
|
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
15
|
|
|
16
16
|
ECOSYSTEM CATALOG:
|
|
17
17
|
${renderEcosystemCatalog()}
|
|
@@ -53,4 +53,6 @@ For generator steps, put the exact command in "command" (e.g. "basalt make:resou
|
|
|
53
53
|
|
|
54
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().
|
|
55
55
|
|
|
56
|
+
ENTITIES vs GENERATOR STEPS: every resource a "generator" step scaffolds MUST also be listed in "entities" with its fields — including when its Prisma model already exists (copy the existing fields). ai:make builds from "entities".
|
|
57
|
+
|
|
56
58
|
RELATIONS: express a belongs-to as an entry in "relations" (e.g. an Appointment belongs to a Patient → { "name": "patient", "model": "Patient" }). Do NOT also add the "<name>Id" field to "fields" — the FK column, the @relation and the inverse field are generated. Prefer generating both sides of a relation in the same plan.`;
|
package/dist/plan/packages.d.ts
CHANGED
|
@@ -17,7 +17,17 @@ export interface EcosystemPackage {
|
|
|
17
17
|
routes?: string;
|
|
18
18
|
/** The Prisma-backed store package, when the default store is in-memory. */
|
|
19
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;
|
|
20
30
|
}
|
|
21
31
|
export declare const ECOSYSTEM_PACKAGES: readonly EcosystemPackage[];
|
|
22
|
-
/**
|
|
32
|
+
/** The catalog for the planner's system prompt: one entry per package, wiring indented under it. */
|
|
23
33
|
export declare function renderEcosystemCatalog(packages?: readonly EcosystemPackage[]): string;
|
package/dist/plan/packages.js
CHANGED
|
@@ -1,39 +1,166 @@
|
|
|
1
1
|
export const ECOSYSTEM_PACKAGES = [
|
|
2
|
-
{
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
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
|
+
},
|
|
29
149
|
];
|
|
30
|
-
/**
|
|
150
|
+
/** The catalog for the planner's system prompt: one entry per package, wiring indented under it. */
|
|
31
151
|
export function renderEcosystemCatalog(packages = ECOSYSTEM_PACKAGES) {
|
|
32
152
|
return packages
|
|
33
153
|
.map((p) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
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');
|
|
37
164
|
})
|
|
38
165
|
.join('\n');
|
|
39
166
|
}
|
package/dist/plan/plan.d.ts
CHANGED
|
@@ -18,3 +18,13 @@ export interface CreatePlanOptions {
|
|
|
18
18
|
export declare function createPlan(provider: AIProvider, ctx: ProjectContext, request: string, options?: CreatePlanOptions): Promise<ArchitecturePlan>;
|
|
19
19
|
/** Parse + normalize the model's JSON into a validated plan. Tolerant of fences and missing arrays. */
|
|
20
20
|
export declare function parsePlan(raw: string, request: string): ArchitecturePlan;
|
|
21
|
+
/** Resource names a `basalt make:resource <Name>` generator step scaffolds. */
|
|
22
|
+
export declare function generatorTargets(plan: ArchitecturePlan): string[];
|
|
23
|
+
/**
|
|
24
|
+
* The plan with an entity for every generator step. Models sometimes leave
|
|
25
|
+
* `entities` empty when the Prisma models already exist and only list
|
|
26
|
+
* `make:resource` steps — ai:make then had nothing to build. A missing entity is
|
|
27
|
+
* taken from the existing model (scalar and enum columns; relations and lists
|
|
28
|
+
* are left to the schema), or created bare when there is none.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withGeneratorEntities(plan: ArchitecturePlan, ctx: ProjectContext): ArchitecturePlan;
|
package/dist/plan/plan.js
CHANGED
|
@@ -160,3 +160,41 @@ function normalizeKind(value) {
|
|
|
160
160
|
? value
|
|
161
161
|
: 'other';
|
|
162
162
|
}
|
|
163
|
+
/** Resource names a `basalt make:resource <Name>` generator step scaffolds. */
|
|
164
|
+
export function generatorTargets(plan) {
|
|
165
|
+
const names = [];
|
|
166
|
+
for (const step of plan.steps) {
|
|
167
|
+
if (step.kind !== 'generator' || !step.command)
|
|
168
|
+
continue;
|
|
169
|
+
const name = /make:resource\s+(\w+)/.exec(step.command)?.[1];
|
|
170
|
+
if (name && !names.some((n) => n.toLowerCase() === name.toLowerCase()))
|
|
171
|
+
names.push(name);
|
|
172
|
+
}
|
|
173
|
+
return names;
|
|
174
|
+
}
|
|
175
|
+
const PRISMA_SCALARS = new Set(['String', 'Int', 'BigInt', 'Float', 'Decimal', 'Boolean', 'DateTime', 'Json', 'Bytes']);
|
|
176
|
+
/**
|
|
177
|
+
* The plan with an entity for every generator step. Models sometimes leave
|
|
178
|
+
* `entities` empty when the Prisma models already exist and only list
|
|
179
|
+
* `make:resource` steps — ai:make then had nothing to build. A missing entity is
|
|
180
|
+
* taken from the existing model (scalar and enum columns; relations and lists
|
|
181
|
+
* are left to the schema), or created bare when there is none.
|
|
182
|
+
*/
|
|
183
|
+
export function withGeneratorEntities(plan, ctx) {
|
|
184
|
+
const known = new Set(plan.entities.map((e) => e.name.toLowerCase()));
|
|
185
|
+
const missing = generatorTargets(plan).filter((name) => !known.has(name.toLowerCase()));
|
|
186
|
+
if (missing.length === 0)
|
|
187
|
+
return plan;
|
|
188
|
+
const models = ctx.prisma?.models ?? [];
|
|
189
|
+
const modelNames = new Set(models.map((m) => m.name));
|
|
190
|
+
const added = missing.map((name) => {
|
|
191
|
+
const model = models.find((m) => m.name.toLowerCase() === name.toLowerCase());
|
|
192
|
+
if (!model)
|
|
193
|
+
return { name, fields: [], tenantScoped: ctx.stack.tenancy };
|
|
194
|
+
const fields = (model.columns ?? [])
|
|
195
|
+
.filter((c) => !c.list && !modelNames.has(c.type))
|
|
196
|
+
.map((c) => ({ name: c.name, type: PRISMA_SCALARS.has(c.type) ? c.type : 'String' }));
|
|
197
|
+
return { name: model.name, fields, tenantScoped: model.tenantScoped };
|
|
198
|
+
});
|
|
199
|
+
return { ...plan, entities: [...plan.entities, ...added] };
|
|
200
|
+
}
|
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,6 @@
|
|
|
1
|
+
import { generatorTargets } from './plan.js';
|
|
1
2
|
/** Render an {@link ArchitecturePlan} in the spec's `ARCHITECTURE PLAN` style. */
|
|
2
|
-
export function renderPlan(plan, io) {
|
|
3
|
+
export function renderPlan(plan, io, options = {}) {
|
|
3
4
|
io.log('ARCHITECTURE PLAN');
|
|
4
5
|
io.log('');
|
|
5
6
|
io.log(`Request: "${plan.request}"`);
|
|
@@ -49,7 +50,13 @@ export function renderPlan(plan, io) {
|
|
|
49
50
|
for (const warning of plan.warnings)
|
|
50
51
|
io.log(` - ${warning}`);
|
|
51
52
|
}
|
|
53
|
+
if (options.footer === false)
|
|
54
|
+
return;
|
|
52
55
|
io.log('');
|
|
53
56
|
io.log('This is a plan only — nothing was changed.');
|
|
54
|
-
|
|
57
|
+
// ai:make only scaffolds entities: suggesting it for a plan without any
|
|
58
|
+
// would end in "nothing to generate" after a confirmation prompt.
|
|
59
|
+
io.log(plan.entities.length > 0 || generatorTargets(plan).length > 0
|
|
60
|
+
? `Next: basalt ai:make "${plan.request}" implements it.`
|
|
61
|
+
: 'Nothing for ai:make to generate — the steps above are applied by hand.');
|
|
55
62
|
}
|