prjct-cli 1.4.0 → 1.5.1
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 +123 -1
- package/bin/prjct.ts +23 -14
- package/core/__tests__/agentic/command-executor.test.ts +19 -19
- package/core/__tests__/agentic/prompt-builder.test.ts +16 -16
- package/core/__tests__/ai-tools/formatters.test.ts +118 -0
- package/core/agentic/command-executor.ts +18 -17
- package/core/agentic/prompt-builder.ts +18 -17
- package/core/agentic/template-executor.ts +2 -2
- package/core/ai-tools/formatters.ts +18 -0
- package/core/ai-tools/registry.ts +17 -14
- package/core/cli/start.ts +18 -17
- package/core/commands/analysis.ts +1 -1
- package/core/commands/setup.ts +8 -8
- package/core/commands/uninstall.ts +11 -11
- package/core/index.ts +103 -21
- package/core/infrastructure/agent-detector.ts +8 -8
- package/core/infrastructure/ai-provider.ts +49 -37
- package/core/infrastructure/command-installer.ts +18 -10
- package/core/infrastructure/path-manager.ts +4 -4
- package/core/infrastructure/setup.ts +124 -119
- package/core/infrastructure/update-checker.ts +14 -13
- package/core/integrations/linear/sync.ts +4 -4
- package/core/services/context-generator.ts +12 -3
- package/core/services/hooks-service.ts +78 -68
- package/core/services/sync-service.ts +64 -6
- package/core/utils/citations.ts +53 -0
- package/core/utils/error-messages.ts +11 -0
- package/core/utils/fs-helpers.ts +14 -0
- package/core/utils/project-credentials.ts +8 -7
- package/dist/bin/prjct.mjs +854 -643
- package/dist/core/infrastructure/command-installer.js +118 -87
- package/dist/core/infrastructure/setup.js +246 -210
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,134 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.1] - 2026-02-06
|
|
4
|
+
|
|
5
|
+
### Refactoring
|
|
6
|
+
|
|
7
|
+
- standardize on fs/promises across codebase (PRJ-93) (#118)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.5.1] - 2026-02-06
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Standardize on fs/promises across codebase (PRJ-93)**: Replaced all synchronous `fs` operations (`existsSync`, `readFileSync`, `writeFileSync`, `mkdirSync`, etc.) with async `fs/promises` equivalents across 22 files
|
|
15
|
+
|
|
16
|
+
### Implementation Details
|
|
17
|
+
|
|
18
|
+
- Created shared `fileExists()` utility in `core/utils/fs-helpers.ts` replacing `existsSync` with `fs.access`
|
|
19
|
+
- Converted all detection functions in `ai-provider.ts` to async with `Promise.all` for parallel checks
|
|
20
|
+
- Applied lazy initialization pattern in `CommandInstaller` to handle async `getActiveProvider()` in constructor
|
|
21
|
+
- Replaced `execSync` with `promisify(exec)` in `registry.ts` and `ai-provider.ts`
|
|
22
|
+
- Converted `setup.ts` (~60 sync ops), `hooks-service.ts` (~30 sync ops), and all command/CLI files
|
|
23
|
+
- Updated prompt-builder and command-executor tests to handle async `build()` and `signalStart/End()`
|
|
24
|
+
- Intentional exceptions: `version.ts` (module-level constants), `jsonl-helper.ts` (`createReadStream`), test files
|
|
25
|
+
|
|
26
|
+
### Learnings
|
|
27
|
+
|
|
28
|
+
- Module-level constants (`VERSION`, `PACKAGE_ROOT`) cannot use async — sync reads at import time are a valid exception
|
|
29
|
+
- `createReadStream` is inherently sync (returns a stream) — the correct pattern for streaming reads
|
|
30
|
+
- Making a function async cascades to all callers — `ai-provider.ts` changes rippled to 10+ files
|
|
31
|
+
- Constructor methods can't be async — solved with lazy `ensureInit()` pattern in `CommandInstaller`
|
|
32
|
+
|
|
33
|
+
### Test Plan
|
|
34
|
+
|
|
35
|
+
#### For QA
|
|
36
|
+
1. Run `bun run build` — verify clean build with no errors
|
|
37
|
+
2. Run `bun test` — verify all 416 tests pass
|
|
38
|
+
3. Run `prjct sync` on a project — verify async fs operations work correctly
|
|
39
|
+
4. Run `prjct start` — verify setup flow works with async file operations
|
|
40
|
+
5. Verify `prjct linear list` works (uses converted linear/sync.ts)
|
|
41
|
+
|
|
42
|
+
#### For Users
|
|
43
|
+
**What changed:** Internal refactor — no user-facing API changes. All sync filesystem operations replaced with async equivalents for better performance.
|
|
44
|
+
**How to use:** No changes needed. All commands work identically.
|
|
45
|
+
**Breaking changes:** None
|
|
46
|
+
|
|
47
|
+
## [1.5.0] - 2026-02-06
|
|
48
|
+
|
|
49
|
+
### Features
|
|
50
|
+
|
|
51
|
+
- add citation format for context sources in templates (PRJ-113) (#117)
|
|
52
|
+
|
|
53
|
+
## [1.4.2] - 2026-02-06
|
|
54
|
+
|
|
55
|
+
### Features
|
|
56
|
+
|
|
57
|
+
- **Source citations in context files (PRJ-113)**: All generated context files now include `<!-- source: file, type -->` HTML comments showing where each section's data was detected from
|
|
58
|
+
|
|
59
|
+
### Implementation Details
|
|
60
|
+
|
|
61
|
+
- Added `SourceInfo` type and `ContextSources` interface with `cite()` helper (`core/utils/citations.ts`)
|
|
62
|
+
- Extended `ProjectContext` with optional `sources` field — backward compatible, falls back to `defaultSources()`
|
|
63
|
+
- Added `buildSources()` to `sync-service.ts` — maps detected ecosystem/commands data to their source files (package.json, lock files, Cargo.toml, etc.)
|
|
64
|
+
- Citations added to 4 markdown formatters: Claude, Cursor, Windsurf, Copilot. Continue.dev skipped (JSON has no comment syntax)
|
|
65
|
+
- Context generator CLAUDE.md also updated with citation support
|
|
66
|
+
- Source types: `detected` (from files), `user-defined` (from config), `inferred` (from heuristics)
|
|
67
|
+
|
|
68
|
+
### Learnings
|
|
69
|
+
|
|
70
|
+
- `context-generator.ts` and `formatters.ts` both independently generate CLAUDE.md content — both must be updated for consistent citations
|
|
71
|
+
- Sources can be determined post-detection from data values rather than threading metadata through every detection method
|
|
72
|
+
- Optional fields with fallback defaults (`sources?`) maintain backward compatibility without breaking existing callers
|
|
73
|
+
|
|
74
|
+
### Test Plan
|
|
75
|
+
|
|
76
|
+
#### For QA
|
|
77
|
+
1. Run `prjct sync --yes` — verify generated context files contain `<!-- source: ... -->` comments
|
|
78
|
+
2. Check CLAUDE.md citations before: THIS PROJECT, Commands, Code Conventions, PROJECT STATE
|
|
79
|
+
3. Check `.cursor/rules/prjct.mdc` citations before Tech Stack and Commands
|
|
80
|
+
4. Check `.windsurf/rules/prjct.md` citations before Stack and Commands
|
|
81
|
+
5. Check `.github/copilot-instructions.md` citations before Project Info and Commands
|
|
82
|
+
6. Verify `.continue/config.json` unchanged (JSON has no comments)
|
|
83
|
+
7. Run `bun test` — all 416 tests pass
|
|
84
|
+
|
|
85
|
+
#### For Users
|
|
86
|
+
**What changed:** Context files now show where data came from via HTML comments
|
|
87
|
+
**How to use:** Run `p. sync` — citations appear automatically
|
|
88
|
+
**Breaking changes:** None
|
|
89
|
+
|
|
90
|
+
## [1.4.1] - 2026-02-06
|
|
91
|
+
|
|
92
|
+
### Improvements
|
|
93
|
+
|
|
94
|
+
- **Better error messages for invalid commands (PRJ-98)**: Consistent, helpful CLI errors with did-you-mean suggestions and required parameter validation
|
|
95
|
+
|
|
96
|
+
### Implementation Details
|
|
97
|
+
|
|
98
|
+
- Added `UNKNOWN_COMMAND` and `MISSING_PARAM` error codes to centralized error catalog (`core/utils/error-messages.ts`)
|
|
99
|
+
- Added `validateCommandParams()` — parses `CommandMeta.params` convention (`<required>` vs `[optional]`) and validates against actual CLI args before command execution
|
|
100
|
+
- Added `findClosestCommand()` with Levenshtein edit distance (threshold ≤ 2) for did-you-mean suggestions on typos
|
|
101
|
+
- All error paths now use `out.failWithHint()` for consistent formatting with hints, docs links, and file references
|
|
102
|
+
- Deprecated and unimplemented command errors also upgraded to use `failWithHint()`
|
|
103
|
+
|
|
104
|
+
### Learnings
|
|
105
|
+
|
|
106
|
+
- `bin/prjct.ts` intercepts many commands (start, context, hooks, doctor, etc.) before `core/index.ts` — changes to dispatch only affect commands that reach core
|
|
107
|
+
- Template-only commands (e.g. `task`) are defined in `command-data.ts` but not registered in the command registry — they don't get param validation via CLI
|
|
108
|
+
- Levenshtein edit distance is simple to implement (~15 lines) and effective for CLI typo suggestions
|
|
109
|
+
|
|
110
|
+
### Test Plan
|
|
111
|
+
|
|
112
|
+
#### For QA
|
|
113
|
+
1. `prjct xyzzy` → "Unknown command: xyzzy" with help hint
|
|
114
|
+
2. `prjct snyc` → "Did you mean 'prjct sync'?"
|
|
115
|
+
3. `prjct shp` → "Did you mean 'prjct ship'?"
|
|
116
|
+
4. `prjct bug` (no args) → "Missing required parameter: description" with usage
|
|
117
|
+
5. `prjct idea` (no args) → "Missing required parameter: description" with usage
|
|
118
|
+
6. `prjct sync --yes` → works normally (no regression)
|
|
119
|
+
7. `prjct dash compact` → works normally (no regression)
|
|
120
|
+
|
|
121
|
+
#### For Users
|
|
122
|
+
**What changed:** CLI now shows helpful error messages with suggestions when you mistype a command or forget a required argument.
|
|
123
|
+
**How to use:** Just use prjct normally — errors are now more helpful automatically.
|
|
124
|
+
**Breaking changes:** None
|
|
125
|
+
|
|
3
126
|
## [1.4.0] - 2026-02-06
|
|
4
127
|
|
|
5
128
|
### Features
|
|
6
129
|
|
|
7
130
|
- programmatic verification checks for sync workflow (PRJ-106) (#115)
|
|
8
131
|
|
|
9
|
-
|
|
10
132
|
## [1.3.1] - 2026-02-06
|
|
11
133
|
|
|
12
134
|
### Features
|
package/bin/prjct.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* auto-install on first CLI use. This is the reliable path.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import fs from 'node:fs'
|
|
12
11
|
import os from 'node:os'
|
|
13
12
|
import path from 'node:path'
|
|
14
13
|
import chalk from 'chalk'
|
|
@@ -16,20 +15,21 @@ import { detectAllProviders } from '../core/infrastructure/ai-provider'
|
|
|
16
15
|
import configManager from '../core/infrastructure/config-manager'
|
|
17
16
|
import editorsConfig from '../core/infrastructure/editors-config'
|
|
18
17
|
import { DEFAULT_PORT, startServer } from '../core/server/server'
|
|
18
|
+
import { fileExists } from '../core/utils/fs-helpers'
|
|
19
19
|
import { VERSION } from '../core/utils/version'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Check if routers are installed for detected providers
|
|
23
23
|
* Returns true if at least one provider has its router installed
|
|
24
24
|
*/
|
|
25
|
-
function checkRoutersInstalled(): boolean {
|
|
25
|
+
async function checkRoutersInstalled(): Promise<boolean> {
|
|
26
26
|
const home = os.homedir()
|
|
27
|
-
const detection = detectAllProviders()
|
|
27
|
+
const detection = await detectAllProviders()
|
|
28
28
|
|
|
29
29
|
// Check Claude router
|
|
30
30
|
if (detection.claude.installed) {
|
|
31
31
|
const claudeRouter = path.join(home, '.claude', 'commands', 'p.md')
|
|
32
|
-
if (!
|
|
32
|
+
if (!(await fileExists(claudeRouter))) {
|
|
33
33
|
return false
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -37,7 +37,7 @@ function checkRoutersInstalled(): boolean {
|
|
|
37
37
|
// Check Gemini router
|
|
38
38
|
if (detection.gemini.installed) {
|
|
39
39
|
const geminiRouter = path.join(home, '.gemini', 'commands', 'p.toml')
|
|
40
|
-
if (!
|
|
40
|
+
if (!(await fileExists(geminiRouter))) {
|
|
41
41
|
return false
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -218,15 +218,24 @@ if (args[0] === 'start' || args[0] === 'setup') {
|
|
|
218
218
|
process.exitCode = 0
|
|
219
219
|
} else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
|
|
220
220
|
// Show version with provider status
|
|
221
|
-
const detection = detectAllProviders()
|
|
221
|
+
const detection = await detectAllProviders()
|
|
222
222
|
const home = os.homedir()
|
|
223
223
|
const cwd = process.cwd()
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
const [
|
|
225
|
+
claudeConfigured,
|
|
226
|
+
geminiConfigured,
|
|
227
|
+
cursorDetected,
|
|
228
|
+
cursorConfigured,
|
|
229
|
+
windsurfDetected,
|
|
230
|
+
windsurfConfigured,
|
|
231
|
+
] = await Promise.all([
|
|
232
|
+
fileExists(path.join(home, '.claude', 'commands', 'p.md')),
|
|
233
|
+
fileExists(path.join(home, '.gemini', 'commands', 'p.toml')),
|
|
234
|
+
fileExists(path.join(cwd, '.cursor')),
|
|
235
|
+
fileExists(path.join(cwd, '.cursor', 'rules', 'prjct.mdc')),
|
|
236
|
+
fileExists(path.join(cwd, '.windsurf')),
|
|
237
|
+
fileExists(path.join(cwd, '.windsurf', 'rules', 'prjct.md')),
|
|
238
|
+
])
|
|
230
239
|
|
|
231
240
|
console.log(`
|
|
232
241
|
${chalk.cyan('p/')} prjct v${VERSION}
|
|
@@ -276,9 +285,9 @@ ${chalk.cyan('https://prjct.app')}
|
|
|
276
285
|
} else {
|
|
277
286
|
// Check if setup has been done
|
|
278
287
|
const configPath = path.join(os.homedir(), '.prjct-cli', 'config', 'installed-editors.json')
|
|
279
|
-
const routersInstalled = checkRoutersInstalled()
|
|
288
|
+
const routersInstalled = await checkRoutersInstalled()
|
|
280
289
|
|
|
281
|
-
if (!
|
|
290
|
+
if (!(await fileExists(configPath)) || !routersInstalled) {
|
|
282
291
|
// First time - prompt to run start
|
|
283
292
|
console.log(`
|
|
284
293
|
${chalk.cyan.bold(' Welcome to prjct!')}
|
|
@@ -106,40 +106,40 @@ describe('CommandExecutor', () => {
|
|
|
106
106
|
}
|
|
107
107
|
})
|
|
108
108
|
|
|
109
|
-
it('should create status file with command name', () => {
|
|
110
|
-
signalStart('test-command')
|
|
109
|
+
it('should create status file with command name', async () => {
|
|
110
|
+
await signalStart('test-command')
|
|
111
111
|
|
|
112
112
|
expect(fs.existsSync(RUNNING_FILE)).toBe(true)
|
|
113
113
|
const content = fs.readFileSync(RUNNING_FILE, 'utf-8')
|
|
114
114
|
expect(content).toBe('/p:test-command')
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
it('should overwrite existing status file', () => {
|
|
118
|
-
signalStart('first-command')
|
|
119
|
-
signalStart('second-command')
|
|
117
|
+
it('should overwrite existing status file', async () => {
|
|
118
|
+
await signalStart('first-command')
|
|
119
|
+
await signalStart('second-command')
|
|
120
120
|
|
|
121
121
|
const content = fs.readFileSync(RUNNING_FILE, 'utf-8')
|
|
122
122
|
expect(content).toBe('/p:second-command')
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
it('should handle filesystem errors gracefully', () => {
|
|
125
|
+
it('should handle filesystem errors gracefully', async () => {
|
|
126
126
|
// This test verifies that errors are silently ignored
|
|
127
127
|
// We can't easily simulate fs errors, but we can verify the function doesn't throw
|
|
128
|
-
expect(
|
|
128
|
+
await expect(signalStart('test-command')).resolves.toBeUndefined()
|
|
129
129
|
})
|
|
130
130
|
})
|
|
131
131
|
|
|
132
132
|
describe('signalEnd', () => {
|
|
133
|
-
it('should remove status file if it exists', () => {
|
|
133
|
+
it('should remove status file if it exists', async () => {
|
|
134
134
|
// Create the file first
|
|
135
|
-
signalStart('test-command')
|
|
135
|
+
await signalStart('test-command')
|
|
136
136
|
expect(fs.existsSync(RUNNING_FILE)).toBe(true)
|
|
137
137
|
|
|
138
|
-
signalEnd()
|
|
138
|
+
await signalEnd()
|
|
139
139
|
expect(fs.existsSync(RUNNING_FILE)).toBe(false)
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
-
it('should not throw if file does not exist', () => {
|
|
142
|
+
it('should not throw if file does not exist', async () => {
|
|
143
143
|
// Ensure file doesn't exist
|
|
144
144
|
try {
|
|
145
145
|
fs.unlinkSync(RUNNING_FILE)
|
|
@@ -147,7 +147,7 @@ describe('CommandExecutor', () => {
|
|
|
147
147
|
// Ignore
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
expect(
|
|
150
|
+
await expect(signalEnd()).resolves.toBeUndefined()
|
|
151
151
|
})
|
|
152
152
|
})
|
|
153
153
|
|
|
@@ -158,20 +158,20 @@ describe('CommandExecutor', () => {
|
|
|
158
158
|
executor = new CommandExecutor()
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
it('should have signalStart method that calls module function', () => {
|
|
162
|
-
executor.signalStart('class-test')
|
|
161
|
+
it('should have signalStart method that calls module function', async () => {
|
|
162
|
+
await executor.signalStart('class-test')
|
|
163
163
|
|
|
164
164
|
expect(fs.existsSync(RUNNING_FILE)).toBe(true)
|
|
165
165
|
const content = fs.readFileSync(RUNNING_FILE, 'utf-8')
|
|
166
166
|
expect(content).toBe('/p:class-test')
|
|
167
167
|
|
|
168
168
|
// Cleanup
|
|
169
|
-
executor.signalEnd()
|
|
169
|
+
await executor.signalEnd()
|
|
170
170
|
})
|
|
171
171
|
|
|
172
|
-
it('should have signalEnd method that calls module function', () => {
|
|
173
|
-
executor.signalStart('class-test')
|
|
174
|
-
executor.signalEnd()
|
|
172
|
+
it('should have signalEnd method that calls module function', async () => {
|
|
173
|
+
await executor.signalStart('class-test')
|
|
174
|
+
await executor.signalEnd()
|
|
175
175
|
|
|
176
176
|
expect(fs.existsSync(RUNNING_FILE)).toBe(false)
|
|
177
177
|
})
|
|
@@ -336,7 +336,7 @@ describe('execute', () => {
|
|
|
336
336
|
memorySystem.getRelevantMemories = mock(() => Promise.resolve([]))
|
|
337
337
|
|
|
338
338
|
// Mock promptBuilder
|
|
339
|
-
promptBuilder.build = mock(() => 'built prompt')
|
|
339
|
+
promptBuilder.build = mock(() => Promise.resolve('built prompt'))
|
|
340
340
|
})
|
|
341
341
|
|
|
342
342
|
afterEach(() => {
|
|
@@ -96,7 +96,7 @@ describe('PromptBuilder', () => {
|
|
|
96
96
|
})
|
|
97
97
|
|
|
98
98
|
describe('Context Filtering by Command Type', () => {
|
|
99
|
-
it('should include patterns for code commands', () => {
|
|
99
|
+
it('should include patterns for code commands', async () => {
|
|
100
100
|
const template = {
|
|
101
101
|
frontmatter: { description: 'Build feature', name: 'p:build' },
|
|
102
102
|
content: '## Flow\nBuild something',
|
|
@@ -105,13 +105,13 @@ describe('PromptBuilder', () => {
|
|
|
105
105
|
const context = { projectPath: '/test', files: ['file1.js'] }
|
|
106
106
|
const state = { analysis: 'Stack: Node.js, React' }
|
|
107
107
|
|
|
108
|
-
const prompt = builder.build(template, context, state)
|
|
108
|
+
const prompt = await builder.build(template, context, state)
|
|
109
109
|
|
|
110
110
|
expect(prompt).toContain('PATTERNS')
|
|
111
111
|
expect(prompt).toContain('Node.js')
|
|
112
112
|
})
|
|
113
113
|
|
|
114
|
-
it('should NOT include patterns for non-code commands', () => {
|
|
114
|
+
it('should NOT include patterns for non-code commands', async () => {
|
|
115
115
|
const template = {
|
|
116
116
|
frontmatter: { description: 'Show current task', name: 'p:now' },
|
|
117
117
|
content: '## Flow\nShow task',
|
|
@@ -120,14 +120,14 @@ describe('PromptBuilder', () => {
|
|
|
120
120
|
const context = { projectPath: '/test', files: ['file1.js'] }
|
|
121
121
|
const state = { analysis: 'Stack: Node.js, React' }
|
|
122
122
|
|
|
123
|
-
const prompt = builder.build(template, context, state)
|
|
123
|
+
const prompt = await builder.build(template, context, state)
|
|
124
124
|
|
|
125
125
|
expect(prompt).not.toContain('## PATTERNS')
|
|
126
126
|
})
|
|
127
127
|
})
|
|
128
128
|
|
|
129
129
|
describe('Project Files Listing', () => {
|
|
130
|
-
it('should list available files when context has files', () => {
|
|
130
|
+
it('should list available files when context has files', async () => {
|
|
131
131
|
const template = {
|
|
132
132
|
frontmatter: { description: 'Test command' },
|
|
133
133
|
content: '## Flow\nDo something',
|
|
@@ -140,7 +140,7 @@ describe('PromptBuilder', () => {
|
|
|
140
140
|
|
|
141
141
|
const state = {}
|
|
142
142
|
|
|
143
|
-
const prompt = builder.build(template, context, state)
|
|
143
|
+
const prompt = await builder.build(template, context, state)
|
|
144
144
|
|
|
145
145
|
expect(prompt).toContain('## FILES:')
|
|
146
146
|
expect(prompt).toContain('3 available')
|
|
@@ -148,7 +148,7 @@ describe('PromptBuilder', () => {
|
|
|
148
148
|
expect(prompt).toContain('Read')
|
|
149
149
|
})
|
|
150
150
|
|
|
151
|
-
it('should show project path when no files listed', () => {
|
|
151
|
+
it('should show project path when no files listed', async () => {
|
|
152
152
|
const template = {
|
|
153
153
|
frontmatter: { description: 'Test command' },
|
|
154
154
|
content: '## Flow\nDo something',
|
|
@@ -157,7 +157,7 @@ describe('PromptBuilder', () => {
|
|
|
157
157
|
const context = { projectPath: '/test/project' }
|
|
158
158
|
const state = {}
|
|
159
159
|
|
|
160
|
-
const prompt = builder.build(template, context, state)
|
|
160
|
+
const prompt = await builder.build(template, context, state)
|
|
161
161
|
|
|
162
162
|
expect(prompt).toContain('## PROJECT:')
|
|
163
163
|
expect(prompt).toContain('/test/project')
|
|
@@ -165,7 +165,7 @@ describe('PromptBuilder', () => {
|
|
|
165
165
|
})
|
|
166
166
|
|
|
167
167
|
describe('Build Complete Prompt', () => {
|
|
168
|
-
it('should include all critical sections', () => {
|
|
168
|
+
it('should include all critical sections', async () => {
|
|
169
169
|
const template = {
|
|
170
170
|
frontmatter: {
|
|
171
171
|
description: 'Test command',
|
|
@@ -182,7 +182,7 @@ describe('PromptBuilder', () => {
|
|
|
182
182
|
|
|
183
183
|
const state = { now: '# NOW\n\n**Current task**' }
|
|
184
184
|
|
|
185
|
-
const prompt = builder.build(template, context, state)
|
|
185
|
+
const prompt = await builder.build(template, context, state)
|
|
186
186
|
|
|
187
187
|
expect(prompt).toContain('TASK:')
|
|
188
188
|
expect(prompt).toContain('TOOLS:')
|
|
@@ -191,7 +191,7 @@ describe('PromptBuilder', () => {
|
|
|
191
191
|
expect(prompt).toContain('## FILES:')
|
|
192
192
|
})
|
|
193
193
|
|
|
194
|
-
it('should be concise (under 2000 chars for simple prompt)', () => {
|
|
194
|
+
it('should be concise (under 2000 chars for simple prompt)', async () => {
|
|
195
195
|
const template = {
|
|
196
196
|
frontmatter: { description: 'Test', 'allowed-tools': ['Read'] },
|
|
197
197
|
content: '## Flow\n1. Test',
|
|
@@ -200,14 +200,14 @@ describe('PromptBuilder', () => {
|
|
|
200
200
|
const context = { projectPath: '/test', files: ['a.js'] }
|
|
201
201
|
const state = {}
|
|
202
202
|
|
|
203
|
-
const prompt = builder.build(template, context, state)
|
|
203
|
+
const prompt = await builder.build(template, context, state)
|
|
204
204
|
|
|
205
205
|
expect(prompt.length).toBeLessThan(2000)
|
|
206
206
|
})
|
|
207
207
|
})
|
|
208
208
|
|
|
209
209
|
describe('Plan Mode (Compressed)', () => {
|
|
210
|
-
it('should include compact plan mode instructions', () => {
|
|
210
|
+
it('should include compact plan mode instructions', async () => {
|
|
211
211
|
const template = {
|
|
212
212
|
frontmatter: { description: 'Test' },
|
|
213
213
|
content: '## Flow\nTest',
|
|
@@ -217,14 +217,14 @@ describe('PromptBuilder', () => {
|
|
|
217
217
|
const state = {}
|
|
218
218
|
const planInfo = { isPlanning: true, allowedTools: ['Read', 'Glob'] }
|
|
219
219
|
|
|
220
|
-
const prompt = builder.build(template, context, state, null, null, null, null, planInfo)
|
|
220
|
+
const prompt = await builder.build(template, context, state, null, null, null, null, planInfo)
|
|
221
221
|
|
|
222
222
|
expect(prompt).toContain('PLAN MODE')
|
|
223
223
|
expect(prompt).toContain('Read-only')
|
|
224
224
|
expect(prompt).toContain('Tools: Read, Glob')
|
|
225
225
|
})
|
|
226
226
|
|
|
227
|
-
it('should include approval required section', () => {
|
|
227
|
+
it('should include approval required section', async () => {
|
|
228
228
|
const template = {
|
|
229
229
|
frontmatter: { description: 'Test' },
|
|
230
230
|
content: '## Flow\nTest',
|
|
@@ -234,7 +234,7 @@ describe('PromptBuilder', () => {
|
|
|
234
234
|
const state = {}
|
|
235
235
|
const planInfo = { requiresApproval: true }
|
|
236
236
|
|
|
237
|
-
const prompt = builder.build(template, context, state, null, null, null, null, planInfo)
|
|
237
|
+
const prompt = await builder.build(template, context, state, null, null, null, null, planInfo)
|
|
238
238
|
|
|
239
239
|
expect(prompt).toContain('APPROVAL REQUIRED')
|
|
240
240
|
expect(prompt).toContain('confirmation')
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Tests for AI Tools Formatters
|
|
3
3
|
*
|
|
4
4
|
* @see PRJ-122
|
|
5
|
+
* @see PRJ-113 (citation support)
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { describe, expect, test } from 'bun:test'
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type ProjectContext,
|
|
16
17
|
} from '../../ai-tools/formatters'
|
|
17
18
|
import { AI_TOOLS, getAIToolConfig } from '../../ai-tools/registry'
|
|
19
|
+
import { type ContextSources, cite, defaultSources } from '../../utils/citations'
|
|
18
20
|
|
|
19
21
|
// =============================================================================
|
|
20
22
|
// Test Fixtures
|
|
@@ -356,3 +358,119 @@ describe('Formatter Edge Cases', () => {
|
|
|
356
358
|
expect(claudeResult).toContain('@scope/my-project')
|
|
357
359
|
})
|
|
358
360
|
})
|
|
361
|
+
|
|
362
|
+
// =============================================================================
|
|
363
|
+
// Citation Tests (PRJ-113)
|
|
364
|
+
// =============================================================================
|
|
365
|
+
|
|
366
|
+
const mockSources: ContextSources = {
|
|
367
|
+
name: { file: 'package.json', type: 'detected' },
|
|
368
|
+
version: { file: 'package.json', type: 'detected' },
|
|
369
|
+
ecosystem: { file: 'package.json', type: 'detected' },
|
|
370
|
+
languages: { file: 'package.json', type: 'detected' },
|
|
371
|
+
frameworks: { file: 'package.json', type: 'detected' },
|
|
372
|
+
commands: { file: 'bun.lockb', type: 'detected' },
|
|
373
|
+
projectType: { file: 'file count + frameworks', type: 'inferred' },
|
|
374
|
+
git: { file: 'git', type: 'detected' },
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const ctxWithSources: ProjectContext = {
|
|
378
|
+
...mockContext,
|
|
379
|
+
sources: mockSources,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
describe('cite() helper', () => {
|
|
383
|
+
test('generates HTML comment with file and type', () => {
|
|
384
|
+
const result = cite({ file: 'package.json', type: 'detected' })
|
|
385
|
+
expect(result).toBe('<!-- source: package.json, detected -->')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
test('supports inferred type', () => {
|
|
389
|
+
const result = cite({ file: 'file count + frameworks', type: 'inferred' })
|
|
390
|
+
expect(result).toBe('<!-- source: file count + frameworks, inferred -->')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('supports user-defined type', () => {
|
|
394
|
+
const result = cite({ file: 'prjct.yaml', type: 'user-defined' })
|
|
395
|
+
expect(result).toBe('<!-- source: prjct.yaml, user-defined -->')
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
describe('defaultSources()', () => {
|
|
400
|
+
test('returns all source fields', () => {
|
|
401
|
+
const sources = defaultSources()
|
|
402
|
+
expect(sources.name).toBeDefined()
|
|
403
|
+
expect(sources.version).toBeDefined()
|
|
404
|
+
expect(sources.ecosystem).toBeDefined()
|
|
405
|
+
expect(sources.languages).toBeDefined()
|
|
406
|
+
expect(sources.frameworks).toBeDefined()
|
|
407
|
+
expect(sources.commands).toBeDefined()
|
|
408
|
+
expect(sources.projectType).toBeDefined()
|
|
409
|
+
expect(sources.git).toBeDefined()
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('git source is always "git"', () => {
|
|
413
|
+
const sources = defaultSources()
|
|
414
|
+
expect(sources.git.file).toBe('git')
|
|
415
|
+
expect(sources.git.type).toBe('detected')
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
describe('Citation integration', () => {
|
|
420
|
+
test('Claude format includes source citations', () => {
|
|
421
|
+
const result = formatForClaude(ctxWithSources, AI_TOOLS.claude)
|
|
422
|
+
|
|
423
|
+
expect(result).toContain('<!-- source: package.json, detected -->')
|
|
424
|
+
expect(result).toContain('<!-- source: bun.lockb, detected -->')
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('Cursor format includes source citations', () => {
|
|
428
|
+
const result = formatForCursor(ctxWithSources, AI_TOOLS.cursor)
|
|
429
|
+
|
|
430
|
+
expect(result).toContain('<!-- source: package.json, detected -->')
|
|
431
|
+
expect(result).toContain('<!-- source: bun.lockb, detected -->')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
test('Windsurf format includes source citations', () => {
|
|
435
|
+
const result = formatForWindsurf(ctxWithSources, AI_TOOLS.windsurf)
|
|
436
|
+
|
|
437
|
+
expect(result).toContain('<!-- source: package.json, detected -->')
|
|
438
|
+
expect(result).toContain('<!-- source: bun.lockb, detected -->')
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test('Copilot format includes source citations', () => {
|
|
442
|
+
const result = formatForCopilot(ctxWithSources, AI_TOOLS.copilot)
|
|
443
|
+
|
|
444
|
+
expect(result).toContain('<!-- source: package.json, detected -->')
|
|
445
|
+
expect(result).toContain('<!-- source: bun.lockb, detected -->')
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
test('formatters work without sources (backward compatible)', () => {
|
|
449
|
+
const ctxNoSources = { ...mockContext }
|
|
450
|
+
delete ctxNoSources.sources
|
|
451
|
+
|
|
452
|
+
// Should not throw
|
|
453
|
+
expect(() => formatForClaude(ctxNoSources, AI_TOOLS.claude)).not.toThrow()
|
|
454
|
+
expect(() => formatForCursor(ctxNoSources, AI_TOOLS.cursor)).not.toThrow()
|
|
455
|
+
expect(() => formatForWindsurf(ctxNoSources, AI_TOOLS.windsurf)).not.toThrow()
|
|
456
|
+
expect(() => formatForCopilot(ctxNoSources, AI_TOOLS.copilot)).not.toThrow()
|
|
457
|
+
|
|
458
|
+
// Should still contain default citation comments
|
|
459
|
+
const result = formatForClaude(ctxNoSources, AI_TOOLS.claude)
|
|
460
|
+
expect(result).toContain('<!-- source:')
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test('Claude format has citations before each major section', () => {
|
|
464
|
+
const result = formatForClaude(ctxWithSources, AI_TOOLS.claude)
|
|
465
|
+
|
|
466
|
+
// Ecosystem citation before project type
|
|
467
|
+
const ecosystemIdx = result.indexOf('<!-- source: package.json, detected -->')
|
|
468
|
+
const projectTypeIdx = result.indexOf('**Type:**')
|
|
469
|
+
expect(ecosystemIdx).toBeLessThan(projectTypeIdx)
|
|
470
|
+
|
|
471
|
+
// Commands citation before commands table
|
|
472
|
+
const commandsCiteIdx = result.indexOf('<!-- source: bun.lockb, detected -->')
|
|
473
|
+
const commandsTableIdx = result.indexOf('| Action | Command |')
|
|
474
|
+
expect(commandsCiteIdx).toBeLessThan(commandsTableIdx)
|
|
475
|
+
})
|
|
476
|
+
})
|