@hitslop/cli 0.1.3 → 0.2.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.3",
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.1",
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.2.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.2.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,213 +301,544 @@ 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
  }
341
- async function scaffold(destination, options = {}) {
723
+ 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.2.0", "@hitslop/svelte": "^0.2.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
- await writeFile2(join3(destination, "manifest.json"), JSON.stringify({
750
+ const manifest = parseManifest({
368
751
  $schema: manifestSchemaURL,
369
752
  slug,
753
+ author: options.author,
370
754
  title,
371
755
  description: options.description || "A small, lovable hitSlop app.",
372
756
  categories: options.categories?.length ? options.categories : ["utilities"],
373
757
  presentation: { width: 560, height: 420 }
374
- }, null, 2) + `
758
+ });
759
+ await writeFile3(join4(destination, "manifest.json"), JSON.stringify(manifest, null, 2) + `
375
760
  `);
376
- const appPath = join3(destination, "src/App.svelte");
377
- 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)));
378
763
  }
379
764
  async function buildSlop(root) {
380
765
  const manifest = await loadManifest(root);
381
- const dataSchema = await generateDataSchema(root);
766
+ const schemaPath = join4(root, "schema.ts");
767
+ const dataSchema = await exists4(schemaPath) ? await loadDataSchema(schemaPath) : undefined;
382
768
  await readDocumentGuideSource(root);
383
- const distRoot = join3(root, "dist");
384
- const output = join3(distRoot, `${manifest.slug}.slop`);
385
- const viteOut = join3(root, ".hitslop", "vite-build");
386
- const themeContract = await readThemeContract(root);
387
- const hasTheme = themeContract !== undefined;
388
- await rm(output, { recursive: true, force: true });
389
- await rm(viteOut, { recursive: true, force: true });
390
- await mkdir2(output, { recursive: true });
391
- await viteBuild({ root, plugins: [hitSlopBuildPlugin({ hasTheme })], build: { outDir: viteOut, emptyOutDir: true } });
392
- const html = join3(viteOut, "index.html");
393
- if (!await exists3(html))
394
- throw new Error("Vite did not produce index.html");
395
- validateThemeReferences(await readFile3(html, "utf8"), themeContract, "generated app.html");
396
- await cp(html, join3(output, "app.html"));
397
- 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)}
398
794
  `);
399
- for (const name of ["assets"]) {
400
- const source = join3(root, name);
401
- if (await exists3(source))
402
- await copyRuntimeTree(source, join3(output, name));
403
- }
404
- if (dataSchema)
405
- 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)}
406
806
  `);
407
- await emitDocumentSkill(root, output);
408
- await rm(viteOut, { recursive: true, force: true });
409
- return { directory: output, manifest };
410
- }
411
- async function generateDataSchema(root) {
412
- const source = join3(root, "schema.ts");
413
- if (!await exists3(source))
414
- return;
415
- const imported = await import(`${pathToFileURL(source).href}?hitslop=${Date.now()}`);
416
- if (!imported.default)
417
- throw new Error("schema.ts must default-export a Zod 4 schema");
418
- let schema;
419
- try {
420
- schema = z.toJSONSchema(imported.default, { target: "draft-2020-12", io: "input" });
421
- } catch (error) {
422
- 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 });
423
823
  }
424
- return schema;
425
824
  }
426
825
  async function validateAuthoringProject(root) {
427
826
  const manifest = await loadManifest(root);
428
827
  await readThemeContract(root);
429
- await generateDataSchema(root);
828
+ if (await exists4(join4(root, "schema.ts")))
829
+ await loadDataSchema(join4(root, "schema.ts"));
430
830
  await readDocumentGuideSource(root);
431
831
  return manifest;
432
832
  }
433
- async function walk(root, directory = root) {
833
+ async function walk2(root, directory = root) {
434
834
  const result = [];
435
- for (const entry of await readdir2(directory, { withFileTypes: true })) {
436
- const absolute = join3(directory, entry.name);
437
- 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("/");
438
838
  if (entry.isSymbolicLink())
439
839
  throw new Error(`Symlinks are not allowed in artifacts: ${rel}`);
440
840
  if (entry.isDirectory())
441
- result.push(...await walk(root, absolute));
841
+ result.push(...await walk2(root, absolute));
442
842
  else if (entry.isFile())
443
843
  result.push(rel);
444
844
  }
@@ -447,33 +847,73 @@ async function walk(root, directory = root) {
447
847
  async function packSlop(directory) {
448
848
  const files = {};
449
849
  const epoch = new Date(2000, 0, 1, 0, 0, 0);
450
- for (const path of await walk(directory))
451
- 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 }];
452
852
  const bytes = zipSync(files, { level: 9 });
453
853
  return { bytes, sha256: sha256(bytes) };
454
854
  }
455
855
  async function copyRuntimeTree(source, destination) {
456
- if ((await lstat2(source)).isSymbolicLink())
856
+ if ((await lstat3(source)).isSymbolicLink())
457
857
  throw new Error(`Symlinks are not allowed in artifacts: ${source}`);
458
- const info = await stat3(source);
858
+ const info = await stat4(source);
459
859
  if (info.isDirectory()) {
460
860
  await mkdir2(destination, { recursive: true });
461
- for (const entry of await readdir2(source))
462
- 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}`);
463
869
  return;
464
870
  }
465
- await mkdir2(dirname2(destination), { recursive: true });
466
871
  await cp(source, destination);
467
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";
468
896
 
469
897
  // src/dev.ts
470
- import { stat as stat4 } from "fs/promises";
471
- import { join as join4, resolve as resolve2 } from "path";
898
+ import { join as join5, resolve as resolve3 } from "path";
472
899
  import { createServer } from "vite";
473
900
 
474
901
  // src/dev-bridge.ts
