@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.
- package/LICENSE +20 -0
- package/README.md +50 -16
- package/bin/bridge.js +129 -0
- package/bin/cli.js +491 -0
- package/bin/cp.js +3542 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +504 -0
- package/bin/lib/config.js +173 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/create.js +246 -0
- package/bin/lib/desktopConfig.js +48 -0
- package/bin/lib/generateTemplate.js +427 -0
- package/bin/lib/install.js +521 -0
- package/bin/lib/installWizardFlow.js +792 -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 +375 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +319 -0
- package/bin/lib/validateModule.js +978 -0
- package/bin/lib/workspace.js +289 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +220 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +33 -0
- package/go/go.sum +52 -0
- package/go/main.go +34 -0
- 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 +869 -0
- 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 +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,39 @@
|
|
|
1
|
+
import spinners from 'cli-spinners'
|
|
2
|
+
import logSymbols from 'log-symbols'
|
|
3
|
+
|
|
4
|
+
/** Same spinner family as ora (dots). */
|
|
5
|
+
const DOTS = spinners.dots.frames
|
|
6
|
+
|
|
7
|
+
export const spinnerFrameCount = DOTS.length
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {'info' | 'ok' | 'error'} [tone]
|
|
11
|
+
*/
|
|
12
|
+
export function statusPrefix(tone = 'info') {
|
|
13
|
+
if (tone === 'ok') return `${logSymbols.success} `
|
|
14
|
+
if (tone === 'error') return `${logSymbols.error} `
|
|
15
|
+
return ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {number} frame
|
|
20
|
+
*/
|
|
21
|
+
export function radarSpinner(frame) {
|
|
22
|
+
return DOTS[frame % DOTS.length]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {number} step
|
|
27
|
+
* @param {number} total
|
|
28
|
+
* @param {number} _frame
|
|
29
|
+
* @param {number} [width]
|
|
30
|
+
*/
|
|
31
|
+
export function progressTrack(step, total, _frame, width = 16) {
|
|
32
|
+
if (total <= 0) return '░'.repeat(width)
|
|
33
|
+
const ratio = Math.min(1, step / total)
|
|
34
|
+
const filled = Math.round(ratio * width)
|
|
35
|
+
if (filled >= width) return '█'.repeat(width)
|
|
36
|
+
const before = '█'.repeat(filled)
|
|
37
|
+
const after = '░'.repeat(width - filled)
|
|
38
|
+
return `${before}${after}`
|
|
39
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { formatCatalogAge } from './catalog.js'
|
|
2
|
+
import { githubHtmlUrl } from './packageSources.js'
|
|
3
|
+
|
|
4
|
+
const ansiRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
5
|
+
function stripTags(str) {
|
|
6
|
+
const cleanAnsi = String(str).replace(ansiRegex, '')
|
|
7
|
+
return cleanAnsi.replace(/\{[^{}]+\}/g, '')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** @typedef {{ pending?: boolean | undefined, colors?: Record<string, string>, columns?: Record<string, number> }} RowContext */
|
|
11
|
+
|
|
12
|
+
const COL = {
|
|
13
|
+
sel: 3,
|
|
14
|
+
name: 28,
|
|
15
|
+
sources: 7,
|
|
16
|
+
dir: 4,
|
|
17
|
+
publisher: 20,
|
|
18
|
+
meta: 14,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getColumnWidths(targetWidth) {
|
|
22
|
+
const showChanges = targetWidth >= 110
|
|
23
|
+
const changesWidth = showChanges ? 10 : 0
|
|
24
|
+
const spaces = showChanges ? 6 : 5
|
|
25
|
+
const fixed = 3 + 7 + 4 + 14 + spaces + changesWidth
|
|
26
|
+
const remaining = targetWidth - fixed
|
|
27
|
+
|
|
28
|
+
let nameWidth = 28
|
|
29
|
+
let pubWidth = 20
|
|
30
|
+
|
|
31
|
+
if (remaining < 48) {
|
|
32
|
+
const minName = 15
|
|
33
|
+
const minPub = 10
|
|
34
|
+
const minTotal = minName + minPub // 25
|
|
35
|
+
if (remaining <= minTotal) {
|
|
36
|
+
nameWidth = minName
|
|
37
|
+
pubWidth = minPub
|
|
38
|
+
} else {
|
|
39
|
+
const ratio = (remaining - minTotal) / (48 - minTotal)
|
|
40
|
+
nameWidth = Math.floor(minName + ratio * (28 - minName))
|
|
41
|
+
pubWidth = remaining - nameWidth
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
const extra = remaining - 48
|
|
45
|
+
const extraName = Math.floor(extra * 0.6)
|
|
46
|
+
const extraPub = extra - extraName
|
|
47
|
+
nameWidth = 28 + extraName
|
|
48
|
+
pubWidth = 20 + extraPub
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
sel: 3,
|
|
53
|
+
name: nameWidth,
|
|
54
|
+
sources: 7,
|
|
55
|
+
dir: 4,
|
|
56
|
+
changes: changesWidth,
|
|
57
|
+
publisher: pubWidth,
|
|
58
|
+
meta: 14,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
const DEFAULT_COLORS = {
|
|
64
|
+
npm: '#7ee787',
|
|
65
|
+
git: 'cyan',
|
|
66
|
+
local: '#ff79c6',
|
|
67
|
+
warn: 'yellow',
|
|
68
|
+
add: '#7ee787',
|
|
69
|
+
remove: 'red',
|
|
70
|
+
accent: '#7ee787',
|
|
71
|
+
muted: '#6c7086',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} text
|
|
76
|
+
* @param {number} width
|
|
77
|
+
*/
|
|
78
|
+
export function padCell(text, width) {
|
|
79
|
+
const raw = String(text)
|
|
80
|
+
if (raw.length >= width) return raw.slice(0, width)
|
|
81
|
+
return raw + ' '.repeat(width - raw.length)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {object} item
|
|
86
|
+
*/
|
|
87
|
+
export function selectionMarker(item) {
|
|
88
|
+
const pending = item.pending
|
|
89
|
+
const installed = item.installed
|
|
90
|
+
if (pending === true) return '[+]'
|
|
91
|
+
if (pending === false) return '[-]'
|
|
92
|
+
if (installed) return '[*]'
|
|
93
|
+
return '[ ]'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {object} item
|
|
98
|
+
*/
|
|
99
|
+
export function sourceSlots(item) {
|
|
100
|
+
const hasNpm = Boolean(item.sourcesMeta?.npm?.version)
|
|
101
|
+
const hasGit = Boolean(item.htmlUrl || item.sourcesMeta?.github)
|
|
102
|
+
const hasLoc = Boolean(item.localSource)
|
|
103
|
+
return {
|
|
104
|
+
npm: hasNpm ? 'NPM' : '---',
|
|
105
|
+
git: hasGit ? 'GIT' : '---',
|
|
106
|
+
loc: hasLoc ? 'LOC' : '---',
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {object} item
|
|
112
|
+
* @param {RowContext} [ctx]
|
|
113
|
+
*/
|
|
114
|
+
export function formatSelectionMarker(item, colors = DEFAULT_COLORS) {
|
|
115
|
+
const pending = item.pending
|
|
116
|
+
const installed = item.installed
|
|
117
|
+
if (pending === true) return `{${colors.muted}-fg}[{/}{${colors.add}-fg}+{/}{${colors.muted}-fg}]{/}`
|
|
118
|
+
if (pending === false) return `{${colors.muted}-fg}[{/}{${colors.remove}-fg}-{/}{${colors.muted}-fg}]{/}`
|
|
119
|
+
if (installed) return `{${colors.muted}-fg}[{/}{${colors.accent}-fg}*{/}{${colors.muted}-fg}]{/}`
|
|
120
|
+
return `{${colors.muted}-fg}[ ]{/}`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {object} item
|
|
125
|
+
* @param {RowContext} [ctx]
|
|
126
|
+
*/
|
|
127
|
+
export function formatCatalogRowPlain(item, ctx = {}) {
|
|
128
|
+
const colors = { ...DEFAULT_COLORS, ...ctx.colors }
|
|
129
|
+
const columns = { ...COL, ...ctx.columns }
|
|
130
|
+
const slots = sourceSlots(item)
|
|
131
|
+
const publisher = item.org && item.org !== 'workspace' ? item.org : 'owdproject'
|
|
132
|
+
const stars = item.stars > 0 ? `*${item.stars}` : ''
|
|
133
|
+
const age = formatCatalogAge(item.updatedAt ?? item.pushedAt)
|
|
134
|
+
|
|
135
|
+
// 1. Status / Selection Marker (3 chars)
|
|
136
|
+
const selTag = formatSelectionMarker(item, colors)
|
|
137
|
+
|
|
138
|
+
// 2. Name Column (columns.name chars)
|
|
139
|
+
let badgeText = (item.isNew || item.isRecent) ? ' new' : ''
|
|
140
|
+
const maxNameLen = columns.name - badgeText.length
|
|
141
|
+
let namePart = item.shortName
|
|
142
|
+
if (namePart.length > maxNameLen) {
|
|
143
|
+
namePart = namePart.slice(0, maxNameLen)
|
|
144
|
+
}
|
|
145
|
+
let nameTag = `{bold}{${colors.accent}-fg}${namePart}{/}{/}`
|
|
146
|
+
if (badgeText) {
|
|
147
|
+
nameTag += `{${colors.warn}-fg}${badgeText}{/}`
|
|
148
|
+
}
|
|
149
|
+
const namePadLen = columns.name - namePart.length - badgeText.length
|
|
150
|
+
if (namePadLen > 0) {
|
|
151
|
+
nameTag += ' '.repeat(namePadLen)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. Sources Column (7 chars)
|
|
155
|
+
const srcNpm =
|
|
156
|
+
slots.npm === 'NPM' ? `{${colors.npm}-fg}${slots.npm}{/}` : `{${colors.muted}-fg}${slots.npm}{/}`
|
|
157
|
+
const srcGit =
|
|
158
|
+
slots.git === 'GIT' ? `{${colors.git}-fg}${slots.git}{/}` : `{${colors.muted}-fg}${slots.git}{/}`
|
|
159
|
+
const sources = `${srcNpm} ${srcGit}`
|
|
160
|
+
|
|
161
|
+
// 3.5 DIR Column (4 chars)
|
|
162
|
+
let dirTag = ''
|
|
163
|
+
const isMissing = item.installed && !item.localSource && !item.inPackageJson
|
|
164
|
+
if (isMissing) {
|
|
165
|
+
dirTag = `{red-fg}MISS{/}`
|
|
166
|
+
} else if (item.localSource) {
|
|
167
|
+
dirTag = `{${colors.npm}-fg}OK{/} `
|
|
168
|
+
} else {
|
|
169
|
+
dirTag = `{${colors.muted}-fg}---{/} `
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 4. Publisher Column (columns.publisher chars)
|
|
173
|
+
let pubPart = publisher
|
|
174
|
+
if (pubPart.length > columns.publisher) {
|
|
175
|
+
pubPart = pubPart.slice(0, columns.publisher)
|
|
176
|
+
}
|
|
177
|
+
const pubColor = item.trusted === false ? colors.warn : colors.muted
|
|
178
|
+
let pubTag = `{${pubColor}-fg}${pubPart}{/}`
|
|
179
|
+
const pubPadLen = columns.publisher - pubPart.length
|
|
180
|
+
if (pubPadLen > 0) {
|
|
181
|
+
pubTag += ' '.repeat(pubPadLen)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 5. Meta Column (columns.meta chars)
|
|
185
|
+
let finalStars = stars
|
|
186
|
+
let finalAge = age
|
|
187
|
+
const metaText = [stars, age].filter(Boolean).join(' ')
|
|
188
|
+
if (metaText.length > columns.meta) {
|
|
189
|
+
if (stars && age) {
|
|
190
|
+
const allowedAgeLen = columns.meta - stars.length - 1
|
|
191
|
+
if (allowedAgeLen > 0) {
|
|
192
|
+
finalAge = age.slice(0, allowedAgeLen)
|
|
193
|
+
} else {
|
|
194
|
+
finalAge = ''
|
|
195
|
+
finalStars = stars.slice(0, columns.meta)
|
|
196
|
+
}
|
|
197
|
+
} else if (stars) {
|
|
198
|
+
finalStars = stars.slice(0, columns.meta)
|
|
199
|
+
} else if (age) {
|
|
200
|
+
finalAge = age.slice(0, columns.meta)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const finalMetaText = [finalStars, finalAge].filter(Boolean).join(' ')
|
|
204
|
+
const metaPadLen = Math.max(0, columns.meta - finalMetaText.length)
|
|
205
|
+
let metaTag = ' '.repeat(metaPadLen)
|
|
206
|
+
if (finalStars && finalAge) {
|
|
207
|
+
metaTag += `{yellow-fg}${finalStars}{/} {${colors.muted}-fg}${finalAge}{/}`
|
|
208
|
+
} else if (finalStars) {
|
|
209
|
+
metaTag += `{yellow-fg}${finalStars}{/}`
|
|
210
|
+
} else if (finalAge) {
|
|
211
|
+
metaTag += `{${colors.muted}-fg}${finalAge}{/}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let changesTag = ''
|
|
215
|
+
if (columns.changes > 0) {
|
|
216
|
+
const parts = []
|
|
217
|
+
|
|
218
|
+
// Local Git changes
|
|
219
|
+
if (item.localSource && ctx.localGitChanges && ctx.localGitChanges.has(item.shortName)) {
|
|
220
|
+
const ch = ctx.localGitChanges.get(item.shortName)
|
|
221
|
+
if (ch.added > 0) parts.push(`{${colors.add}-fg}+${ch.added}{/}`)
|
|
222
|
+
if (ch.modified > 0) parts.push(`{${colors.warn}-fg}~${ch.modified}{/}`)
|
|
223
|
+
if (ch.deleted > 0) parts.push(`{${colors.remove}-fg}-${ch.deleted}{/}`)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Upstream Updates
|
|
227
|
+
if (ctx.packageUpdates && ctx.packageUpdates.has(item.shortName)) {
|
|
228
|
+
const updateInfo = ctx.packageUpdates.get(item.shortName)
|
|
229
|
+
if (updateInfo && updateInfo.hasUpdate) {
|
|
230
|
+
if (updateInfo.localGit) {
|
|
231
|
+
const count = updateInfo.behindCount ? `${updateInfo.behindCount}` : ''
|
|
232
|
+
parts.push(`{cyan-fg}↓${count}{/}`)
|
|
233
|
+
} else if (updateInfo.npm) {
|
|
234
|
+
parts.push(`{${colors.npm}-fg}↑{/}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const txt = parts.join(' ')
|
|
240
|
+
const rawLen = stripTags(txt).length
|
|
241
|
+
changesTag = txt + ' '.repeat(Math.max(0, columns.changes - rawLen))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Join columns with exactly one space (matching header format)
|
|
245
|
+
const cols = [selTag, nameTag, sources, dirTag]
|
|
246
|
+
if (columns.changes > 0) {
|
|
247
|
+
cols.push(changesTag)
|
|
248
|
+
}
|
|
249
|
+
cols.push(pubTag, metaTag)
|
|
250
|
+
return cols.join(' ')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {Record<string, string>} [colors]
|
|
255
|
+
*/
|
|
256
|
+
export function formatLegendLine(colors = DEFAULT_COLORS) {
|
|
257
|
+
const c = { ...DEFAULT_COLORS, ...colors }
|
|
258
|
+
return [
|
|
259
|
+
`{${c.add}-fg}[+]{/} add`,
|
|
260
|
+
`{${c.remove}-fg}[-]{/} remove`,
|
|
261
|
+
`{${c.accent}-fg}[*]{/} on desktop`,
|
|
262
|
+
'|',
|
|
263
|
+
`{${c.npm}-fg}NPM{/} registry`,
|
|
264
|
+
`{${c.git}-fg}GIT{/} repo`,
|
|
265
|
+
`{${c.local}-fg}LOC{/} workspace`,
|
|
266
|
+
'|',
|
|
267
|
+
`{${c.npm}-fg}OK{/} folder exists`,
|
|
268
|
+
`{red-fg}MISS{/} folder missing`,
|
|
269
|
+
'|',
|
|
270
|
+
`{cyan-fg}↓{/} git pull`,
|
|
271
|
+
`{${c.npm}-fg}↑{/} npm update`,
|
|
272
|
+
].join(' ')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {object} item
|
|
277
|
+
* @param {string} targetDir
|
|
278
|
+
* @param {Record<string, string>} [colors]
|
|
279
|
+
* @param {number} [width]
|
|
280
|
+
*/
|
|
281
|
+
export function formatDetailPanel(item, targetDir, colors = DEFAULT_COLORS, width = 76) {
|
|
282
|
+
const c = { ...DEFAULT_COLORS, ...colors }
|
|
283
|
+
const owner = item.org && item.org !== 'workspace' ? item.org : 'owdproject'
|
|
284
|
+
const lines = [`{bold}{${c.accent}-fg}${item.shortName}{/}{/}`]
|
|
285
|
+
|
|
286
|
+
const sourceParts = []
|
|
287
|
+
if (item.sourcesMeta?.npm?.version) {
|
|
288
|
+
sourceParts.push(`{${c.npm}-fg}NPM{/} ${item.sourcesMeta.npm.version}`)
|
|
289
|
+
}
|
|
290
|
+
const gitUrl = item.htmlUrl ?? githubHtmlUrl(owner, item.shortName)
|
|
291
|
+
sourceParts.push(`{${c.git}-fg}GIT{/} ${gitUrl}`)
|
|
292
|
+
if (item.localSource) {
|
|
293
|
+
sourceParts.push(`{${c.local}-fg}LOC{/} ${targetDir}/`)
|
|
294
|
+
}
|
|
295
|
+
lines.push(` Sources ..... ${sourceParts.join(' ')}`)
|
|
296
|
+
|
|
297
|
+
const pub =
|
|
298
|
+
item.trusted === false
|
|
299
|
+
? `{${c.warn}-fg}${owner} (untrusted){/}`
|
|
300
|
+
: owner
|
|
301
|
+
lines.push(` Publisher ... ${pub}`)
|
|
302
|
+
|
|
303
|
+
const starsStr = item.stars > 0 ? `{yellow-fg}★${item.stars}{/}` : '0'
|
|
304
|
+
const dateStr = item.updatedAt ? new Date(item.updatedAt).toLocaleDateString() : '—'
|
|
305
|
+
const kindStr = item.kind ? item.kind.toUpperCase() : '—'
|
|
306
|
+
lines.push(` Stats ....... Stars: ${starsStr} · Updated: ${dateStr} · Type: ${kindStr}`)
|
|
307
|
+
|
|
308
|
+
if (item.localSource) {
|
|
309
|
+
// Show update/change status in details if available
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!item.localSource) {
|
|
313
|
+
const cloneText = ` Clone ....... ${targetDir}/`
|
|
314
|
+
const maxCloneLen = Math.max(15, width - 2)
|
|
315
|
+
const truncatedCloneText = cloneText.length > maxCloneLen ? cloneText.slice(0, maxCloneLen - 3) + '...' : cloneText
|
|
316
|
+
lines.push(` {${c.muted}-fg}${truncatedCloneText}{/}`)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (item.description) {
|
|
320
|
+
const maxDescLen = Math.max(15, width - 4)
|
|
321
|
+
lines.push(` {${c.muted}-fg}${item.description.slice(0, maxDescLen)}{/}`)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const isMissing = item.installed && !item.localSource && !item.inPackageJson
|
|
325
|
+
if (isMissing) {
|
|
326
|
+
lines.push(` {red-fg}{bold}⚠ WARNING: Local workspace folder is missing!{/}{/}`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return lines.join('\n')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function formatHeaderLine(colors = DEFAULT_COLORS, columns = COL) {
|
|
333
|
+
const status = ' '.repeat(columns.sel || 3) + ' '
|
|
334
|
+
const name = 'NAME'.padEnd(columns.name || 28, ' ') + ' '
|
|
335
|
+
const sources = 'SOURCES'.padEnd(columns.sources || 7, ' ') + ' '
|
|
336
|
+
const dir = 'DIR'.padEnd(columns.dir || 4, ' ') + ' '
|
|
337
|
+
const changes = columns.changes > 0 ? 'SYNC'.padEnd(columns.changes, ' ') + ' ' : ''
|
|
338
|
+
const publisher = 'PUBLISHER'.padEnd(columns.publisher || 20, ' ') + ' '
|
|
339
|
+
const meta = 'STARS/AGE'.padStart(columns.meta || 14, ' ')
|
|
340
|
+
return `{bold}${status}${name}${sources}${dir}${changes}${publisher}${meta}{/}`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export { COL as TUI_COLUMN_WIDTHS }
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const DESKTOP_CONFIG_FILENAME = 'desktop.config.ts'
|
|
5
|
+
export const LEGACY_DESKTOP_CONFIG_FILENAME = 'owd.config.ts'
|
|
6
|
+
|
|
7
|
+
export const LEGACY_CONFIG_DEPRECATION =
|
|
8
|
+
'[@owdproject/core] owd.config.ts is deprecated and kept only for backward compatibility; rename to desktop.config.ts (required from @owdproject/core 3.2).'
|
|
9
|
+
|
|
10
|
+
export function resolveDesktopConfigPath(rootDir) {
|
|
11
|
+
const desktopPath = join(rootDir, DESKTOP_CONFIG_FILENAME)
|
|
12
|
+
const legacyPath = join(rootDir, LEGACY_DESKTOP_CONFIG_FILENAME)
|
|
13
|
+
const hasDesktop = existsSync(desktopPath)
|
|
14
|
+
const hasLegacy = existsSync(legacyPath)
|
|
15
|
+
|
|
16
|
+
if (hasDesktop && hasLegacy) {
|
|
17
|
+
console.warn(
|
|
18
|
+
'[@owdproject/core] Both desktop.config.ts and owd.config.ts exist; using desktop.config.ts. Remove owd.config.ts.',
|
|
19
|
+
)
|
|
20
|
+
return { path: desktopPath, file: DESKTOP_CONFIG_FILENAME, legacy: false }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (hasDesktop) {
|
|
24
|
+
return { path: desktopPath, file: DESKTOP_CONFIG_FILENAME, legacy: false }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (hasLegacy) {
|
|
28
|
+
return {
|
|
29
|
+
path: legacyPath,
|
|
30
|
+
file: LEGACY_DESKTOP_CONFIG_FILENAME,
|
|
31
|
+
legacy: true,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function warnLegacyDesktopConfig(resolved) {
|
|
39
|
+
if (resolved?.legacy) {
|
|
40
|
+
console.warn(LEGACY_CONFIG_DEPRECATION)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Path used when creating or updating config from the CLI (always the new filename). */
|
|
45
|
+
export function desktopConfigWritePath(desktopDir) {
|
|
46
|
+
return join(desktopDir, DESKTOP_CONFIG_FILENAME)
|
|
47
|
+
}
|
|
48
|
+
|