prjct-cli 0.10.14 → 0.11.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 (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/dev.js +217 -0
  3. package/bin/prjct +10 -0
  4. package/bin/serve.js +78 -0
  5. package/core/bus/index.js +322 -0
  6. package/core/command-registry.js +65 -0
  7. package/core/domain/snapshot-manager.js +375 -0
  8. package/core/plugin/hooks.js +313 -0
  9. package/core/plugin/index.js +52 -0
  10. package/core/plugin/loader.js +331 -0
  11. package/core/plugin/registry.js +325 -0
  12. package/core/plugins/webhook.js +143 -0
  13. package/core/session/index.js +449 -0
  14. package/core/session/metrics.js +293 -0
  15. package/package.json +28 -4
  16. package/packages/shared/dist/index.d.ts +615 -0
  17. package/packages/shared/dist/index.js +204 -0
  18. package/packages/shared/package.json +29 -0
  19. package/packages/shared/src/index.ts +9 -0
  20. package/packages/shared/src/schemas.ts +124 -0
  21. package/packages/shared/src/types.ts +187 -0
  22. package/packages/shared/src/utils.ts +148 -0
  23. package/packages/shared/tsconfig.json +18 -0
  24. package/packages/web/README.md +36 -0
  25. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  26. package/packages/web/app/api/claude/status/route.ts +34 -0
  27. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  29. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  31. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  32. package/packages/web/app/api/projects/route.ts +16 -0
  33. package/packages/web/app/api/sessions/history/route.ts +122 -0
  34. package/packages/web/app/api/stats/route.ts +38 -0
  35. package/packages/web/app/error.tsx +34 -0
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +155 -0
  38. package/packages/web/app/layout.tsx +43 -0
  39. package/packages/web/app/loading.tsx +7 -0
  40. package/packages/web/app/not-found.tsx +25 -0
  41. package/packages/web/app/page.tsx +227 -0
  42. package/packages/web/app/project/[id]/error.tsx +41 -0
  43. package/packages/web/app/project/[id]/loading.tsx +9 -0
  44. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  45. package/packages/web/app/project/[id]/page.tsx +253 -0
  46. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  47. package/packages/web/app/sessions/page.tsx +165 -0
  48. package/packages/web/app/settings/page.tsx +150 -0
  49. package/packages/web/components/AppSidebar.tsx +113 -0
  50. package/packages/web/components/CommandButton.tsx +39 -0
  51. package/packages/web/components/ConnectionStatus.tsx +29 -0
  52. package/packages/web/components/Logo.tsx +65 -0
  53. package/packages/web/components/MarkdownContent.tsx +123 -0
  54. package/packages/web/components/ProjectAvatar.tsx +54 -0
  55. package/packages/web/components/TechStackBadges.tsx +20 -0
  56. package/packages/web/components/TerminalTab.tsx +84 -0
  57. package/packages/web/components/TerminalTabs.tsx +210 -0
  58. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  59. package/packages/web/components/providers.tsx +45 -0
  60. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  61. package/packages/web/components/ui/badge.tsx +46 -0
  62. package/packages/web/components/ui/button.tsx +60 -0
  63. package/packages/web/components/ui/card.tsx +92 -0
  64. package/packages/web/components/ui/chart.tsx +385 -0
  65. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  66. package/packages/web/components/ui/scroll-area.tsx +58 -0
  67. package/packages/web/components/ui/sheet.tsx +139 -0
  68. package/packages/web/components/ui/tabs.tsx +66 -0
  69. package/packages/web/components/ui/tooltip.tsx +61 -0
  70. package/packages/web/components.json +22 -0
  71. package/packages/web/context/TerminalContext.tsx +45 -0
  72. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  73. package/packages/web/eslint.config.mjs +18 -0
  74. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  75. package/packages/web/hooks/useProjectStats.ts +38 -0
  76. package/packages/web/hooks/useProjects.ts +73 -0
  77. package/packages/web/hooks/useStats.ts +28 -0
  78. package/packages/web/lib/format.ts +23 -0
  79. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  80. package/packages/web/lib/projects.ts +452 -0
  81. package/packages/web/lib/pty.ts +101 -0
  82. package/packages/web/lib/query-config.ts +44 -0
  83. package/packages/web/lib/utils.ts +6 -0
  84. package/packages/web/next-env.d.ts +6 -0
  85. package/packages/web/next.config.ts +7 -0
  86. package/packages/web/package.json +53 -0
  87. package/packages/web/postcss.config.mjs +7 -0
  88. package/packages/web/public/file.svg +1 -0
  89. package/packages/web/public/globe.svg +1 -0
  90. package/packages/web/public/next.svg +1 -0
  91. package/packages/web/public/vercel.svg +1 -0
  92. package/packages/web/public/window.svg +1 -0
  93. package/packages/web/server.ts +262 -0
  94. package/packages/web/tsconfig.json +34 -0
  95. package/templates/commands/done.md +176 -54
  96. package/templates/commands/history.md +176 -0
  97. package/templates/commands/init.md +28 -1
  98. package/templates/commands/now.md +191 -9
  99. package/templates/commands/pause.md +176 -12
  100. package/templates/commands/redo.md +142 -0
  101. package/templates/commands/resume.md +166 -62
  102. package/templates/commands/serve.md +121 -0
  103. package/templates/commands/ship.md +45 -1
  104. package/templates/commands/sync.md +34 -1
  105. package/templates/commands/undo.md +152 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * PluginRegistry - Plugin Discovery and Management
3
+ *
4
+ * Central registry for plugin information, discovery, and management.
5
+ *
6
+ * @version 1.0.0
7
+ */
8
+
9
+ const fs = require('fs').promises
10
+ const path = require('path')
11
+ const pathManager = require('../infrastructure/path-manager')
12
+ const { pluginLoader } = require('./loader')
13
+
14
+ class PluginRegistry {
15
+ constructor() {
16
+ this.availablePlugins = new Map()
17
+ this.initialized = false
18
+ }
19
+
20
+ /**
21
+ * Initialize registry and discover available plugins
22
+ */
23
+ async initialize() {
24
+ if (this.initialized) return
25
+
26
+ await this.discoverPlugins()
27
+ this.initialized = true
28
+ }
29
+
30
+ /**
31
+ * Discover all available plugins (not necessarily loaded)
32
+ */
33
+ async discoverPlugins() {
34
+ // Built-in plugins
35
+ await this.discoverFromPath(
36
+ path.join(__dirname, '..', 'plugins'),
37
+ 'builtin'
38
+ )
39
+
40
+ // Global plugins
41
+ await this.discoverFromPath(
42
+ path.join(pathManager.getGlobalStoragePath(), 'plugins'),
43
+ 'global'
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Discover plugins from a path
49
+ * @param {string} dir
50
+ * @param {string} source
51
+ */
52
+ async discoverFromPath(dir, source) {
53
+ try {
54
+ const entries = await fs.readdir(dir, { withFileTypes: true })
55
+
56
+ for (const entry of entries) {
57
+ try {
58
+ let pluginPath
59
+
60
+ if (entry.isFile() && entry.name.endsWith('.js')) {
61
+ pluginPath = path.join(dir, entry.name)
62
+ } else if (entry.isDirectory()) {
63
+ pluginPath = path.join(dir, entry.name, 'index.js')
64
+ } else {
65
+ continue
66
+ }
67
+
68
+ // Check if file exists
69
+ await fs.access(pluginPath)
70
+
71
+ // Read plugin metadata without loading
72
+ const metadata = await this.readPluginMetadata(pluginPath)
73
+ if (metadata) {
74
+ this.availablePlugins.set(metadata.name, {
75
+ ...metadata,
76
+ path: pluginPath,
77
+ source
78
+ })
79
+ }
80
+ } catch {
81
+ // Skip invalid plugins
82
+ }
83
+ }
84
+ } catch {
85
+ // Directory doesn't exist
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Read plugin metadata without fully loading
91
+ * @param {string} pluginPath
92
+ * @returns {Object|null}
93
+ */
94
+ async readPluginMetadata(pluginPath) {
95
+ try {
96
+ const content = await fs.readFile(pluginPath, 'utf-8')
97
+
98
+ // Extract basic metadata from comments or exports
99
+ const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/)
100
+ const versionMatch = content.match(/version:\s*['"]([^'"]+)['"]/)
101
+ const descMatch = content.match(/description:\s*['"]([^'"]+)['"]/)
102
+
103
+ if (nameMatch) {
104
+ return {
105
+ name: nameMatch[1],
106
+ version: versionMatch ? versionMatch[1] : '1.0.0',
107
+ description: descMatch ? descMatch[1] : ''
108
+ }
109
+ }
110
+
111
+ // Fallback: try to require (might have side effects)
112
+ const plugin = require(pluginPath)
113
+ delete require.cache[require.resolve(pluginPath)]
114
+
115
+ if (plugin.name) {
116
+ return {
117
+ name: plugin.name,
118
+ version: plugin.version || '1.0.0',
119
+ description: plugin.description || ''
120
+ }
121
+ }
122
+ } catch {
123
+ // Can't read metadata
124
+ }
125
+
126
+ return null
127
+ }
128
+
129
+ /**
130
+ * Get all available plugins
131
+ * @returns {Object[]}
132
+ */
133
+ getAvailable() {
134
+ return Array.from(this.availablePlugins.values())
135
+ }
136
+
137
+ /**
138
+ * Get available plugins by source
139
+ * @param {string} source
140
+ * @returns {Object[]}
141
+ */
142
+ getAvailableBySource(source) {
143
+ return this.getAvailable().filter(p => p.source === source)
144
+ }
145
+
146
+ /**
147
+ * Check if a plugin is available
148
+ * @param {string} name
149
+ * @returns {boolean}
150
+ */
151
+ isAvailable(name) {
152
+ return this.availablePlugins.has(name)
153
+ }
154
+
155
+ /**
156
+ * Check if a plugin is loaded
157
+ * @param {string} name
158
+ * @returns {boolean}
159
+ */
160
+ isLoaded(name) {
161
+ return pluginLoader.getPlugin(name) !== null
162
+ }
163
+
164
+ /**
165
+ * Get plugin info
166
+ * @param {string} name
167
+ * @returns {Object|null}
168
+ */
169
+ getPluginInfo(name) {
170
+ const available = this.availablePlugins.get(name)
171
+ const loaded = pluginLoader.getPlugin(name)
172
+
173
+ if (!available && !loaded) return null
174
+
175
+ return {
176
+ ...(available || {}),
177
+ loaded: !!loaded,
178
+ active: loaded ? true : false
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Install a plugin (copy to global plugins)
184
+ * @param {string} sourcePath - Path to plugin file/directory
185
+ * @param {string} [name] - Optional name override
186
+ */
187
+ async install(sourcePath, name = null) {
188
+ const globalPluginsPath = path.join(pathManager.getGlobalStoragePath(), 'plugins')
189
+ await fs.mkdir(globalPluginsPath, { recursive: true })
190
+
191
+ const stat = await fs.stat(sourcePath)
192
+
193
+ if (stat.isFile()) {
194
+ // Single file plugin
195
+ const pluginName = name || path.basename(sourcePath)
196
+ const destPath = path.join(globalPluginsPath, pluginName)
197
+ await fs.copyFile(sourcePath, destPath)
198
+ } else if (stat.isDirectory()) {
199
+ // Directory plugin
200
+ const pluginName = name || path.basename(sourcePath)
201
+ const destPath = path.join(globalPluginsPath, pluginName)
202
+ await this.copyDirectory(sourcePath, destPath)
203
+ }
204
+
205
+ // Refresh available plugins
206
+ await this.discoverPlugins()
207
+ }
208
+
209
+ /**
210
+ * Uninstall a plugin
211
+ * @param {string} name
212
+ */
213
+ async uninstall(name) {
214
+ const plugin = this.availablePlugins.get(name)
215
+
216
+ if (!plugin) {
217
+ throw new Error(`Plugin not found: ${name}`)
218
+ }
219
+
220
+ if (plugin.source === 'builtin') {
221
+ throw new Error('Cannot uninstall built-in plugins')
222
+ }
223
+
224
+ // Unload if loaded
225
+ if (this.isLoaded(name)) {
226
+ await pluginLoader.unloadPlugin(name)
227
+ }
228
+
229
+ // Delete plugin file/directory
230
+ const pluginPath = plugin.path
231
+ const stat = await fs.stat(path.dirname(pluginPath))
232
+
233
+ if (path.basename(pluginPath) === 'index.js') {
234
+ // Directory plugin
235
+ await fs.rm(path.dirname(pluginPath), { recursive: true })
236
+ } else {
237
+ // Single file plugin
238
+ await fs.unlink(pluginPath)
239
+ }
240
+
241
+ // Remove from registry
242
+ this.availablePlugins.delete(name)
243
+ }
244
+
245
+ /**
246
+ * Copy directory recursively
247
+ * @param {string} src
248
+ * @param {string} dest
249
+ */
250
+ async copyDirectory(src, dest) {
251
+ await fs.mkdir(dest, { recursive: true })
252
+ const entries = await fs.readdir(src, { withFileTypes: true })
253
+
254
+ for (const entry of entries) {
255
+ const srcPath = path.join(src, entry.name)
256
+ const destPath = path.join(dest, entry.name)
257
+
258
+ if (entry.isDirectory()) {
259
+ await this.copyDirectory(srcPath, destPath)
260
+ } else {
261
+ await fs.copyFile(srcPath, destPath)
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Generate plugin list for display
268
+ * @returns {string}
269
+ */
270
+ generatePluginList() {
271
+ const available = this.getAvailable()
272
+
273
+ if (available.length === 0) {
274
+ return 'No plugins installed.'
275
+ }
276
+
277
+ let output = '## Installed Plugins\n\n'
278
+
279
+ const bySource = {
280
+ builtin: [],
281
+ global: [],
282
+ project: []
283
+ }
284
+
285
+ for (const plugin of available) {
286
+ bySource[plugin.source].push(plugin)
287
+ }
288
+
289
+ if (bySource.builtin.length > 0) {
290
+ output += '### Built-in\n'
291
+ for (const p of bySource.builtin) {
292
+ const status = this.isLoaded(p.name) ? '●' : '○'
293
+ output += `- ${status} **${p.name}** v${p.version}\n`
294
+ if (p.description) {
295
+ output += ` ${p.description}\n`
296
+ }
297
+ }
298
+ output += '\n'
299
+ }
300
+
301
+ if (bySource.global.length > 0) {
302
+ output += '### Global\n'
303
+ for (const p of bySource.global) {
304
+ const status = this.isLoaded(p.name) ? '●' : '○'
305
+ output += `- ${status} **${p.name}** v${p.version}\n`
306
+ if (p.description) {
307
+ output += ` ${p.description}\n`
308
+ }
309
+ }
310
+ output += '\n'
311
+ }
312
+
313
+ output += '\n● = loaded, ○ = available'
314
+
315
+ return output
316
+ }
317
+ }
318
+
319
+ // Singleton instance
320
+ const pluginRegistry = new PluginRegistry()
321
+
322
+ module.exports = {
323
+ PluginRegistry,
324
+ pluginRegistry
325
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Webhook Plugin for prjct-cli
3
+ *
4
+ * Sends HTTP POST requests to configured webhooks on events.
5
+ * Useful for integrating with Slack, Discord, Zapier, etc.
6
+ *
7
+ * @version 1.0.0
8
+ *
9
+ * Configuration in prjct.config.json:
10
+ * {
11
+ * "plugins": ["webhook"],
12
+ * "webhook": {
13
+ * "url": "https://hooks.example.com/...",
14
+ * "events": ["session.completed", "feature.shipped"],
15
+ * "secret": "optional-signing-secret"
16
+ * }
17
+ * }
18
+ */
19
+
20
+ const { EventTypes } = require('../bus')
21
+ const { HookPoints } = require('../plugin/hooks')
22
+
23
+ const plugin = {
24
+ name: 'webhook',
25
+ version: '1.0.0',
26
+ description: 'Send HTTP webhooks on events',
27
+
28
+ // Plugin state
29
+ config: null,
30
+ enabled: false,
31
+
32
+ /**
33
+ * Activate plugin
34
+ */
35
+ async activate({ config }) {
36
+ this.config = config
37
+
38
+ if (!config.url) {
39
+ console.warn('[webhook] No URL configured, plugin disabled')
40
+ return
41
+ }
42
+
43
+ this.enabled = true
44
+ this.events = config.events || [
45
+ EventTypes.SESSION_COMPLETED,
46
+ EventTypes.FEATURE_SHIPPED,
47
+ EventTypes.SNAPSHOT_CREATED
48
+ ]
49
+ },
50
+
51
+ /**
52
+ * Deactivate plugin
53
+ */
54
+ async deactivate() {
55
+ this.enabled = false
56
+ },
57
+
58
+ /**
59
+ * Event handlers
60
+ */
61
+ events: {
62
+ [EventTypes.SESSION_COMPLETED]: async function(data) {
63
+ await plugin.sendWebhook('session.completed', data)
64
+ },
65
+
66
+ [EventTypes.FEATURE_SHIPPED]: async function(data) {
67
+ await plugin.sendWebhook('feature.shipped', data)
68
+ },
69
+
70
+ [EventTypes.SNAPSHOT_CREATED]: async function(data) {
71
+ await plugin.sendWebhook('snapshot.created', data)
72
+ },
73
+
74
+ [EventTypes.TASK_COMPLETED]: async function(data) {
75
+ await plugin.sendWebhook('task.completed', data)
76
+ }
77
+ },
78
+
79
+ /**
80
+ * Hook handlers
81
+ */
82
+ hooks: {
83
+ [HookPoints.AFTER_FEATURE_SHIP]: async function(data) {
84
+ await plugin.sendWebhook('feature.shipped', {
85
+ feature: data.feature,
86
+ version: data.version,
87
+ timestamp: data.timestamp
88
+ })
89
+ }
90
+ },
91
+
92
+ /**
93
+ * Send webhook request
94
+ * @param {string} event - Event type
95
+ * @param {Object} data - Event data
96
+ */
97
+ async sendWebhook(event, data) {
98
+ if (!this.enabled || !this.config.url) return
99
+
100
+ // Check if this event should be sent
101
+ if (this.config.events && !this.config.events.includes(event)) {
102
+ return
103
+ }
104
+
105
+ const payload = {
106
+ event,
107
+ timestamp: new Date().toISOString(),
108
+ source: 'prjct-cli',
109
+ data
110
+ }
111
+
112
+ try {
113
+ const headers = {
114
+ 'Content-Type': 'application/json',
115
+ 'User-Agent': 'prjct-cli/webhook'
116
+ }
117
+
118
+ // Add signature if secret is configured
119
+ if (this.config.secret) {
120
+ const crypto = require('crypto')
121
+ const signature = crypto
122
+ .createHmac('sha256', this.config.secret)
123
+ .update(JSON.stringify(payload))
124
+ .digest('hex')
125
+ headers['X-Prjct-Signature'] = `sha256=${signature}`
126
+ }
127
+
128
+ const response = await fetch(this.config.url, {
129
+ method: 'POST',
130
+ headers,
131
+ body: JSON.stringify(payload)
132
+ })
133
+
134
+ if (!response.ok) {
135
+ console.error(`[webhook] Request failed: ${response.status}`)
136
+ }
137
+ } catch (error) {
138
+ console.error(`[webhook] Error sending webhook:`, error.message)
139
+ }
140
+ }
141
+ }
142
+
143
+ module.exports = plugin