mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
Merge remote-tracking branch 'origin/main' into nwparker/upgrade-to-local-build
This commit is contained in:
@@ -31,9 +31,15 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm exec oxlint --format github
|
||||
|
||||
- name: Check switch exhaustiveness
|
||||
run: pnpm run lint:switch-exhaustiveness
|
||||
|
||||
- name: Check styled scrollbars
|
||||
run: pnpm check:styled-scrollbars
|
||||
|
||||
- name: Check quadratic buffer concatenation
|
||||
run: pnpm run check:quadratic-buffer-concat
|
||||
|
||||
- name: Check reliability gate manifest
|
||||
run: pnpm run check:reliability-gates
|
||||
|
||||
@@ -46,6 +52,17 @@ jobs:
|
||||
- name: Verify skill freshness manifest
|
||||
run: pnpm run verify:skill-bundle-manifest
|
||||
|
||||
- name: Verify localization catalog
|
||||
run: pnpm run verify:localization-catalog
|
||||
|
||||
- name: Verify localization coverage
|
||||
run: pnpm run verify:localization-coverage
|
||||
|
||||
# Why: project-owned type declarations must live in .ts so tsc
|
||||
# actually checks them. TypeScript's skipLibCheck: true (inherited
|
||||
# from @electron-toolkit/tsconfig) silently widens unresolved names
|
||||
# in .d.ts to `any`, which is how #1186 shipped a broken IPC signature
|
||||
# past typecheck. See docs/preload-typecheck-hole.md.
|
||||
- name: Guard against project-owned .d.ts in preload/shared
|
||||
run: |
|
||||
matches=$(find src/preload src/shared -name '*.d.ts' 2>/dev/null || true)
|
||||
|
||||
@@ -40,5 +40,33 @@
|
||||
"text": "Idioma",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "src/renderer/src/components/settings/terminal-advanced-platform-search.ts",
|
||||
"kind": "object-property:keywords",
|
||||
"text": "Ghostty",
|
||||
"dynamic": false,
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"filePath": "src/renderer/src/components/settings/terminal-advanced-platform-search.ts",
|
||||
"kind": "object-property:keywords",
|
||||
"text": "ghostty",
|
||||
"dynamic": false,
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"filePath": "src/renderer/src/components/settings/terminal-pane-appearance-search.ts",
|
||||
"kind": "object-property:keywords",
|
||||
"text": "Ghostty",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"filePath": "src/renderer/src/components/settings/terminal-pane-appearance-search.ts",
|
||||
"kind": "object-property:keywords",
|
||||
"text": "ghostty",
|
||||
"dynamic": false,
|
||||
"count": 1
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { parse } from 'yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
// Why: pr.yml re-lists the `lint` chain as individual steps so a single failure
|
||||
// does not mask the rest and oxlint keeps its `--format github` annotations.
|
||||
// Hand-maintained mirrors drift (#10601: three verifiers never ran on PRs), so
|
||||
// this gate fails the moment a `lint` step has no counterpart in pr.yml.
|
||||
|
||||
// Flags that only change reporting, so they must not split two otherwise identical commands.
|
||||
const REPORTING_FLAGS_WITH_VALUE = new Set(['--format', '--reporter'])
|
||||
const REPORTING_FLAGS = new Set(['--quiet'])
|
||||
const PACKAGE_RUNNER_TOKENS = new Set(['pnpm', 'npm', 'yarn', 'npx', 'run', 'exec', 'node'])
|
||||
|
||||
function splitCommandChain(command) {
|
||||
return command
|
||||
.split(/\n|&&|;/)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function canonicalize(command) {
|
||||
const tokens = command.split(/\s+/)
|
||||
const canonical = []
|
||||
|
||||
for (let index = 0; index < tokens.length; index += 1) {
|
||||
const token = tokens[index]
|
||||
if (canonical.length === 0 && PACKAGE_RUNNER_TOKENS.has(token)) {
|
||||
continue
|
||||
}
|
||||
if (REPORTING_FLAGS.has(token)) {
|
||||
continue
|
||||
}
|
||||
if (REPORTING_FLAGS_WITH_VALUE.has(token)) {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
canonical.push(token)
|
||||
}
|
||||
|
||||
return canonical.join(' ')
|
||||
}
|
||||
|
||||
/** Expands `pnpm run x` indirection until every entry is a real binary invocation. */
|
||||
function resolveLeafCommands(command, scripts, seen = new Set()) {
|
||||
const leaves = []
|
||||
|
||||
for (const part of splitCommandChain(command)) {
|
||||
const scriptName = part.match(/^(?:pnpm|npm|yarn)(?:\s+run)?\s+([\w:-]+)$/)?.[1]
|
||||
if (scriptName && scripts[scriptName] && !seen.has(scriptName)) {
|
||||
leaves.push(
|
||||
...resolveLeafCommands(scripts[scriptName], scripts, new Set([...seen, scriptName]))
|
||||
)
|
||||
continue
|
||||
}
|
||||
leaves.push(canonicalize(part))
|
||||
}
|
||||
|
||||
return leaves
|
||||
}
|
||||
|
||||
describe('PR workflow lint parity', () => {
|
||||
it('runs every `pnpm lint` step on pull requests', () => {
|
||||
const { scripts } = JSON.parse(readFileSync('package.json', 'utf8'))
|
||||
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
|
||||
// Scan every job: which one hosts the lint steps is an organizational
|
||||
// detail that has already been renamed once (verify -> static_analysis).
|
||||
const workflowCommands = new Set(
|
||||
Object.values(workflow.jobs)
|
||||
.flatMap((job) => job.steps ?? [])
|
||||
.filter((step) => typeof step.run === 'string')
|
||||
.flatMap((step) => resolveLeafCommands(step.run, scripts))
|
||||
)
|
||||
|
||||
const missing = resolveLeafCommands(scripts.lint, scripts).filter(
|
||||
(leaf) => !workflowCommands.has(leaf)
|
||||
)
|
||||
|
||||
expect(
|
||||
missing,
|
||||
`.github/workflows/pr.yml is missing lint steps: ${missing.join(', ')}. ` +
|
||||
'Add a step for each one so PR CI matches `pnpm lint`.'
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -27,7 +27,10 @@ export const PROJECT_COMMAND_SPECS: CommandSpec[] = [
|
||||
usage:
|
||||
'orca project setup-existing-folder --project <id> --host <host-id> --path <path> [--kind git|folder] [--display-name <name>] [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS, 'project', 'host', 'path', 'kind', 'display-name'],
|
||||
notes: ['For remote runtimes, --path must be an absolute path on the remote server.'],
|
||||
notes: [
|
||||
'For remote runtimes, --path must be an absolute path on the remote server.',
|
||||
'SSH targets are set up through the desktop UI because the desktop client owns SSH connections.'
|
||||
],
|
||||
examples: [
|
||||
'orca project setup-existing-folder --project github:stablyai/orca --host local --path ~/orca',
|
||||
'orca project setup-existing-folder --project github:stablyai/orca --host runtime:gpu --path /home/me/orca --kind git --json'
|
||||
|
||||
@@ -7160,6 +7160,67 @@ describe('OrcaRuntimeService', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses SSH hosts instead of setting the project up on the local machine', async () => {
|
||||
// Why: both inputs must be paths the pre-guard code would have accepted. An unwritable
|
||||
// destination fails at mkdir and a non-repo path fails at isGitRepo, which would leave the
|
||||
// side-effect assertions below unable to observe the local clone/probe they exist to catch.
|
||||
const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-ssh-guard-'))
|
||||
const existingFolder = join(destination, 'orca')
|
||||
mkdirSync(existingFolder, { recursive: true })
|
||||
execFileSync('git', ['init'], { cwd: existingFolder, stdio: 'ignore' })
|
||||
const spawnSpy = vi.spyOn(gitRunner, 'gitSpawn').mockImplementation(() => {
|
||||
// Why: unreachable while the guard holds; stubbed so a regression records the call
|
||||
// instead of shelling out to a real network clone.
|
||||
const proc = new EventEmitter() as EventEmitter & { stderr: EventEmitter }
|
||||
proc.stderr = new EventEmitter()
|
||||
queueMicrotask(() => proc.emit('close', 1, null))
|
||||
return proc as never
|
||||
})
|
||||
const repos: Record<string, unknown>[] = []
|
||||
const runtimeStore = {
|
||||
...store,
|
||||
getRepos: () => [...repos] as never,
|
||||
addRepo: (repo: Record<string, unknown>) => {
|
||||
repos.push(repo)
|
||||
}
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
|
||||
try {
|
||||
const cloneError = await runtime
|
||||
.setupProjectClone({
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'ssh:openclaw',
|
||||
url: 'https://example.com/orca.git',
|
||||
destination
|
||||
})
|
||||
.catch((error: unknown) => error)
|
||||
const existingFolderError = await runtime
|
||||
.setupProjectExistingFolder({
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'ssh:openclaw',
|
||||
path: existingFolder,
|
||||
kind: 'git'
|
||||
})
|
||||
.catch((error: unknown) => error)
|
||||
|
||||
// Why: the defect was a silent local clone/probe recorded as remote, not a bad message,
|
||||
// so the absent side effects are asserted before the wording. Both calls are awaited
|
||||
// first so a regression reports the corruption rather than stopping at the first throw.
|
||||
expect(spawnSpy).not.toHaveBeenCalled()
|
||||
expect(repos).toHaveLength(0)
|
||||
expect(cloneError).toMatchObject({
|
||||
message: expect.stringMatching(/SSH hosts are not supported/)
|
||||
})
|
||||
expect(existingFolderError).toMatchObject({
|
||||
message: expect.stringMatching(/SSH hosts are not supported/)
|
||||
})
|
||||
} finally {
|
||||
spawnSpy.mockRestore()
|
||||
await rm(destination, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('adopts public clone repos into host-qualified project setup', async () => {
|
||||
const destination = await mkdtemp(join(tmpdir(), 'orca-runtime-project-clone-'))
|
||||
const clonePath = join(destination, 'orca')
|
||||
|
||||
@@ -1823,6 +1823,19 @@ function runtimeRepoMatchesExecutionHost(
|
||||
return repo.connectionId == null
|
||||
}
|
||||
|
||||
// Why: this runtime only has local git and local fs, so an ssh: host here would clone and
|
||||
// probe the wrong machine and then register the result as remote. SSH setup is owned by the
|
||||
// desktop IPC path (addRemoteRepoFromPath / cloneRemoteRepo), which the renderer routes to;
|
||||
// only `local` and `runtime:` legitimately reach these RPCs.
|
||||
function assertProjectHostSetupHostIsSupported(hostId: ExecutionHostId | null | undefined): void {
|
||||
if (parseExecutionHostId(hostId)?.kind !== 'ssh') {
|
||||
return
|
||||
}
|
||||
throw new Error(
|
||||
'SSH hosts are not supported by this operation. Set the project up from the Orca desktop app, which owns the SSH connection.'
|
||||
)
|
||||
}
|
||||
|
||||
function getRuntimeFolderWorkspaceInstanceId(repo: Repo, instanceId: string): string {
|
||||
return `${getRuntimeFolderWorkspaceRootId(repo)}${FOLDER_WORKSPACE_INSTANCE_SEPARATOR}${instanceId}`
|
||||
}
|
||||
@@ -15309,6 +15322,7 @@ export class OrcaRuntimeService {
|
||||
if (!this.store) {
|
||||
throw new Error('runtime_unavailable')
|
||||
}
|
||||
assertProjectHostSetupHostIsSupported(args.hostId)
|
||||
let repo = await this.addRepo(args.path, args.kind === 'folder' ? 'folder' : 'git', args.hostId)
|
||||
let setup = getProjectHostSetupForRepo(this.listProjectHostSetups(), repo)
|
||||
if (setup.projectId !== args.projectId) {
|
||||
@@ -15351,6 +15365,8 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
|
||||
async setupProjectClone(args: ProjectHostSetupCloneArgs): Promise<ProjectHostSetupResult> {
|
||||
// Why: guard before cloneRepo, which would otherwise clone to the local disk.
|
||||
assertProjectHostSetupHostIsSupported(args.hostId)
|
||||
const repo = await this.cloneRepo(args.url, args.destination, args.hostId)
|
||||
return await this.setupProjectExistingFolder({
|
||||
projectId: args.projectId,
|
||||
|
||||
Reference in New Issue
Block a user