jackherd 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +18 -18
- package/dist/config.d.ts +1 -0
- package/dist/config.js +3 -1
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/kit/workflow/skills/orchestrate/references/start-lane.md +3 -3
- package/package.json +1 -1
- package/schemas/jackherd.schema.json +5 -0
- package/src/config/schema.ts +2 -0
- package/src/lane/command.ts +3 -1
- package/src/lane/port-command.ts +37 -0
- package/src/lane/port.ts +56 -0
- package/src/plans/settings.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
What changed for people who use jackherd, newest first.
|
|
4
4
|
|
|
5
|
+
## 1.20.0
|
|
6
|
+
|
|
7
|
+
- **`jh lane port` picks a lane's dev-server port.** It prints the lowest port from 3100 that nothing listens on and that no running lane holds in the port column of the board (`--board <board.md>` names the board, else it is picked as for `jh herd`). Rows of cleaned-up lanes reserve nothing, so ports no longer climb. `lanes.basePort` in `.jackherd/config.json` moves the start. Run `jh sync` to get the skill text.
|
|
8
|
+
|
|
5
9
|
## 1.19.0
|
|
6
10
|
|
|
7
11
|
- **The coordinator clears its context at quiet moments.** The watcher prints `coordinator clear <n>k` once your coordinator's context reaches `plans.context.coordinator.clearTokens` (250 000 by default), or `clearAfterMergeTokens` (150 000) after a merge with the board written afterwards, and only when no dialog is open, nothing unread waits, no lane is blocked or has a question not yet shown, no approval waits and no PR with self-merge is open. Without your own `clearTokens` a lower `warnTokens` moves it just below that; one you set at or above `warnTokens` is refused. `coordinator warn` now points to clearing, and the hand-over with a note stays for `coordinator alert`. Run `jh sync` to get the skill texts.
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p>
|
|
2
|
-
<img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.
|
|
2
|
+
<img alt="jackherd: a black and white Shih Tzu in a turquoise bandana, three lanes running from him to three agent nodes" src="https://unpkg.com/jackherd@1.20.0/assets/branding/jackherd-logo-outline.svg" width="340">
|
|
3
3
|
<br>
|
|
4
4
|
</p>
|
|
5
5
|
|
|
6
|
-
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies) [](https://unpkg.com/jackherd/INSTALL.md) [](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
6
|
+
[](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd) [](https://www.npmjs.com/package/jackherd?activeTab=dependencies) [](https://unpkg.com/jackherd/INSTALL.md) [](https://unpkg.com/jackherd/INSTALL.md)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.20.0/assets/icons/space.svg">
|
|
7
7
|
|
|
8
8
|
# Jack herds your coding agents.
|
|
9
9
|
|
|
@@ -15,29 +15,29 @@ For developers who'd rather decide than supervise.
|
|
|
15
15
|
curl -fsSL https://unpkg.com/jackherd/install.sh | sh
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
18
|
+
Then `/jh-start` in a repository, and Jack offers your first plan. → [Quickstart](#quickstart)<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.20.0/assets/icons/space.svg">
|
|
19
19
|
|
|
20
20
|
<p>
|
|
21
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
21
|
+
<img src="https://unpkg.com/jackherd@1.20.0/docs/workflow-dark.svg" width="720" alt="jackherd in herdr: a lane hits a question, Jack asks you as a choice, you pick, and the lane works on while the herd view counts its progress.">
|
|
22
22
|
<br>
|
|
23
|
-
<sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
23
|
+
<sub>Jack and his lanes in <a href="https://herdr.dev/">herdr</a>, a terminal for coding agents (recommended). Without it, one Claude Code session builds with subagents.</sub><br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.20.0/assets/icons/space.svg">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
26
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/lanes.svg" width="24" height="34" align="middle" alt=""> <b>Parallel where it pays.</b><br>Phases that don't collide run side by side, each lane on its own branch and worktree. Jack keeps the merge order and has the others rebase.</p>
|
|
27
27
|
|
|
28
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
28
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/decide.svg" width="24" height="34" align="middle" alt=""> <b>You make the calls.</b><br>Commits, pushes and merges wait for your approval. Jack merges on green checks only where you said so: one pull request, or a plan's for up to 24 hours. The guards catch mistakes, not intent; a hard boundary takes branch protection.</p>
|
|
29
29
|
|
|
30
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
30
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/bark.svg" width="24" height="34" align="middle" alt=""> <b>Jack barks.</b><br>A lane gets stuck, a check goes red, a decision needs you: Jack steps in and brings you only what's yours to decide. The herd view shows where every lane stands.</p>
|
|
31
31
|
|
|
32
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
32
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/skills.svg" width="24" height="34" align="middle" alt=""> <b>Works with your skills.</b><br>Keep your own skills: a lane builds with whichever one you pick. Plans written elsewhere run too, once Jack fills in what they're missing.</p>
|
|
33
33
|
|
|
34
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
34
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/ear.svg" width="24" height="34" align="middle" alt=""> <b>Jack listens.</b><br>Review comments on a lane's pull request go back to that lane, which fixes them with a test. <code>/jh-follow</code> brings in anything a CLI or an API can read as JSON: tickets, error logs, status pages.</p>
|
|
35
35
|
|
|
36
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
36
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/model.svg" width="24" height="34" align="middle" alt=""> <b>The right model for each phase.</b><br>Every phase names its model: the stronger one where a mistake is expensive (data, security, a migration), the cheaper one for the rest. Lanes start on it, and a hook refuses to start a phase's subagent on a weaker one.</p>
|
|
37
37
|
|
|
38
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
38
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/bowl.svg" width="24" height="34" align="middle" alt=""> <b>A crash or a full context doesn't cost you the work.</b><br>Plan, board and a hand-over file hold the state, not the chat: the next session picks up where the last one stopped.</p>
|
|
39
39
|
|
|
40
|
-
<p><img src="https://unpkg.com/jackherd@1.
|
|
40
|
+
<p><img src="https://unpkg.com/jackherd@1.20.0/assets/icons/plan.svg" width="24" height="34" align="middle" alt=""> <b>Lives in your repository.</b><br>Plans, board and every decision are Markdown, reviewed like code. <code>jh init</code> gives your team the same setup.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.20.0/assets/icons/space.svg"></p>
|
|
41
41
|
|
|
42
42
|
## Quickstart
|
|
43
43
|
|
|
@@ -68,22 +68,22 @@ Then `/jh-start` in a repository, and Jack offers your first plan. → [Quicksta
|
|
|
68
68
|
/jh-orchestrate docs/plans/board-<name>/board.md
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Linux and macOS are supported (Windows through WSL); the requirements are in [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md). By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
71
|
+
Linux and macOS are supported (Windows through WSL); the requirements are in [INSTALL.md](https://unpkg.com/jackherd/INSTALL.md). By hand (Bun 1.3+): `bun add -g jackherd` or `npm install -g jackherd`, then `jh init --global` for yourself, or `jh init` in a repository to give your team the same skills and guard hooks.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.20.0/assets/icons/space.svg">
|
|
72
72
|
|
|
73
73
|
## Follow anything you can query
|
|
74
74
|
|
|
75
75
|
Tickets, error logs, status pages: if a command or an API call can read it as JSON with your login, Jack can watch it. Recipes cover Jira, Linear, GitHub issues and Projects, Sentry, Google Calendar and status pages; for anything else, `/jh-follow` writes the command with you. One yes, and every change shows up like this:
|
|
76
76
|
|
|
77
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
77
|
+
<img src="https://unpkg.com/jackherd@1.20.0/docs/follow-dark.svg" width="720" alt="A source (ticket, error log or status page) has a new or changed entry; jh watch reports it to Jack; Jack puts a row into Your turn, prefixed with the source's label; its [ask] button makes Jack ask you, and then a lane works it.">
|
|
78
78
|
|
|
79
|
-
The herd sums up your sources in one `Sources` line: new entries per source, or its last run, red while it fails; opened, a click opens an entry. One that needs you lands in Your turn under the source's label, [ask] has Jack look at it, and a ticket shows on the lane that works it.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.
|
|
79
|
+
The herd sums up your sources in one `Sources` line: new entries per source, or its last run, red while it fails; opened, a click opens an entry. One that needs you lands in Your turn under the source's label, [ask] has Jack look at it, and a ticket shows on the lane that works it.<br><img width="1" height="8" alt="" src="https://unpkg.com/jackherd@1.20.0/assets/icons/space.svg">
|
|
80
80
|
|
|
81
81
|
## In detail
|
|
82
82
|
|
|
83
83
|
<details>
|
|
84
84
|
<summary><b>What Jack does on his own, and what waits for you</b></summary>
|
|
85
85
|
|
|
86
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
86
|
+
<img src="https://unpkg.com/jackherd@1.20.0/docs/lanes-dark.svg" alt="Jack prompts two lanes and asks you for decisions and approvals; the lanes send questions back as text; jh watch reads lane state, pull requests and checks and wakes Jack.">
|
|
87
87
|
|
|
88
88
|
| On his own | After your approval | Never |
|
|
89
89
|
| --- | --- | --- |
|
|
@@ -118,7 +118,7 @@ The guards catch mistakes, not intent; for that, use branch protection. The hook
|
|
|
118
118
|
|
|
119
119
|
A **plan** is Markdown with phases, checkbox tasks, a model and a review gate per phase. A **board** lists the lanes of one or more plans: which phases each lane builds, on which model, after what. A **lane** is one agent session in its own git worktree (a second checkout of your repository), and **Jack** is the session in the main checkout that runs `/jh-orchestrate` on a board.
|
|
120
120
|
|
|
121
|
-
<img src="https://unpkg.com/jackherd@1.
|
|
121
|
+
<img src="https://unpkg.com/jackherd@1.20.0/docs/skills-dark.svg" alt="Your own plan or an issue is adopted (Jack adds what is missing, you see the diff) and goes to /jh-orchestrate; /jh-grill hands decisions to /jh-plan; /jh-plan feeds /jh-orchestrate or a single /jh-implement, which your own skill can replace; /jh-implement ends in /jh-pre-commit, which runs the reviewers; /jh-diagnose hands a red command to /jh-implement.">
|
|
122
122
|
|
|
123
123
|
| Skill | Does |
|
|
124
124
|
| --- | --- |
|
package/dist/config.d.ts
CHANGED
|
@@ -120,6 +120,7 @@ declare const jackherdConfigSchema: z.ZodObject<{
|
|
|
120
120
|
copy: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
121
121
|
lockFile: z.ZodOptional<z.ZodString>;
|
|
122
122
|
minFreeGb: z.ZodOptional<z.ZodNumber>;
|
|
123
|
+
basePort: z.ZodOptional<z.ZodNumber>;
|
|
123
124
|
}, z.core.$strict>>;
|
|
124
125
|
models: z.ZodDefault<z.ZodRecord<z.ZodEnum<{
|
|
125
126
|
review: "review";
|
package/dist/config.js
CHANGED
|
@@ -706,7 +706,9 @@ var jackherdConfigSchema = z2.strictObject({
|
|
|
706
706
|
/** The dependency lock file a rebase takes from the base branch and regenerates, never merges by hand; detected from the repository when left out. */
|
|
707
707
|
lockFile: lockFilePath.optional(),
|
|
708
708
|
/** Free gigabytes the disk must hold before `jh lane space` lets a new lane install its dependencies; 5 when left out. */
|
|
709
|
-
minFreeGb: z2.number().positive().optional()
|
|
709
|
+
minFreeGb: z2.number().positive().optional(),
|
|
710
|
+
/** The port `jh lane port` starts counting from when it picks a lane's dev-server port; 3100 when left out. */
|
|
711
|
+
basePort: z2.number().int().min(1).max(65535).optional()
|
|
710
712
|
}).prefault({}),
|
|
711
713
|
models: z2.record(z2.enum(Object.values(MODEL_ROLE)), modelId).default(DEFAULT_MODELS),
|
|
712
714
|
terminal: z2.strictObject({ adapter: z2.enum(Object.values(TERMINAL_ADAPTER)).default(TERMINAL_ADAPTER.none) }).prefault({}),
|
package/install.sh
CHANGED
|
@@ -9,7 +9,7 @@ set -eu
|
|
|
9
9
|
|
|
10
10
|
# The default is the version this file ships with. The release bump changes it together with
|
|
11
11
|
# package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
|
|
12
|
-
JACKHERD_VERSION="${JACKHERD_VERSION:-1.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.20.0}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
|
@@ -32,7 +32,7 @@ column and the plan's dependency table are met and step 1 finds nothing.
|
|
|
32
32
|
4. **Dependencies**, installed in the worktree's pane: the same file.
|
|
33
33
|
5. **Agent with the plan's model:**
|
|
34
34
|
`herdr agent start <lane> --kind claude --pane <pane> --timeout 120000 -- --model <model-id>`.
|
|
35
|
-
- **The lane's row comes first** in the board's `## {{kit.heading.lanes}}` table: lane, port, plan,
|
|
35
|
+
- **The lane's row comes first** in the board's `## {{kit.heading.lanes}}` table: lane, port (from `jh lane port`), plan,
|
|
36
36
|
phases, starts after, model. The phases cell starts with the phase id (`P10c …`), also for a sub-phase, and
|
|
37
37
|
the plan column links the plan once it exists, in the row of every lane that implements one of its
|
|
38
38
|
phases. Without the `lane-start-guard` hook, `grep -F "| \`<lane>\`" <board>` must print the row.
|
|
@@ -63,5 +63,5 @@ herdr pane run <new-pane> "PORT=<port> {{kit.command.dev}}"
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
The new pane starts in the lane's worktree; check with `/proc/<pid>/cwd` (macOS: `lsof -p <pid>`) that the server runs
|
|
66
|
-
there, and before stopping one by PID. Stop only a server you started;
|
|
67
|
-
|
|
66
|
+
there, and before stopping one by PID. Stop only a server you started; another's port
|
|
67
|
+
goes to the user.
|
package/package.json
CHANGED
package/src/config/schema.ts
CHANGED
|
@@ -603,6 +603,8 @@ export const jackherdConfigSchema = z.strictObject({
|
|
|
603
603
|
lockFile: lockFilePath.optional(),
|
|
604
604
|
/** Free gigabytes the disk must hold before `jh lane space` lets a new lane install its dependencies; 5 when left out. */
|
|
605
605
|
minFreeGb: z.number().positive().optional(),
|
|
606
|
+
/** The port `jh lane port` starts counting from when it picks a lane's dev-server port; 3100 when left out. */
|
|
607
|
+
basePort: z.number().int().min(1).max(65535).optional(),
|
|
606
608
|
})
|
|
607
609
|
.prefault({}),
|
|
608
610
|
models: z.record(z.enum(Object.values(MODEL_ROLE)), modelId).default(DEFAULT_MODELS),
|
package/src/lane/command.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { startLaneHintFor } from './start-lane-hint'
|
|
|
4
4
|
|
|
5
5
|
const CLOSE = 'close'
|
|
6
6
|
const SPACE = 'space'
|
|
7
|
+
const PORT = 'port'
|
|
7
8
|
const SEND = 'send'
|
|
8
9
|
const BASE = 'base'
|
|
9
10
|
|
|
@@ -15,12 +16,13 @@ const withStartLaneHint = (code: number, args: readonly string[], cwd: string):
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* `jh lane close …` closes a lane, `jh lane space …` checks the disk for a new one, `jh lane send …` sends its prompt
|
|
19
|
+
* `jh lane close …` closes a lane, `jh lane space …` checks the disk for a new one, `jh lane port …` names its dev-server port, `jh lane send …` sends its prompt
|
|
19
20
|
* file, `jh lane base …` names the ref its worktree starts from, everything else is `jh lane prompt …`. `close` must
|
|
20
21
|
* come first; `jh lane --board b.md close x` reaches the prompt parser and is refused there.
|
|
21
22
|
*/
|
|
22
23
|
export const run = async (args: string[], cwd: string = process.cwd()): Promise<number> => {
|
|
23
24
|
if (args[0] === SPACE) return (await import('./space-command')).run(args.slice(1), cwd)
|
|
25
|
+
if (args[0] === PORT) return (await import('./port-command')).run(args.slice(1), cwd)
|
|
24
26
|
if (args[0] === SEND) return (await import('./send-command')).run(args.slice(1), cwd)
|
|
25
27
|
if (args[0] === BASE)
|
|
26
28
|
return withStartLaneHint(await (await import('./base-command')).run(args.slice(1), cwd), args.slice(1), cwd)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { parseSprintLanes } from '../plan-format'
|
|
4
|
+
import { terminalSafe } from '../plan-format/terminal-text'
|
|
5
|
+
import { boardFor } from '../plans/board-pick'
|
|
6
|
+
import { mainCheckout, worktreeRoot } from '../plans/git'
|
|
7
|
+
import { isPortListening } from '../plans/port-listening'
|
|
8
|
+
import { readSettings } from '../plans/settings'
|
|
9
|
+
import { asksForHelp } from '../preview/options'
|
|
10
|
+
import { freePortNotListening, LANE_PORT_USAGE, MAX_PORT, parsePortOptions, portsOfRunningLanes } from './port'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `jh lane port [--board <board.md>]`: prints the dev-server port for a new lane, the lowest from `lanes.basePort` that
|
|
14
|
+
* nothing listens on and no running lane of the board holds. Exit 0 printed, 1 on a wrong line, an unreadable board or
|
|
15
|
+
* no free port.
|
|
16
|
+
*/
|
|
17
|
+
export const run = async (args: string[], cwd: string = process.cwd()): Promise<number> => {
|
|
18
|
+
if (asksForHelp(args)) {
|
|
19
|
+
console.log(LANE_PORT_USAGE)
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const { boardPath } = parsePortOptions(args)
|
|
24
|
+
const root = mainCheckout(cwd)
|
|
25
|
+
const settings = readSettings([root, worktreeRoot(cwd)])
|
|
26
|
+
const board = boardFor(boardPath === null ? null : resolve(cwd, boardPath), [root], settings, () => [])
|
|
27
|
+
if (board.notice) console.error(terminalSafe(board.notice))
|
|
28
|
+
const reserved = portsOfRunningLanes(parseSprintLanes(readFileSync(board.file, 'utf8'), settings.planLanguage))
|
|
29
|
+
const port = await freePortNotListening(settings.basePort, reserved, MAX_PORT, isPortListening)
|
|
30
|
+
if (port === null) throw new Error(`no free port from ${settings.basePort} to ${MAX_PORT}`)
|
|
31
|
+
console.log(port)
|
|
32
|
+
return 0
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(terminalSafe(error instanceof Error ? error.message : String(error)))
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/lane/port.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SprintLane } from '../plan-format'
|
|
2
|
+
import { CLI_NAME } from '../version/release'
|
|
3
|
+
|
|
4
|
+
/** The port the first lane's dev server gets when the config names none (`lanes.basePort`). */
|
|
5
|
+
export const DEFAULT_BASE_PORT = 3100
|
|
6
|
+
|
|
7
|
+
/** The highest port a machine has. */
|
|
8
|
+
export const MAX_PORT = 65535
|
|
9
|
+
|
|
10
|
+
/** `boardPath` is null where `--board` was left out: the board is then picked as for `jh herd`. */
|
|
11
|
+
export type PortOptions = { boardPath: string | null }
|
|
12
|
+
|
|
13
|
+
const BOARD = '--board'
|
|
14
|
+
|
|
15
|
+
export const LANE_PORT_USAGE = `usage: ${CLI_NAME} lane port [${BOARD} <board.md>]
|
|
16
|
+
|
|
17
|
+
Prints the dev-server port for a new lane: the lowest port from lanes.basePort of the config (default ${DEFAULT_BASE_PORT})
|
|
18
|
+
that nothing listens on and no lane still running holds in the port column of the board. Rows of cleaned-up lanes
|
|
19
|
+
reserve nothing. Exit 1 and one line when no port up to ${MAX_PORT} is free.`
|
|
20
|
+
|
|
21
|
+
/** The lowest port from `base` up to `max` (both included) that is not in `taken`; null when there is none. */
|
|
22
|
+
export const firstFreePort = (base: number, taken: ReadonlySet<number>, max: number): number | null => {
|
|
23
|
+
for (let port = base; port <= max; port += 1) if (!taken.has(port)) return port
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The lowest port from `base` that is neither in `reserved` nor accepts a connection according to `isListening`; the
|
|
29
|
+
* probe is asked only for ports the board does not already hold. Null when none is free up to `max`.
|
|
30
|
+
*/
|
|
31
|
+
export const freePortNotListening = async (
|
|
32
|
+
base: number,
|
|
33
|
+
reserved: ReadonlySet<number>,
|
|
34
|
+
max: number,
|
|
35
|
+
isListening: (port: number) => Promise<boolean>,
|
|
36
|
+
): Promise<number | null> => {
|
|
37
|
+
const taken = new Set(reserved)
|
|
38
|
+
for (let port = firstFreePort(base, taken, max); port !== null; port = firstFreePort(port, taken, max)) {
|
|
39
|
+
if (!(await isListening(port))) return port
|
|
40
|
+
taken.add(port)
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The ports the board's lanes that are not cleaned up hold; a lane without a port in its row reserves none. */
|
|
46
|
+
export const portsOfRunningLanes = (lanes: readonly SprintLane[]): Set<number> =>
|
|
47
|
+
new Set(lanes.flatMap(({ port, closed }) => (closed || port === null ? [] : [port])))
|
|
48
|
+
|
|
49
|
+
/** Reads `[--board <file>]`; throws with the usage on anything else. */
|
|
50
|
+
export const parsePortOptions = (args: readonly string[]): PortOptions => {
|
|
51
|
+
if (args.length === 0) return { boardPath: null }
|
|
52
|
+
const [option, value, ...rest] = args
|
|
53
|
+
if (option !== BOARD || value === undefined || value.startsWith('--') || rest.length > 0)
|
|
54
|
+
throw new Error(LANE_PORT_USAGE)
|
|
55
|
+
return { boardPath: value }
|
|
56
|
+
}
|
package/src/plans/settings.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type JackherdConfig, parseJackherdConfig } from '../config'
|
|
|
4
4
|
import { PROJECT_CONFIG_PATH } from '../config/location'
|
|
5
5
|
import { readConfig } from '../config/read-config'
|
|
6
6
|
import type { ToneName } from '../config/tones'
|
|
7
|
+
import { DEFAULT_BASE_PORT } from '../lane/port'
|
|
7
8
|
import { DEFAULT_MIN_FREE_GB } from '../lane/space'
|
|
8
9
|
import type { TicketSource } from '../watch/plan-badges'
|
|
9
10
|
import type { SourceShow } from '../watch/sources'
|
|
@@ -18,6 +19,8 @@ export type PlansSettings = Pick<
|
|
|
18
19
|
adapter: JackherdConfig['terminal']['adapter']
|
|
19
20
|
/** Free gigabytes a new lane needs on the disk (`lanes.minFreeGb`). */
|
|
20
21
|
minFreeGb: number
|
|
22
|
+
/** The port `jh lane port` starts from (`lanes.basePort`). */
|
|
23
|
+
basePort: number
|
|
21
24
|
fetchIntervalMinutes: number
|
|
22
25
|
/** Whether the running task of a folded lane shimmers; `NO_COLOR` keeps it still whatever this says. */
|
|
23
26
|
shimmer: boolean
|
|
@@ -62,6 +65,7 @@ export const plansSettingsOf = (config: JackherdConfig): PlansSettings => ({
|
|
|
62
65
|
skillPrefix: config.skillPrefix,
|
|
63
66
|
adapter: config.terminal.adapter,
|
|
64
67
|
minFreeGb: config.lanes.minFreeGb ?? DEFAULT_MIN_FREE_GB,
|
|
68
|
+
basePort: config.lanes.basePort ?? DEFAULT_BASE_PORT,
|
|
65
69
|
fetchIntervalMinutes: config.plans.fetchIntervalMinutes,
|
|
66
70
|
shimmer: config.plans.shimmer,
|
|
67
71
|
linkKinds: config.plans.linkKinds,
|