@mettlecast/domain-cli 0.2.2 → 0.2.3

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.
Files changed (46) hide show
  1. package/dist/cli.js +9 -1
  2. package/dist/commands/add-page.js +39 -126
  3. package/dist/commands/build-ui/component-scanner.d.ts +12 -0
  4. package/dist/commands/build-ui/component-scanner.js +50 -0
  5. package/dist/commands/build-ui/page-indexer.d.ts +13 -0
  6. package/dist/commands/build-ui/page-indexer.js +107 -0
  7. package/dist/commands/build-ui.d.ts +3 -0
  8. package/dist/commands/build-ui.js +23 -0
  9. package/dist/templates/api-skeleton.js +1 -1
  10. package/dist/templates/events-skeleton.js +1 -1
  11. package/dist/templates/subscriber-skeleton.js +1 -1
  12. package/package.json +1 -1
  13. package/src/cli.ts +10 -1
  14. package/src/commands/add-page.ts +40 -156
  15. package/src/commands/build-ui/component-scanner.ts +70 -0
  16. package/src/commands/build-ui/page-indexer.ts +134 -0
  17. package/src/commands/build-ui.ts +29 -0
  18. package/src/templates/api-skeleton.ts +1 -1
  19. package/src/templates/events-skeleton.ts +1 -1
  20. package/src/templates/subscriber-skeleton.ts +1 -1
  21. package/dist/templates/dashboard-pages/index.d.ts +0 -101
  22. package/dist/templates/dashboard-pages/index.js +0 -22
  23. package/src/templates/dashboard-pages/account/api-keys.tsx +0 -107
  24. package/src/templates/dashboard-pages/account/audit-log.tsx +0 -60
  25. package/src/templates/dashboard-pages/account/index.ts +0 -5
  26. package/src/templates/dashboard-pages/account/members.tsx +0 -107
  27. package/src/templates/dashboard-pages/account/profile.tsx +0 -53
  28. package/src/templates/dashboard-pages/account/workspace-settings.tsx +0 -64
  29. package/src/templates/dashboard-pages/auth/accept-invitation.tsx +0 -62
  30. package/src/templates/dashboard-pages/auth/choose-org.tsx +0 -66
  31. package/src/templates/dashboard-pages/auth/forgot-password.tsx +0 -69
  32. package/src/templates/dashboard-pages/auth/index.ts +0 -10
  33. package/src/templates/dashboard-pages/auth/login.tsx +0 -75
  34. package/src/templates/dashboard-pages/auth/mfa-setup.tsx +0 -56
  35. package/src/templates/dashboard-pages/auth/mfa-verify.tsx +0 -49
  36. package/src/templates/dashboard-pages/auth/reset-password.tsx +0 -64
  37. package/src/templates/dashboard-pages/auth/sign-out.tsx +0 -20
  38. package/src/templates/dashboard-pages/auth/signup.tsx +0 -71
  39. package/src/templates/dashboard-pages/auth/verify-email.tsx +0 -53
  40. package/src/templates/dashboard-pages/index.ts +0 -22
  41. package/src/templates/dashboard-pages/shell/dashboard-home.tsx +0 -41
  42. package/src/templates/dashboard-pages/shell/forbidden.tsx +0 -46
  43. package/src/templates/dashboard-pages/shell/index.ts +0 -5
  44. package/src/templates/dashboard-pages/shell/maintenance.tsx +0 -27
  45. package/src/templates/dashboard-pages/shell/not-found.tsx +0 -34
  46. package/src/templates/dashboard-pages/shell/server-error.tsx +0 -44
package/dist/cli.js CHANGED
@@ -30,6 +30,7 @@ import { runPowerTune } from './commands/power-tune.js';
30
30
  import { runReseedPage } from './commands/reseed-page.js';
31
31
  import { runAddSeedPage } from './commands/add-seed-page.js';
