flowleap 0.2.3 → 0.2.4
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/bin/flowleap +10 -10
- package/download.mjs +164 -0
- package/package.json +2 -5
- package/install.mjs +0 -148
package/bin/flowleap
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from "child_process";
|
|
3
|
-
import {
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
5
|
-
import { existsSync } from "fs";
|
|
3
|
+
import { ensureBinary, findBinary } from "../download.mjs";
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
let bin = findBinary();
|
|
6
|
+
if (!bin) {
|
|
7
|
+
try {
|
|
8
|
+
bin = await ensureBinary();
|
|
9
|
+
} catch (err) {
|
|
10
|
+
console.error(`flowleap: could not fetch the native binary: ${err.message}`);
|
|
11
|
+
console.error("Retry, or download it manually from https://github.com/abdullahatrash/flowleap-cli/releases");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
try {
|
package/download.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Lazy binary fetcher, invoked from bin/flowleap on first run. There is
|
|
2
|
+
// deliberately no npm install script: install-time code is what npm's
|
|
3
|
+
// allow-scripts guard exists to flag. The binary is downloaded from the
|
|
4
|
+
// GitHub release matching this package's version and verified against the
|
|
5
|
+
// release's checksums.txt before it is ever executed.
|
|
6
|
+
//
|
|
7
|
+
// All human-facing output goes to stderr — the first run may be
|
|
8
|
+
// `flowleap --json …` piped by an agent, and stdout must stay parseable.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { createHash } from "crypto";
|
|
18
|
+
import { join, dirname } from "path";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const REPO = "abdullahatrash/flowleap-cli";
|
|
24
|
+
const isWindows = process.platform === "win32";
|
|
25
|
+
const binaryName = isWindows ? "flowleap-native.exe" : "flowleap-native";
|
|
26
|
+
|
|
27
|
+
// Release tags are named "v<version>"; the publish workflow stamps the npm
|
|
28
|
+
// version from the tag, so package.json is always the source of truth.
|
|
29
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
|
30
|
+
const VERSION = `v${pkg.version}`;
|
|
31
|
+
|
|
32
|
+
const PLATFORM_MAP = {
|
|
33
|
+
"darwin-arm64": "flowleap-darwin-aarch64",
|
|
34
|
+
"darwin-x64": "flowleap-darwin-x86_64",
|
|
35
|
+
"linux-arm64": "flowleap-linux-aarch64",
|
|
36
|
+
"linux-x64": "flowleap-linux-x86_64",
|
|
37
|
+
"win32-x64": "flowleap-windows-x86_64.exe",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Preferred location is inside the package (fast path, removed on uninstall);
|
|
41
|
+
// the per-user cache is the fallback when the package dir isn't writable
|
|
42
|
+
// (e.g. sudo-installed global node).
|
|
43
|
+
function candidates() {
|
|
44
|
+
const cacheBase = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
45
|
+
return [
|
|
46
|
+
join(__dirname, "bin", binaryName),
|
|
47
|
+
join(cacheBase, "flowleap", VERSION, binaryName),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function findBinary() {
|
|
52
|
+
return candidates().find(existsSync) ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatBytes(n) {
|
|
56
|
+
if (!Number.isFinite(n) || n <= 0) return "0 B";
|
|
57
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (n >= 1024 && i < units.length - 1) {
|
|
60
|
+
n /= 1024;
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
return `${n.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function progressReporter(total) {
|
|
67
|
+
const isTTY = Boolean(process.stderr.isTTY);
|
|
68
|
+
const hasTotal = Number.isFinite(total) && total > 0;
|
|
69
|
+
let received = 0;
|
|
70
|
+
let lastRenderAt = 0;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
update(chunkSize) {
|
|
74
|
+
received += chunkSize;
|
|
75
|
+
if (!isTTY) return; // non-TTY (agents, CI): stay quiet beyond the one-line notice
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
if (now - lastRenderAt < 100 && received !== total) return;
|
|
78
|
+
lastRenderAt = now;
|
|
79
|
+
const line = hasTotal
|
|
80
|
+
? ` ${formatBytes(received)} / ${formatBytes(total)} (${Math.floor((received / total) * 100)}%)`
|
|
81
|
+
: ` ${formatBytes(received)}…`;
|
|
82
|
+
process.stderr.write(`\r${line}`);
|
|
83
|
+
},
|
|
84
|
+
finish() {
|
|
85
|
+
if (isTTY) process.stderr.write("\n");
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function fetchOk(url) {
|
|
91
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
94
|
+
}
|
|
95
|
+
return res;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function downloadBytes(url) {
|
|
99
|
+
const res = await fetchOk(url);
|
|
100
|
+
const total = parseInt(res.headers.get("content-length") || "", 10);
|
|
101
|
+
const progress = progressReporter(total);
|
|
102
|
+
const reader = res.body.getReader();
|
|
103
|
+
const chunks = [];
|
|
104
|
+
for (;;) {
|
|
105
|
+
const { done, value } = await reader.read();
|
|
106
|
+
if (done) break;
|
|
107
|
+
chunks.push(value);
|
|
108
|
+
progress.update(value.length);
|
|
109
|
+
}
|
|
110
|
+
progress.finish();
|
|
111
|
+
return Buffer.concat(chunks);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// checksums.txt lines look like "<sha256> <asset-dir>/<asset>".
|
|
115
|
+
function expectedChecksum(text, asset) {
|
|
116
|
+
for (const line of text.split("\n")) {
|
|
117
|
+
const [hash, file] = line.trim().split(/\s+/);
|
|
118
|
+
if (file && (file === asset || file.endsWith(`/${asset}`))) return hash;
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`no checksum entry for ${asset} in checksums.txt`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function ensureBinary() {
|
|
124
|
+
const existing = findBinary();
|
|
125
|
+
if (existing) return existing;
|
|
126
|
+
|
|
127
|
+
const key = `${process.platform}-${process.arch}`;
|
|
128
|
+
const asset = PLATFORM_MAP[key];
|
|
129
|
+
if (!asset) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`unsupported platform ${key} (supported: ${Object.keys(PLATFORM_MAP).join(", ")})`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.stderr.write(`flowleap: first run — downloading ${VERSION} for ${key}…\n`);
|
|
136
|
+
const base = `https://github.com/${REPO}/releases/download/${VERSION}`;
|
|
137
|
+
const [bytes, checksums] = await Promise.all([
|
|
138
|
+
downloadBytes(`${base}/${asset}`),
|
|
139
|
+
fetchOk(`${base}/checksums.txt`).then((r) => r.text()),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
const expected = expectedChecksum(checksums, asset);
|
|
143
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
144
|
+
if (actual !== expected) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`sha256 mismatch for ${asset}: expected ${expected}, got ${actual} — refusing to install`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const target of candidates()) {
|
|
151
|
+
try {
|
|
152
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
153
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
154
|
+
writeFileSync(tmp, bytes, { mode: 0o755 });
|
|
155
|
+
renameSync(tmp, target);
|
|
156
|
+
process.stderr.write(`flowleap: sha256 verified, installed to ${target}\n`);
|
|
157
|
+
return target;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (["EACCES", "EPERM", "EROFS"].includes(err.code)) continue;
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new Error("no writable location for the flowleap binary");
|
|
164
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowleap",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "One CLI for FlowLeap Patent AI — built for humans and AI agents",
|
|
5
5
|
"bin": {
|
|
6
6
|
"flowleap": "bin/flowleap"
|
|
7
7
|
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"postinstall": "node install.mjs"
|
|
10
|
-
},
|
|
11
8
|
"keywords": [
|
|
12
9
|
"patent",
|
|
13
10
|
"ai",
|
|
@@ -25,6 +22,6 @@
|
|
|
25
22
|
},
|
|
26
23
|
"homepage": "https://flowleap.co",
|
|
27
24
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
25
|
+
"node": ">=18"
|
|
29
26
|
}
|
|
30
27
|
}
|
package/install.mjs
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { createWriteStream, chmodSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join, dirname } from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
import { get } from "https";
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const REPO = "abdullahatrash/flowleap-cli";
|
|
8
|
-
|
|
9
|
-
// Read version from package.json so the release tag stays in sync with the
|
|
10
|
-
// published npm version automatically. Assumes release tags are named "v<version>".
|
|
11
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
|
|
12
|
-
const VERSION = `v${pkg.version}`;
|
|
13
|
-
|
|
14
|
-
const PLATFORM_MAP = {
|
|
15
|
-
"darwin-arm64": "flowleap-darwin-aarch64",
|
|
16
|
-
"darwin-x64": "flowleap-darwin-x86_64",
|
|
17
|
-
"linux-arm64": "flowleap-linux-aarch64",
|
|
18
|
-
"linux-x64": "flowleap-linux-x86_64",
|
|
19
|
-
"win32-x64": "flowleap-windows-x86_64.exe",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const key = `${process.platform}-${process.arch}`;
|
|
23
|
-
const asset = PLATFORM_MAP[key];
|
|
24
|
-
|
|
25
|
-
if (!asset) {
|
|
26
|
-
console.error(`Unsupported platform: ${key}`);
|
|
27
|
-
console.error(`Supported: ${Object.keys(PLATFORM_MAP).join(", ")}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const binDir = join(__dirname, "bin");
|
|
32
|
-
const isWindows = process.platform === "win32";
|
|
33
|
-
const binaryName = isWindows ? "flowleap-native.exe" : "flowleap-native";
|
|
34
|
-
const binPath = join(binDir, binaryName);
|
|
35
|
-
|
|
36
|
-
if (existsSync(binPath)) {
|
|
37
|
-
process.exit(0);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
mkdirSync(binDir, { recursive: true });
|
|
41
|
-
|
|
42
|
-
const url = `https://github.com/${REPO}/releases/download/${VERSION}/${asset}`;
|
|
43
|
-
|
|
44
|
-
console.log(`Downloading flowleap ${VERSION} for ${key}...`);
|
|
45
|
-
|
|
46
|
-
function formatBytes(n) {
|
|
47
|
-
if (!Number.isFinite(n) || n <= 0) return "0 B";
|
|
48
|
-
const units = ["B", "KB", "MB", "GB"];
|
|
49
|
-
let i = 0;
|
|
50
|
-
while (n >= 1024 && i < units.length - 1) {
|
|
51
|
-
n /= 1024;
|
|
52
|
-
i++;
|
|
53
|
-
}
|
|
54
|
-
return `${n.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function renderBar(fraction, width = 24) {
|
|
58
|
-
const filled = Math.max(0, Math.min(width, Math.round(fraction * width)));
|
|
59
|
-
const empty = width - filled;
|
|
60
|
-
return `[${"=".repeat(filled)}${filled > 0 && empty > 0 ? ">" : ""}${" ".repeat(Math.max(0, empty - (filled > 0 ? 1 : 0)))}]`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function createProgressReporter(total) {
|
|
64
|
-
const isTTY = Boolean(process.stdout.isTTY);
|
|
65
|
-
const hasTotal = Number.isFinite(total) && total > 0;
|
|
66
|
-
let received = 0;
|
|
67
|
-
let lastRenderAt = 0;
|
|
68
|
-
let lastMilestone = -1;
|
|
69
|
-
|
|
70
|
-
const update = (chunkSize) => {
|
|
71
|
-
received += chunkSize;
|
|
72
|
-
const now = Date.now();
|
|
73
|
-
|
|
74
|
-
if (isTTY) {
|
|
75
|
-
// Throttle TTY redraws to ~10 Hz
|
|
76
|
-
if (now - lastRenderAt < 100 && received !== total) return;
|
|
77
|
-
lastRenderAt = now;
|
|
78
|
-
if (hasTotal) {
|
|
79
|
-
const fraction = received / total;
|
|
80
|
-
const pct = Math.floor(fraction * 100);
|
|
81
|
-
const line = ` ${renderBar(fraction)} ${formatBytes(received)} / ${formatBytes(total)} (${pct}%)`;
|
|
82
|
-
process.stdout.write(`\r${line}`);
|
|
83
|
-
} else {
|
|
84
|
-
process.stdout.write(`\r Downloaded ${formatBytes(received)}...`);
|
|
85
|
-
}
|
|
86
|
-
} else if (hasTotal) {
|
|
87
|
-
// Non-TTY: emit milestone lines so CI logs stay readable
|
|
88
|
-
const pct = Math.floor((received / total) * 100);
|
|
89
|
-
const milestone = Math.floor(pct / 25) * 25;
|
|
90
|
-
if (milestone > lastMilestone && milestone <= 100) {
|
|
91
|
-
lastMilestone = milestone;
|
|
92
|
-
console.log(` ${milestone}% (${formatBytes(received)} / ${formatBytes(total)})`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const finish = () => {
|
|
98
|
-
if (isTTY) process.stdout.write("\n");
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return { update, finish };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function download(url) {
|
|
105
|
-
return new Promise((resolve, reject) => {
|
|
106
|
-
get(url, (res) => {
|
|
107
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
108
|
-
res.resume();
|
|
109
|
-
download(res.headers.location).then(resolve).catch(reject);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (res.statusCode !== 200) {
|
|
113
|
-
reject(new Error(`Download failed: HTTP ${res.statusCode}`));
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const total = parseInt(res.headers["content-length"] || "", 10);
|
|
118
|
-
const progress = createProgressReporter(total);
|
|
119
|
-
|
|
120
|
-
const file = createWriteStream(binPath);
|
|
121
|
-
res.on("data", (chunk) => progress.update(chunk.length));
|
|
122
|
-
res.pipe(file);
|
|
123
|
-
file.on("finish", () => {
|
|
124
|
-
file.close();
|
|
125
|
-
progress.finish();
|
|
126
|
-
if (!isWindows) {
|
|
127
|
-
chmodSync(binPath, 0o755);
|
|
128
|
-
}
|
|
129
|
-
resolve();
|
|
130
|
-
});
|
|
131
|
-
file.on("error", (err) => {
|
|
132
|
-
progress.finish();
|
|
133
|
-
reject(err);
|
|
134
|
-
});
|
|
135
|
-
res.on("error", (err) => {
|
|
136
|
-
progress.finish();
|
|
137
|
-
reject(err);
|
|
138
|
-
});
|
|
139
|
-
}).on("error", reject);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
download(url)
|
|
144
|
-
.then(() => console.log("flowleap installed successfully!"))
|
|
145
|
-
.catch((err) => {
|
|
146
|
-
console.error(`Failed to install flowleap: ${err.message}`);
|
|
147
|
-
process.exit(1);
|
|
148
|
-
});
|