@cortexkit/opencode-magic-context 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli.js +31 -34
- package/dist/index.js +3 -2
- package/dist/shared/conflict-fixer.d.ts.map +1 -1
- package/dist/shared/tui-config.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/shared/assistant-message-extractor.ts +74 -0
- package/src/shared/conflict-detector.ts +310 -0
- package/src/shared/conflict-fixer.ts +216 -0
- package/src/shared/data-path.ts +10 -0
- package/src/shared/error-message.ts +3 -0
- package/src/shared/format-bytes.ts +5 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/internal-initiator-marker.ts +1 -0
- package/src/shared/jsonc-parser.ts +138 -0
- package/src/shared/logger.ts +63 -0
- package/src/shared/model-requirements.ts +84 -0
- package/src/shared/model-suggestion-retry.ts +160 -0
- package/src/shared/normalize-sdk-response.ts +40 -0
- package/src/shared/opencode-compaction-detector.test.ts +222 -0
- package/src/shared/opencode-compaction-detector.ts +81 -0
- package/src/shared/opencode-config-dir-types.ts +15 -0
- package/src/shared/opencode-config-dir.ts +38 -0
- package/src/shared/record-type-guard.ts +3 -0
- package/src/shared/system-directive.ts +9 -0
- package/src/shared/tui-config.ts +60 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface NormalizeSDKResponseOptions {
|
|
2
|
+
preferResponseOnMissingData?: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// Audit note: `as TData` casts are intentional at this boundary. The OpenCode plugin SDK types
|
|
6
|
+
// external responses as `unknown`. Adding Zod validation here would require schema definitions
|
|
7
|
+
// for every SDK response shape, which changes with each OpenCode release. The fallback parameter
|
|
8
|
+
// provides safe degradation when shapes mismatch.
|
|
9
|
+
export function normalizeSDKResponse<TData>(
|
|
10
|
+
response: unknown,
|
|
11
|
+
fallback: TData,
|
|
12
|
+
options?: NormalizeSDKResponseOptions,
|
|
13
|
+
): TData {
|
|
14
|
+
if (response === null || response === undefined) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (Array.isArray(response)) {
|
|
19
|
+
return response as TData;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof response === "object" && "data" in response) {
|
|
23
|
+
const data = (response as { data?: unknown }).data;
|
|
24
|
+
if (data !== null && data !== undefined) {
|
|
25
|
+
return data as TData;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options?.preferResponseOnMissingData === true) {
|
|
29
|
+
return response as TData;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options?.preferResponseOnMissingData === true) {
|
|
36
|
+
return response as TData;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isOpenCodeAutoCompactionEnabled } from "./opencode-compaction-detector";
|
|
5
|
+
import * as configDir from "./opencode-config-dir";
|
|
6
|
+
|
|
7
|
+
describe("opencode-compaction-detector", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = join("/tmp", `compaction-detector-test-${Date.now()}`);
|
|
12
|
+
mkdirSync(join(tmpDir, ".opencode"), { recursive: true });
|
|
13
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
14
|
+
spyOn(configDir, "getOpenCodeConfigPaths").mockReturnValue({
|
|
15
|
+
configJson: join(tmpDir, "user-config", "opencode.json"),
|
|
16
|
+
configJsonc: join(tmpDir, "user-config", "opencode.jsonc"),
|
|
17
|
+
} as ReturnType<typeof configDir.getOpenCodeConfigPaths>);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("#given no config exists", () => {
|
|
26
|
+
it("#then returns true (default: compaction enabled)", () => {
|
|
27
|
+
const emptyDir = join("/tmp", `compaction-empty-${Date.now()}`);
|
|
28
|
+
mkdirSync(emptyDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const result = isOpenCodeAutoCompactionEnabled(emptyDir);
|
|
31
|
+
|
|
32
|
+
expect(result).toBe(true);
|
|
33
|
+
rmSync(emptyDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("#given OPENCODE_DISABLE_AUTOCOMPACT env flag is set", () => {
|
|
38
|
+
it("#then returns false", () => {
|
|
39
|
+
process.env.OPENCODE_DISABLE_AUTOCOMPACT = "1";
|
|
40
|
+
|
|
41
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("#given project config has compaction.auto = false", () => {
|
|
48
|
+
it("#when opencode.json #then returns false", () => {
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
51
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("#when opencode.jsonc #then returns false", () => {
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(tmpDir, ".opencode", "opencode.jsonc"),
|
|
62
|
+
`{
|
|
63
|
+
// compaction disabled
|
|
64
|
+
"compaction": { "auto": false }
|
|
65
|
+
}`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
69
|
+
|
|
70
|
+
expect(result).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("#given project config has compaction.auto = true", () => {
|
|
75
|
+
it("#then returns true", () => {
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
78
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
82
|
+
|
|
83
|
+
expect(result).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("#given project config has compaction.prune = true", () => {
|
|
88
|
+
it("#then returns true (conflict enabled)", () => {
|
|
89
|
+
writeFileSync(
|
|
90
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
91
|
+
JSON.stringify({ compaction: { auto: false, prune: true } }),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
95
|
+
|
|
96
|
+
expect(result).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("#given project config has auto/prune both false", () => {
|
|
101
|
+
it("#then returns false", () => {
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
104
|
+
JSON.stringify({ compaction: { auto: false, prune: false } }),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
108
|
+
|
|
109
|
+
expect(result).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("#given project config has only compaction.prune = false", () => {
|
|
114
|
+
it("#then returns false", () => {
|
|
115
|
+
writeFileSync(
|
|
116
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
117
|
+
JSON.stringify({ compaction: { prune: false } }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
121
|
+
|
|
122
|
+
expect(result).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("#given jsonc and json both exist", () => {
|
|
127
|
+
it("#then jsonc takes precedence", () => {
|
|
128
|
+
writeFileSync(
|
|
129
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
130
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
131
|
+
);
|
|
132
|
+
writeFileSync(
|
|
133
|
+
join(tmpDir, ".opencode", "opencode.jsonc"),
|
|
134
|
+
`{ "compaction": { "auto": false } }`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
138
|
+
|
|
139
|
+
expect(result).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("#given config exists but no compaction field", () => {
|
|
144
|
+
it("#then returns true (default)", () => {
|
|
145
|
+
writeFileSync(
|
|
146
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
147
|
+
JSON.stringify({ model: "claude-opus-4-6" }),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
151
|
+
|
|
152
|
+
expect(result).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("#given env flag overrides config", () => {
|
|
157
|
+
it("#then env flag wins even when config has auto: true", () => {
|
|
158
|
+
process.env.OPENCODE_DISABLE_AUTOCOMPACT = "true";
|
|
159
|
+
writeFileSync(
|
|
160
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
161
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
165
|
+
|
|
166
|
+
expect(result).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("#given root-level project config", () => {
|
|
171
|
+
it("#when root opencode.json has compaction.auto = false #then returns false", () => {
|
|
172
|
+
writeFileSync(
|
|
173
|
+
join(tmpDir, "opencode.json"),
|
|
174
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
178
|
+
|
|
179
|
+
expect(result).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("#when root opencode.jsonc has compaction.auto = false #then returns false", () => {
|
|
183
|
+
writeFileSync(join(tmpDir, "opencode.jsonc"), `{ "compaction": { "auto": false } }`);
|
|
184
|
+
|
|
185
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
186
|
+
|
|
187
|
+
expect(result).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("#given .opencode/ overrides root-level config", () => {
|
|
192
|
+
it("#when root says false but .opencode says true #then .opencode wins", () => {
|
|
193
|
+
writeFileSync(
|
|
194
|
+
join(tmpDir, "opencode.json"),
|
|
195
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
196
|
+
);
|
|
197
|
+
writeFileSync(
|
|
198
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
199
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
203
|
+
|
|
204
|
+
expect(result).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("#when root says true but .opencode says false #then .opencode wins", () => {
|
|
208
|
+
writeFileSync(
|
|
209
|
+
join(tmpDir, "opencode.json"),
|
|
210
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
211
|
+
);
|
|
212
|
+
writeFileSync(
|
|
213
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
214
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
218
|
+
|
|
219
|
+
expect(result).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readJsoncFile } from "./jsonc-parser";
|
|
3
|
+
import { log } from "./logger";
|
|
4
|
+
import { getOpenCodeConfigPaths } from "./opencode-config-dir";
|
|
5
|
+
|
|
6
|
+
interface OpenCodeConfig {
|
|
7
|
+
compaction?: {
|
|
8
|
+
auto?: boolean;
|
|
9
|
+
prune?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasCompactionConflict(
|
|
14
|
+
compaction: OpenCodeConfig["compaction"] | undefined,
|
|
15
|
+
): boolean | undefined {
|
|
16
|
+
if (!compaction) return undefined;
|
|
17
|
+
const hasExplicitSetting = compaction.auto !== undefined || compaction.prune !== undefined;
|
|
18
|
+
if (!hasExplicitSetting) return undefined;
|
|
19
|
+
return compaction.auto === true || compaction.prune === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isOpenCodeAutoCompactionEnabled(directory: string): boolean {
|
|
23
|
+
if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
|
|
24
|
+
log(
|
|
25
|
+
"[compaction-detector] OPENCODE_DISABLE_AUTOCOMPACT env flag set — auto compaction disabled",
|
|
26
|
+
);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const projectCompaction = readProjectCompactionConfig(directory);
|
|
31
|
+
if (projectCompaction !== undefined) {
|
|
32
|
+
log("[compaction-detector] project config compaction conflict =", projectCompaction);
|
|
33
|
+
return projectCompaction;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const userCompaction = readUserCompactionConfig(directory);
|
|
37
|
+
if (userCompaction !== undefined) {
|
|
38
|
+
log("[compaction-detector] user config compaction conflict =", userCompaction);
|
|
39
|
+
return userCompaction;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log("[compaction-detector] no compaction config found — default is enabled");
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readProjectCompactionConfig(directory: string): boolean | undefined {
|
|
47
|
+
// .opencode/ dir config has higher precedence than root-level config in OpenCode's loading order.
|
|
48
|
+
// Check highest precedence first — if .opencode/ sets compaction.auto, that wins.
|
|
49
|
+
const dotOpenCodeJsonc = join(directory, ".opencode", "opencode.jsonc");
|
|
50
|
+
const dotOpenCodeJson = join(directory, ".opencode", "opencode.json");
|
|
51
|
+
const dotOpenCodeConfig =
|
|
52
|
+
readJsoncFile<OpenCodeConfig>(dotOpenCodeJsonc) ??
|
|
53
|
+
readJsoncFile<OpenCodeConfig>(dotOpenCodeJson);
|
|
54
|
+
|
|
55
|
+
const dotOpenCodeCompactionConflict = hasCompactionConflict(dotOpenCodeConfig?.compaction);
|
|
56
|
+
if (dotOpenCodeCompactionConflict !== undefined) {
|
|
57
|
+
return dotOpenCodeCompactionConflict;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Root-level project config (lower precedence than .opencode/)
|
|
61
|
+
const rootJsonc = join(directory, "opencode.jsonc");
|
|
62
|
+
const rootJson = join(directory, "opencode.json");
|
|
63
|
+
const rootConfig =
|
|
64
|
+
readJsoncFile<OpenCodeConfig>(rootJsonc) ?? readJsoncFile<OpenCodeConfig>(rootJson);
|
|
65
|
+
|
|
66
|
+
return hasCompactionConflict(rootConfig?.compaction);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readUserCompactionConfig(_directory: string): boolean | undefined {
|
|
70
|
+
try {
|
|
71
|
+
const paths = getOpenCodeConfigPaths({ binary: "opencode" });
|
|
72
|
+
const config =
|
|
73
|
+
readJsoncFile<OpenCodeConfig>(paths.configJsonc) ??
|
|
74
|
+
readJsoncFile<OpenCodeConfig>(paths.configJson);
|
|
75
|
+
|
|
76
|
+
return hasCompactionConflict(config?.compaction);
|
|
77
|
+
} catch {
|
|
78
|
+
// Intentional: config read is best-effort; missing/unreadable config is not an error
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type OpenCodeBinaryType = "opencode" | "opencode-desktop";
|
|
2
|
+
|
|
3
|
+
export type OpenCodeConfigDirOptions = {
|
|
4
|
+
binary: OpenCodeBinaryType;
|
|
5
|
+
version?: string | null;
|
|
6
|
+
checkExisting?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type OpenCodeConfigPaths = {
|
|
10
|
+
configDir: string;
|
|
11
|
+
configJson: string;
|
|
12
|
+
configJsonc: string;
|
|
13
|
+
packageJson: string;
|
|
14
|
+
omoConfig: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { OpenCodeConfigDirOptions, OpenCodeConfigPaths } from "./opencode-config-dir-types";
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
OpenCodeBinaryType,
|
|
8
|
+
OpenCodeConfigDirOptions,
|
|
9
|
+
OpenCodeConfigPaths,
|
|
10
|
+
} from "./opencode-config-dir-types";
|
|
11
|
+
|
|
12
|
+
function getCliConfigDir(): string {
|
|
13
|
+
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
14
|
+
if (envConfigDir) {
|
|
15
|
+
return resolve(envConfigDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (process.platform === "win32") {
|
|
19
|
+
return join(homedir(), ".config", "opencode");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getOpenCodeConfigDir(_options: OpenCodeConfigDirOptions): string {
|
|
26
|
+
return getCliConfigDir();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenCodeConfigPaths {
|
|
30
|
+
const configDir = getOpenCodeConfigDir(options);
|
|
31
|
+
return {
|
|
32
|
+
configDir,
|
|
33
|
+
configJson: join(configDir, "opencode.json"),
|
|
34
|
+
configJsonc: join(configDir, "opencode.jsonc"),
|
|
35
|
+
packageJson: join(configDir, "package.json"),
|
|
36
|
+
omoConfig: join(configDir, "magic-context.jsonc"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: MAGIC-CONTEXT";
|
|
2
|
+
|
|
3
|
+
export function isSystemDirective(text: string): boolean {
|
|
4
|
+
return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function removeSystemReminders(text: string): string {
|
|
8
|
+
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim();
|
|
9
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-configure tui.json with magic-context TUI plugin entry.
|
|
3
|
+
* Called from the server plugin at startup so the TUI sidebar loads on next restart.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { parse, stringify } from "comment-json";
|
|
9
|
+
import { log } from "./logger";
|
|
10
|
+
import { getOpenCodeConfigPaths } from "./opencode-config-dir";
|
|
11
|
+
|
|
12
|
+
const PLUGIN_NAME = "@cortexkit/opencode-magic-context";
|
|
13
|
+
const PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
|
|
14
|
+
|
|
15
|
+
function resolveTuiConfigPath(): string {
|
|
16
|
+
const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
|
|
17
|
+
const jsoncPath = join(configDir, "tui.jsonc");
|
|
18
|
+
const jsonPath = join(configDir, "tui.json");
|
|
19
|
+
|
|
20
|
+
if (existsSync(jsoncPath)) return jsoncPath;
|
|
21
|
+
if (existsSync(jsonPath)) return jsonPath;
|
|
22
|
+
return jsonPath; // default: create tui.json
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure tui.json has the magic-context TUI plugin entry.
|
|
27
|
+
* Creates tui.json if it doesn't exist. Silently skips if already present.
|
|
28
|
+
*/
|
|
29
|
+
export function ensureTuiPluginEntry(): boolean {
|
|
30
|
+
try {
|
|
31
|
+
const configPath = resolveTuiConfigPath();
|
|
32
|
+
|
|
33
|
+
let config: Record<string, unknown> = {};
|
|
34
|
+
if (existsSync(configPath)) {
|
|
35
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
36
|
+
config = (parse(raw) as Record<string, unknown>) ?? {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const plugins = Array.isArray(config.plugin)
|
|
40
|
+
? config.plugin.filter((p): p is string => typeof p === "string")
|
|
41
|
+
: [];
|
|
42
|
+
|
|
43
|
+
if (plugins.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`))) {
|
|
44
|
+
return false; // Already present
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
plugins.push(PLUGIN_ENTRY);
|
|
48
|
+
config.plugin = plugins;
|
|
49
|
+
|
|
50
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
51
|
+
writeFileSync(configPath, `${stringify(config, null, 2)}\n`);
|
|
52
|
+
log(`[magic-context] added TUI plugin entry to ${configPath}`);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log(
|
|
56
|
+
`[magic-context] failed to update tui.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
57
|
+
);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|