@codepassion/skills 1.4.0 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codepassion/skills",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Agent skills (SKILL.md) for CodePassion (C9N) engineering conventions.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -271,12 +271,20 @@ already owns, not a file drop (see Phase 2).
271
271
 
272
272
  Run in order; this is a table of contents — rules are below.
273
273
 
274
- 1. **Detect (Phase 1).** Scan `apps/*/package.json`. Classify each: `next`→web, `elysia`→api, else
275
- →service. Detect DB by **real artifacts** — a migration-generate script in some `package.json`
276
- AND a migrations dir on disk — capture their actual names; never assume `drizzle:generate` /
277
- `packages/supabase/migrations`. Read root `package.json` for the Bun version / lockfile name.
278
- Ask which environments exist (Alpha / Beta / Production). **Print the detected plan and confirm
279
- before writing anything.**
274
+ 1. **Detect (Phase 1).** First decide the repo shape:
275
+ - **Monorepo** — `apps/` and/or `turbo.json` exist. Scan `apps/*/package.json`, classify each:
276
+ `next`→web, `elysia`→api, else→service. `{{APP_DIR}}` = `apps/<name>`.
277
+ - **Single-app** no `apps/`, no `turbo.json`; the deployable app is the repo root. Classify the
278
+ root `package.json` (`next`→web, etc.). `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = the repo name
279
+ (no `-web` suffix needed), and the **single-app template variants** apply (see "Single-app repos"
280
+ below) — drop the workspace-manifest copies, `outputFileTracingRoot`, `transpilePackages`, and
281
+ the `apps/web/` path segments. The generated `ci.yml` also drops Turbo (see that template's note).
282
+
283
+ Then detect DB by **real artifacts** — a migration-generate script in some `package.json` AND a
284
+ migrations dir on disk — capture their actual names; never assume `drizzle:generate` /
285
+ `packages/supabase/migrations`. Read the app's `package.json` for the Bun version / lockfile name
286
+ (but **pin ≥ 1.3.10** — see `{{BUN_VERSION}}`). Ask which environments exist (Alpha / Beta /
287
+ Production). **Print the detected plan and confirm before writing anything.**
280
288
  2. **Generate (Phase 2).** Copy templates with tokens substituted. **Diff/confirm per file** — if a
281
289
  target exists, show the diff and ask overwrite/skip/merge; never blind-clobber; leave unrelated
282
290
  files (a hand-kept `staging.yaml`) alone. Then do the **web edits** (below). Run the web build
@@ -291,11 +299,11 @@ Run in order; this is a table of contents — rules are below.
291
299
  | Token | Source |
292
300
  |-------|--------|
293
301
  | `{{PACKAGE_NAME}}` | `<project>-<appkey>`, e.g. `smiley-web`. Lowercase. This is the ghcr image name and the `package_name` workflow input. |
294
- | `{{APP_DIR}}` | The app's path, e.g. `apps/web`. |
302
+ | `{{APP_DIR}}` | The app's path, e.g. `apps/web`. **Single-app repo: `.`** (the app is the repo root — this leaves valid `./.next/...` paths in the template). |
295
303
  | `{{APP_KEY}}` | Short lowercase id unique per app: `api`, `web`, `worker`. Used in job names. |
296
304
  | `{{APP_PORT}}` | `3001` api · `3000` web · service's listen port (drop `EXPOSE` for a portless worker). |
297
- | `{{BUN_VERSION}}` | Pin from root toolchain, e.g. `1.3.10`. Same across all Dockerfiles. |
298
- | `{{WORKSPACE_MANIFEST_COPIES}}` | **web template only:** one `COPY <app>/package.json ./<app>/package.json` line **per app** — the `deps` stage runs `bun install` before `COPY . .`, so it needs every workspace manifest to resolve the graph. The api/service compile templates copy only the built app's manifest (matches Smiley's api Dockerfile; avoids pulling a sibling app's postinstall without its src). |
305
+ | `{{BUN_VERSION}}` | **Pin 1.3.10** (same across all Dockerfiles). Do **not** copy the host's `bun --version` — 1.2.x breaks `next build` on Linux with `TypeError: Expected CommonJS module to have a function wrapper` (edge/jest-worker; only shows on Linux, not macOS). |
306
+ | `{{WORKSPACE_MANIFEST_COPIES}}` | **web template only:** one `COPY <app>/package.json ./<app>/package.json` line **per app** — the `deps` stage runs `bun install` before `COPY . .`, so it needs every workspace manifest to resolve the graph. The api/service compile templates copy only the built app's manifest (matches Smiley's api Dockerfile; avoids pulling a sibling app's postinstall without its src). **Single-app repo: empty string** (root `package.json` + `bun.lock` are already copied; there is no `packages/` — also delete the `COPY packages/ ./packages/` line). |
299
307
  | `{{MIGRATION_CHECK}}` | `_migration-check.partial.yml` with `{{MIGRATION_SCRIPT}}`/`{{MIGRATIONS_DIR}}` filled, **or empty string** if no DB. |
300
308
  | `{{ORG_REF}}` | The org reusable-workflow ref — currently `turborepo`. One place; bump here if the org repo retags. |
301
309
  | `{{BUILD_JOBS}}` / `{{DEPLOY_JOBS}}` | Stamp `_build-job.partial` once per app (Alpha only) and `_deploy-job.partial` once per app per environment, pruning unused envs. |
@@ -306,17 +314,46 @@ Run in order; this is a table of contents — rules are below.
306
314
  For each **web** app, edit the files the dev owns; fail loudly if they can't be parsed rather than
307
315
  overwrite:
308
316
 
309
- - `next.config.js` — add `output: "standalone"`, `outputFileTracingRoot` (monorepo root), and
310
- `transpilePackages` for the workspace packages the app imports.
317
+ - `next.config.js` — add `output: "standalone"`. **Monorepo only:** also add `outputFileTracingRoot`
318
+ (monorepo root) and `transpilePackages` for the workspace packages the app imports. **Single-app:
319
+ omit both** (root app traces natively; no workspace packages).
311
320
  - `app/layout.tsx` — add `<head><PublicEnvScript /></head>` (`import { PublicEnvScript } from
