@owdproject/cli 0.0.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +50 -16
  3. package/bin/bridge.js +129 -0
  4. package/bin/cli.js +491 -0
  5. package/bin/cp.js +3542 -0
  6. package/bin/desktop.js +5 -0
  7. package/bin/lib/catalog.js +504 -0
  8. package/bin/lib/config.js +173 -0
  9. package/bin/lib/cpAscii.js +39 -0
  10. package/bin/lib/cpFormat.js +343 -0
  11. package/bin/lib/create.js +246 -0
  12. package/bin/lib/desktopConfig.js +48 -0
  13. package/bin/lib/generateTemplate.js +427 -0
  14. package/bin/lib/install.js +521 -0
  15. package/bin/lib/installWizardFlow.js +792 -0
  16. package/bin/lib/migrateModulePlayground.config.mjs +187 -0
  17. package/bin/lib/migrateModulePlayground.mjs +318 -0
  18. package/bin/lib/npmVersions.js +124 -0
  19. package/bin/lib/packageSources.js +375 -0
  20. package/bin/lib/playgroundContext.js +188 -0
  21. package/bin/lib/scaffold.js +205 -0
  22. package/bin/lib/status.js +319 -0
  23. package/bin/lib/validateModule.js +978 -0
  24. package/bin/lib/workspace.js +289 -0
  25. package/bin/owd.js +2 -118
  26. package/go/bridge/bridge.go +220 -0
  27. package/go/desktop-go +0 -0
  28. package/go/go.mod +33 -0
  29. package/go/go.sum +52 -0
  30. package/go/main.go +34 -0
  31. package/go/owd-cli +0 -0
  32. package/go/tui/engine.go +472 -0
  33. package/go/tui/engine_test.go +40 -0
  34. package/go/tui/handlers.go +604 -0
  35. package/go/tui/helpers.go +916 -0
  36. package/go/tui/helpers_test.go +97 -0
  37. package/go/tui/modals.go +423 -0
  38. package/go/tui/resolver.go +377 -0
  39. package/go/tui/resolver_test.go +86 -0
  40. package/go/tui/styles.go +153 -0
  41. package/go/tui/tasks.go +850 -0
  42. package/go/tui/tui.go +869 -0
  43. package/go/tui/tui_test.go +69 -0
  44. package/go/tui/types.go +230 -0
  45. package/go/tui/view.go +812 -0
  46. package/go/tui/workqueue.go +179 -0
  47. package/go/tui/workqueue_test.go +90 -0
  48. package/package.json +33 -22
  49. package/src/commands/create.js +0 -11
  50. package/src/commands/generate.js +0 -12
  51. package/src/commands/installApp.js +0 -9
  52. package/src/commands/installTheme.js +0 -9
  53. package/src/commands/start.js +0 -13
  54. package/src/utils/cloneRepo.js +0 -25
  55. package/src/utils/installPackage.js +0 -15
  56. package/src/utils/logging.js +0 -15
