@haposoft/cafekit 0.9.7 → 0.10.2

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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Phase: platform detection + selection.
3
+ *
4
+ * Resolves which platforms to install for: auto-detect existing folders, or
5
+ * prompt (clack) when interactive. In non-interactive mode it uses detection
6
+ * only and defaults to Claude when nothing is found — never blocks on input.
7
+ * Sets ctx.platforms, or ctx.cancelled if the user backs out.
8
+ */
9
+
10
+ const {
11
+ PLATFORMS,
12
+ detectPlatforms,
13
+ getPlatformKeys,
14
+ warnLegacyClaudeFolder
15
+ } = require('../lib/context');
16
+
17
+ /** clack select: which platform(s) to install for. */
18
+ async function promptPlatformSelection(ctx) {
19
+ const options = [
20
+ ...getPlatformKeys().map((key) => ({
21
+ value: [key],
22
+ label: PLATFORMS[key].name,
23
+ hint: `${PLATFORMS[key].folder}/`
24
+ })),
25
+ { value: getPlatformKeys(), label: 'All platforms' }
26
+ ];
27
+ const r = await ctx.ui.select(
28
+ { message: 'Select which platform(s) to install for', options },
29
+ []
30
+ );
31
+ if (ctx.ui.isCancel(r)) return [];
32
+ return r;
33
+ }
34
+
35
+ /** clack confirm: install for all detected platforms? */
36
+ async function confirmAllDetected(ctx, detected) {
37
+ const names = detected.map((key) => PLATFORMS[key].name).join(', ');
38
+ const r = await ctx.ui.confirm(
39
+ { message: `Install for all detected platforms? (${names})`, initialValue: true },
40
+ true
41
+ );
42
+ if (ctx.ui.isCancel(r)) return false;
43
+ return r;
44
+ }
45
+
46
+ /**
47
+ * Resolve ctx.platforms via detection + prompts. Sets ctx.cancelled on backout.
48
+ */
49
+ async function resolvePlatforms(ctx) {
50
+ let platforms = detectPlatforms();
51
+
52
+ if (platforms.length === 0) {
53
+ if (ctx.interactive) {
54
+ platforms = await promptPlatformSelection(ctx);
55
+ if (platforms.length === 0) {
56
+ ctx.ui.outro('Installation cancelled.');
57
+ ctx.cancelled = true;
58
+ return ctx;
59
+ }
60
+ } else {
61
+ // Non-interactive with nothing detected → sensible default, never block.
62
+ platforms = ['claude'];
63
+ ctx.ui.info('No platform detected; defaulting to Claude Code (.claude/).');
64
+ }
65
+ } else if (platforms.length > 1 && ctx.interactive) {
66
+ const proceed = await confirmAllDetected(ctx, platforms);
67
+ if (!proceed) {
68
+ platforms = await promptPlatformSelection(ctx);
69
+ if (platforms.length === 0) {
70
+ ctx.ui.outro('Installation cancelled.');
71
+ ctx.cancelled = true;
72
+ return ctx;
73
+ }
74
+ }
75
+ }
76
+
77
+ ctx.platforms = platforms;
78
+
79
+ const platformNames = platforms.map((key) => PLATFORMS[key].name).join(', ');
80
+ ctx.ui.info(`Installing for: ${platformNames}`);
81
+ warnLegacyClaudeFolder(platforms);
82
+
83
+ if (ctx.options.forceOverwrite) {
84
+ ctx.ui.info('Mode: force-overwrite (replace user-modified files; backup kept)');
85
+ } else if (ctx.dryRun) {
86
+ ctx.ui.info('Mode: dry-run (preview only, no changes written)');
87
+ } else {
88
+ ctx.ui.info('Mode: install/update (selective; preserves user-modified files)');
89
+ }
90
+
91
+ return ctx;
92
+ }
93
+
94
+ module.exports = { promptPlatformSelection, resolvePlatforms };
@@ -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 };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Phase: final summary report.
3
+ *
4
+ * Renders install stats (incl. selective-merge counters) + next steps through
5
+ * ctx.ui, so it appears as a framed clack note/outro when interactive and as
6
+ * plain text otherwise.
7
+ */
8
+
9
+ const { PLATFORMS, packageJson } = require('../lib/context');
10
+
11
+ function printSummary(ctx) {
12
+ const { results, platforms, options, ui } = ctx;
13
+
14
+ const stats = [
15
+ `Copied: ${results.copied}`,
16
+ `Updated: ${results.updated}`,
17
+ `Unchanged: ${results.unchanged}`,
18
+ `Preserved: ${results.preserved}`,
19
+ `Skills: ${results.installedSkills > 0 ? 'yes' : 'no'}`,
20
+ `CafeKit Version: ${packageJson.version}`,
21
+ results.missingDependencies > 0 ? `Missing: ${results.missingDependencies}` : null,
22
+ results.errors > 0 ? `Errors: ${results.errors}` : null
23
+ ].filter(Boolean).join('\n');
24
+
25
+ ui.note(stats, ctx.dryRun ? 'Dry-run preview' : 'Installation complete');
26
+
27
+ if (results.preserved > 0) {
28
+ const list = results.preservedFiles.slice(0, 8).map((f) => ` ⊘ ${f}`).join('\n');
29
+ const more = results.preservedFiles.length > 8 ? `\n ... and ${results.preservedFiles.length - 8} more` : '';
30
+ ui.warn(`${results.preserved} user-modified file(s) preserved:\n${list}${more}\n`
31
+ + 'Re-run with --force-overwrite to replace them (a backup is kept under .cafekit-backup/).');
32
+ }
33
+
34
+ if (ctx.dryRun) {
35
+ ui.outro('Dry-run only — re-run without --dry-run to apply.');
36
+ return;
37
+ }
38
+
39
+ const lines = [];
40
+ for (const key of platforms) {
41
+ if (key === 'claude') lines.push('Claude: use /hapo:specs <feature-description>');
42
+ else lines.push('OpenCode: ask the agent to start a feature or brainstorm');
43
+ }
44
+ const envHosts = platforms.map((key) => `${PLATFORMS[key].folder}/.env`).join(' and ');
45
+ lines.push(`API keys isolated in ${envHosts}`);
46
+ if (!options.forceOverwrite) {
47
+ lines.push('Use --force-overwrite to refresh user-modified files');
48
+ }
49
+ ui.note(lines.join('\n'), 'Next steps');
50
+
51
+ ui.outro(`Done — docs: https://github.com/haposoft/cafekit`);
52
+ }
53
+
54
+ module.exports = { printSummary };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Phase: write platform version metadata + install manifest.
3
+ *
4
+ * cafekit.json keeps pure version metadata (schemaVersion, version, platform,
5
+ * timestamps, previousVersion). The ownership baseline is written separately by
6
+ * the run's manifest tracker. Both are skipped in dry-run.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { PLATFORMS, INSTALL_COMMAND, packageJson } = require('../lib/context');
12
+ const { readJsonFile } = require('../lib/copy-utils');
13
+
14
+ function writePlatformVersionMetadata(ctx, platformKey) {
15
+ const platform = PLATFORMS[platformKey];
16
+ const targetPath = path.join(platform.folder, 'cafekit.json');
17
+ const targetExists = fs.existsSync(targetPath);
18
+ const existingMetadata = readJsonFile(targetPath);
19
+ const now = new Date().toISOString();
20
+ const previousVersion = typeof existingMetadata.version === 'string'
21
+ ? existingMetadata.version
22
+ : null;
23
+
24
+ const metadata = {
25
+ schemaVersion: 1,
26
+ packageName: packageJson.name,
27
+ version: packageJson.version,
28
+ platform: platform.id,
29
+ platformName: platform.name,
30
+ installedAt: existingMetadata.installedAt || now,
31
+ lastInstalledAt: now,
32
+ installCommand: INSTALL_COMMAND
33
+ };
34
+
35
+ if (previousVersion && previousVersion !== packageJson.version) {
36
+ metadata.previousVersion = previousVersion;
37
+ }
38
+
39
+ if (!ctx.dryRun) {
40
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
41
+ fs.writeFileSync(targetPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
42
+ }
43
+
44
+ const prefix = ctx.dryRun ? '[dry-run] ' : '';
45
+ if (targetExists) {
46
+ ctx.ui.detail(` ↻ ${prefix}Version metadata updated: ${targetPath}`);
47
+ ctx.results.updated++;
48
+ } else {
49
+ ctx.ui.detail(` ✓ ${prefix}Version metadata installed: ${targetPath}`);
50
+ ctx.results.copied++;
51
+ }
52
+
53
+ // Persist the ownership baseline of everything installed this run.
54
+ const tracker = ctx.trackers[platformKey];
55
+ if (tracker && !ctx.dryRun) {
56
+ tracker.write();
57
+ }
58
+ }
59
+
60
+ module.exports = { writePlatformVersionMetadata };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haposoft/cafekit",
3
- "version": "0.9.7",
3
+ "version": "0.10.2",
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",
@@ -46,5 +46,9 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opencode-ai/plugin": "1.15.11"
49
+ },
50
+ "dependencies": {
51
+ "@clack/prompts": "^1.5.0",
52
+ "picocolors": "^1.1.1"
49
53
  }
50
54
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "cafekit-skill-docx",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Node dependencies for the CafeKit docx skill (docx-js authoring).",
6
+ "dependencies": {
7
+ "docx": "^9.0.0"
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ # DOCX skill Python dependencies (installed by `cafekit --with-skills-deps`)
2
+ # Used by the OOXML editing path (document.py / utilities.py).
3
+
4
+ # Secure XML parsing for existing .docx files
5
+ defusedxml>=0.7.1
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ # PPTX skill Python dependencies (installed by `cafekit --with-skills-deps`)
2
+
3
+ # Extract text from presentations
4
+ markitdown[pptx]>=0.0.1