@krapht/ork 0.3.0 → 1.0.0-alpha.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.
Files changed (39) hide show
  1. package/README.md +16 -12
  2. package/dist/__build_info__-DdTVd2p5.mjs +615 -0
  3. package/dist/bin.mjs +1572 -909
  4. package/dist/clean-NnboOuz6.mjs +88 -0
  5. package/dist/completion-snnYGjgK.mjs +405 -0
  6. package/dist/{context-BFGC_C02.mjs → context-DO3sBy9v.mjs} +30 -191
  7. package/dist/decide-B81wCZQg.mjs +113 -0
  8. package/dist/{find-BcvhT6UN.mjs → find-BKW-uGey.mjs} +1 -1
  9. package/dist/{format-DqpObyMl.mjs → format-CmTlTOjT.mjs} +72 -198
  10. package/dist/help-groups-BwYoAWsh.mjs +354 -0
  11. package/dist/{in-a-run-BbLhZOFW.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
  12. package/dist/{limits-BKYAmqCR.mjs → limits-JCve2QpI.mjs} +1 -1
  13. package/dist/list-C7UEgmtd.mjs +110 -0
  14. package/dist/{mcp-BWsdQyri.mjs → mcp-CKPyNaKD.mjs} +7 -7
  15. package/dist/{output-Q2EdeVUt.mjs → output-B1V6ZJSx.mjs} +130 -36
  16. package/dist/{pause-DKACDfV1.mjs → pause-B_SL_yDz.mjs} +4 -4
  17. package/dist/{relay-CbK-Wme9.mjs → relay-o-Luee1c.mjs} +2 -2
  18. package/dist/remove-DAZ7ZCzt.mjs +194 -0
  19. package/dist/{send-C6yr04Fe.mjs → send-3B64fBNB.mjs} +3 -3
  20. package/dist/{src-D5ruZFgD.mjs → src-BJTegEzY.mjs} +1212 -57
  21. package/dist/{src-DtN1k-B3.mjs → src-C38BPzit.mjs} +251 -18
  22. package/dist/{src-Cz1yOUTg.mjs → src-DHvWU4tX.mjs} +2742 -1659
  23. package/dist/{src-CZOVF2yU.mjs → src-Dyz_Gy1q.mjs} +35 -8
  24. package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
  25. package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
  26. package/dist/ui/assets/app-CH7YC3in.css +1 -0
  27. package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
  28. package/dist/ui/assets/{index-BoG6O-Lp.js → index-D83F2OKZ.js} +2 -2
  29. package/dist/ui/index.html +2 -3
  30. package/dist/ui-CY4xxJUu.mjs +472 -0
  31. package/dist/{usage-BIjf9Xdt.mjs → usage-CJnytoe4.mjs} +6 -5
  32. package/dist/{watch-B8o4Szd_.mjs → watch-BJB82ZJ7.mjs} +7 -7
  33. package/dist/workflows-CiM7S3l0.mjs +88 -0
  34. package/dist/worktrees-86ySXxR6.mjs +212 -0
  35. package/package.json +6 -6
  36. package/dist/__build_info__-CXXMLvjt.mjs +0 -104
  37. package/dist/ui/assets/app-BlDdP396.js +0 -61
  38. package/dist/ui/assets/app-C722xNZ3.css +0 -1
  39. package/dist/ui-CZS1XKla.mjs +0 -160
@@ -1,12 +1,15 @@
1
- import { B as requestContinue, F as linkNodes, H as requestStop, Kt as loadAgents, Nt as readCondition, Qt as placeFor, R as unlinkNodes, Sn as NodeSchema, U as tellLiveRun, V as requestPause, X as detachResume, Z as resumable, Zt as placeFiles, at as processAlive, bn as expandEdge, cn as skillsIn, ct as checkWorkflowText, d as triggersReport, dn as newWorkflowText, en as runDirOf, fn as readDocument, it as pausedOf, ln as WorkflowEditSchema, lt as walkChecks, mn as parseWorkflow, nn as workflowDirOf, nt as followEvents, o as limitsReport, ot as runStatus, pn as renderJsonSchema, r as capsHere, rn as inside, rt as liveStatus, st as runTree, tt as detachRun, un as applyEdits, ut as walkSummary, vn as EdgeInputSchema, yt as nodeSays } from "./src-Cz1yOUTg.mjs";
2
- import { a as pendingDecisions, i as answerDecision, r as nodeSession } from "./__build_info__-CXXMLvjt.mjs";
3
- import { n as newRunId, t as findRun } from "./find-BcvhT6UN.mjs";
4
- import { c as openStore, l as storePath, s as nodeOf } from "./output-Q2EdeVUt.mjs";
1
+ import { B as linkNodes, Cn as runDirOf, Ct as walkChecks, En as inside, Fn as applyEdits, G as requestContinue, Gn as expandEdge, Hn as EdgeInputSchema, Ht as planPost, In as newWorkflowText, J as tellLiveRun, K as requestPause, Ln as readDocument, Nn as skillsIn, Nt as nodeSays, On as LESSON_CAP_TOKENS, Pn as WorkflowEditSchema, Rn as renderJsonSchema, St as checkWorkflowText, Tn as workflowDirOf, U as unlinkNodes, Ut as runGh, Vt as DISCUSSIONS, Wt as sendPost, _t as runStatus, bn as placeFiles, bt as processAlive, ct as explainWorkflowText, d as runningRunIds, f as runningScript, gt as pausedOf, hn as loadAgents, ht as liveStatus, l as planUpgrade, nt as resumable, o as limitsReport, ot as detachRun, p as detectInstaller, q as requestStop, qn as NodeSchema, r as capsHere, st as followEvents, tn as readCondition, tt as detachResume, u as releaseNotes, v as triggersReport, vt as runTree, wt as walkSummary, xn as placeFor, zn as parseWorkflow } from "./src-DHvWU4tX.mjs";
2
+ import { C as feedbackActions, S as placeOf$1, _ as previewPost, a as registryShown, b as acceptItem, c as checkDue, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, o as updateSwitchOff, p as refreshUpdateCache, s as runInstall, u as newerVersion, w as openFixRun, x as lessonPlaceOf, y as recordNote } from "./__build_info__-DdTVd2p5.mjs";
3
+ import { n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-B81wCZQg.mjs";
4
+ import { n as newRunId, t as findRun } from "./find-BKW-uGey.mjs";
5
+ import { c as openStore, l as storePath, s as nodeOf } from "./output-B1V6ZJSx.mjs";
5
6
  import { n as usageReport } from "./report-DK4LGqwF.mjs";
7
+ import { a as planClean, i as keyOf, o as stepActs, r as cleanStep, s as stepPrunes, t as applyClean } from "./clean-NnboOuz6.mjs";
8
+ import { a as readWorktrees, c as planLine, i as projectHead, l as removeRefusal, n as lostWork, o as labelOf, r as planRemove, s as mergeStateOf, t as applyRemove, u as stateParts } from "./remove-DAZ7ZCzt.mjs";
6
9
  import path from "node:path";
7
10
  import { z } from "zod";
8
11
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
9
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
12
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
10
13
  import { homedir } from "node:os";
11
14
  import { createServer } from "node:http";
12
15
  // ../core/src/store/activity.ts
@@ -133,6 +136,194 @@ const BUILDER_API = {
133
136
  body: SaveWorkflowSchema
134
137
  }
135
138
  };
139
+ // ../server/src/contract-context.ts
140
+ const CONTEXT_API = {
141
+ runContext: {
142
+ method: "GET",
143
+ path: "/api/runs/:id/nodes/:node/context"
144
+ },
145
+ builderContext: {
146
+ method: "GET",
147
+ path: "/api/projects/:id/builder/:name/nodes/:node/context"
148
+ }
149
+ };
150
+ // ../server/src/contract-feedback.ts
151
+ const NoteSchema = z.strictObject({
152
+ /** Its first line is the title. */
153
+ text: z.string().min(1),
154
+ /** A node that ran in the run; the run itself without one. */
155
+ node: z.string().min(1).optional(),
156
+ kind: z.string().min(1).optional(),
157
+ severity: z.string().min(1).optional(),
158
+ rating: z.string().min(1).optional()
159
+ });
160
+ /** Dismissing an item, or restoring a dismissed one as `new`. */
161
+ const FeedbackStatusSchema = z.strictObject({ status: z.enum(["dismissed", "new"]) });
162
+ const AcceptSchema = z.strictObject({
163
+ /** Also write it into its workflow's lessons file. */
164
+ lesson: z.boolean(),
165
+ /** The lesson's words; the item's title by default. */
166
+ text: z.string().min(1).optional(),
167
+ /** Which workflow's lessons, when it was seen in more than one. */
168
+ workflow: z.string().min(1).optional()
169
+ });
170
+ const ApplySchema = z.strictObject({ driver: z.enum(["sdk", "cli"]).optional() });
171
+ const PublishPreviewSchema = z.strictObject({
172
+ /** Also hide the repositories' and workflows' names. */
173
+ hideNames: z.boolean() });
174
+ const PublishSchema = z.strictObject({
175
+ hideNames: z.boolean(),
176
+ /** The preview's digest: what the person saw, so nothing else is sent. */
177
+ digest: z.string().min(1)
178
+ });
179
+ const FEEDBACK_API = {
180
+ note: {
181
+ method: "POST",
182
+ path: "/api/runs/:id/notes",
183
+ body: NoteSchema
184
+ },
185
+ feedbackItems: {
186
+ method: "GET",
187
+ path: "/api/feedback"
188
+ },
189
+ feedbackItem: {
190
+ method: "GET",
191
+ path: "/api/feedback/:id"
192
+ },
193
+ feedbackStatus: {
194
+ method: "POST",
195
+ path: "/api/feedback/:id/status",
196
+ body: FeedbackStatusSchema
197
+ },
198
+ accept: {
199
+ method: "POST",
200
+ path: "/api/feedback/:id/accept",
201
+ body: AcceptSchema
202
+ },
203
+ apply: {
204
+ method: "POST",
205
+ path: "/api/feedback/:id/apply",
206
+ body: ApplySchema
207
+ },
208
+ publishPreview: {
209
+ method: "POST",
210
+ path: "/api/feedback/:id/publish/preview",
211
+ body: PublishPreviewSchema
212
+ },
213
+ publish: {
214
+ method: "POST",
215
+ path: "/api/feedback/:id/publish",
216
+ body: PublishSchema
217
+ },
218
+ split: {
219
+ method: "POST",
220
+ path: "/api/feedback/:id/split"
221
+ }
222
+ };
223
+ // ../server/src/contract-links.ts
224
+ /**
225
+ * @file The routes that link two nodes of live runs (RFC-003): list, link and
226
+ * end a link. It imports only `zod`, so a browser bundle can use it.
227
+ */
228
+ /** Two nodes of live runs, each `<run>/<node>` (RFC-003). */
229
+ const LinkSchema = z.strictObject({
230
+ a: z.string().min(3),
231
+ b: z.string().min(3),
232
+ maxMessages: z.number().int().positive().optional()
233
+ });
234
+ const UnlinkSchema = z.strictObject({
235
+ a: z.string().min(3),
236
+ b: z.string().min(3)
237
+ });
238
+ const LINKS_API = {
239
+ links: {
240
+ method: "GET",
241
+ path: "/api/links",
242
+ query: z.strictObject({ run: z.string().min(1).optional() })
243
+ },
244
+ link: {
245
+ method: "POST",
246
+ path: "/api/links",
247
+ body: LinkSchema
248
+ },
249
+ unlink: {
250
+ method: "POST",
251
+ path: "/api/links/end",
252
+ body: UnlinkSchema
253
+ }
254
+ };
255
+ const UPDATE_API = {
256
+ version: {
257
+ method: "GET",
258
+ path: "/api/version"
259
+ },
260
+ upgradePlan: {
261
+ method: "POST",
262
+ path: "/api/upgrade/plan"
263
+ },
264
+ upgrade: {
265
+ method: "POST",
266
+ path: "/api/upgrade",
267
+ body: z.strictObject({
268
+ /** The plan the person read; the server runs its own argv, never a sent one. */
269
+ planId: z.string().min(1) })
270
+ },
271
+ restart: {
272
+ method: "POST",
273
+ path: "/api/restart"
274
+ }
275
+ };
276
+ // ../server/src/contract-worktrees.ts
277
+ const Root = z.string().min(1);
278
+ const WorktreesQuerySchema = z.strictObject({
279
+ /** A project's root; every project Ork knows without one. */
280
+ project: Root.optional() });
281
+ const RemovePlanSchema = z.strictObject({
282
+ project: Root,
283
+ /** The item's branch, or a detached worktree's path. */
284
+ key: z.string().min(1),
285
+ /** Also delete the branch (implied for a branch with no worktree). */
286
+ deleteBranch: z.boolean()
287
+ });
288
+ const RemoveSchema = RemovePlanSchema.extend({
289
+ /** The plan needs it: removing loses work the person was shown. */
290
+ force: z.boolean(),
291
+ /** The plan's digest: what the person saw, so nothing else goes. */
292
+ digest: z.string().min(1)
293
+ });
294
+ const CleanPlanSchema = z.strictObject({
295
+ project: Root.optional(),
296
+ /** Also delete the merged branches (`--branches`). */
297
+ branches: z.boolean()
298
+ });
299
+ const CleanSchema = CleanPlanSchema.extend({ digest: z.string().min(1) });
300
+ const WORKTREES_API = {
301
+ worktrees: {
302
+ method: "GET",
303
+ path: "/api/worktrees",
304
+ query: WorktreesQuerySchema
305
+ },
306
+ worktreeRemovePlan: {
307
+ method: "POST",
308
+ path: "/api/worktrees/remove/plan",
309
+ body: RemovePlanSchema
310
+ },
311
+ worktreeRemove: {
312
+ method: "POST",
313
+ path: "/api/worktrees/remove",
314
+ body: RemoveSchema
315
+ },
316
+ worktreesCleanPlan: {
317
+ method: "POST",
318
+ path: "/api/worktrees/clean/plan",
319
+ body: CleanPlanSchema
320
+ },
321
+ worktreesClean: {
322
+ method: "POST",
323
+ path: "/api/worktrees/clean",
324
+ body: CleanSchema
325
+ }
326
+ };
136
327
  // ../server/src/contract.ts
137
328
  const Count = z.coerce.number().int().positive();
138
329
  const Seq = z.coerce.number().int().nonnegative();
@@ -160,7 +351,11 @@ const DecisionsQuerySchema = z.strictObject({
160
351
  run: z.string().min(1).optional(),
161
352
  project: z.string().min(1).optional()
162
353
  });
163
- const AnswerSchema = z.strictObject({ choose: z.string().min(1) });
354
+ /** An answer: the option, and what the person said with it, if anything. */
355
+ const AnswerSchema = z.strictObject({
356
+ choose: z.string().min(1),
357
+ note: z.string().optional()
358
+ });
164
359
  const RunFlags = {
165
360
  inputs: z.record(z.string(), z.string()).optional(),
166
361
  driver: Driver.optional(),
@@ -179,17 +374,6 @@ const MessageSchema = z.strictObject({
179
374
  node: z.string().min(1),
180
375
  body: z.string().min(1)
181
376
  });
182
- /** Two nodes of live runs, each `<run>/<node>` (RFC-003). */
183
- const LinkSchema = z.strictObject({
184
- a: z.string().min(3),
185
- b: z.string().min(3),
186
- maxMessages: z.number().int().positive().optional()
187
- });
188
- const UnlinkSchema = z.strictObject({
189
- a: z.string().min(3),
190
- b: z.string().min(3)
191
- });
192
- const LinksQuerySchema = z.strictObject({ run: z.string().min(1).optional() });
193
377
  /** Pause, continue or stop: the whole run, or `node` alone (ADR-041). */
194
378
  const ControlSchema = z.strictObject({ node: z.string().min(1).optional() });
195
379
  /** Every JSON route; the two event streams are in {@link STREAMS}. */
@@ -199,6 +383,10 @@ const API = {
199
383
  path: "/api/health"
200
384
  },
201
385
  ...BUILDER_API,
386
+ ...CONTEXT_API,
387
+ ...FEEDBACK_API,
388
+ ...WORKTREES_API,
389
+ ...UPDATE_API,
202
390
  projects: {
203
391
  method: "GET",
204
392
  path: "/api/projects"
@@ -282,21 +470,7 @@ const API = {
282
470
  method: "GET",
283
471
  path: "/api/triggers"
284
472
  },
285
- links: {
286
- method: "GET",
287
- path: "/api/links",
288
- query: LinksQuerySchema
289
- },
290
- link: {
291
- method: "POST",
292
- path: "/api/links",
293
- body: LinkSchema
294
- },
295
- unlink: {
296
- method: "POST",
297
- path: "/api/links/end",
298
- body: UnlinkSchema
299
- }
473
+ ...LINKS_API
300
474
  };
301
475
  /** The Server-Sent Event streams, and the events each sends. */
302
476
  const STREAMS = {
@@ -305,8 +479,12 @@ const STREAMS = {
305
479
  * `Last-Event-ID`, or `?after=`.
306
480
  */
307
481
  run: "/api/runs/:id/stream",
308
- /** `runs` and `decisions`, each when it changes; `?project=` keeps one. */
309
- overview: "/api/stream"
482
+ /**
483
+ * `runs`, `decisions`, `version`, each when it changes; `?project=` keeps
484
+ * one. `upgrade`: the upgrade's output, then `end` (contract-update.ts).
485
+ */
486
+ overview: "/api/stream",
487
+ upgrade: "/api/upgrade/stream"
310
488
  };
311
489
  // ../server/src/graph.ts
312
490
  /**
@@ -361,8 +539,9 @@ const runGraph = (events, file) => {
361
539
  };
362
540
  // ../server/src/guards.ts
363
541
  /**
364
- * @file What the server lets in (ADR-039, specs/server.md#security): only its
365
- * own loopback `Host` (against DNS rebinding); only with its token, from the
542
+ * @file What the server lets in (ADR-039,
543
+ * specs/server/streams-contract-and-security.md#security): only its own
544
+ * loopback `Host` (against DNS rebinding); only with its token, from the
366
545
  * session cookie or an `Authorization: Bearer` header; and, for a write sent
367
546
  * with the cookie, only from its own `Origin` (against CSRF). The token in
368
547
  * `?token=` is swapped once for the cookie, with a redirect that takes it out
@@ -683,14 +862,14 @@ const shrink = (value, longest = LONGEST) => {
683
862
  return value;
684
863
  };
685
864
  const answer = (value) => ({ text: JSON.stringify(shrink(value)) });
686
- const refused = (error) => ({
865
+ const refused$1 = (error) => ({
687
866
  text: error,
688
867
  isError: true
689
868
  });
690
869
  /** A route's reply as an answer, `then` shaping what it gave. */
691
870
  const fromReply = async (reply, then = (body) => body) => {
692
871
  const settled = await reply;
693
- return "error" in settled ? refused(settled.error) : answer(then(settled.body));
872
+ return "error" in settled ? refused$1(settled.error) : answer(then(settled.body));
694
873
  };
695
874
  const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
696
875
  /** A field of an object, or `undefined`. */
@@ -721,7 +900,7 @@ const formatPart = (nodeType) => {
721
900
  }
722
901
  });
723
902
  const branch = branches.find((item) => typeOf(item) === nodeType);
724
- return branch === void 0 ? refused(`No node type \`${nodeType}\`. The types are: ${types.map((type) => `\`${type}\``).join(", ")}.`) : { text: JSON.stringify(branch) };
903
+ return branch === void 0 ? refused$1(`No node type \`${nodeType}\`. The types are: ${types.map((type) => `\`${type}\``).join(", ")}.`) : { text: JSON.stringify(branch) };
725
904
  };
726
905
  // ../server/src/mcp-tool.ts
727
906
  /**
@@ -738,13 +917,13 @@ const tool = (name, description, shape, run, options = {}) => {
738
917
  readOnly: options.readOnly ?? true,
739
918
  call: (args) => {
740
919
  const parsed = schema.safeParse(args ?? {});
741
- return Promise.resolve(parsed.success ? run(parsed.data) : refused(z.prettifyError(parsed.error)));
920
+ return Promise.resolve(parsed.success ? run(parsed.data) : refused$1(z.prettifyError(parsed.error)));
742
921
  }
743
922
  };
744
923
  };
745
924
  // ../server/src/write-routes.ts
746
925
  /**
747
- * @file The API's write routes (specs/server.md#api): answer a decision; stop,
926
+ * @file The API's write routes (specs/server/api.md): answer a decision; stop,
748
927
  * pause or continue a run or one of its nodes (ADR-041); message a live run's
749
928
  * node; and start or resume a run in the background. Each does what its CLI
750
929
  * command does (`ork decide --id`, `stop`, `pause`, `continue`, `chat -m`,
@@ -781,7 +960,7 @@ const writeRoutes = (context) => {
781
960
  const script = context.script === void 0 ? {} : { script: context.script };
782
961
  return {
783
962
  answer: async ({ params, body }) => {
784
- const answered = await answerDecision(context.store, params.id, body.choose, context.home, "ui");
963
+ const answered = await answerDecision(context.store, params.id, body.choose, context.home, "ui", body.note);
785
964
  return answered.ok ? ok({
786
965
  decision: params.id,
787
966
  answer: body.choose
@@ -882,11 +1061,11 @@ const textOf = (context, key, args) => {
882
1061
  const place = projectOf(context, key);
883
1062
  if (place === void 0) return {
884
1063
  ok: false,
885
- answer: refused(`No registered project \`${key}\`.`)
1064
+ answer: refused$1(`No registered project \`${key}\`.`)
886
1065
  };
