vibecarbon 0.7.0 → 0.8.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/carbon/biome.json +3 -2
- package/carbon/package.json +20 -20
- package/carbon/src/server/lib/env.ts +1 -1
- package/package.json +13 -13
- package/src/lib/orphan.js +221 -0
- package/src/up.js +22 -7
package/carbon/biome.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"root": false,
|
|
3
|
+
"$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
|
|
3
4
|
"linter": {
|
|
4
5
|
"enabled": true,
|
|
5
6
|
"rules": {
|
|
6
|
-
"
|
|
7
|
+
"preset": "recommended",
|
|
7
8
|
"complexity": {
|
|
8
9
|
"noUselessFragments": "warn"
|
|
9
10
|
},
|
package/carbon/package.json
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"stripe:listen": "stripe listen --forward-to localhost:3000/api/webhooks/stripe"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@base-ui/react": "^1.
|
|
53
|
+
"@base-ui/react": "^1.6.0",
|
|
54
54
|
"@fontsource-variable/noto-sans": "^5.2.10",
|
|
55
55
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
56
56
|
"@hono/node-server": "^2.0.6",
|
|
@@ -58,39 +58,39 @@
|
|
|
58
58
|
"@hookform/resolvers": "^5.4.0",
|
|
59
59
|
"@mdx-js/react": "^3.1.1",
|
|
60
60
|
"@mdx-js/rollup": "^3.1.1",
|
|
61
|
-
"@scalar/api-reference-react": "^0.9.
|
|
62
|
-
"@supabase/supabase-js": "^2.
|
|
63
|
-
"@tanstack/react-query": "^5.
|
|
61
|
+
"@scalar/api-reference-react": "^0.9.50",
|
|
62
|
+
"@supabase/supabase-js": "^2.110.0",
|
|
63
|
+
"@tanstack/react-query": "^5.101.2",
|
|
64
64
|
"class-variance-authority": "^0.7.1",
|
|
65
65
|
"clsx": "^2.1.1",
|
|
66
66
|
"cmdk": "^1.1.1",
|
|
67
67
|
"date-fns": "^4.4.0",
|
|
68
68
|
"embla-carousel-react": "^8.6.0",
|
|
69
|
-
"framer-motion": "^12.
|
|
69
|
+
"framer-motion": "^12.42.2",
|
|
70
70
|
"hono": "^4.12.27",
|
|
71
|
-
"i18next": "^26.3.
|
|
71
|
+
"i18next": "^26.3.4",
|
|
72
72
|
"i18next-browser-languagedetector": "^8.2.1",
|
|
73
73
|
"input-otp": "^1.4.2",
|
|
74
74
|
"ioredis": "^5.11.1",
|
|
75
75
|
"lenis": "^1.3.25",
|
|
76
|
-
"lucide-react": "^1.
|
|
76
|
+
"lucide-react": "^1.22.0",
|
|
77
77
|
"next-themes": "^0.4.6",
|
|
78
|
-
"nodemailer": "^9.0.
|
|
78
|
+
"nodemailer": "^9.0.3",
|
|
79
79
|
"pino": "^10.3.1",
|
|
80
80
|
"react": "^19.2.7",
|
|
81
81
|
"react-day-picker": "^10.0.1",
|
|
82
82
|
"react-dom": "^19.2.7",
|
|
83
|
-
"react-hook-form": "^7.
|
|
83
|
+
"react-hook-form": "^7.80.0",
|
|
84
84
|
"react-i18next": "^17.0.8",
|
|
85
|
-
"react-resizable-panels": "^4.
|
|
86
|
-
"react-router-dom": "^7.
|
|
87
|
-
"recharts": "^3.
|
|
85
|
+
"react-resizable-panels": "^4.12.0",
|
|
86
|
+
"react-router-dom": "^7.18.1",
|
|
87
|
+
"recharts": "^3.9.1",
|
|
88
88
|
"rehype-autolink-headings": "^7.1.0",
|
|
89
89
|
"rehype-slug": "^6.0.0",
|
|
90
90
|
"remark-frontmatter": "^5.0.0",
|
|
91
91
|
"remark-mdx-frontmatter": "^5.2.0",
|
|
92
92
|
"sonner": "^2.0.7",
|
|
93
|
-
"stripe": "^22.
|
|
93
|
+
"stripe": "^22.3.0",
|
|
94
94
|
"tailwind-merge": "^3.6.0",
|
|
95
95
|
"vaul": "^1.1.2",
|
|
96
96
|
"zod": "^4.4.3"
|
|
@@ -122,16 +122,16 @@
|
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
"devDependencies": {
|
|
125
|
-
"@biomejs/biome": "^2.
|
|
125
|
+
"@biomejs/biome": "^2.5.1",
|
|
126
126
|
"@hono/swagger-ui": "^0.6.1",
|
|
127
|
-
"@scalar/hono-api-reference": "^0.
|
|
127
|
+
"@scalar/hono-api-reference": "^0.11.7",
|
|
128
128
|
"@tailwindcss/typography": "^0.5.20",
|
|
129
129
|
"@tailwindcss/vite": "^4.3.2",
|
|
130
130
|
"@testing-library/jest-dom": "^6.9.1",
|
|
131
131
|
"@testing-library/react": "^16.3.2",
|
|
132
132
|
"@testing-library/user-event": "^14.6.1",
|
|
133
|
-
"@types/node": "^
|
|
134
|
-
"@types/nodemailer": "^8.0.
|
|
133
|
+
"@types/node": "^26.0.1",
|
|
134
|
+
"@types/nodemailer": "^8.0.1",
|
|
135
135
|
"@types/pg": "^8.20.0",
|
|
136
136
|
"@types/react": "^19.2.17",
|
|
137
137
|
"@types/react-dom": "^19.2.3",
|
|
@@ -139,15 +139,15 @@
|
|
|
139
139
|
"@vitest/coverage-v8": "^4.1.9",
|
|
140
140
|
"esbuild": "^0.28.1",
|
|
141
141
|
"jsdom": "^29.1.1",
|
|
142
|
-
"pg": "^8.
|
|
142
|
+
"pg": "^8.22.0",
|
|
143
143
|
"pino-pretty": "^13.1.3",
|
|
144
144
|
"remark-gfm": "^4.0.1",
|
|
145
|
-
"shadcn": "^4.
|
|
145
|
+
"shadcn": "^4.12.0",
|
|
146
146
|
"tailwindcss": "^4.3.2",
|
|
147
147
|
"tsx": "^4.22.4",
|
|
148
148
|
"tw-animate-css": "^1.4.0",
|
|
149
149
|
"typescript": "^6.0.3",
|
|
150
|
-
"vite": "^8.
|
|
150
|
+
"vite": "^8.1.2",
|
|
151
151
|
"vitest": "^4.1.9"
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -102,7 +102,7 @@ if (process.env.NODE_ENV !== 'production' && portOffset !== 0) {
|
|
|
102
102
|
// makes "" mean "not set", matching operator intent (and how the k8s Secret
|
|
103
103
|
// path already skips empty values).
|
|
104
104
|
const cleanedEnv = Object.fromEntries(
|
|
105
|
-
Object.entries(process.env).filter(([, value]) => value !== '')
|
|
105
|
+
Object.entries(process.env).filter(([, value]) => value !== '')
|
|
106
106
|
);
|
|
107
107
|
|
|
108
108
|
// Validate environment variables
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecarbon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Create and manage production-ready Vibecarbon applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./src/cli.js",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"url": "https://github.com/hyperformant/vibecarbon.git"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@aws-sdk/client-s3": "^3.
|
|
61
|
-
"@clack/prompts": "^1.
|
|
62
|
-
"@pulumi/hcloud": "^1.
|
|
63
|
-
"@pulumi/pulumi": "^3.
|
|
60
|
+
"@aws-sdk/client-s3": "^3.1077.0",
|
|
61
|
+
"@clack/prompts": "^1.6.0",
|
|
62
|
+
"@pulumi/hcloud": "^1.39.0",
|
|
63
|
+
"@pulumi/pulumi": "^3.248.0",
|
|
64
64
|
"bcryptjs": "^3.0.3",
|
|
65
|
-
"undici": "^8.
|
|
65
|
+
"undici": "^8.5.0",
|
|
66
66
|
"which": "^7.0.0"
|
|
67
67
|
},
|
|
68
68
|
"pnpm": {
|
|
@@ -81,18 +81,18 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@biomejs/biome": "^2.
|
|
84
|
+
"@biomejs/biome": "^2.5.1",
|
|
85
85
|
"@semantic-release/changelog": "^6.0.3",
|
|
86
86
|
"@semantic-release/git": "^10.0.1",
|
|
87
87
|
"@types/better-sqlite3": "^7.6.13",
|
|
88
|
-
"@vitest/coverage-v8": "^4.1.
|
|
88
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
89
89
|
"autocannon": "^8.0.0",
|
|
90
|
-
"better-sqlite3": "^12.
|
|
91
|
-
"conventional-changelog-conventionalcommits": "^
|
|
92
|
-
"semantic-release": "^25.0.
|
|
90
|
+
"better-sqlite3": "^12.11.1",
|
|
91
|
+
"conventional-changelog-conventionalcommits": "^10.2.0",
|
|
92
|
+
"semantic-release": "^25.0.5",
|
|
93
93
|
"tsx": "^4.22.4",
|
|
94
|
-
"vite": "^8.
|
|
95
|
-
"vitest": "^4.1.
|
|
94
|
+
"vite": "^8.1.2",
|
|
95
|
+
"vitest": "^4.1.9",
|
|
96
96
|
"zod": "^4.4.3"
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orphan dev-session reclaim helpers for `vibecarbon up`.
|
|
3
|
+
*
|
|
4
|
+
* When a project port is in use, the most common cause is an orphaned dev
|
|
5
|
+
* session belonging to the SAME project — a previous `node scripts/dev.js`
|
|
6
|
+
* tree (its child Vite + tsx API servers) that outlived its parent and was
|
|
7
|
+
* reparented to init while still holding the port. Rather than dodge the
|
|
8
|
+
* conflict by bumping DEV_PORT_OFFSET (which leaves the runaway alive and
|
|
9
|
+
* drifts the project onto a new port band), we identify the process actually
|
|
10
|
+
* listening on the port and, if it belongs to this project's working
|
|
11
|
+
* directory, kill its whole process group so a clean restart can reuse the
|
|
12
|
+
* normal ports.
|
|
13
|
+
*
|
|
14
|
+
* Strict scoping is the safety contract: we kill a process only when its
|
|
15
|
+
* working directory resolves to the project dir (or a subdir). A process we
|
|
16
|
+
* can't introspect (different user → EACCES) is treated as foreign and never
|
|
17
|
+
* touched. On non-Unix platforms, or when `lsof`/`ps` are unavailable, the
|
|
18
|
+
* helpers degrade to no-ops and the caller falls back to offset-bumping.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { readlinkSync } from 'node:fs';
|
|
23
|
+
import { resolve, sep } from 'node:path';
|
|
24
|
+
|
|
25
|
+
const isUnix = () => process.platform !== 'win32';
|
|
26
|
+
|
|
27
|
+
const RUN_OPTS = { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* PIDs listening on the given TCP port. Returns deduped positive integers,
|
|
31
|
+
* or [] on non-Unix, missing `lsof`, or any error.
|
|
32
|
+
* @param {number} port
|
|
33
|
+
* @returns {number[]}
|
|
34
|
+
*/
|
|
35
|
+
export function findPortListeners(port) {
|
|
36
|
+
if (!isUnix()) return [];
|
|
37
|
+
try {
|
|
38
|
+
const out = execFileSync('lsof', ['-t', `-iTCP:${port}`, '-sTCP:LISTEN'], RUN_OPTS);
|
|
39
|
+
const pids = out
|
|
40
|
+
.trim()
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => Number(line.trim()))
|
|
43
|
+
.filter((n) => Number.isInteger(n) && n > 0);
|
|
44
|
+
return [...new Set(pids)];
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolved working directory of a process, or null if it can't be read
|
|
52
|
+
* (gone, foreign user / EACCES, or unsupported platform). Linux reads
|
|
53
|
+
* /proc/<pid>/cwd; other Unixes fall back to `lsof`.
|
|
54
|
+
* @param {number} pid
|
|
55
|
+
* @returns {string | null}
|
|
56
|
+
*/
|
|
57
|
+
export function getProcessCwd(pid) {
|
|
58
|
+
if (!isUnix()) return null;
|
|
59
|
+
try {
|
|
60
|
+
return readlinkSync(`/proc/${pid}/cwd`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// Different user owns the process — treat as foreign, never kill.
|
|
63
|
+
if (err && err.code === 'EACCES') return null;
|
|
64
|
+
// ENOENT (no /proc, e.g. macOS) → try lsof; other errors → null below.
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const out = execFileSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], RUN_OPTS);
|
|
68
|
+
const nameLine = out.split('\n').find((line) => line.startsWith('n'));
|
|
69
|
+
return nameLine ? nameLine.slice(1) : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Process group id (pgid) of a process, or null on failure.
|
|
77
|
+
* @param {number} pid
|
|
78
|
+
* @returns {number | null}
|
|
79
|
+
*/
|
|
80
|
+
export function getProcessGroup(pid) {
|
|
81
|
+
if (!isUnix()) return null;
|
|
82
|
+
try {
|
|
83
|
+
const out = execFileSync('ps', ['-o', 'pgid=', '-p', String(pid)], RUN_OPTS);
|
|
84
|
+
const pgid = Number(out.trim());
|
|
85
|
+
return Number.isInteger(pgid) && pgid > 0 ? pgid : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* True iff the process's working directory resolves to `projectCwd` or a
|
|
93
|
+
* subdirectory of it.
|
|
94
|
+
* @param {number} pid
|
|
95
|
+
* @param {string} projectCwd
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
export function isOwnedByProject(pid, projectCwd) {
|
|
99
|
+
const cwd = getProcessCwd(pid);
|
|
100
|
+
if (!cwd) return false;
|
|
101
|
+
const proc = resolve(cwd);
|
|
102
|
+
const root = resolve(projectCwd);
|
|
103
|
+
return proc === root || proc.startsWith(root + sep);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
107
|
+
|
|
108
|
+
/** True if the process is still alive (signal 0 probe). */
|
|
109
|
+
function isAlive(pid) {
|
|
110
|
+
try {
|
|
111
|
+
process.kill(pid, 0);
|
|
112
|
+
return true;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// ESRCH → gone. EPERM → alive but not ours (shouldn't happen for an
|
|
115
|
+
// owned orphan) — report alive so we don't claim a false reclaim.
|
|
116
|
+
return !!err && err.code !== 'ESRCH';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Best-effort signal to a target; swallows ESRCH/EPERM races. */
|
|
121
|
+
function signal(target, sig) {
|
|
122
|
+
try {
|
|
123
|
+
process.kill(target, sig);
|
|
124
|
+
} catch {
|
|
125
|
+
// Already gone or not permitted — nothing more we can do.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reclaim a port held by this project's own orphaned dev session.
|
|
131
|
+
*
|
|
132
|
+
* Finds the listeners on `port`, keeps only those whose working directory
|
|
133
|
+
* belongs to `projectCwd`, and kills each owned holder's process group
|
|
134
|
+
* (SIGTERM, then SIGKILL after a grace period). Foreign holders are reported
|
|
135
|
+
* but never touched.
|
|
136
|
+
*
|
|
137
|
+
* @param {number} port
|
|
138
|
+
* @param {string} projectCwd
|
|
139
|
+
* @param {{ graceMs?: number, intervalMs?: number }} [opts]
|
|
140
|
+
* @returns {Promise<{ killed: number[], foreign: number[], freed: boolean }>}
|
|
141
|
+
*/
|
|
142
|
+
export async function reclaimPort(port, projectCwd, { graceMs = 2000, intervalMs = 100 } = {}) {
|
|
143
|
+
const listeners = findPortListeners(port);
|
|
144
|
+
if (listeners.length === 0) return { killed: [], foreign: [], freed: false };
|
|
145
|
+
|
|
146
|
+
const owned = [];
|
|
147
|
+
const foreign = [];
|
|
148
|
+
for (const pid of listeners) {
|
|
149
|
+
if (isOwnedByProject(pid, projectCwd)) owned.push(pid);
|
|
150
|
+
else foreign.push(pid);
|
|
151
|
+
}
|
|
152
|
+
if (owned.length === 0) return { killed: [], foreign, freed: false };
|
|
153
|
+
|
|
154
|
+
// Prefer group-targeted kills so the whole dev.js → vite/tsx tree dies in
|
|
155
|
+
// one shot; fall back to the bare pid if we can't resolve a pgid.
|
|
156
|
+
const seenGroups = new Set();
|
|
157
|
+
const targets = [];
|
|
158
|
+
for (const pid of owned) {
|
|
159
|
+
const pgid = getProcessGroup(pid);
|
|
160
|
+
if (pgid) {
|
|
161
|
+
if (!seenGroups.has(pgid)) {
|
|
162
|
+
seenGroups.add(pgid);
|
|
163
|
+
targets.push(-pgid);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
targets.push(pid);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const target of targets) signal(target, 'SIGTERM');
|
|
171
|
+
|
|
172
|
+
const deadline = Date.now() + graceMs;
|
|
173
|
+
while (owned.some(isAlive) && Date.now() < deadline) {
|
|
174
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (owned.some(isAlive)) {
|
|
178
|
+
for (const target of targets) signal(target, 'SIGKILL');
|
|
179
|
+
await sleep(intervalMs);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const killed = owned.filter((pid) => !isAlive(pid));
|
|
183
|
+
const freed = !findPortListeners(port).length;
|
|
184
|
+
return { killed, foreign, freed };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @typedef {{ name: string, port: number }} PortConflict
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Reclaim each conflicting port held by this project's orphaned dev session,
|
|
193
|
+
* then report which conflicts remain (foreign or otherwise unrecoverable) so
|
|
194
|
+
* the caller can fall back to offset-bumping for just those.
|
|
195
|
+
*
|
|
196
|
+
* Each port is re-checked after reclaim because killing one orphan's process
|
|
197
|
+
* group can free a sibling port in the same dev tree.
|
|
198
|
+
*
|
|
199
|
+
* @param {PortConflict[]} conflicts
|
|
200
|
+
* @param {string} projectCwd
|
|
201
|
+
* @param {{
|
|
202
|
+
* reclaim?: (port: number, cwd: string) => Promise<{ killed: number[], foreign: number[], freed: boolean }>,
|
|
203
|
+
* recheck: (port: number) => Promise<boolean>,
|
|
204
|
+
* onReclaim?: (conflict: PortConflict, result: { killed: number[] }) => void,
|
|
205
|
+
* }} deps
|
|
206
|
+
* @returns {Promise<{ reclaimed: Array<PortConflict & { killed: number[] }>, remaining: PortConflict[] }>}
|
|
207
|
+
*/
|
|
208
|
+
export async function reclaimOrphanPorts(conflicts, projectCwd, deps) {
|
|
209
|
+
const { reclaim = reclaimPort, recheck, onReclaim } = deps;
|
|
210
|
+
const reclaimed = [];
|
|
211
|
+
const remaining = [];
|
|
212
|
+
for (const conflict of conflicts) {
|
|
213
|
+
const result = await reclaim(conflict.port, projectCwd);
|
|
214
|
+
if (result.killed.length > 0) {
|
|
215
|
+
reclaimed.push({ ...conflict, killed: result.killed });
|
|
216
|
+
onReclaim?.(conflict, result);
|
|
217
|
+
}
|
|
218
|
+
if (await recheck(conflict.port)) remaining.push(conflict);
|
|
219
|
+
}
|
|
220
|
+
return { reclaimed, remaining };
|
|
221
|
+
}
|
package/src/up.js
CHANGED
|
@@ -14,6 +14,7 @@ import { renderHelp } from './lib/cli/help.js';
|
|
|
14
14
|
import { parseFlags } from './lib/cli/parse-flags.js';
|
|
15
15
|
import { c, printBanner } from './lib/colors.js';
|
|
16
16
|
import { runCommandThroughTaskLog } from './lib/command.js';
|
|
17
|
+
import { reclaimOrphanPorts } from './lib/orphan.js';
|
|
17
18
|
import { detectPackageManager } from './lib/project.js';
|
|
18
19
|
import { assertInProjectDir } from './lib/project-guard.js';
|
|
19
20
|
import { VERSION } from './lib/version.js';
|
|
@@ -219,14 +220,28 @@ export async function run(args = []) {
|
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
if (conflicts.length > 0) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
// First reclaim any ports held by THIS project's own orphaned dev session
|
|
224
|
+
// (a previous `dev.js` tree that outlived its parent and kept squatting on
|
|
225
|
+
// the ports). Killing the orphan lets us reuse the normal port band; only
|
|
226
|
+
// genuinely foreign conflicts fall through to offset-bumping.
|
|
227
|
+
const { remaining } = await reclaimOrphanPorts(conflicts, cwd, {
|
|
228
|
+
recheck: isPortInUse,
|
|
229
|
+
onReclaim: ({ name, port }, { killed }) =>
|
|
230
|
+
p.log.success(
|
|
231
|
+
`Reclaimed ${name} port ${c.bold(String(port))} from orphaned dev session (PID ${killed.join(', ')})`,
|
|
232
|
+
),
|
|
233
|
+
});
|
|
226
234
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
if (remaining.length > 0) {
|
|
236
|
+
p.log.warn('Port conflicts detected (not owned by this project):');
|
|
237
|
+
for (const { name, port } of remaining) {
|
|
238
|
+
p.log.message(` ${c.dim(name.padEnd(20))} port ${c.bold(String(port))} is in use`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const offset = await findFreeOffset(cwd);
|
|
242
|
+
setPortOffset(offset, cwd);
|
|
243
|
+
p.log.success(`Applied DEV_PORT_OFFSET=${offset} to .env.local`);
|
|
244
|
+
}
|
|
230
245
|
}
|
|
231
246
|
|
|
232
247
|
const pm = detectPackageManager(cwd);
|