cortexterm 0.1.0 → 0.1.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/README.md +13 -0
- package/bin/cortexterm.mjs +152 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,3 +12,16 @@ runs on its own: you can close the terminal (its start output goes to `~/.cortex
|
|
|
12
12
|
running the command again while it runs only says so.
|
|
13
13
|
|
|
14
14
|
Arguments go to the app: `npx cortexterm --headless`, `npx cortexterm --safe`.
|
|
15
|
+
|
|
16
|
+
## A Mac nobody sits at: the runner
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx cortexterm runner
|
|
20
|
+
npx cortexterm runner join <invitation>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`runner` keeps CortexTerm running on this Mac without a window, now and at every login (a LaunchAgent,
|
|
24
|
+
`~/Library/LaunchAgents/com.cortexterm.runner.plist`; its output goes to `~/.cortexterm/logs/runner.log`).
|
|
25
|
+
`runner join` joins it to your network with an invitation made in Settings > Mesh on another computer.
|
|
26
|
+
The other computers then list it under COMPUTERS and open shells on it. `npx cortexterm runner off`
|
|
27
|
+
stops it. On Linux, start it with `npx cortexterm --headless` for now.
|
package/bin/cortexterm.mjs
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// later runs start the installed app at once, without looking at the catalog: the app updates its
|
|
5
5
|
// frame and plugins itself, so a newer app release is only for machines that have none. The app runs
|
|
6
6
|
// on its own: closing the terminal does not quit it (its start output goes to ~/.cortexterm/logs/start.log).
|
|
7
|
-
// Arguments go to the app (`--headless`, `--safe`).
|
|
7
|
+
// Arguments go to the app (`--headless`, `--safe`). `npx cortexterm runner` keeps it running without a
|
|
8
|
+
// window at every login (a LaunchAgent), for a Mac nobody sits at; `runner join <invitation>` joins
|
|
9
|
+
// that Mac to the network, `runner off` stops it.
|
|
8
10
|
|
|
9
11
|
import { spawn } from 'node:child_process';
|
|
10
12
|
import { createHash } from 'node:crypto';
|
|
@@ -14,6 +16,7 @@ import https from 'node:https';
|
|
|
14
16
|
import { homedir } from 'node:os';
|
|
15
17
|
import { join } from 'node:path';
|
|
16
18
|
import { pipeline } from 'node:stream/promises';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
17
20
|
|
|
18
21
|
const REPO = process.env.CORTEXTERM_REPO || 'agentbusiness/CortexTerm';
|
|
19
22
|
// CORTEXTERM_CATALOG: another catalog (a mirror, or a release under test).
|
|
@@ -152,17 +155,8 @@ function prune(keep) {
|
|
|
152
155
|
.forEach((name) => rmSync(join(APPS, name), { recursive: true, force: true }));
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
158
|
-
process.stdout.write('npx cortexterm [--headless] [--safe]\n\nStarts CortexTerm; the first run downloads it into ~/.cortexterm/app.\n');
|
|
159
|
-
return 0;
|
|
160
|
-
}
|
|
161
|
-
const pid = running();
|
|
162
|
-
if (pid) {
|
|
163
|
-
say(`CortexTerm is already running (pid ${pid})`);
|
|
164
|
-
return 0;
|
|
165
|
-
}
|
|
158
|
+
/** The installed app to start (downloaded first on a machine that has none), or null. */
|
|
159
|
+
async function ensureApp() {
|
|
166
160
|
const key = platform();
|
|
167
161
|
// The installed app updates itself (frame and plugins): only a machine without one downloads.
|
|
168
162
|
if (installed().length === 0) {
|
|
@@ -178,9 +172,154 @@ async function main() {
|
|
|
178
172
|
const chosen = installed()[0];
|
|
179
173
|
if (!chosen) {
|
|
180
174
|
say('CortexTerm is not installed and could not be downloaded');
|
|
181
|
-
return
|
|
175
|
+
return null;
|
|
182
176
|
}
|
|
183
177
|
prune(chosen);
|
|
178
|
+
return chosen;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- the runner: CortexTerm with no window, kept running on a Mac nobody sits at -------------------
|
|
182
|
+
|
|
183
|
+
const LABEL = 'com.cortexterm.runner';
|
|
184
|
+
const AGENT = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
185
|
+
const RUNNER = join(HOME, 'runner', 'cortexterm.mjs');
|
|
186
|
+
|
|
187
|
+
const xml = (text) => String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
188
|
+
|
|
189
|
+
/** A LaunchAgent that starts this launcher (a copy kept in the home: npx's own is temporary) in
|
|
190
|
+
* the foreground with `--headless` at every login, and again when the app ends with an error. */
|
|
191
|
+
function agentPlist() {
|
|
192
|
+
const env = process.env.CORTEX_HOME ? `<key>EnvironmentVariables</key><dict><key>CORTEX_HOME</key><string>${xml(HOME)}</string></dict>` : '';
|
|
193
|
+
const log = xml(join(HOME, 'logs', 'runner.log'));
|
|
194
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
195
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
196
|
+
<plist version="1.0"><dict>
|
|
197
|
+
<key>Label</key><string>${LABEL}</string>
|
|
198
|
+
<key>ProgramArguments</key><array><string>${xml(process.execPath)}</string><string>${xml(RUNNER)}</string><string>--headless</string><string>--foreground</string></array>
|
|
199
|
+
<key>RunAtLoad</key><true/>
|
|
200
|
+
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
201
|
+
<key>StandardOutPath</key><string>${log}</string>
|
|
202
|
+
<key>StandardErrorPath</key><string>${log}</string>
|
|
203
|
+
${env}
|
|
204
|
+
</dict></plist>
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function launchctl(args) {
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
const child = spawn('/bin/launchctl', args, { stdio: 'ignore' });
|
|
211
|
+
child.on('error', () => resolve(false));
|
|
212
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The frame's API token: in the Keychain on macOS, in `run/frame.json` elsewhere. */
|
|
217
|
+
function frameToken(info) {
|
|
218
|
+
if (info.token) return Promise.resolve(info.token);
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
let out = '';
|
|
221
|
+
const child = spawn('/usr/bin/security', ['find-generic-password', '-s', 'CortexTerm frame token', '-a', HOME, '-w'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
222
|
+
child.stdout.on('data', (chunk) => { out += chunk; });
|
|
223
|
+
child.on('error', reject);
|
|
224
|
+
child.on('exit', (code) => (code === 0 ? resolve(out.trim()) : reject(new Error('the frame token is not in the Keychain'))));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** One call on the running frame (`POST /api/rpc`), as scripts make it. */
|
|
229
|
+
async function frameCall(method, params) {
|
|
230
|
+
let info;
|
|
231
|
+
try {
|
|
232
|
+
info = JSON.parse(readFileSync(join(HOME, 'run', 'frame.json'), 'utf8'));
|
|
233
|
+
process.kill(info.pid, 0);
|
|
234
|
+
} catch {
|
|
235
|
+
throw new Error('CortexTerm is not running here: start it first (npx cortexterm runner)');
|
|
236
|
+
}
|
|
237
|
+
const token = await frameToken(info);
|
|
238
|
+
const response = await fetch(`http://127.0.0.1:${info.port}/api/rpc?token=${encodeURIComponent(token)}`, {
|
|
239
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ method, params }),
|
|
240
|
+
});
|
|
241
|
+
const answer = await response.json();
|
|
242
|
+
if (!answer.ok) throw new Error(answer.error?.message ?? JSON.stringify(answer.error));
|
|
243
|
+
return answer.result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function runner(args) {
|
|
247
|
+
const [what, ...rest] = args;
|
|
248
|
+
if (what === 'join') {
|
|
249
|
+
const code = rest.join(' ').trim();
|
|
250
|
+
if (!code) {
|
|
251
|
+
say('npx cortexterm runner join <invitation>: the invitation from Settings > Mesh on a computer of your network');
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
const member = await frameCall('mesh.pair.join', { code });
|
|
255
|
+
say(`joined the network as ${member.name}`);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
if (process.platform !== 'darwin') {
|
|
259
|
+
say('the runner is for Macs for now; elsewhere start CortexTerm with `npx cortexterm --headless`');
|
|
260
|
+
return 1;
|
|
261
|
+
}
|
|
262
|
+
const domain = `gui/${process.getuid()}`;
|
|
263
|
+
if (what === 'off') {
|
|
264
|
+
await launchctl(['bootout', `${domain}/${LABEL}`]);
|
|
265
|
+
rmSync(AGENT, { force: true });
|
|
266
|
+
say('the runner is off: CortexTerm no longer starts at login, and the runner\'s CortexTerm has stopped');
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
if (what) {
|
|
270
|
+
say(`unknown runner command \`${what}\` (runner, runner join <invitation>, runner off)`);
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
if (!(await ensureApp())) return 1;
|
|
274
|
+
mkdirSync(join(HOME, 'runner'), { recursive: true });
|
|
275
|
+
mkdirSync(join(HOME, 'logs'), { recursive: true });
|
|
276
|
+
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
277
|
+
writeFileSync(RUNNER, readFileSync(fileURLToPath(import.meta.url)));
|
|
278
|
+
writeFileSync(AGENT, agentPlist());
|
|
279
|
+
await launchctl(['bootout', `${domain}/${LABEL}`]);
|
|
280
|
+
if (!(await launchctl(['bootstrap', domain, AGENT]))) {
|
|
281
|
+
say(`cannot start the runner (launchctl bootstrap ${domain} ${AGENT} failed)`);
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
const pid = running();
|
|
285
|
+
say(pid
|
|
286
|
+
? `CortexTerm is already running here (pid ${pid}); the runner keeps it running without a window from the next login`
|
|
287
|
+
: 'the runner is on: CortexTerm runs here without a window, now and at every login');
|
|
288
|
+
say('to join your network: npx cortexterm runner join <invitation>');
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function main() {
|
|
293
|
+
const args = process.argv.slice(2);
|
|
294
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
295
|
+
process.stdout.write([
|
|
296
|
+
'npx cortexterm [--headless] [--safe] start CortexTerm; the first run downloads it into ~/.cortexterm/app',
|
|
297
|
+
'npx cortexterm runner keep CortexTerm running here without a window, at every login (macOS)',
|
|
298
|
+
'npx cortexterm runner join <invitation> join this computer to your network (the invitation from Settings > Mesh)',
|
|
299
|
+
'npx cortexterm runner off stop the runner',
|
|
300
|
+
'',
|
|
301
|
+
].join('\n'));
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
if (args[0] === 'runner') return runner(args.slice(1));
|
|
305
|
+
const pid = running();
|
|
306
|
+
if (pid) {
|
|
307
|
+
say(`CortexTerm is already running (pid ${pid})`);
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
const chosen = await ensureApp();
|
|
311
|
+
if (!chosen) return 1;
|
|
312
|
+
// The runner's LaunchAgent: the app in the foreground, so launchd sees it end.
|
|
313
|
+
if (args.includes('--foreground')) {
|
|
314
|
+
const child = spawn(executable(join(APPS, chosen)), args.filter((arg) => arg !== '--foreground'), { stdio: 'inherit' });
|
|
315
|
+
return await new Promise((resolve) => {
|
|
316
|
+
child.on('error', (error) => {
|
|
317
|
+
say(`cannot start CortexTerm: ${error.message}`);
|
|
318
|
+
resolve(1);
|
|
319
|
+
});
|
|
320
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
321
|
+
});
|
|
322
|
+
}
|
|
184
323
|
// Its own session, detached from this terminal: closing the terminal does not quit it.
|
|
185
324
|
mkdirSync(join(HOME, 'logs'), { recursive: true });
|
|
186
325
|
const log = openSync(START_LOG, 'a');
|
package/package.json
CHANGED