cc-hub 0.1.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.
Files changed (41) hide show
  1. package/README.md +92 -0
  2. package/README.zh-CN.md +92 -0
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +6 -0
  5. package/dist/components/ui/modal.d.ts +10 -0
  6. package/dist/components/ui/modal.js +11 -0
  7. package/dist/components/ui/status-bar.d.ts +15 -0
  8. package/dist/components/ui/status-bar.js +6 -0
  9. package/dist/components/ui/tab-bar.d.ts +14 -0
  10. package/dist/components/ui/tab-bar.js +10 -0
  11. package/dist/components/ui/table.d.ts +36 -0
  12. package/dist/components/ui/table.js +79 -0
  13. package/dist/config/presets.d.ts +2 -0
  14. package/dist/config/presets.js +57 -0
  15. package/dist/store/claude-config.d.ts +5 -0
  16. package/dist/store/claude-config.js +79 -0
  17. package/dist/store/config-store.d.ts +15 -0
  18. package/dist/store/config-store.js +138 -0
  19. package/dist/store/model-store.d.ts +8 -0
  20. package/dist/store/model-store.js +76 -0
  21. package/dist/test-escape-compiled.d.ts +1 -0
  22. package/dist/test-escape-compiled.js +17 -0
  23. package/dist/tui/add-model.d.ts +7 -0
  24. package/dist/tui/add-model.js +102 -0
  25. package/dist/tui/add-provider.d.ts +13 -0
  26. package/dist/tui/add-provider.js +185 -0
  27. package/dist/tui/app.d.ts +1 -0
  28. package/dist/tui/app.js +163 -0
  29. package/dist/tui/confirm.d.ts +7 -0
  30. package/dist/tui/confirm.js +5 -0
  31. package/dist/tui/dashboard.d.ts +10 -0
  32. package/dist/tui/dashboard.js +45 -0
  33. package/dist/tui/edit-model.d.ts +8 -0
  34. package/dist/tui/edit-model.js +54 -0
  35. package/dist/tui/edit-provider.d.ts +12 -0
  36. package/dist/tui/edit-provider.js +140 -0
  37. package/dist/tui/scenario-config.d.ts +8 -0
  38. package/dist/tui/scenario-config.js +87 -0
  39. package/dist/types.d.ts +19 -0
  40. package/dist/types.js +2 -0
  41. package/package.json +45 -0
