@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.
- package/AGENTS.md +147 -0
- package/CLAUDE.md +4 -184
- package/README.md +146 -163
- package/bridge.js +16 -2
- package/cli/postinstall.js +34 -0
- package/cli/screenbook.js +84 -34
- package/cli/templates.js +54 -1
- package/core/browser.js +22 -7
- package/core/project.js +28 -7
- package/core/serve.js +22 -7
- package/docs/README.md +25 -14
- package/docs/brand/logo-mark.png +0 -0
- package/docs/brand/logo-transparent.png +0 -0
- package/docs/brand/logo.png +0 -0
- package/docs/brand/readme-header.png +0 -0
- package/docs/contributing/architecture.md +153 -0
- package/{TESTING.md → docs/contributing/testing.md} +13 -8
- package/docs/{app-mode-guide.md → guides/app-mode.md} +17 -10
- package/docs/{authoring-guide.md → guides/authoring.md} +43 -17
- package/docs/guides/getting-started.md +84 -0
- package/docs/{studio-guide.md → guides/studio.md} +3 -2
- package/docs/{troubleshooting.md → guides/troubleshooting.md} +11 -8
- package/docs/{cli.md → reference/cli.md} +15 -10
- package/docs/{mcp-agents.md → reference/mcp-agents.md} +3 -3
- package/docs/shots/android-dark.png +0 -0
- package/docs/shots/android-light.png +0 -0
- package/docs/shots/app-mode-dark.png +0 -0
- package/docs/shots/app-mode-light.png +0 -0
- package/docs/shots/hero-studio-dark.png +0 -0
- package/docs/shots/hero-studio-light.png +0 -0
- package/docs/shots/review-mode-dark.png +0 -0
- package/docs/shots/review-mode-light.png +0 -0
- package/docs/shots/rtl-arabic-dark.png +0 -0
- package/docs/shots/rtl-arabic-light.png +0 -0
- package/docs/shots/theme-lab-dark.png +0 -0
- package/docs/shots/theme-lab-light.png +0 -0
- package/docs/shots/theming-noir-dark.png +0 -0
- package/docs/shots/theming-noir-light.png +0 -0
- package/docs/shots/web-dashboard-dark.png +0 -0
- package/docs/shots/web-dashboard-light.png +0 -0
- package/examples/qahwa/features/order/screens/010-menu.html +1 -1
- package/examples/qahwa/features/order/screens/020-drink.html +1 -1
- package/examples/qahwa/features/order/screens/030-cart.html +1 -1
- package/examples/qahwa/features/order/screens/040-status.html +1 -1
- package/examples/qahwa/features/order/screens/050-status-android.html +1 -1
- package/examples/qahwa/features/roastery/screens/010-orders.html +1 -1
- package/examples/qahwa/features/roastery/screens/020-menu-editor.html +1 -1
- package/examples/spa-demo/app/index.html +1 -1
- package/examples/tokens-lab/features/lab/screens/010-swatches.html +1 -1
- package/examples/tokens-lab/features/lab/screens/020-typography.html +1 -1
- package/mcp/server.js +4 -2
- package/package.json +7 -6
- package/shell/screenhost.js +4 -1
- package/shell/selftest.js +18 -2
- package/shell/shell.css +17 -9
- package/shell/store.js +2 -0
- package/skill/SKILL.md +1 -1
- package/skill/references/cli-mcp.md +1 -1
- package/skill/references/screen-contract.md +1 -1
- package/docs/getting-started.md +0 -88
- package/docs/shots/app-mode.png +0 -0
- package/docs/shots/hero-studio.png +0 -0
- package/docs/shots/rtl-arabic.png +0 -0
- package/docs/shots/theme-lab.png +0 -0
- package/docs/shots/theming-noir.png +0 -0
- 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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
/*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
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
|
|
88
|
-
embedded app
|
|
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(
|
|
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
|
|
101
|
-
console.log(`
|
|
102
|
-
console.log(`
|
|
103
|
-
|
|
104
|
-
|
|
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 + '/
|
|
127
|
-
console.log(` selftest ${dim(srv.url + '/
|
|
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
|
-
|
|
209
|
-
|
|
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} —
|
|
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('
|
|
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
|
-
|
|
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="../../../
|
|
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
|
|
25
|
-
`
|
|
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}/
|
|
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}/
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
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(
|
|
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: '/
|
|
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
|
|
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
|
-
/*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
10
|
-
| [Authoring](authoring
|
|
11
|
-
| [
|
|
12
|
-
| [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|