902
+ import { protocolVersion } from "@hitslop/schema/bridge";
475
903
  var hostStyle = `<style data-hitslop-host>*{scrollbar-width:none!important}*::-webkit-scrollbar{width:0!important;height:0!important;display:none!important}</style>`;
476
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
+ }
477
917
  const listeners = { json: new Set(), sqlite: new Set(), media: new Set() };
478
918
  let jsonValue;
479
919
  let jsonRevision = 0;
@@ -492,6 +932,8 @@ var devHostJavaScript = `(() => {
492
932
  const drag = async () => undefined;
493
933
 
494
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,
495
937
  json: Object.freeze({
496
938
  open: async (value) => {
497
939
  if (!jsonOpened) { jsonValue = clone(value); jsonOpened = true; }
@@ -502,7 +944,7 @@ var devHostJavaScript = `(() => {
502
944
  return { value: clone(jsonValue), revision: revision() };
503
945
  },
504
946
  write: async (value, expectedRevision) => {
505
- 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' });
506
948
  jsonValue = clone(value); jsonOpened = true; jsonRevision += 1;
507
949
  const result = { revision: revision() };
508
950
  emit('json', { source: 'app', revision: result.revision });
@@ -545,14 +987,8 @@ var injectHost = (html, options = {}) => {
545
987
  };
546
988
 
547
989
  // src/dev.ts
548
- var exists4 = async (path) => {
549
- try {
550
- await stat4(path);
551
- return true;
552
- } catch {
553
- return false;
554
- }
555
- };
990
+ init_project();
991
+ init_theme();
556
992
  function mockHostPlugin(options = {}) {
557
993
  return {
558
994
  name: "hitslop-browser-preview",
@@ -564,8 +1000,8 @@ function mockHostPlugin(options = {}) {
564
1000
  }
565
1001
  async function runDev(root) {
566
1002
  await loadManifest(root);
567
- const themeHref = await exists4(join4(root, "assets", "theme.css")) ? "/assets/theme.css" : undefined;
568
- 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 } : {})] });
569
1005
  await server.listen();
570
1006
  const url = server.resolvedUrls?.local[0];
571
1007
  if (!url)
@@ -574,42 +1010,46 @@ async function runDev(root) {
574
1010
  if (process.platform === "darwin")
575
1011
  Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
576
1012
  }
577
- async function runNative(arguments_) {
578
- const override = process.env.HITSLOP_NATIVE_CLI;
579
- const arch = process.arch === "arm64" ? "arm64-apple-macosx" : "x86_64-apple-macosx";
580
- const repoDebugBin = resolve2(import.meta.dir, `../../../apps/apple/Packages/HitSlopApple/.build/${arch}/debug/hitslop-native`);
581
- const repoReleaseBin = resolve2(import.meta.dir, `../../../apps/apple/Packages/HitSlopApple/.build/${arch}/release/hitslop-native`);
582
- 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);
583
- for (const executable of candidates) {
584
- try {
585
- const processResult = Bun.spawn([executable, ...arguments_], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
586
- const status = await processResult.exited;
587
- if (status === 0)
588
- return;
589
- if (executable === candidates.at(-1))
590
- throw new Error(`hitslop-native failed with status ${status}`);
591
- } catch (error) {
592
- if (executable === candidates.at(-1))
593
- 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" });
594
1035
  }
595
- }
596
- throw new Error("Install hitSlop or set HITSLOP_NATIVE_CLI to use native rendering.");
1036
+ };
597
1037
  }
598
1038
 
599
1039
  // src/publish.ts
600
- import { join as join8, resolve as resolve5 } from "path";
601
- import { cp as cp3, mkdir as mkdir5, rm as rm3 } from "fs/promises";
1040
+ init_project();
602
1041
  import { canonicalPublishEnvelope } from "@hitslop/schema";
603
1042
 
604
1043
  // src/identity.ts
1044
+ init_project();
605
1045
  import { getPublicKeyAsync, keygenAsync, signAsync } from "@noble/ed25519";
606
1046
  import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
