Skip to content

Commit 69daacb

Browse files
committed
smbCloud Deploy enhancement
1 parent 480587f commit 69daacb

16 files changed

Lines changed: 851 additions & 139 deletions

File tree

.agents/skills/smbcloud-deploy-monorepo/SKILL.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ smb deploy --project <name>
180180

181181
### Strategy: `nextjs-ssr`
182182

183-
Local build, rsync three directories, then SSH delete + fresh PM2 start (prefer server `ecosystem.config.js` when present). See `smbcloud-deploy-nextjs` skill for details.
183+
Local build, rsync three directories, then SSH delete + fresh PM2 start (prefer server `ecosystem.config.cjs` when present; falls back to legacy `.js`). The `.next/standalone/` upload should use `--copy-links` so `pnpm` symlinked dependencies land as real files in the deployed tree. If `outputFileTracingRoot` points above the sub-project directory, Next may preserve the sub-project path inside standalone output, and the deploy path must place `.next/static` and `public` under that nested runtime directory. See `smbcloud-deploy-nextjs` skill for details.
184184

185185
Required fields: `kind`, `source`, `path`, `pm2_app`, `package_manager`, `port`
186186

@@ -211,6 +211,7 @@ For `nextjs-ssr` and `vite-spa`, the CLI changes into the `source` directory bef
211211
- `pnpm install` runs inside `source/`
212212
- `pnpm build` runs inside `source/`
213213
- rsync sources are relative to `source/`
214+
- for Next.js SSR, standalone output may still preserve the `source/` path inside `.next/standalone/` when tracing is rooted above the app directory
214215

215216
For `rails`, the CLI:
216217

@@ -376,7 +377,7 @@ steps:
376377
- name: Upload
377378
run: |
378379
cd frontend/splitfire-web
379-
rsync -az --delete --mkpath .next/standalone/ $GIT_USER@$GIT_HOST:$REMOTE_PATH/
380+
rsync -az --delete --copy-links --mkpath .next/standalone/ $GIT_USER@$GIT_HOST:$REMOTE_PATH/
380381
rsync -az --delete --mkpath .next/static/ $GIT_USER@$GIT_HOST:$REMOTE_PATH/.next/static/
381382
rsync -az --delete --mkpath public/ $GIT_USER@$GIT_HOST:$REMOTE_PATH/public/
382383
@@ -389,7 +390,9 @@ steps:
389390
if pm2 describe "splitfire-web" > /dev/null 2>&1; then
390391
pm2 delete "splitfire-web"
391392
fi
392-
if [ -f ecosystem.config.js ]; then
393+
if [ -f ecosystem.config.cjs ]; then
394+
pm2 start ecosystem.config.cjs --only "splitfire-web" --env production
395+
elif [ -f ecosystem.config.js ]; then
393396
pm2 start ecosystem.config.js --only "splitfire-web" --env production
394397
else
395398
PORT=3010 HOSTNAME=127.0.0.1 pm2 start node --name "splitfire-web" -- server.js
@@ -446,15 +449,23 @@ All CI rsync commands must include:
446449
- `--delete` — removes stale files on the remote (prevents serving old assets)
447450
- `-az` — archive mode with compression
448451

452+
For Next.js SSR standalone uploads, also include:
453+
454+
- `--copy-links` on `.next/standalone/` — dereferences `pnpm` symlinked dependencies so the server does not end up with broken `node_modules` links
455+
449456
### CI PM2 restart
450457

451458
Current CI and CLI parity should use delete + fresh start, not restart-in-place:
452459

453460
- `pm2 delete <app>` if it exists
454-
- `pm2 start ecosystem.config.js --only <app> --env production` when the server file exists
461+
- `pm2 start ecosystem.config.cjs --only <app> --env production` when the `.cjs` file exists (preferred)
462+
- fall back to `pm2 start ecosystem.config.js --only <app> --env production` if only the legacy `.js` file exists
455463
- otherwise fall back to `PORT=<port> HOSTNAME=127.0.0.1 pm2 start node --name <app> -- server.js`
456464
- `pm2 save`
457465

