@shigureni/minion 0.1.0 → 0.2.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.
Files changed (58) hide show
  1. package/README.md +79 -33
  2. package/cli/src/app.ts +2 -0
  3. package/cli/src/factory.ts +8 -1
  4. package/cli/src/index.ts +15 -2
  5. package/cli/src/lib/body-validator.ts +13 -0
  6. package/cli/src/lib/format.ts +36 -0
  7. package/cli/src/router.ts +2 -1
  8. package/cli/src/routes/config/get/[key]/route.ts +7 -7
  9. package/cli/src/routes/config/list/route.ts +4 -5
  10. package/cli/src/routes/config/set/[key]/[value]/route.ts +5 -7
  11. package/cli/src/routes/dev/route.ts +12 -7
  12. package/cli/src/routes/dex/route.ts +66 -0
  13. package/cli/src/routes/kill/route.ts +7 -5
  14. package/cli/src/routes/not-found.ts +1 -1
  15. package/cli/src/routes/reboot/route.ts +12 -7
  16. package/cli/src/routes/start/route.ts +7 -6
  17. package/cli/src/routes/status/route.ts +5 -5
  18. package/lib/engine/app/app-paths.ts +46 -0
  19. package/lib/engine/app/app-runner.ts +235 -0
  20. package/lib/engine/app/source-hash.ts +34 -0
  21. package/lib/engine/collection/achievements.ts +143 -0
  22. package/lib/engine/collection/collection-store.ts +43 -0
  23. package/lib/engine/collection/collection-tracker.ts +97 -0
  24. package/lib/engine/collection/moon.ts +26 -0
  25. package/lib/engine/collection/species.ts +275 -0
  26. package/lib/engine/config/config-store.ts +36 -0
  27. package/lib/engine/errors.ts +20 -0
  28. package/lib/engine/fs/file-system.ts +31 -0
  29. package/lib/engine/fs/json-file-store.ts +57 -0
  30. package/lib/engine/fs/memory-file-system.ts +106 -0
  31. package/lib/engine/fs/node-file-system.ts +87 -0
  32. package/lib/engine/gateway/gateway-daemon.ts +14 -0
  33. package/lib/engine/gateway/gateway-routes.ts +17 -0
  34. package/lib/engine/gateway/gateway-server.ts +126 -0
  35. package/lib/engine/gateway/pet-behavior.ts +136 -0
  36. package/lib/engine/gateway/resolve-daemon-script.ts +7 -0
  37. package/lib/engine/gateway/sessions.ts +127 -0
  38. package/lib/engine/process/memory-process-runner.ts +67 -0
  39. package/lib/engine/process/node-process-runner.ts +51 -0
  40. package/lib/engine/process/process-runner.ts +21 -0
  41. package/lib/engine/random/memory-random-source.ts +22 -0
  42. package/lib/engine/random/node-random-source.ts +7 -0
  43. package/lib/engine/random/random-source.ts +9 -0
  44. package/lib/engine/stats/date-window.ts +31 -0
  45. package/lib/engine/stats/session-stats-tracker.ts +125 -0
  46. package/lib/engine/stats/stats-collector.ts +100 -0
  47. package/lib/engine/stats/stats-snapshot.ts +31 -0
  48. package/lib/engine/stats/token-usage-tracker.ts +170 -0
  49. package/lib/engine/time/clock.ts +11 -0
  50. package/lib/engine/time/memory-clock.ts +26 -0
  51. package/lib/engine/time/node-clock.ts +7 -0
  52. package/lib/index.ts +65 -0
  53. package/lib/minion.ts +194 -0
  54. package/package.json +9 -1
  55. package/swift/Sources/open-minion/open_minion.swift +2 -1
  56. package/tsconfig.json +3 -2
  57. package/cli/src/gateway.ts +0 -204
  58. package/cli/src/lib/process.ts +0 -204
package/README.md CHANGED
@@ -1,65 +1,111 @@
1
1
  # minion
2
2
 
