Skip to content

Commit e29d49e

Browse files
authored
feat: Add --prompt-file CLI option (#29)
* feat: Add --prompt-file CLI option * Fix tests
1 parent f164573 commit e29d49e

File tree

8 files changed

+118
-6
lines changed

8 files changed

+118
-6
lines changed

.github/copilot-instructions.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,42 @@ x => x + x
3636

3737
```javascript
3838
for (let i = 0, n = str.length; i < 10; i++) {
39-
if (x < 10) {
40-
foo();
41-
}
39+
if (x < 10) {
40+
foo();
41+
}
4242
}
4343

4444
function f(x: number, y: string): void { }
4545
```
4646

47+
## Project Structure
48+
49+
- Use `src` for source code
50+
- TypeScript types are found in `src/types.ts`
51+
- Use `tests` for test code
52+
- Test files have the same name as the source file they are testing, but with a `.test.js` suffix
53+
- Use a `fixtures` subfolder in `tests` for test data files (e.g., prompt files)
54+
55+
## Coding Approach
56+
57+
- Tests should always be updated to reflect the latest changes, especially for new CLI flags or options (e.g., `--prompt-file`)
58+
- The README file should always be updated to document new CLI options and usage examples
59+
- Use `async/await` for asynchronous code
60+
- Use `Promise.all` for parallel asynchronous code
61+
- When adding CLI flags, ensure argument parsing uses camelCase in code and kebab-case for CLI, and map as needed
62+
- Always handle file paths robustly in tests (prefer `path.resolve` for test fixture files)
63+
- `console` can only be used in `cli.js`.
64+
- Use `console.log` for logging messages to the user
65+
- Use `console.error` for errors
66+
- Do not use `console` for debugging (except for temporary debug output during test troubleshooting)
67+
68+
## Commands
69+
70+
- Use `npm test` to run all tests
71+
- To test an individual file use `npx mocha tests/<test-file>.test.js`
72+
- Use `npm run lint` to run the linter
73+
- Use `npm run fmt` to format the code
74+
4775
## OpenAI API
4876

4977
For OpenAI API calls, we use the Responses API.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ npx social-changelog --org <org> --repo <repo> --name <project-name>
4242
- `--repo, -r` - The repository name
4343
- `--name, -n` - (Optional) The display name of the project (defaults to org/repo)
4444
- `--tag, -t` - (Optional) Specific release tag to use (defaults to latest)
45+
- `--prompt-file` - (Optional) Path to a file containing a custom prompt to use instead of the default
4546
- `--help, -h` - Show help information
4647

4748
### CLI Examples
@@ -52,6 +53,12 @@ Generate post for latest release:
5253
npx social-changelog --org humanwhocodes --repo social-changelog
5354
```
5455

56+
Use a custom prompt file:
57+
58+
```bash
59+
npx social-changelog --org humanwhocodes --repo social-changelog --prompt-file ./my-prompt.txt
60+
```
61+
5562
By default, the `org/repo` will be used as the project name. You can override this by providing the `--name` option:
5663

5764
```bash

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"fmt": "prettier --write .",
4646
"prepublishOnly": "npm run build",
4747
"pretest": "npm run build",
48-
"test:unit": "mocha --exit tests/**/*.*",
48+
"test:unit": "mocha --exit tests/**/*.js",
4949
"test:jsr": "npx jsr@latest publish --dry-run",
5050
"test": "npm run test:unit"
5151
},

