buildcrew 1.8.6 → 1.9.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.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * buildcrew CC hook installer.
3
+ *
4
+ * Registers hook entries in .claude/settings.json that invoke
5
+ * `npx buildcrew-hook <kind>` on each agent/file event. The hook writes
6
+ * a styled banner to the terminal AND appends to events.jsonl so that
7
+ * `npx buildcrew watch` can show a live view in a separate pane.
8
+ *
9
+ * Idempotent — re-install replaces prior buildcrew entries without
10
+ * touching other hooks or permissions in the file.
11
+ */
12
+
13
+ import { promises as fsp } from "node:fs";
14
+ import path from "node:path";
15
+ import os from "node:os";
16
+
17
+ const BUILDCREW_TAG = "buildcrew-hook";
18
+
19
+ export function resolveSettingsPath({ scope, cwd }) {
20
+ if (scope === "global") return path.join(os.homedir(), ".claude", "settings.json");
21
+ return path.join(cwd, ".claude", "settings.json");
22
+ }
23
+
24
+ export function resolvePermissionsPath({ scope, cwd }) {
25
+ if (scope === "global") return path.join(os.homedir(), ".claude", "settings.local.json");
26
+ return path.join(cwd, ".claude", "settings.local.json");
27
+ }
28
+
29
+ export function buildcrewHooks() {
30
+ const cmd = (kind) => `npx buildcrew-hook ${kind}`;
31
+ const mk = (kind, matcher) => ({
32
+ [BUILDCREW_TAG]: true,
33
+ ...(matcher ? { matcher } : {}),
34
+ hooks: [{ type: "command", command: cmd(kind) }],
35
+ });
36
+ return {
37
+ PreToolUse: [mk("pre-agent", "Agent")],
38
+ PostToolUse: [
39
+ mk("post-agent", "Agent"),
40
+ mk("file-written", "Write|Edit|MultiEdit"),
41
+ ],
42
+ UserPromptSubmit: [mk("user-prompt")],
43
+ Stop: [mk("session-end")],
44
+ };
45
+ }
46
+
47
+ export function buildcrewPermissions() {
48
+ return {
49
+ allow: [
50
+ "Agent", "Task", "Read", "Glob", "Grep", "Write", "Edit", "MultiEdit",
51
+ "NotebookEdit", "WebFetch", "WebSearch",
52
+ "Bash(npm *)", "Bash(npx *)", "Bash(node *)",
53
+ "Bash(git status*)", "Bash(git diff*)", "Bash(git log*)",
54
+ "Bash(git branch*)", "Bash(git show*)", "Bash(git add:*)", "Bash(git commit:*)",
55
+ "Bash(ls *)", "Bash(pwd)", "Bash(which *)",
56
+ "Bash(cat *)", "Bash(head *)", "Bash(tail *)",
57
+ "Bash(mkdir *)", "Bash(touch *)", "Bash(echo *)",
58
+ ],
59
+ deny: [
60
+ "Bash(rm -rf *)", "Bash(sudo *)",
61
+ "Bash(git push --force*)", "Bash(git reset --hard*)",
62
+ "Bash(curl * -X POST*)",
63
+ ],
64
+ };
65
+ }
66
+
67
+ export async function install({ scope = "project", cwd = process.cwd(), withPermissions = false } = {}) {
68
+ const settingsPath = resolveSettingsPath({ scope, cwd });
69
+ const { current, existed } = await readJson(settingsPath);
70
+ const next = mergeHooks(current, buildcrewHooks());
71
+
72
+ let permissionsResult = null;
73
+ if (withPermissions) {
74
+ permissionsResult = await installPermissions({ scope, cwd });
75
+ }
76
+
77
+ if (existed) await backup(settingsPath);
78
+ await atomicWrite(settingsPath, JSON.stringify(next, null, 2) + "\n");
79
+ return { action: "installed", settingsPath, existed, permissions: permissionsResult };
80
+ }
81
+
82
+ export async function installPermissions({ scope = "project", cwd = process.cwd() } = {}) {
83
+ const permPath = resolvePermissionsPath({ scope, cwd });
84
+ const { current, existed } = await readJson(permPath);
85
+ const rec = buildcrewPermissions();
86
+
87
+ const nextPerms = current.permissions && typeof current.permissions === "object"
88
+ ? { ...current.permissions } : {};
89
+ nextPerms.allow = mergeUnique(nextPerms.allow, rec.allow);
90
+ nextPerms.deny = mergeUnique(nextPerms.deny, rec.deny);
91
+
92
+ if (existed) await backup(permPath);
93
+ await atomicWrite(permPath, JSON.stringify({ ...current, permissions: nextPerms }, null, 2) + "\n");
94
+ return { action: "installed", permPath, existed };
95
+ }
96
+
97
+ export async function uninstall({ scope = "project", cwd = process.cwd() } = {}) {
98
+ const settingsPath = resolveSettingsPath({ scope, cwd });
99
+ const { current, existed } = await readJson(settingsPath);
100
+ if (!existed) return { action: "noop", reason: "no settings file", settingsPath };
101
+ const next = stripBuildcrewHooks(current);
102
+ await backup(settingsPath);
103
+ await atomicWrite(settingsPath, JSON.stringify(next, null, 2) + "\n");
104
+ return { action: "uninstalled", settingsPath };
105
+ }
106
+
107
+ // ------------ internals ------------
108
+
109
+ async function readJson(p) {
110
+ try {
111
+ const buf = await fsp.readFile(p, "utf8");
112
+ return { current: JSON.parse(buf), existed: true };
113
+ } catch (err) {
114
+ if (err.code === "ENOENT") return { current: {}, existed: false };
115
+ throw new Error(`failed to read ${p}: ${err.message}`);
116
+ }
117
+ }
118
+
119
+ function mergeHooks(current, add) {
120
+ const out = { ...current };
121
+ const existing = out.hooks && typeof out.hooks === "object" ? { ...out.hooks } : {};
122
+ for (const [event, entries] of Object.entries(add)) {
123
+ const prev = Array.isArray(existing[event]) ? existing[event] : [];
124
+ // Remove prior buildcrew entries to stay idempotent
125
+ const filtered = prev.filter((e) => !(e && e[BUILDCREW_TAG]));
126
+ existing[event] = [...filtered, ...entries];
127
+ }
128
+ out.hooks = existing;
129
+ return out;
130
+ }
131
+
132
+ function stripBuildcrewHooks(current) {
133
+ const out = { ...current };
134
+ if (!out.hooks || typeof out.hooks !== "object") return out;
135
+ const cleaned = {};
136
+ for (const [event, entries] of Object.entries(out.hooks)) {
137
+ if (!Array.isArray(entries)) { cleaned[event] = entries; continue; }
138
+ const filtered = entries.filter((e) => !(e && e[BUILDCREW_TAG]));
139
+ if (filtered.length > 0) cleaned[event] = filtered;
140
+ }
141
+ if (Object.keys(cleaned).length === 0) delete out.hooks;
142
+ else out.hooks = cleaned;
143
+ return out;
144
+ }
145
+
146
+ function mergeUnique(existing, additions) {
147
+ const base = Array.isArray(existing) ? existing.slice() : [];
148
+ const seen = new Set(base);
149
+ for (const a of additions) if (!seen.has(a)) { base.push(a); seen.add(a); }
150
+ return base;
151
+ }
152
+
153
+ async function backup(p) {
154
+ try {
155
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
156
+ await fsp.copyFile(p, `${p}.buildcrew-backup-${ts}`);
157
+ } catch { /* best-effort */ }
158
+ }
159
+
160
+ async function atomicWrite(p, content) {
161
+ await fsp.mkdir(path.dirname(p), { recursive: true });
162
+ const tmp = `${p}.tmp-${process.pid}`;
163
+ await fsp.writeFile(tmp, content, "utf8");
164
+ await fsp.rename(tmp, p);
165
+ }
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "buildcrew",
3
- "version": "1.8.6",
3
+ "version": "1.9.0",
4
4
  "description": "15 AI agents for Claude Code — full development lifecycle from product thinking to production monitoring",
5
5
  "homepage": "https://buildcrew-landing.vercel.app",
6
6
  "author": "z1nun",
7
7
  "license": "MIT",
8
8
  "type": "module",
9
9
  "bin": {
10
- "buildcrew": "./bin/setup.js"
10
+ "buildcrew": "./bin/setup.js",
11
+ "buildcrew-hook": "./bin/hook.js",
12
+ "buildcrew-watch": "./bin/watch.js"
11
13
  },
12
14
  "files": [
13
15
  "bin/",
16
+ "lib/",
14
17
  "agents/",
15
18
  "templates/",
16
19
  "README.md",
@@ -34,7 +37,8 @@
34
37
  "url": "https://github.com/z1nun/buildcrew.git"
35
38
  },
36
39
  "scripts": {
37
- "test": "vitest run"
40
+ "test": "vitest run",
41
+ "watch": "node bin/watch.js"
38
42
  },
39
43
  "devDependencies": {
40
44
  "vitest": "^4.1.0"