cli-setu 0.1.0 → 0.2.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 +12 -12
- package/bin/setu.js +31 -19
- package/package.json +2 -2
- package/src/cli-adapters/index.js +2 -2
- package/src/cli-adapters/shell.js +4 -4
- package/src/config.js +2 -2
- package/src/hooks.js +5 -5
- package/src/pair.js +44 -15
- package/src/status.js +24 -6
- package/src/stop.js +4 -4
- package/src/supervisor.js +35 -19
- package/src/unpair.js +65 -0
- package/src/update.js +10 -10
package/README.md
CHANGED
|
@@ -11,29 +11,29 @@ npm install -g cli-setu
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
setu # bridges the current folder as a plain shell session
|
|
15
|
-
setu --cli claude # same, but runs Claude Code inside the bridged session
|
|
16
|
-
setu --cli codex # or Codex
|
|
14
|
+
cli-setu # bridges the current folder as a plain shell session
|
|
15
|
+
cli-setu --cli claude # same, but runs Claude Code inside the bridged session
|
|
16
|
+
cli-setu --cli codex # or Codex
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The first time you run it, `setu` prints a QR code (and an 8-digit fallback code) - scan it with the CLIसेतु app to pair this computer and start watching the session live.
|
|
19
|
+
The first time you run it, `cli-setu` prints a QR code (and an 8-digit fallback code) - scan it with the CLIसेतु app to pair this computer and start watching the session live.
|
|
20
20
|
|
|
21
|
-
Once paired, running `setu` in any folder just works - no QR code needed again, unless this is a device you haven't paired before.
|
|
21
|
+
Once paired, running `cli-setu` in any folder just works - no QR code needed again, unless this is a device you haven't paired before.
|
|
22
22
|
|
|
23
23
|
## Other commands
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
setu pair # pair this computer, without starting a session
|
|
27
|
-
setu status # show pairing status and any sessions currently running
|
|
28
|
-
setu stop # stop a running session
|
|
29
|
-
setu --update # check for and install the latest version
|
|
30
|
-
setu --version # print the installed version
|
|
31
|
-
setu --help # show all commands
|
|
26
|
+
cli-setu pair # pair this computer, without starting a session
|
|
27
|
+
cli-setu status # show pairing status and any sessions currently running
|
|
28
|
+
cli-setu stop # stop a running session
|
|
29
|
+
cli-setu --update # check for and install the latest version
|
|
30
|
+
cli-setu --version # print the installed version
|
|
31
|
+
cli-setu --help # show all commands
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## How it works
|
|
35
35
|
|
|
36
|
-
`setu` spawns your real terminal (or Claude Code/Codex) inside a PTY, same as any terminal emulator. Every byte it produces streams live to the CLIसेतु app over a secure relay; anything you type on the phone writes back into that same PTY, exactly like a local keystroke. Permission prompts (file edits, shell commands, etc.) are detected and relayed as structured approve/reject cards instead of raw text, when the adapter for your CLI supports it.
|
|
36
|
+
`cli-setu` spawns your real terminal (or Claude Code/Codex) inside a PTY, same as any terminal emulator. Every byte it produces streams live to the CLIसेतु app over a secure relay; anything you type on the phone writes back into that same PTY, exactly like a local keystroke. Permission prompts (file edits, shell commands, etc.) are detected and relayed as structured approve/reject cards instead of raw text, when the adapter for your CLI supports it.
|
|
37
37
|
|
|
38
38
|
## License
|
|
39
39
|
|
package/bin/setu.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Command dispatcher for the `setu` CLI. Kept deliberately thin - each
|
|
2
|
+
// Command dispatcher for the `cli-setu` CLI. Kept deliberately thin - each
|
|
3
3
|
// real subcommand's logic lives in src/, this file only parses argv and
|
|
4
4
|
// calls the right one.
|
|
5
|
-
|
|
5
|
+
//
|
|
6
|
+
// TEMPORARY: defaults to the DEV server (dev-api.clisetu.com), not
|
|
7
|
+
// production, because only dev-api is live right now. MUST switch back to
|
|
8
|
+
// https://api.clisetu.com and republish before announcing this package to
|
|
9
|
+
// real users as "production ready" - see clisetuapp/backend's deployment
|
|
10
|
+
// guide for when production actually goes live.
|
|
11
|
+
const AUTH_SERVER = process.env.CLI_SETU_SERVER || 'https://dev-api.clisetu.com';
|
|
6
12
|
const AUTH_SERVER_PUBLIC = process.env.CLI_SETU_SERVER_PUBLIC || AUTH_SERVER;
|
|
7
13
|
|
|
8
14
|
// Real bug found by testing: this used to switch on process.argv[2]
|
|
9
15
|
// directly against the subcommand list (pair/status/stop), with any
|
|
10
|
-
// other value (including a real --flag, e.g. `setu --cli claude`)
|
|
16
|
+
// other value (including a real --flag, e.g. `cli-setu --cli claude`)
|
|
11
17
|
// falling through to "Unknown command" and exiting immediately - the
|
|
12
18
|
// whole multi-CLI feature (claude/codex/shell adapters) was unreachable
|
|
13
|
-
// through the real `setu` command this entire time, only ever testable
|
|
19
|
+
// through the real `cli-setu` command this entire time, only ever testable
|
|
14
20
|
// by calling node src/supervisor.js --cli claude directly. Flags
|
|
15
21
|
// (anything starting with "-") are no longer mistaken for subcommand
|
|
16
|
-
// names - they're checked first, and anything left over (bare `setu`, or
|
|
17
|
-
// `setu --cli claude`/other flags meant for the bridged session itself)
|
|
22
|
+
// names - they're checked first, and anything left over (bare `cli-setu`, or
|
|
23
|
+
// `cli-setu --cli claude`/other flags meant for the bridged session itself)
|
|
18
24
|
// falls through to the same "watch this session" path as before, which
|
|
19
25
|
// still parses --cli on its own further down the line (supervisor.js) -
|
|
20
26
|
// this dispatcher doesn't need to understand every flag supervisor.js
|
|
@@ -57,18 +63,23 @@ async function main() {
|
|
|
57
63
|
await stop();
|
|
58
64
|
return;
|
|
59
65
|
}
|
|
66
|
+
case 'unpair': {
|
|
67
|
+
const { unpair } = require('../src/unpair');
|
|
68
|
+
await unpair(AUTH_SERVER);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
60
71
|
default:
|
|
61
|
-
console.error(`[setu] Unknown command: ${firstArg}`);
|
|
72
|
+
console.error(`[cli-setu] Unknown command: ${firstArg}`);
|
|
62
73
|
printHelp();
|
|
63
74
|
process.exit(1);
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
// Bare `setu` (or `setu --cli <name>`, `setu --cli <name> <workingDir>`,
|
|
78
|
+
// Bare `cli-setu` (or `cli-setu --cli <name>`, `cli-setu --cli <name> <workingDir>`,
|
|
68
79
|
// etc. - real flags meant for the bridged session itself, parsed by
|
|
69
80
|
// supervisor.js) - watch the current directory as a session. Real
|
|
70
81
|
// product requirement: a brand-new user should be able to run just
|
|
71
|
-
// `setu` (no separate `setu pair` step to discover first) and see the
|
|
82
|
+
// `cli-setu` (no separate `cli-setu pair` step to discover first) and see the
|
|
72
83
|
// QR/8-digit code immediately, the same "first run pairs AND starts
|
|
73
84
|
// watching this session in one step" behavior the original
|
|
74
85
|
// proof-of-concept had. Only pairs if not already paired - an
|
|
@@ -83,22 +94,23 @@ async function main() {
|
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
function printHelp() {
|
|
86
|
-
console.log(`[setu] Bridge your terminal to your phone.
|
|
97
|
+
console.log(`[cli-setu] Bridge your terminal to your phone.
|
|
87
98
|
|
|
88
99
|
Usage:
|
|
89
|
-
setu Watch this folder as a session (pairs first if needed)
|
|
90
|
-
setu --cli <name> Same, but run a specific CLI inside the bridged
|
|
100
|
+
cli-setu Watch this folder as a session (pairs first if needed)
|
|
101
|
+
cli-setu --cli <name> Same, but run a specific CLI inside the bridged
|
|
91
102
|
session instead of the default plain shell
|
|
92
103
|
(e.g. --cli claude, --cli codex)
|
|
93
|
-
setu pair Pair this computer with your phone, nothing else
|
|
94
|
-
setu status Show pairing status and any sessions running now
|
|
95
|
-
setu stop Stop a running session
|
|
96
|
-
setu
|
|
97
|
-
setu --
|
|
98
|
-
setu --
|
|
104
|
+
cli-setu pair Pair this computer with your phone, nothing else
|
|
105
|
+
cli-setu status Show pairing status and any sessions running now
|
|
106
|
+
cli-setu stop Stop a running session
|
|
107
|
+
cli-setu unpair Unpair this computer (asks for confirmation)
|
|
108
|
+
cli-setu --version, -v Print the installed version
|
|
109
|
+
cli-setu --update Check for and install the latest version
|
|
110
|
+
cli-setu --help, -h Show this help`);
|
|
99
111
|
}
|
|
100
112
|
|
|
101
113
|
main().catch((err) => {
|
|
102
|
-
console.error('[setu] Error:', err.message);
|
|
114
|
+
console.error('[cli-setu] Error:', err.message);
|
|
103
115
|
process.exit(1);
|
|
104
116
|
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-setu",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Bridge your terminal to your phone — run and approve Claude Code, Codex CLI and other AI coding agents from anywhere.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
|
-
"setu": "bin/setu.js"
|
|
7
|
+
"cli-setu": "bin/setu.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "src/config.js",
|
|
10
10
|
"engines": {
|
|
@@ -13,11 +13,11 @@ const KNOWN_ADAPTERS = {
|
|
|
13
13
|
shell,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
// Real product decision (confirmed directly by the PM): bare `setu` with
|
|
16
|
+
// Real product decision (confirmed directly by the PM): bare `cli-setu` with
|
|
17
17
|
// no --cli flag bridges the user's own plain terminal, not Claude Code
|
|
18
18
|
// specifically - the user decides what to run once it's open. Defaulting
|
|
19
19
|
// to 'claude' here silently assumed every user wanted an AI CLI running
|
|
20
|
-
// the instant they typed `setu`, which isn't actually the product's real
|
|
20
|
+
// the instant they typed `cli-setu`, which isn't actually the product's real
|
|
21
21
|
// intent (a genuine remote terminal, not an AI-agent-only bridge).
|
|
22
22
|
function getAdapter(cliName) {
|
|
23
23
|
const normalized = (cliName || 'shell').toLowerCase();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// The default adapter when a user runs plain `setu` with no --cli flag at
|
|
2
|
-
// all - a real gap in the earlier design: `setu` (bare) always launched
|
|
1
|
+
// The default adapter when a user runs plain `cli-setu` with no --cli flag at
|
|
2
|
+
// all - a real gap in the earlier design: `cli-setu` (bare) always launched
|
|
3
3
|
// Claude Code specifically, silently assuming that's what every user
|
|
4
4
|
// wants. The actual product intent (confirmed directly by the PM) is
|
|
5
|
-
// simpler and more honest: `setu` bridges the user's OWN real terminal to
|
|
5
|
+
// simpler and more honest: `cli-setu` bridges the user's OWN real terminal to
|
|
6
6
|
// their phone - a genuine remote shell, nothing AI-specific - and the
|
|
7
7
|
// user decides what to run inside it once it's open (`claude`, `codex`,
|
|
8
8
|
// `npm install`, `git status`, anything). Running an AI CLI inside this
|
|
@@ -43,7 +43,7 @@ module.exports = {
|
|
|
43
43
|
// INSIDE this shell, that CLI's own real adapter (claude.js/codex.js)
|
|
44
44
|
// still isn't active here, since supervisor.js picks exactly one
|
|
45
45
|
// adapter per process/session at startup - a real user wanting
|
|
46
|
-
// structured approvals for a specific CLI should run `setu --cli
|
|
46
|
+
// structured approvals for a specific CLI should run `cli-setu --cli
|
|
47
47
|
// claude` (or codex) directly, same as always. This adapter is
|
|
48
48
|
// specifically for "just give me my terminal, I'll decide what to run."
|
|
49
49
|
approvals: null,
|
package/src/config.js
CHANGED
|
@@ -15,7 +15,7 @@ function defaultDeviceName() {
|
|
|
15
15
|
return os.hostname() || 'My Computer';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Saved once, on the first `setu pair`, so a later OS-level hostname
|
|
18
|
+
// Saved once, on the first `cli-setu pair`, so a later OS-level hostname
|
|
19
19
|
// change (renaming the machine, moving to a new user profile) doesn't
|
|
20
20
|
// silently rename an already-paired device out from under the user - the
|
|
21
21
|
// Devices tab's identity for this machine stays whatever it was when it
|
|
@@ -33,7 +33,7 @@ function writeConfig(config) {
|
|
|
33
33
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// Called by `setu pair` - creates the config the FIRST time, using the
|
|
36
|
+
// Called by `cli-setu pair` - creates the config the FIRST time, using the
|
|
37
37
|
// auto-detected hostname as the default device name. A config that
|
|
38
38
|
// already exists (this machine has paired before) is left untouched;
|
|
39
39
|
// re-pairing intentionally reuses the same device identity rather than
|
package/src/hooks.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// that - every user's global install lives at a different path (varies by
|
|
5
5
|
// OS, by nvm/volta version manager, by whether it's a global or local
|
|
6
6
|
// install), and expecting someone to hand-edit JSON before their first
|
|
7
|
-
// `setu` run defeats the entire point of "npm install -g cli-setu" being
|
|
7
|
+
// `cli-setu` run defeats the entire point of "npm install -g cli-setu" being
|
|
8
8
|
// a real, ready-to-use install. This ensures the CURRENT project's
|
|
9
9
|
// .claude/settings.json has the right hook registered, pointing at
|
|
10
10
|
// wherever THIS running copy of cli-setu actually lives on disk right now
|
|
@@ -34,7 +34,7 @@ function alreadyRegistered(existingHooksForEvent) {
|
|
|
34
34
|
|
|
35
35
|
// Ensures the CURRENT working directory's .claude/settings.json has both
|
|
36
36
|
// hooks pointed at this package's real install location. Called once at
|
|
37
|
-
// the start of every `setu` (bare) run - cheap (a few small file reads/
|
|
37
|
+
// the start of every `cli-setu` (bare) run - cheap (a few small file reads/
|
|
38
38
|
// writes), and self-healing if the package is ever moved/reinstalled at a
|
|
39
39
|
// different path (a stale command string gets overwritten with today's
|
|
40
40
|
// real path automatically, no manual fix needed).
|
|
@@ -48,8 +48,8 @@ function ensureHooksRegistered(projectDir) {
|
|
|
48
48
|
} catch {
|
|
49
49
|
// No settings.json yet, or it's not valid JSON - start fresh rather
|
|
50
50
|
// than crash; an unreadable existing file is left untouched below by
|
|
51
|
-
// simply not being parsed further, but we still want `setu` itself
|
|
52
|
-
// keep working for this session's approval detection.
|
|
51
|
+
// simply not being parsed further, but we still want `cli-setu` itself
|
|
52
|
+
// to keep working for this session's approval detection.
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
settings.hooks = settings.hooks || {};
|
|
@@ -73,7 +73,7 @@ function ensureHooksRegistered(projectDir) {
|
|
|
73
73
|
if (changed) {
|
|
74
74
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
75
75
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
76
|
-
console.log('[setu] Registered the approval-detection hook in .claude/settings.json');
|
|
76
|
+
console.log('[cli-setu] Registered the approval-detection hook in .claude/settings.json');
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
package/src/pair.js
CHANGED
|
@@ -11,18 +11,47 @@ async function pair(authServer, authServerPublic) {
|
|
|
11
11
|
const config = loadOrCreateConfig();
|
|
12
12
|
|
|
13
13
|
if (config.deviceToken) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Real bug found by testing: unpairing from the phone deletes the
|
|
15
|
+
// device server-side, but the laptop's local config.json still has
|
|
16
|
+
// the old (now-dead) token - this used to trust that local file
|
|
17
|
+
// blindly and declare itself "already paired" even when the server
|
|
18
|
+
// had no record of it at all. Confirm the saved token is actually
|
|
19
|
+
// still valid before trusting it; a dead token (401 - the server
|
|
20
|
+
// genuinely doesn't recognize it) falls through to a real re-pair.
|
|
21
|
+
// A network error/unreachable server is a THIRD, distinct outcome -
|
|
22
|
+
// deliberately NOT treated as "unpaired," since that would force an
|
|
23
|
+
// unwanted re-pair on every flaky-wifi run; it proceeds as if still
|
|
24
|
+
// paired instead, same as this check not existing at all.
|
|
25
|
+
let serverSaysUnpaired = false;
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${authServer}/device/whoami`, {
|
|
28
|
+
headers: { Authorization: `Bearer ${config.deviceToken}` },
|
|
29
|
+
});
|
|
30
|
+
serverSaysUnpaired = res.status === 401;
|
|
31
|
+
} catch {
|
|
32
|
+
// Network error/unreachable - unknown, not "unpaired". Fall through
|
|
33
|
+
// to the "already paired" branch below, unchanged from before this
|
|
34
|
+
// check existed.
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!serverSaysUnpaired) {
|
|
38
|
+
console.log(`[cli-setu] "${config.deviceName}" is already paired.`);
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`[cli-setu] This device's pairing is no longer valid on the server (likely unpaired from the app) - re-pairing...`);
|
|
43
|
+
config.deviceToken = null;
|
|
16
44
|
}
|
|
17
45
|
|
|
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.)');
|
|
46
|
+
console.log(`[cli-setu] Pairing as "${config.deviceName}"...`);
|
|
47
|
+
console.log('[cli-setu] (This is your computer\'s name. Rename it in ~/.cli-setu/config.json before pairing if you want something else.)');
|
|
20
48
|
|
|
21
49
|
// qr-start also needs a sessionId up front (mirrors supervisor-qr.js's
|
|
22
50
|
// "the QR both pairs it AND jumps the phone straight to watching THIS
|
|
23
|
-
// session" behavior) - 'pairing' is a placeholder id here since
|
|
24
|
-
// pair` itself never streams a terminal; the real per-run
|
|
25
|
-
// generated fresh by supervisor.js on every `setu`
|
|
51
|
+
// session" behavior) - 'pairing' is a placeholder id here since
|
|
52
|
+
// `cli-setu pair` itself never streams a terminal; the real per-run
|
|
53
|
+
// session id is generated fresh by supervisor.js on every `cli-setu`
|
|
54
|
+
// (bare) invocation.
|
|
26
55
|
const startRes = await fetch(`${authServer}/device/pair/qr-start`, {
|
|
27
56
|
method: 'POST',
|
|
28
57
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -34,8 +63,8 @@ async function pair(authServer, authServerPublic) {
|
|
|
34
63
|
|
|
35
64
|
console.log('');
|
|
36
65
|
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})`);
|
|
66
|
+
console.log('[cli-setu] Scan this with your phone camera.');
|
|
67
|
+
console.log(`[cli-setu] (Testing on one PC? Paste this URL into a browser tab instead: ${scanUrl})`);
|
|
39
68
|
|
|
40
69
|
const codeRes = await fetch(`${authServer}/device/pair/start`, {
|
|
41
70
|
method: 'POST',
|
|
@@ -43,8 +72,8 @@ async function pair(authServer, authServerPublic) {
|
|
|
43
72
|
body: JSON.stringify({ deviceName: config.deviceName }),
|
|
44
73
|
});
|
|
45
74
|
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...');
|
|
75
|
+
console.log(`[cli-setu] Or type this code in the app's Devices tab: ${typedCode}`);
|
|
76
|
+
console.log('[cli-setu] Waiting for scan or code entry...');
|
|
48
77
|
|
|
49
78
|
let confirmed = false;
|
|
50
79
|
while (!confirmed) {
|
|
@@ -56,13 +85,13 @@ async function pair(authServer, authServerPublic) {
|
|
|
56
85
|
confirmed = true;
|
|
57
86
|
config.deviceToken = status.deviceToken;
|
|
58
87
|
writeConfig(config);
|
|
59
|
-
console.log(`[setu] ✓ Paired via QR and saved to ~/.cli-setu/config.json`);
|
|
88
|
+
console.log(`[cli-setu] ✓ Paired via QR and saved to ~/.cli-setu/config.json`);
|
|
60
89
|
break;
|
|
61
90
|
} else if (status.status === 'EXPIRED') {
|
|
62
|
-
console.error('[setu] QR code expired. Run `setu pair` again to get a new one.');
|
|
91
|
+
console.error('[cli-setu] QR code expired. Run `cli-setu pair` again to get a new one.');
|
|
63
92
|
process.exit(1);
|
|
64
93
|
} else if (status.status === 'REJECTED') {
|
|
65
|
-
console.error(`[setu] Pairing rejected: ${status.rejectReason || 'the server declined this pairing.'}`);
|
|
94
|
+
console.error(`[cli-setu] Pairing rejected: ${status.rejectReason || 'the server declined this pairing.'}`);
|
|
66
95
|
process.exit(1);
|
|
67
96
|
}
|
|
68
97
|
|
|
@@ -72,7 +101,7 @@ async function pair(authServer, authServerPublic) {
|
|
|
72
101
|
confirmed = true;
|
|
73
102
|
config.deviceToken = typedStatus.deviceToken;
|
|
74
103
|
writeConfig(config);
|
|
75
|
-
console.log(`[setu] ✓ Paired via typed code and saved to ~/.cli-setu/config.json`);
|
|
104
|
+
console.log(`[cli-setu] ✓ Paired via typed code and saved to ~/.cli-setu/config.json`);
|
|
76
105
|
break;
|
|
77
106
|
}
|
|
78
107
|
}
|
package/src/status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// `setu status` - reports whether this computer is paired, and which
|
|
2
|
-
// sessions (if any) currently have a `setu` process running, using the
|
|
1
|
+
// `cli-setu status` - reports whether this computer is paired, and which
|
|
2
|
+
// sessions (if any) currently have a `cli-setu` process running, using the
|
|
3
3
|
// PID files supervisor.js writes/removes around its own lifetime (see
|
|
4
4
|
// its own comment on why - there was no way to answer this question at
|
|
5
5
|
// all before those existed).
|
|
@@ -21,10 +21,28 @@ function isProcessAlive(pid) {
|
|
|
21
21
|
async function status(authServer) {
|
|
22
22
|
const config = readConfig();
|
|
23
23
|
if (!config || !config.deviceToken) {
|
|
24
|
-
console.log('[setu] Not paired. Run `setu pair` to get started.');
|
|
24
|
+
console.log('[cli-setu] Not paired. Run `cli-setu pair` to get started.');
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
-
console.log(`[setu] Paired as "${config.deviceName}".`);
|
|
27
|
+
console.log(`[cli-setu] Paired as "${config.deviceName}".`);
|
|
28
|
+
|
|
29
|
+
// Real gap found by testing with multiple real accounts across multiple
|
|
30
|
+
// laptops: there was no way to confirm which Google account a laptop was
|
|
31
|
+
// paired to without checking the phone app directly - the device token
|
|
32
|
+
// itself carries no human-readable identity. Best-effort: a network
|
|
33
|
+
// failure here shouldn't block the rest of `status` from reporting what
|
|
34
|
+
// it already knows locally.
|
|
35
|
+
try {
|
|
36
|
+
const whoamiRes = await fetch(`${authServer}/device/whoami`, {
|
|
37
|
+
headers: { Authorization: `Bearer ${config.deviceToken}` },
|
|
38
|
+
});
|
|
39
|
+
if (whoamiRes.ok) {
|
|
40
|
+
const { userId } = await whoamiRes.json();
|
|
41
|
+
console.log(`[cli-setu] Paired to account: ${userId}`);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Offline, or the server's unreachable - status still works from local state alone.
|
|
45
|
+
}
|
|
28
46
|
|
|
29
47
|
let files;
|
|
30
48
|
try {
|
|
@@ -52,11 +70,11 @@ async function status(authServer) {
|
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
if (running.length === 0) {
|
|
55
|
-
console.log('[setu] No sessions currently running. Run `setu` in a project folder to start one.');
|
|
73
|
+
console.log('[cli-setu] No sessions currently running. Run `cli-setu` in a project folder to start one.');
|
|
56
74
|
return;
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
console.log(`[setu] ${running.length} session(s) running:`);
|
|
77
|
+
console.log(`[cli-setu] ${running.length} session(s) running:`);
|
|
60
78
|
for (const s of running) {
|
|
61
79
|
console.log(` - ${s.sessionId} · ${s.cwd} · started ${s.startedAt} (pid ${s.pid})`);
|
|
62
80
|
}
|
package/src/stop.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `setu stop` - cleanly disconnects every session currently running on
|
|
1
|
+
// `cli-setu stop` - cleanly disconnects every session currently running on
|
|
2
2
|
// this computer. Real gap in the PoC this replaces: the only way to stop
|
|
3
3
|
// a supervisor was closing its terminal window (or Ctrl+C, which only
|
|
4
4
|
// reaches the one you're actually looking at) - this reaches every
|
|
@@ -15,7 +15,7 @@ async function stop() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (files.length === 0) {
|
|
18
|
-
console.log('[setu] No sessions currently running.');
|
|
18
|
+
console.log('[cli-setu] No sessions currently running.');
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -35,7 +35,7 @@ async function stop() {
|
|
|
35
35
|
// PID file. A hard kill would skip all of that and leave the CLI
|
|
36
36
|
// process and the Azure connection to time out on their own.
|
|
37
37
|
process.kill(info.pid, 'SIGINT');
|
|
38
|
-
console.log(`[setu] Stopped ${info.sessionId} (${info.cwd})`);
|
|
38
|
+
console.log(`[cli-setu] Stopped ${info.sessionId} (${info.cwd})`);
|
|
39
39
|
stopped++;
|
|
40
40
|
} catch {
|
|
41
41
|
// Already dead - just clean up the stale file.
|
|
@@ -44,7 +44,7 @@ async function stop() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (stopped === 0) {
|
|
47
|
-
console.log('[setu] Sessions were already stopped (stale state cleaned up).');
|
|
47
|
+
console.log('[cli-setu] Sessions were already stopped (stale state cleaned up).');
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
package/src/supervisor.js
CHANGED
|
@@ -74,24 +74,27 @@ function log(...args) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// Real config, from pairing (src/pair.js, either via bin/setu.js's own
|
|
77
|
-
// bare-`setu` pairing check or the explicit `setu pair` command) -
|
|
78
|
-
// argv. bin/setu.js already guarantees this exists before requiring
|
|
79
|
-
// file for a normal `setu` run; this is a defensive fallback for
|
|
80
|
-
// requiring supervisor.js directly, so a null deviceToken fails
|
|
81
|
-
// clear message instead of a confusing crash deep in connectToAzure().
|
|
77
|
+
// bare-`cli-setu` pairing check or the explicit `cli-setu pair` command) -
|
|
78
|
+
// not argv. bin/setu.js already guarantees this exists before requiring
|
|
79
|
+
// this file for a normal `cli-setu` run; this is a defensive fallback for
|
|
80
|
+
// anyone requiring supervisor.js directly, so a null deviceToken fails
|
|
81
|
+
// with a clear message instead of a confusing crash deep in connectToAzure().
|
|
82
82
|
const config = readConfig();
|
|
83
83
|
if (!config || !config.deviceToken) {
|
|
84
|
-
console.error('[setu] This computer isn\'t paired yet. Run `setu` again, or run `setu pair` directly.');
|
|
84
|
+
console.error('[cli-setu] This computer isn\'t paired yet. Run `cli-setu` again, or run `cli-setu pair` directly.');
|
|
85
85
|
process.exit(1);
|
|
86
86
|
}
|
|
87
87
|
const DEVICE_NAME = config.deviceName;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// TEMPORARY: defaults to the DEV server, not production - see bin/setu.js's
|
|
90
|
+
// matching comment. Must switch back to https://api.clisetu.com before
|
|
91
|
+
// treating this package as production-ready for real users.
|
|
92
|
+
const AUTH_SERVER = process.env.CLI_SETU_SERVER || 'https://dev-api.clisetu.com';
|
|
90
93
|
|
|
91
|
-
// --cli still supported, e.g. `setu --cli codex`, for the same reason it
|
|
94
|
+
// --cli still supported, e.g. `cli-setu --cli codex`, for the same reason it
|
|
92
95
|
// existed before: one paired device can run more than one kind of CLI
|
|
93
96
|
// across different sessions/folders. Real product decision (confirmed
|
|
94
|
-
// directly by the PM): bare `setu` with no --cli flag bridges the user's
|
|
97
|
+
// directly by the PM): bare `cli-setu` with no --cli flag bridges the user's
|
|
95
98
|
// own plain terminal (see cli-adapters/shell.js), not Claude Code by
|
|
96
99
|
// default - the user decides what to run once it's open, same as
|
|
97
100
|
// getAdapter's own default (index.js) - kept in sync here rather than
|
|
@@ -105,7 +108,7 @@ const WORKING_DIR = process.cwd();
|
|
|
105
108
|
// plus the real working directory (not just the folder's basename, which
|
|
106
109
|
// collides the moment two different projects share a folder name like
|
|
107
110
|
// "app" on the same machine) plus a short random suffix so re-running
|
|
108
|
-
// `setu` in the same folder twice at once (e.g. two terminal tabs) never
|
|
111
|
+
// `cli-setu` in the same folder twice at once (e.g. two terminal tabs) never
|
|
109
112
|
// silently shares one session.
|
|
110
113
|
const dirHash = crypto.createHash('sha1').update(WORKING_DIR).digest('hex').slice(0, 8);
|
|
111
114
|
const SESSION_ID = `${os.hostname()}-${dirHash}-${crypto.randomBytes(3).toString('hex')}`;
|
|
@@ -121,7 +124,7 @@ console.log(`[supervisor] Using CLI adapter: ${adapter.displayName} (${adapter.i
|
|
|
121
124
|
log(`Using CLI adapter: ${adapter.displayName} (${adapter.id})`);
|
|
122
125
|
|
|
123
126
|
// Real bug found by testing: when the spawned adapter is 'shell' (the
|
|
124
|
-
// bare-`setu` default - see cli-adapters/shell.js), the user is expected
|
|
127
|
+
// bare-`cli-setu` default - see cli-adapters/shell.js), the user is expected
|
|
125
128
|
// to freely run `claude` INSIDE that bridged shell whenever they want to.
|
|
126
129
|
// Gating hook registration on "is the spawn-time adapter Claude" (as this
|
|
127
130
|
// used to) meant Claude Code's hooks never got installed into
|
|
@@ -141,21 +144,21 @@ const PROCESSED_FILE = path.join(CONFIG_DIR, `processed-requests.${SESSION_ID}.j
|
|
|
141
144
|
const POLL_INTERVAL_MS = 300;
|
|
142
145
|
|
|
143
146
|
// Always registered now (see QUEUE_FILE's comment above) - covers both
|
|
144
|
-
// `setu --cli claude` (adapter itself is Claude) and bare `setu` /
|
|
145
|
-
// `setu --cli shell` followed by the user manually running `claude`
|
|
147
|
+
// `cli-setu --cli claude` (adapter itself is Claude) and bare `cli-setu` /
|
|
148
|
+
// `cli-setu --cli shell` followed by the user manually running `claude`
|
|
146
149
|
// inside the bridged shell.
|
|
147
150
|
{
|
|
148
151
|
const { ensureHooksRegistered } = require('./hooks');
|
|
149
152
|
ensureHooksRegistered(WORKING_DIR);
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
// Real config, from `setu pair` (src/config.js) - not a per-device-name
|
|
153
|
-
// token file. Pairing is now a separate, one-time command (`setu pair`)
|
|
155
|
+
// Real config, from `cli-setu pair` (src/config.js) - not a per-device-name
|
|
156
|
+
// token file. Pairing is now a separate, one-time command (`cli-setu pair`)
|
|
154
157
|
// that must already have succeeded before this ever runs (checked above,
|
|
155
158
|
// where the process exits early if config.deviceToken is missing) - so
|
|
156
159
|
// unlike the PoC's ensurePaired(), there is no QR-pairing fallback here at
|
|
157
160
|
// all. A session watcher's job is to watch a session, not to also be the
|
|
158
|
-
// pairing flow; keeping them separate is what makes `setu` (bare) not
|
|
161
|
+
// pairing flow; keeping them separate is what makes `cli-setu` (bare) not
|
|
159
162
|
// need a QR code/camera every single run.
|
|
160
163
|
let deviceToken = config.deviceToken;
|
|
161
164
|
let deviceId = null;
|
|
@@ -233,6 +236,19 @@ async function connectToAzure() {
|
|
|
233
236
|
log(`[supervisor] /device/connect rejected (403): ${err.error || 'no reason given'}`);
|
|
234
237
|
process.exit(1);
|
|
235
238
|
}
|
|
239
|
+
// Real bug found by testing: a 401 here means the server genuinely
|
|
240
|
+
// doesn't recognize this device token anymore (e.g. unpaired from the
|
|
241
|
+
// phone app) - this used to fall into the generic retry-every-3s path
|
|
242
|
+
// below, silently looping forever on a token that will never become
|
|
243
|
+
// valid again, with no indication to the user of what's actually
|
|
244
|
+
// wrong or what to do about it. Treated the same as 403 now: stop,
|
|
245
|
+
// say so clearly, point at the real fix.
|
|
246
|
+
if (res.status === 401) {
|
|
247
|
+
console.error(`\n[supervisor] This device is no longer paired (it may have been unpaired from the app).`);
|
|
248
|
+
console.error('[supervisor] Run `cli-setu pair` to pair it again.');
|
|
249
|
+
log(`[supervisor] /device/connect rejected (401): ${err.error || 'no reason given'}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
236
252
|
log(`[supervisor] /device/connect failed: ${err.error || res.status}`);
|
|
237
253
|
setTimeout(connectToAzure, 3000);
|
|
238
254
|
return;
|
|
@@ -585,7 +601,7 @@ function handleAzureDecision(msg) {
|
|
|
585
601
|
// right of the actual PTY content - not a phone-side rendering bug at
|
|
586
602
|
// all, a genuine laptop-side geometry mismatch. process.stdout.columns/
|
|
587
603
|
// rows reflect the REAL terminal's actual current size at the moment
|
|
588
|
-
// `setu` starts (falling back to the old 120x30 only if those aren't
|
|
604
|
+
// `cli-setu` starts (falling back to the old 120x30 only if those aren't
|
|
589
605
|
// available - e.g. output piped to a file, not a real TTY). Still a
|
|
590
606
|
// FIXED size after this point, same reasoning as before (not read back
|
|
591
607
|
// off ptyProcess after spawn, never resized again - see the
|
|
@@ -1236,8 +1252,8 @@ async function main() {
|
|
|
1236
1252
|
process.stdin.resume();
|
|
1237
1253
|
process.stdin.on('data', (data) => ptyProcess.write(data.toString()));
|
|
1238
1254
|
|
|
1239
|
-
// Real gap in the PoC this replaces: there was no way for `setu status`
|
|
1240
|
-
// or `setu stop` (both new - see src/status.js, src/stop.js) to know
|
|
1255
|
+
// Real gap in the PoC this replaces: there was no way for `cli-setu status`
|
|
1256
|
+
// or `cli-setu stop` (both new - see src/status.js, src/stop.js) to know
|
|
1241
1257
|
// whether a session watcher was even running, or on which PID, since
|
|
1242
1258
|
// supervisor-qr.js was always started and killed by hand in a visible
|
|
1243
1259
|
// terminal. One small PID file per running session (keyed by
|
package/src/unpair.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `cli-setu unpair` - lets this laptop remove itself from its paired
|
|
2
|
+
// account, without needing the phone app. Real gap found by testing:
|
|
3
|
+
// unpairing only ever existed from the phone's Devices tab - there was no
|
|
4
|
+
// way to do this from the laptop side at all, and no way to recover from
|
|
5
|
+
// a stale local pairing except manually deleting ~/.cli-setu/config.json
|
|
6
|
+
// by hand.
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const { readConfig, writeConfig } = require('./config');
|
|
9
|
+
|
|
10
|
+
function confirm(question) {
|
|
11
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(question, (answer) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function unpair(authServer) {
|
|
21
|
+
const config = readConfig();
|
|
22
|
+
if (!config || !config.deviceToken) {
|
|
23
|
+
console.log('[cli-setu] Not paired - nothing to unpair.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const confirmed = await confirm(
|
|
28
|
+
`[cli-setu] This will unpair "${config.deviceName}" and stop it from connecting. Continue? (y/N) `
|
|
29
|
+
);
|
|
30
|
+
if (!confirmed) {
|
|
31
|
+
console.log('[cli-setu] Cancelled.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${authServer}/device/unpair-self`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { Authorization: `Bearer ${config.deviceToken}` },
|
|
39
|
+
});
|
|
40
|
+
// A 404 here means the server already has no record of this device
|
|
41
|
+
// (e.g. it was unpaired from the phone app already) - that's still a
|
|
42
|
+
// successful outcome from the user's point of view, so the local
|
|
43
|
+
// config is cleared the same way as a real 200.
|
|
44
|
+
if (!res.ok && res.status !== 404) {
|
|
45
|
+
const err = await res.json().catch(() => ({}));
|
|
46
|
+
console.error(`[cli-setu] Unpair failed: ${err.error || res.status}`);
|
|
47
|
+
console.error('[cli-setu] Local pairing left untouched - try again once the server is reachable.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`[cli-setu] Could not reach the server: ${err.message}`);
|
|
52
|
+
console.error('[cli-setu] Local pairing left untouched - try again once you\'re back online.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Only clear the local file once the server confirms - never unpair
|
|
57
|
+
// locally while leaving a real, still-active device row behind on the
|
|
58
|
+
// server (that would orphan it with no way to remove it via the CLI,
|
|
59
|
+
// since cli-setu pair would need a device token this file no longer has).
|
|
60
|
+
config.deviceToken = null;
|
|
61
|
+
writeConfig(config);
|
|
62
|
+
console.log(`[cli-setu] ✓ Unpaired. Run \`cli-setu pair\` to pair this computer again.`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { unpair };
|
package/src/update.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `setu --update` - checks the real npm registry for a newer published
|
|
1
|
+
// `cli-setu --update` - checks the real npm registry for a newer published
|
|
2
2
|
// version, and if one exists, runs the real upgrade command on the
|
|
3
3
|
// user's behalf (confirmed directly: don't just tell them what to run,
|
|
4
4
|
// actually run it) so they don't have to remember/type
|
|
@@ -35,24 +35,24 @@ function isNewer(latest, current) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function update() {
|
|
38
|
-
console.log(`[setu] Current version: ${pkg.version}`);
|
|
39
|
-
console.log('[setu] Checking for updates...');
|
|
38
|
+
console.log(`[cli-setu] Current version: ${pkg.version}`);
|
|
39
|
+
console.log('[cli-setu] Checking for updates...');
|
|
40
40
|
|
|
41
41
|
let latest;
|
|
42
42
|
try {
|
|
43
43
|
latest = await fetchLatestVersion();
|
|
44
44
|
} catch (err) {
|
|
45
|
-
console.error(`[setu] Could not check for updates: ${err.message}`);
|
|
45
|
+
console.error(`[cli-setu] Could not check for updates: ${err.message}`);
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (!isNewer(latest, pkg.version)) {
|
|
50
|
-
console.log(`[setu] Already on the latest version (${pkg.version}).`);
|
|
50
|
+
console.log(`[cli-setu] Already on the latest version (${pkg.version}).`);
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
console.log(`[setu] A newer version is available: ${latest} (you have ${pkg.version}).`);
|
|
55
|
-
console.log(`[setu] Installing ${pkg.name}@${latest}...`);
|
|
54
|
+
console.log(`[cli-setu] A newer version is available: ${latest} (you have ${pkg.version}).`);
|
|
55
|
+
console.log(`[cli-setu] Installing ${pkg.name}@${latest}...`);
|
|
56
56
|
try {
|
|
57
57
|
// execFile (not exec) - no shell involved, consistent with this
|
|
58
58
|
// project's own established convention for anything that runs a
|
|
@@ -62,10 +62,10 @@ async function update() {
|
|
|
62
62
|
shell: process.platform === 'win32', // npm.cmd on Windows needs a shell to resolve; execFile alone can't find it otherwise
|
|
63
63
|
});
|
|
64
64
|
if (stdout) console.log(stdout.trim());
|
|
65
|
-
console.log(`[setu] ✓ Updated to ${latest}.`);
|
|
65
|
+
console.log(`[cli-setu] ✓ Updated to ${latest}.`);
|
|
66
66
|
} catch (err) {
|
|
67
|
-
console.error(`[setu] Update failed: ${err.message}`);
|
|
68
|
-
console.error(`[setu] You can try running it yourself: npm install -g ${pkg.name}@${latest}`);
|
|
67
|
+
console.error(`[cli-setu] Update failed: ${err.message}`);
|
|
68
|
+
console.error(`[cli-setu] You can try running it yourself: npm install -g ${pkg.name}@${latest}`);
|
|
69
69
|
process.exit(1);
|
|
70
70
|
}
|
|
71
71
|
}
|