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.
- package/bin/create-walle.js +171 -65
- package/package.json +1 -1
package/bin/create-walle.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const { execFileSync,
|
|
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 <
|
|
52
|
-
${BOLD}npx create-walle
|
|
53
|
-
${BOLD}npx create-walle
|
|
54
|
-
${BOLD}npx create-walle
|
|
55
|
-
${BOLD}npx create-walle
|
|
56
|
-
${BOLD}npx create-walle
|
|
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}
|
|
59
|
-
|
|
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}"
|
|
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
|
-
|
|
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=
|
|
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}
|
|
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
|
-
|
|
146
|
-
|
|
179
|
+
console.log(`${BOLD}${CYAN} Wall-E${RESET} — Updating to v${pkg.version}...\n`);
|
|
180
|
+
console.log(` ${DIM}Directory: ${dir}${RESET}`);
|
|
147
181
|
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
${
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
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(
|
|
398
|
+
fs.writeFileSync(INSTALL_PATH_FILE, dir + '\n');
|
|
295
399
|
}
|
|
296
400
|
|
|
297
401
|
function findWalleDir() {
|
|
298
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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 '
|
|
429
|
+
return '3456';
|
|
324
430
|
}
|
|
325
431
|
|
|
326
432
|
function detectName() {
|