@haposoft/cafekit 0.15.1 → 0.16.0-rc.1
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 +6 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +230 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +553 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/i18n.js +12 -3
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/lib/skill-deps.js +45 -7
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +41 -7
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/bin/phases/select-platform.js +23 -0
- package/bin/phases/skills-setup.js +7 -3
- package/package.json +15 -5
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +21 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +3 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/chrome-devtools/scripts/package-lock.json +261 -150
- package/src/claude/skills/chrome-devtools/scripts/package.json +1 -1
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +58 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/pptx/scripts/package-lock.json +868 -0
- package/src/claude/skills/pptx/scripts/package.json +1 -1
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/codex/hooks.json +14 -14
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
|
@@ -19,10 +19,18 @@ const { sha256 } = require('../lib/manifest');
|
|
|
19
19
|
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
20
20
|
const { report, treeAction } = require('./report');
|
|
21
21
|
const { normalizeOpenCodeBody } = require('../lib/opencode-install');
|
|
22
|
+
const {
|
|
23
|
+
upsertManagedCoreBlock,
|
|
24
|
+
managedRange,
|
|
25
|
+
migrateExactLegacyBlock,
|
|
26
|
+
preserveAddressingSection
|
|
27
|
+
} = require('../lib/instruction-blocks');
|
|
22
28
|
|
|
23
29
|
const SRC = path.join(__dirname, '../../src');
|
|
24
30
|
const CLAUDE_START = '<!-- CAFEKIT CLAUDE START -->';
|
|
25
31
|
const CLAUDE_END = '<!-- CAFEKIT CLAUDE END -->';
|
|
32
|
+
const LEGACY_CORE_START = '<!-- CAFEKIT CLAUDE AGENTS START -->';
|
|
33
|
+
const LEGACY_CORE_END = '<!-- CAFEKIT CLAUDE AGENTS END -->';
|
|
26
34
|
|
|
27
35
|
function managedClaudeBlock(template) {
|
|
28
36
|
return `${CLAUDE_START}\n${template.trimEnd()}\n${CLAUDE_END}`;
|
|
@@ -30,9 +38,11 @@ function managedClaudeBlock(template) {
|
|
|
30
38
|
|
|
31
39
|
function managedClaudeRange(content) {
|
|
32
40
|
const start = content.indexOf(CLAUDE_START);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const endStart = content.indexOf(CLAUDE_END);
|
|
42
|
+
if (start === -1 && endStart === -1) return null;
|
|
43
|
+
const duplicateStart = content.indexOf(CLAUDE_START, start + CLAUDE_START.length);
|
|
44
|
+
const duplicateEnd = content.indexOf(CLAUDE_END, endStart + CLAUDE_END.length);
|
|
45
|
+
if (start === -1 || endStart <= start || duplicateStart >= 0 || duplicateEnd >= 0) return false;
|
|
36
46
|
return { start, end: endStart + CLAUDE_END.length, bodyStart: start + CLAUDE_START.length, bodyEnd: endStart };
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -45,7 +55,19 @@ function transformManagedClaudeContent(content, transform) {
|
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
function upsertManagedClaudeBlock(existing, template, legacyOwned) {
|
|
48
|
-
|
|
58
|
+
// When an existing managed block present, reinstall replaces it. If the new
|
|
59
|
+
// template dropped its Addressing section, carry the saved section over from
|
|
60
|
+
// the existing managed block so the user's address survives for setupAddressing.
|
|
61
|
+
const existingRange = managedClaudeRange(existing);
|
|
62
|
+
if (existingRange === false) return existing;
|
|
63
|
+
const existingManagedBody = existingRange
|
|
64
|
+
? existing.slice(existingRange.bodyStart, existingRange.bodyEnd)
|
|
65
|
+
: '';
|
|
66
|
+
let body = template;
|
|
67
|
+
if (existingRange) {
|
|
68
|
+
body = preserveAddressingSection(template, existingManagedBody);
|
|
69
|
+
}
|
|
70
|
+
const block = managedClaudeBlock(body);
|
|
49
71
|
const range = managedClaudeRange(existing);
|
|
50
72
|
if (range) {
|
|
51
73
|
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
@@ -60,6 +82,44 @@ function upsertManagedClaudeBlock(existing, template, legacyOwned) {
|
|
|
60
82
|
return `${existing}${separator}${block}\n`;
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
function ensureSharedAgentsMdCore(ctx) {
|
|
86
|
+
const src = path.join(SRC, 'common/AGENTS.md');
|
|
87
|
+
const dest = 'AGENTS.md';
|
|
88
|
+
if (!fs.existsSync(src)) {
|
|
89
|
+
ctx.ui.warn('Shared AGENTS.md template not found');
|
|
90
|
+
ctx.results.missingDependencies++;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const template = fs.readFileSync(src, 'utf8');
|
|
95
|
+
const exists = fs.existsSync(dest);
|
|
96
|
+
const existing = exists ? fs.readFileSync(dest, 'utf8') : '';
|
|
97
|
+
if (managedRange(existing) === false) {
|
|
98
|
+
ctx.ui.warn(`AGENTS.md: malformed CafeKit CORE marker topology; preserved ${dest} without writing`);
|
|
99
|
+
ctx.results.errors++;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const migrated = migrateExactLegacyBlock(
|
|
103
|
+
existing,
|
|
104
|
+
LEGACY_CORE_START,
|
|
105
|
+
LEGACY_CORE_END,
|
|
106
|
+
template
|
|
107
|
+
);
|
|
108
|
+
const next = upsertManagedCoreBlock(migrated, template);
|
|
109
|
+
const action = !exists ? 'created' : next === existing ? 'unchanged' : 'updated';
|
|
110
|
+
|
|
111
|
+
if (!ctx.dryRun && action !== 'unchanged') {
|
|
112
|
+
fs.writeFileSync(dest, next, 'utf8');
|
|
113
|
+
}
|
|
114
|
+
report(ctx, action, 'AGENTS.md (shared CafeKit core)');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Install the shared AGENTS.md core used by Claude's @AGENTS.md import. */
|
|
118
|
+
function copyClaudeAgentsMdFile(ctx, platformKey) {
|
|
119
|
+
if (platformKey !== 'claude') return;
|
|
120
|
+
ensureSharedAgentsMdCore(ctx);
|
|
121
|
+
}
|
|
122
|
+
|
|
63
123
|
/** Copy ROUTING.md (SKILLS_DIR substituted; OpenCode body normalized). */
|
|
64
124
|
function copyRoutingFile(ctx, platformKey) {
|
|
65
125
|
const platform = PLATFORMS[platformKey];
|
|
@@ -132,6 +192,37 @@ function removeObsoleteClaudeRuntimeFiles(ctx, platformKey) {
|
|
|
132
192
|
});
|
|
133
193
|
}
|
|
134
194
|
|
|
195
|
+
/** Remove renamed agents only when ownership manifest proves pristine bytes. */
|
|
196
|
+
function removeObsoleteAgents(ctx, platformKey) {
|
|
197
|
+
const platform = PLATFORMS[platformKey];
|
|
198
|
+
const obsolete = ctx.manifest?.obsolete?.agents?.[platformKey] || [];
|
|
199
|
+
const tracker = ctx.trackers?.[platformKey];
|
|
200
|
+
if (!platform || !tracker || obsolete.length === 0) return;
|
|
201
|
+
|
|
202
|
+
const ownership = ctx.ownership?.[platform.folder] || { files: {} };
|
|
203
|
+
for (const relPath of obsolete) {
|
|
204
|
+
const targetPath = path.join(platform.folder, relPath);
|
|
205
|
+
if (!fs.existsSync(targetPath)) continue;
|
|
206
|
+
|
|
207
|
+
const key = tracker.keyFor(targetPath);
|
|
208
|
+
const recorded = ownership.files?.[key];
|
|
209
|
+
const currentHash = sha256(fs.readFileSync(targetPath));
|
|
210
|
+
if (recorded?.sha256 === currentHash) {
|
|
211
|
+
if (!ctx.dryRun) {
|
|
212
|
+
fs.rmSync(targetPath, { force: true });
|
|
213
|
+
tracker.prune(key);
|
|
214
|
+
}
|
|
215
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete agent: ${targetPath}`);
|
|
216
|
+
ctx.results.updated++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ctx.ui.warn(`Preserved user-owned obsolete agent: ${targetPath}`);
|
|
221
|
+
ctx.results.preserved++;
|
|
222
|
+
ctx.results.preservedFiles.push(targetPath);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
135
226
|
/** Install CLAUDE.md at the project root (ownership-aware). Claude only. */
|
|
136
227
|
function copyClaudeMdFile(ctx, platformKey) {
|
|
137
228
|
if (platformKey !== 'claude') return;
|
|
@@ -152,6 +243,11 @@ function copyClaudeMdFile(ctx, platformKey) {
|
|
|
152
243
|
const template = fs.readFileSync(src, 'utf8');
|
|
153
244
|
const exists = fs.existsSync(dest);
|
|
154
245
|
const existing = exists ? fs.readFileSync(dest, 'utf8') : '';
|
|
246
|
+
if (managedClaudeRange(existing) === false) {
|
|
247
|
+
ctx.ui.warn(`CLAUDE.md: malformed CafeKit CLAUDE marker topology; preserved ${dest} without writing`);
|
|
248
|
+
ctx.results.errors++;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
155
251
|
const legacyRecord = ctx.ownership?.[PLATFORMS.claude.folder]?.files?.['../CLAUDE.md'];
|
|
156
252
|
const legacyOwned = Boolean(legacyRecord && legacyRecord.sha256 === sha256(existing));
|
|
157
253
|
const next = upsertManagedClaudeBlock(existing, template, legacyOwned);
|
|
@@ -187,6 +283,9 @@ module.exports = {
|
|
|
187
283
|
copyClaudeRuntimeFiles,
|
|
188
284
|
removeObsoleteClaudeRuntimeFiles,
|
|
189
285
|
copyClaudeMdFile,
|
|
286
|
+
copyClaudeAgentsMdFile,
|
|
287
|
+
ensureSharedAgentsMdCore,
|
|
190
288
|
copyRulesDirectory,
|
|
289
|
+
removeObsoleteAgents,
|
|
191
290
|
transformManagedClaudeContent
|
|
192
291
|
};
|
|
@@ -8,6 +8,7 @@ const { report, treeAction } = require('./report');
|
|
|
8
8
|
const { copyRulesDirectory } = require('./claude-runtime');
|
|
9
9
|
const {
|
|
10
10
|
normalizeCodexBody,
|
|
11
|
+
managedRange,
|
|
11
12
|
upsertManagedCodexBlock
|
|
12
13
|
} = require('../lib/codex-install');
|
|
13
14
|
|
|
@@ -20,6 +21,33 @@ const CODEX_OWN_RUNTIME = [
|
|
|
20
21
|
['hooks.json', 'hooks.json']
|
|
21
22
|
];
|
|
22
23
|
|
|
24
|
+
const WINDOWS_HOOK_COMMAND = /^node "\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
|
|
25
|
+
|
|
26
|
+
function materializeWindowsHookCommands(content, projectRoot = process.cwd()) {
|
|
27
|
+
const config = JSON.parse(content);
|
|
28
|
+
const canonicalRoot = fs.realpathSync(projectRoot);
|
|
29
|
+
|
|
30
|
+
for (const groups of Object.values(config.hooks || {})) {
|
|
31
|
+
for (const group of groups) {
|
|
32
|
+
for (const handler of group.hooks || []) {
|
|
33
|
+
if (typeof handler.commandWindows !== 'string') continue;
|
|
34
|
+
const match = handler.commandWindows.match(WINDOWS_HOOK_COMMAND);
|
|
35
|
+
if (!match) {
|
|
36
|
+
throw new Error(`Unsupported Codex Windows hook command: ${handler.commandWindows}`);
|
|
37
|
+
}
|
|
38
|
+
const hookPath = path.join(canonicalRoot, '.codex', 'hooks', match[1]);
|
|
39
|
+
const encodedPath = Buffer.from(hookPath, 'utf8').toString('base64url');
|
|
40
|
+
handler.commandWindows = (
|
|
41
|
+
'node -e "require(Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\'))" ' +
|
|
42
|
+
encodedPath
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
23
51
|
function writeSourceFile(ctx, platformKey, src, dest, label, transform) {
|
|
24
52
|
const platform = PLATFORMS[platformKey];
|
|
25
53
|
const { action } = writeManagedFile({
|
|
@@ -41,7 +69,10 @@ function installRuntimeFiles(ctx, platformKey) {
|
|
|
41
69
|
platformKey,
|
|
42
70
|
path.join(CODEX_SRC, sourceRel),
|
|
43
71
|
path.join(platform.folder, targetRel),
|
|
44
|
-
`Codex runtime: ${targetRel}
|
|
72
|
+
`Codex runtime: ${targetRel}`,
|
|
73
|
+
sourceRel === 'hooks.json'
|
|
74
|
+
? (content) => materializeWindowsHookCommands(content, process.cwd())
|
|
75
|
+
: undefined
|
|
45
76
|
);
|
|
46
77
|
}
|
|
47
78
|
writeSourceFile(
|
|
@@ -77,8 +108,8 @@ function installNativeRuleOverrides(ctx, platformKey) {
|
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
function installManagedAgentsMd(ctx) {
|
|
80
|
-
const
|
|
81
|
-
if (!fs.existsSync(
|
|
111
|
+
const runtimeSource = path.join(CODEX_SRC, 'AGENTS.md');
|
|
112
|
+
if (!fs.existsSync(runtimeSource)) {
|
|
82
113
|
report(ctx, 'missing', 'Codex AGENTS.md template');
|
|
83
114
|
return;
|
|
84
115
|
}
|
|
@@ -86,9 +117,12 @@ function installManagedAgentsMd(ctx) {
|
|
|
86
117
|
const destination = 'AGENTS.md';
|
|
87
118
|
const exists = fs.existsSync(destination);
|
|
88
119
|
const existing = exists ? fs.readFileSync(destination, 'utf8') : '';
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
.
|
|
120
|
+
if (managedRange(existing) === false) {
|
|
121
|
+
ctx.ui.warn(`AGENTS.md: malformed CafeKit CODEX marker topology; preserved ${destination} without writing`);
|
|
122
|
+
ctx.results.errors++;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const block = normalizeCodexBody(fs.readFileSync(runtimeSource, 'utf8'));
|
|
92
126
|
const next = upsertManagedCodexBlock(existing, block);
|
|
93
127
|
const action = !exists ? 'created' : next === existing ? 'unchanged' : 'updated';
|
|
94
128
|
|
|
@@ -107,4 +141,4 @@ function installCodexRuntime(ctx, platformKey) {
|
|
|
107
141
|
return ctx;
|
|
108
142
|
}
|
|
109
143
|
|
|
110
|
-
module.exports = { installCodexRuntime };
|
|
144
|
+
module.exports = { installCodexRuntime, materializeWindowsHookCommands };
|
|
@@ -15,9 +15,10 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { PLATFORMS } = require('../lib/context');
|
|
17
17
|
const { LANGUAGE_LABELS } = require('../lib/i18n');
|
|
18
|
-
const { setupOpenCodeModel } = require('../lib/opencode-install');
|
|
18
|
+
const { setupOpenCodeModel, transformManagedOpenCodeContent } = require('../lib/opencode-install');
|
|
19
19
|
const { transformManagedCodexContent } = require('../lib/codex-install');
|
|
20
20
|
const { transformManagedClaudeContent } = require('./claude-runtime');
|
|
21
|
+
const { transformManagedCoreContent } = require('../lib/instruction-blocks');
|
|
21
22
|
const ASSISTANT_NAMES = {
|
|
22
23
|
claude: 'Claude Code',
|
|
23
24
|
codex: 'Codex CLI',
|
|
@@ -48,6 +49,10 @@ function instructionTargets(ctx) {
|
|
|
48
49
|
codex: {
|
|
49
50
|
file: path.join(process.cwd(), 'AGENTS.md'),
|
|
50
51
|
transform: transformManagedCodexContent
|
|
52
|
+
},
|
|
53
|
+
opencode: {
|
|
54
|
+
file: path.join(process.cwd(), 'AGENTS.md'),
|
|
55
|
+
transform: transformManagedOpenCodeContent
|
|
51
56
|
}
|
|
52
57
|
};
|
|
53
58
|
return Object.keys(targets)
|
|
@@ -84,44 +89,44 @@ function patchSettingsLanguage(ctx) {
|
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
// Use locale (freeform label) so "Korean" shows correctly, not just "en".
|
|
87
|
-
const label = ctx.locale
|
|
88
|
-
if (
|
|
92
|
+
const label = ctx.locale;
|
|
93
|
+
if (!label) return;
|
|
89
94
|
settings.language = label;
|
|
90
95
|
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
/**
|
|
94
|
-
* Patch the "## Language Consistency" section in
|
|
95
|
-
*
|
|
96
|
-
* relying on hooks reading runtime.json.
|
|
97
|
-
*
|
|
98
|
-
* The section is identified by a stable marker comment so subsequent installs
|
|
99
|
-
* can update it idempotently.
|
|
99
|
+
* Patch the "## Language Consistency" section in the shared AGENTS.md core
|
|
100
|
+
* block so every installed platform uses the chosen response language.
|
|
100
101
|
*/
|
|
101
102
|
function patchLanguageSection(ctx) {
|
|
102
|
-
//
|
|
103
|
-
|
|
103
|
+
// No explicit/saved locale means follow the user's language; do not inject an
|
|
104
|
+
// English override into the shared core block.
|
|
105
|
+
const locale = ctx.locale;
|
|
104
106
|
if (!locale || locale === 'en' || locale === 'English') return;
|
|
105
107
|
|
|
106
108
|
// Use locale directly when set (e.g. "Korean"), or map from lang code.
|
|
107
109
|
const label = locale in LANGUAGE_LABEL_BY_CODE ? LANGUAGE_LABEL_BY_CODE[locale] : locale;
|
|
108
|
-
|
|
109
110
|
const newSection = `## Language Consistency <!-- cafekit:lang -->
|
|
110
111
|
|
|
111
|
-
Always respond in **${label}**. Technical terms, code identifiers, and file paths may remain in English, but
|
|
112
|
+
Always respond in **${label}**. Technical terms, code identifiers, and file paths may remain in English, but explanations, comments directed at the user, and structured output must be in ${label}.
|
|
112
113
|
|
|
113
114
|
`;
|
|
114
|
-
|
|
115
|
-
// Replace only inside the installer-owned block.
|
|
116
115
|
const markerRe = /## Language Consistency <!-- cafekit:lang -->[\s\S]*?(?=\n##|\n*$)/;
|
|
117
116
|
const genericRe = /## Language Consistency\n[\s\S]*?(?=\n##|\n*$)/;
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const target = path.join(process.cwd(), 'AGENTS.md');
|
|
118
|
+
|
|
119
|
+
updateInstructionTarget(
|
|
120
|
+
{
|
|
121
|
+
file: target,
|
|
122
|
+
transform: transformManagedCoreContent
|
|
123
|
+
},
|
|
124
|
+
(managed) => {
|
|
120
125
|
if (markerRe.test(managed)) return managed.replace(markerRe, newSection);
|
|
121
126
|
if (genericRe.test(managed)) return managed.replace(genericRe, newSection);
|
|
122
127
|
return `${managed.trimEnd()}\n\n${newSection}\n`;
|
|
123
|
-
}
|
|
124
|
-
|
|
128
|
+
}
|
|
129
|
+
);
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
function patchRuntimeLocale(ctx) {
|
|
@@ -136,7 +141,9 @@ function patchRuntimeLocale(ctx) {
|
|
|
136
141
|
}
|
|
137
142
|
data.locale = data.locale || {};
|
|
138
143
|
// Use locale (freeform label) so custom languages propagate to the AI hook.
|
|
139
|
-
|
|
144
|
+
// A null locale intentionally leaves the runtime's current value untouched.
|
|
145
|
+
const locale = ctx.locale;
|
|
146
|
+
if (!locale) continue;
|
|
140
147
|
if (data.locale.responseLanguage === locale) continue;
|
|
141
148
|
// Hardening: never downgrade an existing configured label to a bare default
|
|
142
149
|
// code when this run never made an explicit language choice (ctx.locale
|
|
@@ -181,7 +188,7 @@ ${name} always addresses the user as "${userAddress}" throughout the conversatio
|
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
async function setupAddressing(ctx) {
|
|
184
|
-
if (!ctx.platforms.some((key) => key === 'claude' || key === 'codex')) return;
|
|
191
|
+
if (!ctx.platforms.some((key) => key === 'claude' || key === 'codex' || key === 'opencode')) return;
|
|
185
192
|
|
|
186
193
|
let existingName = null;
|
|
187
194
|
for (const target of instructionTargets(ctx)) {
|
|
@@ -243,7 +250,7 @@ async function runPostInstall(ctx) {
|
|
|
243
250
|
|
|
244
251
|
await setupAddressing(ctx);
|
|
245
252
|
|
|
246
|
-
// Patch Language Consistency section in
|
|
253
|
+
// Patch Language Consistency section in the shared AGENTS.md core block.
|
|
247
254
|
patchLanguageSection(ctx);
|
|
248
255
|
|
|
249
256
|
// Persist chosen language into each platform's runtime.json (records in tracker).
|
|
@@ -24,13 +24,87 @@ function hasPattern(lines, pattern) {
|
|
|
24
24
|
return lines.includes(pattern) || lines.includes(bare) || lines.includes(withSlash);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const PLAN_PATTERNS = new Set([
|
|
28
|
+
'plans',
|
|
29
|
+
'plans/',
|
|
30
|
+
'plans/*',
|
|
31
|
+
'plans/**',
|
|
32
|
+
'plans/**/*',
|
|
33
|
+
'!plans/*.md',
|
|
34
|
+
'!plans/templates',
|
|
35
|
+
'!plans/templates/',
|
|
36
|
+
'!plans/templates/*',
|
|
37
|
+
'!plans/templates/**',
|
|
38
|
+
'!plans/templates/**/*'
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function isManagedPattern(line, patterns) {
|
|
42
|
+
return patterns.has(line.trim());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isTrivia(line) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
return trimmed === '' || trimmed.startsWith('#');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function migrateManagedPlanPatterns(lines, header, patterns) {
|
|
51
|
+
const headerIndex = lines.findIndex((line) => line.trim() === header);
|
|
52
|
+
if (headerIndex < 0) return false;
|
|
53
|
+
|
|
54
|
+
const managedPatterns = new Set([...patterns, ...PLAN_PATTERNS]);
|
|
55
|
+
const blockStart = headerIndex + 1;
|
|
56
|
+
let blockEnd = blockStart;
|
|
57
|
+
let pendingTriviaStart = null;
|
|
58
|
+
let sawManagedPattern = false;
|
|
59
|
+
|
|
60
|
+
for (let index = blockStart; index < lines.length; index++) {
|
|
61
|
+
if (isManagedPattern(lines[index], managedPatterns)) {
|
|
62
|
+
sawManagedPattern = true;
|
|
63
|
+
pendingTriviaStart = null;
|
|
64
|
+
blockEnd = index + 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (isTrivia(lines[index])) {
|
|
68
|
+
if (pendingTriviaStart === null) pendingTriviaStart = index;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
blockEnd = pendingTriviaStart ?? index;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!sawManagedPattern) return false;
|
|
76
|
+
if (blockEnd === blockStart && pendingTriviaStart !== null) return false;
|
|
77
|
+
|
|
78
|
+
const block = lines.slice(blockStart, blockEnd);
|
|
79
|
+
const existingPlanPatterns = block
|
|
80
|
+
.filter((line) => PLAN_PATTERNS.has(line.trim()))
|
|
81
|
+
.map((line) => line.trim());
|
|
82
|
+
const canonicalPlanPatterns = patterns.slice(1, 5);
|
|
83
|
+
if (
|
|
84
|
+
existingPlanPatterns.length === canonicalPlanPatterns.length &&
|
|
85
|
+
existingPlanPatterns.every((pattern, index) => pattern === canonicalPlanPatterns[index])
|
|
86
|
+
) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const firstPlanIndex = block.findIndex((line) => PLAN_PATTERNS.has(line.trim()));
|
|
91
|
+
if (firstPlanIndex < 0) return false;
|
|
92
|
+
|
|
93
|
+
const withoutPlans = block.filter((line) => !PLAN_PATTERNS.has(line.trim()));
|
|
94
|
+
withoutPlans.splice(firstPlanIndex, 0, ...canonicalPlanPatterns);
|
|
95
|
+
lines.splice(blockStart, block.length, ...withoutPlans);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
27
99
|
function ensureGitignore(ctx) {
|
|
28
100
|
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
29
101
|
const header = '# CafeKit / Ecosystem';
|
|
30
102
|
const patterns = [
|
|
31
103
|
'specs/_shared/',
|
|
32
|
-
'plans
|
|
104
|
+
'plans/*',
|
|
105
|
+
'!plans/*.md',
|
|
33
106
|
'!plans/templates/',
|
|
107
|
+
'!plans/templates/**',
|
|
34
108
|
'.cafekit-backup/',
|
|
35
109
|
'.cafekit.lock',
|
|
36
110
|
// Local runtime payload — reinstall with npx; keep out of git
|
|
@@ -50,16 +124,21 @@ function ensureGitignore(ctx) {
|
|
|
50
124
|
}
|
|
51
125
|
|
|
52
126
|
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
53
|
-
const lines = content.split('\n')
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
127
|
+
const lines = content.split('\n');
|
|
128
|
+
const migrated = migrateManagedPlanPatterns(lines, header, patterns);
|
|
129
|
+
const normalizedLines = lines.map((line) => line.trim());
|
|
130
|
+
const missing = patterns.filter((p) => !hasPattern(normalizedLines, p));
|
|
131
|
+
|
|
132
|
+
if (migrated || missing.length > 0) {
|
|
133
|
+
let newContent = lines.join('\n');
|
|
134
|
+
if (missing.length > 0) {
|
|
135
|
+
if (!newContent.endsWith('\n')) newContent += '\n';
|
|
136
|
+
if (!content.includes(header)) newContent += `\n${header}\n`;
|
|
137
|
+
newContent += missing.join('\n') + '\n';
|
|
138
|
+
}
|
|
61
139
|
if (!ctx.dryRun) fs.writeFileSync(gitignorePath, newContent, 'utf8');
|
|
62
|
-
|
|
140
|
+
const details = migrated ? 'normalized managed plans patterns' : `added ${missing.join(', ')}`;
|
|
141
|
+
ctx.ui.detail(` ↻ ${prefix}.gitignore updated: ${details}`);
|
|
63
142
|
ctx.results.updated++;
|
|
64
143
|
} else {
|
|
65
144
|
ctx.ui.detail(` → ${prefix}.gitignore already up to date`);
|
|
@@ -128,6 +128,19 @@ function cancelSelection(ctx) {
|
|
|
128
128
|
return [];
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
async function promptAddMorePlatforms(ctx, savedPlatforms) {
|
|
132
|
+
const allKeys = getPlatformKeys();
|
|
133
|
+
const available = allKeys.filter((key) => !savedPlatforms.includes(key));
|
|
134
|
+
if (available.length === 0) return false;
|
|
135
|
+
|
|
136
|
+
const r = await ctx.ui.confirm(
|
|
137
|
+
{ message: ctx.t('addPlatformsPrompt', { names: platformNames(savedPlatforms) }), initialValue: false },
|
|
138
|
+
false
|
|
139
|
+
);
|
|
140
|
+
if (ctx.ui.isCancel(r)) return false;
|
|
141
|
+
return r;
|
|
142
|
+
}
|
|
143
|
+
|
|
131
144
|
async function promptPlatforms(ctx) {
|
|
132
145
|
const platforms = await promptPlatformSelection(ctx);
|
|
133
146
|
return platforms.length > 0 ? platforms : cancelSelection(ctx);
|
|
@@ -164,6 +177,16 @@ async function resolvePlatforms(ctx) {
|
|
|
164
177
|
platforms = await promptPlatforms(ctx);
|
|
165
178
|
if (ctx.cancelled) return ctx;
|
|
166
179
|
}
|
|
180
|
+
} else if (ctx.interactive) {
|
|
181
|
+
// No newly detected platforms, but user might want to ADD more platforms
|
|
182
|
+
const wantAdd = await promptAddMorePlatforms(ctx, savedPlatforms);
|
|
183
|
+
if (wantAdd) {
|
|
184
|
+
const added = await promptPlatformSelection(ctx);
|
|
185
|
+
if (ctx.cancelled) return ctx;
|
|
186
|
+
if (added.length > 0) {
|
|
187
|
+
platforms = [...new Set([...platforms, ...added])];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
167
190
|
}
|
|
168
191
|
ctx.platforms = platforms;
|
|
169
192
|
ctx.ui.info(ctx.t('platformKept', { names: platformNames(platforms) }));
|
|
@@ -79,12 +79,16 @@ async function setupNode(ctx, skillsDir) {
|
|
|
79
79
|
const extraEnv = (skill === 'chrome-devtools' && systemChrome)
|
|
80
80
|
? { PUPPETEER_SKIP_DOWNLOAD: 'true' }
|
|
81
81
|
: {};
|
|
82
|
-
const
|
|
82
|
+
const result = await dep.npmInstall(dir, hasNodeModules, extraEnv);
|
|
83
83
|
|
|
84
|
-
if (ok) {
|
|
84
|
+
if (result.ok) {
|
|
85
85
|
ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
|
|
86
86
|
} else {
|
|
87
|
-
ctx.ui.stopSpinner(ctx.t('npmFailed', {
|
|
87
|
+
ctx.ui.stopSpinner(ctx.t('npmFailed', {
|
|
88
|
+
skill,
|
|
89
|
+
dir,
|
|
90
|
+
error: dep.commandFailureReason(result)
|
|
91
|
+
}));
|
|
88
92
|
continue;
|
|
89
93
|
}
|
|
90
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0-rc.1",
|
|
4
4
|
"description": "Native spec-driven workflows for Claude Code, Codex CLI, and OpenCode. Bundles CafeKit skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,15 +9,25 @@
|
|
|
9
9
|
"cafekit": "bin/install.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node scripts/run-skill-self-tests.mjs"
|
|
12
|
+
"test": "node scripts/run-skill-self-tests.mjs",
|
|
13
|
+
"test:package": "node --test bin/__tests__/package-inventory.test.js",
|
|
14
|
+
"test:secrets": "node --test bin/__tests__/secret-scanner.test.js",
|
|
15
|
+
"test:policy": "node --test bin/__tests__/develop-contract.test.js"
|
|
13
16
|
},
|
|
14
17
|
"files": [
|
|
15
18
|
"bin",
|
|
16
19
|
"src",
|
|
17
20
|
"README.md",
|
|
21
|
+
"!src/**/.logs/**",
|
|
22
|
+
"!src/**/*.log",
|
|
18
23
|
"!src/**/.coverage",
|
|
19
|
-
"!src
|
|
20
|
-
"!src
|
|
24
|
+
"!src/**/.coverage/**",
|
|
25
|
+
"!src/**/__pycache__/**",
|
|
26
|
+
"!src/**/*.pyc",
|
|
27
|
+
"!src/**/.cache/**",
|
|
28
|
+
"!src/**/.state/**",
|
|
29
|
+
"!src/**/.tmp/**",
|
|
30
|
+
"!src/.codex/**"
|
|
21
31
|
],
|
|
22
32
|
"repository": {
|
|
23
33
|
"type": "git",
|
|
@@ -49,7 +59,7 @@
|
|
|
49
59
|
"@opencode-ai/plugin": "1.15.11"
|
|
50
60
|
},
|
|
51
61
|
"dependencies": {
|
|
52
|
-
"@clack/prompts": "
|
|
62
|
+
"@clack/prompts": "0.11.0",
|
|
53
63
|
"picocolors": "^1.1.1"
|
|
54
64
|
}
|
|
55
65
|
}
|