@dashup/connector 0.1.1 → 0.1.4

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.
Files changed (3) hide show
  1. package/README.md +14 -29
  2. package/dist/index.js +358 -163
  3. package/package.json +6 -5
package/README.md CHANGED
@@ -5,32 +5,24 @@ Run beside the editor on each computer, including the PC whose Zed window connec
5
5
  With Node 24:
6
6
 
7
7
  ```sh
8
- npm install -g @dashup/connector
9
- dashup pair
8
+ npx @dashup/connector pair
10
9
  ```
11
10
 
12
- From a checkout, use pnpm 10.34.5 from the workspace root and `pnpm --filter @dashup/connector dashup` in place of `dashup`:
11
+ To keep `dashup` as a command, install it with `npm install -g @dashup/connector`. The package bundles the workspace code; screen and input need the native helper from `packages/desktop`, which it does not include.
13
12
 
14
- ```sh
15
- pnpm install
16
- pnpm exec turbo build --filter=@dashup/connector...
17
- ```
18
-
19
- The published package bundles the workspace code, and CI publishes it when the package version is new. Screen and input need the native helper from `packages/desktop`, which the package does not include.
20
-
21
- `dashup pair` prints a QR code that expires in five minutes, then keeps the connector running; scan it in the phone app under Pair a computer. The first phone adds the computer to its account, so the terminal names that account and shares nothing until you answer `y`. Later codes pair more phones on the same account without asking, including through a connector that is already running. A phone that scans an expired code gets a pairing the computer revokes, because it never kept that key. It uses `https://api.dashup.com`; for a local API, add `--api http://localhost:3000`. A physical phone needs a reachable HTTPS API. After restarting the connector, use `dashup watch`.
13
+ `dashup pair` walks through pairing in the terminal: it shows a QR code that expires in five minutes, waits for a phone to scan it under Pair a computer, and offers to keep Dashup running in the background. The first phone adds the computer to its account, so the walkthrough names that account and shares nothing until you confirm it. Later codes pair more phones on the same account without asking, including through a connector that is already running. A phone that scans an expired code gets a pairing the computer revokes, because it never kept that key. To pair without the prompt, as an agent can, name the account with `--account you@example.com`; a phone signed in to any other account is refused. Without a terminal, the code opens in a browser on this computer instead of printing, because it carries the pairing's encryption key and an agent's output leaves the computer. Pairing then exits, and `dashup install` keeps Dashup running. Where no browser can open, pairing needs a terminal. It uses `https://api.dashup.com`; for a local API, add `--api http://localhost:3000`. A physical phone needs a reachable HTTPS API. To run a paired connector in the foreground, use `dashup watch`.
22
14
 
23
15
  ## Existing projects and alerts
24
16
 
25
17
  ```sh
26
- pnpm --filter @dashup/connector dashup doctor
27
- pnpm --filter @dashup/connector dashup projects
28
- pnpm --filter @dashup/connector dashup alert --project PROJECT_ID --title "Dashup is connected"
18
+ dashup doctor
19
+ dashup projects
20
+ dashup alert --project PROJECT_ID --title "Dashup is connected"
29
21
  ```
30
22
 
31
23
  The connector automatically reads existing Claude Code, Codex, and OpenCode conversations alongside Zed workspaces and VS Code chat history. It honors the harness's existing data directory, authentication, and native session IDs. Native stores are opened read-only; discovery never launches an agent, creates or resumes a conversation, or changes editor settings. Opening the same supported harness session through Zed and its native history uses one Dashup thread when their actual session identities match. Matching directory names alone never merges local and SSH projects.
32
24
 
33
- A thread's time is its last conversation message in the agent's own history; editor bookkeeping such as Zed's sidebar restamps never moves it. Subagent sessions (OpenCode children, Codex spawned agents) carry their parent thread. Tool calls are joined with their results into one step with status, duration and, for file edits, a unified diff. Saved history has unknown live status and read-only controls. File changes publish project metadata and can produce a rate-limited “updated” alert. A workspace becomes a project once it has a conversation. After a scan that read every source, the connector publishes its list of projects, which retires any project it published earlier and left off. Each phone read request reads a thread's native history once and publishes a snapshot of separately encrypted message fragments to the database. The phone reads bounded latest and earlier pages; realtime thread updates carry only summary and approval metadata. Native history remains the source, and an incomplete upload never replaces the previous published snapshot. Native live interfaces provide actual status and pending choices. Tool integrations can also queue explicit encrypted alerts with `alert`.
25
+ A thread's time is its last conversation message in the agent's own history; editor bookkeeping such as Zed's sidebar restamps never moves it. Subagent sessions (OpenCode children, Codex spawned agents) carry their parent thread. Tool calls are joined with their results into one step with status, duration and, for file edits, a unified diff. Saved history has unknown live status and read-only controls. File changes publish project metadata; only an approval, a question, an error or a finished turn sends an alert. A workspace becomes a project once it has a conversation. After every change the connector publishes its overview: every project, and the conversations Home shows (anything that needs the person or is working, then the newest 20). The phone renders Home from the overview alone and decodes a project's full snapshot only when a page opens it. Only a scan that read every source retires a project it published earlier and left off. Each phone read request reads a thread's native history once and publishes a snapshot of separately encrypted message fragments to the database. The phone reads bounded latest and earlier pages; realtime thread updates carry only summary and approval metadata. Native history remains the source, and an incomplete upload never replaces the previous published snapshot. Native live interfaces provide actual status and pending choices. Tool integrations can also queue explicit encrypted alerts with `alert`.
34
26
 
35
27
  Project paths, SSH details, thread content, approvals, and alerts are encrypted separately for every paired phone. The encrypted outbox survives a connector restart. Parcae handles realtime delivery and reconnect; replay counters are saved before commands execute. Revoking a pairing stops its viewer and discards its connector key and queued content.
36
28
 
@@ -64,10 +56,10 @@ Live discovery is bounded to 100 sessions per endpoint. OpenCode supplies the la
64
56
  The default history and native-host path needs no wrapper or editor configuration. For an explicitly configured external ACP agent, use the connector as the editor's stdio wrapper; it uses the official ACP SDK's protocol contracts:
65
57
 
66
58
  ```sh
67
- node /absolute/path/to/dashup/apps/connector/dist/index.js acp -- /absolute/path/to/original-agent ORIGINAL_ARGUMENTS
59
+ dashup acp -- /absolute/path/to/original-agent ORIGINAL_ARGUMENTS
68
60
  ```
69
61
 
70
- Set the editor's existing agent command to Node, with the arguments above. Start the connector first and launch the agent through that editor configuration. The proxy starts the original command once and forwards the editor's own ACP stream. Phone messages and approval choices go to that same process and session. Only native approval choices are offered, including native file scope when the agent provides it. The proxy does not create another session when opening a phone thread.
62
+ Install `dashup` globally and set the editor's existing agent command to it, with the arguments above. Start the connector first and launch the agent through that editor configuration. The proxy starts the original command once and forwards the editor's own ACP stream. Phone messages and approval choices go to that same process and session. Only native approval choices are offered, including native file scope when the agent provides it. The proxy does not create another session when opening a phone thread.
71
63
 
