Skip to content

Commit d4bfa75

Browse files
authored
build: modularize codebase and improve type generation (#31)
Split monolithic skills-core.ts into focused modules (skills, agents, commands, frontmatter, bootstrap, walk-dir). Remove skill-tool hook system. Enable TypeScript declaration generation in build process. Update all imports and tests accordingly.
1 parent 99f70c7 commit d4bfa75

18 files changed

Lines changed: 520 additions & 747 deletions

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@
2121
],
2222
"scripts": {
2323
"clean": "rimraf dist",
24-
"build": "bun run clean && bun build src/index.ts src/cli.ts --outdir dist --target bun --splitting --packages external",
24+
"build": "bun run clean && bun build src/index.ts src/cli.ts --outdir dist --target bun --splitting --packages external && tsc --emitDeclarationOnly",
2525
"dev": "bun --watch src/index.ts",
2626
"test": "bun test tests/unit",
2727
"test:integration": "bun test tests/integration",
2828
"test:all": "bun test",
2929
"typecheck": "tsc --noEmit",
3030
"lint": "biome check .",
3131
"fix": "bun run lint --fix",
32-
"prepublishOnly": "bun run build && bun run test"
32+
"prepublishOnly": "bun run build"
3333
},
3434
"keywords": [
3535
"opencode",

src/cli.ts

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
#!/usr/bin/env node
22
import fs from 'node:fs'
33
import path from 'node:path'
4+
import * as agents from './lib/agents.js'
5+
import * as commands from './lib/commands.js'
6+
import { getConfigPaths } from './lib/config.js'
47
import {
58
type AgentMode,
69
type ContentType,
710
convertContent,
811
} from './lib/converter.js'
9-
import * as skillsCore from './lib/skills-core.js'
12+
import * as skills from './lib/skills.js'
1013

1114
const VERSION = '0.1.0'
1215

@@ -37,46 +40,32 @@ Examples:
3740
systematic config show
3841
`
3942

40-
function getUserConfigDir(): string {
41-
return path.join(
42-
process.env.HOME || process.env.USERPROFILE || '.',
43-
'.config/opencode',
44-
)
45-
}
46-
47-
function getProjectConfigDir(): string {
48-
return path.join(process.cwd(), '.opencode')
49-
}
50-
5143
function listItems(type: string): void {
5244
const packageRoot = path.resolve(import.meta.dirname, '..')
5345
const bundledDir = packageRoot
5446

55-
let finder: (
56-
dir: string,
57-
sourceType: 'bundled',
58-
) => Array<{ name: string; sourceType: string }>
47+
let finder: (dir: string) => Array<{ name: string }>
5948
let subdir: string
6049

6150
switch (type) {
6251
case 'skills':
63-
finder = skillsCore.findSkillsInDir
52+
finder = skills.findSkillsInDir
6453
subdir = 'skills'
6554
break
6655
case 'agents':
67-
finder = skillsCore.findAgentsInDir
56+
finder = agents.findAgentsInDir
6857
subdir = 'agents'
6958
break
7059
case 'commands':
71-
finder = skillsCore.findCommandsInDir
60+
finder = commands.findCommandsInDir
7261
subdir = 'commands'
7362
break
7463
default:
7564
console.error(`Unknown type: ${type}. Use: skills, agents, commands`)
7665
process.exit(1)
7766
}
7867

79-
const items = finder(path.join(bundledDir, subdir), 'bundled')
68+
const items = finder(path.join(bundledDir, subdir))
8069

8170
if (items.length === 0) {
8271
console.log(`No ${type} found.`)
@@ -85,7 +74,7 @@ function listItems(type: string): void {
8574

8675
console.log(`Available ${type}:\n`)
8776
for (const item of items.sort((a, b) => a.name.localeCompare(b.name))) {
88-
console.log(` ${item.name} (${item.sourceType})`)
77+
console.log(` ${item.name}`)
8978
}
9079
}
9180

@@ -124,33 +113,29 @@ function runConvert(type: string, filePath: string, modeArg?: string): void {
124113
}
125114

126115
function configShow(): void {
127-
const userDir = getUserConfigDir()
128-
const projectDir = getProjectConfigDir()
116+
const paths = getConfigPaths(process.cwd())
129117

130118
console.log('Configuration locations:\n')
131-
console.log(` User config: ${path.join(userDir, 'systematic.json')}`)
132-
console.log(` Project config: ${path.join(projectDir, 'systematic.json')}`)
119+
console.log(` User config: ${paths.userConfig}`)
120+
console.log(` Project config: ${paths.projectConfig}`)
133121

134-
const projectConfig = path.join(projectDir, 'systematic.json')
135-
if (fs.existsSync(projectConfig)) {
122+
if (fs.existsSync(paths.projectConfig)) {
136123
console.log('\nProject configuration:')
137-
console.log(fs.readFileSync(projectConfig, 'utf-8'))
124+
console.log(fs.readFileSync(paths.projectConfig, 'utf-8'))
138125
}
139126

140-
const userConfig = path.join(userDir, 'systematic.json')
141-
if (fs.existsSync(userConfig)) {
127+
if (fs.existsSync(paths.userConfig)) {
142128
console.log('\nUser configuration:')
143-
console.log(fs.readFileSync(userConfig, 'utf-8'))
129+
console.log(fs.readFileSync(paths.userConfig, 'utf-8'))
144130
}
145131
}
146132

147133
function configPath(): void {
148-
const userDir = getUserConfigDir()
149-
const projectDir = getProjectConfigDir()
134+
const paths = getConfigPaths(process.cwd())
150135

151136
console.log('Config file paths:')
152-
console.log(` User: ${path.join(userDir, 'systematic.json')}`)
153-
console.log(` Project: ${path.join(projectDir, 'systematic.json')}`)
137+
console.log(` User: ${paths.userConfig}`)
138+
console.log(` Project: ${paths.projectConfig}`)
154139
}
155140

156141
const args = process.argv.slice(2)

src/index.ts

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import fs from 'node:fs'
2-
import os from 'node:os'
32
import path from 'node:path'
43
import { fileURLToPath } from 'node:url'
54
import type { Plugin } from '@opencode-ai/plugin'
6-
import { loadConfig, type SystematicConfig } from './lib/config.js'
5+
import { getBootstrapContent } from './lib/bootstrap.js'
6+
import { loadConfig } from './lib/config.js'
77
import { createConfigHandler } from './lib/config-handler.js'
88
import { createSkillTool } from './lib/skill-tool.js'
9-
import * as skillsCore from './lib/skills-core.js'
109

1110
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1211

@@ -28,57 +27,6 @@ const getPackageVersion = (): string => {
2827
}
2928
}
3029

31-
const getBootstrapContent = (config: SystematicConfig): string | null => {
32-
if (!config.bootstrap.enabled) return null
33-
34-
if (config.bootstrap.file) {
35-
const customPath = config.bootstrap.file.startsWith('~/')
36-
? path.join(os.homedir(), config.bootstrap.file.slice(2))
37-
: config.bootstrap.file
38-
if (fs.existsSync(customPath)) {
39-
return fs.readFileSync(customPath, 'utf8')
40-
}
41-
}
42-
43-
const usingSystematicPath = path.join(
44-
bundledSkillsDir,
45-
'using-systematic/SKILL.md',
46-
)
47-
if (!fs.existsSync(usingSystematicPath)) return null
48-
49-
const fullContent = fs.readFileSync(usingSystematicPath, 'utf8')
50-
const content = skillsCore.stripFrontmatter(fullContent)
51-
52-
const toolMapping = `**Tool Mapping for OpenCode:**
53-
When skills reference tools you don't have, substitute OpenCode equivalents:
54-
- \`TodoWrite\` → \`update_plan\`
55-
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
56-
- \`Skill\` tool → OpenCode's native \`skill\` tool
57-
- \`SystematicSkill\` tool → \`systematic_skill\` (Systematic plugin skills)
58-
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
59-
60-
**Skills naming:**
61-
- Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
62-
- Skills can also be invoked without prefix if unambiguous
63-
64-
**Skills usage:**
65-
- Use \`systematic_skill\` to load Systematic bundled skills
66-
- Use the native \`skill\` tool for non-Systematic skills
67-
68-
**Skills location:**
69-
Bundled skills are in \`${bundledSkillsDir}/\``
70-
71-
return `<SYSTEMATIC_WORKFLOWS>
72-
You have access to structured engineering workflows via the systematic plugin.
73-
74-
**IMPORTANT: The using-systematic skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the systematic_skill tool to load "using-systematic" again - that would be redundant.**
75-
76-
${content}
77-
78-
${toolMapping}
79-
</SYSTEMATIC_WORKFLOWS>`
80-
}
81-
8230
export const SystematicPlugin: Plugin = async ({ client, directory }) => {
8331
const config = loadConfig(directory)
8432

@@ -131,7 +79,7 @@ export const SystematicPlugin: Plugin = async ({ client, directory }) => {
13179
) {
13280
return
13381
}
134-
const content = getBootstrapContent(config)
82+
const content = getBootstrapContent(config, { bundledSkillsDir })
13583
if (content) {
13684
if (!output.system) {
13785
output.system = []

src/lib/agents.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { walkDir } from './walk-dir.js'
2+
import { stripFrontmatter } from './frontmatter.js'
3+
4+
export interface AgentFrontmatter {
5+
name: string
6+
description: string
7+
prompt: string
8+
}
9+
10+
export interface AgentInfo {
11+
name: string
12+
file: string
13+
category?: string
14+
}
15+
16+
export function findAgentsInDir(dir: string, maxDepth = 2): AgentInfo[] {
17+
const entries = walkDir(dir, {
18+
maxDepth,
19+
filter: (e) => !e.isDirectory && e.name.endsWith('.md'),
20+
})
21+
22+
return entries.map((entry) => ({
23+
name: entry.name.replace(/\.md$/, ''),
24+
file: entry.path,
25+
category: entry.category,
26+
}))
27+
}
28+
29+
export function extractAgentFrontmatter(content: string): AgentFrontmatter {
30+
const lines = content.split('\n')
31+
32+
let inFrontmatter = false
33+
let name = ''
34+
let description = ''
35+
36+
for (const line of lines) {
37+
if (line.trim() === '---') {
38+
if (inFrontmatter) break
39+
inFrontmatter = true
40+
continue
41+
}
42+
43+
if (inFrontmatter) {
44+
const match = line.match(/^(\w+(?:-\w+)*):\s*(.*)$/)
45+
if (match) {
46+
const [, key, value] = match
47+
if (key === 'name') name = value.trim()
48+
if (key === 'description') description = value.trim()
49+
}
50+
}
51+
}
52+
53+
return { name, description, prompt: stripFrontmatter(content) }
54+
}

src/lib/bootstrap.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import fs from 'node:fs'
2+
import os from 'node:os'
3+
import path from 'node:path'
4+
import type { SystematicConfig } from './config.js'
5+
import { stripFrontmatter } from './frontmatter.js'
6+
7+
export interface BootstrapDeps {
8+
bundledSkillsDir: string
9+
}
10+
11+
function getToolMappingTemplate(bundledSkillsDir: string): string {
12+
return `**Tool Mapping for OpenCode:**
13+
When skills reference tools you don't have, substitute OpenCode equivalents:
14+
- \`TodoWrite\` → \`update_plan\`
15+
- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
16+
- \`Skill\` tool → OpenCode's native \`skill\` tool
17+
- \`SystematicSkill\` tool → \`systematic_skill\` (Systematic plugin skills)
18+
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
19+
20+
**Skills naming:**
21+
- Bundled skills use the \`systematic:\` prefix (e.g., \`systematic:brainstorming\`)
22+
- Skills can also be invoked without prefix if unambiguous
23+
24+
**Skills usage:**
25+
- Use \`systematic_skill\` to load Systematic bundled skills
26+
- Use the native \`skill\` tool for non-Systematic skills
27+
28+
**Skills location:**
29+
Bundled skills are in \`${bundledSkillsDir}/\``
30+
}
31+
32+
export function getBootstrapContent(
33+
config: SystematicConfig,
34+
deps: BootstrapDeps,
35+
): string | null {
36+
const { bundledSkillsDir } = deps
37+
38+
if (!config.bootstrap.enabled) return null
39+
40+
if (config.bootstrap.file) {
41+
const customPath = config.bootstrap.file.startsWith('~/')
42+
? path.join(os.homedir(), config.bootstrap.file.slice(2))
43+
: config.bootstrap.file
44+
if (fs.existsSync(customPath)) {
45+
return fs.readFileSync(customPath, 'utf8')
46+
}
47+
}
48+
49+
const usingSystematicPath = path.join(
50+
bundledSkillsDir,
51+
'using-systematic/SKILL.md',
52+
)
53+
if (!fs.existsSync(usingSystematicPath)) return null
54+
55+
const fullContent = fs.readFileSync(usingSystematicPath, 'utf8')
56+
const content = stripFrontmatter(fullContent)
57+
const toolMapping = getToolMappingTemplate(bundledSkillsDir)
58+
59+
return `<SYSTEMATIC_WORKFLOWS>
60+
You have access to structured engineering workflows via the systematic plugin.
61+
62+
**IMPORTANT: The using-systematic skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the systematic_skill tool to load "using-systematic" again - that would be redundant.**
63+
64+
${content}
65+
66+
${toolMapping}
67+
</SYSTEMATIC_WORKFLOWS>`
68+
}

0 commit comments

Comments
 (0)