prjct-cli 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -0
- package/core/__tests__/ai-tools/formatters.test.ts +118 -0
- package/core/ai-tools/formatters.ts +18 -0
- package/core/commands/analysis.ts +22 -0
- package/core/index.ts +91 -10
- package/core/services/context-generator.ts +12 -3
- package/core/services/sync-service.ts +77 -3
- package/core/services/sync-verifier.ts +273 -0
- package/core/types/config.ts +14 -0
- package/core/utils/citations.ts +53 -0
- package/core/utils/error-messages.ts +11 -0
- package/dist/bin/prjct.mjs +721 -342
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,139 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.0] - 2026-02-06
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add citation format for context sources in templates (PRJ-113) (#117)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.4.2] - 2026-02-06
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **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
|
|
15
|
+
|
|
16
|
+
### Implementation Details
|
|
17
|
+
|
|
18
|
+
- Added `SourceInfo` type and `ContextSources` interface with `cite()` helper (`core/utils/citations.ts`)
|
|
19
|
+
- Extended `ProjectContext` with optional `sources` field — backward compatible, falls back to `defaultSources()`
|
|
20
|
+
- Added `buildSources()` to `sync-service.ts` — maps detected ecosystem/commands data to their source files (package.json, lock files, Cargo.toml, etc.)
|
|
21
|
+
- Citations added to 4 markdown formatters: Claude, Cursor, Windsurf, Copilot. Continue.dev skipped (JSON has no comment syntax)
|
|
22
|
+
- Context generator CLAUDE.md also updated with citation support
|
|
23
|
+
- Source types: `detected` (from files), `user-defined` (from config), `inferred` (from heuristics)
|
|
24
|
+
|
|
25
|
+
### Learnings
|
|
26
|
+
|
|
27
|
+
- `context-generator.ts` and `formatters.ts` both independently generate CLAUDE.md content — both must be updated for consistent citations
|
|
28
|
+
- Sources can be determined post-detection from data values rather than threading metadata through every detection method
|
|
29
|
+
- Optional fields with fallback defaults (`sources?`) maintain backward compatibility without breaking existing callers
|
|
30
|
+
|
|
31
|
+
### Test Plan
|
|
32
|
+
|
|
33
|
+
#### For QA
|
|
34
|
+
1. Run `prjct sync --yes` — verify generated context files contain `<!-- source: ... -->` comments
|
|
35
|
+
2. Check CLAUDE.md citations before: THIS PROJECT, Commands, Code Conventions, PROJECT STATE
|
|
36
|
+
3. Check `.cursor/rules/prjct.mdc` citations before Tech Stack and Commands
|
|
37
|
+
4. Check `.windsurf/rules/prjct.md` citations before Stack and Commands
|
|
38
|
+
5. Check `.github/copilot-instructions.md` citations before Project Info and Commands
|
|
39
|
+
6. Verify `.continue/config.json` unchanged (JSON has no comments)
|
|
40
|
+
7. Run `bun test` — all 416 tests pass
|
|
41
|
+
|
|
42
|
+
#### For Users
|
|
43
|
+
**What changed:** Context files now show where data came from via HTML comments
|
|
44
|
+
**How to use:** Run `p. sync` — citations appear automatically
|
|
45
|
+
**Breaking changes:** None
|
|
46
|
+
|
|
47
|
+
## [1.4.1] - 2026-02-06
|
|
48
|
+
|
|
49
|
+
### Improvements
|
|
50
|
+
|
|
51
|
+
- **Better error messages for invalid commands (PRJ-98)**: Consistent, helpful CLI errors with did-you-mean suggestions and required parameter validation
|
|
52
|
+
|
|
53
|
+
### Implementation Details
|
|
54
|
+
|
|
55
|
+
- Added `UNKNOWN_COMMAND` and `MISSING_PARAM` error codes to centralized error catalog (`core/utils/error-messages.ts`)
|
|
56
|
+
- Added `validateCommandParams()` — parses `CommandMeta.params` convention (`<required>` vs `[optional]`) and validates against actual CLI args before command execution
|
|
57
|
+
- Added `findClosestCommand()` with Levenshtein edit distance (threshold ≤ 2) for did-you-mean suggestions on typos
|
|
58
|
+
- All error paths now use `out.failWithHint()` for consistent formatting with hints, docs links, and file references
|
|
59
|
+
- Deprecated and unimplemented command errors also upgraded to use `failWithHint()`
|
|
60
|
+
|
|
61
|
+
### Learnings
|
|
62
|
+
|
|
63
|
+
- `bin/prjct.ts` intercepts many commands (start, context, hooks, doctor, etc.) before `core/index.ts` — changes to dispatch only affect commands that reach core
|
|
64
|
+
- 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
|
|
65
|
+
- Levenshtein edit distance is simple to implement (~15 lines) and effective for CLI typo suggestions
|
|
66
|
+
|
|
67
|
+
### Test Plan
|
|
68
|
+
|
|
69
|
+
#### For QA
|
|
70
|
+
1. `prjct xyzzy` → "Unknown command: xyzzy" with help hint
|
|
71
|
+
2. `prjct snyc` → "Did you mean 'prjct sync'?"
|
|
72
|
+
3. `prjct shp` → "Did you mean 'prjct ship'?"
|
|
73
|
+
4. `prjct bug` (no args) → "Missing required parameter: description" with usage
|
|
74
|
+
5. `prjct idea` (no args) → "Missing required parameter: description" with usage
|
|
75
|
+
6. `prjct sync --yes` → works normally (no regression)
|
|
76
|
+
7. `prjct dash compact` → works normally (no regression)
|
|
77
|
+
|
|
78
|
+
#### For Users
|
|
79
|
+
**What changed:** CLI now shows helpful error messages with suggestions when you mistype a command or forget a required argument.
|
|
80
|
+
**How to use:** Just use prjct normally — errors are now more helpful automatically.
|
|
81
|
+
**Breaking changes:** None
|
|
82
|
+
|
|
83
|
+
## [1.4.0] - 2026-02-06
|
|
84
|
+
|
|
85
|
+
### Features
|
|
86
|
+
|
|
87
|
+
- programmatic verification checks for sync workflow (PRJ-106) (#115)
|
|
88
|
+
|
|
89
|
+
## [1.3.1] - 2026-02-06
|
|
90
|
+
|
|
91
|
+
### Features
|
|
92
|
+
|
|
93
|
+
- **Programmatic verification checks for sync workflow (PRJ-106)**: Post-sync validation with built-in and custom checks
|
|
94
|
+
|
|
95
|
+
### Implementation Details
|
|
96
|
+
|
|
97
|
+
New `SyncVerifier` service (`core/services/sync-verifier.ts`) that runs verification checks after every sync. Three built-in checks run automatically:
|
|
98
|
+
- **Context files exist** — verifies `context/CLAUDE.md` was generated
|
|
99
|
+
- **JSON files valid** — validates `storage/state.json` syntax
|
|
100
|
+
- **No sensitive data** — scans context files for leaked API keys, passwords, secrets
|
|
101
|
+
|
|
102
|
+
Custom checks configurable in `.prjct/prjct.config.json`:
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"verification": {
|
|
106
|
+
"checks": [
|
|
107
|
+
{ "name": "Lint CLAUDE.md", "command": "npx markdownlint CLAUDE.md" },
|
|
108
|
+
{ "name": "Custom validator", "script": ".prjct/verify.sh" }
|
|
109
|
+
],
|
|
110
|
+
"failFast": false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Integration: wired into `sync-service.ts` after file generation (step 11), results returned in `SyncResult.verification`. Display in `showSyncResult()` shows pass/fail per check with timing.
|
|
116
|
+
|
|
117
|
+
### Learnings
|
|
118
|
+
|
|
119
|
+
- Non-critical verification must be wrapped in try/catch so it never breaks the sync workflow
|
|
120
|
+
- Config types must match optional fields between `LocalConfig` and `VerificationConfig` (both `checks` must be optional)
|
|
121
|
+
- Built-in + custom extensibility pattern (always run built-ins, then user commands) provides good defaults with flexibility
|
|
122
|
+
|
|
123
|
+
### Test Plan
|
|
124
|
+
|
|
125
|
+
#### For QA
|
|
126
|
+
1. Run `prjct sync --yes` — verify "Verified" section with 3 checks passing
|
|
127
|
+
2. Add custom check to `.prjct/prjct.config.json` — verify it runs after sync
|
|
128
|
+
3. Add failing custom check (`command: "exit 1"`) — verify `✗` with error
|
|
129
|
+
4. Set `failFast: true` with failing check — verify remaining checks skipped
|
|
130
|
+
5. Run `bun run build && bun run typecheck` — zero errors
|
|
131
|
+
|
|
132
|
+
#### For Users
|
|
133
|
+
**What changed:** `prjct sync` now validates generated output with pass/fail checks
|
|
134
|
+
**How to use:** Built-in checks run automatically. Add custom checks in `.prjct/prjct.config.json`
|
|
135
|
+
**Breaking changes:** None
|
|
136
|
+
|
|
3
137
|
## [1.3.0] - 2026-02-06
|
|
4
138
|
|
|
5
139
|
### Features
|
|
@@ -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
|
+
})
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Windsurf: Similar to Cursor
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { type ContextSources, cite, defaultSources } from '../utils/citations'
|
|
11
12
|
import type { AIToolConfig } from './registry'
|
|
12
13
|
|
|
13
14
|
export interface ProjectContext {
|
|
@@ -35,6 +36,7 @@ export interface ProjectContext {
|
|
|
35
36
|
workflow: string[]
|
|
36
37
|
domain: string[]
|
|
37
38
|
}
|
|
39
|
+
sources?: ContextSources
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -42,6 +44,8 @@ export interface ProjectContext {
|
|
|
42
44
|
* Detailed markdown with full context
|
|
43
45
|
*/
|
|
44
46
|
export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): string {
|
|
47
|
+
const s = ctx.sources || defaultSources()
|
|
48
|
+
|
|
45
49
|
return `# ${ctx.name} - Project Rules
|
|
46
50
|
<!-- projectId: ${ctx.projectId} -->
|
|
47
51
|
<!-- Generated: ${new Date().toISOString()} -->
|
|
@@ -49,11 +53,13 @@ export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): str
|
|
|
49
53
|
|
|
50
54
|
## THIS PROJECT (${ctx.ecosystem})
|
|
51
55
|
|
|
56
|
+
${cite(s.ecosystem)}
|
|
52
57
|
**Type:** ${ctx.projectType}
|
|
53
58
|
**Path:** ${ctx.repoPath}
|
|
54
59
|
|
|
55
60
|
### Commands (USE THESE, NOT OTHERS)
|
|
56
61
|
|
|
62
|
+
${cite(s.commands)}
|
|
57
63
|
| Action | Command |
|
|
58
64
|
|--------|---------|
|
|
59
65
|
| Install dependencies | \`${ctx.commands.install}\` |
|
|
@@ -65,7 +71,9 @@ export function formatForClaude(ctx: ProjectContext, _config: AIToolConfig): str
|
|
|
65
71
|
|
|
66
72
|
### Code Conventions
|
|
67
73
|
|
|
74
|
+
${cite(s.languages)}
|
|
68
75
|
- **Languages**: ${ctx.languages.join(', ') || 'Not detected'}
|
|
76
|
+
${cite(s.frameworks)}
|
|
69
77
|
- **Frameworks**: ${ctx.frameworks.join(', ') || 'Not detected'}
|
|
70
78
|
|
|
71
79
|
---
|
|
@@ -93,6 +101,7 @@ p. sync → p. task "desc" → [work] → p. done → p. ship
|
|
|
93
101
|
|
|
94
102
|
## PROJECT STATE
|
|
95
103
|
|
|
104
|
+
${cite(s.name)}
|
|
96
105
|
| Field | Value |
|
|
97
106
|
|-------|-------|
|
|
98
107
|
| Name | ${ctx.name} |
|
|
@@ -120,6 +129,7 @@ Load from \`~/.prjct-cli/projects/${ctx.projectId}/agents/\`:
|
|
|
120
129
|
* @see https://cursor.com/docs/context/rules
|
|
121
130
|
*/
|
|
122
131
|
export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): string {
|
|
132
|
+
const s = ctx.sources || defaultSources()
|
|
123
133
|
const lines: string[] = []
|
|
124
134
|
|
|
125
135
|
// MDC format with YAML frontmatter
|
|
@@ -135,6 +145,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
|
|
|
135
145
|
lines.push('')
|
|
136
146
|
|
|
137
147
|
// Tech stack
|
|
148
|
+
lines.push(cite(s.languages))
|
|
138
149
|
lines.push('## Tech Stack')
|
|
139
150
|
if (ctx.languages.length > 0) {
|
|
140
151
|
lines.push(`- Languages: ${ctx.languages.join(', ')}`)
|
|
@@ -145,6 +156,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
|
|
|
145
156
|
lines.push('')
|
|
146
157
|
|
|
147
158
|
// Commands
|
|
159
|
+
lines.push(cite(s.commands))
|
|
148
160
|
lines.push('## Commands')
|
|
149
161
|
lines.push(`- Install: \`${ctx.commands.install}\``)
|
|
150
162
|
lines.push(`- Dev: \`${ctx.commands.dev}\``)
|
|
@@ -175,6 +187,7 @@ export function formatForCursor(ctx: ProjectContext, _config: AIToolConfig): str
|
|
|
175
187
|
* Minimal bullet points
|
|
176
188
|
*/
|
|
177
189
|
export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): string {
|
|
190
|
+
const s = ctx.sources || defaultSources()
|
|
178
191
|
const lines: string[] = []
|
|
179
192
|
|
|
180
193
|
lines.push('# Copilot Instructions')
|
|
@@ -183,6 +196,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
|
|
|
183
196
|
lines.push('')
|
|
184
197
|
|
|
185
198
|
// Key info
|
|
199
|
+
lines.push(cite(s.ecosystem))
|
|
186
200
|
lines.push('## Project Info')
|
|
187
201
|
lines.push(`- Type: ${ctx.projectType}`)
|
|
188
202
|
lines.push(`- Stack: ${ctx.frameworks.join(', ') || ctx.ecosystem}`)
|
|
@@ -196,6 +210,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
|
|
|
196
210
|
lines.push('')
|
|
197
211
|
|
|
198
212
|
// Commands
|
|
213
|
+
lines.push(cite(s.commands))
|
|
199
214
|
lines.push('## Commands')
|
|
200
215
|
lines.push(`- Test: \`${ctx.commands.test}\``)
|
|
201
216
|
lines.push(`- Build: \`${ctx.commands.build}\``)
|
|
@@ -210,6 +225,7 @@ export function formatForCopilot(ctx: ProjectContext, _config: AIToolConfig): st
|
|
|
210
225
|
* @see https://docs.windsurf.com/windsurf/cascade/memories
|
|
211
226
|
*/
|
|
212
227
|
export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): string {
|
|
228
|
+
const s = ctx.sources || defaultSources()
|
|
213
229
|
const lines: string[] = []
|
|
214
230
|
|
|
215
231
|
// YAML frontmatter (Windsurf uses trigger: always_on instead of alwaysApply)
|
|
@@ -226,6 +242,7 @@ export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): s
|
|
|
226
242
|
lines.push('')
|
|
227
243
|
|
|
228
244
|
// Tech stack (concise)
|
|
245
|
+
lines.push(cite(s.languages))
|
|
229
246
|
lines.push('## Stack')
|
|
230
247
|
lines.push(`- ${ctx.languages.join(', ')}`)
|
|
231
248
|
if (ctx.frameworks.length > 0) {
|
|
@@ -234,6 +251,7 @@ export function formatForWindsurf(ctx: ProjectContext, _config: AIToolConfig): s
|
|
|
234
251
|
lines.push('')
|
|
235
252
|
|
|
236
253
|
// Commands (essential only)
|
|
254
|
+
lines.push(cite(s.commands))
|
|
237
255
|
lines.push('## Commands')
|
|
238
256
|
lines.push('```bash')
|
|
239
257
|
lines.push(`# Install`)
|
|
@@ -529,6 +529,28 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
529
529
|
console.log('')
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
533
|
+
// VERIFICATION - Post-sync validation checks
|
|
534
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
535
|
+
if (result.verification) {
|
|
536
|
+
const v = result.verification
|
|
537
|
+
if (v.passed) {
|
|
538
|
+
const items = v.checks.map((c) => `${c.name} (${c.durationMs}ms)`)
|
|
539
|
+
out.section('Verified')
|
|
540
|
+
out.list(items, { bullet: '✓' })
|
|
541
|
+
} else {
|
|
542
|
+
out.section('Verification')
|
|
543
|
+
const items = v.checks.map((c) =>
|
|
544
|
+
c.passed ? `✓ ${c.name}` : `✗ ${c.name}${c.error ? ` — ${c.error}` : ''}`
|
|
545
|
+
)
|
|
546
|
+
out.list(items)
|
|
547
|
+
if (v.skippedCount > 0) {
|
|
548
|
+
out.warn(`${v.skippedCount} check(s) skipped (fail-fast)`)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
console.log('')
|
|
552
|
+
}
|
|
553
|
+
|
|
532
554
|
// ═══════════════════════════════════════════════════════════════════════
|
|
533
555
|
// NEXT STEPS - Clear call to action
|
|
534
556
|
// ═══════════════════════════════════════════════════════════════════════
|
package/core/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { CommandMeta } from './commands/registry'
|
|
|
15
15
|
import { detectAllProviders, detectAntigravity } from './infrastructure/ai-provider'
|
|
16
16
|
import configManager from './infrastructure/config-manager'
|
|
17
17
|
import { sessionTracker } from './services/session-tracker'
|
|
18
|
+
import { getError } from './utils/error-messages'
|
|
18
19
|
import out from './utils/output'
|
|
19
20
|
|
|
20
21
|
interface ParsedCommandArgs {
|
|
@@ -53,27 +54,34 @@ async function main(): Promise<void> {
|
|
|
53
54
|
const cmd = commandRegistry.getByName(commandName)
|
|
54
55
|
|
|
55
56
|
if (!cmd) {
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const suggestion = findClosestCommand(commandName)
|
|
58
|
+
const hint = suggestion
|
|
59
|
+
? `Did you mean 'prjct ${suggestion}'? Run 'prjct --help' for all commands`
|
|
60
|
+
: "Run 'prjct --help' to see available commands"
|
|
61
|
+
out.failWithHint(
|
|
62
|
+
getError('UNKNOWN_COMMAND', { message: `Unknown command: ${commandName}`, hint })
|
|
63
|
+
)
|
|
58
64
|
out.end()
|
|
59
65
|
process.exit(1)
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
// 2. Check if deprecated
|
|
63
69
|
if (cmd.deprecated) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
70
|
+
const hint = cmd.replacedBy
|
|
71
|
+
? `Use 'prjct ${cmd.replacedBy}' instead`
|
|
72
|
+
: "Run 'prjct --help' to see available commands"
|
|
73
|
+
out.failWithHint({ message: `Command '${commandName}' is deprecated`, hint })
|
|
68
74
|
out.end()
|
|
69
75
|
process.exit(1)
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
// 3. Check if implemented
|
|
73
79
|
if (!cmd.implemented) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
out.failWithHint({
|
|
81
|
+
message: `Command '${commandName}' is not yet implemented`,
|
|
82
|
+
hint: "Run 'prjct --help' to see available commands",
|
|
83
|
+
docs: 'https://github.com/jlopezlira/prjct-cli',
|
|
84
|
+
})
|
|
77
85
|
out.end()
|
|
78
86
|
process.exit(1)
|
|
79
87
|
}
|
|
@@ -81,7 +89,15 @@ async function main(): Promise<void> {
|
|
|
81
89
|
// 4. Parse arguments
|
|
82
90
|
const { parsedArgs, options } = parseCommandArgs(cmd, rawArgs)
|
|
83
91
|
|
|
84
|
-
// 4.5.
|
|
92
|
+
// 4.5. Validate required params
|
|
93
|
+
const paramError = validateCommandParams(cmd, parsedArgs)
|
|
94
|
+
if (paramError) {
|
|
95
|
+
out.failWithHint(paramError)
|
|
96
|
+
out.end()
|
|
97
|
+
process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4.6. Session tracking — touch/create session before command execution
|
|
85
101
|
let projectId: string | null = null
|
|
86
102
|
const commandStartTime = Date.now()
|
|
87
103
|
try {
|
|
@@ -193,6 +209,71 @@ async function main(): Promise<void> {
|
|
|
193
209
|
}
|
|
194
210
|
}
|
|
195
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Validate that required params are provided
|
|
214
|
+
* Parses CommandMeta.params: <required> vs [optional]
|
|
215
|
+
*/
|
|
216
|
+
function validateCommandParams(
|
|
217
|
+
cmd: CommandMeta,
|
|
218
|
+
parsedArgs: string[]
|
|
219
|
+
): import('./utils/error-messages').ErrorWithHint | null {
|
|
220
|
+
if (!cmd.params) return null
|
|
221
|
+
|
|
222
|
+
// Extract required params: tokens wrapped in <angle brackets>
|
|
223
|
+
const requiredParams = cmd.params.match(/<[^>]+>/g)
|
|
224
|
+
if (!requiredParams || requiredParams.length === 0) return null
|
|
225
|
+
|
|
226
|
+
// Check if enough positional args provided
|
|
227
|
+
if (parsedArgs.length < requiredParams.length) {
|
|
228
|
+
const paramNames = requiredParams.map((p) => p.slice(1, -1)).join(', ')
|
|
229
|
+
const usage = cmd.usage.terminal || `prjct ${cmd.name} ${cmd.params}`
|
|
230
|
+
return getError('MISSING_PARAM', {
|
|
231
|
+
message: `Missing required parameter: ${paramNames}`,
|
|
232
|
+
hint: `Usage: ${usage}`,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find closest matching command name for did-you-mean suggestions
|
|
241
|
+
* Uses Levenshtein edit distance — suggests if distance <= 2
|
|
242
|
+
*/
|
|
243
|
+
function findClosestCommand(input: string): string | null {
|
|
244
|
+
const allNames = commandRegistry.getAll().map((c) => c.name)
|
|
245
|
+
let best: string | null = null
|
|
246
|
+
let bestDist = Infinity
|
|
247
|
+
|
|
248
|
+
for (const name of allNames) {
|
|
249
|
+
const dist = editDistance(input.toLowerCase(), name.toLowerCase())
|
|
250
|
+
if (dist < bestDist) {
|
|
251
|
+
bestDist = dist
|
|
252
|
+
best = name
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Only suggest if edit distance is at most 2
|
|
257
|
+
return bestDist <= 2 ? best : null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function editDistance(a: string, b: string): number {
|
|
261
|
+
const m = a.length
|
|
262
|
+
const n = b.length
|
|
263
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
|
|
264
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i
|
|
265
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j
|
|
266
|
+
for (let i = 1; i <= m; i++) {
|
|
267
|
+
for (let j = 1; j <= n; j++) {
|
|
268
|
+
dp[i][j] =
|
|
269
|
+
a[i - 1] === b[j - 1]
|
|
270
|
+
? dp[i - 1][j - 1]
|
|
271
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return dp[m][n]
|
|
275
|
+
}
|
|
276
|
+
|
|
196
277
|
/**
|
|
197
278
|
* Parse command arguments dynamically
|
|
198
279
|
*/
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs/promises'
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import pathManager from '../infrastructure/path-manager'
|
|
15
|
+
import { type ContextSources, cite, defaultSources } from '../utils/citations'
|
|
15
16
|
import dateHelper from '../utils/date-helper'
|
|
16
17
|
import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
|
|
17
18
|
import { NestedContextResolver } from './nested-context-resolver'
|
|
@@ -103,13 +104,14 @@ export class ContextFileGenerator {
|
|
|
103
104
|
git: GitData,
|
|
104
105
|
stats: ProjectStats,
|
|
105
106
|
commands: Commands,
|
|
106
|
-
agents: AgentInfo[]
|
|
107
|
+
agents: AgentInfo[],
|
|
108
|
+
sources?: ContextSources
|
|
107
109
|
): Promise<string[]> {
|
|
108
110
|
const contextPath = path.join(this.config.globalPath, 'context')
|
|
109
111
|
|
|
110
112
|
// Generate all context files IN PARALLEL
|
|
111
113
|
await Promise.all([
|
|
112
|
-
this.generateClaudeMd(contextPath, git, stats, commands, agents),
|
|
114
|
+
this.generateClaudeMd(contextPath, git, stats, commands, agents, sources),
|
|
113
115
|
this.generateNowMd(contextPath),
|
|
114
116
|
this.generateNextMd(contextPath),
|
|
115
117
|
this.generateIdeasMd(contextPath),
|
|
@@ -137,10 +139,12 @@ export class ContextFileGenerator {
|
|
|
137
139
|
git: GitData,
|
|
138
140
|
stats: ProjectStats,
|
|
139
141
|
commands: Commands,
|
|
140
|
-
agents: AgentInfo[]
|
|
142
|
+
agents: AgentInfo[],
|
|
143
|
+
sources?: ContextSources
|
|
141
144
|
): Promise<void> {
|
|
142
145
|
const workflowAgents = agents.filter((a) => a.type === 'workflow').map((a) => a.name)
|
|
143
146
|
const domainAgents = agents.filter((a) => a.type === 'domain').map((a) => a.name)
|
|
147
|
+
const s = sources || defaultSources()
|
|
144
148
|
|
|
145
149
|
const content = `# ${stats.name} - Project Rules
|
|
146
150
|
<!-- projectId: ${this.config.projectId} -->
|
|
@@ -149,11 +153,13 @@ export class ContextFileGenerator {
|
|
|
149
153
|
|
|
150
154
|
## THIS PROJECT (${stats.ecosystem})
|
|
151
155
|
|
|
156
|
+
${cite(s.ecosystem)}
|
|
152
157
|
**Type:** ${stats.projectType}
|
|
153
158
|
**Path:** ${this.config.projectPath}
|
|
154
159
|
|
|
155
160
|
### Commands (USE THESE, NOT OTHERS)
|
|
156
161
|
|
|
162
|
+
${cite(s.commands)}
|
|
157
163
|
| Action | Command |
|
|
158
164
|
|--------|---------|
|
|
159
165
|
| Install dependencies | \`${commands.install}\` |
|
|
@@ -165,7 +171,9 @@ export class ContextFileGenerator {
|
|
|
165
171
|
|
|
166
172
|
### Code Conventions
|
|
167
173
|
|
|
174
|
+
${cite(s.languages)}
|
|
168
175
|
- **Languages**: ${stats.languages.join(', ') || 'Not detected'}
|
|
176
|
+
${cite(s.frameworks)}
|
|
169
177
|
- **Frameworks**: ${stats.frameworks.join(', ') || 'Not detected'}
|
|
170
178
|
|
|
171
179
|
---
|
|
@@ -193,6 +201,7 @@ p. sync → p. task "desc" → [work] → p. done → p. ship
|
|
|
193
201
|
|
|
194
202
|
## PROJECT STATE
|
|
195
203
|
|
|
204
|
+
${cite(s.name)}
|
|
196
205
|
| Field | Value |
|
|
197
206
|
|-------|-------|
|
|
198
207
|
| Name | ${stats.name} |
|