@mettlecast/domain-cli 0.2.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/CHANGELOG.md +18 -0
- package/README.md +133 -0
- package/dist/builder/build-registry.d.ts +20 -0
- package/dist/builder/build-registry.js +178 -0
- package/dist/builder/build-types.d.ts +10 -0
- package/dist/builder/build-types.js +32 -0
- package/dist/builder/load-domain-module-config.d.ts +12 -0
- package/dist/builder/load-domain-module-config.js +34 -0
- package/dist/builder/load-module.d.ts +26 -0
- package/dist/builder/load-module.js +114 -0
- package/dist/builder/zod-to-json.d.ts +10 -0
- package/dist/builder/zod-to-json.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +338 -0
- package/dist/commands/add-api.d.ts +32 -0
- package/dist/commands/add-api.js +106 -0
- package/dist/commands/add-component.d.ts +15 -0
- package/dist/commands/add-component.js +78 -0
- package/dist/commands/add-domain.d.ts +21 -0
- package/dist/commands/add-domain.js +71 -0
- package/dist/commands/add-flow.d.ts +14 -0
- package/dist/commands/add-flow.js +108 -0
- package/dist/commands/add-hook.d.ts +15 -0
- package/dist/commands/add-hook.js +65 -0
- package/dist/commands/add-migration.d.ts +26 -0
- package/dist/commands/add-migration.js +93 -0
- package/dist/commands/add-module.d.ts +7 -0
- package/dist/commands/add-module.js +261 -0
- package/dist/commands/add-page.d.ts +13 -0
- package/dist/commands/add-page.js +170 -0
- package/dist/commands/add-seed-page.d.ts +5 -0
- package/dist/commands/add-seed-page.js +251 -0
- package/dist/commands/add-subscriber.d.ts +30 -0
- package/dist/commands/add-subscriber.js +87 -0
- package/dist/commands/add-test.d.ts +22 -0
- package/dist/commands/add-test.js +285 -0
- package/dist/commands/audit.d.ts +39 -0
- package/dist/commands/audit.js +171 -0
- package/dist/commands/build-catalog.d.ts +68 -0
- package/dist/commands/build-catalog.js +105 -0
- package/dist/commands/build-flows.d.ts +26 -0
- package/dist/commands/build-flows.js +174 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.js +39 -0
- package/dist/commands/check-hashes.d.ts +38 -0
- package/dist/commands/check-hashes.js +97 -0
- package/dist/commands/create-project.d.ts +5 -0
- package/dist/commands/create-project.js +272 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +48 -0
- package/dist/commands/doctor.js +1301 -0
- package/dist/commands/explain.d.ts +34 -0
- package/dist/commands/explain.js +167 -0
- package/dist/commands/power-tune.d.ts +1 -0
- package/dist/commands/power-tune.js +87 -0
- package/dist/commands/reseed-page.d.ts +6 -0
- package/dist/commands/reseed-page.js +297 -0
- package/dist/commands/show-dns.d.ts +15 -0
- package/dist/commands/show-dns.js +101 -0
- package/dist/commands/show.d.ts +46 -0
- package/dist/commands/show.js +135 -0
- package/dist/commands/test.d.ts +18 -0
- package/dist/commands/test.js +56 -0
- package/dist/commands/upgrade-backend.d.ts +9 -0
- package/dist/commands/upgrade-backend.js +46 -0
- package/dist/commands/upgrade-event.d.ts +22 -0
- package/dist/commands/upgrade-event.js +111 -0
- package/dist/commands/upgrade-frontend.d.ts +5 -0
- package/dist/commands/upgrade-frontend.js +99 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +495 -0
- package/dist/commands/validate.d.ts +30 -0
- package/dist/commands/validate.js +148 -0
- package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
- package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
- package/dist/runtime-stubs/load-secrets.d.ts +11 -0
- package/dist/runtime-stubs/load-secrets.js +37 -0
- package/dist/server/api-server.d.ts +21 -0
- package/dist/server/api-server.js +14 -0
- package/dist/server/mount-routes.d.ts +19 -0
- package/dist/server/mount-routes.js +42 -0
- package/dist/templates/api-skeleton.d.ts +18 -0
- package/dist/templates/api-skeleton.js +69 -0
- package/dist/templates/claude-md.d.ts +4 -0
- package/dist/templates/claude-md.js +7 -0
- package/dist/templates/dashboard-pages/index.d.ts +101 -0
- package/dist/templates/dashboard-pages/index.js +22 -0
- package/dist/templates/domain-config.d.ts +10 -0
- package/dist/templates/domain-config.js +19 -0
- package/dist/templates/events-skeleton.d.ts +9 -0
- package/dist/templates/events-skeleton.js +23 -0
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +69 -0
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +71 -0
- package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
- package/dist/templates/patterns/api/simple-crud.js +95 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +98 -0
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
- package/dist/templates/patterns/component/index.d.ts +25 -0
- package/dist/templates/patterns/component/index.js +814 -0
- package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
- package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
- package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
- package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
- package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
- package/dist/templates/patterns/flow/human-in-loop.js +90 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
- package/dist/templates/patterns/hook/index.d.ts +7 -0
- package/dist/templates/patterns/hook/index.js +378 -0
- package/dist/templates/patterns/migration/column-add.d.ts +5 -0
- package/dist/templates/patterns/migration/column-add.js +52 -0
- package/dist/templates/patterns/migration/event-store.d.ts +6 -0
- package/dist/templates/patterns/migration/event-store.js +97 -0
- package/dist/templates/patterns/migration/index.d.ts +5 -0
- package/dist/templates/patterns/migration/index.js +52 -0
- package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
- package/dist/templates/patterns/migration/rls-policy.js +78 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
- package/dist/templates/patterns/page/dashboard.d.ts +4 -0
- package/dist/templates/patterns/page/dashboard.js +164 -0
- package/dist/templates/patterns/page/detail.d.ts +4 -0
- package/dist/templates/patterns/page/detail.js +162 -0
- package/dist/templates/patterns/page/form.d.ts +4 -0
- package/dist/templates/patterns/page/form.js +192 -0
- package/dist/templates/patterns/page/index.d.ts +8 -0
- package/dist/templates/patterns/page/index.js +14 -0
- package/dist/templates/patterns/page/list.d.ts +4 -0
- package/dist/templates/patterns/page/list.js +138 -0
- package/dist/templates/patterns/page/settings.d.ts +4 -0
- package/dist/templates/patterns/page/settings.js +318 -0
- package/dist/templates/patterns/page/wizard.d.ts +4 -0
- package/dist/templates/patterns/page/wizard.js +293 -0
- package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
- package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
- package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
- package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
- package/dist/templates/readme.d.ts +4 -0
- package/dist/templates/readme.js +7 -0
- package/dist/templates/subscriber-skeleton.d.ts +11 -0
- package/dist/templates/subscriber-skeleton.js +23 -0
- package/dist/utils/checksum.d.ts +18 -0
- package/dist/utils/checksum.js +39 -0
- package/dist/utils/file-helpers.d.ts +35 -0
- package/dist/utils/file-helpers.js +59 -0
- package/dist/utils/header-inject.d.ts +15 -0
- package/dist/utils/header-inject.js +93 -0
- package/dist/utils/install-file.d.ts +17 -0
- package/dist/utils/install-file.js +78 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.js +93 -0
- package/dist/utils/s3-fetch.d.ts +25 -0
- package/dist/utils/s3-fetch.js +52 -0
- package/dist/utils/scaffold-config.d.ts +95 -0
- package/dist/utils/scaffold-config.js +53 -0
- package/dist/utils/token-substitution.d.ts +35 -0
- package/dist/utils/token-substitution.js +52 -0
- package/dist/utils/zod-extractor.d.ts +10 -0
- package/dist/utils/zod-extractor.js +28 -0
- package/package.json +43 -0
- package/src/__tests__/build-registry.test.ts +115 -0
- package/src/__tests__/build-types.test.ts +100 -0
- package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
- package/src/__tests__/commands/add-domain.test.ts +174 -0
- package/src/__tests__/commands/add-flow.test.ts +69 -0
- package/src/__tests__/commands/add-module.test.ts +207 -0
- package/src/__tests__/commands/build-flows.test.ts +154 -0
- package/src/__tests__/commands/check-hashes.test.ts +142 -0
- package/src/__tests__/commands/create-project.test.ts +712 -0
- package/src/__tests__/commands/upgrade.test.ts +762 -0
- package/src/__tests__/doctor.test.ts +291 -0
- package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
- package/src/__tests__/load-domain-module-config.test.ts +10 -0
- package/src/__tests__/mount-routes.test.ts +100 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
- package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
- package/src/__tests__/utils/checksum.test.ts +90 -0
- package/src/__tests__/utils/header-inject.test.ts +89 -0
- package/src/__tests__/utils/install-file.test.ts +207 -0
- package/src/__tests__/utils/manifest.test.ts +229 -0
- package/src/__tests__/utils/token-substitution.test.ts +134 -0
- package/src/__tests__/validate.test.ts +18 -0
- package/src/builder/build-registry.ts +237 -0
- package/src/builder/build-types.ts +36 -0
- package/src/builder/load-domain-module-config.ts +43 -0
- package/src/builder/load-module.ts +133 -0
- package/src/builder/zod-to-json.ts +16 -0
- package/src/cli.ts +387 -0
- package/src/commands/add-api.ts +145 -0
- package/src/commands/add-component.ts +116 -0
- package/src/commands/add-domain.ts +100 -0
- package/src/commands/add-flow.ts +136 -0
- package/src/commands/add-hook.ts +100 -0
- package/src/commands/add-migration.ts +129 -0
- package/src/commands/add-module.ts +347 -0
- package/src/commands/add-page.ts +242 -0
- package/src/commands/add-seed-page.ts +338 -0
- package/src/commands/add-subscriber.ts +123 -0
- package/src/commands/add-test.ts +328 -0
- package/src/commands/audit.ts +217 -0
- package/src/commands/build-catalog.ts +181 -0
- package/src/commands/build-flows.ts +198 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/check-hashes.ts +150 -0
- package/src/commands/create-project.ts +367 -0
- package/src/commands/dev.ts +47 -0
- package/src/commands/doctor.ts +1362 -0
- package/src/commands/explain.ts +209 -0
- package/src/commands/power-tune.ts +112 -0
- package/src/commands/reseed-page.ts +410 -0
- package/src/commands/show-dns.ts +139 -0
- package/src/commands/show.ts +179 -0
- package/src/commands/test.ts +91 -0
- package/src/commands/upgrade-backend.ts +72 -0
- package/src/commands/upgrade-event.ts +175 -0
- package/src/commands/upgrade-frontend.ts +135 -0
- package/src/commands/upgrade.ts +668 -0
- package/src/commands/validate.ts +208 -0
- package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
- package/src/runtime-stubs/load-secrets.ts +40 -0
- package/src/server/api-server.ts +34 -0
- package/src/server/mount-routes.ts +68 -0
- package/src/templates/api-skeleton.ts +72 -0
- package/src/templates/claude-md.ts +7 -0
- package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
- package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
- package/src/templates/dashboard-pages/account/index.ts +5 -0
- package/src/templates/dashboard-pages/account/members.tsx +107 -0
- package/src/templates/dashboard-pages/account/profile.tsx +53 -0
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
- package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
- package/src/templates/dashboard-pages/auth/index.ts +10 -0
- package/src/templates/dashboard-pages/auth/login.tsx +75 -0
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
- package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
- package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
- package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
- package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
- package/src/templates/dashboard-pages/index.ts +22 -0
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
- package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
- package/src/templates/dashboard-pages/shell/index.ts +5 -0
- package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
- package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
- package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
- package/src/templates/domain-config.ts +20 -0
- package/src/templates/events-skeleton.ts +24 -0
- package/src/templates/patterns/api/create-with-event.ts +72 -0
- package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
- package/src/templates/patterns/api/paginated-list.ts +74 -0
- package/src/templates/patterns/api/simple-crud.ts +100 -0
- package/src/templates/patterns/api/system-admin.ts +101 -0
- package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
- package/src/templates/patterns/component/index.ts +833 -0
- package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
- package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
- package/src/templates/patterns/flow/human-in-loop.ts +93 -0
- package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
- package/src/templates/patterns/hook/index.ts +382 -0
- package/src/templates/patterns/migration/column-add.ts +54 -0
- package/src/templates/patterns/migration/event-store.ts +99 -0
- package/src/templates/patterns/migration/index.ts +54 -0
- package/src/templates/patterns/migration/rls-policy.ts +80 -0
- package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
- package/src/templates/patterns/page/dashboard.ts +164 -0
- package/src/templates/patterns/page/detail.ts +162 -0
- package/src/templates/patterns/page/form.ts +192 -0
- package/src/templates/patterns/page/index.ts +17 -0
- package/src/templates/patterns/page/list.ts +138 -0
- package/src/templates/patterns/page/settings.ts +318 -0
- package/src/templates/patterns/page/wizard.ts +293 -0
- package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
- package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
- package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
- package/src/templates/readme.ts +7 -0
- package/src/templates/subscriber-skeleton.ts +28 -0
- package/src/utils/checksum.ts +43 -0
- package/src/utils/file-helpers.ts +91 -0
- package/src/utils/header-inject.ts +126 -0
- package/src/utils/install-file.ts +99 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/manifest.ts +137 -0
- package/src/utils/s3-fetch.ts +91 -0
- package/src/utils/scaffold-config.ts +172 -0
- package/src/utils/token-substitution.ts +80 -0
- package/src/utils/zod-extractor.ts +31 -0
- package/tsconfig.json +13 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { runBuild } from './commands/build.js';
|
|
4
|
+
import { runValidate } from './commands/validate.js';
|
|
5
|
+
import { runTest } from './commands/test.js';
|
|
6
|
+
import { runDev } from './commands/dev.js';
|
|
7
|
+
import { runBuildCatalog } from './commands/build-catalog.js';
|
|
8
|
+
import { runAddDomain } from './commands/add-domain.js';
|
|
9
|
+
import { runAddApi, API_PATTERNS } from './commands/add-api.js';
|
|
10
|
+
import { runAddSubscriber, SUBSCRIBER_PATTERNS } from './commands/add-subscriber.js';
|
|
11
|
+
import { runAddFlow, FLOW_PATTERNS } from './commands/add-flow.js';
|
|
12
|
+
import { runAddModule } from './commands/add-module.js';
|
|
13
|
+
import { runBuildFlows } from './commands/build-flows.js';
|
|
14
|
+
import { runDoctor } from './commands/doctor.js';
|
|
15
|
+
import { runCheckHashes } from './commands/check-hashes.js';
|
|
16
|
+
import { runUpgradeBackend } from './commands/upgrade-backend.js';
|
|
17
|
+
import { runAddPage } from './commands/add-page.js';
|
|
18
|
+
import { runCreateProject } from './commands/create-project.js';
|
|
19
|
+
import { runUpgradeFrontend } from './commands/upgrade-frontend.js';
|
|
20
|
+
import { runUpgrade } from './commands/upgrade.js';
|
|
21
|
+
import { runAddMigration, MIGRATION_PATTERNS } from './commands/add-migration.js';
|
|
22
|
+
import { runAddTest } from './commands/add-test.js';
|
|
23
|
+
import { runUpgradeEvent } from './commands/upgrade-event.js';
|
|
24
|
+
import { runShowDomainCli } from './commands/show.js';
|
|
25
|
+
import { runAuditCli } from './commands/audit.js';
|
|
26
|
+
import { runExplainCli } from './commands/explain.js';
|
|
27
|
+
import { runAddHook, type HookPattern, HOOK_PATTERNS } from './commands/add-hook.js';
|
|
28
|
+
import { runAddComponent, type ComponentTier, COMPONENT_TIERS } from './commands/add-component.js';
|
|
29
|
+
import { runPowerTune } from './commands/power-tune.js';
|
|
30
|
+
import { runReseedPage } from './commands/reseed-page.js';
|
|
31
|
+
import { runAddSeedPage } from './commands/add-seed-page.js';
|
|
32
|
+
import { runShowDns } from './commands/show-dns.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Root CLI program for the TIB Domain Module local developer toolchain.
|
|
36
|
+
* Provides build, validate, test, dev, add-domain, add-api, and add-subscriber subcommands.
|
|
37
|
+
*/
|
|
38
|
+
const program = new Command();
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.name('mc-domain-module')
|
|
42
|
+
.description('TIB Domain Module local developer toolchain')
|
|
43
|
+
.version('0.1.0');
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('build <domain>')
|
|
47
|
+
.description('Build the DomainRegistry from source and write .mc/domain-registry.json')
|
|
48
|
+
.option('--out <path>', 'Override the output file path')
|
|
49
|
+
.action(async (domain: string, opts: { out?: string }) => {
|
|
50
|
+
await runBuild({ domainRoot: domain, outFile: opts.out });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.command('validate <domain>')
|
|
55
|
+
.description('Build the registry and validate structure (CI gate — exits 1 on failure)')
|
|
56
|
+
.action(async (domain: string) => {
|
|
57
|
+
await runValidate(domain, true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command('test <domain> <handler-id>')
|
|
62
|
+
.description('Invoke a handler with fixture data and print the result')
|
|
63
|
+
.requiredOption('--fixture <path>', 'Path to fixture JSON file ({ event, ctx? })')
|
|
64
|
+
.action(async (domain: string, handlerId: string, opts: { fixture: string }) => {
|
|
65
|
+
await runTest({ domainRoot: domain, handlerId, fixturePath: opts.fixture });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command('dev <domain>')
|
|
70
|
+
.description('Start a local HTTP server simulating API Gateway for all defineApi handlers')
|
|
71
|
+
.option('--port <n>', 'Port to listen on', '3000')
|
|
72
|
+
.action(async (domain: string, opts: { port?: string }) => {
|
|
73
|
+
await runDev({ domainRoot: domain, port: opts.port ? parseInt(opts.port, 10) : 3000 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.command('build-catalog')
|
|
78
|
+
.description('Merge all per-domain registry files into .mc/domain-registry.json for TIB sync')
|
|
79
|
+
.option('--tib-dir <path>', 'Path to the .tib directory (defaults to .tib in cwd)')
|
|
80
|
+
.action(async (opts: { tibDir?: string }) => {
|
|
81
|
+
await runBuildCatalog(opts.tibDir);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
program
|
|
85
|
+
.command('add-domain <id>')
|
|
86
|
+
.description(
|
|
87
|
+
'Scaffold a new domain under domains/<id> and register it in .mc/scaffold-config.json'
|
|
88
|
+
)
|
|
89
|
+
.option('--tenancy <mode>', 'Tenancy mode (required|none|system)', 'required')
|
|
90
|
+
.action(async (id: string, opts: { tenancy: 'required' | 'none' | 'system' }) => {
|
|
91
|
+
await runAddDomain({ id, tenancy: opts.tenancy });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command('add-api <domain> <id>')
|
|
96
|
+
.description('Add a new API primitive to an existing domain')
|
|
97
|
+
.option('--method <method>', 'HTTP method', 'GET')
|
|
98
|
+
.option('--tenancy <mode>', 'Tenancy mode', 'required')
|
|
99
|
+
.option('--pattern <pattern>', `Pattern template: ${API_PATTERNS.join(' | ')}`)
|
|
100
|
+
.action(
|
|
101
|
+
async (
|
|
102
|
+
domain: string,
|
|
103
|
+
id: string,
|
|
104
|
+
opts: { method: string; tenancy: 'required' | 'none' | 'system'; pattern?: string }
|
|
105
|
+
) => {
|
|
106
|
+
await runAddApi({
|
|
107
|
+
domain,
|
|
108
|
+
id,
|
|
109
|
+
method: opts.method,
|
|
110
|
+
tenancy: opts.tenancy,
|
|
111
|
+
pattern: opts.pattern as typeof API_PATTERNS[number] | undefined,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
program
|
|
117
|
+
.command('add-subscriber <domain> <id>')
|
|
118
|
+
.description('Add a new event subscriber to an existing domain')
|
|
119
|
+
.requiredOption('--event <eventId>', 'Event id to subscribe to')
|
|
120
|
+
.action(async (domain: string, id: string, opts: { event: string }) => {
|
|
121
|
+
await runAddSubscriber({ domain, id, event: opts.event });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
program
|
|
125
|
+
.command('add-flow <domain> <id>')
|
|
126
|
+
.description('Scaffold a new flow under domains/<domain>/flows/<id>.ts')
|
|
127
|
+
.option('--type <type>', 'Flow type: express (default) or standard', 'express')
|
|
128
|
+
.action(async (domain: string, id: string, opts: { type: string }) => {
|
|
129
|
+
await runAddFlow({ domain, id, type: opts.type as 'express' | 'standard' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('add-module <module>')
|
|
134
|
+
.description('Install a scaffold module from S3 at the project\'s pinned scaffold version and raise a PR')
|
|
135
|
+
.option('--github-token <token>', 'GitHub token for PR creation (overrides GITHUB_TOKEN env var)')
|
|
136
|
+
.option('--dry-run', 'Show what would be installed without making changes', false)
|
|
137
|
+
.action(async (module: string, opts: { githubToken?: string; dryRun: boolean }) => {
|
|
138
|
+
await runAddModule({ moduleId: module, githubToken: opts.githubToken, dryRun: opts.dryRun });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
program
|
|
142
|
+
.command('build-flows')
|
|
143
|
+
.description('Build all flows into .mc/flows-registry.json')
|
|
144
|
+
.option('--out <path>', 'Override the output file path')
|
|
145
|
+
.action(async (opts: { out?: string }) => {
|
|
146
|
+
await runBuildFlows({ outFile: opts.out });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
program
|
|
150
|
+
.command('doctor')
|
|
151
|
+
.description('Run all structural checks for the current TIB project (strict by default — any FAIL causes exit code 1)')
|
|
152
|
+
.option('--strict', 'DEPRECATED: Doctor is always strict now. This flag is a no-op.', false)
|
|
153
|
+
.option('--json', 'Output JSON instead of pretty table', false)
|
|
154
|
+
.action(async (opts: { strict: boolean; json: boolean }) => {
|
|
155
|
+
if (opts.strict) {
|
|
156
|
+
// eslint-disable-next-line no-console
|
|
157
|
+
console.warn('⚠ --strict is deprecated: doctor is always strict now. This flag is a no-op.');
|
|
158
|
+
}
|
|
159
|
+
const report = await runDoctor({ projectRoot: process.cwd() });
|
|
160
|
+
if (opts.json) {
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.log(JSON.stringify(report, null, 2));
|
|
163
|
+
} else {
|
|
164
|
+
// Pretty-print table
|
|
165
|
+
// eslint-disable-next-line no-console
|
|
166
|
+
console.log('\n=== TIB Project Health Check ===\n');
|
|
167
|
+
for (const check of report.checks) {
|
|
168
|
+
const symbol = check.status === 'PASS' ? '✓' : check.status === 'WARN' ? '⚠' : '✗';
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.log(`${symbol} ${check.name}: ${check.message}`);
|
|
171
|
+
if (check.fixHint) {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
173
|
+
console.log(` Fix: ${check.fixHint}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.log(`\nExit code: ${report.exitCode}\n`);
|
|
178
|
+
}
|
|
179
|
+
process.exit(report.exitCode);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
program
|
|
183
|
+
.command('check-hashes')
|
|
184
|
+
.description('Verify infra/modules/ has not been hand-edited since last scaffold')
|
|
185
|
+
.action(async () => {
|
|
186
|
+
const result = await runCheckHashes({});
|
|
187
|
+
process.exit(result.ok ? 0 : 1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
program
|
|
191
|
+
.command('upgrade-backend <target-major>')
|
|
192
|
+
.description('Run jscodeshift/ts-morph migrations between major versions of domain-runtime')
|
|
193
|
+
.option('--repo-root <path>', 'Root of the repo (defaults to cwd)')
|
|
194
|
+
.option('--dry-run', 'Print changes without applying', false)
|
|
195
|
+
.action(async (targetMajor: string, opts: { repoRoot?: string; dryRun: boolean }) => {
|
|
196
|
+
await runUpgradeBackend({ targetMajor, repoRoot: opts.repoRoot, dryRun: opts.dryRun });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
program
|
|
200
|
+
.command('add-page <pageId>')
|
|
201
|
+
.description('Scaffold a seed page from the dashboard-pages template registry')
|
|
202
|
+
.option('--output <dir>', 'Override the output directory (default: src/pages/<category>)')
|
|
203
|
+
.option('--pattern <pattern>', 'Scaffold with a page pattern template (list, detail, form, wizard, dashboard, settings)')
|
|
204
|
+
.option('--layout <layout>', 'Layout to use: app (default) or auth', 'app')
|
|
205
|
+
.option('--auth', 'Scaffold with auth guard wiring (login-redirect on unauthenticated)')
|
|
206
|
+
.option('--protected', 'Add requireAuth() loader to the route')
|
|
207
|
+
.option('--public', 'No auth guard (public page)')
|
|
208
|
+
.action(async (pageId: string, opts: { output?: string; pattern?: string; layout: string; auth?: boolean; protected?: boolean; public?: boolean }) => {
|
|
209
|
+
// Validate pattern early to give a clear error before the inner command
|
|
210
|
+
if (opts.pattern && !['list', 'detail', 'form', 'wizard', 'dashboard', 'settings'].includes(opts.pattern)) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Unknown page pattern "${opts.pattern}". Available patterns: list, detail, form, wizard, dashboard, settings`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (!['app', 'auth'].includes(opts.layout)) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Unknown layout "${opts.layout}". Available layouts: app, auth`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
await runAddPage({
|
|
221
|
+
pageId,
|
|
222
|
+
outputDir: opts.output,
|
|
223
|
+
pattern: opts.pattern as import('./commands/add-page.js').AddPageOptions['pattern'],
|
|
224
|
+
layout: opts.layout as 'app' | 'auth',
|
|
225
|
+
auth: opts.auth,
|
|
226
|
+
protected: opts.protected,
|
|
227
|
+
public: opts.public,
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
program
|
|
232
|
+
.command('add-hook <name>')
|
|
233
|
+
.description('Scaffold a typed data-fetching hook (TanStack Query + @mettlecast/sdk)')
|
|
234
|
+
.option('--pattern <pattern>', 'Hook pattern', 'paginated-list')
|
|
235
|
+
.option('--output <dir>', 'Override the output directory (default: src/hooks/)')
|
|
236
|
+
.action(async (name: string, opts: { pattern: string; output?: string }) => {
|
|
237
|
+
if (!HOOK_PATTERNS.includes(opts.pattern as HookPattern)) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Unknown hook pattern "${opts.pattern}". Available patterns: ${HOOK_PATTERNS.join(', ')}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
await runAddHook({
|
|
243
|
+
name,
|
|
244
|
+
pattern: opts.pattern as HookPattern,
|
|
245
|
+
outputDir: opts.output,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
program
|
|
250
|
+
.command('add-component <name>')
|
|
251
|
+
.description('Scaffold a design-system component with Storybook story and a11y check')
|
|
252
|
+
.option('--pattern <tier>', 'Component tier: atom, molecule, or organism', 'atom')
|
|
253
|
+
.option('--output <dir>', 'Override the output directory (default: src/components/)')
|
|
254
|
+
.action(async (name: string, opts: { pattern: string; output?: string }) => {
|
|
255
|
+
if (!COMPONENT_TIERS.includes(opts.pattern as ComponentTier)) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Unknown component tier "${opts.pattern}". Available tiers: ${COMPONENT_TIERS.join(', ')}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
await runAddComponent({
|
|
261
|
+
name,
|
|
262
|
+
tier: opts.pattern as ComponentTier,
|
|
263
|
+
outputDir: opts.output,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
program
|
|
268
|
+
.command('create-project <name>')
|
|
269
|
+
.description('Initialise a new TIB frontend project with seed pages and router wiring')
|
|
270
|
+
.option('--out <dir>', 'Parent directory for the new project (default: current directory)')
|
|
271
|
+
.action(async (name: string, opts: { out?: string }) => {
|
|
272
|
+
await runCreateProject({ name, outDir: opts.out });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
program
|
|
276
|
+
.command('upgrade-frontend')
|
|
277
|
+
.description('Check @mettlecast/* frontend package versions and report missing/outdated packages')
|
|
278
|
+
.option('--dry-run', 'Report issues without making changes', false)
|
|
279
|
+
.option('--project-dir <path>', 'Path to the project to check (default: current directory)')
|
|
280
|
+
.action(async (opts: { dryRun: boolean; projectDir?: string }) => {
|
|
281
|
+
await runUpgradeFrontend({ dryRun: opts.dryRun, projectDir: opts.projectDir });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
program
|
|
285
|
+
.command('upgrade [version]')
|
|
286
|
+
.description('Upgrade scaffold to a new version — diffs S3 tarballs, raises a GitHub PR with exact file changes')
|
|
287
|
+
.option('--dry-run', 'Preview changes without writing files or creating PR', false)
|
|
288
|
+
.option('--project-dir <path>', 'Path to the project to upgrade')
|
|
289
|
+
.option('--github-token <token>', 'GitHub token for PR creation (overrides GITHUB_TOKEN env var)')
|
|
290
|
+
.option('--frontend-components', 'Scope upgrade to scaffold-tracked frontend files only (opens a PR with Chromatic visual diff)', false)
|
|
291
|
+
.action(async (version: string | undefined, opts: { dryRun: boolean; projectDir?: string; githubToken?: string; frontendComponents: boolean }) => {
|
|
292
|
+
await runUpgrade(version, { dryRun: opts.dryRun, projectDir: opts.projectDir ?? process.cwd(), githubToken: opts.githubToken, frontendComponents: opts.frontendComponents });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── New W4 commands ───────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
program
|
|
298
|
+
.command('add-migration <domain> <name>')
|
|
299
|
+
.description('Add a new database migration to an existing domain')
|
|
300
|
+
.requiredOption('--pattern <pattern>', `Migration pattern: ${MIGRATION_PATTERNS.join(' | ')}`)
|
|
301
|
+
.action(async (domain: string, name: string, opts: { pattern: string }) => {
|
|
302
|
+
await runAddMigration({
|
|
303
|
+
domain,
|
|
304
|
+
name,
|
|
305
|
+
pattern: opts.pattern as typeof MIGRATION_PATTERNS[number],
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
program
|
|
310
|
+
.command('add-test <domain> <primitive-id>')
|
|
311
|
+
.description('Scaffold a vitest fixture for an existing domain primitive')
|
|
312
|
+
.action(async (domain: string, primitiveId: string) => {
|
|
313
|
+
await runAddTest({ domain, primitiveId });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
program
|
|
317
|
+
.command('upgrade-event <domain> <event-id>')
|
|
318
|
+
.description('Bump an event definition to a new version with sunset semantics')
|
|
319
|
+
.requiredOption('--to-version <N>', 'Target version number (integer >= 2)', parseInt)
|
|
320
|
+
.action(async (domain: string, eventId: string, opts: { toVersion: number }) => {
|
|
321
|
+
await runUpgradeEvent({ domain, eventId, toVersion: opts.toVersion });
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
program
|
|
325
|
+
.command('show domain <domain>')
|
|
326
|
+
.description('Show a structured JSON summary of a domain')
|
|
327
|
+
.action(async (domain: string) => {
|
|
328
|
+
await runShowDomainCli({ domain });
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
program
|
|
332
|
+
.command('audit [domain]')
|
|
333
|
+
.description('Run structural audit checks, optionally filtered to one domain')
|
|
334
|
+
.option('--json', 'Output structured JSON', false)
|
|
335
|
+
.action(async (domain: string | undefined, opts: { json: boolean }) => {
|
|
336
|
+
await runAuditCli({ domain, json: opts.json });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
program
|
|
340
|
+
.command('explain <rule-id>')
|
|
341
|
+
.description('Explain a rule ID with description, examples, and K-node reference')
|
|
342
|
+
.action(async (ruleId: string) => {
|
|
343
|
+
runExplainCli({ ruleId });
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
program
|
|
347
|
+
.command('power-tune <domain> <primitive>')
|
|
348
|
+
.description('Run AWS Lambda Power Tuning on a deployed Lambda. Returns recommended memorySize.')
|
|
349
|
+
.option('--strategy <strategy>', 'Optimization strategy: cost, speed, or balanced', 'cost')
|
|
350
|
+
.action(async (domain: string, primitive: string) => {
|
|
351
|
+
await runPowerTune(domain, primitive);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
program
|
|
355
|
+
.command('reseed-page [name]')
|
|
356
|
+
.description('Force-reseed a seed page (or all seed pages) with the latest scaffold version. Opens a [DESTRUCTIVE] PR.')
|
|
357
|
+
.option('--all', 'Reseed all seed pages in bulk (single PR)', false)
|
|
358
|
+
.option('--project-dir <path>', 'Path to the project to reseed')
|
|
359
|
+
.action(async (name: string | undefined, opts: { all: boolean; projectDir?: string }) => {
|
|
360
|
+
if (!name && !opts.all) {
|
|
361
|
+
throw new Error('Provide a page name or use --all to reseed all seed pages');
|
|
362
|
+
}
|
|
363
|
+
await runReseedPage({ pageName: name, all: opts.all, projectDir: opts.projectDir });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
program
|
|
367
|
+
.command('add-seed-page <name>')
|
|
368
|
+
.description('Add a seed page from the scaffold library. The page is yours immediately — TIB will never auto-update it.')
|
|
369
|
+
.option('--project-dir <path>', 'Path to the project')
|
|
370
|
+
.action(async (name: string, opts: { projectDir?: string }) => {
|
|
371
|
+
await runAddSeedPage({ pageName: name, projectDir: opts.projectDir });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
program
|
|
375
|
+
.command('show-dns')
|
|
376
|
+
.description('Print DNS records (zone-file format) needed to activate custom domains')
|
|
377
|
+
.option('--env <env>', 'Filter to a single environment (dev|staging|prod)')
|
|
378
|
+
.option('--project-dir <path>', 'Path to the project root (defaults to cwd)')
|
|
379
|
+
.action(async (opts: { env?: string; projectDir?: string }) => {
|
|
380
|
+
await runShowDns(opts);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
384
|
+
// eslint-disable-next-line no-console
|
|
385
|
+
console.error(err);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to add a new API to an existing domain.
|
|
3
|
+
* Supports --pattern to scaffold from predefined templates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFile, access, mkdir } from 'node:fs/promises';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { apiSkeletonTemplate, apiFixtureSkeleton } from '../templates/api-skeleton.js';
|
|
9
|
+
import { simpleCrudTemplate, simpleCrudListTemplate } from '../templates/patterns/api/simple-crud.js';
|
|
10
|
+
import { paginatedListTemplate } from '../templates/patterns/api/paginated-list.js';
|
|
11
|
+
import { createWithEventTemplate } from '../templates/patterns/api/create-with-event.js';
|
|
12
|
+
import { idempotentMutationTemplate } from '../templates/patterns/api/idempotent-mutation.js';
|
|
13
|
+
import { webhookReceiverStyleTemplate } from '../templates/patterns/api/webhook-receiver-style.js';
|
|
14
|
+
import { systemAdminTemplate } from '../templates/patterns/api/system-admin.js';
|
|
15
|
+
import { cliLogger } from '../utils/logger.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Valid API pattern IDs.
|
|
19
|
+
*/
|
|
20
|
+
export const API_PATTERNS = [
|
|
21
|
+
'simple-crud',
|
|
22
|
+
'paginated-list',
|
|
23
|
+
'create-with-event',
|
|
24
|
+
'idempotent-mutation',
|
|
25
|
+
'webhook-receiver-style',
|
|
26
|
+
'system-admin',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export type ApiPattern = (typeof API_PATTERNS)[number];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options for the add-api command.
|
|
33
|
+
*/
|
|
34
|
+
export interface AddApiOptions {
|
|
35
|
+
/** Domain ID in kebab-case. */
|
|
36
|
+
domain: string;
|
|
37
|
+
/** API ID in kebab-case. */
|
|
38
|
+
id: string;
|
|
39
|
+
/** HTTP method (defaults to GET). */
|
|
40
|
+
method?: string;
|
|
41
|
+
/** Tenancy mode: 'required', 'none', or 'system'. */
|
|
42
|
+
tenancy: 'required' | 'none' | 'system';
|
|
43
|
+
/** Pattern template ID (optional — when provided, uses pattern instead of skeleton). */
|
|
44
|
+
pattern?: ApiPattern;
|
|
45
|
+
/** Override directory for domain root (used in tests). */
|
|
46
|
+
domainsDir?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execute the add-api command: create a new API handler in an existing domain.
|
|
53
|
+
* @param opts - AddApiOptions specifying domain, API ID, and tenancy.
|
|
54
|
+
* @throws If domain does not exist, API ID is invalid, or file already exists.
|
|
55
|
+
*/
|
|
56
|
+
export async function runAddApi(opts: AddApiOptions): Promise<void> {
|
|
57
|
+
if (!KEBAB_REGEX.test(opts.id)) {
|
|
58
|
+
throw new Error(`Invalid API id "${opts.id}" — must be kebab-case`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!KEBAB_REGEX.test(opts.domain)) {
|
|
62
|
+
throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
|
|
66
|
+
const domainDir = join(root, opts.domain);
|
|
67
|
+
const domainConfigPath = join(domainDir, 'domain.config.ts');
|
|
68
|
+
|
|
69
|
+
// Verify domain exists
|
|
70
|
+
try {
|
|
71
|
+
await access(domainConfigPath);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const apiFilePath = join(domainDir, 'api', `${opts.id}.ts`);
|
|
77
|
+
|
|
78
|
+
// Refuse if API already exists
|
|
79
|
+
try {
|
|
80
|
+
await access(apiFilePath);
|
|
81
|
+
throw new Error(`API "${opts.id}" already exists at ${apiFilePath}`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Resolve template content
|
|
89
|
+
let apiContent: string;
|
|
90
|
+
if (opts.pattern) {
|
|
91
|
+
apiContent = resolveApiPattern(opts.pattern, opts.domain, opts.id, opts.tenancy);
|
|
92
|
+
cliLogger.info({ pattern: opts.pattern }, 'Using pattern template');
|
|
93
|
+
} else {
|
|
94
|
+
apiContent = apiSkeletonTemplate(opts.domain, opts.id, opts.tenancy);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Write API file
|
|
98
|
+
await writeFile(apiFilePath, apiContent);
|
|
99
|
+
|
|
100
|
+
// Create fixture
|
|
101
|
+
const apiTestDir = join(domainDir, 'api', '__tests__');
|
|
102
|
+
await mkdir(apiTestDir, { recursive: true });
|
|
103
|
+
await writeFile(
|
|
104
|
+
join(apiTestDir, `${opts.id}.fixture.json`),
|
|
105
|
+
apiFixtureSkeleton(opts.domain, opts.id)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
cliLogger.info({ domain: opts.domain, api: opts.id, method: opts.method ?? 'GET', pattern: opts.pattern ?? 'skeleton' }, 'API added');
|
|
109
|
+
// eslint-disable-next-line no-console
|
|
110
|
+
console.log(`\n✓ API "${opts.id}" added to domain "${opts.domain}" at ${apiFilePath}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a pattern template to its generated TypeScript source.
|
|
115
|
+
* @param pattern - The pattern ID.
|
|
116
|
+
* @param domain - Domain ID.
|
|
117
|
+
* @param id - API ID.
|
|
118
|
+
* @param tenancy - Tenancy mode.
|
|
119
|
+
* @returns TypeScript source code as a string.
|
|
120
|
+
*/
|
|
121
|
+
function resolveApiPattern(
|
|
122
|
+
pattern: ApiPattern,
|
|
123
|
+
domain: string,
|
|
124
|
+
id: string,
|
|
125
|
+
tenancy: string,
|
|
126
|
+
): string {
|
|
127
|
+
switch (pattern) {
|
|
128
|
+
case 'simple-crud':
|
|
129
|
+
return simpleCrudTemplate(domain, id, tenancy) +
|
|
130
|
+
'\n' +
|
|
131
|
+
simpleCrudListTemplate(domain, id, tenancy);
|
|
132
|
+
case 'paginated-list':
|
|
133
|
+
return paginatedListTemplate(domain, id, tenancy);
|
|
134
|
+
case 'create-with-event':
|
|
135
|
+
return createWithEventTemplate(domain, id, tenancy);
|
|
136
|
+
case 'idempotent-mutation':
|
|
137
|
+
return idempotentMutationTemplate(domain, id, tenancy);
|
|
138
|
+
case 'webhook-receiver-style':
|
|
139
|
+
return webhookReceiverStyleTemplate(domain, id, tenancy);
|
|
140
|
+
case 'system-admin':
|
|
141
|
+
return systemAdminTemplate(domain, id, tenancy);
|
|
142
|
+
default:
|
|
143
|
+
throw new Error(`Unknown API pattern "${pattern}". Available: ${API_PATTERNS.join(', ')}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: tib add-component <name> --pattern <tier>
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds a design-system component with Storybook story and a11y check.
|
|
5
|
+
*
|
|
6
|
+
* Tiers:
|
|
7
|
+
* - atom: Single design token usage (Button, Input, Icon).
|
|
8
|
+
* - molecule: Composition of 2+ atoms (SearchBar, FormField, DataRow).
|
|
9
|
+
* - organism: Complex section (DataTable, WizardFlow, DashboardWidget).
|
|
10
|
+
*/
|
|
11
|
+
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
12
|
+
import { join, resolve } from 'node:path';
|
|
13
|
+
import { cliLogger } from '../utils/logger.js';
|
|
14
|
+
import {
|
|
15
|
+
atomTemplate,
|
|
16
|
+
moleculeTemplate,
|
|
17
|
+
organismTemplate,
|
|
18
|
+
} from '../templates/patterns/component/index.js';
|
|
19
|
+
|
|
20
|
+
/** Supported component tiers. */
|
|
21
|
+
export type ComponentTier = 'atom' | 'molecule' | 'organism';
|
|
22
|
+
|
|
23
|
+
export const COMPONENT_TIERS: readonly ComponentTier[] = [
|
|
24
|
+
'atom',
|
|
25
|
+
'molecule',
|
|
26
|
+
'organism',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
30
|
+
|
|
31
|
+
const TIER_TEMPLATES: Record<
|
|
32
|
+
ComponentTier,
|
|
33
|
+
(name: string) => { component: string; story: string; test: string }
|
|
34
|
+
> = {
|
|
35
|
+
atom: atomTemplate,
|
|
36
|
+
molecule: moleculeTemplate,
|
|
37
|
+
organism: organismTemplate,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface AddComponentOptions {
|
|
41
|
+
/** Component name in kebab-case (e.g. "action-button"). */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Component tier / pattern. */
|
|
44
|
+
tier: ComponentTier;
|
|
45
|
+
/** Override the output base directory (default: src/components/). */
|
|
46
|
+
outputDir?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scaffold a design-system component with story + test files.
|
|
51
|
+
*/
|
|
52
|
+
export async function runAddComponent(opts: AddComponentOptions): Promise<void> {
|
|
53
|
+
const { name, tier, outputDir } = opts;
|
|
54
|
+
|
|
55
|
+
if (!KEBAB_REGEX.test(name)) {
|
|
56
|
+
throw new Error(`Invalid component name "${name}" — must be kebab-case`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!COMPONENT_TIERS.includes(tier)) {
|
|
60
|
+
const available = COMPONENT_TIERS.join(', ');
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Unknown component tier "${tier}".\n\nAvailable tiers: ${available}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const templateFn = TIER_TEMPLATES[tier];
|
|
67
|
+
const { component, story, test } = templateFn(name);
|
|
68
|
+
|
|
69
|
+
const baseDir = outputDir
|
|
70
|
+
? resolve(outputDir)
|
|
71
|
+
: resolve(process.cwd(), 'src', 'components');
|
|
72
|
+
|
|
73
|
+
const compDir = join(baseDir, name);
|
|
74
|
+
|
|
75
|
+
const files: Array<{ name: string; content: string }> = [
|
|
76
|
+
{ name: `${name}.tsx`, content: component },
|
|
77
|
+
{ name: `${name}.stories.tsx`, content: story },
|
|
78
|
+
{ name: `${name}.test.tsx`, content: test },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Check for existing component directory first
|
|
82
|
+
try {
|
|
83
|
+
await access(compDir);
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Component directory already exists at ${compDir}. Remove it first or use a different --output directory.`
|
|
86
|
+
);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await mkdir(compDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const outPath = join(compDir, file.name);
|
|
95
|
+
await writeFile(outPath, file.content, 'utf8');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
cliLogger.info({ name, tier, compDir }, 'Component scaffolded');
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log(
|
|
101
|
+
`\n✓ Component "${name}" (tier: ${tier}) added in ${compDir}/\n` +
|
|
102
|
+
` ${name}.tsx — component implementation\n` +
|
|
103
|
+
` ${name}.stories.tsx — Storybook story (2+ variants)\n` +
|
|
104
|
+
` ${name}.test.tsx — Playwright smoke test + a11y check\n` +
|
|
105
|
+
`\n` +
|
|
106
|
+
` Import: import { ${capitalize(name)} } from './components/${name}/${name}';\n`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Capitalizes the first character of a kebab-case name into PascalCase. */
|
|
111
|
+
function capitalize(s: string): string {
|
|
112
|
+
return s
|
|
113
|
+
.split('-')
|
|
114
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
115
|
+
.join('');
|
|
116
|
+
}
|