@veedubin/neuralgentics 0.9.2 → 0.9.3
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/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +180 -0
- package/dist/cli.js.map +1 -0
- package/dist/neuralgentics/download.d.ts +101 -0
- package/dist/neuralgentics/download.d.ts.map +1 -0
- package/dist/neuralgentics/download.js +348 -0
- package/dist/neuralgentics/download.js.map +1 -0
- package/dist/neuralgentics/init.d.ts +85 -0
- package/dist/neuralgentics/init.d.ts.map +1 -0
- package/dist/neuralgentics/init.js +719 -0
- package/dist/neuralgentics/init.js.map +1 -0
- package/dist/neuralgentics/merge.d.ts +56 -0
- package/dist/neuralgentics/merge.d.ts.map +1 -0
- package/dist/neuralgentics/merge.js +216 -0
- package/dist/neuralgentics/merge.js.map +1 -0
- package/package.json +4 -1
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for the `neuralgentics` command — TypeScript port of the
|
|
4
|
+
* Python `neuralgentics-cli/src/neuralgentics/cli.py`.
|
|
5
|
+
*
|
|
6
|
+
* Single-command bootstrap: pass `--init` (or the positional alias `init`)
|
|
7
|
+
* to download + place the neuralgentics OpenCode plugin into `--target`.
|
|
8
|
+
*
|
|
9
|
+
* Uses Node.js `util.parseArgs` for argv parsing (Node 20+ built-in).
|
|
10
|
+
*/
|
|
11
|
+
declare function main(argv: string[]): Promise<number>;
|
|
12
|
+
export { main };
|
|
13
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAqGH,iBAAe,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuEnD;AAwBD,OAAO,EAAE,IAAI,EAAE,CAAC"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for the `neuralgentics` command — TypeScript port of the
|
|
4
|
+
* Python `neuralgentics-cli/src/neuralgentics/cli.py`.
|
|
5
|
+
*
|
|
6
|
+
* Single-command bootstrap: pass `--init` (or the positional alias `init`)
|
|
7
|
+
* to download + place the neuralgentics OpenCode plugin into `--target`.
|
|
8
|
+
*
|
|
9
|
+
* Uses Node.js `util.parseArgs` for argv parsing (Node 20+ built-in).
|
|
10
|
+
*/
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import { CLI_VERSION, runInit, NeuralgenticsError } from "./neuralgentics/init.js";
|
|
13
|
+
/** Sentinel for `--version` with no argument (distinguishes bare vs. with-arg). */
|
|
14
|
+
const CLI_VERSION_SENTINEL = "__cli__";
|
|
15
|
+
function parseArgv(argv) {
|
|
16
|
+
const { values } = parseArgs({
|
|
17
|
+
args: argv,
|
|
18
|
+
options: {
|
|
19
|
+
init: { type: "boolean", default: false },
|
|
20
|
+
version: { type: "string" },
|
|
21
|
+
target: { type: "string", short: "t", default: "." },
|
|
22
|
+
force: { type: "boolean", default: false },
|
|
23
|
+
"dry-run": { type: "boolean", default: false },
|
|
24
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
25
|
+
repo: { type: "string", default: "Veedubin/neuralgentics" },
|
|
26
|
+
offline: { type: "boolean", default: false },
|
|
27
|
+
"with-backend": { type: "boolean", default: false },
|
|
28
|
+
},
|
|
29
|
+
allowPositionals: true,
|
|
30
|
+
});
|
|
31
|
+
// version handling: bare --version is intercepted in main() before
|
|
32
|
+
// parseArgs runs. Here we only see --version=<X.Y.Z> or --version X.Y.Z.
|
|
33
|
+
const version = values.version ?? "latest";
|
|
34
|
+
// Reconstruct positionals by re-parsing (parseArgs doesn't return them
|
|
35
|
+
// when tokens:false). We use a lightweight scan instead.
|
|
36
|
+
const positionals = [];
|
|
37
|
+
const knownFlags = new Set([
|
|
38
|
+
"--init", "--force", "--dry-run", "--yes", "-y", "--offline",
|
|
39
|
+
"--with-backend", "-h", "--help",
|
|
40
|
+
]);
|
|
41
|
+
const valueFlags = new Set(["--version", "--target", "-t", "--repo"]);
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a.startsWith("--") && a.includes("="))
|
|
45
|
+
continue; // --foo=bar form
|
|
46
|
+
if (knownFlags.has(a))
|
|
47
|
+
continue;
|
|
48
|
+
if (valueFlags.has(a)) {
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (a.startsWith("-"))
|
|
53
|
+
continue;
|
|
54
|
+
// Bare `--version` (already handled in main) — skip its potential next.
|
|
55
|
+
positionals.push(a);
|
|
56
|
+
}
|
|
57
|
+
const command = positionals.length > 0 ? positionals[0] : undefined;
|
|
58
|
+
return {
|
|
59
|
+
init: values.init === true,
|
|
60
|
+
target: values.target ?? ".",
|
|
61
|
+
force: values.force === true,
|
|
62
|
+
dryRun: values["dry-run"] === true,
|
|
63
|
+
yes: values.yes === true,
|
|
64
|
+
repo: values.repo ?? "Veedubin/neuralgentics",
|
|
65
|
+
version,
|
|
66
|
+
offline: values.offline === true,
|
|
67
|
+
withBackend: values["with-backend"] === true,
|
|
68
|
+
command,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function printHelp() {
|
|
72
|
+
process.stdout.write(`Usage: neuralgentics [init] [options]\n` +
|
|
73
|
+
`\n` +
|
|
74
|
+
`Bootstrapper CLI for the neuralgentics OpenCode plugin.\n` +
|
|
75
|
+
`\n` +
|
|
76
|
+
`Options:\n` +
|
|
77
|
+
` --init Bootstrap the target directory with the neuralgentics plugin.\n` +
|
|
78
|
+
` --version [VER] With no arg: print CLI version and exit. With arg: plugin version to install.\n` +
|
|
79
|
+
` --target, -t DIR Directory to bootstrap (default: current directory).\n` +
|
|
80
|
+
` --force Overwrite existing .opencode/ files even if user-modified.\n` +
|
|
81
|
+
` --dry-run Preview all actions without writing anything.\n` +
|
|
82
|
+
` --yes, -y Skip all confirmation prompts.\n` +
|
|
83
|
+
` --repo REPO GitHub repository to download from (default: Veedubin/neuralgentics).\n` +
|
|
84
|
+
` --offline Use a bundled tarball instead of downloading (not yet available).\n` +
|
|
85
|
+
` --with-backend Set up database containers (podman-compose / docker).\n` +
|
|
86
|
+
` -h, --help Show this help and exit.\n` +
|
|
87
|
+
`\n` +
|
|
88
|
+
`Positional alias: \`neuralgentics init\` is equivalent to \`--init\`.\n`);
|
|
89
|
+
}
|
|
90
|
+
function formatError(err) {
|
|
91
|
+
return `[ERROR] ${err.message}\nSuggestion: ${err.remediation}`;
|
|
92
|
+
}
|
|
93
|
+
async function main(argv) {
|
|
94
|
+
// Handle -h / --help before parseArgs (which doesn't know these).
|
|
95
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
96
|
+
printHelp();
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
// Handle bare `--version` (no following value) BEFORE parseArgs, because
|
|
100
|
+
// `parseArgs` with type:string requires a value and errors on bare form.
|
|
101
|
+
const bareVersionIdx = argv.indexOf("--version");
|
|
102
|
+
if (bareVersionIdx !== -1) {
|
|
103
|
+
const next = argv[bareVersionIdx + 1];
|
|
104
|
+
if (next === undefined || next.startsWith("-")) {
|
|
105
|
+
// Bare `--version` — print CLI version and exit 0.
|
|
106
|
+
process.stdout.write(`neuralgentics ${CLI_VERSION}\n`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = parseArgv(argv);
|
|
113
|
+
}
|
|
114
|
+
catch (exc) {
|
|
115
|
+
process.stderr.write(`[ERROR] ${exc instanceof Error ? exc.message : String(exc)}\n` +
|
|
116
|
+
`Run \`neuralgentics --help\` for usage.\n`);
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
// --version with an explicit argument: install plugin v<version>.
|
|
120
|
+
// (Bare form was handled above.) parsed.version is "latest" or "X.Y.Z".
|
|
121
|
+
// If the user passed `--version=X.Y.Z`, parsed.version is that value.
|
|
122
|
+
if (parsed.version !== "latest") {
|
|
123
|
+
// Only treat as "print version" if it was the sentinel — which we already
|
|
124
|
+
// handled above. Otherwise it's a plugin version to install.
|
|
125
|
+
}
|
|
126
|
+
// init requested via --init or the positional `init` alias.
|
|
127
|
+
const initRequested = parsed.init || parsed.command === "init";
|
|
128
|
+
if (!initRequested) {
|
|
129
|
+
printHelp();
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
const opts = {
|
|
133
|
+
init: true,
|
|
134
|
+
target: parsed.target,
|
|
135
|
+
force: parsed.force,
|
|
136
|
+
dryRun: parsed.dryRun,
|
|
137
|
+
yes: parsed.yes,
|
|
138
|
+
repo: parsed.repo,
|
|
139
|
+
version: parsed.version,
|
|
140
|
+
offline: parsed.offline,
|
|
141
|
+
withBackend: parsed.withBackend,
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
return await runInit(opts);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if (err instanceof NeuralgenticsError) {
|
|
148
|
+
process.stderr.write(formatError(err) + "\n");
|
|
149
|
+
return err.exitCode;
|
|
150
|
+
}
|
|
151
|
+
if (err instanceof Error) {
|
|
152
|
+
process.stderr.write(`[ERROR] ${err.message}\n`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
process.stderr.write(`[ERROR] ${String(err)}\n`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Entry point — only run when invoked directly (not when imported).
|
|
160
|
+
const isDirectInvocation = process.argv[1] && (process.argv[1].endsWith("cli.js") ||
|
|
161
|
+
process.argv[1].endsWith("cli") ||
|
|
162
|
+
process.argv[1].endsWith("neuralgentics"));
|
|
163
|
+
if (isDirectInvocation) {
|
|
164
|
+
main(process.argv.slice(2))
|
|
165
|
+
.then((code) => {
|
|
166
|
+
process.exitCode = code;
|
|
167
|
+
})
|
|
168
|
+
.catch((err) => {
|
|
169
|
+
if (err instanceof NeuralgenticsError) {
|
|
170
|
+
process.stderr.write(formatError(err) + "\n");
|
|
171
|
+
process.exitCode = err.exitCode;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
process.stderr.write(`[ERROR] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export { main };
|
|
180
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAe,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAEhG,mFAAmF;AACnF,MAAM,oBAAoB,GAAG,SAAS,CAAC;AAevC,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC3B,IAAI,EAAE,IAAI;QACV,OAAO,EAAE;YACP,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;YACzC,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;YAC3B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE;YACpD,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;YAC1C,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;YAC9C,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;YACpD,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,wBAAwB,EAAE;YAC3D,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;YAC5C,cAAc,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;SACpD;QACD,gBAAgB,EAAE,IAAI;KACvB,CAAC,CAAC;IAEH,mEAAmE;IACnE,yEAAyE;IACzE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC3C,uEAAuE;IACvE,yDAAyD;IACzD,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;QACzB,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW;QAC5D,gBAAgB,EAAE,IAAI,EAAE,QAAQ;KACjC,CAAC,CAAC;IACH,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IACtE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,iBAAiB;QACtE,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAC,CAAC,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QACzC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAChC,wEAAwE;QACxE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,CAAC;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEpE,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI;QAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG;QAC5B,KAAK,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI;QAC5B,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,KAAK,IAAI;QAClC,GAAG,EAAE,MAAM,CAAC,GAAG,KAAK,IAAI;QACxB,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,wBAAwB;QAC7C,OAAO;QACP,OAAO,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI;QAChC,WAAW,EAAE,MAAM,CAAC,cAAc,CAAC,KAAK,IAAI;QAC5C,OAAO;KACR,CAAC;AACJ,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,yCAAyC;QACvC,IAAI;QACJ,2DAA2D;QAC3D,IAAI;QACJ,YAAY;QACZ,uFAAuF;QACvF,uGAAuG;QACvG,8EAA8E;QAC9E,oFAAoF;QACpF,uEAAuE;QACvE,wDAAwD;QACxD,+FAA+F;QAC/F,2FAA2F;QAC3F,+EAA+E;QAC/E,kDAAkD;QAClD,IAAI;QACJ,yEAAyE,CAC5E,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,GAAuB;IAC1C,OAAO,WAAW,GAAG,CAAC,OAAO,iBAAiB,GAAG,CAAC,WAAW,EAAE,CAAC;AAClE,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,kEAAkE;IAClE,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnD,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,CAAC;IACX,CAAC;IAED,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACjD,IAAI,cAAc,KAAK,CAAC,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC;QACtC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,mDAAmD;YACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,WAAW,IAAI,CAAC,CAAC;YACvD,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAED,IAAI,MAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,WAAW,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI;YAC7D,2CAA2C,CAC9C,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,kEAAkE;IAClE,wEAAwE;IACxE,sEAAsE;IACtE,IAAI,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAChC,0EAA0E;QAC1E,6DAA6D;IAC/D,CAAC;IAED,4DAA4D;IAC5D,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC;IAC/D,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,IAAI,GAAgB;QACxB,IAAI,EAAE,IAAI;QACV,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,WAAW,EAAE,MAAM,CAAC,WAAW;KAChC,CAAC;IAEF,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;YACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAC9C,OAAO,GAAG,CAAC,QAAQ,CAAC;QACtB,CAAC;QACD,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;YACzB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;YACjD,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;IAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,CAC1C,CAAC;AACF,IAAI,kBAAkB,EAAE,CAAC;IACvB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SACxB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACtB,IAAI,GAAG,YAAY,kBAAkB,EAAE,CAAC;YACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAC9C,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAClC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED,OAAO,EAAE,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub release download, SHA256 verification, and tarball extraction —
|
|
3
|
+
* TypeScript port of `neuralgentics-cli/src/neuralgentics/download.py`.
|
|
4
|
+
*
|
|
5
|
+
* Uses Node 20+ built-ins only:
|
|
6
|
+
* - Native `fetch()` for HTTP (no `httpx`/`axios`).
|
|
7
|
+
* - `crypto.createHash('sha256')` for verification.
|
|
8
|
+
* - `child_process.execSync('tar -xzf ...')` for extraction.
|
|
9
|
+
*
|
|
10
|
+
* Public API:
|
|
11
|
+
* - `resolveVersion` — turn `"latest"` or `"X.Y.Z"` into a concrete semver
|
|
12
|
+
* string, calling the GitHub API for `"latest"`. Cached for 1 hour in
|
|
13
|
+
* `~/.cache/neuralgentics/version_cache.json`.
|
|
14
|
+
* - `downloadTarball` — fetch `neuralgentics-{version}.tar.gz` and
|
|
15
|
+
* `checksums.txt` from a GitHub release into a per-pid temp dir.
|
|
16
|
+
* - `verifySha256` — parse `checksums.txt` and compare the tarball's
|
|
17
|
+
* actual SHA256 against it.
|
|
18
|
+
* - `extractTarball` — extract the tarball to `dest` with
|
|
19
|
+
* `--strip-components=1` semantics via `tar -xzf`.
|
|
20
|
+
*/
|
|
21
|
+
/** Base class for all neuralgentics CLI errors (mirrors `errors.py`). */
|
|
22
|
+
export declare class NeuralgenticsError extends Error {
|
|
23
|
+
readonly exitCode: number;
|
|
24
|
+
readonly remediation: string;
|
|
25
|
+
constructor(message: string, remediation?: string);
|
|
26
|
+
}
|
|
27
|
+
/** An HTTP request failed (connect error, bad status, etc.). */
|
|
28
|
+
export declare class NetworkError extends NeuralgenticsError {
|
|
29
|
+
readonly exitCode = 6;
|
|
30
|
+
readonly remediation = "Check your network connection and verify the version exists on the GitHub releases page.";
|
|
31
|
+
constructor(message: string, remediation?: string);
|
|
32
|
+
}
|
|
33
|
+
/** Downloaded artifact's SHA256 did not match the published checksum. */
|
|
34
|
+
export declare class Sha256Mismatch extends NeuralgenticsError {
|
|
35
|
+
readonly exitCode = 7;
|
|
36
|
+
readonly remediation = "Re-run. If the problem persists, report it on GitHub issues \u2014 the download may be tampered.";
|
|
37
|
+
constructor(message: string, remediation?: string);
|
|
38
|
+
}
|
|
39
|
+
/** The downloaded tarball could not be extracted. */
|
|
40
|
+
export declare class TarballCorrupt extends NeuralgenticsError {
|
|
41
|
+
readonly exitCode = 8;
|
|
42
|
+
readonly remediation = "Check disk space and re-download. The archive may be corrupt.";
|
|
43
|
+
constructor(message: string, remediation?: string);
|
|
44
|
+
}
|
|
45
|
+
/** A file expected after extraction was missing. */
|
|
46
|
+
export declare class ExtractionFailed extends NeuralgenticsError {
|
|
47
|
+
readonly exitCode = 9;
|
|
48
|
+
readonly remediation = "Check the release assets on GitHub \u2014 the archive may be incomplete.";
|
|
49
|
+
constructor(message: string, remediation?: string);
|
|
50
|
+
}
|
|
51
|
+
/** The requested plugin version does not exist on GitHub. */
|
|
52
|
+
export declare class VersionNotFound extends NeuralgenticsError {
|
|
53
|
+
readonly exitCode = 14;
|
|
54
|
+
readonly remediation = "Check available versions at the GitHub releases page. Use 'latest' for the most recent release.";
|
|
55
|
+
constructor(message: string, remediation?: string);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve `version` to a concrete `X.Y.Z` string.
|
|
59
|
+
*
|
|
60
|
+
* - If `version === "latest"`, query the GitHub API
|
|
61
|
+
* `GET /repos/{repo}/releases/latest` and return `tag_name` with the
|
|
62
|
+
* leading `v` stripped. Cached for 1 hour in
|
|
63
|
+
* `~/.cache/neuralgentics/version_cache.json` (keyed by `repo`).
|
|
64
|
+
* - Otherwise, validate `version` matches `X.Y.Z` and return it as-is.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveVersion(version: string, repo: string, githubToken?: string): Promise<string>;
|
|
67
|
+
/** Result of `downloadTarball`: the local paths to the tarball + checksums. */
|
|
68
|
+
export interface DownloadedTarball {
|
|
69
|
+
tarballPath: string;
|
|
70
|
+
checksumsPath: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Download `neuralgentics-{version}.tar.gz` and `checksums.txt`.
|
|
74
|
+
*
|
|
75
|
+
* Returns the local paths. Files are written to a per-pid, per-version temp
|
|
76
|
+
* directory under `os.tmpdir()`. On any exception the temp directory is
|
|
77
|
+
* removed.
|
|
78
|
+
*/
|
|
79
|
+
export declare function downloadTarball(version: string, repo: string, githubToken?: string): Promise<DownloadedTarball>;
|
|
80
|
+
/**
|
|
81
|
+
* Verify `tarballPath` against the entry in `checksumsPath`.
|
|
82
|
+
*
|
|
83
|
+
* `checksums.txt` lines have the form `<sha> <filename>` (two spaces, GNU
|
|
84
|
+
* coreutils format). Raises `Sha256Mismatch` if the file is missing from
|
|
85
|
+
* checksums or the computed hash doesn't match.
|
|
86
|
+
*/
|
|
87
|
+
export declare function verifySha256(tarballPath: string, checksumsPath: string): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Extract `tarballPath` into `dest` with `--strip-components=1` semantics.
|
|
90
|
+
*
|
|
91
|
+
* The release tarball has a single top-level directory
|
|
92
|
+
* (`neuralgentics-{version}/`); this function flattens that one level so
|
|
93
|
+
* files land directly under `dest` (e.g. `dest/.opencode/agents/coder.md`).
|
|
94
|
+
*
|
|
95
|
+
* Uses `tar -xzf` via `child_process.execSync` (available on all Linux/Mac).
|
|
96
|
+
* After extraction, verifies that `dest/.opencode/agents/coder.md` exists —
|
|
97
|
+
* a known required file from the archive layout. Raises `ExtractionFailed`
|
|
98
|
+
* if missing, `TarballCorrupt` if the archive can't be opened.
|
|
99
|
+
*/
|
|
100
|
+
export declare function extractTarball(tarballPath: string, dest: string): Promise<void>;
|
|
101
|
+
//# sourceMappingURL=download.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/neuralgentics/download.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA8BH,yEAAyE;AACzE,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAK;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAgD;gBAChE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAOlD;AAED,gEAAgE;AAChE,qBAAa,YAAa,SAAQ,kBAAkB;IAClD,QAAQ,CAAC,QAAQ,KAAK;IACtB,QAAQ,CAAC,WAAW,8FACyE;gBACjF,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAIlD;AAED,yEAAyE;AACzE,qBAAa,cAAe,SAAQ,kBAAkB;IACpD,QAAQ,CAAC,QAAQ,KAAK;IACtB,QAAQ,CAAC,WAAW,sGAC4E;gBACpF,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAIlD;AAED,qDAAqD;AACrD,qBAAa,cAAe,SAAQ,kBAAkB;IACpD,QAAQ,CAAC,QAAQ,KAAK;IACtB,QAAQ,CAAC,WAAW,mEAAmE;gBAC3E,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAIlD;AAED,oDAAoD;AACpD,qBAAa,gBAAiB,SAAQ,kBAAkB;IACtD,QAAQ,CAAC,QAAQ,KAAK;IACtB,QAAQ,CAAC,WAAW,8EAAyE;gBACjF,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAIlD;AAED,6DAA6D;AAC7D,qBAAa,eAAgB,SAAQ,kBAAkB;IACrD,QAAQ,CAAC,QAAQ,MAAM;IACvB,QAAQ,CAAC,WAAW,qGACgF;gBACxF,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAIlD;AAMD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CAgBjB;AA4ED,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,iBAAiB,CAAC,CAgC5B;AAwCD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ5F;AAoCD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBrF"}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub release download, SHA256 verification, and tarball extraction —
|
|
3
|
+
* TypeScript port of `neuralgentics-cli/src/neuralgentics/download.py`.
|
|
4
|
+
*
|
|
5
|
+
* Uses Node 20+ built-ins only:
|
|
6
|
+
* - Native `fetch()` for HTTP (no `httpx`/`axios`).
|
|
7
|
+
* - `crypto.createHash('sha256')` for verification.
|
|
8
|
+
* - `child_process.execSync('tar -xzf ...')` for extraction.
|
|
9
|
+
*
|
|
10
|
+
* Public API:
|
|
11
|
+
* - `resolveVersion` — turn `"latest"` or `"X.Y.Z"` into a concrete semver
|
|
12
|
+
* string, calling the GitHub API for `"latest"`. Cached for 1 hour in
|
|
13
|
+
* `~/.cache/neuralgentics/version_cache.json`.
|
|
14
|
+
* - `downloadTarball` — fetch `neuralgentics-{version}.tar.gz` and
|
|
15
|
+
* `checksums.txt` from a GitHub release into a per-pid temp dir.
|
|
16
|
+
* - `verifySha256` — parse `checksums.txt` and compare the tarball's
|
|
17
|
+
* actual SHA256 against it.
|
|
18
|
+
* - `extractTarball` — extract the tarball to `dest` with
|
|
19
|
+
* `--strip-components=1` semantics via `tar -xzf`.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { execSync } from "node:child_process";
|
|
23
|
+
import { promises as fs } from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import * as os from "node:os";
|
|
26
|
+
/** Base URL for the GitHub REST API. */
|
|
27
|
+
const GITHUB_API = "https://api.github.com";
|
|
28
|
+
/** Base URL for GitHub release asset downloads (not rate-limited). */
|
|
29
|
+
const GITHUB_DOWNLOAD = "https://github.com";
|
|
30
|
+
/** Cache file for `latest` version resolution (1-hour TTL). */
|
|
31
|
+
const CACHE_DIR = path.join(os.homedir(), ".cache", "neuralgentics");
|
|
32
|
+
const CACHE_FILE = path.join(CACHE_DIR, "version_cache.json");
|
|
33
|
+
/** Cache TTL in seconds (1 hour). */
|
|
34
|
+
const CACHE_TTL = 3600;
|
|
35
|
+
/** Read chunk size for SHA256 computation (64 KiB). */
|
|
36
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
37
|
+
/** Regex for a valid `X.Y.Z` semver (no pre-release suffix). */
|
|
38
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
39
|
+
/** The known required file inside the extracted tarball layout. */
|
|
40
|
+
const REQUIRED_EXTRACTED_FILE = path.join(".opencode", "agents", "coder.md");
|
|
41
|
+
/** Base class for all neuralgentics CLI errors (mirrors `errors.py`). */
|
|
42
|
+
export class NeuralgenticsError extends Error {
|
|
43
|
+
exitCode = 1;
|
|
44
|
+
remediation = "See the documentation for troubleshooting.";
|
|
45
|
+
constructor(message, remediation) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "NeuralgenticsError";
|
|
48
|
+
if (remediation !== undefined) {
|
|
49
|
+
this.remediation = remediation;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** An HTTP request failed (connect error, bad status, etc.). */
|
|
54
|
+
export class NetworkError extends NeuralgenticsError {
|
|
55
|
+
exitCode = 6;
|
|
56
|
+
remediation = "Check your network connection and verify the version exists on the GitHub releases page.";
|
|
57
|
+
constructor(message, remediation) {
|
|
58
|
+
super(message, remediation);
|
|
59
|
+
this.name = "NetworkError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Downloaded artifact's SHA256 did not match the published checksum. */
|
|
63
|
+
export class Sha256Mismatch extends NeuralgenticsError {
|
|
64
|
+
exitCode = 7;
|
|
65
|
+
remediation = "Re-run. If the problem persists, report it on GitHub issues — the download may be tampered.";
|
|
66
|
+
constructor(message, remediation) {
|
|
67
|
+
super(message, remediation);
|
|
68
|
+
this.name = "Sha256Mismatch";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** The downloaded tarball could not be extracted. */
|
|
72
|
+
export class TarballCorrupt extends NeuralgenticsError {
|
|
73
|
+
exitCode = 8;
|
|
74
|
+
remediation = "Check disk space and re-download. The archive may be corrupt.";
|
|
75
|
+
constructor(message, remediation) {
|
|
76
|
+
super(message, remediation);
|
|
77
|
+
this.name = "TarballCorrupt";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** A file expected after extraction was missing. */
|
|
81
|
+
export class ExtractionFailed extends NeuralgenticsError {
|
|
82
|
+
exitCode = 9;
|
|
83
|
+
remediation = "Check the release assets on GitHub — the archive may be incomplete.";
|
|
84
|
+
constructor(message, remediation) {
|
|
85
|
+
super(message, remediation);
|
|
86
|
+
this.name = "ExtractionFailed";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The requested plugin version does not exist on GitHub. */
|
|
90
|
+
export class VersionNotFound extends NeuralgenticsError {
|
|
91
|
+
exitCode = 14;
|
|
92
|
+
remediation = "Check available versions at the GitHub releases page. Use 'latest' for the most recent release.";
|
|
93
|
+
constructor(message, remediation) {
|
|
94
|
+
super(message, remediation);
|
|
95
|
+
this.name = "VersionNotFound";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve `version` to a concrete `X.Y.Z` string.
|
|
100
|
+
*
|
|
101
|
+
* - If `version === "latest"`, query the GitHub API
|
|
102
|
+
* `GET /repos/{repo}/releases/latest` and return `tag_name` with the
|
|
103
|
+
* leading `v` stripped. Cached for 1 hour in
|
|
104
|
+
* `~/.cache/neuralgentics/version_cache.json` (keyed by `repo`).
|
|
105
|
+
* - Otherwise, validate `version` matches `X.Y.Z` and return it as-is.
|
|
106
|
+
*/
|
|
107
|
+
export async function resolveVersion(version, repo, githubToken) {
|
|
108
|
+
if (version === "latest") {
|
|
109
|
+
const cached = await readCachedLatest(repo);
|
|
110
|
+
if (cached !== null)
|
|
111
|
+
return cached;
|
|
112
|
+
const tag = await fetchLatestTag(repo, githubToken);
|
|
113
|
+
const resolved = tag.startsWith("v") ? tag.slice(1) : tag;
|
|
114
|
+
if (!SEMVER_RE.test(resolved)) {
|
|
115
|
+
throw new VersionNotFound(`GitHub returned a non-semver tag name ${JSON.stringify(tag)} for ${repo}.`);
|
|
116
|
+
}
|
|
117
|
+
await writeCachedLatest(repo, resolved);
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
if (SEMVER_RE.test(version))
|
|
121
|
+
return version;
|
|
122
|
+
throw new VersionNotFound(`Version ${JSON.stringify(version)} is not a valid X.Y.Z semver.`);
|
|
123
|
+
}
|
|
124
|
+
async function readCachedLatest(repo) {
|
|
125
|
+
try {
|
|
126
|
+
const raw = await fs.readFile(CACHE_FILE, "utf-8");
|
|
127
|
+
const data = JSON.parse(raw);
|
|
128
|
+
const entry = data[repo];
|
|
129
|
+
if (!entry || typeof entry.ts !== "number" || typeof entry.version !== "string") {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (Date.now() / 1000 - entry.ts > CACHE_TTL)
|
|
133
|
+
return null;
|
|
134
|
+
return entry.version;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function writeCachedLatest(repo, version) {
|
|
141
|
+
try {
|
|
142
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
143
|
+
let data = {};
|
|
144
|
+
try {
|
|
145
|
+
const raw = await fs.readFile(CACHE_FILE, "utf-8");
|
|
146
|
+
data = JSON.parse(raw);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
data = {};
|
|
150
|
+
}
|
|
151
|
+
data[repo] = { ts: Date.now() / 1000, version };
|
|
152
|
+
await fs.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Best-effort cache; ignore write failures.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function resolveToken(githubToken) {
|
|
159
|
+
return githubToken ?? process.env.GITHUB_TOKEN;
|
|
160
|
+
}
|
|
161
|
+
async function fetchLatestTag(repo, githubToken) {
|
|
162
|
+
const url = `${GITHUB_API}/repos/${repo}/releases/latest`;
|
|
163
|
+
const headers = { Accept: "application/vnd.github+json" };
|
|
164
|
+
const token = resolveToken(githubToken);
|
|
165
|
+
if (token)
|
|
166
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
167
|
+
let resp;
|
|
168
|
+
try {
|
|
169
|
+
resp = await fetch(url, { headers, redirect: "follow" });
|
|
170
|
+
}
|
|
171
|
+
catch (exc) {
|
|
172
|
+
throw new NetworkError(`Failed to reach GitHub API at ${url}: ${exc instanceof Error ? exc.message : String(exc)}`, "Check your network connection and that the repository exists.");
|
|
173
|
+
}
|
|
174
|
+
if (resp.status === 404) {
|
|
175
|
+
throw new VersionNotFound(`No releases found for ${repo}.`);
|
|
176
|
+
}
|
|
177
|
+
if (!resp.ok) {
|
|
178
|
+
throw new NetworkError(`GitHub API returned ${resp.status} ${resp.statusText} for ${url}.`);
|
|
179
|
+
}
|
|
180
|
+
let body;
|
|
181
|
+
try {
|
|
182
|
+
body = await resp.json();
|
|
183
|
+
}
|
|
184
|
+
catch (exc) {
|
|
185
|
+
throw new NetworkError(`GitHub API returned invalid JSON for ${url}: ${exc instanceof Error ? exc.message : String(exc)}.`);
|
|
186
|
+
}
|
|
187
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
188
|
+
throw new VersionNotFound(`GitHub API response for ${repo} had no 'tag_name' field.`);
|
|
189
|
+
}
|
|
190
|
+
const tag = body.tag_name;
|
|
191
|
+
if (typeof tag !== "string" || tag.length === 0) {
|
|
192
|
+
throw new VersionNotFound(`GitHub API response for ${repo} had no 'tag_name' field.`);
|
|
193
|
+
}
|
|
194
|
+
return tag;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Download `neuralgentics-{version}.tar.gz` and `checksums.txt`.
|
|
198
|
+
*
|
|
199
|
+
* Returns the local paths. Files are written to a per-pid, per-version temp
|
|
200
|
+
* directory under `os.tmpdir()`. On any exception the temp directory is
|
|
201
|
+
* removed.
|
|
202
|
+
*/
|
|
203
|
+
export async function downloadTarball(version, repo, githubToken) {
|
|
204
|
+
if (!SEMVER_RE.test(version)) {
|
|
205
|
+
throw new VersionNotFound(`Version ${JSON.stringify(version)} is not a valid X.Y.Z semver.`);
|
|
206
|
+
}
|
|
207
|
+
const tmpDir = path.join(os.tmpdir(), `neuralgentics-${process.pid}-${version}`);
|
|
208
|
+
await fs.mkdir(tmpDir, { recursive: true });
|
|
209
|
+
const tarballPath = path.join(tmpDir, `neuralgentics-${version}.tar.gz`);
|
|
210
|
+
const checksumsPath = path.join(tmpDir, "checksums.txt");
|
|
211
|
+
const base = `${GITHUB_DOWNLOAD}/${repo}/releases/download/v${version}`;
|
|
212
|
+
const tarballUrl = `${base}/neuralgentics-${version}.tar.gz`;
|
|
213
|
+
const checksumsUrl = `${base}/checksums.txt`;
|
|
214
|
+
const headers = {};
|
|
215
|
+
const token = resolveToken(githubToken);
|
|
216
|
+
if (token)
|
|
217
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
218
|
+
try {
|
|
219
|
+
await streamDownload(tarballUrl, tarballPath, headers);
|
|
220
|
+
await streamDownload(checksumsUrl, checksumsPath, headers);
|
|
221
|
+
}
|
|
222
|
+
catch (exc) {
|
|
223
|
+
// Cleanup the temp dir on any failure so we don't leave partial files.
|
|
224
|
+
try {
|
|
225
|
+
const entries = await fs.readdir(tmpDir);
|
|
226
|
+
await Promise.all(entries.map((e) => fs.unlink(path.join(tmpDir, e)).catch(() => { })));
|
|
227
|
+
await fs.rmdir(tmpDir).catch(() => { });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Best-effort cleanup.
|
|
231
|
+
}
|
|
232
|
+
throw exc;
|
|
233
|
+
}
|
|
234
|
+
return { tarballPath, checksumsPath };
|
|
235
|
+
}
|
|
236
|
+
async function streamDownload(url, dest, headers) {
|
|
237
|
+
let resp;
|
|
238
|
+
try {
|
|
239
|
+
resp = await fetch(url, { headers, redirect: "follow" });
|
|
240
|
+
}
|
|
241
|
+
catch (exc) {
|
|
242
|
+
throw new NetworkError(`Failed to download ${url}: ${exc instanceof Error ? exc.message : String(exc)}`, "Check your network connection and retry.");
|
|
243
|
+
}
|
|
244
|
+
if (!resp.ok) {
|
|
245
|
+
throw new NetworkError(`Failed to download ${url}: ${resp.status} ${resp.statusText}.`, "Check your network connection and that the version exists on the GitHub releases page.");
|
|
246
|
+
}
|
|
247
|
+
const body = resp.body;
|
|
248
|
+
if (body === null) {
|
|
249
|
+
throw new NetworkError(`Failed to download ${url}: empty response body.`);
|
|
250
|
+
}
|
|
251
|
+
const fileHandle = await fs.open(dest, "w");
|
|
252
|
+
try {
|
|
253
|
+
const reader = body.getReader();
|
|
254
|
+
// eslint-disable-next-line no-constant-condition
|
|
255
|
+
while (true) {
|
|
256
|
+
const { done, value } = await reader.read();
|
|
257
|
+
if (done)
|
|
258
|
+
break;
|
|
259
|
+
if (value)
|
|
260
|
+
await fileHandle.write(value);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
await fileHandle.close();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Verify `tarballPath` against the entry in `checksumsPath`.
|
|
269
|
+
*
|
|
270
|
+
* `checksums.txt` lines have the form `<sha> <filename>` (two spaces, GNU
|
|
271
|
+
* coreutils format). Raises `Sha256Mismatch` if the file is missing from
|
|
272
|
+
* checksums or the computed hash doesn't match.
|
|
273
|
+
*/
|
|
274
|
+
export async function verifySha256(tarballPath, checksumsPath) {
|
|
275
|
+
const expected = await lookupChecksum(checksumsPath, path.basename(tarballPath));
|
|
276
|
+
const actual = await sha256OfFile(tarballPath);
|
|
277
|
+
if (actual !== expected) {
|
|
278
|
+
throw new Sha256Mismatch(`SHA256 verification failed for ${path.basename(tarballPath)}. Expected: ${expected}, got: ${actual}.`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function lookupChecksum(checksumsPath, filename) {
|
|
282
|
+
const text = await fs.readFile(checksumsPath, "utf-8");
|
|
283
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
284
|
+
const line = rawLine.trim();
|
|
285
|
+
if (!line || line.startsWith("#"))
|
|
286
|
+
continue;
|
|
287
|
+
const parts = line.split(/\s+/);
|
|
288
|
+
if (parts.length !== 2)
|
|
289
|
+
continue;
|
|
290
|
+
const [sha, rawName] = parts;
|
|
291
|
+
// The checksum file may prefix the name with `*` (binary mode) or `./`.
|
|
292
|
+
const name = rawName.replace(/^\*/, "").replace(/^\.\//, "");
|
|
293
|
+
if (name === filename && /^[0-9a-f]{64}$/.test(sha)) {
|
|
294
|
+
return sha;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw new Sha256Mismatch(`${filename} not found in ${path.basename(checksumsPath)}.`);
|
|
298
|
+
}
|
|
299
|
+
async function sha256OfFile(filePath) {
|
|
300
|
+
const hash = createHash("sha256");
|
|
301
|
+
const handle = await fs.open(filePath, "r");
|
|
302
|
+
try {
|
|
303
|
+
const buffer = Buffer.alloc(CHUNK_SIZE);
|
|
304
|
+
// eslint-disable-next-line no-constant-condition
|
|
305
|
+
while (true) {
|
|
306
|
+
const { bytesRead } = await handle.read(buffer, 0, CHUNK_SIZE, null);
|
|
307
|
+
if (bytesRead === 0)
|
|
308
|
+
break;
|
|
309
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
await handle.close();
|
|
314
|
+
}
|
|
315
|
+
return hash.digest("hex");
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Extract `tarballPath` into `dest` with `--strip-components=1` semantics.
|
|
319
|
+
*
|
|
320
|
+
* The release tarball has a single top-level directory
|
|
321
|
+
* (`neuralgentics-{version}/`); this function flattens that one level so
|
|
322
|
+
* files land directly under `dest` (e.g. `dest/.opencode/agents/coder.md`).
|
|
323
|
+
*
|
|
324
|
+
* Uses `tar -xzf` via `child_process.execSync` (available on all Linux/Mac).
|
|
325
|
+
* After extraction, verifies that `dest/.opencode/agents/coder.md` exists —
|
|
326
|
+
* a known required file from the archive layout. Raises `ExtractionFailed`
|
|
327
|
+
* if missing, `TarballCorrupt` if the archive can't be opened.
|
|
328
|
+
*/
|
|
329
|
+
export async function extractTarball(tarballPath, dest) {
|
|
330
|
+
await fs.mkdir(dest, { recursive: true });
|
|
331
|
+
try {
|
|
332
|
+
execSync(`tar -xzf "${tarballPath}" -C "${dest}" --strip-components=1`, {
|
|
333
|
+
stdio: "pipe",
|
|
334
|
+
timeout: 60_000,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch (exc) {
|
|
338
|
+
throw new TarballCorrupt(`Failed to extract tarball: ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
339
|
+
}
|
|
340
|
+
const required = path.join(dest, REQUIRED_EXTRACTED_FILE);
|
|
341
|
+
try {
|
|
342
|
+
await fs.access(required);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
throw new ExtractionFailed(`${REQUIRED_EXTRACTED_FILE} not found after extraction.`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
//# sourceMappingURL=download.js.map
|