buildwright 0.0.13 → 0.0.15

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 CHANGED
@@ -82,6 +82,25 @@ Then open your AI editor and run:
82
82
  /bw-work "your task"
83
83
  ```
84
84
 
85
+ ### Update
86
+
87
+ ```bash
88
+ npm install -g buildwright@latest
89
+ cd your-project
90
+ buildwright update
91
+ ```
92
+
93
+ `buildwright update` refreshes Buildwright commands, agents, default steering,
94
+ and Buildwright-owned support scripts. It also removes paths from the old
95
+ pre-`/bw-work` model so generated tool configs do not contain both old and new
96
+ workflows.
97
+
98
+ Steering is only touched if Buildwright ships the file. The default
99
+ `philosophy.md` is refreshed in place only when it is unmodified (a known shipped
100
+ version); a customized `philosophy.md` is preserved. Any steering file Buildwright
101
+ does not ship — your `tech.md`, `product.md`, or org-injected docs such as
102
+ `quality-gates.md` — is never deleted or overwritten.
103
+
85
104
  ### From Source
86
105
 
87
106
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildwright",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Lightweight engineering workflow for agent-led development.",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -15,6 +15,7 @@
15
15
  "templates/"
16
16
  ],
17
17
  "scripts": {
18
+ "test": "node --test",
18
19
  "prepack": "node scripts/prepack.js",
19
20
  "postpack": "node scripts/postpack.js"
20
21
  },
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const crypto = require('crypto');
5
6
  const https = require('https');
6
7
  const { execSync } = require('child_process');
7
8
  const { isBuildwrightInstalled } = require('../utils/detect');
@@ -17,6 +18,104 @@ const RESET = '\x1b[0m';
17
18
 
18
19
  const GITHUB_REPO = 'raunakkathuria/buildwright';
19
20
  const UPDATE_DIRS = ['commands', 'agents', 'steering'];
21
+ const SUPPORT_FILES = [
22
+ 'scripts/sync-agents.sh',
23
+ 'scripts/validate-docs.sh',
24
+ 'scripts/validate-skill.sh',
25
+ 'scripts/install-hooks.sh',
26
+ 'scripts/hooks/pre-commit',
27
+ 'scripts/hooks/post-merge',
28
+ 'scripts/hooks/post-checkout',
29
+ ];
30
+ const REMOVED_PATHS = [
31
+ '.buildwright/commands/bw-new-feature.md',
32
+ '.buildwright/commands/bw-quick.md',
33
+ '.buildwright/commands/bw-claw.md',
34
+ '.buildwright/commands/bw-help.md',
35
+ '.buildwright/agents/architect.md',
36
+ '.buildwright/claws',
37
+ '.buildwright/skills',
38
+ '.buildwright/tasks/TEMPLATE.md',
39
+ 'docs/requirements/TEMPLATE.md',
40
+ '.claude/commands/bw-new-feature.md',
41
+ '.claude/commands/bw-quick.md',
42
+ '.claude/commands/bw-claw.md',
43
+ '.claude/commands/bw-help.md',
44
+ '.claude/agents/architect.md',
45
+ '.claude/claws',
46
+ '.claude/tasks',
47
+ '.opencode/commands/bw-new-feature.md',
48
+ '.opencode/commands/bw-quick.md',
49
+ '.opencode/commands/bw-claw.md',
50
+ '.opencode/commands/bw-help.md',
51
+ '.opencode/agents/architect.md',
52
+ '.opencode/claws',
53
+ '.opencode/skills',
54
+ '.cursor/rules/commands/bw-new-feature.mdc',
55
+ '.cursor/rules/commands/bw-quick.mdc',
56
+ '.cursor/rules/commands/bw-claw.mdc',
57
+ '.cursor/rules/commands/bw-help.mdc',
58
+ '.cursor/rules/agents/architect.mdc',
59
+ '.cursor/rules/claws',
60
+ 'skills/bw-new-feature',
61
+ 'skills/bw-quick',
62
+ 'skills/bw-claw',
63
+ 'skills/bw-help',
64
+ ];
65
+
66
+ // Steering files Buildwright ships and may update in place. Keyed by filename,
67
+ // each value is the set of SHA-256 hashes of every version Buildwright has ever
68
+ // shipped for that file. An existing steering file is overwritten on update ONLY
69
+ // when its hash is in this set (i.e. it is an unmodified, previously-shipped
70
+ // copy); a customized file (hash absent) is preserved. Files Buildwright does not
71
+ // ship at all are never touched.
72
+ //
73
+ // RELEASE STEP: whenever a managed steering file changes, append the superseded
74
+ // version's SHA-256 here so unmodified installs keep auto-updating.
75
+ const MANAGED_STEERING_HASHES = {
76
+ 'philosophy.md': new Set([
77
+ '476fe491e139a211d9483942bd60435513813227c589ae0c29ba1e082672757a',
78
+ ]),
79
+ };
80
+
81
+ function sha256(filePath) {
82
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
83
+ }
84
+
85
+ /**
86
+ * Copy shipped steering files into dest. New shipped files are added. An existing
87
+ * file is overwritten only when its content matches a known shipped hash (i.e. the
88
+ * user has not customized it); customized or unmanaged files are preserved. Files
89
+ * not shipped by Buildwright are never touched. Steering is a flat dir of .md files.
90
+ * @param {object} [managedHashes] - filename -> Set of known shipped hashes
91
+ * @returns {{updated: string[], preserved: string[]}}
92
+ */
93
+ function updateSteering(src, dest, managedHashes = MANAGED_STEERING_HASHES) {
94
+ fs.mkdirSync(dest, { recursive: true });
95
+ const updated = [];
96
+ const preserved = [];
97
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
98
+ if (!entry.isFile()) continue;
99
+ const realSrc = fs.realpathSync(path.join(src, entry.name));
100
+ const destPath = path.join(dest, entry.name);
101
+ if (!fs.existsSync(destPath)) {
102
+ fs.copyFileSync(realSrc, destPath);
103
+ updated.push(entry.name);
104
+ continue;
105
+ }
106
+ const known = managedHashes[entry.name];
107
+ const localHash = sha256(destPath);
108
+ if (known && known.has(localHash)) {
109
+ if (sha256(realSrc) !== localHash) {
110
+ fs.copyFileSync(realSrc, destPath);
111
+ updated.push(entry.name);
112
+ }
113
+ } else {
114
+ preserved.push(entry.name);
115
+ }
116
+ }
117
+ return { updated, preserved };
118
+ }
20
119
 
21
120
  /**
22
121
  * Download a URL following redirects. Returns a Buffer.
@@ -73,7 +172,7 @@ async function update() {
73
172
 
74
173
  console.log(`${BOLD}Updating Buildwright in ${cwd}...${RESET}\n`);
75
174
  console.log(`Updating: ${UPDATE_DIRS.map(d => `.buildwright/${d}/`).join(', ')}`);
76
- console.log(`Preserving: project-created steering files such as tech.md and product.md\n`);
175
+ console.log(`Preserving: customized and org-injected steering files (only an unmodified philosophy.md is refreshed)\n`);
77
176
 
78
177
  let tmpDir;
79
178
  try {
@@ -86,8 +185,27 @@ async function update() {
86
185
  throw new Error('Downloaded archive is missing .buildwright/ directory');
87
186
  }
88
187
 
89
- // This version is not backward compatible with the older command model.
90
- // Update only adds current files; users should re-run init for a clean tree.
188
+ const removed = [];
189
+ for (const relativePath of REMOVED_PATHS) {
190
+ const target = path.join(cwd, relativePath);
191
+ if (!fs.existsSync(target)) continue;
192
+ fs.rmSync(target, { recursive: true, force: true });
193
+ removed.push(relativePath);
194
+ }
195
+ for (const relativePath of ['.buildwright/tasks', 'docs/requirements']) {
196
+ const target = path.join(cwd, relativePath);
197
+ if (!fs.existsSync(target)) continue;
198
+ if (fs.statSync(target).isDirectory() && fs.readdirSync(target).length === 0) {
199
+ fs.rmdirSync(target);
200
+ }
201
+ }
202
+ if (removed.length > 0) {
203
+ console.log(` Removed old Buildwright paths:`);
204
+ for (const relativePath of removed) {
205
+ console.log(` - ${relativePath}`);
206
+ }
207
+ }
208
+
91
209
  for (const dir of UPDATE_DIRS) {
92
210
  const src = path.join(srcBuildwright, dir);
93
211
  const dest = path.join(cwd, '.buildwright', dir);
@@ -97,8 +215,24 @@ async function update() {
97
215
  }
98
216
  console.log(` Updating .buildwright/${dir}/`);
99
217
  fs.mkdirSync(dest, { recursive: true });
100
- copyDir(src, dest, { skipExisting: dir === 'steering' });
218
+ if (dir === 'steering') {
219
+ const { preserved } = updateSteering(src, dest);
220
+ if (preserved.length > 0) {
221
+ console.log(` Preserved customized steering files: ${preserved.join(', ')}`);
222
+ }
223
+ } else {
224
+ copyDir(src, dest);
225
+ }
226
+ }
227
+
228
+ for (const file of SUPPORT_FILES) {
229
+ const src = path.join(extractedRoot, file);
230
+ const dest = path.join(cwd, file);
231
+ if (!fs.existsSync(src)) continue;
232
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
233
+ fs.copyFileSync(src, dest);
101
234
  }
235
+ console.log(` Updated Buildwright support scripts`);
102
236
 
103
237
  // Also add CLAUDE.md if it doesn't already exist locally
104
238
  const srcClaude = path.join(extractedRoot, 'CLAUDE.md');
@@ -119,7 +253,7 @@ async function update() {
119
253
 
120
254
  console.log('');
121
255
  console.log(`${GREEN}${BOLD}Update complete!${RESET}`);
122
- console.log('commands, agents, and default steering: new files added. Existing files unchanged.');
256
+ console.log('commands, agents, and default steering updated.');
123
257
  console.log('Your custom files are unchanged.\n');
124
258
 
125
259
  } catch (err) {
@@ -133,4 +267,4 @@ async function update() {
133
267
  }
134
268
  }
135
269
 
136
- module.exports = { update };
270
+ module.exports = { update, updateSteering, REMOVED_PATHS, MANAGED_STEERING_HASHES };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const crypto = require('crypto');
9
+
10
+ const { updateSteering, REMOVED_PATHS, MANAGED_STEERING_HASHES } = require('./update');
11
+
12
+ const sha256 = (s) => crypto.createHash('sha256').update(s).digest('hex');
13
+
14
+ function tmpProject() {
15
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bw-update-test-'));
16
+ const src = path.join(root, 'src');
17
+ const dest = path.join(root, 'dest');
18
+ fs.mkdirSync(src, { recursive: true });
19
+ fs.mkdirSync(dest, { recursive: true });
20
+ return { root, src, dest };
21
+ }
22
+
23
+ test('copies a shipped steering file that is absent locally', () => {
24
+ const { src, dest } = tmpProject();
25
+ fs.writeFileSync(path.join(src, 'philosophy.md'), 'NEW philosophy');
26
+
27
+ const { updated, preserved } = updateSteering(src, dest, {});
28
+
29
+ assert.deepStrictEqual(updated, ['philosophy.md']);
30
+ assert.deepStrictEqual(preserved, []);
31
+ assert.strictEqual(fs.readFileSync(path.join(dest, 'philosophy.md'), 'utf8'), 'NEW philosophy');
32
+ });
33
+
34
+ test('overwrites an unmodified shipped file (hash match) with the latest', () => {
35
+ const { src, dest } = tmpProject();
36
+ const oldContent = 'OLD philosophy';
37
+ fs.writeFileSync(path.join(dest, 'philosophy.md'), oldContent);
38
+ fs.writeFileSync(path.join(src, 'philosophy.md'), 'NEW philosophy');
39
+
40
+ const managed = { 'philosophy.md': new Set([sha256(oldContent)]) };
41
+ const { updated, preserved } = updateSteering(src, dest, managed);
42
+
43
+ assert.deepStrictEqual(updated, ['philosophy.md']);
44
+ assert.deepStrictEqual(preserved, []);
45
+ assert.strictEqual(fs.readFileSync(path.join(dest, 'philosophy.md'), 'utf8'), 'NEW philosophy');
46
+ });
47
+
48
+ test('preserves a customized steering file (hash not in the managed set)', () => {
49
+ const { src, dest } = tmpProject();
50
+ const customContent = 'CUSTOM org philosophy';
51
+ fs.writeFileSync(path.join(dest, 'philosophy.md'), customContent);
52
+ fs.writeFileSync(path.join(src, 'philosophy.md'), 'NEW philosophy');
53
+
54
+ // managed set only knows some other (shipped) hash, not the custom content
55
+ const managed = { 'philosophy.md': new Set([sha256('some shipped version')]) };
56
+ const { updated, preserved } = updateSteering(src, dest, managed);
57
+
58
+ assert.deepStrictEqual(updated, []);
59
+ assert.deepStrictEqual(preserved, ['philosophy.md']);
60
+ assert.strictEqual(fs.readFileSync(path.join(dest, 'philosophy.md'), 'utf8'), customContent);
61
+ });
62
+
63
+ test('never touches an org steering file that Buildwright does not ship', () => {
64
+ const { src, dest } = tmpProject();
65
+ // Buildwright ships only philosophy.md
66
+ fs.writeFileSync(path.join(src, 'philosophy.md'), 'NEW philosophy');
67
+ // org injected its own doc at a colliding-style path
68
+ const orgDoc = 'org quality gates';
69
+ fs.writeFileSync(path.join(dest, 'quality-gates.md'), orgDoc);
70
+
71
+ updateSteering(src, dest, MANAGED_STEERING_HASHES);
72
+
73
+ assert.strictEqual(fs.existsSync(path.join(dest, 'quality-gates.md')), true);
74
+ assert.strictEqual(fs.readFileSync(path.join(dest, 'quality-gates.md'), 'utf8'), orgDoc);
75
+ });
76
+
77
+ test('REMOVED_PATHS no longer deletes org-injected steering docs', () => {
78
+ assert.ok(!REMOVED_PATHS.includes('.buildwright/steering/quality-gates.md'));
79
+ assert.ok(!REMOVED_PATHS.includes('.buildwright/steering/naming-conventions.md'));
80
+ assert.ok(!REMOVED_PATHS.includes('.buildwright/steering/engineering-philosophy.md'));
81
+ });
82
+
83
+ test('REMOVED_PATHS still cleans up non-steering legacy paths', () => {
84
+ assert.ok(REMOVED_PATHS.includes('.buildwright/commands/bw-quick.md'));
85
+ assert.ok(REMOVED_PATHS.includes('.buildwright/agents/architect.md'));
86
+ });
@@ -33,6 +33,17 @@ cd "$ROOT_DIR"
33
33
  # Helpers
34
34
  # ============================================================================
35
35
 
36
+ sed_inplace() {
37
+ local expression="$1"
38
+ local file="$2"
39
+
40
+ if sed --version >/dev/null 2>&1; then
41
+ sed -i -e "$expression" "$file"
42
+ else
43
+ sed -i '' -e "$expression" "$file"
44
+ fi
45
+ }
46
+
36
47
  # sync_dir SRC DST [REWRITE_FROM REWRITE_TO]
37
48
  # Copies directory, optionally rewriting path references in .md files
38
49
  sync_dir() {
@@ -58,9 +69,9 @@ sync_dir() {
58
69
  if [ -n "$rewrite_from" ] && [ -n "$rewrite_to" ]; then
59
70
  # Only rewrite @@.buildwright/ (read instructions) → tool-specific path
60
71
  # Bare .buildwright/ (write/canonical instructions) stays untouched
61
- find "$tmpdir" -name "*.md" -exec sed -i '' \
62
- -e "s|@@${rewrite_from}|${rewrite_to}|g" \
63
- {} + 2>/dev/null || true
72
+ while IFS= read -r file; do
73
+ sed_inplace "s|@@${rewrite_from}|${rewrite_to}|g" "$file"
74
+ done < <(find "$tmpdir" -name "*.md" -type f)
64
75
  fi
65
76
  if ! diff -rq "$tmpdir" "$dst" > /dev/null 2>&1; then
66
77
  echo "OUT OF SYNC: $dst differs from $src"
@@ -74,9 +85,9 @@ sync_dir() {
74
85
  # @@.buildwright/ = "resolve to tool-specific dir" → gets rewritten
75
86
  # Bare .buildwright/ = "canonical path" → stays untouched
76
87
  if [ -n "$rewrite_from" ] && [ -n "$rewrite_to" ]; then
77
- find "$dst" -name "*.md" -exec sed -i '' \
78
- -e "s|@@${rewrite_from}|${rewrite_to}|g" \
79
- {} + 2>/dev/null || true
88
+ while IFS= read -r file; do
89
+ sed_inplace "s|@@${rewrite_from}|${rewrite_to}|g" "$file"
90
+ done < <(find "$dst" -name "*.md" -type f)
80
91
  fi
81
92
  echo " synced $src → $dst"
82
93
  fi