prjct-cli 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/CLAUDE.md +300 -0
  3. package/LICENSE +21 -0
  4. package/README.md +424 -0
  5. package/bin/prjct +214 -0
  6. package/core/agent-detector.js +249 -0
  7. package/core/agents/claude-agent.js +250 -0
  8. package/core/agents/codex-agent.js +256 -0
  9. package/core/agents/terminal-agent.js +465 -0
  10. package/core/analyzer.js +596 -0
  11. package/core/animations-simple.js +240 -0
  12. package/core/animations.js +277 -0
  13. package/core/author-detector.js +218 -0
  14. package/core/capability-installer.js +190 -0
  15. package/core/command-installer.js +775 -0
  16. package/core/commands.js +2050 -0
  17. package/core/config-manager.js +335 -0
  18. package/core/migrator.js +784 -0
  19. package/core/path-manager.js +324 -0
  20. package/core/project-capabilities.js +144 -0
  21. package/core/session-manager.js +439 -0
  22. package/core/version.js +107 -0
  23. package/core/workflow-engine.js +213 -0
  24. package/core/workflow-prompts.js +192 -0
  25. package/core/workflow-rules.js +147 -0
  26. package/package.json +80 -0
  27. package/scripts/install.sh +433 -0
  28. package/scripts/verify-installation.sh +158 -0
  29. package/templates/agents/AGENTS.md +164 -0
  30. package/templates/commands/analyze.md +125 -0
  31. package/templates/commands/cleanup.md +102 -0
  32. package/templates/commands/context.md +105 -0
  33. package/templates/commands/design.md +113 -0
  34. package/templates/commands/done.md +44 -0
  35. package/templates/commands/fix.md +87 -0
  36. package/templates/commands/git.md +79 -0
  37. package/templates/commands/help.md +72 -0
  38. package/templates/commands/idea.md +50 -0
  39. package/templates/commands/init.md +237 -0
  40. package/templates/commands/next.md +74 -0
  41. package/templates/commands/now.md +35 -0
  42. package/templates/commands/progress.md +92 -0
  43. package/templates/commands/recap.md +86 -0
  44. package/templates/commands/roadmap.md +107 -0
  45. package/templates/commands/ship.md +41 -0
  46. package/templates/commands/stuck.md +48 -0
  47. package/templates/commands/task.md +97 -0
  48. package/templates/commands/test.md +94 -0
  49. package/templates/commands/workflow.md +224 -0
  50. package/templates/examples/natural-language-examples.md +320 -0
  51. package/templates/mcp-config.json +8 -0
  52. package/templates/workflows/analyze.md +159 -0
  53. package/templates/workflows/cleanup.md +73 -0
  54. package/templates/workflows/context.md +72 -0
  55. package/templates/workflows/design.md +88 -0
  56. package/templates/workflows/done.md +20 -0
  57. package/templates/workflows/fix.md +201 -0
  58. package/templates/workflows/git.md +192 -0
  59. package/templates/workflows/help.md +13 -0
  60. package/templates/workflows/idea.md +22 -0
  61. package/templates/workflows/init.md +80 -0
  62. package/templates/workflows/natural-language-handler.md +183 -0
  63. package/templates/workflows/next.md +44 -0
  64. package/templates/workflows/now.md +19 -0
  65. package/templates/workflows/progress.md +113 -0
  66. package/templates/workflows/recap.md +66 -0
  67. package/templates/workflows/roadmap.md +95 -0
  68. package/templates/workflows/ship.md +18 -0
  69. package/templates/workflows/stuck.md +25 -0
  70. package/templates/workflows/task.md +109 -0
  71. package/templates/workflows/test.md +243 -0
