dev-dump 0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -0
  3. package/index.js +529 -0
  4. package/package.json +42 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # dev-dump
2
+
3
+ Flexible, semver-compliant snapshot & prerelease version generator.
4
+
5
+ Generate deterministic, sortable prerelease versions for CI/CD pipelines, canary releases, feature branch deploys, and multi-stage release workflows — with zero dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g dev-dump # global
11
+ npm install -D dev-dump # devDependency
12
+ npx dev-dump # one-off
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # Default: {preid}.{timestamp}.{hash}
19
+ npx dev-dump
20
+ # → 1.2.3-dev.202603171950.abc1234
21
+
22
+ # Alpha prerelease
23
+ npx dev-dump -p alpha
24
+ # → 1.2.3-alpha.202603171950.abc1234
25
+
26
+ # Auto-increment from npm registry
27
+ npx dev-dump -f "{preid}.{inc}" --from-registry
28
+ # → 1.2.3-dev.0 (next time: dev.1, dev.2, …)
29
+
30
+ # Canary release
31
+ npx dev-dump -f "canary.{hash}"
32
+ # → 1.2.3-canary.abc1234
33
+
34
+ # Feature branch snapshot
35
+ npx dev-dump -f "{branch}.{timestamp}.{hash}"
36
+ # → 1.2.3-feat-login.202603171950.abc1234
37
+
38
+ # Dry run (don't write package.json)
39
+ npx dev-dump --dry-run
40
+
41
+ # Capture for CI pipelines
42
+ VERSION=$(npx dev-dump --stdout-only)
43
+ echo "Publishing $VERSION"
44
+ ```
45
+
46
+ ## Template Variables
47
+
48
+ The `-f` / `--format` flag defines the **prerelease** portion of the version. The base `MAJOR.MINOR.PATCH` is read from `package.json` (or overridden with `--base`).
49
+
50
+ | Variable | Description | Example |
51
+ |---|---|---|
52
+ | `{preid}` | Prerelease identifier (`--preid`) | `dev`, `alpha`, `beta`, `rc` |
53
+ | `{timestamp}` | `YYYYMMDDHHmm` | `202603171950` |
54
+ | `{ts}` | `YYMMDDHHmm` | `2603171950` |
55
+ | `{date}` | `YYYYMMDD` | `20260317` |
56
+ | `{time}` | `HHmm` | `1950` |
57
+ | `{hash}` | Git short hash (7 chars) | `abc1234` |
58
+ | `{hash-long}` | Git full hash (40 chars) | `abc1234567890…` |
59
+ | `{inc}` | Auto-increment counter (needs `--from-registry`) | `0`, `1`, `2` |
60
+ | `{branch}` | Git branch name (sanitised) | `feat-login` |
61
+ | `{base}` | `MAJOR.MINOR.PATCH` | `1.2.3` |
62
+
63
+ ## Options
64
+
65
+ | Option | Short | Default | Description |
66
+ |---|---|---|---|
67
+ | `--preid <id>` | `-p` | `dev` | Prerelease identifier |
68
+ | `--format <tpl>` | `-f` | `{preid}.{timestamp}.{hash}` | Prerelease template |
69
+ | `--base <ver>` | `-b` | from `package.json` | Override base version |
70
+ | `--from-registry` | `-r` | `false` | Resolve base / `{inc}` from npm registry |
71
+ | `--tag <tag>` | `-t` | `latest` | dist-tag for `--from-registry` |
72
+ | `--dry-run` | `-d` | `false` | Print version without writing files |
73
+ | `--stdout-only` | `-s` | `false` | Only output version to stdout |
74
+ | `--no-git-tag` | | `true` | Don't create a git tag |
75
+ | `--cwd <path>` | | `.` | Working directory |
76
+ | `--help` | `-h` | | Show help |
77
+ | `--version` | `-v` | | Show tool version |
78
+
79
+ ## Common Recipes
80
+
81
+ ### CI/CD Snapshot (default)
82
+
83
+ ```bash
84
+ npx dev-dump
85
+ # 1.2.3-dev.202603171950.abc1234
86
+ npm publish --tag dev
87
+ ```
88
+
89
+ ### Standard Prerelease Cycle
90
+
91
+ ```bash
92
+ # alpha phase
93
+ npx dev-dump -p alpha -f "{preid}.{inc}" -r
94
+ # 1.2.3-alpha.0 → alpha.1 → alpha.2
95
+
96
+ # beta phase
97
+ npx dev-dump -p beta -f "{preid}.{inc}" -r
98
+ # 1.2.3-beta.0 → beta.1
99
+
100
+ # release candidate
101
+ npx dev-dump -p rc -f "{preid}.{inc}" -r
102
+ # 1.2.3-rc.0 → rc.1
103
+
104
+ # final release
105
+ npm version 1.2.3
106
+ ```
107
+
108
+ ### Feature Branch Deploy
109
+
110
+ ```bash
111
+ npx dev-dump -f "{branch}.{timestamp}.{hash}"
112
+ # 1.2.3-feat-login.202603171950.abc1234
113
+ npm publish --tag "$(git branch --show-current | tr '/' '-')"
114
+ ```
115
+
116
+ ### Canary Release
117
+
118
+ ```bash
119
+ npx dev-dump -f "canary.{hash}"
120
+ # 1.2.3-canary.abc1234
121
+ npm publish --tag canary
122
+ ```
123
+
124
+ ### Override Base for Next Major
125
+
126
+ ```bash
127
+ npx dev-dump -b 2.0.0 -p alpha
128
+ # 2.0.0-alpha.202603171950.abc1234
129
+ ```
130
+
131
+ ### GitHub Actions Example
132
+
133
+ ```yaml
134
+ - name: Publish snapshot
135
+ run: |
136
+ VERSION=$(npx dev-dump --stdout-only)
137
+ npm version "$VERSION" --no-git-tag-version
138
+ npm publish --tag dev
139
+ env:
140
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
141
+ ```
142
+
143
+ ## SemVer Compliance
144
+
145
+ All generated versions are fully compliant with [Semantic Versioning 2.0.0](https://semver.org/):
146
+
147
+ - Prerelease identifiers use only `[0-9A-Za-z-]`
148
+ - No leading zeros on numeric identifiers
149
+ - Empty identifiers are automatically removed
150
+ - Branch names and custom strings are sanitised (`/` `_` → `-`)
151
+ - Versions are validated with a strict SemVer regex before output
152
+ - Ordering is verified: timestamps naturally sort correctly, `{inc}` is monotonically increasing
153
+
154
+ ## Programmatic API
155
+
156
+ ```js
157
+ const {
158
+ parseSemver,
159
+ isValidSemver,
160
+ semverCompare,
161
+ sanitise,
162
+ interpolate,
163
+ buildVariables,
164
+ resolveInc,
165
+ resolveBaseFromRegistry,
166
+ } = require('dev-dump');
167
+
168
+ // Parse
169
+ parseSemver('1.2.3-dev.1');
170
+ // → { major: 1, minor: 2, patch: 3, prerelease: 'dev.1', build: '', base: '1.2.3' }
171
+
172
+ // Compare
173
+ semverCompare('1.0.0-alpha', '1.0.0-beta'); // → -1
174
+
175
+ // Sanitise branch names
176
+ sanitise('feature/my_branch'); // → 'feature-my-branch'
177
+
178
+ // Interpolate custom template
179
+ interpolate('{preid}.{timestamp}', { preid: 'dev', timestamp: '202603171950' });
180
+ // → 'dev.202603171950'
181
+ ```
182
+
183
+ ## Zero Dependencies
184
+
185
+ This package has **no runtime dependencies**. It includes a built-in semver parser, comparator, and validator — everything needed in a single `index.js` file.
186
+
187
+ ## License
188
+
189
+ MIT
package/index.js ADDED
@@ -0,0 +1,529 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * dev-dump
5
+ *
6
+ * A flexible CLI tool for generating semver-compliant snapshot / prerelease
7
+ * version strings. Supports customisable templates with variables such as
8
+ * {preid}, {timestamp}, {hash}, {branch}, {inc} and more.
9
+ *
10
+ * Usage:
11
+ * npx dev-dump [options]
12
+ *
13
+ * Options:
14
+ * -p, --preid <id> Prerelease identifier (default: "dev")
15
+ * -f, --format <tpl> Prerelease template (default: "{preid}.{timestamp}.{hash}")
16
+ * -b, --base <ver> Override base MAJOR.MINOR.PATCH
17
+ * -r, --from-registry Resolve base / inc from the npm registry
18
+ * -t, --tag <tag> dist-tag used with --from-registry (default: "latest")
19
+ * -d, --dry-run Print the version without writing package.json
20
+ * -s, --stdout-only Only print the version string to stdout (implies --dry-run)
21
+ * --no-git-tag Skip creating a git tag (default: true)
22
+ * --cwd <path> Working directory (default: ".")
23
+ * -h, --help Show this help message
24
+ * -v, --version Show tool version
25
+ *
26
+ * Template variables:
27
+ * {preid} --preid value e.g. dev, alpha, beta
28
+ * {timestamp} YYYYMMDDHHmm e.g. 202603171950
29
+ * {ts} YYMMDDHHmm e.g. 2603171950
30
+ * {date} YYYYMMDD e.g. 20260317
31
+ * {time} HHmm e.g. 1950
32
+ * {hash} git short hash (7) e.g. abc1234
33
+ * {hash-long} git full hash (40) e.g. abc1234...
34
+ * {inc} auto-increment (needs --from-registry or local scan)
35
+ * {branch} current git branch (sanitised) e.g. feat-login
36
+ * {base} MAJOR.MINOR.PATCH e.g. 1.2.3
37
+ *
38
+ * @license MIT
39
+ */
40
+
41
+ 'use strict';
42
+
43
+ const { execSync } = require('child_process');
44
+ const fs = require('fs');
45
+ const path = require('path');
46
+
47
+ // ────────────────────────────────────────────────────────────────────────────
48
+ // Minimal semver helpers (no external dependency)
49
+ // ────────────────────────────────────────────────────────────────────────────
50
+
51
+ const SEMVER_RE =
52
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[\da-zA-Z-]*[a-zA-Z-][\da-zA-Z-]*)(?:\.(?:0|[1-9]\d*|[\da-zA-Z-]*[a-zA-Z-][\da-zA-Z-]*))*))?(?:\+([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$/;
53
+
54
+ function parseSemver(v) {
55
+ const m = String(v).replace(/^[=v]+/, '').match(SEMVER_RE);
56
+ if (!m) return null;
57
+ return {
58
+ major: Number(m[1]),
59
+ minor: Number(m[2]),
60
+ patch: Number(m[3]),
61
+ prerelease: m[4] || '',
62
+ build: m[5] || '',
63
+ base: `${m[1]}.${m[2]}.${m[3]}`,
64
+ raw: m[0],
65
+ };
66
+ }
67
+
68
+ function isValidSemver(v) {
69
+ return parseSemver(v) !== null;
70
+ }
71
+
72
+ /**
73
+ * Compare two semver strings. Returns -1 | 0 | 1.
74
+ * Implements the full SemVer 2.0.0 precedence rules including prerelease.
75
+ */
76
+ function semverCompare(a, b) {
77
+ const pa = parseSemver(a);
78
+ const pb = parseSemver(b);
79
+ if (!pa || !pb) return 0;
80
+
81
+ // Compare MAJOR.MINOR.PATCH
82
+ for (const k of ['major', 'minor', 'patch']) {
83
+ if (pa[k] > pb[k]) return 1;
84
+ if (pa[k] < pb[k]) return -1;
85
+ }
86
+
87
+ // Both have no prerelease → equal
88
+ if (!pa.prerelease && !pb.prerelease) return 0;
89
+ // One has prerelease, the other doesn't → the one WITHOUT is greater
90
+ if (!pa.prerelease) return 1;
91
+ if (!pb.prerelease) return -1;
92
+
93
+ // Compare prerelease identifiers
94
+ const aParts = pa.prerelease.split('.');
95
+ const bParts = pb.prerelease.split('.');
96
+ const len = Math.max(aParts.length, bParts.length);
97
+ for (let i = 0; i < len; i++) {
98
+ if (i >= aParts.length) return -1; // fewer fields → lower
99
+ if (i >= bParts.length) return 1;
100
+ const ai = aParts[i];
101
+ const bi = bParts[i];
102
+ const aNum = /^\d+$/.test(ai);
103
+ const bNum = /^\d+$/.test(bi);
104
+ if (aNum && bNum) {
105
+ const diff = Number(ai) - Number(bi);
106
+ if (diff !== 0) return diff > 0 ? 1 : -1;
107
+ } else if (aNum) {
108
+ return -1; // numeric < alphanumeric
109
+ } else if (bNum) {
110
+ return 1;
111
+ } else {
112
+ if (ai > bi) return 1;
113
+ if (ai < bi) return -1;
114
+ }
115
+ }
116
+ return 0;
117
+ }
118
+
119
+ // ────────────────────────────────────────────────────────────────────────────
120
+ // Helpers
121
+ // ────────────────────────────────────────────────────────────────────────────
122
+
123
+ function run(cmd, cwd) {
124
+ try {
125
+ return execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'] })
126
+ .toString()
127
+ .trim();
128
+ } catch {
129
+ return '';
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Sanitise a string so that it only contains characters allowed by SemVer in
135
+ * prerelease identifiers: [0-9A-Za-z-].
136
+ */
137
+ function sanitise(str) {
138
+ return str
139
+ .replace(/[^0-9A-Za-z-]+/g, '-') // replace illegal chars
140
+ .replace(/--+/g, '-') // collapse repeated dashes
141
+ .replace(/^-|-$/g, ''); // trim leading/trailing dash
142
+ }
143
+
144
+ /**
145
+ * Remove leading zeros from a pure-numeric identifier (SemVer rule: numeric
146
+ * identifiers MUST NOT include leading zeroes).
147
+ * Non-numeric identifiers are returned as-is.
148
+ */
149
+ function stripLeadingZero(id) {
150
+ if (/^\d+$/.test(id)) {
151
+ return String(Number(id));
152
+ }
153
+ return id;
154
+ }
155
+
156
+ /**
157
+ * Pad a number to `n` digits.
158
+ */
159
+ const pad = (v, n = 2) => String(v).padStart(n, '0');
160
+
161
+ /**
162
+ * Build a map of all template variables available for the current environment.
163
+ */
164
+ function buildVariables(opts) {
165
+ const now = new Date();
166
+ const yyyy = now.getFullYear();
167
+ const MM = pad(now.getMonth() + 1);
168
+ const dd = pad(now.getDate());
169
+ const HH = pad(now.getHours());
170
+ const mm = pad(now.getMinutes());
171
+
172
+ const timestamp = `${yyyy}${MM}${dd}${HH}${mm}`;
173
+ const ts = `${String(yyyy).slice(2)}${MM}${dd}${HH}${mm}`;
174
+ const date = `${yyyy}${MM}${dd}`;
175
+ const time = `${HH}${mm}`;
176
+
177
+ const hash = run('git rev-parse --short HEAD', opts.cwd);
178
+ const hashLong = run('git rev-parse HEAD', opts.cwd);
179
+ const rawBranch = run('git rev-parse --abbrev-ref HEAD', opts.cwd);
180
+ const branch = sanitise(rawBranch || 'unknown');
181
+
182
+ return {
183
+ preid: sanitise(opts.preid),
184
+ timestamp,
185
+ ts,
186
+ date,
187
+ time,
188
+ hash: hash || '0000000',
189
+ 'hash-long': hashLong || '0'.repeat(40),
190
+ branch,
191
+ base: opts._resolvedBase || '0.0.0',
192
+ inc: String(opts._resolvedInc ?? 0),
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Resolve {inc} by scanning the npm registry for existing versions that share
198
+ * the same base and preid pattern, then returning max + 1.
199
+ */
200
+ function resolveInc(pkgName, base, preid, cwd) {
201
+ if (!pkgName) return 0;
202
+ const raw = run(`npm view ${pkgName} versions --json 2>/dev/null`, cwd);
203
+ if (!raw) return 0;
204
+
205
+ let versions;
206
+ try {
207
+ versions = JSON.parse(raw);
208
+ } catch {
209
+ return 0;
210
+ }
211
+ if (!Array.isArray(versions)) {
212
+ versions = [versions]; // single version comes as string
213
+ }
214
+
215
+ // Filter to versions whose base matches and prerelease starts with preid
216
+ const prefix = `${base}-${preid}.`;
217
+ let maxInc = -1;
218
+ for (const v of versions) {
219
+ if (!v.startsWith(prefix)) continue;
220
+ const rest = v.slice(prefix.length);
221
+ // Try to extract leading numeric inc value
222
+ const firstDotOrEnd = rest.indexOf('.');
223
+ const candidate = firstDotOrEnd === -1 ? rest : rest.slice(0, firstDotOrEnd);
224
+ if (/^\d+$/.test(candidate)) {
225
+ const n = Number(candidate);
226
+ if (n > maxInc) maxInc = n;
227
+ }
228
+ }
229
+ return maxInc + 1;
230
+ }
231
+
232
+ /**
233
+ * Resolve the base version from the npm registry when --from-registry is set.
234
+ */
235
+ function resolveBaseFromRegistry(pkgName, tag, cwd) {
236
+ if (!pkgName) return null;
237
+ const v = run(
238
+ `npm view ${pkgName}@${tag} version 2>/dev/null`,
239
+ cwd,
240
+ );
241
+ if (v && isValidSemver(v)) {
242
+ return parseSemver(v).base;
243
+ }
244
+ return null;
245
+ }
246
+
247
+ /**
248
+ * Interpolate the format template with the variable map, then validate &
249
+ * clean each prerelease identifier.
250
+ */
251
+ function interpolate(format, vars) {
252
+ let result = format.replace(/\{([^}]+)\}/g, (_, key) => {
253
+ const k = key.trim().toLowerCase();
254
+ if (vars.hasOwnProperty(k)) return vars[k];
255
+ // Unknown variable – leave as sanitised literal
256
+ return sanitise(key);
257
+ });
258
+
259
+ // Split into identifiers, sanitise each, drop empties
260
+ const ids = result.split('.').map(id => {
261
+ const s = sanitise(id);
262
+ return stripLeadingZero(s);
263
+ }).filter(Boolean);
264
+
265
+ return ids.join('.');
266
+ }
267
+
268
+ // ────────────────────────────────────────────────────────────────────────────
269
+ // CLI argument parser
270
+ // ────────────────────────────────────────────────────────────────────────────
271
+
272
+ function parseArgs(argv) {
273
+ const opts = {
274
+ preid: 'dev',
275
+ format: '{preid}.{timestamp}.{hash}',
276
+ base: null,
277
+ fromRegistry: false,
278
+ tag: 'latest',
279
+ dryRun: false,
280
+ stdoutOnly: false,
281
+ noGitTag: true,
282
+ cwd: process.cwd(),
283
+ help: false,
284
+ showVersion: false,
285
+ };
286
+
287
+ const args = argv.slice(2);
288
+ for (let i = 0; i < args.length; i++) {
289
+ const a = args[i];
290
+ switch (a) {
291
+ case '-p': case '--preid':
292
+ opts.preid = args[++i] || 'dev';
293
+ break;
294
+ case '-f': case '--format':
295
+ opts.format = args[++i] || opts.format;
296
+ break;
297
+ case '-b': case '--base':
298
+ opts.base = args[++i] || null;
299
+ break;
300
+ case '-r': case '--from-registry':
301
+ opts.fromRegistry = true;
302
+ break;
303
+ case '-t': case '--tag':
304
+ opts.tag = args[++i] || 'latest';
305
+ break;
306
+ case '-d': case '--dry-run':
307
+ opts.dryRun = true;
308
+ break;
309
+ case '-s': case '--stdout-only':
310
+ opts.stdoutOnly = true;
311
+ opts.dryRun = true;
312
+ break;
313
+ case '--no-git-tag':
314
+ opts.noGitTag = true;
315
+ break;
316
+ case '--cwd':
317
+ opts.cwd = path.resolve(args[++i] || '.');
318
+ break;
319
+ case '-h': case '--help':
320
+ opts.help = true;
321
+ break;
322
+ case '-v': case '--version':
323
+ opts.showVersion = true;
324
+ break;
325
+ default:
326
+ // Positional: treat as preid shorthand
327
+ if (!a.startsWith('-')) {
328
+ opts.preid = a;
329
+ }
330
+ break;
331
+ }
332
+ }
333
+ return opts;
334
+ }
335
+
336
+ // ────────────────────────────────────────────────────────────────────────────
337
+ // Help text
338
+ // ────────────────────────────────────────────────────────────────────────────
339
+
340
+ const HELP = `
341
+ dev-dump — Flexible semver-compliant snapshot version generator
342
+
343
+ Usage:
344
+ npx dev-dump [options] [preid]
345
+
346
+ Options:
347
+ -p, --preid <id> Prerelease identifier (default: "dev")
348
+ -f, --format <tpl> Prerelease template (default: "{preid}.{timestamp}.{hash}")
349
+ -b, --base <ver> Override base MAJOR.MINOR.PATCH
350
+ -r, --from-registry Resolve base / inc from npm registry
351
+ -t, --tag <tag> dist-tag for --from-registry (default: "latest")
352
+ -d, --dry-run Print version without modifying files
353
+ -s, --stdout-only Only output the version string (implies --dry-run)
354
+ --no-git-tag Do not create a git tag (default)
355
+ --cwd <path> Working directory (default: ".")
356
+ -h, --help Show this help message
357
+ -v, --version Show tool version
358
+
359
+ Template variables:
360
+ {preid} Prerelease id dev, alpha, beta, rc, canary
361
+ {timestamp} YYYYMMDDHHmm 202603171950
362
+ {ts} YYMMDDHHmm 2603171950
363
+ {date} YYYYMMDD 20260317
364
+ {time} HHmm 1950
365
+ {hash} Git short hash (7) abc1234
366
+ {hash-long} Git full hash (40) abc1234567890...
367
+ {inc} Auto-increment counter 0, 1, 2, ... (needs --from-registry)
368
+ {branch} Git branch (sanitised) feat-login
369
+ {base} MAJOR.MINOR.PATCH 1.2.3
370
+
371
+ Examples:
372
+ npx dev-dump # 1.2.3-dev.202603171950.abc1234
373
+ npx dev-dump -p alpha # 1.2.3-alpha.202603171950.abc1234
374
+ npx dev-dump -f "{preid}.{inc}" -r # 1.2.3-dev.0 (auto-increment from registry)
375
+ npx dev-dump -f "{branch}.{ts}.{hash}" # 1.2.3-feat-login.2603171950.abc1234
376
+ npx dev-dump -f "canary.{hash}" # 1.2.3-canary.abc1234
377
+ npx dev-dump -b 2.0.0 -p beta # 2.0.0-beta.202603171950.abc1234
378
+ npx dev-dump --dry-run # prints version without writing
379
+ npx dev-dump --stdout-only # only outputs version string
380
+ `.trim();
381
+
382
+ // ────────────────────────────────────────────────────────────────────────────
383
+ // Main
384
+ // ────────────────────────────────────────────────────────────────────────────
385
+
386
+ function main() {
387
+ const opts = parseArgs(process.argv);
388
+
389
+ // --help
390
+ if (opts.help) {
391
+ console.log(HELP);
392
+ process.exit(0);
393
+ }
394
+
395
+ // --version
396
+ if (opts.showVersion) {
397
+ const self = path.join(__dirname, 'package.json');
398
+ if (fs.existsSync(self)) {
399
+ console.log(JSON.parse(fs.readFileSync(self, 'utf8')).version);
400
+ } else {
401
+ console.log('0.0.0');
402
+ }
403
+ process.exit(0);
404
+ }
405
+
406
+ // 1. Locate package.json
407
+ const pkgPath = path.resolve(opts.cwd, 'package.json');
408
+ let pkg = {};
409
+ let hasPkg = false;
410
+ if (fs.existsSync(pkgPath)) {
411
+ try {
412
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
413
+ hasPkg = true;
414
+ } catch (e) {
415
+ error(`Failed to parse ${pkgPath}: ${e.message}`);
416
+ }
417
+ }
418
+
419
+ // 2. Determine base version
420
+ let base;
421
+ if (opts.base) {
422
+ // User-supplied
423
+ const p = parseSemver(opts.base);
424
+ if (!p) error(`Invalid --base version: ${opts.base}`);
425
+ base = p.base;
426
+ } else if (opts.fromRegistry && pkg.name) {
427
+ const regBase = resolveBaseFromRegistry(pkg.name, opts.tag, opts.cwd);
428
+ if (regBase) {
429
+ base = regBase;
430
+ if (!opts.stdoutOnly) {
431
+ console.error(`ℹ Base from registry (${opts.tag}): ${base}`);
432
+ }
433
+ }
434
+ }
435
+ if (!base && hasPkg && pkg.version) {
436
+ const p = parseSemver(pkg.version);
437
+ if (p) base = p.base;
438
+ }
439
+ if (!base) {
440
+ error(
441
+ 'Could not determine base version. Provide --base, ensure package.json has a version, or use --from-registry.',
442
+ );
443
+ }
444
+ opts._resolvedBase = base;
445
+
446
+ // 3. Resolve {inc} if the template uses it
447
+ if (opts.format.includes('{inc}')) {
448
+ if (!pkg.name) {
449
+ error('{inc} requires a package name in package.json (or --from-registry).');
450
+ }
451
+ opts._resolvedInc = resolveInc(pkg.name, base, sanitise(opts.preid), opts.cwd);
452
+ if (!opts.stdoutOnly) {
453
+ console.error(`ℹ Resolved {inc} = ${opts._resolvedInc}`);
454
+ }
455
+ }
456
+
457
+ // 4. Build variable map & interpolate
458
+ const vars = buildVariables(opts);
459
+ const prerelease = interpolate(opts.format, vars);
460
+
461
+ if (!prerelease) {
462
+ error('Template produced an empty prerelease string.');
463
+ }
464
+
465
+ const newVersion = `${base}-${prerelease}`;
466
+
467
+ // 5. Validate
468
+ if (!isValidSemver(newVersion)) {
469
+ error(`Generated version is not valid semver: ${newVersion}`);
470
+ }
471
+
472
+ // 6. Verify ordering: new version should be >= base-prerelease level
473
+ // (this is a sanity check, not a hard gate)
474
+ if (hasPkg && pkg.version && isValidSemver(pkg.version)) {
475
+ if (semverCompare(newVersion, pkg.version) < 0) {
476
+ if (!opts.stdoutOnly) {
477
+ console.error(
478
+ `⚠ Warning: generated version ${newVersion} is lower than current ${pkg.version}`,
479
+ );
480
+ }
481
+ }
482
+ }
483
+
484
+ // 7. Output
485
+ if (opts.stdoutOnly) {
486
+ process.stdout.write(newVersion);
487
+ process.exit(0);
488
+ }
489
+
490
+ if (opts.dryRun) {
491
+ console.log(newVersion);
492
+ process.exit(0);
493
+ }
494
+
495
+ // Write to package.json
496
+ if (!hasPkg) {
497
+ error('No package.json found. Use --dry-run or --stdout-only.');
498
+ }
499
+
500
+ pkg.version = newVersion;
501
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
502
+ console.log(`✔ ${newVersion}`);
503
+ }
504
+
505
+ function error(msg) {
506
+ console.error(`✖ ${msg}`);
507
+ process.exit(1);
508
+ }
509
+
510
+ // ────────────────────────────────────────────────────────────────────────────
511
+ // Export for programmatic use
512
+ // ────────────────────────────────────────────────────────────────────────────
513
+
514
+ module.exports = {
515
+ parseSemver,
516
+ isValidSemver,
517
+ semverCompare,
518
+ sanitise,
519
+ stripLeadingZero,
520
+ interpolate,
521
+ buildVariables,
522
+ resolveInc,
523
+ resolveBaseFromRegistry,
524
+ };
525
+
526
+ // Run when executed directly
527
+ if (require.main === module) {
528
+ main();
529
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "dev-dump",
3
+ "version": "0.0.1",
4
+ "description": "Zero-dependency CLI tool to generate timestamp and git-hash based development versions",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "dev-dump": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node test.js"
11
+ },
12
+ "keywords": [
13
+ "semver",
14
+ "version",
15
+ "snapshot",
16
+ "prerelease",
17
+ "dev",
18
+ "alpha",
19
+ "beta",
20
+ "canary",
21
+ "ci",
22
+ "npm-version",
23
+ "git",
24
+ "hash",
25
+ "timestamp",
26
+ "bump"
27
+ ],
28
+ "files": [
29
+ "index.js",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/gitaiQAQ/dev-dump.git"
36
+ },
37
+ "author": "gitaiQAQ",
38
+ "license": "MIT",
39
+ "engines": {
40
+ "node": ">=14.0.0"
41
+ }
42
+ }