@iceinvein/agent-skills 0.1.39 → 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 (148) hide show
  1. package/README.md +18 -2
  2. package/dist/cli/index.js +105 -28
  3. package/package.json +1 -1
  4. package/skills/index.json +14 -2
  5. package/skills/magpie/README.md +2 -1
  6. package/skills/magpie/SKILL.md +138 -45
  7. package/skills/magpie/bin/magpie.ts +43 -0
  8. package/skills/magpie/fixtures/example-pr/brief.json +18 -0
  9. package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
  10. package/skills/magpie/fixtures/fake-gh.sh +14 -0
  11. package/skills/magpie/package.json +1 -1
  12. package/skills/magpie/references/peer-review.md +7 -2
  13. package/skills/magpie/references/scout.md +90 -0
  14. package/skills/magpie/references/specialists.md +117 -6
  15. package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
  16. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
  17. package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
  18. package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
  19. package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
  20. package/skills/magpie/scripts/__tests__/gh.test.ts +90 -0
  21. package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
  22. package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
  23. package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
  24. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
  25. package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
  26. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +173 -1
  27. package/skills/magpie/scripts/__tests__/render-findings.test.ts +151 -1
  28. package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
  29. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
  30. package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
  31. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +207 -7
  32. package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
  33. package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
  34. package/skills/magpie/scripts/diff-chunks.ts +28 -0
  35. package/skills/magpie/scripts/findings-files.ts +32 -0
  36. package/skills/magpie/scripts/gh.ts +68 -14
  37. package/skills/magpie/scripts/git-diff.ts +111 -0
  38. package/skills/magpie/scripts/path-filter.ts +9 -5
  39. package/skills/magpie/scripts/preview-cmd.ts +11 -1
  40. package/skills/magpie/scripts/refresh.ts +32 -3
  41. package/skills/magpie/scripts/render-cmd.ts +35 -11
  42. package/skills/magpie/scripts/render-findings.ts +78 -2
  43. package/skills/magpie/scripts/render-progress.ts +6 -1
  44. package/skills/magpie/scripts/setup-cmd.ts +38 -1
  45. package/skills/magpie/scripts/shard.ts +171 -0
  46. package/skills/magpie/scripts/status-cmd.ts +7 -4
  47. package/skills/magpie/scripts/types.ts +50 -0
  48. package/skills/magpie/skill.json +2 -2
  49. package/skills/magpie/templates/styles.css +66 -0
  50. package/skills/migrate/README.md +194 -0
  51. package/skills/migrate/SKILL.md +197 -0
  52. package/skills/migrate/bin/migrate +15 -0
  53. package/skills/migrate/bin/migrate.ts +309 -0
  54. package/skills/migrate/biome.json +35 -0
  55. package/skills/migrate/bun.lock +24 -0
  56. package/skills/migrate/docs/architecture.md +294 -0
  57. package/skills/migrate/docs/reference.md +590 -0
  58. package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
  59. package/skills/migrate/fixtures/tiny-express/app.js +29 -0
  60. package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
  61. package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
  62. package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
  63. package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
  64. package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
  65. package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
  66. package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
  67. package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
  68. package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
  69. package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
  70. package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
  71. package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
  72. package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
  73. package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
  74. package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
  75. package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
  76. package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
  77. package/skills/migrate/install.sh +68 -0
  78. package/skills/migrate/package.json +17 -0
  79. package/skills/migrate/references/phases/enumerate.md +291 -0
  80. package/skills/migrate/references/phases/extract.md +652 -0
  81. package/skills/migrate/references/phases/parity.md +275 -0
  82. package/skills/migrate/references/phases/probe.md +135 -0
  83. package/skills/migrate/references/phases/queue.md +242 -0
  84. package/skills/migrate/references/phases/seam.md +416 -0
  85. package/skills/migrate/references/recipes/README.md +116 -0
  86. package/skills/migrate/references/recipes/aspnet.md +287 -0
  87. package/skills/migrate/references/run-ops.md +280 -0
  88. package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
  89. package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
  90. package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
  91. package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
  92. package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
  93. package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
  94. package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
  95. package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
  96. package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
  97. package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
  98. package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
  99. package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
  100. package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
  101. package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
  102. package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
  103. package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
  104. package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
  105. package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
  106. package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
  107. package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
  108. package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
  109. package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
  110. package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
  111. package/skills/migrate/scripts/census-cmd.ts +109 -0
  112. package/skills/migrate/scripts/census.ts +342 -0
  113. package/skills/migrate/scripts/check-cmd.ts +24 -0
  114. package/skills/migrate/scripts/check.ts +376 -0
  115. package/skills/migrate/scripts/citations.ts +92 -0
  116. package/skills/migrate/scripts/config.ts +237 -0
  117. package/skills/migrate/scripts/ids.ts +31 -0
  118. package/skills/migrate/scripts/import-cmd.ts +141 -0
  119. package/skills/migrate/scripts/init-cmd.ts +118 -0
  120. package/skills/migrate/scripts/leaks.ts +184 -0
  121. package/skills/migrate/scripts/lock.ts +188 -0
  122. package/skills/migrate/scripts/paths.ts +103 -0
  123. package/skills/migrate/scripts/phase-cmd.ts +63 -0
  124. package/skills/migrate/scripts/phases.ts +113 -0
  125. package/skills/migrate/scripts/queue-cmd.ts +98 -0
  126. package/skills/migrate/scripts/queue.ts +258 -0
  127. package/skills/migrate/scripts/report-cmd.ts +47 -0
  128. package/skills/migrate/scripts/report.ts +131 -0
  129. package/skills/migrate/scripts/reset-cmd.ts +120 -0
  130. package/skills/migrate/scripts/status-cmd.ts +52 -0
  131. package/skills/migrate/scripts/store.ts +159 -0
  132. package/skills/migrate/scripts/types.ts +137 -0
  133. package/skills/migrate/scripts/validate.ts +221 -0
  134. package/skills/migrate/skill.json +33 -0
  135. package/skills/migrate/templates/config.toml +27 -0
  136. package/skills/migrate/templates/queue-item.md +17 -0
  137. package/skills/migrate/tsconfig.json +18 -0
  138. package/skills/migrate/uninstall.sh +31 -0
  139. package/skills/sluice/SKILL.md +82 -0
  140. package/skills/sluice/references/deep-channel.md +94 -0
  141. package/skills/sluice/references/finish.md +35 -0
  142. package/skills/sluice/references/intent.md +29 -0
  143. package/skills/sluice/references/review.md +42 -0
  144. package/skills/sluice/references/root-cause.md +38 -0
  145. package/skills/sluice/references/show-or-say.md +36 -0
  146. package/skills/sluice/references/test-first.md +35 -0
  147. package/skills/sluice/references/verify.md +26 -0
  148. package/skills/sluice/skill.json +32 -0
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: migrate
3
+ description: Source-agnostic legacy migration mapping. Walks a legacy codebase through probe, enumerate, seam, extract, parity, and queue, building an auditable requirements ledger with mandatory citations and a `migrate check` gate in place of self-reported completeness. Use when the user asks to migrate, re-specify, replatform, or map a legacy system onto a new stack, or to resume, check, or report on a mapping run already under way.
4
+ ---
5
+
6
+ # migrate
7
+
8
+ ## Prerequisites
9
+
10
+ - `migrate` on `PATH`, put there by this skill's `install.sh`.
11
+ - A read-only checkout of the legacy source. `migrate check`'s `citations` and
12
+ `source` gates read it; nothing here writes to it, and every writer in the CLI
13
+ refuses a path that resolves inside it.
14
+ - A target repo that is a git working copy. The store lives inside it and
15
+ commits alongside your own work; there is no separate run directory.
16
+
17
+ ## Phase walkthrough
18
+
19
+ Work phases 0 through 7 in order. Do not skip ahead: the run-state gate fails a
20
+ phase marked `done` while its predecessor is still `pending`, so working out of
21
+ order just produces a violation you undo later.
22
+
23
+ ### 0. Probe
24
+
25
+ Produces `.migrate/config.toml` (detected source stack, `runnable` or
26
+ `source-only` basis, the target profile), written by `migrate init`, plus
27
+ `.migrate/parity-basis.md`: hand-written prose carrying the detection
28
+ evidence, since no command writes it either.
29
+
30
+ Read `references/phases/probe.md` before dispatching anything.
31
+
32
+ ```
33
+ migrate init --source <path> --scope "<text>" --name <target> \
34
+ [--source-stack <s>] [--target-stack <s>] [--basis <runnable|source-only>]
35
+ migrate phase probe --status done
36
+ ```
37
+
38
+ ### 1. Enumerate
39
+
40
+ Produces `elements.jsonl`, every row `unaccounted`, and one `lens` census
41
+ record per declared surface type.
42
+
43
+ Read `references/phases/enumerate.md` before dispatching anything.
44
+
45
+ Fanout unit: one agent per (surface, lens) pair.
46
+
47
+ ```
48
+ migrate import elements <batch.json>
49
+ migrate census <lens-record.json>
50
+ migrate phase enumerate --status done
51
+ ```
52
+
53
+ ### 2. Seam
54
+
55
+ Produces `capabilities.jsonl` (the seam partition), `seam.json` (run-level
56
+ seam metadata), and `seam.md` (the validators' raw evidence). All three are
57
+ hand-written: there is no `seam` verb, so nothing in the CLI authors their
58
+ content. (`migrate reset --phase seam` does write to these paths, clearing
59
+ `capabilities.jsonl` and deleting the other two, but that undoes the phase
60
+ rather than authoring it.)
61
+
62
+ Read `references/phases/seam.md` before dispatching anything.
63
+
64
+ ```
65
+ migrate phase seam --status done
66
+ ```
67
+
68
+ ### 3. Extract
69
+
70
+ Produces `requirements.jsonl`, the attribute/rule-sweep/closer census records,
71
+ and a terminal disposition on every element.
72
+
73
+ Read `references/phases/extract.md` before dispatching anything.
74
+
75
+ Fanout unit: one agent per capability.
76
+
77
+ ```
78
+ migrate import reqs <batch.json>
79
+ migrate import elements <batch.json>
80
+ migrate census <record.json>
81
+ migrate queue add <item.md>
82
+ migrate phase extract --status done
83
+ ```
84
+
85
+ `queue add` is not optional here. An `out-of-scope` disposition's queue id
86
+ and a `queued` confidence's queue id are both checked by the `refs` gate, and
87
+ a queue id with no file behind it is a violation the moment anything checks,
88
+ not a future one. File each item in the same pass that names it.
89
+
90
+ The second import carries the resolved `disposition` (`mapped` or
91
+ `out-of-scope`); it is the only writer of a *resolved* value there, so this
92
+ line is the ledger write-back itself, not something the phase-status flip
93
+ does for you. `migrate reset --phase extract` also writes this field, but
94
+ only back to `unaccounted`; it clears, it does not resolve.
95
+
96
+ ### 4. Parity
97
+
98
+ Produces `deltas.jsonl` and a parity plan on every requirement whose
99
+ confidence is not `queued`.
100
+
101
+ Read `references/phases/parity.md` before dispatching anything.
102
+
103
+ ```
104
+ migrate import deltas <batch.json>
105
+ migrate import reqs <batch.json>
106
+ migrate queue add <item.md>
107
+ migrate phase parity --status done
108
+ ```
109
+
110
+ Same rule as phase 3: a `rubric` plan below `high` must carry a queue id, and
111
+ the `refs` gate checks it resolves, so file the item in this pass.
112
+
113
+ The second import carries the resolved `parity` value; as in extract, it is
114
+ the only writer of a *resolved* value, and this line is the write-back
115
+ itself. `migrate reset --phase parity` also writes this field, but only
116
+ back to `null`; it clears, it does not resolve.
117
+
118
+ ### 5. Queue
119
+
120
+ Produces the queue items carrying forward anything ambiguous: evidence,
121
+ options, and a recommendation, filed for an owner to adjudicate.
122
+
123
+ Read `references/phases/queue.md` before dispatching anything.
124
+
125
+ ```
126
+ migrate queue add <item.md>
127
+ migrate phase queue --status done
128
+ ```
129
+
130
+ ### 6. Adjudicate
131
+
132
+ A run stops at the queue in this version of the tool: `adjudicate` has no verb
133
+ yet, so nothing here can move a queue item's status past `open`. `migrate
134
+ status` and `migrate queue list` are the terminus; adjudication arrives with
135
+ its verb in the next milestone.
136
+
137
+ ### 7. Handoff
138
+
139
+ `handoff` has no verb yet either, for the same reason. `migrate status` and
140
+ `migrate queue list` remain the terminus; handoff arrives with its verb in the
141
+ next milestone.
142
+
143
+ ## Checking as you go
144
+
145
+ Run `migrate check --phase <current>` after every batch. It bounds the
146
+ run-state gate at that phase; the other nine gates always read the whole
147
+ store, so a coverage or census gap past your current phase still fails on its
148
+ own gate regardless of `--phase`.
149
+
150
+ Run plain `migrate check` only when claiming the whole migration is complete:
151
+ with no `--phase`, it gates every phase through `handoff`. In this version
152
+ that cannot pass, because `adjudicate` and `handoff` have no verbs to complete
153
+ them. `migrate check --phase queue` is the practical terminus for this
154
+ milestone; its exit 0 is what "done, for now" means.
155
+
156
+ ```
157
+ migrate check --phase queue
158
+ migrate check
159
+ ```
160
+
161
+ ## Resuming a crashed run
162
+
163
+ ```
164
+ migrate status
165
+ migrate phase
166
+ ```
167
+
168
+ `migrate status` prints the last committed batch and the outstanding work;
169
+ `migrate phase` (no name) prints every phase's status and batch count, one
170
+ line each, so you can see exactly where the run stopped. To re-enter one
171
+ phase, clear its derived rows and re-run it:
172
+
173
+ ```
174
+ migrate reset --phase <phase>
175
+ ```
176
+
177
+ Re-importing a batch upserts rows by id rather than duplicating them, so
178
+ progress from before the crash is not lost by retrying it.
179
+
180
+ ## Aborting
181
+
182
+ There is no run directory to delete: the store lives at `.migrate/` inside the
183
+ target repo. `references/run-ops.md` holds the batch-checkpoint discipline (a
184
+ git commit after every `migrate import`); if it was followed, an aborted run
185
+ leaves behind exactly whatever the last commit captured. Leave `.migrate/` in
186
+ place either way. Discard only uncommitted scratch files, such as a
187
+ `batch.json` you built but never imported. `.migrate/.env`, if a runtime lens
188
+ created one, must never be committed regardless of how the run ends. `init`
189
+ takes care of the ignore entry in all three cases, and says on stdout when it
190
+ changed something: `init: created <path> with .migrate/.env` when the target
191
+ had no `.gitignore`, `init: appended .migrate/.env to <path>` when it had one
192
+ without the entry, and **nothing at all** when the entry was already there,
193
+ since there was nothing to change. Silence from `init` on this is the
194
+ already-correct case, not a skipped one. If you edited `.gitignore` after
195
+ `init` ran, check the entry is still there before committing anything, because
196
+ nothing re-checks it: the `leaks` gate that would catch a committed value is
197
+ opt-in (`migrate check --leaks`).
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # Resolve symlinks so we find the real migrate.ts even when invoked via
3
+ # a /usr/local/bin or ~/.local/bin symlink.
4
+ SCRIPT="${BASH_SOURCE[0]}"
5
+ while [ -L "$SCRIPT" ]; do
6
+ DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
7
+ TARGET="$(readlink "$SCRIPT")"
8
+ if [[ "$TARGET" = /* ]]; then
9
+ SCRIPT="$TARGET"
10
+ else
11
+ SCRIPT="$DIR/$TARGET"
12
+ fi
13
+ done
14
+ DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
15
+ exec bun "$DIR/migrate.ts" "$@"
@@ -0,0 +1,309 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import pkg from '../package.json' with { type: 'json' }
4
+
5
+ const VERSION = (pkg as { version: string }).version
6
+
7
+ const USAGE = `Usage: migrate <subcommand> [args]
8
+
9
+ Subcommands:
10
+ init --source <path> --scope <text> --name <target>
11
+ Write .migrate/config.toml
12
+ import <elements|reqs|deltas> <batch.json> [--force-unlock]
13
+ Validated bulk append to the store
14
+ census <record.json> [--force-unlock]
15
+ Record a lens accounting record
16
+ phase [<name>] [--status <s>] [--force-unlock]
17
+ Print phase state, or set one phase's status
18
+ queue add <file.md> Add a queue item
19
+ queue list [--open] List queue items, severity first
20
+ queue show <id> Print one queue item
21
+ check [--phase <p>] [--no-citations] [--leaks]
22
+ Run the gates; without --phase, exit 0 means
23
+ the whole migration is complete
24
+ status Phase state, counts, resume pointer
25
+ reset --phase <phase> [--force-unlock]
26
+ Clear one phase's derived rows
27
+ report [--out <dir>] Render markdown views
28
+ --version Print the migrate version
29
+ --help Show this message`
30
+
31
+ type Handler = (args: string[]) => Promise<number> | number
32
+
33
+ // A flag's value is missing, or the value slot is occupied by another flag
34
+ // (`init --scope --name newapp` used to read scope as the literal string
35
+ // "--name" and carry on), in both cases the request is malformed, not a
36
+ // value the command can act on. Every `--flag <value>` parser in this file
37
+ // shares this one check so the three no longer disagree about which of
38
+ // those two shapes is a usage error and which is silently tolerated.
39
+ function readFlag(args: string[], name: string): { value?: string; error?: string } {
40
+ const at = args.indexOf(name)
41
+ if (at === -1) return {}
42
+ const value = args[at + 1]
43
+ if (value === undefined || value.startsWith('--')) {
44
+ return { error: `${name} needs a value` }
45
+ }
46
+ return { value }
47
+ }
48
+
49
+ const HANDLERS: Record<string, Handler> = {
50
+ init: async (args) => {
51
+ const names = ['--source', '--scope', '--name', '--source-stack', '--target-stack', '--basis']
52
+ const values: Record<string, string | undefined> = {}
53
+ for (const name of names) {
54
+ const result = readFlag(args, name)
55
+ if (result.error) {
56
+ process.stderr.write(`init: ${result.error}\n`)
57
+ return 2
58
+ }
59
+ values[name] = result.value
60
+ }
61
+ const sourcePath = values['--source']
62
+ const scope = values['--scope']
63
+ const targetName = values['--name']
64
+ if (!sourcePath || !scope || !targetName) {
65
+ process.stderr.write(
66
+ 'init: want --source <path> --scope <text> --name <target> [--source-stack <s>] [--target-stack <s>] [--basis <runnable|source-only>]\n',
67
+ )
68
+ return 2
69
+ }
70
+ const { runInit } = await import('../scripts/init-cmd.ts')
71
+ return runInit({
72
+ root: process.cwd(),
73
+ sourcePath,
74
+ scope,
75
+ targetName,
76
+ ...(values['--source-stack'] ? { sourceStack: values['--source-stack'] } : {}),
77
+ ...(values['--target-stack'] ? { targetStack: values['--target-stack'] } : {}),
78
+ ...(values['--basis'] ? { basis: values['--basis'] } : {}),
79
+ })
80
+ },
81
+ import: async (args) => {
82
+ const kind = args[0]
83
+ const file = args[1]
84
+ if (kind !== 'elements' && kind !== 'reqs' && kind !== 'deltas') {
85
+ process.stderr.write('import: want <elements|reqs|deltas> <batch.json>\n')
86
+ return 2
87
+ }
88
+ if (!file) {
89
+ process.stderr.write('import: missing <batch.json>\n')
90
+ return 2
91
+ }
92
+ const { findStoreRoot } = await import('../scripts/paths.ts')
93
+ const root = await findStoreRoot(process.cwd())
94
+ if (!root) {
95
+ process.stderr.write('import: no .migrate store found above the cwd\n')
96
+ return 2
97
+ }
98
+ const forceUnlock = args.includes('--force-unlock')
99
+ const { runImport } = await import('../scripts/import-cmd.ts')
100
+ return runImport({
101
+ root,
102
+ kind,
103
+ batchFile: file,
104
+ ...(forceUnlock ? { forceUnlock: true } : {}),
105
+ })
106
+ },
107
+ census: async (args) => {
108
+ const file = args[0]
109
+ if (!file) {
110
+ process.stderr.write('census: missing <record.json>\n')
111
+ return 2
112
+ }
113
+ const { findStoreRoot } = await import('../scripts/paths.ts')
114
+ const root = await findStoreRoot(process.cwd())
115
+ if (!root) {
116
+ process.stderr.write('census: no .migrate store found above the cwd\n')
117
+ return 2
118
+ }
119
+ const forceUnlock = args.includes('--force-unlock')
120
+ const { runCensus } = await import('../scripts/census-cmd.ts')
121
+ return runCensus({ root, file, ...(forceUnlock ? { forceUnlock: true } : {}) })
122
+ },
123
+ phase: async (args) => {
124
+ const status = readFlag(args, '--status')
125
+ if (status.error) {
126
+ process.stderr.write(`phase: ${status.error}\n`)
127
+ return 2
128
+ }
129
+ // args[0] is the phase name only when it is a positional. A flag sitting
130
+ // in that slot (`phase --force-unlock enumerate --status done`) leaves the
131
+ // name undefined, which used to silently downgrade a write to a read:
132
+ // every phase's line printed, exit 0, nothing moved. `import` and `census`
133
+ // both reject the identical flag-ordering mistake at 2, because their
134
+ // first positional is load-bearing too. Any write-intent flag with no
135
+ // phase name to apply it to is that same mistake, so it is refused here
136
+ // rather than serviced as a listing nobody asked for.
137
+ const name = args[0]?.startsWith('--') ? undefined : args[0]
138
+ if (!name && (status.value !== undefined || args.includes('--force-unlock'))) {
139
+ process.stderr.write(
140
+ 'phase: want <name> before --status/--force-unlock, as in `phase enumerate --status done`\n',
141
+ )
142
+ return 2
143
+ }
144
+ const { findStoreRoot } = await import('../scripts/paths.ts')
145
+ const root = await findStoreRoot(process.cwd())
146
+ if (!root) {
147
+ process.stderr.write('phase: no .migrate store found above the cwd\n')
148
+ return 2
149
+ }
150
+ const forceUnlock = args.includes('--force-unlock')
151
+ const { runPhase } = await import('../scripts/phase-cmd.ts')
152
+ return runPhase({
153
+ root,
154
+ ...(name ? { name } : {}),
155
+ ...(status.value ? { status: status.value } : {}),
156
+ ...(forceUnlock ? { forceUnlock: true } : {}),
157
+ })
158
+ },
159
+ queue: async (args) => {
160
+ const { findStoreRoot } = await import('../scripts/paths.ts')
161
+ const root = await findStoreRoot(process.cwd())
162
+ if (!root) {
163
+ process.stderr.write('queue: no .migrate store found above the cwd\n')
164
+ return 2
165
+ }
166
+ const { runQueue } = await import('../scripts/queue-cmd.ts')
167
+ return runQueue({ root, args })
168
+ },
169
+ check: async (args) => {
170
+ const phase = readFlag(args, '--phase')
171
+ if (phase.error) {
172
+ process.stderr.write(`check: ${phase.error}\n`)
173
+ return 2
174
+ }
175
+ const { isPhase, PHASES } = await import('../scripts/phases.ts')
176
+ if (phase.value && !isPhase(phase.value)) {
177
+ process.stderr.write(
178
+ `check: unknown phase ${phase.value}; want one of ${PHASES.join(', ')}\n`,
179
+ )
180
+ return 2
181
+ }
182
+ const { findStoreRoot } = await import('../scripts/paths.ts')
183
+ const root = await findStoreRoot(process.cwd())
184
+ if (!root) {
185
+ process.stderr.write('check: no .migrate store found above the cwd\n')
186
+ return 2
187
+ }
188
+ const { runCheckCmd } = await import('../scripts/check-cmd.ts')
189
+ return runCheckCmd({
190
+ root,
191
+ // --citations is accepted and ignored: citations are on by default now,
192
+ // and silently rejecting the old flag would break every invocation
193
+ // written against Milestone 1.
194
+ citations: !args.includes('--no-citations'),
195
+ leaks: args.includes('--leaks'),
196
+ ...(phase.value && isPhase(phase.value) ? { phase: phase.value } : {}),
197
+ })
198
+ },
199
+ status: async () => {
200
+ const { findStoreRoot } = await import('../scripts/paths.ts')
201
+ const root = await findStoreRoot(process.cwd())
202
+ if (!root) {
203
+ process.stderr.write('status: no .migrate store found above the cwd\n')
204
+ return 2
205
+ }
206
+ const { runStatus } = await import('../scripts/status-cmd.ts')
207
+ return runStatus({ root })
208
+ },
209
+ reset: async (args) => {
210
+ const result = readFlag(args, '--phase')
211
+ if (result.error) {
212
+ process.stderr.write(`reset: ${result.error}\n`)
213
+ return 2
214
+ }
215
+ const phase = result.value
216
+ if (!phase) {
217
+ process.stderr.write('reset: missing --phase <phase>\n')
218
+ return 2
219
+ }
220
+ const { findStoreRoot } = await import('../scripts/paths.ts')
221
+ const root = await findStoreRoot(process.cwd())
222
+ if (!root) {
223
+ process.stderr.write('reset: no .migrate store found above the cwd\n')
224
+ return 2
225
+ }
226
+ const forceUnlock = args.includes('--force-unlock')
227
+ const { runReset } = await import('../scripts/reset-cmd.ts')
228
+ return runReset({ root, phase, ...(forceUnlock ? { forceUnlock: true } : {}) })
229
+ },
230
+ report: async (args) => {
231
+ const result = readFlag(args, '--out')
232
+ if (result.error) {
233
+ process.stderr.write(`report: ${result.error}\n`)
234
+ return 2
235
+ }
236
+ const outDir = result.value
237
+ const { findStoreRoot } = await import('../scripts/paths.ts')
238
+ const root = await findStoreRoot(process.cwd())
239
+ if (!root) {
240
+ process.stderr.write('report: no .migrate store found above the cwd\n')
241
+ return 2
242
+ }
243
+ const { runReport } = await import('../scripts/report-cmd.ts')
244
+ return runReport({ root, ...(outDir ? { outDir } : {}) })
245
+ },
246
+ }
247
+
248
+ // A handler's rejection reaching the guard below is not guaranteed to be an
249
+ // Error: `(e as Error).message` on a non-Error rejection prints the useless
250
+ // "sub: undefined", and on a rejected `null` or `undefined` it throws inside
251
+ // the catch itself -- the one shape that would otherwise still escape this
252
+ // guard uncaught. Every other value (a string, a plain object, an Error)
253
+ // converts to a message without throwing.
254
+ export function errorMessage(e: unknown): string {
255
+ return e instanceof Error ? e.message : String(e)
256
+ }
257
+
258
+ export async function main(argv: string[]): Promise<number> {
259
+ const [sub, ...rest] = argv
260
+ if (sub === '--help' || sub === '-h') {
261
+ process.stdout.write(`${USAGE}\n`)
262
+ return 0
263
+ }
264
+ if (sub === '--version' || sub === '-V') {
265
+ process.stdout.write(`migrate ${VERSION}\n`)
266
+ return 0
267
+ }
268
+ if (!sub) {
269
+ process.stderr.write(`${USAGE}\n`)
270
+ return 2
271
+ }
272
+ const handler = HANDLERS[sub]
273
+ if (!handler) {
274
+ process.stderr.write(`Unknown subcommand: ${sub}\n${USAGE}\n`)
275
+ return 2
276
+ }
277
+ try {
278
+ return await handler(rest)
279
+ } catch (e) {
280
+ // Every failure a handler recognizes already returns its own exit code
281
+ // without throwing (see import-cmd.ts, census-cmd.ts, queue-cmd.ts,
282
+ // check-cmd.ts). An Error reaching here is one none of them classified,
283
+ // and in this codebase that is dominated by one shape: loadConfig
284
+ // throwing because config.toml is missing or malformed, unguarded at
285
+ // its import-cmd.ts, census-cmd.ts, queue-cmd.ts and check.ts call
286
+ // sites, or an equivalent case where the store itself cannot be read
287
+ // (a corrupt phases.json, a malformed row in an existing store file).
288
+ // Every one of those means the request could never have been serviced
289
+ // as posed, not that a well-formed request turned up a bad answer, so
290
+ // it takes the same code already used a few lines up for "no .migrate
291
+ // store found above the cwd" and, in every handler, for the
292
+ // assertNotUnderSource containment refusal: 2, not 1. The message
293
+ // printed is the Error's own message (never a generic replacement, so a
294
+ // genuine bug is still visible), prefixed the way every handler prefixes
295
+ // its own diagnostics, and nothing else: no stack trace.
296
+ process.stderr.write(`${sub}: ${errorMessage(e)}\n`)
297
+ return 2
298
+ }
299
+ }
300
+
301
+ // Guarded so this module can be imported (e.g. by tests, to exercise `main`
302
+ // and `errorMessage` directly) without re-running the CLI against the
303
+ // importing process's own argv and calling process.exit out from under it.
304
+ // Running `bun bin/migrate.ts ...` directly still takes this branch exactly
305
+ // as before.
306
+ if (import.meta.main) {
307
+ const code = await main(process.argv.slice(2))
308
+ process.exit(code)
309
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json",
3
+ "assist": {
4
+ "enabled": true,
5
+ "actions": {
6
+ "source": {
7
+ "organizeImports": "on"
8
+ }
9
+ }
10
+ },
11
+ "linter": {
12
+ "enabled": true,
13
+ "rules": {
14
+ "recommended": true,
15
+ "suspicious": {
16
+ "noArrayIndexKey": "off"
17
+ }
18
+ }
19
+ },
20
+ "formatter": {
21
+ "enabled": true,
22
+ "indentStyle": "space",
23
+ "indentWidth": 2,
24
+ "lineWidth": 100
25
+ },
26
+ "javascript": {
27
+ "formatter": {
28
+ "quoteStyle": "single",
29
+ "semicolons": "asNeeded"
30
+ }
31
+ },
32
+ "files": {
33
+ "includes": ["bin/**", "scripts/**"]
34
+ }
35
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "migrate",
7
+ "devDependencies": {
8
+ "@types/bun": "^1.2.0",
9
+ "typescript": "^5.9.0",
10
+ },
11
+ },
12
+ },
13
+ "packages": {
14
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
15
+
16
+ "@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="],
17
+
18
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
19
+
20
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
21
+
22
+ "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
23
+ }
24
+ }