@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,140 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
mergeProcessEnvWithProjectEnv,
|
|
7
|
+
resolveLegacyProjectHomeSegmentPaths,
|
|
8
|
+
resolveProjectHomePath,
|
|
9
|
+
resolveProjectOoBaseDirName,
|
|
10
|
+
resolveProjectOoPath
|
|
11
|
+
} from '@oneworks/utils'
|
|
12
|
+
import type { Command } from 'commander'
|
|
13
|
+
import fg from 'fast-glob'
|
|
14
|
+
|
|
15
|
+
const CLEAR_PROJECT_HOME_TARGETS = [
|
|
16
|
+
'logs',
|
|
17
|
+
'caches'
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
const CLEAR_MOCK_TARGETS = [
|
|
21
|
+
'.mock/.claude/debug',
|
|
22
|
+
'.mock/.claude/todos',
|
|
23
|
+
'.mock/.claude/session-env',
|
|
24
|
+
'.mock/.claude/projects',
|
|
25
|
+
'.mock/.claude-core-router/logs',
|
|
26
|
+
'.mock/.claude-code-router/logs'
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
const BENCHMARK_LOG_PATTERNS = [
|
|
30
|
+
'benchmarks/specs/**/logs',
|
|
31
|
+
'benchmarks/entities/**/logs',
|
|
32
|
+
'benchmarks/cases/**/logs'
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
const CLAUDE_CODE_ROUTER_LOG_FILE_PATTERNS = [
|
|
36
|
+
'.claude-code-router/*.log',
|
|
37
|
+
'.claude-code-router/*.log.*'
|
|
38
|
+
] as const
|
|
39
|
+
|
|
40
|
+
const CLAUDE_CODE_ROUTER_SESSION_LOG_PATTERN = '.claude-code-router/*/logs'
|
|
41
|
+
|
|
42
|
+
export interface RunClearCommandOptions {
|
|
43
|
+
cwd?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const resolveSegmentTargetDirs = (
|
|
47
|
+
cwd: string,
|
|
48
|
+
env: NodeJS.ProcessEnv,
|
|
49
|
+
segment: 'logs' | 'caches' | '.mock'
|
|
50
|
+
) => {
|
|
51
|
+
const paths = resolveLegacyProjectHomeSegmentPaths(cwd, env, segment)
|
|
52
|
+
return [
|
|
53
|
+
paths.targetDir,
|
|
54
|
+
...paths.sourceDirs
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolveMockTargetDirs = (cwd: string, env: NodeJS.ProcessEnv, target: string) => {
|
|
59
|
+
const mockRelativePath = target.replace(/^\.mock\//u, '')
|
|
60
|
+
return resolveSegmentTargetDirs(cwd, env, '.mock')
|
|
61
|
+
.map(mockRoot => path.resolve(mockRoot, ...mockRelativePath.split('/')))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function collectClearTargets(cwd: string, env: NodeJS.ProcessEnv) {
|
|
65
|
+
const aiBaseDir = resolveProjectOoPath(cwd, env)
|
|
66
|
+
const logsDir = resolveProjectOoPath(cwd, env, 'logs')
|
|
67
|
+
const mockHomeDirs = resolveSegmentTargetDirs(cwd, env, '.mock')
|
|
68
|
+
const benchmarkLogDirs = await fg([...BENCHMARK_LOG_PATTERNS], {
|
|
69
|
+
cwd: aiBaseDir,
|
|
70
|
+
onlyDirectories: true,
|
|
71
|
+
deep: 10,
|
|
72
|
+
absolute: true
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const claudeCodeRouterSessionLogDirs = (
|
|
76
|
+
await Promise.all(mockHomeDirs.map(mockHomeDir =>
|
|
77
|
+
fg(CLAUDE_CODE_ROUTER_SESSION_LOG_PATTERN, {
|
|
78
|
+
cwd: mockHomeDir,
|
|
79
|
+
onlyDirectories: true,
|
|
80
|
+
deep: 2,
|
|
81
|
+
absolute: true
|
|
82
|
+
})
|
|
83
|
+
))
|
|
84
|
+
).flat()
|
|
85
|
+
|
|
86
|
+
const claudeCodeRouterLogFiles = (
|
|
87
|
+
await Promise.all(mockHomeDirs.map(mockHomeDir =>
|
|
88
|
+
fg([...CLAUDE_CODE_ROUTER_LOG_FILE_PATTERNS], {
|
|
89
|
+
cwd: mockHomeDir,
|
|
90
|
+
onlyFiles: true,
|
|
91
|
+
deep: 1,
|
|
92
|
+
absolute: true
|
|
93
|
+
})
|
|
94
|
+
))
|
|
95
|
+
).flat()
|
|
96
|
+
|
|
97
|
+
return [
|
|
98
|
+
path.resolve(cwd, '.logs'),
|
|
99
|
+
...CLEAR_PROJECT_HOME_TARGETS.flatMap(target => resolveSegmentTargetDirs(cwd, env, target)),
|
|
100
|
+
...CLEAR_MOCK_TARGETS.flatMap(target => resolveMockTargetDirs(cwd, env, target)),
|
|
101
|
+
...benchmarkLogDirs.filter(dir => dir !== logsDir && !dir.startsWith(`${logsDir}${path.sep}`)),
|
|
102
|
+
...claudeCodeRouterSessionLogDirs.map(dir => path.dirname(dir)),
|
|
103
|
+
...claudeCodeRouterLogFiles
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function runClearCommand(options: RunClearCommandOptions = {}) {
|
|
108
|
+
const cwd = options.cwd ?? process.cwd()
|
|
109
|
+
const env = mergeProcessEnvWithProjectEnv(undefined, { workspaceFolder: cwd })
|
|
110
|
+
const targets = Array.from(new Set(await collectClearTargets(cwd, env)))
|
|
111
|
+
|
|
112
|
+
await Promise.all(
|
|
113
|
+
targets.map(target => fs.rm(target, { force: true, recursive: true }))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
await Promise.all([
|
|
117
|
+
fs.mkdir(resolveProjectOoPath(cwd, env, 'logs'), { recursive: true }),
|
|
118
|
+
fs.mkdir(resolveProjectOoPath(cwd, env, 'caches'), { recursive: true })
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
await Promise.all([
|
|
122
|
+
fs.writeFile(resolveProjectOoPath(cwd, env, 'logs', '.gitkeep'), ''),
|
|
123
|
+
fs.writeFile(resolveProjectOoPath(cwd, env, 'caches', '.gitkeep'), '')
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
console.log(
|
|
127
|
+
`Clear logs and cache successfully (${resolveProjectOoBaseDirName(env)} assets, ${
|
|
128
|
+
resolveProjectHomePath(cwd, env)
|
|
129
|
+
} runtime data)`
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function registerClearCommand(program: Command) {
|
|
134
|
+
program
|
|
135
|
+
.command('clear')
|
|
136
|
+
.description('Clear logs and cache of sub-agents')
|
|
137
|
+
.action(async () => {
|
|
138
|
+
await runClearCommand()
|
|
139
|
+
})
|
|
140
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getConfigSectionValueAtPath,
|
|
5
|
+
hasConfigSectionValue,
|
|
6
|
+
parseConfigSectionPath,
|
|
7
|
+
resolveConfigSectionPath,
|
|
8
|
+
setConfigSectionValueAtPath,
|
|
9
|
+
unsetConfigSectionValueAtPath,
|
|
10
|
+
updateConfigFile,
|
|
11
|
+
validateConfigSection
|
|
12
|
+
} from '@oneworks/config'
|
|
13
|
+
|
|
14
|
+
import { resolveReadableConfigValue } from './display-state'
|
|
15
|
+
import { resolveSetPathInput, resolveSetValueInput, resolveWritableSource } from './interactive'
|
|
16
|
+
import { resolveReadState } from './read-state'
|
|
17
|
+
import { resolveClearedSectionValue, resolveListOutput, resolveTextListRows } from './section-state'
|
|
18
|
+
import {
|
|
19
|
+
assertWritableConfigSection,
|
|
20
|
+
formatDisplayValue,
|
|
21
|
+
formatValidationIssues,
|
|
22
|
+
loadCommandState,
|
|
23
|
+
parseConfigValueInput,
|
|
24
|
+
printJsonResult,
|
|
25
|
+
resolveSourceSections
|
|
26
|
+
} from './shared'
|
|
27
|
+
import type { ConfigGetOptions, ConfigListOptions, ConfigSetOptions, ConfigUnsetOptions } from './shared'
|
|
28
|
+
|
|
29
|
+
export const runListCommand = async (pathInput: string | undefined, opts: ConfigListOptions) => {
|
|
30
|
+
const source = opts.source ?? 'merged'
|
|
31
|
+
if (pathInput != null && pathInput.trim() !== '') {
|
|
32
|
+
const resolved = await resolveReadState(pathInput, source === 'all' ? 'merged' : source)
|
|
33
|
+
if (opts.json) {
|
|
34
|
+
printJsonResult({
|
|
35
|
+
ok: true,
|
|
36
|
+
workspaceFolder: resolved.cwd,
|
|
37
|
+
source: resolved.source,
|
|
38
|
+
path: resolved.resolvedPath?.normalizedPath ?? null,
|
|
39
|
+
section: resolved.resolvedPath?.section ?? null,
|
|
40
|
+
value: resolved.value
|
|
41
|
+
})
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(formatDisplayValue(resolveReadableConfigValue(resolved)))
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const cwd = process.cwd()
|
|
49
|
+
const state = await loadCommandState(cwd)
|
|
50
|
+
if (opts.json) {
|
|
51
|
+
const output = {
|
|
52
|
+
ok: true,
|
|
53
|
+
workspaceFolder: cwd,
|
|
54
|
+
sections: resolveListOutput(state, source)
|
|
55
|
+
} as const
|
|
56
|
+
printJsonResult(
|
|
57
|
+
source === 'all'
|
|
58
|
+
? {
|
|
59
|
+
...output,
|
|
60
|
+
present: state.present
|
|
61
|
+
}
|
|
62
|
+
: output
|
|
63
|
+
)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
console.log(`Workspace: ${cwd}`)
|
|
67
|
+
console.log(
|
|
68
|
+
`Config present: global=${state.present.global ? 'yes' : 'no'}, project=${
|
|
69
|
+
state.present.project ? 'yes' : 'no'
|
|
70
|
+
}, local=${state.present.user ? 'yes' : 'no'}`
|
|
71
|
+
)
|
|
72
|
+
console.table(resolveTextListRows(state, source))
|
|
73
|
+
console.log('Examples:')
|
|
74
|
+
console.log(' oneworks config get general.defaultModel')
|
|
75
|
+
console.log(' oneworks config set general.defaultModel gpt-5.4 --type string')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const runGetCommand = async (pathInput: string | undefined, opts: ConfigGetOptions) => {
|
|
79
|
+
const resolved = await resolveReadState(pathInput, opts.source)
|
|
80
|
+
if (opts.json) {
|
|
81
|
+
printJsonResult({
|
|
82
|
+
ok: true,
|
|
83
|
+
workspaceFolder: resolved.cwd,
|
|
84
|
+
source: resolved.source,
|
|
85
|
+
path: resolved.resolvedPath?.normalizedPath ?? null,
|
|
86
|
+
section: resolved.resolvedPath?.section ?? null,
|
|
87
|
+
value: resolved.value
|
|
88
|
+
})
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
process.stdout.write(formatDisplayValue(resolveReadableConfigValue(resolved)))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const runSetCommand = async (
|
|
95
|
+
pathInput: string | undefined,
|
|
96
|
+
valueInput: string | undefined,
|
|
97
|
+
opts: ConfigSetOptions
|
|
98
|
+
) => {
|
|
99
|
+
const cwd = process.cwd()
|
|
100
|
+
const source = await resolveWritableSource(opts.source, opts.json)
|
|
101
|
+
const path = await resolveSetPathInput(pathInput, opts.json)
|
|
102
|
+
const type = opts.type ?? 'auto'
|
|
103
|
+
const rawValue = await resolveSetValueInput(valueInput, type, opts.json)
|
|
104
|
+
const nextValue = parseConfigValueInput(rawValue, type)
|
|
105
|
+
const resolvedPath = resolveConfigSectionPath(parseConfigSectionPath(path))
|
|
106
|
+
assertWritableConfigSection(source, resolvedPath.section)
|
|
107
|
+
const state = await loadCommandState(cwd)
|
|
108
|
+
const updatedSections = setConfigSectionValueAtPath(
|
|
109
|
+
resolveSourceSections(state, source),
|
|
110
|
+
resolvedPath,
|
|
111
|
+
nextValue
|
|
112
|
+
)
|
|
113
|
+
const sectionValue = updatedSections[resolvedPath.section]
|
|
114
|
+
const parsed = await validateConfigSection(resolvedPath.section, sectionValue, { cwd })
|
|
115
|
+
if (!parsed.success) {
|
|
116
|
+
throw new Error(`Invalid config value for ${resolvedPath.section}:\n${formatValidationIssues(parsed.error)}`)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await updateConfigFile({
|
|
120
|
+
workspaceFolder: cwd,
|
|
121
|
+
source,
|
|
122
|
+
section: resolvedPath.section,
|
|
123
|
+
value: parsed.data
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const updatedValue = getConfigSectionValueAtPath(updatedSections, resolvedPath).value
|
|
127
|
+
if (opts.json) {
|
|
128
|
+
printJsonResult({
|
|
129
|
+
ok: true,
|
|
130
|
+
workspaceFolder: cwd,
|
|
131
|
+
source,
|
|
132
|
+
path: resolvedPath.normalizedPath,
|
|
133
|
+
section: resolvedPath.section,
|
|
134
|
+
configPath: result.configPath,
|
|
135
|
+
value: updatedValue
|
|
136
|
+
})
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`Updated ${resolvedPath.normalizedPath} in ${source} config.`)
|
|
141
|
+
console.log(`File: ${result.configPath}`)
|
|
142
|
+
process.stdout.write(formatDisplayValue(updatedValue))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const runUnsetCommand = async (
|
|
146
|
+
pathInput: string | undefined,
|
|
147
|
+
opts: ConfigUnsetOptions
|
|
148
|
+
) => {
|
|
149
|
+
const cwd = process.cwd()
|
|
150
|
+
const source = await resolveWritableSource(opts.source, opts.json)
|
|
151
|
+
const path = await resolveSetPathInput(pathInput, opts.json)
|
|
152
|
+
const resolvedPath = resolveConfigSectionPath(parseConfigSectionPath(path))
|
|
153
|
+
assertWritableConfigSection(source, resolvedPath.section)
|
|
154
|
+
const state = await loadCommandState(cwd)
|
|
155
|
+
const currentSections = resolveSourceSections(state, source)
|
|
156
|
+
const currentValue = getConfigSectionValueAtPath(currentSections, resolvedPath)
|
|
157
|
+
if (!currentValue.exists || (resolvedPath.sectionPath.length === 0 && !hasConfigSectionValue(currentValue.value))) {
|
|
158
|
+
throw new Error(`Config path "${resolvedPath.normalizedPath}" was not found in ${source} config.`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const updatedSections = resolvedPath.sectionPath.length === 0
|
|
162
|
+
? {
|
|
163
|
+
...currentSections,
|
|
164
|
+
[resolvedPath.section]: resolveClearedSectionValue(resolvedPath.section)
|
|
165
|
+
}
|
|
166
|
+
: unsetConfigSectionValueAtPath(currentSections, resolvedPath)
|
|
167
|
+
|
|
168
|
+
const sectionValue = updatedSections[resolvedPath.section]
|
|
169
|
+
const parsed = await validateConfigSection(resolvedPath.section, sectionValue, { cwd })
|
|
170
|
+
if (!parsed.success) {
|
|
171
|
+
throw new Error(`Invalid config value for ${resolvedPath.section}:\n${formatValidationIssues(parsed.error)}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = await updateConfigFile({
|
|
175
|
+
workspaceFolder: cwd,
|
|
176
|
+
source,
|
|
177
|
+
section: resolvedPath.section,
|
|
178
|
+
value: parsed.data
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (opts.json) {
|
|
182
|
+
printJsonResult({
|
|
183
|
+
ok: true,
|
|
184
|
+
workspaceFolder: cwd,
|
|
185
|
+
source,
|
|
186
|
+
path: resolvedPath.normalizedPath,
|
|
187
|
+
section: resolvedPath.section,
|
|
188
|
+
configPath: result.configPath,
|
|
189
|
+
removed: true
|
|
190
|
+
})
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(`Removed ${resolvedPath.normalizedPath} from ${source} config.`)
|
|
195
|
+
console.log(`File: ${result.configPath}`)
|
|
196
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ModelMetadataConfig, ModelServiceConfig } from '@oneworks/types'
|
|
2
|
+
|
|
3
|
+
import type { ResolvedReadState } from './read-state'
|
|
4
|
+
|
|
5
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => (
|
|
6
|
+
value != null &&
|
|
7
|
+
typeof value === 'object' &&
|
|
8
|
+
!Array.isArray(value)
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const asModelMetadataRecord = (value: unknown): Record<string, ModelMetadataConfig> => (
|
|
12
|
+
isRecord(value) ? value as Record<string, ModelMetadataConfig> : {}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const asModelServiceRecord = (value: unknown): Record<string, ModelServiceConfig> => (
|
|
16
|
+
isRecord(value) ? value as Record<string, ModelServiceConfig> : {}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const resolveModelDisplayMetadata = (params: {
|
|
20
|
+
selector: string
|
|
21
|
+
serviceKey: string
|
|
22
|
+
model: string
|
|
23
|
+
modelMetadata: Record<string, ModelMetadataConfig>
|
|
24
|
+
}) => {
|
|
25
|
+
const merged = {
|
|
26
|
+
...(params.modelMetadata[params.serviceKey] ?? {}),
|
|
27
|
+
...(params.modelMetadata[params.model] ?? {}),
|
|
28
|
+
...(params.modelMetadata[params.selector] ?? {})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Object.keys(merged).length === 0 ? undefined : merged
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const buildModelsDisplayValue = (params: {
|
|
35
|
+
modelMetadata: Record<string, ModelMetadataConfig>
|
|
36
|
+
modelServices: Record<string, ModelServiceConfig>
|
|
37
|
+
}) => {
|
|
38
|
+
const consumedMetadataKeys = new Set<string>()
|
|
39
|
+
|
|
40
|
+
const services = Object.fromEntries(
|
|
41
|
+
Object.entries(params.modelServices).flatMap(([serviceKey, serviceValue]) => {
|
|
42
|
+
const models = Array.isArray(serviceValue?.models)
|
|
43
|
+
? serviceValue.models.filter((model): model is string => typeof model === 'string' && model.trim() !== '')
|
|
44
|
+
: []
|
|
45
|
+
|
|
46
|
+
if (models.length === 0) {
|
|
47
|
+
return []
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const entries = Object.fromEntries(
|
|
51
|
+
models.map((model) => {
|
|
52
|
+
const selector = `${serviceKey},${model}`
|
|
53
|
+
const metadata = resolveModelDisplayMetadata({
|
|
54
|
+
selector,
|
|
55
|
+
serviceKey,
|
|
56
|
+
model,
|
|
57
|
+
modelMetadata: params.modelMetadata
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
consumedMetadataKeys.add(serviceKey)
|
|
61
|
+
consumedMetadataKeys.add(model)
|
|
62
|
+
consumedMetadataKeys.add(selector)
|
|
63
|
+
|
|
64
|
+
return [
|
|
65
|
+
model,
|
|
66
|
+
metadata == null
|
|
67
|
+
? {
|
|
68
|
+
selector
|
|
69
|
+
}
|
|
70
|
+
: {
|
|
71
|
+
selector,
|
|
72
|
+
...metadata
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return [[serviceKey, entries]]
|
|
79
|
+
})
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (Object.keys(services).length === 0) {
|
|
83
|
+
return params.modelMetadata
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const remainingMetadata = Object.fromEntries(
|
|
87
|
+
Object.entries(params.modelMetadata)
|
|
88
|
+
.filter(([key]) => !consumedMetadataKeys.has(key))
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return Object.keys(remainingMetadata).length === 0
|
|
92
|
+
? services
|
|
93
|
+
: {
|
|
94
|
+
services,
|
|
95
|
+
metadata: remainingMetadata
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const resolveReadableConfigValue = (resolved: ResolvedReadState) => {
|
|
100
|
+
if (resolved.resolvedPath?.section !== 'models' || resolved.resolvedPath.sectionPath.length > 0) {
|
|
101
|
+
return resolved.value
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return buildModelsDisplayValue({
|
|
105
|
+
modelMetadata: asModelMetadataRecord(resolved.state.sections[resolved.source].models),
|
|
106
|
+
modelServices: asModelServiceRecord(resolved.state.sections[resolved.source].modelServices)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { Option } from 'commander'
|
|
4
|
+
import type { Command } from 'commander'
|
|
5
|
+
|
|
6
|
+
import { runGetCommand, runListCommand, runSetCommand, runUnsetCommand } from './actions'
|
|
7
|
+
import {
|
|
8
|
+
CONFIG_LIST_SOURCES,
|
|
9
|
+
CONFIG_READ_SOURCES,
|
|
10
|
+
CONFIG_SET_SOURCES,
|
|
11
|
+
CONFIG_VALUE_TYPES,
|
|
12
|
+
formatErrorMessage
|
|
13
|
+
} from './shared'
|
|
14
|
+
import type { ConfigGetOptions, ConfigListOptions, ConfigSetOptions, ConfigUnsetOptions } from './shared'
|
|
15
|
+
|
|
16
|
+
const exitWithError = (error: unknown, json = false): never => {
|
|
17
|
+
const message = formatErrorMessage(error)
|
|
18
|
+
if (json) {
|
|
19
|
+
console.error(JSON.stringify({ ok: false, error: message }, null, 2))
|
|
20
|
+
} else {
|
|
21
|
+
console.error(message)
|
|
22
|
+
}
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerConfigCommand(program: Command) {
|
|
27
|
+
const configCommand = program
|
|
28
|
+
.command('config')
|
|
29
|
+
.description('Inspect and update workspace config files')
|
|
30
|
+
|
|
31
|
+
configCommand
|
|
32
|
+
.command('list [path]')
|
|
33
|
+
.description('List config sections or show a config subtree')
|
|
34
|
+
.addOption(
|
|
35
|
+
new Option('--source <source>', 'List a single source instead of all sources')
|
|
36
|
+
.choices([...CONFIG_LIST_SOURCES])
|
|
37
|
+
)
|
|
38
|
+
.option('--json', 'Print JSON output', false)
|
|
39
|
+
.addHelpText(
|
|
40
|
+
'after',
|
|
41
|
+
`
|
|
42
|
+
Examples:
|
|
43
|
+
oneworks config list
|
|
44
|
+
oneworks config list models --json
|
|
45
|
+
oneworks config list adapters.codex --source project
|
|
46
|
+
oneworks config list --source all
|
|
47
|
+
`
|
|
48
|
+
)
|
|
49
|
+
.action(async (path: string | undefined, opts: ConfigListOptions) => {
|
|
50
|
+
try {
|
|
51
|
+
await runListCommand(path, opts)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
exitWithError(error, opts.json)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
configCommand
|
|
58
|
+
.command('get [path]')
|
|
59
|
+
.description('Read a config section or a nested config value')
|
|
60
|
+
.addOption(
|
|
61
|
+
new Option('--source <source>', 'Read from a specific config source')
|
|
62
|
+
.choices([...CONFIG_READ_SOURCES])
|
|
63
|
+
)
|
|
64
|
+
.option('--json', 'Print JSON output', false)
|
|
65
|
+
.addHelpText(
|
|
66
|
+
'after',
|
|
67
|
+
`
|
|
68
|
+
Examples:
|
|
69
|
+
oneworks config get general.defaultModel
|
|
70
|
+
oneworks config get permissions.allow --source project
|
|
71
|
+
oneworks config get '["models","gpt-4.1","title"]' --source merged --json
|
|
72
|
+
`
|
|
73
|
+
)
|
|
74
|
+
.action(async (path: string | undefined, opts: ConfigGetOptions) => {
|
|
75
|
+
try {
|
|
76
|
+
await runGetCommand(path, opts)
|
|
77
|
+
} catch (error) {
|
|
78
|
+
exitWithError(error, opts.json)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
configCommand
|
|
83
|
+
.command('set [path] [value]')
|
|
84
|
+
.description('Update a config section or nested config value')
|
|
85
|
+
.addOption(
|
|
86
|
+
new Option('--source <source>', 'Write to a specific config source')
|
|
87
|
+
.choices([...CONFIG_SET_SOURCES])
|
|
88
|
+
)
|
|
89
|
+
.addOption(
|
|
90
|
+
new Option('--type <type>', 'How to parse the provided value')
|
|
91
|
+
.choices([...CONFIG_VALUE_TYPES])
|
|
92
|
+
)
|
|
93
|
+
.option('--json', 'Print JSON output', false)
|
|
94
|
+
.addHelpText(
|
|
95
|
+
'after',
|
|
96
|
+
`
|
|
97
|
+
Examples:
|
|
98
|
+
oneworks config set general.defaultModel gpt-5.4 --type string
|
|
99
|
+
oneworks config set general.permissions '{"allow":["Read"]}' --type json
|
|
100
|
+
echo '{"args":["--port","3000"]}' | oneworks config set mcp.mcpServers.docs --type json --json
|
|
101
|
+
`
|
|
102
|
+
)
|
|
103
|
+
.action(async (path: string | undefined, value: string | undefined, opts: ConfigSetOptions) => {
|
|
104
|
+
try {
|
|
105
|
+
await runSetCommand(path, value, opts)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
exitWithError(error, opts.json)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
configCommand
|
|
112
|
+
.command('unset [path]')
|
|
113
|
+
.description('Remove a config section or nested config value')
|
|
114
|
+
.addOption(
|
|
115
|
+
new Option('--source <source>', 'Write to a specific config source')
|
|
116
|
+
.choices([...CONFIG_SET_SOURCES])
|
|
117
|
+
)
|
|
118
|
+
.option('--json', 'Print JSON output', false)
|
|
119
|
+
.addHelpText(
|
|
120
|
+
'after',
|
|
121
|
+
`
|
|
122
|
+
Examples:
|
|
123
|
+
oneworks config unset general.defaultModel
|
|
124
|
+
oneworks config unset general.permissions.allow --source project
|
|
125
|
+
oneworks config unset plugins --source user --json
|
|
126
|
+
`
|
|
127
|
+
)
|
|
128
|
+
.action(async (path: string | undefined, opts: ConfigUnsetOptions) => {
|
|
129
|
+
try {
|
|
130
|
+
await runUnsetCommand(path, opts)
|
|
131
|
+
} catch (error) {
|
|
132
|
+
exitWithError(error, opts.json)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { createInterface } from 'node:readline/promises'
|
|
3
|
+
|
|
4
|
+
import type { ConfigSource } from '@oneworks/types'
|
|
5
|
+
|
|
6
|
+
import { CONFIG_SET_SOURCES, isInteractiveTerminal } from './shared'
|
|
7
|
+
import type { ConfigValueType } from './shared'
|
|
8
|
+
|
|
9
|
+
const readAllStdin = async () =>
|
|
10
|
+
await new Promise<string>((resolve, reject) => {
|
|
11
|
+
const chunks: string[] = []
|
|
12
|
+
process.stdin.setEncoding('utf8')
|
|
13
|
+
process.stdin.on('data', chunk => chunks.push(String(chunk)))
|
|
14
|
+
process.stdin.once('end', () => resolve(chunks.join('')))
|
|
15
|
+
process.stdin.once('error', reject)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const promptSelect = async <TValue extends string>(params: {
|
|
19
|
+
question: string
|
|
20
|
+
choices: readonly TValue[]
|
|
21
|
+
defaultValue: TValue
|
|
22
|
+
}) => {
|
|
23
|
+
const rl = createInterface({
|
|
24
|
+
input: process.stdin,
|
|
25
|
+
output: process.stdout
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
while (true) {
|
|
30
|
+
console.log(params.choices.map((choice, index) => ` ${index + 1}. ${choice}`).join('\n'))
|
|
31
|
+
const answer = (await rl.question(`${params.question} [${params.defaultValue}]: `)).trim()
|
|
32
|
+
if (answer === '') {
|
|
33
|
+
return params.defaultValue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const matchedChoice = params.choices.find(choice => choice === answer)
|
|
37
|
+
if (matchedChoice != null) {
|
|
38
|
+
return matchedChoice
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const index = Number.parseInt(answer, 10)
|
|
42
|
+
if (Number.isInteger(index) && index >= 1 && index <= params.choices.length) {
|
|
43
|
+
return params.choices[index - 1]!
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('Please choose one of the listed values.')
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
rl.close()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const promptText = async (question: string) => {
|
|
54
|
+
const rl = createInterface({
|
|
55
|
+
input: process.stdin,
|
|
56
|
+
output: process.stdout
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return (await rl.question(question)).trim()
|
|
61
|
+
} finally {
|
|
62
|
+
rl.close()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const resolveWritableSource = async (
|
|
67
|
+
source: ConfigSource | undefined,
|
|
68
|
+
json: boolean | undefined
|
|
69
|
+
) => {
|
|
70
|
+
if (source != null) return source
|
|
71
|
+
if (json || !isInteractiveTerminal()) return 'project'
|
|
72
|
+
|
|
73
|
+
return await promptSelect({
|
|
74
|
+
question: 'Write target',
|
|
75
|
+
choices: CONFIG_SET_SOURCES,
|
|
76
|
+
defaultValue: 'project'
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const resolveSetPathInput = async (
|
|
81
|
+
pathInput: string | undefined,
|
|
82
|
+
json: boolean | undefined
|
|
83
|
+
) => {
|
|
84
|
+
if (pathInput != null) return pathInput
|
|
85
|
+
if (json || !isInteractiveTerminal()) {
|
|
86
|
+
throw new TypeError('Config path is required for set.')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prompted = await promptText(
|
|
90
|
+
'Config path (examples: general.defaultModel, general.permissions, ["models","gpt-4.1","title"]): '
|
|
91
|
+
)
|
|
92
|
+
if (prompted === '') {
|
|
93
|
+
throw new TypeError('Config path is required for set.')
|
|
94
|
+
}
|
|
95
|
+
return prompted
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const resolveSetValueInput = async (
|
|
99
|
+
valueInput: string | undefined,
|
|
100
|
+
type: ConfigValueType,
|
|
101
|
+
json: boolean | undefined
|
|
102
|
+
) => {
|
|
103
|
+
if (valueInput != null) return valueInput
|
|
104
|
+
|
|
105
|
+
if (!process.stdin.isTTY) {
|
|
106
|
+
const stdinValue = await readAllStdin()
|
|
107
|
+
if (stdinValue !== '') {
|
|
108
|
+
return stdinValue
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (type === 'null') {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (json || !isInteractiveTerminal()) {
|
|
117
|
+
throw new TypeError('Config value is required. Pass it as an argument or pipe it via stdin.')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return await promptText(`Config value (${type === 'auto' ? 'auto' : type} parse mode): `)
|
|
121
|
+
}
|