code-abyss 2.0.6 → 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 -81
  46. package/skills/tools/override-refusal/agents/openai.yaml +0 -4
  47. package/skills/tools/override-refusal/scripts/refusal_rewriter.js +0 -340
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function ensureReportsDir(projectRoot) {
7
+ const dir = path.join(projectRoot, '.code-abyss', 'reports');
8
+ fs.mkdirSync(dir, { recursive: true });
9
+ return dir;
10
+ }
11
+
12
+ function safeStamp(date = new Date()) {
13
+ return date.toISOString().replace(/[:]/g, '-');
14
+ }
15
+
16
+ function writeReportArtifact(projectRoot, kind, payload) {
17
+ const reportsDir = ensureReportsDir(projectRoot);
18
+ const fileName = `${kind}-${safeStamp()}.json`;
19
+ const filePath = path.join(reportsDir, fileName);
20
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
21
+ return filePath;
22
+ }
23
+
24
+ function listReportArtifacts(projectRoot, kindPrefix = null) {
25
+ const reportsDir = ensureReportsDir(projectRoot);
26
+ return fs.readdirSync(reportsDir)
27
+ .filter((name) => name.endsWith('.json'))
28
+ .filter((name) => !kindPrefix || name.startsWith(kindPrefix))
29
+ .map((name) => {
30
+ const filePath = path.join(reportsDir, name);
31
+ const stat = fs.statSync(filePath);
32
+ return {
33
+ name,
34
+ path: filePath,
35
+ mtimeMs: stat.mtimeMs,
36
+ };
37
+ })
38
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
39
+ }
40
+
41
+ function readLatestReportArtifact(projectRoot, kindPrefix = null) {
42
+ const reports = listReportArtifacts(projectRoot, kindPrefix);
43
+ if (reports.length === 0) return null;
44
+ const latest = reports[0];
45
+ return {
46
+ ...latest,
47
+ data: JSON.parse(fs.readFileSync(latest.path, 'utf8')),
48
+ };
49
+ }
50
+
51
+ function deriveReportKind(name) {
52
+ return name.replace(/-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z\.json$/, '');
53
+ }
54
+
55
+ function summarizeReportArtifacts(projectRoot, kindPrefix = null) {
56
+ const reports = listReportArtifacts(projectRoot, kindPrefix);
57
+ const seen = new Set();
58
+ const summary = [];
59
+
60
+ reports.forEach((report) => {
61
+ const kind = deriveReportKind(report.name);
62
+ if (seen.has(kind)) return;
63
+ seen.add(kind);
64
+
65
+ const data = JSON.parse(fs.readFileSync(report.path, 'utf8'));
66
+ summary.push({
67
+ kind,
68
+ name: report.name,
69
+ path: report.path,
70
+ mtimeMs: report.mtimeMs,
71
+ target: data.target || null,
72
+ pack: data.pack || null,
73
+ packReports: data.pack_reports || [],
74
+ reports: data.reports || [],
75
+ });
76
+ });
77
+
78
+ return summary;
79
+ }
80
+
81
+ module.exports = {
82
+ ensureReportsDir,
83
+ writeReportArtifact,
84
+ listReportArtifacts,
85
+ readLatestReportArtifact,
86
+ summarizeReportArtifacts,
87
+ };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { getPack } = require('./pack-registry');
4
+ const { shared, getVendorProvider } = require('./vendor-providers');
5
+
6
+ function getPackVendorDir(projectRoot, packName) {
7
+ return shared.path.join(projectRoot, '.code-abyss', 'vendor', packName);
8
+ }
9
+
10
+ function ensurePackUpstream(projectRoot, packName) {
11
+ const pack = getPack(projectRoot, packName);
12
+ if (!pack.upstream) {
13
+ throw new Error(`pack ${packName} 未声明 upstream`);
14
+ }
15
+ const upstream = {
16
+ provider: pack.upstream.provider || 'git',
17
+ ...pack.upstream,
18
+ };
19
+ getVendorProvider(upstream.provider, projectRoot).validate(upstream);
20
+ return upstream;
21
+ }
22
+
23
+ function syncPackVendor(projectRoot, packName) {
24
+ const upstream = ensurePackUpstream(projectRoot, packName);
25
+ const vendorDir = getPackVendorDir(projectRoot, packName);
26
+ shared.fs.mkdirSync(shared.path.dirname(vendorDir), { recursive: true });
27
+ const provider = getVendorProvider(upstream.provider, projectRoot);
28
+ return provider.sync({ projectRoot, upstream, vendorDir, shared, packName });
29
+ }
30
+
31
+ function removePackVendor(projectRoot, packName) {
32
+ const vendorDir = getPackVendorDir(projectRoot, packName);
33
+ if (!shared.fs.existsSync(vendorDir)) {
34
+ return { pack: packName, removed: false, vendorDir };
35
+ }
36
+ shared.fs.rmSync(vendorDir, { recursive: true, force: true });
37
+ return { pack: packName, removed: true, vendorDir };
38
+ }
39
+
40
+ function readVendorMetadata(projectRoot, packName) {
41
+ const vendorDir = getPackVendorDir(projectRoot, packName);
42
+ const metaPath = shared.path.join(vendorDir, '.code-abyss-vendor.json');
43
+ if (!shared.fs.existsSync(metaPath)) return null;
44
+ return JSON.parse(shared.fs.readFileSync(metaPath, 'utf8'));
45
+ }
46
+
47
+ function getPackVendorStatus(projectRoot, packName) {
48
+ const upstream = ensurePackUpstream(projectRoot, packName);
49
+ const vendorDir = getPackVendorDir(projectRoot, packName);
50
+ const provider = upstream.provider || 'git';
51
+ const exists = shared.fs.existsSync(vendorDir);
52
+
53
+ if (!exists) {
54
+ return {
55
+ pack: packName,
56
+ provider,
57
+ vendorDir,
58
+ exists: false,
59
+ dirty: false,
60
+ drifted: false,
61
+ currentCommit: null,
62
+ targetCommit: upstream.commit || null,
63
+ sourceExists: false,
64
+ metadata: null,
65
+ };
66
+ }
67
+
68
+ const metadata = readVendorMetadata(projectRoot, packName);
69
+ return getVendorProvider(provider, projectRoot).status({ projectRoot, upstream, vendorDir, shared, packName, metadata });
70
+ }
71
+
72
+ module.exports = {
73
+ getPackVendorDir,
74
+ ensurePackUpstream,
75
+ syncPackVendor,
76
+ removePackVendor,
77
+ readVendorMetadata,
78
+ getPackVendorStatus,
79
+ resolveUpstreamPath: shared.resolveUpstreamPath,
80
+ hashDirectory: shared.hashDirectory,
81
+ hashFile: shared.hashFile,
82
+ };
@@ -2,16 +2,26 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { listTargetNames } = require('./target-registry');
5
6
 
