create-walle 0.3.3 → 0.4.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 (2) hide show
  1. package/bin/create-walle.js +171 -65
  2. package/package.json +1 -1
@@ -1,19 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { execFileSync, execFile, spawn } = require('child_process');
4
+ const { execFileSync, spawn } = require('child_process');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
 
8
8
  const BOLD = '\x1b[1m';
9
9
  const DIM = '\x1b[2m';
10
10
  const GREEN = '\x1b[32m';
11
+ const YELLOW = '\x1b[33m';
11
12
  const RED = '\x1b[31m';
12
13
  const CYAN = '\x1b[36m';
13
14
  const RESET = '\x1b[0m';
14
15
 
15
16
  const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
16
17
  const LABEL = 'com.walle.server';
18
+ const INSTALL_PATH_FILE = path.join(process.env.HOME, '.walle', 'install-path');
19
+
20
+ // Files to preserve during update (user config, not code)
21
+ const PRESERVE_ON_UPDATE = ['.env', 'wall-e/wall-e-config.json'];
17
22
 
18
23
  // ── CLI Router ──
19
24
 
@@ -21,6 +26,8 @@ const command = process.argv[2] || '';
21
26
 
22
27
  if (command === 'install') {
23
28
  install(process.argv[3]);
29
+ } else if (command === 'update' || command === 'upgrade') {
30
+ update();
24
31
  } else if (command === 'start') {
25
32
  start();
26
33
  } else if (command === 'stop') {
@@ -33,8 +40,10 @@ if (command === 'install') {
33
40
  uninstall();
34
41
  } else if (command === '--help' || command === '-h' || command === 'help') {
35
42
  usage();
43
+ } else if (command === '--version' || command === '-v') {
44
+ const pkg = require('../package.json');
45
+ console.log(pkg.version);
36
46
  } else if (command && !command.startsWith('-')) {
37
- // Backwards compat: bare directory name = install
38
47
  install(command);
39
48
  } else {
40
49
  usage();
@@ -48,28 +57,46 @@ ${BOLD}${CYAN} Wall-E${RESET} — Your personal digital twin
48
57
 
49
58
  ${BOLD}Usage:${RESET}
50
59
 
51
- ${BOLD}npx create-walle install <directory>${RESET} Install Wall-E and start the service
52
- ${BOLD}npx create-walle start${RESET} Start Wall-E (auto-restarts on reboot)
53
- ${BOLD}npx create-walle stop${RESET} Stop Wall-E
54
- ${BOLD}npx create-walle status${RESET} Check if Wall-E is running
55
- ${BOLD}npx create-walle logs${RESET} Tail the service logs
56
- ${BOLD}npx create-walle uninstall${RESET} Remove the auto-start service
60
+ ${BOLD}npx create-walle install <dir>${RESET} Install (or detect existing install and update)
61
+ ${BOLD}npx create-walle update${RESET} Update code to latest, keep your config
62
+ ${BOLD}npx create-walle start${RESET} Start Wall-E (auto-restarts on reboot)
63
+ ${BOLD}npx create-walle stop${RESET} Stop Wall-E
64
+ ${BOLD}npx create-walle status${RESET} Check if Wall-E is running
65
+ ${BOLD}npx create-walle logs${RESET} Tail the service logs
66
+ ${BOLD}npx create-walle uninstall${RESET} Remove the auto-start service
57
67
 
58
- ${DIM}Run 'start' and 'stop' from inside your Wall-E directory,
59
- or set WALLE_DIR to point to it.${RESET}
68
+ ${DIM}Commands like start/stop/update auto-detect your install directory.
69
+ Or set WALLE_DIR to point to it.${RESET}
60
70
  `);
61
71
  }
62
72
 
63
73
  function install(targetDir) {
74
+ // If no target dir given, check for existing install
64
75
  if (!targetDir) {
76
+ const existing = findWalleDirSilent();
77
+ if (existing) {
78
+ console.log(`\n Found existing install at ${DIM}${existing}${RESET}`);
79
+ console.log(` Running update instead...\n`);
80
+ update();
81
+ return;
82
+ }
65
83
  console.log(`\n ${RED}Missing directory name.${RESET} Usage: npx create-walle install ./my-agent\n`);
66
84
  process.exit(1);
67
85
  }
68
86
 
87
+ // If target dir exists and has Wall-E, treat as update
88
+ if (fs.existsSync(targetDir) && fs.existsSync(path.join(targetDir, 'claude-task-manager', 'server.js'))) {
89
+ console.log(`\n ${CYAN}Wall-E already installed at ${targetDir}.${RESET} Updating...\n`);
90
+ saveWalleDir(path.resolve(targetDir));
91
+ update();
92
+ return;
93
+ }
94
+
95
+ // Fresh install
69
96
  console.log(`\n${BOLD}${CYAN} Wall-E${RESET} — Installing...\n`);
70
97
 
71
98
  if (fs.existsSync(targetDir)) {
72
- console.log(` ${RED}Directory "${targetDir}" already exists.${RESET}\n`);
99
+ console.log(` ${RED}Directory "${targetDir}" exists but is not a Wall-E install.${RESET}\n`);
73
100
  process.exit(1);
74
101
  }
75
102
  if (!fs.existsSync(TEMPLATE_DIR)) {
@@ -77,7 +104,6 @@ function install(targetDir) {
77
104
  process.exit(1);
78
105
  }
79
106
 
80
- // Auto-detect
81
107
  const ownerName = detectName().replace(/[\r\n=]/g, '').trim().slice(0, 200);
82
108
  const timezone = detectTimezone();
83
109
  const nameParts = ownerName.split(/\s+/);
@@ -88,19 +114,11 @@ function install(targetDir) {
88
114
  console.log(` ${DIM}Timezone: ${timezone}${RESET}`);
89
115
  console.log(` ${DIM}Port: ${port}${RESET}`);
90
116
 
91
- // Copy template
92
117
  console.log(`\n Copying files...`);
93
118
  copyRecursive(TEMPLATE_DIR, targetDir);
94
119
 
95
- // Install deps
96
120
  console.log(` Installing dependencies...\n`);
97
- try {
98
- execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(targetDir, 'claude-task-manager'), stdio: 'inherit' });
99
- execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(targetDir, 'wall-e'), stdio: 'inherit' });
100
- } catch {
101
- console.error(`\n ${RED}npm install failed.${RESET} Try manually:\n cd ${targetDir}/claude-task-manager && npm install\n cd ${targetDir}/wall-e && npm install\n`);
102
- process.exit(1);
103
- }
121
+ npmInstall(targetDir);
104
122
 
105
123
  // .env
106
124
  const envLines = [
@@ -108,7 +126,7 @@ function install(targetDir) {
108
126
  `# Finish setup at http://localhost:${port}/setup.html`,
109
127
  '',
110
128
  `WALLE_OWNER_NAME=${ownerName}`,
111
- '# ANTHROPIC_API_KEY=sk-ant-...',
129
+ '# ANTHROPIC_API_KEY=',
112
130
  '',
113
131
  `CTM_PORT=${port}`,
114
132
  `WALL_E_PORT=${wallePort}`,
@@ -130,57 +148,95 @@ function install(targetDir) {
130
148
  { mode: 0o600 }
131
149
  );
132
150
 
133
- // Data dir
134
151
  fs.mkdirSync(path.join(process.env.HOME, '.walle', 'data'), { recursive: true });
135
-
136
- // Git init
137
152
  try { execFileSync('git', ['init', '-q'], { cwd: targetDir, stdio: 'ignore' }); } catch {}
138
153
 
139
- // Save install path for start/stop
140
154
  saveWalleDir(path.resolve(targetDir));
141
155
 
156
+ // Start the service
157
+ console.log(` Starting Wall-E...`);
158
+ startForegroundOrService(path.resolve(targetDir), port);
159
+
142
160
  console.log(`
143
- ${GREEN}${BOLD}Done!${RESET}
161
+ ${GREEN}${BOLD}Wall-E is running!${RESET}
162
+
163
+ ${BOLD}Open:${RESET} http://localhost:${port}
164
+
165
+ ${BOLD}Commands:${RESET}
166
+ npx create-walle update ${DIM}Update to latest version${RESET}
167
+ npx create-walle stop ${DIM}Stop the service${RESET}
168
+ npx create-walle start ${DIM}Restart${RESET}
169
+ npx create-walle status ${DIM}Check status${RESET}
170
+ npx create-walle logs ${DIM}View logs${RESET}
171
+ `);
172
+ }
173
+
174
+ function update() {
175
+ const dir = findWalleDir();
176
+ const port = readPort(dir);
177
+ const pkg = require('../package.json');
144
178
 
145
- ${BOLD}Start Wall-E:${RESET}
146
- cd ${targetDir} && node claude-task-manager/server.js
179
+ console.log(`${BOLD}${CYAN} Wall-E${RESET} — Updating to v${pkg.version}...\n`);
180
+ console.log(` ${DIM}Directory: ${dir}${RESET}`);
147
181
 
148
- ${BOLD}Then open:${RESET} http://localhost:${port}
182
+ if (!fs.existsSync(TEMPLATE_DIR)) {
183
+ console.error(' Template not found. Try: npm cache clean --force && npx create-walle@latest update');
184
+ process.exit(1);
185
+ }
186
+
187
+ // 1. Stop if running
188
+ console.log(` Stopping Wall-E...`);
189
+ stopQuiet(dir, port);
190
+
191
+ // 2. Backup preserved files
192
+ const backups = {};
193
+ for (const relPath of PRESERVE_ON_UPDATE) {
194
+ const fullPath = path.join(dir, relPath);
195
+ if (fs.existsSync(fullPath)) {
196
+ backups[relPath] = fs.readFileSync(fullPath);
197
+ }
198
+ }
199
+
200
+ // 3. Copy new code (overwrite everything except node_modules, .git, data)
201
+ console.log(` Updating code...`);
202
+ copyRecursive(TEMPLATE_DIR, dir);
203
+
204
+ // 4. Restore preserved files
205
+ for (const [relPath, content] of Object.entries(backups)) {
206
+ const fullPath = path.join(dir, relPath);
207
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
208
+ fs.writeFileSync(fullPath, content);
209
+ }
210
+
211
+ // 5. Reinstall deps (in case package.json changed)
212
+ console.log(` Installing dependencies...\n`);
213
+ npmInstall(dir);
149
214
 
150
- ${BOLD}Auto-start on login (optional):${RESET}
151
- npx create-walle start
215
+ // 6. Start again
216
+ console.log(`\n Starting Wall-E...`);
217
+ startForegroundOrService(dir, port);
218
+
219
+ console.log(`
220
+ ${GREEN}${BOLD}Updated to v${pkg.version}!${RESET} http://localhost:${port}
152
221
 
153
- ${BOLD}Other commands:${RESET}
154
- npx create-walle stop ${DIM}Stop the service${RESET}
155
- npx create-walle status ${DIM}Check status${RESET}
156
- npx create-walle logs ${DIM}View logs${RESET}
222
+ ${DIM}Your .env and config were preserved.${RESET}
157
223
  `);
158
224
  }
159
225
 
160
226
  function start() {
161
227
  const dir = findWalleDir();
162
228
  const port = readPort(dir);
163
-
164
229
  console.log(`\n Starting Wall-E from ${DIM}${dir}${RESET} on port ${port}...`);
165
230
  installService(dir, port);
166
231
  console.log(` ${GREEN}Running!${RESET} http://localhost:${port}\n`);
167
232
  }
168
233
 
169
234
  function stop() {
170
- const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
171
- if (process.platform === 'darwin' && fs.existsSync(plist)) {
172
- try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
173
- console.log(`\n ${GREEN}Wall-E stopped.${RESET}`);
174
- console.log(` Run ${BOLD}npx create-walle start${RESET} to restart.\n`);
175
- } else {
176
- // Fallback: try killing the process
177
- try {
178
- const dir = findWalleDir();
179
- const port = readPort(dir);
180
- execFileSync('curl', ['-sX', 'POST', `http://localhost:${port}/api/stop/walle`], { timeout: 3000 });
181
- } catch {}
182
- console.log(`\n Stopped (or was not running).\n`);
183
- }
235
+ const dir = findWalleDirSilent();
236
+ const port = dir ? readPort(dir) : '3456';
237
+ stopQuiet(dir, port);
238
+ console.log(`\n ${GREEN}Wall-E stopped.${RESET}`);
239
+ console.log(` Run ${BOLD}npx create-walle start${RESET} to restart.\n`);
184
240
  }
185
241
 
186
242
  function status() {
@@ -213,12 +269,52 @@ function uninstall() {
213
269
  const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
214
270
  try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
215
271
  try { fs.unlinkSync(plist); } catch {}
216
- try { fs.unlinkSync(path.join(process.env.HOME, '.walle', 'install-path')); } catch {}
272
+ try { fs.unlinkSync(INSTALL_PATH_FILE); } catch {}
217
273
  console.log(`\n ${GREEN}Service uninstalled.${RESET} Your data in ~/.walle/data is preserved.\n`);
218
274
  }
219
275
 
220
276
  // ── Service Management ──
221
277
 
278
+ function stopQuiet(dir, port) {
279
+ // Try launchctl first
280
+ const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
281
+ if (process.platform === 'darwin' && fs.existsSync(plist)) {
282
+ try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
283
+ }
284
+ // Also try API
285
+ try { execFileSync('curl', ['-sX', 'POST', `http://localhost:${port}/api/restart/ctm`], { timeout: 2000 }); } catch {}
286
+ // Wait for port to free up
287
+ for (let i = 0; i < 10; i++) {
288
+ try {
289
+ execFileSync('curl', ['-s', '--max-time', '1', `http://localhost:${port}/api/services/status`], { timeout: 2000 });
290
+ // Still running, wait
291
+ execFileSync('sleep', ['0.5']);
292
+ } catch {
293
+ break; // Port is free
294
+ }
295
+ }
296
+ }
297
+
298
+ function startForegroundOrService(dir, port) {
299
+ // If launchd plist exists, reload it. Otherwise start in background.
300
+ const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
301
+ if (process.platform === 'darwin' && fs.existsSync(plist)) {
302
+ // Update plist with latest paths, then reload
303
+ installService(dir, port);
304
+ } else {
305
+ // Start in background without launchd
306
+ const logDir = path.join(process.env.HOME, '.walle', 'logs');
307
+ fs.mkdirSync(logDir, { recursive: true });
308
+ const child = spawn('node', ['claude-task-manager/server.js'], {
309
+ cwd: dir,
310
+ detached: true,
311
+ stdio: ['ignore', fs.openSync(path.join(logDir, 'walle.log'), 'a'), fs.openSync(path.join(logDir, 'walle.err'), 'a')],
312
+ env: { ...process.env, CTM_PORT: port, WALL_E_PORT: String(parseInt(port) + 1) },
313
+ });
314
+ child.unref();
315
+ }
316
+ }
317
+
222
318
  function installService(walleDir, port) {
223
319
  const logDir = path.join(process.env.HOME, '.walle', 'logs');
224
320
  fs.mkdirSync(logDir, { recursive: true });
@@ -229,7 +325,6 @@ function installService(walleDir, port) {
229
325
  fs.mkdirSync(plistDir, { recursive: true });
230
326
  const plistPath = path.join(plistDir, `${LABEL}.plist`);
231
327
 
232
- // Build env vars from .env file
233
328
  let envDict = ` <key>PATH</key>\n <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>\n`;
234
329
  envDict += ` <key>HOME</key>\n <string>${process.env.HOME}</string>\n`;
235
330
  envDict += ` <key>CTM_PORT</key>\n <string>${port}</string>\n`;
@@ -274,7 +369,6 @@ ${envDict} </dict>
274
369
  try { execFileSync('launchctl', ['unload', plistPath], { stdio: 'ignore' }); } catch {}
275
370
  execFileSync('launchctl', ['load', plistPath]);
276
371
  } else {
277
- // Linux/other: just start directly in background
278
372
  const child = spawn('node', ['claude-task-manager/server.js'], {
279
373
  cwd: walleDir,
280
374
  detached: true,
@@ -288,30 +382,42 @@ ${envDict} </dict>
288
382
 
289
383
  // ── Helpers ──
290
384
 
385
+ function npmInstall(dir) {
386
+ try {
387
+ execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(dir, 'claude-task-manager'), stdio: 'inherit' });
388
+ execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(dir, 'wall-e'), stdio: 'inherit' });
389
+ } catch {
390
+ console.error(`\n ${RED}npm install failed.${RESET} Try manually:\n cd ${dir}/claude-task-manager && npm install\n cd ${dir}/wall-e && npm install\n`);
391
+ process.exit(1);
392
+ }
393
+ }
394
+
291
395
  function saveWalleDir(dir) {
292
396
  const metaDir = path.join(process.env.HOME, '.walle');
293
397
  fs.mkdirSync(metaDir, { recursive: true });
294
- fs.writeFileSync(path.join(metaDir, 'install-path'), dir + '\n');
398
+ fs.writeFileSync(INSTALL_PATH_FILE, dir + '\n');
295
399
  }
296
400
 
297
401
  function findWalleDir() {
298
- // 1. WALLE_DIR env
402
+ const dir = findWalleDirSilent();
403
+ if (dir) return dir;
404
+ console.error(`\n ${RED}Can't find Wall-E installation.${RESET}`);
405
+ console.error(` Run ${BOLD}npx create-walle install ./my-agent${RESET} first.\n`);
406
+ process.exit(1);
407
+ }
408
+
409
+ function findWalleDirSilent() {
299
410
  if (process.env.WALLE_DIR && fs.existsSync(path.join(process.env.WALLE_DIR, 'claude-task-manager'))) {
300
411
  return process.env.WALLE_DIR;
301
412
  }
302
- // 2. Current directory
303
413
  if (fs.existsSync(path.join(process.cwd(), 'claude-task-manager', 'server.js'))) {
304
414
  return process.cwd();
305
415
  }
306
- // 3. Saved install path
307
- const saved = path.join(process.env.HOME, '.walle', 'install-path');
308
- if (fs.existsSync(saved)) {
309
- const dir = fs.readFileSync(saved, 'utf8').trim();
416
+ if (fs.existsSync(INSTALL_PATH_FILE)) {
417
+ const dir = fs.readFileSync(INSTALL_PATH_FILE, 'utf8').trim();
310
418
  if (fs.existsSync(path.join(dir, 'claude-task-manager', 'server.js'))) return dir;
311
419
  }
312
- console.error(`\n ${RED}Can't find Wall-E installation.${RESET}`);
313
- console.error(` Run from inside the Wall-E directory, or set WALLE_DIR.\n`);
314
- process.exit(1);
420
+ return null;
315
421
  }
316
422
 
317
423
  function readPort(dir) {
@@ -320,7 +426,7 @@ function readPort(dir) {
320
426
  const m = env.match(/^CTM_PORT=(\d+)/m);
321
427
  if (m) return m[1];
322
428
  } catch {}
323
- return '4567';
429
+ return '3456';
324
430
  }
325
431
 
326
432
  function detectName() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Set up Wall-E — your personal digital twin",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"