roughbook 0.0.0-stage → 0.1.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.
Files changed (144) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +166 -2
  3. package/dist/agents/claude.js +214 -0
  4. package/dist/agents/claude.js.map +1 -0
  5. package/dist/agents/codex.js +179 -0
  6. package/dist/agents/codex.js.map +1 -0
  7. package/dist/agents/command.js +47 -0
  8. package/dist/agents/command.js.map +1 -0
  9. package/dist/agents/index.js +21 -0
  10. package/dist/agents/index.js.map +1 -0
  11. package/dist/agents/process.js +76 -0
  12. package/dist/agents/process.js.map +1 -0
  13. package/dist/agents/session.js +26 -0
  14. package/dist/agents/session.js.map +1 -0
  15. package/dist/agents/types.js +2 -0
  16. package/dist/agents/types.js.map +1 -0
  17. package/dist/api/client.js +138 -0
  18. package/dist/api/client.js.map +1 -0
  19. package/dist/api/errors.js +40 -0
  20. package/dist/api/errors.js.map +1 -0
  21. package/dist/api/outbox.js +105 -0
  22. package/dist/api/outbox.js.map +1 -0
  23. package/dist/api/types.js +4 -0
  24. package/dist/api/types.js.map +1 -0
  25. package/dist/cli.js +10 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/commands/agents.js +39 -0
  28. package/dist/commands/agents.js.map +1 -0
  29. package/dist/commands/context.js +27 -0
  30. package/dist/commands/context.js.map +1 -0
  31. package/dist/commands/done.js +125 -0
  32. package/dist/commands/done.js.map +1 -0
  33. package/dist/commands/index.js +101 -0
  34. package/dist/commands/index.js.map +1 -0
  35. package/dist/commands/init.js +203 -0
  36. package/dist/commands/init.js.map +1 -0
  37. package/dist/commands/login.js +93 -0
  38. package/dist/commands/login.js.map +1 -0
  39. package/dist/commands/logout.js +34 -0
  40. package/dist/commands/logout.js.map +1 -0
  41. package/dist/commands/promote.js +105 -0
  42. package/dist/commands/promote.js.map +1 -0
  43. package/dist/commands/pull.js +45 -0
  44. package/dist/commands/pull.js.map +1 -0
  45. package/dist/commands/revert.js +39 -0
  46. package/dist/commands/revert.js.map +1 -0
  47. package/dist/commands/review.js +73 -0
  48. package/dist/commands/review.js.map +1 -0
  49. package/dist/commands/run.js +147 -0
  50. package/dist/commands/run.js.map +1 -0
  51. package/dist/commands/shared.js +81 -0
  52. package/dist/commands/shared.js.map +1 -0
  53. package/dist/commands/start.js +119 -0
  54. package/dist/commands/start.js.map +1 -0
  55. package/dist/commands/status.js +62 -0
  56. package/dist/commands/status.js.map +1 -0
  57. package/dist/config/credentials.js +17 -0
  58. package/dist/config/credentials.js.map +1 -0
  59. package/dist/config/server.js +29 -0
  60. package/dist/config/server.js.map +1 -0
  61. package/dist/config/settings.js +21 -0
  62. package/dist/config/settings.js.map +1 -0
  63. package/dist/config/userDir.js +18 -0
  64. package/dist/config/userDir.js.map +1 -0
  65. package/dist/fs/json.js +44 -0
  66. package/dist/fs/json.js.map +1 -0
  67. package/dist/fs/lock.js +38 -0
  68. package/dist/fs/lock.js.map +1 -0
  69. package/dist/io.js +26 -0
  70. package/dist/io.js.map +1 -0
  71. package/dist/launch/open.js +48 -0
  72. package/dist/launch/open.js.map +1 -0
  73. package/dist/launch/platform.js +38 -0
  74. package/dist/launch/platform.js.map +1 -0
  75. package/dist/launch/pty.js +61 -0
  76. package/dist/launch/pty.js.map +1 -0
  77. package/dist/launch/terminal.js +194 -0
  78. package/dist/launch/terminal.js.map +1 -0
  79. package/dist/launch/which.js +59 -0
  80. package/dist/launch/which.js.map +1 -0
  81. package/dist/repo/diff.js +26 -0
  82. package/dist/repo/diff.js.map +1 -0
  83. package/dist/repo/framework.js +41 -0
  84. package/dist/repo/framework.js.map +1 -0
  85. package/dist/repo/link.js +28 -0
  86. package/dist/repo/link.js.map +1 -0
  87. package/dist/repo/prep.js +447 -0
  88. package/dist/repo/prep.js.map +1 -0
  89. package/dist/repo/preview.js +144 -0
  90. package/dist/repo/preview.js.map +1 -0
  91. package/dist/repo/root.js +48 -0
  92. package/dist/repo/root.js.map +1 -0
  93. package/dist/repo/scratch.js +111 -0
  94. package/dist/repo/scratch.js.map +1 -0
  95. package/dist/runs/checks.js +160 -0
  96. package/dist/runs/checks.js.map +1 -0
  97. package/dist/runs/lifecycle.js +142 -0
  98. package/dist/runs/lifecycle.js.map +1 -0
  99. package/dist/runs/promote.js +185 -0
  100. package/dist/runs/promote.js.map +1 -0
  101. package/dist/runs/pull.js +182 -0
  102. package/dist/runs/pull.js.map +1 -0
  103. package/dist/runs/reviews.js +34 -0
  104. package/dist/runs/reviews.js.map +1 -0
  105. package/dist/runs/slots.js +96 -0
  106. package/dist/runs/slots.js.map +1 -0
  107. package/dist/runs/snapshot.js +265 -0
  108. package/dist/runs/snapshot.js.map +1 -0
  109. package/dist/runs/store.js +38 -0
  110. package/dist/runs/store.js.map +1 -0
  111. package/dist/runs/types.js +3 -0
  112. package/dist/runs/types.js.map +1 -0
  113. package/dist/runs/watch.js +42 -0
  114. package/dist/runs/watch.js.map +1 -0
  115. package/dist/screenshot/browser.js +163 -0
  116. package/dist/screenshot/browser.js.map +1 -0
  117. package/dist/screenshot/iteration.js +37 -0
  118. package/dist/screenshot/iteration.js.map +1 -0
  119. package/dist/server/app.js +321 -0
  120. package/dist/server/app.js.map +1 -0
  121. package/dist/server/security.js +44 -0
  122. package/dist/server/security.js.map +1 -0
  123. package/dist/server/studio.js +618 -0
  124. package/dist/server/studio.js.map +1 -0
  125. package/dist/server/terminals.js +142 -0
  126. package/dist/server/terminals.js.map +1 -0
  127. package/dist/ui/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  128. package/dist/ui/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  129. package/dist/ui/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  130. package/dist/ui/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  131. package/dist/ui/assets/index-D-TSXfvz.css +1 -0
  132. package/dist/ui/assets/index-HGV8FKwI.js +27 -0
  133. package/dist/ui/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  134. package/dist/ui/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  135. package/dist/ui/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  136. package/dist/ui/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  137. package/dist/ui/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
  138. package/dist/ui/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  139. package/dist/ui/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  140. package/dist/ui/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  141. package/dist/ui/index.html +14 -0
  142. package/dist/version.js +4 -0
  143. package/dist/version.js.map +1 -0
  144. package/package.json +68 -4