6
- const SUPPORTED_TARGETS = new Set(['claude', 'codex']);
7
+ const SUPPORTED_TARGETS = new Set(listTargetNames());
8
+
9
+ // Module-level cache: projectRoot → normalized styles
10
+ const _cache = new Map();
11
+
12
+ function clearStyleCache() {
13
+ _cache.clear();
14
+ }
7
15
 
8
16
  function loadStyleRegistry(projectRoot) {
17
+ if (_cache.has(projectRoot)) return _cache.get(projectRoot);
18
+
9
19
  const registryPath = path.join(projectRoot, 'output-styles', 'index.json');
10
20
  const raw = fs.readFileSync(registryPath, 'utf8');
11
21
  const parsed = JSON.parse(raw);
12
22
  const styles = Array.isArray(parsed.styles) ? parsed.styles : null;
13
23
  if (!styles || styles.length === 0) {
14
- throw new Error('output-styles/index.json 缺少 styles 列表');
24
+ throw new Error('output-styles/index.json 缺少 styles 列表. Check output-styles/index.json has a "styles" array');
15
25
  }
16
26
 
17
27
  const seen = new Set();
@@ -22,9 +32,10 @@ function loadStyleRegistry(projectRoot) {
22
32
  });
23
33
 
24
34
  if (defaultCount !== 1) {
25
- throw new Error('style registry 必须且只能有一个 default style');
35
+ throw new Error('style registry 必须且只能有一个 default style. Check output-styles/index.json — exactly one entry must have "default: true"');
26
36
  }
