cstm-changeset 1.0.1
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/LICENSE.md +9 -0
- package/README.md +77 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paul Morar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# cstm-changeset
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/cstm-changeset)
|
|
4
|
+
[](./LICENSE.md)
|
|
5
|
+
|
|
6
|
+
A CLI wrapper around `changeset add` that prompts for structured business context — because changelogs should tell the *why*, not just the *what*.
|
|
7
|
+
|
|
8
|
+
## Why?
|
|
9
|
+
|
|
10
|
+
[Changesets](https://github.com/changesets/changesets) is a fantastic tool for managing versioning and changelogs. But out of the box, it only captures *what* changed. When a teammate (or future you) looks back at the changelog, questions like these often go unanswered:
|
|
11
|
+
|
|
12
|
+
- What business value did this change bring?
|
|
13
|
+
- Did this affect clients? How?
|
|
14
|
+
- Was it tested? How thoroughly?
|
|
15
|
+
|
|
16
|
+
**cstm-changeset** fixes that by adding a quick interactive prompt after `changeset add`, appending structured context directly to your changeset file.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -D cstm-changeset
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
> **Note:** Requires Node.js 24+ and `@changesets/cli` as a peer dependency.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Instead of running `npx changeset add`, run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx cstm-changeset
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The CLI will:
|
|
35
|
+
|
|
36
|
+
1. Run the standard `changeset add` flow (package selection, bump type, summary)
|
|
37
|
+
2. Prompt you with a few follow-up questions about business context
|
|
38
|
+
3. Append the answers to the newly created changeset file
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
Here's what the interactive prompt looks like:
|
|
43
|
+
|
|
44
|
+
<!-- TODO: Add screenshot -->
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
And here's what gets appended to your changeset:
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
## Business Context
|
|
51
|
+
|
|
52
|
+
**Business value:** Improves checkout conversion by reducing page load time
|
|
53
|
+
|
|
54
|
+
**Client impact:** Yes — Clients using the legacy API will need to migrate
|
|
55
|
+
|
|
56
|
+
**Tested:** Yes — Ran load tests simulating 10k concurrent users
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## How It Works
|
|
60
|
+
|
|
61
|
+
1. Detects your `.changeset` directory
|
|
62
|
+
2. Runs `npx changeset add` under the hood
|
|
63
|
+
3. Identifies the newly created changeset file
|
|
64
|
+
4. Asks three quick questions:
|
|
65
|
+
- What value does this change bring to the business?
|
|
66
|
+
- Does this carry client impact? (if yes, describe it)
|
|
67
|
+
- Has this been tested? (if yes, describe how)
|
|
68
|
+
5. Appends a "Business Context" section to the changeset
|
|
69
|
+
|
|
70
|
+
## Requirements
|
|
71
|
+
|
|
72
|
+
- Node.js **24+**
|
|
73
|
+
- `@changesets/cli` **2+** (peer dependency)
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
[MIT](./LICENSE.md)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Interface } from "node:readline";
|
|
3
|
+
interface BusinessContext {
|
|
4
|
+
businessValue: string;
|
|
5
|
+
hasClientImpact: boolean;
|
|
6
|
+
clientImpactDetail: string;
|
|
7
|
+
isTested: boolean;
|
|
8
|
+
testingDetail: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function getChangesetDir(): string;
|
|
11
|
+
export declare function getChangesetFiles(dir: string): string[];
|
|
12
|
+
export declare function askYesNo(rl: Interface, question: string): Promise<boolean>;
|
|
13
|
+
export declare function askFreeText(rl: Interface, question: string): Promise<string>;
|
|
14
|
+
export declare function buildAnnotation(ctx: BusinessContext): string;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAGA,OAAO,EAAmB,SAAS,EAAE,MAAM,eAAe,CAAC;AAM3D,UAAU,eAAe;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAID,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAYvD;AAQD,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,SAAS,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,wBAAsB,WAAW,CAC/B,EAAE,EAAE,SAAS,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAGjB;AAID,wBAAgB,eAAe,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAU5D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
// ─── Filesystem ───────────────────────────────────────────────────────────────
|
|
7
|
+
export function getChangesetDir() {
|
|
8
|
+
return resolve(process.cwd(), ".changeset");
|
|
9
|
+
}
|
|
10
|
+
export function getChangesetFiles(dir) {
|
|
11
|
+
try {
|
|
12
|
+
return readdirSync(dir).filter((f) => f.endsWith(".md") && f !== "README.md");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
console.error(`\n❌ Could not find a .changeset directory at:\n ${dir}\n\n` +
|
|
16
|
+
` Make sure you're running this from the root of a project that uses changesets.\n`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// ─── Prompt helpers ───────────────────────────────────────────────────────────
|
|
21
|
+
function ask(rl, question) {
|
|
22
|
+
return new Promise((res) => rl.question(question, res));
|
|
23
|
+
}
|
|
24
|
+
export async function askYesNo(rl, question) {
|
|
25
|
+
while (true) {
|
|
26
|
+
const answer = (await ask(rl, `${question} (y/n): `)).trim().toLowerCase();
|
|
27
|
+
if (answer === "y" || answer === "yes")
|
|
28
|
+
return true;
|
|
29
|
+
if (answer === "n" || answer === "no")
|
|
30
|
+
return false;
|
|
31
|
+
console.log(' Please answer "y" or "n".');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function askFreeText(rl, question) {
|
|
35
|
+
const answer = (await ask(rl, `${question}\n> `)).trim();
|
|
36
|
+
return answer || "No details provided.";
|
|
37
|
+
}
|
|
38
|
+
// ─── Annotation ───────────────────────────────────────────────────────────────
|
|
39
|
+
export function buildAnnotation(ctx) {
|
|
40
|
+
return `
|
|
41
|
+
## Business Context
|
|
42
|
+
|
|
43
|
+
**Business value:** ${ctx.businessValue}
|
|
44
|
+
|
|
45
|
+
**Client impact:** ${ctx.hasClientImpact ? `Yes — ${ctx.clientImpactDetail}` : "No"}
|
|
46
|
+
|
|
47
|
+
**Tested:** ${ctx.isTested ? `Yes — ${ctx.testingDetail}` : "No"}
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
51
|
+
async function main() {
|
|
52
|
+
const changesetDir = getChangesetDir();
|
|
53
|
+
const before = new Set(getChangesetFiles(changesetDir));
|
|
54
|
+
console.log("\n📝 Running changeset add...\n");
|
|
55
|
+
const result = spawnSync("npx", ["changeset", "add"], { stdio: "inherit" });
|
|
56
|
+
if (result.status !== 0) {
|
|
57
|
+
console.error("\n❌ changeset add failed. Exiting.");
|
|
58
|
+
process.exit(result.status ?? 1);
|
|
59
|
+
}
|
|
60
|
+
const after = getChangesetFiles(changesetDir);
|
|
61
|
+
const newFiles = after.filter((f) => !before.has(f));
|
|
62
|
+
if (newFiles.length === 0) {
|
|
63
|
+
console.log("\nNo new changeset file detected — nothing to annotate.");
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
const newFile = resolve(changesetDir, newFiles[0]);
|
|
67
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
68
|
+
console.log("\n─────────────────────────────────────────────");
|
|
69
|
+
console.log(" 📋 A few more questions for the changelog");
|
|
70
|
+
console.log("─────────────────────────────────────────────\n");
|
|
71
|
+
const businessValue = await askFreeText(rl, "1. What value does this change bring to the business?");
|
|
72
|
+
console.log();
|
|
73
|
+
const hasClientImpact = await askYesNo(rl, "2. Does this carry client impact?");
|
|
74
|
+
let clientImpactDetail = "";
|
|
75
|
+
if (hasClientImpact) {
|
|
76
|
+
clientImpactDetail = await askFreeText(rl, " Please describe the client impact:");
|
|
77
|
+
}
|
|
78
|
+
console.log();
|
|
79
|
+
const isTested = await askYesNo(rl, "3. Has this been tested?");
|
|
80
|
+
let testingDetail = "";
|
|
81
|
+
if (isTested) {
|
|
82
|
+
testingDetail = await askFreeText(rl, " Briefly describe how it was tested:");
|
|
83
|
+
}
|
|
84
|
+
rl.close();
|
|
85
|
+
const existingContent = readFileSync(newFile, "utf8");
|
|
86
|
+
const annotation = buildAnnotation({
|
|
87
|
+
businessValue,
|
|
88
|
+
hasClientImpact,
|
|
89
|
+
clientImpactDetail,
|
|
90
|
+
isTested,
|
|
91
|
+
testingDetail,
|
|
92
|
+
});
|
|
93
|
+
writeFileSync(newFile, existingContent.trimEnd() + "\n" + annotation);
|
|
94
|
+
console.log(`\n✅ Changeset annotated: ${newFiles[0]}\n`);
|
|
95
|
+
}
|
|
96
|
+
main().catch((err) => {
|
|
97
|
+
console.error(err);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cstm-changeset",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A changeset add wrapper that prompts for structured business context",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"cstm-changeset": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@changesets/cli": ">=2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@changesets/cli": "^2.27.0",
|
|
24
|
+
"@types/node": "^20.0.0",
|
|
25
|
+
"tsx": "^4.0.0",
|
|
26
|
+
"typescript": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"changesets",
|
|
30
|
+
"changelog",
|
|
31
|
+
"cli"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|