@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,291 @@
|
|
|
1
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import type { Diagnostic } from "./check.ts";
|
|
4
|
+
|
|
5
|
+
export type PackageShapeKind = "keyword-only" | "manifest" | "conventional";
|
|
6
|
+
|
|
7
|
+
export interface PackageShapeEvidence {
|
|
8
|
+
kind: PackageShapeKind;
|
|
9
|
+
verified: boolean;
|
|
10
|
+
evidence: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PackedFile {
|
|
14
|
+
path: string;
|
|
15
|
+
size: number;
|
|
16
|
+
mode?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PackReport {
|
|
20
|
+
root: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
command: string[];
|
|
23
|
+
name?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
filename?: string;
|
|
26
|
+
size?: number;
|
|
27
|
+
unpackedSize?: number;
|
|
28
|
+
shasum?: string;
|
|
29
|
+
integrity?: string;
|
|
30
|
+
files: PackedFile[];
|
|
31
|
+
shape: PackageShapeEvidence;
|
|
32
|
+
diagnostics: Diagnostic[];
|
|
33
|
+
truncated: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PackCommandResult {
|
|
37
|
+
code: number;
|
|
38
|
+
stdout: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
truncated?: boolean;
|
|
41
|
+
timedOut?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type PackCommand = (cwd: string, args: string[]) => Promise<PackCommandResult>;
|
|
45
|
+
|
|
46
|
+
const PACK_ARGS = ["pack", "--dry-run", "--json", "--ignore-scripts"] as const;
|
|
47
|
+
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
48
|
+
const MAX_FILES = 2_000;
|
|
49
|
+
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
50
|
+
const TIMEOUT_MS = 30_000;
|
|
51
|
+
const SENSITIVE_PATH =
|
|
52
|
+
/(^|\/)(\.env(?:\.|$)|\.npmrc$|\.git(?:\/|$)|id_rsa$|id_ed25519$|credentials?(?:\.|$)|secrets?(?:\.|$)|.*\.(?:pem|key|p12|pfx))$/i;
|
|
53
|
+
const CONVENTIONAL_RESOURCE = /^(extensions?|skills?|prompts?|themes?)\//i;
|
|
54
|
+
|
|
55
|
+
interface NpmPackEntry {
|
|
56
|
+
name?: string;
|
|
57
|
+
version?: string;
|
|
58
|
+
filename?: string;
|
|
59
|
+
size?: number;
|
|
60
|
+
unpackedSize?: number;
|
|
61
|
+
shasum?: string;
|
|
62
|
+
integrity?: string;
|
|
63
|
+
files?: Array<{ path?: string; size?: number; mode?: number }>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function boundedText(
|
|
67
|
+
stream: ReadableStream<Uint8Array>,
|
|
68
|
+
maxBytes: number,
|
|
69
|
+
onLimit: () => void,
|
|
70
|
+
): Promise<{ text: string; truncated: boolean }> {
|
|
71
|
+
return (async () => {
|
|
72
|
+
const reader = stream.getReader();
|
|
73
|
+
const chunks: Uint8Array[] = [];
|
|
74
|
+
let bytes = 0;
|
|
75
|
+
let truncated = false;
|
|
76
|
+
for (;;) {
|
|
77
|
+
const { done, value } = await reader.read();
|
|
78
|
+
if (done) break;
|
|
79
|
+
if (bytes + value.byteLength > maxBytes) {
|
|
80
|
+
const remaining = Math.max(0, maxBytes - bytes);
|
|
81
|
+
if (remaining > 0) chunks.push(value.subarray(0, remaining));
|
|
82
|
+
truncated = true;
|
|
83
|
+
onLimit();
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
chunks.push(value);
|
|
87
|
+
bytes += value.byteLength;
|
|
88
|
+
}
|
|
89
|
+
const merged = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0));
|
|
90
|
+
let offset = 0;
|
|
91
|
+
for (const chunk of chunks) {
|
|
92
|
+
merged.set(chunk, offset);
|
|
93
|
+
offset += chunk.byteLength;
|
|
94
|
+
}
|
|
95
|
+
return { text: new TextDecoder().decode(merged), truncated };
|
|
96
|
+
})();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const runNpmPack: PackCommand = async (cwd, args) => {
|
|
100
|
+
const proc = Bun.spawn(["npm", ...args], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe", env: process.env });
|
|
101
|
+
let timedOut = false;
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
timedOut = true;
|
|
104
|
+
proc.kill();
|
|
105
|
+
}, TIMEOUT_MS);
|
|
106
|
+
const kill = () => proc.kill();
|
|
107
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
108
|
+
boundedText(proc.stdout, MAX_OUTPUT_BYTES, kill),
|
|
109
|
+
boundedText(proc.stderr, 64 * 1024, kill),
|
|
110
|
+
proc.exited,
|
|
111
|
+
]);
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
return { code, stdout: stdout.text, stderr: stderr.text, truncated: stdout.truncated || stderr.truncated, timedOut };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
117
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stringPatterns(value: unknown): string[] {
|
|
121
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, 100) : [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalize(path: string): string {
|
|
125
|
+
return path.replace(/^\.\//, "").replaceAll("\\", "/");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function matches(path: string, pattern: string): boolean {
|
|
129
|
+
const normalized = normalize(pattern);
|
|
130
|
+
try {
|
|
131
|
+
return /[*?{[\]]/.test(normalized)
|
|
132
|
+
? new Bun.Glob(normalized).match(path)
|
|
133
|
+
: path === normalized || path.startsWith(`${normalized.replace(/\/$/, "")}/`);
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function inspectShape(pkg: Record<string, unknown>, files: PackedFile[]): PackageShapeEvidence {
|
|
140
|
+
const paths = files.map((file) => file.path);
|
|
141
|
+
const pi = isRecord(pkg.pi) ? pkg.pi : undefined;
|
|
142
|
+
const evidence: string[] = [];
|
|
143
|
+
if (pi) {
|
|
144
|
+
for (const field of ["extensions", "skills", "prompts", "themes"] as const) {
|
|
145
|
+
if (stringPatterns(pi[field]).some((pattern) => paths.some((path) => matches(path, pattern)))) evidence.push(`pi.${field}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (evidence.length > 0) return { kind: "manifest", verified: true, evidence };
|
|
149
|
+
const conventional = [...new Set(paths.filter((path) => CONVENTIONAL_RESOURCE.test(path)).map((path) => path.split("/")[0]!))].slice(
|
|
150
|
+
0,
|
|
151
|
+
20,
|
|
152
|
+
);
|
|
153
|
+
if (conventional.length > 0) return { kind: "conventional", verified: true, evidence: conventional };
|
|
154
|
+
return { kind: "keyword-only", verified: true, evidence: ["no Pi manifest resources or conventional resource directories in tarball"] };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function failed(root: string, code: string, message: string): PackReport {
|
|
158
|
+
return {
|
|
159
|
+
root,
|
|
160
|
+
ok: false,
|
|
161
|
+
command: ["npm", ...PACK_ARGS],
|
|
162
|
+
files: [],
|
|
163
|
+
shape: { kind: "keyword-only", verified: false, evidence: [] },
|
|
164
|
+
diagnostics: [{ code, severity: "error", path: "package.json", message: message.slice(0, 4_000) }],
|
|
165
|
+
truncated: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export class NpmPackVerifier {
|
|
170
|
+
constructor(private readonly command: PackCommand = runNpmPack) {}
|
|
171
|
+
|
|
172
|
+
async verify(packagePath: string): Promise<PackReport> {
|
|
173
|
+
let root: string;
|
|
174
|
+
let pkg: Record<string, unknown>;
|
|
175
|
+
try {
|
|
176
|
+
root = realpathSync(resolve(packagePath));
|
|
177
|
+
const raw = readFileSync(resolve(root, "package.json"));
|
|
178
|
+
if (raw.byteLength > MAX_MANIFEST_BYTES)
|
|
179
|
+
return failed(root, "PACK_MANIFEST_TOO_LARGE", "package.json exceeds the 1 MiB verification bound");
|
|
180
|
+
pkg = JSON.parse(raw.toString()) as Record<string, unknown>;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return failed(resolve(packagePath), "PACK_MANIFEST_INVALID", error instanceof Error ? error.message : String(error));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let result: PackCommandResult;
|
|
186
|
+
try {
|
|
187
|
+
result = await this.command(root, [...PACK_ARGS]);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return failed(root, "PACK_COMMAND_FAILED", error instanceof Error ? error.message : String(error));
|
|
190
|
+
}
|
|
191
|
+
if (result.timedOut) return failed(root, "PACK_TIMEOUT", `npm pack exceeded ${TIMEOUT_MS} ms`);
|
|
192
|
+
if (result.code !== 0) return failed(root, "PACK_COMMAND_FAILED", (result.stderr || `npm pack exited ${result.code}`).slice(0, 4_000));
|
|
193
|
+
if (result.truncated) return failed(root, "PACK_OUTPUT_LIMIT", "npm pack output exceeded its bounded capture");
|
|
194
|
+
|
|
195
|
+
let entry: NpmPackEntry;
|
|
196
|
+
try {
|
|
197
|
+
const parsed = JSON.parse(result.stdout) as unknown;
|
|
198
|
+
if (!Array.isArray(parsed) || parsed.length !== 1 || !isRecord(parsed[0])) throw new Error("expected one npm pack result");
|
|
199
|
+
entry = parsed[0] as NpmPackEntry;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return failed(root, "PACK_JSON_INVALID", error instanceof Error ? error.message : String(error));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const rawFiles = Array.isArray(entry.files) ? entry.files : [];
|
|
205
|
+
const truncated = rawFiles.length > MAX_FILES;
|
|
206
|
+
const files = rawFiles.slice(0, MAX_FILES).flatMap((file) =>
|
|
207
|
+
typeof file.path === "string"
|
|
208
|
+
? [
|
|
209
|
+
{
|
|
210
|
+
path: normalize(file.path).slice(0, 4_096),
|
|
211
|
+
size: Number.isFinite(file.size) ? Math.max(0, Number(file.size)) : 0,
|
|
212
|
+
mode: file.mode,
|
|
213
|
+
},
|
|
214
|
+
]
|
|
215
|
+
: [],
|
|
216
|
+
);
|
|
217
|
+
const diagnostics: Diagnostic[] = [];
|
|
218
|
+
if (truncated)
|
|
219
|
+
diagnostics.push({ code: "PACK_FILE_LIMIT", severity: "error", path: ".", message: `tarball has more than ${MAX_FILES} files` });
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
if (isAbsolute(file.path) || file.path.split("/").includes(".."))
|
|
222
|
+
diagnostics.push({
|
|
223
|
+
code: "PACK_PATH_INVALID",
|
|
224
|
+
severity: "error",
|
|
225
|
+
path: file.path,
|
|
226
|
+
message: "npm pack reported a path outside the package tarball root",
|
|
227
|
+
});
|
|
228
|
+
if (SENSITIVE_PATH.test(file.path))
|
|
229
|
+
diagnostics.push({
|
|
230
|
+
code: "PACK_SENSITIVE_FILE",
|
|
231
|
+
severity: "error",
|
|
232
|
+
path: file.path,
|
|
233
|
+
message: "sensitive-looking file is included in the npm tarball",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const pi = isRecord(pkg.pi) ? pkg.pi : undefined;
|
|
237
|
+
if (pi) {
|
|
238
|
+
for (const field of ["extensions", "skills", "prompts", "themes"] as const) {
|
|
239
|
+
for (const pattern of stringPatterns(pi[field])) {
|
|
240
|
+
if (!files.some((file) => matches(file.path, pattern)))
|
|
241
|
+
diagnostics.push({
|
|
242
|
+
code: "PACK_DECLARED_RESOURCE_MISSING",
|
|
243
|
+
severity: "error",
|
|
244
|
+
path: "package.json",
|
|
245
|
+
message: `pi.${field} resource ${pattern} is absent from npm pack output`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if ((entry.unpackedSize ?? 0) > 20 * 1024 * 1024)
|
|
251
|
+
diagnostics.push({ code: "PACK_UNPACKED_SIZE_HIGH", severity: "warning", path: ".", message: "tarball expands beyond 20 MiB" });
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
root,
|
|
255
|
+
ok: !diagnostics.some((item) => item.severity === "error"),
|
|
256
|
+
command: ["npm", ...PACK_ARGS],
|
|
257
|
+
name: entry.name,
|
|
258
|
+
version: entry.version,
|
|
259
|
+
filename: entry.filename,
|
|
260
|
+
size: entry.size,
|
|
261
|
+
unpackedSize: entry.unpackedSize,
|
|
262
|
+
shasum: entry.shasum,
|
|
263
|
+
integrity: entry.integrity,
|
|
264
|
+
files,
|
|
265
|
+
shape: inspectShape(pkg, files),
|
|
266
|
+
diagnostics,
|
|
267
|
+
truncated,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function formatPackReport(report: PackReport, json = false): string {
|
|
273
|
+
if (json) {
|
|
274
|
+
let fileLimit = Math.min(report.files.length, 200);
|
|
275
|
+
let value: PackReport & { outputTruncated?: boolean } = report;
|
|
276
|
+
let encoded = JSON.stringify(value);
|
|
277
|
+
while (encoded.length > 64 * 1024 && fileLimit > 0) {
|
|
278
|
+
fileLimit = Math.floor(fileLimit / 2);
|
|
279
|
+
value = { ...report, files: report.files.slice(0, fileLimit), diagnostics: report.diagnostics.slice(0, 10), outputTruncated: true };
|
|
280
|
+
encoded = JSON.stringify(value);
|
|
281
|
+
}
|
|
282
|
+
return `${encoded}\n`;
|
|
283
|
+
}
|
|
284
|
+
let out = `${report.ok ? "ok" : "failed"}: ${report.name ?? report.root}${report.version ? `@${report.version}` : ""}\n`;
|
|
285
|
+
out += `shape: ${report.shape.kind}${report.shape.verified ? " (verified from npm pack output)" : ""}\n`;
|
|
286
|
+
if (report.integrity) out += `integrity: ${report.integrity}\n`;
|
|
287
|
+
out += `files: ${report.files.length}, packed: ${report.size ?? "?"} bytes, unpacked: ${report.unpackedSize ?? "?"} bytes\n`;
|
|
288
|
+
for (const diagnostic of report.diagnostics)
|
|
289
|
+
out += `${diagnostic.severity} ${diagnostic.code} ${diagnostic.path}: ${diagnostic.message}\n`;
|
|
290
|
+
return out.slice(0, 16 * 1024);
|
|
291
|
+
}
|