Skip to content

refactor(config): centralize workspace config helpers in openwork-server (1/3)#1997

Open
benjaminshafii wants to merge 1 commit into
devfrom
cleanup/shared-config-module
Open

refactor(config): centralize workspace config helpers in openwork-server (1/3)#1997
benjaminshafii wants to merge 1 commit into
devfrom
cleanup/shared-config-module

Conversation

@benjaminshafii
Copy link
Copy Markdown
Member

What

First of a 3-PR cleanup of how OpenWork stores/injects config, plugins, and extensions. This one is a pure, behavior-preserving refactor — the foundation the next two build on.

Makes apps/server the single source of truth for:

  • the per-workspace openwork.json schema + defaults
  • the read/write helpers for it
  • the default opencode.json writer

…and has the Electron main process consume them instead of keeping its own drifting copies.

Why

The same path-resolution, openwork.json schema, and opencode.json default were defined twice — once in apps/server/src (the in-process server that actually boots on every launch) and once hand-rolled in apps/desktop/electron/main.mjs (the IPC handlers). They had already drifted. This collapses them to one definition.

Changes

  • apps/server/src/workspace-files.ts — now exports WorkspaceOpenworkConfig + defaultWorkspaceOpenworkConfig / readWorkspaceOpenworkConfig / writeWorkspaceOpenworkConfig (pure; the write helper returns the path string, no Electron-shaped return value).
  • apps/server/src/workspace-init.ts — exports ensureOpencodeConfig; its ensureWorkspaceOpenworkConfig now reuses the shared default + writer (removes the duplicate inline schema).
  • apps/desktop/electron/main.mjs — imports those helpers lazily from the compiled server bundle by absolute path, mirroring the existing runtime.mjs:1052 dev-vs-packaged resolution. Drops the local defaultWorkspaceOpenworkConfig, read/writeWorkspaceOpenworkConfig, ensureDefaultWorkspaceOpencodeConfig, workspaceOpencodeConfigPath, and the now-unused BROWSER_PLUGIN const. The IPC handlers keep their exact return contracts (execResult wrapping for writes, raw config object for reads).

Behavior change

None. Same file paths, same schema, same config content. check-electron-bridge.mjs only matches case labels (not handler bodies), so the swapped internals don't affect the bridge contract.

Verification

  • pnpm --filter openwork-server typecheck
  • pnpm --filter openwork-server build ✅ (exports present in dist)
  • bun test workspace-init / jsonc / portable-files / portable-opencode — 15 pass ✅
  • pnpm --filter @openwork/desktop typecheck:electron
  • node apps/desktop/scripts/check-electron-bridge.mjs — 50 methods covered ✅
  • node --test runtime + remote-workspace — 13 pass ✅
  • Adversarial multi-agent review (behavior-equivalence / runtime-packaging / regression-scope) — no confirmed findings ✅

Follow-ups (stacked on this branch)

  • 2/3 — move openwork.json out of .opencode/.openwork/ (with migration)
  • 3/3 — single source of truth for the plugin injection channel (env OPENCODE_CONFIG_CONTENT)

🤖 Generated with Claude Code

Make apps/server the single source of truth for the per-workspace
openwork.json schema/defaults and the opencode.json default writer:

- workspace-files.ts now exports WorkspaceOpenworkConfig plus
  defaultWorkspaceOpenworkConfig / readWorkspaceOpenworkConfig /
  writeWorkspaceOpenworkConfig (pure, no Electron return shape).
- workspace-init.ts exports ensureOpencodeConfig and its
  ensureWorkspaceOpenworkConfig reuses the shared default/writer.
- Electron main.mjs imports these lazily from the compiled server
  bundle (absolute-path dynamic import, mirroring runtime.mjs's
  dev-vs-packaged candidate resolution) and drops its drifting local
  copies of defaultWorkspaceOpenworkConfig, read/writeWorkspaceOpenworkConfig,
  ensureDefaultWorkspaceOpencodeConfig and workspaceOpencodeConfigPath.

Behavior-preserving: same file paths, same schema, same config content.
The check-electron-bridge validator only matches case labels, so the
swapped handler bodies are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-app Ready Ready Preview, Comment May 29, 2026 12:27am
openwork-den Ready Ready Preview, Comment May 29, 2026 12:27am
openwork-den-worker-proxy Ready Ready Preview, Comment May 29, 2026 12:27am
openwork-landing Ready Ready Preview, Comment, Open in v0 May 29, 2026 12:27am

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/electron/main.mjs">

<violation number="1" location="apps/desktop/electron/main.mjs:1443">
P2: If `import()` rejects (e.g., the bundle file exists but fails to load), the rejected promise is cached forever and never retried. Wrap the async body in try/catch and reset `serverConfigHelpersPromise = null` on any failure, not just the file-existence check.</violation>
</file>

<file name="apps/server/src/workspace-files.ts">

<violation number="1" location="apps/server/src/workspace-files.ts:77">
P2: Wrap `JSON.parse(raw)` in a try-catch and fall back to defaults on parse failure. The file is user-editable, and a malformed `openwork.json` will propagate an unhandled `SyntaxError` to callers that don't guard against it.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

: [...packagedPaths, devPath];
}

function loadServerConfigHelpers() {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: If import() rejects (e.g., the bundle file exists but fails to load), the rejected promise is cached forever and never retried. Wrap the async body in try/catch and reset serverConfigHelpersPromise = null on any failure, not just the file-existence check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/electron/main.mjs, line 1443:

<comment>If `import()` rejects (e.g., the bundle file exists but fails to load), the rejected promise is cached forever and never retried. Wrap the async body in try/catch and reset `serverConfigHelpersPromise = null` on any failure, not just the file-existence check.</comment>

<file context>
@@ -1424,43 +1423,46 @@ function validateSkillName(raw) {
+    : [...packagedPaths, devPath];
+}
+
+function loadServerConfigHelpers() {
+  if (serverConfigHelpersPromise) return serverConfigHelpersPromise;
+  serverConfigHelpersPromise = (async () => {
</file context>
Fix with Cubic

Comment on lines +77 to +78
const raw = await readFile(configPath, "utf8");
return JSON.parse(raw) as WorkspaceOpenworkConfig;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Wrap JSON.parse(raw) in a try-catch and fall back to defaults on parse failure. The file is user-editable, and a malformed openwork.json will propagate an unhandled SyntaxError to callers that don't guard against it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/workspace-files.ts, line 77:

<comment>Wrap `JSON.parse(raw)` in a try-catch and fall back to defaults on parse failure. The file is user-editable, and a malformed `openwork.json` will propagate an unhandled `SyntaxError` to callers that don't guard against it.</comment>

<file context>
@@ -28,3 +29,61 @@ export function projectCommandsDir(workspaceRoot: string): string {
+  if (!existsSync(configPath)) {
+    return defaultWorkspaceOpenworkConfig(workspaceRoot);
+  }
+  const raw = await readFile(configPath, "utf8");
+  return JSON.parse(raw) as WorkspaceOpenworkConfig;
+}
</file context>
Suggested change
const raw = await readFile(configPath, "utf8");
return JSON.parse(raw) as WorkspaceOpenworkConfig;
try {
const raw = await readFile(configPath, "utf8");
return JSON.parse(raw) as WorkspaceOpenworkConfig;
} catch {
return defaultWorkspaceOpenworkConfig(workspaceRoot);
}
Fix with Cubic

@src-opn
Copy link
Copy Markdown
Collaborator

src-opn commented May 29, 2026

please wait for #1999

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants