@hyperdrive.bot/gut 0.1.10 → 0.1.12
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/README.md +130 -5
- package/dist/base-command.d.ts +2 -0
- package/dist/base-command.js +3 -0
- package/dist/commands/auth/login.d.ts +10 -0
- package/dist/commands/auth/login.js +103 -0
- package/dist/commands/auth/logout.d.ts +6 -0
- package/dist/commands/auth/logout.js +39 -0
- package/dist/commands/auth/status.d.ts +9 -0
- package/dist/commands/auth/status.js +87 -0
- package/dist/commands/ticket/config.d.ts +13 -0
- package/dist/commands/ticket/config.js +22 -0
- package/dist/commands/ticket/sync.d.ts +1 -0
- package/dist/commands/ticket/sync.js +62 -8
- package/dist/commands/worktree/create.d.ts +15 -0
- package/dist/commands/worktree/create.js +138 -0
- package/dist/models/entity.model.d.ts +16 -0
- package/dist/models/ticket.model.d.ts +2 -0
- package/dist/services/git.service.d.ts +11 -0
- package/dist/services/git.service.js +57 -0
- package/dist/services/git.service.test.d.ts +1 -0
- package/dist/services/git.service.test.js +101 -0
- package/dist/services/gut-api.service.d.ts +20 -1
- package/dist/services/gut-api.service.js +30 -2
- package/dist/services/tenant.service.d.ts +14 -0
- package/dist/services/tenant.service.js +24 -0
- package/dist/services/ticket.service.d.ts +3 -1
- package/dist/services/ticket.service.js +7 -3
- package/dist/services/worktree.service.d.ts +16 -0
- package/dist/services/worktree.service.js +60 -0
- package/oclif.manifest.json +229 -4
- package/package.json +16 -5
|
@@ -96,12 +96,16 @@ export class TicketService {
|
|
|
96
96
|
/**
|
|
97
97
|
* Sync ticket with external source
|
|
98
98
|
*/
|
|
99
|
-
async syncTicket(ticketId, direction = 'push') {
|
|
99
|
+
async syncTicket(ticketId, direction = 'push', options) {
|
|
100
100
|
if (this.useAuth && this.apiService) {
|
|
101
|
-
return this.apiService.syncTicket(ticketId, direction);
|
|
101
|
+
return this.apiService.syncTicket(ticketId, direction, options);
|
|
102
102
|
}
|
|
103
103
|
// Fallback to unauthenticated mode
|
|
104
|
-
const
|
|
104
|
+
const params = new URLSearchParams({ direction });
|
|
105
|
+
if (options?.noEnrich) {
|
|
106
|
+
params.set('noEnrich', 'true');
|
|
107
|
+
}
|
|
108
|
+
const response = await this.makeFetchRequest('POST', `/gut/tickets/${encodeURIComponent(ticketId)}/sync?${params.toString()}`);
|
|
105
109
|
return response;
|
|
106
110
|
}
|
|
107
111
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorktreeRecord, WorktreeState } from '../models/entity.model.js';
|
|
2
|
+
import type { ConfigService } from './config.service.js';
|
|
3
|
+
import type { FocusService } from './focus.service.js';
|
|
4
|
+
import type { GitService } from './git.service.js';
|
|
5
|
+
export declare class WorktreeService {
|
|
6
|
+
private readonly configService;
|
|
7
|
+
private readonly gitService;
|
|
8
|
+
private readonly focusService;
|
|
9
|
+
private readonly stateFile;
|
|
10
|
+
private readonly stateFileTmp;
|
|
11
|
+
constructor(configService: ConfigService, gitService: GitService, focusService: FocusService);
|
|
12
|
+
get(name: string): WorktreeRecord | null;
|
|
13
|
+
list(): WorktreeRecord[];
|
|
14
|
+
loadState(): WorktreeState;
|
|
15
|
+
saveState(state: WorktreeState): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export class WorktreeService {
|
|
4
|
+
configService;
|
|
5
|
+
gitService;
|
|
6
|
+
focusService;
|
|
7
|
+
stateFile;
|
|
8
|
+
stateFileTmp;
|
|
9
|
+
constructor(configService, gitService, focusService) {
|
|
10
|
+
this.configService = configService;
|
|
11
|
+
this.gitService = gitService;
|
|
12
|
+
this.focusService = focusService;
|
|
13
|
+
this.stateFile = path.join(this.configService.getGutDir(), 'worktrees.json');
|
|
14
|
+
this.stateFileTmp = `${this.stateFile}.tmp`;
|
|
15
|
+
}
|
|
16
|
+
get(name) {
|
|
17
|
+
const state = this.loadState();
|
|
18
|
+
return state.worktrees.find(w => w.name === name) ?? null;
|
|
19
|
+
}
|
|
20
|
+
list() {
|
|
21
|
+
return this.loadState().worktrees;
|
|
22
|
+
}
|
|
23
|
+
loadState() {
|
|
24
|
+
if (!fs.existsSync(this.stateFile)) {
|
|
25
|
+
return { worktrees: [] };
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const content = fs.readFileSync(this.stateFile, 'utf-8');
|
|
29
|
+
const parsed = JSON.parse(content);
|
|
30
|
+
if (!Array.isArray(parsed.worktrees)) {
|
|
31
|
+
return { worktrees: [] };
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
console.warn('Corrupt worktrees.json, resetting state');
|
|
37
|
+
return { worktrees: [] };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
saveState(state) {
|
|
41
|
+
const serialized = JSON.stringify(state, null, 2);
|
|
42
|
+
try {
|
|
43
|
+
fs.writeFileSync(this.stateFileTmp, serialized, 'utf-8');
|
|
44
|
+
fs.renameSync(this.stateFileTmp, this.stateFile);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Cleanup tmp file on failure
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(this.stateFileTmp)) {
|
|
50
|
+
fs.unlinkSync(this.stateFileTmp);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignore cleanup errors
|
|
55
|
+
}
|
|
56
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
57
|
+
throw new Error(`Failed to save worktree state: ${message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1302,6 +1302,107 @@
|
|
|
1302
1302
|
"workspace.js"
|
|
1303
1303
|
]
|
|
1304
1304
|
},
|
|
1305
|
+
"auth:login": {
|
|
1306
|
+
"aliases": [],
|
|
1307
|
+
"args": {},
|
|
1308
|
+
"description": "Authenticate with Gut using OAuth 2.0 PKCE flow",
|
|
1309
|
+
"examples": [
|
|
1310
|
+
"<%= config.bin %> <%= command.id %>",
|
|
1311
|
+
"<%= config.bin %> <%= command.id %> --domain acme.hyperdrive.bot",
|
|
1312
|
+
"<%= config.bin %> <%= command.id %> --port 9876"
|
|
1313
|
+
],
|
|
1314
|
+
"flags": {
|
|
1315
|
+
"domain": {
|
|
1316
|
+
"char": "d",
|
|
1317
|
+
"description": "Tenant domain (e.g., acme.hyperdrive.bot)",
|
|
1318
|
+
"name": "domain",
|
|
1319
|
+
"hasDynamicHelp": false,
|
|
1320
|
+
"multiple": false,
|
|
1321
|
+
"type": "option"
|
|
1322
|
+
},
|
|
1323
|
+
"port": {
|
|
1324
|
+
"char": "p",
|
|
1325
|
+
"description": "Local callback server port",
|
|
1326
|
+
"name": "port",
|
|
1327
|
+
"default": 8766,
|
|
1328
|
+
"hasDynamicHelp": false,
|
|
1329
|
+
"multiple": false,
|
|
1330
|
+
"type": "option"
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
"hasDynamicHelp": false,
|
|
1334
|
+
"hiddenAliases": [],
|
|
1335
|
+
"id": "auth:login",
|
|
1336
|
+
"pluginAlias": "@hyperdrive.bot/gut",
|
|
1337
|
+
"pluginName": "@hyperdrive.bot/gut",
|
|
1338
|
+
"pluginType": "core",
|
|
1339
|
+
"strict": true,
|
|
1340
|
+
"enableJsonFlag": false,
|
|
1341
|
+
"isESM": true,
|
|
1342
|
+
"relativePath": [
|
|
1343
|
+
"dist",
|
|
1344
|
+
"commands",
|
|
1345
|
+
"auth",
|
|
1346
|
+
"login.js"
|
|
1347
|
+
]
|
|
1348
|
+
},
|
|
1349
|
+
"auth:logout": {
|
|
1350
|
+
"aliases": [],
|
|
1351
|
+
"args": {},
|
|
1352
|
+
"description": "Remove stored credentials and logout",
|
|
1353
|
+
"examples": [
|
|
1354
|
+
"<%= config.bin %> <%= command.id %>"
|
|
1355
|
+
],
|
|
1356
|
+
"flags": {},
|
|
1357
|
+
"hasDynamicHelp": false,
|
|
1358
|
+
"hiddenAliases": [],
|
|
1359
|
+
"id": "auth:logout",
|
|
1360
|
+
"pluginAlias": "@hyperdrive.bot/gut",
|
|
1361
|
+
"pluginName": "@hyperdrive.bot/gut",
|
|
1362
|
+
"pluginType": "core",
|
|
1363
|
+
"strict": true,
|
|
1364
|
+
"enableJsonFlag": false,
|
|
1365
|
+
"isESM": true,
|
|
1366
|
+
"relativePath": [
|
|
1367
|
+
"dist",
|
|
1368
|
+
"commands",
|
|
1369
|
+
"auth",
|
|
1370
|
+
"logout.js"
|
|
1371
|
+
]
|
|
1372
|
+
},
|
|
1373
|
+
"auth:status": {
|
|
1374
|
+
"aliases": [],
|
|
1375
|
+
"args": {},
|
|
1376
|
+
"description": "Show current authentication status",
|
|
1377
|
+
"examples": [
|
|
1378
|
+
"<%= config.bin %> <%= command.id %>",
|
|
1379
|
+
"<%= config.bin %> <%= command.id %> --verbose"
|
|
1380
|
+
],
|
|
1381
|
+
"flags": {
|
|
1382
|
+
"verbose": {
|
|
1383
|
+
"char": "v",
|
|
1384
|
+
"description": "Show detailed credential information",
|
|
1385
|
+
"name": "verbose",
|
|
1386
|
+
"allowNo": false,
|
|
1387
|
+
"type": "boolean"
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1390
|
+
"hasDynamicHelp": false,
|
|
1391
|
+
"hiddenAliases": [],
|
|
1392
|
+
"id": "auth:status",
|
|
1393
|
+
"pluginAlias": "@hyperdrive.bot/gut",
|
|
1394
|
+
"pluginName": "@hyperdrive.bot/gut",
|
|
1395
|
+
"pluginType": "core",
|
|
1396
|
+
"strict": true,
|
|
1397
|
+
"enableJsonFlag": false,
|
|
1398
|
+
"isESM": true,
|
|
1399
|
+
"relativePath": [
|
|
1400
|
+
"dist",
|
|
1401
|
+
"commands",
|
|
1402
|
+
"auth",
|
|
1403
|
+
"status.js"
|
|
1404
|
+
]
|
|
1405
|
+
},
|
|
1305
1406
|
"entity:add": {
|
|
1306
1407
|
"aliases": [],
|
|
1307
1408
|
"args": {
|
|
@@ -1603,6 +1704,70 @@
|
|
|
1603
1704
|
"remove.js"
|
|
1604
1705
|
]
|
|
1605
1706
|
},
|
|
1707
|
+
"ticket:config": {
|
|
1708
|
+
"aliases": [],
|
|
1709
|
+
"args": {},
|
|
1710
|
+
"description": "Configure ticket source (JIRA, GitHub, etc.) for this tenant",
|
|
1711
|
+
"examples": [
|
|
1712
|
+
"<%= config.bin %> ticket config --show",
|
|
1713
|
+
"<%= config.bin %> ticket config --type jira --url https://company.atlassian.net --secret-arn arn:aws:secretsmanager:..."
|
|
1714
|
+
],
|
|
1715
|
+
"flags": {
|
|
1716
|
+
"secret-arn": {
|
|
1717
|
+
"description": "AWS Secrets Manager ARN for credentials",
|
|
1718
|
+
"name": "secret-arn",
|
|
1719
|
+
"hasDynamicHelp": false,
|
|
1720
|
+
"multiple": false,
|
|
1721
|
+
"type": "option"
|
|
1722
|
+
},
|
|
1723
|
+
"json": {
|
|
1724
|
+
"char": "j",
|
|
1725
|
+
"description": "output as JSON",
|
|
1726
|
+
"name": "json",
|
|
1727
|
+
"allowNo": false,
|
|
1728
|
+
"type": "boolean"
|
|
1729
|
+
},
|
|
1730
|
+
"show": {
|
|
1731
|
+
"description": "show current configuration",
|
|
1732
|
+
"name": "show",
|
|
1733
|
+
"allowNo": false,
|
|
1734
|
+
"type": "boolean"
|
|
1735
|
+
},
|
|
1736
|
+
"type": {
|
|
1737
|
+
"description": "source type",
|
|
1738
|
+
"name": "type",
|
|
1739
|
+
"hasDynamicHelp": false,
|
|
1740
|
+
"multiple": false,
|
|
1741
|
+
"options": [
|
|
1742
|
+
"jira",
|
|
1743
|
+
"github",
|
|
1744
|
+
"linear"
|
|
1745
|
+
],
|
|
1746
|
+
"type": "option"
|
|
1747
|
+
},
|
|
1748
|
+
"url": {
|
|
1749
|
+
"description": "base URL (e.g., https://company.atlassian.net)",
|
|
1750
|
+
"name": "url",
|
|
1751
|
+
"hasDynamicHelp": false,
|
|
1752
|
+
"multiple": false,
|
|
1753
|
+
"type": "option"
|
|
1754
|
+
}
|
|
1755
|
+
},
|
|
1756
|
+
"hasDynamicHelp": false,
|
|
1757
|
+
"hiddenAliases": [],
|
|
1758
|
+
"id": "ticket:config",
|
|
1759
|
+
"pluginAlias": "@hyperdrive.bot/gut",
|
|
1760
|
+
"pluginName": "@hyperdrive.bot/gut",
|
|
1761
|
+
"pluginType": "core",
|
|
1762
|
+
"strict": true,
|
|
1763
|
+
"isESM": true,
|
|
1764
|
+
"relativePath": [
|
|
1765
|
+
"dist",
|
|
1766
|
+
"commands",
|
|
1767
|
+
"ticket",
|
|
1768
|
+
"config.js"
|
|
1769
|
+
]
|
|
1770
|
+
},
|
|
1606
1771
|
"ticket:focus": {
|
|
1607
1772
|
"aliases": [],
|
|
1608
1773
|
"args": {
|
|
@@ -1867,16 +2032,17 @@
|
|
|
1867
2032
|
"aliases": [],
|
|
1868
2033
|
"args": {
|
|
1869
2034
|
"ticketId": {
|
|
1870
|
-
"description": "ticket ID to sync",
|
|
2035
|
+
"description": "ticket ID to sync (e.g., PROJ-1234)",
|
|
1871
2036
|
"name": "ticketId",
|
|
1872
2037
|
"required": true
|
|
1873
2038
|
}
|
|
1874
2039
|
},
|
|
1875
|
-
"description": "Sync ticket state with external source (JIRA, GitHub, etc.)",
|
|
2040
|
+
"description": "Sync ticket state with external source (JIRA, GitHub, etc.)\n\nFor pull direction:\n - If ticket exists in gut: updates from source\n - If ticket doesn't exist: creates it and queues enrichment\n\nFor push direction:\n - Updates source system with gut ticket state",
|
|
1876
2041
|
"examples": [
|
|
1877
2042
|
"<%= config.bin %> <%= command.id %> PROJ-1234",
|
|
1878
2043
|
"<%= config.bin %> <%= command.id %> PROJ-1234 --direction push",
|
|
1879
|
-
"<%= config.bin %> <%= command.id %> PROJ-1234 --direction pull"
|
|
2044
|
+
"<%= config.bin %> <%= command.id %> PROJ-1234 --direction pull",
|
|
2045
|
+
"<%= config.bin %> <%= command.id %> PROJ-1234 --direction pull --no-enrich"
|
|
1880
2046
|
],
|
|
1881
2047
|
"flags": {
|
|
1882
2048
|
"direction": {
|
|
@@ -1898,6 +2064,12 @@
|
|
|
1898
2064
|
"name": "json",
|
|
1899
2065
|
"allowNo": false,
|
|
1900
2066
|
"type": "boolean"
|
|
2067
|
+
},
|
|
2068
|
+
"no-enrich": {
|
|
2069
|
+
"description": "skip enrichment queue when pulling new tickets",
|
|
2070
|
+
"name": "no-enrich",
|
|
2071
|
+
"allowNo": false,
|
|
2072
|
+
"type": "boolean"
|
|
1901
2073
|
}
|
|
1902
2074
|
},
|
|
1903
2075
|
"hasDynamicHelp": false,
|
|
@@ -2000,7 +2172,60 @@
|
|
|
2000
2172
|
"ticket",
|
|
2001
2173
|
"update.js"
|
|
2002
2174
|
]
|
|
2175
|
+
},
|
|
2176
|
+
"worktree:create": {
|
|
2177
|
+
"aliases": [],
|
|
2178
|
+
"args": {
|
|
2179
|
+
"name": {
|
|
2180
|
+
"description": "Branch name for the worktree",
|
|
2181
|
+
"name": "name",
|
|
2182
|
+
"required": true
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
"description": "Create mirrored worktrees for super-repo and focused entities",
|
|
2186
|
+
"examples": [
|
|
2187
|
+
"<%= config.bin %> worktree create workflow/deploy-batch",
|
|
2188
|
+
"<%= config.bin %> worktree create workflow/deploy-batch --from master --install",
|
|
2189
|
+
"<%= config.bin %> worktree create feature/story-42 --base-dir /home/user/worktrees"
|
|
2190
|
+
],
|
|
2191
|
+
"flags": {
|
|
2192
|
+
"base-dir": {
|
|
2193
|
+
"description": "Root directory for worktrees",
|
|
2194
|
+
"name": "base-dir",
|
|
2195
|
+
"default": "/tmp/gut-worktrees",
|
|
2196
|
+
"hasDynamicHelp": false,
|
|
2197
|
+
"multiple": false,
|
|
2198
|
+
"type": "option"
|
|
2199
|
+
},
|
|
2200
|
+
"from": {
|
|
2201
|
+
"description": "Base branch to create from (defaults to current branch)",
|
|
2202
|
+
"name": "from",
|
|
2203
|
+
"hasDynamicHelp": false,
|
|
2204
|
+
"multiple": false,
|
|
2205
|
+
"type": "option"
|
|
2206
|
+
},
|
|
2207
|
+
"install": {
|
|
2208
|
+
"description": "Run pnpm install after creation",
|
|
2209
|
+
"name": "install",
|
|
2210
|
+
"allowNo": false,
|
|
2211
|
+
"type": "boolean"
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
"hasDynamicHelp": false,
|
|
2215
|
+
"hiddenAliases": [],
|
|
2216
|
+
"id": "worktree:create",
|
|
2217
|
+
"pluginAlias": "@hyperdrive.bot/gut",
|
|
2218
|
+
"pluginName": "@hyperdrive.bot/gut",
|
|
2219
|
+
"pluginType": "core",
|
|
2220
|
+
"strict": true,
|
|
2221
|
+
"isESM": true,
|
|
2222
|
+
"relativePath": [
|
|
2223
|
+
"dist",
|
|
2224
|
+
"commands",
|
|
2225
|
+
"worktree",
|
|
2226
|
+
"create.js"
|
|
2227
|
+
]
|
|
2003
2228
|
}
|
|
2004
2229
|
},
|
|
2005
|
-
"version": "0.1.
|
|
2230
|
+
"version": "0.1.12"
|
|
2006
2231
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperdrive.bot/gut",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Git Unified Tooling - Enhanced git with workspace intelligence for entity-based organization",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"/bin",
|
|
13
13
|
"/dist",
|
|
14
|
-
"/oclif.manifest.json"
|
|
14
|
+
"/oclif.manifest.json",
|
|
15
|
+
"/templates"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"build": "rm -rf dist && tsc -b",
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
"lint": "eslint .",
|
|
20
21
|
"postpack": "rm -f oclif.manifest.json",
|
|
21
22
|
"prepack": "npm run build && npx oclif manifest || true",
|
|
22
|
-
"test": "
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:unit": "vitest run",
|
|
23
25
|
"version": "npx oclif readme && git add README.md || true"
|
|
24
26
|
},
|
|
25
27
|
"keywords": [
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"@hyperdrive.bot/cli-auth": "^1.1.1",
|
|
41
|
+
"@hyperdrive.bot/plugin-telemetry": "file:../telemetry-plugin",
|
|
39
42
|
"@oclif/core": "^4.5.2",
|
|
40
43
|
"@oclif/plugin-help": "^6.2.32",
|
|
41
44
|
"axios": "^1.7.1",
|
|
@@ -56,14 +59,16 @@
|
|
|
56
59
|
"eslint-config-oclif-typescript": "^3.1.14",
|
|
57
60
|
"oclif": "^4.22.16",
|
|
58
61
|
"ts-node": "^10.9.2",
|
|
59
|
-
"typescript": "^5.9.2"
|
|
62
|
+
"typescript": "^5.9.2",
|
|
63
|
+
"vitest": "^4.1.0"
|
|
60
64
|
},
|
|
61
65
|
"oclif": {
|
|
62
66
|
"bin": "gut",
|
|
63
67
|
"dirname": "gut",
|
|
64
68
|
"commands": "./dist/commands",
|
|
65
69
|
"plugins": [
|
|
66
|
-
"@oclif/plugin-help"
|
|
70
|
+
"@oclif/plugin-help",
|
|
71
|
+
"@hyperdrive.bot/plugin-telemetry"
|
|
67
72
|
],
|
|
68
73
|
"topicSeparator": " ",
|
|
69
74
|
"topics": {
|
|
@@ -75,6 +80,12 @@
|
|
|
75
80
|
},
|
|
76
81
|
"ticket": {
|
|
77
82
|
"description": "Manage gut tickets from the ADHB system"
|
|
83
|
+
},
|
|
84
|
+
"claude": {
|
|
85
|
+
"description": "Claude Code integration"
|
|
86
|
+
},
|
|
87
|
+
"worktree": {
|
|
88
|
+
"description": "Manage entity-aware git worktrees for isolated pipeline runs"
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
},
|