@@ -0,0 +1,324 @@
1
+ const fs = require('fs').promises
2
+ const path = require('path')
3
+ const crypto = require('crypto')
4
+ const os = require('os')
5
+
6
+ /**
7
+ * PathManager - Manages project paths between local and global storage
8
+ *
9
+ * Key responsibilities:
10
+ * - Generate unique project identifiers from project paths
11
+ * - Manage paths between local project and global storage
12
+ * - Ensure directory structures exist
13
+ *
14
+ * @version 0.2.1
15
+ */
16
+ class PathManager {
17
+ constructor() {
18
+ this.globalBaseDir = path.join(os.homedir(), '.prjct-cli')
19
+ this.globalProjectsDir = path.join(this.globalBaseDir, 'projects')
20
+ this.globalConfigDir = path.join(this.globalBaseDir, 'config')
21
+ }
22
+
23
+ /**
24
+ * Generate a unique project ID from the absolute project path
25
+ * Uses SHA-256 hash of the absolute path for consistency
26
+ *
27
+ * @param {string} projectPath - Absolute path to the project
28
+ * @returns {string} - 12-character hash ID
29
+ */
30
+ generateProjectId(projectPath) {
31
+ const absolutePath = path.resolve(projectPath)
32
+ const hash = crypto.createHash('sha256').update(absolutePath).digest('hex')
33
+ return hash.substring(0, 12) // Use first 12 chars for readability
34
+ }
35
+
36
+ /**
37
+ * Get the base global storage path
38
+ *
39
+ * @returns {string} - Path to global base directory (~/.prjct-cli)
40
+ */
41
+ getGlobalBasePath() {
42
+ return this.globalBaseDir
43
+ }
44
+
45
+ /**
46
+ * Get the global storage path for a project
47
+ *
48
+ * @param {string} projectId - The project identifier
49
+ * @returns {string} - Path to global project storage
50
+ */
51
+ getGlobalProjectPath(projectId) {
52
+ return path.join(this.globalProjectsDir, projectId)
53
+ }
54
+
55
+ /**
56
+ * Get the local config file path for a project
57
+ *
58
+ * @param {string} projectPath - Path to the project
59
+ * @returns {string} - Path to .prjct/prjct.config.json
60
+ */
61
+ getLocalConfigPath(projectPath) {
62
+ return path.join(projectPath, '.prjct', 'prjct.config.json')
63
+ }
64
+
65
+ /**
66
+ * Get the global config file path for a project
67
+ * This file stores authors and other system data that shouldn't be versioned
68
+ *
69
+ * @param {string} projectId - The project identifier
70
+ * @returns {string} - Path to ~/.prjct-cli/projects/{id}/project.json
71
+ */
72
+ getGlobalProjectConfigPath(projectId) {
73
+ return path.join(this.getGlobalProjectPath(projectId), 'project.json')
74
+ }
75
+
76
+ /**
77
+ * Get the legacy .prjct directory path
78
+ *
79
+ * @param {string} projectPath - Path to the project
80
+ * @returns {string} - Path to legacy .prjct directory
81
+ */
82
+ getLegacyPrjctPath(projectPath) {
83
+ return path.join(projectPath, '.prjct')
84
+ }
85
+
86
+ /**
87
+ * Check if a project has legacy .prjct directory
88
+ *
89
+ * @param {string} projectPath - Path to the project
90
+ * @returns {Promise<boolean>} - True if legacy directory exists
91
+ */
92
+ async hasLegacyStructure(projectPath) {
93
+ try {
94
+ const legacyPath = this.getLegacyPrjctPath(projectPath)
95
+ await fs.access(legacyPath)
96
+ return true
97
+ } catch {
98
+ return false
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Check if a project has the new config file
104
+ *
105
+ * @param {string} projectPath - Path to the project
106
+ * @returns {Promise<boolean>} - True if config exists
107
+ */
108
+ async hasConfig(projectPath) {
109
+ try {
110
+ const configPath = this.getLocalConfigPath(projectPath)
111
+ await fs.access(configPath)
112
+ return true
113
+ } catch {
114
+ return false
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Ensure the global directory structure exists
120
+ * Creates all necessary directories in ~/.prjct-cli/
121
+ *
122
+ * @returns {Promise<void>}
123
+ */
124
+ async ensureGlobalStructure() {
125
+ await fs.mkdir(this.globalBaseDir, { recursive: true })
126
+ await fs.mkdir(this.globalProjectsDir, { recursive: true })
127
+ await fs.mkdir(this.globalConfigDir, { recursive: true })
128
+ }
129
+
130
+ /**
131
+ * Ensure the project-specific global structure exists
132
+ * Creates the layered directory structure for a project
133
+ *
134
+ * @param {string} projectId - The project identifier
135
+ * @returns {Promise<string>} - Path to the project's global storage
136
+ */
137
+ async ensureProjectStructure(projectId) {
138
+ await this.ensureGlobalStructure()
139
+
140
+ const projectPath = this.getGlobalProjectPath(projectId)
141
+
142
+ const layers = ['core', 'progress', 'planning', 'analysis', 'memory']
143
+
144
+ for (const layer of layers) {
145
+ await fs.mkdir(path.join(projectPath, layer), { recursive: true })
146
+ }
147
+
148
+ await fs.mkdir(path.join(projectPath, 'planning', 'tasks'), { recursive: true })
149
+
150
+ await fs.mkdir(path.join(projectPath, 'sessions'), { recursive: true })
151
+
152
+ return projectPath
153
+ }
154
+
155
+ /**
156
+ * Get session directory path for a specific date
157
+ * Creates hierarchical structure: sessions/YYYY/MM/DD/
158
+ *
159
+ * @param {string} projectId - The project identifier
160
+ * @param {Date} date - Date for the session (defaults to today)
161
+ * @returns {string} - Path to session directory
162
+ */
163
+ getSessionPath(projectId, date = new Date()) {
164
+ const year = date.getFullYear().toString()
165
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
166
+ const day = date.getDate().toString().padStart(2, '0')
167
+
168
+ return path.join(
169
+ this.getGlobalProjectPath(projectId),
170
+ 'sessions',
171
+ year,
172
+ month,
173
+ day,
174
+ )
175
+ }
176
+
177
+ /**
178
+ * Get current session directory path (today)
179
+ *
180
+ * @param {string} projectId - The project identifier
181
+ * @returns {string} - Path to today's session directory
182
+ */
183
+ getCurrentSessionPath(projectId) {
184
+ return this.getSessionPath(projectId, new Date())
185
+ }
186
+
187
+ /**
188
+ * Ensure session directory exists for a specific date
189
+ *
190
+ * @param {string} projectId - The project identifier
191
+ * @param {Date} date - Date for the session (defaults to today)
192
+ * @returns {Promise<string>} - Path to session directory
193
+ */
194
+ async ensureSessionPath(projectId, date = new Date()) {
195
+ const sessionPath = this.getSessionPath(projectId, date)
196
+ await fs.mkdir(sessionPath, { recursive: true })
197
+ return sessionPath
198
+ }
199
+
200
+ /**
201
+ * List all session dates for a project
202
+ *
203
+ * @param {string} projectId - The project identifier
204
+ * @param {number} year - Optional year filter
205
+ * @param {number} month - Optional month filter (1-12)
206
+ * @returns {Promise<Array<{year: string, month: string, day: string, path: string}>>} - Array of session info
207
+ */
208
+ async listSessions(projectId, year = null, month = null) {
209
+ const sessionsDir = path.join(this.getGlobalProjectPath(projectId), 'sessions')
210
+ const sessions = []
211
+
212
+ try {
213
+ const years = await fs.readdir(sessionsDir, { withFileTypes: true })
214
+
215
+ for (const yearEntry of years) {
216
+ if (!yearEntry.isDirectory()) continue
217
+ if (year && yearEntry.name !== year.toString()) continue
218
+
219
+ const yearPath = path.join(sessionsDir, yearEntry.name)
220
+ const months = await fs.readdir(yearPath, { withFileTypes: true })
221
+
222
+ for (const monthEntry of months) {
223
+ if (!monthEntry.isDirectory()) continue
224
+ if (month && monthEntry.name !== month.toString().padStart(2, '0')) continue
225
+
226
+ const monthPath = path.join(yearPath, monthEntry.name)
227
+ const days = await fs.readdir(monthPath, { withFileTypes: true })
228
+
229
+ for (const dayEntry of days) {
230
+ if (!dayEntry.isDirectory()) continue
231
+
232
+ sessions.push({
233
+ year: yearEntry.name,
234
+ month: monthEntry.name,
235
+ day: dayEntry.name,
236
+ path: path.join(monthPath, dayEntry.name),
237
+ date: new Date(`${yearEntry.name}-${monthEntry.name}-${dayEntry.name}`),
238
+ })
239
+ }
240
+ }
241
+ }
242
+
243
+ sessions.sort((a, b) => b.date - a.date)
244
+ return sessions
245
+ } catch {
246
+ return []
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Get sessions within a date range
252
+ *
253
+ * @param {string} projectId - The project identifier
254
+ * @param {Date} fromDate - Start date (inclusive)
255
+ * @param {Date} toDate - End date (inclusive, defaults to today)
256
+ * @returns {Promise<Array>} - Array of session info within range
257
+ */
258
+ async getSessionsInRange(projectId, fromDate, toDate = new Date()) {
259
+ const allSessions = await this.listSessions(projectId)
260
+
261
+ return allSessions.filter(session => session.date >= fromDate && session.date <= toDate)
262
+ }
263
+
264
+ /**
265
+ * Get the path for a specific file in the global structure
266
+ *
267
+ * @param {string} projectId - The project identifier
268
+ * @param {string} layer - The layer (core, progress, planning, analysis, memory)
269
+ * @param {string} filename - The filename
270
+ * @returns {string} - Full path to the file
271
+ */
272
+ getFilePath(projectId, layer, filename) {
273
+ return path.join(this.getGlobalProjectPath(projectId), layer, filename)
274
+ }
275
+
276
+ /**
277
+ * Get all project IDs in global storage
278
+ *
279
+ * @returns {Promise<string[]>} - Array of project IDs
280
+ */
281
+ async listProjects() {
282
+ try {
283
+ await this.ensureGlobalStructure()
284
+ const entries = await fs.readdir(this.globalProjectsDir, { withFileTypes: true })
285
+ return entries
286
+ .filter(entry => entry.isDirectory())
287
+ .map(entry => entry.name)
288
+ } catch {
289
+ return []
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Check if a project exists in global storage
295
+ *
296
+ * @param {string} projectId - The project identifier
297
+ * @returns {Promise<boolean>} - True if project exists
298
+ */
299
+ async projectExists(projectId) {
300
+ try {
301
+ const projectPath = this.getGlobalProjectPath(projectId)
302
+ await fs.access(projectPath)
303
+ return true
304
+ } catch {
305
+ return false
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Get the relative path from home directory for display
311
+ *
312
+ * @param {string} absolutePath - Absolute path
313
+ * @returns {string} - Path with ~ notation
314
+ */
315
+ getDisplayPath(absolutePath) {
316
+ const homeDir = os.homedir()
317
+ if (absolutePath.startsWith(homeDir)) {
318
+ return absolutePath.replace(homeDir, '~')
319
+ }
320
+ return absolutePath
321
+ }
322
+ }
323
+
324
+ module.exports = new PathManager()
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Project Capabilities Detector
3
+ * Detects ONLY what exists - no assumptions, no hallucinations
4
+ */
5
+
6
+ const fs = require('fs').promises
7
+ const path = require('path')
8
+
9
+ class ProjectCapabilities {
10
+ /**
11
+ * Detect project capabilities
12
+ * @param {string} projectPath - Project root path
13
+ * @returns {Promise<Object>} Capabilities object
14
+ */
15
+ async detect(projectPath = process.cwd()) {
16
+ return {
17
+ design: await this.hasDesign(projectPath),
18
+ test: await this.hasTest(projectPath),
19
+ docs: await this.hasDocs(projectPath),
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Check if project has design system
25
+ */
26
+ async hasDesign(projectPath) {
27
+ return (
28
+ await this.hasFolder(projectPath, 'design') ||
29
+ await this.hasFolder(projectPath, 'designs') ||
30
+ await this.hasFolder(projectPath, '.storybook') ||
31
+ await this.hasDep(projectPath, '@storybook/react') ||
32
+ await this.hasDep(projectPath, '@storybook/vue') ||
33
+ await this.hasFiles(projectPath, '**/*.figma')
34
+ )
35
+ }
36
+
37
+ /**
38
+ * Check if project has test framework
39
+ */
40
+ async hasTest(projectPath) {
41
+ return (
42
+ await this.hasDep(projectPath, 'jest') ||
43
+ await this.hasDep(projectPath, 'vitest') ||
44
+ await this.hasDep(projectPath, 'mocha') ||
45
+ await this.hasDep(projectPath, '@jest/core') ||
46
+ await this.hasFiles(projectPath, '**/*.{test,spec}.{js,ts,jsx,tsx}') ||
47
+ await this.hasFile(projectPath, 'jest.config.js') ||
48
+ await this.hasFile(projectPath, 'vitest.config.js')
49
+ )
50
+ }
51
+
52
+ /**
53
+ * Check if project has documentation system
54
+ */
55
+ async hasDocs(projectPath) {
56
+ return (
57
+ await this.hasFolder(projectPath, 'docs') ||
58
+ await this.hasFolder(projectPath, 'documentation') ||
59
+ await this.hasFile(projectPath, 'README.md') ||
60
+ await this.hasDep(projectPath, 'typedoc') ||
61
+ await this.hasDep(projectPath, 'jsdoc')
62
+ )
63
+ }
64
+
65
+ /**
66
+ * Check if folder exists
67
+ */
68
+ async hasFolder(projectPath, folderName) {
69
+ try {
70
+ const folderPath = path.join(projectPath, folderName)
71
+ const stat = await fs.stat(folderPath)
72
+ return stat.isDirectory()
73
+ } catch {
74
+ return false
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Check if file exists
80
+ */
81
+ async hasFile(projectPath, fileName) {
82
+ try {
83
+ const filePath = path.join(projectPath, fileName)
84
+ await fs.access(filePath)
85
+ return true
86
+ } catch {
87
+ return false
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Check if files matching pattern exist
93
+ */
94
+ async hasFiles(projectPath, pattern) {
95
+ try {
96
+ // Convert glob pattern to regex
97
+ const regex = this.globToRegex(pattern)
98
+ const files = await fs.readdir(projectPath, { recursive: true })
99
+
100
+ // Filter by pattern and ignore node_modules, dist, build
101
+ return files.some(file => {
102
+ const skip = file.includes('node_modules/') ||
103
+ file.includes('dist/') ||
104
+ file.includes('build/')
105
+ return !skip && regex.test(file)
106
+ })
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Convert simple glob pattern to regex
114
+ */
115
+ globToRegex(pattern) {
116
+ // Convert **/*.{test,spec}.{js,ts,jsx,tsx} to regex
117
+ const escaped = pattern
118
+ .replace(/\./g, '\\.')
119
+ .replace(/\*\*/g, '.*')
120
+ .replace(/\*/g, '[^/]*')
121
+ .replace(/\{([^}]+)\}/g, (_, group) => `(${group.split(',').join('|')})`)
122
+ return new RegExp(escaped)
123
+ }
124
+
125
+ /**
126
+ * Check if dependency exists in package.json
127
+ */
128
+ async hasDep(projectPath, depName) {
129
+ try {
130
+ const pkgPath = path.join(projectPath, 'package.json')
131
+ const content = await fs.readFile(pkgPath, 'utf8')
132
+ const pkg = JSON.parse(content)
133
+
134
+ return !!(
135
+ (pkg.dependencies && pkg.dependencies[depName]) ||
136
+ (pkg.devDependencies && pkg.devDependencies[depName])
137
+ )
138
+ } catch {
139
+ return false
140
+ }
141
+ }
142
+ }
143
+
144
+ module.exports = new ProjectCapabilities()