coder-config 0.40.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +553 -0
  3. package/cli.js +431 -0
  4. package/config-loader.js +294 -0
  5. package/hooks/activity-track.sh +56 -0
  6. package/hooks/codex-workstream.sh +44 -0
  7. package/hooks/gemini-workstream.sh +44 -0
  8. package/hooks/workstream-inject.sh +20 -0
  9. package/lib/activity.js +283 -0
  10. package/lib/apply.js +344 -0
  11. package/lib/cli.js +267 -0
  12. package/lib/config.js +171 -0
  13. package/lib/constants.js +55 -0
  14. package/lib/env.js +114 -0
  15. package/lib/index.js +47 -0
  16. package/lib/init.js +122 -0
  17. package/lib/mcps.js +139 -0
  18. package/lib/memory.js +201 -0
  19. package/lib/projects.js +138 -0
  20. package/lib/registry.js +83 -0
  21. package/lib/utils.js +129 -0
  22. package/lib/workstreams.js +652 -0
  23. package/package.json +80 -0
  24. package/scripts/capture-screenshots.js +142 -0
  25. package/scripts/postinstall.js +122 -0
  26. package/scripts/release.sh +71 -0
  27. package/scripts/sync-version.js +77 -0
  28. package/scripts/tauri-prepare.js +328 -0
  29. package/shared/mcp-registry.json +76 -0
  30. package/ui/dist/assets/index-DbZ3_HBD.js +3204 -0
  31. package/ui/dist/assets/index-DjLdm3Mr.css +32 -0
  32. package/ui/dist/icons/icon-192.svg +16 -0
  33. package/ui/dist/icons/icon-512.svg +16 -0
  34. package/ui/dist/index.html +39 -0
  35. package/ui/dist/manifest.json +25 -0
  36. package/ui/dist/sw.js +24 -0
  37. package/ui/dist/tutorial/claude-settings.png +0 -0
  38. package/ui/dist/tutorial/header.png +0 -0
  39. package/ui/dist/tutorial/mcp-registry.png +0 -0
  40. package/ui/dist/tutorial/memory-view.png +0 -0
  41. package/ui/dist/tutorial/permissions.png +0 -0
  42. package/ui/dist/tutorial/plugins-view.png +0 -0
  43. package/ui/dist/tutorial/project-explorer.png +0 -0
  44. package/ui/dist/tutorial/projects-view.png +0 -0
  45. package/ui/dist/tutorial/sidebar.png +0 -0
  46. package/ui/dist/tutorial/tutorial-view.png +0 -0
  47. package/ui/dist/tutorial/workstreams-view.png +0 -0
  48. package/ui/routes/activity.js +58 -0
  49. package/ui/routes/commands.js +74 -0
  50. package/ui/routes/configs.js +329 -0
  51. package/ui/routes/env.js +40 -0
  52. package/ui/routes/file-explorer.js +668 -0
  53. package/ui/routes/index.js +41 -0
  54. package/ui/routes/mcp-discovery.js +235 -0
  55. package/ui/routes/memory.js +385 -0
  56. package/ui/routes/package.json +3 -0
  57. package/ui/routes/plugins.js +466 -0
  58. package/ui/routes/projects.js +198 -0
  59. package/ui/routes/registry.js +30 -0
  60. package/ui/routes/rules.js +74 -0
  61. package/ui/routes/search.js +125 -0
  62. package/ui/routes/settings.js +381 -0
  63. package/ui/routes/subprojects.js +208 -0
  64. package/ui/routes/tool-sync.js +127 -0
  65. package/ui/routes/updates.js +339 -0
  66. package/ui/routes/workstreams.js +224 -0
  67. package/ui/server.cjs +773 -0
  68. package/ui/terminal-server.cjs +160 -0
