@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Valid flow pattern IDs.
|
|
3
|
+
*/
|
|
4
|
+
export declare const FLOW_PATTERNS: readonly ["fan-out-reaction", "multi-step-orchestration", "autonomous-remediation", "human-in-loop"];
|
|
5
|
+
export type FlowPattern = (typeof FLOW_PATTERNS)[number];
|
|
6
|
+
export interface AddFlowOptions {
|
|
7
|
+
domain: string;
|
|
8
|
+
id: string;
|
|
9
|
+
type?: 'express' | 'standard';
|
|
10
|
+
/** Pattern template ID (optional). */
|
|
11
|
+
pattern?: FlowPattern;
|
|
12
|
+
projectRoot?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function runAddFlow(opts: AddFlowOptions): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { mkdir, writeFile, access } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { fanOutReactionTemplate } from '../templates/patterns/flow/fan-out-reaction.js';
|
|
4
|
+
import { multiStepOrchestrationTemplate } from '../templates/patterns/flow/multi-step-orchestration.js';
|
|
5
|
+
import { autonomousRemediationTemplate } from '../templates/patterns/flow/autonomous-remediation.js';
|
|
6
|
+
import { humanInLoopTemplate } from '../templates/patterns/flow/human-in-loop.js';
|
|
7
|
+
import { cliLogger } from '../utils/logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Valid flow pattern IDs.
|
|
10
|
+
*/
|
|
11
|
+
export const FLOW_PATTERNS = [
|
|
12
|
+
'fan-out-reaction',
|
|
13
|
+
'multi-step-orchestration',
|
|
14
|
+
'autonomous-remediation',
|
|
15
|
+
'human-in-loop',
|
|
16
|
+
];
|
|
17
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
18
|
+
function toCamelCase(s) {
|
|
19
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
20
|
+
}
|
|
21
|
+
function flowSkeletonTemplate(domain, id, type) {
|
|
22
|
+
const varName = toCamelCase(id);
|
|
23
|
+
return `import { defineFlow } from '@mettlecast/domain-runtime';
|
|
24
|
+
|
|
25
|
+
export const ${varName} = defineFlow({
|
|
26
|
+
id: '${id}',
|
|
27
|
+
owningDomain: '${domain}',
|
|
28
|
+
type: '${type}',
|
|
29
|
+
name: '${id}',
|
|
30
|
+
steps: [
|
|
31
|
+
{
|
|
32
|
+
type: 'domain-action',
|
|
33
|
+
name: 'ExampleAction',
|
|
34
|
+
domainId: '${domain}',
|
|
35
|
+
actionId: 'example-action',
|
|
36
|
+
next: 'Done',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'flow-control',
|
|
40
|
+
control: 'succeed',
|
|
41
|
+
name: 'Done',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
export async function runAddFlow(opts) {
|
|
48
|
+
if (!KEBAB_REGEX.test(opts.domain)) {
|
|
49
|
+
throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
|
|
50
|
+
}
|
|
51
|
+
if (!KEBAB_REGEX.test(opts.id)) {
|
|
52
|
+
throw new Error(`Invalid flow id "${opts.id}" — must be kebab-case (lowercase with hyphens)`);
|
|
53
|
+
}
|
|
54
|
+
const root = opts.projectRoot ?? resolve(process.cwd());
|
|
55
|
+
const flowType = opts.type ?? 'express';
|
|
56
|
+
// Verify domain exists
|
|
57
|
+
const domainDir = join(root, 'domains', opts.domain);
|
|
58
|
+
try {
|
|
59
|
+
await access(domainDir);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error(`Domain "${opts.domain}" does not exist at ${domainDir}`);
|
|
63
|
+
}
|
|
64
|
+
const flowsDir = join(domainDir, 'flows');
|
|
65
|
+
const flowFile = join(flowsDir, `${opts.id}.ts`);
|
|
66
|
+
// Refuse if flow already exists
|
|
67
|
+
try {
|
|
68
|
+
await access(flowFile);
|
|
69
|
+
throw new Error(`Flow "${opts.id}" already exists at ${flowFile}`);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err.code !== 'ENOENT') {
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await mkdir(flowsDir, { recursive: true });
|
|
77
|
+
// Resolve template content
|
|
78
|
+
let flowContent;
|
|
79
|
+
if (opts.pattern) {
|
|
80
|
+
flowContent = resolveFlowPattern(opts.pattern, opts.domain, opts.id);
|
|
81
|
+
cliLogger.info({ pattern: opts.pattern }, 'Using pattern template');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
flowContent = flowSkeletonTemplate(opts.domain, opts.id, flowType);
|
|
85
|
+
}
|
|
86
|
+
await writeFile(flowFile, flowContent);
|
|
87
|
+
cliLogger.info({ domain: opts.domain, id: opts.id, file: flowFile, pattern: opts.pattern ?? 'skeleton' }, 'Flow scaffolded');
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.log(`\n✓ Flow "${opts.id}" added at ${flowFile}\nNext: edit the flow definition, ` +
|
|
90
|
+
`then run \`mc-domain-module build-flows\` to generate the registry.`);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a flow pattern template to its generated TypeScript source.
|
|
94
|
+
*/
|
|
95
|
+
function resolveFlowPattern(pattern, domain, id) {
|
|
96
|
+
switch (pattern) {
|
|
97
|
+
case 'fan-out-reaction':
|
|
98
|
+
return fanOutReactionTemplate(domain, id);
|
|
99
|
+
case 'multi-step-orchestration':
|
|
100
|
+
return multiStepOrchestrationTemplate(domain, id);
|
|
101
|
+
case 'autonomous-remediation':
|
|
102
|
+
return autonomousRemediationTemplate(domain, id);
|
|
103
|
+
case 'human-in-loop':
|
|
104
|
+
return humanInLoopTemplate(domain, id);
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`Unknown flow pattern "${pattern}". Available: ${FLOW_PATTERNS.join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Supported hook patterns. */
|
|
2
|
+
export type HookPattern = 'paginated-list' | 'mutation' | 'feature-flag' | 'subscription';
|
|
3
|
+
export declare const HOOK_PATTERNS: readonly HookPattern[];
|
|
4
|
+
export interface AddHookOptions {
|
|
5
|
+
/** Hook name in kebab-case (e.g. "search-items"). */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Hook pattern to scaffold. */
|
|
8
|
+
pattern: HookPattern;
|
|
9
|
+
/** Override the output directory (default: src/hooks/). */
|
|
10
|
+
outputDir?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Scaffold a typed data-fetching hook file.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runAddHook(opts: AddHookOptions): Promise<void>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: tib add-hook <name> --pattern <pattern>
|
|
3
|
+
*
|
|
4
|
+
* Scaffolds a typed data-fetching hook using TanStack Query and @mettlecast/sdk.
|
|
5
|
+
*/
|
|
6
|
+
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { cliLogger } from '../utils/logger.js';
|
|
9
|
+
import { paginatedListHookTemplate, mutationHookTemplate, featureFlagHookTemplate, subscriptionHookTemplate, } from '../templates/patterns/hook/index.js';
|
|
10
|
+
export const HOOK_PATTERNS = [
|
|
11
|
+
'paginated-list',
|
|
12
|
+
'mutation',
|
|
13
|
+
'feature-flag',
|
|
14
|
+
'subscription',
|
|
15
|
+
];
|
|
16
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
17
|
+
const PATTERN_TEMPLATES = {
|
|
18
|
+
'paginated-list': paginatedListHookTemplate,
|
|
19
|
+
mutation: mutationHookTemplate,
|
|
20
|
+
'feature-flag': featureFlagHookTemplate,
|
|
21
|
+
subscription: subscriptionHookTemplate,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Scaffold a typed data-fetching hook file.
|
|
25
|
+
*/
|
|
26
|
+
export async function runAddHook(opts) {
|
|
27
|
+
const { name, pattern, outputDir } = opts;
|
|
28
|
+
if (!KEBAB_REGEX.test(name)) {
|
|
29
|
+
throw new Error(`Invalid hook name "${name}" — must be kebab-case`);
|
|
30
|
+
}
|
|
31
|
+
if (!HOOK_PATTERNS.includes(pattern)) {
|
|
32
|
+
const available = HOOK_PATTERNS.join(', ');
|
|
33
|
+
throw new Error(`Unknown hook pattern "${pattern}".\n\nAvailable patterns: ${available}`);
|
|
34
|
+
}
|
|
35
|
+
const templateFn = PATTERN_TEMPLATES[pattern];
|
|
36
|
+
const content = templateFn(name);
|
|
37
|
+
const outDir = outputDir
|
|
38
|
+
? resolve(outputDir)
|
|
39
|
+
: resolve(process.cwd(), 'src', 'hooks');
|
|
40
|
+
await mkdir(outDir, { recursive: true });
|
|
41
|
+
const fileName = `use-${name}.ts`;
|
|
42
|
+
const outPath = join(outDir, fileName);
|
|
43
|
+
// Refuse if target already exists
|
|
44
|
+
try {
|
|
45
|
+
await access(outPath);
|
|
46
|
+
throw new Error(`Hook file already exists at ${outPath}. Remove it first or use a different --output directory.`);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
if (err.code !== 'ENOENT')
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
await writeFile(outPath, content, 'utf8');
|
|
53
|
+
cliLogger.info({ name, pattern, outPath }, 'Hook scaffolded');
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.log(`\n✓ Hook "use-${name}" (pattern: ${pattern}) added at ${outPath}\n` +
|
|
56
|
+
` Import: import { use${capitalize(name)} } from './hooks/use-${name}';\n` +
|
|
57
|
+
` Pattern: ${pattern}\n`);
|
|
58
|
+
}
|
|
59
|
+
/** Capitalizes the first character of a kebab-case name into PascalCase. */
|
|
60
|
+
function capitalize(s) {
|
|
61
|
+
return s
|
|
62
|
+
.split('-')
|
|
63
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
64
|
+
.join('');
|
|
65
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to add a new database migration to an existing domain.
|
|
3
|
+
* Supports --pattern for predefined migration templates.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Valid migration pattern IDs.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MIGRATION_PATTERNS: readonly ["tenant-scoped-table", "rls-policy", "column-add", "index", "event-store"];
|
|
9
|
+
export type MigrationPattern = (typeof MIGRATION_PATTERNS)[number];
|
|
10
|
+
/**
|
|
11
|
+
* Options for the add-migration command.
|
|
12
|
+
*/
|
|
13
|
+
export interface AddMigrationOptions {
|
|
14
|
+
/** Domain ID in kebab-case. */
|
|
15
|
+
domain: string;
|
|
16
|
+
/** Migration name in kebab-case. */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Pattern template ID (required). */
|
|
19
|
+
pattern: MigrationPattern;
|
|
20
|
+
/** Override directory for domain root (used in tests). */
|
|
21
|
+
domainsDir?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Execute the add-migration command: create a new SQL migration in an existing domain.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runAddMigration(opts: AddMigrationOptions): Promise<void>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command to add a new database migration to an existing domain.
|
|
3
|
+
* Supports --pattern for predefined migration templates.
|
|
4
|
+
*/
|
|
5
|
+
import { writeFile, access, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { tenantScopedTableTemplate } from '../templates/patterns/migration/tenant-scoped-table.js';
|
|
8
|
+
import { rlsPolicyTemplate } from '../templates/patterns/migration/rls-policy.js';
|
|
9
|
+
import { columnAddTemplate } from '../templates/patterns/migration/column-add.js';
|
|
10
|
+
import { indexTemplate } from '../templates/patterns/migration/index.js';
|
|
11
|
+
import { eventStoreTemplate } from '../templates/patterns/migration/event-store.js';
|
|
12
|
+
import { cliLogger } from '../utils/logger.js';
|
|
13
|
+
/**
|
|
14
|
+
* Valid migration pattern IDs.
|
|
15
|
+
*/
|
|
16
|
+
export const MIGRATION_PATTERNS = [
|
|
17
|
+
'tenant-scoped-table',
|
|
18
|
+
'rls-policy',
|
|
19
|
+
'column-add',
|
|
20
|
+
'index',
|
|
21
|
+
'event-store',
|
|
22
|
+
];
|
|
23
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
24
|
+
/**
|
|
25
|
+
* Generates a timestamp prefix matching the project convention.
|
|
26
|
+
*/
|
|
27
|
+
function generateTimestamp() {
|
|
28
|
+
return new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Execute the add-migration command: create a new SQL migration in an existing domain.
|
|
32
|
+
*/
|
|
33
|
+
export async function runAddMigration(opts) {
|
|
34
|
+
if (!KEBAB_REGEX.test(opts.domain)) {
|
|
35
|
+
throw new Error(`Invalid domain id "${opts.domain}" — must be kebab-case`);
|
|
36
|
+
}
|
|
37
|
+
if (!KEBAB_REGEX.test(opts.name)) {
|
|
38
|
+
throw new Error(`Invalid migration name "${opts.name}" — must be kebab-case`);
|
|
39
|
+
}
|
|
40
|
+
if (!MIGRATION_PATTERNS.includes(opts.pattern)) {
|
|
41
|
+
throw new Error(`Unknown migration pattern "${opts.pattern}". Available: ${MIGRATION_PATTERNS.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
const root = opts.domainsDir ?? resolve(process.cwd(), 'domains');
|
|
44
|
+
const domainDir = join(root, opts.domain);
|
|
45
|
+
const domainConfigPath = join(domainDir, 'domain.config.ts');
|
|
46
|
+
// Verify domain exists
|
|
47
|
+
try {
|
|
48
|
+
await access(domainConfigPath);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error(`Domain "${opts.domain}" not found at ${domainDir}`);
|
|
52
|
+
}
|
|
53
|
+
const migrationsDir = join(domainDir, 'db', 'migrations');
|
|
54
|
+
await mkdir(migrationsDir, { recursive: true });
|
|
55
|
+
const timestamp = generateTimestamp();
|
|
56
|
+
const fileName = `${timestamp}_${opts.name}.sql`;
|
|
57
|
+
const filePath = join(migrationsDir, fileName);
|
|
58
|
+
// Refuse if migration already exists
|
|
59
|
+
try {
|
|
60
|
+
await access(filePath);
|
|
61
|
+
throw new Error(`Migration "${fileName}" already exists at ${filePath}`);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.code !== 'ENOENT') {
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Resolve pattern template
|
|
69
|
+
const sqlContent = resolveMigrationPattern(opts.pattern, opts.domain, opts.name);
|
|
70
|
+
await writeFile(filePath, sqlContent);
|
|
71
|
+
cliLogger.info({ domain: opts.domain, migration: opts.name, pattern: opts.pattern, file: filePath }, 'Migration added');
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.log(`\n✓ Migration "${opts.name}" added to domain "${opts.domain}" at ${filePath}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a migration pattern template to its generated SQL source.
|
|
77
|
+
*/
|
|
78
|
+
function resolveMigrationPattern(pattern, domain, name) {
|
|
79
|
+
switch (pattern) {
|
|
80
|
+
case 'tenant-scoped-table':
|
|
81
|
+
return tenantScopedTableTemplate(domain, name);
|
|
82
|
+
case 'rls-policy':
|
|
83
|
+
return rlsPolicyTemplate(domain, name);
|
|
84
|
+
case 'column-add':
|
|
85
|
+
return columnAddTemplate(domain, name);
|
|
86
|
+
case 'index':
|
|
87
|
+
return indexTemplate(domain, name);
|
|
88
|
+
case 'event-store':
|
|
89
|
+
return eventStoreTemplate(domain, name);
|
|
90
|
+
default:
|
|
91
|
+
throw new Error(`Unknown migration pattern "${pattern}". Available: ${MIGRATION_PATTERNS.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { Parser as TarParser } from 'tar';
|
|
5
|
+
import { readScaffoldConfig, writeScaffoldConfig } from '../utils/scaffold-config.js';
|
|
6
|
+
import { readManifest, writeManifest, upsertManifestFile, inferPolicyFromPath } from '../utils/manifest.js';
|
|
7
|
+
import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
|
|
8
|
+
import { applyTokens, buildTokenContext, stripTplExtension } from '../utils/token-substitution.js';
|
|
9
|
+
import { injectHeader } from '../utils/header-inject.js';
|
|
10
|
+
import { computeChecksumString } from '../utils/checksum.js';
|
|
11
|
+
import { installScaffoldFile } from '../utils/install-file.js';
|
|
12
|
+
import { cliLogger } from '../utils/logger.js';
|
|
13
|
+
async function parseTarball(buf) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
const chunks = {};
|
|
16
|
+
await new Promise((resolve, reject) => {
|
|
17
|
+
const parser = new TarParser({
|
|
18
|
+
onentry: (entry) => {
|
|
19
|
+
const p = entry.path;
|
|
20
|
+
chunks[p] = [];
|
|
21
|
+
entry.on('data', (chunk) => chunks[p].push(chunk));
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
parser.on('finish', resolve);
|
|
25
|
+
parser.on('error', reject);
|
|
26
|
+
Readable.from(buf).pipe(parser);
|
|
27
|
+
});
|
|
28
|
+
for (const [path, buffers] of Object.entries(chunks)) {
|
|
29
|
+
if (buffers.length > 0) {
|
|
30
|
+
entries.push({ path, content: Buffer.concat(buffers) });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return entries;
|
|
34
|
+
}
|
|
35
|
+
function getTarEntry(entries, path) {
|
|
36
|
+
return entries.find((e) => e.path === path);
|
|
37
|
+
}
|
|
38
|
+
async function getCliVersion() {
|
|
39
|
+
try {
|
|
40
|
+
const { createRequire } = await import('node:module');
|
|
41
|
+
const require = createRequire(import.meta.url);
|
|
42
|
+
const pkgJson = require('../../package.json');
|
|
43
|
+
return pkgJson.version;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return '0.0.0';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function detectRepoInfo() {
|
|
50
|
+
try {
|
|
51
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
stdio: 'pipe',
|
|
54
|
+
}).trim();
|
|
55
|
+
// Parse git@github.com:owner/repo.git or https://github.com/owner/repo.git
|
|
56
|
+
let match = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(remoteUrl);
|
|
57
|
+
if (match) {
|
|
58
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/, '') };
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function createGitHubPR(owner, repo, branchName, title, body, token) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${token}`,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
title,
|
|
76
|
+
body,
|
|
77
|
+
head: branchName,
|
|
78
|
+
base: 'develop',
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
const error = await response.json();
|
|
83
|
+
cliLogger.warn(error, 'Failed to create GitHub PR');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const data = (await response.json());
|
|
87
|
+
if (data.html_url && data.number) {
|
|
88
|
+
return { prUrl: data.html_url, prNumber: data.number };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
cliLogger.warn(err, 'Failed to create GitHub PR');
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function runAddModule(opts) {
|
|
98
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
99
|
+
const moduleId = opts.moduleId;
|
|
100
|
+
const dryRun = opts.dryRun ?? false;
|
|
101
|
+
// 1. Read .mc/scaffold-config.json
|
|
102
|
+
let config;
|
|
103
|
+
try {
|
|
104
|
+
config = await readScaffoldConfig(projectDir);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
throw new Error(`Failed to read scaffold config from ${projectDir}: ${err}`);
|
|
108
|
+
}
|
|
109
|
+
// 2. Fetch modules.json from S3
|
|
110
|
+
let modulesJson;
|
|
111
|
+
try {
|
|
112
|
+
modulesJson = await fetchModulesJson(config.scaffoldBucket, config.scaffoldVersion);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw new Error(`Failed to fetch modules.json for version ${config.scaffoldVersion}: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
// 3. Validate moduleId is a known module
|
|
118
|
+
const module = modulesJson.modules.find((m) => m.id === moduleId);
|
|
119
|
+
if (!module) {
|
|
120
|
+
const available = modulesJson.modules.map((m) => m.id).join(', ');
|
|
121
|
+
throw new Error(`Unknown module "${moduleId}". Available modules: ${available}`);
|
|
122
|
+
}
|
|
123
|
+
// 4. Check moduleId is NOT already installed
|
|
124
|
+
if (config.enabledModules.includes(moduleId)) {
|
|
125
|
+
throw new Error(`Module "${moduleId}" is already installed`);
|
|
126
|
+
}
|
|
127
|
+
// 5. Check all dependencies of moduleId ARE installed
|
|
128
|
+
for (const dep of module.dependencies) {
|
|
129
|
+
if (!config.enabledModules.includes(dep)) {
|
|
130
|
+
throw new Error(`Module "${moduleId}" requires "${dep}" which is not installed. ` +
|
|
131
|
+
`Install "${dep}" first with: tib add-module ${dep}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 6. Fetch tarball from S3
|
|
135
|
+
let tarBuf;
|
|
136
|
+
try {
|
|
137
|
+
tarBuf = await fetchModuleTarball(config.scaffoldBucket, config.scaffoldVersion, module.tarball, module.sha256);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
throw new Error(`Failed to fetch tarball for ${moduleId}: ${err}`);
|
|
141
|
+
}
|
|
142
|
+
// 7. Parse tarball
|
|
143
|
+
const tarEntries = await parseTarball(tarBuf);
|
|
144
|
+
// 8. Extract MANIFEST.json
|
|
145
|
+
const manifestEntry = getTarEntry(tarEntries, 'MANIFEST.json');
|
|
146
|
+
if (!manifestEntry) {
|
|
147
|
+
throw new Error(`No MANIFEST.json found in tarball for ${moduleId}`);
|
|
148
|
+
}
|
|
149
|
+
const tarManifest = JSON.parse(manifestEntry.content.toString('utf-8'));
|
|
150
|
+
// 9. Read manifest.json
|
|
151
|
+
let manifest = await readManifest(projectDir);
|
|
152
|
+
if (!manifest) {
|
|
153
|
+
throw new Error(`No .mc/manifest.json found in ${projectDir}`);
|
|
154
|
+
}
|
|
155
|
+
// 10. Get CLI version for header
|
|
156
|
+
const cliVersion = await getCliVersion();
|
|
157
|
+
// 11. Build token context
|
|
158
|
+
const tokenCtx = buildTokenContext(config, config.scaffoldVersion, cliVersion);
|
|
159
|
+
// 12. Process files from tarball
|
|
160
|
+
const filesInstalled = [];
|
|
161
|
+
for (const fileEntry of tarManifest) {
|
|
162
|
+
const tarEntry = getTarEntry(tarEntries, fileEntry.path);
|
|
163
|
+
if (!tarEntry) {
|
|
164
|
+
cliLogger.warn({ moduleId, path: fileEntry.path }, 'File in MANIFEST.json not found in tarball');
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Determine install path
|
|
168
|
+
let installPath = fileEntry.installedAs ?? fileEntry.path;
|
|
169
|
+
// Strip .tpl extension
|
|
170
|
+
if (installPath.endsWith('.tpl')) {
|
|
171
|
+
installPath = stripTplExtension(installPath);
|
|
172
|
+
}
|
|
173
|
+
// Process content
|
|
174
|
+
let content = tarEntry.content.toString('utf-8');
|
|
175
|
+
// Token substitution for template files
|
|
176
|
+
if (fileEntry.isTemplate) {
|
|
177
|
+
content = applyTokens(content, tokenCtx);
|
|
178
|
+
}
|
|
179
|
+
// Inject scaffold header
|
|
180
|
+
content = injectHeader(content, installPath, moduleId, module.version);
|
|
181
|
+
if (!dryRun) {
|
|
182
|
+
// Determine policy: use fileEntry.policy if present, else infer from path
|
|
183
|
+
const policy = fileEntry.policy ?? inferPolicyFromPath(installPath);
|
|
184
|
+
// Get current entry if exists
|
|
185
|
+
const currentEntry = manifest.files.find((f) => f.path === installPath);
|
|
186
|
+
// Install file using policy-based dispatch
|
|
187
|
+
const destPath = join(projectDir, installPath);
|
|
188
|
+
const installResult = await installScaffoldFile(destPath, content, policy, currentEntry, {
|
|
189
|
+
dryRun: false,
|
|
190
|
+
});
|
|
191
|
+
// Only update manifest if file was not skipped
|
|
192
|
+
if (installResult.status !== 'skipped') {
|
|
193
|
+
const sha256 = computeChecksumString(content);
|
|
194
|
+
upsertManifestFile(manifest, {
|
|
195
|
+
path: installPath,
|
|
196
|
+
module: moduleId,
|
|
197
|
+
moduleVersion: module.version,
|
|
198
|
+
sha256,
|
|
199
|
+
wasTemplate: fileEntry.isTemplate,
|
|
200
|
+
installedAt: new Date().toISOString(),
|
|
201
|
+
policy,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
filesInstalled.push(installPath);
|
|
206
|
+
}
|
|
207
|
+
if (dryRun) {
|
|
208
|
+
console.log(`\n✓ Dry run: would install ${filesInstalled.length} file(s) from ${moduleId}@${module.version}`);
|
|
209
|
+
filesInstalled.forEach((f) => console.log(` ${f}`));
|
|
210
|
+
console.log(`\nTo apply changes, run: tib add-module ${moduleId}\n`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// 13. Update .mc/manifest.json
|
|
214
|
+
await writeManifest(projectDir, manifest);
|
|
215
|
+
// 14. Update .mc/scaffold-config.json
|
|
216
|
+
config.enabledModules = [...config.enabledModules, moduleId];
|
|
217
|
+
await writeScaffoldConfig(projectDir, config);
|
|
218
|
+
console.log(`\n✓ Module "${moduleId}" installed (${filesInstalled.length} file(s) added)`);
|
|
219
|
+
// 15. Git operations
|
|
220
|
+
const branchName = `tib/add-module-${moduleId}`;
|
|
221
|
+
let commitSha = null;
|
|
222
|
+
try {
|
|
223
|
+
// Create new branch
|
|
224
|
+
execSync(`git -C "${projectDir}" checkout -b ${branchName}`, { stdio: 'pipe' });
|
|
225
|
+
// Stage changes
|
|
226
|
+
execSync(`git -C "${projectDir}" add .mc/`, { stdio: 'pipe' });
|
|
227
|
+
// Commit
|
|
228
|
+
const commitMsg = `feat: add tib module ${moduleId}@${config.scaffoldVersion}`;
|
|
229
|
+
execSync(`git -C "${projectDir}" commit -m "${commitMsg}"`, { stdio: 'pipe' });
|
|
230
|
+
commitSha = execSync(`git -C "${projectDir}" rev-parse HEAD`, {
|
|
231
|
+
encoding: 'utf8',
|
|
232
|
+
stdio: 'pipe',
|
|
233
|
+
}).trim();
|
|
234
|
+
cliLogger.info({ moduleId, commitSha, branchName }, 'Module commit created');
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
cliLogger.warn(err, `Failed to create git commit for module ${moduleId}`);
|
|
238
|
+
}
|
|
239
|
+
// 16. GitHub PR creation if token available
|
|
240
|
+
const githubToken = opts.githubToken ?? process.env.GITHUB_TOKEN;
|
|
241
|
+
if (githubToken && commitSha) {
|
|
242
|
+
try {
|
|
243
|
+
// Push branch to remote
|
|
244
|
+
execSync(`git -C "${projectDir}" push -u origin ${branchName}`, { stdio: 'pipe' });
|
|
245
|
+
const repoInfo = await detectRepoInfo();
|
|
246
|
+
if (repoInfo) {
|
|
247
|
+
const prResult = await createGitHubPR(repoInfo.owner, repoInfo.repo, branchName, `feat: add tib module ${moduleId}`, `Adds optional scaffold module: ${moduleId}@${config.scaffoldVersion}\n\nFiles added: ${filesInstalled.length}`, githubToken);
|
|
248
|
+
if (prResult) {
|
|
249
|
+
console.log(`\nPR created: ${prResult.prUrl}`);
|
|
250
|
+
cliLogger.info({ prUrl: prResult.prUrl, prNumber: prResult.prNumber }, 'Module PR created');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
cliLogger.warn(err, 'Failed to push and create PR');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (commitSha) {
|
|
259
|
+
console.log(`\nBranch "${branchName}" created. Push with: git push -u origin ${branchName}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PagePattern } from '../templates/patterns/page/index.js';
|
|
2
|
+
export interface AddPageOptions {
|
|
3
|
+
pageId: string;
|
|
4
|
+
outputDir?: string;
|
|
5
|
+
auth?: boolean;
|
|
6
|
+
protected?: boolean;
|
|
7
|
+
public?: boolean;
|
|
8
|
+
/** Pattern-based scaffolding (list, detail, form, wizard, dashboard, settings). */
|
|
9
|
+
pattern?: PagePattern;
|
|
10
|
+
/** Layout to use: app (default) or auth. */
|
|
11
|
+
layout?: 'app' | 'auth';
|
|
12
|
+
}
|
|
13
|
+
export declare function runAddPage(opts: AddPageOptions): Promise<void>;
|