@eclipse-docks/cli 0.7.114

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/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @eclipse-docks/cli
2
+
3
+ Developer CLI for apps built on Eclipse Docks.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @eclipse-docks/cli release # bump patch, prompt for release notes
9
+ npx @eclipse-docks/cli release minor # bump minor
10
+ npx @eclipse-docks/cli release major -m "notes" # bump major with inline release notes
11
+ npx @eclipse-docks/cli release v1.2.3 # explicit version
12
+ npx @eclipse-docks/cli release --dry-run # print what would be committed
13
+ npx @eclipse-docks/cli release --no-push # commit locally without pushing
14
+ ```
15
+
16
+ ## `release`
17
+
18
+ Creates an empty commit whose subject is the next version (`vX.Y.Z`) and pushes it.
19
+ Pair this with a CI workflow that tags/releases on that commit subject (see
20
+ [`.github/workflows/app-release.yml`](../../.github/workflows/app-release.yml) for a
21
+ reusable one) — the commit body becomes the release notes.
22
+
23
+ If no release notes are given via `-m`, an end-user-facing summary is generated from the
24
+ commits since the last version, grouped under **Features**, **Fixes**, **Docs**, and
25
+ **Chore/Refactor**, and offered as the default (press Enter on an empty first line to
26
+ accept it).
package/bin.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { commands } from './src/index.js';
3
+
4
+ async function main() {
5
+ const [name, ...rest] = process.argv.slice(2);
6
+ const command = name ? commands[name] : undefined;
7
+
8
+ if (!command) {
9
+ console.error(
10
+ `Usage: docks <command> [...args]\n\nCommands:\n${Object.keys(commands)
11
+ .map((c) => ` ${c}`)
12
+ .join('\n')}`,
13
+ );
14
+ process.exit(name ? 1 : 0);
15
+ }
16
+
17
+ await command(rest);
18
+ }
19
+
20
+ main().catch((err) => {
21
+ console.error(err instanceof Error ? err.message : err);
22
+ process.exit(1);
23
+ });
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@eclipse-docks/cli",
3
+ "version": "0.7.114",
4
+ "description": "Developer CLI for Eclipse Docks apps (release automation and more)",
5
+ "type": "module",
6
+ "bin": {
7
+ "docks": "./bin.js"
8
+ },
9
+ "files": [
10
+ "bin.js",
11
+ "src"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/eclipse-docks/core"
16
+ },
17
+ "license": "EPL-2.0"
18
+ }
@@ -0,0 +1,162 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createInterface } from 'node:readline/promises';
3
+
4
+ const VERSION_RE = /^v\d+\.\d+\.\d+$/;
5
+
6
+ function git(args, cwd) {
7
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
8
+ if (result.status !== 0) {
9
+ throw new Error(`git ${args.join(' ')} failed: ${result.stderr.trim()}`);
10
+ }
11
+ return result.stdout.trim();
12
+ }
13
+
14
+ function gitOrNull(args, cwd) {
15
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
16
+ return result.status === 0 ? result.stdout.trim() : null;
17
+ }
18
+
19
+ function parseArgs(argv) {
20
+ const opts = { bump: 'patch', version: '', notes: '', dryRun: false, push: true };
21
+
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const arg = argv[i];
24
+ if (arg === 'major' || arg === 'minor' || arg === 'patch') {
25
+ opts.bump = arg;
26
+ } else if (VERSION_RE.test(arg)) {
27
+ opts.version = arg;
28
+ } else if (arg === '-m' || arg === '--message') {
29
+ opts.notes = argv[++i] ?? '';
30
+ } else if (arg === '-n' || arg === '--dry-run') {
31
+ opts.dryRun = true;
32
+ } else if (arg === '-p' || arg === '--push') {
33
+ opts.push = true;
34
+ } else if (arg === '--no-push') {
35
+ opts.push = false;
36
+ } else {
37
+ throw new Error(
38
+ `Unknown argument: ${arg}\nUsage: docks release [major|minor|patch|vX.Y.Z] [-m "release notes"] [--dry-run] [--no-push]`,
39
+ );
40
+ }
41
+ }
42
+
43
+ return opts;
44
+ }
45
+
46
+ function findLastVersion(cwd) {
47
+ const log = gitOrNull(['log', '--pretty=%s'], cwd);
48
+ if (!log) return null;
49
+ for (const subject of log.split('\n')) {
50
+ if (VERSION_RE.test(subject)) return subject;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function bumpVersion(last, bump) {
56
+ const [major, minor, patch] = last.slice(1).split('.').map(Number);
57
+ switch (bump) {
58
+ case 'major':
59
+ return `v${major + 1}.0.0`;
60
+ case 'minor':
61
+ return `v${major}.${minor + 1}.0`;
62
+ default:
63
+ return `v${major}.${minor}.${patch + 1}`;
64
+ }
65
+ }
66
+
67
+ function generateSummary(cwd, last) {
68
+ // find the commit whose subject *is* the last version, not a tag (tags may not exist locally)
69
+ const lastCommit = last ? gitOrNull(['log', '-E', '--pretty=%H', '-1', `--grep=^${last}$`], cwd) : null;
70
+ const range = lastCommit ? `${lastCommit}..HEAD` : 'HEAD';
71
+
72
+ const log = gitOrNull(['log', range, '--no-merges', '--pretty=%s'], cwd) ?? '';
73
+ const buckets = { features: [], fixes: [], docs: [], chores: [] };
74
+
75
+ for (const subject of log.split('\n')) {
76
+ if (!subject || VERSION_RE.test(subject)) continue;
77
+
78
+ const stripped = subject.includes(': ') ? subject.slice(subject.indexOf(': ') + 2) : subject;
79
+ const text = stripped.charAt(0).toUpperCase() + stripped.slice(1);
80
+
81
+ if (/^(feat|perf)/.test(subject)) buckets.features.push(text);
82
+ else if (/^fix/.test(subject)) buckets.fixes.push(text);
83
+ else if (/^docs/.test(subject)) buckets.docs.push(text);
84
+ else if (/^(chore|ci|build|test|refactor|style)/.test(subject)) buckets.chores.push(text);
85
+ else buckets.features.push(text);
86
+ }
87
+
88
+ const sections = [];
89
+ const render = (title, items) => {
90
+ if (items.length) sections.push(`${title}\n${items.map((i) => `- ${i}`).join('\n')}`);
91
+ };
92
+ render('Features', buckets.features);
93
+ render('Fixes', buckets.fixes);
94
+ render('Docs', buckets.docs);
95
+ render('Chore/Refactor', buckets.chores);
96
+
97
+ return sections.join('\n\n');
98
+ }
99
+
100
+ async function promptNotes(version, summary) {
101
+ console.log(`Enter release notes for ${version} (end with an empty line).`);
102
+ if (summary) {
103
+ console.log('Press Enter on an empty first line to use this auto-generated summary:');
104
+ console.log(summary.replace(/^/gm, ' '));
105
+ }
106
+
107
+ const rl = createInterface({ input: process.stdin });
108
+ const lines = [];
109
+ for await (const line of rl) {
110
+ if (line === '') break;
111
+ lines.push(line);
112
+ }
113
+ rl.close();
114
+
115
+ return lines.length ? lines.join('\n') + '\n' : summary ? summary + '\n' : '';
116
+ }
117
+
118
+ export async function release(argv, cwd = process.cwd()) {
119
+ const opts = parseArgs(argv);
120
+
121
+ if (!opts.dryRun && git(['status', '--porcelain'], cwd) !== '') {
122
+ throw new Error('Working tree is not clean. Commit or stash your changes first.');
123
+ }
124
+
125
+ const last = findLastVersion(cwd);
126
+ let version = opts.version;
127
+ if (!version) {
128
+ version = bumpVersion(last ?? 'v0.0.0', opts.bump);
129
+ console.log(`Last version commit: ${last ?? 'v0.0.0'}`);
130
+ }
131
+
132
+ if (gitOrNull(['rev-parse', version], cwd)) {
133
+ throw new Error(`Tag ${version} already exists.`);
134
+ }
135
+
136
+ let notes = opts.notes;
137
+ if (!notes) {
138
+ const summary = generateSummary(cwd, last);
139
+ notes = await promptNotes(version, summary);
140
+ }
141
+
142
+ if (opts.dryRun) {
143
+ console.log(`\n[dry run] would create empty commit:\n ${version}\n`);
144
+ console.log(notes.replace(/^/gm, ' '));
145
+ if (opts.push) {
146
+ const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
147
+ console.log(`[dry run] would push to origin ${branch}`);
148
+ }
149
+ return;
150
+ }
151
+
152
+ git(['commit', '--allow-empty', '-m', version, '-m', notes], cwd);
153
+
154
+ const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
155
+ if (opts.push) {
156
+ git(['push', 'origin', branch], cwd);
157
+ console.log(`\nPushed ${version} to origin/${branch}; the release workflow will take over.`);
158
+ } else {
159
+ console.log(`\nCreated release commit for ${version}. Push it to trigger the release:`);
160
+ console.log(` git push origin ${branch}`);
161
+ }
162
+ }
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { release } from './commands/release.js';
2
+
3
+ export const commands = {
4
+ release,
5
+ };