agentsw 0.10.1 → 0.11.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 +102 -29
- package/README.zh-CN.md +91 -27
- package/dist/commands.js +266 -55
- package/dist/config-transaction.js +22 -17
- package/dist/i18n.js +29 -13
- package/dist/index.js +10 -3
- package/dist/menu.js +39 -8
- package/dist/targets/claudecode.js +2 -0
- package/dist/targets/codex.js +5 -1
- package/dist/targets/dsh.js +3 -2
- package/dist/targets/omp.js +4 -21
- package/dist/targets/pistyle.js +10 -28
- package/dist/targets/wire.js +65 -0
- package/dist/targets/workbuddy.js +7 -3
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -17,7 +17,7 @@ import { SafeConfigError, drainPendingWrites, readTextIfExists, setDryRun } from
|
|
|
17
17
|
import { applyModelFilter } from "./filter.js";
|
|
18
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
19
19
|
import { t } from "./i18n.js";
|
|
20
|
-
import { cancelInteractive, emitJson, error, isJson, note, out, table } from "./ui.js";
|
|
20
|
+
import { cancelInteractive, emitJson, error, isJson, note, out, table, warn } from "./ui.js";
|
|
21
21
|
import { withProgress } from "./progress.js";
|
|
22
22
|
import { appsReport, listReport, modelsQueryReport, providerMetadataReport, providerModelsReport, statusReport, } from "./report.js";
|
|
23
23
|
/** Share successes and failures without fetching until a model actually needs the supplement. */
|
|
@@ -483,12 +483,15 @@ function reportResults(results) {
|
|
|
483
483
|
out(`${pc.yellow(t("common.skip"))} ${r.app.padEnd(9)} ${pc.dim(r.skipped)}`);
|
|
484
484
|
continue;
|
|
485
485
|
}
|
|
486
|
-
|
|
486
|
+
// An empty change list means the app was already in sync: say so rather than
|
|
487
|
+
// printing a bare "ok" with nothing after it.
|
|
488
|
+
const detail = r.changed.length ? r.changed.join(", ") : pc.dim(t("preview.unchanged"));
|
|
489
|
+
out(`${pc.green("ok ")} ${r.app.padEnd(9)} ${detail}`);
|
|
487
490
|
for (const note of r.notes)
|
|
488
491
|
out(` ${" ".repeat(9)} ${pc.dim(note)}`);
|
|
489
492
|
}
|
|
490
493
|
}
|
|
491
|
-
async function runTargets(op, provider, appsFilter,
|
|
494
|
+
async function runTargets(op, provider, appsFilter, opts = {}) {
|
|
492
495
|
const selected = resolveTargets(appsFilter);
|
|
493
496
|
const explicit = appsFilter !== undefined && appsFilter !== "all";
|
|
494
497
|
const results = [];
|
|
@@ -502,6 +505,18 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
502
505
|
});
|
|
503
506
|
continue;
|
|
504
507
|
}
|
|
508
|
+
if (op === "apply" && opts.oneSlot && target.singleProvider && provider.id !== opts.oneSlot.winners.get(target.id)) {
|
|
509
|
+
const winner = opts.oneSlot.winners.get(target.id);
|
|
510
|
+
results.push({
|
|
511
|
+
app: target.id,
|
|
512
|
+
changed: [],
|
|
513
|
+
notes: [],
|
|
514
|
+
skipped: winner === undefined
|
|
515
|
+
? `${target.name} holds one provider at a time; left as it is because this run does not say which`
|
|
516
|
+
: `${target.name} holds one provider at a time; written for ${winner}`,
|
|
517
|
+
});
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
505
520
|
try {
|
|
506
521
|
if (!explicit && !target.detect()) {
|
|
507
522
|
results.push({ app: target.id, changed: [], notes: [], skipped: `${target.name} not detected (pass --apps ${target.id} to force)` });
|
|
@@ -510,7 +525,7 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
510
525
|
results.push(await target[op](provider));
|
|
511
526
|
}
|
|
512
527
|
catch (err) {
|
|
513
|
-
const safe = redactErrors && !(err instanceof SafeConfigError);
|
|
528
|
+
const safe = opts.redactErrors === true && !(err instanceof SafeConfigError);
|
|
514
529
|
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${safe ? t("preview.failedSafely") : err.message}`) });
|
|
515
530
|
process.exitCode = 1;
|
|
516
531
|
}
|
|
@@ -629,20 +644,20 @@ function printFileDiff(file, next, apiKey) {
|
|
|
629
644
|
if (oldText === newText)
|
|
630
645
|
out(pc.dim(t("preview.onlyRedacted")));
|
|
631
646
|
}
|
|
632
|
-
async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
647
|
+
async function runWithOptionalDryRun(op, provider, apps, dryRun, oneSlot) {
|
|
633
648
|
const summarize = (results, files) => ({
|
|
634
649
|
apps: results.map((r) => r.app),
|
|
635
650
|
files,
|
|
636
651
|
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
637
652
|
});
|
|
638
653
|
if (!dryRun) {
|
|
639
|
-
const results = await runTargets(op, provider, apps);
|
|
654
|
+
const results = await runTargets(op, provider, apps, { oneSlot });
|
|
640
655
|
reportResults(results);
|
|
641
656
|
return summarize(results, results.flatMap((r) => (r.skipped ? [] : r.changed)));
|
|
642
657
|
}
|
|
643
658
|
setDryRun(true);
|
|
644
659
|
try {
|
|
645
|
-
const results = await runTargets(op, provider, apps, true);
|
|
660
|
+
const results = await runTargets(op, provider, apps, { redactErrors: true, oneSlot });
|
|
646
661
|
const writes = drainPendingWrites();
|
|
647
662
|
for (const r of results) {
|
|
648
663
|
if (r.skipped)
|
|
@@ -662,12 +677,25 @@ export async function cmdUse(id, opts) {
|
|
|
662
677
|
resolveTargets(opts.apps);
|
|
663
678
|
const store = loadStore();
|
|
664
679
|
const provider = getProvider(store, id);
|
|
665
|
-
|
|
666
|
-
if (!provider.models.some((m) => m.id ===
|
|
667
|
-
fail(t("use.modelMissing", { model
|
|
680
|
+
const requireConfigured = (model) => {
|
|
681
|
+
if (!provider.models.some((m) => m.id === model)) {
|
|
682
|
+
fail(t("use.modelMissing", { model, id, have: provider.models.map((m) => m.id).join(", ") }));
|
|
668
683
|
}
|
|
684
|
+
};
|
|
685
|
+
if (opts.model) {
|
|
686
|
+
requireConfigured(opts.model);
|
|
669
687
|
provider.defaultModel = opts.model;
|
|
670
688
|
}
|
|
689
|
+
// The slot agents reach for cheap background work — Claude Code's haiku variables. Leaving it
|
|
690
|
+
// unset means "same as the default model", so an expensive default silently becomes the
|
|
691
|
+
// background model too; `--no-small-model` returns the slot to that fallback.
|
|
692
|
+
if (typeof opts.smallModel === "string") {
|
|
693
|
+
requireConfigured(opts.smallModel);
|
|
694
|
+
provider.smallModel = opts.smallModel;
|
|
695
|
+
}
|
|
696
|
+
else if (opts.smallModel === false) {
|
|
697
|
+
delete provider.smallModel;
|
|
698
|
+
}
|
|
671
699
|
if (!opts.dryRun) {
|
|
672
700
|
store.active = id;
|
|
673
701
|
saveStore(store);
|
|
@@ -678,18 +706,189 @@ export async function cmdUse(id, opts) {
|
|
|
678
706
|
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome });
|
|
679
707
|
}
|
|
680
708
|
}
|
|
709
|
+
/**
|
|
710
|
+
* Shared by `discover` and `sync`: fetch the live model list, re-enrich from the catalog, persist.
|
|
711
|
+
* `pinned` ids survive a listing that dropped them, so they are not reported as "removed upstream".
|
|
712
|
+
* `save: false` keeps the refreshed list in memory for the caller to preview: a dry run shows what a
|
|
713
|
+
* real sync would write without leaving the fetched list behind in the store.
|
|
714
|
+
*/
|
|
715
|
+
async function refreshProviderModels(store, provider, save = true) {
|
|
716
|
+
const listed = await withProgress(t("add.discovering", { url: provider.baseUrl }), (progress) => discoverProviderModels(provider, {
|
|
717
|
+
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: provider.baseUrl, page, count })),
|
|
718
|
+
}));
|
|
719
|
+
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
720
|
+
const outcome = applyModelFilter(listed, provider.modelFilter, pinned);
|
|
721
|
+
reportDropped(outcome.dropped);
|
|
722
|
+
const ids = outcome.kept;
|
|
723
|
+
const retained = new Set([...ids, ...pinned]);
|
|
724
|
+
const known = provider.models.map((m) => m.id);
|
|
725
|
+
const added = ids.filter((m) => !known.includes(m));
|
|
726
|
+
const removed = known.filter((m) => !retained.has(m));
|
|
727
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
728
|
+
provider.models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, [...retained], { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) }));
|
|
729
|
+
// Reported by the caller, where it reads after the table or the per-app results instead of
|
|
730
|
+
// scrolling away above them.
|
|
731
|
+
const missing = [...new Set(pinned)].filter((model) => !ids.includes(model));
|
|
732
|
+
if (save)
|
|
733
|
+
saveStore(store);
|
|
734
|
+
return { ids, added, removed, missing, catalog };
|
|
735
|
+
}
|
|
736
|
+
function printModelRefresh(provider, refresh) {
|
|
737
|
+
out(`${pc.bold(provider.id)}: ${refresh.ids.length} models (${pc.green(`+${refresh.added.length}`)} / ${pc.red(`-${refresh.removed.length}`)})` +
|
|
738
|
+
(refresh.added.length ? `\n new: ${refresh.added.join(", ")}` : "") +
|
|
739
|
+
(refresh.removed.length ? `\n removed upstream: ${refresh.removed.join(", ")}` : ""));
|
|
740
|
+
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
741
|
+
printUncatalogedHint(provider.models, refresh.catalog, provider.modelsDevId);
|
|
742
|
+
}
|
|
743
|
+
/** Comma-separated ids, the same shape `--apps` takes. Repeats collapse; blanks are ignored. */
|
|
744
|
+
function splitProviderList(value) {
|
|
745
|
+
return [...new Set(value.split(",").map((id) => id.trim()).filter(Boolean))];
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* One provider selection for every command that acts on providers: `--all` takes every configured
|
|
749
|
+
* one, `--provider` takes a comma-separated list, and `fallback` says what no selection means —
|
|
750
|
+
* `"active"` for the commands that write (sync), `"all"` for the ones that only read and refresh.
|
|
751
|
+
* Every id is resolved up front so an unknown one fails before the first network call, never after
|
|
752
|
+
* half a run has been saved. An empty store is an error only when `--all` asked for it by name; the
|
|
753
|
+
* implicit all-providers fallback keeps reporting zero, as it did before there was a flag.
|
|
754
|
+
*/
|
|
755
|
+
function resolveProviderIds(store, opts, fallback) {
|
|
756
|
+
if (opts.all && opts.provider !== undefined)
|
|
757
|
+
fail(t("select.allAndProvider"));
|
|
758
|
+
let ids;
|
|
759
|
+
if (opts.all) {
|
|
760
|
+
ids = Object.keys(store.providers);
|
|
761
|
+
if (!ids.length)
|
|
762
|
+
fail(t("select.noProviders"));
|
|
763
|
+
}
|
|
764
|
+
else if (opts.provider !== undefined) {
|
|
765
|
+
ids = splitProviderList(opts.provider);
|
|
766
|
+
if (!ids.length)
|
|
767
|
+
fail(t("select.noneSelected"));
|
|
768
|
+
}
|
|
769
|
+
else if (fallback === "all") {
|
|
770
|
+
ids = Object.keys(store.providers);
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
if (!store.active)
|
|
774
|
+
fail(t("sync.noActive"));
|
|
775
|
+
ids = [store.active];
|
|
776
|
+
}
|
|
777
|
+
for (const id of ids)
|
|
778
|
+
getProvider(store, id);
|
|
779
|
+
return ids;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Apps that name a single provider are written once (see `RunTargetsOptions.oneSlot`); for the rest
|
|
783
|
+
* the last writer owns whatever "current" pointer the app keeps, so a multi-provider sync finishes
|
|
784
|
+
* on the active provider and leaves the agents where the store says they already are.
|
|
785
|
+
*/
|
|
786
|
+
function activeLast(store, ids) {
|
|
787
|
+
const active = store.active;
|
|
788
|
+
if (ids.length < 2 || !active || !ids.includes(active))
|
|
789
|
+
return ids;
|
|
790
|
+
return [...ids.filter((id) => id !== active), active];
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Which provider each single-slot app should receive. The active provider wins where the app can
|
|
794
|
+
* accept it; where it cannot — Claude Code takes anthropic endpoints only, and the active provider
|
|
795
|
+
* may well be an openai one — the single compatible provider in the run wins instead, because
|
|
796
|
+
* writing the only candidate takes nothing away. With several compatible candidates and no active
|
|
797
|
+
* provider among them the choice is the user's, so the app is left pointing where it already does.
|
|
798
|
+
*/
|
|
799
|
+
function oneSlotWinners(selected, store, ids) {
|
|
800
|
+
const winners = new Map();
|
|
801
|
+
for (const target of selected) {
|
|
802
|
+
if (!target.singleProvider)
|
|
803
|
+
continue;
|
|
804
|
+
const usable = ids.filter((id) => supportsProtocol(target, store.providers[id].protocol));
|
|
805
|
+
const winner = store.active !== undefined && usable.includes(store.active)
|
|
806
|
+
? store.active
|
|
807
|
+
: usable.length === 1 ? usable[0] : undefined;
|
|
808
|
+
if (winner !== undefined)
|
|
809
|
+
winners.set(target.id, winner);
|
|
810
|
+
}
|
|
811
|
+
return winners;
|
|
812
|
+
}
|
|
813
|
+
/** Section separator plus the tail line that says how many of several providers came through. */
|
|
814
|
+
function printMultiSummary(count, failed, key) {
|
|
815
|
+
const done = count - failed;
|
|
816
|
+
out(`\n${failed
|
|
817
|
+
? pc.yellow(t(key === "sync" ? "sync.summaryFailed" : "discover.summaryFailed", { ok: done, count }))
|
|
818
|
+
: t(key === "sync" ? "sync.summaryOk" : "discover.summaryOk", { count })}`);
|
|
819
|
+
}
|
|
681
820
|
export async function cmdSync(opts) {
|
|
682
|
-
resolveTargets(opts.apps);
|
|
821
|
+
const selectedTargets = resolveTargets(opts.apps);
|
|
683
822
|
const store = loadStore();
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
823
|
+
const ids = activeLast(store, resolveProviderIds(store, opts, "active"));
|
|
824
|
+
const many = ids.length > 1;
|
|
825
|
+
const oneSlot = many ? { winners: oneSlotWinners(selectedTargets, store, ids) } : undefined;
|
|
826
|
+
const saveRefresh = opts.dryRun !== true;
|
|
827
|
+
const reports = [];
|
|
828
|
+
let failed = 0;
|
|
829
|
+
for (const [index, id] of ids.entries()) {
|
|
830
|
+
const provider = getProvider(store, id);
|
|
831
|
+
if (index)
|
|
832
|
+
out("");
|
|
833
|
+
let refresh;
|
|
834
|
+
if (opts.refresh !== false) {
|
|
835
|
+
try {
|
|
836
|
+
refresh = await refreshProviderModels(store, provider, saveRefresh);
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
// One unreachable provider must not strand the others: report it, write nothing for it,
|
|
840
|
+
// and fail the run. A single-provider sync has nothing else to do, so it still throws.
|
|
841
|
+
if (!many)
|
|
842
|
+
throw new Error(`${err.message}\n${t("sync.refreshFailed")}`);
|
|
843
|
+
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
844
|
+
process.exitCode = 1;
|
|
845
|
+
failed += 1;
|
|
846
|
+
reports.push({ provider: id, refreshFailed: true });
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (!isJson()) {
|
|
850
|
+
printModelRefresh(provider, refresh);
|
|
851
|
+
// --dry-run promises to write nothing, so say the fetched list only lived long enough to preview.
|
|
852
|
+
if (!saveRefresh)
|
|
853
|
+
note(t("sync.refreshNotSaved"));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
out(`${t("sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
|
|
857
|
+
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun, oneSlot);
|
|
858
|
+
if (refresh?.missing.length) {
|
|
859
|
+
const models = refresh.missing.join(", ");
|
|
860
|
+
warn(t(opts.dryRun ? "refresh.pinnedMissing" : "sync.pinnedMissingWritten", { models, id }));
|
|
861
|
+
}
|
|
862
|
+
reports.push({
|
|
863
|
+
provider: id,
|
|
864
|
+
model: provider.defaultModel,
|
|
865
|
+
...(refresh
|
|
866
|
+
? {
|
|
867
|
+
refreshed: {
|
|
868
|
+
models: refresh.ids.length,
|
|
869
|
+
added: refresh.added,
|
|
870
|
+
removed: refresh.removed,
|
|
871
|
+
missing: refresh.missing,
|
|
872
|
+
saved: saveRefresh,
|
|
873
|
+
},
|
|
874
|
+
}
|
|
875
|
+
: {}),
|
|
876
|
+
...outcome,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
690
879
|
if (isJson()) {
|
|
691
|
-
|
|
880
|
+
const first = reports[0] ?? {};
|
|
881
|
+
emitJson(many
|
|
882
|
+
? {
|
|
883
|
+
dryRun: opts.dryRun === true,
|
|
884
|
+
providers: reports,
|
|
885
|
+
files: [...new Set(reports.flatMap((report) => report.files ?? []))],
|
|
886
|
+
}
|
|
887
|
+
: { ...first, dryRun: opts.dryRun === true });
|
|
888
|
+
return;
|
|
692
889
|
}
|
|
890
|
+
if (many)
|
|
891
|
+
printMultiSummary(ids.length, failed, "sync");
|
|
693
892
|
}
|
|
694
893
|
export function cmdStatus() {
|
|
695
894
|
const store = loadStore();
|
|
@@ -767,7 +966,8 @@ export async function cmdModels(query, opts) {
|
|
|
767
966
|
}
|
|
768
967
|
export async function cmdRefreshMeta(opts = {}) {
|
|
769
968
|
const store = loadStore();
|
|
770
|
-
|
|
969
|
+
// Omitting the selection still means every provider: this command only reads catalogs.
|
|
970
|
+
const providers = resolveProviderIds(store, opts, "all").map((id) => getProvider(store, id));
|
|
771
971
|
resolveMetadataOptions(opts);
|
|
772
972
|
for (const provider of providers)
|
|
773
973
|
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
@@ -794,50 +994,61 @@ export async function cmdRefreshMeta(opts = {}) {
|
|
|
794
994
|
for (const provider of providers)
|
|
795
995
|
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
796
996
|
}
|
|
797
|
-
export async function cmdDiscover(
|
|
997
|
+
export async function cmdDiscover(ids, opts) {
|
|
798
998
|
resolveTargets(opts.apps);
|
|
799
999
|
const store = loadStore();
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1000
|
+
// Ids may arrive as positional arguments, as one comma-separated list, or through `-p` for
|
|
1001
|
+
// symmetry with sync; all three say the same thing, so accept them together.
|
|
1002
|
+
const listed = [...(Array.isArray(ids) ? ids : [ids ?? ""]), opts.provider ?? ""].filter(Boolean).join(",");
|
|
1003
|
+
const selected = resolveProviderIds(store, { provider: listed || undefined, all: opts.all }, "active");
|
|
1004
|
+
// Reject bad metadata options once, before the first provider is contacted.
|
|
1005
|
+
resolveMetadataOptions(opts);
|
|
1006
|
+
const many = selected.length > 1;
|
|
1007
|
+
const reports = [];
|
|
1008
|
+
const refreshed = [];
|
|
1009
|
+
for (const [index, id] of selected.entries()) {
|
|
1010
|
+
const provider = getProvider(store, id);
|
|
1011
|
+
if (index)
|
|
1012
|
+
out("");
|
|
1013
|
+
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
1014
|
+
// flags override and re-persist the filter; --no-filter clears it
|
|
1015
|
+
const flagFilter = parseFilterOpts(opts, provider.modelFilter);
|
|
1016
|
+
if (opts.filter === false)
|
|
1017
|
+
provider.modelFilter = undefined;
|
|
1018
|
+
else if (flagFilter)
|
|
1019
|
+
provider.modelFilter = flagFilter;
|
|
1020
|
+
let refresh;
|
|
1021
|
+
try {
|
|
1022
|
+
refresh = await refreshProviderModels(store, provider);
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
// One unreachable endpoint must not strand the others; a single provider still throws.
|
|
1026
|
+
if (!many)
|
|
1027
|
+
throw err;
|
|
1028
|
+
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
1029
|
+
process.exitCode = 1;
|
|
1030
|
+
reports.push({ provider: id, refreshFailed: true });
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
refreshed.push(id);
|
|
1034
|
+
reports.push({ provider: id, models: refresh.ids.length, added: refresh.added, removed: refresh.removed, missing: refresh.missing });
|
|
1035
|
+
if (!isJson())
|
|
1036
|
+
printModelRefresh(provider, refresh);
|
|
1037
|
+
if (refresh.missing.length)
|
|
1038
|
+
warn(t("refresh.pinnedMissing", { models: refresh.missing.join(", "), id }));
|
|
825
1039
|
}
|
|
826
|
-
saveStore(store);
|
|
827
1040
|
if (isJson()) {
|
|
828
|
-
emitJson(
|
|
1041
|
+
emitJson(many ? { providers: reports } : (reports[0] ?? {}));
|
|
829
1042
|
return;
|
|
830
1043
|
}
|
|
831
|
-
|
|
832
|
-
(
|
|
833
|
-
|
|
834
|
-
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
835
|
-
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
836
|
-
if (opts.sync) {
|
|
1044
|
+
if (many)
|
|
1045
|
+
printMultiSummary(selected.length, selected.length - refreshed.length, "discover");
|
|
1046
|
+
if (opts.sync && refreshed.length) {
|
|
837
1047
|
out("");
|
|
838
|
-
|
|
1048
|
+
// The lists are already fresh: write them without a second fetch.
|
|
1049
|
+
await cmdSync({ provider: refreshed.join(","), apps: opts.apps, refresh: false });
|
|
839
1050
|
}
|
|
840
|
-
else {
|
|
1051
|
+
else if (!opts.sync) {
|
|
841
1052
|
note(`\n${t("discover.next")}`);
|
|
842
1053
|
}
|
|
843
1054
|
}
|
|
@@ -144,26 +144,31 @@ export function commitFileChanges(changes, opts = {}) {
|
|
|
144
144
|
return withWriteLock(() => {
|
|
145
145
|
// Another writer may have committed between planning and acquiring the lock.
|
|
146
146
|
validateReads();
|
|
147
|
+
// Only a file that already existed has anything to save. When every planned
|
|
148
|
+
// file is new, no transaction directory is created: an empty one (nothing but
|
|
149
|
+
// its manifest) would read as a backup that does not exist.
|
|
147
150
|
let backupDir;
|
|
148
151
|
let currentFile = backupsDir;
|
|
149
152
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
if (plan.some((change) => change.before !== undefined)) {
|
|
154
|
+
fs.mkdirSync(backupsDir, { recursive: true, mode: 0o700 });
|
|
155
|
+
backupDir = fs.mkdtempSync(path.join(backupsDir, "transaction-"));
|
|
156
|
+
fs.chmodSync(backupDir, 0o700);
|
|
157
|
+
for (const [index, change] of plan.entries()) {
|
|
158
|
+
currentFile = change.file;
|
|
159
|
+
if (change.before === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
const backup = path.join(backupDir, `${index}-${path.basename(change.file)}`);
|
|
162
|
+
fs.writeFileSync(backup, change.before, { flag: "wx", mode: 0o600 });
|
|
163
|
+
fs.chmodSync(backup, 0o600);
|
|
164
|
+
}
|
|
165
|
+
fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify(plan.map((change, index) => ({
|
|
166
|
+
file: change.file,
|
|
167
|
+
backup: change.before === undefined ? null : `${index}-${path.basename(change.file)}`,
|
|
168
|
+
mode: change.original.mode,
|
|
169
|
+
})), null, 2) + "\n", { flag: "wx", mode: 0o600 });
|
|
170
|
+
fs.chmodSync(path.join(backupDir, "manifest.json"), 0o600);
|
|
160
171
|
}
|
|
161
|
-
fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify(plan.map((change, index) => ({
|
|
162
|
-
file: change.file,
|
|
163
|
-
backup: change.before === undefined ? null : `${index}-${path.basename(change.file)}`,
|
|
164
|
-
mode: change.original.mode,
|
|
165
|
-
})), null, 2) + "\n", { flag: "wx", mode: 0o600 });
|
|
166
|
-
fs.chmodSync(path.join(backupDir, "manifest.json"), 0o600);
|
|
167
172
|
validateReads();
|
|
168
173
|
}
|
|
169
174
|
catch {
|
|
@@ -216,7 +221,7 @@ export function commitFileChanges(changes, opts = {}) {
|
|
|
216
221
|
}
|
|
217
222
|
catch { /* Only remove empty directories created by this transaction. */ }
|
|
218
223
|
}
|
|
219
|
-
throw new Error(`${currentFile}: configuration transaction failed; ${failed.length ? `rollback incomplete for ${failed.join(", ")}` : "previous writes rolled back"}
|
|
224
|
+
throw new Error(`${currentFile}: configuration transaction failed; ${failed.length ? `rollback incomplete for ${failed.join(", ")}` : "previous writes rolled back"}${backupDir ? `; backups: ${backupDir}` : "; no files needed backing up"}`);
|
|
220
225
|
}
|
|
221
226
|
return { files, backupDir };
|
|
222
227
|
});
|
package/dist/i18n.js
CHANGED
|
@@ -14,10 +14,10 @@ const messages = {
|
|
|
14
14
|
"cmd.prune": { en: "remove a provider's entries from app configs (keeps it in agentsw)", "zh-CN": "从应用配置移除供应商条目(仍保留在 agentsw)" },
|
|
15
15
|
"cmd.rename": { en: "rename a provider and migrate config references with backups", "zh-CN": "重命名供应商并迁移配置引用(自动备份)" },
|
|
16
16
|
"cmd.use": { en: "set the active provider and write it into app configs", "zh-CN": "设为当前供应商并写入应用配置" },
|
|
17
|
-
"cmd.sync": { en: "re-apply
|
|
17
|
+
"cmd.sync": { en: "refresh a provider's model list and re-apply it to app configs (--no-refresh writes the saved list)", "zh-CN": "刷新供应商模型列表并重新应用到应用配置(--no-refresh 只写入已保存的列表)" },
|
|
18
18
|
"cmd.status": { en: "show detected apps and what they currently point at", "zh-CN": "显示检测到的应用及当前供应商" },
|
|
19
19
|
"cmd.models": { en: "search models.dev, or show a configured provider's models", "zh-CN": "搜索 models.dev 或显示已配置供应商的模型" },
|
|
20
|
-
"cmd.discover": { en: "refresh
|
|
20
|
+
"cmd.discover": { en: "refresh model lists and configured metadata sources for one or more providers", "zh-CN": "刷新一个或多个供应商的模型列表,并通过已启用的目录补全参数" },
|
|
21
21
|
"cmd.import": { en: "scan app configs, preview and dedupe existing custom providers, then import", "zh-CN": "扫描应用配置,预览并去重已有自定义供应商后导入" },
|
|
22
22
|
"cmd.apps": { en: "check installed agents: current version vs latest", "zh-CN": "检查智能体已安装版本与最新版本" },
|
|
23
23
|
"cmd.install": { en: "install an agent CLI", "zh-CN": "安装智能体 CLI" },
|
|
@@ -35,6 +35,7 @@ const messages = {
|
|
|
35
35
|
"opt.models": { en: "comma-separated model ids", "zh-CN": "逗号分隔的模型 id" },
|
|
36
36
|
"opt.defaultModel": { en: "default model (defaults to first)", "zh-CN": "默认模型(默认取第一个)" },
|
|
37
37
|
"opt.smallModel": { en: "small/fast model (Claude Code haiku slot)", "zh-CN": "小型/快速模型(Claude Code haiku 槽位)" },
|
|
38
|
+
"opt.noSmallModel": { en: "clear the small/fast model so the default model fills that slot", "zh-CN": "清除小型/快速模型,该槽位改用默认模型" },
|
|
38
39
|
"opt.reasoning": { en: "preferred reasoning effort (codex): minimal|low|medium|high", "zh-CN": "Codex 首选推理等级: minimal|low|medium|high" },
|
|
39
40
|
"opt.discover": { en: "list model ids from the provider's /v1/models", "zh-CN": "从供应商 /v1/models 获取模型 id" },
|
|
40
41
|
"opt.include": { en: "keep only models matching comma-separated globs", "zh-CN": "仅保留匹配逗号分隔 glob 的模型" },
|
|
@@ -47,7 +48,7 @@ const messages = {
|
|
|
47
48
|
"opt.noGatewayMetadata": { en: "disable Gateway fetching; keep saved parameters (same as --metadata-mode off)", "zh-CN": "关闭 Gateway 获取,保留已保存参数(等同 --metadata-mode off)" },
|
|
48
49
|
"opt.gatewayModels": { en: 'replace exact aliases as JSON: {"local-id":"creator/model"}; {} clears them', "zh-CN": '用 JSON 替换精确映射:{"本地模型ID":"creator/model"};{} 清空' },
|
|
49
50
|
"opt.metadataDetails": { en: "show field sources, conflicts and Gateway reference pricing as JSON (requires --provider)", "zh-CN": "以 JSON 查看字段来源、冲突和 Gateway 参考价格(需 --provider)" },
|
|
50
|
-
"opt.refreshProvider": { en: "refresh/configure only
|
|
51
|
+
"opt.refreshProvider": { en: "refresh/configure only these saved providers, comma-separated (default: all)", "zh-CN": "仅刷新和设置这些供应商,逗号分隔(默认全部)" },
|
|
51
52
|
"opt.yes": { en: "non-interactive; require all flags", "zh-CN": "非交互模式;要求提供全部参数" },
|
|
52
53
|
"opt.prune": { en: "also remove entries from app configs", "zh-CN": "同时从应用配置中移除条目" },
|
|
53
54
|
"opt.apps": { en: "comma-separated apps or 'all'", "zh-CN": "逗号分隔的应用或 'all'" },
|
|
@@ -56,9 +57,12 @@ const messages = {
|
|
|
56
57
|
"opt.appsDetailed": { en: "comma-separated apps (claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh) or 'all'", "zh-CN": "逗号分隔的应用(claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh)或 'all'" },
|
|
57
58
|
"opt.model": { en: "override default model while switching", "zh-CN": "切换时覆盖默认模型" },
|
|
58
59
|
"opt.dryRun": { en: "preview config diff without writing", "zh-CN": "仅预览配置差异,不写入" },
|
|
59
|
-
"opt.provider": { en: "sync
|
|
60
|
+
"opt.provider": { en: "sync these providers instead of the active one (comma-separated)", "zh-CN": "同步指定供应商而非当前供应商(逗号分隔)" },
|
|
61
|
+
"opt.syncAll": { en: "sync every configured provider", "zh-CN": "同步全部已配置的供应商" },
|
|
62
|
+
"opt.refreshAll": { en: "refresh every configured provider", "zh-CN": "刷新全部已配置的供应商" },
|
|
60
63
|
"opt.showProvider": { en: "show models of a configured provider", "zh-CN": "显示已配置供应商的模型" },
|
|
61
64
|
"opt.refresh": { en: "force-refresh models.dev cache", "zh-CN": "强制刷新 models.dev 缓存" },
|
|
65
|
+
"opt.noRefresh": { en: "skip the model-list refresh; write the saved list only", "zh-CN": "跳过模型列表刷新;只写入已保存的列表" },
|
|
62
66
|
"opt.limit": { en: "maximum results", "zh-CN": "最大结果数" },
|
|
63
67
|
"opt.syncAfter": { en: "push refreshed provider into app configs", "zh-CN": "刷新后推送到应用配置" },
|
|
64
68
|
"opt.appsSync": { en: "apps to sync when --sync is set", "zh-CN": "--sync 时要同步的应用" },
|
|
@@ -80,8 +84,7 @@ const messages = {
|
|
|
80
84
|
"menu.use": { en: "Switch provider and default model", "zh-CN": "切换供应商和默认模型" },
|
|
81
85
|
"menu.status": { en: "View each agent's current configuration", "zh-CN": "查看各智能体当前配置" },
|
|
82
86
|
"menu.list": { en: "View providers saved in agentsw", "zh-CN": "查看 agentsw 供应商列表" },
|
|
83
|
-
"menu.sync": { en: "
|
|
84
|
-
"menu.discover": { en: "Update a provider's model list", "zh-CN": "更新供应商模型列表" },
|
|
87
|
+
"menu.sync": { en: "Sync providers", "zh-CN": "同步供应商" },
|
|
85
88
|
"menu.metadata": { en: "Configure and refresh model metadata", "zh-CN": "设置并刷新模型参数补充源" },
|
|
86
89
|
"menu.metadataHelp": { en: "Keep the model list; choose automatic, always-on or disabled AI Gateway lookup, then refresh saved parameters only.", "zh-CN": "保留模型列表,选择 AI Gateway 自动按需、始终补充或关闭,仅刷新保存的参数。" },
|
|
87
90
|
"menu.metadataProvider": { en: "which provider's metadata settings should be updated?", "zh-CN": "选择要设置模型参数补充源的供应商" },
|
|
@@ -128,8 +131,8 @@ const messages = {
|
|
|
128
131
|
"menu.appsAll": { en: "all {count} detected apps", "zh-CN": "全部 {count} 个检测到的应用" },
|
|
129
132
|
"menu.appNotDetected": { en: "(not detected)", "zh-CN": "(未检测到)" },
|
|
130
133
|
"menu.appsHint": { en: "↑/↓ move · Space toggle · Enter confirm", "zh-CN": "↑/↓ 移动 · 空格切换 · Enter 确认" },
|
|
131
|
-
"menu.
|
|
132
|
-
"menu.
|
|
134
|
+
"menu.syncFor": { en: "which providers should be synced?", "zh-CN": "选择要同步的供应商(可多选)" },
|
|
135
|
+
"menu.providersAll": { en: "all {count} configured providers", "zh-CN": "全部 {count} 个已配置供应商" },
|
|
133
136
|
"menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
|
|
134
137
|
"menu.upgrade": { en: "upgrade all installed agents that have updates?", "zh-CN": "是否升级所有有新版本的已安装智能体?" },
|
|
135
138
|
"menu.installApp": { en: "Install a new coding agent", "zh-CN": "安装新的智能体" },
|
|
@@ -205,8 +208,7 @@ const messages = {
|
|
|
205
208
|
"menu.useHelp": { en: "Select a provider and default model, then write them to compatible agent configs.", "zh-CN": "选择供应商和默认模型,然后写入支持该协议的智能体配置。" },
|
|
206
209
|
"menu.statusHelp": { en: "Show detected agents, their current providers and config file locations.", "zh-CN": "查看检测到的智能体、所用供应商及配置文件位置。" },
|
|
207
210
|
"menu.listHelp": { en: "Show only the providers, protocols and default models saved in agentsw.", "zh-CN": "只查看 agentsw 已保存的供应商、协议和默认模型。" },
|
|
208
|
-
"menu.syncHelp": { en: "
|
|
209
|
-
"menu.discoverHelp": { en: "Fetch models from the provider, fill in model metadata, and optionally sync to agents.", "zh-CN": "从供应商接口重新获取模型并补全模型参数,可选择同步到智能体。" },
|
|
211
|
+
"menu.syncHelp": { en: "Fetch each selected provider's current model list, fill in saved parameters, then write them to the selected agent configs.", "zh-CN": "依次获取所选供应商的最新模型列表并补全保存的参数,然后写入所选智能体配置。" },
|
|
210
212
|
"menu.renameHelp": { en: "Change the config ID and update references; keep custom display names.", "zh-CN": "修改配置中的标识并更新关联引用,保留自定义显示名称。" },
|
|
211
213
|
"menu.removeHelp": { en: "Choose where to delete: agentsw only, one agent only, or both.", "zh-CN": "先选择删除范围:仅 agentsw、仅某个智能体,或两处一起。" },
|
|
212
214
|
"menu.appsHelp": { en: "Compare installed and latest versions, then choose whether to upgrade.", "zh-CN": "对比已安装版本和最新版本,再选择是否升级。" },
|
|
@@ -241,6 +243,14 @@ const messages = {
|
|
|
241
243
|
"use.switching": { en: "switching to {id} ({protocol}) · default model {model}", "zh-CN": "正在切换到 {id} ({protocol})· 默认模型 {model}" },
|
|
242
244
|
"sync.noActive": { en: "no active provider; run agentsw use <id> first", "zh-CN": "没有当前供应商;请先运行 agentsw use <id>" },
|
|
243
245
|
"sync.syncing": { en: "syncing provider {id} · default model {model}", "zh-CN": "正在同步供应商 {id} · 默认模型 {model}" },
|
|
246
|
+
"select.allAndProvider": { en: "--all cannot be combined with a provider selection", "zh-CN": "--all 不能与指定的供应商同时使用" },
|
|
247
|
+
"select.noProviders": { en: "no providers configured; run agentsw add first", "zh-CN": "尚未配置任何供应商;请先运行 agentsw add" },
|
|
248
|
+
"select.noneSelected": { en: "the provider selection is empty; name at least one id", "zh-CN": "未指定任何供应商,至少需要一个 id" },
|
|
249
|
+
"sync.refreshSkipped": { en: "skipping {id}: {message}", "zh-CN": "已跳过 {id}:{message}" },
|
|
250
|
+
"sync.summaryOk": { en: "synced {count} providers", "zh-CN": "已同步 {count} 个供应商" },
|
|
251
|
+
"sync.summaryFailed": { en: "synced {ok} of {count} providers; the rest were skipped", "zh-CN": "已同步 {count} 个中的 {ok} 个供应商,其余已跳过" },
|
|
252
|
+
"sync.refreshNotSaved": { en: "dry run: the refreshed model list was not saved", "zh-CN": "试运行:刷新到的模型列表未保存" },
|
|
253
|
+
"sync.refreshFailed": { en: "could not refresh the model list; pass --no-refresh to write the saved list instead", "zh-CN": "无法刷新模型列表;可以加 --no-refresh 只写入已保存的列表" },
|
|
244
254
|
"status.config": { en: "config: {file}", "zh-CN": "配置:{file}" },
|
|
245
255
|
"status.active": { en: "active provider: {id}", "zh-CN": "当前供应商:{id}" },
|
|
246
256
|
"status.none": { en: "(none)", "zh-CN": "(无)" },
|
|
@@ -375,9 +385,15 @@ const messages = {
|
|
|
375
385
|
},
|
|
376
386
|
"upgrade.done": { en: "{id} -> {version}", "zh-CN": "{id} -> {version}" },
|
|
377
387
|
"upgrade.failed": { en: "{id} upgrade failed: {message}", "zh-CN": "{id} 升级失败:{message}" },
|
|
378
|
-
"discover.
|
|
379
|
-
|
|
380
|
-
|
|
388
|
+
"discover.summaryOk": { en: "refreshed {count} providers", "zh-CN": "已刷新 {count} 个供应商" },
|
|
389
|
+
"discover.summaryFailed": { en: "refreshed {ok} of {count} providers; the rest were skipped", "zh-CN": "已刷新 {count} 个中的 {ok} 个供应商,其余已跳过" },
|
|
390
|
+
"refresh.pinnedMissing": {
|
|
391
|
+
en: "{models} no longer listed by the endpoint; kept as configured — pick a listed model with: agentsw use {id} -m <model>",
|
|
392
|
+
"zh-CN": "{models} 已不在端点的模型列表中;仍按原配置保留 —— 可用 agentsw use {id} -m <model> 换成列表中的模型",
|
|
393
|
+
},
|
|
394
|
+
"sync.pinnedMissingWritten": {
|
|
395
|
+
en: "{models} no longer listed by the endpoint but was written to the agents anyway; pick a listed model with: agentsw use {id} -m <model>",
|
|
396
|
+
"zh-CN": "{models} 已不在端点的模型列表中,但仍写入了各智能体;可用 agentsw use {id} -m <model> 换成列表中的模型",
|
|
381
397
|
},
|
|
382
398
|
"discover.next": {
|
|
383
399
|
en: "run `agentsw sync` to push into app configs",
|
package/dist/index.js
CHANGED
|
@@ -133,14 +133,18 @@ program
|
|
|
133
133
|
.description(t("cmd.use"))
|
|
134
134
|
.option("-a, --apps <apps>", t("opt.appsDetailed"))
|
|
135
135
|
.option("-m, --model <id>", t("opt.model"))
|
|
136
|
+
.option("--small-model <id>", t("opt.smallModel"))
|
|
137
|
+
.option("--no-small-model", t("opt.noSmallModel"))
|
|
136
138
|
.option("-n, --dry-run", t("opt.dryRun"))
|
|
137
139
|
.action(cmdUse);
|
|
138
140
|
program
|
|
139
141
|
.command("sync")
|
|
140
142
|
.description(t("cmd.sync"))
|
|
141
143
|
.option("-a, --apps <apps>", t("opt.apps"))
|
|
142
|
-
.option("-p, --provider <
|
|
144
|
+
.option("-p, --provider <ids>", t("opt.provider"))
|
|
145
|
+
.option("--all", t("opt.syncAll"))
|
|
143
146
|
.option("-n, --dry-run", t("opt.dryRun"))
|
|
147
|
+
.option("--no-refresh", t("opt.noRefresh"))
|
|
144
148
|
.action(cmdSync);
|
|
145
149
|
program.command("status").description(t("cmd.status")).action(cmdStatus);
|
|
146
150
|
program
|
|
@@ -152,8 +156,10 @@ program
|
|
|
152
156
|
.option("-l, --limit <n>", t("opt.limit"), "30")
|
|
153
157
|
.action(cmdModels);
|
|
154
158
|
program
|
|
155
|
-
.command("discover
|
|
159
|
+
.command("discover [ids...]")
|
|
156
160
|
.description(t("cmd.discover"))
|
|
161
|
+
.option("-p, --provider <ids>", t("opt.refreshProvider"))
|
|
162
|
+
.option("--all", t("opt.refreshAll"))
|
|
157
163
|
.option("-s, --sync", t("opt.syncAfter"))
|
|
158
164
|
.option("-a, --apps <apps>", t("opt.appsSync"))
|
|
159
165
|
.option("--include <globs>", t("opt.setInclude"))
|
|
@@ -181,7 +187,8 @@ program.command("upgrade [apps...]").description(t("cmd.upgrade")).action(cmdUpg
|
|
|
181
187
|
program
|
|
182
188
|
.command("refresh")
|
|
183
189
|
.description(t("cmd.refresh"))
|
|
184
|
-
.option("-p, --provider <
|
|
190
|
+
.option("-p, --provider <ids>", t("opt.refreshProvider"))
|
|
191
|
+
.option("--all", t("opt.refreshAll"))
|
|
185
192
|
.option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
|
|
186
193
|
.option("--gateway-metadata", t("opt.gatewayMetadata"))
|
|
187
194
|
.option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
|