conductor-remote 1.7.0 → 1.8.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/README.md CHANGED
@@ -23,6 +23,15 @@ npm i -g conductor-remote
23
23
  conductor-remote service install # run on login; prints your phone URL
24
24
  ```
25
25
 
26
+ **Private tailnet only?** Pass `--expose tailnet` to skip public Funnel, so the URL
27
+ is reachable only from devices logged into your tailnet:
28
+
29
+ ```bash
30
+ conductor-remote service install --expose tailnet
31
+ ```
32
+
33
+ The choice is remembered across re-deploys; see **Reachability** below for the trade-off.
34
+
26
35
  Or the one-liner (checks Node, offers `brew install node` if missing, installs,
27
36
  registers the service):
28
37
 
@@ -35,12 +44,24 @@ on your machine, so `npm i -g` finishes in about a second. `service install`
35
44
  prints a phone URL with an embedded token; open it and **Add to Home Screen**.
36
45
  Manage the service with `conductor-remote service status|restart|uninstall`.
37
46
 
38
- **Reachability (`EXPOSE`).** By default the URL is exposed publicly via
47
+ **Install flags.** `service install` takes flags for the install-time knobs (each
48
+ also settable via the env var in brackets — the flag wins when both are given).
49
+ Run `conductor-remote --help` for the full list. The common ones:
50
+
51
+ | Flag | Env | Purpose |
52
+ | --- | --- | --- |
53
+ | `--expose public\|tailnet` | `EXPOSE` | Reachability (public Funnel default, or tailnet-only) |
54
+ | `--port <n>` | `RELAY_PORT` | Listen port (default `8787`) |
55
+ | `--token <secret>` | `RELAY_TOKEN` | Pin the shared secret (default: generated + persisted) |
56
+ | `--write-strategy <s>` | `WRITE_STRATEGY` | `applescript` (default) or `sidecar` |
57
+
58
+ **Reachability (`--expose`).** By default the URL is exposed publicly via
39
59
  [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) — reachable from any
40
60
  browser, gated by the embedded 128-bit token (so the phone needs **no** Tailscale
41
61
  app). Funnel must be enabled once for your tailnet (Admin console). To keep it
42
62
  tailnet-only instead (devices logged into your tailnet, via `tailscale serve`),
43
- install with `EXPOSE=tailnet`. The choice is remembered across re-deploys.
63
+ install with `--expose tailnet` (or `EXPOSE=tailnet`). The choice is remembered
64
+ across re-deploys.
44
65
 
45
66
  ## Architecture
46
67
 
@@ -131,16 +152,21 @@ so the home-screen URL stays valid across restarts and reboots. Logs land in
131
152
  and the AppleScript write path needs Accessibility permission granted to that
132
153
  `node` binary (System Settings ▸ Privacy & Security ▸ Accessibility).
133
154
 
134
- ### Config (env)
135
-
136
- | Var | Default | Purpose |
137
- | --- | --- | --- |
138
- | `RELAY_PORT` | `8787` | Listen port |
139
- | `RELAY_HOST` | auto (Tailscale `100.x`, else `127.0.0.1`) | Bind address |
140
- | `RELAY_TOKEN` | persisted (auto-generated, reused across restarts) | Override the shared secret |
141
- | `WRITE_STRATEGY` | `applescript` | `applescript` (focused session) or `sidecar` (precise per-session — see below) |
142
- | `CONDUCTOR_DB` | `~/Library/Application Support/com.conductor.app/conductor.db` | State DB |
143
- | `CONDUCTOR_WORKSPACES` | `~/conductor/workspaces` | Worktree root |
155
+ ### Config (env / flags)
156
+
157
+ Each knob is an env var (honored by `yarn start` and `service install`) and, for
158
+ `service install`, the CLI flag in the second column. A flag wins over the ambient
159
+ env. `EXPOSE`/`--expose` is documented under Install above.
160
+
161
+ | Var | Flag | Default | Purpose |
162
+ | --- | --- | --- | --- |
163
+ | `RELAY_PORT` | `--port` | `8787` | Listen port |
164
+ | `RELAY_HOST` | `--host` | auto (Tailscale `100.x`, else `127.0.0.1`) | Bind address |
165
+ | `RELAY_TOKEN` | `--token` | persisted (auto-generated, reused across restarts) | Override the shared secret |
166
+ | `WRITE_STRATEGY` | `--write-strategy` | `applescript` | `applescript` (focused session) or `sidecar` (precise per-session — see below) |
167
+ | `AUTO_UPDATE` | `--auto-update` | `auto` | Self-update mode: `auto` / `check` / `off` |
168
+ | `CONDUCTOR_DB` | `--db` | `~/Library/Application Support/com.conductor.app/conductor.db` | State DB |
169
+ | `CONDUCTOR_WORKSPACES` | `--workspaces` | `~/conductor/workspaces` | Worktree root |
144
170
 
145
171
  The token is auto-generated once and persisted, so the home-screen icon keeps
146
172
  working across restarts without any env var. To pin a specific secret (e.g. to
package/bin/cli.js CHANGED
@@ -37,7 +37,17 @@ function usage() {
37
37
  ' conductor-remote service <subcommand> manage the login LaunchAgent',
38
38
  ' install | uninstall | restart | status',
39
39
  '',
40
- 'Env: RELAY_HOST, RELAY_PORT, RELAY_TOKEN, WRITE_STRATEGY, AUTO_UPDATE, CONDUCTOR_DB, CONDUCTOR_WORKSPACES'
40
+ 'Install flags (each also settable via the env var in [brackets]):',
41
+ ' --expose public|tailnet reachability: public Funnel (default) or tailnet-only [EXPOSE]',
42
+ ' --port <n> listen port (default 8787) [RELAY_PORT]',
43
+ ' --host <addr> bind address (default 127.0.0.1) [RELAY_HOST]',
44
+ ' --token <secret> pin the shared secret (default: generated + persisted) [RELAY_TOKEN]',
45
+ ' --write-strategy <s> applescript (default) | sidecar [WRITE_STRATEGY]',
46
+ ' --auto-update <mode> auto (default) | check | off [AUTO_UPDATE]',
47
+ ' --db <path> Conductor state DB [CONDUCTOR_DB]',
48
+ ' --workspaces <path> worktree root [CONDUCTOR_WORKSPACES]',
49
+ '',
50
+ 'e.g. conductor-remote service install --expose tailnet --port 9000'
41
51
  ].join('\n')
42
52
  )
43
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
@@ -21,6 +21,45 @@ const logDir = path.join(os.homedir(), 'Library', 'Logs', 'conductor-remote')
21
21
  const uid = process.getuid?.() ?? 0
22
22
  const domain = `gui/${uid}`
23
23
 
24
+ /**
25
+ * Install-time knobs are accepted as documented CLI flags OR the matching env var — a flag wins over the
26
+ * ambient env. Parsed flags are folded back into process.env so everything downstream (and the plist we
27
+ * bake) keeps reading a single source. Runs before any module-level env read below.
28
+ */
29
+ const FLAG_ENV: Record<string, string> = {
30
+ '--expose': 'EXPOSE',
31
+ '--port': 'RELAY_PORT',
32
+ '--host': 'RELAY_HOST',
33
+ '--token': 'RELAY_TOKEN',
34
+ '--write-strategy': 'WRITE_STRATEGY',
35
+ '--auto-update': 'AUTO_UPDATE',
36
+ '--db': 'CONDUCTOR_DB',
37
+ '--workspaces': 'CONDUCTOR_WORKSPACES'
38
+ }
39
+
40
+ function applyFlags(argv: string[]): void {
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const arg = argv[i]
43
+ if (!arg.startsWith('--')) continue
44
+ const eq = arg.indexOf('=')
45
+ const name = eq === -1 ? arg : arg.slice(0, eq)
46
+ const envKey = FLAG_ENV[name]
47
+ if (!envKey) {
48
+ console.error(`unknown flag: ${name}\n known: ${Object.keys(FLAG_ENV).join(', ')}`)
49
+ process.exit(1)
50
+ }
51
+ const value = eq === -1 ? argv[++i] : arg.slice(eq + 1)
52
+ if (value === undefined) {
53
+ console.error(`flag ${name} needs a value (e.g. ${name} <value>)`)
54
+ process.exit(1)
55
+ }
56
+ process.env[envKey] = value
57
+ }
58
+ }
59
+
60
+ // argv[2] is the subcommand (see bottom); flags follow it.
61
+ applyFlags(process.argv.slice(3))
62
+
24
63
  function xml(s: string): string {
25
64
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
26
65
  }
@@ -95,6 +134,8 @@ function buildPlist(): string {
95
134
  if (process.env.RELAY_HOST) envEntries.push(['RELAY_HOST', process.env.RELAY_HOST])
96
135
  if (process.env.RELAY_PORT) envEntries.push(['RELAY_PORT', process.env.RELAY_PORT])
97
136
  if (process.env.AUTO_UPDATE) envEntries.push(['AUTO_UPDATE', process.env.AUTO_UPDATE])
137
+ if (process.env.CONDUCTOR_DB) envEntries.push(['CONDUCTOR_DB', process.env.CONDUCTOR_DB])
138
+ if (process.env.CONDUCTOR_WORKSPACES) envEntries.push(['CONDUCTOR_WORKSPACES', process.env.CONDUCTOR_WORKSPACES])
98
139
  const envXml = envEntries.map(([k, v]) => `\t\t<key>${xml(k)}</key>\n\t\t<string>${xml(v)}</string>`).join('\n')
99
140
  return `<?xml version="1.0" encoding="UTF-8"?>
