@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.
- package/README.md +14 -29
- package/dist/index.js +358 -163
- 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
|
-
|
|
9
|
-
dashup pair
|
|
8
|
+
npx @dashup/connector pair
|
|
10
9
|
```
|
|
11
10
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
59
|
+
dashup acp -- /absolute/path/to/original-agent ORIGINAL_ARGUMENTS
|
|
68
60
|
```
|
|
69
61
|
|
|
70
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4056
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
4071
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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 ||
|
|
6029
|
+
--total || pause2();
|
|
5971
6030
|
});
|
|
5972
6031
|
}
|
|
5973
6032
|
} else {
|
|
5974
|
-
|
|
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
|
|
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(
|
|
15045
|
-
else refOps.set(segments[0], [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15766
|
+
log2.debug(`\u2190 ${method.toUpperCase()} ${fullPath} (${ms}ms)`);
|
|
15708
15767
|
resolve3(parsed.result);
|
|
15709
15768
|
} else {
|
|
15710
|
-
|
|
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
|
-
|
|
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
|
|
16737
|
+
import { promisify as promisify3 } from "node:util";
|
|
16685
16738
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
16686
|
-
import { execFile as
|
|
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 =
|
|
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
|
|
17234
|
+
import { promisify as promisify4 } from "node:util";
|
|
17182
17235
|
import { createHash as createHash4 } from "node:crypto";
|
|
17183
|
-
import { execFile as
|
|
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 =
|
|
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
|
|
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
|
-
//
|
|
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
|
|
18366
|
-
const
|
|
18367
|
-
if (this.
|
|
18368
|
-
await this.send(pair,
|
|
18369
|
-
this.listed.set(pair.id,
|
|
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
|
-
|
|
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 {
|
|
18749
|
-
import {
|
|
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 {
|
|
18752
|
-
import {
|
|
18753
|
-
import {
|
|
18754
|
-
var run4 =
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
|
|
18765
|
-
|
|
18766
|
-
|
|
18767
|
-
|
|
18768
|
-
|
|
18769
|
-
|
|
18770
|
-
|
|
18771
|
-
|
|
18772
|
-
|
|
18773
|
-
|
|
18774
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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 =
|
|
18942
|
-
|
|
18943
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|