github-issue-tower-defence-management 1.86.0 → 1.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.eslintrc.cjs +5 -1
  2. package/.github/workflows/console-ui.yml +47 -0
  3. package/.prettierignore +3 -0
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +42 -0
  6. package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
  7. package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
  8. package/bin/adapter/entry-points/console/ui-dist/index.html +13 -0
  9. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +54 -12
  10. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  11. package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js +67 -0
  12. package/bin/adapter/entry-points/handlers/inTmuxByHumanDataWriter.js.map +1 -0
  13. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +306 -0
  14. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  15. package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js +91 -0
  16. package/bin/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.js.map +1 -0
  17. package/package.json +22 -2
  18. package/scripts/copyConsoleUiDist.mjs +35 -0
  19. package/src/adapter/entry-points/console/ui/.storybook/main.ts +12 -0
  20. package/src/adapter/entry-points/console/ui/.storybook/preview.ts +15 -0
  21. package/src/adapter/entry-points/console/ui/biome.json +47 -0
  22. package/src/adapter/entry-points/console/ui/components.json +20 -0
  23. package/src/adapter/entry-points/console/ui/index.html +12 -0
  24. package/src/adapter/entry-points/console/ui/src/components/ui/badge.stories.tsx +35 -0
  25. package/src/adapter/entry-points/console/ui/src/components/ui/badge.tsx +28 -0
  26. package/src/adapter/entry-points/console/ui/src/components/ui/button.stories.tsx +34 -0
  27. package/src/adapter/entry-points/console/ui/src/components/ui/button.tsx +50 -0
  28. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +44 -0
  29. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +58 -0
  30. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +34 -0
  31. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +32 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +47 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +65 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.ts +64 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +19 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +69 -0
  37. package/src/adapter/entry-points/console/ui/src/index.css +31 -0
  38. package/src/adapter/entry-points/console/ui/src/lib/utils.ts +4 -0
  39. package/src/adapter/entry-points/console/ui/src/main.tsx +15 -0
  40. package/src/adapter/entry-points/console/ui/src/vite-env.d.ts +1 -0
  41. package/src/adapter/entry-points/console/ui/tsconfig.json +24 -0
  42. package/src/adapter/entry-points/console/ui/vite.config.ts +19 -0
  43. package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
  44. package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
  45. package/src/adapter/entry-points/console/ui-dist/index.html +13 -0
  46. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +26 -0
  47. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +266 -0
  48. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.ts +103 -0
  49. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +630 -0
  50. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +492 -0
  51. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +51 -0
  52. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +285 -0
  53. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.ts +182 -0
  54. package/tsconfig.build.json +7 -1
  55. package/tsconfig.json +6 -1
  56. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  57. package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts +16 -0
  58. package/types/adapter/entry-points/handlers/inTmuxByHumanDataWriter.d.ts.map +1 -0
  59. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +18 -1
  60. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  61. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +47 -0
  62. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  63. package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts +57 -0
  64. package/types/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.d.ts.map +1 -0
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>TDPM Console</title>
7
+ <script type="module" crossorigin src="./assets/index-DcOZ02ON.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-DFxrSRH4.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
@@ -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
+ };