@hobocode/thought-layer 0.4.1 → 0.5.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 +8 -2
- package/SECURITY.md +50 -0
- package/core/backend.ts +272 -0
- package/core/deploy-io.ts +19 -4
- package/core/deploy.ts +5 -0
- package/core/index.ts +1 -0
- package/core/scaffold.ts +7 -0
- package/dist/tl.js +14 -1
- package/package.json +3 -1
- package/prompts/tl-build.md +1 -1
- package/prompts/tl-deploy.md +1 -1
- package/skills/thought-layer-build/SKILL.md +26 -3
- package/skills/thought-layer-deploy/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ This is open source and BYOK by design. The point is to help people build real t
|
|
|
17
17
|
- **thought-layer-prd.** Draft the complete PRD — with a first-cut domain glossary and testable requirements — from the validated idea and business model. The plan the grill then hardens.
|
|
18
18
|
- **thought-layer-grill.** The last design step: grills the draft PRD against the domain one question at a time, sharpening the glossary and hardening the requirements inline until it is build-ready. Runs after the PRD, not instead of the framework.
|
|
19
19
|
- **thought-layer-naming.** Name the thing, with rationale and domain-ready slugs.
|
|
20
|
+
- **thought-layer-build.** Build the hardened PRD into a static-first, deploy-ready artifact, verified to run, and leave a manifest the deploy step reads. When a requirement genuinely needs a server, it also emits a real backend (serverless functions, a `schema.sql`, a names-only `.env.example`, and a `BACKEND.md` guide), with Neon Postgres as the documented default.
|
|
21
|
+
- **thought-layer-deploy.** Take the build live to a URL you own, with no lock-in: a Netlify token deploys into your own account, or the Netlify CLI handles a logged-in or anonymous deploy.
|
|
22
|
+
- **thought-layer-speedrun.** A fast, unranked path to a build-ready spec when you do not need the full panel and score.
|
|
23
|
+
- **Optional deep-dives**, pulled in when you want to go further than the backbone: `thought-layer-strategy`, `thought-layer-brand`, `thought-layer-market-research`, and `thought-layer-business-model`.
|
|
20
24
|
|
|
21
25
|
**A Pi package** that adds, on top of the skills:
|
|
22
26
|
|
|
@@ -30,11 +34,11 @@ This is open source and BYOK by design. The point is to help people build real t
|
|
|
30
34
|
|
|
31
35
|
```bash
|
|
32
36
|
pi install npm:@hobocode/thought-layer
|
|
33
|
-
# or
|
|
37
|
+
# or track the latest from GitHub:
|
|
34
38
|
pi install git:github.com/hobocode-ofc/thought-layer-kit
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
Installing the package lights up the skills, the `/tl` commands, and the `tl_score
|
|
41
|
+
Installing the package lights up the skills, the `/tl` commands, and the deterministic tools (`tl_score`, `tl_domains`, `tl_project`, `tl_state`, `tl_scaffold`, `deploy`). You can also invoke a skill directly with `/skill:thought-layer-panel`.
|
|
38
42
|
|
|
39
43
|
### Claude Code (or any agent that reads the Agent Skills format)
|
|
40
44
|
|
|
@@ -65,6 +69,8 @@ The hosted version of the rigor lives at [weareallproductmanagersnow.com](https:
|
|
|
65
69
|
- **Done:** the rigor as portable skills; a Pi package with deterministic tools + slash commands; the portable progress file (`tl_state` / the `tl` CLI) shared with the web app; and the speedrun.
|
|
66
70
|
- **Phase 3 (done):** a `build` step that turns the hardened PRD into a deploy-ready artifact, built by your own agent (`/tl-build`), with a deterministic `tl_scaffold` tool that writes an instantly-deployable branded static site as the floor.
|
|
67
71
|
- **Phase 4 (done):** a `deploy` step (`/tl-deploy`, the `deploy` tool, or `tl deploy`) that takes the build live to a URL you own, closing the loop. With a Netlify token it deploys into your own account via the file-digest API (owned immediately, no claim step); with no token it delegates to your Netlify CLI - logged in it creates a site in your account, logged out it deploys anonymously with a one-hour claim link. BYOK, no central account, no lock-in. `--dry-run` shows the plan first.
|
|
72
|
+
- **Backend-capable build (done):** when the three-question backend test shows a product genuinely needs a server, `/tl-build` emits a real backend alongside the static front end (serverless functions per backend requirement, a `schema.sql`, a names-only `.env.example`, an updated `netlify.toml`, and a `BACKEND.md` guide), with Neon Postgres as the documented default and overridable to any Postgres. Static stays the default, gated by the same backend test.
|
|
73
|
+
- **Next:** automated backend deploy. `tl deploy` ships the static front end today and points at `BACKEND.md` for the one-time backend steps; teaching the deploy step to provision the database and ship the functions into your own account is the follow-up.
|
|
68
74
|
|
|
69
75
|
## Notes for contributors
|
|
70
76
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
The Thought Layer Kit is bring-your-own-key by design. It has no server, no
|
|
4
|
+
telemetry, and no central account. The threat model and the guarantees below
|
|
5
|
+
follow from that.
|
|
6
|
+
|
|
7
|
+
## What the kit handles
|
|
8
|
+
|
|
9
|
+
- **Secrets are read from the environment only.** The Netlify token
|
|
10
|
+
(`NETLIFY_AUTH_TOKEN` / `NETLIFY_TOKEN`) and the domain-check key
|
|
11
|
+
(`THOUGHT_LAYER_DOMAIN_KEY` / `RAPIDAPI_KEY`) are read from `process.env`. They
|
|
12
|
+
are never accepted as tool or CLI parameters, never logged or printed, and
|
|
13
|
+
never written to disk. The `deploy.json` record stores the resulting URLs and
|
|
14
|
+
ids, never the token. When a build emits a backend, the generated `.env.example`
|
|
15
|
+
is a names-only contract (every line is a bare `NAME=`): real values live only
|
|
16
|
+
in the host environment, never in a committed file.
|
|
17
|
+
- **Deploys go to your own account.** With a token, the deploy uses Netlify's
|
|
18
|
+
file-digest API to publish into your account. With no token, it delegates to
|
|
19
|
+
your installed Netlify CLI (a site in your account when logged in, or an
|
|
20
|
+
anonymous, claimable site when logged out). Nothing is hosted on infrastructure
|
|
21
|
+
we control, and there is no claim handshake we mediate.
|
|
22
|
+
- **No shell injection.** External commands (the Netlify CLI) are invoked with an
|
|
23
|
+
argument array and no shell, so values such as a site name or publish directory
|
|
24
|
+
cannot break out into a shell. Site names are sanitized to `[a-z0-9-]`.
|
|
25
|
+
- **File writes are confined.** The scaffold and state-file writers resolve paths
|
|
26
|
+
against the working directory; the state file and build artifacts live under
|
|
27
|
+
`.thought-layer/` and the chosen publish directory.
|
|
28
|
+
- **No network calls you did not ask for.** The only outbound requests are to the
|
|
29
|
+
Netlify API (deploy) and, if you set a domain key, the RapidAPI domains
|
|
30
|
+
endpoint. There is no analytics or phone-home.
|
|
31
|
+
|
|
32
|
+
## What stays your responsibility
|
|
33
|
+
|
|
34
|
+
- The kit runs inside your own agent (Pi, Claude Code, or another) on your own
|
|
35
|
+
model and keys. The quality and safety of code an agent builds from a spec is a
|
|
36
|
+
function of that agent and model, not the kit.
|
|
37
|
+
- Keep your provider keys and Netlify token in your environment or your agent's
|
|
38
|
+
secret store, not in committed files. `.thought-layer/` and `.env` are
|
|
39
|
+
gitignored in this repo for that reason.
|
|
40
|
+
|
|
41
|
+
## Reporting a vulnerability
|
|
42
|
+
|
|
43
|
+
Email security reports to **jerm@hobocode.net**. Please include steps to
|
|
44
|
+
reproduce and the affected version. We aim to acknowledge within a few days.
|
|
45
|
+
Public disclosure is welcome once a fix is released.
|
|
46
|
+
|
|
47
|
+
## Supported versions
|
|
48
|
+
|
|
49
|
+
The latest published `@hobocode/thought-layer` release on npm is the supported
|
|
50
|
+
version. Fixes ship forward; please update before reporting.
|
package/core/backend.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// The typed contract + deterministic boilerplate for the backend-capable build.
|
|
2
|
+
//
|
|
3
|
+
// Static stays the default. When the build skill's three-question backend test
|
|
4
|
+
// says a product genuinely needs a server (a browser-unsafe secret, shared or
|
|
5
|
+
// persistent state, or trusted server-side enforcement), the agent emits a real
|
|
6
|
+
// serverless backend alongside the static front end. This module is the
|
|
7
|
+
// deterministic floor for that path, mirroring scaffold.ts: the agent supplies
|
|
8
|
+
// judgment (which functions, which tables), and these pure helpers render the
|
|
9
|
+
// fixed, dash-free boilerplate (the env contract and the deploy guide) and
|
|
10
|
+
// coerce a hand-written backend block into a clean shape.
|
|
11
|
+
//
|
|
12
|
+
// Pure TypeScript, no node imports. The chosen runtime DB driver
|
|
13
|
+
// (`@neondatabase/serverless`) is a dependency of the GENERATED product, never
|
|
14
|
+
// of the kit. Nothing here holds or provisions credentials: secrets live only
|
|
15
|
+
// in the host environment, and the emitted .env.example carries names, never
|
|
16
|
+
// values.
|
|
17
|
+
//
|
|
18
|
+
// Copy rule (enforced by tests): no em-dashes, no en-dashes, and no spaced
|
|
19
|
+
// hyphen dashes (" - ", " -- ") in any generated prose. Use commas, colons,
|
|
20
|
+
// periods, and parentheses instead.
|
|
21
|
+
|
|
22
|
+
export type BackendKind = "serverless" | "server" | null;
|
|
23
|
+
|
|
24
|
+
// One environment variable the backend reads. Names only: a value never lives
|
|
25
|
+
// in this spec, the manifest, or the emitted .env.example. Real values stay in
|
|
26
|
+
// the host environment.
|
|
27
|
+
export interface EnvVarSpec {
|
|
28
|
+
name: string;
|
|
29
|
+
required: boolean;
|
|
30
|
+
description: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Where the backend keeps shared or persistent state. The kit's documented
|
|
34
|
+
// default is Neon Postgres, reached through the `DATABASE_URL` connection
|
|
35
|
+
// string. Overridable to any Postgres by pointing `DATABASE_URL` elsewhere.
|
|
36
|
+
export interface DatabaseSpec {
|
|
37
|
+
provider: string; // "neon" by default
|
|
38
|
+
schemaFile: string; // "schema.sql"
|
|
39
|
+
envVar: string; // "DATABASE_URL"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The structured backend payload carried on the build manifest. The build skill
|
|
43
|
+
// writes it when hasBackend is true; the deploy-automation follow-up consumes
|
|
44
|
+
// it. Today the deploy step only reads it for messaging.
|
|
45
|
+
export interface BackendMeta {
|
|
46
|
+
backendKind: BackendKind;
|
|
47
|
+
functionsDir: string; // "netlify/functions"
|
|
48
|
+
runtime: string; // "nodejs20.x"
|
|
49
|
+
nodeVersion: string; // "20"
|
|
50
|
+
envVars: EnvVarSpec[];
|
|
51
|
+
database: DatabaseSpec | null;
|
|
52
|
+
guide: string; // "BACKEND.md"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The single documented default database. Neon is also exactly what managed
|
|
56
|
+
// Netlify DB provisions, so it is both the Netlify native choice and portable.
|
|
57
|
+
export const NEON_DEFAULT_DB: DatabaseSpec = {
|
|
58
|
+
provider: "neon",
|
|
59
|
+
schemaFile: "schema.sql",
|
|
60
|
+
envVar: "DATABASE_URL",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---- helpers -----------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
// Coerce a name to a safe environment-variable identifier. Real env var names
|
|
66
|
+
// are UPPER_SNAKE; this hardens the names-only invariant so a stray space or
|
|
67
|
+
// lowercase letter can never produce a line that fails the `^[A-Z0-9_]+=$`
|
|
68
|
+
// security check.
|
|
69
|
+
function envName(raw: string): string {
|
|
70
|
+
const cleaned = String(raw || "")
|
|
71
|
+
.trim()
|
|
72
|
+
.toUpperCase()
|
|
73
|
+
.replace(/[^A-Z0-9_]+/g, "_")
|
|
74
|
+
.replace(/^_+|_+$/g, "");
|
|
75
|
+
return cleaned || "VAR";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Dedupe by safe name and sort, so the same set of vars always renders byte for
|
|
79
|
+
// byte the same regardless of input order.
|
|
80
|
+
function dedupeSortEnv(envVars: EnvVarSpec[]): EnvVarSpec[] {
|
|
81
|
+
const seen = new Set<string>();
|
|
82
|
+
const out: EnvVarSpec[] = [];
|
|
83
|
+
for (const v of envVars || []) {
|
|
84
|
+
const name = envName(v?.name ?? "");
|
|
85
|
+
if (seen.has(name)) continue;
|
|
86
|
+
seen.add(name);
|
|
87
|
+
out.push({ name, required: v?.required !== false, description: String(v?.description ?? "") });
|
|
88
|
+
}
|
|
89
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- the names-only env contract ---------------------------------------------
|
|
93
|
+
|
|
94
|
+
// Render a deterministic .env.example. Every variable line is exactly `NAME=`
|
|
95
|
+
// with nothing after the equals sign. The security invariant (no value is ever
|
|
96
|
+
// written) is locked by a unit test that matches `^[A-Z0-9_]+=$` on every
|
|
97
|
+
// non-comment, non-blank line.
|
|
98
|
+
export function renderEnvExample(envVars: EnvVarSpec[]): string {
|
|
99
|
+
const vars = dedupeSortEnv(envVars);
|
|
100
|
+
const lines: string[] = [
|
|
101
|
+
"# Environment variables for this project.",
|
|
102
|
+
"# Names only. Real values live in your host environment (the Netlify UI, or netlify env:set), never in this file.",
|
|
103
|
+
"# Copy this file to .env for local work and fill the values there. .env is gitignored; this .env.example is committed so the contract travels with the repo.",
|
|
104
|
+
];
|
|
105
|
+
if (vars.length === 0) {
|
|
106
|
+
lines.push("", "# No backend environment variables were declared for this build.");
|
|
107
|
+
return lines.join("\n") + "\n";
|
|
108
|
+
}
|
|
109
|
+
for (const v of vars) {
|
|
110
|
+
const note = v.description.trim() ? v.description.trim() : "Set this value in your host environment.";
|
|
111
|
+
lines.push("", `# ${note} (${v.required ? "required" : "optional"})`, `${v.name}=`);
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n") + "\n";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- the deploy guide --------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export interface BackendEndpoint {
|
|
119
|
+
name: string; // function file name without extension, glossary-named
|
|
120
|
+
rid: string; // the backend requirement id it implements
|
|
121
|
+
summary: string; // one line of what it does
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface BackendGuideInput {
|
|
125
|
+
brandName?: string;
|
|
126
|
+
backendKind?: BackendKind;
|
|
127
|
+
functionsDir?: string;
|
|
128
|
+
runtime?: string;
|
|
129
|
+
database?: DatabaseSpec | null;
|
|
130
|
+
envVars?: EnvVarSpec[];
|
|
131
|
+
endpoints?: BackendEndpoint[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const cell = (s: string): string => String(s ?? "").replace(/\|/g, "\\|").replace(/\n+/g, " ").trim();
|
|
135
|
+
|
|
136
|
+
// Render a deterministic BACKEND.md for the Neon-on-Netlify path. The agent
|
|
137
|
+
// supplies the endpoint and env lists; the prose, section order, and the honest
|
|
138
|
+
// "deploy automation is a follow-up" status are fixed and dash-free.
|
|
139
|
+
export function renderBackendGuide(input: BackendGuideInput): string {
|
|
140
|
+
const brand = (input.brandName || "This project").trim() || "This project";
|
|
141
|
+
const functionsDir = (input.functionsDir || "netlify/functions").trim() || "netlify/functions";
|
|
142
|
+
const db = input.database ?? NEON_DEFAULT_DB;
|
|
143
|
+
const envVars = dedupeSortEnv(input.envVars || []);
|
|
144
|
+
const endpoints = [...(input.endpoints || [])].sort(
|
|
145
|
+
(a, b) => String(a.rid).localeCompare(String(b.rid)) || String(a.name).localeCompare(String(b.name)),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const envRows =
|
|
149
|
+
envVars.length > 0
|
|
150
|
+
? envVars
|
|
151
|
+
.map((v) => `| \`${v.name}\` | ${v.required ? "yes" : "no"} | ${cell(v.description) || "Set in your host environment."} |`)
|
|
152
|
+
.join("\n")
|
|
153
|
+
: `| \`${db.envVar}\` | yes | ${db.provider === "neon" ? "Neon Postgres connection string" : "Database connection string"} |`;
|
|
154
|
+
|
|
155
|
+
const fnRows =
|
|
156
|
+
endpoints.length > 0
|
|
157
|
+
? endpoints.map((e) => `| \`${cell(e.name)}\` | ${cell(e.rid)} | ${cell(e.summary) || "See the function source."} |`).join("\n")
|
|
158
|
+
: "| (none emitted) | n/a | This build declared a backend but wrote no functions; see DECISIONS.md. |";
|
|
159
|
+
|
|
160
|
+
return `# Backend deploy guide
|
|
161
|
+
|
|
162
|
+
${brand} needs a server side, so the build emitted a backend alongside the static front end. This guide covers what is in the repo, the honest status of automated backend deploy, and the one-time manual steps to take it live yourself.
|
|
163
|
+
|
|
164
|
+
## What is in the repo
|
|
165
|
+
|
|
166
|
+
- \`${functionsDir}/\`: one serverless function per backend requirement (the table further down maps each to its R-ID). The front end calls a function at \`/.netlify/functions/<name>\`.
|
|
167
|
+
- \`${db.schemaFile}\`: the database schema. Tables and columns use the project glossary terms. Apply it once against your database.
|
|
168
|
+
- \`.env.example\`: the environment variable names this backend reads. Names only, no values. Copy it to \`.env\` for local work and set real values in your host environment.
|
|
169
|
+
- \`netlify.toml\`: declares the functions directory (\`[functions] directory = "${functionsDir}"\`) so Netlify bundles and serves the functions next to the static publish directory.
|
|
170
|
+
|
|
171
|
+
## Status: automated backend deploy is a follow-up
|
|
172
|
+
|
|
173
|
+
\`tl deploy\` publishes the static front end today. It does not yet provision the database or ship the functions for you; that automation is the next piece of work. Until it lands, do the steps below once. The front end deploys and works now; the functions go live when you complete these steps.
|
|
174
|
+
|
|
175
|
+
## 1. Provision the database
|
|
176
|
+
|
|
177
|
+
The documented default is Neon Postgres. Netlify DB is managed Neon, so this is the Netlify native choice as well as a portable one.
|
|
178
|
+
|
|
179
|
+
1. Create a Neon project at neon.tech, or run \`netlify db init\` to provision managed Neon from your own Netlify account.
|
|
180
|
+
2. Copy the connection string. It looks like \`postgresql://USER:PASSWORD@HOST/DB?sslmode=require\`.
|
|
181
|
+
3. Apply the schema once: \`psql "$${db.envVar}" -f ${db.schemaFile}\`.
|
|
182
|
+
|
|
183
|
+
When you provision managed Neon through Netlify, Netlify sets \`NETLIFY_DATABASE_URL\` for you. The code reads \`${db.envVar}\` as the portable name, so set that (or map one to the other) in the next step.
|
|
184
|
+
|
|
185
|
+
To use a different Postgres provider, point \`${db.envVar}\` at it instead. Any standard Postgres works with the Neon serverless driver, so this is not locked to one vendor.
|
|
186
|
+
|
|
187
|
+
## 2. Set the environment variables
|
|
188
|
+
|
|
189
|
+
Set these in your host environment (the Netlify UI under Site settings, Environment variables, or \`netlify env:set NAME value\`). Never commit real values; \`.env.example\` carries names only.
|
|
190
|
+
|
|
191
|
+
| Variable | Required | Purpose |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
${envRows}
|
|
194
|
+
|
|
195
|
+
## 3. The functions
|
|
196
|
+
|
|
197
|
+
Each backend requirement maps to one function. The front end reaches it at \`/.netlify/functions/<name>\`.
|
|
198
|
+
|
|
199
|
+
| Function | Requirement | Does |
|
|
200
|
+
| --- | --- | --- |
|
|
201
|
+
${fnRows}
|
|
202
|
+
|
|
203
|
+
## 4. Deploy with the functions present
|
|
204
|
+
|
|
205
|
+
1. Confirm \`netlify.toml\` declares the functions directory.
|
|
206
|
+
2. Set the environment variables from step 2 on the target site.
|
|
207
|
+
3. From the project root, with the functions in place, run \`netlify deploy --prod\`. (Once the backend deploy automation lands, \`tl deploy\` will do this for you.)
|
|
208
|
+
|
|
209
|
+
\`netlify deploy\` ships both the static publish directory and the functions in one go. After it completes, call a function once to confirm it responds, then use the live site.
|
|
210
|
+
`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- defensive normalizer ----------------------------------------------------
|
|
214
|
+
|
|
215
|
+
function normalizeDatabase(raw: unknown): DatabaseSpec | null {
|
|
216
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
217
|
+
const r = raw as Record<string, unknown>;
|
|
218
|
+
const str = (v: unknown, fb: string): string => (typeof v === "string" && v.trim() ? v.trim() : fb);
|
|
219
|
+
return {
|
|
220
|
+
provider: str(r["provider"], "neon"),
|
|
221
|
+
schemaFile: str(r["schemaFile"], "schema.sql"),
|
|
222
|
+
envVar: str(r["envVar"], "DATABASE_URL"),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function normalizeEnvVars(raw: unknown): EnvVarSpec[] {
|
|
227
|
+
if (!Array.isArray(raw)) return [];
|
|
228
|
+
const seen = new Set<string>();
|
|
229
|
+
const out: EnvVarSpec[] = [];
|
|
230
|
+
for (const item of raw) {
|
|
231
|
+
if (!item || typeof item !== "object") continue;
|
|
232
|
+
const r = item as Record<string, unknown>;
|
|
233
|
+
const name = typeof r["name"] === "string" ? r["name"].trim() : "";
|
|
234
|
+
if (!name) continue; // drop nameless vars
|
|
235
|
+
if (seen.has(name)) continue; // dedupe
|
|
236
|
+
seen.add(name);
|
|
237
|
+
out.push({
|
|
238
|
+
name,
|
|
239
|
+
required: typeof r["required"] === "boolean" ? r["required"] : true,
|
|
240
|
+
description: typeof r["description"] === "string" ? r["description"] : "",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Coerce a hand-written backend block (the agent fills it into build.json) into
|
|
247
|
+
// a clean BackendMeta, or null when there is nothing recognizable. Mirrors the
|
|
248
|
+
// progress.ts normalizers: forgiving on input, strict on output.
|
|
249
|
+
export function normalizeBackendMeta(raw: unknown): BackendMeta | null {
|
|
250
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
251
|
+
const r = raw as Record<string, unknown>;
|
|
252
|
+
|
|
253
|
+
const kindRaw = r["backendKind"];
|
|
254
|
+
const kind: BackendKind = kindRaw === "server" ? "server" : kindRaw === "serverless" ? "serverless" : null;
|
|
255
|
+
const envVars = normalizeEnvVars(r["envVars"]);
|
|
256
|
+
const database = normalizeDatabase(r["database"]);
|
|
257
|
+
const hasFunctionsDir = typeof r["functionsDir"] === "string" && (r["functionsDir"] as string).trim().length > 0;
|
|
258
|
+
|
|
259
|
+
// Nothing the deploy step could act on: treat as no backend.
|
|
260
|
+
if (kind === null && envVars.length === 0 && database === null && !hasFunctionsDir) return null;
|
|
261
|
+
|
|
262
|
+
const str = (v: unknown, fb: string): string => (typeof v === "string" && v.trim() ? v.trim() : fb);
|
|
263
|
+
return {
|
|
264
|
+
backendKind: kind ?? "serverless",
|
|
265
|
+
functionsDir: str(r["functionsDir"], "netlify/functions"),
|
|
266
|
+
runtime: str(r["runtime"], "nodejs20.x"),
|
|
267
|
+
nodeVersion: str(r["nodeVersion"], "20"),
|
|
268
|
+
envVars,
|
|
269
|
+
database,
|
|
270
|
+
guide: str(r["guide"], "BACKEND.md"),
|
|
271
|
+
};
|
|
272
|
+
}
|
package/core/deploy-io.ts
CHANGED
|
@@ -257,9 +257,22 @@ export async function runDeploy(opts: DeployRunOptions, ctx: { deployedAt: strin
|
|
|
257
257
|
return { ok: false, message: `Publish dir ${publishDirAbs} is empty - nothing to deploy.`, details: {} };
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
+
// build.json may predate the backend block, so read it defensively.
|
|
261
|
+
const backend = manifest.backend ?? null;
|
|
260
262
|
const backendWarn = manifest.hasBackend
|
|
261
|
-
?
|
|
262
|
-
|
|
263
|
+
? (() => {
|
|
264
|
+
const guide = backend?.guide || "BACKEND.md";
|
|
265
|
+
const dbEnv = backend?.database?.envVar || "DATABASE_URL";
|
|
266
|
+
const names = (backend?.envVars || []).map((v) => v.name).filter(Boolean);
|
|
267
|
+
const others = names.filter((n) => n !== dbEnv);
|
|
268
|
+
const envList = others.length ? `${dbEnv} plus ${others.join(", ")}` : dbEnv;
|
|
269
|
+
return (
|
|
270
|
+
`\n\nStatic deploy: the front end goes live now. This project also has a backend ` +
|
|
271
|
+
`(build.json hasBackend is true${manifest.backendNote ? `: ${manifest.backendNote}` : ""}). ` +
|
|
272
|
+
`The backend artifact (serverless functions, schema.sql, a names-only .env.example) is built but not auto-deployed yet; backend deploy automation is a follow-up. ` +
|
|
273
|
+
`To run it yourself, follow ${guide}: provision Neon Postgres, set ${envList} in your host environment, then run netlify deploy with the functions present.`
|
|
274
|
+
);
|
|
275
|
+
})()
|
|
263
276
|
: "";
|
|
264
277
|
|
|
265
278
|
const token = process.env.NETLIFY_AUTH_TOKEN || process.env.NETLIFY_TOKEN || "";
|
|
@@ -318,7 +331,8 @@ export async function runDeploy(opts: DeployRunOptions, ctx: { deployedAt: strin
|
|
|
318
331
|
deployRecord({
|
|
319
332
|
deployedAt: ctx.deployedAt, mode: owned ? "cli" : "anonymous", publishDir: manifest.publishDir, fileCount,
|
|
320
333
|
url, adminUrl: null, claimUrl, siteId: null, deployId: null,
|
|
321
|
-
hasBackend: manifest.hasBackend, backendNote: manifest.backendNote,
|
|
334
|
+
hasBackend: manifest.hasBackend, backendNote: manifest.backendNote, backendKind: backend?.backendKind ?? null,
|
|
335
|
+
buildProducer: manifest.producer, stateFile,
|
|
322
336
|
}),
|
|
323
337
|
);
|
|
324
338
|
// If anonymity was explicitly asked for but the CLI is logged in, it went
|
|
@@ -349,7 +363,8 @@ export async function runDeploy(opts: DeployRunOptions, ctx: { deployedAt: strin
|
|
|
349
363
|
deployRecord({
|
|
350
364
|
deployedAt: ctx.deployedAt, mode: "token", publishDir: manifest.publishDir, fileCount,
|
|
351
365
|
url: r.url || null, adminUrl: r.adminUrl || null, claimUrl: null, siteId: r.siteId, deployId: r.deployId,
|
|
352
|
-
hasBackend: manifest.hasBackend, backendNote: manifest.backendNote,
|
|
366
|
+
hasBackend: manifest.hasBackend, backendNote: manifest.backendNote, backendKind: backend?.backendKind ?? null,
|
|
367
|
+
buildProducer: manifest.producer, stateFile,
|
|
353
368
|
}),
|
|
354
369
|
);
|
|
355
370
|
return {
|
package/core/deploy.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// import and it is pure (content in, hex out).
|
|
11
11
|
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
|
+
import type { BackendKind } from "./backend.ts";
|
|
13
14
|
|
|
14
15
|
export const sha1Hex = (data: Buffer | string): string =>
|
|
15
16
|
createHash("sha1").update(data).digest("hex");
|
|
@@ -106,6 +107,10 @@ export interface DeployRecord {
|
|
|
106
107
|
deployId: string | null;
|
|
107
108
|
hasBackend: boolean;
|
|
108
109
|
backendNote: string | null;
|
|
110
|
+
// Provenance of the backend payload from build.json (null for a static build).
|
|
111
|
+
// The static deploy does not ship a backend; this just records what the build
|
|
112
|
+
// declared, for the deploy-automation follow-up to read.
|
|
113
|
+
backendKind?: BackendKind | null;
|
|
109
114
|
buildProducer: "agent" | "scaffold" | null;
|
|
110
115
|
stateFile: string | null;
|
|
111
116
|
}
|
package/core/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from "./stages.ts";
|
|
|
15
15
|
export * from "./stage-map.ts";
|
|
16
16
|
export * from "./state-file.ts";
|
|
17
17
|
export * from "./state-ops.ts";
|
|
18
|
+
export * from "./backend.ts";
|
|
18
19
|
export * from "./scaffold.ts";
|
|
19
20
|
export * from "./scaffold-io.ts";
|
|
20
21
|
export * from "./deploy.ts";
|
package/core/scaffold.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// so this emits plain HTML. Pure: no fs, no Date - the caller supplies builtAt.
|
|
12
12
|
|
|
13
13
|
import type { ProgressState } from "./progress.ts";
|
|
14
|
+
import type { BackendMeta } from "./backend.ts";
|
|
14
15
|
|
|
15
16
|
export interface StarterSiteSpec {
|
|
16
17
|
brandName: string;
|
|
@@ -45,6 +46,11 @@ export interface BuildManifest {
|
|
|
45
46
|
stack: string;
|
|
46
47
|
hasBackend: boolean;
|
|
47
48
|
backendNote: string | null;
|
|
49
|
+
// The structured backend payload, present only when the build emitted a real
|
|
50
|
+
// backend (hasBackend true). Optional + nullable so existing static build.json
|
|
51
|
+
// files round-trip and the deploy reader tolerates its absence. The deploy
|
|
52
|
+
// automation follow-up consumes it; today only deploy messaging reads it.
|
|
53
|
+
backend?: BackendMeta | null;
|
|
48
54
|
buildCommand: string | null;
|
|
49
55
|
installCommand: string | null;
|
|
50
56
|
nodeVersion: string;
|
|
@@ -264,6 +270,7 @@ export function scaffoldManifest(
|
|
|
264
270
|
stack: "static",
|
|
265
271
|
hasBackend: false,
|
|
266
272
|
backendNote: null,
|
|
273
|
+
backend: null,
|
|
267
274
|
buildCommand: null,
|
|
268
275
|
installCommand: null,
|
|
269
276
|
nodeVersion: "20",
|
package/dist/tl.js
CHANGED
|
@@ -624,6 +624,7 @@ function scaffoldManifest(publishDir, builtAt, provenance) {
|
|
|
624
624
|
stack: "static",
|
|
625
625
|
hasBackend: false,
|
|
626
626
|
backendNote: null,
|
|
627
|
+
backend: null,
|
|
627
628
|
buildCommand: null,
|
|
628
629
|
installCommand: null,
|
|
629
630
|
nodeVersion: "20",
|
|
@@ -867,7 +868,17 @@ async function runDeploy(opts, ctx) {
|
|
|
867
868
|
if (fileCount === 0) {
|
|
868
869
|
return { ok: false, message: `Publish dir ${publishDirAbs} is empty - nothing to deploy.`, details: {} };
|
|
869
870
|
}
|
|
870
|
-
const
|
|
871
|
+
const backend = manifest.backend ?? null;
|
|
872
|
+
const backendWarn = manifest.hasBackend ? (() => {
|
|
873
|
+
const guide = backend?.guide || "BACKEND.md";
|
|
874
|
+
const dbEnv = backend?.database?.envVar || "DATABASE_URL";
|
|
875
|
+
const names = (backend?.envVars || []).map((v) => v.name).filter(Boolean);
|
|
876
|
+
const others = names.filter((n) => n !== dbEnv);
|
|
877
|
+
const envList = others.length ? `${dbEnv} plus ${others.join(", ")}` : dbEnv;
|
|
878
|
+
return `
|
|
879
|
+
|
|
880
|
+
Static deploy: the front end goes live now. This project also has a backend (build.json hasBackend is true${manifest.backendNote ? `: ${manifest.backendNote}` : ""}). The backend artifact (serverless functions, schema.sql, a names-only .env.example) is built but not auto-deployed yet; backend deploy automation is a follow-up. To run it yourself, follow ${guide}: provision Neon Postgres, set ${envList} in your host environment, then run netlify deploy with the functions present.`;
|
|
881
|
+
})() : "";
|
|
871
882
|
const token = process.env.NETLIFY_AUTH_TOKEN || process.env.NETLIFY_TOKEN || "";
|
|
872
883
|
const writeRecord = (rec) => {
|
|
873
884
|
const recPath = join3(dirname3(stateFile), "deploy.json");
|
|
@@ -921,6 +932,7 @@ async function runDeploy(opts, ctx) {
|
|
|
921
932
|
deployId: null,
|
|
922
933
|
hasBackend: manifest.hasBackend,
|
|
923
934
|
backendNote: manifest.backendNote,
|
|
935
|
+
backendKind: backend?.backendKind ?? null,
|
|
924
936
|
buildProducer: manifest.producer,
|
|
925
937
|
stateFile
|
|
926
938
|
})
|
|
@@ -957,6 +969,7 @@ Recorded ${recPath}.${backendWarn}` + (!url || !claimUrl ? `
|
|
|
957
969
|
deployId: r.deployId,
|
|
958
970
|
hasBackend: manifest.hasBackend,
|
|
959
971
|
backendNote: manifest.backendNote,
|
|
972
|
+
backendKind: backend?.backendKind ?? null,
|
|
960
973
|
buildProducer: manifest.producer,
|
|
961
974
|
stateFile
|
|
962
975
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hobocode/thought-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The Thought Layer: rigor for building. Validate an idea, grill it into a buildable spec, then build and deploy it, inside the agent you already use. BYOK, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hobocode LLC <jerm@hobocode.net>",
|
|
@@ -64,12 +64,14 @@
|
|
|
64
64
|
"core/stage-map.ts",
|
|
65
65
|
"core/state-file.ts",
|
|
66
66
|
"core/state-ops.ts",
|
|
67
|
+
"core/backend.ts",
|
|
67
68
|
"core/scaffold.ts",
|
|
68
69
|
"core/scaffold-io.ts",
|
|
69
70
|
"core/deploy.ts",
|
|
70
71
|
"core/deploy-io.ts",
|
|
71
72
|
"dist",
|
|
72
73
|
"README.md",
|
|
74
|
+
"SECURITY.md",
|
|
73
75
|
"LICENSE"
|
|
74
76
|
]
|
|
75
77
|
}
|
package/prompts/tl-build.md
CHANGED
|
@@ -2,6 +2,6 @@ Apply the **thought-layer-build** skill. Build the hardened PRD into a static-fi
|
|
|
2
2
|
|
|
3
3
|
If there is no hardened PRD in the state file (`state.prd.markdown` + requirements), say so and point me to `/tl` (or `/tl-prd` then `/tl-grill`) rather than building from a bare idea - only proceed cold if I tell you to.
|
|
4
4
|
|
|
5
|
-
Honor the ubiquitous language (the glossary), R-ID traceability, the out-of-scope list, mobile+desktop, the brand if present, and the full SEO/discoverability layer. Default to static;
|
|
5
|
+
Honor the ubiquitous language (the glossary), R-ID traceability, the out-of-scope list, mobile+desktop, the brand if present, and the full SEO/discoverability layer. Default to static; when the three-question backend test shows a requirement genuinely needs a server, build the real backend too: serverless functions under `netlify/functions/` (one per backend R-ID, glossary-named), a `schema.sql`, a names-only `.env.example` (with `!.env.example` un-ignored in `.gitignore`), an updated `netlify.toml`, and a `BACKEND.md` guide, recorded in the manifest's `backend` block. Be honest that backend deploy automation is a follow-up, so `tl deploy` still ships the static front end for now. Verify the build runs, and leave `.thought-layer/build.json` + `DECISIONS.md` + `TRACEABILITY.md`.
|
|
6
6
|
|
|
7
7
|
Read the spec from the state file (default `.thought-layer/state.json`; honor `--path` / `THOUGHT_LAYER_STATE` if a named file is in use). For the fastest deployable floor, or if a full build is too much, run the **tl_scaffold** tool to write a branded, SEO-complete static landing site and the same `build.json` manifest.
|
package/prompts/tl-deploy.md
CHANGED
|
@@ -2,6 +2,6 @@ Apply the **thought-layer-deploy** skill. Take the built site live to a URL I ow
|
|
|
2
2
|
|
|
3
3
|
Read `.thought-layer/build.json` (next to the state file; honor `--path` / `THOUGHT_LAYER_STATE` if a named file is in use) for the publish directory and entry. If there is no `build.json`, say so and point me to `/tl-build` (or the `tl_scaffold` tool) rather than guessing - the build has to run first.
|
|
4
4
|
|
|
5
|
-
Default to a dry run first so I can see exactly which files would ship and where. Then deploy: if `NETLIFY_AUTH_TOKEN` is set, deploy into my own Netlify account (owned immediately); otherwise delegate to my Netlify CLI - logged in it creates a site in my account, logged out it deploys anonymously with a one-hour claim link. Read the token only from the environment, never ask me to paste it. If `build.json` says `hasBackend: true`,
|
|
5
|
+
Default to a dry run first so I can see exactly which files would ship and where. Then deploy: if `NETLIFY_AUTH_TOKEN` is set, deploy into my own Netlify account (owned immediately); otherwise delegate to my Netlify CLI - logged in it creates a site in my account, logged out it deploys anonymously with a one-hour claim link. Read the token only from the environment, never ask me to paste it. If `build.json` says `hasBackend: true`, tell me plainly that this static deploy ships only the front end for now (backend deploy automation is a follow-up), and point me at `BACKEND.md` for the one-time steps to run the backend (provision Neon, set `DATABASE_URL`, then `netlify deploy` with the functions present).
|
|
6
6
|
|
|
7
7
|
After it is live, tell me the URL and (anonymous only) the claim link, and that a `.thought-layer/deploy.json` record was written.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thought-layer-build
|
|
3
|
-
description: "Turn the hardened PRD into a static-first, deploy-ready artifact, built directly by this agent. Reads the build brief from the shared state file (PRD, glossary, R-ID requirements, brand, business context, open to-dos), then builds a self-contained site or a Vite/static build that yields a predictable publish directory (dist/), honoring ubiquitous language, R-ID traceability, the out-of-scope list, mobile+desktop, brand, and the full SEO/discoverability layer.
|
|
3
|
+
description: "Turn the hardened PRD into a static-first, deploy-ready artifact, built directly by this agent. Reads the build brief from the shared state file (PRD, glossary, R-ID requirements, brand, business context, open to-dos), then builds a self-contained site or a Vite/static build that yields a predictable publish directory (dist/), honoring ubiquitous language, R-ID traceability, the out-of-scope list, mobile+desktop, brand, and the full SEO/discoverability layer. When the spec genuinely requires a server, it also emits a real backend (serverless functions under netlify/functions/, a schema.sql, a names-only .env.example, an updated netlify.toml, and a BACKEND.md guide) and records it in the manifest, while staying honest that backend deploy automation is a follow-up so the default deploy path stays static. Verifies the build runs and writes a .thought-layer/build.json manifest for the deploy step. Run it after the grill has hardened the PRD; it is a standalone build step, not a validation stage."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Build it: the hardened PRD becomes a deploy-ready artifact
|
|
@@ -49,7 +49,17 @@ Default to a self-contained static site or a Vite/Astro/static build whose outpu
|
|
|
49
49
|
|
|
50
50
|
If all three are no, build **static, full stop.** localStorage, static data files, BYOK client-side AI calls, and third-party embeddable widgets do **not** count as a backend - many specs that sound like they need a server can ship a compelling static slice first.
|
|
51
51
|
|
|
52
|
-
**When a backend is genuinely required
|
|
52
|
+
**When a backend is genuinely required, build it for real (do not just warn).** Build the static front end as above, set `hasBackend: true` and a one-line `backendNote`, and emit a coherent, buildable serverless backend alongside it:
|
|
53
|
+
|
|
54
|
+
- **Serverless functions, one per backend R-ID**, under `netlify/functions/`. Name each file in the ubiquitous language (the glossary term for what it does, e.g. `netlify/functions/dispatch.ts`), open it with a comment naming the R-ID it implements, and keep it inside the out-of-scope boundary. Each function reads its inputs, talks to the database, and returns JSON. Give the front end a clear seam: it calls the function at `fetch('/.netlify/functions/<name>')`, never a hardcoded host.
|
|
55
|
+
- **A `schema.sql`** at the project root, derived from the PRD data requirements and the domain entities. Name tables and columns in the glossary terms (no synonyms), and keep it idempotent where you can (`create table if not exists ...`).
|
|
56
|
+
- **Neon Postgres by default.** The functions reach the database through the Neon serverless driver (`@neondatabase/serverless`, added to the product's `package.json`, never the kit's) and read the connection string from `DATABASE_URL` (Netlify sets `NETLIFY_DATABASE_URL` when you provision managed Neon, so read `DATABASE_URL` and map it). Neon is the single documented default and is overridable to any Postgres by pointing `DATABASE_URL` elsewhere; state that in `BACKEND.md` and do not invent a second provider.
|
|
57
|
+
- **A names-only `.env.example`** at the project root listing every variable the backend reads (`DATABASE_URL` plus any others), each as a bare `NAME=` under a one-line comment. Never write a real value; real values live only in the host environment.
|
|
58
|
+
- **An updated `netlify.toml`.** Extend the existing publish + redirect block (do not replace it) with a `[functions]` table declaring `directory = "netlify/functions"`. Keep the static publish dir and the SPA redirect intact.
|
|
59
|
+
- **A `BACKEND.md` deploy guide** at the project root: what is in the repo, the honest status (automated backend deploy is a follow-up, so `tl deploy` ships only the front end today), how to provision Neon, the env-var table, the function-to-R-ID table, and the manual `netlify deploy` steps. The kit's `renderBackendGuide` and `renderEnvExample` helpers (in `core/backend.ts`) produce a dash-free skeleton if you have the core available; otherwise write the same content by hand.
|
|
60
|
+
- **A project `.gitignore`** that ignores `.env` and `.env.*` but un-ignores the contract with `!.env.example`. Without that line the env contract is silently un-committable, and the deploy follow-up cannot read it.
|
|
61
|
+
|
|
62
|
+
Then record the backend in the manifest's `backend` block (shape below). Do **not** attempt to deploy the backend in this step: `tl deploy` publishes the static front end, and backend deploy automation is the explicit follow-up. Say so plainly in chat and point the user at `BACKEND.md`.
|
|
53
63
|
|
|
54
64
|
## SEO and discoverability (build all of it, do not skip it)
|
|
55
65
|
|
|
@@ -67,7 +77,8 @@ Do not declare victory - check:
|
|
|
67
77
|
2. **Confirm the publish dir + entry load.** `dist/index.html` (or your entry) exists and is non-trivial. Where a preview or browser tool is available, load it and confirm it renders; otherwise inspect the built HTML for the expected title/nav/hero and that the mobile viewport meta is set.
|
|
68
78
|
3. **R-ID coverage.** Walk TRACEABILITY.md: each R-ID is implemented (with a pointer) or explicitly deferred (with a reason). Put the counts in `build.json.requirements`.
|
|
69
79
|
4. **SEO check.** Confirm the SEO files are actually in the publish dir; set `build.json.seo.*` from reality, not intent.
|
|
70
|
-
5. **
|
|
80
|
+
5. **Backend check (only when `hasBackend`).** Each backend R-ID maps to a function in TRACEABILITY.md; `netlify/functions/` has one file per backend R-ID; `.env.example` is values-free (every variable line is a bare `NAME=`, never a value); `schema.sql` is non-empty; `netlify.toml` declares the functions directory; `.gitignore` has `!.env.example`. Do **not** try to run the backend or reach a database here (there is no `DATABASE_URL` in the build env); confirm the artifact is coherent and buildable, not live.
|
|
81
|
+
6. **Report** what is built and what is deferred, plainly, in chat.
|
|
71
82
|
|
|
72
83
|
## Honest about being model-built
|
|
73
84
|
|
|
@@ -82,6 +93,7 @@ Write three files with your own file tools (the manifest is NOT a `tl_state` art
|
|
|
82
93
|
{ "app": "thought-layer", "kind": "build", "version": 1, "builtAt": "<ISO>",
|
|
83
94
|
"producer": "agent", "publishDir": "dist", "entry": "index.html",
|
|
84
95
|
"stack": "static|vite|astro|next-static|other", "hasBackend": false, "backendNote": null,
|
|
96
|
+
"backend": null,
|
|
85
97
|
"buildCommand": null, "installCommand": null, "nodeVersion": "20",
|
|
86
98
|
"provenance": { "stateFile": "<the file you read>", "prdTs": <state.prd.ts>, "grillDone": <bool>, "fromSpeedrun": <bool> },
|
|
87
99
|
"requirements": { "total": 0, "built": 0, "deferred": 0, "deferredIds": [] },
|
|
@@ -90,6 +102,17 @@ Write three files with your own file tools (the manifest is NOT a `tl_state` art
|
|
|
90
102
|
"verified": { "buildRan": true, "publishDirExists": true, "entryLoads": true, "notes": "..." } }
|
|
91
103
|
```
|
|
92
104
|
`publishDir` + `entry` are load-bearing - the deploy step reads them. (The `tl_scaffold` tool writes this same manifest with `producer: "scaffold"`.)
|
|
105
|
+
|
|
106
|
+
When `hasBackend` is `true`, populate `backend` (leave it `null` for a static build). This is the forward-looking contract the backend deploy automation will consume:
|
|
107
|
+
|
|
108
|
+
```jsonc
|
|
109
|
+
"backend": {
|
|
110
|
+
"backendKind": "serverless", "functionsDir": "netlify/functions",
|
|
111
|
+
"runtime": "nodejs20.x", "nodeVersion": "20",
|
|
112
|
+
"envVars": [{ "name": "DATABASE_URL", "required": true, "description": "Neon Postgres connection string" }],
|
|
113
|
+
"database": { "provider": "neon", "schemaFile": "schema.sql", "envVar": "DATABASE_URL" },
|
|
114
|
+
"guide": "BACKEND.md" }
|
|
115
|
+
```
|
|
93
116
|
- `DECISIONS.md` - every choice the spec did not pin down, one line each, with the reason.
|
|
94
117
|
- `TRACEABILITY.md` - the R-ID map. (`SEO.md` comes from the SEO step.)
|
|
95
118
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thought-layer-deploy
|
|
3
|
-
description: "Take the built site live to a user-owned URL with no lock-in, the last step after the build. Reads .thought-layer/build.json (the publish dir + entry) next to the state file, then deploys to Netlify by one of two BYOK models: with NETLIFY_AUTH_TOKEN set it deploys into the user's OWN account via the file-digest API (owned immediately, no claim), and with no token it delegates to the user's Netlify CLI (logged in: a new site in their account; logged out: an anonymous, claimable URL with a one-hour claim link). Static-first: if build.json says hasBackend
|
|
3
|
+
description: "Take the built site live to a user-owned URL with no lock-in, the last step after the build. Reads .thought-layer/build.json (the publish dir + entry) next to the state file, then deploys to Netlify by one of two BYOK models: with NETLIFY_AUTH_TOKEN set it deploys into the user's OWN account via the file-digest API (owned immediately, no claim), and with no token it delegates to the user's Netlify CLI (logged in: a new site in their account; logged out: an anonymous, claimable URL with a one-hour claim link). Static-first: if build.json says hasBackend, the build also emitted a backend (functions, schema.sql, a names-only .env.example, BACKEND.md), but backend deploy automation is a follow-up, so this step ships only the front end for now and points at BACKEND.md. Prefers the deploy tool (Pi) or the tl deploy CLI (any shell agent) so the deploy is one mechanical, honest step, never hand-rolled. Run it after thought-layer-build (or tl_scaffold) has produced build.json."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Deploy it: the build goes live to a URL you own
|
|
@@ -35,7 +35,7 @@ If neither a token nor a usable CLI is available, relay the tool's guidance hone
|
|
|
35
35
|
|
|
36
36
|
## Static-first honesty
|
|
37
37
|
|
|
38
|
-
The default deploy publishes a **static** publish directory. If `build.json.hasBackend` is `true`, the
|
|
38
|
+
The default deploy publishes a **static** publish directory. If `build.json.hasBackend` is `true`, the build also emitted a real backend (serverless functions, a `schema.sql`, a names-only `.env.example`, an updated `netlify.toml`, and a `BACKEND.md` guide), but **backend deploy automation is a follow-up**, so this step ships only the front end for now. The tool warns; repeat its guidance plainly and point the user at `BACKEND.md`. To run the backend they provision Neon Postgres, set `DATABASE_URL` (and the other names from `.env.example`) in their host environment, then run `netlify deploy` with the functions present. Do not imply a backend is running when it is not.
|
|
39
39
|
|
|
40
40
|
## After it is live
|
|
41
41
|
|