cli-setu 0.1.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/src/pair.js ADDED
@@ -0,0 +1,83 @@
1
+ // The one-time pairing flow, exactly as proven in supervisor-qr.js's
2
+ // ensurePaired() - same real HTTP calls, same QR + typed-code dual path
3
+ // (either wins, whichever the phone actually uses). What changes here:
4
+ // the device name comes from config.js's auto-detected hostname instead
5
+ // of a hand-typed argv, and the result is saved to ~/.cli-setu/config.json
6
+ // instead of a per-device-name file in the install folder.
7
+ const qrcode = require('qrcode-terminal');
8
+ const { loadOrCreateConfig, writeConfig } = require('./config');
9
+
10
+ async function pair(authServer, authServerPublic) {
11
+ const config = loadOrCreateConfig();
12
+
13
+ if (config.deviceToken) {
14
+ console.log(`[setu] "${config.deviceName}" is already paired.`);
15
+ return config;
16
+ }
17
+
18
+ console.log(`[setu] Pairing as "${config.deviceName}"...`);
19
+ console.log('[setu] (This is your computer\'s name. Rename it in ~/.cli-setu/config.json before pairing if you want something else.)');
20
+
21
+ // qr-start also needs a sessionId up front (mirrors supervisor-qr.js's
22
+ // "the QR both pairs it AND jumps the phone straight to watching THIS
23
+ // session" behavior) - 'pairing' is a placeholder id here since `setu
24
+ // pair` itself never streams a terminal; the real per-run session id is
25
+ // generated fresh by supervisor.js on every `setu` (bare) invocation.
26
+ const startRes = await fetch(`${authServer}/device/pair/qr-start`, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ deviceName: config.deviceName, sessionId: 'pairing' }),
30
+ });
31
+ const { qrToken } = await startRes.json();
32
+
33
+ const scanUrl = `${authServerPublic}/scan.html?token=${qrToken}&session=pairing&device=${encodeURIComponent(config.deviceName)}`;
34
+
35
+ console.log('');
36
+ qrcode.generate(scanUrl, { small: true });
37
+ console.log('[setu] Scan this with your phone camera.');
38
+ console.log(`[setu] (Testing on one PC? Paste this URL into a browser tab instead: ${scanUrl})`);
39
+
40
+ const codeRes = await fetch(`${authServer}/device/pair/start`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ deviceName: config.deviceName }),
44
+ });
45
+ const { code: typedCode, pairId: typedPairId } = await codeRes.json();
46
+ console.log(`[setu] Or type this code in the app's Devices tab: ${typedCode}`);
47
+ console.log('[setu] Waiting for scan or code entry...');
48
+
49
+ let confirmed = false;
50
+ while (!confirmed) {
51
+ await new Promise((r) => setTimeout(r, 2000));
52
+ const statusRes = await fetch(`${authServer}/device/pair/qr-status?token=${qrToken}`);
53
+ const status = await statusRes.json();
54
+
55
+ if (status.status === 'CONFIRMED') {
56
+ confirmed = true;
57
+ config.deviceToken = status.deviceToken;
58
+ writeConfig(config);
59
+ console.log(`[setu] ✓ Paired via QR and saved to ~/.cli-setu/config.json`);
60
+ break;
61
+ } else if (status.status === 'EXPIRED') {
62
+ console.error('[setu] QR code expired. Run `setu pair` again to get a new one.');
63
+ process.exit(1);
64
+ } else if (status.status === 'REJECTED') {
65
+ console.error(`[setu] Pairing rejected: ${status.rejectReason || 'the server declined this pairing.'}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ const typedStatusRes = await fetch(`${authServer}/device/pair/status?pairId=${typedPairId}`);
70
+ const typedStatus = await typedStatusRes.json();
71
+ if (typedStatus.status === 'CONFIRMED') {
72
+ confirmed = true;
73
+ config.deviceToken = typedStatus.deviceToken;
74
+ writeConfig(config);
75
+ console.log(`[setu] ✓ Paired via typed code and saved to ~/.cli-setu/config.json`);
76
+ break;
77
+ }
78
+ }
79
+
80
+ return config;
81
+ }
82
+
83
+ module.exports = { pair };
package/src/status.js ADDED
@@ -0,0 +1,65 @@
1
+ // `setu status` - reports whether this computer is paired, and which
2
+ // sessions (if any) currently have a `setu` process running, using the
3
+ // PID files supervisor.js writes/removes around its own lifetime (see
4
+ // its own comment on why - there was no way to answer this question at
5
+ // all before those existed).
6
+ const fs = require('fs');
7
+ const { readConfig, CONFIG_DIR } = require('./config');
8
+
9
+ function isProcessAlive(pid) {
10
+ try {
11
+ // Signal 0 sends nothing - it only checks whether the PID exists and
12
+ // is one this user can signal. Throws if it doesn't (already exited,
13
+ // e.g. the process crashed without reaching its own cleanup code).
14
+ process.kill(pid, 0);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ async function status(authServer) {
22
+ const config = readConfig();
23
+ if (!config || !config.deviceToken) {
24
+ console.log('[setu] Not paired. Run `setu pair` to get started.');
25
+ return;
26
+ }
27
+ console.log(`[setu] Paired as "${config.deviceName}".`);
28
+
29
+ let files;
30
+ try {
31
+ files = fs.readdirSync(CONFIG_DIR).filter((f) => f.startsWith('session.') && f.endsWith('.pid'));
32
+ } catch {
33
+ files = [];
34
+ }
35
+
36
+ const running = [];
37
+ for (const file of files) {
38
+ let info;
39
+ try {
40
+ info = JSON.parse(fs.readFileSync(`${CONFIG_DIR}/${file}`, 'utf8'));
41
+ } catch {
42
+ continue;
43
+ }
44
+ if (isProcessAlive(info.pid)) {
45
+ running.push(info);
46
+ } else {
47
+ // Stale PID file from a process that crashed/was killed without
48
+ // reaching its own SIGINT/exit cleanup - clean it up now rather
49
+ // than reporting a dead session as running forever.
50
+ try { fs.unlinkSync(`${CONFIG_DIR}/${file}`); } catch {}
51
+ }
52
+ }
53
+
54
+ if (running.length === 0) {
55
+ console.log('[setu] No sessions currently running. Run `setu` in a project folder to start one.');
56
+ return;
57
+ }
58
+
59
+ console.log(`[setu] ${running.length} session(s) running:`);
60
+ for (const s of running) {
61
+ console.log(` - ${s.sessionId} · ${s.cwd} · started ${s.startedAt} (pid ${s.pid})`);
62
+ }
63
+ }
64
+
65
+ module.exports = { status };
package/src/stop.js ADDED
@@ -0,0 +1,51 @@
1
+ // `setu stop` - cleanly disconnects every session currently running on
2
+ // this computer. Real gap in the PoC this replaces: the only way to stop
3
+ // a supervisor was closing its terminal window (or Ctrl+C, which only
4
+ // reaches the one you're actually looking at) - this reaches every
5
+ // running session at once via the same PID files status.js reads.
6
+ const fs = require('fs');
7
+ const { CONFIG_DIR } = require('./config');
8
+
9
+ async function stop() {
10
+ let files;
11
+ try {
12
+ files = fs.readdirSync(CONFIG_DIR).filter((f) => f.startsWith('session.') && f.endsWith('.pid'));
13
+ } catch {
14
+ files = [];
15
+ }
16
+
17
+ if (files.length === 0) {
18
+ console.log('[setu] No sessions currently running.');
19
+ return;
20
+ }
21
+
22
+ let stopped = 0;
23
+ for (const file of files) {
24
+ const pidFile = `${CONFIG_DIR}/${file}`;
25
+ let info;
26
+ try {
27
+ info = JSON.parse(fs.readFileSync(pidFile, 'utf8'));
28
+ } catch {
29
+ continue;
30
+ }
31
+ try {
32
+ // SIGINT (not SIGKILL) - the same signal Ctrl+C sends, so the
33
+ // process's own handler (supervisor.js's process.on('SIGINT', ...))
34
+ // runs: kills the PTY cleanly, closes the WebSocket, removes its own
35
+ // PID file. A hard kill would skip all of that and leave the CLI
36
+ // process and the Azure connection to time out on their own.
37
+ process.kill(info.pid, 'SIGINT');
38
+ console.log(`[setu] Stopped ${info.sessionId} (${info.cwd})`);
39
+ stopped++;
40
+ } catch {
41
+ // Already dead - just clean up the stale file.
42
+ try { fs.unlinkSync(pidFile); } catch {}
43
+ }
44
+ }
45
+
46
+ if (stopped === 0) {
47
+ console.log('[setu] Sessions were already stopped (stale state cleaned up).');
48
+ }
49
+ }
50
+
51
+ module.exports = { stop };