@danypops/pi-packed 0.19.9 → 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/approval/permission.ts +1 -1
- package/extension/src/packed.ts +2 -2
- package/extension/src/tabs/security-tui.ts +1 -1
- package/extension/src/tool-output.ts +1 -1
- 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
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
|
|
6
|
+
import { createApp, type OperationInputs, type OperationName, type OperationOutputs } from "../src/daemon/service.ts";
|
|
7
|
+
import type { Installer, PkgInfo, Registry, SearchPage } from "../src/packages/package.ts";
|
|
8
|
+
import { listPackageResources, toggleResource } from "../src/packages/resources.ts";
|
|
9
|
+
|
|
10
|
+
const roots: string[] = [];
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function piHome(settingsPackages: unknown[]): string {
|
|
16
|
+
const root = mkdtempSync(join(tmpdir(), "packed-resources-"));
|
|
17
|
+
roots.push(root);
|
|
18
|
+
writeFileSync(join(root, "settings.json"), JSON.stringify({ packages: settingsPackages }, null, 2));
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function installNpmPackage(home: string, name: string, pi: Record<string, unknown>, files: Record<string, string>): void {
|
|
23
|
+
const dir = join(home, "npm", "node_modules", name);
|
|
24
|
+
mkdirSync(dir, { recursive: true });
|
|
25
|
+
writeFileSync(join(dir, "package.json"), JSON.stringify({ name, version: "1.0.0", pi }, null, 2));
|
|
26
|
+
for (const [path, content] of Object.entries(files)) {
|
|
27
|
+
mkdirSync(join(dir, path, ".."), { recursive: true });
|
|
28
|
+
writeFileSync(join(dir, path), content);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("listPackageResources", () => {
|
|
33
|
+
it("lists an npm package's discovered resources with the default all-enabled state", () => {
|
|
34
|
+
const home = piHome(["npm:pi-demo"]);
|
|
35
|
+
installNpmPackage(
|
|
36
|
+
home,
|
|
37
|
+
"pi-demo",
|
|
38
|
+
{ extensions: ["extensions/*.ts"], skills: ["skills/*/SKILL.md"] },
|
|
39
|
+
{
|
|
40
|
+
"extensions/one.ts": "",
|
|
41
|
+
"extensions/two.ts": "",
|
|
42
|
+
"skills/review/SKILL.md": "",
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const { global, project } = listPackageResources(home);
|
|
47
|
+
|
|
48
|
+
expect(project).toEqual([]);
|
|
49
|
+
expect(global).toHaveLength(1);
|
|
50
|
+
expect(global[0]?.source).toBe("npm:pi-demo");
|
|
51
|
+
expect(global[0]?.extensions).toEqual([
|
|
52
|
+
{ path: "extensions/one.ts", enabled: true },
|
|
53
|
+
{ path: "extensions/two.ts", enabled: true },
|
|
54
|
+
]);
|
|
55
|
+
expect(global[0]?.skills).toEqual([{ path: "skills/review/SKILL.md", enabled: true }]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("applies a settings-level filter, narrowing what the package already declares", () => {
|
|
59
|
+
const home = piHome([{ source: "npm:pi-demo", extensions: ["!extensions/two.ts"] }]);
|
|
60
|
+
installNpmPackage(home, "pi-demo", { extensions: ["extensions/*.ts"] }, { "extensions/one.ts": "", "extensions/two.ts": "" });
|
|
61
|
+
|
|
62
|
+
const { global } = listPackageResources(home);
|
|
63
|
+
|
|
64
|
+
expect(global[0]?.extensions).toEqual([
|
|
65
|
+
{ path: "extensions/one.ts", enabled: true },
|
|
66
|
+
{ path: "extensions/two.ts", enabled: false },
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("treats an empty filter array as loading none of that resource type", () => {
|
|
71
|
+
const home = piHome([{ source: "npm:pi-demo", extensions: [] }]);
|
|
72
|
+
installNpmPackage(home, "pi-demo", { extensions: ["extensions/*.ts"] }, { "extensions/one.ts": "" });
|
|
73
|
+
|
|
74
|
+
const { global } = listPackageResources(home);
|
|
75
|
+
|
|
76
|
+
expect(global[0]?.extensions).toEqual([{ path: "extensions/one.ts", enabled: false }]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("omits git:, https:, and local sources rather than reporting them inaccurately", () => {
|
|
80
|
+
const home = piHome(["git:github.com/example/repo", "https://example.test/pkg", "/abs/local/pkg", "npm:pi-demo"]);
|
|
81
|
+
installNpmPackage(home, "pi-demo", {}, { "extensions/one.ts": "" });
|
|
82
|
+
|
|
83
|
+
const { global } = listPackageResources(home);
|
|
84
|
+
|
|
85
|
+
expect(global).toHaveLength(1);
|
|
86
|
+
expect(global[0]?.source).toBe("npm:pi-demo");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("reads project settings and resolves project-local npm installs under .pi/, matching setup.ts's own -l convention", () => {
|
|
90
|
+
const home = piHome(["npm:pi-demo"]);
|
|
91
|
+
installNpmPackage(home, "pi-demo", {}, { "extensions/one.ts": "" });
|
|
92
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "packed-resources-project-"));
|
|
93
|
+
roots.push(projectRoot);
|
|
94
|
+
const projectHome = join(projectRoot, ".pi");
|
|
95
|
+
mkdirSync(projectHome, { recursive: true });
|
|
96
|
+
writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-demo-local"] }));
|
|
97
|
+
installNpmPackage(projectHome, "pi-demo-local", {}, { "extensions/local.ts": "" });
|
|
98
|
+
|
|
99
|
+
const { global, project } = listPackageResources(home, projectRoot);
|
|
100
|
+
|
|
101
|
+
expect(global).toHaveLength(1);
|
|
102
|
+
expect(project).toHaveLength(1);
|
|
103
|
+
expect(project[0]?.source).toBe("npm:pi-demo-local");
|
|
104
|
+
expect(project[0]?.scope).toBe("project");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("is unaffected by a missing project settings file", () => {
|
|
108
|
+
const home = piHome(["npm:pi-demo"]);
|
|
109
|
+
installNpmPackage(home, "pi-demo", {}, { "extensions/one.ts": "" });
|
|
110
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "packed-resources-project-"));
|
|
111
|
+
roots.push(projectRoot);
|
|
112
|
+
|
|
113
|
+
const { global, project } = listPackageResources(home, projectRoot);
|
|
114
|
+
|
|
115
|
+
expect(global).toHaveLength(1);
|
|
116
|
+
expect(project).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
class NoopRegistry implements Registry {
|
|
121
|
+
async search(): Promise<SearchPage> {
|
|
122
|
+
return { results: [], total: 0 };
|
|
123
|
+
}
|
|
124
|
+
async searchPage(): Promise<SearchPage> {
|
|
125
|
+
return { results: [], total: 0 };
|
|
126
|
+
}
|
|
127
|
+
async searchAll() {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
async info(name: string): Promise<PkgInfo> {
|
|
131
|
+
return { name, version: "1.0.0" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class NoopInstaller implements Installer {
|
|
136
|
+
async install() {
|
|
137
|
+
return "ok";
|
|
138
|
+
}
|
|
139
|
+
async remove() {
|
|
140
|
+
return "ok";
|
|
141
|
+
}
|
|
142
|
+
async update() {
|
|
143
|
+
return { output: "ok", reloadRequired: false, alreadyUpToDate: true, pinned: false };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function rpcClient(piHome: string) {
|
|
148
|
+
const app = createApp({
|
|
149
|
+
reg: new NoopRegistry(),
|
|
150
|
+
inst: new NoopInstaller(),
|
|
151
|
+
token: "test-token",
|
|
152
|
+
stateDir: mkdtempSync(join(tmpdir(), "packed-resources-state-")),
|
|
153
|
+
dataDir: mkdtempSync(join(tmpdir(), "packed-resources-data-")),
|
|
154
|
+
piHome,
|
|
155
|
+
});
|
|
156
|
+
return new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
|
|
157
|
+
label: "Packed",
|
|
158
|
+
transport: (request) => app.fetch(request),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
describe("resources.list / resources.toggle (daemon RPC wiring)", () => {
|
|
163
|
+
it("lists resources through the authenticated RPC operation, matching the pure domain function", async () => {
|
|
164
|
+
const home = piHome(["npm:pi-demo"]);
|
|
165
|
+
installNpmPackage(home, "pi-demo", { extensions: ["extensions/*.ts"] }, { "extensions/one.ts": "" });
|
|
166
|
+
const client = rpcClient(home);
|
|
167
|
+
|
|
168
|
+
const result = await client.call("resources.list", {});
|
|
169
|
+
|
|
170
|
+
expect(result).toEqual(listPackageResources(home));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("requires approval before toggling, matching every other settings-mutating operation", async () => {
|
|
174
|
+
const home = piHome(["npm:pi-demo"]);
|
|
175
|
+
installNpmPackage(home, "pi-demo", { extensions: ["extensions/*.ts"] }, { "extensions/one.ts": "" });
|
|
176
|
+
const client = rpcClient(home);
|
|
177
|
+
|
|
178
|
+
await expect(
|
|
179
|
+
client.call("resources.toggle", { source: "npm:pi-demo", field: "extensions", path: "extensions/one.ts", enabled: false }),
|
|
180
|
+
).rejects.toThrow();
|
|
181
|
+
|
|
182
|
+
const approved = await client.call("resources.toggle", {
|
|
183
|
+
source: "npm:pi-demo",
|
|
184
|
+
field: "extensions",
|
|
185
|
+
path: "extensions/one.ts",
|
|
186
|
+
enabled: false,
|
|
187
|
+
approved: true,
|
|
188
|
+
});
|
|
189
|
+
expect(approved.ok).toBe(true);
|
|
190
|
+
const { global } = await client.call("resources.list", {});
|
|
191
|
+
expect(global[0]?.extensions).toEqual([{ path: "extensions/one.ts", enabled: false }]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("rejects a path-escaping toggle request instead of writing it", async () => {
|
|
195
|
+
const home = piHome(["npm:pi-demo"]);
|
|
196
|
+
installNpmPackage(home, "pi-demo", { extensions: ["extensions/*.ts"] }, { "extensions/one.ts": "" });
|
|
197
|
+
const client = rpcClient(home);
|
|
198
|
+
|
|
199
|
+
await expect(
|
|
200
|
+
client.call("resources.toggle", {
|
|
201
|
+
source: "npm:pi-demo",
|
|
202
|
+
field: "extensions",
|
|
203
|
+
path: "../../etc/passwd",
|
|
204
|
+
enabled: false,
|
|
205
|
+
approved: true,
|
|
206
|
+
}),
|
|
207
|
+
).rejects.toThrow();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("toggleResource", () => {
|
|
212
|
+
it("disables a resource with a minimal -path override, leaving the package's own glob defaults untouched", async () => {
|
|
213
|
+
const home = piHome(["npm:pi-demo"]);
|
|
214
|
+
installNpmPackage(home, "pi-demo", { extensions: ["extensions/*.ts"] }, { "extensions/one.ts": "" });
|
|
215
|
+
const settingsPath = join(home, "settings.json");
|
|
216
|
+
|
|
217
|
+
const result = await toggleResource({
|
|
218
|
+
settingsPath,
|
|
219
|
+
source: "npm:pi-demo",
|
|
220
|
+
field: "extensions",
|
|
221
|
+
path: "extensions/one.ts",
|
|
222
|
+
enabled: false,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result.ok).toBe(true);
|
|
226
|
+
const written = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
227
|
+
expect(written.packages).toEqual([{ source: "npm:pi-demo", extensions: ["-extensions/one.ts"] }]);
|
|
228
|
+
const { global } = listPackageResources(home);
|
|
229
|
+
expect(global[0]?.extensions).toEqual([{ path: "extensions/one.ts", enabled: false }]);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("is idempotent and replaces a prior override for the same exact path instead of accumulating duplicates", async () => {
|
|
233
|
+
const home = piHome([{ source: "npm:pi-demo", extensions: ["-extensions/one.ts"] }]);
|
|
234
|
+
installNpmPackage(home, "pi-demo", { extensions: ["extensions/*.ts"] }, { "extensions/one.ts": "" });
|
|
235
|
+
const settingsPath = join(home, "settings.json");
|
|
236
|
+
|
|
237
|
+
await toggleResource({ settingsPath, source: "npm:pi-demo", field: "extensions", path: "extensions/one.ts", enabled: true });
|
|
238
|
+
|
|
239
|
+
const written = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
240
|
+
expect(written.packages).toEqual([{ source: "npm:pi-demo", extensions: ["+extensions/one.ts"] }]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("fails closed with an error, not a throw, when the package is not in settings", async () => {
|
|
244
|
+
const home = piHome(["npm:other"]);
|
|
245
|
+
const result = await toggleResource({
|
|
246
|
+
settingsPath: join(home, "settings.json"),
|
|
247
|
+
source: "npm:pi-demo",
|
|
248
|
+
field: "extensions",
|
|
249
|
+
path: "extensions/one.ts",
|
|
250
|
+
enabled: false,
|
|
251
|
+
});
|
|
252
|
+
expect(result.ok).toBe(false);
|
|
253
|
+
expect(result.error).toContain("not found");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { PACKAGE_OPERATIONS, packagePermissionDecision, readSecuritySettings, writeSecuritySettings } from "../src/security/security.ts";
|
|
6
|
+
|
|
7
|
+
describe("package permission policy", () => {
|
|
8
|
+
it("defaults every arbitrary-code and settings/install-root mutation to approval", () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "packed-security-"));
|
|
10
|
+
const settings = readSecuritySettings(dir);
|
|
11
|
+
expect(settings).toEqual({ mutationApproval: "always" });
|
|
12
|
+
expect(PACKAGE_OPERATIONS).toEqual([
|
|
13
|
+
"search",
|
|
14
|
+
"info",
|
|
15
|
+
"installed",
|
|
16
|
+
"catalog",
|
|
17
|
+
"index.status",
|
|
18
|
+
"index.build",
|
|
19
|
+
"updates",
|
|
20
|
+
"check",
|
|
21
|
+
"setup.plan",
|
|
22
|
+
"security.read",
|
|
23
|
+
"mirror",
|
|
24
|
+
"setup.export",
|
|
25
|
+
"setup.update",
|
|
26
|
+
"install",
|
|
27
|
+
"install_service",
|
|
28
|
+
"restart_service",
|
|
29
|
+
"setup.apply",
|
|
30
|
+
"update",
|
|
31
|
+
"update.self",
|
|
32
|
+
"remove",
|
|
33
|
+
"resources.list",
|
|
34
|
+
"resources.toggle",
|
|
35
|
+
"security.write",
|
|
36
|
+
"pi.status",
|
|
37
|
+
"advisories.scan",
|
|
38
|
+
"doctor",
|
|
39
|
+
]);
|
|
40
|
+
const decisions = Object.fromEntries(
|
|
41
|
+
PACKAGE_OPERATIONS.map((operation) => [operation, packagePermissionDecision(settings, operation)]),
|
|
42
|
+
);
|
|
43
|
+
expect(decisions).toMatchObject({
|
|
44
|
+
search: { classification: "read", approvalRequired: false },
|
|
45
|
+
info: { classification: "read", approvalRequired: false },
|
|
46
|
+
installed: { classification: "read", approvalRequired: false },
|
|
47
|
+
catalog: { classification: "read", approvalRequired: false },
|
|
48
|
+
"index.status": { classification: "read", approvalRequired: false },
|
|
49
|
+
"index.build": { classification: "maintenance", approvalRequired: false },
|
|
50
|
+
updates: { classification: "read", approvalRequired: false },
|
|
51
|
+
check: { classification: "read", approvalRequired: false },
|
|
52
|
+
"setup.plan": { classification: "read", approvalRequired: false },
|
|
53
|
+
"security.read": { classification: "read", approvalRequired: false },
|
|
54
|
+
mirror: { classification: "maintenance", approvalRequired: false },
|
|
55
|
+
"setup.export": { classification: "maintenance", approvalRequired: false },
|
|
56
|
+
"setup.update": { classification: "maintenance", approvalRequired: false },
|
|
57
|
+
install: { classification: "code-execution", approvalRequired: true },
|
|
58
|
+
install_service: { classification: "code-execution", approvalRequired: true },
|
|
59
|
+
restart_service: { classification: "code-execution", approvalRequired: true },
|
|
60
|
+
"setup.apply": { classification: "code-execution", approvalRequired: true },
|
|
61
|
+
update: { classification: "code-execution", approvalRequired: true },
|
|
62
|
+
"update.self": { classification: "code-execution", approvalRequired: true },
|
|
63
|
+
remove: { classification: "settings-mutation", approvalRequired: true },
|
|
64
|
+
"resources.list": { classification: "read", approvalRequired: false },
|
|
65
|
+
"resources.toggle": { classification: "settings-mutation", approvalRequired: true },
|
|
66
|
+
"security.write": { classification: "security-mutation", approvalRequired: true },
|
|
67
|
+
"pi.status": { classification: "read", approvalRequired: false },
|
|
68
|
+
"advisories.scan": { classification: "read", approvalRequired: false },
|
|
69
|
+
doctor: { classification: "read", approvalRequired: false },
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("persists an explicit unsafe opt-out and migrates the prior storage key", async () => {
|
|
74
|
+
const dir = mkdtempSync(join(tmpdir(), "packed-security-"));
|
|
75
|
+
expect(await writeSecuritySettings(dir, { mutationApproval: "never" })).toEqual({ mutationApproval: "never" });
|
|
76
|
+
expect(readSecuritySettings(dir)).toEqual({ mutationApproval: "never" });
|
|
77
|
+
|
|
78
|
+
const legacyDir = mkdtempSync(join(tmpdir(), "packed-security-legacy-"));
|
|
79
|
+
writeFileSync(join(legacyDir, "security.json"), '{"installApproval":"never"}\n');
|
|
80
|
+
expect(readSecuritySettings(legacyDir)).toEqual({ mutationApproval: "never" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("makes the explicit opt-out govern every guarded operation", () => {
|
|
84
|
+
const settings = { mutationApproval: "never" as const };
|
|
85
|
+
for (const operation of PACKAGE_OPERATIONS) {
|
|
86
|
+
expect(packagePermissionDecision(settings, operation).approvalRequired).toBe(false);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { PkgInfo, Registry } from "../src/packages/package.ts";
|
|
3
|
+
import type { InteractiveRunResult } from "../src/publish/publish.ts";
|
|
4
|
+
import { detectSelfInstallMethod, PACKED_PACKAGE_NAME, runSelfUpdate, type SelfUpdateDeps } from "../src/self-update/self-update.ts";
|
|
5
|
+
import { VERSION } from "../src/shared/version.ts";
|
|
6
|
+
|
|
7
|
+
class FakeRegistry implements Pick<Registry, "info"> {
|
|
8
|
+
constructor(private version: string) {}
|
|
9
|
+
async info(): Promise<PkgInfo> {
|
|
10
|
+
return { name: PACKED_PACKAGE_NAME, version: this.version };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ok = (code = 0): InteractiveRunResult => ({ ok: true, code });
|
|
15
|
+
const failed = (code = 1): InteractiveRunResult => ({ ok: false, code });
|
|
16
|
+
|
|
17
|
+
describe("detectSelfInstallMethod", () => {
|
|
18
|
+
it("recognizes an npm-global install by its node_modules path", () => {
|
|
19
|
+
const method = detectSelfInstallMethod(`file:///usr/local/lib/node_modules/${PACKED_PACKAGE_NAME}/src/cli.ts`);
|
|
20
|
+
expect(method).toEqual({ kind: "npm-global" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("recognizes a Bun-global install by its install/global/node_modules path", () => {
|
|
24
|
+
const method = detectSelfInstallMethod(`file:///home/dev/.cache/.bun/install/global/node_modules/${PACKED_PACKAGE_NAME}/src/cli.ts`);
|
|
25
|
+
expect(method).toEqual({ kind: "bun-global" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("prefers bun-global over the broader npm-global check, since a bun-global path also contains node_modules/<pkg>/", () => {
|
|
29
|
+
const method = detectSelfInstallMethod(`file:///home/dev/.bun/install/global/node_modules/${PACKED_PACKAGE_NAME}/src/cli.ts`);
|
|
30
|
+
expect(method.kind).toBe("bun-global");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("recognizes anything else as a local checkout, carrying the real resolved path", () => {
|
|
34
|
+
const method = detectSelfInstallMethod("file:///home/dev/Projects/packed/packages/packed/src/cli.ts");
|
|
35
|
+
expect(method.kind).toBe("local-checkout");
|
|
36
|
+
expect(method.kind === "local-checkout" && method.path).toBe("/home/dev/Projects/packed/packages/packed/src/cli.ts");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("runSelfUpdate", () => {
|
|
41
|
+
function deps(overrides: Partial<SelfUpdateDeps> = {}): SelfUpdateDeps {
|
|
42
|
+
return { registry: new FakeRegistry("9.9.9"), installMethod: { kind: "npm-global" }, ...overrides };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it("never attempts an npm install for a local checkout -- update it with git, not packed", async () => {
|
|
46
|
+
let called = false;
|
|
47
|
+
const report = await runSelfUpdate(
|
|
48
|
+
deps({
|
|
49
|
+
installMethod: { kind: "local-checkout", path: "/home/dev/packed" },
|
|
50
|
+
runNpmInstall: async () => {
|
|
51
|
+
called = true;
|
|
52
|
+
return ok();
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
expect(called).toBe(false);
|
|
57
|
+
expect(report.updated).toBe(false);
|
|
58
|
+
expect(report.message).toContain("local checkout at /home/dev/packed");
|
|
59
|
+
expect(report.message).toContain("git pull");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("still restarts the service for a local checkout -- that's the actual point for a dev loop", async () => {
|
|
63
|
+
let restarted = false;
|
|
64
|
+
const report = await runSelfUpdate(
|
|
65
|
+
deps({
|
|
66
|
+
installMethod: { kind: "local-checkout", path: "/home/dev/packed" },
|
|
67
|
+
isServiceInstalled: () => true,
|
|
68
|
+
restartService: async () => {
|
|
69
|
+
restarted = true;
|
|
70
|
+
return ok();
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
expect(restarted).toBe(true);
|
|
75
|
+
expect(report.ok).toBe(true);
|
|
76
|
+
expect(report.restarted).toBe(true);
|
|
77
|
+
expect(report.updated).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("runs npm install --global for an npm-global install and reports the version transition", async () => {
|
|
81
|
+
let args: string[] | undefined;
|
|
82
|
+
const report = await runSelfUpdate(
|
|
83
|
+
deps({
|
|
84
|
+
runNpmInstall: async (a) => {
|
|
85
|
+
args = a;
|
|
86
|
+
return ok();
|
|
87
|
+
},
|
|
88
|
+
isServiceInstalled: () => false,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
expect(args).toEqual(["install", "--global", `${PACKED_PACKAGE_NAME}@latest`]);
|
|
92
|
+
expect(report.updated).toBe(true);
|
|
93
|
+
expect(report.previousVersion).toBe(VERSION);
|
|
94
|
+
expect(report.latestVersion).toBe("9.9.9");
|
|
95
|
+
expect(report.message).toContain("updated via npm");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("fails, never restarting, when npm install itself fails", async () => {
|
|
99
|
+
let restartCalled = false;
|
|
100
|
+
const report = await runSelfUpdate(
|
|
101
|
+
deps({
|
|
102
|
+
runNpmInstall: async () => failed(1),
|
|
103
|
+
isServiceInstalled: () => true,
|
|
104
|
+
restartService: async () => {
|
|
105
|
+
restartCalled = true;
|
|
106
|
+
return ok();
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
expect(report.ok).toBe(false);
|
|
111
|
+
expect(report.updated).toBe(false);
|
|
112
|
+
expect(report.restarted).toBe(false);
|
|
113
|
+
expect(restartCalled).toBe(false);
|
|
114
|
+
expect(report.message).toContain("npm install --global");
|
|
115
|
+
expect(report.message).toContain("failed (exit 1)");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("proceeds even when the registry lookup for the latest version fails -- best-effort only", async () => {
|
|
119
|
+
const report = await runSelfUpdate(
|
|
120
|
+
deps({
|
|
121
|
+
registry: {
|
|
122
|
+
info: async () => {
|
|
123
|
+
throw new Error("registry down");
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
runNpmInstall: async () => ok(),
|
|
127
|
+
isServiceInstalled: () => false,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
expect(report.updated).toBe(true);
|
|
131
|
+
expect(report.latestVersion).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("reports no supervised service found, never guessing at a restart", async () => {
|
|
135
|
+
const report = await runSelfUpdate(
|
|
136
|
+
deps({
|
|
137
|
+
runNpmInstall: async () => ok(),
|
|
138
|
+
isServiceInstalled: () => false,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
expect(report.ok).toBe(true);
|
|
142
|
+
expect(report.restarted).toBe(false);
|
|
143
|
+
expect(report.message).toContain("no supervised pi-packed service was found");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("reports restart-unsupported explicitly when the platform has no restart mechanism wired in", async () => {
|
|
147
|
+
const report = await runSelfUpdate(
|
|
148
|
+
deps({
|
|
149
|
+
runNpmInstall: async () => ok(),
|
|
150
|
+
isServiceInstalled: () => true,
|
|
151
|
+
restartService: undefined,
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
expect(report.ok).toBe(true);
|
|
155
|
+
expect(report.updated).toBe(true);
|
|
156
|
+
expect(report.restarted).toBe(false);
|
|
157
|
+
expect(report.message).toContain("isn't supported on this platform yet");
|
|
158
|
+
expect(report.message).toContain("systemctl --user restart pi-packed.service");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("reports an updated-but-not-restarted outcome, never a full failure, when the restart command itself fails", async () => {
|
|
162
|
+
const report = await runSelfUpdate(
|
|
163
|
+
deps({
|
|
164
|
+
runNpmInstall: async () => ok(),
|
|
165
|
+
isServiceInstalled: () => true,
|
|
166
|
+
restartService: async () => failed(3),
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
expect(report.ok).toBe(true);
|
|
170
|
+
expect(report.updated).toBe(true);
|
|
171
|
+
expect(report.restarted).toBe(false);
|
|
172
|
+
expect(report.message).toContain("restarting pi-packed.service failed (exit 3)");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("reports full success when the update and restart both succeed", async () => {
|
|
176
|
+
const report = await runSelfUpdate(
|
|
177
|
+
deps({
|
|
178
|
+
runNpmInstall: async () => ok(),
|
|
179
|
+
isServiceInstalled: () => true,
|
|
180
|
+
restartService: async () => ok(),
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
expect(report).toEqual({
|
|
184
|
+
ok: true,
|
|
185
|
+
previousVersion: VERSION,
|
|
186
|
+
latestVersion: "9.9.9",
|
|
187
|
+
updated: true,
|
|
188
|
+
restarted: true,
|
|
189
|
+
message: "updated via npm; restarted the pi-packed service",
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("runs bun add --global for a bun-global install, never touching npm, and reports the version transition", async () => {
|
|
194
|
+
let bunArgs: string[] | undefined;
|
|
195
|
+
let npmCalled = false;
|
|
196
|
+
const report = await runSelfUpdate(
|
|
197
|
+
deps({
|
|
198
|
+
installMethod: { kind: "bun-global" },
|
|
199
|
+
runBunInstall: async (a) => {
|
|
200
|
+
bunArgs = a;
|
|
201
|
+
return ok();
|
|
202
|
+
},
|
|
203
|
+
runNpmInstall: async () => {
|
|
204
|
+
npmCalled = true;
|
|
205
|
+
return ok();
|
|
206
|
+
},
|
|
207
|
+
isServiceInstalled: () => false,
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
expect(bunArgs).toEqual(["add", "--global", `${PACKED_PACKAGE_NAME}@latest`]);
|
|
211
|
+
expect(npmCalled).toBe(false);
|
|
212
|
+
expect(report.updated).toBe(true);
|
|
213
|
+
expect(report.previousVersion).toBe(VERSION);
|
|
214
|
+
expect(report.latestVersion).toBe("9.9.9");
|
|
215
|
+
expect(report.message).toContain("updated via bun");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("fails, never restarting, when bun add itself fails", async () => {
|
|
219
|
+
let restartCalled = false;
|
|
220
|
+
const report = await runSelfUpdate(
|
|
221
|
+
deps({
|
|
222
|
+
installMethod: { kind: "bun-global" },
|
|
223
|
+
runBunInstall: async () => failed(1),
|
|
224
|
+
isServiceInstalled: () => true,
|
|
225
|
+
restartService: async () => {
|
|
226
|
+
restartCalled = true;
|
|
227
|
+
return ok();
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
expect(report.ok).toBe(false);
|
|
232
|
+
expect(report.updated).toBe(false);
|
|
233
|
+
expect(report.restarted).toBe(false);
|
|
234
|
+
expect(restartCalled).toBe(false);
|
|
235
|
+
expect(report.message).toContain("bun add --global");
|
|
236
|
+
expect(report.message).toContain("failed (exit 1)");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("restarts the service after a successful bun-global update, same as npm-global", async () => {
|
|
240
|
+
const report = await runSelfUpdate(
|
|
241
|
+
deps({
|
|
242
|
+
installMethod: { kind: "bun-global" },
|
|
243
|
+
runBunInstall: async () => ok(),
|
|
244
|
+
isServiceInstalled: () => true,
|
|
245
|
+
restartService: async () => ok(),
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
expect(report).toEqual({
|
|
249
|
+
ok: true,
|
|
250
|
+
previousVersion: VERSION,
|
|
251
|
+
latestVersion: "9.9.9",
|
|
252
|
+
updated: true,
|
|
253
|
+
restarted: true,
|
|
254
|
+
message: "updated via bun; restarted the pi-packed service",
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|