312
- "next-runtime-env"`). Client code reads public config with `env("NEXT_PUBLIC_X")`, **not**
313
- `process.env` (which bakes at build undefined at runtime).
321
+ "next-runtime-env"`).
322
+ - **Convert EVERY `process.env.NEXT_PUBLIC_*` read to `env("NEXT_PUBLIC_X")`** not just client
323
+ components. Next **inlines every `process.env.NEXT_PUBLIC_*` literal at build time in SERVER code and
324
+ middleware too**, so any read left as `process.env` bakes the build value (or a build-time
325
+ placeholder) into the server/middleware bundle and runtime injection can't override it. This is the
326
+ #1 time-sink: a stray `process.env.NEXT_PUBLIC_SUPABASE_URL` in `middleware.ts` bakes the placeholder
327
+ → `supabase.auth.getUser()` fails → **auth redirect loop** that only shows in the container, not
328
+ locally. Convert: client components, **server components, route handlers (`app/api/**`), middleware,
329
+ and any t3-env `runtimeEnv` mapping**. Non-public server vars (`DATABASE_URL`, `*_SERVICE_KEY`) stay
330
+ on `process.env` — they aren't inlined and are present at runtime.
331
+ - **Phase-2 gate — must pass before you call the web edits done:**
332
+ `grep -rn 'process.env.NEXT_PUBLIC' {{APP_DIR}}` (the app dir — `.` for single-app, `apps/web` for a
333
+ monorepo app; not every repo has a `src/`) returns **nothing**. Any hit is a latent runtime bug.
314
334
  - Flag `next-runtime-env` as a dependency to add — **do not run `bun add` yourself**; tell the dev.
315
335
  - **Caveat:** runtime values only reach the browser on non-statically-prerendered pages. A page that
316
336
  reads runtime public env must opt out of static caching (`export const dynamic = "force-dynamic"`)
317
337
  or rely on `<PublicEnvScript>` keeping the document dynamic. Note this so future feature work
