@pmelab/gtd 10.8.0 → 10.9.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.
@@ -53693,7 +53693,7 @@ const uiJsonSchema = {
53693
53693
  properties: {
53694
53694
  port: {
53695
53695
  type: "integer",
53696
- description: "TCP port `gtd ui` listens on."
53696
+ description: "For ui, the tailscale serve port (default: the first free of 8443, 10000, 443), or the bind port when --host opts out of serve (default: a free port)."
53697
53697
  },
53698
53698
  host: {
53699
53699
  type: "string",
@@ -210495,7 +210495,19 @@ var UiListener = class UiListener extends Tag("UiListener")() {
210495
210495
  });
210496
210496
  }) });
210497
210497
  };
210498
- const DEFAULT_PORT = 8443;
210498
+ /**
210499
+ * The tailscale serve front door's fixed candidate list, walked in order:
210500
+ * a default tailnet permits HTTPS serve only on 443, 8443 and 10000 unless
210501
+ * its policy file says otherwise — an arbitrary high port simply fails to
210502
+ * publish, so nothing outside this list is worth trying.
210503
+ */
210504
+ const SERVE_PORT_CANDIDATES = [
210505
+ 8443,
210506
+ 1e4,
210507
+ 443
210508
+ ];
210509
+ /** `0` means "auto-pick" for both `--port`/`ui.port` — identical to giving no port at all, and kept out of `publishServe --https=0`, which cannot publish. */
210510
+ const normalizePort = (port) => port === 0 ? void 0 : port;
210499
210511
  /**
210500
210512
  * `--dev` needs the gtd SOURCE checkout (its `src/web/`, its `tsdown.config.ts`,
210501
210513
  * its devDependencies) to rebuild against — never the invoking directory,
@@ -210648,20 +210660,25 @@ const probeLiveServeMapping = (servePort) => gen(function* () {
210648
210660
  * check, our own) is stale — cleared via `unpublishServe` before a fresh
210649
210661
  * publish; a record naming another LIVE gtd ui is left alone too, since two
210650
210662
  * instances racing the same port is exactly the case ownership exists to
210651
- * prevent. Returns `true` when it's safe to proceed to `publishServe`,
210652
- * `false` when the caller should fall back without ever calling it.
210663
+ * prevent. Returns `"clear"` when it's safe to proceed to `publishServe`;
210664
+ * `"occupied"` (a foreign live mapping) and `"unknowable"` (a probe that
210665
+ * failed to answer) are both "don't publish", but the caller's candidate
210666
+ * walk (Task 2) treats them differently — `"occupied"` is per-port and
210667
+ * walkable, `"unknowable"` says nothing about any port and would only
210668
+ * re-run the same failing subprocess against every remaining candidate, so
210669
+ * it aborts the walk instead of advancing it.
210653
210670
  */
210654
210671
  const clearOrphanForPublish = (servePort) => gen(function* () {
210655
210672
  const record = readServeRecord(servePort);
210656
210673
  if (record === void 0) {
210657
210674
  const probe = yield* probeLiveServeMapping(servePort);
210658
- if (!probe.ok) return false;
210659
- return probe.mapping === void 0;
210675
+ if (!probe.ok) return "unknowable";
210676
+ return probe.mapping === void 0 ? "clear" : "occupied";
210660
210677
  }
210661
- if (isPidAlive(record.pid) && record.pid !== process.pid) return false;
210678
+ if (isPidAlive(record.pid) && record.pid !== process.pid) return "occupied";
210662
210679
  yield* unpublishServe(servePort).pipe(catchAll(() => succeed$3(void 0)));
210663
210680
  deleteServeRecord(servePort);
210664
- return true;
210681
+ return "clear";
210665
210682
  });
