@danypops/pi-packed 0.19.7 → 0.19.10
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/dist/client.d.ts +109 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1 -0
- package/dist/protocol.d.ts +221 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +1 -0
- package/extension/src/{permission.ts → approval/permission.ts} +1 -1
- package/extension/src/index.ts +1 -1
- package/extension/src/packed.ts +2 -2
- package/extension/src/{discover.ts → tabs/discover.ts} +4 -4
- package/extension/src/{resource-config.ts → tabs/resource-config.ts} +4 -4
- package/extension/src/{security-tui.ts → tabs/security-tui.ts} +3 -3
- package/extension/src/tool-output.ts +1 -1
- package/extension/src/tools.ts +2 -2
- package/extension/src/tui.ts +4 -4
- package/package.json +31 -8
- package/service/schema/pi-setup-v1.schema.json +70 -0
- package/service/setup/danypops-ecosystem.pi-setup.json +15 -0
- package/service/src/adoption/advisories.ts +268 -0
- package/service/src/adoption/check.ts +872 -0
- package/service/src/adoption/commit-freshness.ts +167 -0
- package/service/src/adoption/doctor.ts +135 -0
- package/service/src/adoption/install-validation.ts +187 -0
- package/service/src/adoption/pack.ts +291 -0
- package/service/src/adoption/score.ts +466 -0
- package/service/src/adoption/smoke-child.ts +113 -0
- package/service/src/adoption/smoke.ts +282 -0
- package/service/src/cli/cli.ts +926 -0
- package/service/src/daemon/cleanup.ts +76 -0
- package/service/src/daemon/client.ts +412 -0
- package/service/src/daemon/daemon-service.ts +249 -0
- package/service/src/daemon/daemon.ts +110 -0
- package/service/src/daemon/service.ts +664 -0
- package/service/src/daemon/watcher.ts +92 -0
- package/service/src/index/build-index.ts +256 -0
- package/service/src/packages/catalog.ts +61 -0
- package/service/src/packages/db.ts +224 -0
- package/service/src/packages/install.ts +60 -0
- package/service/src/packages/installed.ts +123 -0
- package/service/src/packages/package.ts +141 -0
- package/service/src/packages/resources.ts +203 -0
- package/service/src/pi/pi-version.ts +171 -0
- package/service/src/public/atomic-json.ts +32 -0
- package/service/src/public/client.ts +277 -0
- package/service/src/public/protocol.ts +169 -0
- package/service/src/publish/publish.ts +855 -0
- package/service/src/registry/registry.ts +246 -0
- package/service/src/security/security.ts +128 -0
- package/service/src/self-update/self-update.ts +148 -0
- package/service/src/setup/setup.ts +761 -0
- package/service/src/shared/atomic-json.ts +33 -0
- package/service/src/shared/cache.ts +21 -0
- package/service/src/shared/constants.ts +73 -0
- package/service/src/shared/log.ts +21 -0
- package/service/src/shared/paths.ts +88 -0
- package/service/src/shared/state.ts +15 -0
- package/service/src/shared/version.ts +46 -0
- package/service/test/advisories.test.ts +287 -0
- package/service/test/check.test.ts +368 -0
- package/service/test/cleanup.test.ts +220 -0
- package/service/test/cli.test.ts +1303 -0
- package/service/test/core.test.ts +181 -0
- package/service/test/daemon-kit-migration.test.ts +181 -0
- package/service/test/daemon-service.test.ts +238 -0
- package/service/test/db.test.ts +178 -0
- package/service/test/doctor.test.ts +234 -0
- package/service/test/domain.test.ts +291 -0
- package/service/test/fixtures/install-validation/broken-package/extension/index.ts +3 -0
- package/service/test/fixtures/install-validation/broken-package/package.json +8 -0
- package/service/test/fixtures/install-validation/healthy-package/extension/index.ts +3 -0
- package/service/test/fixtures/install-validation/healthy-package/package.json +8 -0
- package/service/test/fixtures/install-validation/no-manifest-package/package.json +5 -0
- package/service/test/index.test.ts +353 -0
- package/service/test/install-validation.test.ts +114 -0
- package/service/test/install.test.ts +113 -0
- package/service/test/log.test.ts +42 -0
- package/service/test/pack-score.test.ts +513 -0
- package/service/test/pi-version.test.ts +318 -0
- package/service/test/public-boundary.test.ts +54 -0
- package/service/test/public-client.test.ts +127 -0
- package/service/test/public-consumer.ts +8 -0
- package/service/test/publish.test.ts +333 -0
- package/service/test/registry-contract.test.ts +148 -0
- package/service/test/resources.test.ts +255 -0
- package/service/test/security.test.ts +89 -0
- package/service/test/self-update.test.ts +257 -0
- package/service/test/service.test.ts +555 -0
- package/service/test/setup.test.ts +375 -0
- package/service/test/smoke.test.ts +118 -0
- package/service/test/version.test.ts +37 -0
- package/service/tsconfig.consumer.json +13 -0
- package/service/tsconfig.public.json +12 -0
- /package/extension/src/{reload.ts → approval/reload.ts} +0 -0
- /package/extension/src/{discover-model.ts → tabs/discover-model.ts} +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install-validation.test.ts — the real dry-run scenario: stage an actual
|
|
3
|
+
* npm tarball (via `npm pack` against a local fixture directory, so it's
|
|
4
|
+
* offline/deterministic -- npm pack works identically for a local path and
|
|
5
|
+
* a registry spec) and headlessly load-check it via pi-extension-harness's
|
|
6
|
+
* real mock-pi-cli subprocess. No mocking of npm, tar, or the harness --
|
|
7
|
+
* a broken fixture must genuinely be refused, a healthy one must genuinely
|
|
8
|
+
* pass.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import type { InstallValidationResult, InstallValidator } from "../src/adoption/install-validation.ts";
|
|
14
|
+
import { bareNpmSpec, HeadlessInstallValidator, validateExtensionLoadsHeadless } from "../src/adoption/install-validation.ts";
|
|
15
|
+
import { ExecInstaller } from "../src/packages/install.ts";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const FIXTURES = join(__dirname, "fixtures/install-validation");
|
|
19
|
+
const HEALTHY = join(FIXTURES, "healthy-package");
|
|
20
|
+
const BROKEN = join(FIXTURES, "broken-package");
|
|
21
|
+
const NO_MANIFEST = join(FIXTURES, "no-manifest-package");
|
|
22
|
+
|
|
23
|
+
describe("bareNpmSpec", () => {
|
|
24
|
+
it("strips the npm: scheme", () => {
|
|
25
|
+
expect(bareNpmSpec("npm:pi-lsp@1.0.0")).toBe("pi-lsp@1.0.0");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns undefined for git:/https: sources -- out of scope for headless validation", () => {
|
|
29
|
+
expect(bareNpmSpec("git:github.com/u/r@main")).toBeUndefined();
|
|
30
|
+
expect(bareNpmSpec("https://example.com/pkg.tgz")).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("validateExtensionLoadsHeadless (real mock-pi-cli subprocess, no mocking)", () => {
|
|
35
|
+
it("reports ok for an extension that registers cleanly", async () => {
|
|
36
|
+
const result = await validateExtensionLoadsHeadless(join(HEALTHY, "extension/index.ts"));
|
|
37
|
+
expect(result.ok).toBe(true);
|
|
38
|
+
}, 20_000);
|
|
39
|
+
|
|
40
|
+
it("reports the real failure message for an extension that throws during registration", async () => {
|
|
41
|
+
const result = await validateExtensionLoadsHeadless(join(BROKEN, "extension/index.ts"));
|
|
42
|
+
expect(result.ok).toBe(false);
|
|
43
|
+
expect(result.message).toContain("deliberately fails during registration");
|
|
44
|
+
}, 20_000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("HeadlessInstallValidator (real npm pack + tar extraction, no mocking)", () => {
|
|
48
|
+
it("passes through non-npm sources unchanged -- nothing to stage from an npm tarball", async () => {
|
|
49
|
+
const validator = new HeadlessInstallValidator();
|
|
50
|
+
const result = await validator.validate("git:github.com/u/r@main");
|
|
51
|
+
expect(result).toEqual({ ok: true, source: "git:github.com/u/r@main", extensions: [] });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("passes a package with no pi.extensions -- most npm packages, nothing to validate", async () => {
|
|
55
|
+
const validator = new HeadlessInstallValidator();
|
|
56
|
+
const result = await validator.validate(`npm:${NO_MANIFEST}`);
|
|
57
|
+
expect(result.ok).toBe(true);
|
|
58
|
+
expect(result.extensions).toEqual([]);
|
|
59
|
+
}, 20_000);
|
|
60
|
+
|
|
61
|
+
it("refuses a real broken package, naming which extension failed and why", async () => {
|
|
62
|
+
const validator = new HeadlessInstallValidator();
|
|
63
|
+
const result = await validator.validate(`npm:${BROKEN}`);
|
|
64
|
+
expect(result.ok).toBe(false);
|
|
65
|
+
expect(result.extensions).toHaveLength(1);
|
|
66
|
+
expect(result.extensions[0]).toMatchObject({ path: "extension/index.ts", ok: false });
|
|
67
|
+
expect(result.extensions[0]?.message).toContain("deliberately fails during registration");
|
|
68
|
+
}, 20_000);
|
|
69
|
+
|
|
70
|
+
it("approves a real healthy package -- every declared extension reports ok", async () => {
|
|
71
|
+
const validator = new HeadlessInstallValidator();
|
|
72
|
+
const result = await validator.validate(`npm:${HEALTHY}`);
|
|
73
|
+
expect(result.ok).toBe(true);
|
|
74
|
+
expect(result.extensions).toEqual([{ path: "extension/index.ts", ok: true }]);
|
|
75
|
+
}, 20_000);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("ExecInstaller.install() -- refuses before ever spawning the real pi binary", () => {
|
|
79
|
+
function fakeValidator(result: InstallValidationResult): InstallValidator {
|
|
80
|
+
return { validate: async () => result };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
it("never spawns pi when the validator reports a failed extension", async () => {
|
|
84
|
+
const installer = new ExecInstaller(
|
|
85
|
+
"/bin/false",
|
|
86
|
+
"/tmp/unused-pihome",
|
|
87
|
+
fakeValidator({
|
|
88
|
+
ok: false,
|
|
89
|
+
source: "npm:broken-pkg",
|
|
90
|
+
extensions: [{ path: "extension/index.ts", ok: false, message: "boom" }],
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await expect(installer.install("npm:broken-pkg")).rejects.toThrow(/install refused.*extension\/index\.ts: boom/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("proceeds to the real install when the validator approves", async () => {
|
|
98
|
+
// /bin/true always exits 0 -- proves the real spawn path was reached
|
|
99
|
+
// (a refused install never gets this far to find out).
|
|
100
|
+
const installer = new ExecInstaller(
|
|
101
|
+
"/bin/true",
|
|
102
|
+
"/tmp/unused-pihome",
|
|
103
|
+
fakeValidator({ ok: true, source: "npm:good-pkg", extensions: [] }),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await expect(installer.install("npm:good-pkg")).resolves.toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("uses the real HeadlessInstallValidator by default, refusing a genuinely broken candidate end-to-end", async () => {
|
|
110
|
+
const installer = new ExecInstaller("/bin/true", "/tmp/unused-pihome");
|
|
111
|
+
|
|
112
|
+
await expect(installer.install(`npm:${BROKEN}`)).rejects.toThrow(/install refused/);
|
|
113
|
+
}, 20_000);
|
|
114
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExecInstaller.update() — a real Bun.spawn subprocess round-trip, not a
|
|
3
|
+
* mocked one, matching the rest of this suite's real-I/O style.
|
|
4
|
+
*
|
|
5
|
+
* `pi update --extension <source>` was verified empirically (against the
|
|
6
|
+
* real `pi` binary) to exit 0 and print "Updated <source>" whether or not
|
|
7
|
+
* anything actually changed: both for a pinned, already-current source and
|
|
8
|
+
* for an unpinned, already-latest one. ExecInstaller must not trust that
|
|
9
|
+
* text; these tests drive a fake `pi` binary that reproduces the same
|
|
10
|
+
* always-"Updated"-regardless-of-outcome text, and assert the real on-disk
|
|
11
|
+
* version diff is what actually drives reloadRequired/alreadyUpToDate.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from "bun:test";
|
|
14
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { ExecInstaller } from "../src/packages/install.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A fake `pi` binary: always prints "Updated <source>" and exits 0 (matching
|
|
21
|
+
* real `pi`'s observed behavior for a no-op). When `rewrite` is given, it
|
|
22
|
+
* additionally overwrites that exact package's on-disk version -- letting a
|
|
23
|
+
* test simulate a genuine version change on demand. The rewrite target is
|
|
24
|
+
* baked into the script file itself (not an env var) so it is immune to
|
|
25
|
+
* Bun.spawn's default env snapshot not picking up late process.env writes.
|
|
26
|
+
*/
|
|
27
|
+
function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newVersion: string }): string {
|
|
28
|
+
const script = join(dir, "fake-pi");
|
|
29
|
+
const rewriteLine = rewrite
|
|
30
|
+
? `mkdir -p '${join(rewrite.piHome, "npm", "node_modules", rewrite.name)}' && printf '{"version":"%s"}' '${rewrite.newVersion}' > '${join(rewrite.piHome, "npm", "node_modules", rewrite.name, "package.json")}'`
|
|
31
|
+
: "true";
|
|
32
|
+
writeFileSync(
|
|
33
|
+
script,
|
|
34
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal bash positional-parameter syntax in a generated fixture script, not JS interpolation
|
|
35
|
+
["#!/usr/bin/env bash", "set -euo pipefail", 'source="${3:-}"', rewriteLine, 'echo "Updated $source"', "exit 0"].join("\n"),
|
|
36
|
+
);
|
|
37
|
+
chmodSync(script, 0o755);
|
|
38
|
+
return script;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writePiHome(nodeModules: Record<string, string> = {}): string {
|
|
42
|
+
const dir = mkdtempSync(join(tmpdir(), "packed-exec-pihome-"));
|
|
43
|
+
for (const [name, version] of Object.entries(nodeModules)) {
|
|
44
|
+
const pkgDir = join(dir, "npm", "node_modules", name);
|
|
45
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
46
|
+
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ version }));
|
|
47
|
+
}
|
|
48
|
+
return dir;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguous exit-0 text", () => {
|
|
52
|
+
it("pinned source, version genuinely unchanged: alreadyUpToDate, reloadRequired false", async () => {
|
|
53
|
+
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
54
|
+
const bin = writeFakePi(scriptDir);
|
|
55
|
+
const piHome = writePiHome({ "@scope/pkg": "1.2.3" });
|
|
56
|
+
const installer = new ExecInstaller(bin, piHome);
|
|
57
|
+
|
|
58
|
+
const outcome = await installer.update("npm:@scope/pkg@1.2.3");
|
|
59
|
+
|
|
60
|
+
expect(outcome.output).toBe("Updated npm:@scope/pkg@1.2.3");
|
|
61
|
+
expect(outcome.pinned).toBe(true);
|
|
62
|
+
expect(outcome.alreadyUpToDate).toBe(true);
|
|
63
|
+
expect(outcome.reloadRequired).toBe(false);
|
|
64
|
+
expect(outcome.previousVersion).toBe("1.2.3");
|
|
65
|
+
expect(outcome.currentVersion).toBe("1.2.3");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('unpinned source, already latest (pi still exits 0 and says "Updated"): alreadyUpToDate', async () => {
|
|
69
|
+
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
70
|
+
const bin = writeFakePi(scriptDir);
|
|
71
|
+
const piHome = writePiHome({ plain: "0.5.0" });
|
|
72
|
+
const installer = new ExecInstaller(bin, piHome);
|
|
73
|
+
|
|
74
|
+
const outcome = await installer.update("npm:plain");
|
|
75
|
+
|
|
76
|
+
expect(outcome.pinned).toBe(false);
|
|
77
|
+
expect(outcome.alreadyUpToDate).toBe(true);
|
|
78
|
+
expect(outcome.reloadRequired).toBe(false);
|
|
79
|
+
expect(outcome.previousVersion).toBe("0.5.0");
|
|
80
|
+
expect(outcome.currentVersion).toBe("0.5.0");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("unpinned source, a real version change happens: reloadRequired true", async () => {
|
|
84
|
+
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
85
|
+
const piHome = writePiHome({ plain: "0.5.0" });
|
|
86
|
+
const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
|
|
87
|
+
const installer = new ExecInstaller(bin, piHome);
|
|
88
|
+
|
|
89
|
+
const outcome = await installer.update("npm:plain");
|
|
90
|
+
|
|
91
|
+
expect(outcome.alreadyUpToDate).toBe(false);
|
|
92
|
+
expect(outcome.reloadRequired).toBe(true);
|
|
93
|
+
expect(outcome.previousVersion).toBe("0.5.0");
|
|
94
|
+
expect(outcome.currentVersion).toBe("0.6.0");
|
|
95
|
+
expect(readFileSync(join(piHome, "npm", "node_modules", "plain", "package.json"), "utf8")).toContain("0.6.0");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("git: source (no npm resolution possible either side): conservatively assumes it may have changed", async () => {
|
|
99
|
+
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
100
|
+
const bin = writeFakePi(scriptDir);
|
|
101
|
+
const piHome = writePiHome();
|
|
102
|
+
const installer = new ExecInstaller(bin, piHome);
|
|
103
|
+
|
|
104
|
+
const outcome = await installer.update("git:github.com/u/r@main");
|
|
105
|
+
|
|
106
|
+
expect(outcome.pinned).toBe(false);
|
|
107
|
+
expect(outcome.previousVersion).toBeUndefined();
|
|
108
|
+
expect(outcome.currentVersion).toBeUndefined();
|
|
109
|
+
// No ground truth either side -- must not falsely claim nothing changed.
|
|
110
|
+
expect(outcome.alreadyUpToDate).toBe(false);
|
|
111
|
+
expect(outcome.reloadRequired).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createLogger, type LogSink } from "../src/shared/log.ts";
|
|
3
|
+
|
|
4
|
+
function capture(): { lines: string[]; sink: LogSink } {
|
|
5
|
+
const lines: string[] = [];
|
|
6
|
+
return { lines, sink: (l) => lines.push(l) };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("logger", () => {
|
|
10
|
+
it("emits daemon-kit structured JSON with component, level, message, and fields", () => {
|
|
11
|
+
const { lines, sink } = capture();
|
|
12
|
+
const log = createLogger("test", sink, "debug");
|
|
13
|
+
log.info("page fetched", { from: 250, status: 200, ms: 42 });
|
|
14
|
+
const entry = JSON.parse(lines[0]!);
|
|
15
|
+
expect(entry).toMatchObject({ level: "info", component: "test", msg: "page fetched", from: 250, status: 200, ms: 42 });
|
|
16
|
+
expect(entry.timestamp).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("filters below the minimum level", () => {
|
|
20
|
+
const { lines, sink } = capture();
|
|
21
|
+
const log = createLogger("test", sink, "info");
|
|
22
|
+
log.debug("noisy");
|
|
23
|
+
log.info("kept");
|
|
24
|
+
expect(lines).toHaveLength(1);
|
|
25
|
+
expect(JSON.parse(lines[0]!).msg).toBe("kept");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("warn and error always pass at info level", () => {
|
|
29
|
+
const { lines, sink } = capture();
|
|
30
|
+
const log = createLogger("test", sink, "info");
|
|
31
|
+
log.warn("w");
|
|
32
|
+
log.error("e");
|
|
33
|
+
expect(lines).toHaveLength(2);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("fields are optional", () => {
|
|
37
|
+
const { lines, sink } = capture();
|
|
38
|
+
const log = createLogger("test", sink, "debug");
|
|
39
|
+
log.debug("plain");
|
|
40
|
+
expect(JSON.parse(lines[0]!).msg).toBe("plain");
|
|
41
|
+
});
|
|
42
|
+
});
|