agentsw 0.6.1 → 0.7.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 +22 -0
- package/README.zh-CN.md +21 -0
- package/dist/commands.js +30 -40
- package/dist/config-transaction.js +138 -0
- package/dist/envfile.js +27 -0
- package/dist/i18n.js +24 -3
- package/dist/import.js +35 -21
- package/dist/index.js +16 -3
- package/dist/jsonc.js +111 -0
- package/dist/menu.js +90 -9
- package/dist/provider-actions.js +37 -0
- package/dist/remove.js +651 -0
- package/dist/rename.js +458 -0
- package/dist/slug.js +17 -8
- package/dist/sources/ccswitch.js +4 -2
- package/dist/targets/claudecode.js +4 -3
- package/dist/targets/omp.js +52 -13
- package/dist/targets/pistyle.js +57 -24
- package/dist/targets/workbuddy.js +11 -10
- package/package.json +2 -1
package/dist/jsonc.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { applyEdits, createScanner, getNodeValue, modify, parseTree, printParseErrorCode, } from "jsonc-parser";
|
|
3
|
+
export function isJsonObject(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function parseJsoncObject(file, text) {
|
|
7
|
+
const source = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
8
|
+
const errors = [];
|
|
9
|
+
const root = parseTree(source, errors, { allowTrailingComma: true });
|
|
10
|
+
const error = errors[0];
|
|
11
|
+
if (error) {
|
|
12
|
+
// Do not quote source text: config files can contain credentials.
|
|
13
|
+
throw new Error(`${file}: invalid JSONC (${printParseErrorCode(error.error)} at offset ${error.offset})`);
|
|
14
|
+
}
|
|
15
|
+
if (root?.type !== "object")
|
|
16
|
+
throw new Error(`${file}: expected a JSON object at the document root`);
|
|
17
|
+
// Parsing resolves duplicate keys last-wins, but modify edits the first occurrence.
|
|
18
|
+
// Reject that ambiguity at any depth rather than syncing a value the agent will not read.
|
|
19
|
+
const checkDuplicates = (node) => {
|
|
20
|
+
const names = node.type === "object" ? new Set() : undefined;
|
|
21
|
+
for (const child of node.children ?? []) {
|
|
22
|
+
if (names) {
|
|
23
|
+
const name = child.children[0].value;
|
|
24
|
+
if (names.has(name)) {
|
|
25
|
+
throw new Error(`${file}: invalid JSONC (duplicate object property at offset ${child.offset})`);
|
|
26
|
+
}
|
|
27
|
+
names.add(name);
|
|
28
|
+
}
|
|
29
|
+
checkDuplicates(child);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
checkDuplicates(root);
|
|
33
|
+
return getNodeValue(root);
|
|
34
|
+
}
|
|
35
|
+
/** Only missing files initialize to an empty object; unreadable or invalid files must not be replaced. */
|
|
36
|
+
export function readJsoncObject(file) {
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = fs.readFileSync(file, "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code === "ENOENT")
|
|
43
|
+
return { file, text: "{}\n", value: {} };
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
return { file, text, value: parseJsoncObject(file, text) };
|
|
47
|
+
}
|
|
48
|
+
function comments(text) {
|
|
49
|
+
const result = [];
|
|
50
|
+
const scanner = createScanner(text);
|
|
51
|
+
for (let token = scanner.scan(); token !== 17 /* SyntaxKind.EOF */; token = scanner.scan()) {
|
|
52
|
+
if (token === 12 /* SyntaxKind.LineCommentTrivia */ || token === 13 /* SyntaxKind.BlockCommentTrivia */) {
|
|
53
|
+
result.push(text.slice(scanner.getTokenOffset(), scanner.getTokenOffset() + scanner.getTokenLength()));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/** modify() may include adjacent comments in deletion ranges; retain them as detached comments. */
|
|
59
|
+
function retainComments(text, edit, eol) {
|
|
60
|
+
const remaining = new Map();
|
|
61
|
+
for (const comment of comments(edit.content))
|
|
62
|
+
remaining.set(comment, (remaining.get(comment) ?? 0) + 1);
|
|
63
|
+
const removed = [];
|
|
64
|
+
for (const comment of comments(text.slice(edit.offset, edit.offset + edit.length))) {
|
|
65
|
+
const count = remaining.get(comment) ?? 0;
|
|
66
|
+
if (count)
|
|
67
|
+
remaining.set(comment, count - 1);
|
|
68
|
+
else
|
|
69
|
+
removed.push(comment);
|
|
70
|
+
}
|
|
71
|
+
return removed.length ? { ...edit, content: eol + removed.join(eol) + eol + edit.content } : edit;
|
|
72
|
+
}
|
|
73
|
+
/** Change only differing values, leaving unrelated fields, comments, and string literals intact. */
|
|
74
|
+
export function editJsoncObject(document, value) {
|
|
75
|
+
const bom = document.text.startsWith("\uFEFF") ? "\uFEFF" : "";
|
|
76
|
+
let text = bom ? document.text.slice(1) : document.text;
|
|
77
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
78
|
+
const update = (location, previous, next) => {
|
|
79
|
+
if (previous === next)
|
|
80
|
+
return;
|
|
81
|
+
if (isJsonObject(previous) && isJsonObject(next)) {
|
|
82
|
+
const keys = Object.keys(next);
|
|
83
|
+
if (keys.length || !Object.keys(previous).length) {
|
|
84
|
+
// Add before deleting, so a trailing-comma object never temporarily loses its last property.
|
|
85
|
+
for (const key of keys)
|
|
86
|
+
update([...location, key], previous[key], next[key]);
|
|
87
|
+
for (const key of Object.keys(previous)) {
|
|
88
|
+
if (!Object.hasOwn(next, key))
|
|
89
|
+
update([...location, key], previous[key], undefined);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// Replacing an emptying object also removes its trailing comma (modify's last-property
|
|
94
|
+
// deletion otherwise leaves `{,}`). retainComments keeps comments from the removed subtree.
|
|
95
|
+
}
|
|
96
|
+
// Replace shrinking arrays whole: modify's last-element deletion can truncate a compact
|
|
97
|
+
// value incorrectly. Equal-length/growing arrays still get incremental, comment-safe edits.
|
|
98
|
+
if (Array.isArray(previous) && Array.isArray(next) && next.length >= previous.length) {
|
|
99
|
+
for (let index = 0; index < next.length; index++) {
|
|
100
|
+
update([...location, index], previous[index], next[index]);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const edits = modify(text, location, next, { formattingOptions: { insertSpaces: true, tabSize: 2, eol } });
|
|
105
|
+
text = applyEdits(text, edits.map((edit) => retainComments(text, edit, eol)));
|
|
106
|
+
};
|
|
107
|
+
update([], document.value, value);
|
|
108
|
+
// Never let a library edit boundary or comment relocation write malformed output.
|
|
109
|
+
parseJsoncObject(document.file, text);
|
|
110
|
+
return bom + text;
|
|
111
|
+
}
|
package/dist/menu.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import prompts from "prompts";
|
|
3
|
-
import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdDiscover, cmdImport, cmdList,
|
|
3
|
+
import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdDiscover, cmdImport, cmdList, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
|
|
4
4
|
import { getLocale, setLocale, t } from "./i18n.js";
|
|
5
5
|
import { loadStore, saveStore } from "./store.js";
|
|
6
6
|
import { appPackages, appCommand, installedVersion } from "./apps.js";
|
|
7
|
+
import { cmdRemoveProvider, cmdRename } from "./provider-actions.js";
|
|
8
|
+
import { listRemovableProviders } from "./remove.js";
|
|
9
|
+
import { providerIdFromBaseUrl } from "./slug.js";
|
|
10
|
+
import { targets } from "./targets/index.js";
|
|
7
11
|
function bye() {
|
|
8
12
|
console.log(pc.dim(`\n${t("menu.bye")}`));
|
|
9
13
|
process.exit(0);
|
|
@@ -57,6 +61,80 @@ async function pickProvider(message) {
|
|
|
57
61
|
const p = store.providers[id];
|
|
58
62
|
return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
|
|
59
63
|
}
|
|
64
|
+
async function renameFromMenu() {
|
|
65
|
+
const picked = await pickProvider(t("menu.renameProvider"));
|
|
66
|
+
if (!picked)
|
|
67
|
+
return;
|
|
68
|
+
const provider = loadStore().providers[picked.id];
|
|
69
|
+
const { newId } = await prompts({
|
|
70
|
+
type: "text",
|
|
71
|
+
name: "newId",
|
|
72
|
+
message: t("menu.newId"),
|
|
73
|
+
initial: providerIdFromBaseUrl(provider.baseUrl, provider.protocol),
|
|
74
|
+
validate: (value) => /^[a-z0-9][a-z0-9_-]*$/.test(value.trim()) || t("add.idInvalid"),
|
|
75
|
+
}, cancel);
|
|
76
|
+
if (!newId)
|
|
77
|
+
return;
|
|
78
|
+
const next = String(newId).trim();
|
|
79
|
+
await cmdRename(picked.id, next, { dryRun: true });
|
|
80
|
+
if (await askToggle(t("menu.renameConfirm", { oldId: picked.id, newId: next }))) {
|
|
81
|
+
await cmdRename(picked.id, next);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function removeFromMenu() {
|
|
85
|
+
const { scope } = await prompts({
|
|
86
|
+
type: "select",
|
|
87
|
+
name: "scope",
|
|
88
|
+
message: t("menu.removeScope"),
|
|
89
|
+
hint: t("menu.selectInstructions"),
|
|
90
|
+
choices: [
|
|
91
|
+
{ title: t("menu.removeStore"), value: "store" },
|
|
92
|
+
{ title: t("menu.removeEverywhere"), value: "everywhere" },
|
|
93
|
+
{ title: t("menu.removeLocal"), value: "local" },
|
|
94
|
+
],
|
|
95
|
+
}, cancel);
|
|
96
|
+
if (!scope)
|
|
97
|
+
return;
|
|
98
|
+
let app;
|
|
99
|
+
if (scope === "local") {
|
|
100
|
+
const answer = await prompts({
|
|
101
|
+
type: "select",
|
|
102
|
+
name: "app",
|
|
103
|
+
message: t("menu.removeApp"),
|
|
104
|
+
hint: t("menu.selectInstructions"),
|
|
105
|
+
choices: targets.map((target) => ({ title: `${target.id} · ${target.name}`, value: target.id })),
|
|
106
|
+
}, cancel);
|
|
107
|
+
app = answer.app;
|
|
108
|
+
if (!app)
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const entries = app
|
|
112
|
+
? listRemovableProviders(app)
|
|
113
|
+
: Object.values(loadStore().providers).map((provider) => ({ id: provider.id, name: provider.name }));
|
|
114
|
+
if (!entries.length) {
|
|
115
|
+
console.log(pc.dim(t("menu.noRemovable")));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const { selected } = await prompts({
|
|
119
|
+
type: "select",
|
|
120
|
+
name: "selected",
|
|
121
|
+
message: t("menu.removeProvider"),
|
|
122
|
+
hint: t("menu.selectInstructions"),
|
|
123
|
+
choices: entries.map((entry, index) => ({
|
|
124
|
+
title: `${entry.id} · ${entry.app ?? "agentsw"}${entry.name && entry.name !== entry.id ? ` · ${entry.name}` : ""}`,
|
|
125
|
+
value: index,
|
|
126
|
+
})),
|
|
127
|
+
}, cancel);
|
|
128
|
+
const picked = entries[selected];
|
|
129
|
+
if (!picked)
|
|
130
|
+
return;
|
|
131
|
+
const opts = scope === "local" ? { apps: picked.app } : { prune: scope === "everywhere" };
|
|
132
|
+
await cmdRemoveProvider(picked.id, { ...opts, dryRun: true });
|
|
133
|
+
const scopeLabel = picked.app ?? t(scope === "everywhere" ? "menu.removeEverywhere" : "menu.removeStore");
|
|
134
|
+
if (await askToggle(t("menu.removeConfirmScope", { id: picked.id, scope: scopeLabel }))) {
|
|
135
|
+
await cmdRemoveProvider(picked.id, opts);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
60
138
|
export async function cmdMenu() {
|
|
61
139
|
if (!loadStore().language)
|
|
62
140
|
await chooseLanguage();
|
|
@@ -82,6 +160,7 @@ export async function cmdMenu() {
|
|
|
82
160
|
{ title: t("menu.list"), value: "list" },
|
|
83
161
|
{ title: t("menu.sync"), value: "sync" },
|
|
84
162
|
{ title: t("menu.discover"), value: "discover" },
|
|
163
|
+
{ title: t("menu.rename"), value: "rename" },
|
|
85
164
|
{ title: t("menu.remove"), value: "remove" },
|
|
86
165
|
{ title: t("menu.apps"), value: "apps" },
|
|
87
166
|
{ title: t("menu.installApp"), value: "install" },
|
|
@@ -142,14 +221,16 @@ export async function cmdMenu() {
|
|
|
142
221
|
const sync = await askToggle(t("menu.pushRefresh"));
|
|
143
222
|
await cmdDiscover(picked.id, { sync });
|
|
144
223
|
}
|
|
145
|
-
else if (action === "remove") {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
224
|
+
else if (action === "rename" || action === "remove") {
|
|
225
|
+
try {
|
|
226
|
+
if (action === "rename")
|
|
227
|
+
await renameFromMenu();
|
|
228
|
+
else
|
|
229
|
+
await removeFromMenu();
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
console.error(pc.red(error instanceof Error ? error.message : String(error)));
|
|
233
|
+
}
|
|
153
234
|
}
|
|
154
235
|
else if (action === "apps") {
|
|
155
236
|
await cmdApps();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { t } from "./i18n.js";
|
|
3
|
+
import { renameProvider } from "./rename.js";
|
|
4
|
+
import { listRemovableProviders, removeProvider } from "./remove.js";
|
|
5
|
+
function reportChanges(result, dryRun) {
|
|
6
|
+
console.log(t(dryRun ? "manage.preview" : "manage.changed", { count: result.files.length }));
|
|
7
|
+
for (const file of result.files)
|
|
8
|
+
console.log(` ${file}`);
|
|
9
|
+
if (result.backupDir)
|
|
10
|
+
console.log(pc.dim(t("manage.backup", { path: result.backupDir })));
|
|
11
|
+
}
|
|
12
|
+
export async function cmdRename(id, newId, opts = {}) {
|
|
13
|
+
const result = await renameProvider(id, newId, opts);
|
|
14
|
+
if (!opts.dryRun)
|
|
15
|
+
console.log(pc.green(t("rename.done", { oldId: id, newId })));
|
|
16
|
+
reportChanges(result, opts.dryRun);
|
|
17
|
+
}
|
|
18
|
+
export async function cmdRemoveProvider(id, opts = {}) {
|
|
19
|
+
const result = await removeProvider(id, opts);
|
|
20
|
+
if (!opts.dryRun) {
|
|
21
|
+
console.log(pc.green(opts.apps
|
|
22
|
+
? t("remove.localDone", { id, apps: opts.apps })
|
|
23
|
+
: t("remove.removed", { id })));
|
|
24
|
+
if (!opts.apps && !opts.prune)
|
|
25
|
+
console.log(pc.dim(t("remove.note")));
|
|
26
|
+
}
|
|
27
|
+
reportChanges(result, opts.dryRun);
|
|
28
|
+
}
|
|
29
|
+
export function cmdListLocalProviders(apps) {
|
|
30
|
+
const rows = listRemovableProviders(apps);
|
|
31
|
+
if (!rows.length) {
|
|
32
|
+
console.log(t("list.localNone"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const row of rows)
|
|
36
|
+
console.log(`${row.app.padEnd(12)} ${row.id}${row.name && row.name !== row.id ? ` · ${row.name}` : ""}`);
|
|
37
|
+
}
|