466+
467+
When a monorepo root `package.json` has `"type": "module"`, PM2 cannot load a `.js` ecosystem config because Node treats it as ESM. The CLI auto-migrates `.js` → `.cjs` on the server during deploy and checks `.cjs` first when starting PM2.
468+
458469
This matches the standalone Next.js deploy model and avoids stale PM2 command state from older deploy methods.
459470

460471
## Deployment tracking
@@ -527,12 +538,16 @@ Two `nextjs-ssr` apps have the same `port` value. PM2 will start both, but only
527538

528539
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`.
529540

541+
### `Cannot find module 'next'` after Next.js SSR deploy
542+
543+
The remote app directory still contains broken `pnpm` symlinks from `.next/standalone/`, or the standalone output itself is missing traced runtime files. Use the current CLI or CI flow with `rsync --copy-links` for the standalone upload, verify `node .next/standalone/server.js` works locally, then redeploy and inspect `/home/git/apps/web/<app>/node_modules/next` on the server.
544+
530545
### PM2 not picking up new environment variables
531546

532547
Current CLI parity is delete + fresh start. Check these in order:
533548

534-
1. the server's `ecosystem.config.js` was edited, not just the repo copy
535-
2. the deploy script is actually starting from `ecosystem.config.js`
549+
1. the server's `ecosystem.config.cjs` (or legacy `.js`) was edited, not just the repo copy
550+
2. the deploy script is actually starting from `ecosystem.config.cjs` — if only a `.js` file exists and `"type": "module"` is set in `package.json`, PM2 will fail; rename to `.cjs`
536551
3. `pm2 save` ran after the fresh start
537552

538553
If you are changing env manually outside the deploy flow, `pm2 restart <app> --update-env && pm2 save` is still a valid operator command.
@@ -567,6 +582,7 @@ If you are changing env manually outside the deploy flow, `pm2 restart <app> --u
567582
- using `kind = "nextjs-ssr"` on a project without `output: "standalone"` in its Next.js config
568583
- running `smb deploy` from a subdirectory instead of the monorepo root — `source` paths will not resolve
569584
- forgetting `--mkpath` on rsync in CI workflows — first deploys fail because the remote directory does not exist
585+
- forgetting `--copy-links` on `.next/standalone/` uploads — the server receives broken `pnpm` symlinks and PM2 crashes with `Cannot find module 'next'`
570586
- keeping stale Nginx configs pointing at old deploy paths after migrating to a new directory structure
571587
- mixing up `path` (remote destination) and `source` (local directory) — they serve different purposes
572588
- treating the root workspace id as the deployable app id

.agents/skills/smbcloud-deploy-nextjs/SKILL.md

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ The CLI route in `process_deploy.rs` short-circuits on:
3636
That path builds locally, then:
3737

3838
- rsyncs `.next/standalone/` into the remote app directory root
39-
- rsyncs `.next/static/` into `remote/.next/static/`
40-
- rsyncs `public/` into `remote/public/`
39+
- rsyncs `.next/static/` into the runtime directory's `.next/static/`
40+
- rsyncs `public/` into the runtime directory's `public/`
4141
- runs `ssh` and restarts the app with PM2
4242

43+
When `outputFileTracingRoot` points above the app directory, Next may preserve the source path inside `.next/standalone/` (for example `.next/standalone/web/server.js`). The deploy path must keep that nested runtime layout intact.
44+
4345
Do not describe this flow as git based unless the project is actually using the generic git deploy path.
4446

4547
## Required app configuration
@@ -97,18 +99,26 @@ Important rules:
9799

98100
## Expected remote layout
99101

100-
After a successful deploy, the remote directory should look like:
102+
After a successful deploy, the remote directory should look like one of these layouts:
101103

