@cs4alhaider/screenbook 0.1.0 → 0.2.1

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 (66) hide show
  1. package/AGENTS.md +147 -0
  2. package/CLAUDE.md +4 -184
  3. package/README.md +146 -163
  4. package/bridge.js +16 -2
  5. package/cli/postinstall.js +34 -0
  6. package/cli/screenbook.js +84 -34
  7. package/cli/templates.js +54 -1
  8. package/core/browser.js +22 -7
  9. package/core/project.js +28 -7
  10. package/core/serve.js +22 -7
  11. package/docs/README.md +25 -14
  12. package/docs/brand/logo-mark.png +0 -0
  13. package/docs/brand/logo-transparent.png +0 -0
  14. package/docs/brand/logo.png +0 -0
  15. package/docs/brand/readme-header.png +0 -0
  16. package/docs/contributing/architecture.md +153 -0
  17. package/{TESTING.md → docs/contributing/testing.md} +13 -8
  18. package/docs/{app-mode-guide.md → guides/app-mode.md} +17 -10
  19. package/docs/{authoring-guide.md → guides/authoring.md} +43 -17
  20. package/docs/guides/getting-started.md +84 -0
  21. package/docs/{studio-guide.md → guides/studio.md} +3 -2
  22. package/docs/{troubleshooting.md → guides/troubleshooting.md} +11 -8
  23. package/docs/{cli.md → reference/cli.md} +15 -10
  24. package/docs/{mcp-agents.md → reference/mcp-agents.md} +3 -3
  25. package/docs/shots/android-dark.png +0 -0
  26. package/docs/shots/android-light.png +0 -0
  27. package/docs/shots/app-mode-dark.png +0 -0
  28. package/docs/shots/app-mode-light.png +0 -0
  29. package/docs/shots/hero-studio-dark.png +0 -0
  30. package/docs/shots/hero-studio-light.png +0 -0
  31. package/docs/shots/review-mode-dark.png +0 -0
  32. package/docs/shots/review-mode-light.png +0 -0
  33. package/docs/shots/rtl-arabic-dark.png +0 -0
  34. package/docs/shots/rtl-arabic-light.png +0 -0
  35. package/docs/shots/theme-lab-dark.png +0 -0
  36. package/docs/shots/theme-lab-light.png +0 -0
  37. package/docs/shots/theming-noir-dark.png +0 -0
  38. package/docs/shots/theming-noir-light.png +0 -0
  39. package/docs/shots/web-dashboard-dark.png +0 -0
  40. package/docs/shots/web-dashboard-light.png +0 -0
  41. package/examples/qahwa/features/order/screens/010-menu.html +1 -1
  42. package/examples/qahwa/features/order/screens/020-drink.html +1 -1
  43. package/examples/qahwa/features/order/screens/030-cart.html +1 -1
  44. package/examples/qahwa/features/order/screens/040-status.html +1 -1
  45. package/examples/qahwa/features/order/screens/050-status-android.html +1 -1
  46. package/examples/qahwa/features/roastery/screens/010-orders.html +1 -1
  47. package/examples/qahwa/features/roastery/screens/020-menu-editor.html +1 -1
  48. package/examples/spa-demo/app/index.html +1 -1
  49. package/examples/tokens-lab/features/lab/screens/010-swatches.html +1 -1
  50. package/examples/tokens-lab/features/lab/screens/020-typography.html +1 -1
  51. package/mcp/server.js +4 -2
  52. package/package.json +7 -6
  53. package/shell/screenhost.js +4 -1
  54. package/shell/selftest.js +18 -2
  55. package/shell/shell.css +17 -9
  56. package/shell/store.js +2 -0
  57. package/skill/SKILL.md +1 -1
  58. package/skill/references/cli-mcp.md +1 -1
  59. package/skill/references/screen-contract.md +1 -1
  60. package/docs/getting-started.md +0 -88
  61. package/docs/shots/app-mode.png +0 -0
  62. package/docs/shots/hero-studio.png +0 -0
  63. package/docs/shots/rtl-arabic.png +0 -0
  64. package/docs/shots/theme-lab.png +0 -0
  65. package/docs/shots/theming-noir.png +0 -0
  66. package/docs/shots/web-dashboard.png +0 -0
