@owdproject/cli 0.0.3 → 0.2.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.
Files changed (56) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +50 -16
  3. package/bin/bridge.js +129 -0
  4. package/bin/cli.js +491 -0
  5. package/bin/cp.js +3542 -0
  6. package/bin/desktop.js +5 -0
  7. package/bin/lib/catalog.js +504 -0
  8. package/bin/lib/config.js +173 -0
  9. package/bin/lib/cpAscii.js +39 -0
  10. package/bin/lib/cpFormat.js +343 -0
  11. package/bin/lib/create.js +246 -0
  12. package/bin/lib/desktopConfig.js +48 -0
  13. package/bin/lib/generateTemplate.js +427 -0
  14. package/bin/lib/install.js +521 -0
  15. package/bin/lib/installWizardFlow.js +792 -0
  16. package/bin/lib/migrateModulePlayground.config.mjs +187 -0
  17. package/bin/lib/migrateModulePlayground.mjs +318 -0
  18. package/bin/lib/npmVersions.js +124 -0
  19. package/bin/lib/packageSources.js +375 -0
  20. package/bin/lib/playgroundContext.js +188 -0
  21. package/bin/lib/scaffold.js +205 -0
  22. package/bin/lib/status.js +319 -0
  23. package/bin/lib/validateModule.js +978 -0
  24. package/bin/lib/workspace.js +289 -0
  25. package/bin/owd.js +2 -118
  26. package/go/bridge/bridge.go +220 -0
  27. package/go/desktop-go +0 -0
  28. package/go/go.mod +33 -0
  29. package/go/go.sum +52 -0
  30. package/go/main.go +34 -0
  31. package/go/owd-cli +0 -0
  32. package/go/tui/engine.go +472 -0
  33. package/go/tui/engine_test.go +40 -0
  34. package/go/tui/handlers.go +604 -0
  35. package/go/tui/helpers.go +916 -0
  36. package/go/tui/helpers_test.go +97 -0
  37. package/go/tui/modals.go +423 -0
  38. package/go/tui/resolver.go +377 -0
  39. package/go/tui/resolver_test.go +86 -0
  40. package/go/tui/styles.go +153 -0
  41. package/go/tui/tasks.go +850 -0
  42. package/go/tui/tui.go +869 -0
  43. package/go/tui/tui_test.go +69 -0
  44. package/go/tui/types.go +230 -0
  45. package/go/tui/view.go +812 -0
  46. package/go/tui/workqueue.go +179 -0
  47. package/go/tui/workqueue_test.go +90 -0
  48. package/package.json +33 -22
  49. package/src/commands/create.js +0 -11
  50. package/src/commands/generate.js +0 -12
  51. package/src/commands/installApp.js +0 -9
  52. package/src/commands/installTheme.js +0 -9
  53. package/src/commands/start.js +0 -13
  54. package/src/utils/cloneRepo.js +0 -25
  55. package/src/utils/installPackage.js +0 -15
  56. 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,319 @@
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
+ 'nuxt\\.mjs dev',
53
+ 'nuxi\\.mjs dev',
54
+ 'nx run desktop:serve',
55
+ 'node.*\\.nuxt',
56
+ 'desktop.*nuxt',
57
+ ]
58
+
59
+ for (const pattern of patterns) {
60
+ try {
61
+ const out = execSync(`pgrep -af ${JSON.stringify(pattern)}`, {
62
+ encoding: 'utf8',
63
+ stdio: ['ignore', 'pipe', 'ignore'],
64
+ })
65
+ for (const line of out.split('\n').filter(Boolean)) {
66
+ if (line.includes('pgrep')) continue
67
+ const pid = Number.parseInt(line.trim().split(/\s+/)[0], 10)
68
+ if (Number.isFinite(pid)) return { pid, cmdline: line.trim() }
69
+ }
70
+ } catch {
71
+ /* try next pattern */
72
+ }
73
+ }
74
+
75
+ return null
76
+ }
77
+
78
+ export function findProcessOnPort(port) {
79
+ try {
80
+ const out = execSync(`ss -tlnp 2>/dev/null | grep :${port} || lsof -i :${port} -t 2>/dev/null | head -1`, {
81
+ encoding: 'utf8',
82
+ stdio: ['ignore', 'pipe', 'ignore'],
83
+ })
84
+ const pidMatch = out.match(/pid=(\d+)/) ?? out.match(/^(\d+)/m)
85
+ if (pidMatch) {
86
+ const pid = Number.parseInt(pidMatch[1], 10)
87
+ if (Number.isFinite(pid)) return pid
88
+ }
89
+ } catch {
90
+ /* ignore */
91
+ }
92
+ return null
93
+ }
94
+
95
+ export function readProcessStats(pid) {
96
+ if (!pid || !existsSync(`/proc/${pid}/status`)) {
97
+ return { memMb: 0, threads: 0, rssKb: 0 }
98
+ }
99
+
100
+ try {
101
+ const status = readFileSync(`/proc/${pid}/status`, 'utf8')
102
+ const vmRss = status.match(/^VmRSS:\s+(\d+)/m)?.[1]
103
+ const threads = status.match(/^Threads:\s+(\d+)/m)?.[1]
104
+ const rssKb = vmRss ? Number(vmRss) : 0
105
+ return {
106
+ memMb: rssKb ? Math.round(rssKb / 1024) : 0,
107
+ rssKb,
108
+ threads: threads ? Number(threads) : 0,
109
+ }
110
+ } catch {
111
+ return { memMb: 0, threads: 0, rssKb: 0 }
112
+ }
113
+ }
114
+
115
+ const SPARK_CHARS = '▁▂▃▄▅▆▇█'
116
+
117
+ /** @param {number[]} samples RSS in MiB */
118
+ export function formatSparkline(samples) {
119
+ if (!samples.length) return '—'
120
+ const min = Math.min(...samples)
121
+ const max = Math.max(...samples)
122
+ const span = max - min || 1
123
+ return samples
124
+ .map((v) => {
125
+ const idx = Math.min(
126
+ SPARK_CHARS.length - 1,
127
+ Math.floor(((v - min) / span) * (SPARK_CHARS.length - 1)),
128
+ )
129
+ return SPARK_CHARS[idx]
130
+ })
131
+ .join('')
132
+ }
133
+
134
+ /** @param {number} value @param {number} max @param {number} [width] */
135
+ export function formatBar(value, max, width = 12) {
136
+ if (max <= 0) return '░'.repeat(width)
137
+ const filled = Math.round((value / max) * width)
138
+ return '█'.repeat(Math.min(width, filled)) + '░'.repeat(Math.max(0, width - filled))
139
+ }
140
+
141
+ export async function probeDevServer(port) {
142
+ const url = `http://127.0.0.1:${port}/`
143
+ try {
144
+ const controller = new AbortController()
145
+ const timer = setTimeout(() => controller.abort(), 1200)
146
+ const res = await fetch(url, { signal: controller.signal, redirect: 'manual' })
147
+ clearTimeout(timer)
148
+ return { up: true, status: res.status, url }
149
+ } catch {
150
+ return { up: false, status: 0, url }
151
+ }
152
+ }
153
+
154
+ export async function getClientStatus(workspaceRoot, port) {
155
+ const http = await probeDevServer(port)
156
+ const fromFile = readPidFile(workspaceRoot)
157
+ const fromPg = findDevProcess()
158
+ const portPid = findProcessOnPort(port)
159
+
160
+ const pid = fromPg?.pid ?? portPid ?? fromFile ?? null
161
+ const stats = pid ? readProcessStats(pid) : { memMb: 0, threads: 0 }
162
+
163
+ const isPidRunning = (p) => {
164
+ if (!p) return false
165
+ try {
166
+ process.kill(p, 0)
167
+ return true
168
+ } catch (err) {
169
+ return err.code === 'EPERM'
170
+ }
171
+ }
172
+
173
+ const running = http.up ||
174
+ (fromPg && isPidRunning(fromPg.pid)) ||
175
+ (portPid && isPidRunning(portPid)) ||
176
+ (fromFile && isPidRunning(fromFile))
177
+
178
+ return {
179
+ running: Boolean(running),
180
+ pid,
181
+ http,
182
+ stats,
183
+ port,
184
+ cmdline: fromPg?.cmdline ?? null,
185
+ }
186
+ }
187
+
188
+ let devChild = null
189
+
190
+ /**
191
+ * @param {import('./playgroundContext.js').DevTarget | string} target
192
+ */
193
+ export function startDev(target) {
194
+ const resolved = normalizeDevTarget(target)
195
+ if (!resolved) {
196
+ throw new Error('startDev: missing workspace root')
197
+ }
198
+
199
+ const { workspaceRoot } = resolved
200
+ const spawnCwd = devSpawnCwd(resolved)
201
+
202
+ if (devChild?.pid) {
203
+ try {
204
+ process.kill(devChild.pid, 0)
205
+ return devChild
206
+ } catch {
207
+ devChild = null
208
+ }
209
+ }
210
+
211
+ mkdirSync(desktopMetaDir(workspaceRoot), { recursive: true })
212
+ const logPath = devLogPath(workspaceRoot)
213
+
214
+ devChild = spawn('pnpm', ['run', 'dev'], {
215
+ cwd: spawnCwd,
216
+ detached: true,
217
+ stdio: ['ignore', 'pipe', 'pipe'],
218
+ })
219
+
220
+ const appendLog = (chunk) => {
221
+ try {
222
+ appendFileSync(logPath, chunk)
223
+ } catch {
224
+ /* ignore */
225
+ }
226
+ }
227
+ try {
228
+ appendFileSync(
229
+ logPath,
230
+ `[owd] dev target: ${devTargetLogLabel(resolved)} (cwd ${spawnCwd})\n`,
231
+ )
232
+ } catch {
233
+ /* ignore */
234
+ }
235
+ devChild.stdout?.on('data', appendLog)
236
+ devChild.stderr?.on('data', appendLog)
237
+
238
+ devChild.unref()
239
+ writeFileSync(devPidPath(workspaceRoot), String(devChild.pid))
240
+ return devChild
241
+ }
242
+
243
+ export function stopDev(workspaceRoot, pid) {
244
+ const fromPg = findDevProcess()
245
+ const targets = new Set([pid, fromPg?.pid, readPidFile(workspaceRoot)].filter(Boolean))
246
+
247
+ for (const p of targets) {
248
+ try {
249
+ process.kill(-p, 'SIGTERM')
250
+ } catch {
251
+ try {
252
+ process.kill(p, 'SIGTERM')
253
+ } catch {
254
+ /* ignore */
255
+ }
256
+ }
257
+ }
258
+
259
+ try {
260
+ unlinkSync(devPidPath(workspaceRoot))
261
+ } catch {
262
+ /* ignore */
263
+ }
264
+
265
+ devChild = null
266
+ return targets.size > 0
267
+ }
268
+
269
+ export async function waitForDevStop(workspaceRoot, port, timeoutMs = 10_000) {
270
+ const started = Date.now()
271
+ while (Date.now() - started < timeoutMs) {
272
+ const status = await getClientStatus(workspaceRoot, port)
273
+ if (!status.running && !status.http.up) return status
274
+ await sleep(500)
275
+ }
276
+ return getClientStatus(workspaceRoot, port)
277
+ }
278
+
279
+ export async function waitForDev(workspaceRoot, port, timeoutMs = 90_000) {
280
+ const started = Date.now()
281
+ while (Date.now() - started < timeoutMs) {
282
+ const status = await getClientStatus(workspaceRoot, port)
283
+ if (status.http.up) return status
284
+ if (Date.now() - started > 5000 && !status.running) {
285
+ return status
286
+ }
287
+ await sleep(1500)
288
+ }
289
+ return getClientStatus(workspaceRoot, port)
290
+ }
291
+
292
+ export function runScript(workspaceRoot, script) {
293
+ return spawn('pnpm', ['run', script], {
294
+ cwd: workspaceRoot,
295
+ stdio: 'inherit',
296
+ shell: false,
297
+ })
298
+ }
299
+
300
+ /** Run dev in the foreground (`desktop dev`). */
301
+ export function runDevForeground(target) {
302
+ const resolved = normalizeDevTarget(target)
303
+ if (!resolved) {
304
+ console.error('Not inside an OWD workspace.')
305
+ process.exit(1)
306
+ }
307
+
308
+ const spawnCwd = devSpawnCwd(resolved)
309
+ if (resolved.mode === 'playground' && resolved.packageName) {
310
+ console.log(`Starting playground: ${resolved.packageName}`)
311
+ }
312
+
313
+ const child = spawn('pnpm', ['run', 'dev'], {
314
+ cwd: spawnCwd,
315
+ stdio: 'inherit',
316
+ shell: false,
317
+ })
318
+ child.on('exit', (code) => process.exit(code ?? 0))
319
+ }