@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,286 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { Effect, pipe, Console } from 'effect'
|
|
3
|
+
import {
|
|
4
|
+
ConfigService,
|
|
5
|
+
ConfigServiceLive,
|
|
6
|
+
ConfigError,
|
|
7
|
+
type ConfigServiceImpl,
|
|
8
|
+
} from '@/services/config'
|
|
9
|
+
import type { GerritCredentials } from '@/schemas/gerrit'
|
|
10
|
+
import { AppConfig } from '@/schemas/config'
|
|
11
|
+
import { Schema } from '@effect/schema'
|
|
12
|
+
import { input, password } from '@inquirer/prompts'
|
|
13
|
+
import { spawn } from 'node:child_process'
|
|
14
|
+
import { normalizeGerritHost } from '@/utils/url-parser'
|
|
15
|
+
|
|
16
|
+
// Check if a command exists on the system
|
|
17
|
+
const checkCommandExists = (command: string): Promise<boolean> =>
|
|
18
|
+
new Promise((resolve) => {
|
|
19
|
+
const child = spawn('which', [command], { stdio: 'ignore' })
|
|
20
|
+
child.on('close', (code) => {
|
|
21
|
+
resolve(code === 0)
|
|
22
|
+
})
|
|
23
|
+
child.on('error', () => {
|
|
24
|
+
resolve(false)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// AI tools to check for in order of preference
|
|
29
|
+
const AI_TOOLS = ['claude', 'llm', 'opencode', 'gemini'] as const
|
|
30
|
+
|
|
31
|
+
// Effect wrapper for detecting available AI tools
|
|
32
|
+
const detectAvailableAITools = () =>
|
|
33
|
+
Effect.tryPromise({
|
|
34
|
+
try: async () => {
|
|
35
|
+
const availableTools: string[] = []
|
|
36
|
+
|
|
37
|
+
for (const tool of AI_TOOLS) {
|
|
38
|
+
const exists = await checkCommandExists(tool)
|
|
39
|
+
if (exists) {
|
|
40
|
+
availableTools.push(tool)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return availableTools
|
|
45
|
+
},
|
|
46
|
+
catch: (error) => new ConfigError({ message: `Failed to detect AI tools: ${error}` }),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Effect wrapper for getting existing config
|
|
50
|
+
const getExistingConfig = (configService: ConfigServiceImpl) =>
|
|
51
|
+
configService.getFullConfig.pipe(Effect.orElseSucceed(() => null))
|
|
52
|
+
|
|
53
|
+
// Test connection with credentials
|
|
54
|
+
const verifyCredentials = (credentials: GerritCredentials) =>
|
|
55
|
+
Effect.tryPromise({
|
|
56
|
+
try: async () => {
|
|
57
|
+
const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')
|
|
58
|
+
const response = await fetch(`${credentials.host}/a/config/server/version`, {
|
|
59
|
+
headers: { Authorization: `Basic ${auth}` },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`Authentication failed: ${response.status}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return response.ok
|
|
67
|
+
},
|
|
68
|
+
catch: (error) => {
|
|
69
|
+
if (error instanceof Error) {
|
|
70
|
+
// Authentication/permission errors
|
|
71
|
+
if (error.message.includes('401')) {
|
|
72
|
+
return new ConfigError({
|
|
73
|
+
message: 'Invalid credentials. Please check your username and password.',
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
if (error.message.includes('403')) {
|
|
77
|
+
return new ConfigError({
|
|
78
|
+
message: 'Access denied. Please verify your credentials and server permissions.',
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Network/hostname errors
|
|
83
|
+
if (error.message.includes('ENOTFOUND')) {
|
|
84
|
+
return new ConfigError({
|
|
85
|
+
message: `Hostname not found. Please check that the Gerrit URL is correct.\nExample: https://gerrit.example.com (without /a/ or paths)`,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
if (error.message.includes('ECONNREFUSED')) {
|
|
89
|
+
return new ConfigError({
|
|
90
|
+
message: `Connection refused. The server may be down or the port may be incorrect.\nPlease verify the URL and try again.`,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
if (error.message.includes('ETIMEDOUT')) {
|
|
94
|
+
return new ConfigError({
|
|
95
|
+
message: `Connection timed out. Please check:\n• Your internet connection\n• The Gerrit server URL\n• Any firewall or VPN settings`,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (error.message.includes('certificate') || error.message.includes('SSL')) {
|
|
99
|
+
return new ConfigError({
|
|
100
|
+
message: `SSL/Certificate error. Please ensure the URL uses HTTPS and the certificate is valid.`,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// URL format errors
|
|
105
|
+
if (error.message.includes('Invalid URL') || error.message.includes('fetch failed')) {
|
|
106
|
+
return new ConfigError({
|
|
107
|
+
message: `Invalid URL format. Please use the full URL including https://\nExample: https://gerrit.example.com`,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Generic network errors
|
|
112
|
+
if (error.message.includes('network') || error.message.includes('fetch')) {
|
|
113
|
+
return new ConfigError({
|
|
114
|
+
message: `Network error: ${error.message}\nPlease check your connection and the Gerrit server URL.`,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new ConfigError({ message: error.message })
|
|
119
|
+
}
|
|
120
|
+
return new ConfigError({ message: 'Unknown error occurred' })
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Pure Effect-based setup implementation using inquirer
|
|
125
|
+
const setupEffect = (configService: ConfigServiceImpl) =>
|
|
126
|
+
pipe(
|
|
127
|
+
Effect.all([getExistingConfig(configService), detectAvailableAITools()]),
|
|
128
|
+
Effect.flatMap(([existingConfig, availableTools]) =>
|
|
129
|
+
pipe(
|
|
130
|
+
Console.log(chalk.bold('🔧 Gerrit CLI Setup')),
|
|
131
|
+
Effect.flatMap(() => Console.log('')),
|
|
132
|
+
Effect.flatMap(() => {
|
|
133
|
+
if (existingConfig) {
|
|
134
|
+
return Console.log(chalk.dim('(Press Enter to keep existing values)'))
|
|
135
|
+
} else {
|
|
136
|
+
return pipe(
|
|
137
|
+
Console.log(chalk.cyan('Please provide your Gerrit connection details:')),
|
|
138
|
+
Effect.flatMap(() =>
|
|
139
|
+
Console.log(chalk.dim('Example URL: https://gerrit.example.com')),
|
|
140
|
+
),
|
|
141
|
+
Effect.flatMap(() =>
|
|
142
|
+
Console.log(
|
|
143
|
+
chalk.dim(
|
|
144
|
+
'You can find your HTTP password in Gerrit Settings > HTTP Credentials',
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
Effect.flatMap(() =>
|
|
152
|
+
Effect.tryPromise({
|
|
153
|
+
try: async () => {
|
|
154
|
+
console.log('')
|
|
155
|
+
|
|
156
|
+
// Enable raw mode for proper password masking
|
|
157
|
+
const wasRawMode = process.stdin.isRaw
|
|
158
|
+
if (process.stdin.isTTY && !wasRawMode) {
|
|
159
|
+
process.stdin.setRawMode(true)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// Gerrit Host URL
|
|
164
|
+
const host = await input({
|
|
165
|
+
message: 'Gerrit Host URL (e.g., https://gerrit.example.com)',
|
|
166
|
+
default: existingConfig?.host,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Username
|
|
170
|
+
const username = await input({
|
|
171
|
+
message: 'Username (your Gerrit username)',
|
|
172
|
+
default: existingConfig?.username,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// Password - with proper masking and visual feedback
|
|
176
|
+
const passwordMessage = existingConfig?.password
|
|
177
|
+
? `HTTP Password (generated from Gerrit settings) ${chalk.dim('(press Enter to keep existing)')}`
|
|
178
|
+
: 'HTTP Password (generated from Gerrit settings)'
|
|
179
|
+
|
|
180
|
+
const passwordValue =
|
|
181
|
+
(await password({
|
|
182
|
+
message: passwordMessage,
|
|
183
|
+
mask: true, // Show * characters as user types
|
|
184
|
+
})) ||
|
|
185
|
+
existingConfig?.password ||
|
|
186
|
+
''
|
|
187
|
+
|
|
188
|
+
console.log('')
|
|
189
|
+
console.log(chalk.yellow('Optional: AI Configuration'))
|
|
190
|
+
|
|
191
|
+
// Show detected AI tools
|
|
192
|
+
if (availableTools.length > 0) {
|
|
193
|
+
console.log(chalk.dim(`Detected AI tools: ${availableTools.join(', ')}`))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get default suggestion
|
|
197
|
+
const defaultCommand =
|
|
198
|
+
existingConfig?.aiTool ||
|
|
199
|
+
(availableTools.includes('claude') ? 'claude' : availableTools[0]) ||
|
|
200
|
+
''
|
|
201
|
+
|
|
202
|
+
// AI tool command with smart default
|
|
203
|
+
const aiToolCommand = await input({
|
|
204
|
+
message:
|
|
205
|
+
availableTools.length > 0
|
|
206
|
+
? 'AI tool command (detected from system)'
|
|
207
|
+
: 'AI tool command (e.g., claude, llm, opencode, gemini)',
|
|
208
|
+
default: defaultCommand || 'claude',
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Build flat config
|
|
212
|
+
const configData = {
|
|
213
|
+
host: normalizeGerritHost(host),
|
|
214
|
+
username: username.trim(),
|
|
215
|
+
password: passwordValue,
|
|
216
|
+
...(aiToolCommand && {
|
|
217
|
+
aiTool: aiToolCommand,
|
|
218
|
+
}),
|
|
219
|
+
aiAutoDetect: !aiToolCommand,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Validate config using Schema instead of type assertion
|
|
223
|
+
const fullConfig = Schema.decodeUnknownSync(AppConfig)(configData)
|
|
224
|
+
|
|
225
|
+
return fullConfig
|
|
226
|
+
} finally {
|
|
227
|
+
// Restore raw mode state
|
|
228
|
+
if (process.stdin.isTTY && !wasRawMode) {
|
|
229
|
+
process.stdin.setRawMode(false)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
catch: (error) => {
|
|
234
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
235
|
+
console.log(`\n${chalk.yellow('Setup cancelled')}`)
|
|
236
|
+
process.exit(0)
|
|
237
|
+
}
|
|
238
|
+
return new ConfigError({
|
|
239
|
+
message: error instanceof Error ? error.message : 'Failed to get user input',
|
|
240
|
+
})
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
Effect.tap(() => Console.log('\nVerifying credentials...')),
|
|
247
|
+
Effect.flatMap((config) =>
|
|
248
|
+
pipe(
|
|
249
|
+
verifyCredentials({
|
|
250
|
+
host: config.host,
|
|
251
|
+
username: config.username,
|
|
252
|
+
password: config.password,
|
|
253
|
+
}),
|
|
254
|
+
Effect.map(() => config),
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
Effect.tap(() => Console.log(chalk.green('Successfully authenticated'))),
|
|
258
|
+
Effect.flatMap((config) => configService.saveFullConfig(config)),
|
|
259
|
+
Effect.tap(() => Console.log(chalk.green('\nConfiguration saved successfully!'))),
|
|
260
|
+
Effect.tap(() => Console.log('You can now use:')),
|
|
261
|
+
Effect.tap(() => Console.log(' • "ger mine" to view your changes')),
|
|
262
|
+
Effect.tap(() => Console.log(' • "ger show <change-id>" to view change details')),
|
|
263
|
+
Effect.tap(() => Console.log(' • "ger review <change-id>" to review with AI')),
|
|
264
|
+
Effect.catchAll((error) =>
|
|
265
|
+
pipe(
|
|
266
|
+
Console.error(
|
|
267
|
+
chalk.red(`\n${error instanceof ConfigError ? error.message : `Setup failed: ${error}`}`),
|
|
268
|
+
),
|
|
269
|
+
Effect.flatMap(() => Effect.fail(error)),
|
|
270
|
+
),
|
|
271
|
+
),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
export async function setup(): Promise<void> {
|
|
275
|
+
const program = pipe(
|
|
276
|
+
ConfigService,
|
|
277
|
+
Effect.flatMap((configService) => setupEffect(configService)),
|
|
278
|
+
).pipe(Effect.provide(ConfigServiceLive))
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
await Effect.runPromise(program)
|
|
282
|
+
} catch {
|
|
283
|
+
// Error already handled and displayed
|
|
284
|
+
process.exit(1)
|
|
285
|
+
}
|
|
286
|
+
}
|