promptkit-os 1.11.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/bin/promptkit-os.js +65 -47
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ If `.promptkit/` already exists and is not empty, the courier **refuses to overl
|
|
|
28
28
|
# git-submodule install
|
|
29
29
|
git submodule update --remote --merge .promptkit && bash .promptkit/init.sh
|
|
30
30
|
|
|
31
|
-
# courier install (
|
|
31
|
+
# courier install (replace the existing tree; new couriers verify the archive)
|
|
32
32
|
rm -rf .promptkit
|
|
33
33
|
npx promptkit-os@latest --balanced
|
|
34
34
|
```
|
|
@@ -37,7 +37,11 @@ The courier detects which case applies (a submodule install has `.promptkit/.git
|
|
|
37
37
|
|
|
38
38
|
## Version pinning
|
|
39
39
|
|
|
40
|
-
`npx promptkit-os@X.Y.Z` fetches
|
|
40
|
+
`npx promptkit-os@X.Y.Z` fetches the GitHub archive for tag `vX.Y.Z`, not a branch head. New couriers require a valid SHA-256 digest for their own version before download and verify the archive **before** creating `.promptkit/`, extracting files, or running the installer. The release workflow independently checks the tagged archive, injects the digest, and checks the actual npm-packed courier before publication. A missing or mismatched digest stops installation; it is not a warning-only path.
|
|
41
|
+
|
|
42
|
+
**Historical exception:** the already-published immutable npm courier `promptkit-os@1.11.0` did not contain its own archive pin and warns/proceeds by default. npm package integrity/provenance authenticates that npm tarball, **not** the second-stage GitHub archive it downloads. Existing installed files are not retroactively checked or changed by the new policy. For `1.11.0`, an operator can independently obtain a trusted archive SHA-256 and set both `PROMPTKIT_TARBALL_SHA256=<trusted-64-hex-sha256>` and `PROMPTKIT_REQUIRE_INTEGRITY_PIN=1` (in PowerShell, set `$env:PROMPTKIT_TARBALL_SHA256` and `$env:PROMPTKIT_REQUIRE_INTEGRITY_PIN`); the published courier verifies the override before extraction. New couriers also accept an explicitly supplied trusted `PROMPTKIT_TARBALL_SHA256` for archive rotation. The override is an **operator-controlled trust input**: do not derive it from the same untrusted download, and investigate a mismatch rather than disabling verification.
|
|
43
|
+
|
|
44
|
+
GitHub-generated tag archives are not guaranteed byte-for-byte immutable forever. A legitimate archive-regeneration change may require a separately verified override or a new courier release; a fixed tag name alone does not guarantee reproducible archive bytes.
|
|
41
45
|
|
|
42
46
|
## Removal
|
|
43
47
|
|
package/bin/promptkit-os.js
CHANGED
|
@@ -12,7 +12,6 @@ const { spawnSync } = require("node:child_process");
|
|
|
12
12
|
const crypto = require("node:crypto");
|
|
13
13
|
const fs = require("node:fs");
|
|
14
14
|
const path = require("node:path");
|
|
15
|
-
const os = require("node:os");
|
|
16
15
|
|
|
17
16
|
const PKG = require("../package.json");
|
|
18
17
|
const REPO = "lowqualityloey/promptkit-os";
|
|
@@ -24,15 +23,11 @@ const KIT_DIR = ".promptkit";
|
|
|
24
23
|
// a sidecar would either be excluded from the published package (useless) or
|
|
25
24
|
// require a workflow change to allowlist.
|
|
26
25
|
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// tarball before extracting or executing anything from it. If no pin exists for
|
|
33
|
-
// the running version the courier warns loudly and continues (pins can only be
|
|
34
|
-
// minted after the tag exists); set PROMPTKIT_REQUIRE_INTEGRITY_PIN=1 to fail
|
|
35
|
-
// closed instead. PROMPTKIT_TARBALL_SHA256 overrides the map (testing / rotation).
|
|
26
|
+
// Release pins are inserted and checked by the repository release workflow after
|
|
27
|
+
// it checks out the tag and before npm publication. Do not mint pins by hand. A
|
|
28
|
+
// missing or invalid own-version pin fails closed before download. An explicitly
|
|
29
|
+
// supplied PROMPTKIT_TARBALL_SHA256 overrides the map as an operator-controlled
|
|
30
|
+
// trust input for testing or archive rotation.
|
|
36
31
|
//
|
|
37
32
|
// CAVEAT: these digests pin GitHub's *generated* archive for a tag. That archive
|
|
38
33
|
// is reproducible today (verified by double download when each pin was minted)
|
|
@@ -42,6 +37,9 @@ const KIT_DIR = ".promptkit";
|
|
|
42
37
|
// record before treating it as tampering; rotate with PROMPTKIT_TARBALL_SHA256.
|
|
43
38
|
const TARBALL_SHA256_BY_VERSION = {
|
|
44
39
|
"1.10.1": "1e55459914a1c46aa71df8f9cb8ce4c9ff37d1c02d67f70e5607b3bfd4f48d4b",
|
|
40
|
+
"1.11.0": "6503fdfcde286055b8c053aa0ffa8bdf244cf77df3321cccd6a8d8f780195ade",
|
|
41
|
+
"2.0.1": "6f3d667346bbc8cdc42e916fafcbde5dd0e42753398fe528609bf990ea35365b",
|
|
42
|
+
// @pin-insert
|
|
45
43
|
};
|
|
46
44
|
|
|
47
45
|
function die(msg) {
|
|
@@ -62,12 +60,12 @@ function passthroughFlags(argv) {
|
|
|
62
60
|
const out = [];
|
|
63
61
|
for (const arg of argv) {
|
|
64
62
|
if (arg === "--force") continue; // courier-only flag, never forwarded to the installer
|
|
65
|
-
if (known.has(arg) || arg.startsWith("--host=") || arg.startsWith("--target=") || arg.startsWith("--tracking=")) {
|
|
63
|
+
if (known.has(arg) || arg.startsWith("--host=") || arg.startsWith("--add-host=") || arg.startsWith("--target=") || arg.startsWith("--tracking=")) {
|
|
66
64
|
out.push(arg);
|
|
67
65
|
} else if (!arg.startsWith("-")) {
|
|
68
66
|
out.push(arg); // positional project root
|
|
69
67
|
} else {
|
|
70
|
-
die(`unknown flag '${arg}' —
|
|
68
|
+
die(`unknown flag '${arg}' — the courier forwards a fixed allowlist; run the installer directly for full flags (init.sh --help / init.ps1 --help)`);
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
return out;
|
|
@@ -105,20 +103,28 @@ function projectRootFrom(args) {
|
|
|
105
103
|
return path.resolve(positional[0] || process.cwd());
|
|
106
104
|
}
|
|
107
105
|
|
|
106
|
+
function normalizedDigest(digest, version) {
|
|
107
|
+
if (typeof digest !== "string" || !/^[0-9a-fA-F]{64}$/.test(digest)) {
|
|
108
|
+
die(`invalid SHA-256 digest for v${version} — expected exactly 64 hexadecimal characters; refusing to download, extract or execute`);
|
|
109
|
+
}
|
|
110
|
+
return digest.toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
function expectedTarballDigest(version) {
|
|
109
114
|
const override = process.env.PROMPTKIT_TARBALL_SHA256;
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
const digest = override === undefined ? TARBALL_SHA256_BY_VERSION[version] : override;
|
|
116
|
+
if (digest === undefined) {
|
|
117
|
+
die(`no integrity pin for v${version} — refusing to download, extract or execute; supply a trusted PROMPTKIT_TARBALL_SHA256`);
|
|
118
|
+
}
|
|
119
|
+
return normalizedDigest(digest, version);
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
// Aborts (non-zero exit, no extract, no exec) unless `tarball` hashes to
|
|
115
|
-
// `expectedHex`. Comparison is constant-time
|
|
116
|
-
//
|
|
123
|
+
// `expectedHex`. Comparison is constant-time. Must be called after download()
|
|
124
|
+
// and before extract().
|
|
117
125
|
function verifyTarballIntegrity(tarball, expectedHex, version) {
|
|
126
|
+
const expected = Buffer.from(normalizedDigest(expectedHex, version), "hex");
|
|
118
127
|
const actual = crypto.createHash("sha256").update(tarball).digest();
|
|
119
|
-
const expected = Buffer.from(String(expectedHex), "hex");
|
|
120
|
-
// Buffer.from(hex) never throws on bad input — it truncates — so the length
|
|
121
|
-
// check below is what turns a malformed pin into a mismatch (fail closed).
|
|
122
128
|
if (expected.length !== actual.length || !crypto.timingSafeEqual(actual, expected)) {
|
|
123
129
|
die(
|
|
124
130
|
`tarball integrity mismatch for v${version} ` +
|
|
@@ -137,13 +143,21 @@ async function download(url) {
|
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
async function extract(tarball, dest) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
// Every tar operand must stay relative: GNU tar — the first `tar` on PATH inside
|
|
147
|
+
// Git Bash — reads `C:\...` operands as remote host:path and dies with
|
|
148
|
+
// "Cannot connect to C: resolve failed", so the staging-temp-dir + absolute -C
|
|
149
|
+
// form broke the Windows leg wherever Git Bash leads PATH. Staging the archive
|
|
150
|
+
// inside `dest` (created by the caller) and extracting with cwd=dest leaves no
|
|
151
|
+
// absolute path in argv at all, on GNU tar, bsdtar, and POSIX tar alike.
|
|
152
|
+
const stagedTarball = path.join(dest, ".promptkit-release.tar.gz");
|
|
153
|
+
fs.writeFileSync(stagedTarball, tarball);
|
|
154
|
+
const res = spawnSync("tar", ["-xzf", ".promptkit-release.tar.gz", "--strip-components=1"], { cwd: dest, stdio: "inherit" });
|
|
155
|
+
fs.rmSync(stagedTarball, { force: true });
|
|
156
|
+
if (res.error) {
|
|
157
|
+
const hint = res.error.code === "ENOENT" ? "tar not found on PATH" : res.error.message;
|
|
158
|
+
die(`failed to extract release tarball: ${hint}`);
|
|
159
|
+
}
|
|
160
|
+
if (res.status !== 0) die(`failed to extract release tarball (tar exit ${res.status ?? res.signal})`);
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
function runInstaller(kitDir, args) {
|
|
@@ -152,16 +166,32 @@ function runInstaller(kitDir, args) {
|
|
|
152
166
|
const ps = path.join(kitDir, "init.ps1");
|
|
153
167
|
if (!fs.existsSync(ps)) die("init.ps1 missing from release tarball");
|
|
154
168
|
const res = spawnSync("pwsh", ["-NoProfile", "-File", ps, ...args], { stdio: "inherit" });
|
|
155
|
-
if (res.
|
|
169
|
+
if (res.error) {
|
|
170
|
+
const hint = res.error.code === "ENOENT"
|
|
171
|
+
? "pwsh not found — PowerShell 7 is required on Windows (aka.ms/powershell); or use the git-submodule door and run bash .promptkit/init.sh"
|
|
172
|
+
: res.error.message;
|
|
173
|
+
die(`failed to launch init.ps1: ${hint}`);
|
|
174
|
+
}
|
|
175
|
+
if (res.status !== 0) die(`init.ps1 failed (exit ${res.status ?? res.signal})`);
|
|
156
176
|
} else {
|
|
157
177
|
const sh = path.join(kitDir, "init.sh");
|
|
158
178
|
if (!fs.existsSync(sh)) die("init.sh missing from release tarball");
|
|
159
179
|
fs.chmodSync(sh, 0o755);
|
|
160
180
|
const res = spawnSync("bash", [sh, ...args], { stdio: "inherit" });
|
|
161
|
-
if (res.
|
|
181
|
+
if (res.error) {
|
|
182
|
+
const hint = res.error.code === "ENOENT"
|
|
183
|
+
? "bash not found — a POSIX shell is required to run init.sh"
|
|
184
|
+
: res.error.message;
|
|
185
|
+
die(`failed to launch init.sh: ${hint}`);
|
|
186
|
+
}
|
|
187
|
+
if (res.status !== 0) die(`init.sh failed (exit ${res.status ?? res.signal})`);
|
|
162
188
|
}
|
|
163
189
|
}
|
|
164
190
|
|
|
191
|
+
function installerArgs(args, version) {
|
|
192
|
+
return [...args, `--engine-version=v${version}`];
|
|
193
|
+
}
|
|
194
|
+
|
|
165
195
|
async function main() {
|
|
166
196
|
const version = PKG.version;
|
|
167
197
|
if (version === "0.0.0") {
|
|
@@ -182,7 +212,7 @@ async function main() {
|
|
|
182
212
|
" npx promptkit-os@latest --lite\n" +
|
|
183
213
|
" npx promptkit-os@latest --turbo --experimental\n" +
|
|
184
214
|
" npx promptkit-os@latest /path/to/project\n\n" +
|
|
185
|
-
"Pass-through flags: --lite --balanced --turbo --experimental --host= --target= --tracking= --reconfigure\n" +
|
|
215
|
+
"Pass-through flags: --lite --balanced --turbo --experimental --host= --add-host= --target= --tracking= --reconfigure\n" +
|
|
186
216
|
"Courier flags: --force (overlay a non-empty .promptkit/; not recommended)\n\n" +
|
|
187
217
|
"Updating an existing install? Use the canonical path instead:\n" +
|
|
188
218
|
" git submodule update --remote --merge .promptkit && bash .promptkit/init.sh\n\n" +
|
|
@@ -198,32 +228,20 @@ async function main() {
|
|
|
198
228
|
const url = `https://github.com/${REPO}/archive/refs/tags/v${version}.tar.gz`;
|
|
199
229
|
const kitDir = path.join(root, KIT_DIR);
|
|
200
230
|
assertInstallTargetIsClean(kitDir, force);
|
|
201
|
-
fs.mkdirSync(kitDir, { recursive: true });
|
|
202
231
|
|
|
203
|
-
const tarball = await download(url);
|
|
204
232
|
const expectedDigest = expectedTarballDigest(version);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
die(
|
|
210
|
-
`no integrity pin for v${version} — refusing to extract or execute. ` +
|
|
211
|
-
"Add the sha256 of the release tarball to TARBALL_SHA256_BY_VERSION (see above)."
|
|
212
|
-
);
|
|
213
|
-
} else {
|
|
214
|
-
process.stderr.write(
|
|
215
|
-
`[promptkit-os] warning: no integrity pin for v${version}; ` +
|
|
216
|
-
"proceeding unverified — set PROMPTKIT_REQUIRE_INTEGRITY_PIN=1 to fail closed.\n"
|
|
217
|
-
);
|
|
218
|
-
}
|
|
233
|
+
const tarball = await download(url);
|
|
234
|
+
verifyTarballIntegrity(tarball, expectedDigest, version);
|
|
235
|
+
process.stderr.write(`[promptkit-os] verified release tarball integrity (sha256) for v${version}\n`);
|
|
236
|
+
fs.mkdirSync(kitDir, { recursive: true });
|
|
219
237
|
await extract(tarball, kitDir);
|
|
220
238
|
process.stderr.write(`[promptkit-os] extracted release v${version} into ${KIT_DIR}/\n`);
|
|
221
239
|
|
|
222
|
-
runInstaller(kitDir, args);
|
|
240
|
+
runInstaller(kitDir, installerArgs(args, version));
|
|
223
241
|
}
|
|
224
242
|
|
|
225
243
|
if (require.main === module) {
|
|
226
244
|
main().catch((err) => die(err && err.message ? err.message : String(err)));
|
|
227
245
|
} else {
|
|
228
|
-
module.exports = { verifyTarballIntegrity, expectedTarballDigest, TARBALL_SHA256_BY_VERSION };
|
|
246
|
+
module.exports = { verifyTarballIntegrity, expectedTarballDigest, TARBALL_SHA256_BY_VERSION, installerArgs };
|
|
229
247
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptkit-os",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Courier for PromptKit OS: fetches a pinned release tarball and runs the canonical installer. Not a runtime dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"promptkit",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
|
-
"test": "bash test/smoke.sh"
|
|
34
|
+
"test": "bash -c 'if [ -d test ]; then bash test/smoke.sh; fi'"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public",
|