@nebutra/landing-builder 0.1.2 → 0.1.4
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -676
- package/package.json +21 -11
- package/.nebutra/debug/content-store.jsonl +0 -5
- package/.nebutra/debug/event-log.jsonl +0 -1
- package/.nebutra/debug/landing-builder.jsonl +0 -1
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -15
- package/.turbo/turbo-typecheck.log +0 -4
- package/examples/doctor.ts +0 -11
- package/examples/quickstart.ts +0 -30
- package/examples/theme.ts +0 -20
- package/plays/one_pager/SKILL.md +0 -41
- package/src/cli.ts +0 -55
- package/src/index.test.ts +0 -112
- package/src/index.ts +0 -291
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/landing-builder",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "BrandContext-first landing page Play package for generating deployable launch sites",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,21 +23,25 @@
|
|
|
23
23
|
"category": "ai",
|
|
24
24
|
"summary": "Brand-aware landing page Play product over lower content and execution capabilities"
|
|
25
25
|
},
|
|
26
|
-
"main": "./
|
|
27
|
-
"types": "./
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
28
|
"exports": {
|
|
29
|
-
".":
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
30
34
|
},
|
|
31
35
|
"dependencies": {
|
|
32
|
-
"@nebutra/capability-kit": "0.2.
|
|
33
|
-
"@nebutra/
|
|
34
|
-
"@nebutra/
|
|
35
|
-
"@nebutra/
|
|
36
|
-
"@nebutra/
|
|
37
|
-
"@nebutra/play-loader": "0.1.
|
|
36
|
+
"@nebutra/capability-kit": "0.2.4",
|
|
37
|
+
"@nebutra/errors": "0.1.2",
|
|
38
|
+
"@nebutra/event-log": "0.1.4",
|
|
39
|
+
"@nebutra/generation-context": "0.1.3",
|
|
40
|
+
"@nebutra/content-store": "0.1.4",
|
|
41
|
+
"@nebutra/play-loader": "0.1.3"
|
|
38
42
|
},
|
|
39
43
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^
|
|
44
|
+
"@types/node": "^25.9.1",
|
|
41
45
|
"tsup": "^8.5.1",
|
|
42
46
|
"tsx": "^4.21.0",
|
|
43
47
|
"typescript": "^5.9.3",
|
|
@@ -55,6 +59,12 @@
|
|
|
55
59
|
"publishConfig": {
|
|
56
60
|
"access": "public"
|
|
57
61
|
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"README.md",
|
|
65
|
+
"LICENSE",
|
|
66
|
+
"CHANGELOG.md"
|
|
67
|
+
],
|
|
58
68
|
"scripts": {
|
|
59
69
|
"build": "tsup",
|
|
60
70
|
"test": "vitest run",
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{"at":"2026-05-19T08:15:26.801Z","type":"reindex","tenantId":"tenant_a","files":0}
|
|
2
|
-
{"at":"2026-05-19T08:15:26.871Z","type":"write","tenantId":"tenant_a","path":"company/landing/index.html"}
|
|
3
|
-
{"at":"2026-05-19T08:15:26.902Z","type":"write","tenantId":"tenant_a","path":"company/landing/theme.css"}
|
|
4
|
-
{"at":"2026-05-19T08:15:26.909Z","type":"write","tenantId":"tenant_a","path":"company/landing/deploy.json"}
|
|
5
|
-
{"at":"2026-05-19T08:15:27.113Z","type":"reindex","tenantId":"","files":0}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"at":"2026-05-19T08:15:26.951Z","traceId":"landing_builder_loop_mpccyhfq","kind":"content_write","affected":["company/landing/index.html","company/landing/theme.css","company/landing/deploy.json"],"parent":null,"snapshot":{"company/landing/index.html":"<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Loop</title>\n <link rel=\"stylesheet\" href=\"./theme.css\" />\n</head>\n<body data-brand-id=\"loop\">\n <main class=\"page\">\n <section class=\"hero\">\n <p class=\"eyebrow\">electric / terminal-native</p>\n <h1>Loop</h1>\n <p class=\"lead\">AI debugging for indie devs</p>\n <a class=\"cta\" href=\"#waitlist\">Join the waitlist</a>\n </section>\n <section class=\"proof\">\n <h2>Why it matters</h2>\n <p>technical, direct, warm execution, grounded in one shared brand context.</p>\n </section>\n <section id=\"waitlist\" class=\"waitlist\">\n <h2>Join the waitlist</h2>\n <form>\n <input aria-label=\"Email\" placeholder=\"you@example.com\" />\n <button type=\"button\">Join</button>\n </form>\n </section>\n </main>\n</body>\n</html>\n<!-- #00D4FF #F3C14B -->\n","company/landing/theme.css":":root {\n --brand-primary: #00D4FF;\n --brand-background: #111318;\n --brand-accent: #F3C14B;\n --brand-heading: Geist Mono;\n --brand-body: Geist Sans;\n}\n\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--brand-background); color: color-mix(in oklab, var(--brand-primary) 80%, black); font-family: var(--brand-body), system-ui, sans-serif; }\n.page { min-height: 100vh; }\n.hero { min-height: 82vh; display: grid; align-content: center; gap: 24px; padding: clamp(32px, 8vw, 96px); }\n.eyebrow { color: var(--brand-accent); font-family: var(--brand-heading), monospace; text-transform: uppercase; letter-spacing: 0; }\nh1 { max-width: 11ch; margin: 0; font-family: var(--brand-heading), system-ui; font-size: clamp(56px, 12vw, 132px); line-height: .92; letter-spacing: 0; }\n.lead { max-width: 760px; font-size: clamp(20px, 3vw, 34px); line-height: 1.2; }\n.cta, button { width: fit-content; border: 0; border-radius: 8px; background: var(--brand-primary); color: var(--brand-background); padding: 14px 20px; font: inherit; text-decoration: none; }\n.proof, .waitlist { padding: 48px clamp(32px, 8vw, 96px); border-top: 1px solid color-mix(in oklab, var(--brand-primary) 20%, transparent); }\ninput { min-width: min(360px, 100%); border: 1px solid color-mix(in oklab, var(--brand-primary) 35%, transparent); border-radius: 8px; padding: 14px 16px; font: inherit; }\nform { display: flex; flex-wrap: wrap; gap: 12px; }\n","company/landing/deploy.json":"{\n \"mode\": \"one_pager\",\n \"brandId\": \"loop\",\n \"entrypoint\": \"company/landing/index.html\",\n \"targets\": [\n \"managed-hosting\",\n \"edge-pages\",\n \"static-hosting\",\n \"git-pages\"\n ],\n \"requiresExplicitDeployConsent\": true\n}\n"},"id":"5b8c9ee05fdcc8ad9c0602d11b1f24bc253b4ef8e9cc986882f070f057ca19c5","tenantId":"tenant_a","summary":"content_write touched 3 path(s)","objectHashes":["282b113584ef5dca1d7d5a8ab0acda009a083ae1d338148e3b5f72d7e2326f0d","efd0c917e24ab7adf2d36be8e9800535dcc7e1c3a8c433826a1a13bb996fe967","2515ff8fb770aeb2584f10e096f9e430af9d9e69022d76f2c96c7feccbfe19eb"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"at":"2026-05-19T08:15:26.970Z","type":"run","tenantId":"tenant_a","brandId":"loop","eventId":"5b8c9ee05fdcc8ad9c0602d11b1f24bc253b4ef8e9cc986882f070f057ca19c5"}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/landing-builder@0.1.2 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/landing-builder
|
|
3
|
-
> tsup
|
|
4
|
-
|
|
5
|
-
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
6
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.5.1
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/landing-builder/tsup.config.ts
|
|
9
|
-
[34mCLI[39m Target: es2022
|
|
10
|
-
[34mCLI[39m Cleaning output folder
|
|
11
|
-
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/cli.js [22m[32m1.78 KB[39m
|
|
13
|
-
[32mESM[39m [1mdist/index.js [22m[32m262.00 B[39m
|
|
14
|
-
[32mESM[39m [1mdist/chunk-HQNDYIXK.js [22m[32m8.25 KB[39m
|
|
15
|
-
[32mESM[39m [1mdist/cli.js.map [22m[32m3.24 KB[39m
|
|
16
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m71.00 B[39m
|
|
17
|
-
[32mESM[39m [1mdist/chunk-HQNDYIXK.js.map [22m[32m15.31 KB[39m
|
|
18
|
-
[32mESM[39m ⚡️ Build success in 60ms
|
|
19
|
-
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in 10057ms
|
|
21
|
-
[32mDTS[39m [1mdist/cli.d.ts [22m[32m13.00 B[39m
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m2.64 KB[39m
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/landing-builder@0.1.2 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/landing-builder
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.4 [39m[90m/home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/ai/landing-builder[39m
|
|
7
|
-
|
|
8
|
-
[32m✓[39m src/index.test.ts [2m([22m[2m4 tests[22m[2m)[22m[33m 718[2mms[22m[39m
|
|
9
|
-
[33m[2m✓[22m[39m runs one_pager and writes site files plus a deploy handoff [33m 469[2mms[22m[39m
|
|
10
|
-
|
|
11
|
-
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
12
|
-
[2m Tests [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
13
|
-
[2m Start at [22m 08:15:23
|
|
14
|
-
[2m Duration [22m 3.51s[2m (transform 1.30s, setup 0ms, import 1.73s, tests 718ms, environment 0ms)[22m
|
|
15
|
-
|
package/examples/doctor.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { LandingBuilder } from "../src/index";
|
|
2
|
-
|
|
3
|
-
const builder = await LandingBuilder.open(".nebutra/landing-builder-example", {
|
|
4
|
-
tenantId: "local",
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
process.stdout.write(`${JSON.stringify(await builder.doctor(), null, 2)}\n`);
|
|
9
|
-
} finally {
|
|
10
|
-
await builder.close();
|
|
11
|
-
}
|
package/examples/quickstart.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { LandingBuilder } from "../src/index";
|
|
2
|
-
|
|
3
|
-
const builder = await LandingBuilder.open(".nebutra/landing-builder-example", {
|
|
4
|
-
tenantId: "local",
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
const result = await builder.runOnePager({
|
|
9
|
-
brand: {
|
|
10
|
-
tenantId: "local",
|
|
11
|
-
brandId: "loop",
|
|
12
|
-
name: "Loop",
|
|
13
|
-
palette: [
|
|
14
|
-
{ name: "electric cyan", hex: "#00D4FF", role: "primary" },
|
|
15
|
-
{ name: "carbon black", hex: "#111318", role: "background" },
|
|
16
|
-
],
|
|
17
|
-
typography: { heading: "Geist Mono", body: "Geist Sans", accent: "Geist Mono" },
|
|
18
|
-
visualStyle: { name: "cyberpunk", keywords: ["electric"], avoid: [] },
|
|
19
|
-
referenceImages: [],
|
|
20
|
-
forbidden: [],
|
|
21
|
-
toneKeywords: ["technical"],
|
|
22
|
-
sourcePath: "company/BRAND.md",
|
|
23
|
-
},
|
|
24
|
-
productDesc: "AI debugging for indie devs",
|
|
25
|
-
ctaText: "Join the waitlist",
|
|
26
|
-
});
|
|
27
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
28
|
-
} finally {
|
|
29
|
-
await builder.close();
|
|
30
|
-
}
|
package/examples/theme.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { renderTailwindTheme } from "../src/index";
|
|
2
|
-
|
|
3
|
-
process.stdout.write(
|
|
4
|
-
`${JSON.stringify(
|
|
5
|
-
renderTailwindTheme({
|
|
6
|
-
tenantId: "local",
|
|
7
|
-
brandId: "loop",
|
|
8
|
-
name: "Loop",
|
|
9
|
-
palette: [{ name: "electric cyan", hex: "#00D4FF", role: "primary" }],
|
|
10
|
-
typography: { heading: "Geist Mono", body: "Geist Sans", accent: "Geist Mono" },
|
|
11
|
-
visualStyle: { name: "minimal", keywords: ["clear"], avoid: [] },
|
|
12
|
-
referenceImages: [],
|
|
13
|
-
forbidden: [],
|
|
14
|
-
toneKeywords: ["direct"],
|
|
15
|
-
sourcePath: "company/BRAND.md",
|
|
16
|
-
}),
|
|
17
|
-
null,
|
|
18
|
-
2,
|
|
19
|
-
)}\n`,
|
|
20
|
-
);
|
package/plays/one_pager/SKILL.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: one_pager
|
|
3
|
-
kind: play
|
|
4
|
-
version: 1.0.0
|
|
5
|
-
description: Build a brand-aware static landing page from BrandContext and a product description
|
|
6
|
-
inputs:
|
|
7
|
-
brand_md: { type: file, path: company/BRAND.md, mime: text/markdown }
|
|
8
|
-
product_desc: { type: string, max_length: 500 }
|
|
9
|
-
cta_text: { type: string, max_length: 80 }
|
|
10
|
-
outputs:
|
|
11
|
-
html: { type: file, path: company/landing/index.html, mime: text/html }
|
|
12
|
-
theme: { type: file, path: company/landing/theme.css, mime: text/css }
|
|
13
|
-
deploy_manifest: { type: file, path: company/landing/deploy.json, mime: application/json }
|
|
14
|
-
budget:
|
|
15
|
-
duration_s: 60
|
|
16
|
-
cost_usd: 0.5
|
|
17
|
-
required_skills:
|
|
18
|
-
- content_store.write
|
|
19
|
-
- content_store.read
|
|
20
|
-
- code_execution.preview
|
|
21
|
-
sub_agents:
|
|
22
|
-
- role: web_builder
|
|
23
|
-
allowed_skills: [content_store.write, code_execution.preview]
|
|
24
|
-
depends_on_plays: []
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## What this play does
|
|
28
|
-
|
|
29
|
-
Builds a single-page launch site from the current BrandContext.
|
|
30
|
-
|
|
31
|
-
The Play owns the site composition, theme assembly, preview handoff, and deploy
|
|
32
|
-
manifest. It does not own sandbox execution, deploy credentials, or the
|
|
33
|
-
BrandContext schema.
|
|
34
|
-
|
|
35
|
-
## Rules
|
|
36
|
-
|
|
37
|
-
- Read BrandContext first.
|
|
38
|
-
- Use brand colors and typography as the source of truth.
|
|
39
|
-
- Write generated files through content-store.
|
|
40
|
-
- Produce a deploy manifest, but require explicit deploy consent before any
|
|
41
|
-
external provider call.
|
package/src/cli.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { LandingBuilder, readLandingBuilderDebug } from "./index";
|
|
2
|
-
|
|
3
|
-
const command = process.argv[2] ?? "doctor";
|
|
4
|
-
const root = process.env.LANDING_BUILDER_ROOT ?? ".nebutra/landing-builder";
|
|
5
|
-
const tenantId = process.env.NEBUTRA_TENANT_ID ?? "local";
|
|
6
|
-
|
|
7
|
-
const brand = {
|
|
8
|
-
tenantId,
|
|
9
|
-
brandId: "loop",
|
|
10
|
-
name: "Loop",
|
|
11
|
-
palette: [
|
|
12
|
-
{ name: "electric cyan", hex: "#00D4FF", role: "primary" },
|
|
13
|
-
{ name: "carbon black", hex: "#111318", role: "background" },
|
|
14
|
-
{ name: "signal amber", hex: "#F3C14B", role: "accent" },
|
|
15
|
-
],
|
|
16
|
-
typography: { heading: "Geist Mono", body: "Geist Sans", accent: "Geist Mono" },
|
|
17
|
-
visualStyle: { name: "cyberpunk", keywords: ["electric", "terminal-native"], avoid: [] },
|
|
18
|
-
referenceImages: [],
|
|
19
|
-
forbidden: [],
|
|
20
|
-
toneKeywords: ["technical", "direct", "warm"],
|
|
21
|
-
sourcePath: "company/BRAND.md",
|
|
22
|
-
} as const;
|
|
23
|
-
|
|
24
|
-
if (command === "doctor") {
|
|
25
|
-
const builder = await LandingBuilder.open(root, { tenantId });
|
|
26
|
-
try {
|
|
27
|
-
process.stdout.write(`${JSON.stringify(await builder.doctor(), null, 2)}\n`);
|
|
28
|
-
} finally {
|
|
29
|
-
await builder.close();
|
|
30
|
-
}
|
|
31
|
-
} else if (command === "quickstart") {
|
|
32
|
-
const builder = await LandingBuilder.open(root, { tenantId });
|
|
33
|
-
try {
|
|
34
|
-
process.stdout.write(
|
|
35
|
-
`${JSON.stringify(
|
|
36
|
-
await builder.runOnePager({
|
|
37
|
-
brand,
|
|
38
|
-
productDesc: process.argv.slice(3).join(" ") || "AI debugging for indie devs",
|
|
39
|
-
ctaText: "Join the waitlist",
|
|
40
|
-
}),
|
|
41
|
-
null,
|
|
42
|
-
2,
|
|
43
|
-
)}\n`,
|
|
44
|
-
);
|
|
45
|
-
} finally {
|
|
46
|
-
await builder.close();
|
|
47
|
-
}
|
|
48
|
-
} else if (command === "debug") {
|
|
49
|
-
process.stdout.write(
|
|
50
|
-
`${JSON.stringify({ capability: "landing-builder", entries: await readLandingBuilderDebug() }, null, 2)}\n`,
|
|
51
|
-
);
|
|
52
|
-
} else {
|
|
53
|
-
process.stderr.write(`Unknown landing-builder command: ${command}\n`);
|
|
54
|
-
process.exitCode = 1;
|
|
55
|
-
}
|
package/src/index.test.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { parsePlayMarkdown } from "@nebutra/play-loader";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
import {
|
|
7
|
-
LandingBuilder,
|
|
8
|
-
readLandingBuilderDebug,
|
|
9
|
-
renderOnePagerHtml,
|
|
10
|
-
renderTailwindTheme,
|
|
11
|
-
} from "./index";
|
|
12
|
-
|
|
13
|
-
let root: string | undefined;
|
|
14
|
-
let builder: LandingBuilder | undefined;
|
|
15
|
-
|
|
16
|
-
const brand = {
|
|
17
|
-
tenantId: "tenant_a",
|
|
18
|
-
brandId: "loop",
|
|
19
|
-
name: "Loop",
|
|
20
|
-
palette: [
|
|
21
|
-
{ name: "electric cyan", hex: "#00D4FF", role: "primary" },
|
|
22
|
-
{ name: "carbon black", hex: "#111318", role: "background" },
|
|
23
|
-
{ name: "signal amber", hex: "#F3C14B", role: "accent" },
|
|
24
|
-
],
|
|
25
|
-
typography: { heading: "Geist Mono", body: "Geist Sans", accent: "Geist Mono" },
|
|
26
|
-
visualStyle: {
|
|
27
|
-
name: "cyberpunk",
|
|
28
|
-
keywords: ["electric", "terminal-native"],
|
|
29
|
-
avoid: ["generic stock"],
|
|
30
|
-
},
|
|
31
|
-
referenceImages: [],
|
|
32
|
-
forbidden: ["generic stock"],
|
|
33
|
-
toneKeywords: ["technical", "direct", "warm"],
|
|
34
|
-
sourcePath: "company/BRAND.md",
|
|
35
|
-
} as const;
|
|
36
|
-
|
|
37
|
-
afterEach(async () => {
|
|
38
|
-
if (builder) await builder.close();
|
|
39
|
-
if (root) await rm(root, { recursive: true, force: true });
|
|
40
|
-
root = undefined;
|
|
41
|
-
builder = undefined;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
async function open(): Promise<LandingBuilder> {
|
|
45
|
-
root = await mkdtemp(join(tmpdir(), "landing-builder-"));
|
|
46
|
-
builder = await LandingBuilder.open(root, { tenantId: "tenant_a" });
|
|
47
|
-
return builder;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe("landing-builder", () => {
|
|
51
|
-
it("renders a brand-aware one pager without owning BrandContext", () => {
|
|
52
|
-
const theme = renderTailwindTheme(brand);
|
|
53
|
-
const html = renderOnePagerHtml({
|
|
54
|
-
brand,
|
|
55
|
-
productDesc: "AI debugging for indie devs",
|
|
56
|
-
ctaText: "Join the waitlist",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(theme.cssVariables).toContain("--brand-primary: #00D4FF");
|
|
60
|
-
expect(html).toContain("Loop");
|
|
61
|
-
expect(html).toContain("AI debugging for indie devs");
|
|
62
|
-
expect(html).toContain("Join the waitlist");
|
|
63
|
-
expect(html).toContain('data-brand-id="loop"');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("runs one_pager and writes site files plus a deploy handoff", async () => {
|
|
67
|
-
const runtime = await open();
|
|
68
|
-
|
|
69
|
-
const result = await runtime.runOnePager({
|
|
70
|
-
brand,
|
|
71
|
-
productDesc: "AI debugging for indie devs",
|
|
72
|
-
ctaText: "Join the waitlist",
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(result.play).toBe("one_pager");
|
|
76
|
-
expect(result.files.map((file) => file.path)).toEqual([
|
|
77
|
-
"company/landing/index.html",
|
|
78
|
-
"company/landing/theme.css",
|
|
79
|
-
"company/landing/deploy.json",
|
|
80
|
-
]);
|
|
81
|
-
expect(result.preview.kind).toBe("static-content-store");
|
|
82
|
-
expect(result.deploy.status).toBe("handoff");
|
|
83
|
-
expect(result.eventId).toEqual(expect.any(String));
|
|
84
|
-
await expect(readLandingBuilderDebug(root)).resolves.toEqual(expect.any(Array));
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("requires tenant context before persistent operations", async () => {
|
|
88
|
-
root = await mkdtemp(join(tmpdir(), "landing-builder-"));
|
|
89
|
-
const runtime = await LandingBuilder.open(root, { tenantId: "" });
|
|
90
|
-
builder = runtime;
|
|
91
|
-
|
|
92
|
-
await expect(
|
|
93
|
-
runtime.runOnePager({
|
|
94
|
-
brand: { ...brand, tenantId: "" },
|
|
95
|
-
productDesc: "AI debugging",
|
|
96
|
-
ctaText: "Join",
|
|
97
|
-
}),
|
|
98
|
-
).rejects.toMatchObject({
|
|
99
|
-
capability: "landing-builder",
|
|
100
|
-
suggestion: expect.stringContaining("tenantId"),
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("keeps the landing Play as SKILL.md instead of a new workflow format", async () => {
|
|
105
|
-
const skill = await readFile(join(process.cwd(), "plays", "one_pager", "SKILL.md"), "utf8");
|
|
106
|
-
const play = parsePlayMarkdown(skill);
|
|
107
|
-
|
|
108
|
-
expect(play.meta).toMatchObject({ name: "one_pager", kind: "play", version: "1.0.0" });
|
|
109
|
-
expect(play.requiredSkills).toContain("content_store.write");
|
|
110
|
-
expect(play.subAgents.map((agent) => agent.role)).toContain("web_builder");
|
|
111
|
-
});
|
|
112
|
-
});
|