@haposoft/cafekit 0.13.4 → 0.14.0
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/README.md +2 -23
- package/bin/lib/i18n.js +2 -2
- package/bin/lib/manifest.js +15 -0
- package/bin/lib/opencode-install.js +0 -14
- package/bin/phases/claude-runtime.js +4 -1
- package/bin/phases/claude-settings.js +38 -7
- package/bin/phases/root-config.js +5 -24
- package/package.json +1 -1
- package/src/claude/agents/ui-ux-designer.md +4 -4
- package/src/claude/gitignore +4 -11
- package/src/claude/hooks/__tests__/privacy-block.test.js +79 -0
- package/src/claude/hooks/__tests__/session-writeenv.test.js +68 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +270 -0
- package/src/claude/hooks/privacy-block.cjs +22 -2
- package/src/claude/hooks/session.cjs +5 -1
- package/src/claude/hooks/spec-gate.cjs +173 -0
- package/src/claude/hooks/spec-state.cjs +10 -17
- package/src/claude/hooks/task-scaffold-guard.cjs +4 -2
- package/src/claude/migration-manifest.json +5 -5
- package/src/claude/rules/skill-domain-routing.md +2 -3
- package/src/claude/rules/skill-workflow-routing.md +2 -11
- package/src/claude/rules/state-sync.md +4 -0
- package/src/claude/runtime.json +4 -0
- package/src/claude/scripts/validate-docs.cjs +4 -7
- package/src/claude/scripts/validate-spec-output.cjs +29 -9
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/ai-multimodal/.env.example +11 -13
- package/src/claude/skills/ai-multimodal/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +25 -27
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +23 -27
- package/src/claude/skills/ai-multimodal/references/vision-understanding.md +26 -26
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +1 -1
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +7 -9
- package/src/claude/skills/brainstorm/SKILL.md +1 -2
- package/src/claude/skills/docx/SKILL.md +1 -1
- package/src/claude/skills/frontend-design/SKILL.md +1 -1
- package/src/claude/skills/frontend-design/references/ai-multimodal-overview.md +4 -4
- package/src/claude/skills/frontend-design/references/analysis-techniques.md +3 -3
- package/src/claude/skills/frontend-design/references/asset-generation.md +2 -2
- package/src/claude/skills/frontend-design/references/design-extraction-overview.md +4 -4
- package/src/claude/skills/frontend-design/references/extraction-best-practices.md +3 -3
- package/src/claude/skills/frontend-design/references/technical-accessibility.md +1 -1
- package/src/claude/skills/frontend-design/references/technical-optimization.md +1 -1
- package/src/claude/skills/frontend-design/references/technical-overview.md +2 -2
- package/src/claude/skills/frontend-design/references/technical-workflows.md +6 -6
- package/src/claude/skills/frontend-design/references/visual-analysis-overview.md +3 -3
- package/src/claude/skills/inspect/SKILL.md +3 -3
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +9 -9
- package/src/claude/skills/pdf/SKILL.md +1 -1
- package/src/claude/skills/pptx/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +1 -1
- package/src/claude/skills/xlsx/SKILL.md +1 -1
- package/src/opencode/AGENTS.md +1 -2
- package/src/opencode/plugins/session.ts +2 -2
- package/src/opencode/plugins/spec-state.ts +244 -0
- package/src/opencode/plugins/task-scaffold-guard.ts +161 -0
- package/src/claude/scripts/browser-tool.cjs +0 -138
- package/src/claude/skills/delegate/SKILL.md +0 -124
- package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
- package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
- package/src/claude/skills/generate-graph/LICENSE +0 -21
- package/src/claude/skills/generate-graph/README.md +0 -523
- package/src/claude/skills/generate-graph/SKILL.md +0 -429
- package/src/claude/skills/generate-graph/agentloop-core.svg +0 -101
- package/src/claude/skills/generate-graph/agents/openai.yaml +0 -4
- package/src/claude/skills/generate-graph/assets/samples/sample-style1-flat.png +0 -0
- package/src/claude/skills/generate-graph/assets/samples/sample-style2-dark.png +0 -0
- package/src/claude/skills/generate-graph/assets/samples/sample-style3-blueprint.png +0 -0
- package/src/claude/skills/generate-graph/assets/samples/sample-style4-notion.png +0 -0
- package/src/claude/skills/generate-graph/assets/samples/sample-style5-glass.png +0 -0
- package/src/claude/skills/generate-graph/assets/samples/sample-style6-claude.png +0 -0
- package/src/claude/skills/generate-graph/assets/samples/sample-style7-openai.png +0 -0
- package/src/claude/skills/generate-graph/fixtures/agent-memory-types-style4.json +0 -181
- package/src/claude/skills/generate-graph/fixtures/api-flow-style7.json +0 -40
- package/src/claude/skills/generate-graph/fixtures/mem0-style1.json +0 -297
- package/src/claude/skills/generate-graph/fixtures/microservices-style3.json +0 -64
- package/src/claude/skills/generate-graph/fixtures/multi-agent-style5.json +0 -45
- package/src/claude/skills/generate-graph/fixtures/system-architecture-style6.json +0 -48
- package/src/claude/skills/generate-graph/fixtures/tool-call-style2.json +0 -182
- package/src/claude/skills/generate-graph/package.json +0 -42
- package/src/claude/skills/generate-graph/references/icons.md +0 -281
- package/src/claude/skills/generate-graph/references/style-1-flat-icon.md +0 -108
- package/src/claude/skills/generate-graph/references/style-2-dark-terminal.md +0 -107
- package/src/claude/skills/generate-graph/references/style-3-blueprint.md +0 -113
- package/src/claude/skills/generate-graph/references/style-4-notion-clean.md +0 -94
- package/src/claude/skills/generate-graph/references/style-5-glassmorphism.md +0 -125
- package/src/claude/skills/generate-graph/references/style-6-claude-official.md +0 -209
- package/src/claude/skills/generate-graph/references/style-7-openai.md +0 -215
- package/src/claude/skills/generate-graph/references/style-diagram-matrix.md +0 -135
- package/src/claude/skills/generate-graph/references/svg-layout-best-practices.md +0 -100
- package/src/claude/skills/generate-graph/scripts/generate-diagram.sh +0 -157
- package/src/claude/skills/generate-graph/scripts/generate-from-template.py +0 -1556
- package/src/claude/skills/generate-graph/scripts/test-all-styles.sh +0 -135
- package/src/claude/skills/generate-graph/scripts/validate-svg.sh +0 -292
- package/src/claude/skills/generate-graph/templates/agent-architecture.svg +0 -28
- package/src/claude/skills/generate-graph/templates/architecture.svg +0 -23
- package/src/claude/skills/generate-graph/templates/comparison-matrix.svg +0 -14
- package/src/claude/skills/generate-graph/templates/data-flow.svg +0 -28
- package/src/claude/skills/generate-graph/templates/er-diagram.svg +0 -21
- package/src/claude/skills/generate-graph/templates/flowchart.svg +0 -21
- package/src/claude/skills/generate-graph/templates/sequence.svg +0 -20
- package/src/claude/skills/generate-graph/templates/state-machine.svg +0 -20
- package/src/claude/skills/generate-graph/templates/timeline.svg +0 -19
- package/src/claude/skills/generate-graph/templates/use-case.svg +0 -21
- package/src/claude/skills/impact-analysis/SKILL.md +0 -276
- package/src/claude/skills/impact-analysis/references/change-detection.md +0 -270
- package/src/claude/skills/impact-analysis/references/dependency-scouting.md +0 -338
- package/src/claude/skills/impact-analysis/references/edge-case-identification.md +0 -439
- package/src/claude/skills/impact-analysis/references/industry-techniques.md +0 -695
- package/src/claude/skills/impact-analysis/references/practical-techniques-guide.md +0 -753
- package/src/claude/skills/impact-analysis/references/project-detection.md +0 -704
- package/src/claude/skills/impact-analysis/references/react-native-customization.md +0 -508
- package/src/claude/skills/impact-analysis/references/report-template.md +0 -604
- package/src/claude/skills/impact-analysis/references/test-scenario-generation.md +0 -459
- package/src/claude/skills/impact-analysis/scripts/README.md +0 -476
- package/src/claude/skills/impact-analysis/scripts/ast-analyze.js +0 -403
- package/src/claude/skills/impact-analysis/scripts/calculate-risk.js +0 -475
- package/src/claude/skills/impact-analysis/scripts/find-dependencies.sh +0 -202
- package/src/claude/skills/impact-analysis/scripts/run-analysis.sh +0 -312
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Claude Code-first spec-driven workflow and runtime bundle for AI coding assistants.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://claude.ai/code)
|
|
8
8
|
|
|
@@ -46,26 +46,6 @@ Requirements:
|
|
|
46
46
|
- Node.js 18+
|
|
47
47
|
- Claude Code project with `.claude/`, OpenCode project with `.opencode/` or `opencode.json`, or choose a runtime when prompted
|
|
48
48
|
|
|
49
|
-
## Git ignore policy
|
|
50
|
-
|
|
51
|
-
On install, CafeKit updates the **project-root** `.gitignore` with:
|
|
52
|
-
|
|
53
|
-
```text
|
|
54
|
-
# CafeKit / Ecosystem
|
|
55
|
-
specs/_shared/
|
|
56
|
-
plans/
|
|
57
|
-
!plans/templates/
|
|
58
|
-
.cafekit-backup/
|
|
59
|
-
.cafekit.lock
|
|
60
|
-
.claude/
|
|
61
|
-
.opencode/
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Runtime folders are local — reinstall with `npx @haposoft/cafekit` rather than
|
|
65
|
-
committing them. Inside the runtime, CafeKit also installs `.claude/.gitignore`
|
|
66
|
-
(or `.opencode/.gitignore`) so secrets, skill dependencies, session state, and
|
|
67
|
-
hook logs stay out of git even if someone force-adds the folder.
|
|
68
|
-
|
|
69
49
|
## What Gets Installed
|
|
70
50
|
|
|
71
51
|
Claude Code targets:
|
|
@@ -131,9 +111,8 @@ CafeKit ships many skills, but the main release surface is:
|
|
|
131
111
|
- `/hapo:docs [--init|--update|--summarize|--reconstruct]`: create project docs or reconstruct as-is system documentation from source code
|
|
132
112
|
- `/hapo:test [scope|--full]`: run verification and return a structured verdict
|
|
133
113
|
- `/hapo:code-review [scope|--pending]`: adversarial review focused on correctness, regressions, and security
|
|
134
|
-
- `/hapo:generate-graph <diagram request>`: generate technical SVG/PNG diagrams
|
|
135
114
|
|
|
136
|
-
Common companion skills bundled in this package include `inspect`, `
|
|
115
|
+
Common companion skills bundled in this package include `inspect`, `research`, `ai-multimodal`, `frontend-development`, `backend-development`, and `react-best-practices`.
|
|
137
116
|
|
|
138
117
|
CafeKit uses rule-based skill routing guidance instead of an automatic prompt-scoring hook. See `.claude/rules/skill-workflow-routing.md`, `.claude/rules/skill-domain-routing.md`, or run:
|
|
139
118
|
|
package/bin/lib/i18n.js
CHANGED
|
@@ -366,11 +366,11 @@ const MESSAGES = {
|
|
|
366
366
|
}
|
|
367
367
|
};
|
|
368
368
|
|
|
369
|
-
/** Normalize a language code to a supported one; unknown → '
|
|
369
|
+
/** Normalize a language code to a supported one; unknown → 'en'. */
|
|
370
370
|
function resolveLang(code) {
|
|
371
371
|
if (!code) return 'en';
|
|
372
372
|
const lc = String(code).toLowerCase().slice(0, 2);
|
|
373
|
-
return SUPPORTED.includes(lc) ? lc : '
|
|
373
|
+
return SUPPORTED.includes(lc) ? lc : 'en';
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
/** Build a translator. Falls back to English per-key, then the key itself. */
|
package/bin/lib/manifest.js
CHANGED
|
@@ -105,6 +105,21 @@ function createTracker(platformFolder, version) {
|
|
|
105
105
|
this._pruned.add(key);
|
|
106
106
|
},
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Prune every tracked key under a directory prefix (e.g. "skills/foo/").
|
|
110
|
+
* Marks prior-manifest keys with the same prefix for deletion on write().
|
|
111
|
+
*/
|
|
112
|
+
prunePrefix(relPrefix) {
|
|
113
|
+
const prefix = relPrefix.replace(/\\/g, '/');
|
|
114
|
+
for (const key of Object.keys(files)) {
|
|
115
|
+
if (key.startsWith(prefix)) delete files[key];
|
|
116
|
+
}
|
|
117
|
+
const prior = read(platformFolder);
|
|
118
|
+
for (const key of Object.keys(prior.files || {})) {
|
|
119
|
+
if (key.startsWith(prefix)) this._pruned.add(key);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
108
123
|
/** Recorded hash for a relPath written earlier this run, or null. */
|
|
109
124
|
recorded(rel) {
|
|
110
125
|
const key = rel.replace(/\\/g, '/');
|
|
@@ -94,13 +94,6 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
94
94
|
subtask: true,
|
|
95
95
|
description: 'Inspect a codebase, artifact, or external context before planning.'
|
|
96
96
|
},
|
|
97
|
-
{
|
|
98
|
-
fileName: 'generate-graph.md',
|
|
99
|
-
skillName: 'generate-graph',
|
|
100
|
-
agent: 'god-developer',
|
|
101
|
-
subtask: true,
|
|
102
|
-
description: 'Generate technical diagrams from project context.'
|
|
103
|
-
},
|
|
104
97
|
// Domain skills — wrappers so users can invoke them directly via slash
|
|
105
98
|
// commands. Each delegates to the matching skill under .opencode/skills/.
|
|
106
99
|
{
|
|
@@ -166,13 +159,6 @@ const OPENCODE_COMMAND_TEMPLATES = [
|
|
|
166
159
|
subtask: true,
|
|
167
160
|
description: 'Run git workflows: branch, commit, tag, push, or release prep.'
|
|
168
161
|
},
|
|
169
|
-
{
|
|
170
|
-
fileName: 'impact-analysis.md',
|
|
171
|
-
skillName: 'impact-analysis',
|
|
172
|
-
agent: 'inspector',
|
|
173
|
-
subtask: true,
|
|
174
|
-
description: 'Assess affected files, blast radius, and side effects of a change.'
|
|
175
|
-
},
|
|
176
162
|
{
|
|
177
163
|
fileName: 'mobile-development.md',
|
|
178
164
|
skillName: 'mobile-development',
|
|
@@ -78,11 +78,14 @@ function removeObsoleteClaudeRuntimeFiles(ctx, platformKey) {
|
|
|
78
78
|
const targetPath = path.join(targetBase, relPath);
|
|
79
79
|
if (!fs.existsSync(targetPath)) return;
|
|
80
80
|
|
|
81
|
+
const isDir = fs.statSync(targetPath).isDirectory();
|
|
81
82
|
if (!ctx.dryRun) {
|
|
82
|
-
fs.rmSync(targetPath, { force: true });
|
|
83
|
+
fs.rmSync(targetPath, { force: true, recursive: isDir });
|
|
83
84
|
// Remove the entry from the manifest so it doesn't linger as a zombie.
|
|
84
85
|
if (ctx.trackers && ctx.trackers.claude) {
|
|
85
86
|
ctx.trackers.claude.prune(relPath);
|
|
87
|
+
// Directories: prune every tracked file underneath as well.
|
|
88
|
+
if (isDir) ctx.trackers.claude.prunePrefix(`${relPath}/`);
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
ctx.ui.detail(` ↻ ${ctx.dryRun ? '[dry-run] ' : ''}Removed obsolete runtime: ${relPath}`);
|
|
@@ -85,7 +85,16 @@ function mergeClaudeSettings(ctx, platformKey) {
|
|
|
85
85
|
const managedSettings = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
|
|
86
86
|
let existingSettings = {};
|
|
87
87
|
if (fs.existsSync(targetPath)) {
|
|
88
|
-
|
|
88
|
+
// A malformed user settings.json must not abort the whole install: skip the
|
|
89
|
+
// settings merge (never overwrite the user's file blind) and tell them why.
|
|
90
|
+
try {
|
|
91
|
+
existingSettings = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
ctx.ui.warn(`Settings: ${targetPath} is not valid JSON (${e.message}). ` +
|
|
94
|
+
'Skipping settings merge — fix the file and re-run the installer.');
|
|
95
|
+
ctx.results.errors++;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
const mergedSettings = pruneObsoleteSettingsHooks({ ...existingSettings }, ctx);
|
|
@@ -100,7 +109,10 @@ function mergeClaudeSettings(ctx, platformKey) {
|
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
// hooks:
|
|
112
|
+
// hooks: merge managed hooks per command (not per entry). An entry is keyed
|
|
113
|
+
// by its matcher; commands missing from the matching entry are appended, so
|
|
114
|
+
// a new command added to an existing matcher entry is not swallowed on
|
|
115
|
+
// upgrade (the old dedupe only looked at each entry's FIRST command).
|
|
104
116
|
if (managedSettings.hooks) {
|
|
105
117
|
mergedSettings.hooks = mergedSettings.hooks || {};
|
|
106
118
|
Object.keys(managedSettings.hooks).forEach((eventName) => {
|
|
@@ -108,14 +120,33 @@ function mergeClaudeSettings(ctx, platformKey) {
|
|
|
108
120
|
const existingHooks = mergedSettings.hooks[eventName] || [];
|
|
109
121
|
const mergedHooks = [...existingHooks];
|
|
110
122
|
|
|
123
|
+
// Commands already registered anywhere under this event (any matcher):
|
|
124
|
+
// a user may have moved a hook to another entry — never re-add it.
|
|
125
|
+
const eventCommands = new Set(
|
|
126
|
+
mergedHooks.flatMap((entry) => (entry?.hooks || []).map((h) => h?.command)).filter(Boolean)
|
|
127
|
+
);
|
|
128
|
+
|
|
111
129
|
managedHooks.forEach((managedHook) => {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
130
|
+
const managedMatcher = managedHook.matcher || '';
|
|
131
|
+
const managedCommands = (managedHook.hooks || [])
|
|
132
|
+
.filter((h) => h?.command && !eventCommands.has(h.command));
|
|
133
|
+
if (managedCommands.length === 0) return;
|
|
134
|
+
|
|
135
|
+
const target = mergedHooks.find((entry) =>
|
|
136
|
+
(entry?.matcher || '') === managedMatcher && Array.isArray(entry?.hooks));
|
|
137
|
+
|
|
138
|
+
if (!target) {
|
|
139
|
+
mergedHooks.push({ ...managedHook, hooks: managedCommands });
|
|
140
|
+
managedCommands.forEach((h) => eventCommands.add(h.command));
|
|
117
141
|
ctx.ui.detail(` ✓ ${ctx.dryRun ? '[dry-run] ' : ''}Settings: hook ${eventName} merged`);
|
|
142
|
+
return;
|
|
118
143
|
}
|
|
144
|
+
|
|
145
|
+
managedCommands.forEach((hook) => {
|
|
146
|
+
target.hooks.push(hook);
|
|
147
|
+
eventCommands.add(hook.command);
|
|
148
|
+
});
|
|
149
|
+
ctx.ui.detail(` ✓ ${ctx.dryRun ? '[dry-run] ' : ''}Settings: hook ${eventName} merged (+${managedCommands.length} command${managedCommands.length > 1 ? 's' : ''})`);
|
|
119
150
|
});
|
|
120
151
|
|
|
121
152
|
mergedSettings.hooks[eventName] = mergedHooks;
|
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Phase: root project configuration (.gitignore).
|
|
3
3
|
*
|
|
4
|
-
* Ensures the root .gitignore carries CafeKit-managed patterns
|
|
5
|
-
*
|
|
6
|
-
* - install safety artifacts (backup, lock)
|
|
7
|
-
* - runtime folders (.claude/, .opencode/) — reinstall via
|
|
8
|
-
* `npx @haposoft/cafekit`; do not commit the local payload
|
|
9
|
-
*
|
|
10
|
-
* Layered with the in-folder `.claude/.gitignore` /
|
|
11
|
-
* `.opencode/.gitignore` templates (secrets, skill deps, session
|
|
12
|
-
* state) so force-adds and partial un-ignores stay safe.
|
|
13
|
-
*
|
|
14
|
-
* Honors dry-run.
|
|
4
|
+
* Ensures the root .gitignore carries CafeKit-managed patterns, including the
|
|
5
|
+
* new install backup dir and lock file so they never get committed. Honors dry-run.
|
|
15
6
|
*/
|
|
16
7
|
|
|
17
8
|
const fs = require('fs');
|
|
18
9
|
const path = require('path');
|
|
19
10
|
|
|
20
|
-
/** True if `lines` already carries `pattern` with or without a trailing slash. */
|
|
21
|
-
function hasPattern(lines, pattern) {
|
|
22
|
-
const bare = pattern.replace(/\/$/, '');
|
|
23
|
-
const withSlash = bare + '/';
|
|
24
|
-
return lines.includes(pattern) || lines.includes(bare) || lines.includes(withSlash);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
11
|
function ensureGitignore(ctx) {
|
|
28
12
|
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
29
13
|
const header = '# CafeKit / Ecosystem';
|
|
@@ -32,10 +16,7 @@ function ensureGitignore(ctx) {
|
|
|
32
16
|
'plans/',
|
|
33
17
|
'!plans/templates/',
|
|
34
18
|
'.cafekit-backup/',
|
|
35
|
-
'.cafekit.lock'
|
|
36
|
-
// Local runtime payload — reinstall with npx; keep out of git
|
|
37
|
-
'.claude/',
|
|
38
|
-
'.opencode/'
|
|
19
|
+
'.cafekit.lock'
|
|
39
20
|
];
|
|
40
21
|
const prefix = ctx.dryRun ? '[dry-run] ' : '';
|
|
41
22
|
|
|
@@ -49,7 +30,7 @@ function ensureGitignore(ctx) {
|
|
|
49
30
|
|
|
50
31
|
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
51
32
|
const lines = content.split('\n').map((l) => l.trim());
|
|
52
|
-
const missing = patterns.filter((p) => !
|
|
33
|
+
const missing = patterns.filter((p) => !lines.includes(p));
|
|
53
34
|
|
|
54
35
|
if (missing.length > 0) {
|
|
55
36
|
let newContent = content;
|
|
@@ -65,4 +46,4 @@ function ensureGitignore(ctx) {
|
|
|
65
46
|
}
|
|
66
47
|
}
|
|
67
48
|
|
|
68
|
-
module.exports = { ensureGitignore
|
|
49
|
+
module.exports = { ensureGitignore };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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",
|
|
@@ -25,10 +25,10 @@ You are an award-caliber UI/UX designer. You merge aesthetic excellence with eng
|
|
|
25
25
|
### Phase 1: Research & Trend Scouting
|
|
26
26
|
- Execute explicit searches using the bundled `ui-ux-pro-max` toolkit:
|
|
27
27
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
.claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<product-type>" --domain product
|
|
29
|
+
.claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<style-keywords>" --domain style
|
|
30
|
+
.claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<mood>" --domain typography
|
|
31
|
+
.claude/skills/.venv/bin/python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<industry>" --domain color
|
|
32
32
|
```
|
|
33
33
|
- Study current design trends sourced from Dribbble, Awwwards, Mobbin via the python extractor outputs.
|
|
34
34
|
- Review existing `docs/design-guidelines.md` if it exists.
|
package/src/claude/gitignore
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Also ignored at project root by the installer. This file protects
|
|
3
|
-
# force-adds (`git add -f`) and partial un-ignores of the runtime folder.
|
|
4
|
-
|
|
5
|
-
# Secrets and local credentials
|
|
1
|
+
# Environment and Sandbox credentials within .claude
|
|
6
2
|
.env
|
|
7
3
|
.env.*
|
|
8
4
|
!.env.example
|
|
9
5
|
|
|
10
|
-
#
|
|
6
|
+
# Virtual environments for python scripts (skills)
|
|
11
7
|
skills/.venv/
|
|
12
|
-
skills/**/.venv/
|
|
13
|
-
skills/**/node_modules/
|
|
14
8
|
.venv/
|
|
15
9
|
venv/
|
|
10
|
+
skills/**/node_modules/
|
|
16
11
|
__pycache__/
|
|
17
|
-
**/__pycache__/
|
|
18
12
|
*.pyc
|
|
19
13
|
|
|
20
|
-
#
|
|
14
|
+
# System generated state, caches, and logs
|
|
21
15
|
session-state/
|
|
22
16
|
hooks/.logs/
|
|
23
|
-
plugins/.logs/
|
|
24
17
|
agent-memory/
|
|
25
18
|
settings.bak.json
|
|
26
19
|
.mcp.json
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Behavioral tests for privacy-block.cjs. The hook is run as a real subprocess:
|
|
4
|
+
// a PreToolUse payload is piped to stdin and the exit code is asserted
|
|
5
|
+
// (0 = allow, 2 = block). Covers the symlink-bypass hardening.
|
|
6
|
+
|
|
7
|
+
const { test } = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const HOOK = path.join(__dirname, '..', 'privacy-block.cjs');
|
|
15
|
+
|
|
16
|
+
/** Run the hook with a tool payload; return { code, stderr }. */
|
|
17
|
+
function runHook(payload, cwd) {
|
|
18
|
+
const res = spawnSync(process.execPath, [HOOK], {
|
|
19
|
+
input: JSON.stringify({ ...payload, cwd: cwd || payload.cwd }),
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
});
|
|
22
|
+
return { code: res.status, stderr: res.stderr || '' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function tmpDir() {
|
|
26
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ck-privacy-'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('blocks a direct Read of .env (exit 2)', () => {
|
|
30
|
+
const dir = tmpDir();
|
|
31
|
+
try {
|
|
32
|
+
const env = path.join(dir, '.env');
|
|
33
|
+
fs.writeFileSync(env, 'SECRET=1');
|
|
34
|
+
const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: env } }, dir);
|
|
35
|
+
assert.strictEqual(code, 2);
|
|
36
|
+
} finally {
|
|
37
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('blocks a symlink whose target is .env (regression: symlink bypass)', () => {
|
|
42
|
+
const dir = tmpDir();
|
|
43
|
+
try {
|
|
44
|
+
const env = path.join(dir, '.env');
|
|
45
|
+
fs.writeFileSync(env, 'SECRET=1');
|
|
46
|
+
const link = path.join(dir, 'notes.txt'); // harmless-looking name
|
|
47
|
+
fs.symlinkSync(env, link);
|
|
48
|
+
const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
|
|
49
|
+
assert.strictEqual(code, 2, 'symlink to .env must be blocked, not allowed');
|
|
50
|
+
} finally {
|
|
51
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('allows a normal non-sensitive file (exit 0)', () => {
|
|
56
|
+
const dir = tmpDir();
|
|
57
|
+
try {
|
|
58
|
+
const f = path.join(dir, 'README.md');
|
|
59
|
+
fs.writeFileSync(f, '# hi');
|
|
60
|
+
const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: f } }, dir);
|
|
61
|
+
assert.strictEqual(code, 0);
|
|
62
|
+
} finally {
|
|
63
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('allows a symlink to an exempt .env.example (exemption still wins)', () => {
|
|
68
|
+
const dir = tmpDir();
|
|
69
|
+
try {
|
|
70
|
+
const example = path.join(dir, '.env.example');
|
|
71
|
+
fs.writeFileSync(example, 'SECRET=');
|
|
72
|
+
const link = path.join(dir, 'sample.txt');
|
|
73
|
+
fs.symlinkSync(example, link);
|
|
74
|
+
const { code } = runHook({ tool_name: 'Read', tool_input: { file_path: link } }, dir);
|
|
75
|
+
assert.strictEqual(code, 0, 'symlink to an exempt file must stay allowed');
|
|
76
|
+
} finally {
|
|
77
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Behavioral test for session.cjs writeEnv escaping. A git branch name is an
|
|
4
|
+
// attacker-influenced value that flows into GIT_BRANCH in CLAUDE_ENV_FILE. If
|
|
5
|
+
// the value is not escaped, sourcing that env file expands `$HOME` and executes
|
|
6
|
+
// the `pwd` command substitution. We build a repo whose branch carries both
|
|
7
|
+
// payloads (space-free, since git bans spaces in refs), run the hook, then
|
|
8
|
+
// source the produced env file and assert GIT_BRANCH read back is the exact
|
|
9
|
+
// literal branch name — nothing expanded or executed.
|
|
10
|
+
|
|
11
|
+
const { test } = require('node:test');
|
|
12
|
+
const assert = require('node:assert');
|
|
13
|
+
const { spawnSync } = require('node:child_process');
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
const HOOK = path.join(__dirname, '..', 'session.cjs');
|
|
19
|
+
|
|
20
|
+
function git(args, cwd) {
|
|
21
|
+
const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
22
|
+
if (res.status !== 0) {
|
|
23
|
+
throw new Error(`git ${args.join(' ')} failed: ${res.stderr}`);
|
|
24
|
+
}
|
|
25
|
+
return res.stdout.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('session.cjs escapes shell metacharacters in GIT_BRANCH (no injection when sourced)', () => {
|
|
29
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-session-'));
|
|
30
|
+
const envFile = path.join(dir, 'env.sh');
|
|
31
|
+
try {
|
|
32
|
+
// Branch name carries both a $HOME expansion and a `pwd` command
|
|
33
|
+
// substitution. Both are space-free so git accepts them as a valid ref.
|
|
34
|
+
const payloadBranch = 'evil$HOME-x`pwd`';
|
|
35
|
+
git(['init', '-q'], dir);
|
|
36
|
+
git(['config', 'user.email', 't@t.co'], dir);
|
|
37
|
+
git(['config', 'user.name', 't'], dir);
|
|
38
|
+
git(['commit', '-q', '--allow-empty', '-m', 'init'], dir);
|
|
39
|
+
git(['checkout', '-q', '-b', payloadBranch], dir);
|
|
40
|
+
|
|
41
|
+
// Run the hook exactly as Claude Code would (SessionStart on stdin).
|
|
42
|
+
const res = spawnSync(process.execPath, [HOOK], {
|
|
43
|
+
cwd: dir,
|
|
44
|
+
input: JSON.stringify({ source: 'startup' }),
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
env: { ...process.env, CLAUDE_ENV_FILE: envFile },
|
|
47
|
+
});
|
|
48
|
+
assert.strictEqual(res.status, 0, 'hook must exit 0 (fail-open)');
|
|
49
|
+
assert.ok(fs.existsSync(envFile), 'env file must be written');
|
|
50
|
+
|
|
51
|
+
const envText = fs.readFileSync(envFile, 'utf8');
|
|
52
|
+
const branchLine = envText.split('\n').find((l) => l.startsWith('export GIT_BRANCH='));
|
|
53
|
+
assert.ok(branchLine, 'GIT_BRANCH must be present');
|
|
54
|
+
assert.ok(branchLine.includes('\\$') && branchLine.includes('\\`'), 'both $ and backtick must be escaped');
|
|
55
|
+
|
|
56
|
+
// The real proof: source the env file, then echo the value back. It must
|
|
57
|
+
// equal the literal branch name — no $HOME expansion, no pwd execution.
|
|
58
|
+
const sourced = spawnSync('sh', ['-c', `. "${envFile}" && printf '%s' "$GIT_BRANCH"`], { encoding: 'utf8' });
|
|
59
|
+
assert.strictEqual(sourced.status, 0, 'sourcing env file should succeed');
|
|
60
|
+
assert.strictEqual(
|
|
61
|
+
sourced.stdout,
|
|
62
|
+
payloadBranch,
|
|
63
|
+
'GIT_BRANCH must read back as the exact literal branch (no expansion/execution)',
|
|
64
|
+
);
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|