@edspencer/paddock 0.80.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/app.d.ts +7 -0
  3. package/packages/server/dist/app.js +1 -1
  4. package/packages/server/dist/auth.d.ts +3 -2
  5. package/packages/server/dist/auth.js +11 -0
  6. package/packages/server/dist/background-live.d.ts +17 -0
  7. package/packages/server/dist/background-live.js +24 -0
  8. package/packages/server/dist/busy.d.ts +96 -0
  9. package/packages/server/dist/busy.js +88 -0
  10. package/packages/server/dist/cli/args.d.ts +70 -29
  11. package/packages/server/dist/cli/args.js +306 -7
  12. package/packages/server/dist/cli/busy-command.d.ts +58 -0
  13. package/packages/server/dist/cli/busy-command.js +108 -0
  14. package/packages/server/dist/cli/busy.d.ts +69 -0
  15. package/packages/server/dist/cli/busy.js +150 -0
  16. package/packages/server/dist/cli/output.d.ts +52 -0
  17. package/packages/server/dist/cli/output.js +84 -0
  18. package/packages/server/dist/cli/paddock.js +65 -21
  19. package/packages/server/dist/cli/service/index.d.ts +64 -0
  20. package/packages/server/dist/cli/service/index.js +208 -7
  21. package/packages/server/dist/cli/service/launchd.d.ts +15 -1
  22. package/packages/server/dist/cli/service/launchd.js +116 -18
  23. package/packages/server/dist/cli/service/ready.d.ts +30 -0
  24. package/packages/server/dist/cli/service/ready.js +35 -0
  25. package/packages/server/dist/cli/service/systemd.js +27 -0
  26. package/packages/server/dist/cli/update/install-method.d.ts +131 -0
  27. package/packages/server/dist/cli/update/install-method.js +258 -0
  28. package/packages/server/dist/cli/update/lock.d.ts +40 -0
  29. package/packages/server/dist/cli/update/lock.js +108 -0
  30. package/packages/server/dist/cli/update/package-manager.d.ts +109 -0
  31. package/packages/server/dist/cli/update/package-manager.js +221 -0
  32. package/packages/server/dist/cli/update/registry.d.ts +89 -0
  33. package/packages/server/dist/cli/update/registry.js +145 -0
  34. package/packages/server/dist/cli/update/semver.d.ts +68 -0
  35. package/packages/server/dist/cli/update/semver.js +232 -0
  36. package/packages/server/dist/cli/update/update-check.d.ts +80 -0
  37. package/packages/server/dist/cli/update/update-check.js +116 -0
  38. package/packages/server/dist/cli/update/update.d.ts +160 -0
  39. package/packages/server/dist/cli/update/update.js +609 -0
  40. package/packages/server/dist/cli/update-command.d.ts +20 -0
  41. package/packages/server/dist/cli/update-command.js +113 -0
  42. package/packages/server/dist/herdctl.d.ts +19 -0
  43. package/packages/server/dist/herdctl.js +26 -0
  44. package/packages/server/dist/routes/meta.js +45 -0
  45. package/packages/server/dist/runtime-file.d.ts +26 -0
  46. package/packages/server/dist/runtime-file.js +91 -0
  47. package/packages/server/dist/start.d.ts +33 -0
  48. package/packages/server/dist/start.js +115 -8
  49. package/packages/server/dist/ws-context.d.ts +9 -0
  50. package/packages/server/dist/ws-turn.js +1 -0
  51. package/packages/server/package.json +1 -1
  52. package/packages/web/dist/assets/{DeleteProjectDialog-DHczLhwQ.js → DeleteProjectDialog-CphdGB3R.js} +1 -1
  53. package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +2 -0
  54. package/packages/web/dist/assets/{DiscoverView-DtTOpTji.js → DiscoverView-B_kcF4sm.js} +1 -1
  55. package/packages/web/dist/assets/{InstanceConfigPage-BRoBh1C1.js → InstanceConfigPage-CvUK5DKW.js} +1 -1
  56. package/packages/web/dist/assets/{MarkdownRenderer-DJf8IJuk.js → MarkdownRenderer-DvZ-j_Ao.js} +1 -1
  57. package/packages/web/dist/assets/{ProjectRedirect-EioguUKW.js → ProjectRedirect-DayHvUH2.js} +1 -1
  58. package/packages/web/dist/assets/{ProjectView-BiKnGfk6.js → ProjectView-CCw83gUi.js} +3 -3
  59. package/packages/web/dist/assets/{ProjectsGrid-D6kfaIal.js → ProjectsGrid-B3Klg2H4.js} +1 -1
  60. package/packages/web/dist/assets/{RootHome-DpcyuV6e.js → RootHome-BxcDvpgI.js} +1 -1
  61. package/packages/web/dist/assets/{Surfaces-CHM29PhP.js → Surfaces-7UlhFBSG.js} +1 -1
  62. package/packages/web/dist/assets/{arc-fiqD05Hu.js → arc-DNkh5o5Q.js} +1 -1
  63. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BuuB2Nkf.js → architectureDiagram-3BPJPVTR-BrgN3oLF.js} +1 -1
  64. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DQMBuzfi.js → blockDiagram-GPEHLZMM-D05XOJSk.js} +1 -1
  65. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-BKN0UxCH.js → c4Diagram-AAUBKEIU-CKaMBhMP.js} +1 -1
  66. package/packages/web/dist/assets/channel-JrEedxB8.js +2 -0
  67. package/packages/web/dist/assets/{chunk-2J33WTMH-DW37EupX.js → chunk-2J33WTMH-8hgadCQc.js} +1 -1
  68. package/packages/web/dist/assets/{chunk-4BX2VUAB-Dyv7zV_c.js → chunk-4BX2VUAB-CBfEC-pu.js} +1 -1
  69. package/packages/web/dist/assets/{chunk-55IACEB6-DzMR0dOk.js → chunk-55IACEB6-DV1eMgHk.js} +1 -1
  70. package/packages/web/dist/assets/{chunk-727SXJPM-BdZ3Sor8.js → chunk-727SXJPM-Cfgl5fxY.js} +1 -1
  71. package/packages/web/dist/assets/{chunk-AQP2D5EJ-B4i7_og0.js → chunk-AQP2D5EJ-BXhqD295.js} +1 -1
  72. package/packages/web/dist/assets/{chunk-FMBD7UC4-jcTr85it.js → chunk-FMBD7UC4-TFGOPtek.js} +1 -1
  73. package/packages/web/dist/assets/{chunk-ND2GUHAM-fgMsadqB.js → chunk-ND2GUHAM-CHFM665R.js} +1 -1
  74. package/packages/web/dist/assets/{chunk-QZHKN3VN-bJNHYS1W.js → chunk-QZHKN3VN-CqEf6MMT.js} +1 -1
  75. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +2 -0
  76. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +2 -0
  77. package/packages/web/dist/assets/{core-Bk4KFrmN.js → core-DmEaKxBb.js} +1 -1
  78. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BIqCSlQy.js → cose-bilkent-S5V4N54A-BnGI6jkr.js} +1 -1
  79. package/packages/web/dist/assets/{dagre-BM42HDAG-6nyhtVQz.js → dagre-BM42HDAG-CKoPx61E.js} +1 -1
  80. package/packages/web/dist/assets/{diagram-2AECGRRQ-CjizfipR.js → diagram-2AECGRRQ-mMmtlBl1.js} +1 -1
  81. package/packages/web/dist/assets/{diagram-5GNKFQAL-DlgB5vWb.js → diagram-5GNKFQAL-Cdz9Mtnt.js} +1 -1
  82. package/packages/web/dist/assets/{diagram-KO2AKTUF-CNC9qanV.js → diagram-KO2AKTUF-DQxM3wYj.js} +1 -1
  83. package/packages/web/dist/assets/{diagram-LMA3HP47-DSJ-s9lh.js → diagram-LMA3HP47-BWrinPsq.js} +1 -1
  84. package/packages/web/dist/assets/{diagram-OG6HWLK6-C9FoHBDg.js → diagram-OG6HWLK6-U6MYUve2.js} +1 -1
  85. package/packages/web/dist/assets/{documentTitle-Cj_AY97j.js → documentTitle-DGjRXuTP.js} +1 -1
  86. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-z6gku4Yt.js → erDiagram-TEJ5UH35-xqMCsXxY.js} +1 -1
  87. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Dl5c_FgU.js → flowDiagram-I6XJVG4X-PMfy_czF.js} +1 -1
  88. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CIscJ-Dt.js → ganttDiagram-6RSMTGT7-whhYYE4N.js} +1 -1
  89. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CvttOXM_.js → gitGraphDiagram-PVQCEYII-C51JW4tf.js} +1 -1
  90. package/packages/web/dist/assets/{index-DN82do6Z.js → index-CHOtvlnO.js} +2 -2
  91. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-dzWYdDJj.js → infoDiagram-5YYISTIA-SH8cr905.js} +1 -1
  92. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-8yX9k4bo.js → ishikawaDiagram-YF4QCWOH-BjqaoU3U.js} +1 -1
  93. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BmitzJDR.js → journeyDiagram-JHISSGLW-CBmdEVrd.js} +1 -1
  94. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BZmIkfkW.js → kanban-definition-UN3LZRKU-U8njjMLZ.js} +1 -1
  95. package/packages/web/dist/assets/{linear-BBLXE4o7.js → linear-DF66GPs1.js} +1 -1
  96. package/packages/web/dist/assets/{mermaid.core-mSKff4yu.js → mermaid.core-CouJz7Ph.js} +4 -4
  97. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BWAUa4CP.js → mindmap-definition-RKZ34NQL-BYIf0oFt.js} +1 -1
  98. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-VmVdiL4C.js → pieDiagram-4H26LBE5-Cm8rgg09.js} +1 -1
  99. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Dybfv6NU.js → quadrantDiagram-W4KKPZXB-C3Il5kOH.js} +1 -1
  100. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-BPnOvB7w.js → requirementDiagram-4Y6WPE33-D3PE2nTe.js} +1 -1
  101. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-YXIEmKw7.js → sankeyDiagram-5OEKKPKP-DdmvBM18.js} +1 -1
  102. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DintIRUe.js → sequenceDiagram-3UESZ5HK-B8M_GAIq.js} +1 -1
  103. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-eMsjahWp.js → stateDiagram-AJRCARHV-DZkXnK1_.js} +1 -1
  104. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +2 -0
  105. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-as4zRCPn.js → timeline-definition-PNZ67QCA-IHXq7rqi.js} +1 -1
  106. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CPzvUkM0.js → vennDiagram-CIIHVFJN-Dga7tLtT.js} +1 -1
  107. package/packages/web/dist/assets/{wardley-L42UT6IY-q6moXj5g.js → wardley-L42UT6IY-CmcwnEJ-.js} +1 -1
  108. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BsEEb9Gm.js → wardleyDiagram-YWT4CUSO-zHuSLrD4.js} +1 -1
  109. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DSgZlcZW.js → xychartDiagram-2RQKCTM6-D93MU_ic.js} +1 -1
  110. package/packages/web/dist/index.html +1 -1
  111. package/packages/web/dist/sw.js +1 -1
  112. package/packages/web/dist/assets/DiscoverPage-BSGSzTEs.js +0 -2
  113. package/packages/web/dist/assets/channel-B7yh6Su_.js +0 -2
  114. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DIlhMPgi.js +0 -2
  115. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DIlhMPgi.js +0 -2
  116. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CpAOGzbO.js +0 -2
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Paddock's bindings for the generic updater (`cli/update/`): the package name,
3
+ * the service hooks, the busy guard and the health probe
4
+ * (CLI contract SPEC §1, §5, §6).
5
+ *
6
+ * Kept separate from `cli/update/*` for the same reason `busy-command.ts` is
7
+ * kept separate from `cli/busy.ts`: those files are the ones wave C4 lifts into
8
+ * `reference/`, and everything Paddock-specific lives on this side of the seam.
9
+ * Four things are genuinely Paddock's own, and they are the four hooks:
10
+ *
11
+ * - **the package name and layout.** `@edspencer/paddock` is SYNTHESIZED from
12
+ * two private workspaces (`scripts/make-npm-package.mjs`), so the published
13
+ * tarball's root manifest carries that name and the CLI sits at
14
+ * `packages/server/dist/cli/paddock.js` under it. In the repo, nothing is
15
+ * called `@edspencer/paddock` at all — which is exactly why §6's walk-up
16
+ * reports a checkout there, correctly.
17
+ * - **the busy check**, including the keeper-chat trap: a Paddock chat that
18
+ * runs `paddock update` is itself one of the turns the answer counts.
19
+ * - **the service**, through `cli/service/index.ts`, so the unit is read by the
20
+ * code that writes it.
21
+ * - **the health probe**, which waits for the version rather than for a 200.
22
+ */
23
+ import path from "node:path";
24
+ import { createOutput } from "./output.js";
25
+ import { EXIT } from "./exit-codes.js";
26
+ import { UPDATE_SYNOPSIS } from "./args.js";
27
+ import { guardBusy } from "./busy.js";
28
+ import { PADDOCK_NOUNS, paddockBusyHint } from "./busy-command.js";
29
+ import { PADDOCK_NAME, PADDOCK_VERSION } from "../version.js";
30
+ import { updateServiceHooks } from "./service/index.js";
31
+ import { waitForVersion } from "./service/ready.js";
32
+ import { runUpdate } from "./update/update.js";
33
+ /** The published package. Used for the registry, the install detection and the install command. */
34
+ export const PADDOCK_PACKAGE = "@edspencer/paddock";
35
+ /** Where the CLI sits inside that package. See the layout note in make-npm-package.mjs. */
36
+ export const PADDOCK_BIN_SUBPATH = path.join("packages", "server", "dist", "cli", "paddock.js");
37
+ /** The image an `update` inside a container tells you to pull (SPEC §6, §9). */
38
+ export const PADDOCK_IMAGE = "ghcr.io/edspencer/paddock";
39
+ /** Release notes for a version — the `Changes:` line of SPEC §3's notice. */
40
+ export function paddockChangelogUrl(version) {
41
+ return `https://github.com/edspencer/paddock/releases/tag/v${version}`;
42
+ }
43
+ /**
44
+ * `paddock update [--check] [--to <version|tag>] [--yes] [--force] [--json]`
45
+ * (SPEC §1, §5). `upgrade` reaches the same place.
46
+ *
47
+ * `ctx` is the same {@link ServiceContext} `paddock service` is handed, so the
48
+ * two agree about which Node, which script and which unit — an `update` that
49
+ * refreshed a different unit from the one `status` reports would be the worst
50
+ * possible kind of nearly-working.
51
+ */
52
+ export async function runUpdateCommand(opts, ctx) {
53
+ const out = createOutput({ tool: "paddock", json: opts.json === true });
54
+ // The service layer decides the home, because the home has to be the one
55
+ // `service status` reads the update cache from: the UNIT's data dir when
56
+ // there is a unit, and this invocation's otherwise (#824's rule).
57
+ let service;
58
+ try {
59
+ service = updateServiceHooks(ctx);
60
+ }
61
+ catch (err) {
62
+ // An unsupported platform — `backendFor` throws for anything but darwin and
63
+ // linux. `update` still has work it could do there in principle, but it
64
+ // cannot refresh a unit, and pretending otherwise is how a Windows user
65
+ // gets a half-finished update. Report it as the failure it is.
66
+ return out.error(err instanceof Error ? err.message : String(err));
67
+ }
68
+ const product = {
69
+ name: PADDOCK_NAME,
70
+ tool: "paddock",
71
+ packageName: PADDOCK_PACKAGE,
72
+ envPrefix: "PADDOCK",
73
+ home: service.home,
74
+ currentVersion: PADDOCK_VERSION,
75
+ // NOT realpath'd: `findCli` re-resolves this symlink after the install,
76
+ // which is how it finds the new CLI under a package manager that puts the
77
+ // version in the package's real path. SPEC §6's walk-up realpaths it
78
+ // itself.
79
+ entry: process.argv[1] ?? ctx.scriptPath,
80
+ binSubpath: PADDOCK_BIN_SUBPATH,
81
+ image: PADDOCK_IMAGE,
82
+ usage: UPDATE_SYNOPSIS,
83
+ changelogUrl: paddockChangelogUrl,
84
+ guardBusy: async ({ force, home }) => {
85
+ const refusal = await guardBusy({
86
+ home,
87
+ out,
88
+ name: PADDOCK_NAME,
89
+ nouns: PADDOCK_NOUNS,
90
+ hint: paddockBusyHint,
91
+ ...(force ? { force: true } : {}),
92
+ });
93
+ if (refusal === null)
94
+ return null;
95
+ // The way past it, spelled out — the same shape `service stop` prints.
96
+ out.warn("\n paddock update --force update it anyway\n");
97
+ return EXIT.busy;
98
+ },
99
+ service: service.hooks,
100
+ waitForVersion: async ({ url, version, timeoutMs }) => waitForVersion({ url, version, timeoutMs }),
101
+ };
102
+ // `CliOptions` spells `--check` as `checkOnly`, because `check` reads as a
103
+ // verb in a bag of flags that also carries `write` and `follow`. Mapped here
104
+ // rather than renamed in the generic module, which is the one the other five
105
+ // tools copy.
106
+ return runUpdate(product, {
107
+ ...(opts.checkOnly === true ? { check: true } : {}),
108
+ ...(opts.to !== undefined ? { to: opts.to } : {}),
109
+ ...(opts.yes === true ? { yes: true } : {}),
110
+ ...(opts.force === true ? { force: true } : {}),
111
+ ...(opts.json === true ? { json: true } : {}),
112
+ }, { out });
113
+ }
@@ -371,6 +371,25 @@ export declare class HerdctlService {
371
371
  * error, returns `false` — the pre-#397 behaviour.
372
372
  */
373
373
  isSdkSessionLive(sessionId: string): boolean;
374
+ /**
375
+ * How many scheduler jobs are running right now, read from memory.
376
+ *
377
+ * This exists for `GET /api/busy` (CLI contract SPEC §4), which must read
378
+ * in-memory state and never walk the disk. The number the contract's ROADMAP
379
+ * names is `fleetStatus().counts.runningJobs` — but {@link fleetStatus} is
380
+ * built from `getAgentInfo()`, which re-reads the fleet state FILE on every
381
+ * call, and that re-parse has been this server's single largest CPU cost.
382
+ *
383
+ * The two are the same number. `counts.runningJobs` is
384
+ * `Σ scheduler.getRunningJobCount(agent.qualifiedName)` over the configured
385
+ * agents, and `getTotalRunningJobCount()` is the size of the same scheduler
386
+ * map keyed `<agent>/<schedule>` — one entry per running agent+schedule pair
387
+ * either way. So this reads the authority directly instead of a derived
388
+ * snapshot of it.
389
+ *
390
+ * Null-safe like {@link isSdkSessionLive}: no fleet or no scheduler means 0.
391
+ */
392
+ runningJobCount(): number;
374
393
  fleetStatus(): Promise<FleetStatus>;
375
394
  agents(): Promise<AgentInfo[]>;
376
395
  /**
@@ -528,6 +528,32 @@ export class HerdctlService {
528
528
  return false;
529
529
  }
530
530
  }
531
+ /**
532
+ * How many scheduler jobs are running right now, read from memory.
533
+ *
534
+ * This exists for `GET /api/busy` (CLI contract SPEC §4), which must read
535
+ * in-memory state and never walk the disk. The number the contract's ROADMAP
536
+ * names is `fleetStatus().counts.runningJobs` — but {@link fleetStatus} is
537
+ * built from `getAgentInfo()`, which re-reads the fleet state FILE on every
538
+ * call, and that re-parse has been this server's single largest CPU cost.
539
+ *
540
+ * The two are the same number. `counts.runningJobs` is
541
+ * `Σ scheduler.getRunningJobCount(agent.qualifiedName)` over the configured
542
+ * agents, and `getTotalRunningJobCount()` is the size of the same scheduler
543
+ * map keyed `<agent>/<schedule>` — one entry per running agent+schedule pair
544
+ * either way. So this reads the authority directly instead of a derived
545
+ * snapshot of it.
546
+ *
547
+ * Null-safe like {@link isSdkSessionLive}: no fleet or no scheduler means 0.
548
+ */
549
+ runningJobCount() {
550
+ try {
551
+ return this.fleet?.getScheduler()?.getTotalRunningJobCount() ?? 0;
552
+ }
553
+ catch {
554
+ return 0;
555
+ }
556
+ }
531
557
  async fleetStatus() {
532
558
  return this.manager.getFleetStatus();
533
559
  }
@@ -6,8 +6,14 @@ import { resolveModels, resolveDefaultModel } from "../models.js";
6
6
  import { sendProjectError } from "../route-errors.js";
7
7
  import { cspFor, parseRangeHeader } from "../http-bytes.js";
8
8
  import { PADDOCK_VERSION } from "../version.js";
9
+ import { collectBusy } from "../busy.js";
9
10
  export function registerMetaRoutes(app, ctx) {
10
11
  const { herdctl, transcriber, attachments, cfg } = ctx;
12
+ // Both optional for the same reason `turn-interlock.ts` takes an optional hub:
13
+ // a server built without the WS layer (several integration tests) has neither,
14
+ // and the honest answer for it is zero rather than a crash.
15
+ const hub = ctx.managementOpsContext?.hub;
16
+ const background = ctx.managementOpsContext?.background;
11
17
  // --- voice dictation (#voice): capability probe + transcription -------
12
18
  // The composer polls this to decide whether to show a mic button. `available`
13
19
  // is false on instances with dictation off (or a misconfigured remote).
@@ -128,6 +134,45 @@ export function registerMetaRoutes(app, ctx) {
128
134
  },
129
135
  },
130
136
  }, async () => ({ ok: true, version: PADDOCK_VERSION }));
