@haposoft/cafekit 0.15.1 → 0.15.3
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 +3 -1
- package/bin/__tests__/codex-native.test.js +228 -0
- package/bin/__tests__/installer-safety.test.js +43 -0
- package/bin/lib/i18n.js +12 -3
- package/bin/lib/skill-deps.js +45 -7
- package/bin/phases/codex-runtime.js +32 -2
- package/bin/phases/select-platform.js +23 -0
- package/bin/phases/skills-setup.js +7 -3
- package/package.json +2 -2
- package/src/claude/skills/chrome-devtools/scripts/package-lock.json +261 -150
- package/src/claude/skills/chrome-devtools/scripts/package.json +1 -1
- package/src/claude/skills/pptx/scripts/package-lock.json +868 -0
- package/src/claude/skills/pptx/scripts/package.json +1 -1
- package/src/codex/hooks.json +14 -14
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Native spec-driven workflow and runtime bundle for Claude Code, Codex CLI, and OpenCode.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://claude.ai/code)
|
|
8
8
|
[](https://developers.openai.com/codex)
|
|
@@ -134,6 +134,8 @@ AGENTS.md
|
|
|
134
134
|
CafeKit owns only its marked block in root `AGENTS.md`. Codex discovers skills
|
|
135
135
|
and custom agents natively after the repository is trusted; no project
|
|
136
136
|
`config.toml` is generated. Review and trust project hooks with `/hooks`.
|
|
137
|
+
On Windows, installed hook commands are bound to the canonical project path
|
|
138
|
+
without requiring Git and remain stable when a session starts in a subdirectory.
|
|
137
139
|
CafeKit uses Codex's native status and usage UI instead of installing the
|
|
138
140
|
Claude statusline.
|
|
139
141
|
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
resolvePlatforms,
|
|
19
19
|
selectLanguage
|
|
20
20
|
} = require('../phases/select-platform');
|
|
21
|
+
const { MESSAGES } = require('../lib/i18n');
|
|
21
22
|
|
|
22
23
|
const PACKAGE_ROOT = path.join(__dirname, '../..');
|
|
23
24
|
const PACKAGE_VERSION = JSON.parse(
|
|
@@ -59,6 +60,12 @@ function allFiles(root, predicate) {
|
|
|
59
60
|
return found;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function allHookHandlers(config) {
|
|
64
|
+
return Object.values(config.hooks).flatMap((groups) => (
|
|
65
|
+
groups.flatMap((group) => group.hooks)
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
|
|
62
69
|
function parseGeneratedTomlString(content, key) {
|
|
63
70
|
const match = content.match(new RegExp(`^${key} = (.+)$`, 'm'));
|
|
64
71
|
assert.ok(match, `missing TOML key: ${key}`);
|
|
@@ -196,6 +203,114 @@ test('same-version install can add Codex beside an existing runtime', async () =
|
|
|
196
203
|
}
|
|
197
204
|
});
|
|
198
205
|
|
|
206
|
+
test('resolvePlatforms interactive prompts to add more platforms when prior install exists', async () => {
|
|
207
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-add-platforms-'));
|
|
208
|
+
const originalCwd = process.cwd();
|
|
209
|
+
try {
|
|
210
|
+
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
path.join(root, '.claude', 'cafekit.json'),
|
|
213
|
+
`${JSON.stringify({ version: PACKAGE_VERSION, platform: 'claude' })}\n`
|
|
214
|
+
);
|
|
215
|
+
process.chdir(root);
|
|
216
|
+
const ctx = {
|
|
217
|
+
options: { platforms: [], forceOverwrite: false },
|
|
218
|
+
interactive: true,
|
|
219
|
+
dryRun: false,
|
|
220
|
+
ui: {
|
|
221
|
+
info() {},
|
|
222
|
+
confirm: async ({ message }) => {
|
|
223
|
+
if (message.includes('Existing platforms') || message.includes('addPlatformsPrompt')) return true;
|
|
224
|
+
if (message.includes('confirmAllDetected') || message.includes('existing configs')) return true;
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
select: async ({ message, options }) => {
|
|
228
|
+
if (message.includes('selectPlatform') || message.includes('Select platform')) {
|
|
229
|
+
return ['codex'];
|
|
230
|
+
}
|
|
231
|
+
return options[0].value;
|
|
232
|
+
},
|
|
233
|
+
isCancel: () => false
|
|
234
|
+
},
|
|
235
|
+
t: (key, vars) => {
|
|
236
|
+
const tpl = (MESSAGES.en && MESSAGES.en[key]) || key;
|
|
237
|
+
if (vars) return tpl.replace(/\{(\w+)\}/g, (_, name) => (vars[name] !== undefined ? String(vars[name]) : `{${name}}`));
|
|
238
|
+
return tpl;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
await resolvePlatforms(ctx);
|
|
242
|
+
assert.deepEqual(ctx.platforms.sort(), ['claude', 'codex'].sort());
|
|
243
|
+
} finally {
|
|
244
|
+
process.chdir(originalCwd);
|
|
245
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('resolvePlatforms interactive keeps existing platforms when user declines to add more', async () => {
|
|
250
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-keep-platforms-'));
|
|
251
|
+
const originalCwd = process.cwd();
|
|
252
|
+
try {
|
|
253
|
+
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
254
|
+
fs.writeFileSync(
|
|
255
|
+
path.join(root, '.claude', 'cafekit.json'),
|
|
256
|
+
`${JSON.stringify({ version: PACKAGE_VERSION, platform: 'claude' })}\n`
|
|
257
|
+
);
|
|
258
|
+
process.chdir(root);
|
|
259
|
+
const ctx = {
|
|
260
|
+
options: { platforms: [], forceOverwrite: false },
|
|
261
|
+
interactive: true,
|
|
262
|
+
dryRun: false,
|
|
263
|
+
ui: {
|
|
264
|
+
info() {},
|
|
265
|
+
confirm: async ({ message }) => {
|
|
266
|
+
// Check that the message is properly rendered without {names} placeholder
|
|
267
|
+
// If i18n uses {existing} but ctx passes {names}, the rendered message will contain {existing}
|
|
268
|
+
if (message.includes('Existing platforms') || message.includes('既存プラットフォーム') || message.includes('Nền tảng hiện có')) {
|
|
269
|
+
assert.ok(!message.includes('{'), `Message should not contain unrendered placeholder: ${message}`);
|
|
270
|
+
assert.ok(!message.includes('}'), `Message should not contain unrendered placeholder: ${message}`);
|
|
271
|
+
return false; // decline to add more
|
|
272
|
+
}
|
|
273
|
+
return true; // confirmAllDetected
|
|
274
|
+
},
|
|
275
|
+
select: async () => ['claude'],
|
|
276
|
+
isCancel: () => false
|
|
277
|
+
},
|
|
278
|
+
t: (key, vars) => {
|
|
279
|
+
const tpl = (MESSAGES.en && MESSAGES.en[key]) || key;
|
|
280
|
+
if (vars) return tpl.replace(/\{(\w+)\}/g, (_, name) => (vars[name] !== undefined ? String(vars[name]) : `{${name}}`));
|
|
281
|
+
return tpl;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
await resolvePlatforms(ctx);
|
|
285
|
+
assert.deepEqual(ctx.platforms, ['claude']);
|
|
286
|
+
} finally {
|
|
287
|
+
process.chdir(originalCwd);
|
|
288
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('addPlatformsPrompt placeholder consistency across all locales', () => {
|
|
293
|
+
const expectedPlaceholders = ['names'];
|
|
294
|
+
const placeholderRegex = /\{(\w+)\}/g;
|
|
295
|
+
|
|
296
|
+
for (const [locale, messages] of Object.entries(MESSAGES)) {
|
|
297
|
+
const template = messages.addPlatformsPrompt;
|
|
298
|
+
assert.ok(template, `Missing addPlatformsPrompt key in locale: ${locale}`);
|
|
299
|
+
|
|
300
|
+
const placeholders = [];
|
|
301
|
+
let match;
|
|
302
|
+
while ((match = placeholderRegex.exec(template)) !== null) {
|
|
303
|
+
placeholders.push(match[1]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
assert.deepEqual(
|
|
307
|
+
placeholders,
|
|
308
|
+
expectedPlaceholders,
|
|
309
|
+
`Locale ${locale} addPlatformsPrompt has incorrect placeholders. Template: "${template}". Expected: ${JSON.stringify(expectedPlaceholders)}, Got: ${JSON.stringify(placeholders)}`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
199
314
|
test('same-version non-interactive install performs a selective refresh', async () => {
|
|
200
315
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-version-'));
|
|
201
316
|
const originalCwd = process.cwd();
|
|
@@ -316,6 +431,64 @@ test('Codex dry-run leaves both managed roots untouched', () => {
|
|
|
316
431
|
});
|
|
317
432
|
});
|
|
318
433
|
|
|
434
|
+
test('Codex Windows hook launchers stay project-bound without Git from nested cwd', () => {
|
|
435
|
+
inTempProject((root) => {
|
|
436
|
+
const projectRoot = path.join(root, 'project with spaces');
|
|
437
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
438
|
+
const installed = install(projectRoot);
|
|
439
|
+
assert.equal(installed.status, 0, `${installed.stdout}\n${installed.stderr}`);
|
|
440
|
+
|
|
441
|
+
const config = JSON.parse(
|
|
442
|
+
fs.readFileSync(path.join(projectRoot, '.codex', 'hooks.json'), 'utf8')
|
|
443
|
+
);
|
|
444
|
+
const handlers = allHookHandlers(config);
|
|
445
|
+
assert.equal(handlers.length, 14);
|
|
446
|
+
for (const handler of handlers) {
|
|
447
|
+
assert.doesNotMatch(handler.commandWindows, /\$\(/);
|
|
448
|
+
assert.doesNotMatch(handler.commandWindows, /\bgit\b/i);
|
|
449
|
+
assert.doesNotMatch(handler.commandWindows, /process\.cwd\(\)|existsSync/);
|
|
450
|
+
const encodedPath = handler.commandWindows.match(/\s([A-Za-z0-9_-]+)$/)?.[1];
|
|
451
|
+
assert.ok(encodedPath, `missing encoded hook path in: ${handler.commandWindows}`);
|
|
452
|
+
const target = Buffer.from(encodedPath, 'base64url').toString('utf8');
|
|
453
|
+
assert.equal(path.dirname(target), fs.realpathSync(path.join(projectRoot, '.codex', 'hooks')));
|
|
454
|
+
assert.equal(
|
|
455
|
+
fs.existsSync(target),
|
|
456
|
+
true,
|
|
457
|
+
`missing installed hook: ${path.basename(target)}`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const nested = path.join(projectRoot, 'nested', 'workspace');
|
|
462
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
463
|
+
const shadowHooks = path.join(projectRoot, 'nested', '.codex', 'hooks');
|
|
464
|
+
const shadowMarker = path.join(root, 'shadow-hook-ran');
|
|
465
|
+
fs.mkdirSync(shadowHooks, { recursive: true });
|
|
466
|
+
fs.writeFileSync(
|
|
467
|
+
path.join(shadowHooks, 'session.cjs'),
|
|
468
|
+
`require('node:fs').writeFileSync(${JSON.stringify(shadowMarker)}, 'unsafe')\n`
|
|
469
|
+
);
|
|
470
|
+
const session = config.hooks.SessionStart[0].hooks[0];
|
|
471
|
+
const nodeCommand = session.commandWindows.replace(/^node /, `"${process.execPath}" `);
|
|
472
|
+
const noGitEnv = { ...process.env, PATH: '' };
|
|
473
|
+
const launched = spawnSync(nodeCommand, {
|
|
474
|
+
cwd: nested,
|
|
475
|
+
encoding: 'utf8',
|
|
476
|
+
input: JSON.stringify({
|
|
477
|
+
session_id: 'windows-launcher-test',
|
|
478
|
+
cwd: nested,
|
|
479
|
+
hook_event_name: 'SessionStart',
|
|
480
|
+
source: 'startup'
|
|
481
|
+
}),
|
|
482
|
+
env: noGitEnv,
|
|
483
|
+
shell: true
|
|
484
|
+
});
|
|
485
|
+
assert.equal(launched.status, 0, launched.stderr);
|
|
486
|
+
assert.match(launched.stdout, /Session startup\./);
|
|
487
|
+
assert.match(launched.stdout, /CafeKit project root:/);
|
|
488
|
+
assert.equal(fs.existsSync(shadowMarker), false);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
319
492
|
test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
320
493
|
inTempProject((root) => {
|
|
321
494
|
const userInstructions = '# User rules\n\nKeep this exact.\n';
|
|
@@ -456,3 +629,58 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
|
|
|
456
629
|
assert.doesNotMatch(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
|
|
457
630
|
});
|
|
458
631
|
});
|
|
632
|
+
|
|
633
|
+
test('Codex install on top of existing Claude installation preserves content and adds Codex', () => {
|
|
634
|
+
inTempProject((root) => {
|
|
635
|
+
// Setup existing Claude installation with content
|
|
636
|
+
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
637
|
+
fs.writeFileSync(
|
|
638
|
+
path.join(root, '.claude', 'cafekit.json'),
|
|
639
|
+
`${JSON.stringify({ version: PACKAGE_VERSION, platform: 'claude' })}\n`
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// Existing CLAUDE.md with content
|
|
643
|
+
const claudeMdContent = '# CLAUDE.md\n\n## User Instructions\n\nThis is my existing CLAUDE.md content.\n';
|
|
644
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), claudeMdContent);
|
|
645
|
+
|
|
646
|
+
// Existing AGENTS.md with content
|
|
647
|
+
const agentsMdContent = '# AGENTS.md\n\n## User Rules\n\nKeep this exact.\n';
|
|
648
|
+
fs.writeFileSync(path.join(root, 'AGENTS.md'), agentsMdContent);
|
|
649
|
+
|
|
650
|
+
// Run Codex install
|
|
651
|
+
const result = install(root, ['--platform', 'codex']);
|
|
652
|
+
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
|
653
|
+
|
|
654
|
+
// Assert .claude/ and its content still exist
|
|
655
|
+
assert.equal(fs.existsSync(path.join(root, '.claude')), true, '.claude should exist');
|
|
656
|
+
assert.equal(fs.existsSync(path.join(root, '.claude', 'cafekit.json')), true, '.claude/cafekit.json should exist');
|
|
657
|
+
const claudeMetadata = JSON.parse(fs.readFileSync(path.join(root, '.claude', 'cafekit.json'), 'utf8'));
|
|
658
|
+
assert.equal(claudeMetadata.platform, 'claude', '.claude/cafekit.json should still be claude platform');
|
|
659
|
+
|
|
660
|
+
// Assert .codex/ is created with payload
|
|
661
|
+
assert.equal(fs.existsSync(path.join(root, '.codex')), true, '.codex should be created');
|
|
662
|
+
assert.equal(fs.existsSync(path.join(root, '.codex', 'hooks.json')), true, '.codex/hooks.json should exist');
|
|
663
|
+
assert.equal(fs.existsSync(path.join(root, '.codex', 'runtime.json')), true, '.codex/runtime.json should exist');
|
|
664
|
+
assert.equal(fs.existsSync(path.join(root, '.codex', 'hooks', 'privacy-block.cjs')), true, '.codex/hooks/privacy-block.cjs should exist');
|
|
665
|
+
assert.equal(fs.existsSync(path.join(root, '.codex', 'rules', 'workflow.md')), true, '.codex/rules/workflow.md should exist');
|
|
666
|
+
assert.equal(fs.existsSync(path.join(root, '.codex', 'scripts', 'validate-spec-output.cjs')), true, '.codex/scripts/validate-spec-output.cjs should exist');
|
|
667
|
+
assert.equal(fs.existsSync(path.join(root, '.agents', '.gitignore')), true, '.agents/.gitignore should exist');
|
|
668
|
+
assert.equal(fs.existsSync(path.join(root, '.agents', 'skills', 'specs', 'SKILL.md')), true, '.agents/skills/specs/SKILL.md should exist');
|
|
669
|
+
|
|
670
|
+
// Assert .codex/cafekit.json has platform codex
|
|
671
|
+
const codexMetadata = JSON.parse(fs.readFileSync(path.join(root, '.codex', 'cafekit.json'), 'utf8'));
|
|
672
|
+
assert.equal(codexMetadata.platform, 'codex', '.codex/cafekit.json should have platform codex');
|
|
673
|
+
assert.equal(codexMetadata.version, PACKAGE_VERSION, '.codex/cafekit.json should have current version');
|
|
674
|
+
|
|
675
|
+
// Assert AGENTS.md root contains CODEX markers AND preserves original content
|
|
676
|
+
const agentsMd = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
|
|
677
|
+
assert.match(agentsMd, /<!-- CAFEKIT CODEX START -->/, 'AGENTS.md should have CODEX START marker');
|
|
678
|
+
assert.match(agentsMd, /<!-- CAFEKIT CODEX END -->/, 'AGENTS.md should have CODEX END marker');
|
|
679
|
+
assert.match(agentsMd, /Keep this exact\./, 'AGENTS.md should preserve original user content');
|
|
680
|
+
assert.ok(agentsMd.startsWith(agentsMdContent), 'AGENTS.md should start with original user content');
|
|
681
|
+
|
|
682
|
+
// Assert CLAUDE.md is unchanged
|
|
683
|
+
const claudeMd = fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf8');
|
|
684
|
+
assert.equal(claudeMd, claudeMdContent, 'CLAUDE.md should be unchanged');
|
|
685
|
+
});
|
|
686
|
+
});
|
|
@@ -11,6 +11,10 @@ const {
|
|
|
11
11
|
copyOpenCodeAgentsMdFile,
|
|
12
12
|
normalizeOpenCodeBody
|
|
13
13
|
} = require('../lib/opencode-install');
|
|
14
|
+
const {
|
|
15
|
+
commandFailureReason,
|
|
16
|
+
resolvePackageCommand
|
|
17
|
+
} = require('../lib/skill-deps');
|
|
14
18
|
const { copyClaudeMdFile } = require('../phases/claude-runtime');
|
|
15
19
|
const { configureAddressing, patchRuntimeLocale } = require('../phases/post-install');
|
|
16
20
|
|
|
@@ -18,6 +22,45 @@ const START = '<!-- CAFEKIT CLAUDE START -->';
|
|
|
18
22
|
const END = '<!-- CAFEKIT CLAUDE END -->';
|
|
19
23
|
const TEMPLATE = fs.readFileSync(path.join(__dirname, '../../src/claude/CLAUDE.md'), 'utf8');
|
|
20
24
|
|
|
25
|
+
test('skill dependency package commands use cmd.exe for Windows npm launchers', () => {
|
|
26
|
+
const windows = resolvePackageCommand(
|
|
27
|
+
'npm',
|
|
28
|
+
['install', '--no-audit', '--no-fund'],
|
|
29
|
+
'win32',
|
|
30
|
+
{ ComSpec: 'C:\\Windows\\System32\\cmd.exe' }
|
|
31
|
+
);
|
|
32
|
+
assert.deepEqual(windows, {
|
|
33
|
+
command: 'C:\\Windows\\System32\\cmd.exe',
|
|
34
|
+
args: ['/d', '/s', '/c', 'npm.cmd', 'install', '--no-audit', '--no-fund']
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const posix = resolvePackageCommand('npx', ['playwright', '--version'], 'linux', {});
|
|
38
|
+
assert.deepEqual(posix, {
|
|
39
|
+
command: 'npx',
|
|
40
|
+
args: ['playwright', '--version']
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('skill dependency failures surface the useful npm or spawn error code', () => {
|
|
45
|
+
assert.equal(
|
|
46
|
+
commandFailureReason({
|
|
47
|
+
stderr: "npm error Request failed\nnpm error code ENOTCACHED\nnpm error Log unavailable\n",
|
|
48
|
+
stdout: '',
|
|
49
|
+
status: 1
|
|
50
|
+
}),
|
|
51
|
+
'npm error code ENOTCACHED'
|
|
52
|
+
);
|
|
53
|
+
assert.equal(
|
|
54
|
+
commandFailureReason({
|
|
55
|
+
stderr: 'spawn npm.cmd EINVAL',
|
|
56
|
+
stdout: '',
|
|
57
|
+
status: null,
|
|
58
|
+
errorCode: 'EINVAL'
|
|
59
|
+
}),
|
|
60
|
+
'spawn npm.cmd EINVAL'
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
21
64
|
function inTempProject(run) {
|
|
22
65
|
const originalCwd = process.cwd();
|
|
23
66
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-installer-safety-'));
|
package/bin/lib/i18n.js
CHANGED
|
@@ -79,7 +79,7 @@ const MESSAGES = {
|
|
|
79
79
|
pipFailed: '{skill} — some packages failed. Retry:\n {cmd}',
|
|
80
80
|
npmInstalling: 'Installing Node packages: {skill}',
|
|
81
81
|
npmInstalled: '{skill} — Node packages ready',
|
|
82
|
-
npmFailed: '{skill} — npm install failed.
|
|
82
|
+
npmFailed: '{skill} — npm install failed ({error}). Retry in: {dir}',
|
|
83
83
|
playwrightInstalling: 'Downloading Playwright browser for {skill}',
|
|
84
84
|
playwrightReady: '{skill} — Playwright browser ready',
|
|
85
85
|
playwrightSkipped: '{skill} — Playwright browser download skipped',
|
|
@@ -132,6 +132,9 @@ const MESSAGES = {
|
|
|
132
132
|
langKept: 'Language: {lang} (saved — skipping prompt)',
|
|
133
133
|
platformKept: 'Platform: {names} (saved — skipping prompt)',
|
|
134
134
|
|
|
135
|
+
// Add more platforms prompt (interactive, prior install exists)
|
|
136
|
+
addPlatformsPrompt: 'Existing platforms: {names}. Add more?',
|
|
137
|
+
|
|
135
138
|
// Version picker
|
|
136
139
|
versionPickPrompt: 'Select CafeKit version to install:',
|
|
137
140
|
versionPickCurrent: '(installed)',
|
|
@@ -199,7 +202,7 @@ const MESSAGES = {
|
|
|
199
202
|
pipFailed: '{skill} — 一部のパッケージが失敗しました。再試行:\n {cmd}',
|
|
200
203
|
npmInstalling: 'Node パッケージをインストール中: {skill}',
|
|
201
204
|
npmInstalled: '{skill} — Node パッケージの準備ができました',
|
|
202
|
-
npmFailed: '{skill} — npm install
|
|
205
|
+
npmFailed: '{skill} — npm install に失敗しました ({error})。{dir} で手動再試行してください。',
|
|
203
206
|
playwrightInstalling: '{skill} 用の Playwright ブラウザをダウンロード中',
|
|
204
207
|
playwrightReady: '{skill} — Playwright ブラウザの準備ができました',
|
|
205
208
|
playwrightSkipped: '{skill} — Playwright ブラウザのダウンロードをスキップしました',
|
|
@@ -249,6 +252,9 @@ const MESSAGES = {
|
|
|
249
252
|
langKept: '言語: {lang} (保存済み — スキップ)',
|
|
250
253
|
platformKept: 'プラットフォーム: {names} (保存済み — スキップ)',
|
|
251
254
|
|
|
255
|
+
// Add more platforms prompt (interactive, prior install exists)
|
|
256
|
+
addPlatformsPrompt: '既存プラットフォーム: {names}。さらに追加しますか?',
|
|
257
|
+
|
|
252
258
|
// Version picker
|
|
253
259
|
versionPickPrompt: 'インストールする CafeKit バージョンを選択:',
|
|
254
260
|
versionPickCurrent: '(インストール済み)',
|
|
@@ -316,7 +322,7 @@ const MESSAGES = {
|
|
|
316
322
|
pipFailed: '{skill} — một số gói cài thất bại. Chạy lại thủ công:\n {cmd}',
|
|
317
323
|
npmInstalling: 'Đang cài gói Node: {skill}',
|
|
318
324
|
npmInstalled: '{skill} — gói Node đã sẵn sàng',
|
|
319
|
-
npmFailed: '{skill} — npm install thất bại. Hãy thử lại trong: {dir}',
|
|
325
|
+
npmFailed: '{skill} — npm install thất bại ({error}). Hãy thử lại trong: {dir}',
|
|
320
326
|
playwrightInstalling: 'Đang tải Playwright browser cho {skill}',
|
|
321
327
|
playwrightReady: '{skill} — Playwright browser đã sẵn sàng',
|
|
322
328
|
playwrightSkipped: '{skill} — đã bỏ qua tải Playwright browser',
|
|
@@ -366,6 +372,9 @@ const MESSAGES = {
|
|
|
366
372
|
langKept: 'Ngôn ngữ: {lang} (đã lưu — bỏ qua chọn lại)',
|
|
367
373
|
platformKept: 'Nền tảng: {names} (đã lưu — bỏ qua chọn lại)',
|
|
368
374
|
|
|
375
|
+
// Add more platforms prompt (interactive, prior install exists)
|
|
376
|
+
addPlatformsPrompt: 'Nền tảng hiện có: {names}. Thêm nền tảng khác?',
|
|
377
|
+
|
|
369
378
|
// Version picker
|
|
370
379
|
versionPickPrompt: 'Chọn phiên bản CafeKit để cài:',
|
|
371
380
|
versionPickCurrent: '(đang cài)',
|
package/bin/lib/skill-deps.js
CHANGED
|
@@ -17,6 +17,19 @@ const { spawnSync, spawn } = require('child_process');
|
|
|
17
17
|
|
|
18
18
|
const isWindows = process.platform === 'win32';
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* npm and npx are `.cmd` launchers on Windows, so Node cannot spawn them
|
|
22
|
+
* directly without a command interpreter. Keep the shell boundary explicit;
|
|
23
|
+
* all arguments passed here are installer-owned constants.
|
|
24
|
+
*/
|
|
25
|
+
function resolvePackageCommand(command, args, platform = process.platform, env = process.env) {
|
|
26
|
+
if (platform !== 'win32') return { command, args };
|
|
27
|
+
return {
|
|
28
|
+
command: env.ComSpec || env.COMSPEC || 'cmd.exe',
|
|
29
|
+
args: ['/d', '/s', '/c', `${command}.cmd`, ...args]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
/** Run a command synchronously, return { ok, stdout, stderr, status }. Never throws. */
|
|
21
34
|
function run(cmd, args, opts = {}) {
|
|
22
35
|
const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
|
|
@@ -48,11 +61,32 @@ function runAsync(cmd, args, opts = {}) {
|
|
|
48
61
|
});
|
|
49
62
|
});
|
|
50
63
|
child.on('error', (err) => {
|
|
51
|
-
resolve({
|
|
64
|
+
resolve({
|
|
65
|
+
ok: false,
|
|
66
|
+
status: null,
|
|
67
|
+
stdout,
|
|
68
|
+
stderr: stderr || err.message,
|
|
69
|
+
errorCode: err.code || ''
|
|
70
|
+
});
|
|
52
71
|
});
|
|
53
72
|
});
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
/** Return one actionable line without flooding the installer UI with npm output. */
|
|
76
|
+
function commandFailureReason(result) {
|
|
77
|
+
const lines = `${result.stderr || ''}\n${result.stdout || ''}`
|
|
78
|
+
.split(/\r?\n/)
|
|
79
|
+
.map((line) => line.trim())
|
|
80
|
+
.filter(Boolean);
|
|
81
|
+
return lines.find((line) => (
|
|
82
|
+
/\bE[A-Z0-9_]{2,}\b/.test(line) || /\bnpm error code\b/i.test(line)
|
|
83
|
+
))
|
|
84
|
+
|| lines.find((line) => /^npm error\b/i.test(line))
|
|
85
|
+
|| lines.at(-1)
|
|
86
|
+
|| result.errorCode
|
|
87
|
+
|| `exit ${result.status ?? 'unknown'}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
/** Is a command available on PATH? */
|
|
57
91
|
function hasCmd(name) {
|
|
58
92
|
const probe = isWindows ? run('where', [name]) : run('which', [name]);
|
|
@@ -104,12 +138,12 @@ async function pipInstall(skillsDir, requirementsPath, upgrade = false) {
|
|
|
104
138
|
return r.ok;
|
|
105
139
|
}
|
|
106
140
|
|
|
107
|
-
/** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir.
|
|
141
|
+
/** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. */
|
|
108
142
|
async function npmInstall(dir, upgrade = false, extraEnv = {}) {
|
|
109
143
|
const args = upgrade ? ['update', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund'];
|
|
110
144
|
const env = { ...process.env, ...extraEnv };
|
|
111
|
-
const
|
|
112
|
-
return
|
|
145
|
+
const npm = resolvePackageCommand('npm', args);
|
|
146
|
+
return runAsync(npm.command, npm.args, { cwd: dir, env });
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
/**
|
|
@@ -130,10 +164,11 @@ async function installChromium(scriptsDir, interactive = false) {
|
|
|
130
164
|
const r = await runAsync('node', [installMjs], { cwd: scriptsDir });
|
|
131
165
|
return r.ok;
|
|
132
166
|
}
|
|
167
|
+
const npx = resolvePackageCommand('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome']);
|
|
133
168
|
if (interactive) {
|
|
134
|
-
return run(
|
|
169
|
+
return run(npx.command, npx.args, { cwd: scriptsDir, stdio: 'inherit' }).ok;
|
|
135
170
|
}
|
|
136
|
-
const r = await runAsync(
|
|
171
|
+
const r = await runAsync(npx.command, npx.args, { cwd: scriptsDir });
|
|
137
172
|
return r.ok;
|
|
138
173
|
}
|
|
139
174
|
|
|
@@ -149,7 +184,8 @@ function pkgHasDep(scriptsDir, name) {
|
|
|
149
184
|
|
|
150
185
|
/** Download the Playwright Chromium browser (for the pptx html2pptx workflow). Async — keeps spinner alive. */
|
|
151
186
|
async function installPlaywrightBrowser(scriptsDir) {
|
|
152
|
-
const
|
|
187
|
+
const npx = resolvePackageCommand('npx', ['--yes', 'playwright', 'install', 'chromium']);
|
|
188
|
+
const r = await runAsync(npx.command, npx.args, { cwd: scriptsDir });
|
|
153
189
|
return r.ok;
|
|
154
190
|
}
|
|
155
191
|
|
|
@@ -255,6 +291,8 @@ function systemHint(tool) {
|
|
|
255
291
|
|
|
256
292
|
module.exports = {
|
|
257
293
|
isWindows,
|
|
294
|
+
resolvePackageCommand,
|
|
295
|
+
commandFailureReason,
|
|
258
296
|
hasCmd,
|
|
259
297
|
findPython,
|
|
260
298
|
venvDir,
|
|
@@ -20,6 +20,33 @@ const CODEX_OWN_RUNTIME = [
|
|
|
20
20
|
['hooks.json', 'hooks.json']
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
+
const WINDOWS_HOOK_COMMAND = /^node "\.codex\/hooks\/([a-z0-9-]+\.cjs)"$/;
|
|
24
|
+
|
|
25
|
+
function materializeWindowsHookCommands(content, projectRoot = process.cwd()) {
|
|
26
|
+
const config = JSON.parse(content);
|
|
27
|
+
const canonicalRoot = fs.realpathSync(projectRoot);
|
|
28
|
+
|
|
29
|
+
for (const groups of Object.values(config.hooks || {})) {
|
|
30
|
+
for (const group of groups) {
|
|
31
|
+
for (const handler of group.hooks || []) {
|
|
32
|
+
if (typeof handler.commandWindows !== 'string') continue;
|
|
33
|
+
const match = handler.commandWindows.match(WINDOWS_HOOK_COMMAND);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error(`Unsupported Codex Windows hook command: ${handler.commandWindows}`);
|
|
36
|
+
}
|
|
37
|
+
const hookPath = path.join(canonicalRoot, '.codex', 'hooks', match[1]);
|
|
38
|
+
const encodedPath = Buffer.from(hookPath, 'utf8').toString('base64url');
|
|
39
|
+
handler.commandWindows = (
|
|
40
|
+
'node -e "require(Buffer.from(process.argv[1],\'base64url\').toString(\'utf8\'))" ' +
|
|
41
|
+
encodedPath
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
function writeSourceFile(ctx, platformKey, src, dest, label, transform) {
|
|
24
51
|
const platform = PLATFORMS[platformKey];
|
|
25
52
|
const { action } = writeManagedFile({
|
|
@@ -41,7 +68,10 @@ function installRuntimeFiles(ctx, platformKey) {
|
|
|
41
68
|
platformKey,
|
|
42
69
|
path.join(CODEX_SRC, sourceRel),
|
|
43
70
|
path.join(platform.folder, targetRel),
|
|
44
|
-
`Codex runtime: ${targetRel}
|
|
71
|
+
`Codex runtime: ${targetRel}`,
|
|
72
|
+
sourceRel === 'hooks.json'
|
|
73
|
+
? (content) => materializeWindowsHookCommands(content, process.cwd())
|
|
74
|
+
: undefined
|
|
45
75
|
);
|
|
46
76
|
}
|
|
47
77
|
writeSourceFile(
|
|
@@ -107,4 +137,4 @@ function installCodexRuntime(ctx, platformKey) {
|
|
|
107
137
|
return ctx;
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
module.exports = { installCodexRuntime };
|
|
140
|
+
module.exports = { installCodexRuntime, materializeWindowsHookCommands };
|
|
@@ -128,6 +128,19 @@ function cancelSelection(ctx) {
|
|
|
128
128
|
return [];
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
async function promptAddMorePlatforms(ctx, savedPlatforms) {
|
|
132
|
+
const allKeys = getPlatformKeys();
|
|
133
|
+
const available = allKeys.filter((key) => !savedPlatforms.includes(key));
|
|
134
|
+
if (available.length === 0) return false;
|
|
135
|
+
|
|
136
|
+
const r = await ctx.ui.confirm(
|
|
137
|
+
{ message: ctx.t('addPlatformsPrompt', { names: platformNames(savedPlatforms) }), initialValue: false },
|
|
138
|
+
false
|
|
139
|
+
);
|
|
140
|
+
if (ctx.ui.isCancel(r)) return false;
|
|
141
|
+
return r;
|
|
142
|
+
}
|
|
143
|
+
|
|
131
144
|
async function promptPlatforms(ctx) {
|
|
132
145
|
const platforms = await promptPlatformSelection(ctx);
|
|
133
146
|
return platforms.length > 0 ? platforms : cancelSelection(ctx);
|
|
@@ -164,6 +177,16 @@ async function resolvePlatforms(ctx) {
|
|
|
164
177
|
platforms = await promptPlatforms(ctx);
|
|
165
178
|
if (ctx.cancelled) return ctx;
|
|
166
179
|
}
|
|
180
|
+
} else if (ctx.interactive) {
|
|
181
|
+
// No newly detected platforms, but user might want to ADD more platforms
|
|
182
|
+
const wantAdd = await promptAddMorePlatforms(ctx, savedPlatforms);
|
|
183
|
+
if (wantAdd) {
|
|
184
|
+
const added = await promptPlatformSelection(ctx);
|
|
185
|
+
if (ctx.cancelled) return ctx;
|
|
186
|
+
if (added.length > 0) {
|
|
187
|
+
platforms = [...new Set([...platforms, ...added])];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
167
190
|
}
|
|
168
191
|
ctx.platforms = platforms;
|
|
169
192
|
ctx.ui.info(ctx.t('platformKept', { names: platformNames(platforms) }));
|
|
@@ -79,12 +79,16 @@ async function setupNode(ctx, skillsDir) {
|
|
|
79
79
|
const extraEnv = (skill === 'chrome-devtools' && systemChrome)
|
|
80
80
|
? { PUPPETEER_SKIP_DOWNLOAD: 'true' }
|
|
81
81
|
: {};
|
|
82
|
-
const
|
|
82
|
+
const result = await dep.npmInstall(dir, hasNodeModules, extraEnv);
|
|
83
83
|
|
|
84
|
-
if (ok) {
|
|
84
|
+
if (result.ok) {
|
|
85
85
|
ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
|
|
86
86
|
} else {
|
|
87
|
-
ctx.ui.stopSpinner(ctx.t('npmFailed', {
|
|
87
|
+
ctx.ui.stopSpinner(ctx.t('npmFailed', {
|
|
88
|
+
skill,
|
|
89
|
+
dir,
|
|
90
|
+
error: dep.commandFailureReason(result)
|
|
91
|
+
}));
|
|
88
92
|
continue;
|
|
89
93
|
}
|
|
90
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "Native spec-driven workflows for Claude Code, Codex CLI, and OpenCode. Bundles CafeKit skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@opencode-ai/plugin": "1.15.11"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@clack/prompts": "
|
|
52
|
+
"@clack/prompts": "0.11.0",
|
|
53
53
|
"picocolors": "^1.1.1"
|
|
54
54
|
}
|
|
55
55
|
}
|