@regressionproof/cli 0.3.7 → 0.3.9
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/build/.spruce/settings.json +16 -0
- package/build/cli.d.ts +2 -0
- package/build/cli.js +52 -0
- package/build/commands/invite/AcceptInvite.d.ts +1 -0
- package/build/commands/invite/AcceptInvite.js +17 -0
- package/build/commands/invite/CreateInvite.d.ts +1 -0
- package/build/commands/invite/CreateInvite.js +42 -0
- package/build/commands/invite/ListInvites.d.ts +1 -0
- package/build/commands/invite/ListInvites.js +18 -0
- package/build/commands/invite/RevokeInvite.d.ts +1 -0
- package/build/commands/invite/RevokeInvite.js +11 -0
- package/build/components/Init.d.ts +4 -0
- package/build/components/Init.js +286 -0
- package/build/components/Init.tsx +436 -0
- package/build/config/ConfigManager.d.ts +17 -0
- package/build/config/ConfigManager.js +35 -0
- package/build/esm/cli.d.ts +2 -0
- package/build/esm/cli.js +52 -0
- package/build/esm/commands/invite/AcceptInvite.d.ts +1 -0
- package/build/esm/commands/invite/AcceptInvite.js +29 -0
- package/build/esm/commands/invite/CreateInvite.d.ts +1 -0
- package/build/esm/commands/invite/CreateInvite.js +55 -0
- package/build/esm/commands/invite/ListInvites.d.ts +1 -0
- package/build/esm/commands/invite/ListInvites.js +30 -0
- package/build/esm/commands/invite/RevokeInvite.d.ts +1 -0
- package/build/esm/commands/invite/RevokeInvite.js +23 -0
- package/build/esm/components/Init.d.ts +4 -0
- package/build/esm/components/Init.js +301 -0
- package/build/esm/config/ConfigManager.d.ts +17 -0
- package/build/esm/config/ConfigManager.js +34 -0
- package/build/esm/index.d.ts +1 -0
- package/build/esm/index.js +2 -0
- package/build/esm/jest/JestConfigurator.d.ts +12 -0
- package/build/esm/jest/JestConfigurator.js +57 -0
- package/build/esm/utilities/slug.d.ts +2 -0
- package/build/esm/utilities/slug.js +22 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +2 -0
- package/build/jest/JestConfigurator.d.ts +12 -0
- package/build/jest/JestConfigurator.js +59 -0
- package/build/utilities/slug.d.ts +2 -0
- package/build/utilities/slug.js +21 -0
- package/package.json +5 -3
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import type { RegressionProofClient } from '@regressionproof/client'
|
|
4
|
+
import { buildRegressionProofClient } from '@regressionproof/client'
|
|
5
|
+
import { Box, Text, useApp } from 'ink'
|
|
6
|
+
import BigText from 'ink-big-text'
|
|
7
|
+
import TextInput from 'ink-text-input'
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import ConfigManager, { Credentials } from '../config/ConfigManager.js'
|
|
10
|
+
import JestConfigurator, { JestConfigResult } from '../jest/JestConfigurator.js'
|
|
11
|
+
import { getRepoNameFromGit, toSlug } from '../utilities/slug.js'
|
|
12
|
+
|
|
13
|
+
const API_URL =
|
|
14
|
+
process.env.REGRESSIONPROOF_API_URL ?? 'https://api.regressionproof.ai'
|
|
15
|
+
|
|
16
|
+
class InitComponent extends React.Component<Props, State> {
|
|
17
|
+
private checkTimeout: NodeJS.Timeout | null = null
|
|
18
|
+
private apiClient: RegressionProofClient
|
|
19
|
+
private configManager: ConfigManager
|
|
20
|
+
|
|
21
|
+
public constructor(props: Props) {
|
|
22
|
+
super(props)
|
|
23
|
+
|
|
24
|
+
const providedName = props.projectName
|
|
25
|
+
const defaultName = providedName ?? getRepoNameFromGit()
|
|
26
|
+
|
|
27
|
+
this.configManager = new ConfigManager()
|
|
28
|
+
this.apiClient = buildRegressionProofClient(API_URL)
|
|
29
|
+
|
|
30
|
+
// Check if already registered (idempotent)
|
|
31
|
+
const existingCreds = this.configManager.loadCredentials(defaultName)
|
|
32
|
+
if (existingCreds) {
|
|
33
|
+
this.state = {
|
|
34
|
+
name: defaultName,
|
|
35
|
+
step: 'installing',
|
|
36
|
+
availability: 'available',
|
|
37
|
+
errorMessage: '',
|
|
38
|
+
credentials: existingCreds,
|
|
39
|
+
jestConfig: null,
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
this.state = {
|
|
43
|
+
name: defaultName,
|
|
44
|
+
step: providedName ? 'registering' : 'input',
|
|
45
|
+
availability: 'idle',
|
|
46
|
+
errorMessage: '',
|
|
47
|
+
credentials: null,
|
|
48
|
+
jestConfig: null,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public componentDidMount(): void {
|
|
54
|
+
// If we have a provided name and not already registered, start registration
|
|
55
|
+
if (this.props.projectName && this.state.step === 'registering') {
|
|
56
|
+
void this.register()
|
|
57
|
+
} else if (this.state.step === 'installing') {
|
|
58
|
+
void this.installAndConfigure()
|
|
59
|
+
} else {
|
|
60
|
+
void this.checkAvailability()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public componentDidUpdate(_: Props, prevState: State): void {
|
|
65
|
+
if (prevState.name !== this.state.name && this.state.step === 'input') {
|
|
66
|
+
void this.checkAvailability()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public componentWillUnmount(): void {
|
|
71
|
+
this.clearCheckTimeout()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private clearCheckTimeout(): void {
|
|
75
|
+
if (this.checkTimeout) {
|
|
76
|
+
clearTimeout(this.checkTimeout)
|
|
77
|
+
this.checkTimeout = null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async checkAvailability(): Promise<void> {
|
|
82
|
+
this.clearCheckTimeout()
|
|
83
|
+
|
|
84
|
+
const { name } = this.state
|
|
85
|
+
if (name.length < 3) {
|
|
86
|
+
this.setState({ availability: 'idle', errorMessage: '' })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.setState({ availability: 'checking' })
|
|
91
|
+
|
|
92
|
+
this.checkTimeout = setTimeout(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const isAvailable =
|
|
95
|
+
await this.apiClient.checkNameAvailability(name)
|
|
96
|
+
this.setState({
|
|
97
|
+
availability: isAvailable ? 'available' : 'taken',
|
|
98
|
+
errorMessage: '',
|
|
99
|
+
})
|
|
100
|
+
} catch (err) {
|
|
101
|
+
this.setState({
|
|
102
|
+
availability: 'error',
|
|
103
|
+
errorMessage: this.formatError(err),
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}, 300)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private formatError(err: unknown): string {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
111
|
+
const cause =
|
|
112
|
+
err instanceof Error && 'cause' in err ? ` (${err.cause})` : ''
|
|
113
|
+
return `${message}${cause} - ${API_URL}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private handleNameChange = (value: string): void => {
|
|
117
|
+
this.setState({ name: toSlug(value) })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private handleSubmit = async (): Promise<void> => {
|
|
121
|
+
const { availability, name } = this.state
|
|
122
|
+
if (availability !== 'available' || name.length < 3) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.setState({ step: 'registering' })
|
|
127
|
+
await this.register()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async register(): Promise<void> {
|
|
131
|
+
const { name } = this.state
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const credentials = await this.apiClient.registerProject({ name })
|
|
135
|
+
this.setState({ credentials })
|
|
136
|
+
|
|
137
|
+
this.configManager.saveCredentials(name, credentials)
|
|
138
|
+
await this.installAndConfigure()
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.setState({
|
|
141
|
+
step: 'error',
|
|
142
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private async installAndConfigure(): Promise<void> {
|
|
148
|
+
this.setState({ step: 'installing' })
|
|
149
|
+
const installResult = this.installDependencies()
|
|
150
|
+
if (!installResult.success) {
|
|
151
|
+
this.setState({
|
|
152
|
+
step: 'error',
|
|
153
|
+
errorMessage:
|
|
154
|
+
installResult.message ?? 'Failed to install dependencies.',
|
|
155
|
+
})
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.setState({ step: 'configuring' })
|
|
160
|
+
const jestConfigurator = new JestConfigurator()
|
|
161
|
+
const jestConfig = jestConfigurator.configure()
|
|
162
|
+
this.setState({ jestConfig, step: 'success' })
|
|
163
|
+
|
|
164
|
+
setTimeout(() => this.props.exit(), 3000)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private installDependencies(): InstallResult {
|
|
168
|
+
if (!existsSync('package.json')) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
message:
|
|
172
|
+
'No package.json found. regressionproof currently supports Node.js + Jest projects.',
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let packageJson: {
|
|
177
|
+
dependencies?: Record<string, string>
|
|
178
|
+
devDependencies?: Record<string, string>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as {
|
|
183
|
+
dependencies?: Record<string, string>
|
|
184
|
+
devDependencies?: Record<string, string>
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
message: `Failed to read package.json: ${message}`,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const hasReporter = Boolean(
|
|
195
|
+
packageJson.dependencies?.['@regressionproof/jest-reporter'] ??
|
|
196
|
+
packageJson.devDependencies?.['@regressionproof/jest-reporter']
|
|
197
|
+
)
|
|
198
|
+
if (hasReporter) {
|
|
199
|
+
return { success: true }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const packageManager = this.getPackageManager()
|
|
203
|
+
const result = spawnSync(
|
|
204
|
+
packageManager.command,
|
|
205
|
+
[...packageManager.args, '@regressionproof/jest-reporter'],
|
|
206
|
+
{
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
shell: true,
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if (result.error || result.status !== 0) {
|
|
213
|
+
const details =
|
|
214
|
+
result.stderr?.trim() ||
|
|
215
|
+
result.stdout?.trim() ||
|
|
216
|
+
result.error?.message
|
|
217
|
+
return {
|
|
218
|
+
success: false,
|
|
219
|
+
message: `Failed to install dependencies${
|
|
220
|
+
details ? `: ${details}` : ''
|
|
221
|
+
}`,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { success: true }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private getPackageManager(): PackageManager {
|
|
229
|
+
if (existsSync('pnpm-lock.yaml')) {
|
|
230
|
+
return { command: 'pnpm', args: ['add', '-D'] }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (existsSync('yarn.lock')) {
|
|
234
|
+
return { command: 'yarn', args: ['add', '-D'] }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (existsSync('package-lock.json')) {
|
|
238
|
+
return { command: 'npm', args: ['install', '-D'] }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { command: 'npm', args: ['install', '-D'] }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private renderStatusIndicator(): React.ReactNode {
|
|
245
|
+
const { availability, errorMessage } = this.state
|
|
246
|
+
|
|
247
|
+
switch (availability) {
|
|
248
|
+
case 'idle':
|
|
249
|
+
return null
|
|
250
|
+
case 'checking':
|
|
251
|
+
return <Text color="yellow">checking...</Text>
|
|
252
|
+
case 'available':
|
|
253
|
+
return <Text color="green">available (press Enter)</Text>
|
|
254
|
+
case 'taken':
|
|
255
|
+
return <Text color="red">already taken</Text>
|
|
256
|
+
case 'error':
|
|
257
|
+
return <Text color="red">{errorMessage}</Text>
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private renderRegistering(): React.ReactElement {
|
|
262
|
+
return (
|
|
263
|
+
<Box flexDirection="column" padding={1}>
|
|
264
|
+
<Text color="yellow">
|
|
265
|
+
Registering project "{this.state.name}"...
|
|
266
|
+
</Text>
|
|
267
|
+
</Box>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private renderConfiguring(): React.ReactElement {
|
|
272
|
+
return (
|
|
273
|
+
<Box flexDirection="column" padding={1}>
|
|
274
|
+
<Text color="yellow">Configuring Jest reporter...</Text>
|
|
275
|
+
</Box>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private renderInstalling(): React.ReactElement {
|
|
280
|
+
return (
|
|
281
|
+
<Box flexDirection="column" padding={1}>
|
|
282
|
+
<Text color="yellow">Installing dependencies...</Text>
|
|
283
|
+
</Box>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private renderSuccess(): React.ReactElement {
|
|
288
|
+
const { name, credentials, jestConfig } = this.state
|
|
289
|
+
const configDir = this.configManager.getConfigDir(name)
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<Box flexDirection="column" padding={1}>
|
|
293
|
+
<Text color="green" bold>
|
|
294
|
+
Project registered successfully!
|
|
295
|
+
</Text>
|
|
296
|
+
<Box marginTop={1} flexDirection="column">
|
|
297
|
+
<Text>
|
|
298
|
+
Config saved to:{' '}
|
|
299
|
+
<Text color="cyan">{configDir}/config.json</Text>
|
|
300
|
+
</Text>
|
|
301
|
+
<Text>
|
|
302
|
+
Git remote: <Text color="cyan">{credentials?.url}</Text>
|
|
303
|
+
</Text>
|
|
304
|
+
{jestConfig?.configured ? (
|
|
305
|
+
<Text>
|
|
306
|
+
Jest reporter added to:{' '}
|
|
307
|
+
<Text color="cyan">{jestConfig.location}</Text>
|
|
308
|
+
</Text>
|
|
309
|
+
) : (
|
|
310
|
+
<Text color="yellow">
|
|
311
|
+
Could not auto-configure Jest. Add manually:
|
|
312
|
+
</Text>
|
|
313
|
+
)}
|
|
314
|
+
</Box>
|
|
315
|
+
{!jestConfig?.configured && (
|
|
316
|
+
<Box marginTop={1} flexDirection="column">
|
|
317
|
+
<Text color="gray">// jest.config.js</Text>
|
|
318
|
+
<Text color="gray">
|
|
319
|
+
reporters: ['default',
|
|
320
|
+
'@regressionproof/jest-reporter']
|
|
321
|
+
</Text>
|
|
322
|
+
</Box>
|
|
323
|
+
)}
|
|
324
|
+
<Box marginTop={1}>
|
|
325
|
+
<Text color="green">
|
|
326
|
+
Run your tests and snapshots will be captured
|
|
327
|
+
automatically!
|
|
328
|
+
</Text>
|
|
329
|
+
</Box>
|
|
330
|
+
</Box>
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private renderError(): React.ReactElement {
|
|
335
|
+
return (
|
|
336
|
+
<Box flexDirection="column" padding={1}>
|
|
337
|
+
<Text color="red" bold>
|
|
338
|
+
Registration failed
|
|
339
|
+
</Text>
|
|
340
|
+
<Text color="red">{this.state.errorMessage}</Text>
|
|
341
|
+
</Box>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private renderInput(): React.ReactElement {
|
|
346
|
+
const { name } = this.state
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<Box flexDirection="column" padding={1}>
|
|
350
|
+
<BigText
|
|
351
|
+
text="regressionproof.ai"
|
|
352
|
+
font="tiny"
|
|
353
|
+
colors={['magenta', 'cyan']}
|
|
354
|
+
/>
|
|
355
|
+
<Text color="gray">Teaching LLMs to write better code.</Text>
|
|
356
|
+
|
|
357
|
+
<Box marginTop={1} flexDirection="column">
|
|
358
|
+
<Text bold>Project name:</Text>
|
|
359
|
+
<Box>
|
|
360
|
+
<TextInput
|
|
361
|
+
value={name}
|
|
362
|
+
onChange={this.handleNameChange}
|
|
363
|
+
onSubmit={this.handleSubmit}
|
|
364
|
+
placeholder="my-awesome-project"
|
|
365
|
+
/>
|
|
366
|
+
<Box marginLeft={2}>{this.renderStatusIndicator()}</Box>
|
|
367
|
+
</Box>
|
|
368
|
+
{name.length > 0 && name.length < 3 && (
|
|
369
|
+
<Text color="gray">
|
|
370
|
+
Name must be at least 3 characters
|
|
371
|
+
</Text>
|
|
372
|
+
)}
|
|
373
|
+
</Box>
|
|
374
|
+
</Box>
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
public render(): React.ReactElement {
|
|
379
|
+
const { step } = this.state
|
|
380
|
+
|
|
381
|
+
switch (step) {
|
|
382
|
+
case 'registering':
|
|
383
|
+
return this.renderRegistering()
|
|
384
|
+
case 'installing':
|
|
385
|
+
return this.renderInstalling()
|
|
386
|
+
case 'configuring':
|
|
387
|
+
return this.renderConfiguring()
|
|
388
|
+
case 'success':
|
|
389
|
+
return this.renderSuccess()
|
|
390
|
+
case 'error':
|
|
391
|
+
return this.renderError()
|
|
392
|
+
default:
|
|
393
|
+
return this.renderInput()
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default function Init(props: {
|
|
399
|
+
projectName?: string
|
|
400
|
+
}): React.ReactElement {
|
|
401
|
+
const { exit } = useApp()
|
|
402
|
+
return <InitComponent exit={exit} projectName={props.projectName} />
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type Step =
|
|
406
|
+
| 'input'
|
|
407
|
+
| 'registering'
|
|
408
|
+
| 'installing'
|
|
409
|
+
| 'configuring'
|
|
410
|
+
| 'success'
|
|
411
|
+
| 'error'
|
|
412
|
+
type Availability = 'idle' | 'checking' | 'available' | 'taken' | 'error'
|
|
413
|
+
|
|
414
|
+
interface Props {
|
|
415
|
+
exit: () => void
|
|
416
|
+
projectName?: string
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
interface State {
|
|
420
|
+
name: string
|
|
421
|
+
step: Step
|
|
422
|
+
availability: Availability
|
|
423
|
+
errorMessage: string
|
|
424
|
+
credentials: Credentials | null
|
|
425
|
+
jestConfig: JestConfigResult | null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
interface PackageManager {
|
|
429
|
+
command: string
|
|
430
|
+
args: string[]
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
interface InstallResult {
|
|
434
|
+
success: boolean
|
|
435
|
+
message?: string
|
|
436
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default class ConfigManager {
|
|
2
|
+
private baseDir;
|
|
3
|
+
constructor(baseDir?: string);
|
|
4
|
+
getConfigDir(projectName: string): string;
|
|
5
|
+
saveCredentials(projectName: string, credentials: Credentials): void;
|
|
6
|
+
loadCredentials(projectName: string): Credentials | null;
|
|
7
|
+
}
|
|
8
|
+
export interface Credentials {
|
|
9
|
+
url: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProjectConfig {
|
|
13
|
+
remote: {
|
|
14
|
+
url: string;
|
|
15
|
+
token: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export default class ConfigManager {
|
|
5
|
+
baseDir;
|
|
6
|
+
constructor(baseDir = path.join(os.homedir(), '.regressionproof')) {
|
|
7
|
+
this.baseDir = baseDir;
|
|
8
|
+
}
|
|
9
|
+
getConfigDir(projectName) {
|
|
10
|
+
return path.join(this.baseDir, projectName);
|
|
11
|
+
}
|
|
12
|
+
saveCredentials(projectName, credentials) {
|
|
13
|
+
const configDir = this.getConfigDir(projectName);
|
|
14
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
15
|
+
const configPath = path.join(configDir, 'config.json');
|
|
16
|
+
const config = {
|
|
17
|
+
remote: {
|
|
18
|
+
url: credentials.url,
|
|
19
|
+
token: credentials.token,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
23
|
+
}
|
|
24
|
+
loadCredentials(projectName) {
|
|
25
|
+
const configPath = path.join(this.getConfigDir(projectName), 'config.json');
|
|
26
|
+
if (!fs.existsSync(configPath)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
30
|
+
return {
|
|
31
|
+
url: config.remote.url,
|
|
32
|
+
token: config.remote.token,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
package/build/esm/cli.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import acceptInvite from './commands/invite/AcceptInvite.js.js';
|
|
6
|
+
import createInvite from './commands/invite/CreateInvite.js.js';
|
|
7
|
+
import listInvites from './commands/invite/ListInvites.js.js';
|
|
8
|
+
import revokeInvite from './commands/invite/RevokeInvite.js.js';
|
|
9
|
+
import Init from './components/Init.js.js';
|
|
10
|
+
const command = process.argv[2];
|
|
11
|
+
const projectNameArg = process.argv[3];
|
|
12
|
+
if (command === 'init') {
|
|
13
|
+
render(React.createElement(Init, { projectName: projectNameArg }));
|
|
14
|
+
}
|
|
15
|
+
else if (command === 'invite') {
|
|
16
|
+
const subcommand = process.argv[3];
|
|
17
|
+
const arg = process.argv[4];
|
|
18
|
+
if (subcommand === 'create') {
|
|
19
|
+
const noteArg = process.argv.find((value) => value.startsWith('--note='));
|
|
20
|
+
const note = noteArg ? noteArg.replace('--note=', '') : undefined;
|
|
21
|
+
void createInvite(arg, note);
|
|
22
|
+
}
|
|
23
|
+
else if (subcommand === 'accept') {
|
|
24
|
+
if (!arg) {
|
|
25
|
+
console.error('Usage: regressionproof invite accept <token>');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
void acceptInvite(arg);
|
|
29
|
+
}
|
|
30
|
+
else if (subcommand === 'list') {
|
|
31
|
+
void listInvites(arg);
|
|
32
|
+
}
|
|
33
|
+
else if (subcommand === 'revoke') {
|
|
34
|
+
if (!arg) {
|
|
35
|
+
console.error('Usage: regressionproof invite revoke <token>');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
void revokeInvite(arg);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.error('Usage: regressionproof invite <create|accept|list|revoke>');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log('Usage: regressionproof <command>');
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log('Commands:');
|
|
49
|
+
console.log(' init [projectName] Initialize a new project');
|
|
50
|
+
console.log(' invite ... Manage project invites');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function acceptInvite(token: string): Promise<void>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var _a;
|
|
11
|
+
const API_URL = (_a = process.env.REGRESSIONPROOF_API_URL) !== null && _a !== void 0 ? _a : 'https://api.regressionproof.ai';
|
|
12
|
+
export default function acceptInvite(token) {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
const response = yield fetch(`${API_URL}/invites/accept`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({ token }),
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
const text = yield response.text();
|
|
23
|
+
throw new Error(`Invite accept failed: ${response.status} ${text}`);
|
|
24
|
+
}
|
|
25
|
+
const data = (yield response.json());
|
|
26
|
+
console.log('Project URL:', data.url);
|
|
27
|
+
console.log('Project token:', data.token);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function createInvite(projectName?: string, note?: string): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var _a;
|
|
11
|
+
import ConfigManager from '../../config/ConfigManager.js.js';
|
|
12
|
+
import { getRepoNameFromGit, toSlug } from '../../utilities/slug.js.js';
|
|
13
|
+
const API_URL = (_a = process.env.REGRESSIONPROOF_API_URL) !== null && _a !== void 0 ? _a : 'https://api.regressionproof.ai';
|
|
14
|
+
class InviteCreator {
|
|
15
|
+
constructor(configManager = new ConfigManager()) {
|
|
16
|
+
this.configManager = configManager;
|
|
17
|
+
}
|
|
18
|
+
run(projectNameArg, note) {
|
|
19
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20
|
+
const projectName = this.resolveProjectName(projectNameArg);
|
|
21
|
+
const creds = this.configManager.loadCredentials(projectName);
|
|
22
|
+
if (!creds) {
|
|
23
|
+
throw new Error(`No credentials found for ${projectName}. Run regressionproof init first.`);
|
|
24
|
+
}
|
|
25
|
+
const response = yield fetch(`${API_URL}/invites`, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
Authorization: `Bearer ${creds.token}`,
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ name: projectName, note }),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
const text = yield response.text();
|
|
35
|
+
throw new Error(`Invite create failed: ${response.status} ${text}`);
|
|
36
|
+
}
|
|
37
|
+
const data = (yield response.json());
|
|
38
|
+
console.log('Invite token:', data.token);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
resolveProjectName(projectNameArg) {
|
|
42
|
+
const provided = projectNameArg ? toSlug(projectNameArg) : '';
|
|
43
|
+
const name = provided || getRepoNameFromGit();
|
|
44
|
+
if (!name) {
|
|
45
|
+
throw new Error('Project name is required. Provide it explicitly or ensure git origin is set.');
|
|
46
|
+
}
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export default function createInvite(projectName, note) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
const creator = new InviteCreator();
|
|
53
|
+
yield creator.run(projectName, note);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function listInvites(projectName?: string): Promise<void>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var _a;
|
|
11
|
+
const API_URL = (_a = process.env.REGRESSIONPROOF_API_URL) !== null && _a !== void 0 ? _a : 'https://api.regressionproof.ai';
|
|
12
|
+
export default function listInvites(projectName) {
|
|
13
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14
|
+
const query = projectName ? `?name=${encodeURIComponent(projectName)}` : '';
|
|
15
|
+
const response = yield fetch(`${API_URL}/invites${query}`);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const text = yield response.text();
|
|
18
|
+
throw new Error(`Invite list failed: ${response.status} ${text}`);
|
|
19
|
+
}
|
|
20
|
+
const data = (yield response.json());
|
|
21
|
+
if (data.length === 0) {
|
|
22
|
+
console.log('No invites found.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const invite of data) {
|
|
26
|
+
console.log(`${invite.projectName} | ${invite.status} | created ${invite.createdAt}` +
|
|
27
|
+
(invite.note ? ` | ${invite.note}` : ''));
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function revokeInvite(token: string): Promise<void>;
|