@haposoft/cafekit 0.16.0 → 0.16.2
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 +5 -3
- package/bin/__tests__/codex-hooks.test.js +125 -0
- package/bin/__tests__/codex-native.test.js +95 -0
- package/bin/__tests__/develop-contract.test.js +43 -1
- package/bin/__tests__/grok-envelope.test.js +271 -0
- package/bin/__tests__/grok-platform.test.js +133 -0
- package/bin/__tests__/hook-payload.test.js +194 -0
- package/bin/__tests__/omp-bridge.test.js +168 -0
- package/bin/__tests__/omp-hooks.test.js +178 -0
- package/bin/__tests__/omp-runtime.test.js +150 -0
- package/bin/__tests__/orca-session.test.js +225 -0
- package/bin/__tests__/orca-skill.test.js +210 -0
- package/bin/__tests__/package-inventory.test.js +1 -0
- package/bin/__tests__/runtime-dir.test.js +158 -0
- package/bin/__tests__/runtime-schema.test.js +5 -3
- package/bin/__tests__/specs-v2-execution-closeout.test.js +28 -1
- package/bin/install.js +5 -5
- package/bin/lib/context.js +50 -3
- package/bin/lib/i18n.js +3 -30
- package/bin/phases/codex-hooks.js +90 -17
- package/bin/phases/copy-payload.js +7 -0
- package/bin/phases/omp-runtime.js +140 -0
- package/bin/phases/root-config.js +3 -0
- package/bin/phases/select-platform.js +22 -1
- package/bin/phases/summary.js +0 -4
- package/package.json +1 -1
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +2 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +338 -0
- package/src/claude/hooks/__tests__/state.test.js +2 -0
- package/src/claude/hooks/agent.cjs +12 -7
- package/src/claude/hooks/completion-authority.cjs +10 -4
- package/src/claude/hooks/docs-sync.cjs +2 -1
- package/src/claude/hooks/inspect-block.cjs +27 -7
- package/src/claude/hooks/lib/config.cjs +5 -2
- package/src/claude/hooks/lib/counter.cjs +7 -5
- package/src/claude/hooks/lib/hook-payload.cjs +160 -0
- package/src/claude/hooks/lib/hook-state-dir.cjs +2 -2
- package/src/claude/hooks/lib/runtime-dir.cjs +49 -0
- package/src/claude/hooks/precompact.cjs +2 -1
- package/src/claude/hooks/privacy-block.cjs +4 -2
- package/src/claude/hooks/rules.cjs +4 -2
- package/src/claude/hooks/secret-output-guardrail.cjs +2 -1
- package/src/claude/hooks/semantic-review-authority.cjs +6 -4
- package/src/claude/hooks/session.cjs +16 -8
- package/src/claude/hooks/spec-gate.cjs +18 -8
- package/src/claude/hooks/spec-state.cjs +9 -5
- package/src/claude/hooks/state.cjs +6 -4
- package/src/claude/hooks/task-scaffold-guard.cjs +29 -8
- package/src/claude/hooks/usage.cjs +5 -4
- package/src/claude/migration-manifest.json +3 -0
- package/src/claude/rules/skill-domain-routing.md +1 -0
- package/src/claude/rules/state-sync.md +7 -1
- package/src/claude/rules/workflow.md +2 -0
- package/src/claude/scripts/provenance.cjs +11 -4
- package/src/claude/scripts/spec-receipt.cjs +65 -4
- package/src/claude/skills/orca/SKILL.md +51 -0
- package/src/claude/skills/specs/SKILL.md +10 -11
- package/src/codex/hooks/session.cjs +6 -1
- package/src/codex/hooks/spec-state.cjs +1 -1
- package/src/codex/hooks.json +18 -18
- package/src/codex/rules/state-sync.md +7 -1
- package/src/omp/extensions/cafekit-bridge.mjs +233 -0
- package/src/omp/hooks/privacy-block.cjs +250 -0
- package/src/omp/runtime.json +33 -0
- package/bin/lib/rtk-run.js +0 -28
- package/bin/phases/setup-rtk.js +0 -159
package/bin/lib/i18n.js
CHANGED
|
@@ -62,16 +62,6 @@ const MESSAGES = {
|
|
|
62
62
|
documentSkillsMetadataRecovery: 'Could not read {path}; preserving document skills to avoid destructive cleanup.',
|
|
63
63
|
skillsSkipped: 'Skill dependencies skipped.\n Run later: npx @haposoft/cafekit --with-skills-deps',
|
|
64
64
|
|
|
65
|
-
// rtk token-saver setup
|
|
66
|
-
rtkConfirm: 'Install the rtk token-saver? (compacts git/grep/ls/build/test output for Claude Code Bash commands)',
|
|
67
|
-
rtkInstalling: 'Installing rtk via {method}',
|
|
68
|
-
rtkInstalled: 'rtk installed',
|
|
69
|
-
rtkInstallFailed: 'Could not install rtk — skipping (non-fatal). Install manually later: https://github.com/rtk-ai/rtk',
|
|
70
|
-
rtkInitFailed: 'rtk init -g failed: {reason} — hook not registered (non-fatal)',
|
|
71
|
-
rtkNeedsJq: 'rtk hook needs jq to run. Install jq, then the hook activates automatically.',
|
|
72
|
-
rtkFailed: 'rtk setup error: {reason} — skipped (non-fatal)',
|
|
73
|
-
rtkSkipped: 'rtk token-saver skipped.\n Run later: npx @haposoft/cafekit --with-rtk',
|
|
74
|
-
rtkSummary: 'rtk token-saver: hook registered for Claude Code Bash commands',
|
|
75
65
|
venvCreating: 'Setting up Python environment',
|
|
76
66
|
venvReady: 'Python environment ready',
|
|
77
67
|
venvFailed: 'Could not create Python environment — check your Python 3 installation.',
|
|
@@ -130,6 +120,7 @@ const MESSAGES = {
|
|
|
130
120
|
|
|
131
121
|
// Platform/language auto-skip when already installed
|
|
132
122
|
installingFor: 'Installing for: {names}',
|
|
123
|
+
grokCompatNotice: 'Grok CLI reads this install through its Claude compatibility. Trust the project first: run `grok --trust` or `/hooks-trust`, or its project hooks are skipped.',
|
|
133
124
|
langKept: 'Language: {lang} (saved — skipping prompt)',
|
|
134
125
|
platformKept: 'Platform: {names} (saved — skipping prompt)',
|
|
135
126
|
|
|
@@ -186,16 +177,6 @@ const MESSAGES = {
|
|
|
186
177
|
documentSkillsMetadataRecovery: '{path} を読み取れないため、破壊的な削除を避けて document skills を保持します。',
|
|
187
178
|
skillsSkipped: 'スキルの依存関係をスキップしました。\n 後で実行: npx @haposoft/cafekit --with-skills-deps',
|
|
188
179
|
|
|
189
|
-
// rtk token-saver setup
|
|
190
|
-
rtkConfirm: 'rtk トークンセーバーをインストールしますか?(Claude Code の Bash コマンドの git/grep/ls/build/test 出力を圧縮します)',
|
|
191
|
-
rtkInstalling: '{method} で rtk をインストール中',
|
|
192
|
-
rtkInstalled: 'rtk をインストールしました',
|
|
193
|
-
rtkInstallFailed: 'rtk をインストールできませんでした — スキップします(致命的ではありません)。後で手動でインストール: https://github.com/rtk-ai/rtk',
|
|
194
|
-
rtkInitFailed: 'rtk init -g が失敗しました: {reason} — フックは未登録です(致命的ではありません)',
|
|
195
|
-
rtkNeedsJq: 'rtk フックの実行には jq が必要です。jq をインストールするとフックが自動的に有効になります。',
|
|
196
|
-
rtkFailed: 'rtk セットアップエラー: {reason} — スキップしました(致命的ではありません)',
|
|
197
|
-
rtkSkipped: 'rtk トークンセーバーをスキップしました。\n 後で実行: npx @haposoft/cafekit --with-rtk',
|
|
198
|
-
rtkSummary: 'rtk トークンセーバー: Claude Code の Bash コマンド用にフックを登録しました',
|
|
199
180
|
venvCreating: 'Python 環境を準備しています',
|
|
200
181
|
venvReady: 'Python 環境の準備ができました',
|
|
201
182
|
venvFailed: 'Python 環境を作成できませんでした — Python 3 のインストールを確認してください。',
|
|
@@ -251,6 +232,7 @@ const MESSAGES = {
|
|
|
251
232
|
|
|
252
233
|
// Platform/language auto-skip when already installed
|
|
253
234
|
installingFor: 'インストール先: {names}',
|
|
235
|
+
grokCompatNotice: 'Grok CLI はこのインストールを Claude 互換として読み込みます。先にプロジェクトを信頼してください: `grok --trust` または `/hooks-trust`。信頼しないとプロジェクトフックはスキップされます。',
|
|
254
236
|
langKept: '言語: {lang} (保存済み — スキップ)',
|
|
255
237
|
platformKept: 'プラットフォーム: {names} (保存済み — スキップ)',
|
|
256
238
|
|
|
@@ -307,16 +289,6 @@ const MESSAGES = {
|
|
|
307
289
|
documentSkillsMetadataRecovery: 'Không đọc được {path}; giữ nguyên skill tài liệu để tránh dọn nhầm dữ liệu.',
|
|
308
290
|
skillsSkipped: 'Đã bỏ qua dependencies skill.\n Cài sau bằng lệnh: npx @haposoft/cafekit --with-skills-deps',
|
|
309
291
|
|
|
310
|
-
// rtk token-saver setup
|
|
311
|
-
rtkConfirm: 'Cài rtk token-saver? (rút gọn output git/grep/ls/build/test cho các lệnh Bash của Claude Code)',
|
|
312
|
-
rtkInstalling: 'Đang cài rtk qua {method}',
|
|
313
|
-
rtkInstalled: 'Đã cài rtk',
|
|
314
|
-
rtkInstallFailed: 'Không thể cài rtk — bỏ qua (không nghiêm trọng). Cài thủ công sau: https://github.com/rtk-ai/rtk',
|
|
315
|
-
rtkInitFailed: 'rtk init -g thất bại: {reason} — hook chưa được đăng ký (không nghiêm trọng)',
|
|
316
|
-
rtkNeedsJq: 'Hook của rtk cần jq để chạy. Cài jq thì hook sẽ tự kích hoạt.',
|
|
317
|
-
rtkFailed: 'Lỗi cài rtk: {reason} — đã bỏ qua (không nghiêm trọng)',
|
|
318
|
-
rtkSkipped: 'Đã bỏ qua rtk token-saver.\n Cài sau bằng lệnh: npx @haposoft/cafekit --with-rtk',
|
|
319
|
-
rtkSummary: 'rtk token-saver: đã đăng ký hook cho các lệnh Bash của Claude Code',
|
|
320
292
|
venvCreating: 'Đang thiết lập môi trường Python',
|
|
321
293
|
venvReady: 'Môi trường Python đã sẵn sàng',
|
|
322
294
|
venvFailed: 'Không thể tạo môi trường Python — hãy kiểm tra cài đặt Python 3.',
|
|
@@ -372,6 +344,7 @@ const MESSAGES = {
|
|
|
372
344
|
|
|
373
345
|
// Platform/language auto-skip when already installed
|
|
374
346
|
installingFor: 'Cài cho: {names}',
|
|
347
|
+
grokCompatNotice: 'Grok CLI đọc bản cài này qua lớp tương thích Claude. Hãy trust project trước: chạy `grok --trust` hoặc `/hooks-trust`, nếu không project hooks sẽ bị bỏ qua.',
|
|
375
348
|
langKept: 'Ngôn ngữ: {lang} (đã lưu — bỏ qua chọn lại)',
|
|
376
349
|
platformKept: 'Nền tảng: {names} (đã lưu — bỏ qua chọn lại)',
|
|
377
350
|
|
|
@@ -16,7 +16,9 @@ const path = require('path');
|
|
|
16
16
|
const { PLATFORMS } = require('../lib/context');
|
|
17
17
|
|
|
18
18
|
const CODEX_SRC = path.join(__dirname, '../../src/codex');
|
|
19
|
-
const
|
|
19
|
+
const HOOK_COMMAND_TEMPLATE = /^node "\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
|
|
20
|
+
// What an installer before 0.16.2 wrote for POSIX. Kept only to recognize and repair it.
|
|
21
|
+
const LEGACY_GIT_COMMAND = /^node "\$\(git rev-parse --show-toplevel\)\/\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
|
|
20
22
|
|
|
21
23
|
/** The hook script a command runs, which is stable across installs and platforms. */
|
|
22
24
|
function hookScript(command) {
|
|
@@ -25,21 +27,81 @@ function hookScript(command) {
|
|
|
25
27
|
return match ? match[1] : null;
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** Single quotes suppress every shell expansion, so only a literal quote needs escaping. */
|
|
31
|
+
function shellQuote(value) {
|
|
32
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hookPathFor(script, projectRoot) {
|
|
36
|
+
return path.join(fs.realpathSync(projectRoot), '.codex', 'hooks', script);
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
40
|
+
* The POSIX launcher carries the installed path literally.
|
|
41
|
+
*
|
|
42
|
+
* It used to resolve its own root with `$(git rev-parse --show-toplevel)`, which returns
|
|
43
|
+
* nothing outside a repository and the *outer* root for a project nested inside one. Both
|
|
44
|
+
* cases produced a path that does not exist, so Codex started every hook, every hook died,
|
|
45
|
+
* and the gates were silently absent. The path is known at install time, so it is written
|
|
46
|
+
* directly. The literal `/hooks/<script>.cjs` segment must survive, because hookScript()
|
|
47
|
+
* reads it as the identity that lets a reinstall recognize CafeKit's own entries.
|
|
32
48
|
*/
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
function posixHookCommand(script, projectRoot) {
|
|
50
|
+
return `node ${shellQuote(hookPathFor(script, projectRoot))}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Absolute Windows paths cannot be quoted safely inside the Codex command string, so that
|
|
55
|
+
* side travels base64url and is decoded by the launcher.
|
|
56
|
+
*/
|
|
57
|
+
function windowsHookCommand(script, projectRoot) {
|
|
58
|
+
const encoded = Buffer.from(hookPathFor(script, projectRoot), 'utf8').toString('base64url');
|
|
59
|
+
return 'node -e "process.argv[1]=Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\');require(\'module\').runMain()" ' + encoded;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Rewrite a managed template handler into absolute launchers for both platforms. */
|
|
63
|
+
function materializeHookCommands(handler, projectRoot) {
|
|
64
|
+
const next = { ...handler };
|
|
65
|
+
for (const [field, build] of [['command', posixHookCommand], ['commandWindows', windowsHookCommand]]) {
|
|
66
|
+
if (typeof handler[field] !== 'string') continue;
|
|
67
|
+
const match = handler[field].match(HOOK_COMMAND_TEMPLATE);
|
|
68
|
+
if (!match) throw new Error(`Unsupported Codex hook ${field}: ${handler[field]}`);
|
|
69
|
+
next[field] = build(match[1], projectRoot);
|
|
70
|
+
}
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Repair launchers an older installer wrote. A reinstall treats an already-registered
|
|
76
|
+
* script as the user's placement and leaves it alone, so without this pass every existing
|
|
77
|
+
* project would keep its broken git-derived command forever. Only the exact byte pattern
|
|
78
|
+
* CafeKit itself emitted is rewritten; anything else stays untouched.
|
|
79
|
+
*/
|
|
80
|
+
function repairLegacyCodexLaunchers(config, projectRoot) {
|
|
81
|
+
if (!config.hooks) return { config, repaired: 0 };
|
|
82
|
+
let repaired = 0;
|
|
83
|
+
const hooks = {};
|
|
84
|
+
for (const [eventName, groups] of Object.entries(config.hooks)) {
|
|
85
|
+
if (!Array.isArray(groups)) { hooks[eventName] = groups; continue; }
|
|
86
|
+
hooks[eventName] = groups.map((group) => {
|
|
87
|
+
if (!Array.isArray(group?.hooks)) return group;
|
|
88
|
+
return {
|
|
89
|
+
...group,
|
|
90
|
+
hooks: group.hooks.map((handler) => {
|
|
91
|
+
const legacy = typeof handler?.command === 'string' && handler.command.match(LEGACY_GIT_COMMAND);
|
|
92
|
+
const template = typeof handler?.commandWindows === 'string'
|
|
93
|
+
&& handler.commandWindows.match(HOOK_COMMAND_TEMPLATE);
|
|
94
|
+
if (!legacy && !template) return handler;
|
|
95
|
+
const next = { ...handler };
|
|
96
|
+
if (legacy) next.command = posixHookCommand(legacy[1], projectRoot);
|
|
97
|
+
if (template) next.commandWindows = windowsHookCommand(template[1], projectRoot);
|
|
98
|
+
repaired += 1;
|
|
99
|
+
return next;
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return { config: repaired > 0 ? { ...config, hooks } : config, repaired };
|
|
43
105
|
}
|
|
44
106
|
|
|
45
107
|
/** Drop CafeKit hooks the manifest has retired, leaving foreign entries untouched. */
|
|
@@ -96,12 +158,18 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
|
|
|
96
158
|
}
|
|
97
159
|
}
|
|
98
160
|
|
|
99
|
-
const { config:
|
|
161
|
+
const { config: pruned, removed } = pruneObsoleteCodexHooks({ ...existing }, ctx);
|
|
100
162
|
if (removed > 0) {
|
|
101
163
|
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: removed ${removed} obsolete hook(s)`);
|
|
102
164
|
ctx.results.updated++;
|
|
103
165
|
}
|
|
104
166
|
|
|
167
|
+
const { config: base, repaired } = repairLegacyCodexLaunchers(pruned, projectRoot);
|
|
168
|
+
if (repaired > 0) {
|
|
169
|
+
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Codex hooks: repaired ${repaired} launcher(s) that resolved their path through Git`);
|
|
170
|
+
ctx.results.updated++;
|
|
171
|
+
}
|
|
172
|
+
|
|
105
173
|
const merged = { ...base };
|
|
106
174
|
if (typeof managed.description === 'string' && typeof merged.description !== 'string') {
|
|
107
175
|
merged.description = managed.description;
|
|
@@ -124,7 +192,7 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
|
|
|
124
192
|
const script = hookScript(handler?.command);
|
|
125
193
|
return script && !registered.has(script);
|
|
126
194
|
})
|
|
127
|
-
.map((handler) =>
|
|
195
|
+
.map((handler) => materializeHookCommands(handler, projectRoot));
|
|
128
196
|
if (wanted.length === 0) continue;
|
|
129
197
|
|
|
130
198
|
const matcher = managedGroup.matcher || '';
|
|
@@ -153,4 +221,9 @@ function mergeCodexHooks(ctx, platformKey, projectRoot = process.cwd()) {
|
|
|
153
221
|
}
|
|
154
222
|
}
|
|
155
223
|
|
|
156
|
-
module.exports = {
|
|
224
|
+
module.exports = {
|
|
225
|
+
mergeCodexHooks,
|
|
226
|
+
pruneObsoleteCodexHooks,
|
|
227
|
+
repairLegacyCodexLaunchers,
|
|
228
|
+
hookScript,
|
|
229
|
+
};
|
|
@@ -189,7 +189,14 @@ function copyPlatformFiles(ctx, platformKey) {
|
|
|
189
189
|
} else {
|
|
190
190
|
report(ctx, 'missing', `agents: ${platform.agentsDir}`);
|
|
191
191
|
}
|
|
192
|
+
}
|
|
192
193
|
|
|
194
|
+
// ── Scripts ─────────────────────────────────────────────
|
|
195
|
+
// Gated on its own capability. This block used to sit inside the agents guard,
|
|
196
|
+
// so a platform with agents: false and scripts: true (omp) never received
|
|
197
|
+
// .omp/scripts/, and every Stop-time gate that requires ../scripts/*.cjs answered
|
|
198
|
+
// "Completion gate unavailable" on each session_stop.
|
|
199
|
+
if (hasPlatformCapability(platformKey, 'scripts')) {
|
|
193
200
|
const scriptsSourceDir = path.join(SRC, 'claude/scripts');
|
|
194
201
|
if (fs.existsSync(scriptsSourceDir)) {
|
|
195
202
|
const scriptsDest = getRuntimeSupportTargetDir(platformKey, 'scripts');
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase: Oh My Pi runtime.
|
|
5
|
+
*
|
|
6
|
+
* omp discovers `.claude/skills` and `.agents/skills` on its own, so CafeKit copies no
|
|
7
|
+
* skill payload here. What omp does not have is CafeKit's enforcement chain, so this
|
|
8
|
+
* phase provisions what carries it:
|
|
9
|
+
*
|
|
10
|
+
* .omp/hooks/ the gate scripts: the Claude set, with omp's overlay on top
|
|
11
|
+
* .omp/extensions/ the bridge omp auto-loads, which dispatches those scripts
|
|
12
|
+
* .omp/runtime.json the hooks' configuration, beside the schema that describes it
|
|
13
|
+
* .omp/rules/ the rules the hooks inject
|
|
14
|
+
*
|
|
15
|
+
* The hooks are portable: they derive their runtime directory from their own location,
|
|
16
|
+
* so the Claude bytes run unchanged under `.omp/`. Only the files that genuinely differ
|
|
17
|
+
* for omp live in `src/omp/hooks/` and are written over the Claude set afterwards, so a
|
|
18
|
+
* change to a shared hook reaches omp without anyone remembering to re-fork it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { PLATFORMS } = require('../lib/context');
|
|
24
|
+
const { writeManagedFile, copyManagedTree } = require('../lib/managed-writer');
|
|
25
|
+
const { report, treeAction } = require('./report');
|
|
26
|
+
const { copyRulesDirectory } = require('./claude-runtime');
|
|
27
|
+
|
|
28
|
+
const SRC = path.join(__dirname, '../../src');
|
|
29
|
+
const CLAUDE_SRC = path.join(SRC, 'claude');
|
|
30
|
+
const OMP_SRC = path.join(SRC, 'omp');
|
|
31
|
+
|
|
32
|
+
/** Files under `src/omp/hooks/` that replace their Claude counterpart, relative to the hooks dir. */
|
|
33
|
+
function overlayFiles(dir = path.join(OMP_SRC, 'hooks'), prefix = '') {
|
|
34
|
+
if (!fs.existsSync(dir)) return [];
|
|
35
|
+
const found = [];
|
|
36
|
+
for (const name of fs.readdirSync(dir)) {
|
|
37
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
38
|
+
if (fs.statSync(path.join(dir, name)).isDirectory()) found.push(...overlayFiles(path.join(dir, name), rel));
|
|
39
|
+
else found.push(rel);
|
|
40
|
+
}
|
|
41
|
+
return found.sort();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The Claude hook set is the list the migration manifest ships to `.claude/`, not a
|
|
46
|
+
* directory walk: the source tree also holds `__tests__/`, which must never install.
|
|
47
|
+
*/
|
|
48
|
+
function claudeHookFiles(ctx) {
|
|
49
|
+
const files = ctx.manifest?.runtime?.files || [];
|
|
50
|
+
return files.filter((rel) => rel.startsWith('hooks/')).map((rel) => rel.slice('hooks/'.length));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeSourceFile(ctx, platformKey, src, dest, label) {
|
|
54
|
+
const platform = PLATFORMS[platformKey];
|
|
55
|
+
const { action } = writeManagedFile({
|
|
56
|
+
src,
|
|
57
|
+
dest,
|
|
58
|
+
platformFolder: platform.folder,
|
|
59
|
+
ctx,
|
|
60
|
+
tracker: ctx.trackers[platformKey]
|
|
61
|
+
});
|
|
62
|
+
report(ctx, action, label);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function installHooks(ctx, platformKey) {
|
|
66
|
+
const platform = PLATFORMS[platformKey];
|
|
67
|
+
const overlay = new Set(overlayFiles());
|
|
68
|
+
// Claude set first, skipping anything the overlay is about to replace so a file is
|
|
69
|
+
// written once and the manifest records the bytes that actually land.
|
|
70
|
+
for (const rel of claudeHookFiles(ctx)) {
|
|
71
|
+
if (overlay.has(rel)) continue;
|
|
72
|
+
writeSourceFile(
|
|
73
|
+
ctx,
|
|
74
|
+
platformKey,
|
|
75
|
+
path.join(CLAUDE_SRC, 'hooks', rel),
|
|
76
|
+
path.join(platform.folder, 'hooks', rel),
|
|
77
|
+
`omp hooks: ${rel}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
for (const rel of overlay) {
|
|
81
|
+
writeSourceFile(
|
|
82
|
+
ctx,
|
|
83
|
+
platformKey,
|
|
84
|
+
path.join(OMP_SRC, 'hooks', rel),
|
|
85
|
+
path.join(platform.folder, 'hooks', rel),
|
|
86
|
+
`omp hooks (overlay): ${rel}`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function installRuntimeFiles(ctx, platformKey) {
|
|
92
|
+
const platform = PLATFORMS[platformKey];
|
|
93
|
+
writeSourceFile(
|
|
94
|
+
ctx,
|
|
95
|
+
platformKey,
|
|
96
|
+
path.join(OMP_SRC, 'runtime.json'),
|
|
97
|
+
path.join(platform.folder, 'runtime.json'),
|
|
98
|
+
'omp runtime: runtime.json'
|
|
99
|
+
);
|
|
100
|
+
// One canonical runtime schema lives in the Claude source tree; every installed
|
|
101
|
+
// runtime references it as ./runtime.schema.json beside its runtime.json.
|
|
102
|
+
writeSourceFile(
|
|
103
|
+
ctx,
|
|
104
|
+
platformKey,
|
|
105
|
+
path.join(CLAUDE_SRC, 'runtime.schema.json'),
|
|
106
|
+
path.join(platform.folder, 'runtime.schema.json'),
|
|
107
|
+
'omp runtime schema'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function installBridge(ctx, platformKey) {
|
|
112
|
+
const platform = PLATFORMS[platformKey];
|
|
113
|
+
const bridgeSource = path.join(OMP_SRC, 'extensions');
|
|
114
|
+
const bridgeDest = path.join(platform.folder, 'extensions');
|
|
115
|
+
if (fs.existsSync(bridgeSource)) {
|
|
116
|
+
const bridge = copyManagedTree({
|
|
117
|
+
src: bridgeSource,
|
|
118
|
+
dest: bridgeDest,
|
|
119
|
+
platformFolder: platform.folder,
|
|
120
|
+
ctx,
|
|
121
|
+
tracker: ctx.trackers[platformKey]
|
|
122
|
+
});
|
|
123
|
+
report(ctx, treeAction(bridge), 'omp bridge extension');
|
|
124
|
+
} else if (!ctx.dryRun) {
|
|
125
|
+
// A half-provisioned install is visibly empty rather than silently absent.
|
|
126
|
+
fs.mkdirSync(bridgeDest, { recursive: true });
|
|
127
|
+
ctx.ui.detail(' ✓ omp extensions directory created (bridge not present)');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function installOmpRuntime(ctx, platformKey) {
|
|
132
|
+
if (platformKey !== 'omp') return;
|
|
133
|
+
installHooks(ctx, platformKey);
|
|
134
|
+
installRuntimeFiles(ctx, platformKey);
|
|
135
|
+
installBridge(ctx, platformKey);
|
|
136
|
+
copyRulesDirectory(ctx, platformKey);
|
|
137
|
+
return ctx;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { installOmpRuntime, overlayFiles, claudeHookFiles };
|
|
@@ -110,6 +110,9 @@ function ensureGitignore(ctx) {
|
|
|
110
110
|
// Local runtime payload — reinstall with npx; keep out of git
|
|
111
111
|
'.claude/',
|
|
112
112
|
'.codex/',
|
|
113
|
+
// omp auto-executes every file in .omp/extensions/, so a committed copy
|
|
114
|
+
// would run arbitrary code on clone. Keep it out of git like the rest.
|
|
115
|
+
'.omp/',
|
|
113
116
|
'.agents/'
|
|
114
117
|
];
|
|
115
118
|
const prefix = ctx.dryRun ? '[dry-run] ' : '';
|
|
@@ -11,7 +11,8 @@ const path = require('path');
|
|
|
11
11
|
const {
|
|
12
12
|
PLATFORMS,
|
|
13
13
|
detectPlatforms,
|
|
14
|
-
getPlatformKeys
|
|
14
|
+
getPlatformKeys,
|
|
15
|
+
resolvePlatformAliases
|
|
15
16
|
} = require('../lib/context');
|
|
16
17
|
const { SUPPORTED, LANGUAGE_LABELS, OTHER_LABEL } = require('../lib/i18n');
|
|
17
18
|
|
|
@@ -36,7 +37,25 @@ function getInstalledLocale(platformKeys = getPlatformKeys()) {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** First step: pick the installer/UI language (interactive only). */
|
|
40
|
+
/**
|
|
41
|
+
* Resolve host aliases on the requested platform list, once, before anything reads it.
|
|
42
|
+
*
|
|
43
|
+
* selectLanguage() filters this list through PLATFORMS to decide which installed runtime
|
|
44
|
+
* to take a saved locale from, so an unresolved alias would empty that list and fall back
|
|
45
|
+
* to scanning every platform folder — `--platform grok` in a project that also has
|
|
46
|
+
* `.codex/` would then silently take Codex's language.
|
|
47
|
+
*/
|
|
48
|
+
function applyPlatformAliases(ctx) {
|
|
49
|
+
const requested = ctx.options.platforms || [];
|
|
50
|
+
if (requested.length === 0) return ctx;
|
|
51
|
+
const { platforms, aliased } = resolvePlatformAliases(requested);
|
|
52
|
+
ctx.options.platforms = platforms;
|
|
53
|
+
ctx.aliasedHost = aliased;
|
|
54
|
+
return ctx;
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
async function selectLanguage(ctx) {
|
|
58
|
+
applyPlatformAliases(ctx);
|
|
40
59
|
// Restore the saved locale BEFORE the interactivity check: a non-interactive
|
|
41
60
|
// upgrade (--yes) must not forget the configured language. Skipping this left
|
|
42
61
|
// ctx at the 'en' default, and patchRuntimeLocale then clobbered the user's
|
|
@@ -161,6 +180,7 @@ async function resolvePlatforms(ctx) {
|
|
|
161
180
|
ctx.ui.info(ctx.t('installingFor', {
|
|
162
181
|
names: platformNames(requested)
|
|
163
182
|
}));
|
|
183
|
+
if (ctx.aliasedHost) ctx.ui.info(ctx.t('grokCompatNotice'));
|
|
164
184
|
reportInstallMode(ctx);
|
|
165
185
|
return ctx;
|
|
166
186
|
}
|
|
@@ -244,6 +264,7 @@ function getInstalledPlatform() {
|
|
|
244
264
|
|
|
245
265
|
module.exports = {
|
|
246
266
|
selectLanguage,
|
|
267
|
+
applyPlatformAliases,
|
|
247
268
|
promptPlatformSelection,
|
|
248
269
|
resolvePlatforms,
|
|
249
270
|
getInstalledPlatform,
|
package/bin/phases/summary.js
CHANGED
|
@@ -59,10 +59,6 @@ function printSummary(ctx) {
|
|
|
59
59
|
}[key];
|
|
60
60
|
if (messageKey) lines.push(t(messageKey));
|
|
61
61
|
}
|
|
62
|
-
// rtk token-saver result (set by phases/setup-rtk.js when setup completed).
|
|
63
|
-
if (ctx.rtkSetupRan) {
|
|
64
|
-
lines.push(t('rtkSummary'));
|
|
65
|
-
}
|
|
66
62
|
const keySkills = skillsNeedingKeys(platforms);
|
|
67
63
|
if (keySkills.length > 0) {
|
|
68
64
|
lines.push(t('nsKeys', { skills: keySkills.join(', ') }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"description": "Native spec-driven workflows for Claude Code and Codex CLI. Bundles CafeKit skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,8 @@ function installSemanticRuntime(target, runtime) {
|
|
|
30
30
|
for (const name of ['semantic-review-authority.cjs', 'completion-authority-state.cjs']) {
|
|
31
31
|
fs.copyFileSync(path.join(PACKAGE_ROOT, `src/${runtime}/hooks`, name), path.join(hooks, name));
|
|
32
32
|
}
|
|
33
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-dir.cjs'), path.join(hooks, 'lib/runtime-dir.cjs'));
|
|
34
|
+
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/hook-payload.cjs'), path.join(hooks, 'lib/hook-payload.cjs'));
|
|
33
35
|
fs.copyFileSync(
|
|
34
36
|
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
|
|
35
37
|
path.join(hooks, 'lib/runtime-path-safety.cjs'),
|