@haposoft/cafekit 0.11.8 → 0.11.9

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]);
@@ -76,35 +101,37 @@ function pipInstall(skillsDir, requirementsPath, upgrade = false) {
76
101
  return run(venvPython(skillsDir), args).ok;
77
102
  }
78
103
 
79
- /** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. */
80
- function npmInstall(dir, upgrade = false, extraEnv = {}) {
104
+ /** `npm install` (fresh) or `npm update` (existing) in a skill scripts dir. Async — keeps spinner alive. */
105
+ async function npmInstall(dir, upgrade = false, extraEnv = {}) {
81
106
  const args = upgrade ? ['update', '--no-audit', '--no-fund'] : ['install', '--no-audit', '--no-fund'];
82
107
  const env = { ...process.env, ...extraEnv };
83
- return run('npm', args, { cwd: dir, env }).ok;
108
+ const r = await runAsync('npm', args, { cwd: dir, env });
109
+ return r.ok;
84
110
  }
85
111
 
86
112
  /**
87
113
  * Download the Puppeteer Chromium browser for chrome-devtools.
88
114
  * 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.
115
+ * fall back to the bundled @puppeteer/browsers CLI.
92
116
  *
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).
117
+ * Non-interactive (default): async — keeps spinner alive during download.
118
+ * Interactive: sync with stdio inherit — native progress bar renders in TTY.
95
119
  */
96
- function installChromium(scriptsDir, interactive = false) {
97
- const stdioOpt = interactive ? 'inherit' : 'pipe';
120
+ async function installChromium(scriptsDir, interactive = false) {
98
121
  // Absolute path so it resolves regardless of the child process cwd.
99
122
  const installMjs = path.resolve(scriptsDir, 'node_modules', 'puppeteer', 'install.mjs');
100
123
  if (fs.existsSync(installMjs)) {
101
- const opts = { cwd: scriptsDir, stdio: stdioOpt };
102
- if (interactive) opts.encoding = 'utf8';
103
- return run('node', [installMjs], opts).ok;
124
+ if (interactive) {
125
+ return run('node', [installMjs], { cwd: scriptsDir, stdio: 'inherit' }).ok;
126
+ }
127
+ const r = await runAsync('node', [installMjs], { cwd: scriptsDir });
128
+ return r.ok;
129
+ }
130
+ if (interactive) {
131
+ return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir, stdio: 'inherit' }).ok;
104
132
  }
105
- const opts = { cwd: scriptsDir, stdio: stdioOpt };
106
- if (interactive) opts.encoding = 'utf8';
107
- return run('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], opts).ok;
133
+ const r = await runAsync('npx', ['--yes', '@puppeteer/browsers', 'install', 'chrome'], { cwd: scriptsDir });
134
+ return r.ok;
108
135
  }
109
136
 
110
137
  /** Does a skill scripts dir declare a given npm dependency? */
@@ -117,9 +144,10 @@ function pkgHasDep(scriptsDir, name) {
117
144
  }
118
145
  }
119
146
 
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;
147
+ /** Download the Playwright Chromium browser (for the pptx html2pptx workflow). Async — keeps spinner alive. */
148
+ async function installPlaywrightBrowser(scriptsDir) {
149
+ const r = await runAsync('npx', ['--yes', 'playwright', 'install', 'chromium'], { cwd: scriptsDir });
150
+ return r.ok;
123
151
  }
124
152
 
125
153
  /**
@@ -65,7 +65,7 @@ function setupPython(ctx, skillsDir) {
65
65
  }
66
66
 
67
67
  /** Run skill-local npm installs/upgrades + browser binaries for a platform's skills. */
68
- function setupNode(ctx, skillsDir) {
68
+ async function setupNode(ctx, skillsDir) {
69
69
  // Detect system Chrome before npm install — skip ~300MB Chromium download if found
70
70
  const chromeScripts = path.join(skillsDir, 'chrome-devtools', 'scripts');
71
71
  const hasChromePkg = fs.existsSync(path.join(chromeScripts, 'package.json'));
@@ -78,7 +78,7 @@ function setupNode(ctx, skillsDir) {
78
78
  const extraEnv = (skill === 'chrome-devtools' && systemChrome)
79
79
  ? { PUPPETEER_SKIP_DOWNLOAD: 'true' }
80
80
  : {};
81
- const ok = dep.npmInstall(dir, hasNodeModules, extraEnv);
81
+ const ok = await dep.npmInstall(dir, hasNodeModules, extraEnv);
82
82
 
83
83
  if (ok) {
84
84
  ctx.ui.stopSpinner(ctx.t('npmInstalled', { skill }));
@@ -90,7 +90,7 @@ function setupNode(ctx, skillsDir) {
90
90
  // Skills using Playwright (e.g. pptx) also need a browser binary.
91
91
  if (dep.pkgHasDep(dir, 'playwright')) {
92
92
  ctx.ui.startSpinner(ctx.t('playwrightInstalling', { skill }));
93
- const pw = dep.installPlaywrightBrowser(dir);
93
+ const pw = await dep.installPlaywrightBrowser(dir);
94
94
  ctx.ui.stopSpinner(pw ? ctx.t('playwrightReady', { skill }) : ctx.t('playwrightSkipped', { skill }));
95
95
  }
96
96
  }
@@ -102,7 +102,7 @@ function setupNode(ctx, skillsDir) {
102
102
  ctx.ui.info(ctx.t('chromiumSystemChromeFound', { path: systemChrome }));
103
103
  } else {
104
104
  ctx.ui.info(ctx.t('chromiumInstalling'));
105
- const ok = dep.installChromium(chromeScripts, true);
105
+ const ok = await dep.installChromium(chromeScripts, true);
106
106
  ctx.ui.info(ok ? '✓ ' + ctx.t('chromiumReady') : ctx.t('chromiumSkipped'));
107
107
  }
108
108
  }
@@ -128,7 +128,7 @@ async function setupSkillDeps(ctx) {
128
128
 
129
129
  for (const key of ctx.platforms) {
130
130
  setupPython(ctx, PLATFORMS[key].skillsDir);
131
- setupNode(ctx, PLATFORMS[key].skillsDir);
131
+ await setupNode(ctx, PLATFORMS[key].skillsDir);
132
132
  }
133
133
 
134
134
  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.9",
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",