@edspencer/paddock 0.82.1 → 0.83.1

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 (94) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/cli/args.d.ts +29 -4
  3. package/packages/server/dist/cli/args.js +143 -5
  4. package/packages/server/dist/cli/busy.js +1 -1
  5. package/packages/server/dist/cli/doctor.d.ts +60 -0
  6. package/packages/server/dist/cli/doctor.js +580 -0
  7. package/packages/server/dist/cli/exit-codes.js +1 -1
  8. package/packages/server/dist/cli/output.js +1 -1
  9. package/packages/server/dist/cli/paddock.js +28 -3
  10. package/packages/server/dist/cli/service/index.d.ts +34 -1
  11. package/packages/server/dist/cli/service/index.js +142 -16
  12. package/packages/server/dist/cli/service/launchd.js +12 -2
  13. package/packages/server/dist/cli/service/logs.js +1 -1
  14. package/packages/server/dist/cli/service/manager.js +1 -1
  15. package/packages/server/dist/cli/service/path-env.js +1 -1
  16. package/packages/server/dist/cli/service/ready.d.ts +16 -23
  17. package/packages/server/dist/cli/service/ready.js +42 -0
  18. package/packages/server/dist/cli/service/systemd.js +16 -3
  19. package/packages/server/dist/cli/update/health.js +1 -1
  20. package/packages/server/dist/cli/update/install-method.js +1 -1
  21. package/packages/server/dist/cli/update/lock.js +1 -1
  22. package/packages/server/dist/cli/update/package-manager.js +1 -1
  23. package/packages/server/dist/cli/update/registry.js +1 -1
  24. package/packages/server/dist/cli/update/semver.js +1 -1
  25. package/packages/server/dist/cli/update/update-check.js +1 -1
  26. package/packages/server/dist/cli/update/update.js +1 -1
  27. package/packages/server/dist/runtime-file.d.ts +1 -1
  28. package/packages/server/dist/runtime-file.js +9 -13
  29. package/packages/server/package.json +1 -1
  30. package/packages/web/dist/assets/{DeleteProjectDialog-CgHpQctt.js → DeleteProjectDialog-BPwSkY3s.js} +1 -1
  31. package/packages/web/dist/assets/DiscoverPage-DVoF9Vil.js +2 -0
  32. package/packages/web/dist/assets/{DiscoverView-BKqI0I1E.js → DiscoverView-Bz4Rxg-V.js} +1 -1
  33. package/packages/web/dist/assets/{InstanceConfigPage-DL3raCK1.js → InstanceConfigPage-CXHem0Ab.js} +1 -1
  34. package/packages/web/dist/assets/{MarkdownRenderer-DcjEuf3s.js → MarkdownRenderer-2vrOdDh7.js} +1 -1
  35. package/packages/web/dist/assets/{ProjectRedirect-DerkA-nE.js → ProjectRedirect-V77rycxm.js} +1 -1
  36. package/packages/web/dist/assets/{ProjectView-NwfC3_w7.js → ProjectView-C1qr6nlt.js} +3 -3
  37. package/packages/web/dist/assets/{ProjectsGrid-BeORa0UH.js → ProjectsGrid-Jq1LRFcs.js} +1 -1
  38. package/packages/web/dist/assets/{RootHome-BYJs8qJ6.js → RootHome-H2k4p0ry.js} +1 -1
  39. package/packages/web/dist/assets/{Surfaces-w-kEaAYU.js → Surfaces-By65IjL4.js} +1 -1
  40. package/packages/web/dist/assets/{arc-Bv56hasM.js → arc-BzdIzLZ7.js} +1 -1
  41. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BHEknx9i.js → architectureDiagram-3BPJPVTR-DT9cGrnU.js} +1 -1
  42. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DtxTMrPX.js → blockDiagram-GPEHLZMM-BjD-1DXr.js} +1 -1
  43. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DRpEYOlM.js → c4Diagram-AAUBKEIU-DV5q5E08.js} +1 -1
  44. package/packages/web/dist/assets/channel-D0zKU6oW.js +2 -0
  45. package/packages/web/dist/assets/{chunk-2J33WTMH-S4s7GCg7.js → chunk-2J33WTMH-mzG-wWrp.js} +1 -1
  46. package/packages/web/dist/assets/{chunk-4BX2VUAB-BkWVPLf1.js → chunk-4BX2VUAB-BJijOYsf.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-55IACEB6-4-FQp01F.js → chunk-55IACEB6-CSHJ3zhe.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-727SXJPM-BeyDWdIb.js → chunk-727SXJPM-BfdGsNJG.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-AQP2D5EJ-ZSxKy745.js → chunk-AQP2D5EJ-2OJN-ttV.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-FMBD7UC4-Bq8uV9j-.js → chunk-FMBD7UC4-bvjojDsP.js} +1 -1
  51. package/packages/web/dist/assets/{chunk-ND2GUHAM-BOFsRyHk.js → chunk-ND2GUHAM-DnDJhgvR.js} +1 -1
  52. package/packages/web/dist/assets/{chunk-QZHKN3VN-uZ6oNc4m.js → chunk-QZHKN3VN-Bw6DAHLR.js} +1 -1
  53. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-bF2OmpN6.js +2 -0
  54. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-bF2OmpN6.js +2 -0
  55. package/packages/web/dist/assets/{core-CYuJAM2U.js → core-BKN_gaN-.js} +1 -1
  56. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-DIzkVSFb.js → cose-bilkent-S5V4N54A-BUfjSTaX.js} +1 -1
  57. package/packages/web/dist/assets/{dagre-BM42HDAG-CHR6jJBv.js → dagre-BM42HDAG-LwxKA-3H.js} +1 -1
  58. package/packages/web/dist/assets/{diagram-2AECGRRQ-BAFXCH8Q.js → diagram-2AECGRRQ-DHS0Xgf9.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-5GNKFQAL-BJ5t5gbz.js → diagram-5GNKFQAL-CNDLt1m9.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-KO2AKTUF-D9Xt0IVg.js → diagram-KO2AKTUF-D7eVz-kU.js} +1 -1
  61. package/packages/web/dist/assets/{diagram-LMA3HP47-Bq38u6sy.js → diagram-LMA3HP47-Cc0shYXj.js} +1 -1
  62. package/packages/web/dist/assets/{diagram-OG6HWLK6-CMQVqkpi.js → diagram-OG6HWLK6-CdiThnZF.js} +1 -1
  63. package/packages/web/dist/assets/{documentTitle-M1lrfXRU.js → documentTitle-BgvYvKAe.js} +1 -1
  64. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C63M1g9b.js → erDiagram-TEJ5UH35--YKwrR0M.js} +1 -1
  65. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-D3j8ACN7.js → flowDiagram-I6XJVG4X-BuWFXtpn.js} +1 -1
  66. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-B1Fd1A2Y.js → ganttDiagram-6RSMTGT7-F6CLJBJ1.js} +1 -1
  67. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CEhZhDdR.js → gitGraphDiagram-PVQCEYII-BXrk9SVF.js} +1 -1
  68. package/packages/web/dist/assets/{index-1E9yHciS.js → index-2MWb3aF0.js} +2 -2
  69. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-Ctquaew6.js → infoDiagram-5YYISTIA-D9iTq2aK.js} +1 -1
  70. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DV4GvA1R.js → ishikawaDiagram-YF4QCWOH-CE2vjaAw.js} +1 -1
  71. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-C_Cy9waA.js → journeyDiagram-JHISSGLW-CyuASyD1.js} +1 -1
  72. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CGr9ctEf.js → kanban-definition-UN3LZRKU-C-JcQkTl.js} +1 -1
  73. package/packages/web/dist/assets/{linear-D3U8NOkt.js → linear-vd9dAgq3.js} +1 -1
  74. package/packages/web/dist/assets/{mermaid.core-BiArdWKV.js → mermaid.core-C-uiHBIT.js} +4 -4
  75. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CZIpzj8Z.js → mindmap-definition-RKZ34NQL-CQQKVCUB.js} +1 -1
  76. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-1MaeS3rt.js → pieDiagram-4H26LBE5-CB92bTxC.js} +1 -1
  77. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B5pgQ54e.js → quadrantDiagram-W4KKPZXB-Cu5Ux2WT.js} +1 -1
  78. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-BNnxrpGr.js → requirementDiagram-4Y6WPE33-FJxTktFV.js} +1 -1
  79. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DsxVH-hM.js → sankeyDiagram-5OEKKPKP-Bkxk5fIB.js} +1 -1
  80. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-Ke9LcFZI.js → sequenceDiagram-3UESZ5HK-gw8EPzuw.js} +1 -1
  81. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-UJzmflND.js → stateDiagram-AJRCARHV-CIHq9Q4N.js} +1 -1
  82. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Bp8j_o9f.js +2 -0
  83. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BRjNALVI.js → timeline-definition-PNZ67QCA-CI_99nZc.js} +1 -1
  84. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-ClMAspxz.js → vennDiagram-CIIHVFJN-D5e169vs.js} +1 -1
  85. package/packages/web/dist/assets/{wardley-L42UT6IY-UPv4JEmz.js → wardley-L42UT6IY-C-EFyR0g.js} +1 -1
  86. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BgYJbdEx.js → wardleyDiagram-YWT4CUSO-Cp6Fld6d.js} +1 -1
  87. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-Bfk5HjGx.js → xychartDiagram-2RQKCTM6-D0iq_qGw.js} +1 -1
  88. package/packages/web/dist/index.html +1 -1
  89. package/packages/web/dist/sw.js +1 -1
  90. package/packages/web/dist/assets/DiscoverPage-CG6cf-0i.js +0 -2
  91. package/packages/web/dist/assets/channel-CdIiAqY2.js +0 -2
  92. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCCUHba0.js +0 -2
  93. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCCUHba0.js +0 -2
  94. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgUc3HkP.js +0 -2
