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/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, cmdRemove, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
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
- const picked = await pickProvider(t("menu.removeProvider"));
147
- if (!picked)
148
- continue;
149
- if (!(await askToggle(t("menu.reallyRemove", { id: pc.bold(picked.id) }))))
150
- continue;
151
- const prune = await askToggle(t("menu.pruneConfigs"));
152
- await cmdRemove(picked.id, { prune });
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
+ }