@@ -0,0 +1,163 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ // src/tui/app.tsx
3
+ import React from "react";
4
+ import { useApp, useInput, Box, Text } from "ink";
5
+ import { loadConfig, saveConfig, setActiveModel, findModel, updateScenarioModels, removeModelFromProvider, getConfigError, clearConfigError, } from "../store/config-store.js";
6
+ import { activateModel } from "../store/claude-config.js";
7
+ import { Dashboard } from "./dashboard.js";
8
+ import { ScenarioConfig } from "./scenario-config.js";
9
+ import { Modal } from "../components/ui/modal.js";
10
+ export function App() {
11
+ const { exit } = useApp();
12
+ const [store, setStore] = React.useState(() => loadConfig());
13
+ const [screen, setScreen] = React.useState({ type: "dashboard" });
14
+ const [statusMessage, setStatusMessage] = React.useState(() => getConfigError());
15
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
16
+ // Auto-clear status message
17
+ React.useEffect(() => {
18
+ if (!statusMessage)
19
+ return;
20
+ const timer = setTimeout(() => setStatusMessage(null), 3000);
21
+ return () => clearTimeout(timer);
22
+ }, [statusMessage]);
23
+ React.useEffect(() => {
24
+ clearConfigError();
25
+ }, []);
26
+ // Model-only rows for navigation (no headers)
27
+ function getModelRows() {
28
+ return store.providers.flatMap((p) => p.models.map((m) => ({ providerId: p.id, modelId: m })));
29
+ }
30
+ React.useEffect(() => {
31
+ const n = getModelRows().length;
32
+ if (n === 0)
33
+ return;
34
+ setSelectedIndex((prev) => Math.max(0, Math.min(prev, n - 1)));
35
+ }, [store.providers.length]);
36
+ // --- Action handlers ---
37
+ const handleSelect = React.useCallback((modelId) => {
38
+ const updated = setActiveModel(store, modelId);
39
+ // Always sync scenarioModels to the selected model
40
+ const withScenarios = updateScenarioModels(updated, {
41
+ opusModelId: modelId,
42
+ sonnetModelId: modelId,
43
+ haikuModelId: modelId,
44
+ subagentModelId: modelId,
45
+ });
46
+ setStore(withScenarios);
47
+ saveConfig(withScenarios);
48
+ const resolved = findModel(withScenarios, modelId);
49
+ if (resolved) {
50
+ activateModel(withScenarios, modelId, withScenarios.scenarioModels);
51
+ }
52
+ }, [store]);
53
+ const handleDelete = React.useCallback((providerId, modelId) => {
54
+ const provider = store.providers.find((p) => p.id === providerId);
55
+ if (!provider || !provider.models.includes(modelId))
56
+ return;
57
+ setScreen({
58
+ type: "confirm",
59
+ message: `Delete model "${modelId}"?`,
60
+ onConfirm: () => {
61
+ let updated = removeModelFromProvider(store, providerId, modelId);
62
+ // Clean up scenarioModels references to the deleted model
63
+ const sm = updated.scenarioModels;
64
+ if (sm.opusModelId === modelId ||
65
+ sm.sonnetModelId === modelId ||
66
+ sm.haikuModelId === modelId ||
67
+ sm.subagentModelId === modelId) {
68
+ updated = {
69
+ ...updated,
70
+ scenarioModels: {
71
+ opusModelId: sm.opusModelId === modelId ? undefined : sm.opusModelId,
72
+ sonnetModelId: sm.sonnetModelId === modelId ? undefined : sm.sonnetModelId,
73
+ haikuModelId: sm.haikuModelId === modelId ? undefined : sm.haikuModelId,
74
+ subagentModelId: sm.subagentModelId === modelId
75
+ ? undefined
76
+ : sm.subagentModelId,
77
+ },
78
+ };
79
+ }
80
+ setStore(updated);
81
+ saveConfig(updated);
82
+ setScreen({ type: "dashboard" });
83
+ setStatusMessage(`Deleted ${modelId}`);
84
+ },
85
+ });
86
+ }, [store]);
87
+ const handleConfirmCancel = React.useCallback(() => setScreen({ type: "dashboard" }), []);
88
+ const handleScenarioSave = React.useCallback((updates) => {
89
+ const updated = {
90
+ ...store,
91
+ scenarioModels: updates,
92
+ };
93
+ setStore(updated);
94
+ saveConfig(updated);
95
+ // Stay on scenario page, only Esc goes back to dashboard
96
+ if (updated.activeModelId) {
97
+ activateModel(updated, updated.activeModelId, updated.scenarioModels);
98
+ }
99
+ }, [store]);
100
+ const handleScenarioCancel = React.useCallback(() => setScreen({ type: "dashboard" }), []);
101
+ // --- Centralized keyboard handling ---
102
+ useInput((input, key) => {
103
+ if (input === "q") {
104
+ exit();
105
+ return;
106
+ }
107
+ switch (screen.type) {
108
+ case "dashboard": {
109
+ if (input === "s") {
110
+ setScreen({ type: "scenario" });
111
+ return;
112
+ }
113
+ if (key.escape) {
114
+ exit();
115
+ return;
116
+ }
117
+ if (key.upArrow || key.downArrow) {
118
+ const models = getModelRows();
119
+ if (models.length === 0)
120
+ return;
121
+ const nextPos = key.upArrow
122
+ ? (selectedIndex - 1 + models.length) % models.length
123
+ : (selectedIndex + 1) % models.length;
124
+ setSelectedIndex(nextPos);
125
+ return;
126
+ }
127
+ if (key.return) {
128
+ const models = getModelRows();
129
+ const row = models[selectedIndex];
130
+ if (row)
131
+ handleSelect(row.modelId);
132
+ return;
133
+ }
134
+ if (input === "d") {
135
+ const models = getModelRows();
136
+ const row = models[selectedIndex];
137
+ if (row) {
138
+ handleDelete(row.providerId, row.modelId);
139
+ }
140
+ return;
141
+ }
142
+ return;
143
+ }
144
+ case "scenario": {
145
+ // ScenarioConfig handles its own keyboard input (↑↓←→Enter Esc)
146
+ return;
147
+ }
148
+ case "confirm": {
149
+ if (input === "y" || input === "Y") {
150
+ screen.onConfirm();
151
+ return;
152
+ }
153
+ if (input === "n" || input === "N" || input === "escape") {
154
+ setScreen({ type: "dashboard" });
155
+ return;
156
+ }
157
+ return;
158
+ }
159
+ }
160
+ });
161
+ // --- Render ---
162
+ return (_jsxs(Box, { flexDirection: "column", children: [statusMessage && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "green", children: ["\u2713 ", statusMessage] }) })), screen.type === "dashboard" && (_jsx(Dashboard, { store: store, selectedIndex: selectedIndex, onSelect: handleSelect, onDelete: handleDelete, onScenario: () => setScreen({ type: "scenario" }) })), screen.type === "scenario" && (_jsx(ScenarioConfig, { store: store, onSave: handleScenarioSave, onCancel: handleScenarioCancel })), screen.type === "confirm" && (_jsx(Modal, { title: "Confirm", borderColor: "yellow", borderStyle: "round", onClose: () => setScreen({ type: "dashboard" }), children: _jsxs(Box, { paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { children: screen.message }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsxs(Text, { children: [_jsxs(Text, { inverse: true, bold: true, children: [" ", "Y", " "] }), _jsx(Text, { dimColor: true, children: " Yes" })] }), _jsxs(Text, { children: [_jsxs(Text, { inverse: true, bold: true, children: [" ", "N", " "] }), _jsx(Text, { dimColor: true, children: " No" })] }), _jsxs(Text, { children: [_jsxs(Text, { inverse: true, bold: true, children: [" ", "Esc", " "] }), _jsx(Text, { dimColor: true, children: " Cancel" })] })] })] }) }))] }));
163
+ }
@@ -0,0 +1,7 @@
1
+ interface ConfirmProps {
2
+ message: string;
3
+ onConfirm: () => void;
4
+ onCancel: () => void;
5
+ }
6
+ export declare function Confirm({ message, onConfirm, onCancel }: ConfirmProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ export function Confirm({ message, onConfirm, onCancel }) {
4
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { children: message }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "[Y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: "red", children: "[N]" }), _jsx(Text, { children: "o" })] })] }));
5
+ }
@@ -0,0 +1,10 @@
1
+ import { ConfigStore } from "../types.js";
2
+ interface DashboardProps {
3
+ store: ConfigStore;
4
+ selectedIndex: number;
5
+ onSelect: (modelId: string) => void;
6
+ onDelete: (providerId: string, modelId: string) => void;
7
+ onScenario: () => void;
8
+ }
9
+ export declare function Dashboard({ store, selectedIndex }: DashboardProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/tui/dashboard.tsx
3
+ import { Box, Text } from "ink";
4
+ import { Table } from "../components/ui/table.js";
5
+ import { StatusBar } from "../components/ui/status-bar.js";
6
+ export function Dashboard({ store, selectedIndex }) {
7
+ const rows = [];
8
+ for (const p of store.providers) {
9
+ for (const m of p.models) {
10
+ rows.push({
11
+ providerName: p.name,
12
+ modelId: m,
13
+ active: m === store.activeModelId,
14
+ isSelected: rows.length === selectedIndex,
15
+ });
16
+ }
17
+ }
18
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "cc-hub" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "dim", children: "Select a model to switch your active Claude Code configuration." }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: rows.length === 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "dim", children: "No providers configured." }), _jsxs(Text, { color: "dim", children: ["Edit ", _jsx(Text, { color: "cyan", children: "~/.cc-hub/config.json" }), " to add providers, then restart."] })] })) : (_jsx(Table, { columns: [
19
+ { header: "Provider", minWidth: 10 },
20
+ { header: "Model", minWidth: 24 },
21
+ { header: "Active", width: 8 },
22
+ ], rows: rows.map((row) => [
23
+ {
24
+ text: row.providerName,
25
+ color: row.isSelected ? "green" : undefined,
26
+ bold: row.isSelected,
27
+ },
28
+ {
29
+ text: row.modelId,
30
+ color: row.isSelected ? "green" : undefined,
31
+ bold: row.isSelected,
32
+ },
33
+ {
34
+ text: row.active ? "●" : "",
35
+ color: row.active ? "green" : undefined,
36
+ bold: row.active,
37
+ },
38
+ ]) })) }), _jsx(Box, { marginTop: 1, children: _jsx(StatusBar, { items: [
39
+ { key: "Enter", label: "Switch" },
40
+ { key: "↑↓", label: "Navigate" },
41
+ { key: "d", label: "Delete" },
42
+ { key: "s", label: "Scenarios" },
43
+ { key: "q", label: "Quit" },
44
+ ] }) })] }));
45
+ }
@@ -0,0 +1,8 @@
1
+ import { ModelConfig } from "../types.js";
2
+ interface EditModelProps {
3
+ model: ModelConfig;
4
+ onSave: (updates: Partial<ModelConfig>) => void;
5
+ onCancel: () => void;
6
+ }
7
+ export declare function EditModel({ model, onSave, onCancel }: EditModelProps): import("react/jsx-runtime").JSX.Element | null;
8
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ // src/tui/edit-model.tsx
3
+ import React from "react";
4
+ import { Box, Text, useInput } from "ink";
5
+ import TextInput from "ink-text-input";
6
+ export function EditModel({ model, onSave, onCancel }) {
7
+ const [field, setField] = React.useState("name");
8
+ const [name, setName] = React.useState(model.name);
9
+ const [baseUrl, setBaseUrl] = React.useState(model.baseUrl);
10
+ const [apiKey, setApiKey] = React.useState(model.apiKey);
11
+ const [modelId, setModelId] = React.useState(model.modelId || "");
12
+ // Handle escape BEFORE TextInput's useInput can process it
13
+ useInput((_, key) => {
14
+ if (key.escape) {
15
+ onCancel();
16
+ }
17
+ });
18
+ if (field === "done") {
19
+ onSave({ name, baseUrl, apiKey, modelId: modelId || undefined });
20
+ return null;
21
+ }
22
+ const fields = [
23
+ { key: "name", label: "Name" },
24
+ { key: "baseUrl", label: "Base URL" },
25
+ { key: "apiKey", label: "API Key" },
26
+ { key: "modelId", label: "Model ID (optional)" },
27
+ ];
28
+ const valueMap = { name, baseUrl, apiKey, modelId };
29
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Edit Model: ", model.id] }), _jsx(Text, { children: "─".repeat(50) }), fields.map((f) => (_jsxs(Box, { children: [_jsx(Text, { children: f.key === field ? "> " : " " }), _jsx(Text, { color: "dim", children: f.label.padEnd(16) }), f.key === field ? (_jsx(Text, { color: "green", children: maskIf(f.key === "apiKey", valueMap[f.key]) })) : (_jsx(Text, { color: "dim", children: maskIf(f.key === "apiKey", valueMap[f.key]) }))] }, f.key))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(TextInput, { value: valueMap[field], onChange: (v) => {
30
+ if (field === "name")
31
+ setName(v);
32
+ else if (field === "baseUrl")
33
+ setBaseUrl(v);
34
+ else if (field === "apiKey")
35
+ setApiKey(v);
36
+ else if (field === "modelId")
37
+ setModelId(v);
38
+ }, onSubmit: () => {
39
+ const idx = fields.findIndex((f) => f.key === field);
40
+ if (idx < fields.length - 1) {
41
+ setField(fields[idx + 1].key);
42
+ }
43
+ else {
44
+ setField("done");
45
+ }
46
+ }, focus: false }), _jsx(Text, { color: "dim", children: "Press Enter for next field, Esc to cancel" })] })] }));
47
+ }
48
+ function maskIf(shouldMask, value) {
49
+ if (!shouldMask)
50
+ return value;
51
+ if (value.length <= 6)
52
+ return "••••••";
53
+ return value.slice(0, 3) + "••••••" + value.slice(-3);
54
+ }
@@ -0,0 +1,12 @@
1
+ import { Provider } from "../types.js";
2
+ interface EditProviderProps {
3
+ provider: Provider;
4
+ onSave: (providerId: string, updates: {
5
+ name?: string;
6
+ baseUrl?: string;
7
+ apiKey?: string;
8
+ }) => void;
9
+ onCancel: () => void;
10
+ }
11
+ export declare function EditProvider({ provider, onSave, onCancel }: EditProviderProps): import("react/jsx-runtime").JSX.Element | null;
12
+ export {};
@@ -0,0 +1,140 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ // src/tui/edit-provider.tsx
3
+ import React from "react";
4
+ import { Box, Text, useInput } from "ink";
5
+ import TextInput from "ink-text-input";
6
+ export function EditProvider({ provider, onSave, onCancel }) {
7
+ const [step, setStep] = React.useState("provider-name");
8
+ const [name, setName] = React.useState(provider.name);
9
+ const [baseUrl, setBaseUrl] = React.useState(provider.baseUrl);
10
+ const [apiKey, setApiKey] = React.useState(provider.apiKey);
11
+ const [modelList, setModelList] = React.useState(provider.models);
12
+ const [modelIndex, setModelIndex] = React.useState(0);
13
+ const [pendingModel, setPendingModel] = React.useState({ name: "", modelId: "", id: "" });
14
+ const [addName, setAddName] = React.useState("");
15
+ const [addModelId, setAddModelId] = React.useState("");
16
+ // Global keyboard handling
17
+ useInput((input, key) => {
18
+ if (key.escape) {
19
+ if (step.startsWith("edit-model-")) {
20
+ setStep("models");
21
+ }
22
+ else if (step.startsWith("add-model-")) {
23
+ setStep("models");
24
+ }
25
+ else {
26
+ onCancel();
27
+ }
28
+ return;
29
+ }
30
+ switch (step) {
31
+ case "models": {
32
+ if (key.upArrow) {
33
+ setModelIndex((i) => Math.max(0, i - 1));
34
+ return;
35
+ }
36
+ if (key.downArrow) {
37
+ setModelIndex((i) => Math.min(modelList.length - 1, i + 1));
38
+ return;
39
+ }
40
+ if (input === "e" && modelList.length > 0) {
41
+ const m = modelList[modelIndex];
42
+ setPendingModel({ name: m.name, modelId: m.modelId, id: m.id });
43
+ setStep("edit-model-name");
44
+ return;
45
+ }
46
+ if (input === "a") {
47
+ setAddName("");
48
+ setAddModelId("");
49
+ setStep("add-model-name");
50
+ return;
51
+ }
52
+ if (input === "d" && modelList.length > 0) {
53
+ setModelList((list) => list.filter((_, i) => i !== modelIndex));
54
+ setModelIndex((i) => Math.max(0, Math.min(i, modelList.length - 2)));
55
+ return;
56
+ }
57
+ if (key.return) {
58
+ setStep("done");
59
+ return;
60
+ }
61
+ return;
62
+ }
63
+ }
64
+ });
65
+ if (step === "done") {
66
+ onSave(provider.id, { name, baseUrl, apiKey });
67
+ return null;
68
+ }
69
+ // Provider field editing steps
70
+ if (step === "provider-name" || step === "provider-baseurl" || step === "provider-apikey") {
71
+ const fieldMap = {
72
+ "provider-name": { label: "Provider name", value: name, set: setName, next: "provider-baseurl" },
73
+ "provider-baseurl": { label: "Base URL", value: baseUrl, set: setBaseUrl, next: "provider-apikey" },
74
+ "provider-apikey": { label: "API Key", value: apiKey, set: setApiKey, next: "models" },
75
+ };
76
+ const f = fieldMap[step];
77
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Edit Provider: ", provider.id] }), _jsx(Text, { children: "─".repeat(40) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "> " }), _jsxs(Text, { bold: true, children: [f.label, ": "] }), _jsx(Text, { color: "green", children: step === "provider-apikey" ? maskValue(f.value) : f.value })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(TextInput, { value: f.value, onChange: f.set, onSubmit: () => setStep(f.next), focus: false }), _jsx(Text, { color: "dim", children: "Press Enter for next field" })] })] }));
78
+ }
79
+ // Models list
80
+ if (step === "models") {
81
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Edit Provider: ", name] }), _jsx(Text, { children: "─".repeat(50) }), modelList.length === 0 ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "dim", children: "No models. Press " }), _jsx(Text, { color: "green", bold: true, children: "a" }), _jsx(Text, { color: "dim", children: " to add one." })] })) : (modelList.map((m, i) => (_jsxs(Box, { children: [_jsx(Text, { children: i === modelIndex ? "> " : " " }), _jsx(Text, { color: "dim", children: m.id.padEnd(20) }), _jsx(Text, { children: m.name }), _jsxs(Text, { color: "dim", children: [" (", m.modelId, ")"] })] }, m.id)))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "dim", children: [_jsx(Text, { color: "green", bold: true, children: "e" }), ": Edit", " ", _jsx(Text, { color: "green", bold: true, children: "a" }), ": Add", " ", _jsx(Text, { color: "green", bold: true, children: "d" }), ": Delete", " ", _jsx(Text, { color: "green", bold: true, children: "Enter" }), ": Done", " ", _jsx(Text, { color: "green", bold: true, children: "Esc" }), ": Cancel"] }) })] }));
82
+ }
83
+ // Edit model fields
84
+ if (step.startsWith("edit-model-")) {
85
+ const editFields = [
86
+ { key: "edit-model-name", label: "Name", getValue: () => pendingModel.name, setValue: (v) => setPendingModel((p) => ({ ...p, name: v })), next: "edit-model-modelId" },
87
+ { key: "edit-model-modelId", label: "Model ID", getValue: () => pendingModel.modelId, setValue: (v) => setPendingModel((p) => ({ ...p, modelId: v })), next: "edit-model-id" },
88
+ { key: "edit-model-id", label: "Local ID", getValue: () => pendingModel.id, setValue: (v) => setPendingModel((p) => ({ ...p, id: v })), next: "models" },
89
+ ];
90
+ const current = editFields.find((f) => f.key === step);
91
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Edit Model" }), _jsx(Text, { children: "─".repeat(40) }), editFields.map((f) => (_jsxs(Box, { children: [_jsx(Text, { children: f.key === step ? "> " : " " }), _jsx(Text, { color: "dim", children: f.label.padEnd(12) }), _jsx(Text, { color: f.key === step ? "green" : "dim", children: f.getValue() })] }, f.key))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(TextInput, { value: current.getValue(), onChange: current.setValue, onSubmit: () => {
92
+ const idx = editFields.findIndex((f) => f.key === step);
93
+ if (idx < editFields.length - 1) {
94
+ setStep(editFields[idx + 1].key);
95
+ }
96
+ else {
97
+ setModelList((list) => list.map((m) => m.id === pendingModel.id
98
+ ? { id: pendingModel.id, name: pendingModel.name, modelId: pendingModel.modelId }
99
+ : m));
100
+ setStep("models");
101
+ }
102
+ }, focus: false }), _jsx(Text, { color: "dim", children: "Press Enter for next field" })] })] }));
103
+ }
104
+ // Add model fields
105
+ if (step.startsWith("add-model-")) {
106
+ const addFields = [
107
+ { key: "add-model-name", label: "Name", value: addName, set: setAddName, next: "add-model-modelId" },
108
+ { key: "add-model-modelId", label: "Model ID", value: addModelId, set: setAddModelId, next: "models" },
109
+ ];
110
+ const current = addFields.find((f) => f.key === step);
111
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Add Model" }), _jsx(Text, { children: "─".repeat(40) }), addFields.map((f) => (_jsxs(Box, { children: [_jsx(Text, { children: f.key === step ? "> " : " " }), _jsx(Text, { color: "dim", children: f.label.padEnd(12) }), _jsx(Text, { color: f.key === step ? "green" : "dim", children: f.value || "(empty)" })] }, f.key))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(TextInput, { value: current.value, onChange: current.set, onSubmit: () => {
112
+ const idx = addFields.findIndex((f) => f.key === step);
113
+ if (idx < addFields.length - 1) {
114
+ setStep(addFields[idx + 1].key);
115
+ }
116
+ else {
117
+ if (addName.trim()) {
118
+ const id = toKebabCase(addName);
119
+ setModelList((list) => [
120
+ ...list,
121
+ { id, name: addName.trim(), modelId: addModelId.trim() || id },
122
+ ]);
123
+ }
124
+ setStep("models");
125
+ }
126
+ }, focus: false }), _jsx(Text, { color: "dim", children: "Press Enter for next field" })] })] }));
127
+ }
128
+ return null;
129
+ }
130
+ function maskValue(value) {
131
+ if (value.length <= 6)
132
+ return "••••••";
133
+ return value.slice(0, 3) + "••••••" + value.slice(-3);
134
+ }
135
+ function toKebabCase(str) {
136
+ return str
137
+ .toLowerCase()
138
+ .replace(/[^a-z0-9]+/g, "-")
139
+ .replace(/^-|-$/g, "");
140
+ }
@@ -0,0 +1,8 @@
1
+ import { ConfigStore, ScenarioModels } from "../types.js";
2
+ interface ScenarioConfigProps {
3
+ store: ConfigStore;
4
+ onSave: (updates: ScenarioModels) => void;
5
+ onCancel: () => void;
6
+ }
7
+ export declare function ScenarioConfig({ store, onSave, onCancel, }: ScenarioConfigProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/tui/scenario-config.tsx
3
+ import React from "react";
4
+ import { Box, Text, useInput } from "ink";
5
+ import { Table } from "../components/ui/table.js";
6
+ import { StatusBar } from "../components/ui/status-bar.js";
7
+ const FIELDS = [
8
+ { key: "opusModelId", label: "Opus", envVar: "ANTHROPIC_DEFAULT_OPUS_MODEL" },
9
+ {
10
+ key: "sonnetModelId",
11
+ label: "Sonnet",
12
+ envVar: "ANTHROPIC_DEFAULT_SONNET_MODEL",
13
+ },
14
+ {
15
+ key: "haikuModelId",
16
+ label: "Haiku",
17
+ envVar: "ANTHROPIC_DEFAULT_HAIKU_MODEL",
18
+ },
19
+ {
20
+ key: "subagentModelId",
21
+ label: "Subagent",
22
+ envVar: "CLAUDE_CODE_SUBAGENT_MODEL",
23
+ },
24
+ ];
25
+ export function ScenarioConfig({ store, onSave, onCancel, }) {
26
+ const activeProvider = store.providers.find((p) => p.id === store.activeProviderId);
27
+ const allModels = activeProvider ? activeProvider.models : [];
28
+ const [index, setIndex] = React.useState(0);
29
+ const [values, setValues] = React.useState({
30
+ opusModelId: store.scenarioModels.opusModelId,
31
+ sonnetModelId: store.scenarioModels.sonnetModelId,
32
+ haikuModelId: store.scenarioModels.haikuModelId,
33
+ subagentModelId: store.scenarioModels.subagentModelId,
34
+ });
35
+ useInput((input, key) => {
36
+ if (key.escape) {
37
+ onCancel();
38
+ return;
39
+ }
40
+ if (key.upArrow) {
41
+ setIndex((i) => Math.max(0, i - 1));
42
+ return;
43
+ }
44
+ if (key.downArrow) {
45
+ setIndex((i) => Math.min(FIELDS.length - 1, i + 1));
46
+ return;
47
+ }
48
+ if (key.leftArrow || key.rightArrow) {
49
+ const field = FIELDS[index].key;
50
+ const currentIdx = allModels.findIndex((m) => m === values[field]);
51
+ const dir = key.leftArrow ? -1 : 1;
52
+ const nextIdx = (currentIdx + dir + allModels.length + 1) % (allModels.length + 1);
53
+ const nextValue = nextIdx === allModels.length ? undefined : allModels[nextIdx];
54
+ setValues((v) => ({ ...v, [field]: nextValue }));
55
+ return;
56
+ }
57
+ if (key.return) {
58
+ onSave(values);
59
+ return;
60
+ }
61
+ });
62
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Scenario Mappings" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "dim", children: "Map Claude Code's sonnet/opus/haiku/subagent aliases to your models." }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(Table, { columns: [
63
+ { header: "Role", minWidth: 10 },
64
+ { header: "Model", minWidth: 24 },
65
+ { header: "Env Var", minWidth: 20 },
66
+ ], rows: FIELDS.map((f, i) => [
67
+ {
68
+ text: f.label,
69
+ color: i === index ? "green" : undefined,
70
+ bold: i === index,
71
+ },
72
+ {
73
+ text: values[f.key] || "(none)",
74
+ color: i === index ? "green" : "dim",
75
+ bold: i === index,
76
+ },
77
+ {
78
+ text: f.envVar,
79
+ color: "dim",
80
+ },
81
+ ]) }) }), _jsx(Box, { marginTop: 1, children: _jsx(StatusBar, { items: [
82
+ { key: "Enter", label: "Save" },
83
+ { key: "↑↓", label: "Switch role" },
84
+ { key: "←→", label: "Switch model" },
85
+ { key: "Esc", label: "Cancel" },
86
+ ] }) })] }));
87
+ }
@@ -0,0 +1,19 @@
1
+ export interface Provider {
2
+ id: string;
3
+ name: string;
4
+ baseUrl: string;
5
+ apiKey: string;
6
+ models: string[];
7
+ }
8
+ export interface ScenarioModels {
9
+ opusModelId?: string;
10
+ sonnetModelId?: string;
11
+ haikuModelId?: string;
12
+ subagentModelId?: string;
13
+ }
14
+ export interface ConfigStore {
15
+ providers: Provider[];
16
+ activeProviderId: string | null;
17
+ activeModelId: string | null;
18
+ scenarioModels: ScenarioModels;
19
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // src/types.ts
2
+ export {};
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "cc-hub",
3
+ "version": "0.1.0",
4
+ "description": "TUI for managing Claude Code model configurations",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-hub": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "pnpm run build && pnpm start",
11
+ "dev:watch": "nodemon --watch src --ext ts,tsx --exec 'pnpm run build && pnpm start'",
12
+ "build": "node build.mjs",
13
+ "start": "node dist/cli.js"
14
+ },
15
+ "dependencies": {
16
+ "ink": "^6.0.0",
17
+ "ink-select-input": "^6.0.0",
18
+ "ink-spinner": "^5.0.0",
19
+ "ink-text-input": "^6.0.0",
20
+ "json5": "^2.2.3",
21
+ "react": "^19.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/json5": "^2.2.0",
25
+ "@types/node": "^22.0.0",
26
+ "@types/react": "^19.0.0",
27
+ "nodemon": "^3.1.14",
28
+ "tsx": "^4.19.0",
29
+ "typescript": "^5.7.0"
30
+ },
31
+ "license": "MIT",
32
+ "keywords": [
33
+ "claude",
34
+ "claude-code",
35
+ "llm",
36
+ "tui"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/slashspace/cc-hub.git"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ]
45
+ }