@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { splitFrontmatter } = require('./codex-frontmatter');
|
|
5
|
+
|
|
6
|
+
const CODEX_BLOCK_START = '<!-- CAFEKIT CODEX START -->';
|
|
7
|
+
const CODEX_BLOCK_END = '<!-- CAFEKIT CODEX END -->';
|
|
8
|
+
const INSTRUCTION_EXTENSIONS = new Set(['.md', '.mdx', '.txt']);
|
|
9
|
+
|
|
10
|
+
const AGENT_NAMES = [
|
|
11
|
+
'brainstormer',
|
|
12
|
+
'code-auditor',
|
|
13
|
+
'debugger',
|
|
14
|
+
'deployer',
|
|
15
|
+
'docs-keeper',
|
|
16
|
+
'git-ops',
|
|
17
|
+
'god-developer',
|
|
18
|
+
'inspector',
|
|
19
|
+
'project-manager',
|
|
20
|
+
'researcher',
|
|
21
|
+
'spec-maker',
|
|
22
|
+
'test-runner',
|
|
23
|
+
'ui-ux-designer'
|
|
24
|
+
];
|
|
25
|
+
const INSTRUCTION_REPLACEMENTS = [
|
|
26
|
+
[/`?\bAskUserQuestion\b`?/g, 'a structured user-input request'],
|
|
27
|
+
[/`?\bTodoWrite\b`?/g, 'task-state tracking'],
|
|
28
|
+
[/`?\bTaskCreate\b`?/g, 'task creation'],
|
|
29
|
+
[/`?\bTaskUpdate\b`?/g, 'task-state updates'],
|
|
30
|
+
[/`?\bTaskList\b`?/g, 'task-state inspection'],
|
|
31
|
+
[/`?\bTaskGet\b`?/g, 'task detail inspection'],
|
|
32
|
+
[/`Bash`/g, '`exec_command`'],
|
|
33
|
+
[/`Glob`/g, '`rg --files`'],
|
|
34
|
+
[/`Grep`/g, '`rg`'],
|
|
35
|
+
[/`Read`/g, 'targeted file reads'],
|
|
36
|
+
[/`Write`|`Edit`|`NotebookEdit`/g, '`apply_patch`'],
|
|
37
|
+
[/`WebSearch`/g, 'native web search'],
|
|
38
|
+
[/`WebFetch`/g, 'native page retrieval'],
|
|
39
|
+
[/`?Agent`?\s+\(legacy\s+`?Task`?\)/g, 'subagent delegation']
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function codexAgentName(value) {
|
|
43
|
+
const raw = path.basename(String(value || ''), path.extname(String(value || '')));
|
|
44
|
+
return raw
|
|
45
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
46
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
47
|
+
.replace(/^_+|_+$/g, '')
|
|
48
|
+
.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeAgentRole(name) {
|
|
52
|
+
return String(name || '').toLowerCase() === 'explore'
|
|
53
|
+
? 'explorer'
|
|
54
|
+
: codexAgentName(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function applyReplacements(content, replacements) {
|
|
58
|
+
let next = content;
|
|
59
|
+
for (const [pattern, replacement] of replacements) {
|
|
60
|
+
next = next.replace(pattern, replacement);
|
|
61
|
+
}
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeRuntimePaths(content) {
|
|
66
|
+
return content
|
|
67
|
+
.replace(/\.claude\/skills\b/g, '.agents/skills')
|
|
68
|
+
.replace(/\.claude\\skills\b/g, '.agents\\skills')
|
|
69
|
+
.replace(/\.claude(?=[/\\])/g, '.codex')
|
|
70
|
+
.replace(/(['"])\.claude\1/g, '$1.codex$1')
|
|
71
|
+
.replace(/`\.claude`/g, '`.codex`');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeSkillNames(content) {
|
|
75
|
+
return content
|
|
76
|
+
.replace(/\/hapo:([a-z0-9-]+)/gi, (_match, name) => `$hapo-${name}`)
|
|
77
|
+
.replace(/\bhapo:([a-z0-9-]+)/gi, (_match, name) => `hapo-${name}`)
|
|
78
|
+
.replace(
|
|
79
|
+
/(^|[\s("'`→])\/(brainstorm|code-review|debug|develop|docs|frontend-design|git|hotfix|inspect|question|research|specs|test)(?=$|[\s<`),.:])/gim,
|
|
80
|
+
(_match, prefix, name) => `${prefix}$hapo-${name}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeAgentNames(content) {
|
|
85
|
+
let next = content;
|
|
86
|
+
for (const name of AGENT_NAMES) {
|
|
87
|
+
const snake = codexAgentName(name);
|
|
88
|
+
next = next.replace(new RegExp(`\\b${name}\\b`, 'g'), snake);
|
|
89
|
+
}
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeAgentInvocations(content) {
|
|
94
|
+
return content
|
|
95
|
+
.replace(/\bAgent\s*\(/g, 'spawn_agent(')
|
|
96
|
+
.replace(/\bsubagent_type\s*=\s*"([^"]+)"/g, (_match, name) => (
|
|
97
|
+
`agent_type="${normalizeAgentRole(name)}", fork_turns="none"`
|
|
98
|
+
))
|
|
99
|
+
.replace(/\bsubagent_type\s*:\s*"([^"]+)"/g, (_match, name) => (
|
|
100
|
+
`agent_type: "${normalizeAgentRole(name)}"\nfork_turns: "none"`
|
|
101
|
+
))
|
|
102
|
+
.replace(/\bprompt\s*=/g, 'message=')
|
|
103
|
+
.replace(/\bdescription\s*=\s*"([^"]+)"/g, (_match, label) => (
|
|
104
|
+
`task_name="${codexAgentName(label) || 'cafekit_task'}"`
|
|
105
|
+
))
|
|
106
|
+
.replace(/`Agent`/g, '`spawn_agent`')
|
|
107
|
+
.replace(/\bAgent tool\b/g, 'spawn_agent tool')
|
|
108
|
+
.replace(/\bExplore subagents?\b/g, (match) => match.replace('Explore', 'explorer'))
|
|
109
|
+
.replace(/`SendMessage`/g, '`send_message`')
|
|
110
|
+
.replace(/\bSendMessage\s*\(/g, 'send_message(');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isInstructionAsset(sourcePath) {
|
|
114
|
+
if (!sourcePath) return true;
|
|
115
|
+
return INSTRUCTION_EXTENSIONS.has(path.extname(sourcePath).toLowerCase());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeCodexBody(content, sourcePath = '') {
|
|
119
|
+
let next = normalizeRuntimePaths(String(content));
|
|
120
|
+
next = next
|
|
121
|
+
.replace(/\bCLAUDE\.md\b/g, 'AGENTS.md')
|
|
122
|
+
.replace(/\bClaude Code\b/g, 'Codex CLI');
|
|
123
|
+
|
|
124
|
+
if (!isInstructionAsset(sourcePath)) return next;
|
|
125
|
+
|
|
126
|
+
next = normalizeAgentInvocations(normalizeAgentNames(normalizeSkillNames(next)));
|
|
127
|
+
return applyReplacements(next, INSTRUCTION_REPLACEMENTS);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getCodexCopyOptions(baseOptions = {}) {
|
|
131
|
+
return {
|
|
132
|
+
...baseOptions,
|
|
133
|
+
transform: (content, sourcePath) => normalizeCodexBody(content, sourcePath)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function convertCodexAgentContent(content, fileName = '') {
|
|
138
|
+
const { frontmatter, body } = splitFrontmatter(content);
|
|
139
|
+
const name = codexAgentName(frontmatter.name || fileName);
|
|
140
|
+
const description = normalizeCodexBody(
|
|
141
|
+
frontmatter.description || `CafeKit Codex agent: ${name}`
|
|
142
|
+
);
|
|
143
|
+
const instructions = normalizeCodexBody(body).trim();
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
`name = ${JSON.stringify(name)}`,
|
|
147
|
+
`description = ${JSON.stringify(description)}`,
|
|
148
|
+
`developer_instructions = ${JSON.stringify(instructions)}`,
|
|
149
|
+
''
|
|
150
|
+
].join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function managedRange(content) {
|
|
154
|
+
const start = content.indexOf(CODEX_BLOCK_START);
|
|
155
|
+
const end = content.indexOf(CODEX_BLOCK_END);
|
|
156
|
+
if (start < 0 && end < 0) return null;
|
|
157
|
+
const duplicateStart = content.indexOf(CODEX_BLOCK_START, start + CODEX_BLOCK_START.length);
|
|
158
|
+
const duplicateEnd = content.indexOf(CODEX_BLOCK_END, end + CODEX_BLOCK_END.length);
|
|
159
|
+
return start >= 0 && end > start && duplicateStart < 0 && duplicateEnd < 0
|
|
160
|
+
? { start, end }
|
|
161
|
+
: false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function upsertManagedCodexBlock(existingContent, blockContent) {
|
|
165
|
+
const existing = String(existingContent || '');
|
|
166
|
+
const block = `${CODEX_BLOCK_START}\n${String(blockContent).trim()}\n${CODEX_BLOCK_END}`;
|
|
167
|
+
const range = managedRange(existing);
|
|
168
|
+
if (range === false) return existing;
|
|
169
|
+
if (range) {
|
|
170
|
+
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end + CODEX_BLOCK_END.length)}`;
|
|
171
|
+
}
|
|
172
|
+
if (!existing) return `${block}\n`;
|
|
173
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
174
|
+
return `${existing}${separator}${block}\n`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Transform only the CafeKit-owned body, preserving all surrounding bytes. */
|
|
178
|
+
function transformManagedCodexContent(content, transform) {
|
|
179
|
+
const existing = String(content || '');
|
|
180
|
+
const range = managedRange(existing);
|
|
181
|
+
if (!range || typeof transform !== 'function') return existing;
|
|
182
|
+
const bodyStart = range.start + CODEX_BLOCK_START.length;
|
|
183
|
+
const bodyEnd = range.end;
|
|
184
|
+
const body = existing.slice(bodyStart, bodyEnd);
|
|
185
|
+
return `${existing.slice(0, bodyStart)}${transform(body)}${existing.slice(bodyEnd)}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
normalizeCodexBody,
|
|
190
|
+
getCodexCopyOptions,
|
|
191
|
+
codexAgentName,
|
|
192
|
+
convertCodexAgentContent,
|
|
193
|
+
upsertManagedCodexBlock,
|
|
194
|
+
transformManagedCodexContent
|
|
195
|
+
};
|
package/bin/lib/context.js
CHANGED
|
@@ -56,11 +56,13 @@ function loadClaudeMigrationManifest() {
|
|
|
56
56
|
const DEPENDENCY_TEMPLATES = {
|
|
57
57
|
commands: {
|
|
58
58
|
claude: {},
|
|
59
|
-
opencode: {}
|
|
59
|
+
opencode: {},
|
|
60
|
+
codex: {}
|
|
60
61
|
},
|
|
61
62
|
agents: {
|
|
62
63
|
claude: {},
|
|
63
|
-
opencode: {}
|
|
64
|
+
opencode: {},
|
|
65
|
+
codex: {}
|
|
64
66
|
}
|
|
65
67
|
};
|
|
66
68
|
|
|
@@ -79,7 +81,16 @@ const PLATFORMS = {
|
|
|
79
81
|
skillsRef: '.claude/skills',
|
|
80
82
|
commandPrefix: '/',
|
|
81
83
|
sourceDir: 'claude', // Maps to src/claude/
|
|
82
|
-
sourceSubdir: 'commands'
|
|
84
|
+
sourceSubdir: 'commands', // Source subfolder within src/claude/
|
|
85
|
+
backupTargets: ['.claude', 'CLAUDE.md'],
|
|
86
|
+
capabilities: {
|
|
87
|
+
skills: true,
|
|
88
|
+
agents: true,
|
|
89
|
+
references: true,
|
|
90
|
+
scripts: true,
|
|
91
|
+
rules: true,
|
|
92
|
+
commands: true
|
|
93
|
+
}
|
|
83
94
|
},
|
|
84
95
|
opencode: {
|
|
85
96
|
id: 'opencode',
|
|
@@ -93,7 +104,43 @@ const PLATFORMS = {
|
|
|
93
104
|
skillsRef: '.opencode/skills',
|
|
94
105
|
commandPrefix: '/',
|
|
95
106
|
sourceDir: 'claude',
|
|
96
|
-
sourceSubdir: '
|
|
107
|
+
sourceSubdir: 'commands', // Dead config (manifest commands.core is []); kept for shape parity
|
|
108
|
+
backupTargets: ['.opencode', 'AGENTS.md', 'opencode.json', 'opencode.jsonc'],
|
|
109
|
+
capabilities: {
|
|
110
|
+
skills: true,
|
|
111
|
+
agents: true,
|
|
112
|
+
references: true,
|
|
113
|
+
scripts: true,
|
|
114
|
+
rules: true,
|
|
115
|
+
commands: false
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
codex: {
|
|
119
|
+
id: 'codex',
|
|
120
|
+
name: 'Codex CLI',
|
|
121
|
+
description: 'OpenAI Codex CLI',
|
|
122
|
+
folder: '.codex',
|
|
123
|
+
detectFiles: ['.codex'],
|
|
124
|
+
commandsDir: null,
|
|
125
|
+
skillsDir: '.agents/skills',
|
|
126
|
+
agentsDir: '.codex/agents',
|
|
127
|
+
skillsRef: '.agents/skills',
|
|
128
|
+
commandPrefix: '$',
|
|
129
|
+
sourceDir: 'claude',
|
|
130
|
+
sourceSubdir: null,
|
|
131
|
+
backupTargets: ['.codex', '.agents', 'AGENTS.md'],
|
|
132
|
+
ownership: {
|
|
133
|
+
recordRoot: '.',
|
|
134
|
+
allowedRoots: ['.codex', '.agents']
|
|
135
|
+
},
|
|
136
|
+
capabilities: {
|
|
137
|
+
skills: true,
|
|
138
|
+
agents: true,
|
|
139
|
+
references: true,
|
|
140
|
+
scripts: true,
|
|
141
|
+
rules: true,
|
|
142
|
+
commands: false
|
|
143
|
+
}
|
|
97
144
|
}
|
|
98
145
|
// Add new platforms here:
|
|
99
146
|
// cursor: {
|
|
@@ -136,8 +183,8 @@ function getPlatformKeys() {
|
|
|
136
183
|
return Object.keys(PLATFORMS);
|
|
137
184
|
}
|
|
138
185
|
|
|
139
|
-
function
|
|
140
|
-
return platformKey
|
|
186
|
+
function hasPlatformCapability(platformKey, capability) {
|
|
187
|
+
return Boolean(PLATFORMS[platformKey]?.capabilities?.[capability]);
|
|
141
188
|
}
|
|
142
189
|
|
|
143
190
|
function getRuntimeSupportTargetDir(platformKey, subdir) {
|
|
@@ -164,6 +211,11 @@ function getCopyOptions(platformKey, baseOptions = {}) {
|
|
|
164
211
|
if (platformKey === 'opencode') {
|
|
165
212
|
return getOpenCodeCopyOptions(baseOptions);
|
|
166
213
|
}
|
|
214
|
+
if (platformKey === 'codex') {
|
|
215
|
+
// Lazy require avoids coupling the shared registry to a runtime adapter
|
|
216
|
+
// during module initialization.
|
|
217
|
+
return require('./codex-install').getCodexCopyOptions(baseOptions);
|
|
218
|
+
}
|
|
167
219
|
return baseOptions;
|
|
168
220
|
}
|
|
169
221
|
|
|
@@ -186,7 +238,8 @@ function parseInstallerArgs(argv) {
|
|
|
186
238
|
dryRun: false,
|
|
187
239
|
yes: false,
|
|
188
240
|
withSkillsDeps: false,
|
|
189
|
-
withRtk: false
|
|
241
|
+
withRtk: false,
|
|
242
|
+
platforms: []
|
|
190
243
|
};
|
|
191
244
|
|
|
192
245
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -202,6 +255,10 @@ function parseInstallerArgs(argv) {
|
|
|
202
255
|
args.withSkillsDeps = true;
|
|
203
256
|
} else if (arg === '--with-rtk') {
|
|
204
257
|
args.withRtk = true;
|
|
258
|
+
} else if (arg === '--platform') {
|
|
259
|
+
args.platforms.push(...String(argv[++i] || '').split(',').filter(Boolean));
|
|
260
|
+
} else if (arg.startsWith('--platform=')) {
|
|
261
|
+
args.platforms.push(...arg.slice('--platform='.length).split(',').filter(Boolean));
|
|
205
262
|
} else if (arg === '--lang') {
|
|
206
263
|
args.lang = argv[++i];
|
|
207
264
|
} else if (arg.startsWith('--lang=')) {
|
|
@@ -288,7 +345,7 @@ module.exports = {
|
|
|
288
345
|
detectPlatforms,
|
|
289
346
|
formatPlatformList,
|
|
290
347
|
getPlatformKeys,
|
|
291
|
-
|
|
348
|
+
hasPlatformCapability,
|
|
292
349
|
getRuntimeSupportTargetDir,
|
|
293
350
|
warnLegacyClaudeFolder,
|
|
294
351
|
getCopyOptions,
|
package/bin/lib/copy-utils.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const TEXT_REWRITE_EXTENSIONS = new Set([
|
|
7
|
-
'.md', '.mdx', '.txt', '.json', '.cjs', '.js', '.mjs', '.ts', '.sh', '.yml', '.yaml'
|
|
7
|
+
'.md', '.mdx', '.txt', '.json', '.cjs', '.js', '.mjs', '.ts', '.py', '.sh', '.toml', '.yml', '.yaml'
|
|
8
8
|
]);
|
|
9
9
|
|
|
10
10
|
function isTextAsset(filePath) {
|
package/bin/lib/i18n.js
CHANGED
|
@@ -38,7 +38,7 @@ const MESSAGES = {
|
|
|
38
38
|
modeInstall: 'Mode: install / update • your customisations are preserved',
|
|
39
39
|
|
|
40
40
|
// Intro
|
|
41
|
-
introDesc: 'AI-native development workflow
|
|
41
|
+
introDesc: 'AI-native development workflow',
|
|
42
42
|
|
|
43
43
|
// Platform install spinner
|
|
44
44
|
installingPlatform: 'Installing {name}',
|
|
@@ -108,6 +108,7 @@ const MESSAGES = {
|
|
|
108
108
|
nextStepsTitle: 'Next steps',
|
|
109
109
|
nsClaude: 'Start coding: /hapo:specs <feature>',
|
|
110
110
|
nsOpencode: 'Start coding: ask the agent to begin a new feature or brainstorm',
|
|
111
|
+
nsCodex: 'Start with `$hapo-specs <feature>`; review and trust project hooks with `/hooks`',
|
|
111
112
|
nsKeys: 'Some skills need API keys: {skills}\n Copy <skill>/.env.example → .env and fill in the values',
|
|
112
113
|
nsForce: 'To force-refresh managed files: npx @haposoft/cafekit --force-overwrite',
|
|
113
114
|
dryRunOnly: 'Dry-run complete — no files were changed.\n Re-run without --dry-run to apply.',
|
|
@@ -115,11 +116,13 @@ const MESSAGES = {
|
|
|
115
116
|
|
|
116
117
|
// Version check
|
|
117
118
|
versionUpToDate: 'Already up to date ({v}). Use --force-overwrite to reinstall.',
|
|
119
|
+
versionRefreshing: 'Refreshing CafeKit {v}; your modified files will be preserved.',
|
|
118
120
|
versionDowngrade: 'Downgrading {from} → {to}. Features may be removed. Continue?',
|
|
119
121
|
versionSamePrompt: 'CafeKit {v} is already installed. What would you like to do?',
|
|
120
122
|
versionForceReinstall: 'Reinstalling CafeKit {v} (--force-overwrite)...',
|
|
121
123
|
versionUpgradePrompt: 'CafeKit {from} → {to}: Update available!',
|
|
122
124
|
updateOption: 'Update to {v}',
|
|
125
|
+
refreshOption: 'Refresh managed files (preserve your edits)',
|
|
123
126
|
reinstallOption: 'Reinstall (overwrite managed files)',
|
|
124
127
|
reinstallCurrentOption: 'Reinstall {v}',
|
|
125
128
|
skipOption: 'Skip (exit)',
|
|
@@ -158,7 +161,7 @@ const MESSAGES = {
|
|
|
158
161
|
modeInstall: 'モード: インストール / 更新 • カスタマイズは保持されます',
|
|
159
162
|
|
|
160
163
|
// Intro
|
|
161
|
-
introDesc: '
|
|
164
|
+
introDesc: 'AI ネイティブ開発ワークフロー',
|
|
162
165
|
|
|
163
166
|
installingPlatform: '{name} をインストール中',
|
|
164
167
|
platformInstalled: '{name} — {files} ファイル、{skills} スキル',
|
|
@@ -223,17 +226,20 @@ const MESSAGES = {
|
|
|
223
226
|
nextStepsTitle: '次のステップ',
|
|
224
227
|
nsClaude: 'コーディング開始: /hapo:specs <機能名>',
|
|
225
228
|
nsOpencode: 'コーディング開始: エージェントに新機能の開始またはブレストを依頼',
|
|
229
|
+
nsCodex: '`$hapo-specs <機能名>` で開始し、`/hooks` でプロジェクトフックを確認・信頼',
|
|
226
230
|
nsKeys: '一部のスキルに API キーが必要です: {skills}\n <skill>/.env.example を .env にコピーし、値を入力してください',
|
|
227
231
|
nsForce: '管理ファイルを強制更新: npx @haposoft/cafekit --force-overwrite',
|
|
228
232
|
dryRunOnly: 'ドライラン完了 — ファイルへの変更はありませんでした。\n 変更を適用するには --dry-run なしで再実行してください。',
|
|
229
233
|
outroDone: '完了! ドキュメント: https://github.com/haposoft/cafekit',
|
|
230
234
|
|
|
231
235
|
versionUpToDate: '最新バージョンです({v})。再インストールするには --force-overwrite を使用してください。',
|
|
236
|
+
versionRefreshing: 'CafeKit {v} を更新します。編集済みファイルは保持されます。',
|
|
232
237
|
versionDowngrade: '{from} → {to} へのダウングレードです。機能が削除される可能性があります。続けますか?',
|
|
233
238
|
versionSamePrompt: 'CafeKit {v} は既にインストールされています。何をしますか?',
|
|
234
239
|
versionForceReinstall: 'CafeKit {v} を再インストール中 (--force-overwrite)...',
|
|
235
240
|
versionUpgradePrompt: 'CafeKit {from} → {to}: アップデートが利用可能です!',
|
|
236
241
|
updateOption: '{v} にアップデート',
|
|
242
|
+
refreshOption: '管理対象ファイルを更新(編集内容を保持)',
|
|
237
243
|
reinstallOption: '再インストール(管理対象ファイルを上書き)',
|
|
238
244
|
reinstallCurrentOption: '{v} を再インストール',
|
|
239
245
|
skipOption: 'スキップ(終了)',
|
|
@@ -272,7 +278,7 @@ const MESSAGES = {
|
|
|
272
278
|
modeInstall: 'Chế độ: cài / cập nhật • nội dung bạn tuỳ chỉnh được giữ nguyên',
|
|
273
279
|
|
|
274
280
|
// Intro
|
|
275
|
-
introDesc: 'Quy trình phát triển AI-native
|
|
281
|
+
introDesc: 'Quy trình phát triển AI-native',
|
|
276
282
|
|
|
277
283
|
installingPlatform: 'Đang cài {name}',
|
|
278
284
|
platformInstalled: '{name} — {files} tệp, {skills} skill',
|
|
@@ -337,17 +343,20 @@ const MESSAGES = {
|
|
|
337
343
|
nextStepsTitle: 'Bước tiếp theo',
|
|
338
344
|
nsClaude: 'Bắt đầu code: /hapo:specs <mô-tả-tính-năng>',
|
|
339
345
|
nsOpencode: 'Bắt đầu code: yêu cầu agent khởi động tính năng mới hoặc brainstorm',
|
|
346
|
+
nsCodex: 'Bắt đầu bằng `$hapo-specs <tính-năng>`; kiểm tra và trust project hooks bằng `/hooks`',
|
|
340
347
|
nsKeys: 'Một số skill cần API key: {skills}\n Sao chép <skill>/.env.example → .env rồi điền giá trị',
|
|
341
348
|
nsForce: 'Làm mới file được quản lý: npx @haposoft/cafekit --force-overwrite',
|
|
342
349
|
dryRunOnly: 'Dry-run hoàn tất — không có tệp nào thay đổi.\n Chạy lại không có --dry-run để áp dụng.',
|
|
343
350
|
outroDone: 'Xong! Tài liệu: https://github.com/haposoft/cafekit',
|
|
344
351
|
|
|
345
352
|
versionUpToDate: 'Đã là phiên bản mới nhất ({v}). Dùng --force-overwrite để cài lại.',
|
|
353
|
+
versionRefreshing: 'Đang làm mới CafeKit {v}; các tệp bạn đã sửa sẽ được giữ nguyên.',
|
|
346
354
|
versionDowngrade: 'Đang hạ cấp {from} → {to}. Một số tính năng có thể bị xoá. Tiếp tục?',
|
|
347
355
|
versionSamePrompt: 'CafeKit {v} đã được cài đặt. Bạn muốn làm gì?',
|
|
348
356
|
versionForceReinstall: 'Đang cài lại CafeKit {v} (--force-overwrite)...',
|
|
349
357
|
versionUpgradePrompt: 'CafeKit {from} → {to}: Có bản cập nhật mới!',
|
|
350
358
|
updateOption: 'Cập nhật lên {v}',
|
|
359
|
+
refreshOption: 'Làm mới file được quản lý (giữ phần bạn đã sửa)',
|
|
351
360
|
reinstallOption: 'Cài lại (ghi đè file được quản lý)',
|
|
352
361
|
reinstallCurrentOption: 'Cài lại {v}',
|
|
353
362
|
skipOption: 'Bỏ qua (thoát)',
|
|
@@ -22,6 +22,7 @@ const fs = require('fs');
|
|
|
22
22
|
const path = require('path');
|
|
23
23
|
const { isTextAsset } = require('./copy-utils');
|
|
24
24
|
const manifest = require('./manifest');
|
|
25
|
+
const { assertNoSymlinkPath } = require('./path-safety');
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Resolve the payload bytes for a source file, applying a text transform when
|
|
@@ -47,6 +48,7 @@ function writeManagedFile(opts) {
|
|
|
47
48
|
if (!fs.existsSync(src)) {
|
|
48
49
|
return { action: 'missing', state: 'absent' };
|
|
49
50
|
}
|
|
51
|
+
assertNoSymlinkPath(dest);
|
|
50
52
|
|
|
51
53
|
const { data, utf8 } = readPayload(src, transform);
|
|
52
54
|
const payloadHash = manifest.sha256(data);
|
|
@@ -56,7 +58,9 @@ function writeManagedFile(opts) {
|
|
|
56
58
|
// A file already written earlier THIS run (e.g. a spec template copied as part
|
|
57
59
|
// of the specs/ tree, then revisited by the template-sync loop) is ours — treat
|
|
58
60
|
// it as pristine against what we just wrote, not as a user-created file.
|
|
59
|
-
const relForRun =
|
|
61
|
+
const relForRun = tracker?.keyFor
|
|
62
|
+
? tracker.keyFor(dest)
|
|
63
|
+
: path.relative(platformFolder, dest).replace(/\\/g, '/');
|
|
60
64
|
const writtenThisRun = tracker ? tracker.recorded(relForRun) : null;
|
|
61
65
|
|
|
62
66
|
let cls;
|
|
@@ -68,7 +72,13 @@ function writeManagedFile(opts) {
|
|
|
68
72
|
changed: writtenThisRun !== payloadHash
|
|
69
73
|
};
|
|
70
74
|
} else {
|
|
71
|
-
cls = manifest.classify(
|
|
75
|
+
cls = manifest.classify(
|
|
76
|
+
dest,
|
|
77
|
+
platformFolder,
|
|
78
|
+
ownership,
|
|
79
|
+
payloadHash,
|
|
80
|
+
tracker?.recordRoot || platformFolder
|
|
81
|
+
);
|
|
72
82
|
}
|
|
73
83
|
const forced = Boolean(ctx.options.forceOverwrite);
|
|
74
84
|
|
|
@@ -102,6 +112,11 @@ function writeManagedFile(opts) {
|
|
|
102
112
|
} else {
|
|
103
113
|
fs.writeFileSync(dest, data);
|
|
104
114
|
}
|
|
115
|
+
if (process.platform !== 'win32') {
|
|
116
|
+
const sourceMode = fs.statSync(src).mode;
|
|
117
|
+
const destinationMode = fs.statSync(dest).mode;
|
|
118
|
+
fs.chmodSync(dest, (destinationMode & ~0o111) | (sourceMode & 0o111));
|
|
119
|
+
}
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
// Record created/updated/unchanged files so the manifest reflects reality.
|
package/bin/lib/manifest.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const crypto = require('crypto');
|
|
20
|
+
const { assertNoSymlinkPath, isInside } = require('./path-safety');
|
|
20
21
|
|
|
21
22
|
const MANIFEST_FILE = 'cafekit-manifest.json';
|
|
22
23
|
const SCHEMA_VERSION = 1;
|
|
@@ -75,10 +76,26 @@ function read(platformFolder) {
|
|
|
75
76
|
* writes them back as the new baseline. relPath keys are normalized to forward
|
|
76
77
|
* slashes and are relative to the platform folder.
|
|
77
78
|
*/
|
|
78
|
-
function createTracker(platformFolder, version) {
|
|
79
|
+
function createTracker(platformFolder, version, options = {}) {
|
|
79
80
|
const files = {};
|
|
81
|
+
const recordRoot = path.resolve(options.recordRoot || platformFolder);
|
|
82
|
+
const allowedRoots = (options.allowedRoots || [options.recordRoot || platformFolder])
|
|
83
|
+
.map((root) => path.resolve(root));
|
|
84
|
+
|
|
85
|
+
function keyFor(filePath) {
|
|
86
|
+
const target = path.resolve(filePath);
|
|
87
|
+
if (!allowedRoots.some((root) => isInside(root, target))) {
|
|
88
|
+
throw new Error(`Managed file is outside allowed roots: ${filePath}`);
|
|
89
|
+
}
|
|
90
|
+
assertNoSymlinkPath(target);
|
|
91
|
+
return path.relative(recordRoot, target).replace(/\\/g, '/');
|
|
92
|
+
}
|
|
80
93
|
|
|
81
94
|
return {
|
|
95
|
+
recordRoot,
|
|
96
|
+
allowedRoots,
|
|
97
|
+
keyFor,
|
|
98
|
+
|
|
82
99
|
/**
|
|
83
100
|
* Record a managed file by its on-disk path. Hashes the file as written.
|
|
84
101
|
* Skips silently if the file is missing (e.g. dry-run).
|
|
@@ -86,7 +103,7 @@ function createTracker(platformFolder, version) {
|
|
|
86
103
|
record(absPath) {
|
|
87
104
|
const hash = hashFile(absPath);
|
|
88
105
|
if (hash === null) return;
|
|
89
|
-
const rel =
|
|
106
|
+
const rel = keyFor(absPath);
|
|
90
107
|
files[rel] = { sha256: hash, version };
|
|
91
108
|
},
|
|
92
109
|
|
|
@@ -159,8 +176,8 @@ function createTracker(platformFolder, version) {
|
|
|
159
176
|
* state: 'absent' | 'pristine' | 'user-modified' | 'user-created'
|
|
160
177
|
* changed: whether the payload differs from what's on disk (false → skip copy)
|
|
161
178
|
*/
|
|
162
|
-
function classify(absPath, platformFolder, manifest, payloadHash) {
|
|
163
|
-
const rel = path.relative(
|
|
179
|
+
function classify(absPath, platformFolder, manifest, payloadHash, recordRoot = platformFolder) {
|
|
180
|
+
const rel = path.relative(recordRoot, absPath).replace(/\\/g, '/');
|
|
164
181
|
const recorded = manifest.files[rel];
|
|
165
182
|
const recordedHash = recorded ? recorded.sha256 : null;
|
|
166
183
|
const diskHash = hashFile(absPath);
|
|
@@ -419,6 +419,20 @@ function upsertCafeKitAgentsBlock(existingContent, blockContent) {
|
|
|
419
419
|
return existingContent.replace(blockPattern, managedBlock);
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
// Pre-managed-block releases wrote the shipped template as the whole file.
|
|
423
|
+
// Wrap only an exact shipped prefix and preserve any user/Codex suffix.
|
|
424
|
+
if (existingContent.startsWith(blockContent)) {
|
|
425
|
+
return `${managedBlock}${existingContent.slice(blockContent.length)}`;
|
|
426
|
+
}
|
|
427
|
+
const trimmedLegacy = blockContent.trimEnd();
|
|
428
|
+
if (
|
|
429
|
+
existingContent.startsWith(trimmedLegacy)
|
|
430
|
+
&& ['', '\n', '\r'].includes(existingContent.charAt(trimmedLegacy.length))
|
|
431
|
+
) {
|
|
432
|
+
return `${managedBlock}${existingContent.slice(trimmedLegacy.length)}`;
|
|
433
|
+
}
|
|
434
|
+
if (!existingContent) return `${managedBlock}\n`;
|
|
435
|
+
|
|
422
436
|
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
423
437
|
return `${existingContent}${separator}${managedBlock}\n`;
|
|
424
438
|
}
|
|
@@ -442,35 +456,23 @@ function getOpenCodeAgentsTemplateContent() {
|
|
|
442
456
|
function copyOpenCodeAgentsMdFile(platformKey, results, options = {}) {
|
|
443
457
|
if (platformKey !== 'opencode') return;
|
|
444
458
|
|
|
445
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
446
459
|
const dest = 'AGENTS.md';
|
|
447
460
|
const content = getOpenCodeAgentsTemplateContent();
|
|
448
461
|
|
|
449
462
|
if (content) {
|
|
450
463
|
const destinationExists = fs.existsSync(dest);
|
|
464
|
+
const existingContent = destinationExists ? fs.readFileSync(dest, 'utf8') : '';
|
|
465
|
+
const nextContent = upsertCafeKitAgentsBlock(existingContent, content);
|
|
451
466
|
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (nextContent === existingContent) {
|
|
457
|
-
console.log(` → Skipped: AGENTS.md (already up to date)`);
|
|
458
|
-
results.skipped++;
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
|
|
467
|
+
if (nextContent === existingContent) {
|
|
468
|
+
console.log(` → Skipped: AGENTS.md (already up to date)`);
|
|
469
|
+
results.skipped++;
|
|
470
|
+
} else if (destinationExists) {
|
|
462
471
|
fs.writeFileSync(dest, nextContent, 'utf8');
|
|
463
472
|
console.log(` ↻ AGENTS.md merged`);
|
|
464
473
|
results.updated++;
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
fs.writeFileSync(dest, content, 'utf8');
|
|
469
|
-
|
|
470
|
-
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
471
|
-
console.log(` ↻ AGENTS.md overwritten`);
|
|
472
|
-
results.updated++;
|
|
473
474
|
} else {
|
|
475
|
+
fs.writeFileSync(dest, nextContent, 'utf8');
|
|
474
476
|
console.log(` ✓ AGENTS.md installed`);
|
|
475
477
|
results.copied++;
|
|
476
478
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function isInside(base, target) {
|
|
7
|
+
const relative = path.relative(base, target);
|
|
8
|
+
return relative === ''
|
|
9
|
+
|| (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reject installer targets that traverse symlinks below the project root.
|
|
14
|
+
* Managed payloads and rollback snapshots must never follow a project-owned
|
|
15
|
+
* link into another directory.
|
|
16
|
+
*/
|
|
17
|
+
function assertNoSymlinkPath(filePath, basePath = process.cwd()) {
|
|
18
|
+
const base = path.resolve(basePath);
|
|
19
|
+
const target = path.resolve(filePath);
|
|
20
|
+
if (!isInside(base, target)) {
|
|
21
|
+
throw new Error(`Managed path is outside the project root: ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const relative = path.relative(base, target);
|
|
25
|
+
let cursor = base;
|
|
26
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
27
|
+
cursor = path.join(cursor, segment);
|
|
28
|
+
try {
|
|
29
|
+
if (fs.lstatSync(cursor).isSymbolicLink()) {
|
|
30
|
+
throw new Error(`Refusing to follow symlinked managed path: ${filePath}`);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') break;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return target;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { assertNoSymlinkPath, isInside };
|