@@ -0,0 +1,289 @@
1
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+ import { execSync } from 'node:child_process'
4
+ import {
5
+ resolveDesktopConfigPath,
6
+ desktopConfigWritePath,
7
+ } from './desktopConfig.js'
8
+
9
+ export const SCOPE = '@owdproject/'
10
+
11
+ /** Not loadable via desktop.config `modules` (core is the shell). */
12
+ export const DESKTOP_NON_INSTALLABLE = new Set([
13
+ '@owdproject/core',
14
+ '@owdproject/cli',
15
+ '@owdproject/nx',
16
+ ])
17
+
18
+ /** Installed documentation modules (current + deprecated package name). */
19
+ export const DOCS_MODULE_PACKAGES = [
20
+ '@owdproject/module-docs',
21
+ '@owdproject/docs',
22
+ ]
23
+
24
+ export function hasDocsModuleInstalled(config, deps = {}) {
25
+ return DOCS_MODULE_PACKAGES.some(
26
+ (name) => config?.modules?.includes(name) || deps[name],
27
+ )
28
+ }
29
+
30
+ export function docsBasePathFromConfig(config) {
31
+ const base = config?.docs?.basePath
32
+ if (typeof base === 'string' && base.startsWith('/')) return base
33
+ return '/docs'
34
+ }
35
+
36
+ /** Theme-internal UI kits — pulled in by themes, not listed as desktop modules. */
37
+ export function isDesktopKitPackage(pkgName) {
38
+ const isScoped = pkgName.startsWith(SCOPE)
39
+ const short = isScoped ? pkgName.slice(SCOPE.length) : pkgName
40
+ if (!isScoped && !pkgName.startsWith('kit-')) return false
41
+ return short.startsWith('kit-')
42
+ }
43
+
44
+ /** Scaffold / blueprint packages — not loadable desktop modules. */
45
+ export function isDesktopTemplatePackage(pkgName) {
46
+ const isScoped = pkgName.startsWith(SCOPE)
47
+ const short = isScoped ? pkgName.slice(SCOPE.length) : pkgName
48
+ if (!isScoped && !pkgName.endsWith('-template')) return false
49
+ return short.endsWith('-template')
50
+ }
51
+
52
+ export function isInstallableDesktopModule(pkgName) {
53
+ const isScoped = pkgName.startsWith(SCOPE)
54
+ if (!isScoped && !pkgName.startsWith('module-')) return false
55
+ return (
56
+ !DESKTOP_NON_INSTALLABLE.has(pkgName) &&
57
+ !isDesktopKitPackage(pkgName) &&
58
+ !isDesktopTemplatePackage(pkgName)
59
+ )
60
+ }
61
+
62
+ export const KINDS = {
63
+ app: {
64
+ label: 'Apps',
65
+ topic: 'owd-apps',
66
+ workspaceDir: 'apps',
67
+ configKey: 'apps',
68
+ nx: 'desktop:install-app',
69
+ },
70
+ module: {
71
+ label: 'Modules',
72
+ topic: 'owd-modules',
73
+ workspaceDir: 'packages',
74
+ configKey: 'modules',
75
+ nx: 'desktop:install-module',
76
+ },
77
+ theme: {
78
+ label: 'Themes',
79
+ topic: 'owd-themes',
80
+ workspaceDir: 'themes',
81
+ configKey: 'theme',
82
+ nx: 'desktop:install-theme',
83
+ },
84
+ }
85
+
86
+ export function findWorkspaceRoot(startDir = process.cwd()) {
87
+ let dir = startDir
88
+ for (;;) {
89
+ if (
90
+ existsSync(join(dir, 'pnpm-workspace.yaml')) &&
91
+ existsSync(join(dir, 'nx.json'))
92
+ ) {
93
+ return dir
94
+ }
95
+ const parent = dirname(dir)
96
+ if (parent === dir) return null
97
+ dir = parent
98
+ }
99
+ }
100
+
101
+ export function desktopMetaDir(workspaceRoot) {
102
+ return join(workspaceRoot, '.desktop')
103
+ }
104
+
105
+ export function settingsPath(workspaceRoot) {
106
+ return join(desktopMetaDir(workspaceRoot), 'settings.json')
107
+ }
108
+
109
+ /** @typedef {'npm' | 'workspace'} InstallMode */
110
+
111
+ export function normalizeInstallMode(value) {
112
+ if (value === 'workspace' || value === 'dev') return 'workspace'
113
+ return 'npm'
114
+ }
115
+
116
+ export function isWorkspaceInstallMode(settings) {
117
+ return normalizeInstallMode(settings?.installMode) === 'workspace'
118
+ }
119
+
120
+ /** @typedef {'updated' | 'name' | 'stars' | 'installed'} CatalogSortMode */
121
+
122
+ export function normalizeCatalogSort(value) {
123
+ const modes = ['updated', 'name', 'stars', 'installed']
124
+ return modes.includes(value) ? /** @type {CatalogSortMode} */ (value) : 'updated'
125
+ }
126
+
127
+ export function loadSettings(workspaceRoot, metaDir = null) {
128
+ const defaults = {
129
+ devPort: 3000,
130
+ githubUser: null,
131
+ githubOrgs: ['owdproject'],
132
+ installMode: /** @type {InstallMode} */ ('npm'),
133
+ catalogSort: /** @type {CatalogSortMode} */ ('updated'),
134
+ catalogNewDays: 14,
135
+ }
136
+
137
+ try {
138
+ const path = metaDir ? join(metaDir, 'settings.json') : settingsPath(workspaceRoot)
139
+ if (!existsSync(path)) {
140
+ return { ...defaults, ...detectGithubUser(), installMode: 'npm' }
141
+ }
142
+ const merged = { ...defaults, ...detectGithubUser(), ...JSON.parse(readFileSync(path, 'utf8')) }
143
+ merged.installMode = normalizeInstallMode(merged.installMode)
144
+ merged.catalogSort = normalizeCatalogSort(merged.catalogSort)
145
+ merged.catalogNewDays =
146
+ typeof merged.catalogNewDays === 'number' && merged.catalogNewDays > 0
147
+ ? merged.catalogNewDays
148
+ : 14
149
+ return merged
150
+ } catch {
151
+ return { ...defaults, ...detectGithubUser(), installMode: 'npm' }
152
+ }
153
+ }
154
+
155
+ function detectGithubUser() {
156
+ const fromEnv = process.env.OWD_GITHUB_USER?.trim()
157
+ if (fromEnv) return { githubUser: fromEnv }
158
+
159
+ try {
160
+ const user = execSync('git config --get github.user || git config --get user.username', {
161
+ encoding: 'utf8',
162
+ stdio: ['ignore', 'pipe', 'ignore'],
163
+ }).trim()
164
+ if (user) return { githubUser: user }
165
+ } catch {
166
+ /* ignore */
167
+ }
168
+
169
+ return {}
170
+ }
171
+
172
+ export function saveSettings(workspaceRoot, settings, metaDir = null) {
173
+ const dir = metaDir || desktopMetaDir(workspaceRoot)
174
+ mkdirSync(dir, { recursive: true })
175
+ const path = metaDir ? join(metaDir, 'settings.json') : settingsPath(workspaceRoot)
176
+ writeFileSync(path, JSON.stringify(settings, null, 2) + '\n')
177
+ }
178
+
179
+ export function desktopPaths(workspaceRoot) {
180
+ const desktop = join(workspaceRoot, 'desktop')
181
+ const resolved = resolveDesktopConfigPath(desktop)
182
+ const configWrite = desktopConfigWritePath(desktop)
183
+
184
+ return {
185
+ desktop,
186
+ config: resolved?.path ?? configWrite,
187
+ configWrite,
188
+ configLegacy: resolved?.legacy ?? false,
189
+ packageJson: join(desktop, 'package.json'),
190
+ }
191
+ }
192
+
193
+ export function shortName(pkgName) {
194
+ return pkgName.startsWith(SCOPE) ? pkgName.slice(SCOPE.length) : pkgName
195
+ }
196
+
197
+ export function fullName(short) {
198
+ return short.startsWith(SCOPE) ? short : `${SCOPE}${short}`
199
+ }
200
+
201
+ export function inferKind(shortName) {
202
+ if (shortName.startsWith('app-')) return 'app'
203
+ if (shortName.startsWith('theme-')) return 'theme'
204
+ return 'module'
205
+ }
206
+
207
+ export function getPackagesRequiredByTheme(workspaceRoot, themeName) {
208
+ if (!themeName) return []
209
+ const themeShort = shortName(themeName)
210
+ const pkgJsonPath = join(workspaceRoot, 'themes', themeShort, 'package.json')
211
+ if (!existsSync(pkgJsonPath)) return []
212
+ try {
213
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
214
+ const themeDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies }
215
+ return Object.keys(themeDeps).filter(
216
+ (dep) => isDesktopKitPackage(dep) || isInstallableDesktopModule(dep)
217
+ )
218
+ } catch {
219
+ return []
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Scans all package.json files in the workspace (including desktop/)
225
+ * and returns any @owdproject/ dependencies that use the 'workspace:' protocol
226
+ * but are not present locally on disk.
227
+ * @param {string} workspaceRoot
228
+ * @returns {string[]}
229
+ */
230
+ export function findMissingWorkspaceDependencies(workspaceRoot) {
231
+ const missing = new Set()
232
+ const localPackages = new Set()
233
+ const pkgPaths = []
234
+
235
+ // 1. Scan KINDS subdirectories for local packages
236
+ for (const kind of Object.values(KINDS)) {
237
+ const dir = join(workspaceRoot, kind.workspaceDir)
238
+ if (!existsSync(dir)) continue
239
+ try {
240
+ const entries = readdirSync(dir, { withFileTypes: true })
241
+ for (const entry of entries) {
242
+ if (entry.isDirectory()) {
243
+ const pkgJsonPath = join(dir, entry.name, 'package.json')
244
+ if (existsSync(pkgJsonPath)) {
245
+ const pkgPath = join(dir, entry.name)
246
+ pkgPaths.push(pkgPath)
247
+ try {
248
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
249
+ if (pkgJson.name) {
250
+ localPackages.add(pkgJson.name)
251
+ }
252
+ } catch {}
253
+ }
254
+ }
255
+ }
256
+ } catch {}
257
+ }
258
+
259
+ // 2. Include the desktop app itself
260
+ const desktopDir = join(workspaceRoot, 'desktop')
261
+ if (existsSync(join(desktopDir, 'package.json'))) {
262
+ pkgPaths.push(desktopDir)
263
+ try {
264
+ const pkgJson = JSON.parse(readFileSync(join(desktopDir, 'package.json'), 'utf8'))
265
+ if (pkgJson.name) {
266
+ localPackages.add(pkgJson.name)
267
+ }
268
+ } catch {}
269
+ }
270
+
271
+ // 3. Find missing workspace:* dependencies
272
+ for (const pkgPath of pkgPaths) {
273
+ try {
274
+ const pkgJson = JSON.parse(readFileSync(join(pkgPath, 'package.json'), 'utf8'))
275
+ const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies }
276
+ for (const [depName, version] of Object.entries(deps)) {
277
+ if (depName.startsWith(SCOPE) && typeof version === 'string' && version.startsWith('workspace:')) {
278
+ if (!localPackages.has(depName)) {
279
+ missing.add(depName)
280
+ }
281
+ }
282
+ }
283
+ } catch {}
284
+ }
285
+
286
+ return Array.from(missing)
287
+ }
288
+
289
+
package/bin/owd.js CHANGED
@@ -1,121 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Command } from 'commander'
4
- import chalk from 'chalk'
5
- import gradient from 'gradient-string'
6
- import inquirer from 'inquirer'
7
- import { execSync } from 'child_process'
8
- import fs from 'fs'
9
- import path from 'path'
3
+ import { runCli } from './cli.js'
10
4
 
