@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.
Files changed (178) hide show
  1. package/.opencode/README.md +47 -0
  2. package/.opencode/install-manifest.json +41 -0
  3. package/.opencode/lib/artifact-scaffolder.js +111 -0
  4. package/.opencode/lib/contract-consistency.js +218 -0
  5. package/.opencode/lib/parallel-execution-rules.js +261 -0
  6. package/.opencode/lib/runtime-paths.js +95 -0
  7. package/.opencode/lib/runtime-summary.js +82 -0
  8. package/.opencode/lib/state-guard.js +99 -0
  9. package/.opencode/lib/task-board-rules.js +375 -0
  10. package/.opencode/lib/work-item-store.js +280 -0
  11. package/.opencode/lib/workflow-state-controller.js +1739 -0
  12. package/.opencode/lib/workflow-state-rules.js +331 -0
  13. package/.opencode/opencode.json +93 -0
  14. package/.opencode/package.json +3 -0
  15. package/.opencode/tests/artifact-scaffolder.test.js +733 -0
  16. package/.opencode/tests/multi-work-item-runtime.test.js +369 -0
  17. package/.opencode/tests/parallel-execution-runtime.test.js +259 -0
  18. package/.opencode/tests/session-start-hook.test.js +357 -0
  19. package/.opencode/tests/state-guard.test.js +124 -0
  20. package/.opencode/tests/task-board-rules.test.js +204 -0
  21. package/.opencode/tests/work-item-store.test.js +380 -0
  22. package/.opencode/tests/workflow-behavior.test.js +149 -0
  23. package/.opencode/tests/workflow-contract-consistency.test.js +387 -0
  24. package/.opencode/tests/workflow-state-cli.test.js +1275 -0
  25. package/.opencode/tests/workflow-state-controller.test.js +1038 -0
  26. package/.opencode/work-items/feature-001/state.json +70 -0
  27. package/.opencode/work-items/index.json +13 -0
  28. package/.opencode/workflow-state.js +489 -0
  29. package/.opencode/workflow-state.json +70 -0
  30. package/AGENTS.md +265 -0
  31. package/README.md +401 -0
  32. package/agents/architect-agent.md +63 -0
  33. package/agents/ba-agent.md +56 -0
  34. package/agents/code-reviewer.md +77 -0
  35. package/agents/fullstack-agent.md +115 -0
  36. package/agents/master-orchestrator.md +60 -0
  37. package/agents/pm-agent.md +56 -0
  38. package/agents/qa-agent.md +124 -0
  39. package/agents/tech-lead-agent.md +60 -0
  40. package/assets/install-bundle/README.md +7 -0
  41. package/assets/install-bundle/opencode/README.md +11 -0
  42. package/assets/install-bundle/opencode/agents/ArchitectAgent.md +63 -0
  43. package/assets/install-bundle/opencode/agents/BAAgent.md +56 -0
  44. package/assets/install-bundle/opencode/agents/CodeReviewer.md +77 -0
  45. package/assets/install-bundle/opencode/agents/FullstackAgent.md +115 -0
  46. package/assets/install-bundle/opencode/agents/MasterOrchestrator.md +60 -0
  47. package/assets/install-bundle/opencode/agents/PMAgent.md +56 -0
  48. package/assets/install-bundle/opencode/agents/QAAgent.md +124 -0
  49. package/assets/install-bundle/opencode/agents/TechLeadAgent.md +60 -0
  50. package/assets/install-bundle/opencode/commands/brainstorm.md +44 -0
  51. package/assets/install-bundle/opencode/commands/delivery.md +45 -0
  52. package/assets/install-bundle/opencode/commands/execute-plan.md +44 -0
  53. package/assets/install-bundle/opencode/commands/migrate.md +61 -0
  54. package/assets/install-bundle/opencode/commands/quick-task.md +45 -0
  55. package/assets/install-bundle/opencode/commands/task.md +46 -0
  56. package/assets/install-bundle/opencode/commands/write-plan.md +50 -0
  57. package/assets/install-bundle/opencode/context/core/lane-selection.md +54 -0
  58. package/assets/install-bundle/opencode/skills/brainstorming/SKILL.md +51 -0
  59. package/assets/install-bundle/opencode/skills/code-review/SKILL.md +48 -0
  60. package/assets/install-bundle/opencode/skills/subagent-driven-development/SKILL.md +79 -0
  61. package/assets/install-bundle/opencode/skills/systematic-debugging/SKILL.md +61 -0
  62. package/assets/install-bundle/opencode/skills/test-driven-development/SKILL.md +48 -0
  63. package/assets/install-bundle/opencode/skills/using-skills/SKILL.md +39 -0
  64. package/assets/install-bundle/opencode/skills/verification-before-completion/SKILL.md +137 -0
  65. package/assets/install-bundle/opencode/skills/writing-plans/SKILL.md +68 -0
  66. package/assets/install-bundle/opencode/skills/writing-specs/SKILL.md +47 -0
  67. package/assets/opencode.json.template +11 -0
  68. package/assets/openkit-install.json.template +19 -0
  69. package/bin/openkit.js +9 -0
  70. package/commands/brainstorm.md +44 -0
  71. package/commands/delivery.md +45 -0
  72. package/commands/execute-plan.md +44 -0
  73. package/commands/migrate.md +61 -0
  74. package/commands/quick-task.md +45 -0
  75. package/commands/task.md +46 -0
  76. package/commands/write-plan.md +50 -0
  77. package/context/core/approval-gates.md +146 -0
  78. package/context/core/code-quality.md +42 -0
  79. package/context/core/issue-routing.md +85 -0
  80. package/context/core/lane-selection.md +54 -0
  81. package/context/core/project-config.md +143 -0
  82. package/context/core/session-resume.md +85 -0
  83. package/context/core/workflow-state-schema.md +224 -0
  84. package/context/core/workflow.md +442 -0
  85. package/context/navigation.md +94 -0
  86. package/docs/adr/README.md +6 -0
  87. package/docs/architecture/2026-03-20-task-intake-dashboard.md +54 -0
  88. package/docs/architecture/README.md +7 -0
  89. package/docs/briefs/2026-03-20-task-intake-dashboard.md +48 -0
  90. package/docs/briefs/README.md +7 -0
  91. package/docs/governance/README.md +25 -0
  92. package/docs/governance/adr-policy.md +27 -0
  93. package/docs/governance/definition-of-done.md +17 -0
  94. package/docs/governance/naming-conventions.md +21 -0
  95. package/docs/governance/severity-levels.md +12 -0
  96. package/docs/maintainer/README.md +51 -0
  97. package/docs/operations/README.md +79 -0
  98. package/docs/operations/internal-records/2026-03-24-release-checklist.md +79 -0
  99. package/docs/operations/internal-records/2026-03-24-simplified-install-ux.md +36 -0
  100. package/docs/operations/internal-records/README.md +18 -0
  101. package/docs/operations/runbooks/README.md +23 -0
  102. package/docs/operations/runbooks/openkit-daily-usage.md +288 -0
  103. package/docs/operations/runbooks/workflow-state-smoke-tests.md +302 -0
  104. package/docs/operator/README.md +50 -0
  105. package/docs/plans/2026-03-20-task-intake-dashboard.md +49 -0
  106. package/docs/plans/2026-03-21-openkit-full-delivery-multi-task-runtime.md +521 -0
  107. package/docs/plans/2026-03-23-openkit-global-install-runtime.md +157 -0
  108. package/docs/plans/README.md +7 -0
  109. package/docs/qa/2026-03-20-task-intake-dashboard.md +41 -0
  110. package/docs/qa/README.md +7 -0
  111. package/docs/specs/2026-03-20-task-intake-dashboard.md +50 -0
  112. package/docs/specs/2026-03-21-openkit-full-delivery-multi-task-runtime.md +462 -0
  113. package/docs/specs/README.md +7 -0
  114. package/docs/templates/README.md +36 -0
  115. package/docs/templates/adr-template.md +18 -0
  116. package/docs/templates/architecture-template.md +31 -0
  117. package/docs/templates/implementation-plan-template.md +32 -0
  118. package/docs/templates/migration-baseline-checklist.md +48 -0
  119. package/docs/templates/migration-plan-template.md +52 -0
  120. package/docs/templates/migration-report-template.md +74 -0
  121. package/docs/templates/migration-verify-checklist.md +39 -0
  122. package/docs/templates/product-brief-template.md +32 -0
  123. package/docs/templates/qa-report-template.md +37 -0
  124. package/docs/templates/quick-task-template.md +36 -0
  125. package/docs/templates/spec-template.md +31 -0
  126. package/hooks/hooks.json +16 -0
  127. package/hooks/session-start +162 -0
  128. package/package.json +24 -0
  129. package/registry.json +328 -0
  130. package/skills/brainstorming/SKILL.md +51 -0
  131. package/skills/code-review/SKILL.md +48 -0
  132. package/skills/subagent-driven-development/SKILL.md +79 -0
  133. package/skills/systematic-debugging/SKILL.md +61 -0
  134. package/skills/test-driven-development/SKILL.md +48 -0
  135. package/skills/using-skills/SKILL.md +39 -0
  136. package/skills/verification-before-completion/SKILL.md +137 -0
  137. package/skills/writing-plans/SKILL.md +68 -0
  138. package/skills/writing-specs/SKILL.md +47 -0
  139. package/src/audit/vietnamese-detection.js +259 -0
  140. package/src/cli/commands/detect-vietnamese.js +24 -0
  141. package/src/cli/commands/doctor.js +33 -0
  142. package/src/cli/commands/help.js +33 -0
  143. package/src/cli/commands/init.js +25 -0
  144. package/src/cli/commands/install-global.js +26 -0
  145. package/src/cli/commands/install.js +25 -0
  146. package/src/cli/commands/run.js +63 -0
  147. package/src/cli/commands/uninstall.js +32 -0
  148. package/src/cli/commands/upgrade.js +25 -0
  149. package/src/cli/conflict-output.js +19 -0
  150. package/src/cli/index.js +56 -0
  151. package/src/global/doctor.js +101 -0
  152. package/src/global/ensure-install.js +32 -0
  153. package/src/global/install-state.js +73 -0
  154. package/src/global/launcher.js +51 -0
  155. package/src/global/materialize.js +123 -0
  156. package/src/global/paths.js +85 -0
  157. package/src/global/uninstall.js +25 -0
  158. package/src/global/workspace-state.js +63 -0
  159. package/src/install/asset-manifest.js +284 -0
  160. package/src/install/conflicts.js +43 -0
  161. package/src/install/discovery.js +138 -0
  162. package/src/install/install-state.js +136 -0
  163. package/src/install/materialize.js +158 -0
  164. package/src/install/merge-policy.js +145 -0
  165. package/src/runtime/doctor.js +281 -0
  166. package/src/runtime/launcher.js +49 -0
  167. package/src/runtime/opencode-layering.js +135 -0
  168. package/src/runtime/openkit-managed-summary.js +27 -0
  169. package/tests/cli/openkit-cli.test.js +417 -0
  170. package/tests/global/doctor.test.js +130 -0
  171. package/tests/global/ensure-install.test.js +105 -0
  172. package/tests/install/discovery.test.js +124 -0
  173. package/tests/install/install-state.test.js +346 -0
  174. package/tests/install/materialize.test.js +244 -0
  175. package/tests/install/merge-policy.test.js +177 -0
  176. package/tests/runtime/doctor.test.js +430 -0
  177. package/tests/runtime/launcher.test.js +230 -0
  178. package/tests/runtime/module-boundary.test.js +16 -0
