@lumenflow/cli 2.5.0 → 2.6.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.
Files changed (29) hide show
  1. package/README.md +11 -8
  2. package/dist/__tests__/gates-config.test.js +304 -0
  3. package/dist/__tests__/init-scripts.test.js +111 -0
  4. package/dist/__tests__/templates-sync.test.js +219 -0
  5. package/dist/gates.js +64 -15
  6. package/dist/init.js +90 -0
  7. package/dist/orchestrate-init-status.js +37 -9
  8. package/dist/orchestrate-initiative.js +10 -4
  9. package/dist/sync-templates.js +137 -5
  10. package/dist/wu-prep.js +131 -8
  11. package/dist/wu-spawn.js +7 -2
  12. package/package.json +7 -7
  13. package/templates/core/.lumenflow/constraints.md.template +61 -3
  14. package/templates/core/LUMENFLOW.md.template +85 -23
  15. package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
  16. package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
  17. package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
  18. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
  19. package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
  20. package/templates/core/ai/onboarding/release-process.md.template +8 -2
  21. package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
  22. package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
  23. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
  24. package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
  25. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
  26. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
  27. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
  28. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
  29. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
package/README.md CHANGED
@@ -16,19 +16,22 @@ npm install @lumenflow/cli
16
16
  ## Quick Start
17
17
 
18
18
  ```bash
19
+ # Install the CLI
20
+ pnpm add -D @lumenflow/cli # or: npm install -D @lumenflow/cli
21
+
19
22
  # Initialize LumenFlow (works with any AI)
20
- npx lumenflow-init
23
+ pnpm exec lumenflow
21
24
 
22
25
  # Or specify your AI tool for enhanced integration
23
- npx lumenflow-init --client claude # Claude Code
24
- npx lumenflow-init --client cursor # Cursor
25
- npx lumenflow-init --client windsurf # Windsurf
26
- npx lumenflow-init --client cline # Cline
27
- npx lumenflow-init --client aider # Aider
28
- npx lumenflow-init --client all # All integrations
26
+ pnpm exec lumenflow --client claude # Claude Code
27
+ pnpm exec lumenflow --client cursor # Cursor
28
+ pnpm exec lumenflow --client windsurf # Windsurf
29
+ pnpm exec lumenflow --client cline # Cline
30
+ pnpm exec lumenflow --client aider # Aider
31
+ pnpm exec lumenflow --client all # All integrations
29
32
  ```
30
33
 
31
- The default `lumenflow-init` creates `AGENTS.md` and `LUMENFLOW.md` which work with **any AI coding assistant**. The `--client` flag adds vendor-specific configuration files for deeper integration.
34
+ The default `lumenflow` command creates `AGENTS.md` and `LUMENFLOW.md` which work with **any AI coding assistant**. The `--client` flag adds vendor-specific configuration files for deeper integration.
32
35
 
