@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.
- package/README.md +16 -12
- package/dist/__build_info__-DdTVd2p5.mjs +615 -0
- package/dist/bin.mjs +1572 -909
- package/dist/clean-NnboOuz6.mjs +88 -0
- package/dist/completion-snnYGjgK.mjs +405 -0
- package/dist/{context-BFGC_C02.mjs → context-DO3sBy9v.mjs} +30 -191
- package/dist/decide-B81wCZQg.mjs +113 -0
- package/dist/{find-BcvhT6UN.mjs → find-BKW-uGey.mjs} +1 -1
- package/dist/{format-DqpObyMl.mjs → format-CmTlTOjT.mjs} +72 -198
- package/dist/help-groups-BwYoAWsh.mjs +354 -0
- package/dist/{in-a-run-BbLhZOFW.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
- package/dist/{limits-BKYAmqCR.mjs → limits-JCve2QpI.mjs} +1 -1
- package/dist/list-C7UEgmtd.mjs +110 -0
- package/dist/{mcp-BWsdQyri.mjs → mcp-CKPyNaKD.mjs} +7 -7
- package/dist/{output-Q2EdeVUt.mjs → output-B1V6ZJSx.mjs} +130 -36
- package/dist/{pause-DKACDfV1.mjs → pause-B_SL_yDz.mjs} +4 -4
- package/dist/{relay-CbK-Wme9.mjs → relay-o-Luee1c.mjs} +2 -2
- package/dist/remove-DAZ7ZCzt.mjs +194 -0
- package/dist/{send-C6yr04Fe.mjs → send-3B64fBNB.mjs} +3 -3
- package/dist/{src-D5ruZFgD.mjs → src-BJTegEzY.mjs} +1212 -57
- package/dist/{src-DtN1k-B3.mjs → src-C38BPzit.mjs} +251 -18
- package/dist/{src-Cz1yOUTg.mjs → src-DHvWU4tX.mjs} +2742 -1659
- package/dist/{src-CZOVF2yU.mjs → src-Dyz_Gy1q.mjs} +35 -8
- package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
- package/dist/ui/assets/app-CH7YC3in.css +1 -0
- package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
- package/dist/ui/assets/{index-BoG6O-Lp.js → index-D83F2OKZ.js} +2 -2
- package/dist/ui/index.html +2 -3
- package/dist/ui-CY4xxJUu.mjs +472 -0
- package/dist/{usage-BIjf9Xdt.mjs → usage-CJnytoe4.mjs} +6 -5
- package/dist/{watch-B8o4Szd_.mjs → watch-BJB82ZJ7.mjs} +7 -7
- package/dist/workflows-CiM7S3l0.mjs +88 -0
- package/dist/worktrees-86ySXxR6.mjs +212 -0
- package/package.json +6 -6
- package/dist/__build_info__-CXXMLvjt.mjs +0 -104
- package/dist/ui/assets/app-BlDdP396.js +0 -61
- package/dist/ui/assets/app-C722xNZ3.css +0 -1
- package/dist/ui-CZS1XKla.mjs +0 -160
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { B as
|
|
2
|
-
import { a as
|
|
3
|
-
import { n as
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
309
|
-
|
|
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,
|
|
365
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 [
|
|
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 };
|