210666
210683
  /**
210667
210684
  * Task 3's serve-first path: probe for a tailnet hostname, run the Task 4
@@ -210679,12 +210696,20 @@ const attemptServe = (servePort, worktree, uiListener, handler) => gen(function*
210679
210696
  const tailscaleStatus = yield* probeTailscaleStatus();
210680
210697
  if (tailscaleStatus === void 0) return {
210681
210698
  ok: false,
210699
+ retry: false,
210682
210700
  reason: "no Tailscale backend detected"
210683
210701
  };
210684
- if (!(yield* clearOrphanForPublish(servePort))) return {
210702
+ const orphanStatus = yield* clearOrphanForPublish(servePort);
210703
+ if (orphanStatus === "occupied") return {
210685
210704
  ok: false,
210705
+ retry: true,
210686
210706
  reason: `port ${servePort} already carries a tailscale serve mapping this instance does not own`
210687
210707
  };
210708
+ if (orphanStatus === "unknowable") return {
210709
+ ok: false,
210710
+ retry: false,
210711
+ reason: `could not confirm port ${servePort} is free — tailscale serve status --json failed`
210712
+ };
210688
210713
  const boundAttempt = yield* uiListener.listen({
210689
210714
  host: "127.0.0.1",
210690
210715
  port: 0,
@@ -210698,6 +210723,7 @@ const attemptServe = (servePort, worktree, uiListener, handler) => gen(function*
210698
210723
  })));
210699
210724
  if (!boundAttempt.ok) return {
210700
210725
  ok: false,
210726
+ retry: false,
210701
210727
  reason: `could not bind the loopback listener: ${boundAttempt.reason}`
210702
210728
  };
210703
210729
  const bound = boundAttempt.bound;
@@ -210713,6 +210739,7 @@ const attemptServe = (servePort, worktree, uiListener, handler) => gen(function*
210713
210739
  yield* sync$1(() => bound.close());
210714
210740
  return {
210715
210741
  ok: false,
210742
+ retry: true,
210716
210743
  reason: `tailscale serve failed: ${publishResult.reason}`
210717
210744
  };
210718
210745
  }
@@ -210726,7 +210753,8 @@ const attemptServe = (servePort, worktree, uiListener, handler) => gen(function*
210726
210753
  return {
210727
210754
  ok: true,
210728
210755
  bound,
210729
- url: servePort === 443 ? `https://${tailscaleStatus.hostname}/` : `https://${tailscaleStatus.hostname}:${servePort}/`
210756
+ url: servePort === 443 ? `https://${tailscaleStatus.hostname}/` : `https://${tailscaleStatus.hostname}:${servePort}/`,
210757
+ servePort
210730
210758
  };
210731
210759
  });
210732
210760
  /**
@@ -210756,17 +210784,20 @@ const teardownServe = (servePort) => gen(function* () {
210756
210784
  * so that function's own cyclomatic/cognitive complexity stays flat (mirrors
210757
210785
  * why `resolveHostsAndCert` was pulled out for Package 02): an explicit
210758
210786
  * `--host`/`ui.host` or `--self-signed` skips serve entirely (step 1);
210759
- * otherwise `attemptServe` is tried first, its own failure reason printed
210760
- * above the URL before falling back (step 3) — never a refusal either way.
210787
+ * otherwise Task 2's candidate walk is tried first — a one-element list when
210788
+ * `explicitPort` was given, `SERVE_PORT_CANDIDATES` otherwise — printing the
210789
+ * LAST candidate's failure reason (prefixed with every port tried, when more
210790
+ * than one was) above the URL before falling back — never a refusal either
210791
+ * way.
210761
210792
  */