607
- import { chmod, mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
608
- import { join as join5, resolve as resolve3 } from "path";
609
- import { homedir, userInfo } from "os";
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";
1049
+ import { homedir } from "os";
610
1050
  var service = "app.hitslop.cli";
611
1051
  var account = "publisher";
612
- var fallback = join5(homedir(), ".hitslop", "identity.json");
1052
+ var fallback = join6(homedir(), ".hitslop", "identity.json");
613
1053
  var encode = (bytes) => Buffer.from(bytes).toString("base64url");
614
1054
  var decode = (value) => new Uint8Array(Buffer.from(value, "base64url"));
615
1055
  async function keychainRead() {
@@ -625,26 +1065,24 @@ async function keychainWrite(value) {
625
1065
  return result.exitCode === 0;
626
1066
  }
627
1067
  async function readStored() {
628
- const value = await keychainRead() ?? await readFile4(fallback, "utf8").catch(() => null);
1068
+ const value = await keychainRead() ?? await readFile6(fallback, "utf8").catch(() => null);
629
1069
  return value ? JSON.parse(value) : null;
630
1070
  }
631
1071
  async function storeIdentity(identity) {
632
1072
  const json = JSON.stringify(identity);
633
1073
  if (!await keychainWrite(json)) {
634
- await mkdir3(join5(homedir(), ".hitslop"), { recursive: true });
635
- await writeFile3(fallback, json, { mode: 384 });
1074
+ await mkdir3(join6(homedir(), ".hitslop"), { recursive: true });
1075
+ await writeFile4(fallback, json, { mode: 384 });
636
1076
  await chmod(fallback, 384);
637
1077
  }
638
1078
  }
639
1079
  async function validated(identity) {
640
- if (!identity.displayName.trim() || identity.displayName.length > 80)
641
- throw new Error("Publisher name must contain 1\u201380 characters.");
642
1080
  const derivedPublic = await getPublicKeyAsync(decode(identity.privateKey));
643
1081
  if (encode(derivedPublic) !== identity.publicKey)
644
1082
  throw new Error("Identity public and private keys do not match.");
645
1083
  if (sha256(derivedPublic).slice(0, 32) !== identity.keyId)
646
1084
  throw new Error("Identity key ID does not match its public key.");
647
- return { ...identity, displayName: identity.displayName.trim() };
1085
+ return { privateKey: identity.privateKey, publicKey: identity.publicKey, keyId: identity.keyId };
648
1086
  }
649
1087
  async function getIdentity() {
650
1088
  const stored = await readStored();
@@ -652,16 +1090,10 @@ async function getIdentity() {
652
1090
  return validated(stored);
653
1091
  const pair = await keygenAsync();
654
1092
  const publicKey = pair.publicKey ?? await getPublicKeyAsync(pair.secretKey);
655
- const identity = await validated({ privateKey: encode(pair.secretKey), publicKey: encode(publicKey), keyId: sha256(publicKey).slice(0, 32), displayName: userInfo().username });
1093
+ const identity = await validated({ privateKey: encode(pair.secretKey), publicKey: encode(publicKey), keyId: sha256(publicKey).slice(0, 32) });
656
1094
  await storeIdentity(identity);
657
1095
  return identity;
658
1096
  }
659
- async function setIdentityName(displayName) {
660
- const identity = await getIdentity();
661
- const next = await validated({ ...identity, displayName });
662
- await storeIdentity(next);
663
- return next;
664
- }
665
1097
  async function exportIdentity(path, passphrase) {
666
1098
  if (passphrase.length < 10)
667
1099
  throw new Error("Identity export passphrases must contain at least 10 characters.");
@@ -672,15 +1104,15 @@ async function exportIdentity(path, passphrase) {
672
1104
  const cipher = createCipheriv("aes-256-gcm", key, iv);
673
1105
  const ciphertext = Buffer.concat([cipher.update(JSON.stringify(identity), "utf8"), cipher.final()]);
674
1106
  const payload = { version: 1, kdf: "scrypt", salt: encode(salt), cipher: "aes-256-gcm", iv: encode(iv), tag: encode(cipher.getAuthTag()), ciphertext: encode(ciphertext) };
675
- const output = resolve3(path);
676
- await writeFile3(output, `${JSON.stringify(payload, null, 2)}
1107
+ const output = resolve4(path);
1108
+ await writeFile4(output, `${JSON.stringify(payload, null, 2)}
677
1109
  `, { mode: 384 });
678
1110
  await chmod(output, 384);
679
1111
  }
680
1112
  async function importIdentity(path, passphrase, force = false) {
681
1113
  if (await readStored() && !force)
682
1114
  throw new Error("A publisher identity already exists. Pass --force to replace it.");
683
- const payload = JSON.parse(await readFile4(resolve3(path), "utf8"));
1115
+ const payload = JSON.parse(await readFile6(resolve4(path), "utf8"));
684
1116
  if (payload.version !== 1 || payload.kdf !== "scrypt" || payload.cipher !== "aes-256-gcm")
685
1117
  throw new Error("Unsupported identity export format.");
686
1118
  const key = scryptSync(passphrase, decode(payload.salt), 32, { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
@@ -698,390 +1130,161 @@ async function importIdentity(path, passphrase, force = false) {
698
1130
  }
699
1131
  var sign = async (message, identity) => encode(await signAsync(message, decode(identity.privateKey)));
700
1132
 
701
- // src/install.ts
702
- 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";
703
- import { randomUUID } from "crypto";
704
- import { homedir as homedir2 } from "os";
705
- import { join as join7, resolve as resolve4 } from "path";
706
-
707
- // src/runtime-package.ts
708
- import { Database } from "bun:sqlite";
709
- import { lstat as lstat3, readFile as readFile6, readdir as readdir3, stat as stat5 } from "fs/promises";
710
- import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
711
- import { unzipSync } from "fflate";
712
- import { decode as decodePng3 } from "fast-png";
713
- import * as z2 from "zod";
714
-
715
- // src/static-preview.ts
716
- import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
717
- import { decode as decodePng2, encode as encodePng } from "fast-png";
718
- var MAX_PREVIEW_BYTES = 5 * 1024 * 1024;
719
- var ICON_SIZE = 512;
720
- function validateStaticPng(bytes, label) {
721
- if (bytes.byteLength === 0)
722
- throw new Error(`${label} is empty.`);
723
- if (bytes.byteLength > MAX_PREVIEW_BYTES)
724
- throw new Error(`${label} cannot exceed 5 MiB.`);
725
- try {
726
- return decodePng2(bytes, { checkCrc: true });
727
- } catch {
728
- throw new Error(`${label} must be a valid PNG image.`);
729
- }
730
- }
731
- function validateIconPng(bytes, label = "The template icon") {
732
- const image = validateStaticPng(bytes, label);
733
- if (image.width !== ICON_SIZE || image.height !== ICON_SIZE)
734
- throw new Error(`${label} must be exactly ${ICON_SIZE}x${ICON_SIZE} pixels.`);
735
- return image;
736
- }
737
- function iconFromPng(bytes, size = ICON_SIZE) {
738
- const image = validateStaticPng(bytes, "The template preview");
739
- const scale = size / Math.max(image.width, image.height);
740
- const width = Math.max(1, Math.round(image.width * scale));
741
- const height = Math.max(1, Math.round(image.height * scale));
742
- const output = image.depth === 16 ? new Uint16Array(size * size * 4) : new Uint8Array(size * size * 4);
743
- const alpha = image.depth === 16 ? 65535 : 255;
744
- const offsetX = Math.floor((size - width) / 2);
745
- const offsetY = Math.floor((size - height) / 2);
746
- for (let y = 0;y < height; y += 1) {
747
- const sourceY = Math.min(image.height - 1, Math.floor((y + 0.5) * image.height / height));
748
- for (let x = 0;x < width; x += 1) {
749
- const sourceX = Math.min(image.width - 1, Math.floor((x + 0.5) * image.width / width));
750
- const sourceOffset = (sourceY * image.width + sourceX) * image.channels;
751
- const destinationOffset = ((offsetY + y) * size + offsetX + x) * 4;
752
- if (image.channels === 1 || image.channels === 2) {
753
- const gray = image.data[sourceOffset];
754
- output[destinationOffset] = gray;
755
- output[destinationOffset + 1] = gray;
756
- output[destinationOffset + 2] = gray;
757
- output[destinationOffset + 3] = image.channels === 2 ? image.data[sourceOffset + 1] : alpha;
758
- } else {
759
- output[destinationOffset] = image.data[sourceOffset];
760
- output[destinationOffset + 1] = image.data[sourceOffset + 1];
761
- output[destinationOffset + 2] = image.data[sourceOffset + 2];
762
- output[destinationOffset + 3] = image.channels === 4 ? image.data[sourceOffset + 3] : alpha;
763
- }
764
- }
765
- }
766
- return encodePng({ width: size, height: size, data: output, depth: image.depth, channels: 4 });
767
- }
768
- async function writeDefaultIcon(previewPath, iconPath) {
769
- await writeFile4(iconPath, iconFromPng(await readFile5(previewPath)));
770
- }
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";
771
1136
 
772
- // src/runtime-package.ts
773
- var decoder3 = new TextDecoder("utf-8", { fatal: true });
774
- var exists5 = async (path) => stat5(path).then(() => true).catch(() => false);
775
- var MiB = 1024 * 1024;
776
- async function validateRuntimePackage(root, options = {}) {
777
- if (basename2(root).startsWith("."))
778
- throw new Error("Runtime package path is not valid.");
779
- const rootInfo = await lstat3(root).catch(() => {
780
- return;
781
- });
782
- if (!rootInfo?.isDirectory() || rootInfo.isSymbolicLink())
783
- throw new Error("A runtime .slop must be a directory, not a symlink.");
784
- const manifest = await loadManifest(root);
785
- await requireUTF8File(join6(root, "app.html"), "app.html");
786
- await rejectSymlinks(root);
787
- const allowed = new Set(["manifest.json", "app.html", "data.schema.json", "assets", "stores", "QuickLook", ".agents", "Icon\r"]);
788
- for (const entry of await readdir3(root))
789
- if (!allowed.has(entry))
790
- throw new Error(`Runtime packages cannot contain ${entry}.`);
791
- const forbidden = new Set(["package.json", "bun.lock", "bun.lockb", "node_modules", "source", "src", "build", "document.json", ".build", ".hitslop", "style.css"]);
792
- for (const path of await walk2(root)) {
793
- if (forbidden.has(basename2(path).toLowerCase()))
794
- throw new Error(`Runtime packages cannot contain ${relative2(root, path)}.`);
795
- }
796
- if (options.template && await exists5(join6(root, "stores")))
797
- throw new Error("Template packages cannot contain stores.");
798
- if (options.template && await exists5(join6(root, "Icon\r")))
799
- throw new Error("Template packages cannot contain Finder metadata Icon\\r.");
800
- const schema = await readDataSchema(root);
801
- const contract = await readThemeContract(root);
802
- const appHTML = await readFile6(join6(root, "app.html"), "utf8");
803
- validateThemeReferences(appHTML, contract, "app.html");
804
- await validateStores(root, schema, contract);
805
- await validateQuickLook(root, options.requirePreview ?? false);
806
- await validateDocumentSkill(root);
807
- return manifest;
808
- }
809
- async function readDataSchema(root) {
810
- const current = join6(root, "data.schema.json");
811
- if (!await exists5(current))
812
- return;
813
- await requireUTF8File(current, "data.schema.json");
814
- try {
815
- const value = JSON.parse(await readFile6(current, "utf8"));
816
- if (!value || typeof value !== "object" || Array.isArray(value))
817
- throw new Error("schema root must be an object");
818
- z2.fromJSONSchema(value);
819
- return value;
820
- } catch (error) {
821
- throw new Error(`data.schema.json must contain valid JSON Schema: ${error instanceof Error ? error.message : String(error)}`);
822
- }
823
- }
824
- async function validateStores(root, schema, contract) {
825
- const stores = join6(root, "stores");
826
- if (!await exists5(stores))
827
- return;
828
- const info = await lstat3(stores);
829
- if (!info.isDirectory() || info.isSymbolicLink())
830
- throw new Error("stores must be a directory.");
831
- const allowed = new Set(["data.json", "data.sqlite", "data.sqlite-wal", "data.sqlite-shm", "media", "theme.css"]);
832
- for (const entry of await readdir3(stores, { withFileTypes: true })) {
833
- if (!allowed.has(entry.name))
834
- throw new Error(`Unexpected store entry stores/${entry.name}.`);
835
- if (entry.name === "media" ? !entry.isDirectory() : !entry.isFile())
836
- throw new Error(`Invalid store entry stores/${entry.name}.`);
837
- }
838
- const json = join6(stores, "data.json");
839
- if (await exists5(json)) {
840
- let value;
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);
1152
+ }
1153
+ async function runNativeCandidates(candidates, arguments_, explicit = false) {
1154
+ for (const executable of candidates) {
1155
+ let child;
841
1156
  try {
842
- value = JSON.parse(decoder3.decode(await readFile6(json)));
1157
+ child = Bun.spawn([executable, ...arguments_], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
843
1158
  } catch (error) {
844
- throw new Error(`stores/data.json must be valid UTF-8 JSON: ${error instanceof Error ? error.message : String(error)}`);
845
- }
846
- if (schema) {
847
- let validator;
848
- try {
849
- validator = z2.fromJSONSchema(schema);
850
- } catch (error) {
851
- throw new Error(`The data schema is not supported: ${error instanceof Error ? error.message : String(error)}`);
852
- }
853
- const result = validator.safeParse(value);
854
- if (!result.success)
855
- 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 });
856
1162
  }
857
- }
858
- const sqlite = join6(stores, "data.sqlite");
859
- for (const sidecar of ["data.sqlite-wal", "data.sqlite-shm"]) {
860
- if (await exists5(join6(stores, sidecar)) && !await exists5(sqlite))
861
- throw new Error(`stores/${sidecar} requires stores/data.sqlite.`);
862
- }
863
- if (await exists5(sqlite))
864
- validateSQLite(sqlite);
865
- const media = join6(stores, "media");
866
- if (await exists5(media))
867
- await validateMedia(media);
868
- await validateThemeOverride(root, contract);
869
- }
870
- function validateSQLite(path) {
871
- let database;
872
- try {
873
- database = new Database(path, { readonly: true, strict: true });
874
- const rows = database.query("PRAGMA quick_check").all();
875
- if (rows.length !== 1 || Object.values(rows[0] ?? {})[0] !== "ok")
876
- throw new Error("PRAGMA quick_check did not return ok");
877
- } catch (error) {
878
- throw new Error(`stores/data.sqlite failed its integrity check: ${error instanceof Error ? error.message : String(error)}`);
879
- } finally {
880
- database?.close();
881
- }
882
- }
883
- async function validateMedia(directory) {
884
- const entries = await readdir3(directory, { withFileTypes: true });
885
- for (const entry of entries) {
886
- if (!entry.isFile() || entry.isSymbolicLink())
887
- throw new Error(`Named media must be regular files: stores/media/${entry.name}.`);
888
- if (!/^[a-z][a-z0-9-]{0,63}$/.test(entry.name))
889
- throw new Error(`Invalid named media key: ${entry.name}.`);
890
- const bytes = await readFile6(join6(directory, entry.name));
891
- if (!isImage(bytes) && !isBoundedZip(bytes))
892
- throw new Error(`Unsupported named media content: ${entry.name}.`);
893
- }
894
- }
895
- function isImage(bytes) {
896
- if (bytes.byteLength > 25 * MiB)
897
- return false;
898
- if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
899
- try {
900
- decodePng3(bytes, { checkCrc: true });
901
- return true;
902
- } catch {
903
- return false;
904
- }
905
- }
906
- if (bytes[0] === 255 && bytes[1] === 216 && bytes.at(-2) === 255 && bytes.at(-1) === 217)
907
- return true;
908
- if (String.fromCharCode(...bytes.subarray(0, 6)).match(/^GIF8[79]a$/))
909
- return true;
910
- return String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP";
911
- }
912
- function isBoundedZip(bytes) {
913
- if (bytes.byteLength > 10 * MiB || bytes[0] !== 80 || bytes[1] !== 75)
914
- return false;
915
- try {
916
- const files = unzipSync(bytes);
917
- const names = Object.keys(files);
918
- if (!names.length || names.length > 256)
919
- return false;
920
- let total = 0;
921
- for (const [name, value] of Object.entries(files)) {
922
- if (!safeArchivePath(name) || value.byteLength > 25 * MiB)
923
- return false;
924
- total += value.byteLength;
925
- if (total > 50 * MiB)
926
- return false;
927
- }
928
- return true;
929
- } catch {
930
- return false;
931
- }
932
- }
933
- var safeArchivePath = (path) => Boolean(path) && !path.startsWith("/") && !path.includes("\\") && !path.includes("\x00") && path.replace(/\/$/, "").split("/").every((part) => part && part !== "." && part !== "..");
934
- async function validateQuickLook(root, required) {
935
- const directory = join6(root, "QuickLook");
936
- if (!await exists5(directory)) {
937
- if (required)
938
- 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}`);
939
1166
  return;
940
1167
  }
941
- const info = await lstat3(directory);
942
- if (!info.isDirectory() || info.isSymbolicLink())
943
- throw new Error("QuickLook must be a directory.");
944
- const allowed = new Set(["Preview.png", "Icon.png"]);
945
- for (const entry of await readdir3(directory))
946
- if (!allowed.has(entry))
947
- throw new Error(`Unexpected QuickLook entry QuickLook/${entry}.`);
948
- const preview = join6(directory, "Preview.png");
949
- const icon = join6(directory, "Icon.png");
950
- if (required && (!await exists5(preview) || !await exists5(icon)))
951
- throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
952
- if (await exists5(preview))
953
- validateStaticPng(await readFile6(preview), "QuickLook/Preview.png");
954
- if (await exists5(icon))
955
- validateIconPng(await readFile6(icon));
1168
+ throw new Error("Install hitSlop or set HITSLOP_NATIVE_CLI to use native rendering.");
956
1169
  }
957
- async function requireUTF8File(path, label) {
958
- const info = await lstat3(path).catch(() => {
959
- return;
960
- });
961
- if (!info?.isFile() || info.isSymbolicLink())
962
- 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");
963
1179
  try {
964
- decoder3.decode(await readFile6(path));
965
- } catch {
966
- 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 });
967
1196
  }
1197
+ await validateRuntimePackage(directory, { template: true, requirePreview: true });
968
1198
  }
969
- async function rejectSymlinks(root) {
970
- for (const path of await walk2(root))
971
- if ((await lstat3(path)).isSymbolicLink())
972
- throw new Error(`Runtime packages cannot contain symlinks: ${relative2(root, path)}.`);
973
- }
974
- async function walk2(root, directory = root) {
975
- const result = [];
976
- for (const entry of await readdir3(directory, { withFileTypes: true })) {
977
- const path = join6(directory, entry.name);
978
- result.push(path);
979
- if (entry.isDirectory() && !entry.isSymbolicLink())
980
- result.push(...await walk2(root, path));
981
- }
982
- 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;
983
1227
  }
984
1228
 
985
1229
  // src/install.ts
986
- 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);
987
1237
  async function installTemplate(input, options = {}) {
988
- const source = resolve4(input);
1238
+ const source = resolve7(input);
989
1239
  const runtime = await isRuntimePackage(source) ? source : (await buildSlop(source)).directory;
990
1240
  const manifest = await loadManifest(runtime);
991
1241
  await validateTemplatePackage(runtime);
992
- const templatesRoot = resolve4(options.templatesRoot ?? process.env.HITSLOP_TEMPLATES_ROOT ?? join7(homedir2(), ".hitslop", "templates"));
993
- const target = join7(templatesRoot, `${manifest.slug}.slop`);
994
- 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);
995
1245
  if (replaced && !options.force) {
996
1246
  if (!options.confirmOverwrite)
997
1247
  throw new Error(`Template ${manifest.slug} is already installed. Pass --force to replace it.`);
998
1248
  if (!await options.confirmOverwrite(target))
999
1249
  throw new Error("Install cancelled.");
1000
1250
  }
