@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,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addProjectSkeleton = addProjectSkeleton;
|
|
4
|
+
exports.envBase = envBase;
|
|
5
|
+
exports.ciYaml = ciYaml;
|
|
6
|
+
/**
|
|
7
|
+
* Adds the project-level skeleton shared by every recipe: workspace config,
|
|
8
|
+
* base tsconfig, migration runner, CI, and docs. Everything here is a safe
|
|
9
|
+
* "create" — re-running never clobbers edited files.
|
|
10
|
+
*/
|
|
11
|
+
function addProjectSkeleton(plan, opts) {
|
|
12
|
+
const { name } = opts;
|
|
13
|
+
plan.create("package.json", JSON.stringify({
|
|
14
|
+
name,
|
|
15
|
+
private: true,
|
|
16
|
+
version: "0.1.0",
|
|
17
|
+
packageManager: "pnpm@9.7.0",
|
|
18
|
+
engines: { node: ">=20" },
|
|
19
|
+
scripts: {
|
|
20
|
+
dev: "pnpm -r --parallel --filter './apps/*' dev",
|
|
21
|
+
build: "pnpm -r build",
|
|
22
|
+
typecheck: "pnpm -r typecheck",
|
|
23
|
+
test: "pnpm -r test",
|
|
24
|
+
migrate: "tsx scripts/migrate.ts",
|
|
25
|
+
},
|
|
26
|
+
devDependencies: {
|
|
27
|
+
"@types/node": "^20.14.0",
|
|
28
|
+
"@types/pg": "^8.11.6",
|
|
29
|
+
pg: "^8.12.0",
|
|
30
|
+
tsx: "^4.16.0",
|
|
31
|
+
typescript: "^5.5.4",
|
|
32
|
+
},
|
|
33
|
+
}, null, 2) + "\n", "root workspace package.json");
|
|
34
|
+
plan.create("pnpm-workspace.yaml", ["packages:", " - apps/*", " - packages/*", ""].join("\n"), "pnpm workspace definition");
|
|
35
|
+
plan.create("tsconfig.base.json", JSON.stringify({
|
|
36
|
+
compilerOptions: {
|
|
37
|
+
target: "ES2022",
|
|
38
|
+
module: "commonjs",
|
|
39
|
+
moduleResolution: "node",
|
|
40
|
+
lib: ["ES2022"],
|
|
41
|
+
declaration: true,
|
|
42
|
+
sourceMap: true,
|
|
43
|
+
strict: true,
|
|
44
|
+
esModuleInterop: true,
|
|
45
|
+
skipLibCheck: true,
|
|
46
|
+
forceConsistentCasingInFileNames: true,
|
|
47
|
+
resolveJsonModule: true,
|
|
48
|
+
types: ["node"],
|
|
49
|
+
},
|
|
50
|
+
}, null, 2) + "\n", "shared TypeScript base config (CommonJS, matches @obh/* packages)");
|
|
51
|
+
plan.create(".gitignore", ["node_modules/", "dist/", "coverage/", "*.log", ".env", ".env.local", ".DS_Store", ""].join("\n"), "gitignore");
|
|
52
|
+
plan.create(".env.example", envBase(name), "environment example");
|
|
53
|
+
plan.create("migrations/0001_init.sql", INIT_SQL, "initial product migration");
|
|
54
|
+
plan.create("scripts/migrate.ts", MIGRATE_RUNNER, "migration runner (SQL files + platform primitives)");
|
|
55
|
+
plan.create("scripts/migrations.d/.gitkeep", "", "platform migration wiring directory");
|
|
56
|
+
plan.create(".github/workflows/ci.yml", ciYaml(name), "GitHub Actions CI (build, typecheck, test, Postgres service)");
|
|
57
|
+
plan.create("README.md", readme(name), "project readme");
|
|
58
|
+
plan.create("AGENTS.md", agents(name), "conventions doc for humans and Claude skills");
|
|
59
|
+
}
|
|
60
|
+
const INIT_SQL = `-- 0001_init.sql — product schema baseline.
|
|
61
|
+
-- Product tables live in the default (public) schema.
|
|
62
|
+
-- OBH platform primitives own their own tables under the "platform" schema
|
|
63
|
+
-- and are migrated by their packages via scripts/migrations.d/*.
|
|
64
|
+
|
|
65
|
+
create table if not exists health_check (
|
|
66
|
+
id integer primary key default 1,
|
|
67
|
+
checked_at timestamptz not null default now()
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
insert into health_check (id) values (1) on conflict (id) do nothing;
|
|
71
|
+
`;
|
|
72
|
+
const MIGRATE_RUNNER = `#!/usr/bin/env tsx
|
|
73
|
+
/**
|
|
74
|
+
* Migration runner. Applies:
|
|
75
|
+
* 1. Product SQL migrations in migrations/*.sql (tracked in _forge_migrations).
|
|
76
|
+
* 2. Platform primitive migrations registered in scripts/migrations.d/*.
|
|
77
|
+
*
|
|
78
|
+
* Each \`forge add <primitive>\` drops a file into scripts/migrations.d/ that
|
|
79
|
+
* exports \`migrate(pool)\`. This runner discovers them — nothing here is patched
|
|
80
|
+
* by hand.
|
|
81
|
+
*/
|
|
82
|
+
import { readdirSync, readFileSync } from "node:fs"
|
|
83
|
+
import path from "node:path"
|
|
84
|
+
import { Pool } from "pg"
|
|
85
|
+
|
|
86
|
+
async function main(): Promise<void> {
|
|
87
|
+
const databaseUrl = process.env.DATABASE_URL
|
|
88
|
+
if (!databaseUrl) throw new Error("DATABASE_URL is not set")
|
|
89
|
+
const pool = new Pool({ connectionString: databaseUrl })
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await pool.query(
|
|
93
|
+
\`create table if not exists _forge_migrations (
|
|
94
|
+
name text primary key,
|
|
95
|
+
applied_at timestamptz not null default now()
|
|
96
|
+
)\`,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// 1. Product SQL migrations, in filename order.
|
|
100
|
+
const migrationsDir = path.join(__dirname, "..", "migrations")
|
|
101
|
+
const sqlFiles = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort()
|
|
102
|
+
for (const file of sqlFiles) {
|
|
103
|
+
const done = await pool.query("select 1 from _forge_migrations where name = $1", [file])
|
|
104
|
+
if (done.rowCount) continue
|
|
105
|
+
const sql = readFileSync(path.join(migrationsDir, file), "utf8")
|
|
106
|
+
await pool.query(sql)
|
|
107
|
+
await pool.query("insert into _forge_migrations (name) values ($1)", [file])
|
|
108
|
+
console.log("applied", file)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Platform primitive migrations (each capability drops a module here).
|
|
112
|
+
const dir = path.join(__dirname, "migrations.d")
|
|
113
|
+
const modules = readdirSync(dir).filter((f) => /\\.(ts|js)$/.test(f) && !f.startsWith("."))
|
|
114
|
+
for (const file of modules.sort()) {
|
|
115
|
+
const mod = await import(path.join(dir, file))
|
|
116
|
+
if (typeof mod.migrate === "function") {
|
|
117
|
+
await mod.migrate(pool)
|
|
118
|
+
console.log("migrated platform:", file)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
await pool.end()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch((err) => {
|
|
127
|
+
console.error(err)
|
|
128
|
+
process.exit(1)
|
|
129
|
+
})
|
|
130
|
+
`;
|
|
131
|
+
function envBase(name) {
|
|
132
|
+
return `# ${name} environment
|
|
133
|
+
# Copy to .env and fill in. Never commit real secrets.
|
|
134
|
+
|
|
135
|
+
DATABASE_URL=postgres://postgres:postgres@localhost:5432/${name}
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
function ciYaml(name) {
|
|
139
|
+
return `name: CI
|
|
140
|
+
|
|
141
|
+
on:
|
|
142
|
+
push:
|
|
143
|
+
branches: [main]
|
|
144
|
+
pull_request:
|
|
145
|
+
|
|
146
|
+
jobs:
|
|
147
|
+
build-test:
|
|
148
|
+
runs-on: ubuntu-latest
|
|
149
|
+
|
|
150
|
+
services:
|
|
151
|
+
postgres:
|
|
152
|
+
image: postgres:16
|
|
153
|
+
env:
|
|
154
|
+
POSTGRES_USER: postgres
|
|
155
|
+
POSTGRES_PASSWORD: postgres
|
|
156
|
+
POSTGRES_DB: ${name}_test
|
|
157
|
+
ports:
|
|
158
|
+
- 5432:5432
|
|
159
|
+
options: >-
|
|
160
|
+
--health-cmd "pg_isready -U postgres"
|
|
161
|
+
--health-interval 10s
|
|
162
|
+
--health-timeout 5s
|
|
163
|
+
--health-retries 5
|
|
164
|
+
|
|
165
|
+
env:
|
|
166
|
+
DATABASE_URL: postgres://postgres:postgres@localhost:5432/${name}_test
|
|
167
|
+
|
|
168
|
+
steps:
|
|
169
|
+
- uses: actions/checkout@v4
|
|
170
|
+
- uses: pnpm/action-setup@v4
|
|
171
|
+
with:
|
|
172
|
+
version: 9
|
|
173
|
+
- uses: actions/setup-node@v4
|
|
174
|
+
with:
|
|
175
|
+
node-version: 20
|
|
176
|
+
cache: pnpm
|
|
177
|
+
- run: pnpm install --no-frozen-lockfile
|
|
178
|
+
- run: pnpm build
|
|
179
|
+
- run: pnpm typecheck
|
|
180
|
+
- run: pnpm test
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function readme(name) {
|
|
184
|
+
return `# ${name}
|
|
185
|
+
|
|
186
|
+
Scaffolded with [OBH Forge](https://github.com/EObrien60/forge).
|
|
187
|
+
|
|
188
|
+
## Layout
|
|
189
|
+
|
|
190
|
+
\`\`\`
|
|
191
|
+
apps/ deployable applications (api, admin, worker, …)
|
|
192
|
+
packages/ shared internal packages (sdk, ui, config)
|
|
193
|
+
deploy/ lwd deployment manifests
|
|
194
|
+
docker/ Dockerfiles
|
|
195
|
+
migrations/ product SQL migrations
|
|
196
|
+
scripts/ migrate, seed, dev helpers
|
|
197
|
+
forge.json record of what Forge generated (see below)
|
|
198
|
+
\`\`\`
|
|
199
|
+
|
|
200
|
+
## Develop
|
|
201
|
+
|
|
202
|
+
\`\`\`bash
|
|
203
|
+
pnpm install
|
|
204
|
+
pnpm migrate # apply product + platform migrations (needs DATABASE_URL)
|
|
205
|
+
pnpm dev # run all apps
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
## Platform primitives
|
|
209
|
+
|
|
210
|
+
Add OBH platform capabilities incrementally — each is opt-in:
|
|
211
|
+
|
|
212
|
+
\`\`\`bash
|
|
213
|
+
forge add events
|
|
214
|
+
forge add jobs
|
|
215
|
+
forge add files
|
|
216
|
+
forge add audit
|
|
217
|
+
\`\`\`
|
|
218
|
+
|
|
219
|
+
\`forge inspect\` reports the project shape; \`forge doctor\` checks it against
|
|
220
|
+
OBH conventions. \`forge.json\` records what was generated.
|
|
221
|
+
|
|
222
|
+
## Deploy
|
|
223
|
+
|
|
224
|
+
Deployment manifests live in \`deploy/*.lwd.toml\` and target lwd. Set secret
|
|
225
|
+
values out-of-band with \`lwd secret set <app> <KEY>\` — manifests contain secret
|
|
226
|
+
names only.
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
function agents(name) {
|
|
230
|
+
return `# ${name} — conventions
|
|
231
|
+
|
|
232
|
+
This project was scaffolded by OBH Forge. Conventions Claude skills and humans
|
|
233
|
+
can rely on:
|
|
234
|
+
|
|
235
|
+
- **Package manager:** pnpm workspace (\`apps/*\`, \`packages/*\`).
|
|
236
|
+
- **Language:** TypeScript, \`strict\`, CommonJS (matches \`@obh/*\` packages).
|
|
237
|
+
- **Database:** Postgres. Product tables in \`public\`; platform primitives own
|
|
238
|
+
the \`platform\` schema. No ORM auto-sync — migrations run via \`pnpm migrate\`.
|
|
239
|
+
- **API:** routes in \`apps/api/src/routes/*.ts\`, each exporting \`register(app)\`;
|
|
240
|
+
the server auto-mounts them. Add routes by dropping files, not editing a router.
|
|
241
|
+
- **Worker:** consumers in \`apps/worker/src/consumers.d/*.ts\`, auto-loaded.
|
|
242
|
+
- **Migrations:** platform primitives register in \`scripts/migrations.d/*.ts\`.
|
|
243
|
+
- **Deploy:** lwd manifests in \`deploy/*.lwd.toml\`; secret names only.
|
|
244
|
+
- **Record:** \`forge.json\` tracks generated shape and installed primitives.
|
|
245
|
+
|
|
246
|
+
Prefer boring, explicit code. Generated files are yours to edit or delete.
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=root.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root.js","sourceRoot":"","sources":["../../src/generators/root.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExampleDomain } from "../types";
|
|
2
|
+
import type { Plan } from "../project/plan";
|
|
3
|
+
export interface SdkOptions {
|
|
4
|
+
scope: string;
|
|
5
|
+
example: ExampleDomain;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Generates the shared SDK package: the typed contract between backend and
|
|
9
|
+
* frontends. This is the piece whose absence produced qMechanic's 3,200-line
|
|
10
|
+
* defensive apiClient, so Forge makes it the one place request/response shapes
|
|
11
|
+
* are defined.
|
|
12
|
+
*/
|
|
13
|
+
export declare function addSdkPackage(plan: Plan, opts: SdkOptions): void;
|
|
14
|
+
//# sourceMappingURL=sdk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/generators/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,aAAa,CAAA;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAwDhE"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addSdkPackage = addSdkPackage;
|
|
4
|
+
/**
|
|
5
|
+
* Generates the shared SDK package: the typed contract between backend and
|
|
6
|
+
* frontends. This is the piece whose absence produced qMechanic's 3,200-line
|
|
7
|
+
* defensive apiClient, so Forge makes it the one place request/response shapes
|
|
8
|
+
* are defined.
|
|
9
|
+
*/
|
|
10
|
+
function addSdkPackage(plan, opts) {
|
|
11
|
+
const dir = "packages/sdk";
|
|
12
|
+
const notes = opts.example === "notes";
|
|
13
|
+
plan.create(`${dir}/package.json`, JSON.stringify({
|
|
14
|
+
name: `${opts.scope}/sdk`,
|
|
15
|
+
version: "0.1.0",
|
|
16
|
+
private: true,
|
|
17
|
+
// ESM so Vite/Rollup can tree-shake the named exports (a CommonJS SDK
|
|
18
|
+
// breaks `vite build` in a consuming frontend).
|
|
19
|
+
type: "module",
|
|
20
|
+
main: "dist/index.js",
|
|
21
|
+
module: "dist/index.js",
|
|
22
|
+
types: "dist/index.d.ts",
|
|
23
|
+
files: ["dist"],
|
|
24
|
+
scripts: {
|
|
25
|
+
build: "tsc -p tsconfig.json",
|
|
26
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
27
|
+
test: "vitest run --passWithNoTests",
|
|
28
|
+
},
|
|
29
|
+
devDependencies: { typescript: "^5.5.4", vitest: "^2.0.5" },
|
|
30
|
+
}, null, 2) + "\n", "SDK package.json");
|
|
31
|
+
plan.create(`${dir}/tsconfig.json`, JSON.stringify({
|
|
32
|
+
extends: "../../tsconfig.base.json",
|
|
33
|
+
compilerOptions: {
|
|
34
|
+
rootDir: "src",
|
|
35
|
+
outDir: "dist",
|
|
36
|
+
// Emit ESM (overrides the CommonJS base) so frontends bundle it cleanly.
|
|
37
|
+
module: "ESNext",
|
|
38
|
+
moduleResolution: "bundler",
|
|
39
|
+
lib: ["ES2022", "DOM"],
|
|
40
|
+
},
|
|
41
|
+
include: ["src"],
|
|
42
|
+
exclude: ["dist", "node_modules", "src/**/*.test.ts"],
|
|
43
|
+
}, null, 2) + "\n", "SDK tsconfig");
|
|
44
|
+
plan.create(`${dir}/src/index.ts`, INDEX, "SDK entrypoint");
|
|
45
|
+
plan.create(`${dir}/src/types.ts`, notes ? TYPES_NOTES : TYPES_BASE, "shared domain types");
|
|
46
|
+
plan.create(`${dir}/src/client.ts`, notes ? CLIENT_NOTES : CLIENT_BASE, "typed API client");
|
|
47
|
+
}
|
|
48
|
+
const INDEX = `// Public surface of the SDK — the one contract shared by API and frontends.
|
|
49
|
+
export * from "./types"
|
|
50
|
+
export { createClient } from "./client"
|
|
51
|
+
export type { Client, ClientOptions } from "./client"
|
|
52
|
+
`;
|
|
53
|
+
const TYPES_BASE = `export interface HealthStatus {
|
|
54
|
+
status: "ok" | "degraded"
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
const TYPES_NOTES = `export interface HealthStatus {
|
|
58
|
+
status: "ok" | "degraded"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Note {
|
|
62
|
+
id: string
|
|
63
|
+
title: string
|
|
64
|
+
body: string
|
|
65
|
+
createdAt: string
|
|
66
|
+
updatedAt: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CreateNoteInput {
|
|
70
|
+
title: string
|
|
71
|
+
body?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface UpdateNoteInput {
|
|
75
|
+
title?: string
|
|
76
|
+
body?: string
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
const CLIENT_HEAD = `import type { HealthStatus } from "./types"
|
|
80
|
+
|
|
81
|
+
export interface ClientOptions {
|
|
82
|
+
baseUrl: string
|
|
83
|
+
/** Optional bearer token provider. */
|
|
84
|
+
token?: () => string | null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function request<T>(opts: ClientOptions, path: string, init?: RequestInit): Promise<T> {
|
|
88
|
+
const headers = new Headers(init?.headers)
|
|
89
|
+
if (init?.body) headers.set("content-type", "application/json")
|
|
90
|
+
const token = opts.token?.()
|
|
91
|
+
if (token) headers.set("authorization", "Bearer " + token)
|
|
92
|
+
const res = await fetch(opts.baseUrl + path, { ...init, headers })
|
|
93
|
+
if (!res.ok) throw new Error(path + " failed: " + res.status)
|
|
94
|
+
if (res.status === 204) return undefined as T
|
|
95
|
+
return (await res.json()) as T
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
const CLIENT_BASE = `${CLIENT_HEAD}
|
|
99
|
+
export interface Client {
|
|
100
|
+
health(): Promise<HealthStatus>
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A thin typed fetch client. Add one method per API resource. */
|
|
104
|
+
export function createClient(opts: ClientOptions): Client {
|
|
105
|
+
return {
|
|
106
|
+
health: () => request<HealthStatus>(opts, "/health"),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
const CLIENT_NOTES = `import type { CreateNoteInput, Note, UpdateNoteInput } from "./types"
|
|
111
|
+
${CLIENT_HEAD}
|
|
112
|
+
export interface Client {
|
|
113
|
+
health(): Promise<HealthStatus>
|
|
114
|
+
notes: {
|
|
115
|
+
list(): Promise<Note[]>
|
|
116
|
+
get(id: string): Promise<Note>
|
|
117
|
+
create(input: CreateNoteInput): Promise<Note>
|
|
118
|
+
update(id: string, input: UpdateNoteInput): Promise<Note>
|
|
119
|
+
remove(id: string): Promise<void>
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A thin typed fetch client — the single contract shared with every frontend. */
|
|
124
|
+
export function createClient(opts: ClientOptions): Client {
|
|
125
|
+
return {
|
|
126
|
+
health: () => request<HealthStatus>(opts, "/health"),
|
|
127
|
+
notes: {
|
|
128
|
+
list: () => request<Note[]>(opts, "/notes"),
|
|
129
|
+
get: (id) => request<Note>(opts, "/notes/" + id),
|
|
130
|
+
create: (input) => request<Note>(opts, "/notes", { method: "POST", body: JSON.stringify(input) }),
|
|
131
|
+
update: (id, input) => request<Note>(opts, "/notes/" + id, { method: "PUT", body: JSON.stringify(input) }),
|
|
132
|
+
remove: (id) => request<void>(opts, "/notes/" + id, { method: "DELETE" }),
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
//# sourceMappingURL=sdk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk.js","sourceRoot":"","sources":["../../src/generators/sdk.ts"],"names":[],"mappings":";;AAcA,sCAwDC;AA9DD;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,IAAU,EAAE,IAAgB;IACxD,MAAM,GAAG,GAAG,cAAc,CAAA;IAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,KAAK,OAAO,CAAA;IAEtC,IAAI,CAAC,MAAM,CACT,GAAG,GAAG,eAAe,EACrB,IAAI,CAAC,SAAS,CACZ;QACE,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,MAAM;QACzB,OAAO,EAAE,OAAO;QAChB,OAAO,EAAE,IAAI;QACb,sEAAsE;QACtE,gDAAgD;QAChD,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,eAAe;QACvB,KAAK,EAAE,iBAAiB;QACxB,KAAK,EAAE,CAAC,MAAM,CAAC;QACf,OAAO,EAAE;YACP,KAAK,EAAE,sBAAsB;YAC7B,SAAS,EAAE,+BAA+B;YAC1C,IAAI,EAAE,8BAA8B;SACrC;QACD,eAAe,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE;KAC5D,EACD,IAAI,EACJ,CAAC,CACF,GAAG,IAAI,EACR,kBAAkB,CACnB,CAAA;IAED,IAAI,CAAC,MAAM,CACT,GAAG,GAAG,gBAAgB,EACtB,IAAI,CAAC,SAAS,CACZ;QACE,OAAO,EAAE,0BAA0B;QACnC,eAAe,EAAE;YACf,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,MAAM;YACd,yEAAyE;YACzE,MAAM,EAAE,QAAQ;YAChB,gBAAgB,EAAE,SAAS;YAC3B,GAAG,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC;SACvB;QACD,OAAO,EAAE,CAAC,KAAK,CAAC;QAChB,OAAO,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,kBAAkB,CAAC;KACtD,EACD,IAAI,EACJ,CAAC,CACF,GAAG,IAAI,EACR,cAAc,CACf,CAAA;IAED,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,eAAe,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAA;IAC3D,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,eAAe,EAAE,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,EAAE,qBAAqB,CAAC,CAAA;IAC3F,IAAI,CAAC,MAAM,CAAC,GAAG,GAAG,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAA;AAC7F,CAAC;AAED,MAAM,KAAK,GAAG;;;;CAIb,CAAA;AAED,MAAM,UAAU,GAAG;;;CAGlB,CAAA;AAED,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;CAqBnB,CAAA;AAED,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;CAkBnB,CAAA;AAED,MAAM,WAAW,GAAG,GAAG,WAAW;;;;;;;;;;;CAWjC,CAAA;AAED,MAAM,YAAY,GAAG;EACnB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;CAyBZ,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExampleDomain } from "../types";
|
|
2
|
+
import type { Plan } from "../project/plan";
|
|
3
|
+
export interface WebOptions {
|
|
4
|
+
scope: string;
|
|
5
|
+
/** App directory name, e.g. "admin". */
|
|
6
|
+
name: string;
|
|
7
|
+
example: ExampleDomain;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generates a Vite + React admin frontend that consumes the shared SDK. Because
|
|
11
|
+
* it imports the workspace SDK, its Docker build runs from the repo root
|
|
12
|
+
* (lwd git.path = ".") and filters to this app — see the generated Dockerfile.
|
|
13
|
+
*/
|
|
14
|
+
export declare function addWebApp(plan: Plan, opts: WebOptions): void;
|
|
15
|
+
//# sourceMappingURL=web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/generators/web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,aAAa,CAAA;CACvB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAwE5D"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addWebApp = addWebApp;
|
|
4
|
+
/**
|
|
5
|
+
* Generates a Vite + React admin frontend that consumes the shared SDK. Because
|
|
6
|
+
* it imports the workspace SDK, its Docker build runs from the repo root
|
|
7
|
+
* (lwd git.path = ".") and filters to this app — see the generated Dockerfile.
|
|
8
|
+
*/
|
|
9
|
+
function addWebApp(plan, opts) {
|
|
10
|
+
const dir = `apps/${opts.name}`;
|
|
11
|
+
const pkgName = `${opts.scope}/${opts.name}`;
|
|
12
|
+
plan.create(`${dir}/package.json`, JSON.stringify({
|
|
13
|
+
name: pkgName,
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
private: true,
|
|
16
|
+
type: "module",
|
|
17
|
+
scripts: {
|
|
18
|
+
dev: "vite",
|
|
19
|
+
build: "vite build",
|
|
20
|
+
preview: "vite preview",
|
|
21
|
+
typecheck: "tsc --noEmit",
|
|
22
|
+
test: "vitest run --passWithNoTests",
|
|
23
|
+
},
|
|
24
|
+
dependencies: {
|
|
25
|
+
[`${opts.scope}/sdk`]: "workspace:*",
|
|
26
|
+
react: "^18.3.1",
|
|
27
|
+
"react-dom": "^18.3.1",
|
|
28
|
+
"react-router-dom": "^7.9.3",
|
|
29
|
+
},
|
|
30
|
+
devDependencies: {
|
|
31
|
+
"@types/react": "^18.3.3",
|
|
32
|
+
"@types/react-dom": "^18.3.0",
|
|
33
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
34
|
+
typescript: "^5.5.4",
|
|
35
|
+
vite: "^5.4.0",
|
|
36
|
+
vitest: "^2.0.5",
|
|
37
|
+
},
|
|
38
|
+
}, null, 2) + "\n", "admin app package.json");
|
|
39
|
+
plan.create(`${dir}/tsconfig.json`, JSON.stringify({
|
|
40
|
+
compilerOptions: {
|
|
41
|
+
target: "ES2022",
|
|
42
|
+
lib: ["ES2022", "DOM", "DOM.Iterable"],
|
|
43
|
+
module: "ESNext",
|
|
44
|
+
moduleResolution: "bundler",
|
|
45
|
+
jsx: "react-jsx",
|
|
46
|
+
strict: true,
|
|
47
|
+
skipLibCheck: true,
|
|
48
|
+
esModuleInterop: true,
|
|
49
|
+
resolveJsonModule: true,
|
|
50
|
+
noEmit: true,
|
|
51
|
+
},
|
|
52
|
+
include: ["src"],
|
|
53
|
+
}, null, 2) + "\n", "admin tsconfig (ESM/bundler for Vite)");
|
|
54
|
+
plan.create(`${dir}/vite.config.ts`, VITE_CONFIG, "vite config");
|
|
55
|
+
plan.create(`${dir}/index.html`, indexHtml(opts.name), "admin index.html");
|
|
56
|
+
plan.create(`${dir}/src/main.tsx`, MAIN, "admin entry");
|
|
57
|
+
plan.create(`${dir}/src/vite-env.d.ts`, '/// <reference types="vite/client" />\n', "vite client types (import.meta.env)");
|
|
58
|
+
plan.create(`${dir}/src/api.ts`, apiTs(opts.scope, opts.example), "admin API client instance");
|
|
59
|
+
plan.create(`${dir}/src/App.tsx`, opts.example === "notes" ? APP_NOTES : APP_BASE, "admin App");
|
|
60
|
+
plan.create(`${dir}/nginx.conf`, NGINX, "nginx SPA config");
|
|
61
|
+
plan.create(`${dir}/Dockerfile`, dockerfile(pkgName, opts.name), "admin Dockerfile (root-context build)");
|
|
62
|
+
}
|
|
63
|
+
const VITE_CONFIG = `import { defineConfig } from "vite"
|
|
64
|
+
import react from "@vitejs/plugin-react"
|
|
65
|
+
|
|
66
|
+
export default defineConfig({
|
|
67
|
+
plugins: [react()],
|
|
68
|
+
})
|
|
69
|
+
`;
|
|
70
|
+
function indexHtml(name) {
|
|
71
|
+
return `<!doctype html>
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<head>
|
|
74
|
+
<meta charset="UTF-8" />
|
|
75
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
76
|
+
<title>${name}</title>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<div id="root"></div>
|
|
80
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
const MAIN = `import React from "react"
|
|
86
|
+
import { createRoot } from "react-dom/client"
|
|
87
|
+
import { App } from "./App"
|
|
88
|
+
|
|
89
|
+
createRoot(document.getElementById("root")!).render(
|
|
90
|
+
<React.StrictMode>
|
|
91
|
+
<App />
|
|
92
|
+
</React.StrictMode>,
|
|
93
|
+
)
|
|
94
|
+
`;
|
|
95
|
+
function apiTs(scope, example) {
|
|
96
|
+
const typeReexport = example === "notes"
|
|
97
|
+
? `export type { HealthStatus, Note, CreateNoteInput, UpdateNoteInput } from "${scope}/sdk"`
|
|
98
|
+
: `export type { HealthStatus } from "${scope}/sdk"`;
|
|
99
|
+
return `import { createClient } from "${scope}/sdk"
|
|
100
|
+
${typeReexport}
|
|
101
|
+
|
|
102
|
+
export const api = createClient({
|
|
103
|
+
baseUrl: import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080",
|
|
104
|
+
token: () => localStorage.getItem("token"),
|
|
105
|
+
})
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
const APP_BASE = `import { useEffect, useState } from "react"
|
|
109
|
+
import { api, type HealthStatus } from "./api"
|
|
110
|
+
|
|
111
|
+
export function App(): JSX.Element {
|
|
112
|
+
const [health, setHealth] = useState<HealthStatus | null>(null)
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
api.health().then(setHealth).catch(() => setHealth({ status: "degraded" }))
|
|
115
|
+
}, [])
|
|
116
|
+
return (
|
|
117
|
+
<main style={{ fontFamily: "system-ui", padding: 32, maxWidth: 720, margin: "0 auto" }}>
|
|
118
|
+
<h1>Admin</h1>
|
|
119
|
+
<p>API health: {health?.status ?? "…"}</p>
|
|
120
|
+
</main>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
const APP_NOTES = `import { useEffect, useState } from "react"
|
|
125
|
+
import { api, type Note } from "./api"
|
|
126
|
+
|
|
127
|
+
export function App(): JSX.Element {
|
|
128
|
+
const [notes, setNotes] = useState<Note[]>([])
|
|
129
|
+
const [title, setTitle] = useState("")
|
|
130
|
+
const [body, setBody] = useState("")
|
|
131
|
+
const [error, setError] = useState<string | null>(null)
|
|
132
|
+
|
|
133
|
+
const reload = () => api.notes.list().then(setNotes).catch((e) => setError(String(e)))
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
reload()
|
|
136
|
+
}, [])
|
|
137
|
+
|
|
138
|
+
const submit = async (e: React.FormEvent) => {
|
|
139
|
+
e.preventDefault()
|
|
140
|
+
if (!title.trim()) return
|
|
141
|
+
await api.notes.create({ title, body })
|
|
142
|
+
setTitle("")
|
|
143
|
+
setBody("")
|
|
144
|
+
await reload()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const remove = async (id: string) => {
|
|
148
|
+
await api.notes.remove(id)
|
|
149
|
+
await reload()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<main style={{ fontFamily: "system-ui", padding: 32, maxWidth: 720, margin: "0 auto" }}>
|
|
154
|
+
<h1>Notes</h1>
|
|
155
|
+
{error && <p style={{ color: "crimson" }}>{error}</p>}
|
|
156
|
+
|
|
157
|
+
<form onSubmit={submit} style={{ display: "grid", gap: 8, marginBottom: 24 }}>
|
|
158
|
+
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
|
|
159
|
+
<textarea value={body} onChange={(e) => setBody(e.target.value)} placeholder="Body" rows={3} />
|
|
160
|
+
<button type="submit">Add note</button>
|
|
161
|
+
</form>
|
|
162
|
+
|
|
163
|
+
<ul style={{ display: "grid", gap: 12, listStyle: "none", padding: 0 }}>
|
|
164
|
+
{notes.map((n) => (
|
|
165
|
+
<li key={n.id} style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12 }}>
|
|
166
|
+
<strong>{n.title}</strong>
|
|
167
|
+
<p style={{ margin: "4px 0", whiteSpace: "pre-wrap" }}>{n.body}</p>
|
|
168
|
+
<button onClick={() => remove(n.id)}>Delete</button>
|
|
169
|
+
</li>
|
|
170
|
+
))}
|
|
171
|
+
</ul>
|
|
172
|
+
</main>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
const NGINX = `server {
|
|
177
|
+
listen 80;
|
|
178
|
+
root /usr/share/nginx/html;
|
|
179
|
+
index index.html;
|
|
180
|
+
location / {
|
|
181
|
+
try_files $uri $uri/ /index.html;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
function dockerfile(pkgName, name) {
|
|
186
|
+
return `# Deployed by lwd with git.path = "." and build.dockerfile = "apps/${name}/Dockerfile"
|
|
187
|
+
# (root context so the workspace SDK is available to the build).
|
|
188
|
+
FROM node:20-alpine AS builder
|
|
189
|
+
WORKDIR /repo
|
|
190
|
+
RUN npm install -g pnpm@9
|
|
191
|
+
COPY . .
|
|
192
|
+
RUN pnpm install --no-frozen-lockfile
|
|
193
|
+
ARG VITE_API_BASE_URL
|
|
194
|
+
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
|
195
|
+
RUN pnpm --filter "${pkgName}" build
|
|
196
|
+
|
|
197
|
+
FROM nginx:1.27-alpine
|
|
198
|
+
COPY --from=builder /repo/apps/${name}/dist /usr/share/nginx/html
|
|
199
|
+
COPY apps/${name}/nginx.conf /etc/nginx/conf.d/default.conf
|
|
200
|
+
EXPOSE 80
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/generators/web.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Plan } from "../project/plan";
|
|
2
|
+
export interface WorkerOptions {
|
|
3
|
+
scope: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Generates a worker app. Consumers live in src/consumers.d/*.ts, each exporting
|
|
7
|
+
* an optional `init(ctx)` and a `tick(ctx)`. The worker loads them by convention
|
|
8
|
+
* and drives them on an interval — capabilities add background work by dropping
|
|
9
|
+
* a file, never by editing the loop. Includes a plain-HTTP health port because
|
|
10
|
+
* lwd surfaces (including workers) require one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function addWorkerApp(plan: Plan, opts: WorkerOptions): void;
|
|
13
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/generators/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CAmDlE"}
|