@hitslop/cli 0.1.4 → 0.3.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/dist/cli.js CHANGED
@@ -1,59 +1,69 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
3
18
 
4
- // src/cli.ts
5
- import { Crust } from "@crustjs/core";
6
- import { join as join9 } from "path";
7
- import { stat as stat7 } from "fs/promises";
8
- import { createInterface } from "readline/promises";
9
- import { SlopCategorySchema } from "@hitslop/schema";
10
-
11
- // src/project.ts
12
- import { cp, lstat as lstat2, mkdir as mkdir2, readFile as readFile3, readdir as readdir2, rm, stat as stat3, writeFile as writeFile2 } from "fs/promises";
13
- import { basename, dirname as dirname2, join as join3, relative, resolve, sep } from "path";
14
- import { fileURLToPath, pathToFileURL } from "url";
15
- import { createHash } from "crypto";
16
- import { zipSync } from "fflate";
17
- import { decode as decodePng } from "fast-png";
18
- import { build as viteBuild } from "vite";
19
- import * as z from "zod";
20
- import { manifestSchemaURL, parseManifest } from "@hitslop/schema";
21
19
  // package.json
22
- var package_default = {
23
- name: "@hitslop/cli",
24
- version: "0.1.4",
25
- description: "Create, develop, build, register, and publish hitSlop apps.",
26
- license: "MIT",
27
- repository: { type: "git", url: "git+https://github.com/hitslop/hitslop.git", directory: "packages/cli" },
28
- homepage: "https://hitslop.com",
29
- bugs: { url: "https://github.com/hitslop/hitslop/issues" },
30
- keywords: ["hitslop", "cli", "local-first", "mini-apps"],
31
- publishConfig: { access: "public" },
32
- type: "module",
33
- bin: {
34
- slop: "dist/cli.js"
35
- },
36
- files: ["dist", "templates"],
37
- scripts: {
38
- build: "bun ../../scripts/clean-dist.ts && bun build src/cli.ts --target bun --format esm --packages external --outfile dist/cli.js && /bin/chmod +x dist/cli.js",
39
- check: "tsc -p tsconfig.json",
40
- test: "bun run --cwd ../schema build && bun test"
41
- },
42
- dependencies: {
43
- "@crustjs/core": "^0.0.19",
44
- "@hitslop/schema": "^0.1.3",
45
- "@noble/ed25519": "^3.2.0",
46
- "fast-png": "^8.0.0",
47
- fflate: "^0.8.3",
48
- postcss: "^8.5.6",
49
- vite: "^8.2.2",
50
- zod: "^4.5.2"
51
- },
52
- devDependencies: {
53
- "@types/bun": "latest",
54
- typescript: "^7.0.2"
55
- }
56
- };
20
+ var package_default;
21
+ var init_package = __esm(() => {
22
+ package_default = {
23
+ name: "@hitslop/cli",
24
+ version: "0.3.0",
25
+ description: "Create, develop, build, register, and publish hitSlop apps.",
26
+ license: "MIT",
27
+ repository: {
28
+ type: "git",
29
+ url: "git+https://github.com/hitslop/hitslop.git",
30
+ directory: "packages/cli"
31
+ },
32
+ homepage: "https://hitslop.com",
33
+ bugs: {
34
+ url: "https://github.com/hitslop/hitslop/issues"
35
+ },
36
+ keywords: ["hitslop", "cli", "local-first", "mini-apps"],
37
+ publishConfig: {
38
+ access: "public"
39
+ },
40
+ type: "module",
41
+ bin: {
42
+ slop: "dist/cli.js"
43
+ },
44
+ files: ["dist", "templates"],
45
+ scripts: {
46
+ build: "bun ../../scripts/clean-dist.ts && bun build src/cli.ts src/module-loader.ts --target bun --format esm --packages external --outdir dist && /bin/chmod +x dist/cli.js",
47
+ check: "tsc -p tsconfig.json",
48
+ test: "bun run --cwd ../schema build && bun test"
49
+ },
50
+ dependencies: {
51
+ "@crustjs/core": "^0.0.19",
52
+ "@hitslop/schema": "^0.3.0",
53
+ "@noble/ed25519": "^3.2.0",
54
+ "fast-png": "^8.0.0",
55
+ fflate: "^0.8.3",
56
+ parse5: "^8.0.1",
57
+ postcss: "^8.5.6",
58
+ vite: "^8.2.2"
59
+ },
60
+ devDependencies: {
61
+ "@types/bun": "latest",
62
+ typebox: "^1.3.26",
63
+ typescript: "^7.0.2"
64
+ }
65
+ };
66
+ });
57
67
 
58
68
  // src/document-skill.ts
59
69
  import { lstat, mkdir, readFile, readdir, stat, writeFile } from "fs/promises";
@@ -63,11 +73,8 @@ import {
63
73
  documentGuideSource,
64
74
  documentSkillContent,
65
75
  documentSkillPath,
66
- isCanonicalDocumentSkill,
67
76
  maxDocumentGuideBytes
68
77
  } from "@hitslop/schema";
69
- var decoder = new TextDecoder("utf-8", { fatal: true });
70
- var exists = async (path) => stat(path).then(() => true).catch(() => false);
71
78
  async function emitDocumentSkill(sourceRoot, outputRoot) {
72
79
  const skill = join(outputRoot, documentSkillPath);
73
80
  await mkdir(dirname(skill), { recursive: true });
@@ -99,7 +106,7 @@ async function readDocumentGuideSource(sourceRoot) {
99
106
  async function validateDocumentSkill(root) {
100
107
  const agents = join(root, ".agents");
101
108
  if (!await exists(agents)) {
102
- throw new Error(`Runtime package is missing ${documentSkillPath}.`);
109
+ return;
103
110
  }
104
111
  await requireDirectory(agents, ".agents");
105
112
  await requireOnly(agents, new Set(["skills"]), ".agents");
@@ -115,14 +122,11 @@ async function validateDocumentSkill(root) {
115
122
  });
116
123
  if (!skillInfo?.isFile() || skillInfo.isSymbolicLink())
117
124
  throw new Error(`${documentSkillPath} must be a regular file.`);
118
- let canonical = false;
119
125
  try {
120
- canonical = isCanonicalDocumentSkill(await readFile(skillPath));
126
+ decoder.decode(await readFile(skillPath));
121
127
  } catch {
122
128
  throw new Error(`${documentSkillPath} must be UTF-8.`);
123
129
  }
124
- if (!canonical)
125
- throw new Error(`${documentSkillPath} is not a recognized canonical hitSlop document skill.`);
126
130
  const references = join(folder, "references");
127
131
  if (!await exists(references))
128
132
  return;
@@ -157,13 +161,60 @@ async function requireOnly(directory, allowed, label, optionalReferences = false
157
161
  }
158
162
  }
159
163
  }
164
+ var decoder, exists = async (path) => stat(path).then(() => true).catch(() => false);
165
+ var init_document_skill = __esm(() => {
166
+ decoder = new TextDecoder("utf-8", { fatal: true });
167
+ });
168
+
169
+ // src/evaluate-module.ts
170
+ import { fileURLToPath } from "url";
171
+ import { dirname as dirname2, resolve } from "path";
172
+ async function evaluateModule(path, kind, timeoutMs = 1e4) {
173
+ const absolute = resolve(path);
174
+ const loader = Bun.resolveSync("./module-loader", dirname2(fileURLToPath(import.meta.url)));
175
+ let receive;
176
+ const result = new Promise((resolve2) => {
177
+ receive = resolve2;
178
+ });
179
+ const child = Bun.spawn([process.execPath, loader, absolute, kind], {
180
+ stdin: "ignore",
181
+ stdout: process.stderr.fd,
182
+ stderr: process.stderr.fd,
183
+ ipc: (message) => receive(message)
184
+ });
185
+ let timer;
186
+ try {
187
+ const message = await Promise.race([
188
+ result,
189
+ child.exited.then((code) => {
190
+ throw new Error(`Loader exited without a result (status ${code})`);
191
+ }),
192
+ new Promise((_, reject) => {
193
+ timer = setTimeout(() => reject(new Error(`${kind} evaluation timed out after ${timeoutMs}ms`)), timeoutMs);
194
+ })
195
+ ]);
196
+ if (!message || typeof message !== "object" || !("ok" in message))
197
+ throw new Error("Invalid loader response");
198
+ if (message.ok === false && "error" in message && typeof message.error === "string")
199
+ throw new Error(message.error);
200
+ if (message.ok !== true || !("value" in message))
201
+ throw new Error("Invalid loader response");
202
+ return message.value;
203
+ } catch (error) {
204
+ throw new Error(`Could not evaluate ${kind}.ts (${absolute}): ${error instanceof Error ? error.message : String(error)}`, { cause: error });
205
+ } finally {
206
+ clearTimeout(timer);
207
+ child.disconnect();
208
+ child.kill("SIGKILL");
209
+ await child.exited;
210
+ }
211
+ }
212
+ var init_evaluate_module = () => {};
160
213
 
161
214
  // src/theme.ts
162
215
  import { readFile as readFile2, stat as stat2 } from "fs/promises";
163
216
  import { join as join2 } from "path";
164
217
  import postcss from "postcss";
