@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,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
+ }