@parlor/agent-host 0.0.0-stage → 0.2.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 -0
- package/README.md +16 -2
- package/dist/acp.d.ts +165 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +626 -0
- package/dist/acp.js.map +1 -0
- package/dist/adapter-launch.d.ts +7 -0
- package/dist/adapter-launch.d.ts.map +1 -0
- package/dist/adapter-launch.js +35 -0
- package/dist/adapter-launch.js.map +1 -0
- package/dist/agents.d.ts +32 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +132 -0
- package/dist/agents.js.map +1 -0
- package/dist/assertion.d.ts +9 -0
- package/dist/assertion.d.ts.map +1 -0
- package/dist/assertion.js +22 -0
- package/dist/assertion.js.map +1 -0
- package/dist/control.d.ts +106 -0
- package/dist/control.d.ts.map +1 -0
- package/dist/control.js +32 -0
- package/dist/control.js.map +1 -0
- package/dist/daemon.d.ts +50 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +543 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dev-host.d.ts +5 -0
- package/dist/dev-host.d.ts.map +1 -0
- package/dist/dev-host.js +29 -0
- package/dist/dev-host.js.map +1 -0
- package/dist/device-list.d.ts +6 -0
- package/dist/device-list.d.ts.map +1 -0
- package/dist/device-list.js +30 -0
- package/dist/device-list.js.map +1 -0
- package/dist/embedded.d.ts +6 -0
- package/dist/embedded.d.ts.map +1 -0
- package/dist/embedded.js +3 -0
- package/dist/embedded.js.map +1 -0
- package/dist/fold.d.ts +38 -0
- package/dist/fold.d.ts.map +1 -0
- package/dist/fold.js +165 -0
- package/dist/fold.js.map +1 -0
- package/dist/grants.d.ts +54 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +157 -0
- package/dist/grants.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/json-file.d.ts +6 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +35 -0
- package/dist/json-file.js.map +1 -0
- package/dist/lines.d.ts +5 -0
- package/dist/lines.d.ts.map +1 -0
- package/dist/lines.js +9 -0
- package/dist/lines.js.map +1 -0
- package/dist/link-device.d.ts +28 -0
- package/dist/link-device.d.ts.map +1 -0
- package/dist/link-device.js +40 -0
- package/dist/link-device.js.map +1 -0
- package/dist/link.d.ts +52 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +277 -0
- package/dist/link.js.map +1 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +105 -0
- package/dist/lock.js.map +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +18 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +520 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp-proxy.d.ts +33 -0
- package/dist/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp-proxy.js +317 -0
- package/dist/mcp-proxy.js.map +1 -0
- package/dist/naming.d.ts +7 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +42 -0
- package/dist/naming.js.map +1 -0
- package/dist/pairing.d.ts +68 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +312 -0
- package/dist/pairing.js.map +1 -0
- package/dist/paths.d.ts +19 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +23 -0
- package/dist/paths.js.map +1 -0
- package/dist/policy.d.ts +37 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +60 -0
- package/dist/policy.js.map +1 -0
- package/dist/pool.d.ts +34 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +112 -0
- package/dist/pool.js.map +1 -0
- package/dist/prompter.d.ts +33 -0
- package/dist/prompter.d.ts.map +1 -0
- package/dist/prompter.js +98 -0
- package/dist/prompter.js.map +1 -0
- package/dist/release-main.d.ts +2 -0
- package/dist/release-main.d.ts.map +1 -0
- package/dist/release-main.js +50 -0
- package/dist/release-main.js.map +1 -0
- package/dist/release.d.ts +22 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +81 -0
- package/dist/release.js.map +1 -0
- package/dist/revoke.d.ts +10 -0
- package/dist/revoke.d.ts.map +1 -0
- package/dist/revoke.js +23 -0
- package/dist/revoke.js.map +1 -0
- package/dist/self.d.ts +7 -0
- package/dist/self.d.ts.map +1 -0
- package/dist/self.js +10 -0
- package/dist/self.js.map +1 -0
- package/dist/service.d.ts +97 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +205 -0
- package/dist/service.js.map +1 -0
- package/dist/shell-path.d.ts +10 -0
- package/dist/shell-path.d.ts.map +1 -0
- package/dist/shell-path.js +64 -0
- package/dist/shell-path.js.map +1 -0
- package/dist/skeleton-kit.d.ts +8 -0
- package/dist/skeleton-kit.d.ts.map +1 -0
- package/dist/skeleton-kit.js +143 -0
- package/dist/skeleton-kit.js.map +1 -0
- package/dist/spec-kit.d.ts +102 -0
- package/dist/spec-kit.d.ts.map +1 -0
- package/dist/spec-kit.js +372 -0
- package/dist/spec-kit.js.map +1 -0
- package/dist/testing/fake-agent.d.ts +65 -0
- package/dist/testing/fake-agent.d.ts.map +1 -0
- package/dist/testing/fake-agent.js +238 -0
- package/dist/testing/fake-agent.js.map +1 -0
- package/dist/trust.d.ts +20 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +27 -0
- package/dist/trust.js.map +1 -0
- package/dist/turn.d.ts +32 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +650 -0
- package/dist/turn.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +57 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ProtocolRange } from "@parlor/chat";
|
|
2
|
+
import type { HostPaths } from "./paths.js";
|
|
3
|
+
import { type ReleaseSignature } from "./release.js";
|
|
4
|
+
export interface HostRelease {
|
|
5
|
+
readonly version: string;
|
|
6
|
+
/** Host protocols it speaks with app backends. */
|
|
7
|
+
readonly protocols: ProtocolRange;
|
|
8
|
+
/** The control protocol it speaks with CLIs and desktop sidecars. */
|
|
9
|
+
readonly control: number;
|
|
10
|
+
}
|
|
11
|
+
export type ServiceClient = {
|
|
12
|
+
readonly kind: "cli";
|
|
13
|
+
readonly release: HostRelease;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "desktop-sidecar";
|
|
16
|
+
readonly app: string;
|
|
17
|
+
readonly release: HostRelease;
|
|
18
|
+
/** The bundled binary, copied in when newer than what is installed. */
|
|
19
|
+
readonly binary: string;
|
|
20
|
+
/** Its `.sig`, `<binary>.sig` when the bundle keeps them together. */
|
|
21
|
+
readonly signature?: string;
|
|
22
|
+
};
|
|
23
|
+
export type AdoptOutcome = {
|
|
24
|
+
readonly kind: "using";
|
|
25
|
+
readonly running: HostRelease;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "installed";
|
|
28
|
+
readonly release: HostRelease;
|
|
29
|
+
}
|
|
30
|
+
/** The old service handed its links over; its running turns finish under it. */
|
|
31
|
+
| {
|
|
32
|
+
readonly kind: "upgraded";
|
|
33
|
+
readonly from: HostRelease;
|
|
34
|
+
readonly to: HostRelease;
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: "refused";
|
|
37
|
+
readonly reason: "unsigned" | "downgrade" | "legacy_service";
|
|
38
|
+
readonly running?: HostRelease;
|
|
39
|
+
};
|
|
40
|
+
export type Situation = {
|
|
41
|
+
readonly service: "absent";
|
|
42
|
+
readonly installed: boolean;
|
|
43
|
+
} | {
|
|
44
|
+
readonly service: "legacy";
|
|
45
|
+
} | {
|
|
46
|
+
readonly service: "running";
|
|
47
|
+
readonly running: HostRelease;
|
|
48
|
+
};
|
|
49
|
+
type Sidecar = Extract<ServiceClient, {
|
|
50
|
+
readonly kind: "desktop-sidecar";
|
|
51
|
+
}>;
|
|
52
|
+
export type Action = {
|
|
53
|
+
readonly do: "start_installed";
|
|
54
|
+
} | {
|
|
55
|
+
readonly do: "spawn_self";
|
|
56
|
+
} | {
|
|
57
|
+
readonly do: "install_bundled";
|
|
58
|
+
readonly client: Sidecar;
|
|
59
|
+
} | {
|
|
60
|
+
readonly do: "refuse_legacy";
|
|
61
|
+
} | {
|
|
62
|
+
readonly do: "use";
|
|
63
|
+
readonly running: HostRelease;
|
|
64
|
+
} | {
|
|
65
|
+
readonly do: "refuse_downgrade";
|
|
66
|
+
readonly running: HostRelease;
|
|
67
|
+
} | {
|
|
68
|
+
readonly do: "hand_over";
|
|
69
|
+
readonly client: Sidecar;
|
|
70
|
+
readonly running: HostRelease;
|
|
71
|
+
};
|
|
72
|
+
export declare function actionFor(situation: Situation, client: ServiceClient): Action;
|
|
73
|
+
export interface ServiceDeps {
|
|
74
|
+
readonly releaseKey: string | undefined;
|
|
75
|
+
readonly readyMs: number;
|
|
76
|
+
readonly pollMs: number;
|
|
77
|
+
readonly start?: ((paths: HostPaths, command: string, args: readonly string[], lockToken: string) => Promise<void>) | undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Copies a verified binary into `versions/<version>/` and repoints `current`, each step
|
|
81
|
+
* atomic and repeatable; false when the copy is not the binary the signature describes.
|
|
82
|
+
*/
|
|
83
|
+
export declare function installRelease(paths: HostPaths, source: {
|
|
84
|
+
readonly binary: string;
|
|
85
|
+
readonly signature: string;
|
|
86
|
+
}, release: ReleaseSignature): Promise<boolean>;
|
|
87
|
+
/** LaunchAgent, systemd user unit or Task Scheduler, pointing at `paths.current`. */
|
|
88
|
+
export declare function installLoginItem(paths: HostPaths): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Leaves exactly one per-user service running, the newest: adopt, install or upgrade.
|
|
91
|
+
* Only binaries signed by the Parlor release key, never a downgrade; installs hold a lock
|
|
92
|
+
* under `paths.root`, so two apps can run it at once.
|
|
93
|
+
*/
|
|
94
|
+
export declare function ensureServiceWith(deps: ServiceDeps, paths: HostPaths, client: ServiceClient): Promise<AdoptOutcome>;
|
|
95
|
+
export declare function ensureService(paths: HostPaths, client: ServiceClient): Promise<AdoptOutcome>;
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, chmod, copyFile, mkdir, open, rename, rm, symlink, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { connectControl } from "./control.js";
|
|
5
|
+
import { LOCK_TOKEN_ENV, withFileLock } from "./lock.js";
|
|
6
|
+
import { compareVersions, RELEASE_KEY, sha256File, signedRelease, } from "./release.js";
|
|
7
|
+
import { selfInvocation } from "./self.js";
|
|
8
|
+
export function actionFor(situation, client) {
|
|
9
|
+
switch (situation.service) {
|
|
10
|
+
case "legacy":
|
|
11
|
+
return { do: "refuse_legacy" };
|
|
12
|
+
case "absent":
|
|
13
|
+
if (situation.installed)
|
|
14
|
+
return { do: "start_installed" };
|
|
15
|
+
return client.kind === "cli"
|
|
16
|
+
? { do: "spawn_self" }
|
|
17
|
+
: { do: "install_bundled", client };
|
|
18
|
+
case "running": {
|
|
19
|
+
const { running } = situation;
|
|
20
|
+
if (client.kind === "cli")
|
|
21
|
+
return { do: "use", running };
|
|
22
|
+
const order = compareVersions(client.release.version, running.version);
|
|
23
|
+
if (order > 0)
|
|
24
|
+
return { do: "hand_over", client, running };
|
|
25
|
+
return order === 0 ? { do: "use", running } : { do: "refuse_downgrade", running };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const DEFAULTS = { releaseKey: RELEASE_KEY, readyMs: 15_000, pollMs: 100 };
|
|
30
|
+
const startWith = (deps) => deps.start ?? spawnService;
|
|
31
|
+
const SERVICE_BINARY = "parlor-host";
|
|
32
|
+
const SIGNATURE_FILE = "parlor-host.sig";
|
|
33
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
const exists = (path) => access(path).then(() => true, () => false);
|
|
35
|
+
async function observe(paths, client) {
|
|
36
|
+
const control = await connectControl(paths.control);
|
|
37
|
+
if (!control) {
|
|
38
|
+
return {
|
|
39
|
+
service: "absent",
|
|
40
|
+
installed: await exists(join(paths.current, SERVICE_BINARY)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const reply = await control.send({ op: "hello", client });
|
|
44
|
+
control.close();
|
|
45
|
+
return reply.ok
|
|
46
|
+
? { service: "running", running: reply.release }
|
|
47
|
+
: { service: "legacy" };
|
|
48
|
+
}
|
|
49
|
+
async function spawnService(paths, command, args, lockToken) {
|
|
50
|
+
await mkdir(paths.logs, { recursive: true, mode: 0o700 });
|
|
51
|
+
const output = await open(join(paths.logs, "daemon.out"), "a", 0o600);
|
|
52
|
+
const child = spawn(command, args, {
|
|
53
|
+
cwd: paths.root,
|
|
54
|
+
detached: true,
|
|
55
|
+
stdio: ["ignore", output.fd, output.fd],
|
|
56
|
+
env: { ...process.env, [LOCK_TOKEN_ENV]: lockToken },
|
|
57
|
+
});
|
|
58
|
+
child.unref();
|
|
59
|
+
await output.close();
|
|
60
|
+
}
|
|
61
|
+
async function untilRunning(paths, client, deps, ready = () => true) {
|
|
62
|
+
for (const deadline = Date.now() + deps.readyMs; Date.now() < deadline;) {
|
|
63
|
+
const situation = await observe(paths, client);
|
|
64
|
+
if (situation.service === "running" && ready(situation.running)) {
|
|
65
|
+
return situation.running;
|
|
66
|
+
}
|
|
67
|
+
await sleep(deps.pollMs);
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Parlor Host did not start; see ${join(paths.logs, "daemon.out")}.`);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Copies a verified binary into `versions/<version>/` and repoints `current`, each step
|
|
73
|
+
* atomic and repeatable; false when the copy is not the binary the signature describes.
|
|
74
|
+
*/
|
|
75
|
+
export async function installRelease(paths, source, release) {
|
|
76
|
+
const target = join(paths.versions, release.version);
|
|
77
|
+
const installed = join(target, SERVICE_BINARY);
|
|
78
|
+
const present = (await sha256File(installed).catch(() => undefined)) === release.sha256;
|
|
79
|
+
if (!present) {
|
|
80
|
+
const staged = join(paths.versions, `.${release.version}.tmp-${process.pid}`);
|
|
81
|
+
await rm(staged, { recursive: true, force: true });
|
|
82
|
+
await mkdir(staged, { recursive: true, mode: 0o700 });
|
|
83
|
+
await copyFile(source.binary, join(staged, SERVICE_BINARY));
|
|
84
|
+
await chmod(join(staged, SERVICE_BINARY), 0o755);
|
|
85
|
+
await copyFile(source.signature, join(staged, SIGNATURE_FILE));
|
|
86
|
+
if ((await sha256File(join(staged, SERVICE_BINARY))) !== release.sha256) {
|
|
87
|
+
await rm(staged, { recursive: true, force: true });
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
await rm(target, { recursive: true, force: true });
|
|
91
|
+
await rename(staged, target);
|
|
92
|
+
}
|
|
93
|
+
const link = `${paths.current}.tmp-${process.pid}`;
|
|
94
|
+
await rm(link, { force: true });
|
|
95
|
+
await symlink(join("versions", release.version), link);
|
|
96
|
+
await rename(link, paths.current);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const escapeXml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
100
|
+
/** LaunchAgent, systemd user unit or Task Scheduler, pointing at `paths.current`. */
|
|
101
|
+
export async function installLoginItem(paths) {
|
|
102
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
103
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
104
|
+
<plist version="1.0">
|
|
105
|
+
<dict>
|
|
106
|
+
<key>Label</key>
|
|
107
|
+
<string>dev.parlor.host</string>
|
|
108
|
+
<key>ProgramArguments</key>
|
|
109
|
+
<array>
|
|
110
|
+
<string>${escapeXml(join(paths.current, SERVICE_BINARY))}</string>
|
|
111
|
+
<string>daemon</string>
|
|
112
|
+
</array>
|
|
113
|
+
<key>RunAtLoad</key>
|
|
114
|
+
<true/>
|
|
115
|
+
<key>KeepAlive</key>
|
|
116
|
+
<dict>
|
|
117
|
+
<key>SuccessfulExit</key>
|
|
118
|
+
<false/>
|
|
119
|
+
</dict>
|
|
120
|
+
<key>StandardOutPath</key>
|
|
121
|
+
<string>${escapeXml(join(paths.logs, "daemon.out"))}</string>
|
|
122
|
+
<key>StandardErrorPath</key>
|
|
123
|
+
<string>${escapeXml(join(paths.logs, "daemon.out"))}</string>
|
|
124
|
+
</dict>
|
|
125
|
+
</plist>
|
|
126
|
+
`;
|
|
127
|
+
await mkdir(dirname(paths.launchAgent), { recursive: true });
|
|
128
|
+
const staged = `${paths.launchAgent}.${process.pid}.tmp`;
|
|
129
|
+
await writeFile(staged, plist, { mode: 0o644 });
|
|
130
|
+
await rename(staged, paths.launchAgent);
|
|
131
|
+
}
|
|
132
|
+
async function installBundled(paths, client, deps, lockToken) {
|
|
133
|
+
const source = {
|
|
134
|
+
binary: client.binary,
|
|
135
|
+
signature: client.signature ?? `${client.binary}.sig`,
|
|
136
|
+
};
|
|
137
|
+
const release = await signedRelease(source.binary, source.signature, deps.releaseKey);
|
|
138
|
+
if (!release ||
|
|
139
|
+
release.version !== client.release.version ||
|
|
140
|
+
!(await installRelease(paths, source, release))) {
|
|
141
|
+
return { kind: "refused", reason: "unsigned" };
|
|
142
|
+
}
|
|
143
|
+
await installLoginItem(paths);
|
|
144
|
+
await startWith(deps)(paths, join(paths.current, SERVICE_BINARY), ["daemon"], lockToken);
|
|
145
|
+
return { kind: "installed", release: await untilRunning(paths, client, deps) };
|
|
146
|
+
}
|
|
147
|
+
async function handOver(paths, client, running, deps, lockToken) {
|
|
148
|
+
const control = await connectControl(paths.control);
|
|
149
|
+
if (!control)
|
|
150
|
+
throw new Error("Parlor Host stopped answering during the upgrade.");
|
|
151
|
+
const reply = await control.send({
|
|
152
|
+
op: "handover",
|
|
153
|
+
binary: client.binary,
|
|
154
|
+
signature: client.signature ?? `${client.binary}.sig`,
|
|
155
|
+
});
|
|
156
|
+
control.close();
|
|
157
|
+
if (!reply.ok) {
|
|
158
|
+
return reply.error === "unsigned" || reply.error === "downgrade"
|
|
159
|
+
? { kind: "refused", reason: reply.error, running }
|
|
160
|
+
: (() => {
|
|
161
|
+
throw new Error(reply.error);
|
|
162
|
+
})();
|
|
163
|
+
}
|
|
164
|
+
await startWith(deps)(paths, join(paths.current, SERVICE_BINARY), ["daemon"], lockToken);
|
|
165
|
+
const to = await untilRunning(paths, client, deps, (release) => compareVersions(release.version, running.version) > 0);
|
|
166
|
+
return { kind: "upgraded", from: running, to };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Leaves exactly one per-user service running, the newest: adopt, install or upgrade.
|
|
170
|
+
* Only binaries signed by the Parlor release key, never a downgrade; installs hold a lock
|
|
171
|
+
* under `paths.root`, so two apps can run it at once.
|
|
172
|
+
*/
|
|
173
|
+
export async function ensureServiceWith(deps, paths, client) {
|
|
174
|
+
await mkdir(paths.root, { recursive: true, mode: 0o700 });
|
|
175
|
+
return withFileLock(paths.lock, async (lockToken) => {
|
|
176
|
+
for (;;) {
|
|
177
|
+
const action = actionFor(await observe(paths, client), client);
|
|
178
|
+
switch (action.do) {
|
|
179
|
+
case "refuse_legacy":
|
|
180
|
+
return { kind: "refused", reason: "legacy_service" };
|
|
181
|
+
case "use":
|
|
182
|
+
return { kind: "using", running: action.running };
|
|
183
|
+
case "refuse_downgrade":
|
|
184
|
+
return { kind: "refused", reason: "downgrade", running: action.running };
|
|
185
|
+
case "hand_over":
|
|
186
|
+
return handOver(paths, action.client, action.running, deps, lockToken);
|
|
187
|
+
case "install_bundled":
|
|
188
|
+
return installBundled(paths, action.client, deps, lockToken);
|
|
189
|
+
case "start_installed":
|
|
190
|
+
await startWith(deps)(paths, join(paths.current, SERVICE_BINARY), ["daemon"], lockToken);
|
|
191
|
+
await untilRunning(paths, client, deps);
|
|
192
|
+
continue;
|
|
193
|
+
case "spawn_self": {
|
|
194
|
+
const self = selfInvocation(["daemon"]);
|
|
195
|
+
await startWith(deps)(paths, self.command, self.args, lockToken);
|
|
196
|
+
return { kind: "installed", release: await untilRunning(paths, client, deps) };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
export function ensureService(paths, client) {
|
|
203
|
+
return ensureServiceWith(DEFAULTS, paths, client);
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport {\n access,\n chmod,\n copyFile,\n mkdir,\n open,\n rename,\n rm,\n symlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport type { ProtocolRange } from \"@parlor/chat\";\n\nimport { connectControl } from \"./control.js\";\nimport { LOCK_TOKEN_ENV, withFileLock } from \"./lock.js\";\nimport type { HostPaths } from \"./paths.js\";\nimport {\n compareVersions,\n RELEASE_KEY,\n type ReleaseSignature,\n sha256File,\n signedRelease,\n} from \"./release.js\";\nimport { selfInvocation } from \"./self.js\";\n\nexport interface HostRelease {\n readonly version: string;\n /** Host protocols it speaks with app backends. */\n readonly protocols: ProtocolRange;\n /** The control protocol it speaks with CLIs and desktop sidecars. */\n readonly control: number;\n}\n\nexport type ServiceClient =\n | { readonly kind: \"cli\"; readonly release: HostRelease }\n | {\n readonly kind: \"desktop-sidecar\";\n readonly app: string;\n readonly release: HostRelease;\n /** The bundled binary, copied in when newer than what is installed. */\n readonly binary: string;\n /** Its `.sig`, `<binary>.sig` when the bundle keeps them together. */\n readonly signature?: string;\n };\n\nexport type AdoptOutcome =\n | { readonly kind: \"using\"; readonly running: HostRelease }\n | { readonly kind: \"installed\"; readonly release: HostRelease }\n /** The old service handed its links over; its running turns finish under it. */\n | { readonly kind: \"upgraded\"; readonly from: HostRelease; readonly to: HostRelease }\n | {\n readonly kind: \"refused\";\n readonly reason: \"unsigned\" | \"downgrade\" | \"legacy_service\";\n readonly running?: HostRelease;\n };\n\nexport type Situation =\n | { readonly service: \"absent\"; readonly installed: boolean }\n | { readonly service: \"legacy\" }\n | { readonly service: \"running\"; readonly running: HostRelease };\n\ntype Sidecar = Extract<ServiceClient, { readonly kind: \"desktop-sidecar\" }>;\n\nexport type Action =\n | { readonly do: \"start_installed\" }\n | { readonly do: \"spawn_self\" }\n | { readonly do: \"install_bundled\"; readonly client: Sidecar }\n | { readonly do: \"refuse_legacy\" }\n | { readonly do: \"use\"; readonly running: HostRelease }\n | { readonly do: \"refuse_downgrade\"; readonly running: HostRelease }\n | { readonly do: \"hand_over\"; readonly client: Sidecar; readonly running: HostRelease };\n\nexport function actionFor(situation: Situation, client: ServiceClient): Action {\n switch (situation.service) {\n case \"legacy\":\n return { do: \"refuse_legacy\" };\n case \"absent\":\n if (situation.installed) return { do: \"start_installed\" };\n return client.kind === \"cli\"\n ? { do: \"spawn_self\" }\n : { do: \"install_bundled\", client };\n case \"running\": {\n const { running } = situation;\n if (client.kind === \"cli\") return { do: \"use\", running };\n const order = compareVersions(client.release.version, running.version);\n if (order > 0) return { do: \"hand_over\", client, running };\n return order === 0 ? { do: \"use\", running } : { do: \"refuse_downgrade\", running };\n }\n }\n}\n\nexport interface ServiceDeps {\n readonly releaseKey: string | undefined;\n readonly readyMs: number;\n readonly pollMs: number;\n readonly start?:\n | ((\n paths: HostPaths,\n command: string,\n args: readonly string[],\n lockToken: string,\n ) => Promise<void>)\n | undefined;\n}\n\nconst DEFAULTS: ServiceDeps = { releaseKey: RELEASE_KEY, readyMs: 15_000, pollMs: 100 };\n\nconst startWith = (deps: ServiceDeps) => deps.start ?? spawnService;\nconst SERVICE_BINARY = \"parlor-host\";\nconst SIGNATURE_FILE = \"parlor-host.sig\";\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst exists = (path: string) =>\n access(path).then(\n () => true,\n () => false,\n );\n\nasync function observe(paths: HostPaths, client: ServiceClient): Promise<Situation> {\n const control = await connectControl(paths.control);\n if (!control) {\n return {\n service: \"absent\",\n installed: await exists(join(paths.current, SERVICE_BINARY)),\n };\n }\n const reply = await control.send({ op: \"hello\", client });\n control.close();\n return reply.ok\n ? { service: \"running\", running: reply.release }\n : { service: \"legacy\" };\n}\n\nasync function spawnService(\n paths: HostPaths,\n command: string,\n args: readonly string[],\n lockToken: string,\n): Promise<void> {\n await mkdir(paths.logs, { recursive: true, mode: 0o700 });\n const output = await open(join(paths.logs, \"daemon.out\"), \"a\", 0o600);\n const child = spawn(command, args, {\n cwd: paths.root,\n detached: true,\n stdio: [\"ignore\", output.fd, output.fd],\n env: { ...process.env, [LOCK_TOKEN_ENV]: lockToken },\n });\n child.unref();\n await output.close();\n}\n\nasync function untilRunning(\n paths: HostPaths,\n client: ServiceClient,\n deps: ServiceDeps,\n ready: (running: HostRelease) => boolean = () => true,\n): Promise<HostRelease> {\n for (const deadline = Date.now() + deps.readyMs; Date.now() < deadline;) {\n const situation = await observe(paths, client);\n if (situation.service === \"running\" && ready(situation.running)) {\n return situation.running;\n }\n await sleep(deps.pollMs);\n }\n throw new Error(`Parlor Host did not start; see ${join(paths.logs, \"daemon.out\")}.`);\n}\n\n/**\n * Copies a verified binary into `versions/<version>/` and repoints `current`, each step\n * atomic and repeatable; false when the copy is not the binary the signature describes.\n */\nexport async function installRelease(\n paths: HostPaths,\n source: { readonly binary: string; readonly signature: string },\n release: ReleaseSignature,\n): Promise<boolean> {\n const target = join(paths.versions, release.version);\n const installed = join(target, SERVICE_BINARY);\n const present = (await sha256File(installed).catch(() => undefined)) === release.sha256;\n if (!present) {\n const staged = join(paths.versions, `.${release.version}.tmp-${process.pid}`);\n await rm(staged, { recursive: true, force: true });\n await mkdir(staged, { recursive: true, mode: 0o700 });\n await copyFile(source.binary, join(staged, SERVICE_BINARY));\n await chmod(join(staged, SERVICE_BINARY), 0o755);\n await copyFile(source.signature, join(staged, SIGNATURE_FILE));\n if ((await sha256File(join(staged, SERVICE_BINARY))) !== release.sha256) {\n await rm(staged, { recursive: true, force: true });\n return false;\n }\n await rm(target, { recursive: true, force: true });\n await rename(staged, target);\n }\n const link = `${paths.current}.tmp-${process.pid}`;\n await rm(link, { force: true });\n await symlink(join(\"versions\", release.version), link);\n await rename(link, paths.current);\n return true;\n}\n\nconst escapeXml = (text: string) =>\n text.replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\");\n\n/** LaunchAgent, systemd user unit or Task Scheduler, pointing at `paths.current`. */\nexport async function installLoginItem(paths: HostPaths): Promise<void> {\n const plist = `<?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>\n <string>dev.parlor.host</string>\n <key>ProgramArguments</key>\n <array>\n <string>${escapeXml(join(paths.current, SERVICE_BINARY))}</string>\n <string>daemon</string>\n </array>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n <key>StandardOutPath</key>\n <string>${escapeXml(join(paths.logs, \"daemon.out\"))}</string>\n <key>StandardErrorPath</key>\n <string>${escapeXml(join(paths.logs, \"daemon.out\"))}</string>\n</dict>\n</plist>\n`;\n await mkdir(dirname(paths.launchAgent), { recursive: true });\n const staged = `${paths.launchAgent}.${process.pid}.tmp`;\n await writeFile(staged, plist, { mode: 0o644 });\n await rename(staged, paths.launchAgent);\n}\n\nasync function installBundled(\n paths: HostPaths,\n client: Sidecar,\n deps: ServiceDeps,\n lockToken: string,\n): Promise<AdoptOutcome> {\n const source = {\n binary: client.binary,\n signature: client.signature ?? `${client.binary}.sig`,\n };\n const release = await signedRelease(source.binary, source.signature, deps.releaseKey);\n if (\n !release ||\n release.version !== client.release.version ||\n !(await installRelease(paths, source, release))\n ) {\n return { kind: \"refused\", reason: \"unsigned\" };\n }\n await installLoginItem(paths);\n await startWith(deps)(\n paths,\n join(paths.current, SERVICE_BINARY),\n [\"daemon\"],\n lockToken,\n );\n return { kind: \"installed\", release: await untilRunning(paths, client, deps) };\n}\n\nasync function handOver(\n paths: HostPaths,\n client: Sidecar,\n running: HostRelease,\n deps: ServiceDeps,\n lockToken: string,\n): Promise<AdoptOutcome> {\n const control = await connectControl(paths.control);\n if (!control) throw new Error(\"Parlor Host stopped answering during the upgrade.\");\n const reply = await control.send({\n op: \"handover\",\n binary: client.binary,\n signature: client.signature ?? `${client.binary}.sig`,\n });\n control.close();\n if (!reply.ok) {\n return reply.error === \"unsigned\" || reply.error === \"downgrade\"\n ? { kind: \"refused\", reason: reply.error, running }\n : (() => {\n throw new Error(reply.error);\n })();\n }\n await startWith(deps)(\n paths,\n join(paths.current, SERVICE_BINARY),\n [\"daemon\"],\n lockToken,\n );\n const to = await untilRunning(\n paths,\n client,\n deps,\n (release) => compareVersions(release.version, running.version) > 0,\n );\n return { kind: \"upgraded\", from: running, to };\n}\n\n/**\n * Leaves exactly one per-user service running, the newest: adopt, install or upgrade.\n * Only binaries signed by the Parlor release key, never a downgrade; installs hold a lock\n * under `paths.root`, so two apps can run it at once.\n */\nexport async function ensureServiceWith(\n deps: ServiceDeps,\n paths: HostPaths,\n client: ServiceClient,\n): Promise<AdoptOutcome> {\n await mkdir(paths.root, { recursive: true, mode: 0o700 });\n return withFileLock(paths.lock, async (lockToken) => {\n for (;;) {\n const action = actionFor(await observe(paths, client), client);\n switch (action.do) {\n case \"refuse_legacy\":\n return { kind: \"refused\", reason: \"legacy_service\" };\n case \"use\":\n return { kind: \"using\", running: action.running };\n case \"refuse_downgrade\":\n return { kind: \"refused\", reason: \"downgrade\", running: action.running };\n case \"hand_over\":\n return handOver(paths, action.client, action.running, deps, lockToken);\n case \"install_bundled\":\n return installBundled(paths, action.client, deps, lockToken);\n case \"start_installed\":\n await startWith(deps)(\n paths,\n join(paths.current, SERVICE_BINARY),\n [\"daemon\"],\n lockToken,\n );\n await untilRunning(paths, client, deps);\n continue;\n case \"spawn_self\": {\n const self = selfInvocation([\"daemon\"]);\n await startWith(deps)(paths, self.command, self.args, lockToken);\n return { kind: \"installed\", release: await untilRunning(paths, client, deps) };\n }\n }\n }\n });\n}\n\nexport function ensureService(\n paths: HostPaths,\n client: ServiceClient,\n): Promise<AdoptOutcome> {\n return ensureServiceWith(DEFAULTS, paths, client);\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PATH agents are found on: this process's own first, then what the login shell adds,
|
|
3
|
+
* then the directories agent CLIs install into, since a LaunchAgent or a GUI app gets only
|
|
4
|
+
* launchd's system PATH.
|
|
5
|
+
*/
|
|
6
|
+
export declare function agentPath(): Promise<string>;
|
|
7
|
+
/** The first executable `name` on `path`, as an absolute path. */
|
|
8
|
+
export declare function resolveBinary(name: string, path: string): Promise<string | undefined>;
|
|
9
|
+
export declare function agentEnvironment(): Promise<Record<string, string>>;
|
|
10
|
+
//# sourceMappingURL=shell-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-path.d.ts","sourceRoot":"","sources":["../src/shell-path.ts"],"names":[],"mappings":"AA+BA;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAY3C;AAED,kEAAkE;AAClE,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU7B;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOxE"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, constants } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { delimiter, join } from "node:path";
|
|
5
|
+
const MARK = "__PARLOR_PATH__";
|
|
6
|
+
const TIMEOUT_MS = 3_000;
|
|
7
|
+
let resolved;
|
|
8
|
+
const wellKnownDirectories = () => [
|
|
9
|
+
join(homedir(), ".local", "bin"),
|
|
10
|
+
"/opt/homebrew/bin",
|
|
11
|
+
"/usr/local/bin",
|
|
12
|
+
];
|
|
13
|
+
function fromLoginShell() {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
16
|
+
const child = spawn(shell, ["-ilc", `printf '${MARK}%s${MARK}' "$PATH"`], {
|
|
17
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
18
|
+
timeout: TIMEOUT_MS,
|
|
19
|
+
});
|
|
20
|
+
let out = "";
|
|
21
|
+
child.stdout.setEncoding("utf8");
|
|
22
|
+
child.stdout.on("data", (chunk) => (out += chunk));
|
|
23
|
+
child.once("error", () => resolve(undefined));
|
|
24
|
+
child.once("close", () => resolve(out.split(MARK)[1]));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The PATH agents are found on: this process's own first, then what the login shell adds,
|
|
29
|
+
* then the directories agent CLIs install into, since a LaunchAgent or a GUI app gets only
|
|
30
|
+
* launchd's system PATH.
|
|
31
|
+
*/
|
|
32
|
+
export function agentPath() {
|
|
33
|
+
resolved ??= fromLoginShell().then((login) => {
|
|
34
|
+
const own = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
35
|
+
const known = (entry) => own.includes(entry);
|
|
36
|
+
const fromShell = (login ?? "")
|
|
37
|
+
.split(delimiter)
|
|
38
|
+
.filter((entry) => entry && !known(entry));
|
|
39
|
+
const rest = [...own, ...fromShell];
|
|
40
|
+
const installed = wellKnownDirectories().filter((entry) => !rest.includes(entry));
|
|
41
|
+
return [...rest, ...installed].join(delimiter);
|
|
42
|
+
});
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
/** The first executable `name` on `path`, as an absolute path. */
|
|
46
|
+
export async function resolveBinary(name, path) {
|
|
47
|
+
for (const directory of path.split(delimiter).filter(Boolean)) {
|
|
48
|
+
const candidate = join(directory, name);
|
|
49
|
+
const found = await access(candidate, constants.X_OK).then(() => candidate, () => undefined);
|
|
50
|
+
if (found !== undefined)
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
export async function agentEnvironment() {
|
|
56
|
+
const env = {};
|
|
57
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
58
|
+
if (value !== undefined && !name.startsWith("PARLOR_"))
|
|
59
|
+
env[name] = value;
|
|
60
|
+
}
|
|
61
|
+
env.PATH = await agentPath();
|
|
62
|
+
return env;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=shell-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-path.js","sourceRoot":"","sources":["../src/shell-path.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport { access, constants } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { delimiter, join } from \"node:path\";\n\nconst MARK = \"__PARLOR_PATH__\";\nconst TIMEOUT_MS = 3_000;\n\nlet resolved: Promise<string> | undefined;\n\nconst wellKnownDirectories = () => [\n join(homedir(), \".local\", \"bin\"),\n \"/opt/homebrew/bin\",\n \"/usr/local/bin\",\n];\n\nfunction fromLoginShell(): Promise<string | undefined> {\n return new Promise((resolve) => {\n const shell = process.env.SHELL || \"/bin/zsh\";\n const child = spawn(shell, [\"-ilc\", `printf '${MARK}%s${MARK}' \"$PATH\"`], {\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: TIMEOUT_MS,\n });\n let out = \"\";\n child.stdout.setEncoding(\"utf8\");\n child.stdout.on(\"data\", (chunk: string) => (out += chunk));\n child.once(\"error\", () => resolve(undefined));\n child.once(\"close\", () => resolve(out.split(MARK)[1]));\n });\n}\n\n/**\n * The PATH agents are found on: this process's own first, then what the login shell adds,\n * then the directories agent CLIs install into, since a LaunchAgent or a GUI app gets only\n * launchd's system PATH.\n */\nexport function agentPath(): Promise<string> {\n resolved ??= fromLoginShell().then((login) => {\n const own = (process.env.PATH ?? \"\").split(delimiter).filter(Boolean);\n const known = (entry: string) => own.includes(entry);\n const fromShell = (login ?? \"\")\n .split(delimiter)\n .filter((entry) => entry && !known(entry));\n const rest = [...own, ...fromShell];\n const installed = wellKnownDirectories().filter((entry) => !rest.includes(entry));\n return [...rest, ...installed].join(delimiter);\n });\n return resolved;\n}\n\n/** The first executable `name` on `path`, as an absolute path. */\nexport async function resolveBinary(\n name: string,\n path: string,\n): Promise<string | undefined> {\n for (const directory of path.split(delimiter).filter(Boolean)) {\n const candidate = join(directory, name);\n const found = await access(candidate, constants.X_OK).then(\n () => candidate,\n () => undefined,\n );\n if (found !== undefined) return found;\n }\n return undefined;\n}\n\nexport async function agentEnvironment(): Promise<Record<string, string>> {\n const env: Record<string, string> = {};\n for (const [name, value] of Object.entries(process.env)) {\n if (value !== undefined && !name.startsWith(\"PARLOR_\")) env[name] = value;\n }\n env.PATH = await agentPath();\n return env;\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const compact: (text: string) => string;
|
|
2
|
+
export interface Difference {
|
|
3
|
+
readonly file: string;
|
|
4
|
+
readonly symbol: string;
|
|
5
|
+
readonly kind: "changed" | "missing" | "added";
|
|
6
|
+
}
|
|
7
|
+
export declare function differences(file: string, skeleton: string, actual: string, only?: readonly string[]): Difference[];
|
|
8
|
+
//# sourceMappingURL=skeleton-kit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skeleton-kit.d.ts","sourceRoot":"","sources":["../src/skeleton-kit.ts"],"names":[],"mappings":"AA6DA,eAAO,MAAM,OAAO,SAAU,MAAM,WAIA,CAAC;AA8DrC,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;CAChD;AAED,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GACvB,UAAU,EAAE,CAcd"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const OPENING = "([{";
|
|
2
|
+
const CLOSING = ")]}";
|
|
3
|
+
function statementsOf(text) {
|
|
4
|
+
const statements = [];
|
|
5
|
+
let current = "";
|
|
6
|
+
let depth = 0;
|
|
7
|
+
const open = [];
|
|
8
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
9
|
+
const char = text[at];
|
|
10
|
+
const next = text[at + 1];
|
|
11
|
+
if (char === "/" && next === "/") {
|
|
12
|
+
while (at < text.length && text[at] !== "\n")
|
|
13
|
+
at += 1;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (char === "/" && next === "*") {
|
|
17
|
+
at = text.indexOf("*/", at + 2) + 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
21
|
+
let end = at + 1;
|
|
22
|
+
while (end < text.length && text[end] !== char)
|
|
23
|
+
end += text[end] === "\\" ? 2 : 1;
|
|
24
|
+
current += text.slice(at, end + 1);
|
|
25
|
+
at = end;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (CLOSING.includes(char) && depth === 0) {
|
|
29
|
+
current = "";
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
current += char;
|
|
33
|
+
if (OPENING.includes(char)) {
|
|
34
|
+
depth += 1;
|
|
35
|
+
open.push(CLOSING[OPENING.indexOf(char)]);
|
|
36
|
+
}
|
|
37
|
+
else if (CLOSING.includes(char)) {
|
|
38
|
+
depth -= 1;
|
|
39
|
+
open.pop();
|
|
40
|
+
const blockEnded = char === "}" && depth === 0;
|
|
41
|
+
const rest = text.slice(at + 1).match(/^\s*(\S)/)?.[1];
|
|
42
|
+
if (blockEnded && rest !== ";" && rest !== "," && rest !== "." && rest !== ")") {
|
|
43
|
+
const head = current.trim();
|
|
44
|
+
const isBlock = /^(export\s+)?(async\s+)?(function|interface|class|enum)\b/.test(head);
|
|
45
|
+
const hasBody = !/^(export\s+)?(async\s+)?function\b/.test(head) ||
|
|
46
|
+
endOfSignature(head) < head.length;
|
|
47
|
+
if (isBlock && hasBody) {
|
|
48
|
+
statements.push(current.trim());
|
|
49
|
+
current = "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (char === ";" && depth === 0) {
|
|
54
|
+
statements.push(current.trim());
|
|
55
|
+
current = "";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (current.trim())
|
|
59
|
+
statements.push(`${current.trim()}${open.reverse().join("")}`);
|
|
60
|
+
return statements;
|
|
61
|
+
}
|
|
62
|
+
export const compact = (text) => text
|
|
63
|
+
.replace(/\s+/g, " ")
|
|
64
|
+
.replace(/\s*([(){}[\],;:<>|&=?])\s*/g, "$1")
|
|
65
|
+
.replace(/[,;](?=[)\]}>])/g, "");
|
|
66
|
+
function endOfSignature(statement) {
|
|
67
|
+
let parens = 0;
|
|
68
|
+
let angles = 0;
|
|
69
|
+
let seenParams = false;
|
|
70
|
+
for (let at = 0; at < statement.length; at += 1) {
|
|
71
|
+
const char = statement[at];
|
|
72
|
+
if (char === "(")
|
|
73
|
+
parens += 1;
|
|
74
|
+
else if (char === ")") {
|
|
75
|
+
parens -= 1;
|
|
76
|
+
if (parens === 0)
|
|
77
|
+
seenParams = true;
|
|
78
|
+
}
|
|
79
|
+
else if (parens === 0 && seenParams) {
|
|
80
|
+
if (char === "<")
|
|
81
|
+
angles += 1;
|
|
82
|
+
else if (char === ">" && statement[at - 1] !== "=")
|
|
83
|
+
angles -= 1;
|
|
84
|
+
else if (char === "{" &&
|
|
85
|
+
angles === 0 &&
|
|
86
|
+
!/[:|&]\s*$/.test(statement.slice(0, at))) {
|
|
87
|
+
return at;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return statement.length;
|
|
92
|
+
}
|
|
93
|
+
function signaturesOf(text) {
|
|
94
|
+
const signatures = new Map();
|
|
95
|
+
const add = (name, signature) => signatures.set(name, [signatures.get(name), compact(signature)].filter(Boolean).join("\n"));
|
|
96
|
+
for (const statement of statementsOf(text)) {
|
|
97
|
+
if (!statement.startsWith("export"))
|
|
98
|
+
continue;
|
|
99
|
+
const declaration = statement.replace(/^export\s+(default\s+)?/, "");
|
|
100
|
+
const named = /^(?:async\s+)?(function|interface|type|class|enum)\s+(\w+)/.exec(declaration);
|
|
101
|
+
if (named) {
|
|
102
|
+
const [, kind, name] = named;
|
|
103
|
+
const keep = kind === "function"
|
|
104
|
+
? declaration.slice(0, endOfSignature(declaration)).replace(/^async\s+/, "")
|
|
105
|
+
: declaration;
|
|
106
|
+
add(name, keep);
|
|
107
|
+
}
|
|
108
|
+
else if (/^(const|let)\s/.test(declaration)) {
|
|
109
|
+
const name = /^(?:const|let)\s+(\w+)/.exec(declaration)[1];
|
|
110
|
+
const [head] = declaration.split(/(?<![=!<>])=(?![=>])/);
|
|
111
|
+
const annotated = /^(?:const|let)\s+\w+\s*:/.test(declaration);
|
|
112
|
+
const callee = /=\s*([\w.]+(?:<[^(]*>)?)\(/.exec(declaration)?.[1];
|
|
113
|
+
add(name, annotated ? head : `${name} = ${callee ?? ""}`);
|
|
114
|
+
}
|
|
115
|
+
else if (/^(type\s+)?\{/.test(declaration) || declaration.startsWith("*")) {
|
|
116
|
+
for (const [, name] of declaration.matchAll(/(?:\btype\s+)?(\w+)(?=\s*[,}])/g)) {
|
|
117
|
+
if (name !== "type")
|
|
118
|
+
add(name, "re-export");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return signatures;
|
|
123
|
+
}
|
|
124
|
+
export function differences(file, skeleton, actual, only) {
|
|
125
|
+
const wanted = signaturesOf(skeleton);
|
|
126
|
+
const found = signaturesOf(actual);
|
|
127
|
+
const names = new Set([...wanted.keys(), ...found.keys()]);
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const symbol of [...names].sort()) {
|
|
130
|
+
if (only && !only.includes(symbol))
|
|
131
|
+
continue;
|
|
132
|
+
const before = wanted.get(symbol);
|
|
133
|
+
const after = found.get(symbol);
|
|
134
|
+
if (before === undefined)
|
|
135
|
+
out.push({ file, symbol, kind: "added" });
|
|
136
|
+
else if (after === undefined)
|
|
137
|
+
out.push({ file, symbol, kind: "missing" });
|
|
138
|
+
else if (before !== after)
|
|
139
|
+
out.push({ file, symbol, kind: "changed" });
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=skeleton-kit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skeleton-kit.js","sourceRoot":"","sources":["../src/skeleton-kit.ts"],"names":[],"mappings":"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","sourcesContent":["const OPENING = \"([{\";\nconst CLOSING = \")]}\";\n\nfunction statementsOf(text: string): string[] {\n const statements: string[] = [];\n let current = \"\";\n let depth = 0;\n const open: string[] = [];\n for (let at = 0; at < text.length; at += 1) {\n const char = text[at]!;\n const next = text[at + 1];\n if (char === \"/\" && next === \"/\") {\n while (at < text.length && text[at] !== \"\\n\") at += 1;\n continue;\n }\n if (char === \"/\" && next === \"*\") {\n at = text.indexOf(\"*/\", at + 2) + 1;\n continue;\n }\n if (char === '\"' || char === \"'\" || char === \"`\") {\n let end = at + 1;\n while (end < text.length && text[end] !== char) end += text[end] === \"\\\\\" ? 2 : 1;\n current += text.slice(at, end + 1);\n at = end;\n continue;\n }\n if (CLOSING.includes(char) && depth === 0) {\n current = \"\";\n continue;\n }\n current += char;\n if (OPENING.includes(char)) {\n depth += 1;\n open.push(CLOSING[OPENING.indexOf(char)]!);\n } else if (CLOSING.includes(char)) {\n depth -= 1;\n open.pop();\n const blockEnded = char === \"}\" && depth === 0;\n const rest = text.slice(at + 1).match(/^\\s*(\\S)/)?.[1];\n if (blockEnded && rest !== \";\" && rest !== \",\" && rest !== \".\" && rest !== \")\") {\n const head = current.trim();\n const isBlock = /^(export\\s+)?(async\\s+)?(function|interface|class|enum)\\b/.test(\n head,\n );\n const hasBody =\n !/^(export\\s+)?(async\\s+)?function\\b/.test(head) ||\n endOfSignature(head) < head.length;\n if (isBlock && hasBody) {\n statements.push(current.trim());\n current = \"\";\n }\n }\n } else if (char === \";\" && depth === 0) {\n statements.push(current.trim());\n current = \"\";\n }\n }\n if (current.trim()) statements.push(`${current.trim()}${open.reverse().join(\"\")}`);\n return statements;\n}\n\nexport const compact = (text: string) =>\n text\n .replace(/\\s+/g, \" \")\n .replace(/\\s*([(){}[\\],;:<>|&=?])\\s*/g, \"$1\")\n .replace(/[,;](?=[)\\]}>])/g, \"\");\n\nfunction endOfSignature(statement: string): number {\n let parens = 0;\n let angles = 0;\n let seenParams = false;\n for (let at = 0; at < statement.length; at += 1) {\n const char = statement[at]!;\n if (char === \"(\") parens += 1;\n else if (char === \")\") {\n parens -= 1;\n if (parens === 0) seenParams = true;\n } else if (parens === 0 && seenParams) {\n if (char === \"<\") angles += 1;\n else if (char === \">\" && statement[at - 1] !== \"=\") angles -= 1;\n else if (\n char === \"{\" &&\n angles === 0 &&\n !/[:|&]\\s*$/.test(statement.slice(0, at))\n ) {\n return at;\n }\n }\n }\n return statement.length;\n}\n\nfunction signaturesOf(text: string): Map<string, string> {\n const signatures = new Map<string, string>();\n const add = (name: string, signature: string) =>\n signatures.set(\n name,\n [signatures.get(name), compact(signature)].filter(Boolean).join(\"\\n\"),\n );\n for (const statement of statementsOf(text)) {\n if (!statement.startsWith(\"export\")) continue;\n const declaration = statement.replace(/^export\\s+(default\\s+)?/, \"\");\n const named = /^(?:async\\s+)?(function|interface|type|class|enum)\\s+(\\w+)/.exec(\n declaration,\n );\n if (named) {\n const [, kind, name] = named;\n const keep =\n kind === \"function\"\n ? declaration.slice(0, endOfSignature(declaration)).replace(/^async\\s+/, \"\")\n : declaration;\n add(name!, keep);\n } else if (/^(const|let)\\s/.test(declaration)) {\n const name = /^(?:const|let)\\s+(\\w+)/.exec(declaration)![1]!;\n const [head] = declaration.split(/(?<![=!<>])=(?![=>])/);\n const annotated = /^(?:const|let)\\s+\\w+\\s*:/.test(declaration);\n const callee = /=\\s*([\\w.]+(?:<[^(]*>)?)\\(/.exec(declaration)?.[1];\n add(name, annotated ? head! : `${name} = ${callee ?? \"\"}`);\n } else if (/^(type\\s+)?\\{/.test(declaration) || declaration.startsWith(\"*\")) {\n for (const [, name] of declaration.matchAll(/(?:\\btype\\s+)?(\\w+)(?=\\s*[,}])/g)) {\n if (name !== \"type\") add(name!, \"re-export\");\n }\n }\n }\n return signatures;\n}\n\nexport interface Difference {\n readonly file: string;\n readonly symbol: string;\n readonly kind: \"changed\" | \"missing\" | \"added\";\n}\n\nexport function differences(\n file: string,\n skeleton: string,\n actual: string,\n only?: readonly string[],\n): Difference[] {\n const wanted = signaturesOf(skeleton);\n const found = signaturesOf(actual);\n const names = new Set([...wanted.keys(), ...found.keys()]);\n const out: Difference[] = [];\n for (const symbol of [...names].sort()) {\n if (only && !only.includes(symbol)) continue;\n const before = wanted.get(symbol);\n const after = found.get(symbol);\n if (before === undefined) out.push({ file, symbol, kind: \"added\" });\n else if (after === undefined) out.push({ file, symbol, kind: \"missing\" });\n else if (before !== after) out.push({ file, symbol, kind: \"changed\" });\n }\n return out;\n}\n"]}
|