318
338
  doesn't silently bake a static page.
319
339
 
340
+ ## Single-app repos (no `apps/`, no `turbo.json`)
341
+
342
+ The templates are monorepo-shaped; one set of token substitutions makes them serve a root-level single
343
+ app. Apply these deltas (verified on `ddg-jewelry-admin`):
344
+
345
+ - **Tokens:** `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = repo name (no `-web` suffix),
346
+ `{{WORKSPACE_MANIFEST_COPIES}}` = empty.
347
+ - **`Dockerfile.web`:** delete the `COPY packages/ ./packages/` line (no `packages/`). The runner
348
+ `COPY` lines collapse to `.next/standalone ./`, `.next/static ./.next/static`, `public ./public`
349
+ (the `{{APP_DIR}}=.` substitution yields valid `./.next/...` — fine to simplify).
350
+ - **`next.config`:** `output: "standalone"` only — no `outputFileTracingRoot`, no `transpilePackages`.
351
+ - **`ci.yml`:** drop the Turbo `env:`, the `.turbo` cache step, and swap `bun run check-types` for
352
+ `bunx tsc --noEmit` if the repo has no `check-types` script (single apps usually don't).
353
+ - **Runner (optional but recommended):** a single Next app deploys cleanly on a Node runner
354
+ (`node:22-alpine` or `gcr.io/distroless/nodejs22-debian12`, `CMD ["server.js"]`). Distroless cuts the
355
+ image ~4× and drops the runner CVEs (seen on `ddg-jewelry-admin`).
356
+
320
357
  ## Phase 3 — interactive ops checklist (the deliverable)
321
358
 
322
359
  Derived from the actual `codepassion-team/cicd` reusable workflows. Walk these one at a time:
@@ -342,8 +379,14 @@ visibility), or the image pull silently fails.
342
379
  **Web runtime env in Coolify** (the scars):
343
380
  - `PORT=3000` — Next standalone obeys `PORT`; Coolify defaults it to `80`, which 502s behind a
344
381
  Caddy `:3000` proxy. This is the single most common web-deploy failure.
345
- - `HOSTNAME=0.0.0.0`.
382
+ - **Coolify `Ports Exposes` (General page) is a SEPARATE setting from the `PORT` env** — it defaults
383
+ to `80` and must be set to the app port (`3000`). With `PORT=3000` but Ports Exposes still `80`, the
384
+ deploy log warns `PORT (3000) does not match configured ports_exposes: 80` and the proxy 502s. Fix
385
+ both.
386
+ - `HOSTNAME=0.0.0.0` — the Dockerfile already bakes this; only set it if something overrides it.
346
387
  - The app's `NEXT_PUBLIC_*` values — injected here at runtime, not baked into the image.
388
+ - **Set ALL env values UNQUOTED** (public and server alike) — Coolify passes values literally, so a
389
+ quoted `DATABASE_URL="…"` keeps the quotes and breaks `new URL()` (`ERR_INVALID_URL`).
347
390
 
348
391
  ## Phase 4 — verify
349
392
 
@@ -31,12 +31,20 @@ already owns, not a file drop (see Phase 2).
31
31
 
32
32
  Run in order; this is a table of contents — rules are below.
33
33
 
34
- 1. **Detect (Phase 1).** Scan `apps/*/package.json`. Classify each: `next`→web, `elysia`→api, else
35
- →service. Detect DB by **real artifacts** — a migration-generate script in some `package.json`
36
- AND a migrations dir on disk — capture their actual names; never assume `drizzle:generate` /
37
- `packages/supabase/migrations`. Read root `package.json` for the Bun version / lockfile name.
38
- Ask which environments exist (Alpha / Beta / Production). **Print the detected plan and confirm
39
- before writing anything.**
34
+ 1. **Detect (Phase 1).** First decide the repo shape:
35
+ - **Monorepo** — `apps/` and/or `turbo.json` exist. Scan `apps/*/package.json`, classify each:
36
+ `next`→web, `elysia`→api, else→service. `{{APP_DIR}}` = `apps/<name>`.
37
+ - **Single-app** no `apps/`, no `turbo.json`; the deployable app is the repo root. Classify the
38
+ root `package.json` (`next`→web, etc.). `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = the repo name
39
+ (no `-web` suffix needed), and the **single-app template variants** apply (see "Single-app repos"
40
+ below) — drop the workspace-manifest copies, `outputFileTracingRoot`, `transpilePackages`, and
41
+ the `apps/web/` path segments. The generated `ci.yml` also drops Turbo (see that template's note).
42
+
43
+ Then detect DB by **real artifacts** — a migration-generate script in some `package.json` AND a
44
+ migrations dir on disk — capture their actual names; never assume `drizzle:generate` /
45
+ `packages/supabase/migrations`. Read the app's `package.json` for the Bun version / lockfile name
46
+ (but **pin ≥ 1.3.10** — see `{{BUN_VERSION}}`). Ask which environments exist (Alpha / Beta /
47
+ Production). **Print the detected plan and confirm before writing anything.**
40
48
  2. **Generate (Phase 2).** Copy templates with tokens substituted. **Diff/confirm per file** — if a
41
49
  target exists, show the diff and ask overwrite/skip/merge; never blind-clobber; leave unrelated
42
50
  files (a hand-kept `staging.yaml`) alone. Then do the **web edits** (below). Run the web build
@@ -51,11 +59,11 @@ Run in order; this is a table of contents — rules are below.
51
59
  | Token | Source |
52
60
  |-------|--------|
53
61
  | `{{PACKAGE_NAME}}` | `<project>-<appkey>`, e.g. `smiley-web`. Lowercase. This is the ghcr image name and the `package_name` workflow input. |
54
- | `{{APP_DIR}}` | The app's path, e.g. `apps/web`. |
62
+ | `{{APP_DIR}}` | The app's path, e.g. `apps/web`. **Single-app repo: `.`** (the app is the repo root — this leaves valid `./.next/...` paths in the template). |
55
63
  | `{{APP_KEY}}` | Short lowercase id unique per app: `api`, `web`, `worker`. Used in job names. |
56
64
  | `{{APP_PORT}}` | `3001` api · `3000` web · service's listen port (drop `EXPOSE` for a portless worker). |
57
- | `{{BUN_VERSION}}` | Pin from root toolchain, e.g. `1.3.10`. Same across all Dockerfiles. |
58
- | `{{WORKSPACE_MANIFEST_COPIES}}` | **web template only:** one `COPY <app>/package.json ./<app>/package.json` line **per app** — the `deps` stage runs `bun install` before `COPY . .`, so it needs every workspace manifest to resolve the graph. The api/service compile templates copy only the built app's manifest (matches Smiley's api Dockerfile; avoids pulling a sibling app's postinstall without its src). |
65
+ | `{{BUN_VERSION}}` | **Pin 1.3.10** (same across all Dockerfiles). Do **not** copy the host's `bun --version` — 1.2.x breaks `next build` on Linux with `TypeError: Expected CommonJS module to have a function wrapper` (edge/jest-worker; only shows on Linux, not macOS). |
66
+ | `{{WORKSPACE_MANIFEST_COPIES}}` | **web template only:** one `COPY <app>/package.json ./<app>/package.json` line **per app** — the `deps` stage runs `bun install` before `COPY . .`, so it needs every workspace manifest to resolve the graph. The api/service compile templates copy only the built app's manifest (matches Smiley's api Dockerfile; avoids pulling a sibling app's postinstall without its src). **Single-app repo: empty string** (root `package.json` + `bun.lock` are already copied; there is no `packages/` — also delete the `COPY packages/ ./packages/` line). |
59
67
  | `{{MIGRATION_CHECK}}` | `_migration-check.partial.yml` with `{{MIGRATION_SCRIPT}}`/`{{MIGRATIONS_DIR}}` filled, **or empty string** if no DB. |
60
68
  | `{{ORG_REF}}` | The org reusable-workflow ref — currently `turborepo`. One place; bump here if the org repo retags. |
61
69
  | `{{BUILD_JOBS}}` / `{{DEPLOY_JOBS}}` | Stamp `_build-job.partial` once per app (Alpha only) and `_deploy-job.partial` once per app per environment, pruning unused envs. |
@@ -66,17 +74,46 @@ Run in order; this is a table of contents — rules are below.
66
74
  For each **web** app, edit the files the dev owns; fail loudly if they can't be parsed rather than
67
75
  overwrite:
68
76
 
69
- - `next.config.js` — add `output: "standalone"`, `outputFileTracingRoot` (monorepo root), and
70
- `transpilePackages` for the workspace packages the app imports.
77
+ - `next.config.js` — add `output: "standalone"`. **Monorepo only:** also add `outputFileTracingRoot`
78
+ (monorepo root) and `transpilePackages` for the workspace packages the app imports. **Single-app:
79
+ omit both** (root app traces natively; no workspace packages).
71
80
  - `app/layout.tsx` — add `<head><PublicEnvScript /></head>` (`import { PublicEnvScript } from
72
- "next-runtime-env"`). Client code reads public config with `env("NEXT_PUBLIC_X")`, **not**
73
- `process.env` (which bakes at build undefined at runtime).
81
+ "next-runtime-env"`).
82
+ - **Convert EVERY `process.env.NEXT_PUBLIC_*` read to `env("NEXT_PUBLIC_X")`** not just client
83
+ components. Next **inlines every `process.env.NEXT_PUBLIC_*` literal at build time in SERVER code and
84
+ middleware too**, so any read left as `process.env` bakes the build value (or a build-time
85
+ placeholder) into the server/middleware bundle and runtime injection can't override it. This is the
86
+ #1 time-sink: a stray `process.env.NEXT_PUBLIC_SUPABASE_URL` in `middleware.ts` bakes the placeholder
87
+ → `supabase.auth.getUser()` fails → **auth redirect loop** that only shows in the container, not
88
+ locally. Convert: client components, **server components, route handlers (`app/api/**`), middleware,
89
+ and any t3-env `runtimeEnv` mapping**. Non-public server vars (`DATABASE_URL`, `*_SERVICE_KEY`) stay
90
+ on `process.env` — they aren't inlined and are present at runtime.
91
+ - **Phase-2 gate — must pass before you call the web edits done:**
92
+ `grep -rn 'process.env.NEXT_PUBLIC' {{APP_DIR}}` (the app dir — `.` for single-app, `apps/web` for a
93
+ monorepo app; not every repo has a `src/`) returns **nothing**. Any hit is a latent runtime bug.
74
94
  - Flag `next-runtime-env` as a dependency to add — **do not run `bun add` yourself**; tell the dev.
75
95
  - **Caveat:** runtime values only reach the browser on non-statically-prerendered pages. A page that
76
96
  reads runtime public env must opt out of static caching (`export const dynamic = "force-dynamic"`)
77
97
  or rely on `<PublicEnvScript>` keeping the document dynamic. Note this so future feature work
78
98
  doesn't silently bake a static page.
79
99
 
100
+ ## Single-app repos (no `apps/`, no `turbo.json`)
101
+
102
+ The templates are monorepo-shaped; one set of token substitutions makes them serve a root-level single
103
+ app. Apply these deltas (verified on `ddg-jewelry-admin`):
104
+
105
+ - **Tokens:** `{{APP_DIR}}` = `.`, `{{PACKAGE_NAME}}` = repo name (no `-web` suffix),
106
+ `{{WORKSPACE_MANIFEST_COPIES}}` = empty.
107
+ - **`Dockerfile.web`:** delete the `COPY packages/ ./packages/` line (no `packages/`). The runner
108
+ `COPY` lines collapse to `.next/standalone ./`, `.next/static ./.next/static`, `public ./public`
109
+ (the `{{APP_DIR}}=.` substitution yields valid `./.next/...` — fine to simplify).
110
+ - **`next.config`:** `output: "standalone"` only — no `outputFileTracingRoot`, no `transpilePackages`.
111
+ - **`ci.yml`:** drop the Turbo `env:`, the `.turbo` cache step, and swap `bun run check-types` for
112
+ `bunx tsc --noEmit` if the repo has no `check-types` script (single apps usually don't).
113
+ - **Runner (optional but recommended):** a single Next app deploys cleanly on a Node runner
114
+ (`node:22-alpine` or `gcr.io/distroless/nodejs22-debian12`, `CMD ["server.js"]`). Distroless cuts the
115
+ image ~4× and drops the runner CVEs (seen on `ddg-jewelry-admin`).
116
+
80
117
  ## Phase 3 — interactive ops checklist (the deliverable)
81
118
 
82
119
  Derived from the actual `codepassion-team/cicd` reusable workflows. Walk these one at a time:
@@ -102,8 +139,14 @@ visibility), or the image pull silently fails.
102
139
  **Web runtime env in Coolify** (the scars):
103
140
  - `PORT=3000` — Next standalone obeys `PORT`; Coolify defaults it to `80`, which 502s behind a
104
141
  Caddy `:3000` proxy. This is the single most common web-deploy failure.
105
- - `HOSTNAME=0.0.0.0`.
142
+ - **Coolify `Ports Exposes` (General page) is a SEPARATE setting from the `PORT` env** — it defaults
143
+ to `80` and must be set to the app port (`3000`). With `PORT=3000` but Ports Exposes still `80`, the
144
+ deploy log warns `PORT (3000) does not match configured ports_exposes: 80` and the proxy 502s. Fix
145
+ both.
146
+ - `HOSTNAME=0.0.0.0` — the Dockerfile already bakes this; only set it if something overrides it.
106
147
  - The app's `NEXT_PUBLIC_*` values — injected here at runtime, not baked into the image.
148
+ - **Set ALL env values UNQUOTED** (public and server alike) — Coolify passes values literally, so a
149
+ quoted `DATABASE_URL="…"` keeps the quotes and breaks `new URL()` (`ERR_INVALID_URL`).
107
150
 
108
151
  ## Phase 4 — verify
109
152
 
@@ -3,6 +3,11 @@
3
3
  # Public env is read at RUNTIME via next-runtime-env (<PublicEnvScript>), so this
4
4
  # image bakes no NEXT_PUBLIC_* — Coolify injects them at run, like the API image.
5
5
  # Generated by c9n-pipeline; see the ADR this skill points you to.
6
+ #
7
+ # {{BUN_VERSION}} must be >= 1.3.10 — 1.2.x breaks `next build` on Linux.
8
+ # Single-app repo: {{APP_DIR}}=. and {{WORKSPACE_MANIFEST_COPIES}}= empty; also
9
+ # delete the `COPY packages/ ./packages/` line below (no packages/ dir). See the
10
+ # "Single-app repos" section in SKILL.md.
6
11
  # -----------------------------------------------------------------------------
7
12
 
8
13
  FROM oven/bun:{{BUN_VERSION}} AS base
@@ -34,5 +34,17 @@ jobs:
34
34
  - name: Check types
35
35
  run: bun run check-types
36
36
  - name: Build
37
- run: bun run build
37
+ # Placeholder NEXT_PUBLIC_* values satisfy module-init that reads them at import
38
+ # time (e.g. Supabase clients) — CI has no .env files, so without these
39
+ # `next build` throws "Error: supabaseUrl is required" collecting page
40
+ # data. Real values are injected at runtime via next-runtime-env, never
41
+ # baked. Mirrors the Dockerfile build stage. Add one line per public var
42
+ # the app constructs a client from at import.
43
+ run: SKIP_ENV_VALIDATION=1 bun run build
44
+ env:
45
+ NEXT_PUBLIC_SUPABASE_URL: http://placeholder.invalid
46
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder
38
47
  {{MIGRATION_CHECK}}
48
+ # Single-app repos (no turbo.json): remove the TURBO_TOKEN/TURBO_TEAM `env:` and
49
+ # the `.turbo` cache step above, and swap `bun run check-types` for
50
+ # `bunx tsc --noEmit` if the repo has no check-types script.