137
+ // The contract's second server endpoint (SPEC §4, §8): is this instance busy?
138
+ //
139
+ // **Counts only, and no auth, and those two facts are the same fact.** There
140
+ // is nothing in the answer to protect — no chat title, no session id, no
141
+ // project name, just three integers and the version — which is why the route
142
+ // can sit beside `/api/health` in `HEALTH_PATHS` without widening what an
143
+ // unauthenticated caller can learn beyond "something is happening here".
144
+ // `reasons` (a line per piece of work, for a person) waits on SPEC §10's first
145
+ // question: how a CLI proves it is local, given that a reverse proxy on the
146
+ // same host also connects from loopback. Until that is designed, this answer
147
+ // is the whole answer for every caller — so nothing here may grow a string
148
+ // field without revisiting the auth decision above it.
149
+ //
150
+ // The callers that make the cheapness requirement real: `paddock service
151
+ // stop`, `paddock update`, and Watchtower's pre-update hook, which runs
152
+ // `paddock busy` before every image pull. `collectBusy` is synchronous and
153
+ // reads three in-memory structures — see busy.ts for why `job` does NOT come
154
+ // from `fleetStatus()`.
155
+ app.get("/api/busy", {
156
+ schema: {
157
+ tags: ["System"],
158
+ summary: "Is this instance busy?",
159
+ description: "The CLI contract's busy check (SPEC §4). Returns `{ busy, version, counts }`, where `counts` has `chat` (chat turns running, including fired triggers and cron fires), `background` (live sessions holding background work after their turn ended) and `job` (herdctl scheduler jobs). `busy` is true only for work a restart would lose or cut short. Counts only — never labels, ids or titles — so it needs no authentication. Reads in-memory state, never the disk.",
160
+ response: {
161
+ 200: {
162
+ description: "Whether work a restart would cut short is under way.",
163
+ type: "object",
164
+ additionalProperties: true,
165
+ },
166
+ },
167
+ },
168
+ }, async () => collectBusy({
169
+ runningChats: () => hub?.runningSessions().length ?? 0,
170
+ // Tasks in a session count only while its SDK session is still live —
171
+ // see `BackgroundRegistry.liveTaskCount`, which explains why a stale
172
+ // batch-mode entry must not wedge this at busy.
173
+ backgroundTasks: () => background?.liveTaskCount((id) => herdctl.isSdkSessionLive(id)) ?? 0,
174
+ runningJobs: () => herdctl.runningJobCount(),
175
+ }, PADDOCK_VERSION));
131
176
  // --- instance-wide settings (issue #385) --------------------------------
