@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,347 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { Parser as TarParser } from 'tar';
|
|
6
|
+
import { readScaffoldConfig, writeScaffoldConfig } from '../utils/scaffold-config.js';
|
|
7
|
+
import { readManifest, writeManifest, upsertManifestFile, inferPolicyFromPath } from '../utils/manifest.js';
|
|
8
|
+
import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
|
|
9
|
+
import { applyTokens, buildTokenContext, stripTplExtension } from '../utils/token-substitution.js';
|
|
10
|
+
import { injectHeader } from '../utils/header-inject.js';
|
|
11
|
+
import { computeChecksumString } from '../utils/checksum.js';
|
|
12
|
+
import { installScaffoldFile } from '../utils/install-file.js';
|
|
13
|
+
import { cliLogger } from '../utils/logger.js';
|
|
14
|
+
|
|
15
|
+
export interface AddModuleOptions {
|
|
16
|
+
moduleId: string;
|
|
17
|
+
githubToken?: string;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
projectDir?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TarballManifestEntry {
|
|
23
|
+
path: string;
|
|
24
|
+
installedAs?: string;
|
|
25
|
+
isTemplate: boolean;
|
|
26
|
+
policy?: 'owned' | 'tracked' | 'seed';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TarEntry {
|
|
30
|
+
path: string;
|
|
31
|
+
content: Buffer;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function parseTarball(buf: Buffer): Promise<TarEntry[]> {
|
|
35
|
+
const entries: TarEntry[] = [];
|
|
36
|
+
const chunks: { [key: string]: Buffer[] } = {};
|
|
37
|
+
|
|
38
|
+
await new Promise<void>((resolve, reject) => {
|
|
39
|
+
const parser = new TarParser({
|
|
40
|
+
onentry: (entry) => {
|
|
41
|
+
const p = entry.path;
|
|
42
|
+
chunks[p] = [];
|
|
43
|
+
entry.on('data', (chunk: Buffer) => chunks[p].push(chunk));
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
parser.on('finish', resolve);
|
|
47
|
+
parser.on('error', reject);
|
|
48
|
+
Readable.from(buf).pipe(parser);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
for (const [path, buffers] of Object.entries(chunks)) {
|
|
52
|
+
if (buffers.length > 0) {
|
|
53
|
+
entries.push({ path, content: Buffer.concat(buffers) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entries;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getTarEntry(entries: TarEntry[], path: string): TarEntry | undefined {
|
|
61
|
+
return entries.find((e) => e.path === path);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function getCliVersion(): Promise<string> {
|
|
65
|
+
try {
|
|
66
|
+
const { createRequire } = await import('node:module');
|
|
67
|
+
const require = createRequire(import.meta.url);
|
|
68
|
+
const pkgJson = require('../../package.json') as { version: string };
|
|
69
|
+
return pkgJson.version;
|
|
70
|
+
} catch {
|
|
71
|
+
return '0.0.0';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function detectRepoInfo(): Promise<{ owner: string; repo: string } | null> {
|
|
76
|
+
try {
|
|
77
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
stdio: 'pipe',
|
|
80
|
+
}).trim();
|
|
81
|
+
|
|
82
|
+
// Parse git@github.com:owner/repo.git or https://github.com/owner/repo.git
|
|
83
|
+
let match = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(remoteUrl);
|
|
84
|
+
if (match) {
|
|
85
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/, '') };
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function createGitHubPR(
|
|
94
|
+
owner: string,
|
|
95
|
+
repo: string,
|
|
96
|
+
branchName: string,
|
|
97
|
+
title: string,
|
|
98
|
+
body: string,
|
|
99
|
+
token: string
|
|
100
|
+
): Promise<{ prUrl: string; prNumber: number } | null> {
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${token}`,
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
title,
|
|
110
|
+
body,
|
|
111
|
+
head: branchName,
|
|
112
|
+
base: 'develop',
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = await response.json();
|
|
118
|
+
cliLogger.warn(error, 'Failed to create GitHub PR');
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = (await response.json()) as { html_url?: string; number?: number };
|
|
123
|
+
if (data.html_url && data.number) {
|
|
124
|
+
return { prUrl: data.html_url, prNumber: data.number };
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
cliLogger.warn(err, 'Failed to create GitHub PR');
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function runAddModule(opts: AddModuleOptions): Promise<void> {
|
|
134
|
+
const projectDir = opts.projectDir ?? process.cwd();
|
|
135
|
+
const moduleId = opts.moduleId;
|
|
136
|
+
const dryRun = opts.dryRun ?? false;
|
|
137
|
+
|
|
138
|
+
// 1. Read .mc/scaffold-config.json
|
|
139
|
+
let config;
|
|
140
|
+
try {
|
|
141
|
+
config = await readScaffoldConfig(projectDir);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new Error(`Failed to read scaffold config from ${projectDir}: ${err}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Fetch modules.json from S3
|
|
147
|
+
let modulesJson;
|
|
148
|
+
try {
|
|
149
|
+
modulesJson = await fetchModulesJson(config.scaffoldBucket, config.scaffoldVersion);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
throw new Error(`Failed to fetch modules.json for version ${config.scaffoldVersion}: ${err}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. Validate moduleId is a known module
|
|
155
|
+
const module = modulesJson.modules.find((m) => m.id === moduleId);
|
|
156
|
+
if (!module) {
|
|
157
|
+
const available = modulesJson.modules.map((m) => m.id).join(', ');
|
|
158
|
+
throw new Error(`Unknown module "${moduleId}". Available modules: ${available}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 4. Check moduleId is NOT already installed
|
|
162
|
+
if (config.enabledModules.includes(moduleId as any)) {
|
|
163
|
+
throw new Error(`Module "${moduleId}" is already installed`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 5. Check all dependencies of moduleId ARE installed
|
|
167
|
+
for (const dep of module.dependencies) {
|
|
168
|
+
if (!config.enabledModules.includes(dep as any)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Module "${moduleId}" requires "${dep}" which is not installed. ` +
|
|
171
|
+
`Install "${dep}" first with: tib add-module ${dep}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 6. Fetch tarball from S3
|
|
177
|
+
let tarBuf;
|
|
178
|
+
try {
|
|
179
|
+
tarBuf = await fetchModuleTarball(
|
|
180
|
+
config.scaffoldBucket,
|
|
181
|
+
config.scaffoldVersion,
|
|
182
|
+
module.tarball,
|
|
183
|
+
module.sha256
|
|
184
|
+
);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
throw new Error(`Failed to fetch tarball for ${moduleId}: ${err}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 7. Parse tarball
|
|
190
|
+
const tarEntries = await parseTarball(tarBuf);
|
|
191
|
+
|
|
192
|
+
// 8. Extract MANIFEST.json
|
|
193
|
+
const manifestEntry = getTarEntry(tarEntries, 'MANIFEST.json');
|
|
194
|
+
if (!manifestEntry) {
|
|
195
|
+
throw new Error(`No MANIFEST.json found in tarball for ${moduleId}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const tarManifest = JSON.parse(manifestEntry.content.toString('utf-8')) as TarballManifestEntry[];
|
|
199
|
+
|
|
200
|
+
// 9. Read manifest.json
|
|
201
|
+
let manifest = await readManifest(projectDir);
|
|
202
|
+
if (!manifest) {
|
|
203
|
+
throw new Error(`No .mc/manifest.json found in ${projectDir}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 10. Get CLI version for header
|
|
207
|
+
const cliVersion = await getCliVersion();
|
|
208
|
+
|
|
209
|
+
// 11. Build token context
|
|
210
|
+
const tokenCtx = buildTokenContext(config, config.scaffoldVersion, cliVersion);
|
|
211
|
+
|
|
212
|
+
// 12. Process files from tarball
|
|
213
|
+
const filesInstalled: string[] = [];
|
|
214
|
+
|
|
215
|
+
for (const fileEntry of tarManifest) {
|
|
216
|
+
const tarEntry = getTarEntry(tarEntries, fileEntry.path);
|
|
217
|
+
if (!tarEntry) {
|
|
218
|
+
cliLogger.warn({ moduleId, path: fileEntry.path }, 'File in MANIFEST.json not found in tarball');
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Determine install path
|
|
223
|
+
let installPath = fileEntry.installedAs ?? fileEntry.path;
|
|
224
|
+
|
|
225
|
+
// Strip .tpl extension
|
|
226
|
+
if (installPath.endsWith('.tpl')) {
|
|
227
|
+
installPath = stripTplExtension(installPath);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Process content
|
|
231
|
+
let content = tarEntry.content.toString('utf-8');
|
|
232
|
+
|
|
233
|
+
// Token substitution for template files
|
|
234
|
+
if (fileEntry.isTemplate) {
|
|
235
|
+
content = applyTokens(content, tokenCtx);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Inject scaffold header
|
|
239
|
+
content = injectHeader(content, installPath, moduleId, module.version);
|
|
240
|
+
|
|
241
|
+
if (!dryRun) {
|
|
242
|
+
// Determine policy: use fileEntry.policy if present, else infer from path
|
|
243
|
+
const policy = fileEntry.policy ?? inferPolicyFromPath(installPath);
|
|
244
|
+
|
|
245
|
+
// Get current entry if exists
|
|
246
|
+
const currentEntry = manifest.files.find((f) => f.path === installPath);
|
|
247
|
+
|
|
248
|
+
// Install file using policy-based dispatch
|
|
249
|
+
const destPath = join(projectDir, installPath);
|
|
250
|
+
const installResult = await installScaffoldFile(destPath, content, policy, currentEntry, {
|
|
251
|
+
dryRun: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Only update manifest if file was not skipped
|
|
255
|
+
if (installResult.status !== 'skipped') {
|
|
256
|
+
const sha256 = computeChecksumString(content);
|
|
257
|
+
upsertManifestFile(manifest, {
|
|
258
|
+
path: installPath,
|
|
259
|
+
module: moduleId,
|
|
260
|
+
moduleVersion: module.version,
|
|
261
|
+
sha256,
|
|
262
|
+
wasTemplate: fileEntry.isTemplate,
|
|
263
|
+
installedAt: new Date().toISOString(),
|
|
264
|
+
policy,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
filesInstalled.push(installPath);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (dryRun) {
|
|
273
|
+
console.log(`\n✓ Dry run: would install ${filesInstalled.length} file(s) from ${moduleId}@${module.version}`);
|
|
274
|
+
filesInstalled.forEach((f) => console.log(` ${f}`));
|
|
275
|
+
console.log(
|
|
276
|
+
`\nTo apply changes, run: tib add-module ${moduleId}\n`
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 13. Update .mc/manifest.json
|
|
282
|
+
await writeManifest(projectDir, manifest);
|
|
283
|
+
|
|
284
|
+
// 14. Update .mc/scaffold-config.json
|
|
285
|
+
config.enabledModules = [...config.enabledModules, moduleId as any];
|
|
286
|
+
await writeScaffoldConfig(projectDir, config);
|
|
287
|
+
|
|
288
|
+
console.log(
|
|
289
|
+
`\n✓ Module "${moduleId}" installed (${filesInstalled.length} file(s) added)`
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// 15. Git operations
|
|
293
|
+
const branchName = `tib/add-module-${moduleId}`;
|
|
294
|
+
let commitSha: string | null = null;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
// Create new branch
|
|
298
|
+
execSync(`git -C "${projectDir}" checkout -b ${branchName}`, { stdio: 'pipe' });
|
|
299
|
+
|
|
300
|
+
// Stage changes
|
|
301
|
+
execSync(`git -C "${projectDir}" add .mc/`, { stdio: 'pipe' });
|
|
302
|
+
|
|
303
|
+
// Commit
|
|
304
|
+
const commitMsg = `feat: add tib module ${moduleId}@${config.scaffoldVersion}`;
|
|
305
|
+
execSync(`git -C "${projectDir}" commit -m "${commitMsg}"`, { stdio: 'pipe' });
|
|
306
|
+
|
|
307
|
+
commitSha = execSync(`git -C "${projectDir}" rev-parse HEAD`, {
|
|
308
|
+
encoding: 'utf8',
|
|
309
|
+
stdio: 'pipe',
|
|
310
|
+
}).trim();
|
|
311
|
+
|
|
312
|
+
cliLogger.info({ moduleId, commitSha, branchName }, 'Module commit created');
|
|
313
|
+
} catch (err) {
|
|
314
|
+
cliLogger.warn(err, `Failed to create git commit for module ${moduleId}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 16. GitHub PR creation if token available
|
|
318
|
+
const githubToken = opts.githubToken ?? process.env.GITHUB_TOKEN;
|
|
319
|
+
|
|
320
|
+
if (githubToken && commitSha) {
|
|
321
|
+
try {
|
|
322
|
+
// Push branch to remote
|
|
323
|
+
execSync(`git -C "${projectDir}" push -u origin ${branchName}`, { stdio: 'pipe' });
|
|
324
|
+
|
|
325
|
+
const repoInfo = await detectRepoInfo();
|
|
326
|
+
if (repoInfo) {
|
|
327
|
+
const prResult = await createGitHubPR(
|
|
328
|
+
repoInfo.owner,
|
|
329
|
+
repoInfo.repo,
|
|
330
|
+
branchName,
|
|
331
|
+
`feat: add tib module ${moduleId}`,
|
|
332
|
+
`Adds optional scaffold module: ${moduleId}@${config.scaffoldVersion}\n\nFiles added: ${filesInstalled.length}`,
|
|
333
|
+
githubToken
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (prResult) {
|
|
337
|
+
console.log(`\nPR created: ${prResult.prUrl}`);
|
|
338
|
+
cliLogger.info({ prUrl: prResult.prUrl, prNumber: prResult.prNumber }, 'Module PR created');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
cliLogger.warn(err, 'Failed to push and create PR');
|
|
343
|
+
}
|
|
344
|
+
} else if (commitSha) {
|
|
345
|
+
console.log(`\nBranch "${branchName}" created. Push with: git push -u origin ${branchName}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { Parser as TarParser } from 'tar';
|
|
6
|
+
import { cliLogger } from '../utils/logger.js';
|
|
7
|
+
import { readScaffoldConfig } from '../utils/scaffold-config.js';
|
|
8
|
+
import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
|
|
9
|
+
import {
|
|
10
|
+
type PagePattern,
|
|
11
|
+
PAGE_PATTERNS,
|
|
12
|
+
listPageTemplate,
|
|
13
|
+
detailPageTemplate,
|
|
14
|
+
formPageTemplate,
|
|
15
|
+
wizardPageTemplate,
|
|
16
|
+
dashboardPageTemplate,
|
|
17
|
+
settingsPageTemplate,
|
|
18
|
+
} from '../templates/patterns/page/index.js';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const TEMPLATES_DIR = resolve(__dirname, '..', 'templates', 'dashboard-pages');
|
|
22
|
+
|
|
23
|
+
/** Map from pattern name to its template function. */
|
|
24
|
+
const PATTERN_TEMPLATES: Record<PagePattern, (name: string) => string> = {
|
|
25
|
+
list: listPageTemplate,
|
|
26
|
+
detail: detailPageTemplate,
|
|
27
|
+
form: formPageTemplate,
|
|
28
|
+
wizard: wizardPageTemplate,
|
|
29
|
+
dashboard: dashboardPageTemplate,
|
|
30
|
+
settings: settingsPageTemplate,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
|
|
34
|
+
|
|
35
|
+
export interface AddPageOptions {
|
|
36
|
+
pageId: string;
|
|
37
|
+
outputDir?: string;
|
|
38
|
+
auth?: boolean;
|
|
39
|
+
protected?: boolean;
|
|
40
|
+
public?: boolean;
|
|
41
|
+
/** Pattern-based scaffolding (list, detail, form, wizard, dashboard, settings). */
|
|
42
|
+
pattern?: PagePattern;
|
|
43
|
+
/** Layout to use: app (default) or auth. */
|
|
44
|
+
layout?: 'app' | 'auth';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PageEntry {
|
|
48
|
+
id: string;
|
|
49
|
+
category: string;
|
|
50
|
+
path: string;
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function loadRegistry(): Promise<PageEntry[]> {
|
|
55
|
+
// Dynamic import avoids tsx compilation issues with .tsx templates
|
|
56
|
+
const mod = await import('../templates/dashboard-pages/index.js') as { PAGE_REGISTRY: readonly PageEntry[] };
|
|
57
|
+
return [...mod.PAGE_REGISTRY];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getCategoryForPageId(pageId: string): string {
|
|
61
|
+
const authPages = ['login', 'signup', 'verify-email', 'forgot-password', 'reset-password', 'mfa-setup', 'mfa-verify', 'choose-org', 'accept-invitation', 'sign-out'];
|
|
62
|
+
const accountPages = ['profile', 'workspace-settings', 'members', 'api-keys', 'audit-log'];
|
|
63
|
+
|
|
64
|
+
if (authPages.includes(pageId)) return 'auth';
|
|
65
|
+
if (accountPages.includes(pageId)) return 'account';
|
|
66
|
+
return 'shell';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function fetchPageFromS3(
|
|
70
|
+
scaffoldBucket: string,
|
|
71
|
+
scaffoldVersion: string,
|
|
72
|
+
pageId: string
|
|
73
|
+
): Promise<string | null> {
|
|
74
|
+
try {
|
|
75
|
+
const modulesJson = await fetchModulesJson(scaffoldBucket, scaffoldVersion);
|
|
76
|
+
const frontendModule = modulesJson.modules.find(m => m.id === 'frontend');
|
|
77
|
+
|
|
78
|
+
if (!frontendModule) {
|
|
79
|
+
throw new Error(`Frontend module not found in modules.json for version ${scaffoldVersion}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tarballBuffer = await fetchModuleTarball(
|
|
83
|
+
scaffoldBucket,
|
|
84
|
+
scaffoldVersion,
|
|
85
|
+
frontendModule.tarball,
|
|
86
|
+
frontendModule.sha256
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Extract the page file from the tarball
|
|
90
|
+
const pageFileName = `${pageId}.tsx`;
|
|
91
|
+
const category = getCategoryForPageId(pageId);
|
|
92
|
+
const expectedPath = `frontend/src/pages/${category}/${pageFileName}`;
|
|
93
|
+
|
|
94
|
+
let content = '';
|
|
95
|
+
const chunks: Buffer[] = [];
|
|
96
|
+
|
|
97
|
+
await new Promise<void>((resolve, reject) => {
|
|
98
|
+
const parser = new TarParser({
|
|
99
|
+
onentry: (entry) => {
|
|
100
|
+
if (entry.path === expectedPath) {
|
|
101
|
+
entry.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
102
|
+
} else {
|
|
103
|
+
entry.resume();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
parser.on('finish', resolve);
|
|
108
|
+
parser.on('error', reject);
|
|
109
|
+
Readable.from(tarballBuffer).pipe(parser);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (chunks.length === 0) {
|
|
113
|
+
throw new Error(`Page template not found at ${expectedPath} in frontend tarball`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
content = Buffer.concat(chunks).toString('utf-8');
|
|
117
|
+
return content;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
cliLogger.warn(err, 'Failed to fetch from S3, falling back to local templates');
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function loadFromLocal(pageId: string, entry: PageEntry): Promise<string> {
|
|
125
|
+
const templatePath = join(TEMPLATES_DIR, entry.path);
|
|
126
|
+
try {
|
|
127
|
+
const content = await readFile(templatePath, 'utf8');
|
|
128
|
+
cliLogger.warn({ pageId }, 'Using local fallback template (scaffold version not available)');
|
|
129
|
+
return content;
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error(`Template file not found at ${templatePath}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function runAddPage(opts: AddPageOptions): Promise<void> {
|
|
136
|
+
const { pageId, pattern, layout = 'app' } = opts;
|
|
137
|
+
|
|
138
|
+
if (!KEBAB_REGEX.test(pageId)) {
|
|
139
|
+
throw new Error(`Invalid page id "${pageId}" — must be kebab-case`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Pattern mode ──
|
|
143
|
+
if (pattern) {
|
|
144
|
+
if (!PAGE_PATTERNS.includes(pattern)) {
|
|
145
|
+
const available = PAGE_PATTERNS.join(', ');
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Unknown page pattern "${pattern}".\n\nAvailable patterns: ${available}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const templateFn = PATTERN_TEMPLATES[pattern];
|
|
152
|
+
let content = templateFn(pageId);
|
|
153
|
+
|
|
154
|
+
// Prepend layout comment
|
|
155
|
+
const layoutComment = `// Layout: ${layout === 'auth' ? 'AuthLayout' : 'AppLayout'} — import and use in your router config\n\n`;
|
|
156
|
+
content = layoutComment + content;
|
|
157
|
+
|
|
158
|
+
const outDir = opts.outputDir
|
|
159
|
+
? resolve(opts.outputDir)
|
|
160
|
+
: resolve(process.cwd(), 'src', 'pages', pageId);
|
|
161
|
+
|
|
162
|
+
await mkdir(outDir, { recursive: true });
|
|
163
|
+
|
|
164
|
+
const fileName = `${pageId}.tsx`;
|
|
165
|
+
const outPath = join(outDir, fileName);
|
|
166
|
+
|
|
167
|
+
// Refuse if target already exists
|
|
168
|
+
try {
|
|
169
|
+
await access(outPath);
|
|
170
|
+
throw new Error(`File already exists at ${outPath}. Remove it first or use a different --output directory.`);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await writeFile(outPath, content, 'utf8');
|
|
176
|
+
|
|
177
|
+
cliLogger.info({ pageId, pattern, layout, outPath }, 'Page scaffolded from pattern');
|
|
178
|
+
// eslint-disable-next-line no-console
|
|
179
|
+
console.log(
|
|
180
|
+
`\n✓ Page "${pageId}" (pattern: ${pattern}, layout: ${layout}) added at ${outPath}\n` +
|
|
181
|
+
` Next: add a route in src/router.tsx for this page\n`
|
|
182
|
+
);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Registry mode (existing behaviour) ──
|
|
187
|
+
const registry = await loadRegistry();
|
|
188
|
+
const entry = registry.find(p => p.id === pageId);
|
|
189
|
+
|
|
190
|
+
if (!entry) {
|
|
191
|
+
const available = registry.map(p => p.id).join(', ');
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Unknown page id "${pageId}".\n\nAvailable pages: ${available}\n` +
|
|
194
|
+
`Tip: use --pattern <pattern> to scaffold with a page pattern. Available patterns: ${PAGE_PATTERNS.join(', ')}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let content: string | null = null;
|
|
199
|
+
|
|
200
|
+
// Try S3 fetch first
|
|
201
|
+
try {
|
|
202
|
+
const config = await readScaffoldConfig(process.cwd());
|
|
203
|
+
if (config.scaffoldVersion && config.scaffoldBucket) {
|
|
204
|
+
content = await fetchPageFromS3(config.scaffoldBucket, config.scaffoldVersion, pageId);
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
cliLogger.debug(err, 'Could not read scaffold config');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Fall back to local templates
|
|
211
|
+
if (!content) {
|
|
212
|
+
content = await loadFromLocal(pageId, entry);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const outDir = opts.outputDir
|
|
216
|
+
? resolve(opts.outputDir)
|
|
217
|
+
: resolve(process.cwd(), 'src', 'pages', entry.category);
|
|
218
|
+
|
|
219
|
+
await mkdir(outDir, { recursive: true });
|
|
220
|
+
|
|
221
|
+
const fileName = `${basename(entry.path)}`;
|
|
222
|
+
const outPath = join(outDir, fileName);
|
|
223
|
+
|
|
224
|
+
// Refuse if target already exists
|
|
225
|
+
try {
|
|
226
|
+
await access(outPath);
|
|
227
|
+
throw new Error(`File already exists at ${outPath}. Remove it first or use a different --output directory.`);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await writeFile(outPath, content, 'utf8');
|
|
233
|
+
|
|
234
|
+
cliLogger.info({ pageId, layout, outPath }, 'Page scaffolded');
|
|
235
|
+
// eslint-disable-next-line no-console
|
|
236
|
+
console.log(
|
|
237
|
+
`\n✓ Page "${pageId}" (layout: ${layout}) added at ${outPath}\n` +
|
|
238
|
+
` Component: ${entry.name}\n` +
|
|
239
|
+
` Next: import { ${entry.name} } from './${entry.category}/${basename(entry.path, '.tsx')}'\n` +
|
|
240
|
+
` Then add a route in src/router.tsx\n`
|
|
241
|
+
);
|
|
242
|
+
}
|