11
- import {cloneRepo} from '../src/utils/cloneRepo.js'
12
-
13
- const program = new Command()
14
- const owdGradient = gradient('#0080ff', '#53bdf3', '#0080ff')
15
-
16
- const logTitle = () => {
17
- console.log(owdGradient(`\n
18
- ___ __ __ _ ___ _ _
19
- /___\\_ __ ___ _ __ / / /\\ \\ \\___| |__ / \\___ ___| | _| |_ ___ _ __
20
- // // '_ \\ / _ \\ '_ \\ \\ \\/ \\/ / _ \\ '_ \\ / /\\ / _ \\/ __| |/ / __/ _ \\| '_ \\
21
- / \\_//| |_) | __/ | | | \\ /\\ / __/ |_) | / /_// __/\\__ \\ <| || (_) | |_) |
22
- \\___/ | .__/ \\___|_| |_| \\/ \\/ \\___|_.__/ /___,' \\___||___/_|\\_\\\\__\\___/| .__/
23
- |_| |_|
24
- \n`))
25
- console.log(chalk.white(summonInspiration()))
26
- console.log(chalk.white('\n──────────────────────────────────────────────────────────────────────────────\n'))
27
- console.log(chalk.white(' ➜ ') + chalk.whiteBright('npx @owdproject/cli create') + chalk.gray(' Create a new project from the template'))
28
- console.log(chalk.white(' ➜ ') + chalk.whiteBright('npx @owdproject/cli start') + chalk.gray(' Start the dev server for local development'))
29
- console.log(chalk.white(' ➜ ') + chalk.whiteBright('npx @owdproject/cli generate') + chalk.gray(' Generate project for static deployment'))
30
- console.log(chalk.white('\n──────────────────────────────────────────────────────────────────────────────\n'))
31
- }
32
-
33
- const logServerInfo = () => {
34
- console.log(`\n ${chalk.red.bold('➜ Local:')} ${chalk.white('http://localhost:3000/')}`)
35
- console.log(` ${chalk.red.bold('➜ Network:')} ${chalk.white('http://localhost:3000/')} \n`)
36
- }
37
-
38
- program.command('create')
39
- .description('Create a new project')
40
- .action(async () => {
41
- logTitle()
42
- console.log(chalk.green('✨ Creating new project\n'))
43
-
44
- const { projectDir } = await inquirer.prompt([
45
- {
46
- type: 'input',
47
- name: 'projectDir',
48
- message: 'Enter the directory name for your new project:',
49
- default: 'owd-client'
50
- }
51
- ])
52
-
53
- const targetDir = path.join(process.cwd(), projectDir)
54
-
55
- if (fs.existsSync(targetDir)) {
56
- console.log(chalk.red(`Directory ${projectDir} already exists!`))
57
- return
58
- }
59
-
60
- fs.mkdirSync(targetDir, { recursive: true })
61
-
62
- try {
63
- await cloneRepo(targetDir)
64
-
65
- process.chdir(targetDir)
66
-
67
- console.log(chalk.green('Installing dependencies...'))
68
- execSync('npm install', { stdio: 'inherit' })
69
-
70
- console.log(chalk.blue('Starting development server...'))
71
- execSync('npm run dev', { stdio: 'inherit' })
72
- } catch (error) {
73
- console.error(error)
74
- console.log(chalk.red('✖ Error during project setup'))
75
- }
76
- })
77
-
78
- program.command('start')
79
- .description('Start the project in development mode')
80
- .action(() => {
81
- logTitle()
82
-
83
- try {
84
- execSync('npx nuxi dev --host --qr=false', { stdio: 'inherit' })
85
- console.log(chalk.blue('🚀 Starting development server'))
86
- logServerInfo()
87
- } catch (error) {
88
- console.log(chalk.red('✖ Error starting Nuxt server'))
89
- console.log(chalk.yellow(error.message))
90
- } finally {
91
- console.log(chalk.white('👨‍💻 If you need help, check the logs or visit our docs'))
92
- }
93
- })
94
-
95
- program.command('generate')
96
- .description('Generate deployment files')
97
- .action(() => {
98
- logTitle()
99
-
100
- try {
101
- execSync('npx nuxi generate', { stdio: 'inherit' })
102
- console.log(chalk.green('✅ Static site generated successfully!'))
103
- } catch (error) {
104
- console.log(chalk.red('✖ Error generating static site'))
105
- }
106
- })
107
-
108
- function summonInspiration() {
109
- const phrases = [
110
- "Launching your digital workspace with Open Web Desktop",
111
- "Unleashing the potential of your browser with Open Web Desktop",
112
- "Crafting a better web experience with Open Web Desktop",
113
- "Transforming how you showcase your portfolio on a browser",
114
- "Bringing your digital garden to life, right in the browser",
115
- "Opening up new possibilities with Open Web Desktop",
116
- ];
117
- const randomIndex = Math.floor(Math.random() * phrases.length);
118
- return phrases[randomIndex];
119
- }
120
-
121
- program.parse()
5
+ await runCli('owd', process.argv.slice(2), { deprecated: true })
@@ -0,0 +1,220 @@
1
+ package bridge
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "fmt"
7
+ "os"
8
+ "os/exec"
9
+ "path/filepath"
10
+ )
11
+
12
+ type Config struct {
13
+ Theme *string `json:"theme"`
14
+ Apps []string `json:"apps"`
15
+ Modules []string `json:"modules"`
16
+ }
17
+
18
+ type Settings struct {
19
+ DevPort int `json:"devPort"`
20
+ GithubUser *string `json:"githubUser"`
21
+ GithubOrgs []string `json:"githubOrgs"`
22
+ InstallMode string `json:"installMode"`
23
+ CatalogSort string `json:"catalogSort"`
24
+ CatalogNewDays int `json:"catalogNewDays"`
25
+ LastInstallChoices map[string]interface{} `json:"lastInstallChoices"`
26
+ }
27
+
28
+ type WorkspacePaths struct {
29
+ Desktop string `json:"desktop"`
30
+ Config string `json:"config"`
31
+ ConfigWrite string `json:"configWrite"`
32
+ ConfigLegacy bool `json:"configLegacy"`
33
+ PackageJson string `json:"packageJson"`
34
+ IsPlayground bool `json:"isPlayground"`
35
+ PackageName string `json:"packageName,omitempty"`
36
+ PackageDir string `json:"packageDir,omitempty"`
37
+ MetaDir string `json:"metaDir"`
38
+ }
39
+
40
+ type WorkspaceContext struct {
41
+ WorkspaceRoot string `json:"workspaceRoot"`
42
+ Settings Settings `json:"settings"`
43
+ Config Config `json:"config"`
44
+ Deps []string `json:"deps"`
45
+ Paths WorkspacePaths `json:"paths"`
46
+ }
47
+
48
+ type GithubForkInfo struct {
49
+ Owner string `json:"owner"`
50
+ Exists bool `json:"exists"`
51
+ IsFork bool `json:"isFork"`
52
+ }
53
+
54
+ type GithubSources struct {
55
+ Official struct {
56
+ Owner string `json:"owner"`
57
+ HtmlUrl string `json:"htmlUrl"`
58
+ } `json:"official"`
59
+ Fork *GithubForkInfo `json:"fork"`
60
+ }
61
+
62
+ type NpmSourceInfo struct {
63
+ Version string `json:"version"`
64
+ }
65
+
66
+ type SourcesMetadata struct {
67
+ Npm *NpmSourceInfo `json:"npm"`
68
+ Github GithubSources `json:"github"`
69
+ }
70
+
71
+ type CatalogEntry struct {
72
+ ShortName string `json:"shortName"`
73
+ Description string `json:"description"`
74
+ Stars int `json:"stars"`
75
+ HtmlUrl *string `json:"htmlUrl"`
76
+ Org string `json:"org"`
77
+ Kind string `json:"kind"`
78
+ LocalPath *string `json:"localPath"`
79
+ Remote *string `json:"remote"`
80
+ Version *string `json:"version"`
81
+ Name string `json:"name"`
82
+ Trusted bool `json:"trusted"`
83
+ Installed bool `json:"installed"`
84
+ LocalSource bool `json:"localSource"`
85
+ InPackageJson bool `json:"inPackageJson"`
86
+ ActiveTheme bool `json:"activeTheme"`
87
+ SourcesMeta *SourcesMetadata `json:"sourcesMeta"`
88
+ UpdatedAt *string `json:"updatedAt"`
89
+ PushedAt *string `json:"pushedAt"`
90
+ }
91
+
92
+ type CatalogResponse struct {
93
+ Entries []CatalogEntry `json:"entries"`
94
+ CacheAge string `json:"cacheAge"`
95
+ }
96
+
97
+ type WritePayload struct {
98
+ Config *Config `json:"config,omitempty"`
99
+ DepsToAdd map[string]string `json:"depsToAdd,omitempty"`
100
+ DepsToRemove []string `json:"depsToRemove,omitempty"`
101
+ Settings *Settings `json:"settings,omitempty"`
102
+ }
103
+
104
+ // FindWorkspaceRoot searches upwards from the current directory for pnpm-workspace.yaml
105
+ func FindWorkspaceRoot() (string, error) {
106
+ dir, err := os.Getwd()
107
+ if err != nil {
108
+ return "", err
109
+ }
110
+ for {
111
+ if _, err := os.Stat(filepath.Join(dir, "pnpm-workspace.yaml")); err == nil {
112
+ return dir, nil
113
+ }
114
+ parent := filepath.Dir(dir)
115
+ if parent == dir {
116
+ break
117
+ }
118
+ dir = parent
119
+ }
120
+ return "", fmt.Errorf("not inside an OWD workspace")
121
+ }
122
+
123
+ func getBridgePath(root string) string {
124
+ return filepath.Join(root, "packages", "cli", "bin", "bridge.js")
125
+ }
126
+
127
+ // ReadWorkspaceContext runs the node bridge with --read
128
+ func ReadWorkspaceContext(root string) (*WorkspaceContext, error) {
129
+ cmd := exec.Command("node", getBridgePath(root), "--read")
130
+ cmd.Dir = root
131
+ var stdout, stderr bytes.Buffer
132
+ cmd.Stdout = &stdout
133
+ cmd.Stderr = &stderr
134
+
135
+ if err := cmd.Run(); err != nil {
136
+ return nil, fmt.Errorf("bridge --read failed: %w (stderr: %s)", err, stderr.String())
137
+ }
138
+
139
+ var ctx WorkspaceContext
140
+ if err := json.Unmarshal(stdout.Bytes(), &ctx); err != nil {
141
+ return nil, fmt.Errorf("failed to unmarshal context JSON: %w", err)
142
+ }
143
+
144
+ return &ctx, nil
145
+ }
146
+
147
+ // ReadCatalog runs the node bridge with --catalog
148
+ func ReadCatalog(root string, force bool) (*CatalogResponse, error) {
149
+ args := []string{getBridgePath(root), "--catalog"}
150
+ if force {
151
+ args = append(args, "--force")
152
+ }
153
+ cmd := exec.Command("node", args...)
154
+ cmd.Dir = root
155
+ var stdout, stderr bytes.Buffer
156
+ cmd.Stdout = &stdout
157
+ cmd.Stderr = &stderr
158
+
159
+ if err := cmd.Run(); err != nil {
160
+ return nil, fmt.Errorf("bridge --catalog failed: %w (stderr: %s)", err, stderr.String())
161
+ }
162
+
163
+ var cat CatalogResponse
164
+ if err := json.Unmarshal(stdout.Bytes(), &cat); err != nil {
165
+ return nil, fmt.Errorf("failed to unmarshal catalog JSON: %w", err)
166
+ }
167
+
168
+ return &cat, nil
169
+ }
170
+
171
+ // WriteConfigOnly writes desktop.config.ts (theme/apps/modules only).
172
+ func WriteConfigOnly(root string, config *Config) error {
173
+ return writeBridge(root, &WritePayload{Config: config})
174
+ }
175
+
176
+ // WritePackageJsonDeps updates desktop/package.json dependencies.
177
+ func WritePackageJsonDeps(root string, toAdd map[string]string, toRemove []string) error {
178
+ return writeBridge(root, &WritePayload{
179
+ DepsToAdd: toAdd,
180
+ DepsToRemove: toRemove,
181
+ })
182
+ }
183
+
184
+ // WriteChanges feeds the write payload to the node bridge via stdin
185
+ func WriteChanges(root string, payload *WritePayload) error {
186
+ return writeBridge(root, payload)
187
+ }
188
+
189
+ func writeBridge(root string, payload *WritePayload) error {
190
+ cmd := exec.Command("node", getBridgePath(root), "--write")
191
+ cmd.Dir = root
192
+ var stdout, stderr bytes.Buffer
193
+ cmd.Stdout = &stdout
194
+ cmd.Stderr = &stderr
195
+
196
+ stdinPipe, err := cmd.StdinPipe()
197
+ if err != nil {
198
+ return err
199
+ }
200
+
201
+ if err := cmd.Start(); err != nil {
202
+ return err
203
+ }
204
+
205
+ payloadBytes, err := json.Marshal(payload)
206
+ if err != nil {
207
+ return err
208
+ }
209
+
210
+ if _, err := stdinPipe.Write(payloadBytes); err != nil {
211
+ return err
212
+ }
213
+ stdinPipe.Close()
214
+
215
+ if err := cmd.Wait(); err != nil {
216
+ return fmt.Errorf("bridge --write failed: %w (stderr: %s)", err, stderr.String())
217
+ }
218
+
219
+ return nil
220
+ }
package/go/desktop-go ADDED
Binary file
package/go/go.mod ADDED
@@ -0,0 +1,33 @@
1
+ module owd-cli
2
+
3
+ go 1.24.2
4
+
5
+ require (
6
+ github.com/charmbracelet/bubbles v1.0.0
7
+ github.com/charmbracelet/bubbletea v1.3.10
8
+ github.com/charmbracelet/lipgloss v1.1.0
9
+ )
10
+
11
+ require (
12
+ github.com/atotto/clipboard v0.1.4 // indirect
13
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
14
+ github.com/charmbracelet/colorprofile v0.4.1 // indirect
15
+ github.com/charmbracelet/x/ansi v0.11.6 // indirect
16
+ github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
17
+ github.com/charmbracelet/x/term v0.2.2 // indirect
18
+ github.com/clipperhouse/displaywidth v0.9.0 // indirect
19
+ github.com/clipperhouse/stringish v0.1.1 // indirect
20
+ github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
21
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
22
+ github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
23
+ github.com/mattn/go-isatty v0.0.20 // indirect
24
+ github.com/mattn/go-localereader v0.0.1 // indirect
25
+ github.com/mattn/go-runewidth v0.0.19 // indirect
26
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
27
+ github.com/muesli/cancelreader v0.2.2 // indirect
28
+ github.com/muesli/termenv v0.16.0 // indirect
29
+ github.com/rivo/uniseg v0.4.7 // indirect
30
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
31
+ golang.org/x/sys v0.38.0 // indirect
32
+ golang.org/x/text v0.3.8 // indirect
33
+ )