@slkiser/opencode-quota 3.11.2 → 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 +90 -62
- package/dist/bin/opencode-quota.d.ts.map +1 -1
- package/dist/bin/opencode-quota.js +16 -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/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 +524 -158
- package/dist/lib/init-installer.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.map +1 -1
- package/dist/lib/scoped-update.js +32 -22
- package/dist/lib/scoped-update.js.map +1 -1
- 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 +6 -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 +345 -41
- package/dist/tui.tsx +267 -18
- package/package.json +28 -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";
|
|
12
|
+
import { getQuotaToastConfigPath, QUOTA_TOAST_CONFIG_RELATIVE_PATH, QUOTA_TOAST_CONFIG_RELATIVE_PATHS, } from "./config.js";
|
|
11
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,44 +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 = ["
|
|
36
|
+
const QUOTA_UI_CHOICE_ORDER = ["sidebar", "toast", "compact_status", "none"];
|
|
34
37
|
function normalizeQuotaUiIntent(selections) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
42
49
|
const seen = new Set();
|
|
43
|
-
for (const rawChoice of
|
|
50
|
+
for (const rawChoice of quotaUi) {
|
|
44
51
|
if (typeof rawChoice !== "string" ||
|
|
45
52
|
!QUOTA_UI_CHOICE_ORDER.includes(rawChoice)) {
|
|
46
53
|
throw new InitInstallerError(`Unknown Quota UI option: ${String(rawChoice)}`);
|
|
47
54
|
}
|
|
48
55
|
seen.add(rawChoice);
|
|
49
56
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (legacyCompactMode !== "home_bottom" && legacyCompactMode !== "home_bottom_session_prompt") {
|
|
53
|
-
throw new InitInstallerError(`Unknown Compact TUI status: ${String(legacyCompactMode)}`);
|
|
54
|
-
}
|
|
55
|
-
seen.delete("none");
|
|
56
|
-
seen.add("compact_status");
|
|
57
|
-
}
|
|
58
|
-
let choices = QUOTA_UI_CHOICE_ORDER.filter((choice) => seen.has(choice));
|
|
59
|
-
if (choices.length === 0) {
|
|
60
|
-
choices = ["none"];
|
|
61
|
-
}
|
|
62
|
-
else if (choices.length > 1 && choices.includes("none")) {
|
|
63
|
-
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.");
|
|
64
59
|
}
|
|
60
|
+
const choices = QUOTA_UI_CHOICE_ORDER.filter((choice) => seen.has(choice));
|
|
65
61
|
const enableSidebarPanel = choices.includes("sidebar");
|
|
66
62
|
const enableCompactStatus = choices.includes("compact_status");
|
|
67
63
|
return {
|
|
68
64
|
choices,
|
|
69
65
|
enableToast: choices.includes("toast"),
|
|
70
|
-
installTuiPlugin:
|
|
66
|
+
installTuiPlugin: selections.interfaces !== "web",
|
|
71
67
|
enableSidebarPanel,
|
|
72
68
|
enableCompactStatus,
|
|
73
69
|
};
|
|
@@ -80,7 +76,7 @@ function getUiLabel(choices) {
|
|
|
80
76
|
return "Sidebar";
|
|
81
77
|
if (choice === "compact_status")
|
|
82
78
|
return "Compact status";
|
|
83
|
-
return "
|
|
79
|
+
return "Manual commands only";
|
|
84
80
|
});
|
|
85
81
|
return labels.join(" + ");
|
|
86
82
|
}
|
|
@@ -90,13 +86,6 @@ function getProviderModeLabel(mode) {
|
|
|
90
86
|
function getPercentDisplayModeLabel(mode) {
|
|
91
87
|
return mode === "used" ? "Used" : "Remaining";
|
|
92
88
|
}
|
|
93
|
-
function getTuiCompactStatusLabel(mode) {
|
|
94
|
-
if (mode === "home_bottom")
|
|
95
|
-
return "Home bottom only";
|
|
96
|
-
if (mode === "home_bottom_session_prompt")
|
|
97
|
-
return "Home bottom + session prompt";
|
|
98
|
-
return "Off";
|
|
99
|
-
}
|
|
100
89
|
function resolveRequestedProviders(selections) {
|
|
101
90
|
if (selections.providerMode === "auto") {
|
|
102
91
|
return "auto";
|
|
@@ -199,9 +188,11 @@ function setInstallerOwnedSetting(target, key, value, pathLabel, edit) {
|
|
|
199
188
|
return;
|
|
200
189
|
}
|
|
201
190
|
if (!jsonEqual(target[key], value)) {
|
|
191
|
+
const previousValue = target[key];
|
|
202
192
|
target[key] = value;
|
|
203
193
|
edit.changed = true;
|
|
204
194
|
edit.updatedKeys.push(pathLabel);
|
|
195
|
+
edit.valueChanges.push(`${pathLabel}: ${JSON.stringify(previousValue)} -> ${JSON.stringify(value)}`);
|
|
205
196
|
}
|
|
206
197
|
}
|
|
207
198
|
function planTuiSidebarPanelConfig(params) {
|
|
@@ -300,7 +291,10 @@ async function readLegacyQuotaToastSeed(baseDir) {
|
|
|
300
291
|
if (!target.existed) {
|
|
301
292
|
return null;
|
|
302
293
|
}
|
|
303
|
-
const root = await readExistingConfig(
|
|
294
|
+
const root = await readExistingConfig({
|
|
295
|
+
path: target.sourcePath,
|
|
296
|
+
format: target.sourcePath.endsWith(".jsonc") ? "jsonc" : "json",
|
|
297
|
+
});
|
|
304
298
|
const experimental = isPlainObject(root.experimental) ? root.experimental : null;
|
|
305
299
|
const quotaToast = experimental && isPlainObject(experimental.quotaToast) ? experimental.quotaToast : null;
|
|
306
300
|
return quotaToast ? cloneJsonObject(quotaToast) : null;
|
|
@@ -361,7 +355,12 @@ function syncLegacyQuotaToast(params) {
|
|
|
361
355
|
}
|
|
362
356
|
}
|
|
363
357
|
async function planOpencodeEdit(params) {
|
|
364
|
-
const target = resolveEditableConfigPath({
|
|
358
|
+
const target = resolveEditableConfigPath({
|
|
359
|
+
dir: params.baseDir,
|
|
360
|
+
kind: "opencode",
|
|
361
|
+
preferredFormat: params.selections.configFormat ?? "jsonc",
|
|
362
|
+
convertJsonToJsonc: true,
|
|
363
|
+
});
|
|
365
364
|
const edit = {
|
|
366
365
|
kind: "opencode",
|
|
367
366
|
path: target.path,
|
|
@@ -371,13 +370,19 @@ async function planOpencodeEdit(params) {
|
|
|
371
370
|
addedPlugins: [],
|
|
372
371
|
addedKeys: [],
|
|
373
372
|
updatedKeys: [],
|
|
373
|
+
valueChanges: [],
|
|
374
374
|
skippedValues: [],
|
|
375
|
-
warnings:
|
|
376
|
-
? ["Existing JSONC comments/trailing commas will be stripped."]
|
|
377
|
-
: [],
|
|
375
|
+
warnings: [],
|
|
378
376
|
};
|
|
379
|
-
const root = target.existed
|
|
380
|
-
|
|
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
|
+
}
|
|
381
386
|
const plugin = ensureTopLevelPluginArray(root, edit);
|
|
382
387
|
appendQuotaPluginIfMissing({
|
|
383
388
|
container: plugin,
|
|
@@ -392,46 +397,109 @@ async function planOpencodeEdit(params) {
|
|
|
392
397
|
edit,
|
|
393
398
|
});
|
|
394
399
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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;
|
|
398
416
|
return edit;
|
|
399
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
|
+
}
|
|
400
454
|
async function planQuotaConfigEdit(params) {
|
|
401
|
-
const
|
|
402
|
-
const existed = existsSync(path);
|
|
455
|
+
const target = resolveQuotaConfigTarget(params.baseDir, params.selections.configFormat ?? "jsonc");
|
|
403
456
|
const edit = {
|
|
404
457
|
kind: "quota",
|
|
405
|
-
path,
|
|
406
|
-
existed,
|
|
407
|
-
format:
|
|
458
|
+
path: target.path,
|
|
459
|
+
existed: target.existed,
|
|
460
|
+
format: target.format,
|
|
408
461
|
changed: false,
|
|
409
462
|
addedPlugins: [],
|
|
410
463
|
addedKeys: [],
|
|
411
464
|
updatedKeys: [],
|
|
465
|
+
valueChanges: [],
|
|
412
466
|
skippedValues: [],
|
|
413
467
|
warnings: [],
|
|
414
468
|
};
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
})
|
|
418
479
|
: legacyQuotaToast
|
|
419
480
|
? cloneJsonObject(legacyQuotaToast)
|
|
420
481
|
: {};
|
|
421
|
-
if (!existed) {
|
|
422
|
-
edit.changed = true;
|
|
482
|
+
if (!target.existed) {
|
|
423
483
|
edit.addedKeys.push(legacyQuotaToast
|
|
424
|
-
? `${QUOTA_TOAST_CONFIG_RELATIVE_PATH} (seeded from experimental.quotaToast)`
|
|
425
|
-
:
|
|
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);
|
|
426
488
|
}
|
|
427
489
|
setInstallerOwnedSetting(quotaToast, "enableToast", params.quotaUiIntent.enableToast, "quotaToast.enableToast", edit);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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);
|
|
435
503
|
planTuiSidebarPanelConfig({
|
|
436
504
|
quotaToast,
|
|
437
505
|
quotaUiIntent: params.quotaUiIntent,
|
|
@@ -447,14 +515,86 @@ async function planQuotaConfigEdit(params) {
|
|
|
447
515
|
selections: params.selections,
|
|
448
516
|
edit,
|
|
449
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;
|
|
450
550
|
edit.plannedData = quotaToast;
|
|
451
|
-
if (edit.changed) {
|
|
452
|
-
edit.nextData = quotaToast;
|
|
453
|
-
}
|
|
454
551
|
return edit;
|
|
455
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
|
+
}
|
|
456
591
|
async function planTuiEdit(params) {
|
|
457
|
-
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
|
+
});
|
|
458
598
|
const edit = {
|
|
459
599
|
kind: "tui",
|
|
460
600
|
path: target.path,
|
|
@@ -464,54 +604,95 @@ async function planTuiEdit(params) {
|
|
|
464
604
|
addedPlugins: [],
|
|
465
605
|
addedKeys: [],
|
|
466
606
|
updatedKeys: [],
|
|
607
|
+
valueChanges: [],
|
|
467
608
|
skippedValues: [],
|
|
468
|
-
warnings:
|
|
469
|
-
? ["Existing JSONC comments/trailing commas will be stripped."]
|
|
470
|
-
: [],
|
|
609
|
+
warnings: [],
|
|
471
610
|
};
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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);
|
|
477
622
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
appendQuotaPluginIfMissing({
|
|
481
|
-
container: pluginTarget.container,
|
|
482
|
-
pathLabel: pluginTarget.pathLabel,
|
|
483
|
-
kind: "tui",
|
|
484
|
-
edit,
|
|
485
|
-
});
|
|
623
|
+
if (params.selections.interfaces === "web") {
|
|
624
|
+
removeQuotaPluginsFromTui(root, edit);
|
|
486
625
|
}
|
|
487
|
-
|
|
488
|
-
|
|
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
|
+
}
|
|
489
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;
|
|
490
659
|
return edit;
|
|
491
660
|
}
|
|
492
661
|
function buildPlanSummary(plan) {
|
|
493
662
|
const quotaUiIntent = normalizeQuotaUiIntent(plan.selections);
|
|
663
|
+
const opencodeFormat = plan.edits.find((edit) => edit.kind === "opencode")?.format ??
|
|
664
|
+
plan.selections.configFormat ??
|
|
665
|
+
"jsonc";
|
|
494
666
|
const lines = [
|
|
667
|
+
`Interface: ${plan.selections.interfaces === "tui" ? "TUI" : plan.selections.interfaces === "web" ? "Web" : "Both"}`,
|
|
495
668
|
`Scope: ${plan.selections.scope} (${plan.baseDir})`,
|
|
496
|
-
`
|
|
497
|
-
`Provider mode: ${getProviderModeLabel(plan.selections.providerMode)}`,
|
|
498
|
-
`Quota reset periods: ${getQuotaFormatStyleLabel(plan.selections.formatStyle)}`,
|
|
499
|
-
`Quota percentage meaning: ${getPercentDisplayModeLabel(plan.selections.percentDisplayMode)}`,
|
|
500
|
-
`Session token details: ${plan.selections.showSessionTokens ? "Show" : "Hide"}`,
|
|
669
|
+
`Config format: ${opencodeFormat.toUpperCase()}`,
|
|
501
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"}`);
|
|
502
676
|
if (plan.selections.maintainerAnnouncements !== undefined) {
|
|
503
677
|
lines.push(`Maintainer announcements: ${plan.selections.maintainerAnnouncements ? "Enabled" : "Disabled"}`);
|
|
504
678
|
}
|
|
505
679
|
if (quotaUiIntent.enableCompactStatus) {
|
|
506
|
-
lines.push(
|
|
680
|
+
lines.push("Compact status mode: Home bottom + session prompt");
|
|
507
681
|
}
|
|
508
682
|
const requestedProviders = resolveRequestedProviders(plan.selections);
|
|
509
683
|
if (requestedProviders !== "auto") {
|
|
510
684
|
lines.push(`Manual providers: ${requestedProviders.map((providerId) => getQuotaProviderDisplayLabel(providerId)).join(", ")}`);
|
|
511
685
|
}
|
|
512
686
|
for (const edit of plan.edits) {
|
|
513
|
-
const
|
|
514
|
-
|
|
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}`);
|
|
515
696
|
for (const plugin of edit.addedPlugins) {
|
|
516
697
|
lines.push(` + plugin ${plugin}`);
|
|
517
698
|
}
|
|
@@ -521,6 +702,9 @@ function buildPlanSummary(plan) {
|
|
|
521
702
|
for (const key of edit.updatedKeys) {
|
|
522
703
|
lines.push(` ~ ${key}`);
|
|
523
704
|
}
|
|
705
|
+
for (const change of edit.valueChanges) {
|
|
706
|
+
lines.push(` ${change}`);
|
|
707
|
+
}
|
|
524
708
|
for (const skipped of edit.skippedValues) {
|
|
525
709
|
lines.push(` = ${skipped}`);
|
|
526
710
|
}
|
|
@@ -562,10 +746,18 @@ export function resolveInitInstallerBaseDir(params) {
|
|
|
562
746
|
return findGitWorktreeRoot(cwd) ?? cwd;
|
|
563
747
|
}
|
|
564
748
|
export async function planInitInstaller(params) {
|
|
565
|
-
const
|
|
566
|
-
const selections = {
|
|
749
|
+
const requestedSelections = {
|
|
567
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",
|
|
568
757
|
quotaUi: quotaUiIntent.choices,
|
|
758
|
+
tuiCommandDisplay: params.selections.interfaces === "web"
|
|
759
|
+
? undefined
|
|
760
|
+
: (params.selections.tuiCommandDisplay ?? "inline"),
|
|
569
761
|
maintainerAnnouncements: params.selections.maintainerAnnouncements,
|
|
570
762
|
manualProviders: params.selections.providerMode === "manual"
|
|
571
763
|
? resolveRequestedProviders(params.selections)
|
|
@@ -586,8 +778,9 @@ export async function planInitInstaller(params) {
|
|
|
586
778
|
}),
|
|
587
779
|
quotaEdit,
|
|
588
780
|
];
|
|
589
|
-
|
|
590
|
-
|
|
781
|
+
const tuiEdit = await planTuiEdit({ selections, baseDir });
|
|
782
|
+
if (selections.interfaces !== "web" || tuiEdit.existed) {
|
|
783
|
+
edits.push(tuiEdit);
|
|
591
784
|
}
|
|
592
785
|
const quickSetupNotes = buildQuickSetupNotes(selections);
|
|
593
786
|
const warnings = edits.flatMap((edit) => edit.warnings);
|
|
@@ -605,13 +798,30 @@ export async function planInitInstaller(params) {
|
|
|
605
798
|
export async function applyInitInstallerPlan(plan) {
|
|
606
799
|
const writtenPaths = [];
|
|
607
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
|
+
}
|
|
608
813
|
for (const edit of plan.edits) {
|
|
609
|
-
if (!edit.changed || !edit.nextData) {
|
|
814
|
+
if (!edit.changed || (!edit.nextData && !edit.documentEdit)) {
|
|
610
815
|
unchangedPaths.push(edit.path);
|
|
611
816
|
continue;
|
|
612
817
|
}
|
|
613
818
|
try {
|
|
614
|
-
|
|
819
|
+
if (edit.documentEdit) {
|
|
820
|
+
await applyConfigDocumentEdit(edit.documentEdit);
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
await writeJsonAtomic(edit.path, edit.nextData, { trailingNewline: true });
|
|
824
|
+
}
|
|
615
825
|
writtenPaths.push(edit.path);
|
|
616
826
|
}
|
|
617
827
|
catch (error) {
|
|
@@ -626,63 +836,183 @@ export async function applyInitInstallerPlan(plan) {
|
|
|
626
836
|
unchangedPaths,
|
|
627
837
|
};
|
|
628
838
|
}
|
|
629
|
-
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
|
+
}
|
|
630
912
|
const scope = await prompts.select({
|
|
631
|
-
message: "
|
|
913
|
+
message: "Where should OpenCode Quota be configured?",
|
|
914
|
+
initialValue: "global",
|
|
632
915
|
options: [
|
|
633
|
-
{ label: "Project config", value: "project", hint: "install only for this repo/worktree" },
|
|
634
916
|
{
|
|
635
|
-
label: "Global OpenCode config",
|
|
917
|
+
label: "Global OpenCode config (recommended)",
|
|
636
918
|
value: "global",
|
|
637
919
|
hint: "install for all projects using your global config",
|
|
638
920
|
},
|
|
921
|
+
{ label: "Project config", value: "project", hint: "install only for this repo/worktree" },
|
|
639
922
|
],
|
|
640
923
|
});
|
|
641
924
|
if (prompts.isCancel(scope))
|
|
642
925
|
return null;
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
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",
|
|
646
936
|
options: [
|
|
647
937
|
{
|
|
648
|
-
label: "
|
|
649
|
-
value: "
|
|
650
|
-
hint: "
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
label: "Sidebar panel",
|
|
654
|
-
value: "sidebar",
|
|
655
|
-
hint: "full Quota panel in the OpenCode session sidebar",
|
|
656
|
-
},
|
|
657
|
-
{
|
|
658
|
-
label: "Compact status line",
|
|
659
|
-
value: "compact_status",
|
|
660
|
-
hint: "short quota summary in the TUI status area",
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
label: "No automatic UI surfaces",
|
|
664
|
-
value: "none",
|
|
665
|
-
hint: "no toast, sidebar, compact status, or TUI dialogs; server slash commands stay installed",
|
|
938
|
+
label: "JSONC (recommended)",
|
|
939
|
+
value: "jsonc",
|
|
940
|
+
hint: "keeps useful section comments and trailing commas",
|
|
666
941
|
},
|
|
942
|
+
{ label: "JSON", value: "json", hint: "strict JSON without comments" },
|
|
667
943
|
],
|
|
668
944
|
});
|
|
669
|
-
if (prompts.isCancel(
|
|
945
|
+
if (prompts.isCancel(configFormat))
|
|
670
946
|
return null;
|
|
671
|
-
|
|
672
|
-
|
|
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;
|
|
673
1002
|
}
|
|
674
1003
|
const providerMode = await prompts.select({
|
|
675
|
-
message: "
|
|
1004
|
+
message: "How should pre-configured providers be selected?",
|
|
1005
|
+
initialValue: existing.providerMode ?? "auto",
|
|
676
1006
|
options: [
|
|
677
1007
|
{
|
|
678
1008
|
label: "Auto-detect providers",
|
|
679
1009
|
value: "auto",
|
|
680
|
-
hint: "
|
|
1010
|
+
hint: "use providers found in your OpenCode configuration and authentication",
|
|
681
1011
|
},
|
|
682
1012
|
{
|
|
683
1013
|
label: "Choose providers manually",
|
|
684
1014
|
value: "manual",
|
|
685
|
-
hint: "only
|
|
1015
|
+
hint: "track only the pre-configured providers you select",
|
|
686
1016
|
},
|
|
687
1017
|
],
|
|
688
1018
|
});
|
|
@@ -691,8 +1021,9 @@ async function promptForSelections(prompts) {
|
|
|
691
1021
|
let manualProviders = [];
|
|
692
1022
|
if (providerMode === "manual") {
|
|
693
1023
|
const selected = await prompts.multiselect({
|
|
694
|
-
message: "
|
|
1024
|
+
message: "Which pre-configured providers should be tracked?",
|
|
695
1025
|
required: true,
|
|
1026
|
+
initialValues: existing.manualProviders,
|
|
696
1027
|
options: getInstallerProviderPromptOptions(),
|
|
697
1028
|
});
|
|
698
1029
|
if (prompts.isCancel(selected))
|
|
@@ -702,23 +1033,29 @@ async function promptForSelections(prompts) {
|
|
|
702
1033
|
}
|
|
703
1034
|
manualProviders = selected.filter((value) => typeof value === "string");
|
|
704
1035
|
}
|
|
1036
|
+
prompts.log.info("Custom providers are configured after installation.");
|
|
1037
|
+
prompts.log.info("npx @slkiser/opencode-quota@latest provider add");
|
|
705
1038
|
const formatStyle = await prompts.select({
|
|
706
1039
|
message: "Quota reset periods",
|
|
1040
|
+
initialValue: existing.formatStyle ?? "allWindows",
|
|
707
1041
|
options: [
|
|
708
1042
|
{
|
|
709
|
-
label: "All reset periods
|
|
1043
|
+
label: "All reset periods",
|
|
710
1044
|
value: "allWindows",
|
|
1045
|
+
hint: "show every available quota window",
|
|
711
1046
|
},
|
|
712
1047
|
{
|
|
713
|
-
label: "One reset period
|
|
1048
|
+
label: "One reset period (expiring soonest)",
|
|
714
1049
|
value: "singleWindow",
|
|
1050
|
+
hint: "show only the next quota window to expire",
|
|
715
1051
|
},
|
|
716
1052
|
],
|
|
717
1053
|
});
|
|
718
1054
|
if (prompts.isCancel(formatStyle))
|
|
719
1055
|
return null;
|
|
720
1056
|
const percentDisplayMode = await prompts.select({
|
|
721
|
-
message: "
|
|
1057
|
+
message: "What should quota percentages show?",
|
|
1058
|
+
initialValue: existing.percentDisplayMode ?? "remaining",
|
|
722
1059
|
options: [
|
|
723
1060
|
{ label: "Remaining quota", value: "remaining", hint: "show how much quota is left" },
|
|
724
1061
|
{ label: "Used quota", value: "used", hint: "show how much quota has been consumed" },
|
|
@@ -727,42 +1064,54 @@ async function promptForSelections(prompts) {
|
|
|
727
1064
|
if (prompts.isCancel(percentDisplayMode))
|
|
728
1065
|
return null;
|
|
729
1066
|
const showSessionTokens = await prompts.select({
|
|
730
|
-
message: "Session
|
|
1067
|
+
message: "Session input/output tokens",
|
|
1068
|
+
initialValue: existing.showSessionTokens === true ? "yes" : "no",
|
|
731
1069
|
options: [
|
|
732
|
-
{ label: "Hide
|
|
1070
|
+
{ label: "Hide", value: "no", hint: "keep output shorter" },
|
|
733
1071
|
{
|
|
734
|
-
label: "Show
|
|
1072
|
+
label: "Show",
|
|
735
1073
|
value: "yes",
|
|
736
|
-
hint: "include current
|
|
1074
|
+
hint: "include current-session input and output totals",
|
|
737
1075
|
},
|
|
738
1076
|
],
|
|
739
1077
|
});
|
|
740
1078
|
if (prompts.isCancel(showSessionTokens))
|
|
741
1079
|
return null;
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
+
}
|
|
748
1088
|
return {
|
|
1089
|
+
interfaces: interfaces,
|
|
749
1090
|
scope: scope,
|
|
750
|
-
quotaUi:
|
|
1091
|
+
quotaUi: Array.isArray(quotaUi)
|
|
1092
|
+
? quotaUi.filter((value) => typeof value === "string")
|
|
1093
|
+
: ["none"],
|
|
751
1094
|
providerMode: providerMode,
|
|
752
1095
|
manualProviders,
|
|
753
1096
|
formatStyle: formatStyle,
|
|
754
1097
|
percentDisplayMode: percentDisplayMode,
|
|
755
1098
|
showSessionTokens: showSessionTokens === "yes",
|
|
756
|
-
|
|
1099
|
+
tuiCommandDisplay: interfaces === "web" ? undefined : tuiCommandDisplay,
|
|
1100
|
+
maintainerAnnouncements: interfaces === "web" ? undefined : maintainerAnnouncements !== false,
|
|
1101
|
+
configFormat: configFormat,
|
|
757
1102
|
};
|
|
758
1103
|
}
|
|
759
1104
|
export async function runInitInstaller(params) {
|
|
760
1105
|
const prompts = params?.prompts ?? (await import("@clack/prompts"));
|
|
761
1106
|
prompts.intro("Configure @slkiser/opencode-quota");
|
|
762
1107
|
try {
|
|
763
|
-
const selections = await promptForSelections(prompts
|
|
1108
|
+
const selections = await promptForSelections(prompts, {
|
|
1109
|
+
cwd: params?.cwd,
|
|
1110
|
+
env: params?.env,
|
|
1111
|
+
homeDir: params?.homeDir,
|
|
1112
|
+
});
|
|
764
1113
|
if (!selections) {
|
|
765
|
-
prompts.outro("
|
|
1114
|
+
prompts.outro("OpenCode Quota setup cancelled — no files changed.");
|
|
766
1115
|
return 0;
|
|
767
1116
|
}
|
|
768
1117
|
const plan = await planInitInstaller({
|
|
@@ -776,7 +1125,11 @@ export async function runInitInstaller(params) {
|
|
|
776
1125
|
prompts.log.info(line);
|
|
777
1126
|
}
|
|
778
1127
|
if (!plan.edits.some((edit) => edit.changed)) {
|
|
779
|
-
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.");
|
|
780
1133
|
return 0;
|
|
781
1134
|
}
|
|
782
1135
|
const confirmed = await prompts.confirm({
|
|
@@ -784,7 +1137,7 @@ export async function runInitInstaller(params) {
|
|
|
784
1137
|
initialValue: true,
|
|
785
1138
|
});
|
|
786
1139
|
if (prompts.isCancel(confirmed) || !confirmed) {
|
|
787
|
-
prompts.outro("
|
|
1140
|
+
prompts.outro("OpenCode Quota setup cancelled — no files changed.");
|
|
788
1141
|
return 0;
|
|
789
1142
|
}
|
|
790
1143
|
const result = await applyInitInstallerPlan(plan);
|
|
@@ -800,20 +1153,33 @@ export async function runInitInstaller(params) {
|
|
|
800
1153
|
prompts.log.info(`- ${note.label}: README.md#${note.anchor}`);
|
|
801
1154
|
}
|
|
802
1155
|
}
|
|
803
|
-
|
|
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"));
|
|
804
1170
|
return 0;
|
|
805
1171
|
}
|
|
806
1172
|
catch (error) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
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.");
|
|
812
1179
|
}
|
|
813
1180
|
else {
|
|
814
|
-
prompts.
|
|
1181
|
+
prompts.outro("OpenCode Quota setup failed before any files changed. Fix the reason above, then rerun init.");
|
|
815
1182
|
}
|
|
816
|
-
prompts.outro("Quota init failed");
|
|
817
1183
|
return 1;
|
|
818
1184
|
}
|
|
819
1185
|
}
|