prjct-cli 0.19.0 → 0.20.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.
Files changed (230) hide show
  1. package/CHANGELOG.md +66 -6
  2. package/CLAUDE.md +56 -15
  3. package/README.md +5 -6
  4. package/bin/prjct +59 -42
  5. package/bin/prjct.ts +60 -0
  6. package/core/__tests__/agentic/memory-system.test.ts +18 -3
  7. package/core/__tests__/agentic/plan-mode.test.ts +55 -26
  8. package/core/__tests__/agentic/prompt-builder.test.ts +6 -6
  9. package/core/__tests__/utils/project-commands.test.ts +72 -0
  10. package/core/agentic/agent-router.ts +3 -12
  11. package/core/agentic/command-executor.ts +372 -3
  12. package/core/agentic/context-builder.ts +7 -27
  13. package/core/agentic/ground-truth.ts +604 -5
  14. package/core/agentic/index.ts +180 -0
  15. package/core/agentic/loop-detector.ts +418 -4
  16. package/core/agentic/memory-system.ts +857 -3
  17. package/core/agentic/plan-mode.ts +491 -4
  18. package/core/agentic/prompt-builder.ts +44 -65
  19. package/core/agentic/services.ts +13 -5
  20. package/core/agentic/skill-loader.ts +112 -0
  21. package/core/agentic/smart-context.ts +37 -122
  22. package/core/agentic/template-loader.ts +79 -122
  23. package/core/agentic/tool-registry.ts +5 -11
  24. package/core/agents/index.ts +1 -1
  25. package/core/agents/performance.ts +4 -2
  26. package/core/bus/bus.ts +262 -0
  27. package/core/bus/index.ts +3 -313
  28. package/core/commands/analysis.ts +5 -5
  29. package/core/commands/analytics.ts +11 -11
  30. package/core/commands/base.ts +33 -209
  31. package/core/commands/cleanup.ts +148 -0
  32. package/core/commands/command-data.ts +346 -0
  33. package/core/commands/commands.ts +216 -0
  34. package/core/commands/design.ts +83 -0
  35. package/core/commands/index.ts +13 -207
  36. package/core/commands/maintenance.ts +52 -473
  37. package/core/commands/planning.ts +3 -3
  38. package/core/commands/register.ts +104 -0
  39. package/core/commands/registry.ts +441 -0
  40. package/core/commands/setup.ts +25 -9
  41. package/core/commands/shipping.ts +48 -11
  42. package/core/commands/snapshots.ts +299 -0
  43. package/core/commands/workflow.ts +2 -2
  44. package/core/constants/index.ts +254 -4
  45. package/core/domain/agent-loader.ts +5 -6
  46. package/core/domain/task-stack.ts +555 -4
  47. package/core/errors.ts +127 -1
  48. package/core/events/events.ts +87 -0
  49. package/core/events/index.ts +4 -138
  50. package/core/index.ts +15 -23
  51. package/core/infrastructure/agent-detector.ts +126 -201
  52. package/core/infrastructure/author-detector.ts +99 -171
  53. package/core/infrastructure/command-installer.ts +476 -4
  54. package/core/infrastructure/config-manager.ts +41 -37
  55. package/core/infrastructure/path-manager.ts +59 -9
  56. package/core/infrastructure/permission-manager.ts +286 -0
  57. package/core/outcomes/analyzer.ts +7 -41
  58. package/core/outcomes/index.ts +1 -1
  59. package/core/outcomes/recorder.ts +1 -1
  60. package/core/{plugins → plugin/builtin}/webhook.ts +6 -22
  61. package/core/plugin/loader.ts +5 -5
  62. package/core/plugin/registry.ts +2 -2
  63. package/core/schemas/ideas.ts +85 -54
  64. package/core/schemas/index.ts +14 -33
  65. package/core/schemas/permissions.ts +177 -0
  66. package/core/schemas/project.ts +39 -12
  67. package/core/schemas/roadmap.ts +94 -59
  68. package/core/schemas/schemas.ts +39 -0
  69. package/core/schemas/shipped.ts +87 -60
  70. package/core/schemas/state.ts +110 -70
  71. package/core/server/index.ts +21 -0
  72. package/core/server/routes.ts +165 -0
  73. package/core/server/server.ts +136 -0
  74. package/core/server/sse.ts +135 -0
  75. package/core/services/agent-service.ts +170 -0
  76. package/core/services/breakdown-service.ts +126 -0
  77. package/core/services/index.ts +21 -0
  78. package/core/services/memory-service.ts +108 -0
  79. package/core/services/project-service.ts +146 -0
  80. package/core/services/skill-service.ts +253 -0
  81. package/core/session/compaction.ts +257 -0
  82. package/core/session/index.ts +20 -8
  83. package/core/{infrastructure/session-manager/migration.ts → session/log-migration.ts} +9 -9
  84. package/core/{infrastructure/session-manager/session-manager.ts → session/session-log-manager.ts} +27 -26
  85. package/core/session/{session-manager.ts → task-session-manager.ts} +7 -4
  86. package/core/session/utils.ts +1 -1
  87. package/core/storage/ideas-storage.ts +10 -26
  88. package/core/storage/index.ts +14 -162
  89. package/core/storage/queue-storage.ts +13 -11
  90. package/core/storage/shipped-storage.ts +4 -17
  91. package/core/storage/state-storage.ts +35 -43
  92. package/core/storage/storage-manager.ts +42 -52
  93. package/core/storage/storage.ts +160 -0
  94. package/core/sync/auth-config.ts +1 -8
  95. package/core/sync/index.ts +17 -10
  96. package/core/sync/oauth-handler.ts +1 -6
  97. package/core/sync/sync-client.ts +6 -34
  98. package/core/sync/sync-manager.ts +11 -40
  99. package/core/types/agentic.ts +577 -0
  100. package/core/types/agents.ts +145 -0
  101. package/core/types/bus.ts +82 -0
  102. package/core/types/commands.ts +366 -0
  103. package/core/types/config.ts +66 -0
  104. package/core/types/core.ts +96 -0
  105. package/core/types/domain.ts +71 -0
  106. package/core/types/events.ts +42 -0
  107. package/core/types/fs.ts +56 -0
  108. package/core/types/index.ts +387 -500
  109. package/core/types/infrastructure.ts +196 -0
  110. package/core/{agentic/memory-system/types.ts → types/memory.ts} +33 -8
  111. package/core/{outcomes/types.ts → types/outcomes.ts} +53 -8
  112. package/core/types/plugin.ts +25 -0
  113. package/core/types/server.ts +54 -0
  114. package/core/types/services.ts +65 -0
  115. package/core/types/session.ts +135 -0
  116. package/core/types/storage.ts +148 -0
  117. package/core/types/sync.ts +121 -0
  118. package/core/types/task.ts +72 -0
  119. package/core/types/template.ts +24 -0
  120. package/core/types/utils.ts +90 -0
  121. package/core/utils/cache.ts +195 -0
  122. package/core/utils/collection-filters.ts +245 -0
  123. package/core/utils/date-helper.ts +1 -5
  124. package/core/utils/file-helper.ts +20 -10
  125. package/core/utils/jsonl-helper.ts +5 -8
  126. package/core/utils/markdown-builder.ts +277 -0
  127. package/core/utils/project-commands.ts +132 -0
  128. package/core/utils/runtime.ts +119 -0
  129. package/dist/bin/prjct.mjs +12568 -0
  130. package/package.json +13 -8
  131. package/scripts/build.js +106 -0
  132. package/scripts/postinstall.js +50 -8
  133. package/templates/agentic/agents/uxui.md +210 -0
  134. package/templates/agentic/subagent-generation.md +1 -1
  135. package/templates/commands/bug.md +219 -41
  136. package/templates/commands/feature.md +368 -80
  137. package/templates/commands/serve.md +118 -0
  138. package/templates/commands/ship.md +152 -14
  139. package/templates/commands/skill.md +110 -0
  140. package/templates/commands/sync.md +63 -4
  141. package/templates/commands/test.md +40 -188
  142. package/templates/mcp-config.json +0 -36
  143. package/templates/permissions/default.jsonc +60 -0
  144. package/templates/permissions/permissive.jsonc +49 -0
  145. package/templates/permissions/strict.jsonc +62 -0
  146. package/templates/skills/code-review.md +47 -0
  147. package/templates/skills/debug.md +61 -0
  148. package/templates/skills/refactor.md +47 -0
  149. package/templates/subagents/domain/devops.md +1 -1
  150. package/templates/subagents/domain/testing.md +6 -10
  151. package/templates/subagents/workflow/prjct-shipper.md +16 -7
  152. package/templates/tools/bash.txt +22 -0
  153. package/templates/tools/edit.txt +18 -0
  154. package/templates/tools/glob.txt +19 -0
  155. package/templates/tools/grep.txt +21 -0
  156. package/templates/tools/read.txt +14 -0
  157. package/templates/tools/task.txt +20 -0
  158. package/templates/tools/webfetch.txt +16 -0
  159. package/templates/tools/websearch.txt +18 -0
  160. package/templates/tools/write.txt +17 -0
  161. package/core/agentic/command-executor/command-executor.ts +0 -312
  162. package/core/agentic/command-executor/index.ts +0 -16
  163. package/core/agentic/command-executor/status-signal.ts +0 -38
  164. package/core/agentic/command-executor/types.ts +0 -79
  165. package/core/agentic/ground-truth/index.ts +0 -76
  166. package/core/agentic/ground-truth/types.ts +0 -33
  167. package/core/agentic/ground-truth/utils.ts +0 -48
  168. package/core/agentic/ground-truth/verifiers/analyze.ts +0 -54
  169. package/core/agentic/ground-truth/verifiers/done.ts +0 -75
  170. package/core/agentic/ground-truth/verifiers/feature.ts +0 -70
  171. package/core/agentic/ground-truth/verifiers/index.ts +0 -37
  172. package/core/agentic/ground-truth/verifiers/init.ts +0 -52
  173. package/core/agentic/ground-truth/verifiers/now.ts +0 -57
  174. package/core/agentic/ground-truth/verifiers/ship.ts +0 -85
  175. package/core/agentic/ground-truth/verifiers/spec.ts +0 -45
  176. package/core/agentic/ground-truth/verifiers/sync.ts +0 -47
  177. package/core/agentic/ground-truth/verifiers.ts +0 -6
  178. package/core/agentic/loop-detector/error-analysis.ts +0 -97
  179. package/core/agentic/loop-detector/hallucination.ts +0 -71
  180. package/core/agentic/loop-detector/index.ts +0 -41
  181. package/core/agentic/loop-detector/loop-detector.ts +0 -222
  182. package/core/agentic/loop-detector/types.ts +0 -66
  183. package/core/agentic/memory-system/history.ts +0 -53
  184. package/core/agentic/memory-system/index.ts +0 -192
  185. package/core/agentic/memory-system/patterns.ts +0 -156
  186. package/core/agentic/memory-system/semantic-memories.ts +0 -278
  187. package/core/agentic/memory-system/session.ts +0 -21
  188. package/core/agentic/plan-mode/approval.ts +0 -57
  189. package/core/agentic/plan-mode/constants.ts +0 -44
  190. package/core/agentic/plan-mode/index.ts +0 -28
  191. package/core/agentic/plan-mode/plan-mode.ts +0 -407
  192. package/core/agentic/plan-mode/types.ts +0 -193
  193. package/core/agents/types.ts +0 -126
  194. package/core/command-registry/categories.ts +0 -23
  195. package/core/command-registry/commands.ts +0 -15
  196. package/core/command-registry/core-commands.ts +0 -344
  197. package/core/command-registry/index.ts +0 -158
  198. package/core/command-registry/optional-commands.ts +0 -163
  199. package/core/command-registry/setup-commands.ts +0 -83
  200. package/core/command-registry/types.ts +0 -59
  201. package/core/command-registry.ts +0 -9
  202. package/core/commands/types.ts +0 -185
  203. package/core/commands.ts +0 -11
  204. package/core/constants/formats.ts +0 -187
  205. package/core/context-sync.ts +0 -18
  206. package/core/data/index.ts +0 -27
  207. package/core/data/md-base-manager.ts +0 -203
  208. package/core/data/md-ideas-manager.ts +0 -155
  209. package/core/data/md-queue-manager.ts +0 -180
  210. package/core/data/md-shipped-manager.ts +0 -90
  211. package/core/data/md-state-manager.ts +0 -137
  212. package/core/domain/task-stack/index.ts +0 -19
  213. package/core/domain/task-stack/parser.ts +0 -86
  214. package/core/domain/task-stack/storage.ts +0 -123
  215. package/core/domain/task-stack/task-stack.ts +0 -340
  216. package/core/domain/task-stack/types.ts +0 -51
  217. package/core/infrastructure/command-installer/command-installer.ts +0 -327
  218. package/core/infrastructure/command-installer/global-config.ts +0 -136
  219. package/core/infrastructure/command-installer/index.ts +0 -25
  220. package/core/infrastructure/command-installer/types.ts +0 -41
  221. package/core/infrastructure/session-manager/index.ts +0 -23
  222. package/core/infrastructure/session-manager/types.ts +0 -45
  223. package/core/infrastructure/session-manager.ts +0 -8
  224. package/core/serializers/ideas-serializer.ts +0 -187
  225. package/core/serializers/index.ts +0 -36
  226. package/core/serializers/queue-serializer.ts +0 -210
  227. package/core/serializers/shipped-serializer.ts +0 -108
  228. package/core/serializers/state-serializer.ts +0 -136
  229. package/core/session/types.ts +0 -29
  230. /package/core/infrastructure/{agents/claude-agent.ts → claude-agent.ts} +0 -0
