@krapht/ork 1.0.0-alpha.14 → 1.0.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +10 -4
  2. package/dist/__build_info__-DcRdK0Du.mjs +359 -0
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/{attach-CdtGX9se.mjs → attach-7lB5_jpr.mjs} +5 -4
  5. package/dist/{store-CleF7KeS.mjs → attach-B-FeE1dJ.mjs} +60 -25
  6. package/dist/bin.mjs +328 -1734
  7. package/dist/brief-BvMtIFio.mjs +58 -0
  8. package/dist/{builtin-files-Bdq23IIi.mjs → builtin-files-BRSOObAv.mjs} +1 -1
  9. package/dist/command-tree-BkSYWmYJ.mjs +63 -0
  10. package/dist/{completion--ywde-56.mjs → completion-Cgb2qPPh.mjs} +45 -6
  11. package/dist/context-BN9A2Nnu.mjs +106 -0
  12. package/dist/{find-BVX-bL4u.mjs → find-mLYsR0uC.mjs} +2 -2
  13. package/dist/flags-CdgIH1L6.mjs +230 -0
  14. package/dist/follow-ups-DxCsOby1.mjs +496 -0
  15. package/dist/{format-CvTE5CZQ.mjs → format-hiDvtNGM.mjs} +35 -8
  16. package/dist/{help-groups-CpR2UzSn.mjs → help-groups-hB6kY34d.mjs} +5 -63
  17. package/dist/{in-a-run-cL5N9gpH.mjs → in-a-run-a447hxq8.mjs} +1 -1
  18. package/dist/{__build_info__-B82-dGJY.mjs → install-23g_1ngs.mjs} +3 -227
  19. package/dist/{limits-Dhe--wmX.mjs → limits-C_Xz_t3C.mjs} +1 -1
  20. package/dist/{mcp-CsmboyXv.mjs → mcp-CWng9I9n.mjs} +6 -6
  21. package/dist/{once-CrwI-ZdN.mjs → once-lvwcWfVf.mjs} +3 -3
  22. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  23. package/dist/{pause-DssrYcda.mjs → pause-Cy11KrXK.mjs} +3 -3
  24. package/dist/post-C9soCx7i.mjs +46 -0
  25. package/dist/print-BaWiWf4v.mjs +19 -0
  26. package/dist/project-records-BRiyQzUo.mjs +230 -0
  27. package/dist/{relay-DASRgsTQ.mjs → relay-C0M1TWMB.mjs} +1 -1
  28. package/dist/run-B-j3Qxqc.mjs +1464 -0
  29. package/dist/{run-D8divGCs.mjs → run-D_u9V_Ve.mjs} +1 -1
  30. package/dist/{send-CXv8bx0i.mjs → send-qBxj0JbJ.mjs} +2 -2
  31. package/dist/{src-njdtejgZ.mjs → src-CXBmaGwS.mjs} +213 -75
  32. package/dist/{src-C7z_6fhn.mjs → src-DHDQ0itC.mjs} +106 -50
  33. package/dist/{src-tsuZRu0H.mjs → src-DN-RPsil.mjs} +5423 -4467
  34. package/dist/{src-8Mul0695.mjs → src-nXLIwDrt.mjs} +325 -81
  35. package/dist/store-Dyr1mP8K.mjs +16 -0
  36. package/dist/ui/assets/app-BPSCxb_l.css +1 -0
  37. package/dist/ui/assets/app-CnNRowFn.js +111 -0
  38. package/dist/ui/assets/{index-D-_uPI2v.js → index-CVzwYlz3.js} +2 -2
  39. package/dist/ui/index.html +1 -1
  40. package/dist/{ui-CNO1M0qT.mjs → ui-DLrSt5ny.mjs} +3 -3
  41. package/dist/{usage-BVXi0iFY.mjs → usage-CxmpYYO4.mjs} +61 -107
  42. package/dist/{context-Y_I4Mofq.mjs → validate-BVrT43GO.mjs} +35 -108
  43. package/dist/{watch-DVNIciHp.mjs → watch-mkLm4t9D.mjs} +19 -5
  44. package/dist/{workflows-B30QkwTk.mjs → workflows-Bvl2Fquu.mjs} +2 -2
  45. package/dist/{worktrees-DD4_TpkH.mjs → worktrees-DOOCoS2w.mjs} +49 -20
  46. package/package.json +8 -4
  47. package/dist/ui/assets/app-D_nAPnHM.css +0 -1
  48. package/dist/ui/assets/app-DqBAtXSZ.js +0 -111
