create-walle 0.3.3 → 0.4.0

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 +166 -61
  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,13 +148,9 @@ 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
 
142
156
  console.log(`
@@ -147,40 +161,81 @@ function install(targetDir) {
147
161
 
148
162
  ${BOLD}Then open:${RESET} http://localhost:${port}
149
163
 
150
- ${BOLD}Auto-start on login (optional):${RESET}
151
- npx create-walle start
152
-
153
164
  ${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}
165
+ npx create-walle update ${DIM}Update to latest version${RESET}
166
+ npx create-walle start ${DIM}Start as background service${RESET}
167
+ npx create-walle stop ${DIM}Stop the service${RESET}
168
+ npx create-walle status ${DIM}Check status${RESET}
169
+ npx create-walle logs ${DIM}View logs${RESET}
157
170
  `);
158
171
  }
159
172
 
160
- function start() {
173
+ function update() {
161
174
  const dir = findWalleDir();
162
175
  const port = readPort(dir);
176
+ const pkg = require('../package.json');
177
+
178
+ console.log(`${BOLD}${CYAN} Wall-E${RESET} — Updating to v${pkg.version}...\n`);
179
+ console.log(` ${DIM}Directory: ${dir}${RESET}`);
163
180
 
181
+ if (!fs.existsSync(TEMPLATE_DIR)) {
182
+ console.error(' Template not found. Try: npm cache clean --force && npx create-walle@latest update');
183
+ process.exit(1);
184
+ }
185
+
186
+ // 1. Stop if running
187
+ console.log(` Stopping Wall-E...`);
188
+ stopQuiet(dir, port);
189
+
190
+ // 2. Backup preserved files
191
+ const backups = {};
192
+ for (const relPath of PRESERVE_ON_UPDATE) {
193
+ const fullPath = path.join(dir, relPath);
194
+ if (fs.existsSync(fullPath)) {
195
+ backups[relPath] = fs.readFileSync(fullPath);
196
+ }
197
+ }
198
+
199
+ // 3. Copy new code (overwrite everything except node_modules, .git, data)
200
+ console.log(` Updating code...`);
201
+ copyRecursive(TEMPLATE_DIR, dir);
202
+
203
+ // 4. Restore preserved files
204
+ for (const [relPath, content] of Object.entries(backups)) {
205
+ const fullPath = path.join(dir, relPath);
206
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
207
+ fs.writeFileSync(fullPath, content);
208
+ }
209
+
210
+ // 5. Reinstall deps (in case package.json changed)
211
+ console.log(` Installing dependencies...\n`);
212
+ npmInstall(dir);
213
+
214
+ // 6. Start again
215
+ console.log(`\n Starting Wall-E...`);
216
+ startForegroundOrService(dir, port);
217
+
218
+ console.log(`
219
+ ${GREEN}${BOLD}Updated to v${pkg.version}!${RESET} http://localhost:${port}
220
+
221
+ ${DIM}Your .env and config were preserved.${RESET}
222
+ `);
223
+ }
224
+
225
+ function start() {
226
+ const dir = findWalleDir();
227
+ const port = readPort(dir);
164
228
  console.log(`\n Starting Wall-E from ${DIM}${dir}${RESET} on port ${port}...`);
165
229
  installService(dir, port);
166
230
  console.log(` ${GREEN}Running!${RESET} http://localhost:${port}\n`);
167
231
  }
168
232
 
169
233
  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
- }
234
+ const dir = findWalleDirSilent();
235
+ const port = dir ? readPort(dir) : '3456';
236
+ stopQuiet(dir, port);
237
+ console.log(`\n ${GREEN}Wall-E stopped.${RESET}`);
238
+ console.log(` Run ${BOLD}npx create-walle start${RESET} to restart.\n`);
184
239
  }
185
240
 
