@regression-io/claude-config 0.37.21 → 0.37.25

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.
package/cli.js CHANGED
@@ -18,14 +18,22 @@ const command = args[0] || '';
18
18
  // PID file for daemon mode
19
19
  const PID_FILE = path.join(os.homedir(), '.claude-config', 'ui.pid');
20
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
+
21
25
  // UI command needs special handling (starts web server with better error handling)
22
26
  if (command === 'ui' || command === 'web' || command === 'server') {
23
- // Check for subcommand: ui stop, ui status
27
+ // Check for subcommand: ui stop, ui status, ui install, ui uninstall
24
28
  const subcommand = args[1];
25
29
  if (subcommand === 'stop') {
26
30
  stopDaemon();
27
31
  } else if (subcommand === 'status') {
28
32
  checkDaemonStatus();
33
+ } else if (subcommand === 'install') {
34
+ installLaunchAgent();
35
+ } else if (subcommand === 'uninstall') {
36
+ uninstallLaunchAgent();
29
37
  } else {
30
38
  startUI();
31
39
  }
@@ -65,21 +73,183 @@ function stopDaemon() {
65
73
  }
66
74
 
67
75
  function checkDaemonStatus() {
68
- if (!fs.existsSync(PID_FILE)) {
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 {
69
113
  console.log('Daemon: not running');
70
- return;
114
+ console.log('Run: claude-config ui');
71
115
  }
116
+ }
72
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;
73
127
  try {
74
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());
75
- // Check if process is running
76
- process.kill(pid, 0);
77
- console.log(`Daemon: running (PID: ${pid})`);
78
- console.log(`UI available at: http://localhost:3333`);
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
+ }
79
211
  } catch (err) {
80
- console.log('Daemon: not running (stale PID file)');
81
- fs.unlinkSync(PID_FILE);
212
+ console.error('Failed to load LaunchAgent:', err.message);
213
+ process.exit(1);
82
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');
83
253
  }
84
254
 
85
255
  function startDaemon(flags) {
package/lib/cli.js CHANGED
@@ -232,7 +232,11 @@ Maintenance:
232
232
  update Check npm for updates and install if available
233
233
  update --check Check for updates without installing
234
234
  update /path/to/source Update from local development source
235
- ui [--port=3333] Open web UI
235
+ ui [--port=3333] Start web UI (daemon mode by default)
236
+ ui install Auto-start on login (macOS LaunchAgent)
237
+ ui uninstall Remove auto-start
238
+ ui status Check if UI daemon is running
239
+ ui stop Stop the UI daemon
236
240
  version Show version info
237
241
 
238
242
  Plugins (use Claude Code CLI):
package/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.37.21';
5
+ const VERSION = '0.37.25';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@regression-io/claude-config",
3
- "version": "0.37.21",
3
+ "version": "0.37.25",
4
4
  "description": "Configuration management UI for Claude Code and Antigravity - manage MCPs, rules, commands, memory, and project folders",
5
5
  "author": "regression.io",
6
6
  "main": "config-loader.js",
@@ -42,7 +42,7 @@ function getVersionFromFile(filePath) {
42
42
  function fetchNpmVersion() {
43
43
  return new Promise((resolve) => {
44
44
  const url = 'https://registry.npmjs.org/@regression-io/claude-config/latest';
45
- https.get(url, (res) => {
45
+ const req = https.get(url, (res) => {
46
46
  let data = '';
47
47
  res.on('data', chunk => data += chunk);
48
48
  res.on('end', () => {
@@ -52,6 +52,7 @@ function fetchNpmVersion() {
52
52
  const tarballUrl = parsed.dist?.tarball;
53
53
 
54
54
  if (!version || !tarballUrl) {
55
+ console.log('[update-check] npm registry response missing version or tarball');
55
56
  resolve(null);
56
57
  return;
57
58
  }
@@ -61,8 +62,7 @@ function fetchNpmVersion() {
61
62
  const options = {
62
63
  hostname: tarball.hostname,
63
64
  path: tarball.pathname,
64
- method: 'HEAD',
65
- timeout: 5000
65
+ method: 'HEAD'
66
66
  };
67
67
 
68
68
  const verifyReq = https.request(options, (verifyRes) => {
@@ -70,20 +70,35 @@ function fetchNpmVersion() {
70
70
  if (verifyRes.statusCode === 200) {
71
71
  resolve(version);
72
72
  } else {
73
+ console.log(`[update-check] tarball not accessible (status ${verifyRes.statusCode})`);
73
74
  resolve(null);
74
75
  }
75
76
  });
76
- verifyReq.on('error', () => resolve(null));
77
- verifyReq.on('timeout', () => {
77
+ verifyReq.setTimeout(5000, () => {
78
+ console.log('[update-check] tarball verification timed out');
78
79
  verifyReq.destroy();
79
80
  resolve(null);
80
81
  });
82
+ verifyReq.on('error', (e) => {
83
+ console.log('[update-check] tarball verification error:', e.message);
84
+ resolve(null);
85
+ });
81
86
  verifyReq.end();
82
87
  } catch (e) {
88
+ console.log('[update-check] failed to parse npm response:', e.message);
83
89
  resolve(null);
84
90
  }
85
91
  });
86
- }).on('error', () => resolve(null));
92
+ });
93
+ req.setTimeout(10000, () => {
94
+ console.log('[update-check] npm registry request timed out');
95
+ req.destroy();
96
+ resolve(null);
97
+ });
98
+ req.on('error', (e) => {
99
+ console.log('[update-check] npm registry error:', e.message);
100
+ resolve(null);
101
+ });
87
102
  });
88
103
  }
89
104
 
@@ -118,7 +133,10 @@ async function checkForUpdates(manager, dirname) {
118
133
  // Check npm for latest version
119
134
  const npmVersion = await fetchNpmVersion();
120
135
 
136
+ console.log(`[update-check] installed: ${installedVersion}, npm: ${npmVersion}`);
137
+
121
138
  if (npmVersion && isNewerVersion(npmVersion, installedVersion)) {
139
+ console.log('[update-check] update available');
122
140
  return {
123
141
  updateAvailable: true,
124
142
  installedVersion,
package/ui/server.cjs CHANGED
@@ -611,8 +611,13 @@ class ConfigUIServer {
611
611
  case '/api/projects/active':
612
612
  if (req.method === 'GET') return this.json(res, routes.projects.getActiveProject(this.manager, this.projectDir, () => this.getHierarchy(), () => routes.subprojects.getSubprojectsForDir(this.manager, this.config, this.projectDir)));
613
613
  if (req.method === 'PUT') {
614
- const result = routes.projects.setActiveProject(this.manager, body.id, () => this.getHierarchy(), () => routes.subprojects.getSubprojectsForDir(this.manager, this.config, this.projectDir));
615
- if (result.success) this.projectDir = result.project.path;
614
+ const result = routes.projects.setActiveProject(
615
+ this.manager,
616
+ body.id,
617
+ (newDir) => { this.projectDir = newDir; },
618
+ () => this.getHierarchy(),
619
+ () => routes.subprojects.getSubprojectsForDir(this.manager, this.config, this.projectDir)
620
+ );
616
621
  return this.json(res, result);
617
622
  }
618
623
  break;