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.
- package/README.md +129 -58
- package/bin/adapters/claude.js +16 -12
- package/bin/adapters/codex.js +110 -37
- package/bin/adapters/gemini.js +92 -0
- package/bin/install.js +521 -130
- package/bin/lib/ccline.js +18 -8
- package/bin/lib/gstack-claude.js +164 -0
- package/bin/lib/gstack-codex.js +347 -0
- package/bin/lib/gstack-gemini.js +140 -0
- package/bin/lib/pack-bootstrap.js +92 -0
- package/bin/lib/pack-docs.js +61 -0
- package/bin/lib/pack-registry.js +400 -0
- package/bin/lib/pack-reports.js +87 -0
- package/bin/lib/pack-vendor.js +82 -0
- package/bin/lib/style-registry.js +29 -7
- package/bin/lib/target-registry.js +74 -0
- package/bin/lib/utils.js +69 -6
- package/bin/lib/vendor-providers/archive.js +56 -0
- package/bin/lib/vendor-providers/git.js +59 -0
- package/bin/lib/vendor-providers/index.js +91 -0
- package/bin/lib/vendor-providers/local-dir.js +55 -0
- package/bin/lib/vendor-providers/shared.js +86 -0
- package/bin/packs.js +581 -0
- package/config/AGENTS.md +0 -2
- package/config/CLAUDE.md +43 -326
- package/config/codex-config.example.toml +26 -20
- package/config/gemini-settings.example.json +6 -0
- package/config/instruction.md +87 -0
- package/output-styles/abyss-command.md +7 -30
- package/output-styles/abyss-concise.md +11 -52
- package/output-styles/abyss-cultivator.md +20 -249
- package/output-styles/abyss-ritual.md +11 -45
- package/output-styles/index.json +20 -4
- package/package.json +26 -14
- package/packs/abyss/manifest.json +25 -0
- package/packs/gstack/manifest.json +151 -0
- package/skills/SKILL.md +33 -237
- package/skills/domains/frontend-design/agents/openai.yaml +1 -1
- package/skills/run_skill.js +25 -3
- package/skills/tools/gen-docs/agents/openai.yaml +1 -1
- package/skills/tools/verify-change/agents/openai.yaml +1 -1
- package/skills/tools/verify-module/agents/openai.yaml +1 -1
- package/skills/tools/verify-quality/agents/openai.yaml +1 -1
- package/skills/tools/verify-security/agents/openai.yaml +1 -1
- package/skills/tools/override-refusal/SKILL.md +0 -81
- package/skills/tools/override-refusal/agents/openai.yaml +0 -4
- 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(
|
|
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 :
|
|
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
|
|
103
|
-
const style = resolveStyle(projectRoot, styleSlug, '
|
|
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
|
-
|
|
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) {
|
|
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 (
|
|
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
|
-
|
|
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
|
+
};
|