@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.
@@ -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). Returns true on success. */
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
- return run(py, ['-m', 'venv', venvDir(skillsDir)]).ok;
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
- return run(venvPython(skillsDir), ['-m', 'pip', 'install', '--upgrade', 'pip', '--prefer-binary']).ok;
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
- return run(venvPython(skillsDir), args).ok;
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
- return run('npm', args, { cwd: dir, env }).ok;
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. `npx puppeteer ...` is
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
- * When `interactive` is true, stdio is inherited so the native progress bar
94
- * renders directly on the user's terminal (TTY-only, no pipe capture).
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
- const opts = { cwd: scriptsDir, stdio: stdioOpt };
102
- if (interactive) opts.encoding = 'utf8';
103
- return run('node', [installMjs], opts).ok;
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 opts = { cwd: scriptsDir, stdio: stdioOpt };
106
- if (interactive) opts.encoding = 'utf8';
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
- return run('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir }).ok;
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
- if (!dep.createVenv(skillsDir, py)) {
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.8",
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",