feat(sdk): add update verification

This commit is contained in:
jubnl
2026-07-13 17:35:17 +02:00
parent 1b4ef8e9fe
commit 5003dc7f33
9 changed files with 1205 additions and 4 deletions
+29
View File
@@ -152,4 +152,33 @@ bin if you install the package):
- `release [dir] --repo o/n --tag vX [--sign [key]] [--merge f]` — pack → GitHub release → entry, in one go.
- `publish [dir] --repo o/n --tag vX [--sign [key]] [--no-preflight]` — **the lot**: pack → tag + release → preflight → open the PR.
## Update notice
Both CLIs (`trek-plugin` and `create-trek-plugin`) tell you when a newer SDK has been
published. This matters more than the usual "you're on an old version" nag: the
registry entry format, the manifest rules and the permission catalog all move with the
TREK host, so a stale SDK can `pack` and `submit` an entry that today's registry CI
rejects.
It is powered by [`update-notifier`](https://github.com/sindresorhus/update-notifier),
the standard for npm CLIs:
- At most **once every 24 hours**, a detached background process asks
`registry.npmjs.org` for this package's `latest` version and caches the answer under
`$XDG_CONFIG_HOME/configstore/` (or `~/.config/…`).
- Your command **never waits for it** — the notice is printed from that cache, so a
fresh install learns about an update on a later run.
- The notice goes to **stderr**, so `pack --json` and `entry` keep piping clean JSON.
The request is an unauthenticated GET for a public package — the same one `npm install`
makes (npm's servers see your IP, as for any download). TREK has no telemetry, and this
isn't any. To turn it off:
```bash
export NO_UPDATE_NOTIFIER=1
```
It is already silent in CI (any `CI` env var), under `NODE_ENV=test`, and whenever
stdout isn't a terminal (i.e. when piped or redirected).
The SDK tooling in this repo is MIT. Your plugin is your own code under your own license.
+768 -3
View File
@@ -1,16 +1,17 @@
{
"name": "trek-plugin-sdk",
"version": "1.4.0",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "trek-plugin-sdk",
"version": "1.4.0",
"version": "1.5.0",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^1.7.0",
"semver": "^7.7.4"
"semver": "^7.7.4",
"update-notifier": "^7.3.1"
},
"bin": {
"create-trek-plugin": "dist/cli/create.js",
@@ -20,6 +21,7 @@
"devDependencies": {
"@types/node": "^22.20.0",
"@types/semver": "^7.7.1",
"@types/update-notifier": "^6.0.8",
"typescript": "^5.9.3",
"vitest": "^4.1.9"
},
@@ -125,6 +127,47 @@
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@pnpm/config.env-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
"integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==",
"license": "MIT",
"engines": {
"node": ">=12.22.0"
}
},
"node_modules/@pnpm/network.ca-file": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz",
"integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==",
"license": "MIT",
"dependencies": {
"graceful-fs": "4.2.10"
},
"engines": {
"node": ">=12.22.0"
}
},
"node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"license": "ISC"
},
"node_modules/@pnpm/npm-conf": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.3.tgz",
"integrity": "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==",
"license": "MIT",
"dependencies": {
"@pnpm/config.env-replace": "^1.1.0",
"@pnpm/network.ca-file": "^1.0.1",
"config-chain": "^1.1.11"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
@@ -418,6 +461,13 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/configstore": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-6.0.2.tgz",
"integrity": "sha512-OS//b51j9uyR3zvwD04Kfs5kHpve2qalQ18JhY/ho3voGYUTPLEG90/ocfKPI48hyHH8T04f7KEEbK6Ue60oZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -449,6 +499,125 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/update-notifier": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-6.0.8.tgz",
"integrity": "sha512-IlDFnfSVfYQD+cKIg63DEXn3RFmd7W1iYtKQsJodcHK9R1yr8aKbKaPKfBxzPpcHCq2DU8zUq4PIPmy19Thjfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/configstore": "*",
"boxen": "^7.1.1"
}
},
"node_modules/@types/update-notifier/node_modules/boxen": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz",
"integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-align": "^3.0.1",
"camelcase": "^7.0.1",
"chalk": "^5.2.0",
"cli-boxes": "^3.0.0",
"string-width": "^5.1.2",
"type-fest": "^2.13.0",
"widest-line": "^4.0.1",
"wrap-ansi": "^8.1.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/update-notifier/node_modules/camelcase": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz",
"integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/update-notifier/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/update-notifier/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/update-notifier/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/update-notifier/node_modules/widest-line": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
"integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==",
"dev": true,
"license": "MIT",
"dependencies": {
"string-width": "^5.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@types/update-notifier/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@vitest/expect": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
@@ -562,6 +731,80 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"license": "ISC",
"dependencies": {
"string-width": "^4.1.0"
}
},
"node_modules/ansi-align/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-align/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/ansi-align/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-align/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -572,6 +815,50 @@
"node": ">=12"
}
},
"node_modules/atomically": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz",
"integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==",
"license": "MIT",
"dependencies": {
"stubborn-fs": "^2.0.0",
"when-exit": "^2.1.4"
}
},
"node_modules/boxen": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
"integrity": "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==",
"license": "MIT",
"dependencies": {
"ansi-align": "^3.0.1",
"camelcase": "^8.0.0",
"chalk": "^5.3.0",
"cli-boxes": "^3.0.0",
"string-width": "^7.2.0",
"type-fest": "^4.21.0",
"widest-line": "^5.0.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
"integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -582,6 +869,64 @@
"node": ">=18"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cli-boxes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz",
"integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/config-chain": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
"license": "MIT",
"dependencies": {
"ini": "^1.3.4",
"proto-list": "~1.2.1"
}
},
"node_modules/config-chain/node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/configstore": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz",
"integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==",
"license": "BSD-2-Clause",
"dependencies": {
"atomically": "^2.0.3",
"dot-prop": "^9.0.0",
"graceful-fs": "^4.2.11",
"xdg-basedir": "^5.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -589,6 +934,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -599,6 +953,34 @@
"node": ">=8"
}
},
"node_modules/dot-prop": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz",
"integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
"license": "MIT",
"dependencies": {
"type-fest": "^4.18.2"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"license": "MIT"
},
"node_modules/es-module-lexer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",
@@ -606,6 +988,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/escape-goat": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz",
"integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -683,6 +1077,139 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/global-directory": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
"integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
"license": "MIT",
"dependencies": {
"ini": "4.1.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
"integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-in-ci": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-1.0.0.tgz",
"integrity": "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==",
"license": "MIT",
"bin": {
"is-in-ci": "cli.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-installed-globally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz",
"integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==",
"license": "MIT",
"dependencies": {
"global-directory": "^4.0.1",
"is-path-inside": "^4.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-npm": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz",
"integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-path-inside": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ky": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz",
"integrity": "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sindresorhus/ky?sponsor=1"
}
},
"node_modules/latest-version": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-9.0.0.tgz",
"integrity": "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==",
"license": "MIT",
"dependencies": {
"package-json": "^10.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -954,6 +1481,15 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/nanoid": {
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
@@ -987,6 +1523,24 @@
"node": ">=12.20.0"
}
},
"node_modules/package-json": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz",
"integrity": "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==",
"license": "MIT",
"dependencies": {
"ky": "^1.2.0",
"registry-auth-token": "^5.0.2",
"registry-url": "^6.0.1",
"semver": "^7.6.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -1043,6 +1597,75 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"license": "ISC"
},
"node_modules/pupa": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz",
"integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==",
"license": "MIT",
"dependencies": {
"escape-goat": "^4.0.0"
},
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/registry-auth-token": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz",
"integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==",
"license": "MIT",
"dependencies": {
"@pnpm/npm-conf": "^3.0.2"
},
"engines": {
"node": ">=14"
}
},
"node_modules/registry-url": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz",
"integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==",
"license": "MIT",
"dependencies": {
"rc": "1.2.8"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rolldown": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
@@ -1126,6 +1749,62 @@
"dev": true,
"license": "MIT"
},
"node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stubborn-fs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz",
"integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==",
"license": "MIT",
"dependencies": {
"stubborn-utils": "^1.0.1"
}
},
"node_modules/stubborn-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz",
"integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -1178,6 +1857,18 @@
"license": "0BSD",
"optional": true
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1199,6 +1890,30 @@
"dev": true,
"license": "MIT"
},
"node_modules/update-notifier": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz",
"integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==",
"license": "BSD-2-Clause",
"dependencies": {
"boxen": "^8.0.1",
"chalk": "^5.3.0",
"configstore": "^7.0.0",
"is-in-ci": "^1.0.0",
"is-installed-globally": "^1.0.0",
"is-npm": "^6.0.0",
"latest-version": "^9.0.0",
"pupa": "^3.1.0",
"semver": "^7.6.3",
"xdg-basedir": "^5.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/yeoman/update-notifier?sponsor=1"
}
},
"node_modules/vite": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
@@ -1367,6 +2082,12 @@
}
}
},
"node_modules/when-exit": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
"integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
"license": "MIT"
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -1383,6 +2104,50 @@
"engines": {
"node": ">=8"
}
},
"node_modules/widest-line": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz",
"integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==",
"license": "MIT",
"dependencies": {
"string-width": "^7.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/xdg-basedir": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
"integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+3 -1
View File
@@ -64,7 +64,8 @@
],
"dependencies": {
"@clack/prompts": "^1.7.0",
"semver": "^7.7.4"
"semver": "^7.7.4",
"update-notifier": "^7.3.1"
},
"scripts": {
"build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/finish-cjs.mjs",
@@ -75,6 +76,7 @@
"devDependencies": {
"@types/node": "^22.20.0",
"@types/semver": "^7.7.1",
"@types/update-notifier": "^6.0.8",
"typescript": "^5.9.3",
"vitest": "^4.1.9"
},
+2
View File
@@ -15,6 +15,7 @@ import {
PERMISSION_CATALOG,
} from './ui.js';
import { KNOWN_ADDONS } from '../manifest.js';
import { notifySdkUpdate } from './update-notice.js';
/** This package's own version, for the scaffold's devDependency range. */
function sdkVersionRange(): string {
@@ -491,6 +492,7 @@ export async function interactiveScaffold(defaultDir: string, presetName?: strin
// CLI entry
if (process.argv[1] && process.argv[1].endsWith('create.js')) {
notifySdkUpdate();
const args = process.argv.slice(2);
const name = args.find((a: string) => !a.startsWith('-'));
const typeIdx = args.indexOf('--type');
+4
View File
@@ -41,6 +41,7 @@ import {
promptText, promptConfirm, clackLogSink, missingArgs,
} from './ui.js';
import { runMenu } from './menu.js';
import { notifySdkUpdate } from './update-notice.js';
const [cmd, ...args] = process.argv.slice(2);
@@ -159,6 +160,9 @@ The goal: create -> dev -> publish, and never hand-compute sha256/size/commitSha
or hand-write the registry JSON.`;
async function main(): Promise<void> {
// Advisory only (update-notifier): prints from cache on stderr, refreshes in the
// background, never blocks. `dev` never returns, so this runs up front.
notifySdkUpdate();
// Before dispatch, so assertKnownFlags never sees `--help` and rejects it as unknown.
// Help is not an error: it goes to stdout and exits 0.
if (cmd === 'help' || cmd === '--help' || cmd === '-h' || flags.help) {
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env node
/**
* The background half of the update notice (#plugins) — see update-check.ts.
*
* Spawned detached and unref'd, so nothing is waiting on it: it asks npm for the
* `latest` dist-tag, writes the cache the NEXT command will read, and exits. It
* prints nothing, ever, and every failure (offline, DNS, 404, timeout, read-only
* cache dir) is a silent no-op — the author is not debugging our update check.
*/
import fs from 'node:fs';
import path from 'node:path';
import { PACKAGE_NAME, cachePath, type UpdateCache } from './update-check.js';
const REGISTRY = 'https://registry.npmjs.org';
/** Generous: nobody is waiting on this process. */
const TIMEOUT_MS = 3000;
/** Ask the registry for the `latest` dist-tag. `null` on any failure. */
export async function fetchLatest(registry: string = REGISTRY): Promise<string | null> {
try {
const res = await fetch(`${registry}/${PACKAGE_NAME}/latest`, {
signal: AbortSignal.timeout(TIMEOUT_MS),
headers: { accept: 'application/json' },
});
if (!res.ok) return null;
const body = (await res.json()) as { version?: unknown };
return typeof body?.version === 'string' ? body.version : null;
} catch {
return null;
}
}
/** Write the cache the next command reads. Silently gives up if it can't. */
export function writeCache(file: string, cache: UpdateCache): void {
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify(cache));
} catch {
// A cache we can't write just means we check again next time.
}
}
/** Fetch → cache. Exported whole so the tests can drive it with a stubbed `fetch`. */
export async function refresh(file: string = cachePath(), now: number = Date.now()): Promise<void> {
const latest = await fetchLatest();
if (latest) writeCache(file, { lastCheck: now, latest });
}
// Bin entry. Never throws, never prints, always exits 0.
if (process.argv[1] && process.argv[1].endsWith('update-check-worker.js')) {
refresh().catch(() => {}).finally(() => process.exit(0));
}
+112
View File
@@ -0,0 +1,112 @@
/**
* "Your SDK is out of date" — the advisory update notice (#plugins).
*
* A stale SDK is not a cosmetic problem: the registry-entry format, the manifest
* validator and the permission catalog all move with the host, so an author on an
* old SDK can happily `pack` and `submit` an entry that today's registry CI rejects.
*
* Three rules keep this safe to call from the top of every command:
* 1. ZERO added latency. The command only ever prints from a cache file; the
* refresh runs in a DETACHED child (see update-check-worker.ts). An un-awaited
* in-process fetch() would NOT do — Node keeps the socket alive and won't exit
* until it resolves, so every command would pay ~300ms.
* 2. STDERR only. stdout is a pure data channel (`entry`, `pack --json`, PR URLs) —
* see the contract in ui.ts.
* 3. It CANNOT fail a command. Every path is wrapped; a broken cache, a missing
* worker or an offline machine are all silently indistinguishable from "no news".
*/
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import semver from 'semver';
export const PACKAGE_NAME = 'trek-plugin-sdk';
export const CACHE_FILE = 'update-check.json';
export const TTL_MS = 24 * 60 * 60 * 1000;
export interface UpdateCache {
/** Epoch ms of the last successful registry read. */
lastCheck: number;
/** The `latest` dist-tag as of `lastCheck`. */
latest: string;
}
/** This package's own version — the thing we compare the registry against. */
export function sdkVersion(): string {
try {
const pkg = createRequire(import.meta.url)('../../package.json') as { version?: string };
return pkg.version ?? '0.0.0';
} catch {
return '0.0.0';
}
}
/**
* Where the cache lives. Takes its inputs as arguments (rather than reading the
* environment itself) so tests can point it at a temp dir without touching $HOME.
*/
export function cachePath(env: NodeJS.ProcessEnv = process.env, homedir = os.homedir(), tmpdir = os.tmpdir()): string {
const base = env.XDG_CACHE_HOME || (homedir ? path.join(homedir, '.cache') : tmpdir);
return path.join(base, PACKAGE_NAME, CACHE_FILE);
}
/** Missing, unreadable, corrupt, or wrong-shaped — all are simply "no news". Never throws. */
export function readCache(file: string): UpdateCache | null {
try {
const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Partial<UpdateCache>;
if (typeof raw?.lastCheck !== 'number' || typeof raw?.latest !== 'string') return null;
return { lastCheck: raw.lastCheck, latest: raw.latest };
} catch {
return null;
}
}
/** An absent cache is stale — that is what triggers the very first refresh. */
export function isStale(cache: UpdateCache | null, now: number, ttlMs: number = TTL_MS): boolean {
return !cache || now - cache.lastCheck >= ttlMs;
}
/**
* The notice is for humans at a terminal, and for nobody else: not CI logs, not
* pipes, and not authors who have opted out.
*/
export function isSilenced(env: NodeJS.ProcessEnv = process.env, isTTY: boolean = Boolean(process.stderr.isTTY)): boolean {
if (env.TREK_SDK_NO_UPDATE_CHECK || env.NO_UPDATE_NOTIFIER || env.CI) return true;
return !isTTY;
}
/** The notice itself — `null` when there is nothing to say (same version, or ahead of it). */
export function banner(current: string, latest: string): string | null {
if (!semver.valid(current) || !semver.valid(latest) || !semver.gt(latest, current)) return null;
const dim = (s: string): string => `\x1b[2m${s}\x1b[0m`;
return dim(`\n Update available ${current} → ${latest} · npm i -g ${PACKAGE_NAME}@latest\n`);
}
/** Detached + unref'd, so the parent process exits the instant its own work is done. */
function spawnRefresh(): void {
const worker = fileURLToPath(new URL('./update-check-worker.js', import.meta.url));
spawn(process.execPath, [worker], { detached: true, stdio: 'ignore' }).unref();
}
/**
* The one entry point. Call it at the top of a command; it returns immediately.
* Prints from cache, and refreshes that cache in the background for the NEXT run —
* which is why a brand-new install learns about an update one run late. That is the
* price of never making an author wait on the network.
*/
export function maybeNotify(now: number = Date.now()): void {
try {
if (isSilenced()) return;
const cache = readCache(cachePath());
if (cache) {
const msg = banner(sdkVersion(), cache.latest);
if (msg) process.stderr.write(msg);
}
if (isStale(cache, now)) spawnRefresh();
} catch {
// An update notice is never worth failing a command over.
}
}
+26
View File
@@ -0,0 +1,26 @@
/**
* "Your SDK is out of date" — the advisory update notice (#plugins).
*
* A stale SDK is not cosmetic: the registry-entry format, the manifest validator and
* the permission catalog all move with the host, so an author on an old SDK can `pack`
* and `submit` an entry that today's registry CI rejects.
*
* This is a thin wrapper over `update-notifier` (the de-facto standard for npm CLIs):
* a detached background process checks the registry at most once a day, the notice is
* printed from cache on stderr, and CI / non-TTY / `NO_UPDATE_NOTIFIER` all silence it.
* We hand it off rather than hand-roll the same detached-child + cache dance ourselves.
*/
import updateNotifier from 'update-notifier';
import { createRequire } from 'node:module';
export function notifySdkUpdate(): void {
try {
// `../../package.json` resolves to the package root from dist/cli/ (prod) and
// src/cli/ (tests) alike. createRequire keeps it working on every Node >=18,
// unlike `import ... with { type: 'json' }`, which Node 18 does not accept.
const pkg = createRequire(import.meta.url)('../../package.json') as { name: string; version: string };
updateNotifier({ pkg }).notify();
} catch {
// An update notice is never worth disturbing a command over.
}
}
+209
View File
@@ -0,0 +1,209 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const spawn = vi.hoisted(() => vi.fn(() => ({ unref: vi.fn() })));
vi.mock('node:child_process', async (orig) => ({
...(await orig<typeof import('node:child_process')>()),
spawn,
}));
const {
banner, cachePath, readCache, isStale, isSilenced, maybeNotify, sdkVersion, PACKAGE_NAME, TTL_MS,
} = await import('../src/cli/update-check.js');
const { fetchLatest, writeCache, refresh } = await import('../src/cli/update-check-worker.js');
let tmp: string;
beforeEach(() => {
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'update-check-'));
spawn.mockClear();
});
afterEach(() => {
fs.rmSync(tmp, { recursive: true, force: true });
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
describe('banner', () => {
it('announces only a strictly newer version', () => {
expect(banner('1.2.0', '1.5.0')).toContain('1.2.0 → 1.5.0');
expect(banner('1.5.0', '1.5.0')).toBeNull();
expect(banner('1.6.0', '1.5.0')).toBeNull();
});
it('says nothing when either version is unparseable', () => {
expect(banner('not-a-version', '1.5.0')).toBeNull();
expect(banner('1.2.0', 'garbage')).toBeNull();
});
});
describe('sdkVersion', () => {
it("reads this package's real version", () => {
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
expect(sdkVersion()).toBe(pkg.version);
});
});
describe('cachePath', () => {
it('prefers XDG_CACHE_HOME, else ~/.cache', () => {
expect(cachePath({ XDG_CACHE_HOME: '/xdg' }, '/home/a', '/tmp'))
.toBe(path.join('/xdg', PACKAGE_NAME, 'update-check.json'));
expect(cachePath({}, '/home/a', '/tmp'))
.toBe(path.join('/home/a', '.cache', PACKAGE_NAME, 'update-check.json'));
});
});
describe('readCache', () => {
it('reads a well-formed cache', () => {
const f = path.join(tmp, 'c.json');
fs.writeFileSync(f, JSON.stringify({ lastCheck: 42, latest: '9.9.9' }));
expect(readCache(f)).toEqual({ lastCheck: 42, latest: '9.9.9' });
});
it('returns null — never throws — for missing, corrupt or wrong-shaped files', () => {
expect(readCache(path.join(tmp, 'nope.json'))).toBeNull();
const corrupt = path.join(tmp, 'corrupt.json');
fs.writeFileSync(corrupt, '{not json');
expect(readCache(corrupt)).toBeNull();
const wrong = path.join(tmp, 'wrong.json');
fs.writeFileSync(wrong, JSON.stringify({ latest: 5 }));
expect(readCache(wrong)).toBeNull();
});
});
describe('isStale', () => {
it('treats an absent cache as stale, and expires at the TTL boundary', () => {
expect(isStale(null, 1_000_000)).toBe(true);
expect(isStale({ lastCheck: 1000, latest: '1.0.0' }, 1000 + TTL_MS - 1)).toBe(false);
expect(isStale({ lastCheck: 1000, latest: '1.0.0' }, 1000 + TTL_MS)).toBe(true);
});
});
describe('isSilenced', () => {
it('is silent in CI, when opted out, and when stderr is not a TTY', () => {
expect(isSilenced({ CI: '1' }, true)).toBe(true);
expect(isSilenced({ NO_UPDATE_NOTIFIER: '1' }, true)).toBe(true);
expect(isSilenced({ TREK_SDK_NO_UPDATE_CHECK: '1' }, true)).toBe(true);
expect(isSilenced({}, false)).toBe(true);
expect(isSilenced({}, true)).toBe(false);
});
});
describe('maybeNotify', () => {
/** A human at a terminal, with $XDG_CACHE_HOME pointed at the temp dir. */
function asHuman(): ReturnType<typeof vi.spyOn> {
vi.stubEnv('CI', '');
vi.stubEnv('NO_UPDATE_NOTIFIER', '');
vi.stubEnv('TREK_SDK_NO_UPDATE_CHECK', '');
vi.stubEnv('XDG_CACHE_HOME', tmp);
vi.spyOn(process.stderr, 'isTTY', 'get').mockReturnValue(true);
return vi.spyOn(process.stderr, 'write').mockReturnValue(true);
}
function seedCache(cache: { lastCheck: number; latest: string }): void {
const f = path.join(tmp, PACKAGE_NAME, 'update-check.json');
fs.mkdirSync(path.dirname(f), { recursive: true });
fs.writeFileSync(f, JSON.stringify(cache));
}
it('prints the notice when a fresh cache holds a newer version', () => {
const write = asHuman();
seedCache({ lastCheck: Date.now(), latest: '999.0.0' });
maybeNotify();
expect(write).toHaveBeenCalledOnce();
expect(String(write.mock.calls[0][0])).toContain(`999.0.0 · npm i -g ${PACKAGE_NAME}@latest`);
expect(spawn).not.toHaveBeenCalled(); // fresh cache — no refresh
});
it('prints nothing when the cached version is the one we are running', () => {
const write = asHuman();
seedCache({ lastCheck: Date.now(), latest: sdkVersion() });
maybeNotify();
expect(write).not.toHaveBeenCalled();
});
it('is silent on a cold cache, but spawns the background refresh for next time', () => {
const write = asHuman();
maybeNotify();
expect(write).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalledOnce();
const [, args, opts] = spawn.mock.calls[0] as unknown as [string, string[], Record<string, unknown>];
expect(args[0]).toMatch(/update-check-worker\.js$/);
expect(opts).toMatchObject({ detached: true, stdio: 'ignore' });
});
it('refreshes a stale cache while still printing what it knows', () => {
const write = asHuman();
seedCache({ lastCheck: Date.now() - TTL_MS - 1, latest: '999.0.0' });
maybeNotify();
expect(write).toHaveBeenCalledOnce();
expect(spawn).toHaveBeenCalledOnce();
});
it('does nothing at all — no print, no spawn — when silenced', () => {
const write = asHuman();
vi.stubEnv('CI', '1');
seedCache({ lastCheck: Date.now(), latest: '999.0.0' });
maybeNotify();
expect(write).not.toHaveBeenCalled();
expect(spawn).not.toHaveBeenCalled();
});
it('survives a corrupt cache without throwing', () => {
const write = asHuman();
const f = path.join(tmp, PACKAGE_NAME, 'update-check.json');
fs.mkdirSync(path.dirname(f), { recursive: true });
fs.writeFileSync(f, 'not json at all');
expect(() => maybeNotify()).not.toThrow();
expect(write).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalledOnce(); // unreadable == no news == refresh
});
});
describe('worker', () => {
it('returns the latest dist-tag version', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '2.0.0' }))));
await expect(fetchLatest()).resolves.toBe('2.0.0');
});
it('returns null on a non-200, a malformed body, or a network failure', async () => {
vi.stubGlobal('fetch', vi.fn(async () => new Response('nope', { status: 404 })));
await expect(fetchLatest()).resolves.toBeNull();
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: 42 }))));
await expect(fetchLatest()).resolves.toBeNull();
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('ENOTFOUND'); }));
await expect(fetchLatest()).resolves.toBeNull();
});
it('writes the cache only when the fetch succeeded', async () => {
const f = path.join(tmp, 'sub', 'update-check.json');
vi.stubGlobal('fetch', vi.fn(async () => { throw new Error('offline'); }));
await refresh(f, 123);
expect(fs.existsSync(f)).toBe(false);
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ version: '3.1.0' }))));
await refresh(f, 123);
expect(readCache(f)).toEqual({ lastCheck: 123, latest: '3.1.0' });
});
it('gives up silently when the cache is unwritable', () => {
expect(() => writeCache('/proc/definitely/not/writable.json', { lastCheck: 1, latest: '1.0.0' })).not.toThrow();
});
});