jackherd 0.0.1 → 1.0.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 (250) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +38 -0
  4. package/README.md +1122 -3
  5. package/assets/branding/README.md +62 -0
  6. package/assets/branding/jackherd-icon-256.png +0 -0
  7. package/assets/branding/jackherd-icon-512.png +0 -0
  8. package/assets/branding/jackherd-icon-64.png +0 -0
  9. package/assets/branding/jackherd-icon-small-32.png +0 -0
  10. package/assets/branding/jackherd-icon-small-64.png +0 -0
  11. package/assets/branding/jackherd-icon-small.svg +1 -0
  12. package/assets/branding/jackherd-icon.svg +1 -0
  13. package/assets/branding/jackherd-logo-1024.png +0 -0
  14. package/assets/branding/jackherd-logo-512.png +0 -0
  15. package/assets/branding/jackherd-logo-dark-1024.png +0 -0
  16. package/assets/branding/jackherd-logo-dark-512.png +0 -0
  17. package/assets/branding/jackherd-logo-dark.svg +1 -0
  18. package/assets/branding/jackherd-logo-mark-1024.png +0 -0
  19. package/assets/branding/jackherd-logo-mark-512.png +0 -0
  20. package/assets/branding/jackherd-logo-mark.svg +1 -0
  21. package/assets/branding/jackherd-logo.svg +1 -0
  22. package/dist/chunk-R6MGHCD7.js +93 -0
  23. package/dist/config.d.ts +167 -0
  24. package/dist/config.js +401 -0
  25. package/dist/plan-format.d.ts +325 -0
  26. package/dist/plan-format.js +636 -0
  27. package/docs/lanes.mmd +9 -0
  28. package/docs/lanes.svg +1 -0
  29. package/docs/skills.mmd +10 -0
  30. package/docs/skills.svg +1 -0
  31. package/docs/workflow.svg +172 -0
  32. package/herdr-plugin/herdr-plugin.toml +22 -0
  33. package/herdr-plugin/open-plans.ts +43 -0
  34. package/herdr-plugin/restore-plans.ts +107 -0
  35. package/install.sh +157 -0
  36. package/kit/hooks/hooks/git-guard/decision.ts +438 -0
  37. package/kit/hooks/hooks/git-guard/index.ts +38 -0
  38. package/kit/hooks/hooks/git-guard/shell.ts +407 -0
  39. package/kit/hooks/hooks/watch-guard/background.ts +36 -0
  40. package/kit/hooks/hooks/watch-guard/decision.ts +53 -0
  41. package/kit/hooks/hooks/watch-guard/index.ts +11 -0
  42. package/kit/hooks/hooks/watch-guard/shell-input.ts +32 -0
  43. package/kit/hooks/hooks/watch-guard/starts.ts +154 -0
  44. package/kit/hooks/hooks/watch-guard/text-fallback.ts +40 -0
  45. package/kit/hooks/hooks.json +30 -0
  46. package/kit/hooks/statusline.sh +72 -0
  47. package/kit/review/agents/devops.md +11 -0
  48. package/kit/review/agents/public-content.md +39 -0
  49. package/kit/review/agents/security-audit.md +11 -0
  50. package/kit/review/agents/uncle-bob.md +11 -0
  51. package/kit/review/skills/devops/SKILL.md +50 -0
  52. package/kit/review/skills/diagnose/SKILL.md +140 -0
  53. package/kit/review/skills/diagnose/references/feedback-loops.md +59 -0
  54. package/kit/review/skills/diagnose/scripts/hitl-loop.template.sh +71 -0
  55. package/kit/review/skills/pre-commit/SKILL.md +100 -0
  56. package/kit/review/skills/review/SKILL.md +43 -0
  57. package/kit/review/skills/security-audit/SKILL.md +65 -0
  58. package/kit/review/skills/uncle-bob/SKILL.md +43 -0
  59. package/kit/rules/rules/kit/arc42.md +89 -0
  60. package/kit/rules/rules/kit/architecture-files.md +87 -0
  61. package/kit/rules/rules/kit/code-style.md +159 -0
  62. package/kit/rules/rules/kit/pull-requests.md +59 -0
  63. package/kit/rules/rules/kit/testing.md +91 -0
  64. package/kit/rules/rules/kit/writing-for-agents.md +110 -0
  65. package/kit/workflow/agents/explore.md +25 -0
  66. package/kit/workflow/agents/implement.md +16 -0
  67. package/kit/workflow/agents/plan.md +13 -0
  68. package/kit/workflow/agents/spec-review.md +96 -0
  69. package/kit/workflow/skills/follow/SKILL.md +48 -0
  70. package/kit/workflow/skills/grill/SKILL.md +60 -0
  71. package/kit/workflow/skills/implement/SKILL.md +107 -0
  72. package/kit/workflow/skills/orchestrate/SKILL.md +565 -0
  73. package/kit/workflow/skills/orchestrate/handover/decision.ts +247 -0
  74. package/kit/workflow/skills/orchestrate/handover/index.ts +171 -0
  75. package/kit/workflow/skills/plan/SKILL.md +118 -0
  76. package/kit/workflow/skills/plan/references/template-English.md +92 -0
  77. package/kit/workflow/skills/plan/references/template-German.md +92 -0
  78. package/kit/workflow/skills/start/SKILL.md +47 -0
  79. package/package.json +63 -5
  80. package/schemas/jackherd.schema.json +601 -0
  81. package/src/check/changes.ts +45 -0
  82. package/src/check/command-line.ts +12 -0
  83. package/src/check/command.ts +136 -0
  84. package/src/check/deferral-arrows.ts +41 -0
  85. package/src/check/execute.ts +29 -0
  86. package/src/check/glob.ts +15 -0
  87. package/src/check/plan-structure-check.ts +135 -0
  88. package/src/check/plan.ts +106 -0
  89. package/src/check/public-content-check.ts +32 -0
  90. package/src/check/public-content-facts.ts +177 -0
  91. package/src/check/public-content.ts +216 -0
  92. package/src/check/report.ts +55 -0
  93. package/src/check/result.ts +42 -0
  94. package/src/check/roots.ts +36 -0
  95. package/src/check/routing.ts +22 -0
  96. package/src/check/schema.ts +83 -0
  97. package/src/check/settings.ts +44 -0
  98. package/src/claude/paths.ts +14 -0
  99. package/src/cli.ts +46 -0
  100. package/src/config/command.ts +45 -0
  101. package/src/config/detect.ts +196 -0
  102. package/src/config/index.ts +2 -0
  103. package/src/config/json-schema.ts +5 -0
  104. package/src/config/legacy-names.ts +56 -0
  105. package/src/config/link-kinds.ts +25 -0
  106. package/src/config/location.ts +46 -0
  107. package/src/config/read-config.ts +42 -0
  108. package/src/config/repository-facts.ts +19 -0
  109. package/src/config/repository-values.ts +46 -0
  110. package/src/config/schema.ts +302 -0
  111. package/src/context/command.ts +39 -0
  112. package/src/context/coordinator-name.ts +16 -0
  113. package/src/context/fill.ts +69 -0
  114. package/src/context/index.ts +10 -0
  115. package/src/context/level.ts +22 -0
  116. package/src/context/options.ts +29 -0
  117. package/src/context/report.ts +42 -0
  118. package/src/context/session-log.ts +91 -0
  119. package/src/plan-format/drift.ts +69 -0
  120. package/src/plan-format/header.ts +27 -0
  121. package/src/plan-format/index.ts +9 -0
  122. package/src/plan-format/lanes.ts +128 -0
  123. package/src/plan-format/markdown.ts +98 -0
  124. package/src/plan-format/phase-tally.ts +55 -0
  125. package/src/plan-format/phases.ts +224 -0
  126. package/src/plan-format/plan-structure.ts +150 -0
  127. package/src/plan-format/progress.ts +202 -0
  128. package/src/plan-format/sprint-document.ts +69 -0
  129. package/src/plan-format/terminal-text.ts +28 -0
  130. package/src/plans/board-choice.ts +60 -0
  131. package/src/plans/board-pick.ts +72 -0
  132. package/src/plans/board-rows.ts +62 -0
  133. package/src/plans/command.ts +60 -0
  134. package/src/plans/ensure-panes.ts +71 -0
  135. package/src/plans/error-text.ts +5 -0
  136. package/src/plans/fetch.ts +96 -0
  137. package/src/plans/git.ts +145 -0
  138. package/src/plans/github.ts +35 -0
  139. package/src/plans/lane-discovery.ts +34 -0
  140. package/src/plans/lane-plans.ts +80 -0
  141. package/src/plans/layout.ts +881 -0
  142. package/src/plans/link-chips.ts +74 -0
  143. package/src/plans/live.ts +336 -0
  144. package/src/plans/markdown-html.ts +140 -0
  145. package/src/plans/options.ts +15 -0
  146. package/src/plans/phase-source.ts +71 -0
  147. package/src/plans/plan-chips.ts +75 -0
  148. package/src/plans/plan-panes.ts +206 -0
  149. package/src/plans/planned.ts +117 -0
  150. package/src/plans/pull-requests.ts +107 -0
  151. package/src/plans/restart-decision.ts +42 -0
  152. package/src/plans/selection.ts +352 -0
  153. package/src/plans/settings.ts +68 -0
  154. package/src/plans/single-flight.ts +25 -0
  155. package/src/plans/snapshot.ts +300 -0
  156. package/src/plans/sprint-export.ts +103 -0
  157. package/src/plans/sprint-html.ts +301 -0
  158. package/src/plans/sprint-page.ts +287 -0
  159. package/src/plans/supervisor.ts +124 -0
  160. package/src/plans/symbols.ts +54 -0
  161. package/src/plans/texts.ts +103 -0
  162. package/src/plans/unlisted-lanes.ts +45 -0
  163. package/src/plans/view.ts +68 -0
  164. package/src/plans/worked-phases.ts +53 -0
  165. package/src/sync/checksum.ts +13 -0
  166. package/src/sync/choice.ts +14 -0
  167. package/src/sync/command-context.ts +113 -0
  168. package/src/sync/command.ts +94 -0
  169. package/src/sync/error-message.ts +1 -0
  170. package/src/sync/gitignore.ts +45 -0
  171. package/src/sync/global-kit.ts +176 -0
  172. package/src/sync/herdr-plugin.ts +175 -0
  173. package/src/sync/if-changed.ts +109 -0
  174. package/src/sync/init-command.ts +239 -0
  175. package/src/sync/init.ts +136 -0
  176. package/src/sync/kit-hooks.ts +18 -0
  177. package/src/sync/kit-path.ts +77 -0
  178. package/src/sync/kit-source.ts +169 -0
  179. package/src/sync/kit.ts +9 -0
  180. package/src/sync/legacy-files.ts +116 -0
  181. package/src/sync/manifest.ts +44 -0
  182. package/src/sync/next-steps.ts +42 -0
  183. package/src/sync/plan-file.ts +154 -0
  184. package/src/sync/plan-hooks.ts +165 -0
  185. package/src/sync/project-fs.ts +132 -0
  186. package/src/sync/render.ts +60 -0
  187. package/src/sync/report.ts +96 -0
  188. package/src/sync/settings-hooks.ts +90 -0
  189. package/src/sync/settings-migration.ts +94 -0
  190. package/src/sync/settings-offer.ts +168 -0
  191. package/src/sync/setup-questions.ts +70 -0
  192. package/src/sync/setup.ts +52 -0
  193. package/src/sync/sync-plan.ts +120 -0
  194. package/src/sync/sync-result.ts +9 -0
  195. package/src/sync/sync.ts +307 -0
  196. package/src/sync/tools.ts +52 -0
  197. package/src/terminal/herdr.ts +91 -0
  198. package/src/terminal/index.ts +13 -0
  199. package/src/terminal/none.ts +7 -0
  200. package/src/terminal/open.ts +39 -0
  201. package/src/terminal/text.ts +159 -0
  202. package/src/terminal/types.ts +30 -0
  203. package/src/terminal/width.ts +90 -0
  204. package/src/update/command.ts +65 -0
  205. package/src/update/package-manager.ts +56 -0
  206. package/src/update/update.ts +105 -0
  207. package/src/version/installed-version.ts +32 -0
  208. package/src/version/release.ts +22 -0
  209. package/src/version/skill-diff.ts +63 -0
  210. package/src/version/started-kit.ts +14 -0
  211. package/src/vocabulary/index.ts +132 -0
  212. package/src/watch/base-tip.ts +18 -0
  213. package/src/watch/command.ts +919 -0
  214. package/src/watch/coordinator-fill.ts +76 -0
  215. package/src/watch/feedback-read.ts +73 -0
  216. package/src/watch/feedback.ts +308 -0
  217. package/src/watch/gh.ts +22 -0
  218. package/src/watch/github-read.ts +175 -0
  219. package/src/watch/herdr.ts +11 -0
  220. package/src/watch/json-guards.ts +9 -0
  221. package/src/watch/kit-update.ts +18 -0
  222. package/src/watch/lane-fill.ts +36 -0
  223. package/src/watch/lane-message.ts +99 -0
  224. package/src/watch/lane-progress.ts +44 -0
  225. package/src/watch/lane-report-file.ts +53 -0
  226. package/src/watch/lane-report.ts +270 -0
  227. package/src/watch/lane-search.ts +41 -0
  228. package/src/watch/lane-sources.ts +46 -0
  229. package/src/watch/lane-watch.ts +121 -0
  230. package/src/watch/monitor-tool.ts +19 -0
  231. package/src/watch/options.ts +79 -0
  232. package/src/watch/plan-badges-read.ts +30 -0
  233. package/src/watch/plan-badges.ts +66 -0
  234. package/src/watch/plan-views.ts +59 -0
  235. package/src/watch/preview-read.ts +60 -0
  236. package/src/watch/previews.ts +234 -0
  237. package/src/watch/pull-requests.ts +117 -0
  238. package/src/watch/single-report.ts +15 -0
  239. package/src/watch/skill-change.ts +61 -0
  240. package/src/watch/source-dry-run-command.ts +35 -0
  241. package/src/watch/source-dry-run.ts +124 -0
  242. package/src/watch/source-fields.ts +9 -0
  243. package/src/watch/source-path.ts +40 -0
  244. package/src/watch/source-run.ts +90 -0
  245. package/src/watch/sources.ts +344 -0
  246. package/src/watch/stream-output.ts +14 -0
  247. package/src/watch/texts.ts +76 -0
  248. package/src/watch/watch-events.ts +399 -0
  249. package/src/watch/watcher-lock.ts +145 -0
  250. package/src/watch/workflow-runs.ts +96 -0
