@bud.sh/app 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import * as Crypto from "node:crypto";
9
9
  import { createHash, randomBytes, randomUUID } from "node:crypto";
10
10
  import { any, array, boolean, createSchema, defaulted, enum_, fail, number, object, optional, parse, parseSafe, string } from "remix/data-schema";
11
11
  import { gunzipSync, gzipSync } from "node:zlib";
12
+ import { RoutePattern } from "remix/route-pattern";
12
13
  import { staticFiles } from "remix/middleware/static";
13
14
  import { createController, createRouter } from "remix/router";
14
15
  import * as NodeChildProcess from "node:child_process";
@@ -18,7 +19,6 @@ import * as NFS from "node:fs";
18
19
  import { readFileSync } from "node:fs";
19
20
  import * as OS from "node:os";
20
21
  import { tmpdir } from "node:os";
21
- import { RoutePattern } from "remix/route-pattern";
22
22
  import { clientEntry, navigate, on, ref } from "remix/component";
23
23
  import { jsx, jsxs } from "remix/component/jsx-runtime";
24
24
  import { createMultiMatcher } from "remix/route-pattern/match";
@@ -7646,7 +7646,7 @@ var logWithLevel$1 = (level) => (...message) => {
7646
7646
  return void_$2;
7647
7647
  });
7648
7648
  };
