@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,427 @@
1
+ import { execSync } from 'node:child_process'
2
+ import {
3
+ cpSync,
4
+ existsSync,
5
+ mkdtempSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { basename, dirname, join, relative, resolve } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import {
17
+ fetchLatestVersions,
18
+ isOwdScopedPackage,
19
+ resolveLatestVersions,
20
+ } from './npmVersions.js'
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url))
23
+ const BLUEPRINT_ROOT = resolve(__dirname, '../../../core/template-blueprint')
24
+
25
+ export const STARTER_DESKTOP_DEPS = [
26
+ '@owdproject/core',
27
+ '@owdproject/theme-nova',
28
+ '@owdproject/app-about',
29
+ 'h3',
30
+ ]
31
+
32
+ /** Used when a starter package is not on npm yet (before first publish). */
33
+ export const TEMPLATE_NPM_FALLBACK_VERSIONS = {
34
+ '@owdproject/core': '3.4.0',
35
+ '@owdproject/theme-nova': '0.0.1',
36
+ }
37
+
38
+ const DESKTOP_COPY_FILES = [
39
+ 'tsconfig.json',
40
+ 'tsconfig.app.json',
41
+ 'tsconfig.spec.json',
42
+ '.npmrc',
43
+ ]
44
+
45
+ const ROOT_COPY_FROM_MONOREPO = [
46
+ 'nx.json',
47
+ 'tsconfig.base.json',
48
+ 'tsconfig.json',
49
+ '.editorconfig',
50
+ '.npmrc',
51
+ '.prettierrc',
52
+ '.prettierignore',
53
+ ]
54
+
55
+ const DESKTOP_SKIP_NAMES = new Set([
56
+ 'node_modules',
57
+ '.nuxt',
58
+ '.output',
59
+ 'dist',
60
+ '.env',
61
+ '.data',
62
+ 'vitest.config.ts',
63
+ 'tsconfig.tsbuildinfo',
64
+ 'package.json',
65
+ 'desktop.config.ts',
66
+ 'nuxt.config.ts',
67
+ 'app',
68
+ ])
69
+
70
+ const BLUEPRINT_SKIP_NAMES = new Set(['root-package.json'])
71
+
72
+ /**
73
+ * @param {string} blueprintRoot
74
+ */
75
+ export function resolveBlueprintRoot(blueprintRoot = BLUEPRINT_ROOT) {
76
+ if (!existsSync(join(blueprintRoot, 'pnpm-workspace.yaml'))) {
77
+ throw new Error(`Template blueprint not found at ${blueprintRoot}`)
78
+ }
79
+ return blueprintRoot
80
+ }
81
+
82
+ /**
83
+ * @param {string} src
84
+ * @param {string} dest
85
+ * @param {Set<string>} [skipNames]
86
+ */
87
+ function copyTree(src, dest, skipNames = new Set()) {
88
+ if (!existsSync(src)) return
89
+ const stat = statSync(src)
90
+ if (stat.isFile()) {
91
+ mkdirSync(dirname(dest), { recursive: true })
92
+ cpSync(src, dest)
93
+ return
94
+ }
95
+
96
+ mkdirSync(dest, { recursive: true })
97
+ for (const name of readdirSync(src)) {
98
+ if (skipNames.has(name)) continue
99
+ copyTree(join(src, name), join(dest, name), skipNames)
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @param {string} workspaceRoot
105
+ */
106
+ function readMonorepoPackageJson(workspaceRoot) {
107
+ const path = join(workspaceRoot, 'package.json')
108
+ return JSON.parse(readFileSync(path, 'utf8'))
109
+ }
110
+
111
+ function resolveCatalogVersion(workspaceRoot, name, specifier) {
112
+ if (specifier !== 'catalog:') return specifier
113
+
114
+ const yamlPath = join(workspaceRoot, 'pnpm-workspace.yaml')
115
+ if (!existsSync(yamlPath)) return specifier
116
+
117
+ const content = readFileSync(yamlPath, 'utf8')
118
+ const lines = content.split('\n')
119
+ let insideCatalog = false
120
+ for (const line of lines) {
121
+ const trimmed = line.trim()
122
+ if (trimmed === 'catalog:') {
123
+ insideCatalog = true
124
+ continue
125
+ }
126
+ if (insideCatalog) {
127
+ if (line.match(/^\s+/)) {
128
+ const match = trimmed.match(/^([^:]+):\s*(.+)$/)
129
+ if (match && match[1].trim() === name) {
130
+ return match[2].trim()
131
+ }
132
+ } else if (trimmed) {
133
+ insideCatalog = false
134
+ }
135
+ }
136
+ }
137
+
138
+ return specifier
139
+ }
140
+
141
+ /**
142
+ * @param {string} workspaceRoot
143
+ * @param {{ useCache?: boolean }} [options]
144
+ */
145
+ export async function resolveTemplateVersions(workspaceRoot, options = {}) {
146
+ const monorepo = readMonorepoPackageJson(workspaceRoot)
147
+ const monorepoDevDeps = monorepo.devDependencies ?? {}
148
+
149
+ const owdPackages = [
150
+ ...new Set([
151
+ ...STARTER_DESKTOP_DEPS,
152
+ ...Object.keys(monorepoDevDeps).filter(isOwdScopedPackage),
153
+ ]),
154
+ ]
155
+
156
+ const npmLatest = await resolveLatestVersions(workspaceRoot, owdPackages, options)
157
+
158
+ /** @type {Record<string, string>} */
159
+ const versions = { ...monorepoDevDeps, ...npmLatest }
160
+ for (const name of Object.keys(versions)) {
161
+ if (versions[name] === 'catalog:') {
162
+ versions[name] = resolveCatalogVersion(workspaceRoot, name, versions[name])
163
+ }
164
+ }
165
+
166
+ for (const name of STARTER_DESKTOP_DEPS) {
167
+ versions[name] =
168
+ npmLatest[name] ?? TEMPLATE_NPM_FALLBACK_VERSIONS[name] ?? monorepoDevDeps[name]
169
+ if (!versions[name]) {
170
+ throw new Error(`Could not resolve version for ${name}`)
171
+ }
172
+ }
173
+
174
+ return versions
175
+ }
176
+
177
+ /**
178
+ * @param {Record<string, string>} versions
179
+ */
180
+ function buildTemplateRootPackageJson(versions) {
181
+ const base = JSON.parse(readFileSync(join(BLUEPRINT_ROOT, 'root-package.json'), 'utf8'))
182
+ /** @type {Record<string, string>} */
183
+ const devDependencies = {}
184
+ for (const [name, range] of Object.entries(versions)) {
185
+ if (name === 'h3') continue
186
+ devDependencies[name] = range
187
+ }
188
+ base.devDependencies = Object.fromEntries(
189
+ Object.entries(devDependencies).sort(([a], [b]) => a.localeCompare(b)),
190
+ )
191
+ return `${JSON.stringify(base, null, 2)}\n`
192
+ }
193
+
194
+ /**
195
+ * @param {Record<string, string>} versions
196
+ */
197
+ function buildTemplateDesktopPackageJson(versions) {
198
+ /** @type {Record<string, string>} */
199
+ const dependencies = {}
200
+ for (const name of STARTER_DESKTOP_DEPS) {
201
+ dependencies[name] = versions[name]
202
+ }
203
+
204
+ const pkg = {
205
+ name: '@owdproject/client',
206
+ private: true,
207
+ nx: { name: 'desktop' },
208
+ scripts: {
209
+ build: 'nuxt generate',
210
+ dev: 'nuxt dev --host',
211
+ generate: 'nuxt generate --dev',
212
+ postinstall: 'nuxt prepare',
213
+ preview: 'nuxt preview',
214
+ },
215
+ dependencies: Object.fromEntries(
216
+ Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)),
217
+ ),
218
+ }
219
+
220
+ return `${JSON.stringify(pkg, null, 2)}\n`
221
+ }
222
+
223
+ /**
224
+ * @param {string} srcPath
225
+ */
226
+ function readProjectJson(srcPath) {
227
+ const raw = readFileSync(srcPath, 'utf8')
228
+ const data = JSON.parse(raw)
229
+ if (data.name === 'desktopx') data.name = 'desktop'
230
+ return `${JSON.stringify(data, null, 2)}\n`
231
+ }
232
+
233
+ /**
234
+ * @param {string} workspaceRoot
235
+ * @param {string} outputRoot
236
+ * @param {{ versions: Record<string, string> }} ctx
237
+ */
238
+ function writeGeneratedFiles(workspaceRoot, outputRoot, ctx) {
239
+ const { versions } = ctx
240
+ const desktopSrc = join(workspaceRoot, 'desktop')
241
+ const desktopOut = join(outputRoot, 'desktop')
242
+ const blueprint = resolveBlueprintRoot()
243
+
244
+ mkdirSync(outputRoot, { recursive: true })
245
+
246
+ copyTree(blueprint, outputRoot, BLUEPRINT_SKIP_NAMES)
247
+
248
+ writeFileSync(join(outputRoot, 'package.json'), buildTemplateRootPackageJson(versions))
249
+
250
+ mkdirSync(desktopOut, { recursive: true })
251
+ for (const file of DESKTOP_COPY_FILES) {
252
+ const src = join(desktopSrc, file)
253
+ if (existsSync(src)) {
254
+ cpSync(src, join(desktopOut, file))
255
+ }
256
+ }
257
+
258
+ const i18nBlueprint = join(blueprint, 'desktop/i18n')
259
+ if (existsSync(i18nBlueprint)) {
260
+ copyTree(i18nBlueprint, join(desktopOut, 'i18n'))
261
+ }
262
+
263
+ const projectJsonSrc = join(desktopSrc, 'project.json')
264
+ if (existsSync(projectJsonSrc)) {
265
+ writeFileSync(join(desktopOut, 'project.json'), readProjectJson(projectJsonSrc))
266
+ }
267
+
268
+ for (const file of ROOT_COPY_FROM_MONOREPO) {
269
+ const src = join(workspaceRoot, file)
270
+ if (existsSync(src)) {
271
+ cpSync(src, join(outputRoot, file))
272
+ }
273
+ }
274
+
275
+ writeFileSync(join(desktopOut, 'package.json'), buildTemplateDesktopPackageJson(versions))
276
+ }
277
+
278
+ /**
279
+ * @param {string} dir
280
+ * @returns {string[]}
281
+ */
282
+ function listFilesRecursive(dir) {
283
+ if (!existsSync(dir)) return []
284
+ /** @type {string[]} */
285
+ const files = []
286
+ for (const name of readdirSync(dir)) {
287
+ const path = join(dir, name)
288
+ const stat = statSync(path)
289
+ if (stat.isDirectory()) {
290
+ if (name === 'node_modules' || name === '.git') continue
291
+ files.push(...listFilesRecursive(path))
292
+ } else {
293
+ files.push(path)
294
+ }
295
+ }
296
+ return files
297
+ }
298
+
299
+ /**
300
+ * @param {string} aRoot
301
+ * @param {string} bRoot
302
+ */
303
+ export function diffTemplateTrees(aRoot, bRoot) {
304
+ const aFiles = new Set(listFilesRecursive(aRoot).map((p) => relative(aRoot, p)))
305
+ const bFiles = new Set(listFilesRecursive(bRoot).map((p) => relative(bRoot, p)))
306
+ /** @type {{ path: string, type: 'missing' | 'extra' | 'changed' }[]} */
307
+ const diffs = []
308
+
309
+ for (const rel of aFiles) {
310
+ if (!bFiles.has(rel)) {
311
+ diffs.push({ path: rel, type: 'missing' })
312
+ continue
313
+ }
314
+ const a = readFileSync(join(aRoot, rel))
315
+ const b = readFileSync(join(bRoot, rel))
316
+ if (!a.equals(b)) diffs.push({ path: rel, type: 'changed' })
317
+ }
318
+
319
+ for (const rel of bFiles) {
320
+ if (!aFiles.has(rel)) diffs.push({ path: rel, type: 'extra' })
321
+ }
322
+
323
+ return diffs.sort((x, y) => x.path.localeCompare(y.path))
324
+ }
325
+
326
+ /**
327
+ * @param {string} workspaceRoot
328
+ * @param {{ dryRun?: boolean, check?: boolean, useCache?: boolean }} [options]
329
+ */
330
+ export async function generateTemplate(workspaceRoot, options = {}) {
331
+ const { dryRun = false, check = false, useCache = true } = options
332
+ const templateOut = join(workspaceRoot, 'template')
333
+ const desktopPath = join(workspaceRoot, 'desktop')
334
+
335
+ if (!existsSync(desktopPath)) {
336
+ throw new Error(`Monorepo desktop/ not found at ${desktopPath}`)
337
+ }
338
+ resolveBlueprintRoot()
339
+
340
+ const versions = await resolveTemplateVersions(workspaceRoot, { useCache })
341
+
342
+ if (dryRun) {
343
+ console.log('\nTemplate generation plan (dry run)\n')
344
+ console.log('Output:', templateOut)
345
+ console.log('\nStarter desktop dependencies:')
346
+ for (const name of STARTER_DESKTOP_DEPS) {
347
+ console.log(` ${name} ${versions[name]}`)
348
+ }
349
+ console.log('\nRoot devDependencies (@owdproject/* from npm):')
350
+ for (const [name, range] of Object.entries(versions).sort(([a], [b]) => a.localeCompare(b))) {
351
+ if (isOwdScopedPackage(name)) console.log(` ${name} ${range}`)
352
+ }
353
+ console.log('\nSources:')
354
+ console.log(` blueprint ${BLUEPRINT_ROOT}`)
355
+ console.log(` desktop/ ${desktopPath}`)
356
+ console.log(` monorepo ${workspaceRoot}`)
357
+ console.log('')
358
+ return { versions, outputRoot: templateOut }
359
+ }
360
+
361
+ const outputRoot = check ? mkdtempSync(join(tmpdir(), 'owd-template-check-')) : templateOut
362
+
363
+ try {
364
+ if (!check && existsSync(templateOut)) {
365
+ rmSync(templateOut, { recursive: true, force: true })
366
+ }
367
+
368
+ writeGeneratedFiles(workspaceRoot, outputRoot, { versions })
369
+
370
+
371
+ if (check) {
372
+ const diffs = diffTemplateTrees(templateOut, outputRoot)
373
+ if (diffs.length === 0) {
374
+ console.log('\n✓ template/ is up to date\n')
375
+ return { ok: true, versions }
376
+ }
377
+
378
+ console.error('\n✗ template/ is out of date\n')
379
+ for (const d of diffs.slice(0, 40)) {
380
+ const label =
381
+ d.type === 'missing'
382
+ ? 'only in committed template'
383
+ : d.type === 'extra'
384
+ ? 'missing from committed template'
385
+ : 'changed'
386
+ console.error(` ${label}: ${d.path}`)
387
+ }
388
+ if (diffs.length > 40) {
389
+ console.error(` … and ${diffs.length - 40} more`)
390
+ }
391
+ console.error('\nRun: pnpm desktop template\n')
392
+ return { ok: false, versions, diffs }
393
+ }
394
+
395
+ console.log('\n✓ Generated template/\n')
396
+ console.log('Starter packages:')
397
+ for (const name of STARTER_DESKTOP_DEPS) {
398
+ console.log(` ${name} ${versions[name]}`)
399
+ }
400
+ console.log('\nCommit template/ when ready.\n')
401
+ return { ok: true, versions, outputRoot }
402
+ } finally {
403
+ if (check && existsSync(outputRoot)) {
404
+ rmSync(outputRoot, { recursive: true, force: true })
405
+ }
406
+ }
407
+ }
408
+
409
+ /**
410
+ * @param {{ workspaceRoot: string, dryRun?: boolean, check?: boolean }} options
411
+ */
412
+ export async function runGenerateTemplateCli(options) {
413
+ const { workspaceRoot, dryRun = false, check = false } = options
414
+
415
+ if (!workspaceRoot) {
416
+ console.error('\n✗ Not inside an OWD workspace.\n')
417
+ process.exit(1)
418
+ }
419
+
420
+ try {
421
+ const result = await generateTemplate(workspaceRoot, { dryRun, check })
422
+ if (check && result.ok === false) process.exit(1)
423
+ } catch (error) {
424
+ console.error(`\n✗ ${error.message ?? String(error)}\n`)
425
+ process.exit(1)
426
+ }
427
+ }