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,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cleanClosedIssueOwnerCallFiles = void 0;
4
+ const InTmuxByHumanSessionReconcileUseCase_1 = require("../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase");
5
+ const ownerCallFileStore_1 = require("./ownerCallFileStore");
6
+ const cleanClosedIssueOwnerCallFiles = (params) => {
7
+ const { inTmuxDataOutputDir, pjcode, issues } = params;
8
+ if (!inTmuxDataOutputDir || !pjcode) {
9
+ return;
10
+ }
11
+ for (const issue of issues.filter((candidate) => candidate.isClosed)) {
12
+ (0, ownerCallFileStore_1.ownerCallFileDelete)({
13
+ dataDir: inTmuxDataOutputDir,
14
+ projectCode: pjcode,
15
+ sessionName: (0, InTmuxByHumanSessionReconcileUseCase_1.toTmuxSessionName)(issue.url),
16
+ });
17
+ }
18
+ };
19
+ exports.cleanClosedIssueOwnerCallFiles = cleanClosedIssueOwnerCallFiles;
20
+ //# sourceMappingURL=ownerCallFileCleaner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownerCallFileCleaner.js","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/ownerCallFileCleaner.ts"],"names":[],"mappings":";;;AACA,+HAAyG;AACzG,6DAA2D;AAQpD,MAAM,8BAA8B,GAAG,CAC5C,MAA4C,EACtC,EAAE;IACR,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IACvD,IAAI,CAAC,mBAAmB,IAAI,CAAC,MAAM,EAAE,CAAC;QACpC,OAAO;IACT,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrE,IAAA,wCAAmB,EAAC;YAClB,OAAO,EAAE,mBAAmB;YAC5B,WAAW,EAAE,MAAM;YACnB,WAAW,EAAE,IAAA,wDAAiB,EAAC,KAAK,CAAC,GAAG,CAAC;SAC1C,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC;AAdW,QAAA,8BAA8B,kCAczC"}
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ownerCallProjectCodeInInTmuxByHumanData = exports.ownerCallFileDeleteInEveryProject = exports.ownerCallFileDelete = exports.ownerCallFileAppend = exports.ownerCallFilePath = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const OwnerCallFile_1 = require("../../../domain/usecases/intmux/OwnerCallFile");
10
+ const ownerCallFilePath = (dataDir, projectCode, sessionName) => path_1.default.join(dataDir, (0, OwnerCallFile_1.ownerCallFileRelativePath)(projectCode, sessionName));
11
+ exports.ownerCallFilePath = ownerCallFilePath;
12
+ const ownerCallFileAppend = (params) => {
13
+ const filePath = (0, exports.ownerCallFilePath)(params.dataDir, params.projectCode, params.ownerCall.sessionName);
14
+ fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
15
+ fs_1.default.appendFileSync(filePath, (0, OwnerCallFile_1.ownerCallYamlDocument)(params.ownerCall));
16
+ };
17
+ exports.ownerCallFileAppend = ownerCallFileAppend;
18
+ const ownerCallFileDelete = (params) => {
19
+ fs_1.default.rmSync((0, exports.ownerCallFilePath)(params.dataDir, params.projectCode, params.sessionName), { force: true });
20
+ };
21
+ exports.ownerCallFileDelete = ownerCallFileDelete;
22
+ const projectCodeDirectoryNames = (dataDir) => {
23
+ const ownerCallDirectory = path_1.default.join(dataDir, OwnerCallFile_1.OWNER_CALL_FILE_DIRECTORY_NAME);
24
+ if (!fs_1.default.existsSync(ownerCallDirectory)) {
25
+ return [];
26
+ }
27
+ return fs_1.default
28
+ .readdirSync(ownerCallDirectory, { withFileTypes: true })
29
+ .filter((entry) => entry.isDirectory())
30
+ .map((entry) => entry.name);
31
+ };
32
+ const ownerCallFileDeleteInEveryProject = (params) => {
33
+ for (const projectCode of projectCodeDirectoryNames(params.dataDir)) {
34
+ (0, exports.ownerCallFileDelete)({
35
+ dataDir: params.dataDir,
36
+ projectCode,
37
+ sessionName: params.sessionName,
38
+ });
39
+ }
40
+ };
41
+ exports.ownerCallFileDeleteInEveryProject = ownerCallFileDeleteInEveryProject;
42
+ // The in-tmux-by-human data the scheduled run writes into the same directory
43
+ // serveWeb serves: one `{projectCode}.v4.json` per project, each listing the
44
+ // sessions of that project. `index.v4.json` only names the project files, so
45
+ // it is not read here.
46
+ const IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX = '.v4.json';
47
+ const IN_TMUX_BY_HUMAN_INDEX_FILE_NAME = `index${IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX}`;
48
+ const isUnknownArray = (value) => Array.isArray(value);
49
+ const isRecord = (value) => typeof value === 'object' && value !== null;
50
+ const arrayOrEmpty = (value) => isUnknownArray(value) ? value : [];
51
+ const propertyOrUndefined = (value, key) => isRecord(value) ? value[key] : undefined;
52
+ const sessionNamesInProjectDataFile = (filePath) => {
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(fs_1.default.readFileSync(filePath, 'utf-8'));
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ return arrayOrEmpty(propertyOrUndefined(parsed, 'groups')).flatMap((group) => arrayOrEmpty(propertyOrUndefined(group, 'sessions'))
61
+ .map((session) => propertyOrUndefined(session, 'name'))
62
+ .filter((name) => typeof name === 'string'));
63
+ };
64
+ const inTmuxByHumanProjectSessionNames = (dataDir) => {
65
+ if (!fs_1.default.existsSync(dataDir)) {
66
+ return [];
67
+ }
68
+ return fs_1.default
69
+ .readdirSync(dataDir, { withFileTypes: true })
70
+ .filter((entry) => entry.isFile() &&
71
+ entry.name.endsWith(IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX) &&
72
+ entry.name !== IN_TMUX_BY_HUMAN_INDEX_FILE_NAME)
73
+ .map((entry) => ({
74
+ projectCode: entry.name.slice(0, entry.name.length - IN_TMUX_BY_HUMAN_PROJECT_DATA_SUFFIX.length),
75
+ sessionNames: sessionNamesInProjectDataFile(path_1.default.join(dataDir, entry.name)),
76
+ }));
77
+ };
78
+ const ownerCallProjectCodeInInTmuxByHumanData = (dataDir, sessionName) => (0, OwnerCallFile_1.ownerCallProjectCodeOfSession)(inTmuxByHumanProjectSessionNames(dataDir), sessionName);
79
+ exports.ownerCallProjectCodeInInTmuxByHumanData = ownerCallProjectCodeInInTmuxByHumanData;
80
+ //# sourceMappingURL=ownerCallFileStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownerCallFileStore.js","sourceRoot":"","sources":["../../../../src/adapter/entry-points/handlers/ownerCallFileStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ownerCallYamlDocument = exports.isOwnerCallCalledAtValid = exports.ownerCallProjectCodeOfSession = exports.ownerCallFileRelativePath = exports.ownerCallFileSessionKey = exports.OWNER_CALL_FILE_EXTENSION = exports.OWNER_CALL_FILE_PROJECT_CODE_FOR_NO_PROJECT = exports.OWNER_CALL_FILE_DIRECTORY_NAME = void 0;
4
+ const InTmuxByHumanSessionReconcileUseCase_1 = require("./InTmuxByHumanSessionReconcileUseCase");
5
+ exports.OWNER_CALL_FILE_DIRECTORY_NAME = 'call-to-user';
6
+ exports.OWNER_CALL_FILE_PROJECT_CODE_FOR_NO_PROJECT = 'NA';
7
+ exports.OWNER_CALL_FILE_EXTENSION = '.yaml';
8
+ // The key one owner call file is named by. It is the tmux session name
9
+ // derivation every other in-tmux caller uses, followed by the replacement of
10
+ // the slashes tmux keeps but a file name cannot hold. Applying it to a name
11
+ // that already went through it gives that same name back, so the writing side,
12
+ // which knows the tmux session name, and the reading side, which knows the
13
+ // issue url, reach the same file.
14
+ const ownerCallFileSessionKey = (sessionName) => (0, InTmuxByHumanSessionReconcileUseCase_1.toTmuxSessionName)(sessionName).replace(/\//g, '_');
15
+ exports.ownerCallFileSessionKey = ownerCallFileSessionKey;
16
+ const ownerCallFileRelativePath = (projectCode, sessionName) => {
17
+ const directory = projectCode ?? exports.OWNER_CALL_FILE_PROJECT_CODE_FOR_NO_PROJECT;
18
+ const fileName = (0, exports.ownerCallFileSessionKey)(sessionName);
19
+ return `${exports.OWNER_CALL_FILE_DIRECTORY_NAME}/${directory}/${fileName}${exports.OWNER_CALL_FILE_EXTENSION}`;
20
+ };
21
+ exports.ownerCallFileRelativePath = ownerCallFileRelativePath;
22
+ const ownerCallProjectCodeOfSession = (projects, sessionName) => {
23
+ const key = (0, exports.ownerCallFileSessionKey)(sessionName);
24
+ const owningProject = projects.find((project) => project.sessionNames.some((candidate) => (0, exports.ownerCallFileSessionKey)(candidate) === key));
25
+ return owningProject ? owningProject.projectCode : null;
26
+ };
27
+ exports.ownerCallProjectCodeOfSession = ownerCallProjectCodeOfSession;
28
+ const CALLED_AT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
29
+ const isOwnerCallCalledAtValid = (calledAt) => {
30
+ if (!CALLED_AT_PATTERN.test(calledAt)) {
31
+ return false;
32
+ }
33
+ const parsed = new Date(calledAt);
34
+ return (Number.isFinite(parsed.getTime()) &&
35
+ parsed.toISOString().replace(/\.\d{3}Z$/, 'Z') === calledAt);
36
+ };
37
+ exports.isOwnerCallCalledAtValid = isOwnerCallCalledAtValid;
38
+ const BODY_INDENTATION = ' ';
39
+ const bodyBlockLines = (body) => body
40
+ .replace(/\n+$/, '')
41
+ .split('\n')
42
+ .map((line) => (line.length === 0 ? '' : `${BODY_INDENTATION}${line}`));
43
+ const ownerCallYamlDocument = (ownerCall) => [
44
+ '---',
45
+ `sessionName: ${JSON.stringify(ownerCall.sessionName)}`,
46
+ `calledAt: ${JSON.stringify(ownerCall.calledAt)}`,
47
+ `body: |${BODY_INDENTATION.length}`,
48
+ ...bodyBlockLines(ownerCall.body),
49
+ '',
50
+ ].join('\n');
51
+ exports.ownerCallYamlDocument = ownerCallYamlDocument;
52
+ //# sourceMappingURL=OwnerCallFile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OwnerCallFile.js","sourceRoot":"","sources":["../../../../src/domain/usecases/intmux/OwnerCallFile.ts"],"names":[],"mappings":";;;AAAA,iGAA2E;AAE9D,QAAA,8BAA8B,GAAG,cAAc,CAAC;AAEhD,QAAA,2CAA2C,GAAG,IAAI,CAAC;AAEnD,QAAA,yBAAyB,GAAG,OAAO,CAAC;AAQjD,uEAAuE;AACvE,6EAA6E;AAC7E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,kCAAkC;AAC3B,MAAM,uBAAuB,GAAG,CAAC,WAAmB,EAAU,EAAE,CACrE,IAAA,wDAAiB,EAAC,WAAW,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AADxC,QAAA,uBAAuB,2BACiB;AAE9C,MAAM,yBAAyB,GAAG,CACvC,WAA0B,EAC1B,WAAmB,EACX,EAAE;IACV,MAAM,SAAS,GAAG,WAAW,IAAI,mDAA2C,CAAC;IAC7E,MAAM,QAAQ,GAAG,IAAA,+BAAuB,EAAC,WAAW,CAAC,CAAC;IACtD,OAAO,GAAG,sCAA8B,IAAI,SAAS,IAAI,QAAQ,GAAG,iCAAyB,EAAE,CAAC;AAClG,CAAC,CAAC;AAPW,QAAA,yBAAyB,6BAOpC;AAOK,MAAM,6BAA6B,GAAG,CAC3C,QAAwC,EACxC,WAAmB,EACJ,EAAE;IACjB,MAAM,GAAG,GAAG,IAAA,+BAAuB,EAAC,WAAW,CAAC,CAAC;IACjD,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAC9C,OAAO,CAAC,YAAY,CAAC,IAAI,CACvB,CAAC,SAAS,EAAE,EAAE,CAAC,IAAA,+BAAuB,EAAC,SAAS,CAAC,KAAK,GAAG,CAC1D,CACF,CAAC;IACF,OAAO,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC,CAAC;AAXW,QAAA,6BAA6B,iCAWxC;AAEF,MAAM,iBAAiB,GAAG,wCAAwC,CAAC;AAE5D,MAAM,wBAAwB,GAAG,CAAC,QAAgB,EAAW,EAAE;IACpE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClC,OAAO,CACL,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACjC,MAAM,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,KAAK,QAAQ,CAC5D,CAAC;AACJ,CAAC,CAAC;AATW,QAAA,wBAAwB,4BASnC;AAEF,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B,MAAM,cAAc,GAAG,CAAC,IAAY,EAAY,EAAE,CAChD,IAAI;KACD,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;KACnB,KAAK,CAAC,IAAI,CAAC;KACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,gBAAgB,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;AAErE,MAAM,qBAAqB,GAAG,CAAC,SAAoB,EAAU,EAAE,CACpE;IACE,KAAK;IACL,gBAAgB,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE;IACvD,aAAa,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;IACjD,UAAU,gBAAgB,CAAC,MAAM,EAAE;IACnC,GAAG,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC;IACjC,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AARF,QAAA,qBAAqB,yBAQnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.152.3",
3
+ "version": "1.153.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'fs';
2
+ import * as os from 'os';
2
3
  import * as path from 'path';
3
4
  import YAML from 'yaml';
4
5
 
@@ -21,6 +22,8 @@ import {
21
22
  import { StartPreparationUseCase } from '../../../domain/usecases/StartPreparationUseCase';
22
23
  import { NotifyFinishedIssuePreparationUseCase } from '../../../domain/usecases/NotifyFinishedIssuePreparationUseCase';
23
24
  import { CheckIssueReviewReadinessUseCase } from '../../../domain/usecases/CheckIssueReviewReadinessUseCase';
25
+ import { ownerCallFileRelativePath } from '../../../domain/usecases/intmux/OwnerCallFile';
26
+ import { toTmuxSessionName } from '../../../domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase';
24
27
 
25
28
  jest.mock('../../../domain/usecases/StartPreparationUseCase');
26
29
  jest.mock('../../../domain/usecases/NotifyFinishedIssuePreparationUseCase');
@@ -2371,6 +2374,192 @@ mysteryKey: 'value'
2371
2374
  });
