@owdproject/core 3.1.4-beta.0 → 3.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 (64) hide show
  1. package/README.md +6 -6
  2. package/bin/cli.js +395 -0
  3. package/bin/desktop.js +5 -0
  4. package/bin/lib/catalog.js +293 -0
  5. package/bin/lib/config.js +109 -0
  6. package/bin/lib/desktopConfig.js +56 -0
  7. package/bin/lib/generateTemplate.js +389 -0
  8. package/bin/lib/install.js +408 -0
  9. package/bin/lib/migrateModulePlayground.config.mjs +164 -0
  10. package/bin/lib/migrateModulePlayground.mjs +313 -0
  11. package/bin/lib/npmVersions.js +124 -0
  12. package/bin/lib/playgroundContext.js +188 -0
  13. package/bin/lib/scaffold.js +197 -0
  14. package/bin/lib/status.js +289 -0
  15. package/bin/lib/validateModule.js +683 -0
  16. package/bin/lib/workspace.js +182 -0
  17. package/bin/owd.js +2 -25
  18. package/bin/tui.js +1379 -0
  19. package/dist/module.d.mts +13 -0
  20. package/dist/module.json +9 -0
  21. package/dist/module.mjs +219 -0
  22. package/dist/types.d.mts +7 -0
  23. package/module.ts +53 -19
  24. package/package.json +78 -22
  25. package/runtime/components/Core/Application/CoreApplicationRender.vue +3 -1
  26. package/runtime/components/Core/Application/CoreApplicationWindowsRender.vue +28 -7
  27. package/runtime/components/Core/Desktop/CoreDesktop.vue +11 -7
  28. package/runtime/components/Core/Window/CoreWindow.vue +45 -9
  29. package/runtime/components/Core/Window/CoreWindowNav.vue +26 -8
  30. package/runtime/composables/useApplicationEntries.ts +1 -1
  31. package/runtime/composables/useApplicationManager.ts +282 -4
  32. package/runtime/composables/useDesktopManager.ts +46 -4
  33. package/runtime/composables/useOwdDialogs.ts +13 -0
  34. package/runtime/composables/useTerminalManager.ts +62 -3
  35. package/runtime/composables/useWorkspaceOverview.ts +41 -0
  36. package/runtime/core/controllers/ApplicationController.ts +120 -30
  37. package/runtime/core/controllers/WindowController.ts +5 -10
  38. package/runtime/dialogs/owdDialogProvider.ts +81 -0
  39. package/runtime/plugins/01.owd-dialogs-fallback.ts +15 -0
  40. package/runtime/stores/storeApplicationMeta.ts +56 -12
  41. package/runtime/stores/storeDesktop.ts +79 -0
  42. package/runtime/stores/storeDesktopDefaultApps.ts +53 -0
  43. package/runtime/stores/storeDesktopExplorer.ts +74 -0
  44. package/runtime/stores/storeDesktopWorkspace.ts +14 -2
  45. package/runtime/utils/captureElementToCanvas.ts +21 -0
  46. package/runtime/utils/explorerEntryPath.ts +10 -0
  47. package/runtime/utils/resolveDesktopConfigPath.ts +52 -0
  48. package/runtime/utils/validateOwdUserConfig.ts +44 -0
  49. package/types/index.d.ts +64 -12
  50. package/CHANGELOG.md +0 -25
  51. package/project.json +0 -4
  52. package/runtime/composables/useClipboardFs.ts +0 -25
  53. package/runtime/core/managers/ApplicationManager.ts +0 -281
  54. package/runtime/core/managers/DesktopManager.ts +0 -47
  55. package/runtime/core/managers/TerminalManager.ts +0 -48
  56. package/runtime/stores/storeApplication.ts +0 -235
  57. package/test/basic.test.ts +0 -15
  58. package/test/fixtures/basic/app.vue +0 -5
  59. package/test/fixtures/basic/nuxt.config.ts +0 -5
  60. package/test/fixtures/basic/package.json +0 -5
  61. package/tsconfig.app.json +0 -31
  62. package/tsconfig.json +0 -12
  63. package/tsconfig.spec.json +0 -37
  64. package/vitest.config.ts +0 -24