132
177
  // A top-level admin screen over the frozen instance config. GET reports every
133
178
  // surfaced field (value / default / editable / sensitive / env-shadow); PUT
@@ -0,0 +1,26 @@
1
+ export interface RuntimeInfo {
2
+ pid: number;
3
+ port: number;
4
+ /** Where the CLI reaches it, e.g. `http://127.0.0.1:3729`. */
5
+ url: string;
6
+ version: string;
7
+ /** ISO 8601. */
8
+ startedAt: string;
9
+ }
10
+ export declare const RUNTIME_FILE = "runtime.json";
11
+ export declare function runtimeFilePath(home: string): string;
12
+ /** Writes the file atomically (a temporary file, then a rename), readable only by this user. */
13
+ export declare function writeRuntimeFile(home: string, info: Omit<RuntimeInfo, "startedAt"> & {
14
+ startedAt?: string;
15
+ }): RuntimeInfo;
16
+ /** The file's contents, or null if it's missing or isn't a runtime file (a half-written or foreign file). */
17
+ export declare function readRuntimeFile(home: string): RuntimeInfo | null;
18
+ /**
19
+ * Removes the file, but only if it's still ours: a second instance may have started since and written its own.
20
+ * Call it on a clean exit. Returns whether it removed anything.
21
+ */
22
+ export declare function removeRuntimeFile(home: string, pid?: number): boolean;
23
+ /** Whether a process with this pid exists. EPERM means it exists but isn't ours, which still counts. */
24
+ export declare function isPidAlive(pid: number): boolean;
25
+ /** The runtime file, if the process it names is still alive. A crashed instance leaves a stale file behind. */
26
+ export declare function readLiveRuntimeFile(home: string): RuntimeInfo | null;
@@ -0,0 +1,91 @@
1
+ // From edspencer/cli-contract reference/runtime-file.ts @ SPEC 0.1; local changes: `node:fs` / `node:path` imports
2
+ // unchanged, nothing else — the module is Node built-ins only and Paddock's `<home>` is its data dir, so it needed
3
+ // no adaptation. Kept verbatim on purpose: wave C4 re-syncs it, and a divergence here would be a divergence in the
4
+ // one file every tool's `busy`, `service status` and `update` agree on.
5
+ //
6
+ // `<home>/runtime.json`: what a running instance tells its own CLI (SPEC §4, ROADMAP). `serve` writes it once it's
7
+ // listening and removes it on a clean exit; `busy`, `service status` and `update` read it to find the instance.
8
+ //
9
+ // It holds `{ pid, port, url, version, startedAt }`. A token for /api/busy will join it once SPEC §10's first question
10
+ // is settled; the file is written 0600 already, so adding one later changes nothing about who can read it.
11
+ // Node built-ins only.
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ export const RUNTIME_FILE = "runtime.json";
15
+ export function runtimeFilePath(home) {
16
+ return path.join(home, RUNTIME_FILE);
17
+ }
18
+ /** Writes the file atomically (a temporary file, then a rename), readable only by this user. */
19
+ export function writeRuntimeFile(home, info) {
20
+ const full = {
21
+ pid: info.pid,
22
+ port: info.port,
23
+ url: info.url,
24
+ version: info.version,
25
+ startedAt: info.startedAt ?? new Date().toISOString(),
26
+ };
27
+ fs.mkdirSync(home, { recursive: true });
28
+ const file = runtimeFilePath(home);
29
+ const tmp = `${file}.${process.pid}.tmp`;
30
+ fs.writeFileSync(tmp, `${JSON.stringify(full, null, 2)}\n`, { mode: 0o600 });
31
+ try {
32
+ fs.renameSync(tmp, file);
33
+ }
34
+ catch (err) {
35
+ fs.rmSync(tmp, { force: true });
36
+ throw err;
37
+ }
38
+ return full;
39
+ }
40
+ /** The file's contents, or null if it's missing or isn't a runtime file (a half-written or foreign file). */
41
+ export function readRuntimeFile(home) {
42
+ let raw;
43
+ try {
44
+ raw = fs.readFileSync(runtimeFilePath(home), "utf8");
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ try {
50
+ const v = JSON.parse(raw);
51
+ if (Number.isInteger(v.pid) &&
52
+ Number.isInteger(v.port) &&
53
+ typeof v.url === "string" &&
54
+ typeof v.version === "string" &&
55
+ typeof v.startedAt === "string") {
56
+ return v;
57
+ }
58
+ }
59
+ catch {
60
+ // fall through
61
+ }
62
+ return null;
63
+ }
64
+ /**
65
+ * Removes the file, but only if it's still ours: a second instance may have started since and written its own.
66
+ * Call it on a clean exit. Returns whether it removed anything.
67
+ */
68
+ export function removeRuntimeFile(home, pid = process.pid) {
69
+ const info = readRuntimeFile(home);
70
+ if (!info || info.pid !== pid)
71
+ return false;
72
+ fs.rmSync(runtimeFilePath(home), { force: true });
73
+ return true;
74
+ }
75
+ /** Whether a process with this pid exists. EPERM means it exists but isn't ours, which still counts. */
76
+ export function isPidAlive(pid) {
77
+ if (!Number.isInteger(pid) || pid <= 0)
78
+ return false;
79
+ try {
80
+ process.kill(pid, 0);
81
+ return true;
82
+ }
83
+ catch (err) {
84
+ return err.code === "EPERM";
85
+ }
86
+ }
87
+ /** The runtime file, if the process it names is still alive. A crashed instance leaves a stale file behind. */
88
+ export function readLiveRuntimeFile(home) {
89
+ const info = readRuntimeFile(home);
90
+ return info && isPidAlive(info.pid) ? info : null;
91
+ }
@@ -1 +1,34 @@
1
+ /**
2
+ * How long a clean shutdown gets before we exit anyway.
3
+ *
4
+ * The CLI contract requires the SIGTERM path to exit "within the unit's stop
5
+ * timeout" (SPEC §4, §8), and the tightest of those is **launchd's**: with no
6
+ * `ExitTimeOut` in the plist, a LaunchAgent gets 20 seconds before SIGKILL
7
+ * (systemd's user default is 90). So the budget is 20 seconds, and this sits
8
+ * inside it with room for the exit itself.
9
+ *
10
+ * The deadline matters because the two awaits below are not bounded by anything
11
+ * else. `app.close()` waits for in-flight requests and open WebSockets, and a
12
+ * browser holding `/ws` is the normal state of a running Paddock; `fleet.stop()`
13
+ * talks to subprocesses. Exceeding the budget is strictly worse than exiting at
14
+ * it: SIGKILL skips the rest of this function, so the runtime file would be left
15
+ * behind for the next instance's `busy` to find and the log would have no record
16
+ * of what was cut short.
17
+ */
18
+ export declare const SHUTDOWN_DEADLINE_MS = 15000;
19
+ /**
20
+ * The URL to record in `runtime.json` — the one a CLI on this machine can
21
+ * actually fetch, which is not always the one we bound.
22
+ *
23
+ * A wildcard bind is the normal case for a container or a dev box (`HOST=0.0.0.0`
24
+ * is what this repo's own `pm` wrapper injects), and `http://0.0.0.0:<port>` is
25
+ * not an address to connect TO. Node happens to resolve it to loopback on Linux
26
+ * and fails on some other stacks, so the file records loopback explicitly rather
27
+ * than relying on that. A real interface address is left alone: someone who
28
+ * bound `192.168.1.10` meant it, and that address reaches the server from here
29
+ * as well as from anywhere else.
30
+ *
31
+ * IPv6 gets brackets, because `new URL('http://::1:7233')` is not a URL.
32
+ */
33
+ export declare function runtimeUrl(host: string, port: number): string;
1
34
  export declare function start(): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Process lifecycle for a Paddock server: bind the port and wire signal-driven
3
- * shutdown. Split out of `index.ts` so the `paddock` CLI (src/cli/paddock.ts)
4
- * and the plain `node dist/index.js` entrypoint share one implementation.
2
+ * Process lifecycle for a Paddock server: bind the port, publish
3
+ * `runtime.json`, and wire signal-driven shutdown. Split out of `index.ts` so
4
+ * the `paddock` CLI (src/cli/paddock.ts) and the plain `node dist/index.js`
5
+ * entrypoint share one implementation.
5
6
  *
6
7
  * Config is resolved INSIDE `buildApp()`, not at module load, so a caller may
7
8
  * still mutate `process.env` right up until this function is invoked. The CLI
@@ -9,8 +10,51 @@
9
10
  */
10
11
  import { buildApp } from "./app.js";
11
12
  import { startServiceLogRotation } from "./service-log.js";
13
+ import { removeRuntimeFile, writeRuntimeFile } from "./runtime-file.js";
14
+ import { PADDOCK_VERSION } from "./version.js";
15
+ /**
16
+ * How long a clean shutdown gets before we exit anyway.
17
+ *
18
+ * The CLI contract requires the SIGTERM path to exit "within the unit's stop
19
+ * timeout" (SPEC §4, §8), and the tightest of those is **launchd's**: with no
20
+ * `ExitTimeOut` in the plist, a LaunchAgent gets 20 seconds before SIGKILL
21
+ * (systemd's user default is 90). So the budget is 20 seconds, and this sits
22
+ * inside it with room for the exit itself.
23
+ *
24
+ * The deadline matters because the two awaits below are not bounded by anything
25
+ * else. `app.close()` waits for in-flight requests and open WebSockets, and a
26
+ * browser holding `/ws` is the normal state of a running Paddock; `fleet.stop()`
27
+ * talks to subprocesses. Exceeding the budget is strictly worse than exiting at
28
+ * it: SIGKILL skips the rest of this function, so the runtime file would be left
29
+ * behind for the next instance's `busy` to find and the log would have no record
30
+ * of what was cut short.
31
+ */
32
+ export const SHUTDOWN_DEADLINE_MS = 15_000;
33
+ /**
34
+ * The URL to record in `runtime.json` — the one a CLI on this machine can
35
+ * actually fetch, which is not always the one we bound.
36
+ *
37
+ * A wildcard bind is the normal case for a container or a dev box (`HOST=0.0.0.0`
38
+ * is what this repo's own `pm` wrapper injects), and `http://0.0.0.0:<port>` is
39
+ * not an address to connect TO. Node happens to resolve it to loopback on Linux
40
+ * and fails on some other stacks, so the file records loopback explicitly rather
41
+ * than relying on that. A real interface address is left alone: someone who
42
+ * bound `192.168.1.10` meant it, and that address reaches the server from here
43
+ * as well as from anywhere else.
44
+ *
45
+ * IPv6 gets brackets, because `new URL('http://::1:7233')` is not a URL.
46
+ */
47
+ export function runtimeUrl(host, port) {
48
+ const loopback = {
49
+ "0.0.0.0": "127.0.0.1",
50
+ "::": "::1",
51
+ "": "127.0.0.1",
52
+ };
53
+ const h = loopback[host] ?? host;
54
+ return `http://${h.includes(":") ? `[${h}]` : h}:${String(port)}`;
55
+ }
12
56
  export async function start() {
13
- const { app, cfg, close } = await buildApp();
57
+ const { app, cfg, close, hub } = await buildApp();
14
58
  // Nothing else rotates the service log: the supervisor appends to it through
15
59
  // its own descriptor, a `systemd --user` unit writing to a file gets no
16
60
  // journal rotation, and macOS has nothing that would (SPEC §7). A no-op on a
@@ -20,13 +64,76 @@ export async function start() {
20
64
  app.log.warn(message);
21
65
  },
22
66
  });