72
64
  The wrapper and connector must run on the same execution host. For SSH projects, confirm where that particular external agent runs before configuring its command; [Zed treats external-agent runtime and authentication as agent-owned](https://zed.dev/docs/ai/external-agents#remote-projects). Pair the development machine separately if it owns the wrapper process. Reading the PC's saved SSH project does not attach to a remote agent process.
73
65
 
@@ -79,8 +71,8 @@ Build the native WebRTC helper on the computer, following [desktop build require
79
71
 
80
72
  ```sh
81
73
  pnpm --filter @dashup/desktop build
82
- pnpm --filter @dashup/connector dashup permissions --screen
83
- pnpm --filter @dashup/connector dashup permissions --screen --control
74
+ dashup permissions --screen
75
+ dashup permissions --screen --control
84
76
  ```
85
77
 
86
78
  Run `permissions` without flags to disable access. Video and input use a WebRTC peer connection; SDP, ICE candidates, and DTLS fingerprints travel inside paired-device encrypted envelopes. Only one screen session is active at a time. The relay does not carry decoded frames. Set `DASHUP_DESKTOP_BIN` if the helper lives outside its workspace build path.
@@ -89,22 +81,15 @@ macOS requires Screen Recording and Accessibility permission. Windows uses the s
89
81
 
90
82
  ## Start at login
91
83
 
92
- After pairing, stop the foreground connector and install the built entry point:
93
-
94
- ```sh
95
- node apps/connector/dist/index.js stop
96
- node apps/connector/dist/index.js install
97
- ```
98
-
99
- Installation registers a macOS LaunchAgent, Windows Startup entry, or Linux user systemd service. Linux requires a user systemd session. These source startup paths require the checkout and Node executable to remain at their installed locations. Signed desktop installers are not included.
84
+ Answering yes to the walkthrough's background question, or running `dashup install` on a paired computer, registers a macOS LaunchAgent, Windows Startup entry, or Linux user systemd service and starts it, taking over from any connector already running for that state file. The service runs `watch` with the state file it was paired with (`DASHUP_CONFIG`). Linux requires a user systemd session; without a desktop session, as over SSH, install also turns on lingering so the service keeps running after you log out. Under npx or pnpm dlx, whose caches can be cleared, the service runs a copy of that install kept in `app/` beside the state file; a checkout or global install runs in place. The Node executable must stay at its installed location. Signed desktop installers are not included.
100
85
 
101
- Remove startup with `launchctl unload ~/Library/LaunchAgents/com.dashup.connector.plist` and delete that file on macOS; delete `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Dashup.vbs` on Windows; or run `systemctl --user disable --now dashup.service` and delete `~/.config/systemd/user/dashup.service` on Linux. A macOS KeepAlive job restarts after `stop`; unload it when stopping the service.
86
+ `dashup uninstall` stops Dashup and removes the startup entry and its `app/` copy; the computer stays paired. `dashup stop` leaves the service stopped until the next sign-in; a crash restarts it.
102
87
 
103
88
  ## Local state and checks
104
89
 
105
90
  `~/.dashup/connector.json` holds this computer's secret, pairing keys, encrypted outbox, and replay counters. It is written atomically with private Unix permissions; it is not currently protected by an OS credential vault. Do not share or copy it to another connector. `DASHUP_CONFIG` selects a separate state file. `DASHUP_ZED_DATA` and `DASHUP_VSCODE_DATA` select custom editor data directories. Native history discovery honors `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, and XDG paths. The selected conversation is parsed locally on demand; Dashup does not create another plaintext transcript database.
106
91
 
107
- For isolated development fixtures, `DASHUP_LIVE_DISCOVERY=0` disables automatic process discovery. Explicit `DASHUP_OPENCODE_ENDPOINT`, `DASHUP_CODEX_SOCKET`, and `DASHUP_AHP_DATA` overrides allow local test endpoints without modifying editor configuration. They are not required for ordinary discovery.
92
+ From a checkout, run `pnpm install` and `pnpm exec turbo build --filter=@dashup/connector...` with pnpm 10.34.5, then use `pnpm --filter @dashup/connector dashup` in place of `dashup`. For isolated development fixtures, `DASHUP_LIVE_DISCOVERY=0` disables automatic process discovery. Explicit `DASHUP_OPENCODE_ENDPOINT`, `DASHUP_CODEX_SOCKET`, and `DASHUP_AHP_DATA` overrides allow local test endpoints without modifying editor configuration. They are not required for ordinary discovery.
108
93
 
109
94
  ```sh
110
95
  pnpm --filter @dashup/connector test
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ var require_XMLHttpRequest = __commonJS({
42
42
  "use strict";
43
43
  var fs = __require("fs");
44
44
  var Url = __require("url");
45
- var spawn4 = __require("child_process").spawn;
45
+ var spawn5 = __require("child_process").spawn;
46
46
  module.exports = XMLHttpRequest3;
47
47
  XMLHttpRequest3.XMLHttpRequest = XMLHttpRequest3;
48
48
  function XMLHttpRequest3(opts) {
@@ -338,7 +338,7 @@ var require_XMLHttpRequest = __commonJS({
338
338
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
339
339
  fs.writeFileSync(syncFile, "", "utf8");
340
340
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
341
- var syncProc = spawn4(process.argv[0], ["-e", execString]);
341
+ var syncProc = spawn5(process.argv[0], ["-e", execString]);
342
342
  var statusText;
343
343
  while (fs.existsSync(syncFile)) {
344
344
  }
@@ -1028,7 +1028,7 @@ var require_node = __commonJS({
1028
1028
  var tty = __require("tty");
1029
1029
  var util = __require("util");
1030
1030
  exports.init = init;
1031
- exports.log = log2;
1031
+ exports.log = log4;
1032
1032
  exports.formatArgs = formatArgs;
1033
1033
  exports.save = save;
1034
1034
  exports.load = load;
@@ -1163,7 +1163,7 @@ var require_node = __commonJS({
1163
1163
  }
1164
1164
  return (/* @__PURE__ */ new Date()).toISOString() + " ";
1165
1165
  }
1166
- function log2(...args2) {
1166
+ function log4(...args2) {
1167
1167
  return process.stderr.write(util.formatWithOptions(exports.inspectOpts, ...args2) + "\n");
1168
1168
  }
1169
1169
  function save(namespaces) {
@@ -2706,7 +2706,8 @@ var require_pluralize = __commonJS({
2706
2706
 
2707
2707
  // src/index.ts
2708
2708
  import { randomBytes as randomBytes5, timingSafeEqual } from "node:crypto";
2709
- import { createServer } from "node:http";
2709
+ import { createServer as createServer2 } from "node:http";
2710
+ import * as ui2 from "@clack/prompts";
2710
2711
  import { z as z3 } from "zod";
2711
2712
 
2712
2713
  // ../../packages/protocol/src/index.ts
@@ -4003,62 +4004,116 @@ async function proxyAcp(config2, executable, args2) {
4003
4004
 
4004
4005
  // src/pair.ts
4005
4006
  import { hostname } from "node:os";
4007
+ import { promisify } from "node:util";
4008
+ import { createServer } from "node:http";
4009
+ import { execFile } from "node:child_process";
4006
4010
  import { randomBytes as randomBytes2, createHash as createHash2 } from "node:crypto";
4007
- import { createInterface as createInterface2 } from "node:readline/promises";
4008
4011
  import QRCode from "qrcode";
4012
+ import * as ui from "@clack/prompts";
4009
4013
  import { z } from "zod";
4014
+
4015
+ // package.json
4016
+ var version = "0.1.4";
4017
+
4018
+ // src/pair.ts
4010
4019
  var Invitation = z.object({
4011
4020
  code: z.string().regex(/^[a-f0-9]{16}$/),
4012
4021
  key: z.string().regex(/^[a-f0-9]{64}$/),
4013
4022
  text: z.string().min(1)
4014
4023
  }).strict();
4024
+ var interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
4025
+ var intro2 = () => ui.intro(`Dashup ${version}`);
4015
4026
  function invitation(config2, secret = config2.secret) {
4016
4027
  const code = randomBytes2(8).toString("hex");
4017
4028
  const key = newSecret(randomBytes2);
4018
4029
  const computer = createHash2("sha256").update(secret).digest("hex");
4019
4030
  return { code, key, text: formatPairingCode({ api: config2.api, computer, code, key }) };
4020
4031
  }
4021
- async function scanned(config2, { code, text: text3 }, secret = config2.secret) {
4022
- const qr = await QRCode.toString(text3, {
4023
- type: "terminal",
4024
- small: true,
4025
- errorCorrectionLevel: "L"
4026
- });
4027
- process.stdout.write(
4028
- `
4029
- Pairing code: ${text3}
4030
-
4031
- ${qr}
4032
- Scan this in the Dashup app (Pair a computer). It expires in 5 minutes.
4033
- `
4034
- );
4035
- for (const deadline = Date.now() + PAIRING_TTL_MS; Date.now() < deadline; ) {
4036
- await new Promise((resolve3) => setTimeout(resolve3, 1e3));
4037
- const response = await fetch(`${config2.api}/v1/dashup/pairings?code=${code}`, {
4038
- headers: { Authorization: `Bearer ${secret}` }
4032
+ var SCAN = "Scan this in the Dashup app under Pair a computer. It expires in 5 minutes.";
4033
+ var BROWSER = {
4034
+ darwin: ["open"],
4035
+ win32: ["rundll32", "url.dll,FileProtocolHandler"]
4036
+ };
4037
+ var NO_BROWSER = "No browser here can show the pairing code. Run dashup pair in a terminal on this computer.";
4038
+ async function show(text3) {
4039
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
4040
+ throw new Error(NO_BROWSER);
4041
+ const path = `/${randomBytes2(16).toString("hex")}`;
4042
+ const qr = await QRCode.toString(text3, { type: "svg", margin: 2, errorCorrectionLevel: "L" });
4043
+ const page = `<!doctype html><meta charset="utf-8"><title>Pair with Dashup</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#fff;color:#16181d;font:16px/1.5 system-ui,sans-serif}main{max-width:22rem;padding:2rem;text-align:center}svg{width:100%;margin:1rem 0}code{font-size:11px;word-break:break-all;color:#5d6470}</style><main><p>${SCAN}</p>${qr}<p>Can't scan? Paste this code instead:</p><code>${text3}</code></main>`;
4044
+ const server = createServer((request2, response) => {
4045
+ if (request2.url !== path) return void response.writeHead(404).end();
4046
+ response.writeHead(200, {
4047
+ "Content-Type": "text/html; charset=utf-8",
4048
+ "Cache-Control": "no-store",
4049
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'",
4050
+ "Referrer-Policy": "no-referrer"
4039
4051
  });
4040
- if (response.status === 401 || response.status === 404) continue;
4041
- const value2 = await response.json();
4042
- if (!response.ok || !value2.success)
4043
- throw new Error(value2.error ?? `Dashup request failed (${response.status})`);
4044
- return value2.result;
4045
- }
4046
- throw new Error("The pairing code expired. Run dashup pair for a new one.");
4047
- }
4048
- async function confirm(question) {
4049
- const prompt = createInterface2({ input: process.stdin, output: process.stdout });
4050
- const answer = await new Promise((resolve3) => {
4051
- prompt.once("close", () => resolve3(""));
4052
- prompt.once("SIGINT", () => prompt.close());
4053
- prompt.question(question).then(resolve3, () => resolve3(""));
4052
+ response.end(page);
4054
4053
  });
4055
- prompt.close();
4056
- return /^y(es)?$/i.test(answer.trim());
4054
+ await new Promise((resolve3) => server.listen(0, "127.0.0.1", resolve3));
4055
+ const [command2, ...args2] = BROWSER[process.platform] ?? ["xdg-open"];
4056
+ const url2 = `http://127.0.0.1:${server.address().port}${path}`;
4057
+ try {
4058
+ await promisify(execFile)(command2, [...args2, url2]);
4059
+ } catch {
4060
+ server.close();
4061
+ throw new Error(NO_BROWSER);
4062
+ }
4063
+ process.stdout.write(`Opened the pairing code in a browser on this computer. ${SCAN}
4064
+ `);
4065
+ return () => server.close();
4057
4066
  }
4058
- async function link(config2) {
4067
+ async function scanned(config2, { code, text: text3 }, done, secret = config2.secret) {
4068
+ if (interactive) {
4069
+ const qr = await QRCode.toString(text3, {
4070
+ type: "terminal",
4071
+ small: true,
4072
+ errorCorrectionLevel: "L"
4073
+ });
4074
+ ui.log.step(SCAN);
4075
+ ui.log.message("Can't scan? Paste this code instead:", { spacing: 0 });
4076
+ process.stdout.write(` ${text3}
4077
+ `);
4078
+ ui.log.message(qr.split("\n").filter((row) => row.replace(/\x1b\[\d+m/g, "")));
4079
+ }
4080
+ const close = interactive ? void 0 : await show(text3);
4081
+ const waiting = interactive ? ui.spinner() : void 0;
4082
+ waiting?.start("Waiting for your phone");
4083
+ try {
4084
+ for (const deadline = Date.now() + PAIRING_TTL_MS; Date.now() < deadline; ) {
4085
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
4086
+ const response = await fetch(`${config2.api}/v1/dashup/pairings?code=${code}`, {
4087
+ headers: { Authorization: `Bearer ${secret}` }
4088
+ });
4089
+ if (response.status === 401 || response.status === 404) continue;
4090
+ const value2 = await response.json();
4091
+ if (!response.ok || !value2.success)
4092
+ throw new Error(value2.error ?? `Dashup request failed (${response.status})`);
4093
+ if (waiting) waiting.stop(done(value2.result.email));
4094
+ else process.stdout.write(`${done(value2.result.email)}.
4095
+ `);
4096
+ return value2.result;
4097
+ }
4098
+ throw new Error("The pairing code expired. Run dashup pair for a new one.");
4099
+ } catch (error) {
4100
+ if (!waiting) throw error;
4101
+ waiting.error(error instanceof Error ? error.message : "Pairing failed");
4102
+ throw Object.assign(error, { reported: true });
4103
+ } finally {
4104
+ close?.();
4105
+ }
4106
+ }
4107
+ var reported = (error) => error?.reported === true;
4108
+ async function link(config2, account) {
4059
4109
  const secret = newSecret(randomBytes2);
4060
4110
  const invite = invitation(config2, secret);
4061
- const { pairing, email } = await scanned(config2, invite, secret);
4111
+ const { pairing, email } = await scanned(
4112
+ config2,
4113
+ invite,
4114
+ (account2) => `Scanned by ${account2}`,
4115
+ secret
4116
+ );
4062
4117
  const linked = { ...config2, secret };
4063
4118
  const platform = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
4064
4119
  const { device } = await api(linked, "/dashup/devices", {
@@ -4067,11 +4122,14 @@ async function link(config2) {
4067
4122
  kind: "desktop",
4068
4123
  platform
4069
4124
  });
4070
- if (!await confirm(`
4071
- ${email} scanned the code. Pair this computer with that account? (y/N) `)) {
4125
+ const confirmed = account ? email.toLowerCase() === account.toLowerCase() : await ui.confirm({ message: `Pair this computer with ${email}?`, initialValue: false });
4126
+ if (confirmed !== true) {
4072
4127
  await api(linked, `/dashup/pairings/${pairing.id}`, void 0, "DELETE").catch(() => {
4073
4128
  });
4074
- throw new Error("Pairing cancelled. Nothing was shared.");
4129
+ ui.cancel(
4130
+ account ? `Pairing cancelled: that phone uses ${email}, not ${account}. Nothing was shared.` : "Pairing cancelled. Nothing was shared."
4131
+ );
4132
+ return false;
4075
4133
  }
4076
4134
  Object.assign(config2, {
4077
4135
  secret,
@@ -4082,18 +4140,19 @@ ${email} scanned the code. Pair this computer with that account? (y/N) `)) {
4082
4140
  unconfirmed: {}
4083
4141
  });
4084
4142
  saveConfig(config2);
4143
+ return true;
4085
4144
  }
4086
4145
 
4087
4146
  // src/viewer.ts
4088
4147
  import { existsSync as existsSync2 } from "node:fs";
4089
4148
  import { fileURLToPath } from "node:url";
4090
- import { createInterface as createInterface3 } from "node:readline";
4149
+ import { createInterface as createInterface2 } from "node:readline";
4091
4150
  import { spawn as spawn2 } from "node:child_process";
4092
4151
 
4093
4152
  // src/screen.ts
4094
- import { promisify } from "node:util";
4095
- import { execFile, spawnSync } from "node:child_process";
4096
- var run = promisify(execFile);
4153
+ import { promisify as promisify2 } from "node:util";
4154
+ import { execFile as execFile2, spawnSync } from "node:child_process";
4155
+ var run = promisify2(execFile2);
4097
4156
  var powershell = (source) => run("powershell.exe", [
4098
4157
  "-NoProfile",
4099
4158
  "-NonInteractive",
@@ -4268,7 +4327,7 @@ var Viewer = class _Viewer {
4268
4327
  const child = spawn2(DESKTOP_BIN, [], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
4269
4328
  this.process = child;
4270
4329
  let events = Promise.resolve();
4271
- createInterface3({ input: child.stdout }).on("error", () => this.failed(child)).on("line", (line) => {
4330
+ createInterface2({ input: child.stdout }).on("error", () => this.failed(child)).on("line", (line) => {
4272
4331
  events = events.then(() => {
4273
4332
  if (this.process === child) return this.receive(line);
4274
4333
  }).catch(() => {
@@ -5947,7 +6006,7 @@ var Polling = class extends Transport {
5947
6006
  */
5948
6007
  pause(onPause) {
5949
6008
  this.readyState = "pausing";
5950
- const pause = () => {
6009
+ const pause2 = () => {
5951
6010
  debug2("paused");
5952
6011
  this.readyState = "paused";
5953
6012
  onPause();
@@ -5959,7 +6018,7 @@ var Polling = class extends Transport {
5959
6018
  total++;
5960
6019
  this.once("pollComplete", function() {
5961
6020
  debug2("pre-pause polling complete");
5962
- --total || pause();
6021
+ --total || pause2();
5963
6022
  });
5964
6023
  }
5965
6024
  if (!this.writable) {
@@ -5967,11 +6026,11 @@ var Polling = class extends Transport {
5967
6026
  total++;
5968
6027
  this.once("drain", function() {
5969
6028
  debug2("pre-pause writing complete");
5970
- --total || pause();
6029
+ --total || pause2();
5971
6030
  });
5972
6031
  }
5973
6032
  } else {
5974
- pause();
6033
+ pause2();
5975
6034
  }
5976
6035
  }
5977
6036
  /**
@@ -15035,14 +15094,14 @@ var Model = class _Model extends import_index2.default {
15035
15094
  continue;
15036
15095
  }
15037
15096
  const pathStart = op.path.indexOf("/", 1);
15038
- const relative = {
15097
+ const relative2 = {
15039
15098
  ...op,
15040
15099
  path: op.path.slice(pathStart),
15041
15100
  ...from ? { from: from.slice(from.indexOf("/", 1)) } : {}
15042
15101
  };
15043
15102
  const grouped = refOps.get(segments[0]);
15044
- if (grouped) grouped.push(relative);
15045
- else refOps.set(segments[0], [relative]);
15103
+ if (grouped) grouped.push(relative2);
15104
+ else refOps.set(segments[0], [relative2]);
15046
15105
  }
15047
15106
  let didRefChange = false;
15048
15107
  const changedRefOps = [];
@@ -15217,7 +15276,7 @@ function prefix(ansi) {
15217
15276
  const code = codes[ansi] ?? "";
15218
15277
  return [`${code}${tag}\x1B[0m`];
15219
15278
  }
15220
- var log = {
15279
+ var log2 = {
15221
15280
  warn: (...args2) => isDev && console.warn(...prefix("#fb3"), ...args2),
15222
15281
  error: (...args2) => isDev && console.error(...prefix("#f44"), ...args2),
15223
15282
  debug: (...args2) => isDev && console.debug(...prefix("#888"), ...args2)
@@ -15257,7 +15316,7 @@ var SessionMachine = class {
15257
15316
  this.state.status = nextStatus;
15258
15317
  this.state.userId = userId;
15259
15318
  this.state.version++;
15260
- log.debug(
15319
+ log2.debug(
15261
15320
  userId ? `session: authenticated ${userId}` : "session: anonymous"
15262
15321
  );
15263
15322
  this._fireReady();
@@ -15272,7 +15331,7 @@ var SessionMachine = class {
15272
15331
  this.state.status = "terminated";
15273
15332
  this.state.userId = null;
15274
15333
  this.state.version++;
15275
- log.debug("session: terminated");
15334
+ log2.debug("session: terminated");
15276
15335
  this._fireReady();
15277
15336
  this._notify();
15278
15337
  }
@@ -15324,7 +15383,7 @@ var ConnectionMachine = class {
15324
15383
  this.state.status = status2;
15325
15384
  this.state.lastError = err2;
15326
15385
  this.state.version++;
15327
- log.debug(
15386
+ log2.debug(
15328
15387
  `connection: ${status2}${err2 ? ` (${err2.message})` : ""}`
15329
15388
  );
15330
15389
  for (const fn of this._listeners) fn();
@@ -15454,7 +15513,7 @@ var SocketTransport = class extends import_index2.default {
15454
15513
  this.helloState = "resolved";
15455
15514
  const userId = response.userId ?? null;
15456
15515
  const ms = (performance.now() - t0).toFixed(0);
15457
- log.debug(
15516
+ log2.debug(
15458
15517
  `hello: ${userId ? `userId=${userId}` : "anonymous"} (${ms}ms)`
15459
15518
  );
15460
15519
  this.session.resolve(userId);
@@ -15465,7 +15524,7 @@ var SocketTransport = class extends import_index2.default {
15465
15524
  },
15466
15525
  (err2) => {
15467
15526
  const error = err2 instanceof Error ? err2 : new Error(String(err2));
15468
- log.warn(`hello: token resolution failed (${error.message})`);
15527
+ log2.warn(`hello: token resolution failed (${error.message})`);
15469
15528
  this._rejectHandshake(generation, error);
15470
15529
  }
15471
15530
  );
@@ -15685,12 +15744,12 @@ var SocketTransport = class extends import_index2.default {
15685
15744
  const t0 = performance.now();
15686
15745
  const fullPath = `/${this.version}${path}`;
15687
15746
  const timeoutMs = options?.timeout ?? DEFAULT_TIMEOUT;
15688
- log.debug(`\u2192 ${method.toUpperCase()} ${fullPath}`);
15747
+ log2.debug(`\u2192 ${method.toUpperCase()} ${fullPath}`);
15689
15748
  return new Promise((resolve3, reject) => {
15690
15749
  const timeout = setTimeout(() => {
15691
15750
  this.socket.off(id);
15692
15751
  this.pendingCalls.delete(id);
15693
- log.debug(
15752
+ log2.debug(
15694
15753
  `\u2717 ${method.toUpperCase()} ${fullPath} timeout (${(timeoutMs / 1e3).toFixed(0)}s)`
15695
15754
  );
15696
15755
  reject(new Error(`RPC timeout: ${method} ${path}`));
@@ -15704,10 +15763,10 @@ var SocketTransport = class extends import_index2.default {
15704
15763
  const uncompressed = pako.ungzip(msg, { to: "string" });
15705
15764
  const parsed = (0, import_compress_json.decompress)(JSON.parse(uncompressed));
15706
15765
  if (parsed.success) {
15707
- log.debug(`\u2190 ${method.toUpperCase()} ${fullPath} (${ms}ms)`);
15766
+ log2.debug(`\u2190 ${method.toUpperCase()} ${fullPath} (${ms}ms)`);
15708
15767
  resolve3(parsed.result);
15709
15768
  } else {
15710
- log.debug(
15769
+ log2.debug(
15711
15770
  `\u2717 ${method.toUpperCase()} ${fullPath} (${ms}ms) ${parsed.error || parsed.message}`
15712
15771
  );
15713
15772
  const details = parsed.error && typeof parsed.error === "object" ? parsed.error : parsed;
@@ -15723,7 +15782,7 @@ var SocketTransport = class extends import_index2.default {
15723
15782
  reject(error);
15724
15783
  }
15725
15784
  } catch (err2) {
15726
- log.debug(
15785
+ log2.debug(
15727
15786
  `\u2717 ${method.toUpperCase()} ${fullPath} (${ms}ms) parse error`
15728
15787
  );
15729
15788
  reject(err2);
@@ -16187,7 +16246,6 @@ var Device = class extends Model {
16187
16246
  name = "";
16188
16247
  kind = "desktop";
16189
16248
  platform = "linux";
16190
- lastSeenAt = /* @__PURE__ */ new Date();
16191
16249
  pushToken = "";
16192
16250
  pushProvider = "";
16193
16251
  // A computer signs in with the secret this hashes; phones sign in with an account session.
@@ -16344,11 +16402,6 @@ import { connect } from "node:net";
16344
16402
  import { randomUUID as randomUUID2 } from "node:crypto";
16345
16403
  import { join as join4, isAbsolute as isAbsolute2 } from "node:path";
16346
16404
  import { stat, readdir, readFile } from "node:fs/promises";
16347
-
16348
- // package.json
16349
- var version = "0.1.1";
16350
-
16351
- // src/harness/codex.ts
16352
16405
  var REQUEST_MS = 4e3;
16353
16406
  var MAX_BYTES = 8 * 1024 * 1024;
16354
16407
  var MAX_SESSIONS = 100;
@@ -16681,9 +16734,9 @@ var Codex = class {
16681
16734
  import Socket3 from "ws";
16682
16735
  import { homedir as homedir5 } from "node:os";
16683
16736
  import { connect as connect2 } from "node:net";
16684
- import { promisify as promisify2 } from "node:util";
16737
+ import { promisify as promisify3 } from "node:util";
16685
16738
  import { fileURLToPath as fileURLToPath2 } from "node:url";
16686
- import { execFile as execFile2 } from "node:child_process";
16739
+ import { execFile as execFile3 } from "node:child_process";
16687
16740
  import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
16688
16741
  import { stat as stat2, readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
16689
16742
  import { join as join5, dirname as dirname3, basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
@@ -16696,7 +16749,7 @@ var MAX_METADATA = 64 * 1024;
16696
16749
  var MAX_HOSTS = 16;
16697
16750
  var MAX_SESSIONS2 = 100;
16698
16751
  var ROOT_CHANNEL = "ahp-root://";
16699
- var run2 = promisify2(execFile2);
16752
+ var run2 = promisify3(execFile3);
16700
16753
  var CONFIRMATION_CHOICES = [
16701
16754
  { id: "approve", label: "Allow once", kind: "approve" },
16702
16755
  { id: "deny", label: "Reject", kind: "deny" }
@@ -17178,11 +17231,11 @@ var VSCode = class {
17178
17231
 
17179
17232
  // src/harness/opencode.ts
17180
17233
  import { join as join6 } from "node:path";
17181
- import { promisify as promisify3 } from "node:util";
17234
+ import { promisify as promisify4 } from "node:util";
17182
17235
  import { createHash as createHash4 } from "node:crypto";
17183
- import { execFile as execFile3 } from "node:child_process";
17236
+ import { execFile as execFile4 } from "node:child_process";
17184
17237
  import { stat as stat3, readlink, readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
17185
- var run3 = promisify3(execFile3);
17238
+ var run3 = promisify4(execFile4);
17186
17239
  var REQUEST_MS3 = 4e3;
17187
17240
  var MAX_BYTES3 = 8 * 1024 * 1024;
17188
17241
  var MAX_SESSIONS3 = 100;
@@ -18079,7 +18132,7 @@ var NativeQueue = class {
18079
18132
 
18080
18133
  // src/connector.ts
18081
18134
  var EPHEMERAL_TTL_MS = 6e4;
18082
- var HISTORY_ALERT_INTERVAL_MS = 6e4;
18135
+ var HOME_RECENT = 20;
18083
18136
  var Command = z2.object({
18084
18137
  type: z2.literal("command"),
18085
18138
  requestId: z2.string().min(1),
@@ -18153,9 +18206,10 @@ var Connector = class {
18153
18206
  releases = [];
18154
18207
  announced = /* @__PURE__ */ new Set();
18155
18208
  listed = /* @__PURE__ */ new Map();
18209
+ listedProjects = /* @__PURE__ */ new Map();
18210
+ overviewQueued = false;
18156
18211
  snapshots = /* @__PURE__ */ new Set();
18157
18212
  nativeSnapshots = /* @__PURE__ */ new Map();
18158
- updatedAlerts = /* @__PURE__ */ new Map();
18159
18213
  queueReceipts = /* @__PURE__ */ new Map();
18160
18214
  // The content key behind each pairing code on show, until a phone pairs with it.
18161
18215
  invitations = /* @__PURE__ */ new Map();
@@ -18321,6 +18375,7 @@ var Connector = class {
18321
18375
  );
18322
18376
  this.config.outbox.push(envelope);
18323
18377
  saveConfig(this.config);
18378
+ if (content.type === "project" || content.type === "thread-state") this.refreshOverview();
18324
18379
  await this.flush();
18325
18380
  }
18326
18381
  discardLegacySnapshots() {
@@ -18360,13 +18415,48 @@ var Connector = class {
18360
18415
  await this.send(pair, { type: "project", project });
18361
18416
  await this.listProjects(pair);
18362
18417
  }
18363
- // Only a scan that read every source can tell which projects are gone.
18418
+ // The phone renders Home from this alone; it opens a project's full snapshot only on demand.
18419
+ overview() {
18420
+ const projects = [...this.discovery.projects.values()];
18421
+ const sessions = projects.flatMap((project) => project.threads.map((thread) => ({
18422
+ projectId: project.id,
18423
+ thread,
18424
+ approvals: this.threadState(project, thread).approvals
18425
+ }))).sort((a, b) => b.thread.updatedAt.localeCompare(a.thread.updatedAt));
18426
+ const busy = ({ thread, approvals }) => approvals.length > 0 || thread.status === "attention" || thread.status === "working";
18427
+ if (!this.discovery.warnings.length) this.listedProjects.clear();
18428
+ for (const { threads, ...project } of projects) {
18429
+ const started = threads.filter((thread) => !thread.parent);
18430
+ this.listedProjects.set(project.id, {
18431
+ ...project,
18432
+ active: started.reduce((newest, thread) => thread.updatedAt > newest ? thread.updatedAt : newest, ""),
18433
+ started: started.length
18434
+ });
18435
+ }
18436
+ return {
18437
+ type: "projects",
18438
+ projects: [...this.listedProjects.values()],
18439
+ sessions: [
18440
+ ...sessions.filter(busy),
18441
+ ...sessions.filter((session) => !busy(session) && !session.thread.parent).slice(0, HOME_RECENT)
18442
+ ]
18443
+ };
18444
+ }
18364
18445
  async listProjects(pair) {
18365
- const ids = [...this.discovery.projects.keys()].sort();
18366
- const list2 = JSON.stringify(ids);
18367
- if (this.discovery.warnings.length || this.listed.get(pair.id) === list2) return;
18368
- await this.send(pair, { type: "projects", ids });
18369
- this.listed.set(pair.id, list2);
18446
+ const overview = this.overview();
18447
+ const text3 = JSON.stringify(overview);
18448
+ if (this.listed.get(pair.id) === text3) return;
18449
+ await this.send(pair, overview);
18450
+ this.listed.set(pair.id, text3);
18451
+ }
18452
+ // Each change of a project or thread brings the overview up to date once it settles.
18453
+ refreshOverview() {
18454
+ if (this.overviewQueued) return;
18455
+ this.overviewQueued = true;
18456
+ this.work = this.work.then(async () => {
18457
+ this.overviewQueued = false;
18458
+ for (const pair of this.activePairs()) await this.listProjects(pair);
18459
+ }).catch((error) => this.report(error));
18370
18460
  }
18371
18461
  queueChange() {
18372
18462
  const run5 = this.work.then(() => this.changed());
@@ -18408,26 +18498,9 @@ var Connector = class {
18408
18498
  this.stopWatching();
18409
18499
  this.stopWatching = this.discovery.watch(this.rescan);
18410
18500
  for (const project of projects) {
18411
- const old = previous.get(project.id);
18412
- if (JSON.stringify(old) === JSON.stringify(project)) continue;
18501
+ if (JSON.stringify(previous.get(project.id)) === JSON.stringify(project)) continue;
18413
18502
  for (const pair of this.activePairs()) await this.send(pair, { type: "project", project });
18414
- for (const thread of project.threads) {
18415
- const before = old?.threads.find((item) => item.id === thread.id);
18416
- if (before && before.updatedAt !== thread.updatedAt && !this.discovery.locations.get(thread.id)?.live && !this.attached(thread)) {
18417
- const last = this.updatedAlerts.get(thread.id) ?? 0;
18418
- if (Date.now() - last < HISTORY_ALERT_INTERVAL_MS) continue;
18419
- this.updatedAlerts.set(thread.id, Date.now());
18420
- await this.attention(
18421
- project.id,
18422
- thread.id,
18423
- thread.title,
18424
- "The existing thread was updated.",
18425
- "updated"
18426
- );
18427
- }
18428
- }
18429
18503
  }
18430
- for (const pair of this.activePairs()) await this.listProjects(pair);
18431
18504
  for (const { project, thread, session } of live) {
18432
18505
  const snapshot = this.threadState(project, thread);
18433
18506
  const prior = this.nativeSnapshots.get(thread.id);
@@ -18446,6 +18519,7 @@ var Connector = class {
18446
18519
  "complete"
18447
18520
  );
18448
18521
  }
18522
+ for (const pair of this.activePairs()) await this.listProjects(pair);
18449
18523
  await this.publishQueueReceipts();
18450
18524
  }
18451
18525
  refreshQueues() {
@@ -18745,63 +18819,96 @@ var Connector = class {
18745
18819
  };
18746
18820
 
18747
18821
  // src/install.ts
18748
- import { homedir as homedir6 } from "node:os";
18749
- import { dirname as dirname5, join as join8 } from "node:path";
18822
+ import { existsSync as existsSync6 } from "node:fs";
18823
+ import { homedir as homedir6, userInfo } from "node:os";
18824
+ import { promisify as promisify5 } from "node:util";
18750
18825
  import { fileURLToPath as fileURLToPath3 } from "node:url";
18751
- import { promisify as promisify4 } from "node:util";
18752
- import { execFile as execFile4 } from "node:child_process";
18753
- import { mkdir, writeFile } from "node:fs/promises";
18754
- var run4 = promisify4(execFile4);
18826
+ import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
18827
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
18828
+ import { dirname as dirname5, join as join8, relative, sep } from "node:path";
18829
+ var run4 = promisify5(execFile5);
18755
18830
  var xml = (value2) => value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
18756
- async function installStartup() {
18757
- const entry = fileURLToPath3(import.meta.url);
18758
- if (!entry.endsWith("/dist/index.js") && !entry.endsWith("\\dist\\index.js"))
18759
- throw new Error(
18760
- "Build the connector before installing startup: pnpm --filter @dashup/connector build"
18761
- );
18762
- const node = process.execPath;
18763
- if (process.platform === "darwin") {
18764
- const path = join8(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist");
18765
- await mkdir(dirname5(path), { recursive: true });
18766
- await writeFile(
18767
- path,
18768
- `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.dashup.connector</string><key>ProgramArguments</key><array><string>${xml(node)}</string><string>${xml(entry)}</string><string>watch</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/></dict></plist>`
18769
- );
18770
- await run4("launchctl", ["load", path]);
18771
- } else if (process.platform === "win32") {
18772
- const path = join8(
18773
- process.env.APPDATA ?? join8(homedir6(), "AppData/Roaming"),
18774
- "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
18775
- );
18776
- const quote = (value2) => '""' + value2.replaceAll('"', '""') + '""';
18777
- await writeFile(
18778
- path,
18779
- `CreateObject("WScript.Shell").Run "${quote(node)} ${quote(entry)} watch", 0, False\r
18780
- `
18781
- );
18782
- } else {
18783
- const path = join8(homedir6(), ".config/systemd/user/dashup.service");
18784
- await mkdir(dirname5(path), { recursive: true });
18785
- const quote = (value2) => '"' + value2.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%") + '"';
18786
- await writeFile(
18787
- path,
18788
- `[Unit]
18831
+ var NODE_FLAGS = ["--disable-warning=ExperimentalWarning"];
18832
+ var serviceFile = () => process.platform === "darwin" ? join8(homedir6(), "Library/LaunchAgents/com.dashup.connector.plist") : process.platform === "win32" ? join8(
18833
+ process.env.APPDATA ?? join8(homedir6(), "AppData/Roaming"),
18834
+ "Microsoft/Windows/Start Menu/Programs/Startup/Dashup.vbs"
18835
+ ) : join8(homedir6(), ".config/systemd/user/dashup.service");
18836
+ var startupInstalled = () => existsSync6(serviceFile());
18837
+ function service(platform, node, entry, config2) {
18838
+ const command2 = [node, ...NODE_FLAGS, entry];
18839
+ if (platform === "darwin")
18840
+ return `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>Label</key><string>com.dashup.connector</string><key>EnvironmentVariables</key><dict><key>DASHUP_CONFIG</key><string>${xml(config2)}</string></dict><key>ProgramArguments</key><array>${[...command2, "watch"].map((value2) => `<string>${xml(value2)}</string>`).join("")}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict></dict></plist>`;
18841
+ if (platform === "win32") {
18842
+ const quote2 = (value2) => '""' + value2.replaceAll('"', '""') + '""';
18843
+ return `Set shell = CreateObject("WScript.Shell")\r
18844
+ shell.Environment("Process")("DASHUP_CONFIG") = "${config2.replaceAll('"', '""')}"\r
18845
+ shell.Run "${command2.map(quote2).join(" ")} watch", 0, False\r
18846
+ `;
18847
+ }
18848
+ const quote = (value2) => '"' + value2.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%") + '"';
18849
+ return `[Unit]
18789
18850
  Description=Dashup desktop connector
18790
18851
  After=network-online.target
18791
18852
 
18792
18853
  [Service]
18793
- ExecStart=${quote(node)} ${quote(entry)} watch
18854
+ Environment=${quote(`DASHUP_CONFIG=${config2}`)}
18855
+ ExecStart=${command2.map(quote).join(" ")} watch
18794
18856
  Restart=on-failure
18795
18857
  RestartSec=5
18796
18858
 
18797
18859
  [Install]
18798
18860
  WantedBy=default.target
18799
- `
18861
+ `;
18862
+ }
18863
+ async function durableEntry() {
18864
+ const entry = fileURLToPath3(import.meta.url);
18865
+ if (!entry.endsWith(`${sep}dist${sep}index.js`))
18866
+ throw new Error(
18867
+ "Build the connector before installing startup: pnpm --filter @dashup/connector build"
18800
18868
  );
18869
+ if (!["_npx", "dlx"].some((cache2) => entry.includes(`${sep}${cache2}${sep}`))) return entry;
18870
+ const root = entry.slice(0, entry.indexOf(`${sep}node_modules${sep}`));
18871
+ const app = join8(dirname5(configPath()), "app");
18872
+ await rm(app, { recursive: true, force: true });
18873
+ await cp(join8(root, "node_modules"), join8(app, "node_modules"), {
18874
+ recursive: true,
18875
+ verbatimSymlinks: true
18876
+ });
18877
+ return join8(app, relative(root, entry));
18878
+ }
18879
+ async function installStartup() {
18880
+ const path = serviceFile();
18881
+ await mkdir(dirname5(path), { recursive: true });
18882
+ await writeFile(
18883
+ path,
18884
+ service(process.platform, process.execPath, await durableEntry(), configPath())
18885
+ );
18886
+ if (process.platform === "darwin") {
18887
+ await run4("launchctl", ["unload", path]).catch(() => {
18888
+ });
18889
+ await run4("launchctl", ["load", path]);
18890
+ } else if (process.platform === "win32") {
18891
+ spawn4("wscript.exe", [path], { detached: true, stdio: "ignore" }).unref();
18892
+ } else {
18801
18893
  await run4("systemctl", ["--user", "daemon-reload"]);
18802
- await run4("systemctl", ["--user", "enable", "--now", "dashup.service"]);
18894
+ await run4("systemctl", ["--user", "enable", "dashup.service"]);
18895
+ await run4("systemctl", ["--user", "restart", "dashup.service"]);
18896
+ if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
18897
+ return run4("loginctl", ["enable-linger"]).then(
18898
+ () => void 0,
18899
+ () => `It stops when you log out. To keep it running, run sudo loginctl enable-linger ${userInfo().username}.`
18900
+ );
18803
18901
  }
18804
- process.stdout.write("Dashup will start when you sign in to this computer.\n");
18902
+ }
18903
+ async function uninstallStartup() {
18904
+ const path = serviceFile();
18905
+ if (process.platform === "darwin") await run4("launchctl", ["unload", path]).catch(() => {
18906
+ });
18907
+ else if (process.platform !== "win32")
18908
+ await run4("systemctl", ["--user", "disable", "dashup.service"]).catch(() => {
18909
+ });
18910
+ await rm(path, { force: true });
18911
+ await rm(join8(dirname5(configPath()), "app"), { recursive: true, force: true });
18805
18912
  }
18806
18913
 
18807
18914
  // src/index.ts
@@ -18820,6 +18927,42 @@ var refused = (error) => {
18820
18927
  if (error.cause?.code === "ECONNREFUSED") return null;
18821
18928
  throw error;
18822
18929
  };
18930
+ var pause = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
18931
+ async function answering(expected) {
18932
+ for (let tries = 0; tries < 40; tries++) {
18933
+ const status2 = await running("/status", { signal: AbortSignal.timeout(1e3) }).catch(
18934
+ () => null
18935
+ );
18936
+ if (Boolean(status2?.ok) === expected) return true;
18937
+ await pause(250);
18938
+ }
18939
+ return false;
18940
+ }
18941
+ var BACKGROUND = "Run Dashup in the background and start it when you sign in?";
18942
+ var DONE2 = "Your projects appear on your phone.";
18943
+ async function background() {
18944
+ const waiting = interactive ? ui2.spinner() : void 0;
18945
+ waiting?.start("Starting Dashup in the background");
18946
+ let failure = "Dashup did not start in the background. Run dashup watch to see why.";
18947
+ try {
18948
+ await running("/stop", { method: "POST" }).catch(() => null);
18949
+ await answering(false);
18950
+ const note = await installStartup();
18951
+ if (await answering(true)) {
18952
+ const message = `Dashup runs in the background and starts when you sign in.${note ? ` ${note}` : ""}`;
18953
+ if (waiting) waiting.stop(message);
18954
+ else process.stdout.write(`${message}
18955
+ `);
18956
+ return true;
18957
+ }
18958
+ } catch (error) {
18959
+ if (error instanceof Error) failure = error.message;
18960
+ }
18961
+ if (waiting) waiting.error(failure);
18962
+ else process.stderr.write(`${failure}
18963
+ `);
18964
+ return false;
18965
+ }
18823
18966
  var authorized = (request2) => {
18824
18967
  const expected = Buffer.from(`Bearer ${config.localSecret}`);
18825
18968
  const actual = Buffer.from(request2.headers.authorization ?? "");
@@ -18830,7 +18973,7 @@ var Attention = z3.object({
18830
18973
  threadId: z3.string().default(""),
18831
18974
  title: z3.string().min(1).max(200),
18832
18975
  body: z3.string().max(4e3).default(""),
18833
- reason: z3.enum(["input", "approval", "error", "complete", "updated"]).default("input")
18976
+ reason: z3.enum(["input", "approval", "error", "complete"]).default("input")
18834
18977
  }).strict();
18835
18978
  async function main() {
18836
18979
  if (option("--api")) {
@@ -18843,7 +18986,21 @@ async function main() {
18843
18986
  throw new Error("Use a separate DASHUP_CONFIG file to connect a different server");
18844
18987
  config.api = url2.origin;
18845
18988
  }
18846
- if (command === "install") return installStartup();
18989
+ const account = option("--account");
18990
+ if (account !== void 0 && !/^[^\s@]+@[^\s@]+$/.test(account))
18991
+ throw new Error("Use --account with the email you sign in to the Dashup app with");
18992
+ if (command === "install") {
18993
+ if (!config.secret) throw new Error("Pair this computer first: run dashup pair.");
18994
+ if (!await background()) process.exitCode = 1;
18995
+ return;
18996
+ }
18997
+ if (command === "uninstall") {
18998
+ await running("/stop", { method: "POST" }).catch(() => null);
18999
+ await answering(false);
19000
+ await uninstallStartup();
19001
+ process.stdout.write("Dashup no longer starts when you sign in. This computer stays paired.\n");
19002
+ return;
19003
+ }
18847
19004
  if (command === "stop") {
18848
19005
  const response = await running("/stop", { method: "POST" }).catch(refused);
18849
19006
  if (!response) throw new Error("Dashup is not running on this computer");
@@ -18934,14 +19091,26 @@ async function main() {
18934
19091
  }
18935
19092
  if (!["pair", "watch"].includes(command)) {
18936
19093
  process.stdout.write(
18937
- "Dashup: pair | watch | projects | alert --project <id> | doctor | permissions --screen --control | stop | acp -- <original-agent-command> | install\n"
19094
+ "Dashup: pair [--account <email>] | watch | projects | alert --project <id> | doctor | permissions --screen --control | stop | acp -- <original-agent-command> | install | uninstall\n"
18938
19095
  );
18939
19096
  return;
18940
19097
  }
18941
- const pairPhone = async (invite) => {
18942
- const { email } = await scanned(config, invite);
18943
- process.stdout.write(`Paired a phone signed in as ${email}.
18944
- `);
19098
+ const pairPhone = (invite) => scanned(config, invite, (email) => `Paired a phone signed in as ${email}`);
19099
+ const keepRunning = async (stopHere) => {
19100
+ const answer = startupInstalled() || interactive && await ui2.confirm({ message: BACKGROUND }) === true;
19101
+ if (answer) {
19102
+ stopHere();
19103
+ const started = await background();
19104
+ ui2.outro(started ? DONE2 : "Run dashup to keep Dashup running in this terminal.");
19105
+ process.exit(started ? 0 : 1);
19106
+ }
19107
+ if (!interactive) {
19108
+ stopHere();
19109
+ process.stdout.write("Paired. Run dashup install to keep Dashup running in the background.\n");
19110
+ process.exit(0);
19111
+ }
19112
+ ui2.log.info("Dashup runs in this terminal. Keep it open; Ctrl+C stops it.");
19113
+ ui2.outro(DONE2);
18945
19114
  };
18946
19115
  const status2 = await running("/status", { signal: AbortSignal.timeout(1e3) }).catch(refused);
18947
19116
  if (status2?.ok) {
@@ -18956,10 +19125,21 @@ async function main() {
18956
19125
  throw new Error(
18957
19126
  "An older Dashup connector is running. Run dashup stop, then run dashup pair with the updated connector."
18958
19127
  );
18959
- return pairPhone(invite.data);
19128
+ if (interactive) intro2();
19129
+ await pairPhone(invite.data);
19130
+ if (interactive && !startupInstalled()) {
19131
+ const answer = await ui2.confirm({ message: BACKGROUND });
19132
+ if (answer === true && !await background()) process.exitCode = 1;
19133
+ }
19134
+ if (interactive) ui2.outro(DONE2);
19135
+ process.exit();
18960
19136
  }
19137
+ if (!config.secret && !interactive && !account)
19138
+ throw new Error(
19139
+ "To pair this computer, run dashup pair in a terminal, or add --account with your Dashup email."
19140
+ );
18961
19141
  let connector;
18962
- const server = createServer(async (request2, response) => {
19142
+ const server = createServer2(async (request2, response) => {
18963
19143
  if (!authorized(request2)) {
18964
19144
  response.writeHead(403).end();
18965
19145
  return;
@@ -19036,12 +19216,26 @@ async function main() {
19036
19216
  });
19037
19217
  try {
19038
19218
  const linking = !config.secret;
19039
- if (linking) await link(config);
19219
+ if (linking) {
19220
+ intro2();
19221
+ if (!await link(config, account)) process.exit(1);
19222
+ await keepRunning(() => server.close());
19223
+ }
19040
19224
  connector = new Connector(config);
19041
19225
  await connector.start();
19042
- if (command === "pair" && !linking)
19043
- pairPhone(connector.invite()).catch((error) => process.stderr.write(`${error instanceof Error ? error.message : "Pairing failed"}
19044
- `));
19226
+ if (command === "pair" && !linking) {
19227
+ if (interactive) intro2();
19228
+ const paired = await pairPhone(connector.invite()).catch((error) => {
19229
+ if (!reported(error))
19230
+ process.stderr.write(`${error instanceof Error ? error.message : "Pairing failed"}
19231
+ `);
19232
+ });
19233
+ if (paired)
19234
+ await keepRunning(() => {
19235
+ connector?.stop();
19236
+ server.close();
19237
+ });
19238
+ }
19045
19239
  } catch (error) {
19046
19240
  connector?.stop();
19047
19241
  server.close();
@@ -19049,7 +19243,8 @@ async function main() {
19049
19243
  }
19050
19244
  }
19051
19245
  main().catch((error) => {
19052
- process.stderr.write(`${error instanceof Error ? error.message : "Dashup failed"}
19246
+ if (!reported(error))
19247
+ process.stderr.write(`${error instanceof Error ? error.message : "Dashup failed"}
19053
19248
  `);
19054
19249
  process.exit(1);
19055
19250
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashup/connector",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dashup": "dist/index.js"
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@agentclientprotocol/sdk": "1.9.0",
19
+ "@clack/prompts": "^1.8.1",
19
20
  "@microsoft/agent-host-protocol": "1.0.0",
20
21
  "qrcode": "^1.5.4",
21
22
  "ws": "^8.21.3",
@@ -28,11 +29,11 @@
28
29
  "tsup": "^8.5.1",
29
30
  "tsx": "^4.22.4",
30
31
  "typescript": "^5.9.3",
31
- "@dashup/models": "0.1.0",
32
32
  "@dashup/typescript-config": "0.0.0",
33
+ "@parcae/lynx": "0.1.0",
34
+ "@dashup/models": "0.1.0",
33
35
  "@dashup/protocol": "0.1.0",
34
- "@parcae/sdk": "0.8.1",
35
- "@parcae/lynx": "0.1.0"
36
+ "@parcae/sdk": "0.8.1"
36
37
  },
37
38
  "scripts": {
38
39
  "dashup": "tsx src/index.ts",
@@ -40,7 +41,7 @@
40
41
  "start": "node dist/index.js watch",
41
42
  "build": "tsup",
42
43
  "check-types": "tsc --noEmit",
43
- "test": "node --import tsx --test tests/connector.test.ts tests/history.test.ts tests/harness-history.test.ts tests/harness-live.test.ts tests/harness-queue.test.ts tests/harness-vscode.test.ts tests/stop.test.ts tests/viewer.test.ts",
44
+ "test": "node --import tsx --test tests/connector.test.ts tests/history.test.ts tests/harness-history.test.ts tests/harness-live.test.ts tests/harness-queue.test.ts tests/harness-vscode.test.ts tests/install.test.ts tests/stop.test.ts tests/viewer.test.ts",
44
45
  "test:workflow": "node --import tsx tests/workflow.test.ts"
45
46
  }
46
47
  }