@@ -0,0 +1,293 @@
1
+ import {
2
+ existsSync,
3
+ readFileSync,
4
+ readdirSync,
5
+ mkdirSync,
6
+ writeFileSync,
7
+ statSync,
8
+ } from 'node:fs'
9
+ import { join } from 'node:path'
10
+ import { execSync } from 'node:child_process'
11
+ import {
12
+ KINDS,
13
+ SCOPE,
14
+ fullName,
15
+ shortName,
16
+ inferKind,
17
+ isInstallableDesktopModule,
18
+ desktopMetaDir,
19
+ } from './workspace.js'
20
+ import { hasLocalWorkspaceSource } from './install.js'
21
+
22
+ const CACHE_TTL_MS = 60 * 60 * 1000
23
+
24
+ function cachePath(workspaceRoot) {
25
+ return join(desktopMetaDir(workspaceRoot), 'catalog-cache.json')
26
+ }
27
+
28
+ function readCache(workspaceRoot) {
29
+ const path = cachePath(workspaceRoot)
30
+ if (!existsSync(path)) return null
31
+ try {
32
+ const data = JSON.parse(readFileSync(path, 'utf8'))
33
+ if (Date.now() - data.fetchedAt > CACHE_TTL_MS) return null
34
+ const ageMin = Math.round((Date.now() - data.fetchedAt) / 60_000)
35
+ return { entries: data.entries, cacheAge: `${ageMin}m` }
36
+ } catch {
37
+ return null
38
+ }
39
+ }
40
+
41
+ function formatCacheAge(fetchedAt) {
42
+ const ageMin = Math.round((Date.now() - fetchedAt) / 60_000)
43
+ if (ageMin < 1) return '<1m'
44
+ if (ageMin < 60) return `${ageMin}m`
45
+ return `${Math.round(ageMin / 60)}h`
46
+ }
47
+
48
+ function writeCache(workspaceRoot, entries) {
49
+ mkdirSync(desktopMetaDir(workspaceRoot), { recursive: true })
50
+ writeFileSync(
51
+ cachePath(workspaceRoot),
52
+ JSON.stringify({ fetchedAt: Date.now(), entries }, null, 2),
53
+ )
54
+ }
55
+
56
+ async function fetchTopic(topic, org) {
57
+ const q = encodeURIComponent(`topic:${topic} org:${org}`)
58
+ const url = `https://api.github.com/search/repositories?q=${q}&sort=stars&per_page=100`
59
+ const headers = {
60
+ Accept: 'application/vnd.github+json',
61
+ 'User-Agent': 'owd-desktop-cli',
62
+ }
63
+ if (process.env.GITHUB_TOKEN) {
64
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`
65
+ }
66
+
67
+ const res = await fetch(url, { headers })
68
+ if (!res.ok) throw new Error(`GitHub API ${res.status} for ${org}/${topic}`)
69
+ const json = await res.json()
70
+ return (json.items ?? []).map((repo) => ({
71
+ shortName: repo.name,
72
+ description: repo.description ?? '',
73
+ stars: repo.stargazers_count ?? 0,
74
+ htmlUrl: repo.html_url,
75
+ org: repo.owner?.login ?? org,
76
+ kind: inferKind(repo.name),
77
+ }))
78
+ }
79
+
80
+ async function fetchUserRepos(user) {
81
+ const url = `https://api.github.com/users/${encodeURIComponent(user)}/repos?per_page=100&sort=updated`
82
+ const headers = {
83
+ Accept: 'application/vnd.github+json',
84
+ 'User-Agent': 'owd-desktop-cli',
85
+ }
86
+ if (process.env.GITHUB_TOKEN) {
87
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`
88
+ }
89
+
90
+ const res = await fetch(url, { headers })
91
+ if (!res.ok) return []
92
+ const repos = await res.json()
93
+ return repos
94
+ .filter((repo) => /^(app|theme|module|kit)-/.test(repo.name))
95
+ .map((repo) => ({
96
+ shortName: repo.name,
97
+ description: repo.description ?? '',
98
+ stars: repo.stargazers_count ?? 0,
99
+ htmlUrl: repo.html_url,
100
+ org: repo.owner?.login ?? user,
101
+ kind: inferKind(repo.name),
102
+ }))
103
+ }
104
+
105
+ function scanLocal(workspaceRoot, kind) {
106
+ const dir = join(workspaceRoot, KINDS[kind].workspaceDir)
107
+ if (!existsSync(dir)) return []
108
+
109
+ return readdirSync(dir, { withFileTypes: true })
110
+ .filter((d) => d.isDirectory())
111
+ .map((d) => {
112
+ const pkgPath = join(dir, d.name, 'package.json')
113
+ if (!existsSync(pkgPath)) return null
114
+ try {
115
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
116
+ if (!pkg.name?.startsWith(SCOPE)) return null
117
+ let remote = null
118
+ try {
119
+ remote = execSync(`git -C ${join(dir, d.name)} remote get-url origin`, {
120
+ encoding: 'utf8',
121
+ stdio: ['ignore', 'pipe', 'ignore'],
122
+ }).trim()
123
+ } catch {
124
+ /* no git */
125
+ }
126
+ return {
127
+ shortName: shortName(pkg.name),
128
+ description: pkg.description ?? '',
129
+ stars: 0,
130
+ htmlUrl: pkg.homepage ?? pkg.repository?.url ?? null,
131
+ org: 'workspace',
132
+ kind,
133
+ localPath: join(KINDS[kind].workspaceDir, d.name),
134
+ remote,
135
+ version: pkg.version,
136
+ }
137
+ } catch {
138
+ return null
139
+ }
140
+ })
141
+ .filter(Boolean)
142
+ }
143
+
144
+ export async function loadCatalog(workspaceRoot, settings) {
145
+ const cached = readCache(workspaceRoot)
146
+ let remote = cached?.entries ?? null
147
+ let cacheAge = cached?.cacheAge ?? null
148
+
149
+ if (!remote) {
150
+ remote = []
151
+ const orgs = [...new Set([...(settings.githubOrgs ?? ['owdproject']), settings.githubUser].filter(Boolean))]
152
+
153
+ for (const org of orgs) {
154
+ for (const kind of Object.keys(KINDS)) {
155
+ try {
156
+ const items = await fetchTopic(KINDS[kind].topic, org)
157
+ remote.push(...items)
158
+ } catch {
159
+ /* offline — use local only */
160
+ }
161
+ }
162
+ }
163
+
164
+ if (settings.githubUser && !orgs.includes(settings.githubUser)) {
165
+ try {
166
+ remote.push(...(await fetchUserRepos(settings.githubUser)))
167
+ } catch {
168
+ /* ignore */
169
+ }
170
+ }
171
+
172
+ if (remote.length) {
173
+ const at = Date.now()
174
+ writeCache(workspaceRoot, remote)
175
+ cacheAge = formatCacheAge(at)
176
+ }
177
+ }
178
+
179
+ const byName = new Map()
180
+
181
+ for (const item of remote) {
182
+ byName.set(item.shortName, { ...item, sources: [item.org] })
183
+ }
184
+
185
+ for (const kind of Object.keys(KINDS)) {
186
+ for (const item of scanLocal(workspaceRoot, kind)) {
187
+ const prev = byName.get(item.shortName)
188
+ if (prev) {
189
+ byName.set(item.shortName, {
190
+ ...prev,
191
+ ...item,
192
+ localPath: item.localPath,
193
+ remote: item.remote ?? prev.htmlUrl,
194
+ sources: [...new Set([...(prev.sources ?? []), 'workspace'])],
195
+ })
196
+ } else {
197
+ byName.set(item.shortName, { ...item, sources: ['workspace'] })
198
+ }
199
+ }
200
+ }
201
+
202
+ const entries = [...byName.values()].map((entry) => ({
203
+ ...entry,
204
+ name: fullName(entry.shortName),
205
+ kind: entry.kind ?? inferKind(entry.shortName),
206
+ }))
207
+
208
+ return {
209
+ entries,
210
+ cacheAge: cacheAge ?? 'live',
211
+ }
212
+ }
213
+
214
+ export function filterCatalog(catalog, kind) {
215
+ return catalog
216
+ .filter((e) => e.kind === kind)
217
+ .filter((e) => kind !== 'module' || isInstallableDesktopModule(e.name))
218
+ .sort((a, b) => {
219
+ if (a.org === 'workspace' && b.org !== 'workspace') return -1
220
+ if (b.org === 'workspace' && a.org !== 'workspace') return 1
221
+ return (b.stars ?? 0) - (a.stars ?? 0) || a.shortName.localeCompare(b.shortName)
222
+ })
223
+ }
224
+
225
+ export function mergeInstalled(catalog, config, deps, workspaceRoot) {
226
+ const installedApps = new Set(config.apps ?? [])
227
+ const installedModules = new Set(config.modules ?? [])
228
+ const activeTheme = config.theme
229
+
230
+ return catalog.map((entry) => {
231
+ let installed = false
232
+ if (entry.kind === 'app') installed = installedApps.has(entry.name)
233
+ else if (entry.kind === 'module') installed = installedModules.has(entry.name)
234
+ else if (entry.kind === 'theme') installed = activeTheme === entry.name
235
+
236
+ const localSource = workspaceRoot
237
+ ? hasLocalWorkspaceSource(workspaceRoot, entry.name)
238
+ : entry.org === 'workspace'
239
+
240
+ return {
241
+ ...entry,
242
+ installed,
243
+ localSource,
244
+ inPackageJson: deps.includes(entry.name),
245
+ activeTheme: entry.kind === 'theme' && activeTheme === entry.name,
246
+ }
247
+ })
248
+ }
249
+
250
+ export function readThemeMeta(workspaceRoot, themePkgName) {
251
+ const short = shortName(themePkgName)
252
+ const localDir = join(workspaceRoot, 'themes', short)
253
+ const pkgPath = join(localDir, 'package.json')
254
+
255
+ if (!existsSync(pkgPath)) {
256
+ return {
257
+ name: themePkgName,
258
+ description: 'Theme package not in workspace',
259
+ version: '—',
260
+ author: '—',
261
+ remote: null,
262
+ license: '—',
263
+ }
264
+ }
265
+
266
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
267
+ let remote = null
268
+ try {
269
+ remote = execSync(`git -C ${localDir} remote get-url origin`, {
270
+ encoding: 'utf8',
271
+ stdio: ['ignore', 'pipe', 'ignore'],
272
+ }).trim()
273
+ } catch {
274
+ remote = pkg.repository?.url ?? pkg.homepage ?? null
275
+ }
276
+
277
+ const author =
278
+ typeof pkg.author === 'string'
279
+ ? pkg.author
280
+ : pkg.author?.name
281
+ ? `${pkg.author.name}${pkg.author.url ? ` · ${pkg.author.url}` : ''}`
282
+ : '—'
283
+
284
+ return {
285
+ name: pkg.name,
286
+ description: pkg.description ?? '',
287
+ version: pkg.version ?? '—',
288
+ author,
289
+ license: pkg.license ?? 'MIT',
290
+ remote,
291
+ homepage: pkg.homepage ?? null,
292
+ }
293
+ }
@@ -0,0 +1,109 @@
1
+ import { readFileSync, existsSync, copyFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { join } from 'node:path'
4
+ import { SCOPE } from './workspace.js'
5
+ import { LEGACY_CONFIG_DEPRECATION } from './desktopConfig.js'
6
+
7
+ const require = createRequire(import.meta.url)
8
+
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
+ const { Project, SyntaxKind } = require('ts-morph')
18
+ return { Project, SyntaxKind }
19
+ }
20
+
21
+ function getConfigObject(sourceFile, SyntaxKind) {
22
+ const exportDefault = sourceFile.getExportAssignmentOrThrow((exp) => !exp.isExportEquals())
23
+ const callExpr = exportDefault.getExpressionIfKindOrThrow(SyntaxKind.CallExpression)
24
+ const args = callExpr.getArguments()
25
+ if (args.length === 0) throw new Error('defineDesktopConfig({}) is empty')
26
+ return args[0].asKindOrThrow(SyntaxKind.ObjectLiteralExpression)
27
+ }
28
+
29
+ function readArrayProperty(configObj, key, SyntaxKind) {
30
+ const prop = configObj.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment)
31
+ if (!prop) return []
32
+ const init = prop.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression)
33
+ if (!init) return []
34
+ return init
35
+ .getElements()
36
+ .map((el) => el.getText().trim().replace(/['"`]/g, ''))
37
+ .filter(Boolean)
38
+ }
39
+
40
+ function readTheme(configObj, SyntaxKind) {
41
+ const prop = configObj.getProperty('theme')?.asKind(SyntaxKind.PropertyAssignment)
42
+ if (!prop) return null
43
+ return prop.getInitializer()?.getText().trim().replace(/['"`]/g, '') ?? null
44
+ }
45
+
46
+ function setArrayProperty(configObj, key, values, SyntaxKind) {
47
+ const quoted = values.map((v) => `'${v}'`).join(', ')
48
+ const initializer = `[ ${quoted} ]`
49
+ const existing = configObj.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment)
50
+ if (existing) {
51
+ existing.setInitializer(initializer)
52
+ } else {
53
+ configObj.addPropertyAssignment({ name: key, initializer })
54
+ }
55
+ }
56
+
57
+ export function readDesktopConfig(configPath, workspaceRoot) {
58
+ const { Project, SyntaxKind } = loadTsMorph(workspaceRoot)
59
+ const project = new Project()
60
+ const sourceFile = project.addSourceFileAtPath(configPath)
61
+ const configObj = getConfigObject(sourceFile, SyntaxKind)
62
+
63
+ return {
64
+ theme: readTheme(configObj, SyntaxKind),
65
+ apps: readArrayProperty(configObj, 'apps', SyntaxKind),
66
+ modules: readArrayProperty(configObj, 'modules', SyntaxKind),
67
+ }
68
+ }
69
+
70
+ /**
71
+ * When saving from the TUI, always target desktop.config.ts (migrate from legacy on first save).
72
+ *
73
+ * @param {{ config: string, configWrite: string }} paths
74
+ */
75
+ export function resolveConfigPathForWrite(paths) {
76
+ if (paths.config === paths.configWrite) {
77
+ return paths.configWrite
78
+ }
79
+ if (!existsSync(paths.configWrite) && existsSync(paths.config)) {
80
+ copyFileSync(paths.config, paths.configWrite)
81
+ console.warn(LEGACY_CONFIG_DEPRECATION)
82
+ }
83
+ return paths.configWrite
84
+ }
85
+
86
+ export function writeDesktopConfig(configPath, workspaceRoot, state) {
87
+ const { Project, SyntaxKind } = loadTsMorph(workspaceRoot)
88
+ const project = new Project()
89
+ const sourceFile = project.addSourceFileAtPath(configPath)
90
+ const configObj = getConfigObject(sourceFile, SyntaxKind)
91
+
92
+ if (state.theme) {
93
+ const themeProp = configObj.getProperty('theme')?.asKind(SyntaxKind.PropertyAssignment)
94
+ if (themeProp) themeProp.setInitializer(`'${state.theme}'`)
95
+ else configObj.addPropertyAssignment({ name: 'theme', initializer: `'${state.theme}'` })
96
+ }
97
+
98
+ if (state.apps) setArrayProperty(configObj, 'apps', state.apps, SyntaxKind)
99
+ if (state.modules) setArrayProperty(configObj, 'modules', state.modules, SyntaxKind)
100
+
101
+ sourceFile.saveSync()
102
+ }
103
+
104
+ export function readDesktopDependencies(packageJsonPath) {
105
+ if (!existsSync(packageJsonPath)) return []
106
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
107
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
108
+ return Object.keys(deps).filter((name) => name.startsWith(SCOPE))
109
+ }
@@ -0,0 +1,56 @@
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
+ /**
11
+ * Resolve the desktop config file under a Nuxt project root (or desktop/ folder).
12
+ *
13
+ * @param {string} rootDir
14
+ * @returns {{ path: string, file: string, legacy: boolean } | null}
15
+ */
16
+ export function resolveDesktopConfigPath(rootDir) {
17
+ const desktopPath = join(rootDir, DESKTOP_CONFIG_FILENAME)
18
+ const legacyPath = join(rootDir, LEGACY_DESKTOP_CONFIG_FILENAME)
19
+ const hasDesktop = existsSync(desktopPath)
20
+ const hasLegacy = existsSync(legacyPath)
21
+
22
+ if (hasDesktop && hasLegacy) {
23
+ console.warn(
24
+ '[@owdproject/core] Both desktop.config.ts and owd.config.ts exist; using desktop.config.ts. Remove owd.config.ts.',
25
+ )
26
+ return { path: desktopPath, file: DESKTOP_CONFIG_FILENAME, legacy: false }
27
+ }
28
+
29
+ if (hasDesktop) {
30
+ return { path: desktopPath, file: DESKTOP_CONFIG_FILENAME, legacy: false }
31
+ }
32
+
33
+ if (hasLegacy) {
34
+ return { path: legacyPath, file: LEGACY_DESKTOP_CONFIG_FILENAME, legacy: true }
35
+ }
36
+
37
+ return null
38
+ }
39
+
40
+ /**
41
+ * @param {{ legacy?: boolean, file?: string } | null | undefined} resolved
42
+ */
43
+ export function warnLegacyDesktopConfig(resolved) {
44
+ if (resolved?.legacy) {
45
+ console.warn(LEGACY_CONFIG_DEPRECATION)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Path used when creating or updating config from the CLI (always the new filename).
51
+ *
52
+ * @param {string} desktopDir
53
+ */
54
+ export function desktopConfigWritePath(desktopDir) {
55
+ return join(desktopDir, DESKTOP_CONFIG_FILENAME)
56
+ }