opencastle 0.26.1 → 0.27.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 (226) hide show
  1. package/README.md +7 -1
  2. package/bin/cli.mjs +10 -0
  3. package/dist/cli/agents.d.ts +3 -0
  4. package/dist/cli/agents.d.ts.map +1 -0
  5. package/dist/cli/agents.js +161 -0
  6. package/dist/cli/agents.js.map +1 -0
  7. package/dist/cli/baselines.d.ts +3 -0
  8. package/dist/cli/baselines.d.ts.map +1 -0
  9. package/dist/cli/baselines.js +128 -0
  10. package/dist/cli/baselines.js.map +1 -0
  11. package/dist/cli/convoy/engine.d.ts +68 -2
  12. package/dist/cli/convoy/engine.d.ts.map +1 -1
  13. package/dist/cli/convoy/engine.js +2102 -26
  14. package/dist/cli/convoy/engine.js.map +1 -1
  15. package/dist/cli/convoy/engine.test.js +1572 -70
  16. package/dist/cli/convoy/engine.test.js.map +1 -1
  17. package/dist/cli/convoy/events.d.ts +4 -1
  18. package/dist/cli/convoy/events.d.ts.map +1 -1
  19. package/dist/cli/convoy/events.js +74 -13
  20. package/dist/cli/convoy/events.js.map +1 -1
  21. package/dist/cli/convoy/events.test.js +154 -27
  22. package/dist/cli/convoy/events.test.js.map +1 -1
  23. package/dist/cli/convoy/expertise.d.ts +16 -0
  24. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  25. package/dist/cli/convoy/expertise.js +121 -0
  26. package/dist/cli/convoy/expertise.js.map +1 -0
  27. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  28. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  29. package/dist/cli/convoy/expertise.test.js +96 -0
  30. package/dist/cli/convoy/expertise.test.js.map +1 -0
  31. package/dist/cli/convoy/export.test.js +1 -0
  32. package/dist/cli/convoy/export.test.js.map +1 -1
  33. package/dist/cli/convoy/formula.d.ts +19 -0
  34. package/dist/cli/convoy/formula.d.ts.map +1 -0
  35. package/dist/cli/convoy/formula.js +142 -0
  36. package/dist/cli/convoy/formula.js.map +1 -0
  37. package/dist/cli/convoy/formula.test.d.ts +2 -0
  38. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  39. package/dist/cli/convoy/formula.test.js +342 -0
  40. package/dist/cli/convoy/formula.test.js.map +1 -0
  41. package/dist/cli/convoy/gates.d.ts +128 -0
  42. package/dist/cli/convoy/gates.d.ts.map +1 -0
  43. package/dist/cli/convoy/gates.js +606 -0
  44. package/dist/cli/convoy/gates.js.map +1 -0
  45. package/dist/cli/convoy/gates.test.d.ts +2 -0
  46. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  47. package/dist/cli/convoy/gates.test.js +976 -0
  48. package/dist/cli/convoy/gates.test.js.map +1 -0
  49. package/dist/cli/convoy/health.d.ts +11 -0
  50. package/dist/cli/convoy/health.d.ts.map +1 -1
  51. package/dist/cli/convoy/health.js +54 -0
  52. package/dist/cli/convoy/health.js.map +1 -1
  53. package/dist/cli/convoy/health.test.js +56 -1
  54. package/dist/cli/convoy/health.test.js.map +1 -1
  55. package/dist/cli/convoy/issues.d.ts +8 -0
  56. package/dist/cli/convoy/issues.d.ts.map +1 -0
  57. package/dist/cli/convoy/issues.js +98 -0
  58. package/dist/cli/convoy/issues.js.map +1 -0
  59. package/dist/cli/convoy/issues.test.d.ts +2 -0
  60. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  61. package/dist/cli/convoy/issues.test.js +107 -0
  62. package/dist/cli/convoy/issues.test.js.map +1 -0
  63. package/dist/cli/convoy/knowledge.d.ts +5 -0
  64. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  65. package/dist/cli/convoy/knowledge.js +116 -0
  66. package/dist/cli/convoy/knowledge.js.map +1 -0
  67. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  68. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  69. package/dist/cli/convoy/knowledge.test.js +87 -0
  70. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  71. package/dist/cli/convoy/lessons.d.ts +17 -0
  72. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  73. package/dist/cli/convoy/lessons.js +149 -0
  74. package/dist/cli/convoy/lessons.js.map +1 -0
  75. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  76. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  77. package/dist/cli/convoy/lessons.test.js +135 -0
  78. package/dist/cli/convoy/lessons.test.js.map +1 -0
  79. package/dist/cli/convoy/lock.d.ts +13 -0
  80. package/dist/cli/convoy/lock.d.ts.map +1 -0
  81. package/dist/cli/convoy/lock.js +88 -0
  82. package/dist/cli/convoy/lock.js.map +1 -0
  83. package/dist/cli/convoy/lock.test.d.ts +2 -0
  84. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  85. package/dist/cli/convoy/lock.test.js +136 -0
  86. package/dist/cli/convoy/lock.test.js.map +1 -0
  87. package/dist/cli/convoy/merge.d.ts +4 -0
  88. package/dist/cli/convoy/merge.d.ts.map +1 -1
  89. package/dist/cli/convoy/merge.js +18 -1
  90. package/dist/cli/convoy/merge.js.map +1 -1
  91. package/dist/cli/convoy/merge.test.js +6 -7
  92. package/dist/cli/convoy/merge.test.js.map +1 -1
  93. package/dist/cli/convoy/partition.d.ts +51 -0
  94. package/dist/cli/convoy/partition.d.ts.map +1 -0
  95. package/dist/cli/convoy/partition.js +186 -0
  96. package/dist/cli/convoy/partition.js.map +1 -0
  97. package/dist/cli/convoy/partition.test.d.ts +2 -0
  98. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  99. package/dist/cli/convoy/partition.test.js +315 -0
  100. package/dist/cli/convoy/partition.test.js.map +1 -0
  101. package/dist/cli/convoy/pipeline.test.js +6 -0
  102. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  103. package/dist/cli/convoy/store.d.ts +47 -5
  104. package/dist/cli/convoy/store.d.ts.map +1 -1
  105. package/dist/cli/convoy/store.js +525 -19
  106. package/dist/cli/convoy/store.js.map +1 -1
  107. package/dist/cli/convoy/store.test.js +1345 -12
  108. package/dist/cli/convoy/store.test.js.map +1 -1
  109. package/dist/cli/convoy/types.d.ts +156 -2
  110. package/dist/cli/convoy/types.d.ts.map +1 -1
  111. package/dist/cli/destroy.d.ts +3 -0
  112. package/dist/cli/destroy.d.ts.map +1 -0
  113. package/dist/cli/destroy.js +69 -0
  114. package/dist/cli/destroy.js.map +1 -0
  115. package/dist/cli/destroy.test.d.ts +2 -0
  116. package/dist/cli/destroy.test.d.ts.map +1 -0
  117. package/dist/cli/destroy.test.js +116 -0
  118. package/dist/cli/destroy.test.js.map +1 -0
  119. package/dist/cli/gitignore.d.ts +9 -0
  120. package/dist/cli/gitignore.d.ts.map +1 -1
  121. package/dist/cli/gitignore.js +29 -0
  122. package/dist/cli/gitignore.js.map +1 -1
  123. package/dist/cli/plan.d.ts +3 -0
  124. package/dist/cli/plan.d.ts.map +1 -0
  125. package/dist/cli/plan.js +288 -0
  126. package/dist/cli/plan.js.map +1 -0
  127. package/dist/cli/run/adapters/claude.d.ts +2 -0
  128. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  129. package/dist/cli/run/adapters/claude.js +89 -49
  130. package/dist/cli/run/adapters/claude.js.map +1 -1
  131. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  132. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  133. package/dist/cli/run/adapters/claude.test.js +205 -0
  134. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  135. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  136. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  137. package/dist/cli/run/adapters/copilot.js +84 -46
  138. package/dist/cli/run/adapters/copilot.js.map +1 -1
  139. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  140. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  141. package/dist/cli/run/adapters/copilot.test.js +195 -0
  142. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  143. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  144. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  145. package/dist/cli/run/adapters/cursor.js +83 -47
  146. package/dist/cli/run/adapters/cursor.js.map +1 -1
  147. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  148. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  149. package/dist/cli/run/adapters/cursor.test.js +129 -0
  150. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  151. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  152. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  153. package/dist/cli/run/adapters/opencode.js +81 -47
  154. package/dist/cli/run/adapters/opencode.js.map +1 -1
  155. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  156. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  157. package/dist/cli/run/adapters/opencode.test.js +119 -0
  158. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  159. package/dist/cli/run/executor.js +1 -1
  160. package/dist/cli/run/executor.js.map +1 -1
  161. package/dist/cli/run/schema.d.ts.map +1 -1
  162. package/dist/cli/run/schema.js +245 -4
  163. package/dist/cli/run/schema.js.map +1 -1
  164. package/dist/cli/run/schema.test.js +669 -0
  165. package/dist/cli/run/schema.test.js.map +1 -1
  166. package/dist/cli/run.d.ts.map +1 -1
  167. package/dist/cli/run.js +362 -22
  168. package/dist/cli/run.js.map +1 -1
  169. package/dist/cli/types.d.ts +85 -2
  170. package/dist/cli/types.d.ts.map +1 -1
  171. package/dist/cli/types.js.map +1 -1
  172. package/dist/cli/watch.d.ts +15 -0
  173. package/dist/cli/watch.d.ts.map +1 -0
  174. package/dist/cli/watch.js +279 -0
  175. package/dist/cli/watch.js.map +1 -0
  176. package/package.json +1 -1
  177. package/src/cli/agents.ts +177 -0
  178. package/src/cli/baselines.ts +143 -0
  179. package/src/cli/convoy/engine.test.ts +1839 -70
  180. package/src/cli/convoy/engine.ts +2417 -38
  181. package/src/cli/convoy/events.test.ts +179 -38
  182. package/src/cli/convoy/events.ts +88 -16
  183. package/src/cli/convoy/expertise.test.ts +128 -0
  184. package/src/cli/convoy/expertise.ts +163 -0
  185. package/src/cli/convoy/export.test.ts +1 -0
  186. package/src/cli/convoy/formula.test.ts +405 -0
  187. package/src/cli/convoy/formula.ts +174 -0
  188. package/src/cli/convoy/gates.test.ts +1169 -0
  189. package/src/cli/convoy/gates.ts +774 -0
  190. package/src/cli/convoy/health.test.ts +64 -2
  191. package/src/cli/convoy/health.ts +80 -2
  192. package/src/cli/convoy/issues.test.ts +143 -0
  193. package/src/cli/convoy/issues.ts +136 -0
  194. package/src/cli/convoy/knowledge.test.ts +101 -0
  195. package/src/cli/convoy/knowledge.ts +132 -0
  196. package/src/cli/convoy/lessons.test.ts +188 -0
  197. package/src/cli/convoy/lessons.ts +164 -0
  198. package/src/cli/convoy/lock.test.ts +181 -0
  199. package/src/cli/convoy/lock.ts +103 -0
  200. package/src/cli/convoy/merge.test.ts +6 -7
  201. package/src/cli/convoy/merge.ts +19 -1
  202. package/src/cli/convoy/partition.test.ts +423 -0
  203. package/src/cli/convoy/partition.ts +232 -0
  204. package/src/cli/convoy/pipeline.test.ts +6 -0
  205. package/src/cli/convoy/store.test.ts +1512 -14
  206. package/src/cli/convoy/store.ts +676 -30
  207. package/src/cli/convoy/types.ts +170 -1
  208. package/src/cli/destroy.test.ts +141 -0
  209. package/src/cli/destroy.ts +88 -0
  210. package/src/cli/gitignore.ts +36 -0
  211. package/src/cli/plan.ts +316 -0
  212. package/src/cli/run/adapters/claude.test.ts +234 -0
  213. package/src/cli/run/adapters/claude.ts +45 -5
  214. package/src/cli/run/adapters/copilot.test.ts +224 -0
  215. package/src/cli/run/adapters/copilot.ts +34 -4
  216. package/src/cli/run/adapters/cursor.test.ts +144 -0
  217. package/src/cli/run/adapters/cursor.ts +33 -2
  218. package/src/cli/run/adapters/opencode.test.ts +135 -0
  219. package/src/cli/run/adapters/opencode.ts +30 -2
  220. package/src/cli/run/executor.ts +1 -1
  221. package/src/cli/run/schema.test.ts +758 -0
  222. package/src/cli/run/schema.ts +300 -25
  223. package/src/cli/run.ts +341 -21
  224. package/src/cli/types.ts +86 -1
  225. package/src/cli/watch.ts +298 -0
  226. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
