@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
package/bin/cli.js ADDED
@@ -0,0 +1,491 @@
1
+ import { spawn, execSync } from 'node:child_process'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { runDevForeground } from './lib/status.js'
4
+ import { existsSync } from 'node:fs'
5
+ import { join } from 'node:path'
6
+ import getopts from 'getopts'
7
+ import {
8
+ resolveSource,
9
+ printPlan,
10
+ printNextSteps,
11
+ runWorkspaceInstall,
12
+ runNpmInstall,
13
+ } from './lib/install.js'
14
+ import { findWorkspaceRoot, inferKind } from './lib/workspace.js'
15
+ import { resolveDevTarget } from './lib/playgroundContext.js'
16
+ import { scaffoldProject } from './lib/scaffold.js'
17
+
18
+ const SCOPE = '@owdproject/'
19
+
20
+ /** @type {Record<string, { configKey: string, workspaceDir: string, nx: string, label: string }>} */
21
+ const KINDS = {
22
+ app: {
23
+ label: 'app',
24
+ configKey: 'apps',
25
+ workspaceDir: 'apps',
26
+ nx: 'desktop:install-app',
27
+ },
28
+ module: {
29
+ label: 'module',
30
+ configKey: 'modules',
31
+ workspaceDir: 'packages',
32
+ nx: 'desktop:install-module',
33
+ },
34
+ theme: {
35
+ label: 'theme',
36
+ configKey: 'theme',
37
+ workspaceDir: 'themes',
38
+ nx: 'desktop:install-theme',
39
+ },
40
+ }
41
+
42
+ const LEGACY_COMMANDS = {
43
+ 'install-app': 'app',
44
+ 'install-module': 'module',
45
+ 'install-theme': 'theme',
46
+ }
47
+
48
+ /** @type {string} */
49
+ let commandName = 'desktop'
50
+
51
+ function buildHelp(name) {
52
+ return `
53
+ ${name} — add apps, modules, and themes to your Open Web Desktop
54
+
55
+ USAGE
56
+ ${name} Open the control panel (interactive TUI)
57
+ ${name} dev [--playground] Start dev server (auto-detects module playground)
58
+
59
+ CONTROL PANEL (TUI)
60
+ m Open command menu (all actions)
61
+ s Start the Nuxt dev server
62
+ x Stop the Nuxt dev server
63
+ R Reboot the Nuxt dev server (stop + start)
64
+ w Save catalog/theme changes to desktop.config.ts
65
+ 1 / 2 / 3 Apps / Modules / Themes catalog tabs
66
+ o / O Cycle sort (o) or open sort picker (O)
67
+ i Open in-app docs (when module-docs is installed)
68
+ g Settings (GitHub user, SSH, trusted orgs)
69
+ r Refresh package list from GitHub (detects new modules)
70
+ b Run pnpm run generate
71
+ n Create a new app or module (interactive wizard)
72
+ q / Esc Quit
73
+ ${name} init [dir] Create a new OWD project (then opens the control panel)
74
+ ${name} create Create a new app or module (interactive wizard)
75
+ ${name} create <kind> <name> Scaffold a new app or module (e.g. desktop create app todo)
76
+ ${name} add <package> [options]
77
+ ${name} add <kind> <name> [options]
78
+ ${name} validate [path...] Check Nuxt module + playground layout
79
+ ${name} template [--dry-run] [--check] Regenerate client/template/ (maintainers)
80
+
81
+ TEMPLATE (monorepo maintainers)
82
+ ${name} template Write template/ from blueprint + desktop/ + latest npm versions
83
+ ${name} template --dry-run Show planned @owdproject/* and starter versions
84
+ ${name} template --check Fail if committed template/ differs (CI)
85
+
86
+ VALIDATE
87
+ ${name} validate Validate cwd package, or all apps/themes/modules at repo root
88
+ ${name} validate . Validate explicit package directory
89
+ ${name} validate apps Validate every @owdproject/* module under apps/
90
+ --json Machine-readable output
91
+ --strict Treat warnings as failures
92
+ --smoke Run dev:prepare + nuxt build playground (slow, CI)
93
+
94
+ INSTALL SOURCES (control panel or CLI)
95
+ npm registry (default for desktop add)
96
+ --dev clone github.com/owdproject/<package>
97
+ --from <user> clone from GitHub user or full git URL (SSH supported)
98
+
99
+ EXAMPLES
100
+ pnpm ${name} # control panel
101
+ pnpm ${name} dev # monorepo desktop (from repo root)
102
+ cd apps/app-about && pnpm ${name} dev # app-about playground
103
+ pnpm ${name} dev --playground # force playground when cwd is in a module
104
+ ${name} init my-desktop # scaffold + pnpm install + control panel
105
+ ${name} add app-todo --npm
106
+ ${name} add app-todo --dev
107
+ ${name} add module-persistence --from dxlliv
108
+
109
+ KINDS (optional — inferred from the package name)
110
+ app app-* → apps/
111
+ module module-* → packages/ (kit-* and *-template are not listed in the control panel)
112
+ theme theme-* → themes/
113
+
114
+ OPTIONS
115
+ --playground Start the module playground dev server (when cwd is inside an @owdproject/* package with playground/)
116
+ --from <source> Git source (user, user/repo, or URL)
117
+ --branch <name> Git branch to clone
118
+ --npm Install from npm (default when --from is omitted)
119
+ --dev, --workspace Clone from github.com/owdproject/<package>
120
+ --protocol https|ssh With --from <user>, use HTTPS (default) or SSH clone URL
121
+ --dry-run Print the plan without changing anything
122
+ -h, --help Show this help
123
+
124
+ --from <source>
125
+ (omit) npm registry (default)
126
+ npm npm registry
127
+ owdproject Clone official repo (monorepo only)
128
+ <github-user> Clone github.com/<user>/<package> (your fork)
129
+ <user>/<repo> Explicit GitHub repo
130
+ <git-url> Full clone URL
131
+
132
+ LEGACY (still supported)
133
+ ${name} install-app @owdproject/app-todo
134
+ → prefer: ${name} add app-todo
135
+ `
136
+ }
137
+
138
+ async function openControlPanel(name) {
139
+ const cliDir = join(fileURLToPath(import.meta.url), '..')
140
+ const goDir = join(cliDir, '../go')
141
+ const binaryPath = join(goDir, 'desktop-go')
142
+ const readline = await import('node:readline/promises')
143
+
144
+ // Compile the Go Control Panel if binary doesn't exist
145
+ if (!existsSync(binaryPath)) {
146
+ console.log('Go Control Panel binary not found. Compiling...')
147
+ try {
148
+ execSync('go build -o desktop-go .', { cwd: goDir, stdio: 'inherit' })
149
+ console.log('Go Control Panel compiled successfully!')
150
+ } catch (err) {
151
+ console.warn('Failed to compile Go Control Panel, falling back to legacy CP:', err.message)
152
+ const child = spawn(process.execPath, [
153
+ '-e',
154
+ `import('./cp.js').then(m => m.runCp(${JSON.stringify(name)}))`
155
+ ], {
156
+ cwd: cliDir,
157
+ stdio: 'inherit',
158
+ })
159
+ child.on('exit', async (code) => {
160
+ if (code === 10) {
161
+ const { runCreateCli } = await import('./lib/create.js')
162
+ const workspaceRoot = findWorkspaceRoot()
163
+ try {
164
+ await runCreateCli({ workspaceRoot })
165
+ } catch (err) {
166
+ console.error('\n✗ Error creating package:', err.message)
167
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
168
+ await rl.question('\nPress Enter to return to the control panel...')
169
+ rl.close()
170
+ }
171
+ await openControlPanel(name)
172
+ } else {
173
+ process.exit(code ?? 0)
174
+ }
175
+ })
176
+ return
177
+ }
178
+ }
179
+
180
+ // Execute Go TUI
181
+ const child = spawn(binaryPath, [], { stdio: 'inherit' })
182
+ child.on('exit', async (code) => {
183
+ if (code === 10) {
184
+ const { runCreateCli } = await import('./lib/create.js')
185
+ const workspaceRoot = findWorkspaceRoot()
186
+ try {
187
+ await runCreateCli({ workspaceRoot })
188
+ } catch (err) {
189
+ console.error('\n✗ Error creating package:', err.message)
190
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
191
+ await rl.question('\nPress Enter to return to the control panel...')
192
+ rl.close()
193
+ }
194
+ await openControlPanel(name)
195
+ } else {
196
+ process.exit(code ?? 0)
197
+ }
198
+ })
199
+ }
200
+
201
+ function fail(message, hint) {
202
+ console.error(`\n✗ ${message}`)
203
+ if (hint) console.error(` ${hint}`)
204
+ console.error(`\nRun \`${commandName} --help\` for usage.\n`)
205
+ process.exit(1)
206
+ }
207
+
208
+ function warn(message) {
209
+ console.warn(`\n⚠ ${message}\n`)
210
+ }
211
+
212
+ /**
213
+ * @param {string} input
214
+ * @param {string | undefined} explicitKind
215
+ */
216
+ function normalizePackage(input, explicitKind) {
217
+ let raw = input.trim()
218
+ if (raw.startsWith(SCOPE)) raw = raw.slice(SCOPE.length)
219
+
220
+ if (!raw) {
221
+ fail('Package name is required.', `Example: ${commandName} add app-todo`)
222
+ }
223
+
224
+ const kind = explicitKind ?? inferKind(raw)
225
+
226
+ if (explicitKind) {
227
+ const prefixes = {
228
+ app: 'app-',
229
+ theme: 'theme-',
230
+ module: ['module-', 'kit-'],
231
+ }
232
+ const expected = prefixes[explicitKind]
233
+ const list = Array.isArray(expected) ? expected : [expected]
234
+ if (!list.some((p) => raw.startsWith(p))) {
235
+ raw = `${list[0]}${raw}`
236
+ }
237
+ }
238
+
239
+ return { pkgName: `${SCOPE}${raw}`, shortName: raw, kind }
240
+ }
241
+
242
+ function mapLegacyFlags(parsed) {
243
+ const { dev, workspace, fork, repo, branch, from, npm } = parsed
244
+ let mappedFrom = from
245
+ let mappedNpm = npm
246
+ const forceDev = dev || workspace
247
+
248
+ if (fork || repo) {
249
+ warn('`--fork` and `--repo` are deprecated — use `--from` instead.')
250
+ mappedFrom = repo || fork
251
+ }
252
+ if (forceDev && !mappedFrom && !mappedNpm) {
253
+ mappedFrom = 'owdproject'
254
+ }
255
+ if (mappedNpm) mappedFrom = 'npm'
256
+
257
+ return { from: mappedFrom, branch, npm: mappedNpm, dev: forceDev }
258
+ }
259
+
260
+ function parseAddArgs(positionals) {
261
+ const kindNames = Object.keys(KINDS)
262
+ if (positionals.length === 0) {
263
+ fail('Missing package name.', `Example: ${commandName} add app-todo`)
264
+ }
265
+
266
+ if (kindNames.includes(positionals[0])) {
267
+ const kind = positionals[0]
268
+ const name = positionals[1]
269
+ if (!name) {
270
+ fail(`Missing name for kind "${kind}".`, `Example: ${commandName} add ${kind} todo`)
271
+ }
272
+ return normalizePackage(name, kind)
273
+ }
274
+
275
+ return normalizePackage(positionals[0])
276
+ }
277
+
278
+ function parseLegacyInstall(cmd, positionals) {
279
+ const kind = LEGACY_COMMANDS[cmd]
280
+ const raw = positionals[0]
281
+ if (!raw) {
282
+ fail('Missing package name.', `Example: ${commandName} ${cmd} ${SCOPE}app-todo`)
283
+ }
284
+ warn(`\`${cmd}\` is deprecated — use \`${commandName} add …\` instead.`)
285
+ return normalizePackage(raw, kind)
286
+ }
287
+
288
+ /**
289
+ * @param {string} name CLI binary name (`desktop` or deprecated `owd`)
290
+ * @param {string[]} argv Arguments (without node and script path)
291
+ * @param {{ deprecated?: boolean }} [options]
292
+ */
293
+ export async function runCli(name, argv, options = {}) {
294
+ commandName = name
295
+
296
+ if (options.deprecated) {
297
+ warn('The `owd` command is deprecated. Use `desktop` instead.')
298
+ }
299
+
300
+ const parsed = getopts(argv, {
301
+ alias: { h: 'help', b: 'branch' },
302
+ boolean: ['help', 'npm', 'dry-run', 'dev', 'workspace', 'playground', 'json', 'strict', 'smoke', 'check'],
303
+ string: ['from', 'branch', 'fork', 'repo', 'protocol'],
304
+ })
305
+
306
+ const { _, help, npm: npmFlag } = parsed
307
+ const dryRun = parsed['dry-run'] === true
308
+ const { from, branch, npm: npmFromLegacy, dev: devFlag } = mapLegacyFlags(parsed)
309
+ const protocol = parsed.protocol === 'ssh' ? 'ssh' : 'https'
310
+ const useNpm = npmFlag || npmFromLegacy || (!from && !devFlag)
311
+
312
+ if (help || _[0] === 'help') {
313
+ console.log(buildHelp(name))
314
+ process.exit(0)
315
+ }
316
+
317
+ const cmd = _[0]
318
+
319
+ if (cmd === 'init') {
320
+ const projectDir = (_[1] || 'owd-client').trim()
321
+ const cwd = process.cwd()
322
+
323
+ if (findWorkspaceRoot(cwd)) {
324
+ fail(
325
+ 'Already inside an OWD workspace.',
326
+ `Use \`${name}\` to manage this project, or run init from a parent directory.`,
327
+ )
328
+ }
329
+
330
+ try {
331
+ await scaffoldProject({ dir: projectDir, cwd, commandName: name })
332
+ } catch (error) {
333
+ fail(error.message ?? String(error))
334
+ }
335
+ return
336
+ }
337
+
338
+ const workspaceRoot = findWorkspaceRoot()
339
+
340
+ if (cmd === 'dev') {
341
+ if (!workspaceRoot) {
342
+ fail(
343
+ 'Not inside an OWD workspace.',
344
+ 'Run from the client repo, or try: desktop · desktop add <package>',
345
+ )
346
+ }
347
+ const forcePlayground = parsed.playground === true
348
+ const devTarget = resolveDevTarget(process.cwd(), workspaceRoot, { forcePlayground })
349
+ if (forcePlayground && !devTarget) {
350
+ fail(
351
+ 'No @owdproject module with playground/desktop.config.ts found from this directory.',
352
+ 'Run from an app/theme/module package (e.g. apps/app-about), or omit --playground for the monorepo desktop.',
353
+ )
354
+ }
355
+ runDevForeground(devTarget)
356
+ return
357
+ }
358
+
359
+ if (cmd === 'validate') {
360
+ const { runValidateCli, formatValidationReport } = await import('./lib/validateModule.js')
361
+ const strict = parsed.strict === true
362
+ const json = parsed.json === true
363
+ const smoke = parsed.smoke === true
364
+ const paths = _.slice(1).map(String)
365
+ const { exitCode, results } = runValidateCli(paths, {
366
+ workspaceRoot,
367
+ json,
368
+ strict,
369
+ smoke,
370
+ })
371
+ for (const result of results) {
372
+ console.log(formatValidationReport(result, { json }))
373
+ if (!json) console.log('')
374
+ }
375
+ process.exit(exitCode)
376
+ }
377
+
378
+ if (cmd === 'template') {
379
+ if (!workspaceRoot) {
380
+ fail(
381
+ 'Not inside an OWD workspace.',
382
+ `Run from the client monorepo root to regenerate template/.`,
383
+ )
384
+ }
385
+ const { runGenerateTemplateCli } = await import('./lib/generateTemplate.js')
386
+ await runGenerateTemplateCli({
387
+ workspaceRoot,
388
+ dryRun: parsed['dry-run'] === true,
389
+ check: parsed.check === true,
390
+ })
391
+ return
392
+ }
393
+
394
+ if (cmd === 'create' || cmd === 'new') {
395
+ if (!workspaceRoot) {
396
+ fail(
397
+ 'Not inside an OWD workspace.',
398
+ `Run from inside an OWD workspace directory to scaffold a new package.`,
399
+ )
400
+ }
401
+ const { runCreateCli } = await import('./lib/create.js')
402
+ const kind = _[1]
403
+ const name = _[2]
404
+ await runCreateCli({ kind, name, workspaceRoot })
405
+ return
406
+ }
407
+
408
+ if (!cmd) {
409
+ if (!workspaceRoot) {
410
+ fail(
411
+ 'Not inside an OWD workspace.',
412
+ 'Run from the client repo, or try: desktop · desktop add <package>',
413
+ )
414
+ }
415
+ await openControlPanel(name)
416
+ return
417
+ }
418
+
419
+ if (cmd === 'ui') {
420
+ fail('The `ui` subcommand was removed.', `Use \`${name}\` to open the control panel.`)
421
+ }
422
+
423
+ if (!workspaceRoot) {
424
+ fail(
425
+ 'Not inside an OWD workspace.',
426
+ 'Run from the client repo (needs pnpm-workspace.yaml + nx.json), or use a generated project with the same layout.',
427
+ )
428
+ }
429
+
430
+ let pkgInfo
431
+
432
+ if (cmd === 'add') {
433
+ pkgInfo = parseAddArgs(_.slice(1))
434
+ } else if (LEGACY_COMMANDS[cmd]) {
435
+ pkgInfo = parseLegacyInstall(cmd, _.slice(1))
436
+ } else {
437
+ fail(`Unknown command: ${cmd}`, `Use \`${name}\`, \`${name} dev\`, or \`${name} add <package>\`.`)
438
+ }
439
+
440
+ const { pkgName, shortName, kind } = pkgInfo
441
+ const inMonorepo = Boolean(workspaceRoot)
442
+ let resolvedFrom = useNpm ? 'npm' : from
443
+ if (
444
+ !useNpm &&
445
+ resolvedFrom &&
446
+ resolvedFrom !== 'npm' &&
447
+ resolvedFrom !== 'owdproject' &&
448
+ resolvedFrom !== 'official' &&
449
+ protocol === 'ssh' &&
450
+ !/[@/]/.test(resolvedFrom)
451
+ ) {
452
+ resolvedFrom = `git@github.com:${resolvedFrom}/${shortName}.git`
453
+ }
454
+ const source = resolveSource(resolvedFrom, shortName, inMonorepo, {
455
+ npm: useNpm,
456
+ dev: devFlag,
457
+ })
458
+
459
+ if (source.mode === 'npm') {
460
+ if (dryRun) {
461
+ printPlan({
462
+ pkgName,
463
+ kind,
464
+ targetDir: '(npm → node_modules)',
465
+ source,
466
+ branch: null,
467
+ dryRun: true,
468
+ })
469
+ process.exit(0)
470
+ }
471
+ try {
472
+ await runNpmInstall(kind, pkgName, workspaceRoot)
473
+ } catch (error) {
474
+ fail(error.message ?? String(error))
475
+ }
476
+ } else {
477
+ try {
478
+ await runWorkspaceInstall({
479
+ pkgName,
480
+ shortName,
481
+ kind,
482
+ workspaceRoot,
483
+ source,
484
+ branch,
485
+ dryRun,
486
+ })
487
+ } catch (error) {
488
+ fail(error.message ?? String(error))
489
+ }
490
+ }
491
+ }