fireflyy 3.0.11 → 4.0.0-dev.352994a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -418
- package/assets/firefly.schema.json +118 -112
- package/dist/dry-run-BfYCtldz.js +38 -0
- package/dist/filesystem.service-DdVwnqoa.js +57 -0
- package/dist/git.service-DarjfyXF.js +587 -0
- package/dist/index.d.ts +43 -23
- package/dist/index.js +20 -1
- package/dist/main.js +110 -16
- package/dist/package-json.service-QN7SzRTt.js +70 -0
- package/dist/program-DSPj4l5A.js +3457 -0
- package/dist/result.constructors-C9M1MP3_.js +261 -0
- package/dist/result.utilities-B03Jkhlx.js +32 -0
- package/dist/schema.utilities-BGd9t1wm.js +60 -0
- package/package.json +88 -84
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { t as logger } from "./main.js";
|
|
2
|
+
import { i as FireflyOkAsync, m as failedError, o as failedErrAsync } from "./result.constructors-C9M1MP3_.js";
|
|
3
|
+
import { t as withDryRun } from "./dry-run-BfYCtldz.js";
|
|
4
|
+
import { ResultAsync } from "neverthrow";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/infrastructure/executors/git-command.executor.ts
|
|
8
|
+
/**
|
|
9
|
+
* Git commands that modify repository state.
|
|
10
|
+
* These commands are skipped during dry-run mode.
|
|
11
|
+
*/
|
|
12
|
+
const SIDE_EFFECT_COMMANDS = new Set([
|
|
13
|
+
"add",
|
|
14
|
+
"am",
|
|
15
|
+
"apply",
|
|
16
|
+
"branch",
|
|
17
|
+
"checkout",
|
|
18
|
+
"cherry-pick",
|
|
19
|
+
"clean",
|
|
20
|
+
"commit",
|
|
21
|
+
"fetch",
|
|
22
|
+
"merge",
|
|
23
|
+
"mv",
|
|
24
|
+
"pull",
|
|
25
|
+
"push",
|
|
26
|
+
"rebase",
|
|
27
|
+
"reset",
|
|
28
|
+
"restore",
|
|
29
|
+
"revert",
|
|
30
|
+
"rm",
|
|
31
|
+
"stash",
|
|
32
|
+
"switch",
|
|
33
|
+
"tag",
|
|
34
|
+
"worktree"
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* Git commands that benefit from streaming execution.
|
|
38
|
+
* These commands often produce large outputs that should be processed incrementally.
|
|
39
|
+
*/
|
|
40
|
+
const STREAMING_COMMANDS = new Set([
|
|
41
|
+
"rev-list",
|
|
42
|
+
"log",
|
|
43
|
+
"show",
|
|
44
|
+
"diff",
|
|
45
|
+
"blame",
|
|
46
|
+
"cat-file"
|
|
47
|
+
]);
|
|
48
|
+
const gitArgsSchema = z.array(z.string().min(1));
|
|
49
|
+
/**
|
|
50
|
+
* Determines if the given arguments contain commands that modify repository state.
|
|
51
|
+
*
|
|
52
|
+
* @param args - Git command arguments to check
|
|
53
|
+
* @returns `true` if any argument is a side-effect command
|
|
54
|
+
*/
|
|
55
|
+
function hasSideEffects(args) {
|
|
56
|
+
if (args.length === 0) return false;
|
|
57
|
+
return args.map((token) => token.toLowerCase()).some((token) => SIDE_EFFECT_COMMANDS.has(token));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Determines if the given arguments should use streaming execution.
|
|
61
|
+
*
|
|
62
|
+
* @param args - Git command arguments to check
|
|
63
|
+
* @returns `true` if streaming execution is recommended
|
|
64
|
+
*/
|
|
65
|
+
function shouldUseStreaming(args) {
|
|
66
|
+
if (args.length === 0) return false;
|
|
67
|
+
return args.map((token) => token.toLowerCase()).some((token) => STREAMING_COMMANDS.has(token));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reads a process stdout stream into a string with abort support.
|
|
71
|
+
*
|
|
72
|
+
* @param args - Git command arguments
|
|
73
|
+
* @param spawnOptions - Spawn configuration for the Git process
|
|
74
|
+
* @param signal - Optional abort signal for cancellation
|
|
75
|
+
* @returns Promise resolving to the complete stdout output
|
|
76
|
+
*/
|
|
77
|
+
function streamToString(args, spawnOptions, signal) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const proc = Bun.spawn(["git", ...args], spawnOptions);
|
|
80
|
+
if (signal) {
|
|
81
|
+
const onAbort = () => {
|
|
82
|
+
proc.kill();
|
|
83
|
+
reject(new Error("Git command aborted", { cause: signal.reason }));
|
|
84
|
+
};
|
|
85
|
+
if (signal.aborted) {
|
|
86
|
+
onAbort();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
90
|
+
}
|
|
91
|
+
const chunks = [];
|
|
92
|
+
const reader = proc.stdout.getReader();
|
|
93
|
+
const decoder = new TextDecoder();
|
|
94
|
+
const readChunks = () => {
|
|
95
|
+
if (signal?.aborted) {
|
|
96
|
+
reader.cancel();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
reader.read().then((readResult) => {
|
|
100
|
+
if (readResult.done) {
|
|
101
|
+
const finalChunk = decoder.decode();
|
|
102
|
+
if (finalChunk) chunks.push(finalChunk);
|
|
103
|
+
proc.exited.then(() => {
|
|
104
|
+
if (proc.exitCode !== 0) new Response(proc.stderr).text().then((stderrText) => {
|
|
105
|
+
const errorMessage = `Git process exited with code ${proc.exitCode}: ${stderrText}`;
|
|
106
|
+
reject(new Error(errorMessage));
|
|
107
|
+
});
|
|
108
|
+
else resolve(chunks.join(""));
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
const decodedChunk = decoder.decode(readResult.value, { stream: true });
|
|
112
|
+
chunks.push(decodedChunk);
|
|
113
|
+
readChunks();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
readChunks();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Executes a Git command using buffered output collection.
|
|
122
|
+
* Waits for the process to complete before returning the full output.
|
|
123
|
+
*
|
|
124
|
+
* @param args - Git command arguments
|
|
125
|
+
* @param spawnOptions - Spawn configuration for the Git process
|
|
126
|
+
* @param commandStr - Full command string for error messages
|
|
127
|
+
* @param signal - Optional abort signal for cancellation
|
|
128
|
+
* @returns FireflyAsyncResult containing stdout output or error
|
|
129
|
+
*/
|
|
130
|
+
function executeGitCommandBuffered(args, spawnOptions, commandStr, signal) {
|
|
131
|
+
const proc = Bun.spawn(["git", ...args], spawnOptions);
|
|
132
|
+
const outputPromise = new Response(proc.stdout).text();
|
|
133
|
+
const exitPromise = proc.exited;
|
|
134
|
+
const abortPromise = signal ? new Promise((_, reject) => {
|
|
135
|
+
const onAbort = () => {
|
|
136
|
+
proc.kill();
|
|
137
|
+
reject(new Error(`Git command aborted: ${commandStr}`, { cause: signal.reason }));
|
|
138
|
+
};
|
|
139
|
+
if (signal.aborted) onAbort();
|
|
140
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
141
|
+
}) : null;
|
|
142
|
+
const executionPromise = Promise.all([outputPromise, exitPromise]).then(([output]) => {
|
|
143
|
+
if (proc.exitCode !== 0) return new Response(proc.stderr).text().then((stderrText) => {
|
|
144
|
+
const errorMessage = `Git process exited with code ${proc.exitCode}: ${stderrText}`;
|
|
145
|
+
return Promise.reject(new Error(errorMessage));
|
|
146
|
+
});
|
|
147
|
+
return output;
|
|
148
|
+
});
|
|
149
|
+
const racePromise = abortPromise ? Promise.race([executionPromise, abortPromise]) : executionPromise;
|
|
150
|
+
return ResultAsync.fromPromise(racePromise, (error) => failedError({
|
|
151
|
+
message: `Git command failed: ${commandStr}`,
|
|
152
|
+
details: error
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Executes a Git command using streaming output collection.
|
|
157
|
+
*
|
|
158
|
+
* @param args - Git command arguments
|
|
159
|
+
* @param spawnOptions - Spawn configuration for the Git process
|
|
160
|
+
* @param commandStr - Full command string for error messages
|
|
161
|
+
* @param signal - Optional abort signal for cancellation
|
|
162
|
+
* @returns FireflyAsyncResult containing stdout output or error
|
|
163
|
+
*/
|
|
164
|
+
function executeGitCommandStreaming(args, spawnOptions, commandStr, signal) {
|
|
165
|
+
return ResultAsync.fromPromise(streamToString(args, spawnOptions, signal), (error) => failedError({
|
|
166
|
+
message: `Git command failed: ${commandStr}`,
|
|
167
|
+
details: error
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Executes a Git command with comprehensive error handling and cancellation support.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* // Simple command
|
|
176
|
+
* const result = await executeGitCommand(["status", "--porcelain"]);
|
|
177
|
+
*
|
|
178
|
+
* // With timeout
|
|
179
|
+
* const result = await executeGitCommand(["log", "--oneline"], { timeoutMs: 5000 });
|
|
180
|
+
*
|
|
181
|
+
* // With abort signal
|
|
182
|
+
* const controller = new AbortController();
|
|
183
|
+
* const result = await executeGitCommand(["fetch", "--all"], { signal: controller.signal });
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* @param args - Git command arguments (e.g., ["status", "--porcelain"])
|
|
187
|
+
* @param options - Execution options for dry-run, timeout, etc.
|
|
188
|
+
* @returns FireflyAsyncResult containing stdout output or FireflyError
|
|
189
|
+
*/
|
|
190
|
+
function executeGitCommand(args, options = {}) {
|
|
191
|
+
const resolvedOptions = {
|
|
192
|
+
verbose: true,
|
|
193
|
+
...options
|
|
194
|
+
};
|
|
195
|
+
const parseResult = gitArgsSchema.safeParse(args);
|
|
196
|
+
if (!parseResult.success) return failedErrAsync({
|
|
197
|
+
message: "Invalid git arguments",
|
|
198
|
+
details: parseResult.error
|
|
199
|
+
});
|
|
200
|
+
const validatedArgs = parseResult.data;
|
|
201
|
+
const commandStr = `git ${validatedArgs.join(" ")}`;
|
|
202
|
+
const signal = resolvedOptions.signal ?? (resolvedOptions.timeoutMs ? AbortSignal.timeout(resolvedOptions.timeoutMs) : void 0);
|
|
203
|
+
if (signal?.aborted) return failedErrAsync({
|
|
204
|
+
message: `Git command aborted before execution: ${commandStr}`,
|
|
205
|
+
details: signal.reason
|
|
206
|
+
});
|
|
207
|
+
const useStreaming = shouldUseStreaming(validatedArgs) && !resolvedOptions.forceBuffered;
|
|
208
|
+
const executionMode = useStreaming ? "streaming" : "buffered";
|
|
209
|
+
if (resolvedOptions.verbose) logger.verbose(`GitCommandExecutor: Executing git command (${executionMode}): ${commandStr}`);
|
|
210
|
+
if (resolvedOptions.dryRun && hasSideEffects(validatedArgs)) {
|
|
211
|
+
const dryRunMessage = `Dry run: Skipping ${commandStr}`;
|
|
212
|
+
return withDryRun(resolvedOptions, dryRunMessage, () => FireflyOkAsync(dryRunMessage), dryRunMessage);
|
|
213
|
+
}
|
|
214
|
+
const spawnOptions = {
|
|
215
|
+
cwd: resolvedOptions.cwd ?? process.cwd(),
|
|
216
|
+
stdout: "pipe",
|
|
217
|
+
stderr: "pipe"
|
|
218
|
+
};
|
|
219
|
+
if (useStreaming) return executeGitCommandStreaming(validatedArgs, spawnOptions, commandStr, signal);
|
|
220
|
+
return executeGitCommandBuffered(validatedArgs, spawnOptions, commandStr, signal);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/services/implementations/git.service.ts
|
|
225
|
+
const CURRENT_BRANCH_MARKER_REGEX = /^\*\s*/;
|
|
226
|
+
const REMOTES_PREFIX_REGEX = /^remotes\//;
|
|
227
|
+
/**
|
|
228
|
+
* Default implementation of the git service.
|
|
229
|
+
*/
|
|
230
|
+
var DefaultGitService = class {
|
|
231
|
+
/**
|
|
232
|
+
* The working directory where git commands are executed.
|
|
233
|
+
*/
|
|
234
|
+
cwd;
|
|
235
|
+
/**
|
|
236
|
+
* Creates a new git service.
|
|
237
|
+
* @param cwd - The working directory for git operations
|
|
238
|
+
*/
|
|
239
|
+
constructor(cwd) {
|
|
240
|
+
this.cwd = cwd;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Executes a git command with the configured working directory
|
|
244
|
+
*
|
|
245
|
+
* @param args - Git command arguments
|
|
246
|
+
* @param options - Execution options
|
|
247
|
+
* @returns Command output or error
|
|
248
|
+
*/
|
|
249
|
+
git(args, options) {
|
|
250
|
+
return executeGitCommand(args, {
|
|
251
|
+
cwd: this.cwd,
|
|
252
|
+
dryRun: options?.dryRun,
|
|
253
|
+
verbose: options?.verbose ?? true
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
isInsideRepository() {
|
|
257
|
+
return this.git(["rev-parse", "--is-inside-work-tree"]).map(() => true).orElse(() => FireflyOkAsync(false));
|
|
258
|
+
}
|
|
259
|
+
getRepositoryRoot() {
|
|
260
|
+
return this.git(["rev-parse", "--show-toplevel"]).map((output) => output.trim());
|
|
261
|
+
}
|
|
262
|
+
getRemoteUrl(remote) {
|
|
263
|
+
const remoteName = remote ?? "origin";
|
|
264
|
+
return this.git([
|
|
265
|
+
"remote",
|
|
266
|
+
"get-url",
|
|
267
|
+
remoteName
|
|
268
|
+
]).map((output) => output.trim());
|
|
269
|
+
}
|
|
270
|
+
getStatus() {
|
|
271
|
+
return this.git(["status", "--porcelain"]).map((output) => {
|
|
272
|
+
const lines = output.split("\n").filter((line) => line.length > 0);
|
|
273
|
+
let hasStaged = false;
|
|
274
|
+
let hasUnstaged = false;
|
|
275
|
+
let hasUntracked = false;
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
const index = line[0];
|
|
278
|
+
const workTree = line[1];
|
|
279
|
+
if (index === "?") hasUntracked = true;
|
|
280
|
+
else if (index !== " " && index !== "?") hasStaged = true;
|
|
281
|
+
if (workTree !== " " && workTree !== "?") hasUnstaged = true;
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
hasStaged,
|
|
285
|
+
hasUnstaged,
|
|
286
|
+
hasUntracked,
|
|
287
|
+
isClean: lines.length === 0
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
isWorkingTreeClean() {
|
|
292
|
+
return this.getStatus().map((status) => status.isClean);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Parses git status porcelain output into structured file status objects.
|
|
296
|
+
*
|
|
297
|
+
* @param output - The raw output from `git status --porcelain`
|
|
298
|
+
* @returns Array of GitFileStatus objects
|
|
299
|
+
*/
|
|
300
|
+
parseStatusOutput(output) {
|
|
301
|
+
return output.split("\n").filter((line) => line.length >= 3).map((line) => {
|
|
302
|
+
const indexStatus = line[0] ?? " ";
|
|
303
|
+
const workTreeStatus = line[1] ?? " ";
|
|
304
|
+
return {
|
|
305
|
+
path: line.slice(3).trim(),
|
|
306
|
+
indexStatus,
|
|
307
|
+
workTreeStatus
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
getFiles(filter) {
|
|
312
|
+
const includeStaged = filter?.staged ?? true;
|
|
313
|
+
const includeUnstaged = filter?.unstaged ?? true;
|
|
314
|
+
return this.git(["status", "--porcelain"]).map((output) => {
|
|
315
|
+
return this.parseStatusOutput(output).filter((file) => {
|
|
316
|
+
const isStaged = file.indexStatus !== " " && file.indexStatus !== "?";
|
|
317
|
+
const isUnstaged = file.workTreeStatus !== " " && file.workTreeStatus !== "?";
|
|
318
|
+
if (includeStaged && includeUnstaged) return isStaged || isUnstaged;
|
|
319
|
+
if (includeStaged) return isStaged;
|
|
320
|
+
if (includeUnstaged) return isUnstaged;
|
|
321
|
+
return false;
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
getFileNames(filter) {
|
|
326
|
+
return this.getFiles(filter).map((files) => files.map((file) => file.path));
|
|
327
|
+
}
|
|
328
|
+
getCurrentBranch() {
|
|
329
|
+
return this.git([
|
|
330
|
+
"rev-parse",
|
|
331
|
+
"--abbrev-ref",
|
|
332
|
+
"HEAD"
|
|
333
|
+
]).map((output) => output.trim());
|
|
334
|
+
}
|
|
335
|
+
hasBranch(branch) {
|
|
336
|
+
return this.git([
|
|
337
|
+
"rev-parse",
|
|
338
|
+
"--verify",
|
|
339
|
+
branch
|
|
340
|
+
]).map(() => true).orElse(() => FireflyOkAsync(false));
|
|
341
|
+
}
|
|
342
|
+
parseBranchLine(line) {
|
|
343
|
+
const isCurrent = line.startsWith("*");
|
|
344
|
+
const isRemote = line.includes("remotes/");
|
|
345
|
+
let branchName = line.replace(CURRENT_BRANCH_MARKER_REGEX, "").trim();
|
|
346
|
+
if (isRemote) branchName = branchName.replace(REMOTES_PREFIX_REGEX, "");
|
|
347
|
+
return {
|
|
348
|
+
name: branchName,
|
|
349
|
+
isCurrent,
|
|
350
|
+
isRemote
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
listBranches(includeRemote) {
|
|
354
|
+
const args = ["branch"];
|
|
355
|
+
if (includeRemote) args.push("-a");
|
|
356
|
+
return this.git(args).map((output) => {
|
|
357
|
+
return output.split("\n").filter((line) => line.trim().length > 0).map((line) => this.parseBranchLine(line));
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
createCommit(message, options) {
|
|
361
|
+
if (options?.dryRun) {
|
|
362
|
+
logger.verbose("GitService: Dry run, skipping commit");
|
|
363
|
+
return FireflyOkAsync({ sha: "dry-run-sha" });
|
|
364
|
+
}
|
|
365
|
+
const args = [
|
|
366
|
+
"commit",
|
|
367
|
+
"-m",
|
|
368
|
+
message
|
|
369
|
+
];
|
|
370
|
+
if (options?.sign) args.push("-S");
|
|
371
|
+
if (options?.allowEmpty) args.push("--allow-empty");
|
|
372
|
+
if (options?.noVerify) args.push("--no-verify");
|
|
373
|
+
if (options?.paths && options.paths.length > 0) args.push("--", ...options.paths);
|
|
374
|
+
return this.git(args).andThen(() => this.git(["rev-parse", "HEAD"]).map((sha) => ({ sha: sha.trim().substring(0, 7) })));
|
|
375
|
+
}
|
|
376
|
+
getCommitHashesSince(since) {
|
|
377
|
+
const args = since ? ["rev-list", `${since}..HEAD`] : ["rev-list", "HEAD"];
|
|
378
|
+
return this.git(args).map((output) => output.trim().split("\n").map((line) => line.trim()).filter((line) => line.length > 0));
|
|
379
|
+
}
|
|
380
|
+
getCommitDetails(hash) {
|
|
381
|
+
const format = [
|
|
382
|
+
"hash:%H",
|
|
383
|
+
"date:%ci",
|
|
384
|
+
"author:%an <%ae>",
|
|
385
|
+
"subject:%s",
|
|
386
|
+
"body:%b",
|
|
387
|
+
"notes:%N"
|
|
388
|
+
].join("%n");
|
|
389
|
+
return this.git([
|
|
390
|
+
"show",
|
|
391
|
+
"--no-patch",
|
|
392
|
+
`--format=${format}`,
|
|
393
|
+
hash
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
getUnpushedCommits() {
|
|
397
|
+
return this.getCurrentBranch().andThen((branch) => {
|
|
398
|
+
const upstream = `origin/${branch}`;
|
|
399
|
+
return this.git([
|
|
400
|
+
"rev-list",
|
|
401
|
+
"--count",
|
|
402
|
+
`${upstream}..HEAD`
|
|
403
|
+
]).map((output) => {
|
|
404
|
+
const count = Number.parseInt(output.trim(), 10) || 0;
|
|
405
|
+
return {
|
|
406
|
+
hasUnpushed: count > 0,
|
|
407
|
+
count
|
|
408
|
+
};
|
|
409
|
+
}).orElse(() => {
|
|
410
|
+
return this.git([
|
|
411
|
+
"rev-list",
|
|
412
|
+
"--count",
|
|
413
|
+
"HEAD"
|
|
414
|
+
]).map((output) => {
|
|
415
|
+
const count = Number.parseInt(output.trim(), 10) || 0;
|
|
416
|
+
return {
|
|
417
|
+
hasUnpushed: count > 0,
|
|
418
|
+
count
|
|
419
|
+
};
|
|
420
|
+
}).orElse(() => FireflyOkAsync({
|
|
421
|
+
hasUnpushed: false,
|
|
422
|
+
count: 0
|
|
423
|
+
}));
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
createTag(name, options) {
|
|
428
|
+
if (options?.dryRun) {
|
|
429
|
+
logger.verbose(`GitService: Dry run, skipping tag creation: ${name}`);
|
|
430
|
+
return FireflyOkAsync(void 0);
|
|
431
|
+
}
|
|
432
|
+
const args = ["tag"];
|
|
433
|
+
if (options?.message) args.push("-a", name, "-m", options.message);
|
|
434
|
+
else args.push(name);
|
|
435
|
+
if (options?.sign) args.push("-s");
|
|
436
|
+
return this.git(args).map(() => void 0);
|
|
437
|
+
}
|
|
438
|
+
deleteTag(name, options) {
|
|
439
|
+
const scope = options?.scope ?? "local";
|
|
440
|
+
const remote = options?.remote ?? "origin";
|
|
441
|
+
if (options?.dryRun) {
|
|
442
|
+
logger.verbose(`GitService: Dry run, skipping tag deletion (${scope}): ${name}`);
|
|
443
|
+
return FireflyOkAsync(void 0);
|
|
444
|
+
}
|
|
445
|
+
if (scope === "local") return this.git([
|
|
446
|
+
"tag",
|
|
447
|
+
"-d",
|
|
448
|
+
name
|
|
449
|
+
]).map(() => void 0);
|
|
450
|
+
if (scope === "remote") return this.git([
|
|
451
|
+
"push",
|
|
452
|
+
remote,
|
|
453
|
+
`:refs/tags/${name}`
|
|
454
|
+
]).map(() => void 0);
|
|
455
|
+
return this.git([
|
|
456
|
+
"tag",
|
|
457
|
+
"-d",
|
|
458
|
+
name
|
|
459
|
+
]).andThen(() => this.git([
|
|
460
|
+
"push",
|
|
461
|
+
remote,
|
|
462
|
+
`:refs/tags/${name}`
|
|
463
|
+
])).map(() => void 0);
|
|
464
|
+
}
|
|
465
|
+
hasTag(name) {
|
|
466
|
+
return this.git([
|
|
467
|
+
"tag",
|
|
468
|
+
"--list",
|
|
469
|
+
name
|
|
470
|
+
]).map((output) => output.trim() === name);
|
|
471
|
+
}
|
|
472
|
+
hasAnyTags() {
|
|
473
|
+
return this.getLatestTag().map((tag) => tag !== null);
|
|
474
|
+
}
|
|
475
|
+
listTags() {
|
|
476
|
+
return this.git(["tag", "--list"]).map((output) => output.split("\n").map((tag) => tag.trim()).filter((tag) => tag.length > 0));
|
|
477
|
+
}
|
|
478
|
+
getLatestTag() {
|
|
479
|
+
return this.git([
|
|
480
|
+
"describe",
|
|
481
|
+
"--tags",
|
|
482
|
+
"--abbrev=0"
|
|
483
|
+
]).map((output) => {
|
|
484
|
+
return output.trim() || null;
|
|
485
|
+
}).orElse((error) => {
|
|
486
|
+
if (error.message.includes("No names found") || error.message.includes("fatal")) return FireflyOkAsync(null);
|
|
487
|
+
return FireflyOkAsync(null);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
getTagMessage(name) {
|
|
491
|
+
return this.git([
|
|
492
|
+
"tag",
|
|
493
|
+
"-l",
|
|
494
|
+
"--format=%(contents)",
|
|
495
|
+
name
|
|
496
|
+
]).map((output) => {
|
|
497
|
+
return output.trim() || null;
|
|
498
|
+
}).orElse(() => FireflyOkAsync(null));
|
|
499
|
+
}
|
|
500
|
+
stage(paths) {
|
|
501
|
+
const pathArray = Array.isArray(paths) ? paths : [paths];
|
|
502
|
+
return this.git(["add", ...pathArray]).map(() => void 0);
|
|
503
|
+
}
|
|
504
|
+
unstage(paths) {
|
|
505
|
+
const pathArray = Array.isArray(paths) ? paths : [paths];
|
|
506
|
+
return this.git([
|
|
507
|
+
"reset",
|
|
508
|
+
"HEAD",
|
|
509
|
+
"--",
|
|
510
|
+
...pathArray
|
|
511
|
+
]).map(() => void 0);
|
|
512
|
+
}
|
|
513
|
+
push(options) {
|
|
514
|
+
if (options?.dryRun) {
|
|
515
|
+
logger.verbose("GitService: Dry run, skipping push");
|
|
516
|
+
return FireflyOkAsync(void 0);
|
|
517
|
+
}
|
|
518
|
+
const args = ["push"];
|
|
519
|
+
const remote = options?.remote ?? "origin";
|
|
520
|
+
args.push(remote);
|
|
521
|
+
if (options?.branch) args.push(options.branch);
|
|
522
|
+
if (options?.tags) args.push("--tags");
|
|
523
|
+
if (options?.followTags) args.push("--follow-tags");
|
|
524
|
+
return this.git(args).map(() => void 0);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Gets the upstream remote name for the current branch.
|
|
528
|
+
* @returns The remote name or null if no upstream is configured.
|
|
529
|
+
*/
|
|
530
|
+
getUpstreamRemote() {
|
|
531
|
+
return this.git([
|
|
532
|
+
"rev-parse",
|
|
533
|
+
"--abbrev-ref",
|
|
534
|
+
"--symbolic-full-name",
|
|
535
|
+
"@{upstream}"
|
|
536
|
+
]).map((output) => {
|
|
537
|
+
const upstream = output.trim();
|
|
538
|
+
const slashIndex = upstream.indexOf("/");
|
|
539
|
+
if (slashIndex > 0) return upstream.substring(0, slashIndex);
|
|
540
|
+
return null;
|
|
541
|
+
}).orElse(() => FireflyOkAsync(null));
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Lists all configured remotes.
|
|
545
|
+
* @returns Array of remote names.
|
|
546
|
+
*/
|
|
547
|
+
listRemotes() {
|
|
548
|
+
return this.git(["remote"]).map((output) => output.split("\n").map((remote) => remote.trim()).filter((remote) => remote.length > 0));
|
|
549
|
+
}
|
|
550
|
+
inferRepositoryUrl() {
|
|
551
|
+
return this.getUpstreamRemote().andThen((upstreamRemote) => {
|
|
552
|
+
if (upstreamRemote) {
|
|
553
|
+
logger.verbose(`GitService: Inferring repository URL from upstream remote: ${upstreamRemote}`);
|
|
554
|
+
return this.getRemoteUrl(upstreamRemote).map((url) => url).orElse(() => this.tryOriginOrFirstRemote());
|
|
555
|
+
}
|
|
556
|
+
return this.tryOriginOrFirstRemote();
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Tries to get the repository URL from 'origin' or the first available remote.
|
|
561
|
+
*/
|
|
562
|
+
tryOriginOrFirstRemote() {
|
|
563
|
+
return this.getRemoteUrl("origin").map((url) => {
|
|
564
|
+
logger.verbose("GitService: Inferring repository URL from origin remote");
|
|
565
|
+
return url;
|
|
566
|
+
}).orElse(() => {
|
|
567
|
+
return this.listRemotes().andThen((remotes) => {
|
|
568
|
+
if (remotes.length === 0) {
|
|
569
|
+
logger.verbose("GitService: No remotes configured, cannot infer repository URL");
|
|
570
|
+
return FireflyOkAsync(null);
|
|
571
|
+
}
|
|
572
|
+
const firstRemote = remotes[0];
|
|
573
|
+
logger.verbose(`GitService: Inferring repository URL from first remote: ${firstRemote}`);
|
|
574
|
+
return this.getRemoteUrl(firstRemote).map((url) => url).orElse(() => FireflyOkAsync(null));
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
580
|
+
* Creates a git service instance.
|
|
581
|
+
*/
|
|
582
|
+
function createGitService(cwd) {
|
|
583
|
+
return new DefaultGitService(cwd);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
//#endregion
|
|
587
|
+
export { createGitService };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
import z
|
|
1
|
+
import z from "zod";
|
|
2
2
|
|
|
3
|
-
//#region src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//#region src/modules/configuration/config-schema.provider.d.ts
|
|
12
|
-
declare const schemas: {
|
|
13
|
-
readonly release: z.ZodObject<{
|
|
3
|
+
//#region src/cli/config/config.schema.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Complete Firefly configuration schema.
|
|
7
|
+
* Combines global options with command-specific configuration sections.
|
|
8
|
+
*/
|
|
9
|
+
declare const FireflyConfigSchema: z.ZodObject<{
|
|
10
|
+
release: z.ZodOptional<z.ZodObject<{
|
|
14
11
|
name: z.ZodOptional<z.ZodString>;
|
|
15
12
|
scope: z.ZodOptional<z.ZodString>;
|
|
16
13
|
base: z.ZodDefault<z.ZodString>;
|
|
14
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
17
15
|
changelogPath: z.ZodDefault<z.ZodString>;
|
|
18
16
|
bumpStrategy: z.ZodDefault<z.ZodUnion<[z.ZodEnum<{
|
|
19
17
|
auto: "auto";
|
|
@@ -29,7 +27,7 @@ declare const schemas: {
|
|
|
29
27
|
prepatch: "prepatch";
|
|
30
28
|
graduate: "graduate";
|
|
31
29
|
}>>;
|
|
32
|
-
preReleaseId: z.
|
|
30
|
+
preReleaseId: z.ZodOptional<z.ZodString>;
|
|
33
31
|
preReleaseBase: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<"0">, z.ZodLiteral<"1">]>>;
|
|
34
32
|
releaseNotes: z.ZodDefault<z.ZodString>;
|
|
35
33
|
commitMessage: z.ZodDefault<z.ZodString>;
|
|
@@ -44,16 +42,38 @@ declare const schemas: {
|
|
|
44
42
|
releaseLatest: z.ZodDefault<z.ZodCoercedBoolean<unknown>>;
|
|
45
43
|
releasePreRelease: z.ZodDefault<z.ZodCoercedBoolean<unknown>>;
|
|
46
44
|
releaseDraft: z.ZodDefault<z.ZodCoercedBoolean<unknown>>;
|
|
47
|
-
}, z.core.$strip
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
47
|
+
dryRun: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
verbose: z.ZodOptional<z.ZodBoolean>;
|
|
49
|
+
enableRollback: z.ZodOptional<z.ZodBoolean>;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
/**
|
|
52
|
+
* TypeScript type for Firefly configuration.
|
|
53
|
+
* Use this type when you need to reference the configuration shape without runtime validation.
|
|
54
|
+
*/
|
|
55
|
+
type FireflyConfig = z.infer<typeof FireflyConfigSchema>;
|
|
54
56
|
//#endregion
|
|
55
|
-
//#region src/
|
|
56
|
-
|
|
57
|
+
//#region src/config/index.d.ts
|
|
58
|
+
/**
|
|
59
|
+
* Helper function to define a type-safe Firefly configuration.
|
|
60
|
+
*
|
|
61
|
+
* Provides IntelliSense autocompletion and type checking for config files.
|
|
62
|
+
*
|
|
63
|
+
* @param options - The configuration options
|
|
64
|
+
* @returns The same options (identity function for type inference)
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* export default defineConfig({
|
|
69
|
+
* verbose: true,
|
|
70
|
+
* release: {
|
|
71
|
+
* bumpStrategy: "conventional",
|
|
72
|
+
* releaseType: "github",
|
|
73
|
+
* },
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
57
77
|
declare function defineConfig<T extends FireflyConfig>(options: T): T;
|
|
58
78
|
//#endregion
|
|
59
|
-
export {
|
|
79
|
+
export { defineConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
//#region src/
|
|
1
|
+
//#region src/config/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Helper function to define a type-safe Firefly configuration.
|
|
4
|
+
*
|
|
5
|
+
* Provides IntelliSense autocompletion and type checking for config files.
|
|
6
|
+
*
|
|
7
|
+
* @param options - The configuration options
|
|
8
|
+
* @returns The same options (identity function for type inference)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* verbose: true,
|
|
14
|
+
* release: {
|
|
15
|
+
* bumpStrategy: "conventional",
|
|
16
|
+
* releaseType: "github",
|
|
17
|
+
* },
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
2
21
|
function defineConfig(options) {
|
|
3
22
|
return options;
|
|
4
23
|
}
|