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.
- package/README.md +7 -1
- package/bin/cli.mjs +10 -0
- package/dist/cli/agents.d.ts +3 -0
- package/dist/cli/agents.d.ts.map +1 -0
- package/dist/cli/agents.js +161 -0
- package/dist/cli/agents.js.map +1 -0
- package/dist/cli/baselines.d.ts +3 -0
- package/dist/cli/baselines.d.ts.map +1 -0
- package/dist/cli/baselines.js +128 -0
- package/dist/cli/baselines.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts +68 -2
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2102 -26
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +1572 -70
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/events.d.ts +4 -1
- package/dist/cli/convoy/events.d.ts.map +1 -1
- package/dist/cli/convoy/events.js +74 -13
- package/dist/cli/convoy/events.js.map +1 -1
- package/dist/cli/convoy/events.test.js +154 -27
- package/dist/cli/convoy/events.test.js.map +1 -1
- package/dist/cli/convoy/expertise.d.ts +16 -0
- package/dist/cli/convoy/expertise.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.js +121 -0
- package/dist/cli/convoy/expertise.js.map +1 -0
- package/dist/cli/convoy/expertise.test.d.ts +2 -0
- package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
- package/dist/cli/convoy/expertise.test.js +96 -0
- package/dist/cli/convoy/expertise.test.js.map +1 -0
- package/dist/cli/convoy/export.test.js +1 -0
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/formula.d.ts +19 -0
- package/dist/cli/convoy/formula.d.ts.map +1 -0
- package/dist/cli/convoy/formula.js +142 -0
- package/dist/cli/convoy/formula.js.map +1 -0
- package/dist/cli/convoy/formula.test.d.ts +2 -0
- package/dist/cli/convoy/formula.test.d.ts.map +1 -0
- package/dist/cli/convoy/formula.test.js +342 -0
- package/dist/cli/convoy/formula.test.js.map +1 -0
- package/dist/cli/convoy/gates.d.ts +128 -0
- package/dist/cli/convoy/gates.d.ts.map +1 -0
- package/dist/cli/convoy/gates.js +606 -0
- package/dist/cli/convoy/gates.js.map +1 -0
- package/dist/cli/convoy/gates.test.d.ts +2 -0
- package/dist/cli/convoy/gates.test.d.ts.map +1 -0
- package/dist/cli/convoy/gates.test.js +976 -0
- package/dist/cli/convoy/gates.test.js.map +1 -0
- package/dist/cli/convoy/health.d.ts +11 -0
- package/dist/cli/convoy/health.d.ts.map +1 -1
- package/dist/cli/convoy/health.js +54 -0
- package/dist/cli/convoy/health.js.map +1 -1
- package/dist/cli/convoy/health.test.js +56 -1
- package/dist/cli/convoy/health.test.js.map +1 -1
- package/dist/cli/convoy/issues.d.ts +8 -0
- package/dist/cli/convoy/issues.d.ts.map +1 -0
- package/dist/cli/convoy/issues.js +98 -0
- package/dist/cli/convoy/issues.js.map +1 -0
- package/dist/cli/convoy/issues.test.d.ts +2 -0
- package/dist/cli/convoy/issues.test.d.ts.map +1 -0
- package/dist/cli/convoy/issues.test.js +107 -0
- package/dist/cli/convoy/issues.test.js.map +1 -0
- package/dist/cli/convoy/knowledge.d.ts +5 -0
- package/dist/cli/convoy/knowledge.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.js +116 -0
- package/dist/cli/convoy/knowledge.js.map +1 -0
- package/dist/cli/convoy/knowledge.test.d.ts +2 -0
- package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
- package/dist/cli/convoy/knowledge.test.js +87 -0
- package/dist/cli/convoy/knowledge.test.js.map +1 -0
- package/dist/cli/convoy/lessons.d.ts +17 -0
- package/dist/cli/convoy/lessons.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.js +149 -0
- package/dist/cli/convoy/lessons.js.map +1 -0
- package/dist/cli/convoy/lessons.test.d.ts +2 -0
- package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
- package/dist/cli/convoy/lessons.test.js +135 -0
- package/dist/cli/convoy/lessons.test.js.map +1 -0
- package/dist/cli/convoy/lock.d.ts +13 -0
- package/dist/cli/convoy/lock.d.ts.map +1 -0
- package/dist/cli/convoy/lock.js +88 -0
- package/dist/cli/convoy/lock.js.map +1 -0
- package/dist/cli/convoy/lock.test.d.ts +2 -0
- package/dist/cli/convoy/lock.test.d.ts.map +1 -0
- package/dist/cli/convoy/lock.test.js +136 -0
- package/dist/cli/convoy/lock.test.js.map +1 -0
- package/dist/cli/convoy/merge.d.ts +4 -0
- package/dist/cli/convoy/merge.d.ts.map +1 -1
- package/dist/cli/convoy/merge.js +18 -1
- package/dist/cli/convoy/merge.js.map +1 -1
- package/dist/cli/convoy/merge.test.js +6 -7
- package/dist/cli/convoy/merge.test.js.map +1 -1
- package/dist/cli/convoy/partition.d.ts +51 -0
- package/dist/cli/convoy/partition.d.ts.map +1 -0
- package/dist/cli/convoy/partition.js +186 -0
- package/dist/cli/convoy/partition.js.map +1 -0
- package/dist/cli/convoy/partition.test.d.ts +2 -0
- package/dist/cli/convoy/partition.test.d.ts.map +1 -0
- package/dist/cli/convoy/partition.test.js +315 -0
- package/dist/cli/convoy/partition.test.js.map +1 -0
- package/dist/cli/convoy/pipeline.test.js +6 -0
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +47 -5
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +525 -19
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +1345 -12
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/types.d.ts +156 -2
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/destroy.d.ts +3 -0
- package/dist/cli/destroy.d.ts.map +1 -0
- package/dist/cli/destroy.js +69 -0
- package/dist/cli/destroy.js.map +1 -0
- package/dist/cli/destroy.test.d.ts +2 -0
- package/dist/cli/destroy.test.d.ts.map +1 -0
- package/dist/cli/destroy.test.js +116 -0
- package/dist/cli/destroy.test.js.map +1 -0
- package/dist/cli/gitignore.d.ts +9 -0
- package/dist/cli/gitignore.d.ts.map +1 -1
- package/dist/cli/gitignore.js +29 -0
- package/dist/cli/gitignore.js.map +1 -1
- package/dist/cli/plan.d.ts +3 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +288 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/run/adapters/claude.d.ts +2 -0
- package/dist/cli/run/adapters/claude.d.ts.map +1 -1
- package/dist/cli/run/adapters/claude.js +89 -49
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/claude.test.d.ts +2 -0
- package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/claude.test.js +205 -0
- package/dist/cli/run/adapters/claude.test.js.map +1 -0
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +84 -46
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
- package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/copilot.test.js +195 -0
- package/dist/cli/run/adapters/copilot.test.js.map +1 -0
- package/dist/cli/run/adapters/cursor.d.ts +1 -0
- package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/run/adapters/cursor.js +83 -47
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
- package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/cursor.test.js +129 -0
- package/dist/cli/run/adapters/cursor.test.js.map +1 -0
- package/dist/cli/run/adapters/opencode.d.ts +1 -0
- package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
- package/dist/cli/run/adapters/opencode.js +81 -47
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
- package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
- package/dist/cli/run/adapters/opencode.test.js +119 -0
- package/dist/cli/run/adapters/opencode.test.js.map +1 -0
- package/dist/cli/run/executor.js +1 -1
- package/dist/cli/run/executor.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +245 -4
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +669 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +362 -22
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +85 -2
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/cli/watch.d.ts +15 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +279 -0
- package/dist/cli/watch.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/agents.ts +177 -0
- package/src/cli/baselines.ts +143 -0
- package/src/cli/convoy/engine.test.ts +1839 -70
- package/src/cli/convoy/engine.ts +2417 -38
- package/src/cli/convoy/events.test.ts +179 -38
- package/src/cli/convoy/events.ts +88 -16
- package/src/cli/convoy/expertise.test.ts +128 -0
- package/src/cli/convoy/expertise.ts +163 -0
- package/src/cli/convoy/export.test.ts +1 -0
- package/src/cli/convoy/formula.test.ts +405 -0
- package/src/cli/convoy/formula.ts +174 -0
- package/src/cli/convoy/gates.test.ts +1169 -0
- package/src/cli/convoy/gates.ts +774 -0
- package/src/cli/convoy/health.test.ts +64 -2
- package/src/cli/convoy/health.ts +80 -2
- package/src/cli/convoy/issues.test.ts +143 -0
- package/src/cli/convoy/issues.ts +136 -0
- package/src/cli/convoy/knowledge.test.ts +101 -0
- package/src/cli/convoy/knowledge.ts +132 -0
- package/src/cli/convoy/lessons.test.ts +188 -0
- package/src/cli/convoy/lessons.ts +164 -0
- package/src/cli/convoy/lock.test.ts +181 -0
- package/src/cli/convoy/lock.ts +103 -0
- package/src/cli/convoy/merge.test.ts +6 -7
- package/src/cli/convoy/merge.ts +19 -1
- package/src/cli/convoy/partition.test.ts +423 -0
- package/src/cli/convoy/partition.ts +232 -0
- package/src/cli/convoy/pipeline.test.ts +6 -0
- package/src/cli/convoy/store.test.ts +1512 -14
- package/src/cli/convoy/store.ts +676 -30
- package/src/cli/convoy/types.ts +170 -1
- package/src/cli/destroy.test.ts +141 -0
- package/src/cli/destroy.ts +88 -0
- package/src/cli/gitignore.ts +36 -0
- package/src/cli/plan.ts +316 -0
- package/src/cli/run/adapters/claude.test.ts +234 -0
- package/src/cli/run/adapters/claude.ts +45 -5
- package/src/cli/run/adapters/copilot.test.ts +224 -0
- package/src/cli/run/adapters/copilot.ts +34 -4
- package/src/cli/run/adapters/cursor.test.ts +144 -0
- package/src/cli/run/adapters/cursor.ts +33 -2
- package/src/cli/run/adapters/opencode.test.ts +135 -0
- package/src/cli/run/adapters/opencode.ts +30 -2
- package/src/cli/run/executor.ts +1 -1
- package/src/cli/run/schema.test.ts +758 -0
- package/src/cli/run/schema.ts +300 -25
- package/src/cli/run.ts +341 -21
- package/src/cli/types.ts +86 -1
- package/src/cli/watch.ts +298 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
package/src/cli/convoy/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed'
|
|
1
|
+
export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed' | 'hook-failed'
|
|
2
2
|
|
|
3
3
|
export type ConvoyTaskStatus =
|
|
4
4
|
| 'pending'
|
|
@@ -6,8 +6,13 @@ export type ConvoyTaskStatus =
|
|
|
6
6
|
| 'running'
|
|
7
7
|
| 'done'
|
|
8
8
|
| 'failed'
|
|
9
|
+
| 'gate-failed'
|
|
10
|
+
| 'review-blocked'
|
|
9
11
|
| 'timed-out'
|
|
10
12
|
| 'skipped'
|
|
13
|
+
| 'hook-failed'
|
|
14
|
+
| 'disputed'
|
|
15
|
+
| 'wait-for-input'
|
|
11
16
|
|
|
12
17
|
export type WorkerStatus = 'spawned' | 'running' | 'done' | 'failed' | 'killed'
|
|
13
18
|
|
|
@@ -26,6 +31,9 @@ export interface ConvoyRecord {
|
|
|
26
31
|
total_tokens: number | null
|
|
27
32
|
total_cost_usd: string | null
|
|
28
33
|
pipeline_id: string | null
|
|
34
|
+
circuit_state: string | null
|
|
35
|
+
review_tokens_total: number | null
|
|
36
|
+
review_budget: number | null
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
export interface TaskRecord {
|
|
@@ -52,6 +60,24 @@ export interface TaskRecord {
|
|
|
52
60
|
completion_tokens: number | null
|
|
53
61
|
total_tokens: number | null
|
|
54
62
|
cost_usd: string | null
|
|
63
|
+
gates: string | null
|
|
64
|
+
on_exhausted: 'dlq' | 'skip' | 'stop'
|
|
65
|
+
injected: number
|
|
66
|
+
provenance: string | null
|
|
67
|
+
idempotency_key: string | null
|
|
68
|
+
current_step: number | null
|
|
69
|
+
total_steps: number | null
|
|
70
|
+
review_level: string | null
|
|
71
|
+
review_verdict: string | null
|
|
72
|
+
review_tokens: number | null
|
|
73
|
+
review_model: string | null
|
|
74
|
+
panel_attempts: number
|
|
75
|
+
dispute_id: string | null
|
|
76
|
+
drift_score: number | null
|
|
77
|
+
drift_retried: number
|
|
78
|
+
outputs?: string | null // JSON array of TaskOutput
|
|
79
|
+
inputs?: string | null // JSON array of TaskInput
|
|
80
|
+
discovered_issues?: string | null // JSON array
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
export interface WorkerRecord {
|
|
@@ -90,3 +116,146 @@ export interface PipelineRecord {
|
|
|
90
116
|
total_tokens: number | null
|
|
91
117
|
total_cost_usd: string | null
|
|
92
118
|
}
|
|
119
|
+
|
|
120
|
+
export interface BuiltInGatesConfig {
|
|
121
|
+
secret_scan?: boolean
|
|
122
|
+
blast_radius?: boolean
|
|
123
|
+
dependency_audit?: 'auto' | boolean
|
|
124
|
+
regression_test?: 'auto' | boolean
|
|
125
|
+
browser_test?: 'auto' | boolean
|
|
126
|
+
gate_timeout?: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
export interface BrowserTestConfig {
|
|
131
|
+
urls: string[]
|
|
132
|
+
check_console_errors?: boolean
|
|
133
|
+
visual_diff_threshold?: number
|
|
134
|
+
a11y?: boolean
|
|
135
|
+
severity_threshold?: 'critical' | 'serious' | 'moderate' | 'minor'
|
|
136
|
+
baselines_dir?: string
|
|
137
|
+
}
|
|
138
|
+
export interface GuardConfig {
|
|
139
|
+
enabled?: boolean // default: true
|
|
140
|
+
agent?: string // optional agent name (e.g. 'session-guard')
|
|
141
|
+
checks?: string[] // e.g. ['observability', 'cleanup', 'cost-report']
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface DlqRecord {
|
|
145
|
+
id: string
|
|
146
|
+
convoy_id: string
|
|
147
|
+
task_id: string
|
|
148
|
+
agent: string
|
|
149
|
+
failure_type: string
|
|
150
|
+
error_output: string | null
|
|
151
|
+
attempts: number
|
|
152
|
+
tokens_spent: number | null
|
|
153
|
+
escalation_task_id: string | null
|
|
154
|
+
resolved: number
|
|
155
|
+
resolution: string | null
|
|
156
|
+
created_at: string
|
|
157
|
+
resolved_at: string | null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface CircuitBreakerConfig {
|
|
161
|
+
threshold?: number // failures before Open (default: 3)
|
|
162
|
+
cooldown_ms?: number // ms in Open before Half-Open (default: 300000 = 5min)
|
|
163
|
+
fallback_agent?: string // reassign pending tasks when circuit opens
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface TaskOutput {
|
|
167
|
+
name: string
|
|
168
|
+
type: 'file' | 'summary' | 'json'
|
|
169
|
+
description?: string
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface TaskInput {
|
|
173
|
+
from: string
|
|
174
|
+
name: string
|
|
175
|
+
as?: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ArtifactRecord {
|
|
179
|
+
id: string
|
|
180
|
+
convoy_id: string
|
|
181
|
+
task_id: string
|
|
182
|
+
name: string
|
|
183
|
+
type: 'file' | 'summary' | 'json'
|
|
184
|
+
content: string
|
|
185
|
+
created_at: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface AgentIdentityRecord {
|
|
189
|
+
id: string
|
|
190
|
+
agent: string
|
|
191
|
+
convoy_id: string
|
|
192
|
+
task_id: string
|
|
193
|
+
summary: string
|
|
194
|
+
created_at: string
|
|
195
|
+
retention_days: number
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface StepCondition {
|
|
199
|
+
step: string // reference previous step by id
|
|
200
|
+
exitCode?: { eq?: number; ne?: number; gt?: number; lt?: number }
|
|
201
|
+
fileExists?: { path: string }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface TaskStep {
|
|
205
|
+
id?: string
|
|
206
|
+
prompt: string
|
|
207
|
+
gates?: string[]
|
|
208
|
+
max_retries?: number // inherits from task if omitted
|
|
209
|
+
if?: StepCondition
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface Hook {
|
|
213
|
+
type: 'review' | 'guard' | 'agent' | 'command' | 'validate'
|
|
214
|
+
name?: string
|
|
215
|
+
prompt?: string // for agent hooks
|
|
216
|
+
command?: string // for command hooks
|
|
217
|
+
on?: 'pre_task' | 'post_task' | 'post_convoy'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface TaskStepRecord {
|
|
221
|
+
id: number
|
|
222
|
+
task_id: string
|
|
223
|
+
step_index: number
|
|
224
|
+
prompt: string
|
|
225
|
+
gates: string | null
|
|
226
|
+
status: string
|
|
227
|
+
exit_code: number | null
|
|
228
|
+
output: string | null
|
|
229
|
+
started_at: string | null
|
|
230
|
+
finished_at: string | null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface WatchTrigger {
|
|
234
|
+
type: 'file-change' | 'cron' | 'git-push'
|
|
235
|
+
glob?: string // for file-change: glob pattern to watch
|
|
236
|
+
schedule?: string // for cron: 5-field cron expression
|
|
237
|
+
branch?: string // for git-push: branch name pattern
|
|
238
|
+
debounce_ms?: number // file-change debounce (default: 500ms)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface WatchConfig {
|
|
242
|
+
triggers: WatchTrigger[]
|
|
243
|
+
clear_scratchpad?: boolean // clear scratchpad on watch start
|
|
244
|
+
scratchpad_retention_days?: number // auto-clear scratchpad entries older than N days
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface ScratchpadRecord {
|
|
248
|
+
key: string
|
|
249
|
+
value: string
|
|
250
|
+
updated_at: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface MCPServerConfig {
|
|
254
|
+
name: string
|
|
255
|
+
type: string
|
|
256
|
+
local?: boolean
|
|
257
|
+
command?: string
|
|
258
|
+
args?: string[]
|
|
259
|
+
url?: string
|
|
260
|
+
config?: Record<string, unknown>
|
|
261
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
|
|
7
|
+
vi.mock('./prompt.js', () => ({
|
|
8
|
+
confirm: vi.fn().mockResolvedValue(true),
|
|
9
|
+
closePrompts: vi.fn(),
|
|
10
|
+
c: {
|
|
11
|
+
green: (s: string) => s,
|
|
12
|
+
dim: (s: string) => s,
|
|
13
|
+
bold: (s: string) => s,
|
|
14
|
+
red: (s: string) => s,
|
|
15
|
+
cyan: (s: string) => s,
|
|
16
|
+
yellow: (s: string) => s,
|
|
17
|
+
magenta: (s: string) => s,
|
|
18
|
+
},
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
import destroy from './destroy.js'
|
|
22
|
+
import { confirm } from './prompt.js'
|
|
23
|
+
import type { Manifest } from './types.js'
|
|
24
|
+
|
|
25
|
+
const START_MARKER = '# >>> OpenCastle managed (do not edit) >>>'
|
|
26
|
+
const END_MARKER = '# <<< OpenCastle managed <<<'
|
|
27
|
+
|
|
28
|
+
async function writeManifestFile(dir: string, manifest: Partial<Manifest> = {}): Promise<void> {
|
|
29
|
+
await mkdir(join(dir, '.opencastle'), { recursive: true })
|
|
30
|
+
const full: Manifest = {
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
ide: 'vscode',
|
|
33
|
+
ides: ['vscode'],
|
|
34
|
+
installedAt: new Date().toISOString(),
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
managedPaths: { framework: [], customizable: [] },
|
|
37
|
+
...manifest,
|
|
38
|
+
}
|
|
39
|
+
await writeFile(join(dir, '.opencastle', 'manifest.json'), JSON.stringify(full, null, 2))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function writeGitignoreWithBlock(dir: string, userEntries = 'node_modules\n'): Promise<void> {
|
|
43
|
+
const block = [userEntries, '', START_MARKER, '.github/', '!.github/customizations/', END_MARKER, ''].join('\n')
|
|
44
|
+
await writeFile(join(dir, '.gitignore'), block)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Tests ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe('destroy', () => {
|
|
50
|
+
let tmpDir: string
|
|
51
|
+
let cwdSpy: ReturnType<typeof vi.spyOn>
|
|
52
|
+
let exitSpy: ReturnType<typeof vi.spyOn>
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'oc-destroy-'))
|
|
56
|
+
cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmpDir)
|
|
57
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
58
|
+
throw new Error('process.exit called')
|
|
59
|
+
})
|
|
60
|
+
vi.mocked(confirm).mockResolvedValue(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
cwdSpy.mockRestore()
|
|
65
|
+
exitSpy.mockRestore()
|
|
66
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('removes all managed framework files', async () => {
|
|
70
|
+
await writeManifestFile(tmpDir, {
|
|
71
|
+
managedPaths: {
|
|
72
|
+
framework: ['.github/instructions/general.instructions.md', '.github/copilot-instructions.md'],
|
|
73
|
+
customizable: [],
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
await mkdir(join(tmpDir, '.github', 'instructions'), { recursive: true })
|
|
77
|
+
await writeFile(join(tmpDir, '.github', 'instructions', 'general.instructions.md'), 'content')
|
|
78
|
+
await writeFile(join(tmpDir, '.github', 'copilot-instructions.md'), 'content')
|
|
79
|
+
|
|
80
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
81
|
+
|
|
82
|
+
expect(existsSync(join(tmpDir, '.github', 'instructions', 'general.instructions.md'))).toBe(false)
|
|
83
|
+
expect(existsSync(join(tmpDir, '.github', 'copilot-instructions.md'))).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('removes .opencastle/ directory', async () => {
|
|
87
|
+
await writeManifestFile(tmpDir)
|
|
88
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(true)
|
|
89
|
+
|
|
90
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
91
|
+
|
|
92
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('removes legacy .opencastle.json manifest', async () => {
|
|
96
|
+
await writeManifestFile(tmpDir)
|
|
97
|
+
const legacyPath = join(tmpDir, '.opencastle.json')
|
|
98
|
+
await writeFile(legacyPath, JSON.stringify({ version: '0.1.0', ide: 'vscode', installedAt: '', updatedAt: '' }))
|
|
99
|
+
|
|
100
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
101
|
+
|
|
102
|
+
expect(existsSync(legacyPath)).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('cleans the gitignore block but keeps user entries', async () => {
|
|
106
|
+
await writeManifestFile(tmpDir)
|
|
107
|
+
await writeGitignoreWithBlock(tmpDir, 'node_modules\ndist\n')
|
|
108
|
+
|
|
109
|
+
await destroy({ pkgRoot: tmpDir, args: [] })
|
|
110
|
+
|
|
111
|
+
const gitignorePath = join(tmpDir, '.gitignore')
|
|
112
|
+
expect(existsSync(gitignorePath)).toBe(true)
|
|
113
|
+
const { readFile } = await import('node:fs/promises')
|
|
114
|
+
const content = await readFile(gitignorePath, 'utf8')
|
|
115
|
+
expect(content).not.toContain(START_MARKER)
|
|
116
|
+
expect(content).not.toContain(END_MARKER)
|
|
117
|
+
expect(content).toContain('node_modules')
|
|
118
|
+
expect(content).toContain('dist')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('dry-run makes no changes', async () => {
|
|
122
|
+
await writeManifestFile(tmpDir, {
|
|
123
|
+
managedPaths: { framework: ['some-file.md'], customizable: [] },
|
|
124
|
+
})
|
|
125
|
+
await writeFile(join(tmpDir, 'some-file.md'), 'content')
|
|
126
|
+
await writeGitignoreWithBlock(tmpDir)
|
|
127
|
+
|
|
128
|
+
await destroy({ pkgRoot: tmpDir, args: ['--dry-run'] })
|
|
129
|
+
|
|
130
|
+
expect(existsSync(join(tmpDir, 'some-file.md'))).toBe(true)
|
|
131
|
+
expect(existsSync(join(tmpDir, '.opencastle'))).toBe(true)
|
|
132
|
+
const { readFile } = await import('node:fs/promises')
|
|
133
|
+
const gitignore = await readFile(join(tmpDir, '.gitignore'), 'utf8')
|
|
134
|
+
expect(gitignore).toContain(START_MARKER)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('exits with error when no manifest found', async () => {
|
|
138
|
+
await expect(destroy({ pkgRoot: tmpDir, args: [] })).rejects.toThrow('process.exit called')
|
|
139
|
+
expect(exitSpy).toHaveBeenCalledWith(1)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { unlink } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { readManifest } from './manifest.js'
|
|
5
|
+
import { removeDirIfExists } from './copy.js'
|
|
6
|
+
import { removeGitignoreBlock } from './gitignore.js'
|
|
7
|
+
import { confirm, closePrompts, c } from './prompt.js'
|
|
8
|
+
import type { CliContext } from './types.js'
|
|
9
|
+
|
|
10
|
+
export default async function destroy({
|
|
11
|
+
pkgRoot: _pkgRoot,
|
|
12
|
+
args,
|
|
13
|
+
}: CliContext): Promise<void> {
|
|
14
|
+
const projectRoot = process.cwd()
|
|
15
|
+
const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
|
|
16
|
+
|
|
17
|
+
const manifest = await readManifest(projectRoot)
|
|
18
|
+
if (!manifest) {
|
|
19
|
+
console.error(' ✗ No OpenCastle installation found.')
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const frameworkPaths = manifest.managedPaths?.framework ?? []
|
|
24
|
+
const customizablePaths = manifest.managedPaths?.customizable ?? []
|
|
25
|
+
const legacyManifestPath = resolve(projectRoot, '.opencastle.json')
|
|
26
|
+
const hasLegacy = existsSync(legacyManifestPath)
|
|
27
|
+
|
|
28
|
+
console.log(`\n 🏰 OpenCastle destroy\n`)
|
|
29
|
+
console.log(' This will permanently remove:\n')
|
|
30
|
+
|
|
31
|
+
for (const p of frameworkPaths) {
|
|
32
|
+
console.log(` ${c.dim(p)}`)
|
|
33
|
+
}
|
|
34
|
+
for (const p of customizablePaths) {
|
|
35
|
+
console.log(` ${c.dim(p)}`)
|
|
36
|
+
}
|
|
37
|
+
console.log(` ${c.dim('.opencastle/')}`)
|
|
38
|
+
if (hasLegacy) {
|
|
39
|
+
console.log(` ${c.dim('.opencastle.json')}`)
|
|
40
|
+
}
|
|
41
|
+
console.log(` ${c.dim('.gitignore block')}\n`)
|
|
42
|
+
|
|
43
|
+
if (dryRun) {
|
|
44
|
+
console.log(' [dry-run] No files were changed.\n')
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const proceed = await confirm(
|
|
49
|
+
'This will permanently delete all OpenCastle files. Continue?',
|
|
50
|
+
false
|
|
51
|
+
)
|
|
52
|
+
if (!proceed) {
|
|
53
|
+
console.log(' Aborted.')
|
|
54
|
+
closePrompts()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let removed = 0
|
|
59
|
+
|
|
60
|
+
for (const p of [...frameworkPaths, ...customizablePaths]) {
|
|
61
|
+
if (p.endsWith('/')) {
|
|
62
|
+
const dir = resolve(projectRoot, p)
|
|
63
|
+
await removeDirIfExists(dir)
|
|
64
|
+
removed++
|
|
65
|
+
} else {
|
|
66
|
+
const file = resolve(projectRoot, p)
|
|
67
|
+
if (existsSync(file)) {
|
|
68
|
+
await unlink(file)
|
|
69
|
+
removed++
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await removeDirIfExists(resolve(projectRoot, '.opencastle'))
|
|
75
|
+
removed++
|
|
76
|
+
|
|
77
|
+
if (hasLegacy) {
|
|
78
|
+
await unlink(legacyManifestPath)
|
|
79
|
+
removed++
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const gitignoreResult = await removeGitignoreBlock(projectRoot)
|
|
83
|
+
|
|
84
|
+
console.log(`\n ${c.green('✓')} Removed ${removed} path(s)${gitignoreResult === 'removed' ? ' + .gitignore block' : ''}.`)
|
|
85
|
+
console.log(` You can uninstall: ${c.bold('npm uninstall opencastle')}\n`)
|
|
86
|
+
|
|
87
|
+
closePrompts()
|
|
88
|
+
}
|
package/src/cli/gitignore.ts
CHANGED
|
@@ -72,3 +72,39 @@ export async function updateGitignore(
|
|
|
72
72
|
await writeFile(gitignorePath, existing + separator + block + '\n', 'utf8')
|
|
73
73
|
return 'updated'
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Remove the OpenCastle managed block from `.gitignore`.
|
|
78
|
+
*
|
|
79
|
+
* - No-op if no `.gitignore` exists or no block is present.
|
|
80
|
+
* - Cleans up resulting double blank lines.
|
|
81
|
+
* - Deletes `.gitignore` if the file becomes empty after removal.
|
|
82
|
+
* - Returns 'removed' or 'unchanged'.
|
|
83
|
+
*/
|
|
84
|
+
export async function removeGitignoreBlock(
|
|
85
|
+
projectRoot: string
|
|
86
|
+
): Promise<'removed' | 'unchanged'> {
|
|
87
|
+
const gitignorePath = resolve(projectRoot, '.gitignore')
|
|
88
|
+
if (!existsSync(gitignorePath)) return 'unchanged'
|
|
89
|
+
|
|
90
|
+
const existing = await readFile(gitignorePath, 'utf8')
|
|
91
|
+
const startIdx = existing.indexOf(START_MARKER)
|
|
92
|
+
const endIdx = existing.indexOf(END_MARKER)
|
|
93
|
+
|
|
94
|
+
if (startIdx === -1 || endIdx === -1) return 'unchanged'
|
|
95
|
+
|
|
96
|
+
const before = existing.slice(0, startIdx)
|
|
97
|
+
const after = existing.slice(endIdx + END_MARKER.length)
|
|
98
|
+
|
|
99
|
+
// Collapse consecutive blank lines left by removal
|
|
100
|
+
const updated = (before + after).replace(/\n{3,}/g, '\n\n').trimEnd()
|
|
101
|
+
|
|
102
|
+
if (!updated) {
|
|
103
|
+
const { unlink } = await import('node:fs/promises')
|
|
104
|
+
await unlink(gitignorePath)
|
|
105
|
+
return 'removed'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await writeFile(gitignorePath, updated + '\n', 'utf8')
|
|
109
|
+
return 'removed'
|
|
110
|
+
}
|