165
- var exists2 = async (path) => stat2(path).then(() => true).catch(() => false);
166
- var decoder2 = new TextDecoder("utf-8", { fatal: true });
167
218
  async function readUTF8(path, label) {
168
219
  try {
169
220
  return decoder2.decode(await readFile2(path));
@@ -172,13 +223,31 @@ async function readUTF8(path, label) {
172
223
  }
173
224
  }
174
225
  async function readThemeContract(root) {
226
+ const source = await readThemeCSS(root);
227
+ return themeContract(source);
228
+ }
229
+ function themeContract(source) {
230
+ if (source === undefined)
231
+ return;
232
+ const contract = parseTheme(source, "theme defaults");
233
+ validateThemeReferences(source, contract, "theme defaults");
234
+ return contract;
235
+ }
236
+ async function readThemeCSS(root) {
237
+ const definition = join2(root, "theme.ts");
175
238
  const path = join2(root, "assets", "theme.css");
239
+ if (await exists2(definition)) {
240
+ if (await exists2(path))
241
+ throw new Error("Define theme defaults in theme.ts or assets/theme.css, not both.");
242
+ const css = await evaluateModule(definition, "theme");
243
+ if (typeof css !== "string")
244
+ throw new Error("theme.ts must default-export defineTheme(...)");
245
+ parseTheme(css, "theme.ts");
246
+ return css;
247
+ }
176
248
  if (!await exists2(path))
177
249
  return;
178
- const source = await readUTF8(path, "assets/theme.css");
179
- const contract = parseTheme(source, "assets/theme.css");
180
- validateThemeReferences(source, contract, "assets/theme.css");
181
- return contract;
250
+ return readUTF8(path, "assets/theme.css");
182
251
  }
183
252
  async function validateThemeOverride(root, contract) {
184
253
  const path = join2(root, "stores", "theme.css");
@@ -232,109 +301,422 @@ function parseTheme(source, label) {
232
301
  throw new Error(`${label} must define at least one --slop-* custom property.`);
233
302
  return properties;
234
303
  }
304
+ var exists2 = async (path) => stat2(path).then(() => true).catch(() => false), decoder2;
305
+ var init_theme = __esm(() => {
306
+ init_evaluate_module();
307
+ decoder2 = new TextDecoder("utf-8", { fatal: true });
308
+ });
235
309
 
236
310
  // src/vite-build-plugin.ts
237
- import { version as viteVersion } from "vite";
238
- var escapePattern = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
239
- function inlineScript(html, fileName, code) {
240
- const name = escapePattern(fileName);
241
- const tag = new RegExp(`<script([^>]*?)\\ssrc=["'](?:[^"']*\\/)?${name}["']([^>]*)>\\s*</script>`, "g");
242
- const safeCode = code.replace(/"?__VITE_PRELOAD__"?/g, "void 0").replace(/<(\/script>|!--)/g, "\\x3C$1");
243
- return html.replace(tag, (_match, before, after) => `<script${before}${after}>${safeCode.trim()}</script>`);
311
+ import { parse } from "parse5";
312
+ function elements(html) {
313
+ const result = [];
314
+ const visit = (node) => {
315
+ if ("tagName" in node)
316
+ result.push(node);
317
+ if ("childNodes" in node)
318
+ node.childNodes.forEach(visit);
319
+ };
320
+ visit(parse(html, { sourceCodeLocationInfo: true }));
321
+ return result;
322
+ }
323
+ function replaceElements(html, replace) {
324
+ for (const element of elements(html).reverse()) {
325
+ const replacement = replace(element);
326
+ const location = element.sourceCodeLocation;
327
+ if (replacement !== undefined && location)
328
+ html = html.slice(0, location.startOffset) + replacement + html.slice(location.endOffset);
329
+ }
330
+ return html;
244
331
  }
245
332
  function inlineStyle(html, fileName, css) {
246
- const name = escapePattern(fileName);
247
- const tag = new RegExp(`<link([^>]*?)\\shref=["'](?:[^"']*\\/)?${name}["']([^>]*)>`, "g");
248
- return html.replace(tag, (_match, before, after) => `<style${before}${after}>${css.replace('@charset "UTF-8";', "").trim()}</style>`);
249
- }
250
- var addThemeLinks = (html) => {
251
- if (html.includes("data-hitslop-theme-default"))
252
- return html;
253
- const links = '<link rel="stylesheet" href="assets/theme.css" data-hitslop-theme-default><link rel="stylesheet" href="theme.css" data-hitslop-theme>';
254
- return /<\/head>/i.test(html) ? html.replace(/<\/head>/i, `${links}</head>`) : links + html;
255
- };
333
+ return replaceElements(html, (node) => {
334
+ if (node.tagName !== "link" || !matches(attribute(node, "href"), fileName))
335
+ return;
336
+ const media = attribute(node, "media");
337
+ const attributes = media ? ' media="' + media.replaceAll("&", "&amp;").replaceAll('"', "&quot;") + '"' : "";
338
+ return "<style" + attributes + ">" + css.replace(/@charset\s+"UTF-8";/i, "").replace(/<\/style/gi, "<\\/style").trim() + "</style>";
339
+ });
340
+ }
256
341
  function hitSlopBuildPlugin(options) {
257
- const configure = (config) => {
258
- config.base = "./";
259
- config.build ??= {};
260
- config.build.assetsInlineLimit = () => true;
261
- config.build.chunkSizeWarningLimit = 1e8;
262
- config.build.cssCodeSplit = false;
263
- config.build.assetsDir = "";
264
- config.build.rollupOptions ??= {};
265
- config.build.rollupOptions.output ??= {};
266
- const outputs = Array.isArray(config.build.rollupOptions.output) ? config.build.rollupOptions.output : [config.build.rollupOptions.output];
267
- for (const output of outputs) {
268
- if (Number.parseInt(viteVersion.split(".")[0] ?? "0", 10) >= 8) {
269
- output.codeSplitting = false;
270
- } else
271
- output.inlineDynamicImports = true;
272
- }
273
- };
274
342
  return {
275
343
  name: "hitslop:build",
276
344
  enforce: "post",
277
- config: configure,
345
+ config: () => ({ base: "./", build: { cssCodeSplit: false, assetsDir: "assets", sourcemap: false, assetsInlineLimit: 4096 } }),
278
346
  generateBundle(_output, bundle) {
279
- const htmlAssets = Object.values(bundle).filter((item) => item.type === "asset" && item.fileName.endsWith(".html"));
280
- const scripts = Object.values(bundle).filter((item) => item.type === "chunk" && /\.[mc]?js$/.test(item.fileName));
281
- const styles = Object.values(bundle).filter((item) => item.type === "asset" && item.fileName.endsWith(".css"));
282
- const consumed = new Set;
283
- for (const asset of htmlAssets) {
284
- let html = typeof asset.source === "string" ? asset.source : new TextDecoder().decode(asset.source);
285
- for (const script of scripts) {
286
- html = inlineScript(html, script.fileName, script.code);
287
- consumed.add(script.fileName);
347
+ for (const asset of Object.values(bundle)) {
348
+ if (asset.type !== "asset" || !asset.fileName.endsWith(".html"))
349
+ continue;
350
+ let html = String(asset.source);
351
+ for (const style of Object.values(bundle)) {
352
+ if (style.type !== "asset" || !style.fileName.endsWith(".css"))
353
+ continue;
354
+ const directory = style.fileName.slice(0, style.fileName.lastIndexOf("/") + 1);
355
+ const css = String(style.source).replace(/url\((['"]?)(\.\/)?([^)'"\s]+)\1\)/g, (match, quote, _dot, url) => /^(?:[a-z]+:|\/|#)/i.test(url) ? match : "url(" + quote + directory + url + quote + ")");
356
+ const updated = inlineStyle(html, style.fileName, css);
357
+ if (updated !== html) {
358
+ html = updated;
359
+ delete bundle[style.fileName];
360
+ }
288
361
  }
289
- for (const style of styles) {
290
- const css = typeof style.source === "string" ? style.source : new TextDecoder().decode(style.source);
291
- html = inlineStyle(html, style.fileName, css);
292
- consumed.add(style.fileName);
362
+ if (options.hasTheme) {
363
+ const head = elements(html).find((node) => node.tagName === "head");
364
+ const offset = head?.sourceCodeLocation?.endTag?.startOffset ?? 0;
365
+ const links = '<link rel="stylesheet" href="assets/theme.css" data-hitslop-theme-default><link rel="stylesheet" href="theme.css" data-hitslop-theme>';
366
+ html = html.slice(0, offset) + links + html.slice(offset);
293
367
  }
294
- if (options.hasTheme)
295
- html = addThemeLinks(html);
296
368
  asset.source = html;
297
369
  }
298
- for (const fileName of consumed)
299
- delete bundle[fileName];
300
370
  }
301
371
  };
302
372
  }
373
+ var attribute = (node, name) => node.attrs.find((attr) => attr.name === name)?.value, matches = (value, file) => value?.replace(/^\.\//, "") === file;
374
+ var init_vite_build_plugin = () => {};
303
375
 
304
- // src/project.ts
305
- var manifestPath = (root) => join3(root, "manifest.json");
306
- async function loadManifest(root) {
307
- const manifest = parseManifest(JSON.parse(await readFile3(manifestPath(root), "utf8")));
308
- await validateSkin(root, manifest);
376
+ // src/data-schema.ts
377
+ import { dataSchemaFromJSON } from "@hitslop/schema";
378
+ async function loadDataSchema(path, timeoutMs = 1e4) {
379
+ return dataSchemaFromJSON(await evaluateModule(path, "schema", timeoutMs));
380
+ }
381
+ var init_data_schema = __esm(() => {
382
+ init_evaluate_module();
383
+ });
384
+
385
+ // src/static-preview.ts
386
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
387
+ import { checkPngDimensions } from "@hitslop/schema";
388
+ import { decode as decodePng, encode as encodePng } from "fast-png";
389
+ function validateStaticPng(bytes, label) {
390
+ if (bytes.byteLength === 0)
391
+ throw new Error(`${label} is empty.`);
392
+ if (bytes.byteLength > MAX_PREVIEW_BYTES)
393
+ throw new Error(`${label} cannot exceed 5 MiB.`);
394
+ checkPngDimensions(bytes, label);
395
+ try {
396
+ return decodePng(bytes, { checkCrc: true });
397
+ } catch {
398
+ throw new Error(`${label} must be a valid PNG image.`);
399
+ }
400
+ }
401
+ function validateIconPng(bytes, label = "The template icon") {
402
+ checkPngDimensions(bytes, label, { width: ICON_SIZE, height: ICON_SIZE });
403
+ return validateStaticPng(bytes, label);
404
+ }
405
+ function iconFromPng(bytes, size = ICON_SIZE) {
406
+ const image = validateStaticPng(bytes, "The template preview");
407
+ const scale = size / Math.max(image.width, image.height);
408
+ const width = Math.max(1, Math.round(image.width * scale));
409
+ const height = Math.max(1, Math.round(image.height * scale));
410
+ const output = image.depth === 16 ? new Uint16Array(size * size * 4) : new Uint8Array(size * size * 4);
411
+ const alpha = image.depth === 16 ? 65535 : 255;
412
+ const offsetX = Math.floor((size - width) / 2);
413
+ const offsetY = Math.floor((size - height) / 2);
414
+ const samples = (position, sourceSize, targetSize) => {
415
+ const ratio = sourceSize / targetSize;
416
+ if (ratio < 1) {
417
+ const center = (position + 0.5) * ratio - 0.5;
418
+ const lower = Math.floor(center), fraction = center - lower;
419
+ return [[Math.max(0, Math.min(sourceSize - 1, lower)), 1 - fraction], [Math.max(0, Math.min(sourceSize - 1, lower + 1)), fraction]];
420
+ }
421
+ const start = position * ratio, end = (position + 1) * ratio;
422
+ const result = [];
423
+ for (let index = Math.floor(start);index < Math.ceil(end); index++) {
424
+ result.push([Math.min(sourceSize - 1, index), Math.max(0, Math.min(end, index + 1) - Math.max(start, index)) / ratio]);
425
+ }
426
+ return result;
427
+ };
428
+ const columns = Array.from({ length: width }, (_, x) => samples(x, image.width, width));
429
+ for (let y = 0;y < height; y += 1) {
430
+ const rows = samples(y, image.height, height);
431
+ for (let x = 0;x < width; x += 1) {
432
+ let red = 0, green = 0, blue = 0, opacity = 0;
433
+ for (const [sy, wy] of rows)
434
+ for (const [sx, wx] of columns[x]) {
435
+ const offset = (sy * image.width + sx) * image.channels;
436
+ const a = (image.channels === 2 || image.channels === 4 ? image.data[offset + image.channels - 1] : alpha) / alpha;
437
+ const weight = wx * wy * a;
438
+ const gray = image.channels <= 2;
439
+ red += image.data[offset] * weight;
440
+ green += image.data[offset + (gray ? 0 : 1)] * weight;
441
+ blue += image.data[offset + (gray ? 0 : 2)] * weight;
442
+ opacity += weight;
443
+ }
444
+ const destination = ((offsetY + y) * size + offsetX + x) * 4;
445
+ output[destination] = Math.round(opacity ? red / opacity : 0);
446
+ output[destination + 1] = Math.round(opacity ? green / opacity : 0);
447
+ output[destination + 2] = Math.round(opacity ? blue / opacity : 0);
448
+ output[destination + 3] = Math.round(opacity * alpha);
449
+ }
450
+ }
451
+ return encodePng({ width: size, height: size, data: output, depth: image.depth, channels: 4 });
452
+ }
453
+ async function writeDefaultIcon(previewPath, iconPath) {
454
+ await writeFile2(iconPath, iconFromPng(await readFile3(previewPath)));
455
+ }
456
+ var MAX_PREVIEW_BYTES, ICON_SIZE = 512;
457
+ var init_static_preview = __esm(() => {
458
+ MAX_PREVIEW_BYTES = 5 * 1024 * 1024;
459
+ });
460
+
461
+ // src/runtime-package.ts
462
+ var exports_runtime_package = {};
463
+ __export(exports_runtime_package, {
464
+ validateRuntimePackage: () => validateRuntimePackage
465
+ });
466
+ import { Database } from "bun:sqlite";
467
+ import { forbiddenPackageNames, validateTemplatePath } from "@hitslop/schema";
468
+ import { lstat as lstat2, readFile as readFile4, readdir as readdir2, stat as stat3 } from "fs/promises";
469
+ import { basename, join as join3, relative, sep } from "path";
470
+ import { unzipSync } from "fflate";
471
+ import { decode as decodePng2 } from "fast-png";
472
+ import { compileDataSchema } from "@hitslop/schema";
473
+ async function validateRuntimePackage(root, options = {}) {
474
+ if (basename(root).startsWith("."))
475
+ throw new Error("Runtime package path is not valid.");
476
+ const rootInfo = await lstat2(root).catch(() => {
477
+ return;
478
+ });
479
+ if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink())
480
+ throw new Error("A runtime .slop must be a directory, not a symlink.");
481
+ const manifest = await loadManifest(root);
482
+ await requireUTF8File(join3(root, "app.html"), "app.html");
483
+ await rejectSymlinks(root);
484
+ const allowed = new Set(["manifest.json", "app.html", "data.schema.json", "assets", "stores", "QuickLook", ".agents", "Icon\r"]);
485
+ for (const entry of await readdir2(root))
486
+ if (!allowed.has(entry))
487
+ throw new Error(`Runtime packages cannot contain ${entry}.`);
488
+ for (const path of await walk(root)) {
489
+ if (forbiddenPackageNames.has(basename(path).toLowerCase()))
490
+ throw new Error(`Runtime packages cannot contain ${relative(root, path)}.`);
491
+ if (options.template)
492
+ validateTemplatePath(relative(root, path).split(sep).join("/"));
493
+ }
494
+ if (options.template && await exists3(join3(root, "stores")))
495
+ throw new Error("Template packages cannot contain stores.");
496
+ if (options.template && await exists3(join3(root, "Icon\r")))
497
+ throw new Error("Template packages cannot contain Finder metadata Icon\\r.");
498
+ const schema = await readDataSchema(root);
499
+ const contract = await readThemeContract(root);
500
+ const appHTML = await readFile4(join3(root, "app.html"), "utf8");
501
+ validateThemeReferences(appHTML, contract, "app.html");
502
+ await validateStores(root, schema, contract);
503
+ await validateQuickLook(root, options.requirePreview ?? false);
504
+ await validateDocumentSkill(root);
309
505
  return manifest;
310
506
  }
311
- var exists3 = async (path) => {
507
+ async function readDataSchema(root) {
508
+ const current = join3(root, "data.schema.json");
509
+ if (!await exists3(current))
510
+ return;
511
+ await requireUTF8File(current, "data.schema.json");
512
+ try {
513
+ const value = JSON.parse(await readFile4(current, "utf8"));
514
+ if (!value || typeof value !== "object" || Array.isArray(value))
515
+ throw new Error("schema root must be an object");
516
+ compileDataSchema(value);
517
+ return value;
518
+ } catch (error) {
519
+ throw new Error(`data.schema.json must contain valid JSON Schema: ${error instanceof Error ? error.message : String(error)}`);
520
+ }
521
+ }
522
+ async function validateStores(root, schema, contract) {
523
+ const stores = join3(root, "stores");
524
+ if (!await exists3(stores))
525
+ return;
526
+ const info = await lstat2(stores);
527
+ if (!info.isDirectory() || info.isSymbolicLink())
528
+ throw new Error("stores must be a directory.");
529
+ const allowed = new Set(["data.json", "data.sqlite", "data.sqlite-wal", "data.sqlite-shm", "media", "theme.css"]);
530
+ for (const entry of await readdir2(stores, { withFileTypes: true })) {
531
+ if (!allowed.has(entry.name))
532
+ throw new Error(`Unexpected store entry stores/${entry.name}.`);
533
+ if (entry.name === "media" ? !entry.isDirectory() : !entry.isFile())
534
+ throw new Error(`Invalid store entry stores/${entry.name}.`);
535
+ }
536
+ const json = join3(stores, "data.json");
537
+ if (await exists3(json)) {
538
+ let value;
539
+ try {
540
+ value = JSON.parse(decoder3.decode(await readFile4(json)));
541
+ } catch (error) {
542
+ throw new Error(`stores/data.json must be valid UTF-8 JSON: ${error instanceof Error ? error.message : String(error)}`);
543
+ }
544
+ if (schema) {
545
+ let validator;
546
+ try {
547
+ validator = compileDataSchema(schema);
548
+ } catch (error) {
549
+ throw new Error(`The data schema is not supported: ${error instanceof Error ? error.message : String(error)}`);
550
+ }
551
+ validator(value);
552
+ }
553
+ }
554
+ const sqlite = join3(stores, "data.sqlite");
555
+ for (const sidecar of ["data.sqlite-wal", "data.sqlite-shm"]) {
556
+ if (await exists3(join3(stores, sidecar)) && !await exists3(sqlite))
557
+ throw new Error(`stores/${sidecar} requires stores/data.sqlite.`);
558
+ }
559
+ if (await exists3(sqlite))
560
+ validateSQLite(sqlite);
561
+ const media = join3(stores, "media");
562
+ if (await exists3(media))
563
+ await validateMedia(media);
564
+ await validateThemeOverride(root, contract);
565
+ }
566
+ function validateSQLite(path) {
567
+ let database;
568
+ try {
569
+ database = new Database(path, { readonly: true, strict: true });
570
+ const rows = database.query("PRAGMA quick_check").all();
571
+ if (rows.length !== 1 || Object.values(rows[0] ?? {})[0] !== "ok")
572
+ throw new Error("PRAGMA quick_check did not return ok");
573
+ } catch (error) {
574
+ throw new Error(`stores/data.sqlite failed its integrity check: ${error instanceof Error ? error.message : String(error)}`);
575
+ } finally {
576
+ database?.close();
577
+ }
578
+ }
579
+ async function validateMedia(directory) {
580
+ const entries = await readdir2(directory, { withFileTypes: true });
581
+ for (const entry of entries) {
582
+ if (!entry.isFile() || entry.isSymbolicLink())
583
+ throw new Error(`Named media must be regular files: stores/media/${entry.name}.`);
584
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(entry.name))
585
+ throw new Error(`Invalid named media key: ${entry.name}.`);
586
+ const bytes = await readFile4(join3(directory, entry.name));
587
+ if (!isImage(bytes) && !isBoundedZip(bytes))
588
+ throw new Error(`Unsupported named media content: ${entry.name}.`);
589
+ }
590
+ }
591
+ function isImage(bytes) {
592
+ if (bytes.byteLength > 25 * MiB)
593
+ return false;
594
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
595
+ try {
596
+ decodePng2(bytes, { checkCrc: true });
597
+ return true;
598
+ } catch {
599
+ return false;
600
+ }
601
+ }
602
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes.at(-2) === 255 && bytes.at(-1) === 217)
603
+ return true;
604
+ if (String.fromCharCode(...bytes.subarray(0, 6)).match(/^GIF8[79]a$/))
605
+ return true;
606
+ return String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP";
607
+ }
608
+ function isBoundedZip(bytes) {
609
+ if (bytes.byteLength > 10 * MiB || bytes[0] !== 80 || bytes[1] !== 75)
610
+ return false;
312
611
  try {
313
- await stat3(path);
612
+ const files = unzipSync(bytes);
613
+ const names = Object.keys(files);
614
+ if (!names.length || names.length > 256)
615
+ return false;
616
+ let total = 0;
617
+ for (const [name, value] of Object.entries(files)) {
618
+ if (!safeArchivePath(name) || value.byteLength > 25 * MiB)
619
+ return false;
620
+ total += value.byteLength;
621
+ if (total > 50 * MiB)
622
+ return false;
623
+ }
314
624
  return true;
315
625
  } catch {
316
626
  return false;
317
627
  }
318
- };
319
- var sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
628
+ }
629
+ async function validateQuickLook(root, required) {
630
+ const directory = join3(root, "QuickLook");
631
+ if (!await exists3(directory)) {
632
+ if (required)
633
+ throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
634
+ return;
635
+ }
636
+ const info = await lstat2(directory);
637
+ if (!info.isDirectory() || info.isSymbolicLink())
638
+ throw new Error("QuickLook must be a directory.");
639
+ const allowed = new Set(["Preview.png", "Icon.png"]);
640
+ for (const entry of await readdir2(directory))
641
+ if (!allowed.has(entry))
642
+ throw new Error(`Unexpected QuickLook entry QuickLook/${entry}.`);
643
+ const preview = join3(directory, "Preview.png");
644
+ const icon = join3(directory, "Icon.png");
645
+ if (required && (!await exists3(preview) || !await exists3(icon)))
646
+ throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
647
+ if (await exists3(preview))
648
+ validateStaticPng(await readFile4(preview), "QuickLook/Preview.png");
649
+ if (await exists3(icon))
650
+ validateIconPng(await readFile4(icon));
651
+ }
652
+ async function requireUTF8File(path, label) {
653
+ const info = await lstat2(path).catch(() => {
654
+ return;
655
+ });
656
+ if (!info?.isFile() || info.isSymbolicLink())
657
+ throw new Error(`${label} must be a regular file.`);
658
+ try {
659
+ decoder3.decode(await readFile4(path));
660
+ } catch {
661
+ throw new Error(`${label} must be UTF-8.`);
662
+ }
663
+ }
664
+ async function rejectSymlinks(root) {
665
+ for (const path of await walk(root))
666
+ if ((await lstat2(path)).isSymbolicLink())
667
+ throw new Error(`Runtime packages cannot contain symlinks: ${relative(root, path)}.`);
668
+ }
669
+ async function walk(root, directory = root) {
670
+ const result = [];
671
+ for (const entry of await readdir2(directory, { withFileTypes: true })) {
672
+ const path = join3(directory, entry.name);
673
+ result.push(path);
674
+ if (entry.isDirectory() && !entry.isSymbolicLink())
675
+ result.push(...await walk(root, path));
676
+ }
677
+ return result.sort((left, right) => relative(root, left).split(sep).join("/").localeCompare(relative(root, right).split(sep).join("/")));
678
+ }
679
+ var decoder3, exists3 = async (path) => stat3(path).then(() => true).catch(() => false), MiB, safeArchivePath = (path) => Boolean(path) && !path.startsWith("/") && !path.includes("\\") && !path.includes("\x00") && path.replace(/\/$/, "").split("/").every((part) => part && part !== "." && part !== "..");
680
+ var init_runtime_package = __esm(() => {
681
+ init_document_skill();
682
+ init_project();
683
+ init_static_preview();
684
+ init_theme();
685
+ decoder3 = new TextDecoder("utf-8", { fatal: true });
686
+ MiB = 1024 * 1024;
687
+ });
688
+
689
+ // src/project.ts
690
+ import { cp, lstat as lstat3, mkdir as mkdir2, mkdtemp, readFile as readFile5, readdir as readdir3, rename, rm, stat as stat4, writeFile as writeFile3 } from "fs/promises";
691
+ import { basename as basename2, dirname as dirname3, join as join4, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
692
+ import { fileURLToPath as fileURLToPath2 } from "url";
693
+ import { createHash } from "crypto";
694
+ import { zipSync } from "fflate";
695
+ import { decode as decodePng3 } from "fast-png";
696
+ import { build as viteBuild } from "vite";
697
+ import { checkPngDimensions as checkPngDimensions2, manifestSchemaURL, parseManifest } from "@hitslop/schema";
698
+ async function loadManifest(root) {
699
+ const manifest = parseManifest(JSON.parse(await readFile5(manifestPath(root), "utf8")));
700
+ await validateSkin(root, manifest);
701
+ return manifest;
702
+ }
320
703
  async function validateSkin(root, manifest) {
321
704
  if (!("skin" in manifest.presentation))
322
705
  return;
323
- const path = join3(root, manifest.presentation.skin);
324
- if (!await exists3(path))
706
+ const path = join4(root, manifest.presentation.skin);
707
+ if (!await exists4(path))
325
708
  throw new Error(`Missing window skin: ${manifest.presentation.skin}`);
326
- const info = await lstat2(path);
709
+ const info = await lstat3(path);
327
710
  if (!info.isFile() || info.isSymbolicLink())
328
711
  throw new Error("The window skin must be a regular file, not a symlink.");
329
712
  let png;
713
+ const bytes = await readFile5(path);
714
+ checkPngDimensions2(bytes, "The window skin", manifest.presentation);
330
715
  try {
331
- png = decodePng(await readFile3(path), { checkCrc: true });
716
+ png = decodePng3(bytes, { checkCrc: true });
332
717
  } catch {
333
718
  throw new Error("The window skin must be a valid PNG image.");
334
719
  }
335
- if (png.width !== manifest.presentation.width || png.height !== manifest.presentation.height) {
336
- throw new Error(`The window skin must be exactly ${manifest.presentation.width}x${manifest.presentation.height} pixels; received ${png.width}x${png.height}.`);
337
- }
338
720
  if (png.channels !== 4)
339
721
  throw new Error("The window skin must be an RGBA PNG image.");
340
722
  }
@@ -342,26 +724,27 @@ async function scaffold(destination, options) {
342
724
  const template = options.template ?? "svelte-counter";
343
725
  if (!new Set(["svelte", "svelte-counter"]).has(template))
344
726
  throw new Error(`Unknown authoring template: ${template}`);
345
- if (await exists3(destination) && (await readdir2(destination)).length)
727
+ if (await exists4(destination) && (await readdir3(destination)).length)
346
728
  throw new Error(`Destination is not empty: ${destination}`);
347
- const slug = basename(destination).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "my-slop";
729
+ const slug = basename2(destination).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "my-slop";
348
730
  const defaultTitle = slug.split("-").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
349
731
  const title = options.title || defaultTitle;
350
- const templateRoot = fileURLToPath(new URL("../templates/svelte", import.meta.url));
732
+ const templateRoot = fileURLToPath2(new URL("../templates/svelte", import.meta.url));
351
733
  await cp(templateRoot, destination, { recursive: true });
352
- await writeFile2(join3(destination, "package.json"), JSON.stringify({
734
+ await writeFile3(join4(destination, "package.json"), JSON.stringify({
353
735
  name: slug,
354
736
  private: true,
355
737
  type: "module",
356
738
  scripts: {
357
739
  dev: "slop dev",
740
+ check: "svelte-check --tsgo --tsconfig tsconfig.json",
358
741
  validate: "slop validate",
359
742
  build: "slop build",
360
743
  register: "slop register",
361
744
  publish: "slop publish"
362
745
  },
363
- dependencies: { "@hitslop/runtime": "^0.1.2", "@hitslop/svelte": "^0.1.2", "bits-ui": "^2.19.0", svelte: "^5.0.0", zod: "^4.5.2" },
364
- devDependencies: { "@hitslop/cli": `^${package_default.version}`, "@sveltejs/vite-plugin-svelte": "^7.0.0", "@vanilla-extract/css": "^1.17.4", "@vanilla-extract/vite-plugin": "^5.1.1", vite: "^8.0.0" }
746
+ dependencies: { "@hitslop/runtime": "^0.3.0", "@hitslop/svelte": "^0.3.0", "bits-ui": "^2.19.0", svelte: "^5.57.0", typebox: "^1.3.26" },
747
+ devDependencies: { "@hitslop/cli": `^${package_default.version}`, "@sveltejs/vite-plugin-svelte": "^7.3.0", "@vanilla-extract/css": "^1.21.2", "@vanilla-extract/vite-plugin": "^5.2.6", vite: "^8.2.2", "svelte-check": "^4.7.6", "@typescript/native": "npm:typescript@^7.0.2", typescript: "^6.0.3" }
365
748
  }, null, 2) + `
366
749
  `);
367
750
  const manifest = parseManifest({
@@ -373,74 +756,89 @@ async function scaffold(destination, options) {
373
756
  categories: options.categories?.length ? options.categories : ["utilities"],
374
757
  presentation: { width: 560, height: 420 }
375
758
  });
376
- await writeFile2(join3(destination, "manifest.json"), JSON.stringify(manifest, null, 2) + `
759
+ await writeFile3(join4(destination, "manifest.json"), JSON.stringify(manifest, null, 2) + `
377
760
  `);
378
- const appPath = join3(destination, "src/App.svelte");
379
- await writeFile2(appPath, (await readFile3(appPath, "utf8")).replace("__SLOP_TITLE_LITERAL__", JSON.stringify(title)));
761
+ const appPath = join4(destination, "src/App.svelte");
762
+ await writeFile3(appPath, (await readFile5(appPath, "utf8")).replace("__SLOP_TITLE_LITERAL__", JSON.stringify(title)));
380
763
  }
381
764
  async function buildSlop(root) {
382
765
  const manifest = await loadManifest(root);
383
- const dataSchema = await generateDataSchema(root);
766
+ const schemaPath = join4(root, "schema.ts");
767
+ const dataSchema = await exists4(schemaPath) ? await loadDataSchema(schemaPath) : undefined;
384
768
  await readDocumentGuideSource(root);
385
- const distRoot = join3(root, "dist");
386
- const output = join3(distRoot, `${manifest.slug}.slop`);
387
- const viteOut = join3(root, ".hitslop", "vite-build");
388
- const themeContract = await readThemeContract(root);
389
- const hasTheme = themeContract !== undefined;
390
- await rm(output, { recursive: true, force: true });
391
- await rm(viteOut, { recursive: true, force: true });
392
- await mkdir2(output, { recursive: true });
393
- await viteBuild({ root, plugins: [hitSlopBuildPlugin({ hasTheme })], build: { outDir: viteOut, emptyOutDir: true } });
394
- const html = join3(viteOut, "index.html");
395
- if (!await exists3(html))
396
- throw new Error("Vite did not produce index.html");
397
- validateThemeReferences(await readFile3(html, "utf8"), themeContract, "generated app.html");
398
- await cp(html, join3(output, "app.html"));
399
- await writeFile2(join3(output, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
769
+ const distRoot = join4(root, "dist");
770
+ await mkdir2(distRoot, { recursive: true });
771
+ const staging = await mkdtemp(join4(distRoot, ".build-"));
772
+ const output = join4(staging, `${manifest.slug}.slop`);
773
+ const destination = join4(distRoot, `${manifest.slug}.slop`);
774
+ const viteOut = join4(staging, "vite");
775
+ try {
776
+ const themeCSS = await readThemeCSS(root);
777
+ const contract = themeContract(themeCSS);
778
+ const hasTheme = contract !== undefined;
779
+ await mkdir2(output, { recursive: true });
780
+ await viteBuild({ root, plugins: [hitSlopBuildPlugin({ hasTheme })], build: { outDir: viteOut, emptyOutDir: true } });
781
+ const html = join4(viteOut, "index.html");
782
+ if (!await exists4(html))
783
+ throw new Error("Vite did not produce index.html");
784
+ validateThemeReferences(await readFile5(html, "utf8"), contract, "generated app.html");
785
+ await cp(html, join4(output, "app.html"));
786
+ for (const entry of await readdir3(viteOut)) {
787
+ if (entry === "index.html")
788
+ continue;
789
+ if (entry !== "assets")
790
+ throw new Error(`Unsupported Vite output ${entry}; emit runtime resources under assets/.`);
791
+ await copyRuntimeTree(join4(viteOut, entry), join4(output, entry));
792
+ }
793
+ await writeFile3(join4(output, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
400
794
  `);
401
- for (const name of ["assets"]) {
402
- const source = join3(root, name);
403
- if (await exists3(source))
404
- await copyRuntimeTree(source, join3(output, name));
405
- }
406
- if (dataSchema)
407
- await writeFile2(join3(output, "data.schema.json"), `${JSON.stringify(dataSchema, null, 2)}
795
+ for (const name of ["assets"]) {
796
+ const source = join4(root, name);
797
+ if (await exists4(source))
798
+ await copyRuntimeTree(source, join4(output, name));
799
+ }
800
+ if (themeCSS !== undefined) {
801
+ await mkdir2(join4(output, "assets"), { recursive: true });
802
+ await writeFile3(join4(output, "assets/theme.css"), themeCSS);
803
+ }
804
+ if (dataSchema)
805
+ await writeFile3(join4(output, "data.schema.json"), `${JSON.stringify(dataSchema, null, 2)}
408
806
  `);
409
- await emitDocumentSkill(root, output);
410
- await rm(viteOut, { recursive: true, force: true });
411
- return { directory: output, manifest };
412
- }
413
- async function generateDataSchema(root) {
414
- const source = join3(root, "schema.ts");
415
- if (!await exists3(source))
416
- return;
417
- const imported = await import(`${pathToFileURL(source).href}?hitslop=${Date.now()}`);
418
- if (!imported.default)
419
- throw new Error("schema.ts must default-export a Zod 4 schema");
420
- let schema;
421
- try {
422
- schema = z.toJSONSchema(imported.default, { target: "draft-2020-12", io: "input" });
423
- } catch (error) {
424
- throw new Error(`Could not generate data.schema.json from schema.ts: ${error instanceof Error ? error.message : String(error)}`);
807
+ await emitDocumentSkill(root, output);
808
+ const { validateRuntimePackage: validateRuntimePackage2 } = await Promise.resolve().then(() => (init_runtime_package(), exports_runtime_package));
809
+ await validateRuntimePackage2(output, { template: true });
810
+ const previous = join4(staging, "previous.slop");
811
+ if (await exists4(destination))
812
+ await rename(destination, previous);
813
+ try {
814
+ await rename(output, destination);
815
+ } catch (error) {
816
+ if (await exists4(previous))
817
+ await rename(previous, destination);
818
+ throw error;
819
+ }
820
+ return { directory: destination, manifest };
821
+ } finally {
822
+ await rm(staging, { recursive: true, force: true });
425
823
  }
426
- return schema;
427
824
  }
428
825
  async function validateAuthoringProject(root) {
429
826
  const manifest = await loadManifest(root);
430
827
  await readThemeContract(root);
431
- await generateDataSchema(root);
828
+ if (await exists4(join4(root, "schema.ts")))
829
+ await loadDataSchema(join4(root, "schema.ts"));
432
830
  await readDocumentGuideSource(root);
433
831
  return manifest;
434
832
  }
435
- async function walk(root, directory = root) {
833
+ async function walk2(root, directory = root) {
436
834
  const result = [];
437
- for (const entry of await readdir2(directory, { withFileTypes: true })) {
438
- const absolute = join3(directory, entry.name);
439
- const rel = relative(root, absolute).split(sep).join("/");
835
+ for (const entry of await readdir3(directory, { withFileTypes: true })) {
836
+ const absolute = join4(directory, entry.name);
837
+ const rel = relative2(root, absolute).split(sep2).join("/");
440
838
  if (entry.isSymbolicLink())
441
839
  throw new Error(`Symlinks are not allowed in artifacts: ${rel}`);
442
840
  if (entry.isDirectory())
443
- result.push(...await walk(root, absolute));
841
+ result.push(...await walk2(root, absolute));
444
842
  else if (entry.isFile())
445
843
  result.push(rel);
446
844
  }
@@ -449,33 +847,73 @@ async function walk(root, directory = root) {
449
847
  async function packSlop(directory) {
450
848
  const files = {};
451
849
  const epoch = new Date(2000, 0, 1, 0, 0, 0);
452
- for (const path of await walk(directory))
453
- files[path] = [new Uint8Array(await Bun.file(join3(directory, path)).arrayBuffer()), { mtime: epoch, level: 9 }];
850
+ for (const path of await walk2(directory))
851
+ files[path] = [new Uint8Array(await Bun.file(join4(directory, path)).arrayBuffer()), { mtime: epoch, level: 9 }];
454
852
  const bytes = zipSync(files, { level: 9 });
455
853
  return { bytes, sha256: sha256(bytes) };
456
854
  }
457
855
  async function copyRuntimeTree(source, destination) {
458
- if ((await lstat2(source)).isSymbolicLink())
856
+ if ((await lstat3(source)).isSymbolicLink())
459
857
  throw new Error(`Symlinks are not allowed in artifacts: ${source}`);
460
- const info = await stat3(source);
858
+ const info = await stat4(source);
461
859
  if (info.isDirectory()) {
462
860
  await mkdir2(destination, { recursive: true });
463
- for (const entry of await readdir2(source))
464
- await copyRuntimeTree(join3(source, entry), join3(destination, entry));
861
+ for (const entry of await readdir3(source))
862
+ await copyRuntimeTree(join4(source, entry), join4(destination, entry));
863
+ return;
864
+ }
865
+ await mkdir2(dirname3(destination), { recursive: true });
866
+ if (await exists4(destination)) {
867
+ if (!(await readFile5(source)).equals(await readFile5(destination)))
868
+ throw new Error(`Runtime asset collision: ${destination}`);
465
869
  return;
466
870
  }
467
- await mkdir2(dirname2(destination), { recursive: true });
468
871
  await cp(source, destination);
469
872
  }
873
+ var manifestPath = (root) => join4(root, "manifest.json"), exists4 = async (path) => {
874
+ try {
875
+ await stat4(path);
876
+ return true;
877
+ } catch {
878
+ return false;
879
+ }
880
+ }, sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
881
+ var init_project = __esm(() => {
882
+ init_package();
883
+ init_document_skill();
884
+ init_theme();
885
+ init_vite_build_plugin();
886
+ init_data_schema();
887
+ });
888
+
889
+ // src/cli.ts
890
+ init_project();
891
+ import { Crust } from "@crustjs/core";
892
+ import { join as join10 } from "path";
893
+ import { stat as stat6 } from "fs/promises";
894
+ import { createInterface } from "readline/promises";
895
+ import { validate, SlopCategorySchema } from "@hitslop/schema";
470
896
 
471
897
  // src/dev.ts
472
- import { stat as stat4 } from "fs/promises";
473
- import { join as join4, resolve as resolve2 } from "path";
898
+ import { join as join5, resolve as resolve3 } from "path";
474
899
  import { createServer } from "vite";
475
900
 
476
901
  // src/dev-bridge.ts
902
+ import { protocolVersion } from "@hitslop/schema/bridge";
477
903
  var hostStyle = `<style data-hitslop-host>*{scrollbar-width:none!important}*::-webkit-scrollbar{width:0!important;height:0!important;display:none!important}</style>`;
478
904
  var devHostJavaScript = `(() => {
905
+ const captureMode = new URL(window.location?.href ?? 'http://localhost/').searchParams.get('capture');
906
+ if (captureMode === 'icon') document.documentElement.dataset.slopRenderer = 'true';
907
+ if (captureMode === 'icon' || captureMode === 'export') {
908
+ window.addEventListener('slop:ready', () => {
909
+ setTimeout(async () => {
910
+ try {
911
+ if (captureMode === 'icon') document.documentElement.style.width = '512px';
912
+ await window.__hitslopCapture.begin('dev-preview', captureMode, {blockInteraction:false});
913
+ } catch (error) { console.error('Capture preview failed', error); }
914
+ }, 0);
915
+ }, {once:true});
916
+ }
479
917
  const listeners = { json: new Set(), sqlite: new Set(), media: new Set() };
480
918
  let jsonValue;
481
919
  let jsonRevision = 0;
@@ -494,6 +932,8 @@ var devHostJavaScript = `(() => {
494
932
  const drag = async () => undefined;
495
933
 
496
934
  window.slop = Object.freeze({
935
+ info: async () => ({ protocolVersion: ${protocolVersion}, capabilities: ['host.info', 'json.open', 'json.read', 'json.write', 'window.resize', 'window.drag'] }),
936
+ flush: async () => undefined,
497
937
  json: Object.freeze({
498
938
  open: async (value) => {
499
939
  if (!jsonOpened) { jsonValue = clone(value); jsonOpened = true; }
@@ -504,7 +944,7 @@ var devHostJavaScript = `(() => {
504
944
  return { value: clone(jsonValue), revision: revision() };
505
945
  },
506
946
  write: async (value, expectedRevision) => {
507
- if (expectedRevision && expectedRevision !== revision()) throw new Error('revision_conflict');
947
+ if (expectedRevision !== undefined && expectedRevision !== revision()) throw Object.assign(new Error('Document changed'), { code: 'revision_conflict' });
508
948
  jsonValue = clone(value); jsonOpened = true; jsonRevision += 1;
509
949
  const result = { revision: revision() };
510
950
  emit('json', { source: 'app', revision: result.revision });
@@ -547,14 +987,8 @@ var injectHost = (html, options = {}) => {
547
987
  };
548
988
 
549
989
  // src/dev.ts
550
- var exists4 = async (path) => {
551
- try {
552
- await stat4(path);
553
- return true;
554
- } catch {
555
- return false;
556
- }
557
- };
990
+ init_project();
991
+ init_theme();
558
992
  function mockHostPlugin(options = {}) {
559
993
  return {
560
994
  name: "hitslop-browser-preview",
@@ -566,8 +1000,8 @@ function mockHostPlugin(options = {}) {
566
1000
  }
567
1001
  async function runDev(root) {
568
1002
  await loadManifest(root);
569
- const themeHref = await exists4(join4(root, "assets", "theme.css")) ? "/assets/theme.css" : undefined;
570
- const server = await createServer({ root, plugins: [mockHostPlugin(themeHref ? { themeHref } : {})] });
1003
+ const themeHref = await readThemeCSS(root) !== undefined ? "/assets/theme.css" : undefined;
1004
+ const server = await createServer({ root, plugins: [themePreviewPlugin(root), mockHostPlugin(themeHref ? { themeHref } : {})] });
571
1005
  await server.listen();
572
1006
  const url = server.resolvedUrls?.local[0];
573
1007
  if (!url)
@@ -576,42 +1010,46 @@ async function runDev(root) {
576
1010
  if (process.platform === "darwin")
577
1011
  Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
578
1012
  }
579
- async function runNative(arguments_) {
580
- const override = process.env.HITSLOP_NATIVE_CLI;
581
- const arch = process.arch === "arm64" ? "arm64-apple-macosx" : "x86_64-apple-macosx";
582
- const repoDebugBin = resolve2(import.meta.dir, `../../../apps/apple/Packages/HitSlopApple/.build/${arch}/debug/hitslop-native`);
583
- const repoReleaseBin = resolve2(import.meta.dir, `../../../apps/apple/Packages/HitSlopApple/.build/${arch}/release/hitslop-native`);
584
- const candidates = [override, "/Applications/hitSlop.app/Contents/Helpers/hitslop-native", `${process.env.HOME}/Applications/hitSlop.app/Contents/Helpers/hitslop-native`, repoDebugBin, repoReleaseBin, "hitslop-native"].filter(Boolean);
585
- for (const executable of candidates) {
586
- try {
587
- const processResult = Bun.spawn([executable, ...arguments_], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
588
- const status = await processResult.exited;
589
- if (status === 0)
590
- return;
591
- if (executable === candidates.at(-1))
592
- throw new Error(`hitslop-native failed with status ${status}`);
593
- } catch (error) {
594
- if (executable === candidates.at(-1))
595
- throw error;
1013
+ function themePreviewPlugin(root) {
1014
+ root = resolve3(root);
1015
+ return {
1016
+ name: "hitslop:theme-preview",
1017
+ configureServer(server) {
1018
+ server.middlewares.use(async (request, response, next) => {
1019
+ if (request.url?.split("?")[0] !== "/assets/theme.css")
1020
+ return next();
1021
+ try {
1022
+ const css = await readThemeCSS(root);
1023
+ if (css === undefined)
1024
+ return next();
1025
+ response.setHeader("Content-Type", "text/css; charset=utf-8");
1026
+ response.end(css);
1027
+ } catch (error) {
1028
+ next(error);
1029
+ }
1030
+ });
1031
+ },
1032
+ handleHotUpdate({ file, server }) {
1033
+ if (file === join5(root, "theme.ts"))
1034
+ server.ws.send({ type: "full-reload" });
596
1035
  }
597
- }
598
- throw new Error("Install hitSlop or set HITSLOP_NATIVE_CLI to use native rendering.");
1036
+ };
599
1037
  }
600
1038
 
601
1039
  // src/publish.ts
602
- import { join as join8, resolve as resolve5 } from "path";
603
- import { cp as cp3, mkdir as mkdir5, rm as rm3 } from "fs/promises";
1040
+ init_project();
604
1041
  import { canonicalPublishEnvelope } from "@hitslop/schema";
605
1042
 
606
1043
  // src/identity.ts
1044
+ init_project();
607
1045
  import { getPublicKeyAsync, keygenAsync, signAsync } from "@noble/ed25519";
608
1046
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
609
- import { chmod, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
610
- import { join as join5, resolve as resolve3 } from "path";
1047
+ import { chmod, mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
1048
+ import { join as join6, resolve as resolve4 } from "path";
611
1049
  import { homedir } from "os";
612
1050
  var service = "app.hitslop.cli";
613
1051
  var account = "publisher";
614
- var fallback = join5(homedir(), ".hitslop", "identity.json");
1052
+ var fallback = join6(homedir(), ".hitslop", "identity.json");
615
1053
  var encode = (bytes) => Buffer.from(bytes).toString("base64url");
616
1054
  var decode = (value) => new Uint8Array(Buffer.from(value, "base64url"));
617
1055
  async function keychainRead() {
@@ -627,14 +1065,14 @@ async function keychainWrite(value) {
627
1065
  return result.exitCode === 0;
628
1066
  }
629
1067
  async function readStored() {
630
- const value = await keychainRead() ?? await readFile4(fallback, "utf8").catch(() => null);
1068
+ const value = await keychainRead() ?? await readFile6(fallback, "utf8").catch(() => null);
631
1069
  return value ? JSON.parse(value) : null;
632
1070
  }
633
1071
  async function storeIdentity(identity) {
634
1072
  const json = JSON.stringify(identity);
635
1073
  if (!await keychainWrite(json)) {
636
- await mkdir3(join5(homedir(), ".hitslop"), { recursive: true });
637
- await writeFile3(fallback, json, { mode: 384 });
1074
+ await mkdir3(join6(homedir(), ".hitslop"), { recursive: true });
1075
+ await writeFile4(fallback, json, { mode: 384 });
638
1076
  await chmod(fallback, 384);
639
1077
  }
640
1078
  }
@@ -666,15 +1104,15 @@ async function exportIdentity(path, passphrase) {
666
1104
  const cipher = createCipheriv("aes-256-gcm", key, iv);
667
1105
  const ciphertext = Buffer.concat([cipher.update(JSON.stringify(identity), "utf8"), cipher.final()]);
668
1106
  const payload = { version: 1, kdf: "scrypt", salt: encode(salt), cipher: "aes-256-gcm", iv: encode(iv), tag: encode(cipher.getAuthTag()), ciphertext: encode(ciphertext) };
669
- const output = resolve3(path);
670
- await writeFile3(output, `${JSON.stringify(payload, null, 2)}
1107
+ const output = resolve4(path);
1108
+ await writeFile4(output, `${JSON.stringify(payload, null, 2)}
671
1109
  `, { mode: 384 });
672
1110
  await chmod(output, 384);
673
1111
  }
674
1112
  async function importIdentity(path, passphrase, force = false) {
675
1113
  if (await readStored() && !force)
676
1114
  throw new Error("A publisher identity already exists. Pass --force to replace it.");
677
- const payload = JSON.parse(await readFile4(resolve3(path), "utf8"));
1115
+ const payload = JSON.parse(await readFile6(resolve4(path), "utf8"));
678
1116
  if (payload.version !== 1 || payload.kdf !== "scrypt" || payload.cipher !== "aes-256-gcm")
679
1117
  throw new Error("Unsupported identity export format.");
680
1118
  const key = scryptSync(passphrase, decode(payload.salt), 32, { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
@@ -692,390 +1130,161 @@ async function importIdentity(path, passphrase, force = false) {
692
1130
  }
693
1131
  var sign = async (message, identity) => encode(await signAsync(message, decode(identity.privateKey)));
694
1132
 
695
- // src/install.ts
696
- import { chmod as chmod2, cp as cp2, lstat as lstat4, mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename, rm as rm2, stat as stat6 } from "fs/promises";
697
- import { randomUUID } from "crypto";
698
- import { homedir as homedir2 } from "os";
699
- import { join as join7, resolve as resolve4 } from "path";
700
-
701
- // src/runtime-package.ts
702
- import { Database } from "bun:sqlite";
703
- import { lstat as lstat3, readFile as readFile6, readdir as readdir3, stat as stat5 } from "fs/promises";
704
- import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
705
- import { unzipSync } from "fflate";
706
- import { decode as decodePng3 } from "fast-png";
707
- import * as z2 from "zod";
708
-
709
- // src/static-preview.ts
710
- import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
711
- import { decode as decodePng2, encode as encodePng } from "fast-png";
712
- var MAX_PREVIEW_BYTES = 5 * 1024 * 1024;
713
- var ICON_SIZE = 512;
714
- function validateStaticPng(bytes, label) {
715
- if (bytes.byteLength === 0)
716
- throw new Error(`${label} is empty.`);
717
- if (bytes.byteLength > MAX_PREVIEW_BYTES)
718
- throw new Error(`${label} cannot exceed 5 MiB.`);
719
- try {
720
- return decodePng2(bytes, { checkCrc: true });
721
- } catch {
722
- throw new Error(`${label} must be a valid PNG image.`);
723
- }
724
- }
725
- function validateIconPng(bytes, label = "The template icon") {
726
- const image = validateStaticPng(bytes, label);
727
- if (image.width !== ICON_SIZE || image.height !== ICON_SIZE)
728
- throw new Error(`${label} must be exactly ${ICON_SIZE}x${ICON_SIZE} pixels.`);
729
- return image;
730
- }
731
- function iconFromPng(bytes, size = ICON_SIZE) {
732
- const image = validateStaticPng(bytes, "The template preview");
733
- const scale = size / Math.max(image.width, image.height);
734
- const width = Math.max(1, Math.round(image.width * scale));
735
- const height = Math.max(1, Math.round(image.height * scale));
736
- const output = image.depth === 16 ? new Uint16Array(size * size * 4) : new Uint8Array(size * size * 4);
737
- const alpha = image.depth === 16 ? 65535 : 255;
738
- const offsetX = Math.floor((size - width) / 2);
739
- const offsetY = Math.floor((size - height) / 2);
740
- for (let y = 0;y < height; y += 1) {
741
- const sourceY = Math.min(image.height - 1, Math.floor((y + 0.5) * image.height / height));
742
- for (let x = 0;x < width; x += 1) {
743
- const sourceX = Math.min(image.width - 1, Math.floor((x + 0.5) * image.width / width));
744
- const sourceOffset = (sourceY * image.width + sourceX) * image.channels;
745
- const destinationOffset = ((offsetY + y) * size + offsetX + x) * 4;
746
- if (image.channels === 1 || image.channels === 2) {
747
- const gray = image.data[sourceOffset];
748
- output[destinationOffset] = gray;
749
- output[destinationOffset + 1] = gray;
750
- output[destinationOffset + 2] = gray;
751
- output[destinationOffset + 3] = image.channels === 2 ? image.data[sourceOffset + 1] : alpha;
752
- } else {
753
- output[destinationOffset] = image.data[sourceOffset];
754
- output[destinationOffset + 1] = image.data[sourceOffset + 1];
755
- output[destinationOffset + 2] = image.data[sourceOffset + 2];
756
- output[destinationOffset + 3] = image.channels === 4 ? image.data[sourceOffset + 3] : alpha;
757
- }
758
- }
759
- }
760
- return encodePng({ width: size, height: size, data: output, depth: image.depth, channels: 4 });
761
- }
762
- async function writeDefaultIcon(previewPath, iconPath) {
763
- await writeFile4(iconPath, iconFromPng(await readFile5(previewPath)));
764
- }
1133
+ // src/template-capture.ts
1134
+ import { cp as cp2, mkdir as mkdir4, readFile as readFile7, rm as rm2 } from "fs/promises";
1135
+ import { join as join8, resolve as resolve6 } from "path";
765
1136
 
766
- // src/runtime-package.ts
767
- var decoder3 = new TextDecoder("utf-8", { fatal: true });
768
- var exists5 = async (path) => stat5(path).then(() => true).catch(() => false);
769
- var MiB = 1024 * 1024;
770
- async function validateRuntimePackage(root, options = {}) {
771
- if (basename2(root).startsWith("."))
772
- throw new Error("Runtime package path is not valid.");
773
- const rootInfo = await lstat3(root).catch(() => {
774
- return;
775
- });
776
- if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink())
777
- throw new Error("A runtime .slop must be a directory, not a symlink.");
778
- const manifest = await loadManifest(root);
779
- await requireUTF8File(join6(root, "app.html"), "app.html");
780
- await rejectSymlinks(root);
781
- const allowed = new Set(["manifest.json", "app.html", "data.schema.json", "assets", "stores", "QuickLook", ".agents", "Icon\r"]);
782
- for (const entry of await readdir3(root))
783
- if (!allowed.has(entry))
784
- throw new Error(`Runtime packages cannot contain ${entry}.`);
785
- const forbidden = new Set(["package.json", "bun.lock", "bun.lockb", "node_modules", "source", "src", "build", "document.json", ".build", ".hitslop", "style.css"]);
786
- for (const path of await walk2(root)) {
787
- if (forbidden.has(basename2(path).toLowerCase()))
788
- throw new Error(`Runtime packages cannot contain ${relative2(root, path)}.`);
789
- }
790
- if (options.template && await exists5(join6(root, "stores")))
791
- throw new Error("Template packages cannot contain stores.");
792
- if (options.template && await exists5(join6(root, "Icon\r")))
793
- throw new Error("Template packages cannot contain Finder metadata Icon\\r.");
794
- const schema = await readDataSchema(root);
795
- const contract = await readThemeContract(root);
796
- const appHTML = await readFile6(join6(root, "app.html"), "utf8");
797
- validateThemeReferences(appHTML, contract, "app.html");
798
- await validateStores(root, schema, contract);
799
- await validateQuickLook(root, options.requirePreview ?? false);
800
- await validateDocumentSkill(root);
801
- return manifest;
802
- }
803
- async function readDataSchema(root) {
804
- const current = join6(root, "data.schema.json");
805
- if (!await exists5(current))
806
- return;
807
- await requireUTF8File(current, "data.schema.json");
808
- try {
809
- const value = JSON.parse(await readFile6(current, "utf8"));
810
- if (!value || typeof value !== "object" || Array.isArray(value))
811
- throw new Error("schema root must be an object");
812
- z2.fromJSONSchema(value);
813
- return value;
814
- } catch (error) {
815
- throw new Error(`data.schema.json must contain valid JSON Schema: ${error instanceof Error ? error.message : String(error)}`);
816
- }
1137
+ // src/native.ts
1138
+ import { homedir as homedir2 } from "os";
1139
+ import { join as join7, resolve as resolve5 } from "path";
1140
+ async function runNative(arguments_) {
1141
+ const override = process.env.HITSLOP_NATIVE_CLI;
1142
+ const arch = process.arch === "arm64" ? "arm64-apple-macosx" : "x86_64-apple-macosx";
1143
+ const build = resolve5(import.meta.dir, "../../../apps/apple/Packages/HitSlopApple/.build", arch);
1144
+ const candidates = override !== undefined ? [override] : [
1145
+ "/Applications/hitSlop.app/Contents/Helpers/hitslop-native",
1146
+ join7(homedir2(), "Applications/hitSlop.app/Contents/Helpers/hitslop-native"),
1147
+ join7(build, "debug/hitslop-native"),
1148
+ join7(build, "release/hitslop-native"),
1149
+ "hitslop-native"
1150
+ ];
1151
+ await runNativeCandidates(candidates, arguments_, override !== undefined);
817
1152
  }
818
- async function validateStores(root, schema, contract) {
819
- const stores = join6(root, "stores");
820
- if (!await exists5(stores))
821
- return;
822
- const info = await lstat3(stores);
823
- if (!info.isDirectory() || info.isSymbolicLink())
824
- throw new Error("stores must be a directory.");
825
- const allowed = new Set(["data.json", "data.sqlite", "data.sqlite-wal", "data.sqlite-shm", "media", "theme.css"]);
826
- for (const entry of await readdir3(stores, { withFileTypes: true })) {
827
- if (!allowed.has(entry.name))
828
- throw new Error(`Unexpected store entry stores/${entry.name}.`);
829
- if (entry.name === "media" ? !entry.isDirectory() : !entry.isFile())
830
- throw new Error(`Invalid store entry stores/${entry.name}.`);
831
- }
832
- const json = join6(stores, "data.json");
833
- if (await exists5(json)) {
834
- let value;
1153
+ async function runNativeCandidates(candidates, arguments_, explicit = false) {
1154
+ for (const executable of candidates) {
1155
+ let child;
835
1156
  try {
836
- value = JSON.parse(decoder3.decode(await readFile6(json)));
1157
+ child = Bun.spawn([executable, ...arguments_], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
837
1158
  } catch (error) {
838
- throw new Error(`stores/data.json must be valid UTF-8 JSON: ${error instanceof Error ? error.message : String(error)}`);
839
- }
840
- if (schema) {
841
- let validator;
842
- try {
843
- validator = z2.fromJSONSchema(schema);
844
- } catch (error) {
845
- throw new Error(`The data schema is not supported: ${error instanceof Error ? error.message : String(error)}`);
846
- }
847
- const result = validator.safeParse(value);
848
- if (!result.success)
849
- throw new Error(`stores/data.json does not match the data schema: ${z2.prettifyError(result.error)}`);
1159
+ if (!explicit && error && typeof error === "object" && "code" in error && error.code === "ENOENT")
1160
+ continue;
1161
+ throw new Error(`Could not launch native helper ${executable}`, { cause: error });
850
1162
  }
851
- }
852
- const sqlite = join6(stores, "data.sqlite");
853
- for (const sidecar of ["data.sqlite-wal", "data.sqlite-shm"]) {
854
- if (await exists5(join6(stores, sidecar)) && !await exists5(sqlite))
855
- throw new Error(`stores/${sidecar} requires stores/data.sqlite.`);
856
- }
857
- if (await exists5(sqlite))
858
- validateSQLite(sqlite);
859
- const media = join6(stores, "media");
860
- if (await exists5(media))
861
- await validateMedia(media);
862
- await validateThemeOverride(root, contract);
863
- }
864
- function validateSQLite(path) {
865
- let database;
866
- try {
867
- database = new Database(path, { readonly: true, strict: true });
868
- const rows = database.query("PRAGMA quick_check").all();
869
- if (rows.length !== 1 || Object.values(rows[0] ?? {})[0] !== "ok")
870
- throw new Error("PRAGMA quick_check did not return ok");
871
- } catch (error) {
872
- throw new Error(`stores/data.sqlite failed its integrity check: ${error instanceof Error ? error.message : String(error)}`);
873
- } finally {
874
- database?.close();
875
- }
876
- }
877
- async function validateMedia(directory) {
878
- const entries = await readdir3(directory, { withFileTypes: true });
879
- for (const entry of entries) {
880
- if (!entry.isFile() || entry.isSymbolicLink())
881
- throw new Error(`Named media must be regular files: stores/media/${entry.name}.`);
882
- if (!/^[a-z][a-z0-9-]{0,63}$/.test(entry.name))
883
- throw new Error(`Invalid named media key: ${entry.name}.`);
884
- const bytes = await readFile6(join6(directory, entry.name));
885
- if (!isImage(bytes) && !isBoundedZip(bytes))
886
- throw new Error(`Unsupported named media content: ${entry.name}.`);
887
- }
888
- }
889
- function isImage(bytes) {
890
- if (bytes.byteLength > 25 * MiB)
891
- return false;
892
- if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
893
- try {
894
- decodePng3(bytes, { checkCrc: true });
895
- return true;
896
- } catch {
897
- return false;
898
- }
899
- }
900
- if (bytes[0] === 255 && bytes[1] === 216 && bytes.at(-2) === 255 && bytes.at(-1) === 217)
901
- return true;
902
- if (String.fromCharCode(...bytes.subarray(0, 6)).match(/^GIF8[79]a$/))
903
- return true;
904
- return String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP";
905
- }
906
- function isBoundedZip(bytes) {
907
- if (bytes.byteLength > 10 * MiB || bytes[0] !== 80 || bytes[1] !== 75)
908
- return false;
909
- try {
910
- const files = unzipSync(bytes);
911
- const names = Object.keys(files);
912
- if (!names.length || names.length > 256)
913
- return false;
914
- let total = 0;
915
- for (const [name, value] of Object.entries(files)) {
916
- if (!safeArchivePath(name) || value.byteLength > 25 * MiB)
917
- return false;
918
- total += value.byteLength;
919
- if (total > 50 * MiB)
920
- return false;
921
- }
922
- return true;
923
- } catch {
924
- return false;
925
- }
926
- }
927
- var safeArchivePath = (path) => Boolean(path) && !path.startsWith("/") && !path.includes("\\") && !path.includes("\x00") && path.replace(/\/$/, "").split("/").every((part) => part && part !== "." && part !== "..");
928
- async function validateQuickLook(root, required) {
929
- const directory = join6(root, "QuickLook");
930
- if (!await exists5(directory)) {
931
- if (required)
932
- throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
1163
+ const status = await child.exited;
1164
+ if (status !== 0)
1165
+ throw new Error(`Native helper ${executable} failed with status ${status}`);
933
1166
  return;
934
1167
  }
935
- const info = await lstat3(directory);
936
- if (!info.isDirectory() || info.isSymbolicLink())
937
- throw new Error("QuickLook must be a directory.");
938
- const allowed = new Set(["Preview.png", "Icon.png"]);
939
- for (const entry of await readdir3(directory))
940
- if (!allowed.has(entry))
941
- throw new Error(`Unexpected QuickLook entry QuickLook/${entry}.`);
942
- const preview = join6(directory, "Preview.png");
943
- const icon = join6(directory, "Icon.png");
944
- if (required && (!await exists5(preview) || !await exists5(icon)))
945
- throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
946
- if (await exists5(preview))
947
- validateStaticPng(await readFile6(preview), "QuickLook/Preview.png");
948
- if (await exists5(icon))
949
- validateIconPng(await readFile6(icon));
1168
+ throw new Error("Install hitSlop or set HITSLOP_NATIVE_CLI to use native rendering.");
950
1169
  }
951
- async function requireUTF8File(path, label) {
952
- const info = await lstat3(path).catch(() => {
953
- return;
954
- });
955
- if (!info?.isFile() || info.isSymbolicLink())
956
- throw new Error(`${label} must be a regular file.`);
1170
+
1171
+ // src/template-capture.ts
1172
+ init_static_preview();
1173
+ init_runtime_package();
1174
+ async function prepareTemplateCaptures(directory, options = {}) {
1175
+ const quickLook = join8(directory, "QuickLook");
1176
+ await mkdir4(quickLook, { recursive: true });
1177
+ const preview = join8(quickLook, "Preview.png");
1178
+ const icon = join8(quickLook, "Icon.png");
957
1179
  try {
958
- decoder3.decode(await readFile6(path));
959
- } catch {
960
- throw new Error(`${label} must be UTF-8.`);
1180
+ if (options.preview)
1181
+ await cp2(resolve6(options.preview), preview);
1182
+ else
1183
+ await (options.capturePreview ?? ((path, output) => runNative(["screenshot", path, "--output", output])))(directory, preview);
1184
+ validateStaticPng(await readFile7(preview), "The template preview");
1185
+ if (options.icon)
1186
+ await cp2(resolve6(options.icon), icon);
1187
+ else {
1188
+ await rm2(icon, { force: true });
1189
+ const captured = options.captureIcon ? await options.captureIcon(directory, icon) : await runNative(["screenshot", directory, "--target", "icon", "--if-present", "--output", icon]).then(() => Bun.file(icon).exists());
1190
+ if (!captured || !await Bun.file(icon).exists())
1191
+ await writeDefaultIcon(preview, icon);
1192
+ }
1193
+ validateIconPng(await readFile7(icon));
1194
+ } finally {
1195
+ await rm2(join8(directory, "stores"), { recursive: true, force: true });
961
1196
  }
1197
+ await validateRuntimePackage(directory, { template: true, requirePreview: true });
962
1198
  }
963
- async function rejectSymlinks(root) {
964
- for (const path of await walk2(root))
965
- if ((await lstat3(path)).isSymbolicLink())
966
- throw new Error(`Runtime packages cannot contain symlinks: ${relative2(root, path)}.`);
967
- }
968
- async function walk2(root, directory = root) {
969
- const result = [];
970
- for (const entry of await readdir3(directory, { withFileTypes: true })) {
971
- const path = join6(directory, entry.name);
972
- result.push(path);
973
- if (entry.isDirectory() && !entry.isSymbolicLink())
974
- result.push(...await walk2(root, path));
975
- }
976
- return result.sort((left, right) => relative2(root, left).split(sep2).join("/").localeCompare(relative2(root, right).split(sep2).join("/")));
1199
+
1200
+ // src/publish.ts
1201
+ var blobPart = (bytes) => Uint8Array.from(bytes);
1202
+ async function publishSlop(root, flags) {
1203
+ const built = await buildSlop(root);
1204
+ await prepareTemplateCaptures(built.directory, flags);
1205
+ const packed = await packSlop(built.directory);
1206
+ const identity = await getIdentity();
1207
+ const envelope = {
1208
+ format: "hitslop-publish/3",
1209
+ requestId: crypto.randomUUID(),
1210
+ publisherKeyId: identity.keyId,
1211
+ publicKey: identity.publicKey,
1212
+ artifactSha256: packed.sha256,
1213
+ artifactBytes: packed.bytes.byteLength,
1214
+ timestamp: Date.now()
1215
+ };
1216
+ const signature = await sign(canonicalPublishEnvelope(envelope), identity);
1217
+ const form = new FormData;
1218
+ form.set("envelope", JSON.stringify(envelope));
1219
+ form.set("signature", signature);
1220
+ form.set("artifact", new Blob([blobPart(packed.bytes)], { type: "application/zip" }), `${built.manifest.slug}.slop.zip`);
1221
+ const endpoint = flags.registry ?? process.env.HITSLOP_REGISTRY_URL ?? "https://api.hitslop.com/api/publish";
1222
+ const response = await fetch(endpoint, { method: "POST", body: form });
1223
+ const body = await response.text();
1224
+ if (!response.ok)
1225
+ throw new Error(`Publish failed (${response.status}): ${body}`);
1226
+ return body;
977
1227
  }
978
1228
 
979
1229
  // src/install.ts
980
- var exists6 = async (path) => stat6(path).then(() => true).catch(() => false);
1230
+ init_project();
1231
+ init_runtime_package();
1232
+ import { chmod as chmod2, cp as cp3, lstat as lstat4, mkdir as mkdir5, readdir as readdir4, rename as rename2, rm as rm3, stat as stat5 } from "fs/promises";
1233
+ import { randomUUID } from "crypto";
1234
+ import { homedir as homedir3 } from "os";
1235
+ import { join as join9, resolve as resolve7 } from "path";
1236
+ var exists5 = async (path) => stat5(path).then(() => true).catch(() => false);
981
1237
  async function installTemplate(input, options = {}) {
982
- const source = resolve4(input);
1238
+ const source = resolve7(input);
983
1239
  const runtime = await isRuntimePackage(source) ? source : (await buildSlop(source)).directory;
984
1240
  const manifest = await loadManifest(runtime);
985
1241
  await validateTemplatePackage(runtime);
986
- const templatesRoot = resolve4(options.templatesRoot ?? process.env.HITSLOP_TEMPLATES_ROOT ?? join7(homedir2(), ".hitslop", "templates"));
987
- const target = join7(templatesRoot, `${manifest.slug}.slop`);
988
- const replaced = await exists6(target);
1242
+ const templatesRoot = resolve7(options.templatesRoot ?? process.env.HITSLOP_TEMPLATES_ROOT ?? join9(homedir3(), ".hitslop", "templates"));
1243
+ const target = join9(templatesRoot, `${manifest.slug}.slop`);
1244
+ const replaced = await exists5(target);
989
1245
  if (replaced && !options.force) {
990
1246
  if (!options.confirmOverwrite)
991
1247
  throw new Error(`Template ${manifest.slug} is already installed. Pass --force to replace it.`);
992
1248
  if (!await options.confirmOverwrite(target))
993
1249
  throw new Error("Install cancelled.");
994
1250
  }
995
- await mkdir4(templatesRoot, { recursive: true });
996
- const staging = join7(templatesRoot, `.${manifest.slug}.${randomUUID()}.installing.slop`);
997
- const backup = join7(templatesRoot, `.${manifest.slug}.${randomUUID()}.backup.slop`);
1251
+ await mkdir5(templatesRoot, { recursive: true });
1252
+ const stagingRoot = join9(templatesRoot, `.${manifest.slug}.${randomUUID()}.installing`);
1253
+ const staging = join9(stagingRoot, `${manifest.slug}.slop`);
1254
+ const backup = join9(templatesRoot, `.${manifest.slug}.${randomUUID()}.backup.slop`);
998
1255
  try {
999
- await cp2(runtime, staging, { recursive: true, errorOnExist: true });
1000
- const quickLook = join7(staging, "QuickLook");
1001
- const preview = join7(quickLook, "Preview.png");
1002
- await mkdir4(quickLook, { recursive: true });
1003
- if (options.preview)
1004
- await cp2(resolve4(options.preview), preview);
1005
- else {
1006
- const capture = options.capturePreview ?? ((packageDirectory, output) => runNative(["screenshot", packageDirectory, "--output", output]));
1007
- await capture(staging, preview);
1008
- }
1009
- await validatePreview(preview);
1010
- const icon = join7(quickLook, "Icon.png");
1011
- if (options.icon)
1012
- await cp2(resolve4(options.icon), icon);
1013
- else {
1014
- await rm2(icon, { force: true });
1015
- const captured = options.captureIcon ? await options.captureIcon(staging, icon) : await captureOptionalIcon(staging, icon);
1016
- if (!captured || !await exists6(icon))
1017
- await writeDefaultIcon(preview, icon);
1018
- }
1019
- validateIconPng(await readFile7(icon));
1020
- await rm2(join7(staging, "stores"), { recursive: true, force: true });
1021
- await validateQuickLook2(staging, true);
1256
+ await mkdir5(stagingRoot);
1257
+ await cp3(runtime, staging, { recursive: true, errorOnExist: true });
1258
+ await prepareTemplateCaptures(staging, options);
1022
1259
  if (replaced) {
1023
1260
  await makePackageWritable(target);
1024
- await rename(target, backup);
1261
+ await rename2(target, backup);
1025
1262
  }
1026
1263
  try {
1027
- await rename(staging, target);
1264
+ await rename2(staging, target);
1028
1265
  } catch (error) {
1029
- if (replaced && await exists6(backup))
1030
- await rename(backup, target);
1266
+ if (replaced && await exists5(backup))
1267
+ await rename2(backup, target);
1031
1268
  throw error;
1032
1269
  }
1033
1270
  await makePackageImmutable(target);
1034
- await rm2(backup, { recursive: true, force: true });
1271
+ await rm3(backup, { recursive: true, force: true });
1035
1272
  return { directory: target, manifest, replaced };
1036
1273
  } finally {
1037
- await rm2(staging, { recursive: true, force: true });
1038
- if (await exists6(backup) && !await exists6(target)) {
1039
- await rename(backup, target);
1274
+ await rm3(stagingRoot, { recursive: true, force: true });
1275
+ if (await exists5(backup) && !await exists5(target)) {
1276
+ await rename2(backup, target);
1040
1277
  await makePackageImmutable(target).catch(() => {});
1041
1278
  } else
1042
- await rm2(backup, { recursive: true, force: true });
1279
+ await rm3(backup, { recursive: true, force: true });
1043
1280
  }
1044
1281
  }
1045
- async function captureOptionalIcon(packageDirectory, output) {
1046
- await runNative(["screenshot", packageDirectory, "--target", "icon", "--if-present", "--output", output]);
1047
- return await exists6(output);
1048
- }
1049
- async function validatePreview(path) {
1050
- validateStaticPng(await readFile7(path), "The template preview");
1051
- }
1052
1282
  async function isRuntimePackage(path) {
1053
- return await exists6(join7(path, "manifest.json")) && await exists6(join7(path, "app.html"));
1283
+ return await exists5(join9(path, "manifest.json")) && await exists5(join9(path, "app.html"));
1054
1284
  }
1055
1285
  async function validateTemplatePackage(path, options = {}) {
1056
1286
  await validateRuntimePackage(path, { template: true, requirePreview: options.requirePreview ?? false });
1057
1287
  }
1058
- async function validateQuickLook2(path, requirePreview) {
1059
- const directory = join7(path, "QuickLook");
1060
- if (!await exists6(directory)) {
1061
- if (requirePreview)
1062
- throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
1063
- return;
1064
- }
1065
- const allowed = new Set(["Preview.png", "Icon.png"]);
1066
- for (const entry of await readdir4(directory))
1067
- if (!allowed.has(entry))
1068
- throw new Error(`Template packages cannot contain QuickLook/${entry}.`);
1069
- const preview = join7(directory, "Preview.png");
1070
- const icon = join7(directory, "Icon.png");
1071
- if (!await exists6(preview) || !await exists6(icon)) {
1072
- if (requirePreview)
1073
- throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
1074
- return;
1075
- }
1076
- await validatePreview(preview);
1077
- validateIconPng(await readFile7(icon));
1078
- }
1079
1288
  async function makePackageWritable(root) {
1080
1289
  const walk3 = async (path) => {
1081
1290
  const info = await lstat4(path);
@@ -1085,7 +1294,7 @@ async function makePackageWritable(root) {
1085
1294
  await chmod2(path, mode | (info.isDirectory() ? 448 : 384));
1086
1295
  if (info.isDirectory())
1087
1296
  for (const entry of await readdir4(path))
1088
- await walk3(join7(path, entry));
1297
+ await walk3(join9(path, entry));
1089
1298
  };
1090
1299
  await walk3(root);
1091
1300
  }
@@ -1096,61 +1305,17 @@ async function makePackageImmutable(root) {
1096
1305
  throw new Error(`Runtime packages cannot contain symlinks: ${path}`);
1097
1306
  if (info.isDirectory())
1098
1307
  for (const entry of await readdir4(path))
1099
- await walk3(join7(path, entry));
1308
+ await walk3(join9(path, entry));
1100
1309
  await chmod2(path, info.mode & 511 & ~146);
1101
1310
  };
1102
1311
  await walk3(root);
1103
1312
  }
1104
1313
 
1105
- // src/publish.ts
1106
- var blobPart = (bytes) => Uint8Array.from(bytes);
1107
- async function publishSlop(root, flags) {
1108
- const built = await buildSlop(root);
1109
- const quickLook = join8(built.directory, "QuickLook");
1110
- const preview = join8(quickLook, "Preview.png");
1111
- await mkdir5(quickLook, { recursive: true });
1112
- if (flags.preview)
1113
- await cp3(resolve5(flags.preview), preview);
1114
- else
1115
- await runNative(["screenshot", built.directory, "--output", preview]);
1116
- validateStaticPng(new Uint8Array(await Bun.file(preview).arrayBuffer()), "The template preview");
1117
- const icon = join8(quickLook, "Icon.png");
1118
- if (flags.icon)
1119
- await cp3(resolve5(flags.icon), icon);
1120
- else {
1121
- await rm3(icon, { force: true });
1122
- await runNative(["screenshot", built.directory, "--target", "icon", "--if-present", "--output", icon]);
1123
- if (!await Bun.file(icon).exists())
1124
- await writeDefaultIcon(preview, icon);
1125
- }
1126
- validateIconPng(new Uint8Array(await Bun.file(icon).arrayBuffer()));
1127
- await rm3(join8(built.directory, "stores"), { recursive: true, force: true });
1128
- await validateTemplatePackage(built.directory, { requirePreview: true });
1129
- const packed = await packSlop(built.directory);
1130
- const identity = await getIdentity();
1131
- const envelope = {
1132
- format: "hitslop-publish/3",
1133
- requestId: crypto.randomUUID(),
1134
- publisherKeyId: identity.keyId,
1135
- publicKey: identity.publicKey,
1136
- artifactSha256: packed.sha256,
1137
- artifactBytes: packed.bytes.byteLength,
1138
- timestamp: Date.now()
1139
- };
1140
- const signature = await sign(canonicalPublishEnvelope(envelope), identity);
1141
- const form = new FormData;
1142
- form.set("envelope", JSON.stringify(envelope));
1143
- form.set("signature", signature);
1144
- form.set("artifact", new Blob([blobPart(packed.bytes)], { type: "application/zip" }), `${built.manifest.slug}.slop.zip`);
1145
- const endpoint = flags.registry ?? process.env.HITSLOP_REGISTRY_URL ?? "https://api.hitslop.com/api/publish";
1146
- const response = await fetch(endpoint, { method: "POST", body: form });
1147
- const body = await response.text();
1148
- if (!response.ok)
1149
- throw new Error(`Publish failed (${response.status}): ${body}`);
1150
- return body;
1151
- }
1314
+ // src/cli.ts
1315
+ init_runtime_package();
1152
1316
 
1153
1317
  // src/render.ts
1318
+ init_runtime_package();
1154
1319
  async function exportDocument(path, options, native = runNative) {
1155
1320
  await validateRuntimePackage(path);
1156
1321
  await native(["export", path, "--format", options.format, "--output", options.output]);
@@ -1170,7 +1335,7 @@ async function screenshotDocument(path, options, native = runNative) {
1170
1335
 
1171
1336
  // src/cli.ts
1172
1337
  var titleFor = (directory) => directory.split("/").filter(Boolean).at(-1)?.split(/[-_ ]+/).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ") || "My Slop";
1173
- var parseCategories = (values) => values.map((value) => SlopCategorySchema.parse(value.trim().toLowerCase()));
1338
+ var parseCategories = (values) => values.map((value) => validate(SlopCategorySchema, value.trim().toLowerCase()));
1174
1339
  async function initMetadata(directory, flags) {
1175
1340
  const authorName = flags["author-name"]?.trim();
1176
1341
  const authorURL = flags["author-url"]?.trim();
@@ -1222,7 +1387,7 @@ app = app.command("init", (command) => command.meta({ description: "Create a Sve
1222
1387
  console.log(`Created ${args.directory}`);
1223
1388
  }));
1224
1389
  app = app.command("validate", (command) => command.meta({ description: "Validate an authoring project or built .slop document." }).args([{ name: "path", type: "path", default: "." }]).run(async ({ args }) => {
1225
- const runtime = await stat7(join9(args.path, "app.html")).then((value) => value.isFile()).catch(() => false);
1390
+ const runtime = await stat6(join10(args.path, "app.html")).then((value) => value.isFile()).catch(() => false);
1226
1391
  const manifest = runtime ? await validateRuntimePackage(args.path) : await validateAuthoringProject(args.path);
1227
1392
  console.log(`valid ${manifest.slug}`);
1228
1393
  }));
@@ -1270,7 +1435,7 @@ async function secret(prompt) {
1270
1435
  process.stdout.write(prompt);
1271
1436
  process.stdin.setRawMode(true);
1272
1437
  process.stdin.resume();
1273
- return new Promise((resolve6, reject) => {
1438
+ return new Promise((resolve8, reject) => {
1274
1439
  let value = "";
1275
1440
  const onData = (chunk) => {
1276
1441
  for (const byte of chunk) {
@@ -1283,7 +1448,7 @@ async function secret(prompt) {
1283
1448
  cleanup();
1284
1449
  process.stdout.write(`
1285
1450
  `);
1286
- resolve6(value);
1451
+ resolve8(value);
1287
1452
  return;
1288
1453
  }
1289
1454
  if (byte === 127 || byte === 8) {