@@ -1,6 +1,6 @@
1
1
  import { type CliOptions, type ServiceAction } from "../args.js";
2
2
  import { isNpxPath, serviceDir } from "./spec.js";
3
- import { type Runner, type ServiceSpec } from "./manager.js";
3
+ import { type LingerState, type Runner, type ServiceManager as ServiceBackend, type ServiceSpec } from "./manager.js";
4
4
  import { type UpdateSummary } from "../update/update-check.js";
5
5
  import { type HealthAnswer, type Readiness } from "./ready.js";
6
6
  export interface ServiceContext {
@@ -44,7 +44,38 @@ export interface ServiceContext {
44
44
  force?: boolean;
45
45
  json?: boolean;
46
46
  }) => Promise<number | null>;
47
+ /**
48
+ * Who holds `host:port` — nothing, a Paddock, or something else — asked by
49
+ * `install` before it writes a unit (SPEC §1). Injected so a test's answer
50
+ * does not depend on what happens to be listening on the box running it.
51
+ */
52
+ portOwner?: (host: string, port: number) => Promise<PortOwner>;
47
53
  }
54
+ export type PortOwner = "free" | "paddock" | "other";
55
+ /** The real {@link ServiceContext.portOwner}: a TCP connect, then `/api/health` and `/api/busy`. */
56
+ export declare function portOwner(host: string, port: number): Promise<PortOwner>;
57
+ /**
58
+ * Why `install` won't write a unit that runs a checkout (SPEC §1, §6).
59
+ *
60
+ * A checkout (or an `npm link`) is a working tree: it gets rebuilt, switched to
61
+ * another branch, cleaned and moved, and a unit that runs it follows every one
62
+ * of those — half-built `dist/` included — at the next login, unattended. The
63
+ * flag is there for working on Paddock itself, where that is the point.
64
+ */
65
+ export declare function checkoutRefusal(packageRoot: string): string;
66
+ /**
67
+ * Pick a backend, or explain why there isn't one.
68
+ *
69
+ * Windows is the honest gap. It has no per-user service concept that maps onto
70
+ * either of these without a scheduled task or a service wrapper, and shipping
71
+ * an untested third writer would be worse than saying so.
72
+ */
73
+ export declare function serviceBackend(ctx: ServiceContext): ServiceBackend;
74
+ /**
75
+ * The `enable-linger` line, printed only to the people it applies to (SPEC §7): when linger is off. Nothing when it's
76
+ * on, and nothing when loginctl couldn't answer (`doctor` reports that check as skipped).
77
+ */
78
+ export declare function lingerNote(linger: LingerState): string | undefined;
48
79
  /**
49
80
  * The version of the Paddock the unit points at.
50
81
  *
@@ -104,6 +135,8 @@ export interface StatusReport {
104
135
  /** A stale unit, a missing Node — things that are wrong but not fatal. */