102-
- `/home/git/apps/web/<app>/server.js`
103-
- `/home/git/apps/web/<app>/.next/static/...`
104-
- `/home/git/apps/web/<app>/public/...`
104+
- flat runtime
105+
- `/home/git/apps/web/<app>/server.js`
106+
- `/home/git/apps/web/<app>/.next/static/...`
107+
- `/home/git/apps/web/<app>/public/...`
108+
- nested runtime when standalone preserves the source path
109+
- `/home/git/apps/web/<app>/node_modules/...`
110+
- `/home/git/apps/web/<app>/web/server.js`
111+
- `/home/git/apps/web/<app>/web/.next/static/...`
112+
- `/home/git/apps/web/<app>/web/public/...`
105113

106-
Note that `server.js` is expected at the remote root because the CLI uploads the contents of `.next/standalone/`, not the folder itself.
114+
The CLI uploads the contents of `.next/standalone/`, not the folder itself. In nested-runtime mode it also needs to keep PM2 compatibility for old root-level `server.js` and `.next/standalone/server.js` entrypoints.
107115

108116
## PM2 behavior
109117

110118
The CLI's deploy path now deletes and starts fresh, preferring the server's
111-
`ecosystem.config.js` when it exists.
119+
ecosystem config file when it exists. The CLI checks for `ecosystem.config.cjs`
120+
first, then falls back to `ecosystem.config.js`. During deploy, the server-side
121+
migration auto-renames `.js` to `.cjs` when applicable.
112122

113123
Conceptually it behaves like:
114124

@@ -120,7 +130,9 @@ if pm2 describe "$PM2_APP" > /dev/null 2>&1; then
120130
pm2 delete "$PM2_APP"
121131
fi
122132

123-
if [ -f ecosystem.config.js ]; then
133+
if [ -f ecosystem.config.cjs ]; then
134+
pm2 start ecosystem.config.cjs --only "$PM2_APP" --env production
135+
elif [ -f ecosystem.config.js ]; then
124136
pm2 start ecosystem.config.js --only "$PM2_APP" --env production
125137
else
126138
NODE_ENV=production PORT=<port> HOSTNAME=127.0.0.1 pm2 start node --name "$PM2_APP" -- server.js
@@ -133,11 +145,13 @@ This has two consequences:
133145

134146
- old git-based `post-receive` hooks are irrelevant for the SSR path
135147
- the configured `port` in `.smb/config.toml` is used for the fallback start path
136-
- the live server `ecosystem.config.js` is the runtime source of truth when present
148+
- the live server `ecosystem.config.cjs` (or `.js`) is the runtime source of truth when present
149+
150+
### Environment variables — ecosystem file (standard pattern)
137151

138-
### Environment variables — ecosystem file (standard pattern)
152+
The server runs in a **multi-tenant setup**: multiple Next.js apps share one server, each managed as a separate PM2 process. The standard way to manage per-app environment variables is an `ecosystem.config.cjs` file kept **on the server only** (never committed to the app repo).
139153

140-
The server runs in a **multi-tenant setup**: multiple Next.js apps share one server, each managed as a separate PM2 process. The standard way to manage per-app environment variables is an `ecosystem.config.js` file kept **on the server only** (never committed to the app repo).
154+
Use the `.cjs` extension. This is required when the project's `package.json` has `"type": "module"`, because Node treats `.js` files as ESM in that case and `module.exports` syntax will fail.
141155

142156
The flow works like this:
143157

@@ -147,7 +161,7 @@ The flow works like this:
147161
ssh git@<server>
148162
cd /home/git/apps/web/<app>
149163

