prjct-cli 0.45.5 → 0.47.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 +60 -0
- package/bin/prjct.ts +3 -47
- package/core/__tests__/utils/preserve-sections.test.ts +216 -0
- package/core/agentic/command-executor.ts +8 -1
- package/core/ai-tools/generator.ts +10 -1
- package/core/commands/command-data.ts +16 -0
- package/core/commands/commands.ts +7 -0
- package/core/commands/register.ts +1 -0
- package/core/commands/setup.ts +4 -4
- package/core/commands/shipping.ts +26 -3
- package/core/commands/workflow.ts +105 -2
- package/core/services/context-generator.ts +12 -1
- package/core/utils/help.ts +321 -0
- package/core/utils/preserve-sections.ts +218 -0
- package/core/utils/subtask-table.ts +234 -0
- package/core/workflow/index.ts +1 -0
- package/core/workflow/workflow-preferences.ts +312 -0
- package/dist/bin/prjct.mjs +4464 -3830
- package/package.json +1 -1
- package/templates/commands/workflow.md +150 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.47.0] - 2026-01-30
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Complete and improve help text documentation - PRJ-133 (#73)
|
|
8
|
+
- Workflow hooks via natural language - PRJ-137 (#72)
|
|
9
|
+
- Subtask progress dashboard with domain colors - PRJ-138 (#71)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [0.48.1] - 2026-01-30
|
|
13
|
+
|
|
14
|
+
### Improved
|
|
15
|
+
|
|
16
|
+
- **Complete and improve help text documentation** (PRJ-133)
|
|
17
|
+
- New structured help system with `prjct help <command>` support
|
|
18
|
+
- Per-command help with usage, parameters, and features
|
|
19
|
+
- Commands grouped by category with `prjct help commands`
|
|
20
|
+
- Clean visual formatting with Quick Start, Terminal Commands, AI Agent Commands sections
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## [0.48.0] - 2026-01-29
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Workflow hooks via natural language** (PRJ-137)
|
|
28
|
+
- Configure hooks with `p. workflow "antes de ship corre los tests"`
|
|
29
|
+
- Supports before/after hooks for task, done, ship, sync commands
|
|
30
|
+
- Three scopes: permanent (persisted), session, one-time
|
|
31
|
+
- Uses existing memory system for storage
|
|
32
|
+
- No JSON config needed - just talk to the LLM
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## [0.47.0] - 2026-01-29
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **Subtask progress dashboard with domain-specific colors** (PRJ-138)
|
|
40
|
+
- Each domain (frontend, backend, database, etc.) displays in unique color
|
|
41
|
+
- Hash-based color assignment ensures consistency across sessions
|
|
42
|
+
- Integrated into command execution flow for visual task tracking
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## [0.46.0] - 2026-01-30
|
|
46
|
+
|
|
47
|
+
### Features
|
|
48
|
+
|
|
49
|
+
- preserve user customizations during sync - PRJ-115 (#70)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## [0.46.0] - 2026-01-30
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **Preserve user customizations during sync** (PRJ-115)
|
|
57
|
+
- Wrap custom content in `<!-- prjct:preserve -->` markers
|
|
58
|
+
- Content survives `p. sync` regeneration
|
|
59
|
+
- Supports named sections: `<!-- prjct:preserve:my-rules -->`
|
|
60
|
+
- Works with CLAUDE.md, .cursorrules, and all context files
|
|
61
|
+
|
|
62
|
+
|
|
3
63
|
## [0.45.5] - 2026-01-30
|
|
4
64
|
|
|
5
65
|
### Bug Fixes
|
package/bin/prjct.ts
CHANGED
|
@@ -184,53 +184,9 @@ if (args[0] === 'start' || args[0] === 'setup') {
|
|
|
184
184
|
}
|
|
185
185
|
} else if (args[0] === 'help' || args[0] === '-h' || args[0] === '--help') {
|
|
186
186
|
// Show help - bypass setup check to always show help
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
QUICK START
|
|
192
|
-
-----------
|
|
193
|
-
Claude/Gemini:
|
|
194
|
-
1. prjct start Configure your AI provider
|
|
195
|
-
2. cd my-project && prjct init
|
|
196
|
-
3. Open in Claude Code or Gemini CLI
|
|
197
|
-
4. Type: p. sync Analyze project
|
|
198
|
-
|
|
199
|
-
Cursor IDE:
|
|
200
|
-
1. cd my-project && prjct init
|
|
201
|
-
2. Open in Cursor
|
|
202
|
-
3. Type: /sync Analyze project
|
|
203
|
-
|
|
204
|
-
COMMANDS (inside your AI agent)
|
|
205
|
-
-------------------------------
|
|
206
|
-
Claude/Gemini Cursor Description
|
|
207
|
-
─────────────────────────────────────────────────────
|
|
208
|
-
p. sync /sync Analyze project
|
|
209
|
-
p. task "desc" /task "desc" Start a task
|
|
210
|
-
p. done /done Complete subtask
|
|
211
|
-
p. ship "name" /ship "name" Ship with PR
|
|
212
|
-
|
|
213
|
-
TERMINAL COMMANDS (this CLI)
|
|
214
|
-
----------------------------
|
|
215
|
-
prjct start First-time setup (Claude/Gemini global config)
|
|
216
|
-
prjct init Initialize project (required for Cursor)
|
|
217
|
-
prjct setup Reconfigure installations
|
|
218
|
-
prjct sync Sync project state
|
|
219
|
-
prjct watch Auto-sync on file changes (Ctrl+C to stop)
|
|
220
|
-
prjct doctor Check system health and dependencies
|
|
221
|
-
prjct uninstall Complete system removal of prjct
|
|
222
|
-
|
|
223
|
-
FLAGS
|
|
224
|
-
-----
|
|
225
|
-
--quiet, -q Suppress all output (only errors to stderr)
|
|
226
|
-
--version, -v Show version
|
|
227
|
-
--help, -h Show this help
|
|
228
|
-
|
|
229
|
-
MORE INFO
|
|
230
|
-
---------
|
|
231
|
-
Documentation: https://prjct.app
|
|
232
|
-
GitHub: https://github.com/jlopezlira/prjct-cli
|
|
233
|
-
`)
|
|
187
|
+
const { getHelp } = await import('../core/utils/help')
|
|
188
|
+
const topic = args[1] // Optional: prjct help <command>
|
|
189
|
+
console.log(getHelp(topic))
|
|
234
190
|
process.exitCode = 0
|
|
235
191
|
} else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
|
|
236
192
|
// Show version with provider status
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
extractPreservedContent,
|
|
4
|
+
extractPreservedSections,
|
|
5
|
+
hasPreservedSections,
|
|
6
|
+
mergePreservedSections,
|
|
7
|
+
stripPreservedSections,
|
|
8
|
+
validatePreserveBlocks,
|
|
9
|
+
wrapInPreserveMarkers,
|
|
10
|
+
} from '../../utils/preserve-sections'
|
|
11
|
+
|
|
12
|
+
describe('preserve-sections', () => {
|
|
13
|
+
describe('extractPreservedSections', () => {
|
|
14
|
+
it('should extract a single preserved section', () => {
|
|
15
|
+
const content = `# Header
|
|
16
|
+
|
|
17
|
+
<!-- prjct:preserve -->
|
|
18
|
+
My custom content
|
|
19
|
+
<!-- /prjct:preserve -->
|
|
20
|
+
|
|
21
|
+
# Footer`
|
|
22
|
+
|
|
23
|
+
const sections = extractPreservedSections(content)
|
|
24
|
+
expect(sections).toHaveLength(1)
|
|
25
|
+
expect(sections[0].content).toContain('My custom content')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should extract multiple preserved sections', () => {
|
|
29
|
+
const content = `# Header
|
|
30
|
+
|
|
31
|
+
<!-- prjct:preserve -->
|
|
32
|
+
First section
|
|
33
|
+
<!-- /prjct:preserve -->
|
|
34
|
+
|
|
35
|
+
Some content
|
|
36
|
+
|
|
37
|
+
<!-- prjct:preserve -->
|
|
38
|
+
Second section
|
|
39
|
+
<!-- /prjct:preserve -->`
|
|
40
|
+
|
|
41
|
+
const sections = extractPreservedSections(content)
|
|
42
|
+
expect(sections).toHaveLength(2)
|
|
43
|
+
expect(sections[0].content).toContain('First section')
|
|
44
|
+
expect(sections[1].content).toContain('Second section')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should handle named sections', () => {
|
|
48
|
+
const content = `<!-- prjct:preserve:custom-rules -->
|
|
49
|
+
My rules
|
|
50
|
+
<!-- /prjct:preserve -->`
|
|
51
|
+
|
|
52
|
+
const sections = extractPreservedSections(content)
|
|
53
|
+
expect(sections).toHaveLength(1)
|
|
54
|
+
expect(sections[0].id).toBe('custom-rules')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should return empty array when no preserved sections', () => {
|
|
58
|
+
const content = '# Just normal content'
|
|
59
|
+
const sections = extractPreservedSections(content)
|
|
60
|
+
expect(sections).toHaveLength(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should ignore unclosed preserve blocks', () => {
|
|
64
|
+
const content = `<!-- prjct:preserve -->
|
|
65
|
+
No closing tag here`
|
|
66
|
+
|
|
67
|
+
const sections = extractPreservedSections(content)
|
|
68
|
+
expect(sections).toHaveLength(0)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('extractPreservedContent', () => {
|
|
73
|
+
it('should extract inner content without markers', () => {
|
|
74
|
+
const content = `<!-- prjct:preserve -->
|
|
75
|
+
My content
|
|
76
|
+
<!-- /prjct:preserve -->`
|
|
77
|
+
|
|
78
|
+
const inner = extractPreservedContent(content)
|
|
79
|
+
expect(inner).toHaveLength(1)
|
|
80
|
+
expect(inner[0]).toBe('My content')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('hasPreservedSections', () => {
|
|
85
|
+
it('should return true when preserved sections exist', () => {
|
|
86
|
+
const content = '<!-- prjct:preserve -->content<!-- /prjct:preserve -->'
|
|
87
|
+
expect(hasPreservedSections(content)).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should return false when no preserved sections', () => {
|
|
91
|
+
const content = '# Normal markdown'
|
|
92
|
+
expect(hasPreservedSections(content)).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('mergePreservedSections', () => {
|
|
97
|
+
it('should append preserved sections to new content', () => {
|
|
98
|
+
const oldContent = `# Old Header
|
|
99
|
+
|
|
100
|
+
<!-- prjct:preserve -->
|
|
101
|
+
# My Rules
|
|
102
|
+
- Use tabs
|
|
103
|
+
<!-- /prjct:preserve -->`
|
|
104
|
+
|
|
105
|
+
const newContent = `# New Generated Content
|
|
106
|
+
|
|
107
|
+
This is fresh.`
|
|
108
|
+
|
|
109
|
+
const merged = mergePreservedSections(newContent, oldContent)
|
|
110
|
+
|
|
111
|
+
expect(merged).toContain('# New Generated Content')
|
|
112
|
+
expect(merged).toContain('My Rules')
|
|
113
|
+
expect(merged).toContain('Use tabs')
|
|
114
|
+
expect(merged).toContain('prjct:preserve')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should return new content unchanged when no preserved sections', () => {
|
|
118
|
+
const oldContent = '# Old content without preserve markers'
|
|
119
|
+
const newContent = '# New content'
|
|
120
|
+
|
|
121
|
+
const merged = mergePreservedSections(newContent, oldContent)
|
|
122
|
+
expect(merged).toBe(newContent)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should preserve multiple sections', () => {
|
|
126
|
+
const oldContent = `<!-- prjct:preserve -->
|
|
127
|
+
Section 1
|
|
128
|
+
<!-- /prjct:preserve -->
|
|
129
|
+
|
|
130
|
+
<!-- prjct:preserve -->
|
|
131
|
+
Section 2
|
|
132
|
+
<!-- /prjct:preserve -->`
|
|
133
|
+
|
|
134
|
+
const newContent = '# New'
|
|
135
|
+
|
|
136
|
+
const merged = mergePreservedSections(newContent, oldContent)
|
|
137
|
+
expect(merged).toContain('Section 1')
|
|
138
|
+
expect(merged).toContain('Section 2')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('wrapInPreserveMarkers', () => {
|
|
143
|
+
it('should wrap content with default markers', () => {
|
|
144
|
+
const content = 'My content'
|
|
145
|
+
const wrapped = wrapInPreserveMarkers(content)
|
|
146
|
+
|
|
147
|
+
expect(wrapped).toContain('<!-- prjct:preserve -->')
|
|
148
|
+
expect(wrapped).toContain('My content')
|
|
149
|
+
expect(wrapped).toContain('<!-- /prjct:preserve -->')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should wrap content with named markers', () => {
|
|
153
|
+
const content = 'My content'
|
|
154
|
+
const wrapped = wrapInPreserveMarkers(content, 'custom')
|
|
155
|
+
|
|
156
|
+
expect(wrapped).toContain('<!-- prjct:preserve:custom -->')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('stripPreservedSections', () => {
|
|
161
|
+
it('should remove preserved sections from content', () => {
|
|
162
|
+
const content = `# Header
|
|
163
|
+
|
|
164
|
+
<!-- prjct:preserve -->
|
|
165
|
+
Custom stuff
|
|
166
|
+
<!-- /prjct:preserve -->
|
|
167
|
+
|
|
168
|
+
# Footer`
|
|
169
|
+
|
|
170
|
+
const stripped = stripPreservedSections(content)
|
|
171
|
+
expect(stripped).toContain('# Header')
|
|
172
|
+
expect(stripped).toContain('# Footer')
|
|
173
|
+
expect(stripped).not.toContain('Custom stuff')
|
|
174
|
+
expect(stripped).not.toContain('prjct:preserve')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should return content unchanged when no preserved sections', () => {
|
|
178
|
+
const content = '# Normal content'
|
|
179
|
+
const stripped = stripPreservedSections(content)
|
|
180
|
+
expect(stripped).toBe(content)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('validatePreserveBlocks', () => {
|
|
185
|
+
it('should validate correct blocks', () => {
|
|
186
|
+
const content = `<!-- prjct:preserve -->
|
|
187
|
+
Content
|
|
188
|
+
<!-- /prjct:preserve -->`
|
|
189
|
+
|
|
190
|
+
const result = validatePreserveBlocks(content)
|
|
191
|
+
expect(result.valid).toBe(true)
|
|
192
|
+
expect(result.errors).toHaveLength(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should detect mismatched markers', () => {
|
|
196
|
+
const content = `<!-- prjct:preserve -->
|
|
197
|
+
Content without closing`
|
|
198
|
+
|
|
199
|
+
const result = validatePreserveBlocks(content)
|
|
200
|
+
expect(result.valid).toBe(false)
|
|
201
|
+
expect(result.errors.length).toBeGreaterThan(0)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should detect nested blocks', () => {
|
|
205
|
+
const content = `<!-- prjct:preserve -->
|
|
206
|
+
<!-- prjct:preserve -->
|
|
207
|
+
Nested
|
|
208
|
+
<!-- /prjct:preserve -->
|
|
209
|
+
<!-- /prjct:preserve -->`
|
|
210
|
+
|
|
211
|
+
const result = validatePreserveBlocks(content)
|
|
212
|
+
expect(result.valid).toBe(false)
|
|
213
|
+
expect(result.errors.some((e) => e.includes('Nested'))).toBe(true)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
SimpleExecutionResult,
|
|
19
19
|
} from '../types'
|
|
20
20
|
import { agentStream } from '../utils/agent-stream'
|
|
21
|
+
import { printSubtaskProgress, type SubtaskDisplay } from '../utils/subtask-table'
|
|
21
22
|
import chainOfThought from './chain-of-thought'
|
|
22
23
|
import contextBuilder from './context-builder'
|
|
23
24
|
import groundTruth from './ground-truth'
|
|
@@ -192,7 +193,13 @@ export class CommandExecutor {
|
|
|
192
193
|
|
|
193
194
|
// Show subtasks if fragmented
|
|
194
195
|
if (orchestratorContext.requiresFragmentation && orchestratorContext.subtasks) {
|
|
195
|
-
|
|
196
|
+
const subtaskDisplay: SubtaskDisplay[] = orchestratorContext.subtasks.map((s) => ({
|
|
197
|
+
id: s.id,
|
|
198
|
+
domain: s.domain,
|
|
199
|
+
description: s.description,
|
|
200
|
+
status: s.status,
|
|
201
|
+
}))
|
|
202
|
+
printSubtaskProgress(subtaskDisplay)
|
|
196
203
|
}
|
|
197
204
|
} catch (error) {
|
|
198
205
|
// Orchestration failed - log warning but continue without it
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs/promises'
|
|
9
9
|
import path from 'node:path'
|
|
10
|
+
import { mergePreservedSections } from '../utils/preserve-sections'
|
|
10
11
|
import { getFormatter, type ProjectContext } from './formatters'
|
|
11
12
|
import { AI_TOOLS, type AIToolConfig, DEFAULT_AI_TOOLS, getAIToolConfig } from './registry'
|
|
12
13
|
|
|
@@ -71,7 +72,7 @@ async function generateForTool(
|
|
|
71
72
|
|
|
72
73
|
try {
|
|
73
74
|
// Generate content
|
|
74
|
-
|
|
75
|
+
let content = formatter(context, config)
|
|
75
76
|
|
|
76
77
|
// Determine output path
|
|
77
78
|
let outputPath: string
|
|
@@ -84,6 +85,14 @@ async function generateForTool(
|
|
|
84
85
|
// Ensure directory exists
|
|
85
86
|
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
|
86
87
|
|
|
88
|
+
// Read existing file to preserve user customizations
|
|
89
|
+
try {
|
|
90
|
+
const existingContent = await fs.readFile(outputPath, 'utf-8')
|
|
91
|
+
content = mergePreservedSections(content, existingContent)
|
|
92
|
+
} catch {
|
|
93
|
+
// File doesn't exist yet - use generated content as-is
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
// Write file
|
|
88
97
|
await fs.writeFile(outputPath, content, 'utf-8')
|
|
89
98
|
|
|
@@ -285,6 +285,22 @@ export const COMMANDS: CommandMeta[] = [
|
|
|
285
285
|
requiresProject: true,
|
|
286
286
|
isOptional: true,
|
|
287
287
|
},
|
|
288
|
+
{
|
|
289
|
+
name: 'workflow',
|
|
290
|
+
group: 'optional',
|
|
291
|
+
description: 'Configure workflow hooks via natural language',
|
|
292
|
+
usage: { claude: '/p:workflow ["config"]', terminal: 'prjct workflow ["config"]' },
|
|
293
|
+
params: '["natural language config"]',
|
|
294
|
+
implemented: true,
|
|
295
|
+
hasTemplate: true,
|
|
296
|
+
requiresProject: true,
|
|
297
|
+
isOptional: true,
|
|
298
|
+
features: [
|
|
299
|
+
'Natural language configuration',
|
|
300
|
+
'Before/after hooks for task, done, ship, sync',
|
|
301
|
+
'Permanent, session, or one-time preferences',
|
|
302
|
+
],
|
|
303
|
+
},
|
|
288
304
|
|
|
289
305
|
// ===== SETUP COMMANDS =====
|
|
290
306
|
{
|
|
@@ -90,6 +90,13 @@ class PrjctCommands {
|
|
|
90
90
|
return this.workflow.resume(taskId, projectPath)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
async workflowPrefs(
|
|
94
|
+
input: string | null = null,
|
|
95
|
+
projectPath: string = process.cwd()
|
|
96
|
+
): Promise<CommandResult> {
|
|
97
|
+
return this.workflow.workflow(input, projectPath)
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
// ========== Planning Commands ==========
|
|
94
101
|
|
|
95
102
|
async init(
|
|
@@ -58,6 +58,7 @@ export function registerAllCommands(): void {
|
|
|
58
58
|
commandRegistry.registerMethod('next', workflow, 'next', getMeta('next'))
|
|
59
59
|
commandRegistry.registerMethod('pause', workflow, 'pause', getMeta('pause'))
|
|
60
60
|
commandRegistry.registerMethod('resume', workflow, 'resume', getMeta('resume'))
|
|
61
|
+
commandRegistry.registerMethod('workflow', workflow, 'workflow', getMeta('workflow'))
|
|
61
62
|
|
|
62
63
|
// Planning commands
|
|
63
64
|
commandRegistry.registerMethod('init', planning, 'init', getMeta('init'))
|
package/core/commands/setup.ts
CHANGED
|
@@ -50,9 +50,9 @@ export class SetupCommands extends PrjctCommandsBase {
|
|
|
50
50
|
|
|
51
51
|
if ((result.errors?.length ?? 0) > 0) {
|
|
52
52
|
console.log(`\n⚠️ ${result.errors?.length ?? 0} errors:`)
|
|
53
|
-
|
|
53
|
+
for (const e of result.errors ?? []) {
|
|
54
54
|
console.log(` - ${e.file}: ${e.error}`)
|
|
55
|
-
|
|
55
|
+
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
console.log('\n🎉 Setup complete!')
|
|
@@ -92,9 +92,9 @@ export class SetupCommands extends PrjctCommandsBase {
|
|
|
92
92
|
|
|
93
93
|
if ((result.errors?.length ?? 0) > 0) {
|
|
94
94
|
console.log(`\n⚠️ ${result.errors?.length ?? 0} errors:`)
|
|
95
|
-
|
|
95
|
+
for (const e of result.errors ?? []) {
|
|
96
96
|
console.log(` - ${e.file}: ${e.error}`)
|
|
97
|
-
|
|
97
|
+
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
console.log('\n📝 Installing global configuration...')
|
|
@@ -10,6 +10,7 @@ import type { CommandResult } from '../types'
|
|
|
10
10
|
import { isNotFoundError } from '../types/fs'
|
|
11
11
|
import { showNextSteps } from '../utils/next-steps'
|
|
12
12
|
import { detectProjectCommands } from '../utils/project-commands'
|
|
13
|
+
import { runWorkflowHooks } from '../workflow/workflow-preferences'
|
|
13
14
|
import { configManager, dateHelper, fileHelper, out, PrjctCommandsBase, toolRegistry } from './base'
|
|
14
15
|
|
|
15
16
|
export class ShippingCommands extends PrjctCommandsBase {
|
|
@@ -48,13 +49,20 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
48
49
|
/**
|
|
49
50
|
* /p:ship - Ship feature with complete automated workflow
|
|
50
51
|
*/
|
|
51
|
-
async ship(
|
|
52
|
+
async ship(
|
|
53
|
+
feature: string | null,
|
|
54
|
+
projectPath: string = process.cwd(),
|
|
55
|
+
options: { skipHooks?: boolean } = {}
|
|
56
|
+
): Promise<CommandResult> {
|
|
52
57
|
try {
|
|
53
58
|
const initResult = await this.ensureProjectInit(projectPath)
|
|
54
59
|
if (!initResult.success) return initResult
|
|
55
60
|
|
|
56
|
-
const
|
|
57
|
-
|
|
61
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
62
|
+
if (!projectId) {
|
|
63
|
+
out.fail('no project ID')
|
|
64
|
+
return { success: false, error: 'No project ID found' }
|
|
65
|
+
}
|
|
58
66
|
|
|
59
67
|
let featureName = feature
|
|
60
68
|
if (!featureName) {
|
|
@@ -63,6 +71,15 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
63
71
|
featureName = currentTask?.description || 'current work'
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
// Run before_ship hooks (using memory-based preferences)
|
|
75
|
+
const beforeResult = await runWorkflowHooks(projectId, 'before', 'ship', {
|
|
76
|
+
projectPath,
|
|
77
|
+
skipHooks: options.skipHooks,
|
|
78
|
+
})
|
|
79
|
+
if (!beforeResult.success) {
|
|
80
|
+
return { success: false, error: `Hook failed: ${beforeResult.failed}` }
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
// Ship steps with progress indicator
|
|
67
84
|
out.step(1, 5, `Linting ${featureName}...`)
|
|
68
85
|
const lintResult = await this._runLint(projectPath)
|
|
@@ -108,6 +125,12 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
108
125
|
})
|
|
109
126
|
}
|
|
110
127
|
|
|
128
|
+
// Run after_ship hooks
|
|
129
|
+
await runWorkflowHooks(projectId, 'after', 'ship', {
|
|
130
|
+
projectPath,
|
|
131
|
+
skipHooks: options.skipHooks,
|
|
132
|
+
})
|
|
133
|
+
|
|
111
134
|
out.done(`v${newVersion} shipped`)
|
|
112
135
|
showNextSteps('ship')
|
|
113
136
|
|
|
@@ -16,6 +16,16 @@ import { queueStorage, stateStorage } from '../storage'
|
|
|
16
16
|
import type { CommandResult } from '../types'
|
|
17
17
|
import { showNextSteps, showStateInfo } from '../utils/next-steps'
|
|
18
18
|
import { getLinearApiKey, getProjectCredentials } from '../utils/project-credentials'
|
|
19
|
+
import {
|
|
20
|
+
formatWorkflowPreferences,
|
|
21
|
+
type HookCommand,
|
|
22
|
+
type HookPhase,
|
|
23
|
+
listWorkflowPreferences,
|
|
24
|
+
type PreferenceScope,
|
|
25
|
+
removeWorkflowPreference,
|
|
26
|
+
runWorkflowHooks,
|
|
27
|
+
setWorkflowPreference,
|
|
28
|
+
} from '../workflow/workflow-preferences'
|
|
19
29
|
import { configManager, dateHelper, out, PrjctCommandsBase } from './base'
|
|
20
30
|
|
|
21
31
|
export class WorkflowCommands extends PrjctCommandsBase {
|
|
@@ -24,7 +34,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
24
34
|
*/
|
|
25
35
|
async now(
|
|
26
36
|
task: string | null = null,
|
|
27
|
-
projectPath: string = process.cwd()
|
|
37
|
+
projectPath: string = process.cwd(),
|
|
38
|
+
options: { skipHooks?: boolean } = {}
|
|
28
39
|
): Promise<CommandResult> {
|
|
29
40
|
try {
|
|
30
41
|
const initResult = await this.ensureProjectInit(projectPath)
|
|
@@ -37,6 +48,15 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
if (task) {
|
|
51
|
+
// Run before_task hooks (using memory-based preferences)
|
|
52
|
+
const beforeResult = await runWorkflowHooks(projectId, 'before', 'task', {
|
|
53
|
+
projectPath,
|
|
54
|
+
skipHooks: options.skipHooks,
|
|
55
|
+
})
|
|
56
|
+
if (!beforeResult.success) {
|
|
57
|
+
return { success: false, error: `Hook failed: ${beforeResult.failed}` }
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
// AGENTIC: Use CommandExecutor for full orchestration support
|
|
41
61
|
const result = await commandExecutor.execute('task', { task }, projectPath)
|
|
42
62
|
|
|
@@ -97,6 +117,12 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
97
117
|
timestamp: dateHelper.getTimestamp(),
|
|
98
118
|
})
|
|
99
119
|
|
|
120
|
+
// Run after_task hooks
|
|
121
|
+
await runWorkflowHooks(projectId, 'after', 'task', {
|
|
122
|
+
projectPath,
|
|
123
|
+
skipHooks: options.skipHooks,
|
|
124
|
+
})
|
|
125
|
+
|
|
100
126
|
return {
|
|
101
127
|
// Include full CommandExecutor result first (orchestratorContext, prompt, etc.)
|
|
102
128
|
...result,
|
|
@@ -127,7 +153,10 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
127
153
|
/**
|
|
128
154
|
* /p:done - Complete current task
|
|
129
155
|
*/
|
|
130
|
-
async done(
|
|
156
|
+
async done(
|
|
157
|
+
projectPath: string = process.cwd(),
|
|
158
|
+
options: { skipHooks?: boolean } = {}
|
|
159
|
+
): Promise<CommandResult> {
|
|
131
160
|
try {
|
|
132
161
|
const initResult = await this.ensureProjectInit(projectPath)
|
|
133
162
|
if (!initResult.success) return initResult
|
|
@@ -146,6 +175,15 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
146
175
|
return { success: true, message: 'No active task to complete' }
|
|
147
176
|
}
|
|
148
177
|
|
|
178
|
+
// Run before_done hooks (using memory-based preferences)
|
|
179
|
+
const beforeResult = await runWorkflowHooks(projectId, 'before', 'done', {
|
|
180
|
+
projectPath,
|
|
181
|
+
skipHooks: options.skipHooks,
|
|
182
|
+
})
|
|
183
|
+
if (!beforeResult.success) {
|
|
184
|
+
return { success: false, error: `Hook failed: ${beforeResult.failed}` }
|
|
185
|
+
}
|
|
186
|
+
|
|
149
187
|
const task = currentTask.description
|
|
150
188
|
let duration = ''
|
|
151
189
|
if (currentTask.startedAt) {
|
|
@@ -184,6 +222,13 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
184
222
|
duration,
|
|
185
223
|
timestamp: dateHelper.getTimestamp(),
|
|
186
224
|
})
|
|
225
|
+
|
|
226
|
+
// Run after_done hooks
|
|
227
|
+
await runWorkflowHooks(projectId, 'after', 'done', {
|
|
228
|
+
projectPath,
|
|
229
|
+
skipHooks: options.skipHooks,
|
|
230
|
+
})
|
|
231
|
+
|
|
187
232
|
return { success: true, task, duration }
|
|
188
233
|
} catch (error) {
|
|
189
234
|
out.fail((error as Error).message)
|
|
@@ -312,4 +357,62 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
312
357
|
return { success: false, error: (error as Error).message }
|
|
313
358
|
}
|
|
314
359
|
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* /p:workflow - View and manage workflow preferences
|
|
363
|
+
*
|
|
364
|
+
* When called without arguments, shows current preferences.
|
|
365
|
+
* With arguments, parses natural language and updates preferences.
|
|
366
|
+
*/
|
|
367
|
+
async workflow(
|
|
368
|
+
input: string | null = null,
|
|
369
|
+
projectPath: string = process.cwd()
|
|
370
|
+
): Promise<CommandResult> {
|
|
371
|
+
try {
|
|
372
|
+
const initResult = await this.ensureProjectInit(projectPath)
|
|
373
|
+
if (!initResult.success) return initResult
|
|
374
|
+
|
|
375
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
376
|
+
if (!projectId) {
|
|
377
|
+
out.fail('no project ID')
|
|
378
|
+
return { success: false, error: 'No project ID found' }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!input) {
|
|
382
|
+
// Show current preferences
|
|
383
|
+
const preferences = await listWorkflowPreferences(projectId)
|
|
384
|
+
console.log(formatWorkflowPreferences(preferences))
|
|
385
|
+
return { success: true, preferences }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Return info for template-based processing
|
|
389
|
+
// The template/LLM will parse the natural language and call the appropriate functions
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
392
|
+
projectId,
|
|
393
|
+
input,
|
|
394
|
+
// Export functions for template use
|
|
395
|
+
setWorkflowPreference: async (pref: {
|
|
396
|
+
hook: HookPhase
|
|
397
|
+
command: HookCommand
|
|
398
|
+
action: string
|
|
399
|
+
scope: PreferenceScope
|
|
400
|
+
}) => {
|
|
401
|
+
await setWorkflowPreference(projectId, {
|
|
402
|
+
...pref,
|
|
403
|
+
createdAt: dateHelper.getTimestamp(),
|
|
404
|
+
})
|
|
405
|
+
},
|
|
406
|
+
removeWorkflowPreference: async (hook: HookPhase, command: HookCommand) => {
|
|
407
|
+
await removeWorkflowPreference(projectId, hook, command)
|
|
408
|
+
},
|
|
409
|
+
listWorkflowPreferences: async () => {
|
|
410
|
+
return listWorkflowPreferences(projectId)
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
out.fail((error as Error).message)
|
|
415
|
+
return { success: false, error: (error as Error).message }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
315
418
|
}
|