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,335 @@
1
+ const fs = require('fs').promises
2
+ const pathManager = require('./path-manager')
3
+ const { VERSION } = require('./version')
4
+
5
+ /**
6
+ * ConfigManager - Manages prjct.config.json files
7
+ *
8
+ * Key responsibilities:
9
+ * - Read and write prjct.config.json
10
+ * - Validate configuration structure
11
+ * - Create new configurations
12
+ * - Update existing configurations
13
+ *
14
+ * @version 0.2.1
15
+ */
16
+ class ConfigManager {
17
+ /**
18
+ * Read the project configuration file
19
+ *
20
+ * @param {string} projectPath - Path to the project
21
+ * @returns {Promise<Object|null>} - Configuration object or null if not found
22
+ */
23
+ async readConfig(projectPath) {
24
+ try {
25
+ const configPath = pathManager.getLocalConfigPath(projectPath)
26
+ const content = await fs.readFile(configPath, 'utf-8')
27
+ return JSON.parse(content)
28
+ } catch {
29
+ return null
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Write the project configuration file
35
+ *
36
+ * @param {string} projectPath - Path to the project
37
+ * @param {Object} config - Configuration object
38
+ * @returns {Promise<void>}
39
+ */
40
+ async writeConfig(projectPath, config) {
41
+ const configPath = pathManager.getLocalConfigPath(projectPath)
42
+ const configDir = pathManager.getLegacyPrjctPath(projectPath)
43
+
44
+ await fs.mkdir(configDir, { recursive: true })
45
+
46
+ const content = JSON.stringify(config, null, 2)
47
+ await fs.writeFile(configPath, content + '\n', 'utf-8')
48
+ }
49
+
50
+ /**
51
+ * Read the global project configuration file
52
+ * Contains authors array and other system data
53
+ *
54
+ * @param {string} projectId - Project identifier
55
+ * @returns {Promise<Object|null>} - Configuration object or null if not found
56
+ */
57
+ async readGlobalConfig(projectId) {
58
+ try {
59
+ const configPath = pathManager.getGlobalProjectConfigPath(projectId)
60
+ const content = await fs.readFile(configPath, 'utf-8')
61
+ return JSON.parse(content)
62
+ } catch {
63
+ return null
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Write the global project configuration file
69
+ *
70
+ * @param {string} projectId - Project identifier
71
+ * @param {Object} config - Configuration object
72
+ * @returns {Promise<void>}
73
+ */
74
+ async writeGlobalConfig(projectId, config) {
75
+ const configPath = pathManager.getGlobalProjectConfigPath(projectId)
76
+ const configDir = pathManager.getGlobalProjectPath(projectId)
77
+
78
+ await fs.mkdir(configDir, { recursive: true })
79
+
80
+ const content = JSON.stringify(config, null, 2)
81
+ await fs.writeFile(configPath, content + '\n', 'utf-8')
82
+ }
83
+
84
+ /**
85
+ * Ensure global config exists, create if not
86
+ *
87
+ * @param {string} projectId - Project identifier
88
+ * @returns {Promise<Object>} - Global configuration
89
+ */
90
+ async ensureGlobalConfig(projectId) {
91
+ let globalConfig = await this.readGlobalConfig(projectId)
92
+
93
+ if (!globalConfig) {
94
+ const now = new Date().toISOString()
95
+ globalConfig = {
96
+ projectId,
97
+ authors: [],
98
+ version: VERSION,
99
+ lastSync: now,
100
+ }
101
+ await this.writeGlobalConfig(projectId, globalConfig)
102
+ }
103
+
104
+ return globalConfig
105
+ }
106
+
107
+ /**
108
+ * Create a new project configuration
109
+ *
110
+ * @param {string} projectPath - Path to the project
111
+ * @param {Object} author - Author information {name, email, github}
112
+ * @returns {Promise<Object>} - Created configuration
113
+ */
114
+ async createConfig(projectPath, author) {
115
+ const projectId = pathManager.generateProjectId(projectPath)
116
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
117
+ const displayPath = pathManager.getDisplayPath(globalPath)
118
+ const now = new Date().toISOString()
119
+
120
+ const localConfig = {
121
+ projectId,
122
+ dataPath: displayPath,
123
+ }
124
+
125
+ await this.writeConfig(projectPath, localConfig)
126
+
127
+ const globalConfig = {
128
+ projectId,
129
+ authors: [
130
+ {
131
+ name: author.name || 'Unknown',
132
+ email: author.email || '',
133
+ github: author.github || '',
134
+ firstContribution: now,
135
+ lastActivity: now,
136
+ },
137
+ ],
138
+ version: VERSION,
139
+ created: now,
140
+ lastSync: now,
141
+ }
142
+
143
+ await this.writeGlobalConfig(projectId, globalConfig)
144
+
145
+ return localConfig
146
+ }
147
+
148
+ /**
149
+ * Update the lastSync timestamp in global config
150
+ *
151
+ * @param {string} projectPath - Path to the project
152
+ * @returns {Promise<void>}
153
+ */
154
+ async updateLastSync(projectPath) {
155
+ const projectId = await this.getProjectId(projectPath)
156
+ const globalConfig = await this.readGlobalConfig(projectId)
157
+ if (globalConfig) {
158
+ globalConfig.lastSync = new Date().toISOString()
159
+ await this.writeGlobalConfig(projectId, globalConfig)
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Validate a local configuration object
165
+ * Local config only contains project metadata (projectId, dataPath)
166
+ * All system data (version, created, lastSync, authors) is in global config
167
+ *
168
+ * @param {Object} config - Configuration to validate
169
+ * @returns {boolean} - True if valid
170
+ */
171
+ validateConfig(config) {
172
+ if (!config) return false
173
+ if (!config.projectId) return false
174
+ if (!config.dataPath) return false
175
+
176
+ return true
177
+ }
178
+
179
+ /**
180
+ * Check if a project needs migration
181
+ * Migration is needed if:
182
+ * - Has legacy .prjct/ structure
183
+ * - AND either no config exists OR files not yet in global location
184
+ *
185
+ * @param {string} projectPath - Path to the project
186
+ * @returns {Promise<boolean>} - True if migration needed
187
+ */
188
+ async needsMigration(projectPath) {
189
+ const hasLegacy = await pathManager.hasLegacyStructure(projectPath)
190
+ if (!hasLegacy) return false
191
+
192
+ const hasConfig = await pathManager.hasConfig(projectPath)
193
+
194
+ if (!hasConfig) return true
195
+
196
+ const config = await this.readConfig(projectPath)
197
+ if (!config || !config.projectId) return true
198
+
199
+ const globalPath = pathManager.getGlobalProjectPath(config.projectId)
200
+ const fs = require('fs').promises
201
+ const path = require('path')
202
+
203
+ try {
204
+ const coreFiles = await fs.readdir(path.join(globalPath, 'core'))
205
+ return coreFiles.length === 0
206
+ } catch {
207
+ return true
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Get the project ID from config, or generate it if config doesn't exist
213
+ *
214
+ * @param {string} projectPath - Path to the project
215
+ * @returns {Promise<string>} - Project ID
216
+ */
217
+ async getProjectId(projectPath) {
218
+ const config = await this.readConfig(projectPath)
219
+ if (config && config.projectId) {
220
+ return config.projectId
221
+ }
222
+ return pathManager.generateProjectId(projectPath)
223
+ }
224
+
225
+ /**
226
+ * Find an author in the authors array by github username
227
+ * Reads from GLOBAL config
228
+ *
229
+ * @param {string} projectId - Project identifier
230
+ * @param {string} githubUsername - GitHub username to search for
231
+ * @returns {Promise<Object|null>} - Author object or null if not found
232
+ */
233
+ async findAuthor(projectId, githubUsername) {
234
+ const globalConfig = await this.readGlobalConfig(projectId)
235
+ if (!globalConfig || !globalConfig.authors) return null
236
+
237
+ return globalConfig.authors.find(a => a.github === githubUsername) || null
238
+ }
239
+
240
+ /**
241
+ * Add a new author to the authors array
242
+ * Writes to GLOBAL config
243
+ *
244
+ * @param {string} projectId - Project identifier
245
+ * @param {Object} author - Author information {name, email, github}
246
+ * @returns {Promise<void>}
247
+ */
248
+ async addAuthor(projectId, author) {
249
+ const globalConfig = await this.ensureGlobalConfig(projectId)
250
+
251
+ const exists = globalConfig.authors.some(a => a.github === author.github)
252
+ if (exists) return
253
+
254
+ const now = new Date().toISOString()
255
+ globalConfig.authors.push({
256
+ name: author.name || 'Unknown',
257
+ email: author.email || '',
258
+ github: author.github || '',
259
+ firstContribution: now,
260
+ lastActivity: now,
261
+ })
262
+
263
+ globalConfig.lastSync = now
264
+ await this.writeGlobalConfig(projectId, globalConfig)
265
+ }
266
+
267
+ /**
268
+ * Update author's last activity timestamp
269
+ * Updates GLOBAL config
270
+ *
271
+ * @param {string} projectId - Project identifier
272
+ * @param {string} githubUsername - GitHub username
273
+ * @returns {Promise<void>}
274
+ */
275
+ async updateAuthorActivity(projectId, githubUsername) {
276
+ const globalConfig = await this.readGlobalConfig(projectId)
277
+ if (!globalConfig || !globalConfig.authors) return
278
+
279
+ const author = globalConfig.authors.find(a => a.github === githubUsername)
280
+ if (author) {
281
+ author.lastActivity = new Date().toISOString()
282
+ globalConfig.lastSync = author.lastActivity
283
+ await this.writeGlobalConfig(projectId, globalConfig)
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Get current author for session (detect or get from global config)
289
+ *
290
+ * @param {string} projectPath - Path to the project
291
+ * @returns {Promise<string>} - GitHub username of current author
292
+ */
293
+ async getCurrentAuthor(projectPath) {
294
+ const authorDetector = require('./author-detector')
295
+ const author = await authorDetector.detect()
296
+
297
+ const projectId = await this.getProjectId(projectPath)
298
+ await this.addAuthor(projectId, author)
299
+
300
+ return author.github || author.name || 'Unknown'
301
+ }
302
+
303
+ /**
304
+ * Check if config exists and is valid
305
+ *
306
+ * @param {string} projectPath - Path to the project
307
+ * @returns {Promise<boolean>} - True if valid config exists
308
+ */
309
+ async isConfigured(projectPath) {
310
+ const config = await this.readConfig(projectPath)
311
+ return this.validateConfig(config)
312
+ }
313
+
314
+ /**
315
+ * Get configuration with defaults
316
+ * Returns LOCAL config only (projectId, dataPath)
317
+ *
318
+ * @param {string} projectPath - Path to the project
319
+ * @returns {Promise<Object>} - Configuration with defaults
320
+ */
321
+ async getConfigWithDefaults(projectPath) {
322
+ const config = await this.readConfig(projectPath)
323
+ if (config) {
324
+ return config
325
+ }
326
+
327
+ const projectId = pathManager.generateProjectId(projectPath)
328
+ return {
329
+ projectId,
330
+ dataPath: pathManager.getDisplayPath(pathManager.getGlobalProjectPath(projectId)),
331
+ }
332
+ }
333
+ }
334
+
335
+ module.exports = new ConfigManager()