@smbcloud / cli / commits / 3488b0d

docs(agents): mark repo public; scrub internal infra from deploy skills

- .agents/AGENTS.md: add an open-source/public-repository boundary — no internal hostnames, account/user IDs, SSH key names, real domains, port registry, secrets, or credential-reading commands; keep docs generic. - deploy skills (nextjs/monorepo/rsync): genericize examples to placeholders and add the generic technical lessons (rsync --copy-links status-23 dangling pnpm symlink, nested-standalone /_next/static, one-file + SAN-cert multi-domain). The fleet-specific internal reference now lives only in the private smbcloud repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Seto Elkahfi committed Jun 15, 2026 at 07:05 UTC 3488b0d7107627d5f0972bda1e69a4ca014e077c
4 files changed +174 -18
.agents/AGENTS.md
+18
@@ -1,3 +1,21 @@
1 +# Public, open-source repository
2 +
3 +`smbcloud-cli` is published on GitHub as **open source**. Everything you commit here — code, comments, docs, and `.agents/skills/*` — is **world-readable and permanent** (git history outlives any later deletion). Before writing anything, ask: "is this safe on a public repo forever?"
4 +
5 +Do **not** add internal smbCloud infrastructure detail or secrets:
6 +
7 +- server hostnames/IPs and operational endpoints beyond what the CLI already targets in source — e.g. account-scoped SSH key names (`id_<n>@smbcloud`), `api-1.smbcloud.xyz`, internal health ports
8 +- account/user IDs and which user or tenant owns which project
9 +- real customer/app domains, PM2 process names, the production port → app → domain registry
10 +- workspace/project IDs and `frontend_app_id` / `deploy_repo_id` values
11 +- secrets/config: API keys, tokens, `.env` values, connection strings, real auth/CORS origins
12 +- commands that read local credentials to enumerate the API (e.g. `cat ~/.smb/token | curl …`)
13 +- incident logs or examples that name real apps, tenants, customers, or dated rollouts
14 +
15 +Keep docs and skills **generic** — describe how the tool behaves, using placeholders (`example.com`, `<app>`, `<source>`, `<port>`, `<n>`). The base API host (`api.smbcloud.xyz`) is already in the source, so it is not a new leak; the items above are. Fleet-specific operational detail and the internal deploy reference live only in the private `smbcloud` repo.
16 +
17 +---
18 +
19 # Rust coding guidelines
20
21 - Prioritize code correctness and clarity. Speed and efficiency are secondary priorities unless otherwise specified.
.agents/skills/smbcloud-deploy-monorepo/SKILL.md
+47 -18
@@ -282,38 +282,43 @@ Current allocations on `api.smbcloud.xyz`:
282
283 ### Nginx configuration per app type
284
285 -**Next.js SSR apps** use reverse proxy:
285 +**Next.js SSR apps** use reverse proxy. In a monorepo the safe default is to
286 +proxy *everything* (including `/_next/static`) to the Node process — do not
287 +serve static from disk with an `alias`, because monorepo builds are **nested**
288 +(see warning below):
289
290 ```nginx
288 -upstream app_name {
289 - server 127.0.0.1:<port>;
290 - keepalive 64;
291 -}
292 -
291 server {
292 server_name example.com;
293
296 - location /_next/static/ {
297 - alias /home/git/apps/web/<app>/.next/static/;
298 - expires max;
299 - add_header Cache-Control "public, max-age=31536000, immutable";
300 - access_log off;
301 - }
302 -
294 location / {
304 - proxy_pass http://app_name;
295 + proxy_pass http://127.0.0.1:<port>;
296 proxy_http_version 1.1;
297 proxy_set_header Host $host;
298 proxy_set_header X-Real-IP $remote_addr;
299 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
300 proxy_set_header X-Forwarded-Proto $scheme;
301 proxy_set_header Upgrade $http_upgrade;
311 - proxy_set_header Connection $connection_upgrade;
302 + proxy_set_header Connection "upgrade";
303 proxy_buffering off;
304 + proxy_cache_bypass $http_upgrade;
305 }
306 }
307 ```
308
309 +> **Nested-static gotcha.** A monorepo sub-project builds with
310 +> `outputFileTracingRoot` pointing at the repo root, so standalone is nested and
311 +> static lands at `…/<app>/<source>/.next/static` (e.g.
312 +> `…/<app>/apps/web/.next/static`), **not** `…/<app>/.next/static`. The
313 +> classic disk alias —
314 +> `location /_next/static/ { alias /home/git/apps/web/<app>/.next/static/; }` —
315 +> therefore 404s every JS/CSS chunk while HTML still renders ("page loads, no
316 +> styles"). Proxy-everything avoids this entirely. If you insist on a disk alias
317 +> for cache headers, point it at the nested path:
318 +> `alias /home/git/apps/web/<app>/<source>/.next/static/;`.
319 +> Flat single-repo builds (`source = "."`) keep static at `…/<app>/.next/static`,
320 +> so an alias there is correct — but proxy-everything is the uniform choice.
321 +
322 **Vite SPA / static apps** use direct file serving:
323
324 ```nginx
@@ -512,6 +517,14 @@ To convert a standalone repo into a monorepo sub-project:
517 3. Set `source` to the sub-directory path
518 4. Adjust `path` if the remote directory structure differs
519
520 +> **Deploy from the sub-project, not the leftover standalone repo.** Once an app
521 +> has been folded into a monorepo (`apps/<name>`), that copy is the source of
522 +> truth — deploy it with `smb deploy -p <name>`. If the old standalone repo
523 +> still exists on disk it is easy to build and deploy from there by accident,
524 +> which silently diverges from what the monorepo would ship. Archive the old
525 +> repo. Note this also flips the build from flat to nested standalone, so the
526 +> nginx `/_next/static` handling may need the nested-path / proxy-everything fix.
527 +
528 ## Debugging monorepo deploys
529
530 ### "No [[projects]] entries found"
@@ -534,9 +547,25 @@ The `source` field is incorrect. It must be relative to the monorepo root where
547
548 Two `nextjs-ssr` apps have the same `port` value. PM2 will start both, but only one will bind the port. Check `pm2 list` and compare ports across all `.smb/config.toml` files that target the same server.
549
537 -### Stale assets after deploy
538 -
539 -The Nginx `alias` directive for `/_next/static/` or the `root` for a Vite SPA points to the wrong directory or a stale deploy path. Verify the Nginx config matches the `path` field in `.smb/config.toml`.
550 +### Stale assets after deploy / `/_next/static` 404s
551 +
552 +The Nginx `alias` for `/_next/static/` (or the Vite SPA `root`) points at the
553 +wrong directory. For a nested monorepo standalone build the assets live at
554 +`<path>/<source>/.next/static`, not `<path>/.next/static` — see the nested-static
555 +gotcha under "Nginx configuration per app type". Either proxy `/_next/static` to
556 +the Node process or fix the alias to the nested path. Confirm with
557 +`curl -sI https://<domain>/_next/static/...` (want 200).
558 +
559 +### `rsync of '.next/standalone/' failed (status 23)`
560 +
561 +`smb deploy` aborts at the standalone upload with empty stderr; running rsync by
562 +hand shows `IO error encountered -- skipping file deletion`. Cause: Next.js
563 +standalone leaves a dangling pnpm symlink (e.g.
564 +`node_modules/.pnpm/node_modules/semver -> ../semver@6.3.1/...` when only
565 +`semver@7.x` was traced) and the CLI's `rsync --copy-links` errors trying to
566 +follow it. Fix: after `next build`, before rsync, run
567 +`find .next/standalone/ -type l ! -exec test -e {} \; -delete`. Proper fix is in
568 +the CLI (prune dangling links before the standalone rsync). See the nextjs skill.
569
570 ### `Cannot find module 'next'` after Next.js SSR deploy
571
.agents/skills/smbcloud-deploy-nextjs/SKILL.md
+103
@@ -264,6 +264,62 @@ location / {
264
265 Nginx must proxy to the actual PM2 port.
266
267 +### Do not `alias` `/_next/static` for nested standalone builds
268 +
269 +The proxy-everything block above is the safe default: let the standalone Node
270 +server serve `/_next/static` itself. The common breakage is an nginx block that
271 +tries to serve those assets from disk:
272 +
273 +```nginx
274 +# WRONG for a nested (monorepo) standalone build
275 +location /_next/static {
276 + alias /home/git/apps/web/<app>/.next/static; # 404s every JS/CSS
277 +}
278 +```
279 +
280 +When the app builds with `outputFileTracingRoot` (monorepo), standalone is
281 +**nested**, so static actually lands at `…/<app>/<source>/.next/static`
282 +(e.g. `…/<app>/apps/web/.next/static`), not `…/<app>/.next/static`. The alias
283 +points at the wrong directory and every chunk 404s while the HTML still loads —
284 +a confusing "page renders, no styles/JS" symptom.
285 +
286 +Fixes, in order of preference:
287 +
288 +1. Drop the `/_next/static` `alias` block and proxy everything to the Node
289 + process (the block above). Layout-independent — works for flat and nested.
290 +2. If you must serve static from disk, point the alias at the nested path:
291 + `alias …/<app>/<source>/.next/static;`.
292 +
293 +Flat (single-repo, `source = "."`) builds keep static at `…/<app>/.next/static`,
294 +so a disk alias there happens to be correct — but proxy-everything still works
295 +and is the recommended uniform pattern.
296 +
297 +### One file + SAN cert for multiple hostnames
298 +
299 +To serve several hostnames for the same app (e.g. a primary domain plus a
300 +`*.5mb.app` alias) from one `server` block, they must share one certificate — a
301 +`server` block can only present one cert. Issue a SAN cert listing both names,
302 +then write one block:
303 +
304 +```sh
305 +sudo certbot certonly --nginx -d app.example.com -d app.example.net --cert-name myapp
306 +```
307 +
308 +```nginx
309 +server {
310 + listen 443 ssl http2;
311 + server_name app.example.com app.example.net;
312 + ssl_certificate /etc/letsencrypt/live/myapp/fullchain.pem;
313 + ssl_certificate_key /etc/letsencrypt/live/myapp/privkey.pem;
314 + # ... single proxy_pass block; Host $host passes the real hostname through
315 +}
316 +```
317 +
318 +Mixing two different certs (a per-domain cert plus a shared wildcard) forces two
319 +`server` blocks. Run `certbot certonly` first, then write the config that
320 +references the new lineage — pointing `ssl_certificate` at a path that does not
321 +exist yet makes `nginx -t` fail.
322 +
323 ## Common failure modes
324
325 ### `.next/standalone` missing
@@ -348,6 +404,48 @@ Fix:
404 - deploy shims must use `import()` not `require()`
405 - the CLI now handles both automatically: it checks for `.cjs` first and the server-side migration auto-renames `.js` to `.cjs` during deploy
406
407 +### `rsync` of `.next/standalone/` fails with status 23 (dangling pnpm symlink)
408 +
409 +A reproducible failure on pnpm monorepos. The deploy aborts at the standalone
410 +upload with an empty stderr and:
411 +
412 +```
413 +✘ rsync of '.next/standalone/' failed (status 23):
414 +```
415 +
416 +Run the same rsync by hand and you see the real cause:
417 +
418 +```
419 +IO error encountered -- skipping file deletion
420 +```
421 +
422 +Cause:
423 +
424 +- Next.js standalone tracing emits a virtual pnpm symlink such as
425 + `.next/standalone/node_modules/.pnpm/node_modules/semver -> ../semver@6.3.1/node_modules/semver`,
426 + but only the version it actually traced (e.g. `semver@7.7.3`) is copied into
427 + the bundle. The `@6.3.1` target is never written, so the symlink dangles.
428 +- The CLI rsyncs standalone with `--copy-links`, which tries to follow every
429 + symlink. A dangling link is an IO error → rsync exits 23 and skips deletion.
430 +- The package is not part of the traced runtime, so the link is safe to drop.
431 +
432 +Find them:
433 +
434 +```sh
435 +find .next/standalone/ -type l ! -exec test -e {} \; -print
436 +```
437 +
438 +Fix:
439 +
440 +- Remove the dangling link(s) before upload:
441 + `find .next/standalone/ -type l ! -exec test -e {} \; -delete`
442 + then re-run the deploy. (The CLI rebuilds on every run, so this must happen
443 + after `next build` and before rsync — when driving the deploy manually,
444 + delete then run the three rsyncs yourself.)
445 +- Proper fix belongs in the CLI: prune dangling symlinks (or use
446 + `--copy-unsafe-links` semantics that tolerate them) before the standalone
447 + rsync. Until then, the manual `find … -delete` is the workaround.
448 +
449 ### Misleading deploy output
450
451 The CLI may print generic deploy messages such as:
@@ -415,6 +513,11 @@ Use the smallest checks that prove the contract.
513 - assuming `deployment_method = 1` alone activates SSR deploy
514 - forgetting `kind = "nextjs-ssr"`
515 - keeping an old git `post-receive` hook and expecting it to manage SSR deploys
516 +- `alias`-ing `/_next/static` for a nested (monorepo) standalone build — static
517 + is under `<app>/<source>/.next/static`, so the alias 404s; proxy everything instead
518 +- leaving a dangling pnpm symlink in `.next/standalone/` — `rsync --copy-links`
519 + fails with status 23; `find … -type l ! -exec test -e {} \; -delete` first
520 +- giving two hostnames one `server` block without a shared SAN cert
521 - using static-file Nginx config for an SSR app
522 - rsyncing `.next/standalone/` without `--copy-links` and shipping broken `pnpm` symlinks to the server
523 - editing the repo copy of `ecosystem.config.cjs` and expecting a normal CLI deploy to overwrite the server copy — it is preserved intentionally
.agents/skills/smbcloud-deploy-rsync/SKILL.md
+6
@@ -170,3 +170,9 @@ If it migrates to the embedding crate, validate equivalent output and error hand
170 - hiding transport stderr that the user needs to debug deploy failures
171 - hardcoding local paths instead of using smbCloud config and user-derived SSH identity
172 - treating the workspace `project.id` as the deployable app identity — always use `frontend_app_id` for app-level deploy tracking
173 +- assuming `--copy-links` is harmless: it follows every symlink, so one dangling
174 + link in the source (e.g. a stray pnpm `.pnpm/node_modules/<pkg>` pointing at an
175 + untraced version) makes rsync exit **23**
176 + (`IO error encountered -- skipping file deletion`). The `nextjs-ssr` path hits
177 + this on `.next/standalone/` — prune dangling links before transfer (or use
178 + `--copy-unsafe-links` semantics). See the nextjs skill.