konpeki 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { createHash, randomBytes } from "node:crypto";
3
- import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
4
3
  import { basename, dirname, join, resolve } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
5
  import { createServer } from "vite";
7
- import { tmpdir } from "node:os";
6
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
8
7
  import { Ajv2020 } from "ajv/dist/2020.js";
9
8
  import "@xmldom/xmldom";
9
+ import lockfile from "proper-lockfile";
10
10
  //#region composition/types.ts
11
- var compositionSchema = "konpeki-composition/v18";
12
- var legacyCompositionSchemas = [
13
- "konpeki-composition/v1",
14
- "konpeki-composition/v2",
15
- "konpeki-composition/v3",
16
- "konpeki-composition/v4",
17
- "konpeki-composition/v5",
18
- "konpeki-composition/v6",
19
- "konpeki-composition/v7",
20
- "konpeki-composition/v8",
21
- "konpeki-composition/v9",
22
- "konpeki-composition/v10",
23
- "konpeki-composition/v11",
24
- "konpeki-composition/v12",
25
- "konpeki-composition/v13",
26
- "konpeki-composition/v14",
27
- "konpeki-composition/v15",
28
- "konpeki-composition/v16",
29
- "konpeki-composition/v17"
30
- ];
11
+ var compositionSchema = "konpeki-composition/v1";
31
12
  var canvasSize = {
32
13
  width: 1920,
33
14
  height: 1080
@@ -341,17 +322,6 @@ var chartTemplates = Object.keys({
341
322
  }
342
323
  });
343
324
  Array.from(new Set(Object.values(diagramDefinitions).map((definition) => definition.category)));
344
- function migrateLegacyDiagramType(type, layout) {
345
- if (typeof type !== "string" || typeof layout !== "string") return void 0;
346
- return {
347
- "process/linear": "process",
348
- "process/branching": "flowchart",
349
- "process/cyclical": "loop-flywheel",
350
- "system/layered": "layer-stack",
351
- "system/request-flow": "sequence",
352
- "system/hub-and-spoke": "architecture"
353
- }[`${type}/${layout}`];
354
- }
355
325
  //#endregion
356
326
  //#region composition/theme-tokens.ts
357
327
  /** Explicit opt-in bindings. Literal SVG attributes always remain fixed. */
@@ -737,7 +707,7 @@ var components = componentKinds.map((kind) => {
737
707
  });
