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