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.