@haposoft/cafekit 0.11.8 → 0.11.10
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/lib/skill-deps.js +61 -30
- package/bin/phases/skills-setup.js +11 -10
- package/package.json +1 -1
package/bin/lib/skill-deps.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
-
const { spawnSync } = require('child_process');
|
|
16
|
+
const { spawnSync, spawn } = require('child_process');
|
|
17
17
|
|
|
18
18
|
const isWindows = process.platform === 'win32';
|
|
19
19
|
|
|
20
|
-
/** Run a command, return { ok, stdout, stderr, status }. Never throws. */
|
|
20
|
+
/** Run a command synchronously, return { ok, stdout, stderr, status }. Never throws. */
|
|
21
21
|
function run(cmd, args, opts = {}) {
|
|
22
22
|
const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
|
|
23
23
|
return {
|
|
@@ -28,6 +28,31 @@ function run(cmd, args, opts = {}) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Run a command asynchronously — keeps the event loop alive so spinners can animate.
|
|
33
|
+
* Returns a Promise<{ ok, stdout, stderr, status }>. Never rejects.
|
|
34
|
+
*/
|
|
35
|
+
function runAsync(cmd, args, opts = {}) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const child = spawn(cmd, args, { stdio: 'pipe', ...opts });
|
|
38
|
+
let stdout = '';
|
|
39
|
+
let stderr = '';
|
|
40
|
+
if (child.stdout) child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
41
|
+
if (child.stderr) child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
42
|
+
child.on('close', (code) => {
|
|
43
|
+
resolve({
|
|
44
|
+
ok: code === 0,
|
|
45
|
+
status: code,
|
|
46
|
+
stdout,
|
|
47
|
+
stderr
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
child.on('error', (err) => {
|
|
51
|
+
resolve({ ok: false, status: null, stdout, stderr: stderr || err.message });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
/** Is a command available on PATH? */
|
|
32
57
|
function hasCmd(name) {
|
|
33
58
|
const probe = isWindows ? run('where', [name]) : run('which', [name]);
|
|
@@ -57,54 +82,59 @@ function venvValid(skillsDir) {
|
|
|
57
82
|
return fs.existsSync(venvPython(skillsDir));
|
|
58
83
|
}
|
|
59
84
|
|
|
60
|
-
/** Create the venv (idempotent: skips if already valid).
|
|
61
|
-
function createVenv(skillsDir, py) {
|
|
85
|
+
/** Create the venv (idempotent: skips if already valid). Async — keeps spinner alive. */
|
|
86
|
+
async function createVenv(skillsDir, py) {
|
|
62
87
|
if (venvValid(skillsDir)) return true;
|
|
63
88
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
64
|
-
|
|
89
|
+
const r = await runAsync(py, ['-m', 'venv', venvDir(skillsDir)]);
|
|
90
|
+
return r.ok;
|
|
65
91
|
}
|
|
66
92
|
|
|
67
|
-
/** Upgrade pip inside the venv (best-effort). */
|
|
68
|
-
function pipUpgrade(skillsDir) {
|
|
69
|
-
|
|
93
|
+
/** Upgrade pip inside the venv (best-effort). Async — keeps spinner alive. */
|
|
94
|
+
async function pipUpgrade(skillsDir) {
|
|
95
|
+
const r = await runAsync(venvPython(skillsDir), ['-m', 'pip', 'install', '--upgrade', 'pip', '--prefer-binary']);
|
|
96
|
+
return r.ok;
|
|
70
97
|
}
|
|
71
98
|
|
|
72
|
-
/** Install or upgrade pip requirements in the venv. */
|
|
73
|
-
function pipInstall(skillsDir, requirementsPath, upgrade = false) {
|
|
99
|
+
/** Install or upgrade pip requirements in the venv. Async — keeps spinner alive. */
|
|
100
|
+
async function pipInstall(skillsDir, requirementsPath, upgrade = false) {
|
|
74
101
|
const args = ['-m', 'pip', 'install', '-r', requirementsPath, '--prefer-binary'];
|
|
75
102
|
if (upgrade) args.push('--upgrade');
|
|
76
|
-
|
|
103
|
+
const r = await runAsync(venvPython(skillsDir), args);
|
|
104
|
+
return r.ok;
|
|
77
105
|
}
|
|
78
106
|
|
|
79
|
-
/** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. */
|
|
80
|
-
function npmInstall(dir, upgrade = false, extraEnv = {}) {
|
|
107
|
+
/** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. Async — keeps spinner alive. */
|
|
108
|
+
async function npmInstall(dir, upgrade = false, extraEnv = {}) {
|
|
81
109
|
const args = upgrade ? ['update', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund'];
|
|
82
110
|
const env = { ...process.env, ...extraEnv };
|
|
83
|
-
|
|
111
|
+
const r = await runAsync('npm', args, { cwd: dir, env });
|
|
112
|
+
return r.ok;
|
|
84
113
|
}
|
|
85
114
|
|
|
86
115
|
/**
|
|
87
116
|
* Download the Puppeteer Chromium browser for chrome-devtools.
|
|
88
117
|
* Prefer puppeteer's own install script (cache-aware, same as its postinstall);
|
|
89
|
-
* fall back to the bundled @puppeteer/browsers CLI.
|
|
90
|
-
* avoided — puppeteer ships no `puppeteer` bin under that invocation and `--yes`
|
|
91
|
-
* would refetch from the registry.
|
|
118
|
+
* fall back to the bundled @puppeteer/browsers CLI.
|
|
92
119
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
120
|
+
* Non-interactive (default): async — keeps spinner alive during download.
|
|
121
|
+
* Interactive: sync with stdio inherit — native progress bar renders in TTY.
|
|
95
122
|
*/
|
|
96
|
-
function installChromium(scriptsDir, interactive = false) {
|
|
97
|
-
const stdioOpt = interactive ? 'inherit' : 'pipe';
|
|
123
|
+
async function installChromium(scriptsDir, interactive = false) {
|
|
98
124
|
// Absolute path so it resolves regardless of the child process cwd.
|
|
99
125
|
const installMjs = path.resolve(scriptsDir, 'node_modules', 'puppeteer', 'install.mjs');
|
|
100
126
|
if (fs.existsSync(installMjs)) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
if (interactive) {
|
|
128
|
+
return run('node', [installMjs], { cwd: scriptsDir, stdio: 'inherit' }).ok;
|
|
129
|
+
}
|
|
130
|
+
const r = await runAsync('node', [installMjs], { cwd: scriptsDir });
|
|
131
|
+
return r.ok;
|
|
132
|
+
}
|
|
133
|
+
if (interactive) {
|
|
134
|
+
return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir, stdio: 'inherit' }).ok;
|
|
104
135
|
}
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], opts).ok;
|
|
136
|
+
const r = await runAsync('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir });
|
|
137
|
+
return r.ok;
|
|
108
138
|
}
|
|
109
139
|
|
|
110
140
|
/** Does a skill scripts dir declare a given npm dependency? */
|
|
@@ -117,9 +147,10 @@ function pkgHasDep(scriptsDir, name) {
|
|
|
117
147
|
}
|
|
118
148
|
}
|
|
119
149
|
|
|
120
|
-
/** Download the Playwright Chromium browser (for the pptx html2pptx workflow). */
|
|
121
|
-
function installPlaywrightBrowser(scriptsDir) {
|
|
122
|
-
|
|
150
|
+
/** Download the Playwright Chromium browser (for the pptx html2pptx workflow). Async — keeps spinner alive. */
|
|
151
|
+
async function installPlaywrightBrowser(scriptsDir) {
|
|
152
|
+
const r = await runAsync('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir });
|
|
153
|
+
return r.ok;
|
|
123
154
|
}
|
|
124
155
|
|
|
125
156
|
/**
|
|
@@ -31,7 +31,7 @@ async function shouldRun(ctx) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** Provision or upgrade Python venv + pip requirements for a platform's skills. */
|
|
34
|
-
function setupPython(ctx, skillsDir) {
|
|
34
|
+
async function setupPython(ctx, skillsDir) {
|
|
35
35
|
const reqs = dep.collectRequirements(skillsDir);
|
|
36
36
|
if (reqs.length === 0) return;
|
|
37
37
|
|
|
@@ -43,19 +43,20 @@ function setupPython(ctx, skillsDir) {
|
|
|
43
43
|
|
|
44
44
|
const alreadyExists = dep.venvValid(skillsDir);
|
|
45
45
|
ctx.ui.startSpinner(ctx.t('venvCreating'));
|
|
46
|
-
|
|
46
|
+
const venvOk = await dep.createVenv(skillsDir, py);
|
|
47
|
+
if (!venvOk) {
|
|
47
48
|
ctx.ui.stopSpinner(ctx.t('venvFailed'));
|
|
48
49
|
ctx.ui.warn(ctx.t('venvFailed'));
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
51
|
-
dep.pipUpgrade(skillsDir);
|
|
52
|
+
await dep.pipUpgrade(skillsDir);
|
|
52
53
|
ctx.ui.stopSpinner(`${ctx.t('venvReady')} (${dep.venvDir(skillsDir)})`);
|
|
53
54
|
|
|
54
55
|
// Upgrade packages if venv already existed (update run), install fresh otherwise.
|
|
55
56
|
const upgrade = alreadyExists;
|
|
56
57
|
for (const { skill, file } of reqs) {
|
|
57
58
|
ctx.ui.startSpinner(ctx.t(upgrade ? 'pipInstalling' : 'pipInstalling', { skill }));
|
|
58
|
-
const ok = dep.pipInstall(skillsDir, file, upgrade);
|
|
59
|
+
const ok = await dep.pipInstall(skillsDir, file, upgrade);
|
|
59
60
|
if (ok) {
|
|
60
61
|
ctx.ui.stopSpinner(ctx.t('pipInstalled', { skill }));
|
|
61
62
|
} else {
|
|
@@ -65,7 +66,7 @@ function setupPython(ctx, skillsDir) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/** Run skill-local npm installs/upgrades + browser binaries for a platform's skills. */
|
|
68
|
-
function setupNode(ctx, skillsDir) {
|
|
69
|
+
async function setupNode(ctx, skillsDir) {
|
|
69
70
|
// Detect system Chrome before npm install — skip ~300MB Chromium download if found
|
|
70
71
|
const chromeScripts = path.join(skillsDir, 'chrome-devtools', 'scripts');
|
|
71
72
|
const hasChromePkg = fs.existsSync(path.join(chromeScripts, 'package.json'));
|
|
@@ -78,7 +79,7 @@ function setupNode(ctx, skillsDir) {
|
|
|
78
79
|
const extraEnv = (skill === 'chrome-devtools' && systemChrome)
|
|
79
80
|
? { PUPPETEER_SKIP_DOWNLOAD: 'true' }
|
|
80
81
|
: {};
|
|
81
|
-
const ok = dep.npmInstall(dir, hasNodeModules, extraEnv);
|
|
82
|
+
const ok = await dep.npmInstall(dir, hasNodeModules, extraEnv);
|
|
82
83
|
|
|
83
84
|
if (ok) {
|
|
84
85
|
ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
|
|
@@ -90,7 +91,7 @@ function setupNode(ctx, skillsDir) {
|
|
|
90
91
|
// Skills using Playwright (e.g. pptx) also need a browser binary.
|
|
91
92
|
if (dep.pkgHasDep(dir, 'playwright')) {
|
|
92
93
|
ctx.ui.startSpinner(ctx.t('playwrightInstalling', { skill }));
|
|
93
|
-
const pw = dep.installPlaywrightBrowser(dir);
|
|
94
|
+
const pw = await dep.installPlaywrightBrowser(dir);
|
|
94
95
|
ctx.ui.stopSpinner(pw ? ctx.t('playwrightReady', { skill }) : ctx.t('playwrightSkipped', { skill }));
|
|
95
96
|
}
|
|
96
97
|
}
|
|
@@ -102,7 +103,7 @@ function setupNode(ctx, skillsDir) {
|
|
|
102
103
|
ctx.ui.info(ctx.t('chromiumSystemChromeFound', { path: systemChrome }));
|
|
103
104
|
} else {
|
|
104
105
|
ctx.ui.info(ctx.t('chromiumInstalling'));
|
|
105
|
-
const ok = dep.installChromium(chromeScripts, true);
|
|
106
|
+
const ok = await dep.installChromium(chromeScripts, true);
|
|
106
107
|
ctx.ui.info(ok ? '✓ ' + ctx.t('chromiumReady') : ctx.t('chromiumSkipped'));
|
|
107
108
|
}
|
|
108
109
|
}
|
|
@@ -127,8 +128,8 @@ async function setupSkillDeps(ctx) {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
for (const key of ctx.platforms) {
|
|
130
|
-
setupPython(ctx, PLATFORMS[key].skillsDir);
|
|
131
|
-
setupNode(ctx, PLATFORMS[key].skillsDir);
|
|
131
|
+
await setupPython(ctx, PLATFORMS[key].skillsDir);
|
|
132
|
+
await setupNode(ctx, PLATFORMS[key].skillsDir);
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
guideManual(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.10",
|
|
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",
|