project-cypher 0.11.0 → 0.12.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.
- package/bin/cypher.js +61 -4
- package/package.json +1 -1
package/bin/cypher.js
CHANGED
|
@@ -52,6 +52,52 @@ const httpOf = s => s.replace(/\/$/, ''), wsOf = s => httpOf(s).replace(/^http/,
|
|
|
52
52
|
|
|
53
53
|
const ask = q => new Promise(r => { const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout }); rl.question(q, a => { rl.close(); r(a.trim()); }); });
|
|
54
54
|
const devName = () => rawCfg().name || os.hostname();
|
|
55
|
+
|
|
56
|
+
// ── Background service (auto-start at boot/login, restarts on crash, waits for internet) ──
|
|
57
|
+
const LOG = path.join(CFG_DIR, 'agent.log'), SELF = [process.execPath, path.resolve(__filename), 'connect'];
|
|
58
|
+
const sh = (cmd, args) => require('child_process').spawnSync(cmd, args, { encoding: 'utf8' });
|
|
59
|
+
const SVC = {
|
|
60
|
+
kind: process.platform === 'darwin' ? 'launchd' : process.platform === 'win32' ? 'task' : 'systemd',
|
|
61
|
+
unit: path.join(os.homedir(), '.config/systemd/user/cypher.service'),
|
|
62
|
+
plist: path.join(os.homedir(), 'Library/LaunchAgents/com.cypher.agent.plist'),
|
|
63
|
+
install() {
|
|
64
|
+
fs.mkdirSync(CFG_DIR, { recursive: true });
|
|
65
|
+
if (this.kind === 'systemd') {
|
|
66
|
+
fs.mkdirSync(path.dirname(this.unit), { recursive: true });
|
|
67
|
+
fs.writeFileSync(this.unit, `[Unit]\nDescription=Project Cypher agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nExecStart=${SELF.map(x => `"${x}"`).join(' ')}\nRestart=on-failure\nRestartSec=5\nStandardOutput=append:${LOG}\nStandardError=append:${LOG}\n\n[Install]\nWantedBy=default.target\n`);
|
|
68
|
+
sh('systemctl', ['--user', 'daemon-reload']); sh('loginctl', ['enable-linger', os.userInfo().username]); // linger = start at boot even before login
|
|
69
|
+
return sh('systemctl', ['--user', 'enable', 'cypher.service']).status === 0;
|
|
70
|
+
}
|
|
71
|
+
if (this.kind === 'launchd') {
|
|
72
|
+
fs.mkdirSync(path.dirname(this.plist), { recursive: true });
|
|
73
|
+
fs.writeFileSync(this.plist, `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>com.cypher.agent</string><key>ProgramArguments</key><array>${SELF.map(x => `<string>${x}</string>`).join('')}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict><key>StandardOutPath</key><string>${LOG}</string><key>StandardErrorPath</key><string>${LOG}</string></dict></plist>\n`);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return sh('schtasks', ['/create', '/f', '/tn', 'ProjectCypher', '/sc', 'onlogon', '/rl', 'limited', '/tr', `cmd /c ""${SELF[0]}" "${SELF[1]}" connect >> "${LOG}" 2>&1"`]).status === 0;
|
|
77
|
+
},
|
|
78
|
+
start() {
|
|
79
|
+
if (!this.install()) return false;
|
|
80
|
+
if (this.kind === 'systemd') return sh('systemctl', ['--user', 'restart', 'cypher.service']).status === 0;
|
|
81
|
+
if (this.kind === 'launchd') { sh('launchctl', ['unload', this.plist]); return sh('launchctl', ['load', '-w', this.plist]).status === 0; }
|
|
82
|
+
return sh('schtasks', ['/run', '/tn', 'ProjectCypher']).status === 0;
|
|
83
|
+
},
|
|
84
|
+
stop() {
|
|
85
|
+
if (this.kind === 'systemd') { sh('systemctl', ['--user', 'stop', 'cypher.service']); return sh('systemctl', ['--user', 'disable', 'cypher.service']).status === 0; }
|
|
86
|
+
if (this.kind === 'launchd') return sh('launchctl', ['unload', '-w', this.plist]).status === 0;
|
|
87
|
+
sh('schtasks', ['/end', '/tn', 'ProjectCypher']); return sh('schtasks', ['/change', '/tn', 'ProjectCypher', '/disable']).status === 0;
|
|
88
|
+
},
|
|
89
|
+
remove() { this.stop(); try { fs.rmSync(this.kind === 'systemd' ? this.unit : this.plist, { force: true }); } catch {} if (this.kind === 'task') sh('schtasks', ['/delete', '/f', '/tn', 'ProjectCypher']); },
|
|
90
|
+
running() {
|
|
91
|
+
if (this.kind === 'systemd') return sh('systemctl', ['--user', 'is-active', 'cypher.service']).stdout.trim() === 'active';
|
|
92
|
+
if (this.kind === 'launchd') return sh('launchctl', ['list', 'com.cypher.agent']).status === 0;
|
|
93
|
+
return /Running/i.test(sh('schtasks', ['/query', '/tn', 'ProjectCypher']).stdout || '');
|
|
94
|
+
},
|
|
95
|
+
enabled() {
|
|
96
|
+
if (this.kind === 'systemd') return sh('systemctl', ['--user', 'is-enabled', 'cypher.service']).stdout.trim() === 'enabled';
|
|
97
|
+
if (this.kind === 'launchd') return fs.existsSync(this.plist);
|
|
98
|
+
return sh('schtasks', ['/query', '/tn', 'ProjectCypher']).status === 0;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
55
101
|
const yes = (a, d) => (a === '' ? d : /^y/i.test(a));
|
|
56
102
|
const has = (cmd, args = ['--version']) => { const r = require('child_process').spawnSync(cmd, args, { encoding: 'utf8' }); return r.error ? null : (r.stdout || r.stderr || '').trim().split('\n')[0]; };
|
|
57
103
|
async function doLink(server) {
|
|
@@ -76,7 +122,7 @@ const cmds = {
|
|
|
76
122
|
async disconnect() {
|
|
77
123
|
const c = load(); if (!c.token) return console.log('already disconnected.');
|
|
78
124
|
await fetch(httpOf(c.server) + '/unlink', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token: c.token }) }).catch(() => null);
|
|
79
|
-
dropToken('disconnected. This computer is no longer linked to your app. Run cypher init to link again.');
|
|
125
|
+
SVC.remove(); dropToken('disconnected. This computer is no longer linked to your app. Run cypher init to link again.');
|
|
80
126
|
},
|
|
81
127
|
// First-run setup: checks Claude Code, reaches the server, shows the QR, adds this folder, goes online.
|
|
82
128
|
async init(...argv) {
|
|
@@ -109,8 +155,8 @@ const cmds = {
|
|
|
109
155
|
ws = ensureRoot(ws);
|
|
110
156
|
console.log(`\nRoot is ready. New projects will be created in ${ws}`);
|
|
111
157
|
console.log('Open Project Cypher and chat with Root: add existing folders, create projects, ask for status.');
|
|
112
|
-
console.log('\
|
|
113
|
-
cmds.connect();
|
|
158
|
+
if (SVC.start()) console.log('\nCypher now runs in the background and starts automatically whenever this computer is on and online.\nManage it with: cypher stats | cypher stop | cypher start');
|
|
159
|
+
else { console.log('\nCould not install the background service. Running in this window (Ctrl+C to stop). Next time: cypher connect'); cmds.connect(); }
|
|
114
160
|
},
|
|
115
161
|
// One command to link this computer to the phone: shows a QR + code, waits for the app, adds the current folder, goes online.
|
|
116
162
|
async link(server) { await doLink(server); cmds.add('.'); cmds.connect(); },
|
|
@@ -150,6 +196,15 @@ const cmds = {
|
|
|
150
196
|
},
|
|
151
197
|
remove(id) { writeRegistry(readRegistry().filter(x => x.id !== id)); console.log('removed ' + id); },
|
|
152
198
|
projects() { const c = load(); if (!c.projects.length) return console.log('none yet. cypher add <folder>'); c.projects.forEach(p => console.log(`${p.id.padEnd(20)} ${p.path}`)); },
|
|
199
|
+
// Service control
|
|
200
|
+
start() { const c = load(); if (!c.token) return console.log('Not linked yet. Run: cypher init'); console.log(SVC.start() ? 'Cypher is running in the background and will start automatically when this computer turns on.' : 'Could not start the background service. Run it in this window instead: cypher connect'); },
|
|
201
|
+
stop() { console.log(SVC.stop() ? 'Cypher stopped and auto-start turned off. Run cypher start to bring it back.' : 'Nothing to stop (or the service is not installed).'); },
|
|
202
|
+
async stats() {
|
|
203
|
+
const c = load(); const r = await fetch(httpOf(c.server) + '/health').then(r => r.json()).catch(() => null);
|
|
204
|
+
console.log(`service: ${SVC.running() ? 'running' : 'stopped'}${SVC.enabled() ? ' (starts automatically)' : ' (auto-start off)'}\ninternet/server: ${r?.ok ? 'reachable' : 'unreachable'} ${c.server}\nlinked: ${c.token ? 'yes, as "' + devName() + '"' : 'no (run cypher init)'}\nprojects: ${c.projects.length}\nlog: ${LOG}`);
|
|
205
|
+
try { const t = fs.readFileSync(LOG, 'utf8').trim().split('\n').slice(-3).join('\n'); if (t) console.log('\nrecent:\n' + t); } catch {}
|
|
206
|
+
},
|
|
207
|
+
logs() { try { console.log(fs.readFileSync(LOG, 'utf8').split('\n').slice(-40).join('\n')); } catch { console.log('no log yet'); } },
|
|
153
208
|
async status() { const c = load(); const r = await fetch(httpOf(c.server) + '/health').then(r => r.json()).catch(() => null); console.log(`server ${c.server}: ${r?.ok ? 'online' : 'unreachable'}\ntoken: ${c.token ? 'set' : 'missing (cypher login)'}\nprojects: ${c.projects.length}`); },
|
|
154
209
|
async latest() { const c = load(); const l = await (await fetch(httpOf(c.server) + '/latest')).json(); console.log(`${l.versionName} (code ${l.versionCode})\n${l.apkUrl}`); },
|
|
155
210
|
connect() {
|
|
@@ -213,7 +268,9 @@ const help = () => console.log(`Project Cypher
|
|
|
213
268
|
cypher add [folder] register a project folder (or just ask Root in the app)
|
|
214
269
|
cypher projects | remove <id> list / remove projects
|
|
215
270
|
cypher disconnect unlink this computer from the app
|
|
216
|
-
cypher connect go online
|
|
271
|
+
cypher connect go online in this window (no background service)
|
|
272
|
+
cypher start | stop start / stop the background service (auto-starts at boot)
|
|
273
|
+
cypher stats | logs service + connection status / recent log
|
|
217
274
|
cypher status | latest server check / latest app build
|
|
218
275
|
|
|
219
276
|
Config: ~/.cypher/config.json (permissionMode defaults to acceptEdits)`);
|