@autono/create-open-pages 0.5.0 → 0.7.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/dist/cli.js CHANGED
@@ -1,17 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import * as p from "@clack/prompts";
3
+ import { S_ERROR } from "@clack/prompts";
2
4
  import chalk from "chalk";
3
5
  import { cp, mkdir, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises";
4
- import { basename, dirname, join, resolve } from "node:path";
6
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import { Command } from "commander";
7
- import prompts from "prompts";
8
9
  import { spawn } from "node:child_process";
10
+ import { createHash } from "node:crypto";
9
11
  import { existsSync } from "node:fs";
10
-
11
12
  //#region src/git.ts
12
13
  const IS_WINDOWS$1 = process.platform === "win32";
13
14
  async function run$1(cmd, args, cwd) {
14
- return new Promise((resolve$1, reject) => {
15
+ return new Promise((resolve, reject) => {
15
16
  const child = spawn(cmd, args, {
16
17
  cwd,
17
18
  stdio: [
@@ -30,7 +31,7 @@ async function run$1(cmd, args, cwd) {
30
31
  stderr += d.toString();
31
32
  });
32
33
  child.on("error", reject);
33
- child.on("close", (code) => resolve$1({
34
+ child.on("close", (code) => resolve({
34
35
  code,
35
36
  stdout,
36
37
  stderr
@@ -39,8 +40,7 @@ async function run$1(cmd, args, cwd) {
39
40
  }
40
41
  async function isGitAvailable() {
41
42
  try {
42
- const res = await run$1("git", ["--version"], process.cwd());
43
- return res.code === 0;
43
+ return (await run$1("git", ["--version"], process.cwd())).code === 0;
44
44
  } catch {
45
45
  return false;
46
46
  }
@@ -62,10 +62,10 @@ async function gitInitAndCommit(target) {
62
62
  status: "skipped-nested",
63
63
  message: "target is already inside a git work tree; leaving parent repo alone"
64
64
  };
65
- const init$1 = await run$1("git", ["init"], target);
66
- if (init$1.code !== 0) return {
65
+ const init = await run$1("git", ["init"], target);
66
+ if (init.code !== 0) return {
67
67
  status: "failed",
68
- message: `git init failed: ${init$1.stderr.trim() || init$1.stdout.trim()}`
68
+ message: `git init failed: ${init.stderr.trim() || init.stdout.trim()}`
69
69
  };
70
70
  const add = await run$1("git", ["add", "-A"], target);
71
71
  if (add.code !== 0) return {
@@ -83,7 +83,6 @@ async function gitInitAndCommit(target) {
83
83
  };
84
84
  return { status: "committed" };
85
85
  }
86
-
87
86
  //#endregion
88
87
  //#region src/init.ts
89
88
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -98,11 +97,10 @@ function sanitizeDirName(value) {
98
97
  }
99
98
  async function isDirNonEmpty(target) {
100
99
  if (!existsSync(target)) return false;
101
- const entries = await readdir(target);
102
- return entries.some((e) => !e.startsWith("."));
100
+ return (await readdir(target)).some((e) => !e.startsWith("."));
103
101
  }
104
102
  function coreVersionRange() {
105
- return `^0.5.0`;
103
+ return `^0.7.0`;
106
104
  }
107
105
  async function linkOrCopy(relSrc, dst) {
108
106
  await rm(dst, {
@@ -128,25 +126,40 @@ async function materializeTemplateLinks(target) {
128
126
  await linkOrCopy(join("..", "..", ".agents", "skills", entry.name), join(claudeSkills, entry.name));
129
127
  }
130
128
  }
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
- });
129
+ async function writeUiManifest(target) {
130
+ const manifest = {};
131
+ for (const entry of [
132
+ "ui",
133
+ "lib",
134
+ "hooks"
135
+ ]) {
136
+ const root = join(target, entry);
137
+ if (!existsSync(root)) continue;
138
+ for (const rel of await collectFiles(root)) {
139
+ const wsRel = join(entry, rel).split(sep).join("/");
140
+ manifest[wsRel] = createHash("sha256").update(await readFile(join(root, rel))).digest("hex");
141
+ }
142
+ }
143
+ const sorted = Object.fromEntries(Object.entries(manifest).sort(([a], [b]) => a.localeCompare(b)));
144
+ await writeFile(join(target, "ui", ".sync-manifest.json"), `${JSON.stringify(sorted, null, 2)}\n`);
141
145
  }
142
- async function init(opts) {
143
- const { dir, force, name, packageManager, install, git } = opts;
146
+ async function collectFiles(dir, prefix = "") {
147
+ const out = [];
148
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
149
+ const rel = prefix ? join(prefix, entry.name) : entry.name;
150
+ if (entry.isDirectory()) out.push(...await collectFiles(join(dir, entry.name), rel));
151
+ else if (entry.isFile()) out.push(rel);
152
+ }
153
+ return out;
154
+ }
155
+ async function scaffold(opts) {
156
+ const { target, force, name } = opts;
144
157
  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
158
  await mkdir(target, { recursive: true });
147
159
  if (await isDirNonEmpty(target) && !force) throw new Error(`Target ${target} is not empty. Pass --force to scaffold into it anyway.`);
148
160
  await cp(TEMPLATE_DIR, target, { recursive: true });
149
161
  await materializeTemplateLinks(target);
162
+ await writeUiManifest(target);
150
163
  const pkgPath = join(target, "package.json");
151
164
  if (existsSync(pkgPath)) {
152
165
  const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
@@ -157,34 +170,38 @@ async function init(opts) {
157
170
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
158
171
  }
159
172
  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
173
  }
187
-
174
+ function installDependencies(pm, cwd) {
175
+ return new Promise((res) => {
176
+ let output = "";
177
+ const child = spawn(pm, ["install"], {
178
+ cwd,
179
+ stdio: [
180
+ "ignore",
181
+ "pipe",
182
+ "pipe"
183
+ ],
184
+ shell: IS_WINDOWS
185
+ });
186
+ child.stdout?.on("data", (chunk) => {
187
+ output += chunk.toString();
188
+ });
189
+ child.stderr?.on("data", (chunk) => {
190
+ output += chunk.toString();
191
+ });
192
+ child.on("error", (err) => res({
193
+ ok: false,
194
+ output: err.message
195
+ }));
196
+ child.on("close", (code) => {
197
+ if (code === 0) res({ ok: true });
198
+ else res({
199
+ ok: false,
200
+ output: output.trim() || `${pm} install exited with code ${code}`
201
+ });
202
+ });
203
+ });
204
+ }
188
205
  //#endregion
189
206
  //#region src/package-manager.ts
190
207
  const PACKAGE_MANAGERS = [
@@ -200,17 +217,18 @@ function detectPackageManager() {
200
217
  if (ua.startsWith("bun")) return "bun";
201
218
  return "npm";
202
219
  }
203
-
204
220
  //#endregion
205
221
  //#region src/index.ts
206
222
  async function readVersion() {
207
223
  const here = dirname(fileURLToPath(import.meta.url));
208
- const pkg = JSON.parse(await readFile(join(here, "..", "package.json"), "utf8"));
209
- return pkg.version;
224
+ return JSON.parse(await readFile(join(here, "..", "package.json"), "utf8")).version;
210
225
  }
211
- function onCancel() {
212
- process.stdout.write(chalk.dim("\nCancelled.\n"));
213
- process.exit(130);
226
+ function unwrap(value) {
227
+ if (p.isCancel(value)) {
228
+ p.cancel("Cancelled.");
229
+ process.exit(130);
230
+ }
231
+ return value;
214
232
  }
215
233
  function packageManagerFromFlags(flags) {
216
234
  const picks = [];
@@ -218,94 +236,134 @@ function packageManagerFromFlags(flags) {
218
236
  if (flags.usePnpm) picks.push("pnpm");
219
237
  if (flags.useYarn) picks.push("yarn");
220
238
  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(", ")}).`);
239
+ if (picks.length > 1) throw new Error(`Only one of --use-npm / --use-pnpm / --use-yarn / --use-bun may be specified (got ${picks.map((pm) => `--use-${pm}`).join(", ")}).`);
222
240
  return picks[0];
223
241
  }
242
+ function displayPath(target) {
243
+ const rel = relative(process.cwd(), target);
244
+ if (rel === "" || rel.startsWith("..")) return target;
245
+ return rel;
246
+ }
247
+ function step(label, isTTY) {
248
+ const spinner = isTTY ? p.spinner() : void 0;
249
+ if (spinner) spinner.start(label);
250
+ else p.log.step(label);
251
+ return {
252
+ done(message) {
253
+ if (spinner) spinner.stop(message);
254
+ else p.log.step(message);
255
+ },
256
+ fail(message) {
257
+ if (spinner) spinner.error(message);
258
+ else p.log.error(message);
259
+ }
260
+ };
261
+ }
262
+ function tail(output, lines = 12) {
263
+ return output.split("\n").slice(-lines).join("\n");
264
+ }
224
265
  async function runInit(dirArg, flags) {
225
266
  const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
267
+ let packageManager = packageManagerFromFlags(flags);
226
268
  let dir = dirArg;
227
- const name = flags.name;
228
269
  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
- }
270
+ const install = flags.install !== false;
271
+ const git = flags.git !== false;
272
+ if (isTTY && dir === void 0) dir = unwrap(await p.text({
273
+ message: "Where should we create your workspace?",
274
+ placeholder: ".",
275
+ defaultValue: "."
276
+ }));
239
277
  if (dir !== void 0) {
240
278
  const safe = sanitizeDirName(dir);
241
279
  if (safe !== dir) {
242
280
  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",
281
+ p.log.warn(`${chalk.bold(`"${dir}"`)} has characters that confuse shells.`);
282
+ dir = sanitizeDirName(unwrap(await p.text({
247
283
  message: "Directory name",
248
- initial: safe
249
- }, { onCancel });
250
- dir = sanitizeDirName(answers.dir ?? safe);
284
+ initialValue: safe,
285
+ validate: (value) => value?.trim() ? void 0 : "Enter a directory name."
286
+ })));
251
287
  }
252
288
  }
253
- if (isTTY && packageManager === void 0 && flags.install !== false) {
289
+ if (isTTY && packageManager === void 0 && install) {
254
290
  const detected = detectPackageManager();
255
- const answers = await prompts({
256
- type: "select",
257
- name: "packageManager",
291
+ packageManager = unwrap(await p.select({
258
292
  message: "Package manager",
259
- choices: PACKAGE_MANAGERS.map((pm) => ({
260
- title: pm,
261
- value: pm
293
+ options: PACKAGE_MANAGERS.map((pm) => ({
294
+ value: pm,
295
+ label: pm,
296
+ hint: pm === detected ? "detected" : void 0
262
297
  })),
263
- initial: PACKAGE_MANAGERS.indexOf(detected)
264
- }, { onCancel });
265
- packageManager = answers.packageManager;
298
+ initialValue: detected
299
+ }));
266
300
  }
267
301
  const resolvedDir = dir ?? ".";
268
302
  const target = resolve(process.cwd(), resolvedDir);
269
303
  if (!force && await isDirNonEmpty(target)) {
270
304
  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"));
305
+ if (!unwrap(await p.confirm({
306
+ message: `${chalk.yellow(displayPath(target))} is not empty. Scaffold into it anyway?`,
307
+ initialValue: false
308
+ }))) {
309
+ p.outro(chalk.dim("Nothing written."));
279
310
  return;
280
311
  }
281
312
  force = true;
282
313
  }
283
- const opts = {
284
- dir: resolvedDir,
314
+ const pm = packageManager ?? detectPackageManager();
315
+ await scaffold({
316
+ target,
285
317
  force,
286
- name,
287
- packageManager: packageManager ?? detectPackageManager(),
288
- install: flags.install !== false,
289
- git: flags.git !== false
290
- };
291
- await init(opts);
318
+ name: flags.name
319
+ });
320
+ p.log.step(`Created workspace ${chalk.dim(`in ${displayPath(target)}`)}`);
321
+ let installed = false;
322
+ if (install) {
323
+ const task = step(`Installing dependencies with ${pm}`, isTTY);
324
+ const result = await installDependencies(pm, target);
325
+ if (result.ok) {
326
+ installed = true;
327
+ task.done(`Installed dependencies with ${pm}`);
328
+ } else {
329
+ task.fail("Dependency install failed");
330
+ p.log.message(chalk.dim(tail(result.output)));
331
+ }
332
+ }
333
+ if (git) {
334
+ const result = await gitInitAndCommit(target);
335
+ if (result.status === "committed") p.log.step("Initialized git repository");
336
+ else if (result.status === "failed") p.log.warn(`Git setup failed ${chalk.dim(`· ${result.message ?? ""}`)}`);
337
+ else p.log.info(`Skipped git init ${chalk.dim(`· ${result.message ?? ""}`)}`);
338
+ }
339
+ const next = [];
340
+ if (target !== process.cwd()) next.push(`cd ${resolvedDir}`);
341
+ if (!installed) next.push(`${pm} install`);
342
+ next.push(pm === "npm" ? "npm run dev" : `${pm} dev`);
343
+ p.note(next.map((line) => chalk.cyan(line)).join("\n"), "Next steps");
344
+ p.outro(`All set! ${chalk.dim("Pages: https://pages.openpages.sh")}`);
292
345
  }
293
346
  async function run(argv) {
294
347
  const version = await readVersion();
295
348
  const program = new Command();
296
349
  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
350
  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);
351
+ p.intro(`${chalk.inverse.bold(" open-pages ")} ${chalk.dim(`v${version}`)}`);
352
+ try {
353
+ await runInit(dir, flags);
354
+ } catch (err) {
355
+ p.cancel(err instanceof Error ? err.message : String(err));
356
+ process.exit(1);
357
+ }
299
358
  });
300
359
  await program.parseAsync(argv, { from: "user" });
301
360
  }
302
-
303
361
  //#endregion
304
362
  //#region src/cli.ts
305
363
  run(process.argv.slice(2)).catch((err) => {
306
364
  const message = err instanceof Error ? err.message : String(err);
307
- process.stderr.write(`${chalk.red("error:")} ${message}\n`);
365
+ process.stderr.write(`\n ${chalk.red(S_ERROR)} ${message}\n`);
308
366
  process.exit(1);
309
367
  });
310
-
311
- //#endregion
368
+ //#endregion
369
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autono/create-open-pages",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Scaffold an open-pages workspace — agent skills preconfigured, live real-PDF preview out of the box.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "prepack": "pnpm build"
19
19
  },
20
20
  "engines": {
21
- "node": ">=18"
21
+ "node": "^20.19.0 || >=22.12.0"
22
22
  },
23
23
  "keywords": [
24
24
  "pdf",
@@ -45,14 +45,13 @@
45
45
  "access": "public"
46
46
  },
47
47
  "dependencies": {
48
+ "@clack/prompts": "^1.7.0",
48
49
  "chalk": "^6.0.0",
49
- "commander": "^15.0.0",
50
- "prompts": "^2.4.2"
50
+ "commander": "^15.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@types/node": "^22.19.17",
54
- "@types/prompts": "^2.4.9",
55
- "tsdown": "^0.9.9",
56
- "typescript": "^5.9.3"
53
+ "@types/node": "^26.6.2",
54
+ "tsdown": "^0.23.0",
55
+ "typescript": "^7.0.2"
57
56
  }
58
57
  }
@@ -31,7 +31,8 @@ Then open `http://localhost:5173`, edit `pages/getting-started/index.tsx`, or cr
31
31
  | `npm run export` | Build pages into `export/<id>/`, one deployable folder per page. |
32
32
  | `npm run preview` | Preview the built workspace locally. |
33
33
  | `npm run sync:skills` | Sync the bundled agent skills into the workspace. |
34
- | `npm run update` | Update `@autono/open-pages` and sync skills. |
34
+ | `npx open-pages sync:ui` | Update `ui/`, `lib/`, and `hooks/` to the installed runtime's set; files you edited are kept (`--force` to overwrite). |
35
+ | `npm run update` | Update `@autono/open-pages` and sync skills and UI. |
35
36
 
36
37
  ## Authoring a page
37
38
 
@@ -1,3 +1,7 @@
1
+ [build]
2
+ command = "npm run build"
3
+ publish = "dist"
4
+
1
5
  [[redirects]]
2
6
  from = "/*"
3
7
  to = "/index.html"
@@ -17,7 +17,7 @@
17
17
  "react-dom": "^19.2.0",
18
18
  "@base-ui/react": "^1.7.0",
19
19
  "@fontsource-variable/geist": "^5.3.0",
20
- "@shadcn/react": "^0.3.0",
20
+ "@shadcn/react": "^0.3.1",
21
21
  "class-variance-authority": "^0.7.1",
22
22
  "clsx": "^2.1.1",
23
23
  "cmdk": "^1.1.1",
@@ -39,7 +39,6 @@
39
39
  "devDependencies": {
40
40
  "@types/react": "^19.2.0",
41
41
  "@types/react-dom": "^19.2.0",
42
- "vite": "^6.4.3",
43
42
  "tailwindcss": "^4.3.3",
44
43
  "typescript": "^5.9.3"
45
44
  }
@@ -1,3 +1,5 @@
1
1
  {
2
+ "buildCommand": "npm run build",
3
+ "outputDirectory": "dist",
2
4
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
3
5
  }