@@ -1,7 +1,6 @@
1
- import { $n as maskSecrets, A as isNewer, Cr as guardedBase, Mn as FEEDBACK_KINDS, Nn as FEEDBACK_SEVERITIES, Sr as estimateTokens, Tr as writePlainFile, Un as targetOf, Vn as fingerprint, j as parseVersion, k as compareVersions, mr as placeFor, pr as placeFiles, wr as plainPathInside, xr as LESSON_CAP_TOKENS, yr as inside, zn as publishPost } from "./src-tsuZRu0H.mjs";
1
+ import { Gr as writePlainFile, Hr as estimateTokens, Nr as placeFiles, Pr as placeFor, Ur as guardedBase, Vr as LESSON_CAP_TOKENS, Wr as plainPathInside, Xn as FEEDBACK_KINDS, Zn as FEEDBACK_SEVERITIES, cr as targetOf, or as fingerprint, rr as publishPost, vr as maskSecrets, zr as inside } from "./src-DN-RPsil.mjs";
2
2
  import path from "node:path";
3
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
4
- import { homedir } from "node:os";
3
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
5
4
  import { spawn } from "node:child_process";
6
5
  // ../core/src/feedback/actions.ts
7
6
  const ALLOWED = { ok: true };
@@ -363,174 +362,6 @@ const previewPost = (store, item, ask) => {
363
362
  })
364
363
  };
365
364
  };
