@aeratic/pagetailor 0.0.0-stage → 0.0.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/README.md CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # @aeratic/pagetailor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Pages as data. Define your app's pages in YAML, bind them to a typed catalog of components and data sources, tailor them per role and per person with small patches, and let an AI agent compose them through a validation gate.
4
+
5
+ Status: 0.0.1, name reservation. The engine (built on json-render) lands in 0.1.
6
+
7
+ ```
8
+ npm init -y
9
+ npx @aeratic/pagetailor init
10
+ npx @aeratic/pagetailor validate
11
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { init } from "../src/init.js";
3
+ import { validate } from "../src/validate.js";
4
+
5
+ const [cmd, ...args] = process.argv.slice(2);
6
+ const help = `pagetailor <command>
7
+
8
+ init create the pagetailor/ folder with manifest, tokens, data catalog, a sample page and base components
9
+ validate check every page and override against the catalog
10
+ `;
11
+
12
+ try {
13
+ if (cmd === "init") await init(process.cwd(), args);
14
+ else if (cmd === "validate") process.exitCode = (await validate(process.cwd())) ? 0 : 1;
15
+ else { console.log(help); if (cmd && cmd !== "help") process.exitCode = 1; }
16
+ } catch (e) { console.error("pagetailor:", e.message); process.exitCode = 1; }
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@aeratic/pagetailor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "Pages as data: declarative, per-user tailored UI that an AI agent can safely compose.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "pagetailor": "bin/cli.js"
9
+ },
10
+ "main": "./src/index.js",
11
+ "exports": {
12
+ ".": "./src/index.js"
13
+ },
14
+ "files": [
15
+ "bin",
16
+ "src",
17
+ "templates",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": [
24
+ "server-driven-ui",
25
+ "generative-ui",
26
+ "declarative-ui",
27
+ "json-render",
28
+ "yaml",
29
+ "personalization",
30
+ "ai-agent"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/aeratic/pagetailor.git"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "yaml": "^2.6.0"
41
+ }
42
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { init } from "./init.js";
2
+ export { validate } from "./validate.js";
package/src/init.js ADDED
@@ -0,0 +1,22 @@
1
+ import { cp, mkdir, access } from "node:fs/promises";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const here = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export async function init(cwd, args = []) {
8
+ const target = join(cwd, "pagetailor");
9
+ const force = args.includes("--force");
10
+ try { await access(target); if (!force) throw new Error("pagetailor/ already exists (use --force to overwrite)"); } catch (e) { if (e.code !== "ENOENT") throw e; }
11
+ await mkdir(target, { recursive: true });
12
+ await cp(join(here, "..", "templates"), target, { recursive: true, force });
13
+ console.log(`created ${target}/
14
+ manifest.yaml app shape: pages, home, navigation, roles
15
+ tokens.yaml design tokens
16
+ data-catalog.yaml typed data sources with roles
17
+ pages/home.yaml a sample page
18
+ overrides/ per-user and per-role patches
19
+ components/ base components, yours to edit
20
+
21
+ next: npx pagetailor validate`);
22
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parse } from "yaml";
4
+
5
+ const load = async (p) => parse(await readFile(p, "utf8"));
6
+
7
+ export async function validate(cwd) {
8
+ const root = join(cwd, "pagetailor");
9
+ const manifest = await load(join(root, "manifest.yaml"));
10
+ const data = await load(join(root, "data-catalog.yaml"));
11
+ const registry = await load(join(root, "components", "registry.yaml"));
12
+ const errors = [];
13
+
14
+ const walk = (node, page, path = "root") => {
15
+ if (!node || typeof node !== "object") return;
16
+ if (node.ref) {
17
+ const [name, version] = String(node.ref).split("@");
18
+ const comp = registry[name];
19
+ if (!comp) errors.push(`${page}: ${path}: unknown component "${name}"`);
20
+ else if (version && !comp.versions?.[version]) errors.push(`${page}: ${path}: "${name}" has no version ${version}`);
21
+ if (node.data?.source) {
22
+ const src = data[node.data.source];
23
+ if (!src) errors.push(`${page}: ${path}: unknown data source "${node.data.source}"`);
24
+ else if (comp?.accepts && src.returns && !compatible(comp.accepts, src.returns))
25
+ errors.push(`${page}: ${path}: "${name}" accepts ${comp.accepts} but "${node.data.source}" returns ${src.returns}`);
26
+ }
27
+ }
28
+ (node.children ?? []).forEach((c, i) => walk(c, page, `${path}/${i}`));
29
+ };
30
+
31
+ for (const file of await readdir(join(root, "pages"))) {
32
+ if (!file.endsWith(".yaml")) continue;
33
+ const page = await load(join(root, "pages", file));
34
+ if (!manifest.pages?.includes(page.page)) errors.push(`${file}: page "${page.page}" is not listed in manifest.yaml`);
35
+ walk(page.layout, file);
36
+ }
37
+
38
+ if (errors.length) { console.error(errors.join("\n")); console.error(`\n${errors.length} problem(s)`); return false; }
39
+ console.log("all pages valid"); return true;
40
+ }
41
+
42
+ // v0: exact match or "any"; a real type lattice comes later.
43
+ const compatible = (accepts, returns) => accepts === "any" || accepts === returns;
@@ -0,0 +1,3 @@
1
+ export default function LineChart({ props, data, children }) {
2
+ return <div data-pagetailor="LineChart">{props?.title}{children}</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function Stack({ props, data, children }) {
2
+ return <div data-pagetailor="Stack">{props?.title}{children}</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function Table({ props, data, children }) {
2
+ return <div data-pagetailor="Table">{props?.title}{children}</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function Text({ props, data, children }) {
2
+ return <div data-pagetailor="Text">{props?.title}{children}</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function TodoList({ props, data, children }) {
2
+ return <div data-pagetailor="TodoList">{props?.title}{children}</div>;
3
+ }
@@ -0,0 +1,14 @@
1
+ # name -> versions, and the data type each component accepts
2
+ layout/stack:
3
+ versions: { "1.0.0": { impl: ./Stack.jsx } }
4
+ charts/line:
5
+ accepts: timeseries<money>
6
+ versions: { "1.0.0": { impl: ./LineChart.jsx } }
7
+ tables/simple:
8
+ accepts: any
9
+ versions: { "1.0.0": { impl: ./Table.jsx } }
10
+ todo/list:
11
+ accepts: list<task>
12
+ versions: { "1.0.0": { impl: ./TodoList.jsx } }
13
+ text/block:
14
+ versions: { "1.0.0": { impl: ./Text.jsx } }
@@ -0,0 +1,10 @@
1
+ # Typed data sources. Components can only bind to sources whose `returns` they accept.
2
+ analytics.revenue:
3
+ description: "Revenue over time"
4
+ params: { period: [7d, 30d, 90d] }
5
+ returns: timeseries<money>
6
+ roles: [admin]
7
+ tasks.mine:
8
+ description: "Tasks assigned to the current user"
9
+ returns: list<task>
10
+ roles: [admin, user]
@@ -0,0 +1,6 @@
1
+ app: my-app
2
+ home: home
3
+ nav:
4
+ position: left
5
+ pages: [home]
6
+ roles: [admin, user]
File without changes
@@ -0,0 +1,6 @@
1
+ One file per user or role, e.g. `user-giulia.yaml`:
2
+
3
+ extends: home
4
+ why: "Reports every Monday, wants revenue first"
5
+ patches:
6
+ - { op: move, ref: charts/line, to: 0 }
@@ -0,0 +1,10 @@
1
+ page: home
2
+ layout:
3
+ ref: layout/stack@1.0.0
4
+ children:
5
+ - ref: charts/line@1.0.0
6
+ props: { title: "Revenue, last 30 days" }
7
+ data: { source: analytics.revenue, params: { period: 30d } }
8
+ - ref: todo/list@1.0.0
9
+ props: { filter: mine }
10
+ data: { source: tasks.mine }
@@ -0,0 +1,6 @@
1
+ color:
2
+ primary: "#1D9E75"
3
+ surface: "#FFFFFF"
4
+ text: "#1A1A1A"
5
+ spacing: { sm: 8, md: 16, lg: 24 }
6
+ font: { base: "Inter", size: 15 }