@oneworks/cli 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/channel.js +7 -0
- package/cli.js +5 -0
- package/mem.js +7 -0
- package/package.json +59 -0
- package/postinstall.js +75 -0
- package/src/AGENTS.md +169 -0
- package/src/channel-cli.ts +19 -0
- package/src/cli-argv.ts +27 -0
- package/src/cli.ts +63 -0
- package/src/commands/@core/adapter-option.ts +85 -0
- package/src/commands/@core/extra-options.ts +12 -0
- package/src/commands/@core/plugin-install.ts +1 -0
- package/src/commands/@core/plugin-source.ts +1 -0
- package/src/commands/accounts.ts +204 -0
- package/src/commands/adapter/prepare-selection.ts +181 -0
- package/src/commands/adapter/prepare.ts +104 -0
- package/src/commands/adapter.ts +48 -0
- package/src/commands/agent/actions.ts +176 -0
- package/src/commands/agent/runtime-store-commands.ts +56 -0
- package/src/commands/agent/runtime-store-events.ts +23 -0
- package/src/commands/agent/runtime-store-session.ts +170 -0
- package/src/commands/agent/runtime-store-shared.ts +139 -0
- package/src/commands/agent/runtime-store.ts +4 -0
- package/src/commands/agent.ts +81 -0
- package/src/commands/benchmark.ts +198 -0
- package/src/commands/channel.ts +594 -0
- package/src/commands/clear.ts +140 -0
- package/src/commands/config/actions.ts +196 -0
- package/src/commands/config/display-state.ts +108 -0
- package/src/commands/config/index.ts +135 -0
- package/src/commands/config/interactive.ts +121 -0
- package/src/commands/config/read-state.ts +56 -0
- package/src/commands/config/section-state.ts +109 -0
- package/src/commands/config/shared.ts +195 -0
- package/src/commands/kill.ts +41 -0
- package/src/commands/list.ts +224 -0
- package/src/commands/memory/context.ts +76 -0
- package/src/commands/memory/entries.ts +131 -0
- package/src/commands/memory/shared.ts +89 -0
- package/src/commands/memory/store.ts +69 -0
- package/src/commands/memory/target.ts +54 -0
- package/src/commands/memory.ts +97 -0
- package/src/commands/plugin.ts +62 -0
- package/src/commands/report-targets.ts +149 -0
- package/src/commands/report.ts +232 -0
- package/src/commands/run/adapter-cli-version.ts +65 -0
- package/src/commands/run/command.ts +982 -0
- package/src/commands/run/input-bridge.ts +108 -0
- package/src/commands/run/input-control.ts +112 -0
- package/src/commands/run/input-decision.ts +88 -0
- package/src/commands/run/options.ts +104 -0
- package/src/commands/run/output.ts +179 -0
- package/src/commands/run/permission-decision.ts +19 -0
- package/src/commands/run/permission-recovery.ts +194 -0
- package/src/commands/run/permission-state.ts +177 -0
- package/src/commands/run/print-idle-timeout.ts +47 -0
- package/src/commands/run/protocol-envelope.ts +111 -0
- package/src/commands/run/protocol-stdio.ts +71 -0
- package/src/commands/run/protocol.ts +391 -0
- package/src/commands/run/runtime-command-bridge.ts +190 -0
- package/src/commands/run/runtime-event-sink.ts +560 -0
- package/src/commands/run/session-exit-controller.ts +45 -0
- package/src/commands/run/types.ts +65 -0
- package/src/commands/run.ts +62 -0
- package/src/commands/session-control.ts +133 -0
- package/src/commands/skills/add-command.ts +88 -0
- package/src/commands/skills/install-command.ts +105 -0
- package/src/commands/skills/install.ts +216 -0
- package/src/commands/skills/progress.ts +126 -0
- package/src/commands/skills/publish-command.ts +85 -0
- package/src/commands/skills/register.ts +17 -0
- package/src/commands/skills/remove-command.ts +102 -0
- package/src/commands/skills/shared.ts +117 -0
- package/src/commands/skills/sync.ts +571 -0
- package/src/commands/skills/types.ts +33 -0
- package/src/commands/skills.ts +1 -0
- package/src/commands/stop.ts +41 -0
- package/src/config.ts +1 -0
- package/src/default-skill-plugin.ts +29 -0
- package/src/env.ts +1 -0
- package/src/hooks/plugins/index.ts +66 -0
- package/src/mem-cli.ts +19 -0
- package/src/session-cache.ts +250 -0
- package/src/session-permission-cache.ts +40 -0
- package/src/utils.ts +25 -0
- package/src/workspace.ts +12 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getCliDefaultSkillNames, getCliDefaultSkillPluginConfig } from '#~/default-skill-plugin.js'
|
|
2
|
+
import { createAdapterOption, normalizeCliAdapterOptionValue, parseCliAdapterOptionValue } from './@core/adapter-option'
|
|
3
|
+
import { applyAdapterCliVersionEnv, persistAdapterCliVersionSelection } from './run/adapter-cli-version'
|
|
4
|
+
import { registerRunCommand } from './run/command'
|
|
5
|
+
import { parseCliInputControlEvent } from './run/input-control'
|
|
6
|
+
import {
|
|
7
|
+
getDisallowedResumeFlags,
|
|
8
|
+
resolveDefaultOneworksMcpServerOption,
|
|
9
|
+
resolveInjectDefaultSystemPromptOption,
|
|
10
|
+
resolveResumeAdapterOptions,
|
|
11
|
+
resolveRunMode
|
|
12
|
+
} from './run/options'
|
|
13
|
+
import {
|
|
14
|
+
getAdapterErrorMessage,
|
|
15
|
+
getAdapterInteractionMessage,
|
|
16
|
+
getPrintableAssistantText,
|
|
17
|
+
handlePrintEvent,
|
|
18
|
+
resolvePrintableStopText,
|
|
19
|
+
shouldPrintResumeHint
|
|
20
|
+
} from './run/output'
|
|
21
|
+
import { createPrintIdleTimeoutController, parsePrintIdleTimeoutSeconds } from './run/print-idle-timeout'
|
|
22
|
+
import {
|
|
23
|
+
executeRuntimeProtocolCommand,
|
|
24
|
+
shouldStartRuntimeConsumer,
|
|
25
|
+
shouldStartRuntimeResumeConsumer
|
|
26
|
+
} from './run/protocol'
|
|
27
|
+
import { runRuntimeProtocolStdio } from './run/protocol-stdio'
|
|
28
|
+
import { createSessionExitController } from './run/session-exit-controller'
|
|
29
|
+
import { RUN_INPUT_FORMATS, RUN_OUTPUT_FORMATS } from './run/types'
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
RUN_INPUT_FORMATS,
|
|
33
|
+
RUN_OUTPUT_FORMATS,
|
|
34
|
+
applyAdapterCliVersionEnv,
|
|
35
|
+
createAdapterOption,
|
|
36
|
+
createPrintIdleTimeoutController,
|
|
37
|
+
createSessionExitController,
|
|
38
|
+
executeRuntimeProtocolCommand,
|
|
39
|
+
getAdapterErrorMessage,
|
|
40
|
+
getAdapterInteractionMessage,
|
|
41
|
+
getCliDefaultSkillNames,
|
|
42
|
+
getCliDefaultSkillPluginConfig,
|
|
43
|
+
getDisallowedResumeFlags,
|
|
44
|
+
getPrintableAssistantText,
|
|
45
|
+
handlePrintEvent,
|
|
46
|
+
normalizeCliAdapterOptionValue,
|
|
47
|
+
parseCliAdapterOptionValue,
|
|
48
|
+
parseCliInputControlEvent,
|
|
49
|
+
parsePrintIdleTimeoutSeconds,
|
|
50
|
+
persistAdapterCliVersionSelection,
|
|
51
|
+
registerRunCommand,
|
|
52
|
+
resolveDefaultOneworksMcpServerOption,
|
|
53
|
+
resolveInjectDefaultSystemPromptOption,
|
|
54
|
+
resolvePrintableStopText,
|
|
55
|
+
resolveResumeAdapterOptions,
|
|
56
|
+
resolveRunMode,
|
|
57
|
+
runRuntimeProtocolStdio,
|
|
58
|
+
shouldPrintResumeHint,
|
|
59
|
+
shouldStartRuntimeConsumer,
|
|
60
|
+
shouldStartRuntimeResumeConsumer
|
|
61
|
+
}
|
|
62
|
+
export type { RunInputFormat, RunOptions, RunOutputFormat } from './run/types'
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
3
|
+
|
|
4
|
+
import { resolveCliSession, writeCliSessionControl, writeCliSessionRecord } from '#~/session-cache.js'
|
|
5
|
+
|
|
6
|
+
const STOP_SIGNAL_TIMEOUT_MS = {
|
|
7
|
+
SIGTERM: 10_000,
|
|
8
|
+
SIGKILL: 2_000
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
const isProcessAlive = (
|
|
12
|
+
pid: number,
|
|
13
|
+
sendSignal: (pid: number, signal?: NodeJS.Signals | number) => void
|
|
14
|
+
) => {
|
|
15
|
+
try {
|
|
16
|
+
sendSignal(pid, 0)
|
|
17
|
+
return true
|
|
18
|
+
} catch (error: any) {
|
|
19
|
+
if (error.code === 'ESRCH') return false
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const waitForProcessExit = async (params: {
|
|
25
|
+
pid: number
|
|
26
|
+
timeoutMs: number
|
|
27
|
+
sendSignal?: (pid: number, signal?: NodeJS.Signals | number) => void
|
|
28
|
+
}) => {
|
|
29
|
+
const sendSignal = params.sendSignal ?? process.kill
|
|
30
|
+
const deadline = Date.now() + params.timeoutMs
|
|
31
|
+
|
|
32
|
+
while (Date.now() < deadline) {
|
|
33
|
+
if (!isProcessAlive(params.pid, sendSignal)) return true
|
|
34
|
+
await delay(100)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return !isProcessAlive(params.pid, sendSignal)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const markCliSessionStopped = async (params: {
|
|
41
|
+
cwd: string
|
|
42
|
+
record: Awaited<ReturnType<typeof resolveCliSession>>
|
|
43
|
+
endTime: number
|
|
44
|
+
}) => {
|
|
45
|
+
const detail = params.record.detail
|
|
46
|
+
if (detail == null) return
|
|
47
|
+
|
|
48
|
+
await writeCliSessionRecord(params.cwd, detail.ctxId, detail.sessionId, {
|
|
49
|
+
...params.record,
|
|
50
|
+
detail: {
|
|
51
|
+
...detail,
|
|
52
|
+
status: 'stopped',
|
|
53
|
+
endTime: params.endTime
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const signalCliSession = async (params: {
|
|
59
|
+
cwd: string
|
|
60
|
+
sessionId: string
|
|
61
|
+
signal: 'SIGTERM' | 'SIGKILL'
|
|
62
|
+
sendSignal?: (pid: number, signal?: NodeJS.Signals | number) => void
|
|
63
|
+
waitForExit?: typeof waitForProcessExit
|
|
64
|
+
now?: () => number
|
|
65
|
+
}) => {
|
|
66
|
+
const sendSignal = params.sendSignal ?? process.kill
|
|
67
|
+
const waitForExit = params.waitForExit ?? waitForProcessExit
|
|
68
|
+
const now = params.now ?? Date.now
|
|
69
|
+
|
|
70
|
+
const record = await resolveCliSession(params.cwd, params.sessionId)
|
|
71
|
+
const detail = record.detail
|
|
72
|
+
if (detail == null) {
|
|
73
|
+
throw new Error(`Session ${params.sessionId} has no task metadata.`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (detail.status !== 'running') {
|
|
77
|
+
return {
|
|
78
|
+
message: `Session ${detail.sessionId} is not running (status: ${detail.status}).`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (detail.pid == null) {
|
|
83
|
+
throw new Error(`Session ${detail.sessionId} has no PID recorded.`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
sendSignal(detail.pid, params.signal)
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
if (error.code === 'ESRCH') {
|
|
90
|
+
await markCliSessionStopped({
|
|
91
|
+
cwd: params.cwd,
|
|
92
|
+
record,
|
|
93
|
+
endTime: now()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
message: `Process ${detail.pid} not found. Marked session ${detail.sessionId} as stopped.`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Failed to signal process ${detail.pid}: ${error.message}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const requestedAt = now()
|
|
104
|
+
const timeoutMs = STOP_SIGNAL_TIMEOUT_MS[params.signal]
|
|
105
|
+
await writeCliSessionControl(params.cwd, detail.ctxId, detail.sessionId, {
|
|
106
|
+
signal: params.signal,
|
|
107
|
+
requestedAt,
|
|
108
|
+
expiresAt: requestedAt + timeoutMs
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const didExit = await waitForExit({
|
|
112
|
+
pid: detail.pid,
|
|
113
|
+
timeoutMs,
|
|
114
|
+
sendSignal
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (didExit) {
|
|
118
|
+
await markCliSessionStopped({
|
|
119
|
+
cwd: params.cwd,
|
|
120
|
+
record,
|
|
121
|
+
endTime: now()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
message: `Sent ${params.signal} to process ${detail.pid} for session ${detail.sessionId}.`
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
message:
|
|
131
|
+
`Sent ${params.signal} to process ${detail.pid} for session ${detail.sessionId}. Waiting for the session to exit.`
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Option } from 'commander'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
|
|
4
|
+
import { updateConfigFile } from '@oneworks/config'
|
|
5
|
+
import { normalizeProjectSkillInstall, resolveConfiguredSkillInstalls, resolveSkillsRegistry } from '@oneworks/utils'
|
|
6
|
+
|
|
7
|
+
import { resolveCliWorkspaceCwd } from '#~/workspace.js'
|
|
8
|
+
|
|
9
|
+
import { buildDeclaredSkillEntry, installDeclaredSkill } from './install'
|
|
10
|
+
import {
|
|
11
|
+
buildGeneralSkillsUpdateValue,
|
|
12
|
+
exitWithError,
|
|
13
|
+
getRawSourceConfig,
|
|
14
|
+
getResolvedSourceConfig,
|
|
15
|
+
isSameDeclaredSkill,
|
|
16
|
+
loadSkillsConfigState,
|
|
17
|
+
matchesSkillSelector,
|
|
18
|
+
printResult
|
|
19
|
+
} from './shared'
|
|
20
|
+
import { CONFIG_WRITE_SOURCES } from './types'
|
|
21
|
+
import type { SkillsAddOptions } from './types'
|
|
22
|
+
|
|
23
|
+
export const registerAddSkillSubcommand = (skillsCommand: Command) => {
|
|
24
|
+
skillsCommand
|
|
25
|
+
.command('add <skill>')
|
|
26
|
+
.description('Declare a project skill in config and ensure it is installed locally')
|
|
27
|
+
.addOption(
|
|
28
|
+
new Option('--config-source <source>', 'Config source to update').choices([...CONFIG_WRITE_SOURCES]).default(
|
|
29
|
+
'project'
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
.option('--source <source>', 'Remote skills CLI source path')
|
|
33
|
+
.option('--version <version>', 'Remote skill version passed to the skills CLI')
|
|
34
|
+
.option('--rename <name>', 'Local skill name after install')
|
|
35
|
+
.option('--registry <registry>', 'Package registry used to install the managed skills CLI')
|
|
36
|
+
.option('--force', 'Replace the existing installed skill if it already exists', false)
|
|
37
|
+
.option('--json', 'Print JSON output', false)
|
|
38
|
+
.action(async (skill: string, opts: SkillsAddOptions) => {
|
|
39
|
+
try {
|
|
40
|
+
const workspaceFolder = resolveCliWorkspaceCwd()
|
|
41
|
+
const declared = buildDeclaredSkillEntry(skill, opts)
|
|
42
|
+
const normalized = normalizeProjectSkillInstall(declared)
|
|
43
|
+
if (normalized == null) {
|
|
44
|
+
throw new Error('Skill reference is required.')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const state = await loadSkillsConfigState(workspaceFolder)
|
|
48
|
+
const source = opts.configSource ?? 'project'
|
|
49
|
+
const sourceConfig = getRawSourceConfig(state, source)
|
|
50
|
+
const resolvedSourceConfig = getResolvedSourceConfig(state, source)
|
|
51
|
+
const configured = resolveConfiguredSkillInstalls(sourceConfig?.skills)
|
|
52
|
+
const resolvedConfigured = resolveConfiguredSkillInstalls(resolvedSourceConfig?.skills)
|
|
53
|
+
const defaultRegistry = opts.registry ?? resolveSkillsRegistry(resolvedSourceConfig?.skills) ??
|
|
54
|
+
resolveSkillsRegistry(state.mergedConfig.skills)
|
|
55
|
+
|
|
56
|
+
const duplicate = resolvedConfigured.find(item => matchesSkillSelector(normalized.targetName, item))
|
|
57
|
+
if (duplicate != null && !isSameDeclaredSkill(duplicate, declared)) {
|
|
58
|
+
throw new Error(`Configured skill target "${normalized.targetName}" already exists in ${source} config.`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const installResult = await installDeclaredSkill({
|
|
62
|
+
force: opts.force,
|
|
63
|
+
registry: defaultRegistry,
|
|
64
|
+
skill: declared,
|
|
65
|
+
workspaceFolder
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const nextSkills = duplicate == null ? [...configured, declared] : configured
|
|
69
|
+
const updated = await updateConfigFile({
|
|
70
|
+
workspaceFolder,
|
|
71
|
+
source,
|
|
72
|
+
section: 'general',
|
|
73
|
+
value: buildGeneralSkillsUpdateValue(sourceConfig, nextSkills)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
printResult({
|
|
77
|
+
action: 'add',
|
|
78
|
+
configPath: updated.configPath,
|
|
79
|
+
declared,
|
|
80
|
+
installDir: installResult.installDir,
|
|
81
|
+
name: installResult.name,
|
|
82
|
+
workspaceFolder
|
|
83
|
+
}, opts.json)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
exitWithError(error, opts.json)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
|
|
3
|
+
import { resolveSkillsRegistry } from '@oneworks/utils'
|
|
4
|
+
|
|
5
|
+
import { resolveCliWorkspaceCwd } from '#~/workspace.js'
|
|
6
|
+
|
|
7
|
+
import { resolveInstallTargets } from './install'
|
|
8
|
+
import { createSkillsProgress } from './progress'
|
|
9
|
+
import { exitWithError, loadSkillsConfigState, printResult } from './shared'
|
|
10
|
+
import { syncProjectSkills } from './sync'
|
|
11
|
+
import type { SkillsInstallOptions } from './types'
|
|
12
|
+
|
|
13
|
+
export const registerInstallSkillSubcommands = (skillsCommand: Command) => {
|
|
14
|
+
skillsCommand
|
|
15
|
+
.command('install [skills...]')
|
|
16
|
+
.description('Install explicit project skills or all configured skills when no arguments are provided')
|
|
17
|
+
.option('--source <source>', 'Remote skills CLI source path for a single explicit skill')
|
|
18
|
+
.option('--version <version>', 'Remote skill version passed to the skills CLI for a single explicit skill')
|
|
19
|
+
.option('--rename <name>', 'Local skill name after install for a single explicit skill')
|
|
20
|
+
.option('--registry <registry>', 'Package registry used to install the managed skills CLI')
|
|
21
|
+
.option('--force', 'Replace existing installed skills', false)
|
|
22
|
+
.option('--json', 'Print JSON output', false)
|
|
23
|
+
.action(async (skills: string[], opts: SkillsInstallOptions) => {
|
|
24
|
+
const progress = createSkillsProgress({ enabled: !opts.json })
|
|
25
|
+
try {
|
|
26
|
+
const workspaceFolder = resolveCliWorkspaceCwd()
|
|
27
|
+
const state = await loadSkillsConfigState(workspaceFolder)
|
|
28
|
+
const defaultRegistry = opts.registry ?? resolveSkillsRegistry(state.mergedConfig.skills)
|
|
29
|
+
const targets = await resolveInstallTargets({
|
|
30
|
+
args: skills,
|
|
31
|
+
options: opts,
|
|
32
|
+
workspaceFolder
|
|
33
|
+
})
|
|
34
|
+
const result = await syncProjectSkills({
|
|
35
|
+
force: opts.force,
|
|
36
|
+
registry: defaultRegistry,
|
|
37
|
+
progress,
|
|
38
|
+
state,
|
|
39
|
+
targets,
|
|
40
|
+
workspaceFolder
|
|
41
|
+
})
|
|
42
|
+
progress.finish(`Installed ${result.installed.length} skills`)
|
|
43
|
+
|
|
44
|
+
printResult({
|
|
45
|
+
action: 'install',
|
|
46
|
+
installed: result.installed.map(item => ({
|
|
47
|
+
dirName: item.dirName,
|
|
48
|
+
installDir: item.installDir,
|
|
49
|
+
name: item.name,
|
|
50
|
+
ref: item.ref,
|
|
51
|
+
skipped: item.skipped
|
|
52
|
+
})),
|
|
53
|
+
workspaceFolder
|
|
54
|
+
}, opts.json)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
progress.fail()
|
|
57
|
+
exitWithError(error, opts.json)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
skillsCommand
|
|
62
|
+
.command('update [skills...]')
|
|
63
|
+
.description('Force refresh explicit project skills or all configured skills when no arguments are provided')
|
|
64
|
+
.option('--source <source>', 'Remote skills CLI source path for a single explicit skill')
|
|
65
|
+
.option('--version <version>', 'Remote skill version passed to the skills CLI for a single explicit skill')
|
|
66
|
+
.option('--rename <name>', 'Local skill name after install for a single explicit skill')
|
|
67
|
+
.option('--registry <registry>', 'Package registry used to install the managed skills CLI')
|
|
68
|
+
.option('--json', 'Print JSON output', false)
|
|
69
|
+
.action(async (skills: string[], opts: Omit<SkillsInstallOptions, 'force'>) => {
|
|
70
|
+
const progress = createSkillsProgress({ enabled: !opts.json })
|
|
71
|
+
try {
|
|
72
|
+
const workspaceFolder = resolveCliWorkspaceCwd()
|
|
73
|
+
const state = await loadSkillsConfigState(workspaceFolder)
|
|
74
|
+
const defaultRegistry = opts.registry ?? resolveSkillsRegistry(state.mergedConfig.skills)
|
|
75
|
+
const targets = await resolveInstallTargets({
|
|
76
|
+
args: skills,
|
|
77
|
+
options: opts,
|
|
78
|
+
workspaceFolder
|
|
79
|
+
})
|
|
80
|
+
const result = await syncProjectSkills({
|
|
81
|
+
force: true,
|
|
82
|
+
registry: defaultRegistry,
|
|
83
|
+
progress,
|
|
84
|
+
state,
|
|
85
|
+
targets,
|
|
86
|
+
workspaceFolder
|
|
87
|
+
})
|
|
88
|
+
progress.finish(`Updated ${result.installed.length} skills`)
|
|
89
|
+
|
|
90
|
+
printResult({
|
|
91
|
+
action: 'update',
|
|
92
|
+
installed: result.installed.map(item => ({
|
|
93
|
+
dirName: item.dirName,
|
|
94
|
+
installDir: item.installDir,
|
|
95
|
+
name: item.name,
|
|
96
|
+
ref: item.ref
|
|
97
|
+
})),
|
|
98
|
+
workspaceFolder
|
|
99
|
+
}, opts.json)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
progress.fail()
|
|
102
|
+
exitWithError(error, opts.json)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- install target resolution stays colocated with command orchestration for now. */
|
|
2
|
+
import { basename, extname } from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import type { ConfigSourceState } from '@oneworks/config'
|
|
6
|
+
import type { ConfiguredSkillInstallConfig } from '@oneworks/types'
|
|
7
|
+
import {
|
|
8
|
+
installProjectSkill,
|
|
9
|
+
normalizeProjectSkillInstall,
|
|
10
|
+
resolveConfiguredSkillInstalls,
|
|
11
|
+
resolveProjectOoPath,
|
|
12
|
+
toSkillSlug
|
|
13
|
+
} from '@oneworks/utils'
|
|
14
|
+
|
|
15
|
+
import { loadSkillsConfigState, pathExists } from './shared'
|
|
16
|
+
import type { SkillsInstallOptions } from './types'
|
|
17
|
+
|
|
18
|
+
export type DeclaredSkillInstallTarget = string | ConfiguredSkillInstallConfig
|
|
19
|
+
|
|
20
|
+
export interface ResolvedSkillInstallTarget {
|
|
21
|
+
declaration: DeclaredSkillInstallTarget
|
|
22
|
+
installPathSegments?: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const buildDeclaredSkillEntry = (
|
|
26
|
+
skillArg: string,
|
|
27
|
+
options: Pick<SkillsInstallOptions, 'registry' | 'rename' | 'source' | 'version'>
|
|
28
|
+
): DeclaredSkillInstallTarget => {
|
|
29
|
+
const skill = typeof skillArg === 'string' && skillArg.trim() !== '' ? skillArg.trim() : undefined
|
|
30
|
+
if (skill == null) {
|
|
31
|
+
throw new Error('Skill reference is required.')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const explicitRegistry = typeof options.registry === 'string' && options.registry.trim() !== ''
|
|
35
|
+
? options.registry.trim()
|
|
36
|
+
: undefined
|
|
37
|
+
const explicitSource = typeof options.source === 'string' && options.source.trim() !== ''
|
|
38
|
+
? options.source.trim()
|
|
39
|
+
: undefined
|
|
40
|
+
const explicitVersion = typeof options.version === 'string' && options.version.trim() !== ''
|
|
41
|
+
? options.version.trim()
|
|
42
|
+
: undefined
|
|
43
|
+
const rename = typeof options.rename === 'string' && options.rename.trim() !== ''
|
|
44
|
+
? options.rename.trim()
|
|
45
|
+
: undefined
|
|
46
|
+
const parsed = normalizeProjectSkillInstall(skill)
|
|
47
|
+
if (parsed == null) {
|
|
48
|
+
throw new Error(`Invalid skill reference "${skillArg}".`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (explicitSource != null && parsed.source != null) {
|
|
52
|
+
throw new Error('--source cannot be used when the skill reference already includes a source.')
|
|
53
|
+
}
|
|
54
|
+
if (explicitRegistry != null && parsed.registry != null) {
|
|
55
|
+
throw new Error('--registry cannot be used when the skill reference already includes a registry.')
|
|
56
|
+
}
|
|
57
|
+
if (explicitVersion != null && parsed.version != null) {
|
|
58
|
+
throw new Error('--version cannot be used when the skill reference already includes a version.')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (explicitRegistry == null && explicitSource == null && explicitVersion == null && rename == null) {
|
|
62
|
+
return skill
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
name: parsed.name,
|
|
67
|
+
...(explicitRegistry != null
|
|
68
|
+
? { registry: explicitRegistry }
|
|
69
|
+
: (parsed.registry != null ? { registry: parsed.registry } : {})),
|
|
70
|
+
...(explicitSource != null
|
|
71
|
+
? { source: explicitSource }
|
|
72
|
+
: (parsed.source != null ? { source: parsed.source } : {})),
|
|
73
|
+
...(explicitVersion != null
|
|
74
|
+
? { version: explicitVersion }
|
|
75
|
+
: (parsed.version != null ? { version: parsed.version } : {})),
|
|
76
|
+
...(rename != null ? { rename } : {})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const basenameWithoutExtension = (filePath: string) => basename(filePath, extname(filePath))
|
|
81
|
+
|
|
82
|
+
const isPathLikeExtend = (value: string) => (
|
|
83
|
+
value.startsWith('.') ||
|
|
84
|
+
value.startsWith('/') ||
|
|
85
|
+
/^[a-z]:[\\/]/i.test(value)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const resolveExtendSegmentBaseName = (source: ConfigSourceState, index: number) => {
|
|
89
|
+
const requestedExtendPath = source.extendPath?.trim()
|
|
90
|
+
if (requestedExtendPath != null && requestedExtendPath !== '' && !isPathLikeExtend(requestedExtendPath)) {
|
|
91
|
+
return requestedExtendPath
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return source.configPath == null ? `extend-${index + 1}` : basenameWithoutExtension(source.configPath)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const stableExtendSegment = (source: ConfigSourceState, index: number, counts: Map<string, number>) => {
|
|
98
|
+
const baseName = resolveExtendSegmentBaseName(source, index)
|
|
99
|
+
const slug = toSkillSlug(baseName) || `extend-${index + 1}`
|
|
100
|
+
if ((counts.get(slug) ?? 0) <= 1) return slug
|
|
101
|
+
return `${slug}-${index + 1}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const resolveExtendSegments = (sources: ConfigSourceState[]) => {
|
|
105
|
+
const counts = new Map<string, number>()
|
|
106
|
+
for (let index = 0; index < sources.length; index++) {
|
|
107
|
+
const baseName = resolveExtendSegmentBaseName(sources[index]!, index)
|
|
108
|
+
const slug = toSkillSlug(baseName) || `extend-${index + 1}`
|
|
109
|
+
counts.set(slug, (counts.get(slug) ?? 0) + 1)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return sources.map((source, index) => stableExtendSegment(source, index, counts))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resolveConfigSourceInstallTargets = (
|
|
116
|
+
source: ConfigSourceState | undefined
|
|
117
|
+
): ResolvedSkillInstallTarget[] => {
|
|
118
|
+
if (source == null) return []
|
|
119
|
+
|
|
120
|
+
const extendSources = source.resolvedExtendSources ?? []
|
|
121
|
+
const extendSegments = resolveExtendSegments(extendSources)
|
|
122
|
+
const targets: ResolvedSkillInstallTarget[] = []
|
|
123
|
+
|
|
124
|
+
for (let index = 0; index < extendSources.length; index++) {
|
|
125
|
+
targets.push(
|
|
126
|
+
...resolveConfiguredSkillInstalls(extendSources[index]?.rawConfig?.skills).map(declaration => ({
|
|
127
|
+
declaration,
|
|
128
|
+
installPathSegments: ['.extends', extendSegments[index]!]
|
|
129
|
+
}))
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
targets.push(
|
|
134
|
+
...resolveConfiguredSkillInstalls(source.rawConfig?.skills).map(declaration => ({
|
|
135
|
+
declaration,
|
|
136
|
+
installPathSegments: []
|
|
137
|
+
}))
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return targets
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const resolveConfiguredSkillInstallTargets = (
|
|
144
|
+
state: Awaited<ReturnType<typeof loadSkillsConfigState>>
|
|
145
|
+
): ResolvedSkillInstallTarget[] => {
|
|
146
|
+
const targets = [
|
|
147
|
+
...(state.globalConfig == null ? [] : resolveConfigSourceInstallTargets(state.globalSource)),
|
|
148
|
+
...resolveConfigSourceInstallTargets(state.projectSource),
|
|
149
|
+
...resolveConfigSourceInstallTargets(state.userSource)
|
|
150
|
+
]
|
|
151
|
+
if (targets.length > 0) return targets
|
|
152
|
+
|
|
153
|
+
return resolveConfiguredSkillInstalls(state.mergedConfig.skills).map(declaration => ({
|
|
154
|
+
declaration,
|
|
155
|
+
installPathSegments: []
|
|
156
|
+
}))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const installDeclaredSkill = async (params: {
|
|
160
|
+
force?: boolean
|
|
161
|
+
registry?: string
|
|
162
|
+
skill: DeclaredSkillInstallTarget
|
|
163
|
+
workspaceFolder: string
|
|
164
|
+
}) => {
|
|
165
|
+
const normalized = normalizeProjectSkillInstall(params.skill)
|
|
166
|
+
if (normalized == null) {
|
|
167
|
+
throw new Error('Skill reference is required.')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const existingSkillPath = resolveProjectOoPath(
|
|
171
|
+
params.workspaceFolder,
|
|
172
|
+
process.env,
|
|
173
|
+
'skills',
|
|
174
|
+
normalized.targetDirName,
|
|
175
|
+
'SKILL.md'
|
|
176
|
+
)
|
|
177
|
+
const hadExisting = await pathExists(existingSkillPath)
|
|
178
|
+
const installed = params.force === true || !hadExisting
|
|
179
|
+
? await installProjectSkill({
|
|
180
|
+
force: params.force,
|
|
181
|
+
registry: params.registry,
|
|
182
|
+
skill: normalized,
|
|
183
|
+
workspaceFolder: params.workspaceFolder
|
|
184
|
+
})
|
|
185
|
+
: {
|
|
186
|
+
dirName: normalized.targetDirName,
|
|
187
|
+
installDir: resolveProjectOoPath(params.workspaceFolder, process.env, 'skills', normalized.targetDirName),
|
|
188
|
+
name: normalized.targetName,
|
|
189
|
+
ref: normalized.ref,
|
|
190
|
+
skillPath: existingSkillPath
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...installed,
|
|
195
|
+
skipped: params.force !== true && hadExisting
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const resolveInstallTargets = async (params: {
|
|
200
|
+
args: string[]
|
|
201
|
+
options: Pick<SkillsInstallOptions, 'rename' | 'source'>
|
|
202
|
+
workspaceFolder: string
|
|
203
|
+
}) => {
|
|
204
|
+
if (params.args.length > 0) {
|
|
205
|
+
if (params.args.length > 1 && (params.options.rename != null || params.options.source != null)) {
|
|
206
|
+
throw new Error('--source and --rename only support a single explicit skill argument.')
|
|
207
|
+
}
|
|
208
|
+
return params.args.map((arg) => ({
|
|
209
|
+
declaration: buildDeclaredSkillEntry(arg, params.options),
|
|
210
|
+
installPathSegments: []
|
|
211
|
+
}))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const state = await loadSkillsConfigState(params.workspaceFolder)
|
|
215
|
+
return resolveConfiguredSkillInstallTargets(state)
|
|
216
|
+
}
|