7649
- var colors = {
7649
+ var colors$1 = {
7650
7650
  bold: "1",
7651
7651
  red: "31",
7652
7652
  green: "32",
@@ -7658,7 +7658,7 @@ var colors = {
7658
7658
  black: "30",
7659
7659
  bgBrightRed: "101"
7660
7660
  };
7661
- colors.gray, colors.blue, colors.green, colors.yellow, colors.red, colors.bgBrightRed, colors.black;
7661
+ colors$1.gray, colors$1.blue, colors$1.green, colors$1.yellow, colors$1.red, colors$1.bgBrightRed, colors$1.black;
7662
7662
  var defaultDateFormat = (date) => `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}.${date.getMilliseconds().toString().padStart(3, "0")}`;
7663
7663
  /** @internal */
7664
7664
  var defaultLogger = /*#__PURE__*/ loggerMake(({ cause, date, fiber, logLevel, message }) => {
@@ -22149,658 +22149,1407 @@ function httpPreviewStorage(base, options = {}) {
22149
22149
  };
22150
22150
  }
22151
22151
  //#endregion
22152
- //#region ../beads/src/lib/contracts.ts
22153
- var MAX_BEADS = 1e3;
22154
- //#endregion
22155
- //#region ../beads/src/lib/validation.ts
22156
- /** Beads returned or was sent data of the wrong shape; `field` names where. */
22157
- var BeadsValidationError = class extends Error {
22158
- field;
22159
- constructor(field) {
22160
- super(`Invalid Beads data at ${field}.`);
22161
- this.name = "BeadsValidationError";
22162
- this.field = field;
22163
- }
22164
- };
22165
- /**
22166
- * Checks that a value is a JSON object.
22167
- * @param value - Untrusted data.
22168
- * @param field - Where the value sits, for the error.
22169
- * @returns The object.
22170
- */
22171
- function object$1(value, field) {
22172
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new BeadsValidationError(field);
22173
- return value;
22174
- }
22175
- /**
22176
- * Checks a string without NUL characters within a length limit.
22177
- * @param value - Untrusted data.
22178
- * @param field - Where the value sits, for the error.
22179
- * @param max - The most characters it may have.
22180
- * @returns The string.
22181
- */
22182
- function text$4(value, field, max = 262144) {
22183
- if (typeof value !== "string" || value.length > max || value.includes("\0")) throw new BeadsValidationError(field);
22184
- return value;
22185
- }
22186
- /**
22187
- * Checks an ID, name or commit made of letters, digits, dots, underscores and hyphens.
22188
- * @param value - Untrusted data.
22189
- * @param field - Where the value sits, for the error.
22190
- * @returns The identifier.
22191
- */
22192
- function identifier$1(value, field) {
22193
- let result = text$4(value, field, 512);
22194
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(result)) throw new BeadsValidationError(field);
22195
- return result;
22196
- }
22197
- /**
22198
- * Checks an optional string.
22199
- * @param value - Untrusted data.
22200
- * @param field - Where the value sits, for the error.
22201
- * @param max - The most characters it may have.
22202
- * @returns The string, or null when it is missing.
22203
- */
22204
- function nullableText(value, field, max) {
22205
- return value == null ? null : text$4(value, field, max);
22206
- }
22152
+ //#region ../project/src/lib/validate.ts
22153
+ var errors = /* @__PURE__ */ new WeakMap();
22207
22154
  /**
22208
- * Checks a nonnegative safe integer.
22209
- * @param value - Untrusted data.
22210
- * @param field - Where the value sits, for the error.
22211
- * @param max - The largest value it may have.
22212
- * @returns The integer.
22155
+ * Validates config, throwing its first problem as the `Error` the config checks report.
22156
+ * @param schema - The config schema.
22157
+ * @param value - The project's untrusted setting.
22158
+ * @returns The validated, normalized setting.
22213
22159
  */
22214
- function integer(value, field, max = Number.MAX_SAFE_INTEGER) {
22215
- if (!Number.isSafeInteger(value) || value < 0 || value > max) throw new BeadsValidationError(field);
22216
- return value;
22160
+ function parseConfig(schema, value) {
22161
+ let result = parseSafe(schema, value, { abortEarly: true });
22162
+ if (result.success) return result.value;
22163
+ let issue = result.issues[0];
22164
+ throw errors.get(issue) ?? new Error(issue.message);
22217
22165
  }
22218
22166
  /**
22219
- * Checks an optional nonnegative integer.
22220
- * @param value - Untrusted data.
22221
- * @param field - Where the value sits, for the error.
22222
- * @param max - The largest value it may have.
22223
- * @returns The integer, or null when it is missing.
22167
+ * Reports any failure of `schema` with one message.
22168
+ * @param schema - The schema to run.
22169
+ * @param message - The message for any of its failures.
22170
+ * @returns The described schema.
22224
22171
  */
22225
- function nullableInteger(value, field, max) {
22226
- return value == null ? null : integer(value, field, max);
22172
+ function described(schema, message) {
22173
+ return createSchema((value, context) => {
22174
+ let result = schema["~run"](value, context);
22175
+ return result.issues ? fail(message, context.path) : result;
22176
+ });
22227
22177
  }
22228
22178
  /**
22229
- * Checks a boolean.
22230
- * @param value - Untrusted data.
22231
- * @param field - Where the value sits, for the error.
22232
- * @returns The boolean.
22179
+ * Applies a rule after `schema` validates.
22180
+ * @param schema - The schema to run first.
22181
+ * @param rule - Returns a message, or undefined when the value is valid.
22182
+ * @returns The checked schema.
22233
22183
  */
22234
- function boolean$2(value, field) {
22235
- if (typeof value !== "boolean") throw new BeadsValidationError(field);
22236
- return value;
22184
+ function check(schema, rule) {
22185
+ return createSchema((value, context) => {
22186
+ let result = schema["~run"](value, context);
22187
+ if (result.issues) return result;
22188
+ let message = rule(result.value);
22189
+ return message === void 0 ? result : fail(message, context.path);
22190
+ });
22237
22191
  }
22238
22192
  /**
22239
- * Checks an ISO 8601 timestamp with a time zone.
22240
- * @param value - Untrusted data.
22241
- * @param field - Where the value sits, for the error.
22242
- * @returns The timestamp as given.
22193
+ * Validates with a function that throws, such as a URL or route map parser, keeping its error.
22194
+ * @param schema - The schema to run first.
22195
+ * @param parse - Turns its value into the output, or throws.
22196
+ * @returns The delegating schema.
22243
22197
  */
22244
- function timestamp(value, field) {
22245
- let result = text$4(value, field, 64);
22246
- if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(result) || !Number.isFinite(Date.parse(result))) throw new BeadsValidationError(field);
22247
- return result;
22198
+ function delegate(schema, parse) {
22199
+ return createSchema((value, context) => {
22200
+ let result = schema["~run"](value, context);
22201
+ if (result.issues) return result;
22202
+ try {
22203
+ return { value: parse(result.value) };
22204
+ } catch (error) {
22205
+ if (!(error instanceof Error)) throw error;
22206
+ let failure = fail(error.message, context.path);
22207
+ errors.set(failure.issues[0], error);
22208
+ return failure;
22209
+ }
22210
+ });
22248
22211
  }
22249
22212
  /**
22250
- * Checks an optional ISO 8601 timestamp.
22251
- * @param value - Untrusted data.
22252
- * @param field - Where the value sits, for the error.
22253
- * @returns The timestamp, or null when it is missing.
22213
+ * Chooses a schema from the unvalidated value.
22214
+ * @param choose - Picks the schema for a value.
22215
+ * @returns The selecting schema.
22254
22216
  */
22255
- function nullableTimestamp(value, field) {
22256
- return value == null ? null : timestamp(value, field);
22217
+ function select$1(choose) {
22218
+ return createSchema((value, context) => choose(value)["~run"](value, context));
22257
22219
  }
22258
22220
  /**
22259
- * Checks a bounded array and each of its entries.
22260
- * @param value - Untrusted data.
22261
- * @param field - Where the value sits, for the error.
22262
- * @param parse - Checks one entry.
22263
- * @param max - The most entries it may have.
22264
- * @returns The parsed entries.
22221
+ * Substitutes a default for null or undefined.
22222
+ * @param schema - The schema for present values.
22223
+ * @param value - Makes the default.
22224
+ * @returns The schema with its default.
22265
22225
  */
22266
- function array$1(value, field, parse, max = 1e4) {
22267
- if (!Array.isArray(value) || value.length > max) throw new BeadsValidationError(field);
22268
- return value.map((entry, i) => parse(entry, `${field}[${i}]`));
22226
+ function fallback(schema, value) {
22227
+ return createSchema((input, context) => input == null ? { value: value() } : schema["~run"](input, context));
22269
22228
  }
22270
22229
  /**
22271
- * Checks that no two values share a key.
22272
- * @param values - The values to check.
22273
- * @param key - Gives each value's key.
22274
- * @param field - Where the values sit, for the error.
22275
- * @returns The values.
22230
+ * A settings object that names its unsupported keys before validating its fields.
22231
+ * @param name - The setting's name in messages, such as `previews`.
22232
+ * @param shape - The schema for each supported key.
22233
+ * @returns The settings schema.
22276
22234
  */
22277
- function unique(values, key, field) {
22278
- if (new Set(values.map(key)).size !== values.length) throw new BeadsValidationError(field);
22279
- return values;
22235
+ function settings(name, shape) {
22236
+ let schema = object(shape);
22237
+ return createSchema((value, context) => {
22238
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail(`${name} must be an object.`, context.path);
22239
+ for (let key of Object.keys(value)) if (!Object.hasOwn(shape, key)) return fail(`${name}.${key} is not a supported setting.`, [...context.path, key]);
22240
+ return schema["~run"](value, context);
22241
+ });
22280
22242
  }
22281
22243
  /**
22282
- * Checks JSON metadata, bounding its depth and its total number of values.
22283
- * @param value - Untrusted data.
22284
- * @param field - Where the value sits, for the error.
22285
- * @param depth - How deep this value is nested.
22286
- * @param budget - The values left to check, shared across the whole tree.
22287
- * @returns The metadata.
22244
+ * A list whose length is checked before its entries.
22245
+ * @param item - The schema for each entry.
22246
+ * @param message - The message when it is not a list or is too short.
22247
+ * @param minimum - The fewest entries it may have.
22248
+ * @returns The list schema.
22288
22249
  */
22289
- function metadata(value, field, depth = 0, budget = { remaining: 1e4 }) {
22290
- if (depth > 12 || --budget.remaining < 0) throw new BeadsValidationError(field);
22291
- if (value === null || typeof value === "boolean") return value;
22292
- if (typeof value === "number" && Number.isFinite(value)) return value;
22293
- if (typeof value === "string") return text$4(value, field);
22294
- if (Array.isArray(value)) return value.map((entry, index) => metadata(entry, `${field}[${index}]`, depth + 1, budget));
22295
- let input = object$1(value, field);
22296
- return Object.fromEntries(Object.entries(input).map(([key, entry]) => [text$4(key, field, 512), metadata(entry, `${field}.${key}`, depth + 1, budget)]));
22250
+ function list(item, message, minimum = 0) {
22251
+ let items = array(item);
22252
+ return createSchema((value, context) => !Array.isArray(value) || value.length < minimum ? fail(message, context.path) : items["~run"](value, context));
22297
22253
  }
22298
- function dependency(value, field) {
22299
- let input = object$1(value, field);
22254
+ //#endregion
22255
+ //#region ../project/src/lib/files.ts
22256
+ /** The Files settings, with their defaults filled in. */
22257
+ var BudFilesSchema = defaulted(settings("files", { edit: fallback(described(boolean(), "files.edit must be true or false."), () => true) }), () => ({ edit: true }));
22258
+ //#endregion
22259
+ //#region ../project/src/lib/flows.ts
22260
+ var message$1 = "flows must be [{ id, title, start, script, steps: [{ id, title }], block? }] with short ids, local paths and unique steps.";
22261
+ var slug = (value) => typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value);
22262
+ var text$4 = (value) => typeof value === "string" && value.trim() !== "" && value.length <= 200;
22263
+ function flow(value) {
22264
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
22265
+ let { id, title, start, script, steps, block = [] } = value;
22266
+ if (!slug(id) || !text$4(title) || !text$4(script) || script.includes("\0")) return null;
22267
+ if (typeof start !== "string" || !start.startsWith("/") || start.startsWith("//")) return null;
22268
+ if (new URL(start, "http://bud.invalid").origin !== "http://bud.invalid") return null;
22269
+ if (!Array.isArray(steps) || !steps.length || steps.length > 50) return null;
22270
+ let resolved = steps.map((step) => {
22271
+ let { id: stepId, title: stepTitle } = step ?? {};
22272
+ return slug(stepId) && text$4(stepTitle) ? {
22273
+ id: stepId,
22274
+ title: stepTitle.trim()
22275
+ } : null;
22276
+ });
22277
+ if (resolved.some((step) => !step)) return null;
22278
+ let ids = resolved.map((step) => step.id);
22279
+ if (new Set(ids).size !== ids.length) return null;
22280
+ if (!Array.isArray(block) || block.some((part) => !text$4(part))) return null;
22300
22281
  return {
22301
- id: identifier$1(input.id, `${field}.id`),
22302
- type: identifier$1(input.type, `${field}.type`),
22303
- title: nullableText(input.title, `${field}.title`),
22304
- status: nullableText(input.status, `${field}.status`, 128),
22305
- priority: nullableInteger(input.priority, `${field}.priority`, 4),
22306
- issueType: nullableText(input.issueType, `${field}.issueType`, 128)
22282
+ id,
22283
+ title: title.trim(),
22284
+ start,
22285
+ script: script.trim(),
22286
+ steps: resolved,
22287
+ block
22307
22288
  };
22308
22289
  }
22290
+ /** The flows, none when omitted. */
22291
+ var BudFlowsSchema = createSchema((value, context) => {
22292
+ if (value === void 0) return { value: [] };
22293
+ if (!Array.isArray(value)) return fail(message$1, context.path);
22294
+ let flows = value.map(flow);
22295
+ if (flows.some((resolved) => !resolved)) return fail(message$1, context.path);
22296
+ let ids = flows.map((resolved) => resolved.id);
22297
+ if (new Set(ids).size !== ids.length) return fail(message$1, context.path);
22298
+ return { value: flows };
22299
+ });
22309
22300
  /**
22310
- * Validates an issue.
22311
- * @param value - Untrusted data.
22312
- * @param field - Where the value sits, for the error.
22313
- * @returns The issue.
22301
+ * Where a flow's step is pictured: a route of its own, named `<flow>/<step>`, in a group named
22302
+ * after the flow. Its path is the start page with the step as its fragment, unique among routes.
22303
+ * @param flow - The flow.
22304
+ * @param step - One of its steps.
22305
+ * @returns The step's route.
22314
22306
  */
22315
- function parseBeadIssue(value, field = "issue") {
22316
- let input = object$1(value, field);
22317
- let string = (key, max) => text$4(input[key], `${field}.${key}`, max);
22318
- let optional = (key, max) => nullableText(input[key], `${field}.${key}`, max);
22319
- let count = (key) => nullableInteger(input[key], `${field}.${key}`);
22320
- let date = (key) => nullableTimestamp(input[key], `${field}.${key}`);
22307
+ function flowStepRoute(flow, step) {
22321
22308
  return {
22322
- id: identifier$1(input.id, `${field}.id`),
22323
- title: string("title"),
22324
- status: identifier$1(input.status, `${field}.status`),
22325
- priority: integer(input.priority, `${field}.priority`, 4),
22326
- type: identifier$1(input.type, `${field}.type`),
22327
- description: string("description"),
22328
- design: string("design"),
22329
- acceptanceCriteria: string("acceptanceCriteria"),
22330
- notes: string("notes"),
22331
- assignee: optional("assignee"),
22332
- owner: optional("owner"),
22333
- createdBy: optional("createdBy"),
22334
- createdAt: timestamp(input.createdAt, `${field}.createdAt`),
22335
- updatedAt: timestamp(input.updatedAt, `${field}.updatedAt`),
22336
- startedAt: date("startedAt"),
22337
- closedAt: date("closedAt"),
22338
- closeReason: optional("closeReason"),
22339
- dueAt: date("dueAt"),
22340
- deferUntil: date("deferUntil"),
22341
- estimatedMinutes: count("estimatedMinutes"),
22342
- externalRef: optional("externalRef"),
22343
- specId: optional("specId"),
22344
- labels: unique(array$1(input.labels, `${field}.labels`, (item, path) => text$4(item, path, 512), 1e3), (item) => item, `${field}.labels`),
22345
- parentId: input.parentId == null ? null : identifier$1(input.parentId, `${field}.parentId`),
22346
- dependencies: array$1(input.dependencies, `${field}.dependencies`, dependency),
22347
- dependents: array$1(input.dependents, `${field}.dependents`, dependency),
22348
- dependencyCount: count("dependencyCount"),
22349
- dependentCount: count("dependentCount"),
22350
- commentCount: count("commentCount"),
22351
- epicTotalChildren: count("epicTotalChildren"),
22352
- epicClosedChildren: count("epicClosedChildren"),
22353
- epicCloseable: input.epicCloseable == null ? null : boolean$2(input.epicCloseable, `${field}.epicCloseable`),
22354
- revision: optional("revision", 256),
22355
- metadata: metadata(input.metadata, `${field}.metadata`),
22356
- contentLoaded: boolean$2(input.contentLoaded, `${field}.contentLoaded`)
22309
+ id: `${flow.id}/${step.id}`,
22310
+ path: `${flow.start.replace(/#.*$/, "")}#${flow.id}-${step.id}`,
22311
+ title: step.title,
22312
+ group: flow.title,
22313
+ flow: {
22314
+ id: flow.id,
22315
+ step: step.id
22316
+ }
22357
22317
  };
22358
22318
  }
22359
- /**
22360
- * Validates a connection state.
22361
- * @param value - Untrusted data.
22362
- * @returns The connection.
22363
- */
22364
- function parseBeadsConnection(value) {
22365
- let input = object$1(value, "connection");
22366
- if (input.state === "ready") {
22367
- let project = object$1(input.project, "connection.project");
22368
- if (input.schemaVersion !== 1) throw new BeadsValidationError("connection.schemaVersion");
22369
- return {
22370
- state: "ready",
22371
- version: text$4(input.version, "connection.version", 128),
22372
- schemaVersion: 1,
22373
- project: {
22374
- name: text$4(project.name, "project.name", 512),
22375
- prefix: nullableText(project.prefix, "project.prefix", 512),
22376
- backend: text$4(project.backend, "project.backend", 128),
22377
- mode: nullableText(project.mode, "project.mode", 128),
22378
- redirected: boolean$2(project.redirected, "project.redirected"),
22379
- worktree: boolean$2(project.worktree, "project.worktree")
22380
- }
22381
- };
22319
+ //#endregion
22320
+ //#region ../project/src/lib/logo.ts
22321
+ /** The image types a logo may be, by extension. */
22322
+ var LOGO_TYPES = Object.freeze({
22323
+ ".svg": "image/svg+xml",
22324
+ ".png": "image/png",
22325
+ ".webp": "image/webp",
22326
+ ".jpg": "image/jpeg",
22327
+ ".jpeg": "image/jpeg",
22328
+ ".gif": "image/gif",
22329
+ ".ico": "image/x-icon"
22330
+ });
22331
+ var logoMessage = "logo must be an image path inside the project (.svg, .png, .webp, .jpg, .gif or .ico), or { light, dark }.";
22332
+ function image(value) {
22333
+ if (typeof value !== "string" || value.length > 1024 || value.includes("\0")) return null;
22334
+ let path = value.trim().replaceAll("\\", "/");
22335
+ if (!path || path.startsWith("/") || /^[a-z]:/i.test(path)) return null;
22336
+ if (path.split("/").includes("..")) return null;
22337
+ let extension = path.slice(path.lastIndexOf(".")).toLowerCase();
22338
+ return Object.hasOwn(LOGO_TYPES, extension) ? path : null;
22339
+ }
22340
+ /** A logo in its one resolved form, or null for Bud's own mark. */
22341
+ var BudLogoSchema = createSchema((value, context) => {
22342
+ if (value === void 0) return { value: null };
22343
+ let light = image(typeof value === "object" && value ? value.light : value);
22344
+ let raw = typeof value === "object" && value ? value.dark : void 0;
22345
+ let dark = raw === void 0 ? null : image(raw);
22346
+ let keys = typeof value === "object" && value ? Object.keys(value) : [];
22347
+ if (light && (raw === void 0 || dark) && keys.every((key) => key === "light" || key === "dark")) return { value: {
22348
+ light,
22349
+ dark
22350
+ } };
22351
+ return fail(logoMessage, context.path);
22352
+ });
22353
+ //#endregion
22354
+ //#region ../project/src/lib/theme.ts
22355
+ /**
22356
+ * Bud's colors and shadows a theme may set, as `--bud-<name>` in its styles. Everything else
22357
+ * Bud derives from them.
22358
+ */
22359
+ var THEME_TOKENS = Object.freeze([
22360
+ "background",
22361
+ "shell",
22362
+ "main",
22363
+ "canvas",
22364
+ "panel",
22365
+ "board",
22366
+ "editor",
22367
+ "editor-tabs",
22368
+ "foreground",
22369
+ "muted",
22370
+ "primary",
22371
+ "primary-hover",
22372
+ "primary-foreground",
22373
+ "border",
22374
+ "tab-divider",
22375
+ "hover",
22376
+ "selected",
22377
+ "control",
22378
+ "control-active",
22379
+ "control-selected",
22380
+ "card",
22381
+ "menu",
22382
+ "menu-hover",
22383
+ "card-frame",
22384
+ "card-surface",
22385
+ "card-border",
22386
+ "card-frame-border",
22387
+ "card-badge",
22388
+ "overlay-shadow",
22389
+ "tooltip",
22390
+ "board-control",
22391
+ "board-control-hover",
22392
+ "board-card",
22393
+ "control-elevation",
22394
+ "board-elevation",
22395
+ "board-card-elevation",
22396
+ "card-elevation",
22397
+ "board-card-lift",
22398
+ "frame-control",
22399
+ "modal-elevation",
22400
+ "segment-elevation",
22401
+ "control-open-elevation",
22402
+ "board-segment",
22403
+ "scrollbar",
22404
+ "focus",
22405
+ "scrollbar-hover",
22406
+ "tooltip-border",
22407
+ "tooltip-shadow"
22408
+ ]);
22409
+ var themeMessage = `theme must be { light, dark }, each mapping Bud's tokens (${THEME_TOKENS.slice(0, 3).join(", ")}, …) to CSS colors or shadows.`;
22410
+ function cssValue(value) {
22411
+ if (typeof value !== "string") return null;
22412
+ let trimmed = value.trim();
22413
+ if (!trimmed || trimmed.length > 300) return null;
22414
+ if (!/^[#a-zA-Z0-9(),.%\s/+-]+$/.test(trimmed) || /url\s*\(/i.test(trimmed)) return null;
22415
+ return trimmed;
22416
+ }
22417
+ function colors(value) {
22418
+ if (value === void 0) return {};
22419
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
22420
+ let result = {};
22421
+ for (let [name, raw] of Object.entries(value)) {
22422
+ let checked = cssValue(raw);
22423
+ if (!THEME_TOKENS.includes(name) || checked === null) return null;
22424
+ result[name] = checked;
22382
22425
  }
22383
- if (![
22384
- "unavailable",
22385
- "not-initialized",
22386
- "unsupported-version"
22387
- ].includes(input.state)) throw new BeadsValidationError("connection.state");
22388
- return {
22389
- state: input.state,
22390
- version: nullableText(input.version, "connection.version", 128),
22391
- message: text$4(input.message, "connection.message", 1e3)
22392
- };
22426
+ return Object.freeze(result);
22393
22427
  }
22394
- /**
22395
- * Validates a status definition.
22396
- * @param value - Untrusted data.
22397
- * @param field - Where the value sits, for the error.
22398
- * @returns The status.
22399
- */
22400
- function parseBeadStatus(value, field = "status") {
22401
- let input = object$1(value, field);
22402
- return {
22403
- name: identifier$1(input.name, `${field}.name`),
22404
- category: nullableText(input.category, `${field}.category`, 128),
22405
- description: text$4(input.description, `${field}.description`),
22406
- custom: boolean$2(input.custom, `${field}.custom`)
22428
+ /** A theme in its one resolved form; Bud's own colors when absent. */
22429
+ var BudThemeSchema = createSchema((value, context) => {
22430
+ if (value === void 0) return { value: {
22431
+ light: {},
22432
+ dark: {}
22433
+ } };
22434
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail(themeMessage, context.path);
22435
+ let { light, dark, ...rest } = value;
22436
+ let resolved = {
22437
+ light: colors(light),
22438
+ dark: colors(dark)
22407
22439
  };
22408
- }
22440
+ if (Object.keys(rest).length || !resolved.light || !resolved.dark) return fail(themeMessage, context.path);
22441
+ return { value: {
22442
+ light: resolved.light,
22443
+ dark: resolved.dark
22444
+ } };
22445
+ });
22409
22446
  /**
22410
- * Validates an issue type definition.
22411
- * @param value - Untrusted data.
22412
- * @param field - Where the value sits, for the error.
22413
- * @returns The type.
22447
+ * The theme as styles that follow Bud's own, so its tokens win in each color scheme.
22448
+ * @param theme - The resolved theme.
22449
+ * @returns A `<style>` element, or an empty string for Bud's own colors.
22414
22450
  */
22415
- function parseBeadType(value, field = "type") {
22416
- let input = object$1(value, field);
22417
- if (![
22418
- "core",
22419
- "system",
22420
- "custom"
22421
- ].includes(input.kind)) throw new BeadsValidationError(`${field}.kind`);
22422
- return {
22423
- name: identifier$1(input.name, `${field}.name`),
22424
- description: text$4(input.description, `${field}.description`),
22425
- kind: input.kind
22451
+ function themeStyle(theme) {
22452
+ let block = (selector, values) => {
22453
+ let entries = Object.entries(values);
22454
+ if (!entries.length) return "";
22455
+ return `${selector}{${entries.map(([name, value]) => `--bud-${name}:${value};`).join("")}}`;
22426
22456
  };
22457
+ let css = block(":root:not([data-theme='dark'])", theme.light) + block(":root[data-theme='dark']", theme.dark);
22458
+ return css ? `<style data-bud-theme>${css}</style>` : "";
22427
22459
  }
22428
- /**
22429
- * Validates a board snapshot, which holds no rows unless its connection is ready.
22430
- * @param value - Untrusted data.
22431
- * @returns The snapshot.
22432
- */
22433
- function parseBeadsSnapshot(value) {
22434
- let input = object$1(value, "snapshot");
22435
- let connection = parseBeadsConnection(input.connection);
22436
- let issues = unique(array$1(input.issues, "snapshot.issues", parseBeadIssue, MAX_BEADS), (issue) => issue.id, "snapshot.issues");
22437
- let readyIds = unique(array$1(input.readyIds, "snapshot.readyIds", identifier$1, MAX_BEADS), (id) => id, "snapshot.readyIds");
22438
- let statuses = unique(array$1(input.statuses, "snapshot.statuses", parseBeadStatus, 1e3), (item) => item.name, "snapshot.statuses");
22439
- let types = unique(array$1(input.types, "snapshot.types", parseBeadType, 1e3), (item) => item.name, "snapshot.types");
22440
- let truncated = boolean$2(input.truncated, "snapshot.truncated");
22441
- if (connection.state !== "ready" && (issues.length || readyIds.length || statuses.length || types.length || truncated)) throw new BeadsValidationError("snapshot.connection");
22442
- return {
22443
- connection,
22444
- issues,
22445
- readyIds,
22446
- statuses,
22447
- types,
22448
- truncated
22449
- };
22460
+ //#endregion
22461
+ //#region ../project/src/lib/branches.ts
22462
+ var message = "branches must be { main?, build?: { install?, build?, serve? } } with a branch name and nonempty shell commands.";
22463
+ function branch(value) {
22464
+ return typeof value === "string" && value.length <= 255 && /^[A-Za-z0-9._/-]+$/.test(value) && !value.startsWith("-") && !value.startsWith("/") && !value.endsWith("/") && !value.endsWith(".lock") && !value.includes("..") && !value.includes("//") ? value : null;
22450
22465
  }
22451
- /**
22452
- * Validates an issue comment.
22453
- * @param value - Untrusted data.
22454
- * @param field - Where the value sits, for the error.
22455
- * @returns The comment.
22456
- */
22457
- function parseBeadDiscussion(value, field = "discussion") {
22458
- let input = object$1(value, field);
22459
- return {
22460
- id: identifier$1(input.id, `${field}.id`),
22461
- issueId: identifier$1(input.issueId, `${field}.issueId`),
22462
- author: text$4(input.author, `${field}.author`),
22463
- body: text$4(input.body, `${field}.body`),
22464
- createdAt: timestamp(input.createdAt, `${field}.createdAt`)
22465
- };
22466
+ function command(value) {
22467
+ return typeof value === "string" && value.trim() && value.length <= 4096 && !value.includes("\0") ? value.trim() : null;
22466
22468
  }
22467
- /**
22468
- * Validates an issue's version at one commit.
22469
- * @param value - Untrusted data.
22470
- * @param field - Where the value sits, for the error.
22471
- * @returns The version.
22472
- */
22473
- function parseBeadHistory(value, field = "history") {
22474
- let input = object$1(value, field);
22475
- return {
22476
- commit: identifier$1(input.commit, `${field}.commit`),
22477
- author: text$4(input.author, `${field}.author`),
22478
- createdAt: timestamp(input.createdAt, `${field}.createdAt`),
22479
- issue: input.issue == null ? null : parseBeadIssue(input.issue, `${field}.issue`)
22469
+ /** The branch settings, defaults filled in. */
22470
+ var BudBranchesSchema = createSchema((value, context) => {
22471
+ let empty = {
22472
+ main: null,
22473
+ install: null,
22474
+ build: null,
22475
+ serve: null
22480
22476
  };
22481
- }
22477
+ if (value === void 0) return { value: empty };
22478
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fail(message, context.path);
22479
+ let input = value;
22480
+ let build = input.build ?? {};
22481
+ if (!build || typeof build !== "object" || Array.isArray(build) || Object.keys(input).some((key) => key !== "main" && key !== "build")) return fail(message, context.path);
22482
+ let steps = build;
22483
+ if (Object.keys(steps).some((key) => ![
22484
+ "install",
22485
+ "build",
22486
+ "serve"
22487
+ ].includes(key))) return fail(message, context.path);
22488
+ let resolved = {
22489
+ main: input.main === void 0 ? null : branch(input.main),
22490
+ install: steps.install === void 0 ? null : command(steps.install),
22491
+ build: steps.build === void 0 ? null : command(steps.build),
22492
+ serve: steps.serve === void 0 ? null : command(steps.serve)
22493
+ };
22494
+ if (input.main !== void 0 && !resolved.main || [
22495
+ "install",
22496
+ "build",
22497
+ "serve"
22498
+ ].some((key) => steps[key] !== void 0 && !resolved[key])) return fail(message, context.path);
22499
+ return { value: resolved };
22500
+ });
22501
+ //#endregion
22502
+ //#region ../project/src/lib/storage.ts
22482
22503
  /**
22483
- * Validates an event log entry.
22484
- * @param value - Untrusted data.
22485
- * @param field - Where the value sits, for the error.
22486
- * @returns The event.
22504
+ * A project module named in the config: a ./ path inside the project, outside node_modules.
22505
+ * @param name - The config option, for the error message.
22506
+ * @param server - Whether it runs on the server (.server.ts) rather than in the browser.
22507
+ * @param message - What to say instead of the default when it is invalid.
22508
+ * @returns A schema for the module's path.
22487
22509
  */
22488
- function parseBeadEvent(value, field = "event") {
22489
- let input = object$1(value, field);
22490
- return {
22491
- id: identifier$1(input.id, `${field}.id`),
22492
- issueId: identifier$1(input.issueId, `${field}.issueId`),
22493
- type: identifier$1(input.type, `${field}.type`),
22494
- actor: text$4(input.actor, `${field}.actor`),
22495
- oldValue: nullableText(input.oldValue, `${field}.oldValue`),
22496
- newValue: nullableText(input.newValue, `${field}.newValue`),
22497
- comment: nullableText(input.comment, `${field}.comment`),
22498
- createdAt: timestamp(input.createdAt, `${field}.createdAt`)
22499
- };
22510
+ function modulePath(name, server, message) {
22511
+ let extension = server ? /\.server\.ts$/ : /\.(?:ts|tsx|js|jsx)$/;
22512
+ return described(string().refine((value) => value.length <= 1024 && value.startsWith("./") && !/[\u0000-\u001f\u007f\\?#%]/.test(value) && extension.test(value) && (server || !/\.server\.[cm]?[jt]sx?$/.test(value)) && !value.slice(2).split("/").some((part) => !part || part === "." || part === ".." || part === "node_modules")), message ?? (server ? `${name} must be a built-in adapter or a ./ module ending in .server.ts inside the project.` : `${name} must be bud:beads or a ./ browser module ending in .ts, .tsx, .js or .jsx inside the project.`));
22500
22513
  }
22501
- function section$3(value, field, parser, max) {
22502
- let input = object$1(value, field);
22503
- let items = array$1(input.items, `${field}.items`, parser, max);
22504
- let truncated = boolean$2(input.truncated, `${field}.truncated`);
22505
- if (input.state === "available") return {
22506
- state: "available",
22507
- items,
22508
- truncated
22509
- };
22510
- if (input.state === "unsupported" && items.length === 0 && truncated === false) return {
22511
- state: "unsupported",
22512
- items: [],
22513
- truncated: false
22514
- };
22515
- throw new BeadsValidationError(`${field}.state`);
22516
- }
22517
- /**
22518
- * Validates an issue's detail, whose sections must all belong to that issue.
22519
- * @param value - Untrusted data.
22520
- * @returns The detail.
22521
- */
22522
- function parseBeadDetail(value) {
22523
- let input = object$1(value, "detail");
22524
- let connection = parseBeadsConnection(input.connection);
22525
- if (connection.state !== "ready") throw new BeadsValidationError("detail.connection");
22526
- let issue = parseBeadIssue(input.issue);
22527
- let discussion = section$3(input.discussion, "discussion", parseBeadDiscussion, 200);
22528
- let history = section$3(input.history, "history", parseBeadHistory, 50);
22529
- let events = section$3(input.events, "events", parseBeadEvent, 50);
22530
- if (!issue.contentLoaded || discussion.items.some((entry) => entry.issueId !== issue.id) || events.items.some((entry) => entry.issueId !== issue.id) || history.items.some((entry) => entry.issue && entry.issue.id !== issue.id)) throw new BeadsValidationError("detail.issue");
22531
- return {
22532
- connection,
22533
- issue,
22534
- discussion,
22535
- history,
22536
- events
22537
- };
22514
+ var builtInStores = enum_([
22515
+ false,
22516
+ "bud:beads",
22517
+ "bud:memory",
22518
+ "bud:git-comments"
22519
+ ]);
22520
+ function store(kind) {
22521
+ let module = modulePath(`storage.${kind}`, true);
22522
+ return defaulted(check(select$1((value) => value === false || value === "bud:beads" || value === "bud:memory" || value === "bud:git-comments" ? builtInStores : module), (value) => kind === "work" && value === "bud:git-comments" ? "storage.work cannot use bud:git-comments; it only stores threads." : void 0), "bud:beads");
22538
22523
  }
22524
+ /** The selected work and discussion stores, with Beads as the default for both. */
22525
+ var BudStorageSchema = defaulted(settings("storage", {
22526
+ work: store("work"),
22527
+ threads: store("threads")
22528
+ }), () => ({
22529
+ work: "bud:beads",
22530
+ threads: "bud:beads"
22531
+ }));
22532
+ /** Task presentation, chosen separately from its storage. */
22533
+ var BudDesignsSchema = defaulted(settings("designs", {
22534
+ tasks: defaulted(select$1((value) => value === "bud:beads" ? enum_(["bud:beads"]) : modulePath("designs.tasks", false)), "bud:beads"),
22535
+ pages: defaulted(enum_(["path", "title"]), "path")
22536
+ }), () => ({
22537
+ tasks: "bud:beads",
22538
+ pages: "path"
22539
+ }));
22539
22540
  //#endregion
22540
- //#region ../beads/src/lib/actions.ts
22541
- var dependencyTypes = [
22542
- "blocks",
22543
- "tracks",
22544
- "related",
22545
- "discovered-from",
22546
- "until",
22547
- "caused-by",
22548
- "validates",
22549
- "relates-to",
22550
- "supersedes"
22551
- ];
22552
- var fields$2 = [
22553
- "title",
22554
- "type",
22555
- "status",
22556
- "priority",
22557
- "assignee",
22558
- "labels",
22559
- "parentId",
22560
- "description",
22561
- "design",
22562
- "acceptanceCriteria",
22563
- "notes",
22564
- "estimatedMinutes",
22565
- "externalRef",
22566
- "specId",
22567
- "dueAt",
22568
- "deferUntil",
22569
- "metadata"
22570
- ];
22571
- function strict$1(value, name, allowed) {
22572
- let input = object$1(value, name);
22573
- for (let key of Object.keys(input)) if (!allowed.includes(key)) throw new BeadsValidationError(`${name}.${key}`);
22574
- return input;
22575
- }
22576
- function nonempty(value, field, max) {
22577
- let result = text$4(value, field, max).trim();
22578
- if (!result) throw new BeadsValidationError(field);
22579
- return result;
22541
+ //#region ../project/src/lib/picker.ts
22542
+ var builtIn = /* @__PURE__ */ new Set([
22543
+ "bud:dom",
22544
+ "bud:react",
22545
+ "bud:react-grab"
22546
+ ]);
22547
+ var projectPicker = modulePath("picker", false, "picker must be bud:dom, bud:react, bud:react-grab or a ./ browser module ending in .ts, .tsx, .js or .jsx inside the project.");
22548
+ /** The picker, with Bud's DOM picker as the default. */
22549
+ var BudPickerSchema = createSchema((value, context) => {
22550
+ if (value === void 0) return { value: {
22551
+ name: "bud:dom",
22552
+ module: null
22553
+ } };
22554
+ if (typeof value === "string" && builtIn.has(value)) return { value: {
22555
+ name: value,
22556
+ module: null
22557
+ } };
22558
+ let result = projectPicker["~run"](value, context);
22559
+ return result.issues ? result : { value: {
22560
+ name: result.value,
22561
+ module: result.value
22562
+ } };
22563
+ });
22564
+ //#endregion
22565
+ //#region ../project/src/lib/previews.ts
22566
+ var readyMessage = "ready must be a CSS selector, { selector } or { expression }.";
22567
+ /** A readiness condition; a string is a selector. */
22568
+ var PreviewReadySchema = createSchema((value, context) => {
22569
+ let ready = typeof value === "string" ? { selector: value } : value;
22570
+ if (ready && typeof ready === "object" && !Array.isArray(ready)) {
22571
+ let entries = Object.entries(ready);
22572
+ let [key, text] = entries[0] ?? [];
22573
+ if (entries.length === 1 && (key === "selector" || key === "expression") && typeof text === "string" && text.trim() && text.length <= 4096) return { value: key === "selector" ? { selector: text.trim() } : { expression: text.trim() } };
22574
+ }
22575
+ return fail(readyMessage, context.path);
22576
+ });
22577
+ var defaultThemes = () => ["light", "dark"];
22578
+ var defaultViewports = () => [{
22579
+ id: "desktop",
22580
+ name: "Desktop",
22581
+ width: 1440,
22582
+ height: 900,
22583
+ mobile: false
22584
+ }];
22585
+ var viewportsMessage = "previews.viewports must list up to 4 { name, width, height?, mobile? } with distinct names, widths from 240 to 3840 and heights from 240 to 4000.";
22586
+ function viewportId(name) {
22587
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
22580
22588
  }
22589
+ var ViewportsSchema = createSchema((value, context) => {
22590
+ if (!Array.isArray(value) || value.length === 0 || value.length > 4) return fail(viewportsMessage, context.path);
22591
+ let viewports = [];
22592
+ for (let entry of value) {
22593
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return fail(viewportsMessage, context.path);
22594
+ let { name, width, height, mobile, ...rest } = entry;
22595
+ let size = (pixels, from, to) => typeof pixels === "number" && Number.isInteger(pixels) && pixels >= from && pixels <= to;
22596
+ if (Object.keys(rest).length || typeof name !== "string" || !name.trim() || name.length > 40 || !viewportId(name) || !size(width, 240, 3840) || height !== void 0 && !size(height, 240, 4e3) || mobile !== void 0 && typeof mobile !== "boolean") return fail(viewportsMessage, context.path);
22597
+ let phone = mobile ?? width < 800;
22598
+ viewports.push({
22599
+ id: viewportId(name),
22600
+ name: name.trim(),
22601
+ width,
22602
+ height: height ?? (phone ? Math.round(width * 844 / 390) : 900),
22603
+ mobile: phone
22604
+ });
22605
+ }
22606
+ if (new Set(viewports.map((viewport) => viewport.id)).size !== viewports.length) return fail(viewportsMessage, context.path);
22607
+ return { value: viewports };
22608
+ });
22609
+ /** The preview settings, with their defaults filled in. */
22610
+ var BudPreviewsSchema = defaulted(settings("previews", {
22611
+ scale: fallback(described(number().refine((scale) => Number.isInteger(scale) && scale >= 1 && scale <= 3), "previews.scale must be 1, 2 or 3."), () => 2),
22612
+ themes: fallback(described(array(enum_(["light", "dark"])).refine((themes) => themes.length > 0 && new Set(themes).size === themes.length), "previews.themes must list light, dark or both, once each."), defaultThemes),
22613
+ viewports: fallback(ViewportsSchema, defaultViewports),
22614
+ ready: fallback(PreviewReadySchema, () => null),
22615
+ readyTimeout: fallback(described(number().refine((ms) => Number.isInteger(ms) && ms >= 1e3 && ms <= 6e5), "previews.readyTimeout must be whole milliseconds from 1000 to 600000."), () => 6e4),
22616
+ css: fallback(described(string().refine((css) => css.length <= 2e4 && !css.includes("<")), "previews.css must be CSS text without <, up to 20000 characters."), () => "")
22617
+ }), () => ({
22618
+ scale: 2,
22619
+ themes: defaultThemes(),
22620
+ viewports: defaultViewports(),
22621
+ ready: null,
22622
+ readyTimeout: 6e4,
22623
+ css: ""
22624
+ }));
22625
+ //#endregion
22626
+ //#region ../project/src/lib/screen-routes.ts
22581
22627
  /**
22582
- * Validates an update's fields, rejecting unknown and oversized ones.
22583
- * @param value - Untrusted data.
22584
- * @returns The patch.
22628
+ * Validates a screen's route map without importing its component module.
22629
+ * @param routes - A Remix route map shared with the screen's definition.
22630
+ * @param path - The configured screen's static path.
22631
+ * @returns Named patterns confined to the screen's path.
22585
22632
  */
22586
- function parseBeadPatch(value) {
22587
- let input = strict$1(value, "patch", fields$2);
22588
- let patch = {};
22589
- if (!Object.keys(input).length) throw new BeadsValidationError("patch");
22590
- if ("title" in input) patch.title = nonempty(input.title, "patch.title", 1e3);
22591
- if ("type" in input) patch.type = identifier$1(input.type, "patch.type");
22592
- if ("status" in input) patch.status = identifier$1(input.status, "patch.status");
22593
- if ("priority" in input) patch.priority = integer(input.priority, "patch.priority", 4);
22594
- if ("assignee" in input) patch.assignee = text$4(input.assignee, "patch.assignee", 256).trim();
22595
- if ("labels" in input) patch.labels = unique(array$1(input.labels, "patch.labels", (value, field) => {
22596
- let label = nonempty(value, field, 128);
22597
- if (/[\r\n,]/.test(label)) throw new BeadsValidationError(field);
22598
- return label;
22599
- }, 100), (label) => label, "patch.labels");
22600
- if ("parentId" in input) patch.parentId = input.parentId === null ? null : identifier$1(input.parentId, "patch.parentId");
22601
- for (let key of [
22602
- "description",
22603
- "design",
22604
- "acceptanceCriteria",
22605
- "notes"
22606
- ]) if (key in input) patch[key] = text$4(input[key], `patch.${key}`, 6e4);
22607
- if ("estimatedMinutes" in input) patch.estimatedMinutes = integer(input.estimatedMinutes, "patch.estimatedMinutes", 1e6);
22608
- for (let key of ["externalRef", "specId"]) if (key in input) patch[key] = text$4(input[key], `patch.${key}`, 2048);
22609
- for (let key of ["dueAt", "deferUntil"]) if (key in input) patch[key] = text$4(input[key], `patch.${key}`, 128);
22610
- if ("metadata" in input) {
22611
- let source = object$1(input.metadata, "patch.metadata");
22612
- if (JSON.stringify(source).length > 32e3) throw new BeadsValidationError("patch.metadata");
22613
- patch.metadata = metadata(source, "patch.metadata");
22633
+ function resolveScreenRoutes(routes, path) {
22634
+ let result = [];
22635
+ let seen = /* @__PURE__ */ new Set();
22636
+ function visit(value, prefix) {
22637
+ if (!value || typeof value !== "object" || Array.isArray(value) || seen.has(value)) throw new Error("Screen routes must be a Remix route map.");
22638
+ seen.add(value);
22639
+ for (let [key, entry] of Object.entries(value)) {
22640
+ let name = prefix ? `${prefix}.${key}` : key;
22641
+ if (!entry || typeof entry !== "object") throw new Error(`Screen route ${name} must be a Remix route.`);
22642
+ if (!("pattern" in entry)) {
22643
+ visit(entry, name);
22644
+ continue;
22645
+ }
22646
+ if (!["GET", "ANY"].includes(entry.method) || typeof entry.href !== "function" || typeof entry.pattern?.source !== "string") throw new Error(`Screen route ${name} must be a GET route.`);
22647
+ let pattern = RoutePattern.parse(entry.pattern.source);
22648
+ let parts = pattern.toJSON();
22649
+ let pathname = `/${parts.pathname}`;
22650
+ if (parts.protocol || parts.hostname || parts.port || parts.pathname.split("/").some((segment) => /^\.{1,2}$/.test(decodeURIComponent(segment))) || pathname !== path && !pathname.startsWith(`${path}/`) && !pathname.startsWith(`${path}(/`)) throw new Error(`Screen route ${name} must stay under ${path}.`);
22651
+ result.push({
22652
+ name,
22653
+ pattern: pattern.source
22654
+ });
22655
+ }
22656
+ seen.delete(value);
22614
22657
  }
22615
- if (new TextEncoder().encode(JSON.stringify(patch)).byteLength > 122880) throw new BeadsValidationError("patch");
22616
- return patch;
22658
+ visit(routes, "");
22659
+ if (!result.length) throw new Error("Screen routes must contain at least one route.");
22660
+ return result;
22617
22661
  }
22662
+ //#endregion
22663
+ //#region ../project/src/lib/screens.ts
22618
22664
  /**
22619
- * Validates a new issue's fields.
22620
- * @param value - Untrusted data.
22621
- * @returns The fields, with a title.
22665
+ * Bud's browser of your live pages, with their comments.
22666
+ * @returns The built-in screen, ready to relabel by spreading.
22622
22667
  */
22623
- function parseBeadCreate(value) {
22624
- let patch = parseBeadPatch(value);
22625
- if (!patch.title) throw new BeadsValidationError("create.title");
22668
+ function pages() {
22626
22669
  return {
22627
- ...patch,
22628
- title: patch.title
22670
+ id: "pages",
22671
+ label: "Pages",
22672
+ icon: "browser",
22673
+ path: "/pages",
22674
+ module: "bud:pages"
22629
22675
  };
22630
22676
  }
22631
22677
  /**
22632
- * Validates a change to an issue.
22633
- * @param value - Untrusted data.
22634
- * @returns The action.
22678
+ * Bud's canvas of every route's screenshot.
22679
+ * @returns The built-in screen, ready to relabel by spreading.
22635
22680
  */
22636
- function parseBeadAction(value) {
22637
- let input = object$1(value, "action");
22638
- let expectedRevision = nonempty(input.expectedRevision, "action.expectedRevision", 256);
22639
- switch (input.kind) {
22640
- case "update":
22641
- strict$1(input, "action", [
22642
- "kind",
22643
- "expectedRevision",
22644
- "patch"
22645
- ]);
22646
- return {
22647
- kind: input.kind,
22648
- expectedRevision,
22649
- patch: parseBeadPatch(input.patch)
22650
- };
22651
- case "claim":
22652
- strict$1(input, "action", ["kind", "expectedRevision"]);
22653
- return {
22654
- kind: input.kind,
22655
- expectedRevision
22656
- };
22657
- case "close":
22658
- case "reopen":
22659
- strict$1(input, "action", [
22660
- "kind",
22661
- "expectedRevision",
22662
- "reason"
22663
- ]);
22664
- return {
22665
- kind: input.kind,
22666
- expectedRevision,
22667
- reason: text$4(input.reason ?? "", "action.reason", 4e3)
22668
- };
22669
- case "add-dependency":
22670
- strict$1(input, "action", [
22671
- "kind",
22672
- "expectedRevision",
22673
- "targetId",
22674
- "type"
22675
- ]);
22676
- if (!dependencyTypes.includes(input.type)) throw new BeadsValidationError("action.type");
22677
- return {
22678
- kind: input.kind,
22679
- expectedRevision,
22680
- targetId: identifier$1(input.targetId, "action.targetId"),
22681
- type: input.type
22682
- };
22683
- case "remove-dependency":
22684
- strict$1(input, "action", [
22685
- "kind",
22686
- "expectedRevision",
22687
- "targetId"
22688
- ]);
22689
- return {
22690
- kind: input.kind,
22691
- expectedRevision,
22692
- targetId: identifier$1(input.targetId, "action.targetId")
22693
- };
22694
- case "comment": {
22695
- strict$1(input, "action", [
22696
- "kind",
22697
- "expectedRevision",
22698
- "body"
22699
- ]);
22700
- let body = nonempty(input.body, "action.body", 6e4);
22701
- if (new TextEncoder().encode(body).byteLength > 122880) throw new BeadsValidationError("action.body");
22702
- return {
22703
- kind: input.kind,
22704
- expectedRevision,
22705
- body
22706
- };
22707
- }
22708
- default: throw new BeadsValidationError("action.kind");
22709
- }
22681
+ function canvas() {
22682
+ return {
22683
+ id: "canvas",
22684
+ label: "Canvas",
22685
+ icon: "grid",
22686
+ path: "/canvas",
22687
+ module: "bud:canvas"
22688
+ };
22710
22689
  }
22711
22690
  /**
22712
- * Validates a sync request.
22713
- * @param value - Untrusted data.
22714
- * @returns The request.
22691
+ * Bud's Beads task board.
22692
+ * @returns The built-in screen, ready to relabel by spreading.
22715
22693
  */
22716
- function parseBeadSync(value) {
22717
- let input = strict$1(value, "sync", ["direction"]);
22718
- if (input.direction !== "pull" && input.direction !== "push") throw new BeadsValidationError("sync.direction");
22719
- return { direction: input.direction };
22694
+ function tasks() {
22695
+ return {
22696
+ id: "tasks",
22697
+ label: "Tasks",
22698
+ icon: "tasks",
22699
+ path: "/tasks",
22700
+ module: "bud:tasks"
22701
+ };
22720
22702
  }
22721
- //#endregion
22722
- //#region ../beads/src/lib/protocol.ts
22723
22703
  /**
22724
- * Turns a CLI or export issue record into the browser's issue. CLI and export records share this
22725
- * one boundary before reaching the browser DTOs.
22726
- * @param value - The record.
22727
- * @param contentLoaded - Whether it holds the issue's long content.
22728
- * @returns The issue.
22704
+ * Bud's file browser and review.
22705
+ * @returns The built-in screen, ready to relabel by spreading.
22729
22706
  */
22730
- function parseCliIssue(value, contentLoaded = false) {
22731
- let input = object$1(value, "issue");
22732
- let id = identifier$1(input.id, "issue.id");
22733
- function dependencies(field) {
22734
- return array$1(input[field] ?? [], `issue.${field}`, (entry, path) => {
22735
- let edge = object$1(entry, path);
22736
- if ("depends_on_id" in edge) {
22737
- if (identifier$1(edge.issue_id, `${path}.issue_id`) !== id) throw new Error("Dependency belongs to a different issue.");
22738
- return {
22739
- id: identifier$1(edge.depends_on_id, `${path}.depends_on_id`),
22740
- type: identifier$1(edge.type, `${path}.type`),
22741
- title: null,
22742
- status: null,
22743
- priority: null,
22744
- issueType: null
22745
- };
22746
- }
22747
- return {
22748
- id: edge.id,
22749
- type: edge.dependency_type,
22750
- title: edge.title ?? null,
22751
- status: edge.status ?? null,
22752
- priority: edge.priority ?? null,
22753
- issueType: edge.issue_type ?? null
22754
- };
22755
- });
22707
+ function files() {
22708
+ return {
22709
+ id: "files",
22710
+ label: "Files",
22711
+ icon: "files",
22712
+ path: "/files",
22713
+ module: "bud:files"
22714
+ };
22715
+ }
22716
+ /** The screens shown when the config doesn't list any. */
22717
+ var defaultScreens = [
22718
+ pages(),
22719
+ canvas(),
22720
+ tasks(),
22721
+ files()
22722
+ ];
22723
+ var builtIns$1 = new Map(defaultScreens.map((screen) => [screen.module, screen]));
22724
+ /** First path segments owned by Bud's routes, its development server and its public assets. */
22725
+ var reservedScreenPaths = [
22726
+ "/comments",
22727
+ ...defaultScreens.map((screen) => screen.path),
22728
+ "/api",
22729
+ "/health",
22730
+ "/__bud",
22731
+ "/assets",
22732
+ "/app",
22733
+ "/src",
22734
+ "/node_modules",
22735
+ "/agents",
22736
+ "/brand",
22737
+ "/fonts",
22738
+ "/frameworks",
22739
+ "/licenses"
22740
+ ];
22741
+ var ScreenSchema = check(described(any().refine((value) => !!value && typeof value === "object"), "Each screen must be an object."), ({ id, label, icon = "layers", module, path }) => {
22742
+ if (typeof id !== "string" || !/^[a-z][a-z0-9-]{0,31}$/.test(id)) return "Screen ids are 1-32 lowercase letters, digits and hyphens.";
22743
+ if (typeof label !== "string" || !label.trim()) return `Screen ${id} needs a label.`;
22744
+ if (typeof icon !== "string" || !icon) return `Screen ${id} needs an icon name.`;
22745
+ if (typeof module !== "string") return `Screen ${id} needs a module.`;
22746
+ let builtIn = builtIns$1.get(module);
22747
+ path ??= builtIn?.path ?? `/${id}`;
22748
+ if (builtIn) {
22749
+ if (id !== builtIn.id || path !== builtIn.path) return `${module} keeps id "${builtIn.id}" and path "${builtIn.path}".`;
22750
+ return;
22756
22751
  }
22757
- return parseBeadIssue({
22758
- id,
22759
- title: input.title,
22760
- status: input.status,
22761
- priority: input.priority,
22762
- type: input.issue_type,
22763
- description: input.description ?? "",
22764
- design: input.design ?? "",
22765
- acceptanceCriteria: input.acceptance_criteria ?? "",
22766
- notes: input.notes ?? "",
22767
- assignee: input.assignee ?? null,
22768
- owner: input.owner ?? null,
22769
- createdBy: input.created_by ?? null,
22770
- createdAt: input.created_at,
22771
- updatedAt: input.updated_at,
22772
- startedAt: input.started_at ?? null,
22773
- closedAt: input.closed_at ?? null,
22774
- closeReason: input.close_reason ?? null,
22775
- dueAt: input.due_at ?? null,
22776
- deferUntil: input.defer_until ?? null,
22777
- estimatedMinutes: input.estimated_minutes ?? null,
22778
- externalRef: input.external_ref ?? null,
22779
- specId: input.spec_id ?? null,
22780
- labels: input.labels ?? [],
22781
- parentId: input.parent ?? null,
22782
- dependencies: dependencies("dependencies"),
22783
- dependents: dependencies("dependents"),
22784
- dependencyCount: input.dependency_count ?? null,
22785
- dependentCount: input.dependent_count ?? null,
22786
- commentCount: input.comment_count ?? null,
22787
- epicTotalChildren: input.epic_total_children ?? null,
22788
- epicClosedChildren: input.epic_closed_children ?? null,
22789
- epicCloseable: input.epic_closeable ?? null,
22790
- revision: input.revision ?? null,
22791
- metadata: input.metadata ?? null,
22792
- contentLoaded
22752
+ if (!/^\.\/[^\0\\]+\.(?:ts|tsx|js|jsx)$/.test(module)) return `Screen ${id} must be a ./ module ending in .ts, .tsx, .js or .jsx.`;
22753
+ if (typeof path !== "string" || !/^\/[a-z0-9][a-z0-9-]*$/.test(path)) return `Screen ${id} needs one static path segment, such as /${id}.`;
22754
+ if (defaultScreens.some((screen) => screen.id === id)) return `Screen id "${id}" is reserved for a built-in screen.`;
22755
+ if (reservedScreenPaths.includes(path)) return `Bud reserves the path ${path}.`;
22756
+ });
22757
+ /** Bud's navigation in order; file and icon checks need the app and happen at startup. */
22758
+ var BudScreensSchema = defaulted(delegate(described(array(any()).refine((screens) => screens.length > 0), "screens must list at least one screen."), (screens) => {
22759
+ let ids = /* @__PURE__ */ new Set();
22760
+ let paths = /* @__PURE__ */ new Set();
22761
+ return screens.map((value) => {
22762
+ let input = parseConfig(ScreenSchema, value);
22763
+ let { id, label, icon = "layers", module } = input;
22764
+ let path = input.path ?? builtIns$1.get(module)?.path ?? `/${id}`;
22765
+ if (ids.has(id) || paths.has(path)) throw new Error("Screen ids and paths must be unique.");
22766
+ ids.add(id);
22767
+ paths.add(path);
22768
+ if (input.routes !== void 0) resolveScreenRoutes(input.routes, path);
22769
+ return {
22770
+ id,
22771
+ label,
22772
+ icon,
22773
+ path,
22774
+ module,
22775
+ ...input.routes === void 0 ? {} : { routes: input.routes }
22776
+ };
22793
22777
  });
22778
+ }), () => defaultScreens.map((screen) => ({ ...screen })));
22779
+ //#endregion
22780
+ //#region ../project/src/lib/config.ts
22781
+ var objectLike = (value) => !!value && typeof value === "object";
22782
+ function section$3(message, shape) {
22783
+ let schema = object(shape);
22784
+ return createSchema((value, context) => objectLike(value) ? schema["~run"](Array.isArray(value) ? { ...value } : value, context) : fail(message, context.path));
22794
22785
  }
22795
- /**
22796
- * Turns the CLI's issue comments into discussion entries with unique IDs.
22797
- * @param value - The CLI's comment list, or nothing.
22798
- * @returns The comments.
22799
- */
22800
- function parseCliDiscussion(value) {
22801
- return unique(array$1(value ?? [], "comments", (entry, field) => {
22802
- let item = object$1(entry, field);
22803
- return parseBeadDiscussion({
22786
+ var ConfigFields = check(section$3("Bud config must export an object.", {
22787
+ routes: described(array(any()).refine((routes) => routes.length > 0), "bud.config must define at least one route."),
22788
+ name: optional(described(string().refine((name) => !!name.trim()), "name must be a nonempty string.")),
22789
+ target: optional(section$3("target must be an object.", {
22790
+ origin: any(),
22791
+ port: any()
22792
+ })),
22793
+ comments: optional(section$3("comments must be an object.", { remote: any() })),
22794
+ port: any(),
22795
+ screens: any(),
22796
+ storage: any(),
22797
+ designs: any(),
22798
+ previews: any(),
22799
+ files: any(),
22800
+ logo: any(),
22801
+ theme: any(),
22802
+ picker: any(),
22803
+ branches: any(),
22804
+ flows: any()
22805
+ }), ({ target }) => {
22806
+ if (target?.port === 0) return "target.port must be a listening port, not zero.";
22807
+ if (target?.origin !== void 0 && (typeof target.origin !== "string" || !target.origin.trim())) return "target.origin must be an HTTP(S) origin.";
22808
+ });
22809
+ var RouteSchema = check(any().transform((input) => typeof input === "string" ? {
22810
+ path: input,
22811
+ title: input === "/" ? "Home" : input
22812
+ } : input), (route) => {
22813
+ if (!route || typeof route.path !== "string" || !route.path.startsWith("/") || route.path.startsWith("//")) return "Canvas routes must be local paths beginning with a single /.";
22814
+ if (new URL(route.path, "http://bud.invalid").origin !== "http://bud.invalid" || route.path.includes("\\")) return "Canvas routes must stay on the target origin.";
22815
+ let id = route.id ?? routePath(route.path);
22816
+ if (typeof id !== "string" || !id.trim()) return "Canvas route IDs and paths must be unique.";
22817
+ if (typeof route.title !== "string" || !route.title.trim()) return "Each route needs a title.";
22818
+ if (route.pattern !== void 0 && (typeof route.pattern !== "string" || !route.pattern.startsWith("/"))) return "A route pattern must be a path beginning with /.";
22819
+ if (route.group !== void 0 && (typeof route.group !== "string" || !route.group.trim() || route.group.length > 120)) return "A route group must be a short name.";
22820
+ if (route.ready !== void 0 && route.ready !== false && !parseSafe(PreviewReadySchema, route.ready).success) return "A route ready must be false, a CSS selector, { selector } or { expression }.";
22821
+ }).transform((route) => {
22822
+ let path = routePath(route.path);
22823
+ return {
22824
+ id: route.id ?? path,
22825
+ path,
22826
+ title: route.title,
22827
+ ...route.pattern === void 0 ? {} : { pattern: route.pattern },
22828
+ ...route.group === void 0 ? {} : { group: route.group.trim() },
22829
+ ...route.ready === void 0 ? {} : { ready: route.ready === false ? null : parseConfig(PreviewReadySchema, route.ready) }
22830
+ };
22831
+ });
22832
+ function routePath(path) {
22833
+ let url = new URL(path, "http://bud.invalid");
22834
+ let search = url.searchParams.toString();
22835
+ return `${url.pathname}${search ? `?${search}` : ""}${url.hash}`;
22836
+ }
22837
+ /** Canvas routes with unique IDs and normalized paths. */
22838
+ var BudRoutesSchema = check(list(RouteSchema, "bud.config must define at least one route.", 1), (routes) => new Set(routes.map((route) => route.id)).size !== routes.length || new Set(routes.map((route) => route.path)).size !== routes.length ? "Canvas route IDs and paths must be unique." : void 0);
22839
+ var port = (value) => value === void 0 || Number.isInteger(value) && value >= 0 && value <= 65535;
22840
+ var remote$1 = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value);
22841
+ /** A project's config, validated and normalized before Bud starts any listeners. */
22842
+ var BudConfigSchema = delegate(check(delegate(check(delegate(check(delegate(ConfigFields, (config) => ({
22843
+ ...config,
22844
+ routes: parseConfig(BudRoutesSchema, config.routes)
22845
+ })), ({ port: companion, target }) => port(companion ?? 0) && port(target?.port) ? void 0 : "Ports must be integers between 0 and 65535."), ({ target, ...config }) => {
22846
+ let origin = target?.origin ?? (target?.port ? `http://127.0.0.1:${target.port}` : void 0);
22847
+ return {
22848
+ ...config,
22849
+ targetOrigin: origin ? normalizeOrigin(origin, "target.origin must be an HTTP(S) origin without a path or credentials.") : origin
22850
+ };
22851
+ }), ({ comments }) => remote$1(comments?.remote ?? "origin") ? void 0 : "comments.remote must be a Git remote name."), (config) => {
22852
+ let flows = parseConfig(BudFlowsSchema, config.flows);
22853
+ let steps = flows.flatMap((flow) => flow.steps.map((step) => flowStepRoute(flow, step)));
22854
+ let home = config.routes.findIndex((route) => route.path === "/") + 1;
22855
+ return {
22856
+ ...config,
22857
+ flows,
22858
+ routes: [
22859
+ ...config.routes.slice(0, home),
22860
+ ...steps,
22861
+ ...config.routes.slice(home)
22862
+ ]
22863
+ };
22864
+ }), ({ routes }) => new Set(routes.map((route) => route.id)).size !== routes.length || new Set(routes.map((route) => route.path)).size !== routes.length ? "A flow step must not reuse a route’s id or path." : void 0), (config) => ({
22865
+ name: config.name ?? "Your project",
22866
+ routes: config.routes,
22867
+ flows: config.flows,
22868
+ port: config.port ?? 0,
22869
+ targetOrigin: config.targetOrigin,
22870
+ remote: config.comments?.remote ?? "origin",
22871
+ screens: parseConfig(BudScreensSchema, config.screens),
22872
+ storage: parseConfig(BudStorageSchema, config.storage),
22873
+ designs: parseConfig(BudDesignsSchema, config.designs),
22874
+ previews: parseConfig(BudPreviewsSchema, config.previews),
22875
+ files: parseConfig(BudFilesSchema, config.files),
22876
+ logo: parseConfig(BudLogoSchema, config.logo),
22877
+ theme: parseConfig(BudThemeSchema, config.theme),
22878
+ picker: parseConfig(BudPickerSchema, config.picker),
22879
+ branches: parseConfig(BudBranchesSchema, config.branches)
22880
+ }));
22881
+ /**
22882
+ * Validates executable project config before starting any listeners.
22883
+ * @param value - The config module's default export.
22884
+ * @returns Normalized route IDs, ports, and URLs.
22885
+ */
22886
+ function resolveBudConfig(value) {
22887
+ return parseConfig(BudConfigSchema, value);
22888
+ }
22889
+ /**
22890
+ * Returns a bare HTTP(S) origin, rejecting credentials, paths, queries and fragments.
22891
+ * @param origin - The URL to check.
22892
+ * @param message - The error's message when it is not a bare origin.
22893
+ * @returns The URL's origin.
22894
+ */
22895
+ function normalizeOrigin(origin, message) {
22896
+ let url = new URL(origin);
22897
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error(message);
22898
+ return url.origin;
22899
+ }
22900
+ //#endregion
22901
+ //#region ../beads/src/lib/contracts.ts
22902
+ var MAX_BEADS = 1e3;
22903
+ //#endregion
22904
+ //#region ../beads/src/lib/validation.ts
22905
+ /** Beads returned or was sent data of the wrong shape; `field` names where. */
22906
+ var BeadsValidationError = class extends Error {
22907
+ field;
22908
+ constructor(field) {
22909
+ super(`Invalid Beads data at ${field}.`);
22910
+ this.name = "BeadsValidationError";
22911
+ this.field = field;
22912
+ }
22913
+ };
22914
+ /**
22915
+ * Checks that a value is a JSON object.
22916
+ * @param value - Untrusted data.
22917
+ * @param field - Where the value sits, for the error.
22918
+ * @returns The object.
22919
+ */
22920
+ function object$1(value, field) {
22921
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new BeadsValidationError(field);
22922
+ return value;
22923
+ }
22924
+ /**
22925
+ * Checks a string without NUL characters within a length limit.
22926
+ * @param value - Untrusted data.
22927
+ * @param field - Where the value sits, for the error.
22928
+ * @param max - The most characters it may have.
22929
+ * @returns The string.
22930
+ */
22931
+ function text$3(value, field, max = 262144) {
22932
+ if (typeof value !== "string" || value.length > max || value.includes("\0")) throw new BeadsValidationError(field);
22933
+ return value;
22934
+ }
22935
+ /**
22936
+ * Checks an ID, name or commit made of letters, digits, dots, underscores and hyphens.
22937
+ * @param value - Untrusted data.
22938
+ * @param field - Where the value sits, for the error.
22939
+ * @returns The identifier.
22940
+ */
22941
+ function identifier$1(value, field) {
22942
+ let result = text$3(value, field, 512);
22943
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(result)) throw new BeadsValidationError(field);
22944
+ return result;
22945
+ }
22946
+ /**
22947
+ * Checks an optional string.
22948
+ * @param value - Untrusted data.
22949
+ * @param field - Where the value sits, for the error.
22950
+ * @param max - The most characters it may have.
22951
+ * @returns The string, or null when it is missing.
22952
+ */
22953
+ function nullableText(value, field, max) {
22954
+ return value == null ? null : text$3(value, field, max);
22955
+ }
22956
+ /**
22957
+ * Checks a nonnegative safe integer.
22958
+ * @param value - Untrusted data.
22959
+ * @param field - Where the value sits, for the error.
22960
+ * @param max - The largest value it may have.
22961
+ * @returns The integer.
22962
+ */
22963
+ function integer(value, field, max = Number.MAX_SAFE_INTEGER) {
22964
+ if (!Number.isSafeInteger(value) || value < 0 || value > max) throw new BeadsValidationError(field);
22965
+ return value;
22966
+ }
22967
+ /**
22968
+ * Checks an optional nonnegative integer.
22969
+ * @param value - Untrusted data.
22970
+ * @param field - Where the value sits, for the error.
22971
+ * @param max - The largest value it may have.
22972
+ * @returns The integer, or null when it is missing.
22973
+ */
22974
+ function nullableInteger(value, field, max) {
22975
+ return value == null ? null : integer(value, field, max);
22976
+ }
22977
+ /**
22978
+ * Checks a boolean.
22979
+ * @param value - Untrusted data.
22980
+ * @param field - Where the value sits, for the error.
22981
+ * @returns The boolean.
22982
+ */
22983
+ function boolean$2(value, field) {
22984
+ if (typeof value !== "boolean") throw new BeadsValidationError(field);
22985
+ return value;
22986
+ }
22987
+ /**
22988
+ * Checks an ISO 8601 timestamp with a time zone.
22989
+ * @param value - Untrusted data.
22990
+ * @param field - Where the value sits, for the error.
22991
+ * @returns The timestamp as given.
22992
+ */
22993
+ function timestamp(value, field) {
22994
+ let result = text$3(value, field, 64);
22995
+ if (!/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(result) || !Number.isFinite(Date.parse(result))) throw new BeadsValidationError(field);
22996
+ return result;
22997
+ }
22998
+ /**
22999
+ * Checks an optional ISO 8601 timestamp.
23000
+ * @param value - Untrusted data.
23001
+ * @param field - Where the value sits, for the error.
23002
+ * @returns The timestamp, or null when it is missing.
23003
+ */
23004
+ function nullableTimestamp(value, field) {
23005
+ return value == null ? null : timestamp(value, field);
23006
+ }
23007
+ /**
23008
+ * Checks a bounded array and each of its entries.
23009
+ * @param value - Untrusted data.
23010
+ * @param field - Where the value sits, for the error.
23011
+ * @param parse - Checks one entry.
23012
+ * @param max - The most entries it may have.
23013
+ * @returns The parsed entries.
23014
+ */
23015
+ function array$1(value, field, parse, max = 1e4) {
23016
+ if (!Array.isArray(value) || value.length > max) throw new BeadsValidationError(field);
23017
+ return value.map((entry, i) => parse(entry, `${field}[${i}]`));
23018
+ }
23019
+ /**
23020
+ * Checks that no two values share a key.
23021
+ * @param values - The values to check.
23022
+ * @param key - Gives each value's key.
23023
+ * @param field - Where the values sit, for the error.
23024
+ * @returns The values.
23025
+ */
23026
+ function unique(values, key, field) {
23027
+ if (new Set(values.map(key)).size !== values.length) throw new BeadsValidationError(field);
23028
+ return values;
23029
+ }
23030
+ /**
23031
+ * Checks JSON metadata, bounding its depth and its total number of values.
23032
+ * @param value - Untrusted data.
23033
+ * @param field - Where the value sits, for the error.
23034
+ * @param depth - How deep this value is nested.
23035
+ * @param budget - The values left to check, shared across the whole tree.
23036
+ * @returns The metadata.
23037
+ */
23038
+ function metadata(value, field, depth = 0, budget = { remaining: 1e4 }) {
23039
+ if (depth > 12 || --budget.remaining < 0) throw new BeadsValidationError(field);
23040
+ if (value === null || typeof value === "boolean") return value;
23041
+ if (typeof value === "number" && Number.isFinite(value)) return value;
23042
+ if (typeof value === "string") return text$3(value, field);
23043
+ if (Array.isArray(value)) return value.map((entry, index) => metadata(entry, `${field}[${index}]`, depth + 1, budget));
23044
+ let input = object$1(value, field);
23045
+ return Object.fromEntries(Object.entries(input).map(([key, entry]) => [text$3(key, field, 512), metadata(entry, `${field}.${key}`, depth + 1, budget)]));
23046
+ }
23047
+ function dependency(value, field) {
23048
+ let input = object$1(value, field);
23049
+ return {
23050
+ id: identifier$1(input.id, `${field}.id`),
23051
+ type: identifier$1(input.type, `${field}.type`),
23052
+ title: nullableText(input.title, `${field}.title`),
23053
+ status: nullableText(input.status, `${field}.status`, 128),
23054
+ priority: nullableInteger(input.priority, `${field}.priority`, 4),
23055
+ issueType: nullableText(input.issueType, `${field}.issueType`, 128)
23056
+ };
23057
+ }
23058
+ /**
23059
+ * Validates an issue.
23060
+ * @param value - Untrusted data.
23061
+ * @param field - Where the value sits, for the error.
23062
+ * @returns The issue.
23063
+ */
23064
+ function parseBeadIssue(value, field = "issue") {
23065
+ let input = object$1(value, field);
23066
+ let string = (key, max) => text$3(input[key], `${field}.${key}`, max);
23067
+ let optional = (key, max) => nullableText(input[key], `${field}.${key}`, max);
23068
+ let count = (key) => nullableInteger(input[key], `${field}.${key}`);
23069
+ let date = (key) => nullableTimestamp(input[key], `${field}.${key}`);
23070
+ return {
23071
+ id: identifier$1(input.id, `${field}.id`),
23072
+ title: string("title"),
23073
+ status: identifier$1(input.status, `${field}.status`),
23074
+ priority: integer(input.priority, `${field}.priority`, 4),
23075
+ type: identifier$1(input.type, `${field}.type`),
23076
+ description: string("description"),
23077
+ design: string("design"),
23078
+ acceptanceCriteria: string("acceptanceCriteria"),
23079
+ notes: string("notes"),
23080
+ assignee: optional("assignee"),
23081
+ owner: optional("owner"),
23082
+ createdBy: optional("createdBy"),
23083
+ createdAt: timestamp(input.createdAt, `${field}.createdAt`),
23084
+ updatedAt: timestamp(input.updatedAt, `${field}.updatedAt`),
23085
+ startedAt: date("startedAt"),
23086
+ closedAt: date("closedAt"),
23087
+ closeReason: optional("closeReason"),
23088
+ dueAt: date("dueAt"),
23089
+ deferUntil: date("deferUntil"),
23090
+ estimatedMinutes: count("estimatedMinutes"),
23091
+ externalRef: optional("externalRef"),
23092
+ specId: optional("specId"),
23093
+ labels: unique(array$1(input.labels, `${field}.labels`, (item, path) => text$3(item, path, 512), 1e3), (item) => item, `${field}.labels`),
23094
+ parentId: input.parentId == null ? null : identifier$1(input.parentId, `${field}.parentId`),
23095
+ dependencies: array$1(input.dependencies, `${field}.dependencies`, dependency),
23096
+ dependents: array$1(input.dependents, `${field}.dependents`, dependency),
23097
+ dependencyCount: count("dependencyCount"),
23098
+ dependentCount: count("dependentCount"),
23099
+ commentCount: count("commentCount"),
23100
+ epicTotalChildren: count("epicTotalChildren"),
23101
+ epicClosedChildren: count("epicClosedChildren"),
23102
+ epicCloseable: input.epicCloseable == null ? null : boolean$2(input.epicCloseable, `${field}.epicCloseable`),
23103
+ revision: optional("revision", 256),
23104
+ metadata: metadata(input.metadata, `${field}.metadata`),
23105
+ contentLoaded: boolean$2(input.contentLoaded, `${field}.contentLoaded`)
23106
+ };
23107
+ }
23108
+ /**
23109
+ * Validates a connection state.
23110
+ * @param value - Untrusted data.
23111
+ * @returns The connection.
23112
+ */
23113
+ function parseBeadsConnection(value) {
23114
+ let input = object$1(value, "connection");
23115
+ if (input.state === "ready") {
23116
+ let project = object$1(input.project, "connection.project");
23117
+ if (input.schemaVersion !== 1) throw new BeadsValidationError("connection.schemaVersion");
23118
+ return {
23119
+ state: "ready",
23120
+ version: text$3(input.version, "connection.version", 128),
23121
+ schemaVersion: 1,
23122
+ project: {
23123
+ name: text$3(project.name, "project.name", 512),
23124
+ prefix: nullableText(project.prefix, "project.prefix", 512),
23125
+ backend: text$3(project.backend, "project.backend", 128),
23126
+ mode: nullableText(project.mode, "project.mode", 128),
23127
+ redirected: boolean$2(project.redirected, "project.redirected"),
23128
+ worktree: boolean$2(project.worktree, "project.worktree")
23129
+ }
23130
+ };
23131
+ }
23132
+ if (![
23133
+ "unavailable",
23134
+ "not-initialized",
23135
+ "unsupported-version"
23136
+ ].includes(input.state)) throw new BeadsValidationError("connection.state");
23137
+ return {
23138
+ state: input.state,
23139
+ version: nullableText(input.version, "connection.version", 128),
23140
+ message: text$3(input.message, "connection.message", 1e3)
23141
+ };
23142
+ }
23143
+ /**
23144
+ * Validates a status definition.
23145
+ * @param value - Untrusted data.
23146
+ * @param field - Where the value sits, for the error.
23147
+ * @returns The status.
23148
+ */
23149
+ function parseBeadStatus(value, field = "status") {
23150
+ let input = object$1(value, field);
23151
+ return {
23152
+ name: identifier$1(input.name, `${field}.name`),
23153
+ category: nullableText(input.category, `${field}.category`, 128),
23154
+ description: text$3(input.description, `${field}.description`),
23155
+ custom: boolean$2(input.custom, `${field}.custom`)
23156
+ };
23157
+ }
23158
+ /**
23159
+ * Validates an issue type definition.
23160
+ * @param value - Untrusted data.
23161
+ * @param field - Where the value sits, for the error.
23162
+ * @returns The type.
23163
+ */
23164
+ function parseBeadType(value, field = "type") {
23165
+ let input = object$1(value, field);
23166
+ if (![
23167
+ "core",
23168
+ "system",
23169
+ "custom"
23170
+ ].includes(input.kind)) throw new BeadsValidationError(`${field}.kind`);
23171
+ return {
23172
+ name: identifier$1(input.name, `${field}.name`),
23173
+ description: text$3(input.description, `${field}.description`),
23174
+ kind: input.kind
23175
+ };
23176
+ }
23177
+ /**
23178
+ * Validates a board snapshot, which holds no rows unless its connection is ready.
23179
+ * @param value - Untrusted data.
23180
+ * @returns The snapshot.
23181
+ */
23182
+ function parseBeadsSnapshot(value) {
23183
+ let input = object$1(value, "snapshot");
23184
+ let connection = parseBeadsConnection(input.connection);
23185
+ let issues = unique(array$1(input.issues, "snapshot.issues", parseBeadIssue, MAX_BEADS), (issue) => issue.id, "snapshot.issues");
23186
+ let readyIds = unique(array$1(input.readyIds, "snapshot.readyIds", identifier$1, MAX_BEADS), (id) => id, "snapshot.readyIds");
23187
+ let statuses = unique(array$1(input.statuses, "snapshot.statuses", parseBeadStatus, 1e3), (item) => item.name, "snapshot.statuses");
23188
+ let types = unique(array$1(input.types, "snapshot.types", parseBeadType, 1e3), (item) => item.name, "snapshot.types");
23189
+ let truncated = boolean$2(input.truncated, "snapshot.truncated");
23190
+ if (connection.state !== "ready" && (issues.length || readyIds.length || statuses.length || types.length || truncated)) throw new BeadsValidationError("snapshot.connection");
23191
+ return {
23192
+ connection,
23193
+ issues,
23194
+ readyIds,
23195
+ statuses,
23196
+ types,
23197
+ truncated
23198
+ };
23199
+ }
23200
+ /**
23201
+ * Validates an issue comment.
23202
+ * @param value - Untrusted data.
23203
+ * @param field - Where the value sits, for the error.
23204
+ * @returns The comment.
23205
+ */
23206
+ function parseBeadDiscussion(value, field = "discussion") {
23207
+ let input = object$1(value, field);
23208
+ return {
23209
+ id: identifier$1(input.id, `${field}.id`),
23210
+ issueId: identifier$1(input.issueId, `${field}.issueId`),
23211
+ author: text$3(input.author, `${field}.author`),
23212
+ body: text$3(input.body, `${field}.body`),
23213
+ createdAt: timestamp(input.createdAt, `${field}.createdAt`)
23214
+ };
23215
+ }
23216
+ /**
23217
+ * Validates an issue's version at one commit.
23218
+ * @param value - Untrusted data.
23219
+ * @param field - Where the value sits, for the error.
23220
+ * @returns The version.
23221
+ */
23222
+ function parseBeadHistory(value, field = "history") {
23223
+ let input = object$1(value, field);
23224
+ return {
23225
+ commit: identifier$1(input.commit, `${field}.commit`),
23226
+ author: text$3(input.author, `${field}.author`),
23227
+ createdAt: timestamp(input.createdAt, `${field}.createdAt`),
23228
+ issue: input.issue == null ? null : parseBeadIssue(input.issue, `${field}.issue`)
23229
+ };
23230
+ }
23231
+ /**
23232
+ * Validates an event log entry.
23233
+ * @param value - Untrusted data.
23234
+ * @param field - Where the value sits, for the error.
23235
+ * @returns The event.
23236
+ */
23237
+ function parseBeadEvent(value, field = "event") {
23238
+ let input = object$1(value, field);
23239
+ return {
23240
+ id: identifier$1(input.id, `${field}.id`),
23241
+ issueId: identifier$1(input.issueId, `${field}.issueId`),
23242
+ type: identifier$1(input.type, `${field}.type`),
23243
+ actor: text$3(input.actor, `${field}.actor`),
23244
+ oldValue: nullableText(input.oldValue, `${field}.oldValue`),
23245
+ newValue: nullableText(input.newValue, `${field}.newValue`),
23246
+ comment: nullableText(input.comment, `${field}.comment`),
23247
+ createdAt: timestamp(input.createdAt, `${field}.createdAt`)
23248
+ };
23249
+ }
23250
+ function section$2(value, field, parser, max) {
23251
+ let input = object$1(value, field);
23252
+ let items = array$1(input.items, `${field}.items`, parser, max);
23253
+ let truncated = boolean$2(input.truncated, `${field}.truncated`);
23254
+ if (input.state === "available") return {
23255
+ state: "available",
23256
+ items,
23257
+ truncated
23258
+ };
23259
+ if (input.state === "unsupported" && items.length === 0 && truncated === false) return {
23260
+ state: "unsupported",
23261
+ items: [],
23262
+ truncated: false
23263
+ };
23264
+ throw new BeadsValidationError(`${field}.state`);
23265
+ }
23266
+ /**
23267
+ * Validates an issue's detail, whose sections must all belong to that issue.
23268
+ * @param value - Untrusted data.
23269
+ * @returns The detail.
23270
+ */
23271
+ function parseBeadDetail(value) {
23272
+ let input = object$1(value, "detail");
23273
+ let connection = parseBeadsConnection(input.connection);
23274
+ if (connection.state !== "ready") throw new BeadsValidationError("detail.connection");
23275
+ let issue = parseBeadIssue(input.issue);
23276
+ let discussion = section$2(input.discussion, "discussion", parseBeadDiscussion, 200);
23277
+ let history = section$2(input.history, "history", parseBeadHistory, 50);
23278
+ let events = section$2(input.events, "events", parseBeadEvent, 50);
23279
+ if (!issue.contentLoaded || discussion.items.some((entry) => entry.issueId !== issue.id) || events.items.some((entry) => entry.issueId !== issue.id) || history.items.some((entry) => entry.issue && entry.issue.id !== issue.id)) throw new BeadsValidationError("detail.issue");
23280
+ return {
23281
+ connection,
23282
+ issue,
23283
+ discussion,
23284
+ history,
23285
+ events
23286
+ };
23287
+ }
23288
+ //#endregion
23289
+ //#region ../beads/src/lib/actions.ts
23290
+ var dependencyTypes = [
23291
+ "blocks",
23292
+ "tracks",
23293
+ "related",
23294
+ "discovered-from",
23295
+ "until",
23296
+ "caused-by",
23297
+ "validates",
23298
+ "relates-to",
23299
+ "supersedes"
23300
+ ];
23301
+ var fields$2 = [
23302
+ "title",
23303
+ "type",
23304
+ "status",
23305
+ "priority",
23306
+ "assignee",
23307
+ "labels",
23308
+ "parentId",
23309
+ "description",
23310
+ "design",
23311
+ "acceptanceCriteria",
23312
+ "notes",
23313
+ "estimatedMinutes",
23314
+ "externalRef",
23315
+ "specId",
23316
+ "dueAt",
23317
+ "deferUntil",
23318
+ "metadata"
23319
+ ];
23320
+ function strict$1(value, name, allowed) {
23321
+ let input = object$1(value, name);
23322
+ for (let key of Object.keys(input)) if (!allowed.includes(key)) throw new BeadsValidationError(`${name}.${key}`);
23323
+ return input;
23324
+ }
23325
+ function nonempty(value, field, max) {
23326
+ let result = text$3(value, field, max).trim();
23327
+ if (!result) throw new BeadsValidationError(field);
23328
+ return result;
23329
+ }
23330
+ /**
23331
+ * Validates an update's fields, rejecting unknown and oversized ones.
23332
+ * @param value - Untrusted data.
23333
+ * @returns The patch.
23334
+ */
23335
+ function parseBeadPatch(value) {
23336
+ let input = strict$1(value, "patch", fields$2);
23337
+ let patch = {};
23338
+ if (!Object.keys(input).length) throw new BeadsValidationError("patch");
23339
+ if ("title" in input) patch.title = nonempty(input.title, "patch.title", 1e3);
23340
+ if ("type" in input) patch.type = identifier$1(input.type, "patch.type");
23341
+ if ("status" in input) patch.status = identifier$1(input.status, "patch.status");
23342
+ if ("priority" in input) patch.priority = integer(input.priority, "patch.priority", 4);
23343
+ if ("assignee" in input) patch.assignee = text$3(input.assignee, "patch.assignee", 256).trim();
23344
+ if ("labels" in input) patch.labels = unique(array$1(input.labels, "patch.labels", (value, field) => {
23345
+ let label = nonempty(value, field, 128);
23346
+ if (/[\r\n,]/.test(label)) throw new BeadsValidationError(field);
23347
+ return label;
23348
+ }, 100), (label) => label, "patch.labels");
23349
+ if ("parentId" in input) patch.parentId = input.parentId === null ? null : identifier$1(input.parentId, "patch.parentId");
23350
+ for (let key of [
23351
+ "description",
23352
+ "design",
23353
+ "acceptanceCriteria",
23354
+ "notes"
23355
+ ]) if (key in input) patch[key] = text$3(input[key], `patch.${key}`, 6e4);
23356
+ if ("estimatedMinutes" in input) patch.estimatedMinutes = integer(input.estimatedMinutes, "patch.estimatedMinutes", 1e6);
23357
+ for (let key of ["externalRef", "specId"]) if (key in input) patch[key] = text$3(input[key], `patch.${key}`, 2048);
23358
+ for (let key of ["dueAt", "deferUntil"]) if (key in input) patch[key] = text$3(input[key], `patch.${key}`, 128);
23359
+ if ("metadata" in input) {
23360
+ let source = object$1(input.metadata, "patch.metadata");
23361
+ if (JSON.stringify(source).length > 32e3) throw new BeadsValidationError("patch.metadata");
23362
+ patch.metadata = metadata(source, "patch.metadata");
23363
+ }
23364
+ if (new TextEncoder().encode(JSON.stringify(patch)).byteLength > 122880) throw new BeadsValidationError("patch");
23365
+ return patch;
23366
+ }
23367
+ /**
23368
+ * Validates a new issue's fields.
23369
+ * @param value - Untrusted data.
23370
+ * @returns The fields, with a title.
23371
+ */
23372
+ function parseBeadCreate(value) {
23373
+ let patch = parseBeadPatch(value);
23374
+ if (!patch.title) throw new BeadsValidationError("create.title");
23375
+ return {
23376
+ ...patch,
23377
+ title: patch.title
23378
+ };
23379
+ }
23380
+ /**
23381
+ * Validates a change to an issue.
23382
+ * @param value - Untrusted data.
23383
+ * @returns The action.
23384
+ */
23385
+ function parseBeadAction(value) {
23386
+ let input = object$1(value, "action");
23387
+ let expectedRevision = nonempty(input.expectedRevision, "action.expectedRevision", 256);
23388
+ switch (input.kind) {
23389
+ case "update":
23390
+ strict$1(input, "action", [
23391
+ "kind",
23392
+ "expectedRevision",
23393
+ "patch"
23394
+ ]);
23395
+ return {
23396
+ kind: input.kind,
23397
+ expectedRevision,
23398
+ patch: parseBeadPatch(input.patch)
23399
+ };
23400
+ case "claim":
23401
+ strict$1(input, "action", ["kind", "expectedRevision"]);
23402
+ return {
23403
+ kind: input.kind,
23404
+ expectedRevision
23405
+ };
23406
+ case "close":
23407
+ case "reopen":
23408
+ strict$1(input, "action", [
23409
+ "kind",
23410
+ "expectedRevision",
23411
+ "reason"
23412
+ ]);
23413
+ return {
23414
+ kind: input.kind,
23415
+ expectedRevision,
23416
+ reason: text$3(input.reason ?? "", "action.reason", 4e3)
23417
+ };
23418
+ case "add-dependency":
23419
+ strict$1(input, "action", [
23420
+ "kind",
23421
+ "expectedRevision",
23422
+ "targetId",
23423
+ "type"
23424
+ ]);
23425
+ if (!dependencyTypes.includes(input.type)) throw new BeadsValidationError("action.type");
23426
+ return {
23427
+ kind: input.kind,
23428
+ expectedRevision,
23429
+ targetId: identifier$1(input.targetId, "action.targetId"),
23430
+ type: input.type
23431
+ };
23432
+ case "remove-dependency":
23433
+ strict$1(input, "action", [
23434
+ "kind",
23435
+ "expectedRevision",
23436
+ "targetId"
23437
+ ]);
23438
+ return {
23439
+ kind: input.kind,
23440
+ expectedRevision,
23441
+ targetId: identifier$1(input.targetId, "action.targetId")
23442
+ };
23443
+ case "comment": {
23444
+ strict$1(input, "action", [
23445
+ "kind",
23446
+ "expectedRevision",
23447
+ "body"
23448
+ ]);
23449
+ let body = nonempty(input.body, "action.body", 6e4);
23450
+ if (new TextEncoder().encode(body).byteLength > 122880) throw new BeadsValidationError("action.body");
23451
+ return {
23452
+ kind: input.kind,
23453
+ expectedRevision,
23454
+ body
23455
+ };
23456
+ }
23457
+ default: throw new BeadsValidationError("action.kind");
23458
+ }
23459
+ }
23460
+ /**
23461
+ * Validates a sync request.
23462
+ * @param value - Untrusted data.
23463
+ * @returns The request.
23464
+ */
23465
+ function parseBeadSync(value) {
23466
+ let input = strict$1(value, "sync", ["direction"]);
23467
+ if (input.direction !== "pull" && input.direction !== "push") throw new BeadsValidationError("sync.direction");
23468
+ return { direction: input.direction };
23469
+ }
23470
+ //#endregion
23471
+ //#region ../beads/src/lib/protocol.ts
23472
+ /**
23473
+ * Turns a CLI or export issue record into the browser's issue. CLI and export records share this
23474
+ * one boundary before reaching the browser DTOs.
23475
+ * @param value - The record.
23476
+ * @param contentLoaded - Whether it holds the issue's long content.
23477
+ * @returns The issue.
23478
+ */
23479
+ function parseCliIssue(value, contentLoaded = false) {
23480
+ let input = object$1(value, "issue");
23481
+ let id = identifier$1(input.id, "issue.id");
23482
+ function dependencies(field) {
23483
+ return array$1(input[field] ?? [], `issue.${field}`, (entry, path) => {
23484
+ let edge = object$1(entry, path);
23485
+ if ("depends_on_id" in edge) {
23486
+ if (identifier$1(edge.issue_id, `${path}.issue_id`) !== id) throw new Error("Dependency belongs to a different issue.");
23487
+ return {
23488
+ id: identifier$1(edge.depends_on_id, `${path}.depends_on_id`),
23489
+ type: identifier$1(edge.type, `${path}.type`),
23490
+ title: null,
23491
+ status: null,
23492
+ priority: null,
23493
+ issueType: null
23494
+ };
23495
+ }
23496
+ return {
23497
+ id: edge.id,
23498
+ type: edge.dependency_type,
23499
+ title: edge.title ?? null,
23500
+ status: edge.status ?? null,
23501
+ priority: edge.priority ?? null,
23502
+ issueType: edge.issue_type ?? null
23503
+ };
23504
+ });
23505
+ }
23506
+ return parseBeadIssue({
23507
+ id,
23508
+ title: input.title,
23509
+ status: input.status,
23510
+ priority: input.priority,
23511
+ type: input.issue_type,
23512
+ description: input.description ?? "",
23513
+ design: input.design ?? "",
23514
+ acceptanceCriteria: input.acceptance_criteria ?? "",
23515
+ notes: input.notes ?? "",
23516
+ assignee: input.assignee ?? null,
23517
+ owner: input.owner ?? null,
23518
+ createdBy: input.created_by ?? null,
23519
+ createdAt: input.created_at,
23520
+ updatedAt: input.updated_at,
23521
+ startedAt: input.started_at ?? null,
23522
+ closedAt: input.closed_at ?? null,
23523
+ closeReason: input.close_reason ?? null,
23524
+ dueAt: input.due_at ?? null,
23525
+ deferUntil: input.defer_until ?? null,
23526
+ estimatedMinutes: input.estimated_minutes ?? null,
23527
+ externalRef: input.external_ref ?? null,
23528
+ specId: input.spec_id ?? null,
23529
+ labels: input.labels ?? [],
23530
+ parentId: input.parent ?? null,
23531
+ dependencies: dependencies("dependencies"),
23532
+ dependents: dependencies("dependents"),
23533
+ dependencyCount: input.dependency_count ?? null,
23534
+ dependentCount: input.dependent_count ?? null,
23535
+ commentCount: input.comment_count ?? null,
23536
+ epicTotalChildren: input.epic_total_children ?? null,
23537
+ epicClosedChildren: input.epic_closed_children ?? null,
23538
+ epicCloseable: input.epic_closeable ?? null,
23539
+ revision: input.revision ?? null,
23540
+ metadata: input.metadata ?? null,
23541
+ contentLoaded
23542
+ });
23543
+ }
23544
+ /**
23545
+ * Turns the CLI's issue comments into discussion entries with unique IDs.
23546
+ * @param value - The CLI's comment list, or nothing.
23547
+ * @returns The comments.
23548
+ */
23549
+ function parseCliDiscussion(value) {
23550
+ return unique(array$1(value ?? [], "comments", (entry, field) => {
23551
+ let item = object$1(entry, field);
23552
+ return parseBeadDiscussion({
22804
23553
  id: item.id,
22805
23554
  issueId: item.issue_id,
22806
23555
  author: item.author,
@@ -23280,7 +24029,7 @@ function payload$1(value) {
23280
24029
  return /^[{[]/.test(value.trim()) ? null : value;
23281
24030
  }
23282
24031
  }
23283
- function text$3(value) {
24032
+ function text$2(value) {
23284
24033
  return typeof value === "string" || typeof value === "number" ? String(value).replace(/\s+/g, " ").trim() : "";
23285
24034
  }
23286
24035
  function field(value, key) {
@@ -23289,7 +24038,7 @@ function field(value, key) {
23289
24038
  function changed$1(key, before, after) {
23290
24039
  let name = fields[key] ?? label$1(key).toLowerCase();
23291
24040
  if (key === "labels") {
23292
- let labels = (value) => Array.isArray(value) ? value.map(text$3).filter(Boolean) : text$3(value).split(",").map((v) => v.trim()).filter(Boolean);
24041
+ let labels = (value) => Array.isArray(value) ? value.map(text$2).filter(Boolean) : text$2(value).split(",").map((v) => v.trim()).filter(Boolean);
23293
24042
  let oldLabels = labels(before);
23294
24043
  let newLabels = labels(after);
23295
24044
  let added = newLabels.filter((value) => !oldLabels.includes(value));
@@ -23305,7 +24054,7 @@ function changed$1(key, before, after) {
23305
24054
  "metadata"
23306
24055
  ].includes(key)) return `updated ${name}`;
23307
24056
  let display = (value) => {
23308
- let result = text$3(value);
24057
+ let result = text$2(value);
23309
24058
  if (!result) return key === "assignee" ? "Unassigned" : "None";
23310
24059
  if (key === "priority" && /^\d$/.test(result)) return `P${result}`;
23311
24060
  return key === "status" || key === "issue_type" ? label$1(result) : result;
@@ -23326,7 +24075,7 @@ function beadActivitySummary(event) {
23326
24075
  case "created": return "created this task";
23327
24076
  case "closed":
23328
24077
  case "reopened": {
23329
- let reason = text$3(payload$1(event.comment)) || text$3(field(after, "close_reason")) || text$3(field(after, "reason"));
24078
+ let reason = text$2(payload$1(event.comment)) || text$2(field(after, "close_reason")) || text$2(field(after, "reason"));
23330
24079
  if (!reason || [
23331
24080
  "closed",
23332
24081
  "open",
@@ -23340,12 +24089,12 @@ function beadActivitySummary(event) {
23340
24089
  case "label_added":
23341
24090
  case "label_removed": {
23342
24091
  let verb = event.type === "label_added" ? "added" : "removed";
23343
- let value = text$3(field(after ?? before, "label")) || text$3(payload$1(event.comment)).replace(/^(?:Added|Removed) label:\s*/i, "");
24092
+ let value = text$2(field(after ?? before, "label")) || text$2(payload$1(event.comment)).replace(/^(?:Added|Removed) label:\s*/i, "");
23344
24093
  return `${verb} a label${value ? `: ${value}` : ""}`;
23345
24094
  }
23346
24095
  case "dependency_added":
23347
24096
  case "dependency_removed": {
23348
- let value = text$3(after ?? before);
24097
+ let value = text$2(after ?? before);
23349
24098
  let match = /^(?:Added|Removed) dependency: \S+ (\S+) (\S+)$/.exec(value);
23350
24099
  let target = match?.[2] ?? (/^[\w.-]+$/.test(value) ? value : "");
23351
24100
  let kind = match?.[1];
@@ -24436,7 +25185,7 @@ function validate$1(parse, reason = "protocol") {
24436
25185
  })
24437
25186
  });
24438
25187
  }
24439
- function section$2(source, map) {
25188
+ function section$1(source, map) {
24440
25189
  return source.state === "available" ? {
24441
25190
  ...source,
24442
25191
  items: source.items.map(map)
@@ -24591,7 +25340,7 @@ var beadsStoreWorkAdapterLayer = effect(WorkAdapter, gen(function* () {
24591
25340
  return yield* validate$1(() => parseWorkDetail({
24592
25341
  connection: beadsWorkConnection(session.connection, schema),
24593
25342
  item: item(issue, schema, ready.ids.includes(id) ? true : ready.truncated ? void 0 : false),
24594
- discussion: section$2(discussion, (message) => ({
25343
+ discussion: section$1(discussion, (message) => ({
24595
25344
  id: message.id,
24596
25345
  body: message.body,
24597
25346
  createdAt: message.createdAt,
@@ -24617,13 +25366,13 @@ var beadsStoreWorkAdapterLayer = effect(WorkAdapter, gen(function* () {
24617
25366
  let events = yield* session.events(id);
24618
25367
  data = yield* validate$1(() => ({
24619
25368
  kind: "activity",
24620
- data: section$2(events, beadsWorkActivity)
25369
+ data: section$1(events, beadsWorkActivity)
24621
25370
  }));
24622
25371
  } else if (sectionId === "history") {
24623
25372
  let history = yield* session.history(id);
24624
25373
  data = yield* validate$1(() => ({
24625
25374
  kind: "history",
24626
- data: section$2(history, (entry) => ({
25375
+ data: section$1(history, (entry) => ({
24627
25376
  revision: entry.commit,
24628
25377
  author: entry.author,
24629
25378
  at: entry.createdAt,
@@ -24958,7 +25707,7 @@ function parseThreadRecord(value) {
24958
25707
  if (initial.parentId !== null) throw new WorkValidationError("thread.metadata.initial.parentId");
24959
25708
  let bindings = unique(unique(array$1(input.bindings, "bindings", binding, MAX_BEADS_THREAD_BYTES), (entry) => entry.id, "bindings.id"), (entry) => entry.nativeId, "bindings.nativeId");
24960
25709
  if (input.source === void 0 !== (input.sourceId === void 0)) throw new WorkValidationError("thread.metadata.source");
24961
- if (input.source !== void 0 && (!text$4(input.source, "thread.metadata.source", 512).trim() || !text$4(input.sourceId, "thread.metadata.sourceId", 512).trim())) throw new WorkValidationError("thread.metadata.source");
25710
+ if (input.source !== void 0 && (!text$3(input.source, "thread.metadata.source", 512).trim() || !text$3(input.sourceId, "thread.metadata.sourceId", 512).trim())) throw new WorkValidationError("thread.metadata.source");
24962
25711
  return {
24963
25712
  version: 1,
24964
25713
  kind,
@@ -24969,8 +25718,8 @@ function parseThreadRecord(value) {
24969
25718
  createdAt: timestamp(input.createdAt, "thread.metadata.createdAt"),
24970
25719
  updatedAt: timestamp(input.updatedAt, "thread.metadata.updatedAt"),
24971
25720
  ...input.source === void 0 ? {} : {
24972
- source: text$4(input.source, "thread.metadata.source", 512),
24973
- sourceId: text$4(input.sourceId, "thread.metadata.sourceId", 512)
25721
+ source: text$3(input.source, "thread.metadata.source", 512),
25722
+ sourceId: text$3(input.sourceId, "thread.metadata.sourceId", 512)
24974
25723
  }
24975
25724
  };
24976
25725
  }
@@ -26355,7 +27104,7 @@ function identifier(value) {
26355
27104
  if (typeof value !== "string" || value.length > 512 || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value)) throw failure$4("input", "Select a valid task ID.");
26356
27105
  return value;
26357
27106
  }
26358
- function text$2(value, field, max, required = false) {
27107
+ function text$1(value, field, max, required = false) {
26359
27108
  if (typeof value !== "string" || value.length > max || value.includes("\0")) throw failure$4("input", `The ${field} is invalid.`);
26360
27109
  if (required && !value.trim()) throw failure$4("input", `The ${field} is required.`);
26361
27110
  return value;
@@ -26365,7 +27114,7 @@ function readFailure(cause) {
26365
27114
  if (cause instanceof Error && /no such table/i.test(cause.message)) return failure$4("not-initialized", "This D1 database has no Beads schema. Run migrateBeadsD1 before using it.", { cause });
26366
27115
  return failure$4("command", "Unable to read project tasks from the D1 store.", { cause });
26367
27116
  }
26368
- async function select$1(core, sql, params = []) {
27117
+ async function select(core, sql, params = []) {
26369
27118
  try {
26370
27119
  return (await core.db.prepare(sql).bind(...params).all()).results;
26371
27120
  } catch (cause) {
@@ -26456,10 +27205,10 @@ function edgeJson(row) {
26456
27205
  * @returns Rows ordered by priority, newest first.
26457
27206
  */
26458
27207
  async function listLabel(core, query) {
26459
- return (await select$1(core, `SELECT ${columns}, ${edges} FROM beads_issues i
27208
+ return (await select(core, `SELECT ${columns}, ${edges} FROM beads_issues i
26460
27209
  WHERE EXISTS (SELECT 1 FROM beads_labels l WHERE l.issue_id = i.id AND l.label = ?)
26461
27210
  ${query.includeInfra ? "" : `AND ${board$1}`}
26462
- ORDER BY i.priority, i.created_at DESC, i.id LIMIT ?`, [text$2(query.label, "label", 512, true), Math.max(0, Math.trunc(query.limit))])).map((row) => listJson(row, true));
27211
+ ORDER BY i.priority, i.created_at DESC, i.id LIMIT ?`, [text$1(query.label, "label", 512, true), Math.max(0, Math.trunc(query.limit))])).map((row) => listJson(row, true));
26463
27212
  }
26464
27213
  /**
26465
27214
  * Lists durable work items as brief board rows, like `bd list --all --flat --brief`.
@@ -26468,7 +27217,7 @@ async function listLabel(core, query) {
26468
27217
  * @returns Rows ordered by priority, newest first.
26469
27218
  */
26470
27219
  async function listBoard(core, limit) {
26471
- return (await select$1(core, `SELECT ${columns}, ${edges} FROM beads_issues i WHERE ${board$1}
27220
+ return (await select(core, `SELECT ${columns}, ${edges} FROM beads_issues i WHERE ${board$1}
26472
27221
  ORDER BY i.priority, i.created_at DESC, i.id LIMIT ?`, [limit])).map((row) => listJson(row, false));
26473
27222
  }
26474
27223
  /**
@@ -26478,7 +27227,7 @@ async function listBoard(core, limit) {
26478
27227
  * @returns Rows ordered by priority, newest first.
26479
27228
  */
26480
27229
  async function listReady(core, limit) {
26481
- return (await select$1(core, `SELECT ${columns}, ${edges} FROM beads_issues i
27230
+ return (await select(core, `SELECT ${columns}, ${edges} FROM beads_issues i
26482
27231
  WHERE ${board$1} AND i.status = 'open'
26483
27232
  AND (i.defer_until IS NULL OR julianday(i.defer_until) <= julianday(?))
26484
27233
  AND NOT EXISTS (
@@ -26556,7 +27305,7 @@ async function listComments(core, id, limit) {
26556
27305
  * @returns Native events.
26557
27306
  */
26558
27307
  async function listEvents(core, id, limit) {
26559
- return (await select$1(core, `SELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at
27308
+ return (await select(core, `SELECT id, issue_id, event_type, actor, old_value, new_value, comment, created_at
26560
27309
  FROM beads_events WHERE issue_id = ? ORDER BY seq DESC LIMIT ?`, [id, limit])).map((row) => {
26561
27310
  let out = {
26562
27311
  id: row.id,
@@ -26702,12 +27451,12 @@ function revision$1(issue, expected) {
26702
27451
  return Number(expected);
26703
27452
  }
26704
27453
  async function exists$1(core, id) {
26705
- return (await select$1(core, "SELECT 1 AS found FROM beads_issues WHERE id = ?", [id])).length > 0;
27454
+ return (await select(core, "SELECT 1 AS found FROM beads_issues WHERE id = ?", [id])).length > 0;
26706
27455
  }
26707
27456
  var alphabet$1 = "0123456789abcdefghijklmnopqrstuvwxyz";
26708
27457
  async function allocate(core, options) {
26709
27458
  if (options.parentId) {
26710
- let children = await select$1(core, "SELECT id FROM beads_issues WHERE id LIKE ? ESCAPE '\\'", [`${options.parentId.replace(/[\\%_]/g, (match) => `\\${match}`)}.%`]);
27459
+ let children = await select(core, "SELECT id FROM beads_issues WHERE id LIKE ? ESCAPE '\\'", [`${options.parentId.replace(/[\\%_]/g, (match) => `\\${match}`)}.%`]);
26711
27460
  let next = 1;
26712
27461
  for (let row of children) {
26713
27462
  let suffix = String(row.id).slice(options.parentId.length + 1);
@@ -26751,7 +27500,7 @@ async function checkEdge(core, id, target, type) {
26751
27500
  if (target === id) throw failure$4("cycle", "This relationship would create a dependency cycle. Choose a different task.");
26752
27501
  if (!await exists$1(core, target)) throw failure$4("not-found", "This task or relationship target no longer exists. Refresh before trying again.");
26753
27502
  if (!blockingTypes.includes(type)) return;
26754
- if ((await select$1(core, `WITH RECURSIVE reach(id) AS (
27503
+ if ((await select(core, `WITH RECURSIVE reach(id) AS (
26755
27504
  SELECT ? UNION
26756
27505
  SELECT d.depends_on_id FROM beads_dependencies d JOIN reach r ON d.issue_id = r.id
26757
27506
  WHERE d.type IN ${blocking})
@@ -26981,7 +27730,7 @@ function reopenPlan(plan, _issue, reason, expected) {
26981
27730
  }
26982
27731
  function labels(values) {
26983
27732
  return values.map((label) => {
26984
- let value = text$2(label, "label", 512, true);
27733
+ let value = text$1(label, "label", 512, true);
26985
27734
  if (/[\r\n]/.test(value)) throw failure$4("input", "The label is invalid.");
26986
27735
  return value;
26987
27736
  });
@@ -26997,9 +27746,9 @@ function metadataObject(value) {
26997
27746
  * @returns The native row, as `bd create --json` prints it.
26998
27747
  */
26999
27748
  async function createRecord(core, value) {
27000
- let title = text$2(value.title, "title", 1e3, true);
27001
- let type = knownType(text$2(value.type, "type", 128, true));
27002
- let actor = text$2(value.actor, "actor", 512, true);
27749
+ let title = text$1(value.title, "title", 1e3, true);
27750
+ let type = knownType(text$1(value.type, "type", 128, true));
27751
+ let actor = text$1(value.actor, "actor", 512, true);
27003
27752
  let names = labels(value.labels);
27004
27753
  let metadata = metadataObject(value.metadata);
27005
27754
  let wisp = type === "message";
@@ -27047,8 +27796,8 @@ async function createRecord(core, value) {
27047
27796
  */
27048
27797
  async function addComment(core, id, value) {
27049
27798
  identifier(id);
27050
- let author = text$2(value.author, "author", 512, true);
27051
- let body = text$2(value.body, "message", 4194304);
27799
+ let author = text$1(value.author, "author", 512, true);
27800
+ let body = text$1(value.body, "message", 4194304);
27052
27801
  let plan = new Plan(core, id, core.now(), author);
27053
27802
  let comment = {
27054
27803
  id: crypto.randomUUID(),
@@ -27075,7 +27824,7 @@ async function addComment(core, id, value) {
27075
27824
  async function addDependency(core, id, target, type) {
27076
27825
  identifier(id);
27077
27826
  identifier(target);
27078
- let kind = text$2(type, "relationship type", 128, true);
27827
+ let kind = text$1(type, "relationship type", 128, true);
27079
27828
  if (!/^[a-z][a-z-]*$/.test(kind)) throw failure$4("input", "The relationship type is invalid.");
27080
27829
  if (!await exists$1(core, id)) throw failure$4("not-found", messages.missing);
27081
27830
  await checkEdge(core, id, target, kind);
@@ -27097,7 +27846,7 @@ async function addDependency(core, id, target, type) {
27097
27846
  async function removeDependency(core, id, target) {
27098
27847
  identifier(id);
27099
27848
  identifier(target);
27100
- if (!(await select$1(core, "SELECT 1 AS found FROM beads_dependencies WHERE issue_id = ? AND depends_on_id = ?", [id, target])).length) throw failure$4("not-found", "This relationship no longer exists. Refresh before trying again.");
27849
+ if (!(await select(core, "SELECT 1 AS found FROM beads_dependencies WHERE issue_id = ? AND depends_on_id = ?", [id, target])).length) throw failure$4("not-found", "This relationship no longer exists. Refresh before trying again.");
27101
27850
  let plan = new Plan(core, id, core.now());
27102
27851
  plan.update({});
27103
27852
  plan.removeEdge(target);
@@ -27115,9 +27864,9 @@ async function removeDependency(core, id, target) {
27115
27864
  */
27116
27865
  async function promote(core, id, value) {
27117
27866
  identifier(id);
27118
- let plan = new Plan(core, id, core.now(), text$2(value.actor, "actor", 512, true));
27867
+ let plan = new Plan(core, id, core.now(), text$1(value.actor, "actor", 512, true));
27119
27868
  plan.update({ ephemeral: 0 });
27120
- plan.event("promoted", { comment: text$2(value.reason, "reason", 4096) });
27869
+ plan.event("promoted", { comment: text$1(value.reason, "reason", 4096) });
27121
27870
  await apply(core, plan);
27122
27871
  }
27123
27872
  /**
@@ -27130,7 +27879,7 @@ async function close(core, id, reason) {
27130
27879
  identifier(id);
27131
27880
  let issue = await required(core, id);
27132
27881
  let plan = new Plan(core, id, core.now());
27133
- closePlan(core, plan, issue, text$2(reason, "reason", 4096));
27882
+ closePlan(core, plan, issue, text$1(reason, "reason", 4096));
27134
27883
  await apply(core, plan);
27135
27884
  }
27136
27885
  /**
@@ -27144,7 +27893,7 @@ async function reopen(core, id, reason) {
27144
27893
  let issue = await required(core, id);
27145
27894
  if (issue.status !== "closed") return;
27146
27895
  let plan = new Plan(core, id, core.now());
27147
- reopenPlan(plan, issue, text$2(reason, "reason", 4096));
27896
+ reopenPlan(plan, issue, text$1(reason, "reason", 4096));
27148
27897
  await apply(core, plan);
27149
27898
  }
27150
27899
  /**
@@ -27307,7 +28056,7 @@ async function importJsonl(core, jsonl) {
27307
28056
  merged: 0
27308
28057
  };
27309
28058
  for (let record of records) {
27310
- let [existing] = await select$1(core, "SELECT updated_at FROM beads_issues WHERE id = ?", [record.issue.id]);
28059
+ let [existing] = await select(core, "SELECT updated_at FROM beads_issues WHERE id = ?", [record.issue.id]);
27311
28060
  let plan = new Plan(core, record.issue.id, at, record.issue.createdBy ?? core.actor);
27312
28061
  if (!existing) {
27313
28062
  plan.insert(record.values);
@@ -27392,7 +28141,7 @@ function protocol(operation, parse) {
27392
28141
  })
27393
28142
  });
27394
28143
  }
27395
- function section$1(items, limit) {
28144
+ function section(items, limit) {
27396
28145
  return {
27397
28146
  state: "available",
27398
28147
  items: items.slice(0, limit),
@@ -27547,13 +28296,13 @@ function makeD1BeadsStore(options) {
27547
28296
  comments: (value, parse) => id(value).pipe(flatMap$1(() => run(() => listComments(store, value))), flatMap$1((rows) => protocol("comments", () => parse(rows)))),
27548
28297
  ready: readyIssues,
27549
28298
  issue,
27550
- discussion: (value) => id(value).pipe(flatMap$1(() => run(() => listComments(store, value, 201))), flatMap$1((rows) => protocol("comments", () => section$1(parseCliDiscussion(rows), 200)))),
28299
+ discussion: (value) => id(value).pipe(flatMap$1(() => run(() => listComments(store, value, 201))), flatMap$1((rows) => protocol("comments", () => section(parseCliDiscussion(rows), 200)))),
27551
28300
  history: () => succeed$2({
27552
28301
  state: "unsupported",
27553
28302
  items: [],
27554
28303
  truncated: false
27555
28304
  }),
27556
- events: (value) => id(value).pipe(flatMap$1(() => run(() => listEvents(store, value, 51))), flatMap$1((rows) => protocol("history", () => section$1(parseCliEvents(rows), 50)))),
28305
+ events: (value) => id(value).pipe(flatMap$1(() => run(() => listEvents(store, value, 51))), flatMap$1((rows) => protocol("history", () => section(parseCliEvents(rows), 50)))),
27557
28306
  snapshot: gen(function* () {
27558
28307
  let issues = yield* run(() => listBoard(store, MAX_BEADS + 1)).pipe(flatMap$1((rows) => protocol("list", () => parseCliIssues(rows))));
27559
28308
  let unblocked = yield* readyIssues(MAX_BEADS + 1);
@@ -35940,1023 +36689,383 @@ var watchNode = (path, info, options) => callback((queue) => acquireRelease(sync
35940
36689
  })
35941
36690
  }));
35942
36691
  return;
35943
- case "change":
35944
- offerUnsafe(queue, {
35945
- _tag: "Update",
35946
- path
35947
- });
35948
- return;
35949
- }
35950
- });
35951
- watcher.on("error", (error) => {
35952
- failCauseUnsafe(queue, fail$7(systemError({
35953
- module: "FileSystem",
35954
- _tag: "Unknown",
35955
- method: "watch",
35956
- pathOrDescriptor: path,
35957
- cause: error
35958
- })));
35959
- });
35960
- watcher.on("close", () => {
35961
- endUnsafe(queue);
35962
- });
35963
- return watcher;
35964
- }), (watcher) => sync(() => watcher.close())).pipe(tap$1(() => sync(() => options?.onReady?.()))));
35965
- var watch = (backend, path, options) => all$1([stat$1(path), serviceOption(WatchBackend)]).pipe(map$2(([stat, callerBackend]) => orElse(callerBackend, () => backend).pipe(flatMap$5((_) => _.register(path, stat, options)), getOrElse(() => watchNode(path, stat, options)))), unwrap$1);
35966
- var writeFile$1 = (path, data, options) => callback$1((resume, signal) => {
35967
- try {
35968
- NFS.writeFile(path, data, {
35969
- signal,
35970
- flag: options?.flag,
35971
- mode: options?.mode
35972
- }, (err) => {
35973
- if (err) resume(fail$6(handleErrnoException("FileSystem", "writeFile")(err, [path])));
35974
- else resume(void_);
35975
- });
35976
- } catch (err) {
35977
- resume(fail$6(handleBadArgument("writeFile")(err)));
35978
- }
35979
- });
35980
- var makeFileSystem = /*#__PURE__*/ map$2(/*#__PURE__*/ serviceOption(WatchBackend), (backend) => make$14({
35981
- access: access$1,
35982
- chmod,
35983
- chown,
35984
- copy,
35985
- copyFile,
35986
- glob,
35987
- link,
35988
- makeDirectory,
35989
- makeTempDirectory,
35990
- makeTempDirectoryScoped,
35991
- makeTempFile,
35992
- makeTempFileScoped,
35993
- open,
35994
- readDirectory,
35995
- readFile: readFile$1,
35996
- readLink,
35997
- realPath,
35998
- remove,
35999
- rename: rename$1,
36000
- stat: stat$1,
36001
- symlink: symlink$1,
36002
- truncate,
36003
- utimes,
36004
- watch(path, options) {
36005
- return watch(backend, path, options);
36006
- },
36007
- writeFile: writeFile$1
36008
- }));
36009
- //#endregion
36010
- //#region ../../node_modules/.pnpm/@effect+platform-node@4.0.5_effect@4.0.5/node_modules/@effect/platform-node/dist/NodeFileSystem.js
36011
- /**
36012
- * Node.js `FileSystem` layer for programs that perform real filesystem I/O.
36013
- *
36014
- * The exported layer satisfies the platform-independent `FileSystem` service
36015
- * with Node-backed operations for files, directories, metadata, permissions,
36016
- * links, temporary paths, and path watching. Effects still call the service from
36017
- * `effect/FileSystem`; this module only chooses the Node implementation.
36018
- *
36019
- * @stability unstable
36020
- * @since 4.0.0
36021
- */
36022
- /**
36023
- * Provides the `FileSystem` service backed by Node filesystem APIs.
36024
- *
36025
- * @stability unstable
36026
- * @category layers
36027
- * @since 4.0.0
36028
- */
36029
- var layer$2 = /* @__PURE__ */ effect(FileSystem)(makeFileSystem);
36030
- //#endregion
36031
- //#region ../../node_modules/.pnpm/@effect+platform-node-shared@4.0.5_effect@4.0.5/node_modules/@effect/platform-node-shared/dist/NodePath.js
36032
- /**
36033
- * Node-backed provider for Effect's `Path` service.
36034
- *
36035
- * This module turns Node's `node:path` and `node:url` APIs into `Path` layers.
36036
- * `layer` uses the host platform path implementation, while `layerPosix` and
36037
- * `layerWin32` provide fixed POSIX and Windows variants. All three layers also
36038
- * include helpers for converting between file paths and file URLs.
36039
- *
36040
- * @stability unstable
36041
- * @since 4.0.0
36042
- */
36043
- var fileUrlOps = (windows) => ({
36044
- fromFileUrl: (url) => try_({
36045
- try: () => NodeUrl.fileURLToPath(url, { windows }),
36046
- catch: (cause) => new BadArgument({
36047
- module: "Path",
36048
- method: "fromFileUrl",
36049
- cause
36050
- })
36051
- }),
36052
- toFileUrl: (path) => try_({
36053
- try: () => NodeUrl.pathToFileURL(path, { windows }),
36054
- catch: (cause) => new BadArgument({
36055
- module: "Path",
36056
- method: "toFileUrl",
36057
- cause
36058
- })
36059
- })
36060
- });
36061
- ({
36062
- ...Path.posix,
36063
- .../*#__PURE__*/ fileUrlOps(false)
36064
- });
36065
- ({
36066
- ...Path.win32,
36067
- .../*#__PURE__*/ fileUrlOps(true)
36068
- });
36069
- provideMerge(layer$4, mergeAll$1(layer$2, /* @__PURE__ */ succeed$3(Path$1)(/*#__PURE__*/ make$13({
36070
- ...Path,
36071
- .../*#__PURE__*/ fileUrlOps(void 0)
36072
- }))));
36073
- TaggedError("GitInputError");
36074
- TaggedError("GitCommandError");
36075
- TaggedError("GitRepositoryError");
36076
- TaggedError("GitIdentityError");
36077
- TaggedError("GitProtocolError");
36078
- TaggedError("GitConflictError");
36079
- TaggedError("GitIndexError");
36080
- //#endregion
36081
- //#region ../git/src/lib/validation.ts
36082
- var text$1 = String$1.check(isNonEmpty(), isPattern(/^[^\0]*$/));
36083
- var oid = String$1.check(isPattern(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/));
36084
- var path$1 = text$1.check(makeFilter((value) => !value.startsWith("/") && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== ".." && part.toLowerCase() !== ".git")));
36085
- var ref$1 = text$1.check(makeFilter((value) => value.startsWith("refs/") && !/[\x00-\x20\x7f~^:?*\[\\]/.test(value) && !value.includes("..") && !value.includes("@{") && !value.endsWith(".") && value.split("/").every((part) => part.length > 0 && !part.startsWith(".") && !part.endsWith(".lock"))));
36086
- var revision = Union([
36087
- oid,
36088
- ref$1,
36089
- Literal("HEAD")
36090
- ]);
36091
- String$1.check(isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/));
36092
- var mode = Literals([
36093
- "100644",
36094
- "100755",
36095
- "120000"
36096
- ]);
36097
- Struct({
36098
- name: text$1.check(isPattern(/^[^\r\n<>]+$/)),
36099
- email: text$1.check(isPattern(/^[^\r\n<>]+$/))
36100
- });
36101
- Struct({
36102
- ref: ref$1,
36103
- expectedHead: NullOr(oid),
36104
- files: ArraySchema(Struct({
36105
- path: path$1,
36106
- contents: Union([
36107
- String$1,
36108
- Uint8Array$1,
36109
- Null
36110
- ]),
36111
- mode: optional$1(mode)
36112
- })).check(isMaxLength(1e4)),
36113
- message: text$1,
36114
- extraParents: optional$1(ArraySchema(oid).check(isMaxLength(16)))
36115
- });
36116
- Struct({
36117
- ...Struct({
36118
- base: optional$1(oid),
36119
- target: optional$1(oid),
36120
- staged: optional$1(Boolean$1),
36121
- paths: optional$1(ArraySchema(path$1).check(isMaxLength(1e4)))
36122
- }).fields,
36123
- context: optional$1(Int$1.check(isBetween({
36124
- minimum: 0,
36125
- maximum: 1e3
36126
- })))
36127
- });
36128
- Struct({ environment: optional$1(Record(String$1, Union([String$1, Undefined]))) });
36129
- Struct({
36130
- commit: oid,
36131
- path: path$1,
36132
- limit: Int$1.check(isBetween({
36133
- minimum: 1,
36134
- maximum: 101
36135
- })),
36136
- skip: optional$1(Int$1.check(isBetween({
36137
- minimum: 0,
36138
- maximum: 1e4
36139
- })))
36140
- });
36141
- Struct({
36142
- from: revision,
36143
- limit: Int$1.check(isBetween({
36144
- minimum: 1,
36145
- maximum: 500
36146
- }))
36147
- });
36148
- Int$1.check(isBetween({
36149
- minimum: 1,
36150
- maximum: 1e3
36151
- }));
36152
- //#endregion
36153
- //#region ../git/src/lib/git.ts
36154
- /** Opens the Git repository that contains a directory. */
36155
- var Git = class extends Service()("@bud.sh/git/Git") {};
36156
- //#endregion
36157
- //#region ../live/src/server/hub.ts
36158
- /**
36159
- * Orders every published change into one stream per companion. Clients resume from their
36160
- * last event id; anything older than the replay buffer, or from an earlier companion,
36161
- * receives a resync instead.
36162
- */
36163
- var LiveHub = class {
36164
- epoch = randomBytes(8).toString("hex");
36165
- #sequence = 0;
36166
- #buffer = [];
36167
- #replay;
36168
- #heartbeatMs;
36169
- #listeners = /* @__PURE__ */ new Set();
36170
- constructor(options = {}) {
36171
- this.#replay = options.replay ?? 1024;
36172
- this.#heartbeatMs = options.heartbeatMs ?? 15e3;
36173
- }
36174
- /**
36175
- * Sends a change to every connected client.
36176
- * @param topic - The domain, such as `previews` or `files`.
36177
- * @param data - JSON data each domain validates with its own schema.
36178
- */
36179
- publish(topic, data) {
36180
- let event = {
36181
- id: `${this.epoch}:${++this.#sequence}`,
36182
- topic,
36183
- data
36184
- };
36185
- this.#buffer.push(event);
36186
- if (this.#buffer.length > this.#replay) this.#buffer.shift();
36187
- for (let listener of [...this.#listeners]) listener(event);
36188
- }
36189
- /**
36190
- * Opens one client's `text/event-stream`.
36191
- * @param lastEventId - The client's `Last-Event-ID`, if it is resuming.
36192
- * @param signal - The request's lifetime; aborting closes the stream.
36193
- * @returns The response body.
36194
- */
36195
- open(lastEventId, signal) {
36196
- let encoder = new TextEncoder();
36197
- let close = () => {};
36198
- return new ReadableStream({
36199
- start: (controller) => {
36200
- let send = (text) => controller.enqueue(encoder.encode(text));
36201
- let write = (event) => send(`id: ${event.id}\nevent: ${event.topic}\ndata: ${JSON.stringify(event.data)}\n\n`);
36202
- let missed = this.#since(lastEventId);
36203
- if (missed) for (let event of missed) write(event);
36204
- else write({
36205
- id: `${this.epoch}:${this.#sequence}`,
36206
- topic: RESYNC,
36207
- data: null
36208
- });
36209
- this.#listeners.add(write);
36210
- let heartbeat = setInterval(() => send(": heartbeat\n\n"), this.#heartbeatMs);
36211
- close = () => {
36212
- clearInterval(heartbeat);
36213
- this.#listeners.delete(write);
36214
- try {
36215
- controller.close();
36216
- } catch {}
36217
- };
36218
- if (signal.aborted) close();
36219
- else signal.addEventListener("abort", close, { once: true });
36220
- },
36221
- cancel: () => close()
36692
+ case "change":
36693
+ offerUnsafe(queue, {
36694
+ _tag: "Update",
36695
+ path
36696
+ });
36697
+ return;
36698
+ }
36699
+ });
36700
+ watcher.on("error", (error) => {
36701
+ failCauseUnsafe(queue, fail$7(systemError({
36702
+ module: "FileSystem",
36703
+ _tag: "Unknown",
36704
+ method: "watch",
36705
+ pathOrDescriptor: path,
36706
+ cause: error
36707
+ })));
36708
+ });
36709
+ watcher.on("close", () => {
36710
+ endUnsafe(queue);
36711
+ });
36712
+ return watcher;
36713
+ }), (watcher) => sync(() => watcher.close())).pipe(tap$1(() => sync(() => options?.onReady?.()))));
36714
+ var watch = (backend, path, options) => all$1([stat$1(path), serviceOption(WatchBackend)]).pipe(map$2(([stat, callerBackend]) => orElse(callerBackend, () => backend).pipe(flatMap$5((_) => _.register(path, stat, options)), getOrElse(() => watchNode(path, stat, options)))), unwrap$1);
36715
+ var writeFile$1 = (path, data, options) => callback$1((resume, signal) => {
36716
+ try {
36717
+ NFS.writeFile(path, data, {
36718
+ signal,
36719
+ flag: options?.flag,
36720
+ mode: options?.mode
36721
+ }, (err) => {
36722
+ if (err) resume(fail$6(handleErrnoException("FileSystem", "writeFile")(err, [path])));
36723
+ else resume(void_);
36222
36724
  });
36725
+ } catch (err) {
36726
+ resume(fail$6(handleBadArgument("writeFile")(err)));
36223
36727
  }
36224
- #since(lastEventId) {
36225
- let last = lastEventId ? parseLiveId(lastEventId) : null;
36226
- if (!last || last.epoch !== this.epoch || last.sequence > this.#sequence) return null;
36227
- let oldest = this.#buffer[0] ? parseLiveId(this.#buffer[0].id).sequence : this.#sequence + 1;
36228
- if (last.sequence < oldest - 1) return null;
36229
- return this.#buffer.filter((event) => parseLiveId(event.id).sequence > last.sequence);
36230
- }
36231
- };
36728
+ });
36729
+ var makeFileSystem = /*#__PURE__*/ map$2(/*#__PURE__*/ serviceOption(WatchBackend), (backend) => make$14({
36730
+ access: access$1,
36731
+ chmod,
36732
+ chown,
36733
+ copy,
36734
+ copyFile,
36735
+ glob,
36736
+ link,
36737
+ makeDirectory,
36738
+ makeTempDirectory,
36739
+ makeTempDirectoryScoped,
36740
+ makeTempFile,
36741
+ makeTempFileScoped,
36742
+ open,
36743
+ readDirectory,
36744
+ readFile: readFile$1,
36745
+ readLink,
36746
+ realPath,
36747
+ remove,
36748
+ rename: rename$1,
36749
+ stat: stat$1,
36750
+ symlink: symlink$1,
36751
+ truncate,
36752
+ utimes,
36753
+ watch(path, options) {
36754
+ return watch(backend, path, options);
36755
+ },
36756
+ writeFile: writeFile$1
36757
+ }));
36232
36758
  //#endregion
36233
- //#region ../live/src/server/service.ts
36234
- /** The companion's ordered live channel: publishes changes and opens client streams. */
36235
- var LiveChannel = class extends Service()("@bud.sh/live/LiveChannel") {};
36759
+ //#region ../../node_modules/.pnpm/@effect+platform-node@4.0.5_effect@4.0.5/node_modules/@effect/platform-node/dist/NodeFileSystem.js
36236
36760
  /**
36237
- * One ordered live channel per companion.
36238
- * @param options - Replay buffer and heartbeat settings.
36239
- * @returns The LiveChannel layer.
36761
+ * Node.js `FileSystem` layer for programs that perform real filesystem I/O.
36762
+ *
36763
+ * The exported layer satisfies the platform-independent `FileSystem` service
36764
+ * with Node-backed operations for files, directories, metadata, permissions,
36765
+ * links, temporary paths, and path watching. Effects still call the service from
36766
+ * `effect/FileSystem`; this module only chooses the Node implementation.
36767
+ *
36768
+ * @stability unstable
36769
+ * @since 4.0.0
36240
36770
  */
36241
- function liveChannelLayer(options = {}) {
36242
- return sync$1(LiveChannel, () => {
36243
- let hub = new LiveHub(options);
36244
- return {
36245
- epoch: hub.epoch,
36246
- publish: (topic, data) => sync(() => hub.publish(topic, data)),
36247
- open: (lastEventId, signal) => hub.open(lastEventId, signal)
36248
- };
36249
- });
36250
- }
36251
36771
  /**
36252
- * Publishes every element of a domain's change stream on the live channel, for the lifetime
36253
- * of the layer that forks it.
36254
- * @param topic - The topic clients subscribe to.
36255
- * @param changes - The domain's change stream.
36256
- * @returns An effect to fork in a scoped layer.
36772
+ * Provides the `FileSystem` service backed by Node filesystem APIs.
36773
+ *
36774
+ * @stability unstable
36775
+ * @category layers
36776
+ * @since 4.0.0
36257
36777
  */
36258
- function publishChanges(topic, changes) {
36259
- return gen(function* () {
36260
- let channel = yield* LiveChannel;
36261
- yield* changes.pipe(runForEach((change) => channel.publish(topic, change)), ignore$2, forkScoped);
36262
- });
36263
- }
36264
- //#endregion
36265
- //#region ../project/src/server/errors.ts
36266
- /** Expected project startup/configuration failure; nested causes remain server-only. */
36267
- var ProjectError = class extends TaggedError("ProjectError") {};
36778
+ var layer$2 = /* @__PURE__ */ effect(FileSystem)(makeFileSystem);
36268
36779
  //#endregion
36269
- //#region ../project/src/server/modules.ts
36780
+ //#region ../../node_modules/.pnpm/@effect+platform-node-shared@4.0.5_effect@4.0.5/node_modules/@effect/platform-node-shared/dist/NodePath.js
36270
36781
  /**
36271
- * Confines a validated config module to a regular, bounded file in its project.
36272
- * @param root - The trusted consumer directory.
36273
- * @param module - A project-relative module already validated by the config resolver.
36274
- * @param options - The config setting and whether its source runs on the server or browser.
36275
- * @returns The canonical filename, without evaluating its code.
36782
+ * Node-backed provider for Effect's `Path` service.
36783
+ *
36784
+ * This module turns Node's `node:path` and `node:url` APIs into `Path` layers.
36785
+ * `layer` uses the host platform path implementation, while `layerPosix` and
36786
+ * `layerWin32` provide fixed POSIX and Windows variants. All three layers also
36787
+ * include helpers for converting between file paths and file URLs.
36788
+ *
36789
+ * @stability unstable
36790
+ * @since 4.0.0
36276
36791
  */
36277
- function projectModuleFile(root, module, options) {
36278
- return gen(function* () {
36279
- let fs = yield* FileSystem;
36280
- let path = yield* Path$1;
36281
- let { setting, kind } = options;
36282
- let project = yield* fs.realPath(root).pipe(mapError$2((cause) => new ProjectError({
36283
- reason: "root",
36284
- message: "Cannot open the module project root.",
36285
- cause
36286
- })));
36287
- let file = yield* fs.realPath(path.resolve(project, module)).pipe(mapError$2((cause) => new ProjectError({
36288
- reason: "read",
36289
- message: `${setting} cannot find ${module} in the project.`,
36290
- cause
36291
- })));
36292
- let relative = path.relative(project, file);
36293
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || relative.split(path.sep).includes("node_modules")) return yield* new ProjectError({
36294
- reason: "invalid",
36295
- message: `${setting} must resolve to a project-owned file outside node_modules.`
36296
- });
36297
- if (kind === "server" && !file.endsWith(".server.ts")) return yield* new ProjectError({
36298
- reason: "invalid",
36299
- message: `${setting} must resolve to a .server.ts module.`
36300
- });
36301
- if (kind === "browser" && (!/\.(?:ts|tsx|js|jsx)$/.test(file) || /\.server\.[cm]?[jt]sx?$/.test(file))) return yield* new ProjectError({
36302
- reason: "invalid",
36303
- message: `${setting} must resolve to a browser module.`
36304
- });
36305
- if ((yield* fs.stat(file).pipe(mapError$2((cause) => new ProjectError({
36306
- reason: "read",
36307
- message: `${setting} cannot inspect ${module}.`,
36792
+ var fileUrlOps = (windows) => ({
36793
+ fromFileUrl: (url) => try_({
36794
+ try: () => NodeUrl.fileURLToPath(url, { windows }),
36795
+ catch: (cause) => new BadArgument({
36796
+ module: "Path",
36797
+ method: "fromFileUrl",
36308
36798
  cause
36309
- })))).type !== "File") return yield* new ProjectError({
36310
- reason: "invalid",
36311
- message: `${setting} must be a regular module file.`
36312
- });
36313
- yield* readText(file, { maxBytes: 262144 }).pipe(mapError$2((cause) => new ProjectError({
36314
- reason: "read",
36315
- message: `${setting} must be a readable module no larger than 256 KiB.`,
36799
+ })
36800
+ }),
36801
+ toFileUrl: (path) => try_({
36802
+ try: () => NodeUrl.pathToFileURL(path, { windows }),
36803
+ catch: (cause) => new BadArgument({
36804
+ module: "Path",
36805
+ method: "toFileUrl",
36316
36806
  cause
36317
- })));
36318
- return file;
36319
- });
36320
- }
36321
- //#endregion
36322
- //#region ../project/src/lib/validate.ts
36323
- var errors = /* @__PURE__ */ new WeakMap();
36324
- /**
36325
- * Validates config, throwing its first problem as the `Error` the config checks report.
36326
- * @param schema - The config schema.
36327
- * @param value - The project's untrusted setting.
36328
- * @returns The validated, normalized setting.
36329
- */
36330
- function parseConfig(schema, value) {
36331
- let result = parseSafe(schema, value, { abortEarly: true });
36332
- if (result.success) return result.value;
36333
- let issue = result.issues[0];
36334
- throw errors.get(issue) ?? new Error(issue.message);
36335
- }
36336
- /**
36337
- * Reports any failure of `schema` with one message.
36338
- * @param schema - The schema to run.
36339
- * @param message - The message for any of its failures.
36340
- * @returns The described schema.
36341
- */
36342
- function described(schema, message) {
36343
- return createSchema((value, context) => {
36344
- let result = schema["~run"](value, context);
36345
- return result.issues ? fail(message, context.path) : result;
36346
- });
36347
- }
36348
- /**
36349
- * Applies a rule after `schema` validates.
36350
- * @param schema - The schema to run first.
36351
- * @param rule - Returns a message, or undefined when the value is valid.
36352
- * @returns The checked schema.
36353
- */
36354
- function check(schema, rule) {
36355
- return createSchema((value, context) => {
36356
- let result = schema["~run"](value, context);
36357
- if (result.issues) return result;
36358
- let message = rule(result.value);
36359
- return message === void 0 ? result : fail(message, context.path);
36360
- });
36361
- }
36362
- /**
36363
- * Validates with a function that throws, such as a URL or route map parser, keeping its error.
36364
- * @param schema - The schema to run first.
36365
- * @param parse - Turns its value into the output, or throws.
36366
- * @returns The delegating schema.
36367
- */
36368
- function delegate(schema, parse) {
36369
- return createSchema((value, context) => {
36370
- let result = schema["~run"](value, context);
36371
- if (result.issues) return result;
36372
- try {
36373
- return { value: parse(result.value) };
36374
- } catch (error) {
36375
- if (!(error instanceof Error)) throw error;
36376
- let failure = fail(error.message, context.path);
36377
- errors.set(failure.issues[0], error);
36378
- return failure;
36379
- }
36380
- });
36381
- }
36382
- /**
36383
- * Chooses a schema from the unvalidated value.
36384
- * @param choose - Picks the schema for a value.
36385
- * @returns The selecting schema.
36386
- */
36387
- function select(choose) {
36388
- return createSchema((value, context) => choose(value)["~run"](value, context));
36389
- }
36390
- /**
36391
- * Substitutes a default for null or undefined.
36392
- * @param schema - The schema for present values.
36393
- * @param value - Makes the default.
36394
- * @returns The schema with its default.
36395
- */
36396
- function fallback(schema, value) {
36397
- return createSchema((input, context) => input == null ? { value: value() } : schema["~run"](input, context));
36398
- }
36399
- /**
36400
- * A settings object that names its unsupported keys before validating its fields.
36401
- * @param name - The setting's name in messages, such as `previews`.
36402
- * @param shape - The schema for each supported key.
36403
- * @returns The settings schema.
36404
- */
36405
- function settings(name, shape) {
36406
- let schema = object(shape);
36407
- return createSchema((value, context) => {
36408
- if (!value || typeof value !== "object" || Array.isArray(value)) return fail(`${name} must be an object.`, context.path);
36409
- for (let key of Object.keys(value)) if (!Object.hasOwn(shape, key)) return fail(`${name}.${key} is not a supported setting.`, [...context.path, key]);
36410
- return schema["~run"](value, context);
36411
- });
36412
- }
36413
- /**
36414
- * A list whose length is checked before its entries.
36415
- * @param item - The schema for each entry.
36416
- * @param message - The message when it is not a list or is too short.
36417
- * @param minimum - The fewest entries it may have.
36418
- * @returns The list schema.
36419
- */
36420
- function list(item, message, minimum = 0) {
36421
- let items = array(item);
36422
- return createSchema((value, context) => !Array.isArray(value) || value.length < minimum ? fail(message, context.path) : items["~run"](value, context));
36423
- }
36424
- //#endregion
36425
- //#region ../project/src/lib/files.ts
36426
- /** The Files settings, with their defaults filled in. */
36427
- var BudFilesSchema = defaulted(settings("files", { edit: fallback(described(boolean(), "files.edit must be true or false."), () => true) }), () => ({ edit: true }));
36428
- //#endregion
36429
- //#region ../project/src/lib/flows.ts
36430
- var message$1 = "flows must be [{ id, title, start, script, steps: [{ id, title }], block? }] with short ids, local paths and unique steps.";
36431
- var slug = (value) => typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value);
36432
- var text = (value) => typeof value === "string" && value.trim() !== "" && value.length <= 200;
36433
- function flow(value) {
36434
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
36435
- let { id, title, start, script, steps, block = [] } = value;
36436
- if (!slug(id) || !text(title) || !text(script) || script.includes("\0")) return null;
36437
- if (typeof start !== "string" || !start.startsWith("/") || start.startsWith("//")) return null;
36438
- if (new URL(start, "http://bud.invalid").origin !== "http://bud.invalid") return null;
36439
- if (!Array.isArray(steps) || !steps.length || steps.length > 50) return null;
36440
- let resolved = steps.map((step) => {
36441
- let { id: stepId, title: stepTitle } = step ?? {};
36442
- return slug(stepId) && text(stepTitle) ? {
36443
- id: stepId,
36444
- title: stepTitle.trim()
36445
- } : null;
36446
- });
36447
- if (resolved.some((step) => !step)) return null;
36448
- let ids = resolved.map((step) => step.id);
36449
- if (new Set(ids).size !== ids.length) return null;
36450
- if (!Array.isArray(block) || block.some((part) => !text(part))) return null;
36451
- return {
36452
- id,
36453
- title: title.trim(),
36454
- start,
36455
- script: script.trim(),
36456
- steps: resolved,
36457
- block
36458
- };
36459
- }
36460
- /** The flows, none when omitted. */
36461
- var BudFlowsSchema = createSchema((value, context) => {
36462
- if (value === void 0) return { value: [] };
36463
- if (!Array.isArray(value)) return fail(message$1, context.path);
36464
- let flows = value.map(flow);
36465
- if (flows.some((resolved) => !resolved)) return fail(message$1, context.path);
36466
- let ids = flows.map((resolved) => resolved.id);
36467
- if (new Set(ids).size !== ids.length) return fail(message$1, context.path);
36468
- return { value: flows };
36469
- });
36470
- /**
36471
- * Where a flow's step is pictured: a route of its own, named `<flow>/<step>`, in a group named
36472
- * after the flow. Its path is the start page with the step as its fragment, unique among routes.
36473
- * @param flow - The flow.
36474
- * @param step - One of its steps.
36475
- * @returns The step's route.
36476
- */
36477
- function flowStepRoute(flow, step) {
36478
- return {
36479
- id: `${flow.id}/${step.id}`,
36480
- path: `${flow.start.replace(/#.*$/, "")}#${flow.id}-${step.id}`,
36481
- title: step.title,
36482
- group: flow.title,
36483
- flow: {
36484
- id: flow.id,
36485
- step: step.id
36486
- }
36487
- };
36488
- }
36489
- //#endregion
36490
- //#region ../project/src/lib/logo.ts
36491
- /** The image types a logo may be, by extension. */
36492
- var LOGO_TYPES = Object.freeze({
36493
- ".svg": "image/svg+xml",
36494
- ".png": "image/png",
36495
- ".webp": "image/webp",
36496
- ".jpg": "image/jpeg",
36497
- ".jpeg": "image/jpeg",
36498
- ".gif": "image/gif",
36499
- ".ico": "image/x-icon"
36807
+ })
36808
+ })
36500
36809
  });
36501
- var logoMessage = "logo must be an image path inside the project (.svg, .png, .webp, .jpg, .gif or .ico), or { light, dark }.";
36502
- function image(value) {
36503
- if (typeof value !== "string" || value.length > 1024 || value.includes("\0")) return null;
36504
- let path = value.trim().replaceAll("\\", "/");
36505
- if (!path || path.startsWith("/") || /^[a-z]:/i.test(path)) return null;
36506
- if (path.split("/").includes("..")) return null;
36507
- let extension = path.slice(path.lastIndexOf(".")).toLowerCase();
36508
- return Object.hasOwn(LOGO_TYPES, extension) ? path : null;
36509
- }
36510
- /** A logo in its one resolved form, or null for Bud's own mark. */
36511
- var BudLogoSchema = createSchema((value, context) => {
36512
- if (value === void 0) return { value: null };
36513
- let light = image(typeof value === "object" && value ? value.light : value);
36514
- let raw = typeof value === "object" && value ? value.dark : void 0;
36515
- let dark = raw === void 0 ? null : image(raw);
36516
- let keys = typeof value === "object" && value ? Object.keys(value) : [];
36517
- if (light && (raw === void 0 || dark) && keys.every((key) => key === "light" || key === "dark")) return { value: {
36518
- light,
36519
- dark
36520
- } };
36521
- return fail(logoMessage, context.path);
36810
+ ({
36811
+ ...Path.posix,
36812
+ .../*#__PURE__*/ fileUrlOps(false)
36522
36813
  });
36523
- //#endregion
36524
- //#region ../project/src/lib/branches.ts
36525
- var message = "branches must be { main?, build?: { install?, build?, serve? } } with a branch name and nonempty shell commands.";
36526
- function branch(value) {
36527
- return typeof value === "string" && value.length <= 255 && /^[A-Za-z0-9._/-]+$/.test(value) && !value.startsWith("-") && !value.startsWith("/") && !value.endsWith("/") && !value.endsWith(".lock") && !value.includes("..") && !value.includes("//") ? value : null;
36528
- }
36529
- function command(value) {
36530
- return typeof value === "string" && value.trim() && value.length <= 4096 && !value.includes("\0") ? value.trim() : null;
36531
- }
36532
- /** The branch settings, defaults filled in. */
36533
- var BudBranchesSchema = createSchema((value, context) => {
36534
- let empty = {
36535
- main: null,
36536
- install: null,
36537
- build: null,
36538
- serve: null
36539
- };
36540
- if (value === void 0) return { value: empty };
36541
- if (!value || typeof value !== "object" || Array.isArray(value)) return fail(message, context.path);
36542
- let input = value;
36543
- let build = input.build ?? {};
36544
- if (!build || typeof build !== "object" || Array.isArray(build) || Object.keys(input).some((key) => key !== "main" && key !== "build")) return fail(message, context.path);
36545
- let steps = build;
36546
- if (Object.keys(steps).some((key) => ![
36547
- "install",
36548
- "build",
36549
- "serve"
36550
- ].includes(key))) return fail(message, context.path);
36551
- let resolved = {
36552
- main: input.main === void 0 ? null : branch(input.main),
36553
- install: steps.install === void 0 ? null : command(steps.install),
36554
- build: steps.build === void 0 ? null : command(steps.build),
36555
- serve: steps.serve === void 0 ? null : command(steps.serve)
36556
- };
36557
- if (input.main !== void 0 && !resolved.main || [
36558
- "install",
36559
- "build",
36560
- "serve"
36561
- ].some((key) => steps[key] !== void 0 && !resolved[key])) return fail(message, context.path);
36562
- return { value: resolved };
36814
+ ({
36815
+ ...Path.win32,
36816
+ .../*#__PURE__*/ fileUrlOps(true)
36563
36817
  });
36818
+ provideMerge(layer$4, mergeAll$1(layer$2, /* @__PURE__ */ succeed$3(Path$1)(/*#__PURE__*/ make$13({
36819
+ ...Path,
36820
+ .../*#__PURE__*/ fileUrlOps(void 0)
36821
+ }))));
36822
+ TaggedError("GitInputError");
36823
+ TaggedError("GitCommandError");
36824
+ TaggedError("GitRepositoryError");
36825
+ TaggedError("GitIdentityError");
36826
+ TaggedError("GitProtocolError");
36827
+ TaggedError("GitConflictError");
36828
+ TaggedError("GitIndexError");
36564
36829
  //#endregion
36565
- //#region ../project/src/lib/storage.ts
36566
- /**
36567
- * A project module named in the config: a ./ path inside the project, outside node_modules.
36568
- * @param name - The config option, for the error message.
36569
- * @param server - Whether it runs on the server (.server.ts) rather than in the browser.
36570
- * @param message - What to say instead of the default when it is invalid.
36571
- * @returns A schema for the module's path.
36572
- */
36573
- function modulePath(name, server, message) {
36574
- let extension = server ? /\.server\.ts$/ : /\.(?:ts|tsx|js|jsx)$/;
36575
- return described(string().refine((value) => value.length <= 1024 && value.startsWith("./") && !/[\u0000-\u001f\u007f\\?#%]/.test(value) && extension.test(value) && (server || !/\.server\.[cm]?[jt]sx?$/.test(value)) && !value.slice(2).split("/").some((part) => !part || part === "." || part === ".." || part === "node_modules")), message ?? (server ? `${name} must be a built-in adapter or a ./ module ending in .server.ts inside the project.` : `${name} must be bud:beads or a ./ browser module ending in .ts, .tsx, .js or .jsx inside the project.`));
36576
- }
36577
- var builtInStores = enum_([
36578
- false,
36579
- "bud:beads",
36580
- "bud:memory",
36581
- "bud:git-comments"
36582
- ]);
36583
- function store(kind) {
36584
- let module = modulePath(`storage.${kind}`, true);
36585
- return defaulted(check(select((value) => value === false || value === "bud:beads" || value === "bud:memory" || value === "bud:git-comments" ? builtInStores : module), (value) => kind === "work" && value === "bud:git-comments" ? "storage.work cannot use bud:git-comments; it only stores threads." : void 0), "bud:beads");
36586
- }
36587
- /** The selected work and discussion stores, with Beads as the default for both. */
36588
- var BudStorageSchema = defaulted(settings("storage", {
36589
- work: store("work"),
36590
- threads: store("threads")
36591
- }), () => ({
36592
- work: "bud:beads",
36593
- threads: "bud:beads"
36594
- }));
36595
- /** Task presentation, chosen separately from its storage. */
36596
- var BudDesignsSchema = defaulted(settings("designs", {
36597
- tasks: defaulted(select((value) => value === "bud:beads" ? enum_(["bud:beads"]) : modulePath("designs.tasks", false)), "bud:beads"),
36598
- pages: defaulted(enum_(["path", "title"]), "path")
36599
- }), () => ({
36600
- tasks: "bud:beads",
36601
- pages: "path"
36602
- }));
36603
- //#endregion
36604
- //#region ../project/src/lib/picker.ts
36605
- var builtIn = /* @__PURE__ */ new Set([
36606
- "bud:dom",
36607
- "bud:react",
36608
- "bud:react-grab"
36830
+ //#region ../git/src/lib/validation.ts
36831
+ var text = String$1.check(isNonEmpty(), isPattern(/^[^\0]*$/));
36832
+ var oid = String$1.check(isPattern(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/));
36833
+ var path$1 = text.check(makeFilter((value) => !value.startsWith("/") && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== ".." && part.toLowerCase() !== ".git")));
36834
+ var ref$1 = text.check(makeFilter((value) => value.startsWith("refs/") && !/[\x00-\x20\x7f~^:?*\[\\]/.test(value) && !value.includes("..") && !value.includes("@{") && !value.endsWith(".") && value.split("/").every((part) => part.length > 0 && !part.startsWith(".") && !part.endsWith(".lock"))));
36835
+ var revision = Union([
36836
+ oid,
36837
+ ref$1,
36838
+ Literal("HEAD")
36609
36839
  ]);
36610
- var projectPicker = modulePath("picker", false, "picker must be bud:dom, bud:react, bud:react-grab or a ./ browser module ending in .ts, .tsx, .js or .jsx inside the project.");
36611
- /** The picker, with Bud's DOM picker as the default. */
36612
- var BudPickerSchema = createSchema((value, context) => {
36613
- if (value === void 0) return { value: {
36614
- name: "bud:dom",
36615
- module: null
36616
- } };
36617
- if (typeof value === "string" && builtIn.has(value)) return { value: {
36618
- name: value,
36619
- module: null
36620
- } };
36621
- let result = projectPicker["~run"](value, context);
36622
- return result.issues ? result : { value: {
36623
- name: result.value,
36624
- module: result.value
36625
- } };
36840
+ String$1.check(isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]*$/));
36841
+ var mode = Literals([
36842
+ "100644",
36843
+ "100755",
36844
+ "120000"
36845
+ ]);
36846
+ Struct({
36847
+ name: text.check(isPattern(/^[^\r\n<>]+$/)),
36848
+ email: text.check(isPattern(/^[^\r\n<>]+$/))
36626
36849
  });
36627
- //#endregion
36628
- //#region ../project/src/lib/previews.ts
36629
- var readyMessage = "ready must be a CSS selector, { selector } or { expression }.";
36630
- /** A readiness condition; a string is a selector. */
36631
- var PreviewReadySchema = createSchema((value, context) => {
36632
- let ready = typeof value === "string" ? { selector: value } : value;
36633
- if (ready && typeof ready === "object" && !Array.isArray(ready)) {
36634
- let entries = Object.entries(ready);
36635
- let [key, text] = entries[0] ?? [];
36636
- if (entries.length === 1 && (key === "selector" || key === "expression") && typeof text === "string" && text.trim() && text.length <= 4096) return { value: key === "selector" ? { selector: text.trim() } : { expression: text.trim() } };
36637
- }
36638
- return fail(readyMessage, context.path);
36850
+ Struct({
36851
+ ref: ref$1,
36852
+ expectedHead: NullOr(oid),
36853
+ files: ArraySchema(Struct({
36854
+ path: path$1,
36855
+ contents: Union([
36856
+ String$1,
36857
+ Uint8Array$1,
36858
+ Null
36859
+ ]),
36860
+ mode: optional$1(mode)
36861
+ })).check(isMaxLength(1e4)),
36862
+ message: text,
36863
+ extraParents: optional$1(ArraySchema(oid).check(isMaxLength(16)))
36639
36864
  });
36640
- var defaultThemes = () => ["light", "dark"];
36641
- var defaultViewports = () => [{
36642
- id: "desktop",
36643
- name: "Desktop",
36644
- width: 1440,
36645
- height: 900,
36646
- mobile: false
36647
- }];
36648
- var viewportsMessage = "previews.viewports must list up to 4 { name, width, height?, mobile? } with distinct names, widths from 240 to 3840 and heights from 240 to 4000.";
36649
- function viewportId(name) {
36650
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
36651
- }
36652
- var ViewportsSchema = createSchema((value, context) => {
36653
- if (!Array.isArray(value) || value.length === 0 || value.length > 4) return fail(viewportsMessage, context.path);
36654
- let viewports = [];
36655
- for (let entry of value) {
36656
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return fail(viewportsMessage, context.path);
36657
- let { name, width, height, mobile, ...rest } = entry;
36658
- let size = (pixels, from, to) => typeof pixels === "number" && Number.isInteger(pixels) && pixels >= from && pixels <= to;
36659
- if (Object.keys(rest).length || typeof name !== "string" || !name.trim() || name.length > 40 || !viewportId(name) || !size(width, 240, 3840) || height !== void 0 && !size(height, 240, 4e3) || mobile !== void 0 && typeof mobile !== "boolean") return fail(viewportsMessage, context.path);
36660
- let phone = mobile ?? width < 800;
36661
- viewports.push({
36662
- id: viewportId(name),
36663
- name: name.trim(),
36664
- width,
36665
- height: height ?? (phone ? Math.round(width * 844 / 390) : 900),
36666
- mobile: phone
36667
- });
36668
- }
36669
- if (new Set(viewports.map((viewport) => viewport.id)).size !== viewports.length) return fail(viewportsMessage, context.path);
36670
- return { value: viewports };
36865
+ Struct({
36866
+ ...Struct({
36867
+ base: optional$1(oid),
36868
+ target: optional$1(oid),
36869
+ staged: optional$1(Boolean$1),
36870
+ paths: optional$1(ArraySchema(path$1).check(isMaxLength(1e4)))
36871
+ }).fields,
36872
+ context: optional$1(Int$1.check(isBetween({
36873
+ minimum: 0,
36874
+ maximum: 1e3
36875
+ })))
36671
36876
  });
36672
- /** The preview settings, with their defaults filled in. */
36673
- var BudPreviewsSchema = defaulted(settings("previews", {
36674
- scale: fallback(described(number().refine((scale) => Number.isInteger(scale) && scale >= 1 && scale <= 3), "previews.scale must be 1, 2 or 3."), () => 2),
36675
- themes: fallback(described(array(enum_(["light", "dark"])).refine((themes) => themes.length > 0 && new Set(themes).size === themes.length), "previews.themes must list light, dark or both, once each."), defaultThemes),
36676
- viewports: fallback(ViewportsSchema, defaultViewports),
36677
- ready: fallback(PreviewReadySchema, () => null),
36678
- readyTimeout: fallback(described(number().refine((ms) => Number.isInteger(ms) && ms >= 1e3 && ms <= 6e5), "previews.readyTimeout must be whole milliseconds from 1000 to 600000."), () => 6e4),
36679
- css: fallback(described(string().refine((css) => css.length <= 2e4 && !css.includes("<")), "previews.css must be CSS text without <, up to 20000 characters."), () => "")
36680
- }), () => ({
36681
- scale: 2,
36682
- themes: defaultThemes(),
36683
- viewports: defaultViewports(),
36684
- ready: null,
36685
- readyTimeout: 6e4,
36686
- css: ""
36877
+ Struct({ environment: optional$1(Record(String$1, Union([String$1, Undefined]))) });
36878
+ Struct({
36879
+ commit: oid,
36880
+ path: path$1,
36881
+ limit: Int$1.check(isBetween({
36882
+ minimum: 1,
36883
+ maximum: 101
36884
+ })),
36885
+ skip: optional$1(Int$1.check(isBetween({
36886
+ minimum: 0,
36887
+ maximum: 1e4
36888
+ })))
36889
+ });
36890
+ Struct({
36891
+ from: revision,
36892
+ limit: Int$1.check(isBetween({
36893
+ minimum: 1,
36894
+ maximum: 500
36895
+ }))
36896
+ });
36897
+ Int$1.check(isBetween({
36898
+ minimum: 1,
36899
+ maximum: 1e3
36687
36900
  }));
36688
36901
  //#endregion
36689
- //#region ../project/src/lib/screen-routes.ts
36902
+ //#region ../git/src/lib/git.ts
36903
+ /** Opens the Git repository that contains a directory. */
36904
+ var Git = class extends Service()("@bud.sh/git/Git") {};
36905
+ //#endregion
36906
+ //#region ../live/src/server/hub.ts
36690
36907
  /**
36691
- * Validates a screen's route map without importing its component module.
36692
- * @param routes - A Remix route map shared with the screen's definition.
36693
- * @param path - The configured screen's static path.
36694
- * @returns Named patterns confined to the screen's path.
36908
+ * Orders every published change into one stream per companion. Clients resume from their
36909
+ * last event id; anything older than the replay buffer, or from an earlier companion,
36910
+ * receives a resync instead.
36695
36911
  */
36696
- function resolveScreenRoutes(routes, path) {
36697
- let result = [];
36698
- let seen = /* @__PURE__ */ new Set();
36699
- function visit(value, prefix) {
36700
- if (!value || typeof value !== "object" || Array.isArray(value) || seen.has(value)) throw new Error("Screen routes must be a Remix route map.");
36701
- seen.add(value);
36702
- for (let [key, entry] of Object.entries(value)) {
36703
- let name = prefix ? `${prefix}.${key}` : key;
36704
- if (!entry || typeof entry !== "object") throw new Error(`Screen route ${name} must be a Remix route.`);
36705
- if (!("pattern" in entry)) {
36706
- visit(entry, name);
36707
- continue;
36708
- }
36709
- if (!["GET", "ANY"].includes(entry.method) || typeof entry.href !== "function" || typeof entry.pattern?.source !== "string") throw new Error(`Screen route ${name} must be a GET route.`);
36710
- let pattern = RoutePattern.parse(entry.pattern.source);
36711
- let parts = pattern.toJSON();
36712
- let pathname = `/${parts.pathname}`;
36713
- if (parts.protocol || parts.hostname || parts.port || parts.pathname.split("/").some((segment) => /^\.{1,2}$/.test(decodeURIComponent(segment))) || pathname !== path && !pathname.startsWith(`${path}/`) && !pathname.startsWith(`${path}(/`)) throw new Error(`Screen route ${name} must stay under ${path}.`);
36714
- result.push({
36715
- name,
36716
- pattern: pattern.source
36717
- });
36718
- }
36719
- seen.delete(value);
36912
+ var LiveHub = class {
36913
+ epoch = randomBytes(8).toString("hex");
36914
+ #sequence = 0;
36915
+ #buffer = [];
36916
+ #replay;
36917
+ #heartbeatMs;
36918
+ #listeners = /* @__PURE__ */ new Set();
36919
+ constructor(options = {}) {
36920
+ this.#replay = options.replay ?? 1024;
36921
+ this.#heartbeatMs = options.heartbeatMs ?? 15e3;
36720
36922
  }
36721
- visit(routes, "");
36722
- if (!result.length) throw new Error("Screen routes must contain at least one route.");
36723
- return result;
36724
- }
36923
+ /**
36924
+ * Sends a change to every connected client.
36925
+ * @param topic - The domain, such as `previews` or `files`.
36926
+ * @param data - JSON data each domain validates with its own schema.
36927
+ */
36928
+ publish(topic, data) {
36929
+ let event = {
36930
+ id: `${this.epoch}:${++this.#sequence}`,
36931
+ topic,
36932
+ data
36933
+ };
36934
+ this.#buffer.push(event);
36935
+ if (this.#buffer.length > this.#replay) this.#buffer.shift();
36936
+ for (let listener of [...this.#listeners]) listener(event);
36937
+ }
36938
+ /**
36939
+ * Opens one client's `text/event-stream`.
36940
+ * @param lastEventId - The client's `Last-Event-ID`, if it is resuming.
36941
+ * @param signal - The request's lifetime; aborting closes the stream.
36942
+ * @returns The response body.
36943
+ */
36944
+ open(lastEventId, signal) {
36945
+ let encoder = new TextEncoder();
36946
+ let close = () => {};
36947
+ return new ReadableStream({
36948
+ start: (controller) => {
36949
+ let send = (text) => controller.enqueue(encoder.encode(text));
36950
+ let write = (event) => send(`id: ${event.id}\nevent: ${event.topic}\ndata: ${JSON.stringify(event.data)}\n\n`);
36951
+ let missed = this.#since(lastEventId);
36952
+ if (missed) for (let event of missed) write(event);
36953
+ else write({
36954
+ id: `${this.epoch}:${this.#sequence}`,
36955
+ topic: RESYNC,
36956
+ data: null
36957
+ });
36958
+ this.#listeners.add(write);
36959
+ let heartbeat = setInterval(() => send(": heartbeat\n\n"), this.#heartbeatMs);
36960
+ close = () => {
36961
+ clearInterval(heartbeat);
36962
+ this.#listeners.delete(write);
36963
+ try {
36964
+ controller.close();
36965
+ } catch {}
36966
+ };
36967
+ if (signal.aborted) close();
36968
+ else signal.addEventListener("abort", close, { once: true });
36969
+ },
36970
+ cancel: () => close()
36971
+ });
36972
+ }
36973
+ #since(lastEventId) {
36974
+ let last = lastEventId ? parseLiveId(lastEventId) : null;
36975
+ if (!last || last.epoch !== this.epoch || last.sequence > this.#sequence) return null;
36976
+ let oldest = this.#buffer[0] ? parseLiveId(this.#buffer[0].id).sequence : this.#sequence + 1;
36977
+ if (last.sequence < oldest - 1) return null;
36978
+ return this.#buffer.filter((event) => parseLiveId(event.id).sequence > last.sequence);
36979
+ }
36980
+ };
36725
36981
  //#endregion
36726
- //#region ../project/src/lib/screens.ts
36727
- /**
36728
- * Bud's browser of your live pages, with their comments.
36729
- * @returns The built-in screen, ready to relabel by spreading.
36730
- */
36731
- function pages() {
36732
- return {
36733
- id: "pages",
36734
- label: "Pages",
36735
- icon: "browser",
36736
- path: "/pages",
36737
- module: "bud:pages"
36738
- };
36739
- }
36740
- /**
36741
- * Bud's canvas of every route's screenshot.
36742
- * @returns The built-in screen, ready to relabel by spreading.
36743
- */
36744
- function canvas() {
36745
- return {
36746
- id: "canvas",
36747
- label: "Canvas",
36748
- icon: "grid",
36749
- path: "/canvas",
36750
- module: "bud:canvas"
36751
- };
36752
- }
36753
- /**
36754
- * Bud's Beads task board.
36755
- * @returns The built-in screen, ready to relabel by spreading.
36756
- */
36757
- function tasks() {
36758
- return {
36759
- id: "tasks",
36760
- label: "Tasks",
36761
- icon: "tasks",
36762
- path: "/tasks",
36763
- module: "bud:tasks"
36764
- };
36765
- }
36982
+ //#region ../live/src/server/service.ts
36983
+ /** The companion's ordered live channel: publishes changes and opens client streams. */
36984
+ var LiveChannel = class extends Service()("@bud.sh/live/LiveChannel") {};
36766
36985
  /**
36767
- * Bud's file browser and review.
36768
- * @returns The built-in screen, ready to relabel by spreading.
36986
+ * One ordered live channel per companion.
36987
+ * @param options - Replay buffer and heartbeat settings.
36988
+ * @returns The LiveChannel layer.
36769
36989
  */
36770
- function files() {
36771
- return {
36772
- id: "files",
36773
- label: "Files",
36774
- icon: "files",
36775
- path: "/files",
36776
- module: "bud:files"
36777
- };
36778
- }
36779
- /** The screens shown when the config doesn't list any. */
36780
- var defaultScreens = [
36781
- pages(),
36782
- canvas(),
36783
- tasks(),
36784
- files()
36785
- ];
36786
- var builtIns$1 = new Map(defaultScreens.map((screen) => [screen.module, screen]));
36787
- /** First path segments owned by Bud's routes, its development server and its public assets. */
36788
- var reservedScreenPaths = [
36789
- "/comments",
36790
- ...defaultScreens.map((screen) => screen.path),
36791
- "/api",
36792
- "/health",
36793
- "/__bud",
36794
- "/assets",
36795
- "/app",
36796
- "/src",
36797
- "/node_modules",
36798
- "/agents",
36799
- "/brand",
36800
- "/fonts",
36801
- "/frameworks",
36802
- "/licenses"
36803
- ];
36804
- var ScreenSchema = check(described(any().refine((value) => !!value && typeof value === "object"), "Each screen must be an object."), ({ id, label, icon = "layers", module, path }) => {
36805
- if (typeof id !== "string" || !/^[a-z][a-z0-9-]{0,31}$/.test(id)) return "Screen ids are 1-32 lowercase letters, digits and hyphens.";
36806
- if (typeof label !== "string" || !label.trim()) return `Screen ${id} needs a label.`;
36807
- if (typeof icon !== "string" || !icon) return `Screen ${id} needs an icon name.`;
36808
- if (typeof module !== "string") return `Screen ${id} needs a module.`;
36809
- let builtIn = builtIns$1.get(module);
36810
- path ??= builtIn?.path ?? `/${id}`;
36811
- if (builtIn) {
36812
- if (id !== builtIn.id || path !== builtIn.path) return `${module} keeps id "${builtIn.id}" and path "${builtIn.path}".`;
36813
- return;
36814
- }
36815
- if (!/^\.\/[^\0\\]+\.(?:ts|tsx|js|jsx)$/.test(module)) return `Screen ${id} must be a ./ module ending in .ts, .tsx, .js or .jsx.`;
36816
- if (typeof path !== "string" || !/^\/[a-z0-9][a-z0-9-]*$/.test(path)) return `Screen ${id} needs one static path segment, such as /${id}.`;
36817
- if (defaultScreens.some((screen) => screen.id === id)) return `Screen id "${id}" is reserved for a built-in screen.`;
36818
- if (reservedScreenPaths.includes(path)) return `Bud reserves the path ${path}.`;
36819
- });
36820
- /** Bud's navigation in order; file and icon checks need the app and happen at startup. */
36821
- var BudScreensSchema = defaulted(delegate(described(array(any()).refine((screens) => screens.length > 0), "screens must list at least one screen."), (screens) => {
36822
- let ids = /* @__PURE__ */ new Set();
36823
- let paths = /* @__PURE__ */ new Set();
36824
- return screens.map((value) => {
36825
- let input = parseConfig(ScreenSchema, value);
36826
- let { id, label, icon = "layers", module } = input;
36827
- let path = input.path ?? builtIns$1.get(module)?.path ?? `/${id}`;
36828
- if (ids.has(id) || paths.has(path)) throw new Error("Screen ids and paths must be unique.");
36829
- ids.add(id);
36830
- paths.add(path);
36831
- if (input.routes !== void 0) resolveScreenRoutes(input.routes, path);
36990
+ function liveChannelLayer(options = {}) {
36991
+ return sync$1(LiveChannel, () => {
36992
+ let hub = new LiveHub(options);
36832
36993
  return {
36833
- id,
36834
- label,
36835
- icon,
36836
- path,
36837
- module,
36838
- ...input.routes === void 0 ? {} : { routes: input.routes }
36994
+ epoch: hub.epoch,
36995
+ publish: (topic, data) => sync(() => hub.publish(topic, data)),
36996
+ open: (lastEventId, signal) => hub.open(lastEventId, signal)
36839
36997
  };
36840
36998
  });
36841
- }), () => defaultScreens.map((screen) => ({ ...screen })));
36842
- //#endregion
36843
- //#region ../project/src/lib/config.ts
36844
- var objectLike = (value) => !!value && typeof value === "object";
36845
- function section(message, shape) {
36846
- let schema = object(shape);
36847
- return createSchema((value, context) => objectLike(value) ? schema["~run"](Array.isArray(value) ? { ...value } : value, context) : fail(message, context.path));
36848
- }
36849
- var ConfigFields = check(section("Bud config must export an object.", {
36850
- routes: described(array(any()).refine((routes) => routes.length > 0), "bud.config must define at least one route."),
36851
- name: optional(described(string().refine((name) => !!name.trim()), "name must be a nonempty string.")),
36852
- target: optional(section("target must be an object.", {
36853
- origin: any(),
36854
- port: any()
36855
- })),
36856
- comments: optional(section("comments must be an object.", { remote: any() })),
36857
- port: any(),
36858
- screens: any(),
36859
- storage: any(),
36860
- designs: any(),
36861
- previews: any(),
36862
- files: any(),
36863
- logo: any(),
36864
- picker: any(),
36865
- branches: any(),
36866
- flows: any()
36867
- }), ({ target }) => {
36868
- if (target?.port === 0) return "target.port must be a listening port, not zero.";
36869
- if (target?.origin !== void 0 && (typeof target.origin !== "string" || !target.origin.trim())) return "target.origin must be an HTTP(S) origin.";
36870
- });
36871
- var RouteSchema = check(any().transform((input) => typeof input === "string" ? {
36872
- path: input,
36873
- title: input === "/" ? "Home" : input
36874
- } : input), (route) => {
36875
- if (!route || typeof route.path !== "string" || !route.path.startsWith("/") || route.path.startsWith("//")) return "Canvas routes must be local paths beginning with a single /.";
36876
- if (new URL(route.path, "http://bud.invalid").origin !== "http://bud.invalid" || route.path.includes("\\")) return "Canvas routes must stay on the target origin.";
36877
- let id = route.id ?? routePath(route.path);
36878
- if (typeof id !== "string" || !id.trim()) return "Canvas route IDs and paths must be unique.";
36879
- if (typeof route.title !== "string" || !route.title.trim()) return "Each route needs a title.";
36880
- if (route.pattern !== void 0 && (typeof route.pattern !== "string" || !route.pattern.startsWith("/"))) return "A route pattern must be a path beginning with /.";
36881
- if (route.group !== void 0 && (typeof route.group !== "string" || !route.group.trim() || route.group.length > 120)) return "A route group must be a short name.";
36882
- if (route.ready !== void 0 && route.ready !== false && !parseSafe(PreviewReadySchema, route.ready).success) return "A route ready must be false, a CSS selector, { selector } or { expression }.";
36883
- }).transform((route) => {
36884
- let path = routePath(route.path);
36885
- return {
36886
- id: route.id ?? path,
36887
- path,
36888
- title: route.title,
36889
- ...route.pattern === void 0 ? {} : { pattern: route.pattern },
36890
- ...route.group === void 0 ? {} : { group: route.group.trim() },
36891
- ...route.ready === void 0 ? {} : { ready: route.ready === false ? null : parseConfig(PreviewReadySchema, route.ready) }
36892
- };
36893
- });
36894
- function routePath(path) {
36895
- let url = new URL(path, "http://bud.invalid");
36896
- let search = url.searchParams.toString();
36897
- return `${url.pathname}${search ? `?${search}` : ""}${url.hash}`;
36898
36999
  }
36899
- /** Canvas routes with unique IDs and normalized paths. */
36900
- var BudRoutesSchema = check(list(RouteSchema, "bud.config must define at least one route.", 1), (routes) => new Set(routes.map((route) => route.id)).size !== routes.length || new Set(routes.map((route) => route.path)).size !== routes.length ? "Canvas route IDs and paths must be unique." : void 0);
36901
- var port = (value) => value === void 0 || Number.isInteger(value) && value >= 0 && value <= 65535;
36902
- var remote = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value);
36903
- /** A project's config, validated and normalized before Bud starts any listeners. */
36904
- var BudConfigSchema = delegate(check(delegate(check(delegate(check(delegate(ConfigFields, (config) => ({
36905
- ...config,
36906
- routes: parseConfig(BudRoutesSchema, config.routes)
36907
- })), ({ port: companion, target }) => port(companion ?? 0) && port(target?.port) ? void 0 : "Ports must be integers between 0 and 65535."), ({ target, ...config }) => {
36908
- let origin = target?.origin ?? (target?.port ? `http://127.0.0.1:${target.port}` : void 0);
36909
- return {
36910
- ...config,
36911
- targetOrigin: origin ? normalizeOrigin(origin, "target.origin must be an HTTP(S) origin without a path or credentials.") : origin
36912
- };
36913
- }), ({ comments }) => remote(comments?.remote ?? "origin") ? void 0 : "comments.remote must be a Git remote name."), (config) => {
36914
- let flows = parseConfig(BudFlowsSchema, config.flows);
36915
- let steps = flows.flatMap((flow) => flow.steps.map((step) => flowStepRoute(flow, step)));
36916
- let home = config.routes.findIndex((route) => route.path === "/") + 1;
36917
- return {
36918
- ...config,
36919
- flows,
36920
- routes: [
36921
- ...config.routes.slice(0, home),
36922
- ...steps,
36923
- ...config.routes.slice(home)
36924
- ]
36925
- };
36926
- }), ({ routes }) => new Set(routes.map((route) => route.id)).size !== routes.length || new Set(routes.map((route) => route.path)).size !== routes.length ? "A flow step must not reuse a route’s id or path." : void 0), (config) => ({
36927
- name: config.name ?? "Your project",
36928
- routes: config.routes,
36929
- flows: config.flows,
36930
- port: config.port ?? 0,
36931
- targetOrigin: config.targetOrigin,
36932
- remote: config.comments?.remote ?? "origin",
36933
- screens: parseConfig(BudScreensSchema, config.screens),
36934
- storage: parseConfig(BudStorageSchema, config.storage),
36935
- designs: parseConfig(BudDesignsSchema, config.designs),
36936
- previews: parseConfig(BudPreviewsSchema, config.previews),
36937
- files: parseConfig(BudFilesSchema, config.files),
36938
- logo: parseConfig(BudLogoSchema, config.logo),
36939
- picker: parseConfig(BudPickerSchema, config.picker),
36940
- branches: parseConfig(BudBranchesSchema, config.branches)
36941
- }));
36942
37000
  /**
36943
- * Validates executable project config before starting any listeners.
36944
- * @param value - The config module's default export.
36945
- * @returns Normalized route IDs, ports, and URLs.
37001
+ * Publishes every element of a domain's change stream on the live channel, for the lifetime
37002
+ * of the layer that forks it.
37003
+ * @param topic - The topic clients subscribe to.
37004
+ * @param changes - The domain's change stream.
37005
+ * @returns An effect to fork in a scoped layer.
36946
37006
  */
36947
- function resolveBudConfig(value) {
36948
- return parseConfig(BudConfigSchema, value);
37007
+ function publishChanges(topic, changes) {
37008
+ return gen(function* () {
37009
+ let channel = yield* LiveChannel;
37010
+ yield* changes.pipe(runForEach((change) => channel.publish(topic, change)), ignore$2, forkScoped);
37011
+ });
36949
37012
  }
37013
+ //#endregion
37014
+ //#region ../project/src/server/errors.ts
37015
+ /** Expected project startup/configuration failure; nested causes remain server-only. */
37016
+ var ProjectError = class extends TaggedError("ProjectError") {};
37017
+ //#endregion
37018
+ //#region ../project/src/server/modules.ts
36950
37019
  /**
36951
- * Returns a bare HTTP(S) origin, rejecting credentials, paths, queries and fragments.
36952
- * @param origin - The URL to check.
36953
- * @param message - The error's message when it is not a bare origin.
36954
- * @returns The URL's origin.
37020
+ * Confines a validated config module to a regular, bounded file in its project.
37021
+ * @param root - The trusted consumer directory.
37022
+ * @param module - A project-relative module already validated by the config resolver.
37023
+ * @param options - The config setting and whether its source runs on the server or browser.
37024
+ * @returns The canonical filename, without evaluating its code.
36955
37025
  */
36956
- function normalizeOrigin(origin, message) {
36957
- let url = new URL(origin);
36958
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error(message);
36959
- return url.origin;
37026
+ function projectModuleFile(root, module, options) {
37027
+ return gen(function* () {
37028
+ let fs = yield* FileSystem;
37029
+ let path = yield* Path$1;
37030
+ let { setting, kind } = options;
37031
+ let project = yield* fs.realPath(root).pipe(mapError$2((cause) => new ProjectError({
37032
+ reason: "root",
37033
+ message: "Cannot open the module project root.",
37034
+ cause
37035
+ })));
37036
+ let file = yield* fs.realPath(path.resolve(project, module)).pipe(mapError$2((cause) => new ProjectError({
37037
+ reason: "read",
37038
+ message: `${setting} cannot find ${module} in the project.`,
37039
+ cause
37040
+ })));
37041
+ let relative = path.relative(project, file);
37042
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || relative.split(path.sep).includes("node_modules")) return yield* new ProjectError({
37043
+ reason: "invalid",
37044
+ message: `${setting} must resolve to a project-owned file outside node_modules.`
37045
+ });
37046
+ if (kind === "server" && !file.endsWith(".server.ts")) return yield* new ProjectError({
37047
+ reason: "invalid",
37048
+ message: `${setting} must resolve to a .server.ts module.`
37049
+ });
37050
+ if (kind === "browser" && (!/\.(?:ts|tsx|js|jsx)$/.test(file) || /\.server\.[cm]?[jt]sx?$/.test(file))) return yield* new ProjectError({
37051
+ reason: "invalid",
37052
+ message: `${setting} must resolve to a browser module.`
37053
+ });
37054
+ if ((yield* fs.stat(file).pipe(mapError$2((cause) => new ProjectError({
37055
+ reason: "read",
37056
+ message: `${setting} cannot inspect ${module}.`,
37057
+ cause
37058
+ })))).type !== "File") return yield* new ProjectError({
37059
+ reason: "invalid",
37060
+ message: `${setting} must be a regular module file.`
37061
+ });
37062
+ yield* readText(file, { maxBytes: 262144 }).pipe(mapError$2((cause) => new ProjectError({
37063
+ reason: "read",
37064
+ message: `${setting} must be a readable module no larger than 256 KiB.`,
37065
+ cause
37066
+ })));
37067
+ return file;
37068
+ });
36960
37069
  }
36961
37070
  //#endregion
36962
37071
  //#region ../project/src/server/config.ts
@@ -38007,7 +38116,7 @@ function createAppController(template, project, runtime) {
38007
38116
  let title = (conversation ? "Discussion" : screen.label).replace(/[&<>]/g, (character) => `&#${character.charCodeAt(0)};`);
38008
38117
  let logo = project?.config.logo;
38009
38118
  let shell = logo ? withLogoIcons(template, logo, (theme) => routes.logo.href({ theme })) : template;
38010
- return new Response(shell.replace("<title>bud.sh</title>", () => `<title>${title} · bud.sh</title>`).replace("<!--app-html-->", () => content), { headers: { "Content-Type": "text/html; charset=utf-8" } });
38119
+ return new Response(shell.replace("<title>bud.sh</title>", () => `<title>${title} · bud.sh</title>`).replace("</head>", () => project ? `${themeStyle(project.config.theme)}</head>` : "</head>").replace("<!--app-html-->", () => content), { headers: { "Content-Type": "text/html; charset=utf-8" } });
38011
38120
  }
38012
38121
  return createController(routes, { actions: {
38013
38122
  conversation({ params, request }) {
@@ -41323,6 +41432,13 @@ async function createNodeHost(options) {
41323
41432
  return response;
41324
41433
  };
41325
41434
  let { config } = await (await call("project")).json();
41435
+ if (options.theme) config = {
41436
+ ...config,
41437
+ theme: resolveBudConfig({
41438
+ routes: ["/"],
41439
+ theme: options.theme
41440
+ }).theme
41441
+ };
41326
41442
  let client = options.client ?? fileURLToPath(new URL("../client/", import.meta.url));
41327
41443
  let template = await readFile(join(client, "index.html"), "utf8");
41328
41444
  let pictures = httpPreviewStorage(remote("store"), {