@@ -0,0 +1,177 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import type { CliContext } from './types.js'
4
+ import { c } from './prompt.js'
5
+
6
+ const HELP = `
7
+ opencastle agents [subcommand] [options]
8
+
9
+ Manage persistent agent identities.
10
+
11
+ Subcommands:
12
+ list List all agent identities (agent, task count, latest date)
13
+ inspect <agent> Show summaries for a specific agent
14
+ purge <agent> Delete all identities for an agent
15
+ compact --older-than <d> Delete identities older than N days
16
+
17
+ Options:
18
+ --older-than <days> Days threshold for compact subcommand
19
+ --yes, -y Skip confirmation prompt
20
+ --help, -h Show this help
21
+ `
22
+
23
+ interface AgentsOptions {
24
+ subcommand: string | null
25
+ agentName: string | null
26
+ olderThan: number | null
27
+ yes: boolean
28
+ help: boolean
29
+ }
30
+
31
+ function parseAgentsArgs(args: string[]): AgentsOptions {
32
+ const opts: AgentsOptions = {
33
+ subcommand: null,
34
+ agentName: null,
35
+ olderThan: null,
36
+ yes: false,
37
+ help: false,
38
+ }
39
+
40
+ for (let i = 0; i < args.length; i++) {
41
+ const arg = args[i]
42
+ switch (arg) {
43
+ case '--help':
44
+ case '-h':
45
+ opts.help = true
46
+ break
47
+ case '--older-than':
48
+ if (i + 1 >= args.length) { console.error(' \u2717 --older-than requires a number'); process.exit(1) }
49
+ opts.olderThan = parseInt(args[++i], 10)
50
+ if (!Number.isFinite(opts.olderThan) || opts.olderThan < 1) {
51
+ console.error(' \u2717 --older-than must be a positive integer')
52
+ process.exit(1)
53
+ }
54
+ break
55
+ case '--yes':
56
+ case '-y':
57
+ opts.yes = true
58
+ break
59
+ default:
60
+ if (arg.startsWith('--')) {
61
+ console.error(` \u2717 Unknown option: ${arg}`)
62
+ console.log(HELP)
63
+ process.exit(1)
64
+ }
65
+ if (!opts.subcommand) {
66
+ opts.subcommand = arg
67
+ } else if (!opts.agentName) {
68
+ opts.agentName = arg
69
+ }
70
+ }
71
+ }
72
+
73
+ return opts
74
+ }
75
+
76
+ export default async function agents({ args }: CliContext): Promise<void> {
77
+ const opts = parseAgentsArgs(args)
78
+
79
+ if (opts.help || !opts.subcommand) {
80
+ console.log(HELP)
81
+ return
82
+ }
83
+
84
+ const dbPath = resolve(process.cwd(), '.opencastle', 'convoy.db')
85
+ if (!existsSync(dbPath)) {
86
+ console.log(' No convoy database found at .opencastle/convoy.db')
87
+ return
88
+ }
89
+
90
+ const { createConvoyStore } = await import('./convoy/store.js')
91
+ const store = createConvoyStore(dbPath)
92
+
93
+ try {
94
+ switch (opts.subcommand) {
95
+ case 'list': {
96
+ const summaries = store.listAgentIdentitySummary()
97
+ if (summaries.length === 0) {
98
+ console.log(' No agent identities found.')
99
+ return
100
+ }
101
+ console.log(`\n Agent Identities (${summaries.length} agents):\n`)
102
+ console.log(` ${'Agent'.padEnd(25)} ${'Tasks'.padEnd(8)} Latest`)
103
+ console.log(` ${'\u2500'.repeat(25)} ${'\u2500'.repeat(8)} ${'\u2500'.repeat(20)}`)
104
+ for (const s of summaries) {
105
+ console.log(` ${s.agent.padEnd(25)} ${String(s.task_count).padEnd(8)} ${s.latest_date}`)
106
+ }
107
+ console.log()
108
+ break
109
+ }
110
+
111
+ case 'inspect': {
112
+ if (!opts.agentName) {
113
+ console.error(' \u2717 inspect requires an agent name: opencastle agents inspect <agent>')
114
+ process.exit(1)
115
+ }
116
+ const identities = store.getAgentIdentities(opts.agentName, 100)
117
+ if (identities.length === 0) {
118
+ console.log(` No identities found for agent "${opts.agentName}".`)
119
+ return
120
+ }
121
+ console.log(`\n Agent: ${opts.agentName} (${identities.length} identities)\n`)
122
+ for (const id of identities) {
123
+ console.log(` ${c.dim('\u2500'.repeat(60))}`)
124
+ console.log(` Task: ${id.task_id} | Convoy: ${id.convoy_id}`)
125
+ console.log(` Date: ${id.created_at} | Retention: ${id.retention_days}d`)
126
+ console.log(` Summary:`)
127
+ const lines = id.summary.split('\n')
128
+ for (const line of lines.slice(0, 10)) {
129
+ console.log(` ${line}`)
130
+ }
131
+ if (lines.length > 10) {
132
+ console.log(` ${c.dim(`... ${lines.length - 10} more lines`)}`)
133
+ }
134
+ console.log()
135
+ }
136
+ break
137
+ }
138
+
139
+ case 'purge': {
140
+ if (!opts.agentName) {
141
+ console.error(' \u2717 purge requires an agent name: opencastle agents purge <agent>')
142
+ process.exit(1)
143
+ }
144
+ if (!opts.yes) {
145
+ const existing = store.getAgentIdentities(opts.agentName, 1000)
146
+ if (existing.length === 0) {
147
+ console.log(` No identities found for agent "${opts.agentName}".`)
148
+ return
149
+ }
150
+ console.log(`\n This will delete ${existing.length} identities for agent "${opts.agentName}".`)
151
+ console.log(` Use --yes or -y to confirm.`)
152
+ return
153
+ }
154
+ const deleted = store.purgeAgentIdentities(opts.agentName)
155
+ console.log(` \u2713 Purged ${deleted} identities for agent "${opts.agentName}".`)
156
+ break
157
+ }
158
+
159
+ case 'compact': {
160
+ if (!opts.olderThan) {
161
+ console.error(' \u2717 compact requires --older-than <days>')
162
+ process.exit(1)
163
+ }
164
+ const deleted = store.deleteAgentIdentitiesOlderThan(opts.olderThan)
165
+ console.log(` \u2713 Deleted ${deleted} identities older than ${opts.olderThan} days.`)
166
+ break
167
+ }
168
+
169
+ default:
170
+ console.error(` \u2717 Unknown subcommand: ${opts.subcommand}`)
171
+ console.log(HELP)
172
+ process.exit(1)
173
+ }
174
+ } finally {
175
+ store.close()
176
+ }
177
+ }
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'
2
+ import { join, basename } from 'node:path'
3
+ import type { CliContext } from './types.js'
4
+ import { scanForSecrets } from './convoy/gates.js'
5
+
6
+ const HELP = `
7
+ opencastle baselines <subcommand> [options]
8
+
9
+ Manage visual regression baselines.
10
+
11
+ Subcommands:
12
+ update --slug <name> --from <file> Update a baseline from a PNG file
13
+ list List all baselines
14
+
15
+ Options:
16
+ --slug <name> Baseline name (used as filename)
17
+ --from <file> Source PNG file path
18
+ --dir <path> Baselines directory (default: .opencastle/baselines)
19
+ --help, -h Show this help
20
+ `
21
+
22
+ interface BaselinesOptions {
23
+ subcommand: string | null
24
+ slug: string | null
25
+ from: string | null
26
+ dir: string
27
+ help: boolean
28
+ }
29
+
30
+ function parseBaselinesArgs(args: string[]): BaselinesOptions {
31
+ const opts: BaselinesOptions = {
32
+ subcommand: null,
33
+ slug: null,
34
+ from: null,
35
+ dir: '.opencastle/baselines',
36
+ help: false,
37
+ }
38
+ for (let i = 0; i < args.length; i++) {
39
+ const arg = args[i]
40
+ switch (arg) {
41
+ case '--help':
42
+ case '-h':
43
+ opts.help = true
44
+ break
45
+ case '--slug':
46
+ if (i + 1 >= args.length) {
47
+ console.error(' \u2717 --slug requires a name')
48
+ process.exit(1)
49
+ }
50
+ opts.slug = args[++i]
51
+ break
52
+ case '--from':
53
+ if (i + 1 >= args.length) {
54
+ console.error(' \u2717 --from requires a file path')
55
+ process.exit(1)
56
+ }
57
+ opts.from = args[++i]
58
+ break
59
+ case '--dir':
60
+ if (i + 1 >= args.length) {
61
+ console.error(' \u2717 --dir requires a path')
62
+ process.exit(1)
63
+ }
64
+ opts.dir = args[++i]
65
+ break
66
+ default:
67
+ if (arg.startsWith('--')) {
68
+ console.error(` \u2717 Unknown option: ${arg}`)
69
+ console.log(HELP)
70
+ process.exit(1)
71
+ }
72
+ if (!opts.subcommand) {
73
+ opts.subcommand = arg
74
+ }
75
+ }
76
+ }
77
+ return opts
78
+ }
79
+
80
+ export default async function baselines({ args }: CliContext): Promise<void> {
81
+ const opts = parseBaselinesArgs(args)
82
+
83
+ if (opts.help || !opts.subcommand) {
84
+ console.log(HELP)
85
+ return
86
+ }
87
+
88
+ switch (opts.subcommand) {
89
+ case 'update': {
90
+ if (!opts.slug) {
91
+ console.error(' \u2717 update requires --slug <name>')
92
+ process.exit(1)
93
+ }
94
+ if (!/^[a-zA-Z0-9_-]+$/.test(opts.slug)) {
95
+ console.error(' \u2717 Slug must contain only alphanumeric characters, hyphens, and underscores')
96
+ process.exit(1)
97
+ }
98
+ if (!opts.from) {
99
+ console.error(' \u2717 update requires --from <file>')
100
+ process.exit(1)
101
+ }
102
+ if (!existsSync(opts.from)) {
103
+ console.error(` \u2717 Source file not found: ${opts.from}`)
104
+ process.exit(1)
105
+ }
106
+ const data = readFileSync(opts.from)
107
+ const scan = scanForSecrets(data.toString('base64'), opts.from)
108
+ if (!scan.clean) {
109
+ console.error(' \u2717 Source file contains potential secrets \u2014 baseline not updated')
110
+ process.exit(1)
111
+ }
112
+ mkdirSync(opts.dir, { recursive: true })
113
+ const dest = join(opts.dir, `${opts.slug}.png`)
114
+ writeFileSync(dest, data)
115
+ console.log(` \u2713 Baseline updated: ${dest}`)
116
+ break
117
+ }
118
+
119
+ case 'list': {
120
+ if (!existsSync(opts.dir)) {
121
+ console.log(` No baselines directory found at ${opts.dir}`)
122
+ return
123
+ }
124
+ const files = readdirSync(opts.dir).filter((f) => f.endsWith('.png'))
125
+ if (files.length === 0) {
126
+ console.log(' No baselines found.')
127
+ return
128
+ }
129
+ console.log(`\n Baselines in ${opts.dir}:\n`)
130
+ for (const file of files) {
131
+ const stats = statSync(join(opts.dir, file))
132
+ console.log(` ${basename(file, '.png').padEnd(30)} ${(stats.size / 1024).toFixed(1)} KB`)
133
+ }
134
+ console.log()
135
+ break
136
+ }
137
+
138
+ default:
139
+ console.error(` \u2717 Unknown subcommand: ${opts.subcommand}`)
140
+ console.log(HELP)
141
+ process.exit(1)
142
+ }
143
+ }