github-issue-tower-defence-management 1.87.0 → 1.88.1
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/CHANGELOG.md +14 -0
- package/README.md +51 -1
- package/bin/adapter/entry-points/cli/index.js +35 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleDataDelivery.js +155 -0
- package/bin/adapter/entry-points/console/consoleDataDelivery.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleDoneStore.js +100 -0
- package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js +178 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleReadApi.js +119 -0
- package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleServer.js +147 -3
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +54 -12
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js +67 -0
- package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js.map +1 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js +91 -0
- package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +94 -0
- package/src/adapter/entry-points/cli/index.ts +61 -0
- package/src/adapter/entry-points/console/consoleDataDelivery.test.ts +184 -0
- package/src/adapter/entry-points/console/consoleDataDelivery.ts +169 -0
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +98 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +91 -0
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +444 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +280 -0
- package/src/adapter/entry-points/console/consoleReadApi.test.ts +297 -0
- package/src/adapter/entry-points/console/consoleReadApi.ts +192 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +269 -0
- package/src/adapter/entry-points/console/consoleServer.ts +228 -4
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +26 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +266 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts +103 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +34 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +3 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +285 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.ts +182 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleDataDelivery.d.ts +23 -0
- package/types/adapter/entry-points/console/consoleDataDelivery.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts +10 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +18 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleReadApi.d.ts +44 -0
- package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts +8 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts +16 -0
- package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts.map +1 -0
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts +57 -0
- package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts.map +1 -0
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
|
|
5
|
+
import { Project } from '../../../domain/entities/Project';
|
|
6
|
+
import {
|
|
7
|
+
buildConsoleDataResponse,
|
|
8
|
+
parseConsoleDataRoute,
|
|
9
|
+
} from './consoleDataDelivery';
|
|
10
|
+
import {
|
|
11
|
+
IssueTitleStateCache,
|
|
12
|
+
handleComments,
|
|
13
|
+
handleIssueTitle,
|
|
14
|
+
handleItemBody,
|
|
15
|
+
handlePrCommits,
|
|
16
|
+
handlePrFiles,
|
|
17
|
+
handleRelatedPrs,
|
|
18
|
+
} from './consoleReadApi';
|
|
19
|
+
import {
|
|
20
|
+
ConsoleOperationContext,
|
|
21
|
+
handleIntmux,
|
|
22
|
+
handleReview,
|
|
23
|
+
handleTriage,
|
|
24
|
+
} from './consoleOperationApi';
|
|
4
25
|
|
|
5
26
|
export const DEFAULT_CONSOLE_PORT = 9981;
|
|
6
27
|
|
|
@@ -108,6 +129,10 @@ export type ConsoleServerOptions = {
|
|
|
108
129
|
accessToken: string;
|
|
109
130
|
uiDistDir: string;
|
|
110
131
|
consoleDataOutputDir: string | null;
|
|
132
|
+
pjcode?: string | null;
|
|
133
|
+
issueRepository?: IssueRepository | null;
|
|
134
|
+
project?: Project | null;
|
|
135
|
+
issueTitleStateCache?: IssueTitleStateCache | null;
|
|
111
136
|
};
|
|
112
137
|
|
|
113
138
|
const sendNotFound = (response: http.ServerResponse): void => {
|
|
@@ -134,11 +159,189 @@ const serveBootstrapIndex = (response: http.ServerResponse): void => {
|
|
|
134
159
|
response.end(PLACEHOLDER_INDEX_HTML);
|
|
135
160
|
};
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
const sendJson = (
|
|
163
|
+
response: http.ServerResponse,
|
|
164
|
+
statusCode: number,
|
|
165
|
+
body: unknown,
|
|
166
|
+
): void => {
|
|
167
|
+
response.writeHead(statusCode, {
|
|
168
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
169
|
+
'Cache-Control': 'no-store',
|
|
170
|
+
});
|
|
171
|
+
response.end(JSON.stringify(body));
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const sendDataResponse = (
|
|
175
|
+
response: http.ServerResponse,
|
|
176
|
+
statusCode: number,
|
|
177
|
+
contentType: string,
|
|
178
|
+
body: string,
|
|
179
|
+
): void => {
|
|
180
|
+
response.writeHead(statusCode, {
|
|
181
|
+
'Content-Type': contentType,
|
|
182
|
+
'Cache-Control': 'no-store',
|
|
183
|
+
});
|
|
184
|
+
response.end(body);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const readRequestBody = (request: http.IncomingMessage): Promise<string> =>
|
|
188
|
+
new Promise((resolve, reject) => {
|
|
189
|
+
const chunks: Uint8Array[] = [];
|
|
190
|
+
request.on('data', (chunk: Uint8Array) => chunks.push(chunk));
|
|
191
|
+
request.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
192
|
+
request.on('error', reject);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
196
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
197
|
+
|
|
198
|
+
const parseRequestBody = (raw: string): Record<string, unknown> | null => {
|
|
199
|
+
if (raw.length === 0) {
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
202
|
+
let parsed: unknown;
|
|
203
|
+
try {
|
|
204
|
+
parsed = JSON.parse(raw);
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (!isRecord(parsed)) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return parsed;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const handleReadApi = async (
|
|
215
|
+
options: ConsoleServerOptions,
|
|
216
|
+
requestPath: string,
|
|
217
|
+
searchParams: URLSearchParams,
|
|
218
|
+
): Promise<{ statusCode: number; body: unknown } | null> => {
|
|
219
|
+
const issueRepository = options.issueRepository ?? null;
|
|
220
|
+
if (issueRepository === null) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const cache = options.issueTitleStateCache ?? null;
|
|
224
|
+
const url = searchParams.get('url');
|
|
225
|
+
switch (requestPath) {
|
|
226
|
+
case '/api/itembody':
|
|
227
|
+
return handleItemBody(issueRepository, url);
|
|
228
|
+
case '/api/comments':
|
|
229
|
+
return handleComments(issueRepository, url);
|
|
230
|
+
case '/api/prfiles':
|
|
231
|
+
return handlePrFiles(issueRepository, url);
|
|
232
|
+
case '/api/prcommits':
|
|
233
|
+
return handlePrCommits(issueRepository, url);
|
|
234
|
+
case '/api/relatedprs':
|
|
235
|
+
return handleRelatedPrs(issueRepository, url);
|
|
236
|
+
case '/api/issuetitle':
|
|
237
|
+
if (cache === null) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return handleIssueTitle(issueRepository, cache, url);
|
|
241
|
+
default:
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const handleOperationApi = async (
|
|
247
|
+
options: ConsoleServerOptions,
|
|
248
|
+
requestPath: string,
|
|
249
|
+
body: Record<string, unknown>,
|
|
250
|
+
): Promise<{ statusCode: number; body: unknown } | null> => {
|
|
251
|
+
const issueRepository = options.issueRepository ?? null;
|
|
252
|
+
const project = options.project ?? null;
|
|
253
|
+
if (issueRepository === null || project === null) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const context: ConsoleOperationContext = {
|
|
257
|
+
issueRepository,
|
|
258
|
+
project,
|
|
259
|
+
consoleDataOutputDir: options.consoleDataOutputDir,
|
|
260
|
+
pjcode: options.pjcode ?? null,
|
|
261
|
+
};
|
|
262
|
+
switch (requestPath) {
|
|
263
|
+
case '/api/review':
|
|
264
|
+
return handleReview(context, body);
|
|
265
|
+
case '/api/triage':
|
|
266
|
+
return handleTriage(context, body);
|
|
267
|
+
case '/api/intmux':
|
|
268
|
+
return handleIntmux(context, body);
|
|
269
|
+
default:
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const handleTokenedRequest = async (
|
|
138
275
|
options: ConsoleServerOptions,
|
|
139
276
|
request: http.IncomingMessage,
|
|
140
277
|
response: http.ServerResponse,
|
|
141
|
-
|
|
278
|
+
requestPath: string,
|
|
279
|
+
searchParams: URLSearchParams,
|
|
280
|
+
): Promise<void> => {
|
|
281
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
282
|
+
|
|
283
|
+
if (requestPath.startsWith('/api/')) {
|
|
284
|
+
if (method === 'GET') {
|
|
285
|
+
const readResult = await handleReadApi(
|
|
286
|
+
options,
|
|
287
|
+
requestPath,
|
|
288
|
+
searchParams,
|
|
289
|
+
);
|
|
290
|
+
if (readResult === null) {
|
|
291
|
+
sendNotFound(response);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
sendJson(response, readResult.statusCode, readResult.body);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (method === 'POST') {
|
|
298
|
+
const raw = await readRequestBody(request);
|
|
299
|
+
const parsedBody = parseRequestBody(raw);
|
|
300
|
+
if (parsedBody === null) {
|
|
301
|
+
sendJson(response, 400, { error: 'invalid JSON body' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const operationResult = await handleOperationApi(
|
|
305
|
+
options,
|
|
306
|
+
requestPath,
|
|
307
|
+
parsedBody,
|
|
308
|
+
);
|
|
309
|
+
if (operationResult === null) {
|
|
310
|
+
sendNotFound(response);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
sendJson(response, operationResult.statusCode, operationResult.body);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
sendNotFound(response);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (method === 'GET') {
|
|
321
|
+
const dataRoute = parseConsoleDataRoute(requestPath);
|
|
322
|
+
if (dataRoute !== null && options.consoleDataOutputDir !== null) {
|
|
323
|
+
const dataResponse = buildConsoleDataResponse(
|
|
324
|
+
options.consoleDataOutputDir,
|
|
325
|
+
dataRoute,
|
|
326
|
+
);
|
|
327
|
+
sendDataResponse(
|
|
328
|
+
response,
|
|
329
|
+
dataResponse.statusCode,
|
|
330
|
+
dataResponse.contentType,
|
|
331
|
+
dataResponse.body,
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
sendNotFound(response);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export const handleConsoleRequest = async (
|
|
341
|
+
options: ConsoleServerOptions,
|
|
342
|
+
request: http.IncomingMessage,
|
|
343
|
+
response: http.ServerResponse,
|
|
344
|
+
): Promise<void> => {
|
|
142
345
|
const requestUrl = new URL(request.url ?? '/', 'http://localhost');
|
|
143
346
|
const requestPath = requestUrl.pathname;
|
|
144
347
|
|
|
@@ -156,7 +359,13 @@ export const handleConsoleRequest = (
|
|
|
156
359
|
sendUnauthorized(response);
|
|
157
360
|
return;
|
|
158
361
|
}
|
|
159
|
-
|
|
362
|
+
await handleTokenedRequest(
|
|
363
|
+
options,
|
|
364
|
+
request,
|
|
365
|
+
response,
|
|
366
|
+
requestPath,
|
|
367
|
+
requestUrl.searchParams,
|
|
368
|
+
);
|
|
160
369
|
return;
|
|
161
370
|
}
|
|
162
371
|
|
|
@@ -196,11 +405,26 @@ export const handleConsoleRequest = (
|
|
|
196
405
|
response.end(staticContent);
|
|
197
406
|
};
|
|
198
407
|
|
|
408
|
+
const sendInternalServerError = (response: http.ServerResponse): void => {
|
|
409
|
+
if (response.headersSent) {
|
|
410
|
+
response.end();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
response.writeHead(500, {
|
|
414
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
415
|
+
'Cache-Control': 'no-store',
|
|
416
|
+
});
|
|
417
|
+
response.end('Internal Server Error');
|
|
418
|
+
};
|
|
419
|
+
|
|
199
420
|
export const createConsoleServer = (
|
|
200
421
|
options: ConsoleServerOptions,
|
|
201
422
|
): http.Server =>
|
|
202
423
|
http.createServer((request, response) => {
|
|
203
|
-
handleConsoleRequest(options, request, response)
|
|
424
|
+
handleConsoleRequest(options, request, response).catch((error) => {
|
|
425
|
+
console.error('console request failed', error);
|
|
426
|
+
sendInternalServerError(response);
|
|
427
|
+
});
|
|
204
428
|
});
|
|
205
429
|
|
|
206
430
|
export type StartConsoleServerOptions = ConsoleServerOptions & {
|
|
@@ -3,6 +3,7 @@ import TYPIA from 'typia';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import { writeSituationFile } from './situationFileWriter';
|
|
5
5
|
import { writeConsoleLists } from './consoleListsWriter';
|
|
6
|
+
import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
|
|
6
7
|
import { writeRotationOrderFile } from './rotationOrderFileWriter';
|
|
7
8
|
import {
|
|
8
9
|
fetchProjectReadme,
|
|
@@ -68,6 +69,10 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
68
69
|
type inputType = Parameters<HandleScheduledEventUseCase['run']>[0] & {
|
|
69
70
|
claudeCodeOauthTokenListJsonPath?: string;
|
|
70
71
|
consoleDataOutputDir?: string;
|
|
72
|
+
inTmuxDataOutputDir?: string;
|
|
73
|
+
inTmuxConsoleBaseUrl?: string;
|
|
74
|
+
inTmuxConsoleToken?: string;
|
|
75
|
+
inTmuxProjectOrder?: string[];
|
|
71
76
|
credentials: {
|
|
72
77
|
manager: {
|
|
73
78
|
github: {
|
|
@@ -383,6 +388,27 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
383
388
|
}`,
|
|
384
389
|
);
|
|
385
390
|
}
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
writeInTmuxByHumanData({
|
|
394
|
+
inTmuxDataOutputDir: mergedInput.inTmuxDataOutputDir ?? null,
|
|
395
|
+
inTmuxConsoleBaseUrl: mergedInput.inTmuxConsoleBaseUrl ?? null,
|
|
396
|
+
inTmuxConsoleToken: mergedInput.inTmuxConsoleToken ?? null,
|
|
397
|
+
inTmuxProjectOrder: mergedInput.inTmuxProjectOrder ?? null,
|
|
398
|
+
pjcode: input.projectName,
|
|
399
|
+
assigneeLogin: input.manager,
|
|
400
|
+
org: input.org,
|
|
401
|
+
repo: input.workingReport.repo,
|
|
402
|
+
project: result.project,
|
|
403
|
+
issues: result.issues,
|
|
404
|
+
});
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(
|
|
407
|
+
`Failed to write in-tmux-by-human data: ${
|
|
408
|
+
error instanceof Error ? error.message : String(error)
|
|
409
|
+
}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
386
412
|
}
|
|
387
413
|
return result;
|
|
388
414
|
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Issue } from '../../../domain/entities/Issue';
|
|
5
|
+
import { FieldOption, Project } from '../../../domain/entities/Project';
|
|
6
|
+
import { writeInTmuxByHumanData } from './inTmuxByHumanDataWriter';
|
|
7
|
+
|
|
8
|
+
const ASSIGNEE = 'owner-login';
|
|
9
|
+
const CONSOLE_BASE_URL = 'https://console.example.test';
|
|
10
|
+
const CONSOLE_TOKEN = 'test-token-value';
|
|
11
|
+
|
|
12
|
+
const option = (
|
|
13
|
+
id: string,
|
|
14
|
+
name: string,
|
|
15
|
+
color: FieldOption['color'],
|
|
16
|
+
): FieldOption => ({ id, name, color, description: '' });
|
|
17
|
+
|
|
18
|
+
const project: Project = {
|
|
19
|
+
id: 'project-node-id',
|
|
20
|
+
url: 'https://github.com/orgs/demo/projects/1',
|
|
21
|
+
databaseId: 1,
|
|
22
|
+
name: 'demo',
|
|
23
|
+
status: {
|
|
24
|
+
name: 'Status',
|
|
25
|
+
fieldId: 'status-field',
|
|
26
|
+
statuses: [option('st-tmux', 'In Tmux by human', 'RED')],
|
|
27
|
+
},
|
|
28
|
+
nextActionDate: null,
|
|
29
|
+
nextActionHour: null,
|
|
30
|
+
story: {
|
|
31
|
+
name: 'story',
|
|
32
|
+
fieldId: 'story-field',
|
|
33
|
+
databaseId: 2,
|
|
34
|
+
stories: [option('s1', 'Story Alpha', 'BLUE')],
|
|
35
|
+
workflowManagementStory: { id: 'wm', name: 'workflow management' },
|
|
36
|
+
},
|
|
37
|
+
remainingEstimationMinutes: null,
|
|
38
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
39
|
+
completionDate50PercentConfidence: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const makeIssue = (overrides: Partial<Issue>): Issue => ({
|
|
43
|
+
nameWithOwner: 'demo/repo',
|
|
44
|
+
number: 1,
|
|
45
|
+
title: 'Issue 1',
|
|
46
|
+
state: 'OPEN',
|
|
47
|
+
status: 'In Tmux by human',
|
|
48
|
+
story: 'Story Alpha',
|
|
49
|
+
nextActionDate: null,
|
|
50
|
+
nextActionHour: null,
|
|
51
|
+
estimationMinutes: null,
|
|
52
|
+
dependedIssueUrls: [],
|
|
53
|
+
completionDate50PercentConfidence: null,
|
|
54
|
+
url: 'https://github.com/demo/repo/issues/1',
|
|
55
|
+
assignees: [ASSIGNEE],
|
|
56
|
+
labels: [],
|
|
57
|
+
org: 'demo',
|
|
58
|
+
repo: 'repo',
|
|
59
|
+
body: 'should never be written',
|
|
60
|
+
itemId: 'item-1',
|
|
61
|
+
isPr: false,
|
|
62
|
+
isInProgress: false,
|
|
63
|
+
isClosed: false,
|
|
64
|
+
createdAt: new Date('2026-06-13T08:18:45.000Z'),
|
|
65
|
+
author: 'someone',
|
|
66
|
+
...overrides,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const baseParams = (outDir: string) => ({
|
|
70
|
+
inTmuxDataOutputDir: outDir,
|
|
71
|
+
inTmuxConsoleBaseUrl: CONSOLE_BASE_URL,
|
|
72
|
+
inTmuxConsoleToken: CONSOLE_TOKEN,
|
|
73
|
+
inTmuxProjectOrder: ['demo'],
|
|
74
|
+
pjcode: 'demo',
|
|
75
|
+
assigneeLogin: ASSIGNEE,
|
|
76
|
+
org: 'demo-org',
|
|
77
|
+
repo: 'demo-repo',
|
|
78
|
+
project,
|
|
79
|
+
issues: [makeIssue({})],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const readJson = (filePath: string): unknown =>
|
|
83
|
+
JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
84
|
+
|
|
85
|
+
describe('writeInTmuxByHumanData', () => {
|
|
86
|
+
let outDir: string;
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'intmux-out-'));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
fs.rmSync(outDir, { recursive: true, force: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const file = (name: string): string => path.join(outDir, name);
|
|
97
|
+
|
|
98
|
+
it('writes the four per-project files and the four index files', () => {
|
|
99
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
100
|
+
for (const name of [
|
|
101
|
+
'demo.json',
|
|
102
|
+
'demo.v2.json',
|
|
103
|
+
'demo.v3.json',
|
|
104
|
+
'demo.v4.json',
|
|
105
|
+
'index.json',
|
|
106
|
+
'index.v2.json',
|
|
107
|
+
'index.v3.json',
|
|
108
|
+
'index.v4.json',
|
|
109
|
+
]) {
|
|
110
|
+
expect(fs.existsSync(file(name))).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('writes pretty-printed JSON ending with a trailing newline', () => {
|
|
115
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
116
|
+
const raw = fs.readFileSync(file('demo.json'), 'utf8');
|
|
117
|
+
expect(raw.endsWith('\n')).toBe(true);
|
|
118
|
+
expect(raw).toContain('\n ');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('writes the v1 shape with a flat url string array', () => {
|
|
122
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
123
|
+
expect(readJson(file('demo.json'))).toEqual([
|
|
124
|
+
{
|
|
125
|
+
story: 'Story Alpha',
|
|
126
|
+
urls: ['https://github.com/demo/repo/issues/1'],
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('writes the v2 shape with url and title objects', () => {
|
|
132
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
133
|
+
expect(readJson(file('demo.v2.json'))).toEqual([
|
|
134
|
+
{
|
|
135
|
+
story: 'Story Alpha',
|
|
136
|
+
urls: [
|
|
137
|
+
{ url: 'https://github.com/demo/repo/issues/1', title: 'Issue 1' },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('writes the v3 document with overviewUrl and a token-free console url', () => {
|
|
144
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
145
|
+
expect(readJson(file('demo.v3.json'))).toEqual({
|
|
146
|
+
version: 3,
|
|
147
|
+
overviewUrl: 'https://github.com/orgs/demo/projects/1',
|
|
148
|
+
tdpmConsoleUrl: 'https://console.example.test/projects/demo/prs',
|
|
149
|
+
groups: [
|
|
150
|
+
{
|
|
151
|
+
story: 'Story Alpha',
|
|
152
|
+
urls: [
|
|
153
|
+
{ url: 'https://github.com/demo/repo/issues/1', title: 'Issue 1' },
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('writes the v4 document with the token-bearing console url and derived new issue url', () => {
|
|
161
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
162
|
+
expect(readJson(file('demo.v4.json'))).toEqual({
|
|
163
|
+
version: 4,
|
|
164
|
+
overviewUrl: 'https://github.com/orgs/demo/projects/1',
|
|
165
|
+
tdpmConsoleUrl:
|
|
166
|
+
'https://console.example.test/projects/demo/prs?k=test-token-value',
|
|
167
|
+
newIssueUrl:
|
|
168
|
+
'https://github.com/demo-org/demo-repo/issues/new?assignees=owner-login',
|
|
169
|
+
groups: [
|
|
170
|
+
{
|
|
171
|
+
story: 'Story Alpha',
|
|
172
|
+
sessions: [
|
|
173
|
+
{
|
|
174
|
+
name: 'https://github.com/demo/repo/issues/1',
|
|
175
|
+
description: 'Issue 1',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('writes index files listing configured projects whose per-project file exists', () => {
|
|
184
|
+
fs.writeFileSync(file('xmile.json'), '[]\n');
|
|
185
|
+
writeInTmuxByHumanData({
|
|
186
|
+
...baseParams(outDir),
|
|
187
|
+
inTmuxProjectOrder: ['demo', 'xmile', 'xcare'],
|
|
188
|
+
});
|
|
189
|
+
expect(readJson(file('index.json'))).toEqual({
|
|
190
|
+
projects: ['demo', 'xmile'],
|
|
191
|
+
});
|
|
192
|
+
expect(readJson(file('index.v2.json'))).toEqual({
|
|
193
|
+
version: 2,
|
|
194
|
+
projects: ['demo', 'xmile'],
|
|
195
|
+
});
|
|
196
|
+
expect(readJson(file('index.v3.json'))).toEqual({
|
|
197
|
+
version: 3,
|
|
198
|
+
projects: ['demo', 'xmile'],
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('builds the v4 index path from the output dir basename and the token', () => {
|
|
203
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
204
|
+
const basename = path.basename(outDir);
|
|
205
|
+
expect(readJson(file('index.v4.json'))).toEqual({
|
|
206
|
+
version: 4,
|
|
207
|
+
projects: [
|
|
208
|
+
{ name: 'demo', path: `/${basename}/demo.v4.json?k=test-token-value` },
|
|
209
|
+
],
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('skips the v4 file and the v4 index when the token is unset but still writes v1, v2, v3 and v1-v3 index', () => {
|
|
214
|
+
writeInTmuxByHumanData({
|
|
215
|
+
...baseParams(outDir),
|
|
216
|
+
inTmuxConsoleToken: undefined,
|
|
217
|
+
});
|
|
218
|
+
expect(fs.existsSync(file('demo.json'))).toBe(true);
|
|
219
|
+
expect(fs.existsSync(file('demo.v2.json'))).toBe(true);
|
|
220
|
+
expect(fs.existsSync(file('demo.v3.json'))).toBe(true);
|
|
221
|
+
expect(fs.existsSync(file('demo.v4.json'))).toBe(false);
|
|
222
|
+
expect(fs.existsSync(file('index.json'))).toBe(true);
|
|
223
|
+
expect(fs.existsSync(file('index.v3.json'))).toBe(true);
|
|
224
|
+
expect(fs.existsSync(file('index.v4.json'))).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('skips the v3 and v4 files when the console base url is unset', () => {
|
|
228
|
+
writeInTmuxByHumanData({
|
|
229
|
+
...baseParams(outDir),
|
|
230
|
+
inTmuxConsoleBaseUrl: undefined,
|
|
231
|
+
});
|
|
232
|
+
expect(fs.existsSync(file('demo.json'))).toBe(true);
|
|
233
|
+
expect(fs.existsSync(file('demo.v2.json'))).toBe(true);
|
|
234
|
+
expect(fs.existsSync(file('demo.v3.json'))).toBe(false);
|
|
235
|
+
expect(fs.existsSync(file('demo.v4.json'))).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('skips the index files when the project order is empty', () => {
|
|
239
|
+
writeInTmuxByHumanData({ ...baseParams(outDir), inTmuxProjectOrder: [] });
|
|
240
|
+
expect(fs.existsSync(file('demo.json'))).toBe(true);
|
|
241
|
+
expect(fs.existsSync(file('index.json'))).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not leave a temp file behind after writing', () => {
|
|
245
|
+
writeInTmuxByHumanData(baseParams(outDir));
|
|
246
|
+
expect(fs.existsSync(`${file('demo.json')}.tmp`)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('is a no-op when the output dir is unset', () => {
|
|
250
|
+
writeInTmuxByHumanData({
|
|
251
|
+
...baseParams(outDir),
|
|
252
|
+
inTmuxDataOutputDir: undefined,
|
|
253
|
+
});
|
|
254
|
+
expect(fs.readdirSync(outDir)).toHaveLength(0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('is a no-op when pjcode is unset', () => {
|
|
258
|
+
writeInTmuxByHumanData({ ...baseParams(outDir), pjcode: '' });
|
|
259
|
+
expect(fs.readdirSync(outDir)).toHaveLength(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('is a no-op when the assignee login is unset', () => {
|
|
263
|
+
writeInTmuxByHumanData({ ...baseParams(outDir), assigneeLogin: null });
|
|
264
|
+
expect(fs.readdirSync(outDir)).toHaveLength(0);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { Issue } from '../../../domain/entities/Issue';
|
|
4
|
+
import type { Project } from '../../../domain/entities/Project';
|
|
5
|
+
import {
|
|
6
|
+
GenerateInTmuxByHumanDataUseCase,
|
|
7
|
+
InTmuxByHumanData,
|
|
8
|
+
} from '../../../domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase';
|
|
9
|
+
|
|
10
|
+
export type InTmuxByHumanDataWriterParams = {
|
|
11
|
+
inTmuxDataOutputDir: string | null | undefined;
|
|
12
|
+
inTmuxConsoleBaseUrl: string | null | undefined;
|
|
13
|
+
inTmuxConsoleToken: string | null | undefined;
|
|
14
|
+
inTmuxProjectOrder: string[] | null | undefined;
|
|
15
|
+
pjcode: string | null | undefined;
|
|
16
|
+
assigneeLogin: string | null | undefined;
|
|
17
|
+
org: string;
|
|
18
|
+
repo: string;
|
|
19
|
+
project: Project;
|
|
20
|
+
issues: Issue[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const writeJsonAtomic = (filePath: string, data: unknown): void => {
|
|
24
|
+
const dir = path.dirname(filePath);
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
const tmpPath = `${filePath}.tmp`;
|
|
27
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`);
|
|
28
|
+
fs.renameSync(tmpPath, filePath);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const writeInTmuxByHumanData = (
|
|
32
|
+
params: InTmuxByHumanDataWriterParams,
|
|
33
|
+
): void => {
|
|
34
|
+
const {
|
|
35
|
+
inTmuxDataOutputDir,
|
|
36
|
+
inTmuxConsoleBaseUrl,
|
|
37
|
+
inTmuxConsoleToken,
|
|
38
|
+
inTmuxProjectOrder,
|
|
39
|
+
pjcode,
|
|
40
|
+
assigneeLogin,
|
|
41
|
+
org,
|
|
42
|
+
repo,
|
|
43
|
+
project,
|
|
44
|
+
issues,
|
|
45
|
+
} = params;
|
|
46
|
+
if (!inTmuxDataOutputDir || !pjcode || !assigneeLogin) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data: InTmuxByHumanData = new GenerateInTmuxByHumanDataUseCase().run({
|
|
51
|
+
project,
|
|
52
|
+
issues,
|
|
53
|
+
pjcode,
|
|
54
|
+
assigneeLogin,
|
|
55
|
+
org,
|
|
56
|
+
repo,
|
|
57
|
+
consoleBaseUrl: inTmuxConsoleBaseUrl ?? null,
|
|
58
|
+
consoleToken: inTmuxConsoleToken ?? null,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
writeJsonAtomic(path.join(inTmuxDataOutputDir, `${pjcode}.json`), data.v1);
|
|
62
|
+
writeJsonAtomic(path.join(inTmuxDataOutputDir, `${pjcode}.v2.json`), data.v2);
|
|
63
|
+
if (data.v3) {
|
|
64
|
+
writeJsonAtomic(
|
|
65
|
+
path.join(inTmuxDataOutputDir, `${pjcode}.v3.json`),
|
|
66
|
+
data.v3,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (data.v4) {
|
|
70
|
+
writeJsonAtomic(
|
|
71
|
+
path.join(inTmuxDataOutputDir, `${pjcode}.v4.json`),
|
|
72
|
+
data.v4,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!inTmuxProjectOrder || inTmuxProjectOrder.length === 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const presentProjects = inTmuxProjectOrder.filter((name) =>
|
|
80
|
+
fs.existsSync(path.join(inTmuxDataOutputDir, `${name}.json`)),
|
|
81
|
+
);
|
|
82
|
+
writeJsonAtomic(path.join(inTmuxDataOutputDir, 'index.json'), {
|
|
83
|
+
projects: presentProjects,
|
|
84
|
+
});
|
|
85
|
+
writeJsonAtomic(path.join(inTmuxDataOutputDir, 'index.v2.json'), {
|
|
86
|
+
version: 2,
|
|
87
|
+
projects: presentProjects,
|
|
88
|
+
});
|
|
89
|
+
writeJsonAtomic(path.join(inTmuxDataOutputDir, 'index.v3.json'), {
|
|
90
|
+
version: 3,
|
|
91
|
+
projects: presentProjects,
|
|
92
|
+
});
|
|
93
|
+
if (inTmuxConsoleToken) {
|
|
94
|
+
const outputDirBasename = path.basename(inTmuxDataOutputDir);
|
|
95
|
+
writeJsonAtomic(path.join(inTmuxDataOutputDir, 'index.v4.json'), {
|
|
96
|
+
version: 4,
|
|
97
|
+
projects: presentProjects.map((name) => ({
|
|
98
|
+
name,
|
|
99
|
+
path: `/${outputDirBasename}/${name}.v4.json?k=${inTmuxConsoleToken}`,
|
|
100
|
+
})),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
};
|