opencastle 0.31.6 → 0.32.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.
- package/LICENSE +93 -21
- package/README.md +9 -3
- package/bin/cli.mjs +15 -0
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +19 -5
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/artifacts-cli.d.ts +3 -0
- package/dist/cli/artifacts-cli.d.ts.map +1 -0
- package/dist/cli/artifacts-cli.js +36 -0
- package/dist/cli/artifacts-cli.js.map +1 -0
- package/dist/cli/baselines.d.ts.map +1 -1
- package/dist/cli/baselines.js +11 -0
- package/dist/cli/baselines.js.map +1 -1
- package/dist/cli/convoy/artifacts.d.ts +25 -0
- package/dist/cli/convoy/artifacts.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.js +129 -0
- package/dist/cli/convoy/artifacts.js.map +1 -0
- package/dist/cli/convoy/artifacts.test.d.ts +2 -0
- package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.test.js +169 -0
- package/dist/cli/convoy/artifacts.test.js.map +1 -0
- package/dist/cli/convoy/compaction.d.ts +23 -0
- package/dist/cli/convoy/compaction.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.js +117 -0
- package/dist/cli/convoy/compaction.js.map +1 -0
- package/dist/cli/convoy/compaction.test.d.ts +2 -0
- package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.test.js +205 -0
- package/dist/cli/convoy/compaction.test.js.map +1 -0
- package/dist/cli/convoy/contracts.d.ts +22 -0
- package/dist/cli/convoy/contracts.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.js +254 -0
- package/dist/cli/convoy/contracts.js.map +1 -0
- package/dist/cli/convoy/contracts.test.d.ts +2 -0
- package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.test.js +239 -0
- package/dist/cli/convoy/contracts.test.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.d.ts +40 -0
- package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.js +282 -0
- package/dist/cli/convoy/dag-analysis.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.js +289 -0
- package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.d.ts +20 -0
- package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.js +82 -0
- package/dist/cli/convoy/effort-scaling.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.js +120 -0
- package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +298 -11
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +155 -18
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
- package/dist/cli/convoy/event-schemas.js +55 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -1
- package/dist/cli/convoy/isolation.d.ts +27 -0
- package/dist/cli/convoy/isolation.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.js +120 -0
- package/dist/cli/convoy/isolation.js.map +1 -0
- package/dist/cli/convoy/isolation.test.d.ts +2 -0
- package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.test.js +105 -0
- package/dist/cli/convoy/isolation.test.js.map +1 -0
- package/dist/cli/convoy/review-stages.d.ts +9 -0
- package/dist/cli/convoy/review-stages.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.js +134 -0
- package/dist/cli/convoy/review-stages.js.map +1 -0
- package/dist/cli/convoy/review-stages.test.d.ts +2 -0
- package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.test.js +197 -0
- package/dist/cli/convoy/review-stages.test.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.d.ts +39 -0
- package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.js +239 -0
- package/dist/cli/convoy/skill-refinement.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.js +230 -0
- package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
- package/dist/cli/convoy/spec-builder.d.ts +1 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
- package/dist/cli/convoy/spec-builder.js +11 -0
- package/dist/cli/convoy/spec-builder.js.map +1 -1
- package/dist/cli/convoy/spec-builder.test.js +54 -0
- package/dist/cli/convoy/spec-builder.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +3 -2
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +20 -2
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +15 -15
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/tdd-gate.d.ts +15 -0
- package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.js +119 -0
- package/dist/cli/convoy/tdd-gate.js.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.js +227 -0
- package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
- package/dist/cli/convoy/types.d.ts +91 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +8 -0
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +54 -0
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/insights.d.ts +3 -0
- package/dist/cli/insights.d.ts.map +1 -0
- package/dist/cli/insights.js +94 -0
- package/dist/cli/insights.js.map +1 -0
- package/dist/cli/lesson.d.ts.map +1 -1
- package/dist/cli/lesson.js +7 -0
- package/dist/cli/lesson.js.map +1 -1
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +7 -0
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/package-config.d.ts +12 -0
- package/dist/cli/package-config.d.ts.map +1 -0
- package/dist/cli/package-config.js +37 -0
- package/dist/cli/package-config.js.map +1 -0
- package/dist/cli/package.d.ts +23 -0
- package/dist/cli/package.d.ts.map +1 -0
- package/dist/cli/package.js +285 -0
- package/dist/cli/package.js.map +1 -0
- package/dist/cli/package.test.d.ts +2 -0
- package/dist/cli/package.test.d.ts.map +1 -0
- package/dist/cli/package.test.js +236 -0
- package/dist/cli/package.test.js.map +1 -0
- package/dist/cli/pipeline.d.ts +6 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +15 -2
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +32 -0
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +51 -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 +10 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +107 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/types.d.ts +4 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.js +2 -2
- package/package.json +3 -2
- package/src/cli/agents.ts +20 -5
- package/src/cli/artifacts-cli.ts +41 -0
- package/src/cli/baselines.ts +12 -0
- package/src/cli/convoy/artifacts.test.ts +201 -0
- package/src/cli/convoy/artifacts.ts +186 -0
- package/src/cli/convoy/compaction.test.ts +245 -0
- package/src/cli/convoy/compaction.ts +164 -0
- package/src/cli/convoy/contracts.test.ts +279 -0
- package/src/cli/convoy/contracts.ts +280 -0
- package/src/cli/convoy/dag-analysis.test.ts +349 -0
- package/src/cli/convoy/dag-analysis.ts +371 -0
- package/src/cli/convoy/effort-scaling.test.ts +140 -0
- package/src/cli/convoy/effort-scaling.ts +90 -0
- package/src/cli/convoy/engine.test.ts +175 -18
- package/src/cli/convoy/engine.ts +315 -12
- package/src/cli/convoy/event-schemas.ts +55 -0
- package/src/cli/convoy/isolation.test.ts +137 -0
- package/src/cli/convoy/isolation.ts +165 -0
- package/src/cli/convoy/review-stages.test.ts +235 -0
- package/src/cli/convoy/review-stages.ts +166 -0
- package/src/cli/convoy/skill-refinement.test.ts +277 -0
- package/src/cli/convoy/skill-refinement.ts +306 -0
- package/src/cli/convoy/spec-builder.test.ts +61 -0
- package/src/cli/convoy/spec-builder.ts +9 -0
- package/src/cli/convoy/store.test.ts +15 -15
- package/src/cli/convoy/store.ts +26 -4
- package/src/cli/convoy/tdd-gate.test.ts +281 -0
- package/src/cli/convoy/tdd-gate.ts +154 -0
- package/src/cli/convoy/types.ts +51 -0
- package/src/cli/dashboard.ts +55 -0
- package/src/cli/insights.ts +99 -0
- package/src/cli/lesson.ts +8 -0
- package/src/cli/log.ts +8 -0
- package/src/cli/package-config.ts +48 -0
- package/src/cli/package.test.ts +276 -0
- package/src/cli/package.ts +329 -0
- package/src/cli/pipeline.ts +21 -2
- package/src/cli/run/schema.test.ts +58 -0
- package/src/cli/run/schema.ts +33 -0
- package/src/cli/run.ts +14 -1
- package/src/cli/skills.ts +121 -0
- package/src/cli/types.ts +4 -1
- package/src/cli/update.ts +2 -2
- package/src/dashboard/dist/_astro/{index.Je1YjU_y.css → index.BRDFmNzR.css} +1 -1
- package/src/dashboard/dist/index.html +163 -2
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +162 -1
- package/src/dashboard/src/styles/dashboard.css +85 -0
- package/src/orchestrator/agents/developer.agent.md +8 -0
- package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -0
- package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +18 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +61 -0
- package/src/orchestrator/skills/decomposition/SKILL.md +35 -0
- package/src/orchestrator/skills/frontend-design/SKILL.md +27 -1
- package/src/orchestrator/skills/project-consistency/SKILL.md +350 -0
package/src/cli/convoy/types.ts
CHANGED
|
@@ -75,9 +75,11 @@ export interface TaskRecord {
|
|
|
75
75
|
dispute_id: string | null
|
|
76
76
|
drift_score: number | null
|
|
77
77
|
drift_retried: number
|
|
78
|
+
compaction_count: number
|
|
78
79
|
outputs?: string | null // JSON array of TaskOutput
|
|
79
80
|
inputs?: string | null // JSON array of TaskInput
|
|
80
81
|
discovered_issues?: string | null // JSON array
|
|
82
|
+
contract_result?: string | null // JSON ContractResult
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
export interface WorkerRecord {
|
|
@@ -117,6 +119,15 @@ export interface PipelineRecord {
|
|
|
117
119
|
total_cost_usd: number | null
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
export interface TDDGateConfig {
|
|
123
|
+
enabled: boolean
|
|
124
|
+
source_patterns: string[]
|
|
125
|
+
test_patterns: string[]
|
|
126
|
+
exclude_patterns: string[]
|
|
127
|
+
mode: 'warn' | 'block'
|
|
128
|
+
exempt_agents: string[]
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
export interface BuiltInGatesConfig {
|
|
121
132
|
secret_scan?: boolean
|
|
122
133
|
blast_radius?: boolean
|
|
@@ -124,6 +135,7 @@ export interface BuiltInGatesConfig {
|
|
|
124
135
|
regression_test?: 'auto' | boolean
|
|
125
136
|
browser_test?: 'auto' | boolean
|
|
126
137
|
gate_timeout?: number
|
|
138
|
+
tdd_check?: boolean | TDDGateConfig
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
|
|
@@ -163,6 +175,12 @@ export interface CircuitBreakerConfig {
|
|
|
163
175
|
fallback_agent?: string // reassign pending tasks when circuit opens
|
|
164
176
|
}
|
|
165
177
|
|
|
178
|
+
export interface CompactionConfig {
|
|
179
|
+
enabled: boolean
|
|
180
|
+
token_threshold_pct: number // e.g., 70 = compact at 70% of model context window
|
|
181
|
+
summary_max_tokens: number // max tokens for the compaction summary
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
export interface TaskOutput {
|
|
167
185
|
name: string
|
|
168
186
|
type: 'file' | 'summary' | 'json'
|
|
@@ -260,6 +278,23 @@ export interface MCPServerConfig {
|
|
|
260
278
|
config?: Record<string, unknown>
|
|
261
279
|
}
|
|
262
280
|
|
|
281
|
+
// ── Two-stage review ─────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
export type ReviewStage = 'spec-compliance' | 'code-quality'
|
|
284
|
+
|
|
285
|
+
export interface StageVerdict {
|
|
286
|
+
stage: ReviewStage
|
|
287
|
+
verdict: 'pass' | 'block'
|
|
288
|
+
issues: string[]
|
|
289
|
+
tokens_used: number
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface TwoStageReviewResult {
|
|
293
|
+
stages: StageVerdict[]
|
|
294
|
+
overall_verdict: 'pass' | 'block'
|
|
295
|
+
total_tokens: number
|
|
296
|
+
}
|
|
297
|
+
|
|
263
298
|
// ---------------------------------------------------------------------------
|
|
264
299
|
// Discriminated union covering every canonical convoy event type.
|
|
265
300
|
// Each variant constrains the `data` shape that callers may pass to emit().
|
|
@@ -321,6 +356,14 @@ export type ConvoyEventType =
|
|
|
321
356
|
| { type: 'watch_stopped'; data?: { reason?: string } }
|
|
322
357
|
| { type: 'worker_killed'; data?: { reason?: string; worker_id?: string; task_id?: string } }
|
|
323
358
|
| { type: 'discovered_issue'; data?: { task_id?: string; title?: string; file?: string; description?: string; severity?: string } }
|
|
359
|
+
| { type: 'contract_violation'; data?: { task_id?: string; agent?: string; missing?: string[]; warnings?: string[] } }
|
|
360
|
+
| { type: 'review_stage_completed'; data?: { stage: string; verdict: string; tokens: number; task_id?: string; model?: string } }
|
|
361
|
+
| { type: 'partition_violation'; data?: { task_id?: string; allowed?: string[]; actual?: string[]; violations?: string[] } }
|
|
362
|
+
| { type: 'context_compacted'; data?: { task_id?: string; compaction_count?: number; summary_path?: string; model?: string; tokens_used?: number } }
|
|
363
|
+
| { type: 'skill_refinement_proposed'; data?: { skill_name?: string; proposal_path?: string; failure_count?: number; confidence?: string } }
|
|
364
|
+
| { type: 'tdd_check_passed'; data?: { task_id?: string; new_source_files?: number; existing_test_files?: number } }
|
|
365
|
+
| { type: 'tdd_check_failed'; data?: { task_id?: string; missing_test_files?: string[]; new_source_files?: number } }
|
|
366
|
+
| { type: 'tdd_check_skipped'; data?: { task_id?: string; reason?: string; agent?: string } }
|
|
324
367
|
|
|
325
368
|
/** All canonical convoy event type strings. Used for runtime validation. */
|
|
326
369
|
export const KNOWN_EVENT_TYPES: Set<string> = new Set<ConvoyEventType['type']>([
|
|
@@ -363,4 +406,12 @@ export const KNOWN_EVENT_TYPES: Set<string> = new Set<ConvoyEventType['type']>([
|
|
|
363
406
|
'watch_stopped',
|
|
364
407
|
'worker_killed',
|
|
365
408
|
'discovered_issue',
|
|
409
|
+
'contract_violation',
|
|
410
|
+
'review_stage_completed',
|
|
411
|
+
'partition_violation',
|
|
412
|
+
'context_compacted',
|
|
413
|
+
'skill_refinement_proposed',
|
|
414
|
+
'tdd_check_passed',
|
|
415
|
+
'tdd_check_failed',
|
|
416
|
+
'tdd_check_skipped',
|
|
366
417
|
])
|
package/src/cli/dashboard.ts
CHANGED
|
@@ -185,6 +185,61 @@ export async function startDashboardServer(
|
|
|
185
185
|
pathname = '/index.html'
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// Handle refresh requests — re-run ETL on demand
|
|
189
|
+
if (pathname === '/data/refresh' && runtimeDataDir) {
|
|
190
|
+
try {
|
|
191
|
+
const { runEtl } = await import('../dashboard/scripts/etl.js')
|
|
192
|
+
const dbPath = resolve(projectRoot, '.opencastle', 'convoy.db')
|
|
193
|
+
const result = await runEtl({ dbPath, outputDir: runtimeDataDir })
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
195
|
+
res.end(JSON.stringify({ ok: true, convoyCount: result.convoyCount }))
|
|
196
|
+
} catch (err) {
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
198
|
+
res.end(JSON.stringify({ ok: true, convoyCount: 0 }))
|
|
199
|
+
}
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle active convoy resolution — queries DB directly (no ETL needed)
|
|
204
|
+
if (pathname === '/data/active-convoy.json' && !seed) {
|
|
205
|
+
try {
|
|
206
|
+
const { createConvoyStore } = await import('./convoy/store.js')
|
|
207
|
+
const dbPath = resolve(projectRoot, '.opencastle', 'convoy.db')
|
|
208
|
+
const { existsSync } = await import('node:fs')
|
|
209
|
+
if (!existsSync(dbPath)) {
|
|
210
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
211
|
+
res.end(JSON.stringify({ convoy: null, pipeline: null }))
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
const store = createConvoyStore(dbPath)
|
|
215
|
+
try {
|
|
216
|
+
// Find the running convoy, or fall back to the latest
|
|
217
|
+
const allConvoys = store.getConvoyList(10, 0)
|
|
218
|
+
const running = allConvoys.find(c => c.status === 'running')
|
|
219
|
+
const pending = allConvoys.find(c => c.status === 'pending')
|
|
220
|
+
const target = running ?? pending ?? (allConvoys.length > 0 ? allConvoys[0] : null)
|
|
221
|
+
|
|
222
|
+
let pipelineConvoys: Array<{ id: string; name: string; status: string }> | null = null
|
|
223
|
+
if (target?.pipeline_id) {
|
|
224
|
+
const pipeConvoys = store.getConvoysByPipeline(target.pipeline_id)
|
|
225
|
+
pipelineConvoys = pipeConvoys.map(c => ({ id: c.id, name: c.name, status: c.status }))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
229
|
+
res.end(JSON.stringify({
|
|
230
|
+
convoy: target ? { id: target.id, name: target.name, status: target.status, pipeline_id: target.pipeline_id ?? null } : null,
|
|
231
|
+
pipeline: pipelineConvoys,
|
|
232
|
+
}))
|
|
233
|
+
} finally {
|
|
234
|
+
store.close()
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
238
|
+
res.end(JSON.stringify({ convoy: null, pipeline: null }))
|
|
239
|
+
}
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
188
243
|
// Handle data file requests — proxy to project logs or dist
|
|
189
244
|
const dataMatch = pathname.match(/^\/data\/(.+\.ndjson)$/)
|
|
190
245
|
if (dataMatch && DATA_FILES.includes(dataMatch[1])) {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import type { CliContext } from './types.js'
|
|
4
|
+
import { createConvoyStore } from './convoy/store.js'
|
|
5
|
+
import { analyzeDAG, formatInsightsMarkdown, formatInsightsJSON } from './convoy/dag-analysis.js'
|
|
6
|
+
|
|
7
|
+
const HELP = `
|
|
8
|
+
opencastle insights [options]
|
|
9
|
+
|
|
10
|
+
Analyze convoy execution history and generate recommendations.
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--json Output machine-readable JSON instead of markdown
|
|
14
|
+
--since <days> Limit analysis window (default: 90 days)
|
|
15
|
+
--db <path> Path to convoy.db (default: auto-detect)
|
|
16
|
+
--help, -h Show this help
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
/** Walk up the directory tree to find .opencastle/convoy.db. */
|
|
20
|
+
async function findConvoyDb(override?: string | null): Promise<string | null> {
|
|
21
|
+
if (override) return override
|
|
22
|
+
|
|
23
|
+
let dir = process.cwd()
|
|
24
|
+
for (;;) {
|
|
25
|
+
const candidate = join(dir, '.opencastle', 'convoy.db')
|
|
26
|
+
try {
|
|
27
|
+
const s = await stat(candidate)
|
|
28
|
+
if (s.isFile()) return candidate
|
|
29
|
+
} catch {
|
|
30
|
+
// not found here — walk up
|
|
31
|
+
}
|
|
32
|
+
const parent = dirname(dir)
|
|
33
|
+
if (parent === dir) break
|
|
34
|
+
dir = parent
|
|
35
|
+
}
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function insights({ args }: CliContext): Promise<void> {
|
|
40
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
41
|
+
console.log(HELP)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let jsonMode = false
|
|
46
|
+
let sinceDays = 90
|
|
47
|
+
let dbOverride: string | null = null
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < args.length; i++) {
|
|
50
|
+
const arg = args[i]
|
|
51
|
+
if (arg === '--json') {
|
|
52
|
+
jsonMode = true
|
|
53
|
+
} else if (arg === '--since') {
|
|
54
|
+
const raw = args[++i]
|
|
55
|
+
const parsed = parseInt(raw, 10)
|
|
56
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
57
|
+
console.error(' \u2717 --since requires a positive integer (number of days)')
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
sinceDays = parsed
|
|
61
|
+
} else if (arg === '--db') {
|
|
62
|
+
dbOverride = args[++i] ?? null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dbPath = await findConvoyDb(dbOverride)
|
|
67
|
+
if (!dbPath) {
|
|
68
|
+
if (jsonMode) {
|
|
69
|
+
console.log(
|
|
70
|
+
JSON.stringify(
|
|
71
|
+
{
|
|
72
|
+
patterns: [],
|
|
73
|
+
agent_stats: [],
|
|
74
|
+
insights: ['No execution history available yet. Run some convoys first.'],
|
|
75
|
+
generated_at: new Date().toISOString(),
|
|
76
|
+
},
|
|
77
|
+
null,
|
|
78
|
+
2,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
} else {
|
|
82
|
+
console.log(' No convoy.db found. Run some convoys first.\n')
|
|
83
|
+
console.log(' Tip: convoy.db is created automatically when you run `opencastle run`.')
|
|
84
|
+
}
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const store = createConvoyStore(dbPath)
|
|
89
|
+
try {
|
|
90
|
+
const rec = analyzeDAG(store, sinceDays)
|
|
91
|
+
if (jsonMode) {
|
|
92
|
+
console.log(formatInsightsJSON(rec))
|
|
93
|
+
} else {
|
|
94
|
+
console.log(formatInsightsMarkdown(rec))
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
store.close()
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/cli/lesson.ts
CHANGED
|
@@ -38,6 +38,7 @@ const HELP = `
|
|
|
38
38
|
--correct <text> The correct approach that works
|
|
39
39
|
--why <text> Root cause explanation
|
|
40
40
|
--customizations-dir <p> Override the customizations directory path
|
|
41
|
+
--dry-run Preview what would be appended without writing
|
|
41
42
|
--help, -h Show this help
|
|
42
43
|
|
|
43
44
|
Examples:
|
|
@@ -230,6 +231,7 @@ export default async function lesson({ args }: CliContext): Promise<void> {
|
|
|
230
231
|
return
|
|
231
232
|
}
|
|
232
233
|
|
|
234
|
+
const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
|
|
233
235
|
let title: string | null = null
|
|
234
236
|
let category: string | null = null
|
|
235
237
|
let severity: string | null = null
|
|
@@ -299,6 +301,12 @@ export default async function lesson({ args }: CliContext): Promise<void> {
|
|
|
299
301
|
process.exit(1)
|
|
300
302
|
}
|
|
301
303
|
|
|
304
|
+
if (dryRun) {
|
|
305
|
+
console.log(` [dry-run] Would append lesson to LESSONS-LEARNED.md:`)
|
|
306
|
+
console.log(` Title: ${title}`)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
302
310
|
try {
|
|
303
311
|
const id = await appendLesson(
|
|
304
312
|
{ title: title!, category: category!, severity: severity!, problem: problem!, wrong, correct, why },
|
package/src/cli/log.ts
CHANGED
|
@@ -11,6 +11,7 @@ const HELP = `
|
|
|
11
11
|
--type <type> Event type (required): session|delegation|review|panel|dispute
|
|
12
12
|
--<field> <value> Any field from the event schema (see documentation)
|
|
13
13
|
--logs-dir <path> Override the logs directory path
|
|
14
|
+
--dry-run Preview what would be logged without writing
|
|
14
15
|
--help, -h Show this help
|
|
15
16
|
|
|
16
17
|
Array fields (comma-separated): file_partition, lessons_added, discoveries, reviewing_agents
|
|
@@ -77,6 +78,7 @@ export default async function log({ args }: CliContext): Promise<void> {
|
|
|
77
78
|
return
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
|
|
80
82
|
let type: string | null = null
|
|
81
83
|
let logsDir: string | null = null
|
|
82
84
|
const fields: Record<string, unknown> = {}
|
|
@@ -122,6 +124,12 @@ export default async function log({ args }: CliContext): Promise<void> {
|
|
|
122
124
|
delete fields['timestamp']
|
|
123
125
|
const record = { type, timestamp, ...fields }
|
|
124
126
|
|
|
127
|
+
if (dryRun) {
|
|
128
|
+
console.log(` [dry-run] Would append to events.ndjson:`)
|
|
129
|
+
console.log(JSON.stringify(record))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
try {
|
|
126
134
|
await appendEvent(record, logsDir)
|
|
127
135
|
console.log(JSON.stringify(record))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Platform skill compatibility -- not all skills are relevant on all platforms
|
|
2
|
+
|
|
3
|
+
export interface PlatformSkillConfig {
|
|
4
|
+
platform: 'claude-code' | 'cursor' | 'opencode' | 'gemini'
|
|
5
|
+
excludedSkills: string[]
|
|
6
|
+
displayName: string
|
|
7
|
+
manifestFile: string
|
|
8
|
+
entryPoint: string
|
|
9
|
+
outputDir: string
|
|
10
|
+
includedDirs: ('skills' | 'agents' | 'instructions' | 'prompts' | 'agent-workflows')[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SESSION_ONLY = ['session-checkpoints']
|
|
14
|
+
const CONCURRENT_ONLY = ['panel-majority-vote']
|
|
15
|
+
const TEAM_LEAD_ONLY = [
|
|
16
|
+
'orchestration-protocols', 'team-lead-reference', 'decomposition',
|
|
17
|
+
'agent-memory', 'context-map', 'fast-review', 'memory-merger',
|
|
18
|
+
]
|
|
19
|
+
const SIMPLE_PLATFORM_EXCLUSIONS = [...SESSION_ONLY, ...CONCURRENT_ONLY, ...TEAM_LEAD_ONLY]
|
|
20
|
+
|
|
21
|
+
export const PLATFORM_CONFIGS: Record<string, PlatformSkillConfig> = {
|
|
22
|
+
'claude-code': {
|
|
23
|
+
platform: 'claude-code', excludedSkills: [], displayName: 'Claude Code',
|
|
24
|
+
manifestFile: 'manifest.json', entryPoint: 'CLAUDE.md', outputDir: 'claude-code',
|
|
25
|
+
includedDirs: ['skills', 'agents', 'instructions', 'prompts', 'agent-workflows'],
|
|
26
|
+
},
|
|
27
|
+
cursor: {
|
|
28
|
+
platform: 'cursor', excludedSkills: [...SESSION_ONLY], displayName: 'Cursor',
|
|
29
|
+
manifestFile: 'manifest.json', entryPoint: '.cursorrules', outputDir: 'cursor',
|
|
30
|
+
includedDirs: ['skills', 'agents', 'instructions'],
|
|
31
|
+
},
|
|
32
|
+
opencode: {
|
|
33
|
+
platform: 'opencode', excludedSkills: [...SIMPLE_PLATFORM_EXCLUSIONS], displayName: 'OpenCode',
|
|
34
|
+
manifestFile: 'manifest.json', entryPoint: 'OPENCODE.md', outputDir: 'opencode',
|
|
35
|
+
includedDirs: ['skills', 'agents', 'instructions'],
|
|
36
|
+
},
|
|
37
|
+
gemini: {
|
|
38
|
+
platform: 'gemini', excludedSkills: [...SIMPLE_PLATFORM_EXCLUSIONS], displayName: 'Gemini CLI',
|
|
39
|
+
manifestFile: 'gemini-extension.json', entryPoint: 'GEMINI.md', outputDir: 'gemini',
|
|
40
|
+
includedDirs: ['skills', 'agents', 'instructions'],
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getSkillsForPlatform(platform: string, allSkills: string[]): string[] {
|
|
45
|
+
const config = PLATFORM_CONFIGS[platform]
|
|
46
|
+
if (!config) return allSkills
|
|
47
|
+
return allSkills.filter(skill => !config.excludedSkills.includes(skill))
|
|
48
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
readVersion,
|
|
7
|
+
listSkillDirs,
|
|
8
|
+
generateManifest,
|
|
9
|
+
generateEntryPoint,
|
|
10
|
+
generateReadme,
|
|
11
|
+
buildPluginPackage,
|
|
12
|
+
parseArgs,
|
|
13
|
+
} from './package.js'
|
|
14
|
+
import { PLATFORM_CONFIGS, getSkillsForPlatform } from './package-config.js'
|
|
15
|
+
|
|
16
|
+
const pkgRoot = resolve(process.cwd())
|
|
17
|
+
|
|
18
|
+
describe('readVersion', () => {
|
|
19
|
+
it('reads the correct version from package.json', () => {
|
|
20
|
+
const version = readVersion(pkgRoot)
|
|
21
|
+
expect(version).toMatch(/^\d+\.\d+\.\d+$/)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('listSkillDirs', () => {
|
|
26
|
+
it('returns skill directory names', () => {
|
|
27
|
+
const skills = listSkillDirs(pkgRoot)
|
|
28
|
+
expect(skills.length).toBeGreaterThan(0)
|
|
29
|
+
expect(skills).toContain('react-development')
|
|
30
|
+
expect(skills).toContain('testing-workflow')
|
|
31
|
+
})
|
|
32
|
+
it('returns only directory names (not paths)', () => {
|
|
33
|
+
const skills = listSkillDirs(pkgRoot)
|
|
34
|
+
expect(skills.every(s => !s.includes('/'))).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('getSkillsForPlatform', () => {
|
|
39
|
+
const allSkills = [
|
|
40
|
+
'react-development', 'session-checkpoints', 'panel-majority-vote',
|
|
41
|
+
'orchestration-protocols', 'team-lead-reference', 'testing-workflow',
|
|
42
|
+
'decomposition', 'agent-memory', 'context-map', 'fast-review', 'memory-merger',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
it('returns all skills for claude-code (fewest exclusions)', () => {
|
|
46
|
+
const skills = getSkillsForPlatform('claude-code', allSkills)
|
|
47
|
+
expect(skills).toEqual(allSkills)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('excludes session-checkpoints for cursor', () => {
|
|
51
|
+
const skills = getSkillsForPlatform('cursor', allSkills)
|
|
52
|
+
expect(skills).not.toContain('session-checkpoints')
|
|
53
|
+
expect(skills).toContain('react-development')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('excludes team-lead skills from opencode', () => {
|
|
57
|
+
const skills = getSkillsForPlatform('opencode', allSkills)
|
|
58
|
+
expect(skills).not.toContain('session-checkpoints')
|
|
59
|
+
expect(skills).not.toContain('panel-majority-vote')
|
|
60
|
+
expect(skills).not.toContain('orchestration-protocols')
|
|
61
|
+
expect(skills).not.toContain('team-lead-reference')
|
|
62
|
+
expect(skills).not.toContain('decomposition')
|
|
63
|
+
expect(skills).not.toContain('agent-memory')
|
|
64
|
+
expect(skills).not.toContain('context-map')
|
|
65
|
+
expect(skills).not.toContain('fast-review')
|
|
66
|
+
expect(skills).not.toContain('memory-merger')
|
|
67
|
+
expect(skills).toContain('react-development')
|
|
68
|
+
expect(skills).toContain('testing-workflow')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('excludes team-lead skills from gemini', () => {
|
|
72
|
+
const skills = getSkillsForPlatform('gemini', allSkills)
|
|
73
|
+
expect(skills).not.toContain('session-checkpoints')
|
|
74
|
+
expect(skills).not.toContain('orchestration-protocols')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('returns all skills for unknown platform', () => {
|
|
78
|
+
const skills = getSkillsForPlatform('unknown-platform', allSkills)
|
|
79
|
+
expect(skills).toEqual(allSkills)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('generateManifest', () => {
|
|
84
|
+
const skills = ['react-development', 'testing-workflow']
|
|
85
|
+
const agents = ['developer', 'reviewer']
|
|
86
|
+
const version = '0.31.6'
|
|
87
|
+
|
|
88
|
+
it('generates correct structure for claude-code', () => {
|
|
89
|
+
const m = generateManifest('claude-code', version, skills, agents) as Record<string, unknown>
|
|
90
|
+
expect(m.name).toBe('opencastle')
|
|
91
|
+
expect(m.version).toBe(version)
|
|
92
|
+
expect(m.skills).toEqual(skills)
|
|
93
|
+
expect(m.agents).toEqual(agents)
|
|
94
|
+
expect(m.hooks).toEqual(['SessionStart'])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('generates correct structure for cursor', () => {
|
|
98
|
+
const m = generateManifest('cursor', version, skills, agents) as Record<string, unknown>
|
|
99
|
+
expect(m.name).toBe('opencastle')
|
|
100
|
+
expect(m.version).toBe(version)
|
|
101
|
+
expect(m.hooks).toBeUndefined()
|
|
102
|
+
expect(m.type).toBeUndefined()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('generates correct structure for opencode', () => {
|
|
106
|
+
const m = generateManifest('opencode', version, skills, agents) as Record<string, unknown>
|
|
107
|
+
expect(m.name).toBe('opencastle')
|
|
108
|
+
expect(m.version).toBe(version)
|
|
109
|
+
expect(m.type).toBeUndefined()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('generates correct structure for gemini (includes type: extension)', () => {
|
|
113
|
+
const m = generateManifest('gemini', version, skills, agents) as Record<string, unknown>
|
|
114
|
+
expect(m.name).toBe('opencastle')
|
|
115
|
+
expect(m.version).toBe(version)
|
|
116
|
+
expect(m.type).toBe('extension')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('includes version from package.json', () => {
|
|
120
|
+
const ver = readVersion(pkgRoot)
|
|
121
|
+
const m = generateManifest('cursor', ver, skills, agents) as Record<string, unknown>
|
|
122
|
+
expect(m.version).toBe(ver)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('generateEntryPoint', () => {
|
|
127
|
+
const skills = ['react-development', 'testing-workflow']
|
|
128
|
+
const version = '1.0.0'
|
|
129
|
+
|
|
130
|
+
it('generates CLAUDE.md content for claude-code', () => {
|
|
131
|
+
const ep = generateEntryPoint('claude-code', version, skills)
|
|
132
|
+
expect(ep).toContain('OpenCastle v' + version)
|
|
133
|
+
expect(ep).toContain('react-development')
|
|
134
|
+
expect(ep).toContain('Claude Code')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('generates .cursorrules content for cursor', () => {
|
|
138
|
+
const ep = generateEntryPoint('cursor', version, skills)
|
|
139
|
+
expect(ep).toContain('OpenCastle v' + version)
|
|
140
|
+
expect(ep).toContain('Cursor')
|
|
141
|
+
expect(ep).toContain('react-development')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('generates OPENCODE.md content for opencode', () => {
|
|
145
|
+
const ep = generateEntryPoint('opencode', version, skills)
|
|
146
|
+
expect(ep).toContain('OpenCastle v' + version)
|
|
147
|
+
expect(ep).toContain('react-development')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('generates GEMINI.md content for gemini', () => {
|
|
151
|
+
const ep = generateEntryPoint('gemini', version, skills)
|
|
152
|
+
expect(ep).toContain('OpenCastle v' + version)
|
|
153
|
+
expect(ep).toContain('react-development')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('generateReadme', () => {
|
|
158
|
+
it('contains installation instructions', () => {
|
|
159
|
+
const readme = generateReadme('claude-code', '1.0.0')
|
|
160
|
+
expect(readme).toContain('Installation')
|
|
161
|
+
expect(readme).toContain('Claude Code')
|
|
162
|
+
expect(readme).toContain('1.0.0')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('references the correct entry point', () => {
|
|
166
|
+
const readme = generateReadme('cursor', '1.0.0')
|
|
167
|
+
expect(readme).toContain('.cursorrules')
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('PLATFORM_CONFIGS', () => {
|
|
172
|
+
it('has configs for all 4 platforms', () => {
|
|
173
|
+
expect(PLATFORM_CONFIGS).toHaveProperty('claude-code')
|
|
174
|
+
expect(PLATFORM_CONFIGS).toHaveProperty('cursor')
|
|
175
|
+
expect(PLATFORM_CONFIGS).toHaveProperty('opencode')
|
|
176
|
+
expect(PLATFORM_CONFIGS).toHaveProperty('gemini')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('each platform config has required fields', () => {
|
|
180
|
+
for (const [, config] of Object.entries(PLATFORM_CONFIGS)) {
|
|
181
|
+
expect(config.outputDir).toBeTruthy()
|
|
182
|
+
expect(config.manifestFile).toBeTruthy()
|
|
183
|
+
expect(config.entryPoint).toBeTruthy()
|
|
184
|
+
expect(Array.isArray(config.includedDirs)).toBe(true)
|
|
185
|
+
expect(config.includedDirs.length).toBeGreaterThan(0)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('gemini uses gemini-extension.json manifest', () => {
|
|
190
|
+
expect(PLATFORM_CONFIGS.gemini.manifestFile).toBe('gemini-extension.json')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('cursor uses .cursorrules entry point', () => {
|
|
194
|
+
expect(PLATFORM_CONFIGS.cursor.entryPoint).toBe('.cursorrules')
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('buildPluginPackage', () => {
|
|
199
|
+
it('creates correct directory structure for claude-code', () => {
|
|
200
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'oc-test-'))
|
|
201
|
+
try {
|
|
202
|
+
const result = buildPluginPackage(pkgRoot, 'claude-code', tmpDir)
|
|
203
|
+
expect(result.platform).toBe('claude-code')
|
|
204
|
+
expect(result.skillCount).toBeGreaterThan(0)
|
|
205
|
+
expect(result.agentCount).toBeGreaterThan(0)
|
|
206
|
+
expect(existsSync(join(result.outputDir, 'manifest.json'))).toBe(true)
|
|
207
|
+
expect(existsSync(join(result.outputDir, 'CLAUDE.md'))).toBe(true)
|
|
208
|
+
expect(existsSync(join(result.outputDir, 'README.md'))).toBe(true)
|
|
209
|
+
expect(existsSync(join(result.outputDir, 'skills'))).toBe(true)
|
|
210
|
+
expect(existsSync(join(result.outputDir, 'agents'))).toBe(true)
|
|
211
|
+
} finally {
|
|
212
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('respects platform skill filtering for gemini', () => {
|
|
217
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'oc-test-'))
|
|
218
|
+
try {
|
|
219
|
+
const result = buildPluginPackage(pkgRoot, 'gemini', tmpDir)
|
|
220
|
+
expect(existsSync(join(result.outputDir, 'skills', 'session-checkpoints'))).toBe(false)
|
|
221
|
+
expect(existsSync(join(result.outputDir, 'skills', 'react-development'))).toBe(true)
|
|
222
|
+
} finally {
|
|
223
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('throws for unknown platform', () => {
|
|
228
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'oc-test-'))
|
|
229
|
+
try {
|
|
230
|
+
expect(() => buildPluginPackage(pkgRoot, 'unknown-platform', tmpDir)).toThrow()
|
|
231
|
+
} finally {
|
|
232
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('parseArgs', () => {
|
|
238
|
+
it('parses --platform flag', () => {
|
|
239
|
+
const opts = parseArgs(['--platform', 'cursor'])
|
|
240
|
+
expect(opts.platform).toBe('cursor')
|
|
241
|
+
expect(opts.all).toBe(false)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('parses -p shorthand', () => {
|
|
245
|
+
const opts = parseArgs(['-p', 'gemini'])
|
|
246
|
+
expect(opts.platform).toBe('gemini')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('parses --all flag', () => {
|
|
250
|
+
const opts = parseArgs(['--all'])
|
|
251
|
+
expect(opts.all).toBe(true)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('parses --output flag', () => {
|
|
255
|
+
const opts = parseArgs(['--output', '/tmp/out'])
|
|
256
|
+
expect(opts.output).toBe('/tmp/out')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('parses -o shorthand', () => {
|
|
260
|
+
const opts = parseArgs(['-o', '/tmp/out'])
|
|
261
|
+
expect(opts.output).toBe('/tmp/out')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('parses --help flag', () => {
|
|
265
|
+
const opts = parseArgs(['--help'])
|
|
266
|
+
expect(opts.help).toBe(true)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('returns defaults for empty args', () => {
|
|
270
|
+
const opts = parseArgs([])
|
|
271
|
+
expect(opts.platform).toBeNull()
|
|
272
|
+
expect(opts.all).toBe(false)
|
|
273
|
+
expect(opts.output).toBe('dist/plugins')
|
|
274
|
+
expect(opts.help).toBe(false)
|
|
275
|
+
})
|
|
276
|
+
})
|