@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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/channel.js +7 -0
  3. package/cli.js +5 -0
  4. package/mem.js +7 -0
  5. package/package.json +59 -0
  6. package/postinstall.js +75 -0
  7. package/src/AGENTS.md +169 -0
  8. package/src/channel-cli.ts +19 -0
  9. package/src/cli-argv.ts +27 -0
  10. package/src/cli.ts +63 -0
  11. package/src/commands/@core/adapter-option.ts +85 -0
  12. package/src/commands/@core/extra-options.ts +12 -0
  13. package/src/commands/@core/plugin-install.ts +1 -0
  14. package/src/commands/@core/plugin-source.ts +1 -0
  15. package/src/commands/accounts.ts +204 -0
  16. package/src/commands/adapter/prepare-selection.ts +181 -0
  17. package/src/commands/adapter/prepare.ts +104 -0
  18. package/src/commands/adapter.ts +48 -0
  19. package/src/commands/agent/actions.ts +176 -0
  20. package/src/commands/agent/runtime-store-commands.ts +56 -0
  21. package/src/commands/agent/runtime-store-events.ts +23 -0
  22. package/src/commands/agent/runtime-store-session.ts +170 -0
  23. package/src/commands/agent/runtime-store-shared.ts +139 -0
  24. package/src/commands/agent/runtime-store.ts +4 -0
  25. package/src/commands/agent.ts +81 -0
  26. package/src/commands/benchmark.ts +198 -0
  27. package/src/commands/channel.ts +594 -0
  28. package/src/commands/clear.ts +140 -0
  29. package/src/commands/config/actions.ts +196 -0
  30. package/src/commands/config/display-state.ts +108 -0
  31. package/src/commands/config/index.ts +135 -0
  32. package/src/commands/config/interactive.ts +121 -0
  33. package/src/commands/config/read-state.ts +56 -0
  34. package/src/commands/config/section-state.ts +109 -0
  35. package/src/commands/config/shared.ts +195 -0
  36. package/src/commands/kill.ts +41 -0
  37. package/src/commands/list.ts +224 -0
  38. package/src/commands/memory/context.ts +76 -0
  39. package/src/commands/memory/entries.ts +131 -0
  40. package/src/commands/memory/shared.ts +89 -0
  41. package/src/commands/memory/store.ts +69 -0
  42. package/src/commands/memory/target.ts +54 -0
  43. package/src/commands/memory.ts +97 -0
  44. package/src/commands/plugin.ts +62 -0
  45. package/src/commands/report-targets.ts +149 -0
  46. package/src/commands/report.ts +232 -0
  47. package/src/commands/run/adapter-cli-version.ts +65 -0
  48. package/src/commands/run/command.ts +982 -0
  49. package/src/commands/run/input-bridge.ts +108 -0
  50. package/src/commands/run/input-control.ts +112 -0
  51. package/src/commands/run/input-decision.ts +88 -0
  52. package/src/commands/run/options.ts +104 -0
  53. package/src/commands/run/output.ts +179 -0
  54. package/src/commands/run/permission-decision.ts +19 -0
  55. package/src/commands/run/permission-recovery.ts +194 -0
  56. package/src/commands/run/permission-state.ts +177 -0
  57. package/src/commands/run/print-idle-timeout.ts +47 -0
  58. package/src/commands/run/protocol-envelope.ts +111 -0
  59. package/src/commands/run/protocol-stdio.ts +71 -0
  60. package/src/commands/run/protocol.ts +391 -0
  61. package/src/commands/run/runtime-command-bridge.ts +190 -0
  62. package/src/commands/run/runtime-event-sink.ts +560 -0
  63. package/src/commands/run/session-exit-controller.ts +45 -0
  64. package/src/commands/run/types.ts +65 -0
  65. package/src/commands/run.ts +62 -0
  66. package/src/commands/session-control.ts +133 -0
  67. package/src/commands/skills/add-command.ts +88 -0
  68. package/src/commands/skills/install-command.ts +105 -0
  69. package/src/commands/skills/install.ts +216 -0
  70. package/src/commands/skills/progress.ts +126 -0
  71. package/src/commands/skills/publish-command.ts +85 -0
  72. package/src/commands/skills/register.ts +17 -0
  73. package/src/commands/skills/remove-command.ts +102 -0
  74. package/src/commands/skills/shared.ts +117 -0
  75. package/src/commands/skills/sync.ts +571 -0
  76. package/src/commands/skills/types.ts +33 -0
  77. package/src/commands/skills.ts +1 -0
  78. package/src/commands/stop.ts +41 -0
  79. package/src/config.ts +1 -0
  80. package/src/default-skill-plugin.ts +29 -0
  81. package/src/env.ts +1 -0
  82. package/src/hooks/plugins/index.ts +66 -0
  83. package/src/mem-cli.ts +19 -0
  84. package/src/session-cache.ts +250 -0
  85. package/src/session-permission-cache.ts +40 -0
  86. package/src/utils.ts +25 -0
  87. 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
+ }