366
- // ../core/src/remote/session.ts
367
- const chattable = (type) => type === "agent.leader" || type === "agent.worker";
368
- /** Reads `node`'s session from a run's events, and every node's last output. */
369
- const nodeSession = (events, node) => {
370
- const session = {
371
- known: false,
372
- chattable: false,
373
- running: false
374
- };
375
- const outputs = {};
376
- for (const { event } of events) {
377
- if (event.type === "run.started") {
378
- const declared = event.nodes.find((n) => n.id === node);
379
- if (declared !== void 0) {
380
- session.known = true;
381
- session.chattable = chattable(declared.type);
382
- }
383
- }
384
- if (event.type === "node.finished") outputs[event.id] = event.output;
385
- if (!("id" in event) || event.id !== node) continue;
386
- if (event.type === "node.started") {
387
- session.known = true;
388
- session.chattable = chattable(event.nodeType);
389
- session.running = true;
390
- }
391
- if (event.type === "node.finished") session.running = false;
392
- if (event.type === "worktree" && event.action === "created") session.checkout = {
393
- path: event.path,
394
- ...event.branch === void 0 ? {} : { branch: event.branch },
395
- own: true,
396
- owner: node
397
- };
398
- if (event.type !== "node.event") continue;
399
- const inner = event.event;
400
- if (inner.fork === true) continue;
401
- if (inner.type === "session.started") {
402
- session.sessionId = inner.sessionId;
403
- if (inner.cwd !== void 0) session.cwd = inner.cwd;
404
- if (inner.driver !== void 0) session.driver = inner.driver;
405
- if (inner.model !== "") session.model = inner.model;
406
- }
407
- if (inner.type === "session.finished" && inner.outcome.sessionId !== "") session.sessionId = inner.outcome.sessionId;
408
- }
409
- return {
410
- session,
411
- outputs
412
- };
413
- };
414
- /** The dist-tags every install follows, toward and after 1.0.0. */
415
- const CHANNEL_TAGS = [
416
- "latest",
417
- "alpha",
418
- "beta",
419
- "rc"
420
- ];
421
- /** The dist-tag of snapshots from `main`, followed only by a canary. */
422
- const CANARY_TAG = "canary";
423
- const updateCachePath = (home = homedir()) => path.join(home, ".ork", "update-check.json");
424
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
425
- const text = (record, key) => {
426
- const value = record[key];
427
- return typeof value === "string" ? value : void 0;
428
- };
429
- const notifiedOf = (value) => {
430
- if (!isRecord(value)) return void 0;
431
- const version = text(value, "version");
432
- const at = text(value, "at");
433
- return version === void 0 || at === void 0 ? void 0 : {
434
- version,
435
- at
436
- };
437
- };
438
- /** The cache, or `null` when there is none or it cannot be read. */
439
- const readUpdateCache = (file) => {
440
- let parsed;
441
- try {
442
- parsed = JSON.parse(readFileSync(file, "utf8"));
443
- } catch {
444
- return null;
445
- }
446
- if (!isRecord(parsed)) return null;
447
- const checkedAt = text(parsed, "checkedAt");
448
- if (checkedAt === void 0) return null;
449
- const latest = text(parsed, "latest");
450
- const notified = notifiedOf(parsed["notified"]);
451
- return {
452
- checkedAt,
453
- ...latest === void 0 ? {} : { latest },
454
- ...notified === void 0 ? {} : { notified }
455
- };
456
- };
457
- /** Writes the cache whole (a temporary file, then a rename). */
458
- const writeUpdateCache = (file, cache) => {
459
- mkdirSync(path.dirname(file), { recursive: true });
460
- const temporary = `${file}.${process.pid}.tmp`;
461
- writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}\n`);
462
- renameSync(temporary, file);
463
- };
464
- const age = (iso, now) => {
465
- const time = Date.parse(iso);
466
- return Number.isNaN(time) ? Infinity : now.getTime() - time;
467
- };
468
- /** Whether it is time to ask the registry again. */
469
- const checkDue = (cache, now) => cache === null || age(cache.checkedAt, now) >= 864e5;
470
- /** Whether `version` is a canary snapshot: its pre-release starts `canary`. */
471
- const isCanary = (version) => parseVersion(version)?.pre[0]?.startsWith(CANARY_TAG) === true;
472
- /**
473
- * The newest version, by semver precedence, among the dist-tags `current`
474
- * follows: the channel tags, and `canary` too when `current` is a canary. A tag
475
- * whose value doesn't parse is skipped; `null` when none does.
476
- */
477
- const newestTagged = (tags, current) => {
478
- return (current !== void 0 && isCanary(current) ? [...CHANNEL_TAGS, "canary"] : CHANNEL_TAGS).flatMap((tag) => {
479
- const value = tags[tag];
480
- const version = typeof value === "string" ? parseVersion(value) : null;
481
- return typeof value === "string" && version !== null ? [{
482
- value,
483
- version
484
- }] : [];
485
- }).reduce((best, candidate) => best === null || compareVersions(candidate.version, best.version) > 0 ? candidate : best, null)?.value ?? null;
486
- };
487
- /**
488
- * The newest version on the registry that the running version follows ({@link
489
- * newestTagged}), or `null` on any failure.
490
- */
491
- const fetchLatest = async (name, options = {}) => {
492
- const registry = (options.registry ?? "https://registry.npmjs.org").replace(/\/+$/u, "");
493
- const get = options.fetch ?? globalThis.fetch;
494
- try {
495
- const response = await get(`${registry}/-/package/${name}/dist-tags`, {
496
- headers: { accept: "application/json" },
497
- signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
498
- });
499
- if (!response.ok) return null;
500
- const tags = await response.json();
501
- return isRecord(tags) ? newestTagged(tags, options.current) : null;
502
- } catch {
503
- return null;
504
- }
505
- };
506
- /**
507
- * Asks the registry and records the answer. A failed check is recorded too, so
508
- * an offline machine is not asked again until the interval passes; the last
509
- * known version is kept. `options.current` is the running version.
510
- */
511
- const refreshUpdateCache = async (file, name, now, options = {}) => {
512
- const before = readUpdateCache(file);
513
- const latest = await fetchLatest(name, options) ?? before?.latest;
514
- const cache = {
515
- ...before,
516
- checkedAt: now.toISOString(),
517
- ...latest === void 0 ? {} : { latest }
518
- };
519
- writeUpdateCache(file, cache);
520
- return cache;
521
- };
522
- /** The newer version the cache knows of, if any. */
523
- const newerVersion = (cache, current) => cache?.latest !== void 0 && isNewer(cache.latest, current) ? cache.latest : null;
524
- /**
525
- * The version to tell the user about now: newer than theirs, and not already
526
- * told today. `null` when there is nothing to say.
527
- */
528
- const pendingNotice = (cache, current, now) => {
529
- const newer = newerVersion(cache, current);
530
- if (newer === null) return null;
531
- const told = cache?.notified;
532
- return told?.version === newer && age(told.at, now) < 864e5 ? null : newer;
533
- };
534
365
  /**
535
366
  * Runs the command; resolves its exit code. Without `onOutput` it writes on the
536
367
  * terminal.
@@ -558,59 +389,4 @@ const runInstall = (argv, onOutput) => new Promise((resolve) => {
558
389
  resolve(code ?? 1);
559
390
  });
560
391
  });
561
- // ../core/src/update/switches.ts
562
- /**
563
- * @file The environment's say over the update check (ADR-038): the variables
564
- * that turn it off (`ORK_NO_UPDATE_CHECK`, the common `NO_UPDATE_NOTIFIER`,
565
- * and `CI`), and the registry to ask instead of npm's. The CLI adds its own
566
- * rules about where a notice may go; the server behind `ork ui` uses only
567
- * these.
568
- */
569
- /** Turns the automatic check and its notice off. */
570
- const NO_UPDATE_ENV = "ORK_NO_UPDATE_CHECK";
571
- /** A registry to ask instead of npm's, e.g. a company mirror. */
572
- const UPDATE_REGISTRY_ENV = "ORK_UPDATE_REGISTRY";
573
- /** The variables that turn the check off, in the order they are looked at. */
574
- const UPDATE_SWITCHES = [
575
- NO_UPDATE_ENV,
576
- "NO_UPDATE_NOTIFIER",
577
- "CI"
578
- ];
579
- /** Set to anything but empty, `0` or `false`. */
580
- const isSwitchedOn = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
581
- /** The variable that turns the check off in `env`, or `null` when none does. */
582
- const updateSwitchOff = (env) => UPDATE_SWITCHES.find((name) => isSwitchedOn(env[name])) ?? null;
583
- /** The registry `ORK_UPDATE_REGISTRY` names, if it names one. */
584
- const registryOf = (env) => {
585
- const registry = env[UPDATE_REGISTRY_ENV];
586
- return registry === void 0 || registry === "" ? void 0 : registry;
587
- };
588
- /** The host of a registry that isn't an http(s) URL, past any `user:pass@`. */
589
- const hostOf = (registry) => registry.replace(/^[a-z][\w+.-]*:\/\//iu, "").replace(/^[^/?#]*@/u, "").split(/[/?#]/u)[0] ?? "";
590
- /**
591
- * The registry as it may be shown (on a page, in a message): an http(s) URL
592
- * without its user name and password, which a mirror's URL may carry; anything
593
- * else only as its host.
594
- */
595
- const registryShown = (registry) => {
596
- try {
597
- const url = new URL(registry);
598
- if (url.protocol === "http:" || url.protocol === "https:") {
599
- if (url.username === "" && url.password === "") return registry;
600
- url.username = "";
601
- url.password = "";
602
- return url.href;
603
- }
604
- } catch {}
605
- return hostOf(registry);
606
- };
607
- // src/__build_info__.ts
608
- /**
609
- * @file The published package's name and version, bound to
610
- * `packages/cli/package.json`. Do not edit by hand: `pnpm changesets:version`
611
- * and `pnpm build` rewrite it (`tools/release/build-info.ts`), and a test
612
- * fails when it is stale.
613
- */
614
- const ORK_NAME = "@krapht/ork";
615
- const ORK_VERSION = "1.0.0-alpha.14";
616
- export { feedbackActions as C, placeOf as S, previewPost as _, registryShown as a, acceptItem as b, checkDue as c, pendingNotice as d, readUpdateCache as f, nodeSession as g, writeUpdateCache as h, registryOf as i, fetchLatest as l, updateCachePath as m, ORK_VERSION as n, updateSwitchOff as o, refreshUpdateCache as p, isSwitchedOn as r, runInstall as s, ORK_NAME as t, newerVersion as u, checkNote as v, openFixRun as w, lessonPlaceOf as x, recordNote as y };
392
+ export { acceptItem as a, feedbackActions as c, recordNote as i, openFixRun as l, previewPost as n, lessonPlaceOf as o, checkNote as r, placeOf as s, runInstall as t };
@@ -1,4 +1,4 @@
1
- import { C as capsHere, Dt as limitsNow, Et as NOT_STARTED, Ot as limitsReport } from "./src-tsuZRu0H.mjs";
1
+ import { _ as capsHere, dt as NOT_STARTED, ft as limitsNow, pt as limitsReport } from "./src-DN-RPsil.mjs";
2
2
  // src/run/limits.ts
3
3
  /**
4
4
  * @file The limits across runs that apply here (RFC-007): the user's settings
@@ -1,8 +1,8 @@
1
- import { A as isNewer } from "./src-tsuZRu0H.mjs";
2
- import { n as ORK_VERSION } from "./__build_info__-B82-dGJY.mjs";
3
- import { p as runMarkerIn } from "./src-C7z_6fhn.mjs";
4
- import { n as explainNode } from "./context-Y_I4Mofq.mjs";
5
- import { a as openMcp, c as refused, o as tool, s as answer } from "./src-8Mul0695.mjs";
1
+ import { w as isNewer } from "./src-DN-RPsil.mjs";
2
+ import { n as ORK_VERSION } from "./__build_info__-DcRdK0Du.mjs";
3
+ import { p as runMarkerIn } from "./src-DHDQ0itC.mjs";
4
+ import { n as explainNode } from "./context-BN9A2Nnu.mjs";
5
+ import { a as openMcp, c as refused, o as tool, s as answer } from "./src-nXLIwDrt.mjs";
6
6
  import { z } from "zod";
7
7
  import { defineCommand } from "citty";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -43,7 +43,7 @@ const orkMcpServer = (options = {}) => {
43
43
  ...options.script === void 0 ? {} : { script: options.script }
44
44
  });
45
45
  const all = [...opened.tools, explainTool(opened, options.home)];
46
- const tools = options.expects !== void 0 && isNewer(options.expects, "1.0.0-alpha.14") ? tooOld(all, options.expects) : all;
46
+ const tools = options.expects !== void 0 && isNewer(options.expects, "1.0.0-alpha.16") ? tooOld(all, options.expects) : all;
47
47
  const server = new McpServer({
48
48
  name: "ork",
49
49
  version: ORK_VERSION
@@ -1,7 +1,7 @@
1
- import "./src-tsuZRu0H.mjs";
2
- import { t as runOnce } from "./run-D8divGCs.mjs";
1
+ import "./src-DN-RPsil.mjs";
2
+ import { t as runOnce } from "./run-D_u9V_Ve.mjs";
3
3
  import { c as write, l as writeErr, s as style } from "./output-B1kwo0YU.mjs";
4
- import { t as ago } from "./format-CvTE5CZQ.mjs";
4
+ import { t as ago } from "./format-hiDvtNGM.mjs";
5
5
  import { spawn } from "node:child_process";
6
6
  import { defineCommand, parseArgs } from "citty";
7
7
  // src/commands/once.ts
@@ -0,0 +1,58 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ // ../core/src/machine/orphan-guard.ts
3
+ /**
4
+ * @file The orphan guard: a node's processes end with `ork`, whatever ends
5
+ * `ork` (SIGKILL, the OOM killer, a crash exit). A child started in its own
6
+ * process group (`detached: true`) under `sh` also starts a small watcher in
7
+ * that group, which reads a pipe that only `ork` holds open. `ork` never
8
+ * writes to it, so when `ork` dies the read sees EOF, and the watcher sends
9
+ * its own group (`kill 0`, never another's) SIGTERM, then SIGKILL after
10
+ * {@link KILL_AFTER_MS}. A line on the pipe instead lets the watcher exit
11
+ * quietly, and so does killing it, which is how a node that ends normally
12
+ * leaves its leftover background processes alone. POSIX only. Two uses:
13
+ *
14
+ * - A shell that outlives the program (the tool runner's timing wrapper):
15
+ * `${orphanGuard(4)} w=$!; "$0" "$@" 4<&-; s=$?; kill -s KILL $w`, then
16
+ * spawn with a `"pipe"` at `stdio[4]` and call
17
+ * `holdGuard(child.stdio[4])`.
18
+ * - A shell that `exec`s the program (a session, so its pid stays the
19
+ * driver's): `${orphanGuard(3)} exec 3<&-; exec "$0" "$@"` (a bare `exec`
20
+ * closes the pipe: macOS's `sh` keeps a copy of one an `exec` command
21
+ * closes, which the driver would inherit), with a `"pipe"` at `stdio[3]`;
22
+ * nothing is left to kill the watcher, so call the function `holdGuard`
23
+ * returns once the driver has exited (on `exit`: the pipe holds `close`).
24
+ */
25
+ /** How long the group gets to end after SIGTERM before SIGKILL. */
26
+ const KILL_AFTER_MS = 2e3;
27
+ /** The file descriptors a redirect can name in every POSIX shell. */
28
+ const SHELL_FDS = [
29
+ 3,
30
+ 4,
31
+ 5,
32
+ 6,
33
+ 7,
34
+ 8,
35
+ 9
36
+ ];
37
+ /**
38
+ * Shell text that starts the watcher in the background on pipe `fd` (3 to 9);
39
+ * it ends with `&`, so `$!` after it is the watcher's pid. The watcher holds no
40
+ * other descriptor, so it never keeps the child's output pipes open.
41
+ */
42
+ const orphanGuard = (fd) => {
43
+ const others = SHELL_FDS.filter((other) => other !== fd).map((other) => `${String(other)}>&-`).join(" ");
44
+ return `{ read -r _ork <&${String(fd)} || { trap '' TERM; kill -s TERM 0; sleep ${String(KILL_AFTER_MS / 1e3)}; kill -s KILL 0; }; } </dev/null >/dev/null 2>&1 ${others} & `;
45
+ };
46
+ /**
47
+ * Keeps `ork`'s end of the watch pipe (`child.stdio[fd]`) open and drained, so
48
+ * it never holds back the child's `close`; returns the release, which writes
49
+ * the line that lets the watcher exit without touching its group.
50
+ */
51
+ const holdGuard = (pipe) => {
52
+ pipe?.on("error", () => {});
53
+ if (pipe instanceof Readable) pipe.resume();
54
+ return () => {
55
+ if (pipe instanceof Writable && pipe.writable) pipe.write("\n");
56
+ };
57
+ };
58
+ export { holdGuard as n, orphanGuard as r, KILL_AFTER_MS as t };
@@ -1,7 +1,7 @@
1
- import { U as openStore, _t as requestPause, gt as requestContinue } from "./src-tsuZRu0H.mjs";
2
- import { t as findRun } from "./find-BVX-bL4u.mjs";
1
+ import { En as requestPause, L as openStore, Tn as requestContinue } from "./src-DN-RPsil.mjs";
2
+ import { t as findRun } from "./find-mLYsR0uC.mjs";
3
3
  import { c as write, l as writeErr, s as style } from "./output-B1kwo0YU.mjs";
4
- import { i as continueRefusal } from "./in-a-run-cL5N9gpH.mjs";
4
+ import { i as continueRefusal } from "./in-a-run-a447hxq8.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  // src/commands/pause.ts
7
7
  /**
@@ -0,0 +1,46 @@
1
+ import { ir as redact } from "./src-DN-RPsil.mjs";
2
+ import { b as githubDraftRefusal } from "./__build_info__-DcRdK0Du.mjs";
3
+ // ../core/src/follow-ups/post.ts
4
+ /** Posts `f`'s draft as a GitHub comment once `confirmed`, and marks it done. */
5
+ const postFollowUpMessage = async (f, options) => {
6
+ const draft = f.draft;
7
+ const refused = githubDraftRefusal(f);
8
+ if (refused !== void 0 || draft === void 0) return {
9
+ ok: false,
10
+ error: refused?.error ?? "it has no draft"
11
+ };
12
+ if (!options.confirmed) return {
13
+ ok: false,
14
+ error: "posting to GitHub needs the maintainer's confirmation"
15
+ };
16
+ const body = redact(draft.text, {
17
+ roots: [f.project],
18
+ home: options.home
19
+ });
20
+ const on = /\/pull\/\d+/u.test(draft.to) ? "pr" : "issue";
21
+ const result = await options.gh([
22
+ on,
23
+ "comment",
24
+ draft.to,
25
+ "--body",
26
+ body
27
+ ], { cwd: f.project });
28
+ if (!result.ok) return {
29
+ ok: false,
30
+ error: `gh failed: ${result.stderr.trim() || result.stdout.trim() || "no output"}`
31
+ };
32
+ const url = result.stdout.trim().split("\n").findLast((line) => line.startsWith("https://"));
33
+ const done = {
34
+ ...f,
35
+ status: "done",
36
+ note: `posted${url === void 0 ? "" : ` as ${url}`}`,
37
+ updatedAt: options.now?.() ?? (/* @__PURE__ */ new Date()).toISOString()
38
+ };
39
+ options.rows.putFollowUp(done);
40
+ return {
41
+ ok: true,
42
+ followUp: done,
43
+ ...url === void 0 ? {} : { url }
44
+ };
45
+ };
46
+ export { postFollowUpMessage as t };
@@ -0,0 +1,19 @@
1
+ import { a as refuseIdsWithJson } from "./flags-CdgIH1L6.mjs";
2
+ import { c as write } from "./output-B1kwo0YU.mjs";
3
+ // src/list/print.ts
4
+ /**
5
+ * Prints `rows`: `--ids` their ids (`table.id`) comma-separated; `--json` the
6
+ * rows (or `json(rows)`); else `human(rows)`, which also says what an empty
7
+ * list is. `--ids` with `--json` is refused (said, exit code set).
8
+ */
9
+ const printList = (rows, table, args, human, json = (shown) => shown) => {
10
+ if (refuseIdsWithJson(args)) return;
11
+ if (args.ids === true) {
12
+ const id = table.id;
13
+ if (id !== void 0 && rows.length > 0) write(rows.map((row) => id(row)).join(","));
14
+ return;
15
+ }
16
+ if (args.json === true) write(JSON.stringify(json(rows), void 0, 2));
17
+ else human(rows);
18
+ };
19
+ export { printList as t };
@@ -0,0 +1,230 @@
1
+ import { Ar as hasInRepoFiles, Br as projectRootOf, Fr as projectsHome, L as openStore, Lr as shownPath, Mr as newProjectId, Nr as placeFiles, jr as inRepoPlace } from "./src-DN-RPsil.mjs";
2
+ import { C as termOf, c as write, l as writeErr, s as style, w as cut } from "./output-B1kwo0YU.mjs";
3
+ import { n as chip } from "./chips-BqcT58qr.mjs";
4
+ import path from "node:path";
5
+ import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
6
+ import { defineCommand } from "citty";
7
+ // src/project/register.ts
8
+ /**
9
+ * @file Registering projects (ADR-035): `ork register` attaches a repository
10
+ * from outside it, with its workflows, rules and runs in
11
+ * `~/.ork/projects/<id>/` and nothing written into the repository; `ork init`
12
+ * records an in-repo project, whose files are its `.ork/`.
13
+ */
14
+ const ID = /^[a-z0-9][a-z0-9._-]*$/u;
15
+ /** The git root of `target`'s main clone, or why there is none. */
16
+ const cloneOf = (target) => {
17
+ if (!existsSync(target)) return {
18
+ ok: false,
19
+ error: `${target} does not exist`
20
+ };
21
+ const root = projectRootOf(realpathSync(target));
22
+ const dotGit = path.join(root, ".git");
23
+ if (!existsSync(dotGit)) return {
24
+ ok: false,
25
+ error: `${target} is not in a git repository`
26
+ };
27
+ if (statSync(dotGit).isFile()) return {
28
+ ok: false,
29
+ error: `${root} is a git worktree; register the main clone instead`
30
+ };
31
+ return {
32
+ ok: true,
33
+ root
34
+ };
35
+ };
36
+ /**
37
+ * Attaches the repository at `target`: creates `~/.ork/projects/<id>/workflows`
38
+ * and records the project. Registering it again returns the same project.
39
+ */
40
+ const registerProject = (store, target, options = {}) => {
41
+ const clone = cloneOf(target);
42
+ if (!clone.ok) return clone;
43
+ const { root } = clone;
44
+ const projects = store.projects();
45
+ const existing = projects.find((p) => p.root === root);
46
+ if (existing?.mode === "attached") return {
47
+ ok: true,
48
+ project: existing,
49
+ created: false
50
+ };
51
+ if (existing !== void 0 || hasInRepoFiles(root)) return {
52
+ ok: false,
53
+ error: `${root} keeps Ork's files in its own .ork/ (in-repo), so it is not attached`
54
+ };
55
+ const taken = projects.map((p) => p.id);
56
+ if (options.id !== void 0) {
57
+ if (!ID.test(options.id)) return {
58
+ ok: false,
59
+ error: `--id \`${options.id}\` must be lowercase letters, digits, ".", "_" or "-"`
60
+ };
61
+ if (taken.includes(options.id)) return {
62
+ ok: false,
63
+ error: `a project is already registered as ${options.id}`
64
+ };
65
+ }
66
+ const id = options.id ?? newProjectId(root, taken);
67
+ const project = {
68
+ id,
69
+ root,
70
+ mode: "attached",
71
+ dir: path.join(projectsHome(options.home), id)
72
+ };
73
+ mkdirSync(placeFiles(project).workflows, { recursive: true });
74
+ return {
75
+ ok: true,
76
+ project: store.addProject(project),
77
+ created: true
78
+ };
79
+ };
80
+ /**
81
+ * Records the in-repo project at `cwd`'s git root, if it is not registered yet;
82
+ * an attached one stays attached (`ork init` must not take it over).
83
+ */
84
+ const recordInRepo = (store, cwd) => {
85
+ const root = projectRootOf(realpathSync(cwd));
86
+ const projects = store.projects();
87
+ const existing = projects.find((p) => p.root === root);
88
+ if (existing?.mode === "attached") return {
89
+ ok: false,
90
+ error: `${root} is attached: its workflows are in ${shownPath(existing.dir, root)}, and Ork writes nothing into the repository`
91
+ };
92
+ if (existing !== void 0) return {
93
+ ok: true,
94
+ project: existing,
95
+ created: false
96
+ };
97
+ const project = {
98
+ ...inRepoPlace(root),
99
+ id: newProjectId(root, projects.map((p) => p.id))
100
+ };
101
+ return {
102
+ ok: true,
103
+ project: store.addProject(project),
104
+ created: true
105
+ };
106
+ };
107
+ /**
108
+ * Points a registered project at its clone's new place, after the clone was
109
+ * moved or renamed; its runs follow, and an in-repo project's `.ork/` with it.
110
+ */
111
+ const relocateProject = (store, id, target) => {
112
+ const projects = store.projects();
113
+ const project = projects.find((p) => p.id === id);
114
+ if (project === void 0) return {
115
+ ok: false,
116
+ error: `No project is registered as ${id}.`
117
+ };
118
+ const clone = cloneOf(target);
119
+ if (!clone.ok) return clone;
120
+ const { root } = clone;
121
+ const other = projects.find((p) => p.root === root && p.id !== id);
122
+ if (other !== void 0) return {
123
+ ok: false,
124
+ error: `${root} is already registered as ${other.id}`
125
+ };
126
+ const dir = project.mode === "in-repo" ? inRepoPlace(root).dir : project.dir;
127
+ store.moveProject(id, {
128
+ root,
129
+ mode: project.mode,
130
+ dir
131
+ });
132
+ return {
133
+ ok: true,
134
+ project: {
135
+ ...project,
136
+ root,
137
+ dir
138
+ },
139
+ created: false
140
+ };
141
+ };
142
+ /**
143
+ * One project's lines for `ork projects`; rich, its mode and a missing clone
144
+ * are chips, its id is padded to `idWidth` and its root is cut to the screen.
145
+ */
146
+ const formatProject = (project, term = termOf(process.stdout), idWidth = 0) => {
147
+ const missing = !existsSync(project.root);
148
+ if (!term.rich) {
149
+ const gone = missing ? style("yellow", " (missing)") : "";
150
+ return [`${style("bold", project.id)} ${style("dim", project.mode)} ${project.root}${gone}`, style("dim", ` files: ${shownPath(project.dir, project.root)}`)];
151
+ }
152
+ const mode = chip(term, project.mode.toUpperCase(), "muted");
153
+ const gone = missing ? ` ${chip(term, "MISSING", "warn")}` : "";
154
+ const room = term.width - (idWidth + 2 + project.mode.length + 2 + 2) - (missing ? 10 : 0);
155
+ return [`${style("bold", project.id.padEnd(idWidth))} ${mode} ${cut(project.root, Math.max(8, room))}${gone}`, style("dim", cut(` files: ${shownPath(project.dir, project.root)}`, term.width))];
156
+ };
157
+ /** Every project's lines, their ids aligned when rich. */
158
+ const formatProjects = (projects, term = termOf(process.stdout)) => {
159
+ const idWidth = term.rich ? Math.max(0, ...projects.map((p) => p.id.length)) : 0;
160
+ return projects.flatMap((p) => formatProject(p, term, idWidth));
161
+ };
162
+ /** What `ork register` prints: the project, and where its files go. */
163
+ const formatRegistered = (project, created) => {
164
+ const files = placeFiles(project);
165
+ const shown = (file) => shownPath(file, project.root);
166
+ return [
167
+ `${style("green", created ? "registered" : "already registered")} ${project.id} → ${project.root}`,
168
+ `\nWorkflows: ${shown(files.workflows)}/ · rules: ${shown(files.rules)} · runs: ${shown(files.runs)}/`,
169
+ `Nothing is written into ${project.root}. In it, ${style("bold", "ork run <name>")} finds the workflows above.`
170
+ ];
171
+ };
172
+ // src/commands/project-records.ts
173
+ /**
174
+ * @file `ork projects`' subcommands on one project's record (ADR-035): `forget`
175
+ * drops it and keeps its files and runs; `move` points it at its clone's new
176
+ * place after the clone was moved or renamed, and its runs follow.
177
+ */
178
+ /** Runs `use` with the store open, and closes it. */
179
+ const withStore = (use) => {
180
+ const store = openStore();
181
+ try {
182
+ return use(store);
183
+ } finally {
184
+ store.close();
185
+ }
186
+ };
187
+ const id = {
188
+ type: "positional",
189
+ description: "The project's id",
190
+ required: true
191
+ };
192
+ const forget = defineCommand({
193
+ meta: {
194
+ name: "forget",
195
+ description: "Forget a registered project; its files and runs are kept"
196
+ },
197
+ args: { id },
198
+ run: ({ args }) => {
199
+ if (withStore((store) => store.forgetProject(args.id))) {
200
+ write(`${style("green", "forgot")} ${args.id} (its files are kept)`);
201
+ return;
202
+ }
203
+ writeErr(`No project is registered as ${args.id}.`);
204
+ process.exitCode = 1;
205
+ }
206
+ });
207
+ const move = defineCommand({
208
+ meta: {
209
+ name: "move",
210
+ description: "Point a registered project at its clone's new place, after moving or renaming it"
211
+ },
212
+ args: {
213
+ id,
214
+ path: {
215
+ type: "positional",
216
+ description: "Where the clone is now",
217
+ required: true
218
+ }
219
+ },
220
+ run: ({ args }) => {
221
+ const result = withStore((store) => relocateProject(store, args.id, args.path));
222
+ if (result.ok) {
223
+ write(`${style("green", "moved")} ${args.id} → ${result.project.root}`);
224
+ return;
225
+ }
226
+ writeErr(result.error);
227
+ process.exitCode = 1;
228
+ }
229
+ });
230
+ export { formatRegistered as a, formatProjects as i, move as n, recordInRepo as o, withStore as r, registerProject as s, forget as t };
@@ -1,4 +1,4 @@
1
- import { U as openStore, ft as addressOf, ht as unlinkNodes, mt as linkNodes } from "./src-tsuZRu0H.mjs";
1
+ import { Cn as linkNodes, L as openStore, wn as unlinkNodes, xn as addressOf } from "./src-DN-RPsil.mjs";
2
2
  import { c as write, l as writeErr, s as style } from "./output-B1kwo0YU.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  // src/commands/relay.ts