lua-cli 3.15.1 → 3.15.3

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.
Files changed (68) hide show
  1. package/dist/api/skills.api.service.d.ts +8 -0
  2. package/dist/api/skills.api.service.d.ts.map +1 -1
  3. package/dist/api/skills.api.service.js +8 -0
  4. package/dist/api/skills.api.service.js.map +1 -1
  5. package/dist/cli/command-definitions.d.ts.map +1 -1
  6. package/dist/cli/command-definitions.js +8 -0
  7. package/dist/cli/command-definitions.js.map +1 -1
  8. package/dist/commands/logs.d.ts.map +1 -1
  9. package/dist/commands/logs.js +13 -6
  10. package/dist/commands/logs.js.map +1 -1
  11. package/dist/commands/push.d.ts +12 -0
  12. package/dist/commands/push.d.ts.map +1 -1
  13. package/dist/commands/push.js +125 -8
  14. package/dist/commands/push.js.map +1 -1
  15. package/dist/commands/source.d.ts.map +1 -1
  16. package/dist/commands/source.js +17 -7
  17. package/dist/commands/source.js.map +1 -1
  18. package/dist/compiler/plugins/agent.plugin.d.ts.map +1 -1
  19. package/dist/compiler/plugins/agent.plugin.js +2 -0
  20. package/dist/compiler/plugins/agent.plugin.js.map +1 -1
  21. package/dist/compiler/utils/common.d.ts +3 -19
  22. package/dist/compiler/utils/common.d.ts.map +1 -1
  23. package/dist/compiler/utils/common.js +8 -47
  24. package/dist/compiler/utils/common.js.map +1 -1
  25. package/dist/interfaces/backup.d.ts +6 -132
  26. package/dist/interfaces/backup.d.ts.map +1 -1
  27. package/dist/interfaces/backup.js +3 -4
  28. package/dist/interfaces/backup.js.map +1 -1
  29. package/dist/utils/backup-helpers.d.ts +6 -58
  30. package/dist/utils/backup-helpers.d.ts.map +1 -1
  31. package/dist/utils/backup-helpers.js +35 -158
  32. package/dist/utils/backup-helpers.js.map +1 -1
  33. package/dist/utils/build-manifest-from-disk.d.ts +3 -12
  34. package/dist/utils/build-manifest-from-disk.d.ts.map +1 -1
  35. package/dist/utils/build-manifest-from-disk.js +10 -100
  36. package/dist/utils/build-manifest-from-disk.js.map +1 -1
  37. package/node_modules/@lua/shared-source-sync/dist/index.d.mts +424 -0
  38. package/node_modules/@lua/shared-source-sync/dist/index.d.ts +424 -0
  39. package/node_modules/@lua/shared-source-sync/dist/index.js +536 -0
  40. package/node_modules/@lua/shared-source-sync/dist/index.mjs +491 -0
  41. package/node_modules/@lua/shared-source-sync/package.json +26 -0
  42. package/node_modules/@lua/shared-source-sync/src/archive.ts +98 -0
  43. package/node_modules/@lua/shared-source-sync/src/filter.ts +72 -0
  44. package/node_modules/@lua/shared-source-sync/src/hash.ts +40 -0
  45. package/node_modules/@lua/shared-source-sync/src/http.ts +132 -0
  46. package/node_modules/@lua/shared-source-sync/src/index.ts +56 -0
  47. package/node_modules/@lua/shared-source-sync/src/pull.ts +50 -0
  48. package/node_modules/@lua/shared-source-sync/src/push.ts +94 -0
  49. package/node_modules/@lua/shared-source-sync/src/restore.ts +83 -0
  50. package/node_modules/@lua/shared-source-sync/src/skill-source.ts +55 -0
  51. package/node_modules/@lua/shared-source-sync/src/transfer.ts +92 -0
  52. package/node_modules/@lua/shared-source-sync/src/types.ts +88 -0
  53. package/node_modules/@lua/shared-source-sync/src/walk.ts +91 -0
  54. package/node_modules/@lua/shared-source-sync/tests/archive.test.ts +108 -0
  55. package/node_modules/@lua/shared-source-sync/tests/hash.test.ts +40 -0
  56. package/node_modules/@lua/shared-source-sync/tests/http.test.ts +77 -0
  57. package/node_modules/@lua/shared-source-sync/tests/push.test.ts +162 -0
  58. package/node_modules/@lua/shared-source-sync/tests/walk.test.ts +84 -0
  59. package/node_modules/@lua/shared-source-sync/tsconfig.json +12 -0
  60. package/node_modules/@lua/shared-source-sync/tsup.config.ts +9 -0
  61. package/node_modules/@lua/shared-source-sync/vitest.config.ts +8 -0
  62. package/node_modules/@lua/shared-types/dist/index.d.mts +27 -0
  63. package/node_modules/@lua/shared-types/dist/index.d.ts +27 -0
  64. package/node_modules/@lua/shared-types/src/agent-config.types.ts +14 -0
  65. package/node_modules/@lua/shared-types/src/chat-stream.types.ts +7 -0
  66. package/node_modules/@lua/shared-types/src/model-registry.ts +6 -0
  67. package/package.json +3 -1
  68. package/template/package.json +1 -1