3
- A tiny desktop pet that walks around your screen and reacts to your
4
- [Claude Code](https://claude.com/claude-code) sessions — idle while you're idle,
5
- running around while a session is busy.
3
+ A tiny desktop pet that walks around your macOS screen and reacts to your
4
+ [Claude Code](https://claude.com/claude-code) sessions — napping while you're
5
+ idle, running around while Claude is busy working.
6
6
 
7
- It's two parts: a Swift menu-less overlay app (`swift/`) that draws the pet, and a
8
- CLI (`cli/`) that builds/runs it and watches `~/.claude/sessions` to decide how the
9
- pet should behave.
7
+ The more you use Claude Code, the more you collect: heavy usage, late-night
8
+ sessions, and day streaks unlock achievements and rare minions in a built-in dex.
10
9
 
11
10
  ## Requirements
12
11
 
13
12
  - macOS
14
13
  - [Bun](https://bun.sh)
15
- - Xcode Command Line Tools (`xcode-select --install`) — needed once, to compile the
16
- Swift app on first run
14
+ - Xcode Command Line Tools (`xcode-select --install`) — needed once, to compile
15
+ the pet on first run
17
16
 
18
- ## Usage
17
+ ## Quick start
19
18
 
20
19
  ```sh
21
20
  bunx @shigureni/minion
22
21
  ```
23
22
 
24
- This builds the app on first run (subsequent runs reuse the build unless the Swift
25
- source changed) and starts it in the background, alongside a small local gateway
26
- that watches your Claude Code sessions.
23
+ The first run compiles the pet (takes a minute), then it appears on your screen
24
+ and starts watching your Claude Code sessions. Later runs reuse the build.
27
25
 
28
- Or install it globally so the `minion` command is always available:
26
+ To keep the `minion` command around:
29
27
 
30
28
  ```sh
31
29
  bun add -g @shigureni/minion
32
30
  minion
33
31
  ```
34
32
 
35
- ### Commands
33
+ ## Commands
36
34
 
37
- | Command | Description |
38
- | --------------------------------- | ------------------------------------------------ |
39
- | `minion` / `minion start` | Start (release build). No-op if already running. |
40
- | `minion dev` | Restart with a debug build. |
41
- | `minion kill` | Stop the app and its gateway. |
42
- | `minion reboot` | Restart with a release build. |
43
- | `minion status` | Show whether the app/gateway are running. |
44
- | `minion config list` | List config values. |
45
- | `minion config get <key>` | Get a config value. |
46
- | `minion config set <key> <value>` | Set a config value. |
35
+ | Command | What it does |
36
+ | -------------------------------- | ---------------------------------------- |
37
+ | `minion` / `minion start` | Start the pet. No-op if already running. |
38
+ | `minion kill` | Stop the pet and its gateway. |
39
+ | `minion reboot` | Restart. |
40
+ | `minion status` | Is it running? |
41
+ | `minion dex` | Your achievements and minion collection. |
42
+ | `minion config list / get / set` | Tweak settings. |
43
+ | `minion dev` | Restart with a debug build. |
47
44
 
48
- Add `-h` to any command for its help text (e.g. `minion start -h`).
45
+ Add `-h` to any command for details. State lives in `~/.minion`.
49
46
 
50
- State (pid files, config, and the Swift build output) lives in `~/.minion`,
51
- independent of wherever the package itself is installed.
47
+ ## The dex
48
+
49
+ `minion dex` turns your Claude Code habits into a collection game. It reads your
50
+ session activity and token usage (from Claude Code's local transcripts) and
51
+ unlocks two kinds of things:
52
+
53
+ - **Achievements** — milestones like your first session, a 7-day streak, running
54
+ 5 sessions at once, or burning a million tokens in a day.
55
+ - **Minions** — the five common species come out at different times of day; the
56
+ rare ones demand something special: 5+ parallel sessions, a week-long streak,
57
+ hopping across 10 different projects, 10M lifetime tokens, coding at 3am...
58
+
59
+ Undiscovered entries stay hidden as `???` until you earn them. Sprites aren't
60
+ wired into the desktop pet yet — for now the dex is a text-only collection — but
61
+ per-species art hooks are already in place.
62
+
63
+ ## Use it as a library
64
+
65
+ Everything the CLI does is also a programmable API:
66
+
67
+ ```ts
68
+ import { Minion } from "@shigureni/minion"
69
+
70
+ const minion = new Minion()
71
+ // { kind: "started", pid } | { kind: "build-failed", ... } | ... | Error — the library never throws
72
+ const result = await minion.app.start()
73
+
74
+ const stats = minion.stats.collect()
75
+ const { species } = minion.collection.evaluate(stats)
76
+ console.log(`right now: ${species.name}`)
77
+ ```
78
+
79
+ Everything is injectable. Bring your own species/achievement catalogs — including
80
+ your own art via each entry's `asset` field — or run fully sandboxed in tests:
81
+
82
+ ```ts
83
+ // Custom catalog: your species, your conditions, your sprites.
84
+ const custom = new Minion({
85
+ species: [
86
+ {
87
+ id: "sunny",
88
+ name: "Sunny Pal",
89
+ rarity: "common",
90
+ description: "Appears during the day.",
91
+ condition: (s) => s.timeBucket === "day",
92
+ asset: "sprites/sunny.png",
93
+ },
94
+ ],
95
+ })
96
+
97
+ // Sandbox: no real disk, processes, or clock.
98
+ const sandbox = Minion.inMemory()
99
+ ```
100
+
101
+ See `lib/index.ts` for the full API surface.
52
102
 
53
103
  ## Development
54
104
 
55
105
  ```sh
56
106
  bun install
57
- bun run check # format + lint + type-check (vite-plus)
107
+ bun run check # format + lint + type-check
58
108
  bun run test
59
- bun run fmt
60
- bun run lint
61
109
  ```
62
110
 
63
- `cli/src/routes/<command>/route.ts` is one file per command (hono-routed, mirrors
64
- the structure of jobantenna-cli) — adding a command means adding a route file and
65
- wiring it into `cli/src/app.ts`.
111
+ Architecture notes and contributor conventions live in [CLAUDE.md](CLAUDE.md).
package/cli/src/app.ts CHANGED
@@ -4,6 +4,7 @@ import configGet from "@/routes/config/get/[key]/route"
4
4
  import configList from "@/routes/config/list/route"
5
5
  import configSet from "@/routes/config/set/[key]/[value]/route"
6
6
  import dev from "@/routes/dev/route"
7
+ import dex from "@/routes/dex/route"
7
8
  import kill from "@/routes/kill/route"
8
9
  import { notFound } from "@/routes/not-found"
9
10
  import reboot from "@/routes/reboot/route"
@@ -17,6 +18,7 @@ base.notFound(notFound)
17
18
  export const app = base
18
19
  .post("/start", ...start)
19
20
  .post("/dev", ...dev)
21
+ .post("/dex", ...dex)
20
22
  .post("/kill", ...kill)
21
23
  .post("/reboot", ...reboot)
22
24
  .post("/status", ...status)
@@ -1,3 +1,10 @@
1
1
  import { createFactory } from "hono/factory"
2
+ import type { Minion } from "@lib/minion"
2
3
 
3
- export const factory = createFactory()
4
+ export type Env = {
5
+ Bindings: {
6
+ minion: Minion
7
+ }
8
+ }
9
+
10
+ export const factory = createFactory<Env>()
package/cli/src/index.ts CHANGED
@@ -2,9 +2,17 @@
2
2
  import { app } from "@/app"
3
3
  import { postJson } from "@/lib/post-json"
4
4
  import { toRequest } from "@/router"
5
+ import { Minion } from "@lib/minion"
5
6
 
6
7
  import pkg from "../../package.json" with { type: "json" }
7
8
 
9
+ const minion = new Minion({
10
+ onEvent: (event) => {
11
+ if (event.type === "build-start") console.log("ビルド中...")
12
+ },
13
+ })
14
+ const env = { minion }
15
+
8
16
  const HELP = `minion - open-minion CLI
9
17
 
10
18
  Usage:
@@ -17,6 +25,7 @@ Commands:
17
25
  kill 停止する
18
26
  reboot 停止してリリースビルドで起動し直す
19
27
  status 起動状況を表示する
28
+ dex 実績とミニオン図鑑を表示する
20
29
  config list 設定値を一覧表示する
21
30
  config get <key> 設定値を取得する
22
31
  config set <key> <value> 設定値を書き込む
@@ -41,13 +50,17 @@ if (request.global.help) {
41
50
  process.exit()
42
51
  }
43
52
 
44
- const helpResponse = await app.request(request.path, postJson({ ...request.body, help: "true" }))
53
+ const helpResponse = await app.request(
54
+ request.path,
55
+ postJson({ ...request.body, help: "true" }),
56
+ env,
57
+ )
45
58
  console.log(helpResponse.ok ? await helpResponse.text() : HELP)
46
59
  process.exit()
47
60
  }
48
61
 
49
62
  try {
50
- const response = await app.request(request.path, postJson(request.body))
63
+ const response = await app.request(request.path, postJson(request.body), env)
51
64
  const text = await response.text()
52
65
 
53
66
  if (!response.ok) {
@@ -0,0 +1,13 @@
1
+ import { zValidator } from "@hono/zod-validator"
2
+ import type { z } from "zod"
3
+
4
+ // zValidator の 400 応答(zod エラーの JSON ダンプ)を CLI 向けの短いテキストにする。
5
+ // strict スキーマと組み合わせて、未知のフラグを黙って無視せずエラーにする。
6
+ export function bodyValidator<T extends z.ZodType>(schema: T) {
7
+ return zValidator("json", schema, (result, c) => {
8
+ if (!result.success) {
9
+ const message = result.error.issues.map((issue) => issue.message).join("\n")
10
+ return c.text(message, 400)
11
+ }
12
+ })
13
+ }
@@ -0,0 +1,36 @@
1
+ import type {
2
+ MinionAppStatus,
3
+ MinionKillResult,
4
+ MinionStartResult,
5
+ } from "@lib/engine/app/app-runner"
6
+
7
+ // ライブラリは構造化された結果を返す。日本語の表示文字列への整形はCLIの責務。
8
+
9
+ export function formatStartResult(result: MinionStartResult): string {
10
+ switch (result.kind) {
11
+ case "started":
12
+ return `起動した (pid ${result.pid})`
13
+ case "already-running":
14
+ return `すでに起動中 (pid ${result.pid})`
15
+ case "build-failed":
16
+ return "ビルド失敗"
17
+ case "lock-conflict":
18
+ return "他の起動処理と競合したため中止"
19
+ }
20
+ }
21
+
22
+ export function isStartError(result: MinionStartResult): boolean {
23
+ return result.kind === "build-failed" || result.kind === "lock-conflict"
24
+ }
25
+
26
+ export function formatKillResult(result: MinionKillResult): string {
27
+ return result.kind === "killed" ? `停止した (pid ${result.pid})` : "起動していない"
28
+ }
29
+
30
+ export function formatAppStatus(status: MinionAppStatus): string {
31
+ const appLine = status.app.running ? `起動中 (pid ${status.app.pid})` : "停止中"
32
+ const gatewayLine = status.gateway.running
33
+ ? `gateway起動中 (pid ${status.gateway.pid})`
34
+ : "gateway停止中"
35
+ return [appLine, gatewayLine].join("\n")
36
+ }
package/cli/src/router.ts CHANGED
@@ -40,6 +40,7 @@ export function toRequest(args: string[]) {
40
40
  }
41
41
  }
42
42
 
43
- const path = `/${segments.join("/")}`
43
+ // `/` `:` を含む値(URLなど)がルーティングを壊さないようエンコードする。Hono がパラメータ取得時にデコードする。
44
+ const path = `/${segments.map(encodeURIComponent).join("/")}`
44
45
  return { path, body, global }
45
46
  }
@@ -1,17 +1,17 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
4
  import { helpGuard } from "@/lib/help-guard"
5
- import { readConfig } from "@/lib/process"
6
5
 
7
- const schema = z.object({})
6
+ const schema = z.strictObject({})
8
7
 
9
8
  export const help = `Usage: minion config get <key>
10
9
 
11
10
  設定値を取得する。`
12
11
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
14
- const key = c.req.param("key")
15
- const config = readConfig()
16
- return c.text(config[key ?? ""] ?? "")
12
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
13
+ const key = c.req.param("key") ?? ""
14
+ const value = c.env.minion.config.get(key)
15
+ if (value === undefined) return c.text(`未設定: ${key}`, 404)
16
+ return c.text(value)
17
17
  })
@@ -1,17 +1,16 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
4
  import { helpGuard } from "@/lib/help-guard"
5
- import { readConfig } from "@/lib/process"
6
5
 
7
- const schema = z.object({})
6
+ const schema = z.strictObject({})
8
7
 
9
8
  export const help = `Usage: minion config list
10
9
 
11
10
  設定値を一覧表示する。`
12
11
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
14
- const config = readConfig()
12
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
13
+ const config = c.env.minion.config.list()
15
14
  const lines = Object.entries(config).map(([k, v]) => `${k} = ${v}`)
16
15
  return c.text(lines.join("\n"))
17
16
  })
@@ -1,20 +1,18 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
4
  import { helpGuard } from "@/lib/help-guard"
5
- import { readConfig, writeConfig } from "@/lib/process"
6
5
 
7
- const schema = z.object({})
6
+ const schema = z.strictObject({})
8
7
 
9
8
  export const help = `Usage: minion config set <key> <value>
10
9
 
11
10
  設定値を書き込む。`
12
11
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
12
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
14
13
  const key = c.req.param("key") ?? ""
15
14
  const value = c.req.param("value") ?? ""
16
- const config = readConfig()
17
- config[key] = value
18
- writeConfig(config)
15
+ const setError = c.env.minion.config.set(key, value)
16
+ if (setError) return c.text(setError.message, 500)
19
17
  return c.text(`${key} = ${value}`)
20
18
  })
@@ -1,17 +1,22 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
+ import { formatKillResult, formatStartResult, isStartError } from "@/lib/format"
4
5
  import { helpGuard } from "@/lib/help-guard"
5
- import { killApp, startApp } from "@/lib/process"
6
6
 
7
- const schema = z.object({})
7
+ const schema = z.strictObject({})
8
8
 
9
9
  export const help = `Usage: minion dev
10
10
 
11
11
  一旦停止してから、デバッグビルドで起動し直す。`
12
12
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
14
- const killMessage = killApp()
15
- const startMessage = await startApp(true)
16
- return c.text([killMessage, startMessage].join("\n"))
13
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
14
+ const killResult = c.env.minion.app.kill()
15
+ if (killResult instanceof Error) return c.text(killResult.message, 500)
16
+ const startResult = await c.env.minion.app.start({ debug: true })
17
+ if (startResult instanceof Error) {
18
+ return c.text([formatKillResult(killResult), startResult.message].join("\n"), 500)
19
+ }
20
+ const text = [formatKillResult(killResult), formatStartResult(startResult)].join("\n")
21
+ return c.text(text, isStartError(startResult) ? 500 : 200)
17
22
  })
@@ -0,0 +1,66 @@
1
+ import { z } from "zod"
2
+ import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
+ import { helpGuard } from "@/lib/help-guard"
5
+ import type { MinionRarity } from "@lib/engine/collection/species"
6
+
7
+ const schema = z.strictObject({})
8
+
9
+ export const help = `Usage: minion dex
10
+
11
+ 実績とミニオン図鑑を表示する。セッション数・同時実行数・時間帯・トークン消費量を
12
+ 元に実績を解除し、条件を満たすと珍しいミニオンが出現する。`
13
+
14
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
15
+ const minion = c.env.minion
16
+ const stats = minion.stats.collect()
17
+ if (stats instanceof Error) return c.text(stats.message, 500)
18
+ const evaluation = minion.collection.evaluate(stats)
19
+ if (evaluation instanceof Error) return c.text(evaluation.message, 500)
20
+ const { achievements, species } = minion.collection.dex()
21
+
22
+ const lines: string[] = []
23
+
24
+ lines.push(
25
+ `現在のミニオン: ${evaluation.species.name} (${rarityLabel(evaluation.species.rarity)})`,
26
+ )
27
+
28
+ const newlyUnlocked = evaluation.newlyUnlockedAchievements
29
+ if (newlyUnlocked.length > 0) {
30
+ lines.push("")
31
+ lines.push("実績を解除した!")
32
+ for (const a of newlyUnlocked) lines.push(` ★ ${a.name} — ${a.description}`)
33
+ }
34
+ if (evaluation.newlyDiscoveredSpecies.length > 0) {
35
+ lines.push("")
36
+ for (const discovered of evaluation.newlyDiscoveredSpecies) {
37
+ lines.push(`新しいミニオンを発見した! ${discovered.name} (${rarityLabel(discovered.rarity)})`)
38
+ }
39
+ }
40
+
41
+ const unlockedCount = achievements.filter((a) => a.unlocked).length
42
+ lines.push("")
43
+ lines.push(`== 実績 (${unlockedCount}/${achievements.length}) ==`)
44
+ for (const a of achievements) {
45
+ const mark = a.unlocked ? "[x]" : "[ ]"
46
+ const detail = a.unlocked ? a.description : "???"
47
+ lines.push(`${mark} ${a.name} — ${detail}`)
48
+ }
49
+
50
+ const discoveredCount = species.filter((s) => s.discovered).length
51
+ lines.push("")
52
+ lines.push(`== ミニオン図鑑 (${discoveredCount}/${species.length}) ==`)
53
+ for (const s of species) {
54
+ const mark = s.discovered ? "[x]" : "[ ]"
55
+ const label = s.discovered
56
+ ? `${s.name} (${rarityLabel(s.rarity)}) — ${s.description}`
57
+ : `??? (${rarityLabel(s.rarity)})`
58
+ lines.push(`${mark} ${label}`)
59
+ }
60
+
61
+ return c.text(lines.join("\n"))
62
+ })
63
+
64
+ function rarityLabel(rarity: MinionRarity): string {
65
+ return rarity === "rare" ? "レア" : "コモン"
66
+ }
@@ -1,15 +1,17 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
+ import { formatKillResult } from "@/lib/format"
4
5
  import { helpGuard } from "@/lib/help-guard"
5
- import { killApp } from "@/lib/process"
6
6
 
7
- const schema = z.object({})
7
+ const schema = z.strictObject({})
8
8
 
9
9
  export const help = `Usage: minion kill
10
10
 
11
11
  アプリとgatewayを停止する。`
12
12
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
14
- return c.text(killApp())
13
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
14
+ const result = c.env.minion.app.kill()
15
+ if (result instanceof Error) return c.text(result.message, 500)
16
+ return c.text(formatKillResult(result))
15
17
  })
@@ -2,7 +2,7 @@ import type { NotFoundHandler } from "hono"
2
2
 
3
3
  export const notFound: NotFoundHandler = (c) => {
4
4
  return c.text(
5
- `使い方: minion [start|dev|kill|reboot|status|config]\n不明なコマンド: ${c.req.path}`,
5
+ `使い方: minion [start|dev|kill|reboot|status|dex|config]\n不明なコマンド: ${c.req.path}`,
6
6
  404,
7
7
  )
8
8
  }
@@ -1,17 +1,22 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
+ import { formatKillResult, formatStartResult, isStartError } from "@/lib/format"
4
5
  import { helpGuard } from "@/lib/help-guard"
5
- import { killApp, startApp } from "@/lib/process"
6
6
 
7
- const schema = z.object({})
7
+ const schema = z.strictObject({})
8
8
 
9
9
  export const help = `Usage: minion reboot
10
10
 
11
11
  一旦停止してから、リリースビルドで起動し直す。`
12
12
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
14
- const killMessage = killApp()
15
- const startMessage = await startApp(false)
16
- return c.text([killMessage, startMessage].join("\n"))
13
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
14
+ const killResult = c.env.minion.app.kill()
15
+ if (killResult instanceof Error) return c.text(killResult.message, 500)
16
+ const startResult = await c.env.minion.app.start()
17
+ if (startResult instanceof Error) {
18
+ return c.text([formatKillResult(killResult), startResult.message].join("\n"), 500)
19
+ }
20
+ const text = [formatKillResult(killResult), formatStartResult(startResult)].join("\n")
21
+ return c.text(text, isStartError(startResult) ? 500 : 200)
17
22
  })
@@ -1,16 +1,17 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
+ import { formatStartResult, isStartError } from "@/lib/format"
4
5
  import { helpGuard } from "@/lib/help-guard"
5
- import { startApp } from "@/lib/process"
6
6
 
7
- const schema = z.object({})
7
+ const schema = z.strictObject({})
8
8
 
9
9
  export const help = `Usage: minion start
10
10
 
11
11
  アプリをリリースビルドで起動する。すでに起動中なら何もしない。`
12
12
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
14
- const message = await startApp(false)
15
- return c.text(message)
13
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
14
+ const result = await c.env.minion.app.start()
15
+ if (result instanceof Error) return c.text(result.message, 500)
16
+ return c.text(formatStartResult(result), isStartError(result) ? 500 : 200)
16
17
  })
@@ -1,15 +1,15 @@
1
- import { zValidator } from "@hono/zod-validator"
2
1
  import { z } from "zod"
3
2
  import { factory } from "@/factory"
3
+ import { bodyValidator } from "@/lib/body-validator"
4
+ import { formatAppStatus } from "@/lib/format"
4
5
  import { helpGuard } from "@/lib/help-guard"
5
- import { statusApp } from "@/lib/process"
6
6
 
7
- const schema = z.object({})
7
+ const schema = z.strictObject({})
8
8
 
9
9
  export const help = `Usage: minion status
10
10
 
11
11
  アプリとgatewayの起動状況を表示する。`
12
12
 
13
- export default factory.createHandlers(helpGuard(help), zValidator("json", schema), async (c) => {
14
- return c.text(statusApp())
13
+ export default factory.createHandlers(helpGuard(help), bodyValidator(schema), async (c) => {
14
+ return c.text(formatAppStatus(c.env.minion.app.status()))
15
15
  })
@@ -0,0 +1,46 @@
1
+ import { homedir } from "node:os"
2
+ import { join } from "node:path"
3
+
4
+ export type MinionPaths = {
5
+ /** `~/.minion` by default — state lives here independent of where the package itself is installed. */
6
+ dataDir: string
7
+ /** The `swift/` directory containing the Xcode Swift Package. */
8
+ appRoot: string
9
+ buildPath: string
10
+ binPath: string
11
+ debugBinPath: string
12
+ dataFile: string
13
+ pidFile: string
14
+ gatewayPidFile: string
15
+ configFile: string
16
+ sessionStatsFile: string
17
+ usageScanFile: string
18
+ collectionFile: string
19
+ }
20
+
21
+ type Props = {
22
+ /** The directory containing `swift/` (the package root). */
23
+ packageRoot: string
24
+ dataDir?: string
25
+ }
26
+
27
+ export function resolveMinionPaths(props: Props): MinionPaths {
28
+ const dataDir = props.dataDir ?? join(homedir(), ".minion")
29
+ const appRoot = join(props.packageRoot, "swift")
30
+ const buildPath = join(dataDir, "build")
31
+
32
+ return {
33
+ dataDir,
34
+ appRoot,
35
+ buildPath,
36
+ binPath: join(buildPath, "release", "open-minion"),
37
+ debugBinPath: join(buildPath, "debug", "open-minion"),
38
+ dataFile: join(dataDir, "data.json"),
39
+ pidFile: join(dataDir, "pid"),
40
+ gatewayPidFile: join(dataDir, "gateway.pid"),
41
+ configFile: join(dataDir, "config.json"),
42
+ sessionStatsFile: join(dataDir, "session-stats.json"),
43
+ usageScanFile: join(dataDir, "usage-scan.json"),
44
+ collectionFile: join(dataDir, "collection.json"),
45
+ }
46
+ }