jackherd 1.7.0 โ 1.7.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.
- package/CHANGELOG.md +5 -0
- package/INSTALL.md +2 -2
- package/README.md +2 -2
- package/herdr-plugin/herdr-plugin.toml +1 -1
- package/install.sh +1 -1
- package/package.json +1 -1
- package/src/config/legacy-names.ts +8 -0
- package/src/sync/command.ts +16 -1
- package/src/update/command.ts +4 -2
- package/src/update/update.ts +33 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
What changed for people who use jackherd, newest first.
|
|
4
4
|
|
|
5
|
+
## 1.7.1
|
|
6
|
+
|
|
7
|
+
- **`jh update` no longer installs the kit into a project.** It synced every project with a `.jackherd/config.json`, including one that holds only settings for the global install, and so wrote kit files, hooks in `.claude/settings.json`, a `.gitignore` line and `.jackherd/lock.json` there unasked. Now it syncs only a project with an install of its own (`.jackherd/lock.json`, or the lock from before the rename); for any other it prints one line that it was not synced and that `jh init` installs the kit there.
|
|
8
|
+
- **`jh sync` refuses a project without an install.** In a directory with a `.jackherd/config.json` but no project install it used to install the kit like `jh init`. Now it exits 2, writes nothing and names `jh init` (installs the kit here) and `jh sync --global` (updates the global kit). `jh sync --check` refuses the same way; `jh init` is unchanged.
|
|
9
|
+
|
|
5
10
|
## 1.7.0
|
|
6
11
|
|
|
7
12
|
- **Tickets show beside their plan.** A `watch.sources` entry with `"ticket": true` marks its entries as tickets, and the herd shows each as a badge in the ๐ line of its lane's plan, right after `๐ Plan`: `๐ก T-123 ยท In Progress`, linked to the ticket. A ticket belongs to the source's `lane`, and to any lane whose name, board row or plan names its id as a whole word, so one source of all your open tickets needs no `lane`. The status comes from the new field `map.status`; without it the badge shows the key alone. A ticket whose last change was reported within a day is amber, and a folded lane then shows `๐ก <n>` in amber. At most three per lane, new ones first, then `+<n>`; a link chip with the same URL gives way to the badge. The badges come from the watcher's saved state, which now keeps URL, status and change time of a marked source's entries; without a running watcher there are none, and the header says so. `/follow` asks whether a source holds tickets and finds the path of their status itself, for you to approve with the entry.
|
package/INSTALL.md
CHANGED
|
@@ -58,7 +58,7 @@ When `init` creates a new config, it asks:
|
|
|
58
58
|
| Config | `.jackherd/config.json` | `~/.jackherd/config.json`, used where a repository has none |
|
|
59
59
|
| Update | `jh sync` | `jh update` |
|
|
60
60
|
|
|
61
|
-
A project config always wins over the global one. Both can exist side by side: in a project with its own kit, the project's skills are the ones in use.
|
|
61
|
+
A project config always wins over the global one. Both can exist side by side: in a project with its own kit, the project's skills are the ones in use. A `.jackherd/config.json` without a project install (no `.jackherd/lock.json`) holds settings for the global install: `jh sync` refuses to run there and names `jh init`, which installs the kit, and `jh sync --global`, which updates the global one; `jh update` leaves the project alone.
|
|
62
62
|
|
|
63
63
|
## Project install
|
|
64
64
|
|
|
@@ -137,7 +137,7 @@ The global skills cannot carry one repository's values, so `sync --global` write
|
|
|
137
137
|
|
|
138
138
|
## Updating
|
|
139
139
|
|
|
140
|
-
`jh update` asks npm for the newest release and, when there is one, installs exactly that version, from your home directory and without install scripts, with the package manager that installed the kit globally (bun, npm, pnpm or yarn, read from where the package lies), then syncs `~/.claude` and, inside a project, the project with the freshly installed code. It prints the old and the new version. A kit that is a project dependency or a source checkout is not a global install, and `update` says so and changes nothing; there, update the dependency and run `jh sync`.
|
|
140
|
+
`jh update` asks npm for the newest release and, when there is one, installs exactly that version, from your home directory and without install scripts, with the package manager that installed the kit globally (bun, npm, pnpm or yarn, read from where the package lies), then syncs `~/.claude` and, inside a project with its own install (`.jackherd/lock.json`), the project with the freshly installed code. A project that has only a `.jackherd/config.json` for the global install is not synced; one line says so, and `jh init` installs the kit there. It prints the old and the new version. A kit that is a project dependency or a source checkout is not a global install, and `update` says so and changes nothing; there, update the dependency and run `jh sync`.
|
|
141
141
|
|
|
142
142
|
A running herd restarts itself with the new code, and a running `watch` prints `=== kit updated <old> โ <new>` and ends, so the coordinator reads its skill again and starts the stream again.
|
|
143
143
|
|
package/README.md
CHANGED
|
@@ -1023,8 +1023,8 @@ At `warn` a lane offers a fresh chat at its next phase boundary, and the watcher
|
|
|
1023
1023
|
| ------- | ------------ | ------- |
|
|
1024
1024
|
| `jh --version` | The installed version, e.g. `1.6.0`; `-v` is the same | none |
|
|
1025
1025
|
| `jh init` | Creates `.jackherd/config.json` and installs the kit | `--global`: into `~/.claude` and `~/.jackherd`; `--yes`/`--no`: answer all its questions (globally the hook and the status line, in a project the prefix and the status line) and take the default [setup](https://unpkg.com/jackherd/INSTALL.md); a status line of your own is replaced only by a yes in the dialog |
|
|
1026
|
-
| `jh sync` | Updates the kit files; edited files get a `.kit-new` beside them | `--global`; `--accept <path>` takes the kit's version; `--global --if-changed` syncs only after a package update and names a newer npm release once a day |
|
|
1027
|
-
| `jh update` | Installs the newest release with the package manager that holds the kit globally, then syncs `~/.claude` and the project with the new code | none; does nothing when the installed version is the latest |
|
|
1026
|
+
| `jh sync` | Updates the kit files; edited files get a `.kit-new` beside them; refuses a project that has a config but no install of its own | `--global`; `--accept <path>` takes the kit's version; `--global --if-changed` syncs only after a package update and names a newer npm release once a day |
|
|
1027
|
+
| `jh update` | Installs the newest release with the package manager that holds the kit globally, then syncs `~/.claude` and, when it has its own install, the project with the new code | none; does nothing when the installed version is the latest |
|
|
1028
1028
|
| `jh config` | The values the current repository decides (`test`, `typecheck`, `lint`, `format`, `install`, `dev`, `lockFile`, `baseBranch`), as set or detected | `<name>`: one bare value |
|
|
1029
1029
|
| `jh check` | Types, lint and tests of the packages you touched, plus the project's own checks; in changed plans, an open checkbox whose `โ P<n>` is not at the end of the line fails, and so does a phase the change adds with tasks but without a review gate (or a `**No review gate:** <reason>` line); in a changed board, a lane row the change adds or edits whose phases cell names no `P<n>` warns; so does one whose phases cell is longer than 250 characters or records a side order of the user (`also (User โฆ`), which belongs as a task in the plan's phase; a phase of a changed plan without a row in its dependency table warns; in a changed plan, a phase whose tasks are all ticked on `origin/<baseBranch>`, that the change does not tick further and that still has open boxes without `โ P<n>` warns | `--skip-slow`: lint and fast checks only; `--parallel <plan>`: only the groups of phases that can run side by side |
|
|
1030
1030
|
| `jh herd` | Live view of the board's lanes and plans ([Herd](#user-content-herd-and-context)) | `--once`: one frame and exit; `--lane <name>`: one lane; `--ensure`: bring back the missing herd panes beside the coordinator and each lane, and exit; a board path (default: the newest board) |
|
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.7.
|
|
12
|
+
JACKHERD_VERSION="${JACKHERD_VERSION:-1.7.1}"
|
|
13
13
|
|
|
14
14
|
JACKHERD_PACKAGE='jackherd'
|
|
15
15
|
# Test seam: the terminal the wizard asks its questions on.
|
package/package.json
CHANGED
|
@@ -21,6 +21,14 @@ export const PROJECT_LEGACY_FILES: readonly Rename[] = [
|
|
|
21
21
|
{ from: LEGACY_MANIFEST_PATH, to: `${TOOL_DIRECTORY}/${MANIFEST_FILE}` },
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
/** The lock files that mark a project install in `root`, the old name counting until 2.0.0 like the old config. */
|
|
25
|
+
export const projectLockCandidates = (root: string): string[] =>
|
|
26
|
+
[`${TOOL_DIRECTORY}/${MANIFEST_FILE}`, LEGACY_MANIFEST_PATH].map((path) => join(root, path))
|
|
27
|
+
|
|
28
|
+
/** True when `root` holds a project install. A lock counts when it exists; a broken one is the sync's to report. */
|
|
29
|
+
export const hasProjectInstall = (root: string, exists: (path: string) => boolean): boolean =>
|
|
30
|
+
projectLockCandidates(root).some(exists)
|
|
31
|
+
|
|
24
32
|
export const PUBLIC_CONTENT_PATH = `${TOOL_DIRECTORY}/public-content.json`
|
|
25
33
|
|
|
26
34
|
/** Where the public-content facts lay before the rename: below `XDG_CONFIG_HOME`, else `~/.config`. */
|
package/src/sync/command.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { hasProjectInstall } from '../config/legacy-names'
|
|
3
|
+
import { configCandidates } from '../config/location'
|
|
4
|
+
import { CLI_NAME, PACKAGE_NAME } from '../version/release'
|
|
2
5
|
import {
|
|
3
6
|
type CommandContext,
|
|
4
7
|
defaultContext,
|
|
@@ -62,6 +65,16 @@ export const parseSyncArgs = (args: string[]): SyncArgs => {
|
|
|
62
65
|
return { ok: true, accept, global, ifChanged, check }
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* True when `root` has a project config but no project install: the config holds settings for the global install,
|
|
70
|
+
* and only `jh init` may turn it into a project install.
|
|
71
|
+
*/
|
|
72
|
+
const isConfigWithoutInstall = (root: string, exists: (path: string) => boolean): boolean =>
|
|
73
|
+
configCandidates(root).some(exists) && !hasProjectInstall(root, exists)
|
|
74
|
+
|
|
75
|
+
const notInstalledLine = (root: string): string =>
|
|
76
|
+
`${CLI_NAME} sync: ${root} has a ${PACKAGE_NAME} config but no project install, so nothing was synced. \`${CLI_NAME} init\` installs the kit here; \`${CLI_NAME} sync ${GLOBAL_FLAG}\` updates the global kit.`
|
|
77
|
+
|
|
65
78
|
const syncGlobal = (context: CommandContext, accept: string[]): Promise<number> =>
|
|
66
79
|
runWithKit(context, 'sync', async (kit) => {
|
|
67
80
|
const result = await runGlobalSync({ home: context.home, kit, accept, configHome: context.configHome })
|
|
@@ -98,6 +111,8 @@ export const runSyncCommand = async (
|
|
|
98
111
|
const { accept, global, ifChanged, check: dryRun } = parsed
|
|
99
112
|
if (ifChanged) return syncGlobalIfChanged(context, accept, check)
|
|
100
113
|
if (global) return syncGlobal(context, accept)
|
|
114
|
+
if (isConfigWithoutInstall(context.projectRoot, existsSync))
|
|
115
|
+
return usageError(context, notInstalledLine(context.projectRoot))
|
|
101
116
|
return runWithKit(
|
|
102
117
|
context,
|
|
103
118
|
'sync',
|
package/src/update/command.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `jh update`: installs the newest release with the package manager that holds the kit globally,
|
|
3
|
-
* then syncs `~/.claude` (and the project, when the working directory is inside one
|
|
4
|
-
* runs only when the user or a session that asked the user starts it; no hook runs it.
|
|
3
|
+
* then syncs `~/.claude` (and the project, when the working directory is inside one with its own install) with
|
|
4
|
+
* the new code. It runs only when the user or a session that asked the user starts it; no hook runs it.
|
|
5
5
|
*
|
|
6
6
|
* jh update
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, realpathSync } from 'node:fs'
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
|
+
import { hasProjectInstall } from '../config/legacy-names'
|
|
11
12
|
import { findProjectRoot } from '../config/location'
|
|
12
13
|
import { defaultUpdateCheck, lockedVersion } from '../sync/if-changed'
|
|
13
14
|
import { locateKit } from '../sync/kit-source'
|
|
@@ -50,6 +51,7 @@ const adaptersFor = async (): Promise<UpdateAdapters> => {
|
|
|
50
51
|
jhOnPath: () => Bun.which(CLI_NAME) !== null,
|
|
51
52
|
home,
|
|
52
53
|
projectRoot: findProjectRoot(process.cwd(), home, existsSync),
|
|
54
|
+
hasProjectKit: (root) => hasProjectInstall(root, existsSync),
|
|
53
55
|
log: (line) => console.log(line),
|
|
54
56
|
}
|
|
55
57
|
}
|
package/src/update/update.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLI_NAME, isNewerVersion, isRelease } from '../version/release'
|
|
1
|
+
import { CLI_NAME, isNewerVersion, isRelease, PACKAGE_NAME } from '../version/release'
|
|
2
2
|
import { installCommand, LEGACY_PACKAGE_NAME, type PackageManager, removeCommand } from './package-manager'
|
|
3
3
|
|
|
4
4
|
export const UPDATE_EXIT = { ok: 0, error: 1 } as const
|
|
@@ -21,8 +21,13 @@ export type UpdateAdapters = {
|
|
|
21
21
|
/** Whether a `jh` command is on the path; false when only the `@crossdelta/agent-kit` shell carries this jackherd. Asked before and after the install. */
|
|
22
22
|
jhOnPath: () => boolean
|
|
23
23
|
home: string
|
|
24
|
-
/** The project the working directory belongs to, or null. */
|
|
24
|
+
/** The project the working directory belongs to (the nearest directory with a project config), or null. */
|
|
25
25
|
projectRoot: string | null
|
|
26
|
+
/**
|
|
27
|
+
* True when `root` holds a project install (a lock of its own); a config alone is settings for the global kit.
|
|
28
|
+
* The lock's existence counts, not its validity: a broken lock is synced, and the sync names the error.
|
|
29
|
+
*/
|
|
30
|
+
hasProjectKit: (root: string) => boolean
|
|
26
31
|
log: (line: string) => void
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -34,10 +39,20 @@ const failed = (log: UpdateAdapters['log'], step: string, command: string): numb
|
|
|
34
39
|
return UPDATE_EXIT.error
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
const notInstalledLine = (projectRoot: string): string =>
|
|
43
|
+
`${projectRoot} has a ${PACKAGE_NAME} config but no project install, so it was not synced; \`${CLI_NAME} init\` installs the kit there.`
|
|
44
|
+
|
|
45
|
+
/** The project `jh update` syncs: only one with an install of its own, since only `jh init` may create one. */
|
|
46
|
+
const installedProject = (adapters: UpdateAdapters): string | null =>
|
|
47
|
+
adapters.projectRoot && adapters.hasProjectKit(adapters.projectRoot) ? adapters.projectRoot : null
|
|
48
|
+
|
|
37
49
|
/** The syncs that bring `~/.claude` and the project to the new files, in the order they run. */
|
|
38
|
-
const syncSteps = async (
|
|
50
|
+
const syncSteps = async (
|
|
51
|
+
adapters: UpdateAdapters,
|
|
52
|
+
project: string | null,
|
|
53
|
+
): Promise<{ argv: string[]; cwd: string }[]> => [
|
|
39
54
|
...((await adapters.hasGlobalKit()) ? [{ argv: [...adapters.kitCli, 'sync', '--global'], cwd: adapters.home }] : []),
|
|
40
|
-
...(
|
|
55
|
+
...(project ? [{ argv: [...adapters.kitCli, 'sync'], cwd: project }] : []),
|
|
41
56
|
]
|
|
42
57
|
|
|
43
58
|
/**
|
|
@@ -59,6 +74,14 @@ const installAndVerify = async (
|
|
|
59
74
|
return UPDATE_EXIT.error
|
|
60
75
|
}
|
|
61
76
|
|
|
77
|
+
/** Runs the syncs with the freshly installed code; the first that fails stops the rest and names its command. */
|
|
78
|
+
const syncAfterInstall = async (adapters: UpdateAdapters, project: string | null): Promise<number> => {
|
|
79
|
+
for (const { argv, cwd } of await syncSteps(adapters, project)) {
|
|
80
|
+
if ((await adapters.run(argv, cwd)) !== 0) return failed(adapters.log, 'the sync', argv.join(' '))
|
|
81
|
+
}
|
|
82
|
+
return UPDATE_EXIT.ok
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
const doneLine = (adapters: UpdateAdapters, manager: PackageManager, latest: string, jhWasOnPath: boolean): string => {
|
|
63
86
|
if (jhWasOnPath) return `${CLI_NAME} updated ${adapters.installedVersion} โ ${latest}.`
|
|
64
87
|
if (adapters.jhOnPath()) return `${CLI_NAME} ${latest} installed globally.`
|
|
@@ -88,10 +111,11 @@ const updateToLatest = async (adapters: UpdateAdapters, manager: PackageManager)
|
|
|
88
111
|
}
|
|
89
112
|
const installed = await installAndVerify(adapters, manager, latest)
|
|
90
113
|
if (installed !== UPDATE_EXIT.ok) return installed
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
const project = installedProject(adapters)
|
|
115
|
+
const synced = await syncAfterInstall(adapters, project)
|
|
116
|
+
if (synced !== UPDATE_EXIT.ok) return synced
|
|
94
117
|
log(doneLine(adapters, manager, latest, jhWasOnPath))
|
|
118
|
+
if (adapters.projectRoot && !project) log(notInstalledLine(adapters.projectRoot))
|
|
95
119
|
return UPDATE_EXIT.ok
|
|
96
120
|
}
|
|
97
121
|
|
|
@@ -99,8 +123,8 @@ const legacyPackageLine = (manager: PackageManager): string =>
|
|
|
99
123
|
`${LEGACY_PACKAGE_NAME} is still installed globally; ${CLI_NAME} replaces it. Remove it with: ${removeCommand(manager, LEGACY_PACKAGE_NAME).join(' ')}`
|
|
100
124
|
|
|
101
125
|
/**
|
|
102
|
-
* Installs the newest release with the manager that holds the kit globally, then syncs `~/.claude` and
|
|
103
|
-
* project with the freshly installed code; a global package from before the rename is named, not removed.
|
|
126
|
+
* Installs the newest release with the manager that holds the kit globally, then syncs `~/.claude` and an
|
|
127
|
+
* installed project with the freshly installed code; a global package from before the rename is named, not removed.
|
|
104
128
|
*/
|
|
105
129
|
export const runUpdate = async (adapters: UpdateAdapters): Promise<number> => {
|
|
106
130
|
const { manager, log } = adapters
|