@leynier/ccst 0.2.1 → 0.3.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/package.json +57 -56
- package/src/commands/completions.ts +53 -47
- package/src/commands/config/dump.ts +40 -0
- package/src/commands/config/load.ts +122 -0
- package/src/commands/create.ts +5 -2
- package/src/commands/delete.ts +9 -6
- package/src/commands/edit.ts +13 -10
- package/src/commands/export.ts +13 -10
- package/src/commands/import-profiles/ccs.ts +94 -69
- package/src/commands/import-profiles/configs.ts +78 -60
- package/src/commands/import.ts +8 -5
- package/src/commands/list.ts +5 -2
- package/src/commands/merge.ts +25 -12
- package/src/commands/rename.ts +14 -11
- package/src/commands/show.ts +13 -10
- package/src/commands/switch.ts +9 -4
- package/src/commands/unset.ts +1 -1
- package/src/core/context-manager.test.ts +49 -47
- package/src/core/context-manager.ts +484 -389
- package/src/core/merge-manager.test.ts +40 -25
- package/src/core/merge-manager.ts +182 -132
- package/src/core/settings-level.ts +11 -8
- package/src/core/state.ts +22 -17
- package/src/index.ts +169 -130
- package/src/types/index.ts +5 -5
- package/src/utils/ccs-paths.ts +120 -0
- package/src/utils/colors.ts +6 -6
- package/src/utils/deep-merge.ts +21 -18
- package/src/utils/interactive.ts +68 -56
- package/src/utils/json.ts +17 -11
- package/src/utils/paths.ts +46 -44
package/src/index.ts
CHANGED
|
@@ -1,146 +1,185 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { listCommand } from "./commands/list.js";
|
|
7
|
-
import { switchCommand, switchPreviousCommand } from "./commands/switch.js";
|
|
3
|
+
import { completionsCommand } from "./commands/completions.js";
|
|
4
|
+
import { configDumpCommand } from "./commands/config/dump.js";
|
|
5
|
+
import { configLoadCommand } from "./commands/config/load.js";
|
|
8
6
|
import { createCommand } from "./commands/create.js";
|
|
9
7
|
import { deleteCommand } from "./commands/delete.js";
|
|
10
|
-
import { renameCommand } from "./commands/rename.js";
|
|
11
8
|
import { editCommand } from "./commands/edit.js";
|
|
12
|
-
import { showCommand } from "./commands/show.js";
|
|
13
9
|
import { exportCommand } from "./commands/export.js";
|
|
14
10
|
import { importCommand } from "./commands/import.js";
|
|
15
|
-
import { unsetCommand } from "./commands/unset.js";
|
|
16
|
-
import { mergeCommand, mergeHistoryCommand, unmergeCommand } from "./commands/merge.js";
|
|
17
|
-
import { completionsCommand } from "./commands/completions.js";
|
|
18
11
|
import { importFromCcs } from "./commands/import-profiles/ccs.js";
|
|
19
12
|
import { importFromConfigs } from "./commands/import-profiles/configs.js";
|
|
13
|
+
import { listCommand } from "./commands/list.js";
|
|
14
|
+
import {
|
|
15
|
+
mergeCommand,
|
|
16
|
+
mergeHistoryCommand,
|
|
17
|
+
unmergeCommand,
|
|
18
|
+
} from "./commands/merge.js";
|
|
19
|
+
import { renameCommand } from "./commands/rename.js";
|
|
20
|
+
import { showCommand } from "./commands/show.js";
|
|
21
|
+
import { switchCommand, switchPreviousCommand } from "./commands/switch.js";
|
|
22
|
+
import { unsetCommand } from "./commands/unset.js";
|
|
23
|
+
import { ContextManager } from "./core/context-manager.js";
|
|
24
|
+
import { resolveSettingsLevel } from "./core/settings-level.js";
|
|
25
|
+
import { getPaths } from "./utils/paths.js";
|
|
20
26
|
|
|
21
27
|
export const program = new Command();
|
|
22
28
|
|
|
23
29
|
const main = async (): Promise<void> => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
30
|
+
program
|
|
31
|
+
.name("ccst")
|
|
32
|
+
.description("Claude Code Switch Tools")
|
|
33
|
+
.argument("[context]", "context name")
|
|
34
|
+
.option("-d, --delete", "delete context")
|
|
35
|
+
.option("-c, --current", "print current context")
|
|
36
|
+
.option("-r, --rename", "rename context")
|
|
37
|
+
.option("-n, --new", "create new context")
|
|
38
|
+
.option("-e, --edit", "edit context")
|
|
39
|
+
.option("-s, --show", "show context")
|
|
40
|
+
.option("--export", "export context to stdout")
|
|
41
|
+
.option("--import", "import context from stdin")
|
|
42
|
+
.option("-u, --unset", "unset current context")
|
|
43
|
+
.option("--completions <shell>", "generate completions")
|
|
44
|
+
.option("-q, --quiet", "show only current context")
|
|
45
|
+
.option("--in-project", "use project settings level")
|
|
46
|
+
.option("--local", "use local settings level")
|
|
47
|
+
.option("--merge-from <source>", "merge permissions from source")
|
|
48
|
+
.option("--unmerge <source>", "remove permissions merged from source")
|
|
49
|
+
.option("--merge-history", "show merge history")
|
|
50
|
+
.option("--merge-full", "merge full settings")
|
|
51
|
+
.allowExcessArguments(false)
|
|
52
|
+
.action(
|
|
53
|
+
async (context: string | undefined, options: Record<string, unknown>) => {
|
|
54
|
+
if (options.completions) {
|
|
55
|
+
completionsCommand(options.completions as string);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const level = resolveSettingsLevel(options);
|
|
59
|
+
const manager = new ContextManager(getPaths(level));
|
|
60
|
+
if (options.current) {
|
|
61
|
+
const current = await manager.getCurrentContext();
|
|
62
|
+
if (current) {
|
|
63
|
+
console.log(current);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (options.unset) {
|
|
68
|
+
await unsetCommand(manager);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (options.delete) {
|
|
72
|
+
await deleteCommand(manager, context);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (options.rename) {
|
|
76
|
+
await renameCommand(manager, context);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (options.new) {
|
|
80
|
+
if (!context) {
|
|
81
|
+
await manager.interactiveCreateContext();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await createCommand(manager, context);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (options.edit) {
|
|
88
|
+
await editCommand(manager, context);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (options.show) {
|
|
92
|
+
await showCommand(manager, context);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (options.export) {
|
|
96
|
+
await exportCommand(manager, context);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (options.import) {
|
|
100
|
+
await importCommand(manager, context);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (options.mergeFrom) {
|
|
104
|
+
await mergeCommand(
|
|
105
|
+
manager,
|
|
106
|
+
options.mergeFrom as string,
|
|
107
|
+
context,
|
|
108
|
+
options.mergeFull as boolean,
|
|
109
|
+
);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (options.unmerge) {
|
|
113
|
+
await unmergeCommand(
|
|
114
|
+
manager,
|
|
115
|
+
options.unmerge as string,
|
|
116
|
+
context,
|
|
117
|
+
options.mergeFull as boolean,
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (options.mergeHistory) {
|
|
122
|
+
await mergeHistoryCommand(manager, context);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (context === "-") {
|
|
126
|
+
await switchPreviousCommand(manager);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (context) {
|
|
130
|
+
await switchCommand(manager, context);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (process.env.CCTX_INTERACTIVE === "1") {
|
|
134
|
+
await manager.interactiveSelect();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await listCommand(manager, options.quiet as boolean);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
const importCommandGroup = program
|
|
141
|
+
.command("import")
|
|
142
|
+
.description("import profiles");
|
|
143
|
+
importCommandGroup
|
|
144
|
+
.command("ccs")
|
|
145
|
+
.description("import from CCS settings")
|
|
146
|
+
.option("-d, --configs-dir <dir>", "configs directory")
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
const manager = new ContextManager(getPaths("user"));
|
|
149
|
+
await importFromCcs(manager, options.configsDir);
|
|
150
|
+
});
|
|
151
|
+
importCommandGroup
|
|
152
|
+
.command("configs")
|
|
153
|
+
.description("import from configs directory")
|
|
154
|
+
.option("-d, --configs-dir <dir>", "configs directory")
|
|
155
|
+
.action(async (options) => {
|
|
156
|
+
const manager = new ContextManager(getPaths("user"));
|
|
157
|
+
await importFromConfigs(manager, options.configsDir);
|
|
158
|
+
});
|
|
159
|
+
const configCommandGroup = program
|
|
160
|
+
.command("config")
|
|
161
|
+
.description("CCS config backup/restore");
|
|
162
|
+
configCommandGroup
|
|
163
|
+
.command("dump")
|
|
164
|
+
.description("export CCS config to zip")
|
|
165
|
+
.argument("[output]", "output path", "ccs-config.zip")
|
|
166
|
+
.action(async (output) => {
|
|
167
|
+
await configDumpCommand(output);
|
|
168
|
+
});
|
|
169
|
+
configCommandGroup
|
|
170
|
+
.command("load")
|
|
171
|
+
.description("import CCS config from zip")
|
|
172
|
+
.argument("[input]", "input path", "ccs-config.zip")
|
|
173
|
+
.option("-r, --replace", "replace all existing files")
|
|
174
|
+
.option("-y, --yes", "skip confirmation prompt")
|
|
175
|
+
.action(async (input, options) => {
|
|
176
|
+
await configLoadCommand(input, options);
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
await program.parseAsync(process.argv);
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
144
183
|
};
|
|
145
184
|
|
|
146
185
|
await main();
|
package/src/types/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export type SettingsLevel = "user" | "project" | "local";
|
|
2
2
|
|
|
3
3
|
export type State = {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
current?: string;
|
|
5
|
+
previous?: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export type MergeHistoryEntry = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
source: string;
|
|
10
|
+
mergedItems: string[];
|
|
11
|
+
timestamp: string;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export type MergeHistory = MergeHistoryEntry[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Cross-platform CCS home directory
|
|
6
|
+
export const getCcsHome = (): string => join(homedir(), ".ccs");
|
|
7
|
+
|
|
8
|
+
// File patterns to backup
|
|
9
|
+
export type CcsBackupFiles = {
|
|
10
|
+
configYaml: string | null;
|
|
11
|
+
settingsProfiles: string[];
|
|
12
|
+
cliproxyAccounts: string | null;
|
|
13
|
+
cliproxyConfig: string | null;
|
|
14
|
+
cliproxyAuthFiles: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Helper to safely read directory contents
|
|
18
|
+
const safeReaddirSync = (dirPath: string): string[] => {
|
|
19
|
+
try {
|
|
20
|
+
return readdirSync(dirPath);
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Get all files to backup from CCS directory
|
|
27
|
+
export const getCcsBackupFiles = (): CcsBackupFiles => {
|
|
28
|
+
const ccsHome = getCcsHome();
|
|
29
|
+
const result: CcsBackupFiles = {
|
|
30
|
+
configYaml: null,
|
|
31
|
+
settingsProfiles: [],
|
|
32
|
+
cliproxyAccounts: null,
|
|
33
|
+
cliproxyConfig: null,
|
|
34
|
+
cliproxyAuthFiles: [],
|
|
35
|
+
};
|
|
36
|
+
if (!existsSync(ccsHome)) {
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
// config.yaml
|
|
40
|
+
const configYamlPath = join(ccsHome, "config.yaml");
|
|
41
|
+
if (existsSync(configYamlPath)) {
|
|
42
|
+
result.configYaml = configYamlPath;
|
|
43
|
+
}
|
|
44
|
+
// *.settings.json profiles
|
|
45
|
+
const rootFiles = safeReaddirSync(ccsHome);
|
|
46
|
+
for (const file of rootFiles) {
|
|
47
|
+
if (file.endsWith(".settings.json")) {
|
|
48
|
+
result.settingsProfiles.push(join(ccsHome, file));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// cliproxy files
|
|
52
|
+
const cliproxyDir = join(ccsHome, "cliproxy");
|
|
53
|
+
if (existsSync(cliproxyDir)) {
|
|
54
|
+
const accountsPath = join(cliproxyDir, "accounts.json");
|
|
55
|
+
if (existsSync(accountsPath)) {
|
|
56
|
+
result.cliproxyAccounts = accountsPath;
|
|
57
|
+
}
|
|
58
|
+
const configPath = join(cliproxyDir, "config.json");
|
|
59
|
+
if (existsSync(configPath)) {
|
|
60
|
+
result.cliproxyConfig = configPath;
|
|
61
|
+
}
|
|
62
|
+
// cliproxy/auth/*.json
|
|
63
|
+
const authDir = join(cliproxyDir, "auth");
|
|
64
|
+
if (existsSync(authDir)) {
|
|
65
|
+
const authFiles = safeReaddirSync(authDir);
|
|
66
|
+
for (const file of authFiles) {
|
|
67
|
+
if (file.endsWith(".json")) {
|
|
68
|
+
result.cliproxyAuthFiles.push(join(authDir, file));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Get relative path from CCS home for zip storage (always use forward slashes)
|
|
77
|
+
export const getRelativePath = (absolutePath: string): string => {
|
|
78
|
+
const ccsHome = getCcsHome();
|
|
79
|
+
const relativePath = relative(ccsHome, absolutePath);
|
|
80
|
+
// Convert to forward slashes for zip compatibility (works on all platforms)
|
|
81
|
+
return relativePath.replace(/\\/g, "/");
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Ensure directory exists for a file path (cross-platform)
|
|
85
|
+
export const ensureDirectoryExists = (filePath: string): void => {
|
|
86
|
+
const dir = dirname(filePath);
|
|
87
|
+
if (dir && dir !== "." && !existsSync(dir)) {
|
|
88
|
+
mkdirSync(dir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Get all backup file paths as a flat array
|
|
93
|
+
export const getAllBackupFilePaths = (): string[] => {
|
|
94
|
+
const files = getCcsBackupFiles();
|
|
95
|
+
const paths: string[] = [];
|
|
96
|
+
if (files.configYaml) {
|
|
97
|
+
paths.push(files.configYaml);
|
|
98
|
+
}
|
|
99
|
+
paths.push(...files.settingsProfiles);
|
|
100
|
+
if (files.cliproxyAccounts) {
|
|
101
|
+
paths.push(files.cliproxyAccounts);
|
|
102
|
+
}
|
|
103
|
+
if (files.cliproxyConfig) {
|
|
104
|
+
paths.push(files.cliproxyConfig);
|
|
105
|
+
}
|
|
106
|
+
paths.push(...files.cliproxyAuthFiles);
|
|
107
|
+
return paths;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Get files that would be deleted in replace mode
|
|
111
|
+
export const getFilesToDelete = (): {
|
|
112
|
+
profiles: string[];
|
|
113
|
+
authFiles: string[];
|
|
114
|
+
} => {
|
|
115
|
+
const files = getCcsBackupFiles();
|
|
116
|
+
return {
|
|
117
|
+
profiles: files.settingsProfiles,
|
|
118
|
+
authFiles: files.cliproxyAuthFiles,
|
|
119
|
+
};
|
|
120
|
+
};
|
package/src/utils/colors.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
|
|
3
3
|
export const colors = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
green: pc.green,
|
|
5
|
+
red: pc.red,
|
|
6
|
+
cyan: pc.cyan,
|
|
7
|
+
yellow: pc.yellow,
|
|
8
|
+
bold: pc.bold,
|
|
9
|
+
dim: pc.dim,
|
|
10
10
|
};
|
package/src/utils/deep-merge.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
export const deepMerge = (
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
export const deepMerge = (
|
|
2
|
+
base: Record<string, unknown>,
|
|
3
|
+
override: Record<string, unknown>,
|
|
4
|
+
): Record<string, unknown> => {
|
|
5
|
+
const result: Record<string, unknown> = { ...base };
|
|
6
|
+
for (const [key, value] of Object.entries(override)) {
|
|
7
|
+
const baseValue = result[key];
|
|
8
|
+
if (isPlainObject(baseValue) && isPlainObject(value)) {
|
|
9
|
+
result[key] = deepMerge(baseValue, value);
|
|
10
|
+
} else {
|
|
11
|
+
result[key] = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
12
15
|
};
|
|
13
16
|
|
|
14
17
|
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (value === null || typeof value !== "object") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return Object.getPrototypeOf(value) === Object.prototype;
|
|
22
25
|
};
|
package/src/utils/interactive.ts
CHANGED
|
@@ -1,69 +1,81 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
const hasFzf = (): boolean => {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const which = spawnSync("which", ["fzf"], { stdio: "ignore" });
|
|
5
|
+
return which.status === 0 && Boolean(process.env.TERM);
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export const selectContext = async (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
export const selectContext = async (
|
|
9
|
+
contexts: string[],
|
|
10
|
+
current?: string,
|
|
11
|
+
): Promise<string | undefined> => {
|
|
12
|
+
if (contexts.length === 0) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (hasFzf()) {
|
|
16
|
+
const input = contexts.join("\n");
|
|
17
|
+
const result = spawnSync("fzf", [], {
|
|
18
|
+
input,
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
21
|
+
});
|
|
22
|
+
if (result.status !== 0) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const selected = String(result.stdout).trim();
|
|
26
|
+
return selected.length > 0 ? selected : undefined;
|
|
27
|
+
}
|
|
28
|
+
return promptSelect(contexts, current);
|
|
22
29
|
};
|
|
23
30
|
|
|
24
|
-
const promptSelect = async (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const promptSelect = async (
|
|
32
|
+
contexts: string[],
|
|
33
|
+
current?: string,
|
|
34
|
+
): Promise<string | undefined> => {
|
|
35
|
+
const lines = contexts.map((ctx, index) => {
|
|
36
|
+
const marker = current && ctx === current ? " (current)" : "";
|
|
37
|
+
return `${index + 1}. ${ctx}${marker}`;
|
|
38
|
+
});
|
|
39
|
+
process.stdout.write(`${lines.join("\n")}\nSelect a context (number): `);
|
|
40
|
+
const input = await readStdinLine();
|
|
41
|
+
const index = Number.parseInt(input, 10);
|
|
42
|
+
if (!Number.isFinite(index) || index < 1 || index > contexts.length) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return contexts[index - 1];
|
|
36
46
|
};
|
|
37
47
|
|
|
38
|
-
export const promptInput = async (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
export const promptInput = async (
|
|
49
|
+
label: string,
|
|
50
|
+
): Promise<string | undefined> => {
|
|
51
|
+
process.stdout.write(`${label}: `);
|
|
52
|
+
const input = await readStdinLine();
|
|
53
|
+
const trimmed = input.trim();
|
|
54
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
const readStdinLine = async (): Promise<string> => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const chunks: Buffer[] = [];
|
|
60
|
+
const onData = (chunk: Buffer) => {
|
|
61
|
+
chunks.push(chunk);
|
|
62
|
+
if (chunk.includes(10)) {
|
|
63
|
+
cleanup();
|
|
64
|
+
resolve(Buffer.concat(chunks).toString("utf8").trim());
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const onEnd = () => {
|
|
68
|
+
cleanup();
|
|
69
|
+
resolve(Buffer.concat(chunks).toString("utf8").trim());
|
|
70
|
+
};
|
|
71
|
+
const cleanup = () => {
|
|
72
|
+
process.stdin.off("data", onData);
|
|
73
|
+
process.stdin.off("end", onEnd);
|
|
74
|
+
};
|
|
75
|
+
if (process.stdin.isTTY) {
|
|
76
|
+
process.stdin.resume();
|
|
77
|
+
}
|
|
78
|
+
process.stdin.on("data", onData);
|
|
79
|
+
process.stdin.on("end", onEnd);
|
|
80
|
+
});
|
|
69
81
|
};
|