@hitslop/cli 0.1.4 → 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/README.md +5 -1
- package/dist/cli.js +797 -632
- package/dist/module-loader.js +25 -0
- package/package.json +17 -8
- package/templates/svelte/.agents/skills/hitslop-authoring/SKILL.md +15 -2
- package/templates/svelte/.agents/skills/hitslop-authoring/references/storage-and-packages.md +10 -4
- package/templates/svelte/.agents/skills/hitslop-authoring/references/workflow.md +23 -9
- package/templates/svelte/.agents/skills/hitslop-design/SKILL.md +3 -3
- package/templates/svelte/.agents/skills/hitslop-design/references/presentation-and-export.md +14 -11
- package/templates/svelte/AGENTS.md +8 -4
- package/templates/svelte/schema.ts +4 -4
- package/templates/svelte/src/App.svelte +24 -16
- package/templates/svelte/src/Export.svelte +9 -0
- package/templates/svelte/src/Icon.svelte +5 -0
- package/templates/svelte/src/Readout.svelte +13 -0
- package/templates/svelte/src/styles.css.ts +5 -4
- package/templates/svelte/theme.ts +12 -0
- package/templates/svelte/tsconfig.json +15 -0
- package/templates/svelte/assets/theme.css +0 -10
- package/templates/svelte/src/theme-contract.css.ts +0 -12
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
const contract = parseTheme(source, "assets/theme.css");
|
|
180
|
-
validateThemeReferences(source, contract, "assets/theme.css");
|
|
181
|
-
return contract;
|
|
250
|
+
return readUTF8(path, "assets/theme.css");
|
|
182
251
|
}
|
|
183
252
|
async function validateThemeOverride(root, contract) {
|
|
184
253
|
const path = join2(root, "stores", "theme.css");
|
|
@@ -232,109 +301,422 @@ function parseTheme(source, label) {
|
|
|
232
301
|
throw new Error(`${label} must define at least one --slop-* custom property.`);
|
|
233
302
|
return properties;
|
|
234
303
|
}
|
|
304
|
+
var exists2 = async (path) => stat2(path).then(() => true).catch(() => false), decoder2;
|
|
305
|
+
var init_theme = __esm(() => {
|
|
306
|
+
init_evaluate_module();
|
|
307
|
+
decoder2 = new TextDecoder("utf-8", { fatal: true });
|
|
308
|
+
});
|
|
235
309
|
|
|
236
310
|
// src/vite-build-plugin.ts
|
|
237
|
-
import {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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("&", "&").replaceAll('"', """) + '"' : "";
|
|
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:
|
|
345
|
+
config: () => ({ base: "./", build: { cssCodeSplit: false, assetsDir: "assets", sourcemap: false, assetsInlineLimit: 4096 } }),
|
|
278
346
|
generateBundle(_output, bundle) {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
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/
|
|
305
|
-
|
|
306
|
-
async function
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
324
|
-
if (!await
|
|
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
|
|
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 =
|
|
716
|
+
png = decodePng3(bytes, { checkCrc: true });
|
|
332
717
|
} catch {
|
|
333
718
|
throw new Error("The window skin must be a valid PNG image.");
|
|
334
719
|
}
|
|
335
|
-
if (png.width !== manifest.presentation.width || png.height !== manifest.presentation.height) {
|
|
336
|
-
throw new Error(`The window skin must be exactly ${manifest.presentation.width}x${manifest.presentation.height} pixels; received ${png.width}x${png.height}.`);
|
|
337
|
-
}
|
|
338
720
|
if (png.channels !== 4)
|
|
339
721
|
throw new Error("The window skin must be an RGBA PNG image.");
|
|
340
722
|
}
|
|
@@ -342,26 +724,27 @@ async function scaffold(destination, options) {
|
|
|
342
724
|
const template = options.template ?? "svelte-counter";
|
|
343
725
|
if (!new Set(["svelte", "svelte-counter"]).has(template))
|
|
344
726
|
throw new Error(`Unknown authoring template: ${template}`);
|
|
345
|
-
if (await
|
|
727
|
+
if (await exists4(destination) && (await readdir3(destination)).length)
|
|
346
728
|
throw new Error(`Destination is not empty: ${destination}`);
|
|
347
|
-
const slug =
|
|
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 =
|
|
732
|
+
const templateRoot = fileURLToPath2(new URL("../templates/svelte", import.meta.url));
|
|
351
733
|
await cp(templateRoot, destination, { recursive: true });
|
|
352
|
-
await
|
|
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.
|
|
364
|
-
devDependencies: { "@hitslop/cli": `^${package_default.version}`, "@sveltejs/vite-plugin-svelte": "^7.
|
|
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
750
|
const manifest = parseManifest({
|
|
@@ -373,74 +756,89 @@ async function scaffold(destination, options) {
|
|
|
373
756
|
categories: options.categories?.length ? options.categories : ["utilities"],
|
|
374
757
|
presentation: { width: 560, height: 420 }
|
|
375
758
|
});
|
|
376
|
-
await
|
|
759
|
+
await writeFile3(join4(destination, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
377
760
|
`);
|
|
378
|
-
const appPath =
|
|
379
|
-
await
|
|
761
|
+
const appPath = join4(destination, "src/App.svelte");
|
|
762
|
+
await writeFile3(appPath, (await readFile5(appPath, "utf8")).replace("__SLOP_TITLE_LITERAL__", JSON.stringify(title)));
|
|
380
763
|
}
|
|
381
764
|
async function buildSlop(root) {
|
|
382
765
|
const manifest = await loadManifest(root);
|
|
383
|
-
const
|
|
766
|
+
const schemaPath = join4(root, "schema.ts");
|
|
767
|
+
const dataSchema = await exists4(schemaPath) ? await loadDataSchema(schemaPath) : undefined;
|
|
384
768
|
await readDocumentGuideSource(root);
|
|
385
|
-
const distRoot =
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
769
|
+
const distRoot = join4(root, "dist");
|
|
770
|
+
await mkdir2(distRoot, { recursive: true });
|
|
771
|
+
const staging = await mkdtemp(join4(distRoot, ".build-"));
|
|
772
|
+
const output = join4(staging, `${manifest.slug}.slop`);
|
|
773
|
+
const destination = join4(distRoot, `${manifest.slug}.slop`);
|
|
774
|
+
const viteOut = join4(staging, "vite");
|
|
775
|
+
try {
|
|
776
|
+
const themeCSS = await readThemeCSS(root);
|
|
777
|
+
const contract = themeContract(themeCSS);
|
|
778
|
+
const hasTheme = contract !== undefined;
|
|
779
|
+
await mkdir2(output, { recursive: true });
|
|
780
|
+
await viteBuild({ root, plugins: [hitSlopBuildPlugin({ hasTheme })], build: { outDir: viteOut, emptyOutDir: true } });
|
|
781
|
+
const html = join4(viteOut, "index.html");
|
|
782
|
+
if (!await exists4(html))
|
|
783
|
+
throw new Error("Vite did not produce index.html");
|
|
784
|
+
validateThemeReferences(await readFile5(html, "utf8"), contract, "generated app.html");
|
|
785
|
+
await cp(html, join4(output, "app.html"));
|
|
786
|
+
for (const entry of await readdir3(viteOut)) {
|
|
787
|
+
if (entry === "index.html")
|
|
788
|
+
continue;
|
|
789
|
+
if (entry !== "assets")
|
|
790
|
+
throw new Error(`Unsupported Vite output ${entry}; emit runtime resources under assets/.`);
|
|
791
|
+
await copyRuntimeTree(join4(viteOut, entry), join4(output, entry));
|
|
792
|
+
}
|
|
793
|
+
await writeFile3(join4(output, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
400
794
|
`);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
795
|
+
for (const name of ["assets"]) {
|
|
796
|
+
const source = join4(root, name);
|
|
797
|
+
if (await exists4(source))
|
|
798
|
+
await copyRuntimeTree(source, join4(output, name));
|
|
799
|
+
}
|
|
800
|
+
if (themeCSS !== undefined) {
|
|
801
|
+
await mkdir2(join4(output, "assets"), { recursive: true });
|
|
802
|
+
await writeFile3(join4(output, "assets/theme.css"), themeCSS);
|
|
803
|
+
}
|
|
804
|
+
if (dataSchema)
|
|
805
|
+
await writeFile3(join4(output, "data.schema.json"), `${JSON.stringify(dataSchema, null, 2)}
|
|
408
806
|
`);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
807
|
+
await emitDocumentSkill(root, output);
|
|
808
|
+
const { validateRuntimePackage: validateRuntimePackage2 } = await Promise.resolve().then(() => (init_runtime_package(), exports_runtime_package));
|
|
809
|
+
await validateRuntimePackage2(output, { template: true });
|
|
810
|
+
const previous = join4(staging, "previous.slop");
|
|
811
|
+
if (await exists4(destination))
|
|
812
|
+
await rename(destination, previous);
|
|
813
|
+
try {
|
|
814
|
+
await rename(output, destination);
|
|
815
|
+
} catch (error) {
|
|
816
|
+
if (await exists4(previous))
|
|
817
|
+
await rename(previous, destination);
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
return { directory: destination, manifest };
|
|
821
|
+
} finally {
|
|
822
|
+
await rm(staging, { recursive: true, force: true });
|
|
425
823
|
}
|
|
426
|
-
return schema;
|
|
427
824
|
}
|
|
428
825
|
async function validateAuthoringProject(root) {
|
|
429
826
|
const manifest = await loadManifest(root);
|
|
430
827
|
await readThemeContract(root);
|
|
431
|
-
await
|
|
828
|
+
if (await exists4(join4(root, "schema.ts")))
|
|
829
|
+
await loadDataSchema(join4(root, "schema.ts"));
|
|
432
830
|
await readDocumentGuideSource(root);
|
|
433
831
|
return manifest;
|
|
434
832
|
}
|
|
435
|
-
async function
|
|
833
|
+
async function walk2(root, directory = root) {
|
|
436
834
|
const result = [];
|
|
437
|
-
for (const entry of await
|
|
438
|
-
const absolute =
|
|
439
|
-
const rel =
|
|
835
|
+
for (const entry of await readdir3(directory, { withFileTypes: true })) {
|
|
836
|
+
const absolute = join4(directory, entry.name);
|
|
837
|
+
const rel = relative2(root, absolute).split(sep2).join("/");
|
|
440
838
|
if (entry.isSymbolicLink())
|
|
441
839
|
throw new Error(`Symlinks are not allowed in artifacts: ${rel}`);
|
|
442
840
|
if (entry.isDirectory())
|
|
443
|
-
result.push(...await
|
|
841
|
+
result.push(...await walk2(root, absolute));
|
|
444
842
|
else if (entry.isFile())
|
|
445
843
|
result.push(rel);
|
|
446
844
|
}
|
|
@@ -449,33 +847,73 @@ async function walk(root, directory = root) {
|
|
|
449
847
|
async function packSlop(directory) {
|
|
450
848
|
const files = {};
|
|
451
849
|
const epoch = new Date(2000, 0, 1, 0, 0, 0);
|
|
452
|
-
for (const path of await
|
|
453
|
-
files[path] = [new Uint8Array(await Bun.file(
|
|
850
|
+
for (const path of await walk2(directory))
|
|
851
|
+
files[path] = [new Uint8Array(await Bun.file(join4(directory, path)).arrayBuffer()), { mtime: epoch, level: 9 }];
|
|
454
852
|
const bytes = zipSync(files, { level: 9 });
|
|
455
853
|
return { bytes, sha256: sha256(bytes) };
|
|
456
854
|
}
|
|
457
855
|
async function copyRuntimeTree(source, destination) {
|
|
458
|
-
if ((await
|
|
856
|
+
if ((await lstat3(source)).isSymbolicLink())
|
|
459
857
|
throw new Error(`Symlinks are not allowed in artifacts: ${source}`);
|
|
460
|
-
const info = await
|
|
858
|
+
const info = await stat4(source);
|
|
461
859
|
if (info.isDirectory()) {
|
|
462
860
|
await mkdir2(destination, { recursive: true });
|
|
463
|
-
for (const entry of await
|
|
464
|
-
await copyRuntimeTree(
|
|
861
|
+
for (const entry of await readdir3(source))
|
|
862
|
+
await copyRuntimeTree(join4(source, entry), join4(destination, entry));
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
await mkdir2(dirname3(destination), { recursive: true });
|
|
866
|
+
if (await exists4(destination)) {
|
|
867
|
+
if (!(await readFile5(source)).equals(await readFile5(destination)))
|
|
868
|
+
throw new Error(`Runtime asset collision: ${destination}`);
|
|
465
869
|
return;
|
|
466
870
|
}
|
|
467
|
-
await mkdir2(dirname2(destination), { recursive: true });
|
|
468
871
|
await cp(source, destination);
|
|
469
872
|
}
|
|
873
|
+
var manifestPath = (root) => join4(root, "manifest.json"), exists4 = async (path) => {
|
|
874
|
+
try {
|
|
875
|
+
await stat4(path);
|
|
876
|
+
return true;
|
|
877
|
+
} catch {
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
}, sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
881
|
+
var init_project = __esm(() => {
|
|
882
|
+
init_package();
|
|
883
|
+
init_document_skill();
|
|
884
|
+
init_theme();
|
|
885
|
+
init_vite_build_plugin();
|
|
886
|
+
init_data_schema();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// src/cli.ts
|
|
890
|
+
init_project();
|
|
891
|
+
import { Crust } from "@crustjs/core";
|
|
892
|
+
import { join as join10 } from "path";
|
|
893
|
+
import { stat as stat6 } from "fs/promises";
|
|
894
|
+
import { createInterface } from "readline/promises";
|
|
895
|
+
import { validate, SlopCategorySchema } from "@hitslop/schema";
|
|
470
896
|
|
|
471
897
|
// src/dev.ts
|
|
472
|
-
import {
|
|
473
|
-
import { join as join4, resolve as resolve2 } from "path";
|
|
898
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
474
899
|
import { createServer } from "vite";
|
|
475
900
|
|
|
476
901
|
// src/dev-bridge.ts
|
|
902
|
+
import { protocolVersion } from "@hitslop/schema/bridge";
|
|
477
903
|
var hostStyle = `<style data-hitslop-host>*{scrollbar-width:none!important}*::-webkit-scrollbar{width:0!important;height:0!important;display:none!important}</style>`;
|
|
478
904
|
var devHostJavaScript = `(() => {
|
|
905
|
+
const captureMode = new URL(window.location?.href ?? 'http://localhost/').searchParams.get('capture');
|
|
906
|
+
if (captureMode === 'icon') document.documentElement.dataset.slopRenderer = 'true';
|
|
907
|
+
if (captureMode === 'icon' || captureMode === 'export') {
|
|
908
|
+
window.addEventListener('slop:ready', () => {
|
|
909
|
+
setTimeout(async () => {
|
|
910
|
+
try {
|
|
911
|
+
if (captureMode === 'icon') document.documentElement.style.width = '512px';
|
|
912
|
+
await window.__hitslopCapture.begin('dev-preview', captureMode, {blockInteraction:false});
|
|
913
|
+
} catch (error) { console.error('Capture preview failed', error); }
|
|
914
|
+
}, 0);
|
|
915
|
+
}, {once:true});
|
|
916
|
+
}
|
|
479
917
|
const listeners = { json: new Set(), sqlite: new Set(), media: new Set() };
|
|
480
918
|
let jsonValue;
|
|
481
919
|
let jsonRevision = 0;
|
|
@@ -494,6 +932,8 @@ var devHostJavaScript = `(() => {
|
|
|
494
932
|
const drag = async () => undefined;
|
|
495
933
|
|
|
496
934
|
window.slop = Object.freeze({
|
|
935
|
+
info: async () => ({ protocolVersion: ${protocolVersion}, capabilities: ['host.info', 'json.open', 'json.read', 'json.write', 'window.resize', 'window.drag'] }),
|
|
936
|
+
flush: async () => undefined,
|
|
497
937
|
json: Object.freeze({
|
|
498
938
|
open: async (value) => {
|
|
499
939
|
if (!jsonOpened) { jsonValue = clone(value); jsonOpened = true; }
|
|
@@ -504,7 +944,7 @@ var devHostJavaScript = `(() => {
|
|
|
504
944
|
return { value: clone(jsonValue), revision: revision() };
|
|
505
945
|
},
|
|
506
946
|
write: async (value, expectedRevision) => {
|
|
507
|
-
if (expectedRevision && expectedRevision !== revision()) throw new Error('revision_conflict');
|
|
947
|
+
if (expectedRevision !== undefined && expectedRevision !== revision()) throw Object.assign(new Error('Document changed'), { code: 'revision_conflict' });
|
|
508
948
|
jsonValue = clone(value); jsonOpened = true; jsonRevision += 1;
|
|
509
949
|
const result = { revision: revision() };
|
|
510
950
|
emit('json', { source: 'app', revision: result.revision });
|
|
@@ -547,14 +987,8 @@ var injectHost = (html, options = {}) => {
|
|
|
547
987
|
};
|
|
548
988
|
|
|
549
989
|
// src/dev.ts
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
await stat4(path);
|
|
553
|
-
return true;
|
|
554
|
-
} catch {
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
};
|
|
990
|
+
init_project();
|
|
991
|
+
init_theme();
|
|
558
992
|
function mockHostPlugin(options = {}) {
|
|
559
993
|
return {
|
|
560
994
|
name: "hitslop-browser-preview",
|
|
@@ -566,8 +1000,8 @@ function mockHostPlugin(options = {}) {
|
|
|
566
1000
|
}
|
|
567
1001
|
async function runDev(root) {
|
|
568
1002
|
await loadManifest(root);
|
|
569
|
-
const themeHref = await
|
|
570
|
-
const server = await createServer({ root, plugins: [mockHostPlugin(themeHref ? { themeHref } : {})] });
|
|
1003
|
+
const themeHref = await readThemeCSS(root) !== undefined ? "/assets/theme.css" : undefined;
|
|
1004
|
+
const server = await createServer({ root, plugins: [themePreviewPlugin(root), mockHostPlugin(themeHref ? { themeHref } : {})] });
|
|
571
1005
|
await server.listen();
|
|
572
1006
|
const url = server.resolvedUrls?.local[0];
|
|
573
1007
|
if (!url)
|
|
@@ -576,42 +1010,46 @@ async function runDev(root) {
|
|
|
576
1010
|
if (process.platform === "darwin")
|
|
577
1011
|
Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
|
|
578
1012
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
1013
|
+
function themePreviewPlugin(root) {
|
|
1014
|
+
root = resolve3(root);
|
|
1015
|
+
return {
|
|
1016
|
+
name: "hitslop:theme-preview",
|
|
1017
|
+
configureServer(server) {
|
|
1018
|
+
server.middlewares.use(async (request, response, next) => {
|
|
1019
|
+
if (request.url?.split("?")[0] !== "/assets/theme.css")
|
|
1020
|
+
return next();
|
|
1021
|
+
try {
|
|
1022
|
+
const css = await readThemeCSS(root);
|
|
1023
|
+
if (css === undefined)
|
|
1024
|
+
return next();
|
|
1025
|
+
response.setHeader("Content-Type", "text/css; charset=utf-8");
|
|
1026
|
+
response.end(css);
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
next(error);
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
},
|
|
1032
|
+
handleHotUpdate({ file, server }) {
|
|
1033
|
+
if (file === join5(root, "theme.ts"))
|
|
1034
|
+
server.ws.send({ type: "full-reload" });
|
|
596
1035
|
}
|
|
597
|
-
}
|
|
598
|
-
throw new Error("Install hitSlop or set HITSLOP_NATIVE_CLI to use native rendering.");
|
|
1036
|
+
};
|
|
599
1037
|
}
|
|
600
1038
|
|
|
601
1039
|
// src/publish.ts
|
|
602
|
-
|
|
603
|
-
import { cp as cp3, mkdir as mkdir5, rm as rm3 } from "fs/promises";
|
|
1040
|
+
init_project();
|
|
604
1041
|
import { canonicalPublishEnvelope } from "@hitslop/schema";
|
|
605
1042
|
|
|
606
1043
|
// src/identity.ts
|
|
1044
|
+
init_project();
|
|
607
1045
|
import { getPublicKeyAsync, keygenAsync, signAsync } from "@noble/ed25519";
|
|
608
1046
|
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
|
|
609
|
-
import { chmod, mkdir as mkdir3, readFile as
|
|
610
|
-
import { join as
|
|
1047
|
+
import { chmod, mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
1048
|
+
import { join as join6, resolve as resolve4 } from "path";
|
|
611
1049
|
import { homedir } from "os";
|
|
612
1050
|
var service = "app.hitslop.cli";
|
|
613
1051
|
var account = "publisher";
|
|
614
|
-
var fallback =
|
|
1052
|
+
var fallback = join6(homedir(), ".hitslop", "identity.json");
|
|
615
1053
|
var encode = (bytes) => Buffer.from(bytes).toString("base64url");
|
|
616
1054
|
var decode = (value) => new Uint8Array(Buffer.from(value, "base64url"));
|
|
617
1055
|
async function keychainRead() {
|
|
@@ -627,14 +1065,14 @@ async function keychainWrite(value) {
|
|
|
627
1065
|
return result.exitCode === 0;
|
|
628
1066
|
}
|
|
629
1067
|
async function readStored() {
|
|
630
|
-
const value = await keychainRead() ?? await
|
|
1068
|
+
const value = await keychainRead() ?? await readFile6(fallback, "utf8").catch(() => null);
|
|
631
1069
|
return value ? JSON.parse(value) : null;
|
|
632
1070
|
}
|
|
633
1071
|
async function storeIdentity(identity) {
|
|
634
1072
|
const json = JSON.stringify(identity);
|
|
635
1073
|
if (!await keychainWrite(json)) {
|
|
636
|
-
await mkdir3(
|
|
637
|
-
await
|
|
1074
|
+
await mkdir3(join6(homedir(), ".hitslop"), { recursive: true });
|
|
1075
|
+
await writeFile4(fallback, json, { mode: 384 });
|
|
638
1076
|
await chmod(fallback, 384);
|
|
639
1077
|
}
|
|
640
1078
|
}
|
|
@@ -666,15 +1104,15 @@ async function exportIdentity(path, passphrase) {
|
|
|
666
1104
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
667
1105
|
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(identity), "utf8"), cipher.final()]);
|
|
668
1106
|
const payload = { version: 1, kdf: "scrypt", salt: encode(salt), cipher: "aes-256-gcm", iv: encode(iv), tag: encode(cipher.getAuthTag()), ciphertext: encode(ciphertext) };
|
|
669
|
-
const output =
|
|
670
|
-
await
|
|
1107
|
+
const output = resolve4(path);
|
|
1108
|
+
await writeFile4(output, `${JSON.stringify(payload, null, 2)}
|
|
671
1109
|
`, { mode: 384 });
|
|
672
1110
|
await chmod(output, 384);
|
|
673
1111
|
}
|
|
674
1112
|
async function importIdentity(path, passphrase, force = false) {
|
|
675
1113
|
if (await readStored() && !force)
|
|
676
1114
|
throw new Error("A publisher identity already exists. Pass --force to replace it.");
|
|
677
|
-
const payload = JSON.parse(await
|
|
1115
|
+
const payload = JSON.parse(await readFile6(resolve4(path), "utf8"));
|
|
678
1116
|
if (payload.version !== 1 || payload.kdf !== "scrypt" || payload.cipher !== "aes-256-gcm")
|
|
679
1117
|
throw new Error("Unsupported identity export format.");
|
|
680
1118
|
const key = scryptSync(passphrase, decode(payload.salt), 32, { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 });
|
|
@@ -692,390 +1130,161 @@ async function importIdentity(path, passphrase, force = false) {
|
|
|
692
1130
|
}
|
|
693
1131
|
var sign = async (message, identity) => encode(await signAsync(message, decode(identity.privateKey)));
|
|
694
1132
|
|
|
695
|
-
// src/
|
|
696
|
-
import {
|
|
697
|
-
import {
|
|
698
|
-
import { homedir as homedir2 } from "os";
|
|
699
|
-
import { join as join7, resolve as resolve4 } from "path";
|
|
700
|
-
|
|
701
|
-
// src/runtime-package.ts
|
|
702
|
-
import { Database } from "bun:sqlite";
|
|
703
|
-
import { lstat as lstat3, readFile as readFile6, readdir as readdir3, stat as stat5 } from "fs/promises";
|
|
704
|
-
import { basename as basename2, join as join6, relative as relative2, sep as sep2 } from "path";
|
|
705
|
-
import { unzipSync } from "fflate";
|
|
706
|
-
import { decode as decodePng3 } from "fast-png";
|
|
707
|
-
import * as z2 from "zod";
|
|
708
|
-
|
|
709
|
-
// src/static-preview.ts
|
|
710
|
-
import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
711
|
-
import { decode as decodePng2, encode as encodePng } from "fast-png";
|
|
712
|
-
var MAX_PREVIEW_BYTES = 5 * 1024 * 1024;
|
|
713
|
-
var ICON_SIZE = 512;
|
|
714
|
-
function validateStaticPng(bytes, label) {
|
|
715
|
-
if (bytes.byteLength === 0)
|
|
716
|
-
throw new Error(`${label} is empty.`);
|
|
717
|
-
if (bytes.byteLength > MAX_PREVIEW_BYTES)
|
|
718
|
-
throw new Error(`${label} cannot exceed 5 MiB.`);
|
|
719
|
-
try {
|
|
720
|
-
return decodePng2(bytes, { checkCrc: true });
|
|
721
|
-
} catch {
|
|
722
|
-
throw new Error(`${label} must be a valid PNG image.`);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
function validateIconPng(bytes, label = "The template icon") {
|
|
726
|
-
const image = validateStaticPng(bytes, label);
|
|
727
|
-
if (image.width !== ICON_SIZE || image.height !== ICON_SIZE)
|
|
728
|
-
throw new Error(`${label} must be exactly ${ICON_SIZE}x${ICON_SIZE} pixels.`);
|
|
729
|
-
return image;
|
|
730
|
-
}
|
|
731
|
-
function iconFromPng(bytes, size = ICON_SIZE) {
|
|
732
|
-
const image = validateStaticPng(bytes, "The template preview");
|
|
733
|
-
const scale = size / Math.max(image.width, image.height);
|
|
734
|
-
const width = Math.max(1, Math.round(image.width * scale));
|
|
735
|
-
const height = Math.max(1, Math.round(image.height * scale));
|
|
736
|
-
const output = image.depth === 16 ? new Uint16Array(size * size * 4) : new Uint8Array(size * size * 4);
|
|
737
|
-
const alpha = image.depth === 16 ? 65535 : 255;
|
|
738
|
-
const offsetX = Math.floor((size - width) / 2);
|
|
739
|
-
const offsetY = Math.floor((size - height) / 2);
|
|
740
|
-
for (let y = 0;y < height; y += 1) {
|
|
741
|
-
const sourceY = Math.min(image.height - 1, Math.floor((y + 0.5) * image.height / height));
|
|
742
|
-
for (let x = 0;x < width; x += 1) {
|
|
743
|
-
const sourceX = Math.min(image.width - 1, Math.floor((x + 0.5) * image.width / width));
|
|
744
|
-
const sourceOffset = (sourceY * image.width + sourceX) * image.channels;
|
|
745
|
-
const destinationOffset = ((offsetY + y) * size + offsetX + x) * 4;
|
|
746
|
-
if (image.channels === 1 || image.channels === 2) {
|
|
747
|
-
const gray = image.data[sourceOffset];
|
|
748
|
-
output[destinationOffset] = gray;
|
|
749
|
-
output[destinationOffset + 1] = gray;
|
|
750
|
-
output[destinationOffset + 2] = gray;
|
|
751
|
-
output[destinationOffset + 3] = image.channels === 2 ? image.data[sourceOffset + 1] : alpha;
|
|
752
|
-
} else {
|
|
753
|
-
output[destinationOffset] = image.data[sourceOffset];
|
|
754
|
-
output[destinationOffset + 1] = image.data[sourceOffset + 1];
|
|
755
|
-
output[destinationOffset + 2] = image.data[sourceOffset + 2];
|
|
756
|
-
output[destinationOffset + 3] = image.channels === 4 ? image.data[sourceOffset + 3] : alpha;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
return encodePng({ width: size, height: size, data: output, depth: image.depth, channels: 4 });
|
|
761
|
-
}
|
|
762
|
-
async function writeDefaultIcon(previewPath, iconPath) {
|
|
763
|
-
await writeFile4(iconPath, iconFromPng(await readFile5(previewPath)));
|
|
764
|
-
}
|
|
1133
|
+
// src/template-capture.ts
|
|
1134
|
+
import { cp as cp2, mkdir as mkdir4, readFile as readFile7, rm as rm2 } from "fs/promises";
|
|
1135
|
+
import { join as join8, resolve as resolve6 } from "path";
|
|
765
1136
|
|
|
766
|
-
// src/
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
await
|
|
781
|
-
const allowed = new Set(["manifest.json", "app.html", "data.schema.json", "assets", "stores", "QuickLook", ".agents", "Icon\r"]);
|
|
782
|
-
for (const entry of await readdir3(root))
|
|
783
|
-
if (!allowed.has(entry))
|
|
784
|
-
throw new Error(`Runtime packages cannot contain ${entry}.`);
|
|
785
|
-
const forbidden = new Set(["package.json", "bun.lock", "bun.lockb", "node_modules", "source", "src", "build", "document.json", ".build", ".hitslop", "style.css"]);
|
|
786
|
-
for (const path of await walk2(root)) {
|
|
787
|
-
if (forbidden.has(basename2(path).toLowerCase()))
|
|
788
|
-
throw new Error(`Runtime packages cannot contain ${relative2(root, path)}.`);
|
|
789
|
-
}
|
|
790
|
-
if (options.template && await exists5(join6(root, "stores")))
|
|
791
|
-
throw new Error("Template packages cannot contain stores.");
|
|
792
|
-
if (options.template && await exists5(join6(root, "Icon\r")))
|
|
793
|
-
throw new Error("Template packages cannot contain Finder metadata Icon\\r.");
|
|
794
|
-
const schema = await readDataSchema(root);
|
|
795
|
-
const contract = await readThemeContract(root);
|
|
796
|
-
const appHTML = await readFile6(join6(root, "app.html"), "utf8");
|
|
797
|
-
validateThemeReferences(appHTML, contract, "app.html");
|
|
798
|
-
await validateStores(root, schema, contract);
|
|
799
|
-
await validateQuickLook(root, options.requirePreview ?? false);
|
|
800
|
-
await validateDocumentSkill(root);
|
|
801
|
-
return manifest;
|
|
802
|
-
}
|
|
803
|
-
async function readDataSchema(root) {
|
|
804
|
-
const current = join6(root, "data.schema.json");
|
|
805
|
-
if (!await exists5(current))
|
|
806
|
-
return;
|
|
807
|
-
await requireUTF8File(current, "data.schema.json");
|
|
808
|
-
try {
|
|
809
|
-
const value = JSON.parse(await readFile6(current, "utf8"));
|
|
810
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
811
|
-
throw new Error("schema root must be an object");
|
|
812
|
-
z2.fromJSONSchema(value);
|
|
813
|
-
return value;
|
|
814
|
-
} catch (error) {
|
|
815
|
-
throw new Error(`data.schema.json must contain valid JSON Schema: ${error instanceof Error ? error.message : String(error)}`);
|
|
816
|
-
}
|
|
1137
|
+
// src/native.ts
|
|
1138
|
+
import { homedir as homedir2 } from "os";
|
|
1139
|
+
import { join as join7, resolve as resolve5 } from "path";
|
|
1140
|
+
async function runNative(arguments_) {
|
|
1141
|
+
const override = process.env.HITSLOP_NATIVE_CLI;
|
|
1142
|
+
const arch = process.arch === "arm64" ? "arm64-apple-macosx" : "x86_64-apple-macosx";
|
|
1143
|
+
const build = resolve5(import.meta.dir, "../../../apps/apple/Packages/HitSlopApple/.build", arch);
|
|
1144
|
+
const candidates = override !== undefined ? [override] : [
|
|
1145
|
+
"/Applications/hitSlop.app/Contents/Helpers/hitslop-native",
|
|
1146
|
+
join7(homedir2(), "Applications/hitSlop.app/Contents/Helpers/hitslop-native"),
|
|
1147
|
+
join7(build, "debug/hitslop-native"),
|
|
1148
|
+
join7(build, "release/hitslop-native"),
|
|
1149
|
+
"hitslop-native"
|
|
1150
|
+
];
|
|
1151
|
+
await runNativeCandidates(candidates, arguments_, override !== undefined);
|
|
817
1152
|
}
|
|
818
|
-
async function
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
return;
|
|
822
|
-
const info = await lstat3(stores);
|
|
823
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
824
|
-
throw new Error("stores must be a directory.");
|
|
825
|
-
const allowed = new Set(["data.json", "data.sqlite", "data.sqlite-wal", "data.sqlite-shm", "media", "theme.css"]);
|
|
826
|
-
for (const entry of await readdir3(stores, { withFileTypes: true })) {
|
|
827
|
-
if (!allowed.has(entry.name))
|
|
828
|
-
throw new Error(`Unexpected store entry stores/${entry.name}.`);
|
|
829
|
-
if (entry.name === "media" ? !entry.isDirectory() : !entry.isFile())
|
|
830
|
-
throw new Error(`Invalid store entry stores/${entry.name}.`);
|
|
831
|
-
}
|
|
832
|
-
const json = join6(stores, "data.json");
|
|
833
|
-
if (await exists5(json)) {
|
|
834
|
-
let value;
|
|
1153
|
+
async function runNativeCandidates(candidates, arguments_, explicit = false) {
|
|
1154
|
+
for (const executable of candidates) {
|
|
1155
|
+
let child;
|
|
835
1156
|
try {
|
|
836
|
-
|
|
1157
|
+
child = Bun.spawn([executable, ...arguments_], { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
|
|
837
1158
|
} catch (error) {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
let validator;
|
|
842
|
-
try {
|
|
843
|
-
validator = z2.fromJSONSchema(schema);
|
|
844
|
-
} catch (error) {
|
|
845
|
-
throw new Error(`The data schema is not supported: ${error instanceof Error ? error.message : String(error)}`);
|
|
846
|
-
}
|
|
847
|
-
const result = validator.safeParse(value);
|
|
848
|
-
if (!result.success)
|
|
849
|
-
throw new Error(`stores/data.json does not match the data schema: ${z2.prettifyError(result.error)}`);
|
|
1159
|
+
if (!explicit && error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
1160
|
+
continue;
|
|
1161
|
+
throw new Error(`Could not launch native helper ${executable}`, { cause: error });
|
|
850
1162
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
if (await exists5(join6(stores, sidecar)) && !await exists5(sqlite))
|
|
855
|
-
throw new Error(`stores/${sidecar} requires stores/data.sqlite.`);
|
|
856
|
-
}
|
|
857
|
-
if (await exists5(sqlite))
|
|
858
|
-
validateSQLite(sqlite);
|
|
859
|
-
const media = join6(stores, "media");
|
|
860
|
-
if (await exists5(media))
|
|
861
|
-
await validateMedia(media);
|
|
862
|
-
await validateThemeOverride(root, contract);
|
|
863
|
-
}
|
|
864
|
-
function validateSQLite(path) {
|
|
865
|
-
let database;
|
|
866
|
-
try {
|
|
867
|
-
database = new Database(path, { readonly: true, strict: true });
|
|
868
|
-
const rows = database.query("PRAGMA quick_check").all();
|
|
869
|
-
if (rows.length !== 1 || Object.values(rows[0] ?? {})[0] !== "ok")
|
|
870
|
-
throw new Error("PRAGMA quick_check did not return ok");
|
|
871
|
-
} catch (error) {
|
|
872
|
-
throw new Error(`stores/data.sqlite failed its integrity check: ${error instanceof Error ? error.message : String(error)}`);
|
|
873
|
-
} finally {
|
|
874
|
-
database?.close();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
async function validateMedia(directory) {
|
|
878
|
-
const entries = await readdir3(directory, { withFileTypes: true });
|
|
879
|
-
for (const entry of entries) {
|
|
880
|
-
if (!entry.isFile() || entry.isSymbolicLink())
|
|
881
|
-
throw new Error(`Named media must be regular files: stores/media/${entry.name}.`);
|
|
882
|
-
if (!/^[a-z][a-z0-9-]{0,63}$/.test(entry.name))
|
|
883
|
-
throw new Error(`Invalid named media key: ${entry.name}.`);
|
|
884
|
-
const bytes = await readFile6(join6(directory, entry.name));
|
|
885
|
-
if (!isImage(bytes) && !isBoundedZip(bytes))
|
|
886
|
-
throw new Error(`Unsupported named media content: ${entry.name}.`);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
function isImage(bytes) {
|
|
890
|
-
if (bytes.byteLength > 25 * MiB)
|
|
891
|
-
return false;
|
|
892
|
-
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
893
|
-
try {
|
|
894
|
-
decodePng3(bytes, { checkCrc: true });
|
|
895
|
-
return true;
|
|
896
|
-
} catch {
|
|
897
|
-
return false;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
if (bytes[0] === 255 && bytes[1] === 216 && bytes.at(-2) === 255 && bytes.at(-1) === 217)
|
|
901
|
-
return true;
|
|
902
|
-
if (String.fromCharCode(...bytes.subarray(0, 6)).match(/^GIF8[79]a$/))
|
|
903
|
-
return true;
|
|
904
|
-
return String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP";
|
|
905
|
-
}
|
|
906
|
-
function isBoundedZip(bytes) {
|
|
907
|
-
if (bytes.byteLength > 10 * MiB || bytes[0] !== 80 || bytes[1] !== 75)
|
|
908
|
-
return false;
|
|
909
|
-
try {
|
|
910
|
-
const files = unzipSync(bytes);
|
|
911
|
-
const names = Object.keys(files);
|
|
912
|
-
if (!names.length || names.length > 256)
|
|
913
|
-
return false;
|
|
914
|
-
let total = 0;
|
|
915
|
-
for (const [name, value] of Object.entries(files)) {
|
|
916
|
-
if (!safeArchivePath(name) || value.byteLength > 25 * MiB)
|
|
917
|
-
return false;
|
|
918
|
-
total += value.byteLength;
|
|
919
|
-
if (total > 50 * MiB)
|
|
920
|
-
return false;
|
|
921
|
-
}
|
|
922
|
-
return true;
|
|
923
|
-
} catch {
|
|
924
|
-
return false;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
var safeArchivePath = (path) => Boolean(path) && !path.startsWith("/") && !path.includes("\\") && !path.includes("\x00") && path.replace(/\/$/, "").split("/").every((part) => part && part !== "." && part !== "..");
|
|
928
|
-
async function validateQuickLook(root, required) {
|
|
929
|
-
const directory = join6(root, "QuickLook");
|
|
930
|
-
if (!await exists5(directory)) {
|
|
931
|
-
if (required)
|
|
932
|
-
throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
|
|
1163
|
+
const status = await child.exited;
|
|
1164
|
+
if (status !== 0)
|
|
1165
|
+
throw new Error(`Native helper ${executable} failed with status ${status}`);
|
|
933
1166
|
return;
|
|
934
1167
|
}
|
|
935
|
-
|
|
936
|
-
if (!info.isDirectory() || info.isSymbolicLink())
|
|
937
|
-
throw new Error("QuickLook must be a directory.");
|
|
938
|
-
const allowed = new Set(["Preview.png", "Icon.png"]);
|
|
939
|
-
for (const entry of await readdir3(directory))
|
|
940
|
-
if (!allowed.has(entry))
|
|
941
|
-
throw new Error(`Unexpected QuickLook entry QuickLook/${entry}.`);
|
|
942
|
-
const preview = join6(directory, "Preview.png");
|
|
943
|
-
const icon = join6(directory, "Icon.png");
|
|
944
|
-
if (required && (!await exists5(preview) || !await exists5(icon)))
|
|
945
|
-
throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
|
|
946
|
-
if (await exists5(preview))
|
|
947
|
-
validateStaticPng(await readFile6(preview), "QuickLook/Preview.png");
|
|
948
|
-
if (await exists5(icon))
|
|
949
|
-
validateIconPng(await readFile6(icon));
|
|
1168
|
+
throw new Error("Install hitSlop or set HITSLOP_NATIVE_CLI to use native rendering.");
|
|
950
1169
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1170
|
+
|
|
1171
|
+
// src/template-capture.ts
|
|
1172
|
+
init_static_preview();
|
|
1173
|
+
init_runtime_package();
|
|
1174
|
+
async function prepareTemplateCaptures(directory, options = {}) {
|
|
1175
|
+
const quickLook = join8(directory, "QuickLook");
|
|
1176
|
+
await mkdir4(quickLook, { recursive: true });
|
|
1177
|
+
const preview = join8(quickLook, "Preview.png");
|
|
1178
|
+
const icon = join8(quickLook, "Icon.png");
|
|
957
1179
|
try {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1180
|
+
if (options.preview)
|
|
1181
|
+
await cp2(resolve6(options.preview), preview);
|
|
1182
|
+
else
|
|
1183
|
+
await (options.capturePreview ?? ((path, output) => runNative(["screenshot", path, "--output", output])))(directory, preview);
|
|
1184
|
+
validateStaticPng(await readFile7(preview), "The template preview");
|
|
1185
|
+
if (options.icon)
|
|
1186
|
+
await cp2(resolve6(options.icon), icon);
|
|
1187
|
+
else {
|
|
1188
|
+
await rm2(icon, { force: true });
|
|
1189
|
+
const captured = options.captureIcon ? await options.captureIcon(directory, icon) : await runNative(["screenshot", directory, "--target", "icon", "--if-present", "--output", icon]).then(() => Bun.file(icon).exists());
|
|
1190
|
+
if (!captured || !await Bun.file(icon).exists())
|
|
1191
|
+
await writeDefaultIcon(preview, icon);
|
|
1192
|
+
}
|
|
1193
|
+
validateIconPng(await readFile7(icon));
|
|
1194
|
+
} finally {
|
|
1195
|
+
await rm2(join8(directory, "stores"), { recursive: true, force: true });
|
|
961
1196
|
}
|
|
1197
|
+
await validateRuntimePackage(directory, { template: true, requirePreview: true });
|
|
962
1198
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1199
|
+
|
|
1200
|
+
// src/publish.ts
|
|
1201
|
+
var blobPart = (bytes) => Uint8Array.from(bytes);
|
|
1202
|
+
async function publishSlop(root, flags) {
|
|
1203
|
+
const built = await buildSlop(root);
|
|
1204
|
+
await prepareTemplateCaptures(built.directory, flags);
|
|
1205
|
+
const packed = await packSlop(built.directory);
|
|
1206
|
+
const identity = await getIdentity();
|
|
1207
|
+
const envelope = {
|
|
1208
|
+
format: "hitslop-publish/3",
|
|
1209
|
+
requestId: crypto.randomUUID(),
|
|
1210
|
+
publisherKeyId: identity.keyId,
|
|
1211
|
+
publicKey: identity.publicKey,
|
|
1212
|
+
artifactSha256: packed.sha256,
|
|
1213
|
+
artifactBytes: packed.bytes.byteLength,
|
|
1214
|
+
timestamp: Date.now()
|
|
1215
|
+
};
|
|
1216
|
+
const signature = await sign(canonicalPublishEnvelope(envelope), identity);
|
|
1217
|
+
const form = new FormData;
|
|
1218
|
+
form.set("envelope", JSON.stringify(envelope));
|
|
1219
|
+
form.set("signature", signature);
|
|
1220
|
+
form.set("artifact", new Blob([blobPart(packed.bytes)], { type: "application/zip" }), `${built.manifest.slug}.slop.zip`);
|
|
1221
|
+
const endpoint = flags.registry ?? process.env.HITSLOP_REGISTRY_URL ?? "https://api.hitslop.com/api/publish";
|
|
1222
|
+
const response = await fetch(endpoint, { method: "POST", body: form });
|
|
1223
|
+
const body = await response.text();
|
|
1224
|
+
if (!response.ok)
|
|
1225
|
+
throw new Error(`Publish failed (${response.status}): ${body}`);
|
|
1226
|
+
return body;
|
|
977
1227
|
}
|
|
978
1228
|
|
|
979
1229
|
// src/install.ts
|
|
980
|
-
|
|
1230
|
+
init_project();
|
|
1231
|
+
init_runtime_package();
|
|
1232
|
+
import { chmod as chmod2, cp as cp3, lstat as lstat4, mkdir as mkdir5, readdir as readdir4, rename as rename2, rm as rm3, stat as stat5 } from "fs/promises";
|
|
1233
|
+
import { randomUUID } from "crypto";
|
|
1234
|
+
import { homedir as homedir3 } from "os";
|
|
1235
|
+
import { join as join9, resolve as resolve7 } from "path";
|
|
1236
|
+
var exists5 = async (path) => stat5(path).then(() => true).catch(() => false);
|
|
981
1237
|
async function installTemplate(input, options = {}) {
|
|
982
|
-
const source =
|
|
1238
|
+
const source = resolve7(input);
|
|
983
1239
|
const runtime = await isRuntimePackage(source) ? source : (await buildSlop(source)).directory;
|
|
984
1240
|
const manifest = await loadManifest(runtime);
|
|
985
1241
|
await validateTemplatePackage(runtime);
|
|
986
|
-
const templatesRoot =
|
|
987
|
-
const target =
|
|
988
|
-
const replaced = await
|
|
1242
|
+
const templatesRoot = resolve7(options.templatesRoot ?? process.env.HITSLOP_TEMPLATES_ROOT ?? join9(homedir3(), ".hitslop", "templates"));
|
|
1243
|
+
const target = join9(templatesRoot, `${manifest.slug}.slop`);
|
|
1244
|
+
const replaced = await exists5(target);
|
|
989
1245
|
if (replaced && !options.force) {
|
|
990
1246
|
if (!options.confirmOverwrite)
|
|
991
1247
|
throw new Error(`Template ${manifest.slug} is already installed. Pass --force to replace it.`);
|
|
992
1248
|
if (!await options.confirmOverwrite(target))
|
|
993
1249
|
throw new Error("Install cancelled.");
|
|
994
1250
|
}
|
|
995
|
-
await
|
|
996
|
-
const
|
|
997
|
-
const
|
|
1251
|
+
await mkdir5(templatesRoot, { recursive: true });
|
|
1252
|
+
const stagingRoot = join9(templatesRoot, `.${manifest.slug}.${randomUUID()}.installing`);
|
|
1253
|
+
const staging = join9(stagingRoot, `${manifest.slug}.slop`);
|
|
1254
|
+
const backup = join9(templatesRoot, `.${manifest.slug}.${randomUUID()}.backup.slop`);
|
|
998
1255
|
try {
|
|
999
|
-
await
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
await mkdir4(quickLook, { recursive: true });
|
|
1003
|
-
if (options.preview)
|
|
1004
|
-
await cp2(resolve4(options.preview), preview);
|
|
1005
|
-
else {
|
|
1006
|
-
const capture = options.capturePreview ?? ((packageDirectory, output) => runNative(["screenshot", packageDirectory, "--output", output]));
|
|
1007
|
-
await capture(staging, preview);
|
|
1008
|
-
}
|
|
1009
|
-
await validatePreview(preview);
|
|
1010
|
-
const icon = join7(quickLook, "Icon.png");
|
|
1011
|
-
if (options.icon)
|
|
1012
|
-
await cp2(resolve4(options.icon), icon);
|
|
1013
|
-
else {
|
|
1014
|
-
await rm2(icon, { force: true });
|
|
1015
|
-
const captured = options.captureIcon ? await options.captureIcon(staging, icon) : await captureOptionalIcon(staging, icon);
|
|
1016
|
-
if (!captured || !await exists6(icon))
|
|
1017
|
-
await writeDefaultIcon(preview, icon);
|
|
1018
|
-
}
|
|
1019
|
-
validateIconPng(await readFile7(icon));
|
|
1020
|
-
await rm2(join7(staging, "stores"), { recursive: true, force: true });
|
|
1021
|
-
await validateQuickLook2(staging, true);
|
|
1256
|
+
await mkdir5(stagingRoot);
|
|
1257
|
+
await cp3(runtime, staging, { recursive: true, errorOnExist: true });
|
|
1258
|
+
await prepareTemplateCaptures(staging, options);
|
|
1022
1259
|
if (replaced) {
|
|
1023
1260
|
await makePackageWritable(target);
|
|
1024
|
-
await
|
|
1261
|
+
await rename2(target, backup);
|
|
1025
1262
|
}
|
|
1026
1263
|
try {
|
|
1027
|
-
await
|
|
1264
|
+
await rename2(staging, target);
|
|
1028
1265
|
} catch (error) {
|
|
1029
|
-
if (replaced && await
|
|
1030
|
-
await
|
|
1266
|
+
if (replaced && await exists5(backup))
|
|
1267
|
+
await rename2(backup, target);
|
|
1031
1268
|
throw error;
|
|
1032
1269
|
}
|
|
1033
1270
|
await makePackageImmutable(target);
|
|
1034
|
-
await
|
|
1271
|
+
await rm3(backup, { recursive: true, force: true });
|
|
1035
1272
|
return { directory: target, manifest, replaced };
|
|
1036
1273
|
} finally {
|
|
1037
|
-
await
|
|
1038
|
-
if (await
|
|
1039
|
-
await
|
|
1274
|
+
await rm3(stagingRoot, { recursive: true, force: true });
|
|
1275
|
+
if (await exists5(backup) && !await exists5(target)) {
|
|
1276
|
+
await rename2(backup, target);
|
|
1040
1277
|
await makePackageImmutable(target).catch(() => {});
|
|
1041
1278
|
} else
|
|
1042
|
-
await
|
|
1279
|
+
await rm3(backup, { recursive: true, force: true });
|
|
1043
1280
|
}
|
|
1044
1281
|
}
|
|
1045
|
-
async function captureOptionalIcon(packageDirectory, output) {
|
|
1046
|
-
await runNative(["screenshot", packageDirectory, "--target", "icon", "--if-present", "--output", output]);
|
|
1047
|
-
return await exists6(output);
|
|
1048
|
-
}
|
|
1049
|
-
async function validatePreview(path) {
|
|
1050
|
-
validateStaticPng(await readFile7(path), "The template preview");
|
|
1051
|
-
}
|
|
1052
1282
|
async function isRuntimePackage(path) {
|
|
1053
|
-
return await
|
|
1283
|
+
return await exists5(join9(path, "manifest.json")) && await exists5(join9(path, "app.html"));
|
|
1054
1284
|
}
|
|
1055
1285
|
async function validateTemplatePackage(path, options = {}) {
|
|
1056
1286
|
await validateRuntimePackage(path, { template: true, requirePreview: options.requirePreview ?? false });
|
|
1057
1287
|
}
|
|
1058
|
-
async function validateQuickLook2(path, requirePreview) {
|
|
1059
|
-
const directory = join7(path, "QuickLook");
|
|
1060
|
-
if (!await exists6(directory)) {
|
|
1061
|
-
if (requirePreview)
|
|
1062
|
-
throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
const allowed = new Set(["Preview.png", "Icon.png"]);
|
|
1066
|
-
for (const entry of await readdir4(directory))
|
|
1067
|
-
if (!allowed.has(entry))
|
|
1068
|
-
throw new Error(`Template packages cannot contain QuickLook/${entry}.`);
|
|
1069
|
-
const preview = join7(directory, "Preview.png");
|
|
1070
|
-
const icon = join7(directory, "Icon.png");
|
|
1071
|
-
if (!await exists6(preview) || !await exists6(icon)) {
|
|
1072
|
-
if (requirePreview)
|
|
1073
|
-
throw new Error("Template packages must contain QuickLook/Preview.png and QuickLook/Icon.png.");
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
await validatePreview(preview);
|
|
1077
|
-
validateIconPng(await readFile7(icon));
|
|
1078
|
-
}
|
|
1079
1288
|
async function makePackageWritable(root) {
|
|
1080
1289
|
const walk3 = async (path) => {
|
|
1081
1290
|
const info = await lstat4(path);
|
|
@@ -1085,7 +1294,7 @@ async function makePackageWritable(root) {
|
|
|
1085
1294
|
await chmod2(path, mode | (info.isDirectory() ? 448 : 384));
|
|
1086
1295
|
if (info.isDirectory())
|
|
1087
1296
|
for (const entry of await readdir4(path))
|
|
1088
|
-
await walk3(
|
|
1297
|
+
await walk3(join9(path, entry));
|
|
1089
1298
|
};
|
|
1090
1299
|
await walk3(root);
|
|
1091
1300
|
}
|
|
@@ -1096,61 +1305,17 @@ async function makePackageImmutable(root) {
|
|
|
1096
1305
|
throw new Error(`Runtime packages cannot contain symlinks: ${path}`);
|
|
1097
1306
|
if (info.isDirectory())
|
|
1098
1307
|
for (const entry of await readdir4(path))
|
|
1099
|
-
await walk3(
|
|
1308
|
+
await walk3(join9(path, entry));
|
|
1100
1309
|
await chmod2(path, info.mode & 511 & ~146);
|
|
1101
1310
|
};
|
|
1102
1311
|
await walk3(root);
|
|
1103
1312
|
}
|
|
1104
1313
|
|
|
1105
|
-
// src/
|
|
1106
|
-
|
|
1107
|
-
async function publishSlop(root, flags) {
|
|
1108
|
-
const built = await buildSlop(root);
|
|
1109
|
-
const quickLook = join8(built.directory, "QuickLook");
|
|
1110
|
-
const preview = join8(quickLook, "Preview.png");
|
|
1111
|
-
await mkdir5(quickLook, { recursive: true });
|
|
1112
|
-
if (flags.preview)
|
|
1113
|
-
await cp3(resolve5(flags.preview), preview);
|
|
1114
|
-
else
|
|
1115
|
-
await runNative(["screenshot", built.directory, "--output", preview]);
|
|
1116
|
-
validateStaticPng(new Uint8Array(await Bun.file(preview).arrayBuffer()), "The template preview");
|
|
1117
|
-
const icon = join8(quickLook, "Icon.png");
|
|
1118
|
-
if (flags.icon)
|
|
1119
|
-
await cp3(resolve5(flags.icon), icon);
|
|
1120
|
-
else {
|
|
1121
|
-
await rm3(icon, { force: true });
|
|
1122
|
-
await runNative(["screenshot", built.directory, "--target", "icon", "--if-present", "--output", icon]);
|
|
1123
|
-
if (!await Bun.file(icon).exists())
|
|
1124
|
-
await writeDefaultIcon(preview, icon);
|
|
1125
|
-
}
|
|
1126
|
-
validateIconPng(new Uint8Array(await Bun.file(icon).arrayBuffer()));
|
|
1127
|
-
await rm3(join8(built.directory, "stores"), { recursive: true, force: true });
|
|
1128
|
-
await validateTemplatePackage(built.directory, { requirePreview: true });
|
|
1129
|
-
const packed = await packSlop(built.directory);
|
|
1130
|
-
const identity = await getIdentity();
|
|
1131
|
-
const envelope = {
|
|
1132
|
-
format: "hitslop-publish/3",
|
|
1133
|
-
requestId: crypto.randomUUID(),
|
|
1134
|
-
publisherKeyId: identity.keyId,
|
|
1135
|
-
publicKey: identity.publicKey,
|
|
1136
|
-
artifactSha256: packed.sha256,
|
|
1137
|
-
artifactBytes: packed.bytes.byteLength,
|
|
1138
|
-
timestamp: Date.now()
|
|
1139
|
-
};
|
|
1140
|
-
const signature = await sign(canonicalPublishEnvelope(envelope), identity);
|
|
1141
|
-
const form = new FormData;
|
|
1142
|
-
form.set("envelope", JSON.stringify(envelope));
|
|
1143
|
-
form.set("signature", signature);
|
|
1144
|
-
form.set("artifact", new Blob([blobPart(packed.bytes)], { type: "application/zip" }), `${built.manifest.slug}.slop.zip`);
|
|
1145
|
-
const endpoint = flags.registry ?? process.env.HITSLOP_REGISTRY_URL ?? "https://api.hitslop.com/api/publish";
|
|
1146
|
-
const response = await fetch(endpoint, { method: "POST", body: form });
|
|
1147
|
-
const body = await response.text();
|
|
1148
|
-
if (!response.ok)
|
|
1149
|
-
throw new Error(`Publish failed (${response.status}): ${body}`);
|
|
1150
|
-
return body;
|
|
1151
|
-
}
|
|
1314
|
+
// src/cli.ts
|
|
1315
|
+
init_runtime_package();
|
|
1152
1316
|
|
|
1153
1317
|
// src/render.ts
|
|
1318
|
+
init_runtime_package();
|
|
1154
1319
|
async function exportDocument(path, options, native = runNative) {
|
|
1155
1320
|
await validateRuntimePackage(path);
|
|
1156
1321
|
await native(["export", path, "--format", options.format, "--output", options.output]);
|
|
@@ -1170,7 +1335,7 @@ async function screenshotDocument(path, options, native = runNative) {
|
|
|
1170
1335
|
|
|
1171
1336
|
// src/cli.ts
|
|
1172
1337
|
var titleFor = (directory) => directory.split("/").filter(Boolean).at(-1)?.split(/[-_ ]+/).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ") || "My Slop";
|
|
1173
|
-
var parseCategories = (values) => values.map((value) => SlopCategorySchema
|
|
1338
|
+
var parseCategories = (values) => values.map((value) => validate(SlopCategorySchema, value.trim().toLowerCase()));
|
|
1174
1339
|
async function initMetadata(directory, flags) {
|
|
1175
1340
|
const authorName = flags["author-name"]?.trim();
|
|
1176
1341
|
const authorURL = flags["author-url"]?.trim();
|
|
@@ -1222,7 +1387,7 @@ app = app.command("init", (command) => command.meta({ description: "Create a Sve
|
|
|
1222
1387
|
console.log(`Created ${args.directory}`);
|
|
1223
1388
|
}));
|
|
1224
1389
|
app = app.command("validate", (command) => command.meta({ description: "Validate an authoring project or built .slop document." }).args([{ name: "path", type: "path", default: "." }]).run(async ({ args }) => {
|
|
1225
|
-
const runtime = await
|
|
1390
|
+
const runtime = await stat6(join10(args.path, "app.html")).then((value) => value.isFile()).catch(() => false);
|
|
1226
1391
|
const manifest = runtime ? await validateRuntimePackage(args.path) : await validateAuthoringProject(args.path);
|
|
1227
1392
|
console.log(`valid ${manifest.slug}`);
|
|
1228
1393
|
}));
|
|
@@ -1270,7 +1435,7 @@ async function secret(prompt) {
|
|
|
1270
1435
|
process.stdout.write(prompt);
|
|
1271
1436
|
process.stdin.setRawMode(true);
|
|
1272
1437
|
process.stdin.resume();
|
|
1273
|
-
return new Promise((
|
|
1438
|
+
return new Promise((resolve8, reject) => {
|
|
1274
1439
|
let value = "";
|
|
1275
1440
|
const onData = (chunk) => {
|
|
1276
1441
|
for (const byte of chunk) {
|
|
@@ -1283,7 +1448,7 @@ async function secret(prompt) {
|
|
|
1283
1448
|
cleanup();
|
|
1284
1449
|
process.stdout.write(`
|
|
1285
1450
|
`);
|
|
1286
|
-
|
|
1451
|
+
resolve8(value);
|
|
1287
1452
|
return;
|
|
1288
1453
|
}
|
|
1289
1454
|
if (byte === 127 || byte === 8) {
|