chimera-sigil 0.1.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/LICENSE +21 -0
- package/README.md +112 -0
- package/assets/chimera-cover.gif +0 -0
- package/assets/chimera-cover.png +0 -0
- package/bin/chimera.js +54 -0
- package/package.json +48 -0
- package/scripts/create-release-asset.mjs +53 -0
- package/scripts/install.mjs +142 -0
- package/scripts/npm-platform.mjs +54 -0
- package/scripts/release-config.mjs +108 -0
- package/scripts/release-integrity.mjs +36 -0
- package/scripts/release-local.mjs +771 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cody Mitchell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Chimera Sigil
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/chimera-cover.gif" alt="Chimera Sigil animated cover" width="420" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Multi-model terminal harness for fast, local-first iteration.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
Chimera Sigil gives you one terminal workflow across Grok, OpenAI, Anthropic, and local Ollama models. It is built for short feedback loops, visible tool use, and fast experimentation without depending on CI spend for every release artifact.
|
|
12
|
+
|
|
13
|
+
## Mission
|
|
14
|
+
|
|
15
|
+
Ship a practical multi-model harness that feels fast enough for daily use, clear enough to inspect, and flexible enough to evolve quickly.
|
|
16
|
+
|
|
17
|
+
## What It Does
|
|
18
|
+
|
|
19
|
+
- Runs a terminal agent with built-in file, search, edit, and shell tools
|
|
20
|
+
- Supports primary models, collaborator models, and fallback chains
|
|
21
|
+
- Persists sessions so single-prompt runs can resume later
|
|
22
|
+
- Supports approval modes for safer noninteractive automation
|
|
23
|
+
- Builds release artifacts locally on macOS, Windows, and Linux/WSL
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
Install from npm:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g chimera-sigil
|
|
31
|
+
chimera --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run from source:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cargo run -p chimera-sigil-cli -- --model grok-3
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Single prompt mode:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cargo run -p chimera-sigil-cli -- --model sonnet --prompt "Summarize this codebase"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Collaborative mode:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
cargo run -p chimera-sigil-cli -- --model grok-3 --collab sonnet,gpt-4o
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Fallback mode:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cargo run -p chimera-sigil-cli -- --model grok-3 --fallback gpt-4o,sonnet
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Approval Modes
|
|
59
|
+
|
|
60
|
+
- `--approval-mode prompt`: ask before writes and command execution
|
|
61
|
+
- `--approval-mode approve`: allow workspace writes, deny shell execution
|
|
62
|
+
- `--approval-mode full`: allow all tools
|
|
63
|
+
- `--auto-approve`: compatibility shorthand for `--approval-mode full`
|
|
64
|
+
|
|
65
|
+
## Environment
|
|
66
|
+
|
|
67
|
+
Set whichever providers you want to use:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export XAI_API_KEY=...
|
|
71
|
+
export OPENAI_API_KEY=...
|
|
72
|
+
export ANTHROPIC_API_KEY=...
|
|
73
|
+
export OLLAMA_BASE_URL=http://localhost:11434/v1
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
No provider keys are committed to this repository. Local machine and release credentials live outside the repo.
|
|
77
|
+
|
|
78
|
+
## Fast Iteration
|
|
79
|
+
|
|
80
|
+
- Prompt mode auto-saves sessions for later resume
|
|
81
|
+
- Release builds can be produced locally instead of relying on GitHub Actions
|
|
82
|
+
- The shipped CLI command stays short: `chimera`
|
|
83
|
+
|
|
84
|
+
## Contributing
|
|
85
|
+
|
|
86
|
+
Contributions are welcome. Keep changes small, tested, and easy to review.
|
|
87
|
+
|
|
88
|
+
Start here:
|
|
89
|
+
|
|
90
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
91
|
+
- [SECURITY.md](SECURITY.md)
|
|
92
|
+
|
|
93
|
+
## Project Layout
|
|
94
|
+
|
|
95
|
+
- `crates/chimera-core`: agent loop, config, sessions
|
|
96
|
+
- `crates/chimera-providers`: model providers and streaming
|
|
97
|
+
- `crates/chimera-tools`: built-in tools and permissions
|
|
98
|
+
- `crates/chimera-cli`: CLI entrypoint
|
|
99
|
+
- `scripts/`: packaging and local-first release tooling
|
|
100
|
+
|
|
101
|
+
## Release Notes
|
|
102
|
+
|
|
103
|
+
The npm package name is `chimera-sigil`. The installed command is `chimera`.
|
|
104
|
+
|
|
105
|
+
Local-first release tooling currently supports:
|
|
106
|
+
|
|
107
|
+
- `aarch64-apple-darwin`
|
|
108
|
+
- `x86_64-apple-darwin`
|
|
109
|
+
- `x86_64-pc-windows-msvc`
|
|
110
|
+
- `x86_64-unknown-linux-gnu`
|
|
111
|
+
|
|
112
|
+
For contributor setup and release workflow details, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
Binary file
|
|
Binary file
|
package/bin/chimera.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { binaryNameForTriple, detectCurrentTarget } from "../scripts/npm-platform.mjs";
|
|
9
|
+
|
|
10
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const packageRoot = path.resolve(scriptDir, "..");
|
|
12
|
+
|
|
13
|
+
function resolveBinaryPath() {
|
|
14
|
+
const target = detectCurrentTarget();
|
|
15
|
+
const binaryName = binaryNameForTriple(target.triple);
|
|
16
|
+
const localBinaryName = process.platform === "win32" ? "chimera.exe" : "chimera";
|
|
17
|
+
const override = process.env.CHIMERA_SIGIL_BIN || process.env.CHIMERA_HARNESS_BIN;
|
|
18
|
+
|
|
19
|
+
const candidates = [
|
|
20
|
+
override,
|
|
21
|
+
path.join(packageRoot, "target", "release", localBinaryName),
|
|
22
|
+
path.join(packageRoot, "target", "debug", localBinaryName),
|
|
23
|
+
path.join(packageRoot, "vendor", target.triple, binaryName)
|
|
24
|
+
].filter(Boolean);
|
|
25
|
+
|
|
26
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const binaryPath = resolveBinaryPath();
|
|
30
|
+
|
|
31
|
+
if (!binaryPath) {
|
|
32
|
+
console.error("chimera binary is not installed for this package.");
|
|
33
|
+
console.error("Reinstall with `npm install -g chimera-sigil` or rerun `npm rebuild chimera-sigil`.");
|
|
34
|
+
console.error(
|
|
35
|
+
"For custom builds, set CHIMERA_SIGIL_BIN=/absolute/path/to/chimera (legacy CHIMERA_HARNESS_BIN also works)."
|
|
36
|
+
);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
41
|
+
|
|
42
|
+
child.on("error", (error) => {
|
|
43
|
+
console.error(`Failed to launch chimera: ${error.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
child.on("exit", (code, signal) => {
|
|
48
|
+
if (signal) {
|
|
49
|
+
process.kill(process.pid, signal);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.exit(code ?? 1);
|
|
54
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chimera-sigil",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Multi-model AI agent harness for the terminal",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"chimera": "bin/chimera.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"LICENSE",
|
|
12
|
+
"README.md",
|
|
13
|
+
"assets/chimera-cover.png",
|
|
14
|
+
"assets/chimera-cover.gif",
|
|
15
|
+
"bin/",
|
|
16
|
+
"scripts/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build:release": "cargo build --release -p chimera-sigil-cli",
|
|
20
|
+
"dist:npm": "npm run build:release && node scripts/create-release-asset.mjs",
|
|
21
|
+
"release:doctor": "node scripts/release-local.mjs --doctor",
|
|
22
|
+
"release:local": "node scripts/release-local.mjs",
|
|
23
|
+
"postinstall": "node scripts/install.mjs"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ai",
|
|
27
|
+
"cli",
|
|
28
|
+
"terminal",
|
|
29
|
+
"agent",
|
|
30
|
+
"llm",
|
|
31
|
+
"rust"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/sproutseeds/chimera-sigil.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/sproutseeds/chimera-sigil/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/sproutseeds/chimera-sigil#readme",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"preferGlobal": true,
|
|
45
|
+
"chimeraSigil": {
|
|
46
|
+
"releasesBaseUrl": "https://github.com/sproutseeds/chimera-sigil/releases/download"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
binaryNameForTriple,
|
|
7
|
+
releaseAssetFileName,
|
|
8
|
+
targetTripleFromEnvOrCurrent
|
|
9
|
+
} from "./npm-platform.mjs";
|
|
10
|
+
import { writeChecksumFile } from "./release-integrity.mjs";
|
|
11
|
+
|
|
12
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const packageRoot = path.resolve(scriptDir, "..");
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
const packageJson = JSON.parse(
|
|
17
|
+
await fs.readFile(path.join(packageRoot, "package.json"), "utf8")
|
|
18
|
+
);
|
|
19
|
+
const version = packageJson.version;
|
|
20
|
+
const targetTriple = targetTripleFromEnvOrCurrent(process.env.TARGET_TRIPLE);
|
|
21
|
+
const binaryName = binaryNameForTriple(targetTriple);
|
|
22
|
+
const targetDir = process.env.TARGET_TRIPLE
|
|
23
|
+
? path.join(packageRoot, "target", targetTriple, "release")
|
|
24
|
+
: path.join(packageRoot, "target", "release");
|
|
25
|
+
const sourceBinary = path.join(targetDir, binaryName);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(sourceBinary);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Compiled binary not found at ${sourceBinary}. Run \`cargo build --release -p chimera-sigil-cli${process.env.TARGET_TRIPLE ? ` --target ${targetTriple}` : ""}\` first.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const distDir = path.join(packageRoot, "dist");
|
|
36
|
+
await fs.mkdir(distDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
const outputPath = path.join(distDir, releaseAssetFileName(version, targetTriple));
|
|
39
|
+
await fs.copyFile(sourceBinary, outputPath);
|
|
40
|
+
|
|
41
|
+
if (!targetTriple.includes("windows")) {
|
|
42
|
+
await fs.chmod(outputPath, 0o755);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`Created release asset: ${outputPath}`);
|
|
46
|
+
const { checksumPath } = await writeChecksumFile(outputPath);
|
|
47
|
+
console.log(`Created checksum: ${checksumPath}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch((error) => {
|
|
51
|
+
console.error(error.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
binaryNameForTriple,
|
|
11
|
+
detectCurrentTarget,
|
|
12
|
+
releaseAssetFileName
|
|
13
|
+
} from "./npm-platform.mjs";
|
|
14
|
+
import {
|
|
15
|
+
checksumFileName,
|
|
16
|
+
parseChecksumContents,
|
|
17
|
+
sha256File
|
|
18
|
+
} from "./release-integrity.mjs";
|
|
19
|
+
|
|
20
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const packageRoot = path.resolve(scriptDir, "..");
|
|
22
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
23
|
+
|
|
24
|
+
function firstNonEmpty(...values) {
|
|
25
|
+
return values.find((value) => value !== undefined && value !== null && value !== "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readPackageJson() {
|
|
29
|
+
return JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fileExists(filePath) {
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(filePath);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function downloadFile(url, destination) {
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
headers: {
|
|
44
|
+
"user-agent": "chimera-sigil-npm-installer"
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok || !response.body) {
|
|
49
|
+
throw new Error(`Download failed with status ${response.status} for ${url}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const skipDownloadFlag = firstNonEmpty(
|
|
57
|
+
process.env.CHIMERA_SIGIL_SKIP_DOWNLOAD,
|
|
58
|
+
process.env.CHIMERA_HARNESS_SKIP_DOWNLOAD
|
|
59
|
+
);
|
|
60
|
+
if (skipDownloadFlag === "1") {
|
|
61
|
+
console.log(
|
|
62
|
+
"Skipping chimera binary download because CHIMERA_SIGIL_SKIP_DOWNLOAD=1."
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const pkg = await readPackageJson();
|
|
68
|
+
const version = pkg.version;
|
|
69
|
+
const releasesBaseUrl = firstNonEmpty(
|
|
70
|
+
process.env.CHIMERA_SIGIL_RELEASES_BASE_URL,
|
|
71
|
+
process.env.CHIMERA_HARNESS_RELEASES_BASE_URL,
|
|
72
|
+
pkg.chimeraSigil?.releasesBaseUrl,
|
|
73
|
+
pkg.chimeraHarness?.releasesBaseUrl
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!releasesBaseUrl) {
|
|
77
|
+
throw new Error("No releases base URL configured for chimera-sigil.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { triple } = detectCurrentTarget();
|
|
81
|
+
const assetName = releaseAssetFileName(version, triple);
|
|
82
|
+
const checksumName = checksumFileName(assetName);
|
|
83
|
+
const binaryName = binaryNameForTriple(triple);
|
|
84
|
+
const installDir = path.join(packageRoot, "vendor", triple);
|
|
85
|
+
const binaryPath = path.join(installDir, binaryName);
|
|
86
|
+
|
|
87
|
+
if (await fileExists(binaryPath)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await fs.mkdir(installDir, { recursive: true });
|
|
92
|
+
|
|
93
|
+
const tempPath = path.join(os.tmpdir(), `${assetName}.${process.pid}.tmp`);
|
|
94
|
+
const checksumTempPath = path.join(os.tmpdir(), `${checksumName}.${process.pid}.tmp`);
|
|
95
|
+
const assetUrl = `${releasesBaseUrl.replace(/\/$/, "")}/v${version}/${assetName}`;
|
|
96
|
+
const checksumUrl = `${releasesBaseUrl.replace(/\/$/, "")}/v${version}/${checksumName}`;
|
|
97
|
+
const skipVerify =
|
|
98
|
+
firstNonEmpty(
|
|
99
|
+
process.env.CHIMERA_SIGIL_SKIP_VERIFY,
|
|
100
|
+
process.env.CHIMERA_HARNESS_SKIP_VERIFY
|
|
101
|
+
) === "1";
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
console.log(`Downloading chimera ${version} for ${triple}...`);
|
|
105
|
+
await downloadFile(assetUrl, tempPath);
|
|
106
|
+
if (!skipVerify) {
|
|
107
|
+
await downloadFile(checksumUrl, checksumTempPath);
|
|
108
|
+
const checksum = parseChecksumContents(
|
|
109
|
+
await fs.readFile(checksumTempPath, "utf8")
|
|
110
|
+
);
|
|
111
|
+
if (checksum.assetName !== assetName) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Checksum file referenced ${checksum.assetName}, expected ${assetName}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const actualHash = await sha256File(tempPath);
|
|
118
|
+
if (actualHash !== checksum.hash) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Checksum mismatch for ${assetName}: expected ${checksum.hash}, got ${actualHash}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
await fs.copyFile(tempPath, binaryPath);
|
|
125
|
+
if (!triple.includes("windows")) {
|
|
126
|
+
await fs.chmod(binaryPath, 0o755);
|
|
127
|
+
}
|
|
128
|
+
console.log(`Installed chimera to ${binaryPath}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Unable to install chimera from ${assetUrl}: ${error.message}`
|
|
132
|
+
);
|
|
133
|
+
} finally {
|
|
134
|
+
await fs.rm(tempPath, { force: true }).catch(() => {});
|
|
135
|
+
await fs.rm(checksumTempPath, { force: true }).catch(() => {});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main().catch((error) => {
|
|
140
|
+
console.error(error.message);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
const TARGETS = {
|
|
4
|
+
"darwin-arm64": "aarch64-apple-darwin",
|
|
5
|
+
"darwin-x64": "x86_64-apple-darwin",
|
|
6
|
+
"linux-arm64-gnu": "aarch64-unknown-linux-gnu",
|
|
7
|
+
"linux-arm64-musl": "aarch64-unknown-linux-musl",
|
|
8
|
+
"linux-x64-gnu": "x86_64-unknown-linux-gnu",
|
|
9
|
+
"linux-x64-musl": "x86_64-unknown-linux-musl",
|
|
10
|
+
"win32-arm64": "aarch64-pc-windows-msvc",
|
|
11
|
+
"win32-x64": "x86_64-pc-windows-msvc"
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function detectLinuxLibc() {
|
|
15
|
+
const report = process.report?.getReport?.();
|
|
16
|
+
return report?.header?.glibcVersionRuntime ? "gnu" : "musl";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function detectCurrentTarget() {
|
|
20
|
+
if (process.platform === "linux") {
|
|
21
|
+
const libc = detectLinuxLibc();
|
|
22
|
+
const key = `${process.platform}-${process.arch}-${libc}`;
|
|
23
|
+
const triple = TARGETS[key];
|
|
24
|
+
if (!triple) {
|
|
25
|
+
throw new Error(`Unsupported platform/arch combination: ${key}`);
|
|
26
|
+
}
|
|
27
|
+
return { triple, key };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const key = `${process.platform}-${process.arch}`;
|
|
31
|
+
const triple = TARGETS[key];
|
|
32
|
+
if (!triple) {
|
|
33
|
+
throw new Error(`Unsupported platform/arch combination: ${key}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { triple, key };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function binaryNameForTriple(triple) {
|
|
40
|
+
return triple.includes("windows") ? "chimera.exe" : "chimera";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function releaseAssetFileName(version, triple) {
|
|
44
|
+
const suffix = triple.includes("windows") ? ".exe" : "";
|
|
45
|
+
return `chimera-sigil-v${version}-${triple}${suffix}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function targetTripleFromEnvOrCurrent(envValue) {
|
|
49
|
+
if (envValue) {
|
|
50
|
+
return envValue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return detectCurrentTarget().triple;
|
|
54
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
|
|
5
|
+
const homeDir = os.homedir();
|
|
6
|
+
|
|
7
|
+
export function defaultGlobalConfigPaths() {
|
|
8
|
+
return [
|
|
9
|
+
path.join(homeDir, ".config", "orp", "chimera-sigil-release.json"),
|
|
10
|
+
path.join(homeDir, ".config", "chimera-sigil", "release.json"),
|
|
11
|
+
path.join(homeDir, ".config", "orp", "chimera-harness-release.json"),
|
|
12
|
+
path.join(homeDir, ".config", "chimera-harness", "release.json")
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function fileExists(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.access(filePath);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function loadGlobalReleaseConfig() {
|
|
26
|
+
for (const configPath of defaultGlobalConfigPaths()) {
|
|
27
|
+
if (!(await fileExists(configPath))) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parsed = JSON.parse(await fs.readFile(configPath, "utf8"));
|
|
32
|
+
return {
|
|
33
|
+
source: configPath,
|
|
34
|
+
config: normalizeReleaseConfig(parsed)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
source: null,
|
|
40
|
+
config: normalizeReleaseConfig({})
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeReleaseConfig(config) {
|
|
45
|
+
return {
|
|
46
|
+
windowsHost: config.windowsHost ?? "",
|
|
47
|
+
windowsUser: config.windowsUser ?? "",
|
|
48
|
+
windowsRoot: config.windowsRoot ?? "",
|
|
49
|
+
wslDistro: config.wslDistro ?? "",
|
|
50
|
+
sshIdentityFile: config.sshIdentityFile ?? "",
|
|
51
|
+
sshPort:
|
|
52
|
+
config.sshPort === undefined || config.sshPort === null
|
|
53
|
+
? ""
|
|
54
|
+
: String(config.sshPort)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function mergeReleaseConfig(base, overlay) {
|
|
59
|
+
const result = { ...base };
|
|
60
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
61
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
62
|
+
result[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return normalizeReleaseConfig(result);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function looksLikePrivateHost(host) {
|
|
69
|
+
return (
|
|
70
|
+
/^10\./.test(host) ||
|
|
71
|
+
/^192\.168\./.test(host) ||
|
|
72
|
+
/^172\.(1[6-9]|2\d|3[0-1])\./.test(host) ||
|
|
73
|
+
host === "localhost"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function findKnownHostCandidates() {
|
|
78
|
+
const knownHostsPath = path.join(homeDir, ".ssh", "known_hosts");
|
|
79
|
+
if (!(await fileExists(knownHostsPath))) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const contents = await fs.readFile(knownHostsPath, "utf8");
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const hosts = [];
|
|
86
|
+
|
|
87
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
88
|
+
const line = rawLine.trim();
|
|
89
|
+
if (!line || line.startsWith("|")) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hostField = line.split(/\s+/)[0];
|
|
94
|
+
if (!hostField) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const host = hostField.replace(/^\[([^\]]+)\]:\d+$/, "$1");
|
|
99
|
+
if (!looksLikePrivateHost(host) || seen.has(host)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
seen.add(host);
|
|
104
|
+
hosts.push(host);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return hosts;
|
|
108
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export async function sha256File(filePath) {
|
|
6
|
+
const bytes = await fs.readFile(filePath);
|
|
7
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function checksumFileName(assetName) {
|
|
11
|
+
return `${assetName}.sha256`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatChecksumContents(hash, assetName) {
|
|
15
|
+
return `${hash} ${assetName}\n`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function writeChecksumFile(assetPath) {
|
|
19
|
+
const hash = await sha256File(assetPath);
|
|
20
|
+
const assetName = path.basename(assetPath);
|
|
21
|
+
const checksumPath = `${assetPath}.sha256`;
|
|
22
|
+
await fs.writeFile(checksumPath, formatChecksumContents(hash, assetName), "utf8");
|
|
23
|
+
return { hash, checksumPath };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseChecksumContents(contents) {
|
|
27
|
+
const match = contents.trim().match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
|
|
28
|
+
if (!match) {
|
|
29
|
+
throw new Error("Invalid checksum file format.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
hash: match[1].toLowerCase(),
|
|
34
|
+
assetName: match[2].trim()
|
|
35
|
+
};
|
|
36
|
+
}
|