@@ -0,0 +1,491 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/hash.ts
5
+ import { createHash } from "crypto";
6
+ var FILE_HASH_LENGTH = 16;
7
+ function hashContentTruncated(content) {
8
+ return createHash("sha256").update(content).digest("hex").slice(0, FILE_HASH_LENGTH);
9
+ }
10
+ __name(hashContentTruncated, "hashContentTruncated");
11
+ function sha256Hex(content) {
12
+ return createHash("sha256").update(content).digest("hex");
13
+ }
14
+ __name(sha256Hex, "sha256Hex");
15
+ function combineFileHashes(files) {
16
+ const sorted = [
17
+ ...files
18
+ ].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
19
+ return sha256Hex(sorted.map((f) => f.hash).join("|"));
20
+ }
21
+ __name(combineFileHashes, "combineFileHashes");
22
+
23
+ // src/filter.ts
24
+ import { extname } from "path";
25
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
26
+ "node_modules",
27
+ "dist",
28
+ "dist-v2",
29
+ ".git",
30
+ ".lua",
31
+ ".temp",
32
+ "coverage",
33
+ ".next",
34
+ ".turbo"
35
+ ]);
36
+ var ARCHIVE_ONLY_SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
37
+ "build"
38
+ ]);
39
+ var DEFAULT_MAX_FILE_BYTES = 256 * 1024;
40
+ function shouldSkipDirectory(name) {
41
+ return SKIP_DIRECTORIES.has(name);
42
+ }
43
+ __name(shouldSkipDirectory, "shouldSkipDirectory");
44
+ function shouldSkipFile(filePathOrName) {
45
+ return filePathOrName.includes("node_modules") || filePathOrName.includes(".test.") || filePathOrName.includes(".spec.") || filePathOrName.includes("__tests__") || filePathOrName.endsWith(".d.ts") || filePathOrName.endsWith(".map") || filePathOrName.startsWith(".") || filePathOrName === "package-lock.json" || filePathOrName === "yarn.lock" || filePathOrName === "pnpm-lock.yaml";
46
+ }
47
+ __name(shouldSkipFile, "shouldSkipFile");
48
+ var KIND_BY_EXT = {
49
+ ".ts": "source",
50
+ ".tsx": "source",
51
+ ".js": "source",
52
+ ".jsx": "source",
53
+ ".yaml": "config",
54
+ ".yml": "config",
55
+ ".json": "config",
56
+ ".toml": "config"
57
+ };
58
+ function classifyFile(relPath) {
59
+ return KIND_BY_EXT[extname(relPath)] ?? "other";
60
+ }
61
+ __name(classifyFile, "classifyFile");
62
+
63
+ // src/walk.ts
64
+ import { readdirSync, readFileSync, statSync } from "fs";
65
+ import { join, sep } from "path";
66
+ function walkWorkspace(rootDir, opts = {}) {
67
+ const maxBytes = opts.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
68
+ const refs = [];
69
+ const contentByHash = /* @__PURE__ */ new Map();
70
+ let totalSize = 0;
71
+ const visit = /* @__PURE__ */ __name((relPrefix) => {
72
+ const absDir = relPrefix ? join(rootDir, relPrefix) : rootDir;
73
+ let entries;
74
+ try {
75
+ entries = readdirSync(absDir, {
76
+ withFileTypes: true
77
+ });
78
+ } catch {
79
+ return;
80
+ }
81
+ for (const entry of entries) {
82
+ if (entry.isDirectory()) {
83
+ if (shouldSkipDirectory(entry.name)) continue;
84
+ if (shouldSkipFile(entry.name)) continue;
85
+ visit(relPrefix ? `${relPrefix}${sep}${entry.name}` : entry.name);
86
+ continue;
87
+ }
88
+ if (!entry.isFile()) continue;
89
+ if (shouldSkipFile(entry.name)) continue;
90
+ const rel = (relPrefix ? `${relPrefix}${sep}${entry.name}` : entry.name).split(sep).join("/");
91
+ const abs = join(rootDir, rel);
92
+ let stats;
93
+ try {
94
+ stats = statSync(abs);
95
+ } catch {
96
+ continue;
97
+ }
98
+ if (stats.size > maxBytes) continue;
99
+ let content;
100
+ try {
101
+ content = readFileSync(abs);
102
+ } catch {
103
+ continue;
104
+ }
105
+ const hash = hashContentTruncated(content.toString("utf-8"));
106
+ refs.push({
107
+ relativePath: rel,
108
+ hash,
109
+ size: stats.size,
110
+ type: classifyFile(rel)
111
+ });
112
+ if (!contentByHash.has(hash)) contentByHash.set(hash, content);
113
+ totalSize += stats.size;
114
+ }
115
+ }, "visit");
116
+ visit("");
117
+ refs.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
118
+ const projectHash = combineFileHashes(refs);
119
+ return {
120
+ files: refs,
121
+ projectHash,
122
+ totalSize,
123
+ contentByHash
124
+ };
125
+ }
126
+ __name(walkWorkspace, "walkWorkspace");
127
+
128
+ // src/http.ts
129
+ var BackupHttpError = class extends Error {
130
+ static {
131
+ __name(this, "BackupHttpError");
132
+ }
133
+ status;
134
+ endpoint;
135
+ body;
136
+ constructor(message, status, endpoint, body) {
137
+ super(message), this.status = status, this.endpoint = endpoint, this.body = body;
138
+ this.name = "BackupHttpError";
139
+ }
140
+ };
141
+ var BackupHttpClient = class {
142
+ static {
143
+ __name(this, "BackupHttpClient");
144
+ }
145
+ options;
146
+ fetchFn;
147
+ constructor(options) {
148
+ this.options = options;
149
+ this.fetchFn = options.fetch ?? fetch;
150
+ }
151
+ url(path) {
152
+ const base = this.options.baseUrl.replace(/\/$/, "");
153
+ return `${base}/developer/agents/${encodeURIComponent(this.options.agentId)}${path}`;
154
+ }
155
+ async json(method, path, body) {
156
+ const endpoint = this.url(path);
157
+ const res = await this.fetchFn(endpoint, {
158
+ method,
159
+ headers: {
160
+ Authorization: this.options.authHeader,
161
+ ...body !== void 0 ? {
162
+ "Content-Type": "application/json"
163
+ } : {},
164
+ Accept: "application/json"
165
+ },
166
+ body: body !== void 0 ? JSON.stringify(body) : void 0
167
+ });
168
+ if (!res.ok) {
169
+ let text2;
170
+ try {
171
+ text2 = await res.text();
172
+ } catch {
173
+ text2 = void 0;
174
+ }
175
+ throw new BackupHttpError(`Backup request failed: ${method} ${path} \u2192 ${res.status} ${res.statusText}`, res.status, endpoint, text2);
176
+ }
177
+ const text = await res.text();
178
+ if (!text) return void 0;
179
+ return JSON.parse(text);
180
+ }
181
+ /** `POST /backup/check-blobs` — returns existing vs missing partition. */
182
+ checkBlobsExist(hashes) {
183
+ return this.json("POST", "/backup/check-blobs", {
184
+ hashes
185
+ });
186
+ }
187
+ /** `POST /backup/blob-upload-urls` — presigned S3 PUT URLs. */
188
+ getBlobUploadUrls(hashes) {
189
+ return this.json("POST", "/backup/blob-upload-urls", {
190
+ hashes
191
+ });
192
+ }
193
+ /** `POST /backup/blob-urls` — presigned S3 GET URLs (for restore). */
194
+ getBlobUrls(hashes) {
195
+ return this.json("POST", "/backup/blob-urls", {
196
+ hashes
197
+ });
198
+ }
199
+ /** `POST /backup/manifest` — final step in a push; returns server metadata. */
200
+ saveManifest(data) {
201
+ return this.json("POST", "/backup/manifest", data);
202
+ }
203
+ /** `GET /backup` — metadata only, no file list. */
204
+ getMetadata() {
205
+ return this.json("GET", "/backup");
206
+ }
207
+ /** `GET /backup/manifest` — metadata + file list. */
208
+ getManifest() {
209
+ return this.json("GET", "/backup/manifest");
210
+ }
211
+ /** `GET /backup/check/:hash` — fast freshness probe. */
212
+ checkBackupExists(hash) {
213
+ return this.json("GET", `/backup/check/${encodeURIComponent(hash)}`);
214
+ }
215
+ };
216
+
217
+ // src/transfer.ts
218
+ import { gunzipSync, gzipSync } from "zlib";
219
+ var DEFAULT_CONCURRENCY = 10;
220
+ async function uploadBlobs(opts) {
221
+ const fetchFn = opts.fetch ?? fetch;
222
+ const concurrency = opts.concurrency ?? DEFAULT_CONCURRENCY;
223
+ const hashes = Object.keys(opts.uploadUrls);
224
+ for (let i = 0; i < hashes.length; i += concurrency) {
225
+ const batch = hashes.slice(i, i + concurrency);
226
+ await Promise.all(batch.map(async (hash) => {
227
+ const buf = opts.contentByHash.get(hash);
228
+ if (!buf) {
229
+ throw new Error(`uploadBlobs: no content for hash ${hash.slice(0, 12)}\u2026`);
230
+ }
231
+ const compressed = gzipSync(buf);
232
+ const res = await fetchFn(opts.uploadUrls[hash], {
233
+ method: "PUT",
234
+ body: compressed,
235
+ headers: {
236
+ "Content-Type": "application/octet-stream"
237
+ }
238
+ });
239
+ if (!res.ok) {
240
+ throw new Error(`S3 upload failed for ${hash.slice(0, 12)}\u2026: ${res.status} ${res.statusText}`);
241
+ }
242
+ }));
243
+ }
244
+ return hashes.length;
245
+ }
246
+ __name(uploadBlobs, "uploadBlobs");
247
+ async function downloadBlobs(opts) {
248
+ const fetchFn = opts.fetch ?? fetch;
249
+ const concurrency = opts.concurrency ?? DEFAULT_CONCURRENCY;
250
+ const out = /* @__PURE__ */ new Map();
251
+ const entries = Object.entries(opts.urls);
252
+ for (let i = 0; i < entries.length; i += concurrency) {
253
+ const batch = entries.slice(i, i + concurrency);
254
+ const downloaded = await Promise.all(batch.map(async ([hash, url]) => {
255
+ const res = await fetchFn(url);
256
+ if (!res.ok) {
257
+ throw new Error(`S3 download failed for ${hash.slice(0, 12)}\u2026: ${res.status} ${res.statusText}`);
258
+ }
259
+ const ab = await res.arrayBuffer();
260
+ const buf = Buffer.from(ab);
261
+ if (buf.length >= 2 && buf[0] === 31 && buf[1] === 139) {
262
+ try {
263
+ return [
264
+ hash,
265
+ gunzipSync(buf)
266
+ ];
267
+ } catch {
268
+ return [
269
+ hash,
270
+ buf
271
+ ];
272
+ }
273
+ }
274
+ return [
275
+ hash,
276
+ buf
277
+ ];
278
+ }));
279
+ for (const [hash, buf] of downloaded) out.set(hash, buf);
280
+ }
281
+ return out;
282
+ }
283
+ __name(downloadBlobs, "downloadBlobs");
284
+
285
+ // src/restore.ts
286
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, statSync as statSync2, writeFileSync } from "fs";
287
+ import { dirname, join as join2, resolve, sep as sep2 } from "path";
288
+ function resolveBackupFileTarget(file, targetDir) {
289
+ const base = resolve(targetDir);
290
+ const sandboxed = file.external ? join2(base, ".lua", "external", file.relativePath) : join2(base, file.relativePath);
291
+ const resolved = resolve(sandboxed);
292
+ if (resolved !== base && !resolved.startsWith(base + sep2)) {
293
+ throw new Error(`Backup entry escapes target directory: ${file.relativePath}`);
294
+ }
295
+ return resolved;
296
+ }
297
+ __name(resolveBackupFileTarget, "resolveBackupFileTarget");
298
+ function restoreFromBlobs(manifest, blobs, targetDir, opts = {}) {
299
+ let filesWritten = 0;
300
+ let filesUnchanged = 0;
301
+ let filesSkipped = 0;
302
+ for (const file of manifest.files) {
303
+ const content = blobs.get(file.hash);
304
+ if (!content) {
305
+ throw new Error(`Blob not found for hash: ${file.hash} (${file.relativePath})`);
306
+ }
307
+ const targetPath = resolveBackupFileTarget(file, targetDir);
308
+ if (existsSync(targetPath)) {
309
+ const sameSize = statSync2(targetPath).size === content.length;
310
+ if (sameSize && readFileSync2(targetPath).equals(content)) {
311
+ filesUnchanged++;
312
+ continue;
313
+ }
314
+ if (!opts.overwrite) {
315
+ filesSkipped++;
316
+ continue;
317
+ }
318
+ }
319
+ mkdirSync(dirname(targetPath), {
320
+ recursive: true
321
+ });
322
+ writeFileSync(targetPath, content);
323
+ filesWritten++;
324
+ }
325
+ return {
326
+ filesWritten,
327
+ filesUnchanged,
328
+ filesSkipped
329
+ };
330
+ }
331
+ __name(restoreFromBlobs, "restoreFromBlobs");
332
+
333
+ // src/push.ts
334
+ async function pushAgentBackup(opts) {
335
+ const snapshot = walkWorkspace(opts.workspaceDir, opts.walkOptions);
336
+ const client = new BackupHttpClient(opts.http);
337
+ const uniqueHashes = [
338
+ ...new Set(snapshot.files.map((f) => f.hash))
339
+ ];
340
+ let filesUploaded = 0;
341
+ if (uniqueHashes.length > 0) {
342
+ const checked = await client.checkBlobsExist(uniqueHashes);
343
+ if (checked.missing.length > 0) {
344
+ const urls = await client.getBlobUploadUrls(checked.missing);
345
+ filesUploaded = await uploadBlobs({
346
+ uploadUrls: urls.urls,
347
+ contentByHash: snapshot.contentByHash,
348
+ fetch: opts.http.fetch,
349
+ concurrency: opts.concurrency
350
+ });
351
+ }
352
+ }
353
+ const metadata = await client.saveManifest({
354
+ projectHash: snapshot.projectHash,
355
+ files: snapshot.files,
356
+ version: opts.version,
357
+ orgId: opts.orgId,
358
+ createdBy: opts.createdBy ?? "cli",
359
+ triggeredBy: opts.triggeredBy
360
+ });
361
+ return {
362
+ projectHash: snapshot.projectHash,
363
+ fileCount: snapshot.files.length,
364
+ filesUploaded,
365
+ activeVersion: metadata.activeVersion,
366
+ metadata,
367
+ files: snapshot.files
368
+ };
369
+ }
370
+ __name(pushAgentBackup, "pushAgentBackup");
371
+
372
+ // src/pull.ts
373
+ async function pullAgentBackup(opts) {
374
+ const client = new BackupHttpClient(opts.http);
375
+ const manifest = await client.getManifest();
376
+ const uniqueHashes = [
377
+ ...new Set(manifest.files.map((f) => f.hash))
378
+ ];
379
+ const blobs = uniqueHashes.length ? await (async () => {
380
+ const urls = await client.getBlobUrls(uniqueHashes);
381
+ return downloadBlobs({
382
+ urls: urls.urls,
383
+ fetch: opts.http.fetch,
384
+ concurrency: opts.concurrency
385
+ });
386
+ })() : /* @__PURE__ */ new Map();
387
+ const result = restoreFromBlobs(manifest, blobs, opts.targetDir, opts.restore);
388
+ return {
389
+ ...result,
390
+ manifest
391
+ };
392
+ }
393
+ __name(pullAgentBackup, "pullAgentBackup");
394
+
395
+ // src/archive.ts
396
+ import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
397
+ import { dirname as dirname2, join as join3, sep as sep3 } from "path";
398
+ import { gunzipSync as gunzipSync2, gzipSync as gzipSync2 } from "zlib";
399
+ var ARCHIVE_SCHEMA_VERSION = 1;
400
+ function encodeWorkspaceArchive(workspaceDir) {
401
+ const files = {};
402
+ try {
403
+ walk(workspaceDir, "", files);
404
+ } catch {
405
+ return null;
406
+ }
407
+ if (Object.keys(files).length === 0) return null;
408
+ const gz = gzipSync2(Buffer.from(JSON.stringify(files), "utf8"));
409
+ return gz.toString("base64");
410
+ }
411
+ __name(encodeWorkspaceArchive, "encodeWorkspaceArchive");
412
+ function decodeWorkspaceArchive(archive, schemaVersion) {
413
+ if (schemaVersion !== void 0 && schemaVersion !== ARCHIVE_SCHEMA_VERSION) {
414
+ throw new Error(`Unsupported archive schema version: ${schemaVersion} (expected ${ARCHIVE_SCHEMA_VERSION})`);
415
+ }
416
+ const raw = gunzipSync2(Buffer.from(archive, "base64")).toString("utf8");
417
+ const parsed = JSON.parse(raw);
418
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
419
+ throw new Error("Decoded archive is not a JSON object map");
420
+ }
421
+ return parsed;
422
+ }
423
+ __name(decodeWorkspaceArchive, "decodeWorkspaceArchive");
424
+ function writeArchiveToWorkspace(workspaceDir, files) {
425
+ for (const [relPath, content] of Object.entries(files)) {
426
+ if (relPath.includes("..")) {
427
+ throw new Error(`Refusing to write archived path containing '..': ${relPath}`);
428
+ }
429
+ const abs = join3(workspaceDir, relPath);
430
+ mkdirSync2(dirname2(abs), {
431
+ recursive: true
432
+ });
433
+ writeFileSync2(abs, content, "utf8");
434
+ }
435
+ }
436
+ __name(writeArchiveToWorkspace, "writeArchiveToWorkspace");
437
+ function walk(root, prefix, out) {
438
+ const dir = prefix ? join3(root, prefix) : root;
439
+ const entries = readdirSync2(dir, {
440
+ withFileTypes: true
441
+ });
442
+ for (const entry of entries) {
443
+ if (entry.isDirectory()) {
444
+ if (shouldSkipDirectory(entry.name) || ARCHIVE_ONLY_SKIP_DIRECTORIES.has(entry.name) || shouldSkipFile(entry.name)) continue;
445
+ walk(root, prefix ? `${prefix}${sep3}${entry.name}` : entry.name, out);
446
+ continue;
447
+ }
448
+ if (!entry.isFile()) continue;
449
+ if (shouldSkipFile(entry.name)) continue;
450
+ const relPath = (prefix ? `${prefix}${sep3}${entry.name}` : entry.name).split(sep3).join("/");
451
+ const abs = join3(dir, entry.name);
452
+ if (statSync3(abs).size > DEFAULT_MAX_FILE_BYTES) continue;
453
+ try {
454
+ out[relPath] = readFileSync3(abs, "utf8");
455
+ } catch {
456
+ }
457
+ }
458
+ }
459
+ __name(walk, "walk");
460
+
461
+ // src/skill-source.ts
462
+ var SKILL_SOURCE_EDITORS = [
463
+ "builder",
464
+ "cli",
465
+ "engineer",
466
+ "system"
467
+ ];
468
+ export {
469
+ ARCHIVE_SCHEMA_VERSION,
470
+ BackupHttpClient,
471
+ BackupHttpError,
472
+ DEFAULT_MAX_FILE_BYTES,
473
+ FILE_HASH_LENGTH,
474
+ SKILL_SOURCE_EDITORS,
475
+ classifyFile,
476
+ combineFileHashes,
477
+ decodeWorkspaceArchive,
478
+ downloadBlobs,
479
+ encodeWorkspaceArchive,
480
+ hashContentTruncated,
481
+ pullAgentBackup,
482
+ pushAgentBackup,
483
+ resolveBackupFileTarget,
484
+ restoreFromBlobs,
485
+ sha256Hex,
486
+ shouldSkipDirectory,
487
+ shouldSkipFile,
488
+ uploadBlobs,
489
+ walkWorkspace,
490
+ writeArchiveToWorkspace
491
+ };
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@lua/shared-source-sync",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "clean": "rm -rf dist",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest"
20
+ },
21
+ "devDependencies": {
22
+ "tsup": "^8.5.1",
23
+ "typescript": "^5.8.2",
24
+ "vitest": "^3.1.1"
25
+ }
26
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Workspace ↔ archive serialization. Wire format:
3
+ * gzip(JSON.stringify({ [relPath]: contents })) → base64.
4
+ * Spec: docs/superpowers/specs/2026-05-02-canonical-tool-source-design.md.
5
+ *
6
+ * A JSON map is used (vs tar) so neither the CLI bundle nor the
7
+ * lua-claude-builder runtime needs a tar dependency. The format is symmetric
8
+ * across both, so encode/decode go through these helpers — never inline.
9
+ */
10
+
11
+ import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs';
12
+ import { dirname, join, sep } from 'path';
13
+ import { gunzipSync, gzipSync } from 'zlib';
14
+ import { ARCHIVE_ONLY_SKIP_DIRECTORIES, DEFAULT_MAX_FILE_BYTES, shouldSkipDirectory, shouldSkipFile } from './filter';
15
+
16
+ export const ARCHIVE_SCHEMA_VERSION = 1;
17
+
18
+ /**
19
+ * Encode a workspace directory tree into a base64-gzip JSON map. Returns
20
+ * `null` if the workspace doesn't exist or is empty.
21
+ */
22
+ export function encodeWorkspaceArchive(workspaceDir: string): string | null {
23
+ const files: Record<string, string> = {};
24
+ try {
25
+ walk(workspaceDir, '', files);
26
+ } catch {
27
+ return null;
28
+ }
29
+ if (Object.keys(files).length === 0) return null;
30
+
31
+ const gz = gzipSync(Buffer.from(JSON.stringify(files), 'utf8'));
32
+ return gz.toString('base64');
33
+ }
34
+
35
+ /**
36
+ * Decode a base64-gzip JSON map back to a `{ relPath: content }` object.
37
+ * Throws on malformed payloads — callers should treat that as a corrupt
38
+ * canonical record and surface clearly rather than silently writing nothing.
39
+ */
40
+ export function decodeWorkspaceArchive(archive: string, schemaVersion?: number): Record<string, string> {
41
+ if (schemaVersion !== undefined && schemaVersion !== ARCHIVE_SCHEMA_VERSION) {
42
+ throw new Error(`Unsupported archive schema version: ${schemaVersion} (expected ${ARCHIVE_SCHEMA_VERSION})`);
43
+ }
44
+ const raw = gunzipSync(Buffer.from(archive, 'base64')).toString('utf8');
45
+ const parsed = JSON.parse(raw);
46
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
47
+ throw new Error('Decoded archive is not a JSON object map');
48
+ }
49
+ return parsed as Record<string, string>;
50
+ }
51
+
52
+ /**
53
+ * Write the contents of a decoded archive into `workspaceDir`. Existing files
54
+ * are overwritten — caller is responsible for save-pending-changes prompts.
55
+ * Defense-in-depth: any `relPath` containing `..` is rejected.
56
+ */
57
+ export function writeArchiveToWorkspace(workspaceDir: string, files: Record<string, string>): void {
58
+ for (const [relPath, content] of Object.entries(files)) {
59
+ if (relPath.includes('..')) {
60
+ throw new Error(`Refusing to write archived path containing '..': ${relPath}`);
61
+ }
62
+ const abs = join(workspaceDir, relPath);
63
+ mkdirSync(dirname(abs), { recursive: true });
64
+ writeFileSync(abs, content, 'utf8');
65
+ }
66
+ }
67
+
68
+ function walk(root: string, prefix: string, out: Record<string, string>): void {
69
+ const dir = prefix ? join(root, prefix) : root;
70
+ const entries = readdirSync(dir, { withFileTypes: true });
71
+ for (const entry of entries) {
72
+ if (entry.isDirectory()) {
73
+ // Archive-only set lives alongside the shared shouldSkipDirectory
74
+ // so build/ stays out of per-skill archives without leaking into
75
+ // the canonical primitive scan (compiler) or the per-agent backup
76
+ // walker — see filter.ts and PR #402 review for the rationale.
77
+ if (
78
+ shouldSkipDirectory(entry.name) ||
79
+ ARCHIVE_ONLY_SKIP_DIRECTORIES.has(entry.name) ||
80
+ shouldSkipFile(entry.name)
81
+ )
82
+ continue;
83
+ walk(root, prefix ? `${prefix}${sep}${entry.name}` : entry.name, out);
84
+ continue;
85
+ }
86
+ if (!entry.isFile()) continue;
87
+ if (shouldSkipFile(entry.name)) continue;
88
+
89
+ const relPath = (prefix ? `${prefix}${sep}${entry.name}` : entry.name).split(sep).join('/');
90
+ const abs = join(dir, entry.name);
91
+ if (statSync(abs).size > DEFAULT_MAX_FILE_BYTES) continue;
92
+ try {
93
+ out[relPath] = readFileSync(abs, 'utf8');
94
+ } catch {
95
+ // Binary or unreadable — skip rather than corrupt the archive
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * File-inclusion rules used when walking an agent workspace.
3
+ *
4
+ * Wire-compat invariant: identical content must produce identical projectHashes
5
+ * across the CLI compile path and any server-side walker, or the per-agent
6
+ * backup dedupe falls apart. The CLI compiler re-exports these from here.
7
+ */
8
+
9
+ import { extname } from 'path';
10
+
11
+ const SKIP_DIRECTORIES = new Set([
12
+ 'node_modules',
13
+ 'dist',
14
+ 'dist-v2',
15
+ '.git',
16
+ '.lua',
17
+ '.temp',
18
+ 'coverage',
19
+ '.next',
20
+ '.turbo',
21
+ ]);
22
+
23
+ /**
24
+ * Directories the per-skill workspace archive encoder excludes ON TOP OF
25
+ * `SKIP_DIRECTORIES`. Kept separate so the archive's denormalized "drop
26
+ * compiled artifacts" semantics doesn't leak into the canonical primitive
27
+ * scan (`compiler/utils/common.ts::shouldSkipDirectory`) or the per-agent
28
+ * backup walker (`walkWorkspace`), both of which must include `build/`
29
+ * source so developers can keep tool code under custom layouts (Vite,
30
+ * Next.js, etc.) without it silently disappearing from compile or sync.
31
+ *
32
+ * Only `archive.ts::walk` consults this set. Adding entries here is safe;
33
+ * adding them to `SKIP_DIRECTORIES` is a category error — see PR #402
34
+ * Bugbot review (2026-05-13) for the regression that caused this split.
35
+ */
36
+ export const ARCHIVE_ONLY_SKIP_DIRECTORIES = new Set(['build']);
37
+
38
+ export const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
39
+
40
+ export function shouldSkipDirectory(name: string): boolean {
41
+ return SKIP_DIRECTORIES.has(name);
42
+ }
43
+
44
+ export function shouldSkipFile(filePathOrName: string): boolean {
45
+ return (
46
+ filePathOrName.includes('node_modules') ||
47
+ filePathOrName.includes('.test.') ||
48
+ filePathOrName.includes('.spec.') ||
49
+ filePathOrName.includes('__tests__') ||
50
+ filePathOrName.endsWith('.d.ts') ||
51
+ filePathOrName.endsWith('.map') ||
52
+ filePathOrName.startsWith('.') ||
53
+ filePathOrName === 'package-lock.json' ||
54
+ filePathOrName === 'yarn.lock' ||
55
+ filePathOrName === 'pnpm-lock.yaml'
56
+ );
57
+ }
58
+
59
+ const KIND_BY_EXT: Record<string, 'source' | 'config'> = {
60
+ '.ts': 'source',
61
+ '.tsx': 'source',
62
+ '.js': 'source',
63
+ '.jsx': 'source',
64
+ '.yaml': 'config',
65
+ '.yml': 'config',
66
+ '.json': 'config',
67
+ '.toml': 'config',
68
+ };
69
+
70
+ export function classifyFile(relPath: string): 'source' | 'config' | 'other' {
71
+ return KIND_BY_EXT[extname(relPath)] ?? 'other';
72
+ }