@owdproject/cli 0.0.3 → 0.1.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/LICENSE +20 -0
- package/README.md +50 -16
- package/bin/bridge.js +127 -0
- package/bin/cli.js +433 -0
- package/bin/cp.js +3523 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +501 -0
- package/bin/lib/config.js +130 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/desktopConfig.js +8 -0
- package/bin/lib/generateTemplate.js +426 -0
- package/bin/lib/install.js +525 -0
- package/bin/lib/installWizardFlow.js +726 -0
- package/bin/lib/migrateModulePlayground.config.mjs +187 -0
- package/bin/lib/migrateModulePlayground.mjs +318 -0
- package/bin/lib/npmVersions.js +124 -0
- package/bin/lib/packageSources.js +365 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +317 -0
- package/bin/lib/validateModule.js +868 -0
- package/bin/lib/workspace.js +222 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +200 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +26 -0
- package/go/go.sum +37 -0
- package/go/main.go +30 -0
- package/go/tui/tui.go +3014 -0
- package/package.json +33 -22
- package/src/commands/create.js +0 -11
- package/src/commands/generate.js +0 -12
- package/src/commands/installApp.js +0 -9
- package/src/commands/installTheme.js +0 -9
- package/src/commands/start.js +0 -13
- package/src/utils/cloneRepo.js +0 -25
- package/src/utils/installPackage.js +0 -15
- package/src/utils/logging.js +0 -15
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const CLIENT_TEMPLATE = resolve(__dirname, '../../../../template')
|
|
17
|
+
const DEFAULT_CLIENT_REPO = 'https://github.com/owdproject/client.git'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} [startDir]
|
|
21
|
+
*/
|
|
22
|
+
export function resolveLocalTemplatePath(startDir = process.cwd()) {
|
|
23
|
+
const fromEnv = process.env.OWD_TEMPLATE_PATH?.trim()
|
|
24
|
+
if (fromEnv && existsSync(join(fromEnv, 'package.json'))) {
|
|
25
|
+
return resolve(fromEnv)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (existsSync(join(CLIENT_TEMPLATE, 'package.json'))) {
|
|
29
|
+
return CLIENT_TEMPLATE
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let dir = startDir
|
|
33
|
+
for (;;) {
|
|
34
|
+
const candidate = join(dir, 'template')
|
|
35
|
+
if (
|
|
36
|
+
existsSync(join(candidate, 'package.json')) &&
|
|
37
|
+
existsSync(join(dir, 'pnpm-workspace.yaml'))
|
|
38
|
+
) {
|
|
39
|
+
return candidate
|
|
40
|
+
}
|
|
41
|
+
const parent = dirname(dir)
|
|
42
|
+
if (parent === dir) break
|
|
43
|
+
dir = parent
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} templatePath
|
|
51
|
+
* @param {string} targetDir
|
|
52
|
+
*/
|
|
53
|
+
export function copyTemplate(templatePath, targetDir) {
|
|
54
|
+
cpSync(templatePath, targetDir, { recursive: true, filter: (src) => !src.includes('node_modules') })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} targetDir
|
|
59
|
+
* @param {{ repo?: string, templatePath?: string | null, cwd?: string }} [options]
|
|
60
|
+
*/
|
|
61
|
+
export async function cloneTemplate(targetDir, options = {}) {
|
|
62
|
+
const setupType = options.setupType ?? 'template'
|
|
63
|
+
|
|
64
|
+
if (setupType === 'clone') {
|
|
65
|
+
const repo = options.repo ?? DEFAULT_CLIENT_REPO
|
|
66
|
+
console.log()
|
|
67
|
+
execSync(`git clone --depth 1 ${JSON.stringify(repo)} ${JSON.stringify(targetDir)}`, {
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
})
|
|
70
|
+
return { source: 'git', path: repo }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const localTemplate = options.templatePath ?? resolveLocalTemplatePath(options.cwd)
|
|
74
|
+
|
|
75
|
+
if (localTemplate) {
|
|
76
|
+
copyTemplate(localTemplate, targetDir)
|
|
77
|
+
return { source: 'local', path: localTemplate }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const repo = options.repo ?? DEFAULT_CLIENT_REPO
|
|
81
|
+
const tempParent = mkdtempSync(join(tmpdir(), 'owd-scaffold-'))
|
|
82
|
+
const tempClone = join(tempParent, 'client')
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
console.log()
|
|
86
|
+
execSync(`git clone --depth 1 ${JSON.stringify(repo)} ${JSON.stringify(tempClone)}`, {
|
|
87
|
+
stdio: 'inherit',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const templatePath = join(tempClone, 'template')
|
|
91
|
+
if (!existsSync(templatePath)) {
|
|
92
|
+
throw new Error('Template directory does not exist in the cloned repository.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
copyTemplate(templatePath, targetDir)
|
|
96
|
+
return { source: 'git', path: repo }
|
|
97
|
+
} finally {
|
|
98
|
+
try {
|
|
99
|
+
rmSync(tempParent, { recursive: true, force: true })
|
|
100
|
+
} catch {
|
|
101
|
+
/* ignore */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} targetDir
|
|
108
|
+
*/
|
|
109
|
+
export function runPostScaffold(targetDir) {
|
|
110
|
+
execSync('pnpm install', { cwd: targetDir, stdio: 'inherit' })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function canLaunchUi() {
|
|
114
|
+
return Boolean(process.stdout.isTTY) && !process.env.CI
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {string} targetDir
|
|
119
|
+
* @param {string} [commandName]
|
|
120
|
+
*/
|
|
121
|
+
export function launchControlPanel(targetDir, commandName = 'desktop') {
|
|
122
|
+
const relDir = basename(targetDir)
|
|
123
|
+
|
|
124
|
+
if (!canLaunchUi()) {
|
|
125
|
+
console.log(`\nNext: cd ${relDir} && pnpm desktop\n`)
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('\nOpening control panel…\n')
|
|
130
|
+
const result = spawnSync('pnpm', ['exec', commandName], {
|
|
131
|
+
cwd: targetDir,
|
|
132
|
+
stdio: 'inherit',
|
|
133
|
+
shell: false,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (result.status !== 0 && result.status !== null) {
|
|
137
|
+
console.log(`\nControl panel exited. Run again with: cd ${relDir} && pnpm desktop\n`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {{ dir?: string, cwd?: string, commandName?: string, skipInstall?: boolean, skipUi?: boolean, repo?: string, setupType?: 'template' | 'clone' }} [options]
|
|
145
|
+
*/
|
|
146
|
+
export async function scaffoldProject(options = {}) {
|
|
147
|
+
const cwd = options.cwd ?? process.cwd()
|
|
148
|
+
const dir = (options.dir ?? 'client').trim()
|
|
149
|
+
const commandName = options.commandName ?? 'desktop'
|
|
150
|
+
const setupType = options.setupType ?? 'template'
|
|
151
|
+
|
|
152
|
+
if (!dir) {
|
|
153
|
+
throw new Error('Project directory name is required.')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const targetDir = resolve(cwd, dir)
|
|
157
|
+
|
|
158
|
+
if (existsSync(targetDir)) {
|
|
159
|
+
if (existsSync(join(targetDir, 'package.json'))) {
|
|
160
|
+
throw new Error(`Directory "${dir}" already exists and contains a package.json.`)
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Directory "${dir}" already exists.`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (setupType !== 'clone') {
|
|
166
|
+
mkdirSync(targetDir, { recursive: true })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cloneResult = await cloneTemplate(targetDir, {
|
|
170
|
+
repo: options.repo,
|
|
171
|
+
cwd,
|
|
172
|
+
setupType,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
if (cloneResult.source === 'local') {
|
|
176
|
+
console.log(`Using local template: ${cloneResult.path}\n`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!options.skipInstall) {
|
|
180
|
+
console.log('Installing dependencies (pnpm install)…\n')
|
|
181
|
+
runPostScaffold(targetDir)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log('\n✓ Project ready\n')
|
|
185
|
+
|
|
186
|
+
if (!options.skipUi) {
|
|
187
|
+
launchControlPanel(targetDir, commandName)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(` cd ${dir}\n`)
|
|
191
|
+
console.log(' pnpm install')
|
|
192
|
+
console.log(' pnpm desktop\n')
|
|
193
|
+
|
|
194
|
+
return { targetDir, dir }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Copy scaffold.js into create-owd for npm publish (run from repo root). */
|
|
198
|
+
export function vendorScaffoldForCreateOwd(createOwdRoot) {
|
|
199
|
+
const src = join(__dirname, 'scaffold.js')
|
|
200
|
+
const destDir = join(createOwdRoot, 'src/lib')
|
|
201
|
+
const dest = join(destDir, 'scaffold.js')
|
|
202
|
+
mkdirSync(destDir, { recursive: true })
|
|
203
|
+
writeFileSync(dest, readFileSync(src, 'utf8'))
|
|
204
|
+
return dest
|
|
205
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { execSync, spawn } from 'node:child_process'
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
unlinkSync,
|
|
8
|
+
appendFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { desktopMetaDir } from './workspace.js'
|
|
12
|
+
import {
|
|
13
|
+
devSpawnCwd,
|
|
14
|
+
devTargetLogLabel,
|
|
15
|
+
normalizeDevTarget,
|
|
16
|
+
} from './playgroundContext.js'
|
|
17
|
+
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function devPidPath(workspaceRoot) {
|
|
23
|
+
return join(desktopMetaDir(workspaceRoot), 'dev.pid')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function devLogPath(workspaceRoot) {
|
|
27
|
+
return join(desktopMetaDir(workspaceRoot), 'dev.log')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readPidFile(workspaceRoot) {
|
|
31
|
+
const path = devPidPath(workspaceRoot)
|
|
32
|
+
if (!existsSync(path)) return null
|
|
33
|
+
const pid = Number.parseInt(readFileSync(path, 'utf8').trim(), 10)
|
|
34
|
+
if (!Number.isFinite(pid)) return null
|
|
35
|
+
try {
|
|
36
|
+
process.kill(pid, 0)
|
|
37
|
+
return pid
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
unlinkSync(path)
|
|
41
|
+
} catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function findDevProcess() {
|
|
49
|
+
const patterns = [
|
|
50
|
+
'nuxt dev',
|
|
51
|
+
'nuxi dev',
|
|
52
|
+
'nx run desktop:serve',
|
|
53
|
+
'node.*\\.nuxt',
|
|
54
|
+
'desktop.*nuxt',
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for (const pattern of patterns) {
|
|
58
|
+
try {
|
|
59
|
+
const out = execSync(`pgrep -af ${JSON.stringify(pattern)}`, {
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
62
|
+
})
|
|
63
|
+
for (const line of out.split('\n').filter(Boolean)) {
|
|
64
|
+
if (line.includes('pgrep')) continue
|
|
65
|
+
const pid = Number.parseInt(line.trim().split(/\s+/)[0], 10)
|
|
66
|
+
if (Number.isFinite(pid)) return { pid, cmdline: line.trim() }
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
/* try next pattern */
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function findProcessOnPort(port) {
|
|
77
|
+
try {
|
|
78
|
+
const out = execSync(`ss -tlnp 2>/dev/null | grep :${port} || lsof -i :${port} -t 2>/dev/null | head -1`, {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
81
|
+
})
|
|
82
|
+
const pidMatch = out.match(/pid=(\d+)/) ?? out.match(/^(\d+)/m)
|
|
83
|
+
if (pidMatch) {
|
|
84
|
+
const pid = Number.parseInt(pidMatch[1], 10)
|
|
85
|
+
if (Number.isFinite(pid)) return pid
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
/* ignore */
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function readProcessStats(pid) {
|
|
94
|
+
if (!pid || !existsSync(`/proc/${pid}/status`)) {
|
|
95
|
+
return { memMb: 0, threads: 0, rssKb: 0 }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const status = readFileSync(`/proc/${pid}/status`, 'utf8')
|
|
100
|
+
const vmRss = status.match(/^VmRSS:\s+(\d+)/m)?.[1]
|
|
101
|
+
const threads = status.match(/^Threads:\s+(\d+)/m)?.[1]
|
|
102
|
+
const rssKb = vmRss ? Number(vmRss) : 0
|
|
103
|
+
return {
|
|
104
|
+
memMb: rssKb ? Math.round(rssKb / 1024) : 0,
|
|
105
|
+
rssKb,
|
|
106
|
+
threads: threads ? Number(threads) : 0,
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
return { memMb: 0, threads: 0, rssKb: 0 }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const SPARK_CHARS = '▁▂▃▄▅▆▇█'
|
|
114
|
+
|
|
115
|
+
/** @param {number[]} samples RSS in MiB */
|
|
116
|
+
export function formatSparkline(samples) {
|
|
117
|
+
if (!samples.length) return '—'
|
|
118
|
+
const min = Math.min(...samples)
|
|
119
|
+
const max = Math.max(...samples)
|
|
120
|
+
const span = max - min || 1
|
|
121
|
+
return samples
|
|
122
|
+
.map((v) => {
|
|
123
|
+
const idx = Math.min(
|
|
124
|
+
SPARK_CHARS.length - 1,
|
|
125
|
+
Math.floor(((v - min) / span) * (SPARK_CHARS.length - 1)),
|
|
126
|
+
)
|
|
127
|
+
return SPARK_CHARS[idx]
|
|
128
|
+
})
|
|
129
|
+
.join('')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @param {number} value @param {number} max @param {number} [width] */
|
|
133
|
+
export function formatBar(value, max, width = 12) {
|
|
134
|
+
if (max <= 0) return '░'.repeat(width)
|
|
135
|
+
const filled = Math.round((value / max) * width)
|
|
136
|
+
return '█'.repeat(Math.min(width, filled)) + '░'.repeat(Math.max(0, width - filled))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function probeDevServer(port) {
|
|
140
|
+
const url = `http://127.0.0.1:${port}/`
|
|
141
|
+
try {
|
|
142
|
+
const controller = new AbortController()
|
|
143
|
+
const timer = setTimeout(() => controller.abort(), 1200)
|
|
144
|
+
const res = await fetch(url, { signal: controller.signal, redirect: 'manual' })
|
|
145
|
+
clearTimeout(timer)
|
|
146
|
+
return { up: true, status: res.status, url }
|
|
147
|
+
} catch {
|
|
148
|
+
return { up: false, status: 0, url }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function getClientStatus(workspaceRoot, port) {
|
|
153
|
+
const http = await probeDevServer(port)
|
|
154
|
+
const fromFile = readPidFile(workspaceRoot)
|
|
155
|
+
const fromPg = findDevProcess()
|
|
156
|
+
const portPid = findProcessOnPort(port)
|
|
157
|
+
|
|
158
|
+
const pid = fromPg?.pid ?? portPid ?? fromFile ?? null
|
|
159
|
+
const stats = pid ? readProcessStats(pid) : { memMb: 0, threads: 0 }
|
|
160
|
+
|
|
161
|
+
const isPidRunning = (p) => {
|
|
162
|
+
if (!p) return false
|
|
163
|
+
try {
|
|
164
|
+
process.kill(p, 0)
|
|
165
|
+
return true
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return err.code === 'EPERM'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const running = http.up ||
|
|
172
|
+
(fromPg && isPidRunning(fromPg.pid)) ||
|
|
173
|
+
(portPid && isPidRunning(portPid)) ||
|
|
174
|
+
(fromFile && isPidRunning(fromFile))
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
running: Boolean(running),
|
|
178
|
+
pid,
|
|
179
|
+
http,
|
|
180
|
+
stats,
|
|
181
|
+
port,
|
|
182
|
+
cmdline: fromPg?.cmdline ?? null,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let devChild = null
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {import('./playgroundContext.js').DevTarget | string} target
|
|
190
|
+
*/
|
|
191
|
+
export function startDev(target) {
|
|
192
|
+
const resolved = normalizeDevTarget(target)
|
|
193
|
+
if (!resolved) {
|
|
194
|
+
throw new Error('startDev: missing workspace root')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const { workspaceRoot } = resolved
|
|
198
|
+
const spawnCwd = devSpawnCwd(resolved)
|
|
199
|
+
|
|
200
|
+
if (devChild?.pid) {
|
|
201
|
+
try {
|
|
202
|
+
process.kill(devChild.pid, 0)
|
|
203
|
+
return devChild
|
|
204
|
+
} catch {
|
|
205
|
+
devChild = null
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
mkdirSync(desktopMetaDir(workspaceRoot), { recursive: true })
|
|
210
|
+
const logPath = devLogPath(workspaceRoot)
|
|
211
|
+
|
|
212
|
+
devChild = spawn('pnpm', ['run', 'dev'], {
|
|
213
|
+
cwd: spawnCwd,
|
|
214
|
+
detached: true,
|
|
215
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const appendLog = (chunk) => {
|
|
219
|
+
try {
|
|
220
|
+
appendFileSync(logPath, chunk)
|
|
221
|
+
} catch {
|
|
222
|
+
/* ignore */
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
appendFileSync(
|
|
227
|
+
logPath,
|
|
228
|
+
`[owd] dev target: ${devTargetLogLabel(resolved)} (cwd ${spawnCwd})\n`,
|
|
229
|
+
)
|
|
230
|
+
} catch {
|
|
231
|
+
/* ignore */
|
|
232
|
+
}
|
|
233
|
+
devChild.stdout?.on('data', appendLog)
|
|
234
|
+
devChild.stderr?.on('data', appendLog)
|
|
235
|
+
|
|
236
|
+
devChild.unref()
|
|
237
|
+
writeFileSync(devPidPath(workspaceRoot), String(devChild.pid))
|
|
238
|
+
return devChild
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function stopDev(workspaceRoot, pid) {
|
|
242
|
+
const fromPg = findDevProcess()
|
|
243
|
+
const targets = new Set([pid, fromPg?.pid, readPidFile(workspaceRoot)].filter(Boolean))
|
|
244
|
+
|
|
245
|
+
for (const p of targets) {
|
|
246
|
+
try {
|
|
247
|
+
process.kill(-p, 'SIGTERM')
|
|
248
|
+
} catch {
|
|
249
|
+
try {
|
|
250
|
+
process.kill(p, 'SIGTERM')
|
|
251
|
+
} catch {
|
|
252
|
+
/* ignore */
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
unlinkSync(devPidPath(workspaceRoot))
|
|
259
|
+
} catch {
|
|
260
|
+
/* ignore */
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
devChild = null
|
|
264
|
+
return targets.size > 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function waitForDevStop(workspaceRoot, port, timeoutMs = 10_000) {
|
|
268
|
+
const started = Date.now()
|
|
269
|
+
while (Date.now() - started < timeoutMs) {
|
|
270
|
+
const status = await getClientStatus(workspaceRoot, port)
|
|
271
|
+
if (!status.running && !status.http.up) return status
|
|
272
|
+
await sleep(500)
|
|
273
|
+
}
|
|
274
|
+
return getClientStatus(workspaceRoot, port)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function waitForDev(workspaceRoot, port, timeoutMs = 90_000) {
|
|
278
|
+
const started = Date.now()
|
|
279
|
+
while (Date.now() - started < timeoutMs) {
|
|
280
|
+
const status = await getClientStatus(workspaceRoot, port)
|
|
281
|
+
if (status.http.up) return status
|
|
282
|
+
if (Date.now() - started > 5000 && !status.running) {
|
|
283
|
+
return status
|
|
284
|
+
}
|
|
285
|
+
await sleep(1500)
|
|
286
|
+
}
|
|
287
|
+
return getClientStatus(workspaceRoot, port)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function runScript(workspaceRoot, script) {
|
|
291
|
+
return spawn('pnpm', ['run', script], {
|
|
292
|
+
cwd: workspaceRoot,
|
|
293
|
+
stdio: 'inherit',
|
|
294
|
+
shell: false,
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Run dev in the foreground (`desktop dev`). */
|
|
299
|
+
export function runDevForeground(target) {
|
|
300
|
+
const resolved = normalizeDevTarget(target)
|
|
301
|
+
if (!resolved) {
|
|
302
|
+
console.error('Not inside an OWD workspace.')
|
|
303
|
+
process.exit(1)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const spawnCwd = devSpawnCwd(resolved)
|
|
307
|
+
if (resolved.mode === 'playground' && resolved.packageName) {
|
|
308
|
+
console.log(`Starting playground: ${resolved.packageName}`)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const child = spawn('pnpm', ['run', 'dev'], {
|
|
312
|
+
cwd: spawnCwd,
|
|
313
|
+
stdio: 'inherit',
|
|
314
|
+
shell: false,
|
|
315
|
+
})
|
|
316
|
+
child.on('exit', (code) => process.exit(code ?? 0))
|
|
317
|
+
}
|