@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.
- package/README.md +11 -8
- package/dist/__tests__/gates-config.test.js +304 -0
- package/dist/__tests__/init-scripts.test.js +111 -0
- package/dist/__tests__/templates-sync.test.js +219 -0
- package/dist/gates.js +64 -15
- package/dist/init.js +90 -0
- package/dist/orchestrate-init-status.js +37 -9
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/sync-templates.js +137 -5
- package/dist/wu-prep.js +131 -8
- package/dist/wu-spawn.js +7 -2
- package/package.json +7 -7
- package/templates/core/.lumenflow/constraints.md.template +61 -3
- package/templates/core/LUMENFLOW.md.template +85 -23
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
- package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
- package/templates/core/ai/onboarding/release-process.md.template +8 -2
- package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
- package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
- package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
- 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
|
-
|
|
23
|
+
pnpm exec lumenflow
|
|
21
24
|
|
|
22
25
|
# Or specify your AI tool for enhanced integration
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
+
});
|