mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* refactor(host): resolve the app root through the port in fork-reachable modules
`parcel-watcher-entry-path.ts` and `session-scanner-service-entry-path.ts` read the
app root via `require('electron').app` inside a try/catch that already returns null
when Electron is absent. They were therefore correct under plain Node at runtime and
only failed the *static* text check — which is real, not pedantic: the comment in
`ports/port-scan-command-client.ts:19` records that the plain-node-entry-guard fails
on that literal text, try/catch or not.
`hasAppEnvironment() ? getAppEnvironment() : null` gives the identical "no app root
here" answer without the text. That restores `hasAppEnvironment`, which an earlier
commit in this stack deleted as unused — it now has the caller it was waiting for.
Ratchet baseline 27 → 25.
Verified: 74 files / 458 tests; `pnpm typecheck` clean; `oxlint` clean.
* feat(orcad): boot the Orca runtime on plain Node
Closes the last two Electron couplings and makes `orcad` a working artifact:
a 4.43 MB Node bundle that boots, pairs, registers a repo, creates a real git
worktree and round-trips a PTY — with zero `require("electron")`.
Ratchet 2 -> 0, so `config/runtime-electron-baseline.txt` is now empty and its
test asserts exactly that: any reachable electron import is a regression.
- speech: inject the service factories, so importing ModelManager for its type
no longer drags Electron's streaming net.request into the graph
- filesystem-watcher: add a WorktreeWatcherRemoval port. Every entry in those
maps arrives through an ipcMain handler carrying a renderer sender, so a host
with no renderer has nothing to close, restore or forget — the inert default
is what the desktop code does against empty maps, not a stub hiding work
- user-data-path / profile-storage-paths: resolve userData through
AppEnvironment. These surfaced only once orcad pulled the store in
Both host ports now anchor to a realm-global symbol. `vi.resetModules()` gives
the re-imported graph a fresh module copy, so a binding installed before the
reset silently read back as uninstalled.
The acceptance smoke drives both hosts through one code path (`--target
orcad|electron`) and seeds its own git repo, so it is hermetic and asserts the
same contract of each. Wired into PR CI.
* test(smoke): remove the seeded workspace container, not just the worktree
* test(smoke): surface the server's stderr when it dies before ready
* fix(smoke): build node-pty for Node before booting orcad in CI
* fix(smoke): drive the CLI built from this checkout, not one on PATH
* docs(ratchet): say the baseline must stay empty, not merely shrink
* build(orcad): externalize only the native modules actually in the graph
113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
import { getAppEnvironment } from '../../../shared/app-environment'
|
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import type { PersistedState } from '../../../shared/persisted-state-types'
|
|
import { hardenExistingSecureFile } from '../../../shared/secure-file'
|
|
import { MOBILE_PAIRING_USERDATA_FILES } from '../../runtime/mobile-pairing-files'
|
|
|
|
// Why capture once (not a module const, not per-call): a const resolves before configureDevUserDataPath() redirects userData (dev/prod collide);
|
|
// per-call resolves after app.setName('Orca') flips path case and loses data on case-sensitive FS. index.ts calls initDataPath() at the right moment.
|
|
let _dataFile: string | null = null
|
|
let _userDataDir: string | null = null
|
|
|
|
export function initDataPath(): void {
|
|
const userDataDir = getAppEnvironment().getPath('userData')
|
|
_userDataDir = userDataDir
|
|
_dataFile = join(userDataDir, 'orca-data.json')
|
|
}
|
|
|
|
export function getDataFile(): string {
|
|
if (!_dataFile) {
|
|
// Safety fallback — should not be hit in normal startup.
|
|
const userDataDir = getAppEnvironment().getPath('userData')
|
|
_userDataDir = userDataDir
|
|
_dataFile = join(userDataDir, 'orca-data.json')
|
|
}
|
|
return _dataFile
|
|
}
|
|
|
|
// Why a sidecar: githubCache refreshes every poll and would rewrite the whole multi-MB orca-data.json each cycle.
|
|
// Snapshotted best-effort at quit for instant badges next launch; safe to lose.
|
|
export function getGithubCacheFile(dataFile = getDataFile()): string {
|
|
return join(dirname(dataFile), 'orca-github-cache.json')
|
|
}
|
|
|
|
export function readGithubCacheSnapshot(dataFile: string): PersistedState['githubCache'] | null {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(getGithubCacheFile(dataFile), 'utf-8')) as unknown
|
|
const isPlainRecord = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
if (
|
|
isPlainRecord(parsed) &&
|
|
isPlainRecord((parsed as { pr?: unknown }).pr) &&
|
|
isPlainRecord((parsed as { issue?: unknown }).issue)
|
|
) {
|
|
return parsed as PersistedState['githubCache']
|
|
}
|
|
} catch {
|
|
// Missing or corrupt snapshot: start with an empty cache and refetch.
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Return the userData directory captured at initDataPath() time, before app.setName() can change how getAppEnvironment().getPath('userData') resolves.
|
|
*
|
|
* Subsystems sharing storage with orca-data.json read this instead of resolving late, which on case-sensitive FS can lose paired devices.
|
|
*/
|
|
export function getCanonicalUserDataPath(): string {
|
|
if (!_userDataDir) {
|
|
// Safety fallback — should not be hit in normal startup.
|
|
_userDataDir = getAppEnvironment().getPath('userData')
|
|
}
|
|
return _userDataDir
|
|
}
|
|
|
|
/**
|
|
* Copy legacy mobile pairing credentials into the canonical userData directory.
|
|
*
|
|
* Copies the registry and E2EE keypair forward as a pair so an update doesn't force a re-pair or mix devices with the wrong key.
|
|
*
|
|
* Sources are deliberately left in place: a copy-then-delete has no atomic form across the userData
|
|
* dirs, and losing the originals to a crash mid-migration would strand every paired device. They stay
|
|
* readable by an older build the user rolls back to; removing them is a separate cleanup decision.
|
|
*/
|
|
export function migrateMobilePairingDataToCanonicalUserDataPath(sourceUserDataDir: string): void {
|
|
const targetUserDataDir = getCanonicalUserDataPath()
|
|
if (resolve(sourceUserDataDir) === resolve(targetUserDataDir)) {
|
|
return
|
|
}
|
|
|
|
const migrations = MOBILE_PAIRING_USERDATA_FILES.map((fileName) => ({
|
|
sourcePath: join(sourceUserDataDir, fileName),
|
|
targetPath: join(targetUserDataDir, fileName)
|
|
}))
|
|
if (migrations.some(({ sourcePath }) => !existsSync(sourcePath))) {
|
|
return
|
|
}
|
|
if (migrations.some(({ targetPath }) => existsSync(targetPath))) {
|
|
return
|
|
}
|
|
|
|
mkdirSync(targetUserDataDir, { recursive: true })
|
|
const copied: string[] = []
|
|
try {
|
|
for (const { sourcePath, targetPath } of migrations) {
|
|
copyFileSync(sourcePath, targetPath)
|
|
copied.push(targetPath)
|
|
// Why: copyFileSync drops Windows ACLs, so re-assert current-user-only on these credential copies (device tokens, E2EE key).
|
|
hardenExistingSecureFile(targetPath)
|
|
}
|
|
} catch (error) {
|
|
// Why: a half-copied pair mixes devices with the wrong key, and the existing-target guard above would block the retry.
|
|
for (const targetPath of copied) {
|
|
try {
|
|
rmSync(targetPath, { force: true })
|
|
} catch {
|
|
// Best effort — leave the retry guard to the next launch.
|
|
}
|
|
}
|
|
console.error('[persistence] Failed to migrate mobile pairing files forward:', error)
|
|
}
|
|
}
|