@haposoft/cafekit 0.10.0 → 0.10.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/bin/install.js +3 -1
- package/bin/lib/context.js +4 -1
- package/bin/lib/skill-deps.js +184 -0
- package/bin/phases/post-install.js +17 -69
- package/bin/phases/skills-setup.js +119 -0
- package/package.json +1 -1
- package/src/claude/skills/docx/scripts/package.json +9 -0
- package/src/claude/skills/docx/scripts/requirements.txt +5 -0
- package/src/claude/skills/pdf/scripts/requirements.txt +17 -0
- package/src/claude/skills/pptx/scripts/package.json +14 -0
- package/src/claude/skills/pptx/scripts/requirements.txt +4 -0
package/bin/install.js
CHANGED
|
@@ -34,6 +34,7 @@ const { installOpenCodeRuntime } = require('./phases/opencode-runtime');
|
|
|
34
34
|
const { writePlatformVersionMetadata } = require('./phases/write-metadata');
|
|
35
35
|
const { ensureGitignore } = require('./phases/root-config');
|
|
36
36
|
const { runPostInstall } = require('./phases/post-install');
|
|
37
|
+
const { setupSkillDeps } = require('./phases/skills-setup');
|
|
37
38
|
const { printSummary } = require('./phases/summary');
|
|
38
39
|
|
|
39
40
|
/** Install a single platform: payload + runtime + metadata, under one spinner. */
|
|
@@ -85,7 +86,7 @@ async function main() {
|
|
|
85
86
|
|
|
86
87
|
try {
|
|
87
88
|
ctx = buildContext(process.argv, `${Date.now()}`);
|
|
88
|
-
ctx.ui.intro(`${ctx.ui.pc.bgCyan(ctx.ui.pc.black(' CafeKit '))}
|
|
89
|
+
ctx.ui.intro(`${ctx.ui.pc.bgCyan(ctx.ui.pc.black(' CafeKit '))}Installer v${packageJson.version} · Multi-platform SDD`);
|
|
89
90
|
if (got.reclaimed) ctx.ui.info('Reclaimed a stale install lock from a dead process.');
|
|
90
91
|
|
|
91
92
|
await resolvePlatforms(ctx);
|
|
@@ -108,6 +109,7 @@ async function main() {
|
|
|
108
109
|
|
|
109
110
|
ensureGitignore(ctx);
|
|
110
111
|
await runPostInstall(ctx);
|
|
112
|
+
await setupSkillDeps(ctx);
|
|
111
113
|
printSummary(ctx);
|
|
112
114
|
|
|
113
115
|
if (!ctx.dryRun) backup.prune(3);
|
package/bin/lib/context.js
CHANGED
|
@@ -183,7 +183,8 @@ function parseInstallerArgs(argv) {
|
|
|
183
183
|
const args = {
|
|
184
184
|
forceOverwrite: false,
|
|
185
185
|
dryRun: false,
|
|
186
|
-
yes: false
|
|
186
|
+
yes: false,
|
|
187
|
+
withSkillsDeps: false
|
|
187
188
|
};
|
|
188
189
|
|
|
189
190
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -195,6 +196,8 @@ function parseInstallerArgs(argv) {
|
|
|
195
196
|
args.dryRun = true;
|
|
196
197
|
} else if (arg === '--yes' || arg === '-y') {
|
|
197
198
|
args.yes = true;
|
|
199
|
+
} else if (arg === '--with-skills-deps') {
|
|
200
|
+
args.withSkillsDeps = true;
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
203
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill dependency setup primitives (cross-platform, no sudo).
|
|
3
|
+
*
|
|
4
|
+
* Provides the building blocks the skills-setup phase uses to provision the
|
|
5
|
+
* parts that a plain file-copy install can't: a Python venv + pip requirements,
|
|
6
|
+
* skill-local npm installs, and the Puppeteer Chromium binary. System binaries
|
|
7
|
+
* and global npm tools are NOT auto-installed here — they are detected and the
|
|
8
|
+
* phase prints guidance.
|
|
9
|
+
*
|
|
10
|
+
* All commands run via spawnSync (blocking is fine for a CLI). Detection is
|
|
11
|
+
* silent; install callers decide how to surface progress/failures.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const isWindows = process.platform === 'win32';
|
|
19
|
+
|
|
20
|
+
/** Run a command, return { ok, stdout, stderr, status }. Never throws. */
|
|
21
|
+
function run(cmd, args, opts = {}) {
|
|
22
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
|
|
23
|
+
return {
|
|
24
|
+
ok: !r.error && r.status === 0,
|
|
25
|
+
status: r.status,
|
|
26
|
+
stdout: r.stdout || '',
|
|
27
|
+
stderr: r.stderr || ''
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Is a command available on PATH? */
|
|
32
|
+
function hasCmd(name) {
|
|
33
|
+
const probe = isWindows ? run('where', [name]) : run('which', [name]);
|
|
34
|
+
return probe.ok;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Find a usable Python launcher ('python3' or 'python'), or null. */
|
|
38
|
+
function findPython() {
|
|
39
|
+
for (const cmd of ['python3', 'python']) {
|
|
40
|
+
if (hasCmd(cmd) && run(cmd, ['--version']).ok) return cmd;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function venvDir(skillsDir) {
|
|
46
|
+
return path.join(skillsDir, '.venv');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Path to the venv's python interpreter (OS-specific). */
|
|
50
|
+
function venvPython(skillsDir) {
|
|
51
|
+
return isWindows
|
|
52
|
+
? path.join(venvDir(skillsDir), 'Scripts', 'python.exe')
|
|
53
|
+
: path.join(venvDir(skillsDir), 'bin', 'python3');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function venvValid(skillsDir) {
|
|
57
|
+
return fs.existsSync(venvPython(skillsDir));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Create the venv (idempotent: skips if already valid). Returns true on success. */
|
|
61
|
+
function createVenv(skillsDir, py) {
|
|
62
|
+
if (venvValid(skillsDir)) return true;
|
|
63
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
64
|
+
return run(py, ['-m', 'venv', venvDir(skillsDir)]).ok;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Upgrade pip inside the venv (best-effort). */
|
|
68
|
+
function pipUpgrade(skillsDir) {
|
|
69
|
+
return run(venvPython(skillsDir), ['-m', 'pip', 'install', '--upgrade', 'pip', '--prefer-binary']).ok;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Install a requirements.txt into the venv (wheels first). Returns true on success. */
|
|
73
|
+
function pipInstall(skillsDir, requirementsPath) {
|
|
74
|
+
return run(venvPython(skillsDir), [
|
|
75
|
+
'-m', 'pip', 'install', '-r', requirementsPath, '--prefer-binary'
|
|
76
|
+
]).ok;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `npm install` in a skill scripts dir. Returns true on success. */
|
|
80
|
+
function npmInstall(dir) {
|
|
81
|
+
return run('npm', ['install', '--no-audit', '--no-fund'], { cwd: dir }).ok;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Download the Puppeteer Chromium browser for chrome-devtools.
|
|
86
|
+
* Prefer puppeteer's own install script (cache-aware, same as its postinstall);
|
|
87
|
+
* fall back to the bundled @puppeteer/browsers CLI. `npx puppeteer ...` is
|
|
88
|
+
* avoided — puppeteer ships no `puppeteer` bin under that invocation and `--yes`
|
|
89
|
+
* would refetch from the registry.
|
|
90
|
+
*/
|
|
91
|
+
function installChromium(scriptsDir) {
|
|
92
|
+
// Absolute path so it resolves regardless of the child process cwd.
|
|
93
|
+
const installMjs = path.resolve(scriptsDir, 'node_modules', 'puppeteer', 'install.mjs');
|
|
94
|
+
if (fs.existsSync(installMjs)) {
|
|
95
|
+
return run('node', [installMjs], { cwd: scriptsDir }).ok;
|
|
96
|
+
}
|
|
97
|
+
return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir }).ok;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Does a skill scripts dir declare a given npm dependency? */
|
|
101
|
+
function pkgHasDep(scriptsDir, name) {
|
|
102
|
+
try {
|
|
103
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(scriptsDir, 'package.json'), 'utf8'));
|
|
104
|
+
return Boolean((pkg.dependencies && pkg.dependencies[name]) || (pkg.devDependencies && pkg.devDependencies[name]));
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Download the Playwright Chromium browser (for the pptx html2pptx workflow). */
|
|
111
|
+
function installPlaywrightBrowser(scriptsDir) {
|
|
112
|
+
return run('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir }).ok;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* List installed skills that ship a runnable scripts/requirements.txt
|
|
117
|
+
* (excludes the tests/ requirements). Returns [{ skill, file }].
|
|
118
|
+
*/
|
|
119
|
+
function collectRequirements(skillsDir) {
|
|
120
|
+
const out = [];
|
|
121
|
+
if (!fs.existsSync(skillsDir)) return out;
|
|
122
|
+
for (const skill of fs.readdirSync(skillsDir)) {
|
|
123
|
+
const file = path.join(skillsDir, skill, 'scripts', 'requirements.txt');
|
|
124
|
+
if (fs.existsSync(file)) out.push({ skill, file });
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* List installed skills with a scripts/package.json that still needs installing
|
|
131
|
+
* (no node_modules present). Returns [{ skill, dir }].
|
|
132
|
+
*/
|
|
133
|
+
function collectSkillPackages(skillsDir) {
|
|
134
|
+
const out = [];
|
|
135
|
+
if (!fs.existsSync(skillsDir)) return out;
|
|
136
|
+
for (const skill of fs.readdirSync(skillsDir)) {
|
|
137
|
+
const dir = path.join(skillsDir, skill, 'scripts');
|
|
138
|
+
if (fs.existsSync(path.join(dir, 'package.json')) && !fs.existsSync(path.join(dir, 'node_modules'))) {
|
|
139
|
+
out.push({ skill, dir });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** External binaries some skills shell out to — detected, never auto-installed. */
|
|
146
|
+
const SYSTEM_TOOLS = [
|
|
147
|
+
{ cmd: 'ffmpeg', why: 'ai-multimodal (audio/video)', brew: 'brew install ffmpeg', apt: 'sudo apt install ffmpeg', win: 'winget install ffmpeg' },
|
|
148
|
+
{ cmd: 'pdftotext', why: 'pdf (text extraction, poppler)', brew: 'brew install poppler', apt: 'sudo apt install poppler-utils', win: 'choco install poppler' },
|
|
149
|
+
{ cmd: 'rsvg-convert', why: 'generate-graph (PNG export, librsvg)', brew: 'brew install librsvg', apt: 'sudo apt install librsvg2-bin', win: 'choco install rsvg-convert' },
|
|
150
|
+
{ cmd: 'tesseract', why: 'pdf/ai-multimodal (OCR)', brew: 'brew install tesseract', apt: 'sudo apt install tesseract-ocr', win: 'choco install tesseract' }
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/** Global npm CLIs some skills expect — detected, never auto-installed. */
|
|
154
|
+
const GLOBAL_NPM = [
|
|
155
|
+
{ cmd: 'agent-browser', pkg: 'agent-browser', why: 'agent-browser skill' }
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
/** Per-OS install hint for a SYSTEM_TOOLS entry. */
|
|
159
|
+
function systemHint(tool) {
|
|
160
|
+
if (process.platform === 'darwin') return tool.brew;
|
|
161
|
+
if (process.platform === 'win32') return tool.win;
|
|
162
|
+
return tool.apt;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
isWindows,
|
|
167
|
+
hasCmd,
|
|
168
|
+
findPython,
|
|
169
|
+
venvDir,
|
|
170
|
+
venvPython,
|
|
171
|
+
venvValid,
|
|
172
|
+
createVenv,
|
|
173
|
+
pipUpgrade,
|
|
174
|
+
pipInstall,
|
|
175
|
+
npmInstall,
|
|
176
|
+
installChromium,
|
|
177
|
+
collectRequirements,
|
|
178
|
+
collectSkillPackages,
|
|
179
|
+
pkgHasDep,
|
|
180
|
+
installPlaywrightBrowser,
|
|
181
|
+
SYSTEM_TOOLS,
|
|
182
|
+
GLOBAL_NPM,
|
|
183
|
+
systemHint
|
|
184
|
+
};
|
|
@@ -1,75 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Phase: post-install runtime configuration.
|
|
3
3
|
*
|
|
4
|
-
* OpenCode model
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* OpenCode model + addressing config. Interactive prompts use ctx.ui (clack);
|
|
5
|
+
* in non-interactive/dry-run they are skipped via fallbacks so spawned/CI runs
|
|
6
|
+
* never hang.
|
|
7
|
+
*
|
|
8
|
+
* Note: CafeKit no longer prompts for a Gemini API key. The upstream gemini-cli
|
|
9
|
+
* was removed, and the key now serves only the ai-multimodal skill — which
|
|
10
|
+
* documents it in its own `.env.example`. Users set GEMINI_API_KEY there when
|
|
11
|
+
* (and only if) they use that skill.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
const fs = require('fs');
|
|
11
15
|
const path = require('path');
|
|
12
|
-
const { execSync } = require('child_process');
|
|
13
|
-
const { PLATFORMS } = require('../lib/context');
|
|
14
16
|
const { setupOpenCodeModel } = require('../lib/opencode-install');
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return true;
|
|
20
|
-
} catch {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function installGeminiCLI(ctx) {
|
|
26
|
-
ctx.ui.startSpinner('Installing gemini-cli...');
|
|
27
|
-
try {
|
|
28
|
-
execSync('npm install -g @google/gemini-cli', { stdio: 'ignore' });
|
|
29
|
-
ctx.ui.stopSpinner('gemini-cli installed');
|
|
30
|
-
return true;
|
|
31
|
-
} catch {
|
|
32
|
-
ctx.ui.stopSpinner('Could not install gemini-cli automatically');
|
|
33
|
-
ctx.ui.warn('Run manually: npm install -g @google/gemini-cli');
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function configureGeminiKey(ctx, apiKey, platforms) {
|
|
39
|
-
const envBody = `GEMINI_API_KEY=${apiKey}\nVISUAL_MODEL=gemma-4-31b-it\nSEARCH_MODEL=gemini-2.5-pro\n`;
|
|
40
|
-
const targets = platforms.filter((key) => PLATFORMS[key]).map((key) => PLATFORMS[key].folder);
|
|
41
|
-
if (targets.length === 0) targets.push('.claude');
|
|
42
|
-
|
|
43
|
-
for (const folder of targets) {
|
|
44
|
-
try {
|
|
45
|
-
const targetDir = path.join(process.cwd(), folder);
|
|
46
|
-
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
47
|
-
fs.writeFileSync(path.join(targetDir, '.env'), envBody, { mode: 0o600 });
|
|
48
|
-
ctx.ui.success(`Gemini API key stored in ${folder}/.env (0600)`);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
ctx.ui.error(`Failed to configure Gemini API key: ${error.message}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function setupGemini(ctx) {
|
|
56
|
-
if (checkGeminiCLI()) {
|
|
57
|
-
ctx.ui.info('gemini-cli already installed');
|
|
58
|
-
} else if (ctx.interactive) {
|
|
59
|
-
const yes = await ctx.ui.confirm(
|
|
60
|
-
{ message: 'Install gemini-cli? (enables hapo:inspect ext mode)', initialValue: false },
|
|
61
|
-
false
|
|
62
|
-
);
|
|
63
|
-
if (yes === true) installGeminiCLI(ctx);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const apiKey = await ctx.ui.text(
|
|
67
|
-
{ message: 'Gemini API key (Enter to skip)', placeholder: 'aistudio.google.com/apikey' },
|
|
68
|
-
''
|
|
69
|
-
);
|
|
70
|
-
if (apiKey && typeof apiKey === 'string' && apiKey.trim()) {
|
|
71
|
-
configureGeminiKey(ctx, apiKey.trim(), ctx.platforms);
|
|
72
|
-
}
|
|
18
|
+
/** Human assistant name for the active platform(s). */
|
|
19
|
+
function assistantName(ctx) {
|
|
20
|
+
return ctx.platforms.includes('claude') ? 'Claude Code' : 'OpenCode';
|
|
73
21
|
}
|
|
74
22
|
|
|
75
23
|
function configureAddressing(ctx, userAddress) {
|
|
@@ -79,10 +27,11 @@ function configureAddressing(ctx, userAddress) {
|
|
|
79
27
|
return;
|
|
80
28
|
}
|
|
81
29
|
|
|
30
|
+
const name = assistantName(ctx);
|
|
82
31
|
let content = fs.readFileSync(claudeMdFile, 'utf8');
|
|
83
32
|
const addressingSection = `## Addressing (Context Overflow Indicator)
|
|
84
33
|
|
|
85
|
-
|
|
34
|
+
${name} always addresses the user as "${userAddress}" throughout the conversation. If it stops doing so, it is a sign the context has been compacted/truncated — tell the user to consider \`/clear\`.`;
|
|
86
35
|
|
|
87
36
|
// Idempotent: replace existing section (shared marker) or append.
|
|
88
37
|
const regex = /##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?(?=\n##|\n*$)/;
|
|
@@ -91,14 +40,14 @@ The AI always addresses the user as "${userAddress}" throughout the conversation
|
|
|
91
40
|
: `${content.endsWith('\n') ? content : `${content}\n`}\n${addressingSection}\n`;
|
|
92
41
|
|
|
93
42
|
fs.writeFileSync(claudeMdFile, content, 'utf8');
|
|
94
|
-
ctx.ui.success(
|
|
43
|
+
ctx.ui.success(`${name} will call you "${userAddress}"`);
|
|
95
44
|
}
|
|
96
45
|
|
|
97
46
|
async function setupAddressing(ctx) {
|
|
98
47
|
if (!ctx.platforms.includes('claude')) return;
|
|
99
48
|
|
|
100
49
|
const answer = await ctx.ui.text(
|
|
101
|
-
{ message: 'How should the AI address you?
|
|
50
|
+
{ message: 'How should the AI address you?', placeholder: 'e.g. boss, sir — Enter to skip' },
|
|
102
51
|
''
|
|
103
52
|
);
|
|
104
53
|
if (ctx.ui.isCancel(answer)) return;
|
|
@@ -115,7 +64,7 @@ async function setupAddressing(ctx) {
|
|
|
115
64
|
/** Run the post-install configuration sequence. */
|
|
116
65
|
async function runPostInstall(ctx) {
|
|
117
66
|
if (ctx.dryRun) {
|
|
118
|
-
ctx.ui.info('[dry-run] Skipping OpenCode model /
|
|
67
|
+
ctx.ui.info('[dry-run] Skipping OpenCode model / addressing setup');
|
|
119
68
|
return ctx;
|
|
120
69
|
}
|
|
121
70
|
|
|
@@ -126,7 +75,6 @@ async function runPostInstall(ctx) {
|
|
|
126
75
|
await setupOpenCodeModel(ctx.platforms, ctx.results);
|
|
127
76
|
}
|
|
128
77
|
|
|
129
|
-
await setupGemini(ctx);
|
|
130
78
|
await setupAddressing(ctx);
|
|
131
79
|
|
|
132
80
|
// Re-record CLAUDE.md baseline so the installer-managed file (template +
|
|
@@ -138,4 +86,4 @@ async function runPostInstall(ctx) {
|
|
|
138
86
|
return ctx;
|
|
139
87
|
}
|
|
140
88
|
|
|
141
|
-
module.exports = {
|
|
89
|
+
module.exports = { configureAddressing, runPostInstall };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase: skill dependency setup (opt-in, safe cross-platform).
|
|
3
|
+
*
|
|
4
|
+
* Provisions the parts a file-copy install can't:
|
|
5
|
+
* - Python venv at <skillsDir>/.venv + pip install each skill's requirements
|
|
6
|
+
* - skill-local `npm install` (scripts/package.json without node_modules)
|
|
7
|
+
* - Puppeteer Chromium binary (chrome-devtools)
|
|
8
|
+
*
|
|
9
|
+
* System binaries (ffmpeg, poppler, librsvg, tesseract) and global npm CLIs are
|
|
10
|
+
* NOT auto-installed — they are detected and printed as guidance. All steps are
|
|
11
|
+
* non-fatal: a failure never breaks the install.
|
|
12
|
+
*
|
|
13
|
+
* Runs only when opted in: `--with-skills-deps`, or an interactive confirm.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { PLATFORMS } = require('../lib/context');
|
|
19
|
+
const dep = require('../lib/skill-deps');
|
|
20
|
+
|
|
21
|
+
async function shouldRun(ctx) {
|
|
22
|
+
if (ctx.dryRun) return false;
|
|
23
|
+
if (ctx.options.withSkillsDeps) return true;
|
|
24
|
+
if (!ctx.interactive) return false;
|
|
25
|
+
const yes = await ctx.ui.confirm(
|
|
26
|
+
{ message: 'Install skill dependencies now? (Python venv + pip + Chromium — slower)', initialValue: false },
|
|
27
|
+
false
|
|
28
|
+
);
|
|
29
|
+
return yes === true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Provision Python venv + pip requirements for a platform's skills. */
|
|
33
|
+
function setupPython(ctx, skillsDir) {
|
|
34
|
+
const reqs = dep.collectRequirements(skillsDir);
|
|
35
|
+
if (reqs.length === 0) return;
|
|
36
|
+
|
|
37
|
+
const py = dep.findPython();
|
|
38
|
+
if (!py) {
|
|
39
|
+
ctx.ui.warn('Python 3 not found — skipped venv. Install Python 3, then re-run with --with-skills-deps.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ctx.ui.startSpinner('Creating Python venv...');
|
|
44
|
+
if (!dep.createVenv(skillsDir, py)) {
|
|
45
|
+
ctx.ui.stopSpinner('Could not create Python venv');
|
|
46
|
+
ctx.ui.warn(`venv creation failed at ${dep.venvDir(skillsDir)} — check your Python install.`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
dep.pipUpgrade(skillsDir);
|
|
50
|
+
ctx.ui.stopSpinner(`Python venv ready (${dep.venvDir(skillsDir)})`);
|
|
51
|
+
|
|
52
|
+
for (const { skill, file } of reqs) {
|
|
53
|
+
ctx.ui.startSpinner(`Installing Python deps: ${skill}...`);
|
|
54
|
+
const ok = dep.pipInstall(skillsDir, file);
|
|
55
|
+
ctx.ui.stopSpinner(ok ? `${skill}: Python deps installed` : `${skill}: some Python deps failed`);
|
|
56
|
+
if (!ok) {
|
|
57
|
+
ctx.ui.warn(`pip failed for ${skill}. Retry: "${dep.venvPython(skillsDir)}" -m pip install -r "${file}"`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Run skill-local npm installs + Chromium for a platform's skills. */
|
|
63
|
+
function setupNode(ctx, skillsDir) {
|
|
64
|
+
for (const { skill, dir } of dep.collectSkillPackages(skillsDir)) {
|
|
65
|
+
ctx.ui.startSpinner(`Installing npm deps: ${skill}...`);
|
|
66
|
+
const ok = dep.npmInstall(dir);
|
|
67
|
+
ctx.ui.stopSpinner(ok ? `${skill}: npm deps installed` : `${skill}: npm install failed`);
|
|
68
|
+
if (!ok) {
|
|
69
|
+
ctx.ui.warn(`npm install failed in ${dir} — retry manually.`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Skills using Playwright (e.g. pptx html2pptx) need a browser binary too.
|
|
73
|
+
if (dep.pkgHasDep(dir, 'playwright')) {
|
|
74
|
+
ctx.ui.startSpinner(`Downloading Playwright browser for ${skill}...`);
|
|
75
|
+
const pw = dep.installPlaywrightBrowser(dir);
|
|
76
|
+
ctx.ui.stopSpinner(pw ? `${skill}: Playwright browser ready` : `${skill}: Playwright browser skipped`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const chromeScripts = path.join(skillsDir, 'chrome-devtools', 'scripts');
|
|
81
|
+
if (fs.existsSync(path.join(chromeScripts, 'package.json'))) {
|
|
82
|
+
ctx.ui.startSpinner('Downloading Chromium for chrome-devtools...');
|
|
83
|
+
const ok = dep.installChromium(chromeScripts);
|
|
84
|
+
ctx.ui.stopSpinner(ok ? 'Chromium ready' : 'Chromium download skipped/failed');
|
|
85
|
+
if (!ok) ctx.ui.warn('Chromium not downloaded — chrome-devtools may prompt on first use.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Detect (don't install) system binaries + global npm CLIs and print guidance. */
|
|
90
|
+
function guideManual(ctx) {
|
|
91
|
+
const missingTools = dep.SYSTEM_TOOLS.filter((t) => !dep.hasCmd(t.cmd));
|
|
92
|
+
const missingNpm = dep.GLOBAL_NPM.filter((g) => !dep.hasCmd(g.cmd));
|
|
93
|
+
if (missingTools.length === 0 && missingNpm.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const lines = [];
|
|
96
|
+
for (const t of missingTools) lines.push(`${t.cmd} — ${t.why}\n ${dep.systemHint(t)}`);
|
|
97
|
+
for (const g of missingNpm) lines.push(`${g.cmd} — ${g.why}\n npm install -g ${g.pkg}`);
|
|
98
|
+
ctx.ui.note(lines.join('\n'), 'Optional tools to install manually');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function setupSkillDeps(ctx) {
|
|
102
|
+
if (!(await shouldRun(ctx))) {
|
|
103
|
+
if (!ctx.dryRun) {
|
|
104
|
+
ctx.ui.info('Skill dependencies skipped — run later with: npx @haposoft/cafekit --with-skills-deps');
|
|
105
|
+
}
|
|
106
|
+
return ctx;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const key of ctx.platforms) {
|
|
110
|
+
const skillsDir = PLATFORMS[key].skillsDir;
|
|
111
|
+
setupPython(ctx, skillsDir);
|
|
112
|
+
setupNode(ctx, skillsDir);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
guideManual(ctx);
|
|
116
|
+
return ctx;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { setupSkillDeps };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Claude Code-first spec-driven workflow for AI coding assistants. Bundles CafeKit hapo: skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# PDF skill dependencies (installed by `cafekit --with-skills-deps`)
|
|
2
|
+
# System tools also needed: poppler (pdftotext/pdf2image), tesseract (OCR).
|
|
3
|
+
|
|
4
|
+
# Read / write / split / merge
|
|
5
|
+
pypdf>=4.0.0
|
|
6
|
+
|
|
7
|
+
# Text + table extraction
|
|
8
|
+
pdfplumber>=0.11.0
|
|
9
|
+
pandas>=2.0.0
|
|
10
|
+
|
|
11
|
+
# Create PDFs
|
|
12
|
+
reportlab>=4.0.0
|
|
13
|
+
|
|
14
|
+
# OCR for scanned PDFs (needs system poppler + tesseract)
|
|
15
|
+
pytesseract>=0.3.10
|
|
16
|
+
pdf2image>=1.17.0
|
|
17
|
+
Pillow>=10.0.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cafekit-skill-pptx",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Node dependencies for the CafeKit pptx skill (html2pptx workflow).",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"pptxgenjs": "^3.12.0",
|
|
8
|
+
"playwright": "^1.40.0",
|
|
9
|
+
"sharp": "^0.33.0",
|
|
10
|
+
"react-icons": "^5.0.0",
|
|
11
|
+
"react": "^18.0.0",
|
|
12
|
+
"react-dom": "^18.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|