backend-skeleton 1.2.0 → 1.3.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 +100 -1
- package/bin/bskel.mjs +379 -6
- package/contracts/csv.mjs +100 -0
- package/lib/cli.mjs +62 -1
- package/lib/doctor.mjs +11 -10
- package/lib/workflow.mjs +6 -0
- package/new/index.mjs +20 -0
- package/package.json +5 -2
- package/patterns/schema.sql +18 -0
- package/patterns/store.mjs +122 -0
- package/scanners/adapters/_express-shared.mjs +7 -9
- package/scanners/adapters/java-spring.mjs +7 -9
- package/scanners/adapters/python-fastapi.mjs +7 -9
- package/scanners/db/erd.mjs +0 -0
- package/scanners/index.mjs +70 -17
- package/scanners/render.mjs +12 -3
- package/scanners/text-util.mjs +22 -0
- package/schemas/pattern-record.schema.json +19 -0
- package/schemas/scan-report.schema.json +6 -2
package/README.md
CHANGED
|
@@ -32,9 +32,13 @@ check for a specific failure mode found the same way — see `DECISIONS.md` for
|
|
|
32
32
|
|
|
33
33
|
- [Status: 1.0.0](#status-100)
|
|
34
34
|
- [Quickstart](#quickstart)
|
|
35
|
+
- [Try it in 10 seconds](#try-it-in-10-seconds)
|
|
36
|
+
- [The gated workflow](#the-gated-workflow)
|
|
35
37
|
- [Starting from nothing (greenfield)](#starting-from-nothing-greenfield)
|
|
36
38
|
- [Publishing a feature's contract as OpenAPI (optional)](#publishing-a-features-contract-as-openapi-optional)
|
|
39
|
+
- [A CSV table of a feature's contract (optional)](#a-csv-table-of-a-features-contract-optional)
|
|
37
40
|
- [Database schema (optional)](#database-schema-optional)
|
|
41
|
+
- [An ERD of your database schema (optional)](#an-erd-of-your-database-schema-optional)
|
|
38
42
|
- [Applying DDL to a live database (optional)](#applying-ddl-to-a-live-database-optional)
|
|
39
43
|
- [Declaring field-to-field dependencies (optional)](#declaring-field-to-field-dependencies-optional)
|
|
40
44
|
- [Patching a config file (optional)](#patching-a-config-file-optional)
|
|
@@ -84,6 +88,21 @@ production -- which still splits the same way it always has:
|
|
|
84
88
|
|
|
85
89
|
## Quickstart
|
|
86
90
|
|
|
91
|
+
### Try it in 10 seconds
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install -g backend-skeleton # or: npx backend-skeleton <command>
|
|
95
|
+
cd <any-existing-repo> # must be a git repository -- that's the only requirement
|
|
96
|
+
bskel scan # zero flags: every module/controller/entity/enum this repo's
|
|
97
|
+
# adapter can see, unscored -- no preflight, no feature, no
|
|
98
|
+
# files written, no gate touched
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
That's a read-only look, not the gated workflow — for real feature work (collision-checked against
|
|
102
|
+
a specific idea, contract-gated, codegen), see below.
|
|
103
|
+
|
|
104
|
+
### The gated workflow
|
|
105
|
+
|
|
87
106
|
```bash
|
|
88
107
|
npm install -g backend-skeleton # or: npx backend-skeleton <command>
|
|
89
108
|
cd <target-repo> # must be a git repository
|
|
@@ -193,6 +212,27 @@ bskel new --stack fastapi --slug my-service \
|
|
|
193
212
|
The full parameter list, the measured API-validation matrix behind that split, and the warning
|
|
194
213
|
behaviour are in `D-greenfield-parameters` in `DECISIONS.md`.
|
|
195
214
|
|
|
215
|
+
#### Remembering your own conventions across projects (optional)
|
|
216
|
+
|
|
217
|
+
If you start several projects with the same conventions, `bskel new` can record them into a
|
|
218
|
+
database **you own** -- never bskel's own state, never a shared store:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
export MY_PATTERNS=postgres://localhost/my_patterns # once: run patterns/schema.sql against it
|
|
222
|
+
|
|
223
|
+
bskel new --stack spring --slug billing \
|
|
224
|
+
--java-version 21 --group-id com.acme --dependencies web,data-jpa,validation,flyway \
|
|
225
|
+
--record-pattern --pattern-database-url-env MY_PATTERNS
|
|
226
|
+
|
|
227
|
+
bskel pattern suggest --stack spring --pattern-database-url-env MY_PATTERNS
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`pattern suggest` prints what you've recorded, with per-value frequency, and a ready-to-paste
|
|
231
|
+
command line at the bottom -- it never runs `bskel new` for you and `bskel new` has no flag that
|
|
232
|
+
would accept a suggestion as a default. Every value in a generated project is still one you typed
|
|
233
|
+
in that invocation. Omitting `--record-pattern`/`--pattern-database-url-env` leaves `bskel new`
|
|
234
|
+
exactly as it is today. See `D-pattern-accrual` in `DECISIONS.md`.
|
|
235
|
+
|
|
196
236
|
### Publishing a feature's contract as OpenAPI (optional)
|
|
197
237
|
|
|
198
238
|
```bash
|
|
@@ -232,6 +272,31 @@ contract's paths don't reflect (`--allow-unprefixed` overrides), and stamps ever
|
|
|
232
272
|
reconciling a contract against its own export would make it confirm itself. See `D-openapi-export`
|
|
233
273
|
in `DECISIONS.md`.
|
|
234
274
|
|
|
275
|
+
### A CSV table of a feature's contract (optional)
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
bskel contract export-csv --feature 001-organization-management --out organization.csv
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
One row per operation, opened by someone who will never read a JSON Schema — a PM reviewing scope,
|
|
282
|
+
a lead deciding whether a partial contract is good enough to waive. Fourteen columns, always, in
|
|
283
|
+
the same order: `operation_id, verb, path, path_params, path_params_unverified, body,
|
|
284
|
+
request_body_required, request_body_fields, response_fields, error_fields, provenance, summary,
|
|
285
|
+
tags, security`.
|
|
286
|
+
|
|
287
|
+
**Every column is always present, even when every row leaves it blank** — a scan-only contract
|
|
288
|
+
(no `--openapi-file`) never states `summary`/`tags`/`security`, and the blank columns *are* that
|
|
289
|
+
finding, not something to hide: `export-csv` prints exactly which columns are empty for every
|
|
290
|
+
operation, and why, on stderr. Dropping an empty column would make the file's own shape depend on
|
|
291
|
+
its content, so it never does.
|
|
292
|
+
|
|
293
|
+
**Unlike `contract export`, this command is deliberately UNGATED** — it works even when the
|
|
294
|
+
`contract` gate hasn't passed yet, because its single most valuable moment is reviewing a partial
|
|
295
|
+
contract to decide whether to waive it. An unreflected global path-prefix signal (the thing
|
|
296
|
+
`contract export` hard-refuses on) downgrades to a stderr warning here instead. `--bom` prepends a
|
|
297
|
+
UTF-8 byte-order mark for Excel-on-Windows, which otherwise mangles non-ASCII text; omit it for
|
|
298
|
+
`pandas`/`csv.DictReader`/`diff`, which don't want one. See `D-contract-csv` in `DECISIONS.md`.
|
|
299
|
+
|
|
235
300
|
### Database schema (optional)
|
|
236
301
|
|
|
237
302
|
`bskel scan --db` additionally scans Flyway/Liquibase migration files (local only, no network).
|
|
@@ -249,6 +314,40 @@ report also carries `generated_at` — when the underlying data was actually cap
|
|
|
249
314
|
be judged for staleness rather than trusted blindly. See `D-cross-feature-fk-inference` in
|
|
250
315
|
`DECISIONS.md`.
|
|
251
316
|
|
|
317
|
+
### An ERD of your database schema (optional)
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
bskel db erd --database-url-env BSKEL_DB_URL --schema public --out schema.mmd
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
A Mermaid `erDiagram` of the database — paste it straight into a GitHub/GitLab/Notion/Obsidian
|
|
324
|
+
markdown file (fence it in \`\`\`mermaid) or [mermaid.live](https://mermaid.live) and it renders
|
|
325
|
+
with no extra tooling. Works two ways:
|
|
326
|
+
|
|
327
|
+
- **With `--database-url-env`**: a real, live Postgres introspection — full column types,
|
|
328
|
+
nullability, and primary/foreign keys.
|
|
329
|
+
- **Without it**: falls back to scanning Flyway/Liquibase `.sql` migration files (no network, no
|
|
330
|
+
credentials needed) — a real but **degraded** diagram, clearly marked as such in the file itself:
|
|
331
|
+
every column types as `unknown`, no `PK` badge appears anywhere, and a header block spells out
|
|
332
|
+
exactly what's missing. Useful for evaluating the tool on a repo you don't have DB credentials
|
|
333
|
+
for yet.
|
|
334
|
+
|
|
335
|
+
Two things this diagram deliberately does **not** guess:
|
|
336
|
+
|
|
337
|
+
- **Composite foreign keys.** Postgres's own `information_schema` doesn't retain which source
|
|
338
|
+
column pairs with which target column once a foreign key spans more than one column — the raw
|
|
339
|
+
data is a cross product that can include pairs that were never declared. Rather than draw a wrong
|
|
340
|
+
relationship line, a composite FK collapses to one line labeled with all its source columns
|
|
341
|
+
joined by `+`, with the ambiguity spelled out in a `%%` comment above it. (Composite *primary*
|
|
342
|
+
keys have no such problem and render fully.)
|
|
343
|
+
- **1:1 vs 1:N.** The child side of every relationship is drawn as "zero or more," never "exactly
|
|
344
|
+
one" — telling those apart needs a UNIQUE constraint check this tool doesn't perform. The header
|
|
345
|
+
says so.
|
|
346
|
+
|
|
347
|
+
Whole-schema only in this version (no `--feature`/`--tables` filtering yet) — for a very large
|
|
348
|
+
schema, `db erd` prints a note above 40 entities rather than silently producing an unreadable
|
|
349
|
+
diagram. See `D-db-erd` in `DECISIONS.md`.
|
|
350
|
+
|
|
252
351
|
### Applying DDL to a live database (optional)
|
|
253
352
|
|
|
254
353
|
`bskel patch propose --kind ddl-apply` extends the same propose/approve/apply/rollback lifecycle
|
|
@@ -385,7 +484,7 @@ below).
|
|
|
385
484
|
|---|---|---|
|
|
386
485
|
| Node.js | `>=18` | ES2022 (`Object.hasOwn`) + ESM top-level `await` — nothing newer is used anywhere in the runtime code (verified by grep across every recent-ES-addition pattern; see `D-npm-packaging` in `DECISIONS.md`) |
|
|
387
486
|
| git | required | every gate is git-state-derived |
|
|
388
|
-
| [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) | required for `scan`/`handles` | every scanner adapter shells out to it
|
|
487
|
+
| [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`) | required for `scan`/`handles` | every scanner adapter shells out to it at `detect()` time behind a blanket try/catch — traced live, missing `rg` does NOT throw, it silently makes every real adapter detect nothing (degrades to the low-confidence `generic-grep` fallback). `bskel scan`'s report now carries a `rg_available: false` field plus an explicit `unknowns` warning whenever this happens, so it stays distinguishable from a genuinely-unrecognized repo — see `D-zero-config-scan` in `DECISIONS.md` |
|
|
389
488
|
| `gh` (GitHub CLI) | optional | only used for `preflight`'s 3-way default-branch cross-check; already soft-guarded, never a hard requirement |
|
|
390
489
|
| `python3` | optional | only needed to run this repository's own cross-language codec test — `bskel` itself never invokes `python3` |
|
|
391
490
|
| a build wrapper (`gradlew`/`pom.xml`+`mvnw`/`package.json`) | optional | only `bskel verify --build` needs one; `handles emit` never compiles anything itself |
|
package/bin/bskel.mjs
CHANGED
|
@@ -44,7 +44,8 @@ import {
|
|
|
44
44
|
findCollisions, evaluateCrossFeatureFindings, waiverKey,
|
|
45
45
|
crossFeatureReportPath, loadCrossFeatureReport, loadCrossFeatureResolution, saveCrossFeatureResolution,
|
|
46
46
|
} from '../lib/cross-feature-collisions.mjs';
|
|
47
|
-
import { STACKS as NEW_STACKS, ALL_STACK_PARAMS, stacksAccepting } from '../new/index.mjs';
|
|
47
|
+
import { STACKS as NEW_STACKS, ALL_STACK_PARAMS, stacksAccepting, reusableParamsFor } from '../new/index.mjs';
|
|
48
|
+
import { recordPattern, listPatterns, getPattern, isMissingPatternTable, summarizePatternFrequency } from '../patterns/store.mjs';
|
|
48
49
|
import {
|
|
49
50
|
requireSingleLineText, requireValidJavaPackageName, requireValidArtifactId,
|
|
50
51
|
requireValidPythonVersion, requireValidLicense, requireValidDatabase, requireSupportedJavaVersion,
|
|
@@ -53,6 +54,8 @@ import {
|
|
|
53
54
|
import { DEFAULT_GROUP_ID, DEFAULT_JAVA_VERSION, resolveSpringDependencies } from '../new/spring.mjs';
|
|
54
55
|
import { buildReconciliation, snapshotFromReconciliation, describeSourceFile } from '../contracts/openapi.mjs';
|
|
55
56
|
import { buildOpenApiDocument, pathPrefixCandidates, unreflectedPathPrefixes, STATUS_CODE_MODES } from '../contracts/export.mjs';
|
|
57
|
+
import { buildContractCsv } from '../contracts/csv.mjs';
|
|
58
|
+
import { buildErdDiagram } from '../scanners/db/erd.mjs';
|
|
56
59
|
import { loadCatalogEntry, listCatalogChoices, planApply, applyPlan } from '../stack/apply.mjs';
|
|
57
60
|
import { PROVIDERS, PROVIDER_LOAD_ERRORS, providerById } from '../handles/registry.mjs';
|
|
58
61
|
import { detectAstHelperAvailable, runAstClassify } from '../handles/providers/java-spring/ast-bridge.mjs';
|
|
@@ -65,7 +68,7 @@ import { plan as planTypeScriptExpress } from '../handles/providers/typescript-e
|
|
|
65
68
|
import { emitObserveTypeScriptExpress } from '../handles/providers/typescript-express/observe.mjs';
|
|
66
69
|
import { collectGateStatuses, runBuildCheck, checkArtifacts, checkResolverConflicts } from '../lib/verify.mjs';
|
|
67
70
|
import { computeWorkflowState } from '../lib/workflow.mjs';
|
|
68
|
-
import { computeDoctorChecks, WORKFLOWS as DOCTOR_WORKFLOWS } from '../lib/doctor.mjs';
|
|
71
|
+
import { computeDoctorChecks, WORKFLOWS as DOCTOR_WORKFLOWS, binaryAvailable } from '../lib/doctor.mjs';
|
|
69
72
|
import { parseCommand, renderCommandHelp, diagnostic } from '../lib/cli.mjs';
|
|
70
73
|
import { EXIT_CODES } from '../lib/exit-codes.mjs';
|
|
71
74
|
import { RESIDUAL_TEMPLATE_VAR_RE } from '../lib/template.mjs';
|
|
@@ -76,7 +79,10 @@ const SKILL_ROOT = path.resolve(__dirname, '..');
|
|
|
76
79
|
function usage() {
|
|
77
80
|
console.error(`bskel -- backend-skeleton CLI
|
|
78
81
|
|
|
79
|
-
bskel new --stack spring|fastapi --slug <name> [--dir <path>] [--offline] [--json] [--name <text>] [--description <text>] [--project-version <v>] [--group-id <pkg>] [--artifact-id <id>] [--package-name <pkg>] [--java-version <n>] [--packaging jar|war] [--dependencies a,b,c] [--add-dependencies a,b,c] [--python-version <spec>] [--port N] [--license <spdx>] [--database postgres|sqlite|none]
|
|
82
|
+
bskel new --stack spring|fastapi --slug <name> [--dir <path>] [--offline] [--json] [--name <text>] [--description <text>] [--project-version <v>] [--group-id <pkg>] [--artifact-id <id>] [--package-name <pkg>] [--java-version <n>] [--packaging jar|war] [--dependencies a,b,c] [--add-dependencies a,b,c] [--python-version <spec>] [--port N] [--license <spdx>] [--database postgres|sqlite|none] [--record-pattern --pattern-database-url-env <NAME>]
|
|
83
|
+
bskel pattern list --pattern-database-url-env <NAME> [--stack spring|fastapi] [--json]
|
|
84
|
+
bskel pattern show <pattern_id> --pattern-database-url-env <NAME> [--json]
|
|
85
|
+
bskel pattern suggest --stack spring|fastapi --pattern-database-url-env <NAME> [--json]
|
|
80
86
|
bskel preflight [--max-behind N] [--offline|--no-fetch] [--allow-dirty] [--max-age-minutes N] [--fetch-timeout-seconds N] [--json]
|
|
81
87
|
bskel scan [--feature <id>] [--terms a,b,c] [--json] [--accept-low-confidence] [--db [--database-url-env <NAME>] [--schema public]]
|
|
82
88
|
bskel scan disposition --feature <id> --mode reuse|extend|replace|parallel [--module <name>] [--note "..."] [--breaking-approved]
|
|
@@ -92,6 +98,8 @@ function usage() {
|
|
|
92
98
|
bskel feature archive <id> --reason "..." [--json]
|
|
93
99
|
bskel contract emit --feature <id> [--module <name>] [--json] [--openapi-file <path>] [--path-prefix /api/v0] [--descriptions]
|
|
94
100
|
bskel contract export --feature <id> [--out <path>] [--json] [--allow-unprefixed] [--status-codes range|literal]
|
|
101
|
+
bskel contract export-csv --feature <id> [--out <path>] [--bom] [--json]
|
|
102
|
+
bskel db erd [--database-url-env <NAME>] [--schema public] [--out <path>] [--json]
|
|
95
103
|
bskel contract history --feature <id> [--json]
|
|
96
104
|
bskel contract validate --feature <id> --file <envelope.json>
|
|
97
105
|
bskel contract tool-schema --feature <id> --operation <operationId>
|
|
@@ -605,8 +613,16 @@ async function cmdScan(args) {
|
|
|
605
613
|
setContext('scan', flags);
|
|
606
614
|
const root = requireRepoRoot();
|
|
607
615
|
const terms = deriveTerms(flags);
|
|
608
|
-
|
|
609
|
-
|
|
616
|
+
// D-zero-config-scan: only refuse when the user EXPLICITLY tried to supply terms (or a
|
|
617
|
+
// --feature) and it resolved to nothing -- a real usage mistake worth catching (a `--terms ""`/
|
|
618
|
+
// `--terms ,,` typo, or a --feature slug that pathologically derives zero words), same as
|
|
619
|
+
// before this item. Neither flag given at all is no longer an error -- it's the new zero-flag
|
|
620
|
+
// "inventory" mode (every module this adapter finds, unscored -- see scanners/index.mjs's
|
|
621
|
+
// runScan()), reachable only because the block below still gates it on `!flags.feature` before
|
|
622
|
+
// any gate/file write happens, exactly like today's ad-hoc mode already does.
|
|
623
|
+
const termsFlagGiven = args.some((a) => a === '--terms' || a.startsWith('--terms='));
|
|
624
|
+
if (terms.length === 0 && (termsFlagGiven || flags.feature)) {
|
|
625
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'usage: bskel scan [--feature <id>] [--terms a,b,c] (--terms was given but resolved to no real search term -- pass at least one, e.g. --terms organization, or drop --terms entirely for a full unscored inventory of every module this repo\'s adapter finds)');
|
|
610
626
|
}
|
|
611
627
|
if (flags.feature) {
|
|
612
628
|
requireValidFeatureId(flags.feature);
|
|
@@ -614,6 +630,11 @@ async function cmdScan(args) {
|
|
|
614
630
|
}
|
|
615
631
|
|
|
616
632
|
const dbSchema = await resolveDbSchemaOrExit(root, flags);
|
|
633
|
+
// D-zero-config-scan: the same CLI-boundary-resolved-input pattern `dbSchema` above already
|
|
634
|
+
// establishes, applied to a second external dependency -- computed once, used on BOTH the
|
|
635
|
+
// inventory and the --feature-scoped path (a real Spring repo scanned with --feature+--terms is
|
|
636
|
+
// exactly as vulnerable to the silent detect()-time degradation as the zero-flag case).
|
|
637
|
+
const rgAvailable = binaryAvailable('rg');
|
|
617
638
|
|
|
618
639
|
// G1: a broken adapter file doesn't stop the adapters that DID load, but every `scan` run
|
|
619
640
|
// says so loudly (also see `bskel doctor`, which exits 1 while any of these remain).
|
|
@@ -622,7 +643,7 @@ async function cmdScan(args) {
|
|
|
622
643
|
}
|
|
623
644
|
let report;
|
|
624
645
|
try {
|
|
625
|
-
report = runScan({ repoRoot: root, terms, includeDb: flags.db, dbSchema });
|
|
646
|
+
report = runScan({ repoRoot: root, terms, includeDb: flags.db, dbSchema, rgAvailable });
|
|
626
647
|
} catch (err) {
|
|
627
648
|
// Unreachable with the two shipped adapters (generic-grep's specificity-0 detect() is
|
|
628
649
|
// unconditional) -- becomes reachable the moment a future adapter's detect() is
|
|
@@ -1589,6 +1610,181 @@ function cmdContractExport(args) {
|
|
|
1589
1610
|
process.exitCode = EXIT.PASS;
|
|
1590
1611
|
}
|
|
1591
1612
|
|
|
1613
|
+
// D-contract-csv: the soft-refusal counterpart to loadScanReportOrExit() above -- returns null
|
|
1614
|
+
// (skip the path-prefix check entirely) on ANY failure (missing file, unparseable JSON, schema
|
|
1615
|
+
// violation) instead of exiting. `contract export-csv` treats this check as advisory (see C5 in
|
|
1616
|
+
// D-contract-csv, DECISIONS.md): a scan report that can't be read is not itself a reason to block a
|
|
1617
|
+
// human-reviewed artifact, unlike `contract export`'s own hard guard for a machine-consumed one.
|
|
1618
|
+
function tryLoadScanReport(root, featureId) {
|
|
1619
|
+
const scanReportPath = specPath(root, featureId, 'brownfield-scan.json');
|
|
1620
|
+
if (!fs.existsSync(scanReportPath)) return null;
|
|
1621
|
+
try {
|
|
1622
|
+
const parsed = JSON.parse(fs.readFileSync(scanReportPath, 'utf8'));
|
|
1623
|
+
const { ok } = validateAgainstSchema('scan-report.schema.json', parsed);
|
|
1624
|
+
return ok ? parsed : null;
|
|
1625
|
+
} catch {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// D-contract-csv: a spreadsheet-shaped projection of a feature contract. Mirrors
|
|
1631
|
+
// cmdContractExport's overall shape (loadContract, zero-operation refusal, --out/stdout via
|
|
1632
|
+
// writeFileAtomic) but is deliberately UNGATED -- it never calls requireNamedGate('contract', ...)
|
|
1633
|
+
// and never hard-refuses on an unreflected path prefix, only warns. See D-contract-csv in
|
|
1634
|
+
// DECISIONS.md for the full risk-model argument (a CSV is read by a human deciding whether to
|
|
1635
|
+
// waive a partial contract -- gating it would make it useless exactly when it matters).
|
|
1636
|
+
function cmdContractExportCsv(args) {
|
|
1637
|
+
const flags = parseCommand('contract export-csv', args);
|
|
1638
|
+
if (flags.help) { console.log(renderCommandHelp('contract export-csv')); process.exit(0); }
|
|
1639
|
+
setContext('contract export-csv', flags);
|
|
1640
|
+
const root = requireRepoRoot();
|
|
1641
|
+
requireValidFeatureId(flags.feature);
|
|
1642
|
+
|
|
1643
|
+
const contract = loadContract(root, flags.feature);
|
|
1644
|
+
|
|
1645
|
+
// Same positive-false-claim refusal `contract export` itself makes (see that function's own
|
|
1646
|
+
// comment) -- a zero-row CSV handed to a stakeholder reads as "this feature has no API".
|
|
1647
|
+
if (Object.keys(contract.operations).length === 0) {
|
|
1648
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `\`${flags.feature}\`'s contract has zero operations (completeness: ${contract.completeness.status}) -- exporting it would produce a table positively claiming this API has no operations. Fix --module/--terms and re-run \`bskel contract emit --feature ${flags.feature}\`.`);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// C5 (D-contract-csv): advisory only. If the scan report cannot be read, the check is skipped
|
|
1652
|
+
// silently rather than refusing (contrast cmdContractExport's loadScanReportOrExit(), which hard-exits).
|
|
1653
|
+
let pathPrefixWarning = null;
|
|
1654
|
+
const scanReport = tryLoadScanReport(root, flags.feature);
|
|
1655
|
+
if (scanReport) {
|
|
1656
|
+
const candidates = pathPrefixCandidates(scanReport.path_prefix_signals);
|
|
1657
|
+
const unreflected = unreflectedPathPrefixes(contract, candidates);
|
|
1658
|
+
if (unreflected.length > 0) {
|
|
1659
|
+
pathPrefixWarning = `this repo's scan found a global path-prefix signal (${unreflected.join(', ')}) that ${flags.feature}'s contract paths do not reflect -- the paths in this CSV may be missing it. Re-run \`bskel contract emit --feature ${flags.feature} --openapi-file <real-generated-doc>\` to correct them, or treat this export as informational only.`;
|
|
1660
|
+
console.error(`note: ${pathPrefixWarning}`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const built = buildContractCsv({ contract });
|
|
1665
|
+
if (!built.ok) {
|
|
1666
|
+
// Unreachable given the zero-operation refusal above already covers this case -- kept as
|
|
1667
|
+
// a real check rather than assuming buildContractCsv()'s precondition forever.
|
|
1668
|
+
fail(EXIT_CODES.NOT_PASSED, 'INVALID_ARTIFACT', `cannot export ${flags.feature}'s contract to CSV: ${built.error}`);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// C2 (D-contract-csv): unconditional -- a reader must not have to guess whether a blank column
|
|
1672
|
+
// means "nothing to show" or "the tool is broken".
|
|
1673
|
+
if (built.emptyColumns.length > 0) {
|
|
1674
|
+
console.error(`note: ${built.emptyColumns.length} of ${built.columns.length} columns are empty for every operation (${built.emptyColumns.join(', ')}) -- this contract was emitted without --openapi-file, so no source document ever stated them. Re-run \`bskel contract emit --feature ${flags.feature} --openapi-file <doc>\` to populate them.`);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// C4 (D-contract-csv): BOM is opt-in -- Excel-on-Windows mangles non-ASCII without one, but a
|
|
1678
|
+
// BOM breaks naive parsers/`head`/`diff` for everyone else, so neither default is right for everyone.
|
|
1679
|
+
const csvText = flags.bom ? `\uFEFF${built.csv}` : built.csv;
|
|
1680
|
+
|
|
1681
|
+
if (flags.out) {
|
|
1682
|
+
const outPath = path.resolve(process.cwd(), flags.out);
|
|
1683
|
+
writeFileAtomic(outPath, csvText);
|
|
1684
|
+
if (flags.json) {
|
|
1685
|
+
console.log(JSON.stringify({
|
|
1686
|
+
schema: 'sbf.contract-export-csv/1',
|
|
1687
|
+
feature_id: contract.feature_id,
|
|
1688
|
+
out: flags.out,
|
|
1689
|
+
row_count: built.rowCount,
|
|
1690
|
+
columns: built.columns,
|
|
1691
|
+
completeness: contract.completeness.status,
|
|
1692
|
+
path_prefix_warning: pathPrefixWarning,
|
|
1693
|
+
}, null, 2));
|
|
1694
|
+
} else if (!flags.quiet) {
|
|
1695
|
+
console.log(`wrote ${flags.out} -- ${built.rowCount} operation(s), completeness: ${contract.completeness.status}`);
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
// process.stdout.write, not console.log -- `csvText` already ends in exactly one `\n`
|
|
1699
|
+
// (contracts/csv.mjs's toCsv()), and console.log would append a SECOND one, making stdout
|
|
1700
|
+
// byte-different from the file --out writes. C7 (D-contract-csv): the artifact is the only
|
|
1701
|
+
// thing on stdout here.
|
|
1702
|
+
process.stdout.write(csvText);
|
|
1703
|
+
if (flags.json) {
|
|
1704
|
+
console.error('note: --json has no effect without --out -- stdout is the CSV itself. Pass --out <path> to get both.');
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
// D-process-exit-audit: NOT process.exit() -- same reasoning as cmdContractExport's own
|
|
1708
|
+
// trailing comment; a 300-operation CSV can clear the 64KB pipe buffer just as easily.
|
|
1709
|
+
process.exitCode = EXIT.PASS;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// D-db-erd: a Mermaid `erDiagram` of the database plane -- repo-independent like `bskel new`/
|
|
1713
|
+
// `bskel pattern *` (no --feature; the database plane is not feature-scoped, see E6 in D-db-erd,
|
|
1714
|
+
// DECISIONS.md). Reuses resolveDbSchemaOrExit() unchanged (via a synthetic `db: true`) so
|
|
1715
|
+
// --database-url-env's env-var handling and every error string stay byte-identical to `scan --db`.
|
|
1716
|
+
async function cmdDbErd(args) {
|
|
1717
|
+
const flags = parseCommand('db erd', args);
|
|
1718
|
+
if (flags.help) { console.log(renderCommandHelp('db erd')); process.exit(0); }
|
|
1719
|
+
setContext('db erd', flags);
|
|
1720
|
+
const root = requireRepoRoot();
|
|
1721
|
+
|
|
1722
|
+
// E1 (D-db-erd)'s own `--db` flag doesn't exist on this command -- the verb `db erd` implies
|
|
1723
|
+
// it, so a synthetic `db: true` is threaded through to the exact same helper `scan --db` uses,
|
|
1724
|
+
// never a second copy of its env-var-unset/connection-failure handling.
|
|
1725
|
+
const { live, migrations } = await resolveDbSchemaOrExit(root, { ...flags, db: true });
|
|
1726
|
+
|
|
1727
|
+
if (!live && (!migrations || migrations.tables.length === 0)) {
|
|
1728
|
+
if (migrations && migrations.tool === 'liquibase') {
|
|
1729
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `no schema to draw: this repo's Liquibase changelogs were detected (${migrations.files.length} file(s)) but none are plain .sql -- XML/YAML changelog parsing is not supported (see D-db-schema-plane in DECISIONS.md). Pass --database-url-env <NAME> for live introspection instead.`);
|
|
1730
|
+
}
|
|
1731
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'no schema to draw: no Flyway/Liquibase migration files found in this repo, and no --database-url-env was given. Pass --database-url-env <NAME> (an already-exported environment variable; never read from .env directly -- see D-db-schema-plane in DECISIONS.md) for live introspection.');
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const version = JSON.parse(fs.readFileSync(path.join(SKILL_ROOT, 'package.json'), 'utf8')).version;
|
|
1735
|
+
const invocation = flags['database-url-env']
|
|
1736
|
+
? `bskel db erd --database-url-env ${flags['database-url-env']} --schema ${flags.schema}`
|
|
1737
|
+
: 'bskel db erd';
|
|
1738
|
+
const built = buildErdDiagram({ live, migrations, generatedBy: `bskel ${version} -- \`${invocation}\`` });
|
|
1739
|
+
if (!built.ok) {
|
|
1740
|
+
// Unreachable given the refusal above already covers both reasons buildErdDiagram() can
|
|
1741
|
+
// report -- kept as a real check rather than assuming its precondition forever.
|
|
1742
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `no schema to draw (${built.reason})`);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// E2 (D-db-erd): unconditional when degraded -- a reader must see this before the diagram, not
|
|
1746
|
+
// discover it by noticing every type says `unknown`.
|
|
1747
|
+
if (built.degraded) {
|
|
1748
|
+
console.error(`note: this diagram is DEGRADED -- built from migration files, not a live database. Missing: ${built.missing.join(', ')}. Pass --database-url-env <NAME> for a complete diagram.`);
|
|
1749
|
+
}
|
|
1750
|
+
// E6 (D-db-erd): a readability nudge only, never a refusal -- whole-schema is the only mode
|
|
1751
|
+
// this version supports.
|
|
1752
|
+
if (built.entityCount > 40) {
|
|
1753
|
+
console.error(`note: ${built.entityCount} entities -- this diagram may be dense (whole-schema only in this version; see E6 in D-db-erd, DECISIONS.md).`);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
if (flags.out) {
|
|
1757
|
+
const outPath = path.resolve(process.cwd(), flags.out);
|
|
1758
|
+
writeFileAtomic(outPath, built.mermaid);
|
|
1759
|
+
if (flags.json) {
|
|
1760
|
+
console.log(JSON.stringify({
|
|
1761
|
+
schema: 'sbf.db-erd/1',
|
|
1762
|
+
out: flags.out,
|
|
1763
|
+
plane: built.plane,
|
|
1764
|
+
entity_count: built.entityCount,
|
|
1765
|
+
relationship_count: built.relationshipCount,
|
|
1766
|
+
unresolved_relationships: built.unresolvedRelationships,
|
|
1767
|
+
external_tables: built.externalTables,
|
|
1768
|
+
renames: built.renames,
|
|
1769
|
+
degraded: built.degraded,
|
|
1770
|
+
missing: built.missing,
|
|
1771
|
+
}, null, 2));
|
|
1772
|
+
} else if (!flags.quiet) {
|
|
1773
|
+
console.log(`wrote ${flags.out} -- ${built.entityCount} entity(ies), ${built.relationshipCount} relationship(s), plane: ${built.plane}${built.degraded ? ' (degraded)' : ''}`);
|
|
1774
|
+
}
|
|
1775
|
+
} else {
|
|
1776
|
+
// process.stdout.write, not console.log -- same E10 (D-db-erd) byte-exactness reasoning as
|
|
1777
|
+
// cmdContractExportCsv's own C7 (built.mermaid already ends in exactly one `\n`).
|
|
1778
|
+
process.stdout.write(built.mermaid);
|
|
1779
|
+
if (flags.json) {
|
|
1780
|
+
console.error('note: --json has no effect without --out -- stdout is the diagram itself. Pass --out <path> to get both.');
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
// D-process-exit-audit: NOT process.exit() -- a 200-table diagram can clear the 64KB pipe
|
|
1784
|
+
// buffer just as easily as a schema-rich OpenAPI export.
|
|
1785
|
+
process.exitCode = EXIT.PASS;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1592
1788
|
// A5: the `scan disposition` of contracts -- lets a human explicitly accept a `partial`
|
|
1593
1789
|
// contract's outstanding warnings so the `contract` gate can pass. Deliberately no wildcard
|
|
1594
1790
|
// waiver: `--all` expands to the SPECIFIC code+subject pairs present right now, recorded as
|
|
@@ -3617,6 +3813,132 @@ async function resolveNewParams(stack, flags) {
|
|
|
3617
3813
|
};
|
|
3618
3814
|
}
|
|
3619
3815
|
|
|
3816
|
+
// D-pattern-accrual: all three pattern commands are repo-independent, like `bskel new` itself --
|
|
3817
|
+
// a pattern store is a user-owned CROSS-PROJECT resource, never scoped to the current repo/feature,
|
|
3818
|
+
// so none of them call requireRepoRoot(). Read-only; --pattern-database-url-env is required on all
|
|
3819
|
+
// three (mirrors O7's `handles audit` -- there is no meaningful "run without a live connection" mode).
|
|
3820
|
+
async function cmdPatternList(args) {
|
|
3821
|
+
const flags = parseCommand('pattern list', args);
|
|
3822
|
+
if (flags.help) { console.log(renderCommandHelp('pattern list')); process.exit(0); }
|
|
3823
|
+
setContext('pattern list', flags);
|
|
3824
|
+
|
|
3825
|
+
const connectionString = process.env[flags['pattern-database-url-env']];
|
|
3826
|
+
if (!connectionString) {
|
|
3827
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--pattern-database-url-env ${flags['pattern-database-url-env']} names an environment variable that isn't set -- export it first (never read from .env directly; see D-db-schema-plane in DECISIONS.md)`);
|
|
3828
|
+
}
|
|
3829
|
+
if (flags.stack && !NEW_STACKS[flags.stack]) {
|
|
3830
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--stack must be one of: ${Object.keys(NEW_STACKS).join(', ')} (got ${JSON.stringify(flags.stack)})`);
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
let records;
|
|
3834
|
+
try {
|
|
3835
|
+
records = await listPatterns({ connectionString, stack: flags.stack });
|
|
3836
|
+
} catch (err) {
|
|
3837
|
+
if (isMissingPatternTable(err)) {
|
|
3838
|
+
fail(EXIT_CODES.REFRESH_FAILED, 'REFRESH_FAILED', 'sbf_pattern does not exist in this database -- run patterns/schema.sql against it first (bskel never applies it automatically; see D-migration-scope in DECISIONS.md).');
|
|
3839
|
+
}
|
|
3840
|
+
fail(EXIT_CODES.REFRESH_FAILED, 'REFRESH_FAILED', `could not query the pattern store: ${describeConnectionError(err)}`);
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
if (flags.json) {
|
|
3844
|
+
console.log(JSON.stringify({ records }, null, 2));
|
|
3845
|
+
} else {
|
|
3846
|
+
console.log(`pattern store -- ${records.length} record(s)${flags.stack ? ` (stack: ${flags.stack})` : ''}`);
|
|
3847
|
+
for (const r of records) {
|
|
3848
|
+
console.log(` ${r.pattern_id} [${r.stack}] ${r.recorded_at} ${JSON.stringify(r.params)}`);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
process.exit(0);
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
async function cmdPatternShow(args) {
|
|
3855
|
+
const flags = parseCommand('pattern show', args);
|
|
3856
|
+
if (flags.help) { console.log(renderCommandHelp('pattern show')); process.exit(0); }
|
|
3857
|
+
setContext('pattern show', flags);
|
|
3858
|
+
const patternId = flags._[0];
|
|
3859
|
+
if (!patternId) {
|
|
3860
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', 'usage: bskel pattern show <pattern_id> --pattern-database-url-env <NAME>');
|
|
3861
|
+
}
|
|
3862
|
+
const connectionString = process.env[flags['pattern-database-url-env']];
|
|
3863
|
+
if (!connectionString) {
|
|
3864
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--pattern-database-url-env ${flags['pattern-database-url-env']} names an environment variable that isn't set -- export it first (never read from .env directly; see D-db-schema-plane in DECISIONS.md)`);
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
let record;
|
|
3868
|
+
try {
|
|
3869
|
+
record = await getPattern({ connectionString, patternId });
|
|
3870
|
+
} catch (err) {
|
|
3871
|
+
if (isMissingPatternTable(err)) {
|
|
3872
|
+
fail(EXIT_CODES.REFRESH_FAILED, 'REFRESH_FAILED', 'sbf_pattern does not exist in this database -- run patterns/schema.sql against it first (bskel never applies it automatically; see D-migration-scope in DECISIONS.md).');
|
|
3873
|
+
}
|
|
3874
|
+
fail(EXIT_CODES.REFRESH_FAILED, 'REFRESH_FAILED', `could not query the pattern store: ${describeConnectionError(err)}`);
|
|
3875
|
+
}
|
|
3876
|
+
if (!record) {
|
|
3877
|
+
fail(EXIT_CODES.NOT_PASSED, 'MISSING_ARTIFACT', `no pattern record with id ${patternId}`);
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
if (flags.json) {
|
|
3881
|
+
console.log(JSON.stringify(record, null, 2));
|
|
3882
|
+
} else {
|
|
3883
|
+
console.log(`pattern ${record.pattern_id} [${record.stack}] recorded ${record.recorded_at}`);
|
|
3884
|
+
for (const [k, v] of Object.entries(record.params)) console.log(` --${k} ${v}`);
|
|
3885
|
+
}
|
|
3886
|
+
process.exit(0);
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
// D-pattern-accrual: `suggest`'s output is TEXT only -- a per-value frequency breakdown, honest about
|
|
3890
|
+
// disagreement, plus one paste-ready command line built from the top-ranked value per param. Never
|
|
3891
|
+
// fed into `bskel new` as a default; `bskel new` has no flag that would accept it as one (see
|
|
3892
|
+
// D-pattern-accrual's WHY for why this is the one design decision that keeps this feature inside
|
|
3893
|
+
// D-greenfield-parameters' safe/unsafe line).
|
|
3894
|
+
async function cmdPatternSuggest(args) {
|
|
3895
|
+
const flags = parseCommand('pattern suggest', args);
|
|
3896
|
+
if (flags.help) { console.log(renderCommandHelp('pattern suggest')); process.exit(0); }
|
|
3897
|
+
setContext('pattern suggest', flags);
|
|
3898
|
+
|
|
3899
|
+
const stack = NEW_STACKS[flags.stack];
|
|
3900
|
+
if (!stack) {
|
|
3901
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--stack must be one of: ${Object.keys(NEW_STACKS).join(', ')} (got ${JSON.stringify(flags.stack)})`);
|
|
3902
|
+
}
|
|
3903
|
+
const connectionString = process.env[flags['pattern-database-url-env']];
|
|
3904
|
+
if (!connectionString) {
|
|
3905
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--pattern-database-url-env ${flags['pattern-database-url-env']} names an environment variable that isn't set -- export it first (never read from .env directly; see D-db-schema-plane in DECISIONS.md)`);
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
let records;
|
|
3909
|
+
try {
|
|
3910
|
+
records = await listPatterns({ connectionString, stack: flags.stack });
|
|
3911
|
+
} catch (err) {
|
|
3912
|
+
if (isMissingPatternTable(err)) {
|
|
3913
|
+
fail(EXIT_CODES.REFRESH_FAILED, 'REFRESH_FAILED', 'sbf_pattern does not exist in this database -- run patterns/schema.sql against it first (bskel never applies it automatically; see D-migration-scope in DECISIONS.md).');
|
|
3914
|
+
}
|
|
3915
|
+
fail(EXIT_CODES.REFRESH_FAILED, 'REFRESH_FAILED', `could not query the pattern store: ${describeConnectionError(err)}`);
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
const summary = summarizePatternFrequency(records, stack.reusableParams);
|
|
3919
|
+
const suggestedFlags = summary.map(({ param, values }) => `--${param} ${values[0].value}`).join(' ');
|
|
3920
|
+
const suggestedCommand = `bskel new --stack ${stack.id} --slug <name>${suggestedFlags ? ` ${suggestedFlags}` : ''}`;
|
|
3921
|
+
|
|
3922
|
+
if (flags.json) {
|
|
3923
|
+
console.log(JSON.stringify({ stack: stack.id, total_records: records.length, summary, suggested_command: suggestedCommand }, null, 2));
|
|
3924
|
+
} else {
|
|
3925
|
+
console.log(`pattern suggest -- ${records.length} recorded ${stack.id} run(s)`);
|
|
3926
|
+
if (records.length === 0) {
|
|
3927
|
+
console.log(' (no patterns recorded yet for this stack -- nothing to suggest)');
|
|
3928
|
+
} else {
|
|
3929
|
+
for (const { param, values } of summary) {
|
|
3930
|
+
for (const { value, count, total } of values) {
|
|
3931
|
+
console.log(` --${param} ${value} (${count}/${total} runs)`);
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
console.log('');
|
|
3935
|
+
console.log('suggested command (edit before running -- bskel never applies this automatically):');
|
|
3936
|
+
console.log(` ${suggestedCommand}`);
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
process.exit(0);
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3620
3942
|
async function cmdNew(args) {
|
|
3621
3943
|
const flags = parseCommand('new', args);
|
|
3622
3944
|
if (flags.help) { console.log(renderCommandHelp('new')); process.exit(0); }
|
|
@@ -3628,6 +3950,21 @@ async function cmdNew(args) {
|
|
|
3628
3950
|
}
|
|
3629
3951
|
requireValidSlug(flags.slug);
|
|
3630
3952
|
requireStackParams(stack, flags);
|
|
3953
|
+
// D-pattern-accrual: checked BEFORE any network call or filesystem write, same ordering
|
|
3954
|
+
// principle P2b's own comment states above -- a usage mistake (flag given without its required
|
|
3955
|
+
// partner, or an env var that was never exported) must never leave a half-scaffolded project
|
|
3956
|
+
// behind. The actual DB write later in this function is a SEPARATE, best-effort concern; this
|
|
3957
|
+
// block only validates that recording, if requested, is even POSSIBLE to attempt.
|
|
3958
|
+
let patternConnectionString = null;
|
|
3959
|
+
if (flags['record-pattern']) {
|
|
3960
|
+
if (!flags['pattern-database-url-env']) {
|
|
3961
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', '--record-pattern requires --pattern-database-url-env <NAME>. Nothing was written.');
|
|
3962
|
+
}
|
|
3963
|
+
patternConnectionString = process.env[flags['pattern-database-url-env']];
|
|
3964
|
+
if (!patternConnectionString) {
|
|
3965
|
+
fail(EXIT_CODES.BAD_ARGS, 'BAD_ARGS', `--pattern-database-url-env ${flags['pattern-database-url-env']} names an environment variable that isn't set -- export it first (never read from .env directly; see D-db-schema-plane in DECISIONS.md). Nothing was written.`);
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3631
3968
|
|
|
3632
3969
|
let stackParams;
|
|
3633
3970
|
let warnings;
|
|
@@ -3671,6 +4008,27 @@ async function cmdNew(args) {
|
|
|
3671
4008
|
}
|
|
3672
4009
|
execFileSync('git', commitArgs, { cwd: dir });
|
|
3673
4010
|
|
|
4011
|
+
// D-pattern-accrual: best-effort, deliberately AFTER the project is already scaffolded and
|
|
4012
|
+
// committed -- failing here would be strictly worse than not recording (the project the user
|
|
4013
|
+
// asked for already exists). Only ever a stderr warning, never a fail()/non-zero exit; records
|
|
4014
|
+
// ONLY the flags new/index.mjs's reusableParamsFor(stack) names, and only the ones the user
|
|
4015
|
+
// actually typed (a param the user never passed stays absent from `params`, not defaulted in --
|
|
4016
|
+
// an omission is itself real information for `pattern suggest`, see patterns/store.mjs).
|
|
4017
|
+
if (patternConnectionString) {
|
|
4018
|
+
const patternParams = {};
|
|
4019
|
+
for (const param of reusableParamsFor(stack.id)) {
|
|
4020
|
+
if (flags[param] != null) patternParams[param] = String(flags[param]);
|
|
4021
|
+
}
|
|
4022
|
+
try {
|
|
4023
|
+
await recordPattern({ connectionString: patternConnectionString, stack: stack.id, params: patternParams });
|
|
4024
|
+
} catch (err) {
|
|
4025
|
+
const hint = isMissingPatternTable(err)
|
|
4026
|
+
? 'sbf_pattern does not exist in this database -- run patterns/schema.sql against it first (bskel never applies it automatically; see D-migration-scope).'
|
|
4027
|
+
: describeConnectionError(err);
|
|
4028
|
+
console.error(`warning: --record-pattern could not write to the pattern store: ${hint}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3674
4032
|
const { postScaffoldNotes = [], ...resultRest } = result;
|
|
3675
4033
|
if (flags.json) {
|
|
3676
4034
|
console.log(JSON.stringify({ stack: flags.stack, dir, ...resultRest, warnings, postScaffoldNotes }, null, 2));
|
|
@@ -3779,6 +4137,7 @@ async function dispatchCommand(cmd, rest) {
|
|
|
3779
4137
|
const subArgs = rest.slice(1);
|
|
3780
4138
|
if (sub === 'emit') return cmdContractEmit(subArgs);
|
|
3781
4139
|
if (sub === 'export') return cmdContractExport(subArgs);
|
|
4140
|
+
if (sub === 'export-csv') return cmdContractExportCsv(subArgs);
|
|
3782
4141
|
if (sub === 'history') return cmdContractHistory(subArgs);
|
|
3783
4142
|
if (sub === 'validate') return cmdContractValidate(subArgs);
|
|
3784
4143
|
if (sub === 'tool-schema') return cmdContractToolSchema(subArgs);
|
|
@@ -3871,6 +4230,20 @@ async function dispatchCommand(cmd, rest) {
|
|
|
3871
4230
|
return cmdServe(rest);
|
|
3872
4231
|
case 'new':
|
|
3873
4232
|
return cmdNew(rest);
|
|
4233
|
+
case 'pattern': {
|
|
4234
|
+
if (rest[0] === 'list') return cmdPatternList(rest.slice(1));
|
|
4235
|
+
if (rest[0] === 'show') return cmdPatternShow(rest.slice(1));
|
|
4236
|
+
if (rest[0] === 'suggest') return cmdPatternSuggest(rest.slice(1));
|
|
4237
|
+
usage();
|
|
4238
|
+
process.exit(14);
|
|
4239
|
+
break;
|
|
4240
|
+
}
|
|
4241
|
+
case 'db': {
|
|
4242
|
+
if (rest[0] === 'erd') return await cmdDbErd(rest.slice(1));
|
|
4243
|
+
usage();
|
|
4244
|
+
process.exit(14);
|
|
4245
|
+
break;
|
|
4246
|
+
}
|
|
3874
4247
|
default:
|
|
3875
4248
|
usage();
|
|
3876
4249
|
process.exit(cmd ? 14 : 0);
|