Skip to content

Commit c331eee

Browse files
xesrevinuGit Agent
andcommitted
feat(cli): enhance command with progress tracking
- Added progress rendering for CLI commands. - Integrated with shared tracing to improve UX. This commit features enhancements in the CLI by adding progress tracking. The new rendering system, along with shared tracing, provides users with better feedback on command execution. Co-Authored-By: Git Agent <noreply@git-agent.dev>
1 parent 8b688f3 commit c331eee

3 files changed

Lines changed: 139 additions & 79 deletions

File tree

src/cli.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
66
import PackageJson from "../package.json" with { type: "json" };
77
import { commandRoot } from "./commands/root";
88
import { renderError } from "./shared/errors";
9+
import { gitAgentProgressRenderConfig } from "./shared/progress-config";
10+
import { makeProgressLayer } from "./shared/tracing";
911

10-
const Live = Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer).pipe(
11-
Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv())),
12-
);
12+
const Live = Layer.mergeAll(
13+
NodeServices.layer,
14+
FetchHttpClient.layer,
15+
makeProgressLayer(gitAgentProgressRenderConfig),
16+
).pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv())));
1317

1418
const program = Command.run(commandRoot, {
1519
version: PackageJson.version,

src/commands/commit.ts

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { emptyProjectConfig } from "../domain/project";
77
import { ConfigError } from "../shared/errors";
88
import { printCommitResult, printDryRunResult } from "../shared/output";
99
import { parseCsvValues } from "../shared/text";
10+
import { withProgressSpan } from "../shared/tracing";
1011
import { runCommitService } from "../services/commit-service";
1112
import { detectVcs, getVcsClient } from "../services/vcs";
1213
import {
@@ -58,52 +59,18 @@ const parseTrailers = (
5859
return trailers;
5960
});
6061

61-
export const commandCommit = Command.make(
62-
"commit",
63-
{
64-
cwd: cwdFlag,
65-
vcs: vcsFlag,
66-
apiKey: apiKeyFlag,
67-
baseUrl: baseUrlFlag,
68-
model: modelFlag,
69-
free: freeFlag,
70-
intent: Flag.optional(
71-
Flag.string("intent").pipe(Flag.withDescription("Describe the intent of the change.")),
72-
),
73-
dryRun: Flag.boolean("dry-run").pipe(
74-
Flag.withDescription("Print messages without committing."),
75-
),
76-
noStage: Flag.boolean("no-stage").pipe(Flag.withDescription("Skip auto-staging; git only.")),
77-
amend: Flag.boolean("amend").pipe(
78-
Flag.withDescription("Regenerate and amend the previous commit."),
79-
),
80-
maxDiffLines: Flag.integer("max-diff-lines").pipe(
81-
Flag.withDefault(0),
82-
Flag.withDescription("Maximum diff lines to send to the model. 0 means unlimited."),
83-
),
84-
noAttribution: Flag.boolean("no-attribution").pipe(
85-
Flag.withAlias("--no-git-agent"),
86-
Flag.withDescription("Omit the default Git Agent trailer."),
87-
),
88-
coAuthor: Flag.string("co-author").pipe(
89-
Flag.withDescription("Co-author trailer. Repeat the flag or use comma-separated values."),
90-
Flag.between(0, Number.MAX_SAFE_INTEGER),
91-
),
92-
trailer: Flag.string("trailer").pipe(
93-
Flag.withDescription(
94-
'Trailer in "Key: Value" format. Repeat the flag or use comma-separated values.',
95-
),
96-
Flag.between(0, Number.MAX_SAFE_INTEGER),
97-
),
98-
},
99-
Effect.fn(function* (input) {
62+
const runCommitCommand = Effect.fn(
63+
function* (input) {
10064
if (input.amend && input.noStage) {
10165
return yield* Effect.fail(
10266
new ConfigError({ message: "--amend and --no-stage cannot be used together" }),
10367
);
10468
}
10569

10670
const vcsKind = yield* detectVcs(input.cwd, toOptionalString(input.vcs));
71+
yield* Effect.annotateCurrentSpan({
72+
vcs: vcsKind,
73+
});
10774
const vcs = getVcsClient(vcsKind);
10875
const provider = yield* resolveProviderConfig({
10976
cwd: input.cwd,
@@ -132,7 +99,7 @@ export const commandCommit = Command.make(
13299
provider.noModelCoAuthor || projectConfig.noModelCoAuthor,
133100
);
134101

135-
const result = yield* runCommitService({
102+
return yield* runCommitService({
136103
cwd: repoRoot,
137104
provider,
138105
vcs,
@@ -144,7 +111,56 @@ export const commandCommit = Command.make(
144111
amend: input.amend,
145112
maxDiffLines: input.maxDiffLines > 0 ? input.maxDiffLines : projectConfig.maxDiffLines,
146113
});
114+
},
115+
(effect, input) =>
116+
withProgressSpan(effect, "commit.prepare-request", {
117+
amend: input.amend,
118+
dry_run: input.dryRun,
119+
no_stage: input.noStage,
120+
requested_vcs: toOptionalString(input.vcs) ?? "auto",
121+
}),
122+
);
147123

124+
export const commandCommit = Command.make(
125+
"commit",
126+
{
127+
cwd: cwdFlag,
128+
vcs: vcsFlag,
129+
apiKey: apiKeyFlag,
130+
baseUrl: baseUrlFlag,
131+
model: modelFlag,
132+
free: freeFlag,
133+
intent: Flag.optional(
134+
Flag.string("intent").pipe(Flag.withDescription("Describe the intent of the change.")),
135+
),
136+
dryRun: Flag.boolean("dry-run").pipe(
137+
Flag.withDescription("Print messages without committing."),
138+
),
139+
noStage: Flag.boolean("no-stage").pipe(Flag.withDescription("Skip auto-staging; git only.")),
140+
amend: Flag.boolean("amend").pipe(
141+
Flag.withDescription("Regenerate and amend the previous commit."),
142+
),
143+
maxDiffLines: Flag.integer("max-diff-lines").pipe(
144+
Flag.withDefault(0),
145+
Flag.withDescription("Maximum diff lines to send to the model. 0 means unlimited."),
146+
),
147+
noAttribution: Flag.boolean("no-attribution").pipe(
148+
Flag.withAlias("--no-git-agent"),
149+
Flag.withDescription("Omit the default Git Agent trailer."),
150+
),
151+
coAuthor: Flag.string("co-author").pipe(
152+
Flag.withDescription("Co-author trailer. Repeat the flag or use comma-separated values."),
153+
Flag.between(0, Number.MAX_SAFE_INTEGER),
154+
),
155+
trailer: Flag.string("trailer").pipe(
156+
Flag.withDescription(
157+
'Trailer in "Key: Value" format. Repeat the flag or use comma-separated values.',
158+
),
159+
Flag.between(0, Number.MAX_SAFE_INTEGER),
160+
),
161+
},
162+
Effect.fn(function* (input) {
163+
const result = yield* runCommitCommand(input);
148164
if (result.dryRun) {
149165
yield* printDryRunResult(result.commits);
150166
return;

src/commands/init.ts

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { resolveProviderConfig } from "../config/provider";
1111
import { emptyProjectConfig } from "../domain/project";
1212
import { ConfigError } from "../shared/errors";
1313
import { parseCsvValues } from "../shared/text";
14+
import { withProgressSpan } from "../shared/tracing";
1415
import { generateGitignore } from "../services/gitignore-service";
1516
import { generateProjectScopes } from "../services/scope-service";
1617
import { detectVcs, getVcsClient } from "../services/vcs";
@@ -24,40 +25,20 @@ import {
2425
vcsFlag,
2526
} from "./shared";
2627

27-
export const commandInit = Command.make(
28-
"init",
29-
{
30-
cwd: cwdFlag,
31-
vcs: vcsFlag,
32-
apiKey: apiKeyFlag,
33-
baseUrl: baseUrlFlag,
34-
model: modelFlag,
35-
free: freeFlag,
36-
scope: Flag.boolean("scope").pipe(Flag.withDescription("Generate scopes via AI.")),
37-
gitignore: Flag.boolean("gitignore").pipe(Flag.withDescription("Generate .gitignore via AI.")),
38-
force: Flag.boolean("force").pipe(
39-
Flag.withDescription("Overwrite existing config or .gitignore."),
40-
),
41-
maxCommits: Flag.integer("max-commits").pipe(
42-
Flag.withDefault(200),
43-
Flag.withDescription("Maximum commit count to analyze for scopes."),
44-
),
45-
local: Flag.boolean("local").pipe(
46-
Flag.withDescription("Write config to .git-agent/config.local.yml."),
47-
),
48-
hook: Flag.string("hook").pipe(
49-
Flag.withDescription("Hook to configure. Repeat the flag or use comma-separated values."),
50-
Flag.between(0, Number.MAX_SAFE_INTEGER),
51-
),
52-
},
53-
Effect.fn(function* (input) {
28+
const runInitCommand = Effect.fn(
29+
function* (input) {
5430
const fs = yield* FileSystem.FileSystem;
5531
const vcsKind = yield* detectVcs(input.cwd, toOptionalString(input.vcs));
32+
yield* Effect.annotateCurrentSpan({
33+
vcs: vcsKind,
34+
});
5635
const vcs = getVcsClient(vcsKind);
5736
const isRepo = yield* vcs.isRepo(input.cwd);
5837
const hooks = parseCsvValues(input.hook);
5938
if (!isRepo) {
60-
const output = yield* vcs.initRepo(input.cwd);
39+
const output = yield* withProgressSpan(vcs.initRepo(input.cwd), "init.initialize-repository", {
40+
vcs: vcsKind,
41+
});
6142
if (output.length > 0) {
6243
yield* Console.log(output);
6344
}
@@ -76,9 +57,7 @@ export const commandInit = Command.make(
7657
);
7758
}
7859

79-
const configPath = yield* input.local
80-
? localConfigPath(repoRoot)
81-
: projectConfigWritePath(repoRoot);
60+
const configPath = yield* input.local ? localConfigPath(repoRoot) : projectConfigWritePath(repoRoot);
8261
if (!input.force) {
8362
const exists = yield* fs.exists(configPath);
8463
if (exists) {
@@ -109,24 +88,85 @@ export const commandInit = Command.make(
10988
}
11089

11190
if (doGitignore || fullWizard) {
112-
const techs = yield* generateGitignore(provider, vcs, repoRoot);
91+
const techs = yield* withProgressSpan(generateGitignore(provider, vcs, repoRoot), "init.generate-gitignore", {
92+
vcs: vcsKind,
93+
full_wizard: fullWizard,
94+
});
11395
yield* Console.log(`.gitignore updated: ${techs.join(", ")}`);
11496
}
11597

11698
if (doScope || fullWizard) {
117-
const scopes = yield* generateProjectScopes(provider, vcs, repoRoot, input.maxCommits);
99+
const scopes = yield* withProgressSpan(
100+
generateProjectScopes(provider, vcs, repoRoot, input.maxCommits),
101+
"init.generate-scopes",
102+
{
103+
vcs: vcsKind,
104+
full_wizard: fullWizard,
105+
max_commits: input.maxCommits,
106+
},
107+
);
118108
yield* mergeAndSaveScopes(configPath, scopes);
119109
yield* Console.log(`scopes written to ${configPath}`);
120110
}
121111

122112
if (fullWizard) {
123-
yield* writeProjectField(configPath, "hook", "conventional");
113+
yield* withProgressSpan(writeProjectField(configPath, "hook", "conventional"), "init.write-default-hook", {
114+
path: configPath,
115+
});
124116
} else if (hooks.length > 0) {
125-
yield* writeProjectField(configPath, "hook", hooks.join(","));
117+
yield* withProgressSpan(writeProjectField(configPath, "hook", hooks.join(",")), "init.write-hook", {
118+
path: configPath,
119+
hook_count: hooks.length,
120+
});
126121
}
127122

128123
if (!doScope && !doGitignore && !fullWizard && hooks.length > 0) {
129-
yield* mergeAndSaveScopes(configPath, emptyProjectConfig().scopes);
124+
yield* withProgressSpan(
125+
mergeAndSaveScopes(configPath, emptyProjectConfig().scopes),
126+
"init.write-project-config",
127+
{
128+
path: configPath,
129+
},
130+
);
130131
}
132+
},
133+
(effect, input) =>
134+
withProgressSpan(effect, "init.run", {
135+
force: input.force,
136+
gitignore: input.gitignore,
137+
local: input.local,
138+
requested_vcs: toOptionalString(input.vcs) ?? "auto",
139+
scope: input.scope,
140+
}),
141+
);
142+
143+
export const commandInit = Command.make(
144+
"init",
145+
{
146+
cwd: cwdFlag,
147+
vcs: vcsFlag,
148+
apiKey: apiKeyFlag,
149+
baseUrl: baseUrlFlag,
150+
model: modelFlag,
151+
free: freeFlag,
152+
scope: Flag.boolean("scope").pipe(Flag.withDescription("Generate scopes via AI.")),
153+
gitignore: Flag.boolean("gitignore").pipe(Flag.withDescription("Generate .gitignore via AI.")),
154+
force: Flag.boolean("force").pipe(
155+
Flag.withDescription("Overwrite existing config or .gitignore."),
156+
),
157+
maxCommits: Flag.integer("max-commits").pipe(
158+
Flag.withDefault(200),
159+
Flag.withDescription("Maximum commit count to analyze for scopes."),
160+
),
161+
local: Flag.boolean("local").pipe(
162+
Flag.withDescription("Write config to .git-agent/config.local.yml."),
163+
),
164+
hook: Flag.string("hook").pipe(
165+
Flag.withDescription("Hook to configure. Repeat the flag or use comma-separated values."),
166+
Flag.between(0, Number.MAX_SAFE_INTEGER),
167+
),
168+
},
169+
Effect.fn(function* (input) {
170+
yield* runInitCommand(input);
131171
}),
132172
).pipe(Command.withDescription("Initialize git-agent in the current repository."));

0 commit comments

Comments
 (0)