piqnote 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/.github/workflows/ci.yml +73 -0
- package/.piqnoterc +9 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +623 -0
- package/README.md +106 -0
- package/dist/ai/factory.js +24 -0
- package/dist/ai/localProvider.js +23 -0
- package/dist/ai/mockProvider.js +28 -0
- package/dist/ai/openAiProvider.js +85 -0
- package/dist/ai/provider.js +2 -0
- package/dist/analyzer/diffAnalyzer.js +83 -0
- package/dist/analyzer/scorer.js +53 -0
- package/dist/cli.js +236 -0
- package/dist/config/loader.js +39 -0
- package/dist/config/types.js +2 -0
- package/dist/formatter/commitFormatter.js +26 -0
- package/dist/git/gitClient.js +110 -0
- package/package.json +51 -0
- package/src/ai/factory.ts +32 -0
- package/src/ai/localProvider.ts +22 -0
- package/src/ai/mockProvider.ts +30 -0
- package/src/ai/openAiProvider.ts +109 -0
- package/src/ai/provider.ts +19 -0
- package/src/analyzer/diffAnalyzer.ts +96 -0
- package/src/analyzer/scorer.ts +68 -0
- package/src/cli.ts +314 -0
- package/src/config/loader.ts +36 -0
- package/src/config/types.ts +11 -0
- package/src/formatter/commitFormatter.ts +37 -0
- package/src/git/gitClient.ts +96 -0
- package/tsconfig.json +15 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { loadConfig, getDefaultConfig } from "./config/loader";
|
|
6
|
+
import { PiqnoteConfig } from "./config/types";
|
|
7
|
+
import { analyzeDiff } from "./analyzer/diffAnalyzer";
|
|
8
|
+
import { scoreCommit } from "./analyzer/scorer";
|
|
9
|
+
import { formatCommit } from "./formatter/commitFormatter";
|
|
10
|
+
import { getProvider, generateWithProvider } from "./ai/factory";
|
|
11
|
+
import {
|
|
12
|
+
getStagedDiff,
|
|
13
|
+
isGitRepo,
|
|
14
|
+
hasStagedChanges,
|
|
15
|
+
getStagedFiles,
|
|
16
|
+
stageAll,
|
|
17
|
+
commitMessage,
|
|
18
|
+
getBranches,
|
|
19
|
+
getCurrentBranch,
|
|
20
|
+
checkoutBranch,
|
|
21
|
+
createBranch,
|
|
22
|
+
} from "./git/gitClient";
|
|
23
|
+
|
|
24
|
+
interface CliOptions {
|
|
25
|
+
interactive: boolean;
|
|
26
|
+
score: boolean;
|
|
27
|
+
offline: boolean;
|
|
28
|
+
auto: boolean;
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ActionChoice =
|
|
33
|
+
| "accept-commit"
|
|
34
|
+
| "accept-stage"
|
|
35
|
+
| "edit-subject"
|
|
36
|
+
| "edit-full"
|
|
37
|
+
| "regenerate"
|
|
38
|
+
| "skip";
|
|
39
|
+
|
|
40
|
+
function ensureBullets(responseBullets: string[] | undefined, stagedFiles: string[]): string[] {
|
|
41
|
+
if (responseBullets && responseBullets.length) return responseBullets;
|
|
42
|
+
return stagedFiles.slice(0, 5).map((file) => file);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function promptAction(): Promise<ActionChoice> {
|
|
46
|
+
const { action } = await inquirer.prompt([
|
|
47
|
+
{
|
|
48
|
+
type: "list",
|
|
49
|
+
name: "action",
|
|
50
|
+
message: "What do you want to do?",
|
|
51
|
+
choices: [
|
|
52
|
+
{ name: "Accept & commit", value: "accept-commit" },
|
|
53
|
+
{ name: "Accept & stage only", value: "accept-stage" },
|
|
54
|
+
{ name: "Edit subject", value: "edit-subject" },
|
|
55
|
+
{ name: "Edit full message", value: "edit-full" },
|
|
56
|
+
{ name: "Regenerate", value: "regenerate" },
|
|
57
|
+
{ name: "Skip", value: "skip" },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
return action as ActionChoice;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function promptSubjectEdit(subject: string): Promise<string> {
|
|
65
|
+
const { edited } = await inquirer.prompt([
|
|
66
|
+
{
|
|
67
|
+
type: "input",
|
|
68
|
+
name: "edited",
|
|
69
|
+
default: subject,
|
|
70
|
+
message: "Edit subject (<=72 chars)",
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
return edited as string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function promptFullEdit(initial: string): Promise<string> {
|
|
77
|
+
const { edited } = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: "editor",
|
|
80
|
+
name: "edited",
|
|
81
|
+
default: initial,
|
|
82
|
+
message: "Edit full commit message",
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
return edited as string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function promptBranch(cwd: string): Promise<string> {
|
|
89
|
+
const branches = getBranches(cwd);
|
|
90
|
+
const current = getCurrentBranch(cwd);
|
|
91
|
+
const baseChoices = branches
|
|
92
|
+
.filter((b) => b !== current)
|
|
93
|
+
.map((b) => ({ name: b, value: b }));
|
|
94
|
+
|
|
95
|
+
const choices = [
|
|
96
|
+
{ name: `(current) ${current}`, value: current },
|
|
97
|
+
...baseChoices,
|
|
98
|
+
{ name: "Create new branch", value: "__create__" },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const { branch } = await inquirer.prompt([
|
|
102
|
+
{
|
|
103
|
+
type: "list",
|
|
104
|
+
name: "branch",
|
|
105
|
+
message: "Select branch for commit",
|
|
106
|
+
default: current,
|
|
107
|
+
choices,
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
if (branch === "__create__") {
|
|
112
|
+
const { name } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: "input",
|
|
115
|
+
name: "name",
|
|
116
|
+
message: "New branch name",
|
|
117
|
+
validate: (val: string) => (val && val.trim().length > 0 ? true : "Enter a branch name"),
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
createBranch(cwd, name.trim());
|
|
121
|
+
return name.trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (branch !== current) {
|
|
125
|
+
checkoutBranch(cwd, branch);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return branch as string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderSuggestion(message: string, showScore: boolean, diff: string) {
|
|
132
|
+
console.log("\n" + chalk.blue.bold("Piqnote suggestion:"));
|
|
133
|
+
console.log(chalk.green(message));
|
|
134
|
+
|
|
135
|
+
if (showScore) {
|
|
136
|
+
const insights = analyzeDiff(diff);
|
|
137
|
+
const score = scoreCommit(message, insights);
|
|
138
|
+
console.log("\n" + chalk.yellow(`Quality score: ${score.total}/100`));
|
|
139
|
+
score.details.forEach((item) => {
|
|
140
|
+
const label = chalk.gray("-");
|
|
141
|
+
console.log(`${label} ${item.label}: ${item.points} (${item.reason})`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatWithFallback(
|
|
147
|
+
subject: string,
|
|
148
|
+
bullets: string[] | undefined,
|
|
149
|
+
insightsScope: string | undefined,
|
|
150
|
+
config: PiqnoteConfig,
|
|
151
|
+
stagedFiles: string[]
|
|
152
|
+
): string {
|
|
153
|
+
const mergedBullets = ensureBullets(bullets, stagedFiles);
|
|
154
|
+
return formatCommit({ subject, bullets: mergedBullets, insightsScope }, config);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function handleCommit(cwd: string, message: string, dryRun: boolean) {
|
|
158
|
+
if (dryRun) {
|
|
159
|
+
console.log(chalk.yellow("Dry-run: commit not created."));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
stageAll(cwd);
|
|
163
|
+
commitMessage(cwd, message);
|
|
164
|
+
console.log(chalk.green("Commit created."));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function autoFlow(cwd: string, message: string, options: CliOptions, diff: string) {
|
|
168
|
+
renderSuggestion(message, options.score, diff);
|
|
169
|
+
await handleCommit(cwd, message, options.dryRun);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function interactiveFlow(
|
|
173
|
+
cwd: string,
|
|
174
|
+
config: PiqnoteConfig,
|
|
175
|
+
options: CliOptions,
|
|
176
|
+
diff: string,
|
|
177
|
+
providerInput: {
|
|
178
|
+
subject: string;
|
|
179
|
+
bullets: string[];
|
|
180
|
+
insightsScope?: string;
|
|
181
|
+
regenerate: () => Promise<{ subject: string; bullets: string[] }>;
|
|
182
|
+
}
|
|
183
|
+
) {
|
|
184
|
+
let message = formatWithFallback(
|
|
185
|
+
providerInput.subject,
|
|
186
|
+
providerInput.bullets,
|
|
187
|
+
providerInput.insightsScope,
|
|
188
|
+
config,
|
|
189
|
+
getStagedFiles(cwd)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
let loop = true;
|
|
193
|
+
while (loop) {
|
|
194
|
+
renderSuggestion(message, options.score, diff);
|
|
195
|
+
const action = await promptAction();
|
|
196
|
+
|
|
197
|
+
if (action === "edit-subject") {
|
|
198
|
+
const subject = message.split("\n")[0];
|
|
199
|
+
const edited = await promptSubjectEdit(subject);
|
|
200
|
+
const rest = message.split("\n").slice(1);
|
|
201
|
+
message = [edited, ...rest].join("\n");
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (action === "edit-full") {
|
|
206
|
+
message = await promptFullEdit(message);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (action === "regenerate") {
|
|
211
|
+
const next = await providerInput.regenerate();
|
|
212
|
+
message = formatWithFallback(next.subject, next.bullets, providerInput.insightsScope, config, getStagedFiles(cwd));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (action === "accept-stage") {
|
|
217
|
+
stageAll(cwd);
|
|
218
|
+
console.log(chalk.green("Staged changes updated."));
|
|
219
|
+
loop = false;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (action === "accept-commit") {
|
|
224
|
+
const branch = await promptBranch(cwd);
|
|
225
|
+
console.log(chalk.gray(`Using branch: ${branch}`));
|
|
226
|
+
await handleCommit(cwd, message, options.dryRun);
|
|
227
|
+
loop = false;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (action === "skip") {
|
|
232
|
+
console.log("Skipped committing.");
|
|
233
|
+
loop = false;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function main() {
|
|
240
|
+
const program = new Command();
|
|
241
|
+
program
|
|
242
|
+
.name("piqnote")
|
|
243
|
+
.description("Piqnote CLI by PromethIQ - generate commit messages")
|
|
244
|
+
.option("-i, --interactive", "Review interactively")
|
|
245
|
+
.option("--no-interactive", "Disable interactive review")
|
|
246
|
+
.option("--auto", "Commit automatically to current branch", false)
|
|
247
|
+
.option("--dry-run", "Show suggestions only; no commit", false)
|
|
248
|
+
.option("--score", "Show commit quality score", false)
|
|
249
|
+
.option("--offline", "Use offline heuristics", false)
|
|
250
|
+
.version("0.1.0");
|
|
251
|
+
|
|
252
|
+
program.parse(process.argv);
|
|
253
|
+
const raw = program.opts();
|
|
254
|
+
const options: CliOptions = {
|
|
255
|
+
interactive: raw.noInteractive ? false : raw.interactive ?? true,
|
|
256
|
+
score: Boolean(raw.score),
|
|
257
|
+
offline: Boolean(raw.offline),
|
|
258
|
+
auto: Boolean(raw.auto),
|
|
259
|
+
dryRun: Boolean(raw.dryRun),
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const cwd = process.cwd();
|
|
263
|
+
if (!isGitRepo(cwd)) {
|
|
264
|
+
console.error("Piqnote: not a git repository.");
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!hasStagedChanges(cwd)) {
|
|
269
|
+
console.error("Piqnote: no staged changes. Stage files first with 'git add'.");
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const diff = getStagedDiff(cwd);
|
|
274
|
+
const config = loadConfig(cwd) || getDefaultConfig();
|
|
275
|
+
const insights = analyzeDiff(diff);
|
|
276
|
+
const provider = getProvider(config, { offline: options.offline || config.offline });
|
|
277
|
+
const stagedFiles = getStagedFiles(cwd);
|
|
278
|
+
|
|
279
|
+
const generate = async () => {
|
|
280
|
+
const res = await generateWithProvider(provider, {
|
|
281
|
+
diff,
|
|
282
|
+
insights,
|
|
283
|
+
language: config.language,
|
|
284
|
+
style: config.style,
|
|
285
|
+
});
|
|
286
|
+
return res;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const initial = await generate();
|
|
290
|
+
const formattedInitial = formatWithFallback(initial.subject, initial.bullets, insights.scope, config, stagedFiles);
|
|
291
|
+
|
|
292
|
+
if (!options.interactive || options.auto) {
|
|
293
|
+
await autoFlow(cwd, formattedInitial, options, diff);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await interactiveFlow(cwd, config, options, diff, {
|
|
298
|
+
subject: initial.subject,
|
|
299
|
+
bullets: ensureBullets(initial.bullets, stagedFiles),
|
|
300
|
+
insightsScope: insights.scope,
|
|
301
|
+
regenerate: async () => {
|
|
302
|
+
const next = await generate();
|
|
303
|
+
return {
|
|
304
|
+
subject: next.subject,
|
|
305
|
+
bullets: ensureBullets(next.bullets, getStagedFiles(cwd)),
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
main().catch((error) => {
|
|
312
|
+
console.error("Piqnote failed:", error instanceof Error ? error.message : error);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { PiqnoteConfig } from "./types";
|
|
4
|
+
|
|
5
|
+
const defaultConfig: PiqnoteConfig = {
|
|
6
|
+
style: "conventional",
|
|
7
|
+
scope: undefined,
|
|
8
|
+
maxSubjectLength: 72,
|
|
9
|
+
language: "en",
|
|
10
|
+
bulletPrefix: "-",
|
|
11
|
+
provider: "mock",
|
|
12
|
+
offline: false,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function loadConfig(cwd: string = process.cwd()): PiqnoteConfig {
|
|
16
|
+
const configPath = path.join(cwd, ".piqnoterc");
|
|
17
|
+
if (!fs.existsSync(configPath)) {
|
|
18
|
+
return defaultConfig;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
return {
|
|
25
|
+
...defaultConfig,
|
|
26
|
+
...parsed,
|
|
27
|
+
} as PiqnoteConfig;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.warn("Piqnote: failed to parse .piqnoterc, using defaults.");
|
|
30
|
+
return defaultConfig;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getDefaultConfig(): PiqnoteConfig {
|
|
35
|
+
return { ...defaultConfig };
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type CommitStyle = "conventional" | "plain";
|
|
2
|
+
|
|
3
|
+
export interface PiqnoteConfig {
|
|
4
|
+
style: CommitStyle;
|
|
5
|
+
scope?: string;
|
|
6
|
+
maxSubjectLength: number;
|
|
7
|
+
language: string;
|
|
8
|
+
bulletPrefix: string;
|
|
9
|
+
provider: "mock" | "local" | "openai";
|
|
10
|
+
offline?: boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PiqnoteConfig } from "../config/types";
|
|
2
|
+
|
|
3
|
+
function truncate(text: string, max: number): string {
|
|
4
|
+
if (text.length <= max) return text;
|
|
5
|
+
return text.slice(0, max - 3).trimEnd() + "...";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function ensureConventional(subject: string, scope?: string): string {
|
|
9
|
+
const conventionalRegex = /^(feat|fix|chore|docs|refactor|test|perf|build|ci|style|revert)(\(.+\))?:/;
|
|
10
|
+
if (conventionalRegex.test(subject)) {
|
|
11
|
+
return subject;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const scoped = scope ? `chore(${scope}): ${subject}` : `chore: ${subject}`;
|
|
15
|
+
return scoped;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CommitMessagePayload {
|
|
19
|
+
subject: string;
|
|
20
|
+
bullets: string[];
|
|
21
|
+
insightsScope?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatCommit(
|
|
25
|
+
payload: CommitMessagePayload,
|
|
26
|
+
config: PiqnoteConfig
|
|
27
|
+
): string {
|
|
28
|
+
const baseSubject = truncate(payload.subject, config.maxSubjectLength || 72);
|
|
29
|
+
const scoped =
|
|
30
|
+
config.style === "conventional"
|
|
31
|
+
? ensureConventional(baseSubject, config.scope || payload.insightsScope)
|
|
32
|
+
: baseSubject;
|
|
33
|
+
const bullets = payload.bullets?.length
|
|
34
|
+
? payload.bullets.map((b) => `${config.bulletPrefix} ${b.trim()}`)
|
|
35
|
+
: [];
|
|
36
|
+
return [scoped, ...bullets].join("\n");
|
|
37
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
function runGit(command: string, cwd: string): string {
|
|
6
|
+
try {
|
|
7
|
+
return execSync(`git ${command}`, {
|
|
8
|
+
cwd,
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
encoding: "utf-8",
|
|
11
|
+
}).trim();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
14
|
+
throw new Error(`Git command failed: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isGitRepo(cwd: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
runGit("rev-parse --is-inside-work-tree", cwd);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hasStagedChanges(cwd: string): boolean {
|
|
28
|
+
try {
|
|
29
|
+
const output = runGit("diff --cached --name-only", cwd);
|
|
30
|
+
return output.length > 0;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getStagedDiff(cwd: string): string {
|
|
37
|
+
return runGit("diff --cached", cwd);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getStagedFiles(cwd: string): string[] {
|
|
41
|
+
try {
|
|
42
|
+
const output = runGit("diff --cached --name-only", cwd);
|
|
43
|
+
if (!output) return [];
|
|
44
|
+
return output.split("\n").filter(Boolean);
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function stageAll(cwd: string): void {
|
|
51
|
+
runGit("add -A", cwd);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function commitMessage(cwd: string, message: string): void {
|
|
55
|
+
const tempDir = fs.mkdtempSync(path.join(cwd, ".piqnote-"));
|
|
56
|
+
const filePath = path.join(tempDir, "message.txt");
|
|
57
|
+
fs.writeFileSync(filePath, message, "utf-8");
|
|
58
|
+
try {
|
|
59
|
+
runGit(`commit -F "${filePath}"`, cwd);
|
|
60
|
+
} finally {
|
|
61
|
+
try {
|
|
62
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getBranches(cwd: string): string[] {
|
|
69
|
+
try {
|
|
70
|
+
const output = runGit("branch --list --format='%(refname:short)'", cwd);
|
|
71
|
+
return output
|
|
72
|
+
.split("\n")
|
|
73
|
+
.map((b) => b.replace(/'/g, "").trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getCurrentBranch(cwd: string): string {
|
|
81
|
+
try {
|
|
82
|
+
const name = runGit("rev-parse --abbrev-ref HEAD", cwd);
|
|
83
|
+
if (name && name !== "HEAD") return name;
|
|
84
|
+
} catch {
|
|
85
|
+
/* fallthrough */
|
|
86
|
+
}
|
|
87
|
+
return "main";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function checkoutBranch(cwd: string, branch: string): void {
|
|
91
|
+
runGit(`checkout ${branch}`, cwd);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createBranch(cwd: string, branch: string): void {
|
|
95
|
+
runGit(`checkout -b ${branch}`, cwd);
|
|
96
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|