@hobocode/thought-layer 0.2.2 → 0.4.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/README.md +6 -5
- package/core/deploy-io.ts +319 -0
- package/core/deploy.ts +110 -0
- package/core/index.ts +4 -0
- package/core/scaffold-io.ts +56 -0
- package/core/scaffold.ts +276 -0
- package/dist/tl.js +586 -2
- package/extensions/thought-layer.ts +55 -1
- package/package.json +5 -1
- package/prompts/tl-build.md +7 -0
- package/prompts/tl-deploy.md +7 -0
- package/skills/thought-layer-build/SKILL.md +98 -0
- package/skills/thought-layer-deploy/SKILL.md +49 -0
- package/skills/thought-layer-framework/SKILL.md +2 -0
- package/skills/thought-layer-speedrun/SKILL.md +2 -0
package/core/scaffold.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Deterministic, no-LLM starter-site generator: structured spec + brand -> a
|
|
2
|
+
// self-contained, branded, SEO-complete static site. This is the guaranteed
|
|
3
|
+
// "instantly-deployable floor" so even a thin or failed model-build still
|
|
4
|
+
// yields a real, ownable site. The build skill drives the agent to build the
|
|
5
|
+
// PRODUCT; this tool guarantees a deployable landing page from the same spec.
|
|
6
|
+
//
|
|
7
|
+
// Technique ported from the web app's brandLookbookHtml/brandTokens
|
|
8
|
+
// (src/lib/exports.js): esc() HTML escaping, the color-role regex with
|
|
9
|
+
// fallbacks, the fam() Google-Fonts URL builder, inline CSS via :root brand
|
|
10
|
+
// vars. CDATA is needed only inside SVG <style>; HTML <style> does not need it,
|
|
11
|
+
// so this emits plain HTML. Pure: no fs, no Date - the caller supplies builtAt.
|
|
12
|
+
|
|
13
|
+
import type { ProgressState } from "./progress.ts";
|
|
14
|
+
|
|
15
|
+
export interface StarterSiteSpec {
|
|
16
|
+
brandName: string;
|
|
17
|
+
tagline: string;
|
|
18
|
+
pitch: string;
|
|
19
|
+
positioning: string;
|
|
20
|
+
personality: string[];
|
|
21
|
+
palette: { primary: string; accent: string; ink: string; surface: string; muted: string };
|
|
22
|
+
displayFont: string;
|
|
23
|
+
bodyFont: string;
|
|
24
|
+
voiceTone: string;
|
|
25
|
+
logoSvg?: string;
|
|
26
|
+
pricing?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ScaffoldOptions {
|
|
30
|
+
domain?: string;
|
|
31
|
+
founderName?: string;
|
|
32
|
+
socialImage?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The shared build manifest the deploy step consumes. Both producers (this
|
|
36
|
+
// scaffold tool and the build skill's agent) write the same shape.
|
|
37
|
+
export interface BuildManifest {
|
|
38
|
+
app: "thought-layer";
|
|
39
|
+
kind: "build";
|
|
40
|
+
version: 1;
|
|
41
|
+
builtAt: string;
|
|
42
|
+
producer: "agent" | "scaffold";
|
|
43
|
+
publishDir: string;
|
|
44
|
+
entry: string;
|
|
45
|
+
stack: string;
|
|
46
|
+
hasBackend: boolean;
|
|
47
|
+
backendNote: string | null;
|
|
48
|
+
buildCommand: string | null;
|
|
49
|
+
installCommand: string | null;
|
|
50
|
+
nodeVersion: string;
|
|
51
|
+
provenance: { stateFile: string; prdTs: number | null; grillDone: boolean; fromSpeedrun: boolean };
|
|
52
|
+
requirements: { total: number; built: number; deferred: number; deferredIds: string[] };
|
|
53
|
+
seo: Record<string, boolean>;
|
|
54
|
+
artifacts: { traceability: string | null; decisions: string | null; seo: string | null };
|
|
55
|
+
verified: { buildRan: boolean; publishDirExists: boolean; entryLoads: boolean; notes: string };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const esc = (s: unknown): string =>
|
|
59
|
+
String(s ?? "")
|
|
60
|
+
.replace(/&/g, "&")
|
|
61
|
+
.replace(/</g, "<")
|
|
62
|
+
.replace(/>/g, ">")
|
|
63
|
+
.replace(/"/g, """);
|
|
64
|
+
|
|
65
|
+
const fam = (f: string): string => f.trim().replace(/\s+/g, "+");
|
|
66
|
+
|
|
67
|
+
// ---- extract a spec from the portable state file -----------------------------
|
|
68
|
+
|
|
69
|
+
export function extractScaffoldSpec(state: ProgressState): StarterSiteSpec {
|
|
70
|
+
const obj = (v: unknown): Record<string, unknown> =>
|
|
71
|
+
v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
|
|
72
|
+
const str = (v: unknown): string => (typeof v === "string" ? v : "");
|
|
73
|
+
|
|
74
|
+
const brand = obj(state.brand);
|
|
75
|
+
const guide = obj(brand["guide"]);
|
|
76
|
+
const answers = state.answers || {};
|
|
77
|
+
|
|
78
|
+
const palette = Array.isArray(guide["palette"])
|
|
79
|
+
? (guide["palette"] as Array<{ name?: string; hex?: string }>)
|
|
80
|
+
: [];
|
|
81
|
+
const role = (re: string, fb: string): string => {
|
|
82
|
+
const m = palette.find((p) => p?.name && new RegExp(re, "i").test(p.name));
|
|
83
|
+
return m?.hex || fb;
|
|
84
|
+
};
|
|
85
|
+
const typography = obj(guide["typography"]);
|
|
86
|
+
const fontOf = (slot: unknown, fb: string): string => str(obj(slot)["family"]) || fb;
|
|
87
|
+
const voice = obj(guide["voice"]);
|
|
88
|
+
const logos = Array.isArray(brand["logos"]) ? (brand["logos"] as Array<{ id?: string; svg?: string }>) : [];
|
|
89
|
+
const chosen = logos.find((l) => l?.id === brand["chosenLogoId"]) || logos[0];
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
brandName: str(guide["brandName"]) || "Your Product",
|
|
93
|
+
tagline: str(guide["tagline"]),
|
|
94
|
+
pitch: str(answers["pitch"]) || str(answers["what-statement"]),
|
|
95
|
+
positioning: str(guide["positioning"]),
|
|
96
|
+
personality: Array.isArray(guide["personality"])
|
|
97
|
+
? (guide["personality"] as unknown[]).filter((x): x is string => typeof x === "string")
|
|
98
|
+
: [],
|
|
99
|
+
palette: {
|
|
100
|
+
primary: role("primary", palette[0]?.hex || "#1f3a5f"),
|
|
101
|
+
accent: role("accent|secondary", palette[1]?.hex || "#e8743b"),
|
|
102
|
+
ink: role("ink|text|dark|black", "#16202b"),
|
|
103
|
+
surface: role("surface|background|light|paper|off.?white|cream", "#f7f8fa"),
|
|
104
|
+
muted: role("muted|gray|grey|neutral|border", "#8a9099"),
|
|
105
|
+
},
|
|
106
|
+
displayFont: fontOf(typography["display"], "Inter"),
|
|
107
|
+
bodyFont: fontOf(typography["body"], "Inter"),
|
|
108
|
+
voiceTone: str(voice["tone"]),
|
|
109
|
+
logoSvg: chosen?.svg || undefined,
|
|
110
|
+
pricing: str(answers["pricing-model"]) || undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- the site -----------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function indexHtml(spec: StarterSiteSpec, opts: ScaffoldOptions): string {
|
|
117
|
+
const domain = (opts.domain || "https://example.com").replace(/\/+$/, "");
|
|
118
|
+
const founder = opts.founderName || "";
|
|
119
|
+
const social = opts.socialImage || `${domain}/og-image.png`;
|
|
120
|
+
const name = esc(spec.brandName);
|
|
121
|
+
const headline = esc(spec.tagline || spec.brandName);
|
|
122
|
+
const lead = esc(spec.pitch || spec.positioning);
|
|
123
|
+
const desc = esc(spec.pitch || spec.positioning || spec.brandName);
|
|
124
|
+
const fonts = `https://fonts.googleapis.com/css2?family=${fam(spec.displayFont)}:wght@400;600;700;800&family=${fam(spec.bodyFont)}:wght@400;500;600&display=swap`;
|
|
125
|
+
|
|
126
|
+
const graph: Array<Record<string, unknown>> = [
|
|
127
|
+
{ "@type": "Organization", "@id": `${domain}/#org`, name: spec.brandName, url: `${domain}/`, ...(founder ? { founder: { "@id": `${domain}/#founder` } } : {}) },
|
|
128
|
+
...(founder ? [{ "@type": "Person", "@id": `${domain}/#founder`, name: founder, worksFor: { "@id": `${domain}/#org` } }] : []),
|
|
129
|
+
{ "@type": "WebSite", "@id": `${domain}/#website`, url: `${domain}/`, name: spec.brandName, publisher: { "@id": `${domain}/#org` } },
|
|
130
|
+
{ "@type": "WebPage", "@id": `${domain}/#webpage`, url: `${domain}/`, name: `${spec.brandName}${spec.tagline ? " - " + spec.tagline : ""}`, isPartOf: { "@id": `${domain}/#website` }, about: { "@id": `${domain}/#org` }, description: spec.pitch || spec.positioning || spec.brandName },
|
|
131
|
+
];
|
|
132
|
+
// JSON.stringify does NOT escape < or >, so a value containing "</script>"
|
|
133
|
+
// would break out of the ld+json <script> block. Unicode-escape both.
|
|
134
|
+
const jsonLd = JSON.stringify({ "@context": "https://schema.org", "@graph": graph })
|
|
135
|
+
.replace(/</g, "\\u003c")
|
|
136
|
+
.replace(/>/g, "\\u003e");
|
|
137
|
+
|
|
138
|
+
const lockup = spec.logoSvg ? spec.logoSvg : `<span class="wordmark">${name}</span>`;
|
|
139
|
+
const pills = spec.personality.map((t) => `<li class="pill">${esc(t)}</li>`).join("");
|
|
140
|
+
const positioningSection = spec.positioning
|
|
141
|
+
? `<section class="value" aria-labelledby="value-h"><div class="wrap"><h2 id="value-h">Who it is for</h2><p>${esc(spec.positioning)}</p>${pills ? `<ul class="pills" aria-label="What it stands for">${pills}</ul>` : ""}</div></section>`
|
|
142
|
+
: "";
|
|
143
|
+
const pricingSection = spec.pricing
|
|
144
|
+
? `<section class="pricing" aria-labelledby="pricing-h"><div class="wrap"><h2 id="pricing-h">Pricing</h2><p class="price">${esc(spec.pricing)}</p></div></section>`
|
|
145
|
+
: "";
|
|
146
|
+
|
|
147
|
+
return `<!DOCTYPE html>
|
|
148
|
+
<html lang="en">
|
|
149
|
+
<head>
|
|
150
|
+
<meta charset="UTF-8">
|
|
151
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
152
|
+
<title>${name}${spec.tagline ? " - " + headline : ""}</title>
|
|
153
|
+
<meta name="description" content="${desc}">
|
|
154
|
+
<link rel="canonical" href="${domain}/">
|
|
155
|
+
<meta property="og:type" content="website">
|
|
156
|
+
<meta property="og:title" content="${name}">
|
|
157
|
+
<meta property="og:description" content="${desc}">
|
|
158
|
+
<meta property="og:url" content="${domain}/">
|
|
159
|
+
<meta property="og:image" content="${esc(social)}">
|
|
160
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
161
|
+
<meta name="twitter:title" content="${name}">
|
|
162
|
+
<meta name="twitter:description" content="${desc}">
|
|
163
|
+
<meta name="twitter:image" content="${esc(social)}">
|
|
164
|
+
<link href="${esc(fonts)}" rel="stylesheet">
|
|
165
|
+
<script type="application/ld+json">${jsonLd}</script>
|
|
166
|
+
<style>
|
|
167
|
+
:root{--p:${spec.palette.primary};--a:${spec.palette.accent};--ink:${spec.palette.ink};--su:${spec.palette.surface};--mu:${spec.palette.muted};--disp:'${spec.displayFont}',system-ui,sans-serif;--body:'${spec.bodyFont}',system-ui,sans-serif}
|
|
168
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
169
|
+
html{scroll-behavior:smooth}
|
|
170
|
+
body{background:var(--su);color:var(--ink);font-family:var(--body);line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
171
|
+
.wrap{max-width:960px;margin:0 auto;padding:0 24px}
|
|
172
|
+
a{color:var(--a)}
|
|
173
|
+
:focus-visible{outline:3px solid var(--a);outline-offset:2px}
|
|
174
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
|
|
175
|
+
header{padding:24px 0}
|
|
176
|
+
header .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px}
|
|
177
|
+
.wordmark{font-family:var(--disp);font-weight:800;font-size:22px;color:var(--p)}
|
|
178
|
+
.logo svg,.logo img{height:36px;width:auto}
|
|
179
|
+
.cta{display:inline-block;background:var(--p);color:#fff;text-decoration:none;font-weight:600;padding:12px 22px;border-radius:10px;border:0;cursor:pointer;font-size:15px}
|
|
180
|
+
.cta.alt{background:var(--a)}
|
|
181
|
+
.hero{padding:72px 0 56px}
|
|
182
|
+
.hero h1{font-family:var(--disp);font-weight:800;font-size:clamp(34px,6vw,60px);line-height:1.05;color:var(--ink);letter-spacing:-0.02em;max-width:14ch}
|
|
183
|
+
.hero p.lead{font-size:clamp(17px,2.4vw,22px);opacity:.82;margin:20px 0 28px;max-width:42ch}
|
|
184
|
+
.signup{display:flex;gap:10px;flex-wrap:wrap;max-width:460px}
|
|
185
|
+
.signup input[type=email]{flex:1 1 220px;padding:12px 14px;border:1px solid var(--mu);border-radius:10px;font:inherit;background:#fff;color:var(--ink)}
|
|
186
|
+
.value,.pricing{padding:48px 0;border-top:1px solid color-mix(in srgb,var(--mu) 40%,transparent)}
|
|
187
|
+
.value h2,.pricing h2{font-family:var(--disp);font-weight:700;font-size:28px;margin-bottom:12px}
|
|
188
|
+
.value p{max-width:54ch;opacity:.85}
|
|
189
|
+
ul.pills{list-style:none;display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}
|
|
190
|
+
.pill{font-size:13px;font-weight:600;color:var(--p);background:color-mix(in srgb,var(--p) 12%,transparent);padding:6px 14px;border-radius:999px}
|
|
191
|
+
.pricing .price{font-family:var(--disp);font-size:24px;font-weight:700;color:var(--p);margin-top:8px}
|
|
192
|
+
footer{padding:48px 0;border-top:1px solid color-mix(in srgb,var(--mu) 40%,transparent);color:var(--mu);font-size:13px}
|
|
193
|
+
@media (max-width:640px){.hero{padding:48px 0 40px}header .wrap{flex-direction:column;align-items:flex-start}}
|
|
194
|
+
</style>
|
|
195
|
+
</head>
|
|
196
|
+
<body>
|
|
197
|
+
<header><div class="wrap"><div class="logo">${lockup}</div><a class="cta" href="#get-started">Get early access</a></div></header>
|
|
198
|
+
<main>
|
|
199
|
+
<section class="hero" aria-labelledby="hero-h"><div class="wrap">
|
|
200
|
+
<h1 id="hero-h">${headline}</h1>
|
|
201
|
+
<p class="lead">${lead}</p>
|
|
202
|
+
<form name="signups" id="get-started" method="POST" data-netlify="true" class="signup">
|
|
203
|
+
<input type="hidden" name="form-name" value="signups">
|
|
204
|
+
<label class="sr-only" for="email">Email address</label>
|
|
205
|
+
<input id="email" type="email" name="email" placeholder="you@email.com" required>
|
|
206
|
+
<button type="submit" class="cta alt">Get early access</button>
|
|
207
|
+
</form>
|
|
208
|
+
</div></section>
|
|
209
|
+
${positioningSection}
|
|
210
|
+
${pricingSection}
|
|
211
|
+
</main>
|
|
212
|
+
<footer><div class="wrap"><p>${name}${founder ? ` - by ${esc(founder)}` : ""}. Scaffolded by The Thought Layer.</p></div></footer>
|
|
213
|
+
</body>
|
|
214
|
+
</html>
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function companionFiles(spec: StarterSiteSpec, opts: ScaffoldOptions): Record<string, string> {
|
|
219
|
+
const domain = (opts.domain || "https://example.com").replace(/\/+$/, "");
|
|
220
|
+
const name = spec.brandName;
|
|
221
|
+
const summary = spec.pitch || spec.positioning || name;
|
|
222
|
+
|
|
223
|
+
const llms = `# ${name}\n\n> ${summary}\n\n## About\n${name}${spec.tagline ? ` - ${spec.tagline}` : ""}. ${spec.positioning || summary}\n\n## Pages\n- [Home](${domain}/) - ${spec.tagline || summary}\n\n## FAQ\n- What is ${name}? ${summary}\n- Who is it for? ${spec.positioning || "See the home page."}\n`;
|
|
224
|
+
|
|
225
|
+
const robots = `User-agent: *\nAllow: /\n\nUser-agent: GPTBot\nAllow: /\n\nUser-agent: ClaudeBot\nAllow: /\n\nUser-agent: PerplexityBot\nAllow: /\n\nUser-agent: Google-Extended\nAllow: /\n\nSitemap: ${domain}/sitemap.xml\n`;
|
|
226
|
+
|
|
227
|
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>${domain}/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n</urlset>\n`;
|
|
228
|
+
|
|
229
|
+
const redirects = `/* /index.html 200\n`;
|
|
230
|
+
|
|
231
|
+
const netlifyToml = `[build]\n publish = "."\n\n[[redirects]]\n from = "/*"\n to = "/index.html"\n status = 200\n`;
|
|
232
|
+
|
|
233
|
+
const seoDoc = `# SEO and deployment\n\nThis static site was scaffolded deterministically by The Thought Layer. It is ready to deploy (drag the publish folder onto https://app.netlify.com/drop, or use the deploy step). Everything below is already in place; the lines marked TO FILL need your input.\n\n## In place\n- **Structured data**: schema.org JSON-LD (@graph) with Organization, WebSite, and WebPage in index.html.\n- **/llms.txt**: an AI-crawler summary at the site root.\n- **robots.txt**: allows search + AI crawlers and points to the sitemap.\n- **sitemap.xml**: lists the home page.\n- **Canonical + Open Graph + Twitter Card** meta on the page.\n- **_redirects** and **netlify.toml**: SPA fallback (/* -> /index.html).\n- **Email capture**: a Netlify Forms signup ("signups") - submissions appear in your Netlify dashboard after deploy, no backend needed.\n- Semantic, accessible HTML (landmarks, heading order, labelled form, visible focus).\n\n## TO FILL\n- **Domain**: replace ${domain} with your real domain in index.html (canonical/OG), llms.txt, robots.txt, sitemap.xml. Pass --domain to the scaffold tool to set it up front.\n- **Founder + sameAs**: add a Person with real sameAs profile links (LinkedIn, X) to the JSON-LD, and cross-reference the Organization. Pass --founder to seed the name.\n- **Social image**: add a 1200x630 image at /og-image.png (referenced by og:image / twitter:image).\n- **The product**: this is a landing/coming-soon page. Build the actual product with the thought-layer-build skill (/tl-build).\n`;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
"llms.txt": llms,
|
|
237
|
+
"robots.txt": robots,
|
|
238
|
+
"sitemap.xml": sitemap,
|
|
239
|
+
"_redirects": redirects,
|
|
240
|
+
"netlify.toml": netlifyToml,
|
|
241
|
+
"SEO.md": seoDoc,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function buildStarterSite(spec: StarterSiteSpec, opts: ScaffoldOptions = {}): { files: Record<string, string> } {
|
|
246
|
+
return { files: { "index.html": indexHtml(spec, opts), ...companionFiles(spec, opts) } };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---- the manifest (scaffold producer) ----------------------------------------
|
|
250
|
+
|
|
251
|
+
export function scaffoldManifest(
|
|
252
|
+
publishDir: string,
|
|
253
|
+
builtAt: string,
|
|
254
|
+
provenance: { stateFile: string; prdTs: number | null; grillDone: boolean; fromSpeedrun: boolean },
|
|
255
|
+
): BuildManifest {
|
|
256
|
+
return {
|
|
257
|
+
app: "thought-layer",
|
|
258
|
+
kind: "build",
|
|
259
|
+
version: 1,
|
|
260
|
+
builtAt,
|
|
261
|
+
producer: "scaffold",
|
|
262
|
+
publishDir,
|
|
263
|
+
entry: "index.html",
|
|
264
|
+
stack: "static",
|
|
265
|
+
hasBackend: false,
|
|
266
|
+
backendNote: null,
|
|
267
|
+
buildCommand: null,
|
|
268
|
+
installCommand: null,
|
|
269
|
+
nodeVersion: "20",
|
|
270
|
+
provenance,
|
|
271
|
+
requirements: { total: 0, built: 0, deferred: 0, deferredIds: [] },
|
|
272
|
+
seo: { jsonLd: true, llmsTxt: true, sitemap: true, robots: true, canonical: true, openGraph: true, socialImage: false, semanticHtml: true, seoDoc: true, netlifyToml: true },
|
|
273
|
+
artifacts: { traceability: null, decisions: null, seo: "SEO.md" },
|
|
274
|
+
verified: { buildRan: true, publishDirExists: true, entryLoads: true, notes: "deterministic static scaffold; landing page + SEO files written" },
|
|
275
|
+
};
|
|
276
|
+
}
|