@slkiser/opencode-quota 3.11.1 → 4.0.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 +103 -57
- package/dist/bin/opencode-quota.d.ts.map +1 -1
- package/dist/bin/opencode-quota.js +24 -7
- package/dist/bin/opencode-quota.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/anthropic.d.ts.map +1 -1
- package/dist/lib/anthropic.js +106 -3
- package/dist/lib/anthropic.js.map +1 -1
- package/dist/lib/atomic-json.d.ts +1 -0
- package/dist/lib/atomic-json.d.ts.map +1 -1
- package/dist/lib/atomic-json.js +5 -2
- package/dist/lib/atomic-json.js.map +1 -1
- package/dist/lib/cli-show.d.ts.map +1 -1
- package/dist/lib/cli-show.js +11 -7
- package/dist/lib/cli-show.js.map +1 -1
- package/dist/lib/command-surfaces.d.ts +10 -0
- package/dist/lib/command-surfaces.d.ts.map +1 -0
- package/dist/lib/command-surfaces.js +9 -0
- package/dist/lib/command-surfaces.js.map +1 -0
- package/dist/lib/config-file-utils.d.ts +5 -0
- package/dist/lib/config-file-utils.d.ts.map +1 -1
- package/dist/lib/config-file-utils.js +20 -3
- package/dist/lib/config-file-utils.js.map +1 -1
- package/dist/lib/config.d.ts +5 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +124 -32
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts +19 -12
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +462 -566
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/display-sanitize.d.ts.map +1 -1
- package/dist/lib/display-sanitize.js +13 -0
- package/dist/lib/display-sanitize.js.map +1 -1
- package/dist/lib/entries.d.ts +53 -4
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/entries.js.map +1 -1
- package/dist/lib/init-installer.d.ts +11 -1
- package/dist/lib/init-installer.d.ts.map +1 -1
- package/dist/lib/init-installer.js +542 -154
- package/dist/lib/init-installer.js.map +1 -1
- package/dist/lib/openai.d.ts +9 -12
- package/dist/lib/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +69 -40
- package/dist/lib/openai.js.map +1 -1
- package/dist/lib/opencode-config-editor.d.ts +53 -0
- package/dist/lib/opencode-config-editor.d.ts.map +1 -0
- package/dist/lib/opencode-config-editor.js +274 -0
- package/dist/lib/opencode-config-editor.js.map +1 -0
- package/dist/lib/opencode-config-providers.d.ts +18 -0
- package/dist/lib/opencode-config-providers.d.ts.map +1 -1
- package/dist/lib/opencode-config-providers.js +75 -4
- package/dist/lib/opencode-config-providers.js.map +1 -1
- package/dist/lib/opencode-runtime-paths.d.ts.map +1 -1
- package/dist/lib/opencode-runtime-paths.js +10 -3
- package/dist/lib/opencode-runtime-paths.js.map +1 -1
- package/dist/lib/opencode-storage.d.ts +10 -0
- package/dist/lib/opencode-storage.d.ts.map +1 -1
- package/dist/lib/opencode-storage.js +75 -0
- package/dist/lib/opencode-storage.js.map +1 -1
- package/dist/lib/provider-add-command.d.ts +35 -0
- package/dist/lib/provider-add-command.d.ts.map +1 -0
- package/dist/lib/provider-add-command.js +185 -0
- package/dist/lib/provider-add-command.js.map +1 -0
- package/dist/lib/provider-add.d.ts +21 -0
- package/dist/lib/provider-add.d.ts.map +1 -0
- package/dist/lib/provider-add.js +210 -0
- package/dist/lib/provider-add.js.map +1 -0
- package/dist/lib/provider-metadata.d.ts +1 -1
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +11 -5
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/quota-command-format.d.ts +2 -0
- package/dist/lib/quota-command-format.d.ts.map +1 -1
- package/dist/lib/quota-command-format.js +94 -26
- package/dist/lib/quota-command-format.js.map +1 -1
- package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
- package/dist/lib/quota-dialog-commands.js +8 -4
- package/dist/lib/quota-dialog-commands.js.map +1 -1
- package/dist/lib/quota-export-types.d.ts +48 -27
- package/dist/lib/quota-export-types.d.ts.map +1 -1
- package/dist/lib/quota-export-types.js +0 -7
- package/dist/lib/quota-export-types.js.map +1 -1
- package/dist/lib/quota-export.d.ts.map +1 -1
- package/dist/lib/quota-export.js +63 -16
- package/dist/lib/quota-export.js.map +1 -1
- package/dist/lib/quota-providers-local.d.ts +55 -0
- package/dist/lib/quota-providers-local.d.ts.map +1 -0
- package/dist/lib/quota-providers-local.js +313 -0
- package/dist/lib/quota-providers-local.js.map +1 -0
- package/dist/lib/quota-providers-remote.d.ts +23 -0
- package/dist/lib/quota-providers-remote.d.ts.map +1 -0
- package/dist/lib/quota-providers-remote.js +318 -0
- package/dist/lib/quota-providers-remote.js.map +1 -0
- package/dist/lib/quota-providers.d.ts +77 -0
- package/dist/lib/quota-providers.d.ts.map +1 -0
- package/dist/lib/quota-providers.js +630 -0
- package/dist/lib/quota-providers.js.map +1 -0
- package/dist/lib/quota-render-data.d.ts +1 -0
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +35 -16
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-runtime-context.d.ts.map +1 -1
- package/dist/lib/quota-runtime-context.js +3 -4
- package/dist/lib/quota-runtime-context.js.map +1 -1
- package/dist/lib/quota-state.d.ts +5 -2
- package/dist/lib/quota-state.d.ts.map +1 -1
- package/dist/lib/quota-state.js +268 -51
- package/dist/lib/quota-state.js.map +1 -1
- package/dist/lib/quota-status.d.ts +2 -1
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +247 -69
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/qwen-local-quota.d.ts +12 -11
- package/dist/lib/qwen-local-quota.d.ts.map +1 -1
- package/dist/lib/qwen-local-quota.js +82 -87
- package/dist/lib/qwen-local-quota.js.map +1 -1
- package/dist/lib/scoped-update.d.ts +63 -0
- package/dist/lib/scoped-update.d.ts.map +1 -0
- package/dist/lib/scoped-update.js +311 -0
- package/dist/lib/scoped-update.js.map +1 -0
- package/dist/lib/tui-runtime.d.ts +2 -0
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +33 -9
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/types.d.ts +51 -15
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +2 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +75 -100
- package/dist/plugin.js.map +1 -1
- package/dist/providers/alibaba-coding-plan.d.ts.map +1 -1
- package/dist/providers/alibaba-coding-plan.js +35 -4
- package/dist/providers/alibaba-coding-plan.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +13 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/chutes.d.ts.map +1 -1
- package/dist/providers/chutes.js +6 -0
- package/dist/providers/chutes.js.map +1 -1
- package/dist/providers/copilot.d.ts +2 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +143 -60
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/cursor.d.ts.map +1 -1
- package/dist/providers/cursor.js +30 -1
- package/dist/providers/cursor.js.map +1 -1
- package/dist/providers/deepseek.d.ts.map +1 -1
- package/dist/providers/deepseek.js +13 -1
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/google-agy.d.ts.map +1 -1
- package/dist/providers/google-agy.js +7 -1
- package/dist/providers/google-agy.js.map +1 -1
- package/dist/providers/google-antigravity.d.ts.map +1 -1
- package/dist/providers/google-antigravity.js +10 -2
- package/dist/providers/google-antigravity.js.map +1 -1
- package/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/dist/providers/google-gemini-cli.js +8 -2
- package/dist/providers/google-gemini-cli.js.map +1 -1
- package/dist/providers/kimi-code.d.ts.map +1 -1
- package/dist/providers/kimi-code.js +6 -0
- package/dist/providers/kimi-code.js.map +1 -1
- package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
- package/dist/providers/minimax-coding-plan.js +12 -4
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/nanogpt.d.ts.map +1 -1
- package/dist/providers/nanogpt.js +19 -1
- package/dist/providers/nanogpt.js.map +1 -1
- package/dist/providers/ollama-cloud.d.ts.map +1 -1
- package/dist/providers/ollama-cloud.js +12 -0
- package/dist/providers/ollama-cloud.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +7 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +9 -1
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/providers/quota-providers.d.ts +6 -0
- package/dist/providers/quota-providers.d.ts.map +1 -0
- package/dist/providers/quota-providers.js +276 -0
- package/dist/providers/quota-providers.js.map +1 -0
- package/dist/providers/qwen-code.d.ts.map +1 -1
- package/dist/providers/qwen-code.js +29 -3
- package/dist/providers/qwen-code.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +2 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/result-helpers.d.ts +2 -1
- package/dist/providers/result-helpers.d.ts.map +1 -1
- package/dist/providers/result-helpers.js +6 -1
- package/dist/providers/result-helpers.js.map +1 -1
- package/dist/providers/synthetic.d.ts.map +1 -1
- package/dist/providers/synthetic.js +7 -1
- package/dist/providers/synthetic.js.map +1 -1
- package/dist/providers/zai.d.ts.map +1 -1
- package/dist/providers/zai.js +7 -1
- package/dist/providers/zai.js.map +1 -1
- package/dist/providers/zhipu.d.ts.map +1 -1
- package/dist/providers/zhipu.js +7 -1
- package/dist/providers/zhipu.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +148 -122
- package/dist/tui.tsx +122 -114
- package/package.json +29 -21
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
|
-
import { basename } from "path";
|
|
3
|
+
import { basename, join } from "path";
|
|
4
4
|
import { writeJsonAtomic } from "./atomic-json.js";
|
|
5
|
+
import { applyConfigDocumentEdit, planConfigDocumentEdit, validateConfigDocumentEdit, } from "./opencode-config-editor.js";
|
|
5
6
|
import { dedupeNonEmptyStrings, extractPluginSpecsFromParsedConfig, getPluginSpecFromEntry, isQuotaPluginSpec, resolveEditableConfigPath, findGitWorktreeRoot, } from "./config-file-utils.js";
|
|
6
7
|
import { parseJsonOrJsonc } from "./jsonc.js";
|
|
8
|
+
import { QUOTA_PROVIDERS_AGGREGATE_ID } from "./quota-providers.js";
|
|
7
9
|
import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js";
|
|
8
10
|
import { QUOTA_PROVIDER_SHAPES, getQuotaProviderDisplayLabel, normalizeQuotaProviderId, } from "./provider-metadata.js";
|
|
9
11
|
import { getQuotaFormatStyleLabel, isQuotaFormatStyle, resolveQuotaFormatStyle, } from "./quota-format-style.js";
|
|
10
|
-
import { getQuotaToastConfigPath, QUOTA_TOAST_CONFIG_RELATIVE_PATH } from "./config.js";
|
|
11
|
-
const QUOTA_PLUGIN_SPEC = "@slkiser/opencode-quota";
|
|
12
|
+
import { getQuotaToastConfigPath, QUOTA_TOAST_CONFIG_RELATIVE_PATH, QUOTA_TOAST_CONFIG_RELATIVE_PATHS, } from "./config.js";
|
|
13
|
+
const QUOTA_PLUGIN_SPEC = "@slkiser/opencode-quota@latest";
|
|
12
14
|
const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
13
15
|
const TUI_SCHEMA_URL = "https://opencode.ai/tui.json";
|
|
14
16
|
const GITHUB_REPO_URL = "https://github.com/slkiser/opencode-quota";
|
|
15
17
|
const GITHUB_STAR_NOTE = `if this helps, stars are appreciated: ${GITHUB_REPO_URL}`;
|
|
18
|
+
const TUI_COMMAND_DISPLAY_COMMENT = "// OpenCode Quota: tuiCommandDisplay chooses whether native TUI command output appears in the session transcript or a local popup dialog.";
|
|
16
19
|
export class InitInstallerError extends Error {
|
|
17
20
|
details;
|
|
18
21
|
constructor(message, details) {
|
|
@@ -30,48 +33,37 @@ function hasOwnKey(value, key) {
|
|
|
30
33
|
function jsonEqual(left, right) {
|
|
31
34
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
32
35
|
}
|
|
33
|
-
const QUOTA_UI_CHOICE_ORDER = [
|
|
34
|
-
"toast",
|
|
35
|
-
"sidebar",
|
|
36
|
-
"compact_status",
|
|
37
|
-
"none",
|
|
38
|
-
];
|
|
36
|
+
const QUOTA_UI_CHOICE_ORDER = ["sidebar", "toast", "compact_status", "none"];
|
|
39
37
|
function normalizeQuotaUiIntent(selections) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
const rawSelections = selections;
|
|
39
|
+
if (hasOwnKey(rawSelections, "tuiCompactStatus")) {
|
|
40
|
+
throw new InitInstallerError("Unsupported installer selection: tuiCompactStatus");
|
|
41
|
+
}
|
|
42
|
+
const quotaUi = rawSelections.quotaUi;
|
|
43
|
+
if (!Array.isArray(quotaUi)) {
|
|
44
|
+
throw new InitInstallerError("Quota UI selections must be an array.");
|
|
45
|
+
}
|
|
46
|
+
if (quotaUi.length === 0) {
|
|
47
|
+
throw new InitInstallerError("Quota UI selections must not be empty.");
|
|
48
|
+
}
|
|
47
49
|
const seen = new Set();
|
|
48
|
-
for (const rawChoice of
|
|
49
|
-
if (typeof rawChoice !== "string" ||
|
|
50
|
+
for (const rawChoice of quotaUi) {
|
|
51
|
+
if (typeof rawChoice !== "string" ||
|
|
52
|
+
!QUOTA_UI_CHOICE_ORDER.includes(rawChoice)) {
|
|
50
53
|
throw new InitInstallerError(`Unknown Quota UI option: ${String(rawChoice)}`);
|
|
51
54
|
}
|
|
52
55
|
seen.add(rawChoice);
|
|
53
56
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (legacyCompactMode !== "home_bottom" && legacyCompactMode !== "home_bottom_session_prompt") {
|
|
57
|
-
throw new InitInstallerError(`Unknown Compact TUI status: ${String(legacyCompactMode)}`);
|
|
58
|
-
}
|
|
59
|
-
seen.delete("none");
|
|
60
|
-
seen.add("compact_status");
|
|
61
|
-
}
|
|
62
|
-
let choices = QUOTA_UI_CHOICE_ORDER.filter((choice) => seen.has(choice));
|
|
63
|
-
if (choices.length === 0) {
|
|
64
|
-
choices = ["none"];
|
|
65
|
-
}
|
|
66
|
-
else if (choices.length > 1 && choices.includes("none")) {
|
|
67
|
-
choices = choices.filter((choice) => choice !== "none");
|
|
57
|
+
if (seen.has("none") && seen.size > 1) {
|
|
58
|
+
throw new InitInstallerError("Manual commands only cannot be combined with automatic quota displays.");
|
|
68
59
|
}
|
|
60
|
+
const choices = QUOTA_UI_CHOICE_ORDER.filter((choice) => seen.has(choice));
|
|
69
61
|
const enableSidebarPanel = choices.includes("sidebar");
|
|
70
62
|
const enableCompactStatus = choices.includes("compact_status");
|
|
71
63
|
return {
|
|
72
64
|
choices,
|
|
73
65
|
enableToast: choices.includes("toast"),
|
|
74
|
-
installTuiPlugin:
|
|
66
|
+
installTuiPlugin: selections.interfaces !== "web",
|
|
75
67
|
enableSidebarPanel,
|
|
76
68
|
enableCompactStatus,
|
|
77
69
|
};
|
|
@@ -84,7 +76,7 @@ function getUiLabel(choices) {
|
|
|
84
76
|
return "Sidebar";
|
|
85
77
|
if (choice === "compact_status")
|
|
86
78
|
return "Compact status";
|
|
87
|
-
return "
|
|
79
|
+
return "Manual commands only";
|
|
88
80
|
});
|
|
89
81
|
return labels.join(" + ");
|
|
90
82
|
}
|
|
@@ -94,13 +86,6 @@ function getProviderModeLabel(mode) {
|
|
|
94
86
|
function getPercentDisplayModeLabel(mode) {
|
|
95
87
|
return mode === "used" ? "Used" : "Remaining";
|
|
96
88
|
}
|
|
97
|
-
function getTuiCompactStatusLabel(mode) {
|
|
98
|
-
if (mode === "home_bottom")
|
|
99
|
-
return "Home bottom only";
|
|
100
|
-
if (mode === "home_bottom_session_prompt")
|
|
101
|
-
return "Home bottom + session prompt";
|
|
102
|
-
return "Off";
|
|
103
|
-
}
|
|
104
89
|
function resolveRequestedProviders(selections) {
|
|
105
90
|
if (selections.providerMode === "auto") {
|
|
106
91
|
return "auto";
|
|
@@ -203,9 +188,11 @@ function setInstallerOwnedSetting(target, key, value, pathLabel, edit) {
|
|
|
203
188
|
return;
|
|
204
189
|
}
|
|
205
190
|
if (!jsonEqual(target[key], value)) {
|
|
191
|
+
const previousValue = target[key];
|
|
206
192
|
target[key] = value;
|
|
207
193
|
edit.changed = true;
|
|
208
194
|
edit.updatedKeys.push(pathLabel);
|
|
195
|
+
edit.valueChanges.push(`${pathLabel}: ${JSON.stringify(previousValue)} -> ${JSON.stringify(value)}`);
|
|
209
196
|
}
|
|
210
197
|
}
|
|
211
198
|
function planTuiSidebarPanelConfig(params) {
|
|
@@ -304,11 +291,12 @@ async function readLegacyQuotaToastSeed(baseDir) {
|
|
|
304
291
|
if (!target.existed) {
|
|
305
292
|
return null;
|
|
306
293
|
}
|
|
307
|
-
const root = await readExistingConfig(
|
|
294
|
+
const root = await readExistingConfig({
|
|
295
|
+
path: target.sourcePath,
|
|
296
|
+
format: target.sourcePath.endsWith(".jsonc") ? "jsonc" : "json",
|
|
297
|
+
});
|
|
308
298
|
const experimental = isPlainObject(root.experimental) ? root.experimental : null;
|
|
309
|
-
const quotaToast = experimental && isPlainObject(experimental.quotaToast)
|
|
310
|
-
? experimental.quotaToast
|
|
311
|
-
: null;
|
|
299
|
+
const quotaToast = experimental && isPlainObject(experimental.quotaToast) ? experimental.quotaToast : null;
|
|
312
300
|
return quotaToast ? cloneJsonObject(quotaToast) : null;
|
|
313
301
|
}
|
|
314
302
|
function buildQuickSetupNotes(selections) {
|
|
@@ -367,7 +355,12 @@ function syncLegacyQuotaToast(params) {
|
|
|
367
355
|
}
|
|
368
356
|
}
|
|
369
357
|
async function planOpencodeEdit(params) {
|
|
370
|
-
const target = resolveEditableConfigPath({
|
|
358
|
+
const target = resolveEditableConfigPath({
|
|
359
|
+
dir: params.baseDir,
|
|
360
|
+
kind: "opencode",
|
|
361
|
+
preferredFormat: params.selections.configFormat ?? "jsonc",
|
|
362
|
+
convertJsonToJsonc: true,
|
|
363
|
+
});
|
|
371
364
|
const edit = {
|
|
372
365
|
kind: "opencode",
|
|
373
366
|
path: target.path,
|
|
@@ -377,13 +370,19 @@ async function planOpencodeEdit(params) {
|
|
|
377
370
|
addedPlugins: [],
|
|
378
371
|
addedKeys: [],
|
|
379
372
|
updatedKeys: [],
|
|
373
|
+
valueChanges: [],
|
|
380
374
|
skippedValues: [],
|
|
381
|
-
warnings:
|
|
382
|
-
? ["Existing JSONC comments/trailing commas will be stripped."]
|
|
383
|
-
: [],
|
|
375
|
+
warnings: [],
|
|
384
376
|
};
|
|
385
|
-
const root = target.existed
|
|
386
|
-
|
|
377
|
+
const root = target.existed
|
|
378
|
+
? await readExistingConfig({
|
|
379
|
+
path: target.sourcePath,
|
|
380
|
+
format: target.sourcePath.endsWith(".jsonc") ? "jsonc" : "json",
|
|
381
|
+
})
|
|
382
|
+
: {};
|
|
383
|
+
if (!target.existed) {
|
|
384
|
+
ensureSchema(root, OPENCODE_SCHEMA_URL, edit);
|
|
385
|
+
}
|
|
387
386
|
const plugin = ensureTopLevelPluginArray(root, edit);
|
|
388
387
|
appendQuotaPluginIfMissing({
|
|
389
388
|
container: plugin,
|
|
@@ -398,46 +397,109 @@ async function planOpencodeEdit(params) {
|
|
|
398
397
|
edit,
|
|
399
398
|
});
|
|
400
399
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
const documentEdit = await planConfigDocumentEdit({
|
|
401
|
+
target,
|
|
402
|
+
desiredData: root,
|
|
403
|
+
managedComments: [
|
|
404
|
+
{
|
|
405
|
+
path: ["plugin"],
|
|
406
|
+
text: TUI_COMMAND_DISPLAY_COMMENT,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
path: ["plugin"],
|
|
410
|
+
text: "// OpenCode Quota: loads the server plugin for slash commands and quota checks.",
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
edit.changed = documentEdit.changed;
|
|
415
|
+
edit.documentEdit = documentEdit;
|
|
404
416
|
return edit;
|
|
405
417
|
}
|
|
418
|
+
function resolveQuotaConfigTarget(baseDir, preferredFormat) {
|
|
419
|
+
const jsoncPath = getQuotaToastConfigPath(baseDir, "jsonc");
|
|
420
|
+
if (existsSync(jsoncPath)) {
|
|
421
|
+
return {
|
|
422
|
+
path: jsoncPath,
|
|
423
|
+
sourcePath: jsoncPath,
|
|
424
|
+
format: "jsonc",
|
|
425
|
+
existed: true,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const jsonPath = getQuotaToastConfigPath(baseDir, "json");
|
|
429
|
+
if (existsSync(jsonPath)) {
|
|
430
|
+
if (preferredFormat === "jsonc") {
|
|
431
|
+
return {
|
|
432
|
+
path: jsoncPath,
|
|
433
|
+
sourcePath: jsonPath,
|
|
434
|
+
removeSourcePath: jsonPath,
|
|
435
|
+
format: "jsonc",
|
|
436
|
+
existed: true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
path: jsonPath,
|
|
441
|
+
sourcePath: jsonPath,
|
|
442
|
+
format: "json",
|
|
443
|
+
existed: true,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const path = getQuotaToastConfigPath(baseDir, preferredFormat);
|
|
447
|
+
return {
|
|
448
|
+
path,
|
|
449
|
+
sourcePath: path,
|
|
450
|
+
format: preferredFormat,
|
|
451
|
+
existed: false,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
406
454
|
async function planQuotaConfigEdit(params) {
|
|
407
|
-
const
|
|
408
|
-
const existed = existsSync(path);
|
|
455
|
+
const target = resolveQuotaConfigTarget(params.baseDir, params.selections.configFormat ?? "jsonc");
|
|
409
456
|
const edit = {
|
|
410
457
|
kind: "quota",
|
|
411
|
-
path,
|
|
412
|
-
existed,
|
|
413
|
-
format:
|
|
458
|
+
path: target.path,
|
|
459
|
+
existed: target.existed,
|
|
460
|
+
format: target.format,
|
|
414
461
|
changed: false,
|
|
415
462
|
addedPlugins: [],
|
|
416
463
|
addedKeys: [],
|
|
417
464
|
updatedKeys: [],
|
|
465
|
+
valueChanges: [],
|
|
418
466
|
skippedValues: [],
|
|
419
467
|
warnings: [],
|
|
420
468
|
};
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
469
|
+
if (target.path.endsWith(".jsonc") &&
|
|
470
|
+
existsSync(getQuotaToastConfigPath(params.baseDir, "json"))) {
|
|
471
|
+
edit.warnings.push("Both quota-toast.jsonc and quota-toast.json exist; using JSONC and preserving the JSON file.");
|
|
472
|
+
}
|
|
473
|
+
const legacyQuotaToast = target.existed ? null : await readLegacyQuotaToastSeed(params.baseDir);
|
|
474
|
+
const quotaToast = target.existed
|
|
475
|
+
? await readExistingConfig({
|
|
476
|
+
path: target.sourcePath,
|
|
477
|
+
format: target.sourcePath.endsWith(".jsonc") ? "jsonc" : "json",
|
|
478
|
+
})
|
|
424
479
|
: legacyQuotaToast
|
|
425
480
|
? cloneJsonObject(legacyQuotaToast)
|
|
426
481
|
: {};
|
|
427
|
-
if (!existed) {
|
|
428
|
-
edit.changed = true;
|
|
482
|
+
if (!target.existed) {
|
|
429
483
|
edit.addedKeys.push(legacyQuotaToast
|
|
430
|
-
? `${QUOTA_TOAST_CONFIG_RELATIVE_PATH} (seeded from experimental.quotaToast)`
|
|
431
|
-
:
|
|
484
|
+
? `${target.path.endsWith(".jsonc") ? QUOTA_TOAST_CONFIG_RELATIVE_PATHS[0] : QUOTA_TOAST_CONFIG_RELATIVE_PATH} (seeded from experimental.quotaToast)`
|
|
485
|
+
: target.path.endsWith(".jsonc")
|
|
486
|
+
? QUOTA_TOAST_CONFIG_RELATIVE_PATHS[0]
|
|
487
|
+
: QUOTA_TOAST_CONFIG_RELATIVE_PATH);
|
|
432
488
|
}
|
|
433
489
|
setInstallerOwnedSetting(quotaToast, "enableToast", params.quotaUiIntent.enableToast, "quotaToast.enableToast", edit);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
490
|
+
if (params.selections.tuiCommandDisplay !== undefined) {
|
|
491
|
+
setInstallerOwnedSetting(quotaToast, "tuiCommandDisplay", params.selections.tuiCommandDisplay, "quotaToast.tuiCommandDisplay", edit);
|
|
492
|
+
}
|
|
493
|
+
setInstallerOwnedSetting(quotaToast, "showSessionTokens", params.selections.showSessionTokens, "quotaToast.showSessionTokens", edit);
|
|
494
|
+
const requestedProviders = resolveRequestedProviders(params.selections);
|
|
495
|
+
const enabledProviders = requestedProviders !== "auto" &&
|
|
496
|
+
Array.isArray(quotaToast.enabledProviders) &&
|
|
497
|
+
quotaToast.enabledProviders.includes(QUOTA_PROVIDERS_AGGREGATE_ID)
|
|
498
|
+
? [...requestedProviders, QUOTA_PROVIDERS_AGGREGATE_ID]
|
|
499
|
+
: requestedProviders;
|
|
500
|
+
setInstallerOwnedSetting(quotaToast, "enabledProviders", enabledProviders, "quotaToast.enabledProviders", edit);
|
|
501
|
+
setInstallerOwnedSetting(quotaToast, "formatStyle", params.selections.formatStyle, "quotaToast.formatStyle", edit);
|
|
502
|
+
setInstallerOwnedSetting(quotaToast, "percentDisplayMode", params.selections.percentDisplayMode, "quotaToast.percentDisplayMode", edit);
|
|
441
503
|
planTuiSidebarPanelConfig({
|
|
442
504
|
quotaToast,
|
|
443
505
|
quotaUiIntent: params.quotaUiIntent,
|
|
@@ -453,14 +515,86 @@ async function planQuotaConfigEdit(params) {
|
|
|
453
515
|
selections: params.selections,
|
|
454
516
|
edit,
|
|
455
517
|
});
|
|
518
|
+
const documentEdit = await planConfigDocumentEdit({
|
|
519
|
+
target,
|
|
520
|
+
desiredData: quotaToast,
|
|
521
|
+
managedComments: [
|
|
522
|
+
{
|
|
523
|
+
path: ["enableToast"],
|
|
524
|
+
text: "// Automatic quota surfaces. Slash commands remain available when these are disabled.",
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
path: ["enabledProviders"],
|
|
528
|
+
text: '// Provider selection: "auto" detects providers; an array tracks only listed providers.',
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
path: ["formatStyle"],
|
|
532
|
+
text: "// Quota presentation and reset-period choices.",
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
path: ["showSessionTokens"],
|
|
536
|
+
text: "// Include or hide current-session input and output token totals.",
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
path: ["tuiSidebarPanel"],
|
|
540
|
+
text: "// TUI-only surfaces.",
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
path: ["maintainerAnnouncements"],
|
|
544
|
+
text: "// Optional bundled maintainer Home announcements.",
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
});
|
|
548
|
+
edit.changed = documentEdit.changed;
|
|
549
|
+
edit.documentEdit = documentEdit;
|
|
456
550
|
edit.plannedData = quotaToast;
|
|
457
|
-
if (edit.changed) {
|
|
458
|
-
edit.nextData = quotaToast;
|
|
459
|
-
}
|
|
460
551
|
return edit;
|
|
461
552
|
}
|
|
553
|
+
function getCanonicalQuotaPackageSpecForRemoval(entry) {
|
|
554
|
+
const spec = typeof entry === "string"
|
|
555
|
+
? entry
|
|
556
|
+
: Array.isArray(entry) && typeof entry[0] === "string"
|
|
557
|
+
? entry[0]
|
|
558
|
+
: undefined;
|
|
559
|
+
if (spec === "@slkiser/opencode-quota")
|
|
560
|
+
return spec;
|
|
561
|
+
if (spec !== undefined &&
|
|
562
|
+
/^@slkiser\/opencode-quota@(?:v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?|[A-Za-z][0-9A-Za-z._-]*)$/.test(spec)) {
|
|
563
|
+
return spec;
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
function removeQuotaPluginsFromTui(root, edit) {
|
|
568
|
+
const containers = [
|
|
569
|
+
{ value: root.plugin, pathLabel: "plugin" },
|
|
570
|
+
];
|
|
571
|
+
if (isPlainObject(root.tui)) {
|
|
572
|
+
containers.push({ value: root.tui.plugin, pathLabel: "tui.plugin" });
|
|
573
|
+
}
|
|
574
|
+
for (const container of containers) {
|
|
575
|
+
if (!Array.isArray(container.value))
|
|
576
|
+
continue;
|
|
577
|
+
const retained = container.value.filter((entry) => {
|
|
578
|
+
const spec = getCanonicalQuotaPackageSpecForRemoval(entry);
|
|
579
|
+
if (spec !== undefined) {
|
|
580
|
+
edit.updatedKeys.push(`${container.pathLabel}: remove ${spec}`);
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
});
|
|
585
|
+
if (retained.length !== container.value.length) {
|
|
586
|
+
container.value.splice(0, container.value.length, ...retained);
|
|
587
|
+
edit.changed = true;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
462
591
|
async function planTuiEdit(params) {
|
|
463
|
-
const target = resolveEditableConfigPath({
|
|
592
|
+
const target = resolveEditableConfigPath({
|
|
593
|
+
dir: params.baseDir,
|
|
594
|
+
kind: "tui",
|
|
595
|
+
preferredFormat: params.selections.configFormat ?? "jsonc",
|
|
596
|
+
convertJsonToJsonc: params.selections.interfaces !== "web",
|
|
597
|
+
});
|
|
464
598
|
const edit = {
|
|
465
599
|
kind: "tui",
|
|
466
600
|
path: target.path,
|
|
@@ -470,54 +604,95 @@ async function planTuiEdit(params) {
|
|
|
470
604
|
addedPlugins: [],
|
|
471
605
|
addedKeys: [],
|
|
472
606
|
updatedKeys: [],
|
|
607
|
+
valueChanges: [],
|
|
473
608
|
skippedValues: [],
|
|
474
|
-
warnings:
|
|
475
|
-
? ["Existing JSONC comments/trailing commas will be stripped."]
|
|
476
|
-
: [],
|
|
609
|
+
warnings: [],
|
|
477
610
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
611
|
+
if (params.selections.interfaces === "web" && !target.existed) {
|
|
612
|
+
return edit;
|
|
613
|
+
}
|
|
614
|
+
const root = target.existed
|
|
615
|
+
? await readExistingConfig({
|
|
616
|
+
path: target.sourcePath,
|
|
617
|
+
format: target.sourcePath.endsWith(".jsonc") ? "jsonc" : "json",
|
|
618
|
+
})
|
|
619
|
+
: {};
|
|
620
|
+
if (!target.existed) {
|
|
621
|
+
ensureSchema(root, TUI_SCHEMA_URL, edit);
|
|
483
622
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
appendQuotaPluginIfMissing({
|
|
487
|
-
container: pluginTarget.container,
|
|
488
|
-
pathLabel: pluginTarget.pathLabel,
|
|
489
|
-
kind: "tui",
|
|
490
|
-
edit,
|
|
491
|
-
});
|
|
623
|
+
if (params.selections.interfaces === "web") {
|
|
624
|
+
removeQuotaPluginsFromTui(root, edit);
|
|
492
625
|
}
|
|
493
|
-
|
|
494
|
-
|
|
626
|
+
else {
|
|
627
|
+
const existingPluginSpecs = extractPluginSpecsFromParsedConfig(root);
|
|
628
|
+
if (existingPluginSpecs.some((spec) => isQuotaPluginSpec(spec, "tui"))) {
|
|
629
|
+
edit.skippedValues.push(`tui config already includes ${QUOTA_PLUGIN_SPEC}`);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
const pluginTarget = ensureTuiPluginArray(root, edit);
|
|
633
|
+
appendQuotaPluginIfMissing({
|
|
634
|
+
container: pluginTarget.container,
|
|
635
|
+
pathLabel: pluginTarget.pathLabel,
|
|
636
|
+
kind: "tui",
|
|
637
|
+
edit,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
495
640
|
}
|
|
641
|
+
const documentEdit = await planConfigDocumentEdit({
|
|
642
|
+
target,
|
|
643
|
+
desiredData: root,
|
|
644
|
+
managedComments: params.selections.interfaces === "web"
|
|
645
|
+
? []
|
|
646
|
+
: [
|
|
647
|
+
{
|
|
648
|
+
path: ["plugin"],
|
|
649
|
+
text: TUI_COMMAND_DISPLAY_COMMENT,
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
path: ["plugin"],
|
|
653
|
+
text: "// OpenCode Quota: loads the TUI sidebar, compact status, and local commands.",
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
});
|
|
657
|
+
edit.changed = documentEdit.changed;
|
|
658
|
+
edit.documentEdit = documentEdit;
|
|
496
659
|
return edit;
|
|
497
660
|
}
|
|
498
661
|
function buildPlanSummary(plan) {
|
|
499
662
|
const quotaUiIntent = normalizeQuotaUiIntent(plan.selections);
|
|
663
|
+
const opencodeFormat = plan.edits.find((edit) => edit.kind === "opencode")?.format ??
|
|
664
|
+
plan.selections.configFormat ??
|
|
665
|
+
"jsonc";
|
|
500
666
|
const lines = [
|
|
667
|
+
`Interface: ${plan.selections.interfaces === "tui" ? "TUI" : plan.selections.interfaces === "web" ? "Web" : "Both"}`,
|
|
501
668
|
`Scope: ${plan.selections.scope} (${plan.baseDir})`,
|
|
502
|
-
`
|
|
503
|
-
`Provider mode: ${getProviderModeLabel(plan.selections.providerMode)}`,
|
|
504
|
-
`Quota reset periods: ${getQuotaFormatStyleLabel(plan.selections.formatStyle)}`,
|
|
505
|
-
`Quota percentage meaning: ${getPercentDisplayModeLabel(plan.selections.percentDisplayMode)}`,
|
|
506
|
-
`Session token details: ${plan.selections.showSessionTokens ? "Show" : "Hide"}`,
|
|
669
|
+
`Config format: ${opencodeFormat.toUpperCase()}`,
|
|
507
670
|
];
|
|
671
|
+
if (plan.selections.interfaces !== "web") {
|
|
672
|
+
lines.push(`TUI surfaces: ${getUiLabel(quotaUiIntent.choices)}`);
|
|
673
|
+
lines.push(`Command display: ${plan.selections.tuiCommandDisplay === "inline" ? "Inline with messages" : "Popup dialog"}`);
|
|
674
|
+
}
|
|
675
|
+
lines.push(`Provider mode: ${getProviderModeLabel(plan.selections.providerMode)}`, `Quota reset periods: ${getQuotaFormatStyleLabel(plan.selections.formatStyle)}`, `Quota percentage meaning: ${getPercentDisplayModeLabel(plan.selections.percentDisplayMode)}`, `Session input/output tokens: ${plan.selections.showSessionTokens ? "Show" : "Hide"}`);
|
|
508
676
|
if (plan.selections.maintainerAnnouncements !== undefined) {
|
|
509
677
|
lines.push(`Maintainer announcements: ${plan.selections.maintainerAnnouncements ? "Enabled" : "Disabled"}`);
|
|
510
678
|
}
|
|
511
679
|
if (quotaUiIntent.enableCompactStatus) {
|
|
512
|
-
lines.push(
|
|
680
|
+
lines.push("Compact status mode: Home bottom + session prompt");
|
|
513
681
|
}
|
|
514
682
|
const requestedProviders = resolveRequestedProviders(plan.selections);
|
|
515
683
|
if (requestedProviders !== "auto") {
|
|
516
684
|
lines.push(`Manual providers: ${requestedProviders.map((providerId) => getQuotaProviderDisplayLabel(providerId)).join(", ")}`);
|
|
517
685
|
}
|
|
518
686
|
for (const edit of plan.edits) {
|
|
519
|
-
const
|
|
520
|
-
|
|
687
|
+
const convertedFrom = edit.documentEdit?.removeSourcePath;
|
|
688
|
+
const mode = convertedFrom
|
|
689
|
+
? "convert"
|
|
690
|
+
: !edit.existed
|
|
691
|
+
? "create"
|
|
692
|
+
: edit.changed
|
|
693
|
+
? "update"
|
|
694
|
+
: "unchanged";
|
|
695
|
+
lines.push(convertedFrom ? `${mode}: ${convertedFrom} -> ${edit.path}` : `${mode}: ${edit.path}`);
|
|
521
696
|
for (const plugin of edit.addedPlugins) {
|
|
522
697
|
lines.push(` + plugin ${plugin}`);
|
|
523
698
|
}
|
|
@@ -527,6 +702,9 @@ function buildPlanSummary(plan) {
|
|
|
527
702
|
for (const key of edit.updatedKeys) {
|
|
528
703
|
lines.push(` ~ ${key}`);
|
|
529
704
|
}
|
|
705
|
+
for (const change of edit.valueChanges) {
|
|
706
|
+
lines.push(` ${change}`);
|
|
707
|
+
}
|
|
530
708
|
for (const skipped of edit.skippedValues) {
|
|
531
709
|
lines.push(` = ${skipped}`);
|
|
532
710
|
}
|
|
@@ -568,10 +746,18 @@ export function resolveInitInstallerBaseDir(params) {
|
|
|
568
746
|
return findGitWorktreeRoot(cwd) ?? cwd;
|
|
569
747
|
}
|
|
570
748
|
export async function planInitInstaller(params) {
|
|
571
|
-
const
|
|
572
|
-
const selections = {
|
|
749
|
+
const requestedSelections = {
|
|
573
750
|
...params.selections,
|
|
751
|
+
quotaUi: params.selections.interfaces === "web" ? ["none"] : params.selections.quotaUi,
|
|
752
|
+
};
|
|
753
|
+
const quotaUiIntent = normalizeQuotaUiIntent(requestedSelections);
|
|
754
|
+
const selections = {
|
|
755
|
+
...requestedSelections,
|
|
756
|
+
configFormat: params.selections.configFormat ?? "jsonc",
|
|
574
757
|
quotaUi: quotaUiIntent.choices,
|
|
758
|
+
tuiCommandDisplay: params.selections.interfaces === "web"
|
|
759
|
+
? undefined
|
|
760
|
+
: (params.selections.tuiCommandDisplay ?? "inline"),
|
|
575
761
|
maintainerAnnouncements: params.selections.maintainerAnnouncements,
|
|
576
762
|
manualProviders: params.selections.providerMode === "manual"
|
|
577
763
|
? resolveRequestedProviders(params.selections)
|
|
@@ -592,8 +778,9 @@ export async function planInitInstaller(params) {
|
|
|
592
778
|
}),
|
|
593
779
|
quotaEdit,
|
|
594
780
|
];
|
|
595
|
-
|
|
596
|
-
|
|
781
|
+
const tuiEdit = await planTuiEdit({ selections, baseDir });
|
|
782
|
+
if (selections.interfaces !== "web" || tuiEdit.existed) {
|
|
783
|
+
edits.push(tuiEdit);
|
|
597
784
|
}
|
|
598
785
|
const quickSetupNotes = buildQuickSetupNotes(selections);
|
|
599
786
|
const warnings = edits.flatMap((edit) => edit.warnings);
|
|
@@ -611,13 +798,30 @@ export async function planInitInstaller(params) {
|
|
|
611
798
|
export async function applyInitInstallerPlan(plan) {
|
|
612
799
|
const writtenPaths = [];
|
|
613
800
|
const unchangedPaths = [];
|
|
801
|
+
try {
|
|
802
|
+
await Promise.all(plan.edits.flatMap((edit) => edit.documentEdit ? [validateConfigDocumentEdit(edit.documentEdit)] : []));
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
const path = error && typeof error === "object" && "path" in error
|
|
806
|
+
? String(error.path ?? "")
|
|
807
|
+
: "";
|
|
808
|
+
throw new InitInstallerError(`Config changed since preview${path ? `: ${path}` : "."}`, {
|
|
809
|
+
path: path || undefined,
|
|
810
|
+
writtenPaths,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
614
813
|
for (const edit of plan.edits) {
|
|
615
|
-
if (!edit.changed || !edit.nextData) {
|
|
814
|
+
if (!edit.changed || (!edit.nextData && !edit.documentEdit)) {
|
|
616
815
|
unchangedPaths.push(edit.path);
|
|
617
816
|
continue;
|
|
618
817
|
}
|
|
619
818
|
try {
|
|
620
|
-
|
|
819
|
+
if (edit.documentEdit) {
|
|
820
|
+
await applyConfigDocumentEdit(edit.documentEdit);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
await writeJsonAtomic(edit.path, edit.nextData, { trailingNewline: true });
|
|
824
|
+
}
|
|
621
825
|
writtenPaths.push(edit.path);
|
|
622
826
|
}
|
|
623
827
|
catch (error) {
|
|
@@ -632,40 +836,184 @@ export async function applyInitInstallerPlan(plan) {
|
|
|
632
836
|
unchangedPaths,
|
|
633
837
|
};
|
|
634
838
|
}
|
|
635
|
-
async function
|
|
839
|
+
async function readExistingInstallerAnswers(baseDir) {
|
|
840
|
+
const existingQuotaPath = QUOTA_TOAST_CONFIG_RELATIVE_PATHS.map((relativePath) => join(baseDir, relativePath)).find((path) => existsSync(path));
|
|
841
|
+
const existingHostPath = ["opencode.jsonc", "opencode.json"]
|
|
842
|
+
.map((name) => join(baseDir, name))
|
|
843
|
+
.find((path) => existsSync(path));
|
|
844
|
+
const answers = {
|
|
845
|
+
configFormat: existingQuotaPath?.endsWith(".jsonc") || existingHostPath?.endsWith(".jsonc")
|
|
846
|
+
? "jsonc"
|
|
847
|
+
: existingQuotaPath || existingHostPath
|
|
848
|
+
? "json"
|
|
849
|
+
: undefined,
|
|
850
|
+
};
|
|
851
|
+
if (!existingQuotaPath)
|
|
852
|
+
return answers;
|
|
853
|
+
const quotaToast = await readExistingConfig({
|
|
854
|
+
path: existingQuotaPath,
|
|
855
|
+
format: existingQuotaPath.endsWith(".jsonc") ? "jsonc" : "json",
|
|
856
|
+
});
|
|
857
|
+
const quotaUi = [];
|
|
858
|
+
if (isPlainObject(quotaToast.tuiSidebarPanel) && quotaToast.tuiSidebarPanel.enabled === true) {
|
|
859
|
+
quotaUi.push("sidebar");
|
|
860
|
+
}
|
|
861
|
+
if (quotaToast.enableToast === true)
|
|
862
|
+
quotaUi.push("toast");
|
|
863
|
+
if (isPlainObject(quotaToast.tuiCompactStatus) && quotaToast.tuiCompactStatus.enabled === true) {
|
|
864
|
+
quotaUi.push("compact_status");
|
|
865
|
+
}
|
|
866
|
+
answers.quotaUi = quotaUi.length > 0 ? quotaUi : ["none"];
|
|
867
|
+
if (quotaToast.enabledProviders === "auto") {
|
|
868
|
+
answers.providerMode = "auto";
|
|
869
|
+
answers.manualProviders = [];
|
|
870
|
+
}
|
|
871
|
+
else if (Array.isArray(quotaToast.enabledProviders)) {
|
|
872
|
+
answers.providerMode = "manual";
|
|
873
|
+
answers.manualProviders = quotaToast.enabledProviders.filter((value) => typeof value === "string" &&
|
|
874
|
+
QUOTA_PROVIDER_SHAPES.some((shape) => shape.id === normalizeQuotaProviderId(value)));
|
|
875
|
+
}
|
|
876
|
+
if (isQuotaFormatStyle(quotaToast.formatStyle)) {
|
|
877
|
+
answers.formatStyle = resolveQuotaFormatStyle(quotaToast.formatStyle);
|
|
878
|
+
}
|
|
879
|
+
if (quotaToast.percentDisplayMode === "remaining" || quotaToast.percentDisplayMode === "used") {
|
|
880
|
+
answers.percentDisplayMode = quotaToast.percentDisplayMode;
|
|
881
|
+
}
|
|
882
|
+
if (typeof quotaToast.showSessionTokens === "boolean") {
|
|
883
|
+
answers.showSessionTokens = quotaToast.showSessionTokens;
|
|
884
|
+
}
|
|
885
|
+
if (quotaToast.tuiCommandDisplay === "inline" || quotaToast.tuiCommandDisplay === "dialog") {
|
|
886
|
+
answers.tuiCommandDisplay = quotaToast.tuiCommandDisplay;
|
|
887
|
+
}
|
|
888
|
+
if (isPlainObject(quotaToast.maintainerAnnouncements) &&
|
|
889
|
+
typeof quotaToast.maintainerAnnouncements.enabled === "boolean") {
|
|
890
|
+
answers.maintainerAnnouncements = quotaToast.maintainerAnnouncements.enabled;
|
|
891
|
+
}
|
|
892
|
+
return answers;
|
|
893
|
+
}
|
|
894
|
+
async function promptForSelections(prompts, context) {
|
|
895
|
+
const interfaces = await prompts.select({
|
|
896
|
+
message: "Which OpenCode interfaces do you use?",
|
|
897
|
+
options: [
|
|
898
|
+
{ label: "TUI", value: "tui", hint: "terminal interface" },
|
|
899
|
+
{ label: "Web", value: "web", hint: "browser interface" },
|
|
900
|
+
{
|
|
901
|
+
label: "Both",
|
|
902
|
+
value: "both",
|
|
903
|
+
hint: "configure terminal and browser interfaces",
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
});
|
|
907
|
+
if (prompts.isCancel(interfaces))
|
|
908
|
+
return null;
|
|
909
|
+
if (interfaces === "web") {
|
|
910
|
+
prompts.log.info("Web slash commands appear inline. TUI-only surfaces and popup dialogs are unavailable.");
|
|
911
|
+
}
|
|
636
912
|
const scope = await prompts.select({
|
|
637
|
-
message: "
|
|
913
|
+
message: "Where should OpenCode Quota be configured?",
|
|
914
|
+
initialValue: "global",
|
|
638
915
|
options: [
|
|
916
|
+
{
|
|
917
|
+
label: "Global OpenCode config (recommended)",
|
|
918
|
+
value: "global",
|
|
919
|
+
hint: "install for all projects using your global config",
|
|
920
|
+
},
|
|
639
921
|
{ label: "Project config", value: "project", hint: "install only for this repo/worktree" },
|
|
640
|
-
{ label: "Global OpenCode config", value: "global", hint: "install for all projects using your global config" },
|
|
641
922
|
],
|
|
642
923
|
});
|
|
643
924
|
if (prompts.isCancel(scope))
|
|
644
925
|
return null;
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
926
|
+
const baseDir = resolveInitInstallerBaseDir({
|
|
927
|
+
scope: scope,
|
|
928
|
+
cwd: context.cwd,
|
|
929
|
+
env: context.env,
|
|
930
|
+
homeDir: context.homeDir,
|
|
931
|
+
});
|
|
932
|
+
const existing = await readExistingInstallerAnswers(baseDir);
|
|
933
|
+
const configFormat = await prompts.select({
|
|
934
|
+
message: "OpenCode config format",
|
|
935
|
+
initialValue: existing.configFormat ?? "jsonc",
|
|
648
936
|
options: [
|
|
649
|
-
{ label: "Toast", value: "toast", hint: "popup quota summaries after idle/question/compact events" },
|
|
650
|
-
{ label: "Sidebar panel", value: "sidebar", hint: "full Quota panel in the OpenCode session sidebar" },
|
|
651
|
-
{ label: "Compact status line", value: "compact_status", hint: "short quota summary in the TUI status area" },
|
|
652
937
|
{
|
|
653
|
-
label: "
|
|
654
|
-
value: "
|
|
655
|
-
hint: "
|
|
938
|
+
label: "JSONC (recommended)",
|
|
939
|
+
value: "jsonc",
|
|
940
|
+
hint: "keeps useful section comments and trailing commas",
|
|
656
941
|
},
|
|
942
|
+
{ label: "JSON", value: "json", hint: "strict JSON without comments" },
|
|
657
943
|
],
|
|
658
944
|
});
|
|
659
|
-
if (prompts.isCancel(
|
|
945
|
+
if (prompts.isCancel(configFormat))
|
|
660
946
|
return null;
|
|
661
|
-
|
|
662
|
-
|
|
947
|
+
let quotaUi = ["none"];
|
|
948
|
+
let tuiCommandDisplay;
|
|
949
|
+
let maintainerAnnouncements;
|
|
950
|
+
if (interfaces !== "web") {
|
|
951
|
+
while (true) {
|
|
952
|
+
quotaUi = await prompts.multiselect({
|
|
953
|
+
message: "Which automatic quota displays do you want?",
|
|
954
|
+
required: true,
|
|
955
|
+
initialValues: existing.quotaUi ?? ["sidebar"],
|
|
956
|
+
options: [
|
|
957
|
+
{
|
|
958
|
+
label: "Sidebar panel (TUI)",
|
|
959
|
+
value: "sidebar",
|
|
960
|
+
hint: "recommended; full Quota panel in the session sidebar",
|
|
961
|
+
},
|
|
962
|
+
{ label: "Toast (TUI)", value: "toast", hint: "popup quota summaries" },
|
|
963
|
+
{
|
|
964
|
+
label: "Compact status line (TUI)",
|
|
965
|
+
value: "compact_status",
|
|
966
|
+
hint: "short quota summary below the message input",
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
label: "Manual commands only",
|
|
970
|
+
value: "none",
|
|
971
|
+
hint: "no automatic surfaces; slash commands remain available",
|
|
972
|
+
},
|
|
973
|
+
],
|
|
974
|
+
});
|
|
975
|
+
if (prompts.isCancel(quotaUi))
|
|
976
|
+
return null;
|
|
977
|
+
if (!Array.isArray(quotaUi)) {
|
|
978
|
+
throw new InitInstallerError("Automatic quota surfaces require selected options.");
|
|
979
|
+
}
|
|
980
|
+
if (!(quotaUi.includes("none") && quotaUi.length > 1))
|
|
981
|
+
break;
|
|
982
|
+
prompts.log.error("Manual commands only cannot be combined with automatic surfaces.");
|
|
983
|
+
}
|
|
984
|
+
tuiCommandDisplay = await prompts.select({
|
|
985
|
+
message: "Where should slash commands (e.g. /quota) appear?",
|
|
986
|
+
initialValue: existing.tuiCommandDisplay ?? "inline",
|
|
987
|
+
options: [
|
|
988
|
+
{
|
|
989
|
+
label: "Inline with messages",
|
|
990
|
+
value: "inline",
|
|
991
|
+
hint: "persist output in the message transcript",
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
label: "Popup dialog",
|
|
995
|
+
value: "dialog",
|
|
996
|
+
hint: "show output in a temporary TUI popup",
|
|
997
|
+
},
|
|
998
|
+
],
|
|
999
|
+
});
|
|
1000
|
+
if (prompts.isCancel(tuiCommandDisplay))
|
|
1001
|
+
return null;
|
|
663
1002
|
}
|
|
664
1003
|
const providerMode = await prompts.select({
|
|
665
|
-
message: "
|
|
1004
|
+
message: "How should pre-configured providers be selected?",
|
|
1005
|
+
initialValue: existing.providerMode ?? "auto",
|
|
666
1006
|
options: [
|
|
667
|
-
{
|
|
668
|
-
|
|
1007
|
+
{
|
|
1008
|
+
label: "Auto-detect providers",
|
|
1009
|
+
value: "auto",
|
|
1010
|
+
hint: "use providers found in your OpenCode configuration and authentication",
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
label: "Choose providers manually",
|
|
1014
|
+
value: "manual",
|
|
1015
|
+
hint: "track only the pre-configured providers you select",
|
|
1016
|
+
},
|
|
669
1017
|
],
|
|
670
1018
|
});
|
|
671
1019
|
if (prompts.isCancel(providerMode))
|
|
@@ -673,8 +1021,9 @@ async function promptForSelections(prompts) {
|
|
|
673
1021
|
let manualProviders = [];
|
|
674
1022
|
if (providerMode === "manual") {
|
|
675
1023
|
const selected = await prompts.multiselect({
|
|
676
|
-
message: "
|
|
1024
|
+
message: "Which pre-configured providers should be tracked?",
|
|
677
1025
|
required: true,
|
|
1026
|
+
initialValues: existing.manualProviders,
|
|
678
1027
|
options: getInstallerProviderPromptOptions(),
|
|
679
1028
|
});
|
|
680
1029
|
if (prompts.isCancel(selected))
|
|
@@ -684,23 +1033,29 @@ async function promptForSelections(prompts) {
|
|
|
684
1033
|
}
|
|
685
1034
|
manualProviders = selected.filter((value) => typeof value === "string");
|
|
686
1035
|
}
|
|
1036
|
+
prompts.log.info("Custom providers are configured after installation.");
|
|
1037
|
+
prompts.log.info("npx @slkiser/opencode-quota@latest provider add");
|
|
687
1038
|
const formatStyle = await prompts.select({
|
|
688
1039
|
message: "Quota reset periods",
|
|
1040
|
+
initialValue: existing.formatStyle ?? "allWindows",
|
|
689
1041
|
options: [
|
|
690
1042
|
{
|
|
691
|
-
label: "All reset periods
|
|
1043
|
+
label: "All reset periods",
|
|
692
1044
|
value: "allWindows",
|
|
1045
|
+
hint: "show every available quota window",
|
|
693
1046
|
},
|
|
694
1047
|
{
|
|
695
|
-
label: "One reset period
|
|
1048
|
+
label: "One reset period (expiring soonest)",
|
|
696
1049
|
value: "singleWindow",
|
|
1050
|
+
hint: "show only the next quota window to expire",
|
|
697
1051
|
},
|
|
698
1052
|
],
|
|
699
1053
|
});
|
|
700
1054
|
if (prompts.isCancel(formatStyle))
|
|
701
1055
|
return null;
|
|
702
1056
|
const percentDisplayMode = await prompts.select({
|
|
703
|
-
message: "
|
|
1057
|
+
message: "What should quota percentages show?",
|
|
1058
|
+
initialValue: existing.percentDisplayMode ?? "remaining",
|
|
704
1059
|
options: [
|
|
705
1060
|
{ label: "Remaining quota", value: "remaining", hint: "show how much quota is left" },
|
|
706
1061
|
{ label: "Used quota", value: "used", hint: "show how much quota has been consumed" },
|
|
@@ -709,38 +1064,54 @@ async function promptForSelections(prompts) {
|
|
|
709
1064
|
if (prompts.isCancel(percentDisplayMode))
|
|
710
1065
|
return null;
|
|
711
1066
|
const showSessionTokens = await prompts.select({
|
|
712
|
-
message: "Session
|
|
1067
|
+
message: "Session input/output tokens",
|
|
1068
|
+
initialValue: existing.showSessionTokens === true ? "yes" : "no",
|
|
713
1069
|
options: [
|
|
714
|
-
{ label: "Hide
|
|
715
|
-
{
|
|
1070
|
+
{ label: "Hide", value: "no", hint: "keep output shorter" },
|
|
1071
|
+
{
|
|
1072
|
+
label: "Show",
|
|
1073
|
+
value: "yes",
|
|
1074
|
+
hint: "include current-session input and output totals",
|
|
1075
|
+
},
|
|
716
1076
|
],
|
|
717
1077
|
});
|
|
718
1078
|
if (prompts.isCancel(showSessionTokens))
|
|
719
1079
|
return null;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1080
|
+
if (interfaces !== "web") {
|
|
1081
|
+
maintainerAnnouncements = await prompts.confirm({
|
|
1082
|
+
message: "Show maintainer announcements on the TUI Home screen when available?",
|
|
1083
|
+
initialValue: existing.maintainerAnnouncements ?? true,
|
|
1084
|
+
});
|
|
1085
|
+
if (prompts.isCancel(maintainerAnnouncements))
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
726
1088
|
return {
|
|
1089
|
+
interfaces: interfaces,
|
|
727
1090
|
scope: scope,
|
|
728
|
-
quotaUi:
|
|
1091
|
+
quotaUi: Array.isArray(quotaUi)
|
|
1092
|
+
? quotaUi.filter((value) => typeof value === "string")
|
|
1093
|
+
: ["none"],
|
|
729
1094
|
providerMode: providerMode,
|
|
730
1095
|
manualProviders,
|
|
731
1096
|
formatStyle: formatStyle,
|
|
732
1097
|
percentDisplayMode: percentDisplayMode,
|
|
733
1098
|
showSessionTokens: showSessionTokens === "yes",
|
|
734
|
-
|
|
1099
|
+
tuiCommandDisplay: interfaces === "web" ? undefined : tuiCommandDisplay,
|
|
1100
|
+
maintainerAnnouncements: interfaces === "web" ? undefined : maintainerAnnouncements !== false,
|
|
1101
|
+
configFormat: configFormat,
|
|
735
1102
|
};
|
|
736
1103
|
}
|
|
737
1104
|
export async function runInitInstaller(params) {
|
|
738
1105
|
const prompts = params?.prompts ?? (await import("@clack/prompts"));
|
|
739
1106
|
prompts.intro("Configure @slkiser/opencode-quota");
|
|
740
1107
|
try {
|
|
741
|
-
const selections = await promptForSelections(prompts
|
|
1108
|
+
const selections = await promptForSelections(prompts, {
|
|
1109
|
+
cwd: params?.cwd,
|
|
1110
|
+
env: params?.env,
|
|
1111
|
+
homeDir: params?.homeDir,
|
|
1112
|
+
});
|
|
742
1113
|
if (!selections) {
|
|
743
|
-
prompts.outro("
|
|
1114
|
+
prompts.outro("OpenCode Quota setup cancelled — no files changed.");
|
|
744
1115
|
return 0;
|
|
745
1116
|
}
|
|
746
1117
|
const plan = await planInitInstaller({
|
|
@@ -754,7 +1125,11 @@ export async function runInitInstaller(params) {
|
|
|
754
1125
|
prompts.log.info(line);
|
|
755
1126
|
}
|
|
756
1127
|
if (!plan.edits.some((edit) => edit.changed)) {
|
|
757
|
-
prompts.outro(`No
|
|
1128
|
+
prompts.outro(`OpenCode Quota is already configured and current. No files changed. ${GITHUB_STAR_NOTE}`);
|
|
1129
|
+
return 0;
|
|
1130
|
+
}
|
|
1131
|
+
if (params?.dryRun) {
|
|
1132
|
+
prompts.outro("OpenCode Quota setup preview complete — no files changed. Run npx @slkiser/opencode-quota@latest init to apply.");
|
|
758
1133
|
return 0;
|
|
759
1134
|
}
|
|
760
1135
|
const confirmed = await prompts.confirm({
|
|
@@ -762,7 +1137,7 @@ export async function runInitInstaller(params) {
|
|
|
762
1137
|
initialValue: true,
|
|
763
1138
|
});
|
|
764
1139
|
if (prompts.isCancel(confirmed) || !confirmed) {
|
|
765
|
-
prompts.outro("
|
|
1140
|
+
prompts.outro("OpenCode Quota setup cancelled — no files changed.");
|
|
766
1141
|
return 0;
|
|
767
1142
|
}
|
|
768
1143
|
const result = await applyInitInstallerPlan(plan);
|
|
@@ -778,20 +1153,33 @@ export async function runInitInstaller(params) {
|
|
|
778
1153
|
prompts.log.info(`- ${note.label}: README.md#${note.anchor}`);
|
|
779
1154
|
}
|
|
780
1155
|
}
|
|
781
|
-
|
|
1156
|
+
const interfaceLabel = plan.selections.interfaces === "tui"
|
|
1157
|
+
? "TUI"
|
|
1158
|
+
: plan.selections.interfaces === "web"
|
|
1159
|
+
? "Web"
|
|
1160
|
+
: "TUI and Web";
|
|
1161
|
+
const configuredPaths = [...result.writtenPaths, ...result.unchangedPaths];
|
|
1162
|
+
prompts.outro([
|
|
1163
|
+
"OpenCode Quota setup complete.",
|
|
1164
|
+
`Interfaces: ${interfaceLabel}`,
|
|
1165
|
+
"Configured paths:",
|
|
1166
|
+
...configuredPaths.map((path) => `- ${path}`),
|
|
1167
|
+
"Restart OpenCode and run /quota.",
|
|
1168
|
+
`If OpenCode Quota helps, please consider a star: ${GITHUB_REPO_URL}`,
|
|
1169
|
+
].join("\n"));
|
|
782
1170
|
return 0;
|
|
783
1171
|
}
|
|
784
1172
|
catch (error) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
1173
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1174
|
+
prompts.log.error(reason);
|
|
1175
|
+
const writtenPaths = error instanceof InitInstallerError ? (error.details?.writtenPaths ?? []) : [];
|
|
1176
|
+
if (writtenPaths.length > 0) {
|
|
1177
|
+
prompts.log.info(`Files changed before failure: ${writtenPaths.join(", ")}`);
|
|
1178
|
+
prompts.outro("OpenCode Quota setup failed after some files changed. Review the listed files, fix the reason above, then rerun init.");
|
|
790
1179
|
}
|
|
791
1180
|
else {
|
|
792
|
-
prompts.
|
|
1181
|
+
prompts.outro("OpenCode Quota setup failed before any files changed. Fix the reason above, then rerun init.");
|
|
793
1182
|
}
|
|
794
|
-
prompts.outro("Quota init failed");
|
|
795
1183
|
return 1;
|
|
796
1184
|
}
|
|
797
1185
|
}
|