@rawsql-ts/ztd-cli 0.13.2 → 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 +163 -2
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +282 -16
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ztdConfig.d.ts +2 -1
- package/dist/commands/ztdConfig.js +12 -0
- package/dist/commands/ztdConfig.js.map +1 -1
- package/dist/commands/ztdConfigCommand.js +17 -6
- package/dist/commands/ztdConfigCommand.js.map +1 -1
- package/dist/utils/agents.js +19 -2
- package/dist/utils/agents.js.map +1 -1
- package/dist/utils/normalizePulledSchema.js +12 -0
- package/dist/utils/normalizePulledSchema.js.map +1 -1
- package/dist/utils/ztdProjectConfig.d.ts +3 -0
- package/dist/utils/ztdProjectConfig.js +7 -1
- package/dist/utils/ztdProjectConfig.js.map +1 -1
- package/package.json +7 -3
- package/templates/.editorconfig +16 -0
- package/templates/.prettierignore +2 -0
- package/templates/.prettierrc +24 -0
- package/templates/AGENTS.md +326 -0
- package/templates/README.md +239 -0
- package/templates/src/db/sql-client.ts +24 -0
- package/templates/tests/support/global-setup.ts +30 -0
- package/templates/tests/support/testkit-client.ts +742 -0
- package/templates/tests/ztd-layout.generated.ts +8 -0
- package/templates/tsconfig.json +9 -0
- package/templates/vitest.config.ts +13 -0
- package/templates/ztd/AGENTS.md +103 -0
- package/templates/ztd/README.md +84 -0
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?
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/commands/init.js
CHANGED
|
@@ -75,17 +75,49 @@ const SAMPLE_SCHEMA = `CREATE TABLE public.example (
|
|
|
75
75
|
);
|
|
76
76
|
`;
|
|
77
77
|
const README_TEMPLATE = 'README.md';
|
|
78
|
-
const TESTS_CONFIG_TEMPLATE = 'tests/
|
|
78
|
+
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
|
];
|
|
88
|
-
const
|
|
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
|
+
`;
|
|
102
|
+
function resolveTemplateDirectory() {
|
|
103
|
+
const candidates = [
|
|
104
|
+
// Prefer the installed package layout: <pkg>/dist/commands → <pkg>/templates.
|
|
105
|
+
node_path_1.default.resolve(__dirname, '..', '..', '..', 'templates'),
|
|
106
|
+
// Support legacy layouts that copied templates into dist/.
|
|
107
|
+
node_path_1.default.resolve(__dirname, '..', '..', 'templates'),
|
|
108
|
+
// Support running tests directly from the monorepo source tree.
|
|
109
|
+
node_path_1.default.resolve(process.cwd(), 'packages', 'ztd-cli', 'templates')
|
|
110
|
+
];
|
|
111
|
+
// Pick the first directory that contains the expected template entrypoint.
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
if ((0, node_fs_1.existsSync)(node_path_1.default.join(candidate, README_TEMPLATE))) {
|
|
114
|
+
return candidate;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return candidates[0];
|
|
118
|
+
}
|
|
119
|
+
// Resolve templates from a shipped directory so `ztd init` works after `npm install`.
|
|
120
|
+
const TEMPLATE_DIRECTORY = resolveTemplateDirectory();
|
|
89
121
|
const DEFAULT_DEPENDENCIES = {
|
|
90
122
|
ensureDirectory: fs_1.ensureDirectory,
|
|
91
123
|
writeFile: (filePath, contents) => (0, node_fs_1.writeFileSync)(filePath, contents, 'utf8'),
|
|
@@ -101,7 +133,21 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
101
133
|
log: (message) => {
|
|
102
134
|
console.log(message);
|
|
103
135
|
},
|
|
104
|
-
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
|
+
}
|
|
105
151
|
};
|
|
106
152
|
async function runInitCommand(prompter, options) {
|
|
107
153
|
var _a, _b;
|
|
@@ -117,6 +163,7 @@ async function runInitCommand(prompter, options) {
|
|
|
117
163
|
ztdConfig: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'generated', 'ztd-row-map.generated.ts'),
|
|
118
164
|
testsConfig: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'generated', 'ztd-layout.generated.ts'),
|
|
119
165
|
readme: node_path_1.default.join(rootDir, 'README.md'),
|
|
166
|
+
sqlClient: node_path_1.default.join(rootDir, 'src', 'db', 'sql-client.ts'),
|
|
120
167
|
testkitClient: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'support', 'testkit-client.ts'),
|
|
121
168
|
globalSetup: node_path_1.default.join(rootDir, ztdProjectConfig_1.DEFAULT_ZTD_CONFIG.testsDir, 'support', 'global-setup.ts'),
|
|
122
169
|
vitestConfig: node_path_1.default.join(rootDir, 'vitest.config.ts'),
|
|
@@ -131,6 +178,15 @@ async function runInitCommand(prompter, options) {
|
|
|
131
178
|
};
|
|
132
179
|
const relativePath = (key) => node_path_1.default.relative(rootDir, absolutePaths[key]).replace(/\\/g, '/') || absolutePaths[key];
|
|
133
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
|
+
}
|
|
134
190
|
// Ask how the user prefers to populate the initial schema.
|
|
135
191
|
const workflow = await prompter.selectChoice('How do you want to start your database workflow?', ['Pull schema from Postgres (DDL-first)', 'Write DDL manually']);
|
|
136
192
|
if (workflow === 0) {
|
|
@@ -171,7 +227,8 @@ async function runInitCommand(prompter, options) {
|
|
|
171
227
|
extensions: options_1.DEFAULT_EXTENSIONS,
|
|
172
228
|
out: absolutePaths.ztdConfig,
|
|
173
229
|
defaultSchema: projectConfig.ddl.defaultSchema,
|
|
174
|
-
searchPath: projectConfig.ddl.searchPath
|
|
230
|
+
searchPath: projectConfig.ddl.searchPath,
|
|
231
|
+
ddlLint: projectConfig.ddlLint
|
|
175
232
|
});
|
|
176
233
|
}
|
|
177
234
|
else {
|
|
@@ -190,6 +247,12 @@ async function runInitCommand(prompter, options) {
|
|
|
190
247
|
if (readmeSummary) {
|
|
191
248
|
summaries.readme = readmeSummary;
|
|
192
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
|
+
}
|
|
193
256
|
const testkitSummary = await writeTemplateFile(rootDir, absolutePaths.testkitClient, relativePath('testkitClient'), TESTKIT_CLIENT_TEMPLATE, dependencies, prompter);
|
|
194
257
|
if (testkitSummary) {
|
|
195
258
|
summaries.testkitClient = testkitSummary;
|
|
@@ -216,6 +279,10 @@ async function runInitCommand(prompter, options) {
|
|
|
216
279
|
if (ztdDocsReadmeSummary) {
|
|
217
280
|
summaries.ztdDocsReadme = ztdDocsReadmeSummary;
|
|
218
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'));
|
|
219
286
|
const editorconfigSummary = copyTemplateFileIfMissing(rootDir, relativePath('editorconfig'), '.editorconfig', dependencies);
|
|
220
287
|
if (editorconfigSummary) {
|
|
221
288
|
summaries.editorconfig = editorconfigSummary;
|
|
@@ -241,6 +308,7 @@ async function runInitCommand(prompter, options) {
|
|
|
241
308
|
if (agentsRelative) {
|
|
242
309
|
summaries.agents = agentsRelative;
|
|
243
310
|
}
|
|
311
|
+
await ensureTemplateDependenciesInstalled(rootDir, absolutePaths, summaries, dependencies);
|
|
244
312
|
const summaryLines = buildSummaryLines(summaries);
|
|
245
313
|
summaryLines.forEach(dependencies.log);
|
|
246
314
|
return {
|
|
@@ -249,19 +317,165 @@ async function runInitCommand(prompter, options) {
|
|
|
249
317
|
};
|
|
250
318
|
}
|
|
251
319
|
async function ensureAgentsFile(rootDir, fallbackRelative, dependencies) {
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
return { relativePath: relative || fallbackRelative, outcome: 'created' };
|
|
320
|
+
const resolution = resolveOrCreateAgentsFile(rootDir, dependencies);
|
|
321
|
+
if (!resolution) {
|
|
322
|
+
return null;
|
|
256
323
|
}
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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;
|
|
262
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 });
|
|
263
478
|
}
|
|
264
|
-
return null;
|
|
265
479
|
}
|
|
266
480
|
function copyTemplateFileIfMissing(rootDir, relative, templateName, dependencies) {
|
|
267
481
|
const templatePath = node_path_1.default.join(TEMPLATE_DIRECTORY, templateName);
|
|
@@ -279,6 +493,21 @@ function copyTemplateFileIfMissing(rootDir, relative, templateName, dependencies
|
|
|
279
493
|
dependencies.writeFile(targetPath, (0, node_fs_1.readFileSync)(templatePath, 'utf8'));
|
|
280
494
|
return { relativePath: relative, outcome: 'created' };
|
|
281
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
|
+
}
|
|
282
511
|
function ensurePackageJsonFormatting(rootDir, relative, dependencies) {
|
|
283
512
|
var _a, _b;
|
|
284
513
|
const packagePath = node_path_1.default.join(rootDir, 'package.json');
|
|
@@ -405,6 +634,37 @@ function normalizeRelative(rootDir, absolutePath) {
|
|
|
405
634
|
const relative = node_path_1.default.relative(rootDir, absolutePath).replace(/\\/g, '/');
|
|
406
635
|
return relative || absolutePath;
|
|
407
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
|
+
}
|
|
408
668
|
function isRootMarkdown(relative) {
|
|
409
669
|
return relative.toLowerCase().endsWith('.md') && !relative.includes('/');
|
|
410
670
|
}
|
|
@@ -423,6 +683,7 @@ function buildSummaryLines(summaries) {
|
|
|
423
683
|
'testsConfig',
|
|
424
684
|
'ztdConfig',
|
|
425
685
|
'readme',
|
|
686
|
+
'sqlClient',
|
|
426
687
|
'testkitClient',
|
|
427
688
|
'globalSetup',
|
|
428
689
|
'vitestConfig',
|
|
@@ -454,10 +715,15 @@ function registerInitCommand(program) {
|
|
|
454
715
|
program
|
|
455
716
|
.command('init')
|
|
456
717
|
.description('Automate project setup for Zero Table Dependency workflows')
|
|
457
|
-
.
|
|
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) => {
|
|
458
721
|
const prompter = createConsolePrompter();
|
|
459
722
|
try {
|
|
460
|
-
await runInitCommand(prompter
|
|
723
|
+
await runInitCommand(prompter, {
|
|
724
|
+
withSqlClient: options.withSqlclient,
|
|
725
|
+
withAppInterface: options.withAppInterface
|
|
726
|
+
});
|
|
461
727
|
}
|
|
462
728
|
finally {
|
|
463
729
|
prompter.close();
|