105
136
  warnings: string[];
106
137
  }
138
+ /** What `status` reports, and what `doctor`'s service checks read: one report, so the two agree. */
139
+ export declare function buildStatus(ctx: ServiceContext, backend: ServiceBackend): Promise<StatusReport>;
107
140
  /**
108
141
  * Dispatch, returning the exit code rather than setting one (SPEC §2).
109
142
  *
@@ -29,14 +29,39 @@ import { CliError } from "../args.js";
29
29
  import { EXIT } from "../exit-codes.js";
30
30
  import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, PADDOCK_SERVICE, } from "./spec.js";
31
31
  import { spawnRunner, } from "./manager.js";
32
- import { createLaunchd } from "./launchd.js";
33
- import { createSystemd } from "./systemd.js";
32
+ import { createLaunchd, renderPlist } from "./launchd.js";
33
+ import { createSystemd, renderUnit } from "./systemd.js";
34
+ import { underNodeModules } from "../update/install-method.js";
34
35
  import { missingPaths } from "./path-env.js";
35
36
  import { DEFAULT_LOG_LINES, followFile, sizeOf, tailFile } from "./logs.js";
36
37
  import { readLiveRuntimeFile } from "../../runtime-file.js";
37
38
  import { PADDOCK_VERSION } from "../../version.js";
38
39
  import { updateSummary } from "../update/update-check.js";
39
- import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
40
+ import { waitForReady, fetchHealth, identifyPaddock, isListening, READY_TIMEOUT_LABEL, } from "./ready.js";
41
+ /** The real {@link ServiceContext.portOwner}: a TCP connect, then `/api/health` and `/api/busy`. */
42
+ export async function portOwner(host, port) {
43
+ const probe = host === "0.0.0.0" || host === "" ? "127.0.0.1" : host === "::" ? "::1" : host;
44
+ if (!(await isListening(probe, port)))
45
+ return "free";
46
+ const who = await identifyPaddock(`http://${probe.includes(":") ? `[${probe}]` : probe}:${port}`);
47
+ return who?.paddock === true ? "paddock" : "other";
48
+ }
49
+ /**
50
+ * Why `install` won't write a unit that runs a checkout (SPEC §1, §6).
51
+ *
52
+ * A checkout (or an `npm link`) is a working tree: it gets rebuilt, switched to
53
+ * another branch, cleaned and moved, and a unit that runs it follows every one
54
+ * of those — half-built `dist/` included — at the next login, unattended. The
55
+ * flag is there for working on Paddock itself, where that is the point.
56
+ */
57
+ export function checkoutRefusal(packageRoot) {
58
+ return (`this copy of Paddock is a checkout (${packageRoot}), so \`service install\` won't use it.\n` +
59
+ " A unit that runs a checkout follows every rebuild, branch switch and move of\n" +
60
+ " it, at your next login, unattended. Install it properly first:\n\n" +
61
+ " npm i -g @edspencer/paddock && paddock service install\n\n" +
62
+ " Or run this checkout as the service anyway (when working on Paddock):\n" +
63
+ " paddock service install --from-checkout\n");
64
+ }
40
65
  /**
41
66
  * Pick a backend, or explain why there isn't one.
42
67
  *
@@ -44,6 +69,9 @@ import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
44
69
  * either of these without a scheduled task or a service wrapper, and shipping
45
70
  * an untested third writer would be worse than saying so.
46
71
  */
