@openagents-org/agent-launcher 0.2.164 → 0.2.166
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/package.json +1 -1
- package/src/cli.js +15 -7
- package/src/daemon.js +97 -26
- package/src/index.js +4 -0
- package/src/node-config.js +69 -0
- package/src/win-console.js +85 -0
- package/src/workspace-client.js +4 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Before ./index so the daemon and every `agn` subprocess inherit the same
|
|
4
|
+
// no-stray-console-window default on Windows. See win-console.js.
|
|
5
|
+
require('./win-console').installWindowsHideDefault();
|
|
6
|
+
|
|
3
7
|
const { AgentConnector, Daemon } = require('./index');
|
|
4
8
|
const { hasCredentialMetadata, formatAuthGuidance } = require('./auth-guidance');
|
|
5
9
|
|
|
@@ -436,11 +440,13 @@ async function cmdNode(connector, flags, positional) {
|
|
|
436
440
|
print('Connecting this device to your workspace...');
|
|
437
441
|
try {
|
|
438
442
|
const res = await connector.redeemNodePairingCode(code, info);
|
|
439
|
-
|
|
443
|
+
// recordPairing, not saveNode: this device can be a node in several
|
|
444
|
+
// workspaces at once, and a bare save would drop the others' pairings.
|
|
445
|
+
nodeCfg.recordPairing(info.nodeKey, {
|
|
440
446
|
node_id: res.nodeId,
|
|
441
|
-
node_key: info.nodeKey,
|
|
442
447
|
workspace_id: res.workspaceId,
|
|
443
448
|
workspace_slug: res.workspaceSlug,
|
|
449
|
+
workspace_name: res.workspaceName,
|
|
444
450
|
endpoint: connector.workspace.endpoint,
|
|
445
451
|
token: res.token,
|
|
446
452
|
});
|
|
@@ -480,12 +486,14 @@ async function cmdNode(connector, flags, positional) {
|
|
|
480
486
|
}
|
|
481
487
|
|
|
482
488
|
if (sub === 'status') {
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
485
|
-
print(`Node connected: ${n.node_id}`);
|
|
486
|
-
print(` Workspace: ${n.workspace_slug || n.workspace_id}`);
|
|
487
|
-
} else {
|
|
489
|
+
const pairings = nodeCfg.listPairings().filter((p) => p.node_id);
|
|
490
|
+
if (!pairings.length) {
|
|
488
491
|
print('No node connected. Run: agn node connect <pairing-code>');
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
print(`Node connected to ${pairings.length} workspace(s):`);
|
|
495
|
+
for (const p of pairings) {
|
|
496
|
+
print(` ${p.workspace_slug || p.workspace_id} — node ${p.node_id}`);
|
|
489
497
|
}
|
|
490
498
|
return;
|
|
491
499
|
}
|
package/src/daemon.js
CHANGED
|
@@ -31,7 +31,7 @@ class Daemon {
|
|
|
31
31
|
this._statusInterval = null;
|
|
32
32
|
this._cmdInterval = null;
|
|
33
33
|
this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
|
|
34
|
-
this.
|
|
34
|
+
this._nodeClients = new Map(); // workspace_id -> WorkspaceClient, one per pairing
|
|
35
35
|
this._runningCommands = new Set(); // node-command ids currently executing
|
|
36
36
|
this._runtimes = []; // detected agent runtimes for the node view
|
|
37
37
|
this._runtimesInterval = null;
|
|
@@ -39,34 +39,78 @@ class Daemon {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Heartbeat this device (node) to
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Heartbeat this device (node) to EVERY workspace it is paired to,
|
|
43
|
+
* independent of any agent, so each workspace's "connected devices" view
|
|
44
|
+
* stays live even with zero agents there.
|
|
45
|
+
*
|
|
46
|
+
* One device legitimately belongs to several workspaces at once — the node
|
|
47
|
+
* row is per (workspace, device) — so this iterates the pairing list rather
|
|
48
|
+
* than the single top-level record. Each pairing gets its own roster, scoped
|
|
49
|
+
* to that workspace's agents. Best-effort: node liveness is non-critical to
|
|
50
|
+
* agent operation, and one workspace being unreachable must not stop the
|
|
51
|
+
* others from reporting, so the pairings are heartbeated concurrently and
|
|
52
|
+
* failures are contained per pairing.
|
|
45
53
|
*/
|
|
46
54
|
async _nodeHeartbeat() {
|
|
55
|
+
const nodeCfg = require('./node-config');
|
|
56
|
+
const pairings = nodeCfg.listPairings().filter((p) => p && p.node_id && p.token);
|
|
57
|
+
if (!pairings.length) return; // no node connected
|
|
58
|
+
const info = { ...nodeCfg.gatherDeviceInfo(), runtimes: this._runtimes, fs: this._buildFs() };
|
|
59
|
+
await Promise.all(pairings.map((p) => this._nodeHeartbeatOne(nodeCfg, p, info)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The WorkspaceClient for one pairing, created on first use and reused. */
|
|
63
|
+
_nodeClientFor(n) {
|
|
64
|
+
let client = this._nodeClients.get(n.workspace_id);
|
|
65
|
+
if (!client) {
|
|
66
|
+
client = new WorkspaceClient(n.endpoint);
|
|
67
|
+
this._nodeClients.set(n.workspace_id, client);
|
|
68
|
+
}
|
|
69
|
+
return client;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One pairing's heartbeat. Never throws — see _nodeHeartbeat. */
|
|
73
|
+
async _nodeHeartbeatOne(nodeCfg, n, info) {
|
|
74
|
+
let resp;
|
|
47
75
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
resp = await this._nodeClientFor(n).nodeHeartbeat(n.node_id, n.token, { ...info, agents: this._buildRoster(n) });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// A 404 is the workspace's definitive word that this node (or that whole
|
|
79
|
+
// workspace) no longer exists — an owner unpaired the device. Nothing
|
|
80
|
+
// local can revive it, so forget THAT pairing: keep the device key for a
|
|
81
|
+
// future re-pair, and leave every other workspace's pairing alone.
|
|
82
|
+
// Without this the launcher kept showing "connected" long after a remote
|
|
83
|
+
// removal, since node.json was only ever reconciled by the UI on demand.
|
|
84
|
+
// Transient failures (timeouts, 5xx, auth blips) say nothing about the
|
|
85
|
+
// row's existence, so those are swallowed and retried next tick.
|
|
86
|
+
if (err && err.status === 404) {
|
|
87
|
+
try { nodeCfg.clearPairing(n.workspace_id); } catch {}
|
|
88
|
+
this._nodeClients.delete(n.workspace_id);
|
|
89
|
+
this._log(`node ${n.node_id} no longer recognized by workspace ${n.workspace_slug || n.workspace_id} — pairing cleared`);
|
|
53
90
|
}
|
|
54
|
-
|
|
55
|
-
const resp = await this._nodeClient.nodeHeartbeat(n.node_id, n.token, info);
|
|
56
|
-
// The heartbeat response is our push channel: run any queued remote
|
|
57
|
-
// agent-management commands the workspace enqueued for this node. Fire
|
|
58
|
-
// them off without blocking the heartbeat loop (an install can take
|
|
59
|
-
// minutes — awaiting here would stall liveness and stack up commands).
|
|
60
|
-
const commands = (resp && resp.commands) || [];
|
|
61
|
-
for (const cmd of commands) {
|
|
62
|
-
const id = cmd.commandId;
|
|
63
|
-
if (!id || this._runningCommands.has(id)) continue;
|
|
64
|
-
this._runningCommands.add(id);
|
|
65
|
-
this._runNodeCommand(n, cmd).finally(() => this._runningCommands.delete(id));
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
// ignore — will retry next interval
|
|
91
|
+
return; // nothing more to do for this pairing this tick
|
|
69
92
|
}
|
|
93
|
+
// The heartbeat response is our push channel: run any queued remote
|
|
94
|
+
// agent-management commands the workspace enqueued for this node. Fire
|
|
95
|
+
// them off without blocking the heartbeat loop (an install can take
|
|
96
|
+
// minutes — awaiting here would stall liveness and stack up commands).
|
|
97
|
+
const commands = (resp && resp.commands) || [];
|
|
98
|
+
for (const cmd of commands) {
|
|
99
|
+
const id = cmd.commandId;
|
|
100
|
+
if (!id || this._runningCommands.has(id)) continue;
|
|
101
|
+
this._runningCommands.add(id);
|
|
102
|
+
this._runNodeCommand(n, cmd).finally(() => this._runningCommands.delete(id));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* True if agent `a` is bound to the node's currently-connected workspace.
|
|
108
|
+
* A missing network never matches (guards against undefined === undefined
|
|
109
|
+
* leaking local-only agents when a node field is also unset).
|
|
110
|
+
*/
|
|
111
|
+
_agentOnNodeWorkspace(a, node) {
|
|
112
|
+
if (!a || !a.network || !node) return false;
|
|
113
|
+
return a.network === node.workspace_slug || a.network === node.workspace_id;
|
|
70
114
|
}
|
|
71
115
|
|
|
72
116
|
/**
|
|
@@ -74,11 +118,18 @@ class Daemon {
|
|
|
74
118
|
* from config (the source of truth for what's configured) and augmented with
|
|
75
119
|
* live process state, so a removed agent drops off immediately rather than
|
|
76
120
|
* lingering as a stale 'stopped' process entry.
|
|
121
|
+
*
|
|
122
|
+
* SECURITY: scoped to the node's currently-connected workspace. An agent
|
|
123
|
+
* connected to a DIFFERENT workspace (or a local-only agent with no network)
|
|
124
|
+
* must never leak into — or be controllable from — a workspace it was never
|
|
125
|
+
* added to. Each agent belongs to exactly one workspace; this node reports
|
|
126
|
+
* only the agents that belong to the one it's paired with right now.
|
|
77
127
|
*/
|
|
78
|
-
_buildRoster() {
|
|
128
|
+
_buildRoster(node) {
|
|
79
129
|
const roster = [];
|
|
80
130
|
try {
|
|
81
131
|
for (const a of this.config.getAgents()) {
|
|
132
|
+
if (!this._agentOnNodeWorkspace(a, node)) continue;
|
|
82
133
|
const proc = this._processes[a.name];
|
|
83
134
|
// Model: per-agent env override, else the type-level saved model. Working
|
|
84
135
|
// dir: the agent's configured path. Both power the workspace agent cards.
|
|
@@ -172,7 +223,18 @@ class Daemon {
|
|
|
172
223
|
let ok = false;
|
|
173
224
|
let message = '';
|
|
174
225
|
let data = null;
|
|
226
|
+
// SECURITY: a workspace may only act on agents bound to it. Reject commands
|
|
227
|
+
// that target an existing agent connected to a different workspace, so a
|
|
228
|
+
// newly-paired workspace can't start/stop/remove/reconfigure agents that
|
|
229
|
+
// belong to another one (they surface in no roster of ours either).
|
|
230
|
+
const AGENT_SCOPED = new Set(['start_agent', 'stop_agent', 'remove_agent', 'configure_agent']);
|
|
175
231
|
try {
|
|
232
|
+
if (AGENT_SCOPED.has(action)) {
|
|
233
|
+
const existing = this.config.getAgent(name);
|
|
234
|
+
if (!existing || !this._agentOnNodeWorkspace(existing, n)) {
|
|
235
|
+
throw new Error(`Agent '${name}' is not managed by this workspace`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
176
238
|
if (action === 'create_agent') {
|
|
177
239
|
const type = (args.type || '').trim();
|
|
178
240
|
// Working directory: use the one the user picked, else a managed folder
|
|
@@ -245,7 +307,9 @@ class Daemon {
|
|
|
245
307
|
message = e.message || String(e);
|
|
246
308
|
}
|
|
247
309
|
try {
|
|
248
|
-
|
|
310
|
+
// Report back to the workspace that issued the command — with several
|
|
311
|
+
// pairings live, the result must not go out over another one's client.
|
|
312
|
+
await this._nodeClientFor(n).nodeCommandResult(cmd.commandId, n.token, { ok, message, data });
|
|
249
313
|
} catch {
|
|
250
314
|
// best-effort; the command stays 'running' if we can't report back
|
|
251
315
|
}
|
|
@@ -265,6 +329,10 @@ class Daemon {
|
|
|
265
329
|
// Run from home so a created agent's default working dir is sensible
|
|
266
330
|
// (not the ~/.openagents config dir).
|
|
267
331
|
cwd: os.homedir(),
|
|
332
|
+
// The daemon has no console of its own (it is spawned DETACHED), so a
|
|
333
|
+
// child without this gets a fresh console window — the blank black box
|
|
334
|
+
// that used to appear every time _refreshRuntimes() ran.
|
|
335
|
+
windowsHide: true,
|
|
268
336
|
});
|
|
269
337
|
let stdout = '';
|
|
270
338
|
let stderr = '';
|
|
@@ -921,6 +989,9 @@ class Daemon {
|
|
|
921
989
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
922
990
|
env: getEnhancedEnv(opts.env),
|
|
923
991
|
cwd: opts.cwd,
|
|
992
|
+
// Windows: cmd.exe would otherwise open a console window per agent (the
|
|
993
|
+
// daemon has none to inherit) and leave it up for the agent's lifetime.
|
|
994
|
+
windowsHide: true,
|
|
924
995
|
};
|
|
925
996
|
|
|
926
997
|
if (IS_WINDOWS) {
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Must run before anything captures a child_process reference: on Windows it
|
|
4
|
+
// keeps every spawn from popping a stray console window. See win-console.js.
|
|
5
|
+
require('./win-console').installWindowsHideDefault();
|
|
6
|
+
|
|
3
7
|
const { Config } = require('./config');
|
|
4
8
|
const { EnvManager } = require('./env');
|
|
5
9
|
const { Registry } = require('./registry');
|
package/src/node-config.js
CHANGED
|
@@ -40,6 +40,72 @@ function getOrCreateNodeKey() {
|
|
|
40
40
|
return key;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Every workspace this device is paired to, most recent first.
|
|
45
|
+
*
|
|
46
|
+
* All of them are live: the server-side node row is keyed per (workspace,
|
|
47
|
+
* node_key), so one device holds a membership in each workspace it paired with
|
|
48
|
+
* and the daemon heartbeats every one. Records written before `pairings`
|
|
49
|
+
* existed carry a single top-level pairing, which is reconstructed here.
|
|
50
|
+
*/
|
|
51
|
+
function listPairings() {
|
|
52
|
+
const record = loadNode();
|
|
53
|
+
if (!record) return [];
|
|
54
|
+
if (record.pairings && record.pairings.length) return record.pairings;
|
|
55
|
+
return record.workspace_id ? [record] : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add a pairing, or refresh it if this workspace is already paired.
|
|
60
|
+
*
|
|
61
|
+
* Additive on purpose: pairing a second workspace must NOT take the device away
|
|
62
|
+
* from the ones it already belongs to (each issues its own node row). The newest
|
|
63
|
+
* pairing is mirrored at the top level, which is what a daemon predating
|
|
64
|
+
* multi-pairing reads. Mirrors the launcher's writer (`node-pairing.ts`).
|
|
65
|
+
*/
|
|
66
|
+
function recordPairing(nodeKey, pairing) {
|
|
67
|
+
const stamped = { ...pairing, paired_at: new Date().toISOString() };
|
|
68
|
+
const pairings = [
|
|
69
|
+
stamped,
|
|
70
|
+
...listPairings().filter((p) => p.workspace_id !== pairing.workspace_id),
|
|
71
|
+
];
|
|
72
|
+
saveNode({ node_key: nodeKey, ...stamped, pairings });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Forget one workspace's pairing — that workspace no longer recognizes this node.
|
|
77
|
+
*
|
|
78
|
+
* Keeps `node_key` (so a re-pair reuses the same device id) and drops just the
|
|
79
|
+
* one entry: the server-side node row for THAT workspace is gone and can only
|
|
80
|
+
* be replaced by a fresh code, while every other pairing is still good. The top
|
|
81
|
+
* level is re-pointed at whichever pairing is now first, since that is what a
|
|
82
|
+
* daemon predating multi-pairing reads. Mirrors the launcher's writer
|
|
83
|
+
* (`node-pairing.ts`) so the two stay schema-compatible.
|
|
84
|
+
*
|
|
85
|
+
* @param workspaceId which pairing to drop; defaults to the top-level one.
|
|
86
|
+
* @returns the pairing that was dropped, or null if there was no such pairing.
|
|
87
|
+
*/
|
|
88
|
+
function clearPairing(workspaceId) {
|
|
89
|
+
const record = loadNode();
|
|
90
|
+
if (!record) return null;
|
|
91
|
+
const target = workspaceId || record.workspace_id;
|
|
92
|
+
if (!target) return null;
|
|
93
|
+
|
|
94
|
+
const pairings = listPairings();
|
|
95
|
+
const dropped = pairings.find((p) => p.workspace_id === target);
|
|
96
|
+
if (!dropped) return null;
|
|
97
|
+
|
|
98
|
+
const remaining = pairings.filter((p) => p.workspace_id !== target);
|
|
99
|
+
saveNode({ node_key: record.node_key, ...remaining[0], pairings: remaining });
|
|
100
|
+
return {
|
|
101
|
+
node_id: dropped.node_id,
|
|
102
|
+
workspace_id: dropped.workspace_id,
|
|
103
|
+
workspace_slug: dropped.workspace_slug,
|
|
104
|
+
workspace_name: dropped.workspace_name,
|
|
105
|
+
endpoint: dropped.endpoint,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
43
109
|
/** Best-effort device type from the platform (user/UI can override). */
|
|
44
110
|
function inferDeviceType() {
|
|
45
111
|
const p = os.platform();
|
|
@@ -64,6 +130,9 @@ function gatherDeviceInfo() {
|
|
|
64
130
|
module.exports = {
|
|
65
131
|
loadNode,
|
|
66
132
|
saveNode,
|
|
133
|
+
listPairings,
|
|
134
|
+
recordPairing,
|
|
135
|
+
clearPairing,
|
|
67
136
|
getOrCreateNodeKey,
|
|
68
137
|
inferDeviceType,
|
|
69
138
|
gatherDeviceInfo,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stop Windows from popping a console window for every child process we spawn.
|
|
5
|
+
*
|
|
6
|
+
* Neither process that runs this code owns a console: the daemon is spawned
|
|
7
|
+
* DETACHED (`agn up --foreground`), and the launcher's Electron main process is
|
|
8
|
+
* a GUI app. On Windows, when a console-less process launches a console
|
|
9
|
+
* executable *without* CREATE_NO_WINDOW, the OS allocates a brand-new console
|
|
10
|
+
* for the child — an empty black cmd/Windows-Terminal window that pops to the
|
|
11
|
+
* foreground and steals focus. With stdio piped nothing is ever drawn in it,
|
|
12
|
+
* so what the user sees is a blank window appearing out of nowhere.
|
|
13
|
+
*
|
|
14
|
+
* That is the "命令框 keeps popping up" report: `_refreshRuntimes()` shells out
|
|
15
|
+
* to `agn runtimes --json` every two minutes, and the probe it runs keeps the
|
|
16
|
+
* window on screen for as long as it takes to check every installed CLI.
|
|
17
|
+
*
|
|
18
|
+
* Node exposes CREATE_NO_WINDOW as the `windowsHide` spawn option and most call
|
|
19
|
+
* sites pass it — but there are ~60 of them across the adapters and a single
|
|
20
|
+
* miss brings the window back. So the default lives here, applied once at the
|
|
21
|
+
* process entry points (src/index.js, src/cli.js) before any module has
|
|
22
|
+
* captured a reference to child_process.
|
|
23
|
+
*
|
|
24
|
+
* Two things this deliberately does NOT do:
|
|
25
|
+
* - override an explicit `windowsHide: false` — the launcher opens a real
|
|
26
|
+
* terminal for CLI sign-in that way, and that window is the point; and
|
|
27
|
+
* - break interactive output: libuv only applies CREATE_NO_WINDOW when no
|
|
28
|
+
* stdio handle is inherited, so `agn` run from a terminal (stdio 'inherit')
|
|
29
|
+
* is unaffected by this and still prints where the user can see it.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const childProcess = require('child_process');
|
|
33
|
+
|
|
34
|
+
const PATCHED = Symbol.for('openagents.windowsHideDefault');
|
|
35
|
+
const WRAPPED = ['spawn', 'spawnSync', 'exec', 'execSync', 'execFile', 'execFileSync'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return a copy of `args` whose options object carries `windowsHide: true`.
|
|
39
|
+
*
|
|
40
|
+
* Every child_process signature ends with `[options][, callback]`, so the
|
|
41
|
+
* options object — if there is one — is the last non-function argument. The
|
|
42
|
+
* caller's object is copied rather than mutated: callers reuse option objects
|
|
43
|
+
* and must not see ours leak into their state.
|
|
44
|
+
*/
|
|
45
|
+
function withWindowsHide(args) {
|
|
46
|
+
const out = args.slice();
|
|
47
|
+
const cbIdx = typeof out[out.length - 1] === 'function' ? out.length - 1 : -1;
|
|
48
|
+
const optIdx = cbIdx === -1 ? out.length - 1 : cbIdx - 1;
|
|
49
|
+
const opts = optIdx >= 0 ? out[optIdx] : undefined;
|
|
50
|
+
|
|
51
|
+
if (opts && typeof opts === 'object' && !Array.isArray(opts)) {
|
|
52
|
+
if ('windowsHide' in opts) return args; // explicit wins, either way
|
|
53
|
+
out[optIdx] = { ...opts, windowsHide: true };
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
// No options argument at all — insert one ahead of any callback.
|
|
57
|
+
out.splice(cbIdx === -1 ? out.length : cbIdx, 0, { windowsHide: true });
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Default `windowsHide: true` for this process's child_process calls.
|
|
63
|
+
* No-op off Windows, and idempotent.
|
|
64
|
+
*/
|
|
65
|
+
function installWindowsHideDefault() {
|
|
66
|
+
if (process.platform !== 'win32') return;
|
|
67
|
+
if (childProcess[PATCHED]) return;
|
|
68
|
+
|
|
69
|
+
for (const name of WRAPPED) {
|
|
70
|
+
const original = childProcess[name];
|
|
71
|
+
if (typeof original !== 'function') continue;
|
|
72
|
+
const wrapper = function (...args) {
|
|
73
|
+
return original.apply(this, withWindowsHide(args));
|
|
74
|
+
};
|
|
75
|
+
// Keep util.promisify(exec) and friends working.
|
|
76
|
+
for (const sym of Object.getOwnPropertySymbols(original)) {
|
|
77
|
+
wrapper[sym] = original[sym];
|
|
78
|
+
}
|
|
79
|
+
Object.defineProperty(wrapper, 'name', { value: name });
|
|
80
|
+
childProcess[name] = wrapper;
|
|
81
|
+
}
|
|
82
|
+
childProcess[PATCHED] = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { installWindowsHideDefault, withWindowsHide };
|
package/src/workspace-client.js
CHANGED
|
@@ -969,6 +969,10 @@ class WorkspaceClient {
|
|
|
969
969
|
// Preserve structured error details (error_code, hint,
|
|
970
970
|
// quota occupancy, ...) so tool handlers can surface them.
|
|
971
971
|
if (parsed.data && typeof parsed.data === 'object') err.data = parsed.data;
|
|
972
|
+
// The HTTP status lets callers tell a definitive rejection
|
|
973
|
+
// (e.g. 404 "not found") from a transient one (5xx/timeout);
|
|
974
|
+
// the node heartbeat uses it to clear a removed pairing.
|
|
975
|
+
err.status = res.statusCode;
|
|
972
976
|
reject(err);
|
|
973
977
|
}
|
|
974
978
|
} else {
|