@algochad/archcoder 2.0.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/README.md +113 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +5108 -0
- package/bin/cli.test.js +56 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +67 -0
- package/dist/assets/MultiRunWindow-BZp3MjJP.js +1 -0
- package/dist/assets/SettingsWindow-DoGYXpX7.js +1 -0
- package/dist/assets/TerminalView-BN7BR5Ff.js +3 -0
- package/dist/assets/TimelineDialog-ZQ33oVQR.js +1 -0
- package/dist/assets/ToolOutputDialog-Blv3pnug.js +16 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-CtCEGYrr.css +1 -0
- package/dist/assets/index-o_d2wtWC.js +48 -0
- package/dist/assets/main-5QGBtzdq.css +1 -0
- package/dist/assets/main-B6oiMU86.js +8033 -0
- package/dist/assets/vendor--DbVqbJpV.css +1 -0
- package/dist/assets/vendor-.bun-HTKwyaEM.js +10086 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/worker-bqd4RMrj.js +155 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +67 -0
- package/dist/index.html +533 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.svg +16 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.svg +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +22 -0
- package/dist/sw.js +1 -0
- package/package.json +107 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +67 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +67 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.svg +16 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.svg +16 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +22 -0
- package/server/TERMINAL_INPUT_WS_PROTOCOL.md +44 -0
- package/server/index.d.ts +37 -0
- package/server/index.js +14694 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/git/DOCUMENTATION.md +146 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +110 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/service.js +3117 -0
- package/server/lib/github/DOCUMENTATION.md +170 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +478 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/installer/desktop.js +289 -0
- package/server/lib/installer/download.js +208 -0
- package/server/lib/installer/index.js +45 -0
- package/server/lib/installer/platform.js +100 -0
- package/server/lib/notifications/DOCUMENTATION.md +61 -0
- package/server/lib/notifications/index.js +1 -0
- package/server/lib/notifications/message.js +49 -0
- package/server/lib/notifications/message.test.js +59 -0
- package/server/lib/opencode/DOCUMENTATION.md +59 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth.js +81 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/mcp.js +206 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/shared.js +527 -0
- package/server/lib/opencode/skills.js +480 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/ui-auth.js +510 -0
- package/server/lib/package-manager.js +505 -0
- package/server/lib/quota/DOCUMENTATION.md +55 -0
- package/server/lib/quota/index.js +24 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +152 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +15 -0
- package/server/lib/quota/providers/minimax-shared.js +136 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/utils/auth.js +46 -0
- package/server/lib/quota/utils/formatters.js +76 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +32 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +85 -0
- package/server/lib/terminal/DOCUMENTATION.md +114 -0
- package/server/lib/terminal/index.js +12 -0
- package/server/lib/terminal/input-ws-protocol.js +66 -0
- package/server/lib/terminal/input-ws-protocol.test.js +138 -0
- package/server/lib/tts/DOCUMENTATION.md +134 -0
- package/server/lib/tts/index.js +16 -0
- package/server/lib/tts/service.js +162 -0
- package/server/lib/tts/summarization.js +171 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/utils/lru.js +107 -0
- package/server/lib/utils/sse.js +121 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import yaml from 'yaml';
|
|
5
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
6
|
+
|
|
7
|
+
// ============== PATH CONSTANTS ==============
|
|
8
|
+
|
|
9
|
+
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
|
10
|
+
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
|
|
11
|
+
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
|
|
12
|
+
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
|
|
13
|
+
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
|
14
|
+
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
|
15
|
+
? path.resolve(process.env.OPENCODE_CONFIG)
|
|
16
|
+
: null;
|
|
17
|
+
const PROMPT_FILE_PATTERN = /^\{file:(.+)\}$/i;
|
|
18
|
+
|
|
19
|
+
// ============== SCOPE TYPE CONSTANTS ==============
|
|
20
|
+
|
|
21
|
+
const AGENT_SCOPE = {
|
|
22
|
+
USER: 'user',
|
|
23
|
+
PROJECT: 'project'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const COMMAND_SCOPE = {
|
|
27
|
+
USER: 'user',
|
|
28
|
+
PROJECT: 'project'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SKILL_SCOPE = {
|
|
32
|
+
USER: 'user',
|
|
33
|
+
PROJECT: 'project'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ============== DIRECTORY OPERATIONS ==============
|
|
37
|
+
|
|
38
|
+
function ensureDirs() {
|
|
39
|
+
if (!fs.existsSync(OPENCODE_CONFIG_DIR)) {
|
|
40
|
+
fs.mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
if (!fs.existsSync(AGENT_DIR)) {
|
|
43
|
+
fs.mkdirSync(AGENT_DIR, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
if (!fs.existsSync(COMMAND_DIR)) {
|
|
46
|
+
fs.mkdirSync(COMMAND_DIR, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
if (!fs.existsSync(SKILL_DIR)) {
|
|
49
|
+
fs.mkdirSync(SKILL_DIR, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============== MARKDOWN FILE OPERATIONS ==============
|
|
54
|
+
|
|
55
|
+
function parseMdFile(filePath) {
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
57
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
58
|
+
|
|
59
|
+
if (!match) {
|
|
60
|
+
return { frontmatter: {}, body: content.trim() };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let frontmatter = {};
|
|
64
|
+
try {
|
|
65
|
+
frontmatter = yaml.parse(match[1]) || {};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn(`Failed to parse markdown frontmatter ${filePath}, treating as empty:`, error);
|
|
68
|
+
frontmatter = {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const body = match[2].trim();
|
|
72
|
+
return { frontmatter, body };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeMdFile(filePath, frontmatter, body) {
|
|
76
|
+
try {
|
|
77
|
+
const cleanedFrontmatter = Object.fromEntries(
|
|
78
|
+
Object.entries(frontmatter).filter(([, value]) => value != null)
|
|
79
|
+
);
|
|
80
|
+
const yamlStr = yaml.stringify(cleanedFrontmatter);
|
|
81
|
+
const content = `---\n${yamlStr}---\n\n${body}`;
|
|
82
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
83
|
+
console.log(`Successfully wrote markdown file: ${filePath}`);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`Failed to write markdown file ${filePath}:`, error);
|
|
86
|
+
throw new Error('Failed to write agent markdown file');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============== CONFIG FILE OPERATIONS ==============
|
|
91
|
+
|
|
92
|
+
function getProjectConfigCandidates(workingDirectory) {
|
|
93
|
+
if (!workingDirectory) return [];
|
|
94
|
+
return [
|
|
95
|
+
path.join(workingDirectory, 'opencode.json'),
|
|
96
|
+
path.join(workingDirectory, 'opencode.jsonc'),
|
|
97
|
+
path.join(workingDirectory, '.opencode', 'opencode.json'),
|
|
98
|
+
path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getProjectConfigPath(workingDirectory) {
|
|
103
|
+
if (!workingDirectory) return null;
|
|
104
|
+
|
|
105
|
+
const candidates = getProjectConfigCandidates(workingDirectory);
|
|
106
|
+
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (fs.existsSync(candidate)) {
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return candidates[0];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getConfigPaths(workingDirectory) {
|
|
117
|
+
return {
|
|
118
|
+
userPath: CONFIG_FILE,
|
|
119
|
+
projectPath: getProjectConfigPath(workingDirectory),
|
|
120
|
+
customPath: CUSTOM_CONFIG_FILE
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readConfigFile(filePath) {
|
|
125
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
130
|
+
const normalized = content.trim();
|
|
131
|
+
if (!normalized) {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
return parseJsonc(normalized, [], { allowTrailingComma: true });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`Failed to read config file: ${filePath}`, error);
|
|
137
|
+
throw new Error('Failed to read OpenCode configuration');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isPlainObject(value) {
|
|
142
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function mergeConfigs(base, override) {
|
|
146
|
+
if (!isPlainObject(base) || !isPlainObject(override)) {
|
|
147
|
+
return override;
|
|
148
|
+
}
|
|
149
|
+
const result = { ...base };
|
|
150
|
+
for (const [key, value] of Object.entries(override)) {
|
|
151
|
+
if (key in result) {
|
|
152
|
+
const baseValue = result[key];
|
|
153
|
+
if (isPlainObject(baseValue) && isPlainObject(value)) {
|
|
154
|
+
result[key] = mergeConfigs(baseValue, value);
|
|
155
|
+
} else {
|
|
156
|
+
result[key] = value;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
result[key] = value;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function readConfigLayers(workingDirectory) {
|
|
166
|
+
const { userPath, projectPath, customPath } = getConfigPaths(workingDirectory);
|
|
167
|
+
const userConfig = readConfigFile(userPath);
|
|
168
|
+
const projectConfig = readConfigFile(projectPath);
|
|
169
|
+
const customConfig = readConfigFile(customPath);
|
|
170
|
+
const mergedConfig = mergeConfigs(mergeConfigs(userConfig, projectConfig), customConfig);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
userConfig,
|
|
174
|
+
projectConfig,
|
|
175
|
+
customConfig,
|
|
176
|
+
mergedConfig,
|
|
177
|
+
paths: { userPath, projectPath, customPath }
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readConfig(workingDirectory) {
|
|
182
|
+
return readConfigLayers(workingDirectory).mergedConfig;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getConfigForPath(layers, targetPath) {
|
|
186
|
+
if (!targetPath) {
|
|
187
|
+
return layers.userConfig;
|
|
188
|
+
}
|
|
189
|
+
if (layers.paths.customPath && targetPath === layers.paths.customPath) {
|
|
190
|
+
return layers.customConfig;
|
|
191
|
+
}
|
|
192
|
+
if (layers.paths.projectPath && targetPath === layers.paths.projectPath) {
|
|
193
|
+
return layers.projectConfig;
|
|
194
|
+
}
|
|
195
|
+
return layers.userConfig;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeConfig(config, filePath = CONFIG_FILE) {
|
|
199
|
+
try {
|
|
200
|
+
if (fs.existsSync(filePath)) {
|
|
201
|
+
const backupFile = `${filePath}.archcoder.backup`;
|
|
202
|
+
fs.copyFileSync(filePath, backupFile);
|
|
203
|
+
console.log(`Created config backup: ${backupFile}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
207
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), 'utf8');
|
|
208
|
+
console.log(`Successfully wrote config file: ${filePath}`);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(`Failed to write config file: ${filePath}`, error);
|
|
211
|
+
throw new Error('Failed to write OpenCode configuration');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getJsonEntrySource(layers, sectionKey, entryName) {
|
|
216
|
+
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
217
|
+
const customSection = customConfig?.[sectionKey]?.[entryName];
|
|
218
|
+
if (customSection !== undefined) {
|
|
219
|
+
return { section: customSection, config: customConfig, path: paths.customPath, exists: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const projectSection = projectConfig?.[sectionKey]?.[entryName];
|
|
223
|
+
if (projectSection !== undefined) {
|
|
224
|
+
return { section: projectSection, config: projectConfig, path: paths.projectPath, exists: true };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const userSection = userConfig?.[sectionKey]?.[entryName];
|
|
228
|
+
if (userSection !== undefined) {
|
|
229
|
+
return { section: userSection, config: userConfig, path: paths.userPath, exists: true };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { section: null, config: null, path: null, exists: false };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getJsonWriteTarget(layers, preferredScope) {
|
|
236
|
+
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
237
|
+
if (paths.customPath) {
|
|
238
|
+
return { config: customConfig, path: paths.customPath };
|
|
239
|
+
}
|
|
240
|
+
if (preferredScope === AGENT_SCOPE.PROJECT && paths.projectPath) {
|
|
241
|
+
return { config: projectConfig, path: paths.projectPath };
|
|
242
|
+
}
|
|
243
|
+
return { config: userConfig, path: paths.userPath };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============== GIT/WORKTREE HELPERS ==============
|
|
247
|
+
|
|
248
|
+
function getAncestors(startDir, stopDir) {
|
|
249
|
+
if (!startDir) return [];
|
|
250
|
+
const result = [];
|
|
251
|
+
let current = path.resolve(startDir);
|
|
252
|
+
const resolvedStop = stopDir ? path.resolve(stopDir) : null;
|
|
253
|
+
|
|
254
|
+
while (true) {
|
|
255
|
+
result.push(current);
|
|
256
|
+
if (resolvedStop && current === resolvedStop) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
const parent = path.dirname(current);
|
|
260
|
+
if (parent === current) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
current = parent;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function findWorktreeRoot(startDir) {
|
|
270
|
+
if (!startDir) return null;
|
|
271
|
+
let current = path.resolve(startDir);
|
|
272
|
+
|
|
273
|
+
while (true) {
|
|
274
|
+
if (fs.existsSync(path.join(current, '.git'))) {
|
|
275
|
+
return current;
|
|
276
|
+
}
|
|
277
|
+
const parent = path.dirname(current);
|
|
278
|
+
if (parent === current) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
current = parent;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ============== PROMPT FILE HELPERS ==============
|
|
286
|
+
|
|
287
|
+
function isPromptFileReference(value) {
|
|
288
|
+
if (typeof value !== 'string') {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
return PROMPT_FILE_PATTERN.test(value.trim());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function resolvePromptFilePath(reference) {
|
|
295
|
+
const match = typeof reference === 'string' ? reference.trim().match(PROMPT_FILE_PATTERN) : null;
|
|
296
|
+
if (!match) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
let target = match[1].trim();
|
|
300
|
+
if (!target) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (target.startsWith('./')) {
|
|
305
|
+
target = target.slice(2);
|
|
306
|
+
target = path.join(OPENCODE_CONFIG_DIR, target);
|
|
307
|
+
} else if (!path.isAbsolute(target)) {
|
|
308
|
+
target = path.join(OPENCODE_CONFIG_DIR, target);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return target;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function writePromptFile(filePath, content) {
|
|
315
|
+
const dir = path.dirname(filePath);
|
|
316
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
317
|
+
fs.writeFileSync(filePath, content ?? '', 'utf8');
|
|
318
|
+
console.log(`Updated prompt file: ${filePath}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============== SKILL FILE OPERATIONS ==============
|
|
322
|
+
|
|
323
|
+
function walkSkillMdFiles(rootDir) {
|
|
324
|
+
if (!rootDir || !fs.existsSync(rootDir)) return [];
|
|
325
|
+
|
|
326
|
+
const results = [];
|
|
327
|
+
const walk = (dir) => {
|
|
328
|
+
let entries = [];
|
|
329
|
+
try {
|
|
330
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const entry of entries) {
|
|
336
|
+
const fullPath = path.join(dir, entry.name);
|
|
337
|
+
if (entry.isDirectory()) {
|
|
338
|
+
walk(fullPath);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (entry.isFile() && entry.name === 'SKILL.md') {
|
|
342
|
+
results.push(fullPath);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
walk(rootDir);
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function addSkillFromMdFile(skillsMap, skillMdPath, scope, source) {
|
|
352
|
+
let parsed;
|
|
353
|
+
try {
|
|
354
|
+
parsed = parseMdFile(skillMdPath);
|
|
355
|
+
} catch {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const name = typeof parsed.frontmatter?.name === 'string'
|
|
360
|
+
? parsed.frontmatter.name.trim()
|
|
361
|
+
: '';
|
|
362
|
+
const description = typeof parsed.frontmatter?.description === 'string'
|
|
363
|
+
? parsed.frontmatter.description
|
|
364
|
+
: '';
|
|
365
|
+
|
|
366
|
+
if (!name) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
skillsMap.set(name, {
|
|
371
|
+
name,
|
|
372
|
+
path: skillMdPath,
|
|
373
|
+
scope,
|
|
374
|
+
source,
|
|
375
|
+
description,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function resolveSkillSearchDirectories(workingDirectory) {
|
|
380
|
+
const directories = [];
|
|
381
|
+
const pushDir = (dir) => {
|
|
382
|
+
if (!dir) return;
|
|
383
|
+
const resolved = path.resolve(dir);
|
|
384
|
+
if (!directories.includes(resolved)) {
|
|
385
|
+
directories.push(resolved);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
pushDir(OPENCODE_CONFIG_DIR);
|
|
390
|
+
|
|
391
|
+
if (workingDirectory) {
|
|
392
|
+
const worktreeRoot = findWorktreeRoot(workingDirectory) || path.resolve(workingDirectory);
|
|
393
|
+
const projectDirs = getAncestors(workingDirectory, worktreeRoot)
|
|
394
|
+
.map((dir) => path.join(dir, '.opencode'));
|
|
395
|
+
projectDirs.forEach(pushDir);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
pushDir(path.join(os.homedir(), '.opencode'));
|
|
399
|
+
|
|
400
|
+
const customConfigDir = process.env.OPENCODE_CONFIG_DIR
|
|
401
|
+
? path.resolve(process.env.OPENCODE_CONFIG_DIR)
|
|
402
|
+
: null;
|
|
403
|
+
pushDir(customConfigDir);
|
|
404
|
+
|
|
405
|
+
return directories;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function listSkillSupportingFiles(skillDir) {
|
|
409
|
+
if (!fs.existsSync(skillDir)) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const files = [];
|
|
414
|
+
|
|
415
|
+
function walkDir(dir, relativePath = '') {
|
|
416
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
417
|
+
for (const entry of entries) {
|
|
418
|
+
const fullPath = path.join(dir, entry.name);
|
|
419
|
+
const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
420
|
+
|
|
421
|
+
if (entry.isDirectory()) {
|
|
422
|
+
walkDir(fullPath, relPath);
|
|
423
|
+
} else if (entry.name !== 'SKILL.md') {
|
|
424
|
+
files.push({
|
|
425
|
+
name: entry.name,
|
|
426
|
+
path: relPath,
|
|
427
|
+
fullPath: fullPath
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
walkDir(skillDir);
|
|
434
|
+
return files;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function assertPathWithinSkillDir(skillDir, relativePath) {
|
|
438
|
+
const root = fs.realpathSync(skillDir);
|
|
439
|
+
const target = path.resolve(root, relativePath);
|
|
440
|
+
const relative = path.relative(root, target);
|
|
441
|
+
const isWithin = relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
442
|
+
|
|
443
|
+
if (!isWithin) {
|
|
444
|
+
const error = new Error('Access to file denied');
|
|
445
|
+
error.code = 'EACCES';
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return target;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readSkillSupportingFile(skillDir, relativePath) {
|
|
453
|
+
const fullPath = assertPathWithinSkillDir(skillDir, relativePath);
|
|
454
|
+
if (!fs.existsSync(fullPath)) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
return fs.readFileSync(fullPath, 'utf8');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function writeSkillSupportingFile(skillDir, relativePath, content) {
|
|
461
|
+
const fullPath = assertPathWithinSkillDir(skillDir, relativePath);
|
|
462
|
+
const dir = path.dirname(fullPath);
|
|
463
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
464
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function deleteSkillSupportingFile(skillDir, relativePath) {
|
|
468
|
+
const root = fs.realpathSync(skillDir);
|
|
469
|
+
const fullPath = assertPathWithinSkillDir(skillDir, relativePath);
|
|
470
|
+
if (fs.existsSync(fullPath)) {
|
|
471
|
+
fs.unlinkSync(fullPath);
|
|
472
|
+
let parentDir = path.dirname(fullPath);
|
|
473
|
+
while (parentDir !== root) {
|
|
474
|
+
try {
|
|
475
|
+
const entries = fs.readdirSync(parentDir);
|
|
476
|
+
if (entries.length === 0) {
|
|
477
|
+
fs.rmdirSync(parentDir);
|
|
478
|
+
parentDir = path.dirname(parentDir);
|
|
479
|
+
} else {
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export {
|
|
490
|
+
OPENCODE_CONFIG_DIR,
|
|
491
|
+
AGENT_DIR,
|
|
492
|
+
COMMAND_DIR,
|
|
493
|
+
SKILL_DIR,
|
|
494
|
+
CONFIG_FILE,
|
|
495
|
+
CUSTOM_CONFIG_FILE,
|
|
496
|
+
PROMPT_FILE_PATTERN,
|
|
497
|
+
AGENT_SCOPE,
|
|
498
|
+
COMMAND_SCOPE,
|
|
499
|
+
SKILL_SCOPE,
|
|
500
|
+
ensureDirs,
|
|
501
|
+
parseMdFile,
|
|
502
|
+
writeMdFile,
|
|
503
|
+
getProjectConfigCandidates,
|
|
504
|
+
getProjectConfigPath,
|
|
505
|
+
getConfigPaths,
|
|
506
|
+
readConfigFile,
|
|
507
|
+
isPlainObject,
|
|
508
|
+
mergeConfigs,
|
|
509
|
+
readConfigLayers,
|
|
510
|
+
readConfig,
|
|
511
|
+
getConfigForPath,
|
|
512
|
+
writeConfig,
|
|
513
|
+
getJsonEntrySource,
|
|
514
|
+
getJsonWriteTarget,
|
|
515
|
+
getAncestors,
|
|
516
|
+
findWorktreeRoot,
|
|
517
|
+
isPromptFileReference,
|
|
518
|
+
resolvePromptFilePath,
|
|
519
|
+
writePromptFile,
|
|
520
|
+
walkSkillMdFiles,
|
|
521
|
+
addSkillFromMdFile,
|
|
522
|
+
resolveSkillSearchDirectories,
|
|
523
|
+
listSkillSupportingFiles,
|
|
524
|
+
readSkillSupportingFile,
|
|
525
|
+
writeSkillSupportingFile,
|
|
526
|
+
deleteSkillSupportingFile,
|
|
527
|
+
};
|