@dayofweek/dcli 1.3.0 → 1.4.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/skills.js ADDED
@@ -0,0 +1,77 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ function sha256(value) {
5
+ return createHash("sha256").update(value).digest("hex");
6
+ }
7
+ export function validateSkillBundle(bundle) {
8
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(bundle.name) || !bundle.version.trim() || bundle.version.length > 64) {
9
+ throw new Error("Skill bundle metadata is invalid");
10
+ }
11
+ if (bundle.files.length === 0 || bundle.files.length > 64)
12
+ throw new Error("Skill bundle file count is invalid");
13
+ const paths = new Set();
14
+ const files = bundle.files.map((file) => {
15
+ const segments = file.path.split("/");
16
+ if (!file.path ||
17
+ file.path.startsWith("/") ||
18
+ file.path.includes("\\") ||
19
+ file.path.includes("\0") ||
20
+ segments.some((segment) => !segment || segment === "." || segment === "..") ||
21
+ paths.has(file.path) ||
22
+ Buffer.byteLength(file.content) > 1_000_000) {
23
+ throw new Error("Skill bundle contained an unsafe file");
24
+ }
25
+ paths.add(file.path);
26
+ const actual = sha256(file.content);
27
+ if (file.sha256 && file.sha256 !== actual)
28
+ throw new Error("Skill bundle file checksum mismatch");
29
+ return { ...file, sha256: actual };
30
+ });
31
+ const actualBundleHash = sha256(files.map((file) => `${file.path}\0${file.sha256}`).join("\0"));
32
+ if (bundle.hash && bundle.hash !== actualBundleHash)
33
+ throw new Error("Skill bundle manifest checksum mismatch");
34
+ return { ...bundle, hash: actualBundleHash, files };
35
+ }
36
+ export function writeSkillBundle(input, targetDir) {
37
+ const bundle = validateSkillBundle(input);
38
+ let filesWritten = 0;
39
+ let unchanged = 0;
40
+ const conflicts = [];
41
+ const metadataPath = join(targetDir, ".dayofweek-skill.json");
42
+ const previous = existsSync(metadataPath)
43
+ ? JSON.parse(readFileSync(metadataPath, "utf8"))
44
+ : undefined;
45
+ const hashes = {};
46
+ for (const file of bundle.files) {
47
+ const filePath = join(targetDir, file.path);
48
+ const nextHash = file.sha256;
49
+ hashes[file.path] = nextHash;
50
+ mkdirSync(dirname(filePath), { recursive: true });
51
+ if (existsSync(filePath)) {
52
+ const currentHash = sha256(readFileSync(filePath));
53
+ if (currentHash === nextHash) {
54
+ unchanged++;
55
+ continue;
56
+ }
57
+ if (previous?.files?.[file.path] !== currentHash) {
58
+ let suffix = 0;
59
+ let conflictPath = `${filePath}.new`;
60
+ while (existsSync(conflictPath) && suffix < 999) {
61
+ suffix++;
62
+ conflictPath = `${filePath}.new.${suffix}`;
63
+ }
64
+ if (existsSync(conflictPath))
65
+ throw new Error("Too many unresolved skill update conflicts");
66
+ writeFileSync(conflictPath, file.content, { encoding: "utf8", flag: "wx" });
67
+ conflicts.push(suffix === 0 ? `${file.path}.new` : `${file.path}.new.${suffix}`);
68
+ continue;
69
+ }
70
+ }
71
+ writeFileSync(filePath, file.content, "utf-8");
72
+ filesWritten++;
73
+ }
74
+ mkdirSync(targetDir, { recursive: true });
75
+ writeFileSync(metadataPath, JSON.stringify({ name: bundle.name, version: bundle.version, hash: bundle.hash, files: hashes }, null, 2), "utf8");
76
+ return { written: filesWritten, unchanged, conflicts };
77
+ }
package/dist/uri.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export type BrainResource = {
2
+ version: 1;
3
+ areaId: string;
4
+ resourceType: "area" | "note" | "source";
5
+ resourceId?: string;
6
+ };
7
+ export declare function parseBrainResource(input: string): BrainResource;
8
+ export declare function toBrainUri(resource: BrainResource): string;
9
+ export declare function toBrainHttpsUrl(resource: BrainResource): string;
package/dist/uri.js ADDED
@@ -0,0 +1,72 @@
1
+ const HTTPS_ORIGIN = "https://field.dayofweek.com";
2
+ const HTTPS_PREFIX = "/app/brain/";
3
+ const ID_PATTERN = /^[A-Za-z0-9_-]{2,128}$/;
4
+ function assertId(value, label) {
5
+ if (!value || !ID_PATTERN.test(value)) {
6
+ throw new Error(`Invalid ${label}`);
7
+ }
8
+ return value;
9
+ }
10
+ function parseSegments(pathname) {
11
+ if (pathname.includes("%"))
12
+ throw new Error("Encoded brain URI paths are not supported");
13
+ const segments = pathname.split("/").filter(Boolean);
14
+ if (segments.length === 1) {
15
+ return {
16
+ areaId: assertId(segments[0], "area ID"),
17
+ resourceType: "area",
18
+ };
19
+ }
20
+ if (segments.length !== 3)
21
+ throw new Error("Unsupported brain URI form");
22
+ const areaId = assertId(segments[0], "area ID");
23
+ const resourceType = segments[1];
24
+ if (resourceType !== "note" && resourceType !== "source") {
25
+ throw new Error("Unsupported brain resource type");
26
+ }
27
+ return {
28
+ areaId,
29
+ resourceType,
30
+ resourceId: assertId(segments[2], `${resourceType} ID`),
31
+ };
32
+ }
33
+ export function parseBrainResource(input) {
34
+ if (input.length > 512)
35
+ throw new Error("Brain URI is too long");
36
+ let url;
37
+ try {
38
+ url = new URL(input);
39
+ }
40
+ catch {
41
+ throw new Error("Invalid brain URI");
42
+ }
43
+ if (url.username || url.password || url.search || url.hash) {
44
+ throw new Error("Brain URIs cannot contain credentials, queries, or fragments");
45
+ }
46
+ let pathname;
47
+ if (url.protocol === "dayofweek:") {
48
+ if (url.hostname !== "brain" || url.port)
49
+ throw new Error("Invalid brain URI authority");
50
+ pathname = url.pathname;
51
+ }
52
+ else if (url.protocol === "https:") {
53
+ if (url.origin !== HTTPS_ORIGIN || !url.pathname.startsWith(HTTPS_PREFIX)) {
54
+ throw new Error("Unsupported Day of Week deeplink");
55
+ }
56
+ pathname = url.pathname.slice(HTTPS_PREFIX.length - 1);
57
+ }
58
+ else {
59
+ throw new Error("Unsupported brain URI protocol");
60
+ }
61
+ return { version: 1, ...parseSegments(pathname) };
62
+ }
63
+ export function toBrainUri(resource) {
64
+ const areaId = assertId(resource.areaId, "area ID");
65
+ if (resource.resourceType === "area")
66
+ return `dayofweek://brain/${areaId}`;
67
+ const resourceId = assertId(resource.resourceId, `${resource.resourceType} ID`);
68
+ return `dayofweek://brain/${areaId}/${resource.resourceType}/${resourceId}`;
69
+ }
70
+ export function toBrainHttpsUrl(resource) {
71
+ return `${HTTPS_ORIGIN}/app/brain/${toBrainUri(resource).slice("dayofweek://brain/".length)}`;
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayofweek/dcli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for the Day of Week AgTech platform — read data and submit proposals for review",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,6 +16,10 @@
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "dev": "tsx src/bin/dcli.ts",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "standalone:build": "node scripts/build-standalone.mjs",
22
+ "standalone:smoke": "node scripts/smoke-standalone.mjs",
19
23
  "prepublishOnly": "npm run build"
20
24
  },
21
25
  "dependencies": {
@@ -23,9 +27,12 @@
23
27
  "open": "^11.0.0"
24
28
  },
25
29
  "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "esbuild": "^0.25.0",
32
+ "postject": "^1.0.0-alpha.6",
26
33
  "tsx": "^4.21.0",
27
34
  "typescript": "^5.9.0",
28
- "@types/node": "^22.0.0"
35
+ "vitest": "^4.0.0"
29
36
  },
30
37
  "publishConfig": {
31
38
  "access": "public"