@rawsql-ts/ztd-cli 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,8 @@ For actual test execution:
16
16
  pnpm add -D @rawsql-ts/pg-testkit
17
17
  ```
18
18
 
19
+ If you run `npx ztd init`, the CLI will automatically add and install the devDependencies referenced by the generated templates (Postgres defaults to `@rawsql-ts/pg-testkit`).
20
+
19
21
  Then use the CLI through `npx ztd` or the installed `ztd` bin.
20
22
 
21
23
  ## Getting Started (Fast Path)
@@ -26,6 +28,21 @@ Then use the CLI through `npx ztd` or the installed `ztd` bin.
26
28
  npx ztd init
27
29
  ```
28
30
 
31
+ For tutorials and greenfield projects, we recommend the optional SQL client seam:
32
+
33
+ ```bash
34
+ npx ztd init --with-sqlclient
35
+ ```
36
+
37
+ Use `--with-sqlclient` when you want a minimal `SqlClient` boundary for repositories. Skip it if your project
38
+ already has a database abstraction (Prisma, Drizzle, Kysely, custom adapters) to avoid duplicating layers.
39
+
40
+ ```bash
41
+ npx ztd init --with-app-interface
42
+ ```
43
+
44
+ Use `--with-app-interface` to append the application interface guidance block to `AGENTS.md` without generating the ZTD layout or touching other files.
45
+
29
46
  2. Put your schema into `ztd/ddl/`:
30
47
 
31
48
  - Edit the starter file (default): `ztd/ddl/public.sql`, or
@@ -58,12 +75,13 @@ You can introduce ZTD incrementally; existing tests and ORMs can remain untouche
58
75
  - `ztd/ddl/<schema>.sql` (starter schema files you can edit or replace; the default schema is `public.sql`)
59
76
  - `tests/generated/ztd-row-map.generated.ts` (auto-generated `TestRowMap`, the canonical test type contract; do not commit)
60
77
  - `tests/support/testkit-client.ts` (auto-generated helper that boots a database client, wires a driver, and shares fixtures across the suite)
61
- - `ztd.config.json` (CLI defaults and resolver hints: `dialect`, `ddlDir`, `testsDir`, plus `ddl.defaultSchema`/`ddl.searchPath` for resolving unqualified tables)
78
+ - `ztd.config.json` (CLI defaults and resolver hints: `dialect`, `ddlDir`, `testsDir`, `ddlLint`, plus `ddl.defaultSchema`/`ddl.searchPath` for resolving unqualified tables)
62
79
  - `tests/generated/ztd-layout.generated.ts` (generated layout snapshot; do not commit)
63
80
  - `tests/support/global-setup.ts` (shared test setup used by the generated testkit client)
64
81
  - `README.md` describing the workflow and commands
65
- - `AGENTS.md` (copied from the package template unless the project already has one)
82
+ - `AGENTS.md` (copied from the package template unless the project already has one; `--with-app-interface` adds the application interface guidance block at the end)
66
83
  - `ztd/AGENTS.md` and `ztd/README.md` (folder-specific instructions that describe the new schema/domain layout)
84
+ - `src/db/sql-client.ts` (optional; generated only with `--with-sqlclient`)
67
85
  - Optional guide stubs under `src/` and `tests/` if requested
68
86
 
69
87
  The resulting project follows the "DDL -> ztd-config -> tests" flow so you can regenerate everything from SQL-first artifacts.
@@ -74,6 +92,12 @@ The resulting project follows the "DDL -> ztd-config -> tests" flow so you can r
74
92
 
75
93
  Creates a ZTD-ready project layout (DDL folder, config, generated layout, and test support stubs). It does not connect to your database.
76
94
 
95
+ Use `--with-sqlclient` to scaffold a minimal repository-facing SQL client for tutorials and new projects. It is opt-in
96
+ to avoid colliding with existing database layers. If you use `pg`, adapt `client.query(...)` so it returns a plain row
97
+ array (`T[]`) that satisfies the generated `SqlClient` interface.
98
+
99
+ Use `--with-app-interface` to append the application interface guidance block to `AGENTS.md`. This documentation-only option leaves every other file untouched, making it easy to apply the guidance to existing repositories without rerunning the full layout generator.
100
+
77
101
  ### `ztd ztd-config`
78
102
 
79
103
  Reads every `.sql` file under the configured DDL directory and produces the generated artifacts under `tests/generated/`:
@@ -142,6 +166,143 @@ Driver responsibilities live in companion packages such as `@rawsql-ts/pg-testki
142
166
 
143
167
  The driver sits downstream of `testkit-core` and applies the rewrite + fixture pipeline before running any database interaction. Install the driver, configure it in your tests, and point it at the row map from `tests/generated/ztd-row-map.generated.ts`.
144
168
 
169
+ ### SQL rewrite logging (generated `testkit-client.ts`)
170
+
171
+ The `tests/support/testkit-client.ts` helper generated by `ztd init` can emit structured logs that show the SQL before and after pg-testkit rewrites it.
172
+
173
+ Enable it via environment variables:
174
+
175
+ - `ZTD_SQL_LOG=1` (or `true`/`yes`): log original + rewritten SQL
176
+ - `ZTD_SQL_LOG_PARAMS=1` (or `true`/`yes`): include parameters in the logs
177
+
178
+ You can also enable/disable logging per call by passing `ZtdSqlLogOptions` as the second argument to `createTestkitClient`.
179
+
180
+ ## Benchmark summary
181
+
182
+ ### Purpose
183
+
184
+ This benchmark executes the same repository implementation with two different supporting stacks: the Traditional schema/migration workflow (schema setup + seed + query + cleanup) and the ZTD fixture-backed workflow (repository query → rewrite → fixture materialization). The comparison highlights:
185
+
186
+ - End-to-end wall-clock time including runner startup.
187
+ - DB execution time and SQL count so Traditional’s higher SQL volume is explicit.
188
+ - ZTD rewrite and fixture breakdowns so the internal costs of the pg-testkit pipeline are visible.
189
+ - Parallelism effects, runner startup costs, and where the break-even point lies as suite size grows.
190
+
191
+ ### Assumptions and environment
192
+
193
+ This benchmark compares ZTD-style repository tests against a traditional migration-based workflow while exercising the same repository methods. All numbers are measured with the test runner included unless stated otherwise.
194
+
195
+ #### Environment (measured run)
196
+
197
+ - Node.js: v22.14.0
198
+ - OS: Windows 10 (build 26100)
199
+ - CPU: AMD Ryzen 7 7800X3D (16 logical cores)
200
+ - Database: PostgreSQL 18.1 (containerized; `testcontainers`)
201
+ - Parallel workers: 4
202
+ - Report date: 2025-12-20
203
+
204
+ #### Benchmark shape
205
+
206
+ - Repository test cases: 3 (`customer_summary`, `product_ranking`, `sales_summary`)
207
+ - Each test performs: 1 repository call (1 SQL execution per test case)
208
+ - The Traditional workflow wraps every repository execution with migration, seeding, and cleanup SQL, whereas the ZTD workflow captures the same query, feeds it to pg-testkit, and replays the rewritten/select-only statements backed by fixtures.
209
+ - Suite sizes shown in the report:
210
+ - 3 tests (baseline)
211
+ - 30 tests (the same 3 cases repeated to approximate a larger suite)
212
+
213
+ The 30-test suite exists to show how runner overhead amortizes as the number of executed tests grows, while keeping the tested SQL and data constant.
214
+
215
+ #### What is included / excluded
216
+
217
+ - **Runner-included runs (main comparison):** wall-clock time including `pnpm` + `vitest` startup and test execution.
218
+ - **Steady-state section:** measures incremental cost per iteration after the runner is warm (first iteration excluded), to approximate watch/CI-like “many tests per single runner invocation”.
219
+ - **Container startup:** excluded (the Postgres container is shared across runs).
220
+
221
+ #### Fairness / bias notes (important)
222
+
223
+ This benchmark intentionally measures the **Traditional** workflow under favorable assumptions:
224
+
225
+ - **Traditional SQL construction cost is treated as zero**: queries are hard-coded raw SQL strings (no ORM/query-builder generation time).
226
+ - **Traditional migration/DDL generation cost is treated as zero**: schema/migration SQL is also written directly (no ORM schema DSL or migration generation time).
227
+
228
+ In contrast, the **ZTD** benchmark includes the repository layer’s normal SQL usage:
229
+
230
+ - **ZTD includes SQL construction time as exercised by the repository layer** (i.e., whatever the test code does to produce the SQL text), in addition to rewrite/fixture overhead.
231
+
232
+ Because real-world ORM workflows usually add both query generation and migration generation overhead on top of what is measured here, this setup should be interpreted as a **lower bound for Traditional** and a relatively conservative comparison against ZTD.
233
+
234
+ ### Results (runner included)
235
+
236
+ #### End-to-end runtime
237
+
238
+ | Suite size | Scenario | Workers | Avg Total (ms) | Avg Startup (ms) | Avg Execution (ms) | Startup % | Avg ms/test | Avg SQL Count | Avg DB (ms) |
239
+ | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
240
+ | 3 | Traditional | 1 | 1951.08 | 1013.22 | 937.86 | 51.8% | 650.36 | 36 | 123.06 |
241
+ | 3 | Traditional | 4 | 1301.56 | 967.02 | 334.54 | 74.3% | 433.85 | 36 | 39.06 |
242
+ | 3 | ZTD | 1 | 2283.66 | 979.91 | 1303.74 | 42.9% | 761.22 | 3 | 11.34 |
243
+ | 3 | ZTD | 4 | 1430.64 | 957.75 | 472.89 | 66.9% | 476.88 | 3 | 3.81 |
244
+ | 30 | Traditional | 1 | 3085.48 | 1018.71 | 2066.77 | 33.0% | 102.85 | 360 | 1009.85 |
245
+ | 30 | Traditional | 4 | 1788.35 | 996.66 | 791.68 | 55.8% | 59.61 | 360 | 392.83 |
246
+ | 30 | ZTD | 1 | 2480.84 | 957.91 | 1522.94 | 38.6% | 82.69 | 30 | 44.82 |
247
+ | 30 | ZTD | 4 | 1507.46 | 944.57 | 562.88 | 62.7% | 50.25 | 30 | 17.69 |
248
+
249
+ ### What this shows
250
+ - **Small suites (3 tests) are dominated by runner startup.** At this scale, Traditional is faster (both serial and 4-worker), because fixed startup overhead and per-test harness work overwhelm ZTD’s per-test savings.
251
+ - **As suite size grows (30 tests), ZTD becomes faster end-to-end.**
252
+ - Serial: ZTD 2480.84 ms vs Traditional 3085.48 ms
253
+ - 4 workers: ZTD 1507.46 ms vs Traditional 1788.35 ms
254
+ - **Parallel execution helps both approaches**, but the improvement is constrained by startup overhead:
255
+ - With 4 workers, the startup share rises (more of the total becomes fixed runner cost), so scaling is not linear.
256
+
257
+ ### Break-even intuition (where ZTD starts to win)
258
+
259
+ From these results, the practical break-even is **between 3 and 30 tests** under the current environment and runner-included setup.
260
+
261
+ Why:
262
+ - Traditional has high per-test DB work (many SQL statements + significant DB time).
263
+ - ZTD has low per-test DB work (few SQL statements), but adds rewrite + fixture overhead.
264
+ - Once the suite is large enough that **execution dominates startup**, ZTD’s reduced DB work overtakes its rewrite/fixture costs.
265
+
266
+ ### ZTD cost structure (what is expensive)
267
+
268
+ ZTD’s incremental work per test is primarily:
269
+ - **SQL-to-ZTD rewrite time**
270
+ - **Fixture materialization time**
271
+ - **DB query time** (typically small compared to Traditional)
272
+
273
+ A concrete view is easiest in the steady-state section below, where the runner is warm.
274
+
275
+ ### Steady-state (runner warm) incremental cost
276
+
277
+ This approximates watch/CI iterations where the runner has already started (first repetition excluded as warmup).
278
+
279
+ | Suite | Workers | Avg incremental time per iteration (ms) | Avg SQL Count | Avg DB time (ms) | Avg rewrite (ms) | Avg fixture (ms) |
280
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
281
+ | Traditional (30 tests) | 1 | 1260.20 | 360 | 1039.98 | - | - |
282
+ | ZTD (30 tests) | 1 | 93.73 | 30 | 30.08 | 32.00 | 20.42 |
283
+ | ZTD (30 tests) | 4 | 91.14 | 30 | 29.95 | 30.75 | 19.40 |
284
+
285
+ ### What this shows
286
+ - Traditional steady-state is dominated by DB time (~1040 ms out of ~1260 ms).
287
+ - ZTD steady-state is dominated by **rewrite (~31 ms) + fixture (~20 ms)**; DB time is ~30 ms.
288
+ - Parallelism has limited impact in ZTD steady-state here because the per-iteration work is already small and may be bounded by coordination / shared overheads.
289
+
290
+ ### Conclusion
291
+
292
+ - **Runner included (realistic)**:
293
+ - For very small suites, startup dominates and Traditional can be faster.
294
+ - For larger suites, ZTD wins end-to-end due to dramatically lower DB work and SQL count.
295
+ - **Parallel execution matters**, but it mainly reduces the execution portion; runner startup becomes the limiting floor.
296
+ - **ZTD’s main costs are rewrite and fixture preparation**, not DB time. This is good news: optimizing rewrite/fixture logic is the highest-leverage path for further speedups.
297
+
298
+ To regenerate the report, run:
299
+
300
+ ```bash
301
+ pnpm ztd:bench
302
+ ```
303
+
304
+ The report is written to `tmp/bench/report.md`.
305
+
145
306
  ## Concepts (Why ZTD?)
146
307
 
147
308
  ### What is ZTD?
@@ -1,6 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  import { type ZtdConfigGenerationOptions } from './ztdConfig';
3
3
  import { type PullSchemaOptions } from './pull';
4
+ type PackageManager = 'pnpm' | 'npm' | 'yarn';
5
+ type PackageInstallKind = 'devDependencies' | 'install';
4
6
  export interface Prompter {
5
7
  selectChoice(question: string, choices: string[]): Promise<number>;
6
8
  promptInput(question: string, example?: string): Promise<string>;
@@ -25,10 +27,19 @@ export interface ZtdConfigWriterDependencies {
25
27
  checkPgDump: () => boolean;
26
28
  log: (message: string) => void;
27
29
  copyAgentsTemplate: (rootDir: string) => string | null;
30
+ installPackages: (options: {
31
+ rootDir: string;
32
+ kind: PackageInstallKind;
33
+ packages: string[];
34
+ packageManager: PackageManager;
35
+ }) => Promise<void> | void;
28
36
  }
29
37
  export interface InitCommandOptions {
30
38
  rootDir?: string;
31
39
  dependencies?: Partial<ZtdConfigWriterDependencies>;
40
+ withSqlClient?: boolean;
41
+ withAppInterface?: boolean;
32
42
  }
33
43
  export declare function runInitCommand(prompter: Prompter, options?: InitCommandOptions): Promise<InitResult>;
34
44
  export declare function registerInitCommand(program: Command): void;
45
+ export {};
@@ -79,12 +79,26 @@ const TESTS_CONFIG_TEMPLATE = 'tests/ztd-layout.generated.ts';
79
79
  const TESTKIT_CLIENT_TEMPLATE = 'tests/support/testkit-client.ts';
80
80
  const GLOBAL_SETUP_TEMPLATE = 'tests/support/global-setup.ts';
81
81
  const VITEST_CONFIG_TEMPLATE = 'vitest.config.ts';
82
+ const SQL_CLIENT_TEMPLATE = 'src/db/sql-client.ts';
82
83
  const NEXT_STEPS = [
83
84
  ' 1. Review the schema files under ztd/ddl/<schema>.sql',
84
85
  ' 2. Inspect tests/generated/ztd-layout.generated.ts for the SQL layout',
85
86
  ' 3. Run npx ztd ztd-config',
86
87
  ' 4. Run ZTD tests with pg-testkit'
87
88
  ];
89
+ const AGENTS_FILE_CANDIDATES = ['AGENTS.md', 'AGENTS_ztd.md'];
90
+ const APP_INTERFACE_SECTION_MARKER = '## Application Interface Guidance';
91
+ const APP_INTERFACE_SECTION = `---
92
+ ## Application Interface Guidance
93
+
94
+ 1. Repository interfaces follow Command in, Domain out so commands capture inputs and repositories return domain shapes.
95
+ 2. Command definitions and validation stay unified to prevent divergence between surface APIs and SQL expectations.
96
+ 3. Input validation relies on zod v4 or later and happens at the repository boundary before any SQL runs.
97
+ 4. Only validated inputs reach SQL execution; reject raw external objects as soon as possible.
98
+ 5. Domain models live under a dedicated src/domain location so semantics stay centralized.
99
+ 6. Build the CRUD behavior first and revisit repository interfaces during review, not before the SQL works.
100
+ 7. Guard every behavioral change with unit tests so regression risks stay low.
101
+ `;
88
102
  function resolveTemplateDirectory() {
89
103
  const candidates = [
90
104
  // Prefer the installed package layout: <pkg>/dist/commands → <pkg>/templates.
@@ -119,7 +133,21 @@ const DEFAULT_DEPENDENCIES = {
119
133
  log: (message) => {
120
134
  console.log(message);
121
135
  },
122
- copyAgentsTemplate: agents_1.copyAgentsTemplate
136
+ copyAgentsTemplate: agents_1.copyAgentsTemplate,
137
+ installPackages: ({ rootDir, kind, packages, packageManager }) => {
138
+ // Use the Windows shim executables so spawnSync finds the package manager in PATH.
139
+ const executable = resolvePackageManagerExecutable(packageManager);
140
+ const args = buildPackageManagerArgs(kind, packageManager, packages);
141
+ if (args.length === 0) {
142
+ return;
143
+ }
144
+ const result = (0, node_child_process_1.spawnSync)(executable, args, { cwd: rootDir, stdio: 'inherit' });
145
+ if (result.error || result.status !== 0) {
146
+ const base = `Failed to run ${packageManager} ${args.join(' ')}`;
147
+ const reason = result.error ? `: ${result.error.message}` : '';
148
+ throw new Error(`${base}${reason}`);
149
+ }
150
+ }
123
151
  };
124
152
  async function runInitCommand(prompter, options) {
125
153
  var _a, _b;
@@ -135,6 +163,7 @@ async function runInitCommand(prompter, options) {
135
163
  ztdConfig: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'generated', 'ztd-row-map.generated.ts'),
136
164
  testsConfig: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'generated', 'ztd-layout.generated.ts'),
137
165
  readme: node_path_1.default.join(rootDir, 'README.md'),
166
+ sqlClient: node_path_1.default.join(rootDir, 'src', 'db', 'sql-client.ts'),
138
167
  testkitClient: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'support', 'testkit-client.ts'),
139
168
  globalSetup: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'support', 'global-setup.ts'),
140
169
  vitestConfig: node_path_1.default.join(rootDir, 'vitest.config.ts'),
@@ -149,6 +178,15 @@ async function runInitCommand(prompter, options) {
149
178
  };
150
179
  const relativePath = (key) => node_path_1.default.relative(rootDir, absolutePaths[key]).replace(/\\/g, '/') || absolutePaths[key];
151
180
  const summaries = {};
181
+ if (options === null || options === void 0 ? void 0 : options.withAppInterface) {
182
+ // Provide the documentation-only path before triggering any scaffolding work.
183
+ const summary = await appendAppInterfaceGuidance(rootDir, dependencies);
184
+ dependencies.log(`Appended application interface guidance to ${summary.relativePath}.`);
185
+ return {
186
+ summary: `App interface guidance appended to ${summary.relativePath}.`,
187
+ files: [summary]
188
+ };
189
+ }
152
190
  // Ask how the user prefers to populate the initial schema.
153
191
  const workflow = await prompter.selectChoice('How do you want to start your database workflow?', ['Pull schema from Postgres (DDL-first)', 'Write DDL manually']);
154
192
  if (workflow === 0) {
@@ -189,7 +227,8 @@ async function runInitCommand(prompter, options) {
189
227
  extensions: options_1.DEFAULT_EXTENSIONS,
190
228
  out: absolutePaths.ztdConfig,
191
229
  defaultSchema: projectConfig.ddl.defaultSchema,
192
- searchPath: projectConfig.ddl.searchPath
230
+ searchPath: projectConfig.ddl.searchPath,
231
+ ddlLint: projectConfig.ddlLint
193
232
  });
194
233
  }
195
234
  else {
@@ -208,6 +247,12 @@ async function runInitCommand(prompter, options) {
208
247
  if (readmeSummary) {
209
248
  summaries.readme = readmeSummary;
210
249
  }
250
+ if (options === null || options === void 0 ? void 0 : options.withSqlClient) {
251
+ const sqlClientSummary = writeOptionalTemplateFile(absolutePaths.sqlClient, relativePath('sqlClient'), SQL_CLIENT_TEMPLATE, dependencies);
252
+ if (sqlClientSummary) {
253
+ summaries.sqlClient = sqlClientSummary;
254
+ }
255
+ }
211
256
  const testkitSummary = await writeTemplateFile(rootDir, absolutePaths.testkitClient, relativePath('testkitClient'), TESTKIT_CLIENT_TEMPLATE, dependencies, prompter);
212
257
  if (testkitSummary) {
213
258
  summaries.testkitClient = testkitSummary;
@@ -234,6 +279,10 @@ async function runInitCommand(prompter, options) {
234
279
  if (ztdDocsReadmeSummary) {
235
280
  summaries.ztdDocsReadme = ztdDocsReadmeSummary;
236
281
  }
282
+ const ztdRootDir = node_path_1.default.join(rootDir, 'ztd');
283
+ // Ensure the domain-specs and enums anchors exist so contributors immediately see where those artifacts belong.
284
+ dependencies.ensureDirectory(node_path_1.default.join(ztdRootDir, 'domain-specs'));
285
+ dependencies.ensureDirectory(node_path_1.default.join(ztdRootDir, 'enums'));
237
286
  const editorconfigSummary = copyTemplateFileIfMissing(rootDir, relativePath('editorconfig'), '.editorconfig', dependencies);
238
287
  if (editorconfigSummary) {
239
288
  summaries.editorconfig = editorconfigSummary;
@@ -259,6 +308,7 @@ async function runInitCommand(prompter, options) {
259
308
  if (agentsRelative) {
260
309
  summaries.agents = agentsRelative;
261
310
  }
311
+ await ensureTemplateDependenciesInstalled(rootDir, absolutePaths, summaries, dependencies);
262
312
  const summaryLines = buildSummaryLines(summaries);
263
313
  summaryLines.forEach(dependencies.log);
264
314
  return {
@@ -267,19 +317,165 @@ async function runInitCommand(prompter, options) {
267
317
  };
268
318
  }
269
319
  async function ensureAgentsFile(rootDir, fallbackRelative, dependencies) {
270
- const templateTarget = dependencies.copyAgentsTemplate(rootDir);
271
- if (templateTarget) {
272
- const relative = node_path_1.default.relative(rootDir, templateTarget).replace(/\\/g, '/');
273
- return { relativePath: relative || fallbackRelative, outcome: 'created' };
320
+ const resolution = resolveOrCreateAgentsFile(rootDir, dependencies);
321
+ if (!resolution) {
322
+ return null;
274
323
  }
275
- const candidates = ['AGENTS.md', 'AGENTS_ztd.md'];
276
- for (const candidate of candidates) {
277
- const candidatePath = node_path_1.default.join(rootDir, candidate);
278
- if (dependencies.fileExists(candidatePath)) {
279
- return { relativePath: candidate, outcome: 'unchanged' };
324
+ const relative = normalizeRelative(rootDir, resolution.absolutePath);
325
+ return {
326
+ relativePath: relative || fallbackRelative,
327
+ outcome: resolution.created ? 'created' : 'unchanged'
328
+ };
329
+ }
330
+ function resolvePackageManagerExecutable(packageManager) {
331
+ if (process.platform !== 'win32') {
332
+ return packageManager;
333
+ }
334
+ if (packageManager === 'npm') {
335
+ return 'npm.cmd';
336
+ }
337
+ if (packageManager === 'pnpm') {
338
+ return 'pnpm.cmd';
339
+ }
340
+ if (packageManager === 'yarn') {
341
+ return 'yarn.cmd';
342
+ }
343
+ return packageManager;
344
+ }
345
+ function buildPackageManagerArgs(kind, packageManager, packages) {
346
+ if (kind === 'install') {
347
+ return ['install'];
348
+ }
349
+ if (packages.length === 0) {
350
+ return [];
351
+ }
352
+ if (packageManager === 'npm') {
353
+ return ['install', '-D', ...packages];
354
+ }
355
+ return ['add', '-D', ...packages];
356
+ }
357
+ function detectPackageManager(rootDir) {
358
+ // Prefer lockfiles to avoid guessing when multiple package managers are installed.
359
+ if ((0, node_fs_1.existsSync)(node_path_1.default.join(rootDir, 'pnpm-lock.yaml'))) {
360
+ return 'pnpm';
361
+ }
362
+ if ((0, node_fs_1.existsSync)(node_path_1.default.join(rootDir, 'yarn.lock'))) {
363
+ return 'yarn';
364
+ }
365
+ if ((0, node_fs_1.existsSync)(node_path_1.default.join(rootDir, 'package-lock.json'))) {
366
+ return 'npm';
367
+ }
368
+ // Fall back to pnpm because rawsql-ts itself standardizes on pnpm.
369
+ return 'pnpm';
370
+ }
371
+ function extractPackageName(specifier) {
372
+ if (specifier.startsWith('.') ||
373
+ specifier.startsWith('/') ||
374
+ specifier.startsWith('node:') ||
375
+ specifier.startsWith('#')) {
376
+ return null;
377
+ }
378
+ if (specifier.startsWith('@')) {
379
+ const [scope, name] = specifier.split('/');
380
+ if (!scope || !name) {
381
+ return null;
280
382
  }
383
+ return `${scope}/${name}`;
384
+ }
385
+ const [name] = specifier.split('/');
386
+ return name || null;
387
+ }
388
+ function listReferencedPackagesFromSource(source) {
389
+ const packages = new Set();
390
+ const patterns = [
391
+ // Capture ESM imports and re-exports, including `import type`.
392
+ /\bfrom\s+['"]([^'"]+)['"]/g,
393
+ // Capture dynamic imports.
394
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
395
+ // Capture CommonJS requires.
396
+ /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g
397
+ ];
398
+ for (const pattern of patterns) {
399
+ for (const match of source.matchAll(pattern)) {
400
+ const specifier = match[1];
401
+ if (!specifier) {
402
+ continue;
403
+ }
404
+ const packageName = extractPackageName(specifier);
405
+ if (!packageName) {
406
+ continue;
407
+ }
408
+ packages.add(packageName);
409
+ }
410
+ }
411
+ return [...packages];
412
+ }
413
+ function listDeclaredPackages(rootDir) {
414
+ const packagePath = node_path_1.default.join(rootDir, 'package.json');
415
+ if (!(0, node_fs_1.existsSync)(packagePath)) {
416
+ return new Set();
417
+ }
418
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(packagePath, 'utf8'));
419
+ const keys = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
420
+ const declared = new Set();
421
+ for (const key of keys) {
422
+ const record = parsed[key];
423
+ if (!record || typeof record !== 'object') {
424
+ continue;
425
+ }
426
+ for (const name of Object.keys(record)) {
427
+ declared.add(name);
428
+ }
429
+ }
430
+ return declared;
431
+ }
432
+ function listTemplateReferencedPackages(absolutePaths, summaries) {
433
+ const packages = new Set(['@rawsql-ts/pg-testkit']);
434
+ const touchedKeys = Object.entries(summaries)
435
+ .filter((entry) => Boolean(entry[1]))
436
+ .filter(([, summary]) => summary.outcome === 'created' || summary.outcome === 'overwritten')
437
+ .map(([key]) => key);
438
+ for (const key of touchedKeys) {
439
+ const filePath = absolutePaths[key];
440
+ if (!filePath.endsWith('.ts') && !filePath.endsWith('.tsx') && !filePath.endsWith('.js')) {
441
+ continue;
442
+ }
443
+ if (!(0, node_fs_1.existsSync)(filePath)) {
444
+ continue;
445
+ }
446
+ // Parse template output after it is written so the detected packages match the emitted scaffold exactly.
447
+ const contents = (0, node_fs_1.readFileSync)(filePath, 'utf8');
448
+ listReferencedPackagesFromSource(contents).forEach((name) => packages.add(name));
449
+ }
450
+ return [...packages].sort();
451
+ }
452
+ async function ensureTemplateDependenciesInstalled(rootDir, absolutePaths, summaries, dependencies) {
453
+ var _a;
454
+ const packageJsonPath = node_path_1.default.join(rootDir, 'package.json');
455
+ if (!dependencies.fileExists(packageJsonPath)) {
456
+ dependencies.log('Skipping dependency installation because package.json is missing.');
457
+ return;
458
+ }
459
+ const packageManager = detectPackageManager(rootDir);
460
+ const referencedPackages = listTemplateReferencedPackages(absolutePaths, summaries);
461
+ const declaredPackages = listDeclaredPackages(rootDir);
462
+ // Install only packages that are not declared yet to avoid unintentionally bumping pinned versions.
463
+ const missingPackages = referencedPackages.filter((name) => !declaredPackages.has(name));
464
+ if (missingPackages.length > 0) {
465
+ dependencies.log(`Installing devDependencies referenced by templates (${packageManager}): ${missingPackages.join(', ')}`);
466
+ await dependencies.installPackages({
467
+ rootDir,
468
+ kind: 'devDependencies',
469
+ packages: missingPackages,
470
+ packageManager
471
+ });
472
+ return;
473
+ }
474
+ // If package.json was updated earlier in the init run, run install so the new entries resolve in node_modules.
475
+ if (((_a = summaries.package) === null || _a === void 0 ? void 0 : _a.outcome) === 'overwritten') {
476
+ dependencies.log(`Running ${packageManager} install to sync dependencies.`);
477
+ await dependencies.installPackages({ rootDir, kind: 'install', packages: [], packageManager });
281
478
  }
282
- return null;
283
479
  }
284
480
  function copyTemplateFileIfMissing(rootDir, relative, templateName, dependencies) {
285
481
  const templatePath = node_path_1.default.join(TEMPLATE_DIRECTORY, templateName);
@@ -297,6 +493,21 @@ function copyTemplateFileIfMissing(rootDir, relative, templateName, dependencies
297
493
  dependencies.writeFile(targetPath, (0, node_fs_1.readFileSync)(templatePath, 'utf8'));
298
494
  return { relativePath: relative, outcome: 'created' };
299
495
  }
496
+ function writeOptionalTemplateFile(absolutePath, relative, templateName, dependencies) {
497
+ const templatePath = node_path_1.default.join(TEMPLATE_DIRECTORY, templateName);
498
+ // Skip when the template is missing from the installed package.
499
+ if (!(0, node_fs_1.existsSync)(templatePath)) {
500
+ return null;
501
+ }
502
+ if (dependencies.fileExists(absolutePath)) {
503
+ // Preserve existing files for opt-in scaffolds without prompting.
504
+ dependencies.log(`Skipping ${relative} because the file already exists.`);
505
+ return { relativePath: relative, outcome: 'unchanged' };
506
+ }
507
+ dependencies.ensureDirectory(node_path_1.default.dirname(absolutePath));
508
+ dependencies.writeFile(absolutePath, (0, node_fs_1.readFileSync)(templatePath, 'utf8'));
509
+ return { relativePath: relative, outcome: 'created' };
510
+ }
300
511
  function ensurePackageJsonFormatting(rootDir, relative, dependencies) {
301
512
  var _a, _b;
302
513
  const packagePath = node_path_1.default.join(rootDir, 'package.json');
@@ -423,6 +634,37 @@ function normalizeRelative(rootDir, absolutePath) {
423
634
  const relative = node_path_1.default.relative(rootDir, absolutePath).replace(/\\/g, '/');
424
635
  return relative || absolutePath;
425
636
  }
637
+ function resolveOrCreateAgentsFile(rootDir, dependencies) {
638
+ // Prefer materializing the bundled template before looking for existing attention files.
639
+ const templateTarget = dependencies.copyAgentsTemplate(rootDir);
640
+ if (templateTarget) {
641
+ return { absolutePath: templateTarget, created: true };
642
+ }
643
+ for (const candidate of AGENTS_FILE_CANDIDATES) {
644
+ const candidatePath = node_path_1.default.join(rootDir, candidate);
645
+ if (dependencies.fileExists(candidatePath)) {
646
+ return { absolutePath: candidatePath, created: false };
647
+ }
648
+ }
649
+ return null;
650
+ }
651
+ async function appendAppInterfaceGuidance(rootDir, dependencies) {
652
+ const resolution = resolveOrCreateAgentsFile(rootDir, dependencies);
653
+ if (!resolution) {
654
+ throw new Error('Failed to locate or create an AGENTS file for application guidance.');
655
+ }
656
+ const relativePath = normalizeRelative(rootDir, resolution.absolutePath);
657
+ const existingContents = (0, node_fs_1.readFileSync)(resolution.absolutePath, 'utf8');
658
+ // Skip appending when the guidance section already exists to avoid duplicates.
659
+ if (existingContents.includes(APP_INTERFACE_SECTION_MARKER)) {
660
+ return { relativePath, outcome: 'unchanged' };
661
+ }
662
+ // Ensure the appended block is separated by blank lines for readability.
663
+ const baseline = existingContents.endsWith('\n') ? existingContents : `${existingContents}\n`;
664
+ const spacer = baseline.endsWith('\n\n') ? '' : '\n';
665
+ dependencies.writeFile(resolution.absolutePath, `${baseline}${spacer}${APP_INTERFACE_SECTION}\n`);
666
+ return { relativePath, outcome: 'overwritten' };
667
+ }
426
668
  function isRootMarkdown(relative) {
427
669
  return relative.toLowerCase().endsWith('.md') && !relative.includes('/');
428
670
  }
@@ -441,6 +683,7 @@ function buildSummaryLines(summaries) {
441
683
  'testsConfig',
442
684
  'ztdConfig',
443
685
  'readme',
686
+ 'sqlClient',
444
687
  'testkitClient',
445
688
  'globalSetup',
446
689
  'vitestConfig',
@@ -472,10 +715,15 @@ function registerInitCommand(program) {
472
715
  program
473
716
  .command('init')
474
717
  .description('Automate project setup for Zero Table Dependency workflows')
475
- .action(async () => {
718
+ .option('--with-sqlclient', 'Generate a minimal SqlClient interface for repositories')
719
+ .option('--with-app-interface', 'Append application interface guidance to AGENTS.md only')
720
+ .action(async (options) => {
476
721
  const prompter = createConsolePrompter();
477
722
  try {
478
- await runInitCommand(prompter);
723
+ await runInitCommand(prompter, {
724
+ withSqlClient: options.withSqlclient,
725
+ withAppInterface: options.withAppInterface
726
+ });
479
727
  }
480
728
  finally {
481
729
  prompter.close();