33
36
  See [AI Integrations](https://lumenflow.dev/guides/ai-integrations) for details on each tool.
34
37
 
@@ -0,0 +1,304 @@
1
+ /**
2
+ * @file gates-config.test.ts
3
+ * WU-1356: Tests for package manager and script name configuration.
4
+ *
5
+ * Tests configurable package_manager, gates.commands, test_runner, and build_command
6
+ * in .lumenflow.config.yaml for framework agnosticism.
7
+ */
8
+ /* eslint-disable sonarjs/no-duplicate-string -- Test files intentionally repeat string literals for readability */
9
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import * as os from 'node:os';
13
+ import * as yaml from 'yaml';
14
+ // Import the schema and functions we're testing
15
+ import { PackageManagerSchema, TestRunnerSchema, GatesCommandsConfigSchema, parseConfig, } from '@lumenflow/core/dist/lumenflow-config-schema.js';
16
+ import { resolvePackageManager, resolveBuildCommand, resolveGatesCommands, resolveTestRunner, getIgnorePatterns, } from '@lumenflow/core/dist/gates-config.js';
17
+ describe('WU-1356: Package manager and script configuration', () => {
18
+ let tempDir;
19
+ beforeEach(() => {
20
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-test-'));
21
+ });
22
+ afterEach(() => {
23
+ fs.rmSync(tempDir, { recursive: true, force: true });
24
+ });
25
+ describe('PackageManagerSchema', () => {
26
+ it('accepts pnpm as default', () => {
27
+ const result = PackageManagerSchema.parse(undefined);
28
+ expect(result).toBe('pnpm');
29
+ });
30
+ it('accepts npm', () => {
31
+ const result = PackageManagerSchema.parse('npm');
32
+ expect(result).toBe('npm');
33
+ });
34
+ it('accepts yarn', () => {
35
+ const result = PackageManagerSchema.parse('yarn');
36
+ expect(result).toBe('yarn');
37
+ });
38
+ it('accepts bun', () => {
39
+ const result = PackageManagerSchema.parse('bun');
40
+ expect(result).toBe('bun');
41
+ });
42
+ it('rejects invalid package manager', () => {
43
+ expect(() => PackageManagerSchema.parse('invalid')).toThrow();
44
+ });
45
+ });
46
+ describe('TestRunnerSchema', () => {
47
+ it('accepts vitest as default', () => {
48
+ const result = TestRunnerSchema.parse(undefined);
49
+ expect(result).toBe('vitest');
50
+ });
51
+ it('accepts jest', () => {
52
+ const result = TestRunnerSchema.parse('jest');
53
+ expect(result).toBe('jest');
54
+ });
55
+ it('accepts mocha', () => {
56
+ const result = TestRunnerSchema.parse('mocha');
57
+ expect(result).toBe('mocha');
58
+ });
59
+ it('rejects invalid test runner', () => {
60
+ expect(() => TestRunnerSchema.parse('invalid')).toThrow();
61
+ });
62
+ });
63
+ describe('GatesCommandsConfigSchema', () => {
64
+ it('has sensible defaults for test commands', () => {
65
+ const result = GatesCommandsConfigSchema.parse({});
66
+ expect(result.test_full).toBeDefined();
67
+ expect(result.test_docs_only).toBeDefined();
68
+ expect(result.test_incremental).toBeDefined();
69
+ });
70
+ it('allows custom test commands', () => {
71
+ const config = {
72
+ test_full: 'npm test',
73
+ test_docs_only: 'npm test -- --grep docs',
74
+ test_incremental: 'npm test -- --changed',
75
+ };
76
+ const result = GatesCommandsConfigSchema.parse(config);
77
+ expect(result.test_full).toBe('npm test');
78
+ expect(result.test_docs_only).toBe('npm test -- --grep docs');
79
+ expect(result.test_incremental).toBe('npm test -- --changed');
80
+ });
81
+ });
82
+ describe('LumenFlowConfigSchema - package_manager field', () => {
83
+ it('includes package_manager with default pnpm', () => {
84
+ const config = parseConfig({});
85
+ expect(config.package_manager).toBe('pnpm');
86
+ });
87
+ it('accepts npm as package_manager', () => {
88
+ const config = parseConfig({ package_manager: 'npm' });
89
+ expect(config.package_manager).toBe('npm');
90
+ });
91
+ it('accepts yarn as package_manager', () => {
92
+ const config = parseConfig({ package_manager: 'yarn' });
93
+ expect(config.package_manager).toBe('yarn');
94
+ });
95
+ it('accepts bun as package_manager', () => {
96
+ const config = parseConfig({ package_manager: 'bun' });
97
+ expect(config.package_manager).toBe('bun');
98
+ });
99
+ });
100
+ describe('LumenFlowConfigSchema - test_runner field', () => {
101
+ it('includes test_runner with default vitest', () => {
102
+ const config = parseConfig({});
103
+ expect(config.test_runner).toBe('vitest');
104
+ });
105
+ it('accepts jest as test_runner', () => {
106
+ const config = parseConfig({ test_runner: 'jest' });
107
+ expect(config.test_runner).toBe('jest');
108
+ });
109
+ it('accepts mocha as test_runner', () => {
110
+ const config = parseConfig({ test_runner: 'mocha' });
111
+ expect(config.test_runner).toBe('mocha');
112
+ });
113
+ });
114
+ describe('LumenFlowConfigSchema - gates.commands section', () => {
115
+ it('includes gates.commands with defaults', () => {
116
+ const config = parseConfig({});
117
+ expect(config.gates.commands).toBeDefined();
118
+ expect(config.gates.commands.test_full).toBeDefined();
119
+ expect(config.gates.commands.test_docs_only).toBeDefined();
120
+ expect(config.gates.commands.test_incremental).toBeDefined();
121
+ });
122
+ it('allows custom gates commands configuration', () => {
123
+ const config = parseConfig({
124
+ gates: {
125
+ commands: {
126
+ test_full: 'npm test',
127
+ test_docs_only: 'npm test -- --docs',
128
+ test_incremental: 'npm test -- --changed',
129
+ },
130
+ },
131
+ });
132
+ expect(config.gates.commands.test_full).toBe('npm test');
133
+ expect(config.gates.commands.test_docs_only).toBe('npm test -- --docs');
134
+ expect(config.gates.commands.test_incremental).toBe('npm test -- --changed');
135
+ });
136
+ });
137
+ describe('LumenFlowConfigSchema - build_command field', () => {
138
+ it('includes build_command with default for pnpm', () => {
139
+ const config = parseConfig({});
140
+ expect(config.build_command).toBe('pnpm --filter @lumenflow/cli build');
141
+ });
142
+ it('allows custom build_command', () => {
143
+ const config = parseConfig({ build_command: 'npm run build' });
144
+ expect(config.build_command).toBe('npm run build');
145
+ });
146
+ });
147
+ describe('resolvePackageManager', () => {
148
+ it('returns pnpm when no config file exists', () => {
149
+ const result = resolvePackageManager(tempDir);
150
+ expect(result).toBe('pnpm');
151
+ });
152
+ it('returns configured package manager from config file', () => {
153
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
154
+ fs.writeFileSync(configPath, yaml.stringify({ package_manager: 'npm' }));
155
+ const result = resolvePackageManager(tempDir);
156
+ expect(result).toBe('npm');
157
+ });
158
+ it('returns yarn when configured', () => {
159
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
160
+ fs.writeFileSync(configPath, yaml.stringify({ package_manager: 'yarn' }));
161
+ const result = resolvePackageManager(tempDir);
162
+ expect(result).toBe('yarn');
163
+ });
164
+ });
165
+ describe('resolveBuildCommand', () => {
166
+ it('returns default build command when no config file exists', () => {
167
+ const result = resolveBuildCommand(tempDir);
168
+ expect(result).toBe('pnpm --filter @lumenflow/cli build');
169
+ });
170
+ it('returns configured build_command from config file', () => {
171
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
172
+ fs.writeFileSync(configPath, yaml.stringify({ build_command: 'npm run build' }));
173
+ const result = resolveBuildCommand(tempDir);
174
+ expect(result).toBe('npm run build');
175
+ });
176
+ it('adapts default build command for different package managers', () => {
177
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
178
+ fs.writeFileSync(configPath, yaml.stringify({ package_manager: 'npm' }));
179
+ const result = resolveBuildCommand(tempDir);
180
+ expect(result).toContain('npm');
181
+ });
182
+ });
183
+ describe('resolveGatesCommands', () => {
184
+ it('returns default commands when no config file exists', () => {
185
+ const commands = resolveGatesCommands(tempDir);
186
+ expect(commands.test_full).toBeDefined();
187
+ expect(commands.test_docs_only).toBeDefined();
188
+ expect(commands.test_incremental).toBeDefined();
189
+ });
190
+ it('returns configured commands from config file', () => {
191
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
192
+ fs.writeFileSync(configPath, yaml.stringify({
193
+ gates: {
194
+ commands: {
195
+ test_full: 'npm test',
196
+ test_docs_only: 'npm test -- --docs',
197
+ test_incremental: 'npm test -- --changed',
198
+ },
199
+ },
200
+ }));
201
+ const commands = resolveGatesCommands(tempDir);
202
+ expect(commands.test_full).toBe('npm test');
203
+ expect(commands.test_docs_only).toBe('npm test -- --docs');
204
+ expect(commands.test_incremental).toBe('npm test -- --changed');
205
+ });
206
+ });
207
+ describe('resolveTestRunner', () => {
208
+ it('returns vitest when no config file exists', () => {
209
+ const result = resolveTestRunner(tempDir);
210
+ expect(result).toBe('vitest');
211
+ });
212
+ it('returns jest when configured', () => {
213
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
214
+ fs.writeFileSync(configPath, yaml.stringify({ test_runner: 'jest' }));
215
+ const result = resolveTestRunner(tempDir);
216
+ expect(result).toBe('jest');
217
+ });
218
+ });
219
+ describe('getIgnorePatterns', () => {
220
+ it('returns .turbo pattern for vitest', () => {
221
+ const patterns = getIgnorePatterns('vitest');
222
+ expect(patterns).toContain('.turbo');
223
+ });
224
+ it('returns different pattern for jest', () => {
225
+ const patterns = getIgnorePatterns('jest');
226
+ expect(patterns).not.toContain('.turbo');
227
+ });
228
+ it('returns custom pattern from config', () => {
229
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
230
+ fs.writeFileSync(configPath, yaml.stringify({
231
+ gates: {
232
+ ignore_patterns: ['.nx', 'dist'],
233
+ },
234
+ }));
235
+ // This would be a config-aware version
236
+ const patterns = getIgnorePatterns('jest');
237
+ expect(Array.isArray(patterns)).toBe(true);
238
+ });
239
+ });
240
+ });
241
+ describe('WU-1356: npm+jest configuration', () => {
242
+ let tempDir;
243
+ beforeEach(() => {
244
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-npm-jest-'));
245
+ });
246
+ afterEach(() => {
247
+ fs.rmSync(tempDir, { recursive: true, force: true });
248
+ });
249
+ it('supports npm+jest configuration', () => {
250
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
251
+ fs.writeFileSync(configPath, yaml.stringify({
252
+ package_manager: 'npm',
253
+ test_runner: 'jest',
254
+ gates: {
255
+ commands: {
256
+ test_full: 'npm test',
257
+ test_docs_only: 'npm test -- --testPathPattern=docs',
258
+ test_incremental: 'npm test -- --onlyChanged',
259
+ },
260
+ },
261
+ build_command: 'npm run build',
262
+ }));
263
+ const pkgManager = resolvePackageManager(tempDir);
264
+ const testRunner = resolveTestRunner(tempDir);
265
+ const commands = resolveGatesCommands(tempDir);
266
+ const buildCmd = resolveBuildCommand(tempDir);
267
+ expect(pkgManager).toBe('npm');
268
+ expect(testRunner).toBe('jest');
269
+ expect(commands.test_full).toBe('npm test');
270
+ expect(commands.test_incremental).toBe('npm test -- --onlyChanged');
271
+ expect(buildCmd).toBe('npm run build');
272
+ });
273
+ });
274
+ describe('WU-1356: yarn+nx configuration', () => {
275
+ let tempDir;
276
+ beforeEach(() => {
277
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-yarn-nx-'));
278
+ });
279
+ afterEach(() => {
280
+ fs.rmSync(tempDir, { recursive: true, force: true });
281
+ });
282
+ it('supports yarn+nx configuration', () => {
283
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
284
+ fs.writeFileSync(configPath, yaml.stringify({
285
+ package_manager: 'yarn',
286
+ test_runner: 'jest',
287
+ gates: {
288
+ commands: {
289
+ test_full: 'yarn nx run-many --target=test --all',
290
+ test_docs_only: 'yarn nx test docs',
291
+ test_incremental: 'yarn nx affected --target=test',
292
+ },
293
+ },
294
+ build_command: 'yarn nx build @lumenflow/cli',
295
+ }));
296
+ const pkgManager = resolvePackageManager(tempDir);
297
+ const commands = resolveGatesCommands(tempDir);
298
+ const buildCmd = resolveBuildCommand(tempDir);
299
+ expect(pkgManager).toBe('yarn');
300
+ expect(commands.test_full).toBe('yarn nx run-many --target=test --all');
301
+ expect(commands.test_incremental).toBe('yarn nx affected --target=test');
302
+ expect(buildCmd).toBe('yarn nx build @lumenflow/cli');
303
+ });
304
+ });
@@ -93,4 +93,115 @@ describe('init scripts generation (WU-1307)', () => {
93
93
  expect(packageJson.scripts?.['wu:claim']).toBeDefined();
94
94
  expect(packageJson.scripts?.gates).toBeDefined();
95
95
  });
96
+ // WU-1342: Test for all 17 essential commands
97
+ it('should include all 17 essential commands (WU-1342)', async () => {
98
+ // Act
99
+ await scaffoldProject(tempDir, { force: true, full: true });
100
+ // Assert
101
+ const packageJson = readPackageJson();
102
+ // All 17 essential commands that must be present per WU-1342 acceptance criteria
103
+ const essentialScripts = [
104
+ // Core WU lifecycle
105
+ 'wu:claim',
106
+ 'wu:done',
107
+ 'wu:create',
108
+ 'wu:status',
109
+ 'wu:block',
110
+ 'wu:unblock',
111
+ // Additional critical commands (WU-1342)
112
+ 'wu:prep',
113
+ 'wu:recover',
114
+ 'wu:spawn',
115
+ 'wu:validate',
116
+ 'wu:infer-lane',
117
+ // Memory commands
118
+ 'mem:init',
119
+ 'mem:checkpoint',
120
+ 'mem:inbox',
121
+ // Lane commands
122
+ 'lane:suggest',
123
+ // Gates
124
+ 'gates',
125
+ 'gates:docs',
126
+ ];
127
+ for (const scriptName of essentialScripts) {
128
+ expect(packageJson.scripts?.[scriptName], `Missing essential script: ${scriptName}`).toBeDefined();
129
+ }
130
+ // Verify count
131
+ const lumenflowScripts = Object.keys(packageJson.scripts ?? {}).filter((key) => key.startsWith('wu:') ||
132
+ key.startsWith('mem:') ||
133
+ key.startsWith('lane:') ||
134
+ key === 'gates' ||
135
+ key === 'gates:docs');
136
+ expect(lumenflowScripts.length).toBeGreaterThanOrEqual(17);
137
+ });
138
+ });
139
+ describe('init .gitignore generation (WU-1342)', () => {
140
+ let tempDir;
141
+ beforeEach(() => {
142
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-gitignore-'));
143
+ });
144
+ afterEach(() => {
145
+ if (tempDir && fs.existsSync(tempDir)) {
146
+ fs.rmSync(tempDir, { recursive: true, force: true });
147
+ }
148
+ });
149
+ it('should create .gitignore with required exclusions (WU-1342)', async () => {
150
+ // Act
151
+ await scaffoldProject(tempDir, { force: true, full: true });
152
+ // Assert
153
+ const gitignorePath = path.join(tempDir, '.gitignore');
154
+ expect(fs.existsSync(gitignorePath)).toBe(true);
155
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
156
+ // Must include node_modules
157
+ expect(content).toContain('node_modules');
158
+ // Must include .lumenflow/state
159
+ expect(content).toContain('.lumenflow/state');
160
+ // Must include worktrees
161
+ expect(content).toContain('worktrees');
162
+ });
163
+ it('should preserve existing .gitignore content in merge mode (WU-1342)', async () => {
164
+ // Arrange
165
+ const gitignorePath = path.join(tempDir, '.gitignore');
166
+ const existingContent = '# Custom ignores\n.env\n*.log\n';
167
+ fs.writeFileSync(gitignorePath, existingContent);
168
+ // Act
169
+ await scaffoldProject(tempDir, { force: false, full: true, merge: true });
170
+ // Assert
171
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
172
+ // Should preserve existing content
173
+ expect(content).toContain('.env');
174
+ expect(content).toContain('*.log');
175
+ // Should add LumenFlow exclusions
176
+ expect(content).toContain('node_modules');
177
+ expect(content).toContain('.lumenflow/state');
178
+ expect(content).toContain('worktrees');
179
+ });
180
+ });
181
+ describe('init .claude directory creation (WU-1342)', () => {
182
+ let tempDir;
183
+ beforeEach(() => {
184
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-init-claude-'));
185
+ });
186
+ afterEach(() => {
187
+ if (tempDir && fs.existsSync(tempDir)) {
188
+ fs.rmSync(tempDir, { recursive: true, force: true });
189
+ }
190
+ });
191
+ it('should create .claude directory when --client claude specified (WU-1342)', async () => {
192
+ // Act
193
+ await scaffoldProject(tempDir, { force: true, full: false, client: 'claude' });
194
+ // Assert
195
+ const claudeDir = path.join(tempDir, '.claude');
196
+ expect(fs.existsSync(claudeDir)).toBe(true);
197
+ // Should have agents directory
198
+ const agentsDir = path.join(claudeDir, 'agents');
199
+ expect(fs.existsSync(agentsDir)).toBe(true);
200
+ // Should have settings.json
201
+ const settingsPath = path.join(claudeDir, 'settings.json');
202
+ expect(fs.existsSync(settingsPath)).toBe(true);
203
+ // Should have skills directory
204
+ const skillsDir = path.join(claudeDir, 'skills');
205
+ expect(fs.existsSync(skillsDir)).toBe(true);
206
+ });
96
207
  });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @file templates-sync.test.ts
