@haposoft/cafekit 0.15.1 → 0.16.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +230 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +553 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/i18n.js +12 -3
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/lib/skill-deps.js +45 -7
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +41 -7
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/bin/phases/select-platform.js +23 -0
- package/bin/phases/skills-setup.js +7 -3
- package/package.json +15 -5
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +21 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +3 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/chrome-devtools/scripts/package-lock.json +261 -150
- package/src/claude/skills/chrome-devtools/scripts/package.json +1 -1
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +58 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/pptx/scripts/package-lock.json +868 -0
- package/src/claude/skills/pptx/scripts/package.json +1 -1
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/codex/hooks.json +14 -14
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
package/bin/install.js
CHANGED
|
@@ -28,7 +28,9 @@ const {
|
|
|
28
28
|
copyClaudeRuntimeFiles,
|
|
29
29
|
removeObsoleteClaudeRuntimeFiles,
|
|
30
30
|
copyClaudeMdFile,
|
|
31
|
-
|
|
31
|
+
ensureSharedAgentsMdCore,
|
|
32
|
+
copyRulesDirectory,
|
|
33
|
+
removeObsoleteAgents
|
|
32
34
|
} = require('./phases/claude-runtime');
|
|
33
35
|
const { mergeClaudeSettings } = require('./phases/claude-settings');
|
|
34
36
|
const { installOpenCodeRuntime } = require('./phases/opencode-runtime');
|
|
@@ -57,6 +59,7 @@ function installPlatform(ctx, platformKey) {
|
|
|
57
59
|
ctx.ui.startSpinner(ctx.t('installingPlatform', { name: platform.name }));
|
|
58
60
|
|
|
59
61
|
copyPlatformFiles(ctx, platformKey);
|
|
62
|
+
removeObsoleteAgents(ctx, platformKey);
|
|
60
63
|
|
|
61
64
|
if (platformKey === 'claude') {
|
|
62
65
|
copyRoutingFile(ctx, platformKey);
|
|
@@ -174,6 +177,7 @@ async function main() {
|
|
|
174
177
|
ctx.backupDir = backup.snapshot([...targets, '.gitignore'], ctx.runId);
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
ensureSharedAgentsMdCore(ctx);
|
|
177
181
|
for (const platformKey of ctx.platforms) {
|
|
178
182
|
installPlatform(ctx, platformKey);
|
|
179
183
|
}
|
package/bin/lib/codex-install.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { normalizeSourcePaths } = require('./copy-utils');
|
|
4
5
|
const { splitFrontmatter } = require('./codex-frontmatter');
|
|
6
|
+
const { preserveAddressingSection } = require('./instruction-blocks');
|
|
5
7
|
|
|
6
8
|
const CODEX_BLOCK_START = '<!-- CAFEKIT CODEX START -->';
|
|
7
9
|
const CODEX_BLOCK_END = '<!-- CAFEKIT CODEX END -->';
|
|
@@ -14,7 +16,7 @@ const AGENT_NAMES = [
|
|
|
14
16
|
'deployer',
|
|
15
17
|
'docs-keeper',
|
|
16
18
|
'git-ops',
|
|
17
|
-
'
|
|
19
|
+
'implementer',
|
|
18
20
|
'inspector',
|
|
19
21
|
'project-manager',
|
|
20
22
|
'researcher',
|
|
@@ -63,10 +65,13 @@ function applyReplacements(content, replacements) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function normalizeRuntimePaths(content) {
|
|
66
|
-
return content
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
return normalizeSourcePaths(content, {
|
|
69
|
+
runtimeRoot: '.codex',
|
|
70
|
+
skillsRoot: '.agents/skills'
|
|
71
|
+
})
|
|
72
|
+
.replace(/(?<!~\/)\.claude\/skills\b/g, '.agents/skills')
|
|
73
|
+
.replace(/(?<!~\\)\.claude\\skills\b/g, '.agents\\skills')
|
|
74
|
+
.replace(/(?<!~\/)\.claude(?=[/\\])/g, '.codex')
|
|
70
75
|
.replace(/(['"])\.claude\1/g, '$1.codex$1')
|
|
71
76
|
.replace(/`\.claude`/g, '`.codex`');
|
|
72
77
|
}
|
|
@@ -163,8 +168,17 @@ function managedRange(content) {
|
|
|
163
168
|
|
|
164
169
|
function upsertManagedCodexBlock(existingContent, blockContent) {
|
|
165
170
|
const existing = String(existingContent || '');
|
|
166
|
-
|
|
171
|
+
let body = String(blockContent).trim();
|
|
172
|
+
// Reinstall replaces the managed block in place. If the new template dropped
|
|
173
|
+
// its Addressing section, carry the exact section over from the existing
|
|
174
|
+
// managed block so the saved address survives for setupAddressing. Only reads
|
|
175
|
+
// the managed body — never user-owned sections outside the block.
|
|
167
176
|
const range = managedRange(existing);
|
|
177
|
+
if (range) {
|
|
178
|
+
const existingBody = existing.slice(range.start + CODEX_BLOCK_START.length, range.end);
|
|
179
|
+
body = preserveAddressingSection(body, existingBody);
|
|
180
|
+
}
|
|
181
|
+
const block = `${CODEX_BLOCK_START}\n${body}\n${CODEX_BLOCK_END}`;
|
|
168
182
|
if (range === false) return existing;
|
|
169
183
|
if (range) {
|
|
170
184
|
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end + CODEX_BLOCK_END.length)}`;
|
|
@@ -190,6 +204,7 @@ module.exports = {
|
|
|
190
204
|
getCodexCopyOptions,
|
|
191
205
|
codexAgentName,
|
|
192
206
|
convertCodexAgentContent,
|
|
207
|
+
managedRange,
|
|
193
208
|
upsertManagedCodexBlock,
|
|
194
209
|
transformManagedCodexContent
|
|
195
210
|
};
|
package/bin/lib/context.js
CHANGED
|
@@ -16,6 +16,7 @@ const packageJson = require('../../package.json');
|
|
|
16
16
|
const { getOpenCodeCopyOptions } = require('./opencode-install');
|
|
17
17
|
const { isInteractive, createUI } = require('./ui');
|
|
18
18
|
const { resolveLang, createTranslator } = require('./i18n');
|
|
19
|
+
const { normalizeSourcePaths } = require('./copy-utils');
|
|
19
20
|
|
|
20
21
|
const INSTALL_COMMAND = `npx ${packageJson.name}@${packageJson.version}`;
|
|
21
22
|
|
|
@@ -82,7 +83,7 @@ const PLATFORMS = {
|
|
|
82
83
|
commandPrefix: '/',
|
|
83
84
|
sourceDir: 'claude', // Maps to src/claude/
|
|
84
85
|
sourceSubdir: 'commands', // Source subfolder within src/claude/
|
|
85
|
-
backupTargets: ['.claude', 'CLAUDE.md'],
|
|
86
|
+
backupTargets: ['.claude', 'CLAUDE.md', 'AGENTS.md'],
|
|
86
87
|
capabilities: {
|
|
87
88
|
skills: true,
|
|
88
89
|
agents: true,
|
|
@@ -216,7 +217,16 @@ function getCopyOptions(platformKey, baseOptions = {}) {
|
|
|
216
217
|
// during module initialization.
|
|
217
218
|
return require('./codex-install').getCodexCopyOptions(baseOptions);
|
|
218
219
|
}
|
|
219
|
-
return baseOptions;
|
|
220
|
+
if (platformKey !== 'claude') return baseOptions;
|
|
221
|
+
return {
|
|
222
|
+
...baseOptions,
|
|
223
|
+
transform: (content, sourcePath) => normalizeSourcePaths(
|
|
224
|
+
typeof baseOptions.transform === 'function'
|
|
225
|
+
? baseOptions.transform(content, sourcePath)
|
|
226
|
+
: content,
|
|
227
|
+
{ runtimeRoot: '.claude', skillsRoot: '.claude/skills' }
|
|
228
|
+
)
|
|
229
|
+
};
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -305,11 +315,12 @@ function buildContext(argv, runId) {
|
|
|
305
315
|
const options = parseInstallerArgs(argv);
|
|
306
316
|
// Interactive only on a real TTY and not when --yes/CI forces non-interactive.
|
|
307
317
|
const interactive = isInteractive() && !options.yes;
|
|
308
|
-
// Language: --lang wins; otherwise English
|
|
318
|
+
// Language: --lang wins; otherwise English for installer UI only (an interactive
|
|
319
|
+
// prompt or saved runtime locale may change both values).
|
|
309
320
|
const lang = options.lang ? resolveLang(options.lang) : 'en';
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
const locale = options.lang
|
|
321
|
+
// A missing locale means "follow the user's language". Never persist the UI
|
|
322
|
+
// fallback as an AI response-language override on fresh installs/upgrades.
|
|
323
|
+
const locale = options.lang ? options.lang : null;
|
|
313
324
|
return {
|
|
314
325
|
argv,
|
|
315
326
|
runId,
|
package/bin/lib/copy-utils.js
CHANGED
|
@@ -11,6 +11,33 @@ function isTextAsset(filePath) {
|
|
|
11
11
|
return TEXT_REWRITE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Generated artifacts never ship as runtime payload: the coverage db, the
|
|
15
|
+
// Python bytecode cache directory, and bytecode files. Skipping them here keeps
|
|
16
|
+
// both copyRecursive and copyManagedTree (and the OpenCode plugin copy path
|
|
17
|
+
// built on copyRecursive) from ever copying build residue.
|
|
18
|
+
function isGeneratedArtifact(name) {
|
|
19
|
+
const base = String(name || '');
|
|
20
|
+
return (
|
|
21
|
+
base === '.coverage'
|
|
22
|
+
|| base === '__pycache__'
|
|
23
|
+
|| base.endsWith('.pyc')
|
|
24
|
+
|| base.endsWith('.pyo')
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeSourcePaths(content, options = {}) {
|
|
29
|
+
const runtimeRoot = String(options.runtimeRoot || '.claude');
|
|
30
|
+
const skillsRoot = String(options.skillsRoot || `${runtimeRoot}/skills`);
|
|
31
|
+
const runtimeRootWindows = runtimeRoot.replace(/\//g, '\\');
|
|
32
|
+
const skillsRootWindows = skillsRoot.replace(/\//g, '\\');
|
|
33
|
+
|
|
34
|
+
return String(content)
|
|
35
|
+
.replace(/packages\/spec\/src\/claude\/skills(?=\/|$)/g, skillsRoot)
|
|
36
|
+
.replace(/packages\\spec\\src\\claude\\skills(?=\\|$)/g, skillsRootWindows)
|
|
37
|
+
.replace(/packages\/spec\/src\/claude(?=\/|$)/g, runtimeRoot)
|
|
38
|
+
.replace(/packages\\spec\\src\\claude(?=\\|$)/g, runtimeRootWindows);
|
|
39
|
+
}
|
|
40
|
+
|
|
14
41
|
function copyRecursive(src, dest, options = {}) {
|
|
15
42
|
const exists = fs.existsSync(src);
|
|
16
43
|
const stats = exists && fs.statSync(src);
|
|
@@ -22,10 +49,12 @@ function copyRecursive(src, dest, options = {}) {
|
|
|
22
49
|
fs.mkdirSync(dest, { recursive: true });
|
|
23
50
|
}
|
|
24
51
|
fs.readdirSync(src).forEach(childItemName => {
|
|
52
|
+
if (isGeneratedArtifact(childItemName)) return;
|
|
25
53
|
const destItemName = childItemName === 'gitignore' ? '.gitignore' : childItemName;
|
|
26
54
|
copyRecursive(path.join(src, childItemName), path.join(dest, destItemName), options);
|
|
27
55
|
});
|
|
28
56
|
} else {
|
|
57
|
+
if (isGeneratedArtifact(path.basename(src))) return;
|
|
29
58
|
if (fs.existsSync(dest) && !shouldOverwriteManagedFiles) {
|
|
30
59
|
return;
|
|
31
60
|
}
|
|
@@ -56,6 +85,8 @@ function readJsonFile(filePath) {
|
|
|
56
85
|
module.exports = {
|
|
57
86
|
TEXT_REWRITE_EXTENSIONS,
|
|
58
87
|
isTextAsset,
|
|
88
|
+
isGeneratedArtifact,
|
|
89
|
+
normalizeSourcePaths,
|
|
59
90
|
copyRecursive,
|
|
60
91
|
readJsonFile
|
|
61
92
|
};
|
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)',
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const CORE_BLOCK_START = '<!-- CAFEKIT CORE START -->';
|
|
4
|
+
const CORE_BLOCK_END = '<!-- CAFEKIT CORE END -->';
|
|
5
|
+
|
|
6
|
+
function managedRange(content, startMarker = CORE_BLOCK_START, endMarker = CORE_BLOCK_END) {
|
|
7
|
+
const existing = String(content || '');
|
|
8
|
+
const start = existing.indexOf(startMarker);
|
|
9
|
+
const end = existing.indexOf(endMarker);
|
|
10
|
+
if (start < 0 && end < 0) return null;
|
|
11
|
+
const duplicateStart = existing.indexOf(startMarker, start + startMarker.length);
|
|
12
|
+
const duplicateEnd = existing.indexOf(endMarker, end + endMarker.length);
|
|
13
|
+
if (start < 0 || end <= start || duplicateStart >= 0 || duplicateEnd >= 0) return false;
|
|
14
|
+
return {
|
|
15
|
+
start,
|
|
16
|
+
end: end + endMarker.length,
|
|
17
|
+
bodyStart: start + startMarker.length,
|
|
18
|
+
bodyEnd: end
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function migrateExactLegacyBlock(existingContent, startMarker, endMarker, replacementContent) {
|
|
23
|
+
const existing = String(existingContent || '');
|
|
24
|
+
const range = managedRange(existing, startMarker, endMarker);
|
|
25
|
+
if (!range) return existing;
|
|
26
|
+
const body = existing.slice(range.bodyStart, range.bodyEnd).trim();
|
|
27
|
+
if (body !== String(replacementContent).trim()) return existing;
|
|
28
|
+
const block = `${CORE_BLOCK_START}\n${String(replacementContent).trim()}\n${CORE_BLOCK_END}`;
|
|
29
|
+
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function upsertManagedCoreBlock(existingContent, coreContent) {
|
|
33
|
+
const existing = String(existingContent || '');
|
|
34
|
+
const block = `${CORE_BLOCK_START}\n${String(coreContent).trim()}\n${CORE_BLOCK_END}`;
|
|
35
|
+
const range = managedRange(existing);
|
|
36
|
+
if (range === false) return existing;
|
|
37
|
+
if (range) {
|
|
38
|
+
return `${existing.slice(0, range.start)}${block}${existing.slice(range.end)}`;
|
|
39
|
+
}
|
|
40
|
+
if (!existing) return `${block}\n`;
|
|
41
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
42
|
+
return `${existing}${separator}${block}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function transformManagedCoreContent(content, transform) {
|
|
46
|
+
const existing = String(content || '');
|
|
47
|
+
const range = managedRange(existing);
|
|
48
|
+
if (!range || typeof transform !== 'function') return existing;
|
|
49
|
+
const body = existing.slice(range.bodyStart, range.bodyEnd);
|
|
50
|
+
return `${existing.slice(0, range.bodyStart)}${transform(body)}${existing.slice(range.bodyEnd)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Mirrors the section regex used by post-install configureAddressing so an
|
|
54
|
+
// extracted section round-trips exactly back into the managed body.
|
|
55
|
+
const ADDRESSING_SECTION_RE = /##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?(?=\n##|\n*$)/;
|
|
56
|
+
|
|
57
|
+
/** Extract the exact Addressing section (heading through next `##`) from a body, or null. */
|
|
58
|
+
function extractAddressingSection(body) {
|
|
59
|
+
const match = String(body || '').match(ADDRESSING_SECTION_RE);
|
|
60
|
+
return match ? match[0].replace(/\n+$/, '') : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* When a reinstall template drops the Addressing section, carry the exact one
|
|
65
|
+
* from the existing CafeKit-managed block so the saved address survives for
|
|
66
|
+
* setupAddressing. Only reads the managed block body — never user-owned
|
|
67
|
+
* sections outside the block, never the shared CORE block.
|
|
68
|
+
*/
|
|
69
|
+
function preserveAddressingSection(newBody, existingManagedBody) {
|
|
70
|
+
if (extractAddressingSection(newBody) !== null) return newBody;
|
|
71
|
+
const section = extractAddressingSection(existingManagedBody);
|
|
72
|
+
if (section === null) return newBody;
|
|
73
|
+
return `${String(newBody).trimEnd()}\n\n${section}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
CORE_BLOCK_START,
|
|
78
|
+
CORE_BLOCK_END,
|
|
79
|
+
managedRange,
|
|
80
|
+
migrateExactLegacyBlock,
|
|
81
|
+
upsertManagedCoreBlock,
|
|
82
|
+
transformManagedCoreContent,
|
|
83
|
+
extractAddressingSection,
|
|
84
|
+
preserveAddressingSection
|
|
85
|
+
};
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const fs = require('fs');
|
|
22
22
|
const path = require('path');
|
|
23
|
-
const { isTextAsset } = require('./copy-utils');
|
|
23
|
+
const { isTextAsset, isGeneratedArtifact } = require('./copy-utils');
|
|
24
24
|
const manifest = require('./manifest');
|
|
25
25
|
const { assertNoSymlinkPath } = require('./path-safety');
|
|
26
26
|
|
|
@@ -151,6 +151,9 @@ function copyManagedTree(opts) {
|
|
|
151
151
|
const stats = fs.statSync(src);
|
|
152
152
|
if (stats.isDirectory()) {
|
|
153
153
|
for (const childName of fs.readdirSync(src)) {
|
|
154
|
+
// Generated artifacts (coverage, Python bytecode) are not runtime
|
|
155
|
+
// payload — skip so they never ship.
|
|
156
|
+
if (isGeneratedArtifact(childName)) continue;
|
|
154
157
|
const destName = childName === 'gitignore' ? '.gitignore' : childName;
|
|
155
158
|
const childAgg = copyManagedTree({
|
|
156
159
|
...opts,
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
-
const { copyRecursive, isTextAsset } = require('./copy-utils');
|
|
6
|
+
const { copyRecursive, isTextAsset, isGeneratedArtifact, normalizeSourcePaths } = require('./copy-utils');
|
|
7
|
+
const { preserveAddressingSection } = require('./instruction-blocks');
|
|
7
8
|
|
|
8
9
|
const SPEC_ROOT = path.resolve(__dirname, '..', '..');
|
|
9
10
|
const OPENCODE_FOLDER = '.opencode';
|
|
@@ -48,7 +49,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
48
49
|
{
|
|
49
50
|
fileName: 'develop.md',
|
|
50
51
|
skillName: 'develop',
|
|
51
|
-
agent: '
|
|
52
|
+
agent: 'implementer',
|
|
52
53
|
subtask: true,
|
|
53
54
|
description: 'Implement approved CafeKit spec tasks with scope fidelity.'
|
|
54
55
|
},
|
|
@@ -76,7 +77,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
76
77
|
{
|
|
77
78
|
fileName: 'hotfix.md',
|
|
78
79
|
skillName: 'hotfix',
|
|
79
|
-
agent: '
|
|
80
|
+
agent: 'implementer',
|
|
80
81
|
subtask: true,
|
|
81
82
|
description: 'Apply a scout-first, narrow production hotfix.'
|
|
82
83
|
},
|
|
@@ -113,7 +114,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
113
114
|
{
|
|
114
115
|
fileName: 'backend-development.md',
|
|
115
116
|
skillName: 'backend-development',
|
|
116
|
-
agent: '
|
|
117
|
+
agent: 'implementer',
|
|
117
118
|
subtask: true,
|
|
118
119
|
description: 'Implement backend services, APIs, data layers, and integrations.'
|
|
119
120
|
},
|
|
@@ -148,7 +149,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
148
149
|
{
|
|
149
150
|
fileName: 'frontend-development.md',
|
|
150
151
|
skillName: 'frontend-development',
|
|
151
|
-
agent: '
|
|
152
|
+
agent: 'implementer',
|
|
152
153
|
subtask: true,
|
|
153
154
|
description: 'Build React/TypeScript UI components and frontend features.'
|
|
154
155
|
},
|
|
@@ -162,7 +163,7 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
162
163
|
{
|
|
163
164
|
fileName: 'mobile-development.md',
|
|
164
165
|
skillName: 'mobile-development',
|
|
165
|
-
agent: '
|
|
166
|
+
agent: 'implementer',
|
|
166
167
|
subtask: true,
|
|
167
168
|
description: 'Implement iOS, Android, React Native, or Flutter mobile features.'
|
|
168
169
|
},
|
|
@@ -268,9 +269,12 @@ function formatOpenCodeValue(value) {
|
|
|
268
269
|
// Applied to every text asset copied into .opencode/ so the installed
|
|
269
270
|
// content points at the self-contained OpenCode runtime layout.
|
|
270
271
|
function normalizeOpenCodeBody(content) {
|
|
271
|
-
return content
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
return normalizeSourcePaths(content, {
|
|
273
|
+
runtimeRoot: '.opencode',
|
|
274
|
+
skillsRoot: '.opencode/skills'
|
|
275
|
+
})
|
|
276
|
+
.replace(/(?<!~\/)\.claude\/(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode/$1')
|
|
277
|
+
.replace(/(?<!~\\)\.claude\\(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode\\$1')
|
|
274
278
|
.replace(/\.claude\/runtime\.json/g, '.opencode/runtime.json')
|
|
275
279
|
.replace(/\.claude\/ROUTING\.md/g, '.opencode/ROUTING.md')
|
|
276
280
|
.replace(/\.claude\/\.env/g, '.opencode/.env')
|
|
@@ -409,16 +413,44 @@ function createOpenCodeAgentsContent(sourceContent) {
|
|
|
409
413
|
return adaptedContent.replace('## Core Objective', `${openCodeRuntimeBlock}\n## Core Objective`);
|
|
410
414
|
}
|
|
411
415
|
|
|
416
|
+
function managedOpenCodeRange(content) {
|
|
417
|
+
const existing = String(content || '');
|
|
418
|
+
const startMarker = '<!-- CAFEKIT OPENCODE START -->';
|
|
419
|
+
const endMarker = '<!-- CAFEKIT OPENCODE END -->';
|
|
420
|
+
const start = existing.indexOf(startMarker);
|
|
421
|
+
const end = existing.indexOf(endMarker);
|
|
422
|
+
if (start < 0 && end < 0) return null;
|
|
423
|
+
const duplicateStart = existing.indexOf(startMarker, start + startMarker.length);
|
|
424
|
+
const duplicateEnd = existing.indexOf(endMarker, end + endMarker.length);
|
|
425
|
+
if (start < 0 || end <= start || duplicateStart >= 0 || duplicateEnd >= 0) return false;
|
|
426
|
+
return {
|
|
427
|
+
start,
|
|
428
|
+
end: end + endMarker.length,
|
|
429
|
+
bodyStart: start + startMarker.length,
|
|
430
|
+
bodyEnd: end
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
412
435
|
function upsertCafeKitAgentsBlock(existingContent, blockContent) {
|
|
413
436
|
const start = '<!-- CAFEKIT OPENCODE START -->';
|
|
414
437
|
const end = '<!-- CAFEKIT OPENCODE END -->';
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
438
|
+
let body = String(blockContent).trim();
|
|
439
|
+
// Reinstall replaces the managed block in place. If the new template dropped
|
|
440
|
+
// its Addressing section, carry the exact section over from the existing
|
|
441
|
+
// managed block so the saved address survives for setupAddressing. Only reads
|
|
442
|
+
// the managed body — never user-owned sections outside the block.
|
|
443
|
+
const range = managedOpenCodeRange(existingContent);
|
|
444
|
+
if (range) {
|
|
445
|
+
const existingBody = existingContent.slice(range.bodyStart, range.bodyEnd);
|
|
446
|
+
body = preserveAddressingSection(body, existingBody);
|
|
447
|
+
}
|
|
448
|
+
const managedBlock = `${start}\n${body}\n${end}`;
|
|
449
|
+
if (range === false) return existingContent;
|
|
450
|
+
if (range) {
|
|
451
|
+
return `${existingContent.slice(0, range.start)}${managedBlock}${existingContent.slice(range.end)}`;
|
|
420
452
|
}
|
|
421
|
-
|
|
453
|
+
if (!existingContent) return `${managedBlock}\n`;
|
|
422
454
|
// Pre-managed-block releases wrote the shipped template as the whole file.
|
|
423
455
|
// Wrap only an exact shipped prefix and preserve any user/Codex suffix.
|
|
424
456
|
if (existingContent.startsWith(blockContent)) {
|
|
@@ -431,25 +463,23 @@ function upsertCafeKitAgentsBlock(existingContent, blockContent) {
|
|
|
431
463
|
) {
|
|
432
464
|
return `${managedBlock}${existingContent.slice(trimmedLegacy.length)}`;
|
|
433
465
|
}
|
|
434
|
-
if (!existingContent) return `${managedBlock}\n`;
|
|
435
|
-
|
|
436
466
|
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
437
467
|
return `${existingContent}${separator}${managedBlock}\n`;
|
|
438
468
|
}
|
|
439
469
|
|
|
470
|
+
/** Transform only the CafeKit-owned OpenCode body, preserving surrounding bytes. */
|
|
471
|
+
function transformManagedOpenCodeContent(content, transform) {
|
|
472
|
+
const existing = String(content || '');
|
|
473
|
+
const range = managedOpenCodeRange(existing);
|
|
474
|
+
if (!range || typeof transform !== 'function') return existing;
|
|
475
|
+
const body = existing.slice(range.bodyStart, range.bodyEnd);
|
|
476
|
+
return `${existing.slice(0, range.bodyStart)}${transform(body)}${existing.slice(range.bodyEnd)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
440
479
|
function getOpenCodeAgentsTemplateContent() {
|
|
441
480
|
const openCodeTemplate = path.join(SPEC_ROOT, 'src/opencode/AGENTS.md');
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return normalizeOpenCodeBody(fs.readFileSync(openCodeTemplate, 'utf8'));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const claudeTemplate = path.join(SPEC_ROOT, 'src/claude/CLAUDE.md');
|
|
448
|
-
if (!fs.existsSync(claudeTemplate)) {
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return createOpenCodeAgentsContent(fs.readFileSync(claudeTemplate, 'utf8'));
|
|
481
|
+
if (!fs.existsSync(openCodeTemplate)) return null;
|
|
482
|
+
return normalizeOpenCodeBody(fs.readFileSync(openCodeTemplate, 'utf8'));
|
|
453
483
|
}
|
|
454
484
|
|
|
455
485
|
// Copy OpenCode project instructions to AGENTS.md at the project root.
|
|
@@ -462,6 +492,11 @@ function copyOpenCodeAgentsMdFile(platformKey, results, options = {}) {
|
|
|
462
492
|
if (content) {
|
|
463
493
|
const destinationExists = fs.existsSync(dest);
|
|
464
494
|
const existingContent = destinationExists ? fs.readFileSync(dest, 'utf8') : '';
|
|
495
|
+
if (managedOpenCodeRange(existingContent) === false) {
|
|
496
|
+
console.log(` ⚠ AGENTS.md: malformed CafeKit OPENCODE marker topology; preserved ${dest} without writing`);
|
|
497
|
+
results.errors++;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
465
500
|
const nextContent = upsertCafeKitAgentsBlock(existingContent, content);
|
|
466
501
|
|
|
467
502
|
if (nextContent === existingContent) {
|
|
@@ -537,6 +572,10 @@ function copyOpenCodePlugins(platformKey, results, options = {}) {
|
|
|
537
572
|
|
|
538
573
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
539
574
|
entries.forEach(entry => {
|
|
575
|
+
// Skip generated artifacts before any directory/file handling, matching
|
|
576
|
+
// the filter copyRecursive applies to directory children.
|
|
577
|
+
if (isGeneratedArtifact(entry.name)) return;
|
|
578
|
+
|
|
540
579
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
541
580
|
const destPath = path.join(targetDir, entry.name);
|
|
542
581
|
|
|
@@ -555,8 +594,12 @@ function copyOpenCodePlugins(platformKey, results, options = {}) {
|
|
|
555
594
|
return;
|
|
556
595
|
}
|
|
557
596
|
|
|
558
|
-
|
|
559
|
-
|
|
597
|
+
if (isTextAsset(sourcePath)) {
|
|
598
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
599
|
+
fs.writeFileSync(destPath, normalizeOpenCodeBody(sourceContent), 'utf8');
|
|
600
|
+
} else {
|
|
601
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
602
|
+
}
|
|
560
603
|
|
|
561
604
|
if (destExists) {
|
|
562
605
|
console.log(` ↻ Plugin updated: ${entry.name}`);
|
|
@@ -846,6 +889,8 @@ module.exports = {
|
|
|
846
889
|
convertOpenCodeAgentContent,
|
|
847
890
|
convertOpenCodeCommandContent,
|
|
848
891
|
copyOpenCodeAgentsMdFile,
|
|
892
|
+
managedOpenCodeRange,
|
|
893
|
+
transformManagedOpenCodeContent,
|
|
849
894
|
copyOpenCodeSharedRuntimeFiles,
|
|
850
895
|
copyOpenCodePlugins,
|
|
851
896
|
copyOpenCodeCommandTemplates,
|
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,
|