@qpfai/pf-gate-cli 1.0.0
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 +36 -0
- package/artifacts/persons_field-1.0.0-py3-none-any.whl +0 -0
- package/artifacts/python-wheel.json +8 -0
- package/bin/pf.mjs +10 -0
- package/lib/main.mjs +274 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @qpfai/pf-gate-cli
|
|
2
|
+
|
|
3
|
+
Global CLI launcher for PF Gate with first-run runtime bootstrap.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @qpfai/pf-gate-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
Open the Codex-style PF Gate terminal UX:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
PF Gate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run direct CLI commands:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
PF --version
|
|
23
|
+
PF gate --selftest
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
On first run, the launcher creates `~/.pf-gate/runtime/.venv` and installs PF Gate.
|
|
27
|
+
|
|
28
|
+
## Maintainer release flow
|
|
29
|
+
|
|
30
|
+
From repo root:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd npm/pf-gate-cli
|
|
34
|
+
npm run prepare:python
|
|
35
|
+
npm publish --access public
|
|
36
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"generatedAt": "2026-02-16T18:51:30.931Z",
|
|
4
|
+
"pythonPackage": "persons-field",
|
|
5
|
+
"pythonPackageVersion": "1.0.0",
|
|
6
|
+
"wheel": "persons_field-1.0.0-py3-none-any.whl",
|
|
7
|
+
"sha256": "f16ad52f913ea806e8d40a4f49c3a92f50f2708ecce67e7ceaf37b492ed0d793"
|
|
8
|
+
}
|
package/bin/pf.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCli } from "../lib/main.mjs";
|
|
4
|
+
|
|
5
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
console.error(`PF Gate launcher error: ${message}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
|
10
|
+
|
package/lib/main.mjs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
12
|
+
const ARTIFACT_MANIFEST_PATH = path.join(PACKAGE_ROOT, "artifacts", "python-wheel.json");
|
|
13
|
+
const INSTALL_STATE_NAME = "install-state.json";
|
|
14
|
+
const PYTHON_CANDIDATES = ["python3.13", "python3", "python"];
|
|
15
|
+
|
|
16
|
+
function isWindows() {
|
|
17
|
+
return process.platform === "win32";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function runtimePythonPath(runtimeRoot) {
|
|
21
|
+
if (isWindows()) {
|
|
22
|
+
return path.join(runtimeRoot, ".venv", "Scripts", "python.exe");
|
|
23
|
+
}
|
|
24
|
+
return path.join(runtimeRoot, ".venv", "bin", "python");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runtimePipArgs(runtimeRoot) {
|
|
28
|
+
return ["-m", "pip"];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runtimeLauncherArgs(args) {
|
|
32
|
+
return ["-m", "persons_field.terminal.launcher", ...args];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function shouldOpenUx(args) {
|
|
36
|
+
if (args.length === 0) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (args.length !== 1) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const first = String(args[0] || "").trim().toLowerCase();
|
|
43
|
+
return first === "gate" || first === "ux" || first === "shell";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadPackageMetadata() {
|
|
47
|
+
const raw = fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf-8");
|
|
48
|
+
const payload = JSON.parse(raw);
|
|
49
|
+
return {
|
|
50
|
+
name: String(payload.name || "@pfgate/cli"),
|
|
51
|
+
version: String(payload.version || "0.0.0"),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureDir(dirPath) {
|
|
56
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runChecked(command, args, options = {}) {
|
|
60
|
+
const result = spawnSync(command, args, {
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
...options,
|
|
63
|
+
});
|
|
64
|
+
if (result.error) {
|
|
65
|
+
throw result.error;
|
|
66
|
+
}
|
|
67
|
+
if ((result.status ?? 1) !== 0) {
|
|
68
|
+
throw new Error(`Command failed (${result.status}): ${command} ${args.join(" ")}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runCapture(command, args) {
|
|
73
|
+
const result = spawnSync(command, args, {
|
|
74
|
+
stdio: "pipe",
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
});
|
|
77
|
+
if (result.error) {
|
|
78
|
+
return { ok: false, output: "" };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
ok: (result.status ?? 1) === 0,
|
|
82
|
+
output: String(result.stdout || "").trim(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolvePython() {
|
|
87
|
+
const envPython = (process.env.PF_GATE_PYTHON || "").trim();
|
|
88
|
+
const candidates = envPython ? [envPython, ...PYTHON_CANDIDATES] : PYTHON_CANDIDATES;
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
const probe = runCapture(candidate, ["--version"]);
|
|
91
|
+
if (probe.ok) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new Error(
|
|
96
|
+
"No Python runtime found. Install Python 3.13+ or set PF_GATE_PYTHON to an explicit executable."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sha256File(filePath) {
|
|
101
|
+
const data = fs.readFileSync(filePath);
|
|
102
|
+
return createHash("sha256").update(data).digest("hex");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseManifest() {
|
|
106
|
+
if (!fs.existsSync(ARTIFACT_MANIFEST_PATH)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const raw = fs.readFileSync(ARTIFACT_MANIFEST_PATH, "utf-8");
|
|
110
|
+
const payload = JSON.parse(raw);
|
|
111
|
+
const wheelName = String(payload.wheel || "").trim();
|
|
112
|
+
if (!wheelName) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const wheelPath = path.join(PACKAGE_ROOT, "artifacts", wheelName);
|
|
116
|
+
if (!fs.existsSync(wheelPath)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const expectedSha = String(payload.sha256 || "").trim().toLowerCase();
|
|
120
|
+
if (expectedSha) {
|
|
121
|
+
const actualSha = sha256File(wheelPath);
|
|
122
|
+
if (actualSha !== expectedSha) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Wheel checksum mismatch for ${wheelName}. Expected ${expectedSha}, got ${actualSha}.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
kind: "wheel",
|
|
130
|
+
wheelPath,
|
|
131
|
+
wheelName,
|
|
132
|
+
sha256: expectedSha || null,
|
|
133
|
+
packageVersion: String(payload.pythonPackageVersion || "").trim() || null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveInstallSource(cliVersion) {
|
|
138
|
+
const manifestSource = parseManifest();
|
|
139
|
+
if (manifestSource) {
|
|
140
|
+
return manifestSource;
|
|
141
|
+
}
|
|
142
|
+
const pipSpec = (process.env.PF_GATE_PIP_SPEC || `persons-field==${cliVersion}`).trim();
|
|
143
|
+
return {
|
|
144
|
+
kind: "pip",
|
|
145
|
+
pipSpec,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function installStatePath(runtimeRoot) {
|
|
150
|
+
return path.join(runtimeRoot, INSTALL_STATE_NAME);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function loadInstallState(runtimeRoot) {
|
|
154
|
+
const statePath = installStatePath(runtimeRoot);
|
|
155
|
+
if (!fs.existsSync(statePath)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
160
|
+
} catch (_error) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeInstallState(runtimeRoot, payload) {
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
installStatePath(runtimeRoot),
|
|
168
|
+
JSON.stringify(payload, null, 2) + "\n",
|
|
169
|
+
"utf-8"
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function desiredStateFingerprint(source, cliVersion) {
|
|
174
|
+
if (source.kind === "wheel") {
|
|
175
|
+
return `wheel:${source.wheelName}:${source.sha256 || "nosha"}:${cliVersion}`;
|
|
176
|
+
}
|
|
177
|
+
return `pip:${source.pipSpec}:${cliVersion}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isRuntimeHealthy(runtimeRoot, source, cliVersion) {
|
|
181
|
+
const pythonPath = runtimePythonPath(runtimeRoot);
|
|
182
|
+
if (!fs.existsSync(pythonPath)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const state = loadInstallState(runtimeRoot);
|
|
186
|
+
const fingerprint = desiredStateFingerprint(source, cliVersion);
|
|
187
|
+
if (!state || String(state.fingerprint || "") !== fingerprint) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const probe = runCapture(pythonPath, ["-c", "import persons_field"]);
|
|
191
|
+
return probe.ok;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ensureVirtualenv(runtimeRoot, bootstrapPython) {
|
|
195
|
+
const pythonPath = runtimePythonPath(runtimeRoot);
|
|
196
|
+
if (fs.existsSync(pythonPath)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
console.log("PF Gate bootstrap: creating runtime virtual environment...");
|
|
200
|
+
runChecked(bootstrapPython, ["-m", "venv", path.join(runtimeRoot, ".venv")]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function installRuntime(runtimeRoot, source, cliVersion) {
|
|
204
|
+
const pythonPath = runtimePythonPath(runtimeRoot);
|
|
205
|
+
const skipPipUpgrade = String(process.env.PF_GATE_SKIP_PIP_UPGRADE || "").trim() === "1";
|
|
206
|
+
if (!skipPipUpgrade) {
|
|
207
|
+
console.log("PF Gate bootstrap: updating pip...");
|
|
208
|
+
runChecked(pythonPath, [...runtimePipArgs(runtimeRoot), "install", "--upgrade", "pip"]);
|
|
209
|
+
}
|
|
210
|
+
console.log("PF Gate bootstrap: installing PF Gate runtime...");
|
|
211
|
+
if (source.kind === "wheel") {
|
|
212
|
+
runChecked(pythonPath, [
|
|
213
|
+
...runtimePipArgs(runtimeRoot),
|
|
214
|
+
"install",
|
|
215
|
+
"--upgrade",
|
|
216
|
+
source.wheelPath,
|
|
217
|
+
]);
|
|
218
|
+
} else {
|
|
219
|
+
runChecked(pythonPath, [
|
|
220
|
+
...runtimePipArgs(runtimeRoot),
|
|
221
|
+
"install",
|
|
222
|
+
"--upgrade",
|
|
223
|
+
source.pipSpec,
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
const fingerprint = desiredStateFingerprint(source, cliVersion);
|
|
227
|
+
writeInstallState(runtimeRoot, {
|
|
228
|
+
installedAt: new Date().toISOString(),
|
|
229
|
+
cliVersion,
|
|
230
|
+
fingerprint,
|
|
231
|
+
});
|
|
232
|
+
console.log("PF Gate bootstrap: runtime ready.");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveRuntimeRoot() {
|
|
236
|
+
const appHome = (process.env.PF_GATE_HOME || "").trim() || path.join(os.homedir(), ".pf-gate");
|
|
237
|
+
return path.join(appHome, "runtime");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function dryRunEnabled() {
|
|
241
|
+
return String(process.env.PF_GATE_BOOTSTRAP_DRY_RUN || "").trim() === "1";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ensureRuntimeInstalled(cliVersion) {
|
|
245
|
+
const runtimeRoot = resolveRuntimeRoot();
|
|
246
|
+
ensureDir(runtimeRoot);
|
|
247
|
+
const source = resolveInstallSource(cliVersion);
|
|
248
|
+
if (!isRuntimeHealthy(runtimeRoot, source, cliVersion)) {
|
|
249
|
+
const bootstrapPython = resolvePython();
|
|
250
|
+
ensureVirtualenv(runtimeRoot, bootstrapPython);
|
|
251
|
+
installRuntime(runtimeRoot, source, cliVersion);
|
|
252
|
+
}
|
|
253
|
+
return runtimeRoot;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function runCli(args) {
|
|
257
|
+
const metadata = loadPackageMetadata();
|
|
258
|
+
if (dryRunEnabled()) {
|
|
259
|
+
const uiMode = shouldOpenUx(args) ? "ux" : "passthrough";
|
|
260
|
+
console.log(`PF Gate launcher dry-run (${uiMode})`);
|
|
261
|
+
console.log(`args: ${JSON.stringify(args)}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const runtimeRoot = ensureRuntimeInstalled(metadata.version);
|
|
265
|
+
const pythonPath = runtimePythonPath(runtimeRoot);
|
|
266
|
+
const resolvedArgs = args.length === 0 ? ["Gate"] : args;
|
|
267
|
+
const child = spawnSync(pythonPath, runtimeLauncherArgs(resolvedArgs), {
|
|
268
|
+
stdio: "inherit",
|
|
269
|
+
});
|
|
270
|
+
if (child.error) {
|
|
271
|
+
throw child.error;
|
|
272
|
+
}
|
|
273
|
+
process.exit(child.status ?? 1);
|
|
274
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qpfai/pf-gate-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PF Gate terminal launcher with first-run runtime bootstrap.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"PF": "bin/pf.mjs",
|
|
9
|
+
"pf-gate": "bin/pf.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"lib",
|
|
14
|
+
"artifacts",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"prepare:python": "node scripts/prepare-python-wheel.mjs",
|
|
19
|
+
"prepack": "npm run prepare:python",
|
|
20
|
+
"test": "node --test tests/*.test.mjs"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
}
|
|
28
|
+
}
|