@mettlecast/domain-cli 0.2.61 → 0.2.63

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.
@@ -0,0 +1,89 @@
1
+ import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { cliLogger } from '../utils/logger.js';
5
+
6
+ /**
7
+ * Options for the regenerate-modules-hashes command.
8
+ */
9
+ export interface RegenerateModulesHashesOptions {
10
+ /** Root directory of the project (defaults to cwd). */
11
+ projectRoot?: string;
12
+ }
13
+
14
+ /**
15
+ * Manifest of infra/modules/ files with their SHA256 hashes.
16
+ */
17
+ export interface ModulesHashesManifest {
18
+ /** Schema version. */
19
+ version: '1';
20
+ /** ISO timestamp when the manifest was generated. */
21
+ generatedAt: string;
22
+ /** Map of relative file path to SHA256 hex digest. */
23
+ files: Record<string, string>;
24
+ }
25
+
26
+ /**
27
+ * Walk infra/modules/, compute a SHA256 for every file, and write the result
28
+ * to .mc/modules-hashes.json. Used to refresh the scaffold drift baseline.
29
+ * @param opts Options including project root directory.
30
+ * @returns Absolute path to the written manifest file.
31
+ */
32
+ export async function runRegenerateModulesHashes(
33
+ opts: RegenerateModulesHashesOptions = {},
34
+ ): Promise<string> {
35
+ const root = opts.projectRoot ?? process.cwd();
36
+ const mcDir = join(root, '.mc');
37
+ const manifestPath = join(mcDir, 'modules-hashes.json');
38
+ const modulesDir = join(root, 'infra', 'modules');
39
+
40
+ // Files/directories to skip when walking infra/modules/
41
+ const SKIP_NAMES = new Set(['node_modules', 'dist', 'cdk.out', 'package-lock.json', '.npmrc']);
42
+
43
+ /** Normalize CRLF → LF so hashes match across platforms (Windows vs Linux). */
44
+ function normalizeLineEndings(buf: Buffer): Buffer {
45
+ const str = buf.toString('utf8');
46
+ if (!str.includes('\r\n')) return buf;
47
+ return Buffer.from(str.replace(/\r\n/g, '\n'), 'utf8');
48
+ }
49
+
50
+ // Walk infra/modules/ and collect every file path
51
+ async function walk(dir: string): Promise<string[]> {
52
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
53
+ const files: string[] = [];
54
+ for (const entry of entries) {
55
+ if (SKIP_NAMES.has(entry.name)) continue;
56
+ const full = join(dir, entry.name);
57
+ if (entry.isDirectory()) files.push(...await walk(full));
58
+ else if (entry.isFile()) files.push(full);
59
+ }
60
+ return files;
61
+ }
62
+
63
+ const onDiskFiles = await walk(modulesDir);
64
+
65
+ // Compute SHA256 for each file (line-ending-normalised)
66
+ const files: Record<string, string> = {};
67
+ for (const absPath of onDiskFiles) {
68
+ const relPath = relative(root, absPath).replace(/\\/g, '/');
69
+ const content = await readFile(absPath);
70
+ const normalized = normalizeLineEndings(content);
71
+ files[relPath] = createHash('sha256').update(normalized).digest('hex');
72
+ }
73
+
74
+ const manifest: ModulesHashesManifest = {
75
+ version: '1',
76
+ generatedAt: new Date().toISOString(),
77
+ files,
78
+ };
79
+
80
+ await mkdir(mcDir, { recursive: true });
81
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
82
+
83
+ cliLogger.info(
84
+ { path: manifestPath, fileCount: Object.keys(files).length },
85
+ 'modules-hashes manifest regenerated',
86
+ );
87
+
88
+ return manifestPath;
89
+ }
@@ -0,0 +1,79 @@
1
+ import { join } from 'node:path';
2
+ import { cliLogger } from '../utils/logger.js';
3
+ import { readScaffoldConfig } from '../utils/scaffold-config.js';
4
+ import { runBuild } from './build.js';
5
+ import { runBuildCatalog } from './build-catalog.js';
6
+ import { runBuildFlows } from './build-flows.js';
7
+ import { runBuildUi } from './build-ui.js';
8
+ import { runRegenerateModulesHashes } from './regenerate-modules-hashes.js';
9
+ import { runDoctor } from './doctor.js';
10
+
11
+ /**
12
+ * Run the full scaffold update pipeline:
13
+ * 1. Build each domain registry from `.mc/scaffold-config.json`
14
+ * 2. Build the combined domain catalog
15
+ * 3. Build flows registry
16
+ * 4. Build UI manifests
17
+ * 5. Regenerate module hashes
18
+ * 6. Run doctor
19
+ *
20
+ * Per-domain build failures are logged and skipped so that one bad domain does
21
+ * not abort the whole pipeline. The remaining steps still run.
22
+ *
23
+ * @param opts - Optional projectRoot override. Defaults to cwd.
24
+ * @returns success derived from doctor pass/fail and a human-readable summary.
25
+ */
26
+ export async function runUpdateAll(opts?: { projectRoot?: string }): Promise<{ success: boolean; summary: string }> {
27
+ const projectRoot = opts?.projectRoot ?? process.cwd();
28
+
29
+ cliLogger.info({ projectRoot }, 'update-all: starting');
30
+
31
+ // 1. Read scaffold-config to discover domains
32
+ const scaffoldConfig = await readScaffoldConfig(projectRoot);
33
+ const domainIds = scaffoldConfig.domainIds ?? [];
34
+
35
+ cliLogger.info({ domainCount: domainIds.length }, 'update-all: building domain registries');
36
+
37
+ // 2. Build each domain. Per-domain failures are logged and skipped.
38
+ for (const domainId of domainIds) {
39
+ const domainRoot = join(projectRoot, 'domains', domainId);
40
+ try {
41
+ cliLogger.info({ domainId, domainRoot }, 'update-all: building domain');
42
+ await runBuild({ domainRoot });
43
+ } catch (err) {
44
+ cliLogger.error(
45
+ { domainId, err: err instanceof Error ? err.message : String(err) },
46
+ 'update-all: domain build failed — continuing',
47
+ );
48
+ }
49
+ }
50
+
51
+ // 3. Build catalog from .mc
52
+ const mcDir = join(projectRoot, '.mc');
53
+ cliLogger.info({ mcDir }, 'update-all: building domain catalog');
54
+ await runBuildCatalog(mcDir);
55
+
56
+ // 4. Build flows
57
+ cliLogger.info({ projectRoot }, 'update-all: building flows registry');
58
+ await runBuildFlows({ projectRoot });
59
+
60
+ // 5. Build UI manifests
61
+ cliLogger.info({ projectRoot }, 'update-all: building UI manifests');
62
+ await runBuildUi({ projectRoot });
63
+
64
+ // 6. Regenerate module hashes
65
+ cliLogger.info({ projectRoot }, 'update-all: regenerating module hashes');
66
+ await runRegenerateModulesHashes({ projectRoot });
67
+
68
+ // 7. Run doctor — final gate
69
+ cliLogger.info({ projectRoot }, 'update-all: running doctor');
70
+ const report = await runDoctor({ projectRoot });
71
+
72
+ cliLogger.info(
73
+ { pass: report.pass, exitCode: report.exitCode, checkCount: report.checks.length },
74
+ 'update-all: doctor complete',
75
+ );
76
+
77
+ const summary = `Updated ${domainIds.length} domains; doctor ${report.pass ? 'PASS' : 'FAIL'}`;
78
+ return { success: report.pass, summary };
79
+ }
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile, mkdir } from 'fs/promises';
1
+ import { readFile, writeFile, mkdir, access } from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { cliLogger } from './logger.js';
4
4
 
@@ -179,3 +179,18 @@ export function isScaffoldManaged(manifest: TibManifest, filePath: string): bool
179
179
  // Returns true only when the file is in the manifest with policy 'managed'
180
180
  return entry != null && entry.policy === 'managed';
181
181
  }
182
+
183
+ export async function pruneManifest(projectRoot: string, manifest: TibManifest): Promise<number> {
184
+ let pruned = 0;
185
+ const survivors: ManifestFileEntry[] = [];
186
+ for (const entry of manifest.files) {
187
+ try {
188
+ await access(path.join(projectRoot, entry.path));
189
+ survivors.push(entry);
190
+ } catch {
191
+ pruned++;
192
+ }
193
+ }
194
+ manifest.files = survivors;
195
+ return pruned;
196
+ }