100
141
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -132,20 +173,36 @@ function distBuilt(): boolean {
132
173
  return fs.existsSync(path.join(projectDir, 'dist', 'index.html'))
133
174
  }
134
175
 
176
+ function tokenStorePath(): string {
177
+ return path.join(os.homedir(), 'Library', 'Application Support', 'conductor-remote', 'token')
178
+ }
179
+
135
180
  /** Read the persisted token (or env override) purely to print the phone URL — never mints one. */
136
181
  function currentToken(): string | null {
137
182
  if (process.env.RELAY_TOKEN) return process.env.RELAY_TOKEN
138
183
  try {
139
- return (
140
- fs
141
- .readFileSync(path.join(os.homedir(), 'Library', 'Application Support', 'conductor-remote', 'token'), 'utf8')
142
- .trim() || null
143
- )
184
+ return fs.readFileSync(tokenStorePath(), 'utf8').trim() || null
144
185
  } catch {
145
186
  return null
146
187
  }
147
188
  }
148
189
 
190
+ /**
191
+ * A pinned token (`--token` / `RELAY_TOKEN`) is persisted to the token file, not baked into the plist —
192
+ * the launchd daemon has no such env, so it resolves the secret from this file (config.ts ▸ resolveToken).
193
+ * Writing it here keeps the daemon, the printed URL, and later `status` all in agreement.
194
+ */
195
+ function persistPinnedToken(): void {
196
+ const token = process.env.RELAY_TOKEN
197
+ if (!token) return
198
+ try {
199
+ fs.mkdirSync(path.dirname(tokenStorePath()), { recursive: true })
200
+ fs.writeFileSync(tokenStorePath(), token, { mode: 0o600 })
201
+ } catch (err) {
202
+ console.info(` ⚠ could not persist --token (${err instanceof Error ? err.message : err})`)
203
+ }
204
+ }
205
+
149
206
  const RELAY_PORT = process.env.RELAY_PORT ?? '8787'
150
207
 
151
208
  /** Locate the tailscale CLI: PATH first, then the common macOS install locations. Null if absent. */
@@ -354,6 +411,7 @@ function install(): void {
354
411
  console.error('✗ dist/ not built. Run `yarn build` first (or use `yarn deploy`, which builds).')
355
412
  process.exit(1)
356
413
  }
414
+ persistPinnedToken()
357
415
  fs.mkdirSync(path.dirname(plistPath), { recursive: true })
358
416
  fs.mkdirSync(logDir, { recursive: true })
359
417
  fs.writeFileSync(plistPath, buildPlist())
@@ -418,6 +476,10 @@ switch (cmd) {
418
476
  status()
419
477
  break
420
478
  default:
421
- console.error(`unknown command: ${cmd}\nusage: service.ts <install|uninstall|restart|status>`)
479
+ console.error(
480
+ `unknown command: ${cmd}\n` +
481
+ 'usage: service.ts <install|uninstall|restart|status> [flags]\n' +
482
+ ` flags (install): ${Object.keys(FLAG_ENV).join(', ')}`
483
+ )
422
484
  process.exit(1)
423
485
  }