1001
- await mkdir4(templatesRoot, { recursive: true });
1002
- const staging = join7(templatesRoot, `.${manifest.slug}.${randomUUID()}.installing.slop`);
1003
- 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`);
1004
1255
  try {
1005
- await cp2(runtime, staging, { recursive: true, errorOnExist: true });
1006
- const quickLook = join7(staging, "QuickLook");
1007
- const preview = join7(quickLook, "Preview.png");
1008
- await mkdir4(quickLook, { recursive: true });
1009
- if (options.preview)
1010
- await cp2(resolve4(options.preview), preview);
1011
- else {
1012
- const capture = options.capturePreview ?? ((packageDirectory, output) => runNative(["screenshot", packageDirectory, "--output", output]));
1013
- await capture(staging, preview);
1014
- }
1015
- await validatePreview(preview);
1016
- const icon = join7(quickLook, "Icon.png");
1017
- if (options.icon)
1018
- await cp2(resolve4(options.icon), icon);
1019
- else {
1020
- await rm2(icon, { force: true });
1021
- const captured = options.captureIcon ? await options.captureIcon(staging, icon) : await captureOptionalIcon(staging, icon);
1022
- if (!captured || !await exists6(icon))
1023
- await writeDefaultIcon(preview, icon);
1024
- }
1025
- validateIconPng(await readFile7(icon));
1026
- await rm2(join7(staging, "stores"), { recursive: true, force: true });
1027
- await validateQuickLook2(staging, true);
1256
+ await mkdir5(stagingRoot);
1257
+ await cp3(runtime, staging, { recursive: true, errorOnExist: true });
1258
+ await prepareTemplateCaptures(staging, options);
1028
1259
  if (replaced) {
1029
1260
  await makePackageWritable(target);
1030
- await rename(target, backup);
1261
+ await rename2(target, backup);
1031
1262
  }
1032
1263
  try {
1033
- await rename(staging, target);
1264
+ await rename2(staging, target);
1034
1265
  } catch (error) {
1035
- if (replaced && await exists6(backup))
1036
- await rename(backup, target);
1266
+ if (replaced && await exists5(backup))
1267
+ await rename2(backup, target);
1037
1268
  throw error;
1038
1269
  }
1039
1270
  await makePackageImmutable(target);
1040
- await rm2(backup, { recursive: true, force: true });
1271
+ await rm3(backup, { recursive: true, force: true });
1041
1272
  return { directory: target, manifest, replaced };
1042
1273
  } finally {
1043
- await rm2(staging, { recursive: true, force: true });
1044
- if (await exists6(backup) && !await exists6(target)) {
1045
- 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);
1046
1277
  await makePackageImmutable(target).catch(() => {});
1047
1278
  } else
1048
- await rm2(backup, { recursive: true, force: true });
1279
+ await rm3(backup, { recursive: true, force: true });
1049
1280
  }
1050
1281
  }
1051
- async function captureOptionalIcon(packageDirectory, output) {
1052
- await runNative(["screenshot", packageDirectory, "--target", "icon", "--if-present", "--output", output]);
1053
- return await exists6(output);
1054
- }
1055
- async function validatePreview(path) {
1056
- validateStaticPng(await readFile7(path), "The template preview");
1057
- }
1058
1282
  async function isRuntimePackage(path) {
1059
- 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"));
1060
1284
  }
1061
1285
  async function validateTemplatePackage(path, options = {}) {
1062
1286
  await validateRuntimePackage(path, { template: true, requirePreview: options.requirePreview ?? false });
1063
1287
  }
1064
- async function validateQuickLook2(path, requirePreview) {
1065
- const directory = join7(path, "QuickLook");
1066
- if (!await exists6(directory)) {
1067
- if (requirePreview)
1068
- throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
1069
- return;
1070
- }
1071
- const allowed = new Set(["Preview.png", "Icon.png"]);
1072
- for (const entry of await readdir4(directory))
1073
- if (!allowed.has(entry))
1074
- throw new Error(`Template packages cannot contain QuickLook/${entry}.`);
1075
- const preview = join7(directory, "Preview.png");
1076
- const icon = join7(directory, "Icon.png");
1077
- if (!await exists6(preview) || !await exists6(icon)) {
1078
- if (requirePreview)
1079
- throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
1080
- return;
1081
- }
1082
- await validatePreview(preview);
1083
- validateIconPng(await readFile7(icon));
1084
- }
1085
1288
  async function makePackageWritable(root) {
1086
1289
  const walk3 = async (path) => {
1087
1290
  const info = await lstat4(path);
@@ -1091,7 +1294,7 @@ async function makePackageWritable(root) {
1091
1294
  await chmod2(path, mode | (info.isDirectory() ? 448 : 384));
1092
1295
  if (info.isDirectory())
1093
1296
  for (const entry of await readdir4(path))
1094
- await walk3(join7(path, entry));
1297
+ await walk3(join9(path, entry));
1095
1298
  };
1096
1299
  await walk3(root);
1097
1300
  }
@@ -1102,62 +1305,17 @@ async function makePackageImmutable(root) {
1102
1305
  throw new Error(`Runtime packages cannot contain symlinks: ${path}`);
1103
1306
  if (info.isDirectory())
1104
1307
  for (const entry of await readdir4(path))
1105
- await walk3(join7(path, entry));
1308
+ await walk3(join9(path, entry));
1106
1309
  await chmod2(path, info.mode & 511 & ~146);
1107
1310
  };
1108
1311
  await walk3(root);
1109
1312
  }
1110
1313
 
1111
- // src/publish.ts
1112
- var blobPart = (bytes) => Uint8Array.from(bytes);
1113
- async function publishSlop(root, flags) {
1114
- const built = await buildSlop(root);
1115
- const quickLook = join8(built.directory, "QuickLook");
1116
- const preview = join8(quickLook, "Preview.png");
1117
- await mkdir5(quickLook, { recursive: true });
1118
- if (flags.preview)
1119
- await cp3(resolve5(flags.preview), preview);
1120
- else
1121
- await runNative(["screenshot", built.directory, "--output", preview]);
1122
- validateStaticPng(new Uint8Array(await Bun.file(preview).arrayBuffer()), "The template preview");
1123
- const icon = join8(quickLook, "Icon.png");
1124
- if (flags.icon)
1125
- await cp3(resolve5(flags.icon), icon);
1126
- else {
1127
- await rm3(icon, { force: true });
1128
- await runNative(["screenshot", built.directory, "--target", "icon", "--if-present", "--output", icon]);
1129
- if (!await Bun.file(icon).exists())
1130
- await writeDefaultIcon(preview, icon);
1131
- }
1132
- validateIconPng(new Uint8Array(await Bun.file(icon).arrayBuffer()));
1133
- await rm3(join8(built.directory, "stores"), { recursive: true, force: true });
1134
- await validateTemplatePackage(built.directory, { requirePreview: true });
1135
- const packed = await packSlop(built.directory);
1136
- const identity = await getIdentity();
1137
- const envelope = {
1138
- format: "hitslop-publish/2",
1139
- requestId: crypto.randomUUID(),
1140
- publisherKeyId: identity.keyId,
1141
- publicKey: identity.publicKey,
1142
- displayName: identity.displayName,
1143
- artifactSha256: packed.sha256,
1144
- artifactBytes: packed.bytes.byteLength,
1145
- timestamp: Date.now()
1146
- };
1147
- const signature = await sign(canonicalPublishEnvelope(envelope), identity);
1148
- const form = new FormData;
1149
- form.set("envelope", JSON.stringify(envelope));
1150
- form.set("signature", signature);
1151
- form.set("artifact", new Blob([blobPart(packed.bytes)], { type: "application/zip" }), `${built.manifest.slug}.slop.zip`);
1152
- const endpoint = flags.registry ?? process.env.HITSLOP_REGISTRY_URL ?? "https://api.hitslop.com/api/publish";
1153
- const response = await fetch(endpoint, { method: "POST", body: form });
1154
- const body = await response.text();
1155
- if (!response.ok)
1156
- throw new Error(`Publish failed (${response.status}): ${body}`);
1157
- return body;
1158
- }
1314
+ // src/cli.ts
1315
+ init_runtime_package();
1159
1316
 
1160
1317
  // src/render.ts
1318
+ init_runtime_package();
1161
1319
  async function exportDocument(path, options, native = runNative) {
1162
1320
  await validateRuntimePackage(path);
1163
1321
  await native(["export", path, "--format", options.format, "--output", options.output]);
@@ -1177,11 +1335,16 @@ async function screenshotDocument(path, options, native = runNative) {
1177
1335
 
1178
1336
  // src/cli.ts
1179
1337
  var titleFor = (directory) => directory.split("/").filter(Boolean).at(-1)?.split(/[-_ ]+/).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ") || "My Slop";
1180
- var parseCategories = (values) => values.map((value) => SlopCategorySchema.parse(value.trim().toLowerCase()));
1338
+ var parseCategories = (values) => values.map((value) => validate(SlopCategorySchema, value.trim().toLowerCase()));
1181
1339
  async function initMetadata(directory, flags) {
1340
+ const authorName = flags["author-name"]?.trim();
1341
+ const authorURL = flags["author-url"]?.trim();
1182
1342
  const defaults = { title: flags.title || titleFor(directory), description: flags.description || "A small, lovable hitSlop app.", categories: parseCategories(flags.category?.length ? flags.category : ["utilities"]) };
1183
- if (flags.yes || !process.stdin.isTTY)
1184
- return defaults;
1343
+ if (flags.yes || !process.stdin.isTTY) {
1344
+ if (!authorName)
1345
+ throw new Error("--author-name is required with --yes or in a non-interactive terminal.");
1346
+ return { ...defaults, author: { name: authorName, ...authorURL ? { url: authorURL } : {} } };
1347
+ }
1185
1348
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
1186
1349
  try {
1187
1350
  const title = (await prompt.question(`Title (${defaults.title}): `)).trim() || defaults.title;
@@ -1190,7 +1353,11 @@ async function initMetadata(directory, flags) {
1190
1353
  const categories = rawCategories ? parseCategories(rawCategories.split(",").filter(Boolean)) : defaults.categories;
1191
1354
  if (categories.length < 1 || categories.length > 2)
1192
1355
  throw new Error("Choose one or two categories.");
1193
- return { title, description, categories };
1356
+ const name = authorName || (await prompt.question("Author name: ")).trim();
1357
+ if (!name)
1358
+ throw new Error("Author name is required.");
1359
+ const url = authorURL || (await prompt.question("Author URL (optional): ")).trim();
1360
+ return { title, description, categories, author: { name, ...url ? { url } : {} } };
1194
1361
  } finally {
1195
1362
  prompt.close();
1196
1363
  }
@@ -1210,6 +1377,8 @@ app = app.command("init", (command) => command.meta({ description: "Create a Sve
1210
1377
  template: { type: "string", description: "Svelte authoring template (svelte or svelte-counter)." },
1211
1378
  title: { type: "string", description: "Manifest title." },
1212
1379
  description: { type: "string", description: "Manifest description." },
1380
+ "author-name": { type: "string", description: "Required manifest author name." },
1381
+ "author-url": { type: "string", description: "Optional manifest author URL." },
1213
1382
  category: { type: "string", multiple: true, description: "Manifest category; pass once or twice." },
1214
1383
  yes: { type: "boolean", description: "Accept manifest defaults without prompting." }
1215
1384
  }).run(async ({ args, flags }) => {
@@ -1218,7 +1387,7 @@ app = app.command("init", (command) => command.meta({ description: "Create a Sve
1218
1387
  console.log(`Created ${args.directory}`);
1219
1388
  }));
1220
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 }) => {
1221
- 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);
1222
1391
  const manifest = runtime ? await validateRuntimePackage(args.path) : await validateAuthoringProject(args.path);
1223
1392
  console.log(`valid ${manifest.slug}`);
1224
1393
  }));
@@ -1235,7 +1404,7 @@ app = app.command("register", (command) => command.meta({ description: "Build an
1235
1404
  const result = await installTemplate(args.path, { force: flags.force ?? false, ...flags.preview ? { preview: flags.preview } : {}, ...flags.icon ? { icon: flags.icon } : {}, confirmOverwrite: confirmReplacement });
1236
1405
  console.log(`${result.replaced ? "Updated" : "Installed"} ${result.manifest.title} at ${result.directory}`);
1237
1406
  }));
1238
- app = app.command("publish", (command) => command.args([{ name: "path", type: "path", default: "." }]).flags({ registry: { type: "string", description: "Catalog publish endpoint. Defaults to https://api.hitslop.com/api/publish. Set HITSLOP_REGISTRY_URL=http://localhost:3000/api/publish for a local catalog." }, preview: { type: "path" }, icon: { type: "path", description: "Use this 512x512 PNG as the catalog and Finder icon." } }).run(async ({ args, flags }) => {
1407
+ app = app.command("publish", (command) => command.args([{ name: "path", type: "path", default: "." }]).flags({ registry: { type: "string", description: "Catalog publish endpoint. Defaults to https://api.hitslop.com/api/publish. Set HITSLOP_REGISTRY_URL=http://127.0.0.1:5002/api/publish for the local Firebase emulators." }, preview: { type: "path" }, icon: { type: "path", description: "Use this 512x512 PNG as the catalog and Finder icon." } }).run(async ({ args, flags }) => {
1239
1408
  console.log(await publishSlop(args.path, { ...flags.registry ? { registry: flags.registry } : {}, ...flags.preview ? { preview: flags.preview } : {}, ...flags.icon ? { icon: flags.icon } : {} }));
1240
1409
  }));
1241
1410
  app = app.command("export", (command) => command.meta({ description: "Export a built .slop document as a full-height PNG or PDF." }).args([{ name: "path", type: "path", default: "." }]).flags({
@@ -1266,7 +1435,7 @@ async function secret(prompt) {
1266
1435
  process.stdout.write(prompt);
1267
1436
  process.stdin.setRawMode(true);
1268
1437
  process.stdin.resume();
1269
- return new Promise((resolve6, reject) => {
1438
+ return new Promise((resolve8, reject) => {
1270
1439
  let value = "";
1271
1440
  const onData = (chunk) => {
1272
1441
  for (const byte of chunk) {
@@ -1279,7 +1448,7 @@ async function secret(prompt) {
1279
1448
  cleanup();
1280
1449
  process.stdout.write(`
