badgr-cli 1.0.12 → 1.0.15
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/package.json +12 -4
- package/src/badgr.js +46 -50
- package/src/commands/capacity.js +63 -56
- package/src/commands/down.js +26 -23
- package/src/commands/login.js +20 -3
- package/src/commands/run.js +132 -75
- package/src/commands/serve.js +57 -75
- package/src/commands/status.js +35 -48
- package/src/commands/up.js +32 -26
- package/src/fallback.js +34 -63
- package/src/router.js +8 -69
- package/tests/commands.test.js +30 -28
- package/tests/router.test.js +9 -68
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "badgr-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Badgr
|
|
3
|
+
"version": "1.0.15",
|
|
4
|
+
"description": "Badgr, run or serve GPU workloads from one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"badgr": "
|
|
7
|
+
"badgr": "src/badgr.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/badgr.js",
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=18.0.0"
|
|
23
23
|
},
|
|
24
|
-
"keywords": [
|
|
24
|
+
"keywords": [
|
|
25
|
+
"gpu",
|
|
26
|
+
"cli",
|
|
27
|
+
"ai",
|
|
28
|
+
"compute",
|
|
29
|
+
"modal",
|
|
30
|
+
"gateway",
|
|
31
|
+
"openai"
|
|
32
|
+
],
|
|
25
33
|
"license": "MIT"
|
|
26
34
|
}
|
package/src/badgr.js
CHANGED
|
@@ -15,64 +15,60 @@ import { capacityCommand } from './commands/capacity.js';
|
|
|
15
15
|
const HELP = `
|
|
16
16
|
${chalk.bold('badgr')} — run or serve GPU workloads from one command
|
|
17
17
|
|
|
18
|
-
${chalk.bold('
|
|
19
|
-
${chalk.cyan('badgr login')}
|
|
20
|
-
${chalk.cyan('badgr run <
|
|
21
|
-
${chalk.cyan('badgr serve <model>
|
|
22
|
-
${chalk.cyan('badgr status')}
|
|
23
|
-
${chalk.cyan('badgr logs <id>')}
|
|
24
|
-
${chalk.cyan('badgr down <id>')}
|
|
25
|
-
${chalk.cyan('badgr receipts
|
|
26
|
-
${chalk.cyan('badgr capacity
|
|
27
|
-
|
|
28
|
-
${chalk.bold('badgr run OPTIONS')}
|
|
29
|
-
--gpu <type> GPU: RTX_4090, A100, L40S, H100 (default: RTX_4090)
|
|
30
|
-
--image <image> Docker image (default: python:3.11-slim)
|
|
31
|
-
--count <n> GPU count (default: 1)
|
|
32
|
-
--region US|EU|AU Region preference (default: US)
|
|
33
|
-
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
34
|
-
--name <name> Job name (auto-generated if omitted)
|
|
35
|
-
--fallback closest|cheapest If requested GPU is unavailable, interactively pick the
|
|
36
|
-
closest (default) or cheapest alternative
|
|
37
|
-
--no-fallback Exit immediately when requested GPU is unavailable
|
|
38
|
-
--max-runtime <minutes> Auto-teardown job after N minutes (prevents runaway billing)
|
|
39
|
-
--max-cost <$> Auto-teardown when total spend reaches this amount
|
|
40
|
-
|
|
41
|
-
${chalk.bold('badgr serve OPTIONS')}
|
|
42
|
-
--gpu <type> GPU: L40S, A100, H100, RTX_4090 (default: L40S)
|
|
43
|
-
--count <n> GPU count (default: 1)
|
|
44
|
-
--region US|EU|AU Region preference (default: US)
|
|
45
|
-
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
46
|
-
--name <name> Deployment name (auto-generated if omitted)
|
|
18
|
+
${chalk.bold('COMMANDS')}
|
|
19
|
+
${chalk.cyan('badgr login')} Authenticate with your API key
|
|
20
|
+
${chalk.cyan('badgr run <command>')} Run a one-off GPU job
|
|
21
|
+
${chalk.cyan('badgr serve <model>')} Serve a model with an OpenAI-compatible endpoint
|
|
22
|
+
${chalk.cyan('badgr status')} Show what's running and what's billing
|
|
23
|
+
${chalk.cyan('badgr logs <id>')} Stream logs for a running job or endpoint
|
|
24
|
+
${chalk.cyan('badgr down <id>')} Stop a deployment and end billing
|
|
25
|
+
${chalk.cyan('badgr receipts')} Show cost history
|
|
26
|
+
${chalk.cyan('badgr capacity')} Check what GPU capacity is available right now
|
|
47
27
|
|
|
48
28
|
${chalk.bold('EXAMPLES')}
|
|
49
|
-
|
|
29
|
+
${chalk.dim('# Simplest — Badgr picks the GPU:')}
|
|
30
|
+
badgr run python train.py
|
|
31
|
+
badgr serve meta-llama/Llama-3.1-8B-Instruct
|
|
32
|
+
|
|
33
|
+
${chalk.dim('# Pin a specific GPU:')}
|
|
50
34
|
badgr run python train.py --gpu A100
|
|
51
|
-
badgr run python train.py --gpu A100 --fallback closest
|
|
52
|
-
badgr run python train.py --gpu A100 --fallback cheapest
|
|
53
|
-
badgr run python train.py --gpu A100 --no-fallback
|
|
54
|
-
badgr run --image my/image:latest --gpu L40S
|
|
55
35
|
badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S
|
|
56
|
-
|
|
36
|
+
|
|
37
|
+
${chalk.dim('# Add safety caps:')}
|
|
38
|
+
badgr run python train.py --max-runtime 60 --max-cost 5
|
|
39
|
+
|
|
40
|
+
${chalk.dim('# Manage a running deployment:')}
|
|
57
41
|
badgr status
|
|
58
|
-
badgr logs
|
|
59
|
-
badgr down
|
|
60
|
-
badgr receipts
|
|
61
|
-
badgr receipts dep_abc123
|
|
42
|
+
badgr logs dep-abc123
|
|
43
|
+
badgr down dep-abc123
|
|
44
|
+
badgr receipts dep-abc123
|
|
62
45
|
|
|
63
|
-
${chalk.bold('
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
${chalk.bold('badgr run OPTIONS')}
|
|
47
|
+
--gpu <type> GPU type (default: auto — Badgr picks cheapest available)
|
|
48
|
+
--image <image> Docker image (default: python:3.11-slim)
|
|
49
|
+
--count <n> Number of GPUs (default: 1)
|
|
50
|
+
--region US|EU|AU Region preference
|
|
51
|
+
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
52
|
+
--max-runtime <min> Auto-stop after N minutes (recommended)
|
|
53
|
+
--max-cost <$> Auto-stop when spend reaches this amount
|
|
54
|
+
--detach Return immediately, don't stream logs
|
|
55
|
+
|
|
56
|
+
${chalk.bold('badgr serve OPTIONS')}
|
|
57
|
+
--gpu <type> GPU type (default: auto — inferred from model size)
|
|
58
|
+
--count <n> Number of GPUs (default: 1)
|
|
59
|
+
--region US|EU|AU Region preference
|
|
60
|
+
--max-price <$/hr> Hard spend cap per GPU-hour
|
|
61
|
+
--no-wait Skip endpoint health check
|
|
67
62
|
|
|
68
|
-
${chalk.bold('
|
|
69
|
-
${chalk.dim('
|
|
63
|
+
${chalk.bold('AFTER SERVING')}
|
|
64
|
+
${chalk.dim('Point any OpenAI client at the returned URL:')}
|
|
65
|
+
${chalk.dim(' from openai import OpenAI')}
|
|
66
|
+
${chalk.dim(' client = OpenAI(base_url="<endpoint url>", api_key="<your key>")')}
|
|
67
|
+
${chalk.dim(' client.chat.completions.create(model="<model>", messages=[...])')}
|
|
70
68
|
|
|
71
|
-
${chalk.bold('
|
|
72
|
-
${chalk.dim('
|
|
73
|
-
${chalk.dim('
|
|
74
|
-
${chalk.dim(' BADGR_DEBUG=1 badgr serve meta-llama/Llama-3.1-8B-Instruct --gpu L40S')}
|
|
75
|
-
${chalk.dim(' badgr config # show current baseUrl and key')}
|
|
69
|
+
${chalk.bold('DEBUG')}
|
|
70
|
+
${chalk.dim('BADGR_DEBUG=1 badgr run python train.py # full request/response trace')}
|
|
71
|
+
${chalk.dim('badgr config # show current API config')}
|
|
76
72
|
`;
|
|
77
73
|
|
|
78
74
|
async function main() {
|
package/src/commands/capacity.js
CHANGED
|
@@ -2,15 +2,9 @@ import { requireApiKey } from '../config.js';
|
|
|
2
2
|
import { callApi } from '../api.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* badgr capacity
|
|
6
|
-
* badgr capacity --gpu
|
|
7
|
-
* badgr capacity --gpu A100 --region EU
|
|
8
|
-
* badgr capacity --max-price 5
|
|
9
|
-
*
|
|
10
|
-
* Internal diagnostic command: shows per-provider availability breakdown.
|
|
11
|
-
* Provider display names come from the backend — none are hardcoded here.
|
|
5
|
+
* badgr capacity # show what's available right now (auto)
|
|
6
|
+
* badgr capacity --gpu A100 # check a specific GPU type
|
|
12
7
|
*/
|
|
13
|
-
|
|
14
8
|
function parseCapacityArgs(args) {
|
|
15
9
|
const flags = {};
|
|
16
10
|
let i = 0;
|
|
@@ -27,17 +21,50 @@ export async function capacityCommand(config, args, chalk) {
|
|
|
27
21
|
const flags = parseCapacityArgs(args);
|
|
28
22
|
requireApiKey(config);
|
|
29
23
|
|
|
30
|
-
const gpu = flags.gpu || 'RTX_4090';
|
|
31
24
|
const maxPrice = flags.maxPrice ?? 10;
|
|
32
25
|
|
|
26
|
+
// No --gpu: show cheapest runnable GPU across all types
|
|
27
|
+
if (!flags.gpu) {
|
|
28
|
+
console.log(chalk.bold('\nAvailable GPU capacity\n'));
|
|
29
|
+
process.stdout.write(chalk.dim(' Checking availability...\n'));
|
|
30
|
+
|
|
31
|
+
let data;
|
|
32
|
+
try {
|
|
33
|
+
const params = new URLSearchParams({ max_price: String(maxPrice) });
|
|
34
|
+
if (flags.region) params.set('region', flags.region.toUpperCase());
|
|
35
|
+
data = await callApi(`/capacity/auto?${params}`, {
|
|
36
|
+
apiKey: config.apiKey,
|
|
37
|
+
baseUrl: config.baseUrl,
|
|
38
|
+
});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err.errorData?.code === 'NO_CAPACITY_MATCH') {
|
|
41
|
+
console.log(chalk.dim('\n No GPU capacity available right now.\n'));
|
|
42
|
+
console.log(chalk.dim(' Try again in a few minutes, or check a specific GPU with --gpu <type>.\n'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.error(chalk.red(`\n ✗ Capacity check failed: ${err.message}\n`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(` ${chalk.bold('Cheapest available:')} ${chalk.cyan(data.gpu)} in ${data.region} ${chalk.green('$' + data.price.toFixed(2) + '/hr')}`);
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(` ${chalk.bold('Run now:')}`);
|
|
53
|
+
console.log(chalk.cyan(` badgr run python train.py`));
|
|
54
|
+
console.log(chalk.cyan(` badgr serve meta-llama/Llama-3.1-8B-Instruct`));
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(chalk.dim(` Use badgr capacity --gpu A100 to check a specific GPU type.`));
|
|
57
|
+
console.log();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --gpu specified: show availability for that type
|
|
62
|
+
const gpu = flags.gpu.toUpperCase().replace('-', '_');
|
|
33
63
|
const params = new URLSearchParams({ gpu, max_price: String(maxPrice) });
|
|
34
64
|
if (flags.region) params.set('region', flags.region.toUpperCase());
|
|
35
65
|
|
|
36
|
-
console.log(chalk.bold(`\
|
|
37
|
-
|
|
38
|
-
if (flags.region) console.log(` ${chalk.bold('Region:')} ${flags.region.toUpperCase()}`);
|
|
39
|
-
console.log(` ${chalk.bold('Max price:')} $${maxPrice.toFixed(2)}/hr`);
|
|
40
|
-
console.log();
|
|
66
|
+
console.log(chalk.bold(`\nCapacity: ${gpu}\n`));
|
|
67
|
+
process.stdout.write(chalk.dim(' Checking...\n'));
|
|
41
68
|
|
|
42
69
|
let data;
|
|
43
70
|
try {
|
|
@@ -50,55 +77,35 @@ export async function capacityCommand(config, args, chalk) {
|
|
|
50
77
|
process.exit(1);
|
|
51
78
|
}
|
|
52
79
|
|
|
53
|
-
// Per-provider breakdown
|
|
54
|
-
const diags = data.diagnostics ?? [];
|
|
55
|
-
if (diags.length > 0) {
|
|
56
|
-
console.log(chalk.bold(' Provider breakdown:'));
|
|
57
|
-
for (const d of diags) {
|
|
58
|
-
const padded = (d.display_name ?? 'Unknown').padEnd(12);
|
|
59
|
-
if (d.status === 'no_key') {
|
|
60
|
-
console.log(` ${chalk.dim(padded)} ${chalk.dim('disabled — ' + d.reason)}`);
|
|
61
|
-
} else if (d.status === 'error') {
|
|
62
|
-
console.log(` ${chalk.dim(padded)} ${chalk.red('error — ' + d.reason)}`);
|
|
63
|
-
} else if (d.status === 'ok') {
|
|
64
|
-
const price = d.cheapest_price != null ? chalk.green(`cheapest $${d.cheapest_price.toFixed(2)}/hr`) : '';
|
|
65
|
-
console.log(` ${chalk.bold(padded)} ${d.eligible_count} eligible ${price}`);
|
|
66
|
-
} else {
|
|
67
|
-
const reason = d.reason ? chalk.dim(` — ${d.reason}`) : '';
|
|
68
|
-
console.log(` ${chalk.dim(padded)} 0 eligible${reason}`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
console.log();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Direct matches
|
|
75
80
|
const matches = data.matches ?? [];
|
|
76
81
|
if (matches.length > 0) {
|
|
77
|
-
console.log(chalk.bold(' Available now
|
|
82
|
+
console.log(chalk.bold('\n Available now:\n'));
|
|
78
83
|
for (const m of matches) {
|
|
79
|
-
console.log(` ${chalk.green('
|
|
84
|
+
console.log(` ${chalk.green('●')} ${m.gpu} in ${m.region} ${chalk.green('$' + m.price.toFixed(2) + '/hr')}`);
|
|
80
85
|
}
|
|
81
86
|
console.log();
|
|
82
|
-
|
|
83
|
-
console.log(
|
|
87
|
+
const regionFlag = flags.region ? ` --region ${flags.region.toUpperCase()}` : '';
|
|
88
|
+
console.log(` ${chalk.bold('Run:')}`);
|
|
89
|
+
console.log(chalk.cyan(` badgr run python train.py --gpu ${gpu}${regionFlag}`));
|
|
90
|
+
console.log();
|
|
84
91
|
} else {
|
|
85
|
-
|
|
86
|
-
}
|
|
92
|
+
const regionLabel = flags.region ? ` in ${flags.region.toUpperCase()}` : '';
|
|
93
|
+
console.log(chalk.dim(`\n No ${gpu} available right now under $${maxPrice.toFixed(2)}/hr${regionLabel}.\n`));
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
console.log(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
console.log(
|
|
95
|
+
const alternatives = data.alternatives ?? [];
|
|
96
|
+
if (alternatives.length > 0) {
|
|
97
|
+
console.log(chalk.bold(' Available alternatives:\n'));
|
|
98
|
+
for (const a of alternatives) {
|
|
99
|
+
console.log(` ${chalk.dim('●')} ${a.gpu} in ${a.region} $${a.price.toFixed(2)}/hr`);
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
console.log(` ${chalk.bold('Try:')}`);
|
|
103
|
+
for (const a of alternatives.slice(0, 3)) {
|
|
104
|
+
console.log(chalk.cyan(` badgr run python train.py --gpu ${a.gpu}`));
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
} else {
|
|
108
|
+
console.log(chalk.dim(' No alternatives found. Try `badgr capacity` to see global availability.\n'));
|
|
100
109
|
}
|
|
101
110
|
}
|
|
102
|
-
|
|
103
|
-
console.log();
|
|
104
111
|
}
|
package/src/commands/down.js
CHANGED
|
@@ -12,43 +12,46 @@ export async function downCommand(config, args, chalk) {
|
|
|
12
12
|
|
|
13
13
|
requireApiKey(config);
|
|
14
14
|
|
|
15
|
-
const localDep
|
|
16
|
-
|
|
17
|
-
// Resolve the deployment ID to send to the backend
|
|
15
|
+
const localDep = findDeployment(idOrName);
|
|
18
16
|
const deploymentId = localDep?.id ?? idOrName;
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
process.stdout.write(chalk.dim(` Stopping ${deploymentId}...`));
|
|
21
19
|
|
|
22
20
|
let dep;
|
|
23
21
|
try {
|
|
24
22
|
dep = await terminateDeployment(config, deploymentId);
|
|
25
23
|
} catch (err) {
|
|
26
|
-
|
|
24
|
+
process.stdout.write('\n');
|
|
25
|
+
console.error(chalk.red(`\n ✗ Could not stop deployment: ${err.message}\n`));
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
process.stdout.write('\n');
|
|
30
|
+
|
|
31
|
+
// Compute final cost from the deployment timestamps
|
|
32
|
+
const stoppedAt = dep.stopped_at ?? (Date.now() / 1000);
|
|
33
|
+
const startedAt = dep.started_at ?? stoppedAt;
|
|
34
|
+
const runtimeMin = Math.round((stoppedAt - startedAt) / 60);
|
|
35
|
+
const runtimeHr = (stoppedAt - startedAt) / 3600;
|
|
36
|
+
const finalCost = (dep.cost_per_hour || 0) * runtimeHr;
|
|
37
|
+
|
|
30
38
|
const rcptId = generateReceiptId();
|
|
31
39
|
addReceipt({
|
|
32
|
-
receiptId:
|
|
33
|
-
action:
|
|
34
|
-
deploymentId:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
createdAt: new Date().toISOString(),
|
|
40
|
+
receiptId: rcptId,
|
|
41
|
+
action: 'badgr down',
|
|
42
|
+
deploymentId: dep.deployment_id,
|
|
43
|
+
gpu: dep.gpu_type,
|
|
44
|
+
runtimeSeconds: Math.round(stoppedAt - startedAt),
|
|
45
|
+
finalCost,
|
|
46
|
+
status: 'terminated',
|
|
47
|
+
createdAt: new Date().toISOString(),
|
|
41
48
|
});
|
|
42
49
|
|
|
43
|
-
// Remove from local store
|
|
44
50
|
removeDeployment(idOrName);
|
|
45
51
|
|
|
46
|
-
console.log(chalk.green(
|
|
47
|
-
console.log(
|
|
48
|
-
console.log(` ${chalk.bold('
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
console.log(` ${chalk.bold('Uptime:')} ${uptime} min`);
|
|
52
|
-
}
|
|
53
|
-
console.log(`\n ${chalk.bold('Receipt ID:')} ${chalk.dim(rcptId)}\n`);
|
|
52
|
+
console.log(chalk.green('\n✓ Stopped'));
|
|
53
|
+
console.log(chalk.green(' Billing ended\n'));
|
|
54
|
+
console.log(` ${chalk.bold('Runtime:')} ${runtimeMin}m`);
|
|
55
|
+
if (finalCost > 0) console.log(` ${chalk.bold('Final cost:')} $${finalCost.toFixed(4)}`);
|
|
56
|
+
console.log(` ${chalk.bold('Receipt:')} ${chalk.dim(rcptId)}\n`);
|
|
54
57
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { input } from '@inquirer/prompts';
|
|
2
2
|
import { DEFAULTS } from '../config.js';
|
|
3
|
+
import { callApi } from '../api.js';
|
|
3
4
|
|
|
4
5
|
export async function loginCommand(chalk, saveConfigFn) {
|
|
5
|
-
console.log(chalk.bold('\
|
|
6
|
+
console.log(chalk.bold('\nBadgr Login\n'));
|
|
6
7
|
|
|
7
8
|
const apiKey = await input({
|
|
8
9
|
message: 'Enter your Badgr API key:',
|
|
@@ -13,7 +14,23 @@ export async function loginCommand(chalk, saveConfigFn) {
|
|
|
13
14
|
apiKey: apiKey.trim(),
|
|
14
15
|
baseUrl: DEFAULTS.baseUrl,
|
|
15
16
|
});
|
|
16
|
-
|
|
17
|
-
console.log(chalk.
|
|
17
|
+
|
|
18
|
+
console.log(chalk.green('\n✓ Logged in'));
|
|
19
|
+
console.log(chalk.dim(` Config saved to ~/.badgr/config.json`));
|
|
20
|
+
|
|
21
|
+
// Verify the key works against the live API
|
|
22
|
+
try {
|
|
23
|
+
await callApi('/models', { apiKey: config.apiKey, baseUrl: config.baseUrl });
|
|
24
|
+
console.log(chalk.green('✓ API reachable'));
|
|
25
|
+
console.log(chalk.green('✓ API key valid\n'));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.httpStatus === 401 || err.httpStatus === 403) {
|
|
28
|
+
console.log(chalk.yellow('⚠ API key may be invalid — double-check and run badgr login again\n'));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.yellow('⚠ Could not reach the API right now — check your internet connection\n'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(chalk.dim(` Run ${chalk.cyan('badgr run python train.py')} to launch your first job.\n`));
|
|
18
35
|
return config;
|
|
19
36
|
}
|