Skip to content

Commit 62e9784

Browse files
dxllivcursoragent
andcommitted
feat(cp): add Go Bubble Tea control panel with Node bridge
Replace the legacy Node CP entry with a Go TUI that builds on demand. Add bridge.js and config helpers so the TUI can read/write desktop.config and package dependencies. Ignore the local desktop-go binary artifact. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 11828b1 commit 62e9784

14 files changed

Lines changed: 3225 additions & 44 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
node_modules
77

88
# Other
9-
package-lock.json
9+
package-lock.json
10+
11+
# Go control panel binary (built by `go build -o desktop-go .`)
12+
go/desktop-go

bin/bridge.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env node
2+
3+
import { writeFileSync, readFileSync } from 'node:fs'
4+
import {
5+
findWorkspaceRoot,
6+
loadSettings,
7+
desktopPaths,
8+
saveSettings
9+
} from './lib/workspace.js'
10+
import {
11+
readDesktopConfig,
12+
writeDesktopConfig,
13+
readDesktopDependencies,
14+
writeDesktopDependencies
15+
} from './lib/config.js'
16+
import { loadCatalog, mergeInstalled } from './lib/catalog.js'
17+
18+
function getWorkspaceContext() {
19+
const workspaceRoot = findWorkspaceRoot()
20+
if (!workspaceRoot) {
21+
throw new Error('Not inside an OWD workspace')
22+
}
23+
const settings = loadSettings(workspaceRoot)
24+
const paths = desktopPaths(workspaceRoot)
25+
let config = { theme: null, apps: [], modules: [] }
26+
try {
27+
config = readDesktopConfig(paths.config, workspaceRoot)
28+
} catch (err) {
29+
console.error('Config load error:', err)
30+
}
31+
const deps = readDesktopDependencies(paths.packageJson)
32+
return { workspaceRoot, settings, paths, config, deps }
33+
}
34+
35+
async function main() {
36+
const args = process.argv.slice(2)
37+
const cmd = args[0]
38+
39+
try {
40+
if (cmd === '--read') {
41+
const ctx = getWorkspaceContext()
42+
console.log(JSON.stringify({
43+
workspaceRoot: ctx.workspaceRoot,
44+
settings: ctx.settings,
45+
config: ctx.config,
46+
deps: ctx.deps,
47+
}, null, 2))
48+
} else if (cmd === '--catalog') {
49+
const ctx = getWorkspaceContext()
50+
const force = args.includes('--force')
51+
const catalog = await loadCatalog(ctx.workspaceRoot, ctx.settings, { force })
52+
catalog.entries = mergeInstalled(catalog.entries, ctx.config, ctx.deps, ctx.workspaceRoot)
53+
console.log(JSON.stringify(catalog, null, 2))
54+
} else if (cmd === '--write') {
55+
const ctx = getWorkspaceContext()
56+
57+
// Read JSON payload from stdin
58+
let input = ''
59+
for await (const chunk of process.stdin) {
60+
input += chunk
61+
}
62+
63+
const payload = JSON.parse(input)
64+
// payload = { config: { theme, apps, modules }, depsToAdd: { name: version }, depsToRemove: [name], settings?: { ... } }
65+
66+
if (payload.config) {
67+
writeDesktopConfig(ctx.paths.config, ctx.workspaceRoot, payload.config)
68+
}
69+
70+
if (payload.depsToAdd || payload.depsToRemove) {
71+
writeDesktopDependencies(
72+
ctx.paths.packageJson,
73+
payload.depsToAdd || {},
74+
payload.depsToRemove || []
75+
)
76+
}
77+
78+
if (payload.settings) {
79+
saveSettings(ctx.workspaceRoot, payload.settings)
80+
}
81+
82+
console.log(JSON.stringify({ success: true }))
83+
} else {
84+
console.error('Usage: node bridge.js [--read|--catalog [--force]|--write]')
85+
process.exit(1)
86+
}
87+
} catch (err) {
88+
console.error(JSON.stringify({ error: err.message }))
89+
process.exit(1)
90+
}
91+
}
92+
93+
main()

