@haposoft/cafekit 0.14.1 → 0.15.0
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 +86 -11
- package/bin/__tests__/codex-hooks.test.js +331 -0
- package/bin/__tests__/codex-native.test.js +458 -0
- package/bin/__tests__/installer-safety.test.js +259 -0
- package/bin/install.js +30 -8
- package/bin/lib/backup.js +82 -18
- package/bin/lib/codex-frontmatter.js +56 -0
- package/bin/lib/codex-install.js +195 -0
- package/bin/lib/context.js +65 -8
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +12 -3
- package/bin/lib/managed-writer.js +17 -2
- package/bin/lib/manifest.js +21 -4
- package/bin/lib/opencode-install.js +21 -19
- package/bin/lib/path-safety.js +40 -0
- package/bin/lib/version-check.js +25 -12
- package/bin/phases/claude-runtime.js +60 -9
- package/bin/phases/codex-runtime.js +110 -0
- package/bin/phases/copy-payload.js +36 -16
- package/bin/phases/post-install.js +98 -48
- package/bin/phases/root-config.js +4 -2
- package/bin/phases/select-platform.js +105 -66
- package/bin/phases/summary.js +6 -1
- package/package.json +3 -2
- package/src/claude/CLAUDE.md +1 -1
- package/src/claude/agents/code-auditor.md +1 -1
- package/src/claude/agents/debugger.md +1 -0
- package/src/claude/agents/god-developer.md +10 -1
- package/src/claude/agents/inspector.md +1 -0
- package/src/claude/agents/spec-maker.md +1 -1
- package/src/claude/agents/test-runner.md +2 -2
- package/src/claude/hooks/__tests__/privacy-block.test.js +50 -10
- package/src/claude/hooks/__tests__/spec-gate.test.js +75 -1
- package/src/claude/hooks/__tests__/state.test.js +65 -0
- package/src/claude/hooks/docs-sync.cjs +15 -21
- package/src/claude/hooks/inspect-block.cjs +1 -1
- package/src/claude/hooks/lib/config.cjs +0 -5
- package/src/claude/hooks/privacy-block.cjs +39 -62
- package/src/claude/hooks/spec-gate.cjs +18 -7
- package/src/claude/hooks/spec-state.cjs +4 -0
- package/src/claude/hooks/state.cjs +13 -3
- package/src/claude/hooks/usage.cjs +6 -0
- package/src/claude/rules/orchestrator.md +2 -2
- package/src/claude/rules/skill-domain-routing.md +27 -86
- package/src/claude/rules/skill-workflow-routing.md +11 -82
- package/src/claude/rules/state-sync.md +1 -1
- package/src/claude/runtime.json +7 -7
- package/src/claude/scripts/generate-skill-catalog.cjs +8 -3
- package/src/claude/scripts/validate-spec-output.cjs +6 -0
- package/src/claude/skills/code-review/references/spec-compliance-review.md +1 -1
- package/src/claude/skills/develop/SKILL.md +26 -5
- package/src/claude/skills/develop/references/parallel-waves.md +112 -0
- package/src/claude/skills/develop/references/quality-gate.md +10 -2
- package/src/claude/skills/hotfix/references/parallel-patterns.md +1 -1
- package/src/claude/skills/inspect/SKILL.md +42 -113
- package/src/claude/skills/inspect/references/internal-inspection.md +16 -135
- package/src/claude/skills/specs/SKILL.md +134 -386
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/task.md +3 -1
- package/src/claude/skills/sync/SKILL.md +2 -2
- package/src/claude/skills/sync/references/sync-protocols.md +4 -4
- package/src/claude/skills/test/SKILL.md +1 -1
- package/src/claude/status.cjs +9 -6
- package/src/codex/AGENTS.md +30 -0
- package/src/codex/agents-gitignore +11 -0
- package/src/codex/gitignore +12 -0
- package/src/codex/hooks/agent.cjs +66 -0
- package/src/codex/hooks/docs-sync.cjs +66 -0
- package/src/codex/hooks/inspect-block.cjs +40 -0
- package/src/codex/hooks/lib/hook-context.cjs +93 -0
- package/src/codex/hooks/lib/inspect-paths.cjs +199 -0
- package/src/codex/hooks/lib/privacy-state.cjs +199 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +78 -0
- package/src/codex/hooks/lib/spec-utils.cjs +50 -0
- package/src/codex/hooks/lib/state-store.cjs +166 -0
- package/src/codex/hooks/privacy-approval.cjs +59 -0
- package/src/codex/hooks/privacy-block.cjs +182 -0
- package/src/codex/hooks/rules.cjs +79 -0
- package/src/codex/hooks/session.cjs +114 -0
- package/src/codex/hooks/spec-gate.cjs +80 -0
- package/src/codex/hooks/spec-state.cjs +73 -0
- package/src/codex/hooks/state.cjs +157 -0
- package/src/codex/hooks/task-scaffold-guard.cjs +63 -0
- package/src/codex/hooks.json +122 -0
- package/src/codex/rules/hook-protocols.md +32 -0
- package/src/codex/rules/state-sync.md +41 -0
- package/src/codex/runtime.json +28 -0
- package/src/opencode/plugins/docs-sync.ts +10 -16
- package/src/claude/archive-command/code/SKILL.md +0 -55
- package/src/claude/archive-command/code/references/execution-loop.md +0 -21
- package/src/claude/archive-command/code.md +0 -17
- package/src/claude/archive-command/docs.md +0 -609
- package/src/claude/archive-command/review/codebase/parallel.md +0 -76
- package/src/claude/archive-command/spec-design.md +0 -247
- package/src/claude/archive-command/spec-init.md +0 -120
- package/src/claude/archive-command/spec-requirements.md +0 -138
- package/src/claude/archive-command/spec-status.md +0 -98
- package/src/claude/archive-command/spec-tasks.md +0 -173
- package/src/claude/archive-command/spec-validate.md +0 -118
- package/src/claude/archive-command/test.md +0 -8
package/bin/lib/version-check.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Reads each platform's cafekit.json to compare the installed version with the
|
|
5
5
|
* incoming one and decides whether to proceed:
|
|
6
6
|
*
|
|
7
|
-
* same →
|
|
7
|
+
* same → selective refresh (or full reset with --force-overwrite)
|
|
8
8
|
* newer → normal update
|
|
9
9
|
* older → downgrade warning + confirm (interactive) or abort (non-interactive)
|
|
10
10
|
* missing → fresh install, no check needed
|
|
@@ -43,7 +43,7 @@ function getInstalledVersion(platformKey) {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Check versions for all selected platforms and decide whether to proceed.
|
|
46
|
-
* Sets ctx.cancelled on same-version
|
|
46
|
+
* Sets ctx.cancelled on a skipped same-version refresh or unconfirmed downgrade.
|
|
47
47
|
* Sets ctx.isUpdate = true when upgrading from an older version.
|
|
48
48
|
* Shows interactive prompt when version differs, asking user what to do.
|
|
49
49
|
*/
|
|
@@ -65,33 +65,48 @@ async function checkVersions(ctx) {
|
|
|
65
65
|
ctx.isUpdate = false;
|
|
66
66
|
return ctx;
|
|
67
67
|
}
|
|
68
|
+
const hasFreshPlatform = platformsWithInstall.length < ctx.platforms.length;
|
|
68
69
|
|
|
69
70
|
// Get unique versions across all platforms
|
|
70
71
|
const uniqueVersions = [...new Set(platformsWithInstall.map(p => p.installed))];
|
|
71
72
|
const installedVersion = uniqueVersions.length === 1 ? uniqueVersions[0] : `${uniqueVersions.join(', ')}`;
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const compared = platformsWithInstall.map((platform) => ({
|
|
75
|
+
...platform,
|
|
76
|
+
cmp: cmpVersion(platform.installed, incoming)
|
|
77
|
+
}));
|
|
78
|
+
// Any downgrade risk takes precedence. Otherwise update every stale runtime
|
|
79
|
+
// even when the first selected runtime already matches the incoming version.
|
|
80
|
+
const firstPlatform = compared.find((platform) => platform.cmp > 0)
|
|
81
|
+
|| compared.find((platform) => platform.cmp < 0)
|
|
82
|
+
|| compared[0];
|
|
83
|
+
const cmp = compared.some((platform) => platform.cmp > 0)
|
|
84
|
+
? 1
|
|
85
|
+
: compared.some((platform) => platform.cmp < 0) ? -1 : 0;
|
|
76
86
|
|
|
77
87
|
// ── Same version ───────────────────────────────────────────────────────────
|
|
78
88
|
if (cmp === 0) {
|
|
79
89
|
ctx.isUpdate = false;
|
|
80
90
|
|
|
91
|
+
// A same-version runtime must not prevent adding another selected platform
|
|
92
|
+
// (for example, adding Codex to an existing Claude-only installation).
|
|
93
|
+
if (hasFreshPlatform) return ctx;
|
|
94
|
+
|
|
81
95
|
if (ctx.options.forceOverwrite) {
|
|
82
96
|
ctx.ui.info(ctx.t ? ctx.t('versionForceReinstall', { v: installedVersion }) : `Reinstalling CafeKit ${installedVersion} (--force-overwrite)...`);
|
|
83
97
|
return ctx;
|
|
84
98
|
}
|
|
85
99
|
|
|
86
100
|
if (!ctx.interactive) {
|
|
87
|
-
ctx.ui.info(ctx.t
|
|
88
|
-
|
|
101
|
+
ctx.ui.info(ctx.t
|
|
102
|
+
? ctx.t('versionRefreshing', { v: installedVersion })
|
|
103
|
+
: `Refreshing CafeKit ${installedVersion}; user-modified files will be preserved.`);
|
|
89
104
|
return ctx;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
// Show current vs new and ask what to do
|
|
93
108
|
const options = [
|
|
94
|
-
{ value: '
|
|
109
|
+
{ value: 'refresh', label: ctx.t ? ctx.t('refreshOption') : 'Refresh managed files (preserve your edits)' },
|
|
95
110
|
{ value: 'skip', label: ctx.t ? ctx.t('skipOption') : 'Skip (exit)' }
|
|
96
111
|
];
|
|
97
112
|
|
|
@@ -107,10 +122,6 @@ async function checkVersions(ctx) {
|
|
|
107
122
|
return ctx;
|
|
108
123
|
}
|
|
109
124
|
|
|
110
|
-
if (answer === 'reinstall') {
|
|
111
|
-
ctx.options.forceOverwrite = true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
125
|
return ctx;
|
|
115
126
|
}
|
|
116
127
|
|
|
@@ -194,6 +205,8 @@ async function checkVersions(ctx) {
|
|
|
194
205
|
? ctx.t('versionDowngrade', { from: firstPlatform.installed, to: incoming })
|
|
195
206
|
: `Downgrading ${firstPlatform.installed} → ${incoming}. This may remove features.`;
|
|
196
207
|
|
|
208
|
+
if (ctx.options.forceOverwrite) return ctx;
|
|
209
|
+
|
|
197
210
|
if (!ctx.interactive) {
|
|
198
211
|
ctx.ui.warn ? ctx.ui.warn(msg + ' Downgrade aborted in non-interactive mode. Use --force-overwrite to proceed.') : console.warn(msg);
|
|
199
212
|
ctx.cancelled = true;
|
|
@@ -11,15 +11,54 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const {
|
|
13
13
|
PLATFORMS,
|
|
14
|
-
|
|
14
|
+
hasPlatformCapability,
|
|
15
15
|
getRuntimeSupportTargetDir,
|
|
16
16
|
getCopyOptions
|
|
17
17
|
} = require('../lib/context');
|
|
18
|
+
const { sha256 } = require('../lib/manifest');
|
|
18
19
|
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
19
20
|
const { report, treeAction } = require('./report');
|
|
20
21
|
const { normalizeOpenCodeBody } = require('../lib/opencode-install');
|
|
21
22
|
|
|
22
23
|
const SRC = path.join(__dirname, '../../src');
|
|
24
|
+
const CLAUDE_START = '<!-- CAFEKIT CLAUDE START -->';
|
|
25
|
+
const CLAUDE_END = '<!-- CAFEKIT CLAUDE END -->';
|
|
26
|
+
|
|
27
|
+
function managedClaudeBlock(template) {
|
|
28
|
+
return `${CLAUDE_START}\n${template.trimEnd()}\n${CLAUDE_END}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function managedClaudeRange(content) {
|
|
32
|
+
const start = content.indexOf(CLAUDE_START);
|
|
33
|
+
if (start === -1) return null;
|
|
34
|
+
const endStart = content.indexOf(CLAUDE_END, start + CLAUDE_START.length);
|
|
35
|
+
if (endStart === -1) return null;
|
|
36
|
+
return { start, end: endStart + CLAUDE_END.length, bodyStart: start + CLAUDE_START.length, bodyEnd: endStart };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Transform only the CafeKit-owned body, preserving all surrounding bytes. */
|
|
40
|
+
function transformManagedClaudeContent(content, transform) {
|
|
41
|
+
const range = managedClaudeRange(content);
|
|
42
|
+
if (!range) return content;
|
|
43
|
+
const body = content.slice(range.bodyStart, range.bodyEnd);
|
|
44
|
+
return `${content.slice(0, range.bodyStart)}${transform(body)}${content.slice(range.bodyEnd)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function upsertManagedClaudeBlock(existing, template, legacyOwned) {
|
|
48
|
+
const block = managedClaudeBlock(template);
|
|
49
|
+
const range = managedClaudeRange(existing);
|
|
50
|
+
if (range) {
|
|
51
|
+
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Whole-file migration is deliberately limited to exact shipped content or
|
|
55
|
+
// a file whose bytes still match the legacy ownership manifest.
|
|
56
|
+
if (existing === template || legacyOwned) return `${block}\n`;
|
|
57
|
+
if (!existing) return `${block}\n`;
|
|
58
|
+
|
|
59
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
60
|
+
return `${existing}${separator}${block}\n`;
|
|
61
|
+
}
|
|
23
62
|
|
|
24
63
|
/** Copy ROUTING.md (SKILLS_DIR substituted; OpenCode body normalized). */
|
|
25
64
|
function copyRoutingFile(ctx, platformKey) {
|
|
@@ -105,15 +144,28 @@ function copyClaudeMdFile(ctx, platformKey) {
|
|
|
105
144
|
return;
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
147
|
+
const tracker = ctx.trackers.claude;
|
|
148
|
+
// Older releases recorded the root file relative to .claude/. Never retain
|
|
149
|
+
// that whole-file ownership claim, even when this run makes no content change.
|
|
150
|
+
if (tracker) tracker.prune('../CLAUDE.md');
|
|
151
|
+
|
|
152
|
+
const template = fs.readFileSync(src, 'utf8');
|
|
153
|
+
const exists = fs.existsSync(dest);
|
|
154
|
+
const existing = exists ? fs.readFileSync(dest, 'utf8') : '';
|
|
155
|
+
const legacyRecord = ctx.ownership?.[PLATFORMS.claude.folder]?.files?.['../CLAUDE.md'];
|
|
156
|
+
const legacyOwned = Boolean(legacyRecord && legacyRecord.sha256 === sha256(existing));
|
|
157
|
+
const next = upsertManagedClaudeBlock(existing, template, legacyOwned);
|
|
158
|
+
const action = !exists ? 'created' : next === existing ? 'unchanged' : 'updated';
|
|
159
|
+
|
|
160
|
+
if (!ctx.dryRun && action !== 'unchanged') {
|
|
161
|
+
fs.writeFileSync(dest, next, 'utf8');
|
|
162
|
+
}
|
|
111
163
|
report(ctx, action, 'CLAUDE.md');
|
|
112
164
|
}
|
|
113
165
|
|
|
114
166
|
/** Copy the rules/ tree (ownership-aware; no longer force-overwrites). */
|
|
115
167
|
function copyRulesDirectory(ctx, platformKey) {
|
|
116
|
-
if (!
|
|
168
|
+
if (!hasPlatformCapability(platformKey, 'rules')) return;
|
|
117
169
|
|
|
118
170
|
const src = path.join(SRC, 'claude/rules');
|
|
119
171
|
const dest = getRuntimeSupportTargetDir(platformKey, 'rules');
|
|
@@ -123,9 +175,7 @@ function copyRulesDirectory(ctx, platformKey) {
|
|
|
123
175
|
return;
|
|
124
176
|
}
|
|
125
177
|
|
|
126
|
-
const transform = platformKey
|
|
127
|
-
? getCopyOptions('opencode', {}).transform
|
|
128
|
-
: undefined;
|
|
178
|
+
const transform = getCopyOptions(platformKey, {}).transform;
|
|
129
179
|
const agg = copyManagedTree({
|
|
130
180
|
src, dest, platformFolder: PLATFORMS[platformKey].folder, ctx, tracker: ctx.trackers[platformKey], transform
|
|
131
181
|
});
|
|
@@ -137,5 +187,6 @@ module.exports = {
|
|
|
137
187
|
copyClaudeRuntimeFiles,
|
|
138
188
|
removeObsoleteClaudeRuntimeFiles,
|
|
139
189
|
copyClaudeMdFile,
|
|
140
|
-
copyRulesDirectory
|
|
190
|
+
copyRulesDirectory,
|
|
191
|
+
transformManagedClaudeContent
|
|
141
192
|
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { PLATFORMS } = require('../lib/context');
|
|
6
|
+
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
7
|
+
const { report, treeAction } = require('./report');
|
|
8
|
+
const { copyRulesDirectory } = require('./claude-runtime');
|
|
9
|
+
const {
|
|
10
|
+
normalizeCodexBody,
|
|
11
|
+
upsertManagedCodexBlock
|
|
12
|
+
} = require('../lib/codex-install');
|
|
13
|
+
|
|
14
|
+
const SRC = path.join(__dirname, '../../src');
|
|
15
|
+
const CODEX_SRC = path.join(SRC, 'codex');
|
|
16
|
+
|
|
17
|
+
const CODEX_OWN_RUNTIME = [
|
|
18
|
+
['gitignore', '.gitignore'],
|
|
19
|
+
['runtime.json', 'runtime.json'],
|
|
20
|
+
['hooks.json', 'hooks.json']
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function writeSourceFile(ctx, platformKey, src, dest, label, transform) {
|
|
24
|
+
const platform = PLATFORMS[platformKey];
|
|
25
|
+
const { action } = writeManagedFile({
|
|
26
|
+
src,
|
|
27
|
+
dest,
|
|
28
|
+
platformFolder: platform.folder,
|
|
29
|
+
ctx,
|
|
30
|
+
tracker: ctx.trackers[platformKey],
|
|
31
|
+
transform
|
|
32
|
+
});
|
|
33
|
+
report(ctx, action, label);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function installRuntimeFiles(ctx, platformKey) {
|
|
37
|
+
const platform = PLATFORMS[platformKey];
|
|
38
|
+
for (const [sourceRel, targetRel] of CODEX_OWN_RUNTIME) {
|
|
39
|
+
writeSourceFile(
|
|
40
|
+
ctx,
|
|
41
|
+
platformKey,
|
|
42
|
+
path.join(CODEX_SRC, sourceRel),
|
|
43
|
+
path.join(platform.folder, targetRel),
|
|
44
|
+
`Codex runtime: ${targetRel}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
writeSourceFile(
|
|
48
|
+
ctx,
|
|
49
|
+
platformKey,
|
|
50
|
+
path.join(CODEX_SRC, 'agents-gitignore'),
|
|
51
|
+
path.join('.agents', '.gitignore'),
|
|
52
|
+
'Codex skills: .agents/.gitignore'
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const agg = copyManagedTree({
|
|
56
|
+
src: path.join(CODEX_SRC, 'hooks'),
|
|
57
|
+
dest: path.join(platform.folder, 'hooks'),
|
|
58
|
+
platformFolder: platform.folder,
|
|
59
|
+
ctx,
|
|
60
|
+
tracker: ctx.trackers[platformKey]
|
|
61
|
+
});
|
|
62
|
+
report(ctx, treeAction(agg), 'Codex native hooks');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function installNativeRuleOverrides(ctx, platformKey) {
|
|
66
|
+
const platform = PLATFORMS[platformKey];
|
|
67
|
+
const source = path.join(CODEX_SRC, 'rules');
|
|
68
|
+
if (!fs.existsSync(source)) return;
|
|
69
|
+
const agg = copyManagedTree({
|
|
70
|
+
src: source,
|
|
71
|
+
dest: path.join(platform.folder, 'rules'),
|
|
72
|
+
platformFolder: platform.folder,
|
|
73
|
+
ctx,
|
|
74
|
+
tracker: ctx.trackers[platformKey]
|
|
75
|
+
});
|
|
76
|
+
report(ctx, treeAction(agg), 'Codex native rule overrides');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function installManagedAgentsMd(ctx) {
|
|
80
|
+
const source = path.join(CODEX_SRC, 'AGENTS.md');
|
|
81
|
+
if (!fs.existsSync(source)) {
|
|
82
|
+
report(ctx, 'missing', 'Codex AGENTS.md template');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const destination = 'AGENTS.md';
|
|
87
|
+
const exists = fs.existsSync(destination);
|
|
88
|
+
const existing = exists ? fs.readFileSync(destination, 'utf8') : '';
|
|
89
|
+
const block = normalizeCodexBody(fs.readFileSync(source, 'utf8'))
|
|
90
|
+
.replace(/^# CLAUDE\.md\s*$/m, '# CafeKit for Codex CLI')
|
|
91
|
+
.trim();
|
|
92
|
+
const next = upsertManagedCodexBlock(existing, block);
|
|
93
|
+
const action = !exists ? 'created' : next === existing ? 'unchanged' : 'updated';
|
|
94
|
+
|
|
95
|
+
if (!ctx.dryRun && action !== 'unchanged') {
|
|
96
|
+
fs.writeFileSync(destination, next, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
report(ctx, action, 'AGENTS.md (CafeKit Codex block)');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function installCodexRuntime(ctx, platformKey) {
|
|
102
|
+
if (platformKey !== 'codex') return ctx;
|
|
103
|
+
installRuntimeFiles(ctx, platformKey);
|
|
104
|
+
copyRulesDirectory(ctx, platformKey);
|
|
105
|
+
installNativeRuleOverrides(ctx, platformKey);
|
|
106
|
+
installManagedAgentsMd(ctx);
|
|
107
|
+
return ctx;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { installCodexRuntime };
|
|
@@ -11,7 +11,7 @@ const path = require('path');
|
|
|
11
11
|
const {
|
|
12
12
|
PLATFORMS,
|
|
13
13
|
DEPENDENCY_TEMPLATES,
|
|
14
|
-
|
|
14
|
+
hasPlatformCapability,
|
|
15
15
|
getRuntimeSupportTargetDir,
|
|
16
16
|
getCopyOptions
|
|
17
17
|
} = require('../lib/context');
|
|
@@ -21,6 +21,10 @@ const {
|
|
|
21
21
|
convertOpenCodeAgentContent,
|
|
22
22
|
convertOpenCodeCommandContent
|
|
23
23
|
} = require('../lib/opencode-install');
|
|
24
|
+
const {
|
|
25
|
+
codexAgentName,
|
|
26
|
+
convertCodexAgentContent
|
|
27
|
+
} = require('../lib/codex-install');
|
|
24
28
|
|
|
25
29
|
const SRC = path.join(__dirname, '../../src');
|
|
26
30
|
|
|
@@ -53,6 +57,7 @@ function getPlatformSpecFiles(platformKey, ctx) {
|
|
|
53
57
|
function ensureWorkflowDependencies(ctx, platformKey) {
|
|
54
58
|
const platform = PLATFORMS[platformKey];
|
|
55
59
|
const tracker = ctx.trackers[platformKey];
|
|
60
|
+
if (!platform.commandsDir) return;
|
|
56
61
|
const commandTemplates = DEPENDENCY_TEMPLATES.commands[platformKey] || {};
|
|
57
62
|
Object.entries(commandTemplates).forEach(([fileName, content]) => {
|
|
58
63
|
if (!content) return;
|
|
@@ -72,20 +77,18 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
72
77
|
const platformFolder = platform.folder;
|
|
73
78
|
|
|
74
79
|
// For OpenCode, text bodies are path-normalized (.claude → .opencode, etc.).
|
|
75
|
-
const bodyTransform = platformKey
|
|
76
|
-
? getCopyOptions('opencode', {}).transform
|
|
77
|
-
: undefined;
|
|
80
|
+
const bodyTransform = getCopyOptions(platformKey, {}).transform;
|
|
78
81
|
|
|
79
82
|
const skillsSourceDir = path.join(SRC, 'claude/skills');
|
|
80
83
|
const agentsSourceDir = path.join(SRC, platform.sourceDir, 'agents');
|
|
81
84
|
|
|
82
85
|
if (!ctx.dryRun) {
|
|
83
|
-
fs.mkdirSync(platform.skillsDir, { recursive: true });
|
|
84
|
-
fs.mkdirSync(platform.agentsDir, { recursive: true });
|
|
86
|
+
if (platform.skillsDir) fs.mkdirSync(platform.skillsDir, { recursive: true });
|
|
87
|
+
if (platform.agentsDir) fs.mkdirSync(platform.agentsDir, { recursive: true });
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
// ── Skills ──────────────────────────────────────────────
|
|
88
|
-
if (fs.existsSync(skillsSourceDir)) {
|
|
91
|
+
if (hasPlatformCapability(platformKey, 'skills') && fs.existsSync(skillsSourceDir)) {
|
|
89
92
|
const specSkillSource = path.join(skillsSourceDir, 'specs');
|
|
90
93
|
const specSkillDest = path.join(platform.skillsDir, 'specs');
|
|
91
94
|
|
|
@@ -98,9 +101,9 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
// Keep spec templates in sync (Claude command runtime reads these).
|
|
101
|
-
if (platformKey === 'claude') {
|
|
104
|
+
if (platformKey === 'claude' || platformKey === 'codex') {
|
|
102
105
|
const legacyInitTemplate = path.join(platform.skillsDir, 'specs', 'templates', 'init.json');
|
|
103
|
-
if (fs.existsSync(legacyInitTemplate)) {
|
|
106
|
+
if (platformKey === 'claude' && fs.existsSync(legacyInitTemplate)) {
|
|
104
107
|
if (!ctx.dryRun) fs.rmSync(legacyInitTemplate, { force: true });
|
|
105
108
|
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed legacy template: ${legacyInitTemplate}`);
|
|
106
109
|
ctx.results.updated++;
|
|
@@ -118,14 +121,21 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
118
121
|
specTemplates.forEach((fileName) => {
|
|
119
122
|
const src = path.join(specSkillSource, 'templates', fileName);
|
|
120
123
|
const dest = path.join(platform.skillsDir, 'specs', 'templates', fileName);
|
|
121
|
-
const { action } = writeManagedFile({
|
|
124
|
+
const { action } = writeManagedFile({
|
|
125
|
+
src,
|
|
126
|
+
dest,
|
|
127
|
+
platformFolder,
|
|
128
|
+
ctx,
|
|
129
|
+
tracker,
|
|
130
|
+
transform: bodyTransform
|
|
131
|
+
});
|
|
122
132
|
report(ctx, action, `template: ${fileName}`);
|
|
123
133
|
});
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
// Additional required skills.
|
|
127
137
|
let requiredSkills = [];
|
|
128
|
-
if (
|
|
138
|
+
if (hasPlatformCapability(platformKey, 'skills')) {
|
|
129
139
|
requiredSkills = ctx.manifest?.skills?.required || [];
|
|
130
140
|
}
|
|
131
141
|
requiredSkills
|
|
@@ -146,18 +156,24 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
// ── Agents + references + scripts ───────────────────────
|
|
149
|
-
if (
|
|
159
|
+
if (hasPlatformCapability(platformKey, 'agents')) {
|
|
150
160
|
if (fs.existsSync(agentsSourceDir)) {
|
|
151
161
|
const requiredAgents = ctx.manifest?.agents?.required || [
|
|
152
162
|
'tester.md', 'code-reviewer.md', 'fullstack-developer.md', 'debugger.md'
|
|
153
163
|
];
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
let agentTransform;
|
|
165
|
+
if (platformKey === 'opencode') {
|
|
166
|
+
agentTransform = (content, src) => convertOpenCodeAgentContent(content, path.basename(src));
|
|
167
|
+
} else if (platformKey === 'codex') {
|
|
168
|
+
agentTransform = (content, src) => convertCodexAgentContent(content, path.basename(src));
|
|
169
|
+
}
|
|
157
170
|
|
|
158
171
|
requiredAgents.forEach((fileName) => {
|
|
159
172
|
const src = path.join(agentsSourceDir, fileName);
|
|
160
|
-
const
|
|
173
|
+
const destName = platformKey === 'codex'
|
|
174
|
+
? `${codexAgentName(fileName)}.toml`
|
|
175
|
+
: fileName;
|
|
176
|
+
const dest = path.join(platform.agentsDir, destName);
|
|
161
177
|
const { action } = writeManagedFile({
|
|
162
178
|
src, dest, platformFolder, ctx, tracker, transform: agentTransform
|
|
163
179
|
});
|
|
@@ -189,6 +205,10 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
189
205
|
ensureWorkflowDependencies(ctx, platformKey);
|
|
190
206
|
|
|
191
207
|
// ── Commands ────────────────────────────────────────────
|
|
208
|
+
if (!hasPlatformCapability(platformKey, 'commands') || !platform.commandsDir) {
|
|
209
|
+
return ctx;
|
|
210
|
+
}
|
|
211
|
+
|
|
192
212
|
const sourceSubdir = platform.sourceSubdir || 'commands';
|
|
193
213
|
const commandsSourceDir = path.join(SRC, platform.sourceDir, sourceSubdir);
|
|
194
214
|
const specFiles = getPlatformSpecFiles(platformKey, ctx);
|
|
@@ -16,6 +16,59 @@ const path = require('path');
|
|
|
16
16
|
const { PLATFORMS } = require('../lib/context');
|
|
17
17
|
const { LANGUAGE_LABELS } = require('../lib/i18n');
|
|
18
18
|
const { setupOpenCodeModel } = require('../lib/opencode-install');
|
|
19
|
+
const { transformManagedCodexContent } = require('../lib/codex-install');
|
|
20
|
+
const { transformManagedClaudeContent } = require('./claude-runtime');
|
|
21
|
+
const ASSISTANT_NAMES = {
|
|
22
|
+
claude: 'Claude Code',
|
|
23
|
+
codex: 'Codex CLI',
|
|
24
|
+
opencode: 'OpenCode'
|
|
25
|
+
};
|
|
26
|
+
const LANGUAGE_LABEL_BY_CODE = {
|
|
27
|
+
vi: 'Vietnamese',
|
|
28
|
+
ja: 'Japanese',
|
|
29
|
+
en: 'English'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function readManagedBody(filePath, transformManagedContent) {
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
34
|
+
let body = '';
|
|
35
|
+
transformManagedContent(content, (managed) => {
|
|
36
|
+
body = managed;
|
|
37
|
+
return managed;
|
|
38
|
+
});
|
|
39
|
+
return body;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function instructionTargets(ctx) {
|
|
43
|
+
const targets = {
|
|
44
|
+
claude: {
|
|
45
|
+
file: path.join(process.cwd(), 'CLAUDE.md'),
|
|
46
|
+
transform: transformManagedClaudeContent
|
|
47
|
+
},
|
|
48
|
+
codex: {
|
|
49
|
+
file: path.join(process.cwd(), 'AGENTS.md'),
|
|
50
|
+
transform: transformManagedCodexContent
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return Object.keys(targets)
|
|
54
|
+
.filter((key) => ctx.platforms.includes(key))
|
|
55
|
+
.map((key) => ({ key, ...targets[key] }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function updateInstructionTarget(target, updateManaged) {
|
|
59
|
+
if (!fs.existsSync(target.file)) return false;
|
|
60
|
+
const content = fs.readFileSync(target.file, 'utf8');
|
|
61
|
+
const next = target.transform(content, updateManaged);
|
|
62
|
+
if (next === content) return false;
|
|
63
|
+
fs.writeFileSync(target.file, next, 'utf8');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function trackerOwnsPath(tracker, filePath) {
|
|
68
|
+
if (!tracker?.keyFor) return false;
|
|
69
|
+
const trackedKey = tracker.keyFor(filePath);
|
|
70
|
+
return tracker.recorded(trackedKey) !== null;
|
|
71
|
+
}
|
|
19
72
|
|
|
20
73
|
/** Write `"language"` field into .claude/settings.json so it's visible at a glance. */
|
|
21
74
|
function patchSettingsLanguage(ctx) {
|
|
@@ -46,17 +99,12 @@ function patchSettingsLanguage(ctx) {
|
|
|
46
99
|
* can update it idempotently.
|
|
47
100
|
*/
|
|
48
101
|
function patchLanguageSection(ctx) {
|
|
49
|
-
if (!ctx.platforms.includes('claude')) return;
|
|
50
102
|
// Skip when locale is English (default — no override needed in CLAUDE.md).
|
|
51
103
|
const locale = ctx.locale || ctx.lang;
|
|
52
104
|
if (!locale || locale === 'en' || locale === 'English') return;
|
|
53
105
|
|
|
54
|
-
const claudeMdFile = path.join(process.cwd(), 'CLAUDE.md');
|
|
55
|
-
if (!fs.existsSync(claudeMdFile)) return;
|
|
56
|
-
|
|
57
|
-
const LANG_LABEL = { vi: 'Vietnamese', ja: 'Japanese', en: 'English' };
|
|
58
106
|
// Use locale directly when set (e.g. "Korean"), or map from lang code.
|
|
59
|
-
const label = locale in
|
|
107
|
+
const label = locale in LANGUAGE_LABEL_BY_CODE ? LANGUAGE_LABEL_BY_CODE[locale] : locale;
|
|
60
108
|
|
|
61
109
|
const newSection = `## Language Consistency <!-- cafekit:lang -->
|
|
62
110
|
|
|
@@ -64,21 +112,18 @@ Always respond in **${label}**. Technical terms, code identifiers, and file path
|
|
|
64
112
|
|
|
65
113
|
`;
|
|
66
114
|
|
|
67
|
-
|
|
68
|
-
// Replace if marker present, else replace the generic section.
|
|
115
|
+
// Replace only inside the installer-owned block.
|
|
69
116
|
const markerRe = /## Language Consistency <!-- cafekit:lang -->[\s\S]*?(?=\n##|\n*$)/;
|
|
70
117
|
const genericRe = /## Language Consistency\n[\s\S]*?(?=\n##|\n*$)/;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
content += `\n${newSection}\n`;
|
|
118
|
+
for (const target of instructionTargets(ctx)) {
|
|
119
|
+
updateInstructionTarget(target, (managed) => {
|
|
120
|
+
if (markerRe.test(managed)) return managed.replace(markerRe, newSection);
|
|
121
|
+
if (genericRe.test(managed)) return managed.replace(genericRe, newSection);
|
|
122
|
+
return `${managed.trimEnd()}\n\n${newSection}\n`;
|
|
123
|
+
});
|
|
78
124
|
}
|
|
79
|
-
|
|
80
|
-
fs.writeFileSync(claudeMdFile, content, 'utf8');
|
|
81
125
|
}
|
|
126
|
+
|
|
82
127
|
function patchRuntimeLocale(ctx) {
|
|
83
128
|
for (const key of ctx.platforms) {
|
|
84
129
|
const rtPath = path.join(PLATFORMS[key].folder, 'runtime.json');
|
|
@@ -93,51 +138,59 @@ function patchRuntimeLocale(ctx) {
|
|
|
93
138
|
// Use locale (freeform label) so custom languages propagate to the AI hook.
|
|
94
139
|
const locale = ctx.locale || ctx.lang;
|
|
95
140
|
if (data.locale.responseLanguage === locale) continue;
|
|
141
|
+
// Hardening: never downgrade an existing configured label to a bare default
|
|
142
|
+
// code when this run never made an explicit language choice (ctx.locale
|
|
143
|
+
// empty). Protects user config on any code path that skips selectLanguage.
|
|
144
|
+
if (!ctx.locale && data.locale.responseLanguage) continue;
|
|
96
145
|
data.locale.responseLanguage = locale;
|
|
97
146
|
fs.writeFileSync(rtPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
98
|
-
|
|
147
|
+
// A preserved user-created/user-modified runtime was deliberately not
|
|
148
|
+
// recorded by managed-writer. Updating its explicit locale must not turn
|
|
149
|
+
// the whole user file into a pristine CafeKit ownership baseline.
|
|
150
|
+
const tracker = ctx.trackers[key];
|
|
151
|
+
if (trackerOwnsPath(tracker, rtPath)) tracker.record(rtPath);
|
|
99
152
|
}
|
|
100
153
|
}
|
|
101
154
|
|
|
102
155
|
/** Human assistant name for the active platform(s). */
|
|
103
|
-
function assistantName(
|
|
104
|
-
return
|
|
156
|
+
function assistantName(platformKey) {
|
|
157
|
+
return ASSISTANT_NAMES[platformKey] || ASSISTANT_NAMES.opencode;
|
|
105
158
|
}
|
|
106
159
|
|
|
107
160
|
function configureAddressing(ctx, userAddress) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const name = assistantName(ctx);
|
|
115
|
-
let content = fs.readFileSync(claudeMdFile, 'utf8');
|
|
116
|
-
const addressingSection = `## Addressing (Context Overflow Indicator)
|
|
161
|
+
let found = false;
|
|
162
|
+
for (const target of instructionTargets(ctx)) {
|
|
163
|
+
if (!fs.existsSync(target.file)) continue;
|
|
164
|
+
found = true;
|
|
165
|
+
const name = assistantName(target.key);
|
|
166
|
+
const addressingSection = `## Addressing (Context Overflow Indicator)
|
|
117
167
|
|
|
118
168
|
${name} always addresses the user as "${userAddress}" throughout the conversation. If it stops doing so, it is a sign the context has been compacted/truncated — tell the user to consider \`/clear\`.`;
|
|
119
169
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
170
|
+
const regex = /##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?(?=\n##|\n*$)/;
|
|
171
|
+
const changed = updateInstructionTarget(target, (managed) => (
|
|
172
|
+
regex.test(managed)
|
|
173
|
+
? managed.replace(regex, addressingSection)
|
|
174
|
+
: `${managed.endsWith('\n') ? managed : `${managed}\n`}\n${addressingSection}\n`
|
|
175
|
+
));
|
|
176
|
+
if (changed) {
|
|
177
|
+
ctx.ui.success(ctx.t('addressingSet', { name, addr: userAddress }));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!found) ctx.ui.warn('Project instruction file not found; skipped addressing');
|
|
128
181
|
}
|
|
129
182
|
|
|
130
183
|
async function setupAddressing(ctx) {
|
|
131
|
-
if (!ctx.platforms.
|
|
184
|
+
if (!ctx.platforms.some((key) => key === 'claude' || key === 'codex')) return;
|
|
132
185
|
|
|
133
|
-
// Check if there's already an addressing section in CLAUDE.md
|
|
134
|
-
const claudeMdFile = path.join(process.cwd(), 'CLAUDE.md');
|
|
135
186
|
let existingName = null;
|
|
136
|
-
|
|
137
|
-
|
|
187
|
+
for (const target of instructionTargets(ctx)) {
|
|
188
|
+
if (!fs.existsSync(target.file)) continue;
|
|
189
|
+
const content = readManagedBody(target.file, target.transform);
|
|
138
190
|
const match = content.match(/##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?always addresses the user as "([^"]+)"/);
|
|
139
191
|
if (match) {
|
|
140
192
|
existingName = match[1];
|
|
193
|
+
break;
|
|
141
194
|
}
|
|
142
195
|
}
|
|
143
196
|
|
|
@@ -199,15 +252,12 @@ async function runPostInstall(ctx) {
|
|
|
199
252
|
// Write "language" field into .claude/settings.json for visibility.
|
|
200
253
|
patchSettingsLanguage(ctx);
|
|
201
254
|
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
if (ctx.platforms.includes('claude') && ctx.trackers.claude && fs.existsSync('CLAUDE.md')) {
|
|
205
|
-
ctx.trackers.claude.record('CLAUDE.md');
|
|
206
|
-
}
|
|
255
|
+
// Flush touched platform trackers. Root CLAUDE.md is block-managed and must
|
|
256
|
+
// never regain a whole-file ownership record.
|
|
207
257
|
for (const key of ctx.platforms) {
|
|
208
258
|
if (ctx.trackers[key]) ctx.trackers[key].write();
|
|
209
259
|
}
|
|
210
260
|
return ctx;
|
|
211
261
|
}
|
|
212
262
|
|
|
213
|
-
module.exports = { configureAddressing, runPostInstall };
|
|
263
|
+
module.exports = { configureAddressing, patchRuntimeLocale, runPostInstall };
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Ensures the root .gitignore carries CafeKit-managed patterns:
|
|
5
5
|
* - workflow dirs (plans, shared research)
|
|
6
6
|
* - install safety artifacts (backup, lock)
|
|
7
|
-
* - runtime folders (.claude/, .opencode/) — reinstall via
|
|
7
|
+
* - runtime folders (.claude/, .opencode/, .codex/, .agents/) — reinstall via
|
|
8
8
|
* `npx @haposoft/cafekit`; do not commit the local payload
|
|
9
9
|
*
|
|
10
10
|
* Layered with the in-folder `.claude/.gitignore` /
|
|
@@ -35,7 +35,9 @@ function ensureGitignore(ctx) {
|
|
|
35
35
|
'.cafekit.lock',
|
|
36
36
|
// Local runtime payload — reinstall with npx; keep out of git
|
|
37
37
|
'.claude/',
|
|
38
|
-
'.opencode/'
|
|
38
|
+
'.opencode/',
|
|
39
|
+
'.codex/',
|
|
40
|
+
'.agents/'
|
|
39
41
|
];
|
|
40
42
|
const prefix = ctx.dryRun ? '[dry-run] ' : '';
|
|
41
43
|
|