pluidr 0.6.1 → 0.7.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/LICENSE +20 -20
- package/README.md +365 -357
- package/bin/pluidr.js +3 -3
- package/package.json +45 -45
- package/src/cli/commands/doctor.js +101 -99
- package/src/cli/commands/init.js +43 -43
- package/src/cli/commands/launch.js +46 -0
- package/src/cli/commands/uninstall.js +63 -67
- package/src/cli/commands/update.js +6 -22
- package/src/cli/index.js +57 -53
- package/src/cli/wizard/selectModelTier.js +40 -39
- package/src/core/agentPromptWriter.js +32 -32
- package/src/core/agentPromptWriter.test.js +56 -56
- package/src/core/backup.js +40 -38
- package/src/core/configBuilder.js +32 -32
- package/src/core/configBuilder.test.js +93 -47
- package/src/core/configWriter.js +10 -10
- package/src/core/identityHeader.js +8 -8
- package/src/core/paths.js +9 -9
- package/src/core/paths.test.js +21 -21
- package/src/core/pluginWriter.js +29 -29
- package/src/core/squeezeInstaller.js +161 -161
- package/src/core/squeezeInstaller.test.js +75 -75
- package/src/core/version.js +8 -0
- package/src/core/versionCheck.js +44 -0
- package/src/core/versionCheck.test.js +34 -0
- package/src/plugins/README.md +68 -68
- package/src/plugins/pluidr-squeeze.js +77 -77
- package/src/templates/agent-prompts/auditor.txt +20 -20
- package/src/templates/agent-prompts/coder.txt +87 -87
- package/src/templates/agent-prompts/compose-reporter.txt +55 -55
- package/src/templates/agent-prompts/composer.txt +430 -430
- package/src/templates/agent-prompts/debug-reporter.txt +65 -65
- package/src/templates/agent-prompts/debugger.txt +151 -151
- package/src/templates/agent-prompts/fixer.txt +66 -66
- package/src/templates/agent-prompts/hierarchy.txt +96 -96
- package/src/templates/agent-prompts/inspector.txt +79 -79
- package/src/templates/agent-prompts/patcher.txt +20 -20
- package/src/templates/agent-prompts/plan-checker.txt +45 -45
- package/src/templates/agent-prompts/plan-writer.txt +57 -57
- package/src/templates/agent-prompts/probe-reporter.txt +62 -62
- package/src/templates/agent-prompts/prober.txt +90 -90
- package/src/templates/agent-prompts/researcher.txt +48 -48
- package/src/templates/agent-prompts/reviewer.txt +57 -57
- package/src/templates/agent-prompts/tester.txt +66 -66
- package/src/templates/agent-prompts/tracer.txt +33 -33
- package/src/templates/model-defaults.json +73 -55
- package/src/templates/opencode.config.json +481 -481
package/src/core/configWriter.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
import { getConfigDir } from "./paths.js"
|
|
4
|
-
|
|
5
|
-
export function writeConfig(configObject, targetDir) {
|
|
6
|
-
const baseDir = targetDir || getConfigDir()
|
|
7
|
-
const configPath = join(baseDir, "opencode.jsonc")
|
|
8
|
-
mkdirSync(baseDir, { recursive: true })
|
|
9
|
-
writeFileSync(configPath, JSON.stringify(configObject, null, 2), "utf-8")
|
|
10
|
-
}
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { getConfigDir } from "./paths.js"
|
|
4
|
+
|
|
5
|
+
export function writeConfig(configObject, targetDir) {
|
|
6
|
+
const baseDir = targetDir || getConfigDir()
|
|
7
|
+
const configPath = join(baseDir, "opencode.jsonc")
|
|
8
|
+
mkdirSync(baseDir, { recursive: true })
|
|
9
|
+
writeFileSync(configPath, JSON.stringify(configObject, null, 2), "utf-8")
|
|
10
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Single source of truth for the agent identity prefix prepended to every
|
|
2
|
-
// generated prompt file. Mirrors the system identity concept from the
|
|
3
|
-
// dropped hook-based AgentSelfIdentityPlugin, but as a static prepend
|
|
4
|
-
// (KISS: agent names are known at install time).
|
|
5
|
-
|
|
6
|
-
export default function IDENTITY_HEADER(agentName) {
|
|
7
|
-
return `You are the "${agentName}" agent.\n\n`
|
|
8
|
-
}
|
|
1
|
+
// Single source of truth for the agent identity prefix prepended to every
|
|
2
|
+
// generated prompt file. Mirrors the system identity concept from the
|
|
3
|
+
// dropped hook-based AgentSelfIdentityPlugin, but as a static prepend
|
|
4
|
+
// (KISS: agent names are known at install time).
|
|
5
|
+
|
|
6
|
+
export default function IDENTITY_HEADER(agentName) {
|
|
7
|
+
return `You are the "${agentName}" agent.\n\n`
|
|
8
|
+
}
|
package/src/core/paths.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { homedir } from "node:os"
|
|
2
|
-
import { join } from "node:path"
|
|
3
|
-
|
|
4
|
-
const BASE = join(homedir(), ".config", "opencode")
|
|
5
|
-
|
|
6
|
-
export const getConfigDir = () => BASE
|
|
7
|
-
export const getConfigPath = () => join(BASE, "opencode.jsonc")
|
|
8
|
-
export const getPromptsDir = () => join(BASE, "prompts")
|
|
9
|
-
// ponytail: getBackupPath removed (YAGNI)
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
const BASE = join(homedir(), ".config", "opencode")
|
|
5
|
+
|
|
6
|
+
export const getConfigDir = () => BASE
|
|
7
|
+
export const getConfigPath = () => join(BASE, "opencode.jsonc")
|
|
8
|
+
export const getPromptsDir = () => join(BASE, "prompts")
|
|
9
|
+
// ponytail: getBackupPath removed (YAGNI)
|
package/src/core/paths.test.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { describe, it } from "node:test"
|
|
2
|
-
import assert from "node:assert"
|
|
3
|
-
import { homedir } from "node:os"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { getConfigDir, getConfigPath, getPromptsDir } from "./paths.js"
|
|
6
|
-
|
|
7
|
-
const BASE = join(homedir(), ".config", "opencode")
|
|
8
|
-
|
|
9
|
-
describe("paths", () => {
|
|
10
|
-
it("getConfigDir returns ~/.config/opencode", () => {
|
|
11
|
-
assert.strictEqual(getConfigDir(), BASE)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it("getConfigPath returns opencode.jsonc path", () => {
|
|
15
|
-
assert.strictEqual(getConfigPath(), join(BASE, "opencode.jsonc"))
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it("getPromptsDir returns prompts path", () => {
|
|
19
|
-
assert.strictEqual(getPromptsDir(), join(BASE, "prompts"))
|
|
20
|
-
})
|
|
21
|
-
})
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { getConfigDir, getConfigPath, getPromptsDir } from "./paths.js"
|
|
6
|
+
|
|
7
|
+
const BASE = join(homedir(), ".config", "opencode")
|
|
8
|
+
|
|
9
|
+
describe("paths", () => {
|
|
10
|
+
it("getConfigDir returns ~/.config/opencode", () => {
|
|
11
|
+
assert.strictEqual(getConfigDir(), BASE)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("getConfigPath returns opencode.jsonc path", () => {
|
|
15
|
+
assert.strictEqual(getConfigPath(), join(BASE, "opencode.jsonc"))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("getPromptsDir returns prompts path", () => {
|
|
19
|
+
assert.strictEqual(getPromptsDir(), join(BASE, "prompts"))
|
|
20
|
+
})
|
|
21
|
+
})
|
package/src/core/pluginWriter.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { dirname, join, resolve } from "node:path"
|
|
3
|
-
import { fileURLToPath } from "node:url"
|
|
4
|
-
import { getConfigDir } from "./paths.js"
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const PLUGINS = ["pluidr-flow.js", "pluidr-squeeze.js"]
|
|
8
|
-
const PACKAGE_JSON = {
|
|
9
|
-
type: "module",
|
|
10
|
-
dependencies: { "@opencode-ai/plugin": "^1.17.9" },
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function writePluginBundle(targetDir) {
|
|
14
|
-
const baseDir = targetDir || getConfigDir()
|
|
15
|
-
const pluginsDir = join(baseDir, "plugins")
|
|
16
|
-
mkdirSync(pluginsDir, { recursive: true })
|
|
17
|
-
|
|
18
|
-
for (const name of PLUGINS) {
|
|
19
|
-
const sourcePath = resolve(__dirname, "../plugins", name)
|
|
20
|
-
copyFileSync(sourcePath, join(pluginsDir, name))
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function writePluginPackageJson(targetDir) {
|
|
25
|
-
const baseDir = targetDir || getConfigDir()
|
|
26
|
-
const packageJsonPath = join(baseDir, "package.json")
|
|
27
|
-
|
|
28
|
-
writeFileSync(packageJsonPath, JSON.stringify(PACKAGE_JSON, null, 2), "utf-8")
|
|
29
|
-
}
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join, resolve } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { getConfigDir } from "./paths.js"
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const PLUGINS = ["pluidr-flow.js", "pluidr-squeeze.js"]
|
|
8
|
+
const PACKAGE_JSON = {
|
|
9
|
+
type: "module",
|
|
10
|
+
dependencies: { "@opencode-ai/plugin": "^1.17.9" },
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function writePluginBundle(targetDir) {
|
|
14
|
+
const baseDir = targetDir || getConfigDir()
|
|
15
|
+
const pluginsDir = join(baseDir, "plugins")
|
|
16
|
+
mkdirSync(pluginsDir, { recursive: true })
|
|
17
|
+
|
|
18
|
+
for (const name of PLUGINS) {
|
|
19
|
+
const sourcePath = resolve(__dirname, "../plugins", name)
|
|
20
|
+
copyFileSync(sourcePath, join(pluginsDir, name))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writePluginPackageJson(targetDir) {
|
|
25
|
+
const baseDir = targetDir || getConfigDir()
|
|
26
|
+
const packageJsonPath = join(baseDir, "package.json")
|
|
27
|
+
|
|
28
|
+
writeFileSync(packageJsonPath, JSON.stringify(PACKAGE_JSON, null, 2), "utf-8")
|
|
29
|
+
}
|
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process"
|
|
2
|
-
import { chmodSync, mkdirSync, existsSync, writeFileSync, unlinkSync, readFileSync, renameSync } from "node:fs"
|
|
3
|
-
import { extname, join, resolve, dirname } from "node:path"
|
|
4
|
-
import { fileURLToPath } from "node:url"
|
|
5
|
-
import { createHash } from "node:crypto"
|
|
6
|
-
import { getConfigDir } from "./paths.js"
|
|
7
|
-
|
|
8
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
|
|
10
|
-
// Local binary name — friendlier than the upstream "rtk" name
|
|
11
|
-
const BIN_NAME = process.platform === "win32" ? "squeeze.exe" : "squeeze"
|
|
12
|
-
// Name of the binary inside the downloaded archive (upstream uses "rtk")
|
|
13
|
-
const RTK_BIN_NAME = process.platform === "win32" ? "rtk.exe" : "rtk"
|
|
14
|
-
export const ASSETS = {
|
|
15
|
-
"darwin-arm64": "rtk-aarch64-apple-darwin.tar.gz",
|
|
16
|
-
"darwin-x64": "rtk-x86_64-apple-darwin.tar.gz",
|
|
17
|
-
"linux-x64": "rtk-x86_64-unknown-linux-musl.tar.gz",
|
|
18
|
-
"linux-arm64": "rtk-aarch64-unknown-linux-gnu.tar.gz",
|
|
19
|
-
"win32-x64": "rtk-x86_64-pc-windows-msvc.zip",
|
|
20
|
-
}
|
|
21
|
-
const RTK_RELEASE_BASE = "https://github.com/rtk-ai/rtk/releases/latest/download"
|
|
22
|
-
|
|
23
|
-
export function platformAsset() {
|
|
24
|
-
const key = `${process.platform}-${process.arch}`
|
|
25
|
-
return ASSETS[key] ?? null
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function validatePath(path) {
|
|
29
|
-
if (!/^[a-zA-Z0-9_\-\.\:\/\\]+$/.test(path)) {
|
|
30
|
-
throw new Error(`Invalid path: "${path}" contains unsafe characters`)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function findSqueezePath() {
|
|
35
|
-
try {
|
|
36
|
-
const [cmd, ...args] =
|
|
37
|
-
process.platform === "win32" ? ["where", "squeeze"] : ["which", "squeeze"]
|
|
38
|
-
return execFileSync(cmd, args, { stdio: "pipe" })
|
|
39
|
-
.toString()
|
|
40
|
-
.trim()
|
|
41
|
-
.split(/\r?\n/)[0]
|
|
42
|
-
} catch {
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function extract(archivePath, destDir) {
|
|
48
|
-
validatePath(archivePath)
|
|
49
|
-
validatePath(destDir)
|
|
50
|
-
const isZip = extname(archivePath) === ".zip"
|
|
51
|
-
|
|
52
|
-
if (isZip && process.platform === "win32") {
|
|
53
|
-
execFileSync("powershell", [
|
|
54
|
-
"-NoProfile",
|
|
55
|
-
"-Command",
|
|
56
|
-
"Expand-Archive",
|
|
57
|
-
"-LiteralPath", archivePath,
|
|
58
|
-
"-DestinationPath", destDir,
|
|
59
|
-
"-Force"
|
|
60
|
-
], { stdio: "ignore" })
|
|
61
|
-
} else {
|
|
62
|
-
execFileSync("tar", ["-xzf", archivePath, "-C", destDir], { stdio: "ignore" })
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function verifyChecksum(buffer, assetName) {
|
|
67
|
-
const checksumsPath = resolve(__dirname, "../templates/rtk-checksums.json")
|
|
68
|
-
let checksums
|
|
69
|
-
try {
|
|
70
|
-
checksums = JSON.parse(readFileSync(checksumsPath, "utf-8"))
|
|
71
|
-
} catch {
|
|
72
|
-
// No checksums file — skip verification
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const expectedHash = checksums[assetName]
|
|
77
|
-
if (expectedHash === null || expectedHash === undefined) {
|
|
78
|
-
// Hash not yet populated for this platform — silently skip
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const computed = createHash("sha256").update(buffer).digest("hex")
|
|
83
|
-
if (computed !== expectedHash) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`SHA256 mismatch for ${assetName}\n expected: ${expectedHash}\n computed: ${computed}`
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function installSqueeze() {
|
|
91
|
-
let rtkPath = findSqueezePath()
|
|
92
|
-
|
|
93
|
-
if (!rtkPath) {
|
|
94
|
-
// Not on PATH — download it to the managed location
|
|
95
|
-
const asset = platformAsset()
|
|
96
|
-
if (!asset) {
|
|
97
|
-
console.warn("⚠ Squeeze: unsupported platform — squeeze binary unavailable")
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const binDir = join(getConfigDir(), "bin")
|
|
102
|
-
mkdirSync(binDir, { recursive: true })
|
|
103
|
-
|
|
104
|
-
const url = `${RTK_RELEASE_BASE}/${asset}`
|
|
105
|
-
const archivePath = join(binDir, asset)
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
let response
|
|
109
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
110
|
-
try {
|
|
111
|
-
response = await fetch(url)
|
|
112
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
113
|
-
break
|
|
114
|
-
} catch (err) {
|
|
115
|
-
if (attempt < 3) {
|
|
116
|
-
// Retry on any error (network, HTTP, etc.) with exponential backoff
|
|
117
|
-
const delay = Math.pow(2, attempt - 1) * 1000
|
|
118
|
-
await new Promise(r => setTimeout(r, delay))
|
|
119
|
-
continue
|
|
120
|
-
}
|
|
121
|
-
throw err
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const buffer = Buffer.from(await response.arrayBuffer())
|
|
126
|
-
verifyChecksum(buffer, asset)
|
|
127
|
-
writeFileSync(archivePath, buffer)
|
|
128
|
-
|
|
129
|
-
extract(archivePath, binDir)
|
|
130
|
-
|
|
131
|
-
try { unlinkSync(archivePath) } catch {}
|
|
132
|
-
|
|
133
|
-
// Archive contains "rtk"/"rtk.exe" — rename to local BIN_NAME ("squeeze")
|
|
134
|
-
const extracted = join(binDir, RTK_BIN_NAME)
|
|
135
|
-
rtkPath = join(binDir, BIN_NAME)
|
|
136
|
-
if (existsSync(extracted) && extracted !== rtkPath) {
|
|
137
|
-
renameSync(extracted, rtkPath)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (process.platform !== "win32" && existsSync(rtkPath)) {
|
|
141
|
-
chmodSync(rtkPath, 0o755)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Verify that the downloaded binary is executable and runs on this platform
|
|
145
|
-
try {
|
|
146
|
-
execFileSync(rtkPath, ["--help"], { stdio: "ignore" })
|
|
147
|
-
} catch (err) {
|
|
148
|
-
// If it throws because of execution format or permissions (e.g. ENOEXEC, EACCES, ENOENT)
|
|
149
|
-
if (err.code === "ENOEXEC" || err.code === "EACCES" || err.code === "ENOENT") {
|
|
150
|
-
try { unlinkSync(rtkPath) } catch {}
|
|
151
|
-
throw new Error(`Downloaded binary is not executable on this platform: ${err.message}`)
|
|
152
|
-
}
|
|
153
|
-
// If it ran but exited with non-zero (like returning exit code 1 for --help), that's fine, it means it is executable!
|
|
154
|
-
}
|
|
155
|
-
} catch (err) {
|
|
156
|
-
try { unlinkSync(archivePath) } catch {}
|
|
157
|
-
console.warn(`⚠ Squeeze: installation failed (${err.message}) — squeeze plugin disabled`)
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
1
|
+
import { execFileSync } from "node:child_process"
|
|
2
|
+
import { chmodSync, mkdirSync, existsSync, writeFileSync, unlinkSync, readFileSync, renameSync } from "node:fs"
|
|
3
|
+
import { extname, join, resolve, dirname } from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { createHash } from "node:crypto"
|
|
6
|
+
import { getConfigDir } from "./paths.js"
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
// Local binary name — friendlier than the upstream "rtk" name
|
|
11
|
+
const BIN_NAME = process.platform === "win32" ? "squeeze.exe" : "squeeze"
|
|
12
|
+
// Name of the binary inside the downloaded archive (upstream uses "rtk")
|
|
13
|
+
const RTK_BIN_NAME = process.platform === "win32" ? "rtk.exe" : "rtk"
|
|
14
|
+
export const ASSETS = {
|
|
15
|
+
"darwin-arm64": "rtk-aarch64-apple-darwin.tar.gz",
|
|
16
|
+
"darwin-x64": "rtk-x86_64-apple-darwin.tar.gz",
|
|
17
|
+
"linux-x64": "rtk-x86_64-unknown-linux-musl.tar.gz",
|
|
18
|
+
"linux-arm64": "rtk-aarch64-unknown-linux-gnu.tar.gz",
|
|
19
|
+
"win32-x64": "rtk-x86_64-pc-windows-msvc.zip",
|
|
20
|
+
}
|
|
21
|
+
const RTK_RELEASE_BASE = "https://github.com/rtk-ai/rtk/releases/latest/download"
|
|
22
|
+
|
|
23
|
+
export function platformAsset() {
|
|
24
|
+
const key = `${process.platform}-${process.arch}`
|
|
25
|
+
return ASSETS[key] ?? null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validatePath(path) {
|
|
29
|
+
if (!/^[a-zA-Z0-9_\-\.\:\/\\]+$/.test(path)) {
|
|
30
|
+
throw new Error(`Invalid path: "${path}" contains unsafe characters`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findSqueezePath() {
|
|
35
|
+
try {
|
|
36
|
+
const [cmd, ...args] =
|
|
37
|
+
process.platform === "win32" ? ["where", "squeeze"] : ["which", "squeeze"]
|
|
38
|
+
return execFileSync(cmd, args, { stdio: "pipe" })
|
|
39
|
+
.toString()
|
|
40
|
+
.trim()
|
|
41
|
+
.split(/\r?\n/)[0]
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extract(archivePath, destDir) {
|
|
48
|
+
validatePath(archivePath)
|
|
49
|
+
validatePath(destDir)
|
|
50
|
+
const isZip = extname(archivePath) === ".zip"
|
|
51
|
+
|
|
52
|
+
if (isZip && process.platform === "win32") {
|
|
53
|
+
execFileSync("powershell", [
|
|
54
|
+
"-NoProfile",
|
|
55
|
+
"-Command",
|
|
56
|
+
"Expand-Archive",
|
|
57
|
+
"-LiteralPath", archivePath,
|
|
58
|
+
"-DestinationPath", destDir,
|
|
59
|
+
"-Force"
|
|
60
|
+
], { stdio: "ignore" })
|
|
61
|
+
} else {
|
|
62
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", destDir], { stdio: "ignore" })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function verifyChecksum(buffer, assetName) {
|
|
67
|
+
const checksumsPath = resolve(__dirname, "../templates/rtk-checksums.json")
|
|
68
|
+
let checksums
|
|
69
|
+
try {
|
|
70
|
+
checksums = JSON.parse(readFileSync(checksumsPath, "utf-8"))
|
|
71
|
+
} catch {
|
|
72
|
+
// No checksums file — skip verification
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const expectedHash = checksums[assetName]
|
|
77
|
+
if (expectedHash === null || expectedHash === undefined) {
|
|
78
|
+
// Hash not yet populated for this platform — silently skip
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const computed = createHash("sha256").update(buffer).digest("hex")
|
|
83
|
+
if (computed !== expectedHash) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`SHA256 mismatch for ${assetName}\n expected: ${expectedHash}\n computed: ${computed}`
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function installSqueeze() {
|
|
91
|
+
let rtkPath = findSqueezePath()
|
|
92
|
+
|
|
93
|
+
if (!rtkPath) {
|
|
94
|
+
// Not on PATH — download it to the managed location
|
|
95
|
+
const asset = platformAsset()
|
|
96
|
+
if (!asset) {
|
|
97
|
+
console.warn("⚠ Squeeze: unsupported platform — squeeze binary unavailable")
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const binDir = join(getConfigDir(), "bin")
|
|
102
|
+
mkdirSync(binDir, { recursive: true })
|
|
103
|
+
|
|
104
|
+
const url = `${RTK_RELEASE_BASE}/${asset}`
|
|
105
|
+
const archivePath = join(binDir, asset)
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
let response
|
|
109
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
110
|
+
try {
|
|
111
|
+
response = await fetch(url)
|
|
112
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
113
|
+
break
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (attempt < 3) {
|
|
116
|
+
// Retry on any error (network, HTTP, etc.) with exponential backoff
|
|
117
|
+
const delay = Math.pow(2, attempt - 1) * 1000
|
|
118
|
+
await new Promise(r => setTimeout(r, delay))
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
throw err
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
126
|
+
verifyChecksum(buffer, asset)
|
|
127
|
+
writeFileSync(archivePath, buffer)
|
|
128
|
+
|
|
129
|
+
extract(archivePath, binDir)
|
|
130
|
+
|
|
131
|
+
try { unlinkSync(archivePath) } catch {}
|
|
132
|
+
|
|
133
|
+
// Archive contains "rtk"/"rtk.exe" — rename to local BIN_NAME ("squeeze")
|
|
134
|
+
const extracted = join(binDir, RTK_BIN_NAME)
|
|
135
|
+
rtkPath = join(binDir, BIN_NAME)
|
|
136
|
+
if (existsSync(extracted) && extracted !== rtkPath) {
|
|
137
|
+
renameSync(extracted, rtkPath)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (process.platform !== "win32" && existsSync(rtkPath)) {
|
|
141
|
+
chmodSync(rtkPath, 0o755)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Verify that the downloaded binary is executable and runs on this platform
|
|
145
|
+
try {
|
|
146
|
+
execFileSync(rtkPath, ["--help"], { stdio: "ignore" })
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// If it throws because of execution format or permissions (e.g. ENOEXEC, EACCES, ENOENT)
|
|
149
|
+
if (err.code === "ENOEXEC" || err.code === "EACCES" || err.code === "ENOENT") {
|
|
150
|
+
try { unlinkSync(rtkPath) } catch {}
|
|
151
|
+
throw new Error(`Downloaded binary is not executable on this platform: ${err.message}`)
|
|
152
|
+
}
|
|
153
|
+
// If it ran but exited with non-zero (like returning exit code 1 for --help), that's fine, it means it is executable!
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
try { unlinkSync(archivePath) } catch {}
|
|
157
|
+
console.warn(`⚠ Squeeze: installation failed (${err.message}) — squeeze plugin disabled`)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import { describe, it } from "node:test"
|
|
2
|
-
import assert from "node:assert"
|
|
3
|
-
import { platformAsset, validatePath, findSqueezePath, ASSETS } from "./squeezeInstaller.js"
|
|
4
|
-
|
|
5
|
-
describe("platformAsset", () => {
|
|
6
|
-
it("returns a string or null for current platform", () => {
|
|
7
|
-
const result = platformAsset()
|
|
8
|
-
// On a supported platform: string; on unsupported: null
|
|
9
|
-
assert.ok(result === null || typeof result === "string")
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it("returns known asset names for mapped platforms", () => {
|
|
13
|
-
// Verify the ASSETS map has entries for all known platform combos
|
|
14
|
-
const knownCombos = [
|
|
15
|
-
{ plat: "darwin", arch: "arm64" },
|
|
16
|
-
{ plat: "darwin", arch: "x64" },
|
|
17
|
-
{ plat: "linux", arch: "x64" },
|
|
18
|
-
{ plat: "linux", arch: "arm64" },
|
|
19
|
-
{ plat: "win32", arch: "x64" },
|
|
20
|
-
]
|
|
21
|
-
for (const { plat, arch } of knownCombos) {
|
|
22
|
-
const key = `${plat}-${arch}`
|
|
23
|
-
const asset = ASSETS[key]
|
|
24
|
-
assert.ok(asset, `ASSETS map should have entry for ${key}`)
|
|
25
|
-
assert.ok(asset.endsWith(".tar.gz") || asset.endsWith(".zip"),
|
|
26
|
-
`Asset "${asset}" should end with .tar.gz or .zip`)
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe("validatePath", () => {
|
|
32
|
-
it("accepts simple relative paths", () => {
|
|
33
|
-
assert.doesNotThrow(() => validatePath("foo/bar"))
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it("accepts absolute POSIX paths", () => {
|
|
37
|
-
assert.doesNotThrow(() => validatePath("/home/user/.config"))
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it("accepts absolute Windows paths", () => {
|
|
41
|
-
assert.doesNotThrow(() => validatePath("C:\\Users\\test"))
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it("accepts paths with dots and dashes", () => {
|
|
45
|
-
assert.doesNotThrow(() => validatePath("./dir/file-name.ext"))
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it("allows .. in paths (shell metacharacter check only, not path traversal)", () => {
|
|
49
|
-
assert.doesNotThrow(() => validatePath("../evil"))
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it("rejects paths with semicolons", () => {
|
|
53
|
-
assert.throws(() => validatePath("foo;rm -rf /"), /unsafe characters/)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it("rejects paths with backticks", () => {
|
|
57
|
-
assert.throws(() => validatePath("foo`id`"), /unsafe characters/)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it("rejects paths with dollar signs", () => {
|
|
61
|
-
assert.throws(() => validatePath("foo$PATH"), /unsafe characters/)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it("rejects paths with pipe characters", () => {
|
|
65
|
-
assert.throws(() => validatePath("foo|bar"), /unsafe characters/)
|
|
66
|
-
})
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
describe("findSqueezePath", () => {
|
|
70
|
-
it("returns null when squeeze is not available on PATH", () => {
|
|
71
|
-
const result = findSqueezePath()
|
|
72
|
-
// Either null (not on PATH) or a non-empty string (found)
|
|
73
|
-
assert.ok(result === null || typeof result === "string")
|
|
74
|
-
})
|
|
75
|
-
})
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { platformAsset, validatePath, findSqueezePath, ASSETS } from "./squeezeInstaller.js"
|
|
4
|
+
|
|
5
|
+
describe("platformAsset", () => {
|
|
6
|
+
it("returns a string or null for current platform", () => {
|
|
7
|
+
const result = platformAsset()
|
|
8
|
+
// On a supported platform: string; on unsupported: null
|
|
9
|
+
assert.ok(result === null || typeof result === "string")
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it("returns known asset names for mapped platforms", () => {
|
|
13
|
+
// Verify the ASSETS map has entries for all known platform combos
|
|
14
|
+
const knownCombos = [
|
|
15
|
+
{ plat: "darwin", arch: "arm64" },
|
|
16
|
+
{ plat: "darwin", arch: "x64" },
|
|
17
|
+
{ plat: "linux", arch: "x64" },
|
|
18
|
+
{ plat: "linux", arch: "arm64" },
|
|
19
|
+
{ plat: "win32", arch: "x64" },
|
|
20
|
+
]
|
|
21
|
+
for (const { plat, arch } of knownCombos) {
|
|
22
|
+
const key = `${plat}-${arch}`
|
|
23
|
+
const asset = ASSETS[key]
|
|
24
|
+
assert.ok(asset, `ASSETS map should have entry for ${key}`)
|
|
25
|
+
assert.ok(asset.endsWith(".tar.gz") || asset.endsWith(".zip"),
|
|
26
|
+
`Asset "${asset}" should end with .tar.gz or .zip`)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe("validatePath", () => {
|
|
32
|
+
it("accepts simple relative paths", () => {
|
|
33
|
+
assert.doesNotThrow(() => validatePath("foo/bar"))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("accepts absolute POSIX paths", () => {
|
|
37
|
+
assert.doesNotThrow(() => validatePath("/home/user/.config"))
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("accepts absolute Windows paths", () => {
|
|
41
|
+
assert.doesNotThrow(() => validatePath("C:\\Users\\test"))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("accepts paths with dots and dashes", () => {
|
|
45
|
+
assert.doesNotThrow(() => validatePath("./dir/file-name.ext"))
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("allows .. in paths (shell metacharacter check only, not path traversal)", () => {
|
|
49
|
+
assert.doesNotThrow(() => validatePath("../evil"))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("rejects paths with semicolons", () => {
|
|
53
|
+
assert.throws(() => validatePath("foo;rm -rf /"), /unsafe characters/)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("rejects paths with backticks", () => {
|
|
57
|
+
assert.throws(() => validatePath("foo`id`"), /unsafe characters/)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("rejects paths with dollar signs", () => {
|
|
61
|
+
assert.throws(() => validatePath("foo$PATH"), /unsafe characters/)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("rejects paths with pipe characters", () => {
|
|
65
|
+
assert.throws(() => validatePath("foo|bar"), /unsafe characters/)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("findSqueezePath", () => {
|
|
70
|
+
it("returns null when squeeze is not available on PATH", () => {
|
|
71
|
+
const result = findSqueezePath()
|
|
72
|
+
// Either null (not on PATH) or a non-empty string (found)
|
|
73
|
+
assert.ok(result === null || typeof result === "string")
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { resolve, dirname } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
export const version = JSON.parse(
|
|
7
|
+
readFileSync(resolve(__dirname, "../../package.json"), "utf-8")
|
|
8
|
+
).version
|