agentsw 0.9.2 → 0.10.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/README.md CHANGED
@@ -113,7 +113,7 @@ dsh yes openai+anthropic myproxy · glm-5.3-flash ~/.d
113
113
  | [Oh My Pi](https://omp.sh) (omp) | `~/.omp/agent/models.yml` (comments preserved) | both |
114
114
  | [pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) | `~/.pi/agent/models.json` + `settings.json` | both |
115
115
  | [prime-agent](https://github.com/PrimeIntellect-ai/prime-agent) | `~/.prime/agent/models.json` + `settings.json` | both |
116
- | [opencode](https://opencode.ai) | `~/.config/opencode/opencode.json` | both |
116
+ | [opencode](https://opencode.ai) | `~/.config/opencode/opencode.json`, `.jsonc` or `config.json` (the one that exists; comments preserved) | both |
117
117
  | [Hermes](https://pypi.org/project/hermes-agent/) | `~/.hermes/config.yaml` + `.env` (comments preserved) | both |
118
118
  | WorkBuddy | `~/.workbuddy/models.json` + `settings.json` | openai |
119
119
  | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) | `~/.dsh/settings.yaml` + `.credentials.yaml` | both |
@@ -121,6 +121,15 @@ dsh yes openai+anthropic myproxy · glm-5.3-flash ~/.d
121
121
  An agent that is not installed is skipped, not guessed at. `--apps codex,omp` narrows a run;
122
122
  `--apps` with an uninstalled agent forces it.
123
123
 
124
+ Every adapter honors the configuration-directory variable its app defines, and resolves it on
125
+ each command, so `use`, `sync`, `status`, `rename` and `remove` always agree on the same files:
126
+ `CLAUDE_CONFIG_DIR` (Claude Code), `CODEX_HOME` (Codex), `PI_CODING_AGENT_DIR` /
127
+ `PRIME_AGENT_CODING_AGENT_DIR` (pi / prime-agent), `OPENCODE_CONFIG_DIR` and `OPENCODE_CONFIG`
128
+ (opencode), `HERMES_HOME` (Hermes), `DSH_HOME` (dsh), `WORKBUDDY_CONFIG_DIR` (WorkBuddy, with
129
+ the legacy `CODEBUDDY_CONFIG_DIR` as fallback). An empty value is ignored, a leading `~` is
130
+ expanded, and opencode is edited in place whichever of `opencode.json`, `opencode.jsonc` or
131
+ `config.json` exists.
132
+
124
133
  The interactive menu asks the same question before writing: a multi-select where the first row
125
134
  is **all detected apps**, detected agents are listed first and undetected ones are marked. The
126
135
  last selection is remembered, so a menu `use`/`sync` keeps rewriting only the agents you picked.
@@ -294,6 +303,38 @@ Do not combine `--apps` and `--prune`. Renames and removals preflight changes an
294
303
  private backups before writes. App-only removal leaves the store intact, so a later explicit
295
304
  sync of a managed provider can add it back to that app.
296
305
 
306
+ ### Global options
307
+
308
+ These work with every command, written before or after the subcommand:
309
+
310
+ | Option | Effect |
311
+ |---|---|
312
+ | `--json` | write exactly one JSON document to stdout and nothing else; interactive prompts and progress are disabled, so a scripted run never blocks on input |
313
+ | `-q, --quiet` | suppress progress, hints and warnings; results and errors are kept |
314
+ | `--no-color` | disable colored output (`NO_COLOR=1` does the same; `FORCE_COLOR=1` forces color, `--no-color` wins) |
315
+ | `--lang en\|zh-CN` | interface language |
316
+
317
+ ```bash
318
+ asw status --json | jq '.data.apps[] | select(.current != .id)'
319
+ asw use myproxy -a codex --dry-run --json # file paths only, never config bodies
320
+ asw list --quiet
321
+ ```
322
+
323
+ Success is `{"ok":true,"command":"status","version":"…","data":{…}}`; a failure is
324
+ `{"ok":false,…,"error":{"message":"…"}}` on stdout with exit code 1. `--json` overrides
325
+ `--quiet`. Credentials never appear in a JSON payload, and `--json --dry-run` reports the file
326
+ paths a run would touch — never the staged configuration. `install` and `upgrade` hand their
327
+ stdio to the package manager, whose own output still reaches the terminal (under `--json` its
328
+ stdout is routed to stderr so the JSON document stays parseable).
329
+
330
+ Results go to stdout; progress, warnings and the dimmed next-step hints go to stderr. So
331
+ `agentsw status > out.txt` captures only the table, and a script can read stderr for what
332
+ happened without parsing it out of the result.
333
+
334
+ Exit codes: `0` success, `1` error, `130` cancelled at a prompt. Without `--json`,
335
+ `models --provider <id> --metadata` keeps printing its bare JSON object, so existing scripts
336
+ are unaffected.
337
+
297
338
  ## How your configs are treated
298
339
 
299
340
  - Existing provider-store and agent configuration files are backed up before committed
package/README.zh-CN.md CHANGED
@@ -108,7 +108,7 @@ dsh yes openai+anthropic myproxy · glm-5.3-flash ~/.d
108
108
  | [Oh My Pi](https://omp.sh) (omp) | `~/.omp/agent/models.yml`(保留注释) | 双协议 |
109
109
  | [pi](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) | `~/.pi/agent/models.json` + `settings.json` | 双协议 |
110
110
  | [prime-agent](https://github.com/PrimeIntellect-ai/prime-agent) | `~/.prime/agent/models.json` + `settings.json` | 双协议 |
111
- | [opencode](https://opencode.ai) | `~/.config/opencode/opencode.json` | 双协议 |
111
+ | [opencode](https://opencode.ai) | `~/.config/opencode/` 下已存在的 `opencode.json`、`.jsonc` 或 `config.json`(保留注释) | 双协议 |
112
112
  | [Hermes](https://pypi.org/project/hermes-agent/) | `~/.hermes/config.yaml` + `.env`(保留注释) | 双协议 |
113
113
  | WorkBuddy | `~/.workbuddy/models.json` + `settings.json` | openai |
114
114
  | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) | `~/.dsh/settings.yaml` + `.credentials.yaml` | 双协议 |
@@ -116,6 +116,14 @@ dsh yes openai+anthropic myproxy · glm-5.3-flash ~/.d
116
116
  未安装的应用会被跳过,而不是瞎猜。`--apps codex,omp` 只跑指定应用;
117
117
  对未检测到的应用显式传 `--apps` 可强制写入。
118
118
 
119
+ 每个适配器都会遵循对应应用自己的配置目录环境变量,并在每次命令中重新解析,因此
120
+ `use`、`sync`、`status`、`rename`、`remove` 始终指向同一批文件:`CLAUDE_CONFIG_DIR`
121
+ (Claude Code)、`CODEX_HOME`(Codex)、`PI_CODING_AGENT_DIR` / `PRIME_AGENT_CODING_AGENT_DIR`
122
+ (pi / prime-agent)、`OPENCODE_CONFIG_DIR` 与 `OPENCODE_CONFIG`(opencode)、`HERMES_HOME`
123
+ (Hermes)、`DSH_HOME`(dsh)、`WORKBUDDY_CONFIG_DIR`(WorkBuddy,回退到旧的
124
+ `CODEBUDDY_CONFIG_DIR`)。空值会被忽略,开头的 `~` 会展开;opencode 会就地编辑实际存在的
125
+ `opencode.json`、`opencode.jsonc` 或 `config.json`。
126
+
119
127
  交互菜单写配置前会问同一个问题:多选框第一行是**全部检测到的应用**,已检测到的排在前面,
120
128
  未检测到的会标注出来。上次的选择会被记住,所以菜单里的 `use`/`sync` 只会继续写你勾选的那几个。
121
129
  任何配置文件解析失败(JSON/YAML/TOML 非法)都会报出文件与位置,绝不回显出错那一行;
@@ -278,6 +286,35 @@ asw remove unused-provider --apps omp
278
286
  重命名和删除会先检查全部计划变更,再创建私有备份并写入。
279
287
  仅从智能体删除时中央条目仍保留,之后主动同步该供应商会重新写入该智能体。
280
288
 
289
+ ### 全局选项
290
+
291
+ 所有命令都支持,写在子命令前后均可:
292
+
293
+ | 选项 | 作用 |
294
+ |---|---|
295
+ | `--json` | stdout 只输出一份 JSON 文档;同时关闭交互提问与进度,脚本运行绝不因等待输入而挂起 |
296
+ | `-q, --quiet` | 抑制进度、提示与警告;结果与错误保留 |
297
+ | `--no-color` | 关闭彩色输出(`NO_COLOR=1` 等效;`FORCE_COLOR=1` 强制彩色,`--no-color` 优先) |
298
+ | `--lang en\|zh-CN` | 界面语言 |
299
+
300
+ ```bash
301
+ asw status --json | jq '.data.apps[] | select(.current != .id)'
302
+ asw use myproxy -a codex --dry-run --json # 只给文件路径,不含配置正文
303
+ asw list --quiet
304
+ ```
305
+
306
+ 成功时输出 `{"ok":true,"command":"status","version":"…","data":{…}}`;失败时 stdout 输出
307
+ `{"ok":false,…,"error":{"message":"…"}}` 并以退出码 1 结束。`--json` 优先于 `--quiet`。
308
+ JSON 载荷一律不含凭据;`--json --dry-run` 只报告将要改动的文件路径,绝不输出待写入的配置内容。
309
+ `install` 与 `upgrade` 把 stdio 交给包管理器,其输出仍会到达终端(`--json` 下会把它的 stdout
310
+ 转到 stderr,以保证 JSON 文档可解析)。
311
+
312
+ 结果写 stdout,进度、警告与灰字的下一步提示写 stderr。因此 `agentsw status > out.txt` 只截到
313
+ 表格本身,脚本也可以单独读 stderr 了解过程中发生了什么,而不用从结果里把它剥出来。
314
+
315
+ 退出码:`0` 成功,`1` 出错,`130` 在交互提问处取消。不带 `--json` 时
316
+ `models --provider <id> --metadata` 仍输出原来的裸 JSON 对象,现有脚本不受影响。
317
+
281
318
  ## 它如何对待你的配置
282
319
 
283
320
  - 正式提交变更前,已有中央存储和智能体配置先备份到
@@ -0,0 +1,127 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { appDataDir, expandHome, home, localAppDataDir } from "./fsutil.js";
4
+ /**
5
+ * Every app path lives here and is resolved on each call, so one command's `use`/`sync`
6
+ * targets the same files as `remove`/`rename`/`status` — including when the app's own
7
+ * config-directory environment variable is set. An empty value is ignored (the apps do
8
+ * the same); a relative value is kept as-is, exactly as the app would resolve it against
9
+ * its own working directory.
10
+ */
11
+ function envPath(name) {
12
+ const raw = process.env[name]?.trim();
13
+ return raw ? expandHome(raw) : undefined;
14
+ }
15
+ export function claudeDir() {
16
+ return envPath("CLAUDE_CONFIG_DIR") ?? path.join(home, ".claude");
17
+ }
18
+ export function claudeSettingsFile() {
19
+ return path.join(claudeDir(), "settings.json");
20
+ }
21
+ export function codexDir() {
22
+ return envPath("CODEX_HOME") ?? path.join(home, ".codex");
23
+ }
24
+ export function codexConfigFile() {
25
+ return path.join(codexDir(), "config.toml");
26
+ }
27
+ export function codexAuthFile() {
28
+ return path.join(codexDir(), "auth.json");
29
+ }
30
+ const PI_LAYOUT = {
31
+ pi: { env: "PI_CODING_AGENT_DIR", dir: ".pi/agent" },
32
+ prime: { env: "PRIME_AGENT_CODING_AGENT_DIR", dir: ".prime/agent" },
33
+ };
34
+ export function piDir(id) {
35
+ return envPath(PI_LAYOUT[id].env) ?? path.join(home, PI_LAYOUT[id].dir);
36
+ }
37
+ export function piModelsFile(id) {
38
+ return path.join(piDir(id), "models.json");
39
+ }
40
+ export function piSettingsFile(id) {
41
+ return path.join(piDir(id), "settings.json");
42
+ }
43
+ export function ompAgentDir() {
44
+ return path.join(home, ".omp", "agent");
45
+ }
46
+ /** An existing models.yaml wins only when models.yml is absent (omp precedence). */
47
+ export function ompModelsFiles() {
48
+ return ["models.yml", "models.yaml"].map((name) => path.join(ompAgentDir(), name));
49
+ }
50
+ export function ompConfigFiles() {
51
+ return ["config.yml", "config.yaml"].map((name) => path.join(ompAgentDir(), name));
52
+ }
53
+ export function hermesDir() {
54
+ return envPath("HERMES_HOME") ?? localAppDataDir("hermes");
55
+ }
56
+ export function hermesConfigFile() {
57
+ return path.join(hermesDir(), "config.yaml");
58
+ }
59
+ export function hermesEnvFile() {
60
+ return path.join(hermesDir(), ".env");
61
+ }
62
+ export function dshDir() {
63
+ return envPath("DSH_HOME") ?? localAppDataDir("dsh");
64
+ }
65
+ /** The extension picks the document format, so an existing .yml/.json is written in place. */
66
+ export function dshSettingsFiles() {
67
+ return ["settings.yaml", "settings.yml", "settings.json"].map((name) => path.join(dshDir(), name));
68
+ }
69
+ export function dshSettingsFile() {
70
+ return dshSettingsFiles().find((file) => fs.existsSync(file)) ?? path.join(dshDir(), "settings.yaml");
71
+ }
72
+ export function dshCredentialsFile() {
73
+ return path.join(dshDir(), ".credentials.yaml");
74
+ }
75
+ export function workbuddyDir() {
76
+ return envPath("WORKBUDDY_CONFIG_DIR") ?? envPath("CODEBUDDY_CONFIG_DIR") ??
77
+ (process.platform === "win32" ? appDataDir("workbuddy") : path.join(home, ".workbuddy"));
78
+ }
79
+ export function workbuddyModelsFile() {
80
+ return path.join(workbuddyDir(), "models.json");
81
+ }
82
+ export function workbuddySettingsFile() {
83
+ return path.join(workbuddyDir(), "settings.json");
84
+ }
85
+ /** opencode's loader accepts any of these names in its config directory. */
86
+ export const OPENCODE_CONFIG_FILENAMES = ["opencode.json", "opencode.jsonc", "config.json"];
87
+ export function opencodeConfigDirs() {
88
+ const dirs = [];
89
+ const custom = envPath("OPENCODE_CONFIG_DIR");
90
+ if (custom)
91
+ dirs.push(custom);
92
+ const fallback = appDataDir("opencode");
93
+ if (!dirs.includes(fallback))
94
+ dirs.push(fallback);
95
+ return dirs;
96
+ }
97
+ /** Every file a management command must consider, across both directories and the explicit file. */
98
+ export function opencodeConfigFiles() {
99
+ const files = [];
100
+ const add = (file) => {
101
+ if (!files.includes(file))
102
+ files.push(file);
103
+ };
104
+ for (const dir of opencodeConfigDirs()) {
105
+ for (const name of OPENCODE_CONFIG_FILENAMES)
106
+ add(path.join(dir, name));
107
+ }
108
+ const explicit = envPath("OPENCODE_CONFIG");
109
+ if (explicit)
110
+ add(explicit);
111
+ return files;
112
+ }
113
+ /** OPENCODE_CONFIG (an explicit file), else the first existing config in the search order, else the creation default. */
114
+ export function opencodePrimaryConfigFile() {
115
+ const explicit = envPath("OPENCODE_CONFIG");
116
+ if (explicit)
117
+ return explicit;
118
+ const dirs = opencodeConfigDirs();
119
+ for (const dir of dirs) {
120
+ for (const name of OPENCODE_CONFIG_FILENAMES) {
121
+ const file = path.join(dir, name);
122
+ if (fs.existsSync(file))
123
+ return file;
124
+ }
125
+ }
126
+ return path.join(dirs[0] ?? appDataDir("opencode"), "opencode.json");
127
+ }
package/dist/apps.js CHANGED
@@ -2,7 +2,9 @@ import { execFileSync, execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import semver from "semver";
5
- import { appDataDir, home, readJsonIfExists } from "./fsutil.js";
5
+ import { home, readJsonIfExists } from "./fsutil.js";
6
+ import { dshDir, workbuddyDir } from "./app-paths.js";
7
+ import { isJson } from "./ui.js";
6
8
  export const appPackages = [
7
9
  {
8
10
  id: "claude",
@@ -68,7 +70,7 @@ export const appPackages = [
68
70
  name: "WorkBuddy",
69
71
  // Electron desktop app: version from its data dir; managed by its own updater.
70
72
  localVersion: () => {
71
- const dir = workbuddyDataDir();
73
+ const dir = workbuddyDir();
72
74
  const j = readJsonIfExists(path.join(dir, "last-launch.json"));
73
75
  const v = (j?.version ?? j?.appVersion);
74
76
  if (v)
@@ -90,12 +92,6 @@ export const appPackages = [
90
92
  localVersion: () => dshLocalVersion(),
91
93
  },
92
94
  ];
93
- // dsh config dir; respects $DSH_HOME.
94
- function dshHome() {
95
- const env = process.env.DSH_HOME?.trim();
96
- return env ? (env.startsWith("~") ? path.join(home, env.slice(1)) : env) :
97
- process.platform === "win32" ? path.join(home, "AppData", "Local", "dsh") : path.join(home, ".dsh");
98
- }
99
95
  /** dsh version: search npx cache for @deepseek-ai/dsh, fall back to "?" if config dir exists. */
100
96
  function dshLocalVersion() {
101
97
  // 1. Fast path: check global node_modules directly (no subprocess)
@@ -125,14 +121,10 @@ function dshLocalVersion() {
125
121
  }
126
122
  }
127
123
  // 3. Config dir exists -> used via npx, version unknown
128
- if (fs.existsSync(dshHome()))
124
+ if (fs.existsSync(dshDir()))
129
125
  return "?";
130
126
  return undefined;
131
127
  }
132
- function workbuddyDataDir() {
133
- return process.env.WORKBUDDY_CONFIG_DIR?.trim() || process.env.CODEBUDDY_CONFIG_DIR?.trim() ||
134
- (process.platform === "win32" ? appDataDir("workbuddy") : path.join(home, ".workbuddy"));
135
- }
136
128
  const SEMVERISH = /\d+\.\d+(\.\d+)?([-.+][\w.-]+)?/;
137
129
  export function installedVersion(app) {
138
130
  if (app.binary) {
@@ -224,11 +216,15 @@ export function isNewer(installed, latest) {
224
216
  }
225
217
  /** Run an install/upgrade command with live output. Throws on nonzero exit. */
226
218
  export function runShell(command) {
219
+ // --json owns stdout, so the child's output moves to stderr to keep the document parseable.
220
+ const stdio = isJson()
221
+ ? ["inherit", process.stderr.fd, "inherit"]
222
+ : "inherit";
227
223
  if (process.platform === "win32") {
228
- execSync(command, { stdio: "inherit", env: process.env });
224
+ execSync(command, { stdio, env: process.env });
229
225
  }
230
226
  else {
231
- execFileSync("bash", ["-o", "pipefail", "-c", command], { stdio: "inherit", env: process.env });
227
+ execFileSync("bash", ["-o", "pipefail", "-c", command], { stdio, env: process.env });
232
228
  }
233
229
  }
234
230
  /** Resolve the platform-specific command while keeping the package table readable. */