@plainconceptsplatform/agent-harness 2.3.0 → 2.4.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/README.md +23 -12
- package/{src → cli}/commands/join.js +3 -4
- package/{src → cli}/commands/migrate.js +1 -1
- package/{src → cli}/commands/wizard.js +1 -1
- package/{src → cli}/index.js +1 -1
- package/{src → cli}/presets/agents-content.json +4 -4
- package/cli/presets/browser.json +10 -0
- package/{src → cli}/presets/clean.json +21 -21
- package/{src → cli}/presets/optimization.json +37 -37
- package/{src → cli}/presets/platforms.json +76 -76
- package/{src → cli}/presets/quota.json +16 -16
- package/{src → cli}/presets/source.json +23 -23
- package/cli/steps/browser/index.js +48 -0
- package/{src → cli}/steps/clean/index.js +120 -120
- package/{src → cli}/steps/copy/agents.js +119 -119
- package/{src → cli}/steps/copy/commands.js +95 -95
- package/{src → cli}/steps/copy/fullstack-engineer.js +89 -88
- package/{src → cli}/steps/copy/index.js +1 -1
- package/{src → cli}/steps/copy/opencode-json.js +196 -161
- package/{src → cli}/steps/copy/skills.js +2 -2
- package/{src → cli}/steps/models/format.js +88 -88
- package/{src → cli}/steps/models/index.js +64 -64
- package/{src → cli}/steps/openspec/index.js +136 -136
- package/{src → cli}/steps/optimization/codegraph.js +127 -127
- package/{src → cli}/steps/optimization/detect.js +66 -66
- package/{src → cli}/steps/optimization/humanizer.js +17 -17
- package/{src → cli}/steps/optimization/index.js +163 -163
- package/{src → cli}/steps/optimization/memory.js +88 -88
- package/{src → cli}/steps/optimization/patch-guardrails.js +109 -109
- package/{src → cli}/steps/optimization/quota.js +119 -119
- package/{src → cli}/steps/optimization/simple-english.js +17 -17
- package/{src → cli}/steps/optimization/skills-lock.js +30 -30
- package/{src → cli}/steps/platform/index.js +109 -109
- package/{src → cli}/steps/source/index.js +123 -123
- package/{src → cli}/utils/agent-color.js +111 -83
- package/{src → cli}/utils/exec-spinner.js +47 -47
- package/{src → cli}/utils/exec.js +134 -134
- package/{src → cli}/utils/models-pricing.js +42 -42
- package/{src → cli}/utils/paths.js +1 -1
- package/{src → cli}/utils/process.js +3 -3
- package/{src → cli}/utils/terminal.js +6 -6
- package/harness/.agents/skills/browser-automation/SKILL.md +72 -0
- package/{src/content → harness}/.agents/skills/pc-make-engineer/SKILL.md +219 -219
- package/{src/content → harness}/.agents/skills/pc-make-engineer/template.md +80 -80
- package/{src/content → harness}/.agents/skills/pc-plan-archive/SKILL.md +3 -0
- package/{src/content → harness}/.agents/skills/pc-plan-explore/SKILL.md +3 -0
- package/{src/content → harness}/.agents/skills/pc-plan-goal/SKILL.md +5 -16
- package/{src/content → harness}/.agents/skills/pc-plan-goal/branching.md +1 -1
- package/{src/content → harness}/.agents/skills/pc-plan-goal/output.md +5 -8
- package/{src/content → harness}/.agents/skills/pc-plan-story/SKILL.md +3 -0
- package/{src/content → harness}/.agents/skills/pc-userstory-browser/SKILL.md +136 -132
- package/{src/content → harness}/.opencode/package.json +9 -10
- package/{src/content → harness}/.opencode/plugins/pc-subagent-tiers.js +368 -337
- package/{src/content → harness}/ARCHITECTURE.md +16 -16
- package/{src/content → harness}/DESIGN.md +16 -16
- package/{src/content → harness}/opencode.jsonc +47 -41
- package/{src/content → harness}/openspec/config.yaml +20 -20
- package/{src/content → harness}/skills-lock.json +17 -17
- package/package.json +7 -7
- package/src/content/.agents/skills/browser-automation/SKILL.md +0 -66
- package/src/presets/browser.json +0 -22
- package/src/steps/browser/index.js +0 -91
- /package/{src → cli}/commands/shared.js +0 -0
- /package/{src → cli}/commands/single.js +0 -0
- /package/{src → cli}/commands/update.js +0 -0
- /package/{src → cli}/fragments/archive/az.md +0 -0
- /package/{src → cli}/fragments/archive/gh.md +0 -0
- /package/{src → cli}/fragments/archive/gl.md +0 -0
- /package/{src → cli}/fragments/archive/none.md +0 -0
- /package/{src → cli}/fragments/guardrails/codegraph.md +0 -0
- /package/{src → cli}/fragments/guardrails/humanizer.md +0 -0
- /package/{src → cli}/fragments/guardrails/memory.md +0 -0
- /package/{src → cli}/fragments/guardrails/rtk.md +0 -0
- /package/{src → cli}/fragments/guardrails/simple-english.md +0 -0
- /package/{src → cli}/fragments/ops-backlog/az.md +0 -0
- /package/{src → cli}/fragments/ops-backlog/gh.md +0 -0
- /package/{src → cli}/fragments/ops-backlog/jira.md +0 -0
- /package/{src → cli}/fragments/ops-evidence/az.md +0 -0
- /package/{src → cli}/fragments/ops-evidence/gh.md +0 -0
- /package/{src → cli}/fragments/ops-evidence/jira.md +0 -0
- /package/{src → cli}/fragments/ops-review/az.md +0 -0
- /package/{src → cli}/fragments/ops-review/gh.md +0 -0
- /package/{src → cli}/fragments/ops-review/gl.md +0 -0
- /package/{src → cli}/fragments/ops-ship/az.md +0 -0
- /package/{src → cli}/fragments/ops-ship/gh.md +0 -0
- /package/{src → cli}/fragments/ops-ship/gl.md +0 -0
- /package/{src → cli}/presets/models.json +0 -0
- /package/{src → cli}/presets/openspec.json +0 -0
- /package/{src → cli}/steps/metadata/index.js +0 -0
- /package/{src → cli}/steps/models/write.js +0 -0
- /package/{src → cli}/utils/copy.js +0 -0
- /package/{src → cli}/utils/legacy-check.js +0 -0
- /package/{src → cli}/utils/models-cache.js +0 -0
- /package/{src → cli}/utils/update-manifest.js +0 -0
- /package/{src/content → harness}/.agents/skills/pc-guardrails-generic/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-guardrails-project/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-architecture/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-architecture/structure-template.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-design/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-engineer/signal-mapping.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-guardrails/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-guardrails/category-reference.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/category-reference.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-make-user-model/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-ops-evidence/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-ops-ship/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-plan-apply/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-plan-apply/simple-mode.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-plan-goal/failure-policy.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-plan-goal/output-mode.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-plan-propose/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-plan-propose/task-annotation.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-plan-quick/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-repo-audit/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-repo-help/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-repo-initialize/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-repo-onboard/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-repo-verify/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-userstory-az/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-userstory-gh/SKILL.md +0 -0
- /package/{src/content → harness}/.agents/skills/pc-userstory-jira/SKILL.md +0 -0
- /package/{src/content → harness}/.opencode/_gitignore +0 -0
- /package/{src/content → harness}/.opencode/commands/init.md +0 -0
- /package/{src/content → harness}/.opencode/commands/make-architecture.md +0 -0
- /package/{src/content → harness}/.opencode/commands/make-design.md +0 -0
- /package/{src/content → harness}/.opencode/commands/make-engineer.md +0 -0
- /package/{src/content → harness}/.opencode/commands/make-evidence-scaffold.md +0 -0
- /package/{src/content → harness}/.opencode/commands/make-guardrails.md +0 -0
- /package/{src/content → harness}/.opencode/commands/make-user-model.md +0 -0
- /package/{src/content → harness}/.opencode/commands/ops-backlog.md +0 -0
- /package/{src/content → harness}/.opencode/commands/ops-evidence.md +0 -0
- /package/{src/content → harness}/.opencode/commands/ops-review.md +0 -0
- /package/{src/content → harness}/.opencode/commands/ops-ship.md +0 -0
- /package/{src/content → harness}/.opencode/commands/plan-apply.md +0 -0
- /package/{src/content → harness}/.opencode/commands/plan-archive.md +0 -0
- /package/{src/content → harness}/.opencode/commands/plan-explore.md +0 -0
- /package/{src/content → harness}/.opencode/commands/plan-goal.md +0 -0
- /package/{src/content → harness}/.opencode/commands/plan-propose.md +0 -0
- /package/{src/content → harness}/.opencode/commands/plan-quick.md +0 -0
- /package/{src/content → harness}/.opencode/commands/plan-story.md +0 -0
- /package/{src/content → harness}/.opencode/commands/repo-audit.md +0 -0
- /package/{src/content → harness}/.opencode/commands/repo-help.md +0 -0
- /package/{src/content → harness}/.opencode/commands/repo-initialize.md +0 -0
- /package/{src/content → harness}/.opencode/commands/repo-onboard.md +0 -0
- /package/{src/content → harness}/.opencode/commands/repo-verify.md +0 -0
- /package/{src/content → harness}/.opencode/plugins/pc-subagent-monitor.js +0 -0
- /package/{src/content → harness}/.opencode/plugins/pc-system-reminders.js +0 -0
- /package/{src/content → harness}/.opencode/tui/pc-subagents.tsx +0 -0
- /package/{src/content → harness}/.opencode/tui.json +0 -0
- /package/{src/content → harness}/AGENTS.md +0 -0
- /package/{src/content → harness}/openspec/changes/archive/.gitkeep +0 -0
- /package/{src/content → harness}/openspec/specs/.gitkeep +0 -0
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { header, info, prompt, success, warn } from '../../utils/exec.js'
|
|
2
|
-
import { fetchModels } from '../../utils/models-cache.js'
|
|
3
|
-
import { buildDisplayModels, filterModelsByProvider, modelsPreset, pickModel, pickProvider } from './format.js'
|
|
4
|
-
import { writeModelsToConfigs } from './write.js'
|
|
5
|
-
|
|
6
|
-
async function pickRoleModel(role, allModels) {
|
|
7
|
-
const rolePreset = modelsPreset.roles[role]
|
|
8
|
-
const [first, ...rest] = rolePreset.info
|
|
9
|
-
prompt(first)
|
|
10
|
-
for (const line of rest) info(line)
|
|
11
|
-
console.log()
|
|
12
|
-
|
|
13
|
-
while (true) {
|
|
14
|
-
const provider = await pickProvider('Provider:')
|
|
15
|
-
const filtered = filterModelsByProvider(allModels, provider)
|
|
16
|
-
const available = filtered.length > 0 ? filtered : allModels
|
|
17
|
-
|
|
18
|
-
const suggestion = rolePreset.suggestions?.[provider]
|
|
19
|
-
if (suggestion) info(` Suggested: ${suggestion}`)
|
|
20
|
-
if (filtered.length === 0 && provider) warn(` No ${provider} models found: showing all`)
|
|
21
|
-
|
|
22
|
-
const model = await pickModel(rolePreset.prompt, available)
|
|
23
|
-
if (model !== '__back__') return model
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function chooseModels() {
|
|
28
|
-
header('Step 7, Choose models');
|
|
29
|
-
|
|
30
|
-
info('Fetching available models from models.dev...');
|
|
31
|
-
const { models: rawModels, source } = await fetchModels();
|
|
32
|
-
|
|
33
|
-
if (!rawModels) {
|
|
34
|
-
warn('Could not fetch models (offline and no cache). Skipping model selection.');
|
|
35
|
-
warn('Set models later in opencode.jsonc');
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (source === 'stale-cache') {
|
|
40
|
-
warn('Network unavailable, using cached model list (may be outdated).');
|
|
41
|
-
} else if (source === 'cache') {
|
|
42
|
-
info('Using cached model list (refreshes weekly).');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const models = buildDisplayModels(rawModels);
|
|
46
|
-
success(`${models.length} models available`);
|
|
47
|
-
console.log();
|
|
48
|
-
info('Three models: main session, engineers, and light commands.');
|
|
49
|
-
info('Cost indicators: [$] cheap [$$] mid [$$$] expensive');
|
|
50
|
-
info('Pick provider first, then search by name. Change later in opencode.jsonc');
|
|
51
|
-
console.log();
|
|
52
|
-
|
|
53
|
-
const planModel = await pickRoleModel('plan', models); console.log();
|
|
54
|
-
const buildModel = await pickRoleModel('build', models); console.log();
|
|
55
|
-
const fastModel = await pickRoleModel('fast', models); console.log();
|
|
56
|
-
|
|
57
|
-
await writeModelsToConfigs({ planModel, buildModel, fastModel });
|
|
58
|
-
|
|
59
|
-
console.log();
|
|
60
|
-
warn('Make sure you have API access to the selected models.');
|
|
61
|
-
warn('Change them anytime in opencode.jsonc');
|
|
62
|
-
|
|
63
|
-
return { planModel, buildModel, fastModel };
|
|
64
|
-
}
|
|
1
|
+
import { header, info, prompt, success, warn } from '../../utils/exec.js'
|
|
2
|
+
import { fetchModels } from '../../utils/models-cache.js'
|
|
3
|
+
import { buildDisplayModels, filterModelsByProvider, modelsPreset, pickModel, pickProvider } from './format.js'
|
|
4
|
+
import { writeModelsToConfigs } from './write.js'
|
|
5
|
+
|
|
6
|
+
async function pickRoleModel(role, allModels) {
|
|
7
|
+
const rolePreset = modelsPreset.roles[role]
|
|
8
|
+
const [first, ...rest] = rolePreset.info
|
|
9
|
+
prompt(first)
|
|
10
|
+
for (const line of rest) info(line)
|
|
11
|
+
console.log()
|
|
12
|
+
|
|
13
|
+
while (true) {
|
|
14
|
+
const provider = await pickProvider('Provider:')
|
|
15
|
+
const filtered = filterModelsByProvider(allModels, provider)
|
|
16
|
+
const available = filtered.length > 0 ? filtered : allModels
|
|
17
|
+
|
|
18
|
+
const suggestion = rolePreset.suggestions?.[provider]
|
|
19
|
+
if (suggestion) info(` Suggested: ${suggestion}`)
|
|
20
|
+
if (filtered.length === 0 && provider) warn(` No ${provider} models found: showing all`)
|
|
21
|
+
|
|
22
|
+
const model = await pickModel(rolePreset.prompt, available)
|
|
23
|
+
if (model !== '__back__') return model
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function chooseModels() {
|
|
28
|
+
header('Step 7, Choose models');
|
|
29
|
+
|
|
30
|
+
info('Fetching available models from models.dev...');
|
|
31
|
+
const { models: rawModels, source } = await fetchModels();
|
|
32
|
+
|
|
33
|
+
if (!rawModels) {
|
|
34
|
+
warn('Could not fetch models (offline and no cache). Skipping model selection.');
|
|
35
|
+
warn('Set models later in opencode.jsonc');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (source === 'stale-cache') {
|
|
40
|
+
warn('Network unavailable, using cached model list (may be outdated).');
|
|
41
|
+
} else if (source === 'cache') {
|
|
42
|
+
info('Using cached model list (refreshes weekly).');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const models = buildDisplayModels(rawModels);
|
|
46
|
+
success(`${models.length} models available`);
|
|
47
|
+
console.log();
|
|
48
|
+
info('Three models: main session, engineers, and light commands.');
|
|
49
|
+
info('Cost indicators: [$] cheap [$$] mid [$$$] expensive');
|
|
50
|
+
info('Pick provider first, then search by name. Change later in opencode.jsonc');
|
|
51
|
+
console.log();
|
|
52
|
+
|
|
53
|
+
const planModel = await pickRoleModel('plan', models); console.log();
|
|
54
|
+
const buildModel = await pickRoleModel('build', models); console.log();
|
|
55
|
+
const fastModel = await pickRoleModel('fast', models); console.log();
|
|
56
|
+
|
|
57
|
+
await writeModelsToConfigs({ planModel, buildModel, fastModel });
|
|
58
|
+
|
|
59
|
+
console.log();
|
|
60
|
+
warn('Make sure you have API access to the selected models.');
|
|
61
|
+
warn('Change them anytime in opencode.jsonc');
|
|
62
|
+
|
|
63
|
+
return { planModel, buildModel, fastModel };
|
|
64
|
+
}
|
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
import { execa } from "execa"
|
|
2
|
-
import fse from "fs-extra"
|
|
3
|
-
import path from "node:path"
|
|
4
|
-
import { commandExists, error, header, info, loading, success, warn } from "../../utils/exec.js"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
async function cleanOpenspecFiles() {
|
|
8
|
-
const cwd = process.cwd()
|
|
9
|
-
const commandsDir = path.join(cwd, '.opencode', 'commands')
|
|
10
|
-
const opencodeSkillsDir = path.join(cwd, '.opencode', 'skills')
|
|
11
|
-
const agentsSkillsDir = path.join(cwd, '.agents', 'skills')
|
|
12
|
-
|
|
13
|
-
// Delete opsx-* command files generated by OpenSpec: we use /plan-propose and /plan-apply instead
|
|
14
|
-
if (await fse.pathExists(commandsDir)) {
|
|
15
|
-
for (const f of await fse.readdir(commandsDir)) {
|
|
16
|
-
if (f.startsWith('opsx-') && f.endsWith('.md')) {
|
|
17
|
-
await fse.remove(path.join(commandsDir, f))
|
|
18
|
-
info(`Removed generated command: ${f}`)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Move .opencode/skills/* to .agents/skills/: skills should be in the universal location
|
|
24
|
-
if (await fse.pathExists(opencodeSkillsDir)) {
|
|
25
|
-
await fse.ensureDir(agentsSkillsDir)
|
|
26
|
-
for (const dir of await fse.readdir(opencodeSkillsDir)) {
|
|
27
|
-
const src = path.join(opencodeSkillsDir, dir)
|
|
28
|
-
const dest = path.join(agentsSkillsDir, dir)
|
|
29
|
-
if (!(await fse.pathExists(dest))) {
|
|
30
|
-
await fse.move(src, dest)
|
|
31
|
-
info(`Moved skill: ${dir} → .agents/skills/`)
|
|
32
|
-
} else {
|
|
33
|
-
await fse.remove(src)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// Remove .opencode/skills if now empty
|
|
37
|
-
const remaining = await fse.readdir(opencodeSkillsDir)
|
|
38
|
-
if (remaining.length === 0) await fse.remove(opencodeSkillsDir)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
success('OpenSpec generated files cleaned up')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const openspecSteps = {
|
|
45
|
-
check,
|
|
46
|
-
install,
|
|
47
|
-
init,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function initOpenspec() {
|
|
51
|
-
header("Step 6, Initializing OpenSpec")
|
|
52
|
-
|
|
53
|
-
let openSpec = await openspecSteps.check()
|
|
54
|
-
if (!openSpec.available) {
|
|
55
|
-
await openspecSteps.install()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
openSpec = await openspecSteps.check()
|
|
59
|
-
if (!openSpec.available) {
|
|
60
|
-
warn("OpenSpec has not been installed. It is required to create changes.")
|
|
61
|
-
return { ...openSpec, initialized: false }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const initResult = await openspecSteps.init()
|
|
65
|
-
if (!initResult.initialized) {
|
|
66
|
-
warn(
|
|
67
|
-
"OpenSpec has not been initialized. It is required to create changes.",
|
|
68
|
-
)
|
|
69
|
-
return { ...openSpec, initialized: false }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
await cleanOpenspecFiles()
|
|
73
|
-
return { ...openSpec, initialized: true }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function install() {
|
|
77
|
-
info("Installing OpenSpec...")
|
|
78
|
-
try {
|
|
79
|
-
const result = await execa(
|
|
80
|
-
"npm",
|
|
81
|
-
["install", "@fission-ai/openspec", "--global"],
|
|
82
|
-
{
|
|
83
|
-
cwd: process.cwd(),
|
|
84
|
-
stdio: "pipe",
|
|
85
|
-
reject: false,
|
|
86
|
-
},
|
|
87
|
-
)
|
|
88
|
-
if (result.exitCode !== 0) {
|
|
89
|
-
warn("OpenSpec install failed, check output above")
|
|
90
|
-
}
|
|
91
|
-
success("OpenSpec installed")
|
|
92
|
-
} catch (err) {
|
|
93
|
-
error(`Failed to run openspec install: ${err.message}`)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function check() {
|
|
98
|
-
loading("Checking OpenSpec...")
|
|
99
|
-
|
|
100
|
-
const available = await commandExists("openspec")
|
|
101
|
-
|
|
102
|
-
if (available) success("OpenSpec is available")
|
|
103
|
-
else warn("OpenSpec not found on PATH.")
|
|
104
|
-
|
|
105
|
-
return { available }
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function init() {
|
|
109
|
-
loading("Initializing OpenSpec...")
|
|
110
|
-
try {
|
|
111
|
-
const result = await execa(
|
|
112
|
-
"npx",
|
|
113
|
-
["@fission-ai/openspec", "init", "--tools", "opencode", "--force"],
|
|
114
|
-
{
|
|
115
|
-
cwd: process.cwd(),
|
|
116
|
-
reject: false,
|
|
117
|
-
},
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
if (result.exitCode !== 0) {
|
|
121
|
-
const stderr = (result.stderr || "").trim()
|
|
122
|
-
const msg = stderr
|
|
123
|
-
? `init failed: ${stderr}`
|
|
124
|
-
: `init failed with exit code ${result.exitCode}`
|
|
125
|
-
throw new Error(msg)
|
|
126
|
-
}
|
|
127
|
-
} catch (err) {
|
|
128
|
-
error(`Failed to run openspec init: ${err.message}`)
|
|
129
|
-
return { initialized: false }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
success("OpenSpec initialized")
|
|
133
|
-
return { initialized: true }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
import { execa } from "execa"
|
|
2
|
+
import fse from "fs-extra"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { commandExists, error, header, info, loading, success, warn } from "../../utils/exec.js"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async function cleanOpenspecFiles() {
|
|
8
|
+
const cwd = process.cwd()
|
|
9
|
+
const commandsDir = path.join(cwd, '.opencode', 'commands')
|
|
10
|
+
const opencodeSkillsDir = path.join(cwd, '.opencode', 'skills')
|
|
11
|
+
const agentsSkillsDir = path.join(cwd, '.agents', 'skills')
|
|
12
|
+
|
|
13
|
+
// Delete opsx-* command files generated by OpenSpec: we use /plan-propose and /plan-apply instead
|
|
14
|
+
if (await fse.pathExists(commandsDir)) {
|
|
15
|
+
for (const f of await fse.readdir(commandsDir)) {
|
|
16
|
+
if (f.startsWith('opsx-') && f.endsWith('.md')) {
|
|
17
|
+
await fse.remove(path.join(commandsDir, f))
|
|
18
|
+
info(`Removed generated command: ${f}`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Move .opencode/skills/* to .agents/skills/: skills should be in the universal location
|
|
24
|
+
if (await fse.pathExists(opencodeSkillsDir)) {
|
|
25
|
+
await fse.ensureDir(agentsSkillsDir)
|
|
26
|
+
for (const dir of await fse.readdir(opencodeSkillsDir)) {
|
|
27
|
+
const src = path.join(opencodeSkillsDir, dir)
|
|
28
|
+
const dest = path.join(agentsSkillsDir, dir)
|
|
29
|
+
if (!(await fse.pathExists(dest))) {
|
|
30
|
+
await fse.move(src, dest)
|
|
31
|
+
info(`Moved skill: ${dir} → .agents/skills/`)
|
|
32
|
+
} else {
|
|
33
|
+
await fse.remove(src)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Remove .opencode/skills if now empty
|
|
37
|
+
const remaining = await fse.readdir(opencodeSkillsDir)
|
|
38
|
+
if (remaining.length === 0) await fse.remove(opencodeSkillsDir)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
success('OpenSpec generated files cleaned up')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const openspecSteps = {
|
|
45
|
+
check,
|
|
46
|
+
install,
|
|
47
|
+
init,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function initOpenspec() {
|
|
51
|
+
header("Step 6, Initializing OpenSpec")
|
|
52
|
+
|
|
53
|
+
let openSpec = await openspecSteps.check()
|
|
54
|
+
if (!openSpec.available) {
|
|
55
|
+
await openspecSteps.install()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
openSpec = await openspecSteps.check()
|
|
59
|
+
if (!openSpec.available) {
|
|
60
|
+
warn("OpenSpec has not been installed. It is required to create changes.")
|
|
61
|
+
return { ...openSpec, initialized: false }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const initResult = await openspecSteps.init()
|
|
65
|
+
if (!initResult.initialized) {
|
|
66
|
+
warn(
|
|
67
|
+
"OpenSpec has not been initialized. It is required to create changes.",
|
|
68
|
+
)
|
|
69
|
+
return { ...openSpec, initialized: false }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await cleanOpenspecFiles()
|
|
73
|
+
return { ...openSpec, initialized: true }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function install() {
|
|
77
|
+
info("Installing OpenSpec...")
|
|
78
|
+
try {
|
|
79
|
+
const result = await execa(
|
|
80
|
+
"npm",
|
|
81
|
+
["install", "@fission-ai/openspec", "--global"],
|
|
82
|
+
{
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
stdio: "pipe",
|
|
85
|
+
reject: false,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
warn("OpenSpec install failed, check output above")
|
|
90
|
+
}
|
|
91
|
+
success("OpenSpec installed")
|
|
92
|
+
} catch (err) {
|
|
93
|
+
error(`Failed to run openspec install: ${err.message}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function check() {
|
|
98
|
+
loading("Checking OpenSpec...")
|
|
99
|
+
|
|
100
|
+
const available = await commandExists("openspec")
|
|
101
|
+
|
|
102
|
+
if (available) success("OpenSpec is available")
|
|
103
|
+
else warn("OpenSpec not found on PATH.")
|
|
104
|
+
|
|
105
|
+
return { available }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function init() {
|
|
109
|
+
loading("Initializing OpenSpec...")
|
|
110
|
+
try {
|
|
111
|
+
const result = await execa(
|
|
112
|
+
"npx",
|
|
113
|
+
["@fission-ai/openspec", "init", "--tools", "opencode", "--force"],
|
|
114
|
+
{
|
|
115
|
+
cwd: process.cwd(),
|
|
116
|
+
reject: false,
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if (result.exitCode !== 0) {
|
|
121
|
+
const stderr = (result.stderr || "").trim()
|
|
122
|
+
const msg = stderr
|
|
123
|
+
? `init failed: ${stderr}`
|
|
124
|
+
: `init failed with exit code ${result.exitCode}`
|
|
125
|
+
throw new Error(msg)
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
error(`Failed to run openspec init: ${err.message}`)
|
|
129
|
+
return { initialized: false }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
success("OpenSpec initialized")
|
|
133
|
+
return { initialized: true }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
import { execa } from 'execa'
|
|
2
|
-
import fse from 'fs-extra'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
|
|
5
|
-
import { header, success, warn, error, loading } from '../../utils/exec.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Normalizes codegraph's MCP entry in the project-level OpenCode config.
|
|
9
|
-
* Older codegraph versions write `mcpServers`; OpenCode expects `mcp`.
|
|
10
|
-
* Returns true if the config was successfully normalized (or no config exists).
|
|
11
|
-
*/
|
|
12
|
-
export async function fixCodegraphConfig() {
|
|
13
|
-
const cwd = process.cwd()
|
|
14
|
-
const configFile = path.join(cwd, 'opencode.jsonc')
|
|
15
|
-
|
|
16
|
-
if (!await fse.pathExists(configFile)) return true
|
|
17
|
-
|
|
18
|
-
let configText
|
|
19
|
-
let config
|
|
20
|
-
try {
|
|
21
|
-
configText = await fse.readFile(configFile, 'utf-8')
|
|
22
|
-
const errors = []
|
|
23
|
-
config = parseJsonc(configText, errors)
|
|
24
|
-
if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
|
|
25
|
-
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
26
|
-
throw new Error('unexpected structure')
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
warn('Could not parse opencode.jsonc, leaving it untouched')
|
|
30
|
-
return false
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const legacyMcp = config.mcpServers
|
|
34
|
-
const mcp = config.mcp || {}
|
|
35
|
-
const entries = { ...legacyMcp, ...mcp }
|
|
36
|
-
let changed = false
|
|
37
|
-
|
|
38
|
-
if (Object.keys(entries).length > 0) {
|
|
39
|
-
for (const entry of Object.values(entries)) {
|
|
40
|
-
if (Array.isArray(entry.command) && entry.command[0] === 'codegraph') {
|
|
41
|
-
entry.command = ['npx', '@colbymchenry/codegraph', ...entry.command.slice(1)]
|
|
42
|
-
// codegraph defaults to a 30s MCP timeout, which fails under parallel
|
|
43
|
-
// subagent load. Raise it unless the user has set one explicitly.
|
|
44
|
-
if (entry.timeout == null) entry.timeout = 120000
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
for (const [name, entry] of Object.entries(entries)) {
|
|
48
|
-
const edits = modify(configText, ['mcp', name], entry, {
|
|
49
|
-
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
50
|
-
})
|
|
51
|
-
configText = applyEdits(configText, edits)
|
|
52
|
-
changed = true
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (legacyMcp) {
|
|
57
|
-
configText = applyEdits(configText, modify(configText, ['mcpServers'], undefined, {
|
|
58
|
-
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
59
|
-
}))
|
|
60
|
-
changed = true
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (changed) {
|
|
64
|
-
await fse.writeFile(configFile, configText, 'utf-8')
|
|
65
|
-
warn('Normalized codegraph MCP config in opencode.jsonc')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return true
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function installCodegraph(options = {}) {
|
|
72
|
-
if (!options.skipHeader) header('Installing codegraph')
|
|
73
|
-
|
|
74
|
-
const location = options.installScope === 'global' ? 'global' : 'local'
|
|
75
|
-
|
|
76
|
-
loading(`configuring codegraph for opencode (${location})...`)
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const installResult = await execa(
|
|
80
|
-
'npx',
|
|
81
|
-
['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
82
|
-
{
|
|
83
|
-
cwd: process.cwd(),
|
|
84
|
-
reject: false,
|
|
85
|
-
stdio: 'pipe',
|
|
86
|
-
}
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
if (installResult.exitCode !== 0) {
|
|
90
|
-
warn('codegraph install exited with non-zero code')
|
|
91
|
-
return { optedIn: true, installed: false }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const configFixed = await fixCodegraphConfig()
|
|
95
|
-
|
|
96
|
-
if (!configFixed) {
|
|
97
|
-
warn('codegraph config could not be merged: skipping init')
|
|
98
|
-
return { optedIn: true, installed: false }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
success(`codegraph configured for opencode (${location})`)
|
|
102
|
-
} catch (err) {
|
|
103
|
-
error(`Failed to install codegraph: ${err.message}`)
|
|
104
|
-
return { optedIn: true, installed: false }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
loading('initializing codegraph project index...')
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
|
|
111
|
-
cwd: process.cwd(),
|
|
112
|
-
reject: false,
|
|
113
|
-
stdio: 'pipe',
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
if (initResult.exitCode !== 0) {
|
|
117
|
-
warn('codegraph init exited with non-zero code')
|
|
118
|
-
return { optedIn: true, installed: false }
|
|
119
|
-
}
|
|
120
|
-
success('codegraph project index initialized')
|
|
121
|
-
} catch (err) {
|
|
122
|
-
error(`Failed to initialize codegraph: ${err.message}`)
|
|
123
|
-
return { optedIn: true, installed: false }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return { optedIn: true, installed: true }
|
|
127
|
-
}
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import fse from 'fs-extra'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser'
|
|
5
|
+
import { header, success, warn, error, loading } from '../../utils/exec.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Normalizes codegraph's MCP entry in the project-level OpenCode config.
|
|
9
|
+
* Older codegraph versions write `mcpServers`; OpenCode expects `mcp`.
|
|
10
|
+
* Returns true if the config was successfully normalized (or no config exists).
|
|
11
|
+
*/
|
|
12
|
+
export async function fixCodegraphConfig() {
|
|
13
|
+
const cwd = process.cwd()
|
|
14
|
+
const configFile = path.join(cwd, 'opencode.jsonc')
|
|
15
|
+
|
|
16
|
+
if (!await fse.pathExists(configFile)) return true
|
|
17
|
+
|
|
18
|
+
let configText
|
|
19
|
+
let config
|
|
20
|
+
try {
|
|
21
|
+
configText = await fse.readFile(configFile, 'utf-8')
|
|
22
|
+
const errors = []
|
|
23
|
+
config = parseJsonc(configText, errors)
|
|
24
|
+
if (errors.length > 0) throw new Error(`parse errors: ${errors.length}`)
|
|
25
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
26
|
+
throw new Error('unexpected structure')
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
warn('Could not parse opencode.jsonc, leaving it untouched')
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const legacyMcp = config.mcpServers
|
|
34
|
+
const mcp = config.mcp || {}
|
|
35
|
+
const entries = { ...legacyMcp, ...mcp }
|
|
36
|
+
let changed = false
|
|
37
|
+
|
|
38
|
+
if (Object.keys(entries).length > 0) {
|
|
39
|
+
for (const entry of Object.values(entries)) {
|
|
40
|
+
if (Array.isArray(entry.command) && entry.command[0] === 'codegraph') {
|
|
41
|
+
entry.command = ['npx', '@colbymchenry/codegraph', ...entry.command.slice(1)]
|
|
42
|
+
// codegraph defaults to a 30s MCP timeout, which fails under parallel
|
|
43
|
+
// subagent load. Raise it unless the user has set one explicitly.
|
|
44
|
+
if (entry.timeout == null) entry.timeout = 120000
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
48
|
+
const edits = modify(configText, ['mcp', name], entry, {
|
|
49
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
50
|
+
})
|
|
51
|
+
configText = applyEdits(configText, edits)
|
|
52
|
+
changed = true
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (legacyMcp) {
|
|
57
|
+
configText = applyEdits(configText, modify(configText, ['mcpServers'], undefined, {
|
|
58
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
59
|
+
}))
|
|
60
|
+
changed = true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (changed) {
|
|
64
|
+
await fse.writeFile(configFile, configText, 'utf-8')
|
|
65
|
+
warn('Normalized codegraph MCP config in opencode.jsonc')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function installCodegraph(options = {}) {
|
|
72
|
+
if (!options.skipHeader) header('Installing codegraph')
|
|
73
|
+
|
|
74
|
+
const location = options.installScope === 'global' ? 'global' : 'local'
|
|
75
|
+
|
|
76
|
+
loading(`configuring codegraph for opencode (${location})...`)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const installResult = await execa(
|
|
80
|
+
'npx',
|
|
81
|
+
['--yes', '@colbymchenry/codegraph', 'install', '--target=opencode', `--location=${location}`, '--yes'],
|
|
82
|
+
{
|
|
83
|
+
cwd: process.cwd(),
|
|
84
|
+
reject: false,
|
|
85
|
+
stdio: 'pipe',
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (installResult.exitCode !== 0) {
|
|
90
|
+
warn('codegraph install exited with non-zero code')
|
|
91
|
+
return { optedIn: true, installed: false }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const configFixed = await fixCodegraphConfig()
|
|
95
|
+
|
|
96
|
+
if (!configFixed) {
|
|
97
|
+
warn('codegraph config could not be merged: skipping init')
|
|
98
|
+
return { optedIn: true, installed: false }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
success(`codegraph configured for opencode (${location})`)
|
|
102
|
+
} catch (err) {
|
|
103
|
+
error(`Failed to install codegraph: ${err.message}`)
|
|
104
|
+
return { optedIn: true, installed: false }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
loading('initializing codegraph project index...')
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const initResult = await execa('npx', ['@colbymchenry/codegraph', 'init', '-i'], {
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
reject: false,
|
|
113
|
+
stdio: 'pipe',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (initResult.exitCode !== 0) {
|
|
117
|
+
warn('codegraph init exited with non-zero code')
|
|
118
|
+
return { optedIn: true, installed: false }
|
|
119
|
+
}
|
|
120
|
+
success('codegraph project index initialized')
|
|
121
|
+
} catch (err) {
|
|
122
|
+
error(`Failed to initialize codegraph: ${err.message}`)
|
|
123
|
+
return { optedIn: true, installed: false }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { optedIn: true, installed: true }
|
|
127
|
+
}
|