150-
cat > ecosystem.config.js << 'EOF'
164+
cat > ecosystem.config.cjs << 'EOF'
151165
module.exports = {
152166
apps: [{
153167
name: "<pm2_app>",
@@ -163,20 +177,20 @@ The flow works like this:
163177
}
164178
EOF
165179
166-
pm2 start ecosystem.config.js --env production
180+
pm2 start ecosystem.config.cjs --env production
167181
pm2 save
168182
```
169183
170-
2. Every subsequent CLI deploy deletes the PM2 process and starts it fresh. If the server has `ecosystem.config.js`, the CLI starts PM2 from that file again. If not, it falls back to `node server.js` with inline `NODE_ENV`, `PORT`, and `HOSTNAME`.
184+
2. Every subsequent CLI deploy deletes the PM2 process and starts it fresh. The CLI checks for `ecosystem.config.cjs` first, then `ecosystem.config.js`. If neither exists, it falls back to `node server.js` with inline `NODE_ENV`, `PORT`, and `HOSTNAME`.
171185
172-
3. To change or add env vars, SSH in, edit `ecosystem.config.js`, then:
186+
3. To change or add env vars, SSH in, edit `ecosystem.config.cjs`, then:
173187
174188
```sh
175189
pm2 restart <pm2_app> --update-env
176190
pm2 save
177191
```
178192
179-
The ecosystem file is the server-side source of truth for each app's runtime config. The CLI preserves the server copy during rsync and must not delete it.
193+
The ecosystem file is the server-side source of truth for each app's runtime config. The CLI preserves both `ecosystem.config.cjs` and `ecosystem.config.js` on the server during rsync and must not delete either.
180194
181195
### Manual PM2 operations (without ecosystem file)
182196
@@ -203,7 +217,7 @@ pm2 save
203217
204218
Use one source of truth for the runtime port.
205219
206-
If an older `ecosystem.config.js` exists and contains both:
220+
If an older ecosystem config (`.cjs` or `.js`) exists and contains both:
207221
208222
- `args: "start -p 3026"`
209223
- `env_production.PORT = 3025`
@@ -213,7 +227,7 @@ that is a broken configuration. Standardize on one port value.
213227
Current rules:
214228
215229
- fallback fresh-start path uses `port` from `.smb/config.toml`
216-
- ecosystem-managed apps should set the same port in `env_production.PORT`
230+
- ecosystem-managed apps should set the same port in `env_production.PORT` (in `ecosystem.config.cjs`)
217231
- nginx upstream must match that port
218232
219233
## Nginx for SSR
@@ -273,24 +287,67 @@ Check:
273287
274288
### Missing `public/`
275289
276-
The current deploy code tries to rsync `public/` unconditionally.
290+
`public/` is optional. The current deploy code skips it when the directory does not exist.
291+
292+
If assets are missing in production, check whether the app actually has a `public/` directory locally and whether the expected files were copied into the remote `public/` directory.
293+
294+
### `Cannot find module 'next'` or `502 Bad Gateway`
295+
296+
Cause:
297+
298+
- `.next/standalone/node_modules/*` contains `pnpm` symlinks
299+
- rsync preserved the symlinks instead of copying their targets
300+
- or the standalone output itself is incomplete and missing traced runtime files
301+
- PM2 starts `server.js`, Node cannot resolve `next`, and nginx returns `502 Bad Gateway`
302+
303+
Fix:
304+
305+
- use the dedicated `nextjs-ssr` CLI path in current `smbcloud-cli`
306+
- make sure the standalone upload uses `rsync --copy-links`
307+
- verify `node .next/standalone/server.js` works locally before deploy
308+
- if local standalone fails too, add `outputFileTracingIncludes` for the missing runtime files and rebuild
309+
- then restart PM2 and recheck `pm2 logs <pm2_app>`
310+
311+
### `Cannot find module 'styled-jsx'`, `@swc/helpers`, or other pnpm peer deps
277312
278-
If the project has no `public/` directory, deploy may fail. Either:
313+
Cause:
314+
315+
- pnpm stores peer dependencies inside `node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>` with internal symlinks that Node's standard `require.resolve()` cannot follow
316+
- Next.js standalone traces the files into `.pnpm/` correctly, but does not create the flat `node_modules/<pkg>` entries needed for resolution
317+
- this affects `styled-jsx`, `@swc/helpers`, `@next/env`, and other packages that `next` requires at startup
318+
319+
Fix:
279320
280-
- create an empty `public/`
281-
- or patch the CLI to skip missing directories
321+
- the CLI deploy script now auto-hoists: after uploading standalone output, it scans `node_modules/.pnpm/*/node_modules/*` and creates symlinks at `node_modules/<pkg>` for any packages not already present
322+
- this restores the resolution structure that pnpm normally provides via its `.pnpm/node_modules/` virtual directory
323+
- no manual intervention needed for new deploys
324+
- to fix a server manually: `find node_modules/.pnpm -mindepth 2 -maxdepth 2 -type d -name node_modules` and symlink each child into the root `node_modules/`
282325
283326
### Wrong port after deploy
284327
285328
Cause:
286329
287-
- `port` in `.smb/config.toml`, server `ecosystem.config.js`, and nginx upstream disagree
330+
- `port` in `.smb/config.toml`, server ecosystem config (`.cjs` or `.js`), and nginx upstream disagree
288331
289332
Fix:
290333
291334
- standardize the same port in all three places
292335
- if using ecosystem config, make sure PM2 is actually starting from that file
293336
337+
### PM2 "module is not defined" or "require is not defined"
338+
339+
Cause:
340+
341+
- the project's `package.json` has `"type": "module"`, which makes Node treat `.js` files as ESM
342+
- `ecosystem.config.js` uses `module.exports` (CommonJS), which is invalid under ESM
343+
- deploy shims that use `require()` will also fail under ESM
344+
345+
Fix:
346+
347+
- rename `ecosystem.config.js` to `ecosystem.config.cjs` on the server
348+
- deploy shims must use `import()` not `require()`
349+
- the CLI now handles both automatically: it checks for `.cjs` first and the server-side migration auto-renames `.js` to `.cjs` during deploy
350+
294351
### Misleading deploy output
295352
296353
The CLI may print generic deploy messages such as:
@@ -336,7 +393,9 @@ Use the smallest checks that prove the contract.
336393
### Next.js app
337394
338395
- `pnpm build`
339-
- verify `.next/standalone/server.js` exists
396+
- verify standalone contains a runnable server entrypoint
397+
- if `.next/standalone/server.js` exists, verify `node .next/standalone/server.js` starts locally
398+
- if standalone preserves the source path, verify `node .next/standalone/<source>/server.js` starts locally
340399
341400
### smbcloud-cli
342401
@@ -348,6 +407,7 @@ Use the smallest checks that prove the contract.
348407
- `pm2 list`
349408
- `pm2 logs <pm2_app>`
350409
- `ls -la /home/git/apps/web/<app>`
410+
- `ls -ld /home/git/apps/web/<app>/node_modules/next`
351411
- `sudo nginx -t`
352412
353413
## Common mistakes
@@ -356,8 +416,10 @@ Use the smallest checks that prove the contract.
356416
- forgetting `kind = "nextjs-ssr"`
357417
- keeping an old git `post-receive` hook and expecting it to manage SSR deploys
358418
- using static-file Nginx config for an SSR app
359-
- editing the repo copy of `ecosystem.config.js` and expecting a normal CLI deploy to overwrite the server copy — it is preserved intentionally
360-
- editing the server `ecosystem.config.js` but the deploy script falling back to inline `node server.js` because the file path check is wrong
419+
- rsyncing `.next/standalone/` without `--copy-links` and shipping broken `pnpm` symlinks to the server
420+
- editing the repo copy of `ecosystem.config.cjs` and expecting a normal CLI deploy to overwrite the server copy — it is preserved intentionally
421+
- editing the server `ecosystem.config.cjs` but the deploy script falling back to inline `node server.js` because the file path check is wrong
422+
- keeping an `ecosystem.config.js` when the project uses ESM (`"type": "module"` in `package.json`) — rename it to `.cjs` or let the CLI migration handle it
361423
- leaving port values inconsistent across PM2, Nginx, and `.smb/config.toml`
362424
- forgetting to set `frontend_app_id` on the deploy target once app-level deployment tracking is available
363425
- treating browser CORS failures as real internet outages

crates/cli/src/cli/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,9 @@ pub enum Commands {
7070
#[clap(subcommand)]
7171
command: mail::cli::Commands,
7272
},
73+
#[clap(
74+
about = "Migrate local .smb/config.toml deploy fields to the smbCloud server.",
75+
display_order = 4
76+
)]
77+
Migrate {},
7378
}

0 commit comments

Comments
 (0)