@scopebond/agent 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -6
- package/dist/agent.d.ts +2 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +4 -4
- package/dist/agent.js.map +1 -1
- package/dist/allowance-sender.d.ts +8 -0
- package/dist/allowance-sender.d.ts.map +1 -0
- package/dist/allowance-sender.js +80 -0
- package/dist/allowance-sender.js.map +1 -0
- package/dist/autostart.d.ts +13 -0
- package/dist/autostart.d.ts.map +1 -1
- package/dist/autostart.js +60 -10
- package/dist/autostart.js.map +1 -1
- package/dist/cli-main.d.ts +3 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +218 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/cli.js +4 -185
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/ipc.d.ts +7 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +64 -12
- package/dist/ipc.js.map +1 -1
- package/dist/log-file.d.ts +6 -0
- package/dist/log-file.d.ts.map +1 -0
- package/dist/log-file.js +40 -0
- package/dist/log-file.js.map +1 -0
- package/dist/native-update.d.ts +51 -0
- package/dist/native-update.d.ts.map +1 -0
- package/dist/native-update.js +120 -0
- package/dist/native-update.js.map +1 -0
- package/dist/prompt.d.ts +11 -1
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +57 -23
- package/dist/prompt.js.map +1 -1
- package/dist/reconnect.d.ts +19 -0
- package/dist/reconnect.d.ts.map +1 -0
- package/dist/reconnect.js +60 -0
- package/dist/reconnect.js.map +1 -0
- package/dist/self.d.ts +5 -0
- package/dist/self.d.ts.map +1 -0
- package/dist/self.js +15 -0
- package/dist/self.js.map +1 -0
- package/dist/service.d.ts +50 -4
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +292 -18
- package/dist/service.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +4 -9
- package/dist/setup.js.map +1 -1
- package/dist/summary.d.ts +16 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +47 -0
- package/dist/summary.js.map +1 -0
- package/dist/tray-model.d.ts +91 -0
- package/dist/tray-model.d.ts.map +1 -0
- package/dist/tray-model.js +159 -0
- package/dist/tray-model.js.map +1 -0
- package/dist/tray.d.ts.map +1 -1
- package/dist/tray.js +103 -29
- package/dist/tray.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +7 -4
- package/dist/update.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -18,6 +18,9 @@ agent never takes part in a decision. It only keeps everything around the decisi
|
|
|
18
18
|
the workspace recommends a newer agent, it installs it from npm and restarts itself; when the workspace holds
|
|
19
19
|
updates, it changes nothing. It also moves the Scopebond hook entries in agent settings to the recommended hook
|
|
20
20
|
and repairs any entry that can no longer start. Other tools' hook entries are never touched.
|
|
21
|
+
- **A small local store.** With each update check it keeps the local receipt store small: receipts the workspace
|
|
22
|
+
acknowledged are removed after the workspace's retention window (30 days unless it sets 7–365), never one it has not
|
|
23
|
+
acknowledged; a store written by an older hook is rewritten once so it keeps each policy and receipt only once.
|
|
21
24
|
- **Self-check.** Once a day it checks this computer's side (hook entry present and able to start, autostart on,
|
|
22
25
|
nothing stuck in the queue, connection not about to lapse) and sends the result, signed with the key the computer
|
|
23
26
|
enrolled with, so the workspace can show that the whole path works end to end, or what broke.
|
|
@@ -25,7 +28,10 @@ agent never takes part in a decision. It only keeps everything around the decisi
|
|
|
25
28
|
shows its own window (Windows PowerShell with Windows Forms, macOS `osascript`, Linux `zenity`): the rule, the action and a
|
|
26
29
|
reason field. Only the window answers; whatever calls the local channel can open a window but never decide it. One window
|
|
27
30
|
at a time; it closes itself before the hook stops waiting. The reason goes to the workspace once, and waits in the
|
|
28
|
-
Scopebond home while the computer is offline.
|
|
31
|
+
Scopebond home while the computer is offline. As the workspace allows, the window offers **Allow once**, **Allow for 15
|
|
32
|
+
min**, **Always allow this here…** and **Ask an admin** (only the last for a rule set to *Block, person may ask*). The
|
|
33
|
+
agent sends a person's allowances and requests to the workspace (`POST /v1/allowances`, `POST /v1/requests`), each
|
|
34
|
+
signed with the key this computer enrolled with, once; a workspace without these calls leaves them on the computer.
|
|
29
35
|
- **Health.** It reports the same machine-readable status as `scopebond status --json` (`scopebond.status.v1`).
|
|
30
36
|
|
|
31
37
|
## Use
|
|
@@ -84,13 +90,36 @@ opens at sign-in. If the agent stops with an error, the launcher starts it again
|
|
|
84
90
|
each noted in `agent.log`), as launchd and systemd do on macOS and Linux; a clean stop (`stop`, `autostart off`, an
|
|
85
91
|
update handing over) ends it. `status` says whether autostart is on and working.
|
|
86
92
|
|
|
93
|
+
The agent writes `agent.log` itself, a line at a time, so an agent handing over to its update and the updated one
|
|
94
|
+
can both write it. When it updates itself, the updated agent starts through the same launcher and waits for the old
|
|
95
|
+
one to exit; under systemd the service restarts it instead. A launcher written by agent 0.4.6 or earlier redirected
|
|
96
|
+
the agent's output into `agent.log`, which kept the updated agent from starting on Windows: an agent under such a
|
|
97
|
+
launcher starts its update directly, and the update rewrites the launcher (until the next sign-in, its log lines go
|
|
98
|
+
to `agent-handover.log`). After `check` updates the agent, it waits for the updated one, starts it if it did not
|
|
99
|
+
start, and says which version runs.
|
|
100
|
+
|
|
87
101
|
## Where you see it
|
|
88
102
|
|
|
89
|
-
On Windows the agent shows a tray icon (from Windows' own PowerShell, nothing extra to install):
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
103
|
+
On Windows the agent shows a tray icon (from Windows' own PowerShell, nothing extra to install): the Scopebond "S" tile
|
|
104
|
+
with a status badge, its shape and colour together — none when protected, a grey dash when the workspace cannot be
|
|
105
|
+
reached (it keeps checking actions; records wait), an amber "!" when something needs attention, a red "×" for a
|
|
106
|
+
problem, a blue arrow while it works (an update), and a slate tile when the computer is not connected to its workspace.
|
|
107
|
+
Records waiting count only the time the computer was awake: a laptop that slept is not "stuck".
|
|
108
|
+
|
|
109
|
+
Click it (left or right) for the menu: one headline; the rows that have data (Rules, Delivery, Today's actions and
|
|
110
|
+
blocks, Version); the one fix when something is wrong; **Check now**, which always says what it found; **Send records
|
|
111
|
+
now** when records wait; **Update now** when the workspace recommends a newer version; the last few blocks (**Recently blocked**: where the
|
|
112
|
+
workspace lets a person allow a blocked action or ask an admin, clicking one opens the Scopebond window for it, and the
|
|
113
|
+
person can allow it once, for 15 minutes or always, or ask an admin; Scopebond never runs it again by itself); the
|
|
114
|
+
notification setting (All, Problems only — the default —, Off); Help (copy diagnostics without keys or credentials, open
|
|
115
|
+
the Scopebond folder, documentation, About); and Hide icon. There is no "pause" or "quit protection": the hook decides
|
|
116
|
+
every action whether or not the agent runs. A balloon appears when the state gets worse ("needs attention" only after
|
|
117
|
+
five minutes, so sleep and wake do not flap) and once when it is protected again. Set `SCOPEBOND_AGENT_TRAY=off` for no
|
|
118
|
+
icon. On macOS and Linux the agent sends a system notification when things get worse, and once more when they recover.
|
|
119
|
+
|
|
120
|
+
The tray only draws what the agent computes: `GET /tray` on the local channel returns the model (state, headline, rows,
|
|
121
|
+
fix, actions, recent blocks) and the person's settings; `POST /check`, `/update`, `/flush`, `/repair`, `/settings`
|
|
122
|
+
and `/blocked` (an earlier block, answered only in the Scopebond window) do the work. `GET /status` carries the same `health` (level, headline, fix).
|
|
94
123
|
|
|
95
124
|
## Status
|
|
96
125
|
|
package/dist/agent.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface CycleResult {
|
|
|
15
15
|
deliveryError: string | null;
|
|
16
16
|
rules: SyncOutcome["state"] | "skipped";
|
|
17
17
|
missingHookEntries: Harness[];
|
|
18
|
+
/** A request from the workspace's computer page, carried on this cycle's rules check. */
|
|
19
|
+
requested: "flush" | "self_check" | null;
|
|
18
20
|
}
|
|
19
21
|
/** The agents on this computer whose user-level settings should hold the Scopebond hook. */
|
|
20
22
|
export declare function expectedHarnesses(): Harness[];
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAChD,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IACxC,kBAAkB,EAAE,OAAO,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAWA,OAAO,EAGL,KAAK,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAChD,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IACxC,kBAAkB,EAAE,OAAO,EAAE,CAAC;IAC9B,yFAAyF;IACzF,SAAS,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CAAC;CAC1C;AAED,4FAA4F;AAC5F,wBAAgB,iBAAiB,IAAI,OAAO,EAAE,CAE7C;AAED,gGAAgG;AAChG,wBAAgB,kBAAkB,CAAC,SAAS,GAAE,OAAO,EAAwB,GAAG,OAAO,EAAE,CAExF;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAwC1E;AAED,mGAAmG;AACnG,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,UAAU,CAOpE"}
|
package/dist/agent.js
CHANGED
|
@@ -8,7 +8,7 @@ import { join } from "node:path";
|
|
|
8
8
|
import { createCloudExporter } from "@scopebond/gateway";
|
|
9
9
|
import { SqliteCloudOutbox } from "@scopebond/gateway/node";
|
|
10
10
|
import { createSigner } from "@scopebond/sdk";
|
|
11
|
-
import { LOSSLESS_OUTBOX, OUTBOX_FILE, buildStatusJson, cursorDetected, codexDetected, hookVersion, ingestUrl, isHarnessConfigured, loadConnection, policyBuilds, recordDeliveryAttempt, syncPolicy, userHarnessFile, } from "@scopebond/hook";
|
|
11
|
+
import { LOSSLESS_OUTBOX, OUTBOX_FILE, buildStatusJson, cursorDetected, codexDetected, hookVersion, ingestUrl, isHarnessConfigured, loadConnection, policyBuilds, recordDeliveryAttempt, summaryOptions, syncPolicy, userHarnessFile, } from "@scopebond/hook";
|
|
12
12
|
/** The agents on this computer whose user-level settings should hold the Scopebond hook. */
|
|
13
13
|
export function expectedHarnesses() {
|
|
14
14
|
return ["claude", ...(cursorDetected() ? ["cursor"] : []), ...(codexDetected() ? ["codex"] : [])];
|
|
@@ -20,7 +20,7 @@ export function missingHookEntries(harnesses = expectedHarnesses()) {
|
|
|
20
20
|
export async function runCycle(options) {
|
|
21
21
|
const now = options.now ?? Date.now;
|
|
22
22
|
const dir = options.dir;
|
|
23
|
-
const result = { at: now(), connected: false, delivered: 0, pending: 0, deliveryError: null, rules: "skipped", missingHookEntries: [] };
|
|
23
|
+
const result = { at: now(), connected: false, delivered: 0, pending: 0, deliveryError: null, rules: "skipped", missingHookEntries: [], requested: null };
|
|
24
24
|
try {
|
|
25
25
|
result.missingHookEntries = missingHookEntries();
|
|
26
26
|
}
|
|
@@ -35,7 +35,7 @@ export async function runCycle(options) {
|
|
|
35
35
|
const before = outbox.status().pending;
|
|
36
36
|
const exporter = createCloudExporter({
|
|
37
37
|
url: ingestUrl(connection), credential: connection.credential, outbox,
|
|
38
|
-
flushMs: 24 * 60 * 60 * 1000, fetch: options.fetchImpl, now,
|
|
38
|
+
flushMs: 24 * 60 * 60 * 1000, fetch: options.fetchImpl, now, summaries: summaryOptions(dir),
|
|
39
39
|
});
|
|
40
40
|
try {
|
|
41
41
|
const lastSuccess = exporter.status().lastSuccessAt;
|
|
@@ -58,7 +58,7 @@ export async function runCycle(options) {
|
|
|
58
58
|
const keyFile = join(dir, "agent.key");
|
|
59
59
|
if (existsSync(keyFile)) {
|
|
60
60
|
const agentKid = createSigner({ privateKeyPem: readFileSync(keyFile, "utf8") }).kid;
|
|
61
|
-
const outcome = await syncPolicy(dir, { agentKid, hookVersion: hookVersion(), policyBuilds, fetchImpl: options.fetchImpl, timeoutMs: options.timeoutMs ?? 10_000 });
|
|
61
|
+
const outcome = await syncPolicy(dir, { agentKid, hookVersion: hookVersion(), policyBuilds, fetchImpl: options.fetchImpl, timeoutMs: options.timeoutMs ?? 10_000, onRequest: (kind) => { result.requested = kind; } });
|
|
62
62
|
result.rules = outcome.state;
|
|
63
63
|
}
|
|
64
64
|
}
|
package/dist/agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,8FAA8F;AAC9F,8FAA8F;AAC9F,8FAA8F;AAC9F,wBAAwB;AAExB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,eAAe,EAAE,WAAW,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EACpG,mBAAmB,EAAE,cAAc,EAAE,YAAY,EAAE,qBAAqB,EAAE,UAAU,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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","sourcesContent":["// The Scopebond Agent's work, one cycle at a time. The hook still decides every action on its\n// own (enforcement never depends on the agent); the agent keeps everything around it working:\n// it delivers what the hook queued, keeps the workspace rules and the connection current, and\n// knows when the agent settings lost their hook entry. Every step is best-effort and bounded;\n// a cycle never throws.\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { createCloudExporter } from \"@scopebond/gateway\";\nimport { SqliteCloudOutbox } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport {\n LOSSLESS_OUTBOX, OUTBOX_FILE, buildStatusJson, cursorDetected, codexDetected, hookVersion, ingestUrl,\n isHarnessConfigured, loadConnection, policyBuilds, recordDeliveryAttempt, summaryOptions, syncPolicy, userHarnessFile,\n type Harness, type StatusJson, type SyncOutcome,\n} from \"@scopebond/hook\";\n\nexport interface CycleOptions {\n /** The Scopebond home this agent serves (the user's ~/.scopebond by default). */\n dir: string;\n fetchImpl?: typeof fetch;\n now?: () => number;\n /** Per-request timeout for the workspace, in milliseconds. */\n timeoutMs?: number;\n}\n\nexport interface CycleResult {\n at: number;\n connected: boolean;\n delivered: number;\n pending: number;\n deliveryError: string | null;\n rules: SyncOutcome[\"state\"] | \"skipped\";\n missingHookEntries: Harness[];\n /** A request from the workspace's computer page, carried on this cycle's rules check. */\n requested: \"flush\" | \"self_check\" | null;\n}\n\n/** The agents on this computer whose user-level settings should hold the Scopebond hook. */\nexport function expectedHarnesses(): Harness[] {\n return [\"claude\", ...(cursorDetected() ? [\"cursor\" as const] : []), ...(codexDetected() ? [\"codex\" as const] : [])];\n}\n\n/** Agent settings that lost their Scopebond hook entry (an update or a reset can remove it). */\nexport function missingHookEntries(harnesses: Harness[] = expectedHarnesses()): Harness[] {\n return harnesses.filter((h) => existsSync(userHarnessFile(h)) && !isHarnessConfigured(userHarnessFile(h)));\n}\n\nexport async function runCycle(options: CycleOptions): Promise<CycleResult> {\n const now = options.now ?? Date.now;\n const dir = options.dir;\n const result: CycleResult = { at: now(), connected: false, delivered: 0, pending: 0, deliveryError: null, rules: \"skipped\", missingHookEntries: [], requested: null };\n try { result.missingHookEntries = missingHookEntries(); } catch { /* reported as none */ }\n const connection = loadConnection(dir);\n if (!connection) return result;\n result.connected = true;\n\n // 1. Deliver everything the hook queued, in batches, until the queue is empty or the workspace refuses.\n try {\n const outbox = new SqliteCloudOutbox(join(dir, OUTBOX_FILE), LOSSLESS_OUTBOX);\n const before = outbox.status().pending;\n const exporter = createCloudExporter({\n url: ingestUrl(connection), credential: connection.credential, outbox,\n flushMs: 24 * 60 * 60 * 1000, fetch: options.fetchImpl, now, summaries: summaryOptions(dir),\n });\n try {\n const lastSuccess = exporter.status().lastSuccessAt;\n await exporter.flush();\n const status = exporter.status();\n recordDeliveryAttempt(dir, status, now(), lastSuccess);\n result.pending = status.pending;\n result.delivered = Math.max(0, before - status.pending);\n result.deliveryError = status.lastError;\n } finally { exporter.stop(); }\n } catch (error) {\n result.deliveryError = (error as Error).message;\n }\n\n // 2. The rules check: the workspace's rules, the queue report, and credential renewal when due.\n try {\n const keyFile = join(dir, \"agent.key\");\n if (existsSync(keyFile)) {\n const agentKid = createSigner({ privateKeyPem: readFileSync(keyFile, \"utf8\") }).kid;\n const outcome = await syncPolicy(dir, { agentKid, hookVersion: hookVersion(), policyBuilds, fetchImpl: options.fetchImpl, timeoutMs: options.timeoutMs ?? 10_000, onRequest: (kind) => { result.requested = kind; } });\n result.rules = outcome.state;\n }\n } catch { result.rules = \"unavailable\"; }\n return result;\n}\n\n/** The machine-readable status (scopebond.status.v1) for this computer, as the hook reports it. */\nexport function computerStatus(dir: string, now?: number): StatusJson {\n const expected = expectedHarnesses();\n const configured = (h: Harness) => expected.includes(h) && existsSync(userHarnessFile(h)) && isHarnessConfigured(userHarnessFile(h));\n return buildStatusJson({\n version: hookVersion(), activeDir: dir, candidateDirs: [dir], hasPolicy: existsSync(join(dir, \"policy.json\")),\n agents: { claude: configured(\"claude\"), cursor: configured(\"cursor\"), codex: configured(\"codex\") }, now,\n });\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type HookConnection } from "@scopebond/hook";
|
|
2
|
+
export declare const ALLOWANCE_DOMAIN = "scopebond:allowance/v1\n";
|
|
3
|
+
export declare const REQUEST_DOMAIN = "scopebond:request/v1\n";
|
|
4
|
+
export declare function sendAllowancesAndRequests(dir: string, connection: Pick<HookConnection, "url" | "credential">, fetchImpl?: typeof fetch, now?: number): Promise<{
|
|
5
|
+
allowances: number;
|
|
6
|
+
requests: number;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=allowance-sender.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowance-sender.d.ts","sourceRoot":"","sources":["../src/allowance-sender.ts"],"names":[],"mappings":"AAWA,OAAO,EAAgE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEpH,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAC3D,eAAO,MAAM,cAAc,2BAA2B,CAAC;AAEvD,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,YAAY,CAAC,EAAE,SAAS,GAAE,OAAO,KAAa,EAAE,GAAG,SAAa,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAmDzN"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// D144 (SB411): the agent tells the workspace about allowances a person made on this computer and the requests they sent to
|
|
2
|
+
// an admin. Each record is signed by the key this computer enrolled with (the same key that countersigns its receipts), so
|
|
3
|
+
// the workspace can show it was made here. Sent once; one the workspace refuses for good (400) is not sent again; one it
|
|
4
|
+
// could not take (offline, 5xx) waits for the next cycle. A workspace without these calls (404) leaves them on the computer.
|
|
5
|
+
// Each signature covers a domain line and the canonical record, so neither can pass as a receipt signed by the same key. The
|
|
6
|
+
// files are re-read before each write and only what this send changed is applied, so a hook writing meanwhile loses nothing.
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { canonical } from "@scopebond/gateway";
|
|
10
|
+
import { loadOrCreateAttester } from "@scopebond/gateway/node";
|
|
11
|
+
import { readAllowances, readRequests, writeAllowances, writeRequests } from "@scopebond/hook";
|
|
12
|
+
export const ALLOWANCE_DOMAIN = "scopebond:allowance/v1\n";
|
|
13
|
+
export const REQUEST_DOMAIN = "scopebond:request/v1\n";
|
|
14
|
+
export async function sendAllowancesAndRequests(dir, connection, fetchImpl = fetch, now = Date.now()) {
|
|
15
|
+
const sent = { allowances: 0, requests: 0 };
|
|
16
|
+
const keyFile = join(dir, "attester.key");
|
|
17
|
+
if (!existsSync(keyFile))
|
|
18
|
+
return sent; // never makes a key: an unenrolled computer has nothing to send
|
|
19
|
+
const { attester } = loadOrCreateAttester({ file: keyFile });
|
|
20
|
+
const post = async (path, record, domain) => {
|
|
21
|
+
try {
|
|
22
|
+
const body = { record, kid: attester.kid, signature: await attester.sign(domain + canonical(record)) };
|
|
23
|
+
const res = await fetchImpl(new URL(path, connection.url).toString(), {
|
|
24
|
+
method: "POST", redirect: "error", signal: AbortSignal.timeout(10_000),
|
|
25
|
+
headers: { authorization: `Bearer ${connection.credential}`, "content-type": "application/json" }, body: JSON.stringify(body),
|
|
26
|
+
});
|
|
27
|
+
if (res.ok)
|
|
28
|
+
return "sent";
|
|
29
|
+
return res.status === 400 || res.status === 409 ? "refused" : "later";
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return "later";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const at = new Date(now).toISOString();
|
|
36
|
+
const allowanceOutcomes = new Map();
|
|
37
|
+
for (const a of readAllowances(dir)) {
|
|
38
|
+
if (a.created_by !== "person" || a.sent_at)
|
|
39
|
+
continue;
|
|
40
|
+
const { sent_at: _sent, uses: _uses, ...record } = a;
|
|
41
|
+
const outcome = await post("/v1/allowances", record, ALLOWANCE_DOMAIN);
|
|
42
|
+
if (outcome !== "later")
|
|
43
|
+
allowanceOutcomes.set(a.id, outcome);
|
|
44
|
+
}
|
|
45
|
+
if (allowanceOutcomes.size) {
|
|
46
|
+
// Applied to the file as it is now (the hook may have counted a use or added an allowance while this was sending).
|
|
47
|
+
writeAllowances(dir, readAllowances(dir).map((a) => {
|
|
48
|
+
const outcome = allowanceOutcomes.get(a.id);
|
|
49
|
+
if (!outcome || a.sent_at)
|
|
50
|
+
return a;
|
|
51
|
+
if (outcome === "sent") {
|
|
52
|
+
sent.allowances += 1;
|
|
53
|
+
const { reason: _text, ...rest } = a;
|
|
54
|
+
return { ...rest, sent_at: at };
|
|
55
|
+
} // the workspace keeps the text
|
|
56
|
+
return { ...a, sent_at: at };
|
|
57
|
+
}), now);
|
|
58
|
+
}
|
|
59
|
+
const requestOutcomes = new Map();
|
|
60
|
+
for (const r of readRequests(dir)) {
|
|
61
|
+
if (r.sent_at)
|
|
62
|
+
continue;
|
|
63
|
+
const { sent_at: _sent, ...record } = r;
|
|
64
|
+
const outcome = await post("/v1/requests", record, REQUEST_DOMAIN);
|
|
65
|
+
if (outcome !== "later")
|
|
66
|
+
requestOutcomes.set(r.id, outcome);
|
|
67
|
+
}
|
|
68
|
+
if (requestOutcomes.size) {
|
|
69
|
+
writeRequests(dir, readRequests(dir).map((r) => {
|
|
70
|
+
const outcome = requestOutcomes.get(r.id);
|
|
71
|
+
if (!outcome || r.sent_at)
|
|
72
|
+
return r;
|
|
73
|
+
if (outcome === "sent")
|
|
74
|
+
sent.requests += 1;
|
|
75
|
+
return { ...r, sent_at: at };
|
|
76
|
+
}), now);
|
|
77
|
+
}
|
|
78
|
+
return sent;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=allowance-sender.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowance-sender.js","sourceRoot":"","sources":["../src/allowance-sender.ts"],"names":[],"mappings":"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","sourcesContent":["// D144 (SB411): the agent tells the workspace about allowances a person made on this computer and the requests they sent to\n// an admin. Each record is signed by the key this computer enrolled with (the same key that countersigns its receipts), so\n// the workspace can show it was made here. Sent once; one the workspace refuses for good (400) is not sent again; one it\n// could not take (offline, 5xx) waits for the next cycle. A workspace without these calls (404) leaves them on the computer.\n// Each signature covers a domain line and the canonical record, so neither can pass as a receipt signed by the same key. The\n// files are re-read before each write and only what this send changed is applied, so a hook writing meanwhile loses nothing.\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { canonical } from \"@scopebond/gateway\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { readAllowances, readRequests, writeAllowances, writeRequests, type HookConnection } from \"@scopebond/hook\";\n\nexport const ALLOWANCE_DOMAIN = \"scopebond:allowance/v1\\n\";\nexport const REQUEST_DOMAIN = \"scopebond:request/v1\\n\";\n\nexport async function sendAllowancesAndRequests(dir: string, connection: Pick<HookConnection, \"url\" | \"credential\">, fetchImpl: typeof fetch = fetch, now = Date.now()): Promise<{ allowances: number; requests: number }> {\n const sent = { allowances: 0, requests: 0 };\n const keyFile = join(dir, \"attester.key\");\n if (!existsSync(keyFile)) return sent; // never makes a key: an unenrolled computer has nothing to send\n const { attester } = loadOrCreateAttester({ file: keyFile });\n const post = async (path: string, record: Record<string, unknown>, domain: string): Promise<\"sent\" | \"refused\" | \"later\"> => {\n try {\n const body = { record, kid: attester.kid, signature: await attester.sign(domain + canonical(record)) };\n const res = await fetchImpl(new URL(path, connection.url).toString(), {\n method: \"POST\", redirect: \"error\", signal: AbortSignal.timeout(10_000),\n headers: { authorization: `Bearer ${connection.credential}`, \"content-type\": \"application/json\" }, body: JSON.stringify(body),\n });\n if (res.ok) return \"sent\";\n return res.status === 400 || res.status === 409 ? \"refused\" : \"later\";\n } catch { return \"later\"; }\n };\n\n const at = new Date(now).toISOString();\n const allowanceOutcomes = new Map<string, \"sent\" | \"refused\">();\n for (const a of readAllowances(dir)) {\n if (a.created_by !== \"person\" || a.sent_at) continue;\n const { sent_at: _sent, uses: _uses, ...record } = a;\n const outcome = await post(\"/v1/allowances\", record, ALLOWANCE_DOMAIN);\n if (outcome !== \"later\") allowanceOutcomes.set(a.id, outcome);\n }\n if (allowanceOutcomes.size) {\n // Applied to the file as it is now (the hook may have counted a use or added an allowance while this was sending).\n writeAllowances(dir, readAllowances(dir).map((a) => {\n const outcome = allowanceOutcomes.get(a.id);\n if (!outcome || a.sent_at) return a;\n if (outcome === \"sent\") { sent.allowances += 1; const { reason: _text, ...rest } = a; return { ...rest, sent_at: at }; } // the workspace keeps the text\n return { ...a, sent_at: at };\n }), now);\n }\n\n const requestOutcomes = new Map<string, \"sent\" | \"refused\">();\n for (const r of readRequests(dir)) {\n if (r.sent_at) continue;\n const { sent_at: _sent, ...record } = r;\n const outcome = await post(\"/v1/requests\", record, REQUEST_DOMAIN);\n if (outcome !== \"later\") requestOutcomes.set(r.id, outcome);\n }\n if (requestOutcomes.size) {\n writeRequests(dir, readRequests(dir).map((r) => {\n const outcome = requestOutcomes.get(r.id);\n if (!outcome || r.sent_at) return r;\n if (outcome === \"sent\") sent.requests += 1;\n return { ...r, sent_at: at };\n }), now);\n }\n return sent;\n}\n"]}
|
package/dist/autostart.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export declare const LABEL = "com.scopebond.agent";
|
|
2
|
+
/** The agent's log, which the agent writes itself (`SCOPEBOND_AGENT_LOG`) when a Windows launcher or a handover starts it. */
|
|
3
|
+
export declare const AGENT_LOG_ENV = "SCOPEBOND_AGENT_LOG";
|
|
4
|
+
/** Written into every Windows launcher this version makes. A launcher without it redirects the agent's output into
|
|
5
|
+
* agent.log itself, which keeps a replacement agent from starting (see `spawnReplacement`). */
|
|
6
|
+
export declare const LAUNCHER_MARK = "rem Scopebond Agent launcher 2:";
|
|
7
|
+
/** Whether a launcher's text is this version's (an older one is rewritten once no agent runs under it). */
|
|
8
|
+
export declare function launcherIsCurrent(text: string, platform?: NodeJS.Platform): boolean;
|
|
2
9
|
export declare function launcherPath(home: string, platform?: NodeJS.Platform): string;
|
|
10
|
+
/** The Windows launcher. With `cli` empty, `node` is the single executable: it is started as it is, with no Node to find. */
|
|
3
11
|
export declare function windowsLauncher(node: string, cli: string, logFile: string): string;
|
|
12
|
+
/** The macOS/Linux launcher. With `cli` empty, `node` is the single executable, started as it is. */
|
|
4
13
|
export declare function posixLauncher(node: string, cli: string): string;
|
|
5
14
|
export declare function windowsRunCommand(launcher: string): string;
|
|
6
15
|
export declare function macLaunchAgent(launcher: string, logFile: string): string;
|
|
@@ -9,6 +18,10 @@ export declare function autostartPaths(home?: string): {
|
|
|
9
18
|
macPlist: string;
|
|
10
19
|
linuxUnit: string;
|
|
11
20
|
};
|
|
21
|
+
/** Rewrite an older Windows launcher as this version's, keeping autostart as it is. Only safe once no cmd.exe runs the old
|
|
22
|
+
* one (cmd reads a batch file line by line from where it left off): the handover calls it after the old agent has exited.
|
|
23
|
+
* Returns whether it rewrote one. */
|
|
24
|
+
export declare function refreshLauncher(scopebondHome: string, cli: string, node?: string, platform?: NodeJS.Platform): boolean;
|
|
12
25
|
/** The ways to start the agent now on Windows, in order: the launcher under a headless console (no window,
|
|
13
26
|
* as sign-in does), then the launcher through cmd.exe with its window hidden, for a session where the
|
|
14
27
|
* headless console host does not start (seen on Windows Server). macOS and Linux start it themselves
|
package/dist/autostart.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autostart.d.ts","sourceRoot":"","sources":["../src/autostart.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"autostart.d.ts","sourceRoot":"","sources":["../src/autostart.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,KAAK,wBAAwB,CAAC;AAO3C,8HAA8H;AAC9H,eAAO,MAAM,aAAa,wBAAwB,CAAC;AAEnD;gGACgG;AAChG,eAAO,MAAM,aAAa,oCAAoC,CAAC;AAE/D,2GAA2G;AAC3G,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,OAAO,CAErG;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAE/F;AAED,6HAA6H;AAC7H,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyClF;AAED,qGAAqG;AACrG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAsB/D;AAOD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAexE;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CActD;AAED,wBAAgB,cAAc,CAAC,IAAI,SAAY;;;EAK9C;AAED;;sCAEsC;AACtC,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,SAAmB,EAAE,QAAQ,kBAAmB,GAAG,OAAO,CAOjI;AAaD;;;6DAG6D;AAC7D,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAIvH;AAED,0HAA0H;AAC1H,wBAAgB,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,kBAAmB,EAAE,OAAO,SAAI,GAAG,OAAO,CAUjG;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,SAAmB,EAAE,QAAQ,kBAAmB,GAAG,MAAM,CAkBhI;AAED,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,kBAAmB,GAAG,MAAM,CAiB3F;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,aAAa,EAAE,MAAM,EAAE,QAAQ,kBAAmB,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAWhI"}
|
package/dist/autostart.js
CHANGED
|
@@ -6,41 +6,63 @@
|
|
|
6
6
|
// so upgrading Node or switching versions never leaves an autostart entry pointing at nothing.
|
|
7
7
|
// On Windows it runs under `conhost --headless`, so no window opens at sign-in.
|
|
8
8
|
import { execFileSync, spawn } from "node:child_process";
|
|
9
|
-
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { dirname, join, win32 } from "node:path";
|
|
12
|
+
import { isSingleExecutable } from "@scopebond/hook";
|
|
12
13
|
export const LABEL = "com.scopebond.agent";
|
|
13
14
|
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
14
15
|
const RUN_VALUE = "ScopebondAgent";
|
|
15
16
|
const xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
16
17
|
const noQuotes = (s) => s.replace(/"/g, "");
|
|
18
|
+
/** The agent's log, which the agent writes itself (`SCOPEBOND_AGENT_LOG`) when a Windows launcher or a handover starts it. */
|
|
19
|
+
export const AGENT_LOG_ENV = "SCOPEBOND_AGENT_LOG";
|
|
20
|
+
/** Written into every Windows launcher this version makes. A launcher without it redirects the agent's output into
|
|
21
|
+
* agent.log itself, which keeps a replacement agent from starting (see `spawnReplacement`). */
|
|
22
|
+
export const LAUNCHER_MARK = "rem Scopebond Agent launcher 2:";
|
|
23
|
+
/** Whether a launcher's text is this version's (an older one is rewritten once no agent runs under it). */
|
|
24
|
+
export function launcherIsCurrent(text, platform = process.platform) {
|
|
25
|
+
return platform !== "win32" || text.includes(LAUNCHER_MARK);
|
|
26
|
+
}
|
|
17
27
|
export function launcherPath(home, platform = process.platform) {
|
|
18
28
|
return join(home, platform === "win32" ? "agent-launch.cmd" : "agent-launch.sh");
|
|
19
29
|
}
|
|
30
|
+
/** The Windows launcher. With `cli` empty, `node` is the single executable: it is started as it is, with no Node to find. */
|
|
20
31
|
export function windowsLauncher(node, cli, logFile) {
|
|
32
|
+
const log = `"%~dp0${win32.basename(noQuotes(logFile))}"`;
|
|
33
|
+
const single = cli === "";
|
|
21
34
|
return [
|
|
22
35
|
"@echo off",
|
|
23
36
|
// cmd.exe reads a batch file in the console code page: switch to UTF-8 first, so a profile folder
|
|
24
37
|
// with non-ASCII letters in the paths below reads as written.
|
|
25
38
|
"chcp 65001 >nul",
|
|
26
|
-
|
|
39
|
+
`${LAUNCHER_MARK} finds Node and the agent each time, so a Node upgrade never stops it.`,
|
|
27
40
|
"setlocal",
|
|
41
|
+
// The agent writes its own log. A `>> agent.log` here would hold the file shared for reading only, the old agent's
|
|
42
|
+
// children inherit that handle, and a replacement started during an update could then never open it: cmd skips the
|
|
43
|
+
// whole command line when a redirect fails. The log sits beside this launcher; %~dp0 keeps it right whatever the folder is called.
|
|
44
|
+
`set "${AGENT_LOG_ENV}=%~dp0${win32.basename(noQuotes(logFile))}"`,
|
|
28
45
|
`set "NODE=${noQuotes(node)}"`,
|
|
29
|
-
`if not exist "%NODE%"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
...(single ? [`if not exist "%NODE%" exit /b 1`] : [
|
|
47
|
+
`if not exist "%NODE%" set "NODE="`,
|
|
48
|
+
`if not defined NODE for /f "delims=" %%i in ('where node 2^>nul') do if not defined NODE set "NODE=%%i"`,
|
|
49
|
+
`set "CLI=${noQuotes(cli)}"`,
|
|
50
|
+
`if not exist "%CLI%" for /f "delims=" %%i in ('npm.cmd root -g 2^>nul') do set "CLI=%%i\\@scopebond\\agent\\dist\\cli.js"`,
|
|
51
|
+
`if not defined NODE exit /b 1`,
|
|
52
|
+
]),
|
|
34
53
|
// Restart on failure, as launchd (KeepAlive) and systemd (Restart=on-failure) do: a crash restarts the agent after
|
|
35
54
|
// 30 seconds, up to 50 times; a clean exit (stop, autostart off, an update handing over) ends the launcher.
|
|
36
55
|
"set /a TRIES=0",
|
|
37
56
|
":run",
|
|
38
|
-
//
|
|
39
|
-
|
|
57
|
+
// (call) sets the error level to 1 first, so a command that never ran counts as a failure, not a clean stop.
|
|
58
|
+
"(call)",
|
|
59
|
+
single ? `"%NODE%" run >nul 2>&1` : `"%NODE%" --disable-warning=ExperimentalWarning "%CLI%" run >nul 2>&1`,
|
|
40
60
|
`set "CODE=%ERRORLEVEL%"`,
|
|
61
|
+
// An update's handover names the agent to wait for; a restart after a crash has nothing to wait for.
|
|
62
|
+
`set "SCOPEBOND_AGENT_AFTER_PID="`,
|
|
41
63
|
"if %CODE% EQU 0 exit /b 0",
|
|
42
64
|
"set /a TRIES+=1",
|
|
43
|
-
`echo %DATE% %TIME% the agent stopped with exit code %CODE%; restarting in 30 seconds (attempt %TRIES% of 50) >>
|
|
65
|
+
`echo %DATE% %TIME% the agent stopped with exit code %CODE%; restarting in 30 seconds (attempt %TRIES% of 50) >> ${log}`,
|
|
44
66
|
"if %TRIES% GEQ 50 exit /b 1",
|
|
45
67
|
// ping waits without a console to read from (timeout.exe refuses to run under a headless console).
|
|
46
68
|
"ping -n 31 127.0.0.1 >nul",
|
|
@@ -48,8 +70,14 @@ export function windowsLauncher(node, cli, logFile) {
|
|
|
48
70
|
"",
|
|
49
71
|
].join("\r\n");
|
|
50
72
|
}
|
|
73
|
+
/** The macOS/Linux launcher. With `cli` empty, `node` is the single executable, started as it is. */
|
|
51
74
|
export function posixLauncher(node, cli) {
|
|
52
75
|
const q = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
76
|
+
if (cli === "")
|
|
77
|
+
return `#!/bin/sh
|
|
78
|
+
# Scopebond Agent launcher: the single executable.
|
|
79
|
+
exec ${q(node)} run
|
|
80
|
+
`;
|
|
53
81
|
return `#!/bin/sh
|
|
54
82
|
# Scopebond Agent launcher: finds Node and the agent each time, so a Node upgrade never stops it.
|
|
55
83
|
NODE=${q(node)}
|
|
@@ -111,7 +139,29 @@ export function autostartPaths(home = homedir()) {
|
|
|
111
139
|
linuxUnit: join(home, ".config", "systemd", "user", "scopebond-agent.service"),
|
|
112
140
|
};
|
|
113
141
|
}
|
|
142
|
+
/** Rewrite an older Windows launcher as this version's, keeping autostart as it is. Only safe once no cmd.exe runs the old
|
|
143
|
+
* one (cmd reads a batch file line by line from where it left off): the handover calls it after the old agent has exited.
|
|
144
|
+
* Returns whether it rewrote one. */
|
|
145
|
+
export function refreshLauncher(scopebondHome, cli, node = process.execPath, platform = process.platform) {
|
|
146
|
+
const launcher = launcherPath(scopebondHome, platform);
|
|
147
|
+
let text;
|
|
148
|
+
try {
|
|
149
|
+
text = readFileSync(launcher, "utf8");
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (launcherIsCurrent(text, platform))
|
|
155
|
+
return false;
|
|
156
|
+
writeLauncher(scopebondHome, node, cli, platform);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
114
159
|
function writeLauncher(scopebondHome, node, cli, platform) {
|
|
160
|
+
// The single executable is both Node and the agent: the launcher starts it with `run`.
|
|
161
|
+
if (isSingleExecutable()) {
|
|
162
|
+
node = process.execPath;
|
|
163
|
+
cli = "";
|
|
164
|
+
}
|
|
115
165
|
mkdirSync(scopebondHome, { recursive: true });
|
|
116
166
|
const launcher = launcherPath(scopebondHome, platform);
|
|
117
167
|
const log = join(scopebondHome, "agent.log");
|
package/dist/autostart.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autostart.js","sourceRoot":"","sources":["../src/autostart.ts"],"names":[],"mappings":"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","sourcesContent":["// Start the agent when the person signs in to the computer, per user, with no administrator\n// rights: a Run entry on Windows, a LaunchAgent on macOS, a systemd user unit on Linux.\n//\n// Each of them starts a small launcher script in the Scopebond home, not Node directly. The\n// launcher finds Node and the agent when it runs (the recorded paths first, then the system's),\n// so upgrading Node or switching versions never leaves an autostart entry pointing at nothing.\n// On Windows it runs under `conhost --headless`, so no window opens at sign-in.\n\nimport { execFileSync, spawn } from \"node:child_process\";\nimport { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, win32 } from \"node:path\";\n\nexport const LABEL = \"com.scopebond.agent\";\nconst RUN_KEY = \"HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\";\nconst RUN_VALUE = \"ScopebondAgent\";\n\nconst xml = (s: string) => s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\nconst noQuotes = (s: string) => s.replace(/\"/g, \"\");\n\nexport function launcherPath(home: string, platform: NodeJS.Platform = process.platform): string {\n return join(home, platform === \"win32\" ? \"agent-launch.cmd\" : \"agent-launch.sh\");\n}\n\nexport function windowsLauncher(node: string, cli: string, logFile: string): string {\n return [\n \"@echo off\",\n // cmd.exe reads a batch file in the console code page: switch to UTF-8 first, so a profile folder\n // with non-ASCII letters in the paths below reads as written.\n \"chcp 65001 >nul\",\n \"rem Scopebond Agent launcher: finds Node and the agent each time, so a Node upgrade never stops it.\",\n \"setlocal\",\n `set \"NODE=${noQuotes(node)}\"`,\n `if not exist \"%NODE%\" set \"NODE=\"`,\n `if not defined NODE for /f \"delims=\" %%i in ('where node 2^>nul') do if not defined NODE set \"NODE=%%i\"`,\n `set \"CLI=${noQuotes(cli)}\"`,\n `if not exist \"%CLI%\" for /f \"delims=\" %%i in ('npm.cmd root -g 2^>nul') do set \"CLI=%%i\\\\@scopebond\\\\agent\\\\dist\\\\cli.js\"`,\n `if not defined NODE exit /b 1`,\n // Restart on failure, as launchd (KeepAlive) and systemd (Restart=on-failure) do: a crash restarts the agent after\n // 30 seconds, up to 50 times; a clean exit (stop, autostart off, an update handing over) ends the launcher.\n \"set /a TRIES=0\",\n \":run\",\n // The log sits beside this launcher; %~dp0 keeps it right whatever the folder is called.\n `\"%NODE%\" --disable-warning=ExperimentalWarning \"%CLI%\" run >> \"%~dp0${win32.basename(noQuotes(logFile))}\" 2>&1`,\n `set \"CODE=%ERRORLEVEL%\"`,\n \"if %CODE% EQU 0 exit /b 0\",\n \"set /a TRIES+=1\",\n `echo %DATE% %TIME% the agent stopped with exit code %CODE%; restarting in 30 seconds (attempt %TRIES% of 50) >> \"%~dp0${win32.basename(noQuotes(logFile))}\"`,\n \"if %TRIES% GEQ 50 exit /b 1\",\n // ping waits without a console to read from (timeout.exe refuses to run under a headless console).\n \"ping -n 31 127.0.0.1 >nul\",\n \"goto run\",\n \"\",\n ].join(\"\\r\\n\");\n}\n\nexport function posixLauncher(node: string, cli: string): string {\n const q = (s: string) => `'${s.replace(/'/g, \"'\\\\''\")}'`;\n return `#!/bin/sh\n# Scopebond Agent launcher: finds Node and the agent each time, so a Node upgrade never stops it.\nNODE=${q(node)}\nif [ ! -x \"$NODE\" ]; then\n [ -s \"$HOME/.nvm/nvm.sh\" ] && . \"$HOME/.nvm/nvm.sh\" >/dev/null 2>&1\n NODE=\"$(command -v node 2>/dev/null)\"\n for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do\n [ -n \"$NODE\" ] && break\n [ -x \"$candidate\" ] && NODE=\"$candidate\"\n done\nfi\nCLI=${q(cli)}\n[ -f \"$CLI\" ] || CLI=\"$(npm root -g 2>/dev/null)/@scopebond/agent/dist/cli.js\"\n[ -n \"$NODE\" ] || exit 1\nexec \"$NODE\" --disable-warning=ExperimentalWarning \"$CLI\" run\n`;\n}\n\n/** The Run value: the launcher under a headless console host, so nothing appears on screen. */\n/** `cmd /s /c \"\"<launcher>\"\"`: cmd drops only the outer quotes and runs the quoted path as it is, so a\n * profile folder with a space and a `(`, `)` or `&` (\"John (Work)\") still starts the launcher. */\nconst cmdRun = (launcher: string) => `/d /s /c \"\"${noQuotes(launcher)}\"\"`;\n\nexport function windowsRunCommand(launcher: string): string {\n return `conhost.exe --headless cmd.exe ${cmdRun(launcher)}`;\n}\n\nexport function macLaunchAgent(launcher: string, logFile: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key><string>${LABEL}</string>\n <key>ProgramArguments</key>\n <array><string>/bin/sh</string><string>${xml(launcher)}</string></array>\n <key>RunAtLoad</key><true/>\n <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n <key>StandardOutPath</key><string>${xml(logFile)}</string>\n <key>StandardErrorPath</key><string>${xml(logFile)}</string>\n</dict>\n</plist>\n`;\n}\n\nexport function linuxUserUnit(launcher: string): string {\n const q = (s: string) => `\"${s.replace(/([\"\\\\])/g, \"\\\\$1\")}\"`;\n return `[Unit]\nDescription=Scopebond Agent (delivers this computer's records to its Scopebond workspace)\nAfter=network-online.target\n\n[Service]\nExecStart=/bin/sh ${q(launcher)}\nRestart=on-failure\nRestartSec=30\n\n[Install]\nWantedBy=default.target\n`;\n}\n\nexport function autostartPaths(home = homedir()) {\n return {\n macPlist: join(home, \"Library\", \"LaunchAgents\", `${LABEL}.plist`),\n linuxUnit: join(home, \".config\", \"systemd\", \"user\", \"scopebond-agent.service\"),\n };\n}\n\nfunction writeLauncher(scopebondHome: string, node: string, cli: string, platform: NodeJS.Platform): string {\n mkdirSync(scopebondHome, { recursive: true });\n const launcher = launcherPath(scopebondHome, platform);\n const log = join(scopebondHome, \"agent.log\");\n writeFileSync(launcher, platform === \"win32\" ? windowsLauncher(node, cli, log) : posixLauncher(node, cli), { mode: 0o700 });\n if (platform !== \"win32\") chmodSync(launcher, 0o700);\n return launcher;\n}\n\n/** The ways to start the agent now on Windows, in order: the launcher under a headless console (no window,\n * as sign-in does), then the launcher through cmd.exe with its window hidden, for a session where the\n * headless console host does not start (seen on Windows Server). macOS and Linux start it themselves\n * when autostart is turned on (RunAtLoad, enable --now). */\nexport function startCommands(launcher: string, platform: NodeJS.Platform = process.platform): Array<[string, string[]]> {\n if (platform !== \"win32\") return [];\n // Passed verbatim (startNow sets windowsVerbatimArguments): Node's own quoting follows other rules than cmd's.\n return [[\"conhost.exe\", [\"--headless\", \"cmd.exe\", cmdRun(launcher)]], [\"cmd.exe\", [cmdRun(launcher)]]];\n}\n\n/** Start the agent now with the `attempt`-th way (0 first), detached from this terminal. Returns whether it tried one. */\nexport function startNow(scopebondHome: string, platform = process.platform, attempt = 0): boolean {\n const launcher = launcherPath(scopebondHome, platform);\n const command = startCommands(launcher, platform)[attempt];\n if (!command || !existsSync(launcher)) return false;\n try {\n const child = spawn(command[0], command[1], { detached: true, stdio: \"ignore\", windowsHide: true, windowsVerbatimArguments: true });\n child.on(\"error\", () => { /* the next way, or the next sign-in */ });\n child.unref();\n return true;\n } catch { return false; }\n}\n\n/** Turn autostart on for this user. Returns a one-line description of what changed. */\nexport function enableAutostart(scopebondHome: string, cli: string, node = process.execPath, platform = process.platform): string {\n const launcher = writeLauncher(scopebondHome, node, cli, platform);\n const paths = autostartPaths();\n if (platform === \"win32\") {\n execFileSync(\"reg\", [\"add\", RUN_KEY, \"/v\", RUN_VALUE, \"/t\", \"REG_SZ\", \"/d\", windowsRunCommand(launcher), \"/f\"], { stdio: \"ignore\" });\n return `added ${RUN_VALUE} to ${RUN_KEY} (launcher ${launcher})`;\n }\n if (platform === \"darwin\") {\n mkdirSync(dirname(paths.macPlist), { recursive: true });\n writeFileSync(paths.macPlist, macLaunchAgent(launcher, join(scopebondHome, \"agent.log\")));\n try { execFileSync(\"launchctl\", [\"load\", \"-w\", paths.macPlist], { stdio: \"ignore\" }); } catch { /* loads at next sign-in */ }\n return `wrote ${paths.macPlist}`;\n }\n mkdirSync(dirname(paths.linuxUnit), { recursive: true });\n writeFileSync(paths.linuxUnit, linuxUserUnit(launcher));\n try { execFileSync(\"systemctl\", [\"--user\", \"daemon-reload\"], { stdio: \"ignore\" }); execFileSync(\"systemctl\", [\"--user\", \"enable\", \"--now\", \"scopebond-agent.service\"], { stdio: \"ignore\" }); }\n catch { return `wrote ${paths.linuxUnit} (enable it with: systemctl --user enable --now scopebond-agent)`; }\n return `wrote and enabled ${paths.linuxUnit}`;\n}\n\nexport function disableAutostart(scopebondHome: string, platform = process.platform): string {\n const paths = autostartPaths();\n rmSync(launcherPath(scopebondHome, platform), { force: true });\n if (platform === \"win32\") {\n try { execFileSync(\"reg\", [\"delete\", RUN_KEY, \"/v\", RUN_VALUE, \"/f\"], { stdio: \"ignore\" }); } catch { return \"autostart was not on\"; }\n return `removed ${RUN_VALUE} from ${RUN_KEY}`;\n }\n if (platform === \"darwin\") {\n if (!existsSync(paths.macPlist)) return \"autostart was not on\";\n try { execFileSync(\"launchctl\", [\"unload\", \"-w\", paths.macPlist], { stdio: \"ignore\" }); } catch { /* not loaded */ }\n rmSync(paths.macPlist, { force: true });\n return `removed ${paths.macPlist}`;\n }\n if (!existsSync(paths.linuxUnit)) return \"autostart was not on\";\n try { execFileSync(\"systemctl\", [\"--user\", \"disable\", \"--now\", \"scopebond-agent.service\"], { stdio: \"ignore\" }); } catch { /* not enabled */ }\n rmSync(paths.linuxUnit, { force: true });\n return `removed ${paths.linuxUnit}`;\n}\n\n/** Whether autostart is on and its launcher can start the agent now. */\nexport function autostartHealth(scopebondHome: string, platform = process.platform): { on: boolean; ok: boolean; detail: string } {\n const paths = autostartPaths();\n let on = false;\n if (platform === \"win32\") {\n try { execFileSync(\"reg\", [\"query\", RUN_KEY, \"/v\", RUN_VALUE], { stdio: \"ignore\" }); on = true; } catch { on = false; }\n } else on = existsSync(platform === \"darwin\" ? paths.macPlist : paths.linuxUnit);\n // The fix as the person types it: PowerShell blocks the plain scopebond-agent script shim.\n const fix = `${platform === \"win32\" ? \"scopebond-agent.cmd\" : \"scopebond-agent\"} autostart on`;\n if (!on) return { on, ok: false, detail: `the agent does not start with sign-in (run: ${fix})` };\n if (!existsSync(launcherPath(scopebondHome, platform))) return { on, ok: false, detail: `the autostart launcher is missing (run: ${fix})` };\n return { on, ok: true, detail: \"starts with sign-in\" };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"autostart.js","sourceRoot":"","sources":["../src/autostart.ts"],"names":[],"mappings":"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","sourcesContent":["// Start the agent when the person signs in to the computer, per user, with no administrator\n// rights: a Run entry on Windows, a LaunchAgent on macOS, a systemd user unit on Linux.\n//\n// Each of them starts a small launcher script in the Scopebond home, not Node directly. The\n// launcher finds Node and the agent when it runs (the recorded paths first, then the system's),\n// so upgrading Node or switching versions never leaves an autostart entry pointing at nothing.\n// On Windows it runs under `conhost --headless`, so no window opens at sign-in.\n\nimport { execFileSync, spawn } from \"node:child_process\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, win32 } from \"node:path\";\nimport { isSingleExecutable } from \"@scopebond/hook\";\n\nexport const LABEL = \"com.scopebond.agent\";\nconst RUN_KEY = \"HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\";\nconst RUN_VALUE = \"ScopebondAgent\";\n\nconst xml = (s: string) => s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\");\nconst noQuotes = (s: string) => s.replace(/\"/g, \"\");\n\n/** The agent's log, which the agent writes itself (`SCOPEBOND_AGENT_LOG`) when a Windows launcher or a handover starts it. */\nexport const AGENT_LOG_ENV = \"SCOPEBOND_AGENT_LOG\";\n\n/** Written into every Windows launcher this version makes. A launcher without it redirects the agent's output into\n * agent.log itself, which keeps a replacement agent from starting (see `spawnReplacement`). */\nexport const LAUNCHER_MARK = \"rem Scopebond Agent launcher 2:\";\n\n/** Whether a launcher's text is this version's (an older one is rewritten once no agent runs under it). */\nexport function launcherIsCurrent(text: string, platform: NodeJS.Platform = process.platform): boolean {\n return platform !== \"win32\" || text.includes(LAUNCHER_MARK);\n}\n\nexport function launcherPath(home: string, platform: NodeJS.Platform = process.platform): string {\n return join(home, platform === \"win32\" ? \"agent-launch.cmd\" : \"agent-launch.sh\");\n}\n\n/** The Windows launcher. With `cli` empty, `node` is the single executable: it is started as it is, with no Node to find. */\nexport function windowsLauncher(node: string, cli: string, logFile: string): string {\n const log = `\"%~dp0${win32.basename(noQuotes(logFile))}\"`;\n const single = cli === \"\";\n return [\n \"@echo off\",\n // cmd.exe reads a batch file in the console code page: switch to UTF-8 first, so a profile folder\n // with non-ASCII letters in the paths below reads as written.\n \"chcp 65001 >nul\",\n `${LAUNCHER_MARK} finds Node and the agent each time, so a Node upgrade never stops it.`,\n \"setlocal\",\n // The agent writes its own log. A `>> agent.log` here would hold the file shared for reading only, the old agent's\n // children inherit that handle, and a replacement started during an update could then never open it: cmd skips the\n // whole command line when a redirect fails. The log sits beside this launcher; %~dp0 keeps it right whatever the folder is called.\n `set \"${AGENT_LOG_ENV}=%~dp0${win32.basename(noQuotes(logFile))}\"`,\n `set \"NODE=${noQuotes(node)}\"`,\n ...(single ? [`if not exist \"%NODE%\" exit /b 1`] : [\n `if not exist \"%NODE%\" set \"NODE=\"`,\n `if not defined NODE for /f \"delims=\" %%i in ('where node 2^>nul') do if not defined NODE set \"NODE=%%i\"`,\n `set \"CLI=${noQuotes(cli)}\"`,\n `if not exist \"%CLI%\" for /f \"delims=\" %%i in ('npm.cmd root -g 2^>nul') do set \"CLI=%%i\\\\@scopebond\\\\agent\\\\dist\\\\cli.js\"`,\n `if not defined NODE exit /b 1`,\n ]),\n // Restart on failure, as launchd (KeepAlive) and systemd (Restart=on-failure) do: a crash restarts the agent after\n // 30 seconds, up to 50 times; a clean exit (stop, autostart off, an update handing over) ends the launcher.\n \"set /a TRIES=0\",\n \":run\",\n // (call) sets the error level to 1 first, so a command that never ran counts as a failure, not a clean stop.\n \"(call)\",\n single ? `\"%NODE%\" run >nul 2>&1` : `\"%NODE%\" --disable-warning=ExperimentalWarning \"%CLI%\" run >nul 2>&1`,\n `set \"CODE=%ERRORLEVEL%\"`,\n // An update's handover names the agent to wait for; a restart after a crash has nothing to wait for.\n `set \"SCOPEBOND_AGENT_AFTER_PID=\"`,\n \"if %CODE% EQU 0 exit /b 0\",\n \"set /a TRIES+=1\",\n `echo %DATE% %TIME% the agent stopped with exit code %CODE%; restarting in 30 seconds (attempt %TRIES% of 50) >> ${log}`,\n \"if %TRIES% GEQ 50 exit /b 1\",\n // ping waits without a console to read from (timeout.exe refuses to run under a headless console).\n \"ping -n 31 127.0.0.1 >nul\",\n \"goto run\",\n \"\",\n ].join(\"\\r\\n\");\n}\n\n/** The macOS/Linux launcher. With `cli` empty, `node` is the single executable, started as it is. */\nexport function posixLauncher(node: string, cli: string): string {\n const q = (s: string) => `'${s.replace(/'/g, \"'\\\\''\")}'`;\n if (cli === \"\") return `#!/bin/sh\n# Scopebond Agent launcher: the single executable.\nexec ${q(node)} run\n`;\n return `#!/bin/sh\n# Scopebond Agent launcher: finds Node and the agent each time, so a Node upgrade never stops it.\nNODE=${q(node)}\nif [ ! -x \"$NODE\" ]; then\n [ -s \"$HOME/.nvm/nvm.sh\" ] && . \"$HOME/.nvm/nvm.sh\" >/dev/null 2>&1\n NODE=\"$(command -v node 2>/dev/null)\"\n for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do\n [ -n \"$NODE\" ] && break\n [ -x \"$candidate\" ] && NODE=\"$candidate\"\n done\nfi\nCLI=${q(cli)}\n[ -f \"$CLI\" ] || CLI=\"$(npm root -g 2>/dev/null)/@scopebond/agent/dist/cli.js\"\n[ -n \"$NODE\" ] || exit 1\nexec \"$NODE\" --disable-warning=ExperimentalWarning \"$CLI\" run\n`;\n}\n\n/** The Run value: the launcher under a headless console host, so nothing appears on screen. */\n/** `cmd /s /c \"\"<launcher>\"\"`: cmd drops only the outer quotes and runs the quoted path as it is, so a\n * profile folder with a space and a `(`, `)` or `&` (\"John (Work)\") still starts the launcher. */\nconst cmdRun = (launcher: string) => `/d /s /c \"\"${noQuotes(launcher)}\"\"`;\n\nexport function windowsRunCommand(launcher: string): string {\n return `conhost.exe --headless cmd.exe ${cmdRun(launcher)}`;\n}\n\nexport function macLaunchAgent(launcher: string, logFile: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>Label</key><string>${LABEL}</string>\n <key>ProgramArguments</key>\n <array><string>/bin/sh</string><string>${xml(launcher)}</string></array>\n <key>RunAtLoad</key><true/>\n <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n <key>StandardOutPath</key><string>${xml(logFile)}</string>\n <key>StandardErrorPath</key><string>${xml(logFile)}</string>\n</dict>\n</plist>\n`;\n}\n\nexport function linuxUserUnit(launcher: string): string {\n const q = (s: string) => `\"${s.replace(/([\"\\\\])/g, \"\\\\$1\")}\"`;\n return `[Unit]\nDescription=Scopebond Agent (delivers this computer's records to its Scopebond workspace)\nAfter=network-online.target\n\n[Service]\nExecStart=/bin/sh ${q(launcher)}\nRestart=on-failure\nRestartSec=30\n\n[Install]\nWantedBy=default.target\n`;\n}\n\nexport function autostartPaths(home = homedir()) {\n return {\n macPlist: join(home, \"Library\", \"LaunchAgents\", `${LABEL}.plist`),\n linuxUnit: join(home, \".config\", \"systemd\", \"user\", \"scopebond-agent.service\"),\n };\n}\n\n/** Rewrite an older Windows launcher as this version's, keeping autostart as it is. Only safe once no cmd.exe runs the old\n * one (cmd reads a batch file line by line from where it left off): the handover calls it after the old agent has exited.\n * Returns whether it rewrote one. */\nexport function refreshLauncher(scopebondHome: string, cli: string, node = process.execPath, platform = process.platform): boolean {\n const launcher = launcherPath(scopebondHome, platform);\n let text: string;\n try { text = readFileSync(launcher, \"utf8\"); } catch { return false; }\n if (launcherIsCurrent(text, platform)) return false;\n writeLauncher(scopebondHome, node, cli, platform);\n return true;\n}\n\nfunction writeLauncher(scopebondHome: string, node: string, cli: string, platform: NodeJS.Platform): string {\n // The single executable is both Node and the agent: the launcher starts it with `run`.\n if (isSingleExecutable()) { node = process.execPath; cli = \"\"; }\n mkdirSync(scopebondHome, { recursive: true });\n const launcher = launcherPath(scopebondHome, platform);\n const log = join(scopebondHome, \"agent.log\");\n writeFileSync(launcher, platform === \"win32\" ? windowsLauncher(node, cli, log) : posixLauncher(node, cli), { mode: 0o700 });\n if (platform !== \"win32\") chmodSync(launcher, 0o700);\n return launcher;\n}\n\n/** The ways to start the agent now on Windows, in order: the launcher under a headless console (no window,\n * as sign-in does), then the launcher through cmd.exe with its window hidden, for a session where the\n * headless console host does not start (seen on Windows Server). macOS and Linux start it themselves\n * when autostart is turned on (RunAtLoad, enable --now). */\nexport function startCommands(launcher: string, platform: NodeJS.Platform = process.platform): Array<[string, string[]]> {\n if (platform !== \"win32\") return [];\n // Passed verbatim (startNow sets windowsVerbatimArguments): Node's own quoting follows other rules than cmd's.\n return [[\"conhost.exe\", [\"--headless\", \"cmd.exe\", cmdRun(launcher)]], [\"cmd.exe\", [cmdRun(launcher)]]];\n}\n\n/** Start the agent now with the `attempt`-th way (0 first), detached from this terminal. Returns whether it tried one. */\nexport function startNow(scopebondHome: string, platform = process.platform, attempt = 0): boolean {\n const launcher = launcherPath(scopebondHome, platform);\n const command = startCommands(launcher, platform)[attempt];\n if (!command || !existsSync(launcher)) return false;\n try {\n const child = spawn(command[0], command[1], { detached: true, stdio: \"ignore\", windowsHide: true, windowsVerbatimArguments: true });\n child.on(\"error\", () => { /* the next way, or the next sign-in */ });\n child.unref();\n return true;\n } catch { return false; }\n}\n\n/** Turn autostart on for this user. Returns a one-line description of what changed. */\nexport function enableAutostart(scopebondHome: string, cli: string, node = process.execPath, platform = process.platform): string {\n const launcher = writeLauncher(scopebondHome, node, cli, platform);\n const paths = autostartPaths();\n if (platform === \"win32\") {\n execFileSync(\"reg\", [\"add\", RUN_KEY, \"/v\", RUN_VALUE, \"/t\", \"REG_SZ\", \"/d\", windowsRunCommand(launcher), \"/f\"], { stdio: \"ignore\" });\n return `added ${RUN_VALUE} to ${RUN_KEY} (launcher ${launcher})`;\n }\n if (platform === \"darwin\") {\n mkdirSync(dirname(paths.macPlist), { recursive: true });\n writeFileSync(paths.macPlist, macLaunchAgent(launcher, join(scopebondHome, \"agent.log\")));\n try { execFileSync(\"launchctl\", [\"load\", \"-w\", paths.macPlist], { stdio: \"ignore\" }); } catch { /* loads at next sign-in */ }\n return `wrote ${paths.macPlist}`;\n }\n mkdirSync(dirname(paths.linuxUnit), { recursive: true });\n writeFileSync(paths.linuxUnit, linuxUserUnit(launcher));\n try { execFileSync(\"systemctl\", [\"--user\", \"daemon-reload\"], { stdio: \"ignore\" }); execFileSync(\"systemctl\", [\"--user\", \"enable\", \"--now\", \"scopebond-agent.service\"], { stdio: \"ignore\" }); }\n catch { return `wrote ${paths.linuxUnit} (enable it with: systemctl --user enable --now scopebond-agent)`; }\n return `wrote and enabled ${paths.linuxUnit}`;\n}\n\nexport function disableAutostart(scopebondHome: string, platform = process.platform): string {\n const paths = autostartPaths();\n rmSync(launcherPath(scopebondHome, platform), { force: true });\n if (platform === \"win32\") {\n try { execFileSync(\"reg\", [\"delete\", RUN_KEY, \"/v\", RUN_VALUE, \"/f\"], { stdio: \"ignore\" }); } catch { return \"autostart was not on\"; }\n return `removed ${RUN_VALUE} from ${RUN_KEY}`;\n }\n if (platform === \"darwin\") {\n if (!existsSync(paths.macPlist)) return \"autostart was not on\";\n try { execFileSync(\"launchctl\", [\"unload\", \"-w\", paths.macPlist], { stdio: \"ignore\" }); } catch { /* not loaded */ }\n rmSync(paths.macPlist, { force: true });\n return `removed ${paths.macPlist}`;\n }\n if (!existsSync(paths.linuxUnit)) return \"autostart was not on\";\n try { execFileSync(\"systemctl\", [\"--user\", \"disable\", \"--now\", \"scopebond-agent.service\"], { stdio: \"ignore\" }); } catch { /* not enabled */ }\n rmSync(paths.linuxUnit, { force: true });\n return `removed ${paths.linuxUnit}`;\n}\n\n/** Whether autostart is on and its launcher can start the agent now. */\nexport function autostartHealth(scopebondHome: string, platform = process.platform): { on: boolean; ok: boolean; detail: string } {\n const paths = autostartPaths();\n let on = false;\n if (platform === \"win32\") {\n try { execFileSync(\"reg\", [\"query\", RUN_KEY, \"/v\", RUN_VALUE], { stdio: \"ignore\" }); on = true; } catch { on = false; }\n } else on = existsSync(platform === \"darwin\" ? paths.macPlist : paths.linuxUnit);\n // The fix as the person types it: PowerShell blocks the plain scopebond-agent script shim.\n const fix = `${platform === \"win32\" ? \"scopebond-agent.cmd\" : \"scopebond-agent\"} autostart on`;\n if (!on) return { on, ok: false, detail: `the agent does not start with sign-in (run: ${fix})` };\n if (!existsSync(launcherPath(scopebondHome, platform))) return { on, ok: false, detail: `the autostart launcher is missing (run: ${fix})` };\n return { on, ok: true, detail: \"starts with sign-in\" };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-main.d.ts","sourceRoot":"","sources":["../src/cli-main.ts"],"names":[],"mappings":"AAiDA,8HAA8H;AAC9H,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAuGhF"}
|