@mettlecast/domain-cli 0.2.21 → 0.2.23
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/dist/builder/load-module.js +5 -1
- package/dist/cli.js +60 -3
- package/dist/commands/add-api.d.ts +1 -1
- package/dist/commands/add-api.js +45 -17
- package/dist/commands/add-fixture-factory.d.ts +16 -0
- package/dist/commands/add-fixture-factory.js +60 -0
- package/dist/commands/add-module.js +4 -5
- package/dist/commands/add-seed-page.js +4 -5
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/dev.d.ts +30 -3
- package/dist/commands/dev.js +52 -11
- package/dist/commands/doctor.d.ts +22 -0
- package/dist/commands/doctor.js +341 -6
- package/dist/commands/generate-openapi.d.ts +21 -0
- package/dist/commands/generate-openapi.js +117 -0
- package/dist/commands/generate-sdk.d.ts +25 -0
- package/dist/commands/generate-sdk.js +98 -0
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.js +62 -0
- package/dist/commands/reseed-page.js +5 -6
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +28 -6
- package/dist/commands/why.d.ts +47 -0
- package/dist/commands/why.js +129 -0
- package/dist/templates/api-skeleton.d.ts +5 -1
- package/dist/templates/api-skeleton.js +28 -6
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +14 -1
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +20 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +12 -2
- package/dist/templates/patterns/api/simple-crud.d.ts +5 -0
- package/dist/templates/patterns/api/simple-crud.js +22 -4
- package/dist/templates/patterns/api/streaming-list.d.ts +27 -0
- package/dist/templates/patterns/api/streaming-list.js +91 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +14 -4
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +22 -0
- package/dist/utils/s3-fetch.js +23 -32
- package/package.json +4 -1
- package/src/__tests__/commands/add-api.test.ts +160 -0
- package/src/__tests__/commands/dev.test.ts +162 -0
- package/src/__tests__/commands/why.test.ts +199 -0
- package/src/__tests__/doctor.test.ts +336 -1
- package/src/__tests__/smoke/scaffold.test.ts +574 -0
- package/src/builder/load-module.ts +5 -1
- package/src/cli.ts +67 -5
- package/src/commands/add-api.ts +68 -19
- package/src/commands/add-fixture-factory.ts +75 -0
- package/src/commands/add-module.ts +4 -5
- package/src/commands/add-seed-page.ts +4 -5
- package/src/commands/build-flows.ts +2 -2
- package/src/commands/dev.ts +78 -11
- package/src/commands/doctor.ts +379 -12
- package/src/commands/generate-openapi.ts +154 -0
- package/src/commands/generate-sdk.ts +125 -0
- package/src/commands/init.ts +78 -0
- package/src/commands/reseed-page.ts +5 -6
- package/src/commands/upgrade.ts +26 -6
- package/src/commands/why.ts +171 -0
- package/src/templates/api-skeleton.ts +32 -6
- package/src/templates/patterns/api/create-with-event.ts +15 -1
- package/src/templates/patterns/api/idempotent-mutation.ts +21 -0
- package/src/templates/patterns/api/paginated-list.ts +13 -2
- package/src/templates/patterns/api/simple-crud.ts +23 -4
- package/src/templates/patterns/api/streaming-list.ts +91 -0
- package/src/templates/patterns/api/system-admin.ts +15 -4
- package/src/templates/patterns/api/webhook-receiver-style.ts +23 -0
- package/src/utils/s3-fetch.ts +26 -34
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init — bootstrap a freshly scaffolded project so it is ready for
|
|
3
|
+
* local development and CI. Runs npm install, builds the CLI, and
|
|
4
|
+
* runs a baseline doctor check.
|
|
5
|
+
*
|
|
6
|
+
* Usage: npx mc-domain-module init
|
|
7
|
+
*/
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { cliLogger } from '../utils/logger.js';
|
|
12
|
+
import { runDoctor } from './doctor.js';
|
|
13
|
+
export async function runInit(options = {}) {
|
|
14
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
15
|
+
const skipInstall = options.skipInstallIfExists !== false;
|
|
16
|
+
// ── 1. npm install ──────────────────────────────────────────────────
|
|
17
|
+
const nodeModulesDir = join(projectRoot, 'node_modules');
|
|
18
|
+
if (skipInstall && existsSync(nodeModulesDir)) {
|
|
19
|
+
cliLogger.info('node_modules exists — skipping npm install');
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
cliLogger.info('Running npm install...');
|
|
23
|
+
execSync('npm install --legacy-peer-deps', {
|
|
24
|
+
cwd: projectRoot,
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// ── 2. Build the CLI ────────────────────────────────────────────────
|
|
29
|
+
const cliDir = join(projectRoot, 'packages', 'domain-cli');
|
|
30
|
+
if (existsSync(cliDir)) {
|
|
31
|
+
cliLogger.info('Installing domain-cli dependencies...');
|
|
32
|
+
execSync('npm install --legacy-peer-deps', {
|
|
33
|
+
cwd: cliDir,
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
});
|
|
36
|
+
cliLogger.info('Building domain-cli...');
|
|
37
|
+
execSync('npm run build', {
|
|
38
|
+
cwd: cliDir,
|
|
39
|
+
stdio: 'inherit',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// ── 3. Baseline doctor ──────────────────────────────────────────────
|
|
43
|
+
cliLogger.info('Running doctor (baseline)...');
|
|
44
|
+
const report = await runDoctor({ projectRoot, strict: true });
|
|
45
|
+
const passes = report.checks.filter(c => c.status === 'PASS').length;
|
|
46
|
+
const fails = report.checks.filter(c => c.status === 'FAIL').length;
|
|
47
|
+
const warns = report.checks.filter(c => c.status === 'WARN').length;
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.log(`\nDoctor baseline: ${passes} PASS, ${fails} FAIL, ${warns} WARN`);
|
|
50
|
+
if (report.exitCode !== 0) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log('\nDoctor found issues — this is expected on a fresh scaffold.\n' +
|
|
53
|
+
'Run `npx mc-domain-module doctor` for details.\n');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.log('\nProject is ready!');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function runInitCli(opts = {}) {
|
|
61
|
+
await runInit(opts);
|
|
62
|
+
}
|
|
@@ -3,6 +3,7 @@ import { join, resolve, dirname } from 'node:path';
|
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import { createGunzip } from 'node:zlib';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
|
+
import ky from 'ky';
|
|
6
7
|
import { cliLogger } from '../utils/logger.js';
|
|
7
8
|
import { fetchVersionsJson, fetchModulesJson, fetchModuleTarball, } from '../utils/s3-fetch.js';
|
|
8
9
|
import { readManifest } from '../utils/manifest.js';
|
|
@@ -73,21 +74,19 @@ function detectGitRemote(projectDir) {
|
|
|
73
74
|
}
|
|
74
75
|
async function createGitHubPR(owner, repo, token, branch, title, body, labels) {
|
|
75
76
|
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
|
76
|
-
const res = await
|
|
77
|
-
method: 'POST',
|
|
77
|
+
const res = await ky.post(url, {
|
|
78
78
|
headers: {
|
|
79
79
|
Authorization: `Bearer ${token}`,
|
|
80
80
|
Accept: 'application/vnd.github+json',
|
|
81
|
-
'Content-Type': 'application/json',
|
|
82
81
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
83
82
|
},
|
|
84
|
-
|
|
83
|
+
json: {
|
|
85
84
|
title,
|
|
86
85
|
body,
|
|
87
86
|
head: branch,
|
|
88
87
|
base: 'develop',
|
|
89
|
-
labels,
|
|
90
|
-
}
|
|
88
|
+
...(labels && labels.length > 0 && { labels }),
|
|
89
|
+
},
|
|
91
90
|
});
|
|
92
91
|
if (!res.ok) {
|
|
93
92
|
const err = await res.text();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export interface UpgradeOptions {
|
|
2
2
|
dryRun: boolean;
|
|
3
|
+
/** Read-only variant: compute diffs, print a CI-parseable summary, exit 0 (no drift) or 1 (drift detected). Never writes files. */
|
|
4
|
+
check: boolean;
|
|
3
5
|
projectDir: string;
|
|
4
6
|
githubToken?: string;
|
|
5
7
|
frontendComponents?: boolean;
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Readable } from 'node:stream';
|
|
|
3
3
|
import { unlink } from 'node:fs/promises';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
|
+
import ky from 'ky';
|
|
6
7
|
import { cliLogger } from '../utils/logger.js';
|
|
7
8
|
import { fetchVersionsJson, fetchModulesJson, fetchModuleTarball, fetchScaffoldFile, } from '../utils/s3-fetch.js';
|
|
8
9
|
import { computeChecksumString } from '../utils/checksum.js';
|
|
@@ -117,21 +118,19 @@ function detectGitRemote(projectDir) {
|
|
|
117
118
|
}
|
|
118
119
|
async function createGitHubPR(owner, repo, token, branch, title, body, labels) {
|
|
119
120
|
const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
|
120
|
-
const res = await
|
|
121
|
-
method: 'POST',
|
|
121
|
+
const res = await ky.post(url, {
|
|
122
122
|
headers: {
|
|
123
123
|
Authorization: `Bearer ${token}`,
|
|
124
124
|
Accept: 'application/vnd.github+json',
|
|
125
|
-
'Content-Type': 'application/json',
|
|
126
125
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
127
126
|
},
|
|
128
|
-
|
|
127
|
+
json: {
|
|
129
128
|
title,
|
|
130
129
|
body,
|
|
131
130
|
head: branch,
|
|
132
131
|
base: 'develop',
|
|
133
132
|
...(labels && labels.length > 0 && { labels }),
|
|
134
|
-
}
|
|
133
|
+
},
|
|
135
134
|
});
|
|
136
135
|
if (!res.ok) {
|
|
137
136
|
const err = await res.text();
|
|
@@ -252,7 +251,8 @@ function buildFrontendComponentsPrBody(currentVersion, targetVersion, results, c
|
|
|
252
251
|
}
|
|
253
252
|
export async function runUpgrade(packageSpec, opts) {
|
|
254
253
|
const projectDir = resolve(opts.projectDir ?? process.cwd());
|
|
255
|
-
|
|
254
|
+
const actionLabel = opts.check ? 'check' : opts.dryRun ? 'dry run' : 'upgrade';
|
|
255
|
+
console.log(`\nTIB Upgrade (${actionLabel})\n`);
|
|
256
256
|
console.log(`Project: ${projectDir}\n`);
|
|
257
257
|
// 1. Read manifest + scaffold-config
|
|
258
258
|
const manifest = await readManifest(projectDir);
|
|
@@ -420,6 +420,28 @@ export async function runUpgrade(packageSpec, opts) {
|
|
|
420
420
|
console.log(` ${r.path} — new version written to ${r.path}.tib-upgrade`);
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
|
+
if (opts.check) {
|
|
424
|
+
// Read-only CI gate — report drift and exit.
|
|
425
|
+
const totalChanged = added.length + updated.length + deleted.length;
|
|
426
|
+
if (totalChanged === 0 && conflicts.length === 0) {
|
|
427
|
+
console.log('\n✓ No drift — project is up to date with scaffold.\n');
|
|
428
|
+
process.exitCode = 0;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.log(`\n✗ Drift detected: ${totalChanged} file(s) changed, ${conflicts.length} conflict(s).`);
|
|
432
|
+
if (added.length > 0)
|
|
433
|
+
console.log(` Added : ${added.map((r) => r.path).join(', ')}`);
|
|
434
|
+
if (updated.length > 0)
|
|
435
|
+
console.log(` Updated : ${updated.map((r) => r.path).join(', ')}`);
|
|
436
|
+
if (deleted.length > 0)
|
|
437
|
+
console.log(` Deleted : ${deleted.map((r) => r.path).join(', ')}`);
|
|
438
|
+
if (conflicts.length > 0)
|
|
439
|
+
console.log(` Conflicts: ${conflicts.map((r) => r.path).join(', ')}`);
|
|
440
|
+
console.log('\nRun `npx mc-domain-module upgrade` to apply changes.\n');
|
|
441
|
+
process.exitCode = 1;
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
423
445
|
if (opts.dryRun) {
|
|
424
446
|
console.log('\nDry run complete — no files were written.\n');
|
|
425
447
|
return;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to explain the project's current scaffold state and any available upgrades.
|
|
3
|
+
* Reads .mc/manifest.json for the current scaffold version + enabled module list,
|
|
4
|
+
* fetches the latest version from S3, and prints a diff to stdout.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Options for the why command.
|
|
8
|
+
*/
|
|
9
|
+
export interface WhyOptions {
|
|
10
|
+
/** Project root directory. Defaults to process.cwd(). */
|
|
11
|
+
projectRoot?: string;
|
|
12
|
+
/** Override S3 bucket used to fetch versions.json. Defaults to the public TIB bucket. */
|
|
13
|
+
scaffoldBucket?: string;
|
|
14
|
+
/** Output raw JSON instead of a human-readable report. */
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Report describing the project's current scaffold state and any pending upgrade.
|
|
19
|
+
*/
|
|
20
|
+
export interface WhyReport {
|
|
21
|
+
/** Scaffold version recorded in .mc/manifest.json (or "unknown" if no manifest). */
|
|
22
|
+
currentVersion: string;
|
|
23
|
+
/** Module IDs currently enabled in the project. */
|
|
24
|
+
enabledModules: string[];
|
|
25
|
+
/** Latest version published to the scaffold S3 bucket. */
|
|
26
|
+
latestVersion: string;
|
|
27
|
+
/** True when the current pinned version is older than the latest. */
|
|
28
|
+
upgradeAvailable: boolean;
|
|
29
|
+
/** Number of releases between current and latest (-1 if versions cannot be compared). */
|
|
30
|
+
versionsBehind: number;
|
|
31
|
+
/** All known scaffold versions (most recent first). */
|
|
32
|
+
allVersions: string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute the why command: gather local + remote scaffold state, return a report.
|
|
36
|
+
* @param options - WhyOptions with optional projectRoot and scaffoldBucket.
|
|
37
|
+
* @returns A WhyReport describing current vs. latest state.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runWhy(options?: WhyOptions): Promise<WhyReport>;
|
|
40
|
+
/**
|
|
41
|
+
* Format a WhyReport as a multi-line human-readable string.
|
|
42
|
+
*/
|
|
43
|
+
export declare function formatWhyReport(report: WhyReport): string;
|
|
44
|
+
/**
|
|
45
|
+
* CLI entry point: print a human-readable (or JSON) report and return the report.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runWhyCli(options?: WhyOptions): Promise<WhyReport>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to explain the project's current scaffold state and any available upgrades.
|
|
3
|
+
* Reads .mc/manifest.json for the current scaffold version + enabled module list,
|
|
4
|
+
* fetches the latest version from S3, and prints a diff to stdout.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { fetchVersionsJson } from '../utils/s3-fetch.js';
|
|
9
|
+
import { cliLogger } from '../utils/logger.js';
|
|
10
|
+
const DEFAULT_BUCKET = 'mc-scaffold';
|
|
11
|
+
const MANIFEST_PATH_SEGMENTS = ['.mc', 'manifest.json'];
|
|
12
|
+
/**
|
|
13
|
+
* Read the project's current scaffold state from .mc/manifest.json.
|
|
14
|
+
* Returns sensible defaults when the manifest is missing or unreadable.
|
|
15
|
+
*/
|
|
16
|
+
async function readManifestState(projectRoot) {
|
|
17
|
+
const manifestPath = join(projectRoot, ...MANIFEST_PATH_SEGMENTS);
|
|
18
|
+
try {
|
|
19
|
+
const content = await readFile(manifestPath, 'utf-8');
|
|
20
|
+
const parsed = JSON.parse(content);
|
|
21
|
+
const scaffoldVersion = typeof parsed.scaffoldVersion === 'string' ? parsed.scaffoldVersion : 'unknown';
|
|
22
|
+
const enabledModules = Array.isArray(parsed.enabledModules)
|
|
23
|
+
? parsed.enabledModules.filter((m) => typeof m === 'string')
|
|
24
|
+
: [];
|
|
25
|
+
return { scaffoldVersion, enabledModules };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err.code === 'ENOENT') {
|
|
29
|
+
return { scaffoldVersion: 'unknown', enabledModules: [] };
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compute the number of releases between two version strings, using the order
|
|
36
|
+
* returned by versions.json (most recent first). Returns -1 when either version
|
|
37
|
+
* is not present in the list.
|
|
38
|
+
*/
|
|
39
|
+
function computeVersionsBehind(current, latest, all) {
|
|
40
|
+
if (current === 'unknown')
|
|
41
|
+
return -1;
|
|
42
|
+
const latestIdx = all.indexOf(latest);
|
|
43
|
+
const currentIdx = all.indexOf(current);
|
|
44
|
+
if (latestIdx === -1 || currentIdx === -1)
|
|
45
|
+
return -1;
|
|
46
|
+
return Math.max(0, currentIdx - latestIdx);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Execute the why command: gather local + remote scaffold state, return a report.
|
|
50
|
+
* @param options - WhyOptions with optional projectRoot and scaffoldBucket.
|
|
51
|
+
* @returns A WhyReport describing current vs. latest state.
|
|
52
|
+
*/
|
|
53
|
+
export async function runWhy(options = {}) {
|
|
54
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
55
|
+
const bucket = options.scaffoldBucket ?? DEFAULT_BUCKET;
|
|
56
|
+
const { scaffoldVersion, enabledModules } = await readManifestState(projectRoot);
|
|
57
|
+
cliLogger.debug({ scaffoldVersion, enabledModules }, 'why: read local manifest');
|
|
58
|
+
const versionsJson = await fetchVersionsJson(bucket);
|
|
59
|
+
const latestVersion = versionsJson.latest;
|
|
60
|
+
const allVersions = versionsJson.versions;
|
|
61
|
+
const upgradeAvailable = latestVersion !== scaffoldVersion;
|
|
62
|
+
const versionsBehind = computeVersionsBehind(scaffoldVersion, latestVersion, allVersions);
|
|
63
|
+
return {
|
|
64
|
+
currentVersion: scaffoldVersion,
|
|
65
|
+
enabledModules,
|
|
66
|
+
latestVersion,
|
|
67
|
+
upgradeAvailable,
|
|
68
|
+
versionsBehind,
|
|
69
|
+
allVersions,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format a WhyReport as a multi-line human-readable string.
|
|
74
|
+
*/
|
|
75
|
+
export function formatWhyReport(report) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
lines.push('=== Scaffold State ===');
|
|
78
|
+
lines.push(`Current version : ${report.currentVersion}`);
|
|
79
|
+
lines.push(`Latest version : ${report.latestVersion}`);
|
|
80
|
+
if (report.upgradeAvailable) {
|
|
81
|
+
if (report.versionsBehind > 0) {
|
|
82
|
+
lines.push(`Upgrade available: yes (${report.versionsBehind} release(s) behind)`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
lines.push('Upgrade available: yes');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
lines.push('Upgrade available: no — you are on the latest version');
|
|
90
|
+
}
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push(`Enabled modules : ${report.enabledModules.length === 0 ? '(none)' : ''}`);
|
|
93
|
+
for (const m of report.enabledModules) {
|
|
94
|
+
lines.push(` - ${m}`);
|
|
95
|
+
}
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('Pending upgrade diff:');
|
|
98
|
+
if (!report.upgradeAvailable) {
|
|
99
|
+
lines.push(' (none)');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const idx = report.allVersions.indexOf(report.currentVersion);
|
|
103
|
+
if (idx <= 0) {
|
|
104
|
+
lines.push(` (full jump: ${report.currentVersion} → ${report.latestVersion})`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const newer = report.allVersions.slice(0, idx);
|
|
108
|
+
for (const v of newer) {
|
|
109
|
+
lines.push(` ${v} (newer than ${report.currentVersion})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return lines.join('\n') + '\n';
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* CLI entry point: print a human-readable (or JSON) report and return the report.
|
|
117
|
+
*/
|
|
118
|
+
export async function runWhyCli(options = {}) {
|
|
119
|
+
const report = await runWhy(options);
|
|
120
|
+
if (options.json) {
|
|
121
|
+
// eslint-disable-next-line no-console
|
|
122
|
+
console.log(JSON.stringify(report, null, 2));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
process.stdout.write(formatWhyReport(report));
|
|
127
|
+
}
|
|
128
|
+
return report;
|
|
129
|
+
}
|
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
export declare function apiSkeletonTemplate(domainId: string, apiId: string, tenancy: string): string;
|
|
12
12
|
/**
|
|
13
13
|
* Generates a fixture JSON skeleton for API testing.
|
|
14
|
+
* The fixture embeds the input schema's default shape as the example payload
|
|
15
|
+
* in the event body so that the test driver can deserialize it directly.
|
|
14
16
|
* @param domainId - Domain ID in kebab-case.
|
|
15
17
|
* @param apiId - API ID in kebab-case.
|
|
18
|
+
* @param exampleBody - JSON-stringified example payload matching the input
|
|
19
|
+
* schema's default shape. Defaults to '{}'.
|
|
16
20
|
* @returns JSON string with mock event and context for testing.
|
|
17
21
|
*/
|
|
18
|
-
export declare function apiFixtureSkeleton(domainId: string, apiId: string): string;
|
|
22
|
+
export declare function apiFixtureSkeleton(domainId: string, apiId: string, exampleBody?: string): string;
|
|
@@ -18,8 +18,24 @@ function camelCase(s) {
|
|
|
18
18
|
*/
|
|
19
19
|
export function apiSkeletonTemplate(domainId, apiId, tenancy) {
|
|
20
20
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
21
|
+
const outputType = `${camelCase(domainId)}${camelCase(apiId)}Output`;
|
|
21
22
|
return `import { z } from 'zod';
|
|
22
23
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
24
|
+
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
25
|
+
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
26
|
+
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
27
|
+
|
|
28
|
+
// Initialise OpenTelemetry tracing for the process. Idempotent —
|
|
29
|
+
// safe to call at module scope. Sets up OTLP export (when
|
|
30
|
+
// OTEL_EXPORTER_OTLP_ENDPOINT is set) or console-export for
|
|
31
|
+
// local dev. Auto-instruments outbound HTTP calls via ky.
|
|
32
|
+
initOtel();
|
|
33
|
+
|
|
34
|
+
// Shared HTTP client with retry on 503/429. Replace this with
|
|
35
|
+
// your own client if you need custom headers or auth.
|
|
36
|
+
const http = createKyClient();
|
|
37
|
+
|
|
38
|
+
const outputSchema = z.object({ ok: z.boolean() }).default({ ok: true });
|
|
23
39
|
|
|
24
40
|
export const ${camelCase(apiId)} = defineApi({
|
|
25
41
|
id: '${apiId}',
|
|
@@ -29,10 +45,12 @@ export const ${camelCase(apiId)} = defineApi({
|
|
|
29
45
|
versions: {
|
|
30
46
|
v1: {
|
|
31
47
|
status: 'stable',
|
|
32
|
-
input: z.object({}).
|
|
33
|
-
output:
|
|
34
|
-
handler: async (_input, _ctx) => {
|
|
35
|
-
|
|
48
|
+
input: z.object({}).default({}),
|
|
49
|
+
output: outputSchema,
|
|
50
|
+
handler: async (_input, _ctx): Promise<Result<z.infer<typeof outputSchema>, AppError>> => {
|
|
51
|
+
// Replace this with your handler logic.
|
|
52
|
+
// Return ok(value) on success, err({ ... }) on failure.
|
|
53
|
+
return ok({ ok: true });
|
|
36
54
|
},
|
|
37
55
|
},
|
|
38
56
|
},
|
|
@@ -45,11 +63,15 @@ export const ${camelCase(apiId)} = defineApi({
|
|
|
45
63
|
}
|
|
46
64
|
/**
|
|
47
65
|
* Generates a fixture JSON skeleton for API testing.
|
|
66
|
+
* The fixture embeds the input schema's default shape as the example payload
|
|
67
|
+
* in the event body so that the test driver can deserialize it directly.
|
|
48
68
|
* @param domainId - Domain ID in kebab-case.
|
|
49
69
|
* @param apiId - API ID in kebab-case.
|
|
70
|
+
* @param exampleBody - JSON-stringified example payload matching the input
|
|
71
|
+
* schema's default shape. Defaults to '{}'.
|
|
50
72
|
* @returns JSON string with mock event and context for testing.
|
|
51
73
|
*/
|
|
52
|
-
export function apiFixtureSkeleton(domainId, apiId) {
|
|
74
|
+
export function apiFixtureSkeleton(domainId, apiId, exampleBody = '{}') {
|
|
53
75
|
return JSON.stringify({
|
|
54
76
|
description: `Fixture for ${domainId}.${apiId}`,
|
|
55
77
|
event: {
|
|
@@ -68,7 +90,7 @@ export function apiFixtureSkeleton(domainId, apiId) {
|
|
|
68
90
|
},
|
|
69
91
|
pathParameters: {},
|
|
70
92
|
headers: { 'content-type': 'application/json', 'accept-version': '1' },
|
|
71
|
-
body:
|
|
93
|
+
body: exampleBody,
|
|
72
94
|
},
|
|
73
95
|
}, null, 2) + '\n';
|
|
74
96
|
}
|
|
@@ -3,3 +3,8 @@
|
|
|
3
3
|
* Produces: POST handler + publishes a domain event after mutation.
|
|
4
4
|
*/
|
|
5
5
|
export declare function createWithEventTemplate(domain: string, id: string, tenancy: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
8
|
+
* create-with-event pattern. Mirrors the default shape of the input schema.
|
|
9
|
+
*/
|
|
10
|
+
export declare const createWithEventExampleBody: string;
|
|
@@ -10,17 +10,25 @@ export function createWithEventTemplate(domain, id, tenancy) {
|
|
|
10
10
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
11
11
|
return `import { z } from 'zod';
|
|
12
12
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
13
|
+
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
14
|
+
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
15
|
+
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
16
|
+
|
|
17
|
+
initOtel();
|
|
13
18
|
|
|
14
19
|
// ── Zod schemas ──────────────────────────────────────────────
|
|
15
20
|
|
|
16
21
|
const ${varName}Input = z.object({
|
|
17
22
|
name: z.string().min(1),
|
|
18
23
|
payload: z.record(z.unknown()).optional(),
|
|
19
|
-
});
|
|
24
|
+
}).default({ name: 'Example' });
|
|
20
25
|
|
|
21
26
|
const ${varName}Output = z.object({
|
|
22
27
|
id: z.string().uuid(),
|
|
23
28
|
status: z.enum(['created', 'pending']),
|
|
29
|
+
}).default({
|
|
30
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
31
|
+
status: 'created',
|
|
24
32
|
});
|
|
25
33
|
|
|
26
34
|
// ── API definition ───────────────────────────────────────────
|
|
@@ -63,3 +71,8 @@ export const ${varName} = defineApi({
|
|
|
63
71
|
});
|
|
64
72
|
`;
|
|
65
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
76
|
+
* create-with-event pattern. Mirrors the default shape of the input schema.
|
|
77
|
+
*/
|
|
78
|
+
export const createWithEventExampleBody = JSON.stringify({ name: 'Example' });
|
|
@@ -3,3 +3,8 @@
|
|
|
3
3
|
* Produces: PUT with idempotency key middleware.
|
|
4
4
|
*/
|
|
5
5
|
export declare function idempotentMutationTemplate(domain: string, id: string, tenancy: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
8
|
+
* idempotent-mutation pattern. Mirrors the default shape of the input schema.
|
|
9
|
+
*/
|
|
10
|
+
export declare const idempotentMutationExampleBody: string;
|
|
@@ -10,18 +10,30 @@ export function idempotentMutationTemplate(domain, id, tenancy) {
|
|
|
10
10
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
11
11
|
return `import { z } from 'zod';
|
|
12
12
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
13
|
+
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
14
|
+
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
15
|
+
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
16
|
+
|
|
17
|
+
initOtel();
|
|
13
18
|
|
|
14
19
|
// ── Zod schemas ──────────────────────────────────────────────
|
|
15
20
|
|
|
16
21
|
const ${varName}Input = z.object({
|
|
17
22
|
id: z.string().uuid(),
|
|
18
23
|
payload: z.record(z.unknown()),
|
|
24
|
+
}).default({
|
|
25
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
26
|
+
payload: {},
|
|
19
27
|
});
|
|
20
28
|
|
|
21
29
|
const ${varName}Output = z.object({
|
|
22
30
|
id: z.string().uuid(),
|
|
23
31
|
status: z.enum(['applied', 'already-processed']),
|
|
24
32
|
idempotencyKey: z.string(),
|
|
33
|
+
}).default({
|
|
34
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
35
|
+
status: 'applied',
|
|
36
|
+
idempotencyKey: '00000000-0000-0000-0000-000000000000',
|
|
25
37
|
});
|
|
26
38
|
|
|
27
39
|
// ── API definition ───────────────────────────────────────────
|
|
@@ -74,3 +86,11 @@ export const ${varName} = defineApi({
|
|
|
74
86
|
});
|
|
75
87
|
`;
|
|
76
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
91
|
+
* idempotent-mutation pattern. Mirrors the default shape of the input schema.
|
|
92
|
+
*/
|
|
93
|
+
export const idempotentMutationExampleBody = JSON.stringify({
|
|
94
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
95
|
+
payload: {},
|
|
96
|
+
});
|
|
@@ -3,3 +3,8 @@
|
|
|
3
3
|
* Produces: GET with cursor/limit, pagination contract, EMF metrics.
|
|
4
4
|
*/
|
|
5
5
|
export declare function paginatedListTemplate(domain: string, id: string, tenancy: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
8
|
+
* paginated-list pattern. Mirrors the default shape of the input schema.
|
|
9
|
+
*/
|
|
10
|
+
export declare const paginatedListExampleBody = "{}";
|
|
@@ -10,6 +10,11 @@ export function paginatedListTemplate(domain, id, tenancy) {
|
|
|
10
10
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
11
11
|
return `import { z } from 'zod';
|
|
12
12
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
13
|
+
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
14
|
+
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
15
|
+
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
16
|
+
|
|
17
|
+
initOtel();
|
|
13
18
|
|
|
14
19
|
// ── Pagination contract ──────────────────────────────────────
|
|
15
20
|
|
|
@@ -22,13 +27,13 @@ const PageInput = z.object({
|
|
|
22
27
|
.max(100)
|
|
23
28
|
.default(20),
|
|
24
29
|
filter: z.string().optional(),
|
|
25
|
-
});
|
|
30
|
+
}).default({});
|
|
26
31
|
|
|
27
32
|
const PageOutput = z.object({
|
|
28
33
|
items: z.array(z.object({ id: z.string().uuid() })),
|
|
29
34
|
nextCursor: z.string().optional(),
|
|
30
35
|
total: z.number().int().optional(),
|
|
31
|
-
});
|
|
36
|
+
}).default({ items: [], nextCursor: undefined, total: undefined });
|
|
32
37
|
|
|
33
38
|
// ── API definition ───────────────────────────────────────────
|
|
34
39
|
|
|
@@ -61,3 +66,8 @@ export const ${varName} = defineApi({
|
|
|
61
66
|
});
|
|
62
67
|
`;
|
|
63
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
71
|
+
* paginated-list pattern. Mirrors the default shape of the input schema.
|
|
72
|
+
*/
|
|
73
|
+
export const paginatedListExampleBody = '{}';
|
|
@@ -4,3 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function simpleCrudTemplate(domain: string, id: string, tenancy: string): string;
|
|
6
6
|
export declare function simpleCrudListTemplate(domain: string, id: string, tenancy: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
9
|
+
* simple-crud pattern. Mirrors the default shape of the input schema.
|
|
10
|
+
*/
|
|
11
|
+
export declare const simpleCrudExampleBody = "{}";
|
|
@@ -10,23 +10,32 @@ export function simpleCrudTemplate(domain, id, tenancy) {
|
|
|
10
10
|
const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
|
|
11
11
|
return `import { z } from 'zod';
|
|
12
12
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
13
|
+
import { createKyClient, initOtel } from '@mettlecast/domain-runtime';
|
|
14
|
+
import type { Result, AppError } from '@mettlecast/domain-runtime';
|
|
15
|
+
import { ok, err, notFound } from '@mettlecast/domain-runtime';
|
|
16
|
+
|
|
17
|
+
initOtel();
|
|
13
18
|
|
|
14
19
|
// ── Zod schemas ──────────────────────────────────────────────
|
|
15
20
|
|
|
16
21
|
const ${varName}Input = z.object({
|
|
17
22
|
id: z.string().uuid().optional(),
|
|
18
|
-
});
|
|
23
|
+
}).default({});
|
|
19
24
|
|
|
20
25
|
const ${varName}Output = z.object({
|
|
21
26
|
id: z.string().uuid(),
|
|
22
27
|
createdAt: z.string().datetime(),
|
|
23
28
|
updatedAt: z.string().datetime(),
|
|
29
|
+
}).default({
|
|
30
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
31
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
32
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
24
33
|
});
|
|
25
34
|
|
|
26
35
|
const ${varName}ListOutput = z.object({
|
|
27
36
|
items: z.array(${varName}Output),
|
|
28
37
|
nextCursor: z.string().optional(),
|
|
29
|
-
});
|
|
38
|
+
}).default({ items: [], nextCursor: undefined });
|
|
30
39
|
|
|
31
40
|
// ── API definition ───────────────────────────────────────────
|
|
32
41
|
|
|
@@ -62,18 +71,22 @@ import { defineApi } from '@mettlecast/domain-runtime';
|
|
|
62
71
|
const ${varName}ListInput = z.object({
|
|
63
72
|
cursor: z.string().optional(),
|
|
64
73
|
limit: z.number().int().min(1).max(100).default(20),
|
|
65
|
-
});
|
|
74
|
+
}).default({});
|
|
66
75
|
|
|
67
76
|
const ${varName}Output = z.object({
|
|
68
77
|
id: z.string().uuid(),
|
|
69
78
|
createdAt: z.string().datetime(),
|
|
70
79
|
updatedAt: z.string().datetime(),
|
|
80
|
+
}).default({
|
|
81
|
+
id: '00000000-0000-0000-0000-000000000000',
|
|
82
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
83
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
71
84
|
});
|
|
72
85
|
|
|
73
86
|
const ${varName}ListOutput = z.object({
|
|
74
87
|
items: z.array(${varName}Output),
|
|
75
88
|
nextCursor: z.string().optional(),
|
|
76
|
-
});
|
|
89
|
+
}).default({ items: [], nextCursor: undefined });
|
|
77
90
|
|
|
78
91
|
export const ${varName}List = defineApi({
|
|
79
92
|
id: '${id}-list',
|
|
@@ -93,3 +106,8 @@ export const ${varName}List = defineApi({
|
|
|
93
106
|
});
|
|
94
107
|
`;
|
|
95
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Example body string used as the event body in the add-api fixture for the
|
|
111
|
+
* simple-crud pattern. Mirrors the default shape of the input schema.
|
|
112
|
+
*/
|
|
113
|
+
export const simpleCrudExampleBody = '{}';
|