32
32
  import { runShowDns } from './commands/show-dns.js';
33
+ import { runBuildUi } from './commands/build-ui.js';
33
34
  /**
34
35
  * Root CLI program for the TIB Domain Module local developer toolchain.
35
36
  * Provides build, validate, test, dev, add-domain, add-api, and add-subscriber subcommands.
@@ -174,7 +175,7 @@ program
174
175
  });
175
176
  program
176
177
  .command('add-page <pageId>')
177
- .description('Scaffold a seed page from the dashboard-pages template registry')
178
+ .description('Scaffold a .page.json definition (default) or a pattern-based .tsx page component (--pattern)')
178
179
  .option('--output <dir>', 'Override the output directory (default: src/pages/<category>)')
179
180
  .option('--pattern <pattern>', 'Scaffold with a page pattern template (list, detail, form, wizard, dashboard, settings)')
180
181
  .option('--layout <layout>', 'Layout to use: app (default) or auth', 'app')
@@ -331,6 +332,13 @@ program
331
332
  .action(async (opts) => {
332
333
  await runShowDns(opts);
333
334
  });
335
+ program
336
+ .command('build-ui')
337
+ .description('Scan frontend components and page definitions, write .mc/component-manifest.json and .mc/page-registry.json')
338
+ .option('--project-root <path>', 'Root of the project (defaults to cwd)')
339
+ .action(async (opts) => {
340
+ await runBuildUi({ projectRoot: opts.projectRoot });
341
+ });
334
342
  program.parseAsync(process.argv).catch((err) => {
335
343
  // eslint-disable-next-line no-console
336
344
  console.error(err);
@@ -1,14 +1,7 @@
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';
1
+ import { writeFile, mkdir, access } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
6
3
  import { cliLogger } from '../utils/logger.js';
7
- import { readScaffoldConfig } from '../utils/scaffold-config.js';
8
- import { fetchModulesJson, fetchModuleTarball } from '../utils/s3-fetch.js';
9
4
  import { PAGE_PATTERNS, listPageTemplate, detailPageTemplate, formPageTemplate, wizardPageTemplate, dashboardPageTemplate, settingsPageTemplate, } from '../templates/patterns/page/index.js';
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const TEMPLATES_DIR = resolve(__dirname, '..', 'templates', 'dashboard-pages');
12
5
  /** Map from pattern name to its template function. */