bin/cli.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { spawn } from 'node:child_process'
1+
import { spawn, execSync } from 'node:child_process'
2+
import { fileURLToPath } from 'node:url'
23
import { runDevForeground } from './lib/status.js'
34
import { existsSync } from 'node:fs'
45
import { join } from 'node:path'
@@ -132,8 +133,29 @@ LEGACY (still supported)
132133
}
133134

134135
async function openControlPanel(name) {
135-
const { runCp } = await import('./cp.js')
136-
await runCp(name)
136+
const cliDir = join(fileURLToPath(import.meta.url), '..')
137+
const goDir = join(cliDir, '../go')
138+
const binaryPath = join(goDir, 'desktop-go')
139+
140+
// Compile the Go Control Panel if binary doesn't exist
141+
if (!existsSync(binaryPath)) {
142+
console.log('Go Control Panel binary not found. Compiling...')
143+
try {
144+
execSync('go build -o desktop-go .', { cwd: goDir, stdio: 'inherit' })
145+
console.log('Go Control Panel compiled successfully!')
146+
} catch (err) {
147+
console.warn('Failed to compile Go Control Panel, falling back to legacy CP:', err.message)
148+
const { runCp } = await import('./cp.js')
149+
await runCp(name)
150+
return
151+
}
152+
}
153+
154+
// Execute Go TUI
155+
const child = spawn(binaryPath, [], { stdio: 'inherit' })
156+
child.on('exit', (code) => {
157+
process.exit(code ?? 0)
158+
})
137159
}
138160