src/cli.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,28 @@ export class CLI {
8080
const token = this.#env.OPENAI_API_KEY;
8181
const name = flags.name || `${org}/${repo}`;
8282

83+
let prompt = "";
84+
if (flags.promptFile) {
85+
try {
86+
const fsp = await import("node:fs/promises");
87+
prompt = await fsp.readFile(flags.promptFile, "utf8");
88+
} catch (err) {
89+
const message =
90+
err instanceof Error ? err.message : String(err);
91+
this.#console.error(`Error reading prompt file: ${message}`);
92+
return 1;
93+
}
94+
}
95+
8396
try {
8497
const release = await fetchRelease(`${org}/${repo}`, flags.tag);
8598
const generator = githubToken
8699
? new ChatCompletionPostGenerator(githubToken, {
87100
baseUrl: GITHUB_BASE_URL,
88101
model: GITHUB_MODEL,
102+
prompt,
89103
})
90-
: new ResponseAPIPostGenerator(token);
104+
: new ResponseAPIPostGenerator(token, { prompt });
91105
const post = await generator.generateSocialPost(name, release);
92106

93107
this.#console.log(post);
@@ -97,7 +111,6 @@ export class CLI {
97111
);
98112
return 1;
99113
}
100-
101114
return 0;
102115
}
103116

@@ -115,11 +128,18 @@ export class CLI {
115128
name: { type: "string", short: "n" },
116129
tag: { type: "string", short: "t" },
117130
help: { type: "boolean", short: "h" },
131+
"prompt-file": { type: "string" },
118132
},
119133
allowPositionals: false,
120134
strict: false,
121135
});
122136

137+
// Map kebab-case to camelCase for CLIArgs
138+
if ("prompt-file" in flags) {
139+
flags.promptFile = flags["prompt-file"];
140+
delete flags["prompt-file"];
141+
}
142+
123143
return /** @type {CLIArgs} */ (flags);
124144
}
125145

@@ -136,6 +156,7 @@ export class CLI {
136156
this.#console.log(" --repo, -r The repository name");
137157
this.#console.log(" --name, -n The name of the project");
138158
this.#console.log(" --tag, -t The release tag [default: latest]");
159+
this.#console.log(" --prompt-file Path to a custom prompt file");
139160
this.#console.log(" --help, -h Show this help message");
140161
this.#console.log("");
141162
}

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export interface CLIArgs {
146146
name: string | undefined;
147147
tag: string | undefined;
148148
help: boolean | undefined;
149+
promptFile?: string;
149150
}
150151

151152
export interface GptMessage {

tests/cli.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
//-----------------------------------------------------------------------------
88

99
import { strict as assert } from "node:assert";
10+
import path from "node:path";
11+
import fsp from "node:fs/promises";
1012
import { CLI } from "../src/cli.js";
1113
import { MockServer, FetchMocker } from "mentoss";
1214

@@ -309,6 +311,57 @@ describe("CLI", () => {
309311
true,
310312
);
311313
});
314+
315+
it("should use custom prompt from --prompt-file option", async () => {
316+
const promptPath = path.resolve(
317+
"tests",
318+
"fixtures",
319+
"prompt-custom.txt",
320+
);
321+
const customPrompt = await fsp.readFile(promptPath, "utf8");
322+
323+
githubServer.get("/repos/test-org/test-repo/releases/latest", {
324+
status: 200,
325+
body: MOCK_RELEASE,
326+
});
327+
328+
openAIServer.post(
329+
{
330+
url: "/v1/responses",
331+
body: {
332+
instructions: customPrompt,
333+
},
334+
},
335+
{
336+
status: 200,
337+
body: {
338+
id: "resp_456",
339+
output: [
340+
{
341+
content: [
342+
{
343+
type: "output_text",
344+
text: "CUSTOMPROMPT",
345+
},
346+
],
347+
},
348+
],
349+
},
350+
},
351+
);
352+
353+
const exitCode = await cli.execute([
354+
"--org",
355+
"test-org",
356+
"--repo",
357+
"test-repo",
358+
"--prompt-file",
359+
promptPath,
360+
]);
361+
362+
assert.equal(exitCode, 0);
363+
assert.equal(testConsole.logs[0], "CUSTOMPROMPT");
364+
});
312365
});
313366

314367
describe("showHelp()", () => {

tests/fixtures/prompt-custom.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
You are a test prompt for the CLI --prompt-file option. Only output the word: CUSTOMPROMPT

tests/prompt-custom.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
You are a test prompt for the CLI --prompt-file option. Only output the word: CUSTOMPROMPT

0 commit comments

Comments
 (0)