@@ -1,8 +1,480 @@
1
1
  /**
2
- * CommandInstaller
3
- * Re-exports from command-installer/index.ts for backwards compatibility.
2
+ * Command Installer
3
+ * Installs prjct commands in Claude Code and other editors.
4
+ *
5
+ * 100% Claude-focused architecture
6
+ * Handles installation and synchronization of /p:* commands
7
+ * to Claude's native slash command system
8
+ *
9
+ * @version 0.5.0
4
10
  */
5
11
 
6
- import commandInstaller from './command-installer/index'
7
- export * from './command-installer/index'
12
+ import fs from 'fs/promises'
13
+ import path from 'path'
14
+ import os from 'os'
15
+ import type {
16
+ InstallResult,
17
+ UninstallResult,
18
+ CheckResult,
19
+ SyncResult,
20
+ GlobalConfigResult,
21
+ } from '../types'
22
+
23
+ // =============================================================================
24
+ // Global Config
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Install documentation files to ~/.prjct-cli/docs/
29
+ */
30
+ export async function installDocs(): Promise<{ success: boolean; error?: string }> {
31
+ try {
32
+ const docsDir = path.join(os.homedir(), '.prjct-cli', 'docs')
33
+ const templateDocsDir = path.join(__dirname, '../../templates/global/docs')
34
+
35
+ // Ensure docs directory exists
36
+ await fs.mkdir(docsDir, { recursive: true })
37
+
38
+ // Read all doc files from template
39
+ const docFiles = await fs.readdir(templateDocsDir)
40
+
41
+ // Copy each doc file
42
+ for (const file of docFiles) {
43
+ if (file.endsWith('.md')) {
44
+ const srcPath = path.join(templateDocsDir, file)
45
+ const destPath = path.join(docsDir, file)
46
+ const content = await fs.readFile(srcPath, 'utf-8')
47
+ await fs.writeFile(destPath, content, 'utf-8')
48
+ }
49
+ }
50
+
51
+ return { success: true }
52
+ } catch (error) {
53
+ return { success: false, error: (error as Error).message }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Install or update global CLAUDE.md configuration
59
+ */
60
+ export async function installGlobalConfig(
61
+ claudeConfigPath: string,
62
+ detectClaude: () => Promise<boolean>
63
+ ): Promise<GlobalConfigResult> {
64
+ const claudeDetected = await detectClaude()
65
+
66
+ if (!claudeDetected) {
67
+ return {
68
+ success: false,
69
+ error: 'Claude not detected',
70
+ action: 'skipped',
71
+ }
72
+ }
73
+
74
+ try {
75
+ // Ensure ~/.claude directory exists
76
+ const claudeDir = path.join(os.homedir(), '.claude')
77
+ await fs.mkdir(claudeDir, { recursive: true })
78
+
79
+ const globalConfigPath = path.join(claudeDir, 'CLAUDE.md')
80
+ const templatePath = path.join(__dirname, '../../templates/global/CLAUDE.md')
81
+
82
+ // Read template content
83
+ const templateContent = await fs.readFile(templatePath, 'utf-8')
84
+
85
+ // Check if global config already exists
86
+ let existingContent = ''
87
+ let fileExists = false
88
+
89
+ try {
90
+ existingContent = await fs.readFile(globalConfigPath, 'utf-8')
91
+ fileExists = true
92
+ } catch {
93
+ // File doesn't exist, will create new
94
+ fileExists = false
95
+ }
96
+
97
+ if (!fileExists) {
98
+ // Create new file with full template
99
+ await fs.writeFile(globalConfigPath, templateContent, 'utf-8')
100
+ return {
101
+ success: true,
102
+ action: 'created',
103
+ path: globalConfigPath,
104
+ }
105
+ } else {
106
+ // File exists - perform intelligent merge
107
+ const startMarker = '<!-- prjct:start - DO NOT REMOVE THIS MARKER -->'
108
+ const endMarker = '<!-- prjct:end - DO NOT REMOVE THIS MARKER -->'
109
+
110
+ // Check if markers exist in existing file
111
+ const hasMarkers =
112
+ existingContent.includes(startMarker) && existingContent.includes(endMarker)
113
+
114
+ if (!hasMarkers) {
115
+ // No markers - append prjct section at the end
116
+ const updatedContent = existingContent + '\n\n' + templateContent
117
+ await fs.writeFile(globalConfigPath, updatedContent, 'utf-8')
118
+ return {
119
+ success: true,
120
+ action: 'appended',
121
+ path: globalConfigPath,
122
+ }
123
+ } else {
124
+ // Markers exist - replace content between markers
125
+ const beforeMarker = existingContent.substring(0, existingContent.indexOf(startMarker))
126
+ const afterMarker = existingContent.substring(
127
+ existingContent.indexOf(endMarker) + endMarker.length
128
+ )
129
+
130
+ // Extract prjct section from template
131
+ const prjctSection = templateContent.substring(
132
+ templateContent.indexOf(startMarker),
133
+ templateContent.indexOf(endMarker) + endMarker.length
134
+ )
135
+
136
+ const updatedContent = beforeMarker + prjctSection + afterMarker
137
+ await fs.writeFile(globalConfigPath, updatedContent, 'utf-8')
138
+ return {
139
+ success: true,
140
+ action: 'updated',
141
+ path: globalConfigPath,
142
+ }
143
+ }
144
+ }
145
+ } catch (error) {
146
+ return {
147
+ success: false,
148
+ error: (error as Error).message,
149
+ action: 'failed',
150
+ }
151
+ }
152
+ }
153
+
154
+ // =============================================================================
155
+ // Command Installer
156
+ // =============================================================================
157
+
158
+ export class CommandInstaller {
159
+ homeDir: string
160
+ claudeCommandsPath: string
161
+ claudeConfigPath: string
162
+ templatesDir: string
163
+
164
+ constructor() {
165
+ this.homeDir = os.homedir()
166
+ this.claudeCommandsPath = path.join(this.homeDir, '.claude', 'commands', 'p')
167
+ this.claudeConfigPath = path.join(this.homeDir, '.claude')
168
+ this.templatesDir = path.join(__dirname, '..', '..', 'templates', 'commands')
169
+ }
170
+
171
+ /**
172
+ * Detect if Claude is installed
173
+ */
174
+ async detectClaude(): Promise<boolean> {
175
+ try {
176
+ await fs.access(this.claudeConfigPath)
177
+ return true
178
+ } catch {
179
+ return false
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Get list of command files to install
185
+ */
186
+ async getCommandFiles(): Promise<string[]> {
187
+ try {
188
+ const files = await fs.readdir(this.templatesDir)
189
+ return files.filter((f) => f.endsWith('.md'))
190
+ } catch {
191
+ // Fallback to core commands if template directory not accessible
192
+ return [
193
+ 'init.md',
194
+ 'now.md',
195
+ 'done.md',
196
+ 'ship.md',
197
+ 'next.md',
198
+ 'idea.md',
199
+ 'recap.md',
200
+ 'progress.md',
201
+ 'stuck.md',
202
+ 'context.md',
203
+ 'analyze.md',
204
+ 'sync.md',
205
+ 'roadmap.md',
206
+ 'task.md',
207
+ 'git.md',
208
+ 'fix.md',
209
+ 'test.md',
210
+ 'cleanup.md',
211
+ 'design.md',
212
+ ]
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Install commands to Claude
218
+ */
219
+ async installCommands(): Promise<InstallResult> {
220
+ const claudeDetected = await this.detectClaude()
221
+
222
+ if (!claudeDetected) {
223
+ return {
224
+ success: false,
225
+ error: 'Claude not detected. Please install Claude Code or Claude Desktop first.',
226
+ }
227
+ }
228
+
229
+ try {
230
+ // Ensure commands directory exists
231
+ await fs.mkdir(this.claudeCommandsPath, { recursive: true })
232
+
233
+ const commandFiles = await this.getCommandFiles()
234
+ const installed: string[] = []
235
+ const errors: Array<{ file: string; error: string }> = []
236
+
237
+ for (const file of commandFiles) {
238
+ try {
239
+ const sourcePath = path.join(this.templatesDir, file)
240
+ const destPath = path.join(this.claudeCommandsPath, file)
241
+
242
+ const content = await fs.readFile(sourcePath, 'utf-8')
243
+ await fs.writeFile(destPath, content, 'utf-8')
244
+
245
+ installed.push(file.replace('.md', ''))
246
+ } catch (error) {
247
+ errors.push({ file, error: (error as Error).message })
248
+ }
249
+ }
250
+
251
+ return {
252
+ success: true,
253
+ installed,
254
+ errors,
255
+ path: this.claudeCommandsPath,
256
+ }
257
+ } catch (error) {
258
+ return {
259
+ success: false,
260
+ error: (error as Error).message,
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Uninstall commands from Claude
267
+ */
268
+ async uninstallCommands(): Promise<UninstallResult> {
269
+ try {
270
+ const commandFiles = await this.getCommandFiles()
271
+ const uninstalled: string[] = []
272
+ const errors: Array<{ file: string; error: string }> = []
273
+
274
+ for (const file of commandFiles) {
275
+ try {
276
+ const filePath = path.join(this.claudeCommandsPath, file)
277
+ await fs.unlink(filePath)
278
+ uninstalled.push(file.replace('.md', ''))
279
+ } catch (error) {
280
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
281
+ errors.push({ file, error: (error as Error).message })
282
+ }
283
+ }
284
+ }
285
+
286
+ // Try to remove the /p directory if empty
287
+ try {
288
+ await fs.rmdir(this.claudeCommandsPath)
289
+ } catch {
290
+ // Directory not empty or doesn't exist - that's fine
291
+ }
292
+
293
+ return {
294
+ success: true,
295
+ uninstalled,
296
+ errors,
297
+ }
298
+ } catch (error) {
299
+ return {
300
+ success: false,
301
+ error: (error as Error).message,
302
+ }
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Check if commands are already installed
308
+ */
309
+ async checkInstallation(): Promise<CheckResult> {
310
+ const claudeDetected = await this.detectClaude()
311
+
312
+ if (!claudeDetected) {
313
+ return {
314
+ installed: false,
315
+ claudeDetected: false,
316
+ }
317
+ }
318
+
319
+ try {
320
+ await fs.access(this.claudeCommandsPath)
321
+ const files = await fs.readdir(this.claudeCommandsPath)
322
+ const installedCommands = files
323
+ .filter((f) => f.endsWith('.md'))
324
+ .map((f) => f.replace('.md', ''))
325
+
326
+ return {
327
+ installed: installedCommands.length > 0,
328
+ claudeDetected: true,
329
+ commands: installedCommands,
330
+ path: this.claudeCommandsPath,
331
+ }
332
+ } catch {
333
+ return {
334
+ installed: false,
335
+ claudeDetected: true,
336
+ commands: [],
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Update commands (reinstall with latest templates)
343
+ */
344
+ async updateCommands(): Promise<InstallResult> {
345
+ // Simply reinstall - will overwrite with latest templates
346
+ console.log('Updating commands with latest templates...')
347
+ const result = await this.installCommands()
348
+ if (result.success && result.installed) {
349
+ console.log(`Updated ${result.installed.length} commands`)
350
+ }
351
+ return result
352
+ }
353
+
354
+ /**
355
+ * Install to all detected editors (alias for installCommands)
356
+ */
357
+ async installToAll(): Promise<InstallResult> {
358
+ return await this.installCommands()
359
+ }
360
+
361
+ /**
362
+ * Get installation path for Claude commands
363
+ */
364
+ getInstallPath(): string {
365
+ return this.claudeCommandsPath
366
+ }
367
+
368
+ /**
369
+ * Verify command template exists
370
+ */
371
+ async verifyTemplate(commandName: string): Promise<boolean> {
372
+ try {
373
+ const templatePath = path.join(this.templatesDir, `${commandName}.md`)
374
+ await fs.access(templatePath)
375
+ return true
376
+ } catch {
377
+ return false
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Sync commands - intelligent update that detects and removes orphans
383
+ */
384
+ async syncCommands(): Promise<SyncResult> {
385
+ const claudeDetected = await this.detectClaude()
386
+
387
+ if (!claudeDetected) {
388
+ return {
389
+ success: false,
390
+ error: 'Claude not detected',
391
+ added: 0,
392
+ updated: 0,
393
+ removed: 0,
394
+ }
395
+ }
396
+
397
+ try {
398
+ // Ensure commands directory exists
399
+ await fs.mkdir(this.claudeCommandsPath, { recursive: true })
400
+
401
+ // Get current state
402
+ const templateFiles = await this.getCommandFiles()
403
+ let installedFiles: string[] = []
404
+
405
+ try {
406
+ installedFiles = await fs.readdir(this.claudeCommandsPath)
407
+ installedFiles = installedFiles.filter((f) => f.endsWith('.md'))
408
+ } catch {
409
+ // Directory doesn't exist yet
410
+ installedFiles = []
411
+ }
412
+
413
+ const results: SyncResult = {
414
+ success: true,
415
+ added: 0,
416
+ updated: 0,
417
+ removed: 0,
418
+ errors: [],
419
+ }
420
+
421
+ // Install/update all template files (always overwrite)
422
+ for (const file of templateFiles) {
423
+ try {
424
+ const sourcePath = path.join(this.templatesDir, file)
425
+ const destPath = path.join(this.claudeCommandsPath, file)
426
+
427
+ // Check if file exists in installed location
428
+ const exists = installedFiles.includes(file)
429
+
430
+ // Read and write (always overwrite to ensure latest version)
431
+ const content = await fs.readFile(sourcePath, 'utf-8')
432
+ await fs.writeFile(destPath, content, 'utf-8')
433
+
434
+ if (!exists) {
435
+ results.added++
436
+ } else {
437
+ results.updated++
438
+ }
439
+ } catch (error) {
440
+ results.errors!.push({ file, error: (error as Error).message })
441
+ }
442
+ }
443
+
444
+ // Note: We do NOT remove orphaned files
445
+ // Legacy commands from older versions are preserved
446
+ // to avoid breaking existing workflows
447
+
448
+ return results
449
+ } catch (error) {
450
+ return {
451
+ success: false,
452
+ error: (error as Error).message,
453
+ added: 0,
454
+ updated: 0,
455
+ removed: 0,
456
+ }
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Install or update global CLAUDE.md configuration
462
+ */
463
+ async installGlobalConfig(): Promise<GlobalConfigResult> {
464
+ return installGlobalConfig(this.claudeConfigPath, () => this.detectClaude())
465
+ }
466
+
467
+ /**
468
+ * Install documentation files to ~/.prjct-cli/docs/
469
+ */
470
+ async installDocs(): Promise<{ success: boolean; error?: string }> {
471
+ return installDocs()
472
+ }
473
+ }
474
+
475
+ // =============================================================================
476
+ // Exports
477
+ // =============================================================================
478
+
479
+ const commandInstaller = new CommandInstaller()
8
480
  export default commandInstaller
@@ -2,59 +2,58 @@
2
2
  * ConfigManager - Manages prjct.config.json files
3
3
  *
4
4
  * Key responsibilities:
5
- * - Read and write prjct.config.json
5
+ * - Read and write prjct.config.json (supports .jsonc with comments)
6
6
  * - Validate configuration structure
7
7
  * - Create new configurations
8
8
  * - Update existing configurations
9
9
  *
10
- * @version 0.2.1
10
+ * @version 0.3.0
11
11
  */
12
12
 
13
13
  import fs from 'fs/promises'
14
14
  import path from 'path'
15
+ import * as jsonc from 'jsonc-parser'
15
16
  import pathManager from './path-manager'
16
17
  import authorDetector from './author-detector'
17
18
  import { VERSION } from '../utils/version'
18
- import { ConfigError, getErrorMessage } from '../errors'
19
-
20
- interface Author {
21
- name: string
22
- email: string
23
- github: string
24
- firstContribution?: string
25
- lastActivity?: string
26
- }
19
+ import { getTimestamp } from '../utils/date-helper'
20
+ import { getErrorMessage } from '../errors'
21
+ import { isNotFoundError } from '../types/fs'
22
+ import type { Author, LocalConfig, GlobalConfig } from '../types'
27
23
 
28
- interface LocalConfig {
29
- projectId: string
30
- dataPath: string
31
- authors?: Author[]
32
- author?: Author
33
- version?: string
34
- created?: string
35
- lastSync?: string
36
- }
24
+ // Re-export types for convenience
25
+ export type { Author, LocalConfig, GlobalConfig } from '../types'
37
26
 
38
- interface GlobalConfig {
39
- projectId: string
40
- authors: Author[]
41
- version: string
42
- created?: string
43
- lastSync: string
27
+ /**
28
+ * Parse JSON or JSONC content safely
29
+ * Supports comments (line and block style) in config files
30
+ */
31
+ function parseJsonc<T>(content: string): T {
32
+ const errors: jsonc.ParseError[] = []
33
+ const result = jsonc.parse(content, errors, {
34
+ allowTrailingComma: true,
35
+ disallowComments: false,
36
+ })
37
+ if (errors.length > 0) {
38
+ const firstError = errors[0]
39
+ throw new SyntaxError(`JSON parse error at offset ${firstError.offset}: ${jsonc.printParseErrorCode(firstError.error)}`)
40
+ }
41
+ return result
44
42
  }
45
43
 
46
44
  class ConfigManager {
47
45
  /**
48
46
  * Read the project configuration file
47
+ * Supports both .json and .jsonc formats (with comments)
49
48
  */
50
49
  async readConfig(projectPath: string): Promise<LocalConfig | null> {
51
50
  try {
52
51
  const configPath = pathManager.getLocalConfigPath(projectPath)
53
52
  const content = await fs.readFile(configPath, 'utf-8')
54
- return JSON.parse(content)
53
+ return parseJsonc<LocalConfig>(content)
55
54
  } catch (error) {
56
55
  // File not found is expected - return null
57
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
56
+ if (isNotFoundError(error)) {
58
57
  return null
59
58
  }
60
59
  // JSON parse errors or other issues - log and return null
@@ -79,15 +78,16 @@ class ConfigManager {
79
78
  /**
80
79
  * Read the global project configuration file
81
80
  * Contains authors array and other system data
81
+ * Supports both .json and .jsonc formats (with comments)
82
82
  */
83
83
  async readGlobalConfig(projectId: string): Promise<GlobalConfig | null> {
84
84
  try {
85
85
  const configPath = pathManager.getGlobalProjectConfigPath(projectId)
86
86
  const content = await fs.readFile(configPath, 'utf-8')
87
- return JSON.parse(content)
87
+ return parseJsonc<GlobalConfig>(content)
88
88
  } catch (error) {
89
89
  // File not found is expected for new projects
90
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
90
+ if (isNotFoundError(error)) {
91
91
  return null
92
92
  }
93
93
  // Log other errors for debugging
@@ -116,7 +116,7 @@ class ConfigManager {
116
116
  let globalConfig = await this.readGlobalConfig(projectId)
117
117
 
118
118
  if (!globalConfig) {
119
- const now = new Date().toISOString()
119
+ const now = getTimestamp()
120
120
  globalConfig = {
121
121
  projectId,
122
122
  authors: [],
@@ -139,7 +139,7 @@ class ConfigManager {
139
139
  const projectId = pathManager.generateProjectId(projectPath)
140
140
  const globalPath = pathManager.getGlobalProjectPath(projectId)
141
141
  const displayPath = pathManager.getDisplayPath(globalPath)
142
- const now = new Date().toISOString()
142
+ const now = getTimestamp()
143
143
 
144
144
  const localConfig: LocalConfig = {
145
145
  projectId,
@@ -176,7 +176,7 @@ class ConfigManager {
176
176
  const projectId = await this.getProjectId(projectPath)
177
177
  const globalConfig = await this.readGlobalConfig(projectId)
178
178
  if (globalConfig) {
179
- globalConfig.lastSync = new Date().toISOString()
179
+ globalConfig.lastSync = getTimestamp()
180
180
  await this.writeGlobalConfig(projectId, globalConfig)
181
181
  }
182
182
  }
@@ -218,7 +218,7 @@ class ConfigManager {
218
218
  return coreFiles.length === 0
219
219
  } catch (error) {
220
220
  // Directory not found means migration needed
221
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
221
+ if (isNotFoundError(error)) {
222
222
  return true
223
223
  }
224
224
  // Permission errors or other issues - assume migration needed
@@ -261,7 +261,7 @@ class ConfigManager {
261
261
  const exists = globalConfig.authors.some((a) => a.github === author.github)
262
262
  if (exists) return
263
263
 
264
- const now = new Date().toISOString()
264
+ const now = getTimestamp()
265
265
  globalConfig.authors.push({
266
266
  name: author.name || 'Unknown',
267
267
  email: author.email || '',
@@ -284,7 +284,7 @@ class ConfigManager {
284
284
 
285
285
  const author = globalConfig.authors.find((a) => a.github === githubUsername)
286
286
  if (author) {
287
- author.lastActivity = new Date().toISOString()
287
+ author.lastActivity = getTimestamp()
288
288
  globalConfig.lastSync = author.lastActivity
289
289
  await this.writeGlobalConfig(projectId, globalConfig)
290
290
  }
@@ -297,7 +297,11 @@ class ConfigManager {
297
297
  const author = await authorDetector.detect()
298
298
 
299
299
  const projectId = await this.getProjectId(projectPath)
300
- await this.addAuthor(projectId, author)
300
+ await this.addAuthor(projectId, {
301
+ name: author.name ?? undefined,
302
+ email: author.email ?? undefined,
303
+ github: author.github ?? undefined
304
+ })
301
305
 
302
306
  return author.github || author.name || 'Unknown'
303
307
  }