code-abyss 2.0.7 → 2.0.8

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 (47) hide show
  1. package/README.md +129 -58
  2. package/bin/adapters/claude.js +16 -12
  3. package/bin/adapters/codex.js +110 -37
  4. package/bin/adapters/gemini.js +92 -0
  5. package/bin/install.js +521 -130
  6. package/bin/lib/ccline.js +18 -8
  7. package/bin/lib/gstack-claude.js +164 -0
  8. package/bin/lib/gstack-codex.js +347 -0
  9. package/bin/lib/gstack-gemini.js +140 -0
  10. package/bin/lib/pack-bootstrap.js +92 -0
  11. package/bin/lib/pack-docs.js +61 -0
  12. package/bin/lib/pack-registry.js +400 -0
  13. package/bin/lib/pack-reports.js +87 -0
  14. package/bin/lib/pack-vendor.js +82 -0
  15. package/bin/lib/style-registry.js +29 -7
  16. package/bin/lib/target-registry.js +74 -0
  17. package/bin/lib/utils.js +69 -6
  18. package/bin/lib/vendor-providers/archive.js +56 -0
  19. package/bin/lib/vendor-providers/git.js +59 -0
  20. package/bin/lib/vendor-providers/index.js +91 -0
  21. package/bin/lib/vendor-providers/local-dir.js +55 -0
  22. package/bin/lib/vendor-providers/shared.js +86 -0
  23. package/bin/packs.js +581 -0
  24. package/config/AGENTS.md +0 -2
  25. package/config/CLAUDE.md +43 -326
  26. package/config/codex-config.example.toml +26 -20
  27. package/config/gemini-settings.example.json +6 -0
  28. package/config/instruction.md +87 -0
  29. package/output-styles/abyss-command.md +7 -30
  30. package/output-styles/abyss-concise.md +11 -52
  31. package/output-styles/abyss-cultivator.md +20 -249
  32. package/output-styles/abyss-ritual.md +11 -45
  33. package/output-styles/index.json +20 -4
  34. package/package.json +26 -14
  35. package/packs/abyss/manifest.json +25 -0
  36. package/packs/gstack/manifest.json +151 -0
  37. package/skills/SKILL.md +33 -237
  38. package/skills/domains/frontend-design/agents/openai.yaml +1 -1
  39. package/skills/run_skill.js +25 -3
  40. package/skills/tools/gen-docs/agents/openai.yaml +1 -1
  41. package/skills/tools/verify-change/agents/openai.yaml +1 -1
  42. package/skills/tools/verify-module/agents/openai.yaml +1 -1
  43. package/skills/tools/verify-quality/agents/openai.yaml +1 -1
  44. package/skills/tools/verify-security/agents/openai.yaml +1 -1
  45. package/skills/tools/override-refusal/SKILL.md +0 -53
  46. package/skills/tools/override-refusal/agents/openai.yaml +0 -4
  47. package/skills/tools/override-refusal/scripts/refusal_rewriter.js +0 -226
package/bin/lib/ccline.js CHANGED
@@ -2,12 +2,17 @@
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
 
5
- function detectCclineBin(cclineBin) {
5
+ function detectCclineBin(cclineBin, warn) {
6
6
  if (fs.existsSync(cclineBin)) return true;
7
7
  try {
8
8
  require('child_process').execSync('ccline --version', { stdio: 'pipe' });
9
9
  return true;
10
- } catch { return false; }
10
+ } catch (e) {
11
+ if (e.code !== 'ENOENT' && warn) {
12
+ warn(`ccline --version 检测异常: ${e.message}`);
13
+ }
14
+ return false;
15
+ }
11
16
  }
12
17
 
13
18
  function installCclineBin(cclineBin, errors, { info, ok }) {
@@ -21,10 +26,10 @@ function installCclineBin(cclineBin, errors, { info, ok }) {
21
26
  ok('ccline 安装成功 (全局)');
22
27
  return true;
23
28
  } catch {}
24
- errors.push('ccline 二进制安装后仍未检测到');
29
+ errors.push('ccline 二进制安装后仍未检测到. Try: sudo npm install -g @cometix/ccline@1');
25
30
  return false;
26
31
  } catch (e) {
27
- errors.push(`npm install -g @cometix/ccline 失败: ${e.message}`);
32
+ errors.push(`npm install -g @cometix/ccline 失败: ${e.message}. Try: sudo npm install -g @cometix/ccline@1`);
28
33
  return false;
29
34
  }