887
1066
  if (args.workflow === void 0 === (args.text === void 0)) return {
888
1067
  ok: false,
889
- answer: refused("Give either `workflow` (a name or path) or `text`.")
1068
+ answer: refused$1("Give either `workflow` (a name or path) or `text`.")
890
1069
  };
891
1070
  if (args.text !== void 0) {
892
1071
  const file = path.join(placeFiles(place).workflows, "draft.ork.yaml");
@@ -900,7 +1079,7 @@ const textOf = (context, key, args) => {
900
1079
  const file = workflowFile(place, args.workflow ?? "");
901
1080
  return file === null ? {
902
1081
  ok: false,
903
- answer: refused(`No workflow \`${args.workflow ?? ""}\` in \`${place.id}\`'s workflows or inside its root.`)
1082
+ answer: refused$1(`No workflow \`${args.workflow ?? ""}\` in \`${place.id}\`'s workflows or inside its root.`)
904
1083
  } : {
905
1084
  ok: true,
906
1085
  text: readFileSync(file, "utf8"),
@@ -1282,7 +1461,7 @@ const saveDraft = (context, project, name, force) => {
1282
1461
  };
1283
1462
  // ../server/src/builder-routes.ts
1284
1463
  /**
1285
- * @file The workflow builder's routes (RFC-008, specs/server.md#api). Each
1464
+ * @file The workflow builder's routes (RFC-008, specs/server/api.md). Each
1286
1465
  * finds the project, then does its work in `builder.ts`, and a refusal has
1287
1466
  * the words the user will see.
1288
1467
  */
@@ -1383,7 +1562,7 @@ const projectWorkflows = (project) => {
1383
1562
  };
1384
1563
  // ../server/src/read-routes.ts
1385
1564
  /**
1386
- * @file The API's read routes (specs/server.md#api): health, projects and a
1565
+ * @file The API's read routes (specs/server/api.md): health, projects and a
1387
1566
  * project's workflows, runs, a run, its tree and its events, the open
1388
1567
  * decisions, and usage. Each reads the store as the matching CLI command does
1389
1568
  * (`ork projects`, `ps`, `tree`, `logs`, `decide --list`, `usage`).
@@ -1498,7 +1677,7 @@ const mcpTools = (context) => {
1498
1677
  ...NO_QUERY,
1499
1678
  params: { id: args.run }
1500
1679
  });
1501
- if ("error" in view) return refused(view.error);
1680
+ if ("error" in view) return refused$1(view.error);
1502
1681
  const tree = read.tree({
1503
1682
  ...NO_QUERY,
1504
1683
  params: { id: view.body.id }
@@ -1535,6 +1714,93 @@ const mcpTools = (context) => {
1535
1714
  })))
1536
1715
  ];
1537
1716
  };
1717
+ const NOT_HERE = "This server was not started by `ork ui`.";
1718
+ /**
1719
+ * Asks the registry when the daily check is due; never throws. `current` is the
1720
+ * running version, which says whether the `canary` tag counts.
1721
+ */
1722
+ const checkIfDue = async (file, options, current) => {
1723
+ try {
1724
+ const now = /* @__PURE__ */ new Date();
1725
+ const cache = readUpdateCache(file);
1726
+ if (!checkDue(cache, now)) return false;
1727
+ writeUpdateCache(file, {
1728
+ ...cache,
1729
+ checkedAt: now.toISOString()
1730
+ });
1731
+ const registry = registryOf(options.env ?? process.env);
1732
+ await refreshUpdateCache(file, options.name, now, {
1733
+ ...registry === void 0 ? {} : { registry },
1734
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
1735
+ current
1736
+ });
1737
+ return true;
1738
+ } catch {
1739
+ return false;
1740
+ }
1741
+ };
1742
+ const createUpdates = (input) => {
1743
+ const { version, options } = input;
1744
+ const file = updateCachePath(input.home);
1745
+ const env = options?.env ?? process.env;
1746
+ const switched = options === void 0 ? null : updateSwitchOff(env);
1747
+ const reason = options === void 0 ? NOT_HERE : switched === null ? null : `${switched} is set`;
1748
+ let record = null;
1749
+ let job = null;
1750
+ const read = () => {
1751
+ const at = Date.now();
1752
+ const every = options?.readEveryMs ?? 6e4;
1753
+ if (record === null || at - record.at >= every) record = {
1754
+ cache: readUpdateCache(file),
1755
+ at
1756
+ };
1757
+ return record.cache;
1758
+ };
1759
+ let timer;
1760
+ if (options !== void 0 && reason === null) {
1761
+ const due = () => {
1762
+ checkIfDue(file, options, version).then((asked) => {
1763
+ if (asked) record = null;
1764
+ return asked;
1765
+ });
1766
+ };
1767
+ due();
1768
+ timer = setInterval(due, options.checkEveryMs ?? 36e5);
1769
+ timer.unref();
1770
+ }
1771
+ return {
1772
+ info: () => {
1773
+ const cache = read();
1774
+ const newer = options === void 0 || reason !== null ? null : newerVersion(cache, version);
1775
+ const registry = registryOf(env);
1776
+ return {
1777
+ current: version,
1778
+ latest: cache?.latest ?? null,
1779
+ newer,
1780
+ checkedAt: cache?.checkedAt ?? null,
1781
+ releaseNotes: newer === null || options === void 0 ? null : releaseNotes(options.name, newer),
1782
+ check: reason === null ? { on: true } : {
1783
+ on: false,
1784
+ reason
1785
+ },
1786
+ registry: registry === void 0 ? null : registryShown(registry),
1787
+ job: job?.view() ?? null
1788
+ };
1789
+ },
1790
+ options,
1791
+ file,
1792
+ forget: () => {
1793
+ record = null;
1794
+ },
1795
+ job: () => job,
1796
+ setJob: (next) => {
1797
+ job = next;
1798
+ },
1799
+ close: () => {
1800
+ clearInterval(timer);
1801
+ }
1802
+ };
1803
+ };
1538
1804
  // ../server/src/mcp.ts
1539
1805
  /**
1540
1806
  * @file What `ork mcp` opens (RFC-012): the store under a home, and the tools
@@ -1550,7 +1816,8 @@ const openMcp = (options) => {
1550
1816
  pid: process.pid,
1551
1817
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1552
1818
  ...options.home === void 0 ? {} : { home: options.home },
1553
- ...options.script === void 0 ? {} : { script: options.script }
1819
+ ...options.script === void 0 ? {} : { script: options.script },
1820
+ updates: createUpdates({ version: options.version })
1554
1821
  };
1555
1822
  return {
1556
1823
  tools: [...mcpTools(context), ...actingTools(context, { start: options.start })],
@@ -1620,12 +1887,834 @@ const readAsset = (dir, pathname) => {
1620
1887
  return null;
1621
1888
  }
1622
1889
  };
1890
+ // ../server/src/context-routes.ts
1891
+ /**
1892
+ * @file The context inspector's routes: what one node's session would load,
1893
+ * planned without writing anything. A run's node is planned from the run's
1894
+ * workflow file as it is now, naming each file behind it that changed after
1895
+ * the run started; a builder's node from the draft, else the file, so an edit
1896
+ * shows at once.
1897
+ */
1898
+ /** Whether `file` was written after `since`, an ISO time. */
1899
+ const changedAfter = (file, since) => {
1900
+ try {
1901
+ return statSync(file).mtimeMs > Date.parse(since);
1902
+ } catch {
1903
+ return false;
1904
+ }
1905
+ };
1906
+ /** A file as the user knows it: in the project, under `~`, or whole. */
1907
+ const shownAs = (file, root, home) => {
1908
+ const inside = (base) => {
1909
+ const relative = path.relative(base, file);
1910
+ return relative.startsWith("..") || path.isAbsolute(relative) ? void 0 : relative;
1911
+ };
1912
+ const inProject = inside(root);
1913
+ if (inProject !== void 0) return inProject;
1914
+ const inHome = inside(home);
1915
+ return inHome === void 0 ? file : `~/${inHome}`;
1916
+ };
1917
+ const answerOf = (result, changedSinceStart) => result.ok ? {
1918
+ planned: true,
1919
+ explanation: result.explanation,
1920
+ changedSinceStart
1921
+ } : {
1922
+ planned: false,
1923
+ why: result.error,
1924
+ changedSinceStart
1925
+ };
1926
+ const contextRoutes = (context) => {
1927
+ /** Where to plan: in `place`, when the project is registered. */
1928
+ const where = (place) => ({
1929
+ registered: context.store.projects(),
1930
+ ...context.home === void 0 ? {} : { home: context.home },
1931
+ ...place === void 0 ? {} : { place }
1932
+ });
1933
+ return {
1934
+ runContext: ({ params }) => {
1935
+ const found = runOf(context, params.id);
1936
+ if (!found.ok) return found.refusal;
1937
+ const { file, startedAt, project } = found.run;
1938
+ const home = context.home ?? homedir();
1939
+ const text = readText(file);
1940
+ if (text === void 0) return ok({
1941
+ planned: false,
1942
+ why: `The run's workflow file \`${file}\` is gone.`,
1943
+ changedSinceStart: [shownAs(file, project, home)]
1944
+ });
1945
+ const result = explainWorkflowText(text, file, params.node, where(projectOf(context, project)));
1946
+ const read = [file, ...result.ok ? result.sources : []];
1947
+ return ok(answerOf(result, [...new Set(read)].filter((each) => changedAfter(each, startedAt)).map((each) => shownAs(each, project, home))));
1948
+ },
1949
+ builderContext: ({ params }) => {
1950
+ const project = projectOf(context, params.id);
1951
+ if (project === void 0) return fail(404, `No registered project \`${params.id}\`.`);
1952
+ const problem = problemWith(project, params.name);
1953
+ if (problem !== void 0) return fail(problem.refused, problem.error);
1954
+ const file = fileOf(project, params.name);
1955
+ const text = context.store.draft(project.id, params.name)?.text ?? readText(file);
1956
+ if (text === void 0) {
1957
+ const missing = notFound(project, params.name);
1958
+ return fail(missing.refused, missing.error);
1959
+ }
1960
+ return ok(answerOf(explainWorkflowText(text, file, params.node, where(project)), []));
1961
+ }
1962
+ };
1963
+ };
1964
+ // ../server/src/feedback-apply.ts
1965
+ /**
1966
+ * @file Applying an item from the Feedback panel (ADR-059): the same checks as
1967
+ * `ork feedback apply`, then that command started in the background with a
1968
+ * run id picked here, so the Monitor can follow the run and its gate's keep
1969
+ * or discard settles the item exactly as in a terminal.
1970
+ */
1971
+ const BUILTIN = "ork:feedback-apply";
1972
+ /** Starts a run proposing a fix for `item`, or says why it can't. */
1973
+ const applyRoute = async (context, item, driver) => {
1974
+ const { store } = context;
1975
+ const allowed = feedbackActions(item, openFixRun(store, item)).apply;
1976
+ if (!allowed.ok) return fail(409, allowed.error);
1977
+ const found = placeOf$1(store, item, void 0, store.projects());
1978
+ if (!found.ok) return fail(409, found.error);
1979
+ const { place } = found;
1980
+ const problem = builtinProblem(place, BUILTIN);
1981
+ if (problem !== void 0) return fail(409, problem);
1982
+ const runId = newRunId(store);
1983
+ const detached = await detachRun({
1984
+ store,
1985
+ runId,
1986
+ args: [
1987
+ "feedback",
1988
+ "apply",
1989
+ item.id,
1990
+ "--run-id",
1991
+ runId,
1992
+ ...driver === void 0 ? [] : ["--driver", driver]
1993
+ ],
1994
+ cwd: place.root,
1995
+ runDir: runDirOf(place, runId),
1996
+ ...context.script === void 0 ? {} : { script: context.script }
1997
+ });
1998
+ return detached.ok ? accepted({
1999
+ runId,
2000
+ root: place.root
2001
+ }) : fail(400, detached.error || "The run did not start.");
2002
+ };
2003
+ // ../server/src/feedback-detail.ts
2004
+ /**
2005
+ * @file An item as the Feedback panel shows it: its records, which actions it
2006
+ * allows (the rule `ork feedback` keeps too), where a lesson for each of its
2007
+ * workflows would go and how big that file is, the project a fix would be
2008
+ * proposed in, and the run proposing one now, if any.
2009
+ */
2010
+ const detailOf = (store, item) => {
2011
+ const registered = store.projects();
2012
+ const fixRun = openFixRun(store, item);
2013
+ const actions = feedbackActions(item, fixRun);
2014
+ const found = placeOf$1(store, item, void 0, registered);
2015
+ return {
2016
+ item,
2017
+ records: store.feedbackRecords(item.id),
2018
+ actions,
2019
+ lessons: item.target === "ork" ? [] : item.workflows.map((w) => lessonPlaceOf(store, item, w, registered)),
2020
+ lessonCap: LESSON_CAP_TOKENS,
2021
+ project: found.ok && item.target !== "ork" ? {
2022
+ ok: true,
2023
+ root: found.place.root
2024
+ } : {
2025
+ ok: false,
2026
+ error: found.ok ? "It's about Ork itself." : found.error
2027
+ },
2028
+ ...fixRun === void 0 ? {} : { fixRun }
2029
+ };
2030
+ };
2031
+ // ../server/src/feedback-publish.ts
2032
+ /**
2033
+ * @file Publishing an `ork` item from the Feedback panel (ADR-059): the preview
2034
+ * shows exactly what would be sent and where, with a digest of it; the
2035
+ * publish makes the preview again and sends it only when its digest is the
2036
+ * one the person saw, with their own `gh` login.
2037
+ */
2038
+ const refused = (status, error) => ({
2039
+ ok: false,
2040
+ refusal: fail(status, error)
2041
+ });
2042
+ /** The preview of `item`'s post, and its digest, or why there's none. */
2043
+ const previewOf = async (context, item, hideNames) => {
2044
+ const shown = previewPost(context.store, item, {
2045
+ home: context.home ?? homedir(),
2046
+ registered: context.store.projects(),
2047
+ hideNames
2048
+ });
2049
+ if (!shown.ok) return refused(409, shown.error);
2050
+ const planned = await planPost(shown.post, context.gh ?? runGh);
2051
+ if (!planned.ok) return refused(400, planned.error);
2052
+ const digest = createHash("sha256").update(JSON.stringify([shown.post, planned.plan])).digest("hex");
2053
+ return {
2054
+ ok: true,
2055
+ preview: {
2056
+ post: shown.post,
2057
+ plan: planned.plan,
2058
+ repo: `${DISCUSSIONS.owner}/${DISCUSSIONS.name}`,
2059
+ digest
2060
+ }
2061
+ };
2062
+ };
2063
+ /** Sends `item`'s post when it's still the one previewed as `digest`. */
2064
+ const sendPublish = async (context, item, ask) => {
2065
+ const previewed = await previewOf(context, item, ask.hideNames);
2066
+ if (!previewed.ok) return previewed;
2067
+ const { post, plan, digest } = previewed.preview;
2068
+ if (digest !== ask.digest) return refused(409, "What would be sent changed after the preview, so nothing was sent: look at the preview again.");
2069
+ const sent = await sendPost(post, plan, context.gh ?? runGh);
2070
+ if (!sent.ok) return refused(400, sent.error);
2071
+ context.store.setFeedbackStatus(item.id, "published");
2072
+ return {
2073
+ ok: true,
2074
+ url: sent.url,
2075
+ kind: sent.kind
2076
+ };
2077
+ };
2078
+ // ../server/src/feedback-routes.ts
2079
+ /**
2080
+ * @file The feedback loop's routes (specs/feedback.md), through the same core
2081
+ * functions as `ork feedback`: the items and an item's records; dismiss and
2082
+ * restore; accept, maybe as a lesson; apply, which starts `ork feedback
2083
+ * apply` in the background so its keep-or-discard ends the same way; publish
2084
+ * after a preview, sending only what the preview showed; and a person's note
2085
+ * on a run, or one node of it.
2086
+ */
2087
+ /** The item `id` names (its fingerprint, or a group's member), or a 404. */
2088
+ const itemOf = (store, id) => {
2089
+ const [item, other] = store.findFeedback(id);
2090
+ return item === void 0 || other !== void 0 ? {
2091
+ ok: false,
2092
+ refusal: fail(404, `No feedback item ${id}.`)
2093
+ } : {
2094
+ ok: true,
2095
+ item
2096
+ };
2097
+ };
2098
+ /** The item as it is now, after a write. */
2099
+ const fresh = (store, item) => store.findFeedback(item.id)[0] ?? item;
2100
+ const feedbackRoutes = (context) => {
2101
+ const { store } = context;
2102
+ return {
2103
+ note: ({ params, body }) => {
2104
+ const found = runOf(context, params.id);
2105
+ if (!found.ok) return found.refusal;
2106
+ const noted = recordNote(store, found.run, body);
2107
+ return noted.ok ? ok({ item: noted.item }) : fail(400, noted.error);
2108
+ },
2109
+ feedbackItems: () => ok(store.feedbackItems({ all: true })),
2110
+ feedbackItem: ({ params }) => {
2111
+ const found = itemOf(store, params.id);
2112
+ if (!found.ok) return found.refusal;
2113
+ return ok(detailOf(store, found.item));
2114
+ },
2115
+ feedbackStatus: ({ params, body }) => {
2116
+ const found = itemOf(store, params.id);
2117
+ if (!found.ok) return found.refusal;
2118
+ const { item } = found;
2119
+ if (body.status === "new" && item.status !== "dismissed") return fail(409, `${item.id.slice(0, 12)} isn't dismissed, so there's nothing to restore.`);
2120
+ store.setFeedbackStatus(item.id, body.status);
2121
+ return ok(fresh(store, item));
2122
+ },
2123
+ accept: ({ params, body }) => {
2124
+ const found = itemOf(store, params.id);
2125
+ if (!found.ok) return found.refusal;
2126
+ const accepted = acceptItem(store, found.item, body, context.store.projects());
2127
+ if (!accepted.ok) return fail(409, accepted.error);
2128
+ return ok({
2129
+ item: fresh(store, found.item),
2130
+ ...accepted.lesson === void 0 ? {} : { lesson: accepted.lesson }
2131
+ });
2132
+ },
2133
+ apply: ({ params, body }) => {
2134
+ const found = itemOf(store, params.id);
2135
+ if (!found.ok) return found.refusal;
2136
+ return applyRoute(context, found.item, body.driver);
2137
+ },
2138
+ publishPreview: async ({ params, body }) => {
2139
+ const found = itemOf(store, params.id);
2140
+ if (!found.ok) return found.refusal;
2141
+ const previewed = await previewOf(context, found.item, body.hideNames);
2142
+ return previewed.ok ? ok(previewed.preview) : previewed.refusal;
2143
+ },
2144
+ publish: async ({ params, body }) => {
2145
+ const found = itemOf(store, params.id);
2146
+ if (!found.ok) return found.refusal;
2147
+ const sent = await sendPublish(context, found.item, body);
2148
+ return sent.ok ? ok({
2149
+ item: fresh(store, found.item),
2150
+ url: sent.url,
2151
+ kind: sent.kind
2152
+ }) : sent.refusal;
2153
+ },
2154
+ split: ({ params }) => {
2155
+ const found = itemOf(store, params.id);
2156
+ if (!found.ok) return found.refusal;
2157
+ if (found.item.merged.length === 0) return fail(409, `Nothing was merged into ${found.item.id.slice(0, 12)}, so there's nothing to split.`);
2158
+ return ok({ moved: store.splitFeedback(found.item.id) });
2159
+ }
2160
+ };
2161
+ };
2162
+ const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\u001B[@-Z\\-_]/gu;
2163
+ /** A line as a terminal would end up showing it. */
2164
+ const plainLine = (raw) => (raw.replaceAll(ANSI, "").split("\r").findLast((part) => part !== "") ?? "").replaceAll(/\s+$/gu, "");
2165
+ const startJob = (latest, argv, install, onChange) => {
2166
+ const job = {
2167
+ id: randomUUID(),
2168
+ state: "running",
2169
+ latest,
2170
+ command: [...argv],
2171
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
2172
+ };
2173
+ const lines = [];
2174
+ const partial = {
2175
+ stdout: "",
2176
+ stderr: ""
2177
+ };
2178
+ const listeners = /* @__PURE__ */ new Set();
2179
+ let seq = 0;
2180
+ let ended = null;
2181
+ const tell = (event) => {
2182
+ for (const listener of listeners) listener(event);
2183
+ };
2184
+ const keep = (stream, raw) => {
2185
+ seq += 1;
2186
+ const line = {
2187
+ seq,
2188
+ stream,
2189
+ text: plainLine(raw)
2190
+ };
2191
+ lines.push(line);
2192
+ if (lines.length > 2e3) lines.shift();
2193
+ tell({
2194
+ kind: "output",
2195
+ line
2196
+ });
2197
+ };
2198
+ const output = (stream, text) => {
2199
+ const parts = `${partial[stream]}${text}`.split("\n");
2200
+ partial[stream] = parts.pop() ?? "";
2201
+ for (const part of parts) keep(stream, part);
2202
+ };
2203
+ const finished = install(argv, output).catch((error) => {
2204
+ keep("stderr", error instanceof Error ? error.message : String(error));
2205
+ return 1;
2206
+ });
2207
+ const finish = async () => {
2208
+ const code = await finished;
2209
+ for (const stream of ["stdout", "stderr"]) {
2210
+ if (partial[stream] !== "") keep(stream, partial[stream]);
2211
+ partial[stream] = "";
2212
+ }
2213
+ const state = code === 0 ? "succeeded" : "failed";
2214
+ Object.assign(job, {
2215
+ state,
2216
+ code,
2217
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
2218
+ });
2219
+ ended = {
2220
+ state,
2221
+ code,
2222
+ version: latest
2223
+ };
2224
+ onChange();
2225
+ tell({
2226
+ kind: "end",
2227
+ end: ended
2228
+ });
2229
+ };
2230
+ return {
2231
+ view: () => ({
2232
+ ...job,
2233
+ command: [...job.command]
2234
+ }),
2235
+ linesAfter: (after) => ({
2236
+ lines: lines.filter((line) => line.seq > after),
2237
+ dropped: (lines[0]?.seq ?? 1) > after + 1
2238
+ }),
2239
+ end: () => ended,
2240
+ done: finish(),
2241
+ listen: (listener) => {
2242
+ listeners.add(listener);
2243
+ return () => {
2244
+ listeners.delete(listener);
2245
+ };
2246
+ }
2247
+ };
2248
+ };
2249
+ // ../server/src/upgrade-plan.ts
2250
+ /**
2251
+ * @file The upgrade `ork ui` offers, which is `ork upgrade`'s
2252
+ * (specs/cli/updates.md): the plan asks the registry now and records the
2253
+ * answer as `ork upgrade` does, then gives core's plan with an id; running it
2254
+ * rebuilds the plan and runs the server's own command only when the id the
2255
+ * person read still matches, so the page never sends a command.
2256
+ */
2257
+ /** How long the plan waits on the registry, as `ork upgrade`. */
2258
+ const ASK_TIMEOUT_MS = 15e3;
2259
+ const NO_UPGRADE = "This server can't upgrade Ork; run `ork upgrade` in a terminal.";
2260
+ const BUSY = "An upgrade is already running.";
2261
+ const UNREACHABLE = "Could not reach the npm registry to look for a new version. Nothing was run.";
2262
+ const ADVICE = "Ork runs only a package manager's own command from here; this one is yours to run.";
2263
+ /** Records what the registry said, as `ork upgrade` does. */
2264
+ const remember = (file, latest, notified) => {
2265
+ try {
2266
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2267
+ writeUpdateCache(file, {
2268
+ ...readUpdateCache(file),
2269
+ checkedAt: now,
2270
+ latest,
2271
+ ...notified ? { notified: {
2272
+ version: latest,
2273
+ at: now
2274
+ } } : {}
2275
+ });
2276
+ } catch {}
2277
+ };
2278
+ const idOf = (parts) => createHash("sha256").update(JSON.stringify(parts)).digest("hex").slice(0, 16);
2279
+ /** Asks the registry now and says what an upgrade would do. */
2280
+ const askPlan = async (context) => {
2281
+ const { updates, version: current } = context;
2282
+ const { options } = updates;
2283
+ if (options === void 0) return null;
2284
+ const registry = registryOf(options.env ?? process.env);
2285
+ const latest = await fetchLatest(options.name, {
2286
+ ...registry === void 0 ? {} : { registry },
2287
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
2288
+ timeoutMs: ASK_TIMEOUT_MS,
2289
+ current
2290
+ });
2291
+ const script = options.script ?? runningScript();
2292
+ const plan = planUpgrade({
2293
+ name: options.name,
2294
+ current,
2295
+ latest,
2296
+ script
2297
+ });
2298
+ if (plan.kind === "unreachable") return plan;
2299
+ if (latest !== null) remember(updates.file, latest, plan.kind !== "latest");
2300
+ updates.forget();
2301
+ if (plan.kind === "latest") return {
2302
+ kind: "latest",
2303
+ current
2304
+ };
2305
+ const shared = {
2306
+ current,
2307
+ latest: plan.latest,
2308
+ releaseNotes: releaseNotes(options.name, plan.latest),
2309
+ running: runningRunIds(context.store)
2310
+ };
2311
+ if (plan.kind === "command") {
2312
+ const command = [...plan.argv];
2313
+ const { id, label } = plan.installer;
2314
+ return {
2315
+ kind: "command",
2316
+ planId: idOf([
2317
+ plan.kind,
2318
+ plan.latest,
2319
+ id,
2320
+ command
2321
+ ]),
2322
+ ...shared,
2323
+ installer: {
2324
+ id,
2325
+ label
2326
+ },
2327
+ command
2328
+ };
2329
+ }
2330
+ const installer = detectInstaller(script);
2331
+ return {
2332
+ kind: "advice",
2333
+ planId: idOf([
2334
+ plan.kind,
2335
+ plan.latest,
2336
+ installer?.id,
2337
+ plan.text
2338
+ ]),
2339
+ ...shared,
2340
+ installer: installer === void 0 ? null : {
2341
+ id: installer.id,
2342
+ label: installer.label
2343
+ },
2344
+ text: plan.text,
2345
+ words: plan.words,
2346
+ commands: [...plan.commands]
2347
+ };
2348
+ };
2349
+ const changed = (plan) => {
2350
+ return `The plan changed since you read it: ${plan.kind === "latest" ? `Ork ${plan.current} is the latest version now` : `Ork ${plan.latest} is out now`}. Read it again before upgrading.`;
2351
+ };
2352
+ const running = (context) => context.updates.job()?.view().state === "running";
2353
+ /** Runs the plan the person read, when it is still the plan. */
2354
+ const startUpgrade = async (context, planId) => {
2355
+ const { updates } = context;
2356
+ const { options } = updates;
2357
+ if (options === void 0) return fail(400, NO_UPGRADE);
2358
+ if (running(context)) return fail(409, BUSY);
2359
+ const plan = await askPlan(context);
2360
+ if (running(context)) return fail(409, BUSY);
2361
+ if (plan === null || plan.kind === "unreachable") return fail(400, UNREACHABLE);
2362
+ if (plan.kind === "latest" || plan.planId !== planId) return fail(409, changed(plan));
2363
+ if (plan.kind === "advice") return fail(400, ADVICE);
2364
+ const job = startJob(plan.latest, plan.command, options.install ?? runInstall, updates.forget);
2365
+ updates.setJob(job);
2366
+ return accepted({ job: job.view() });
2367
+ };
2368
+ const CANNOT_RESTART = "This server can't restart itself; stop it and run `ork ui`.";
2369
+ const RESTART_BUSY = "An upgrade is running; restart `ork ui` once it has ended.";
2370
+ const updateRoutes = (context, port) => ({
2371
+ version: () => ok(context.updates.info()),
2372
+ upgradePlan: async () => {
2373
+ const plan = await askPlan(context);
2374
+ return plan === null ? fail(400, NO_UPGRADE) : ok(plan);
2375
+ },
2376
+ upgrade: ({ body }) => startUpgrade(context, body.planId),
2377
+ restart: () => {
2378
+ const { restart } = context;
2379
+ if (restart === void 0) return fail(400, CANNOT_RESTART);
2380
+ if (context.updates.job()?.view().state === "running") return fail(409, RESTART_BUSY);
2381
+ setTimeout(restart, 100);
2382
+ return accepted({
2383
+ port: port(),
2384
+ pid: context.pid
2385
+ });
2386
+ }
2387
+ });
2388
+ // ../server/src/worktrees-read.ts
2389
+ /**
2390
+ * @file What the Worktrees panel reads, through the same core functions as `ork
2391
+ * worktrees`: the projects Ork knows (registered, or a stored run's), each
2392
+ * one's items with core's words and what Remove… allows, and the plan Clean
2393
+ * merged… would carry out, with a digest of it. A project git can't read
2394
+ * comes back with its error, never as a failed request.
2395
+ */
2396
+ /** Every project Ork knows, by name: registered ones, then runs' others. */
2397
+ const knownProjects = (context) => {
2398
+ const names = new Map(context.store.projects().map((p) => [p.root, p.id]));
2399
+ for (const run of context.store.runs()) if (!names.has(run.project)) names.set(run.project, path.basename(run.project) || run.project);
2400
+ return [...names].map(([root, name]) => ({
2401
+ root,
2402
+ name
2403
+ })).toSorted((a, b) => a.name.localeCompare(b.name));
2404
+ };
2405
+ /** The project `root` names, or all of them; only roots Ork knows. */
2406
+ const projectsFor = (context, root) => {
2407
+ const known = knownProjects(context);
2408
+ if (root === void 0) return {
2409
+ ok: true,
2410
+ projects: known
2411
+ };
2412
+ const found = known.filter((p) => p.root === root);
2413
+ return found.length === 0 ? {
2414
+ ok: false,
2415
+ refusal: fail(404, `Ork doesn't know a project at ${root}.`)
2416
+ } : {
2417
+ ok: true,
2418
+ projects: found
2419
+ };
2420
+ };
2421
+ const failed = (known, error, fix) => ({
2422
+ ...known,
2423
+ head: null,
2424
+ error,
2425
+ ...fix === void 0 ? {} : { fix }
2426
+ });
2427
+ /** A project's items and runs still going, or why git couldn't read them. */
2428
+ const readProject = async (context, known) => {
2429
+ if (!existsSync(known.root)) {
2430
+ const registered = context.store.projects().find((p) => p.root === known.root);
2431
+ return failed(known, `its folder ${known.root} is gone.`, registered === void 0 ? void 0 : `ork projects move ${registered.id} <new path>`);
2432
+ }
2433
+ const head = await projectHead(known.root);
2434
+ if (!head.ok) return failed(known, "Not in a git repository with a commit.");
2435
+ try {
2436
+ const worktrees = await readWorktrees(context.store, known.root);
2437
+ return {
2438
+ ...known,
2439
+ head: head.branch,
2440
+ worktrees
2441
+ };
2442
+ } catch (error) {
2443
+ return failed(known, error instanceof Error ? error.message : String(error));
2444
+ }
2445
+ };
2446
+ const runKnown = (context, id) => context.store.lookup(id).some((run) => run.id === id);
2447
+ const rowOf = (context, item, live) => {
2448
+ const going = live.has(item.runId);
2449
+ const refusal = removeRefusal(item, going);
2450
+ const keeps = item.merged ? cleanStep(item, {
2451
+ branches: false,
2452
+ force: false,
2453
+ live
2454
+ }).skip : void 0;
2455
+ return {
2456
+ ...item,
2457
+ key: keyOf(item),
2458
+ label: labelOf(item),
2459
+ state: mergeStateOf(item),
2460
+ parts: stateParts(item),
2461
+ run: {
2462
+ known: runKnown(context, item.runId),
2463
+ live: going
2464
+ },
2465
+ ...keeps === void 0 ? {} : { cleanKeeps: keeps },
2466
+ remove: {
2467
+ label: item.path === null ? "Delete branch…" : "Remove…",
2468
+ ...refusal === void 0 ? {} : { refusal }
2469
+ }
2470
+ };
2471
+ };
2472
+ /** A read project as the list shows it. */
2473
+ const listed = (context, read) => {
2474
+ const { worktrees, ...rest } = read;
2475
+ const live = worktrees?.live ?? /* @__PURE__ */ new Set();
2476
+ return {
2477
+ ...rest,
2478
+ items: (worktrees?.items ?? []).map((item) => rowOf(context, item, live))
2479
+ };
2480
+ };
2481
+ /** Clean merged's plan for a read project: never forced. */
2482
+ const cleanStepsOf = (read, branches) => read.worktrees === void 0 ? [] : planClean(read.worktrees.items, { merged: true }, {
2483
+ branches,
2484
+ force: false,
2485
+ live: read.worktrees.live
2486
+ });
2487
+ /** A read project's clean plan as the dialog shows it. */
2488
+ const cleanPlanOf = (read, steps) => ({
2489
+ root: read.root,
2490
+ name: read.name,
2491
+ head: read.head,
2492
+ ...read.error === void 0 ? {} : { error: read.error },
2493
+ steps: steps.map((step) => ({
2494
+ key: keyOf(step.item),
2495
+ line: planLine(step),
2496
+ removeWorktree: step.removeWorktree,
2497
+ deleteBranch: step.deleteBranch,
2498
+ prunes: stepPrunes(step),
2499
+ ...step.skip === void 0 ? {} : { skip: step.skip }
2500
+ }))
2501
+ });
2502
+ /** What a person was shown, as a digest an act hands back. */
2503
+ const digestOf = (shown) => createHash("sha256").update(JSON.stringify(shown)).digest("hex");
2504
+ // ../server/src/worktrees-remove.ts
2505
+ /**
2506
+ * @file Remove… for one item, the twin of `ork worktrees remove`: the plan by
2507
+ * core's rule with what it would lose, as git prints it, and a digest of
2508
+ * both; then the removal, which plans again and goes ahead only when the
2509
+ * digest is the one the person saw, and forces only a plan that needed it.
2510
+ * What to remove is resolved from a fresh listing, never a path sent in.
2511
+ */
2512
+ /** How many of git's lines a well shows; the rest are counted. */
2513
+ const SHOWN_LINES = 20;
2514
+ const firstOf = (all) => all.slice(0, SHOWN_LINES);
2515
+ const moreOf = (all) => Math.max(0, all.length - SHOWN_LINES);
2516
+ /** One item's remove plan, as the dialog shows it, or why there's none. */
2517
+ const removePlanOf = async (context, ask) => {
2518
+ const found = projectsFor(context, ask.project);
2519
+ if (!found.ok) return found;
2520
+ const [known] = found.projects;
2521
+ if (known === void 0) return {
2522
+ ok: false,
2523
+ refusal: fail(404, "No project.")
2524
+ };
2525
+ const read = await readProject(context, known);
2526
+ if (read.worktrees === void 0) {
2527
+ const why = `Could not read ${known.name}'s worktrees: ${read.error ?? ""}`;
2528
+ return {
2529
+ ok: false,
2530
+ refusal: fail(400, why)
2531
+ };
2532
+ }
2533
+ const planned = planRemove(read.worktrees, ask.key, ask.deleteBranch);
2534
+ if (!planned.ok) return {
2535
+ ok: false,
2536
+ refusal: fail(planned.gone ? 404 : 400, planned.error)
2537
+ };
2538
+ const { plan } = planned;
2539
+ const lost = await lostWork(known.root, plan);
2540
+ const losing = {
2541
+ changes: firstOf(lost.changes),
2542
+ commits: firstOf(lost.commits),
2543
+ commitsOn: lost.commitsOn,
2544
+ more: {
2545
+ changes: moreOf(lost.changes),
2546
+ commits: moreOf(lost.commits)
2547
+ }
2548
+ };
2549
+ const { step } = plan;
2550
+ return {
2551
+ ok: true,
2552
+ known,
2553
+ plan,
2554
+ answer: {
2555
+ parts: stateParts(step.item, read.head ?? void 0),
2556
+ merged: step.item.merged,
2557
+ ahead: step.item.ahead,
2558
+ line: planLine(step),
2559
+ removeWorktree: step.removeWorktree,
2560
+ deleteBranch: step.deleteBranch,
2561
+ prunes: stepPrunes(step) && !step.removeWorktree,
2562
+ force: plan.force,
2563
+ losing,
2564
+ digest: digestOf({
2565
+ step,
2566
+ force: plan.force,
2567
+ lost
2568
+ })
2569
+ }
2570
+ };
2571
+ };
2572
+ const CHANGED = "It changed since you read it. Read it again before removing.";
2573
+ /** Removes the item when its plan is still the one shown as `digest`. */
2574
+ const removeIt = async (context, ask, busy) => {
2575
+ if (busy.has(ask.project)) {
2576
+ const name = projectsFor(context, ask.project);
2577
+ const called = name.ok ? name.projects[0]?.name ?? ask.project : ask.project;
2578
+ return {
2579
+ ok: false,
2580
+ refusal: fail(409, `A remove or clean is already running in ${called}.`)
2581
+ };
2582
+ }
2583
+ busy.add(ask.project);
2584
+ try {
2585
+ const planned = await removePlanOf(context, ask);
2586
+ if (!planned.ok) return planned;
2587
+ if (planned.answer.digest !== ask.digest) return {
2588
+ ok: false,
2589
+ refusal: fail(409, CHANGED)
2590
+ };
2591
+ if (planned.plan.force && !ask.force) return {
2592
+ ok: false,
2593
+ refusal: fail(400, "That loses work: confirm what's lost first.")
2594
+ };
2595
+ const done = [];
2596
+ try {
2597
+ await applyRemove(planned.known.root, planned.plan, done);
2598
+ } catch (error) {
2599
+ const why = error instanceof Error ? error.message : String(error);
2600
+ return {
2601
+ ok: false,
2602
+ refusal: fail(400, why)
2603
+ };
2604
+ }
2605
+ return {
2606
+ ok: true,
2607
+ done
2608
+ };
2609
+ } finally {
2610
+ busy.delete(ask.project);
2611
+ }
2612
+ };
2613
+ // ../server/src/worktrees-routes.ts
2614
+ /**
2615
+ * @file The Worktrees panel's routes (specs/worktrees.md), through the same
2616
+ * core functions as `ork worktrees`: the list for every project Ork knows
2617
+ * (read in parallel, a project git can't read carrying its error), Remove…
2618
+ * (worktrees-remove.ts), and Clean merged…, which only ever cleans what's
2619
+ * merged, never forced, and goes ahead only when its plan's digest is the one
2620
+ * the person saw. A project takes one write at a time.
2621
+ */
2622
+ const PLAN_CHANGED = "The plan changed since you read it. Read it again before cleaning.";
2623
+ /** Carries out one project's part of a clean; what it did, and where it stopped. */
2624
+ const cleanOne = async (read, branches) => {
2625
+ const done = [];
2626
+ const acting = cleanStepsOf(read, branches).filter((step) => stepActs(step));
2627
+ try {
2628
+ if (acting.length > 0) await applyClean(read.root, acting, false, done);
2629
+ return {
2630
+ root: read.root,
2631
+ name: read.name,
2632
+ done
2633
+ };
2634
+ } catch (error) {
2635
+ const why = error instanceof Error ? error.message : String(error);
2636
+ return {
2637
+ root: read.root,
2638
+ name: read.name,
2639
+ done,
2640
+ error: why
2641
+ };
2642
+ }
2643
+ };
2644
+ const worktreesRoutes = (context) => {
2645
+ /** The projects a remove or clean is writing to now. */
2646
+ const busy = /* @__PURE__ */ new Set();
2647
+ const readAll = async (project) => {
2648
+ const found = projectsFor(context, project);
2649
+ if (!found.ok) return found;
2650
+ return {
2651
+ ok: true,
2652
+ reads: await Promise.all(found.projects.map((known) => readProject(context, known)))
2653
+ };
2654
+ };
2655
+ const cleanPlan = async (body) => {
2656
+ const all = await readAll(body.project);
2657
+ if (!all.ok) return all;
2658
+ const projects = all.reads.map((read) => cleanPlanOf(read, cleanStepsOf(read, body.branches)));
2659
+ return {
2660
+ ok: true,
2661
+ reads: all.reads,
2662
+ projects,
2663
+ digest: digestOf(projects)
2664
+ };
2665
+ };
2666
+ return {
2667
+ worktrees: async ({ query }) => {
2668
+ const all = await readAll(query.project);
2669
+ if (!all.ok) return all.refusal;
2670
+ return ok({ projects: all.reads.map((read) => listed(context, read)) });
2671
+ },
2672
+ worktreeRemovePlan: async ({ body }) => {
2673
+ const planned = await removePlanOf(context, body);
2674
+ return planned.ok ? ok(planned.answer) : planned.refusal;
2675
+ },
2676
+ worktreeRemove: async ({ body }) => {
2677
+ const removed = await removeIt(context, body, busy);
2678
+ return removed.ok ? ok({ done: removed.done }) : removed.refusal;
2679
+ },
2680
+ worktreesCleanPlan: async ({ body }) => {
2681
+ const planned = await cleanPlan(body);
2682
+ if (!planned.ok) return planned.refusal;
2683
+ return ok({
2684
+ projects: planned.projects,
2685
+ digest: planned.digest
2686
+ });
2687
+ },
2688
+ worktreesClean: async ({ body }) => {
2689
+ const taken = projectsFor(context, body.project);
2690
+ if (!taken.ok) return taken.refusal;
2691
+ const held = taken.projects.find((p) => busy.has(p.root));
2692
+ if (held !== void 0) return fail(409, `A remove or clean is already running in ${held.name}.`);
2693
+ const roots = taken.projects.map((p) => p.root);
2694
+ for (const root of roots) busy.add(root);
2695
+ try {
2696
+ const planned = await cleanPlan(body);
2697
+ if (!planned.ok) return planned.refusal;
2698
+ if (planned.digest !== body.digest) return fail(409, PLAN_CHANGED);
2699
+ const projects = [];
2700
+ for (const read of planned.reads) projects.push(await cleanOne(read, body.branches));
2701
+ return ok({ projects });
2702
+ } finally {
2703
+ for (const root of roots) busy.delete(root);
2704
+ }
2705
+ }
2706
+ };
2707
+ };
1623
2708
  // ../server/src/api-routes.ts
1624
2709
  /** @file Every JSON route's handler, in one place. */
1625
- const apiRoutes = (context) => ({
2710
+ const apiRoutes = (context, port) => ({
1626
2711
  ...readRoutes(context),
1627
2712
  ...writeRoutes(context),
1628
- ...builderRoutes(context)
2713
+ ...builderRoutes(context),
2714
+ ...contextRoutes(context),
2715
+ ...feedbackRoutes(context),
2716
+ ...worktreesRoutes(context),
2717
+ ...updateRoutes(context, port)
1629
2718
  });
1630
2719
  const PLACEHOLDER_HTML = `<!doctype html>
1631
2720
  <html lang="en">
@@ -1780,7 +2869,8 @@ const overviewStream = (context, res, url, shutdown) => {
1780
2869
  const stream = openEventStream(res);
1781
2870
  const last = {
1782
2871
  runs: "",
1783
- decisions: ""
2872
+ decisions: "",
2873
+ version: ""
1784
2874
  };
1785
2875
  const tick = () => {
1786
2876
  const filter = root === void 0 ? {} : { project: root };
@@ -1789,7 +2879,11 @@ const overviewStream = (context, res, url, shutdown) => {
1789
2879
  limit: OVERVIEW_RUNS
1790
2880
  }).map((run) => viewOf$2(context, run));
1791
2881
  const decisions = pendingDecisions(context.store, filter);
1792
- for (const [name, value] of [["runs", runs], ["decisions", decisions]]) {
2882
+ for (const [name, value] of [
2883
+ ["runs", runs],
2884
+ ["decisions", decisions],
2885
+ ["version", context.updates.info()]
2886
+ ]) {
1793
2887
  const text = JSON.stringify(value);
1794
2888
  if (text === last[name]) continue;
1795
2889
  last[name] = text;
@@ -1805,10 +2899,53 @@ const overviewStream = (context, res, url, shutdown) => {
1805
2899
  stream.signal.addEventListener("abort", stop);
1806
2900
  shutdown.addEventListener("abort", stop, { signal: stream.signal });
1807
2901
  };
2902
+ // ../server/src/upgrade-stream.ts
2903
+ const NO_JOB = "No upgrade has run since `ork ui` started.";
2904
+ const upgradeStream = (context, req, res, url, shutdown) => {
2905
+ const job = context.updates.job();
2906
+ if (job === null) {
2907
+ sendError(res, 404, NO_JOB);
2908
+ return;
2909
+ }
2910
+ const after = startAfter(req, url);
2911
+ if (after === null) {
2912
+ sendError(res, 400, "Last-Event-ID or after must be a sequence number.");
2913
+ return;
2914
+ }
2915
+ const stream = openEventStream(res);
2916
+ const { lines, dropped } = job.linesAfter(after);
2917
+ if (dropped) {
2918
+ const note = {
2919
+ seq: (lines[0]?.seq ?? after + 1) - 1,
2920
+ stream: "stderr",
2921
+ text: "Earlier lines dropped."
2922
+ };
2923
+ stream.send("output", note);
2924
+ }
2925
+ for (const line of lines) stream.send("output", line, line.seq);
2926
+ const ended = job.end();
2927
+ if (ended !== null) {
2928
+ stream.send("end", ended);
2929
+ stream.close();
2930
+ return;
2931
+ }
2932
+ const stop = job.listen((event) => {
2933
+ if (event.kind === "output") {
2934
+ stream.send("output", event.line, event.line.seq);
2935
+ return;
2936
+ }
2937
+ stream.send("end", event.end);
2938
+ stream.close();
2939
+ });
2940
+ stream.signal.addEventListener("abort", stop);
2941
+ shutdown.addEventListener("abort", () => {
2942
+ stream.close();
2943
+ }, { signal: stream.signal });
2944
+ };
1808
2945
  // ../server/src/dispatch.ts
1809
2946
  const RUN_STREAM = compilePath(STREAMS.run);
1810
2947
  const createDispatch = (context, access) => {
1811
- const handlers = apiRoutes(context);
2948
+ const handlers = apiRoutes(context, access.port);
1812
2949
  return async (req, res) => {
1813
2950
  const method = req.method ?? "GET";
1814
2951
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -1844,6 +2981,10 @@ const createDispatch = (context, access) => {
1844
2981
  overviewStream(context, res, url, access.shutdown);
1845
2982
  return;
1846
2983
  }
2984
+ if (method === "GET" && url.pathname === STREAMS.upgrade) {
2985
+ upgradeStream(context, req, res, url, access.shutdown);
2986
+ return;
2987
+ }
1847
2988
  if (await route(handlers, req, res, url)) return;
1848
2989
  if (api) sendError(res, 404, `No API route ${url.pathname}.`);
1849
2990
  else sendPage(res, method, url.pathname, context.uiDir);
@@ -1911,6 +3052,11 @@ const startServer = async (options) => {
1911
3052
  const store = openStore(storePath(options.home));
1912
3053
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1913
3054
  const uiDir = options.ui?.find((dir) => hasUi(dir));
3055
+ const updates = createUpdates({
3056
+ version: options.version,
3057
+ ...options.home === void 0 ? {} : { home: options.home },
3058
+ ...options.update === void 0 ? {} : { options: options.update }
3059
+ });
1914
3060
  const context = {
1915
3061
  store,
1916
3062
  version: options.version,
@@ -1918,9 +3064,12 @@ const startServer = async (options) => {
1918
3064
  startedAt,
1919
3065
  ...options.home === void 0 ? {} : { home: options.home },
1920
3066
  ...options.script === void 0 ? {} : { script: options.script },
1921
- ...uiDir === void 0 ? {} : { uiDir }
3067
+ ...uiDir === void 0 ? {} : { uiDir },
3068
+ ...options.gh === void 0 ? {} : { gh: options.gh },
3069
+ updates,
3070
+ ...options.restart === void 0 ? {} : { restart: options.restart }
1922
3071
  };
1923
- const token = newToken();
3072
+ const token = options.token ?? newToken();
1924
3073
  const shutdown = new AbortController();
1925
3074
  const log = options.log ?? ((line) => {
1926
3075
  process.stderr.write(`${line}\n`);
@@ -1941,6 +3090,7 @@ const startServer = async (options) => {
1941
3090
  try {
1942
3091
  port = await listen(server, port);
1943
3092
  } catch (error) {
3093
+ updates.close();
1944
3094
  store.close();
1945
3095
  throw error;
1946
3096
  }
@@ -1956,9 +3106,14 @@ const startServer = async (options) => {
1956
3106
  port,
1957
3107
  url: `http://127.0.0.1:${String(port)}`,
1958
3108
  token,
3109
+ upgrading: () => {
3110
+ const job = updates.job();
3111
+ return job === null || job.end() !== null ? null : job.done;
3112
+ },
1959
3113
  close: () => {
1960
3114
  closed ??= new Promise((resolve) => {
1961
3115
  shutdown.abort();
3116
+ updates.close();
1962
3117
  server.close(() => {
1963
3118
  store.close();
1964
3119
  if (options.record !== false) removeUiFile(file, process.pid);
@@ -1970,4 +3125,4 @@ const startServer = async (options) => {
1970
3125
  }
1971
3126
  };
1972
3127
  };
1973
- export { openMcp as a, refused as c, uiFilePath as i, startServer as n, tool as o, runningServer as r, answer as s, DEFAULT_PORT as t };
3128
+ export { openMcp as a, refused$1 as c, uiFilePath as i, newToken as l, startServer as n, tool as o, runningServer as r, answer as s, DEFAULT_PORT as t };