ag-toolkit 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.
package/dist/git.js ADDED
@@ -0,0 +1,418 @@
1
+ import { simpleGit } from "simple-git";
2
+ import { isValidBranchName } from "./lib/isValidBranchName.js";
3
+ const BRANCH_LIST_FORMAT = "%(refname)%00%(committerdate:iso8601)%00%(objectname)%00%(contents:subject)";
4
+ export class GitOperations {
5
+ git;
6
+ constructor(workingDir) {
7
+ const options = {
8
+ baseDir: workingDir || process.cwd(),
9
+ binary: "git",
10
+ maxConcurrentProcesses: 10,
11
+ config: [],
12
+ trimmed: false,
13
+ };
14
+ this.git = simpleGit(options);
15
+ }
16
+ async getCurrentBranch() {
17
+ const status = await this.git.status();
18
+ return status.current || "HEAD";
19
+ }
20
+ async isWorkdirClean() {
21
+ const status = await this.git.status();
22
+ return status.isClean();
23
+ }
24
+ async fetchAll() {
25
+ await this.git.fetch(["--all"]);
26
+ }
27
+ async getBranchInfos(options = {}) {
28
+ const includeRemote = options.includeRemote === true;
29
+ const [localMerged, remoteMerged, baseBranch] = await Promise.all([
30
+ this.getMergedSet("local"),
31
+ includeRemote
32
+ ? this.getMergedSet("remote")
33
+ : Promise.resolve(new Set()),
34
+ this.getBaseBranch(),
35
+ ]);
36
+ const localBranches = await this.listBranches("refs/heads/", "local");
37
+ const remoteBranches = includeRemote
38
+ ? await this.listBranches("refs/remotes/", "remote")
39
+ : [];
40
+ const allBranches = [...localBranches, ...remoteBranches];
41
+ const withCommitCounts = await Promise.all(allBranches.map(async (branch) => {
42
+ const { ahead, behind } = baseBranch
43
+ ? await this.getAheadBehind(branch.ref, baseBranch)
44
+ : { ahead: 0, behind: 0 };
45
+ const isMerged = branch.type === "local"
46
+ ? localMerged.has(branch.ref)
47
+ : remoteMerged.has(branch.ref);
48
+ return { ...branch, isMerged, ahead, behind };
49
+ }));
50
+ return withCommitCounts.sort((a, b) => {
51
+ if (a.type !== b.type) {
52
+ return a.type === "local" ? -1 : 1;
53
+ }
54
+ const dateA = a.lastCommitDate?.getTime() ?? 0;
55
+ const dateB = b.lastCommitDate?.getTime() ?? 0;
56
+ return dateB - dateA;
57
+ });
58
+ }
59
+ async deleteLocalBranch(branch, options = {}) {
60
+ if (!isValidBranchName(branch)) {
61
+ throw new Error(`Invalid branch name: ${branch}`);
62
+ }
63
+ const args = ["branch", options.force ? "-D" : "-d", branch];
64
+ await this.git.raw(args);
65
+ }
66
+ async deleteRemoteBranch(branch) {
67
+ const { remote, name } = branch;
68
+ if (!isValidBranchName(name)) {
69
+ throw new Error(`Invalid branch name: ${name}`);
70
+ }
71
+ await this.git.push([remote, "--delete", name]);
72
+ }
73
+ async detectDefaultBranch(remote = "origin") {
74
+ try {
75
+ const remoteInfo = await this.git.raw(["remote", "show", remote]);
76
+ const match = remoteInfo.match(/HEAD branch:\s*(.+)/);
77
+ if (match?.[1]) {
78
+ return match[1].trim();
79
+ }
80
+ }
81
+ catch (error) {
82
+ if (error instanceof Error && error.message.includes("No such remote")) {
83
+ throw error;
84
+ }
85
+ }
86
+ try {
87
+ await this.git.raw(["remote", "set-head", remote, "--auto"]);
88
+ const symbolicRef = await this.git.raw([
89
+ "symbolic-ref",
90
+ `refs/remotes/${remote}/HEAD`,
91
+ ]);
92
+ const ref = symbolicRef.trim();
93
+ if (!ref) {
94
+ return null;
95
+ }
96
+ const parts = ref.split("/");
97
+ const branchName = parts.at(-1);
98
+ return branchName ?? null;
99
+ }
100
+ catch (error) {
101
+ if (error instanceof Error && error.message.includes("No such remote")) {
102
+ throw error;
103
+ }
104
+ return null;
105
+ }
106
+ }
107
+ async getBaseBranch() {
108
+ const { all: remoteBranches } = await this.git.branch(["-r"]);
109
+ const defaultRemoteBranches = [
110
+ "origin/main",
111
+ "origin/master",
112
+ "origin/develop",
113
+ ];
114
+ for (const branch of defaultRemoteBranches) {
115
+ if (remoteBranches.includes(branch.trim())) {
116
+ return branch.trim();
117
+ }
118
+ }
119
+ const { all: localBranches } = await this.git.branchLocal();
120
+ const defaultLocalBranches = ["main", "master", "develop"];
121
+ for (const branch of defaultLocalBranches) {
122
+ if (localBranches.includes(branch)) {
123
+ return branch;
124
+ }
125
+ }
126
+ return null;
127
+ }
128
+ async getAheadBehind(branch, base) {
129
+ if (branch === base) {
130
+ return { ahead: 0, behind: 0 };
131
+ }
132
+ try {
133
+ // `git rev-list --left-right --count base...branch` returns "behind ahead"
134
+ const result = await this.git.raw([
135
+ "rev-list",
136
+ "--left-right",
137
+ "--count",
138
+ `${base}...${branch}`,
139
+ ]);
140
+ const [behind, ahead] = result.trim().split("\t").map(Number);
141
+ return { ahead: ahead ?? 0, behind: behind ?? 0 };
142
+ }
143
+ catch {
144
+ return { ahead: 0, behind: 0 };
145
+ }
146
+ }
147
+ async getMergedSet(type) {
148
+ const args = type === "local" ? ["branch", "--merged"] : ["branch", "-r", "--merged"];
149
+ const output = await this.git.raw(args);
150
+ const lines = output
151
+ .split("\n")
152
+ .map((line) => line.replace("*", "").trim());
153
+ const filtered = lines
154
+ .filter(Boolean)
155
+ .filter((name) => !name.includes(" -> "))
156
+ .map((name) => (type === "remote" ? name : name));
157
+ return new Set(filtered);
158
+ }
159
+ async listBranches(refPrefix, type) {
160
+ const output = await this.git.raw([
161
+ "for-each-ref",
162
+ `--format=${BRANCH_LIST_FORMAT.replace("%(refname:short)", "%(refname)")}`,
163
+ refPrefix,
164
+ ]);
165
+ return output
166
+ .split("\n")
167
+ .map((line) => line.trim())
168
+ .filter(Boolean)
169
+ .map((line) => this.parseBranchLine(line, type))
170
+ .filter((branch) => branch !== null);
171
+ }
172
+ parseBranchLine(line, type) {
173
+ const [ref, dateStr, sha, subject] = line.split("\u0000");
174
+ const shortRef = ref?.replace(/^refs\/heads\//, "").replace(/^refs\/remotes\//, "") ?? "";
175
+ if (!shortRef || shortRef.endsWith("/HEAD")) {
176
+ return null;
177
+ }
178
+ let lastCommitDate = null;
179
+ if (dateStr) {
180
+ const parsed = new Date(dateStr);
181
+ if (!Number.isNaN(parsed.getTime())) {
182
+ lastCommitDate = parsed;
183
+ }
184
+ }
185
+ const trimmedSubject = subject?.trim() ?? null;
186
+ const lastCommitSubject = trimmedSubject?.length ? trimmedSubject : null;
187
+ const lastCommitSha = sha?.trim()?.length ? sha.trim() : null;
188
+ if (type === "remote") {
189
+ const [remote, ...nameParts] = shortRef.split("/");
190
+ const name = nameParts.join("/");
191
+ if (!(remote && name) || name === "HEAD") {
192
+ return null;
193
+ }
194
+ return {
195
+ ref: shortRef,
196
+ name,
197
+ type,
198
+ remote,
199
+ lastCommitDate,
200
+ lastCommitSha,
201
+ lastCommitSubject,
202
+ };
203
+ }
204
+ return {
205
+ ref: shortRef,
206
+ name: shortRef,
207
+ type,
208
+ lastCommitDate,
209
+ lastCommitSha,
210
+ lastCommitSubject,
211
+ };
212
+ }
213
+ async getAllBranches() {
214
+ const branches = await this.git.branch(["-r"]);
215
+ return branches.all
216
+ .filter((branch) => typeof branch === "string" &&
217
+ branch.startsWith("origin/") &&
218
+ !branch.includes("HEAD"))
219
+ .map((branch) => branch.replace("origin/", ""));
220
+ }
221
+ async getLocalBranches() {
222
+ const branches = await this.git.branch(["-l"]);
223
+ return branches.all
224
+ .filter((branch) => typeof branch === "string" && !branch.includes("HEAD"))
225
+ .map((branch) => branch.replace("* ", ""));
226
+ }
227
+ async branchExists(branch) {
228
+ if (!isValidBranchName(branch)) {
229
+ throw new Error(`Invalid branch name: ${branch}`);
230
+ }
231
+ try {
232
+ await this.git.raw([
233
+ "show-ref",
234
+ "--verify",
235
+ "--quiet",
236
+ `refs/remotes/origin/${branch}`,
237
+ ]);
238
+ return true;
239
+ }
240
+ catch {
241
+ try {
242
+ await this.git.raw([
243
+ "show-ref",
244
+ "--verify",
245
+ "--quiet",
246
+ `refs/heads/${branch}`,
247
+ ]);
248
+ return true;
249
+ }
250
+ catch {
251
+ return false;
252
+ }
253
+ }
254
+ }
255
+ async resolveBranchRef(branch) {
256
+ try {
257
+ await this.git.raw([
258
+ "show-ref",
259
+ "--verify",
260
+ "--quiet",
261
+ `refs/remotes/origin/${branch}`,
262
+ ]);
263
+ return `origin/${branch}`;
264
+ }
265
+ catch {
266
+ return branch;
267
+ }
268
+ }
269
+ async setupCherryPick(targetBranch) {
270
+ if (!isValidBranchName(targetBranch)) {
271
+ throw new Error(`Invalid branch name: ${targetBranch}`);
272
+ }
273
+ const tempBranchName = `temp-rebase-${process.pid}`;
274
+ const checkoutTarget = await this.resolveBranchRef(targetBranch);
275
+ await this.git.checkout(["-b", tempBranchName, checkoutTarget]);
276
+ return tempBranchName;
277
+ }
278
+ async getMergeBase(branch1, branch2) {
279
+ if (!isValidBranchName(branch1)) {
280
+ throw new Error(`Invalid branch name: ${branch1}`);
281
+ }
282
+ if (!isValidBranchName(branch2)) {
283
+ throw new Error(`Invalid branch name: ${branch2}`);
284
+ }
285
+ const baseBranch = await this.resolveBranchRef(branch1);
286
+ const result = await this.git.raw(["merge-base", baseBranch, branch2]);
287
+ return result.trim();
288
+ }
289
+ async getCommitsToCherryPick(from, to) {
290
+ const result = await this.git.raw([
291
+ "rev-list",
292
+ "--reverse",
293
+ "--no-merges",
294
+ `${from}..${to}`,
295
+ ]);
296
+ return result.trim().split("\n").filter(Boolean);
297
+ }
298
+ async cherryPick(commitSha, options) {
299
+ const args = ["cherry-pick"];
300
+ if (options?.allowEmpty) {
301
+ args.push("--allow-empty");
302
+ }
303
+ args.push(commitSha);
304
+ await this.git.raw(args);
305
+ }
306
+ async continueCherryPick() {
307
+ await this.git.raw(["cherry-pick", "--continue"]);
308
+ }
309
+ async skipCherryPick() {
310
+ await this.git.raw(["cherry-pick", "--skip"]);
311
+ }
312
+ async abortCherryPick() {
313
+ try {
314
+ await this.git.raw(["cherry-pick", "--abort"]);
315
+ }
316
+ catch { }
317
+ }
318
+ async resolveConflictWithStrategy(strategy) {
319
+ await this.git.checkout([`--${strategy}`, "."]);
320
+ await this.git.add(".");
321
+ }
322
+ async finishCherryPick(currentBranch, tempBranchName, options) {
323
+ await this.git.checkout(currentBranch);
324
+ if (options?.createBackup) {
325
+ try {
326
+ const currentSha = (await this.git.revparse([currentBranch])).trim();
327
+ const safeBranch = currentBranch.replace(/\//g, "-");
328
+ const tagName = `agrb-backup-${safeBranch}-${Date.now()}`;
329
+ await this.git.addAnnotatedTag(tagName, `Backup before agrb reset: ${currentBranch} @ ${currentSha}`);
330
+ }
331
+ catch (error) {
332
+ throw new Error(`Failed to create backup tag: ${error instanceof Error ? error.message : String(error)}`);
333
+ }
334
+ }
335
+ await this.git.raw(["reset", "--hard", tempBranchName]);
336
+ }
337
+ async cleanupCherryPick(tempBranchName, originalBranch) {
338
+ try {
339
+ const current = await this.getCurrentBranch();
340
+ if (current === tempBranchName) {
341
+ await this.git.checkout(originalBranch);
342
+ }
343
+ await this.git.branch(["-D", tempBranchName]);
344
+ }
345
+ catch { }
346
+ }
347
+ async performLinearRebase(currentBranch, targetBranch, progressCallback, options) {
348
+ if (!isValidBranchName(targetBranch)) {
349
+ throw new Error(`Invalid branch name: ${targetBranch}`);
350
+ }
351
+ try {
352
+ progressCallback?.("Fetching all branches...");
353
+ await this.fetchAll();
354
+ progressCallback?.("Checking if target branch exists...");
355
+ if (!(await this.branchExists(targetBranch))) {
356
+ throw new Error(`Target branch '${targetBranch}' does not exist`);
357
+ }
358
+ progressCallback?.("Starting linear rebase...");
359
+ await this.git.checkout(currentBranch);
360
+ const rebaseTarget = await this.resolveBranchRef(targetBranch);
361
+ const rebaseArgs = ["rebase", rebaseTarget];
362
+ if (options?.continueOnConflict) {
363
+ rebaseArgs.push("-X", "ours");
364
+ }
365
+ await this.git.raw(rebaseArgs);
366
+ progressCallback?.("Linear rebase completed successfully");
367
+ }
368
+ catch (error) {
369
+ if (options?.continueOnConflict) {
370
+ try {
371
+ progressCallback?.("Conflicts detected, auto-resolving and continuing...");
372
+ await this.git.checkout(["--ours", "."]);
373
+ await this.git.add(".");
374
+ await this.git.raw(["rebase", "--continue"]);
375
+ progressCallback?.("Linear rebase completed with conflicts auto-resolved");
376
+ }
377
+ catch (e) {
378
+ try {
379
+ await this.git.raw(["rebase", "--abort"]);
380
+ }
381
+ catch { }
382
+ throw new Error(`Linear rebase failed during continue: ${e instanceof Error ? e.message : String(e)}`);
383
+ }
384
+ }
385
+ else {
386
+ try {
387
+ await this.git.raw(["rebase", "--abort"]);
388
+ }
389
+ catch { }
390
+ throw new Error(`Linear rebase failed: ${error instanceof Error ? error.message : String(error)}`);
391
+ }
392
+ }
393
+ }
394
+ async getCommitSubject(sha) {
395
+ const out = await this.git.raw(["show", "-s", "--format=%s", sha]);
396
+ return out.trim();
397
+ }
398
+ async startAutostash() {
399
+ const label = `agrb-${process.pid}`;
400
+ await this.git.raw(["stash", "push", "-u", "-m", label]);
401
+ const list = await this.git.raw(["stash", "list", "--format=%gd %gs"]);
402
+ const line = list
403
+ .split("\n")
404
+ .map((l) => l.trim())
405
+ .find((l) => l.includes(label));
406
+ if (!line) {
407
+ return null;
408
+ }
409
+ const ref = line.split(" ")[0];
410
+ return ref || null;
411
+ }
412
+ async popStash(stashRef) {
413
+ await this.git.raw(["stash", "pop", stashRef]);
414
+ }
415
+ async pushWithLease(branch) {
416
+ await this.git.push(["-u", "origin", branch, "--force-with-lease"]);
417
+ }
418
+ }
@@ -0,0 +1 @@
1
+ export * from "./useSearchFilter.js";
@@ -0,0 +1 @@
1
+ export * from "./useSearchFilter.js";
@@ -0,0 +1 @@
1
+ export declare const useSearchFilter: <T extends Record<string, string | number | boolean | null | undefined>>(items: T[], searchTerm: string, searchableFields: (keyof T)[]) => T[];
@@ -0,0 +1,22 @@
1
+ import { useMemo } from "react";
2
+ export const useSearchFilter = (items, searchTerm, searchableFields) => {
3
+ return useMemo(() => {
4
+ if (!searchTerm.trim()) {
5
+ return items;
6
+ }
7
+ const terms = searchTerm
8
+ .toLowerCase()
9
+ .split(/\s+/)
10
+ .filter((term) => term.length > 0);
11
+ if (terms.length === 0) {
12
+ return items;
13
+ }
14
+ return items.filter((item) => terms.every((term) => searchableFields.some((field) => {
15
+ const value = item[field];
16
+ if (typeof value === "string") {
17
+ return value.toLowerCase().includes(term);
18
+ }
19
+ return false;
20
+ })));
21
+ }, [items, searchTerm, searchableFields]);
22
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./components/index.js";
2
+ export * from "./git.js";
3
+ export * from "./hooks/index.js";
4
+ export * from "./lib/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./components/index.js";
2
+ export * from "./git.js";
3
+ export * from "./hooks/index.js";
4
+ export * from "./lib/index.js";
@@ -0,0 +1,30 @@
1
+ interface FlagConfig {
2
+ type: "string" | "boolean";
3
+ shortFlag?: string;
4
+ }
5
+ type FlagsSchema = {
6
+ [key: string]: FlagConfig;
7
+ };
8
+ type Flags<T extends FlagsSchema> = {
9
+ [K in keyof T]: T[K]["type"] extends "string" ? string | undefined : boolean | undefined;
10
+ };
11
+ interface ParsedArgs<T extends FlagsSchema> {
12
+ flags: Flags<T>;
13
+ input: string[];
14
+ help?: string;
15
+ version?: string;
16
+ }
17
+ export declare class ArgParser<T extends FlagsSchema> {
18
+ private readonly schema;
19
+ private readonly helpMessage;
20
+ private readonly version;
21
+ private readonly aliases;
22
+ private readonly longFlagMap;
23
+ constructor(config: {
24
+ schema: T;
25
+ helpMessage: string;
26
+ version: string;
27
+ });
28
+ parse(args: readonly string[]): ParsedArgs<T>;
29
+ }
30
+ export {};
@@ -0,0 +1,84 @@
1
+ export class ArgParser {
2
+ schema;
3
+ helpMessage;
4
+ version;
5
+ aliases = {};
6
+ longFlagMap = {};
7
+ constructor(config) {
8
+ this.schema = config.schema;
9
+ this.helpMessage = config.helpMessage;
10
+ this.version = config.version;
11
+ const camelToKebab = (str) => str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
12
+ for (const longFlag in this.schema) {
13
+ if (Object.hasOwn(this.schema, longFlag)) {
14
+ const flagConfig = this.schema[longFlag];
15
+ const kebabCaseFlag = camelToKebab(longFlag);
16
+ this.longFlagMap[`--${kebabCaseFlag}`] = longFlag;
17
+ if (flagConfig?.shortFlag) {
18
+ this.aliases[`-${flagConfig.shortFlag}`] = `--${kebabCaseFlag}`;
19
+ }
20
+ }
21
+ }
22
+ }
23
+ parse(args) {
24
+ const flags = {};
25
+ const input = [];
26
+ const remainingArgs = [...args];
27
+ for (const longFlag in this.schema) {
28
+ if (Object.hasOwn(this.schema, longFlag)) {
29
+ flags[longFlag] = undefined;
30
+ }
31
+ }
32
+ if (remainingArgs.includes("-h") || remainingArgs.includes("--help")) {
33
+ return { flags: flags, input, help: this.helpMessage };
34
+ }
35
+ if (remainingArgs.includes("-v") || remainingArgs.includes("--version")) {
36
+ return { flags: flags, input, version: this.version };
37
+ }
38
+ let parsingFlags = true;
39
+ while (remainingArgs.length > 0) {
40
+ const arg = remainingArgs.shift();
41
+ if (arg === "--") {
42
+ parsingFlags = false;
43
+ input.push(...remainingArgs);
44
+ break;
45
+ }
46
+ if (parsingFlags) {
47
+ let argName = arg;
48
+ let argValue;
49
+ if (arg.includes("=")) {
50
+ const parts = arg.split("=");
51
+ if (parts[0] !== undefined) {
52
+ argName = parts[0];
53
+ }
54
+ argValue = parts[1];
55
+ }
56
+ const longFlagArg = this.aliases[argName] ?? argName;
57
+ const longFlag = this.longFlagMap[longFlagArg];
58
+ if (longFlag) {
59
+ const flagConfig = this.schema[longFlag];
60
+ if (flagConfig?.type === "boolean") {
61
+ flags[longFlag] = true;
62
+ }
63
+ else if (flagConfig?.type === "string") {
64
+ const value = argValue ?? remainingArgs.shift();
65
+ if (value === undefined) {
66
+ throw new Error(`Flag --${String(longFlag)} requires a value.`);
67
+ }
68
+ flags[longFlag] = value;
69
+ }
70
+ }
71
+ else {
72
+ if (argName.startsWith("-")) {
73
+ throw new Error(`Unknown option: ${argName}`);
74
+ }
75
+ input.push(arg);
76
+ }
77
+ }
78
+ else {
79
+ input.push(arg);
80
+ }
81
+ }
82
+ return { flags: flags, input };
83
+ }
84
+ }
@@ -0,0 +1,11 @@
1
+ import { type ComponentType } from "react";
2
+ import type { Config, ConfigResult } from "./config.js";
3
+ export declare const handleVersionAndHelp: (version: string | null | undefined, help: string | null | undefined, packageName: string, packageVersion: string) => void;
4
+ type ConfigCommands<T extends Config> = {
5
+ getConfig: () => Promise<ConfigResult<T>>;
6
+ getGlobalConfigPath: () => string;
7
+ resetGlobalConfig: () => Promise<void>;
8
+ ConfigEditorComponent: ComponentType;
9
+ };
10
+ export declare const handleConfigCommand: <T extends Config>(command: string, commands: ConfigCommands<T>) => Promise<void>;
11
+ export {};
@@ -0,0 +1,60 @@
1
+ import chalk from "chalk";
2
+ import { render } from "ink";
3
+ import { createElement } from "react";
4
+ export const handleVersionAndHelp = (version, help, packageName, packageVersion) => {
5
+ if (help) {
6
+ console.log(help);
7
+ process.exit(0);
8
+ }
9
+ if (version) {
10
+ console.log(`${packageName} version: ${packageVersion}`);
11
+ process.exit(0);
12
+ }
13
+ };
14
+ export const handleConfigCommand = async (command, commands) => {
15
+ const { getConfig, getGlobalConfigPath, resetGlobalConfig, ConfigEditorComponent, } = commands;
16
+ switch (command) {
17
+ case "show": {
18
+ const { config, sources } = await getConfig();
19
+ console.log("Current configuration:");
20
+ const sourceColors = {
21
+ default: chalk.gray,
22
+ global: chalk.blue,
23
+ local: chalk.green,
24
+ };
25
+ for (const key in config) {
26
+ const k = key;
27
+ const source = sources[k] || "default";
28
+ const color = sourceColors[source] ?? chalk.white;
29
+ console.log(` ${chalk.cyan(k)}: ${chalk.yellow(String(config[k]))} ${color(`(${source})`)}`);
30
+ }
31
+ break;
32
+ }
33
+ case "edit": {
34
+ // biome-ignore lint/complexity/useLiteralKeys: ProcessEnv requires bracket access
35
+ const editor = process.env["EDITOR"] || "vim";
36
+ const { spawn } = await import("node:child_process");
37
+ const child = spawn(editor, [getGlobalConfigPath()], {
38
+ stdio: "inherit",
39
+ env: process.env,
40
+ });
41
+ child.on("exit", (code) => {
42
+ process.exit(code ?? 0);
43
+ });
44
+ return;
45
+ }
46
+ case "reset": {
47
+ await resetGlobalConfig();
48
+ console.log(`Configuration reset to default: ${getGlobalConfigPath()}`);
49
+ break;
50
+ }
51
+ case "set": {
52
+ render(createElement(ConfigEditorComponent));
53
+ return;
54
+ }
55
+ default:
56
+ console.error(`Unknown config command: ${command}`);
57
+ console.log("Available commands: show, edit, reset, set");
58
+ process.exit(1);
59
+ }
60
+ };
@@ -0,0 +1,25 @@
1
+ export type Config = Record<string, unknown>;
2
+ export declare const findUp: (name: string, startDir: string) => Promise<string | null>;
3
+ export declare const readConfigFile: <T extends Config>(filePath: string, validate: (config: unknown) => T) => Promise<T | null>;
4
+ export declare const writeConfig: <T extends Config>(filePath: string, config: T) => Promise<void>;
5
+ export interface ConfigResult<T extends Config> {
6
+ config: T;
7
+ sources: Partial<Record<keyof T, "default" | "global" | "local">>;
8
+ }
9
+ export declare const loadConfig: <T extends Config>(options: {
10
+ toolName: string;
11
+ configFile: string;
12
+ localConfigFile: string;
13
+ defaultConfig: T;
14
+ validate: (config: unknown) => T;
15
+ }, cwd?: string) => Promise<ConfigResult<T>>;
16
+ export declare const getGlobalConfigPath: (toolName: string, configFile: string) => string;
17
+ export declare const findLocalConfigPath: (localConfigFile: string, cwd?: string) => Promise<string | null>;
18
+ export declare const writeLocalConfig: <T extends Config>(localConfigPath: string, config: T) => Promise<void>;
19
+ export declare const loadLocalConfig: <T extends Config>(localConfigFile: string, validate: (config: unknown) => T, cwd?: string) => Promise<{
20
+ path: string;
21
+ config: T | null;
22
+ exists: boolean;
23
+ }>;
24
+ export declare const writeGlobalConfig: <T extends Config>(config: T, toolName: string, configFile: string) => Promise<void>;
25
+ export declare const resetGlobalConfig: <T extends Config>(defaultConfig: T, toolName: string, configFile: string) => Promise<void>;