27
37
 
38
+ _cache.set(projectRoot, normalized);
28
39
  return normalized;
29
40
  }
30
41
 
@@ -65,7 +76,7 @@ function requireNonEmptyString(value, fieldName) {
65
76
  }
66
77
 
67
78
  function normalizeTargets(targets, slug) {
68
- const values = Array.isArray(targets) && targets.length > 0 ? targets : ['claude', 'codex'];
79
+ const values = Array.isArray(targets) && targets.length > 0 ? targets : listTargetNames();
69
80
  values.forEach((target) => {
70
81
  if (!SUPPORTED_TARGETS.has(target)) {
71
82
  throw new Error(`style ${slug} 包含不支持的 target: ${target}`);
@@ -99,10 +110,10 @@ function readStyleContent(projectRoot, style) {
99
110
  return fs.readFileSync(stylePath, 'utf8');
100
111
  }
101
112
 
102
- function renderCodexAgents(projectRoot, styleSlug) {
103
- const style = resolveStyle(projectRoot, styleSlug, 'codex');
113
+ function renderRuntimeGuidance(projectRoot, styleSlug, targetName = 'codex') {
114
+ const style = resolveStyle(projectRoot, styleSlug, targetName === 'gemini' ? 'claude' : targetName);
104
115
  if (!style) {
105
- throw new Error(`未知输出风格: ${styleSlug}`);
116
+ throw new Error(`未知输出风格: ${styleSlug}. Try: node bin/install.js --list-styles`);
106
117
  }
107
118
 
108
119
  const basePath = path.join(projectRoot, 'config', 'CLAUDE.md');
@@ -111,9 +122,20 @@ function renderCodexAgents(projectRoot, styleSlug) {
111
122
  return `${base}\n\n${styleContent}\n`;
112
123
  }
113
124
 
125
+ function renderCodexAgents(projectRoot, styleSlug) {
126
+ return renderRuntimeGuidance(projectRoot, styleSlug, 'codex');
127
+ }
128
+
129
+ function renderGeminiContext(projectRoot, styleSlug) {
130
+ return renderRuntimeGuidance(projectRoot, styleSlug, 'gemini');
131
+ }
132
+
114
133
  module.exports = {
115
134
  listStyles,
116
135
  getDefaultStyle,
117
136
  resolveStyle,
118
137
  renderCodexAgents,
138
+ renderGeminiContext,
139
+ renderRuntimeGuidance,
140
+ clearStyleCache,
119
141
  };
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const INSTALL_TARGETS = Object.freeze([
4
+ {
5
+ name: 'claude',
6
+ label: 'Claude Code',
7
+ actionLabel: 'Claude Code',
8
+ homeDir: '.claude',
9
+ },
10
+ {
11
+ name: 'codex',
12
+ label: 'Codex CLI',
13
+ actionLabel: 'Codex CLI',
14
+ homeDir: '.codex',
15
+ },
16
+ {
17
+ name: 'gemini',
18
+ label: 'Gemini CLI',
19
+ actionLabel: 'Gemini CLI',
20
+ homeDir: '.gemini',
21
+ },
22
+ ]);
23
+
24
+ const MANAGED_ROOTS = Object.freeze({
25
+ claude: '.claude',
26
+ codex: '.codex',
27
+ agents: '.agents',
28
+ gemini: '.gemini',
29
+ });
30
+
31
+ function listInstallTargets() {
32
+ return INSTALL_TARGETS.slice();
33
+ }
34
+
35
+ function listTargetNames() {
36
+ return INSTALL_TARGETS.map((target) => target.name);
37
+ }
38
+
39
+ function isSupportedTarget(targetName) {
40
+ return listTargetNames().includes(targetName);
41
+ }
42
+
43
+ function getTargetMeta(targetName) {
44
+ return INSTALL_TARGETS.find((target) => target.name === targetName) || null;
45
+ }
46
+
47
+ function getManagedRootNames() {
48
+ return Object.keys(MANAGED_ROOTS);
49
+ }
50
+
51
+ function isManagedRoot(rootName) {
52
+ return Object.prototype.hasOwnProperty.call(MANAGED_ROOTS, rootName);
53
+ }
54
+
55
+ function getManagedRootRelativeDir(rootName) {
56
+ if (!isManagedRoot(rootName)) {
57
+ throw new Error(`不支持的安装根: ${rootName}`);
58
+ }
59
+ return MANAGED_ROOTS[rootName];
60
+ }
61
+
62
+ function formatTargetList(joiner = '|') {
63
+ return listTargetNames().join(joiner);
64
+ }
65
+
66
+ module.exports = {
67
+ listInstallTargets,
68
+ listTargetNames,
69
+ isSupportedTarget,
70
+ getTargetMeta,
71
+ getManagedRootNames,
72
+ getManagedRootRelativeDir,
73
+ formatTargetList,
74
+ };
package/bin/lib/utils.js CHANGED
@@ -6,18 +6,24 @@ const SKIP = ['__pycache__', '.pyc', '.pyo', '.egg-info', '.DS_Store', 'Thumbs.d
6
6
 
7
7
  function shouldSkip(name) { return SKIP.some(p => name.includes(p)); }
8
8
 
9
- function copyRecursive(src, dest) {
9
+ function copyRecursive(src, dest, errors) {
10
10
  let stat;
11
11
  try { stat = fs.statSync(src); } catch (e) {
12
- throw new Error(`复制失败: 源路径不存在 ${src} (${e.code})`);
12
+ const err = new Error(`复制失败: 源路径不存在 ${src} (${e.code})`);
13
+ if (errors) { errors.push({ src, dest, error: err }); return; }
14
+ throw err;
13
15
  }
14
16
  if (stat.isDirectory()) {
15
17
  if (shouldSkip(path.basename(src))) return;
16
18
  fs.mkdirSync(dest, { recursive: true });
17
19
  for (const f of fs.readdirSync(src)) {
18
20
  if (!shouldSkip(f)) {
19
- try { copyRecursive(path.join(src, f), path.join(dest, f)); }
20
- catch (e) { console.error(` ⚠ 跳过: ${path.join(src, f)} (${e.message})`); }
21
+ try { copyRecursive(path.join(src, f), path.join(dest, f), errors); }
22
+ catch (e) {
23
+ const entry = { src: path.join(src, f), dest: path.join(dest, f), error: e };
24
+ if (errors) { errors.push(entry); }
25
+ else { console.error(` ⚠ 跳过: ${entry.src} (${e.message})`); }
26
+ }
21
27
  }
22
28
  }
23
29
  } else {
@@ -85,9 +91,66 @@ function parseFrontmatter(content) {
85
91
  if (!m) {
86
92
  throw new Error(`frontmatter 第 ${index + 1} 行格式无效: ${rawLine}`);
87
93
  }
88
- if (!UNSAFE_KEYS.has(m[1])) meta[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
94
+ if (UNSAFE_KEYS.has(m[1])) return;
95
+ let value = m[2].trim();
96
+ // Strip inline comments (unquoted # followed by space or end-of-line)
97
+ value = stripInlineComment(value);
98
+ // Handle YAML inline array syntax: [A, B, C] → "A, B, C"
99
+ if (/^\[.+\]$/.test(value)) {
100
+ value = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).join(', ');
101
+ } else {
102
+ value = value.replace(/^["']|["']$/g, '');
103
+ }
104
+ meta[m[1]] = value;
89
105
  });
90
106
  return meta;
91
107
  }
92
108
 
93
- module.exports = { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, parseFrontmatter, SKIP };
109
+ function stripInlineComment(value) {
110
+ // Preserve # inside quotes; strip unquoted # followed by space or EOL
111
+ let inQuote = false;
112
+ let quoteChar = '';
113
+ for (let i = 0; i < value.length; i++) {
114
+ const ch = value[i];
115
+ if (inQuote) {
116
+ if (ch === quoteChar) inQuote = false;
117
+ } else {
118
+ if (ch === '"' || ch === "'") {
119
+ inQuote = true;
120
+ quoteChar = ch;
121
+ } else if (ch === '#' && (i + 1 === value.length || value[i + 1] === ' ')) {
122
+ return value.slice(0, i).trimEnd();
123
+ }
124
+ }
125
+ }
126
+ return value;
127
+ }
128
+
129
+ function formatActionableError(message, suggestion) {
130
+ if (!suggestion) return message;
131
+ return `${message}. ${suggestion}`;
132
+ }
133
+
134
+ /**
135
+ * 轻量模板引擎 — 支持 {{key}} 变量替换 + {{#key}}...{{/key}} 条件包含 + {{^key}}...{{/key}} 反条件包含
136
+ * @param {string} template - 模板字符串
137
+ * @param {Object} data - 变量映射
138
+ * @returns {string} 渲染结果
139
+ */
140
+ function renderTemplate(template, data) {
141
+ // {{^key}}...{{/key}} — 反条件(key 为 falsy 时包含)
142
+ template = template.replace(/\{\{\^(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, body) => {
143
+ return data[key] ? '' : body;
144
+ });
145
+ // {{#key}}...{{/key}} — 正条件(key 为 truthy 时包含)
146
+ template = template.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, body) => {
147
+ return data[key] ? body : '';
148
+ });
149
+ // {{key}} — 变量替换
150
+ template = template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
151
+ return data[key] !== undefined ? String(data[key]) : `{{${key}}}`;
152
+ });
153
+ return template;
154
+ }
155
+
156
+ module.exports = { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, parseFrontmatter, stripInlineComment, formatActionableError, renderTemplate, SKIP };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ name: 'archive',
5
+ validate(upstream) {
6
+ if (!upstream.path) {
7
+ throw new Error('upstream.provider=archive 时必须提供 path');
8
+ }
9
+ },
10
+ sync({ projectRoot, upstream, vendorDir, shared, packName }) {
11
+ const archivePath = shared.resolveUpstreamPath(projectRoot, upstream.path);
12
+ if (!archivePath || !shared.fs.existsSync(archivePath)) {
13
+ throw new Error(`archive 源不存在: ${archivePath || upstream.path}`);
14
+ }
15
+ shared.rmSafe(vendorDir);
16
+ shared.fs.mkdirSync(vendorDir, { recursive: true });
17
+ shared.extractArchive(archivePath, vendorDir);
18
+ shared.writeVendorMetadata(vendorDir, {
19
+ pack: packName,
20
+ provider: 'archive',
21
+ path: upstream.path,
22
+ version: upstream.version || '',
23
+ sourceSignature: shared.hashFile(archivePath),
24
+ vendorSignature: shared.hashDirectory(vendorDir),
25
+ });
26
+ return {
27
+ pack: packName,
28
+ provider: 'archive',
29
+ action: 'updated',
30
+ vendorDir,
31
+ repo: null,
32
+ commit: null,
33
+ version: upstream.version || '',
34
+ };
35
+ },
36
+ status({ projectRoot, upstream, vendorDir, shared, packName, metadata }) {
37
+ const archivePath = shared.resolveUpstreamPath(projectRoot, upstream.path);
38
+ const sourceExists = !!archivePath && shared.fs.existsSync(archivePath);
39
+ const sourceSignature = sourceExists ? shared.hashFile(archivePath) : null;
40
+ const vendorSignature = shared.hashDirectory(vendorDir);
41
+ const dirty = metadata && metadata.vendorSignature ? vendorSignature !== metadata.vendorSignature : true;
42
+
43
+ return {
44
+ pack: packName,
45
+ provider: 'archive',
46
+ vendorDir,
47
+ exists: true,
48
+ dirty,
49
+ drifted: !sourceExists || sourceSignature !== (metadata && metadata.sourceSignature),
50
+ currentCommit: null,
51
+ targetCommit: null,
52
+ sourceExists,
53
+ metadata,
54
+ };
55
+ },
56
+ };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ name: 'git',
5
+ validate(upstream) {
6
+ if (!upstream.repo || !upstream.commit) {
7
+ throw new Error('upstream.provider=git 时必须提供 repo 和 commit');
8
+ }
9
+ },
10
+ sync({ upstream, vendorDir, shared, packName }) {
11
+ let action = shared.fs.existsSync(vendorDir) ? 'updated' : 'cloned';
12
+ if (!shared.fs.existsSync(vendorDir)) {
13
+ shared.runGit(['clone', '--depth', '1', upstream.repo, vendorDir]);
14
+ }
15
+ shared.runGit(['fetch', '--depth', '1', 'origin', upstream.commit], vendorDir);
16
+ shared.runGit(['checkout', '--detach', upstream.commit], vendorDir);
17
+ shared.writeVendorMetadata(vendorDir, {
18
+ pack: packName,
19
+ provider: 'git',
20
+ repo: upstream.repo,
21
+ commit: upstream.commit,
22
+ version: upstream.version || '',
23
+ sourceSignature: upstream.commit,
24
+ vendorSignature: shared.hashDirectory(vendorDir),
25
+ });
26
+ return {
27
+ pack: packName,
28
+ provider: 'git',
29
+ action,
30
+ vendorDir,
31
+ repo: upstream.repo,
32
+ commit: upstream.commit,
33
+ version: upstream.version || '',
34
+ };
35
+ },
36
+ status({ upstream, vendorDir, shared, packName, metadata }) {
37
+ let currentCommit = null;
38
+ try {
39
+ currentCommit = shared.runGit(['rev-parse', 'HEAD'], vendorDir);
40
+ } catch {
41
+ currentCommit = null;
42
+ }
43
+ const vendorSignature = shared.hashDirectory(vendorDir);
44
+ const dirty = metadata && metadata.vendorSignature ? vendorSignature !== metadata.vendorSignature : true;
45
+
46
+ return {
47
+ pack: packName,
48
+ provider: 'git',
49
+ vendorDir,
50
+ exists: true,
51
+ dirty,
52
+ drifted: currentCommit !== upstream.commit,
53
+ currentCommit,
54
+ targetCommit: upstream.commit,
55
+ sourceExists: true,
56
+ metadata,
57
+ };
58
+ },
59
+ };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const shared = require('./shared');
7
+
8
+ const BUILTIN_DIR = __dirname;
9
+ const BUILTIN_SKIP = new Set(['index.js', 'shared.js']);
10
+
11
+ function isProviderFile(name) {
12
+ return name.endsWith('.js') && !BUILTIN_SKIP.has(name);
13
+ }
14
+
15
+ function validateProviderContract(provider, sourcePath) {
16
+ if (!provider || typeof provider !== 'object') {
17
+ throw new Error(`vendor provider 无效: ${sourcePath}`);
18
+ }
19
+ if (typeof provider.name !== 'string' || provider.name.trim() === '') {
20
+ throw new Error(`vendor provider 缺少 name: ${sourcePath}`);
21
+ }
22
+ ['validate', 'sync', 'status'].forEach((fn) => {
23
+ if (typeof provider[fn] !== 'function') {
24
+ throw new Error(`vendor provider 缺少 ${fn}(): ${sourcePath}`);
25
+ }
26
+ });
27
+ }
28
+
29
+ function loadProvidersFromDir(dirPath) {
30
+ if (!dirPath || !fs.existsSync(dirPath)) return [];
31
+
32
+ return fs.readdirSync(dirPath)
33
+ .filter(isProviderFile)
34
+ .sort()
35
+ .map((fileName) => {
36
+ const fullPath = path.join(dirPath, fileName);
37
+ delete require.cache[require.resolve(fullPath)];
38
+ const provider = require(fullPath);
39
+ validateProviderContract(provider, fullPath);
40
+ return {
41
+ name: provider.name,
42
+ provider,
43
+ sourcePath: fullPath,
44
+ };
45
+ });
46
+ }
47
+
48
+ function getDynamicProviderDirs(projectRoot) {
49
+ if (!projectRoot) return [];
50
+ return [
51
+ path.join(projectRoot, '.code-abyss', 'vendor-providers'),
52
+ path.join(projectRoot, 'vendor-providers'),
53
+ ];
54
+ }
55
+
56
+ function loadVendorProviders(projectRoot = null) {
57
+ const providers = new Map();
58
+
59
+ loadProvidersFromDir(BUILTIN_DIR).forEach(({ name, provider }) => {
60
+ providers.set(name, provider);
61
+ });
62
+
63
+ getDynamicProviderDirs(projectRoot).forEach((dirPath) => {
64
+ loadProvidersFromDir(dirPath).forEach(({ name, provider }) => {
65
+ providers.set(name, provider);
66
+ });
67
+ });
68
+
69
+ return providers;
70
+ }
71
+
72
+ function listVendorProviderNames(projectRoot = null) {
73
+ return [...loadVendorProviders(projectRoot).keys()].sort();
74
+ }
75
+
76
+ function getVendorProvider(providerName = 'git', projectRoot = null) {
77
+ const provider = loadVendorProviders(projectRoot).get(providerName);
78
+ if (!provider) {
79
+ throw new Error(`不支持的 vendor provider: ${providerName}`);
80
+ }
81
+ return provider;
82
+ }
83
+
84
+ module.exports = {
85
+ shared,
86
+ BUILTIN_DIR,
87
+ loadVendorProviders,
88
+ listVendorProviderNames,
89
+ getVendorProvider,
90
+ validateProviderContract,
91
+ };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ name: 'local-dir',
5
+ validate(upstream) {
6
+ if (!upstream.path) {
7
+ throw new Error('upstream.provider=local-dir 时必须提供 path');
8
+ }
9
+ },
10
+ sync({ projectRoot, upstream, vendorDir, shared, packName }) {
11
+ const sourcePath = shared.resolveUpstreamPath(projectRoot, upstream.path);
12
+ if (!sourcePath || !shared.fs.existsSync(sourcePath)) {
13
+ throw new Error(`local-dir 源不存在: ${sourcePath || upstream.path}`);
14
+ }
15
+ shared.rmSafe(vendorDir);
16
+ shared.copyRecursive(sourcePath, vendorDir);
17
+ shared.writeVendorMetadata(vendorDir, {
18
+ pack: packName,
19
+ provider: 'local-dir',
20
+ path: upstream.path,
21
+ version: upstream.version || '',
22
+ sourceSignature: shared.hashDirectory(sourcePath),
23
+ vendorSignature: shared.hashDirectory(vendorDir),
24
+ });
25
+ return {
26
+ pack: packName,
27
+ provider: 'local-dir',
28
+ action: 'updated',
29
+ vendorDir,
30
+ repo: null,
31
+ commit: null,
32
+ version: upstream.version || '',
33
+ };
34
+ },
35
+ status({ projectRoot, upstream, vendorDir, shared, packName, metadata }) {
36
+ const sourcePath = shared.resolveUpstreamPath(projectRoot, upstream.path);
37
+ const sourceExists = !!sourcePath && shared.fs.existsSync(sourcePath);
38
+ const sourceSignature = sourceExists ? shared.hashDirectory(sourcePath) : null;
39
+ const vendorSignature = shared.hashDirectory(vendorDir);
40
+ const dirty = metadata && metadata.vendorSignature ? vendorSignature !== metadata.vendorSignature : true;
41
+
42
+ return {
43
+ pack: packName,
44
+ provider: 'local-dir',
45
+ vendorDir,
46
+ exists: true,
47
+ dirty,
48
+ drifted: !sourceExists || sourceSignature !== (metadata && metadata.sourceSignature),
49
+ currentCommit: null,
50
+ targetCommit: null,
51
+ sourceExists,
52
+ metadata,
53
+ };
54
+ },
55
+ };