3
+ * Tests for templates synchronization and drift detection (WU-1353)
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ import { syncTemplates, syncOnboardingDocs, syncCoreDocs, convertToTemplate, checkTemplateDrift, } from '../sync-templates.js';
10
+ // Constants for frequently used path segments (sonarjs/no-duplicate-string)
11
+ const PACKAGES_DIR = 'packages';
12
+ const LUMENFLOW_SCOPE = '@lumenflow';
13
+ const CLI_DIR = 'cli';
14
+ const TEMPLATES_DIR = 'templates';
15
+ const CORE_DIR = 'core';
16
+ const LUMENFLOW_DOT_DIR = '.lumenflow';
17
+ const CONSTRAINTS_FILE = 'constraints.md';
18
+ const CONSTRAINTS_TEMPLATE = 'constraints.md.template';
19
+ describe('templates-sync', () => {
20
+ let tempDir;
21
+ beforeEach(() => {
22
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'templates-sync-test-'));
23
+ });
24
+ afterEach(() => {
25
+ if (tempDir && fs.existsSync(tempDir)) {
26
+ fs.rmSync(tempDir, { recursive: true, force: true });
27
+ }
28
+ });
29
+ describe('convertToTemplate', () => {
30
+ it('should replace dates with {{DATE}} placeholder', () => {
31
+ const content = 'Updated: 2026-02-02\nCreated: 2025-01-15';
32
+ const result = convertToTemplate(content, '/home/test/project');
33
+ expect(result).toBe('Updated: {{DATE}}\nCreated: {{DATE}}');
34
+ });
35
+ it('should preserve content without dates', () => {
36
+ const content = '# Title\n\nSome content without dates.';
37
+ const result = convertToTemplate(content, '/home/test/project');
38
+ expect(result).toBe(content);
39
+ });
40
+ });
41
+ describe('syncCoreDocs', () => {
42
+ beforeEach(() => {
43
+ // Set up directory structure
44
+ const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
45
+ fs.mkdirSync(templatesDir, { recursive: true });
46
+ // Create source constraints.md with v1.1 content
47
+ const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
48
+ fs.mkdirSync(lumenflowDir, { recursive: true });
49
+ fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# LumenFlow Constraints Capsule
50
+
51
+ **Version:** 1.1
52
+ **Last updated:** 2026-02-02
53
+
54
+ This document contains the 7 non-negotiable constraints.
55
+
56
+ ### 1. Worktree Discipline and Git Safety
57
+
58
+ **MANDATORY PRE-WRITE CHECK**
59
+
60
+ **NEVER "QUICK FIX" ON MAIN**
61
+ `);
62
+ // Create LUMENFLOW.md
63
+ fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), `# LumenFlow Workflow Guide
64
+
65
+ **Last updated:** 2026-02-02
66
+
67
+ ## Critical Rule: Use wu:prep Then wu:done
68
+ `);
69
+ });
70
+ it('should sync constraints.md to template', async () => {
71
+ const result = await syncCoreDocs(tempDir, false);
72
+ expect(result.errors).toHaveLength(0);
73
+ expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/${LUMENFLOW_DOT_DIR}/${CONSTRAINTS_TEMPLATE}`);
74
+ // Verify template content
75
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
76
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
77
+ // Should have {{DATE}} placeholder
78
+ expect(templateContent).toContain('{{DATE}}');
79
+ expect(templateContent).not.toContain('2026-02-02');
80
+ // Should have v1.1 content markers
81
+ expect(templateContent).toContain('Version:** 1.1');
82
+ expect(templateContent).toContain('7 non-negotiable constraints');
83
+ expect(templateContent).toContain('MANDATORY PRE-WRITE CHECK');
84
+ expect(templateContent).toContain('NEVER "QUICK FIX" ON MAIN');
85
+ });
86
+ it('should use dry-run mode without writing files', async () => {
87
+ // First, ensure no template exists
88
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
89
+ // Remove if it exists from beforeEach
90
+ if (fs.existsSync(templatePath)) {
91
+ fs.unlinkSync(templatePath);
92
+ }
93
+ const result = await syncCoreDocs(tempDir, true);
94
+ expect(result.errors).toHaveLength(0);
95
+ expect(result.synced.length).toBeGreaterThan(0);
96
+ expect(fs.existsSync(templatePath)).toBe(false);
97
+ });
98
+ });
99
+ describe('syncOnboardingDocs', () => {
100
+ const ONBOARDING_SUBPATH = [
101
+ 'docs',
102
+ '04-operations',
103
+ '_frameworks',
104
+ 'lumenflow',
105
+ 'agent',
106
+ 'onboarding',
107
+ ];
108
+ const FIRST_WU_MISTAKES_FILE = 'first-wu-mistakes.md';
109
+ beforeEach(() => {
110
+ // Set up onboarding source directory
111
+ const onboardingDir = path.join(tempDir, ...ONBOARDING_SUBPATH);
112
+ fs.mkdirSync(onboardingDir, { recursive: true });
113
+ // Create first-wu-mistakes.md with v1.1 content (11 mistakes)
114
+ fs.writeFileSync(path.join(onboardingDir, FIRST_WU_MISTAKES_FILE), `# First WU Mistakes
115
+
116
+ **Last updated:** 2026-02-02
117
+
118
+ ## Mistake 1: Not Using Worktrees
119
+
120
+ pnpm wu:prep --id WU-123
121
+
122
+ ## Mistake 11: "Quick Fixing" on Main
123
+
124
+ ## Quick Checklist
125
+
126
+ - [ ] Check spec_refs for plans
127
+ `);
128
+ // Set up target directory
129
+ const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding');
130
+ fs.mkdirSync(templatesDir, { recursive: true });
131
+ });
132
+ it('should sync first-wu-mistakes.md to template', async () => {
133
+ const result = await syncOnboardingDocs(tempDir, false);
134
+ expect(result.errors).toHaveLength(0);
135
+ expect(result.synced).toContain(`${PACKAGES_DIR}/${LUMENFLOW_SCOPE}/${CLI_DIR}/${TEMPLATES_DIR}/${CORE_DIR}/ai/onboarding/${FIRST_WU_MISTAKES_FILE}.template`);
136
+ // Verify template content
137
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, 'ai', 'onboarding', `${FIRST_WU_MISTAKES_FILE}.template`);
138
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
139
+ // Should have {{DATE}} placeholder
140
+ expect(templateContent).toContain('{{DATE}}');
141
+ expect(templateContent).not.toContain('2026-02-02');
142
+ // Should have v1.1 content markers
143
+ expect(templateContent).toContain('Mistake 11:');
144
+ expect(templateContent).toContain('Quick Fixing" on Main');
145
+ expect(templateContent).toContain('wu:prep');
146
+ expect(templateContent).toContain('spec_refs');
147
+ });
148
+ });
149
+ describe('checkTemplateDrift', () => {
150
+ beforeEach(() => {
151
+ // Set up source files
152
+ const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
153
+ fs.mkdirSync(lumenflowDir, { recursive: true });
154
+ fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), `# Constraints
155
+ **Version:** 1.1
156
+ **Last updated:** 2026-02-02
157
+ 7 constraints`);
158
+ // Set up template directory
159
+ const templatesDir = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR);
160
+ fs.mkdirSync(templatesDir, { recursive: true });
161
+ });
162
+ it('should detect drift when template is outdated', async () => {
163
+ // Create outdated template (v1.0, 6 constraints)
164
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
165
+ fs.writeFileSync(templatePath, `# Constraints
166
+ **Version:** 1.0
167
+ **Last updated:** {{DATE}}
168
+ 6 constraints`);
169
+ const drift = await checkTemplateDrift(tempDir);
170
+ expect(drift.hasDrift).toBe(true);
171
+ expect(drift.driftingFiles.length).toBeGreaterThan(0);
172
+ expect(drift.driftingFiles.some((f) => f.includes(CONSTRAINTS_FILE))).toBe(true);
173
+ });
174
+ it('should report no drift when templates are in sync', async () => {
175
+ // First sync templates
176
+ await syncCoreDocs(tempDir, false);
177
+ // Then check for drift
178
+ const drift = await checkTemplateDrift(tempDir);
179
+ // After sync, constraints should not be drifting
180
+ expect(drift.driftingFiles.filter((f) => f.includes(CONSTRAINTS_FILE))).toHaveLength(0);
181
+ });
182
+ it('should return detailed drift report', async () => {
183
+ // Create outdated template
184
+ const templatePath = path.join(tempDir, PACKAGES_DIR, LUMENFLOW_SCOPE, CLI_DIR, TEMPLATES_DIR, CORE_DIR, LUMENFLOW_DOT_DIR, CONSTRAINTS_TEMPLATE);
185
+ fs.writeFileSync(templatePath, 'outdated content');
186
+ const drift = await checkTemplateDrift(tempDir);
187
+ expect(drift.hasDrift).toBe(true);
188
+ expect(drift.driftingFiles).toBeDefined();
189
+ expect(Array.isArray(drift.driftingFiles)).toBe(true);
190
+ });
191
+ });
192
+ describe('syncTemplates (full sync)', () => {
193
+ beforeEach(() => {
194
+ // Set up minimal directory structure
195
+ const lumenflowDir = path.join(tempDir, LUMENFLOW_DOT_DIR);
196
+ fs.mkdirSync(lumenflowDir, { recursive: true });
197
+ fs.writeFileSync(path.join(lumenflowDir, CONSTRAINTS_FILE), 'content');
198
+ fs.writeFileSync(path.join(tempDir, 'LUMENFLOW.md'), 'content');
199
+ const onboardingDir = path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
200
+ fs.mkdirSync(onboardingDir, { recursive: true });
201
+ fs.writeFileSync(path.join(onboardingDir, 'first-wu-mistakes.md'), 'content');
202
+ const skillsDir = path.join(tempDir, '.claude', 'skills', 'test-skill');
203
+ fs.mkdirSync(skillsDir, { recursive: true });
204
+ fs.writeFileSync(path.join(skillsDir, 'SKILL.md'), 'skill content');
205
+ });
206
+ it('should sync all template categories', async () => {
207
+ const result = await syncTemplates(tempDir, false);
208
+ expect(result.core.errors).toHaveLength(0);
209
+ expect(result.onboarding.errors).toHaveLength(0);
210
+ expect(result.skills.errors).toHaveLength(0);
211
+ // Should sync at least constraints and LUMENFLOW
212
+ expect(result.core.synced.length).toBeGreaterThanOrEqual(2);
213
+ // Should sync onboarding docs
214
+ expect(result.onboarding.synced.length).toBeGreaterThanOrEqual(1);
215
+ // Should sync skills
216
+ expect(result.skills.synced.length).toBeGreaterThanOrEqual(1);
217
+ });
218
+ });
219
+ });