@owdproject/cli 0.1.0 → 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.
- package/README.md +7 -7
- package/bin/bridge.js +4 -2
- package/bin/cli.js +63 -5
- package/bin/cp.js +42 -23
- package/bin/lib/catalog.js +3 -0
- package/bin/lib/config.js +51 -8
- package/bin/lib/cpFormat.js +2 -2
- package/bin/lib/create.js +246 -0
- package/bin/lib/desktopConfig.js +48 -8
- package/bin/lib/generateTemplate.js +2 -1
- package/bin/lib/install.js +17 -21
- package/bin/lib/installWizardFlow.js +90 -24
- package/bin/lib/packageSources.js +10 -0
- package/bin/lib/status.js +2 -0
- package/bin/lib/validateModule.js +112 -2
- package/bin/lib/workspace.js +68 -1
- package/go/bridge/bridge.go +20 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +17 -10
- package/go/go.sum +34 -19
- package/go/main.go +7 -3
- package/go/owd-cli +0 -0
- package/go/tui/engine.go +472 -0
- package/go/tui/engine_test.go +40 -0
- package/go/tui/handlers.go +604 -0
- package/go/tui/helpers.go +916 -0
- package/go/tui/helpers_test.go +97 -0
- package/go/tui/modals.go +423 -0
- package/go/tui/resolver.go +377 -0
- package/go/tui/resolver_test.go +86 -0
- package/go/tui/styles.go +153 -0
- package/go/tui/tasks.go +850 -0
- package/go/tui/tui.go +451 -2596
- package/go/tui/tui_test.go +69 -0
- package/go/tui/types.go +230 -0
- package/go/tui/view.go +812 -0
- package/go/tui/workqueue.go +179 -0
- package/go/tui/workqueue_test.go +90 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ pnpm add -g @owdproject/cli
|
|
|
31
31
|
Once installed, run the `desktop` command to open the control panel:
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
desktop
|
|
34
|
+
pnpm desktop
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
### CLI Reference
|
|
@@ -39,27 +39,27 @@ desktop
|
|
|
39
39
|
#### Dev Server
|
|
40
40
|
Start the development server for the monorepo or automatically detect a module's playground:
|
|
41
41
|
```bash
|
|
42
|
-
desktop dev [--playground]
|
|
42
|
+
pnpm desktop dev [--playground]
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
#### Add Packages
|
|
46
46
|
Install apps, modules, or themes from npm, local directories, or custom repositories:
|
|
47
47
|
```bash
|
|
48
|
-
desktop add app-todo --npm
|
|
49
|
-
desktop add theme-nova --dev
|
|
50
|
-
desktop add module-fs --from <github-user>
|
|
48
|
+
pnpm desktop add app-todo --npm
|
|
49
|
+
pnpm desktop add theme-nova --dev
|
|
50
|
+
pnpm desktop add module-fs --from <github-user>
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
#### Scaffold a Project
|
|
54
54
|
Initialize a fresh OWD workspace:
|
|
55
55
|
```bash
|
|
56
|
-
desktop init [project-name]
|
|
56
|
+
pnpm desktop init [project-name]
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
#### Validation
|
|
60
60
|
Check Nuxt module configuration and playground directory structures:
|
|
61
61
|
```bash
|
|
62
|
-
desktop validate
|
|
62
|
+
pnpm desktop validate
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
## License
|
package/bin/bridge.js
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
readDesktopConfig,
|
|
13
13
|
writeDesktopConfig,
|
|
14
14
|
readDesktopDependencies,
|
|
15
|
-
writeDesktopDependencies
|
|
15
|
+
writeDesktopDependencies,
|
|
16
|
+
resolveConfigPathForWrite
|
|
16
17
|
} from './lib/config.js'
|
|
17
18
|
import { loadCatalog, mergeInstalled } from './lib/catalog.js'
|
|
18
19
|
import { resolveDevTarget } from './lib/playgroundContext.js'
|
|
@@ -98,7 +99,8 @@ async function main() {
|
|
|
98
99
|
// payload = { config: { theme, apps, modules }, depsToAdd: { name: version }, depsToRemove: [name], settings?: { ... } }
|
|
99
100
|
|
|
100
101
|
if (payload.config) {
|
|
101
|
-
|
|
102
|
+
const configPath = resolveConfigPathForWrite(ctx.paths)
|
|
103
|
+
writeDesktopConfig(configPath, ctx.workspaceRoot, payload.config)
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
if (payload.depsToAdd || payload.depsToRemove) {
|
package/bin/cli.js
CHANGED
|
@@ -68,8 +68,11 @@ CONTROL PANEL (TUI)
|
|
|
68
68
|
g Settings (GitHub user, SSH, trusted orgs)
|
|
69
69
|
r Refresh package list from GitHub (detects new modules)
|
|
70
70
|
b Run pnpm run generate
|
|
71
|
+
n Create a new app or module (interactive wizard)
|
|
71
72
|
q / Esc Quit
|
|
72
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)
|
|
73
76
|
${name} add <package> [options]
|
|
74
77
|
${name} add <kind> <name> [options]
|
|
75
78
|
${name} validate [path...] Check Nuxt module + playground layout
|
|
@@ -136,6 +139,7 @@ async function openControlPanel(name) {
|
|
|
136
139
|
const cliDir = join(fileURLToPath(import.meta.url), '..')
|
|
137
140
|
const goDir = join(cliDir, '../go')
|
|
138
141
|
const binaryPath = join(goDir, 'desktop-go')
|
|
142
|
+
const readline = await import('node:readline/promises')
|
|
139
143
|
|
|
140
144
|
// Compile the Go Control Panel if binary doesn't exist
|
|
141
145
|
if (!existsSync(binaryPath)) {
|
|
@@ -145,16 +149,52 @@ async function openControlPanel(name) {
|
|
|
145
149
|
console.log('Go Control Panel compiled successfully!')
|
|
146
150
|
} catch (err) {
|
|
147
151
|
console.warn('Failed to compile Go Control Panel, falling back to legacy CP:', err.message)
|
|
148
|
-
const
|
|
149
|
-
|
|
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
|
+
})
|
|
150
176
|
return
|
|
151
177
|
}
|
|
152
178
|
}
|
|
153
179
|
|
|
154
180
|
// Execute Go TUI
|
|
155
181
|
const child = spawn(binaryPath, [], { stdio: 'inherit' })
|
|
156
|
-
child.on('exit', (code) => {
|
|
157
|
-
|
|
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
|
+
}
|
|
158
198
|
})
|
|
159
199
|
}
|
|
160
200
|
|
|
@@ -351,6 +391,20 @@ export async function runCli(name, argv, options = {}) {
|
|
|
351
391
|
return
|
|
352
392
|
}
|
|
353
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
|
+
|
|
354
408
|
if (!cmd) {
|
|
355
409
|
if (!workspaceRoot) {
|
|
356
410
|
fail(
|
|
@@ -414,7 +468,11 @@ export async function runCli(name, argv, options = {}) {
|
|
|
414
468
|
})
|
|
415
469
|
process.exit(0)
|
|
416
470
|
}
|
|
417
|
-
|
|
471
|
+
try {
|
|
472
|
+
await runNpmInstall(kind, pkgName, workspaceRoot)
|
|
473
|
+
} catch (error) {
|
|
474
|
+
fail(error.message ?? String(error))
|
|
475
|
+
}
|
|
418
476
|
} else {
|
|
419
477
|
try {
|
|
420
478
|
await runWorkspaceInstall({
|
package/bin/cp.js
CHANGED
|
@@ -489,6 +489,7 @@ export async function runCp(commandName = 'desktop') {
|
|
|
489
489
|
{ id: 'docs', label: 'Open documentation', key: 'i' },
|
|
490
490
|
{ id: 'settings', label: 'Settings', key: 'g' },
|
|
491
491
|
{ id: 'build', label: 'Build (pnpm generate)', key: 'b' },
|
|
492
|
+
{ id: 'create', label: 'Create new app or module', key: 'n' },
|
|
492
493
|
{ id: 'quit', label: 'Quit control panel', key: 'q' },
|
|
493
494
|
]
|
|
494
495
|
|
|
@@ -1385,11 +1386,16 @@ export async function runCp(commandName = 'desktop') {
|
|
|
1385
1386
|
? `{${C.accent}-fg}${keyHint('s')} save (${queue}){/}`
|
|
1386
1387
|
: `${keyHint('s')} save`
|
|
1387
1388
|
|
|
1389
|
+
const serverRunning = isDevServerUp()
|
|
1390
|
+
const serverShortcut = serverRunning
|
|
1391
|
+
? `{${C.accent}-fg}${keyHint('x')} stop server{/}`
|
|
1392
|
+
: `${keyHint('d')} start server`
|
|
1393
|
+
|
|
1388
1394
|
helpBar.setContent(
|
|
1389
1395
|
[
|
|
1390
1396
|
msgLine,
|
|
1391
1397
|
` Legend: {${C.add}-fg}[+]{/} add {${C.remove}-fg}[-]{/} remove {${C.accent}-fg}[*]{/} on desktop | {${C.npm}-fg}NPM{/} registry {${C.git}-fg}GIT{/} repo {${C.local}-fg}LOC{/} workspace | {${C.warn}-fg}WRN{/} untrusted`,
|
|
1392
|
-
` Shortcuts: ↑↓ Space toggle ${keyHint('1')}${keyHint('2')}${keyHint('3')} tabs ${saveShortcut} ${keyHint('
|
|
1398
|
+
` Shortcuts: ↑↓ Space toggle ${keyHint('1')}${keyHint('2')}${keyHint('3')} tabs ${saveShortcut} ${serverShortcut} ${keyHint('g')} settings ${keyHint('O')} sort ${keyHint('m')} menu ${keyHint('u')} updates ${keyHint('r')} refresh ${keyHint('n')} new ${keyHint('q')} quit`,
|
|
1393
1399
|
].join('\n'),
|
|
1394
1400
|
)
|
|
1395
1401
|
}
|
|
@@ -1410,6 +1416,7 @@ export async function runCp(commandName = 'desktop') {
|
|
|
1410
1416
|
const newConfig = readDesktopConfig(paths.config, workspaceRoot)
|
|
1411
1417
|
configError = null
|
|
1412
1418
|
if (!areConfigsEqual(config, newConfig)) {
|
|
1419
|
+
const oldTheme = config ? config.theme : null
|
|
1413
1420
|
config = newConfig
|
|
1414
1421
|
deps = readDesktopDependencies(paths.packageJson)
|
|
1415
1422
|
|
|
@@ -1420,7 +1427,7 @@ export async function runCp(commandName = 'desktop') {
|
|
|
1420
1427
|
pendingPackages.delete(name)
|
|
1421
1428
|
}
|
|
1422
1429
|
}
|
|
1423
|
-
if (pendingTheme ===
|
|
1430
|
+
if (pendingTheme === oldTheme || !pendingTheme) {
|
|
1424
1431
|
pendingTheme = config.theme
|
|
1425
1432
|
}
|
|
1426
1433
|
|
|
@@ -2036,6 +2043,9 @@ export async function runCp(commandName = 'desktop') {
|
|
|
2036
2043
|
refreshTerminalUi()
|
|
2037
2044
|
}
|
|
2038
2045
|
break
|
|
2046
|
+
case 'create':
|
|
2047
|
+
process.exit(10)
|
|
2048
|
+
break
|
|
2039
2049
|
case 'quit':
|
|
2040
2050
|
process.exit(0)
|
|
2041
2051
|
break
|
|
@@ -2723,30 +2733,35 @@ export async function runCp(commandName = 'desktop') {
|
|
|
2723
2733
|
installWizardList.setItems(
|
|
2724
2734
|
installWizardOptions.map((opt) => {
|
|
2725
2735
|
const prefix =
|
|
2726
|
-
opt.kind === 'npm' ? `{${C.npm}-fg}NPM{/}` : `{${C.git}-fg}GIT{/}`
|
|
2736
|
+
opt.kind === 'npm' ? `{${C.npm}-fg}NPM{/}` : opt.kind === 'local' ? `{${C.local}-fg}LOC{/}` : `{${C.git}-fg}GIT{/}`
|
|
2727
2737
|
return `${prefix} ${opt.label}`
|
|
2728
2738
|
}),
|
|
2729
2739
|
)
|
|
2730
2740
|
let defaultIdx = 0
|
|
2731
|
-
const
|
|
2732
|
-
if (
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
if (isNpm) {
|
|
2741
|
-
found = installWizardOptions.findIndex((opt) => opt.id === 'npm')
|
|
2742
|
-
if (found >= 0) defaultIdx = found
|
|
2741
|
+
const isLocalAvailable = installWizardOptions.some((opt) => opt.id === 'local')
|
|
2742
|
+
if (isLocalAvailable) {
|
|
2743
|
+
defaultIdx = installWizardOptions.findIndex((opt) => opt.id === 'local')
|
|
2744
|
+
} else {
|
|
2745
|
+
const lastId = settings.lastInstallChoiceId
|
|
2746
|
+
if (lastId) {
|
|
2747
|
+
let found = installWizardOptions.findIndex((opt) => opt.id === lastId)
|
|
2748
|
+
if (found >= 0) {
|
|
2749
|
+
defaultIdx = found
|
|
2743
2750
|
} else {
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2751
|
+
const isSsh = lastId.includes('ssh')
|
|
2752
|
+
const isHttps = lastId.includes('https')
|
|
2753
|
+
const isNpm = lastId === 'npm'
|
|
2754
|
+
if (isNpm) {
|
|
2755
|
+
found = installWizardOptions.findIndex((opt) => opt.id === 'npm')
|
|
2756
|
+
if (found >= 0) defaultIdx = found
|
|
2757
|
+
} else {
|
|
2758
|
+
found = installWizardOptions.findIndex((opt) => {
|
|
2759
|
+
if (isSsh && opt.protocol === 'ssh') return true
|
|
2760
|
+
if (isHttps && opt.protocol === 'https') return true
|
|
2761
|
+
return false
|
|
2762
|
+
})
|
|
2763
|
+
if (found >= 0) defaultIdx = found
|
|
2764
|
+
}
|
|
2750
2765
|
}
|
|
2751
2766
|
}
|
|
2752
2767
|
}
|
|
@@ -3216,6 +3231,10 @@ export async function runCp(commandName = 'desktop') {
|
|
|
3216
3231
|
if (menuOpen) closeMenu()
|
|
3217
3232
|
else openMenu()
|
|
3218
3233
|
})
|
|
3234
|
+
screen.key(['n', 'N'], () => {
|
|
3235
|
+
if (settingsOpen || installWizardOpen || sortPickerOpen || menuOpen || confirmDialogOpen) return
|
|
3236
|
+
process.exit(10)
|
|
3237
|
+
})
|
|
3219
3238
|
screen.key(['d', 'D'], () => {
|
|
3220
3239
|
if (!overlayBlocksKeys()) {
|
|
3221
3240
|
if (isDevServerUp()) stopDevServer()
|
|
@@ -3481,7 +3500,7 @@ export async function runCp(commandName = 'desktop') {
|
|
|
3481
3500
|
startConfigWatcher()
|
|
3482
3501
|
|
|
3483
3502
|
setStatus(
|
|
3484
|
-
`Select packages · ${keyHint('s')} opens install wizard · ${keyHint('u')} updates · ${keyHint('
|
|
3503
|
+
`Select packages · ${keyHint('s')} opens install wizard · ${keyHint('u')} updates · ${keyHint('d')} start server · ${keyHint('g')} settings`,
|
|
3485
3504
|
'info',
|
|
3486
3505
|
)
|
|
3487
3506
|
|
|
@@ -3490,7 +3509,7 @@ export async function runCp(commandName = 'desktop') {
|
|
|
3490
3509
|
|
|
3491
3510
|
setInterval(async () => {
|
|
3492
3511
|
const now = Date.now()
|
|
3493
|
-
if (now - lastShowcaseTime >=
|
|
3512
|
+
if (now - lastShowcaseTime >= 15000) {
|
|
3494
3513
|
showcaseIndex++
|
|
3495
3514
|
lastShowcaseTime = now
|
|
3496
3515
|
}
|
package/bin/lib/catalog.js
CHANGED
|
@@ -312,6 +312,7 @@ export async function loadCatalog(workspaceRoot, settings, options = {}) {
|
|
|
312
312
|
byName.set(item.shortName, {
|
|
313
313
|
...prev,
|
|
314
314
|
...item,
|
|
315
|
+
stars: prev.stars ?? 0, // preserve GitHub stars — scanLocal always sets 0
|
|
315
316
|
localPath: item.localPath,
|
|
316
317
|
remote: item.remote ?? prev.remote,
|
|
317
318
|
updatedAt: item.updatedAt ?? prev.updatedAt,
|
|
@@ -336,6 +337,8 @@ export async function loadCatalog(workspaceRoot, settings, options = {}) {
|
|
|
336
337
|
trusted: isTrustedPublisher(entry, settings),
|
|
337
338
|
}))
|
|
338
339
|
|
|
340
|
+
entries = entries.filter((e) => !e.name.includes('-template') && !e.shortName.includes('-template'))
|
|
341
|
+
|
|
339
342
|
entries = await enrichCatalogEntries(entries, workspaceRoot, settings)
|
|
340
343
|
|
|
341
344
|
return {
|
package/bin/lib/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, existsSync, copyFileSync } from 'node:fs'
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { SCOPE } from './workspace.js'
|
|
@@ -7,13 +7,6 @@ import { LEGACY_CONFIG_DEPRECATION } from './desktopConfig.js'
|
|
|
7
7
|
const require = createRequire(import.meta.url)
|
|
8
8
|
|
|
9
9
|
function loadTsMorph(workspaceRoot) {
|
|
10
|
-
const utilPath = join(
|
|
11
|
-
workspaceRoot,
|
|
12
|
-
'node_modules/@owdproject/nx/dist/utils/utilConfig.js',
|
|
13
|
-
)
|
|
14
|
-
if (!existsSync(utilPath)) {
|
|
15
|
-
throw new Error('Missing @owdproject/nx — run pnpm install at the repo root.')
|
|
16
|
-
}
|
|
17
10
|
const { Project, SyntaxKind } = require('ts-morph')
|
|
18
11
|
return { Project, SyntaxKind }
|
|
19
12
|
}
|
|
@@ -128,3 +121,53 @@ export function writeDesktopDependencies(packageJsonPath, toAdd, toRemove) {
|
|
|
128
121
|
|
|
129
122
|
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
130
123
|
}
|
|
124
|
+
|
|
125
|
+
export function addToDesktopConfig(configPath, workspaceRoot, key, value) {
|
|
126
|
+
const { Project, SyntaxKind } = loadTsMorph(workspaceRoot)
|
|
127
|
+
const project = new Project()
|
|
128
|
+
const sourceFile = project.addSourceFileAtPath(configPath)
|
|
129
|
+
const configObj = getConfigObject(sourceFile, SyntaxKind)
|
|
130
|
+
|
|
131
|
+
const rawProp = configObj.getProperty(key)
|
|
132
|
+
const targetProp = rawProp?.asKind(SyntaxKind.PropertyAssignment)
|
|
133
|
+
|
|
134
|
+
if (!targetProp) {
|
|
135
|
+
// if property doesn't exist, create it
|
|
136
|
+
configObj.addPropertyAssignment({
|
|
137
|
+
name: key,
|
|
138
|
+
initializer: key === 'theme' ? `'${value}'` : `[ '${value}' ]`,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
sourceFile.saveSync()
|
|
142
|
+
console.log(`🛠️ Created '${key}' and added '${value}'`)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// handle 'theme' key — just set string literal
|
|
147
|
+
if (key === 'theme') {
|
|
148
|
+
targetProp.setInitializer(`'${value}'`)
|
|
149
|
+
sourceFile.saveSync()
|
|
150
|
+
console.log(`🎨 Theme set to '${value}'`)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ensure initializer is an array
|
|
155
|
+
const initializer = targetProp.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
|
|
156
|
+
if (!initializer) {
|
|
157
|
+
throw new Error(`Expected '${key}' to be an array`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const exists = initializer.getElements().some((el) => {
|
|
161
|
+
const text = el.getText().trim().replace(/['"`]/g, '')
|
|
162
|
+
return text === value
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
if (exists) {
|
|
166
|
+
console.log(`⚠️ '${value}' already exists in '${key}'`)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
initializer.addElement(`'${value}'`)
|
|
171
|
+
sourceFile.saveSync()
|
|
172
|
+
console.log(`✅ Added '${value}' to '${key}'`)
|
|
173
|
+
}
|
package/bin/lib/cpFormat.js
CHANGED
|
@@ -160,7 +160,7 @@ export function formatCatalogRowPlain(item, ctx = {}) {
|
|
|
160
160
|
|
|
161
161
|
// 3.5 DIR Column (4 chars)
|
|
162
162
|
let dirTag = ''
|
|
163
|
-
const isMissing = item.installed && !item.localSource
|
|
163
|
+
const isMissing = item.installed && !item.localSource && !item.inPackageJson
|
|
164
164
|
if (isMissing) {
|
|
165
165
|
dirTag = `{red-fg}MISS{/}`
|
|
166
166
|
} else if (item.localSource) {
|
|
@@ -321,7 +321,7 @@ export function formatDetailPanel(item, targetDir, colors = DEFAULT_COLORS, widt
|
|
|
321
321
|
lines.push(` {${c.muted}-fg}${item.description.slice(0, maxDescLen)}{/}`)
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
const isMissing = item.installed && !item.localSource
|
|
324
|
+
const isMissing = item.installed && !item.localSource && !item.inPackageJson
|
|
325
325
|
if (isMissing) {
|
|
326
326
|
lines.push(` {red-fg}{bold}⚠ WARNING: Local workspace folder is missing!{/}{/}`)
|
|
327
327
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, mkdtempSync, rmSync } from 'node:fs'
|
|
2
|
+
import { join, dirname, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import readline from 'node:readline/promises'
|
|
5
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
6
|
+
import { KINDS } from './workspace.js'
|
|
7
|
+
import {
|
|
8
|
+
resolveDesktopConfigPath,
|
|
9
|
+
desktopConfigWritePath,
|
|
10
|
+
} from './desktopConfig.js'
|
|
11
|
+
import { addToDesktopConfig } from './config.js'
|
|
12
|
+
import {
|
|
13
|
+
linkWorkspacePackage,
|
|
14
|
+
spawnAsync,
|
|
15
|
+
} from './install.js'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url)
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const CLI_ROOT = resolve(__dirname, '../..')
|
|
21
|
+
|
|
22
|
+
function pascalCase(str) {
|
|
23
|
+
return str
|
|
24
|
+
.replace(/(?:^|-|_)(\w)/g, (_, c) => c.toUpperCase())
|
|
25
|
+
.replace(/[-_]/g, '')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function camelCase(str) {
|
|
29
|
+
const p = pascalCase(str)
|
|
30
|
+
return p.charAt(0).toLowerCase() + p.slice(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function cloneTemplate(gitUrl, destDir) {
|
|
34
|
+
try {
|
|
35
|
+
await spawnAsync('git', ['clone', '--depth', '1', gitUrl, destDir])
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (gitUrl.startsWith('git@')) {
|
|
38
|
+
const httpsUrl = gitUrl
|
|
39
|
+
.replace('git@github.com:', 'https://github.com/')
|
|
40
|
+
.replace(/\.git$/, '') + '.git'
|
|
41
|
+
console.log(`SSH clone failed, trying HTTPS fallback: ${httpsUrl}`)
|
|
42
|
+
await spawnAsync('git', ['clone', '--depth', '1', httpsUrl, destDir])
|
|
43
|
+
} else {
|
|
44
|
+
throw err
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function copyAndScaffold(srcDir, destDir, replacements) {
|
|
50
|
+
if (!existsSync(destDir)) {
|
|
51
|
+
mkdirSync(destDir, { recursive: true })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const entries = readdirSync(srcDir)
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (
|
|
57
|
+
entry === 'node_modules' ||
|
|
58
|
+
entry === '.nuxt' ||
|
|
59
|
+
entry === 'dist' ||
|
|
60
|
+
entry === '.output' ||
|
|
61
|
+
entry === '.git'
|
|
62
|
+
) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const srcPath = join(srcDir, entry)
|
|
67
|
+
let destEntry = entry
|
|
68
|
+
for (const [placeholder, val] of Object.entries(replacements)) {
|
|
69
|
+
destEntry = destEntry.replaceAll(placeholder, val)
|
|
70
|
+
}
|
|
71
|
+
const destPath = join(destDir, destEntry)
|
|
72
|
+
|
|
73
|
+
const stat = statSync(srcPath)
|
|
74
|
+
if (stat.isDirectory()) {
|
|
75
|
+
copyAndScaffold(srcPath, destPath, replacements)
|
|
76
|
+
} else {
|
|
77
|
+
const fileContent = readFileSync(srcPath)
|
|
78
|
+
const isText = !fileContent.slice(0, 512).includes(0)
|
|
79
|
+
if (isText) {
|
|
80
|
+
let textContent = fileContent.toString('utf8')
|
|
81
|
+
for (const [placeholder, val] of Object.entries(replacements)) {
|
|
82
|
+
textContent = textContent.replaceAll(placeholder, val)
|
|
83
|
+
}
|
|
84
|
+
writeFileSync(destPath, textContent, 'utf8')
|
|
85
|
+
} else {
|
|
86
|
+
writeFileSync(destPath, fileContent)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function runCreateCli(options = {}) {
|
|
93
|
+
const workspaceRoot = options.workspaceRoot
|
|
94
|
+
if (!workspaceRoot) {
|
|
95
|
+
throw new Error('Not inside an OWD workspace.')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let { kind, name } = options
|
|
99
|
+
|
|
100
|
+
const rl = readline.createInterface({ input, output })
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// 1. Prompt for Kind if missing or invalid
|
|
104
|
+
while (!kind || (kind !== 'app' && kind !== 'module')) {
|
|
105
|
+
const answer = await rl.question('Select package kind (app / module) [app]: ')
|
|
106
|
+
const cleaned = answer.trim().toLowerCase()
|
|
107
|
+
if (cleaned === '') {
|
|
108
|
+
kind = 'app'
|
|
109
|
+
} else if (cleaned === 'app' || cleaned === 'module') {
|
|
110
|
+
kind = cleaned
|
|
111
|
+
} else {
|
|
112
|
+
console.log('Invalid kind. Please select "app" or "module".')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Prompt for Name if missing
|
|
117
|
+
while (!name || name.trim() === '') {
|
|
118
|
+
const answer = await rl.question(`Enter ${kind} name: `)
|
|
119
|
+
name = answer.trim().toLowerCase().replace(/[^a-z0-9-_]/g, '')
|
|
120
|
+
if (name === '') {
|
|
121
|
+
console.log('Name is required and should contain only alphanumeric characters, dashes, or underscores.')
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
rl.close()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Normalize prefix from name (e.g. if user entered "app-todo" instead of "todo")
|
|
129
|
+
if (kind === 'app' && name.startsWith('app-')) {
|
|
130
|
+
name = name.slice(4)
|
|
131
|
+
} else if (kind === 'module' && name.startsWith('module-')) {
|
|
132
|
+
name = name.slice(7)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pkgShortName = `${kind}-${name}`
|
|
136
|
+
const pkgFullName = `@owdproject/${pkgShortName}`
|
|
137
|
+
const workspaceDir = KINDS[kind].workspaceDir
|
|
138
|
+
const targetDir = join(workspaceRoot, workspaceDir, pkgShortName)
|
|
139
|
+
|
|
140
|
+
if (existsSync(targetDir)) {
|
|
141
|
+
throw new Error(`Directory already exists: ${targetDir}`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`\nCreating new ${kind} in ${targetDir}...\n`)
|
|
145
|
+
|
|
146
|
+
let templateSrc = null
|
|
147
|
+
let tempCloneDir = null
|
|
148
|
+
|
|
149
|
+
if (kind === 'app') {
|
|
150
|
+
const monorepoPath = join(workspaceRoot, 'apps/app-template')
|
|
151
|
+
if (existsSync(join(monorepoPath, 'package.json'))) {
|
|
152
|
+
templateSrc = monorepoPath
|
|
153
|
+
}
|
|
154
|
+
} else if (kind === 'module') {
|
|
155
|
+
const monorepoPath = join(workspaceRoot, 'packages/module-template')
|
|
156
|
+
if (existsSync(join(monorepoPath, 'package.json'))) {
|
|
157
|
+
templateSrc = monorepoPath
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!templateSrc) {
|
|
162
|
+
const gitUrl = kind === 'app'
|
|
163
|
+
? 'git@github.com:owdproject/app-template.git'
|
|
164
|
+
: 'git@github.com:owdproject/module-template.git'
|
|
165
|
+
|
|
166
|
+
console.log(`Cloning template from ${gitUrl}...`)
|
|
167
|
+
try {
|
|
168
|
+
const tmpPrefix = join(workspaceRoot, '.owd-template-')
|
|
169
|
+
tempCloneDir = mkdtempSync(tmpPrefix)
|
|
170
|
+
await cloneTemplate(gitUrl, tempCloneDir)
|
|
171
|
+
templateSrc = tempCloneDir
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (tempCloneDir && existsSync(tempCloneDir)) {
|
|
174
|
+
try {
|
|
175
|
+
rmSync(tempCloneDir, { recursive: true, force: true })
|
|
176
|
+
} catch {}
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`Failed to clone template from GitHub: ${err.message}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Prepare replacements
|
|
183
|
+
const replacements = {}
|
|
184
|
+
if (kind === 'app') {
|
|
185
|
+
replacements['@owdproject/app-template'] = pkgFullName
|
|
186
|
+
replacements['app-template'] = pkgShortName
|
|
187
|
+
replacements['desktop-app-template'] = `desktop-app-${name}`
|
|
188
|
+
replacements['desktop-app-template-register'] = `desktop-app-${name}-register`
|
|
189
|
+
replacements['appTemplate'] = `app${pascalCase(name)}`
|
|
190
|
+
replacements['AppTemplate'] = `App${pascalCase(name)}`
|
|
191
|
+
replacements['WindowTemplate'] = `Window${pascalCase(name)}`
|
|
192
|
+
replacements['myApp'] = `app${pascalCase(name)}`
|
|
193
|
+
replacements['my-app'] = `app-${name}`
|
|
194
|
+
replacements['ext.domain.app'] = `ext.domain.${name}`
|
|
195
|
+
replacements['Template'] = pascalCase(name)
|
|
196
|
+
} else {
|
|
197
|
+
replacements['@owdproject/module-template'] = pkgFullName
|
|
198
|
+
replacements['module-template'] = pkgShortName
|
|
199
|
+
replacements['desktop-module-template'] = `desktop-module-${name}`
|
|
200
|
+
replacements['desktop-module-template-register'] = `desktop-module-${name}-register`
|
|
201
|
+
replacements['moduleTemplate'] = `module${pascalCase(name)}`
|
|
202
|
+
replacements['ModuleTemplate'] = `Module${pascalCase(name)}`
|
|
203
|
+
replacements['ext.domain.module'] = `ext.domain.${name}`
|
|
204
|
+
replacements['owd-module-template'] = `owd-module-${name}`
|
|
205
|
+
replacements['owd-plugin-template'] = `owd-plugin-${name}`
|
|
206
|
+
replacements['Template'] = pascalCase(name)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Copy and replace placeholders
|
|
210
|
+
try {
|
|
211
|
+
copyAndScaffold(templateSrc, targetDir, replacements)
|
|
212
|
+
console.log(`✓ Files scaffolded from template.`)
|
|
213
|
+
} finally {
|
|
214
|
+
if (tempCloneDir && existsSync(tempCloneDir)) {
|
|
215
|
+
try {
|
|
216
|
+
rmSync(tempCloneDir, { recursive: true, force: true })
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Run pnpm install in root
|
|
222
|
+
console.log('Running pnpm install in workspace root...')
|
|
223
|
+
await spawnAsync('pnpm', ['install'], { cwd: workspaceRoot })
|
|
224
|
+
|
|
225
|
+
// Link package in desktop
|
|
226
|
+
console.log(`Linking ${pkgFullName} in desktop project...`)
|
|
227
|
+
const desktopPath = join(workspaceRoot, 'desktop')
|
|
228
|
+
await linkWorkspacePackage(desktopPath, pkgFullName)
|
|
229
|
+
|
|
230
|
+
// Update desktop/desktop.config.ts
|
|
231
|
+
console.log('Registering package in desktop.config.ts...')
|
|
232
|
+
const resolved = resolveDesktopConfigPath(desktopPath)
|
|
233
|
+
const configPath = resolved?.path ?? desktopConfigWritePath(desktopPath)
|
|
234
|
+
|
|
235
|
+
addToDesktopConfig(configPath, workspaceRoot, KINDS[kind].configKey, pkgFullName)
|
|
236
|
+
|
|
237
|
+
// Run dev:prepare
|
|
238
|
+
console.log(`Running dev:prepare on ${pkgFullName}...`)
|
|
239
|
+
try {
|
|
240
|
+
await spawnAsync('pnpm', ['--filter', pkgFullName, 'run', 'dev:prepare'], { cwd: workspaceRoot })
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.warn(`(warning: dev:prepare failed — it's normal if package does not have the script)`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
console.log(`\n✓ Scaffolded and registered new ${kind}: ${pkgFullName}\n`)
|
|
246
|
+
}
|