+348
| #!/usr/bin/env node | ||
| "use strict"; | ||
| const crypto = require("crypto"); | ||
| const { spawnSync } = require("child_process"); | ||
| const fs = require("fs"); | ||
| const https = require("https"); | ||
| const os = require("os"); | ||
| const path = require("path"); | ||
| const zlib = require("zlib"); | ||
| function resolveBinaryFromPackageDir(pkgDir, ext) { | ||
| for (const name of [`knowns${ext}`, "knowns"]) { | ||
| const candidate = path.join(pkgDir, name); | ||
| if (fs.existsSync(candidate)) { | ||
| return candidate; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| function getPlatformPackage(platform = os.platform(), arch = os.arch()) { | ||
| const platformMap = { | ||
| darwin: "darwin", | ||
| linux: "linux", | ||
| win32: "win", | ||
| }; | ||
| const archMap = { | ||
| arm64: "arm64", | ||
| x64: "x64", | ||
| ia32: "x64", | ||
| }; | ||
| const p = platformMap[platform]; | ||
| const a = archMap[arch]; | ||
| if (!p || !a) { | ||
| return null; | ||
| } | ||
| return { | ||
| name: `@knowns/${p}-${a}`, | ||
| asset: `knowns-${p}-${a}`, | ||
| ext: platform === "win32" ? ".exe" : "", | ||
| packageOs: platform, | ||
| packageCpu: arch === "ia32" ? "x64" : arch, | ||
| }; | ||
| } | ||
| function getPackageRoot() { | ||
| return __dirname; | ||
| } | ||
| function getPackageJson(packageRoot) { | ||
| return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")); | ||
| } | ||
| function getRequestedVersion(packageRoot, pkgName) { | ||
| const pkg = getPackageJson(packageRoot); | ||
| return pkg.optionalDependencies && pkg.optionalDependencies[pkgName]; | ||
| } | ||
| function resolveInstalledPackageDir(packageRoot, pkgName, ext) { | ||
| const pkgParts = pkgName.split("/"); | ||
| const direct = path.join(packageRoot, "node_modules", ...pkgParts); | ||
| if (resolveBinaryFromPackageDir(direct, ext)) { | ||
| return direct; | ||
| } | ||
| try { | ||
| const pkgJson = require.resolve(`${pkgName}/package.json`, { paths: [packageRoot] }); | ||
| const pkgDir = path.dirname(pkgJson); | ||
| if (resolveBinaryFromPackageDir(pkgDir, ext)) { | ||
| return pkgDir; | ||
| } | ||
| } catch {} | ||
| return null; | ||
| } | ||
| function removeDir(dir) { | ||
| fs.rmSync(dir, { recursive: true, force: true }); | ||
| } | ||
| function moveOrCopyDir(src, dest) { | ||
| removeDir(dest); | ||
| fs.mkdirSync(path.dirname(dest), { recursive: true }); | ||
| try { | ||
| fs.renameSync(src, dest); | ||
| return; | ||
| } catch {} | ||
| fs.cpSync(src, dest, { recursive: true, force: true }); | ||
| } | ||
| function getNpmCommand() { | ||
| return process.platform === "win32" ? "npm.cmd" : "npm"; | ||
| } | ||
| function installPackageIntoPackageRoot(packageRoot, pkgName, version) { | ||
| const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "knowns-install-")); | ||
| const env = { ...process.env }; | ||
| delete env.npm_config_global; | ||
| delete env.npm_config_prefix; | ||
| try { | ||
| fs.writeFileSync(path.join(tmpRoot, "package.json"), "{}\n"); | ||
| const result = spawnSync( | ||
| getNpmCommand(), | ||
| ["install", "--loglevel=error", "--prefer-offline", "--no-audit", "--progress=false", "--no-save", `${pkgName}@${version}`], | ||
| { | ||
| cwd: tmpRoot, | ||
| stdio: "pipe", | ||
| encoding: "utf8", | ||
| env, | ||
| } | ||
| ); | ||
| if ((result.status ?? 1) !== 0) { | ||
| throw new Error((result.stderr || result.stdout || `Failed to install ${pkgName}`).trim()); | ||
| } | ||
| const src = path.join(tmpRoot, "node_modules", ...pkgName.split("/")); | ||
| if (!fs.existsSync(src)) { | ||
| throw new Error(`Installed package is missing: ${pkgName}`); | ||
| } | ||
| const dest = path.join(packageRoot, "node_modules", ...pkgName.split("/")); | ||
| moveOrCopyDir(src, dest); | ||
| } finally { | ||
| removeDir(tmpRoot); | ||
| } | ||
| } | ||
| function fetchBuffer(url, redirects = 0) { | ||
| return new Promise((resolve, reject) => { | ||
| https | ||
| .get(url, (response) => { | ||
| const status = response.statusCode || 0; | ||
| if (status >= 300 && status < 400 && response.headers.location) { | ||
| if (redirects >= 5) { | ||
| response.resume(); | ||
| reject(new Error(`Too many redirects while downloading ${url}`)); | ||
| return; | ||
| } | ||
| const nextUrl = new URL(response.headers.location, url).toString(); | ||
| response.resume(); | ||
| fetchBuffer(nextUrl, redirects + 1).then(resolve, reject); | ||
| return; | ||
| } | ||
| if (status < 200 || status >= 300) { | ||
| const chunks = []; | ||
| response.on("data", (chunk) => chunks.push(chunk)); | ||
| response.on("end", () => { | ||
| const body = Buffer.concat(chunks).toString("utf8").trim(); | ||
| reject(new Error(`Download failed (${status}) for ${url}${body ? `: ${body}` : ""}`)); | ||
| }); | ||
| return; | ||
| } | ||
| const chunks = []; | ||
| response.on("data", (chunk) => chunks.push(chunk)); | ||
| response.on("end", () => resolve(Buffer.concat(chunks))); | ||
| }) | ||
| .on("error", reject); | ||
| }); | ||
| } | ||
| function parseSha256(text) { | ||
| const match = text.trim().match(/^([a-f0-9]{64})\b/i); | ||
| if (!match) { | ||
| throw new Error("Invalid sha256 file contents"); | ||
| } | ||
| return match[1].toLowerCase(); | ||
| } | ||
| function verifySha256(buffer, expected) { | ||
| const actual = crypto.createHash("sha256").update(buffer).digest("hex"); | ||
| if (actual !== expected) { | ||
| throw new Error(`Checksum mismatch: expected ${expected}, got ${actual}`); | ||
| } | ||
| } | ||
| function extractTarToDir(buffer, dest) { | ||
| let offset = 0; | ||
| while (offset + 512 <= buffer.length) { | ||
| const header = buffer.subarray(offset, offset + 512); | ||
| offset += 512; | ||
| const rawName = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, ""); | ||
| const sizeText = header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim(); | ||
| const type = header.subarray(156, 157).toString("utf8") || "0"; | ||
| if (!rawName && !sizeText) { | ||
| break; | ||
| } | ||
| const size = sizeText ? parseInt(sizeText, 8) : 0; | ||
| if (!Number.isFinite(size)) { | ||
| throw new Error(`Invalid tar entry size for ${rawName || "<unknown>"}`); | ||
| } | ||
| const normalizedName = rawName.replace(/^package\//, "").replace(/\/$/, ""); | ||
| const filePath = normalizedName ? path.join(dest, normalizedName) : dest; | ||
| if (type === "5") { | ||
| fs.mkdirSync(filePath, { recursive: true }); | ||
| } else if (type === "0" || type === "\0") { | ||
| fs.mkdirSync(path.dirname(filePath), { recursive: true }); | ||
| fs.writeFileSync(filePath, buffer.subarray(offset, offset + size)); | ||
| } | ||
| offset += Math.ceil(size / 512) * 512; | ||
| } | ||
| } | ||
| function writeDownloadedPackageMetadata(dest, platformPackage, version) { | ||
| fs.writeFileSync( | ||
| path.join(dest, "package.json"), | ||
| JSON.stringify( | ||
| { | ||
| name: platformPackage.name, | ||
| version, | ||
| description: `Knowns binary for ${platformPackage.asset.replace(/^knowns-/, "").replace(/-/g, " ")}`, | ||
| os: [platformPackage.packageOs], | ||
| cpu: [platformPackage.packageCpu], | ||
| main: `knowns${platformPackage.ext}`, | ||
| license: "MIT", | ||
| homepage: "https://knowns.sh", | ||
| repository: { | ||
| type: "git", | ||
| url: "git+https://github.com/knowns-dev/knowns.git", | ||
| }, | ||
| }, | ||
| null, | ||
| 2 | ||
| ) + "\n" | ||
| ); | ||
| } | ||
| async function downloadPackageFromGitHubRelease(packageRoot, platformPackage, version) { | ||
| const baseUrl = `https://github.com/knowns-dev/knowns/releases/download/v${version}/${platformPackage.asset}.tar.gz`; | ||
| const checksumUrl = `${baseUrl}.sha256`; | ||
| const [archive, checksumFile] = await Promise.all([fetchBuffer(baseUrl), fetchBuffer(checksumUrl)]); | ||
| const expectedSha = parseSha256(checksumFile.toString("utf8")); | ||
| verifySha256(archive, expectedSha); | ||
| const dest = path.join(packageRoot, "node_modules", ...platformPackage.name.split("/")); | ||
| removeDir(dest); | ||
| fs.mkdirSync(dest, { recursive: true }); | ||
| const extracted = zlib.gunzipSync(archive); | ||
| extractTarToDir(extracted, dest); | ||
| writeDownloadedPackageMetadata(dest, platformPackage, version); | ||
| } | ||
| function getInstallHint(pkgName) { | ||
| const base = process.env.npm_config_global === "true" ? "npm install -g" : "npm install"; | ||
| return `${base} knowns ${pkgName}`; | ||
| } | ||
| async function ensurePlatformBinary() { | ||
| const platformPackage = getPlatformPackage(); | ||
| if (!platformPackage) { | ||
| return; | ||
| } | ||
| const packageRoot = getPackageRoot(); | ||
| const version = getRequestedVersion(packageRoot, platformPackage.name); | ||
| if (!version || version === "0.0.0") { | ||
| return; | ||
| } | ||
| if (resolveInstalledPackageDir(packageRoot, platformPackage.name, platformPackage.ext)) { | ||
| return; | ||
| } | ||
| let npmError = null; | ||
| try { | ||
| installPackageIntoPackageRoot(packageRoot, platformPackage.name, version); | ||
| } catch (error) { | ||
| npmError = error; | ||
| } | ||
| if (!resolveInstalledPackageDir(packageRoot, platformPackage.name, platformPackage.ext)) { | ||
| let githubError = null; | ||
| try { | ||
| await downloadPackageFromGitHubRelease(packageRoot, platformPackage, version); | ||
| } catch (error) { | ||
| githubError = error; | ||
| } | ||
| if (!resolveInstalledPackageDir(packageRoot, platformPackage.name, platformPackage.ext)) { | ||
| const messages = []; | ||
| if (npmError) { | ||
| messages.push(`npm install fallback failed: ${npmError.message}`); | ||
| } | ||
| if (githubError) { | ||
| messages.push(`GitHub release fallback failed: ${githubError.message}`); | ||
| } | ||
| throw new Error(messages.join("\n") || `Could not install ${platformPackage.name}`); | ||
| } | ||
| } | ||
| } | ||
| async function main() { | ||
| try { | ||
| await ensurePlatformBinary(); | ||
| } catch (error) { | ||
| const platformPackage = getPlatformPackage(); | ||
| const details = error && error.message ? error.message : String(error); | ||
| console.error(`Failed to install Knowns platform binary.${platformPackage ? ` Expected package: ${platformPackage.name}.` : ""}`); | ||
| console.error(details); | ||
| if (platformPackage) { | ||
| console.error(`Try: ${getInstallHint(platformPackage.name)}`); | ||
| } | ||
| process.exit(1); | ||
| } | ||
| } | ||
| if (require.main === module) { | ||
| main(); | ||
| } | ||
| module.exports = { | ||
| downloadPackageFromGitHubRelease, | ||
| ensurePlatformBinary, | ||
| extractTarToDir, | ||
| fetchBuffer, | ||
| getInstallHint, | ||
| getPlatformPackage, | ||
| getRequestedVersion, | ||
| installPackageIntoPackageRoot, | ||
| parseSha256, | ||
| resolveBinaryFromPackageDir, | ||
| resolveInstalledPackageDir, | ||
| verifySha256, | ||
| writeDownloadedPackageMetadata, | ||
| }; |
+8
-1
@@ -36,2 +36,9 @@ #!/usr/bin/env node | ||
| function getInstallHint(pkgName) { | ||
| return ( | ||
| `npm install knowns ${pkgName}\n` + | ||
| `Or for global installs: npm install -g knowns ${pkgName}` | ||
| ); | ||
| } | ||
| function getBinaryPath() { | ||
@@ -102,3 +109,3 @@ const platform = os.platform(); | ||
| `Expected package: ${pkgName}\n` + | ||
| `Try reinstalling: npm install knowns` | ||
| `Try reinstalling:\n${getInstallHint(pkgName)}` | ||
| ); | ||
@@ -105,0 +112,0 @@ process.exit(1); |
+11
-8
| { | ||
| "name": "knowns", | ||
| "version": "0.15.2", | ||
| "version": "0.15.3", | ||
| "description": "AI-native project management CLI", | ||
@@ -9,12 +9,15 @@ "bin": { | ||
| }, | ||
| "scripts": {}, | ||
| "scripts": { | ||
| "postinstall": "node install.js" | ||
| }, | ||
| "optionalDependencies": { | ||
| "@knowns/darwin-arm64": "0.15.2", | ||
| "@knowns/darwin-x64": "0.15.2", | ||
| "@knowns/linux-arm64": "0.15.2", | ||
| "@knowns/linux-x64": "0.15.2", | ||
| "@knowns/win-arm64": "0.15.2", | ||
| "@knowns/win-x64": "0.15.2" | ||
| "@knowns/darwin-arm64": "0.15.3", | ||
| "@knowns/darwin-x64": "0.15.3", | ||
| "@knowns/linux-arm64": "0.15.3", | ||
| "@knowns/linux-x64": "0.15.3", | ||
| "@knowns/win-arm64": "0.15.3", | ||
| "@knowns/win-x64": "0.15.3" | ||
| }, | ||
| "license": "MIT", | ||
| "homepage": "https://knowns.sh", | ||
| "repository": { | ||
@@ -21,0 +24,0 @@ "type": "git", |
+17
-3
@@ -146,6 +146,6 @@ <p align="center"> | ||
| # Shell installer (macOS/Linux) | ||
| curl -fsSL https://raw.githubusercontent.com/knowns-dev/knowns/main/install/install.sh | sh | ||
| curl -fsSL https://knowns.sh/script/install | sh | ||
| # Or with wget | ||
| wget -qO- https://raw.githubusercontent.com/knowns-dev/knowns/main/install/install.sh | sh | ||
| wget -qO- https://knowns.sh/script/install | sh | ||
| ``` | ||
@@ -155,6 +155,20 @@ | ||
| # PowerShell installer (Windows) | ||
| irm https://raw.githubusercontent.com/knowns-dev/knowns/main/install/install.ps1 | iex | ||
| irm https://knowns.sh/script/install.ps1 | iex | ||
| ``` | ||
| ### Uninstall | ||
| ```bash | ||
| # Shell uninstaller (macOS/Linux) | ||
| curl -fsSL https://knowns.sh/script/uninstall | sh | ||
| ``` | ||
| ```powershell | ||
| # PowerShell uninstaller (Windows) | ||
| irm https://knowns.sh/script/uninstall.ps1 | iex | ||
| ``` | ||
| The uninstall scripts only remove installed CLI binaries and PATH entries added by the installer. They leave project `.knowns/` folders untouched. | ||
| ```bash | ||
| # npm — installs platform-specific binary automatically | ||
@@ -161,0 +175,0 @@ npm install -g knowns |
Install scripts
Supply chain riskInstall scripts are run when the package is installed or built. Malicious packages often use scripts that run automatically to execute payloads or fetch additional code.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
26997
63.08%4
33.33%400
296.04%1
-50%406
3.57%1
Infinity%8
166.67%4
300%