@autono/create-open-pages 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.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yiwei Ho (open-slide)
4
+ Copyright (c) 2026 Autono Holdings Inc. (open-pdf modifications)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @autono/create-open-pages
2
+
3
+ Scaffold a workspace for [open-pages](https://openpages.sh) — the web page framework built for agents. Your coding agent writes pages as React components or plain HTML; you get a live browser preview, click-to-comment, and static export to any host.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create @autono/open-pages@latest my-pages
9
+ cd my-pages
10
+ npm run dev
11
+ ```
12
+
13
+ This creates a workspace containing:
14
+
15
+ - `pages/getting-started/` — a starter page you can edit or delete.
16
+ - `package.json` — depends on `@autono/open-pages`, which provides the runtime (viewer, inspector, export) and the `open-pages` CLI.
17
+ - `open-pages.config.ts` — optional typed config (pagesDir, port, base).
18
+ - `.claude/skills/` and `.agents/skills/` — agent skills (`create-page`, `page-authoring`, `apply-comments`, `create-theme`, `current-page`).
19
+ - `AGENTS.md` (linked as `CLAUDE.md`) — agent guide for authoring pages.
20
+
21
+ You won't see any Vite, React, Tailwind, or tsconfig files in the workspace. They live inside `@autono/open-pages` and you never touch them.
22
+
23
+ ## Flags
24
+
25
+ | Flag | Description |
26
+ | --- | --- |
27
+ | `init [dir]` | Scaffold into `dir` (defaults to the current directory). |
28
+ | `-f, --force` | Scaffold into a non-empty directory. |
29
+ | `-n, --name <name>` | Override the generated `package.json` name. |
30
+ | `--use-npm` / `--use-pnpm` / `--use-yarn` / `--use-bun` | Pick the package manager for the install step. |
31
+ | `--no-install` | Skip dependency installation. |
32
+ | `--no-git` | Skip git init and the initial commit. |
33
+
34
+ ## The loop
35
+
36
+ 1. Ask your agent to "make a landing page for X" — the `create-page` skill writes the React.
37
+ 2. `npm run dev` shows the live preview at `http://localhost:5173/p/<id>`; every save hot-reloads.
38
+ 3. Press `i`, click anything, leave a note — it lands in the source as a marker.
39
+ 4. The agent runs `apply-comments`; `open-pages export <id>` writes a deployable static folder.
40
+
41
+ Full documentation: [docs.openpages.sh](https://docs.openpages.sh)
42
+
43
+ ## License
44
+
45
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import { cp, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { Command } from "commander";
7
+ import prompts from "prompts";
8
+ import { spawn } from "node:child_process";
9
+ import { existsSync } from "node:fs";
10
+
11
+ //#region src/git.ts
12
+ const IS_WINDOWS$1 = process.platform === "win32";
13
+ async function run$1(cmd, args, cwd) {
14
+ return new Promise((resolve$1, reject) => {
15
+ const child = spawn(cmd, args, {
16
+ cwd,
17
+ stdio: [
18
+ "ignore",
19
+ "pipe",
20
+ "pipe"
21
+ ],
22
+ shell: IS_WINDOWS$1
23
+ });
24
+ let stdout = "";
25
+ let stderr = "";
26
+ child.stdout?.on("data", (d) => {
27
+ stdout += d.toString();
28
+ });
29
+ child.stderr?.on("data", (d) => {
30
+ stderr += d.toString();
31
+ });
32
+ child.on("error", reject);
33
+ child.on("close", (code) => resolve$1({
34
+ code,
35
+ stdout,
36
+ stderr
37
+ }));
38
+ });
39
+ }
40
+ async function isGitAvailable() {
41
+ try {
42
+ const res = await run$1("git", ["--version"], process.cwd());
43
+ return res.code === 0;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ async function isInsideWorkTree(cwd) {
49
+ try {
50
+ const res = await run$1("git", ["rev-parse", "--is-inside-work-tree"], cwd);
51
+ return res.code === 0 && res.stdout.trim() === "true";
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+ async function gitInitAndCommit(target) {
57
+ if (!await isGitAvailable()) return {
58
+ status: "skipped-no-git",
59
+ message: "git binary not found on PATH"
60
+ };
61
+ if (await isInsideWorkTree(target)) return {
62
+ status: "skipped-nested",
63
+ message: "target is already inside a git work tree; leaving parent repo alone"
64
+ };
65
+ const init$1 = await run$1("git", ["init"], target);
66
+ if (init$1.code !== 0) return {
67
+ status: "failed",
68
+ message: `git init failed: ${init$1.stderr.trim() || init$1.stdout.trim()}`
69
+ };
70
+ const add = await run$1("git", ["add", "-A"], target);
71
+ if (add.code !== 0) return {
72
+ status: "failed",
73
+ message: `git add failed: ${add.stderr.trim() || add.stdout.trim()}`
74
+ };
75
+ const commit = await run$1("git", [
76
+ "commit",
77
+ "-m",
78
+ "chore: init open-pages project"
79
+ ], target);
80
+ if (commit.code !== 0) return {
81
+ status: "failed",
82
+ message: `git commit failed: ${commit.stderr.trim() || commit.stdout.trim()}`
83
+ };
84
+ return { status: "committed" };
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/init.ts
89
+ const HERE = dirname(fileURLToPath(import.meta.url));
90
+ const TEMPLATE_DIR = resolve(HERE, "..", "template");
91
+ const IS_WINDOWS = process.platform === "win32";
92
+ function sanitizeDirName(value) {
93
+ const trimmed = value.trim();
94
+ if (trimmed === "." || trimmed === "..") return trimmed;
95
+ const cleaned = trimmed.replace(/\s+/g, "-").replace(/[^\\\p{L}\p{N}_./-]/gu, "-").replace(/-+/g, "-").replace(/(^-|-$)/g, "").replace(/-*([/\\])-*/g, "$1");
96
+ if (cleaned === "" || /^[/\\]+$/.test(cleaned)) return "my-pages";
97
+ return cleaned;
98
+ }
99
+ async function isDirNonEmpty(target) {
100
+ if (!existsSync(target)) return false;
101
+ const entries = await readdir(target);
102
+ return entries.some((e) => !e.startsWith("."));
103
+ }
104
+ function coreVersionRange() {
105
+ return `^0.1.0`;
106
+ }
107
+ async function linkOrCopy(relSrc, dst) {
108
+ await rm(dst, {
109
+ recursive: true,
110
+ force: true
111
+ });
112
+ if (IS_WINDOWS) {
113
+ await cp(resolve(dirname(dst), relSrc), dst, { recursive: true });
114
+ return;
115
+ }
116
+ await symlink(relSrc, dst);
117
+ }
118
+ async function materializeTemplateLinks(target) {
119
+ const claudeMd = join(target, "CLAUDE.md");
120
+ if (!existsSync(claudeMd) && existsSync(join(target, "AGENTS.md"))) await linkOrCopy("AGENTS.md", claudeMd);
121
+ const agentsSkills = join(target, ".agents", "skills");
122
+ if (!existsSync(agentsSkills)) return;
123
+ const claudeSkills = join(target, ".claude", "skills");
124
+ await mkdir(claudeSkills, { recursive: true });
125
+ const entries = await readdir(agentsSkills, { withFileTypes: true });
126
+ for (const entry of entries) {
127
+ if (!entry.isDirectory()) continue;
128
+ await linkOrCopy(join("..", "..", ".agents", "skills", entry.name), join(claudeSkills, entry.name));
129
+ }
130
+ }
131
+ async function runInstall(pm, cwd) {
132
+ await new Promise((res, rej) => {
133
+ const child = spawn(pm, ["install"], {
134
+ cwd,
135
+ stdio: "inherit",
136
+ shell: IS_WINDOWS
137
+ });
138
+ child.on("error", rej);
139
+ child.on("close", (code) => code === 0 ? res() : rej(new Error(`${pm} install exited with code ${code}`)));
140
+ });
141
+ }
142
+ async function init(opts) {
143
+ const { dir, force, name, packageManager, install, git } = opts;
144
+ if (!existsSync(TEMPLATE_DIR)) throw new Error(`Template missing at ${TEMPLATE_DIR}. If you are running from source, run \`pnpm --filter @autono/create-open-pages build\` first.`);
145
+ const target = resolve(process.cwd(), dir);
146
+ await mkdir(target, { recursive: true });
147
+ if (await isDirNonEmpty(target) && !force) throw new Error(`Target ${target} is not empty. Pass --force to scaffold into it anyway.`);
148
+ await cp(TEMPLATE_DIR, target, { recursive: true });
149
+ await materializeTemplateLinks(target);
150
+ const pkgPath = join(target, "package.json");
151
+ if (existsSync(pkgPath)) {
152
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
153
+ pkg.name = name ?? basename(target);
154
+ pkg.version = "0.0.0";
155
+ pkg.private = true;
156
+ if (pkg.dependencies?.["@autono/open-pages"]) pkg.dependencies["@autono/open-pages"] = coreVersionRange();
157
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
158
+ }
159
+ await writeFile(join(target, ".gitignore"), "node_modules\ndist\nexport\n.DS_Store\n");
160
+ const cdTarget = dir === "." ? basename(target) : dir;
161
+ process.stdout.write(`\n${chalk.green.bold("✔ Created open-pages workspace")} ${chalk.dim(`in ${target}`)}\n`);
162
+ let installed = false;
163
+ if (install) {
164
+ process.stdout.write(`\n${chalk.bold(`Installing dependencies with ${packageManager}…`)}\n\n`);
165
+ try {
166
+ await runInstall(packageManager, target);
167
+ installed = true;
168
+ } catch (err) {
169
+ const msg = err instanceof Error ? err.message : String(err);
170
+ process.stdout.write(`\n${chalk.yellow("! Dependency install failed:")} ${chalk.dim(msg)}\n` + chalk.dim(` You can retry manually with \`${packageManager} install\`.\n`));
171
+ }
172
+ }
173
+ if (git) {
174
+ const result = await gitInitAndCommit(target);
175
+ if (result.status === "committed") process.stdout.write(`${chalk.green("✔")} Initialized git repository with first commit.\n`);
176
+ else if (result.status === "skipped-nested") process.stdout.write(`${chalk.yellow("!")} Skipped ${chalk.bold("git init")}: ${chalk.dim(result.message ?? "")}\n`);
177
+ else if (result.status === "skipped-no-git") process.stdout.write(`${chalk.yellow("!")} Skipped git setup: ${chalk.dim(result.message ?? "")}\n`);
178
+ else process.stdout.write(`${chalk.yellow("!")} Git setup failed: ${chalk.dim(result.message ?? "")}\n` + chalk.dim(" You can initialize the repo manually.\n"));
179
+ }
180
+ process.stdout.write(`\n${chalk.bold("Next steps:")}\n`);
181
+ process.stdout.write(` ${chalk.cyan(`cd ${cdTarget}`)}\n`);
182
+ if (!installed && install) process.stdout.write(` ${chalk.cyan(`${packageManager} install`)}\n`);
183
+ else if (!install) process.stdout.write(` ${chalk.cyan(`${packageManager} install`)} ${chalk.dim("# install was skipped")}\n`);
184
+ const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
185
+ process.stdout.write(` ${chalk.cyan(devCommand)}\n`);
186
+ }
187
+
188
+ //#endregion
189
+ //#region src/package-manager.ts
190
+ const PACKAGE_MANAGERS = [
191
+ "npm",
192
+ "pnpm",
193
+ "yarn",
194
+ "bun"
195
+ ];
196
+ function detectPackageManager() {
197
+ const ua = process.env.npm_config_user_agent ?? "";
198
+ if (ua.startsWith("pnpm")) return "pnpm";
199
+ if (ua.startsWith("yarn")) return "yarn";
200
+ if (ua.startsWith("bun")) return "bun";
201
+ return "npm";
202
+ }
203
+
204
+ //#endregion
205
+ //#region src/index.ts
206
+ async function readVersion() {
207
+ const here = dirname(fileURLToPath(import.meta.url));
208
+ const pkg = JSON.parse(await readFile(join(here, "..", "package.json"), "utf8"));
209
+ return pkg.version;
210
+ }
211
+ function onCancel() {
212
+ process.stdout.write(chalk.dim("\nCancelled.\n"));
213
+ process.exit(130);
214
+ }
215
+ function packageManagerFromFlags(flags) {
216
+ const picks = [];
217
+ if (flags.useNpm) picks.push("npm");
218
+ if (flags.usePnpm) picks.push("pnpm");
219
+ if (flags.useYarn) picks.push("yarn");
220
+ if (flags.useBun) picks.push("bun");
221
+ if (picks.length > 1) throw new Error(`Only one of --use-npm / --use-pnpm / --use-yarn / --use-bun may be specified (got ${picks.map((p) => `--use-${p}`).join(", ")}).`);
222
+ return picks[0];
223
+ }
224
+ async function runInit(dirArg, flags) {
225
+ const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
226
+ let dir = dirArg;
227
+ const name = flags.name;
228
+ let force = flags.force ?? false;
229
+ let packageManager = packageManagerFromFlags(flags);
230
+ if (isTTY && dir === void 0) {
231
+ const answers = await prompts({
232
+ type: "text",
233
+ name: "dir",
234
+ message: "Target directory",
235
+ initial: "."
236
+ }, { onCancel });
237
+ dir = answers.dir;
238
+ }
239
+ if (dir !== void 0) {
240
+ const safe = sanitizeDirName(dir);
241
+ if (safe !== dir) {
242
+ if (!isTTY) throw new Error(`Target directory "${dir}" contains characters that break shell commands (spaces, quotes, etc.). Try "${safe}" instead.`);
243
+ process.stdout.write(`${chalk.yellow("!")} ${chalk.bold(`"${dir}"`)} has characters that confuse shells.\n Suggested: ${chalk.cyan(`"${safe}"`)}\n`);
244
+ const answers = await prompts({
245
+ type: "text",
246
+ name: "dir",
247
+ message: "Directory name",
248
+ initial: safe
249
+ }, { onCancel });
250
+ dir = sanitizeDirName(answers.dir ?? safe);
251
+ }
252
+ }
253
+ if (isTTY && packageManager === void 0 && flags.install !== false) {
254
+ const detected = detectPackageManager();
255
+ const answers = await prompts({
256
+ type: "select",
257
+ name: "packageManager",
258
+ message: "Package manager",
259
+ choices: PACKAGE_MANAGERS.map((pm) => ({
260
+ title: pm,
261
+ value: pm
262
+ })),
263
+ initial: PACKAGE_MANAGERS.indexOf(detected)
264
+ }, { onCancel });
265
+ packageManager = answers.packageManager;
266
+ }
267
+ const resolvedDir = dir ?? ".";
268
+ const target = resolve(process.cwd(), resolvedDir);
269
+ if (!force && await isDirNonEmpty(target)) {
270
+ if (!isTTY) throw new Error(`Target ${target} is not empty. Pass --force to scaffold into it anyway.`);
271
+ const { overwrite } = await prompts({
272
+ type: "confirm",
273
+ name: "overwrite",
274
+ message: `${chalk.yellow(target)} is not empty. Scaffold into it anyway?`,
275
+ initial: false
276
+ }, { onCancel });
277
+ if (!overwrite) {
278
+ process.stdout.write(chalk.dim("Aborted.\n"));
279
+ return;
280
+ }
281
+ force = true;
282
+ }
283
+ const opts = {
284
+ dir: resolvedDir,
285
+ force,
286
+ name,
287
+ packageManager: packageManager ?? detectPackageManager(),
288
+ install: flags.install !== false,
289
+ git: flags.git !== false
290
+ };
291
+ await init(opts);
292
+ }
293
+ async function run(argv) {
294
+ const version = await readVersion();
295
+ const program = new Command();
296
+ program.name("create-open-pages").description("Scaffold and manage open-pages workspaces.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `create-open-pages --help` for usage)"));
297
+ program.command("init", { isDefault: true }).description("Create a new open-pages workspace").argument("[dir]", "target directory", void 0).option("-f, --force", "overwrite non-empty target directory", false).option("-n, --name <name>", "override package name (defaults to folder name)").option("--use-npm", "use npm to install dependencies").option("--use-pnpm", "use pnpm to install dependencies").option("--use-yarn", "use yarn to install dependencies").option("--use-bun", "use bun to install dependencies").option("--no-install", "skip dependency installation").option("--no-git", "skip git init and initial commit").action(async (dir, flags) => {
298
+ await runInit(dir, flags);
299
+ });
300
+ await program.parseAsync(argv, { from: "user" });
301
+ }
302
+
303
+ //#endregion
304
+ //#region src/cli.ts
305
+ run(process.argv.slice(2)).catch((err) => {
306
+ const message = err instanceof Error ? err.message : String(err);
307
+ process.stderr.write(`${chalk.red("error:")} ${message}\n`);
308
+ process.exit(1);
309
+ });
310
+
311
+ //#endregion
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@autono/create-open-pages",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold an open-pages workspace — agent skills preconfigured, live real-PDF preview out of the box.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-open-pages": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "template",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "node scripts/sync-template-skills.mjs && tsdown",
16
+ "typecheck": "tsc --noEmit",
17
+ "sync:template-skills": "node scripts/sync-template-skills.mjs",
18
+ "prepack": "pnpm build"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": [
24
+ "pdf",
25
+ "pages",
26
+ "claude-code",
27
+ "agents",
28
+ "scaffold"
29
+ ],
30
+ "license": "MIT",
31
+ "author": {
32
+ "name": "autonoco",
33
+ "url": "https://github.com/autonoco"
34
+ },
35
+ "homepage": "https://openpages.sh",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/autonoco/open-pages.git",
39
+ "directory": "packages/cli"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/autonoco/open-pages/issues"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "chalk": "^6.0.0",
49
+ "commander": "^15.0.0",
50
+ "prompts": "^2.4.2"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.19.17",
54
+ "@types/prompts": "^2.4.9",
55
+ "tsdown": "^0.9.9",
56
+ "typescript": "^5.9.3"
57
+ }
58
+ }
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: apply-comments
3
+ description: Apply pending @page-comment markers written by the open-pages inspector tool. Use when the user asks to "apply comments", "process page comments", "apply the inspector comments", or references markers left inside `pages/<id>/index.tsx` (or its `components/*.tsx`).
4
+ ---
5
+
6
+ # Apply page comments
7
+
8
+ The open-pages viewer has an inspector that lets the user click any element on the live page and attach a textual comment (e.g. *"make this red"*, *"change to 'Open Pages Rocks'"*). Each comment is persisted as an in-source JSX marker inside the page's source — usually `pages/<pageId>/index.tsx`, occasionally a file under `pages/<pageId>/components/`.
9
+
10
+ Your job: read those markers, perform the described edits, and delete the markers.
11
+
12
+ > **Before making any page edit**, consult the **`page-authoring`** skill — it is the technical reference for how a page is structured (file contract, `className` styling, layout and responsive rules, type scale, interactivity). A comment like *"make this bigger"* or *"change the accent colour"* should be applied in a way that stays consistent with those rules and still works on mobile.
13
+
14
+ ## Marker format
15
+
16
+ ```
17
+ {/* @page-comment id="c-<8hex>" ts="<ISO>" text="<base64url(JSON)>" */}
18
+ ```
19
+
20
+ - Inserted as the **first child inside** the JSX element it refers to: a newline + indent + the marker, spliced immediately after the element's opening `>`. The marker is dropped *into* its target, not floated above it. **The marker does not necessarily end its line** — for an element that was written on one line (`<h1>Title</h1>`), the element's children and closing tag follow the marker on the same line.
21
+ - `text` is base64url-encoded JSON: `{"note": "...", "hint"?: "..."}`.
22
+ - Detection regex (authoritative — use exactly this):
23
+
24
+ ```
25
+ /\{\/\*\s*@page-comment\s+id="(c-[a-f0-9]+)"\s+ts="([^"]+)"\s+text="([A-Za-z0-9_\-]+={0,2})"\s*\*\/\}/g
26
+ ```
27
+
28
+ ## Procedure
29
+
30
+ 1. **Identify the target page(s).**
31
+ - If the user names one (`launch`, `pricing`, etc.), work on that single page's source files.
32
+ - If they say "all" or don't specify, scan every `pages/*/index.tsx` and `pages/*/components/*.tsx`. Process each page one at a time.
33
+
34
+ 2. **Read the file and find all markers.**
35
+ - Run the regex above against the whole file.
36
+ - For each match, base64url-decode `text` and `JSON.parse` it to get `{ note, hint? }`.
37
+ - Record each hit as `{ id, lineIndex (0-based), note, hint }`.
38
+ - If there are no markers, tell the user and stop.
39
+
40
+ 3. **Understand each comment in context.**
41
+ - The targeted JSX element is the **enclosing** element of the marker — i.e. read upward from the marker line until you reach the unclosed JSX opening tag whose body the marker lives in. That element is the target. (For self-closing elements like `<img />`, the inspector hoists the marker to the nearest non-self-closing ancestor; in that case the comment usually refers to a child of the enclosing element rather than the enclosing element itself — use the `note` text to disambiguate.)
42
+ - Read enough surrounding code (parent element, sibling elements, `className` strings, any state the element depends on) to apply the change faithfully. A comment inside a `<button>` with an `onClick` may be about behaviour, not looks.
43
+ - If the marker sits inside a `.map` body, the comment applies to the row template — every rendered row changes. If the user clearly meant one item ("make the *middle* one green"), change the data or add a per-item field rather than special-casing the JSX.
44
+ - If the `note` is ambiguous, do the smallest reasonable interpretation and mention the assumption in your summary.
45
+
46
+ 4. **Apply edits in reverse line order.**
47
+ - Sort markers by descending `lineIndex` and process one at a time, using the `Edit` tool.
48
+ - Processing top-down would invalidate line numbers for later markers as the file shrinks/grows.
49
+
50
+ 5. **Remove each marker after applying its edit.**
51
+ - Delete **only the marker text itself** — the `{/* @page-comment … */}` span matched by the detection regex — plus the newline and indentation immediately *before* it (the whitespace the inspector inserted). Never delete the whole line: children and the closing tag often share the marker's line, and removing the line destroys them.
52
+ - After removal, if the element is left split across two lines that were originally one (`<h1 …>\n Title</h1>`), it is fine to rejoin them, but not required.
53
+ - Never leave a marker behind for an edit you applied — that signals a failure. Markers deliberately skipped per the edge cases below stay in place.
54
+
55
+ 6. **Verify.**
56
+ - After all edits, re-read the file and confirm the only remaining markers are ones you reported as skipped.
57
+ - Confirm the edited JSX is well-formed (balanced tags, no dangling attributes) and that changed `className` strings are literal Tailwind utilities. If the project's `package.json` has typecheck/lint scripts, run them with the project's package manager; scaffolded projects ship neither TypeScript nor a linter — there, rely on the running dev server (or the `build` script) to surface compile errors. Fix any errors you introduced.
58
+ - For layout changes, mentally check the Mobile viewport (390px): did the edit introduce a fixed width or a grid with no stacking fallback?
59
+
60
+ 7. **Report.**
61
+ - Summarise: `N applied, M skipped` plus a one-line description of each change (including the page id).
62
+
63
+ ## base64url decoding helper
64
+
65
+ ```js
66
+ function decode(s) {
67
+ const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
68
+ return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64').toString('utf8');
69
+ }
70
+ ```
71
+
72
+ You can run this inline via `node -e '...'` if you need to inspect a payload; otherwise just reason about the decoded string.
73
+
74
+ ## Edge cases
75
+
76
+ - **Marker with no enclosing JSX element** (shouldn't happen — the inspector won't write one — but if you find one): delete it and note as orphan.
77
+ - **Multiple markers stacked on consecutive lines inside the same element**: they all refer to that enclosing element. Read their notes in source order to understand the combined intent, then apply and delete them bottom-up per step 4.
78
+ - **Comment asks for something outside the target element's scope** (e.g. "add a testimonials section"): do the closest-reasonable edit and mention the scope expansion in your summary.
79
+ - **Comment on a plain HTML page** (`pages/<id>/index.html`): the inspector does not run there, so no markers exist. Apply the user's request directly to the HTML.
80
+ - **Can't resolve the comment** (e.g. truly ambiguous, or the file changed shape such that the target element doesn't exist): leave the marker in place and report it as skipped. Don't guess.
81
+
82
+ ## Do not
83
+
84
+ - Do not touch `package.json`, `open-pages.config.ts`, or files outside `pages/`.
85
+ - Do not add dependencies.
86
+ - Do not re-introduce markers or leave `TODO` breadcrumbs — the user already has a record in git.
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: create-page
3
+ description: Use this skill when the user wants to create, build, draft, or generate a new web page, site, or landing page in this open-pages repo. Triggers on phrases like "make a landing page for X", "build a pricing page", "create a portfolio site", "new page", "a dashboard UI", "an HTML page", or when the user asks to add content under `pages/`. Do NOT use for editing the framework itself — only for authoring content inside `pages/<id>/`.
4
+ ---
5
+
6
+ # Create a page in open-pages
7
+
8
+ This skill owns the **workflow** for building a new page. The technical reference — file contract, Tailwind via `className`, layout and responsive rules, type scale, interactivity, assets — lives in the **`page-authoring`** skill. Read that skill whenever you need details on *how* a page is structured. This skill assumes you'll consult it before writing code.
9
+
10
+ You only write files under `pages/<id>/`. Never modify `package.json`, `open-pages.config.ts`, or existing pages.
11
+
12
+ ## Step 1 — Pick a theme
13
+
14
+ List files under `themes/`. If any theme markdown files exist (anything other than `README.md`), call `AskUserQuestion` with each theme id as an option plus a final **"no theme — design from scratch"** option. (`AskUserQuestion` holds at most 4 options — with 4+ themes, offer the 3 most topic-relevant plus "no theme"; the auto-added "Other" lets the user name any omitted theme.)
15
+
16
+ - If the user picks a theme: read `themes/<id>.md` end-to-end. The theme's palette, typography, and fixed components are now authoritative — copy them directly into the page. **Also set `theme: '<theme-id>'` on the `meta` export** so the page back-links to the theme. In Step 2, skip the **visual direction** question (the theme already commits to one); confirm the page's purpose itself before moving on. Sections and interactivity are independent of theme — ask those normally.
17
+ - If the user picks "no theme", or `themes/` contains no theme markdown files: proceed to Step 2 unchanged.
18
+
19
+ If you skip the visual-direction question because a theme was picked, restate the theme name in Step 2 so the user can correct course before you start writing.
20
+
21
+ ## Step 2 — Clarify requirements (MUST ask before writing code)
22
+
23
+ **Before writing any code, lock in the key decisions below via `AskUserQuestion`.** They shape every downstream choice, so locking them in up front avoids rework. Only skip a question when it's already unambiguously answered — by the user's original message, or by a theme picked in Step 1 — and if you skip, restate your assumption so they can correct it.
24
+
25
+ **Purpose comes first.** If the user's initial request is thin ("make me a page", "build a site"), make a *separate* `AskUserQuestion` call first to gather what the page is for, who lands on it, and what content they already have (copy, product names, prices, screenshots, links). Skip this only if already clear — then restate your reading so they can correct course.
26
+
27
+ Then ask these four in a single `AskUserQuestion` call (multi-question form):
28
+
29
+ 1. **Page type** — offer the closest fits: landing / marketing page, product or pricing page, dashboard or app UI, docs or long-form content page, portfolio or personal site, form or signup flow, internal tool. Mark the best fit "(Recommended)". This drives structure and how much interactivity to expect.
30
+
31
+ 2. **Visual direction** — propose 3 directions tailored to *this* page and its audience. Do **not** pull from a fixed preset list. Each option must combine a vibe word + a concrete visual cue (palette, weight, surfaces) so the user can picture it; bare labels like "modern" are too vague. The three options should feel meaningfully different.
32
+
33
+ How options should shift with page type:
34
+ - *SaaS landing page* → **dark launch** (near-black, one neon accent, large display type) · **clean product** (white, slate text, one indigo accent, soft cards) · **editorial** (warm off-white, serif display, generous whitespace)
35
+ - *Dashboard* → **ops console** (dense, slate surfaces, status colors) · **calm workspace** (airy, rounded cards, one accent) · **data-forward** (tables and charts as the spine, mono numerals)
36
+ - *Portfolio* → **gallery** (big imagery, minimal chrome) · **typographic** (type does the work, one accent) · **playful** (color blocks, rounded shapes, motion)
37
+
38
+ Mark the best fit "(Recommended)". (`AskUserQuestion` auto-adds "Other" — don't add a catch-all yourself.)
39
+
40
+ 3. **Sections / content** — offer bundles that fit the type, e.g. for a landing page: hero + features + pricing + FAQ + footer (Recommended); hero + social proof + CTA (short); hero + long-form explainer + CTA. The auto-added "Other" covers custom lists.
41
+
42
+ 4. **Interactivity** — offer: static (links only), light (tabs, toggles, accordions, a pricing switch), form (a contact/signup form that mirrors state locally or posts to a URL they supply), app-like (filters, local state, multiple views). Note that no backend exists — anything that needs one requires a URL from the user.
43
+
44
+ After those, ask follow-ups **only if still unclear**: brand colors, logo/screenshots, real copy (headlines, prices, names). Real pages live on real content — placeholder copy like "Acme, $10/mo" is a last resort; prefer asking for the real values. Responsive is not a question: every page must work at Desktop, Tablet (820px), and Mobile (390px).
45
+
46
+ ## Step 3 — Pick a page id
47
+
48
+ Use **kebab-case**, short, descriptive. Examples: `launch`, `pricing`, `status-board`, `founder-portfolio`, `waitlist`. Check `pages/` to avoid collisions.
49
+
50
+ ## Step 4 — Plan the structure
51
+
52
+ Sketch the page as an ordered list of sections before writing code. Common shapes:
53
+
54
+ | Section | Purpose |
55
+ | --- | --- |
56
+ | Header / nav | Wordmark, 3–5 links, one CTA; collapses on mobile |
57
+ | Hero | One headline, one lede, primary + secondary CTA, optional visual |
58
+ | Social proof | Logos, a metric row, one quote |
59
+ | Features | 3–6 cards or alternating rows, each one benefit |
60
+ | Pricing | 2–3 tiers, a billing toggle, clear CTA per tier |
61
+ | FAQ | Accordion or plain list |
62
+ | CTA band | Restate the ask before the footer |
63
+ | Footer | Links, legal line |
64
+ | App shell | Sidebar/topbar + content area for dashboards and tools |
65
+
66
+ **Rule of thumb:** one idea per section, one CTA per screen. If a section lists things, it wants a grid or a table, not a paragraph.
67
+
68
+ Decide the data shape now: which sections render from a typed const array (`.map`), which are explicit component instances — `page-authoring` explains why this matters for the inspector.
69
+
70
+ ## Step 5 — Commit to a visual direction
71
+
72
+ One palette, one type scale, held for the whole page. The constraints (web type scale, palette structure, contrast) live in `page-authoring` and its `references/typography-and-color.md` — apply them. Define the palette as repeated Tailwind utilities (or a small const map of class strings). Fonts: default to the system stack; load a Google Font or self-hosted file only when the user names one or a theme requires it (`references/assets-and-fonts.md`).
73
+
74
+ ## Step 6 — Write `pages/<id>/index.tsx`
75
+
76
+ Read the **`page-authoring`** skill before writing — file contract, `className` styling, layout and responsive rules, interactivity constraints, assets. Its file-contract example is the starter template. Split large pages into `pages/<id>/components/*.tsx` when a section is more than ~80 lines.
77
+
78
+ ## Step 7 — Self-review
79
+
80
+ Run the checklist in `page-authoring` ("Self-review before finishing"). Check all three viewports.
81
+
82
+ ## Step 8 — Hand off to the user
83
+
84
+ Tell the user:
85
+
86
+ - The page id and file path you created.
87
+ - The preview URL — `http://localhost:5173/p/<id>` — hot-reloads on every edit, with Desktop / Tablet / Mobile toggles and **Open** to view the page by itself.
88
+ - That they can hit **Inspect** (or `i`) in the preview, click any element, and leave comments — then ask you to run `apply-comments`.
89
+ - That `open-pages export <id>` writes `export/<id>/` — a static folder (index.html + assets) they can deploy to Netlify, Vercel, Cloudflare Pages, GitHub Pages, or any static host.
90
+ - If dev isn't running: run the project's `dev` script from the project root with its package manager (`npm run dev`, `pnpm dev`, … — match the lockfile).
91
+
92
+ Don't run the dev server yourself unless asked.