npm-cli-gh-issue-preparator 1.2.0 → 1.3.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [1.3.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.2.0...v1.3.0) (2026-01-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * prevent command injection in xfce4-terminal spawn ([0a4e008](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/0a4e008749b293cd1f1d311ad08adfc9f37033c3))
7
+
8
+
9
+ ### Features
10
+
11
+ * **src:** create CopilotRepository ([c79ed81](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/commit/c79ed8127342a5aa58fc2f83b10334bbb0fc6a0d)), closes [#38](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/issues/38)
12
+
1
13
  # [1.2.0](https://github.com/HiromiShikata/npm-cli-gh-issue-preparator/compare/v1.1.0...v1.2.0) (2026-01-12)
2
14
 
3
15
 
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Xfce4TerminalCopilotRepository = void 0;
4
+ const child_process_1 = require("child_process");
5
+ class Xfce4TerminalCopilotRepository {
6
+ escapeForSingleQuotes(str) {
7
+ return str.replace(/'/g, "'\\''");
8
+ }
9
+ run(prompt, model, processTitle) {
10
+ const escapedPrompt = this.escapeForSingleQuotes(prompt);
11
+ const escapedTitle = this.escapeForSingleQuotes(processTitle);
12
+ const innerCommand = `copilot --model ${model} --allow-all-tools -p '${escapedPrompt}'`;
13
+ const escapedInnerCommand = this.escapeForSingleQuotes(innerCommand);
14
+ const title = `gh-issue-preparator: ${escapedTitle}`;
15
+ const child = (0, child_process_1.spawn)('xfce4-terminal', ['-T', title, '-e', `bash -c '${escapedInnerCommand}'`], {
16
+ detached: true,
17
+ stdio: 'ignore',
18
+ });
19
+ child.on('error', () => {
20
+ // Intentionally empty - fire-and-forget behavior
21
+ // Errors are silently ignored as this is a background terminal spawn
22
+ });
23
+ child.unref();
24
+ }
25
+ }
26
+ exports.Xfce4TerminalCopilotRepository = Xfce4TerminalCopilotRepository;
27
+ //# sourceMappingURL=Xfce4TerminalCopilotRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Xfce4TerminalCopilotRepository.js","sourceRoot":"","sources":["../../../src/adapter/repositories/Xfce4TerminalCopilotRepository.ts"],"names":[],"mappings":";;;AACA,iDAAsC;AAEtC,MAAa,8BAA8B;IACjC,qBAAqB,CAAC,GAAW;QACvC,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,GAAG,CAAC,MAAc,EAAE,KAAmB,EAAE,YAAoB;QAC3D,MAAM,aAAa,GAAG,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,CAAC;QACzD,MAAM,YAAY,GAAG,IAAI,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC;QAC9D,MAAM,YAAY,GAAG,mBAAmB,KAAK,0BAA0B,aAAa,GAAG,CAAC;QACxF,MAAM,mBAAmB,GAAG,IAAI,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC;QACrE,MAAM,KAAK,GAAG,wBAAwB,YAAY,EAAE,CAAC;QAErD,MAAM,KAAK,GAAG,IAAA,qBAAK,EACjB,gBAAgB,EAChB,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,mBAAmB,GAAG,CAAC,EACvD;YACE,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,QAAQ;SAChB,CACF,CAAC;QAEF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACrB,iDAAiD;YACjD,qEAAqE;QACvE,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;CACF;AA5BD,wEA4BC"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=CopilotRepository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopilotRepository.js","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/CopilotRepository.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npm-cli-gh-issue-preparator",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -0,0 +1,143 @@
1
+ const mockUnref = jest.fn();
2
+ const mockOn = jest.fn();
3
+ const mockSpawn = jest.fn(() => ({ on: mockOn, unref: mockUnref }));
4
+
5
+ jest.mock('child_process', () => ({
6
+ spawn: mockSpawn,
7
+ }));
8
+
9
+ import { Xfce4TerminalCopilotRepository } from './Xfce4TerminalCopilotRepository';
10
+
11
+ describe('Xfce4TerminalCopilotRepository', () => {
12
+ let repository: Xfce4TerminalCopilotRepository;
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ repository = new Xfce4TerminalCopilotRepository();
17
+ });
18
+
19
+ describe('run', () => {
20
+ it('should spawn xfce4-terminal with correct arguments', () => {
21
+ repository.run('test prompt', 'gpt-5-mini', 'test-process');
22
+
23
+ expect(mockSpawn).toHaveBeenCalledWith(
24
+ 'xfce4-terminal',
25
+ [
26
+ '-T',
27
+ 'gh-issue-preparator: test-process',
28
+ '-e',
29
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''test prompt'\\'''",
30
+ ],
31
+ {
32
+ detached: true,
33
+ stdio: 'ignore',
34
+ },
35
+ );
36
+ expect(mockOn).toHaveBeenCalledWith('error', expect.any(Function));
37
+ expect(mockUnref).toHaveBeenCalled();
38
+ });
39
+
40
+ it('should escape single quotes in prompt to prevent command injection', () => {
41
+ repository.run(
42
+ "prompt with 'single quotes'",
43
+ 'gpt-5-mini',
44
+ 'test-process',
45
+ );
46
+
47
+ expect(mockSpawn).toHaveBeenCalledWith(
48
+ 'xfce4-terminal',
49
+ [
50
+ '-T',
51
+ 'gh-issue-preparator: test-process',
52
+ '-e',
53
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt with '\\''\\'\\'''\\''single quotes'\\''\\'\\'''\\'''\\'''",
54
+ ],
55
+ {
56
+ detached: true,
57
+ stdio: 'ignore',
58
+ },
59
+ );
60
+ });
61
+
62
+ it('should escape backticks in prompt to prevent command injection', () => {
63
+ repository.run('prompt with `backticks`', 'gpt-5-mini', 'test-process');
64
+
65
+ expect(mockSpawn).toHaveBeenCalledWith(
66
+ 'xfce4-terminal',
67
+ [
68
+ '-T',
69
+ 'gh-issue-preparator: test-process',
70
+ '-e',
71
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt with `backticks`'\\'''",
72
+ ],
73
+ {
74
+ detached: true,
75
+ stdio: 'ignore',
76
+ },
77
+ );
78
+ });
79
+
80
+ it('should escape command substitution in prompt to prevent injection', () => {
81
+ repository.run('prompt with $(whoami)', 'gpt-5-mini', 'test-process');
82
+
83
+ expect(mockSpawn).toHaveBeenCalledWith(
84
+ 'xfce4-terminal',
85
+ [
86
+ '-T',
87
+ 'gh-issue-preparator: test-process',
88
+ '-e',
89
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt with $(whoami)'\\'''",
90
+ ],
91
+ {
92
+ detached: true,
93
+ stdio: 'ignore',
94
+ },
95
+ );
96
+ });
97
+
98
+ it('should escape single quotes in processTitle', () => {
99
+ repository.run('prompt', 'gpt-5-mini', "title with 'quotes'");
100
+
101
+ expect(mockSpawn).toHaveBeenCalledWith(
102
+ 'xfce4-terminal',
103
+ [
104
+ '-T',
105
+ "gh-issue-preparator: title with '\\''quotes'\\''",
106
+ '-e',
107
+ "bash -c 'copilot --model gpt-5-mini --allow-all-tools -p '\\''prompt'\\'''",
108
+ ],
109
+ {
110
+ detached: true,
111
+ stdio: 'ignore',
112
+ },
113
+ );
114
+ });
115
+
116
+ it('should set correct window title', () => {
117
+ repository.run('prompt', 'gpt-5-mini', 'my-custom-title');
118
+
119
+ expect(mockSpawn).toHaveBeenCalledWith(
120
+ 'xfce4-terminal',
121
+ expect.arrayContaining(['-T', 'gh-issue-preparator: my-custom-title']),
122
+ expect.any(Object),
123
+ );
124
+ });
125
+
126
+ it('should register error handler and silently ignore errors', () => {
127
+ let capturedHandler: ((error: Error) => void) | undefined;
128
+ mockOn.mockImplementation(
129
+ (event: string, handler: (error: Error) => void) => {
130
+ if (event === 'error') {
131
+ capturedHandler = handler;
132
+ }
133
+ },
134
+ );
135
+
136
+ repository.run('test', 'gpt-5-mini', 'test');
137
+
138
+ expect(mockOn).toHaveBeenCalledWith('error', expect.any(Function));
139
+ expect(capturedHandler).toBeDefined();
140
+ expect(() => capturedHandler?.(new Error('spawn failed'))).not.toThrow();
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,32 @@
1
+ import { CopilotRepository } from '../../domain/usecases/adapter-interfaces/CopilotRepository';
2
+ import { spawn } from 'child_process';
3
+
4
+ export class Xfce4TerminalCopilotRepository implements CopilotRepository {
5
+ private escapeForSingleQuotes(str: string): string {
6
+ return str.replace(/'/g, "'\\''");
7
+ }
8
+
9
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void {
10
+ const escapedPrompt = this.escapeForSingleQuotes(prompt);
11
+ const escapedTitle = this.escapeForSingleQuotes(processTitle);
12
+ const innerCommand = `copilot --model ${model} --allow-all-tools -p '${escapedPrompt}'`;
13
+ const escapedInnerCommand = this.escapeForSingleQuotes(innerCommand);
14
+ const title = `gh-issue-preparator: ${escapedTitle}`;
15
+
16
+ const child = spawn(
17
+ 'xfce4-terminal',
18
+ ['-T', title, '-e', `bash -c '${escapedInnerCommand}'`],
19
+ {
20
+ detached: true,
21
+ stdio: 'ignore',
22
+ },
23
+ );
24
+
25
+ child.on('error', () => {
26
+ // Intentionally empty - fire-and-forget behavior
27
+ // Errors are silently ignored as this is a background terminal spawn
28
+ });
29
+
30
+ child.unref();
31
+ }
32
+ }
@@ -0,0 +1,3 @@
1
+ export interface CopilotRepository {
2
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void;
3
+ }
@@ -0,0 +1,6 @@
1
+ import { CopilotRepository } from '../../domain/usecases/adapter-interfaces/CopilotRepository';
2
+ export declare class Xfce4TerminalCopilotRepository implements CopilotRepository {
3
+ private escapeForSingleQuotes;
4
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void;
5
+ }
6
+ //# sourceMappingURL=Xfce4TerminalCopilotRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Xfce4TerminalCopilotRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/Xfce4TerminalCopilotRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAG/F,qBAAa,8BAA+B,YAAW,iBAAiB;IACtE,OAAO,CAAC,qBAAqB;IAI7B,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;CAuBrE"}
@@ -0,0 +1,4 @@
1
+ export interface CopilotRepository {
2
+ run(prompt: string, model: 'gpt-5-mini', processTitle: string): void;
3
+ }
4
+ //# sourceMappingURL=CopilotRepository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopilotRepository.d.ts","sourceRoot":"","sources":["../../../../src/domain/usecases/adapter-interfaces/CopilotRepository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtE"}