@duypham93/openkit 0.2.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/.opencode/README.md +47 -0
- package/.opencode/install-manifest.json +41 -0
- package/.opencode/lib/artifact-scaffolder.js +111 -0
- package/.opencode/lib/contract-consistency.js +218 -0
- package/.opencode/lib/parallel-execution-rules.js +261 -0
- package/.opencode/lib/runtime-paths.js +95 -0
- package/.opencode/lib/runtime-summary.js +82 -0
- package/.opencode/lib/state-guard.js +99 -0
- package/.opencode/lib/task-board-rules.js +375 -0
- package/.opencode/lib/work-item-store.js +280 -0
- package/.opencode/lib/workflow-state-controller.js +1739 -0
- package/.opencode/lib/workflow-state-rules.js +331 -0
- package/.opencode/opencode.json +93 -0
- package/.opencode/package.json +3 -0
- package/.opencode/tests/artifact-scaffolder.test.js +733 -0
- package/.opencode/tests/multi-work-item-runtime.test.js +369 -0
- package/.opencode/tests/parallel-execution-runtime.test.js +259 -0
- package/.opencode/tests/session-start-hook.test.js +357 -0
- package/.opencode/tests/state-guard.test.js +124 -0
- package/.opencode/tests/task-board-rules.test.js +204 -0
- package/.opencode/tests/work-item-store.test.js +380 -0
- package/.opencode/tests/workflow-behavior.test.js +149 -0
- package/.opencode/tests/workflow-contract-consistency.test.js +387 -0
- package/.opencode/tests/workflow-state-cli.test.js +1275 -0
- package/.opencode/tests/workflow-state-controller.test.js +1038 -0
- package/.opencode/work-items/feature-001/state.json +70 -0
- package/.opencode/work-items/index.json +13 -0
- package/.opencode/workflow-state.js +489 -0
- package/.opencode/workflow-state.json +70 -0
- package/AGENTS.md +265 -0
- package/README.md +401 -0
- package/agents/architect-agent.md +63 -0
- package/agents/ba-agent.md +56 -0
- package/agents/code-reviewer.md +77 -0
- package/agents/fullstack-agent.md +115 -0
- package/agents/master-orchestrator.md +60 -0
- package/agents/pm-agent.md +56 -0
- package/agents/qa-agent.md +124 -0
- package/agents/tech-lead-agent.md +60 -0
- package/assets/install-bundle/README.md +7 -0
- package/assets/install-bundle/opencode/README.md +11 -0
- package/assets/install-bundle/opencode/agents/ArchitectAgent.md +63 -0
- package/assets/install-bundle/opencode/agents/BAAgent.md +56 -0
- package/assets/install-bundle/opencode/agents/CodeReviewer.md +77 -0
- package/assets/install-bundle/opencode/agents/FullstackAgent.md +115 -0
- package/assets/install-bundle/opencode/agents/MasterOrchestrator.md +60 -0
- package/assets/install-bundle/opencode/agents/PMAgent.md +56 -0
- package/assets/install-bundle/opencode/agents/QAAgent.md +124 -0
- package/assets/install-bundle/opencode/agents/TechLeadAgent.md +60 -0
- package/assets/install-bundle/opencode/commands/brainstorm.md +44 -0
- package/assets/install-bundle/opencode/commands/delivery.md +45 -0
- package/assets/install-bundle/opencode/commands/execute-plan.md +44 -0
- package/assets/install-bundle/opencode/commands/migrate.md +61 -0
- package/assets/install-bundle/opencode/commands/quick-task.md +45 -0
- package/assets/install-bundle/opencode/commands/task.md +46 -0
- package/assets/install-bundle/opencode/commands/write-plan.md +50 -0
- package/assets/install-bundle/opencode/context/core/lane-selection.md +54 -0
- package/assets/install-bundle/opencode/skills/brainstorming/SKILL.md +51 -0
- package/assets/install-bundle/opencode/skills/code-review/SKILL.md +48 -0
- package/assets/install-bundle/opencode/skills/subagent-driven-development/SKILL.md +79 -0
- package/assets/install-bundle/opencode/skills/systematic-debugging/SKILL.md +61 -0
- package/assets/install-bundle/opencode/skills/test-driven-development/SKILL.md +48 -0
- package/assets/install-bundle/opencode/skills/using-skills/SKILL.md +39 -0
- package/assets/install-bundle/opencode/skills/verification-before-completion/SKILL.md +137 -0
- package/assets/install-bundle/opencode/skills/writing-plans/SKILL.md +68 -0
- package/assets/install-bundle/opencode/skills/writing-specs/SKILL.md +47 -0
- package/assets/opencode.json.template +11 -0
- package/assets/openkit-install.json.template +19 -0
- package/bin/openkit.js +9 -0
- package/commands/brainstorm.md +44 -0
- package/commands/delivery.md +45 -0
- package/commands/execute-plan.md +44 -0
- package/commands/migrate.md +61 -0
- package/commands/quick-task.md +45 -0
- package/commands/task.md +46 -0
- package/commands/write-plan.md +50 -0
- package/context/core/approval-gates.md +146 -0
- package/context/core/code-quality.md +42 -0
- package/context/core/issue-routing.md +85 -0
- package/context/core/lane-selection.md +54 -0
- package/context/core/project-config.md +143 -0
- package/context/core/session-resume.md +85 -0
- package/context/core/workflow-state-schema.md +224 -0
- package/context/core/workflow.md +442 -0
- package/context/navigation.md +94 -0
- package/docs/adr/README.md +6 -0
- package/docs/architecture/2026-03-20-task-intake-dashboard.md +54 -0
- package/docs/architecture/README.md +7 -0
- package/docs/briefs/2026-03-20-task-intake-dashboard.md +48 -0
- package/docs/briefs/README.md +7 -0
- package/docs/governance/README.md +25 -0
- package/docs/governance/adr-policy.md +27 -0
- package/docs/governance/definition-of-done.md +17 -0
- package/docs/governance/naming-conventions.md +21 -0
- package/docs/governance/severity-levels.md +12 -0
- package/docs/maintainer/README.md +51 -0
- package/docs/operations/README.md +79 -0
- package/docs/operations/internal-records/2026-03-24-release-checklist.md +79 -0
- package/docs/operations/internal-records/2026-03-24-simplified-install-ux.md +36 -0
- package/docs/operations/internal-records/README.md +18 -0
- package/docs/operations/runbooks/README.md +23 -0
- package/docs/operations/runbooks/openkit-daily-usage.md +288 -0
- package/docs/operations/runbooks/workflow-state-smoke-tests.md +302 -0
- package/docs/operator/README.md +50 -0
- package/docs/plans/2026-03-20-task-intake-dashboard.md +49 -0
- package/docs/plans/2026-03-21-openkit-full-delivery-multi-task-runtime.md +521 -0
- package/docs/plans/2026-03-23-openkit-global-install-runtime.md +157 -0
- package/docs/plans/README.md +7 -0
- package/docs/qa/2026-03-20-task-intake-dashboard.md +41 -0
- package/docs/qa/README.md +7 -0
- package/docs/specs/2026-03-20-task-intake-dashboard.md +50 -0
- package/docs/specs/2026-03-21-openkit-full-delivery-multi-task-runtime.md +462 -0
- package/docs/specs/README.md +7 -0
- package/docs/templates/README.md +36 -0
- package/docs/templates/adr-template.md +18 -0
- package/docs/templates/architecture-template.md +31 -0
- package/docs/templates/implementation-plan-template.md +32 -0
- package/docs/templates/migration-baseline-checklist.md +48 -0
- package/docs/templates/migration-plan-template.md +52 -0
- package/docs/templates/migration-report-template.md +74 -0
- package/docs/templates/migration-verify-checklist.md +39 -0
- package/docs/templates/product-brief-template.md +32 -0
- package/docs/templates/qa-report-template.md +37 -0
- package/docs/templates/quick-task-template.md +36 -0
- package/docs/templates/spec-template.md +31 -0
- package/hooks/hooks.json +16 -0
- package/hooks/session-start +162 -0
- package/package.json +24 -0
- package/registry.json +328 -0
- package/skills/brainstorming/SKILL.md +51 -0
- package/skills/code-review/SKILL.md +48 -0
- package/skills/subagent-driven-development/SKILL.md +79 -0
- package/skills/systematic-debugging/SKILL.md +61 -0
- package/skills/test-driven-development/SKILL.md +48 -0
- package/skills/using-skills/SKILL.md +39 -0
- package/skills/verification-before-completion/SKILL.md +137 -0
- package/skills/writing-plans/SKILL.md +68 -0
- package/skills/writing-specs/SKILL.md +47 -0
- package/src/audit/vietnamese-detection.js +259 -0
- package/src/cli/commands/detect-vietnamese.js +24 -0
- package/src/cli/commands/doctor.js +33 -0
- package/src/cli/commands/help.js +33 -0
- package/src/cli/commands/init.js +25 -0
- package/src/cli/commands/install-global.js +26 -0
- package/src/cli/commands/install.js +25 -0
- package/src/cli/commands/run.js +63 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/upgrade.js +25 -0
- package/src/cli/conflict-output.js +19 -0
- package/src/cli/index.js +56 -0
- package/src/global/doctor.js +101 -0
- package/src/global/ensure-install.js +32 -0
- package/src/global/install-state.js +73 -0
- package/src/global/launcher.js +51 -0
- package/src/global/materialize.js +123 -0
- package/src/global/paths.js +85 -0
- package/src/global/uninstall.js +25 -0
- package/src/global/workspace-state.js +63 -0
- package/src/install/asset-manifest.js +284 -0
- package/src/install/conflicts.js +43 -0
- package/src/install/discovery.js +138 -0
- package/src/install/install-state.js +136 -0
- package/src/install/materialize.js +158 -0
- package/src/install/merge-policy.js +145 -0
- package/src/runtime/doctor.js +281 -0
- package/src/runtime/launcher.js +49 -0
- package/src/runtime/opencode-layering.js +135 -0
- package/src/runtime/openkit-managed-summary.js +27 -0
- package/tests/cli/openkit-cli.test.js +417 -0
- package/tests/global/doctor.test.js +130 -0
- package/tests/global/ensure-install.test.js +105 -0
- package/tests/install/discovery.test.js +124 -0
- package/tests/install/install-state.test.js +346 -0
- package/tests/install/materialize.test.js +244 -0
- package/tests/install/merge-policy.test.js +177 -0
- package/tests/runtime/doctor.test.js +430 -0
- package/tests/runtime/launcher.test.js +230 -0
- package/tests/runtime/module-boundary.test.js +16 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
import { createInstallState } from "./install-state.js"
|
|
6
|
+
import { applyOpenKitMergePolicy } from "./merge-policy.js"
|
|
7
|
+
import { createMaterializationConflict, qualifyMergeConflicts } from "./conflicts.js"
|
|
8
|
+
|
|
9
|
+
const ROOT_MANIFEST_ASSET_ID = "runtime.opencode-manifest"
|
|
10
|
+
const ROOT_MANIFEST_PATH = "opencode.json"
|
|
11
|
+
const INSTALL_STATE_ASSET_ID = "runtime.install-state"
|
|
12
|
+
const INSTALL_STATE_PATH = ".openkit/openkit-install.json"
|
|
13
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const BUNDLE_ROOT = path.resolve(MODULE_DIR, "../..")
|
|
15
|
+
|
|
16
|
+
function readJson(filePath) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readExistingJson(filePath) {
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return readJson(filePath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readTemplate(relativePath) {
|
|
29
|
+
return readJson(path.join(BUNDLE_ROOT, relativePath))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeJson(filePath, value) {
|
|
33
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
34
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeFileIfPresent(filePath) {
|
|
38
|
+
if (fs.existsSync(filePath)) {
|
|
39
|
+
fs.unlinkSync(filePath)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function materializeInstall(projectRoot, { kitVersion = "0.1.0", now } = {}) {
|
|
44
|
+
const desiredRootManifest = readTemplate("assets/opencode.json.template")
|
|
45
|
+
const rootManifestPath = path.join(projectRoot, ROOT_MANIFEST_PATH)
|
|
46
|
+
const installStatePath = path.join(projectRoot, INSTALL_STATE_PATH)
|
|
47
|
+
const existingRootManifest = readExistingJson(rootManifestPath)
|
|
48
|
+
const existingInstallState = readExistingJson(installStatePath)
|
|
49
|
+
|
|
50
|
+
let rootManifestToWrite = desiredRootManifest
|
|
51
|
+
let rootManifestManagedStatus = "materialized"
|
|
52
|
+
let adoptedAssets = []
|
|
53
|
+
const warnings = []
|
|
54
|
+
let conflicts = []
|
|
55
|
+
|
|
56
|
+
if (existingRootManifest !== null) {
|
|
57
|
+
const mergeResult = applyOpenKitMergePolicy({
|
|
58
|
+
currentConfig: existingRootManifest,
|
|
59
|
+
desiredConfig: desiredRootManifest,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
conflicts = qualifyMergeConflicts(mergeResult.conflicts, ROOT_MANIFEST_ASSET_ID, ROOT_MANIFEST_PATH)
|
|
63
|
+
|
|
64
|
+
if (conflicts.length === 0) {
|
|
65
|
+
rootManifestToWrite = mergeResult.config
|
|
66
|
+
} else {
|
|
67
|
+
rootManifestToWrite = existingRootManifest
|
|
68
|
+
rootManifestManagedStatus = null
|
|
69
|
+
adoptedAssets = [
|
|
70
|
+
{
|
|
71
|
+
assetId: ROOT_MANIFEST_ASSET_ID,
|
|
72
|
+
path: ROOT_MANIFEST_PATH,
|
|
73
|
+
adoptedFrom: "user-existing",
|
|
74
|
+
status: "adopted",
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hasExistingInstallState = existingInstallState !== null
|
|
81
|
+
|
|
82
|
+
if (hasExistingInstallState) {
|
|
83
|
+
conflicts.push(
|
|
84
|
+
createMaterializationConflict({
|
|
85
|
+
assetId: INSTALL_STATE_ASSET_ID,
|
|
86
|
+
path: INSTALL_STATE_PATH,
|
|
87
|
+
reason: "existing-managed-asset",
|
|
88
|
+
resolution: "manual-review-required",
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasExistingInstallState) {
|
|
94
|
+
return {
|
|
95
|
+
rootManifestPath,
|
|
96
|
+
installStatePath,
|
|
97
|
+
managedAssets: [],
|
|
98
|
+
adoptedAssets,
|
|
99
|
+
warnings,
|
|
100
|
+
conflicts,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (conflicts.length > 0) {
|
|
105
|
+
return {
|
|
106
|
+
rootManifestPath,
|
|
107
|
+
installStatePath,
|
|
108
|
+
managedAssets: [],
|
|
109
|
+
adoptedAssets,
|
|
110
|
+
warnings,
|
|
111
|
+
conflicts,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const managedAssets = []
|
|
116
|
+
|
|
117
|
+
if (rootManifestManagedStatus !== null) {
|
|
118
|
+
managedAssets.push({
|
|
119
|
+
assetId: ROOT_MANIFEST_ASSET_ID,
|
|
120
|
+
path: ROOT_MANIFEST_PATH,
|
|
121
|
+
status: rootManifestManagedStatus,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
managedAssets.push({
|
|
126
|
+
assetId: INSTALL_STATE_ASSET_ID,
|
|
127
|
+
path: INSTALL_STATE_PATH,
|
|
128
|
+
status: "managed",
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const installState = createInstallState({
|
|
132
|
+
kitVersion,
|
|
133
|
+
profile: "openkit-core",
|
|
134
|
+
managedAssets,
|
|
135
|
+
adoptedAssets,
|
|
136
|
+
warnings,
|
|
137
|
+
conflicts,
|
|
138
|
+
now,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
writeJson(installStatePath, installState)
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
writeJson(rootManifestPath, rootManifestToWrite)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
removeFileIfPresent(installStatePath)
|
|
147
|
+
throw error
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
rootManifestPath,
|
|
152
|
+
installStatePath,
|
|
153
|
+
managedAssets,
|
|
154
|
+
adoptedAssets,
|
|
155
|
+
warnings,
|
|
156
|
+
conflicts,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const OPENKIT_MERGE_ALLOWLIST = new Set([
|
|
2
|
+
"plugin",
|
|
3
|
+
"instructions",
|
|
4
|
+
"installState",
|
|
5
|
+
"productSurface",
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function cloneValue(value) {
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return [...value]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (isPlainObject(value)) {
|
|
18
|
+
return { ...value }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return value
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mergeUniqueArray(currentValue, desiredValue) {
|
|
25
|
+
const currentItems = Array.isArray(currentValue) ? currentValue : []
|
|
26
|
+
const desiredItems = Array.isArray(desiredValue) ? desiredValue : []
|
|
27
|
+
const merged = [...currentItems]
|
|
28
|
+
|
|
29
|
+
for (const item of desiredItems) {
|
|
30
|
+
if (!merged.some((existing) => Object.is(existing, item))) {
|
|
31
|
+
merged.push(item)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return merged
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function valuesMatch(currentValue, desiredValue) {
|
|
39
|
+
if (Object.is(currentValue, desiredValue)) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (Array.isArray(currentValue) || Array.isArray(desiredValue)) {
|
|
44
|
+
if (!Array.isArray(currentValue) || !Array.isArray(desiredValue)) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentValue.length !== desiredValue.length) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return currentValue.every((item, index) => valuesMatch(item, desiredValue[index]))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (isPlainObject(currentValue) || isPlainObject(desiredValue)) {
|
|
56
|
+
if (!isPlainObject(currentValue) || !isPlainObject(desiredValue)) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const currentKeys = Object.keys(currentValue).sort()
|
|
61
|
+
const desiredKeys = Object.keys(desiredValue).sort()
|
|
62
|
+
|
|
63
|
+
if (!valuesMatch(currentKeys, desiredKeys)) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return currentKeys.every((key) => valuesMatch(currentValue[key], desiredValue[key]))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function applyOpenKitMergePolicy({ currentConfig = {}, desiredConfig = {} }) {
|
|
74
|
+
const config = {}
|
|
75
|
+
const conflicts = []
|
|
76
|
+
const appliedFields = []
|
|
77
|
+
const keys = new Set([...Object.keys(currentConfig), ...Object.keys(desiredConfig)])
|
|
78
|
+
|
|
79
|
+
for (const key of keys) {
|
|
80
|
+
const hasCurrent = Object.hasOwn(currentConfig, key)
|
|
81
|
+
const hasDesired = Object.hasOwn(desiredConfig, key)
|
|
82
|
+
|
|
83
|
+
if (!hasDesired) {
|
|
84
|
+
config[key] = cloneValue(currentConfig[key])
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (OPENKIT_MERGE_ALLOWLIST.has(key)) {
|
|
89
|
+
if (Array.isArray(desiredConfig[key])) {
|
|
90
|
+
config[key] = mergeUniqueArray(currentConfig[key], desiredConfig[key])
|
|
91
|
+
appliedFields.push(key)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasCurrent) {
|
|
96
|
+
config[key] = cloneValue(desiredConfig[key])
|
|
97
|
+
appliedFields.push(key)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
config[key] = cloneValue(currentConfig[key])
|
|
102
|
+
|
|
103
|
+
if (!valuesMatch(currentConfig[key], desiredConfig[key])) {
|
|
104
|
+
conflicts.push({
|
|
105
|
+
field: key,
|
|
106
|
+
reason: "unsupported-top-level-key",
|
|
107
|
+
currentValue: cloneValue(currentConfig[key]),
|
|
108
|
+
desiredValue: cloneValue(desiredConfig[key]),
|
|
109
|
+
})
|
|
110
|
+
} else {
|
|
111
|
+
appliedFields.push(key)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!hasCurrent) {
|
|
118
|
+
conflicts.push({
|
|
119
|
+
field: key,
|
|
120
|
+
reason: "unclassified-top-level-key",
|
|
121
|
+
currentValue: undefined,
|
|
122
|
+
desiredValue: cloneValue(desiredConfig[key]),
|
|
123
|
+
})
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
config[key] = cloneValue(currentConfig[key])
|
|
128
|
+
|
|
129
|
+
if (!Object.is(currentConfig[key], desiredConfig[key])) {
|
|
130
|
+
conflicts.push({
|
|
131
|
+
field: key,
|
|
132
|
+
reason: "unsupported-top-level-key",
|
|
133
|
+
currentValue: cloneValue(currentConfig[key]),
|
|
134
|
+
desiredValue: cloneValue(desiredConfig[key]),
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
config,
|
|
141
|
+
conflicts,
|
|
142
|
+
appliedFields,
|
|
143
|
+
mergeAllowlist: [...OPENKIT_MERGE_ALLOWLIST],
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { validateInstallState } from '../install/install-state.js';
|
|
5
|
+
import { discoverProjectShape } from '../install/discovery.js';
|
|
6
|
+
|
|
7
|
+
const EXPECTED_MANAGED_ASSETS = {
|
|
8
|
+
'runtime.opencode-manifest': {
|
|
9
|
+
path: 'opencode.json',
|
|
10
|
+
validate(contents) {
|
|
11
|
+
return (
|
|
12
|
+
contents?.installState?.path === '.openkit/openkit-install.json' &&
|
|
13
|
+
contents?.installState?.schema === 'openkit/install-state@1' &&
|
|
14
|
+
contents?.productSurface?.current === 'global-openkit-install' &&
|
|
15
|
+
contents?.productSurface?.installReadiness === 'managed' &&
|
|
16
|
+
contents?.productSurface?.installationMode === 'openkit-managed'
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
'runtime.install-state': {
|
|
21
|
+
path: '.openkit/openkit-install.json',
|
|
22
|
+
validate(contents) {
|
|
23
|
+
return contents?.installation?.profile === 'openkit-core';
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ROOT_MANIFEST_ASSET_ID = 'runtime.opencode-manifest';
|
|
29
|
+
|
|
30
|
+
function readJsonIfPresent(filePath) {
|
|
31
|
+
if (!fs.existsSync(filePath)) {
|
|
32
|
+
return {
|
|
33
|
+
exists: false,
|
|
34
|
+
malformed: false,
|
|
35
|
+
value: null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
return {
|
|
41
|
+
exists: true,
|
|
42
|
+
malformed: false,
|
|
43
|
+
value: JSON.parse(fs.readFileSync(filePath, 'utf8')),
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return {
|
|
47
|
+
exists: true,
|
|
48
|
+
malformed: true,
|
|
49
|
+
value: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function defaultIsOpenCodeAvailable() {
|
|
55
|
+
const pathValue = process.env.PATH ?? '';
|
|
56
|
+
return pathValue.split(path.delimiter).some((segment) => {
|
|
57
|
+
if (!segment) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const candidate = path.join(segment, 'opencode');
|
|
62
|
+
return fs.existsSync(candidate);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function inspectManagedDoctor({
|
|
67
|
+
projectRoot,
|
|
68
|
+
env = process.env,
|
|
69
|
+
isOpenCodeAvailable = defaultIsOpenCodeAvailable,
|
|
70
|
+
} = {}) {
|
|
71
|
+
const resolvedProjectRoot = projectRoot ?? process.cwd();
|
|
72
|
+
const rootManifestPath = path.join(resolvedProjectRoot, 'opencode.json');
|
|
73
|
+
const runtimeManifestPath = path.join(resolvedProjectRoot, '.opencode', 'opencode.json');
|
|
74
|
+
const installStatePath = path.join(resolvedProjectRoot, '.openkit', 'openkit-install.json');
|
|
75
|
+
const rootManifestResult = readJsonIfPresent(rootManifestPath);
|
|
76
|
+
const installStateResult = readJsonIfPresent(installStatePath);
|
|
77
|
+
const rootManifest = rootManifestResult.value;
|
|
78
|
+
const installState = installStateResult.value;
|
|
79
|
+
const issues = [];
|
|
80
|
+
const driftedAssets = [];
|
|
81
|
+
const ownedAssets = {
|
|
82
|
+
managed: [],
|
|
83
|
+
adopted: [],
|
|
84
|
+
};
|
|
85
|
+
const classification = discoverProjectShape(resolvedProjectRoot).classification;
|
|
86
|
+
|
|
87
|
+
if (!rootManifestResult.exists && !installStateResult.exists) {
|
|
88
|
+
return {
|
|
89
|
+
status: 'install-missing',
|
|
90
|
+
canRunCleanly: false,
|
|
91
|
+
summary: 'Managed install was not found; openkit run cannot proceed cleanly.',
|
|
92
|
+
issues: ['Managed install entrypoint was not found at opencode.json.'],
|
|
93
|
+
driftedAssets,
|
|
94
|
+
ownedAssets,
|
|
95
|
+
classification,
|
|
96
|
+
rootManifestPath,
|
|
97
|
+
runtimeManifestPath,
|
|
98
|
+
installStatePath,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!rootManifestResult.exists || !installStateResult.exists) {
|
|
103
|
+
if (installState) {
|
|
104
|
+
const installStateErrors = validateInstallState(installState);
|
|
105
|
+
if (installStateErrors.length === 0) {
|
|
106
|
+
ownedAssets.managed = (installState.assets?.managed ?? []).map((asset) => asset.path);
|
|
107
|
+
ownedAssets.adopted = (installState.assets?.adopted ?? []).map((asset) => asset.path);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const missingAssets = [];
|
|
112
|
+
if (!rootManifestResult.exists) {
|
|
113
|
+
missingAssets.push('Missing required managed asset: opencode.json');
|
|
114
|
+
}
|
|
115
|
+
if (!installStateResult.exists) {
|
|
116
|
+
missingAssets.push('Missing required managed asset: .openkit/openkit-install.json');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
status: 'install-incomplete',
|
|
121
|
+
canRunCleanly: false,
|
|
122
|
+
summary: 'Managed install is incomplete; openkit run cannot proceed cleanly.',
|
|
123
|
+
issues: missingAssets,
|
|
124
|
+
driftedAssets,
|
|
125
|
+
ownedAssets,
|
|
126
|
+
classification,
|
|
127
|
+
rootManifestPath,
|
|
128
|
+
runtimeManifestPath,
|
|
129
|
+
installStatePath,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (rootManifestResult.malformed || installStateResult.malformed) {
|
|
134
|
+
if (installState && !installStateResult.malformed) {
|
|
135
|
+
const installStateErrors = validateInstallState(installState);
|
|
136
|
+
if (installStateErrors.length === 0) {
|
|
137
|
+
ownedAssets.managed = (installState.assets?.managed ?? []).map((asset) => asset.path);
|
|
138
|
+
ownedAssets.adopted = (installState.assets?.adopted ?? []).map((asset) => asset.path);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (rootManifestResult.malformed) {
|
|
143
|
+
driftedAssets.push('opencode.json');
|
|
144
|
+
issues.push('Managed asset JSON is malformed: opencode.json');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (installStateResult.malformed) {
|
|
148
|
+
driftedAssets.push('.openkit/openkit-install.json');
|
|
149
|
+
issues.push('Managed asset JSON is malformed: .openkit/openkit-install.json');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
status: 'drift-detected',
|
|
154
|
+
canRunCleanly: false,
|
|
155
|
+
summary: 'Managed asset drift was detected; review managed install files before running OpenKit.',
|
|
156
|
+
issues,
|
|
157
|
+
driftedAssets,
|
|
158
|
+
ownedAssets,
|
|
159
|
+
classification,
|
|
160
|
+
rootManifestPath,
|
|
161
|
+
runtimeManifestPath,
|
|
162
|
+
installStatePath,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const installStateErrors = validateInstallState(installState);
|
|
167
|
+
if (installStateErrors.length > 0) {
|
|
168
|
+
return {
|
|
169
|
+
status: 'install-incomplete',
|
|
170
|
+
canRunCleanly: false,
|
|
171
|
+
summary: 'Managed install is incomplete; install state is invalid.',
|
|
172
|
+
issues: installStateErrors,
|
|
173
|
+
driftedAssets,
|
|
174
|
+
ownedAssets,
|
|
175
|
+
classification,
|
|
176
|
+
rootManifestPath,
|
|
177
|
+
runtimeManifestPath,
|
|
178
|
+
installStatePath,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ownedAssets.managed = (installState.assets?.managed ?? []).map((asset) => asset.path);
|
|
183
|
+
ownedAssets.adopted = (installState.assets?.adopted ?? []).map((asset) => asset.path);
|
|
184
|
+
|
|
185
|
+
const adoptedRootManifest = (installState.assets?.adopted ?? []).some(
|
|
186
|
+
(asset) => asset?.assetId === ROOT_MANIFEST_ASSET_ID && asset?.path === 'opencode.json'
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (adoptedRootManifest && !EXPECTED_MANAGED_ASSETS[ROOT_MANIFEST_ASSET_ID].validate(rootManifest)) {
|
|
190
|
+
return {
|
|
191
|
+
status: 'install-incomplete',
|
|
192
|
+
canRunCleanly: false,
|
|
193
|
+
summary: 'Managed install contract is incomplete; adopted root manifest is not compatible enough to run cleanly.',
|
|
194
|
+
issues: ['Adopted root manifest is incompatible with the managed install contract.'],
|
|
195
|
+
driftedAssets,
|
|
196
|
+
ownedAssets,
|
|
197
|
+
classification,
|
|
198
|
+
rootManifestPath,
|
|
199
|
+
runtimeManifestPath,
|
|
200
|
+
installStatePath,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const asset of installState.assets?.managed ?? []) {
|
|
205
|
+
const expectedAsset = EXPECTED_MANAGED_ASSETS[asset.assetId];
|
|
206
|
+
|
|
207
|
+
if (!expectedAsset) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const assetPath = path.join(resolvedProjectRoot, asset.path);
|
|
212
|
+
const contentsResult = readJsonIfPresent(assetPath);
|
|
213
|
+
const contents = contentsResult.value;
|
|
214
|
+
|
|
215
|
+
if (contentsResult.malformed) {
|
|
216
|
+
if (!driftedAssets.includes(asset.path)) {
|
|
217
|
+
driftedAssets.push(asset.path);
|
|
218
|
+
}
|
|
219
|
+
issues.push(`Managed asset JSON is malformed: ${asset.path}`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!contentsResult.exists || !expectedAsset.validate(contents)) {
|
|
224
|
+
if (!driftedAssets.includes(asset.path)) {
|
|
225
|
+
driftedAssets.push(asset.path);
|
|
226
|
+
}
|
|
227
|
+
issues.push(`Drift detected for managed asset: ${asset.path}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (driftedAssets.length > 0) {
|
|
232
|
+
return {
|
|
233
|
+
status: 'drift-detected',
|
|
234
|
+
canRunCleanly: false,
|
|
235
|
+
summary: 'Managed asset drift was detected; review managed install files before running OpenKit.',
|
|
236
|
+
issues,
|
|
237
|
+
driftedAssets,
|
|
238
|
+
ownedAssets,
|
|
239
|
+
classification,
|
|
240
|
+
rootManifestPath,
|
|
241
|
+
runtimeManifestPath,
|
|
242
|
+
installStatePath,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!fs.existsSync(runtimeManifestPath)) {
|
|
247
|
+
issues.push('Missing runtime manifest: .opencode/opencode.json');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!isOpenCodeAvailable(env)) {
|
|
251
|
+
issues.push('OpenCode executable is not available on PATH');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (issues.length > 0) {
|
|
255
|
+
return {
|
|
256
|
+
status: 'runtime-prerequisites-missing',
|
|
257
|
+
canRunCleanly: false,
|
|
258
|
+
summary: 'Runtime launch prerequisites are missing; openkit run cannot proceed cleanly.',
|
|
259
|
+
issues,
|
|
260
|
+
driftedAssets,
|
|
261
|
+
ownedAssets,
|
|
262
|
+
classification,
|
|
263
|
+
rootManifestPath,
|
|
264
|
+
runtimeManifestPath,
|
|
265
|
+
installStatePath,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
status: 'healthy',
|
|
271
|
+
canRunCleanly: true,
|
|
272
|
+
summary: 'Managed install is healthy; openkit run can proceed cleanly.',
|
|
273
|
+
issues: [],
|
|
274
|
+
driftedAssets,
|
|
275
|
+
ownedAssets,
|
|
276
|
+
classification,
|
|
277
|
+
rootManifestPath,
|
|
278
|
+
runtimeManifestPath,
|
|
279
|
+
installStatePath,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
import { buildOpenCodeLayering } from './opencode-layering.js';
|
|
4
|
+
|
|
5
|
+
function formatMissingOpenCodeError() {
|
|
6
|
+
return [
|
|
7
|
+
'Could not find `opencode` on your PATH.',
|
|
8
|
+
'The supported launcher path is `openkit run`, which applies managed config layering for this session.',
|
|
9
|
+
'Install OpenCode or add `opencode` to PATH, then retry.',
|
|
10
|
+
].join('\n');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function launchManagedOpenCode(
|
|
14
|
+
args = [],
|
|
15
|
+
{
|
|
16
|
+
projectRoot = process.cwd(),
|
|
17
|
+
env = process.env,
|
|
18
|
+
spawn = spawnSync,
|
|
19
|
+
stdio = 'inherit',
|
|
20
|
+
} = {}
|
|
21
|
+
) {
|
|
22
|
+
const layering = buildOpenCodeLayering({ projectRoot, env });
|
|
23
|
+
const result = spawn('opencode', args, {
|
|
24
|
+
cwd: projectRoot,
|
|
25
|
+
env: layering.env,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (result.error?.code === 'ENOENT') {
|
|
31
|
+
return {
|
|
32
|
+
exitCode: 1,
|
|
33
|
+
stdout: '',
|
|
34
|
+
stderr: `${formatMissingOpenCodeError()}\n`,
|
|
35
|
+
layering,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (result.error) {
|
|
40
|
+
throw result.error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
exitCode: typeof result.status === 'number' ? result.status : 1,
|
|
45
|
+
stdout: result.stdout ?? '',
|
|
46
|
+
stderr: result.stderr ?? '',
|
|
47
|
+
layering,
|
|
48
|
+
};
|
|
49
|
+
}
|