backend-skeleton 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/bin/bskel.mjs +2384 -0
- package/contracts/completeness.mjs +176 -0
- package/contracts/emit.mjs +287 -0
- package/contracts/export.mjs +325 -0
- package/contracts/openapi.mjs +869 -0
- package/contracts/validate.mjs +147 -0
- package/handles/_engine.mjs +281 -0
- package/handles/codec.mjs +119 -0
- package/handles/conformance.mjs +74 -0
- package/handles/providers/java-spring/ast-bridge.mjs +59 -0
- package/handles/providers/java-spring/ast-helper/build.gradle +34 -0
- package/handles/providers/java-spring/ast-helper/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/handles/providers/java-spring/ast-helper/gradle/wrapper/gradle-wrapper.properties +9 -0
- package/handles/providers/java-spring/ast-helper/gradlew +248 -0
- package/handles/providers/java-spring/ast-helper/gradlew.bat +82 -0
- package/handles/providers/java-spring/ast-helper/settings.gradle +1 -0
- package/handles/providers/java-spring/ast-helper/src/main/java/com/backendskeleton/asthelper/Main.java +178 -0
- package/handles/providers/java-spring/emit.mjs +232 -0
- package/handles/providers/java-spring/patch-strategy.mjs +229 -0
- package/handles/providers/java-spring/plan.mjs +377 -0
- package/handles/providers/java-spring/templates/HandleAspect.java.tmpl +125 -0
- package/handles/providers/java-spring/templates/HandleCodec.java.tmpl +150 -0
- package/handles/providers/java-spring/templates/HandleController.java.tmpl +177 -0
- package/handles/providers/java-spring/templates/HandleRegistry.java.tmpl +107 -0
- package/handles/providers/java-spring/templates/HandleRegistryRepository.java.tmpl +8 -0
- package/handles/providers/java-spring/templates/HandleService.java.tmpl +95 -0
- package/handles/providers/java-spring/templates/HandleSnapshot.java.tmpl +75 -0
- package/handles/providers/java-spring/templates/HandleSnapshotRepository.java.tmpl +20 -0
- package/handles/providers/java-spring/templates/RecordHandleSnapshot.java.tmpl +50 -0
- package/handles/providers/java-spring/templates/ResourceResolver.java.tmpl +50 -0
- package/handles/providers/java-spring/templates/ResourceResolverStub.java.tmpl +77 -0
- package/handles/providers/java-spring/templates/migration.sql.tmpl +34 -0
- package/handles/providers/java-spring.mjs +21 -0
- package/handles/providers/python-fastapi/emit.mjs +171 -0
- package/handles/providers/python-fastapi/plan.mjs +186 -0
- package/handles/providers/python-fastapi/templates/__init__.py.tmpl +1 -0
- package/handles/providers/python-fastapi/templates/codec.py.tmpl +122 -0
- package/handles/providers/python-fastapi/templates/handle_service.py.tmpl +96 -0
- package/handles/providers/python-fastapi/templates/migration.sql.tmpl +35 -0
- package/handles/providers/python-fastapi/templates/record_snapshot.py.tmpl +155 -0
- package/handles/providers/python-fastapi/templates/registry.py.tmpl +37 -0
- package/handles/providers/python-fastapi/templates/resolver.py.tmpl +59 -0
- package/handles/providers/python-fastapi/templates/resolvers_init.py.tmpl +13 -0
- package/handles/providers/python-fastapi/templates/router.py.tmpl +140 -0
- package/handles/providers/python-fastapi/templates/tables.py.tmpl +66 -0
- package/handles/providers/python-fastapi.mjs +22 -0
- package/handles/providers/typescript-express/emit.mjs +128 -0
- package/handles/providers/typescript-express/plan.mjs +234 -0
- package/handles/providers/typescript-express/templates/codec.ts.tmpl +116 -0
- package/handles/providers/typescript-express/templates/registry.ts.tmpl +39 -0
- package/handles/providers/typescript-express/templates/resolver.ts.tmpl +55 -0
- package/handles/providers/typescript-express/templates/resolvers_index.ts.tmpl +11 -0
- package/handles/providers/typescript-express/templates/router.ts.tmpl +122 -0
- package/handles/providers/typescript-express.mjs +20 -0
- package/handles/registry.mjs +90 -0
- package/lib/cli.mjs +430 -0
- package/lib/doctor.mjs +200 -0
- package/lib/exit-codes.mjs +67 -0
- package/lib/featureid.mjs +55 -0
- package/lib/featurelifecycle.mjs +205 -0
- package/lib/fsutil.mjs +50 -0
- package/lib/gate-definitions.mjs +293 -0
- package/lib/gates.mjs +263 -0
- package/lib/handles-manifest.mjs +92 -0
- package/lib/lock.mjs +68 -0
- package/lib/patch-approvals.mjs +56 -0
- package/lib/paths.mjs +21 -0
- package/lib/repo.mjs +44 -0
- package/lib/schema-validate.mjs +56 -0
- package/lib/state.mjs +124 -0
- package/lib/template.mjs +35 -0
- package/lib/verify.mjs +206 -0
- package/lib/workflow.mjs +142 -0
- package/new/fastapi.mjs +165 -0
- package/new/index.mjs +62 -0
- package/new/params.mjs +233 -0
- package/new/spring.mjs +198 -0
- package/new/templates/fastapi/README.md +26 -0
- package/new/templates/fastapi/app/__init__.py +0 -0
- package/new/templates/fastapi/app/main.py +8 -0
- package/new/templates/fastapi/gitignore +6 -0
- package/new/templates/fastapi/pyproject.toml +14 -0
- package/package.json +50 -0
- package/scanners/adapters/_express-shared.mjs +238 -0
- package/scanners/adapters/_java-spring-analyzer.mjs +273 -0
- package/scanners/adapters/generic-grep.mjs +128 -0
- package/scanners/adapters/java-spring.mjs +301 -0
- package/scanners/adapters/javascript-express.mjs +422 -0
- package/scanners/adapters/python-fastapi.mjs +348 -0
- package/scanners/adapters/typescript-express.mjs +299 -0
- package/scanners/capabilities.mjs +90 -0
- package/scanners/conformance.mjs +59 -0
- package/scanners/db/introspect.mjs +109 -0
- package/scanners/db/migrations.mjs +126 -0
- package/scanners/index.mjs +281 -0
- package/scanners/registry.mjs +130 -0
- package/scanners/render.mjs +136 -0
- package/scanners/text-util.mjs +8 -0
- package/schemas/adapter.schema.json +23 -0
- package/schemas/agent-envelope.schema.json +21 -0
- package/schemas/contract-resolution.schema.json +28 -0
- package/schemas/feature-contract.schema.json +78 -0
- package/schemas/feature-index.schema.json +25 -0
- package/schemas/feature.schema.json +17 -0
- package/schemas/gate-event.schema.json +19 -0
- package/schemas/handles-plan.schema.json +31 -0
- package/schemas/handles-provider.schema.json +26 -0
- package/schemas/patch-approvals.schema.json +28 -0
- package/schemas/scan-report.schema.json +102 -0
- package/schemas/stack-choice.schema.json +89 -0
- package/schemas/stack-record.schema.json +20 -0
- package/schemas/state.schema.json +43 -0
- package/scripts/preflight-base-ref.sh +226 -0
- package/stack/apply.mjs +159 -0
- package/stack/bootstrap/_lib.sh +73 -0
- package/stack/bootstrap/ngrok.sh +90 -0
- package/stack/catalog/ngrok.yml +63 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// G4: zero-registration codegen provider discovery -- mirrors scanners/registry.mjs's exact
|
|
2
|
+
// mechanism (D-adapter-registry, G1), with one deliberate difference: a provider is selected by
|
|
3
|
+
// EXACT id match against scanReport.adapter (bin/bskel.mjs), never arbitrated by specificity --
|
|
4
|
+
// there is nothing to arbitrate since selection is 1:1, not "which of these best matches this
|
|
5
|
+
// repo". See D-handles-providers in DECISIONS.md.
|
|
6
|
+
//
|
|
7
|
+
// SECURITY: `loadProviders({providersDir})` below is a test seam ONLY -- same rule as scanners/
|
|
8
|
+
// registry.mjs's loadAdapters, never wire it to a CLI flag or environment variable.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
13
|
+
|
|
14
|
+
const REGISTRY_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const PROVIDERS_DIR = path.join(REGISTRY_DIR, 'providers');
|
|
16
|
+
const SCHEMAS_ROOT = path.join(REGISTRY_DIR, '..', 'schemas');
|
|
17
|
+
const SUPPORTED_CONTRACT = 'sbf.handles-provider/1';
|
|
18
|
+
|
|
19
|
+
let _ajv = null;
|
|
20
|
+
function ajv() {
|
|
21
|
+
if (!_ajv) _ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
22
|
+
return _ajv;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadProviderSchema() {
|
|
26
|
+
return JSON.parse(fs.readFileSync(path.join(SCHEMAS_ROOT, 'handles-provider.schema.json'), 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Filenames only -- `providers/<id>/` implementation directories don't end in .mjs so they're
|
|
30
|
+
// naturally excluded, no special-casing needed. Same O6-style determinism (.sort()) and `_`/`.`
|
|
31
|
+
// skip convention as scanners/registry.mjs's candidateFiles.
|
|
32
|
+
function candidateFiles(dir) {
|
|
33
|
+
if (!fs.existsSync(dir)) return [];
|
|
34
|
+
return fs.readdirSync(dir)
|
|
35
|
+
.filter((name) => name.endsWith('.mjs') && !name.startsWith('_') && !name.startsWith('.'))
|
|
36
|
+
.sort()
|
|
37
|
+
.map((name) => path.join(dir, name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function loadOneProvider(file, schema) {
|
|
41
|
+
const id = path.basename(file, '.mjs');
|
|
42
|
+
let mod;
|
|
43
|
+
try {
|
|
44
|
+
mod = await import(pathToFileURL(file).href);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { error: { file, message: `failed to load: ${err.message}` } };
|
|
47
|
+
}
|
|
48
|
+
const descriptor = mod.provider;
|
|
49
|
+
if (!descriptor || typeof descriptor !== 'object') {
|
|
50
|
+
return { error: { file, message: 'must `export const provider = {...}` (sbf.handles-provider/1 shape) -- no such export found' } };
|
|
51
|
+
}
|
|
52
|
+
if (descriptor.contract !== SUPPORTED_CONTRACT) {
|
|
53
|
+
return { error: { file, message: `declares contract "${descriptor.contract}" -- this build only understands "${SUPPORTED_CONTRACT}"` } };
|
|
54
|
+
}
|
|
55
|
+
if (descriptor.id !== id) {
|
|
56
|
+
return { error: { file, message: `provider.id "${descriptor.id}" must equal its filename "${id}"` } };
|
|
57
|
+
}
|
|
58
|
+
const { plan, emit, ...jsonShape } = descriptor;
|
|
59
|
+
const validateFn = ajv().getSchema(schema.$id) ?? ajv().compile(schema);
|
|
60
|
+
if (!validateFn(jsonShape)) {
|
|
61
|
+
const details = (validateFn.errors ?? []).map((e) => `${e.instancePath || '(root)'} ${e.message}`).join('; ');
|
|
62
|
+
return { error: { file, message: `does not match schemas/handles-provider.schema.json: ${details}` } };
|
|
63
|
+
}
|
|
64
|
+
for (const fnName of ['plan', 'emit']) {
|
|
65
|
+
if (typeof descriptor[fnName] !== 'function') {
|
|
66
|
+
return { error: { file, message: `provider.${fnName} must be a function` } };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { provider: descriptor };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function loadProviders({ providersDir = PROVIDERS_DIR } = {}) {
|
|
73
|
+
const schema = loadProviderSchema();
|
|
74
|
+
const providers = [];
|
|
75
|
+
const errors = [];
|
|
76
|
+
for (const file of candidateFiles(providersDir)) {
|
|
77
|
+
const result = await loadOneProvider(file, schema);
|
|
78
|
+
if (result.error) errors.push(result.error);
|
|
79
|
+
else providers.push(result.provider);
|
|
80
|
+
}
|
|
81
|
+
return { providers, errors };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function providerById(providers, id) {
|
|
85
|
+
return providers.find((p) => p.id === id) ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Top-level await -- see scanners/registry.mjs's identical note for why this is safe. bin/bskel.mjs's
|
|
89
|
+
// cmdHandlesPlan/cmdHandlesEmit stay synchronous callers of PROVIDERS.
|
|
90
|
+
export const { providers: PROVIDERS, errors: PROVIDER_LOAD_ERRORS } = await loadProviders();
|
package/lib/cli.mjs
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// D2 (D-cli-contract): strict argument parsing (node:util.parseArgs) + the JSON-diagnostic
|
|
2
|
+
// channel every `bskel` command shares. This module is arg parsing + output glue only -- same
|
|
3
|
+
// "CLI stays thin, real logic lives in lib/" split D1's lib/workflow.mjs and D5's lib/doctor.mjs
|
|
4
|
+
// already established. See DECISIONS.md D-cli-contract for the full design and why the catalog's
|
|
5
|
+
// own "every handler returns {ok,code,command,diagnostics,next_actions}" prescription was
|
|
6
|
+
// rejected (it would break several commands' own schema-validated stdout artifacts).
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import { REPO_GATE_ID } from './gate-definitions.mjs';
|
|
9
|
+
|
|
10
|
+
export class CliUsageError extends Error {}
|
|
11
|
+
|
|
12
|
+
const NUMERIC_RE = /^(0|[1-9]\d*)$/;
|
|
13
|
+
|
|
14
|
+
function numericError(flag, def, got) {
|
|
15
|
+
const bounds = [
|
|
16
|
+
def.numeric.min != null ? `>= ${def.numeric.min}` : null,
|
|
17
|
+
def.numeric.max != null ? `<= ${def.numeric.max}` : null,
|
|
18
|
+
].filter(Boolean).join(' and ');
|
|
19
|
+
return `--${flag} must be a whole number${bounds ? ` ${bounds}` : ''} (got "${got}")`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// D-cli-contract: mechanically transcribed from bin/bskel.mjs's pre-D2 `parseFlags(args, {...})`
|
|
23
|
+
// call sites, one entry per command -- `options[flag].type`/`.default` are byte-identical to the
|
|
24
|
+
// old spec. New fields this item adds: `numeric` (string-preserving min/max validation -- no
|
|
25
|
+
// forced Number() coercion, so existing parseInt()/shell-arg-passing call sites need no changes),
|
|
26
|
+
// `required` (existence-only; on failure this ALWAYS prints the command's own `usage` line, which
|
|
27
|
+
// is exactly what every pre-D2 required-flag check already did -- see the grounding audit in
|
|
28
|
+
// DECISIONS.md), `hidden` (kept out of --help and the usage()<->COMMANDS drift test -- unused as
|
|
29
|
+
// of A4, which gave `scan`'s own `--db` a real implementation and its own usage() documentation).
|
|
30
|
+
export const COMMANDS = {
|
|
31
|
+
preflight: {
|
|
32
|
+
usage: 'bskel preflight [--max-behind N] [--offline|--no-fetch] [--allow-dirty] [--max-age-minutes N] [--fetch-timeout-seconds N] [--json]',
|
|
33
|
+
options: {
|
|
34
|
+
'max-behind': { type: 'string', default: '0', numeric: { min: 0 } },
|
|
35
|
+
// D-preflight-freshness (S3): --offline is the real name; --no-fetch is kept as an
|
|
36
|
+
// exact alias (see scripts/preflight-base-ref.sh's own comment for why it isn't
|
|
37
|
+
// removed). Both declared here so neither is rejected as unknown.
|
|
38
|
+
offline: { type: 'boolean', default: false },
|
|
39
|
+
'no-fetch': { type: 'boolean', default: false },
|
|
40
|
+
'allow-dirty': { type: 'boolean', default: false },
|
|
41
|
+
// D-preflight-freshness (S3): how long a passed preflight stays fresh before `gate
|
|
42
|
+
// require`/downstream commands treat it as stale purely due to age (see
|
|
43
|
+
// lib/gate-definitions.mjs's `freshness` declaration for the 30-minute default and its
|
|
44
|
+
// data-derived justification). 0 disables the TTL entirely.
|
|
45
|
+
'max-age-minutes': { type: 'string', default: '30', numeric: { min: 0 } },
|
|
46
|
+
'fetch-timeout-seconds': { type: 'string', default: '60', numeric: { min: 0 } },
|
|
47
|
+
json: { type: 'boolean', default: false },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
'gate require': {
|
|
51
|
+
usage: 'bskel gate require <name> [--feature <id>]',
|
|
52
|
+
options: { feature: { type: 'string', default: REPO_GATE_ID } },
|
|
53
|
+
allowPositionals: true,
|
|
54
|
+
},
|
|
55
|
+
'gate force': {
|
|
56
|
+
usage: 'bskel gate force <name> --reason "..." [--feature <id>] [--max-age-minutes N]',
|
|
57
|
+
options: {
|
|
58
|
+
feature: { type: 'string', default: REPO_GATE_ID },
|
|
59
|
+
reason: { type: 'string', default: '' },
|
|
60
|
+
'max-age-minutes': { type: 'string', default: null },
|
|
61
|
+
},
|
|
62
|
+
allowPositionals: true,
|
|
63
|
+
},
|
|
64
|
+
'gate revoke': {
|
|
65
|
+
usage: 'bskel gate revoke <name> --reason "..." [--feature <id>]',
|
|
66
|
+
options: {
|
|
67
|
+
feature: { type: 'string', default: REPO_GATE_ID },
|
|
68
|
+
reason: { type: 'string', default: '' },
|
|
69
|
+
},
|
|
70
|
+
allowPositionals: true,
|
|
71
|
+
},
|
|
72
|
+
'gate history': {
|
|
73
|
+
usage: 'bskel gate history <name> [--feature <id>] [--json]',
|
|
74
|
+
options: {
|
|
75
|
+
feature: { type: 'string', default: REPO_GATE_ID },
|
|
76
|
+
json: { type: 'boolean', default: false },
|
|
77
|
+
},
|
|
78
|
+
allowPositionals: true,
|
|
79
|
+
},
|
|
80
|
+
'gate show': {
|
|
81
|
+
usage: 'bskel gate show [<name>] [--feature <id>]',
|
|
82
|
+
options: { feature: { type: 'string', default: REPO_GATE_ID } },
|
|
83
|
+
allowPositionals: true,
|
|
84
|
+
},
|
|
85
|
+
scan: {
|
|
86
|
+
usage: 'bskel scan [--feature <id>] [--terms a,b,c] [--json] [--accept-low-confidence] [--db [--database-url-env <NAME>] [--schema public]]',
|
|
87
|
+
options: {
|
|
88
|
+
feature: { type: 'string', default: null },
|
|
89
|
+
terms: { type: 'string', default: '' },
|
|
90
|
+
// A4 (D-db-schema-plane): --db now does something real (Plane A migration-file scan,
|
|
91
|
+
// Plane C live introspection when --database-url-env is also given) -- no longer a
|
|
92
|
+
// documented-but-inert placeholder, so no longer `hidden`.
|
|
93
|
+
db: { type: 'boolean', default: false },
|
|
94
|
+
'database-url-env': { type: 'string', default: null },
|
|
95
|
+
schema: { type: 'string', default: 'public' },
|
|
96
|
+
json: { type: 'boolean', default: false },
|
|
97
|
+
'accept-low-confidence': { type: 'boolean', default: false },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
'scan disposition': {
|
|
101
|
+
usage: 'bskel scan disposition --feature <id> --mode reuse|extend|replace|parallel [--module <name>] [--note "..."] [--breaking-approved]',
|
|
102
|
+
options: {
|
|
103
|
+
feature: { type: 'string', default: null, required: true },
|
|
104
|
+
mode: { type: 'string', default: null },
|
|
105
|
+
// S2 (D-gate-precision, part 2): optional -- defaults to the same top-scored module
|
|
106
|
+
// contracts/emit.mjs's selectModule() would ALSO pick with no --module override, so an
|
|
107
|
+
// omitted flag here never silently disagrees with what `contract emit`/`handles plan`
|
|
108
|
+
// actually use by default.
|
|
109
|
+
module: { type: 'string', default: null },
|
|
110
|
+
note: { type: 'string', default: '' },
|
|
111
|
+
'breaking-approved': { type: 'boolean', default: false },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
'scan explain': {
|
|
115
|
+
usage: 'bskel scan explain <module> --feature <id> [--json]',
|
|
116
|
+
options: {
|
|
117
|
+
feature: { type: 'string', default: null, required: true },
|
|
118
|
+
json: { type: 'boolean', default: false },
|
|
119
|
+
},
|
|
120
|
+
allowPositionals: true,
|
|
121
|
+
},
|
|
122
|
+
'feature init': {
|
|
123
|
+
usage: 'bskel feature init --slug <name>',
|
|
124
|
+
options: { slug: { type: 'string', default: null, required: true } },
|
|
125
|
+
},
|
|
126
|
+
'feature list': {
|
|
127
|
+
usage: 'bskel feature list [--all] [--json]',
|
|
128
|
+
options: { all: { type: 'boolean', default: false }, json: { type: 'boolean', default: false } },
|
|
129
|
+
},
|
|
130
|
+
'feature show': {
|
|
131
|
+
usage: 'bskel feature show <id> [--json]',
|
|
132
|
+
options: { json: { type: 'boolean', default: false } },
|
|
133
|
+
allowPositionals: true,
|
|
134
|
+
},
|
|
135
|
+
'feature rename': {
|
|
136
|
+
usage: 'bskel feature rename <id> --to <new-slug> --reason "..." [--json]',
|
|
137
|
+
options: {
|
|
138
|
+
to: { type: 'string', default: null, required: true },
|
|
139
|
+
reason: { type: 'string', default: '' },
|
|
140
|
+
json: { type: 'boolean', default: false },
|
|
141
|
+
},
|
|
142
|
+
allowPositionals: true,
|
|
143
|
+
},
|
|
144
|
+
'feature link': {
|
|
145
|
+
usage: 'bskel feature link <keepId> <aliasId> --reason "..." [--json]',
|
|
146
|
+
options: { reason: { type: 'string', default: '' }, json: { type: 'boolean', default: false } },
|
|
147
|
+
allowPositionals: true,
|
|
148
|
+
},
|
|
149
|
+
'feature archive': {
|
|
150
|
+
usage: 'bskel feature archive <id> --reason "..." [--json]',
|
|
151
|
+
options: { reason: { type: 'string', default: '' }, json: { type: 'boolean', default: false } },
|
|
152
|
+
allowPositionals: true,
|
|
153
|
+
},
|
|
154
|
+
'contract emit': {
|
|
155
|
+
usage: 'bskel contract emit --feature <id> [--module <name>] [--json] [--openapi-file <path>] [--path-prefix /api/v0]',
|
|
156
|
+
options: {
|
|
157
|
+
feature: { type: 'string', default: null, required: true },
|
|
158
|
+
module: { type: 'string', default: null },
|
|
159
|
+
json: { type: 'boolean', default: false },
|
|
160
|
+
'openapi-file': { type: 'string', default: null },
|
|
161
|
+
'path-prefix': { type: 'string', default: null },
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
// A6 (D-openapi-export): the export direction. `--allow-unprefixed` is deliberately NOT a
|
|
165
|
+
// default-on convenience -- it overrides a refusal that exists because the scan found a global
|
|
166
|
+
// path-prefix signal the contract's own paths don't reflect, which is a wrong-paths-handed-to-a
|
|
167
|
+
// -client-generator risk, not a cosmetic one. `--status-codes` defaults to `range` (invents
|
|
168
|
+
// nothing); `literal` trades that for tooling compatibility, with a printed caveat.
|
|
169
|
+
'contract export': {
|
|
170
|
+
usage: 'bskel contract export --feature <id> [--out <path>] [--json] [--allow-unprefixed] [--status-codes range|literal]',
|
|
171
|
+
options: {
|
|
172
|
+
feature: { type: 'string', default: null, required: true },
|
|
173
|
+
out: { type: 'string', default: null },
|
|
174
|
+
'allow-unprefixed': { type: 'boolean', default: false },
|
|
175
|
+
'status-codes': { type: 'string', default: 'range' },
|
|
176
|
+
json: { type: 'boolean', default: false },
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
'contract waive': {
|
|
180
|
+
usage: 'bskel contract waive --feature <id> --code <CODE> (--subject "VERB /path" | --all) --reason "..."',
|
|
181
|
+
options: {
|
|
182
|
+
feature: { type: 'string', default: null, required: true },
|
|
183
|
+
code: { type: 'string', default: null, required: true },
|
|
184
|
+
subject: { type: 'string', default: null },
|
|
185
|
+
all: { type: 'boolean', default: false },
|
|
186
|
+
reason: { type: 'string', default: '' },
|
|
187
|
+
json: { type: 'boolean', default: false },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
'contract validate': {
|
|
191
|
+
usage: 'bskel contract validate --feature <id> --file <envelope.json>',
|
|
192
|
+
options: {
|
|
193
|
+
feature: { type: 'string', default: null, required: true },
|
|
194
|
+
file: { type: 'string', default: null, required: true },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
'contract tool-schema': {
|
|
198
|
+
usage: 'bskel contract tool-schema --feature <id> --operation <operationId>',
|
|
199
|
+
options: {
|
|
200
|
+
feature: { type: 'string', default: null, required: true },
|
|
201
|
+
operation: { type: 'string', default: null, required: true },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
'stack apply': {
|
|
205
|
+
usage: 'bskel stack apply --choice <id> [--apply] [--port N] [--json]',
|
|
206
|
+
options: {
|
|
207
|
+
choice: { type: 'string', default: null },
|
|
208
|
+
apply: { type: 'boolean', default: false },
|
|
209
|
+
port: { type: 'string', default: '8080', numeric: { min: 1, max: 65535 } },
|
|
210
|
+
json: { type: 'boolean', default: false },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
'catalog lint': {
|
|
214
|
+
usage: 'bskel catalog lint [<choice>] [--json]',
|
|
215
|
+
options: { json: { type: 'boolean', default: false } },
|
|
216
|
+
allowPositionals: true,
|
|
217
|
+
},
|
|
218
|
+
'handles plan': {
|
|
219
|
+
usage: 'bskel handles plan --feature <id> [--module <name>] [--resource type1,type2] [--diff] [--ast]',
|
|
220
|
+
options: {
|
|
221
|
+
feature: { type: 'string', default: null, required: true },
|
|
222
|
+
module: { type: 'string', default: null },
|
|
223
|
+
resource: { type: 'string', default: '' },
|
|
224
|
+
diff: { type: 'boolean', default: false },
|
|
225
|
+
// A2 Phase 2 (D-java-ast-helper): opt-in only -- runs the real JavaParser + Symbol
|
|
226
|
+
// Solver helper alongside the always-on regex classification and reports any
|
|
227
|
+
// disagreement. Never automatic; see ast-bridge.mjs.
|
|
228
|
+
ast: { type: 'boolean', default: false },
|
|
229
|
+
json: { type: 'boolean', default: false },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
'handles emit': {
|
|
233
|
+
usage: 'bskel handles emit --feature <id> [--module <name>] [--resource type1,type2] [--force --reason "..."] [--check] [--diff]',
|
|
234
|
+
options: {
|
|
235
|
+
feature: { type: 'string', default: null, required: true },
|
|
236
|
+
module: { type: 'string', default: null },
|
|
237
|
+
resource: { type: 'string', default: '' },
|
|
238
|
+
force: { type: 'boolean', default: false },
|
|
239
|
+
reason: { type: 'string', default: '' },
|
|
240
|
+
check: { type: 'boolean', default: false },
|
|
241
|
+
diff: { type: 'boolean', default: false },
|
|
242
|
+
json: { type: 'boolean', default: false },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
// P2b (D-greenfield-parameters): every parameter below defaults to `null` rather than to its
|
|
246
|
+
// real default value, on purpose -- `cmdNew` has to distinguish "not passed" from "passed the
|
|
247
|
+
// same value the default happens to be" to (a) reject a Spring-only flag given with --stack
|
|
248
|
+
// fastapi and vice versa, and (b) avoid an extra network round-trip validating a --java-version
|
|
249
|
+
// nobody actually asked for. The real defaults live in new/spring.mjs / new/fastapi.mjs, one
|
|
250
|
+
// place each.
|
|
251
|
+
new: {
|
|
252
|
+
usage: '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]',
|
|
253
|
+
options: {
|
|
254
|
+
stack: { type: 'string', default: null, required: true },
|
|
255
|
+
slug: { type: 'string', default: null, required: true },
|
|
256
|
+
dir: { type: 'string', default: null },
|
|
257
|
+
offline: { type: 'boolean', default: false },
|
|
258
|
+
|
|
259
|
+
// Both stacks.
|
|
260
|
+
name: { type: 'string', default: null },
|
|
261
|
+
description: { type: 'string', default: null },
|
|
262
|
+
// NOT `--version`: that is already a GLOBAL flag intercepted in main() before any
|
|
263
|
+
// command-specific parsing (it prints `bskel <version>`), so a command-level --version
|
|
264
|
+
// would be unreachable. This names the GENERATED project's own version field.
|
|
265
|
+
'project-version': { type: 'string', default: null },
|
|
266
|
+
|
|
267
|
+
// --stack spring only.
|
|
268
|
+
'group-id': { type: 'string', default: null },
|
|
269
|
+
'artifact-id': { type: 'string', default: null },
|
|
270
|
+
'package-name': { type: 'string', default: null },
|
|
271
|
+
'java-version': { type: 'string', default: null },
|
|
272
|
+
packaging: { type: 'string', default: null },
|
|
273
|
+
dependencies: { type: 'string', default: null },
|
|
274
|
+
'add-dependencies': { type: 'string', default: null },
|
|
275
|
+
|
|
276
|
+
// --stack fastapi only. `--port`'s numeric bounds mirror `stack apply --port` exactly
|
|
277
|
+
// (the default differs -- 8000 is uvicorn's, 8080 is Spring's -- but the VALIDATION shape
|
|
278
|
+
// is the one this CLI already declares for a port).
|
|
279
|
+
'python-version': { type: 'string', default: null },
|
|
280
|
+
port: { type: 'string', default: null, numeric: { min: 1, max: 65535 } },
|
|
281
|
+
license: { type: 'string', default: null },
|
|
282
|
+
database: { type: 'string', default: null },
|
|
283
|
+
|
|
284
|
+
// Declared-but-hidden: accepted by the parser purely so `cmdNew` can answer with the
|
|
285
|
+
// SPECIFIC reason each is refused (new/index.mjs's SPRING_REFUSED_PARAMS) instead of a
|
|
286
|
+
// bare "Unknown option". `hidden` keeps them out of --help and out of
|
|
287
|
+
// test/doc-integrity.test.mjs's usage()<->COMMANDS flag-set equality check.
|
|
288
|
+
type: { type: 'string', default: null, hidden: true },
|
|
289
|
+
language: { type: 'string', default: null, hidden: true },
|
|
290
|
+
'boot-version': { type: 'string', default: null, hidden: true },
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
'handles patch approve': {
|
|
294
|
+
usage: 'bskel handles patch approve --feature <id> [--module <name>] --resource <Type> --field <name> --strategy patch-wrapper|null-means-unchanged --reason "..." [--json]',
|
|
295
|
+
options: {
|
|
296
|
+
feature: { type: 'string', default: null, required: true },
|
|
297
|
+
module: { type: 'string', default: null },
|
|
298
|
+
resource: { type: 'string', default: null, required: true },
|
|
299
|
+
field: { type: 'string', default: null, required: true },
|
|
300
|
+
strategy: { type: 'string', default: null, required: true },
|
|
301
|
+
reason: { type: 'string', default: '' },
|
|
302
|
+
json: { type: 'boolean', default: false },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
verify: {
|
|
306
|
+
usage: 'bskel verify --feature <id> [--build [--allow-skip-build]] [--json]',
|
|
307
|
+
options: {
|
|
308
|
+
feature: { type: 'string', default: null, required: true },
|
|
309
|
+
build: { type: 'boolean', default: false },
|
|
310
|
+
// S6 (D-verify-integrity): an explicit --build request must not silently no-op when no
|
|
311
|
+
// build tool is recognized -- this is the one opt-out, matching --force/--offline's own
|
|
312
|
+
// "explicit request needs an explicit escape hatch" shape elsewhere in this CLI.
|
|
313
|
+
'allow-skip-build': { type: 'boolean', default: false },
|
|
314
|
+
json: { type: 'boolean', default: false },
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
status: {
|
|
318
|
+
usage: 'bskel status [--feature <id>] [--json]',
|
|
319
|
+
options: {
|
|
320
|
+
feature: { type: 'string', default: null },
|
|
321
|
+
json: { type: 'boolean', default: false },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
next: {
|
|
325
|
+
usage: 'bskel next [--feature <id>] [--json]',
|
|
326
|
+
options: {
|
|
327
|
+
feature: { type: 'string', default: null },
|
|
328
|
+
json: { type: 'boolean', default: false },
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
doctor: {
|
|
332
|
+
usage: 'bskel doctor [--workflow scan|handles|stack] [--json]',
|
|
333
|
+
options: {
|
|
334
|
+
workflow: { type: 'string', default: null },
|
|
335
|
+
json: { type: 'boolean', default: false },
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
function describeParseArgsError(err, spec) {
|
|
341
|
+
const known = [
|
|
342
|
+
...Object.keys(spec.options).filter((f) => !spec.options[f].hidden),
|
|
343
|
+
'help', 'json', 'quiet',
|
|
344
|
+
].sort().map((f) => `--${f}`).join(', ');
|
|
345
|
+
return `${err.message}\nusage: ${spec.usage}\nknown flags: ${known}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Returns the SAME shape `parseFlags()` (pre-D2) returned: `{ _: [...positionals], ...values }`
|
|
349
|
+
// -- every existing `flags.feature`/`flags._[0]`/`flags['max-behind']` call site in bin/bskel.mjs
|
|
350
|
+
// needed zero changes for this. `flags.help === true` short-circuits BEFORE required-field
|
|
351
|
+
// validation (so `bskel handles emit --help` renders help instead of failing on missing
|
|
352
|
+
// --feature). Throws CliUsageError -- never calls process.exit itself, so this stays unit-testable
|
|
353
|
+
// without spawning a process (see test/cli-contract.test.mjs).
|
|
354
|
+
export function parseCommand(name, argv) {
|
|
355
|
+
const spec = COMMANDS[name];
|
|
356
|
+
if (!spec) throw new Error(`bskel-internal: no COMMANDS entry for "${name}"`);
|
|
357
|
+
|
|
358
|
+
const parseArgsOptions = { help: { type: 'boolean' }, quiet: { type: 'boolean' } };
|
|
359
|
+
for (const [flag, def] of Object.entries(spec.options)) parseArgsOptions[flag] = { type: def.type };
|
|
360
|
+
parseArgsOptions.json ??= { type: 'boolean' };
|
|
361
|
+
|
|
362
|
+
let parsed;
|
|
363
|
+
try {
|
|
364
|
+
parsed = parseArgs({
|
|
365
|
+
args: argv,
|
|
366
|
+
options: parseArgsOptions,
|
|
367
|
+
strict: true,
|
|
368
|
+
allowPositionals: Boolean(spec.allowPositionals),
|
|
369
|
+
});
|
|
370
|
+
} catch (err) {
|
|
371
|
+
throw new CliUsageError(describeParseArgsError(err, spec));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const out = { _: parsed.positionals ?? [] };
|
|
375
|
+
for (const [flag, def] of Object.entries(spec.options)) {
|
|
376
|
+
out[flag] = Object.hasOwn(parsed.values, flag) ? parsed.values[flag] : def.default;
|
|
377
|
+
}
|
|
378
|
+
out.json = parsed.values.json ?? false;
|
|
379
|
+
out.quiet = Boolean(parsed.values.quiet);
|
|
380
|
+
out.help = Boolean(parsed.values.help);
|
|
381
|
+
|
|
382
|
+
for (const [flag, def] of Object.entries(spec.options)) {
|
|
383
|
+
if (!def.numeric || out[flag] == null) continue;
|
|
384
|
+
const value = out[flag];
|
|
385
|
+
if (!NUMERIC_RE.test(value) || (def.numeric.min != null && Number(value) < def.numeric.min) || (def.numeric.max != null && Number(value) > def.numeric.max)) {
|
|
386
|
+
throw new CliUsageError(numericError(flag, def, value));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (out.help) return out;
|
|
391
|
+
|
|
392
|
+
for (const [flag, def] of Object.entries(spec.options)) {
|
|
393
|
+
if (def.required && !out[flag]) throw new CliUsageError(`usage: ${spec.usage}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function renderCommandHelp(name) {
|
|
400
|
+
const spec = COMMANDS[name];
|
|
401
|
+
const lines = [`usage: ${spec.usage}`, '', 'flags:'];
|
|
402
|
+
for (const [flag, def] of Object.entries(spec.options)) {
|
|
403
|
+
if (def.hidden) continue;
|
|
404
|
+
const parts = [def.type];
|
|
405
|
+
if (def.default != null && def.type !== 'boolean') parts.push(`default: ${def.default}`);
|
|
406
|
+
if (def.required) parts.push('required');
|
|
407
|
+
lines.push(` --${flag} (${parts.join(', ')})`);
|
|
408
|
+
}
|
|
409
|
+
lines.push(' --json (boolean)');
|
|
410
|
+
lines.push(' --quiet (boolean)');
|
|
411
|
+
lines.push(' --help (boolean)');
|
|
412
|
+
lines.push('', "see 'bskel --help' for the full command list");
|
|
413
|
+
return `${lines.join('\n')}\n`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// D-cli-contract: the additive JSON diagnostic -- only ever printed on a PAYLOAD-LESS early-exit
|
|
417
|
+
// path (a command that would otherwise have exited with empty stdout). Commands whose stdout is
|
|
418
|
+
// itself a schema-validated artifact (scan/contract emit/handles plan, etc.) never call this --
|
|
419
|
+
// see DECISIONS.md for why wrapping those would break `bskel scan --json > brownfield-scan.json`.
|
|
420
|
+
export function diagnostic({ command, code, reason, message, next_actions = [] }) {
|
|
421
|
+
return {
|
|
422
|
+
schema: 'sbf.cli-diagnostic/1',
|
|
423
|
+
ok: false,
|
|
424
|
+
command,
|
|
425
|
+
code,
|
|
426
|
+
reason,
|
|
427
|
+
diagnostics: [{ level: 'error', reason, message }],
|
|
428
|
+
next_actions,
|
|
429
|
+
};
|
|
430
|
+
}
|