30
35
  }
@@ -56,15 +61,20 @@ async function installCcline(ctx, deps) {
56
61
  const cclineBin = path.join(cclineDir, process.platform === 'win32' ? 'ccline.exe' : 'ccline');
57
62
  const errors = [];
58
63
 
59
- let hasBin = detectCclineBin(cclineBin);
64
+ let hasBin = detectCclineBin(cclineBin, warn);
60
65
  if (!hasBin) hasBin = installCclineBin(cclineBin, errors, { info, ok });
61
66
  else ok('ccline 二进制已存在');
62
67
 
63
68
  deployCclineConfig(cclineDir, errors, { HOME, PKG_ROOT, ok });
64
69
 
65
- ctx.settings.statusLine = CCLINE_STATUS_LINE.statusLine;
66
- ok(`statusLine → ${c.cyn(CCLINE_STATUS_LINE.statusLine.command)}`);
67
- fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
70
+ // Only set statusLine if ccline binary was successfully detected/installed
71
+ if (hasBin) {
72
+ ctx.settings.statusLine = CCLINE_STATUS_LINE.statusLine;
73
+ ok(`statusLine → ${c.cyn(CCLINE_STATUS_LINE.statusLine.command)}`);
74
+ fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
75
+ } else {
76
+ warn('跳过 statusLine 配置: ccline 二进制不可用');
77
+ }
68
78
 
69
79
  if (errors.length > 0) {
70
80
  console.log('');
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const {
7
+ copyRecursive,
8
+ rmSafe,
9
+ } = require('./utils');
10
+ const {
11
+ getGstackConfig,
12
+ extractNameAndDescription,
13
+ listTopLevelSkillDirs,
14
+ resolveGstackSource,
15
+ condenseDescription,
16
+ copySkillRuntimeFiles,
17
+ } = require('./gstack-codex');
18
+
19
+ function readFrontmatterBlock(content) {
20
+ const normalized = String(content || '').replace(/\r\n/g, '\n');
21
+ const fmStart = normalized.indexOf('---\n');
22
+ if (fmStart !== 0) return null;
23
+ const fmEnd = normalized.indexOf('\n---', fmStart + 4);
24
+ if (fmEnd === -1) return null;
25
+ return normalized.slice(fmStart + 4, fmEnd);
26
+ }
27
+
28
+ function extractAllowedTools(content) {
29
+ const block = readFrontmatterBlock(content);
30
+ if (!block) return 'Read';
31
+
32
+ const lines = block.split('\n');
33
+ const tools = [];
34
+ let inTools = false;
35
+
36
+ for (const line of lines) {
37
+ if (/^allowed-tools:\s*$/.test(line)) {
38
+ inTools = true;
39
+ continue;
40
+ }
41
+ if (inTools) {
42
+ const item = line.match(/^\s*-\s+(.+)$/);
43
+ if (item) {
44
+ tools.push(item[1].trim());
45
+ continue;
46
+ }
47
+ if (!/^\s/.test(line)) break;
48
+ }
49
+ }
50
+
51
+ return tools.length > 0 ? tools.join(', ') : 'Read';
52
+ }
53
+
54
+ function buildClaudeCommand(name, description, allowedTools, skillPath) {
55
+ const escapedDescription = condenseDescription(description, 180).replace(/"/g, '\\"');
56
+ return [
57
+ '---',
58
+ `name: ${name}`,
59
+ `description: "${escapedDescription}"`,
60
+ `allowed-tools: ${allowedTools || 'Read'}`,
61
+ '---',
62
+ '',
63
+ '读取以下秘典,根据内容为用户提供专业指导:',
64
+ '',
65
+ '```',
66
+ skillPath,
67
+ '```',
68
+ '',
69
+ ].join('\n');
70
+ }
71
+
72
+ function backupPathIfExists(targetPath, backupPath, manifest, rootName, relPath, info) {
73
+ if (!fs.existsSync(targetPath)) return false;
74
+ rmSafe(backupPath);
75
+ copyRecursive(targetPath, backupPath);
76
+ manifest.backups.push({ root: rootName, path: relPath });
77
+ info(`备份: ${rootName}/${relPath}`);
78
+ return true;
79
+ }
80
+
81
+ function installGstackClaudePack({
82
+ HOME,
83
+ backupDir,
84
+ manifest,
85
+ info = () => {},
86
+ ok = () => {},
87
+ warn = () => {},
88
+ env = process.env,
89
+ sourceMode = 'pinned',
90
+ projectRoot = null,
91
+ fallback = false,
92
+ }) {
93
+ const resolved = resolveGstackSource({ HOME, env, warn, sourceMode, projectRoot, hostName: 'claude', fallback });
94
+ const sourceRoot = resolved.sourceRoot;
95
+ if (!sourceRoot) return { installed: false, sourceMode: resolved.mode, reason: resolved.reason };
96
+
97
+ const config = getGstackConfig('claude');
98
+ const skillRoot = path.join(HOME, '.claude', 'skills', 'gstack');
99
+ const commandsRoot = path.join(HOME, '.claude', 'commands');
100
+
101
+ backupPathIfExists(
102
+ skillRoot,
103
+ path.join(backupDir, 'claude', 'skills', 'gstack'),
104
+ manifest,
105
+ 'claude',
106
+ 'skills/gstack',
107
+ info
108
+ );
109
+
110
+ rmSafe(skillRoot);
111
+ fs.mkdirSync(skillRoot, { recursive: true });
112
+ fs.mkdirSync(commandsRoot, { recursive: true });
113
+
114
+ config.runtimeDirs.forEach((dirName) => {
115
+ const src = path.join(sourceRoot, dirName);
116
+ if (!fs.existsSync(src)) return;
117
+ copyRecursive(src, path.join(skillRoot, dirName));
118
+ });
119
+
120
+ config.runtimeFiles.forEach((fileName) => {
121
+ const src = path.join(sourceRoot, fileName);
122
+ if (!fs.existsSync(src)) return;
123
+ copyRecursive(src, path.join(skillRoot, fileName));
124
+ });
125
+
126
+ listTopLevelSkillDirs(sourceRoot, 'claude').forEach((skillDirName) => {
127
+ const srcDir = path.join(sourceRoot, skillDirName);
128
+ const destDir = path.join(skillRoot, skillDirName);
129
+ copySkillRuntimeFiles(srcDir, destDir);
130
+
131
+ const content = fs.readFileSync(path.join(srcDir, 'SKILL.md'), 'utf8');
132
+ const { name, description } = extractNameAndDescription(content);
133
+ const allowedTools = extractAllowedTools(content);
134
+ const skillPath = `~/.claude/skills/gstack/${skillDirName}/SKILL.md`;
135
+
136
+ const commandNames = [name, ...((config.commandAliases && config.commandAliases[name]) || [])];
137
+ commandNames.forEach((commandName) => {
138
+ const relPath = `commands/${commandName}.md`;
139
+ backupPathIfExists(
140
+ path.join(commandsRoot, `${commandName}.md`),
141
+ path.join(backupDir, 'claude', relPath),
142
+ manifest,
143
+ 'claude',
144
+ relPath,
145
+ info
146
+ );
147
+ fs.writeFileSync(
148
+ path.join(commandsRoot, `${commandName}.md`),
149
+ buildClaudeCommand(commandName, description, allowedTools, skillPath)
150
+ );
151
+ manifest.installed.push({ root: 'claude', path: relPath });
152
+ });
153
+ });
154
+
155
+ manifest.installed.push({ root: 'claude', path: 'skills/gstack' });
156
+ ok('claude/skills/gstack (gstack runtime)');
157
+ return { installed: true, sourceMode: resolved.mode, reason: null };
158
+ }
159
+
160
+ module.exports = {
161
+ extractAllowedTools,
162
+ buildClaudeCommand,
163
+ installGstackClaudePack,
164
+ };
@@ -0,0 +1,347 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ const {
8
+ copyRecursive,
9
+ rmSafe,
10
+ } = require('./utils');
11
+ const { getPack } = require('./pack-registry');
12
+
13
+ const PROJECT_ROOT = path.join(__dirname, '..', '..');
14
+ const GSTACK_RUNTIME_EXTRA_DIRS = new Set(['references', 'templates', 'specialists', 'bin', 'migrations', 'vendor']);
15
+ const GSTACK_FRONTMATTER_DESCRIPTION_LIMIT = 240;
16
+
17
+ function normalizeEol(content) {
18
+ return String(content || '').replace(/\r\n/g, '\n');
19
+ }
20
+
21
+ function getGstackConfig(hostName = 'codex', projectRoot = PROJECT_ROOT) {
22
+ const manifest = getPack(projectRoot, 'gstack');
23
+ const hostConfig = manifest.hosts && manifest.hosts[hostName];
24
+ if (!manifest.upstream || !hostConfig) {
25
+ throw new Error(`gstack pack manifest 缺少 upstream 或 ${hostName} host 配置`);
26
+ }
27
+ return {
28
+ upstream: manifest.upstream,
29
+ sourceOverrideEnv: hostConfig.sourceOverrideEnv || 'CODE_ABYSS_GSTACK_SOURCE',
30
+ skipSkills: new Set(hostConfig.skipSkills || []),
31
+ runtimeDirs: hostConfig.runtimeDirs || [],
32
+ runtimeFiles: hostConfig.runtimeFiles || [],
33
+ pathRewrites: hostConfig.pathRewrites || [],
34
+ commandAliases: hostConfig.commandAliases || {},
35
+ };
36
+ }
37
+
38
+ function readFrontmatterBlock(content) {
39
+ const normalized = normalizeEol(content);
40
+ const fmStart = normalized.indexOf('---\n');
41
+ if (fmStart !== 0) return null;
42
+ const fmEnd = normalized.indexOf('\n---', fmStart + 4);
43
+ if (fmEnd === -1) return null;
44
+ return {
45
+ raw: normalized.slice(fmStart + 4, fmEnd),
46
+ body: normalized.slice(fmEnd + 4),
47
+ };
48
+ }
49
+
50
+ function extractNameAndDescription(content) {
51
+ const block = readFrontmatterBlock(content);
52
+ if (!block) return { name: '', description: '' };
53
+
54
+ const nameMatch = block.raw.match(/^name:\s*(.+)$/m);
55
+ const name = nameMatch ? nameMatch[1].trim() : '';
56
+
57
+ let description = '';
58
+ const lines = block.raw.split('\n');
59
+ let inDescription = false;
60
+ const descLines = [];
61
+
62
+ for (const line of lines) {
63
+ if (/^description:\s*\|?\s*$/.test(line)) {
64
+ inDescription = true;
65
+ continue;
66
+ }
67
+ if (/^description:\s*\S/.test(line)) {
68
+ description = line.replace(/^description:\s*/, '').trim();
69
+ break;
70
+ }
71
+ if (inDescription) {
72
+ if (line === '' || /^\s/.test(line)) {
73
+ descLines.push(line.replace(/^ /, ''));
74
+ } else {
75
+ break;
76
+ }
77
+ }
78
+ }
79
+
80
+ if (descLines.length > 0) description = descLines.join('\n').trim();
81
+ return { name, description };
82
+ }
83
+
84
+ function buildCodexFrontmatter(name, description) {
85
+ const safeName = name.trim();
86
+ const safeDesc = condenseDescription(description, GSTACK_FRONTMATTER_DESCRIPTION_LIMIT);
87
+ const indented = safeDesc.split('\n').map((line) => ` ${line}`).join('\n');
88
+ return `---\nname: ${safeName}\ndescription: |\n${indented}\n---`;
89
+ }
90
+
91
+ function condenseDescription(description, limit) {
92
+ const firstParagraph = description.split(/\n\s*\n/)[0] || description;
93
+ const collapsed = firstParagraph.replace(/\s+/g, ' ').trim();
94
+ if (collapsed.length <= limit) return collapsed;
95
+ const truncated = collapsed.slice(0, limit - 3);
96
+ const lastSpace = truncated.lastIndexOf(' ');
97
+ const safe = lastSpace > 40 ? truncated.slice(0, lastSpace) : truncated;
98
+ return `${safe}...`;
99
+ }
100
+
101
+ function injectRuntimeRootPreamble(content) {
102
+ const marker = '```bash\n';
103
+ const idx = content.indexOf(marker);
104
+ if (idx === -1 || content.includes('GSTACK_ROOT=')) return content;
105
+
106
+ const injected = [
107
+ '_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)',
108
+ 'GSTACK_ROOT="$HOME/.agents/skills/gstack"',
109
+ '[ -n "$_ROOT" ] && [ -d "$_ROOT/.agents/skills/gstack" ] && GSTACK_ROOT="$_ROOT/.agents/skills/gstack"',
110
+ 'GSTACK_BIN="$GSTACK_ROOT/bin"',
111
+ 'GSTACK_BROWSE="$GSTACK_ROOT/browse/dist"',
112
+ 'GSTACK_DESIGN="$GSTACK_ROOT/design/dist"',
113
+ ].join('\n');
114
+
115
+ return `${content.slice(0, idx + marker.length)}${injected}\n${content.slice(idx + marker.length)}`;
116
+ }
117
+
118
+ function rewritePaths(content) {
119
+ return getGstackConfig().pathRewrites.reduce((current, [from, to]) => current.split(from).join(to), content);
120
+ }
121
+
122
+ function transformGstackSkillContent(content) {
123
+ const parsed = extractNameAndDescription(content);
124
+ if (!parsed.name || !parsed.description) {
125
+ throw new Error('gstack skill frontmatter 缺少 name 或 description');
126
+ }
127
+
128
+ const block = readFrontmatterBlock(content);
129
+ const stripped = `${buildCodexFrontmatter(parsed.name, parsed.description)}${block ? block.body : content}`;
130
+ return injectRuntimeRootPreamble(rewritePaths(stripped));
131
+ }
132
+
133
+ function listTopLevelSkillDirs(sourceRoot, hostName = 'codex') {
134
+ const config = getGstackConfig(hostName);
135
+ const entries = fs.readdirSync(sourceRoot, { withFileTypes: true });
136
+ return entries
137
+ .filter((entry) => entry.isDirectory())
138
+ .map((entry) => entry.name)
139
+ .filter((name) => fs.existsSync(path.join(sourceRoot, name, 'SKILL.md')))
140
+ .filter((name) => !config.skipSkills.has(name))
141
+ .sort();
142
+ }
143
+
144
+ function ensureDir(p) {
145
+ fs.mkdirSync(p, { recursive: true });
146
+ }
147
+
148
+ function resolveLocalGstackSource(projectRoot, env, sourceOverrideEnv) {
149
+ if (env[sourceOverrideEnv]) return env[sourceOverrideEnv];
150
+ if (!projectRoot) return null;
151
+
152
+ const candidates = [
153
+ path.join(projectRoot, '.code-abyss', 'vendor', 'gstack'),
154
+ path.join(projectRoot, 'vendor', 'gstack'),
155
+ ];
156
+
157
+ return candidates.find((candidate) => fs.existsSync(candidate)) || null;
158
+ }
159
+
160
+ function backupExistingGstack(destPath, backupDir, manifest, info) {
161
+ if (!fs.existsSync(destPath)) return false;
162
+
163
+ const backupPath = path.join(backupDir, 'agents', 'skills', 'gstack');
164
+ rmSafe(backupPath);
165
+ copyRecursive(destPath, backupPath);
166
+ manifest.backups.push({ root: 'agents', path: 'skills/gstack' });
167
+ info('备份: agents/skills/gstack');
168
+ return true;
169
+ }
170
+
171
+ function syncPinnedRepo(targetDir) {
172
+ const { upstream } = getGstackConfig();
173
+ rmSafe(targetDir);
174
+ ensureDir(path.dirname(targetDir));
175
+
176
+ const clone = spawnSync('git', ['clone', '--depth', '1', upstream.repo, targetDir], {
177
+ encoding: 'utf8',
178
+ });
179
+ if (clone.status !== 0) {
180
+ throw new Error(`gstack clone 失败: ${(clone.stderr || clone.stdout || '').trim()}`);
181
+ }
182
+
183
+ const checkout = spawnSync('git', ['-C', targetDir, 'fetch', '--depth', '1', 'origin', upstream.commit], {
184
+ encoding: 'utf8',
185
+ });
186
+ if (checkout.status !== 0) {
187
+ throw new Error(`gstack fetch commit 失败: ${(checkout.stderr || checkout.stdout || '').trim()}`);
188
+ }
189
+
190
+ const detach = spawnSync('git', ['-C', targetDir, 'checkout', '--detach', upstream.commit], {
191
+ encoding: 'utf8',
192
+ });
193
+ if (detach.status !== 0) {
194
+ throw new Error(`gstack checkout 失败: ${(detach.stderr || detach.stdout || '').trim()}`);
195
+ }
196
+ }
197
+
198
+ function ensurePinnedGstackSource({ HOME, env = process.env, warn = () => {} }) {
199
+ const config = getGstackConfig();
200
+ const override = env[config.sourceOverrideEnv];
201
+ if (override) return override;
202
+
203
+ const cacheDir = path.join(HOME, '.code-abyss', 'vendor', `gstack-${config.upstream.commit.slice(0, 12)}`);
204
+ const versionFile = path.join(cacheDir, '.code-abyss-source-version');
205
+
206
+ try {
207
+ if (!fs.existsSync(cacheDir) || !fs.existsSync(versionFile) || fs.readFileSync(versionFile, 'utf8').trim() !== config.upstream.commit) {
208
+ syncPinnedRepo(cacheDir);
209
+ fs.writeFileSync(versionFile, `${config.upstream.commit}\n`);
210
+ }
211
+ return cacheDir;
212
+ } catch (error) {
213
+ warn(`获取 gstack 失败,跳过自动融合: ${error.message}`);
214
+ return null;
215
+ }
216
+ }
217
+
218
+ function resolveGstackSource({
219
+ HOME,
220
+ env = process.env,
221
+ warn = () => {},
222
+ sourceMode = 'pinned',
223
+ projectRoot = null,
224
+ hostName = 'codex',
225
+ fallback = false,
226
+ }) {
227
+ const config = getGstackConfig(hostName);
228
+ if (sourceMode === 'disabled') return { sourceRoot: null, mode: 'disabled', reason: 'disabled' };
229
+
230
+ const localRoot = resolveLocalGstackSource(projectRoot, env, config.sourceOverrideEnv);
231
+
232
+ if (sourceMode === 'local') {
233
+ if (!localRoot) {
234
+ if (fallback) {
235
+ const pinnedRoot = ensurePinnedGstackSource({ HOME, env, warn });
236
+ if (pinnedRoot) {
237
+ return { sourceRoot: pinnedRoot, mode: 'pinned', reason: 'fallback-local-to-pinned' };
238
+ }
239
+ }
240
+ warn('gstack source=local,但未找到本地源(.code-abyss/vendor/gstack 或 env override)');
241
+ return { sourceRoot: null, mode: 'local', reason: 'missing-local-source' };
242
+ }
243
+ return { sourceRoot: localRoot, mode: 'local', reason: null };
244
+ }
245
+
246
+ const pinnedRoot = ensurePinnedGstackSource({ HOME, env, warn });
247
+ if (pinnedRoot) return { sourceRoot: pinnedRoot, mode: 'pinned', reason: null };
248
+ if (fallback && localRoot) {
249
+ return { sourceRoot: localRoot, mode: 'local', reason: 'fallback-pinned-to-local' };
250
+ }
251
+ return { sourceRoot: null, mode: 'pinned', reason: 'fetch-failed' };
252
+ }
253
+
254
+ function copyRuntimeAssets(sourceRoot, destRoot) {
255
+ const config = getGstackConfig();
256
+
257
+ config.runtimeDirs.forEach((dirName) => {
258
+ const src = path.join(sourceRoot, dirName);
259
+ if (!fs.existsSync(src)) return;
260
+ copyRecursive(src, path.join(destRoot, dirName));
261
+ });
262
+
263
+ config.runtimeFiles.forEach((fileName) => {
264
+ const src = path.join(sourceRoot, fileName);
265
+ if (!fs.existsSync(src)) return;
266
+ copyRecursive(src, path.join(destRoot, fileName));
267
+ });
268
+ }
269
+
270
+ function copySkillRuntimeFiles(sourceSkillDir, destSkillDir, { transformSkill = null } = {}) {
271
+ ensureDir(destSkillDir);
272
+ rmSafe(path.join(destSkillDir, 'SKILL.md.tmpl'));
273
+ const sourceSkillPath = path.join(sourceSkillDir, 'SKILL.md');
274
+ if (!fs.existsSync(sourceSkillPath)) return;
275
+
276
+ const content = fs.readFileSync(sourceSkillPath, 'utf8');
277
+ fs.writeFileSync(
278
+ path.join(destSkillDir, 'SKILL.md'),
279
+ typeof transformSkill === 'function' ? transformSkill(content) : content
280
+ );
281
+
282
+ fs.readdirSync(sourceSkillDir, { withFileTypes: true }).forEach((entry) => {
283
+ if (entry.name === 'SKILL.md' || entry.name === 'SKILL.md.tmpl') return;
284
+ if (!entry.isDirectory()) return;
285
+ if (!GSTACK_RUNTIME_EXTRA_DIRS.has(entry.name)) return;
286
+ copyRecursive(path.join(sourceSkillDir, entry.name), path.join(destSkillDir, entry.name));
287
+ });
288
+ }
289
+
290
+ function installNestedSkill(sourceSkillDir, destSkillDir) {
291
+ copySkillRuntimeFiles(sourceSkillDir, destSkillDir, {
292
+ transformSkill: transformGstackSkillContent,
293
+ });
294
+ }
295
+
296
+ function installGstackCodexPack({
297
+ HOME,
298
+ backupDir,
299
+ manifest,
300
+ info = () => {},
301
+ ok = () => {},
302
+ warn = () => {},
303
+ env = process.env,
304
+ sourceMode = 'pinned',
305
+ projectRoot = null,
306
+ fallback = false,
307
+ }) {
308
+ const resolved = resolveGstackSource({ HOME, env, warn, sourceMode, projectRoot, hostName: 'codex', fallback });
309
+ const sourceRoot = resolved.sourceRoot;
310
+ if (!sourceRoot) return { installed: false, sourceMode: resolved.mode, reason: resolved.reason };
311
+ const config = getGstackConfig();
312
+
313
+ const destRoot = path.join(HOME, '.agents', 'skills', 'gstack');
314
+ backupExistingGstack(destRoot, backupDir, manifest, info);
315
+
316
+ rmSafe(destRoot);
317
+ ensureDir(destRoot);
318
+
319
+ const rootSkillPath = path.join(sourceRoot, 'SKILL.md');
320
+ if (!fs.existsSync(rootSkillPath)) {
321
+ warn('gstack 根技能缺失,跳过自动融合');
322
+ return { installed: false, sourceMode: resolved.mode, reason: 'missing-root-skill' };
323
+ }
324
+
325
+ fs.writeFileSync(path.join(destRoot, 'SKILL.md'), transformGstackSkillContent(fs.readFileSync(rootSkillPath, 'utf8')));
326
+ copyRuntimeAssets(sourceRoot, destRoot);
327
+
328
+ listTopLevelSkillDirs(sourceRoot).forEach((skillDir) => {
329
+ installNestedSkill(path.join(sourceRoot, skillDir), path.join(destRoot, skillDir));
330
+ });
331
+
332
+ manifest.installed.push({ root: 'agents', path: 'skills/gstack' });
333
+ ok(`agents/skills/gstack ${config.upstream.version ? `(gstack ${config.upstream.version})` : ''}`);
334
+ return { installed: true, sourceMode: resolved.mode, reason: null };
335
+ }
336
+
337
+ module.exports = {
338
+ getGstackConfig,
339
+ extractNameAndDescription,
340
+ condenseDescription,
341
+ transformGstackSkillContent,
342
+ listTopLevelSkillDirs,
343
+ ensurePinnedGstackSource,
344
+ resolveGstackSource,
345
+ copySkillRuntimeFiles,
346
+ installGstackCodexPack,
347
+ };