bgrun 3.12.1 → 3.12.2
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/dashboard/app/api/check-port/route.ts +35 -0
- package/dashboard/app/api/dependencies/route.ts +40 -0
- package/dashboard/app/api/deploy/[name]/route.ts +6 -41
- package/dashboard/app/api/deploy-all/route.ts +25 -0
- package/dashboard/app/api/next-port/route.ts +32 -0
- package/dashboard/app/api/start/route.ts +6 -0
- package/dashboard/app/globals.css +1313 -108
- package/dashboard/app/page.client.tsx +1576 -8
- package/dashboard/app/page.tsx +194 -5
- package/dist/index.js +129 -15
- package/package.json +1 -1
- package/src/bgrun.test.ts +171 -0
- package/src/db.ts +142 -0
- package/src/deploy.ts +163 -0
- package/src/index.ts +9 -0
- package/src/platform.ts +17 -9
package/src/bgrun.test.ts
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
import { describe, expect, test } from 'bun:test'
|
|
10
10
|
import { parseEnvString, calculateRuntime } from './utils'
|
|
11
11
|
import { stripAnsi, truncateString, truncatePath } from './table'
|
|
12
|
+
import { detectPackageManager, formatDeployToolError } from './deploy'
|
|
13
|
+
import { isProcessRunning } from './platform'
|
|
14
|
+
import { mkdirSync, rmSync } from 'fs'
|
|
15
|
+
|
|
16
|
+
// Use a test-specific database to avoid polluting real data
|
|
17
|
+
process.env.BGRUN_DB = `bgrun-test-${Date.now()}.sqlite`
|
|
18
|
+
import { addDependency, removeDependency, getDependencyGraph, getDependencies, getDependents, getStartOrder, removeAllDependencies } from './db'
|
|
12
19
|
|
|
13
20
|
// ─── parseEnvString ─────────────────────────────────────
|
|
14
21
|
|
|
@@ -107,3 +114,167 @@ describe('truncatePath', () => {
|
|
|
107
114
|
expect(result).toContain('…')
|
|
108
115
|
})
|
|
109
116
|
})
|
|
117
|
+
|
|
118
|
+
// ─── detectPackageManager ───────────────────────────────
|
|
119
|
+
|
|
120
|
+
// ─── isProcessRunning (Windows liveness fallback) ───────
|
|
121
|
+
|
|
122
|
+
describe('isProcessRunning', () => {
|
|
123
|
+
test('returns true for the current process PID', async () => {
|
|
124
|
+
const alive = await isProcessRunning(process.pid)
|
|
125
|
+
expect(alive).toBe(true)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('returns false for PID 0 (intentionally stopped)', async () => {
|
|
129
|
+
const alive = await isProcessRunning(0)
|
|
130
|
+
expect(alive).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('returns false for a very high unlikely PID', async () => {
|
|
134
|
+
const alive = await isProcessRunning(999999)
|
|
135
|
+
expect(alive).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('returns false for negative PID', async () => {
|
|
139
|
+
const alive = await isProcessRunning(-1)
|
|
140
|
+
expect(alive).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// ─── detectPackageManager ───────────────────────────────
|
|
145
|
+
|
|
146
|
+
describe('formatDeployToolError', () => {
|
|
147
|
+
test('returns actionable message for missing binary', () => {
|
|
148
|
+
const msg = formatDeployToolError('pnpm', new Error('command not found: pnpm'))
|
|
149
|
+
expect(msg).toContain("requires 'pnpm'")
|
|
150
|
+
expect(msg).toContain('PATH')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('preserves non-missing-binary failures', () => {
|
|
154
|
+
const msg = formatDeployToolError('npm', new Error('npm ci failed with exit code 1'))
|
|
155
|
+
expect(msg).toContain('Dependency install failed with npm')
|
|
156
|
+
expect(msg).toContain('exit code 1')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('detectPackageManager', () => {
|
|
161
|
+
test('returns null when no package.json exists', async () => {
|
|
162
|
+
const dir = `${process.cwd()}/tmp-no-package-${Date.now()}`
|
|
163
|
+
mkdirSync(dir, { recursive: true })
|
|
164
|
+
try {
|
|
165
|
+
expect(await detectPackageManager(dir)).toBeNull()
|
|
166
|
+
} finally {
|
|
167
|
+
rmSync(dir, { recursive: true, force: true })
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('prefers bun lockfiles', async () => {
|
|
172
|
+
const dir = `${process.cwd()}/tmp-bun-${Date.now()}`
|
|
173
|
+
mkdirSync(dir, { recursive: true })
|
|
174
|
+
try {
|
|
175
|
+
await Bun.write(`${dir}/package.json`, '{}')
|
|
176
|
+
await Bun.write(`${dir}/bun.lock`, '')
|
|
177
|
+
expect(await detectPackageManager(dir)).toBe('bun')
|
|
178
|
+
} finally {
|
|
179
|
+
rmSync(dir, { recursive: true, force: true })
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('detects pnpm, yarn, and npm lockfiles', async () => {
|
|
184
|
+
const base = `${process.cwd()}/tmp-pm-${Date.now()}`
|
|
185
|
+
|
|
186
|
+
const pnpmDir = `${base}-pnpm`
|
|
187
|
+
mkdirSync(pnpmDir, { recursive: true })
|
|
188
|
+
await Bun.write(`${pnpmDir}/package.json`, '{}')
|
|
189
|
+
await Bun.write(`${pnpmDir}/pnpm-lock.yaml`, '')
|
|
190
|
+
expect(await detectPackageManager(pnpmDir)).toBe('pnpm')
|
|
191
|
+
|
|
192
|
+
const yarnDir = `${base}-yarn`
|
|
193
|
+
mkdirSync(yarnDir, { recursive: true })
|
|
194
|
+
await Bun.write(`${yarnDir}/package.json`, '{}')
|
|
195
|
+
await Bun.write(`${yarnDir}/yarn.lock`, '')
|
|
196
|
+
expect(await detectPackageManager(yarnDir)).toBe('yarn')
|
|
197
|
+
|
|
198
|
+
const npmDir = `${base}-npm`
|
|
199
|
+
mkdirSync(npmDir, { recursive: true })
|
|
200
|
+
await Bun.write(`${npmDir}/package.json`, '{}')
|
|
201
|
+
await Bun.write(`${npmDir}/package-lock.json`, '{}')
|
|
202
|
+
expect(await detectPackageManager(npmDir)).toBe('npm')
|
|
203
|
+
|
|
204
|
+
rmSync(pnpmDir, { recursive: true, force: true })
|
|
205
|
+
rmSync(yarnDir, { recursive: true, force: true })
|
|
206
|
+
rmSync(npmDir, { recursive: true, force: true })
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('defaults to bun for package.json projects without a lockfile', async () => {
|
|
210
|
+
const dir = `${process.cwd()}/tmp-default-bun-${Date.now()}`
|
|
211
|
+
mkdirSync(dir, { recursive: true })
|
|
212
|
+
try {
|
|
213
|
+
await Bun.write(`${dir}/package.json`, '{}')
|
|
214
|
+
expect(await detectPackageManager(dir)).toBe('bun')
|
|
215
|
+
} finally {
|
|
216
|
+
rmSync(dir, { recursive: true, force: true })
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// ─── Dependencies ───────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
describe('addDependency', () => {
|
|
224
|
+
test('adds a valid dependency', () => {
|
|
225
|
+
removeAllDependencies('web-server');
|
|
226
|
+
removeAllDependencies('database');
|
|
227
|
+
const ok = addDependency('web-server', 'database');
|
|
228
|
+
expect(ok).toBe(true);
|
|
229
|
+
expect(getDependencies('web-server')).toContain('database');
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('prevents self-dependency', () => {
|
|
233
|
+
expect(addDependency('api', 'api')).toBe(false);
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('prevents duplicate dependency', () => {
|
|
237
|
+
removeAllDependencies('app');
|
|
238
|
+
addDependency('app', 'db');
|
|
239
|
+
expect(addDependency('app', 'db')).toBe(false);
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('prevents circular dependency', () => {
|
|
243
|
+
removeAllDependencies('a');
|
|
244
|
+
removeAllDependencies('b');
|
|
245
|
+
removeAllDependencies('c');
|
|
246
|
+
addDependency('a', 'b');
|
|
247
|
+
addDependency('b', 'c');
|
|
248
|
+
// c -> a would create a cycle
|
|
249
|
+
expect(addDependency('c', 'a')).toBe(false);
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
describe('getDependencyGraph', () => {
|
|
254
|
+
test('returns full graph', () => {
|
|
255
|
+
removeAllDependencies('svc-a');
|
|
256
|
+
removeAllDependencies('svc-b');
|
|
257
|
+
addDependency('svc-a', 'svc-b');
|
|
258
|
+
const graph = getDependencyGraph();
|
|
259
|
+
expect(graph['svc-a']).toContain('svc-b');
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('getDependents', () => {
|
|
264
|
+
test('finds processes that depend on a target', () => {
|
|
265
|
+
removeAllDependencies('frontend');
|
|
266
|
+
removeAllDependencies('backend');
|
|
267
|
+
addDependency('frontend', 'backend');
|
|
268
|
+
expect(getDependents('backend')).toContain('frontend');
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('removeDependency', () => {
|
|
273
|
+
test('removes an existing dependency', () => {
|
|
274
|
+
removeAllDependencies('x');
|
|
275
|
+
addDependency('x', 'y');
|
|
276
|
+
expect(getDependencies('x')).toContain('y');
|
|
277
|
+
removeDependency('x', 'y');
|
|
278
|
+
expect(getDependencies('x')).not.toContain('y');
|
|
279
|
+
})
|
|
280
|
+
})
|
package/src/db.ts
CHANGED
|
@@ -44,6 +44,14 @@ export const HistorySchema = z.object({
|
|
|
44
44
|
|
|
45
45
|
export type History = z.infer<typeof HistorySchema> & { id: number };
|
|
46
46
|
|
|
47
|
+
export const DependencySchema = z.object({
|
|
48
|
+
process_name: z.string(), // the process that has the dependency
|
|
49
|
+
depends_on: z.string(), // the process it depends on
|
|
50
|
+
created_at: z.string().default(() => new Date().toISOString()),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export type Dependency = z.infer<typeof DependencySchema> & { id: number };
|
|
54
|
+
|
|
47
55
|
// =============================================================================
|
|
48
56
|
// DATABASE INITIALIZATION
|
|
49
57
|
// =============================================================================
|
|
@@ -72,11 +80,13 @@ export const db = new Database(dbPath, {
|
|
|
72
80
|
process: ProcessSchema,
|
|
73
81
|
template: TemplateSchema,
|
|
74
82
|
history: HistorySchema,
|
|
83
|
+
dependency: DependencySchema,
|
|
75
84
|
}, {
|
|
76
85
|
indexes: {
|
|
77
86
|
process: ['name', 'timestamp', 'pid'],
|
|
78
87
|
template: ['name'],
|
|
79
88
|
history: ['process_name', 'timestamp'],
|
|
89
|
+
dependency: ['process_name', 'depends_on'],
|
|
80
90
|
},
|
|
81
91
|
});
|
|
82
92
|
|
|
@@ -242,6 +252,138 @@ export function clearOldHistory(daysToKeep = 30) {
|
|
|
242
252
|
return oldEntries.length;
|
|
243
253
|
}
|
|
244
254
|
|
|
255
|
+
// =============================================================================
|
|
256
|
+
// DEPENDENCY FUNCTIONS
|
|
257
|
+
// =============================================================================
|
|
258
|
+
|
|
259
|
+
/** Get all dependencies for a process */
|
|
260
|
+
export function getDependencies(processName: string): string[] {
|
|
261
|
+
return db.dependency.select()
|
|
262
|
+
.where({ process_name: processName })
|
|
263
|
+
.all()
|
|
264
|
+
.map(d => d.depends_on);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Get all processes that depend on a given process */
|
|
268
|
+
export function getDependents(processName: string): string[] {
|
|
269
|
+
return db.dependency.select()
|
|
270
|
+
.where({ depends_on: processName })
|
|
271
|
+
.all()
|
|
272
|
+
.map(d => d.process_name);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Get the full dependency graph: { processName -> [dependsOn...] } */
|
|
276
|
+
export function getDependencyGraph(): Record<string, string[]> {
|
|
277
|
+
const all = db.dependency.select().all();
|
|
278
|
+
const graph: Record<string, string[]> = {};
|
|
279
|
+
for (const dep of all) {
|
|
280
|
+
if (!graph[dep.process_name]) graph[dep.process_name] = [];
|
|
281
|
+
graph[dep.process_name].push(dep.depends_on);
|
|
282
|
+
}
|
|
283
|
+
return graph;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Add a dependency (process_name depends on depends_on) */
|
|
287
|
+
export function addDependency(processName: string, dependsOn: string): boolean {
|
|
288
|
+
// Prevent self-dependency
|
|
289
|
+
if (processName === dependsOn) return false;
|
|
290
|
+
|
|
291
|
+
// Prevent duplicates
|
|
292
|
+
const existing = db.dependency.select()
|
|
293
|
+
.where({ process_name: processName, depends_on: dependsOn })
|
|
294
|
+
.limit(1)
|
|
295
|
+
.get();
|
|
296
|
+
if (existing) return false;
|
|
297
|
+
|
|
298
|
+
// Prevent circular dependencies
|
|
299
|
+
if (wouldCreateCycle(processName, dependsOn)) return false;
|
|
300
|
+
|
|
301
|
+
db.dependency.insert({ process_name: processName, depends_on: dependsOn });
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Remove a dependency */
|
|
306
|
+
export function removeDependency(processName: string, dependsOn: string) {
|
|
307
|
+
const matches = db.dependency.select()
|
|
308
|
+
.where({ process_name: processName, depends_on: dependsOn })
|
|
309
|
+
.all();
|
|
310
|
+
for (const dep of matches) {
|
|
311
|
+
db.dependency.delete(dep.id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Remove all dependencies for a process */
|
|
316
|
+
export function removeAllDependencies(processName: string) {
|
|
317
|
+
const matches = db.dependency.select()
|
|
318
|
+
.where({ process_name: processName })
|
|
319
|
+
.all();
|
|
320
|
+
for (const dep of matches) {
|
|
321
|
+
db.dependency.delete(dep.id);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Check if adding processName -> dependsOn would create a cycle */
|
|
326
|
+
function wouldCreateCycle(processName: string, dependsOn: string): boolean {
|
|
327
|
+
const graph = getDependencyGraph();
|
|
328
|
+
// Add the proposed edge temporarily
|
|
329
|
+
if (!graph[processName]) graph[processName] = [];
|
|
330
|
+
graph[processName].push(dependsOn);
|
|
331
|
+
|
|
332
|
+
// DFS from dependsOn — if we can reach processName, it's a cycle
|
|
333
|
+
const visited = new Set<string>();
|
|
334
|
+
const stack = [dependsOn];
|
|
335
|
+
while (stack.length > 0) {
|
|
336
|
+
const current = stack.pop()!;
|
|
337
|
+
if (current === processName) return true;
|
|
338
|
+
if (visited.has(current)) continue;
|
|
339
|
+
visited.add(current);
|
|
340
|
+
for (const dep of (graph[current] || [])) {
|
|
341
|
+
stack.push(dep);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Get topological start order (processes with no deps first) */
|
|
348
|
+
export function getStartOrder(): string[] {
|
|
349
|
+
const graph = getDependencyGraph();
|
|
350
|
+
const allProcesses = getAllProcesses().map(p => p.name);
|
|
351
|
+
const allNames = new Set(allProcesses);
|
|
352
|
+
|
|
353
|
+
// Build in-degree map
|
|
354
|
+
const inDegree: Record<string, number> = {};
|
|
355
|
+
for (const name of allNames) inDegree[name] = 0;
|
|
356
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
357
|
+
for (const dep of deps) {
|
|
358
|
+
if (allNames.has(dep)) {
|
|
359
|
+
inDegree[proc] = (inDegree[proc] || 0) + 1;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Kahn's algorithm
|
|
365
|
+
const queue: string[] = [];
|
|
366
|
+
for (const name of allNames) {
|
|
367
|
+
if ((inDegree[name] || 0) === 0) queue.push(name);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const order: string[] = [];
|
|
371
|
+
while (queue.length > 0) {
|
|
372
|
+
queue.sort(); // stable alphabetical within same level
|
|
373
|
+
const current = queue.shift()!;
|
|
374
|
+
order.push(current);
|
|
375
|
+
// Find processes that depend on current
|
|
376
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
377
|
+
if (deps.includes(current) && allNames.has(proc)) {
|
|
378
|
+
inDegree[proc]--;
|
|
379
|
+
if (inDegree[proc] === 0) queue.push(proc);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return order;
|
|
385
|
+
}
|
|
386
|
+
|
|
245
387
|
// =============================================================================
|
|
246
388
|
// DEBUG / INFO
|
|
247
389
|
// =============================================================================
|
package/src/deploy.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { getProcess, getAllProcesses, addHistoryEntry } from './db';
|
|
2
|
+
import { handleRun } from './commands/run';
|
|
3
|
+
import { $ } from 'bun';
|
|
4
|
+
|
|
5
|
+
export interface DeployResult {
|
|
6
|
+
name: string;
|
|
7
|
+
ok: boolean;
|
|
8
|
+
skipped?: boolean;
|
|
9
|
+
reason?: string;
|
|
10
|
+
pullOutput?: string;
|
|
11
|
+
installOutput?: string;
|
|
12
|
+
packageManager?: PackageManager;
|
|
13
|
+
installCommand?: string;
|
|
14
|
+
installAttempted?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm' | null;
|
|
18
|
+
|
|
19
|
+
export function formatDeployToolError(manager: Exclude<PackageManager, null>, error: unknown): string {
|
|
20
|
+
const raw = error instanceof Error ? error.message : String(error ?? 'Unknown error');
|
|
21
|
+
const lower = raw.toLowerCase();
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
lower.includes('command not found') ||
|
|
25
|
+
lower.includes('not recognized as an internal or external command') ||
|
|
26
|
+
lower.includes('executable not found') ||
|
|
27
|
+
lower.includes('no such file or directory')
|
|
28
|
+
) {
|
|
29
|
+
return `Deploy requires '${manager}', but it is not installed or not available on PATH.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `Dependency install failed with ${manager}: ${raw}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isInternalProcess(name: string): boolean {
|
|
36
|
+
return name === 'bgr-dashboard' || name === 'bgr-guard';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
40
|
+
return await Bun.file(path).exists();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function isGitRepo(dir: string): Promise<boolean> {
|
|
44
|
+
return await pathExists(`${dir}/.git`) || await pathExists(`${dir}/.git/HEAD`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function detectPackageManager(dir: string): Promise<PackageManager> {
|
|
48
|
+
const hasPackageJson = await pathExists(`${dir}/package.json`);
|
|
49
|
+
if (!hasPackageJson) return null;
|
|
50
|
+
|
|
51
|
+
if (await pathExists(`${dir}/bun.lock`) || await pathExists(`${dir}/bun.lockb`)) return 'bun';
|
|
52
|
+
if (await pathExists(`${dir}/pnpm-lock.yaml`)) return 'pnpm';
|
|
53
|
+
if (await pathExists(`${dir}/yarn.lock`)) return 'yarn';
|
|
54
|
+
if (await pathExists(`${dir}/package-lock.json`) || await pathExists(`${dir}/npm-shrinkwrap.json`)) return 'npm';
|
|
55
|
+
|
|
56
|
+
return 'bun';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getInstallCommand(manager: Exclude<PackageManager, null>): string {
|
|
60
|
+
switch (manager) {
|
|
61
|
+
case 'bun': return 'bun install';
|
|
62
|
+
case 'pnpm': return 'pnpm install --frozen-lockfile';
|
|
63
|
+
case 'yarn': return 'yarn install --frozen-lockfile';
|
|
64
|
+
case 'npm': return 'npm ci';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function installDependencies(dir: string): Promise<{ manager: PackageManager; output: string; command: string }> {
|
|
69
|
+
const manager = await detectPackageManager(dir);
|
|
70
|
+
if (!manager) return { manager: null, output: '', command: '' };
|
|
71
|
+
|
|
72
|
+
$.cwd(dir);
|
|
73
|
+
const command = getInstallCommand(manager);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
switch (manager) {
|
|
77
|
+
case 'bun':
|
|
78
|
+
return { manager, command, output: (await $`bun install`.text()).trim() };
|
|
79
|
+
case 'pnpm':
|
|
80
|
+
return { manager, command, output: (await $`pnpm install --frozen-lockfile`.text()).trim() };
|
|
81
|
+
case 'yarn':
|
|
82
|
+
return { manager, command, output: (await $`yarn install --frozen-lockfile`.text()).trim() };
|
|
83
|
+
case 'npm':
|
|
84
|
+
return { manager, command, output: (await $`npm ci`.text()).trim() };
|
|
85
|
+
default:
|
|
86
|
+
return { manager: null, output: '', command: '' };
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(formatDeployToolError(manager, error));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function deployProcess(name: string): Promise<DeployResult> {
|
|
94
|
+
const proc = getProcess(name);
|
|
95
|
+
if (!proc) {
|
|
96
|
+
return { name, ok: false, reason: `Process '${name}' not found` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isInternalProcess(proc.name)) {
|
|
100
|
+
return { name, ok: false, skipped: true, reason: 'Internal bgrun process skipped' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const dir = proc.workdir;
|
|
104
|
+
if (!(await isGitRepo(dir))) {
|
|
105
|
+
return { name, ok: false, skipped: true, reason: `'${dir}' is not a git repository` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
$.cwd(dir);
|
|
110
|
+
|
|
111
|
+
const pullOutput = (await $`git pull`.text()).trim();
|
|
112
|
+
|
|
113
|
+
const install = await installDependencies(dir);
|
|
114
|
+
const installOutput = install.output;
|
|
115
|
+
|
|
116
|
+
await handleRun({
|
|
117
|
+
action: 'run',
|
|
118
|
+
name,
|
|
119
|
+
force: true,
|
|
120
|
+
remoteName: '',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
addHistoryEntry(name, 'deploy', proc.pid, {
|
|
124
|
+
directory: dir,
|
|
125
|
+
installed: Boolean(install.manager),
|
|
126
|
+
packageManager: install.manager,
|
|
127
|
+
installCommand: install.command,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
name,
|
|
132
|
+
ok: true,
|
|
133
|
+
pullOutput,
|
|
134
|
+
installOutput,
|
|
135
|
+
packageManager: install.manager,
|
|
136
|
+
installCommand: install.command,
|
|
137
|
+
installAttempted: Boolean(install.manager),
|
|
138
|
+
};
|
|
139
|
+
} catch (e: any) {
|
|
140
|
+
return {
|
|
141
|
+
name,
|
|
142
|
+
ok: false,
|
|
143
|
+
reason: e?.message || String(e),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function deployAllProcesses(group?: string): Promise<DeployResult[]> {
|
|
149
|
+
const processes = getAllProcesses()
|
|
150
|
+
.filter(proc => !isInternalProcess(proc.name))
|
|
151
|
+
.filter(proc => !group || proc.group === group);
|
|
152
|
+
|
|
153
|
+
const seen = new Set<string>();
|
|
154
|
+
const results: DeployResult[] = [];
|
|
155
|
+
|
|
156
|
+
for (const proc of processes) {
|
|
157
|
+
if (seen.has(proc.name)) continue;
|
|
158
|
+
seen.add(proc.name);
|
|
159
|
+
results.push(await deployProcess(proc.name));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return results;
|
|
163
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -574,6 +574,15 @@ async function run() {
|
|
|
574
574
|
return;
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
// Explicit "list" command
|
|
578
|
+
if (name === 'list') {
|
|
579
|
+
await showAll({
|
|
580
|
+
json: values.json as boolean | undefined,
|
|
581
|
+
filter: values.filter as string | undefined
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
577
586
|
// List or Run or Details
|
|
578
587
|
if (name) {
|
|
579
588
|
if (!values.command && !values.directory) {
|
package/src/platform.ts
CHANGED
|
@@ -61,9 +61,19 @@ export async function isProcessRunning(pid: number, command?: string): Promise<b
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
if (isWindows()) {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
64
|
+
// Fast path: signal 0 works for many native Windows/Bun invocations.
|
|
65
|
+
// But under MSYS/Git Bash or detached wrapper scenarios it can return
|
|
66
|
+
// false negatives for live Windows PIDs. Fall back to Get-Process so
|
|
67
|
+
// CLI, dashboard, and guard all agree on process liveness.
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
const output = psExec(
|
|
73
|
+
`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
|
|
74
|
+
).trim();
|
|
75
|
+
return output === String(pid);
|
|
76
|
+
}
|
|
67
77
|
} else {
|
|
68
78
|
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
69
79
|
return result.includes(`${pid}`);
|
|
@@ -632,14 +642,12 @@ export async function getProcessPorts(pid: number): Promise<number[]> {
|
|
|
632
642
|
if (ports.size > 0) return Array.from(ports);
|
|
633
643
|
} catch { /* ss not available, try lsof */ }
|
|
634
644
|
|
|
635
|
-
const result = await $`lsof -
|
|
645
|
+
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
636
646
|
const ports = new Set<number>();
|
|
637
647
|
for (const line of result.split('\n')) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
ports.add(parseInt(portMatch[1]));
|
|
642
|
-
}
|
|
648
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
649
|
+
if (portMatch) {
|
|
650
|
+
ports.add(parseInt(portMatch[1]));
|
|
643
651
|
}
|
|
644
652
|
}
|
|
645
653
|
return Array.from(ports);
|