23
- const shutdown = async () => {
67
+ /**
68
+ * Exit on SIGTERM/SIGINT having said what was cut short (SPEC §4, §8).
69
+ *
70
+ * Three things are deliberate here.
71
+ *
72
+ * **It records rather than finishes.** A chat turn is a `claude` subprocess
73
+ * that can run for minutes; no 20-second stop timeout can wait for one, and
74
+ * `herdctl.stop()` ends the fleet with `waitForJobs: false` precisely because
75
+ * waiting is not an option. So the contract's other half applies — record the
76
+ * work under way — and the record is this `warn` line in the service log,
77
+ * which is the one artefact that survives the restart and which
78
+ * `paddock service logs` reads. What is NOT done, and should not be, is
79
+ * writing a note into the cut-short chat's transcript: `claude` owns that file
80
+ * and may append to it after we are gone, so a server-side write cannot be
81
+ * ordered against the subprocess's own and would be the resurrection race
82
+ * `turn-interlock.ts` exists to avoid.
83
+ *
84
+ * The reason a count is enough: `/api/busy` reports the same numbers BEFORE
85
+ * the stop, and `paddock service stop` refuses on them. The log line is for
86
+ * the race §4 admits — work that started between the check and the signal.
87
+ *
88
+ * **It is idempotent.** A supervisor that sends SIGTERM twice, or a Ctrl-C
89
+ * after a SIGTERM, used to re-enter `close()` concurrently: a second
90
+ * `app.close()` on a closing Fastify rejects, and a second `fleet.stop()`
91
+ * races the first. The guard makes the extra signals no-ops.
92
+ *
93
+ * **It exits 0.** The unit's `Restart=always` / `KeepAlive` means the
94
+ * supervisor brings Paddock back whatever the code; 0 is what makes a logout
95
+ * or a `service stop` read as a clean stop rather than a crash in the logs.
96
+ */
97
+ let shuttingDown = false;
98
+ const shutdown = async (signal) => {
99
+ if (shuttingDown)
100
+ return;
101
+ shuttingDown = true;
102
+ const running = hub.runningSessions().length;
103
+ app.log.warn(running > 0
104
+ ? `${signal}: shutting down with ${running} chat turn(s) in flight — they are being cut short. ` +
105
+ "Run `paddock busy` before a stop or an update to see this before it happens."
106
+ : `${signal}: shutting down, no chat turn in flight.`);
24
107
  stopRotation();
25
- await close();
108
+ // Published once we were listening; removed here so a `busy` or a
109
+ // `service status` run after this exit does not find a live-looking
110
+ // instance. Before the slow awaits, because this is the part that must land
111
+ // even if they overrun the deadline below.
112
+ removeRuntimeFile(cfg.dataDir);
113
+ // Bounded: see SHUTDOWN_DEADLINE_MS. `exit(0)` either way, so a hung
114
+ // `app.close()` cannot turn a stop into a SIGKILL.
115
+ await Promise.race([close(), new Promise((r) => setTimeout(r, SHUTDOWN_DEADLINE_MS))]).catch(() => undefined);
26
116
  process.exit(0);
27
117
  };
28
- process.on("SIGINT", shutdown);
29
- process.on("SIGTERM", shutdown);
118
+ process.on("SIGINT", () => void shutdown("SIGINT"));
119
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
30
120
  await app.listen({ port: cfg.port, host: cfg.host });
121
+ // `<dataDir>/runtime.json`, 0600, written only once we are actually listening
122
+ // (SPEC §4) — it is how `paddock busy`, and later `update`, find THIS instance
123
+ // without guessing a port. A failure to write it is logged and not fatal: the
124
+ // server is up and serving, and the one thing that stops working is a CLI
125
+ // finding it, which is better than refusing to run.
126
+ try {
127
+ writeRuntimeFile(cfg.dataDir, {
128
+ pid: process.pid,
129
+ port: cfg.port,
130
+ url: runtimeUrl(cfg.host, cfg.port),
131
+ version: PADDOCK_VERSION,
132
+ });
133
+ }
134
+ catch (err) {
135
+ app.log.warn(`could not write ${cfg.dataDir}/runtime.json (${err.message}) — ` +
136
+ "`paddock busy` will report this instance as not running");
137
+ }
31
138
  app.log.info(`paddock-server listening on http://${cfg.host}:${cfg.port}`);
32
139
  }