139161
function fail(message, hint) {

bin/lib/catalog.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,9 @@ export async function loadCatalog(workspaceRoot, settings, options = {}) {
254254
const previousNames = readPreviousCatalogNames(workspaceRoot)
255255
const newDays = settings.catalogNewDays ?? 14
256256

257-
const cached = options.force ? null : readCache(workspaceRoot)
257+
const cached = options.force ? null : readCacheFile(workspaceRoot)
258258
let remote = cached?.entries ?? null
259-
let cacheAge = cached?.cacheAge ?? null
259+
let cacheAge = cached?.fetchedAt ? formatCacheAge(cached.fetchedAt) : null
260260

261261
if (!remote) {
262262
remote = []

bin/lib/config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,24 @@ export function readDesktopDependencies(packageJsonPath) {
107107
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
108108
return Object.keys(deps).filter((name) => name.startsWith(SCOPE))
109109
}
110+
111+
export function writeDesktopDependencies(packageJsonPath, toAdd, toRemove) {
112+
if (!existsSync(packageJsonPath)) return
113+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
114+
if (!pkg.dependencies) pkg.dependencies = {}
115+
116+
// Add dependencies
117+
for (const [name, version] of Object.entries(toAdd)) {
118+
pkg.dependencies[name] = version
119+
}
120+
121+
// Remove dependencies
122+
for (const name of toRemove) {
123+
delete pkg.dependencies[name]
124+
if (pkg.devDependencies) {
125+
delete pkg.devDependencies[name]
126+
}
127+
}
128+
129+
writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n')
130+
}

bin/lib/migrateModulePlayground.config.mjs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ export const configs = [
104104
fsMounts: { '/mnt/test': '/test.zip' },
105105
copyTestZip: true,
106106
playgroundExtraDevDeps: {
107-
'@owdproject/module-fs': '^0.0.4',
108-
'@owdproject/module-persistence': '^0.0.2',
107+
'@owdproject/module-fs': 'workspace:*',
108+
'@owdproject/module-persistence': 'workspace:*',
109109
},
110110
launch: {
111111
file: 'launch-audioplayer.client.ts',
@@ -143,8 +143,8 @@ export const configs = [
143143
fsMounts: { '/mnt/test': '/test.zip' },
144144
copyTestZip: true,
145145
playgroundExtraDevDeps: {
146-
'@owdproject/module-fs': '^0.0.4',
147-
'@owdproject/module-persistence': '^0.0.2',
146+
'@owdproject/module-fs': 'workspace:*',
147+
'@owdproject/module-persistence': 'workspace:*',
148148
},
149149
dependencies: {
150150
},
@@ -157,4 +157,31 @@ export const configs = [
157157
},
158158
pagesYml: true,
159159
},
160+
{
161+
path: 'apps/app-atproto',
162+
slug: 'app-atproto',
163+
name: '@owdproject/app-atproto',
164+
moduleName: 'desktop-app-atproto',
165+
version: '0.0.3',
166+
playgroundTheme: '@owdproject/theme-win95',
167+
playgroundModules: ['@owdproject/module-atproto'],
168+
playgroundApps: ['@owdproject/app-terminal', '@owdproject/app-atproto'],
169+
dependencies: {
170+
'@owdproject/kit-tailwind': 'workspace:*',
171+
'@owdproject/module-atproto': 'workspace:*',
172+
},
173+
playgroundExtraDevDeps: {
174+
'@owdproject/module-atproto': 'workspace:*',
175+
'@owdproject/theme-win95': 'workspace:*',
176+
'@owdproject/app-terminal': 'workspace:*',
177+
},
178+
launch: {
179+
file: 'launch-atproto.client.ts',
180+
pluginName: 'app-atproto-playground-launch',
181+
dependsOn: 'desktop-app-atproto-register',
182+
appId: 'org.owdproject.atproto',
183+
command: 'atproto',
184+
},
185+
pagesYml: true,
186+
},
160187
]

bin/lib/migrateModulePlayground.mjs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ function buildPackageJson(cfg, old) {
126126
'@nuxt/module-builder': '^1.0.2',
127127
'@nuxt/schema': '^4.3.0',
128128
'@owdproject/core': 'workspace:*',
129-
'@owdproject/theme-nova': '0.0.1',
129+
'@owdproject/theme-nova': 'workspace:*',
130130
nuxt: '^4.3.0',
131131
typescript: '~5.9.3',
132132
...(cfg.devDependencies || {}),
@@ -172,7 +172,7 @@ function buildPlaygroundPkg(cfg) {
172172
nuxt: '^4.4.4',
173173
...(cfg.playgroundExtraDevDeps || {}),
174174
}
175-
if (!cfg.playgroundTheme) deps['@owdproject/theme-nova'] = '0.0.1'
175+
if (!cfg.playgroundTheme) deps['@owdproject/theme-nova'] = 'workspace:*'
176176
if (cfg.playgroundTheme) deps[cfg.playgroundTheme] = 'workspace:*'
177177
return {
178178
name: `${cfg.name}-playground`,
@@ -253,10 +253,15 @@ jobs:
253253

254254
function migrate(cfg) {
255255
const dir = join(ROOT, cfg.path)
256+
const pkgPath = join(dir, 'package.json')
257+
if (!existsSync(pkgPath)) {
258+
console.warn('Skipping path without package.json:', cfg.path)
259+
return
260+
}
256261
const src = join(dir, 'src')
257262
mkdirSync(src, { recursive: true })
258-
if (existsSync(join(dir, 'module.ts'))) renameSync(join(dir, 'module.ts'), join(src, 'module.ts'))
259-
if (existsSync(join(dir, 'runtime'))) renameSync(join(dir, 'runtime'), join(src, 'runtime'))
263+
if (existsSync(join(dir, 'module.ts')) && !existsSync(join(src, 'module.ts'))) renameSync(join(dir, 'module.ts'), join(src, 'module.ts'))
264+
if (existsSync(join(dir, 'runtime')) && !existsSync(join(src, 'runtime'))) renameSync(join(dir, 'runtime'), join(src, 'runtime'))
260265

261266
const registerName = `${cfg.moduleName}-register`
262267
const pluginPath = join(src, 'runtime/plugin.ts')

bin/lib/packageSources.js

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,22 @@ import { desktopMetaDir, fullName, SCOPE, KINDS, inferKind } from './workspace.j
99
* @param {string} pkgShortName
1010
*/
1111
export async function resolveForkUser(githubUser, pkgShortName) {
12-
if (!githubUser || githubUser === 'owdproject') return 'owdproject'
12+
if (!githubUser || githubUser === 'owdproject') return { exists: false, isFork: false }
1313

1414
try {
1515
const res = await fetch(`https://api.github.com/repos/${githubUser}/${pkgShortName}`, {
1616
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'owd-desktop-cli' },
1717
})
18-
if (res.ok) return githubUser
18+
if (res.ok) {
19+
const data = await res.json()
20+
// data.fork === true only if this repo was forked FROM another repo
21+
return { exists: true, isFork: data.fork === true }
22+
}
1923
} catch {
2024
/* ignore */
2125
}
2226

23-
return 'owdproject'
27+
return { exists: false, isFork: false }
2428
}
2529

2630
/**
@@ -205,9 +209,11 @@ export async function resolvePackageSources(shortName, settings, workspaceRoot,
205209

206210
let forkOwner = settings.githubUser && settings.githubUser !== 'owdproject' ? settings.githubUser : null
207211
let forkExists = false
212+
let forkIsActualFork = false
208213
if (forkOwner) {
209-
const resolved = await resolveForkUser(forkOwner, shortName)
210-
forkExists = resolved === forkOwner
214+
const result = await resolveForkUser(forkOwner, shortName)
215+
forkExists = result.exists
216+
forkIsActualFork = result.isFork
211217
}
212218

213219
const local = workspaceRoot && hasLocalWorkspaceSource(workspaceRoot, pkgName)
@@ -222,8 +228,8 @@ export async function resolvePackageSources(shortName, settings, workspaceRoot,
222228
htmlUrl: officialHtml,
223229
exists: Boolean(catalogEntry.htmlUrl || officialOwner),
224230
},
225-
fork: forkOwner
226-
? { owner: forkOwner, exists: forkExists, htmlUrl: githubHtmlUrl(forkOwner, shortName) }
231+
fork: forkOwner && forkExists
232+
? { owner: forkOwner, exists: forkExists, isFork: forkIsActualFork, htmlUrl: githubHtmlUrl(forkOwner, shortName) }
227233
: null,
228234
},
229235
local: local ? true : false,
@@ -325,29 +331,10 @@ export function buildSourceOptions(metadata, settings, sshAuth = {}) {
325331
*/
326332
export async function enrichCatalogEntries(entries, workspaceRoot, settings) {
327333
const cache = readCacheFile(workspaceRoot)
328-
const sshAuth = await detectGithubSshAuth(workspaceRoot)
329-
const shortNames = entries.map((e) => e.shortName)
330-
const missingNpm = shortNames.filter((s) => !cache?.packages?.[s]?.npm)
331-
332-
if (missingNpm.length > 0) {
333-
const packages = { ...(cache?.packages ?? {}) }
334-
for (const shortName of missingNpm.slice(0, 40)) {
335-
const version = fetchLatestVersion(fullName(shortName), { optional: true })
336-
packages[shortName] = {
337-
...(packages[shortName] ?? {}),
338-
npm: version ? { version } : null,
339-
}
340-
}
341-
writeCacheFile(workspaceRoot, {
342-
fetchedAt: Date.now(),
343-
sshAuth: cache?.sshAuth ?? sshAuth,
344-
packages,
345-
})
346-
}
334+
const sshAuth = getCachedSshAuth(workspaceRoot) ?? { available: false }
347335

348-
const freshCache = readCacheFile(workspaceRoot)
349336
return entries.map((entry) => {
350-
const pkgMeta = freshCache?.packages?.[entry.shortName]
337+
const pkgMeta = cache?.packages?.[entry.shortName]
351338
return {
352339
...entry,
353340
trusted: isTrustedPublisher(entry, settings),
@@ -358,6 +345,7 @@ export async function enrichCatalogEntries(entries, workspaceRoot, settings) {
358345
owner: entry.org && entry.org !== 'workspace' ? entry.org : 'owdproject',
359346
htmlUrl: entry.htmlUrl ?? githubHtmlUrl(entry.org ?? 'owdproject', entry.shortName),
360347
},
348+
fork: pkgMeta?.github?.fork ?? null,
361349
},
362350
},
363351
}

0 commit comments

Comments
 (0)