@ravefox/space 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -203
- package/README.md +89 -26
- package/bin/space.js +106 -0
- package/lib/agents.js +119 -0
- package/lib/credentials.js +95 -0
- package/lib/daemon.js +294 -0
- package/lib/harness.js +166 -0
- package/lib/http.js +101 -0
- package/lib/login.js +68 -0
- package/lib/mcp.js +34 -0
- package/lib/runtime.js +241 -0
- package/lib/store.js +96 -0
- package/lib/tools.js +93 -0
- package/package.json +28 -40
- package/NOTICE +0 -44
- package/assets/webhook-sdk/python/bot.py +0 -63
- package/assets/webhook-sdk/python/commonly.py +0 -232
- package/node_modules/@commonlyai/seats/README.md +0 -25
- package/node_modules/@commonlyai/seats/package.json +0 -22
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
- package/node_modules/@commonlyai/seats/src/contract.js +0 -48
- package/node_modules/@commonlyai/seats/src/failures.js +0 -44
- package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
- package/node_modules/@commonlyai/seats/src/index.js +0 -22
- package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
- package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
- package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
- package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
- package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
- package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
- package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
- package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
- package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
- package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
- package/skills/commonly/SKILL.md +0 -233
- package/src/commands/agent.js +0 -6334
- package/src/commands/daemon.js +0 -933
- package/src/commands/dev.js +0 -153
- package/src/commands/doctor.js +0 -141
- package/src/commands/inbox.js +0 -330
- package/src/commands/login.js +0 -194
- package/src/commands/mcp.js +0 -56
- package/src/commands/pod.js +0 -120
- package/src/commands/task.js +0 -279
- package/src/commands/verify.js +0 -48
- package/src/index.js +0 -99
- package/src/lib/adapter-login.js +0 -76
- package/src/lib/adapters/broker-bridge-entry.js +0 -76
- package/src/lib/adapters/claude.js +0 -2207
- package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
- package/src/lib/adapters/codex.js +0 -1898
- package/src/lib/adapters/dsh.js +0 -513
- package/src/lib/adapters/goose.js +0 -510
- package/src/lib/adapters/hermes.js +0 -18
- package/src/lib/adapters/index.js +0 -68
- package/src/lib/adapters/opencode.js +0 -1681
- package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
- package/src/lib/adapters/pi-mcp-client.mjs +0 -617
- package/src/lib/adapters/pi-shell-env.mjs +0 -41
- package/src/lib/adapters/pi.js +0 -1025
- package/src/lib/adapters/qwen.js +0 -523
- package/src/lib/adapters/seat-model.js +0 -8
- package/src/lib/adapters/stub.js +0 -1
- package/src/lib/adapters/tested-version.js +0 -106
- package/src/lib/api.js +0 -205
- package/src/lib/claim-outcome.js +0 -128
- package/src/lib/claimed-work-gate.js +0 -73
- package/src/lib/cli-hint.js +0 -33
- package/src/lib/commit-attribution.js +0 -391
- package/src/lib/config.js +0 -155
- package/src/lib/content-refusal.js +0 -94
- package/src/lib/credential-file.js +0 -208
- package/src/lib/daemon-agent-states.js +0 -95
- package/src/lib/daemon-hint.js +0 -18
- package/src/lib/daemon-logs.js +0 -43
- package/src/lib/daemon-service-windows.js +0 -263
- package/src/lib/daemon-service.js +0 -486
- package/src/lib/daemon-state.js +0 -84
- package/src/lib/daemon-store.js +0 -119
- package/src/lib/daemon-supervisor.js +0 -629
- package/src/lib/daemon-watch.js +0 -115
- package/src/lib/declared-mcp-guard.js +0 -284
- package/src/lib/default-environment.js +0 -207
- package/src/lib/device-login.js +0 -126
- package/src/lib/digest-frame.js +0 -151
- package/src/lib/doctor.js +0 -291
- package/src/lib/egress-proxy.js +0 -160
- package/src/lib/enforcement.js +0 -946
- package/src/lib/environment.js +0 -681
- package/src/lib/first-output-watchdog.js +0 -191
- package/src/lib/git-readonly-server.js +0 -138
- package/src/lib/git-readonly.js +0 -463
- package/src/lib/git-remote-commonly.mjs +0 -100
- package/src/lib/grant-broker-guard.js +0 -345
- package/src/lib/hooks-config.js +0 -238
- package/src/lib/host-gh-broker.js +0 -152
- package/src/lib/host-gh-shim.mjs +0 -98
- package/src/lib/inbox.js +0 -483
- package/src/lib/instance-proxy.js +0 -76
- package/src/lib/machine-host.js +0 -29
- package/src/lib/mcp-credential-delivery.js +0 -121
- package/src/lib/mcp-first-run.js +0 -50
- package/src/lib/mcp-home.js +0 -644
- package/src/lib/mcp-ready.js +0 -68
- package/src/lib/mcp-server-version.js +0 -123
- package/src/lib/mcp-token.js +0 -36
- package/src/lib/mcp-warm-child.mjs +0 -412
- package/src/lib/memory-bridge.js +0 -191
- package/src/lib/memory-import.js +0 -156
- package/src/lib/missing-harness.js +0 -31
- package/src/lib/model-unavailable.js +0 -154
- package/src/lib/native-runtime-inventory.js +0 -40
- package/src/lib/outsider-grant-mcp.js +0 -82
- package/src/lib/own-key-launch.js +0 -42
- package/src/lib/pod-catch-up.js +0 -55
- package/src/lib/pod-focus.js +0 -224
- package/src/lib/pod-tail.js +0 -68
- package/src/lib/poll-liveness.js +0 -103
- package/src/lib/poll-retry.js +0 -81
- package/src/lib/poller.js +0 -200
- package/src/lib/provider-proxy.js +0 -42
- package/src/lib/public-seat-mcp-flag.js +0 -36
- package/src/lib/public-tool-error.js +0 -60
- package/src/lib/public-withheld-tools.js +0 -48
- package/src/lib/reply-receipt.js +0 -55
- package/src/lib/resolve-on-path.js +0 -22
- package/src/lib/runtime-inventory.js +0 -56
- package/src/lib/runtime-observation.js +0 -37
- package/src/lib/sandbox/bwrap.js +0 -275
- package/src/lib/sandbox/codex-perimeter.js +0 -16
- package/src/lib/sandbox/host-check.js +0 -268
- package/src/lib/sandbox/mode.js +0 -49
- package/src/lib/sandbox/opencode-loopback.mjs +0 -68
- package/src/lib/sandbox/seatbelt.js +0 -684
- package/src/lib/sandbox/turn-dir.js +0 -56
- package/src/lib/sandbox/windows-unsandboxed.js +0 -183
- package/src/lib/sandbox/workspace-git-config.js +0 -243
- package/src/lib/sandbox/workspace-pin.js +0 -158
- package/src/lib/seat-runtime-controls.js +0 -61
- package/src/lib/seat-stop.js +0 -97
- package/src/lib/server-skills.js +0 -32
- package/src/lib/session-recycle.js +0 -219
- package/src/lib/session-store.js +0 -215
- package/src/lib/skill-prompt.js +0 -74
- package/src/lib/skills-import.js +0 -113
- package/src/lib/spawn-credential.js +0 -402
- package/src/lib/spawn-retry.js +0 -311
- package/src/lib/stable-install.js +0 -176
- package/src/lib/supervised-turn.js +0 -352
- package/src/lib/task.js +0 -235
- package/src/lib/turn-failure-notice.js +0 -177
- package/src/lib/unconfined-seat-guard.js +0 -94
- package/src/lib/update-check.js +0 -192
- package/src/lib/upstream-refusal.js +0 -1
- package/src/lib/webhook-server.js +0 -109
- package/src/lib/windows-command.js +0 -393
- package/src/lib/workspace-git.js +0 -87
- package/src/lib/workspace-trust.js +0 -162
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A per-spawn credential file: the launcher channel for the runtime token
|
|
3
|
-
* (TASK-082/083, ruled 2026-09-19 — "a credential file, only its PATH in the
|
|
4
|
-
* env").
|
|
5
|
-
*
|
|
6
|
-
* WHY A FILE AND NOT THE ENVIRONMENT. The token used to ride the runtime's own
|
|
7
|
-
* environment (claude expands `${COMMONLY_AGENT_TOKEN}` from it, codex forwards
|
|
8
|
-
* it through `mcp_servers.*.env_vars`), which means every child of the runtime
|
|
9
|
-
* inherited it — measured on 2026-09-19: a `@playwright/mcp` process held the
|
|
10
|
-
* seat's `COMMONLY_AGENT_TOKEN`, and a pi seat's MCP child held the daemon's
|
|
11
|
-
* `COMMONLY_LITELLM_KEY`. A PATH is not a secret, so handing the path to the
|
|
12
|
-
* runtime is safe even though the token it names is not, and an unrelated MCP
|
|
13
|
-
* server that inherits the runtime's environment gets nothing it can use.
|
|
14
|
-
*
|
|
15
|
-
* WHY PER SPAWN. The file lives only as long as one spawn of one runtime, in a
|
|
16
|
-
* directory only its owner can traverse (0700), with the file itself 0600. A
|
|
17
|
-
* fixed path would widen the window to "since the first spawn" and would let two
|
|
18
|
-
* concurrent seats share one credential by accident.
|
|
19
|
-
*
|
|
20
|
-
* The caller names the root; this module never invents one, because the only
|
|
21
|
-
* root it could invent is a directory no adapter cleans up.
|
|
22
|
-
*
|
|
23
|
-
* The reader side is `readToken` in `commonly-mcp/src/client.js`, which resolves
|
|
24
|
-
* fd, then file, then environment — by declaration, and refuses to fall through
|
|
25
|
-
* from a declared source that cannot be read.
|
|
26
|
-
*/
|
|
27
|
-
import {
|
|
28
|
-
chmodSync, existsSync, mkdirSync, rmSync, writeFileSync,
|
|
29
|
-
} from 'node:fs';
|
|
30
|
-
import { dirname, join } from 'node:path';
|
|
31
|
-
import { randomBytes } from 'node:crypto';
|
|
32
|
-
|
|
33
|
-
/** The variable a child reads to find the credential. Never carries the token. */
|
|
34
|
-
export const CREDENTIAL_FILE_VAR = 'COMMONLY_TOKEN_FILE';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The variable that USED to carry the token itself, and still does for a server
|
|
38
|
-
* that cannot read anything else. Kept beside the file var so the two channels
|
|
39
|
-
* are named in one place rather than one per adapter.
|
|
40
|
-
*/
|
|
41
|
-
export const CREDENTIAL_KEY = 'COMMONLY_AGENT_TOKEN';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Write `token` to a fresh 0600 file under `root` and return its path, or null
|
|
45
|
-
* when there is no token to write (a seat bootstrapping without one).
|
|
46
|
-
*
|
|
47
|
-
* `root` is REQUIRED. It used to default to the CLI's own state directory, which
|
|
48
|
-
* meant a caller that forgot it wrote a live credential into
|
|
49
|
-
* `~/.commonly/credentials` — a directory no adapter cleans, because that is not
|
|
50
|
-
* where adapters put theirs. Measured 2026-09-20: 32 such directories on the
|
|
51
|
-
* fleet host, 24 of them written by this repo's own harnesses, all holding a
|
|
52
|
-
* seat credential. A missing root is now an error instead of a silent write into
|
|
53
|
-
* the operator's home. The token check stays first, so "no token, nothing
|
|
54
|
-
* written" remains true without a root.
|
|
55
|
-
*
|
|
56
|
-
* `fs` is injectable so tests can assert the mode and the path shape without
|
|
57
|
-
* writing into the operator's home.
|
|
58
|
-
*/
|
|
59
|
-
export const writeCredentialFile = (token, {
|
|
60
|
-
agentName = 'agent',
|
|
61
|
-
root,
|
|
62
|
-
fs = { mkdirSync, writeFileSync, chmodSync, rmSync },
|
|
63
|
-
now = () => Date.now(),
|
|
64
|
-
random = () => randomBytes(4).toString('hex'),
|
|
65
|
-
} = {}) => {
|
|
66
|
-
const value = typeof token === 'string' ? token.trim() : '';
|
|
67
|
-
if (!value) return null;
|
|
68
|
-
if (typeof root !== 'string' || !root.trim()) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
'writeCredentialFile requires an explicit root: an omitted root used to fall back to '
|
|
71
|
-
+ '~/.commonly/credentials, where nothing sweeps the credential files it writes',
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
const dir = join(root, `${agentName}-${now()}-${random()}`);
|
|
75
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
76
|
-
const path = join(dir, 'token');
|
|
77
|
-
fs.writeFileSync(path, value, { mode: 0o600 });
|
|
78
|
-
// writeFileSync's mode is subject to the process umask, so a 0022 umask still
|
|
79
|
-
// produces 0644. chmod is the assertion, not a courtesy.
|
|
80
|
-
fs.chmodSync(path, 0o600);
|
|
81
|
-
return { path, dir };
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Writes the token again at the path an earlier writeCredentialFile chose, for
|
|
86
|
-
* a retry whose config already names that path (F460: the first run released
|
|
87
|
-
* it once its readers had it).
|
|
88
|
-
*/
|
|
89
|
-
export const restoreCredentialFile = (written, token, {
|
|
90
|
-
fs = {
|
|
91
|
-
mkdirSync, writeFileSync, chmodSync, rmSync,
|
|
92
|
-
},
|
|
93
|
-
} = {}) => {
|
|
94
|
-
const value = typeof token === 'string' ? token.trim() : '';
|
|
95
|
-
if (!written?.path || !written?.dir || !value) return false;
|
|
96
|
-
fs.mkdirSync(written.dir, { recursive: true, mode: 0o700 });
|
|
97
|
-
// Exclusive create: whatever sits at the path is removed, never written through.
|
|
98
|
-
fs.rmSync(written.path, { force: true });
|
|
99
|
-
fs.writeFileSync(written.path, value, { mode: 0o600, flag: 'wx' });
|
|
100
|
-
fs.chmodSync(written.path, 0o600);
|
|
101
|
-
return true;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/** Best-effort removal once the spawn that owns the file has ended. */
|
|
105
|
-
export const removeCredentialFile = (written, { fs = { rmSync } } = {}) => {
|
|
106
|
-
const dir = typeof written === 'string' ? dirname(written) : written?.dir;
|
|
107
|
-
if (!dir) return false;
|
|
108
|
-
try {
|
|
109
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
110
|
-
return true;
|
|
111
|
-
} catch {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/** How long a public seat waits for its MCP servers' ready files (F460). */
|
|
117
|
-
export const READY_DEADLINE_MS = 30_000;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* F460. Delete this spawn's credential as soon as every process that reads it
|
|
121
|
-
* has read it, so nothing the model does later in the turn (a tool, a link the
|
|
122
|
-
* walk missed) can reach it. pi has done this since F146; this is the one
|
|
123
|
-
* shared version.
|
|
124
|
-
*
|
|
125
|
-
* Two triggers, either of which may fire first:
|
|
126
|
-
* - `release('stream')`, called by the adapter on an output event measured to
|
|
127
|
-
* come after every read (pi's first event, OpenCode's step_start);
|
|
128
|
-
* - `readyFiles`: each Commonly MCP server writes its ready file on the host's
|
|
129
|
-
* first `tools/list`, after it read the credential at start, and the broker
|
|
130
|
-
* bridge does the same. Once every listed file exists, the credential goes.
|
|
131
|
-
* With a deadline: a server that never gets ready costs a degraded turn,
|
|
132
|
-
* never a credential left readable for the whole turn.
|
|
133
|
-
*
|
|
134
|
-
* A delete that fails stops the turn: `onFailure(error)` kills the child, and
|
|
135
|
-
* the error (the path, never the value) is the turn's result. `stop()` belongs
|
|
136
|
-
* in the spawn's `finally`, beside `removeCredentialFile`.
|
|
137
|
-
*/
|
|
138
|
-
export const armCredentialRelease = (credential, {
|
|
139
|
-
readyFiles = null,
|
|
140
|
-
// An adapter with a trigger of its own (OpenCode's first step) calls
|
|
141
|
-
// release() itself; an empty list then waits for it, or the deadline.
|
|
142
|
-
ownTrigger = false,
|
|
143
|
-
deadlineMs = READY_DEADLINE_MS,
|
|
144
|
-
pollMs = 100,
|
|
145
|
-
label = 'this seat',
|
|
146
|
-
onFailure = () => {},
|
|
147
|
-
warn = () => {},
|
|
148
|
-
fs = { rmSync, existsSync },
|
|
149
|
-
timers = {
|
|
150
|
-
setTimeout, clearTimeout, setInterval, clearInterval,
|
|
151
|
-
},
|
|
152
|
-
} = {}) => {
|
|
153
|
-
let armed = Boolean(credential?.dir);
|
|
154
|
-
let reason = null;
|
|
155
|
-
let error = null;
|
|
156
|
-
let poll = null;
|
|
157
|
-
let deadline = null;
|
|
158
|
-
const stop = () => {
|
|
159
|
-
if (poll !== null) timers.clearInterval(poll);
|
|
160
|
-
if (deadline !== null) timers.clearTimeout(deadline);
|
|
161
|
-
poll = null;
|
|
162
|
-
deadline = null;
|
|
163
|
-
};
|
|
164
|
-
const release = (why) => {
|
|
165
|
-
if (!armed) return error;
|
|
166
|
-
armed = false;
|
|
167
|
-
reason = why;
|
|
168
|
-
stop();
|
|
169
|
-
try {
|
|
170
|
-
fs.rmSync(credential.dir, { recursive: true, force: true });
|
|
171
|
-
} catch (err) {
|
|
172
|
-
error = new Error(
|
|
173
|
-
`${label} could not delete its runtime credential (${credential.path}) once its readers had it: `
|
|
174
|
-
+ `${err?.code || err?.message || err}; the turn was stopped so the credential does not stay readable to its tools`,
|
|
175
|
-
);
|
|
176
|
-
onFailure(error);
|
|
177
|
-
}
|
|
178
|
-
return error;
|
|
179
|
-
};
|
|
180
|
-
if (armed && Array.isArray(readyFiles)) {
|
|
181
|
-
const waits = ownTrigger && !readyFiles.length;
|
|
182
|
-
const allReady = () => !waits && readyFiles.every((path) => fs.existsSync(path));
|
|
183
|
-
if (allReady()) {
|
|
184
|
-
release('ready');
|
|
185
|
-
} else {
|
|
186
|
-
if (!waits) {
|
|
187
|
-
poll = timers.setInterval(() => { if (allReady()) release('ready'); }, pollMs);
|
|
188
|
-
poll?.unref?.();
|
|
189
|
-
}
|
|
190
|
-
deadline = timers.setTimeout(() => {
|
|
191
|
-
const missing = readyFiles.filter((path) => !fs.existsSync(path));
|
|
192
|
-
warn(
|
|
193
|
-
`${label}: MCP servers not ready after ${Math.round(deadlineMs / 1000)}s `
|
|
194
|
-
+ `(${missing.length ? missing.join(', ') : 'its runtime never reported them started'}); `
|
|
195
|
-
+ 'the runtime credential was deleted anyway, so tools that still need it will fail this turn',
|
|
196
|
-
);
|
|
197
|
-
release('deadline');
|
|
198
|
-
}, deadlineMs);
|
|
199
|
-
deadline?.unref?.();
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return {
|
|
203
|
-
release,
|
|
204
|
-
stop,
|
|
205
|
-
get reason() { return reason; },
|
|
206
|
-
get error() { return error; },
|
|
207
|
-
};
|
|
208
|
-
};
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
// The server's verdict for each agent, as `daemon status --watch` shows it. The
|
|
6
|
-
// verdict is deriveAgentState's (backend/services/agentStateService.ts), the
|
|
7
|
-
// same one the Your Team row and the TASK-655 never-connected line use, so the
|
|
8
|
-
// watch and the product name an agent's state the same way. It is read through
|
|
9
|
-
// the operator's login, because the daemon's own credential cannot call the
|
|
10
|
-
// per-pod agent-states route. Each agent's pod comes from its token record.
|
|
11
|
-
|
|
12
|
-
// The reach states mapped to the words the watch prints. `reachable` is a
|
|
13
|
-
// native or push-webhook agent the server always treats as up.
|
|
14
|
-
const LABEL = {
|
|
15
|
-
listening: 'running',
|
|
16
|
-
reachable: 'running',
|
|
17
|
-
'gone-dark': 'stalled',
|
|
18
|
-
'never-connected': 'not connected',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// When one agent is installed in several pods, the most alive verdict wins.
|
|
22
|
-
const RANK = ['running', 'stalled', 'not connected', 'unknown'];
|
|
23
|
-
|
|
24
|
-
export const reachLabel = (reach) => LABEL[reach] || 'unknown';
|
|
25
|
-
|
|
26
|
-
export const seatKey = (agentName, instanceId) => (
|
|
27
|
-
`${String(agentName || '').toLowerCase()}:${String(instanceId || 'default').toLowerCase()}`
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const moreAlive = (a, b) => (RANK.indexOf(a) <= RANK.indexOf(b) ? a : b);
|
|
31
|
-
|
|
32
|
-
// Reads one pod's agents and returns Map<seatKey, label>. `client` is a
|
|
33
|
-
// user-token client.
|
|
34
|
-
export const loadPodReach = async ({ client, podId }) => {
|
|
35
|
-
const body = await client.get(`/api/pods/${podId}/agent-states`);
|
|
36
|
-
const byAgent = new Map();
|
|
37
|
-
for (const agent of body?.agents || []) {
|
|
38
|
-
byAgent.set(seatKey(agent.agentName, agent.instanceId), reachLabel(agent.state));
|
|
39
|
-
}
|
|
40
|
-
return byAgent;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// The server allows 60 agent-states reads a minute per IP. Each pod is refreshed
|
|
44
|
-
// once per TTL, and the TTL stretches with the pod count, so N pods cost about 50
|
|
45
|
-
// reads a minute whatever N is: 1.2 s per pod at least, and never below `ttlMs`.
|
|
46
|
-
const PER_POD_SPACING_MS = 1_200;
|
|
47
|
-
|
|
48
|
-
// Caches each pod's verdict, so the 2 s watch frame does not re-read every pod.
|
|
49
|
-
// A failed refresh keeps the pod's last verdict and waits one TTL before trying
|
|
50
|
-
// again, so a 429 is neither discarded nor retried every frame. A pod with no
|
|
51
|
-
// earlier verdict throws, and the next call retries it. `load(podId)` returns
|
|
52
|
-
// Map<seatKey, label>. Returns `reachFor(podIds)`, which merges the pods' maps.
|
|
53
|
-
export const createReachCache = ({ load, ttlMs = 10_000, now = Date.now }) => {
|
|
54
|
-
const cached = new Map();
|
|
55
|
-
const reachFor = async (podIds) => {
|
|
56
|
-
const ttl = Math.max(ttlMs, podIds.length * PER_POD_SPACING_MS);
|
|
57
|
-
const merged = new Map();
|
|
58
|
-
for (const podId of podIds) {
|
|
59
|
-
const hit = cached.get(podId);
|
|
60
|
-
let agents;
|
|
61
|
-
if (hit && now() - hit.loadedAt < ttl) {
|
|
62
|
-
agents = hit.agents;
|
|
63
|
-
} else {
|
|
64
|
-
try {
|
|
65
|
-
agents = await load(podId);
|
|
66
|
-
} catch (error) {
|
|
67
|
-
if (!hit) throw error;
|
|
68
|
-
agents = hit.agents;
|
|
69
|
-
}
|
|
70
|
-
cached.set(podId, { agents, loadedAt: now() });
|
|
71
|
-
}
|
|
72
|
-
for (const [key, label] of agents) {
|
|
73
|
-
merged.set(key, merged.has(key) ? moreAlive(merged.get(key), label) : label);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return merged;
|
|
77
|
-
};
|
|
78
|
-
return reachFor;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// The agent's last supervised turn, from its session store, the record
|
|
82
|
-
// `agent list` reads. The most recent entry across pods, or null. The
|
|
83
|
-
// supervisor's own stamp is the spawn time, so the watch does not use it.
|
|
84
|
-
export const readSessionLastTurn = (name, { home = homedir() } = {}) => {
|
|
85
|
-
try {
|
|
86
|
-
const sessions = JSON.parse(readFileSync(join(home, '.commonly', 'sessions', `${name}.json`), 'utf8'));
|
|
87
|
-
let latest = null;
|
|
88
|
-
for (const entry of Object.values(sessions || {})) {
|
|
89
|
-
if (entry?.lastTurn && (!latest || entry.lastTurn > latest)) latest = entry.lastTurn;
|
|
90
|
-
}
|
|
91
|
-
return latest;
|
|
92
|
-
} catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
};
|
package/src/lib/daemon-hint.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// The hint a foreground `commonly agent run` prints for keeping the agent
|
|
2
|
-
// running. The daemon supervises only the agents placed on this computer, so
|
|
3
|
-
// the hint depends on whether a daemon is registered here at all.
|
|
4
|
-
//
|
|
5
|
-
// The two daemon steps depend on each other, so on Windows they print one per
|
|
6
|
-
// line instead of joined with `&&`, which Windows PowerShell 5.1 lacks (F435).
|
|
7
|
-
import { cliName, cliSteps } from './cli-hint.js';
|
|
8
|
-
|
|
9
|
-
export const foregroundDaemonHint = ({ daemonRegistered, platform = process.platform }) => {
|
|
10
|
-
const lead = 'foreground mode; this stops when the terminal closes. To keep it running, ';
|
|
11
|
-
const inWebApp = 'the web app: Agents → Bring your own → On my computer';
|
|
12
|
-
if (daemonRegistered) return `${lead}add the agent to this computer in ${inWebApp}`;
|
|
13
|
-
const cli = cliName(platform);
|
|
14
|
-
const steps = cliSteps([`${cli} daemon register`, `${cli} daemon install`], { platform, indent: ' ' });
|
|
15
|
-
return platform === 'win32'
|
|
16
|
-
? `${lead}add this computer by running these two commands, one after the other:${steps}\n then add the agent to it in ${inWebApp}`
|
|
17
|
-
: `${lead}add this computer with: ${steps}, then add the agent to it in ${inWebApp}`;
|
|
18
|
-
};
|
package/src/lib/daemon-logs.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
|
|
5
|
-
export const daemonLogsDir = (home = homedir()) => join(home, '.commonly', 'logs', 'daemon');
|
|
6
|
-
export const daemonSeatLogPath = (agentName, home = homedir()) => {
|
|
7
|
-
const safeName = String(agentName || 'unknown').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
8
|
-
return join(daemonLogsDir(home), `${safeName}.log`);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const tailLines = (text, count = 50) => {
|
|
12
|
-
const lines = String(text).split(/\r?\n/);
|
|
13
|
-
// A trailing newline is a separator, not an empty log line.
|
|
14
|
-
if (lines.at(-1) === '') lines.pop();
|
|
15
|
-
return lines.slice(-Math.max(0, Number(count) || 0)).join('\n');
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const readLogTail = (path, count = 50) => {
|
|
19
|
-
try {
|
|
20
|
-
return tailLines(readFileSync(path, 'utf8'), count);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
if (error?.code === 'ENOENT') return null;
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const logSize = (path) => statSync(path).size;
|
|
28
|
-
|
|
29
|
-
// Bytes appended since `offset`, for `agent logs --follow`. A file that shrank
|
|
30
|
-
// (rotated or truncated) is read from the start again.
|
|
31
|
-
export const readAppended = (path, offset) => {
|
|
32
|
-
const size = logSize(path);
|
|
33
|
-
const start = size < offset ? 0 : offset;
|
|
34
|
-
if (size === start) return { text: '', offset: size };
|
|
35
|
-
const fd = openSync(path, 'r');
|
|
36
|
-
try {
|
|
37
|
-
const buffer = Buffer.alloc(size - start);
|
|
38
|
-
readSync(fd, buffer, 0, buffer.length, start);
|
|
39
|
-
return { text: buffer.toString('utf8'), offset: size };
|
|
40
|
-
} finally {
|
|
41
|
-
closeSync(fd);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* D245: the Windows form of `commonly daemon install` (ADR-026 D1).
|
|
3
|
-
*
|
|
4
|
-
* macOS has launchd and Linux has systemd; Windows has Task Scheduler. A
|
|
5
|
-
* per-user task needs no administrator, so the install stays a one-liner a
|
|
6
|
-
* person can run in their own PowerShell. The shape mirrors the other two:
|
|
7
|
-
*
|
|
8
|
-
* - The task starts at this user's logon (a LogonTrigger naming the user; a
|
|
9
|
-
* trigger for ANY user's logon needs an administrator).
|
|
10
|
-
* - Keep-alive lives in the launcher, a PowerShell script that loops on
|
|
11
|
-
* `node <cli> daemon run --foreground`: it restarts on every exit except 77
|
|
12
|
-
* and 78, the same codes `RestartPreventExitStatus=77 78` exempts on systemd
|
|
13
|
-
* (a revoked key and a bad local registration). Task Scheduler's own
|
|
14
|
-
* "restart on failure" fires when a task fails to START, not when its
|
|
15
|
-
* program exits non-zero, so it cannot be the keep-alive.
|
|
16
|
-
* - A one-minute repeating trigger with `IgnoreNew` is the watchdog for the
|
|
17
|
-
* launcher itself: while the task runs it starts nothing, and if the
|
|
18
|
-
* launcher is killed the next minute brings it back.
|
|
19
|
-
* - On 77/78 the launcher disables the task before it exits, so the watchdog
|
|
20
|
-
* does not restart a daemon that has been told to stop. That is launchd's
|
|
21
|
-
* "park on a successful exit" in Task Scheduler's terms; `daemon start` and
|
|
22
|
-
* `daemon restart` enable it again.
|
|
23
|
-
*
|
|
24
|
-
* A task cannot carry environment variables, so the provider keys the seats
|
|
25
|
-
* need (TASK-049) go in a JSON file the launcher loads before it starts node.
|
|
26
|
-
* The file is written through the same 0600-at-creation writer as the unit
|
|
27
|
-
* files, and its ACL is then narrowed to this user (best effort; the profile
|
|
28
|
-
* directory is already private by default).
|
|
29
|
-
*
|
|
30
|
-
* Windows has no Seatbelt or bwrap, so a daemon-run seat here is unconfined.
|
|
31
|
-
* That is stated on the BYO page and in docs/agents/daemon-service-windows.md.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { join } from 'path';
|
|
35
|
-
|
|
36
|
-
export const WINDOWS_TASK_NAME = 'CommonlyDaemon';
|
|
37
|
-
|
|
38
|
-
export const windowsServicePaths = (home) => {
|
|
39
|
-
const dir = join(home, '.commonly', 'daemon', 'service');
|
|
40
|
-
return {
|
|
41
|
-
kind: 'schtasks',
|
|
42
|
-
// The task definition doubles as the "is it installed" record, the way the
|
|
43
|
-
// plist and the unit file do on the other two platforms.
|
|
44
|
-
file: join(dir, 'commonly-daemon-task.xml'),
|
|
45
|
-
launcher: join(dir, 'commonly-daemon.ps1'),
|
|
46
|
-
envFile: join(dir, 'service-env.json'),
|
|
47
|
-
taskName: WINDOWS_TASK_NAME,
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const xmlEscape = (value) => String(value)
|
|
52
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
53
|
-
.replace(/"/g, '"');
|
|
54
|
-
|
|
55
|
-
// A PowerShell single-quoted string ends at ANY of the four single-quote
|
|
56
|
-
// characters it recognises (' and the curly ‘ ’ ‚ ‛), and each is escaped by
|
|
57
|
-
// doubling. A user named O'Brien, or a path with a curly quote in it, would
|
|
58
|
-
// otherwise close the string and run the rest of the path as script.
|
|
59
|
-
export const psQuote = (value) => `'${String(value).replace(/['‘’‚‛]/g, (q) => q + q)}'`;
|
|
60
|
-
|
|
61
|
-
export const windowsLauncherScript = ({ nodePath, cliPath, logPath, envFile, taskName = WINDOWS_TASK_NAME }) => `# Space daemon launcher. Written by \`commonly daemon install\`; re-run that command instead of editing this file.
|
|
62
|
-
# It loads the service environment, then keeps \`commonly daemon run\` alive: every exit restarts it after 5 s,
|
|
63
|
-
# except 77 (this computer's key was revoked) and 78 (no valid local registration), which disable the task and stop.
|
|
64
|
-
$ErrorActionPreference = 'Continue'
|
|
65
|
-
$node = ${psQuote(nodePath)}
|
|
66
|
-
$cli = ${psQuote(cliPath)}
|
|
67
|
-
$log = ${psQuote(logPath)}
|
|
68
|
-
$envFile = ${psQuote(envFile)}
|
|
69
|
-
$task = ${psQuote(taskName)}
|
|
70
|
-
if (Test-Path -LiteralPath $envFile) {
|
|
71
|
-
$cfg = Get-Content -LiteralPath $envFile -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
72
|
-
foreach ($p in $cfg.PSObject.Properties) { [Environment]::SetEnvironmentVariable($p.Name, [string]$p.Value, 'Process') }
|
|
73
|
-
}
|
|
74
|
-
$env:Path = (Split-Path -Parent $node) + ';' + $env:Path
|
|
75
|
-
# Ending the task kills this script, not the node it started. The daemon watches this pid and
|
|
76
|
-
# exits (stopping its seats) once it is gone, so stop, restart and reinstall leave no daemon behind.
|
|
77
|
-
$env:COMMONLY_DAEMON_LAUNCHER_PID = "$PID"
|
|
78
|
-
# Read node's output as UTF-8; Windows PowerShell 5.1 otherwise decodes it in the console code page.
|
|
79
|
-
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
80
|
-
while ($true) {
|
|
81
|
-
& $node $cli daemon run --foreground 2>&1 | ForEach-Object { "$_" } | Add-Content -LiteralPath $log -Encoding UTF8
|
|
82
|
-
$code = $LASTEXITCODE
|
|
83
|
-
if ($code -eq 77 -or $code -eq 78) {
|
|
84
|
-
& schtasks.exe /Change /TN $task /DISABLE | Out-Null
|
|
85
|
-
if ($LASTEXITCODE -eq 0) { exit $code }
|
|
86
|
-
# The task could not be disabled, so exiting would let the watchdog start
|
|
87
|
-
# it again within a minute. Park here instead; stop or uninstall ends it.
|
|
88
|
-
"commonly daemon exited $code and the task could not be disabled; parked" | Add-Content -LiteralPath $log -Encoding UTF8
|
|
89
|
-
while ($true) { Start-Sleep -Seconds 3600 }
|
|
90
|
-
}
|
|
91
|
-
Start-Sleep -Seconds 5
|
|
92
|
-
}
|
|
93
|
-
`;
|
|
94
|
-
|
|
95
|
-
// UTF-8 with a BOM: Windows PowerShell 5.1 reads a BOM-less .ps1 in the ANSI
|
|
96
|
-
// code page, which would mangle a non-ASCII user name in any of the paths.
|
|
97
|
-
export const encodeLauncher = (script) => Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from(script, 'utf8')]);
|
|
98
|
-
|
|
99
|
-
export const POWERSHELL = '%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
100
|
-
|
|
101
|
-
export const windowsTaskXml = ({ launcher, userId, home }) => `<?xml version="1.0" encoding="UTF-16"?>
|
|
102
|
-
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
103
|
-
<RegistrationInfo>
|
|
104
|
-
<Description>Space local agent daemon (ADR-026). Installed by: commonly daemon install</Description>
|
|
105
|
-
</RegistrationInfo>
|
|
106
|
-
<Triggers>
|
|
107
|
-
<LogonTrigger>
|
|
108
|
-
<Enabled>true</Enabled>
|
|
109
|
-
<UserId>${xmlEscape(userId)}</UserId>
|
|
110
|
-
</LogonTrigger>
|
|
111
|
-
<TimeTrigger>
|
|
112
|
-
<Repetition>
|
|
113
|
-
<Interval>PT1M</Interval>
|
|
114
|
-
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
115
|
-
</Repetition>
|
|
116
|
-
<StartBoundary>2026-01-01T00:00:00</StartBoundary>
|
|
117
|
-
<Enabled>true</Enabled>
|
|
118
|
-
</TimeTrigger>
|
|
119
|
-
</Triggers>
|
|
120
|
-
<Principals>
|
|
121
|
-
<Principal id="Author">
|
|
122
|
-
<UserId>${xmlEscape(userId)}</UserId>
|
|
123
|
-
<LogonType>InteractiveToken</LogonType>
|
|
124
|
-
<RunLevel>LeastPrivilege</RunLevel>
|
|
125
|
-
</Principal>
|
|
126
|
-
</Principals>
|
|
127
|
-
<Settings>
|
|
128
|
-
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
129
|
-
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
130
|
-
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
131
|
-
<AllowHardTerminate>true</AllowHardTerminate>
|
|
132
|
-
<StartWhenAvailable>true</StartWhenAvailable>
|
|
133
|
-
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
134
|
-
<IdleSettings>
|
|
135
|
-
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
136
|
-
<RestartOnIdle>false</RestartOnIdle>
|
|
137
|
-
</IdleSettings>
|
|
138
|
-
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
139
|
-
<Enabled>true</Enabled>
|
|
140
|
-
<Hidden>false</Hidden>
|
|
141
|
-
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
142
|
-
<WakeToRun>false</WakeToRun>
|
|
143
|
-
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
144
|
-
<Priority>7</Priority>
|
|
145
|
-
</Settings>
|
|
146
|
-
<Actions Context="Author">
|
|
147
|
-
<Exec>
|
|
148
|
-
<Command>${xmlEscape(POWERSHELL)}</Command>
|
|
149
|
-
<Arguments>${xmlEscape(`-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${launcher}"`)}</Arguments>
|
|
150
|
-
<WorkingDirectory>${xmlEscape(home)}</WorkingDirectory>
|
|
151
|
-
</Exec>
|
|
152
|
-
</Actions>
|
|
153
|
-
</Task>
|
|
154
|
-
`;
|
|
155
|
-
|
|
156
|
-
// schtasks reads a task file as UTF-16; the declaration above says so, and the
|
|
157
|
-
// bytes must agree with it (little-endian, with the BOM).
|
|
158
|
-
export const encodeTaskXml = (xml) => Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(xml, 'utf16le')]);
|
|
159
|
-
|
|
160
|
-
// Task Scheduler names a user DOMAIN\user. A bare name resolves too, but the
|
|
161
|
-
// qualified one cannot be mistaken for a same-named account on the domain.
|
|
162
|
-
export const windowsUserId = ({ env = process.env, lookupUsername }) => {
|
|
163
|
-
if (env.USERNAME) return env.USERDOMAIN ? `${env.USERDOMAIN}\\${env.USERNAME}` : env.USERNAME;
|
|
164
|
-
return lookupUsername();
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
export const installWindowsService = async ({
|
|
168
|
-
home, nodePath, cliPath, logPath, providerEnv, env, writeFile, execCmd, lookupUsername, warn,
|
|
169
|
-
}) => {
|
|
170
|
-
const target = windowsServicePaths(home);
|
|
171
|
-
const userId = windowsUserId({ env, lookupUsername });
|
|
172
|
-
writeFile(target.envFile, `${JSON.stringify(Object.fromEntries(providerEnv), null, 2)}\n`);
|
|
173
|
-
try {
|
|
174
|
-
await execCmd(['icacls', target.envFile, '/inheritance:r', '/grant:r', `${userId}:F`]);
|
|
175
|
-
} catch (error) {
|
|
176
|
-
warn(`Could not narrow the permissions on ${target.envFile} (${error.message}). It holds your provider keys; it sits in your profile, which only you can read by default.`);
|
|
177
|
-
}
|
|
178
|
-
writeFile(target.launcher, encodeLauncher(windowsLauncherScript({ nodePath, cliPath, logPath, envFile: target.envFile })));
|
|
179
|
-
writeFile(target.file, encodeTaskXml(windowsTaskXml({ launcher: target.launcher, userId, home })));
|
|
180
|
-
// A running launcher keeps the environment it started with, so end it first
|
|
181
|
-
// (an absent task fails by design) and start the new definition after.
|
|
182
|
-
await execCmd(['schtasks', '/End', '/TN', target.taskName]).catch(() => {});
|
|
183
|
-
await execCmd(['schtasks', '/Create', '/TN', target.taskName, '/XML', target.file, '/F']);
|
|
184
|
-
await execCmd(['schtasks', '/Run', '/TN', target.taskName]);
|
|
185
|
-
return target;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
export const LAUNCHER_PID_ENV = 'COMMONLY_DAEMON_LAUNCHER_PID';
|
|
189
|
-
|
|
190
|
-
const pidAlive = (pid) => {
|
|
191
|
-
try {
|
|
192
|
-
process.kill(pid, 0);
|
|
193
|
-
return true;
|
|
194
|
-
} catch (error) {
|
|
195
|
-
return error.code === 'EPERM';
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* `schtasks /End` terminates the launcher only; the node it started would
|
|
201
|
-
* keep running, unsupervised and invisible to the task (seen on windows-latest,
|
|
202
|
-
* 11-04: a stopped task left its daemon checking in). A daemon started by the
|
|
203
|
-
* launcher therefore polls the launcher's pid and calls `onGone` once it has
|
|
204
|
-
* exited, at once if it is already gone when the watch is armed. Returns a
|
|
205
|
-
* stop function; does nothing outside the launcher.
|
|
206
|
-
*/
|
|
207
|
-
export const watchWindowsLauncher = ({
|
|
208
|
-
env = process.env,
|
|
209
|
-
platform = process.platform,
|
|
210
|
-
isAlive = pidAlive,
|
|
211
|
-
onGone,
|
|
212
|
-
everyMs = 2000,
|
|
213
|
-
setIntervalFn = setInterval,
|
|
214
|
-
clearIntervalFn = clearInterval,
|
|
215
|
-
streams = [process.stdout, process.stderr],
|
|
216
|
-
}) => {
|
|
217
|
-
const pid = Number(env[LAUNCHER_PID_ENV]);
|
|
218
|
-
if (platform !== 'win32' || !Number.isInteger(pid) || pid <= 0) return () => {};
|
|
219
|
-
// The launcher holds this process's stdout and stderr. Once it exits, any
|
|
220
|
-
// log line (a poll, a heartbeat) fails with EPIPE, and an unhandled stream
|
|
221
|
-
// error would end the daemon before it stops its seats. So the streams
|
|
222
|
-
// swallow errors from the moment the watch is armed, not only after the
|
|
223
|
-
// launcher's exit is noticed up to two seconds later.
|
|
224
|
-
for (const stream of streams) stream?.on?.('error', () => {});
|
|
225
|
-
// Already gone when armed (the daemon is armed before its first tick): say
|
|
226
|
-
// so now, rather than after a poll and a heartbeat logged into a dead pipe.
|
|
227
|
-
if (!isAlive(pid)) {
|
|
228
|
-
onGone(pid);
|
|
229
|
-
return () => {};
|
|
230
|
-
}
|
|
231
|
-
const timer = setIntervalFn(() => {
|
|
232
|
-
if (isAlive(pid)) return;
|
|
233
|
-
clearIntervalFn(timer);
|
|
234
|
-
onGone(pid);
|
|
235
|
-
}, everyMs);
|
|
236
|
-
if (typeof timer?.unref === 'function') timer.unref();
|
|
237
|
-
return () => clearIntervalFn(timer);
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
export const uninstallWindowsService = async ({ home, execCmd, removeFile, existsFile }) => {
|
|
241
|
-
const target = windowsServicePaths(home);
|
|
242
|
-
await execCmd(['schtasks', '/End', '/TN', target.taskName]).catch(() => {});
|
|
243
|
-
await execCmd(['schtasks', '/Delete', '/TN', target.taskName, '/F']).catch(() => {});
|
|
244
|
-
for (const file of [target.file, target.launcher, target.envFile]) {
|
|
245
|
-
if (existsFile(file)) removeFile(file);
|
|
246
|
-
}
|
|
247
|
-
return target;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
export const windowsServiceAction = async ({ action, home, execCmd }) => {
|
|
251
|
-
const target = windowsServicePaths(home);
|
|
252
|
-
const name = target.taskName;
|
|
253
|
-
if (action === 'stop') {
|
|
254
|
-
// Disabled first, so the one-minute watchdog cannot start it again.
|
|
255
|
-
await execCmd(['schtasks', '/Change', '/TN', name, '/DISABLE']);
|
|
256
|
-
await execCmd(['schtasks', '/End', '/TN', name]).catch(() => {});
|
|
257
|
-
return target;
|
|
258
|
-
}
|
|
259
|
-
if (action === 'restart') await execCmd(['schtasks', '/End', '/TN', name]).catch(() => {});
|
|
260
|
-
await execCmd(['schtasks', '/Change', '/TN', name, '/ENABLE']);
|
|
261
|
-
await execCmd(['schtasks', '/Run', '/TN', name]);
|
|
262
|
-
return target;
|
|
263
|
-
};
|