@@ -0,0 +1,135 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export const CONFIG_DIR_RELATIVE_PATHS = [
5
+ 'agents_dir',
6
+ 'commands_dir',
7
+ 'skills_dir',
8
+ 'hooks.config',
9
+ ];
10
+
11
+ function isPlainObject(value) {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+
15
+ function deepMerge(base, overlay) {
16
+ if (!isPlainObject(base) || !isPlainObject(overlay)) {
17
+ return overlay;
18
+ }
19
+
20
+ const result = { ...base };
21
+
22
+ for (const [key, value] of Object.entries(overlay)) {
23
+ if (isPlainObject(value) && isPlainObject(result[key])) {
24
+ result[key] = deepMerge(result[key], value);
25
+ continue;
26
+ }
27
+
28
+ result[key] = value;
29
+ }
30
+
31
+ return result;
32
+ }
33
+
34
+ function readJsonIfPresent(filePath) {
35
+ if (!fs.existsSync(filePath)) {
36
+ return null;
37
+ }
38
+
39
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
40
+ }
41
+
42
+ function parseJsonContent(content, sourceLabel) {
43
+ if (!content) {
44
+ return null;
45
+ }
46
+
47
+ try {
48
+ return JSON.parse(content);
49
+ } catch (error) {
50
+ throw new Error(`${sourceLabel} must contain valid JSON.`, { cause: error });
51
+ }
52
+ }
53
+
54
+ function shouldResolveRelativeToConfigDir(keyPath) {
55
+ const joinedPath = keyPath.filter((segment) => typeof segment === 'string').join('.');
56
+ return CONFIG_DIR_RELATIVE_PATHS.includes(joinedPath);
57
+ }
58
+
59
+ function normalizeConfigPaths(value, configDir, keyPath = []) {
60
+ if (Array.isArray(value)) {
61
+ return value.map((entry, index) => normalizeConfigPaths(entry, configDir, [...keyPath, index]));
62
+ }
63
+
64
+ if (isPlainObject(value)) {
65
+ return Object.fromEntries(
66
+ Object.entries(value).map(([key, entry]) => [
67
+ key,
68
+ normalizeConfigPaths(entry, configDir, [...keyPath, key]),
69
+ ])
70
+ );
71
+ }
72
+
73
+ if (typeof value !== 'string' || !configDir || path.isAbsolute(value)) {
74
+ return value;
75
+ }
76
+
77
+ if (!shouldResolveRelativeToConfigDir(keyPath)) {
78
+ return value;
79
+ }
80
+
81
+ return path.resolve(configDir, value);
82
+ }
83
+
84
+ export function buildOpenCodeLayering({ projectRoot, env = process.env }) {
85
+ const runtimeManifestPath = path.join(projectRoot, '.opencode', 'opencode.json');
86
+ const managedConfigDir = path.dirname(runtimeManifestPath);
87
+ const managedConfig = readJsonIfPresent(runtimeManifestPath);
88
+
89
+ if (!managedConfig) {
90
+ throw new Error(
91
+ `OpenKit managed runtime manifest was not found at ${runtimeManifestPath}.`
92
+ );
93
+ }
94
+
95
+ const baselineConfigDir = env.OPENCODE_CONFIG_DIR ?? null;
96
+ const baselineDirConfig = baselineConfigDir
97
+ ? readJsonIfPresent(path.join(baselineConfigDir, 'opencode.json'))
98
+ : null;
99
+ const baselineContentConfig = parseJsonContent(
100
+ env.OPENCODE_CONFIG_CONTENT,
101
+ 'OPENCODE_CONFIG_CONTENT'
102
+ );
103
+
104
+ const normalizedBaselineDirConfig = normalizeConfigPaths(baselineDirConfig ?? {}, baselineConfigDir);
105
+ const normalizedBaselineContentConfig = normalizeConfigPaths(
106
+ baselineContentConfig ?? {},
107
+ baselineConfigDir
108
+ );
109
+ const baselineConfig = deepMerge(normalizedBaselineDirConfig, normalizedBaselineContentConfig);
110
+ const mergedConfig = deepMerge(baselineConfig, managedConfig);
111
+ const layeredEnv = { ...env };
112
+
113
+ if (baselineConfigDir || baselineContentConfig) {
114
+ layeredEnv.OPENCODE_CONFIG_DIR = managedConfigDir;
115
+ layeredEnv.OPENCODE_CONFIG_CONTENT = JSON.stringify(mergedConfig);
116
+ } else {
117
+ layeredEnv.OPENCODE_CONFIG_DIR = managedConfigDir;
118
+ delete layeredEnv.OPENCODE_CONFIG_CONTENT;
119
+ }
120
+
121
+ return {
122
+ env: layeredEnv,
123
+ baseline: {
124
+ configDir: baselineConfigDir,
125
+ hasConfigContent: Boolean(baselineContentConfig),
126
+ config: baselineConfig,
127
+ },
128
+ managedConfig: {
129
+ configDir: managedConfigDir,
130
+ runtimeManifestPath,
131
+ config: managedConfig,
132
+ },
133
+ mergedConfig,
134
+ };
135
+ }
@@ -0,0 +1,27 @@
1
+ function formatList(values) {
2
+ if (!Array.isArray(values) || values.length === 0) {
3
+ return 'none';
4
+ }
5
+
6
+ return values.join(', ');
7
+ }
8
+
9
+ export function renderManagedDoctorSummary(result) {
10
+ const lines = [
11
+ `Status: ${result.status}`,
12
+ `Summary: ${result.summary}`,
13
+ `Can run cleanly: ${result.canRunCleanly ? 'yes' : 'no'}`,
14
+ `Owned by OpenKit: ${formatList(result.ownedAssets?.managed)}`,
15
+ `Adopted by OpenKit: ${formatList(result.ownedAssets?.adopted)}`,
16
+ `Drifted assets: ${formatList(result.driftedAssets)}`,
17
+ ];
18
+
19
+ if (Array.isArray(result.issues) && result.issues.length > 0) {
20
+ lines.push('Issues:');
21
+ for (const issue of result.issues) {
22
+ lines.push(`- ${issue}`);
23
+ }
24
+ }
25
+
26
+ return `${lines.join('\n')}\n`;
27
+ }
@@ -0,0 +1,417 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { spawnSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const worktreeRoot = path.resolve(__dirname, '..', '..');
12
+ const binPath = path.join(worktreeRoot, 'bin', 'openkit.js');
13
+
14
+ function runCli(args, { cwd = worktreeRoot, env } = {}) {
15
+ return spawnSync(process.execPath, [binPath, ...args], {
16
+ cwd,
17
+ encoding: 'utf8',
18
+ env: env ?? process.env,
19
+ });
20
+ }
21
+
22
+ function makeTempDir() {
23
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'openkit-cli-'));
24
+ }
25
+
26
+ function readJson(filePath) {
27
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
28
+ }
29
+
30
+ function writeExecutable(filePath, content) {
31
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
32
+ fs.writeFileSync(filePath, content, 'utf8');
33
+ fs.chmodSync(filePath, 0o755);
34
+ }
35
+
36
+ test('openkit --help shows global-install oriented help', () => {
37
+ const result = runCli(['--help']);
38
+
39
+ assert.equal(result.status, 0);
40
+ assert.match(result.stdout, /npm install -g openkit/);
41
+ assert.match(result.stdout, /perform first-time setup if needed/);
42
+ assert.match(result.stdout, /install-global/);
43
+ assert.match(result.stdout, /upgrade/);
44
+ assert.match(result.stdout, /uninstall/);
45
+ assert.match(result.stdout, /Launch OpenCode and perform first-time setup if needed/);
46
+ assert.equal(result.stderr, '');
47
+ });
48
+
49
+ test('openkit doctor --help shows global doctor help', () => {
50
+ const result = runCli(['doctor', '--help']);
51
+
52
+ assert.equal(result.status, 0);
53
+ assert.match(result.stdout, /global OpenKit install and the current workspace/);
54
+ assert.equal(result.stderr, '');
55
+ });
56
+
57
+ test('install command help reflects manual and compatibility setup paths', () => {
58
+ const installGlobalResult = runCli(['install-global', '--help']);
59
+ const installResult = runCli(['install', '--help']);
60
+ const initResult = runCli(['init', '--help']);
61
+
62
+ assert.equal(installGlobalResult.status, 0);
63
+ assert.match(installGlobalResult.stdout, /Manually install OpenKit globally/i);
64
+ assert.match(installGlobalResult.stdout, /Most users should run `openkit run`/i);
65
+
66
+ assert.equal(installResult.status, 0);
67
+ assert.match(installResult.stdout, /Compatibility alias for manual global setup/i);
68
+ assert.match(installResult.stdout, /Most users should run `openkit run`/i);
69
+
70
+ assert.equal(initResult.status, 0);
71
+ assert.match(initResult.stdout, /Compatibility alias for manual global setup/i);
72
+ assert.match(initResult.stdout, /Most users should run `openkit run`/i);
73
+ });
74
+
75
+ test('openkit install-global materializes global kit and profile files', () => {
76
+ const tempHome = makeTempDir();
77
+ const result = runCli(['install-global'], {
78
+ env: {
79
+ ...process.env,
80
+ OPENCODE_HOME: tempHome,
81
+ },
82
+ });
83
+
84
+ assert.equal(result.status, 0);
85
+ assert.match(result.stdout, /Installed OpenKit globally/);
86
+
87
+ const kitRoot = path.join(tempHome, 'kits', 'openkit');
88
+ const profileRoot = path.join(tempHome, 'profiles', 'openkit');
89
+
90
+ assert.equal(fs.existsSync(path.join(kitRoot, '.opencode', 'workflow-state.js')), true);
91
+ assert.equal(fs.existsSync(path.join(kitRoot, 'commands', 'migrate.md')), true);
92
+ assert.equal(fs.existsSync(path.join(profileRoot, 'opencode.json')), true);
93
+ assert.equal(readJson(path.join(profileRoot, 'opencode.json')).openkit.profile, 'openkit');
94
+ });
95
+
96
+ test('openkit init and install remain compatibility aliases for install-global', () => {
97
+ const tempHome = makeTempDir();
98
+
99
+ const initResult = runCli(['init'], {
100
+ env: {
101
+ ...process.env,
102
+ OPENCODE_HOME: tempHome,
103
+ },
104
+ });
105
+ const installResult = runCli(['install'], {
106
+ env: {
107
+ ...process.env,
108
+ OPENCODE_HOME: tempHome,
109
+ },
110
+ });
111
+
112
+ assert.equal(initResult.status, 0);
113
+ assert.equal(installResult.status, 0);
114
+ assert.match(initResult.stdout, /Installed OpenKit globally/);
115
+ assert.match(installResult.stdout, /Installed OpenKit globally/);
116
+ });
117
+
118
+ test('openkit doctor reports install-missing when global install is absent', () => {
119
+ const tempHome = makeTempDir();
120
+ const projectRoot = makeTempDir();
121
+
122
+ const result = runCli(['doctor'], {
123
+ cwd: projectRoot,
124
+ env: {
125
+ ...process.env,
126
+ OPENCODE_HOME: tempHome,
127
+ PATH: '',
128
+ },
129
+ });
130
+
131
+ assert.equal(result.status, 1);
132
+ assert.match(result.stdout, /Status: install-missing/);
133
+ assert.match(result.stdout, /Global OpenKit install was not found/);
134
+ assert.match(result.stdout, /Next: Run openkit run for first-time setup/);
135
+ assert.match(result.stdout, /Recommended command: openkit run/);
136
+ });
137
+
138
+ test('openkit doctor reports healthy after global install and bootstraps workspace metadata', () => {
139
+ const tempHome = makeTempDir();
140
+ const projectRoot = makeTempDir();
141
+ const fakeBinDir = path.join(tempHome, 'bin');
142
+ writeExecutable(path.join(fakeBinDir, 'opencode'), '#!/bin/sh\nexit 0\n');
143
+
144
+ const installResult = runCli(['install-global'], {
145
+ env: {
146
+ ...process.env,
147
+ OPENCODE_HOME: tempHome,
148
+ },
149
+ });
150
+ assert.equal(installResult.status, 0);
151
+
152
+ const result = runCli(['doctor'], {
153
+ cwd: projectRoot,
154
+ env: {
155
+ ...process.env,
156
+ OPENCODE_HOME: tempHome,
157
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
158
+ },
159
+ });
160
+
161
+ assert.equal(result.status, 0);
162
+ assert.match(result.stdout, /Status: healthy/);
163
+ assert.match(result.stdout, /Workspace root:/);
164
+ assert.match(result.stdout, /Next: Run openkit run/);
165
+ assert.match(result.stdout, /Recommended command: openkit run/);
166
+
167
+ const workspacesRoot = path.join(tempHome, 'workspaces');
168
+ const workspaceEntries = fs.readdirSync(workspacesRoot);
169
+ assert.equal(workspaceEntries.length, 1);
170
+ const workspaceMetaPath = path.join(workspacesRoot, workspaceEntries[0], 'openkit', 'workspace.json');
171
+ assert.equal(fs.existsSync(workspaceMetaPath), true);
172
+ });
173
+
174
+ test('openkit run launches opencode with the global profile and workspace env', () => {
175
+ const tempHome = makeTempDir();
176
+ const projectRoot = makeTempDir();
177
+ const fakeBinDir = path.join(tempHome, 'bin');
178
+ const logPath = path.join(tempHome, 'opencode-run.json');
179
+
180
+ const installResult = runCli(['install-global'], {
181
+ env: {
182
+ ...process.env,
183
+ OPENCODE_HOME: tempHome,
184
+ },
185
+ });
186
+ assert.equal(installResult.status, 0);
187
+
188
+ writeExecutable(
189
+ path.join(fakeBinDir, 'opencode'),
190
+ `#!/usr/bin/env node
191
+ import fs from 'node:fs';
192
+ fs.writeFileSync(process.env.OPENKIT_TEST_LOG_PATH, JSON.stringify({
193
+ argv: process.argv.slice(2),
194
+ cwd: process.cwd(),
195
+ projectRoot: process.env.OPENKIT_PROJECT_ROOT,
196
+ workflowState: process.env.OPENKIT_WORKFLOW_STATE,
197
+ kitRoot: process.env.OPENKIT_KIT_ROOT,
198
+ configDir: process.env.OPENCODE_CONFIG_DIR,
199
+ }, null, 2));
200
+ process.stdout.write('mock opencode launched\\n');
201
+ `
202
+ );
203
+
204
+ const result = runCli(['run', '--mode', 'quick'], {
205
+ cwd: projectRoot,
206
+ env: {
207
+ ...process.env,
208
+ OPENCODE_HOME: tempHome,
209
+ OPENKIT_TEST_LOG_PATH: logPath,
210
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
211
+ },
212
+ });
213
+
214
+ assert.equal(result.status, 0);
215
+ assert.match(result.stdout, /mock opencode launched/);
216
+
217
+ const invocation = readJson(logPath);
218
+ assert.deepEqual(invocation.argv, ['--profile', 'openkit', '--mode', 'quick']);
219
+ assert.equal(fs.realpathSync(invocation.cwd), fs.realpathSync(projectRoot));
220
+ assert.equal(fs.realpathSync(invocation.projectRoot), fs.realpathSync(projectRoot));
221
+ assert.equal(invocation.configDir, path.join(tempHome, 'profiles', 'openkit'));
222
+ assert.match(invocation.workflowState, /workspaces\/.*\/openkit\/\.opencode\/workflow-state\.json$/);
223
+ assert.equal(invocation.kitRoot, path.join(tempHome, 'kits', 'openkit'));
224
+ });
225
+
226
+ test('openkit run does not reinstall when the global install already exists', () => {
227
+ const tempHome = makeTempDir();
228
+ const projectRoot = makeTempDir();
229
+ const fakeBinDir = path.join(tempHome, 'bin');
230
+
231
+ const installResult = runCli(['install-global'], {
232
+ env: {
233
+ ...process.env,
234
+ OPENCODE_HOME: tempHome,
235
+ },
236
+ });
237
+ assert.equal(installResult.status, 0);
238
+
239
+ writeExecutable(path.join(fakeBinDir, 'opencode'), '#!/bin/sh\necho already-installed-run\n');
240
+
241
+ const result = runCli(['run'], {
242
+ cwd: projectRoot,
243
+ env: {
244
+ ...process.env,
245
+ OPENCODE_HOME: tempHome,
246
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
247
+ },
248
+ });
249
+
250
+ assert.equal(result.status, 0);
251
+ assert.doesNotMatch(result.stdout, /Performing first-time setup/);
252
+ assert.match(result.stdout, /already-installed-run/);
253
+ });
254
+
255
+ test('openkit run auto-installs the global kit on first use', () => {
256
+ const tempHome = makeTempDir();
257
+ const projectRoot = makeTempDir();
258
+ const fakeBinDir = path.join(tempHome, 'bin');
259
+ const logPath = path.join(tempHome, 'opencode-auto-install.json');
260
+
261
+ writeExecutable(
262
+ path.join(fakeBinDir, 'opencode'),
263
+ `#!/usr/bin/env node
264
+ import fs from 'node:fs';
265
+ fs.writeFileSync(process.env.OPENKIT_TEST_LOG_PATH, JSON.stringify({
266
+ argv: process.argv.slice(2),
267
+ cwd: process.cwd(),
268
+ projectRoot: process.env.OPENKIT_PROJECT_ROOT,
269
+ workflowState: process.env.OPENKIT_WORKFLOW_STATE,
270
+ kitRoot: process.env.OPENKIT_KIT_ROOT,
271
+ configDir: process.env.OPENCODE_CONFIG_DIR,
272
+ }, null, 2));
273
+ process.stdout.write('mock opencode launched after auto-install\\n');
274
+ `
275
+ );
276
+
277
+ const result = runCli(['run'], {
278
+ cwd: projectRoot,
279
+ env: {
280
+ ...process.env,
281
+ OPENCODE_HOME: tempHome,
282
+ OPENKIT_TEST_LOG_PATH: logPath,
283
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
284
+ },
285
+ });
286
+
287
+ assert.equal(result.status, 0);
288
+ assert.match(result.stdout, /Performing first-time setup/);
289
+ assert.match(result.stdout, /Installed OpenKit globally/);
290
+ assert.match(result.stdout, /mock opencode launched after auto-install/);
291
+ assert.equal(fs.existsSync(path.join(tempHome, 'kits', 'openkit', '.opencode', 'workflow-state.js')), true);
292
+
293
+ const invocation = readJson(logPath);
294
+ assert.deepEqual(invocation.argv, ['--profile', 'openkit']);
295
+ assert.equal(invocation.kitRoot, path.join(tempHome, 'kits', 'openkit'));
296
+ });
297
+
298
+ test('openkit run reports missing opencode after first-time setup completes', () => {
299
+ const tempHome = makeTempDir();
300
+ const projectRoot = makeTempDir();
301
+
302
+ const result = runCli(['run'], {
303
+ cwd: projectRoot,
304
+ env: {
305
+ ...process.env,
306
+ OPENCODE_HOME: tempHome,
307
+ PATH: '',
308
+ },
309
+ });
310
+
311
+ assert.equal(result.status, 1);
312
+ assert.match(result.stdout, /Performing first-time setup/);
313
+ assert.match(result.stdout, /Installed OpenKit globally/);
314
+ assert.match(result.stderr, /Could not find `opencode` on your PATH/);
315
+ });
316
+
317
+ test('openkit run blocks on invalid global install state and recommends upgrade', () => {
318
+ const tempHome = makeTempDir();
319
+ const projectRoot = makeTempDir();
320
+ const kitRoot = path.join(tempHome, 'kits', 'openkit');
321
+
322
+ fs.mkdirSync(kitRoot, { recursive: true });
323
+ fs.writeFileSync(
324
+ path.join(kitRoot, 'install-state.json'),
325
+ `${JSON.stringify({
326
+ schema: 'wrong-schema',
327
+ stateVersion: 1,
328
+ kit: { name: 'OpenKit', version: '0.1.0' },
329
+ installation: {
330
+ profile: 'openkit',
331
+ status: 'installed',
332
+ installedAt: '2026-03-24T00:00:00.000Z',
333
+ },
334
+ }, null, 2)}\n`,
335
+ 'utf8'
336
+ );
337
+
338
+ const result = runCli(['run'], {
339
+ cwd: projectRoot,
340
+ env: {
341
+ ...process.env,
342
+ OPENCODE_HOME: tempHome,
343
+ PATH: process.env.PATH ?? '',
344
+ },
345
+ });
346
+
347
+ assert.equal(result.status, 1);
348
+ assert.match(result.stderr, /schema must be 'openkit\/global-install-state@1'/i);
349
+ assert.match(result.stderr, /Next: Run openkit upgrade to refresh the global install/i);
350
+ });
351
+
352
+ test('openkit upgrade refreshes the global kit install', () => {
353
+ const tempHome = makeTempDir();
354
+
355
+ const installResult = runCli(['install-global'], {
356
+ env: {
357
+ ...process.env,
358
+ OPENCODE_HOME: tempHome,
359
+ },
360
+ });
361
+ assert.equal(installResult.status, 0);
362
+
363
+ const result = runCli(['upgrade'], {
364
+ env: {
365
+ ...process.env,
366
+ OPENCODE_HOME: tempHome,
367
+ },
368
+ });
369
+
370
+ assert.equal(result.status, 0);
371
+ assert.match(result.stdout, /Upgraded OpenKit global install/);
372
+ });
373
+
374
+ test('openkit uninstall removes the global kit and profile and can remove workspace state', () => {
375
+ const tempHome = makeTempDir();
376
+ const projectRoot = makeTempDir();
377
+ const fakeBinDir = path.join(tempHome, 'bin');
378
+ writeExecutable(path.join(fakeBinDir, 'opencode'), '#!/bin/sh\nexit 0\n');
379
+
380
+ let result = runCli(['install-global'], {
381
+ env: {
382
+ ...process.env,
383
+ OPENCODE_HOME: tempHome,
384
+ },
385
+ });
386
+ assert.equal(result.status, 0);
387
+
388
+ result = runCli(['doctor'], {
389
+ cwd: projectRoot,
390
+ env: {
391
+ ...process.env,
392
+ OPENCODE_HOME: tempHome,
393
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH}`,
394
+ },
395
+ });
396
+ assert.equal(result.status, 0);
397
+
398
+ result = runCli(['uninstall', '--remove-workspaces'], {
399
+ env: {
400
+ ...process.env,
401
+ OPENCODE_HOME: tempHome,
402
+ },
403
+ });
404
+ assert.equal(result.status, 0);
405
+ assert.match(result.stdout, /Uninstalled OpenKit global kit/);
406
+ assert.match(result.stdout, /Workspace state was removed/);
407
+ assert.equal(fs.existsSync(path.join(tempHome, 'kits', 'openkit')), false);
408
+ assert.equal(fs.existsSync(path.join(tempHome, 'profiles', 'openkit')), false);
409
+ assert.equal(fs.existsSync(path.join(tempHome, 'workspaces')), false);
410
+ });
411
+
412
+ test('openkit exits non-zero for an unknown command', () => {
413
+ const result = runCli(['unknown-command']);
414
+
415
+ assert.equal(result.status, 1);
416
+ assert.match(result.stderr, /Unknown command: unknown-command/);
417
+ });
@@ -0,0 +1,130 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ import { inspectGlobalDoctor, renderGlobalDoctorSummary } from '../../src/global/doctor.js';
8
+ import { materializeGlobalInstall } from '../../src/global/materialize.js';
9
+
10
+ function makeTempDir() {
11
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'openkit-global-doctor-'));
12
+ }
13
+
14
+ function writeExecutable(filePath, content) {
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ fs.writeFileSync(filePath, content, 'utf8');
17
+ fs.chmodSync(filePath, 0o755);
18
+ }
19
+
20
+ function writeJson(filePath, value) {
21
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
22
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
23
+ }
24
+
25
+ test('global doctor reports next steps for install-missing', () => {
26
+ const tempHome = makeTempDir();
27
+ const projectRoot = makeTempDir();
28
+
29
+ const result = inspectGlobalDoctor({
30
+ projectRoot,
31
+ env: {
32
+ ...process.env,
33
+ OPENCODE_HOME: tempHome,
34
+ PATH: '',
35
+ },
36
+ });
37
+
38
+ assert.equal(result.status, 'install-missing');
39
+ assert.equal(result.nextStep, 'Run openkit run for first-time setup.');
40
+ assert.equal(result.recommendedCommand, 'openkit run');
41
+
42
+ const output = renderGlobalDoctorSummary(result);
43
+ assert.match(output, /Next: Run openkit run for first-time setup\./);
44
+ assert.match(output, /Recommended command: openkit run/);
45
+ });
46
+
47
+ test('global doctor reports next steps for healthy installs', () => {
48
+ const tempHome = makeTempDir();
49
+ const projectRoot = makeTempDir();
50
+ const fakeBinDir = path.join(tempHome, 'bin');
51
+
52
+ materializeGlobalInstall({
53
+ env: {
54
+ ...process.env,
55
+ OPENCODE_HOME: tempHome,
56
+ },
57
+ });
58
+ writeExecutable(path.join(fakeBinDir, 'opencode'), '#!/bin/sh\nexit 0\n');
59
+
60
+ const result = inspectGlobalDoctor({
61
+ projectRoot,
62
+ env: {
63
+ ...process.env,
64
+ OPENCODE_HOME: tempHome,
65
+ PATH: `${fakeBinDir}${path.delimiter}${process.env.PATH ?? ''}`,
66
+ },
67
+ });
68
+
69
+ assert.equal(result.status, 'healthy');
70
+ assert.equal(result.nextStep, 'Run openkit run.');
71
+ assert.equal(result.recommendedCommand, 'openkit run');
72
+ });
73
+
74
+ test('global doctor recommends upgrade for invalid installs', () => {
75
+ const tempHome = makeTempDir();
76
+ const projectRoot = makeTempDir();
77
+
78
+ writeJson(path.join(tempHome, 'kits', 'openkit', 'install-state.json'), {
79
+ schema: 'wrong-schema',
80
+ stateVersion: 1,
81
+ kit: {
82
+ name: 'OpenKit',
83
+ version: '0.1.0',
84
+ },
85
+ installation: {
86
+ profile: 'openkit',
87
+ status: 'installed',
88
+ installedAt: '2026-03-24T00:00:00.000Z',
89
+ },
90
+ });
91
+
92
+ const result = inspectGlobalDoctor({
93
+ projectRoot,
94
+ env: {
95
+ ...process.env,
96
+ OPENCODE_HOME: tempHome,
97
+ PATH: process.env.PATH ?? '',
98
+ },
99
+ });
100
+
101
+ assert.equal(result.status, 'install-invalid');
102
+ assert.equal(result.nextStep, 'Run openkit upgrade to refresh the global install.');
103
+ assert.equal(result.recommendedCommand, 'openkit upgrade');
104
+ });
105
+
106
+ test('global doctor reports workspace issues with guidance', () => {
107
+ const tempHome = makeTempDir();
108
+ const projectRoot = makeTempDir();
109
+
110
+ materializeGlobalInstall({
111
+ env: {
112
+ ...process.env,
113
+ OPENCODE_HOME: tempHome,
114
+ },
115
+ });
116
+
117
+ const result = inspectGlobalDoctor({
118
+ projectRoot,
119
+ env: {
120
+ ...process.env,
121
+ OPENCODE_HOME: tempHome,
122
+ PATH: '',
123
+ },
124
+ });
125
+
126
+ assert.equal(result.status, 'workspace-ready-with-issues');
127
+ assert.equal(result.nextStep, 'Review the issues above before relying on this workspace.');
128
+ assert.equal(result.recommendedCommand, null);
129
+ assert.match(result.issues.join('\n'), /OpenCode executable is not available on PATH/);
130
+ });