@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,89 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { injectHeader } from '../../utils/header-inject.js';
|
|
3
|
+
|
|
4
|
+
const MODULE = 'backend-lambda';
|
|
5
|
+
const VERSION = '1.0.0';
|
|
6
|
+
|
|
7
|
+
describe('injectHeader — TypeScript files', () => {
|
|
8
|
+
it('prepends // @mc-scaffold header to .ts file', () => {
|
|
9
|
+
const result = injectHeader('const x = 1;\n', 'app.ts', MODULE, VERSION);
|
|
10
|
+
expect(result).toMatch(/^\/\/ @mc-scaffold: backend-lambda@1\.0\.0\n/);
|
|
11
|
+
expect(result).toContain('const x = 1;');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('prepends header to .tsx file', () => {
|
|
15
|
+
const result = injectHeader('export default function App() {}\n', 'App.tsx', MODULE, VERSION);
|
|
16
|
+
expect(result.startsWith('// @mc-scaffold:')).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('preserves shebang on first line', () => {
|
|
20
|
+
const content = '#!/usr/bin/env node\nconsole.log("hi");\n';
|
|
21
|
+
const result = injectHeader(content, 'cli.ts', MODULE, VERSION);
|
|
22
|
+
expect(result.startsWith('#!/usr/bin/env node\n')).toBe(true);
|
|
23
|
+
expect(result).toContain('// @mc-scaffold:');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('injectHeader — YAML files', () => {
|
|
28
|
+
it('prepends # @mc-scaffold header to .yml file', () => {
|
|
29
|
+
const result = injectHeader('name: CI\n', 'ci.yml', MODULE, VERSION);
|
|
30
|
+
expect(result).toMatch(/^# @mc-scaffold: backend-lambda@1\.0\.0\n/);
|
|
31
|
+
expect(result).toContain('name: CI');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('prepends header to .yaml file', () => {
|
|
35
|
+
const result = injectHeader('key: value\n', 'config.yaml', MODULE, VERSION);
|
|
36
|
+
expect(result.startsWith('# @mc-scaffold:')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('injectHeader — JSON files', () => {
|
|
41
|
+
it('injects _tibScaffold key as first key in JSON', () => {
|
|
42
|
+
const result = injectHeader('{"name": "test", "version": "1.0.0"}', 'config.json', MODULE, VERSION);
|
|
43
|
+
const parsed = JSON.parse(result) as Record<string, unknown>;
|
|
44
|
+
expect(Object.keys(parsed)[0]).toBe('_tibScaffold');
|
|
45
|
+
expect(parsed._tibScaffold).toBe('backend-lambda@1.0.0');
|
|
46
|
+
expect(parsed.name).toBe('test');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('skips _tibScaffold injection for package.json', () => {
|
|
50
|
+
const content = '{"name": "test"}';
|
|
51
|
+
const result = injectHeader(content, 'package.json', MODULE, VERSION);
|
|
52
|
+
expect(result).toBe(content);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('skips _tibScaffold injection for tsconfig.json', () => {
|
|
56
|
+
const content = '{"compilerOptions": {}}';
|
|
57
|
+
const result = injectHeader(content, 'tsconfig.json', MODULE, VERSION);
|
|
58
|
+
expect(result).toBe(content);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('skips _tibScaffold injection for .fixture.json', () => {
|
|
62
|
+
const content = '{"event": {}}';
|
|
63
|
+
const result = injectHeader(content, 'me.fixture.json', MODULE, VERSION);
|
|
64
|
+
expect(result).toBe(content);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('injectHeader — CSS files', () => {
|
|
69
|
+
it('prepends /* @mc-scaffold */ header to .css file', () => {
|
|
70
|
+
const result = injectHeader('@tailwind base;\n', 'index.css', MODULE, VERSION);
|
|
71
|
+
expect(result).toMatch(/^\/\* @mc-scaffold: backend-lambda@1\.0\.0 \*\//);
|
|
72
|
+
expect(result).toContain('@tailwind base;');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('injectHeader — HTML files', () => {
|
|
77
|
+
it('prepends <!-- @mc-scaffold --> header to .html file', () => {
|
|
78
|
+
const result = injectHeader('<!doctype html>\n', 'index.html', MODULE, VERSION);
|
|
79
|
+
expect(result).toMatch(/^<!-- @mc-scaffold: backend-lambda@1\.0\.0 -->/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('injectHeader — Markdown files', () => {
|
|
84
|
+
it('returns markdown unchanged', () => {
|
|
85
|
+
const content = '# README\n\nSome content.\n';
|
|
86
|
+
const result = injectHeader(content, 'README.md', MODULE, VERSION);
|
|
87
|
+
expect(result).toBe(content);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, rmdir, writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { installScaffoldFile, type FilePolicy } from '../../utils/install-file.js';
|
|
6
|
+
import { computeChecksumString } from '../../utils/checksum.js';
|
|
7
|
+
import type { ManifestFileEntry } from '../../utils/manifest.js';
|
|
8
|
+
|
|
9
|
+
describe('installScaffoldFile', () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'tib-install-test-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
try {
|
|
18
|
+
await rmdir(tmpDir, { recursive: true });
|
|
19
|
+
} catch {
|
|
20
|
+
// Ignore cleanup errors
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('seed policy', () => {
|
|
25
|
+
it('skips write if file already exists', async () => {
|
|
26
|
+
const filePath = join(tmpDir, 'test-seed.txt');
|
|
27
|
+
const existingContent = 'existing content';
|
|
28
|
+
const newContent = 'new content';
|
|
29
|
+
|
|
30
|
+
await writeFile(filePath, existingContent, 'utf-8');
|
|
31
|
+
|
|
32
|
+
const result = await installScaffoldFile(filePath, newContent, 'seed', undefined);
|
|
33
|
+
|
|
34
|
+
expect(result.status).toBe('skipped');
|
|
35
|
+
|
|
36
|
+
// File should not be modified
|
|
37
|
+
const content = await readFile(filePath, 'utf-8');
|
|
38
|
+
expect(content).toBe(existingContent);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('writes file if not exists', async () => {
|
|
42
|
+
const filePath = join(tmpDir, 'test-seed-new.txt');
|
|
43
|
+
const content = 'new content';
|
|
44
|
+
|
|
45
|
+
const result = await installScaffoldFile(filePath, content, 'seed', undefined);
|
|
46
|
+
|
|
47
|
+
expect(result.status).toBe('added');
|
|
48
|
+
|
|
49
|
+
// File should be created with new content
|
|
50
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
51
|
+
expect(fileContent).toBe(content);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('owned policy', () => {
|
|
56
|
+
it('overwrites existing file with updated status', async () => {
|
|
57
|
+
const filePath = join(tmpDir, 'test-owned.txt');
|
|
58
|
+
const oldContent = 'old content';
|
|
59
|
+
const newContent = 'new content';
|
|
60
|
+
|
|
61
|
+
await writeFile(filePath, oldContent, 'utf-8');
|
|
62
|
+
|
|
63
|
+
const result = await installScaffoldFile(filePath, newContent, 'owned', undefined);
|
|
64
|
+
|
|
65
|
+
expect(result.status).toBe('updated');
|
|
66
|
+
|
|
67
|
+
// File should be updated
|
|
68
|
+
const content = await readFile(filePath, 'utf-8');
|
|
69
|
+
expect(content).toBe(newContent);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('writes new file with added status', async () => {
|
|
73
|
+
const filePath = join(tmpDir, 'test-owned-new.txt');
|
|
74
|
+
const content = 'new content';
|
|
75
|
+
|
|
76
|
+
const result = await installScaffoldFile(filePath, content, 'owned', undefined);
|
|
77
|
+
|
|
78
|
+
expect(result.status).toBe('added');
|
|
79
|
+
|
|
80
|
+
// File should be created
|
|
81
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
82
|
+
expect(fileContent).toBe(content);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('tracked policy', () => {
|
|
87
|
+
it('returns unchanged when checksum matches', async () => {
|
|
88
|
+
const filePath = join(tmpDir, 'test-tracked.txt');
|
|
89
|
+
const content = 'content';
|
|
90
|
+
const checksum = computeChecksumString(content);
|
|
91
|
+
|
|
92
|
+
await writeFile(filePath, content, 'utf-8');
|
|
93
|
+
|
|
94
|
+
const currentEntry: ManifestFileEntry = {
|
|
95
|
+
path: 'test-tracked.txt',
|
|
96
|
+
module: 'core',
|
|
97
|
+
moduleVersion: '1.0.0',
|
|
98
|
+
sha256: checksum,
|
|
99
|
+
wasTemplate: false,
|
|
100
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
101
|
+
policy: 'tracked',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = await installScaffoldFile(filePath, content, 'tracked', currentEntry);
|
|
105
|
+
|
|
106
|
+
expect(result.status).toBe('unchanged');
|
|
107
|
+
|
|
108
|
+
// File should not be modified
|
|
109
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
110
|
+
expect(fileContent).toBe(content);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('overwrites when no drift and sha256 differs', async () => {
|
|
114
|
+
const filePath = join(tmpDir, 'test-tracked-update.txt');
|
|
115
|
+
const oldContent = 'old content';
|
|
116
|
+
const newContent = 'new content';
|
|
117
|
+
const oldChecksum = computeChecksumString(oldContent);
|
|
118
|
+
|
|
119
|
+
await writeFile(filePath, oldContent, 'utf-8');
|
|
120
|
+
|
|
121
|
+
const currentEntry: ManifestFileEntry = {
|
|
122
|
+
path: 'test-tracked-update.txt',
|
|
123
|
+
module: 'core',
|
|
124
|
+
moduleVersion: '1.0.0',
|
|
125
|
+
sha256: oldChecksum,
|
|
126
|
+
wasTemplate: false,
|
|
127
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
128
|
+
policy: 'tracked',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const result = await installScaffoldFile(filePath, newContent, 'tracked', currentEntry);
|
|
132
|
+
|
|
133
|
+
expect(result.status).toBe('updated');
|
|
134
|
+
|
|
135
|
+
// File should be updated
|
|
136
|
+
const content = await readFile(filePath, 'utf-8');
|
|
137
|
+
expect(content).toBe(newContent);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('writes conflict file on drift', async () => {
|
|
141
|
+
const filePath = join(tmpDir, 'test-tracked-drift.txt');
|
|
142
|
+
const manifestContent = 'original content';
|
|
143
|
+
const driftedContent = 'user modified content';
|
|
144
|
+
const newContent = 'new content from upgrade';
|
|
145
|
+
const manifestChecksum = computeChecksumString(manifestContent);
|
|
146
|
+
|
|
147
|
+
// Create file with drifted content
|
|
148
|
+
await writeFile(filePath, driftedContent, 'utf-8');
|
|
149
|
+
|
|
150
|
+
const currentEntry: ManifestFileEntry = {
|
|
151
|
+
path: 'test-tracked-drift.txt',
|
|
152
|
+
module: 'core',
|
|
153
|
+
moduleVersion: '1.0.0',
|
|
154
|
+
sha256: manifestChecksum,
|
|
155
|
+
wasTemplate: false,
|
|
156
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
157
|
+
policy: 'tracked',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const result = await installScaffoldFile(filePath, newContent, 'tracked', currentEntry);
|
|
161
|
+
|
|
162
|
+
expect(result.status).toBe('conflict');
|
|
163
|
+
|
|
164
|
+
// Original file should not be modified
|
|
165
|
+
const originalContent = await readFile(filePath, 'utf-8');
|
|
166
|
+
expect(originalContent).toBe(driftedContent);
|
|
167
|
+
|
|
168
|
+
// .tib-upgrade file should be created
|
|
169
|
+
const conflictFile = await readFile(`${filePath}.tib-upgrade`, 'utf-8');
|
|
170
|
+
expect(conflictFile).toBe(newContent);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('adds new tracked file when no currentEntry', async () => {
|
|
174
|
+
const filePath = join(tmpDir, 'test-tracked-new.txt');
|
|
175
|
+
const content = 'new tracked file';
|
|
176
|
+
|
|
177
|
+
const result = await installScaffoldFile(filePath, content, 'tracked', undefined);
|
|
178
|
+
|
|
179
|
+
expect(result.status).toBe('added');
|
|
180
|
+
|
|
181
|
+
// File should be created
|
|
182
|
+
const fileContent = await readFile(filePath, 'utf-8');
|
|
183
|
+
expect(fileContent).toBe(content);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('dry-run mode', () => {
|
|
188
|
+
it('does not write files when dryRun is true', async () => {
|
|
189
|
+
const filePath = join(tmpDir, 'test-dryrun.txt');
|
|
190
|
+
const content = 'new content';
|
|
191
|
+
|
|
192
|
+
const result = await installScaffoldFile(filePath, content, 'owned', undefined, { dryRun: true });
|
|
193
|
+
|
|
194
|
+
expect(result.status).toBe('added');
|
|
195
|
+
|
|
196
|
+
// File should not be created
|
|
197
|
+
let exists = false;
|
|
198
|
+
try {
|
|
199
|
+
await readFile(filePath, 'utf-8');
|
|
200
|
+
exists = true;
|
|
201
|
+
} catch {
|
|
202
|
+
exists = false;
|
|
203
|
+
}
|
|
204
|
+
expect(exists).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, rmdir, writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
7
|
+
cliLogger: {
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
error: vi.fn(),
|
|
10
|
+
warn: vi.fn(),
|
|
11
|
+
debug: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createManifest,
|
|
17
|
+
readManifest,
|
|
18
|
+
writeManifest,
|
|
19
|
+
upsertManifestFile,
|
|
20
|
+
inferPolicyFromPath,
|
|
21
|
+
getManifestFile,
|
|
22
|
+
type ManifestFileEntry,
|
|
23
|
+
type TibManifest,
|
|
24
|
+
} from '../../utils/manifest.js';
|
|
25
|
+
|
|
26
|
+
describe('manifest utilities', () => {
|
|
27
|
+
let tmpDir: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'tib-manifest-test-'));
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
try {
|
|
36
|
+
await rmdir(tmpDir, { recursive: true });
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore cleanup errors
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('createManifest', () => {
|
|
43
|
+
it('produces manifest with v2 schema', () => {
|
|
44
|
+
const manifest = createManifest('1.0.0', 'test-project', 'eu-north-1', ['core']);
|
|
45
|
+
expect(manifest.$schema).toBe('https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json');
|
|
46
|
+
expect(manifest.scaffoldVersion).toBe('1.0.0');
|
|
47
|
+
expect(manifest.projectName).toBe('test-project');
|
|
48
|
+
expect(manifest.awsRegion).toBe('eu-north-1');
|
|
49
|
+
expect(manifest.enabledModules).toEqual(['core']);
|
|
50
|
+
expect(manifest.files).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('inferPolicyFromPath', () => {
|
|
55
|
+
it('returns owned for infra/ paths', () => {
|
|
56
|
+
expect(inferPolicyFromPath('infra/modules/app.ts')).toBe('owned');
|
|
57
|
+
expect(inferPolicyFromPath('infra/main.tf')).toBe('owned');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns owned for .mc/infra/ paths', () => {
|
|
61
|
+
expect(inferPolicyFromPath('.mc/infra/stack.ts')).toBe('owned');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns owned for mc-deploy.yml', () => {
|
|
65
|
+
expect(inferPolicyFromPath('mc-deploy.yml')).toBe('owned');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns owned for .github/workflows/ paths', () => {
|
|
69
|
+
expect(inferPolicyFromPath('.github/workflows/test.yml')).toBe('owned');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns seed for frontend seed paths', () => {
|
|
73
|
+
expect(inferPolicyFromPath('frontend/src/pages/auth/login.tsx')).toBe('seed');
|
|
74
|
+
expect(inferPolicyFromPath('frontend/src/pages/account/profile.tsx')).toBe('seed');
|
|
75
|
+
expect(inferPolicyFromPath('frontend/src/pages/shell/app.tsx')).toBe('seed');
|
|
76
|
+
expect(inferPolicyFromPath('frontend/src/pages/_errors/404.tsx')).toBe('seed');
|
|
77
|
+
expect(inferPolicyFromPath('frontend/src/layouts/main.tsx')).toBe('seed');
|
|
78
|
+
expect(inferPolicyFromPath('frontend/src/styles/brand.css')).toBe('seed');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns tracked for other paths', () => {
|
|
82
|
+
expect(inferPolicyFromPath('frontend/src/components/button.tsx')).toBe('tracked');
|
|
83
|
+
expect(inferPolicyFromPath('package.json')).toBe('tracked');
|
|
84
|
+
expect(inferPolicyFromPath('src/utils/helper.ts')).toBe('tracked');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('upsertManifestFile', () => {
|
|
89
|
+
it('adds new entry to manifest', () => {
|
|
90
|
+
const manifest = createManifest('1.0.0', 'test-project', 'eu-north-1', ['core']);
|
|
91
|
+
const entry: ManifestFileEntry = {
|
|
92
|
+
path: 'infra/main.tf',
|
|
93
|
+
module: 'core',
|
|
94
|
+
moduleVersion: '1.0.0',
|
|
95
|
+
sha256: 'abc123',
|
|
96
|
+
wasTemplate: false,
|
|
97
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
98
|
+
policy: 'owned',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
upsertManifestFile(manifest, entry);
|
|
102
|
+
expect(manifest.files).toHaveLength(1);
|
|
103
|
+
expect(manifest.files[0]).toEqual(entry);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('updates existing entry by path', () => {
|
|
107
|
+
const manifest = createManifest('1.0.0', 'test-project', 'eu-north-1', ['core']);
|
|
108
|
+
const entry1: ManifestFileEntry = {
|
|
109
|
+
path: 'infra/main.tf',
|
|
110
|
+
module: 'core',
|
|
111
|
+
moduleVersion: '1.0.0',
|
|
112
|
+
sha256: 'abc123',
|
|
113
|
+
wasTemplate: false,
|
|
114
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
115
|
+
policy: 'owned',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
upsertManifestFile(manifest, entry1);
|
|
119
|
+
expect(manifest.files).toHaveLength(1);
|
|
120
|
+
|
|
121
|
+
const entry2: ManifestFileEntry = {
|
|
122
|
+
...entry1,
|
|
123
|
+
sha256: 'xyz789',
|
|
124
|
+
moduleVersion: '1.1.0',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
upsertManifestFile(manifest, entry2);
|
|
128
|
+
expect(manifest.files).toHaveLength(1);
|
|
129
|
+
expect(manifest.files[0].sha256).toBe('xyz789');
|
|
130
|
+
expect(manifest.files[0].moduleVersion).toBe('1.1.0');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('preserves policy field in round-trip', () => {
|
|
134
|
+
const manifest = createManifest('1.0.0', 'test-project', 'eu-north-1', ['core']);
|
|
135
|
+
const entry: ManifestFileEntry = {
|
|
136
|
+
path: 'frontend/src/pages/auth/login.tsx',
|
|
137
|
+
module: 'frontend',
|
|
138
|
+
moduleVersion: '1.0.0',
|
|
139
|
+
sha256: 'xyz789',
|
|
140
|
+
wasTemplate: true,
|
|
141
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
142
|
+
policy: 'seed',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
upsertManifestFile(manifest, entry);
|
|
146
|
+
const retrieved = getManifestFile(manifest, entry.path);
|
|
147
|
+
expect(retrieved?.policy).toBe('seed');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('readManifest and writeManifest', () => {
|
|
152
|
+
it('writes and reads manifest with policy fields', async () => {
|
|
153
|
+
const manifest = createManifest('1.0.0', 'test-project', 'eu-north-1', ['core']);
|
|
154
|
+
const entry: ManifestFileEntry = {
|
|
155
|
+
path: 'infra/main.tf',
|
|
156
|
+
module: 'core',
|
|
157
|
+
moduleVersion: '1.0.0',
|
|
158
|
+
sha256: 'abc123',
|
|
159
|
+
wasTemplate: false,
|
|
160
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
161
|
+
policy: 'owned',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
upsertManifestFile(manifest, entry);
|
|
165
|
+
await writeManifest(tmpDir, manifest);
|
|
166
|
+
|
|
167
|
+
const read = await readManifest(tmpDir);
|
|
168
|
+
expect(read).not.toBeNull();
|
|
169
|
+
expect(read?.files).toHaveLength(1);
|
|
170
|
+
expect(read?.files[0].policy).toBe('owned');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('auto-migrates v1 entries (missing policy) on read', async () => {
|
|
174
|
+
// Create a v1 manifest without policy fields
|
|
175
|
+
const v1Manifest = {
|
|
176
|
+
$schema: 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v1.json',
|
|
177
|
+
scaffoldVersion: '1.0.0',
|
|
178
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
179
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
180
|
+
projectName: 'test-project',
|
|
181
|
+
awsRegion: 'eu-north-1',
|
|
182
|
+
enabledModules: ['core'],
|
|
183
|
+
files: [
|
|
184
|
+
{
|
|
185
|
+
path: 'infra/main.tf',
|
|
186
|
+
module: 'core',
|
|
187
|
+
moduleVersion: '1.0.0',
|
|
188
|
+
sha256: 'abc123',
|
|
189
|
+
wasTemplate: false,
|
|
190
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
path: 'frontend/src/pages/auth/login.tsx',
|
|
194
|
+
module: 'frontend',
|
|
195
|
+
moduleVersion: '1.0.0',
|
|
196
|
+
sha256: 'xyz789',
|
|
197
|
+
wasTemplate: true,
|
|
198
|
+
installedAt: '2026-01-01T00:00:00Z',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
await mkdir(join(tmpDir, '.tib'), { recursive: true });
|
|
204
|
+
await writeFile(
|
|
205
|
+
join(tmpDir, '.tib', 'manifest.json'),
|
|
206
|
+
JSON.stringify(v1Manifest, null, 2) + '\n',
|
|
207
|
+
'utf-8'
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const read = await readManifest(tmpDir);
|
|
211
|
+
expect(read).not.toBeNull();
|
|
212
|
+
expect(read!.files).toHaveLength(2);
|
|
213
|
+
|
|
214
|
+
// Check that policies were inferred
|
|
215
|
+
const ownedEntry = read!.files.find((f) => f.path === 'infra/main.tf');
|
|
216
|
+
expect(ownedEntry?.policy).toBe('owned');
|
|
217
|
+
|
|
218
|
+
const seedEntry = read!.files.find((f) => f.path === 'frontend/src/pages/auth/login.tsx');
|
|
219
|
+
expect(seedEntry?.policy).toBe('seed');
|
|
220
|
+
|
|
221
|
+
// Check that manifest was written back with v2 schema and policies
|
|
222
|
+
const reread = await readFile(join(tmpDir, '.tib', 'manifest.json'), 'utf-8');
|
|
223
|
+
const reparsed = JSON.parse(reread);
|
|
224
|
+
expect(reparsed.$schema).toBe('https://mc-scaffold.s3.amazonaws.com/schema/manifest.v2.json');
|
|
225
|
+
expect(reparsed.files[0].policy).toBe('owned');
|
|
226
|
+
expect(reparsed.files[1].policy).toBe('seed');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
applyTokens,
|
|
4
|
+
buildTokenContext,
|
|
5
|
+
isTemplateFile,
|
|
6
|
+
stripTplExtension,
|
|
7
|
+
type TokenContext,
|
|
8
|
+
} from '../../utils/token-substitution.js';
|
|
9
|
+
|
|
10
|
+
const mockCtx: TokenContext = {
|
|
11
|
+
PROJECT_NAME: 'my-project',
|
|
12
|
+
PROJECT_NAME_PASCAL: 'MyProject',
|
|
13
|
+
PROJECT_NAME_UPPER: 'MY_PROJECT',
|
|
14
|
+
AWS_REGION: 'eu-west-1',
|
|
15
|
+
AWS_ACCOUNT_ID: '123456789012',
|
|
16
|
+
ADMIN_EMAIL: 'admin@example.com',
|
|
17
|
+
SCAFFOLD_VERSION: '1.0.0',
|
|
18
|
+
SCAFFOLD_DATE: '2026-01-01',
|
|
19
|
+
TIB_CLI_VERSION: '0.2.0',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('applyTokens', () => {
|
|
23
|
+
it('replaces all 9 known tokens', () => {
|
|
24
|
+
const template = `
|
|
25
|
+
PROJECT_NAME={{PROJECT_NAME}}
|
|
26
|
+
PROJECT_NAME_PASCAL={{PROJECT_NAME_PASCAL}}
|
|
27
|
+
PROJECT_NAME_UPPER={{PROJECT_NAME_UPPER}}
|
|
28
|
+
AWS_REGION={{AWS_REGION}}
|
|
29
|
+
AWS_ACCOUNT_ID={{AWS_ACCOUNT_ID}}
|
|
30
|
+
ADMIN_EMAIL={{ADMIN_EMAIL}}
|
|
31
|
+
SCAFFOLD_VERSION={{SCAFFOLD_VERSION}}
|
|
32
|
+
SCAFFOLD_DATE={{SCAFFOLD_DATE}}
|
|
33
|
+
TIB_CLI_VERSION={{TIB_CLI_VERSION}}
|
|
34
|
+
`;
|
|
35
|
+
const result = applyTokens(template, mockCtx);
|
|
36
|
+
expect(result).not.toContain('{{');
|
|
37
|
+
expect(result).toContain('my-project');
|
|
38
|
+
expect(result).toContain('MyProject');
|
|
39
|
+
expect(result).toContain('MY_PROJECT');
|
|
40
|
+
expect(result).toContain('eu-west-1');
|
|
41
|
+
expect(result).toContain('123456789012');
|
|
42
|
+
expect(result).toContain('admin@example.com');
|
|
43
|
+
expect(result).toContain('1.0.0');
|
|
44
|
+
expect(result).toContain('2026-01-01');
|
|
45
|
+
expect(result).toContain('0.2.0');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('replaces repeated tokens', () => {
|
|
49
|
+
const template = '{{PROJECT_NAME}} and {{PROJECT_NAME}} again';
|
|
50
|
+
const result = applyTokens(template, mockCtx);
|
|
51
|
+
expect(result).toBe('my-project and my-project again');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('leaves non-token content unchanged', () => {
|
|
55
|
+
const template = 'Hello, world! No tokens here.';
|
|
56
|
+
expect(applyTokens(template, mockCtx)).toBe(template);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws on unknown token', () => {
|
|
60
|
+
expect(() => applyTokens('{{UNKNOWN_TOKEN}}', mockCtx)).toThrow('Unknown scaffold token: {{UNKNOWN_TOKEN}}');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles empty template', () => {
|
|
64
|
+
expect(applyTokens('', mockCtx)).toBe('');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('handles empty ADMIN_EMAIL', () => {
|
|
68
|
+
const ctx: TokenContext = { ...mockCtx, ADMIN_EMAIL: '' };
|
|
69
|
+
expect(applyTokens('email={{ADMIN_EMAIL}}', ctx)).toBe('email=');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('buildTokenContext', () => {
|
|
74
|
+
it('produces PascalCase from kebab-case', () => {
|
|
75
|
+
const ctx = buildTokenContext(
|
|
76
|
+
{ projectName: 'my-cool-project', awsRegion: 'eu-west-1', awsAccountId: '123' },
|
|
77
|
+
'1.0.0',
|
|
78
|
+
'0.2.0'
|
|
79
|
+
);
|
|
80
|
+
expect(ctx.PROJECT_NAME_PASCAL).toBe('MyCoolProject');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('produces UPPER_SNAKE from kebab-case', () => {
|
|
84
|
+
const ctx = buildTokenContext(
|
|
85
|
+
{ projectName: 'my-cool-project', awsRegion: 'eu-west-1', awsAccountId: '123' },
|
|
86
|
+
'1.0.0',
|
|
87
|
+
'0.2.0'
|
|
88
|
+
);
|
|
89
|
+
expect(ctx.PROJECT_NAME_UPPER).toBe('MY_COOL_PROJECT');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('uses empty string for missing adminEmail', () => {
|
|
93
|
+
const ctx = buildTokenContext(
|
|
94
|
+
{ projectName: 'p', awsRegion: 'us-east-1', awsAccountId: '123' },
|
|
95
|
+
'1.0.0',
|
|
96
|
+
'0.2.0'
|
|
97
|
+
);
|
|
98
|
+
expect(ctx.ADMIN_EMAIL).toBe('');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('sets SCAFFOLD_DATE to today ISO date', () => {
|
|
102
|
+
const ctx = buildTokenContext(
|
|
103
|
+
{ projectName: 'p', awsRegion: 'us-east-1', awsAccountId: '123' },
|
|
104
|
+
'1.0.0',
|
|
105
|
+
'0.2.0'
|
|
106
|
+
);
|
|
107
|
+
expect(ctx.SCAFFOLD_DATE).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('isTemplateFile', () => {
|
|
112
|
+
it('returns true for .tpl files', () => {
|
|
113
|
+
expect(isTemplateFile('package.json.tpl')).toBe(true);
|
|
114
|
+
expect(isTemplateFile('app.ts.tpl')).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('returns false for non-.tpl files', () => {
|
|
118
|
+
expect(isTemplateFile('package.json')).toBe(false);
|
|
119
|
+
expect(isTemplateFile('app.ts')).toBe(false);
|
|
120
|
+
expect(isTemplateFile('README.md')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('stripTplExtension', () => {
|
|
125
|
+
it('removes .tpl from end', () => {
|
|
126
|
+
expect(stripTplExtension('package.json.tpl')).toBe('package.json');
|
|
127
|
+
expect(stripTplExtension('app.ts.tpl')).toBe('app.ts');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('leaves non-.tpl filenames unchanged', () => {
|
|
131
|
+
expect(stripTplExtension('package.json')).toBe('package.json');
|
|
132
|
+
expect(stripTplExtension('README.md')).toBe('README.md');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../utils/logger.js', () => ({
|
|
4
|
+
cliLogger: {
|
|
5
|
+
info: vi.fn(),
|
|
6
|
+
error: vi.fn(),
|
|
7
|
+
warn: vi.fn(),
|
|
8
|
+
debug: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { runValidate } from '../commands/validate.js';
|
|
13
|
+
|
|
14
|
+
describe('runValidate error shape', () => {
|
|
15
|
+
it('throws when domain root has no domain.config.ts', async () => {
|
|
16
|
+
await expect(runValidate('/nonexistent-path', false)).rejects.toThrow();
|
|
17
|
+
});
|
|
18
|
+
});
|