@@ -0,0 +1,265 @@
1
+ // Enforcement that does not depend on the agent (decision 20 of the split plan): before a run the
2
+ // studio records every file in the repo by hash, and after it lists what changed outside the
3
+ // run's own folder, the same way whichever agent or editor made the change. Files that had
4
+ // uncommitted changes are copied aside first, so a change can be reverted without losing them;
5
+ // committed ones are restored from git.
6
+ import { execFile } from 'node:child_process';
7
+ import { createHash } from 'node:crypto';
8
+ import fs from 'node:fs';
9
+ import { copyFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { readJson, writeJsonAtomic } from '../fs/json.js';
12
+ import { ROUGH_DIR, STATE_DIR, statePath } from '../repo/root.js';
13
+ import { assertRunId } from './store.js';
14
+ /** Never recorded: the studio's own state and screenshots, and what no repo commits. */
15
+ const SKIPPED = [`${ROUGH_DIR}/${STATE_DIR}/`, `${ROUGH_DIR}/.screenshots/`];
16
+ const SKIPPED_DIRS = new Set([
17
+ 'node_modules',
18
+ '.git',
19
+ '.next',
20
+ '.turbo',
21
+ '.vercel',
22
+ 'dist',
23
+ 'out',
24
+ 'build'
25
+ ]);
26
+ /** Larger files are compared by size and modification time instead of read. */
27
+ const HASH_LIMIT = 20 * 1024 * 1024;
28
+ /** Uncommitted files larger than this, or past the total, are not copied aside. */
29
+ const BACKUP_FILE_LIMIT = 5 * 1024 * 1024;
30
+ const BACKUP_TOTAL_LIMIT = 50 * 1024 * 1024;
31
+ function git(repoRoot, args) {
32
+ return new Promise(resolve => {
33
+ execFile('git', args, {
34
+ cwd: repoRoot,
35
+ windowsHide: true,
36
+ timeout: 60_000,
37
+ maxBuffer: 256 * 1024 * 1024,
38
+ encoding: 'buffer'
39
+ }, (error, stdout) => resolve(error ? null : stdout.toString('utf8')));
40
+ });
41
+ }
42
+ function gitBuffer(repoRoot, args) {
43
+ return new Promise(resolve => {
44
+ execFile('git', args, {
45
+ cwd: repoRoot,
46
+ windowsHide: true,
47
+ timeout: 60_000,
48
+ maxBuffer: 256 * 1024 * 1024,
49
+ encoding: 'buffer'
50
+ }, (error, stdout) => resolve(error ? null : stdout));
51
+ });
52
+ }
53
+ function skipped(file) {
54
+ return SKIPPED.some(prefix => file.startsWith(prefix));
55
+ }
56
+ function walk(repoRoot, dir = '') {
57
+ const absolute = path.join(repoRoot, ...dir.split('/').filter(Boolean));
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(absolute, { withFileTypes: true });
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ return entries.flatMap(entry => {
66
+ const relative = dir ? `${dir}/${entry.name}` : entry.name;
67
+ if (entry.isDirectory())
68
+ return SKIPPED_DIRS.has(entry.name) ? [] : walk(repoRoot, relative);
69
+ return entry.isFile() ? [relative] : [];
70
+ });
71
+ }
72
+ /**
73
+ * The repo's files, repo-relative with forward slashes: what git tracks plus what it would add
74
+ * (so .gitignore is respected), or every file but the usual build folders outside git.
75
+ */
76
+ export async function listRepoFiles(repoRoot) {
77
+ const listed = await git(repoRoot, [
78
+ 'ls-files',
79
+ '-z',
80
+ '--cached',
81
+ '--others',
82
+ '--exclude-standard'
83
+ ]);
84
+ const files = listed === null
85
+ ? walk(repoRoot)
86
+ : [...new Set(listed.split('\0').filter(Boolean))].filter(file => fs.existsSync(path.join(repoRoot, ...file.split('/'))));
87
+ return { files: files.filter(file => !skipped(file)).sort(), git: listed !== null };
88
+ }
89
+ async function hashFile(file) {
90
+ try {
91
+ const info = await stat(file);
92
+ if (info.size > HASH_LIMIT)
93
+ return `size:${info.size}:${info.mtimeMs}`;
94
+ return createHash('sha1')
95
+ .update(await readFile(file))
96
+ .digest('hex');
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ /** Runs `work` over `items`, a few at a time. */
103
+ async function eachLimited(items, work, limit = 32) {
104
+ let next = 0;
105
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
106
+ while (next < items.length)
107
+ await work(items[next++]);
108
+ }));
109
+ }
110
+ /** Every file's hash, now. */
111
+ export async function hashRepo(repoRoot) {
112
+ const listing = await listRepoFiles(repoRoot);
113
+ const files = {};
114
+ await eachLimited(listing.files, async (file) => {
115
+ const hash = await hashFile(path.join(repoRoot, ...file.split('/')));
116
+ if (hash)
117
+ files[file] = hash;
118
+ });
119
+ return { files, git: listing.git };
120
+ }
121
+ /** Files git sees as changed, added or untracked: their content exists nowhere else. */
122
+ async function uncommitted(repoRoot) {
123
+ const status = await git(repoRoot, ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
124
+ if (status === null)
125
+ return [];
126
+ // Porcelain paths start at the top of the git repo, which may be above the app.
127
+ const prefix = ((await git(repoRoot, ['rev-parse', '--show-prefix'])) ?? '').trim();
128
+ const entries = status.split('\0').filter(Boolean);
129
+ const files = [];
130
+ for (let i = 0; i < entries.length; i++) {
131
+ const entry = entries[i];
132
+ const code = entry.slice(0, 2);
133
+ const file = entry.slice(3);
134
+ if (file.startsWith(prefix))
135
+ files.push(file.slice(prefix.length));
136
+ // A rename or copy is followed by its old path.
137
+ if (code.includes('R') || code.includes('C'))
138
+ i++;
139
+ }
140
+ return files.filter(file => !skipped(file));
141
+ }
142
+ function snapshotDir(repoRoot, runId) {
143
+ return statePath(repoRoot, 'snapshots', assertRunId(runId));
144
+ }
145
+ /** Records the repo before a run; one snapshot can serve several runs started together. */
146
+ export async function takeSnapshot(repoRoot, runIds) {
147
+ const { files, git: isGit } = await hashRepo(repoRoot);
148
+ const dirty = isGit ? (await uncommitted(repoRoot)).filter(file => file in files) : [];
149
+ const backedUp = [];
150
+ const notBackedUp = [];
151
+ let total = 0;
152
+ const copies = [];
153
+ for (const file of dirty) {
154
+ const size = (await stat(path.join(repoRoot, ...file.split('/')))).size;
155
+ if (size > BACKUP_FILE_LIMIT || total + size > BACKUP_TOTAL_LIMIT) {
156
+ notBackedUp.push(file);
157
+ continue;
158
+ }
159
+ total += size;
160
+ copies.push(file);
161
+ backedUp.push(file);
162
+ }
163
+ const snapshot = {
164
+ createdAt: new Date().toISOString(),
165
+ git: isGit,
166
+ files,
167
+ backedUp,
168
+ notBackedUp
169
+ };
170
+ for (const runId of runIds) {
171
+ const dir = snapshotDir(repoRoot, runId);
172
+ for (const file of copies) {
173
+ const target = path.join(dir, 'files', ...file.split('/'));
174
+ await mkdir(path.dirname(target), { recursive: true });
175
+ await copyFile(path.join(repoRoot, ...file.split('/')), target);
176
+ }
177
+ await writeJsonAtomic(path.join(dir, 'snapshot.json'), snapshot);
178
+ }
179
+ return snapshot;
180
+ }
181
+ /**
182
+ * Files the studio itself wrote while runs were going, such as a pulled sketch: recorded in those
183
+ * runs' snapshots as they are now, so their Done does not count them as the agent's.
184
+ */
185
+ export async function recordOwnWrites(repoRoot, runIds, files) {
186
+ if (!runIds.length || !files.length)
187
+ return;
188
+ const hashes = {};
189
+ for (const file of files) {
190
+ const hash = await hashFile(path.join(repoRoot, ...file.split('/')));
191
+ if (hash)
192
+ hashes[file] = hash;
193
+ }
194
+ for (const runId of runIds) {
195
+ const snapshot = await readSnapshot(repoRoot, runId);
196
+ if (!snapshot)
197
+ continue;
198
+ await writeJsonAtomic(path.join(snapshotDir(repoRoot, runId), 'snapshot.json'), {
199
+ ...snapshot,
200
+ files: { ...snapshot.files, ...hashes }
201
+ });
202
+ }
203
+ }
204
+ export async function readSnapshot(repoRoot, runId) {
205
+ return readJson(path.join(snapshotDir(repoRoot, runId), 'snapshot.json'));
206
+ }
207
+ export async function removeSnapshot(repoRoot, runId) {
208
+ await rm(snapshotDir(repoRoot, runId), { recursive: true, force: true });
209
+ }
210
+ export function compareFiles(before, after) {
211
+ const added = [];
212
+ const changed = [];
213
+ const removed = [];
214
+ for (const [file, hash] of Object.entries(after)) {
215
+ if (!(file in before))
216
+ added.push(file);
217
+ else if (before[file] !== hash)
218
+ changed.push(file);
219
+ }
220
+ for (const file of Object.keys(before))
221
+ if (!(file in after))
222
+ removed.push(file);
223
+ return { added: added.sort(), changed: changed.sort(), removed: removed.sort() };
224
+ }
225
+ export function allChanged(changes) {
226
+ return [...changes.added, ...changes.changed, ...changes.removed].sort();
227
+ }
228
+ /**
229
+ * Puts one file back as the snapshot found it: a file the run created is removed, one with
230
+ * uncommitted changes comes back from its copy, and a committed one from git.
231
+ */
232
+ export async function revertFile(repoRoot, runId, file) {
233
+ const snapshot = await readSnapshot(repoRoot, runId);
234
+ if (!snapshot)
235
+ return 'cannot';
236
+ if (file.split('/').includes('..') || path.isAbsolute(file))
237
+ return 'cannot';
238
+ const target = path.join(repoRoot, ...file.split('/'));
239
+ const expected = snapshot.files[file];
240
+ if (expected === undefined) {
241
+ if (!fs.existsSync(target))
242
+ return 'unchanged';
243
+ await rm(target, { force: true });
244
+ return 'removed';
245
+ }
246
+ if ((await hashFile(target)) === expected)
247
+ return 'unchanged';
248
+ const candidates = [];
249
+ if (snapshot.backedUp.includes(file)) {
250
+ candidates.push(await readFile(path.join(snapshotDir(repoRoot, runId), 'files', ...file.split('/'))));
251
+ }
252
+ else if (snapshot.git && !snapshot.notBackedUp.includes(file)) {
253
+ // The committed file as a checkout would write it, line-ending filters and all, or as stored:
254
+ // whichever the snapshot saw. ./ makes the path relative to the app, not the git repo.
255
+ candidates.push(await gitBuffer(repoRoot, ['cat-file', '--filters', `HEAD:./${file}`]));
256
+ candidates.push(await gitBuffer(repoRoot, ['cat-file', 'blob', `HEAD:./${file}`]));
257
+ }
258
+ const content = candidates.find(candidate => candidate && createHash('sha1').update(candidate).digest('hex') === expected);
259
+ if (!content)
260
+ return 'cannot';
261
+ await mkdir(path.dirname(target), { recursive: true });
262
+ await writeFile(target, content);
263
+ return 'restored';
264
+ }
265
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/runs/snapshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { readJson, writeJsonAtomic } from '../fs/json.js';
3
+ import { statePath } from '../repo/root.js';
4
+ /** Run ids come from the backend; only a UUID becomes part of a file name. */
5
+ const RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
6
+ export function assertRunId(runId) {
7
+ if (!RUN_ID.test(runId))
8
+ throw new Error(`Not a run id: ${runId}`);
9
+ return runId;
10
+ }
11
+ function runFile(repoRoot, runId) {
12
+ return statePath(repoRoot, 'runs', `${assertRunId(runId)}.json`);
13
+ }
14
+ export async function saveRun(repoRoot, run) {
15
+ await writeJsonAtomic(runFile(repoRoot, run.runId), run);
16
+ }
17
+ export async function readRun(repoRoot, runId) {
18
+ return readJson(runFile(repoRoot, runId));
19
+ }
20
+ /** Every run in this repo, newest first. */
21
+ export async function listRuns(repoRoot) {
22
+ let names;
23
+ try {
24
+ names = await readdir(statePath(repoRoot, 'runs'));
25
+ }
26
+ catch (error) {
27
+ if (error.code === 'ENOENT')
28
+ return [];
29
+ throw error;
30
+ }
31
+ const runs = await Promise.all(names
32
+ .filter(name => name.endsWith('.json') && RUN_ID.test(name.slice(0, -5)))
33
+ .map(name => readRun(repoRoot, name.slice(0, -5))));
34
+ return runs
35
+ .filter((run) => run !== null)
36
+ .sort((a, b) => b.startedAt.localeCompare(a.startedAt));
37
+ }
38
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/runs/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,8EAA8E;AAC9E,MAAM,MAAM,GAAG,gEAAgE,CAAC;AAEhF,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,KAAK,EAAE,CAAC,CAAC;IACnE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,OAAO,CAAC,QAAgB,EAAE,KAAa;IAC9C,OAAO,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,QAAgB,EAAE,GAAc;IAC5D,MAAM,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,QAAgB,EAAE,KAAa;IAC3D,OAAO,QAAQ,CAAY,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,4CAA4C;AAC5C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,QAAgB;IAC7C,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO,EAAE,CAAC;QAClE,MAAM,KAAK,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAC5B,KAAK;SACF,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;SACxE,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CACrD,CAAC;IACF,OAAO,IAAI;SACR,MAAM,CAAC,CAAC,GAAG,EAAoB,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC;SAC/C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,3 @@
1
+ // A local run as the backend hands it over, and as the studio keeps it.
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/runs/types.ts"],"names":[],"mappings":"AAAA,wEAAwE"}
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import { ROUGH_DIR, STATE_DIR } from '../repo/root.js';
3
+ /** Folders whose churn means nothing: dependencies, git itself, build output, the studio's state. */
4
+ const IGNORED = [
5
+ 'node_modules/',
6
+ '.git/',
7
+ '.next/',
8
+ '.turbo/',
9
+ `${ROUGH_DIR}/${STATE_DIR}/`,
10
+ `${ROUGH_DIR}/.screenshots/`
11
+ ];
12
+ /** A watcher's file name as a repo-relative path with forward slashes, or null to ignore it. */
13
+ export function watchedPath(filename) {
14
+ if (!filename)
15
+ return null;
16
+ const file = String(filename).replace(/\\/g, '/');
17
+ if (IGNORED.some(prefix => file.startsWith(prefix) || `${file}/` === prefix))
18
+ return null;
19
+ return file;
20
+ }
21
+ /**
22
+ * Calls `onChange` with each file written in the repo while a run is going, so a write outside
23
+ * its folder can be flagged as it happens. The snapshot after the run is still the record; this
24
+ * is only the early warning. Returns a function that stops watching.
25
+ */
26
+ export function watchRepo(repoRoot, onChange) {
27
+ let watcher;
28
+ try {
29
+ watcher = fs.watch(repoRoot, { recursive: true }, (_event, filename) => {
30
+ const file = watchedPath(filename);
31
+ if (file)
32
+ onChange(file);
33
+ });
34
+ }
35
+ catch {
36
+ // Some file systems cannot be watched; the snapshot still catches every write.
37
+ return () => { };
38
+ }
39
+ watcher.on('error', () => watcher.close());
40
+ return () => watcher.close();
41
+ }
42
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/runs/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEvD,qGAAqG;AACrG,MAAM,OAAO,GAAG;IACd,eAAe;IACf,OAAO;IACP,QAAQ;IACR,SAAS;IACT,GAAG,SAAS,IAAI,SAAS,GAAG;IAC5B,GAAG,SAAS,gBAAgB;CAC7B,CAAC;AAEF,gGAAgG;AAChG,MAAM,UAAU,WAAW,CAAC,QAAgC;IAC1D,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAClD,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,GAAG,KAAK,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1F,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,QAAgC;IAC1E,IAAI,OAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE;YACrE,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;YACnC,IAAI,IAAI;gBAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,+EAA+E;QAC/E,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;IAC3C,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,163 @@
1
+ // Screenshots of an iteration through the repo's own dev server, at desktop and phone widths, so
2
+ // the local UI can still show it after the app's code moves on and the live preview no longer
3
+ // compiles. Uses a Chromium browser already on the machine, headless: nothing is downloaded.
4
+ // Ported from roughbook runner/screenshot.ts (v2-local), which knew Windows and macOS; Linux
5
+ // and Chromium are added. Headless Chrome will not make a window narrower than about 500 pixels,
6
+ // so the viewport is set through the DevTools protocol over a pipe instead of --window-size,
7
+ // and Chrome is preferred to Edge, which fills saved passwords in even into a fresh profile.
8
+ import { spawn } from 'node:child_process';
9
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
10
+ import { mkdir, writeFile } from 'node:fs/promises';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ export const SIZES = {
14
+ desktop: { width: 1440, height: 900, mobile: false },
15
+ mobile: { width: 390, height: 844, mobile: true }
16
+ };
17
+ /** Chrome or Chromium, then Edge, or the browser ROUGHBOOK_BROWSER names. */
18
+ export function findBrowser(env = process.env, exists = existsSync, platform = process.platform) {
19
+ const explicit = env.ROUGHBOOK_BROWSER?.trim();
20
+ if (explicit)
21
+ return exists(explicit) ? explicit : null;
22
+ const win = (root, ...parts) => root ? path.win32.join(root, ...parts) : undefined;
23
+ const candidates = platform === 'win32'
24
+ ? [
25
+ win(env.ProgramFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
26
+ win(env['ProgramFiles(x86)'], 'Google', 'Chrome', 'Application', 'chrome.exe'),
27
+ win(env.LOCALAPPDATA, 'Google', 'Chrome', 'Application', 'chrome.exe'),
28
+ win(env.LOCALAPPDATA, 'Chromium', 'Application', 'chrome.exe'),
29
+ win(env['ProgramFiles(x86)'], 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
30
+ win(env.ProgramFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe')
31
+ ]
32
+ : platform === 'darwin'
33
+ ? [
34
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
35
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
36
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
37
+ ]
38
+ : [
39
+ '/usr/bin/google-chrome',
40
+ '/usr/bin/google-chrome-stable',
41
+ '/usr/bin/chromium',
42
+ '/usr/bin/chromium-browser',
43
+ '/snap/bin/chromium',
44
+ '/usr/bin/microsoft-edge',
45
+ '/usr/bin/microsoft-edge-stable',
46
+ '/opt/microsoft/msedge/msedge'
47
+ ];
48
+ return candidates.find((file) => Boolean(file) && exists(file)) ?? null;
49
+ }
50
+ /** The browser's arguments: headless, a throwaway profile, and the DevTools protocol on a pipe. */
51
+ export function browserArgs(profile) {
52
+ return [
53
+ '--headless=new',
54
+ '--disable-gpu',
55
+ '--hide-scrollbars',
56
+ '--no-first-run',
57
+ '--no-default-browser-check',
58
+ '--disable-sync',
59
+ '--disable-extensions',
60
+ `--user-data-dir=${profile}`,
61
+ '--remote-debugging-pipe',
62
+ 'about:blank'
63
+ ];
64
+ }
65
+ /** A DevTools protocol connection over the pipe: NUL-separated JSON in each direction. */
66
+ class Pipe {
67
+ output;
68
+ next = 1;
69
+ buffer = '';
70
+ pending = new Map();
71
+ waiting = [];
72
+ constructor(output, input) {
73
+ this.output = output;
74
+ input.on('data', (chunk) => {
75
+ this.buffer += chunk.toString('utf8');
76
+ let end;
77
+ while ((end = this.buffer.indexOf('\0')) !== -1) {
78
+ const text = this.buffer.slice(0, end);
79
+ this.buffer = this.buffer.slice(end + 1);
80
+ this.receive(JSON.parse(text));
81
+ }
82
+ });
83
+ }
84
+ receive(message) {
85
+ if (message.id !== undefined) {
86
+ const call = this.pending.get(message.id);
87
+ this.pending.delete(message.id);
88
+ if (message.error)
89
+ call?.reject(new Error(JSON.stringify(message.error)));
90
+ else
91
+ call?.resolve(message.result);
92
+ return;
93
+ }
94
+ const index = this.waiting.findIndex(item => item.method === message.method && item.sessionId === message.sessionId);
95
+ if (index !== -1)
96
+ this.waiting.splice(index, 1)[0].resolve();
97
+ }
98
+ send(method, params = {}, sessionId) {
99
+ const id = this.next++;
100
+ return new Promise((resolve, reject) => {
101
+ this.pending.set(id, { resolve: value => resolve(value), reject });
102
+ this.output.write(`${JSON.stringify({ id, method, params, sessionId })}\0`);
103
+ });
104
+ }
105
+ event(method, sessionId) {
106
+ return new Promise(resolve => this.waiting.push({ method, sessionId, resolve }));
107
+ }
108
+ }
109
+ function within(promise, ms) {
110
+ return Promise.race([
111
+ promise,
112
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timed out')), ms).unref())
113
+ ]);
114
+ }
115
+ /** Takes one screenshot at exactly `size`. Resolves true when the file was written; never throws. */
116
+ export async function takeScreenshot({ browser, url, file, size, timeoutMs = 60_000 }) {
117
+ const profile = mkdtempSync(path.join(os.tmpdir(), 'roughbook-shot-'));
118
+ const child = spawn(browser, browserArgs(profile), {
119
+ stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'],
120
+ windowsHide: true
121
+ });
122
+ child.on('error', () => { });
123
+ try {
124
+ const pipe = new Pipe(child.stdio[3], child.stdio[4]);
125
+ const capture = async () => {
126
+ const { targetId } = await pipe.send('Target.createTarget', {
127
+ url: 'about:blank'
128
+ });
129
+ const { sessionId } = await pipe.send('Target.attachToTarget', {
130
+ targetId,
131
+ flatten: true
132
+ });
133
+ await pipe.send('Page.enable', {}, sessionId);
134
+ await pipe.send('Emulation.setDeviceMetricsOverride', { width: size.width, height: size.height, deviceScaleFactor: 1, mobile: size.mobile }, sessionId);
135
+ const loaded = pipe.event('Page.loadEventFired', sessionId);
136
+ await pipe.send('Page.navigate', { url }, sessionId);
137
+ await loaded;
138
+ // Fonts, then a moment for client-side rendering to settle.
139
+ await pipe.send('Runtime.evaluate', { expression: 'document.fonts.ready.then(() => true)', awaitPromise: true }, sessionId);
140
+ await new Promise(resolve => setTimeout(resolve, 1500));
141
+ const { data } = await pipe.send('Page.captureScreenshot', { format: 'png' }, sessionId);
142
+ await mkdir(path.dirname(file), { recursive: true });
143
+ await writeFile(file, Buffer.from(data, 'base64'));
144
+ await pipe.send('Browser.close').catch(() => undefined);
145
+ };
146
+ await within(capture(), timeoutMs);
147
+ return true;
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ finally {
153
+ if (child.exitCode === null)
154
+ child.kill();
155
+ try {
156
+ rmSync(profile, { recursive: true, force: true, maxRetries: 10, retryDelay: 500 });
157
+ }
158
+ catch {
159
+ // The browser may still hold its profile; it stays in the OS temp folder.
160
+ }
161
+ }
162
+ }
163
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/screenshot/browser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import path from 'node:path';
2
+ import { previewUrl } from '../repo/preview.js';
3
+ import { ROUGH_DIR } from '../repo/root.js';
4
+ import { SIZES, findBrowser, takeScreenshot } from './browser.js';
5
+ /** Where an iteration's screenshots go: rough/.screenshots/, never committed. */
6
+ export function screenshotPath(page, component, iteration, size) {
7
+ return `${ROUGH_DIR}/.screenshots/${page}/${component}/${iteration}/${size}.png`;
8
+ }
9
+ /** Desktop and phone screenshots of one iteration, through the repo's dev server. */
10
+ export async function screenshotIteration(repoRoot, devUrl, target, env, take = takeScreenshot, browser = findBrowser(env)) {
11
+ if (!browser) {
12
+ return {
13
+ files: {},
14
+ warning: 'No Chrome, Chromium or Edge was found for screenshots. Set ROUGHBOOK_BROWSER to one.'
15
+ };
16
+ }
17
+ const url = previewUrl(devUrl, target.page, target.component, target.iteration);
18
+ const files = {};
19
+ for (const size of Object.keys(SIZES)) {
20
+ const relative = screenshotPath(target.page, target.component, target.iteration, size);
21
+ const ok = await take({
22
+ browser,
23
+ url,
24
+ file: path.join(repoRoot, ...relative.split('/')),
25
+ size: SIZES[size]
26
+ });
27
+ if (ok)
28
+ files[size] = relative;
29
+ }
30
+ return {
31
+ files,
32
+ warning: Object.keys(files).length === 2
33
+ ? null
34
+ : `The screenshot failed. Is the app's dev server running at ${devUrl}?`
35
+ };
36
+ }
37
+ //# sourceMappingURL=iteration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iteration.js","sourceRoot":"","sources":["../../src/screenshot/iteration.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AASlE,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,SAAiB,EACjB,SAAiB,EACjB,IAAwB;IAExB,OAAO,GAAG,SAAS,iBAAiB,IAAI,IAAI,SAAS,IAAI,SAAS,IAAI,IAAI,MAAM,CAAC;AACnF,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAAgB,EAChB,MAAc,EACd,MAA8D,EAC9D,GAAsB,EACtB,OAA8B,cAAc,EAC5C,UAAyB,WAAW,CAAC,GAAG,CAAC;IAEzC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,EAAE;YACT,OAAO,EACL,sFAAsF;SACzF,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IAChF,MAAM,KAAK,GAAmB,EAAE,CAAC;IACjC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAA2B,EAAE,CAAC;QAChE,MAAM,QAAQ,GAAG,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QACvF,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC;YACpB,OAAO;YACP,GAAG;YACH,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACjD,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC;SAClB,CAAC,CAAC;QACH,IAAI,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC;IACjC,CAAC;IACD,OAAO;QACL,KAAK;QACL,OAAO,EACL,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;YAC7B,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,6DAA6D,MAAM,GAAG;KAC7E,CAAC;AACJ,CAAC"}