plain-forge 1.0.22 → 1.0.23
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
CHANGED
|
@@ -56,7 +56,7 @@ This prompts you to pick an agent and a scope using an arrow-key menu. You can a
|
|
|
56
56
|
npx plain-forge install --agent claude --scope project
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
`install` also installs [`plain-parser`](https://pypi.org/project/plain-parser/), the Python CLI that `plain-healthcheck` uses to validate `.plain` files. It installs it with [uv](https://docs.astral.sh/uv/), installing uv first if it is missing, on Linux, macOS and Windows alike. If plain-parser is already on your `PATH`, nothing is installed. Pass `--skip-plain-parser` to leave both alone.
|
|
59
|
+
`install` also installs [`plain-parser`](https://pypi.org/project/plain-parser/), the Python CLI that `plain-healthcheck` uses to validate `.plain` files. It installs it with [uv](https://docs.astral.sh/uv/), installing uv first if it is missing, on Linux, macOS and Windows alike. On Linux and macOS uv is downloaded by Node itself, so it also works in minimal Docker images without curl, wget or CA certificates; uv then fetches a Python for plain-parser if the host has none. uv puts plain-parser in `~/.local/bin`, which may not be on your `PATH` yet — `install` prints the `export PATH=…` line to add it. If plain-parser is already on your `PATH`, nothing is installed. Pass `--skip-plain-parser` to leave both alone.
|
|
60
60
|
|
|
61
61
|
**Agent options:**
|
|
62
62
|
|
|
@@ -218,6 +218,7 @@ forge/ # canonical content, copied verbatim on install
|
|
|
218
218
|
bin/
|
|
219
219
|
cli.mjs # the `plain-forge` CLI — `install` and `update` commands
|
|
220
220
|
python-tools.mjs # installs uv + plain-parser during `install` (until plain-parser ships on npm)
|
|
221
|
+
download-uv.mjs # downloads uv with Node's fetch, for hosts without curl/wget or CA certificates
|
|
221
222
|
|
|
222
223
|
vendor/
|
|
223
224
|
pyro/ # git submodule: plainlang/pyro, pinned to the release plain-forge ships
|
|
@@ -230,7 +231,7 @@ test/
|
|
|
230
231
|
cli.test.mjs # tests for the install / update CLI
|
|
231
232
|
python-tools.test.mjs # tests for the uv + plain-parser install step
|
|
232
233
|
|
|
233
|
-
package.json # ships only `bin/cli.mjs`, `bin/python-tools.mjs` and `forge/` to npm
|
|
234
|
+
package.json # ships only `bin/cli.mjs`, `bin/python-tools.mjs`, `bin/download-uv.mjs` and `forge/` to npm
|
|
234
235
|
```
|
|
235
236
|
|
|
236
237
|
### Bundled pyro skill
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Downloads a uv release archive with Node's fetch, checks it against the
|
|
2
|
+
// published SHA-256, and puts `uv` and `uvx` into a bin directory.
|
|
3
|
+
//
|
|
4
|
+
// node download-uv.mjs <archive-url> <bin-dir>
|
|
5
|
+
//
|
|
6
|
+
// Unlike astral's install.sh this needs neither curl/wget nor the system CA
|
|
7
|
+
// certificates, which minimal Docker images lack. Only `tar` is required.
|
|
8
|
+
//
|
|
9
|
+
// Temporary, like python-tools.mjs, which runs it: both go away once
|
|
10
|
+
// plain-parser ships as an npm package.
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
async function get(url) {
|
|
18
|
+
const res = await fetch(url);
|
|
19
|
+
if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
|
|
20
|
+
return Buffer.from(await res.arrayBuffer());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function main(url, binDir) {
|
|
24
|
+
const [archive, checksum] = await Promise.all([get(url), get(`${url}.sha256`)]);
|
|
25
|
+
const expected = checksum.toString("utf8").trim().split(/\s+/)[0];
|
|
26
|
+
const actual = crypto.createHash("sha256").update(archive).digest("hex");
|
|
27
|
+
if (expected !== actual) throw new Error(`checksum mismatch for ${url}`);
|
|
28
|
+
|
|
29
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "plain-forge-uv-"));
|
|
30
|
+
try {
|
|
31
|
+
fs.writeFileSync(path.join(tmp, "uv.tar.gz"), archive);
|
|
32
|
+
const tar = spawnSync("tar", ["-xzf", "uv.tar.gz"], { cwd: tmp, stdio: "inherit" });
|
|
33
|
+
if (tar.error || tar.status !== 0) {
|
|
34
|
+
throw new Error(`could not unpack the archive: ${tar.error?.message ?? "tar failed"}`);
|
|
35
|
+
}
|
|
36
|
+
// The archive holds a single folder named after it: uv-<target>/{uv,uvx}.
|
|
37
|
+
const srcDir = path.join(tmp, path.basename(url, ".tar.gz"));
|
|
38
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
39
|
+
for (const exe of ["uv", "uvx"]) {
|
|
40
|
+
const dest = path.join(binDir, exe);
|
|
41
|
+
fs.copyFileSync(path.join(srcDir, exe), dest);
|
|
42
|
+
fs.chmodSync(dest, 0o755);
|
|
43
|
+
}
|
|
44
|
+
console.log(`installed uv and uvx into ${binDir}`);
|
|
45
|
+
} finally {
|
|
46
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [url, binDir] = process.argv.slice(2);
|
|
51
|
+
try {
|
|
52
|
+
await main(url, binDir);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(`downloading uv failed: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
package/bin/python-tools.mjs
CHANGED
|
@@ -10,9 +10,21 @@ import { spawnSync } from "node:child_process";
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
13
14
|
|
|
14
15
|
const PLAIN_PARSER = "plain-parser";
|
|
15
16
|
|
|
17
|
+
const DOWNLOAD_UV = path.join(path.dirname(fileURLToPath(import.meta.url)), "download-uv.mjs");
|
|
18
|
+
const UV_RELEASES = "https://github.com/astral-sh/uv/releases/latest/download";
|
|
19
|
+
// uv release targets per `${platform}-${arch}`. The Linux builds are static
|
|
20
|
+
// musl binaries, so they run on glibc and musl (Alpine) hosts alike.
|
|
21
|
+
const UV_TARGETS = {
|
|
22
|
+
"linux-x64": "x86_64-unknown-linux-musl",
|
|
23
|
+
"linux-arm64": "aarch64-unknown-linux-musl",
|
|
24
|
+
"darwin-x64": "x86_64-apple-darwin",
|
|
25
|
+
"darwin-arm64": "aarch64-apple-darwin",
|
|
26
|
+
};
|
|
27
|
+
|
|
16
28
|
const UV_INSTALLERS = {
|
|
17
29
|
win32: {
|
|
18
30
|
cmd: "powershell.exe",
|
|
@@ -39,8 +51,27 @@ const UV_INSTALLERS = {
|
|
|
39
51
|
},
|
|
40
52
|
};
|
|
41
53
|
|
|
42
|
-
function
|
|
43
|
-
return platform === "win32" ? UV_INSTALLERS.win32 : UV_INSTALLERS.posix;
|
|
54
|
+
function uvManual(platform) {
|
|
55
|
+
return platform === "win32" ? UV_INSTALLERS.win32.manual : UV_INSTALLERS.posix.manual;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Where the uv installer puts uv, unless told otherwise.
|
|
59
|
+
function uvInstallDir(env, home) {
|
|
60
|
+
return env.UV_INSTALL_DIR || env.XDG_BIN_HOME || path.join(home, ".local", "bin");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The ways to install uv, tried in order. On Linux and macOS, uv is first
|
|
64
|
+
// downloaded with Node itself (download-uv.mjs), which needs neither curl/wget
|
|
65
|
+
// nor system CA certificates; astral's install.sh is the fallback for
|
|
66
|
+
// platforms without a known release target, or when that download fails.
|
|
67
|
+
function uvInstallers(platform, arch, env, home) {
|
|
68
|
+
if (platform === "win32") return [UV_INSTALLERS.win32];
|
|
69
|
+
const target = UV_TARGETS[`${platform}-${arch}`];
|
|
70
|
+
const download = target && {
|
|
71
|
+
cmd: process.execPath,
|
|
72
|
+
args: [DOWNLOAD_UV, `${UV_RELEASES}/uv-${target}.tar.gz`, uvInstallDir(env, home)],
|
|
73
|
+
};
|
|
74
|
+
return [download, UV_INSTALLERS.posix].filter(Boolean);
|
|
44
75
|
}
|
|
45
76
|
|
|
46
77
|
// Run a command. `inherit` streams its output to the user; otherwise stdout is
|
|
@@ -76,16 +107,22 @@ function findUv({ platform, env, home, run, exists }) {
|
|
|
76
107
|
return uvCandidates(platform, env, home).find(exists) ?? null;
|
|
77
108
|
}
|
|
78
109
|
|
|
79
|
-
function warnOffPath(log, binDir) {
|
|
110
|
+
function warnOffPath(log, binDir, platform) {
|
|
80
111
|
log(` plain-parser: installed${binDir ? ` into ${binDir}` : ""}, which is not on your PATH yet.`);
|
|
81
|
-
|
|
112
|
+
if (binDir && platform !== "win32") {
|
|
113
|
+
log(` add it to PATH for this shell with:`);
|
|
114
|
+
log(` export PATH="${binDir}:$PATH"`);
|
|
115
|
+
log(` and run "uv tool update-shell" to make that permanent for new terminals.`);
|
|
116
|
+
} else {
|
|
117
|
+
log(` run "uv tool update-shell" and open a new terminal.`);
|
|
118
|
+
}
|
|
82
119
|
return { status: "not-on-path" };
|
|
83
120
|
}
|
|
84
121
|
|
|
85
122
|
function warnManual(log, reason, platform) {
|
|
86
123
|
log(` plain-parser: not installed — ${reason}`);
|
|
87
124
|
log(` plain-forge's healthcheck needs it. Install it yourself with:`);
|
|
88
|
-
log(` ${
|
|
125
|
+
log(` ${uvManual(platform)}`);
|
|
89
126
|
log(` uv tool install ${PLAIN_PARSER}`);
|
|
90
127
|
}
|
|
91
128
|
|
|
@@ -93,6 +130,7 @@ function warnManual(log, reason, platform) {
|
|
|
93
130
|
// Returns { status } — "present", "installed", "not-on-path", or "failed".
|
|
94
131
|
function ensurePlainParser({
|
|
95
132
|
platform = process.platform,
|
|
133
|
+
arch = process.arch,
|
|
96
134
|
env = process.env,
|
|
97
135
|
home = os.homedir(),
|
|
98
136
|
run = runCommand,
|
|
@@ -107,9 +145,11 @@ function ensurePlainParser({
|
|
|
107
145
|
let uv = findUv({ platform, env, home, run, exists });
|
|
108
146
|
if (!uv) {
|
|
109
147
|
log(` uv: not found — installing it (https://docs.astral.sh/uv/)`);
|
|
110
|
-
const { cmd, args }
|
|
111
|
-
|
|
112
|
-
|
|
148
|
+
for (const { cmd, args } of uvInstallers(platform, arch, env, home)) {
|
|
149
|
+
const r = run(cmd, args, { inherit: true });
|
|
150
|
+
uv = !r.error && r.status === 0 ? findUv({ platform, env, home, run, exists }) : null;
|
|
151
|
+
if (uv) break;
|
|
152
|
+
}
|
|
113
153
|
if (!uv) {
|
|
114
154
|
warnManual(log, "installing uv failed", platform);
|
|
115
155
|
return { status: "failed" };
|
|
@@ -119,7 +159,7 @@ function ensurePlainParser({
|
|
|
119
159
|
// Installed by an earlier run, but its bin dir is not on PATH.
|
|
120
160
|
const binDir = run(uv, ["tool", "dir", "--bin"]).stdout.trim();
|
|
121
161
|
const exe = platform === "win32" ? `${PLAIN_PARSER}.exe` : PLAIN_PARSER;
|
|
122
|
-
if (binDir && exists(path.join(binDir, exe))) return warnOffPath(log, binDir);
|
|
162
|
+
if (binDir && exists(path.join(binDir, exe))) return warnOffPath(log, binDir, platform);
|
|
123
163
|
|
|
124
164
|
log(` plain-parser: not found — installing it with uv`);
|
|
125
165
|
// uv downloads a suitable Python (3.11+) itself when the host has none.
|
|
@@ -133,7 +173,7 @@ function ensurePlainParser({
|
|
|
133
173
|
log(` plain-parser: installed`);
|
|
134
174
|
return { status: "installed" };
|
|
135
175
|
}
|
|
136
|
-
return warnOffPath(log, binDir);
|
|
176
|
+
return warnOffPath(log, binDir, platform);
|
|
137
177
|
}
|
|
138
178
|
|
|
139
|
-
export { ensurePlainParser,
|
|
179
|
+
export { ensurePlainParser, uvInstallers, uvCandidates };
|
|
@@ -149,7 +149,7 @@ Add these to every requirement list regardless of project:
|
|
|
149
149
|
|
|
150
150
|
- A shell matching the testing scripts' extension (Bash for `.sh`, PowerShell for `.ps1`).
|
|
151
151
|
- `git` (the renderer uses it; almost every plain project tracks itself in git).
|
|
152
|
-
- `plain-parser` and **Python 3.11 or newer** — `plain-healthcheck` validates every module with `plain-parser check`. Probe `python3 --version` and `plain-parser --help`; install with `pipx install plain-parser` or `uv tool install plain-parser`.
|
|
152
|
+
- `plain-parser` and **Python 3.11 or newer** — `plain-healthcheck` validates every module with `plain-parser check`. Probe `python3 --version` and `plain-parser --help`; install with `pipx install plain-parser` or `uv tool install plain-parser`. When `plain-parser --help` is not found but `~/.local/bin/plain-parser` (or the same file in `uv tool dir --bin`) exists, report `WARN`, not `FAIL`: it is installed but off `PATH`, fixed with `export PATH="$HOME/.local/bin:$PATH"` or `uv tool update-shell`. `plain-parser` is installed with uv, which fetches its own Python, so do not `FAIL` on a missing or older system `python3` when `plain-parser` itself runs.
|
|
153
153
|
|
|
154
154
|
### Step 3 — Probe the host
|
|
155
155
|
|
|
@@ -76,7 +76,7 @@ Step 3 has two halves: a mechanical parse by `plain-parser`, then the checks tha
|
|
|
76
76
|
|
|
77
77
|
[`plain-parser`](https://pypi.org/project/plain-parser/) is the ***plain parser. It parses a module together with its whole `import` / `requires` chain, so running it on every top module covers every module in the project.
|
|
78
78
|
|
|
79
|
-
1. **Check it is installed.** Run `plain-parser --help`. If the command is not found, stop and report `FAIL` with the install command (`pipx install plain-parser` or `uv tool install plain-parser`; it needs Python 3.11 or newer). Do **not** fall back to checking these rules by reading the files — without the parser there is no syntax gate.
|
|
79
|
+
1. **Check it is installed.** Run `plain-parser --help`. If the command is not found, look for it where `plain-forge install` puts it before giving up: `~/.local/bin/plain-parser` (`%USERPROFILE%\.local\bin\plain-parser.exe` on Windows), or the directory printed by `uv tool dir --bin`. If it is there, run it by that full path for the rest of this step and tell the user to add the directory to `PATH`. If it is nowhere, stop and report `FAIL` with the install command (`pipx install plain-parser` or `uv tool install plain-parser`; it needs Python 3.11 or newer). Do **not** fall back to checking these rules by reading the files — without the parser there is no syntax gate.
|
|
80
80
|
2. **Run it from the top module's own directory**, once per top module:
|
|
81
81
|
|
|
82
82
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plain-forge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"description": "Conversational spec-writing tool for ***plain specification language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"files": [
|
|
29
29
|
"bin/cli.mjs",
|
|
30
30
|
"bin/python-tools.mjs",
|
|
31
|
+
"bin/download-uv.mjs",
|
|
31
32
|
"forge"
|
|
32
33
|
],
|
|
33
34
|
"scripts": {
|