buildwright 0.0.14 → 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
@@ -95,6 +95,12 @@ and Buildwright-owned support scripts. It also removes paths from the old
95
95
  pre-`/bw-work` model so generated tool configs do not contain both old and new
96
96
  workflows.
97
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
+
98
104
  ### From Source
99
105
 
100
106
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildwright",
3
- "version": "0.0.14",
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');
@@ -35,9 +36,6 @@ const REMOVED_PATHS = [
35
36
  '.buildwright/claws',
36
37
  '.buildwright/skills',
37
38
  '.buildwright/tasks/TEMPLATE.md',
38
- '.buildwright/steering/quality-gates.md',
39
- '.buildwright/steering/naming-conventions.md',
40
- '.buildwright/steering/engineering-philosophy.md',
41
39
  'docs/requirements/TEMPLATE.md',
42
40
  '.claude/commands/bw-new-feature.md',
43
41
  '.claude/commands/bw-quick.md',
@@ -65,6 +63,60 @@ const REMOVED_PATHS = [
65
63
  'skills/bw-help',
66
64
  ];
67
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
+ }
119
+
68
120
  /**
69
121
  * Download a URL following redirects. Returns a Buffer.
70
122
  */
@@ -120,7 +172,7 @@ async function update() {
120
172
 
121
173
  console.log(`${BOLD}Updating Buildwright in ${cwd}...${RESET}\n`);
122
174
  console.log(`Updating: ${UPDATE_DIRS.map(d => `.buildwright/${d}/`).join(', ')}`);
123
- 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`);
124
176
 
125
177
  let tmpDir;
126
178
  try {
@@ -163,7 +215,14 @@ async function update() {
163
215
  }
164
216
  console.log(` Updating .buildwright/${dir}/`);
165
217
  fs.mkdirSync(dest, { recursive: true });
166
- 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
+ }
167
226
  }
168
227
 
169
228
  for (const file of SUPPORT_FILES) {
@@ -208,4 +267,4 @@ async function update() {
208
267
  }
209
268
  }
210
269
 
211
- 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
+ });