1281
1450
  `);
1282
- resolve6(value);
1451
+ resolve8(value);
1283
1452
  return;
1284
1453
  }
1285
1454
  if (byte === 127 || byte === 8) {
@@ -1303,15 +1472,7 @@ async function runIdentity(arguments_) {
1303
1472
  const action = arguments_[1] ?? "show";
1304
1473
  if (action === "show") {
1305
1474
  const identity = await getIdentity();
1306
- console.log(`${identity.keyId} ${identity.displayName}`);
1307
- return true;
1308
- }
1309
- if (action === "set-name") {
1310
- const name = arguments_.slice(2).join(" ").trim();
1311
- if (!name)
1312
- throw new Error("Usage: slop identity set-name <name>");
1313
- const identity = await setIdentityName(name);
1314
- console.log(`${identity.keyId} ${identity.displayName}`);
1475
+ console.log(identity.keyId);
1315
1476
  return true;
1316
1477
  }
1317
1478
  if (action === "export") {
@@ -1331,7 +1492,7 @@ async function runIdentity(arguments_) {
1331
1492
  if (!path)
1332
1493
  throw new Error("Usage: slop identity import <file> [--force]");
1333
1494
  const identity = await importIdentity(path, await secret("Import passphrase: "), arguments_.includes("--force"));
1334
- console.log(`${identity.keyId} ${identity.displayName}`);
1495
+ console.log(identity.keyId);
1335
1496
  return true;
1336
1497
  }
1337
1498
  throw new Error(`Unknown identity command: ${action}`);