2372
2375
  });
2373
2376
 
2377
+ describe('ownerCallFileAppend and ownerCallFileDelete', () => {
2378
+ const issueUrl = 'https://github.com/OWNER/REPO/issues/1';
2379
+ const otherIssueUrl = 'https://github.com/OWNER/REPO/issues/2';
2380
+ const sessionName = toTmuxSessionName(issueUrl);
2381
+ let ownerCallDataDir = '';
2382
+ let bodyFilePath = '';
2383
+
2384
+ beforeEach(() => {
2385
+ ownerCallDataDir = fs.mkdtempSync(
2386
+ path.join(os.tmpdir(), 'owner-call-cli-'),
2387
+ );
2388
+ bodyFilePath = path.join(ownerCallDataDir, 'body.txt');
2389
+ fs.writeFileSync(bodyFilePath, ' the call body\n\nsecond paragraph\n');
2390
+ });
2391
+
2392
+ afterEach(() => {
2393
+ fs.rmSync(ownerCallDataDir, { recursive: true, force: true });
2394
+ });
2395
+
2396
+ // The in-tmux-by-human data the scheduled run writes into the same
2397
+ // directory serveWeb serves: one file per project, each listing the
2398
+ // sessions of that project by their raw issue url.
2399
+ type InTmuxByHumanProjectData = {
2400
+ projectCode: string;
2401
+ sessionIssueUrls: string[];
2402
+ };
2403
+
2404
+ const writeInTmuxByHumanData = (
2405
+ projects: InTmuxByHumanProjectData[],
2406
+ ): void => {
2407
+ for (const project of projects) {
2408
+ fs.writeFileSync(
2409
+ path.join(ownerCallDataDir, `${project.projectCode}.v4.json`),
2410
+ `${JSON.stringify({
2411
+ version: 4,
2412
+ overviewUrl: 'https://github.com/orgs/OWNER/projects/1',
2413
+ tdpmConsoleUrl: 'http://localhost/projects/code',
2414
+ newIssueUrl: 'https://github.com/OWNER/REPO/issues/new',
2415
+ groups: [
2416
+ {
2417
+ story: 'a story',
2418
+ sessions: project.sessionIssueUrls.map((url) => ({
2419
+ name: url,
2420
+ description: 'an issue title',
2421
+ })),
2422
+ },
2423
+ ],
2424
+ })}\n`,
2425
+ );
2426
+ }
2427
+ };
2428
+
2429
+ const projectCodeListing = (
2430
+ projects: InTmuxByHumanProjectData[],
2431
+ url: string,
2432
+ ): string | null =>
2433
+ projects.find((project) => project.sessionIssueUrls.includes(url))
2434
+ ?.projectCode ?? null;
2435
+
2436
+ const appendedFilePath = (projectCode: string | null): string =>
2437
+ path.join(
2438
+ ownerCallDataDir,
2439
+ ownerCallFileRelativePath(projectCode, sessionName),
2440
+ );
2441
+
2442
+ const runAppend = (calledAt: string): Promise<unknown> =>
2443
+ program.parseAsync([
2444
+ 'node',
2445
+ 'test',
2446
+ 'ownerCallFileAppend',
2447
+ '--session',
2448
+ sessionName,
2449
+ '--calledAt',
2450
+ calledAt,
2451
+ '--body-file',
2452
+ bodyFilePath,
2453
+ '--inTmuxDataDir',
2454
+ ownerCallDataDir,
2455
+ ]);
2456
+
2457
+ it('appends the call under the directory of the project whose session list holds the session', async () => {
2458
+ const projects: InTmuxByHumanProjectData[] = [
2459
+ { projectCode: 'other', sessionIssueUrls: [otherIssueUrl] },
2460
+ { projectCode: 'umino', sessionIssueUrls: [issueUrl] },
2461
+ ];
2462
+ writeInTmuxByHumanData(projects);
2463
+ const stdoutSpy = jest
2464
+ .spyOn(process.stdout, 'write')
2465
+ .mockImplementation(() => true);
2466
+
2467
+ await runAppend('2026-08-14T04:22:28Z');
2468
+
2469
+ const documents = YAML.parseAllDocuments(
2470
+ fs.readFileSync(
2471
+ appendedFilePath(projectCodeListing(projects, issueUrl)),
2472
+ 'utf-8',
2473
+ ),
2474
+ );
2475
+ expect(documents).toHaveLength(1);
2476
+ expect(documents[0].toJS()).toEqual({
2477
+ sessionName,
2478
+ calledAt: '2026-08-14T04:22:28Z',
2479
+ body: ' the call body\n\nsecond paragraph\n',
2480
+ });
2481
+ expect(stdoutSpy).not.toHaveBeenCalled();
2482
+
2483
+ stdoutSpy.mockRestore();
2484
+ });
2485
+
2486
+ it('appends the call under NA when no project lists the session', async () => {
2487
+ const projects: InTmuxByHumanProjectData[] = [
2488
+ { projectCode: 'umino', sessionIssueUrls: [otherIssueUrl] },
2489
+ ];
2490
+ writeInTmuxByHumanData(projects);
2491
+
2492
+ await runAppend('2026-08-14T04:22:28Z');
2493
+
2494
+ expect(projectCodeListing(projects, issueUrl)).toBeNull();
2495
+ expect(fs.existsSync(appendedFilePath(null))).toBe(true);
2496
+ });
2497
+
2498
+ it('rejects a calledAt that is not a UTC ISO-8601 timestamp with second precision', async () => {
2499
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
2500
+ const processExitSpy = jest
2501
+ .spyOn(process, 'exit')
2502
+ .mockImplementation(() => {
2503
+ throw new Error('process.exit called');
2504
+ });
2505
+
2506
+ await expect(runAppend('2026-08-14 04:22:28')).rejects.toThrow(
2507
+ 'process.exit called',
2508
+ );
2509
+ expect(processExitSpy).toHaveBeenCalledWith(1);
2510
+ expect(fs.existsSync(appendedFilePath(null))).toBe(false);
2511
+
2512
+ consoleErrorSpy.mockRestore();
2513
+ processExitSpy.mockRestore();
2514
+ });
2515
+
2516
+ const runDelete = (): Promise<unknown> =>
2517
+ program.parseAsync([
2518
+ 'node',
2519
+ 'test',
2520
+ 'ownerCallFileDelete',
2521
+ '--session',
2522
+ sessionName,
2523
+ '--inTmuxDataDir',
2524
+ ownerCallDataDir,
2525
+ ]);
2526
+
2527
+ it('deletes the file of the session whichever project directory holds it', async () => {
2528
+ const projects: InTmuxByHumanProjectData[] = [
2529
+ { projectCode: 'umino', sessionIssueUrls: [issueUrl] },
2530
+ ];
2531
+ writeInTmuxByHumanData(projects);
2532
+ await runAppend('2026-08-14T04:22:28Z');
2533
+ const writtenFilePath = appendedFilePath(
2534
+ projectCodeListing(projects, issueUrl),
2535
+ );
2536
+ expect(fs.existsSync(writtenFilePath)).toBe(true);
2537
+
2538
+ await runDelete();
2539
+
2540
+ expect(fs.existsSync(writtenFilePath)).toBe(false);
2541
+ });
2542
+
2543
+ it('deletes the file left behind under the project the session has since left', async () => {
2544
+ writeInTmuxByHumanData([
2545
+ { projectCode: 'umino', sessionIssueUrls: [issueUrl] },
2546
+ ]);
2547
+ await runAppend('2026-08-14T04:22:28Z');
2548
+ writeInTmuxByHumanData([
2549
+ { projectCode: 'umino', sessionIssueUrls: [] },
2550
+ { projectCode: 'other', sessionIssueUrls: [issueUrl] },
2551
+ ]);
2552
+
2553
+ await runDelete();
2554
+
2555
+ expect(fs.existsSync(appendedFilePath('umino'))).toBe(false);
2556
+ });
2557
+
2558
+ it('succeeds when the file to delete is already absent', async () => {
2559
+ await expect(runDelete()).resolves.toBeDefined();
2560
+ });
2561
+ });
2562
+
2374
2563
  describe('runCliProgram', () => {
2375
2564
  const mockCheckUseCaseRejectingWith = (error: Error): void => {
2376
2565
  const MockedCheckUseCase = jest.mocked(CheckIssueReviewReadinessUseCase);
@@ -63,6 +63,12 @@ import {
63
63
  loadLiveSessionOauthTokenSelectionSettings,
64
64
  resolveFleetConfigFilePath,
65
65
  } from './fleetConfig';
66
+ import { isOwnerCallCalledAtValid } from '../../../domain/usecases/intmux/OwnerCallFile';
67
+ import {
68
+ ownerCallFileAppend,
69
+ ownerCallFileDeleteInEveryProject,
70
+ ownerCallProjectCodeInInTmuxByHumanData,
71
+ } from '../handlers/ownerCallFileStore';
66
72
 
67
73
  type StartDaemonOptions = {
68
74
  projectUrl?: string;
@@ -102,6 +108,18 @@ type ServeWebOptions = {
102
108
  dashboardProjectNames?: string;
103
109
  };
104
110
 
111
+ type OwnerCallFileAppendOptions = {
112
+ session: string;
113
+ calledAt: string;
114
+ bodyFile: string;
115
+ inTmuxDataDir?: string;
116
+ };
117
+
118
+ type OwnerCallFileDeleteOptions = {
119
+ session: string;
120
+ inTmuxDataDir?: string;
121
+ };
122
+
105
123
  const DEFAULT_IN_TMUX_DATA_DIR =
106
124
  '/home/hiromi/0_workspaces/workspace1/jsonpub/in-tmux-by-human';
107
125
 
@@ -1149,6 +1167,63 @@ program
1149
1167
  }
1150
1168
  });
1151
1169
 
1170
+ program
1171
+ .command('ownerCallFileAppend')
1172
+ .description(
1173
+ 'Append one owner call as a YAML document to the per-session owner call file. The project the session belongs to is resolved from the in-tmux-by-human data in the data directory, and a session no project lists is written under NA. The file is created when it does not exist, and every later call is appended after the existing documents so the oldest call stays first. Nothing is written to stdout on success.',
1174
+ )
1175
+ .requiredOption('--session <name>', 'tmux session name that raised the call')
1176
+ .requiredOption(
1177
+ '--calledAt <timestamp>',
1178
+ 'Time the call was raised, as UTC ISO-8601 with second precision and a trailing Z',
1179
+ )
1180
+ .requiredOption(
1181
+ '--body-file <path>',
1182
+ 'Path to the file holding the call body; the body is read from a file because it is multi-line and can be long',
1183
+ )
1184
+ .option(
1185
+ '--inTmuxDataDir <path>',
1186
+ `Directory the owner call files are written under, the same directory serveWeb serves them from (default: ${DEFAULT_IN_TMUX_DATA_DIR})`,
1187
+ )
1188
+ .action((options: OwnerCallFileAppendOptions) => {
1189
+ if (!isOwnerCallCalledAtValid(options.calledAt)) {
1190
+ console.error(
1191
+ '--calledAt must be a UTC ISO-8601 timestamp with second precision and a trailing Z, for example 2026-08-14T04:22:28Z',
1192
+ );
1193
+ return process.exit(1);
1194
+ }
1195
+ const dataDir = options.inTmuxDataDir ?? DEFAULT_IN_TMUX_DATA_DIR;
1196
+ ownerCallFileAppend({
1197
+ dataDir,
1198
+ projectCode: ownerCallProjectCodeInInTmuxByHumanData(
1199
+ dataDir,
1200
+ options.session,
1201
+ ),
1202
+ ownerCall: {
1203
+ sessionName: options.session,
1204
+ calledAt: options.calledAt,
1205
+ body: fs.readFileSync(options.bodyFile, 'utf-8'),
1206
+ },
1207
+ });
1208
+ });
1209
+
1210
+ program
1211
+ .command('ownerCallFileDelete')
1212
+ .description(
1213
+ 'Delete the owner call file of one session, whichever project directory holds it, so a session that moved between projects after its call was appended leaves nothing behind. It succeeds when the file is already absent, so a reset that runs twice is not an error. Nothing is written to stdout on success.',
1214
+ )
1215
+ .requiredOption('--session <name>', 'tmux session name that raised the call')
1216
+ .option(
1217
+ '--inTmuxDataDir <path>',
1218
+ `Directory the owner call files are written under, the same directory serveWeb serves them from (default: ${DEFAULT_IN_TMUX_DATA_DIR})`,
1219
+ )
1220
+ .action((options: OwnerCallFileDeleteOptions) => {
1221
+ ownerCallFileDeleteInEveryProject({
1222
+ dataDir: options.inTmuxDataDir ?? DEFAULT_IN_TMUX_DATA_DIR,
1223
+ sessionName: options.session,
1224
+ });
1225
+ });
1226
+
1152
1227
  export const reportFatalErrorAndExit = (error: unknown): void => {
1153
1228
  console.error(error);
1154
1229
  process.exit(1);