opencastle 0.20.0 → 0.21.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/dist/cli/adapters/cursor.d.ts.map +1 -1
- package/dist/cli/adapters/cursor.js +3 -12
- package/dist/cli/adapters/cursor.js.map +1 -1
- package/dist/cli/adapters/single-file-base.d.ts.map +1 -1
- package/dist/cli/adapters/single-file-base.js +3 -12
- package/dist/cli/adapters/single-file-base.js.map +1 -1
- package/dist/cli/adapters/vscode.d.ts +1 -1
- package/dist/cli/adapters/vscode.d.ts.map +1 -1
- package/dist/cli/adapters/vscode.js +4 -16
- package/dist/cli/adapters/vscode.js.map +1 -1
- package/dist/cli/dashboard.js +1 -1
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/doctor.js +7 -7
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/eject.js +2 -2
- package/dist/cli/eject.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +13 -4
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +27 -15
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/lesson.js +5 -5
- package/dist/cli/lesson.js.map +1 -1
- package/dist/cli/log.d.ts +1 -1
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +5 -5
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/manifest.d.ts +4 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +16 -5
- package/dist/cli/manifest.js.map +1 -1
- package/dist/cli/stack-config.d.ts.map +1 -1
- package/dist/cli/stack-config.js +2 -14
- package/dist/cli/stack-config.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +87 -34
- package/dist/cli/update.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/adapters/cursor.ts +2 -12
- package/src/cli/adapters/single-file-base.ts +2 -12
- package/src/cli/adapters/vscode.ts +4 -16
- package/src/cli/dashboard.ts +1 -1
- package/src/cli/doctor.ts +7 -7
- package/src/cli/eject.ts +2 -2
- package/src/cli/init.test.ts +28 -15
- package/src/cli/init.ts +14 -4
- package/src/cli/lesson.ts +5 -5
- package/src/cli/log.ts +5 -5
- package/src/cli/manifest.ts +18 -5
- package/src/cli/stack-config.ts +2 -14
- package/src/cli/update.ts +95 -36
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/agent-workflows/README.md +1 -1
- package/src/orchestrator/agent-workflows/bug-fix.md +4 -4
- package/src/orchestrator/agent-workflows/data-pipeline.md +1 -1
- package/src/orchestrator/agent-workflows/database-migration.md +4 -4
- package/src/orchestrator/agent-workflows/feature-implementation.md +3 -3
- package/src/orchestrator/agent-workflows/performance-optimization.md +1 -1
- package/src/orchestrator/agent-workflows/refactoring.md +1 -1
- package/src/orchestrator/agent-workflows/schema-changes.md +2 -2
- package/src/orchestrator/agent-workflows/security-audit.md +4 -4
- package/src/orchestrator/agent-workflows/shared-delivery-phase.md +1 -1
- package/src/orchestrator/agents/api-designer.agent.md +2 -2
- package/src/orchestrator/agents/architect.agent.md +2 -2
- package/src/orchestrator/agents/content-engineer.agent.md +2 -2
- package/src/orchestrator/agents/copywriter.agent.md +2 -2
- package/src/orchestrator/agents/data-expert.agent.md +2 -2
- package/src/orchestrator/agents/database-engineer.agent.md +2 -2
- package/src/orchestrator/agents/developer.agent.md +2 -2
- package/src/orchestrator/agents/devops-expert.agent.md +2 -2
- package/src/orchestrator/agents/documentation-writer.agent.md +2 -2
- package/src/orchestrator/agents/performance-expert.agent.md +2 -2
- package/src/orchestrator/agents/release-manager.agent.md +2 -2
- package/src/orchestrator/agents/researcher.agent.md +4 -4
- package/src/orchestrator/agents/reviewer.agent.md +1 -1
- package/src/orchestrator/agents/security-expert.agent.md +2 -2
- package/src/orchestrator/agents/seo-specialist.agent.md +2 -2
- package/src/orchestrator/agents/session-guard.agent.md +10 -10
- package/src/orchestrator/agents/team-lead.agent.md +3 -3
- package/src/orchestrator/agents/testing-expert.agent.md +2 -2
- package/src/orchestrator/agents/ui-ux-expert.agent.md +2 -2
- package/src/orchestrator/copilot-instructions.md +1 -1
- package/src/orchestrator/customizations/AGENT-PERFORMANCE.md +11 -11
- package/src/orchestrator/customizations/DISPUTES.md +2 -2
- package/src/orchestrator/customizations/README.md +1 -1
- package/src/orchestrator/customizations/logs/README.md +1 -1
- package/src/orchestrator/customizations/project/docs-structure.md +6 -6
- package/src/orchestrator/instructions/ai-optimization.instructions.md +1 -1
- package/src/orchestrator/instructions/general.instructions.md +6 -6
- package/src/orchestrator/plugins/astro/SKILL.md +1 -1
- package/src/orchestrator/plugins/chrome-devtools/SKILL.md +4 -4
- package/src/orchestrator/plugins/contentful/SKILL.md +2 -2
- package/src/orchestrator/plugins/convex/SKILL.md +2 -2
- package/src/orchestrator/plugins/cypress/SKILL.md +2 -2
- package/src/orchestrator/plugins/figma/SKILL.md +1 -1
- package/src/orchestrator/plugins/jira/SKILL.md +3 -3
- package/src/orchestrator/plugins/linear/SKILL.md +2 -2
- package/src/orchestrator/plugins/netlify/SKILL.md +2 -2
- package/src/orchestrator/plugins/nextjs/SKILL.md +1 -1
- package/src/orchestrator/plugins/nx/SKILL.md +1 -1
- package/src/orchestrator/plugins/playwright/SKILL.md +2 -2
- package/src/orchestrator/plugins/prisma/SKILL.md +2 -2
- package/src/orchestrator/plugins/resend/SKILL.md +1 -1
- package/src/orchestrator/plugins/sanity/SKILL.md +2 -2
- package/src/orchestrator/plugins/slack/SKILL.md +2 -2
- package/src/orchestrator/plugins/strapi/SKILL.md +2 -2
- package/src/orchestrator/plugins/supabase/SKILL.md +2 -2
- package/src/orchestrator/plugins/teams/SKILL.md +1 -1
- package/src/orchestrator/plugins/turborepo/SKILL.md +1 -1
- package/src/orchestrator/plugins/vercel/SKILL.md +2 -2
- package/src/orchestrator/plugins/vitest/SKILL.md +2 -2
- package/src/orchestrator/prompts/bootstrap-customizations.prompt.md +8 -8
- package/src/orchestrator/prompts/brainstorm.prompt.md +3 -3
- package/src/orchestrator/prompts/bug-fix.prompt.md +4 -4
- package/src/orchestrator/prompts/create-skill.prompt.md +3 -3
- package/src/orchestrator/prompts/generate-convoy.prompt.md +1 -1
- package/src/orchestrator/prompts/implement-feature.prompt.md +10 -10
- package/src/orchestrator/prompts/quick-refinement.prompt.md +3 -3
- package/src/orchestrator/prompts/resolve-pr-comments.prompt.md +1 -1
- package/src/orchestrator/skills/accessibility-standards/SKILL.md +1 -1
- package/src/orchestrator/skills/agent-hooks/SKILL.md +9 -9
- package/src/orchestrator/skills/agent-memory/SKILL.md +4 -4
- package/src/orchestrator/skills/api-patterns/SKILL.md +2 -2
- package/src/orchestrator/skills/code-commenting/SKILL.md +1 -1
- package/src/orchestrator/skills/context-map/SKILL.md +1 -1
- package/src/orchestrator/skills/data-engineering/SKILL.md +2 -2
- package/src/orchestrator/skills/decomposition/SKILL.md +1 -1
- package/src/orchestrator/skills/deployment-infrastructure/SKILL.md +2 -2
- package/src/orchestrator/skills/documentation-standards/SKILL.md +2 -2
- package/src/orchestrator/skills/fast-review/SKILL.md +2 -2
- package/src/orchestrator/skills/frontend-design/SKILL.md +1 -1
- package/src/orchestrator/skills/git-workflow/SKILL.md +2 -2
- package/src/orchestrator/skills/memory-merger/SKILL.md +3 -3
- package/src/orchestrator/skills/observability-logging/SKILL.md +10 -10
- package/src/orchestrator/skills/orchestration-protocols/SKILL.md +1 -1
- package/src/orchestrator/skills/panel-majority-vote/SKILL.md +2 -2
- package/src/orchestrator/skills/panel-majority-vote/panel-report.template.md +1 -1
- package/src/orchestrator/skills/performance-optimization/SKILL.md +1 -1
- package/src/orchestrator/skills/react-development/SKILL.md +1 -1
- package/src/orchestrator/skills/security-hardening/SKILL.md +1 -1
- package/src/orchestrator/skills/self-improvement/SKILL.md +2 -2
- package/src/orchestrator/skills/seo-patterns/SKILL.md +1 -1
- package/src/orchestrator/skills/session-checkpoints/SKILL.md +5 -5
- package/src/orchestrator/skills/team-lead-reference/SKILL.md +6 -6
- package/src/orchestrator/skills/testing-workflow/SKILL.md +3 -3
- package/src/orchestrator/skills/validation-gates/SKILL.md +1 -1
package/src/cli/init.test.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from './stack-config.js'
|
|
32
32
|
import { ALL_PLUGIN_SKILL_NAMES } from '../orchestrator/plugins/index.js'
|
|
33
33
|
import { IDE_ADAPTERS } from './adapters/index.js'
|
|
34
|
+
import { copyDir, getOrchestratorRoot } from './copy.js'
|
|
34
35
|
|
|
35
36
|
// ── Helpers ────────────────────────────────────────────────────
|
|
36
37
|
|
|
@@ -42,6 +43,16 @@ async function readJson<T = unknown>(path: string): Promise<T> {
|
|
|
42
43
|
return JSON.parse(await readFile(path, 'utf8')) as T
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** Simulate the init.ts customizations scaffold step (copies customizations to .opencastle/). */
|
|
47
|
+
async function scaffoldCustomizations(pkgRoot: string, projectRoot: string, stack: StackConfig): Promise<void> {
|
|
48
|
+
const custSrcDir = resolve(getOrchestratorRoot(pkgRoot), 'customizations')
|
|
49
|
+
if (existsSync(custSrcDir)) {
|
|
50
|
+
const custDestDir = join(projectRoot, '.opencastle')
|
|
51
|
+
const custTransform = getCustomizationsTransform(stack)
|
|
52
|
+
await copyDir(custSrcDir, custDestDir, { transform: custTransform })
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
/** Recursively list all files in a directory (relative paths). */
|
|
46
57
|
async function listFilesRecursive(dir: string, prefix = ''): Promise<string[]> {
|
|
47
58
|
if (!existsSync(dir)) return []
|
|
@@ -290,7 +301,7 @@ describe('gitignore generation', () => {
|
|
|
290
301
|
it('creates .gitignore with framework paths ignored and customizable un-ignored', async () => {
|
|
291
302
|
const managed = {
|
|
292
303
|
framework: ['.github/copilot-instructions.md', '.github/agents/'],
|
|
293
|
-
customizable: ['.
|
|
304
|
+
customizable: ['.opencastle/', '.vscode/mcp.json'],
|
|
294
305
|
}
|
|
295
306
|
|
|
296
307
|
await updateGitignore(tempDir, managed)
|
|
@@ -300,7 +311,7 @@ describe('gitignore generation', () => {
|
|
|
300
311
|
expect(content).toContain('.github/copilot-instructions.md')
|
|
301
312
|
expect(content).toContain('.github/agents/')
|
|
302
313
|
// Customizable paths should be un-ignored
|
|
303
|
-
expect(content).toContain('!.
|
|
314
|
+
expect(content).toContain('!.opencastle/')
|
|
304
315
|
expect(content).toContain('!.vscode/mcp.json')
|
|
305
316
|
// Markers should be present
|
|
306
317
|
expect(content).toContain('# >>> OpenCastle managed (do not edit) >>>')
|
|
@@ -316,14 +327,14 @@ describe('gitignore generation', () => {
|
|
|
316
327
|
|
|
317
328
|
const managed2 = {
|
|
318
329
|
framework: ['.github/agents/', '.github/skills/'],
|
|
319
|
-
customizable: ['.vscode/mcp.json', '.
|
|
330
|
+
customizable: ['.vscode/mcp.json', '.opencastle/'],
|
|
320
331
|
}
|
|
321
332
|
const result = await updateGitignore(tempDir, managed2)
|
|
322
333
|
expect(result).toBe('updated')
|
|
323
334
|
|
|
324
335
|
const content = await readFile(join(tempDir, '.gitignore'), 'utf8')
|
|
325
336
|
expect(content).toContain('.github/skills/')
|
|
326
|
-
expect(content).toContain('!.
|
|
337
|
+
expect(content).toContain('!.opencastle/')
|
|
327
338
|
// Only one managed block
|
|
328
339
|
const startCount = (content.match(/>>> OpenCastle managed/g) ?? []).length
|
|
329
340
|
expect(startCount).toBe(1)
|
|
@@ -356,15 +367,15 @@ describe('VS Code adapter install', () => {
|
|
|
356
367
|
expect(existsSync(join(githubDir, 'skills'))).toBe(true)
|
|
357
368
|
expect(existsSync(join(githubDir, 'agent-workflows'))).toBe(true)
|
|
358
369
|
expect(existsSync(join(githubDir, 'prompts'))).toBe(true)
|
|
359
|
-
expect(existsSync(join(githubDir, 'customizations'))).toBe(true)
|
|
360
370
|
expect(existsSync(join(tempDir, '.vscode', 'mcp.json'))).toBe(true)
|
|
361
371
|
})
|
|
362
372
|
|
|
363
|
-
it('creates all observability log files in
|
|
373
|
+
it('creates all observability log files in .opencastle/logs', async () => {
|
|
364
374
|
const adapter = await IDE_ADAPTERS['vscode']()
|
|
365
375
|
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO)
|
|
376
|
+
await scaffoldCustomizations(PKG_ROOT, tempDir, STACK_EMPTY)
|
|
366
377
|
|
|
367
|
-
const logsDir = join(tempDir, '.
|
|
378
|
+
const logsDir = join(tempDir, '.opencastle', 'logs')
|
|
368
379
|
expect(existsSync(logsDir)).toBe(true)
|
|
369
380
|
for (const file of ['events.ndjson']) {
|
|
370
381
|
expect(existsSync(join(logsDir, file))).toBe(true)
|
|
@@ -516,9 +527,10 @@ describe('VS Code adapter install', () => {
|
|
|
516
527
|
it('fills skill-matrix.json with selected DB and CMS', async () => {
|
|
517
528
|
const adapter = await IDE_ADAPTERS['vscode']()
|
|
518
529
|
await adapter.install(PKG_ROOT, tempDir, STACK_FULL, EMPTY_REPO_INFO)
|
|
530
|
+
await scaffoldCustomizations(PKG_ROOT, tempDir, STACK_FULL)
|
|
519
531
|
|
|
520
532
|
const skillMatrix = await readFile(
|
|
521
|
-
join(tempDir, '.
|
|
533
|
+
join(tempDir, '.opencastle', 'agents', 'skill-matrix.json'),
|
|
522
534
|
'utf8'
|
|
523
535
|
)
|
|
524
536
|
const data = JSON.parse(skillMatrix)
|
|
@@ -533,9 +545,10 @@ describe('VS Code adapter install', () => {
|
|
|
533
545
|
it('leaves skill-matrix.json database/cms slots empty when none selected', async () => {
|
|
534
546
|
const adapter = await IDE_ADAPTERS['vscode']()
|
|
535
547
|
await adapter.install(PKG_ROOT, tempDir, STACK_EMPTY, EMPTY_REPO_INFO)
|
|
548
|
+
await scaffoldCustomizations(PKG_ROOT, tempDir, STACK_EMPTY)
|
|
536
549
|
|
|
537
550
|
const skillMatrix = await readFile(
|
|
538
|
-
join(tempDir, '.
|
|
551
|
+
join(tempDir, '.opencastle', 'agents', 'skill-matrix.json'),
|
|
539
552
|
'utf8'
|
|
540
553
|
)
|
|
541
554
|
const data = JSON.parse(skillMatrix)
|
|
@@ -554,7 +567,7 @@ describe('VS Code adapter install', () => {
|
|
|
554
567
|
expect(paths.framework).toContain('.github/agent-workflows/')
|
|
555
568
|
expect(paths.framework).toContain('.github/prompts/')
|
|
556
569
|
|
|
557
|
-
expect(paths.customizable).toContain('.
|
|
570
|
+
expect(paths.customizable).toContain('.opencastle/')
|
|
558
571
|
expect(paths.customizable).toContain('.vscode/mcp.json')
|
|
559
572
|
})
|
|
560
573
|
})
|
|
@@ -680,7 +693,7 @@ describe('Cursor adapter install', () => {
|
|
|
680
693
|
expect(paths.framework).toContain('.cursor/rules/general.mdc')
|
|
681
694
|
expect(paths.framework).toContain('.cursor/rules/ai-optimization.mdc')
|
|
682
695
|
|
|
683
|
-
expect(paths.customizable).toContain('.
|
|
696
|
+
expect(paths.customizable).toContain('.opencastle/')
|
|
684
697
|
expect(paths.customizable).toContain('.cursor/mcp.json')
|
|
685
698
|
})
|
|
686
699
|
})
|
|
@@ -827,7 +840,7 @@ describe('Claude Code adapter install', () => {
|
|
|
827
840
|
expect(paths.framework).toContain('.claude/skills/')
|
|
828
841
|
expect(paths.framework).toContain('.claude/commands/')
|
|
829
842
|
|
|
830
|
-
expect(paths.customizable).toContain('.
|
|
843
|
+
expect(paths.customizable).toContain('.opencastle/')
|
|
831
844
|
expect(paths.customizable).toContain('.claude/mcp.json')
|
|
832
845
|
})
|
|
833
846
|
})
|
|
@@ -867,7 +880,6 @@ describe('OpenCode adapter install', () => {
|
|
|
867
880
|
expect(existsSync(join(tempDir, '.opencode', 'skills'))).toBe(true)
|
|
868
881
|
expect(existsSync(join(tempDir, '.opencode', 'prompts'))).toBe(true)
|
|
869
882
|
expect(existsSync(join(tempDir, '.opencode', 'workflows'))).toBe(true)
|
|
870
|
-
expect(existsSync(join(tempDir, '.opencode', 'customizations'))).toBe(true)
|
|
871
883
|
})
|
|
872
884
|
|
|
873
885
|
it('generates OpenCode MCP config with mcp format', async () => {
|
|
@@ -919,7 +931,7 @@ describe('OpenCode adapter install', () => {
|
|
|
919
931
|
expect(paths.framework).toContain('.opencode/prompts/')
|
|
920
932
|
expect(paths.framework).toContain('.opencode/workflows/')
|
|
921
933
|
|
|
922
|
-
expect(paths.customizable).toContain('.
|
|
934
|
+
expect(paths.customizable).toContain('.opencastle/')
|
|
923
935
|
expect(paths.customizable).toContain('opencode.json')
|
|
924
936
|
})
|
|
925
937
|
})
|
|
@@ -1078,6 +1090,7 @@ describe('full stack configuration', () => {
|
|
|
1078
1090
|
|
|
1079
1091
|
const adapter = await IDE_ADAPTERS['vscode']()
|
|
1080
1092
|
const result = await adapter.install(PKG_ROOT, tempDir, stack, EMPTY_REPO_INFO)
|
|
1093
|
+
await scaffoldCustomizations(PKG_ROOT, tempDir, stack)
|
|
1081
1094
|
|
|
1082
1095
|
expect(result.created.length).toBeGreaterThan(0)
|
|
1083
1096
|
|
|
@@ -1119,7 +1132,7 @@ describe('full stack configuration', () => {
|
|
|
1119
1132
|
|
|
1120
1133
|
// Skill matrix should be filled
|
|
1121
1134
|
const skillMatrix = await readFile(
|
|
1122
|
-
join(tempDir, '.
|
|
1135
|
+
join(tempDir, '.opencastle', 'agents', 'skill-matrix.json'),
|
|
1123
1136
|
'utf8'
|
|
1124
1137
|
)
|
|
1125
1138
|
const matrixData = JSON.parse(skillMatrix)
|
package/src/cli/init.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { readFile, unlink } from 'node:fs/promises'
|
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import { multiselect, confirm, closePrompts, c } from './prompt.js'
|
|
5
5
|
import { readManifest, writeManifest, createManifest } from './manifest.js'
|
|
6
|
-
import { removeDirIfExists } from './copy.js'
|
|
6
|
+
import { removeDirIfExists, copyDir, getOrchestratorRoot } from './copy.js'
|
|
7
7
|
import { updateGitignore } from './gitignore.js'
|
|
8
|
-
import { getRequiredMcpEnvVars } from './stack-config.js'
|
|
8
|
+
import { getRequiredMcpEnvVars, getCustomizationsTransform } from './stack-config.js'
|
|
9
9
|
import { TECH_PLUGINS, TEAM_PLUGINS } from '../orchestrator/plugins/index.js'
|
|
10
10
|
import { detectRepoInfo, mergeStackIntoRepoInfo, formatRepoInfo, buildDetectedToolsSet } from './detect.js'
|
|
11
11
|
import { IDE_ADAPTERS } from './adapters/index.js'
|
|
@@ -138,7 +138,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
138
138
|
console.log(` ${c.green('+')} ${p}`)
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
console.log(` ${c.green('+')} .opencastle.json`)
|
|
141
|
+
console.log(` ${c.green('+')} .opencastle/manifest.json`)
|
|
142
142
|
console.log(` ${c.green('+')} .gitignore (OpenCastle entries)`)
|
|
143
143
|
console.log(`\n ${c.dim('No files were written.')}\n`)
|
|
144
144
|
closePrompts()
|
|
@@ -217,6 +217,16 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// ── Scaffold customizations to .opencastle/ ──────────────────────────────
|
|
221
|
+
const custSrcDir = resolve(getOrchestratorRoot(pkgRoot), 'customizations')
|
|
222
|
+
if (existsSync(custSrcDir)) {
|
|
223
|
+
const custDestDir = resolve(projectRoot, '.opencastle')
|
|
224
|
+
const custTransform = getCustomizationsTransform(stack)
|
|
225
|
+
const sub = await copyDir(custSrcDir, custDestDir, { transform: custTransform })
|
|
226
|
+
totalCreated += sub.created.length
|
|
227
|
+
totalSkipped += sub.skipped.length
|
|
228
|
+
}
|
|
229
|
+
|
|
220
230
|
// ── Write manifest ──────────────────────────────────────────────
|
|
221
231
|
const manifest = createManifest(pkg.version, ides[0], ides)
|
|
222
232
|
manifest.managedPaths = allManagedPaths
|
|
@@ -311,7 +321,7 @@ export default async function init({ pkgRoot, args }: CliContext): Promise<void>
|
|
|
311
321
|
` ${step}. Run the ${c.cyan('"Bootstrap Customizations"')} prompt to configure for your project`
|
|
312
322
|
)
|
|
313
323
|
step++
|
|
314
|
-
console.log(` ${step}. Commit the
|
|
324
|
+
console.log(` ${step}. Commit the .opencastle/ folder to your repository`)
|
|
315
325
|
console.log()
|
|
316
326
|
|
|
317
327
|
closePrompts()
|
package/src/cli/lesson.ts
CHANGED
|
@@ -25,7 +25,7 @@ type Severity = (typeof SEVERITIES)[number]
|
|
|
25
25
|
const HELP = `
|
|
26
26
|
opencastle lesson [options]
|
|
27
27
|
|
|
28
|
-
Append a structured lesson to .
|
|
28
|
+
Append a structured lesson to .opencastle/LESSONS-LEARNED.md
|
|
29
29
|
|
|
30
30
|
Required flags:
|
|
31
31
|
--title <text> Short descriptive title
|
|
@@ -70,16 +70,16 @@ async function resolveCustomizationsDir(override: string | null): Promise<string
|
|
|
70
70
|
let dir = process.cwd()
|
|
71
71
|
for (;;) {
|
|
72
72
|
try {
|
|
73
|
-
const s = await stat(join(dir, '.
|
|
74
|
-
if (s.isDirectory()) return join(dir, '.
|
|
73
|
+
const s = await stat(join(dir, '.opencastle'))
|
|
74
|
+
if (s.isDirectory()) return join(dir, '.opencastle')
|
|
75
75
|
} catch {
|
|
76
|
-
// .
|
|
76
|
+
// .opencastle not found here, walk up
|
|
77
77
|
}
|
|
78
78
|
const parent = dirname(dir)
|
|
79
79
|
if (parent === dir) break
|
|
80
80
|
dir = parent
|
|
81
81
|
}
|
|
82
|
-
return join(process.cwd(), '.
|
|
82
|
+
return join(process.cwd(), '.opencastle')
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function nextLessonId(content: string): string {
|
package/src/cli/log.ts
CHANGED
|
@@ -41,22 +41,22 @@ function coerceValue(key: string, raw: string): unknown {
|
|
|
41
41
|
return raw
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** Resolve the path to the logs directory (walks up to find .
|
|
44
|
+
/** Resolve the path to the logs directory (walks up to find .opencastle/). */
|
|
45
45
|
export async function resolveLogsDir(override?: string | null): Promise<string> {
|
|
46
46
|
if (override) return override
|
|
47
47
|
let dir = process.cwd()
|
|
48
48
|
for (;;) {
|
|
49
49
|
try {
|
|
50
|
-
const s = await stat(join(dir, '.
|
|
51
|
-
if (s.isDirectory()) return join(dir, '.
|
|
50
|
+
const s = await stat(join(dir, '.opencastle'))
|
|
51
|
+
if (s.isDirectory()) return join(dir, '.opencastle', 'logs')
|
|
52
52
|
} catch {
|
|
53
|
-
// .
|
|
53
|
+
// .opencastle not in this directory, walk up
|
|
54
54
|
}
|
|
55
55
|
const parent = dirname(dir)
|
|
56
56
|
if (parent === dir) break
|
|
57
57
|
dir = parent
|
|
58
58
|
}
|
|
59
|
-
return join(process.cwd(), '.
|
|
59
|
+
return join(process.cwd(), '.opencastle', 'logs')
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Append a structured event record to events.ndjson. */
|
package/src/cli/manifest.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
3
|
import type { Manifest } from './types.js';
|
|
4
4
|
|
|
5
|
-
const MANIFEST_FILE = '.opencastle.json';
|
|
5
|
+
const MANIFEST_FILE = '.opencastle/manifest.json';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Read the project's OpenCastle manifest, or null if not installed.
|
|
9
|
+
* Tries the new location (.opencastle/manifest.json) first, then falls back
|
|
10
|
+
* to the legacy location (.opencastle.json) for backward compatibility.
|
|
9
11
|
*/
|
|
10
12
|
export async function readManifest(
|
|
11
13
|
projectRoot: string
|
|
@@ -17,18 +19,29 @@ export async function readManifest(
|
|
|
17
19
|
);
|
|
18
20
|
return JSON.parse(content) as Manifest;
|
|
19
21
|
} catch {
|
|
20
|
-
|
|
22
|
+
// Fallback to legacy location
|
|
23
|
+
try {
|
|
24
|
+
const content = await readFile(
|
|
25
|
+
resolve(projectRoot, '.opencastle.json'),
|
|
26
|
+
'utf8'
|
|
27
|
+
);
|
|
28
|
+
return JSON.parse(content) as Manifest;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
/**
|
|
25
|
-
* Write the manifest to
|
|
36
|
+
* Write the manifest to .opencastle/manifest.json.
|
|
37
|
+
* Creates the .opencastle/ directory if it doesn't exist.
|
|
26
38
|
*/
|
|
27
39
|
export async function writeManifest(
|
|
28
40
|
projectRoot: string,
|
|
29
41
|
manifest: Manifest
|
|
30
42
|
): Promise<void> {
|
|
31
43
|
const path = resolve(projectRoot, MANIFEST_FILE);
|
|
44
|
+
await mkdir(dirname(path), { recursive: true });
|
|
32
45
|
await writeFile(path, JSON.stringify(manifest, null, 2) + '\n');
|
|
33
46
|
}
|
|
34
47
|
|
package/src/cli/stack-config.ts
CHANGED
|
@@ -257,20 +257,8 @@ const SUBCATEGORY_TO_SLOT: Record<string, string> = {
|
|
|
257
257
|
/**
|
|
258
258
|
* Get the filesystem path to the skill matrix file for a given IDE.
|
|
259
259
|
*/
|
|
260
|
-
function getSkillMatrixPath(projectRoot: string,
|
|
261
|
-
|
|
262
|
-
switch (ide) {
|
|
263
|
-
case 'vscode':
|
|
264
|
-
return resolve(projectRoot, '.github', relativePath);
|
|
265
|
-
case 'cursor':
|
|
266
|
-
return resolve(projectRoot, '.cursor', 'rules', relativePath);
|
|
267
|
-
case 'claude-code':
|
|
268
|
-
return resolve(projectRoot, '.claude', relativePath);
|
|
269
|
-
case 'opencode':
|
|
270
|
-
return resolve(projectRoot, '.opencode', relativePath);
|
|
271
|
-
default:
|
|
272
|
-
return '';
|
|
273
|
-
}
|
|
260
|
+
function getSkillMatrixPath(projectRoot: string, _ide: string): string {
|
|
261
|
+
return resolve(projectRoot, '.opencastle', 'agents', 'skill-matrix.json');
|
|
274
262
|
}
|
|
275
263
|
|
|
276
264
|
/**
|
package/src/cli/update.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { readFile, appendFile, rename } from 'node:fs/promises'
|
|
3
|
+
import { readFile, appendFile, rename, mkdir, writeFile, unlink, copyFile, readdir, rm } from 'node:fs/promises'
|
|
4
4
|
import { readManifest, writeManifest } from './manifest.js'
|
|
5
5
|
import { multiselect, confirm, closePrompts, c } from './prompt.js'
|
|
6
6
|
import { isLegacyStack, migrateStackConfig, IDE_LABELS } from './types.js'
|
|
@@ -17,6 +17,8 @@ export default async function update({
|
|
|
17
17
|
}: CliContext): Promise<void> {
|
|
18
18
|
const projectRoot = process.cwd()
|
|
19
19
|
|
|
20
|
+
await migrateCustomizationsDir(projectRoot)
|
|
21
|
+
|
|
20
22
|
const manifest = await readManifest(projectRoot)
|
|
21
23
|
if (!manifest) {
|
|
22
24
|
console.error(
|
|
@@ -326,9 +328,62 @@ export default async function update({
|
|
|
326
328
|
closePrompts()
|
|
327
329
|
}
|
|
328
330
|
|
|
331
|
+
async function copyDirMigrate(srcDir: string, destDir: string): Promise<void> {
|
|
332
|
+
await mkdir(destDir, { recursive: true })
|
|
333
|
+
for (const entry of await readdir(srcDir, { withFileTypes: true })) {
|
|
334
|
+
const srcPath = resolve(srcDir, entry.name)
|
|
335
|
+
const destPath = resolve(destDir, entry.name)
|
|
336
|
+
if (entry.isDirectory()) {
|
|
337
|
+
await copyDirMigrate(srcPath, destPath)
|
|
338
|
+
} else if (!existsSync(destPath)) {
|
|
339
|
+
await copyFile(srcPath, destPath)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function migrateCustomizationsDir(projectRoot: string): Promise<void> {
|
|
345
|
+
const oldManifestPath = resolve(projectRoot, '.opencastle.json')
|
|
346
|
+
const newOpencastleDir = resolve(projectRoot, '.opencastle')
|
|
347
|
+
const newManifestPath = resolve(newOpencastleDir, 'manifest.json')
|
|
348
|
+
|
|
349
|
+
// Migrate manifest from flat location to .opencastle/manifest.json
|
|
350
|
+
if (existsSync(oldManifestPath) && !existsSync(newManifestPath)) {
|
|
351
|
+
await mkdir(newOpencastleDir, { recursive: true })
|
|
352
|
+
const content = await readFile(oldManifestPath, 'utf8')
|
|
353
|
+
await writeFile(newManifestPath, content)
|
|
354
|
+
await unlink(oldManifestPath)
|
|
355
|
+
console.log(` ${c.green('✓')} Migrated manifest to .opencastle/manifest.json`)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Old customizations directory locations per IDE
|
|
359
|
+
const oldCustDirs = [
|
|
360
|
+
resolve(projectRoot, '.github', 'customizations'),
|
|
361
|
+
resolve(projectRoot, '.cursor', 'rules', 'customizations'),
|
|
362
|
+
resolve(projectRoot, '.claude', 'customizations'),
|
|
363
|
+
resolve(projectRoot, '.opencode', 'customizations'),
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
// Copy from the first found old location (content is the same across IDEs)
|
|
367
|
+
for (const oldDir of oldCustDirs) {
|
|
368
|
+
if (!existsSync(oldDir)) continue
|
|
369
|
+
await copyDirMigrate(oldDir, newOpencastleDir)
|
|
370
|
+
console.log(` ${c.green('✓')} Migrated customizations to .opencastle/`)
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Remove all old customizations directories
|
|
375
|
+
for (const oldDir of oldCustDirs) {
|
|
376
|
+
if (existsSync(oldDir)) {
|
|
377
|
+
await rm(oldDir, { recursive: true })
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
329
382
|
async function migrateLegacyLogs(projectRoot: string): Promise<void> {
|
|
330
|
-
const
|
|
331
|
-
|
|
383
|
+
const candidateLogsDirs = [
|
|
384
|
+
resolve(projectRoot, '.github', 'customizations', 'logs'),
|
|
385
|
+
resolve(projectRoot, '.opencastle', 'logs'),
|
|
386
|
+
]
|
|
332
387
|
|
|
333
388
|
const typeMap: Record<string, string> = {
|
|
334
389
|
'sessions.ndjson': 'session',
|
|
@@ -338,48 +393,52 @@ async function migrateLegacyLogs(projectRoot: string): Promise<void> {
|
|
|
338
393
|
'disputes.ndjson': 'dispute',
|
|
339
394
|
}
|
|
340
395
|
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
for (const [filename, type] of Object.entries(typeMap)) {
|
|
345
|
-
const filePath = resolve(logsDir, filename)
|
|
346
|
-
if (!existsSync(filePath)) continue
|
|
396
|
+
for (const logsDir of candidateLogsDirs) {
|
|
397
|
+
if (!existsSync(logsDir)) continue
|
|
347
398
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
content = await readFile(filePath, 'utf8')
|
|
351
|
-
} catch {
|
|
352
|
-
continue
|
|
353
|
-
}
|
|
399
|
+
const eventsFile = resolve(logsDir, 'events.ndjson')
|
|
400
|
+
let totalMigrated = 0
|
|
354
401
|
|
|
355
|
-
const
|
|
356
|
-
|
|
402
|
+
for (const [filename, type] of Object.entries(typeMap)) {
|
|
403
|
+
const filePath = resolve(logsDir, filename)
|
|
404
|
+
if (!existsSync(filePath)) continue
|
|
357
405
|
|
|
358
|
-
|
|
359
|
-
for (const line of lines) {
|
|
406
|
+
let content: string
|
|
360
407
|
try {
|
|
361
|
-
|
|
362
|
-
if (!record['type']) {
|
|
363
|
-
record['type'] = type
|
|
364
|
-
}
|
|
365
|
-
migratedLines.push(JSON.stringify(record))
|
|
408
|
+
content = await readFile(filePath, 'utf8')
|
|
366
409
|
} catch {
|
|
367
|
-
|
|
410
|
+
continue
|
|
368
411
|
}
|
|
369
|
-
}
|
|
370
412
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
413
|
+
const lines = content.split('\n').filter((line) => line.trim() !== '')
|
|
414
|
+
if (lines.length === 0) continue
|
|
415
|
+
|
|
416
|
+
const migratedLines: string[] = []
|
|
417
|
+
for (const line of lines) {
|
|
418
|
+
try {
|
|
419
|
+
const record = JSON.parse(line) as Record<string, unknown>
|
|
420
|
+
if (!record['type']) {
|
|
421
|
+
record['type'] = type
|
|
422
|
+
}
|
|
423
|
+
migratedLines.push(JSON.stringify(record))
|
|
424
|
+
} catch {
|
|
425
|
+
console.warn(` ${c.yellow('⚠')} Skipping malformed JSON line in ${filename}`)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
375
428
|
|
|
376
|
-
|
|
377
|
-
|
|
429
|
+
if (migratedLines.length > 0) {
|
|
430
|
+
await appendFile(eventsFile, migratedLines.join('\n') + '\n', 'utf8')
|
|
431
|
+
totalMigrated += migratedLines.length
|
|
432
|
+
}
|
|
378
433
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
)
|
|
434
|
+
await rename(filePath, filePath + '.migrated')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (totalMigrated > 0) {
|
|
438
|
+
console.log(
|
|
439
|
+
` ${c.green('✓')} Migrated ${c.bold(String(totalMigrated))} records from legacy log files to events.ndjson`
|
|
440
|
+
)
|
|
441
|
+
}
|
|
383
442
|
}
|
|
384
443
|
}
|
|
385
444
|
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "efd7e9fc",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
4
|
+
"lockfileHash": "851b1664",
|
|
5
|
+
"browserHash": "1b9872e1",
|
|
6
6
|
"optimized": {
|
|
7
7
|
"astro > cssesc": {
|
|
8
8
|
"src": "../../../../../node_modules/cssesc/cssesc.js",
|
|
9
9
|
"file": "astro___cssesc.js",
|
|
10
|
-
"fileHash": "
|
|
10
|
+
"fileHash": "bd1b469f",
|
|
11
11
|
"needsInterop": true
|
|
12
12
|
},
|
|
13
13
|
"astro > aria-query": {
|
|
14
14
|
"src": "../../../../../node_modules/aria-query/lib/index.js",
|
|
15
15
|
"file": "astro___aria-query.js",
|
|
16
|
-
"fileHash": "
|
|
16
|
+
"fileHash": "796388ea",
|
|
17
17
|
"needsInterop": true
|
|
18
18
|
},
|
|
19
19
|
"astro > axobject-query": {
|
|
20
20
|
"src": "../../../../../node_modules/axobject-query/lib/index.js",
|
|
21
21
|
"file": "astro___axobject-query.js",
|
|
22
|
-
"fileHash": "
|
|
22
|
+
"fileHash": "deadbce3",
|
|
23
23
|
"needsInterop": true
|
|
24
24
|
}
|
|
25
25
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .
|
|
1
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
2
2
|
|
|
3
3
|
# Workflow Templates
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .
|
|
1
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
2
2
|
|
|
3
3
|
# Workflow: Bug Fix
|
|
4
4
|
|
|
@@ -29,9 +29,9 @@ Follow the **Delivery Outcome** in `general.instructions.md` and the **Branch Ow
|
|
|
29
29
|
|
|
30
30
|
### Steps
|
|
31
31
|
|
|
32
|
-
1. Check `.
|
|
32
|
+
1. Check `.opencastle/KNOWN-ISSUES.md` for existing entry
|
|
33
33
|
2. Check tracker for existing bug ticket
|
|
34
|
-
3. Read `.
|
|
34
|
+
3. Read `.opencastle/LESSONS-LEARNED.md` for related pitfalls
|
|
35
35
|
4. **Reproduce the bug** — this is mandatory before any fix attempt:
|
|
36
36
|
a. Start the dev server (see the **codebase-tool** skill for the serve command)
|
|
37
37
|
b. Navigate to the affected page in Chrome
|
|
@@ -110,7 +110,7 @@ Follow the **Delivery Outcome** in `general.instructions.md` and the **Branch Ow
|
|
|
110
110
|
4. Test adjacent features for regressions
|
|
111
111
|
5. If security-related: schedule panel review
|
|
112
112
|
6. Move tracker issue to Done
|
|
113
|
-
7. Update `.
|
|
113
|
+
7. Update `.opencastle/KNOWN-ISSUES.md` if the bug was listed there
|
|
114
114
|
8. Commit and push
|
|
115
115
|
|
|
116
116
|
### Exit Criteria
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .
|
|
1
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
2
2
|
|
|
3
3
|
# Workflow: Data Pipeline
|
|
4
4
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .
|
|
1
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
2
2
|
|
|
3
3
|
# Workflow: Database Migration
|
|
4
4
|
|
|
5
5
|
Structured workflow for database schema changes, RLS policies, and data migrations.
|
|
6
6
|
|
|
7
|
-
> **Project config:** For database-specific paths, schema details, and migration conventions, see the relevant database customization file in
|
|
7
|
+
> **Project config:** For database-specific paths, schema details, and migration conventions, see the relevant database customization file in `.opencastle/stack/`.
|
|
8
8
|
|
|
9
9
|
## Phases
|
|
10
10
|
|
|
@@ -34,8 +34,8 @@ Follow the **Delivery Outcome** in `general.instructions.md` and the **Branch Ow
|
|
|
34
34
|
|
|
35
35
|
1. Read current schema in the migrations directory (see database customization) to understand existing tables
|
|
36
36
|
2. Check existing RLS policies using the database query tool
|
|
37
|
-
3. Read `.
|
|
38
|
-
4. Check `.
|
|
37
|
+
3. Read `.opencastle/project.instructions.md` for database architecture
|
|
38
|
+
4. Check `.opencastle/KNOWN-ISSUES.md` for database-related limitations
|
|
39
39
|
5. Document the migration plan: tables affected, columns added/removed, RLS changes
|
|
40
40
|
6. Write rollback strategy (how to reverse the migration)
|
|
41
41
|
7. Create tracker issue with migration details and rollback plan
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .
|
|
1
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
2
2
|
|
|
3
3
|
# Workflow: Feature Implementation
|
|
4
4
|
|
|
@@ -77,7 +77,7 @@ Run the `brainstorm` prompt when the task has ambiguity, multiple valid approach
|
|
|
77
77
|
|
|
78
78
|
### Steps
|
|
79
79
|
|
|
80
|
-
1. Read `.
|
|
80
|
+
1. Read `.opencastle/project.instructions.md`, `.opencastle/KNOWN-ISSUES.md`, `.opencastle/LESSONS-LEARNED.md`
|
|
81
81
|
2. Search codebase for existing implementations
|
|
82
82
|
3. Identify affected apps, libs, and layers
|
|
83
83
|
4. **Spec flow analysis** — Trace the complete user flow end-to-end and identify:
|
|
@@ -212,7 +212,7 @@ If there are no open questions, explicitly state: "No open questions — plan is
|
|
|
212
212
|
- Final responsive sweep at all breakpoints (if UI changes)
|
|
213
213
|
7. Move all issues to Done
|
|
214
214
|
8. Update session checkpoint → delete checkpoint
|
|
215
|
-
9. Update `.
|
|
215
|
+
9. Update `.opencastle/project/roadmap.md`
|
|
216
216
|
|
|
217
217
|
### Exit Criteria
|
|
218
218
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .
|
|
1
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
2
2
|
|
|
3
3
|
# Workflow: Performance Optimization
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .
|
|
1
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
2
2
|
|
|
3
3
|
# Workflow: Code Refactoring
|
|
4
4
|
|