package/cli.js ADDED
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude Config CLI
5
+ *
6
+ * Configuration management for Claude Code
7
+ * CLI-first with optional Web UI
8
+ */
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const { spawn, execSync } = require('child_process');
14
+
15
+ const args = process.argv.slice(2);
16
+ const command = args[0] || '';
17
+
18
+ // PID file for daemon mode
19
+ const PID_FILE = path.join(os.homedir(), '.claude-config', 'ui.pid');
20
+
21
+ // LaunchAgent for macOS auto-start
22
+ const LAUNCH_AGENT_LABEL = 'io.regression.claude-config';
23
+ const LAUNCH_AGENT_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCH_AGENT_LABEL}.plist`);
24
+
25
+ // UI command needs special handling (starts web server with better error handling)
26
+ if (command === 'ui' || command === 'web' || command === 'server') {
27
+ // Check for subcommand: ui stop, ui status, ui install, ui uninstall
28
+ const subcommand = args[1];
29
+ if (subcommand === 'stop') {
30
+ stopDaemon();
31
+ } else if (subcommand === 'status') {
32
+ checkDaemonStatus();
33
+ } else if (subcommand === 'install') {
34
+ installLaunchAgent();
35
+ } else if (subcommand === 'uninstall') {
36
+ uninstallLaunchAgent();
37
+ } else {
38
+ startUI();
39
+ }
40
+ } else {
41
+ // Pass everything to config-loader.js
42
+ const loaderPath = path.join(__dirname, 'config-loader.js');
43
+ const child = spawn('node', [loaderPath, ...args], {
44
+ stdio: 'inherit',
45
+ cwd: process.cwd()
46
+ });
47
+
48
+ child.on('close', (code) => {
49
+ process.exit(code || 0);
50
+ });
51
+ }
52
+
53
+ function stopDaemon() {
54
+ if (!fs.existsSync(PID_FILE)) {
55
+ console.log('No daemon running (PID file not found)');
56
+ return;
57
+ }
58
+
59
+ try {
60
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
61
+ process.kill(pid, 'SIGTERM');
62
+ fs.unlinkSync(PID_FILE);
63
+ console.log(`Stopped daemon (PID: ${pid})`);
64
+ } catch (err) {
65
+ if (err.code === 'ESRCH') {
66
+ // Process doesn't exist, clean up PID file
67
+ fs.unlinkSync(PID_FILE);
68
+ console.log('Daemon was not running, cleaned up stale PID file');
69
+ } else {
70
+ console.error('Failed to stop daemon:', err.message);
71
+ }
72
+ }
73
+ }
74
+
75
+ function checkDaemonStatus() {
76
+ // Check for LaunchAgent first (macOS)
77
+ if (process.platform === 'darwin' && fs.existsSync(LAUNCH_AGENT_PATH)) {
78
+ try {
79
+ const { spawnSync } = require('child_process');
80
+ const result = spawnSync('launchctl', ['list', LAUNCH_AGENT_LABEL], { encoding: 'utf8' });
81
+ if (result.status === 0 && result.stdout) {
82
+ // Parse the PID from output (format: "PID" = 12345;)
83
+ const pidMatch = result.stdout.match(/"PID"\s*=\s*(\d+)/);
84
+ if (pidMatch) {
85
+ console.log(`Daemon: running via LaunchAgent (PID: ${pidMatch[1]})`);
86
+ console.log(`UI available at: http://localhost:3333`);
87
+ console.log(`Auto-start: enabled`);
88
+ return;
89
+ }
90
+ }
91
+ } catch {}
92
+ }
93
+
94
+ // Check PID file (manual daemon mode)
95
+ if (fs.existsSync(PID_FILE)) {
96
+ try {
97
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
98
+ // Check if process is running
99
+ process.kill(pid, 0);
100
+ console.log(`Daemon: running (PID: ${pid})`);
101
+ console.log(`UI available at: http://localhost:3333`);
102
+ return;
103
+ } catch (err) {
104
+ fs.unlinkSync(PID_FILE);
105
+ }
106
+ }
107
+
108
+ // Check if LaunchAgent exists but not running
109
+ if (process.platform === 'darwin' && fs.existsSync(LAUNCH_AGENT_PATH)) {
110
+ console.log('Daemon: not running (LaunchAgent installed but stopped)');
111
+ console.log('Run: launchctl load ~/Library/LaunchAgents/io.regression.claude-config.plist');
112
+ } else {
113
+ console.log('Daemon: not running');
114
+ console.log('Run: claude-config ui');
115
+ }
116
+ }
117
+
118
+ function installLaunchAgent() {
119
+ if (process.platform !== 'darwin') {
120
+ console.error('Auto-start installation is only supported on macOS.');
121
+ console.error('On Linux, create a systemd user service instead.');
122
+ process.exit(1);
123
+ }
124
+
125
+ // Find the claude-config executable
126
+ let execPath;
127
+ try {
128
+ execPath = execSync('which claude-config', { encoding: 'utf8' }).trim();
129
+ } catch {
130
+ execPath = path.join(__dirname, 'cli.js');
131
+ }
132
+
133
+ // Get the PATH that includes node
134
+ let nodePath;
135
+ try {
136
+ nodePath = path.dirname(execSync('which node', { encoding: 'utf8' }).trim());
137
+ } catch {
138
+ nodePath = '/opt/homebrew/bin:/usr/local/bin';
139
+ }
140
+ const envPath = `${nodePath}:/usr/bin:/bin:/usr/sbin:/sbin`;
141
+
142
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
143
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
144
+ <plist version="1.0">
145
+ <dict>
146
+ <key>Label</key>
147
+ <string>${LAUNCH_AGENT_LABEL}</string>
148
+ <key>ProgramArguments</key>
149
+ <array>
150
+ <string>${execPath}</string>
151
+ <string>ui</string>
152
+ <string>--foreground</string>
153
+ </array>
154
+ <key>EnvironmentVariables</key>
155
+ <dict>
156
+ <key>PATH</key>
157
+ <string>${envPath}</string>
158
+ </dict>
159
+ <key>RunAtLoad</key>
160
+ <true/>
161
+ <key>KeepAlive</key>
162
+ <true/>
163
+ <key>StandardOutPath</key>
164
+ <string>${path.join(os.homedir(), '.claude-config', 'ui.log')}</string>
165
+ <key>StandardErrorPath</key>
166
+ <string>${path.join(os.homedir(), '.claude-config', 'ui.log')}</string>
167
+ <key>WorkingDirectory</key>
168
+ <string>${os.homedir()}</string>
169
+ </dict>
170
+ </plist>`;
171
+
172
+ // Ensure LaunchAgents directory exists
173
+ const launchAgentsDir = path.dirname(LAUNCH_AGENT_PATH);
174
+ if (!fs.existsSync(launchAgentsDir)) {
175
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
176
+ }
177
+
178
+ // Ensure log directory exists
179
+ const logDir = path.join(os.homedir(), '.claude-config');
180
+ if (!fs.existsSync(logDir)) {
181
+ fs.mkdirSync(logDir, { recursive: true });
182
+ }
183
+
184
+ // Stop existing daemon if running
185
+ if (fs.existsSync(PID_FILE)) {
186
+ try {
187
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
188
+ process.kill(pid, 'SIGTERM');
189
+ fs.unlinkSync(PID_FILE);
190
+ } catch {}
191
+ }
192
+
193
+ // Unload existing LaunchAgent if present (using spawn to avoid shell)
194
+ if (fs.existsSync(LAUNCH_AGENT_PATH)) {
195
+ try {
196
+ const { spawnSync } = require('child_process');
197
+ spawnSync('launchctl', ['unload', LAUNCH_AGENT_PATH], { stdio: 'ignore' });
198
+ } catch {}
199
+ }
200
+
201
+ // Write plist file
202
+ fs.writeFileSync(LAUNCH_AGENT_PATH, plistContent);
203
+
204
+ // Load the LaunchAgent (using spawn to avoid shell injection)
205
+ try {
206
+ const { spawnSync } = require('child_process');
207
+ const result = spawnSync('launchctl', ['load', LAUNCH_AGENT_PATH]);
208
+ if (result.status !== 0) {
209
+ throw new Error(result.stderr?.toString() || 'launchctl failed');
210
+ }
211
+ } catch (err) {
212
+ console.error('Failed to load LaunchAgent:', err.message);
213
+ process.exit(1);
214
+ }
215
+
216
+ console.log('✓ Installed auto-start for Claude Config UI');
217
+ console.log('');
218
+ console.log('The server will now:');
219
+ console.log(' • Start automatically on login');
220
+ console.log(' • Restart if it crashes');
221
+ console.log(' • Run at http://localhost:3333');
222
+ console.log('');
223
+ console.log('Your PWA can now connect anytime!');
224
+ console.log('');
225
+ console.log('Commands:');
226
+ console.log(' claude-config ui status - Check if running');
227
+ console.log(' claude-config ui uninstall - Remove auto-start');
228
+ }
229
+
230
+ function uninstallLaunchAgent() {
231
+ if (process.platform !== 'darwin') {
232
+ console.error('Auto-start removal is only supported on macOS.');
233
+ process.exit(1);
234
+ }
235
+
236
+ if (!fs.existsSync(LAUNCH_AGENT_PATH)) {
237
+ console.log('Auto-start is not installed.');
238
+ return;
239
+ }
240
+
241
+ // Unload the LaunchAgent (using spawn to avoid shell injection)
242
+ try {
243
+ const { spawnSync } = require('child_process');
244
+ spawnSync('launchctl', ['unload', LAUNCH_AGENT_PATH], { stdio: 'ignore' });
245
+ } catch {}
246
+
247
+ // Remove the plist file
248
+ fs.unlinkSync(LAUNCH_AGENT_PATH);
249
+
250
+ console.log('✓ Removed auto-start for Claude Config UI');
251
+ console.log('');
252
+ console.log('To start manually: claude-config ui');
253
+ }
254
+
255
+ function startDaemon(flags) {
256
+ // Check if already running
257
+ if (fs.existsSync(PID_FILE)) {
258
+ try {
259
+ const existingPid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
260
+ process.kill(existingPid, 0);
261
+ console.log(`Daemon already running (PID: ${existingPid})`);
262
+ console.log(`UI available at: http://localhost:${flags.port}`);
263
+ console.log('Use "claude-config ui stop" to stop the daemon');
264
+ return;
265
+ } catch (err) {
266
+ // Process not running, clean up stale PID file
267
+ fs.unlinkSync(PID_FILE);
268
+ }
269
+ }
270
+
271
+ // Ensure PID directory exists
272
+ const pidDir = path.dirname(PID_FILE);
273
+ if (!fs.existsSync(pidDir)) {
274
+ fs.mkdirSync(pidDir, { recursive: true });
275
+ }
276
+
277
+ // Log file for daemon output
278
+ const logFile = path.join(pidDir, 'ui.log');
279
+
280
+ // Build args for spawned process - must use --foreground since daemon is now default
281
+ const spawnArgs = ['ui', '--foreground'];
282
+ if (flags.port !== 3333) {
283
+ spawnArgs.push('--port', String(flags.port));
284
+ }
285
+ if (flags.dir) {
286
+ spawnArgs.push('--dir', flags.dir);
287
+ }
288
+
289
+ // Spawn detached process
290
+ const out = fs.openSync(logFile, 'a');
291
+ const err = fs.openSync(logFile, 'a');
292
+
293
+ const child = spawn(process.execPath, [__filename, ...spawnArgs], {
294
+ detached: true,
295
+ stdio: ['ignore', out, err],
296
+ cwd: os.homedir()
297
+ });
298
+
299
+ // Write PID file
300
+ fs.writeFileSync(PID_FILE, String(child.pid));
301
+
302
+ // Unref to allow parent to exit
303
+ child.unref();
304
+
305
+ console.log(`Started daemon (PID: ${child.pid})`);
306
+ console.log(`UI available at: http://localhost:${flags.port}`);
307
+ console.log(`Logs: ${logFile}`);
308
+ console.log('\nCommands:');
309
+ console.log(' claude-config ui status - Check daemon status');
310
+ console.log(' claude-config ui stop - Stop the daemon');
311
+ }
312
+
313
+ function startUI() {
314
+ // Parse UI-specific flags
315
+ const flags = {
316
+ port: 3333,
317
+ dir: null, // Will default to active project or home
318
+ foreground: false // Default to daemon mode
319
+ };
320
+
321
+ for (let i = 1; i < args.length; i++) {
322
+ const arg = args[i];
323
+ if (arg === '--port' || arg === '-p') {
324
+ const portArg = args[++i];
325
+ if (!portArg || isNaN(parseInt(portArg))) {
326
+ console.error('Error: --port requires a valid port number');
327
+ process.exit(1);
328
+ }
329
+ flags.port = parseInt(portArg);
330
+ } else if (arg.startsWith('--port=')) {
331
+ const portVal = parseInt(arg.split('=')[1]);
332
+ if (isNaN(portVal)) {
333
+ console.error('Error: --port requires a valid port number');
334
+ process.exit(1);
335
+ }
336
+ flags.port = portVal;
337
+ } else if (arg === '--dir' || arg === '-d') {
338
+ flags.dir = args[++i] || null;
339
+ } else if (arg.startsWith('--dir=')) {
340
+ flags.dir = arg.split('=')[1] || null;
341
+ } else if (arg === '--foreground' || arg === '-f' || arg === '--daemon' || arg === '-D') {
342
+ // --foreground runs in foreground, --daemon kept for backwards compat (now default)
343
+ flags.foreground = (arg === '--foreground' || arg === '-f');
344
+ } else if (!arg.startsWith('-') && fs.existsSync(arg) && fs.statSync(arg).isDirectory()) {
345
+ flags.dir = arg;
346
+ }
347
+ }
348
+
349
+ // Default: daemon mode (spawn detached and exit)
350
+ if (!flags.foreground) {
351
+ return startDaemon(flags);
352
+ }
353
+
354
+ // Validate port range
355
+ if (flags.port < 1 || flags.port > 65535) {
356
+ console.error('Error: Port must be between 1 and 65535');
357
+ process.exit(1);
358
+ }
359
+
360
+ // Validate directory exists (if specified)
361
+ if (flags.dir) {
362
+ if (!fs.existsSync(flags.dir)) {
363
+ console.error(`Error: Directory not found: ${flags.dir}`);
364
+ process.exit(1);
365
+ }
366
+
367
+ if (!fs.statSync(flags.dir).isDirectory()) {
368
+ console.error(`Error: Not a directory: ${flags.dir}`);
369
+ process.exit(1);
370
+ }
371
+
372
+ flags.dir = path.resolve(flags.dir);
373
+ }
374
+ // If no dir specified, server will load from projects registry or use cwd
375
+
376
+ // Load dependencies
377
+ const serverPath = path.join(__dirname, 'ui', 'server.cjs');
378
+
379
+ if (!fs.existsSync(serverPath)) {
380
+ console.error('Error: UI server not found.');
381
+ console.error('The package may not be installed correctly.');
382
+ process.exit(1);
383
+ }
384
+
385
+ const distPath = path.join(__dirname, 'ui', 'dist');
386
+ if (!fs.existsSync(distPath)) {
387
+ console.error('Error: UI build not found.');
388
+ console.error('Run "npm run build" to build the UI first.');
389
+ process.exit(1);
390
+ }
391
+
392
+ let ConfigUIServer, ClaudeConfigManager;
393
+ try {
394
+ ConfigUIServer = require(serverPath);
395
+ ClaudeConfigManager = require('./config-loader.js');
396
+ } catch (err) {
397
+ console.error('Error: Failed to load dependencies');
398
+ console.error(err.message);
399
+ if (err.message.includes('node-pty')) {
400
+ console.error('\nThe terminal feature requires node-pty which failed to load.');
401
+ console.error('Try reinstalling: npm rebuild node-pty');
402
+ }
403
+ process.exit(1);
404
+ }
405
+
406
+ // Handle server errors
407
+ process.on('uncaughtException', (err) => {
408
+ if (err.code === 'EADDRINUSE') {
409
+ console.error(`\nError: Port ${flags.port} is already in use.`);
410
+ console.error(`Try a different port: claude-config ui --port ${flags.port + 1}`);
411
+ process.exit(1);
412
+ } else if (err.code === 'EACCES') {
413
+ console.error(`\nError: Permission denied for port ${flags.port}.`);
414
+ console.error('Ports below 1024 require elevated privileges.');
415
+ process.exit(1);
416
+ } else {
417
+ console.error('\nUnexpected error:', err.message);
418
+ process.exit(1);
419
+ }
420
+ });
421
+
422
+ try {
423
+ const manager = new ClaudeConfigManager();
424
+ const server = new ConfigUIServer(flags.port, flags.dir, manager);
425
+ server.start();
426
+ } catch (err) {
427
+ console.error('Error: Failed to start server');
428
+ console.error(err.message);
429
+ process.exit(1);
430
+ }
431
+ }