@tooldeck/plugin-package 0.1.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 +202 -0
- package/README.md +51 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/digest.d.ts +2 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fflate-zip-adapter.d.ts +14 -0
- package/dist/fflate-zip-adapter.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +710 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +4 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/pack.d.ts +5 -0
- package/dist/pack.d.ts.map +1 -0
- package/dist/package-manifest.d.ts +11 -0
- package/dist/package-manifest.d.ts.map +1 -0
- package/dist/package-reader.d.ts +8 -0
- package/dist/package-reader.d.ts.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/zip-adapter.d.ts +26 -0
- package/dist/zip-adapter.d.ts.map +1 -0
- package/dist/zip-central-directory.d.ts +7 -0
- package/dist/zip-central-directory.d.ts.map +1 -0
- package/dist/zip-entry-policy.d.ts +4 -0
- package/dist/zip-entry-policy.d.ts.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { strFromU8, strToU8, unzipSync, zipSync } from "fflate";
|
|
5
|
+
import { manifestV1Schema } from "@tooldeck/protocol";
|
|
6
|
+
import Ajv from "ajv";
|
|
7
|
+
//#region src/constants.ts
|
|
8
|
+
var TOOLDECK_PACKAGE_EXTENSION = ".tdplugin";
|
|
9
|
+
var TOOLDECK_PACKAGE_MANIFEST_PATH = "tooldeck-package.json";
|
|
10
|
+
var TOOLDECK_PLUGIN_MANIFEST_PATH = "manifest.json";
|
|
11
|
+
var TOOLDECK_PACKAGE_FORMAT_VERSION = "1.0";
|
|
12
|
+
var DEFAULT_PACKAGE_LIMITS = {
|
|
13
|
+
maxPackageSizeBytes: 50 * 1024 * 1024,
|
|
14
|
+
maxUncompressedSizeBytes: 50 * 1024 * 1024,
|
|
15
|
+
maxRegularFileCount: 1e3
|
|
16
|
+
};
|
|
17
|
+
var DEFAULT_PACKAGE_INCLUDE_DIRS = ["dist", "assets"];
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/errors.ts
|
|
20
|
+
var TooldeckPackageError = class extends Error {
|
|
21
|
+
code;
|
|
22
|
+
context;
|
|
23
|
+
constructor(code, message, context = {}) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "TooldeckPackageError";
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.context = context;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function packageError(code, message, context = {}) {
|
|
31
|
+
return new TooldeckPackageError(code, message, context);
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/paths.ts
|
|
35
|
+
function normalizePackagePath(input) {
|
|
36
|
+
const collapsed = input.replace(/\\/g, "/").trim().replace(/^\.\/+/, "").replace(/\/+/g, "/");
|
|
37
|
+
return collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
38
|
+
}
|
|
39
|
+
function assertSafePackagePath(input, fieldPath) {
|
|
40
|
+
const normalized = normalizePackagePath(input);
|
|
41
|
+
if (normalized.length === 0) throw packageError("INVALID_PACKAGE_PATH", "Package path must not be empty.", {
|
|
42
|
+
entryPath: input,
|
|
43
|
+
fieldPath,
|
|
44
|
+
reason: "empty path"
|
|
45
|
+
});
|
|
46
|
+
if (normalized.startsWith("/") || normalized.startsWith("//")) throw packageError("INVALID_PACKAGE_PATH", "Package path must be relative.", {
|
|
47
|
+
entryPath: input,
|
|
48
|
+
fieldPath,
|
|
49
|
+
reason: "absolute path"
|
|
50
|
+
});
|
|
51
|
+
if (/^[A-Za-z]:\//.test(normalized)) throw packageError("INVALID_PACKAGE_PATH", "Package path must not include a drive prefix.", {
|
|
52
|
+
entryPath: input,
|
|
53
|
+
fieldPath,
|
|
54
|
+
reason: "drive-prefixed path"
|
|
55
|
+
});
|
|
56
|
+
if (normalized.split("/").some((segment) => segment === "." || segment === ".." || segment.length === 0)) throw packageError("INVALID_PACKAGE_PATH", "Package path must not contain traversal segments.", {
|
|
57
|
+
entryPath: input,
|
|
58
|
+
fieldPath,
|
|
59
|
+
reason: "path traversal"
|
|
60
|
+
});
|
|
61
|
+
if (isNodeModulesPath(normalized)) throw packageError("NODE_MODULES_NOT_ALLOWED", "Package must not contain node_modules.", {
|
|
62
|
+
entryPath: input,
|
|
63
|
+
fieldPath,
|
|
64
|
+
reason: "node_modules is not allowed"
|
|
65
|
+
});
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
function isNodeModulesPath(input) {
|
|
69
|
+
return normalizePackagePath(input).split("/").some((segment) => segment === "node_modules");
|
|
70
|
+
}
|
|
71
|
+
function dedupeAndSortPackagePaths(paths) {
|
|
72
|
+
return Array.from(new Set(Array.from(paths, (path) => assertSafePackagePath(path)))).sort(comparePackagePaths);
|
|
73
|
+
}
|
|
74
|
+
function comparePackagePaths(a, b) {
|
|
75
|
+
return a.localeCompare(b, "en", { sensitivity: "variant" });
|
|
76
|
+
}
|
|
77
|
+
function packagePathStartsWith(path, directory) {
|
|
78
|
+
const normalizedPath = normalizePackagePath(path);
|
|
79
|
+
const normalizedDirectory = normalizePackagePath(directory);
|
|
80
|
+
return normalizedPath === normalizedDirectory || normalizedPath.startsWith(`${normalizedDirectory}/`);
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/utils.ts
|
|
84
|
+
function isRecord(value) {
|
|
85
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/package-manifest.ts
|
|
89
|
+
function createTooldeckPackageManifest(options) {
|
|
90
|
+
const files = dedupeAndSortPackagePaths([
|
|
91
|
+
TOOLDECK_PLUGIN_MANIFEST_PATH,
|
|
92
|
+
TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
93
|
+
...options.files
|
|
94
|
+
]);
|
|
95
|
+
return {
|
|
96
|
+
formatVersion: "1.0",
|
|
97
|
+
manifestPath: TOOLDECK_PLUGIN_MANIFEST_PATH,
|
|
98
|
+
createdAt: (options.createdAt ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
99
|
+
files
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function validateTooldeckPackageManifest(value) {
|
|
103
|
+
if (!isRecord(value)) throw packageError("INVALID_PACKAGE_METADATA", "tooldeck-package.json must be an object.", { manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH });
|
|
104
|
+
if (value.formatVersion !== "1.0") throw packageError("INVALID_PACKAGE_MANIFEST", "Unsupported Tooldeck package format version.", {
|
|
105
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
106
|
+
fieldPath: "formatVersion",
|
|
107
|
+
reason: `expected 1.0`
|
|
108
|
+
});
|
|
109
|
+
if (value.manifestPath !== "manifest.json") throw packageError("INVALID_PACKAGE_MANIFEST", "Package manifestPath must be manifest.json.", {
|
|
110
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
111
|
+
fieldPath: "manifestPath",
|
|
112
|
+
reason: "1.3 only supports a root manifest.json"
|
|
113
|
+
});
|
|
114
|
+
if (typeof value.createdAt !== "string" || Number.isNaN(Date.parse(value.createdAt))) throw packageError("INVALID_PACKAGE_MANIFEST", "Package createdAt must be an ISO date string.", {
|
|
115
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
116
|
+
fieldPath: "createdAt"
|
|
117
|
+
});
|
|
118
|
+
if (!Array.isArray(value.files) || !value.files.every((file) => typeof file === "string")) throw packageError("INVALID_PACKAGE_MANIFEST", "Package files must be a string array.", {
|
|
119
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
120
|
+
fieldPath: "files"
|
|
121
|
+
});
|
|
122
|
+
const normalizedFiles = value.files.map((file) => assertSafePackagePath(file));
|
|
123
|
+
const files = dedupeAndSortPackagePaths(normalizedFiles);
|
|
124
|
+
if (files.length !== value.files.length) throw packageError("DUPLICATE_FILE_LIST_ENTRY", "Package files must not contain duplicates.", {
|
|
125
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
126
|
+
fieldPath: "files"
|
|
127
|
+
});
|
|
128
|
+
for (let index = 0; index < files.length; index += 1) if (normalizedFiles[index] !== files[index]) throw packageError("UNSORTED_FILE_LIST", "Package files must be sorted.", {
|
|
129
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
130
|
+
fieldPath: "files",
|
|
131
|
+
reason: `expected ${files[index]} at index ${index}`
|
|
132
|
+
});
|
|
133
|
+
for (const requiredPath of [TOOLDECK_PLUGIN_MANIFEST_PATH, TOOLDECK_PACKAGE_MANIFEST_PATH]) {
|
|
134
|
+
assertSafePackagePath(requiredPath);
|
|
135
|
+
if (!files.includes(requiredPath)) throw packageError("INVALID_PACKAGE_MANIFEST", "Package files is missing a required file.", {
|
|
136
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
137
|
+
fieldPath: "files",
|
|
138
|
+
entryPath: requiredPath
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
formatVersion: "1.0",
|
|
143
|
+
manifestPath: TOOLDECK_PLUGIN_MANIFEST_PATH,
|
|
144
|
+
createdAt: value.createdAt,
|
|
145
|
+
files
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function assertPackageFileListMatches(options) {
|
|
149
|
+
const declaredFiles = dedupeAndSortPackagePaths(options.declaredFiles);
|
|
150
|
+
const actualFiles = dedupeAndSortPackagePaths(options.actualFiles);
|
|
151
|
+
if (declaredFiles.length !== actualFiles.length) throw packageError("FILE_LIST_MISMATCH", "Package file list does not match ZIP entries.", {
|
|
152
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
153
|
+
fieldPath: "files",
|
|
154
|
+
reason: "file count mismatch"
|
|
155
|
+
});
|
|
156
|
+
for (let index = 0; index < declaredFiles.length; index += 1) if (declaredFiles[index] !== actualFiles[index]) throw packageError("FILE_LIST_MISMATCH", "Package file list does not match ZIP entries.", {
|
|
157
|
+
manifestPath: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
158
|
+
fieldPath: "files",
|
|
159
|
+
reason: `expected ${declaredFiles[index]}, got ${actualFiles[index]}`
|
|
160
|
+
});
|
|
161
|
+
return declaredFiles;
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/digest.ts
|
|
165
|
+
async function computePackageDigest(packagePath) {
|
|
166
|
+
const data = await readFile(packagePath);
|
|
167
|
+
return createHash("sha256").update(data).digest("hex");
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/zip-central-directory.ts
|
|
171
|
+
var EOCD_SIGNATURE = 101010256;
|
|
172
|
+
var ZIP64_EOCD_LOCATOR_SIGNATURE = 117853008;
|
|
173
|
+
var CENTRAL_DIRECTORY_SIGNATURE = 33639248;
|
|
174
|
+
var EOCD_FIXED_SIZE = 22;
|
|
175
|
+
var CENTRAL_DIRECTORY_HEADER_SIZE = 46;
|
|
176
|
+
var ZIP64_SENTINEL_16 = 65535;
|
|
177
|
+
var ZIP64_SENTINEL_32 = 4294967295;
|
|
178
|
+
function parseCentralDirectory(data) {
|
|
179
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
180
|
+
const eocdOffset = findEndOfCentralDirectory(view);
|
|
181
|
+
if (eocdOffset < 0) throw packageError("INVALID_ZIP", "ZIP end of central directory was not found.");
|
|
182
|
+
if (hasZip64Locator(view, eocdOffset)) throw packageError("UNSUPPORTED_ZIP64", "ZIP64 packages are not supported.");
|
|
183
|
+
const diskNumber = view.getUint16(eocdOffset + 4, true);
|
|
184
|
+
const centralDirectoryDisk = view.getUint16(eocdOffset + 6, true);
|
|
185
|
+
const entriesOnDisk = view.getUint16(eocdOffset + 8, true);
|
|
186
|
+
const entryCount = view.getUint16(eocdOffset + 10, true);
|
|
187
|
+
const centralDirectorySize = view.getUint32(eocdOffset + 12, true);
|
|
188
|
+
const centralDirectoryOffset = view.getUint32(eocdOffset + 16, true);
|
|
189
|
+
if (diskNumber !== 0 || centralDirectoryDisk !== 0 || entriesOnDisk !== entryCount) throw packageError("UNSUPPORTED_ZIP_ENTRY", "Multi-disk ZIP packages are not supported.");
|
|
190
|
+
if (entryCount === ZIP64_SENTINEL_16 || centralDirectorySize === ZIP64_SENTINEL_32 || centralDirectoryOffset === ZIP64_SENTINEL_32) throw packageError("UNSUPPORTED_ZIP64", "ZIP64 packages are not supported.");
|
|
191
|
+
const endOffset = centralDirectoryOffset + centralDirectorySize;
|
|
192
|
+
if (endOffset !== eocdOffset || centralDirectoryOffset > endOffset) throw packageError("INVALID_ZIP", "ZIP central directory range is invalid.");
|
|
193
|
+
const entries = [];
|
|
194
|
+
const entryPaths = /* @__PURE__ */ new Set();
|
|
195
|
+
let offset = centralDirectoryOffset;
|
|
196
|
+
while (offset < endOffset) {
|
|
197
|
+
assertAvailableRange(view, offset, CENTRAL_DIRECTORY_HEADER_SIZE, "ZIP central directory entry");
|
|
198
|
+
if (view.getUint32(offset, true) !== CENTRAL_DIRECTORY_SIGNATURE) throw packageError("INVALID_ZIP", "Invalid ZIP central directory entry.");
|
|
199
|
+
const generalPurposeFlag = view.getUint16(offset + 8, true);
|
|
200
|
+
const compression = view.getUint16(offset + 10, true);
|
|
201
|
+
const compressedSize = view.getUint32(offset + 20, true);
|
|
202
|
+
const uncompressedSize = view.getUint32(offset + 24, true);
|
|
203
|
+
const fileNameLength = view.getUint16(offset + 28, true);
|
|
204
|
+
const extraFieldLength = view.getUint16(offset + 30, true);
|
|
205
|
+
const fileCommentLength = view.getUint16(offset + 32, true);
|
|
206
|
+
const externalAttributes = view.getUint32(offset + 38, true);
|
|
207
|
+
const entrySize = CENTRAL_DIRECTORY_HEADER_SIZE + fileNameLength + extraFieldLength + fileCommentLength;
|
|
208
|
+
assertAvailableRange(view, offset, entrySize, "ZIP central directory entry data");
|
|
209
|
+
if (offset + entrySize > endOffset) throw packageError("INVALID_ZIP", "ZIP central directory entry exceeds its declared range.");
|
|
210
|
+
const fileNameOffset = offset + CENTRAL_DIRECTORY_HEADER_SIZE;
|
|
211
|
+
const rawEntryPath = strFromU8(data.subarray(fileNameOffset, fileNameOffset + fileNameLength));
|
|
212
|
+
const entryPath = normalizePackagePath(rawEntryPath);
|
|
213
|
+
if (entryPaths.has(entryPath)) throw packageError("DUPLICATE_FILE_LIST_ENTRY", "ZIP entries must not contain duplicate paths.", { entryPath });
|
|
214
|
+
entryPaths.add(entryPath);
|
|
215
|
+
if (compressedSize === ZIP64_SENTINEL_32 || uncompressedSize === ZIP64_SENTINEL_32) throw packageError("UNSUPPORTED_ZIP64", "ZIP64 packages are not supported.", { entryPath });
|
|
216
|
+
entries.push({
|
|
217
|
+
path: entryPath,
|
|
218
|
+
kind: classifyEntry(rawEntryPath, externalAttributes),
|
|
219
|
+
compression,
|
|
220
|
+
encrypted: (generalPurposeFlag & 1) === 1,
|
|
221
|
+
compressedSizeBytes: compressedSize,
|
|
222
|
+
uncompressedSizeBytes: uncompressedSize
|
|
223
|
+
});
|
|
224
|
+
offset += entrySize;
|
|
225
|
+
}
|
|
226
|
+
if (offset !== endOffset || entries.length !== entryCount) throw packageError("INVALID_ZIP", "ZIP central directory entry count does not match EOCD.");
|
|
227
|
+
return entries;
|
|
228
|
+
}
|
|
229
|
+
function findEndOfCentralDirectory(view) {
|
|
230
|
+
const minOffset = Math.max(0, view.byteLength - 65557);
|
|
231
|
+
for (let offset = view.byteLength - EOCD_FIXED_SIZE; offset >= minOffset; offset -= 1) {
|
|
232
|
+
if (view.getUint32(offset, true) !== EOCD_SIGNATURE) continue;
|
|
233
|
+
const commentLength = view.getUint16(offset + 20, true);
|
|
234
|
+
if (offset + EOCD_FIXED_SIZE + commentLength === view.byteLength) return offset;
|
|
235
|
+
}
|
|
236
|
+
return -1;
|
|
237
|
+
}
|
|
238
|
+
function hasZip64Locator(view, eocdOffset) {
|
|
239
|
+
const locatorOffset = eocdOffset - 20;
|
|
240
|
+
return locatorOffset >= 0 && view.getUint32(locatorOffset, true) === ZIP64_EOCD_LOCATOR_SIGNATURE;
|
|
241
|
+
}
|
|
242
|
+
function classifyEntry(rawEntryPath, externalAttributes) {
|
|
243
|
+
if (rawEntryPath.replaceAll("\\", "/").endsWith("/")) return "directory";
|
|
244
|
+
const unixType = externalAttributes >>> 16 & 61440;
|
|
245
|
+
if (unixType === 16384) return "directory";
|
|
246
|
+
if (unixType === 40960) return "symlink";
|
|
247
|
+
if (unixType !== 0 && unixType !== 32768) return "special";
|
|
248
|
+
if ((externalAttributes & 16) === 16) return "directory";
|
|
249
|
+
return "file";
|
|
250
|
+
}
|
|
251
|
+
function assertAvailableRange(view, offset, length, description) {
|
|
252
|
+
if (offset < 0 || length < 0 || offset > view.byteLength - length) throw packageError("INVALID_ZIP", `${description} is truncated.`);
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/zip-entry-policy.ts
|
|
256
|
+
function validateZipEntries(entries, limits) {
|
|
257
|
+
let regularFileCount = 0;
|
|
258
|
+
let uncompressedSizeBytes = 0;
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
assertSafePackagePath(entry.path);
|
|
261
|
+
if (entry.encrypted) throw packageError("UNSUPPORTED_ENCRYPTED_ZIP", "Encrypted ZIP entries are not supported.", { entryPath: entry.path });
|
|
262
|
+
if (entry.compression !== 0 && entry.compression !== 8) throw packageError("UNSUPPORTED_ZIP_ENTRY", "Unsupported ZIP compression method.", {
|
|
263
|
+
entryPath: entry.path,
|
|
264
|
+
reason: `compression method ${entry.compression}`
|
|
265
|
+
});
|
|
266
|
+
if (entry.kind !== "file" && entry.kind !== "directory") throw packageError("UNSUPPORTED_ZIP_ENTRY", "Only regular files are supported in packages.", {
|
|
267
|
+
entryPath: entry.path,
|
|
268
|
+
reason: entry.kind
|
|
269
|
+
});
|
|
270
|
+
if (entry.kind === "file") {
|
|
271
|
+
regularFileCount += 1;
|
|
272
|
+
uncompressedSizeBytes += entry.uncompressedSizeBytes ?? 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (regularFileCount > limits.maxRegularFileCount) throw packageError("TOO_MANY_FILES", "Tooldeck package contains too many files.", { reason: `${regularFileCount} > ${limits.maxRegularFileCount}` });
|
|
276
|
+
if (uncompressedSizeBytes > limits.maxUncompressedSizeBytes) throw packageError("UNCOMPRESSED_SIZE_TOO_LARGE", "Tooldeck package is too large after unzip.", { reason: `${uncompressedSizeBytes} > ${limits.maxUncompressedSizeBytes}` });
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/fflate-zip-adapter.ts
|
|
280
|
+
var FflateZipAdapter = class {
|
|
281
|
+
async readArchive(archivePath, limits) {
|
|
282
|
+
await assertPackageFileSize(archivePath, limits);
|
|
283
|
+
const data = await readPackageFile(archivePath);
|
|
284
|
+
const entries = parseCentralDirectory(data);
|
|
285
|
+
validateZipEntries(entries, limits);
|
|
286
|
+
return {
|
|
287
|
+
entries,
|
|
288
|
+
readFile: async (entryPath) => {
|
|
289
|
+
const normalizedPath = assertSafePackagePath(entryPath);
|
|
290
|
+
const file = unzipSingleFile(data, normalizedPath);
|
|
291
|
+
if (!file) throw packageError("FILE_LIST_MISMATCH", "ZIP entry does not exist.", {
|
|
292
|
+
packagePath: archivePath,
|
|
293
|
+
entryPath: normalizedPath
|
|
294
|
+
});
|
|
295
|
+
return file;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async writeArchive(archivePath, entries) {
|
|
300
|
+
const zippable = {};
|
|
301
|
+
for (const entry of entries) {
|
|
302
|
+
const normalizedPath = assertSafePackagePath(entry.path);
|
|
303
|
+
zippable[normalizedPath] = [entry.data, { level: 6 }];
|
|
304
|
+
}
|
|
305
|
+
let data;
|
|
306
|
+
try {
|
|
307
|
+
data = zipSync(zippable);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw packageError("PACKAGE_WRITE_FAILED", "Tooldeck package could not be created.", {
|
|
310
|
+
packagePath: archivePath,
|
|
311
|
+
reason: formatUnknownError(error)
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
await writePackageFile(archivePath, data);
|
|
315
|
+
}
|
|
316
|
+
async extractArchive(options) {
|
|
317
|
+
await assertPackageFileSize(options.archivePath, options.limits);
|
|
318
|
+
const data = await readPackageFile(options.archivePath);
|
|
319
|
+
const entries = parseCentralDirectory(data);
|
|
320
|
+
validateZipEntries(entries, options.limits);
|
|
321
|
+
const files = unzipArchive(data, options.archivePath);
|
|
322
|
+
const destinationDir = path.resolve(options.destinationDir);
|
|
323
|
+
await createExtractionDirectory(destinationDir, options.archivePath);
|
|
324
|
+
const resolvedDestinationDir = path.resolve(destinationDir);
|
|
325
|
+
const destinationRealpath = await getExtractionRealpath(destinationDir, options.archivePath, "<destination>");
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (entry.kind !== "file") continue;
|
|
328
|
+
const normalizedPath = assertSafePackagePath(entry.path);
|
|
329
|
+
const fileData = files.get(normalizedPath);
|
|
330
|
+
if (!fileData) throw packageError("FILE_LIST_MISMATCH", "ZIP entry was not extracted.", {
|
|
331
|
+
packagePath: options.archivePath,
|
|
332
|
+
entryPath: normalizedPath
|
|
333
|
+
});
|
|
334
|
+
const outputPath = path.resolve(destinationDir, ...normalizedPath.split("/"));
|
|
335
|
+
assertContainedPath(resolvedDestinationDir, outputPath, normalizedPath);
|
|
336
|
+
await createExtractionDirectory(path.dirname(outputPath), options.archivePath, normalizedPath);
|
|
337
|
+
await writeExtractedFile(outputPath, fileData, options.archivePath, normalizedPath);
|
|
338
|
+
assertContainedPath(destinationRealpath, await getExtractionRealpath(outputPath, options.archivePath, normalizedPath), normalizedPath);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
function unzipSingleFile(data, entryPath) {
|
|
343
|
+
try {
|
|
344
|
+
const files = unzipSync(data, { filter: (file) => normalizePackagePath(file.name) === entryPath });
|
|
345
|
+
return Object.entries(files).find(([path]) => normalizePackagePath(path) === entryPath)?.[1];
|
|
346
|
+
} catch (error) {
|
|
347
|
+
throw mapFflateError(error);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function unzipArchive(data, archivePath) {
|
|
351
|
+
try {
|
|
352
|
+
return new Map(Object.entries(unzipSync(data)).map(([entryPath, fileData]) => [normalizePackagePath(entryPath), fileData]));
|
|
353
|
+
} catch (error) {
|
|
354
|
+
throw packageError("PACKAGE_EXTRACT_FAILED", "Tooldeck package could not be extracted.", {
|
|
355
|
+
packagePath: archivePath,
|
|
356
|
+
reason: formatUnknownError(error)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function assertPackageFileSize(archivePath, limits) {
|
|
361
|
+
let archiveStat;
|
|
362
|
+
try {
|
|
363
|
+
archiveStat = await stat(archivePath);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw packageError("PACKAGE_READ_FAILED", "Tooldeck package file could not be inspected.", {
|
|
366
|
+
packagePath: archivePath,
|
|
367
|
+
reason: formatUnknownError(error)
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
if (archiveStat.size > limits.maxPackageSizeBytes) throw packageError("PACKAGE_TOO_LARGE", "Tooldeck package file is too large.", {
|
|
371
|
+
packagePath: archivePath,
|
|
372
|
+
reason: `${archiveStat.size} > ${limits.maxPackageSizeBytes}`
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async function readPackageFile(archivePath) {
|
|
376
|
+
try {
|
|
377
|
+
return await readFile(archivePath);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
throw packageError("PACKAGE_READ_FAILED", "Tooldeck package could not be read.", {
|
|
380
|
+
packagePath: archivePath,
|
|
381
|
+
reason: formatUnknownError(error)
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function writePackageFile(archivePath, data) {
|
|
386
|
+
try {
|
|
387
|
+
await mkdir(path.dirname(archivePath), { recursive: true });
|
|
388
|
+
await writeFile(archivePath, data);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw packageError("PACKAGE_WRITE_FAILED", "Tooldeck package could not be written.", {
|
|
391
|
+
packagePath: archivePath,
|
|
392
|
+
reason: formatUnknownError(error)
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async function createExtractionDirectory(directoryPath, archivePath, entryPath) {
|
|
397
|
+
try {
|
|
398
|
+
await mkdir(directoryPath, { recursive: true });
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throw packageError("PACKAGE_EXTRACT_FAILED", "Package extraction directory could not be created.", {
|
|
401
|
+
packagePath: archivePath,
|
|
402
|
+
entryPath,
|
|
403
|
+
reason: formatUnknownError(error)
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async function writeExtractedFile(outputPath, data, archivePath, entryPath) {
|
|
408
|
+
try {
|
|
409
|
+
await writeFile(outputPath, data);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw packageError("PACKAGE_EXTRACT_FAILED", "Package entry could not be extracted.", {
|
|
412
|
+
packagePath: archivePath,
|
|
413
|
+
entryPath,
|
|
414
|
+
reason: formatUnknownError(error)
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function getExtractionRealpath(targetPath, archivePath, entryPath) {
|
|
419
|
+
try {
|
|
420
|
+
return await realpath(targetPath);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
throw packageError("PACKAGE_EXTRACT_FAILED", "Extracted path could not be resolved.", {
|
|
423
|
+
packagePath: archivePath,
|
|
424
|
+
entryPath,
|
|
425
|
+
reason: formatUnknownError(error)
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function assertContainedPath(root, target, entryPath) {
|
|
430
|
+
const relative = path.relative(root, target);
|
|
431
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw packageError("EXTRACTION_ESCAPE", "Package extraction escaped the destination directory.", {
|
|
432
|
+
entryPath,
|
|
433
|
+
reason: target
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function mapFflateError(error) {
|
|
437
|
+
if (isFlateError(error) && error.code === 14) throw packageError("UNSUPPORTED_ZIP_ENTRY", "Unsupported ZIP compression method.", { reason: error.message });
|
|
438
|
+
throw packageError("INVALID_ZIP", "ZIP could not be read.", { reason: formatUnknownError(error) });
|
|
439
|
+
}
|
|
440
|
+
function isFlateError(error) {
|
|
441
|
+
return error instanceof Error && typeof error.code === "number";
|
|
442
|
+
}
|
|
443
|
+
function formatUnknownError(error) {
|
|
444
|
+
return error instanceof Error ? error.message : String(error);
|
|
445
|
+
}
|
|
446
|
+
function textToBytes(text) {
|
|
447
|
+
return strToU8(text);
|
|
448
|
+
}
|
|
449
|
+
function bytesToText(data) {
|
|
450
|
+
return strFromU8(data);
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/manifest.ts
|
|
454
|
+
var validatePackageManifestSchema = new Ajv({
|
|
455
|
+
allErrors: true,
|
|
456
|
+
strict: false,
|
|
457
|
+
validateSchema: false
|
|
458
|
+
}).compile(createPackageManifestSchema());
|
|
459
|
+
function parsePluginManifestText(text, manifestPath) {
|
|
460
|
+
let value;
|
|
461
|
+
try {
|
|
462
|
+
value = JSON.parse(text);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
throw packageError("INVALID_PLUGIN_MANIFEST", "Plugin manifest is not valid JSON.", {
|
|
465
|
+
manifestPath,
|
|
466
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return validatePluginManifestShape(value, manifestPath);
|
|
470
|
+
}
|
|
471
|
+
function validatePluginManifestShape(value, manifestPath) {
|
|
472
|
+
if (!isRecord(value)) throw packageError("INVALID_PLUGIN_MANIFEST", "Plugin manifest must be an object.", { manifestPath });
|
|
473
|
+
if (!validatePackageManifestSchema(value)) throw formatManifestSchemaError(validatePackageManifestSchema.errors ?? [], manifestPath);
|
|
474
|
+
assertSafePackagePath(value.runtime.entry, "runtime.entry");
|
|
475
|
+
assertLocalePaths(value, manifestPath);
|
|
476
|
+
return value;
|
|
477
|
+
}
|
|
478
|
+
function createPackageManifestSchema() {
|
|
479
|
+
const schema = structuredClone(manifestV1Schema);
|
|
480
|
+
if (schema.definitions?.runtime?.properties) schema.definitions.runtime.properties.kind = {
|
|
481
|
+
type: "string",
|
|
482
|
+
minLength: 1
|
|
483
|
+
};
|
|
484
|
+
if (schema.definitions) {
|
|
485
|
+
schema.definitions.tooldeckInputJsonSchema = {
|
|
486
|
+
type: "object",
|
|
487
|
+
description: "A command input JSON Schema object. Full JSON Schema validation is deferred to command input handling."
|
|
488
|
+
};
|
|
489
|
+
schema.definitions.tooldeckJsonSchema = {
|
|
490
|
+
type: "object",
|
|
491
|
+
description: "A JSON Schema object. Full JSON Schema validation is deferred to command input handling."
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return schema;
|
|
495
|
+
}
|
|
496
|
+
function formatManifestSchemaError(errors, manifestPath) {
|
|
497
|
+
const error = errors[0];
|
|
498
|
+
return packageError("INVALID_PLUGIN_MANIFEST", error?.message ? `Plugin manifest schema violation: ${error.message}.` : "Plugin manifest does not match the protocol schema.", {
|
|
499
|
+
manifestPath,
|
|
500
|
+
fieldPath: error ? formatAjvFieldPath(error) : void 0,
|
|
501
|
+
reason: error?.keyword
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
function formatAjvFieldPath(error) {
|
|
505
|
+
const path = pointerToFieldPath(error.instancePath);
|
|
506
|
+
if (error.keyword === "required") return joinFieldPath(path, getStringParam(error.params, "missingProperty"));
|
|
507
|
+
if (error.keyword === "additionalProperties") return joinFieldPath(path, getStringParam(error.params, "additionalProperty"));
|
|
508
|
+
return path;
|
|
509
|
+
}
|
|
510
|
+
function pointerToFieldPath(pointer) {
|
|
511
|
+
if (!pointer) return;
|
|
512
|
+
return pointer.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~")).map((segment, index) => /^\d+$/.test(segment) ? `[${segment}]` : `${index === 0 ? "" : "."}${segment}`).join("");
|
|
513
|
+
}
|
|
514
|
+
function joinFieldPath(base, property) {
|
|
515
|
+
if (!property) return base;
|
|
516
|
+
return base ? `${base}.${property}` : property;
|
|
517
|
+
}
|
|
518
|
+
function getStringParam(params, name) {
|
|
519
|
+
const value = params[name];
|
|
520
|
+
return typeof value === "string" ? value : void 0;
|
|
521
|
+
}
|
|
522
|
+
function assertLocalePaths(value, manifestPath) {
|
|
523
|
+
if (value.locales === void 0) return;
|
|
524
|
+
if (!isRecord(value.locales)) throw packageError("INVALID_PLUGIN_MANIFEST", "Plugin manifest locales must be an object.", {
|
|
525
|
+
manifestPath,
|
|
526
|
+
fieldPath: "locales"
|
|
527
|
+
});
|
|
528
|
+
for (const [locale, localePath] of Object.entries(value.locales)) {
|
|
529
|
+
if (typeof localePath !== "string") throw packageError("INVALID_PLUGIN_MANIFEST", "Plugin locale path must be a string.", {
|
|
530
|
+
manifestPath,
|
|
531
|
+
fieldPath: `locales.${locale}`
|
|
532
|
+
});
|
|
533
|
+
assertSafePackagePath(localePath, `locales.${locale}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/package-reader.ts
|
|
538
|
+
var defaultZipAdapter$1 = new FflateZipAdapter();
|
|
539
|
+
async function readTooldeckPackage(options, zipAdapter = defaultZipAdapter$1) {
|
|
540
|
+
assertTooldeckPackageExtension(options.packagePath);
|
|
541
|
+
const limits = resolvePackageLimits(options.limits);
|
|
542
|
+
const archive = await zipAdapter.readArchive(options.packagePath, limits);
|
|
543
|
+
const actualFiles = archive.entries.filter((entry) => entry.kind === "file").map((entry) => assertSafePackagePath(entry.path));
|
|
544
|
+
if (!actualFiles.includes("tooldeck-package.json")) throw packageError("MISSING_PACKAGE_MANIFEST", "Package is missing tooldeck-package.json.", {
|
|
545
|
+
packagePath: options.packagePath,
|
|
546
|
+
entryPath: TOOLDECK_PACKAGE_MANIFEST_PATH
|
|
547
|
+
});
|
|
548
|
+
if (!actualFiles.includes("manifest.json")) throw packageError("MISSING_PLUGIN_MANIFEST", "Package is missing root manifest.json.", {
|
|
549
|
+
packagePath: options.packagePath,
|
|
550
|
+
entryPath: TOOLDECK_PLUGIN_MANIFEST_PATH
|
|
551
|
+
});
|
|
552
|
+
const packageManifest = validateTooldeckPackageManifest(parseJson(bytesToText(await archive.readFile(TOOLDECK_PACKAGE_MANIFEST_PATH)), TOOLDECK_PACKAGE_MANIFEST_PATH));
|
|
553
|
+
const files = assertPackageFileListMatches({
|
|
554
|
+
declaredFiles: packageManifest.files,
|
|
555
|
+
actualFiles
|
|
556
|
+
});
|
|
557
|
+
const pluginManifest = parsePluginManifestText(bytesToText(await archive.readFile(TOOLDECK_PLUGIN_MANIFEST_PATH)), TOOLDECK_PLUGIN_MANIFEST_PATH);
|
|
558
|
+
const runtimeEntry = assertSafePackagePath(pluginManifest.runtime.entry, "runtime.entry");
|
|
559
|
+
if (!files.includes(runtimeEntry)) throw packageError("MISSING_RUNTIME_ENTRY", "Package is missing manifest runtime entry.", {
|
|
560
|
+
packagePath: options.packagePath,
|
|
561
|
+
entryPath: runtimeEntry,
|
|
562
|
+
fieldPath: "runtime.entry"
|
|
563
|
+
});
|
|
564
|
+
const archiveStat = await stat(options.packagePath);
|
|
565
|
+
const packageDigest = await computePackageDigest(options.packagePath);
|
|
566
|
+
const uncompressedSizeBytes = archive.entries.filter((entry) => entry.kind === "file").reduce((sum, entry) => sum + (entry.uncompressedSizeBytes ?? 0), 0);
|
|
567
|
+
return {
|
|
568
|
+
packagePath: options.packagePath,
|
|
569
|
+
packageDigest,
|
|
570
|
+
packageSizeBytes: archiveStat.size,
|
|
571
|
+
packageManifest,
|
|
572
|
+
pluginManifest,
|
|
573
|
+
files,
|
|
574
|
+
uncompressedSizeBytes
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async function validateTooldeckPackage(options, zipAdapter) {
|
|
578
|
+
return readTooldeckPackage(options, zipAdapter);
|
|
579
|
+
}
|
|
580
|
+
async function unpackTooldeckPackage(options, zipAdapter = defaultZipAdapter$1) {
|
|
581
|
+
const summary = await readTooldeckPackage(options, zipAdapter);
|
|
582
|
+
await zipAdapter.extractArchive({
|
|
583
|
+
archivePath: options.packagePath,
|
|
584
|
+
destinationDir: options.destinationDir,
|
|
585
|
+
limits: resolvePackageLimits(options.limits)
|
|
586
|
+
});
|
|
587
|
+
return summary;
|
|
588
|
+
}
|
|
589
|
+
function resolvePackageLimits(limits) {
|
|
590
|
+
return {
|
|
591
|
+
...DEFAULT_PACKAGE_LIMITS,
|
|
592
|
+
...limits
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function assertTooldeckPackageExtension(packagePath) {
|
|
596
|
+
if (path.extname(packagePath) !== ".tdplugin") throw packageError("INVALID_PACKAGE_EXTENSION", "Tooldeck packages must use .tdplugin.", {
|
|
597
|
+
packagePath,
|
|
598
|
+
reason: `got ${path.extname(packagePath) || "<none>"}`
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function parseJson(text, manifestPath) {
|
|
602
|
+
try {
|
|
603
|
+
return JSON.parse(text);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
throw packageError("INVALID_PACKAGE_METADATA", "Package manifest is not valid JSON.", {
|
|
606
|
+
manifestPath,
|
|
607
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/pack.ts
|
|
613
|
+
var defaultZipAdapter = new FflateZipAdapter();
|
|
614
|
+
async function packTooldeckPlugin(options, zipAdapter = defaultZipAdapter) {
|
|
615
|
+
const manifestFile = path.resolve(options.manifestPath ?? path.join(options.projectDir ?? process.cwd(), "manifest.json"));
|
|
616
|
+
const projectDir = path.resolve(options.projectDir ?? path.dirname(manifestFile));
|
|
617
|
+
const manifestText = await readFile(manifestFile, "utf8");
|
|
618
|
+
const pluginManifest = parsePluginManifestText(manifestText, TOOLDECK_PLUGIN_MANIFEST_PATH);
|
|
619
|
+
const outputPath = options.outputPath ?? path.join(projectDir, `${pluginManifest.id}-${pluginManifest.version}.tdplugin`);
|
|
620
|
+
const files = await collectTooldeckPackageFiles(projectDir, pluginManifest, manifestText);
|
|
621
|
+
const packageManifest = createTooldeckPackageManifest({
|
|
622
|
+
createdAt: options.createdAt,
|
|
623
|
+
files: files.map((file) => file.path)
|
|
624
|
+
});
|
|
625
|
+
const entries = [...files, {
|
|
626
|
+
path: TOOLDECK_PACKAGE_MANIFEST_PATH,
|
|
627
|
+
data: textToBytes(`${JSON.stringify(packageManifest, null, 2)}\n`)
|
|
628
|
+
}];
|
|
629
|
+
await zipAdapter.writeArchive(outputPath, entries);
|
|
630
|
+
const summary = await readTooldeckPackage({
|
|
631
|
+
packagePath: outputPath,
|
|
632
|
+
limits: options.limits
|
|
633
|
+
}, zipAdapter);
|
|
634
|
+
return {
|
|
635
|
+
packagePath: outputPath,
|
|
636
|
+
packageManifest: summary.packageManifest,
|
|
637
|
+
pluginManifest,
|
|
638
|
+
files: summary.files,
|
|
639
|
+
packageDigest: await computePackageDigest(outputPath),
|
|
640
|
+
packageSizeBytes: summary.packageSizeBytes
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function collectTooldeckPackageFiles(projectDir, pluginManifest, manifestText) {
|
|
644
|
+
const packagePaths = dedupeAndSortPackagePaths([
|
|
645
|
+
TOOLDECK_PLUGIN_MANIFEST_PATH,
|
|
646
|
+
pluginManifest.runtime.entry,
|
|
647
|
+
...Object.values(pluginManifest.locales ?? {}).filter((localePath) => typeof localePath === "string"),
|
|
648
|
+
...await collectDefaultDirectoryPackagePaths(projectDir)
|
|
649
|
+
]);
|
|
650
|
+
const files = [];
|
|
651
|
+
for (const packagePath of packagePaths) {
|
|
652
|
+
if (packagePath === "tooldeck-package.json") continue;
|
|
653
|
+
if (packagePath === "manifest.json" && manifestText !== void 0) files.push({
|
|
654
|
+
path: packagePath,
|
|
655
|
+
data: textToBytes(manifestText.endsWith("\n") ? manifestText : `${manifestText}\n`)
|
|
656
|
+
});
|
|
657
|
+
else {
|
|
658
|
+
const absolutePath = resolveProjectPackagePath(projectDir, packagePath);
|
|
659
|
+
files.push({
|
|
660
|
+
path: packagePath,
|
|
661
|
+
data: await readFile(absolutePath)
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return files;
|
|
666
|
+
}
|
|
667
|
+
async function collectDefaultDirectoryPackagePaths(projectDir) {
|
|
668
|
+
const paths = [];
|
|
669
|
+
for (const directory of DEFAULT_PACKAGE_INCLUDE_DIRS) {
|
|
670
|
+
const absoluteDirectory = path.join(projectDir, directory);
|
|
671
|
+
if (!await isDirectory(absoluteDirectory)) continue;
|
|
672
|
+
paths.push(...await collectRegularFiles(projectDir, absoluteDirectory));
|
|
673
|
+
}
|
|
674
|
+
return paths;
|
|
675
|
+
}
|
|
676
|
+
async function collectRegularFiles(projectDir, directory) {
|
|
677
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
678
|
+
const files = [];
|
|
679
|
+
for (const entry of entries) {
|
|
680
|
+
const absolutePath = path.join(directory, entry.name);
|
|
681
|
+
if (entry.isDirectory()) {
|
|
682
|
+
files.push(...await collectRegularFiles(projectDir, absolutePath));
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (!entry.isFile()) continue;
|
|
686
|
+
files.push(toPackagePath(projectDir, absolutePath));
|
|
687
|
+
}
|
|
688
|
+
return files;
|
|
689
|
+
}
|
|
690
|
+
function resolveProjectPackagePath(projectDir, packagePath) {
|
|
691
|
+
const normalizedPath = assertSafePackagePath(packagePath);
|
|
692
|
+
const absolutePath = path.resolve(projectDir, ...normalizedPath.split("/"));
|
|
693
|
+
const relative = path.relative(projectDir, absolutePath);
|
|
694
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Package path escapes the project directory: ${packagePath}`);
|
|
695
|
+
return absolutePath;
|
|
696
|
+
}
|
|
697
|
+
function toPackagePath(projectDir, absolutePath) {
|
|
698
|
+
return assertSafePackagePath(path.relative(projectDir, absolutePath).replaceAll(path.sep, "/"));
|
|
699
|
+
}
|
|
700
|
+
async function isDirectory(filePath) {
|
|
701
|
+
try {
|
|
702
|
+
return (await stat(filePath)).isDirectory();
|
|
703
|
+
} catch {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
//#endregion
|
|
708
|
+
export { DEFAULT_PACKAGE_INCLUDE_DIRS, DEFAULT_PACKAGE_LIMITS, FflateZipAdapter, TOOLDECK_PACKAGE_EXTENSION, TOOLDECK_PACKAGE_FORMAT_VERSION, TOOLDECK_PACKAGE_MANIFEST_PATH, TOOLDECK_PLUGIN_MANIFEST_PATH, TooldeckPackageError, assertPackageFileListMatches, assertSafePackagePath, assertTooldeckPackageExtension, collectTooldeckPackageFiles, comparePackagePaths, computePackageDigest, createTooldeckPackageManifest, dedupeAndSortPackagePaths, isNodeModulesPath, normalizePackagePath, packTooldeckPlugin, packageError, packagePathStartsWith, parsePluginManifestText, readTooldeckPackage, resolvePackageLimits, unpackTooldeckPackage, validatePluginManifestShape, validateTooldeckPackage, validateTooldeckPackageManifest };
|
|
709
|
+
|
|
710
|
+
//# sourceMappingURL=index.js.map
|