@agentic-kit/db-tools 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +138 -0
- package/app-workspace.d.ts +16 -0
- package/app-workspace.js +145 -0
- package/context.d.ts +62 -0
- package/context.js +282 -0
- package/db-probe.d.ts +30 -0
- package/db-probe.js +50 -0
- package/esm/app-workspace.d.ts +16 -0
- package/esm/app-workspace.js +134 -0
- package/esm/context.d.ts +62 -0
- package/esm/context.js +270 -0
- package/esm/db-probe.d.ts +30 -0
- package/esm/db-probe.js +47 -0
- package/esm/host.d.ts +115 -0
- package/esm/host.js +23 -0
- package/esm/index.d.ts +20 -0
- package/esm/index.js +52 -0
- package/esm/policy/add-policies-to-table.d.ts +26 -0
- package/esm/policy/add-policies-to-table.js +66 -0
- package/esm/policy/provision-helpers.d.ts +38 -0
- package/esm/policy/provision-helpers.js +50 -0
- package/esm/provision-database/credential.d.ts +14 -0
- package/esm/provision-database/credential.js +19 -0
- package/esm/provision-database/env-file.d.ts +37 -0
- package/esm/provision-database/env-file.js +85 -0
- package/esm/provision-database/manifest.d.ts +10 -0
- package/esm/provision-database/manifest.js +48 -0
- package/esm/provision-database/pg-fixups.d.ts +16 -0
- package/esm/provision-database/pg-fixups.js +132 -0
- package/esm/provision-database/preset-match.d.ts +13 -0
- package/esm/provision-database/preset-match.js +66 -0
- package/esm/provision-database/presets.d.ts +4 -0
- package/esm/provision-database/presets.js +13 -0
- package/esm/provision-database/request-database.d.ts +61 -0
- package/esm/provision-database/request-database.js +130 -0
- package/esm/provision-database/resolve.d.ts +27 -0
- package/esm/provision-database/resolve.js +49 -0
- package/esm/records/meta.d.ts +41 -0
- package/esm/records/meta.js +167 -0
- package/esm/run-codegen/barrels.d.ts +3 -0
- package/esm/run-codegen/barrels.js +48 -0
- package/esm/run-codegen/endpoints.d.ts +8 -0
- package/esm/run-codegen/endpoints.js +46 -0
- package/esm/schema-resolve.d.ts +23 -0
- package/esm/schema-resolve.js +69 -0
- package/esm/tool-schema.d.ts +3 -0
- package/esm/tool-schema.js +10 -0
- package/esm/tools/add-policies.d.ts +24 -0
- package/esm/tools/add-policies.js +94 -0
- package/esm/tools/add-records.d.ts +18 -0
- package/esm/tools/add-records.js +122 -0
- package/esm/tools/add-relation-schema.d.ts +29 -0
- package/esm/tools/add-relation-schema.js +56 -0
- package/esm/tools/add-relation.d.ts +10 -0
- package/esm/tools/add-relation.js +91 -0
- package/esm/tools/create-api-key.d.ts +25 -0
- package/esm/tools/create-api-key.js +214 -0
- package/esm/tools/describe-schema.d.ts +19 -0
- package/esm/tools/describe-schema.js +130 -0
- package/esm/tools/manage-entity-types.d.ts +40 -0
- package/esm/tools/manage-entity-types.js +201 -0
- package/esm/tools/mutations.d.ts +35 -0
- package/esm/tools/mutations.js +230 -0
- package/esm/tools/provision-blueprint.d.ts +21 -0
- package/esm/tools/provision-blueprint.js +109 -0
- package/esm/tools/provision-database.d.ts +18 -0
- package/esm/tools/provision-database.js +260 -0
- package/esm/tools/run-codegen.d.ts +12 -0
- package/esm/tools/run-codegen.js +137 -0
- package/esm/tools/templates.d.ts +71 -0
- package/esm/tools/templates.js +331 -0
- package/host.d.ts +115 -0
- package/host.js +28 -0
- package/index.d.ts +20 -0
- package/index.js +76 -0
- package/package.json +49 -0
- package/policy/add-policies-to-table.d.ts +26 -0
- package/policy/add-policies-to-table.js +71 -0
- package/policy/provision-helpers.d.ts +38 -0
- package/policy/provision-helpers.js +58 -0
- package/provision-database/credential.d.ts +14 -0
- package/provision-database/credential.js +22 -0
- package/provision-database/env-file.d.ts +37 -0
- package/provision-database/env-file.js +91 -0
- package/provision-database/manifest.d.ts +10 -0
- package/provision-database/manifest.js +56 -0
- package/provision-database/pg-fixups.d.ts +16 -0
- package/provision-database/pg-fixups.js +170 -0
- package/provision-database/preset-match.d.ts +13 -0
- package/provision-database/preset-match.js +70 -0
- package/provision-database/presets.d.ts +4 -0
- package/provision-database/presets.js +17 -0
- package/provision-database/request-database.d.ts +61 -0
- package/provision-database/request-database.js +134 -0
- package/provision-database/resolve.d.ts +27 -0
- package/provision-database/resolve.js +53 -0
- package/records/meta.d.ts +41 -0
- package/records/meta.js +172 -0
- package/run-codegen/barrels.d.ts +3 -0
- package/run-codegen/barrels.js +54 -0
- package/run-codegen/endpoints.d.ts +8 -0
- package/run-codegen/endpoints.js +51 -0
- package/schema-resolve.d.ts +23 -0
- package/schema-resolve.js +75 -0
- package/tool-schema.d.ts +3 -0
- package/tool-schema.js +13 -0
- package/tools/add-policies.d.ts +24 -0
- package/tools/add-policies.js +97 -0
- package/tools/add-records.d.ts +18 -0
- package/tools/add-records.js +125 -0
- package/tools/add-relation-schema.d.ts +29 -0
- package/tools/add-relation-schema.js +60 -0
- package/tools/add-relation.d.ts +10 -0
- package/tools/add-relation.js +94 -0
- package/tools/create-api-key.d.ts +25 -0
- package/tools/create-api-key.js +220 -0
- package/tools/describe-schema.d.ts +19 -0
- package/tools/describe-schema.js +133 -0
- package/tools/manage-entity-types.d.ts +40 -0
- package/tools/manage-entity-types.js +206 -0
- package/tools/mutations.d.ts +35 -0
- package/tools/mutations.js +233 -0
- package/tools/provision-blueprint.d.ts +21 -0
- package/tools/provision-blueprint.js +112 -0
- package/tools/provision-database.d.ts +18 -0
- package/tools/provision-database.js +266 -0
- package/tools/run-codegen.d.ts +12 -0
- package/tools/run-codegen.js +141 -0
- package/tools/templates.d.ts +71 -0
- package/tools/templates.js +335 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.prewarmAppWorkspace = exports.runCodegenTool = void 0;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
const app_workspace_1 = require("../app-workspace");
|
|
12
|
+
Object.defineProperty(exports, "prewarmAppWorkspace", { enumerable: true, get: function () { return app_workspace_1.prewarmAppWorkspace; } });
|
|
13
|
+
const host_1 = require("../host");
|
|
14
|
+
const barrels_1 = require("../run-codegen/barrels");
|
|
15
|
+
const endpoints_1 = require("../run-codegen/endpoints");
|
|
16
|
+
const DEFAULT_API_ENDPOINT = 'http://api.localhost:3000/graphql';
|
|
17
|
+
const RunCodegenZod = zod_1.z.object({});
|
|
18
|
+
function fail(message) {
|
|
19
|
+
return { content: [{ type: 'text', text: message }], details: { success: false, message } };
|
|
20
|
+
}
|
|
21
|
+
const FETCH_RESET_RE = /Failed to fetch schema|Connection reset|ECONNRESET|socket hang up/i;
|
|
22
|
+
// Read the binding provision_database wrote to the project .env. The
|
|
23
|
+
// API_ENDPOINT pin identifies which backend owns the binding — it wins over
|
|
24
|
+
// the app's current backend config, matching context.ts data-plane precedence.
|
|
25
|
+
async function readProjectEnv(cwd) {
|
|
26
|
+
try {
|
|
27
|
+
const env = await (0, promises_1.readFile)(node_path_1.default.join(cwd, '.env'), 'utf8');
|
|
28
|
+
const read = (key) => env.match(new RegExp(`^${key}=(.+)$`, 'm'))?.[1]?.trim();
|
|
29
|
+
return { databaseName: read('DATABASE_NAME'), apiEndpoint: read('API_ENDPOINT') };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
36
|
+
// The codegen CLI introspects three live vhosts (admin/auth/app). Under load the
|
|
37
|
+
// Constructive backend intermittently resets a connection mid-introspection
|
|
38
|
+
// ("Failed to fetch schema: Connection reset"), which is transient — a retry
|
|
39
|
+
// almost always clears it. Re-run the whole generate (it rebuilds all targets)
|
|
40
|
+
// up to `attempts` times, only on a fetch/reset failure; any other failure is
|
|
41
|
+
// real and returned immediately (CODEGEN-RETRY-001).
|
|
42
|
+
async function runCodegenWithRetry(appDir, env, attempts = 4) {
|
|
43
|
+
const sdk = node_path_1.default.join(appDir, 'src', 'graphql', 'sdk');
|
|
44
|
+
let last = { ok: false, out: 'codegen never ran' };
|
|
45
|
+
for (let i = 1; i <= attempts; i++) {
|
|
46
|
+
for (const sub of ['admin', 'auth', 'app']) {
|
|
47
|
+
await (0, promises_1.rm)(node_path_1.default.join(sdk, sub), { recursive: true, force: true });
|
|
48
|
+
}
|
|
49
|
+
last = await (0, app_workspace_1.run)('npx', ['@constructive-io/graphql-codegen', 'generate', '--config', './graphql-codegen.config.ts'], appDir, env);
|
|
50
|
+
if (last.ok)
|
|
51
|
+
return last;
|
|
52
|
+
if (!FETCH_RESET_RE.test(last.out))
|
|
53
|
+
return last;
|
|
54
|
+
if (i < attempts)
|
|
55
|
+
await sleep(2000 * i);
|
|
56
|
+
}
|
|
57
|
+
return last;
|
|
58
|
+
}
|
|
59
|
+
exports.runCodegenTool = {
|
|
60
|
+
name: 'run_codegen',
|
|
61
|
+
label: 'Run codegen',
|
|
62
|
+
description: "Generate the typed GraphQL SDK for the project's Next.js app (packages/app). Runs the exact deterministic sequence — scaffold packages/app from the constructive-app template if it doesn't exist yet, hoist the graphql override to the root workspace so a single grafast resolves, pin codegen to latest, do one clean install, run codegen (retrying transient backend connection resets), and normalize SDK barrels for Turbopack. Run AFTER provisioning the database and creating schema; it scaffolds packages/app itself, so no separate `pgpm init` step is needed. The scaffold + install are usually prewarmed during provision_database, so this is fast.",
|
|
63
|
+
promptSnippet: 'run_codegen: scaffold packages/app (if missing) + regenerate the typed SDK (workspace-config + clean install + codegen-with-retry + barrel-normalize). Run after schema changes. Gated.',
|
|
64
|
+
parameters: RunCodegenZod,
|
|
65
|
+
async execute(_params, ctx) {
|
|
66
|
+
const cwd = ctx.cwd;
|
|
67
|
+
const appDir = node_path_1.default.join(cwd, 'packages', 'app');
|
|
68
|
+
// Materialize packages/app deterministically if it isn't there yet — this
|
|
69
|
+
// replaces the flaky `pgpm init` scaffold step (PGPM-001). When
|
|
70
|
+
// provision_database prewarmed it, this no-ops.
|
|
71
|
+
const scaffold = await (0, app_workspace_1.ensureAppScaffold)(cwd);
|
|
72
|
+
if (!scaffold.ok) {
|
|
73
|
+
return fail(scaffold.out);
|
|
74
|
+
}
|
|
75
|
+
const { databaseName: dbName, apiEndpoint: envApiEndpoint } = await readProjectEnv(cwd);
|
|
76
|
+
if (!dbName) {
|
|
77
|
+
return fail('DATABASE_NAME missing from .env — provision the database first, then re-run.');
|
|
78
|
+
}
|
|
79
|
+
const backend = (0, host_1.getHost)().backendConfig();
|
|
80
|
+
const apiEndpoint = envApiEndpoint || backend?.apiEndpoint || DEFAULT_API_ENDPOINT;
|
|
81
|
+
const planes = (0, endpoints_1.derivePlaneEndpoints)(apiEndpoint, dbName);
|
|
82
|
+
// The dev server reads .env.local (NEXT_PUBLIC_DB_NAME + per-DB endpoint
|
|
83
|
+
// overrides so it doesn't fall back to localhost vhosts on a remote
|
|
84
|
+
// backend); the codegen config reads process.env (passed directly below).
|
|
85
|
+
try {
|
|
86
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(appDir, '.env.local'), (0, endpoints_1.envLocalContent)(dbName, planes));
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return fail(`Failed to write packages/app/.env.local: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
// Hoist the graphql override to the root workspace (PNPM-GRAPHQL-OVERRIDE-001)
|
|
92
|
+
// and pin codegen to latest (TS-001) before the single clean install. When
|
|
93
|
+
// prewarmed by provision_database these are idempotent and the install is a
|
|
94
|
+
// fast no-op; otherwise this is the first install (PNPM-BUILDS-001).
|
|
95
|
+
await (0, app_workspace_1.ensureWorkspaceConfig)(cwd);
|
|
96
|
+
await (0, app_workspace_1.pinCodegenLatest)(appDir);
|
|
97
|
+
const install = await (0, app_workspace_1.run)('pnpm', ['install', '--no-frozen-lockfile'], cwd);
|
|
98
|
+
if (!install.ok && !app_workspace_1.IGNORED_BUILDS_RE.test(install.out)) {
|
|
99
|
+
return fail(`pnpm install failed:\n${install.out}`);
|
|
100
|
+
}
|
|
101
|
+
const codegen = await runCodegenWithRetry(appDir, {
|
|
102
|
+
NEXT_PUBLIC_DB_NAME: dbName,
|
|
103
|
+
...(planes ? (0, endpoints_1.codegenEndpointEnv)(planes) : {}),
|
|
104
|
+
});
|
|
105
|
+
if (!codegen.ok) {
|
|
106
|
+
// A reset that survives every internal retry is a backend OUTAGE, not a code
|
|
107
|
+
// or install problem — re-running run_codegen cannot fix it. Say so explicitly
|
|
108
|
+
// so the agent marks sdk-codegen `blocked` instead of looping the tool to the
|
|
109
|
+
// time cap (CODEGEN-OUTAGE-001 / THRASH-001).
|
|
110
|
+
if (FETCH_RESET_RE.test(codegen.out)) {
|
|
111
|
+
return fail('codegen failed: BACKEND OUTAGE — the Constructive backend reset the connection ' +
|
|
112
|
+
'during schema introspection, and it stayed down across every internal retry. ' +
|
|
113
|
+
'This is an EXTERNAL outage, not a code or install problem; re-running run_codegen ' +
|
|
114
|
+
'will NOT fix it. Call run_preflight once — if the backend is down, mark ' +
|
|
115
|
+
'sdk-codegen `blocked` (SERVER-001, BLOCKED-PROCEED-001) and hand back to ' +
|
|
116
|
+
'build-orchestrator. Do NOT loop on run_codegen (THRASH-001).\n\n' +
|
|
117
|
+
codegen.out);
|
|
118
|
+
}
|
|
119
|
+
return fail(`codegen failed:\n${codegen.out}`);
|
|
120
|
+
}
|
|
121
|
+
// Normalize directory barrels so Turbopack can resolve them (TURBOPACK-BARREL-001).
|
|
122
|
+
const sdkRoot = node_path_1.default.join(appDir, 'src', 'graphql', 'sdk');
|
|
123
|
+
const barrelsRewritten = (0, barrels_1.normalizeSdkBarrels)(sdkRoot);
|
|
124
|
+
// Report which app submodules actually generated — a bare scaffold with only
|
|
125
|
+
// index.ts means codegen had no schema to generate against.
|
|
126
|
+
const appSdk = node_path_1.default.join(sdkRoot, 'app');
|
|
127
|
+
const generatedSubmodules = ['hooks', 'orm', 'types'].filter((sub) => (0, node_fs_1.existsSync)(node_path_1.default.join(appSdk, sub)));
|
|
128
|
+
const message = generatedSubmodules.length > 0
|
|
129
|
+
? `Codegen complete. Generated app submodules: ${generatedSubmodules.join(', ')}. Normalized ${barrelsRewritten} barrel file(s).`
|
|
130
|
+
: `Codegen ran, but no app submodules (hooks/orm/types) were generated — the database may have no entity tables yet. Normalized ${barrelsRewritten} barrel file(s).`;
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: 'text', text: message }],
|
|
133
|
+
details: {
|
|
134
|
+
success: generatedSubmodules.length > 0,
|
|
135
|
+
message,
|
|
136
|
+
barrelsRewritten,
|
|
137
|
+
generatedSubmodules,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type ConfirmPreviewTable, type HarnessTool } from '@agentic-kit/harness';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ProjectContext } from '../context';
|
|
4
|
+
export type TemplateDetails = {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
export type CreateTemplateDetails = TemplateDetails & {
|
|
9
|
+
displayName?: string;
|
|
10
|
+
blueprintName?: string;
|
|
11
|
+
tables?: ConfirmPreviewTable[];
|
|
12
|
+
};
|
|
13
|
+
type TemplateField = {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
isRequired: boolean;
|
|
17
|
+
defaultValue: string | null;
|
|
18
|
+
};
|
|
19
|
+
type TemplateTable = {
|
|
20
|
+
name: string;
|
|
21
|
+
fields: TemplateField[];
|
|
22
|
+
policies: string[];
|
|
23
|
+
relationCount: number;
|
|
24
|
+
};
|
|
25
|
+
type TemplateInfo = {
|
|
26
|
+
displayName: string;
|
|
27
|
+
description: string | null;
|
|
28
|
+
categories: string[];
|
|
29
|
+
tables: TemplateTable[];
|
|
30
|
+
};
|
|
31
|
+
export type ListTemplatesDetails = {
|
|
32
|
+
templates: TemplateInfo[];
|
|
33
|
+
};
|
|
34
|
+
export type CreateTemplatePreview = {
|
|
35
|
+
blueprintName: string;
|
|
36
|
+
tables: ConfirmPreviewTable[];
|
|
37
|
+
};
|
|
38
|
+
export declare function createTemplatePreviewTables(context: ProjectContext, blueprintName: string | undefined): Promise<CreateTemplatePreview>;
|
|
39
|
+
declare const ListTemplatesZod: z.ZodObject<{}, z.core.$strip>;
|
|
40
|
+
export declare const listTemplatesTool: HarnessTool<typeof ListTemplatesZod, ListTemplatesDetails>;
|
|
41
|
+
declare const CreateTemplateZod: z.ZodObject<{
|
|
42
|
+
displayName: z.ZodString;
|
|
43
|
+
blueprintName: z.ZodOptional<z.ZodString>;
|
|
44
|
+
description: z.ZodOptional<z.ZodString>;
|
|
45
|
+
categories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export declare const createTemplateTool: HarnessTool<typeof CreateTemplateZod, CreateTemplateDetails>;
|
|
48
|
+
declare const ApplyTemplateZod: z.ZodObject<{
|
|
49
|
+
templateName: z.ZodString;
|
|
50
|
+
nameOverride: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
export declare const applyTemplateTool: HarnessTool<typeof ApplyTemplateZod, TemplateDetails>;
|
|
53
|
+
declare const UpdateTemplateZod: z.ZodObject<{
|
|
54
|
+
templateName: z.ZodString;
|
|
55
|
+
patch: z.ZodObject<{
|
|
56
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
57
|
+
description: z.ZodOptional<z.ZodString>;
|
|
58
|
+
categories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
59
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
60
|
+
visibility: z.ZodOptional<z.ZodEnum<{
|
|
61
|
+
public: "public";
|
|
62
|
+
private: "private";
|
|
63
|
+
}>>;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export declare const updateTemplateTool: HarnessTool<typeof UpdateTemplateZod, TemplateDetails>;
|
|
67
|
+
declare const DeleteTemplateZod: z.ZodObject<{
|
|
68
|
+
templateName: z.ZodString;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
export declare const deleteTemplateTool: HarnessTool<typeof DeleteTemplateZod, TemplateDetails>;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deleteTemplateTool = exports.updateTemplateTool = exports.applyTemplateTool = exports.createTemplateTool = exports.listTemplatesTool = void 0;
|
|
4
|
+
exports.createTemplatePreviewTables = createTemplatePreviewTables;
|
|
5
|
+
const harness_1 = require("@agentic-kit/harness");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const context_1 = require("../context");
|
|
8
|
+
// A stored blueprint field's `type` is an expanded object (`{ name: 'text' }`),
|
|
9
|
+
// not the bare string the agent passes to provision_blueprint. Flatten it to the
|
|
10
|
+
// type name so the renderer gets a string, never an object (which React can't
|
|
11
|
+
// render as a child).
|
|
12
|
+
function fieldTypeName(raw) {
|
|
13
|
+
if (typeof raw === 'string')
|
|
14
|
+
return raw;
|
|
15
|
+
if (raw && typeof raw === 'object' && typeof raw.name === 'string') {
|
|
16
|
+
return raw.name;
|
|
17
|
+
}
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
// A template's definition is a stored blueprint (see BlueprintDefinitionSchema):
|
|
21
|
+
// { tables: [{ table_name, fields, policies }], relations: [{ source_table, … }] }.
|
|
22
|
+
// Parse it into the same table shape provision_blueprint surfaces so the renderer
|
|
23
|
+
// can reuse TableCard. Defensive throughout — it's untyped JSON from the catalog.
|
|
24
|
+
function parseTemplateTables(definition) {
|
|
25
|
+
if (!definition || typeof definition !== 'object')
|
|
26
|
+
return [];
|
|
27
|
+
const def = definition;
|
|
28
|
+
const tables = Array.isArray(def.tables) ? def.tables : [];
|
|
29
|
+
const relations = Array.isArray(def.relations) ? def.relations : [];
|
|
30
|
+
return tables.map((raw) => {
|
|
31
|
+
const table = raw;
|
|
32
|
+
const fields = Array.isArray(table.fields) ? table.fields : [];
|
|
33
|
+
const policies = Array.isArray(table.policies) ? table.policies : [];
|
|
34
|
+
return {
|
|
35
|
+
name: table.table_name ?? '',
|
|
36
|
+
fields: fields.map((rawField) => {
|
|
37
|
+
const field = rawField;
|
|
38
|
+
return {
|
|
39
|
+
name: field.name ?? '',
|
|
40
|
+
type: fieldTypeName(field.type),
|
|
41
|
+
isRequired: field.is_required ?? false,
|
|
42
|
+
defaultValue: typeof field.default === 'string' ? field.default : null,
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
policies: (0, harness_1.filterInternalPolicies)(policies.map((policy) => policy.$type ?? '').filter(Boolean)),
|
|
46
|
+
relationCount: relations.filter((relation) => relation.source_table === table.table_name).length,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function formatTemplatesText(templates) {
|
|
51
|
+
if (templates.length === 0)
|
|
52
|
+
return 'No blueprint templates available.';
|
|
53
|
+
const lines = [`Available blueprint templates (${templates.length}):`];
|
|
54
|
+
for (const template of templates) {
|
|
55
|
+
const cats = template.categories.length ? ` [${template.categories.join(', ')}]` : '';
|
|
56
|
+
lines.push(` ${template.displayName} — ${template.tables.length} tables${cats}`);
|
|
57
|
+
for (const table of template.tables) {
|
|
58
|
+
lines.push(` - ${table.name} (${table.fields.length} field${table.fields.length === 1 ? '' : 's'})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
// The latest project blueprint (or a named one) — the definition source for
|
|
64
|
+
// create_template, and for the confirm preview of the tables it will template.
|
|
65
|
+
// A blueprintName is a best-effort hint: if it matches nothing (the agent often
|
|
66
|
+
// guesses casing/wording), fall back to the most recent blueprint so the confirm
|
|
67
|
+
// still previews real tables and execute templates the same one it previewed.
|
|
68
|
+
async function fetchLatestBlueprint(context, blueprintName) {
|
|
69
|
+
const found = await context.modules.blueprint
|
|
70
|
+
.findMany({
|
|
71
|
+
select: { displayName: true, definition: true },
|
|
72
|
+
where: {
|
|
73
|
+
databaseId: { equalTo: context.databaseId },
|
|
74
|
+
...(blueprintName ? { displayName: { equalTo: blueprintName } } : {}),
|
|
75
|
+
},
|
|
76
|
+
orderBy: ['CREATED_AT_DESC'],
|
|
77
|
+
first: 1,
|
|
78
|
+
})
|
|
79
|
+
.unwrap();
|
|
80
|
+
return found.blueprints?.nodes?.[0];
|
|
81
|
+
}
|
|
82
|
+
async function findProjectBlueprint(context, blueprintName) {
|
|
83
|
+
let node = await fetchLatestBlueprint(context, blueprintName);
|
|
84
|
+
if (!node && blueprintName)
|
|
85
|
+
node = await fetchLatestBlueprint(context, undefined);
|
|
86
|
+
return node?.definition ? { displayName: node.displayName ?? '', definition: node.definition } : null;
|
|
87
|
+
}
|
|
88
|
+
// The create_template confirm preview — the source blueprint's name and its
|
|
89
|
+
// tables, parsed into the shape the renderer's TableCard reuses. Best-effort: any
|
|
90
|
+
// failure yields no tables rather than blocking the confirm.
|
|
91
|
+
async function createTemplatePreviewTables(context, blueprintName) {
|
|
92
|
+
try {
|
|
93
|
+
const blueprint = await findProjectBlueprint(context, blueprintName);
|
|
94
|
+
if (!blueprint)
|
|
95
|
+
return { blueprintName: '', tables: [] };
|
|
96
|
+
return {
|
|
97
|
+
blueprintName: blueprint.displayName,
|
|
98
|
+
tables: parseTemplateTables(blueprint.definition),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.warn('[db-tools] create_template preview lookup failed:', err);
|
|
103
|
+
return { blueprintName: '', tables: [] };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function resolveTemplateId(client, displayName) {
|
|
107
|
+
const result = await client.blueprintTemplate
|
|
108
|
+
.findMany({
|
|
109
|
+
select: { id: true, displayName: true },
|
|
110
|
+
where: { displayName: { equalTo: displayName } },
|
|
111
|
+
first: 1,
|
|
112
|
+
})
|
|
113
|
+
.unwrap();
|
|
114
|
+
const template = result.blueprintTemplates?.nodes?.[0];
|
|
115
|
+
if (!template?.id)
|
|
116
|
+
throw new Error(`Template "${displayName}" not found`);
|
|
117
|
+
return template.id;
|
|
118
|
+
}
|
|
119
|
+
function done(success, message) {
|
|
120
|
+
return { content: [{ type: 'text', text: message }], details: { success, message } };
|
|
121
|
+
}
|
|
122
|
+
// ── list_templates ────────────────────────────────────────────────────────
|
|
123
|
+
const ListTemplatesZod = zod_1.z.object({});
|
|
124
|
+
exports.listTemplatesTool = {
|
|
125
|
+
name: 'list_templates',
|
|
126
|
+
label: 'List templates',
|
|
127
|
+
description: 'List the blueprint templates available to provision into the project database (display name, description, categories, table count). Read-only — these are a global catalog, not the project schema.',
|
|
128
|
+
promptSnippet: 'list_templates: list the available blueprint templates to apply. Read-only — call before apply_template.',
|
|
129
|
+
parameters: ListTemplatesZod,
|
|
130
|
+
async execute(_params, ctx) {
|
|
131
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
|
|
132
|
+
if (!resolved.context) {
|
|
133
|
+
return { content: [{ type: 'text', text: resolved.reason }], details: { templates: [] } };
|
|
134
|
+
}
|
|
135
|
+
const result = await resolved.context.modules.blueprintTemplate
|
|
136
|
+
.findMany({
|
|
137
|
+
select: {
|
|
138
|
+
displayName: true,
|
|
139
|
+
description: true,
|
|
140
|
+
categories: true,
|
|
141
|
+
definition: true,
|
|
142
|
+
},
|
|
143
|
+
orderBy: ['NAME_ASC'],
|
|
144
|
+
})
|
|
145
|
+
.execute();
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
const message = result.errors.map((e) => e.message).join('; ');
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text: `Failed to read templates: ${message}` }],
|
|
150
|
+
details: { templates: [] },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const templates = (result.data.blueprintTemplates?.nodes ?? []).map((template) => ({
|
|
154
|
+
displayName: template.displayName ?? '',
|
|
155
|
+
description: template.description ?? null,
|
|
156
|
+
categories: Array.isArray(template.categories) ? template.categories : [],
|
|
157
|
+
tables: parseTemplateTables(template.definition),
|
|
158
|
+
}));
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: 'text', text: formatTemplatesText(templates) }],
|
|
161
|
+
details: { templates },
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
// ── create_template ───────────────────────────────────────────────────────
|
|
166
|
+
const CreateTemplateZod = zod_1.z.object({
|
|
167
|
+
displayName: zod_1.z.string().describe('Display name for the new blueprint template'),
|
|
168
|
+
blueprintName: zod_1.z
|
|
169
|
+
.string()
|
|
170
|
+
.describe('Display name of an existing project blueprint to save as the template. Defaults to the most recently provisioned blueprint.')
|
|
171
|
+
.optional(),
|
|
172
|
+
description: zod_1.z.string().describe('Description shown in the template catalog').optional(),
|
|
173
|
+
categories: zod_1.z
|
|
174
|
+
.array(zod_1.z.string())
|
|
175
|
+
.describe('Categories for catalog grouping and discovery')
|
|
176
|
+
.optional(),
|
|
177
|
+
});
|
|
178
|
+
exports.createTemplateTool = {
|
|
179
|
+
name: 'create_template',
|
|
180
|
+
label: 'Create template',
|
|
181
|
+
description: 'Save an existing project blueprint into the global template catalog as a reusable blueprint template. Defaults to the most recently provisioned blueprint; pass blueprintName to pick a specific one by display name. Gated.',
|
|
182
|
+
promptSnippet: 'create_template: save a project blueprint to the reusable template catalog. Gated.',
|
|
183
|
+
parameters: CreateTemplateZod,
|
|
184
|
+
async execute(params, ctx) {
|
|
185
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
|
|
186
|
+
if (!resolved.context)
|
|
187
|
+
return done(false, resolved.reason);
|
|
188
|
+
const { modules, ownerId } = resolved.context;
|
|
189
|
+
if (!ownerId)
|
|
190
|
+
return done(false, 'Not authenticated (missing OWNER_ID in .env).');
|
|
191
|
+
try {
|
|
192
|
+
const blueprint = await findProjectBlueprint(resolved.context, params.blueprintName);
|
|
193
|
+
if (!blueprint) {
|
|
194
|
+
const which = params.blueprintName ? `"${params.blueprintName}"` : 'any blueprint';
|
|
195
|
+
return done(false, `No project blueprint found (${which}). Provision a blueprint first.`);
|
|
196
|
+
}
|
|
197
|
+
const base = params.displayName.toLowerCase().replace(/\s+/g, '_');
|
|
198
|
+
await modules.blueprintTemplate
|
|
199
|
+
.create({
|
|
200
|
+
data: {
|
|
201
|
+
ownerId,
|
|
202
|
+
name: `${base}_${Date.now()}`,
|
|
203
|
+
displayName: params.displayName,
|
|
204
|
+
description: params.description,
|
|
205
|
+
categories: params.categories,
|
|
206
|
+
definition: blueprint.definition,
|
|
207
|
+
},
|
|
208
|
+
select: { id: true, displayName: true },
|
|
209
|
+
})
|
|
210
|
+
.unwrap();
|
|
211
|
+
const message = `Created template "${params.displayName}"`;
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'text', text: message }],
|
|
214
|
+
details: {
|
|
215
|
+
success: true,
|
|
216
|
+
message,
|
|
217
|
+
displayName: params.displayName,
|
|
218
|
+
blueprintName: blueprint.displayName,
|
|
219
|
+
tables: parseTemplateTables(blueprint.definition),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
return done(false, err instanceof Error ? err.message : 'Failed to create template');
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
// ── apply_template ────────────────────────────────────────────────────────
|
|
229
|
+
const ApplyTemplateZod = zod_1.z.object({
|
|
230
|
+
templateName: zod_1.z.string().describe('The display name of the blueprint template to apply'),
|
|
231
|
+
nameOverride: zod_1.z
|
|
232
|
+
.string()
|
|
233
|
+
.describe('Optional name override for the created blueprint (snake_case). Defaults to template name.')
|
|
234
|
+
.optional(),
|
|
235
|
+
});
|
|
236
|
+
exports.applyTemplateTool = {
|
|
237
|
+
name: 'apply_template',
|
|
238
|
+
label: 'Apply template',
|
|
239
|
+
description: 'Apply an existing blueprint template to the project database, creating its tables. Use the template display name (from list_templates).',
|
|
240
|
+
promptSnippet: 'apply_template: provision a saved blueprint template by display name. Gated.',
|
|
241
|
+
parameters: ApplyTemplateZod,
|
|
242
|
+
async execute(params, ctx) {
|
|
243
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
|
|
244
|
+
if (!resolved.context)
|
|
245
|
+
return done(false, resolved.reason);
|
|
246
|
+
const { modules, databaseId, schemaId, ownerId } = resolved.context;
|
|
247
|
+
if (!ownerId)
|
|
248
|
+
return done(false, 'Not authenticated (missing OWNER_ID in .env).');
|
|
249
|
+
try {
|
|
250
|
+
const templateId = await resolveTemplateId(modules, params.templateName);
|
|
251
|
+
const base = params.nameOverride ?? params.templateName.toLowerCase().replace(/\s+/g, '_');
|
|
252
|
+
const nameOverride = `${base}_${Date.now()}`;
|
|
253
|
+
const copyResult = await modules.mutation
|
|
254
|
+
.copyTemplateToBlueprint({ input: { templateId, databaseId, ownerId, nameOverride } }, { select: { result: true } })
|
|
255
|
+
.unwrap();
|
|
256
|
+
const blueprintId = copyResult.copyTemplateToBlueprint?.result;
|
|
257
|
+
if (!blueprintId)
|
|
258
|
+
throw new Error('Failed to copy template');
|
|
259
|
+
const constructResult = await modules.mutation
|
|
260
|
+
.constructBlueprint({ input: { blueprintId, schemaId } }, { select: { result: true } })
|
|
261
|
+
.unwrap();
|
|
262
|
+
const refMap = constructResult.constructBlueprint?.result;
|
|
263
|
+
if (!refMap)
|
|
264
|
+
return done(false, `Construction failed for template "${params.templateName}"`);
|
|
265
|
+
const createdCount = Object.keys(refMap).length;
|
|
266
|
+
return done(true, `Applied template "${params.templateName}" — created ${createdCount} table${createdCount === 1 ? '' : 's'}`);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
return done(false, err instanceof Error ? err.message : 'Failed to apply template');
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
// ── update_template ───────────────────────────────────────────────────────
|
|
274
|
+
const UpdateTemplateZod = zod_1.z.object({
|
|
275
|
+
templateName: zod_1.z.string().describe('The display name of the blueprint template to update'),
|
|
276
|
+
patch: zod_1.z.object({
|
|
277
|
+
displayName: zod_1.z.string().describe('New display name').optional(),
|
|
278
|
+
description: zod_1.z.string().describe('New description').optional(),
|
|
279
|
+
categories: zod_1.z.array(zod_1.z.string()).describe('New categories').optional(),
|
|
280
|
+
tags: zod_1.z.array(zod_1.z.string()).describe('New tags').optional(),
|
|
281
|
+
visibility: zod_1.z.enum(['private', 'public']).describe('New visibility').optional(),
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
exports.updateTemplateTool = {
|
|
285
|
+
name: 'update_template',
|
|
286
|
+
label: 'Update template',
|
|
287
|
+
description: 'Update a blueprint template’s metadata (display name, description, categories, tags, visibility).',
|
|
288
|
+
promptSnippet: 'update_template: edit a saved template’s metadata by display name. Gated.',
|
|
289
|
+
parameters: UpdateTemplateZod,
|
|
290
|
+
async execute(params, ctx) {
|
|
291
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
|
|
292
|
+
if (!resolved.context)
|
|
293
|
+
return done(false, resolved.reason);
|
|
294
|
+
try {
|
|
295
|
+
const templateId = await resolveTemplateId(resolved.context.modules, params.templateName);
|
|
296
|
+
await resolved.context.modules.blueprintTemplate
|
|
297
|
+
.update({
|
|
298
|
+
where: { id: templateId },
|
|
299
|
+
data: params.patch,
|
|
300
|
+
select: { id: true, displayName: true },
|
|
301
|
+
})
|
|
302
|
+
.unwrap();
|
|
303
|
+
return done(true, `Updated template "${params.templateName}"`);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
return done(false, err instanceof Error ? err.message : 'Failed to update template');
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
// ── delete_template ───────────────────────────────────────────────────────
|
|
311
|
+
const DeleteTemplateZod = zod_1.z.object({
|
|
312
|
+
templateName: zod_1.z.string().describe('The display name of the blueprint template to delete'),
|
|
313
|
+
});
|
|
314
|
+
exports.deleteTemplateTool = {
|
|
315
|
+
name: 'delete_template',
|
|
316
|
+
label: 'Delete template',
|
|
317
|
+
description: 'Permanently delete a blueprint template by its display name.',
|
|
318
|
+
promptSnippet: 'delete_template: remove a saved template by display name. Destructive — gated.',
|
|
319
|
+
parameters: DeleteTemplateZod,
|
|
320
|
+
async execute(params, ctx) {
|
|
321
|
+
const resolved = await (0, context_1.resolveProjectContext)(ctx.cwd);
|
|
322
|
+
if (!resolved.context)
|
|
323
|
+
return done(false, resolved.reason);
|
|
324
|
+
try {
|
|
325
|
+
const templateId = await resolveTemplateId(resolved.context.modules, params.templateName);
|
|
326
|
+
await resolved.context.modules.blueprintTemplate
|
|
327
|
+
.delete({ where: { id: templateId }, select: { id: true } })
|
|
328
|
+
.unwrap();
|
|
329
|
+
return done(true, `Deleted template "${params.templateName}"`);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
return done(false, err instanceof Error ? err.message : 'Failed to delete template');
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
};
|