13
6
  const PATTERN_TEMPLATES = {
14
7
  list: listPageTemplate,
@@ -19,137 +12,59 @@ const PATTERN_TEMPLATES = {
19
12
  settings: settingsPageTemplate,
20
13
  };
21
14
  const KEBAB_REGEX = /^[a-z][a-z0-9-]*$/;
22
- async function loadRegistry() {
23
- // Dynamic import avoids tsx compilation issues with .tsx templates
24
- const mod = await import('../templates/dashboard-pages/index.js');
25
- return [...mod.PAGE_REGISTRY];
26
- }
27
- function getCategoryForPageId(pageId) {
28
- const authPages = ['login', 'signup', 'verify-email', 'forgot-password', 'reset-password', 'mfa-setup', 'mfa-verify', 'choose-org', 'accept-invitation', 'sign-out'];
29
- const accountPages = ['profile', 'workspace-settings', 'members', 'api-keys', 'audit-log'];
30
- if (authPages.includes(pageId))
31
- return 'auth';
32
- if (accountPages.includes(pageId))
33
- return 'account';
34
- return 'shell';
35
- }
36
- async function fetchPageFromS3(scaffoldBucket, scaffoldVersion, pageId) {
37
- try {
38
- const modulesJson = await fetchModulesJson(scaffoldBucket, scaffoldVersion);
39
- const frontendModule = modulesJson.modules.find(m => m.id === 'frontend');
40
- if (!frontendModule) {
41
- throw new Error(`Frontend module not found in modules.json for version ${scaffoldVersion}`);
42
- }
43
- const tarballBuffer = await fetchModuleTarball(scaffoldBucket, scaffoldVersion, frontendModule.tarball, frontendModule.sha256);
44
- // Extract the page file from the tarball
45
- const pageFileName = `${pageId}.tsx`;
46
- const category = getCategoryForPageId(pageId);
47
- const expectedPath = `frontend/src/pages/${category}/${pageFileName}`;
48
- let content = '';
49
- const chunks = [];
50
- await new Promise((resolve, reject) => {
51
- const parser = new TarParser({
52
- onentry: (entry) => {
53
- if (entry.path === expectedPath) {
54
- entry.on('data', (chunk) => chunks.push(chunk));
55
- }
56
- else {
57
- entry.resume();
58
- }
59
- },
60
- });
61
- parser.on('finish', resolve);
62
- parser.on('error', reject);
63
- Readable.from(tarballBuffer).pipe(parser);
64
- });
65
- if (chunks.length === 0) {
66
- throw new Error(`Page template not found at ${expectedPath} in frontend tarball`);
67
- }
68
- content = Buffer.concat(chunks).toString('utf-8');
69
- return content;
70
- }
71
- catch (err) {
72
- cliLogger.warn(err, 'Failed to fetch from S3, falling back to local templates');
73
- return null;
74
- }
75
- }
76
- async function loadFromLocal(pageId, entry) {
77
- const templatePath = join(TEMPLATES_DIR, entry.path);
78
- try {
79
- const content = await readFile(templatePath, 'utf8');
80
- cliLogger.warn({ pageId }, 'Using local fallback template (scaffold version not available)');
81
- return content;
82
- }
83
- catch {
84
- throw new Error(`Template file not found at ${templatePath}`);
85
- }
86
- }
87
15
  export async function runAddPage(opts) {
88
16
  const { pageId, pattern, layout = 'app' } = opts;
89
17
  if (!KEBAB_REGEX.test(pageId)) {
90
18
  throw new Error(`Invalid page id "${pageId}" — must be kebab-case`);
91
19
  }
92
- // ── Pattern mode ──
93
- if (pattern) {
94
- if (!PAGE_PATTERNS.includes(pattern)) {
95
- const available = PAGE_PATTERNS.join(', ');
96
- throw new Error(`Unknown page pattern "${pattern}".\n\nAvailable patterns: ${available}`);
97
- }
98
- const templateFn = PATTERN_TEMPLATES[pattern];
99
- let content = templateFn(pageId);
100
- // Prepend layout comment
101
- const layoutComment = `// Layout: ${layout === 'auth' ? 'AuthLayout' : 'AppLayout'} — import and use in your router config\n\n`;
102
- content = layoutComment + content;
103
- const outDir = opts.outputDir
20
+ // ── .page.json output mode (default when no pattern specified) ──
21
+ if (!pattern) {
22
+ const pageJsonContent = JSON.stringify({
23
+ id: pageId,
24
+ type: 'page',
25
+ route: `/${pageId}`,
26
+ tenancy: layout === 'auth' ? 'none' : 'required',
27
+ title: pageId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
28
+ layout: layout,
29
+ tokens: {},
30
+ regions: [],
31
+ }, null, 2) + '\n';
32
+ const pageJsonOutDir = opts.outputDir
104
33
  ? resolve(opts.outputDir)
105
- : resolve(process.cwd(), 'src', 'pages', pageId);
106
- await mkdir(outDir, { recursive: true });
107
- const fileName = `${pageId}.tsx`;
108
- const outPath = join(outDir, fileName);
109
- // Refuse if target already exists
34
+ : resolve(process.cwd(), 'pages');
35
+ await mkdir(pageJsonOutDir, { recursive: true });
36
+ const pageJsonPath = join(pageJsonOutDir, `${pageId}.page.json`);
110
37
  try {
111
- await access(outPath);
112
- throw new Error(`File already exists at ${outPath}. Remove it first or use a different --output directory.`);
38
+ await access(pageJsonPath);
39
+ throw new Error(`File already exists at ${pageJsonPath}. Remove it first or use a different --output directory.`);
113
40
  }
114
41
  catch (err) {
115
42
  if (err.code !== 'ENOENT')
116
43
  throw err;
117
44
  }
118
- await writeFile(outPath, content, 'utf8');
119
- cliLogger.info({ pageId, pattern, layout, outPath }, 'Page scaffolded from pattern');
45
+ await writeFile(pageJsonPath, pageJsonContent, 'utf8');
46
+ cliLogger.info({ pageId, layout, outPath: pageJsonPath }, 'Page .page.json scaffolded');
120
47
  // eslint-disable-next-line no-console
121
- console.log(`\n✓ Page "${pageId}" (pattern: ${pattern}, layout: ${layout}) added at ${outPath}\n` +
122
- ` Next: add a route in src/router.tsx for this page\n`);
48
+ console.log(`\n✓ Page "${pageId}" (layout: ${layout}) added at ${pageJsonPath}\n` +
49
+ ` Next: add the page in the page builder UI or edit the .page.json directly.\n` +
50
+ ` To add a route, update src/router.tsx.tpl.\n`);
123
51
  return;
124
52
  }
125
- // ── Registry mode (existing behaviour) ──
126
- const registry = await loadRegistry();
127
- const entry = registry.find(p => p.id === pageId);
128
- if (!entry) {
129
- const available = registry.map(p => p.id).join(', ');
130
- throw new Error(`Unknown page id "${pageId}".\n\nAvailable pages: ${available}\n` +
131
- `Tip: use --pattern <pattern> to scaffold with a page pattern. Available patterns: ${PAGE_PATTERNS.join(', ')}`);
132
- }
133
- let content = null;
134
- // Try S3 fetch first
135
- try {
136
- const config = await readScaffoldConfig(process.cwd());
137
- if (config.scaffoldVersion && config.scaffoldBucket) {
138
- content = await fetchPageFromS3(config.scaffoldBucket, config.scaffoldVersion, pageId);
139
- }
140
- }
141
- catch (err) {
142
- cliLogger.debug(err, 'Could not read scaffold config');
143
- }
144
- // Fall back to local templates
145
- if (!content) {
146
- content = await loadFromLocal(pageId, entry);
53
+ // ── Pattern mode ──
54
+ if (!PAGE_PATTERNS.includes(pattern)) {
55
+ const available = PAGE_PATTERNS.join(', ');
56
+ throw new Error(`Unknown page pattern "${pattern}".\n\nAvailable patterns: ${available}`);
147
57
  }
58
+ const templateFn = PATTERN_TEMPLATES[pattern];
59
+ let content = templateFn(pageId);
60
+ // Prepend layout comment
61
+ const layoutComment = `// Layout: ${layout === 'auth' ? 'AuthLayout' : 'AppLayout'} — import and use in your router config\n\n`;
62
+ content = layoutComment + content;
148
63
  const outDir = opts.outputDir
149
64
  ? resolve(opts.outputDir)
150
- : resolve(process.cwd(), 'src', 'pages', entry.category);
65
+ : resolve(process.cwd(), 'src', 'pages', pageId);
151
66
  await mkdir(outDir, { recursive: true });
152
- const fileName = `${basename(entry.path)}`;
67
+ const fileName = `${pageId}.tsx`;
153
68
  const outPath = join(outDir, fileName);
154
69
  // Refuse if target already exists
155
70
  try {
@@ -161,10 +76,8 @@ export async function runAddPage(opts) {
161
76
  throw err;
162
77
  }
163
78
  await writeFile(outPath, content, 'utf8');
164
- cliLogger.info({ pageId, layout, outPath }, 'Page scaffolded');
79
+ cliLogger.info({ pageId, pattern, layout, outPath }, 'Page scaffolded from pattern');
165
80
  // eslint-disable-next-line no-console
166
- console.log(`\n✓ Page "${pageId}" (layout: ${layout}) added at ${outPath}\n` +
167
- ` Component: ${entry.name}\n` +
168
- ` Next: import { ${entry.name} } from './${entry.category}/${basename(entry.path, '.tsx')}'\n` +
169
- ` Then add a route in src/router.tsx\n`);
81
+ console.log(`\n✓ Page "${pageId}" (pattern: ${pattern}, layout: ${layout}) added at ${outPath}\n` +
82
+ ` Next: add a route in src/router.tsx for this page\n`);
170
83
  }
@@ -0,0 +1,12 @@
1
+ export interface ComponentManifestEntry {
2
+ id: string;
3
+ displayName: string;
4
+ filePath: string;
5
+ propsSchema: Record<string, unknown>;
6
+ fixture?: unknown;
7
+ category: 'ui' | 'custom';
8
+ }
9
+ export declare function scanComponents(opts: {
10
+ uiDir: string;
11
+ customDir: string;
12
+ }): Promise<ComponentManifestEntry[]>;
@@ -0,0 +1,50 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ function kebabToPascal(kebab) {
4
+ return kebab
5
+ .split('-')
6
+ .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
7
+ .join('');
8
+ }
9
+ async function scanDir(dir, category) {
10
+ const entries = [];
11
+ let files;
12
+ try {
13
+ files = await readdir(dir);
14
+ }
15
+ catch (err) {
16
+ if (err.code === 'ENOENT') {
17
+ return entries;
18
+ }
19
+ throw err;
20
+ }
21
+ for (const file of files) {
22
+ if (file.endsWith('.tsx')) {
23
+ const id = file.slice(0, -4);
24
+ const displayName = kebabToPascal(id);
25
+ const filePath = join(dir, file);
26
+ const entry = {
27
+ id,
28
+ displayName,
29
+ filePath,
30
+ propsSchema: {},
31
+ category,
32
+ };
33
+ const fixtureFilePath = join(dir, `${id}.fixture.json`);
34
+ try {
35
+ const fixtureContent = await readFile(fixtureFilePath, 'utf8');
36
+ entry.fixture = JSON.parse(fixtureContent);
37
+ }
38
+ catch (err) {
39
+ // If fixture doesn't exist or can't be parsed, omit the field
40
+ }
41
+ entries.push(entry);
42
+ }
43
+ }
44
+ return entries;
45
+ }
46
+ export async function scanComponents(opts) {
47
+ const uiEntries = await scanDir(opts.uiDir, 'ui');
48
+ const customEntries = await scanDir(opts.customDir, 'custom');
49
+ return [...uiEntries, ...customEntries];
50
+ }
@@ -0,0 +1,13 @@
1
+ export interface PageRegistryEntry {
2
+ id: string;
3
+ route: string | null;
4
+ tenancy: string | null;
5
+ title: string | null;
6
+ tier: 1 | 2 | 3;
7
+ filePath: string;
8
+ }
9
+ export declare function indexPages(opts: {
10
+ pagesDir: string;
11
+ customDir: string;
12
+ frontendPagesDir: string;
13
+ }): Promise<PageRegistryEntry[]>;
@@ -0,0 +1,107 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ async function readPageJsonDir(dir) {
4
+ const entries = [];
5
+ let files;
6
+ try {
7
+ files = await readdir(dir);
8
+ }
9
+ catch (err) {
10
+ if (err.code === 'ENOENT') {
11
+ return entries;
12
+ }
13
+ throw err;
14
+ }
15
+ for (const file of files) {
16
+ if (file.endsWith('.page.json')) {
17
+ const filePath = join(dir, file);
18
+ try {
19
+ const content = await readFile(filePath, 'utf8');
20
+ const pageData = JSON.parse(content);
21
+ entries.push({
22
+ id: pageData.id,
23
+ route: pageData.route ?? null,
24
+ tenancy: pageData.tenancy ?? null,
25
+ title: pageData.title ?? null,
26
+ tier: 1,
27
+ filePath,
28
+ });
29
+ }
30
+ catch (err) {
31
+ // Skip malformed JSON files
32
+ }
33
+ }
34
+ }
35
+ return entries;
36
+ }
37
+ async function readCustomComponentsDir(dir) {
38
+ const entries = [];
39
+ let files;
40
+ try {
41
+ files = await readdir(dir);
42
+ }
43
+ catch (err) {
44
+ if (err.code === 'ENOENT') {
45
+ return entries;
46
+ }
47
+ throw err;
48
+ }
49
+ for (const file of files) {
50
+ if (file.endsWith('.tsx')) {
51
+ const id = file.slice(0, -4);
52
+ const filePath = join(dir, file);
53
+ entries.push({
54
+ id,
55
+ route: null,
56
+ tenancy: null,
57
+ title: null,
58
+ tier: 2,
59
+ filePath,
60
+ });
61
+ }
62
+ }
63
+ return entries;
64
+ }
65
+ async function walkFrontendPages(dir, baseDir) {
66
+ const entries = [];
67
+ let items;
68
+ try {
69
+ items = await readdir(dir, { withFileTypes: true });
70
+ }
71
+ catch (err) {
72
+ if (err.code === 'ENOENT') {
73
+ return entries;
74
+ }
75
+ throw err;
76
+ }
77
+ for (const item of items) {
78
+ // Skip directories named _errors and shell
79
+ if (item.isDirectory() && (item.name === '_errors' || item.name === 'shell')) {
80
+ continue;
81
+ }
82
+ const itemPath = join(dir, item.name);
83
+ if (item.isDirectory()) {
84
+ const subEntries = await walkFrontendPages(itemPath, baseDir);
85
+ entries.push(...subEntries);
86
+ }
87
+ else if (item.name.endsWith('.tsx')) {
88
+ const relPath = relative(baseDir, itemPath);
89
+ const id = relPath.slice(0, -4);
90
+ entries.push({
91
+ id,
92
+ route: null,
93
+ tenancy: null,
94
+ title: null,
95
+ tier: 3,
96
+ filePath: itemPath,
97
+ });
98
+ }
99
+ }
100
+ return entries;
101
+ }
102
+ export async function indexPages(opts) {
103
+ const tier1 = await readPageJsonDir(opts.pagesDir);
104
+ const tier2 = await readCustomComponentsDir(opts.customDir);
105
+ const tier3 = await walkFrontendPages(opts.frontendPagesDir, opts.frontendPagesDir);
106
+ return [...tier1, ...tier2, ...tier3];
107
+ }
@@ -0,0 +1,3 @@
1
+ export declare function runBuildUi(opts: {
2
+ projectRoot?: string;
3
+ }): Promise<void>;
@@ -0,0 +1,23 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { scanComponents } from './build-ui/component-scanner.js';
4
+ import { indexPages } from './build-ui/page-indexer.js';
5
+ export async function runBuildUi(opts) {
6
+ const projectRoot = opts.projectRoot ?? process.cwd();
7
+ const components = await scanComponents({
8
+ uiDir: join(projectRoot, 'frontend/src/components/ui'),
9
+ customDir: join(projectRoot, 'frontend/src/components/custom'),
10
+ });
11
+ const componentManifestPath = join(projectRoot, '.mc/component-manifest.json');
12
+ await mkdir(join(componentManifestPath, '..'), { recursive: true });
13
+ await writeFile(componentManifestPath, JSON.stringify(components, null, 2), 'utf8');
14
+ const pages = await indexPages({
15
+ pagesDir: join(projectRoot, 'pages'),
16
+ customDir: join(projectRoot, 'frontend/src/components/custom'),
17
+ frontendPagesDir: join(projectRoot, 'frontend/src/pages'),
18
+ });
19
+ const pageRegistryPath = join(projectRoot, '.mc/page-manifest.json');
20
+ await mkdir(join(pageRegistryPath, '..'), { recursive: true });
21
+ await writeFile(pageRegistryPath, JSON.stringify(pages, null, 2), 'utf8');
22
+ process.stderr.write(`[build-ui] Wrote ${components.length} components and ${pages.length} pages.\n`);
23
+ }
@@ -19,7 +19,7 @@ function camelCase(s) {
19
19
  export function apiSkeletonTemplate(domainId, apiId, tenancy) {
20
20
  const tenantPath = tenancy === 'required' ? '/v1/tenants/{tenantId}' : '/v1';
21
21
  return `import { z } from 'zod';
22
- import { defineApi } from '../../../lib/domain-runtime';
22
+ import { defineApi } from '@mettlecast/domain-runtime';
23
23
 
24
24
  export const ${camelCase(apiId)} = defineApi({
25
25
  id: '${apiId}',
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export function eventsSkeletonTemplate(domainId) {
10
10
  return `import { z } from 'zod';
11
- import { defineEvent } from '../../../lib/domain-runtime';
11
+ import { defineEvent } from '@mettlecast/domain-runtime';
12
12
 
13
13
  export const exampleEvent = defineEvent({
14
14
  id: '${domainId}.example',
@@ -9,7 +9,7 @@
9
9
  * @returns TypeScript source code as a string.
10
10
  */
11
11
  export function subscriberSkeletonTemplate(domainId, subscriberId, eventId) {
12
- return `import { defineSubscriber } from '../../../lib/domain-runtime';
12
+ return `import { defineSubscriber } from '@mettlecast/domain-runtime';
13
13
 
14
14
  export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = defineSubscriber({
15
15
  id: '${domainId}.${subscriberId}',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mettlecast/domain-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org",
package/src/cli.ts CHANGED
@@ -30,6 +30,7 @@ import { runPowerTune } from './commands/power-tune.js';
30
30
  import { runReseedPage } from './commands/reseed-page.js';
31
31
  import { runAddSeedPage } from './commands/add-seed-page.js';
32
32
  import { runShowDns } from './commands/show-dns.js';
33
+ import { runBuildUi } from './commands/build-ui.js';
33
34
 
34
35
  /**
35
36
  * Root CLI program for the TIB Domain Module local developer toolchain.
@@ -198,7 +199,7 @@ program
198
199
 
199
200
  program
200
201
  .command('add-page <pageId>')
201
- .description('Scaffold a seed page from the dashboard-pages template registry')
202
+ .description('Scaffold a .page.json definition (default) or a pattern-based .tsx page component (--pattern)')
202
203
  .option('--output <dir>', 'Override the output directory (default: src/pages/<category>)')
203
204
  .option('--pattern <pattern>', 'Scaffold with a page pattern template (list, detail, form, wizard, dashboard, settings)')
204
205
  .option('--layout <layout>', 'Layout to use: app (default) or auth', 'app')
@@ -380,6 +381,14 @@ program
380
381
  await runShowDns(opts);
381
382
  });
382
383
 
384
+ program
385
+ .command('build-ui')
386
+ .description('Scan frontend components and page definitions, write .mc/component-manifest.json and .mc/page-registry.json')
387
+ .option('--project-root <path>', 'Root of the project (defaults to cwd)')
388
+ .action(async (opts: { projectRoot?: string }) => {
389
+ await runBuildUi({ projectRoot: opts.projectRoot });
390
+ });
391
+
383
392
  program.parseAsync(process.argv).catch((err: unknown) => {
384
393
  // eslint-disable-next-line no-console
385
394
  console.error(err);