github-issue-tower-defence-management 1.152.3 → 1.153.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 (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +16 -1
  3. package/bin/adapter/entry-points/cli/index.js +36 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/webServer.js +59 -23
  6. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +11 -0
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js +20 -0
  10. package/bin/adapter/entry-points/handlers/ownerCallFileCleaner.js.map +1 -0
  11. package/bin/adapter/entry-points/handlers/ownerCallFileStore.js +80 -0
  12. package/bin/adapter/entry-points/handlers/ownerCallFileStore.js.map +1 -0
  13. package/bin/domain/usecases/intmux/OwnerCallFile.js +52 -0
  14. package/bin/domain/usecases/intmux/OwnerCallFile.js.map +1 -0
  15. package/package.json +1 -1
  16. package/src/adapter/entry-points/cli/index.test.ts +189 -0
  17. package/src/adapter/entry-points/cli/index.ts +75 -0
  18. package/src/adapter/entry-points/console/webServer.test.ts +317 -0
  19. package/src/adapter/entry-points/console/webServer.ts +86 -26
  20. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +35 -0
  21. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +15 -0
  22. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +109 -0
  23. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.ts +25 -0
  24. package/src/adapter/entry-points/handlers/ownerCallFileStore.test.ts +286 -0
  25. package/src/adapter/entry-points/handlers/ownerCallFileStore.ts +145 -0
  26. package/src/domain/usecases/intmux/OwnerCallFile.test.ts +184 -0
  27. package/src/domain/usecases/intmux/OwnerCallFile.ts +80 -0
  28. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  29. package/types/adapter/entry-points/console/webServer.d.ts +1 -0
  30. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  31. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  32. package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts +8 -0
  33. package/types/adapter/entry-points/handlers/ownerCallFileCleaner.d.ts.map +1 -0
  34. package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts +21 -0
  35. package/types/adapter/entry-points/handlers/ownerCallFileStore.d.ts.map +1 -0
  36. package/types/domain/usecases/intmux/OwnerCallFile.d.ts +18 -0
  37. package/types/domain/usecases/intmux/OwnerCallFile.d.ts.map +1 -0
@@ -0,0 +1,109 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { Issue } from '../../../domain/entities/Issue';
5
+ import { toTmuxSessionName } from '../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase';
6
+ import { cleanClosedIssueOwnerCallFiles } from './ownerCallFileCleaner';
7
+ import { ownerCallFileAppend, ownerCallFilePath } from './ownerCallFileStore';
8
+
9
+ const makeIssue = (overrides: Partial<Issue> = {}): Issue => ({
10
+ nameWithOwner: 'demo/repo',
11
+ number: 1,
12
+ title: 'Issue 1',
13
+ state: 'OPEN',
14
+ status: 'In Tmux by human',
15
+ story: null,
16
+ nextActionDate: null,
17
+ nextActionHour: null,
18
+ estimationMinutes: null,
19
+ dependedIssueUrls: [],
20
+ completionDate50PercentConfidence: null,
21
+ url: 'https://github.com/demo/repo/issues/1',
22
+ assignees: ['owner-login'],
23
+ labels: [],
24
+ org: 'demo',
25
+ repo: 'repo',
26
+ body: '',
27
+ itemId: 'item-1',
28
+ isPr: false,
29
+ isInProgress: false,
30
+ isClosed: false,
31
+ createdAt: new Date('2026-08-01T00:00:00.000Z'),
32
+ author: '',
33
+ closingIssueReferenceUrls: [],
34
+ ...overrides,
35
+ });
36
+
37
+ describe('cleanClosedIssueOwnerCallFiles', () => {
38
+ let dataDir = '';
39
+
40
+ beforeEach(() => {
41
+ dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owner-call-cleaner-'));
42
+ });
43
+
44
+ afterEach(() => {
45
+ fs.rmSync(dataDir, { recursive: true, force: true });
46
+ });
47
+
48
+ const appendCallFor = (issue: Issue): string => {
49
+ const sessionName = toTmuxSessionName(issue.url);
50
+ ownerCallFileAppend({
51
+ dataDir,
52
+ projectCode: 'umino',
53
+ ownerCall: {
54
+ sessionName,
55
+ calledAt: '2026-08-14T04:22:28Z',
56
+ body: 'an unanswered call\n',
57
+ },
58
+ });
59
+ return ownerCallFilePath(dataDir, 'umino', sessionName);
60
+ };
61
+
62
+ it('deletes the owner call file of a session whose issue is closed', () => {
63
+ const closedIssue = makeIssue({
64
+ url: 'https://github.com/demo/repo/issues/7',
65
+ state: 'CLOSED',
66
+ isClosed: true,
67
+ });
68
+ const filePath = appendCallFor(closedIssue);
69
+
70
+ cleanClosedIssueOwnerCallFiles({
71
+ inTmuxDataOutputDir: dataDir,
72
+ pjcode: 'umino',
73
+ issues: [closedIssue],
74
+ });
75
+
76
+ expect(fs.existsSync(filePath)).toBe(false);
77
+ });
78
+
79
+ it('keeps the owner call file of a session whose issue is still open', () => {
80
+ const openIssue = makeIssue();
81
+ const filePath = appendCallFor(openIssue);
82
+
83
+ cleanClosedIssueOwnerCallFiles({
84
+ inTmuxDataOutputDir: dataDir,
85
+ pjcode: 'umino',
86
+ issues: [openIssue],
87
+ });
88
+
89
+ expect(fs.existsSync(filePath)).toBe(true);
90
+ });
91
+
92
+ it('does nothing when the data directory or the project code is not configured', () => {
93
+ const closedIssue = makeIssue({ isClosed: true, state: 'CLOSED' });
94
+ const filePath = appendCallFor(closedIssue);
95
+
96
+ cleanClosedIssueOwnerCallFiles({
97
+ inTmuxDataOutputDir: null,
98
+ pjcode: 'umino',
99
+ issues: [closedIssue],
100
+ });
101
+ cleanClosedIssueOwnerCallFiles({
102
+ inTmuxDataOutputDir: dataDir,
103
+ pjcode: null,
104
+ issues: [closedIssue],
105
+ });
106
+
107
+ expect(fs.existsSync(filePath)).toBe(true);
108
+ });
109
+ });
@@ -0,0 +1,25 @@
1
+ import { Issue } from '../../../domain/entities/Issue';
2
+ import { toTmuxSessionName } from '../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase';
3
+ import { ownerCallFileDelete } from './ownerCallFileStore';
4
+
5
+ export type CleanClosedIssueOwnerCallFilesParams = {
6
+ inTmuxDataOutputDir: string | null | undefined;
7
+ pjcode: string | null | undefined;
8
+ issues: Issue[];
9
+ };
10
+
11
+ export const cleanClosedIssueOwnerCallFiles = (
12
+ params: CleanClosedIssueOwnerCallFilesParams,
13
+ ): void => {
14
+ const { inTmuxDataOutputDir, pjcode, issues } = params;
15
+ if (!inTmuxDataOutputDir || !pjcode) {
16
+ return;
17
+ }
18
+ for (const issue of issues.filter((candidate) => candidate.isClosed)) {
19
+ ownerCallFileDelete({
20
+ dataDir: inTmuxDataOutputDir,
21
+ projectCode: pjcode,
22
+ sessionName: toTmuxSessionName(issue.url),
23
+ });
24
+ }
25
+ };
@@ -0,0 +1,286 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { parseAllDocuments } from 'yaml';
5
+ import {
6
+ ownerCallFileAppend,
7
+ ownerCallFileDelete,
8
+ ownerCallFileDeleteInEveryProject,
9
+ ownerCallFilePath,
10
+ ownerCallProjectCodeInInTmuxByHumanData,
11
+ } from './ownerCallFileStore';
12
+ import { ownerCallFileRelativePath } from '../../../domain/usecases/intmux/OwnerCallFile';
13
+ import { toTmuxSessionName } from '../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase';
14
+
15
+ const toPlainValue = (document: { toJS: () => unknown }): unknown =>
16
+ document.toJS();
17
+
18
+ describe('ownerCallFileStore', () => {
19
+ let dataDir = '';
20
+
21
+ beforeEach(() => {
22
+ dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owner-call-store-'));
23
+ });
24
+
25
+ afterEach(() => {
26
+ fs.rmSync(dataDir, { recursive: true, force: true });
27
+ });
28
+
29
+ const sessionName = 'https_//github_com/OWNER/REPO/issues/1';
30
+
31
+ it('creates the file and its project directory on the first append', () => {
32
+ ownerCallFileAppend({
33
+ dataDir,
34
+ projectCode: 'umino',
35
+ ownerCall: {
36
+ sessionName,
37
+ calledAt: '2026-08-14T04:22:28Z',
38
+ body: 'the only call\n',
39
+ },
40
+ });
41
+
42
+ const filePath = path.join(
43
+ dataDir,
44
+ ownerCallFileRelativePath('umino', sessionName),
45
+ );
46
+ expect(fs.existsSync(filePath)).toBe(true);
47
+ const documents = parseAllDocuments(fs.readFileSync(filePath, 'utf-8'));
48
+ expect(documents).toHaveLength(1);
49
+ expect(documents[0].toJS()).toEqual({
50
+ sessionName,
51
+ calledAt: '2026-08-14T04:22:28Z',
52
+ body: 'the only call\n',
53
+ });
54
+ });
55
+
56
+ it('appends a second document after the first one, oldest first', () => {
57
+ ownerCallFileAppend({
58
+ dataDir,
59
+ projectCode: 'umino',
60
+ ownerCall: {
61
+ sessionName,
62
+ calledAt: '2026-08-14T04:22:28Z',
63
+ body: 'the older call\n',
64
+ },
65
+ });
66
+ ownerCallFileAppend({
67
+ dataDir,
68
+ projectCode: 'umino',
69
+ ownerCall: {
70
+ sessionName,
71
+ calledAt: '2026-08-14T05:00:00Z',
72
+ body: 'the newer call\n',
73
+ },
74
+ });
75
+
76
+ const documents = parseAllDocuments(
77
+ fs.readFileSync(
78
+ ownerCallFilePath(dataDir, 'umino', sessionName),
79
+ 'utf-8',
80
+ ),
81
+ );
82
+ expect(documents.map(toPlainValue)).toEqual([
83
+ {
84
+ sessionName,
85
+ calledAt: '2026-08-14T04:22:28Z',
86
+ body: 'the older call\n',
87
+ },
88
+ {
89
+ sessionName,
90
+ calledAt: '2026-08-14T05:00:00Z',
91
+ body: 'the newer call\n',
92
+ },
93
+ ]);
94
+ });
95
+
96
+ it('writes the file of a session that belongs to no project under NA', () => {
97
+ ownerCallFileAppend({
98
+ dataDir,
99
+ projectCode: null,
100
+ ownerCall: {
101
+ sessionName: 'secretary',
102
+ calledAt: '2026-08-14T04:22:28Z',
103
+ body: 'a call from a long running session\n',
104
+ },
105
+ });
106
+
107
+ expect(
108
+ fs.existsSync(
109
+ path.join(dataDir, ownerCallFileRelativePath(null, 'secretary')),
110
+ ),
111
+ ).toBe(true);
112
+ });
113
+
114
+ it('removes the file of the named project', () => {
115
+ ownerCallFileAppend({
116
+ dataDir,
117
+ projectCode: 'umino',
118
+ ownerCall: {
119
+ sessionName,
120
+ calledAt: '2026-08-14T04:22:28Z',
121
+ body: 'the only call\n',
122
+ },
123
+ });
124
+
125
+ ownerCallFileDelete({ dataDir, projectCode: 'umino', sessionName });
126
+
127
+ expect(
128
+ fs.existsSync(ownerCallFilePath(dataDir, 'umino', sessionName)),
129
+ ).toBe(false);
130
+ });
131
+
132
+ it('succeeds when the file to delete is already absent', () => {
133
+ expect(() =>
134
+ ownerCallFileDelete({ dataDir, projectCode: 'umino', sessionName }),
135
+ ).not.toThrow();
136
+ });
137
+
138
+ it('removes the file of a session whose project code the caller does not know', () => {
139
+ ownerCallFileAppend({
140
+ dataDir,
141
+ projectCode: 'umino',
142
+ ownerCall: {
143
+ sessionName,
144
+ calledAt: '2026-08-14T04:22:28Z',
145
+ body: 'the only call\n',
146
+ },
147
+ });
148
+ ownerCallFileAppend({
149
+ dataDir,
150
+ projectCode: 'other',
151
+ ownerCall: {
152
+ sessionName: 'other_session',
153
+ calledAt: '2026-08-14T04:22:28Z',
154
+ body: 'a call of another session\n',
155
+ },
156
+ });
157
+
158
+ ownerCallFileDeleteInEveryProject({ dataDir, sessionName });
159
+
160
+ expect(
161
+ fs.existsSync(ownerCallFilePath(dataDir, 'umino', sessionName)),
162
+ ).toBe(false);
163
+ expect(
164
+ fs.existsSync(ownerCallFilePath(dataDir, 'other', 'other_session')),
165
+ ).toBe(true);
166
+ });
167
+
168
+ it('succeeds when no owner call directory exists at all', () => {
169
+ expect(() =>
170
+ ownerCallFileDeleteInEveryProject({ dataDir, sessionName }),
171
+ ).not.toThrow();
172
+ });
173
+ });
174
+
175
+ describe('ownerCallProjectCodeInInTmuxByHumanData', () => {
176
+ const issueUrl = 'https://github.com/OWNER/REPO/issues/1';
177
+ const otherIssueUrl = 'https://github.com/OWNER/REPO/issues/2';
178
+ let dataDir = '';
179
+
180
+ beforeEach(() => {
181
+ dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'owner-call-project-'));
182
+ });
183
+
184
+ afterEach(() => {
185
+ fs.rmSync(dataDir, { recursive: true, force: true });
186
+ });
187
+
188
+ const writeProjectData = (projectCode: string, urls: string[]): void => {
189
+ fs.writeFileSync(
190
+ path.join(dataDir, `${projectCode}.v4.json`),
191
+ `${JSON.stringify({
192
+ version: 4,
193
+ overviewUrl: 'https://github.com/orgs/OWNER/projects/1',
194
+ tdpmConsoleUrl: 'http://localhost/projects/code',
195
+ newIssueUrl: 'https://github.com/OWNER/REPO/issues/new',
196
+ groups: [
197
+ {
198
+ story: 'a story',
199
+ sessions: urls.map((url) => ({ name: url, description: 'title' })),
200
+ },
201
+ ],
202
+ })}\n`,
203
+ );
204
+ };
205
+
206
+ it('gives the code of the project whose session list holds the session', () => {
207
+ writeProjectData('other', [otherIssueUrl]);
208
+ writeProjectData('umino', [issueUrl]);
209
+
210
+ expect(
211
+ ownerCallProjectCodeInInTmuxByHumanData(
212
+ dataDir,
213
+ toTmuxSessionName(issueUrl),
214
+ ),
215
+ ).toBe('umino');
216
+ });
217
+
218
+ it('gives null when no project lists the session', () => {
219
+ writeProjectData('umino', [otherIssueUrl]);
220
+
221
+ expect(
222
+ ownerCallProjectCodeInInTmuxByHumanData(
223
+ dataDir,
224
+ toTmuxSessionName(issueUrl),
225
+ ),
226
+ ).toBeNull();
227
+ });
228
+
229
+ it('gives null when the directory holds no in-tmux-by-human data at all', () => {
230
+ expect(
231
+ ownerCallProjectCodeInInTmuxByHumanData(
232
+ dataDir,
233
+ toTmuxSessionName(issueUrl),
234
+ ),
235
+ ).toBeNull();
236
+ });
237
+
238
+ it('gives null when the directory itself is absent', () => {
239
+ expect(
240
+ ownerCallProjectCodeInInTmuxByHumanData(
241
+ path.join(dataDir, 'absent'),
242
+ toTmuxSessionName(issueUrl),
243
+ ),
244
+ ).toBeNull();
245
+ });
246
+
247
+ it('reads neither the project index nor the older versions of the project data', () => {
248
+ fs.writeFileSync(
249
+ path.join(dataDir, 'index.v4.json'),
250
+ `${JSON.stringify({
251
+ version: 4,
252
+ projects: [{ name: 'umino', path: '/in-tmux-by-human/umino.v4.json' }],
253
+ })}\n`,
254
+ );
255
+ fs.writeFileSync(
256
+ path.join(dataDir, 'umino.v3.json'),
257
+ `${JSON.stringify({
258
+ version: 3,
259
+ groups: [{ story: 'a story', urls: [{ url: issueUrl, title: 't' }] }],
260
+ })}\n`,
261
+ );
262
+
263
+ expect(
264
+ ownerCallProjectCodeInInTmuxByHumanData(
265
+ dataDir,
266
+ toTmuxSessionName(issueUrl),
267
+ ),
268
+ ).toBeNull();
269
+ });
270
+
271
+ it('skips a project data file that cannot be read as the expected shape', () => {
272
+ fs.writeFileSync(path.join(dataDir, 'broken.v4.json'), 'not json at all\n');
273
+ fs.writeFileSync(
274
+ path.join(dataDir, 'shapeless.v4.json'),
275
+ `${JSON.stringify({ version: 4, groups: 'not a list' })}\n`,
276
+ );
277
+ writeProjectData('umino', [issueUrl]);
278
+
279
+ expect(
280
+ ownerCallProjectCodeInInTmuxByHumanData(
281
+ dataDir,
282
+ toTmuxSessionName(issueUrl),
283
+ ),
284
+ ).toBe('umino');
285
+ });
286
+ });
@@ -0,0 +1,145 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import {
4
+ OWNER_CALL_FILE_DIRECTORY_NAME,
5
+ OwnerCall,
6
+ OwnerCallProjectSessionNames,
7
+ ownerCallFileRelativePath,
8
+ ownerCallProjectCodeOfSession,
9
+ ownerCallYamlDocument,
10
+ } from '../../../domain/usecases/intmux/OwnerCallFile';
11
+
12
+ export type OwnerCallFileAppendParams = {
13
+ dataDir: string;
14
+ projectCode: string | null;
15
+ ownerCall: OwnerCall;
16
+ };
17
+
18
+ export type OwnerCallFileDeleteParams = {
19
+ dataDir: string;
20
+ projectCode: string | null;
21
+ sessionName: string;
22
+ };
23
+
24
+ export type OwnerCallFileDeleteInEveryProjectParams = {
25
+ dataDir: string;
26
+ sessionName: string;
27
+ };
28
+
29
+ export const ownerCallFilePath = (
30
+ dataDir: string,
31
+ projectCode: string | null,
32
+ sessionName: string,
33
+ ): string =>
34
+ path.join(dataDir, ownerCallFileRelativePath(projectCode, sessionName));
35
+
36
+ export const ownerCallFileAppend = (
37
+ params: OwnerCallFileAppendParams,
38
+ ): void => {
39
+ const filePath = ownerCallFilePath(
40
+ params.dataDir,
41
+ params.projectCode,
42
+ params.ownerCall.sessionName,
43
+ );
44
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
45
+ fs.appendFileSync(filePath, ownerCallYamlDocument(params.ownerCall));
46
+ };
47
+
48
+ export const ownerCallFileDelete = (
49
+ params: OwnerCallFileDeleteParams,
50
+ ): void => {
51
+ fs.rmSync(
52
+ ownerCallFilePath(params.dataDir, params.projectCode, params.sessionName),
53
+ { force: true },
54
+ );
55
+ };
56
+
57
+ const projectCodeDirectoryNames = (dataDir: string): string[] => {
58
+ const ownerCallDirectory = path.join(dataDir, OWNER_CALL_FILE_DIRECTORY_NAME);
59
+ if (!fs.existsSync(ownerCallDirectory)) {
60
+ return [];
61
+ }
62
+ return fs
63
+ .readdirSync(ownerCallDirectory, { withFileTypes: true })
64
+ .filter((entry) => entry.isDirectory())
65
+ .map((entry) => entry.name);
66
+ };
67
+
68
+ export const ownerCallFileDeleteInEveryProject = (
69
+ params: OwnerCallFileDeleteInEveryProjectParams,
70
+ ): void => {
71
+ for (const projectCode of projectCodeDirectoryNames(params.dataDir)) {
72
+ ownerCallFileDelete({
73
+ dataDir: params.dataDir,
74
+ projectCode,
75
+ sessionName: params.sessionName,
76
+ });
77
+ }
78
+ };
79
+
80
+ // The in-tmux-by-human data the scheduled run writes into the same directory
81
+ // serveWeb serves: one `{projectCode}.v4.json` per project, each listing the
82
+ // sessions of that project. `index.v4.json` only names the project files, so
83
+ // it is not read here.
84
+ const IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX = '.v4.json';
85
+ const IN_TMUX_BY_HUMAN_INDEX_FILE_NAME = `index${IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX}`;
86
+
87
+ const isUnknownArray = (value: unknown): value is unknown[] =>
88
+ Array.isArray(value);
89
+
90
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
91
+ typeof value === 'object' && value !== null;
92
+
93
+ const arrayOrEmpty = (value: unknown): unknown[] =>
94
+ isUnknownArray(value) ? value : [];
95
+
96
+ const propertyOrUndefined = (value: unknown, key: string): unknown =>
97
+ isRecord(value) ? value[key] : undefined;
98
+
99
+ const sessionNamesInProjectDataFile = (filePath: string): string[] => {
100
+ let parsed: unknown;
101
+ try {
102
+ parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
103
+ } catch {
104
+ return [];
105
+ }
106
+ return arrayOrEmpty(propertyOrUndefined(parsed, 'groups')).flatMap((group) =>
107
+ arrayOrEmpty(propertyOrUndefined(group, 'sessions'))
108
+ .map((session) => propertyOrUndefined(session, 'name'))
109
+ .filter((name): name is string => typeof name === 'string'),
110
+ );
111
+ };
112
+
113
+ const inTmuxByHumanProjectSessionNames = (
114
+ dataDir: string,
115
+ ): OwnerCallProjectSessionNames[] => {
116
+ if (!fs.existsSync(dataDir)) {
117
+ return [];
118
+ }
119
+ return fs
120
+ .readdirSync(dataDir, { withFileTypes: true })
121
+ .filter(
122
+ (entry) =>
123
+ entry.isFile() &&
124
+ entry.name.endsWith(IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX) &&
125
+ entry.name !== IN_TMUX_BY_HUMAN_INDEX_FILE_NAME,
126
+ )
127
+ .map((entry) => ({
128
+ projectCode: entry.name.slice(
129
+ 0,
130
+ entry.name.length - IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX.length,
131
+ ),
132
+ sessionNames: sessionNamesInProjectDataFile(
133
+ path.join(dataDir, entry.name),
134
+ ),
135
+ }));
136
+ };
137
+
138
+ export const ownerCallProjectCodeInInTmuxByHumanData = (
139
+ dataDir: string,
140
+ sessionName: string,
141
+ ): string | null =>
142
+ ownerCallProjectCodeOfSession(
143
+ inTmuxByHumanProjectSessionNames(dataDir),
144
+ sessionName,
145
+ );