@aaronshaf/ger 1.2.10 → 2.0.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/.ast-grep/rules/no-as-casting.yml +13 -0
- package/.claude-plugin/plugin.json +22 -0
- package/.github/workflows/ci-simple.yml +53 -0
- package/.github/workflows/ci.yml +171 -0
- package/.github/workflows/claude-code-review.yml +83 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/dependency-update.yml +84 -0
- package/.github/workflows/release.yml +166 -0
- package/.github/workflows/security-scan.yml +113 -0
- package/.github/workflows/security.yml +96 -0
- package/.husky/pre-commit +16 -0
- package/.husky/pre-push +25 -0
- package/.lintstagedrc.json +6 -0
- package/.tool-versions +1 -0
- package/CLAUDE.md +105 -0
- package/DEVELOPMENT.md +361 -0
- package/EXAMPLES.md +457 -0
- package/README.md +831 -16
- package/bin/ger +3 -18
- package/biome.json +36 -0
- package/bun.lock +678 -0
- package/bunfig.toml +8 -0
- package/docs/adr/0001-use-effect-for-side-effects.md +65 -0
- package/docs/adr/0002-use-bun-runtime.md +64 -0
- package/docs/adr/0003-store-credentials-in-home-directory.md +75 -0
- package/docs/adr/0004-use-commander-for-cli.md +76 -0
- package/docs/adr/0005-use-effect-schema-for-validation.md +93 -0
- package/docs/adr/0006-use-msw-for-api-mocking.md +89 -0
- package/docs/adr/0007-git-hooks-for-quality.md +94 -0
- package/docs/adr/0008-no-as-typecasting.md +83 -0
- package/docs/adr/0009-file-size-limits.md +82 -0
- package/docs/adr/0010-llm-friendly-xml-output.md +93 -0
- package/docs/adr/0011-ai-tool-strategy-pattern.md +102 -0
- package/docs/adr/0012-build-status-message-parsing.md +94 -0
- package/docs/adr/0013-git-subprocess-integration.md +98 -0
- package/docs/adr/0014-group-management-support.md +95 -0
- package/docs/adr/0015-batch-comment-processing.md +111 -0
- package/docs/adr/0016-flexible-change-identifiers.md +94 -0
- package/docs/adr/0017-git-worktree-support.md +102 -0
- package/docs/adr/0018-auto-install-commit-hook.md +103 -0
- package/docs/adr/0019-sdk-package-exports.md +95 -0
- package/docs/adr/0020-code-coverage-enforcement.md +105 -0
- package/docs/adr/0021-typescript-isolated-declarations.md +83 -0
- package/docs/adr/0022-biome-oxlint-tooling.md +124 -0
- package/docs/adr/README.md +30 -0
- package/docs/prd/README.md +12 -0
- package/docs/prd/architecture.md +325 -0
- package/docs/prd/commands.md +425 -0
- package/docs/prd/data-model.md +349 -0
- package/docs/prd/overview.md +124 -0
- package/index.ts +219 -0
- package/oxlint.json +24 -0
- package/package.json +82 -15
- package/scripts/check-coverage.ts +69 -0
- package/scripts/check-file-size.ts +38 -0
- package/scripts/fix-test-mocks.ts +55 -0
- package/skills/gerrit-workflow/SKILL.md +247 -0
- package/skills/gerrit-workflow/examples.md +572 -0
- package/skills/gerrit-workflow/reference.md +728 -0
- package/src/api/gerrit.ts +696 -0
- package/src/cli/commands/abandon.ts +65 -0
- package/src/cli/commands/add-reviewer.ts +156 -0
- package/src/cli/commands/build-status.ts +282 -0
- package/src/cli/commands/checkout.ts +422 -0
- package/src/cli/commands/comment.ts +460 -0
- package/src/cli/commands/comments.ts +85 -0
- package/src/cli/commands/diff.ts +71 -0
- package/src/cli/commands/extract-url.ts +266 -0
- package/src/cli/commands/groups-members.ts +104 -0
- package/src/cli/commands/groups-show.ts +169 -0
- package/src/cli/commands/groups.ts +137 -0
- package/src/cli/commands/incoming.ts +226 -0
- package/src/cli/commands/init.ts +164 -0
- package/src/cli/commands/mine.ts +115 -0
- package/src/cli/commands/open.ts +57 -0
- package/src/cli/commands/projects.ts +68 -0
- package/src/cli/commands/push.ts +430 -0
- package/src/cli/commands/rebase.ts +52 -0
- package/src/cli/commands/remove-reviewer.ts +123 -0
- package/src/cli/commands/restore.ts +50 -0
- package/src/cli/commands/review.ts +486 -0
- package/src/cli/commands/search.ts +162 -0
- package/src/cli/commands/setup.ts +286 -0
- package/src/cli/commands/show.ts +491 -0
- package/src/cli/commands/status.ts +35 -0
- package/src/cli/commands/submit.ts +108 -0
- package/src/cli/commands/vote.ts +119 -0
- package/src/cli/commands/workspace.ts +200 -0
- package/src/cli/index.ts +53 -0
- package/src/cli/register-commands.ts +659 -0
- package/src/cli/register-group-commands.ts +88 -0
- package/src/cli/register-reviewer-commands.ts +97 -0
- package/src/prompts/default-review.md +86 -0
- package/src/prompts/system-inline-review.md +135 -0
- package/src/prompts/system-overall-review.md +206 -0
- package/src/schemas/config.test.ts +245 -0
- package/src/schemas/config.ts +84 -0
- package/src/schemas/gerrit.ts +681 -0
- package/src/services/commit-hook.ts +314 -0
- package/src/services/config.test.ts +150 -0
- package/src/services/config.ts +250 -0
- package/src/services/git-worktree.ts +342 -0
- package/src/services/review-strategy.ts +292 -0
- package/src/test-utils/mock-generator.ts +138 -0
- package/src/utils/change-id.test.ts +98 -0
- package/src/utils/change-id.ts +63 -0
- package/src/utils/comment-formatters.ts +153 -0
- package/src/utils/diff-context.ts +103 -0
- package/src/utils/diff-formatters.ts +141 -0
- package/src/utils/formatters.ts +85 -0
- package/src/utils/git-commit.test.ts +277 -0
- package/src/utils/git-commit.ts +122 -0
- package/src/utils/index.ts +55 -0
- package/src/utils/message-filters.ts +26 -0
- package/src/utils/review-formatters.ts +89 -0
- package/src/utils/review-prompt-builder.ts +110 -0
- package/src/utils/shell-safety.ts +117 -0
- package/src/utils/status-indicators.ts +100 -0
- package/src/utils/url-parser.test.ts +271 -0
- package/src/utils/url-parser.ts +118 -0
- package/tests/abandon.test.ts +230 -0
- package/tests/add-reviewer.test.ts +579 -0
- package/tests/build-status-watch.test.ts +344 -0
- package/tests/build-status.test.ts +789 -0
- package/tests/change-id-formats.test.ts +268 -0
- package/tests/checkout/integration.test.ts +653 -0
- package/tests/checkout/parse-input.test.ts +55 -0
- package/tests/checkout/validation.test.ts +178 -0
- package/tests/comment-batch-advanced.test.ts +431 -0
- package/tests/comment-gerrit-api-compliance.test.ts +414 -0
- package/tests/comment.test.ts +708 -0
- package/tests/comments.test.ts +323 -0
- package/tests/config-service-simple.test.ts +100 -0
- package/tests/diff.test.ts +419 -0
- package/tests/extract-url.test.ts +517 -0
- package/tests/groups-members.test.ts +256 -0
- package/tests/groups-show.test.ts +323 -0
- package/tests/groups.test.ts +334 -0
- package/tests/helpers/build-status-test-setup.ts +83 -0
- package/tests/helpers/config-mock.ts +27 -0
- package/tests/incoming.test.ts +357 -0
- package/tests/init.test.ts +70 -0
- package/tests/integration/commit-hook.test.ts +246 -0
- package/tests/interactive-incoming.test.ts +173 -0
- package/tests/mine.test.ts +285 -0
- package/tests/mocks/msw-handlers.ts +80 -0
- package/tests/open.test.ts +233 -0
- package/tests/projects.test.ts +259 -0
- package/tests/rebase.test.ts +271 -0
- package/tests/remove-reviewer.test.ts +357 -0
- package/tests/restore.test.ts +237 -0
- package/tests/review.test.ts +135 -0
- package/tests/search.test.ts +712 -0
- package/tests/setup.test.ts +63 -0
- package/tests/show-auto-detect.test.ts +324 -0
- package/tests/show.test.ts +813 -0
- package/tests/status.test.ts +145 -0
- package/tests/submit.test.ts +316 -0
- package/tests/unit/commands/push.test.ts +194 -0
- package/tests/unit/git-branch-detection.test.ts +82 -0
- package/tests/unit/git-worktree.test.ts +55 -0
- package/tests/unit/patterns/push-patterns.test.ts +148 -0
- package/tests/unit/schemas/gerrit.test.ts +85 -0
- package/tests/unit/services/commit-hook.test.ts +132 -0
- package/tests/unit/services/review-strategy.test.ts +349 -0
- package/tests/unit/test-utils/mock-generator.test.ts +154 -0
- package/tests/unit/utils/comment-formatters.test.ts +415 -0
- package/tests/unit/utils/diff-context.test.ts +171 -0
- package/tests/unit/utils/diff-formatters.test.ts +165 -0
- package/tests/unit/utils/formatters.test.ts +411 -0
- package/tests/unit/utils/message-filters.test.ts +227 -0
- package/tests/unit/utils/shell-safety.test.ts +230 -0
- package/tests/unit/utils/status-indicators.test.ts +137 -0
- package/tests/vote.test.ts +317 -0
- package/tests/workspace.test.ts +295 -0
- package/tsconfig.json +36 -5
- package/src/commands/branch.ts +0 -180
- package/src/ger.ts +0 -22
- package/src/types.d.ts +0 -35
- package/src/utils.ts +0 -130
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Schema } from '@effect/schema'
|
|
3
|
+
import { Effect } from 'effect'
|
|
4
|
+
import { AppConfig, AiConfig, aiConfigFromFlat, migrateFromNestedConfig } from './config'
|
|
5
|
+
|
|
6
|
+
describe('Config Schemas', () => {
|
|
7
|
+
describe('AppConfig (Flat Structure)', () => {
|
|
8
|
+
test('validates complete flat config', () => {
|
|
9
|
+
const validConfig = {
|
|
10
|
+
host: 'https://gerrit.example.com',
|
|
11
|
+
username: 'testuser',
|
|
12
|
+
password: 'testpass123',
|
|
13
|
+
aiTool: 'claude' as const,
|
|
14
|
+
aiAutoDetect: true,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = Schema.decodeUnknownSync(AppConfig)(validConfig)
|
|
18
|
+
expect(result).toEqual(validConfig)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('validates minimal flat config with defaults', () => {
|
|
22
|
+
const minimalConfig = {
|
|
23
|
+
host: 'https://gerrit.example.com',
|
|
24
|
+
username: 'testuser',
|
|
25
|
+
password: 'testpass123',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = Schema.decodeUnknownSync(AppConfig)(minimalConfig)
|
|
29
|
+
expect(result).toEqual({
|
|
30
|
+
...minimalConfig,
|
|
31
|
+
aiAutoDetect: true, // default value
|
|
32
|
+
aiTool: undefined,
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('rejects invalid host URL', () => {
|
|
37
|
+
const invalidConfig = {
|
|
38
|
+
host: 'not-a-url',
|
|
39
|
+
username: 'testuser',
|
|
40
|
+
password: 'testpass123',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('rejects empty username', () => {
|
|
47
|
+
const invalidConfig = {
|
|
48
|
+
host: 'https://gerrit.example.com',
|
|
49
|
+
username: '',
|
|
50
|
+
password: 'testpass123',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('rejects empty password', () => {
|
|
57
|
+
const invalidConfig = {
|
|
58
|
+
host: 'https://gerrit.example.com',
|
|
59
|
+
username: 'testuser',
|
|
60
|
+
password: '',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('validates all AI tool options', () => {
|
|
67
|
+
const tools = ['claude', 'llm', 'opencode', 'gemini'] as const
|
|
68
|
+
|
|
69
|
+
tools.forEach((tool) => {
|
|
70
|
+
const config = {
|
|
71
|
+
host: 'https://gerrit.example.com',
|
|
72
|
+
username: 'testuser',
|
|
73
|
+
password: 'testpass123',
|
|
74
|
+
aiTool: tool,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = Schema.decodeUnknownSync(AppConfig)(config)
|
|
78
|
+
expect(result.aiTool).toBe(tool)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('rejects invalid AI tool', () => {
|
|
83
|
+
const invalidConfig = {
|
|
84
|
+
host: 'https://gerrit.example.com',
|
|
85
|
+
username: 'testuser',
|
|
86
|
+
password: 'testpass123',
|
|
87
|
+
aiTool: 'invalid-tool',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
expect(() => Schema.decodeUnknownSync(AppConfig)(invalidConfig)).toThrow()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('Legacy AiConfig (Backward Compatibility)', () => {
|
|
95
|
+
test('validates legacy AI config structure', () => {
|
|
96
|
+
const validAiConfig = {
|
|
97
|
+
tool: 'claude' as const,
|
|
98
|
+
autoDetect: false,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = Schema.decodeUnknownSync(AiConfig)(validAiConfig)
|
|
102
|
+
expect(result).toEqual(validAiConfig)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('validates minimal legacy AI config with defaults', () => {
|
|
106
|
+
const minimalAiConfig = {}
|
|
107
|
+
|
|
108
|
+
const result = Schema.decodeUnknownSync(AiConfig)(minimalAiConfig)
|
|
109
|
+
expect(result).toEqual({
|
|
110
|
+
autoDetect: true, // default value
|
|
111
|
+
tool: undefined,
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('Helper Functions', () => {
|
|
117
|
+
test('aiConfigFromFlat converts flat config to legacy AI config', () => {
|
|
118
|
+
const flatConfig = {
|
|
119
|
+
host: 'https://gerrit.example.com',
|
|
120
|
+
username: 'testuser',
|
|
121
|
+
password: 'testpass123',
|
|
122
|
+
aiTool: 'claude' as const,
|
|
123
|
+
aiAutoDetect: false,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const aiConfig = aiConfigFromFlat(flatConfig)
|
|
127
|
+
expect(aiConfig).toEqual({
|
|
128
|
+
tool: 'claude',
|
|
129
|
+
autoDetect: false,
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('aiConfigFromFlat handles undefined AI options', () => {
|
|
134
|
+
const flatConfig = {
|
|
135
|
+
host: 'https://gerrit.example.com',
|
|
136
|
+
username: 'testuser',
|
|
137
|
+
password: 'testpass123',
|
|
138
|
+
aiAutoDetect: true,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const aiConfig = aiConfigFromFlat(flatConfig)
|
|
142
|
+
expect(aiConfig).toEqual({
|
|
143
|
+
tool: undefined,
|
|
144
|
+
autoDetect: true,
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('migrateFromNestedConfig converts old nested format', () => {
|
|
149
|
+
const nestedConfig = {
|
|
150
|
+
credentials: {
|
|
151
|
+
host: 'https://gerrit.example.com',
|
|
152
|
+
username: 'testuser',
|
|
153
|
+
password: 'testpass123',
|
|
154
|
+
},
|
|
155
|
+
ai: {
|
|
156
|
+
tool: 'claude' as const,
|
|
157
|
+
autoDetect: false,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const flatConfig = migrateFromNestedConfig(nestedConfig)
|
|
162
|
+
expect(flatConfig).toEqual({
|
|
163
|
+
host: 'https://gerrit.example.com',
|
|
164
|
+
username: 'testuser',
|
|
165
|
+
password: 'testpass123',
|
|
166
|
+
aiTool: 'claude',
|
|
167
|
+
aiAutoDetect: false,
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('migrateFromNestedConfig handles missing AI config', () => {
|
|
172
|
+
const nestedConfig = {
|
|
173
|
+
credentials: {
|
|
174
|
+
host: 'https://gerrit.example.com',
|
|
175
|
+
username: 'testuser',
|
|
176
|
+
password: 'testpass123',
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const flatConfig = migrateFromNestedConfig(nestedConfig)
|
|
181
|
+
expect(flatConfig).toEqual({
|
|
182
|
+
host: 'https://gerrit.example.com',
|
|
183
|
+
username: 'testuser',
|
|
184
|
+
password: 'testpass123',
|
|
185
|
+
aiTool: undefined,
|
|
186
|
+
aiAutoDetect: true, // default
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('migrateFromNestedConfig handles partial AI config', () => {
|
|
191
|
+
const nestedConfig = {
|
|
192
|
+
credentials: {
|
|
193
|
+
host: 'https://gerrit.example.com',
|
|
194
|
+
username: 'testuser',
|
|
195
|
+
password: 'testpass123',
|
|
196
|
+
},
|
|
197
|
+
ai: {
|
|
198
|
+
tool: 'llm' as const,
|
|
199
|
+
// autoDetect missing
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const flatConfig = migrateFromNestedConfig(nestedConfig)
|
|
204
|
+
expect(flatConfig).toEqual({
|
|
205
|
+
host: 'https://gerrit.example.com',
|
|
206
|
+
username: 'testuser',
|
|
207
|
+
password: 'testpass123',
|
|
208
|
+
aiTool: 'llm',
|
|
209
|
+
aiAutoDetect: true, // default when missing
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Effect Schema Integration', () => {
|
|
215
|
+
test('Effect.gen with valid flat config', async () => {
|
|
216
|
+
const config = {
|
|
217
|
+
host: 'https://gerrit.example.com',
|
|
218
|
+
username: 'testuser',
|
|
219
|
+
password: 'testpass123',
|
|
220
|
+
aiTool: 'claude' as const,
|
|
221
|
+
aiAutoDetect: true,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = await Effect.gen(function* () {
|
|
225
|
+
return yield* Schema.decodeUnknown(AppConfig)(config)
|
|
226
|
+
}).pipe(Effect.runPromise)
|
|
227
|
+
|
|
228
|
+
expect(result).toEqual(config)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('Effect.gen with validation error', async () => {
|
|
232
|
+
const invalidConfig = {
|
|
233
|
+
host: 'not-a-url',
|
|
234
|
+
username: 'testuser',
|
|
235
|
+
password: 'testpass123',
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await expect(
|
|
239
|
+
Effect.gen(function* () {
|
|
240
|
+
return yield* Schema.decodeUnknown(AppConfig)(invalidConfig)
|
|
241
|
+
}).pipe(Effect.runPromise),
|
|
242
|
+
).rejects.toThrow()
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Schema } from '@effect/schema'
|
|
2
|
+
|
|
3
|
+
// Flat Application Configuration (similar to ji structure)
|
|
4
|
+
export const AppConfig: Schema.Struct<{
|
|
5
|
+
host: typeof Schema.String
|
|
6
|
+
username: typeof Schema.String
|
|
7
|
+
password: typeof Schema.String
|
|
8
|
+
aiTool: Schema.optional<Schema.Literal<['claude', 'llm', 'opencode', 'gemini']>>
|
|
9
|
+
aiAutoDetect: Schema.optionalWith<typeof Schema.Boolean, { default: () => boolean }>
|
|
10
|
+
}> = Schema.Struct({
|
|
11
|
+
// Gerrit credentials (flattened)
|
|
12
|
+
host: Schema.String.pipe(
|
|
13
|
+
Schema.pattern(/^https?:\/\/.+$/),
|
|
14
|
+
Schema.annotations({ description: 'Gerrit server URL' }),
|
|
15
|
+
),
|
|
16
|
+
username: Schema.String.pipe(
|
|
17
|
+
Schema.minLength(1),
|
|
18
|
+
Schema.annotations({ description: 'Gerrit username' }),
|
|
19
|
+
),
|
|
20
|
+
password: Schema.String.pipe(
|
|
21
|
+
Schema.minLength(1),
|
|
22
|
+
Schema.annotations({ description: 'HTTP password or API token' }),
|
|
23
|
+
),
|
|
24
|
+
// AI configuration (flattened)
|
|
25
|
+
aiTool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
|
|
26
|
+
aiAutoDetect: Schema.optionalWith(Schema.Boolean, { default: (): boolean => true }),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export type AppConfig = Schema.Schema.Type<typeof AppConfig>
|
|
30
|
+
|
|
31
|
+
// Legacy schemas for backward compatibility (deprecated)
|
|
32
|
+
export const AiConfig: Schema.Struct<{
|
|
33
|
+
tool: Schema.optional<Schema.Literal<['claude', 'llm', 'opencode', 'gemini']>>
|
|
34
|
+
autoDetect: Schema.optionalWith<typeof Schema.Boolean, { default: () => boolean }>
|
|
35
|
+
}> = Schema.Struct({
|
|
36
|
+
tool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
|
|
37
|
+
autoDetect: Schema.optionalWith(Schema.Boolean, { default: (): boolean => true }),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export type AiConfig = Schema.Schema.Type<typeof AiConfig>
|
|
41
|
+
|
|
42
|
+
// Helper to convert from flat config to legacy AI config
|
|
43
|
+
export const aiConfigFromFlat = (config: AppConfig): AiConfig => ({
|
|
44
|
+
tool: config.aiTool,
|
|
45
|
+
autoDetect: config.aiAutoDetect,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// Schema for validating legacy nested config structure
|
|
49
|
+
const LegacyNestedConfig = Schema.Struct({
|
|
50
|
+
credentials: Schema.Struct({
|
|
51
|
+
host: Schema.String.pipe(
|
|
52
|
+
Schema.pattern(/^https?:\/\/.+$/),
|
|
53
|
+
Schema.annotations({ description: 'Gerrit server URL' }),
|
|
54
|
+
),
|
|
55
|
+
username: Schema.String.pipe(Schema.minLength(1)),
|
|
56
|
+
password: Schema.String.pipe(Schema.minLength(1)),
|
|
57
|
+
}),
|
|
58
|
+
ai: Schema.optional(
|
|
59
|
+
Schema.Struct({
|
|
60
|
+
tool: Schema.optional(Schema.Literal('claude', 'llm', 'opencode', 'gemini')),
|
|
61
|
+
autoDetect: Schema.optional(Schema.Boolean),
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
type LegacyNestedConfig = Schema.Schema.Type<typeof LegacyNestedConfig>
|
|
67
|
+
|
|
68
|
+
// Helper to convert from legacy nested format to flat format with validation
|
|
69
|
+
export const migrateFromNestedConfig = (nested: unknown): AppConfig => {
|
|
70
|
+
// Validate input structure using Schema
|
|
71
|
+
const validatedNested = Schema.decodeUnknownSync(LegacyNestedConfig)(nested)
|
|
72
|
+
|
|
73
|
+
// Convert to flat structure
|
|
74
|
+
const flatConfig = {
|
|
75
|
+
host: validatedNested.credentials.host,
|
|
76
|
+
username: validatedNested.credentials.username,
|
|
77
|
+
password: validatedNested.credentials.password,
|
|
78
|
+
aiTool: validatedNested.ai?.tool,
|
|
79
|
+
aiAutoDetect: validatedNested.ai?.autoDetect ?? true,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate the resulting flat config
|
|
83
|
+
return Schema.decodeUnknownSync(AppConfig)(flatConfig)
|
|
84
|
+
}
|