@@ -0,0 +1,22 @@
1
+ id = "crossdelta.jackherd"
2
+ name = "jackherd"
3
+ version = "1.0.1"
4
+ min_herdr_version = "0.8.2"
5
+ description = "Show the plans of a board's lanes in a herdr pane"
6
+ platforms = ["linux", "macos"]
7
+
8
+ [[panes]]
9
+ id = "plans"
10
+ title = "jackherd plans"
11
+ placement = "split"
12
+ command = ["bunx", "jackherd", "plans"]
13
+
14
+ [[actions]]
15
+ id = "open-plans"
16
+ title = "Open the plans view"
17
+ description = "Split the focused pane to the right and run jh plans there, in the focused pane's directory"
18
+ contexts = ["workspace"]
19
+ command = ["bun", "open-plans.ts"]
20
+
21
+ [[startup]]
22
+ command = ["bun", "restore-plans.ts"]
@@ -0,0 +1,43 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
3
+ export const PLUGIN_ID = 'crossdelta.jackherd'
4
+
5
+ export const PLANS_PANE = 'plans'
6
+
7
+ /** The directory herdr reports for a pane in the JSON answer of `herdr pane get`, null when it names none. */
8
+ export const paneDirectoryOf = (json: string): string | null => {
9
+ try {
10
+ const cwd = (JSON.parse(json) as { result?: { pane?: { cwd?: unknown } } }).result?.pane?.cwd
11
+ return typeof cwd === 'string' && cwd !== '' ? cwd : null
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
17
+ /** Arguments of `herdr plugin pane open` for the plans pane: to the right of the pane, in its directory when known. */
18
+ export const openPlansArguments = (pane: string | null, directory: string | null): string[] => [
19
+ 'plugin',
20
+ 'pane',
21
+ 'open',
22
+ '--plugin',
23
+ PLUGIN_ID,
24
+ '--entrypoint',
25
+ PLANS_PANE,
26
+ '--placement',
27
+ 'split',
28
+ '--direction',
29
+ 'right',
30
+ ...(pane ? ['--target-pane', pane] : []),
31
+ ...(directory ? ['--cwd', directory] : []),
32
+ '--focus',
33
+ ]
34
+
35
+ const main = (): number => {
36
+ const herdr = process.env.HERDR_BIN_PATH || 'herdr'
37
+ const pane = process.env.HERDR_PANE_ID || null
38
+ const answer = pane ? spawnSync(herdr, ['pane', 'get', pane], { encoding: 'utf-8' }) : null
39
+ const directory = answer?.status === 0 ? paneDirectoryOf(answer.stdout) : null
40
+ return spawnSync(herdr, openPlansArguments(pane, directory), { stdio: 'inherit' }).status ?? 1
41
+ }
42
+
43
+ if (import.meta.main) process.exitCode = main()
@@ -0,0 +1,107 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ type Limits = { attempts: number; intervalMs: number }
5
+
6
+ /**
7
+ * Measured on herdr 0.9.0: the hook starts ~85 ms after the server, `pane process-info` answers
8
+ * `pane_not_found` until the terminals spawn at ~0.9 s, and the agents are back at ~3 s. The panes get 10 s, ten
9
+ * times what they took; `plans --ensure` gets four tries 2 s apart, past the ~3 s of the agents, for a pane that
10
+ * goes away between the two reads.
11
+ */
12
+ export const RESTORE_LIMITS = {
13
+ panes: { attempts: 20, intervalMs: 500 },
14
+ ensure: { attempts: 4, intervalMs: 2_000 },
15
+ } as const satisfies Record<string, Limits>
16
+
17
+ /** Runs `attempt` until it works or the attempts are used up, waiting the interval between two of them. */
18
+ export const retried = (attempt: () => boolean, limits: Limits, sleep: (ms: number) => void): boolean => {
19
+ for (let tried = 1; ; tried++) {
20
+ if (attempt()) return true
21
+ if (tried >= limits.attempts) return false
22
+ sleep(limits.intervalMs)
23
+ }
24
+ }
25
+
26
+ /**
27
+ * The directories of the panes an agent runs in, each once, from a `herdr pane list` answer; none when unreadable.
28
+ * `agentPanes` are the panes of `herdr agent list`: right after a restart the pane list names no agent there for a
29
+ * few seconds.
30
+ */
31
+ export const agentDirectoriesOf = (json: string, agentPanes: readonly string[]): string[] => {
32
+ try {
33
+ const panes = (JSON.parse(json) as { result?: { panes?: unknown } }).result?.panes
34
+ if (!Array.isArray(panes)) return []
35
+ const directories = panes.flatMap((pane) => {
36
+ const { cwd, agent, pane_id } = (pane ?? {}) as { cwd?: unknown; agent?: unknown; pane_id?: unknown }
37
+ const hasAgent = typeof agent === 'string' || (typeof pane_id === 'string' && agentPanes.includes(pane_id))
38
+ return typeof cwd === 'string' && hasAgent ? [cwd] : []
39
+ })
40
+ return [...new Set(directories)]
41
+ } catch {
42
+ return []
43
+ }
44
+ }
45
+
46
+ /** The arguments for `bun` that run `plans --ensure` with the CLI of the package this plugin ships in. */
47
+ export const ensureArguments = (pluginRoot: string): string[] => [
48
+ join(dirname(pluginRoot), 'src', 'cli.ts'),
49
+ 'plans',
50
+ '--ensure',
51
+ ]
52
+
53
+ const idsOf = (json: string, key: 'panes' | 'agents'): string[] => {
54
+ try {
55
+ const entries = (JSON.parse(json) as { result?: Record<string, unknown> }).result?.[key]
56
+ if (!Array.isArray(entries)) return []
57
+ return entries.flatMap((entry) => {
58
+ const id = (entry as { pane_id?: unknown } | null)?.pane_id
59
+ return typeof id === 'string' ? [id] : []
60
+ })
61
+ } catch {
62
+ return []
63
+ }
64
+ }
65
+
66
+ const mainCheckoutOf = (directory: string): string | null => {
67
+ const answer = spawnSync('git', ['-C', directory, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
68
+ encoding: 'utf-8',
69
+ })
70
+ return answer.status === 0 ? dirname(answer.stdout.trim()) : null
71
+ }
72
+
73
+ const herdrRun = (args: string[]) =>
74
+ spawnSync(process.env.HERDR_BIN_PATH || 'herdr', args, { encoding: 'utf-8' })
75
+
76
+ /** Whether herdr answers `process-info` for every pane, which it does only once their terminals run. */
77
+ const panesAnswer = (panes: readonly string[]): boolean =>
78
+ panes.every((pane) => herdrRun(['pane', 'process-info', '--pane', pane]).status === 0)
79
+
80
+ const ensureIn = (root: string, pluginRoot: string): boolean =>
81
+ spawnSync('bun', ensureArguments(pluginRoot), { cwd: root, stdio: 'inherit' }).status === 0
82
+
83
+ /**
84
+ * herdr's `[[startup]]` hook: after a server start or a live handoff, brings back the plans views of every
85
+ * repository an agent runs in. It parses herdr's answers itself and never throws, unlike the strict parsers of
86
+ * `src/plans/plan-panes.ts`: a hook that fails leaves nothing behind but a log line, and `plans --ensure`, which it
87
+ * calls, reads the panes again strictly. It reads the panes at once, while herdr still names their agents, then waits until
88
+ * herdr answers for every pane and retries `plans --ensure` within `RESTORE_LIMITS`. A repository without a board
89
+ * or config only prints why and is skipped.
90
+ */
91
+ const main = (): number => {
92
+ const list = herdrRun(['pane', 'list'])
93
+ if (list.status !== 0) console.error(`restore-plans: herdr pane list failed: ${list.stderr || list.status}`)
94
+ const listing = list.stdout ?? ''
95
+ const directories = agentDirectoriesOf(listing, idsOf(herdrRun(['agent', 'list']).stdout ?? '', 'agents'))
96
+ const roots = new Set(directories.flatMap((directory) => mainCheckoutOf(directory) ?? []))
97
+ if (roots.size === 0) return 0
98
+ const sleep = (ms: number) => Bun.sleepSync(ms)
99
+ if (!retried(() => panesAnswer(idsOf(listing, 'panes')), RESTORE_LIMITS.panes, sleep))
100
+ console.error('restore-plans: herdr did not answer for every pane in time, trying anyway')
101
+ for (const root of roots)
102
+ if (!retried(() => ensureIn(root, import.meta.dir), RESTORE_LIMITS.ensure, sleep))
103
+ console.error(`restore-plans: plans --ensure failed in ${root}`)
104
+ return 0
105
+ }
106
+
107
+ if (import.meta.main) process.exitCode = main()
package/install.sh ADDED
@@ -0,0 +1,157 @@
1
+ #!/bin/sh
2
+ # Installs jackherd for Claude Code: Bun only if it is missing, then the kit, then its setup wizard.
3
+ #
4
+ # curl -fsSL https://unpkg.com/jackherd/install.sh | sh
5
+ #
6
+ # Never as root, no edits to shell files: the official Bun installer puts Bun into ~/.bun and says
7
+ # itself which shell file it changed. Every action is announced on one line before it runs.
8
+ set -eu
9
+
10
+ # The default is the version this file ships with. The release bump changes it together with
11
+ # package.json; install.test.ts fails while the two differ. Overridable, but only by a plain release.
12
+ JACKHERD_VERSION="${JACKHERD_VERSION:-1.0.1}"
13
+
14
+ JACKHERD_PACKAGE='jackherd'
15
+ # Test seam: the terminal the wizard asks its questions on.
16
+ JACKHERD_TEST_TTY="${JACKHERD_TEST_TTY:-/dev/tty}"
17
+ BUN_INSTALLER_URL='https://bun.com/install'
18
+ BUN_MIN_MAJOR=1
19
+ BUN_MIN_MINOR=3
20
+
21
+ say() {
22
+ printf 'jackherd install: %s\n' "$1"
23
+ }
24
+
25
+ fail() {
26
+ printf 'jackherd install: %s\n' "$1" >&2
27
+ exit 1
28
+ }
29
+
30
+ # Digits and exactly two inner dots: major.minor.patch, nothing a shell or npm could read differently.
31
+ is_release() {
32
+ case $1 in
33
+ '' | *[!0-9.]* | .* | *. | *..* | *.*.*.*) return 1 ;;
34
+ *.*.*) return 0 ;;
35
+ *) return 1 ;;
36
+ esac
37
+ }
38
+
39
+ is_number() {
40
+ case $1 in
41
+ '' | *[!0-9]*) return 1 ;;
42
+ *) return 0 ;;
43
+ esac
44
+ }
45
+
46
+ check_platform() {
47
+ os=$(uname -s)
48
+ case $os in
49
+ Linux | Darwin) ;;
50
+ *) fail "unsupported system '$os': this installer runs on Linux and macOS only" ;;
51
+ esac
52
+ }
53
+
54
+ bun_bin_dir() {
55
+ printf '%s/bin\n' "${BUN_INSTALL:-$HOME/.bun}"
56
+ }
57
+
58
+ # The bun on PATH, else the one the official installer leaves in ~/.bun/bin; empty when there is none.
59
+ find_bun() {
60
+ if command -v bun >/dev/null 2>&1; then
61
+ command -v bun
62
+ elif [ -x "$(bun_bin_dir)/bun" ]; then
63
+ printf '%s\n' "$(bun_bin_dir)/bun"
64
+ fi
65
+ }
66
+
67
+ require_bun_installer_tools() {
68
+ missing=''
69
+ for tool in curl unzip bash; do
70
+ command -v "$tool" >/dev/null 2>&1 || missing="$missing $tool"
71
+ done
72
+ [ -z "$missing" ] || fail "Bun is missing, and installing it needs:$missing. Install them and run this again."
73
+ }
74
+
75
+ install_bun() {
76
+ require_bun_installer_tools
77
+ installer=$(mktemp) || fail "could not create a temporary file"
78
+ trap 'rm -f "$installer"' EXIT
79
+ # A trapped signal would otherwise let the script carry on; exiting runs the EXIT trap above.
80
+ trap 'exit 1' INT TERM HUP
81
+ say "Bun not found, downloading the official installer from $BUN_INSTALLER_URL"
82
+ curl --proto '=https' --tlsv1.2 -fsSL "$BUN_INSTALLER_URL" -o "$installer" || fail "could not download $BUN_INSTALLER_URL"
83
+ say "running the Bun installer (installs into $(bun_bin_dir), without root)"
84
+ bash "$installer" || fail "the Bun installer failed"
85
+ [ -x "$(bun_bin_dir)/bun" ] || fail "the Bun installer finished, but $(bun_bin_dir)/bun is missing"
86
+ }
87
+
88
+ check_bun_version() {
89
+ version=$("$1" --version) || fail "could not run '$1 --version'"
90
+ major=${version%%.*}
91
+ rest=${version#*.}
92
+ minor=${rest%%.*}
93
+ if ! is_number "$major" || ! is_number "$minor"; then
94
+ fail "could not read the Bun version '$version'"
95
+ fi
96
+ if [ "$major" -lt "$BUN_MIN_MAJOR" ] || { [ "$major" -eq "$BUN_MIN_MAJOR" ] && [ "$minor" -lt "$BUN_MIN_MINOR" ]; }; then
97
+ fail "Bun $version is too old, jackherd needs $BUN_MIN_MAJOR.$BUN_MIN_MINOR or newer. Update it with 'bun upgrade' and run this again."
98
+ fi
99
+ say "using Bun $version ($1)"
100
+ }
101
+
102
+ # Bun's global bin, not whatever jh comes first on PATH: an older install elsewhere stays untouched.
103
+ jh_bin() {
104
+ global_bin=$("$1" pm bin -g) || fail "could not read Bun's global bin ('bun pm bin -g')"
105
+ [ -x "$global_bin/jh" ] || fail "jh is not in Bun's global bin ($global_bin) after the install"
106
+ printf '%s\n' "$global_bin/jh"
107
+ }
108
+
109
+ # Opening it is the test: under 'curl | sh' stdin is the script, and /dev/tty may exist without a terminal.
110
+ terminal_available() {
111
+ (: <"$JACKHERD_TEST_TTY") 2>/dev/null
112
+ }
113
+
114
+ run_setup() {
115
+ if terminal_available; then
116
+ say "starting the setup: jh init --global"
117
+ "$1" init --global <"$JACKHERD_TEST_TTY" || fail "jh init --global failed"
118
+ else
119
+ say "no terminal to ask questions on, setting up with the defaults: jh init --global --yes"
120
+ "$1" init --global --yes </dev/null || fail "jh init --global --yes failed"
121
+ fi
122
+ }
123
+
124
+ main() {
125
+ [ "$(id -u)" -ne 0 ] || fail "run this as your own user, not as root (no sudo)"
126
+ is_release "$JACKHERD_VERSION" || fail "JACKHERD_VERSION must be a plain release like 1.2.3, not '$JACKHERD_VERSION'"
127
+ [ -n "${HOME:-}" ] || fail "HOME is not set"
128
+ check_platform
129
+ # Bun reads bunfig.toml (registry, preload, global bin) from the working directory; one in the
130
+ # directory this was started from must not steer the install. 'init --global' writes to ~ only.
131
+ cd "$HOME" || fail "could not change into HOME ($HOME)"
132
+
133
+ bun=$(find_bun)
134
+ if [ -z "$bun" ]; then
135
+ install_bun
136
+ bun="$(bun_bin_dir)/bun"
137
+ fi
138
+ case $bun in
139
+ /*) ;;
140
+ *) fail "found bun at '$bun', expected an absolute path" ;;
141
+ esac
142
+ check_bun_version "$bun"
143
+ # This run only: the kit's CLI starts with '#!/usr/bin/env bun'.
144
+ PATH="$(dirname "$bun"):$PATH"
145
+ export PATH
146
+
147
+ spec="$JACKHERD_PACKAGE@$JACKHERD_VERSION"
148
+ say "installing $spec: bun add -g --ignore-scripts $spec"
149
+ "$bun" add -g --ignore-scripts "$spec" || fail "bun add -g failed"
150
+
151
+ # An assignment, so a failure inside the substitution stops the run (set -e) instead of passing on ''.
152
+ jh=$(jh_bin "$bun")
153
+ run_setup "$jh"
154
+ say "done"
155
+ }
156
+
157
+ main