refactor(cli): group plugin and skill implementation owners (#41549)

This commit is contained in:
Neil
2026-11-12 05:47:35 -08:00
committed by GitHub
parent 3eb2765e66
commit 382c7cfcb3
92 changed files with 221 additions and 632 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
/skill-stubs/*.md text eol=lf
/skill-stubs/_shared/*.md text eol=lf
/skills/*/SKILL.md text eol=lf
/src/cli/bundled-skill-guides.ts text eol=lf
/src/cli/skills/bundled-skill-guides.ts text eol=lf
# SVG bytes feed embedded data URLs and asset hashes on every build host.
*.svg text eol=lf
# Compiler input line breaks must not depend on the checkout OS.
+1 -1
View File
@@ -308,7 +308,7 @@
"ignorePatterns": [
"src/main/acp/generated/acp-protocol.generated.ts",
"src/shared/rpc-contract/rpc-params-catalog.generated.ts",
"src/cli/bundled-skill-guides.ts",
"src/cli/skills/bundled-skill-guides.ts",
"resources/plugins/declarative/*.mjs",
"**/node_modules",
"**/dist",
+3 -1
View File
@@ -11,7 +11,9 @@ export const CLI_MAIN_ENTRY_NAMES = [
'filesystem/macos-tree-clone',
'orcad/orcad-stale-lock-reclaim',
'orcad/orcad-host-identity-reset',
'plugins/plugin-authoring-checks',
'plugins/plugin-artifact-validation',
'plugins/plugin-content-hash',
'plugins/plugin-catalog-release-entry',
// `orca vm env doctor` runs its checks in the CLI process.
'environment-doctor/environment-doctor',
'environment-doctor/environment-doctor-host',
+3 -1
View File
@@ -348,7 +348,9 @@ module.exports = {
'out/main/filesystem/macos-tree-clone.js',
'out/main/orca-profiles/profile-index-store.js',
'out/main/persistence/profile-state/**',
'out/main/plugins/plugin-authoring-checks.js',
'out/main/plugins/plugin-artifact-validation.js',
'out/main/plugins/plugin-content-hash.js',
'out/main/plugins/plugin-catalog-release-entry.js',
'out/main/environment-doctor/environment-doctor.js',
'out/main/environment-doctor/environment-doctor-host.js',
// `orca plugin test` and `orca plugin check` load the runtime, kit and hooks in Node.
+1 -1
View File
@@ -572,7 +572,7 @@
1 test-no-wall-clock:tests/e2e/host-cold-park-remote-subscriber.unit.test.ts
1 test-timeout-needs-reason:src/cli/agents/agent-launch-process.test.ts
1 test-timeout-needs-reason:src/cli/runtime/serve-pid1-init-reaper.test.ts
1 test-timeout-needs-reason:src/cli/skills-install.test.ts
1 test-timeout-needs-reason:src/cli/skills/skills-install.test.ts
2 test-timeout-needs-reason:src/main/acp/journal/acp-structured-broken-stream.test.ts
4 test-timeout-needs-reason:src/main/agent-hooks/server/testing/http/server-transport-interference.test.ts
1 test-timeout-needs-reason:src/main/agent-hooks/wsl-relay/testing/wsl-hook-relay-live.integration.test.ts
@@ -84,7 +84,7 @@ const NON_UNIT_TEST_RUNNERS = [
// `orca plugin test` runs every example's own tests in Node; the unit test drives it.
name: 'example plugin tests (orca plugin test)',
include: ['examples/plugins/*/src/**/*.test.ts', 'examples/plugins/*/src/**/*.test.tsx'],
sources: ['src/cli/plugin-authoring/plugin-test-runner.test.ts'],
sources: ['src/cli/plugins/authoring/plugin-test-runner.test.ts'],
mirrors: ['readdirSync(EXAMPLES)']
},
{
@@ -17,11 +17,12 @@ const { validatePluginFile, pluginManifestWarnings } = loadPluginSchemaModule(
const { parsePluginManifest } = loadPluginSchemaModule('manifest/plugin-manifest.ts')
// Why the CLI's main entry: `orca plugin validate` and `release-entry` run these same checks, and
// the printed contentHash must be what Orca computes at install.
const {
pluginCatalogReleaseForHead,
validateDeclaredPluginArtifacts,
validatePluginInstallContent
} = loadPluginSchemaModule('../../main/plugins/plugin-authoring-checks.ts')
const { pluginCatalogReleaseForHead } = loadPluginSchemaModule(
'../../main/plugins/plugin-catalog-release-entry.ts'
)
const { validateDeclaredPluginArtifacts, validatePluginInstallContent } = loadPluginSchemaModule(
'../../main/plugins/plugin-artifact-validation.ts'
)
// Why shared: `orca plugin validate` runs these same strict artifact checks.
const { strictPluginArtifactErrors, strictPluginArtifactWarnings } = loadPluginSchemaModule(
'authoring/plugin-strict-artifact-checks.ts'
@@ -4,7 +4,7 @@ import path from 'node:path'
import process from 'node:process'
import { runProcess } from '@orca/process-host'
import { afterEach, describe, expect, it } from 'vitest'
import { createPluginFromTemplate } from '../../../../src/cli/plugin-authoring/plugin-create'
import { createPluginFromTemplate } from '../../../../src/cli/plugins/authoring/plugin-create'
const SCRIPT = path.join(import.meta.dirname, './validate-plugin-files.mjs')
const roots = []
@@ -179,7 +179,7 @@ function serializeEmbeddedModule(guides) {
})
.join(',\n')
return `// Generated by config/scripts/generate-bundled-skill-guides.mjs. Do not edit.\n\nexport type BundledSkillGuideReference = {\n readonly name: string\n readonly markdown: string\n}\n\nexport type BundledSkillGuide = {\n readonly name: string\n readonly description: string\n readonly markdown: string\n readonly aliases: readonly string[]\n readonly references: readonly BundledSkillGuideReference[]\n}\n\n${markdownConstants}\n\n// oxfmt-ignore\nexport const BUNDLED_SKILL_GUIDES = [\n${guideEntries}\n] as const satisfies readonly BundledSkillGuide[]\n`
return `// Generated by config/scripts/skills/bundle-generation/generate-bundled-skill-guides.mjs. Do not edit.\n\nexport type BundledSkillGuideReference = {\n readonly name: string\n readonly markdown: string\n}\n\nexport type BundledSkillGuide = {\n readonly name: string\n readonly description: string\n readonly markdown: string\n readonly aliases: readonly string[]\n readonly references: readonly BundledSkillGuideReference[]\n}\n\n${markdownConstants}\n\n// oxfmt-ignore\nexport const BUNDLED_SKILL_GUIDES = [\n${guideEntries}\n] as const satisfies readonly BundledSkillGuide[]\n`
}
async function readGuideReferences(repoRoot, guideName) {
@@ -317,7 +317,7 @@ async function buildArtifacts(repoRoot = REPO_ROOT) {
return [
{
path: path.join(repoRoot, 'src', 'cli', 'bundled-skill-guides.ts'),
path: path.join(repoRoot, 'src', 'cli', 'skills', 'bundled-skill-guides.ts'),
content: serializeEmbeddedModule(guides)
},
...projections
@@ -165,7 +165,7 @@ const SPECIFIC_PLUGIN_ROUTES = [
'src\\/main\\/plugins\\/panels\\/(?:frame-serving\\/)?plugin-frame-(?:module-|source-serving|protocol\\.ts$)',
'src\\/main\\/plugins\\/plugin-content-hash',
'src\\/shared\\/plugins\\/frames\\/plugin-frame-source-modules',
'src\\/cli\\/plugin-authoring\\/plugin-(?:type-files|components-template)',
'src\\/cli\\/plugins\\/authoring\\/plugin-(?:type-files|components-template)',
'examples\\/plugins\\/hello-tsx\\/'
].join('|'),
'plugin-frame-guest|plugin-smoke'
@@ -218,7 +218,7 @@ const SPECIFIC_PLUGIN_ROUTES = [
'src\\/(?:main\\/plugins\\/view\\/(?!plugin-view-replay)|shared\\/plugins\\/view\\/)',
'src\\/main\\/runtime\\/rpc\\/methods\\/plugin-view',
// Every `plugin shot` checks for a chrome region and resolves the default shots folder.
'src\\/cli\\/(?:handlers\\/plugin-(?:view|shot-chrome|shot-default-dir)|specs\\/plugin-view)',
'src\\/cli\\/(?:plugins\\/view\\/plugin-(?:view|shot-chrome|shot-default-dir)|specs\\/plugin-view)',
'src\\/renderer\\/src\\/hooks\\/(?:ipc-events\\/plugin-view-ipc-bridge|use-document-dark-theme)',
'src\\/renderer\\/src\\/components\\/right-sidebar\\/(?:plugin-panels\\/health\\/(?:plugin-panel-draw-status|use-plugin-panel-draw-report)|right-sidebar-panel-content)'
].join('|'),
@@ -233,14 +233,14 @@ const SPECIFIC_PLUGIN_ROUTES = [
`${STORE}shot-replay`,
'src\\/main\\/plugins\\/view\\/plugin-view-replay',
'src\\/main\\/plugins\\/testing\\/plugin-test-(?:hooks|register)',
'src\\/cli\\/(?:handlers|specs)\\/plugin-shot-scenario',
'src\\/cli\\/plugin-authoring\\/plugin-(?:panel-check|test-runner|test-node)'
'src\\/cli\\/(?:plugins\\/view|specs)\\/plugin-shot-scenario',
'src\\/cli\\/plugins\\/authoring\\/plugin-(?:panel-check|test-runner|test-node)'
].join('|')
),
pluginRoute(
'plugins.view-shot-editor',
['plugin-shot-editor'],
'src\\/cli\\/handlers\\/plugin-shot-editor'
'src\\/cli\\/plugins\\/view\\/plugin-shot-editor'
),
pluginRoute(
'plugins.startup-and-manifest',
+3 -3
View File
@@ -4,7 +4,7 @@ import { JIRA_HANDLER_GROUP } from './jira/jira-handler-group'
import { LINEAR_HANDLER_GROUP } from './linear/linear-handler-group'
import { REFERENCE_HANDLER_GROUP } from './reference-handler-group'
import { SERVE_PAIRING_HANDLER_GROUPS } from './server/serve-pairing-handler-groups'
import { PLUGIN_HANDLER_GROUPS } from './plugin-handler-groups'
import { PLUGIN_HANDLER_GROUPS } from './plugins/plugin-handler-groups'
import { HOST_HANDLER_GROUPS } from './hosts/host-handler-groups'
import { WORKTREE_HANDLER_GROUPS } from './workspaces/worktree-handler-groups'
@@ -248,13 +248,13 @@ export const HANDLER_GROUPS: readonly HandlerGroup[] = [
{
name: 'skill-sharing',
keys: ['skills installed', 'skills share'],
load: async () => (await import('./handlers/skill-sharing.js')).SKILL_SHARING_HANDLERS
load: async () => (await import('./skills/skill-sharing.js')).SKILL_SHARING_HANDLERS
},
...PLUGIN_HANDLER_GROUPS,
{
name: 'skills',
keys: ['skills list', 'skills get', 'skills install', 'skills update'],
load: async () => (await import('./handlers/skills.js')).SKILL_HANDLERS
load: async () => (await import('./skills/skills.js')).SKILL_HANDLERS
},
{
name: 'search',
@@ -1,52 +0,0 @@
import { EventEmitter } from 'node:events'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { onPluginDevWatchInterrupt, waitUnlessAborted } from './plugin-dev-watch-process'
function fakeProcess(platform: NodeJS.Platform) {
const emitter = new EventEmitter()
return Object.assign(emitter, { platform })
}
afterEach(() => vi.useRealTimers())
describe('onPluginDevWatchInterrupt', () => {
it('listens for Ctrl-C and Ctrl-Break on Windows, and unsubscribes', () => {
const target = fakeProcess('win32')
const listener = vi.fn()
const unsubscribe = onPluginDevWatchInterrupt(listener, target)
target.emit('SIGINT')
target.emit('SIGBREAK')
target.emit('SIGTERM')
expect(listener).toHaveBeenCalledTimes(2)
unsubscribe()
target.emit('SIGINT')
expect(listener).toHaveBeenCalledTimes(2)
})
it('also stops on SIGTERM and SIGHUP on POSIX', () => {
const target = fakeProcess('linux')
const listener = vi.fn()
onPluginDevWatchInterrupt(listener, target)
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
target.emit(signal)
}
expect(listener).toHaveBeenCalledTimes(3)
})
})
describe('waitUnlessAborted', () => {
it('resolves after the delay, or at once when aborted', async () => {
vi.useFakeTimers()
const stop = new AbortController()
let done = false
void waitUnlessAborted(500, stop.signal).then(() => {
done = true
})
await vi.advanceTimersByTimeAsync(499)
expect(done).toBe(false)
stop.abort()
await Promise.resolve()
expect(done).toBe(true)
await expect(waitUnlessAborted(500, stop.signal)).resolves.toBeUndefined()
})
})
@@ -1,34 +1,25 @@
import type { CommandHandler } from '../dispatch'
import { createPluginFromTemplate, PluginCreateError } from '../plugin-authoring/plugin-create'
import type { CommandHandler } from '../../dispatch'
import { createPluginFromTemplate, PluginCreateError } from './plugin-create'
import {
inspectPluginFolder,
validatePluginTarget,
type PluginInspection,
type PluginValidationReport
} from '../plugin-authoring/plugin-folder-validation'
import {
isPluginTemplateName,
PLUGIN_TEMPLATE_NAMES
} from '../plugin-authoring/plugin-template-names'
import {
formatPluginReleaseEntry,
pluginReleaseEntryForFolder
} from '../plugin-authoring/plugin-release-entry'
} from './plugin-folder-validation'
import { isPluginTemplateName, PLUGIN_TEMPLATE_NAMES } from './plugin-template-names'
import { formatPluginReleaseEntry, pluginReleaseEntryForFolder } from './plugin-release-entry'
import {
printPluginResult,
requirePluginPathArg,
resolveLocalPluginPath
} from '../plugin-authoring/plugin-cli-output'
import { writePluginTypeFiles } from '../plugin-authoring/plugin-type-files'
import { findPluginTestFiles, runPluginTests } from '../plugin-authoring/plugin-test-runner'
import { checkPluginPanels, formatPluginPanelCheck } from '../plugin-authoring/plugin-panel-check'
import {
checkPluginEditors,
formatPluginEditorCheck
} from '../plugin-authoring/plugin-editor-check'
import { RuntimeClientError } from '../runtime/types'
import { PLUGIN_INVALID_CODE } from '../../shared/plugins/approval/plugin-cli-approval'
import { formatPluginGrantLines } from '../plugin-authoring/plugin-grant-lines'
} from '../plugin-cli-output'
import { writePluginTypeFiles } from './plugin-type-files'
import { findPluginTestFiles, runPluginTests } from './plugin-test-runner'
import { checkPluginPanels, formatPluginPanelCheck } from './plugin-panel-check'
import { checkPluginEditors, formatPluginEditorCheck } from './plugin-editor-check'
import { RuntimeClientError } from '../../runtime/types'
import { PLUGIN_INVALID_CODE } from '../../../shared/plugins/approval/plugin-cli-approval'
import { formatPluginGrantLines } from '../plugin-grant-lines'
function optionalString(flags: Map<string, string | boolean>, name: string): string | undefined {
const value = flags.get(name)
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, statSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { basename, dirname, join, resolve } from 'node:path'
import { parsePluginManifest } from '../../shared/plugins/manifest/plugin-manifest'
import { parsePluginManifest } from '../../../shared/plugins/manifest/plugin-manifest'
import { pluginDisplayNameFromId } from './plugin-display-name'
import { PLUGIN_TEMPLATE_NAMES, type PluginTemplateName } from './plugin-template-names'
import { pluginTemplateKey } from './plugin-template-files'
@@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { checkPluginEditors } from './plugin-editor-check'
import { createPluginFromTemplate } from './plugin-create'
import { validatePluginTarget } from './plugin-folder-validation'
import { checkPluginDevFolder } from '../plugin-dev-watch/plugin-dev-watch-check'
import { checkPluginDevFolder } from '../dev-watch/plugin-dev-watch-check'
const roots: string[] = []
@@ -4,8 +4,8 @@ import { pathToFileURL } from 'node:url'
import type {
PluginEditorCheckIssue,
PluginEditorStaticCheck
} from '../../main/plugins/testing/plugin-editor-static-check'
import { parsePluginManifest } from '../../shared/plugins/manifest/plugin-manifest'
} from '../../../main/plugins/testing/plugin-editor-static-check'
import { parsePluginManifest } from '../../../shared/plugins/manifest/plugin-manifest'
import { pluginTestingFiles } from './plugin-test-runner'
export type PluginEditorCheck = {
@@ -1,4 +1,4 @@
import { PLUGIN_EDITOR_FRAME_SINCE } from '../../shared/plugins/manifest/plugin-schema-field-docs'
import { PLUGIN_EDITOR_FRAME_SINCE } from '../../../shared/plugins/manifest/plugin-schema-field-docs'
import { PLUGIN_TYPES_DIR } from './plugin-type-files'
import {
COMMON_FILES,
@@ -1,32 +1,32 @@
import { createHash } from 'node:crypto'
import { existsSync, readFileSync, statSync } from 'node:fs'
import { basename, dirname, join, resolve, sep } from 'node:path'
import { hashPluginTree } from '../../../main/plugins/plugin-content-hash'
import {
hashPluginTree,
validateDeclaredPluginArtifacts,
validatePluginInstallContent
} from '../../main/plugins/plugin-authoring-checks'
import { PLUGIN_CATALOG_FILENAME } from '../../shared/plugins/catalog/plugin-catalog'
} from '../../../main/plugins/plugin-artifact-validation'
import { PLUGIN_CATALOG_FILENAME } from '../../../shared/plugins/catalog/plugin-catalog'
import {
pluginApprovalGrants,
pluginNeedsCliApproval,
type PluginApprovalGrants
} from '../../shared/plugins/approval/plugin-cli-approval'
} from '../../../shared/plugins/approval/plugin-cli-approval'
import {
strictPluginArtifactErrors,
strictPluginArtifactWarnings
} from '../../shared/plugins/authoring/plugin-strict-artifact-checks'
import { validatePluginFile } from '../../shared/plugins/installation/plugin-file-validation'
} from '../../../shared/plugins/authoring/plugin-strict-artifact-checks'
import { validatePluginFile } from '../../../shared/plugins/installation/plugin-file-validation'
import {
localPluginPublisher,
withPluginPublisher
} from '../../shared/plugins/installation/plugin-publisher-derivation'
} from '../../../shared/plugins/installation/plugin-publisher-derivation'
import {
PLUGIN_MANIFEST_FILENAME,
parsePluginManifest,
qualifiedPluginKey,
type PluginManifest
} from '../../shared/plugins/manifest/plugin-manifest'
} from '../../../shared/plugins/manifest/plugin-manifest'
export type PluginFolderSummary = {
pluginKey: string
@@ -1,4 +1,4 @@
import { PLUGIN_HOOKS_SINCE } from '../../shared/plugins/manifest/plugin-schema-field-docs'
import { PLUGIN_HOOKS_SINCE } from '../../../shared/plugins/manifest/plugin-schema-field-docs'
import {
COMMON_FILES,
json,
@@ -4,9 +4,9 @@ import { join, relative, sep } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { runProcess } from '@orca/process-host'
import { z } from 'zod'
import type { PluginDeclarativeNode } from '../../shared/plugins/declarative/plugin-declarative-catalog'
import { parsePluginDeclarativeTree } from '../../shared/plugins/declarative/plugin-declarative-tree'
import { parsePluginManifest } from '../../shared/plugins/manifest/plugin-manifest'
import type { PluginDeclarativeNode } from '../../../shared/plugins/declarative/plugin-declarative-catalog'
import { parsePluginDeclarativeTree } from '../../../shared/plugins/declarative/plugin-declarative-tree'
import { parsePluginManifest } from '../../../shared/plugins/manifest/plugin-manifest'
import { PLUGIN_PANEL_CHECK_SCRIPT } from './plugin-panel-check-script'
import { selectPluginTestNode } from './plugin-test-node'
import { pluginTestingFiles } from './plugin-test-runner'
@@ -1,9 +1,9 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { runProcess } from '@orca/process-host'
import { afterEach, describe, expect, it } from 'vitest'
import { hashPluginTree } from '../../main/plugins/plugin-authoring-checks'
import { hashPluginTree } from '../../../main/plugins/plugin-content-hash'
import { createPluginFromTemplate } from './plugin-create'
import { validatePluginTarget } from './plugin-folder-validation'
import { formatPluginReleaseEntry, pluginReleaseEntryForFolder } from './plugin-release-entry'
@@ -64,21 +64,6 @@ describe('orca plugin release-entry', () => {
expect(text).toContain(`"commit": "${result.entry?.commit}"`)
})
it('pins the committed files, not uncommitted edits', async () => {
const repo = await tempRoot()
await git(repo, ['init', '--quiet'])
await createPluginFromTemplate({ dir: repo, template: 'theme', publisher: 'acme' })
await git(repo, ['add', '.'])
await git(repo, ['commit', '--quiet', '-m', 'init'])
const committed = await releaseFor(repo)
await writeFile(join(repo, 'NOTES.txt'), 'draft')
const result = await releaseFor(repo)
expect(result.sourcePath).toBeUndefined()
expect(result.entry?.contentHash).toBe(committed.entry?.contentHash)
expect(result.note).toContain('uncommitted changes')
})
it('gives no entry when the commit lacks a file the manifest declares', async () => {
const repo = await tempRoot()
await git(repo, ['init', '--quiet'])
@@ -1,7 +1,7 @@
import {
pluginCatalogReleaseForHead,
type PluginCatalogReleaseReport
} from '../../main/plugins/plugin-authoring-checks'
} from '../../../main/plugins/plugin-catalog-release-entry'
import type { PluginValidationReport } from './plugin-folder-validation'
export type PluginReleaseEntryResult = PluginCatalogReleaseReport & {
@@ -1,7 +1,7 @@
/** File helpers shared by the `orca plugin create` templates. */
import { localPluginPublisher } from '../../shared/plugins/installation/plugin-publisher-derivation'
import { PLUGIN_DERIVED_PUBLISHER_SINCE } from '../../shared/plugins/manifest/plugin-schema-field-docs'
import { localPluginPublisher } from '../../../shared/plugins/installation/plugin-publisher-derivation'
import { PLUGIN_DERIVED_PUBLISHER_SINCE } from '../../../shared/plugins/manifest/plugin-schema-field-docs'
/** `publisher` is deprecated: set only for authors who still support Orca that requires it. */
export type PluginTemplateInput = { publisher?: string; id: string; name: string }
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createPluginFromTemplate } from './plugin-create'
import { parsePluginDeclarativeTree } from '../../shared/plugins/declarative/plugin-declarative-tree'
import { parsePluginDeclarativeTree } from '../../../shared/plugins/declarative/plugin-declarative-tree'
import {
checkPluginPanels,
formatPluginPanelCheck,
@@ -102,7 +102,7 @@ describe('orca plugin test', () => {
)
})
const EXAMPLES = resolve(__dirname, '../../../examples/plugins')
const EXAMPLES = resolve(__dirname, '../../../../examples/plugins')
// Every example with its own tests (examples/plugins/*/src/*.test.tsx).
const TESTED_EXAMPLES = readdirSync(EXAMPLES).filter((name) => {
const src = join(EXAMPLES, name, 'src')
@@ -10,7 +10,7 @@ export const PLUGIN_TEST_FILE = /\.test\.(?:tsx?|mts|jsx?|mjs)$/i
// Not plugin sources: dependencies, Orca's own generated files, VCS and build output.
const SKIPPED_DIRS = new Set(['node_modules', '.orca', '.git', 'dist', 'out', 'coverage'])
const defaultAppRoot = (): string => join(__dirname, '..', '..', '..')
const defaultAppRoot = (): string => join(__dirname, '..', '..', '..', '..')
/** The files Orca ships for testing components panels, from the app this CLI belongs to. */
export function pluginTestingFiles(appRoot = defaultAppRoot()) {
@@ -5,7 +5,7 @@ import { runProcess } from '@orca/process-host'
import { afterEach, describe, expect, it } from 'vitest'
import { createPluginFromTemplate } from './plugin-create'
const TSC = resolve(__dirname, '../../../node_modules/typescript/bin/tsc')
const TSC = resolve(__dirname, '../../../../node_modules/typescript/bin/tsc')
const cleanup: string[] = []
afterEach(async () => {
@@ -1,8 +1,8 @@
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { basename, dirname, join } from 'node:path'
import { isPluginSourceModulePath } from '../../shared/plugins/frames/plugin-frame-source-modules'
import { parsePluginManifest } from '../../shared/plugins/manifest/plugin-manifest'
import { isPluginSourceModulePath } from '../../../shared/plugins/frames/plugin-frame-source-modules'
import { parsePluginManifest } from '../../../shared/plugins/manifest/plugin-manifest'
/** Plugin-relative folder `orca plugin types` owns; dev hashing and loading skip it. */
export const PLUGIN_TYPES_DIR = '.orca/types'
@@ -52,7 +52,7 @@ const GENERATED_NOTE =
/** Packaged apps ship typings as extraResources at <Resources>/plugins; dev: resources/plugins. */
function defaultTypingsRoot(): string {
const appRoot = join(__dirname, '..', '..', '..')
const appRoot = join(__dirname, '..', '..', '..', '..')
return basename(appRoot) === 'app.asar.unpacked'
? join(dirname(appRoot), 'plugins')
: join(appRoot, 'resources', 'plugins')
@@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { createPluginFromTemplate } from './plugin-create'
import { findPluginTestFiles, runPluginTests } from './plugin-test-runner'
const TSC = resolve(__dirname, '../../../node_modules/typescript/bin/tsc')
const TSC = resolve(__dirname, '../../../../node_modules/typescript/bin/tsc')
const roots: string[] = []
afterEach(async () => {
@@ -1,7 +1,7 @@
import { PassThrough } from 'node:stream'
import { describe, expect, it } from 'vitest'
import '../runtime/types'
import '../../runtime/types'
import { createTerminalConfirm } from './plugin-dev-watch-approval'
import './plugin-dev-watch-loop'
@@ -1,8 +1,8 @@
import { createInterface } from 'node:readline'
import { z } from 'zod'
import { formatPluginGrantLines } from '../plugin-authoring/plugin-grant-lines'
import { RuntimeClientError } from '../runtime/types'
import type { DevLoadResult } from '../handlers/plugin-runtime'
import { formatPluginGrantLines } from '../plugin-grant-lines'
import { RuntimeClientError } from '../../runtime/types'
import type { DevLoadResult } from '../plugin-runtime'
/** Asks a yes/no question; false on anything but yes, end of input or Ctrl-C. */
export type PluginDevWatchConfirm = (question: string) => Promise<boolean>
@@ -1,9 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
import {
createPluginDevWatchCheckQueue,
type PluginDevWatchCheckResult
} from './plugin-dev-watch-check'
import type { PluginHostListEntry } from '../../../shared/plugins/plugin-list-entry'
import type { PluginDevWatchCheckResult } from './plugin-dev-watch-check'
import { formatPluginDevWatchEvent, type PluginDevWatchEvent } from './plugin-dev-watch-events'
import { runPluginDevWatch, type PluginDevWatchHost } from './plugin-dev-watch-loop'
@@ -48,21 +45,6 @@ const passed: PluginDevWatchCheckResult = {
],
editors: []
}
const failed: PluginDevWatchCheckResult = {
ok: false,
invalid: [],
panels: [
{
panel: 'todos',
entry: 'src/panel.tsx',
elements: 0,
summary: [],
issues: [{ kind: 'plugin', message: 'boom', location: 'src/panel.tsx:4:7' }],
warnings: []
}
],
editors: []
}
/** Polls: 1 baseline, 2 status (unchanged), 3 logs, 4 status (one save), then Ctrl-C. */
async function watch(
@@ -107,22 +89,6 @@ async function watch(
}
describe('plugin dev watch --check', () => {
it('checks after the load and after each reload, before the unload line', async () => {
const check = vi.fn<NonNullable<PluginDevWatchHost['check']>>()
check.mockResolvedValueOnce(passed).mockResolvedValueOnce(failed)
const events = await watch(check)
expect(check).toHaveBeenCalledTimes(2)
const types = events.map((event) => event.type)
expect(types[0]).toBe('loaded')
expect(types.filter((type) => type === 'check')).toHaveLength(2)
expect(types.indexOf('check', types.indexOf('reloaded'))).toBeGreaterThan(-1)
expect(types.at(-1)).toBe('unloaded')
const last = events.findLast((event) => event.type === 'check')
expect(last && formatPluginDevWatchEvent(last)).toBe(
'✗ check failed for acme.todos\n todos (src/panel.tsx): failed\n src/panel.tsx:4:7: boom'
)
})
it('fails a check that renders while Orca does not run the plugin', async () => {
const pending = entry(1, 'pending')
const events = await watch(vi.fn().mockResolvedValue(passed), [[entry(0)], [pending]])
@@ -135,34 +101,4 @@ describe('plugin dev watch --check', () => {
'✗ check passed, but in Orca acme.todos is pending approval'
)
})
it('reports a check that cannot run as a warning and keeps watching', async () => {
const events = await watch(async () => {
throw new Error('no node')
})
expect(events).toContainEqual({ type: 'warning', message: 'check could not run: no node' })
expect(events.at(-1)?.type).toBe('unloaded')
})
it('does not check without --check', async () => {
const events = await watch(undefined)
expect(events.some((event) => event.type === 'check')).toBe(false)
})
})
describe('createPluginDevWatchCheckQueue', () => {
it('runs one check at a time and folds requests made meanwhile into one rerun', async () => {
const releases: (() => void)[] = []
const run = vi.fn(() => new Promise<void>((resolve) => releases.push(resolve)))
const queue = createPluginDevWatchCheckQueue(run)
queue.request()
queue.request()
queue.request()
expect(run).toHaveBeenCalledTimes(1)
releases.shift()?.()
await vi.waitFor(() => expect(run).toHaveBeenCalledTimes(2))
releases.shift()?.()
await queue.idle()
expect(run).toHaveBeenCalledTimes(2)
})
})
@@ -1,6 +1,6 @@
import { validatePluginTarget } from '../plugin-authoring/plugin-folder-validation'
import { checkPluginPanels, type PluginPanelCheck } from '../plugin-authoring/plugin-panel-check'
import { checkPluginEditors, type PluginEditorCheck } from '../plugin-authoring/plugin-editor-check'
import { validatePluginTarget } from '../authoring/plugin-folder-validation'
import { checkPluginPanels, type PluginPanelCheck } from '../authoring/plugin-panel-check'
import { checkPluginEditors, type PluginEditorCheck } from '../authoring/plugin-editor-check'
/** `orca plugin check` for one folder, as data: validation errors, else each panel's render. */
export type PluginDevWatchCheckResult = {
@@ -1,7 +1,7 @@
import type { PluginHostStatus } from '../../shared/plugins/plugin-list-entry'
import { formatPluginPanelCheck } from '../plugin-authoring/plugin-panel-check'
import type { PluginHostStatus } from '../../../shared/plugins/plugin-list-entry'
import { formatPluginPanelCheck } from '../authoring/plugin-panel-check'
import type { PluginDevWatchCheckResult } from './plugin-dev-watch-check'
import { formatPluginEditorCheck } from '../plugin-authoring/plugin-editor-check'
import { formatPluginEditorCheck } from '../authoring/plugin-editor-check'
import { PLUGIN_DEV_WATCH_SHOOTABLE, type PluginDevWatchShot } from './plugin-dev-watch-shot'
/** `missing`: the host lists no plugin for the folder (for example, its id changed). */
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
import type { LogsResult } from '../handlers/plugin-runtime'
import type { PluginHostListEntry } from '../../../shared/plugins/plugin-list-entry'
import type { LogsResult } from '../plugin-runtime'
import type { PluginDevWatchEvent } from './plugin-dev-watch-events'
import { formatPluginDevWatchEvent } from './plugin-dev-watch-events'
import { runPluginDevWatch, type PluginDevWatchHost } from './plugin-dev-watch-loop'
@@ -184,27 +184,6 @@ describe('runPluginDevWatch', () => {
})
})
it('prints the approval command once when an edit asks for new access', async () => {
const approvalNeeded = vi.fn(async () => ({
message: 'acme.notes asks for new access',
fingerprint: 'ap1-x',
command: 'orca plugin dev reload /work/notes --approve ap1-x'
}))
const { run, events } = harness({
stopAfterPolls: 6,
approvalNeeded,
lists: [
[entry({ devChangeCount: 1, status: 'pending', needsReconsent: true })],
[entry({ devChangeCount: 1, status: 'pending', needsReconsent: true })]
]
})
await run
expect(approvalNeeded).toHaveBeenCalledTimes(1)
expect(events.filter((event) => event.type === 'approval')).toEqual([
expect.objectContaining({ fingerprint: 'ap1-x', command: expect.stringContaining('reload') })
])
})
it('exits 1 without unloading when the host connection drops', async () => {
const { run, events, host } = harness({
failAt: { poll: 1, error: new LostError('connect ECONNREFUSED') }
@@ -222,21 +201,4 @@ describe('runPluginDevWatch', () => {
await expect(run).resolves.toBe(0)
expect(events.filter((event) => event.type === 'warning')).toHaveLength(1)
})
it('leaves a folder loaded before the session in place', async () => {
const { run, events, host } = harness({ added: false })
await expect(run).resolves.toBe(0)
expect(host.devUnload).not.toHaveBeenCalled()
expect(events.at(-1)).toMatchObject({ type: 'unloaded', removed: false })
})
it('exits at once on a second Ctrl-C while unloading', async () => {
let secondInterrupt: () => void = () => {}
const devUnload = vi.fn(async () => secondInterrupt())
const h = harness({ devUnload })
secondInterrupt = h.interrupt
await h.run
expect(devUnload).toHaveBeenCalledTimes(1)
expect(h.exitNow).toHaveBeenCalledWith(130)
})
})
@@ -1,5 +1,5 @@
import type { DevLoadResult, LogsResult } from '../handlers/plugin-runtime'
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
import type { DevLoadResult, LogsResult } from '../plugin-runtime'
import type { PluginHostListEntry } from '../../../shared/plugins/plugin-list-entry'
import type {
PluginDevWatchApproval,
PluginDevWatchEvent,
@@ -1,6 +1,8 @@
import { describe, expect, it, vi } from 'vitest'
import type { PluginHostListEntry, PluginHostStatus } from '../../shared/plugins/plugin-list-entry'
import { RuntimeClientError } from '../runtime/types'
import type {
PluginHostListEntry,
PluginHostStatus
} from '../../../shared/plugins/plugin-list-entry'
import { formatPluginDevWatchEvent, type PluginDevWatchEvent } from './plugin-dev-watch-events'
import { runPluginDevWatch, type PluginDevWatchHost } from './plugin-dev-watch-loop'
import {
@@ -102,40 +104,6 @@ const ofType = <T extends PluginDevWatchEvent['type']>(events: PluginDevWatchEve
events.filter((event): event is Extract<PluginDevWatchEvent, { type: T }> => event.type === type)
describe('plugin dev watch --shot', () => {
it('opens the panel once, then shoots it after the load and each reload, after the check', async () => {
const order: string[] = []
const { host, open } = shotHost(async (pluginKey, panelId) => {
order.push(`shot ${pluginKey}/${panelId}`)
return SHOT
})
const events = await watch(
{
check: async () => {
order.push('check')
return { ok: true, invalid: [], panels: [], editors: [] }
},
shot: host
},
[entry(1), entry(2)]
)
expect(open).toHaveBeenCalledTimes(1)
expect(open).toHaveBeenCalledWith(KEY, 'todos')
expect(order).toEqual([
'check',
`shot ${KEY}/todos`,
'check',
`shot ${KEY}/todos`,
'check',
`shot ${KEY}/todos`
])
const shots = ofType(events, 'shot')
expect(shots).toHaveLength(3)
expect(events.at(-1)?.type).toBe('unloaded')
expect(formatPluginDevWatchEvent(shots[0]!)).toBe(
'shot /work/shots/acme.todos-todos.png (360x700)'
)
})
it('shoots --shot-panel and fails at start, unloading, on an unknown id', async () => {
const { host, open } = shotHost(async () => ({ ...SHOT, panelId: 'stats' }), 'stats')
await watch({ shot: host })
@@ -178,42 +146,4 @@ describe('plugin dev watch --shot', () => {
'shot skipped: the right sidebar is closed, so todos is not on screen; open it again to resume'
)
})
it('skips a reload that left the plugin errored', async () => {
const { host, take } = shotHost(async () => SHOT)
await watch({ shot: host }, [entry(1, 'errored')])
expect(take).toHaveBeenCalledTimes(1)
})
it('turns itself off when the host can never capture', async () => {
const { host, take } = shotHost(async () => {
throw new RuntimeClientError('runtime_unsupported', 'too old')
})
host.open = async () => {
throw new RuntimeClientError('runtime_unsupported', 'too old')
}
const events = await watch({ shot: host }, [entry(1), entry(2)])
expect(take).not.toHaveBeenCalled()
expect(events).toContainEqual({
type: 'warning',
message: 'could not show todos for --shot, so --shot is off: too old'
})
})
it('keeps going after a one-off failure', async () => {
const outcomes: (() => Awaited<ReturnType<Take>>)[] = [
() => {
throw new Error('timeout')
},
() => ({ ...SHOT, settled: false, errors: ['boom'] })
]
const { host, take } = shotHost(async () => (outcomes.shift() ?? (() => SHOT))())
const events = await watch({ shot: host })
expect(take).toHaveBeenCalledTimes(2)
expect(events).toContainEqual({ type: 'warning', message: 'shot could not run: timeout' })
const last = ofType(events, 'shot').at(-1)
expect(last && formatPluginDevWatchEvent(last)).toBe(
'shot /work/shots/acme.todos-todos.png (360x700), not finished drawing\n error: boom'
)
})
})
@@ -1,5 +1,5 @@
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
import { RuntimeClientError, RuntimeRpcFailureError } from '../runtime/types'
import type { PluginHostListEntry } from '../../../shared/plugins/plugin-list-entry'
import { RuntimeClientError, RuntimeRpcFailureError } from '../../runtime/types'
import type { PluginDevWatchEvent } from './plugin-dev-watch-events'
/** One `--shot` PNG written after a load or reload. */
@@ -2,12 +2,13 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { COMMAND_SPECS } from '../specs/index'
import { findCommandSpec, normalizeCommandPositionals, parseArgs, specPaths } from '../args'
import type { HandlerContext } from '../dispatch'
import { reportCliError } from '../cli-error'
import type { RuntimeClient } from '../runtime-client'
import { RuntimeClientError, RuntimeRpcFailureError } from '../runtime/types'
import { COMMAND_SPECS } from '../../specs/index'
import { findCommandSpec, normalizeCommandPositionals, parseArgs } from '../../args'
import { specPaths } from '../../command-spec'
import type { HandlerContext } from '../../dispatch'
import { reportCliError } from '../../cli-error'
import type { RuntimeClient } from '../../runtime/client'
import { RuntimeClientError, RuntimeRpcFailureError } from '../../runtime/types'
import { bindPluginDevWatchHost, PluginHostUnreachableError } from './plugin-dev-watch'
function context(call: ReturnType<typeof vi.fn>, rawArgs: string[] = []): HandlerContext {
@@ -1,28 +1,20 @@
import type { CommandHandler, HandlerContext } from '../dispatch'
import { requirePluginPathArg } from '../plugin-authoring/plugin-cli-output'
import type { CommandHandler, HandlerContext } from '../../dispatch'
import { requirePluginPathArg } from '../plugin-cli-output'
import { formatPluginDevWatchEvent, type PluginDevWatchApproval } from './plugin-dev-watch-events'
import { runPluginDevWatch, type PluginDevWatchHost } from './plugin-dev-watch-loop'
import { onPluginDevWatchInterrupt, waitUnlessAborted } from './plugin-dev-watch-process'
import { RuntimeClientError, RuntimeRpcFailureError } from '../../runtime/types'
import { writeStdoutLine } from '../../stdout-line'
import type { PluginHostListEntry } from '../../../shared/plugins/plugin-list-entry'
import { CLI_BOOLEAN_FLAGS } from '../../../shared/cli-argument-boundary'
import { checkPluginDevFolder } from './plugin-dev-watch-check'
import { pluginDevWatchConfirmFor } from './plugin-dev-watch-approval'
import { pluginDevWatchShotPanel, type PluginDevWatchShotHost } from './plugin-dev-watch-shot'
import {
formatPluginDevWatchEvent,
type PluginDevWatchApproval
} from '../plugin-dev-watch/plugin-dev-watch-events'
import {
runPluginDevWatch,
type PluginDevWatchHost
} from '../plugin-dev-watch/plugin-dev-watch-loop'
import {
onPluginDevWatchInterrupt,
waitUnlessAborted
} from '../plugin-dev-watch/plugin-dev-watch-process'
import { RuntimeClientError, RuntimeRpcFailureError } from '../runtime/types'
import { writeStdoutLine } from '../stdout-line'
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
import { CLI_BOOLEAN_FLAGS } from '../../shared/cli-argument-boundary'
import { checkPluginDevFolder } from '../plugin-dev-watch/plugin-dev-watch-check'
import { pluginDevWatchConfirmFor } from '../plugin-dev-watch/plugin-dev-watch-approval'
import {
pluginDevWatchShotPanel,
type PluginDevWatchShotHost
} from '../plugin-dev-watch/plugin-dev-watch-shot'
import { defaultPluginShotPath, resolveLocalShotPath, takePluginShotIfShown } from './plugin-view'
defaultPluginShotPath,
resolveLocalShotPath,
takePluginShotIfShown
} from '../view/plugin-view'
import {
callPlugins,
refreshDevTypes,
@@ -30,7 +22,7 @@ import {
translatePluginRpcError,
type DevLoadResult,
type LogsResult
} from './plugin-runtime'
} from '../plugin-runtime'
/** The host did not answer: Orca quit, or the network to a remote server dropped. */
export class PluginHostUnreachableError extends Error {}
@@ -19,9 +19,4 @@ describe('PluginFrameLogDedupe', () => {
expect(dedupe.take([line(7, KEY_WARNING)])).toEqual([line(7, KEY_WARNING)])
expect(dedupe.flush()).toEqual([])
})
it('keeps the same text at another level apart', () => {
const dedupe = new PluginFrameLogDedupe()
expect(dedupe.take([line(1, '[frame] boom'), line(2, '[frame] boom', 'error')])).toHaveLength(2)
})
})
@@ -1,4 +1,4 @@
import type { LogsResult } from '../handlers/plugin-runtime'
import type { LogsResult } from '../plugin-runtime'
export type PluginLogLine = LogsResult['lines'][number]
@@ -1,5 +1,5 @@
import type { CommandHandler, HandlerContext } from '../dispatch'
import { printPluginResult, requirePluginPathArg } from '../plugin-authoring/plugin-cli-output'
import { printPluginResult, requirePluginPathArg } from './plugin-cli-output'
import { RuntimeClientError } from '../runtime/types'
import { callPlugins, devTargetParams, stringFlag } from './plugin-runtime'
@@ -1,6 +1,6 @@
import type { CommandHandler } from '../dispatch'
import { getOptionalPositiveIntegerFlag } from '../flags'
import { printPluginResult, requirePluginPathArg } from '../plugin-authoring/plugin-cli-output'
import { printPluginResult, requirePluginPathArg } from './plugin-cli-output'
import { parseDataFlag } from './plugin-dev-emit'
import { callPlugins, devTargetParams, stringFlag } from './plugin-runtime'
@@ -1,4 +1,4 @@
import type { HandlerGroup } from './handler-group-manifest'
import type { HandlerGroup } from '../handler-group-manifest'
// Authoring runs offline; runtime commands reach the Orca host. Split so authoring never loads the RPC graph.
export const PLUGIN_HANDLER_GROUPS: readonly HandlerGroup[] = [
@@ -13,7 +13,7 @@ export const PLUGIN_HANDLER_GROUPS: readonly HandlerGroup[] = [
'plugin test',
'plugin check'
],
load: async () => (await import('./handlers/plugin-authoring.js')).PLUGIN_AUTHORING_HANDLERS
load: async () => (await import('./authoring/plugin-authoring.js')).PLUGIN_AUTHORING_HANDLERS
},
{
name: 'plugin-runtime',
@@ -30,27 +30,27 @@ export const PLUGIN_HANDLER_GROUPS: readonly HandlerGroup[] = [
'plugin dev inspect',
'plugin dev unload'
],
load: async () => (await import('./handlers/plugin-runtime.js')).PLUGIN_RUNTIME_HANDLERS
load: async () => (await import('./plugin-runtime.js')).PLUGIN_RUNTIME_HANDLERS
},
{
name: 'plugin-dev-watch',
keys: ['plugin dev watch'],
load: async () => (await import('./handlers/plugin-dev-watch.js')).PLUGIN_DEV_WATCH_HANDLERS
load: async () => (await import('./dev-watch/plugin-dev-watch.js')).PLUGIN_DEV_WATCH_HANDLERS
},
{
name: 'plugin-dev-emit',
keys: ['plugin dev emit', 'plugin dev hook'],
load: async () => (await import('./handlers/plugin-dev-emit.js')).PLUGIN_DEV_EMIT_HANDLERS
load: async () => (await import('./plugin-dev-emit.js')).PLUGIN_DEV_EMIT_HANDLERS
},
{
name: 'plugin-dev-request',
keys: ['plugin dev request'],
load: async () => (await import('./handlers/plugin-dev-request.js')).PLUGIN_DEV_REQUEST_HANDLERS
load: async () => (await import('./plugin-dev-request.js')).PLUGIN_DEV_REQUEST_HANDLERS
},
{
name: 'plugin-view',
keys: ['plugin open', 'plugin shot'],
load: async () => (await import('./handlers/plugin-view.js')).PLUGIN_VIEW_HANDLERS
load: async () => (await import('./view/plugin-view.js')).PLUGIN_VIEW_HANDLERS
},
{
name: 'plugin-install',
@@ -62,6 +62,6 @@ export const PLUGIN_HANDLER_GROUPS: readonly HandlerGroup[] = [
'plugin catalog add',
'plugin catalog rm'
],
load: async () => (await import('./handlers/plugin-install.js')).PLUGIN_INSTALL_HANDLERS
load: async () => (await import('./plugin-install.js')).PLUGIN_INSTALL_HANDLERS
}
]
@@ -1,12 +1,10 @@
import { existsSync } from 'node:fs'
import { isAbsolute, join, resolve } from 'node:path'
import type { CommandHandler, HandlerContext } from '../dispatch'
import { printPluginResult, requirePluginPathArg } from '../plugin-authoring/plugin-cli-output'
import { printPluginResult, requirePluginPathArg } from './plugin-cli-output'
import { RuntimeClientError } from '../runtime/types'
import {
isQualifiedPluginKey,
PLUGIN_MANIFEST_FILENAME
} from '../../shared/plugins/manifest/plugin-manifest'
import { isQualifiedPluginKey } from '../../shared/plugins/frames/plugin-tab-key'
import { PLUGIN_MANIFEST_FILENAME } from '../../shared/plugins/manifest/plugin-manifest'
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
import {
pluginInstallSourceLines,
@@ -1,7 +1,7 @@
import { existsSync } from 'node:fs'
import { isAbsolute, resolve } from 'node:path'
import type { CommandHandler, HandlerContext } from '../dispatch'
import { printPluginResult, requirePluginPathArg } from '../plugin-authoring/plugin-cli-output'
import { printPluginResult, requirePluginPathArg } from './plugin-cli-output'
import { RuntimeClientError, RuntimeRpcFailureError } from '../runtime/types'
import {
approvalFingerprintOf,
@@ -10,7 +10,7 @@ import {
pendingApprovalsOf,
perPluginApprovalCommands
} from './plugin-approval-rerun'
import { isQualifiedPluginKey } from '../../shared/plugins/manifest/plugin-manifest'
import { isQualifiedPluginKey } from '../../shared/plugins/frames/plugin-tab-key'
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
export type DevLoadResult = {
@@ -156,7 +156,7 @@ export async function refreshDevTypes(ctx: HandlerContext, path: string): Promis
return []
}
// Lazy: the manifest schema it parses with is heavy for every other runtime command.
const types = await import('../plugin-authoring/plugin-type-files.js')
const types = await import('./authoring/plugin-type-files.js')
if (!(await types.pluginFolderWantsTypes(path))) {
return []
}
@@ -2,10 +2,11 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { COMMAND_SPECS } from '../specs'
import { normalizeCommandPositionals, parseArgs, specPaths } from '../args'
import type { HandlerContext } from '../dispatch'
import type { RuntimeClient } from '../runtime-client'
import { COMMAND_SPECS } from '../../specs/index'
import { normalizeCommandPositionals, parseArgs } from '../../args'
import { specPaths } from '../../command-spec'
import type { HandlerContext } from '../../dispatch'
import type { RuntimeClient } from '../../runtime/client'
import { PLUGIN_VIEW_HANDLERS } from './plugin-view'
function context(argv: string[], call: ReturnType<typeof vi.fn>, cwd: string): HandlerContext {
@@ -1,9 +1,9 @@
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import type { HandlerContext } from '../dispatch'
import { printPluginResult } from '../plugin-authoring/plugin-cli-output'
import { RuntimeClientError } from '../runtime/types'
import { callPlugins } from './plugin-runtime'
import type { HandlerContext } from '../../dispatch'
import { printPluginResult } from '../plugin-cli-output'
import { RuntimeClientError } from '../../runtime/types'
import { callPlugins } from '../plugin-runtime'
import { editorShotPluginKey } from './plugin-shot-editor'
type ChromeRegion = 'status-bar' | 'window'
@@ -2,10 +2,11 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { COMMAND_SPECS } from '../specs'
import { normalizeCommandPositionals, parseArgs, specPaths } from '../args'
import type { HandlerContext } from '../dispatch'
import type { RuntimeClient } from '../runtime-client'
import { COMMAND_SPECS } from '../../specs/index'
import { normalizeCommandPositionals, parseArgs } from '../../args'
import { specPaths } from '../../command-spec'
import type { HandlerContext } from '../../dispatch'
import type { RuntimeClient } from '../../runtime/client'
import { PLUGIN_VIEW_HANDLERS } from './plugin-view'
// A 1x1 PNG.
@@ -1,22 +1,22 @@
import { existsSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, join, resolve } from 'node:path'
import type { HandlerContext } from '../dispatch'
import { getOptionalPositiveIntegerFlag } from '../flags'
import { printPluginResult } from '../plugin-authoring/plugin-cli-output'
import { RuntimeClientError } from '../runtime/types'
import { isQualifiedPluginKey } from '../../shared/plugins/manifest/plugin-manifest'
import type { HandlerContext } from '../../dispatch'
import { getOptionalPositiveIntegerFlag } from '../../flags'
import { printPluginResult } from '../plugin-cli-output'
import { RuntimeClientError } from '../../runtime/types'
import { isQualifiedPluginKey } from '../../../shared/plugins/frames/plugin-tab-key'
import {
parsePluginEditorShotActs,
type PluginEditorShotAct
} from '../../shared/plugins/view/plugin-editor-shot-acts'
} from '../../../shared/plugins/view/plugin-editor-shot-acts'
import {
PLUGIN_SHOT_MAX_HEIGHT,
PLUGIN_SHOT_MAX_WIDTH,
PLUGIN_SHOT_MIN_HEIGHT,
PLUGIN_SHOT_MIN_WIDTH
} from '../../shared/rpc-contract/plugins-params'
import { callPlugins, resolveHostPluginPath, stringFlag } from './plugin-runtime'
} from '../../../shared/rpc-contract/plugins-params'
import { callPlugins, resolveHostPluginPath, stringFlag } from '../plugin-runtime'
import { readPluginFolderKey } from './plugin-shot-scenario'
/** What `plugins.shotEditor` answers; `png` is base64. */
@@ -2,12 +2,13 @@ import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { COMMAND_SPECS } from '../specs/index'
import { normalizeCommandPositionals, parseArgs, specPaths } from '../args'
import type { HandlerContext } from '../dispatch'
import type { RuntimeClient } from '../runtime-client'
import { RuntimeRpcFailureError } from '../runtime/types'
import { createPluginFromTemplate } from '../plugin-authoring/plugin-create'
import { COMMAND_SPECS } from '../../specs/index'
import { normalizeCommandPositionals, parseArgs } from '../../args'
import { specPaths } from '../../command-spec'
import type { HandlerContext } from '../../dispatch'
import type { RuntimeClient } from '../../runtime/client'
import { RuntimeRpcFailureError } from '../../runtime/types'
import { createPluginFromTemplate } from '../authoring/plugin-create'
import { PLUGIN_VIEW_HANDLERS, type PluginShotReply } from './plugin-view'
const check = vi.hoisted(() => ({
@@ -15,7 +16,7 @@ const check = vi.hoisted(() => ({
checkPluginPanels: vi.fn(),
formatPluginPanelCheck: vi.fn(() => [' src/panel.tsx: boom'])
}))
vi.mock('../plugin-authoring/plugin-panel-check', () => check)
vi.mock('../authoring/plugin-panel-check', () => check)
const PNG_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
@@ -1,20 +1,20 @@
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, join, resolve } from 'node:path'
import type { HandlerContext } from '../dispatch'
import { RuntimeClientError } from '../runtime/types'
import type { PluginHostListEntry } from '../../shared/plugins/plugin-list-entry'
import type { HandlerContext } from '../../dispatch'
import { RuntimeClientError } from '../../runtime/types'
import type { PluginHostListEntry } from '../../../shared/plugins/plugin-list-entry'
import { isQualifiedPluginKey } from '../../../shared/plugins/frames/plugin-tab-key'
import {
isQualifiedPluginKey,
parsePluginManifest,
qualifiedPluginKey,
type PluginManifest
} from '../../shared/plugins/manifest/plugin-manifest'
} from '../../../shared/plugins/manifest/plugin-manifest'
import {
localPluginPublisher,
withPluginPublisher
} from '../../shared/plugins/installation/plugin-publisher-derivation'
import { callPlugins } from './plugin-runtime'
} from '../../../shared/plugins/installation/plugin-publisher-derivation'
import { callPlugins } from '../plugin-runtime'
import { enclosingPluginFolder } from './plugin-shot-default-dir'
import type { PluginShotReply, PluginShotSaved } from './plugin-view'
@@ -162,7 +162,7 @@ export async function takePluginScenarioShot(
const { dir, manifest, pluginKey } = await resolveFolder(ctx, request.target)
const panel = scenarioPanel(manifest, pluginKey, request.panelId)
// Lazy: the check pulls in the declarative tree validator and the Node selection.
const check = await import('../plugin-authoring/plugin-panel-check.js')
const check = await import('../authoring/plugin-panel-check.js')
const names = await check.readPluginCheckScenarioNames(dir, panel.entry).catch((error) => {
throw refuse('invalid_argument', error instanceof Error ? error.message : String(error))
})
@@ -2,11 +2,12 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { COMMAND_SPECS } from '../specs/index'
import { normalizeCommandPositionals, parseArgs, specPaths } from '../args'
import type { HandlerContext } from '../dispatch'
import type { RuntimeClient } from '../runtime-client'
import type { PluginShotActResult } from '../../shared/plugins/view/plugin-shot-acts'
import { COMMAND_SPECS } from '../../specs/index'
import { normalizeCommandPositionals, parseArgs } from '../../args'
import { specPaths } from '../../command-spec'
import type { HandlerContext } from '../../dispatch'
import type { RuntimeClient } from '../../runtime/client'
import type { PluginShotActResult } from '../../../shared/plugins/view/plugin-shot-acts'
import { PLUGIN_VIEW_HANDLERS, type PluginShotReply } from './plugin-view'
// A 1x1 PNG.
@@ -1,19 +1,19 @@
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, join, resolve } from 'node:path'
import type { CommandHandler, HandlerContext } from '../dispatch'
import { getOptionalPositiveIntegerFlag } from '../flags'
import { printPluginResult } from '../plugin-authoring/plugin-cli-output'
import { RuntimeClientError } from '../runtime/types'
import type { CommandHandler, HandlerContext } from '../../dispatch'
import { getOptionalPositiveIntegerFlag } from '../../flags'
import { printPluginResult } from '../plugin-cli-output'
import { RuntimeClientError } from '../../runtime/types'
import {
PLUGIN_SHOT_MAX_WIDTH,
PLUGIN_SHOT_MIN_WIDTH
} from '../../shared/rpc-contract/plugins-params'
} from '../../../shared/rpc-contract/plugins-params'
import {
parsePluginShotActs,
type PluginShotAct,
type PluginShotActResult
} from '../../shared/plugins/view/plugin-shot-acts'
import { callPlugins, requirePluginKey, resolveHostPluginPath, stringFlag } from './plugin-runtime'
} from '../../../shared/plugins/view/plugin-shot-acts'
import { callPlugins, requirePluginKey, resolveHostPluginPath, stringFlag } from '../plugin-runtime'
import { defaultPluginShotsDir } from './plugin-shot-default-dir'
import { takePluginScenarioShot } from './plugin-shot-scenario'
import { runPluginEditorShot } from './plugin-shot-editor'
+1 -1
View File
@@ -1,7 +1,7 @@
import type { CommandHandler } from '../dispatch'
import { RuntimeClientError } from '../runtime/types'
import { resolveLaunchUserDataPath } from '../runtime/launch-user-data-path'
import { pluginDevWatchConfirmFor } from '../plugin-dev-watch/plugin-dev-watch-approval'
import { pluginDevWatchConfirmFor } from '../plugins/dev-watch/plugin-dev-watch-approval'
const RESET_WARNING =
"This gives this machine's Orca server a new pairing key and installation id and removes every " +
@@ -1,4 +1,4 @@
import { RuntimeClientError } from '../runtime-client'
import { RuntimeClientError } from '../runtime/types'
export type BundledSkillGuideReference = {
name: string
@@ -21,7 +21,7 @@ function canonicalGuides(guides: readonly BundledSkillGuide[]): BundledSkillGuid
/** Defer the large guide table so unrelated commands avoid its startup cost. */
export async function loadCanonicalGuides(): Promise<BundledSkillGuide[]> {
const { BUNDLED_SKILL_GUIDES } = await import('../bundled-skill-guides.js')
const { BUNDLED_SKILL_GUIDES } = await import('./bundled-skill-guides.js')
return canonicalGuides(BUNDLED_SKILL_GUIDES)
}
@@ -1,4 +1,4 @@
// Generated by config/scripts/generate-bundled-skill-guides.mjs. Do not edit.
// Generated by config/scripts/skills/bundle-generation/generate-bundled-skill-guides.mjs. Do not edit.
export type BundledSkillGuideReference = {
readonly name: string
@@ -1,11 +1,11 @@
import { homedir } from 'node:os'
import { realpath } from 'node:fs/promises'
import type { CommandHandler } from '../dispatch'
import { RuntimeClientError } from '../runtime-client'
import { RuntimeClientError } from '../runtime/types'
import { getDefaultUserDataPath } from '../runtime/metadata'
import { getRepeatedStringFlag } from '../flags'
import { writeStdoutLine } from '../stdout-line'
import { skillsCliAgentAliases } from '../skills-cli-agent-aliases'
import { skillsCliAgentAliases } from './skills-cli-agent-aliases'
import { loadCanonicalGuides, type BundledSkillGuide } from './bundled-skill-guide-table'
import type {
BundledSkillOp,
@@ -1,5 +1,5 @@
import type { CommandHandler, HandlerContext } from '../dispatch'
import { RuntimeClientError } from '../runtime-client'
import { RuntimeClientError } from '../runtime/types'
import { writeStdoutLine } from '../stdout-line'
import {
loadCanonicalGuides,
@@ -41,9 +41,7 @@ vi.mock('../runtime-client', async () => {
}
})
import { BUNDLED_SKILL_GUIDES } from '../bundled-skill-guides'
import { main } from '../index'
import { GUIDE_HOST_CAPABILITIES } from './skill-guide-host-gate'
const PUBLISHING_TEXT = /artifacts share|skills share|## Artifacts|## Skill Sharing/
@@ -75,29 +73,6 @@ describe('skills get host-capability gating', () => {
process.exitCode = undefined
})
it('only ships closed, unnested blocks that name a known capability', () => {
const documents = BUNDLED_SKILL_GUIDES.flatMap((guide) => [
guide.markdown,
...guide.references.map((reference) => reference.markdown)
])
for (const markdown of documents) {
let open: string | null = null
for (const [, closing, name] of markdown.matchAll(
/<!-- (\/?)host-capability(?:: ([a-z-]+))? -->/g
)) {
if (closing) {
expect(open).not.toBeNull()
open = null
continue
}
expect(open).toBeNull()
expect(GUIDE_HOST_CAPABILITIES).toContain(name)
open = name
}
expect(open).toBeNull()
}
})
it.each<[string, Host]>([
['publishing is off', { settings: { artifactSharingEnabled: false } }],
['the host omits the settings (headless or older)', { settings: {} }],
@@ -160,11 +135,4 @@ describe('skills get host-capability gating', () => {
expect(guide.stdout).toContain('## Artifacts')
expect(guide.stdout).not.toContain('## Skill Sharing')
})
it('serves an ungated reference without contacting the host', async () => {
const { stdout, stderr } = await skillsGet(['--reference', 'browser'], 'no-access')
expect(stderr).toBe('')
expect(stdout.length).toBeGreaterThan(0)
})
})
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { REPEATED_FLAG_SEPARATOR } from '../args'
import { RuntimeClient, RuntimeRpcFailureError } from '../runtime-client'
import { RuntimeClient } from '../runtime/client'
import { RuntimeRpcFailureError } from '../runtime/types'
import type { HandlerContext } from '../dispatch'
import { SKILL_SHARING_HANDLERS } from './skill-sharing'
@@ -85,123 +85,6 @@ describe('skill sharing CLI handlers', () => {
expect(call).toHaveBeenCalledExactlyOnceWith('settings.get')
})
it('publishes multiple explicit skills and prints only public output', async () => {
const call = vi
.fn()
.mockResolvedValueOnce({
id: 'settings',
ok: true,
result: { settings: { agentSkillSharingEnabled: true } },
_meta: successMeta
})
.mockResolvedValueOnce({
id: 'share',
ok: true,
result: {
status: 'ok',
value: {
share: { id: 'shr_public', url: 'https://share.onorca.dev/skills/share/shr_public' },
version: {
packageId: 'pkg_public',
versionId: 'ver_public',
name: 'team-skills'
},
selectedSkills: [
{ id: 'alpha-id', name: 'alpha', description: null },
{ id: 'beta-id', name: 'beta', description: null }
]
}
},
_meta: successMeta
})
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined)
await SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', `alpha${REPEATED_FLAG_SEPARATOR}beta`],
['bundle-name', 'team-skills'],
['release-notes', 'Initial bundle']
]),
{ json: true }
)
)
expect(call).toHaveBeenLastCalledWith(
'skills.share',
{
skillSelectors: ['alpha', 'beta'],
bundleName: 'team-skills',
releaseNotes: 'Initial bundle',
target: { cwd: '/repo' }
},
{ timeoutMs: 600_000 }
)
const output = String(log.mock.calls[0][0])
expect(JSON.parse(output).result).toEqual({
url: 'https://share.onorca.dev/skills/share/shr_public',
shareId: 'shr_public',
packageId: 'pkg_public',
versionId: 'ver_public',
bundleName: 'team-skills',
skills: [
{ id: 'alpha-id', name: 'alpha', description: null },
{ id: 'beta-id', name: 'beta', description: null }
]
})
})
it('normalizes a human-readable bundle name before publishing', async () => {
const call = vi
.fn()
.mockResolvedValueOnce({
id: 'settings',
ok: true,
result: { settings: { agentSkillSharingEnabled: true } },
_meta: successMeta
})
.mockResolvedValueOnce({
id: 'share',
ok: true,
result: {
status: 'ok',
value: {
share: { id: 'shr_public', url: 'https://share.onorca.dev/skills/share/shr_public' },
version: {
packageId: 'pkg_public',
versionId: 'ver_public',
name: 'team-skills-v2.0'
},
selectedSkills: [{ id: 'alpha-id', name: 'alpha', description: null }]
}
},
_meta: successMeta
})
vi.spyOn(console, 'log').mockImplementation(() => undefined)
await SKILL_SHARING_HANDLERS['skills share']!(
context(
call,
new Map([
['skill', 'alpha'],
['bundle-name', 'Téam Skills -- v2.0']
])
)
)
expect(call).toHaveBeenLastCalledWith(
'skills.share',
{
skillSelectors: ['alpha'],
bundleName: 'team-skills-v2.0',
releaseNotes: '',
target: { cwd: '/repo' }
},
{ timeoutMs: 600_000 }
)
})
it('rejects a bundle name that cannot produce a valid package name', async () => {
const call = vi.fn()
@@ -13,11 +13,8 @@ import type { SkillDiscoveryResult } from '../../shared/skills'
import type { CommandHandler, HandlerContext } from '../dispatch'
import { getRepeatedStringFlag } from '../flags'
import { printResult } from '../command-result-output'
import {
RuntimeClientError,
RuntimeRpcFailureError,
type RuntimeRpcSuccess
} from '../runtime-client'
import { RuntimeClientError, RuntimeRpcFailureError } from '../runtime/types'
import type { RuntimeRpcSuccess } from '../../shared/runtime-rpc-envelope'
const SHARE_TIMEOUT_MS = 10 * 60_000
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT } from '../shared/skills-cli-agent-keys'
import { SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT } from '../../shared/skills-cli-agent-keys'
import { skillsCliAgentAliases } from './skills-cli-agent-aliases'
describe('skills CLI agent aliases', () => {
@@ -1,7 +1,7 @@
import {
isSkillInstallProviderId,
type SkillInstallProviderId
} from '../shared/skill-install-providers'
} from '../../shared/skill-install-providers'
const CANONICAL_PROVIDER_BY_SKILLS_CLI_KEY = new Map<string, SkillInstallProviderId>([
['claude-code', 'claude'],
@@ -13,7 +13,7 @@ import {
import { tmpdir } from 'node:os'
import { isAbsolute, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { BundledSkillInstallResult, BundledSkillPlan } from '../shared/bundled-skill-plan'
import type { BundledSkillInstallResult, BundledSkillPlan } from '../../shared/bundled-skill-plan'
const NAME = 'orca-cli'
const OLD_TEXT = `---\nname: ${NAME}\ndescription: Old\n---\n\nOld body\n`
@@ -24,31 +24,36 @@ const sha = (text: string) => createHash('sha256').update(text).digest('hex')
const { detectAgentsMock } = vi.hoisted(() => ({ detectAgentsMock: vi.fn() }))
// Use the real installer in a temporary home; pin bundled bytes and agent detection for portability.
vi.mock('../main/skills/bundled-skill-install/bundled-skill-sources', () => ({
vi.mock('../../main/skills/bundled-skill-install/bundled-skill-sources', () => ({
BUNDLED_SKILL_SOURCES: [
{ name: NAME, text: NEW_TEXT, releasedDigests: [sha(OLD_TEXT), sha(NEW_TEXT)] }
]
}))
vi.mock('../main/preflight/installed-agent-detection', () => ({
vi.mock('../../main/preflight/installed-agent-detection', () => ({
detectInstalledAgentsWithShellPathHydration: detectAgentsMock
}))
// Avoid the production five-second lock wait in tests.
vi.mock('../main/skills/bundled-skill-install/bundled-skill-local-host', async (importOriginal) => {
const actual = await importOriginal<typeof LocalHost>()
return {
localBundledSkillInput: async (...args: Parameters<typeof actual.localBundledSkillInput>) => ({
...(await actual.localBundledSkillInput(...args)),
lockTimeoutMs: 0
})
vi.mock(
'../../main/skills/bundled-skill-install/bundled-skill-local-host',
async (importOriginal) => {
const actual = await importOriginal<typeof LocalHost>()
return {
localBundledSkillInput: async (
...args: Parameters<typeof actual.localBundledSkillInput>
) => ({
...(await actual.localBundledSkillInput(...args)),
lockTimeoutMs: 0
})
}
}
})
)
import type * as LocalHost from '../main/skills/bundled-skill-install/bundled-skill-local-host'
import { main } from './index'
import type * as LocalHost from '../../main/skills/bundled-skill-install/bundled-skill-local-host'
import { main } from '../index'
import {
acquireSkillInstallLock,
skillInstallLockPath
} from '../main/skills/transaction-state/skill-install-lock'
} from '../../main/skills/transaction-state/skill-install-lock'
type Paths = { root: string; home: string; userData: string; shared: string; claude: string }
@@ -22,8 +22,8 @@ vi.mock('./bundled-skill-guides.js', () => ({
]
}))
vi.mock('./runtime-client', async () => {
const { RuntimeClientError, RuntimeRpcFailureError } = await import('./runtime/types.js')
vi.mock('../runtime-client', async () => {
const { RuntimeClientError, RuntimeRpcFailureError } = await import('../runtime/types.js')
class RuntimeClient {
constructor() {
throw new Error('skills get constructed a RuntimeClient')
@@ -38,7 +38,7 @@ vi.mock('./runtime-client', async () => {
}
})
import { main } from './index'
import { main } from '../index'
function stdoutText(spy: ReturnType<typeof vi.spyOn>): string {
return spy.mock.calls.map((call) => String(call[0])).join('')
+1 -1
View File
@@ -1,6 +1,6 @@
import type { CommandSpec } from '../args'
import { GLOBAL_FLAGS, LOCAL_COMMAND_FLAGS } from '../args'
import { PLUGIN_TEMPLATE_NAMES } from '../plugin-authoring/plugin-template-names'
import { PLUGIN_TEMPLATE_NAMES } from '../plugins/authoring/plugin-template-names'
import { PLUGIN_DEV_HOOK_COMMAND_SPEC, PLUGIN_DEV_REQUEST_COMMAND_SPEC } from './plugin-dev-request'
const TEMPLATES = PLUGIN_TEMPLATE_NAMES.join('|')
@@ -1,11 +0,0 @@
/** The main-process plugin checks `orca plugin validate|inspect|release-entry` runs. One CLI entry so the
* packaged CLI loads them after electron-vite replaces out/main; keep it free of Electron. */
export {
validateDeclaredPluginArtifacts,
validatePluginInstallContent
} from './plugin-artifact-validation'
export { hashPluginTree } from './plugin-content-hash'
export {
pluginCatalogReleaseForHead,
type PluginCatalogReleaseReport
} from './plugin-catalog-release-entry'
+1 -1
View File
@@ -14,7 +14,7 @@ import { expect, test } from './helpers/orca-app'
import { orcaCliResult, runCompiledOrcaCli } from './helpers/compiled-orca-cli'
import { RuntimeClient } from '../../src/cli/runtime/client'
import { disablePluginSystem, enableDevPlugins } from './helpers/plugin-smoke'
import { createPluginFromTemplate } from '../../src/cli/plugin-authoring/plugin-create'
import { createPluginFromTemplate } from '../../src/cli/plugins/authoring/plugin-create'
import { ISOLATED_FRAMES_E2E } from './helpers/plugin-frame-guest'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'