@grest-ts/cli 0.0.27
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 +21 -0
- package/README.md +35 -0
- package/index.mjs +262 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Grest Games OÜ
|
|
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,35 @@
|
|
|
1
|
+
<!-- GREST-TS-BANNER-START -->
|
|
2
|
+
> Part of the [grest-ts](https://github.com/grest-ts/grest-ts) framework.
|
|
3
|
+
> [Documentation](https://github.com/grest-ts/grest-ts#readme) | [All packages](https://github.com/grest-ts/grest-ts#package-reference)
|
|
4
|
+
<!-- GREST-TS-BANNER-END -->
|
|
5
|
+
|
|
6
|
+
# @grest-ts/cli
|
|
7
|
+
|
|
8
|
+
CLI for managing grest-ts in a project.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
grest-ts is single-version: every `@grest-ts/*` package in a project must be at the same version. Inter-package peer dependencies are pinned exactly, so bumping one package forces bumping all of them — and once a `package-lock.json` is in play, npm's resolver can't reconcile the cross-package pins through a partial update.
|
|
13
|
+
|
|
14
|
+
This CLI does the bump atomically.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Run from your project root (single-package or npm workspaces monorepo):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @grest-ts/cli upgrade # bumps to "latest" dist-tag
|
|
22
|
+
npx @grest-ts/cli upgrade 0.0.27 # bumps to a specific version
|
|
23
|
+
npx @grest-ts/cli upgrade next # bumps to the "next" dist-tag
|
|
24
|
+
npx @grest-ts/cli upgrade --dry-run # preview without writing
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What it does
|
|
28
|
+
|
|
29
|
+
1. Discovers every `package.json` (root + workspaces).
|
|
30
|
+
2. Collects every distinct `@grest-ts/*` dep referenced in `dependencies` and `devDependencies`.
|
|
31
|
+
3. Resolves the target version (concrete version or dist-tag).
|
|
32
|
+
4. Pre-flight: verifies every collected package is published at the target version. Fails fast if any are missing.
|
|
33
|
+
5. Rewrites every `@grest-ts/*` entry to `^<target-version>` (canonical form — overwrites any pre-existing range).
|
|
34
|
+
6. Deletes `package-lock.json`.
|
|
35
|
+
7. Runs `npm install`.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, readdirSync, rmSync } from "fs"
|
|
4
|
+
import { join, resolve, dirname } from "path"
|
|
5
|
+
import { spawnSync } from "child_process"
|
|
6
|
+
|
|
7
|
+
const GREST_PREFIX = "@grest-ts/"
|
|
8
|
+
|
|
9
|
+
const COMMANDS = {
|
|
10
|
+
upgrade: runUpgrade,
|
|
11
|
+
help: runHelp,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const [, , cmd, ...args] = process.argv
|
|
16
|
+
|
|
17
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
18
|
+
runHelp()
|
|
19
|
+
process.exit(0)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const handler = COMMANDS[cmd]
|
|
23
|
+
if (!handler) {
|
|
24
|
+
console.error(`Unknown command: ${cmd}\n`)
|
|
25
|
+
runHelp()
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await handler(args)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runHelp() {
|
|
33
|
+
console.log(`grest-ts CLI
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
grest-ts <command> [options]
|
|
37
|
+
|
|
38
|
+
Commands:
|
|
39
|
+
upgrade [version] Atomically update all @grest-ts/* deps to a single version.
|
|
40
|
+
Version can be a semver (e.g. 0.0.27) or a dist-tag
|
|
41
|
+
(latest, next). Defaults to "latest".
|
|
42
|
+
Flags:
|
|
43
|
+
--dry-run Show what would change without writing.
|
|
44
|
+
help Show this message.
|
|
45
|
+
`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function runUpgrade(args) {
|
|
49
|
+
const dryRun = args.includes("--dry-run") || args.includes("--dry")
|
|
50
|
+
const positional = args.filter(a => !a.startsWith("--"))
|
|
51
|
+
const versionArg = positional[0] || "latest"
|
|
52
|
+
|
|
53
|
+
const cwd = process.cwd()
|
|
54
|
+
const rootPkgPath = join(cwd, "package.json")
|
|
55
|
+
if (!existsSync(rootPkgPath)) {
|
|
56
|
+
throw new Error(`No package.json found in ${cwd}`)
|
|
57
|
+
}
|
|
58
|
+
const rootPkg = readJson(rootPkgPath)
|
|
59
|
+
|
|
60
|
+
// 1. Discover all package.json files in the project
|
|
61
|
+
const allPkgPaths = [rootPkgPath, ...findWorkspacePackages(cwd, rootPkg)]
|
|
62
|
+
console.log(`Scanning ${allPkgPaths.length} package.json file(s)...`)
|
|
63
|
+
|
|
64
|
+
// 2. Collect every @grest-ts/* dep referenced anywhere
|
|
65
|
+
const grestDeps = new Set()
|
|
66
|
+
for (const p of allPkgPaths) {
|
|
67
|
+
const pkg = readJson(p)
|
|
68
|
+
for (const block of ["dependencies", "devDependencies"]) {
|
|
69
|
+
if (!pkg[block]) continue
|
|
70
|
+
for (const key of Object.keys(pkg[block])) {
|
|
71
|
+
if (key.startsWith(GREST_PREFIX)) grestDeps.add(key)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (grestDeps.size === 0) {
|
|
77
|
+
console.log("No @grest-ts/* dependencies found. Nothing to do.")
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
console.log(`Found ${grestDeps.size} unique @grest-ts/* dependency name(s).`)
|
|
81
|
+
|
|
82
|
+
// 3. Resolve target version (handle dist-tags)
|
|
83
|
+
const targetVersion = await resolveTargetVersion(versionArg)
|
|
84
|
+
const canonicalRange = `^${targetVersion}`
|
|
85
|
+
console.log(`Target version: ${targetVersion} (writing as "${canonicalRange}")`)
|
|
86
|
+
|
|
87
|
+
// 4. Pre-flight: every grest-ts package must be published at target version.
|
|
88
|
+
// grest-ts is single-version across all packages — partial publishes would
|
|
89
|
+
// leave the project wedged with peer-dep mismatches.
|
|
90
|
+
console.log("Verifying all packages are published at target version...")
|
|
91
|
+
const missing = []
|
|
92
|
+
await Promise.all([...grestDeps].map(async name => {
|
|
93
|
+
if (!(await packageVersionExists(name, targetVersion))) {
|
|
94
|
+
missing.push(name)
|
|
95
|
+
}
|
|
96
|
+
}))
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Not published at ${targetVersion}:\n ` + missing.join("\n ") +
|
|
100
|
+
`\n\ngrest-ts requires all @grest-ts/* packages at the same version. Wait for the publish to complete and retry.`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. Rewrite every package.json — normalize every @grest-ts/* range to canonical form.
|
|
105
|
+
let totalChanged = 0
|
|
106
|
+
let filesChanged = 0
|
|
107
|
+
for (const p of allPkgPaths) {
|
|
108
|
+
const raw = readFileSync(p, "utf-8")
|
|
109
|
+
const pkg = JSON.parse(raw)
|
|
110
|
+
let fileChanged = false
|
|
111
|
+
for (const block of ["dependencies", "devDependencies"]) {
|
|
112
|
+
if (!pkg[block]) continue
|
|
113
|
+
for (const key of Object.keys(pkg[block])) {
|
|
114
|
+
if (!key.startsWith(GREST_PREFIX)) continue
|
|
115
|
+
if (pkg[block][key] !== canonicalRange) {
|
|
116
|
+
pkg[block][key] = canonicalRange
|
|
117
|
+
fileChanged = true
|
|
118
|
+
totalChanged++
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (fileChanged) {
|
|
123
|
+
filesChanged++
|
|
124
|
+
const trailingNewline = raw.endsWith("\n") ? "\n" : ""
|
|
125
|
+
const out = JSON.stringify(pkg, null, 2) + trailingNewline
|
|
126
|
+
if (dryRun) {
|
|
127
|
+
console.log(` [dry] would update ${relativeTo(cwd, p)}`)
|
|
128
|
+
} else {
|
|
129
|
+
writeFileSync(p, out)
|
|
130
|
+
console.log(` updated ${relativeTo(cwd, p)}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
console.log(`${dryRun ? "[dry] " : ""}Updated ${totalChanged} dep entries across ${filesChanged} file(s).`)
|
|
135
|
+
|
|
136
|
+
if (dryRun) {
|
|
137
|
+
console.log("\nDry run complete. Re-run without --dry-run to apply.")
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 6. Clear lockfile + installed @grest-ts/* state.
|
|
142
|
+
// Three things hold npm to old grest-ts versions:
|
|
143
|
+
// - package-lock.json (the visible lockfile)
|
|
144
|
+
// - node_modules/.package-lock.json (npm's hidden install-state lockfile)
|
|
145
|
+
// - node_modules/@grest-ts/* (whose package.json files declare exact-pinned
|
|
146
|
+
// peer deps that conflict with the new versions)
|
|
147
|
+
// All three must go for npm to re-resolve grest-ts cleanly.
|
|
148
|
+
const lockPath = join(cwd, "package-lock.json")
|
|
149
|
+
if (existsSync(lockPath)) {
|
|
150
|
+
console.log("Deleting package-lock.json...")
|
|
151
|
+
unlinkSync(lockPath)
|
|
152
|
+
}
|
|
153
|
+
const hiddenLockPath = join(cwd, "node_modules", ".package-lock.json")
|
|
154
|
+
if (existsSync(hiddenLockPath)) {
|
|
155
|
+
console.log("Deleting node_modules/.package-lock.json...")
|
|
156
|
+
unlinkSync(hiddenLockPath)
|
|
157
|
+
}
|
|
158
|
+
const grestNodeModulesPath = join(cwd, "node_modules", "@grest-ts")
|
|
159
|
+
if (existsSync(grestNodeModulesPath)) {
|
|
160
|
+
console.log("Removing node_modules/@grest-ts/ ...")
|
|
161
|
+
rmSync(grestNodeModulesPath, { recursive: true, force: true })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 7. Reinstall. Pass as a single shell command string — Node refuses to spawn
|
|
165
|
+
// .cmd files directly on Windows (CVE-2024-27980), and passing arg arrays
|
|
166
|
+
// with shell:true triggers DEP0190.
|
|
167
|
+
console.log("Running npm install...")
|
|
168
|
+
const result = spawnSync("npm install", { stdio: "inherit", shell: true, cwd })
|
|
169
|
+
if (result.status !== 0) {
|
|
170
|
+
throw new Error(`npm install exited with code ${result.status}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log(`\n✓ Upgraded all @grest-ts/* to ${canonicalRange}`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- helpers ----
|
|
177
|
+
|
|
178
|
+
function readJson(path) {
|
|
179
|
+
return JSON.parse(readFileSync(path, "utf-8"))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function relativeTo(base, path) {
|
|
183
|
+
if (path.startsWith(base)) {
|
|
184
|
+
const rel = path.slice(base.length).replace(/^[\\/]+/, "")
|
|
185
|
+
return rel || "."
|
|
186
|
+
}
|
|
187
|
+
return path
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findWorkspacePackages(rootDir, rootPkg) {
|
|
191
|
+
const ws = rootPkg.workspaces
|
|
192
|
+
if (!ws) return []
|
|
193
|
+
const patterns = Array.isArray(ws) ? ws : (ws.packages || [])
|
|
194
|
+
|
|
195
|
+
const pkgPaths = []
|
|
196
|
+
for (const pattern of patterns) {
|
|
197
|
+
if (pattern.includes("**")) {
|
|
198
|
+
throw new Error(`Recursive workspace globs are not supported: "${pattern}"`)
|
|
199
|
+
}
|
|
200
|
+
if (pattern.includes("*")) {
|
|
201
|
+
// Only support the common "dir/*" trailing-star form.
|
|
202
|
+
const idx = pattern.indexOf("*")
|
|
203
|
+
const remainder = pattern.slice(idx)
|
|
204
|
+
if (remainder !== "*") {
|
|
205
|
+
throw new Error(`Workspace glob form not supported: "${pattern}" (only "dir/*" is supported)`)
|
|
206
|
+
}
|
|
207
|
+
const baseRel = pattern.slice(0, idx).replace(/[\\/]$/, "")
|
|
208
|
+
const baseAbs = resolve(rootDir, baseRel)
|
|
209
|
+
if (!existsSync(baseAbs)) continue
|
|
210
|
+
for (const entry of readdirSync(baseAbs, { withFileTypes: true })) {
|
|
211
|
+
if (!entry.isDirectory()) continue
|
|
212
|
+
const candidate = join(baseAbs, entry.name, "package.json")
|
|
213
|
+
if (existsSync(candidate)) pkgPaths.push(candidate)
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
const candidate = resolve(rootDir, pattern, "package.json")
|
|
217
|
+
if (existsSync(candidate)) pkgPaths.push(candidate)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return pkgPaths
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function resolveTargetVersion(versionArg) {
|
|
224
|
+
// If versionArg looks like a concrete version, return it as-is.
|
|
225
|
+
if (/^\d+\.\d+\.\d+(?:[-+].+)?$/.test(versionArg)) {
|
|
226
|
+
return versionArg
|
|
227
|
+
}
|
|
228
|
+
// Otherwise treat as a dist-tag and look it up against @grest-ts/http.
|
|
229
|
+
const probe = `${GREST_PREFIX}http`
|
|
230
|
+
const data = await fetchJson(`https://registry.npmjs.org/${probe}`)
|
|
231
|
+
const tags = data["dist-tags"] || {}
|
|
232
|
+
const v = tags[versionArg]
|
|
233
|
+
if (!v) {
|
|
234
|
+
const known = Object.keys(tags).join(", ") || "(none)"
|
|
235
|
+
throw new Error(`No dist-tag "${versionArg}" on ${probe}. Known tags: ${known}`)
|
|
236
|
+
}
|
|
237
|
+
return v
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function packageVersionExists(pkgName, version) {
|
|
241
|
+
const url = `https://registry.npmjs.org/${pkgName}/${version}`
|
|
242
|
+
try {
|
|
243
|
+
const res = await fetch(url)
|
|
244
|
+
return res.ok
|
|
245
|
+
} catch {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function fetchJson(url) {
|
|
251
|
+
const res = await fetch(url)
|
|
252
|
+
if (!res.ok) {
|
|
253
|
+
throw new Error(`GET ${url} failed: ${res.status} ${res.statusText}`)
|
|
254
|
+
}
|
|
255
|
+
return res.json()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main().catch(err => {
|
|
259
|
+
console.error(`\nError: ${err.message}`)
|
|
260
|
+
if (process.env.DEBUG) console.error(err.stack)
|
|
261
|
+
process.exit(1)
|
|
262
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "THIS FILE IS GENERATED - DO NOT EDIT",
|
|
3
|
+
"name": "@grest-ts/cli",
|
|
4
|
+
"version": "0.0.27",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"description": "CLI for managing grest-ts in a project (atomic version upgrades, etc.)",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.mjs"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/grest-ts/grest-ts.git",
|
|
17
|
+
"directory": "packages-tooling/cli"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://grest-ts.com/packages/cli",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/grest-ts/grest-ts/issues"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"typescript",
|
|
25
|
+
"framework",
|
|
26
|
+
"contract",
|
|
27
|
+
"api",
|
|
28
|
+
"microservices",
|
|
29
|
+
"testing",
|
|
30
|
+
"grest-ts",
|
|
31
|
+
"cli",
|
|
32
|
+
"upgrade",
|
|
33
|
+
"tooling"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=25"
|
|
37
|
+
},
|
|
38
|
+
"bin": "./index.mjs"
|
|
39
|
+
}
|