210762
210793
  const resolveListener = (args) => gen(function* () {
210763
- const { options, config, port, explicitHost, worktree, uiListener, handler, out } = args;
210794
+ const { options, config, explicitPort, explicitHost, worktree, uiListener, handler, out } = args;
210764
210795
  const directBind = () => gen(function* () {
210765
210796
  const { bindHost, displayHost, certPair } = yield* resolveHostsAndCert(options, config);
210766
210797
  const bound = yield* uiListener.listen({
210767
210798
  tls: certPair,
210768
210799
  host: bindHost,
210769
- port,
210800
+ port: explicitPort ?? 0,
210770
210801
  handler
210771
210802
  });
210772
210803
  return {
@@ -210782,13 +210813,24 @@ const resolveListener = (args) => gen(function* () {
210782
210813
  teardown: _void
210783
210814
  };
210784
210815
  }
210785
- const attempt = yield* attemptServe(port, worktree, uiListener, handler);
210786
- if (attempt.ok) return {
210787
- bound: attempt.bound,
210788
- url: attempt.url,
210789
- teardown: teardownServe(port)
210790
- };
210791
- out.write(`gtd ui: not using tailscale serve — ${attempt.reason}\n`);
210816
+ const candidates = explicitPort !== void 0 ? [explicitPort] : SERVE_PORT_CANDIDATES;
210817
+ const tried = [];
210818
+ for (const candidate of candidates) {
210819
+ const attempt = yield* attemptServe(candidate, worktree, uiListener, handler);
210820
+ if (attempt.ok) return {
210821
+ bound: attempt.bound,
210822
+ url: attempt.url,
210823
+ teardown: teardownServe(attempt.servePort)
210824
+ };
210825
+ tried.push({
210826
+ port: candidate,
210827
+ reason: attempt.reason
210828
+ });
210829
+ if (!attempt.retry) break;
210830
+ }
210831
+ const last = tried[tried.length - 1];
210832
+ const reason = tried.length > 1 ? `tried ${tried.map((t) => t.port).join(", ")} — ${last.reason}` : last.reason;
210833
+ out.write(`gtd ui: not using tailscale serve — ${reason}\n`);
210792
210834
  const { bound, url } = yield* directBind();
210793
210835
  return {
210794
210836
  bound,
@@ -210815,7 +210857,7 @@ const runUiCommand = (options, config, out) => gen(function* () {
210815
210857
  const fs = yield* FileSystem;
210816
210858
  const uiListener = yield* UiListener;
210817
210859
  const runtime$5 = yield* runtime();
210818
- const port = options.port ?? config?.port ?? DEFAULT_PORT;
210860
+ const explicitPort = normalizePort(options.port ?? config?.port);
210819
210861
  const explicitHost = options.host ?? config?.host;
210820
210862
  const clientHtml = yield* resolveClientHtml(options.dev, runner, fs);
210821
210863
  const writeDeps = {
@@ -210894,7 +210936,7 @@ const runUiCommand = (options, config, out) => gen(function* () {
210894
210936
  const { bound, url, teardown } = yield* resolveListener({
210895
210937
  options,
210896
210938
  config,
210897
- port,
210939
+ explicitPort,
210898
210940
  explicitHost,
210899
210941
  worktree: cwd.root,
210900
210942
  uiListener,
@@ -225915,7 +225957,8 @@ const FLAGS = [
225915
225957
  help: [
225916
225958
  "(gtd visualize/gtd ui only) port to serve on: a free port",
225917
225959
  "for visualize; for ui, the tailscale serve port (default:",
225918
- "8443), or the bind port when --host opts out of serve"
225960
+ "the first free of 8443, 10000, 443), or the bind port when",
225961
+ "--host opts out of serve (default: a free port)"
225919
225962
  ]
225920
225963
  },
225921
225964
  {
@@ -226196,12 +226239,14 @@ const COMMAND_ROWS = [
226196
226239
  "refuses outside a repository like every other state command.",
226197
226240
  "By default, publishes through `tailscale serve` (reachable",
226198
226241
  "from anywhere on the tailnet, including over a DERP relay)",
226199
- "with tailscaled terminating TLS; falls back to binding a",
226200
- "local HTTPS server directly, never refusing, when serve",
226201
- "isn't available. --host <addr> opts out of serve and binds",
226202
- "that address directly instead; --port <n> overrides the",
226203
- "serve port (default: 8443), or the bind port when --host is",
226204
- "given; --self-signed also opts out of serve, generating a",
226242
+ "with tailscaled terminating TLS, walking 8443, then 10000,",
226243
+ "then 443 until one publishes; falls back to binding a local",
226244
+ "HTTPS server directly, never refusing, when none of the",
226245
+ "three works. --host <addr> opts out of serve and binds that",
226246
+ "address directly instead; --port <n> overrides the serve",
226247
+ "port (default: the first free of 8443, 10000, 443), or the",
226248
+ "bind port when --host is given (default: a free port);",
226249
+ "--self-signed also opts out of serve, generating a",
226205
226250
  "throwaway TLS certificate instead of the configured",
226206
226251
  "ui.cert/ui.key; --dev runs against local development sources",
226207
226252
  "instead of the packaged build"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmelab/gtd",
3
- "version": "10.8.0",
3
+ "version": "10.9.0",
4
4
  "private": false,
5
5
  "description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
6
6
  "bin": {
package/schema.json CHANGED
@@ -278,7 +278,7 @@
278
278
  "properties": {
279
279
  "port": {
280
280
  "type": "integer",
281
- "description": "TCP port `gtd ui` listens on."
281
+ "description": "For ui, the tailscale serve port (default: the first free of 8443, 10000, 443), or the bind port when --host opts out of serve (default: a free port)."
282
282
  },
283
283
  "host": {
284
284
  "type": "string",