@@ -14,6 +14,7 @@ import type { SweepService } from "./sweep.js";
14
14
  import type { PaddockConfig } from "./config.js";
15
15
  import type { DriveMode } from "./models.js";
16
16
  import type { SessionHub } from "./session-hub.js";
17
+ import type { BackgroundRegistry } from "./background-live.js";
17
18
  import type { AttachmentStore } from "./attachments.js";
18
19
  import type { QueuedMessageStore } from "./queued-message.js";
19
20
  import type { ArchiveStore } from "./archive.js";
@@ -152,6 +153,14 @@ export type FireTrigger = (slug: string, triggerName: string) => Promise<string
152
153
  export interface ChatHandlerContext {
153
154
  deps: ChatHandlerDeps;
154
155
  hub: SessionHub;
156
+ /**
157
+ * The live background-work registry (#604). Here for the same reason `hub` is:
158
+ * it is per-server state owned by the WS layer that a NON-socket caller has to
159
+ * be able to read. `GET /api/busy` is that caller — a turn can end while its
160
+ * background tasks keep running, so the hub alone would report a session with
161
+ * a live sub-agent as idle (CLI contract SPEC §4).
162
+ */
163
+ background: BackgroundRegistry;
155
164
  startAgentTurn: StartAgentTurn;
156
165
  composePreloadedPrompt: ComposePreloadedPrompt;
157
166
  fireTrigger: FireTrigger;
@@ -85,6 +85,7 @@ export function makeTurnEngine(engine) {
85
85
  const selfMcpCtx = {
86
86
  deps,
87
87
  hub,
88
+ background,
88
89
  startAgentTurn,
89
90
  composePreloadedPrompt: triggers.composePreloadedPrompt,
90
91
  fireTrigger: triggers.fireTrigger,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.80.0",
3
+ "version": "0.82.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,2 +1,2 @@
1
- import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-DN82do6Z.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,m]=l.useState(!1),[f,u]=l.useState(null);if(l.useEffect(()=>{e?u(null):m(!1)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{m(!0),u(null);try{await g()}catch(i){u(i instanceof Error?i.message:"Action failed"),m(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
1
+ import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-CHOtvlnO.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,m]=l.useState(!1),[f,u]=l.useState(null);if(l.useEffect(()=>{e?u(null):m(!1)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{m(!0),u(null);try{await g()}catch(i){u(i instanceof Error?i.message:"Action failed"),m(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
2
2
 
@@ -0,0 +1,2 @@
1
+ import{j as o}from"./index-CHOtvlnO.js";import{D as r}from"./DiscoverView-B_kcF4sm.js";import{u as e}from"./documentTitle-DGjRXuTP.js";import"./Surfaces-7UlhFBSG.js";function p(){return e(["Discover"]),o.jsx(r,{})}export{p as DiscoverPage};
2
+