738
708
  var schema = {
739
709
  $schema: "https://json-schema.org/draft/2020-12/schema",
740
- $id: "https://vcfgdev.github.io/konpeki/composition/v18/schema.json",
710
+ $id: "https://vcfgdev.github.io/konpeki/composition/v1/schema.json",
741
711
  title: compositionSchema,
742
712
  ...object({
743
713
  schema: { const: compositionSchema },
@@ -928,388 +898,15 @@ var structural = new Ajv2020({
928
898
  allErrors: true,
929
899
  strict: false
930
900
  }).compile(schema);
931
- function migratePreV5Document(input) {
932
- const slide = input.slide;
933
- const components = Array.isArray(slide?.components) ? slide.components.map((value) => {
934
- if (!value || typeof value !== "object") return value;
935
- const component = value;
936
- if (component.kind !== "callout" && component.kind !== "comparison" && component.kind !== "text-block") return component;
937
- const previous = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
938
- const common = Object.fromEntries([
939
- "alignment",
940
- "border",
941
- "titleStyle",
942
- "rule"
943
- ].flatMap((key) => key in previous ? [[key, previous[key]]] : []));
944
- const appearance = component.kind === "callout" ? {
945
- ...common,
946
- layout: "single",
947
- orientation: "horizontal",
948
- purpose: "emphasis",
949
- treatment: "strong"
950
- } : component.kind === "comparison" ? {
951
- ...common,
952
- layout: "two-column",
953
- orientation: "horizontal",
954
- purpose: "comparison",
955
- treatment: "plain"
956
- } : {
957
- ...previous,
958
- purpose: previous.purpose ?? "narrative",
959
- treatment: previous.treatment ?? "plain"
960
- };
961
- return {
962
- ...component,
963
- kind: "text-block",
964
- appearance
965
- };
966
- }) : slide?.components;
967
- return {
968
- ...Object.fromEntries(Object.entries(input).filter(([key]) => key !== "target" && key !== "slide")),
969
- schema: "konpeki-composition/v5",
970
- ...slide ? { slides: [{
971
- ...slide,
972
- name: "Slide 01",
973
- components
974
- }] } : {}
975
- };
976
- }
977
- function migrateV5Document(input) {
978
- const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
979
- if (!value || typeof value !== "object") return value;
980
- const slide = value;
981
- const previousComponents = Array.isArray(slide.components) ? slide.components : [];
982
- const pageNumber = previousComponents.find((component) => component.kind === "page-number");
983
- const pageAppearance = pageNumber?.appearance && typeof pageNumber.appearance === "object" ? pageNumber.appearance : {};
984
- const removedIds = new Set(previousComponents.filter((component) => component.kind === "page-number").map((component) => String(component.id)));
985
- const removedSlots = new Set(previousComponents.filter((component) => component.kind === "page-number").flatMap((component) => Array.isArray(component.slotIds) ? component.slotIds.map(String) : []));
986
- const components = previousComponents.flatMap((component) => {
987
- const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
988
- if (component.kind === "page-number") return [];
989
- if (component.kind === "evidence") return [{
990
- ...component,
991
- kind: "visual"
992
- }];
993
- if (component.kind === "process") return [{
994
- ...component,
995
- kind: "diagram",
996
- appearance: {
997
- ...appearance,
998
- type: "process"
999
- }
1000
- }];
1001
- if (component.kind === "system-map") return [{
1002
- ...component,
1003
- kind: "diagram",
1004
- appearance: {
1005
- ...appearance,
1006
- type: "system"
1007
- }
1008
- }];
1009
- return [component];
1010
- });
1011
- const componentIds = new Set(components.map((component) => String(component.id)));
1012
- const groups = Array.isArray(slide.groups) ? slide.groups.map((group) => ({
1013
- ...group,
1014
- childIds: Array.isArray(group.childIds) ? group.childIds.filter((id) => !removedIds.has(String(id))) : group.childIds
1015
- })).filter((group) => !Array.isArray(group.childIds) || group.childIds.length > 0) : slide.groups;
1016
- const groupIds = new Set(Array.isArray(groups) ? groups.map((group) => String(group.id)) : []);
1017
- const contentSlots = Array.isArray(slide.contentSlots) ? slide.contentSlots.filter((slot) => !removedSlots.has(String(slot.id))).map((slot) => {
1018
- if (!Array.isArray(slot.targets)) return slot;
1019
- const targets = slot.targets.filter((target) => !removedIds.has(String(target)) && !removedSlots.has(String(target)));
1020
- return {
1021
- ...slot,
1022
- targets: targets.length ? targets : [String(components[0]?.id ?? "")]
1023
- };
1024
- }) : slide.contentSlots;
1025
- return {
1026
- ...slide,
1027
- pageNumber: {
1028
- style: pageNumber ? pageAppearance.style ?? "01" : "none",
1029
- color: pageAppearance.color ?? "muted"
1030
- },
1031
- components,
1032
- contentSlots,
1033
- groups,
1034
- readingOrder: Array.isArray(slide.readingOrder) ? slide.readingOrder.filter((entry) => entry.kind === "group" ? groupIds.has(String(entry.id)) : componentIds.has(String(entry.id))) : slide.readingOrder,
1035
- paintOrder: Array.isArray(slide.paintOrder) ? slide.paintOrder.filter((id) => !removedIds.has(String(id))) : slide.paintOrder,
1036
- relationships: Array.isArray(slide.relationships) ? slide.relationships.filter((relationship) => {
1037
- const from = relationship.from;
1038
- const to = relationship.to;
1039
- return !removedIds.has(String(from?.nodeId)) && !removedIds.has(String(to?.nodeId));
1040
- }) : slide.relationships
1041
- };
1042
- }) : input.slides;
1043
- return {
1044
- ...input,
1045
- schema: compositionSchema,
1046
- slides
1047
- };
1048
- }
1049
- function migrateV7Document(input) {
1050
- const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
1051
- if (!value || typeof value !== "object") return value;
1052
- const slide = value;
1053
- const components = Array.isArray(slide.components) ? slide.components.map((value) => {
1054
- if (!value || typeof value !== "object") return value;
1055
- const component = value;
1056
- const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
1057
- if (component.kind !== "visual" || appearance.template !== "image") return component;
1058
- return {
1059
- ...component,
1060
- kind: "image",
1061
- appearance: {
1062
- ...appearance.border ? { border: appearance.border } : {},
1063
- fit: "contain"
1064
- }
1065
- };
1066
- }) : slide.components;
1067
- return {
1068
- ...slide,
1069
- components
1070
- };
1071
- }) : input.slides;
1072
- return {
1073
- ...input,
1074
- schema: compositionSchema,
1075
- slides
1076
- };
1077
- }
1078
- function migrateV10Document(input) {
1079
- const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
1080
- if (!value || typeof value !== "object") return value;
1081
- const slide = value;
1082
- const components = Array.isArray(slide.components) ? slide.components.map((value) => {
1083
- if (!value || typeof value !== "object") return value;
1084
- const component = value;
1085
- const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
1086
- if (component.kind === "headline") return {
1087
- ...component,
1088
- kind: "text-block",
1089
- appearance: {
1090
- ...appearance,
1091
- role: "title",
1092
- layout: "single",
1093
- orientation: "horizontal",
1094
- purpose: "narrative",
1095
- treatment: "plain",
1096
- logicalOrder: "none"
1097
- }
1098
- };
1099
- if (component.kind === "footnote") return {
1100
- ...component,
1101
- kind: "text-block",
1102
- appearance: {
1103
- ...appearance,
1104
- role: "footnote",
1105
- layout: "single",
1106
- orientation: "horizontal",
1107
- purpose: "narrative",
1108
- treatment: "plain",
1109
- logicalOrder: "none",
1110
- titleStyle: "plain"
1111
- }
1112
- };
1113
- if (component.kind === "text-block") return {
1114
- ...component,
1115
- appearance: {
1116
- role: "body",
1117
- ...appearance
1118
- }
1119
- };
1120
- if (component.kind === "visual") return {
1121
- ...component,
1122
- kind: "chart"
1123
- };
1124
- if (component.kind === "diagram") {
1125
- const { template, ...rest } = appearance;
1126
- return {
1127
- ...component,
1128
- appearance: {
1129
- ...rest,
1130
- layout: template ?? appearance.layout ?? "linear"
1131
- }
1132
- };
1133
- }
1134
- if (component.kind === "icon" || component.kind === "shape") {
1135
- const slotIds = Array.isArray(component.slotIds) ? component.slotIds.map(String) : [];
1136
- const slotId = slotIds[0] ?? `${String(component.id)}-content`;
1137
- const primitive = component.kind === "icon" ? {
1138
- kind: "icon",
1139
- ...typeof component.intent === "string" && component.intent.trim() ? { name: component.intent.trim() } : {},
1140
- ...appearance.style ? { style: appearance.style } : {},
1141
- ...appearance.color ? { color: appearance.color } : {}
1142
- } : {
1143
- kind: "shape",
1144
- shape: appearance.shape ?? "rectangle",
1145
- ...appearance.color ? { color: appearance.color } : {},
1146
- ...appearance.fill ? { fill: appearance.fill } : {}
1147
- };
1148
- return {
1149
- ...component,
1150
- kind: "diagram",
1151
- slotIds: slotIds.length ? slotIds : [slotId],
1152
- appearance: {
1153
- type: "system",
1154
- layout: "hub-and-spoke",
1155
- border: appearance.border ?? "none",
1156
- colorScheme: "accent-with-muted-context",
1157
- emphasis: "primary",
1158
- density: "sparse"
1159
- },
1160
- topology: {
1161
- kind: "explicit",
1162
- nodes: (slotIds.length ? slotIds : [slotId]).map((currentSlotId, index) => ({
1163
- id: index ? `${String(component.id)}-slot-${index + 1}` : String(component.id),
1164
- slotId: currentSlotId,
1165
- ...index ? { visible: false } : { primitive },
1166
- ...component.preferredRect && typeof component.preferredRect === "object" ? { preferredRect: component.preferredRect } : {}
1167
- })),
1168
- edges: []
1169
- }
1170
- };
1171
- }
1172
- return component;
1173
- }) : slide.components;
1174
- const contentSlots = Array.isArray(slide.contentSlots) ? slide.contentSlots.map((value) => {
1175
- if (!value || typeof value !== "object") return value;
1176
- const slot = value;
1177
- return slot.role === "icon" || slot.role === "shape" ? {
1178
- ...slot,
1179
- role: "entity"
1180
- } : slot;
1181
- }) : slide.contentSlots;
1182
- return {
1183
- ...slide,
1184
- components,
1185
- contentSlots
1186
- };
1187
- }) : input.slides;
1188
- return {
1189
- ...input,
1190
- schema: "konpeki-composition/v11",
1191
- slides
1192
- };
1193
- }
1194
- function migrateV11Document(input) {
1195
- const slides = Array.isArray(input.slides) ? input.slides.map((value) => {
1196
- if (!value || typeof value !== "object") return value;
1197
- const slide = value;
1198
- const components = Array.isArray(slide.components) ? slide.components.map((value) => {
1199
- if (!value || typeof value !== "object") return value;
1200
- const component = value;
1201
- if (component.kind !== "diagram") return component;
1202
- const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
1203
- const migratedType = migrateLegacyDiagramType(appearance.type, appearance.layout);
1204
- const { layout, ...rest } = appearance;
1205
- return {
1206
- ...component,
1207
- appearance: {
1208
- ...rest,
1209
- type: migratedType ?? null
1210
- }
1211
- };
1212
- }) : slide.components;
1213
- return {
1214
- ...slide,
1215
- components
1216
- };
1217
- }) : input.slides;
1218
- return {
1219
- ...input,
1220
- schema: compositionSchema,
1221
- slides
1222
- };
1223
- }
1224
- function migrateInterimV12Sankey(input) {
1225
- if (!input || typeof input !== "object") return input;
1226
- const document = input;
1227
- if (document.schema !== "konpeki-composition/v18" || !Array.isArray(document.slides)) return input;
1228
- const slides = document.slides.map((value) => {
1229
- if (!value || typeof value !== "object") return value;
1230
- const slide = value;
1231
- if (!Array.isArray(slide.components)) return value;
1232
- const components = slide.components.map((componentValue) => {
1233
- if (!componentValue || typeof componentValue !== "object") return componentValue;
1234
- const component = componentValue;
1235
- const appearance = component.appearance && typeof component.appearance === "object" ? component.appearance : {};
1236
- if (component.kind !== "diagram" || appearance.type !== "sankey") return componentValue;
1237
- const { type: _type, ...sharedAppearance } = appearance;
1238
- return {
1239
- ...component,
1240
- kind: "chart",
1241
- appearance: {
1242
- ...sharedAppearance,
1243
- template: "sankey"
1244
- }
1245
- };
1246
- });
1247
- return {
1248
- ...slide,
1249
- components
1250
- };
1251
- });
1252
- return {
1253
- ...document,
1254
- slides
1255
- };
1256
- }
1257
- function preserveLegacyVisualizationChoices(input) {
1258
- if (!input || typeof input !== "object") return input;
1259
- const document = input;
1260
- if (!Array.isArray(document.slides)) return input;
1261
- return {
1262
- ...document,
1263
- slides: document.slides.map((value) => {
1264
- if (!value || typeof value !== "object") return value;
1265
- const slide = value;
1266
- if (!Array.isArray(slide.components)) return value;
1267
- return {
1268
- ...slide,
1269
- components: slide.components.map((value) => {
1270
- if (!value || typeof value !== "object") return value;
1271
- const component = value;
1272
- if (component.kind !== "diagram" && component.kind !== "chart" || !component.appearance || typeof component.appearance !== "object") return value;
1273
- return {
1274
- ...component,
1275
- appearance: {
1276
- ...component.appearance,
1277
- selection: "explicit"
1278
- }
1279
- };
1280
- })
1281
- };
1282
- })
1283
- };
1284
- }
1285
901
  function validateComposition(input) {
1286
- const legacy = Boolean(input && typeof input === "object" && "schema" in input && legacyCompositionSchemas.includes(input.schema));
1287
- const legacyInput = input;
1288
- const normalized = migrateInterimV12Sankey(legacy ? [
1289
- "konpeki-composition/v12",
1290
- "konpeki-composition/v13",
1291
- "konpeki-composition/v14",
1292
- "konpeki-composition/v15",
1293
- "konpeki-composition/v16",
1294
- "konpeki-composition/v17"
1295
- ].includes(String(legacyInput.schema)) ? {
1296
- ...legacyInput,
1297
- schema: compositionSchema
1298
- } : migrateV11Document(legacyInput.schema === "konpeki-composition/v11" ? legacyInput : migrateV10Document([
1299
- "konpeki-composition/v6",
1300
- "konpeki-composition/v7",
1301
- "konpeki-composition/v8",
1302
- "konpeki-composition/v9",
1303
- "konpeki-composition/v10"
1304
- ].includes(String(legacyInput.schema)) ? migrateV7Document(legacyInput) : migrateV7Document(migrateV5Document(legacyInput.schema === "konpeki-composition/v5" ? legacyInput : migratePreV5Document(legacyInput))))) : input);
1305
- const candidate = legacy ? preserveLegacyVisualizationChoices(normalized) : normalized;
1306
- if (!structural(candidate)) return {
902
+ if (!structural(input)) return {
1307
903
  ok: false,
1308
904
  issues: (structural.errors ?? []).map((error) => ({
1309
905
  path: error.instancePath,
1310
906
  message: error.message ?? "Invalid value"
1311
907
  }))
1312
908
  };
909
+ const candidate = input;
1313
910
  const issues = [];
1314
911
  const fail = (path, message) => issues.push({
1315
912
  path,
@@ -1418,16 +1015,7 @@ function validateComposition(input) {
1418
1015
  issues
1419
1016
  } : {
1420
1017
  ok: true,
1421
- document: legacy ? {
1422
- ...candidate,
1423
- slides: candidate.slides.map((slide) => ({
1424
- ...slide,
1425
- components: slide.components.map((component) => component.kind === "text-block" && !component.customVisual && component.content === void 0 ? {
1426
- ...component,
1427
- content: component.intent ?? ""
1428
- } : component)
1429
- }))
1430
- } : candidate
1018
+ document: input
1431
1019
  };
1432
1020
  }
1433
1021
  //#endregion
@@ -1461,10 +1049,6 @@ function parseCompositionJSON(raw) {
1461
1049
  function revisionFor(value) {
1462
1050
  return createHash("sha256").update(value).digest("hex");
1463
1051
  }
1464
- function requestFileFor(compositionPath) {
1465
- const key = revisionFor(resolve(compositionPath));
1466
- return join(tmpdir(), "konpeki", `${key}.request.json`);
1467
- }
1468
1052
  async function readCompositionFile(compositionPath) {
1469
1053
  const raw = await readFile(compositionPath, "utf8");
1470
1054
  const parsed = parseCompositionJSON(raw);
@@ -1500,28 +1084,134 @@ async function saveCompositionFile(compositionPath, expectedRevision, document)
1500
1084
  }
1501
1085
  return { revision: revisionFor(value) };
1502
1086
  }
1503
- async function writeBuildRequest(compositionPath, request) {
1504
- const current = await readCompositionFile(compositionPath);
1505
- if (current.revision !== request.revision) throw Object.assign(/* @__PURE__ */ new Error("Save the latest composition before requesting a build."), {
1506
- code: "REVISION_CONFLICT",
1507
- revision: current.revision
1087
+ //#endregion
1088
+ //#region src/lib/review.ts
1089
+ var emptyReview = () => ({
1090
+ schema: "konpeki-review/v1",
1091
+ version: 0,
1092
+ notes: []
1093
+ });
1094
+ var activeRequest = (request) => request?.status === "submitted" || request?.status === "working";
1095
+ //#endregion
1096
+ //#region bin/review-store.ts
1097
+ var reviewFileFor = (path) => `${resolve(path)}.review.json`;
1098
+ var conflict = (message) => Object.assign(new Error(message), { code: "REVISION_CONFLICT" });
1099
+ var invalid = (message) => Object.assign(new Error(message), { code: "INVALID_REVIEW" });
1100
+ async function readReview(path) {
1101
+ try {
1102
+ const state = JSON.parse(await readFile(reviewFileFor(path), "utf8"));
1103
+ if (state.schema !== "konpeki-review/v1") throw invalid("Unsupported revision-note format.");
1104
+ return state;
1105
+ } catch (error) {
1106
+ if (error.code === "ENOENT") return emptyReview();
1107
+ throw error;
1108
+ }
1109
+ }
1110
+ async function mutate(path, change) {
1111
+ const file = reviewFileFor(path);
1112
+ const release = await lockfile.lock(file, {
1113
+ realpath: false,
1114
+ retries: {
1115
+ retries: 60,
1116
+ minTimeout: 100,
1117
+ maxTimeout: 250
1118
+ }
1119
+ });
1120
+ const temporary = `${file}.${randomUUID()}.tmp`;
1121
+ try {
1122
+ const state = await readReview(path);
1123
+ await change(state);
1124
+ state.version++;
1125
+ await writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`);
1126
+ await rename(temporary, file);
1127
+ return state;
1128
+ } finally {
1129
+ await rm(temporary, { force: true });
1130
+ await release();
1131
+ }
1132
+ }
1133
+ function validateTarget(document, target) {
1134
+ const slide = document.slides.find((s) => s.id === target.slideId);
1135
+ if (!slide) throw invalid("The note's page no longer exists. Remove the note and select a new target.");
1136
+ const component = slide.components.find((c) => c.id === target.componentId);
1137
+ if (target.componentId !== void 0 && !component) throw invalid("The note's component no longer exists. Remove the note and select a new target.");
1138
+ if (target.elementId !== void 0 && !(component?.customVisual?.format === "vector" && component.customVisual.elements.some((e) => e.id === target.elementId))) throw invalid("The note's vector element no longer exists. Remove the note and select a new target.");
1139
+ }
1140
+ async function addRevisionNote(path, target, text) {
1141
+ if (typeof text !== "string" || !text.trim() || text.length > 4e3) throw invalid("Write a revision note of 1–4000 characters.");
1142
+ return mutate(path, async (state) => {
1143
+ if (activeRequest(state.request)) throw conflict("Wait for the current request before adding notes.");
1144
+ validateTarget((await readCompositionFile(path)).document, target);
1145
+ state.notes.push({
1146
+ id: randomUUID(),
1147
+ slideId: target.slideId,
1148
+ ...target.componentId ? { componentId: target.componentId } : {},
1149
+ ...target.elementId ? { elementId: target.elementId } : {},
1150
+ text: text.trim(),
1151
+ resolved: false
1152
+ });
1153
+ });
1154
+ }
1155
+ async function removeRevisionNote(path, id) {
1156
+ return mutate(path, (state) => {
1157
+ if (activeRequest(state.request)) throw conflict("Wait for the current request before removing notes.");
1158
+ state.notes = state.notes.filter((note) => note.id !== id);
1159
+ });
1160
+ }
1161
+ async function writeBuildRequest(path, request) {
1162
+ return mutate(path, async (state) => {
1163
+ if (activeRequest(state.request)) throw conflict("A build request is already waiting or working.");
1164
+ const current = await readCompositionFile(path);
1165
+ if (current.revision !== request.revision) throw conflict("Save the latest composition before requesting a build.");
1166
+ validateTarget(current.document, request);
1167
+ const notes = state.notes.filter((note) => !note.resolved);
1168
+ for (const note of notes) validateTarget(current.document, note);
1169
+ if (typeof request.instruction !== "string" || !request.instruction.trim()) throw invalid("Describe what the agent should build.");
1170
+ state.request = {
1171
+ schema: "konpeki-build-request/v2",
1172
+ id: randomUUID(),
1173
+ compositionPath: resolve(path),
1174
+ revision: current.revision,
1175
+ instruction: request.instruction.trim(),
1176
+ slideId: request.slideId,
1177
+ ...request.componentId ? { componentId: request.componentId } : {},
1178
+ ...request.elementId ? { elementId: request.elementId } : {},
1179
+ notes,
1180
+ status: "submitted"
1181
+ };
1182
+ });
1183
+ }
1184
+ async function claimBuildRequest(path) {
1185
+ if ((await readReview(path)).request?.status !== "submitted") return;
1186
+ let claimed;
1187
+ await mutate(path, (state) => {
1188
+ if (state.request?.status !== "submitted") return;
1189
+ state.request.status = "working";
1190
+ claimed = state.request;
1191
+ });
1192
+ return claimed;
1193
+ }
1194
+ async function finishBuildRequest(path, id, status, message) {
1195
+ if (![
1196
+ "done",
1197
+ "needs-clarification",
1198
+ "failed"
1199
+ ].includes(status)) throw invalid("Status must be done, needs-clarification or failed.");
1200
+ if (status !== "done" && !message?.trim()) throw invalid("Explain what failed or needs clarification.");
1201
+ return mutate(path, async (state) => {
1202
+ if (state.request?.id !== id || !activeRequest(state.request)) throw conflict("This request is no longer active.");
1203
+ if (status === "done" && state.request.status !== "working") throw conflict("Claim the request with konpeki wait before completing it.");
1204
+ if (status === "done") {
1205
+ state.request.resultRevision = (await readCompositionFile(path)).revision;
1206
+ const completed = new Set(state.request.notes.map((note) => note.id));
1207
+ state.notes = state.notes.map((note) => completed.has(note.id) ? {
1208
+ ...note,
1209
+ resolved: true
1210
+ } : note);
1211
+ }
1212
+ state.request.status = status;
1213
+ state.request.message = message?.trim();
1508
1214
  });
1509
- const output = requestFileFor(compositionPath);
1510
- await mkdir(dirname(output), { recursive: true });
1511
- const value = {
1512
- schema: "konpeki-build-request/v1",
1513
- compositionPath: resolve(compositionPath),
1514
- revision: request.revision,
1515
- instruction: request.instruction.trim(),
1516
- slideId: request.slideId,
1517
- ...request.componentId ? { componentId: request.componentId } : {},
1518
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
1519
- };
1520
- await writeFile(output, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1521
- return {
1522
- output,
1523
- request: value
1524
- };
1525
1215
  }
1526
1216
  //#endregion
1527
1217
  //#region bin/session-plugin.ts
@@ -1547,7 +1237,7 @@ async function readJSON(request) {
1547
1237
  function errorResponse(error) {
1548
1238
  const code = error instanceof Error && "code" in error ? error.code : void 0;
1549
1239
  return {
1550
- status: code === "REVISION_CONFLICT" ? 409 : code === "INVALID_COMPOSITION" ? 422 : code === "BODY_TOO_LARGE" ? 413 : error instanceof SyntaxError ? 400 : 500,
1240
+ status: code === "REVISION_CONFLICT" ? 409 : code === "INVALID_COMPOSITION" || code === "INVALID_REVIEW" ? 422 : code === "BODY_TOO_LARGE" ? 413 : error instanceof SyntaxError ? 400 : 500,
1551
1241
  body: {
1552
1242
  error: error instanceof Error ? error.message : "File-session request failed.",
1553
1243
  ...error && typeof error === "object" && "revision" in error ? { revision: error.revision } : {}
@@ -1558,6 +1248,19 @@ function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
1558
1248
  return {
1559
1249
  name: "konpeki-file-session",
1560
1250
  enforce: "pre",
1251
+ config(config) {
1252
+ return { server: { fs: { deny: [
1253
+ ".env",
1254
+ ".env.*",
1255
+ "*.{crt,pem,key,p12,pfx,cer,der}",
1256
+ ".npmrc",
1257
+ ".yarnrc.yml",
1258
+ "**/.git/**",
1259
+ ...config.server?.fs?.deny ?? [],
1260
+ "**/*.review.json",
1261
+ "**/*.review.json.*"
1262
+ ] } } };
1263
+ },
1561
1264
  configureServer(server) {
1562
1265
  server.middlewares.use(async (request, response, next) => {
1563
1266
  const url = new URL(request.url || "/", "http://localhost");
@@ -1568,7 +1271,11 @@ function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
1568
1271
  }
1569
1272
  try {
1570
1273
  if (url.pathname === route && request.method === "GET") {
1571
- sendJSON(response, 200, await readCompositionFile(compositionPath));
1274
+ const review = await readReview(compositionPath);
1275
+ sendJSON(response, 200, {
1276
+ ...await readCompositionFile(compositionPath),
1277
+ review
1278
+ });
1572
1279
  return;
1573
1280
  }
1574
1281
  if (url.pathname === route && request.method === "PUT") {
@@ -1584,7 +1291,20 @@ function fileSessionPlugin({ compositionPath, token, onBuildRequest }) {
1584
1291
  }
1585
1292
  const result = await writeBuildRequest(compositionPath, body);
1586
1293
  onBuildRequest?.(result);
1587
- sendJSON(response, 201, { ok: true });
1294
+ sendJSON(response, 201, result);
1295
+ return;
1296
+ }
1297
+ if (url.pathname === `${route}/notes` && request.method === "POST") {
1298
+ const body = await readJSON(request);
1299
+ sendJSON(response, 201, await addRevisionNote(compositionPath, body, body.text));
1300
+ return;
1301
+ }
1302
+ if (url.pathname === `${route}/notes` && request.method === "DELETE") {
1303
+ sendJSON(response, 200, await removeRevisionNote(compositionPath, (await readJSON(request)).id));
1304
+ return;
1305
+ }
1306
+ if (url.pathname === `${route}/cancel` && request.method === "POST") {
1307
+ sendJSON(response, 200, await finishBuildRequest(compositionPath, (await readJSON(request)).id, "failed", "Cancelled by user. Stop the agent before starting another request."));
1588
1308
  return;
1589
1309
  }
1590
1310
  sendJSON(response, 404, { error: "Unknown file-session endpoint." });
@@ -1603,7 +1323,9 @@ function usage() {
1603
1323
  console.error(`Usage:
1604
1324
  konpeki preview <composition.json> [--host <host>] [--port <port>]
1605
1325
  konpeki validate <composition.json>
1606
- konpeki wait <composition.json>`);
1326
+ konpeki wait <composition.json>
1327
+ konpeki request <composition.json>
1328
+ konpeki finish <composition.json> <request-id> [--status done|needs-clarification|failed] [--message <text>]`);
1607
1329
  }
1608
1330
  function option(name, fallback) {
1609
1331
  const index = process.argv.indexOf(name);
@@ -1616,7 +1338,6 @@ async function preview(input) {
1616
1338
  const host = option("--host", "127.0.0.1");
1617
1339
  const port = Number(option("--port", "4318"));
1618
1340
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Port must be an integer from 1 to 65535.");
1619
- await rm(requestFileFor(compositionPath), { force: true });
1620
1341
  const server = await createServer({
1621
1342
  root,
1622
1343
  configFile: resolve(root, "vite.config.ts"),
@@ -1629,6 +1350,7 @@ async function preview(input) {
1629
1350
  compositionPath,
1630
1351
  token,
1631
1352
  onBuildRequest: ({ request }) => {
1353
+ if (!request) return;
1632
1354
  console.log(`\nBuild requested for ${request.compositionPath}`);
1633
1355
  console.log(`Revision: ${request.revision}`);
1634
1356
  console.log(`Instruction: ${request.instruction}`);
@@ -1649,17 +1371,12 @@ async function validate(input) {
1649
1371
  console.log(`${result.name}: valid (${result.revision})`);
1650
1372
  }
1651
1373
  async function waitForRequest(input) {
1652
- const requestPath = requestFileFor(resolve(input));
1653
- console.log(`Waiting for a Build it request for ${resolve(input)}…`);
1374
+ console.error(`Waiting for a Build it request for ${resolve(input)}…`);
1654
1375
  for (;;) {
1655
- try {
1656
- await access(requestPath);
1657
- const value = await readFile(requestPath, "utf8");
1658
- await rm(requestPath, { force: true });
1659
- process.stdout.write(value);
1376
+ const request = await claimBuildRequest(resolve(input));
1377
+ if (request) {
1378
+ console.log(JSON.stringify(request, null, 2));
1660
1379
  return;
1661
- } catch (error) {
1662
- if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
1663
1380
  }
1664
1381
  await new Promise((resolveWait) => setTimeout(resolveWait, 250));
1665
1382
  }
@@ -1673,7 +1390,12 @@ if (!command || !input) {
1673
1390
  if (command === "preview") await preview(input);
1674
1391
  else if (command === "validate") await validate(input);
1675
1392
  else if (command === "wait") await waitForRequest(input);
1676
- else {
1393
+ else if (command === "request") console.log(JSON.stringify((await readReview(input)).request ?? null, null, 2));
1394
+ else if (command === "finish") {
1395
+ if (!process.argv[4]) throw new Error("Provide the request ID returned by konpeki wait.");
1396
+ const result = await finishBuildRequest(input, process.argv[4], option("--status", "done"), option("--message"));
1397
+ console.log(JSON.stringify(result.request, null, 2));
1398
+ } else {
1677
1399
  usage();
1678
1400
  process.exitCode = 1;
1679
1401
  }