package/cli/screenbook.js CHANGED
@@ -10,7 +10,7 @@ import { startServer } from '../core/serve.js';
10
10
  import * as T from './templates.js';
11
11
 
12
12
  const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
- const VERSION = '0.1.0';
13
+ const VERSION = '0.2.1';
14
14
 
15
15
  /* ---------------- arg parsing ---------------- */
16
16
 
@@ -48,13 +48,17 @@ async function requireRoot() {
48
48
  /* ---------------- commands ---------------- */
49
49
 
50
50
  async function cmdInit() {
51
- const dir = path.resolve(positional[0] || '.');
51
+ /* Everything lands in <project>/.screenbook/ — the self-contained source of
52
+ truth for the prototype. The user decides to commit or gitignore it.
53
+ No cloning, no runtime copy: `serve` mounts the studio from this package. */
54
+ const projectDir = path.resolve(positional[0] || '.');
55
+ const dir = path.join(projectDir, '.screenbook');
52
56
  const configPath = path.join(dir, 'app.config.json');
53
57
  if (await exists(configPath) && !flags.force) {
54
- console.error(bad(`app.config.json already exists in ${dir} — refusing to overwrite (use --force).`));
58
+ console.error(bad(`.screenbook/ already exists in ${projectDir} — refusing to overwrite (use --force).`));
55
59
  process.exit(2);
56
60
  }
57
- const name = flags.name || path.basename(dir);
61
+ const name = flags.name || path.basename(projectDir);
58
62
  const day = today();
59
63
 
60
64
  await mkdirp(path.join(dir, 'features/welcome/screens'));
@@ -63,6 +67,7 @@ async function cmdInit() {
63
67
  await mkdirp(path.join(dir, 'review'));
64
68
 
65
69
  await writeJSON(configPath, T.APP_CONFIG(name));
70
+ await writeAtomic(path.join(dir, 'brief.md'), T.BRIEF(name));
66
71
  await writeJSON(path.join(dir, 'themes/default.json'), T.DEFAULT_THEME);
67
72
  await writeJSON(path.join(dir, 'review/comments.json'), { comments: [] });
68
73
  await writeJSON(path.join(dir, 'features/welcome/feature.json'), T.WELCOME_FEATURE);
@@ -71,39 +76,45 @@ async function cmdInit() {
71
76
  await writeJSON(path.join(dir, 'features/welcome/flows.json'), T.WELCOME_FLOWS);
72
77
  await writeAtomic(path.join(dir, 'features/welcome/screens/010-hello.html'), T.WELCOME_SCREEN(day));
73
78
 
74
- /* agent onboarding ships with the product (EN-32) */
75
- const claudeMd = path.join(dir, 'CLAUDE.md');
76
- if (!(await exists(claudeMd))) {
77
- const contract = await readTextSafe(path.join(ENGINE_DIR, 'CLAUDE.md'));
78
- if (contract) await writeAtomic(claudeMd, contract);
79
- }
79
+ /* Agent onboarding, multi-agent (Abdullah's panel picks):
80
+ - the full contract at .screenbook/AGENTS.md (the 2026 open standard —
81
+ Codex, Cursor, Gemini, Copilot AND Claude Code read it natively)
82
+ - .screenbook/CLAUDE.md as a 3-line pointer for older Claude versions
83
+ - the skill in BOTH standard locations: .agents/skills/ + .claude/skills/
84
+ - a marked, idempotent breadcrumb appended to the PROJECT's root AGENTS.md */
85
+ const contract = await readTextSafe(path.join(ENGINE_DIR, 'AGENTS.md'));
86
+ if (contract) await writeAtomic(path.join(dir, 'AGENTS.md'), contract);
87
+ await writeAtomic(path.join(dir, 'CLAUDE.md'), T.CLAUDE_POINTER);
80
88
  const skillSrc = path.join(ENGINE_DIR, 'skill');
81
89
  if (await exists(skillSrc)) {
82
- await copyDir(skillSrc, path.join(dir, '.claude/skills/screenbook'));
90
+ await copyDir(skillSrc, path.join(projectDir, '.claude/skills/screenbook'));
91
+ await copyDir(skillSrc, path.join(projectDir, '.agents/skills/screenbook'));
83
92
  }
93
+ await upsertRootPointer(projectDir);
84
94
 
85
95
  await regenAllManifests(dir);
86
96
 
87
- /* `init --app <path>`: register an existing SPA in this folder as an
88
- embedded app in the same breath (Abdullah's flow: point ScreenBook at
89
- any folder that already contains an app). */
97
+ /* `init --app <path>`: register an existing SPA from the USER'S project as
98
+ an embedded app recorded as project/<path>, served via the /project mount. */
90
99
  if (flags.app) {
91
- const rel = path.relative(dir, path.resolve(flags.app)) || flags.app;
100
+ const rel = path.relative(projectDir, path.resolve(flags.app));
101
+ if (rel.startsWith('..')) {
102
+ console.error(bad(`--app must point inside ${projectDir}`));
103
+ process.exit(2);
104
+ }
92
105
  const r = await addApp(dir, {
93
- src: rel.split(path.sep).join('/'),
106
+ src: `project/${rel.split(path.sep).join('/')}`,
94
107
  driver: !flags.cooperative
95
108
  });
96
109
  console.log(ok(`✓ app registered: ${r.feature.id}`) + dim(` → ${r.feature.src}`));
97
- if (r.driver) console.log(` fill in ${bold(r.driver)} to teach ScreenBook the app's internals`);
110
+ if (r.driver) console.log(` fill in ${bold('.screenbook/' + r.driver.replace(/^features\//, 'features/'))} to teach ScreenBook the app's internals`);
98
111
  }
99
112
 
100
- console.log(ok(`✓ ScreenBook project scaffolded in ${dir}`));
101
- console.log(` app.config.json · themes/default.json · features/welcome/ · review/`);
102
- console.log(` CLAUDE.md + .claude/skills/screenbook/ ${dim('(agent authoring contract)')}`);
103
- if (!(await exists(path.join(dir, 'engine')))) {
104
- console.log(dim(` note: no engine/ folder here${bold('screenbook serve')} mounts it automatically;`));
105
- console.log(dim(` copy the engine folder in when you want plain static servers to work too.`));
106
- }
113
+ console.log(ok(`✓ ScreenBook ready everything lives in ${bold('.screenbook/')}`));
114
+ console.log(` ${bold('brief.md')} ${dim('(write your requirements here agents read it first)')}`);
115
+ console.log(` app.config.json · themes/ · features/welcome/ · review/ · AGENTS.md ${dim('(+CLAUDE.md pointer)')}`);
116
+ console.log(` agent setup: root AGENTS.md section · .agents/skills/ + .claude/skills/ ${dim('(Claude, Codex, Cursor, Gemini, …)')}`);
117
+ console.log(dim(` commit .screenbook/ or add it to .gitignore your call; it is the source of truth.`));
107
118
  console.log(`\nNext: ${bold(`screenbook serve ${positional[0] || '.'}`)} → http://127.0.0.1:4600`);
108
119
  }
109
120
 
@@ -112,6 +123,25 @@ async function readTextSafe(p) {
112
123
  catch { return null; }
113
124
  }
114
125
 
126
+ /* Root AGENTS.md breadcrumb: create the file if missing; if it exists, replace
127
+ only the marked ScreenBook section (or append it) — user content is never touched. */
128
+ async function upsertRootPointer(projectDir) {
129
+ const file = path.join(projectDir, 'AGENTS.md');
130
+ const START = '<!-- screenbook:start -->';
131
+ const END = '<!-- screenbook:end -->';
132
+ const existing = await readTextSafe(file);
133
+ if (existing === null) {
134
+ await writeAtomic(file, T.ROOT_POINTER + '\n');
135
+ return;
136
+ }
137
+ const s = existing.indexOf(START);
138
+ const e = existing.indexOf(END);
139
+ const next = (s >= 0 && e > s)
140
+ ? existing.slice(0, s) + T.ROOT_POINTER + existing.slice(e + END.length)
141
+ : existing.replace(/\s*$/, '') + '\n\n' + T.ROOT_POINTER + '\n';
142
+ if (next !== existing) await writeAtomic(file, next);
143
+ }
144
+
115
145
  async function copyDir(src, dest) {
116
146
  const fs = await import('node:fs/promises');
117
147
  await fs.mkdir(dest, { recursive: true });
@@ -123,8 +153,8 @@ async function cmdServe() {
123
153
  const port = parseInt(flags.port || '4600', 10);
124
154
  const srv = await startServer({ root, engineDir: ENGINE_DIR, port });
125
155
  console.log(bold(`⧉ ScreenBook`) + ` serving ${dim(root)}`);
126
- console.log(` studio ${ok(srv.url + '/engine/index.html')}`);
127
- console.log(` selftest ${dim(srv.url + '/engine/index.html?selftest=1')}`);
156
+ console.log(` studio ${ok(srv.url + '/screenbook/')}`);
157
+ console.log(` selftest ${dim(srv.url + '/screenbook/?selftest=1')}`);
128
158
  console.log(` live reload + manifest refresh + review sync ${ok('on')} ${dim('(no-cache always)')}`);
129
159
  }
130
160
 
@@ -205,8 +235,21 @@ async function cmdApp() {
205
235
  }
206
236
  positional.splice(0, 2, positional[2]); // remaining positional = project dir
207
237
  const root = await requireRoot();
208
- const rel = path.isAbsolute(srcArg)
209
- ? path.relative(root, srcArg).split(path.sep).join('/')
238
+
239
+ /* Normalize src to something the server can actually mount. Relative paths
240
+ are taken relative to the project directory. Three cases:
241
+ 1. inside the ScreenBook root itself → keep it root-relative
242
+ 2. inside the user's project (.screenbook) → "project/<path>" — served
243
+ by the /project mount that maps one level above the root
244
+ 3. anywhere else → keep the raw argument and let addApp's existence
245
+ check produce the error message */
246
+ const isDotRoot = path.basename(root) === '.screenbook';
247
+ const projectDir = isDotRoot ? path.dirname(root) : root;
248
+ const absSrc = path.isAbsolute(srcArg) ? srcArg : path.resolve(projectDir, srcArg);
249
+ const fromRoot = path.relative(root, absSrc);
250
+ const fromProject = path.relative(projectDir, absSrc);
251
+ const rel = !fromRoot.startsWith('..') ? fromRoot.split(path.sep).join('/')
252
+ : isDotRoot && !fromProject.startsWith('..') ? `project/${fromProject.split(path.sep).join('/')}`
210
253
  : srcArg;
211
254
  const r = await addApp(root, {
212
255
  src: rel,
@@ -253,21 +296,28 @@ async function cmdScreenshot() {
253
296
  }
254
297
 
255
298
  function help() {
256
- console.log(`${bold('screenbook')} ${VERSION} — static prototype studio
299
+ console.log(`${bold('screenbook')} ${VERSION} — a design workspace for AI agents, to present to humans
300
+
301
+ ${bold('Start here')}
302
+ ${ok('screenbook init')} scaffold .screenbook/ in your project — write brief.md first
303
+ ${ok('screenbook serve')} your studio → http://127.0.0.1:4600
304
+ claude mcp add screenbook -- npx -y @cs4alhaider/screenbook mcp --root .
305
+ ${dim('connect your AI agent (any MCP client works)')}
257
306
 
258
- ${bold('init')} [dir] [--name X] [--force] scaffold a consumer project (+ CLAUDE.md + skill)
307
+ ${bold('Commands')}
308
+ ${bold('init')} [dir] [--name X] [--force] scaffold a consumer project (+ agent contract + skill)
259
309
  [--app <src>] also register an existing SPA in that folder as an embedded app
260
310
  ${bold('app add')} <src> [dir] [--id --title --icon --device --chrome self|engine --cooperative]
261
311
  embed an existing single-page app (driver scaffolded unless --cooperative)
262
312
  ${bold('serve')} [dir] [--port 4600] static server · live reload · manifest refresh · review sync
263
313
  ${bold('manifest')} [dir] rescan screens → regenerate features/*/manifest.json
264
- ${bold('validate')} [dir] [--static] [--json] static checks + headless browser self-test
265
- ${bold('mcp')} [--root dir] start the stdio MCP server (for agents):
266
- claude mcp add screenbook -- npx -y @cs4alhaider/screenbook mcp --root .
314
+ ${bold('validate')} [dir] [--static] [--json] static checks + headless browser self-test — the done gate
267
315
  ${bold('screenshot')} [dir] --screen f/s PNG of a screen in its device frame
268
316
  [--mode light,dark] [--dir ltr,rtl] [--scenario id] [--theme id] [--lang xx] [--out dir]
317
+ ${bold('mcp')} [--root dir] stdio MCP server — 18 authoring tools for agents
269
318
 
270
- Projects are folders with app.config.json + features/ + themes/ (see engine/CLAUDE.md).`);
319
+ ${dim('Docs & guides')} https://cs4alhaider.github.io/screenbook/
320
+ ${dim('Bugs & ideas')} https://github.com/cs4alhaider/screenbook/issues`);
271
321
  }
272
322
 
273
323
  const commands = {
package/cli/templates.js CHANGED
@@ -87,7 +87,7 @@ export const WELCOME_SCREEN = (today) => `<!DOCTYPE html>
87
87
  "updated": "${today}"
88
88
  }
89
89
  </script>
90
- <script src="../../../engine/bridge.js"></script>
90
+ <script src="../../../screenbook/bridge.js"></script>
91
91
  <style>
92
92
  * { box-sizing: border-box; }
93
93
  body {
@@ -137,3 +137,56 @@ export const WELCOME_SCREEN = (today) => `<!DOCTYPE html>
137
137
  export const GITIGNORE = `node_modules/
138
138
  .DS_Store
139
139
  `;
140
+
141
+ /* Marked section appended to the PROJECT's root AGENTS.md so every agent
142
+ (Codex, Cursor, Gemini, Claude, …) discovers ScreenBook unprompted.
143
+ Idempotent: init replaces the content between the markers. */
144
+ export const ROOT_POINTER =
145
+ `<!-- screenbook:start -->
146
+ ## UI prototyping (ScreenBook)
147
+
148
+ This project's UI prototypes live in **.screenbook/** — a [ScreenBook](https://github.com/cs4alhaider/screenbook) studio.
149
+ Before designing or changing any screen: read **.screenbook/brief.md** (the requirements)
150
+ and **.screenbook/AGENTS.md** (the authoring contract).
151
+ Preview locally: \`screenbook serve\` → http://127.0.0.1:4600
152
+ Agent tools (MCP, 18 tools): \`npx -y @cs4alhaider/screenbook mcp --root .\`
153
+ <!-- screenbook:end -->`;
154
+
155
+ export const CLAUDE_POINTER = `# ScreenBook
156
+
157
+ The authoring contract for this prototype lives in **AGENTS.md** (same folder) — read
158
+ that, plus **brief.md** for the product requirements, before creating or changing any
159
+ screen.
160
+ `;
161
+
162
+ export const BRIEF = (name) => `# ${name} — product brief
163
+
164
+ > **AI agents: read this file first, before creating or changing any screen.**
165
+ > It is the source of truth for what we are building and why.
166
+
167
+ ## What we're building
168
+
169
+ _One paragraph: the product, who it's for, the core promise._
170
+
171
+ ## The screens we need
172
+
173
+ _List the features and screens you expect, roughly in user-journey order.
174
+ Agents: turn each line into screens under features/, and ask (via review
175
+ comments) when something here is ambiguous._
176
+
177
+ - …
178
+
179
+ ## Design direction
180
+
181
+ _Tone, references, the feeling. The design tokens in themes/ are the palette —
182
+ change them there, not per-screen._
183
+
184
+ ## States that must exist
185
+
186
+ _Empty / first-run · busy / real usage · edge cases. Agents: model these as
187
+ data scenarios so every screen can be reviewed in each state._
188
+
189
+ ## Out of scope (for now)
190
+
191
+ - …
192
+ `;
package/core/browser.js CHANGED
@@ -6,12 +6,13 @@ import path from 'node:path';
6
6
  import { startServer } from './serve.js';
7
7
  import { mkdirp } from './fsx.js';
8
8
 
9
- async function launch() {
9
+ async function launch({ allowDownload = true } = {}) {
10
10
  let lastErr;
11
11
  for (const [pkg, opts] of [
12
12
  ['playwright', { headless: true }],
13
13
  ['playwright-core', { headless: true, channel: 'chrome' }],
14
- ['playwright-core', { headless: true, channel: 'chromium' }]
14
+ ['playwright-core', { headless: true, channel: 'chromium' }],
15
+ ['playwright-core', { headless: true }]
15
16
  ]) {
16
17
  try {
17
18
  const mod = await import(pkg);
@@ -20,10 +21,24 @@ async function launch() {
20
21
  lastErr = err;
21
22
  }
22
23
  }
24
+
25
+ /* No usable browser on this machine — download one automatically, once.
26
+ (Nobody should ever be told to install something by hand.) */
27
+ if (allowDownload) {
28
+ console.error('No browser found — downloading a headless Chromium once (~120MB)…');
29
+ try {
30
+ const { spawnSync } = await import('node:child_process');
31
+ const { createRequire } = await import('node:module');
32
+ const req = createRequire(import.meta.url);
33
+ const cli = req.resolve('playwright-core/cli.js');
34
+ const r = spawnSync(process.execPath, [cli, 'install', 'chromium'], { stdio: ['ignore', 'inherit', 'inherit'] });
35
+ if (r.status === 0) return launch({ allowDownload: false });
36
+ } catch { /* fall through to the error below */ }
37
+ }
38
+
23
39
  throw new Error(
24
- `No headless browser available. Install one inside engine/:\n` +
25
- ` npm i -D playwright-core (uses your installed Chrome)\n` +
26
- ` npm i -D playwright && npx playwright install chromium (self-contained)\n` +
40
+ `No headless browser available and the automatic download failed.\n` +
41
+ `Install Google Chrome, or run: npx playwright-core install chromium\n` +
27
42
  `Underlying error: ${lastErr?.message}`
28
43
  );
29
44
  }
@@ -62,7 +77,7 @@ export async function runBrowserSelftest({ root, engineDir, timeout = 180000 })
62
77
  shellErrors.push(text);
63
78
  });
64
79
 
65
- await page.goto(`${base}/engine/index.html?selftest=1`, { waitUntil: 'domcontentloaded' });
80
+ await page.goto(`${base}/screenbook/?selftest=1`, { waitUntil: 'domcontentloaded' });
66
81
  await page.waitForFunction('window.__selftestDone === true', null, { timeout });
67
82
  const result = await page.evaluate('window.__selftestResult');
68
83
  result.shellErrors = shellErrors;
@@ -87,7 +102,7 @@ export async function screenshot({ root, engineDir, combos, outDir }) {
87
102
  if (c.dir && c.dir !== 'ltr') params.set('dir', c.dir);
88
103
  if (c.lang) params.set('lang', c.lang);
89
104
  const q = params.toString();
90
- const url = `${base}/engine/index.html?solo=1#${encodeURIComponent(c.feature)}/${encodeURIComponent(c.screen)}${q ? `?${q}` : ''}`;
105
+ const url = `${base}/screenbook/?solo=1#${encodeURIComponent(c.feature)}/${encodeURIComponent(c.screen)}${q ? `?${q}` : ''}`;
91
106
 
92
107
  await page.goto('about:blank');
93
108
  await page.goto(url, { waitUntil: 'domcontentloaded' });
package/core/project.js CHANGED
@@ -10,15 +10,36 @@ const MANIFEST_FIELDS = ['title', 'description', 'order', 'device', 'devices', '
10
10
 
11
11
  /* ---------------- roots + config ---------------- */
12
12
 
13
+ /*
14
+ * Find the ScreenBook root for a directory. The standard home is a
15
+ * `.screenbook/` folder inside the user's project (created by `init` — the
16
+ * self-contained source of truth they can commit or gitignore). A config at
17
+ * the directory itself also works (bundled examples, legacy layouts).
18
+ */
13
19
  export async function resolveRoot(dir) {
14
- const root = path.resolve(dir || process.cwd());
15
- if (await exists(path.join(root, 'app.config.json'))) return root;
16
- /* convenience: running from inside engine/ of a project */
17
- const parent = path.dirname(root);
18
- if (await exists(path.join(parent, 'app.config.json'))) return parent;
20
+ const base = path.resolve(dir || process.cwd());
21
+ const candidates = [
22
+ base,
23
+ path.join(base, '.screenbook'),
24
+ path.dirname(base), // running from inside engine/ or .screenbook/
25
+ path.join(path.dirname(base), '.screenbook')
26
+ ];
27
+ for (const c of candidates) {
28
+ if (await exists(path.join(c, 'app.config.json'))) return c;
29
+ }
19
30
  return null;
20
31
  }
21
32
 
33
+ /* Disk location of an app-feature src. Apps usually live in the USER'S
34
+ project, outside .screenbook/ — those are recorded as "project/<path>"
35
+ and served via the /project mount. */
36
+ export function appSrcPath(root, src) {
37
+ if (src.startsWith('project/') && path.basename(root) === '.screenbook') {
38
+ return path.join(path.dirname(root), src.slice('project/'.length));
39
+ }
40
+ return path.join(root, src);
41
+ }
42
+
22
43
  export async function readConfig(root) {
23
44
  return readJSON(path.join(root, 'app.config.json'), {});
24
45
  }
@@ -195,7 +216,7 @@ export async function addFeature(root, { id, title, icon, description, order })
195
216
  */
196
217
  export async function addApp(root, { src, id, title, icon, description, device, chrome, driver, order }) {
197
218
  if (!src) throw new Error('src required — project-relative path to the app\'s index.html');
198
- const srcPath = path.join(root, src);
219
+ const srcPath = appSrcPath(root, src);
199
220
  if (!(await exists(srcPath))) throw new Error(`app entry not found: ${src}`);
200
221
 
201
222
  const appId = id || path.basename(path.dirname(srcPath)).toLowerCase().replace(/[^a-z0-9-]+/g, '-') || 'app';
@@ -386,7 +407,7 @@ export async function staticValidate(root) {
386
407
  const fMeta = await readJSON(path.join(featureDir(root, id), 'feature.json'), null);
387
408
  if (fMeta?.type === 'app') {
388
409
  if (!fMeta.src) errors.push(`${id}: app feature has no "src"`);
389
- else if (!(await exists(path.join(root, fMeta.src)))) errors.push(`${id}: app src not found: ${fMeta.src}`);
410
+ else if (!(await exists(appSrcPath(root, fMeta.src)))) errors.push(`${id}: app src not found: ${fMeta.src}`);
390
411
  if (fMeta.driver && !(await exists(path.join(featureDir(root, id), fMeta.driver)))) {
391
412
  errors.push(`${id}: driver not found: features/${id}/${fMeta.driver}`);
392
413
  }
package/core/serve.js CHANGED
@@ -97,12 +97,12 @@ export function startServer({ root, engineDir, port = 4600, quiet = false }) {
97
97
 
98
98
  try {
99
99
  if (p === '/' || p === '/index.html') {
100
- send(res, 302, '', { Location: '/engine/index.html' });
100
+ send(res, 302, '', { Location: '/screenbook/' });
101
101
  return;
102
102
  }
103
103
 
104
104
  if (p === '/__sb/health') {
105
- send(res, 200, JSON.stringify({ screenbook: true, version: '0.1.0', root: path.basename(root) }),
105
+ send(res, 200, JSON.stringify({ screenbook: true, version: '0.2.1', root: path.basename(root) }),
106
106
  { 'Content-Type': 'application/json' });
107
107
  return;
108
108
  }
@@ -147,16 +147,31 @@ export function startServer({ root, engineDir, port = 4600, quiet = false }) {
147
147
  return;
148
148
  }
149
149
 
150
- /* /engine/* always resolves inside the engine package works even
151
- when the project has no engine/ copy (examples, dev checkouts). */
152
- if (p === '/engine' || p.startsWith('/engine/')) {
153
- const sub = p === '/engine' ? '/index.html' : p.slice('/engine'.length);
154
- const abs = safeJoin(engineDir, sub === '/' ? '/index.html' : sub);
150
+ /* When the root is a .screenbook/ folder, the user's real project sits
151
+ one level up mount it at /project/ so embedded apps (app mode) are
152
+ reachable same-origin without any copying. */
153
+ const parentMount = path.basename(root) === '.screenbook' ? path.dirname(root) : null;
154
+ if (parentMount && (p === '/project' || p.startsWith('/project/'))) {
155
+ const sub = p === '/project' ? '/index.html' : p.slice('/project'.length);
156
+ const abs = safeJoin(parentMount, sub === '/' ? '/index.html' : sub);
155
157
  if (!abs) return send(res, 403, 'forbidden');
156
158
  await serveFile(res, abs);
157
159
  return;
158
160
  }
159
161
 
162
+ /* The studio runtime is served from THIS installed package at
163
+ /screenbook/* — nothing is ever copied into user projects.
164
+ /engine/* remains as a compatibility alias for older screens. */
165
+ for (const prefix of ['/screenbook', '/engine']) {
166
+ if (p === prefix || p.startsWith(prefix + '/')) {
167
+ const sub = p === prefix ? '/index.html' : p.slice(prefix.length);
168
+ const abs = safeJoin(engineDir, sub === '/' ? '/index.html' : sub);
169
+ if (!abs) return send(res, 403, 'forbidden');
170
+ await serveFile(res, abs);
171
+ return;
172
+ }
173
+ }
174
+
160
175
  const abs = safeJoin(root, p);
161
176
  if (!abs) return send(res, 403, 'forbidden');
162
177
  await serveFile(res, abs);
package/docs/README.md CHANGED
@@ -3,19 +3,30 @@
3
3
  Start here. Each guide is short, self-contained, and ordered by how you'll actually meet
4
4
  the tool.
5
5
 
6
+ ## Guides — using ScreenBook
7
+
6
8
  | Guide | Read it when |
7
9
  |-------|--------------|
8
- | [Getting started](getting-started.md) | First contact — prerequisites, install, your first project in five minutes |
9
- | [The studio](studio-guide.md) | A tour of every control in the shell: sidebar, dropdowns, flows, review, Theme Lab, self-test |
10
- | [Authoring](authoring-guide.md) | You're building screens by hand — the full contract for screens, data, scenarios, themes, flows, strings |
11
- | [CLI reference](cli.md) | Every command and flag |
12
- | [AI agents & MCP](mcp-agents.md) | Hooking ScreenBook to Claude Code (or any MCP client) so agents author screens for you |
13
- | [App mode](app-mode-guide.md) | Embedding an EXISTING single-page app instead of (or alongside) file-per-screen features |
14
- | [Troubleshooting](troubleshooting.md) | Something's red, blank, or missing |
15
-
16
- Two more references live at the repo root:
17
-
18
- - **[CLAUDE.md](../CLAUDE.md)** the authoring contract written for AI agents (scaffolded
19
- into every project by `init`; humans are welcome to read it too).
20
- - **[TESTING.md](../TESTING.md)** — the engine's own test map, if you're contributing to
21
- ScreenBook itself.
10
+ | [Getting started](guides/getting-started.md) | First contact — prerequisites, install, your first project in five minutes |
11
+ | [The studio](guides/studio.md) | A tour of every control in the shell: sidebar, dropdowns, flows, review, Theme Lab, self-test |
12
+ | [Authoring](guides/authoring.md) | You're building screens by hand — the full contract for screens, data, scenarios, themes, flows, strings |
13
+ | [App mode](guides/app-mode.md) | Embedding an EXISTING single-page app instead of (or alongside) file-per-screen features |
14
+ | [Troubleshooting](guides/troubleshooting.md) | Something's red, blank, or missing |
15
+
16
+ ## Reference looking things up
17
+
18
+ | Reference | Covers |
19
+ |-----------|--------|
20
+ | [CLI](reference/cli.md) | Every command and flag: `init` · `serve` · `manifest` · `validate` · `screenshot` · `app add` · `mcp` |
21
+ | [AI agents & MCP](reference/mcp-agents.md) | Hooking ScreenBook to Claude Code (or any MCP client) so agents author screens for you |
22
+
23
+ ## Contributing — hacking on ScreenBook itself
24
+
25
+ | Document | Covers |
26
+ |----------|--------|
27
+ | [Architecture](contributing/architecture.md) | How the engine works: the shell, the bridge protocol, the core, CLI, MCP server, and the self-test |
28
+ | [Testing map](contributing/testing.md) | Every test layer, what it covers, how to run it, and where the gaps are |
29
+
30
+ One more reference lives at the repo root: **[AGENTS.md](../AGENTS.md)** — the authoring
31
+ contract written for AI agents (scaffolded into every project by `init`; humans are welcome
32
+ to read it too).
Binary file
Binary file
Binary file
Binary file