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 +19 -0
- package/package.json +2 -1
- package/src/commands/update.js +140 -6
- package/src/commands/update.test.js +86 -0
- package/templates/scripts/sync-agents.sh +17 -6
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.
|
|
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
|
},
|
package/src/commands/update.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|