@obh/forge 0.2.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/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/capabilities/analytics.d.ts +3 -0
- package/dist/capabilities/analytics.d.ts.map +1 -0
- package/dist/capabilities/analytics.js +91 -0
- package/dist/capabilities/analytics.js.map +1 -0
- package/dist/capabilities/api-keys.d.ts +3 -0
- package/dist/capabilities/api-keys.d.ts.map +1 -0
- package/dist/capabilities/api-keys.js +90 -0
- package/dist/capabilities/api-keys.js.map +1 -0
- package/dist/capabilities/audit.d.ts +3 -0
- package/dist/capabilities/audit.d.ts.map +1 -0
- package/dist/capabilities/audit.js +74 -0
- package/dist/capabilities/audit.js.map +1 -0
- package/dist/capabilities/entitlements.d.ts +3 -0
- package/dist/capabilities/entitlements.d.ts.map +1 -0
- package/dist/capabilities/entitlements.js +94 -0
- package/dist/capabilities/entitlements.js.map +1 -0
- package/dist/capabilities/events.d.ts +3 -0
- package/dist/capabilities/events.d.ts.map +1 -0
- package/dist/capabilities/events.js +107 -0
- package/dist/capabilities/events.js.map +1 -0
- package/dist/capabilities/files.d.ts +3 -0
- package/dist/capabilities/files.d.ts.map +1 -0
- package/dist/capabilities/files.js +100 -0
- package/dist/capabilities/files.js.map +1 -0
- package/dist/capabilities/helpers.d.ts +27 -0
- package/dist/capabilities/helpers.d.ts.map +1 -0
- package/dist/capabilities/helpers.js +63 -0
- package/dist/capabilities/helpers.js.map +1 -0
- package/dist/capabilities/import-export.d.ts +3 -0
- package/dist/capabilities/import-export.d.ts.map +1 -0
- package/dist/capabilities/import-export.js +267 -0
- package/dist/capabilities/import-export.js.map +1 -0
- package/dist/capabilities/index.d.ts +16 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +71 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/jobs.d.ts +3 -0
- package/dist/capabilities/jobs.d.ts.map +1 -0
- package/dist/capabilities/jobs.js +117 -0
- package/dist/capabilities/jobs.js.map +1 -0
- package/dist/capabilities/notifications.d.ts +3 -0
- package/dist/capabilities/notifications.d.ts.map +1 -0
- package/dist/capabilities/notifications.js +113 -0
- package/dist/capabilities/notifications.js.map +1 -0
- package/dist/capabilities/search.d.ts +3 -0
- package/dist/capabilities/search.d.ts.map +1 -0
- package/dist/capabilities/search.js +121 -0
- package/dist/capabilities/search.js.map +1 -0
- package/dist/capabilities/settings.d.ts +3 -0
- package/dist/capabilities/settings.d.ts.map +1 -0
- package/dist/capabilities/settings.js +75 -0
- package/dist/capabilities/settings.js.map +1 -0
- package/dist/capabilities/types.d.ts +16 -0
- package/dist/capabilities/types.d.ts.map +1 -0
- package/dist/capabilities/types.js +3 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/capabilities/webhooks.d.ts +3 -0
- package/dist/capabilities/webhooks.d.ts.map +1 -0
- package/dist/capabilities/webhooks.js +117 -0
- package/dist/capabilities/webhooks.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +88 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +94 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +41 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/inspect.d.ts +3 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +64 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/new.d.ts +18 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +149 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/shared.d.ts +14 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +36 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +59 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/stack.d.ts +13 -0
- package/dist/commands/stack.d.ts.map +1 -0
- package/dist/commands/stack.js +192 -0
- package/dist/commands/stack.js.map +1 -0
- package/dist/generators/api.d.ts +16 -0
- package/dist/generators/api.d.ts.map +1 -0
- package/dist/generators/api.js +375 -0
- package/dist/generators/api.js.map +1 -0
- package/dist/generators/lwd.d.ts +12 -0
- package/dist/generators/lwd.d.ts.map +1 -0
- package/dist/generators/lwd.js +143 -0
- package/dist/generators/lwd.js.map +1 -0
- package/dist/generators/mobile.d.ts +16 -0
- package/dist/generators/mobile.d.ts.map +1 -0
- package/dist/generators/mobile.js +180 -0
- package/dist/generators/mobile.js.map +1 -0
- package/dist/generators/platform-package.d.ts +18 -0
- package/dist/generators/platform-package.d.ts.map +1 -0
- package/dist/generators/platform-package.js +568 -0
- package/dist/generators/platform-package.js.map +1 -0
- package/dist/generators/root.d.ts +15 -0
- package/dist/generators/root.d.ts.map +1 -0
- package/dist/generators/root.js +249 -0
- package/dist/generators/root.js.map +1 -0
- package/dist/generators/sdk.d.ts +14 -0
- package/dist/generators/sdk.d.ts.map +1 -0
- package/dist/generators/sdk.js +137 -0
- package/dist/generators/sdk.js.map +1 -0
- package/dist/generators/web.d.ts +15 -0
- package/dist/generators/web.d.ts.map +1 -0
- package/dist/generators/web.js +203 -0
- package/dist/generators/web.js.map +1 -0
- package/dist/generators/worker.d.ts +13 -0
- package/dist/generators/worker.d.ts.map +1 -0
- package/dist/generators/worker.js +148 -0
- package/dist/generators/worker.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/project/context.d.ts +24 -0
- package/dist/project/context.d.ts.map +1 -0
- package/dist/project/context.js +63 -0
- package/dist/project/context.js.map +1 -0
- package/dist/project/manifest.d.ts +26 -0
- package/dist/project/manifest.d.ts.map +1 -0
- package/dist/project/manifest.js +73 -0
- package/dist/project/manifest.js.map +1 -0
- package/dist/project/paths.d.ts +25 -0
- package/dist/project/paths.d.ts.map +1 -0
- package/dist/project/paths.js +51 -0
- package/dist/project/paths.js.map +1 -0
- package/dist/project/plan.d.ts +42 -0
- package/dist/project/plan.d.ts.map +1 -0
- package/dist/project/plan.js +251 -0
- package/dist/project/plan.js.map +1 -0
- package/dist/prompts/index.d.ts +24 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +111 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/recipes/index.d.ts +25 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +77 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/stack/infer.d.ts +4 -0
- package/dist/stack/infer.d.ts.map +1 -0
- package/dist/stack/infer.js +165 -0
- package/dist/stack/infer.js.map +1 -0
- package/dist/stack/lwd.d.ts +24 -0
- package/dist/stack/lwd.d.ts.map +1 -0
- package/dist/stack/lwd.js +62 -0
- package/dist/stack/lwd.js.map +1 -0
- package/dist/stack/lwdtoml.d.ts +38 -0
- package/dist/stack/lwdtoml.d.ts.map +1 -0
- package/dist/stack/lwdtoml.js +57 -0
- package/dist/stack/lwdtoml.js.map +1 -0
- package/dist/stack/manifest.d.ts +15 -0
- package/dist/stack/manifest.d.ts.map +1 -0
- package/dist/stack/manifest.js +86 -0
- package/dist/stack/manifest.js.map +1 -0
- package/dist/stack/types.d.ts +48 -0
- package/dist/stack/types.d.ts.map +1 -0
- package/dist/stack/types.js +5 -0
- package/dist/stack/types.js.map +1 -0
- package/dist/stack/wire.d.ts +40 -0
- package/dist/stack/wire.d.ts.map +1 -0
- package/dist/stack/wire.js +192 -0
- package/dist/stack/wire.js.map +1 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +64 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +17 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +49 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/shell.d.ts +21 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +40 -0
- package/dist/utils/shell.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +40 -0
- package/skills/obh-add-events/SKILL.md +26 -0
- package/skills/obh-generate-audit-rules/SKILL.md +26 -0
- package/skills/obh-inspect-project/SKILL.md +26 -0
- package/skills/obh-lwd-manifest/SKILL.md +26 -0
- package/skills/obh-retrofit-files/SKILL.md +24 -0
- package/skills/obh-retrofit-jobs/SKILL.md +26 -0
- package/skills/obh-sdk-extraction/SKILL.md +24 -0
- package/skills/obh-settings-migration/SKILL.md +24 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addApiApp = addApiApp;
|
|
4
|
+
/**
|
|
5
|
+
* Generates the API app. Two framework flavours (Hono, Express) share the same
|
|
6
|
+
* framework-agnostic core: a pg pool with a `withTx` helper, a transaction-aware
|
|
7
|
+
* domain event bus (subscribers auto-loaded from src/bus.d/*), and — unless
|
|
8
|
+
* disabled — a real `notes` domain (CRUD over Postgres that emits domain facts
|
|
9
|
+
* inside its transaction). Routes auto-mount from src/routes/*.
|
|
10
|
+
*/
|
|
11
|
+
function addApiApp(plan, opts) {
|
|
12
|
+
const dir = "apps/api";
|
|
13
|
+
const hono = opts.framework === "hono";
|
|
14
|
+
plan.create(`${dir}/package.json`, packageJson(opts.scope, opts.framework), "API app package.json");
|
|
15
|
+
plan.create(`${dir}/tsconfig.json`, tsconfig(), "API tsconfig");
|
|
16
|
+
// Framework-agnostic core.
|
|
17
|
+
plan.create(`${dir}/src/config.ts`, CONFIG, "API config loader");
|
|
18
|
+
plan.create(`${dir}/src/db.ts`, DB, "API Postgres pool + withTx");
|
|
19
|
+
plan.create(`${dir}/src/bus.ts`, BUS, "in-app domain event bus (auto-loads src/bus.d/*)");
|
|
20
|
+
plan.create(`${dir}/src/bus.d/.gitkeep`, "", "bus subscribers directory");
|
|
21
|
+
// Framework-specific server + health route.
|
|
22
|
+
plan.create(`${dir}/src/server.ts`, hono ? SERVER_HONO : SERVER_EXPRESS, "API server (auto-mounts src/routes/*)");
|
|
23
|
+
plan.create(`${dir}/src/routes/health.ts`, hono ? HEALTH_HONO : HEALTH_EXPRESS, "health route");
|
|
24
|
+
// Example domain.
|
|
25
|
+
if (opts.example === "notes") {
|
|
26
|
+
plan.create(`${dir}/src/domain/notes.ts`, NOTES_DOMAIN, "notes domain (real CRUD + emits facts in-tx)");
|
|
27
|
+
plan.create(`${dir}/src/routes/notes.ts`, hono ? NOTES_HONO : NOTES_EXPRESS, "notes CRUD route");
|
|
28
|
+
plan.create("migrations/0002_notes.sql", NOTES_SQL, "notes table migration");
|
|
29
|
+
}
|
|
30
|
+
plan.create(`${dir}/Dockerfile`, DOCKERFILE, "API Dockerfile");
|
|
31
|
+
}
|
|
32
|
+
function packageJson(scope, framework) {
|
|
33
|
+
const deps = framework === "hono"
|
|
34
|
+
? { "@hono/node-server": "^1.12.0", hono: "^4.5.0", pg: "^8.12.0" }
|
|
35
|
+
: { cors: "^2.8.5", express: "^4.19.2", pg: "^8.12.0" };
|
|
36
|
+
const devDeps = framework === "hono"
|
|
37
|
+
? { "@types/node": "^20.14.0", "@types/pg": "^8.11.6", tsx: "^4.16.0", typescript: "^5.5.4", vitest: "^2.0.5" }
|
|
38
|
+
: {
|
|
39
|
+
"@types/cors": "^2.8.17",
|
|
40
|
+
"@types/express": "^4.17.21",
|
|
41
|
+
"@types/node": "^20.14.0",
|
|
42
|
+
"@types/pg": "^8.11.6",
|
|
43
|
+
tsx: "^4.16.0",
|
|
44
|
+
typescript: "^5.5.4",
|
|
45
|
+
vitest: "^2.0.5",
|
|
46
|
+
};
|
|
47
|
+
return (JSON.stringify({
|
|
48
|
+
name: `${scope}/api`,
|
|
49
|
+
version: "0.1.0",
|
|
50
|
+
private: true,
|
|
51
|
+
scripts: {
|
|
52
|
+
dev: "tsx watch src/server.ts",
|
|
53
|
+
build: "tsc -p tsconfig.json",
|
|
54
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
55
|
+
start: "node dist/server.js",
|
|
56
|
+
test: "vitest run --passWithNoTests",
|
|
57
|
+
},
|
|
58
|
+
dependencies: deps,
|
|
59
|
+
devDependencies: devDeps,
|
|
60
|
+
}, null, 2) + "\n");
|
|
61
|
+
}
|
|
62
|
+
function tsconfig() {
|
|
63
|
+
return (JSON.stringify({
|
|
64
|
+
extends: "../../tsconfig.base.json",
|
|
65
|
+
compilerOptions: { rootDir: "src", outDir: "dist" },
|
|
66
|
+
include: ["src"],
|
|
67
|
+
exclude: ["dist", "node_modules", "src/**/*.test.ts"],
|
|
68
|
+
}, null, 2) + "\n");
|
|
69
|
+
}
|
|
70
|
+
const CONFIG = `export interface Config {
|
|
71
|
+
port: number
|
|
72
|
+
databaseUrl: string
|
|
73
|
+
corsOrigins: string[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function loadConfig(): Config {
|
|
77
|
+
const databaseUrl = process.env.DATABASE_URL
|
|
78
|
+
if (!databaseUrl) throw new Error("DATABASE_URL is not set")
|
|
79
|
+
return {
|
|
80
|
+
port: Number(process.env.PORT) || 8080,
|
|
81
|
+
databaseUrl,
|
|
82
|
+
// Comma-separated allowlist; defaults to open in dev only.
|
|
83
|
+
corsOrigins: (process.env.CORS_ORIGINS ?? "*").split(",").map((s) => s.trim()),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
const DB = `import { Pool, type PoolClient } from "pg"
|
|
88
|
+
import { loadConfig } from "./config"
|
|
89
|
+
|
|
90
|
+
export const pool = new Pool({ connectionString: loadConfig().databaseUrl })
|
|
91
|
+
|
|
92
|
+
export type Tx = PoolClient
|
|
93
|
+
|
|
94
|
+
/** Run work inside a transaction, committing on success and rolling back on error. */
|
|
95
|
+
export async function withTx<T>(fn: (tx: Tx) => Promise<T>): Promise<T> {
|
|
96
|
+
const client = await pool.connect()
|
|
97
|
+
try {
|
|
98
|
+
await client.query("begin")
|
|
99
|
+
const result = await fn(client)
|
|
100
|
+
await client.query("commit")
|
|
101
|
+
return result
|
|
102
|
+
} catch (err) {
|
|
103
|
+
await client.query("rollback")
|
|
104
|
+
throw err
|
|
105
|
+
} finally {
|
|
106
|
+
client.release()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
const BUS = `import { readdirSync } from "node:fs"
|
|
111
|
+
import path from "node:path"
|
|
112
|
+
import type { Tx } from "./db"
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Domain event bus. Domain code emits facts inside its transaction; subscribers
|
|
116
|
+
* (dropped into src/bus.d/* by \`forge add events\`, etc.) receive them on the
|
|
117
|
+
* SAME transaction, so durability follows the write. With no subscribers, emit
|
|
118
|
+
* is a real no-op — not a stub.
|
|
119
|
+
*/
|
|
120
|
+
export type DomainEmitter = (tx: Tx, name: string, payload: unknown) => Promise<void>
|
|
121
|
+
|
|
122
|
+
const emitters: DomainEmitter[] = []
|
|
123
|
+
|
|
124
|
+
export function onEmit(fn: DomainEmitter): void {
|
|
125
|
+
emitters.push(fn)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function emit(tx: Tx, name: string, payload: unknown): Promise<void> {
|
|
129
|
+
for (const fn of emitters) await fn(tx, name, payload)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Auto-load subscribers. Each module in bus.d calls onEmit() at import time.
|
|
133
|
+
const dir = path.join(__dirname, "bus.d")
|
|
134
|
+
try {
|
|
135
|
+
for (const file of readdirSync(dir).sort()) {
|
|
136
|
+
if (!/\\.(ts|js)$/.test(file) || file.endsWith(".d.ts")) continue
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
138
|
+
require(path.join(dir, file))
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// no bus.d yet
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
const NOTES_DOMAIN = `import { randomUUID } from "node:crypto"
|
|
145
|
+
import { emit } from "../bus"
|
|
146
|
+
import { pool, withTx } from "../db"
|
|
147
|
+
|
|
148
|
+
export interface Note {
|
|
149
|
+
id: string
|
|
150
|
+
title: string
|
|
151
|
+
body: string
|
|
152
|
+
createdAt: string
|
|
153
|
+
updatedAt: string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface Row {
|
|
157
|
+
id: string
|
|
158
|
+
title: string
|
|
159
|
+
body: string
|
|
160
|
+
created_at: Date
|
|
161
|
+
updated_at: Date
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const toNote = (r: Row): Note => ({
|
|
165
|
+
id: r.id,
|
|
166
|
+
title: r.title,
|
|
167
|
+
body: r.body,
|
|
168
|
+
createdAt: r.created_at.toISOString(),
|
|
169
|
+
updatedAt: r.updated_at.toISOString(),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
export async function listNotes(): Promise<Note[]> {
|
|
173
|
+
const { rows } = await pool.query<Row>("select * from notes order by created_at desc")
|
|
174
|
+
return rows.map(toNote)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function getNote(id: string): Promise<Note | null> {
|
|
178
|
+
const { rows } = await pool.query<Row>("select * from notes where id = $1", [id])
|
|
179
|
+
return rows[0] ? toNote(rows[0]) : null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function createNote(input: { title: string; body?: string }): Promise<Note> {
|
|
183
|
+
return withTx(async (tx) => {
|
|
184
|
+
const { rows } = await tx.query<Row>(
|
|
185
|
+
"insert into notes (id, title, body) values ($1, $2, $3) returning *",
|
|
186
|
+
[randomUUID(), input.title, input.body ?? ""],
|
|
187
|
+
)
|
|
188
|
+
const note = toNote(rows[0])
|
|
189
|
+
await emit(tx, "note.created", note)
|
|
190
|
+
return note
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function updateNote(id: string, input: { title?: string; body?: string }): Promise<Note | null> {
|
|
195
|
+
return withTx(async (tx) => {
|
|
196
|
+
const { rows } = await tx.query<Row>(
|
|
197
|
+
"update notes set title = coalesce($2, title), body = coalesce($3, body), updated_at = now() where id = $1 returning *",
|
|
198
|
+
[id, input.title ?? null, input.body ?? null],
|
|
199
|
+
)
|
|
200
|
+
if (!rows[0]) return null
|
|
201
|
+
const note = toNote(rows[0])
|
|
202
|
+
await emit(tx, "note.updated", note)
|
|
203
|
+
return note
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function removeNote(id: string): Promise<boolean> {
|
|
208
|
+
return withTx(async (tx) => {
|
|
209
|
+
const { rows } = await tx.query<Row>("delete from notes where id = $1 returning *", [id])
|
|
210
|
+
if (!rows[0]) return false
|
|
211
|
+
await emit(tx, "note.deleted", toNote(rows[0]))
|
|
212
|
+
return true
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
const NOTES_SQL = `-- 0002_notes.sql — example domain table.
|
|
217
|
+
create table if not exists notes (
|
|
218
|
+
id uuid primary key,
|
|
219
|
+
title text not null,
|
|
220
|
+
body text not null default '',
|
|
221
|
+
created_at timestamptz not null default now(),
|
|
222
|
+
updated_at timestamptz not null default now()
|
|
223
|
+
);
|
|
224
|
+
`;
|
|
225
|
+
const SERVER_HONO = `import { serve } from "@hono/node-server"
|
|
226
|
+
import { Hono } from "hono"
|
|
227
|
+
import { readdirSync } from "node:fs"
|
|
228
|
+
import path from "node:path"
|
|
229
|
+
import { loadConfig } from "./config"
|
|
230
|
+
|
|
231
|
+
const app = new Hono()
|
|
232
|
+
|
|
233
|
+
// Auto-mount every route module in ./routes. Each exports register(app).
|
|
234
|
+
const routesDir = path.join(__dirname, "routes")
|
|
235
|
+
for (const file of readdirSync(routesDir).sort()) {
|
|
236
|
+
if (!/\\.(ts|js)$/.test(file) || file.endsWith(".d.ts")) continue
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
238
|
+
const mod = require(path.join(routesDir, file))
|
|
239
|
+
if (typeof mod.register === "function") mod.register(app)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const cfg = loadConfig()
|
|
243
|
+
serve({ fetch: app.fetch, port: cfg.port })
|
|
244
|
+
console.log("api listening on :" + cfg.port)
|
|
245
|
+
`;
|
|
246
|
+
const SERVER_EXPRESS = `import cors from "cors"
|
|
247
|
+
import express from "express"
|
|
248
|
+
import { readdirSync } from "node:fs"
|
|
249
|
+
import path from "node:path"
|
|
250
|
+
import { loadConfig } from "./config"
|
|
251
|
+
|
|
252
|
+
const cfg = loadConfig()
|
|
253
|
+
const app = express()
|
|
254
|
+
app.use(cors({ origin: cfg.corsOrigins.includes("*") ? true : cfg.corsOrigins }))
|
|
255
|
+
app.use(express.json())
|
|
256
|
+
|
|
257
|
+
// Auto-mount every route module in ./routes. Each exports register(app).
|
|
258
|
+
const routesDir = path.join(__dirname, "routes")
|
|
259
|
+
for (const file of readdirSync(routesDir).sort()) {
|
|
260
|
+
if (!/\\.(ts|js)$/.test(file) || file.endsWith(".d.ts")) continue
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
262
|
+
const mod = require(path.join(routesDir, file))
|
|
263
|
+
if (typeof mod.register === "function") mod.register(app)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
app.listen(cfg.port, () => console.log("api listening on :" + cfg.port))
|
|
267
|
+
`;
|
|
268
|
+
const HEALTH_HONO = `import type { Hono } from "hono"
|
|
269
|
+
import { pool } from "../db"
|
|
270
|
+
|
|
271
|
+
export function register(app: Hono): void {
|
|
272
|
+
app.get("/health", async (c) => {
|
|
273
|
+
try {
|
|
274
|
+
await pool.query("select 1")
|
|
275
|
+
return c.json({ status: "ok" })
|
|
276
|
+
} catch {
|
|
277
|
+
return c.json({ status: "degraded" }, 503)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
`;
|
|
282
|
+
const HEALTH_EXPRESS = `import type { Express, Request, Response } from "express"
|
|
283
|
+
import { pool } from "../db"
|
|
284
|
+
|
|
285
|
+
export function register(app: Express): void {
|
|
286
|
+
app.get("/health", async (_req: Request, res: Response) => {
|
|
287
|
+
try {
|
|
288
|
+
await pool.query("select 1")
|
|
289
|
+
res.json({ status: "ok" })
|
|
290
|
+
} catch {
|
|
291
|
+
res.status(503).json({ status: "degraded" })
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
296
|
+
const NOTES_HONO = `import type { Hono } from "hono"
|
|
297
|
+
import * as notes from "../domain/notes"
|
|
298
|
+
|
|
299
|
+
export function register(app: Hono): void {
|
|
300
|
+
app.get("/notes", async (c) => c.json(await notes.listNotes()))
|
|
301
|
+
|
|
302
|
+
app.get("/notes/:id", async (c) => {
|
|
303
|
+
const note = await notes.getNote(c.req.param("id"))
|
|
304
|
+
return note ? c.json(note) : c.json({ error: "not found" }, 404)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
app.post("/notes", async (c) => {
|
|
308
|
+
const body = await c.req.json<{ title?: string; body?: string }>()
|
|
309
|
+
if (!body.title) return c.json({ error: "title is required" }, 400)
|
|
310
|
+
return c.json(await notes.createNote({ title: body.title, body: body.body }), 201)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
app.put("/notes/:id", async (c) => {
|
|
314
|
+
const body = await c.req.json<{ title?: string; body?: string }>()
|
|
315
|
+
const note = await notes.updateNote(c.req.param("id"), body)
|
|
316
|
+
return note ? c.json(note) : c.json({ error: "not found" }, 404)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
app.delete("/notes/:id", async (c) => {
|
|
320
|
+
const ok = await notes.removeNote(c.req.param("id"))
|
|
321
|
+
return ok ? c.body(null, 204) : c.json({ error: "not found" }, 404)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
`;
|
|
325
|
+
const NOTES_EXPRESS = `import type { Express, Request, Response } from "express"
|
|
326
|
+
import * as notes from "../domain/notes"
|
|
327
|
+
|
|
328
|
+
export function register(app: Express): void {
|
|
329
|
+
app.get("/notes", async (_req: Request, res: Response) => {
|
|
330
|
+
res.json(await notes.listNotes())
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
app.get("/notes/:id", async (req: Request, res: Response) => {
|
|
334
|
+
const note = await notes.getNote(req.params.id)
|
|
335
|
+
if (!note) return res.status(404).json({ error: "not found" })
|
|
336
|
+
res.json(note)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
app.post("/notes", async (req: Request, res: Response) => {
|
|
340
|
+
if (!req.body.title) return res.status(400).json({ error: "title is required" })
|
|
341
|
+
res.status(201).json(await notes.createNote({ title: req.body.title, body: req.body.body }))
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
app.put("/notes/:id", async (req: Request, res: Response) => {
|
|
345
|
+
const note = await notes.updateNote(req.params.id, req.body)
|
|
346
|
+
if (!note) return res.status(404).json({ error: "not found" })
|
|
347
|
+
res.json(note)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
app.delete("/notes/:id", async (req: Request, res: Response) => {
|
|
351
|
+
const ok = await notes.removeNote(req.params.id)
|
|
352
|
+
if (!ok) return res.status(404).json({ error: "not found" })
|
|
353
|
+
res.status(204).end()
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
`;
|
|
357
|
+
const DOCKERFILE = `# Built by lwd from apps/api (git.path = "apps/api").
|
|
358
|
+
FROM node:20-alpine AS builder
|
|
359
|
+
WORKDIR /app
|
|
360
|
+
RUN npm install -g pnpm@9
|
|
361
|
+
COPY package.json ./
|
|
362
|
+
RUN pnpm install --no-frozen-lockfile
|
|
363
|
+
COPY . .
|
|
364
|
+
RUN pnpm build
|
|
365
|
+
|
|
366
|
+
FROM node:20-alpine
|
|
367
|
+
WORKDIR /app
|
|
368
|
+
RUN npm install -g pnpm@9
|
|
369
|
+
COPY package.json ./
|
|
370
|
+
RUN pnpm install --prod --no-frozen-lockfile
|
|
371
|
+
COPY --from=builder /app/dist ./dist
|
|
372
|
+
EXPOSE 8080
|
|
373
|
+
CMD ["node", "dist/server.js"]
|
|
374
|
+
`;
|
|
375
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/generators/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ForgeManifest } from "../types";
|
|
2
|
+
import type { Plan } from "../project/plan";
|
|
3
|
+
/** Compute the ordered, de-duplicated list of lwd secret names for an app. */
|
|
4
|
+
export declare function computeSecrets(manifest: ForgeManifest, includeAuth: boolean): string[];
|
|
5
|
+
/**
|
|
6
|
+
* Generate lwd deployment manifests for the project's current shape. Emits
|
|
7
|
+
* secret NAMES only (values set via `lwd secret set`). Encodes the two hard lwd
|
|
8
|
+
* constraints: a scalable API can't carry a `[[services]]` db, and a separate
|
|
9
|
+
* worker app can't reach another app's isolated backing network.
|
|
10
|
+
*/
|
|
11
|
+
export declare function addLwdManifests(plan: Plan, manifest: ForgeManifest): void;
|
|
12
|
+
//# sourceMappingURL=lwd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lwd.d.ts","sourceRoot":"","sources":["../../src/generators/lwd.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,aAAa,EAAY,MAAM,UAAU,CAAA;AACvE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAU3C,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,EAAE,CAQtF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CA2BzE"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeSecrets = computeSecrets;
|
|
4
|
+
exports.addLwdManifests = addLwdManifests;
|
|
5
|
+
/** Base secret names every project needs, plus per-capability additions. */
|
|
6
|
+
const CAPABILITY_SECRETS = {
|
|
7
|
+
"api-keys": ["API_KEYS_PEPPER"],
|
|
8
|
+
webhooks: ["WEBHOOK_SECRET_ENCRYPTION_KEY"],
|
|
9
|
+
files: ["S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"],
|
|
10
|
+
notifications: ["SMTP_URL"],
|
|
11
|
+
};
|
|
12
|
+
/** Compute the ordered, de-duplicated list of lwd secret names for an app. */
|
|
13
|
+
function computeSecrets(manifest, includeAuth) {
|
|
14
|
+
const secrets = new Set(["DATABASE_URL"]);
|
|
15
|
+
if (includeAuth)
|
|
16
|
+
secrets.add("JWT_SECRET");
|
|
17
|
+
for (const [cap, enabled] of Object.entries(manifest.platform)) {
|
|
18
|
+
if (!enabled)
|
|
19
|
+
continue;
|
|
20
|
+
for (const s of CAPABILITY_SECRETS[cap] ?? [])
|
|
21
|
+
secrets.add(s);
|
|
22
|
+
}
|
|
23
|
+
return [...secrets];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generate lwd deployment manifests for the project's current shape. Emits
|
|
27
|
+
* secret NAMES only (values set via `lwd secret set`). Encodes the two hard lwd
|
|
28
|
+
* constraints: a scalable API can't carry a `[[services]]` db, and a separate
|
|
29
|
+
* worker app can't reach another app's isolated backing network.
|
|
30
|
+
*/
|
|
31
|
+
function addLwdManifests(plan, manifest) {
|
|
32
|
+
const name = manifest.name;
|
|
33
|
+
const topology = manifest.deploy.topology;
|
|
34
|
+
const hasApi = manifest.apps.api !== undefined || Object.values(manifest.apps).some((a) => a.role === "api");
|
|
35
|
+
const hasAdmin = Object.values(manifest.apps).some((a) => a.role === "web");
|
|
36
|
+
const hasWorker = Object.values(manifest.apps).some((a) => a.role === "worker");
|
|
37
|
+
const includeAuth = true;
|
|
38
|
+
const secrets = computeSecrets(manifest, includeAuth);
|
|
39
|
+
const gitUrl = repoUrl(manifest.deploy.repo, name);
|
|
40
|
+
if (hasApi) {
|
|
41
|
+
plan.create(`deploy/api.lwd.toml`, apiManifest(name, topology, secrets, gitUrl), "lwd manifest: API surface");
|
|
42
|
+
}
|
|
43
|
+
if (hasAdmin) {
|
|
44
|
+
plan.create(`deploy/admin.lwd.toml`, adminManifest(name, gitUrl), "lwd manifest: admin frontend");
|
|
45
|
+
}
|
|
46
|
+
if (hasWorker) {
|
|
47
|
+
plan.create(`deploy/worker.lwd.toml`, workerManifest(name, secrets.filter((s) => s === "DATABASE_URL"), gitUrl), "lwd manifest: worker surface");
|
|
48
|
+
}
|
|
49
|
+
if (topology === "split") {
|
|
50
|
+
plan.create(`deploy/db.lwd.toml`, dbManifest(name), "lwd manifest: dedicated Postgres");
|
|
51
|
+
}
|
|
52
|
+
if (!manifest.deploy.repo) {
|
|
53
|
+
plan.nextStep(`No git repo known — set the [git].url in deploy/*.lwd.toml before deploying.`);
|
|
54
|
+
}
|
|
55
|
+
plan.nextStep(`Set secret values: lwd secret set ${name}-api ${secrets.join(" ")}`);
|
|
56
|
+
}
|
|
57
|
+
/** Resolve a repo slug/url (or none) into a git URL for the manifests. */
|
|
58
|
+
function repoUrl(repo, name) {
|
|
59
|
+
if (!repo)
|
|
60
|
+
return `https://github.com/${name}/${name}`;
|
|
61
|
+
if (repo.startsWith("http") || repo.startsWith("git@"))
|
|
62
|
+
return repo.replace(/\.git$/, "");
|
|
63
|
+
return `https://github.com/${repo}`;
|
|
64
|
+
}
|
|
65
|
+
function tomlList(items) {
|
|
66
|
+
return "[" + items.map((s) => `"${s}"`).join(", ") + "]";
|
|
67
|
+
}
|
|
68
|
+
function apiManifest(name, topology, secrets, gitUrl) {
|
|
69
|
+
// In split topology the API scales (replicas) and the DB is a separate app.
|
|
70
|
+
// In small topology the API co-locates a Postgres backing service.
|
|
71
|
+
const scalable = topology === "split";
|
|
72
|
+
const lines = [
|
|
73
|
+
`name = "${name}-api"`,
|
|
74
|
+
`domain = "api.${name}.example.com"`,
|
|
75
|
+
`port = 8080`,
|
|
76
|
+
`env = { NODE_ENV = "production", LOG_LEVEL = "info" }`,
|
|
77
|
+
`secrets = ${tomlList(secrets)}`,
|
|
78
|
+
];
|
|
79
|
+
if (scalable)
|
|
80
|
+
lines.push(`replicas = 2`);
|
|
81
|
+
lines.push(``, `[git]`, `url = "${gitUrl}"`, `ref = "main"`, `path = "apps/api"`, ``, `[build]`, `dockerfile = "Dockerfile"`, ``, `[health]`, `path = "/health"`, `timeout = "30s"`);
|
|
82
|
+
if (!scalable) {
|
|
83
|
+
lines.push(``, `# Small topology: Postgres co-located as a backing service.`, `# NOTE: a co-located backing service means this app cannot use replicas > 1.`, `[[services]]`, `name = "db"`, `image = "postgres:16"`, `env = { POSTGRES_USER = "${name}", POSTGRES_DB = "${name}" }`, `secrets = ["POSTGRES_PASSWORD"]`, `volume = "db-data:/var/lib/postgresql/data"`);
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n") + "\n";
|
|
86
|
+
}
|
|
87
|
+
function adminManifest(name, gitUrl) {
|
|
88
|
+
// Root-context build: the admin imports the workspace SDK, so the build needs
|
|
89
|
+
// the whole repo. git.path = "." and the Dockerfile lives under apps/admin.
|
|
90
|
+
return [
|
|
91
|
+
`name = "${name}-admin"`,
|
|
92
|
+
`domain = "app.${name}.example.com"`,
|
|
93
|
+
`port = 80`,
|
|
94
|
+
``,
|
|
95
|
+
`[git]`,
|
|
96
|
+
`url = "${gitUrl}"`,
|
|
97
|
+
`ref = "main"`,
|
|
98
|
+
`path = "."`,
|
|
99
|
+
``,
|
|
100
|
+
`[build]`,
|
|
101
|
+
`context = "."`,
|
|
102
|
+
`dockerfile = "apps/admin/Dockerfile"`,
|
|
103
|
+
``,
|
|
104
|
+
`[health]`,
|
|
105
|
+
`path = "/"`,
|
|
106
|
+
`timeout = "30s"`,
|
|
107
|
+
``,
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
function workerManifest(name, secrets, gitUrl) {
|
|
111
|
+
return [
|
|
112
|
+
`name = "${name}-worker"`,
|
|
113
|
+
`# lwd has no worker type: a worker is a normal surface app and still needs a`,
|
|
114
|
+
`# domain + health port. In split topology point DATABASE_URL at ${name}-db.`,
|
|
115
|
+
`domain = "worker.internal.${name}.example.com"`,
|
|
116
|
+
`port = 8080`,
|
|
117
|
+
`env = { NODE_ENV = "production", ROLE = "worker" }`,
|
|
118
|
+
`secrets = ${tomlList(secrets.length ? secrets : ["DATABASE_URL"])}`,
|
|
119
|
+
``,
|
|
120
|
+
`[git]`,
|
|
121
|
+
`url = "${gitUrl}"`,
|
|
122
|
+
`ref = "main"`,
|
|
123
|
+
`path = "apps/worker"`,
|
|
124
|
+
``,
|
|
125
|
+
`[build]`,
|
|
126
|
+
`dockerfile = "Dockerfile"`,
|
|
127
|
+
``,
|
|
128
|
+
`[health]`,
|
|
129
|
+
`path = "/health"`,
|
|
130
|
+
``,
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
function dbManifest(name) {
|
|
134
|
+
return [
|
|
135
|
+
`name = "${name}-db"`,
|
|
136
|
+
`image = "postgres:16"`,
|
|
137
|
+
`port = 5432`,
|
|
138
|
+
`env = { POSTGRES_DB = "${name}", POSTGRES_USER = "${name}" }`,
|
|
139
|
+
`secrets = ["POSTGRES_PASSWORD"]`,
|
|
140
|
+
``,
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=lwd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lwd.js","sourceRoot":"","sources":["../../src/generators/lwd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ExampleDomain } from "../types";
|
|
2
|
+
import type { Plan } from "../project/plan";
|
|
3
|
+
export interface MobileOptions {
|
|
4
|
+
scope: string;
|
|
5
|
+
/** App directory name, e.g. "mobile" or "driver". */
|
|
6
|
+
name: string;
|
|
7
|
+
example: ExampleDomain;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generates an Expo (React Native) app that drives the API through the shared
|
|
11
|
+
* SDK. Ships to app stores via EAS, so it gets no lwd manifest or Dockerfile.
|
|
12
|
+
* No build/typecheck scripts, so the workspace CI (pnpm -r build/typecheck)
|
|
13
|
+
* skips it — Expo builds happen through EAS, not CI.
|
|
14
|
+
*/
|
|
15
|
+
export declare function addMobileApp(plan: Plan, opts: MobileOptions): void;
|
|
16
|
+
//# sourceMappingURL=mobile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile.d.ts","sourceRoot":"","sources":["../../src/generators/mobile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,aAAa,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAoDlE"}
|