72
+ export function serviceBackend(ctx) {
73
+ return backendFor(ctx);
74
+ }
47
75
  function backendFor(ctx) {
48
76
  const run = ctx.run ?? spawnRunner;
49
77
  if (ctx.platform === "darwin")
@@ -174,20 +202,26 @@ function installProgress(backend) {
174
202
  " macOS may take a few seconds to approve the background item."
175
203
  : `\n Installing ${kind} and starting Paddock…`;
176
204
  }
177
- /** The `enable-linger` line, printed only to the people it applies to (SPEC §7). */
178
- function lingerNote(linger) {
179
- if (!linger.applies || linger.enabled === true)
205
+ /**
206
+ * The `enable-linger` line, printed only to the people it applies to (SPEC §7): when linger is off. Nothing when it's
207
+ * on, and nothing when loginctl couldn't answer (`doctor` reports that check as skipped).
208
+ */
209
+ export function lingerNote(linger) {
210
+ if (!linger.applies || linger.enabled !== false)
180
211
  return undefined;
181
- const head = linger.enabled === false
182
- ? " Lingering is off, so this service stops when you log out — including"
183
- : " If you log out (this check could not reach loginctl), a user service";
184
- return (`${head}\n` +
212
+ return (" Lingering is off, so this service stops when you log out — including\n" +
185
213
  " out of an SSH session. To keep Paddock up, once:\n" +
186
214
  ` sudo loginctl enable-linger ${linger.user ?? "$USER"}`);
187
215
  }
188
216
  async function install(opts, ctx, backend) {
189
217
  if (isNpxPath(ctx.packageRoot))
190
218
  throw new CliError(NPX_REFUSAL);
219
+ // A checkout is a package root that isn't under any `node_modules` (SPEC §6):
220
+ // `packages/server` in the repo, `<prefix>/lib/node_modules/@edspencer/paddock/
221
+ // packages/server` from npm. `npm link` reads the same, on purpose.
222
+ if (!underNodeModules(ctx.packageRoot) && opts.fromCheckout !== true) {
223
+ throw new CliError(checkoutRefusal(ctx.packageRoot));
224
+ }
191
225
  // `--refresh` (SPEC §5 step 11): re-render an EXISTING unit, keeping what it
192
226
  // records. With nothing installed there is nothing to refresh — exit 4, the
193
227
  // same code every other command that needs a unit returns, rather than
@@ -196,17 +230,57 @@ async function install(opts, ctx, backend) {
196
230
  // CLI, and a silent fallback would move a `--port 9000` service to 7233 on
197
231
  // the one run nobody is watching.
198
232
  const refreshing = opts.refresh === true;
199
- let existing;
233
+ // A reinstall keeps what the installed unit records — its port, host, data
234
+ // dir and verbosity — unless a flag says otherwise (SPEC §1). It used to do
235
+ // that only with `--refresh`, so `service install` over a `--port 9000`
236
+ // service quietly moved it to 7233. `--refresh` still differs in two ways:
237
+ // it ignores `PADDOCK_DATA_DIR` in this shell, and with no unit it exits 4.
238
+ const current = backend.status();
239
+ const existing = current.registered ? current : undefined;
200
240
  if (refreshing) {
201
- existing = backend.status();
202
- if (!existing.registered) {
203
- console.log(notInstalled(backend));
241
+ if (existing === undefined) {
242
+ if (opts.dryRun === true && opts.json === true) {
243
+ // `--dry-run --json` prints its own document (runService leaves it be).
244
+ console.error(notInstalled(backend));
245
+ console.log(JSON.stringify({ ok: false, action: "install", dryRun: true, error: "not installed", unit: backend.unitPath }, null, 2));
246
+ }
247
+ else {
248
+ console.log(notInstalled(backend));
249
+ }
204
250
  return EXIT.NOT_INSTALLED;
205
251
  }
206
252
  }
207
253
  const { spec, dataDir } = specFor(existing !== undefined ? inherit(opts, existing) : opts, ctx, refreshing);
208
254
  const url = `http://${hostFromArgv(spec.args)}:${portFromArgv(spec.args)}`;
209
- if (refreshing) {
255
+ // `--dry-run` (SPEC §1): the unit, on stdout, and nothing written — not the
256
+ // unit, not the service or log folders `backend.install` would make. The
257
+ // where-it-would-go line is on stderr, so `> paddock.service` captures the
258
+ // unit alone.
259
+ if (opts.dryRun === true) {
260
+ const contents = backend.platform === "darwin" ? renderPlist(PADDOCK_SERVICE, spec) : renderUnit(PADDOCK_SERVICE, spec);
261
+ if (opts.json === true) {
262
+ console.log(JSON.stringify({ ok: true, action: "install", dryRun: true, unit: backend.unitPath, contents }, null, 2));
263
+ }
264
+ else {
265
+ console.error(`paddock: dry run: this is the unit \`service install\` would write to ${backend.unitPath}. Nothing was written.`);
266
+ console.log(contents.trimEnd());
267
+ }
268
+ return EXIT.OK;
269
+ }
270
+ // Refuse a port something else holds (SPEC §1): the service would start,
271
+ // fail to bind, and restart every ten seconds, for good. A Paddock holding it
272
+ // is fine — that's the reinstall this replaces — and so is the unit's own
273
+ // running service when it's too old to say it's Paddock (before 0.81).
274
+ const port = Number(portFromArgv(spec.args));
275
+ const owner = await (ctx.portOwner ?? portOwner)(hostFromArgv(spec.args), port);
276
+ if (owner === "other") {
277
+ const itsOwn = current.registered && current.running && portFromArgv(current.argv) === String(port);
278
+ if (!itsOwn) {
279
+ throw new CliError(`port ${port} is held by something that isn't Paddock, so the service could not start.\n` +
280
+ ` Stop it, or pick another port: paddock service install --port ${port + 1}`);
281
+ }
282
+ }
283
+ if (existing !== undefined) {
210
284
  console.log(`\n Re-writing ${backend.unitPath}\n Keeping: paddock ${spec.args.join(" ")}`);
211
285
  }
212
286
  console.log(installProgress(backend));
@@ -327,7 +401,8 @@ function withUpdate(home) {
327
401
  const summary = updateSummary(home, { currentVersion: PADDOCK_VERSION });
328
402
  return summary !== undefined ? { update: summary } : {};
329
403
  }
330
- async function buildStatus(ctx, backend) {
404
+ /** What `status` reports, and what `doctor`'s service checks read: one report, so the two agree. */
405
+ export async function buildStatus(ctx, backend) {
331
406
  const state = backend.status();
332
407
  const base = {
333
408
  installed: state.registered,
@@ -686,6 +761,57 @@ export async function runService(action, opts, ctx) {
686
761
  // `--force` is rejected on every action but `stop` and `restart` — in
687
762
  // `parseCommand`, once, for every verb. See the note there.
688
763
  const backend = backendFor(ctx);
764
+ // `--json` on the actions that change something (SPEC §1): their prose goes
765
+ // to stderr, and stdout gets one document — what was done, and the status
766
+ // afterwards. `status` has its own; `--dry-run` prints its own.
767
+ if (opts.json === true && action !== "status" && action !== "logs" && opts.dryRun !== true) {
768
+ return withJson(action, () => act(action, opts, ctx, backend), ctx, backend);
769
+ }
770
+ return act(action, opts, ctx, backend);
771
+ }
772
+ /**
773
+ * Run `act` with its prose on stderr, then print `{ ok, action, error?,
774
+ * ...status }` on stdout (SPEC §1, §3) — the shape Hallway's `service … --json`
775
+ * prints, and `status --json`'s fields after `ok` and `action`, so a script
776
+ * reads one shape whatever it asked.
777
+ *
778
+ * Every line in this file goes through `console.log`, so the prose is moved by
779
+ * pointing that at stderr for the duration, rather than by threading a writer
780
+ * through every function. A failure still prints its document, and is then
781
+ * re-thrown, so `paddock: <message>` and the exit code are what they are
782
+ * without `--json`.
783
+ */
784
+ async function withJson(action, run, ctx, backend) {
785
+ const log = console.log;
786
+ console.log = (...a) => console.error(...a);
787
+ let code;
788
+ let failure;
789
+ try {
790
+ code = await run();
791
+ }
792
+ catch (err) {
793
+ failure = err;
794
+ code = err instanceof CliError ? err.exitCode : EXIT.FAILED;
795
+ }
796
+ finally {
797
+ console.log = log;
798
+ }
799
+ const error = failure !== undefined
800
+ ? (failure instanceof Error ? failure.message : String(failure)).split("\n")[0]
801
+ : code === EXIT.BUSY
802
+ ? "busy"
803
+ : code === EXIT.NOT_INSTALLED
804
+ ? "not installed"
805
+ : code !== EXIT.OK
806
+ ? "failed"
807
+ : undefined;
808
+ const report = await buildStatus(ctx, backend);
809
+ console.log(JSON.stringify({ ok: code === EXIT.OK, action, ...(error !== undefined ? { error } : {}), ...report }, null, 2));
810
+ if (failure !== undefined)
811
+ throw failure;
812
+ return code;
813
+ }
814
+ async function act(action, opts, ctx, backend) {
689
815
  if (action === "install")
690
816
  return install(opts, ctx, backend);
691
817
  if (action === "uninstall")
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/service/launchd.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/service/launchd.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // macOS: a launchd LaunchAgent in the `gui/<uid>` domain (SPEC §7). It starts at login, not at boot, and runs as the
3
3
  // user, inside their login session (so the login keychain is unlocked). Node built-ins only.
4
4
  //
@@ -135,6 +135,7 @@ export function createLaunchd(id, o) {
135
135
  label: id.label,
136
136
  kind: 'a launchd LaunchAgent',
137
137
  install(spec) {
138
+ const existed = fs.existsSync(file);
138
139
  fs.mkdirSync(path.dirname(file), { recursive: true });
139
140
  fs.mkdirSync(spec.workingDirectory, { recursive: true });
140
141
  // launchd doesn't create the log's folder; without it the job fails to spawn, and says nothing.
@@ -142,7 +143,16 @@ export function createLaunchd(id, o) {
142
143
  fs.writeFileSync(file, renderPlist(id, spec), 'utf8');
143
144
  // A bootout first, because bootstrap refuses a label that's loaded. It fails on a first install, as expected.
144
145
  bootoutAndWait();
145
- bootstrap();
146
+ try {
147
+ bootstrap();
148
+ }
149
+ catch (err) {
150
+ // A first install launchd refused mustn't leave a plist behind: `status` would then say it's installed, and
151
+ // the next login would try it again. A reinstall keeps its plist.
152
+ if (!existed)
153
+ fs.rmSync(file, { force: true });
154
+ throw err;
155
+ }
146
156
  run('launchctl', ['kickstart', '-k', target]);
147
157
  },
148
158
  uninstall() {
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/service/logs.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/service/logs.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // `<tool> service logs [-f] [-n <lines>]` (SPEC §1, §7): one Node implementation on every OS, because every unit
3
3
  // writes to `<home>/logs/service.log`. No `tail`: `tail -f` isn't on Windows, and which `tail` won a PATH lookup would
4
4
  // change the output. Node built-ins only.
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/service/manager.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/service/manager.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // The seam between what a tool's service is and how a service manager spells it (SPEC §7). launchd.ts and
3
3
  // systemd.ts implement `ServiceManager`; Task Scheduler (Windows, P3) is the third, later, behind the same interface.
4
4
  // Every command goes through a `Runner`, so the lifecycle is tested with a fake one, on any OS. Node built-ins only.
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/service/path-env.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/service/path-env.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // The PATH a unit gives the service (SPEC §7): Node's own bin folder first, then the installing shell's PATH without
3
3
  // its per-shell entries, then the system folders. Only folders that exist are kept. Node built-ins only.
4
4
  //
@@ -1,26 +1,3 @@
1
- /**
2
- * "Is it actually up?" — the bounded readiness poll the service CLI uses before
3
- * it claims success (#873, #861).
4
- *
5
- * Every service command that starts something has the same failure mode without
6
- * this: the supervisor accepts the request and returns, so the CLI prints a URL
7
- * and a cheerful past tense while the server is still binding — or is already
8
- * crash-looping on a port clash, which `launchctl` and `systemctl` will both
9
- * happily report as "running" for the ten seconds between restarts. A message
10
- * that can sit over a crash-loop and still say "running at <url>" is not a
11
- * status line, it is a guess.
12
- *
13
- * So the check is deliberately end-to-end: an HTTP request to the URL a human
14
- * would open, not a pid lookup. `/api/health` is the probe because it is in
15
- * `ALWAYS_PUBLIC_PATHS` (`auth.ts`) — it answers whatever `PADDOCK_AUTH_MODE`
16
- * the installed unit runs with, so this does not quietly stop working for the
17
- * people who configured a credential.
18
- *
19
- * The result is a tri-state on purpose. "I could not confirm it" is a real and
20
- * common answer — an unusual bind host, a slow first boot — and it is not the
21
- * same claim as "it failed". Callers print it as a caveat, not an error, and
22
- * point at the logs.
23
- */
24
1
  /** Where the poll got to when it stopped. */
25
2
  export type Readiness = "ready" | "timeout";
26
3
  /** What `/api/health` said, when it said anything (SPEC §8). */
@@ -111,3 +88,19 @@ export type VersionVerdict = {
111
88
  export declare function waitForVersion(opts: ReadyOptions & {
112
89
  version: string;
113
90
  }): Promise<VersionVerdict>;
91
+ /** Is anything accepting connections at host:port? A second at most. */
92
+ export declare function isListening(host: string, port: number): Promise<boolean>;
93
+ /**
94
+ * Is what answers at `url` a Paddock, and which version? Undefined when nothing
95
+ * answers `/api/health` there.
96
+ *
97
+ * `/api/health`'s `{ ok, version }` is the CLI contract's, so every one of the
98
+ * six tools answers it alike; `/api/busy`'s `counts.chat` and
99
+ * `counts.background` are Paddock's own. A Paddock older than 0.81 has no
100
+ * `/api/busy`, and reads as not-Paddock here — callers that can tell it apart
101
+ * some other way (an installed unit on that port) should.
102
+ */
103
+ export declare function identifyPaddock(url: string, fetchImpl?: typeof fetch): Promise<{
104
+ paddock: boolean;
105
+ version?: string;
106
+ } | undefined>;
@@ -21,6 +21,7 @@
21
21
  * same claim as "it failed". Callers print it as a caveat, not an error, and
22
22
  * point at the logs.
23
23
  */
24
+ import net from "node:net";
24
25
  /**
25
26
  * Ask `<url>/api/health` once, and say what it answered.
26
27
  *
@@ -134,3 +135,44 @@ export async function waitForVersion(opts) {
134
135
  await sleep(intervalMs);
135
136
  }
136
137
  }
138
+ /** Is anything accepting connections at host:port? A second at most. */
139
+ export function isListening(host, port) {
140
+ return new Promise((resolve) => {
141
+ const s = net.connect({ host, port });
142
+ s.setTimeout(1000);
143
+ s.once("connect", () => {
144
+ s.destroy();
145
+ resolve(true);
146
+ });
147
+ s.once("timeout", () => {
148
+ s.destroy();
149
+ resolve(false);
150
+ });
151
+ s.once("error", () => resolve(false));
152
+ });
153
+ }
154
+ /**
155
+ * Is what answers at `url` a Paddock, and which version? Undefined when nothing
156
+ * answers `/api/health` there.
157
+ *
158
+ * `/api/health`'s `{ ok, version }` is the CLI contract's, so every one of the
159
+ * six tools answers it alike; `/api/busy`'s `counts.chat` and
160
+ * `counts.background` are Paddock's own. A Paddock older than 0.81 has no
161
+ * `/api/busy`, and reads as not-Paddock here — callers that can tell it apart
162
+ * some other way (an installed unit on that port) should.
163
+ */
164
+ export async function identifyPaddock(url, fetchImpl = fetch) {
165
+ const health = await fetchHealth(url, { fetchImpl });
166
+ if (health === undefined)
167
+ return undefined;
168
+ const version = health.version !== undefined ? { version: health.version } : {};
169
+ try {
170
+ const res = await fetchImpl(`${url}/api/busy`, { signal: AbortSignal.timeout(2_000) });
171
+ const body = (await res.json());
172
+ const counts = body.counts ?? {};
173
+ return { paddock: health.ok && typeof counts.chat === "number" && "background" in counts, ...version };
174
+ }
175
+ catch {
176
+ return { paddock: false, ...version };
177
+ }
178
+ }
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/service/systemd.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/service/systemd.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // Linux: a systemd user unit (`systemctl --user`, SPEC §7), running as the user. Without linger the user's manager,
3
3
  // and the service with it, stops at their last logout, SSH sessions included; `linger()` asks, and the tool prints
4
4
  // the `sudo loginctl enable-linger` line rather than running it. Node built-ins only.
@@ -103,14 +103,27 @@ export function createSystemd(id, o) {
103
103
  kind: 'a systemd --user service',
104
104
  install(spec) {
105
105
  // Asked before the unit is overwritten, while it still describes what's running.
106
- const wasActive = fs.existsSync(file) && isActive();
106
+ const existed = fs.existsSync(file);
107
+ const wasActive = existed && isActive();
107
108
  fs.mkdirSync(path.dirname(file), { recursive: true });
108
109
  fs.mkdirSync(spec.workingDirectory, { recursive: true });
109
110
  fs.mkdirSync(path.dirname(spec.logPath), { recursive: true });
110
111
  fs.writeFileSync(file, renderUnit(id, spec), 'utf8');
111
112
  // Without a reload the manager keeps serving its cached copy of the old unit.
112
113
  run('systemctl', ['--user', 'daemon-reload']);
113
- systemctl('enable', '--now', id.unit);
114
+ try {
115
+ systemctl('enable', '--now', id.unit);
116
+ }
117
+ catch (err) {
118
+ // A first install systemd refused (no systemd here, no user bus) mustn't leave a unit behind: `status` would
119
+ // then say it's installed. A reinstall keeps its unit, which is what's installed either way.
120
+ if (!existed) {
121
+ run('systemctl', ['--user', 'disable', id.unit]);
122
+ fs.rmSync(file, { force: true });
123
+ run('systemctl', ['--user', 'daemon-reload']);
124
+ }
125
+ throw err;
126
+ }
114
127
  // `enable --now` doesn't touch a unit that's already active: a reinstall over a running service would leave the
115
128
  // old process serving and report success. That's the state `update`'s rollback refreshes in, while the failed
116
129
  // version is still up. So restart, but only then: on a stopped unit it would be two starts milliseconds apart,
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/health.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/health.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // Asking `/api/health` (SPEC §8): once, until it answers, or until it answers with one version. The service commands
3
3
  // wait on it before they claim success, and `update` waits for the target version (SPEC §5 step 12). It's an HTTP
4
4
  // request to the URL a person would open, not a pid lookup: launchd and systemd both report a crash-looping job as
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/install-method.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/install-method.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // How was this copy installed? (SPEC §6.) `update` can't run `npm install -g` on a checkout, in a container or out of
3
3
  // npx's cache, so before it touches anything it works out which of the table's ten rows it's in, and either picks the
4
4
  // package manager or prints the command that would work and exits 5.
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/lock.ts @ 0.2; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
1
+ // From edspencer/cli-contract reference/lock.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
2
2
  // `<home>/update.lock`: one update at a time (SPEC §5 step 1). Two at once is easy to cause (an Ansible run and a
3
3
  // person at a terminal) and bad: the second stops the service the first just started, or installs an older target
4
4
  // over a newer one, and the loser's rollback "restores" a version nobody asked for.
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/package-manager.ts @ 0.2; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
1
+ // From edspencer/cli-contract reference/package-manager.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
2
2
  // Running the package manager (SPEC §5 steps 5, 8 and 10, §6, §7). Node built-ins only.
3
3
  //
4
4
  // - **npm is the one beside `process.execPath`**, run as `node <npm-cli.js>`, never whichever `npm` is first on PATH.
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/registry.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/registry.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // The npm registry, as `update` asks it (SPEC §5 step 3): two requests with `fetch` and a 10-second abort.
3
3
  //
4
4
  // GET <registry>/-/package/<name>/dist-tags which version `latest` (or `--to <tag>`) is
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/semver.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
1
+ // From edspencer/cli-contract reference/semver.ts @ 1.0; local changes: `.ts` import specifiers → `.js`.
2
2
  // The small semver `update` needs (SPEC §5 step 4): is the target newer, is it a prerelease, and does its
3
3
  // `engines.node` accept this Node. Node built-ins only.
4
4
  //
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/update-check.ts @ 0.2; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
1
+ // From edspencer/cli-contract reference/update-check.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
2
2
  // `<home>/update-check.json`: what the last registry question found (SPEC §3). Only `update`, `update --check` and
3
3
  // `doctor` ask the registry; they write the answer here, and `service status` repeats it without opening a socket.
4
4
  // It's the document `update --check --json` prints (`schemas/update-check.schema.json`), and `service status --json`'s
@@ -1,4 +1,4 @@
1
- // From edspencer/cli-contract reference/update.ts @ 0.2; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
1
+ // From edspencer/cli-contract reference/update.ts @ 1.0; local changes: `.ts` import specifiers → `.js`, and `exit-codes`, `output` and `runtime-file` imported from where Paddock keeps them (`../`, `../../`).
2
2
  // `<tool> update`: SPEC §5, step by step, for every tool. Node built-ins only; nothing here imports a server.
3
3
  //
4
4
  // It's a sequence over injected hooks. A tool supplies what's its own (`UpdateProduct`): its names, its busy check,
@@ -10,7 +10,7 @@ export interface RuntimeInfo {
10
10
  export declare const RUNTIME_FILE = "runtime.json";
11
11
  export declare function runtimeFilePath(home: string): string;
12
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"> & {
13
+ export declare function writeRuntimeFile(home: string, info: Omit<RuntimeInfo, 'startedAt'> & {
14
14
  startedAt?: string;
15
15
  }): RuntimeInfo;
16
16
  /** The file's contents, or null if it's missing or isn't a runtime file (a half-written or foreign file). */
@@ -1,17 +1,13 @@
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
- //
1
+ // From edspencer/cli-contract reference/runtime-file.ts @ 1.0; local changes: none.
6
2
  // `<home>/runtime.json`: what a running instance tells its own CLI (SPEC §4, ROADMAP). `serve` writes it once it's
7
3
  // listening and removes it on a clean exit; `busy`, `service status` and `update` read it to find the instance.
8
4
  //
9
5
  // It holds `{ pid, port, url, version, startedAt }`. A token for /api/busy will join it once SPEC §10's first question
10
6
  // is settled; the file is written 0600 already, so adding one later changes nothing about who can read it.
11
7
  // Node built-ins only.
12
- import fs from "node:fs";
13
- import path from "node:path";
14
- export const RUNTIME_FILE = "runtime.json";
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ export const RUNTIME_FILE = 'runtime.json';
15
11
  export function runtimeFilePath(home) {
16
12
  return path.join(home, RUNTIME_FILE);
17
13
  }
@@ -41,7 +37,7 @@ export function writeRuntimeFile(home, info) {
41
37
  export function readRuntimeFile(home) {
42
38
  let raw;
43
39
  try {
44
- raw = fs.readFileSync(runtimeFilePath(home), "utf8");
40
+ raw = fs.readFileSync(runtimeFilePath(home), 'utf8');
45
41
  }
46
42
  catch {
47
43
  return null;
@@ -50,9 +46,9 @@ export function readRuntimeFile(home) {
50
46
  const v = JSON.parse(raw);
51
47
  if (Number.isInteger(v.pid) &&
52
48
  Number.isInteger(v.port) &&
53
- typeof v.url === "string" &&
54
- typeof v.version === "string" &&
55
- typeof v.startedAt === "string") {
49
+ typeof v.url === 'string' &&
50
+ typeof v.version === 'string' &&
51
+ typeof v.startedAt === 'string') {
56
52
  return v;
57
53
  }
58
54
  }
@@ -81,7 +77,7 @@ export function isPidAlive(pid) {
81
77
  return true;
82
78
  }
83
79
  catch (err) {
84
- return err.code === "EPERM";
80
+ return err.code === 'EPERM';
85
81
  }
86
82
  }
87
83
  /** The runtime file, if the process it names is still alive. A crashed instance leaves a stale file behind. */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.82.1",
3
+ "version": "0.83.1",
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-1E9yHciS.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-2MWb3aF0.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-2MWb3aF0.js";import{D as r}from"./DiscoverView-Bz4Rxg-V.js";import{u as e}from"./documentTitle-BgvYvKAe.js";import"./Surfaces-By65IjL4.js";function p(){return e(["Discover"]),o.jsx(r,{})}export{p as DiscoverPage};
2
+