186
241
  function status() {
@@ -213,12 +268,52 @@ function uninstall() {
213
268
  const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
214
269
  try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
215
270
  try { fs.unlinkSync(plist); } catch {}
216
- try { fs.unlinkSync(path.join(process.env.HOME, '.walle', 'install-path')); } catch {}
271
+ try { fs.unlinkSync(INSTALL_PATH_FILE); } catch {}
217
272
  console.log(`\n ${GREEN}Service uninstalled.${RESET} Your data in ~/.walle/data is preserved.\n`);
218
273
  }
219
274
 
220
275
  // ── Service Management ──
221
276
 
277
+ function stopQuiet(dir, port) {
278
+ // Try launchctl first
279
+ const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
280
+ if (process.platform === 'darwin' && fs.existsSync(plist)) {
281
+ try { execFileSync('launchctl', ['unload', plist], { stdio: 'ignore' }); } catch {}
282
+ }
283
+ // Also try API
284
+ try { execFileSync('curl', ['-sX', 'POST', `http://localhost:${port}/api/restart/ctm`], { timeout: 2000 }); } catch {}
285
+ // Wait for port to free up
286
+ for (let i = 0; i < 10; i++) {
287
+ try {
288
+ execFileSync('curl', ['-s', '--max-time', '1', `http://localhost:${port}/api/services/status`], { timeout: 2000 });
289
+ // Still running, wait
290
+ execFileSync('sleep', ['0.5']);
291
+ } catch {
292
+ break; // Port is free
293
+ }
294
+ }
295
+ }
296
+
297
+ function startForegroundOrService(dir, port) {
298
+ // If launchd plist exists, reload it. Otherwise start in background.
299
+ const plist = path.join(process.env.HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`);
300
+ if (process.platform === 'darwin' && fs.existsSync(plist)) {
301
+ // Update plist with latest paths, then reload
302
+ installService(dir, port);
303
+ } else {
304
+ // Start in background without launchd
305
+ const logDir = path.join(process.env.HOME, '.walle', 'logs');
306
+ fs.mkdirSync(logDir, { recursive: true });
307
+ const child = spawn('node', ['claude-task-manager/server.js'], {
308
+ cwd: dir,
309
+ detached: true,
310
+ stdio: ['ignore', fs.openSync(path.join(logDir, 'walle.log'), 'a'), fs.openSync(path.join(logDir, 'walle.err'), 'a')],
311
+ env: { ...process.env, CTM_PORT: port, WALL_E_PORT: String(parseInt(port) + 1) },
312
+ });
313
+ child.unref();
314
+ }
315
+ }
316
+
222
317
  function installService(walleDir, port) {
223
318
  const logDir = path.join(process.env.HOME, '.walle', 'logs');
224
319
  fs.mkdirSync(logDir, { recursive: true });
@@ -229,7 +324,6 @@ function installService(walleDir, port) {
229
324
  fs.mkdirSync(plistDir, { recursive: true });
230
325
  const plistPath = path.join(plistDir, `${LABEL}.plist`);
231
326
 
232
- // Build env vars from .env file
233
327
  let envDict = ` <key>PATH</key>\n <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>\n`;
234
328
  envDict += ` <key>HOME</key>\n <string>${process.env.HOME}</string>\n`;
235
329
  envDict += ` <key>CTM_PORT</key>\n <string>${port}</string>\n`;
@@ -274,7 +368,6 @@ ${envDict} </dict>
274
368
  try { execFileSync('launchctl', ['unload', plistPath], { stdio: 'ignore' }); } catch {}
275
369
  execFileSync('launchctl', ['load', plistPath]);
276
370
  } else {
277
- // Linux/other: just start directly in background
278
371
  const child = spawn('node', ['claude-task-manager/server.js'], {
279
372
  cwd: walleDir,
280
373
  detached: true,
@@ -288,30 +381,42 @@ ${envDict} </dict>
288
381
 
289
382
  // ── Helpers ──
290
383
 
384
+ function npmInstall(dir) {
385
+ try {
386
+ execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(dir, 'claude-task-manager'), stdio: 'inherit' });
387
+ execFileSync('npm', ['install', '--loglevel=warn'], { cwd: path.join(dir, 'wall-e'), stdio: 'inherit' });
388
+ } catch {
389
+ 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`);
390
+ process.exit(1);
391
+ }
392
+ }
393
+
291
394
  function saveWalleDir(dir) {
292
395
  const metaDir = path.join(process.env.HOME, '.walle');
293
396
  fs.mkdirSync(metaDir, { recursive: true });
294
- fs.writeFileSync(path.join(metaDir, 'install-path'), dir + '\n');
397
+ fs.writeFileSync(INSTALL_PATH_FILE, dir + '\n');
295
398
  }
296
399
 
297
400
  function findWalleDir() {
298
- // 1. WALLE_DIR env
401
+ const dir = findWalleDirSilent();
402
+ if (dir) return dir;
403
+ console.error(`\n ${RED}Can't find Wall-E installation.${RESET}`);
404
+ console.error(` Run ${BOLD}npx create-walle install ./my-agent${RESET} first.\n`);
405
+ process.exit(1);
406
+ }
407
+
408
+ function findWalleDirSilent() {
299
409
  if (process.env.WALLE_DIR && fs.existsSync(path.join(process.env.WALLE_DIR, 'claude-task-manager'))) {
300
410
  return process.env.WALLE_DIR;
301
411
  }
302
- // 2. Current directory
303
412
  if (fs.existsSync(path.join(process.cwd(), 'claude-task-manager', 'server.js'))) {
304
413
  return process.cwd();
305
414
  }
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();
415
+ if (fs.existsSync(INSTALL_PATH_FILE)) {
416
+ const dir = fs.readFileSync(INSTALL_PATH_FILE, 'utf8').trim();
310
417
  if (fs.existsSync(path.join(dir, 'claude-task-manager', 'server.js'))) return dir;
311
418
  }
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);
419
+ return null;
315
420
  }
316
421
 
317
422
  function readPort(dir) {
@@ -320,7 +425,7 @@ function readPort(dir) {
320
425
  const m = env.match(/^CTM_PORT=(\d+)/m);
321
426
  if (m) return m[1];
322
427
  } catch {}
323
- return '4567';
428
+ return '3456';
324
429
  }
325
430
 
326
431
  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.0",
4
4
  "description": "Set up Wall-E — your personal digital twin",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"