@nail00749/agent-gvozd 0.1.8 → 0.2.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/README.md +68 -11
- package/defaults/agents/back-deep.jsonc +1923 -6
- package/defaults/agents/back-fast.jsonc +1908 -3
- package/defaults/agents/front-deep.jsonc +1978 -17
- package/defaults/agents/front-fast.jsonc +1963 -14
- package/defaults/agents/git.jsonc +20 -0
- package/defaults/agents/master-trusted.jsonc +32 -0
- package/defaults/agents/master.jsonc +0 -1
- package/defaults/agents/review-deep.jsonc +2443 -18
- package/defaults/agents/review-fast.jsonc +2428 -15
- package/defaults/agents/security.jsonc +2213 -29
- package/defaults/prompts/back-deep.md +1 -1
- package/defaults/prompts/back-fast.md +1 -1
- package/defaults/prompts/devops.md +1 -1
- package/defaults/prompts/front-deep.md +1 -1
- package/defaults/prompts/front-fast.md +1 -1
- package/defaults/prompts/master-trusted.md +7 -0
- package/defaults/prompts/master.md +2 -2
- package/defaults/prompts/review-deep.md +3 -1
- package/defaults/prompts/review-fast.md +1 -1
- package/defaults/prompts/security.md +3 -1
- package/dist/cli.js +303 -114
- package/dist/index.js +271 -11
- package/dist/tui.js +1080 -0
- package/package.json +13 -3
- package/defaults/agents/verifier.jsonc +0 -1298
package/dist/cli.js
CHANGED
|
@@ -988,6 +988,7 @@ var i = `${styleText("gray", S_BAR)} `;
|
|
|
988
988
|
|
|
989
989
|
// src/cli.ts
|
|
990
990
|
import { realpathSync as realpathSync5 } from "node:fs";
|
|
991
|
+
import { join as join12 } from "node:path";
|
|
991
992
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
992
993
|
|
|
993
994
|
// src/config.ts
|
|
@@ -7229,8 +7230,8 @@ function loadConfig(projectDirectory, options = {}) {
|
|
|
7229
7230
|
}
|
|
7230
7231
|
|
|
7231
7232
|
// src/cli/doctor.ts
|
|
7232
|
-
import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as
|
|
7233
|
-
import { join as
|
|
7233
|
+
import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
7234
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
7234
7235
|
|
|
7235
7236
|
// src/tool-permissions.ts
|
|
7236
7237
|
function family(command, ...variants) {
|
|
@@ -7336,9 +7337,9 @@ function buildAgentPermissions(agent, mcpServers) {
|
|
|
7336
7337
|
|
|
7337
7338
|
// src/release-metadata.ts
|
|
7338
7339
|
var PACKAGE_NAME = "@nail00749/agent-gvozd";
|
|
7339
|
-
var PACKAGE_VERSION = "0.1
|
|
7340
|
+
var PACKAGE_VERSION = "0.2.1";
|
|
7340
7341
|
var PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
7341
|
-
var SUPPORTED_OPENCODE_VERSION = "2.0
|
|
7342
|
+
var SUPPORTED_OPENCODE_VERSION = "2.0.*";
|
|
7342
7343
|
var CONFIG_SCHEMA_VERSION = 2;
|
|
7343
7344
|
|
|
7344
7345
|
// src/constants.ts
|
|
@@ -7433,12 +7434,15 @@ function redactDiagnostic(error) {
|
|
|
7433
7434
|
|
|
7434
7435
|
// src/cli/opencode.ts
|
|
7435
7436
|
import { spawn } from "node:child_process";
|
|
7436
|
-
import {
|
|
7437
|
+
import { closeSync, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync } from "node:fs";
|
|
7438
|
+
import { isAbsolute as isAbsolute4, join as join4 } from "node:path";
|
|
7439
|
+
import { tmpdir } from "node:os";
|
|
7437
7440
|
var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
7438
7441
|
var AGENT_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
7439
7442
|
var DEFAULT_TIMEOUT_MS = 15000;
|
|
7440
7443
|
var defaultProcessRunner = {
|
|
7441
|
-
run(executable, args, timeoutMs = DEFAULT_TIMEOUT_MS, maxOutputBytes = MAX_OUTPUT_BYTES) {
|
|
7444
|
+
run(executable, args, timeoutMs = DEFAULT_TIMEOUT_MS, maxOutputBytes = MAX_OUTPUT_BYTES, options) {
|
|
7445
|
+
const stdoutFile = options?.stdoutFile;
|
|
7442
7446
|
const appendBounded = (current, chunk) => {
|
|
7443
7447
|
if (Buffer.byteLength(current) >= maxOutputBytes)
|
|
7444
7448
|
return current;
|
|
@@ -7447,11 +7451,21 @@ var defaultProcessRunner = {
|
|
|
7447
7451
|
};
|
|
7448
7452
|
return new Promise((resolve, reject) => {
|
|
7449
7453
|
const grouped = process.platform !== "win32";
|
|
7454
|
+
let stdoutFd;
|
|
7455
|
+
try {
|
|
7456
|
+
if (stdoutFile)
|
|
7457
|
+
stdoutFd = openSync(stdoutFile, "w");
|
|
7458
|
+
} catch (error) {
|
|
7459
|
+
reject(error);
|
|
7460
|
+
return;
|
|
7461
|
+
}
|
|
7450
7462
|
const child = spawn(executable, [...args], {
|
|
7451
7463
|
detached: grouped,
|
|
7452
7464
|
shell: false,
|
|
7453
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
7465
|
+
stdio: ["ignore", stdoutFile ? stdoutFd : "pipe", "pipe"]
|
|
7454
7466
|
});
|
|
7467
|
+
if (stdoutFd !== undefined)
|
|
7468
|
+
closeSync(stdoutFd);
|
|
7455
7469
|
let stdout = "";
|
|
7456
7470
|
let stderr = "";
|
|
7457
7471
|
let settled = false;
|
|
@@ -7494,10 +7508,10 @@ var defaultProcessRunner = {
|
|
|
7494
7508
|
settled = true;
|
|
7495
7509
|
clearTimers();
|
|
7496
7510
|
if (destroy) {
|
|
7497
|
-
child.
|
|
7498
|
-
child.stderr
|
|
7499
|
-
child.stdout
|
|
7500
|
-
child.
|
|
7511
|
+
child.stderr?.off("data", onStderr);
|
|
7512
|
+
child.stderr?.destroy();
|
|
7513
|
+
child.stdout?.off("data", onStdout);
|
|
7514
|
+
child.stdout?.destroy();
|
|
7501
7515
|
}
|
|
7502
7516
|
reject(timeoutError());
|
|
7503
7517
|
};
|
|
@@ -7512,18 +7526,20 @@ var defaultProcessRunner = {
|
|
|
7512
7526
|
hardDeadline.unref();
|
|
7513
7527
|
}, timeoutMs);
|
|
7514
7528
|
timer.unref();
|
|
7515
|
-
child.stdout
|
|
7516
|
-
|
|
7529
|
+
if (child.stdout)
|
|
7530
|
+
child.stdout.on("data", onStdout);
|
|
7531
|
+
if (child.stderr)
|
|
7532
|
+
child.stderr.on("data", onStderr);
|
|
7517
7533
|
child.once("error", (error) => {
|
|
7518
7534
|
clearTimers();
|
|
7519
7535
|
if (settled)
|
|
7520
7536
|
return;
|
|
7521
7537
|
settled = true;
|
|
7522
7538
|
if (timedOut) {
|
|
7523
|
-
child.
|
|
7524
|
-
child.stderr
|
|
7525
|
-
child.stdout
|
|
7526
|
-
child.
|
|
7539
|
+
child.stderr?.off("data", onStderr);
|
|
7540
|
+
child.stderr?.destroy();
|
|
7541
|
+
child.stdout?.off("data", onStdout);
|
|
7542
|
+
child.stdout?.destroy();
|
|
7527
7543
|
}
|
|
7528
7544
|
reject(error);
|
|
7529
7545
|
});
|
|
@@ -7536,7 +7552,16 @@ var defaultProcessRunner = {
|
|
|
7536
7552
|
return;
|
|
7537
7553
|
}
|
|
7538
7554
|
settled = true;
|
|
7539
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
7555
|
+
const finish = (stdoutText) => resolve({ code: code ?? 1, stdout: stdoutText, stderr });
|
|
7556
|
+
if (stdoutFile) {
|
|
7557
|
+
try {
|
|
7558
|
+
finish(bounded(readFileSync3(stdoutFile, "utf8"), maxOutputBytes));
|
|
7559
|
+
} catch (error) {
|
|
7560
|
+
reject(error);
|
|
7561
|
+
}
|
|
7562
|
+
return;
|
|
7563
|
+
}
|
|
7564
|
+
finish(stdout);
|
|
7540
7565
|
});
|
|
7541
7566
|
});
|
|
7542
7567
|
}
|
|
@@ -7544,8 +7569,8 @@ var defaultProcessRunner = {
|
|
|
7544
7569
|
function bounded(value, maxOutputBytes = MAX_OUTPUT_BYTES) {
|
|
7545
7570
|
return Buffer.from(value).subarray(0, maxOutputBytes).toString("utf8");
|
|
7546
7571
|
}
|
|
7547
|
-
async function checked(runner, executable, args, timeoutMs, maxOutputBytes) {
|
|
7548
|
-
const result = await runner.run(executable, args, timeoutMs, maxOutputBytes);
|
|
7572
|
+
async function checked(runner, executable, args, timeoutMs, maxOutputBytes, options) {
|
|
7573
|
+
const result = await runner.run(executable, args, timeoutMs, maxOutputBytes, options);
|
|
7549
7574
|
if (result.code !== 0) {
|
|
7550
7575
|
const detail = redactDiagnostic(bounded(result.stderr).trim());
|
|
7551
7576
|
throw new Error(`OpenCode command exited ${result.code}${detail ? `: ${detail}` : ""}`);
|
|
@@ -7568,6 +7593,18 @@ function parseDebugPaths(output) {
|
|
|
7568
7593
|
function parseOpenCodeVersion(output) {
|
|
7569
7594
|
return output.match(/(?:^|\s)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?=$|\s)/)?.[1];
|
|
7570
7595
|
}
|
|
7596
|
+
function satisfiesOpenCodeRange(version, range) {
|
|
7597
|
+
if (!version)
|
|
7598
|
+
return false;
|
|
7599
|
+
const [prerelease] = version.split("-").slice(1);
|
|
7600
|
+
if (prerelease)
|
|
7601
|
+
return false;
|
|
7602
|
+
if (!range.includes("*"))
|
|
7603
|
+
return version === range;
|
|
7604
|
+
const [rangeMajor, rangeMinor] = range.split(".").slice(0, 2);
|
|
7605
|
+
const [major, minor] = version.split(".").slice(0, 2);
|
|
7606
|
+
return major === rangeMajor && minor === rangeMinor;
|
|
7607
|
+
}
|
|
7571
7608
|
function parseAgentIdentifiers(output) {
|
|
7572
7609
|
try {
|
|
7573
7610
|
const parsed = JSON.parse(output);
|
|
@@ -7603,8 +7640,14 @@ function createClient(executable, runner) {
|
|
|
7603
7640
|
return checked(runner, executable, spec ? ["plugin", "check", spec] : ["plugin", "check"], 30000);
|
|
7604
7641
|
},
|
|
7605
7642
|
async debugAgents() {
|
|
7606
|
-
const
|
|
7607
|
-
|
|
7643
|
+
const directory = mkdtempSync(join4(tmpdir(), "gvozd-agents-"));
|
|
7644
|
+
const stdoutFile = join4(directory, "agents.out");
|
|
7645
|
+
try {
|
|
7646
|
+
const raw = await checked(runner, executable, ["debug", "agents"], 60000, AGENT_OUTPUT_BYTES, { stdoutFile });
|
|
7647
|
+
return parseAgentIdentifiers(raw);
|
|
7648
|
+
} finally {
|
|
7649
|
+
rmSync(directory, { recursive: true, force: true });
|
|
7650
|
+
}
|
|
7608
7651
|
},
|
|
7609
7652
|
serviceStatus() {
|
|
7610
7653
|
return checked(runner, executable, ["service", "status"]);
|
|
@@ -7711,23 +7754,23 @@ function checkGlobalFiles(config, configRoot) {
|
|
|
7711
7754
|
for (const [id, agent] of Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
7712
7755
|
if (agent.disabled)
|
|
7713
7756
|
continue;
|
|
7714
|
-
const path =
|
|
7757
|
+
const path = join5(configRoot, "agents", `${id}.md`);
|
|
7715
7758
|
if (!safeFile(path)) {
|
|
7716
7759
|
missing.push(id);
|
|
7717
7760
|
continue;
|
|
7718
7761
|
}
|
|
7719
|
-
const content =
|
|
7762
|
+
const content = readFileSync4(path, "utf8");
|
|
7720
7763
|
if (!hasGeneratedAgentMarker(content) || content !== renderAgent(agent))
|
|
7721
7764
|
stale.push(id);
|
|
7722
7765
|
}
|
|
7723
7766
|
const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
|
|
7724
7767
|
const orphans = [];
|
|
7725
|
-
const directory =
|
|
7768
|
+
const directory = join5(configRoot, "agents");
|
|
7726
7769
|
if (existsSync3(directory) && lstatSync3(directory).isDirectory() && !lstatSync3(directory).isSymbolicLink()) {
|
|
7727
7770
|
for (const entry of readdirSync3(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
7728
7771
|
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
7729
7772
|
continue;
|
|
7730
|
-
const content =
|
|
7773
|
+
const content = readFileSync4(join5(directory, entry.name), "utf8");
|
|
7731
7774
|
if (hasGeneratedAgentMarker(content))
|
|
7732
7775
|
orphans.push(entry.name.slice(0, -3));
|
|
7733
7776
|
}
|
|
@@ -7744,10 +7787,10 @@ function checkGlobalFiles(config, configRoot) {
|
|
|
7744
7787
|
}
|
|
7745
7788
|
function checkLegacy(config) {
|
|
7746
7789
|
const duplicates = [];
|
|
7747
|
-
const plugin =
|
|
7790
|
+
const plugin = join5(config.projectRoot, ".opencode", "plugins", "agent-gvozd", "index.ts");
|
|
7748
7791
|
if (safeFile(plugin))
|
|
7749
7792
|
duplicates.push("local plugin");
|
|
7750
|
-
const agents = Object.keys(config.agents).filter((id) => safeFile(
|
|
7793
|
+
const agents = Object.keys(config.agents).filter((id) => safeFile(join5(config.projectRoot, ".opencode", "agents", `${id}.md`)));
|
|
7751
7794
|
if (agents.length > 0)
|
|
7752
7795
|
duplicates.push(`${agents.length} local agents`);
|
|
7753
7796
|
if (duplicates.length === 0)
|
|
@@ -7766,7 +7809,7 @@ async function runDoctor(input) {
|
|
|
7766
7809
|
const checks = [];
|
|
7767
7810
|
try {
|
|
7768
7811
|
const version = await input.client.version();
|
|
7769
|
-
checks.push(parseOpenCodeVersion(version)
|
|
7812
|
+
checks.push(satisfiesOpenCodeRange(parseOpenCodeVersion(version), supportedVersion) ? { id: "opencode-version", status: "pass", summary: `OpenCode ${supportedVersion} is available` } : { id: "opencode-version", status: "fail", summary: `unsupported OpenCode version: ${redactDiagnostic(version)}`, remediation: `Install OpenCode ${supportedVersion}` });
|
|
7770
7813
|
} catch (error) {
|
|
7771
7814
|
checks.push({ id: "opencode-version", status: "fail", summary: `OpenCode version check failed: ${redactDiagnostic(error)}`, remediation: `Install OpenCode ${supportedVersion}` });
|
|
7772
7815
|
}
|
|
@@ -7798,13 +7841,13 @@ async function runDoctor(input) {
|
|
|
7798
7841
|
}
|
|
7799
7842
|
let config;
|
|
7800
7843
|
let globalConfig;
|
|
7801
|
-
const configPath =
|
|
7802
|
-
const schemaPath =
|
|
7844
|
+
const configPath = join5(input.configRoot, "gvozd", "config.jsonc");
|
|
7845
|
+
const schemaPath = join5(input.configRoot, "gvozd", "schema.json");
|
|
7803
7846
|
try {
|
|
7804
7847
|
if (!safeFile(configPath) || !safeFile(schemaPath))
|
|
7805
7848
|
throw new Error("global config.jsonc or schema.json is missing");
|
|
7806
7849
|
const schemaErrors = [];
|
|
7807
|
-
const schema = parse2(
|
|
7850
|
+
const schema = parse2(readFileSync4(schemaPath, "utf8"), schemaErrors);
|
|
7808
7851
|
if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION || schema?.$comment !== GENERATED_PLUGIN_MARKER) {
|
|
7809
7852
|
throw new Error("global schema is invalid, incompatible, or unmanaged");
|
|
7810
7853
|
}
|
|
@@ -7871,18 +7914,18 @@ function doctorOperationalFailure(error) {
|
|
|
7871
7914
|
return { schemaVersion: CONFIG_SCHEMA_VERSION, status: "fail", checks };
|
|
7872
7915
|
}
|
|
7873
7916
|
|
|
7874
|
-
// src/cli/
|
|
7875
|
-
import { existsSync as
|
|
7876
|
-
import {
|
|
7917
|
+
// src/cli/agents.ts
|
|
7918
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
7919
|
+
import { join as join8 } from "node:path";
|
|
7877
7920
|
|
|
7878
7921
|
// src/cli/config-store.ts
|
|
7879
|
-
import { accessSync, closeSync, constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync5, mkdirSync, openSync, readFileSync as
|
|
7922
|
+
import { accessSync, closeSync as closeSync2, constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync5, mkdirSync, openSync as openSync2, readFileSync as readFileSync5, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7880
7923
|
import { randomUUID } from "node:crypto";
|
|
7881
|
-
import { dirname as dirname3, join as
|
|
7924
|
+
import { dirname as dirname3, join as join7 } from "node:path";
|
|
7882
7925
|
|
|
7883
7926
|
// src/secure-path.ts
|
|
7884
7927
|
import { lstatSync as lstatSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
7885
|
-
import { isAbsolute as isAbsolute5, join as
|
|
7928
|
+
import { isAbsolute as isAbsolute5, join as join6, parse as parse6, relative as relative3, resolve as resolve5, sep } from "node:path";
|
|
7886
7929
|
function secureCanonicalPath(target, label = "Path") {
|
|
7887
7930
|
if (!isAbsolute5(target))
|
|
7888
7931
|
throw new Error(`${label} must be absolute: ${target}`);
|
|
@@ -7898,7 +7941,7 @@ function secureCanonicalPath(target, label = "Path") {
|
|
|
7898
7941
|
throw new Error(`${label} has an unsafe filesystem root: ${root}`);
|
|
7899
7942
|
const existingDirectories = [{ path: root, stat: rootStat }];
|
|
7900
7943
|
for (const component of components) {
|
|
7901
|
-
current =
|
|
7944
|
+
current = join6(current, component);
|
|
7902
7945
|
if (missing) {
|
|
7903
7946
|
missingSuffix.push(component);
|
|
7904
7947
|
continue;
|
|
@@ -7943,8 +7986,8 @@ function secureCanonicalPath(target, label = "Path") {
|
|
|
7943
7986
|
}
|
|
7944
7987
|
|
|
7945
7988
|
// src/cli/config-store.ts
|
|
7946
|
-
var FAST_AGENT_IDS = ["back-fast", "front-fast", "review-fast", "researcher", "git", "docs"
|
|
7947
|
-
var DEEP_AGENT_IDS = ["master", "planner", "back-deep", "front-deep", "review-deep", "debugger", "security", "devops"];
|
|
7989
|
+
var FAST_AGENT_IDS = ["back-fast", "front-fast", "review-fast", "researcher", "git", "docs"];
|
|
7990
|
+
var DEEP_AGENT_IDS = ["master", "master-trusted", "planner", "back-deep", "front-deep", "review-deep", "debugger", "security", "devops"];
|
|
7948
7991
|
var ALL_AGENT_IDS = [...DEEP_AGENT_IDS, ...FAST_AGENT_IDS, "explorer"];
|
|
7949
7992
|
var formattingOptions = { insertSpaces: true, tabSize: 2, eol: `
|
|
7950
7993
|
` };
|
|
@@ -7969,13 +8012,16 @@ function applyModelProfile(source, profile) {
|
|
|
7969
8012
|
updated = setJsonc(updated, ["agents", "explorer", "models"], profile.agentOverrides.explorer ?? profile.fast);
|
|
7970
8013
|
return updated;
|
|
7971
8014
|
}
|
|
8015
|
+
function writeManagedGlobalFile(path, content, expected) {
|
|
8016
|
+
atomicWrite(path, content, expected);
|
|
8017
|
+
}
|
|
7972
8018
|
function matchesSnapshot(path, expected) {
|
|
7973
8019
|
if (!expected.exists)
|
|
7974
8020
|
return !existsSync4(path);
|
|
7975
8021
|
if (!existsSync4(path))
|
|
7976
8022
|
return false;
|
|
7977
8023
|
const stat = lstatSync5(path);
|
|
7978
|
-
return stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino &&
|
|
8024
|
+
return stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino && readFileSync5(path, "utf8") === expected.bytes;
|
|
7979
8025
|
}
|
|
7980
8026
|
function atomicWrite(path, content, expected) {
|
|
7981
8027
|
const canonicalPath = secureCanonicalPath(path, "Managed global config path");
|
|
@@ -7986,16 +8032,16 @@ function atomicWrite(path, content, expected) {
|
|
|
7986
8032
|
throw new Error(`Global config path changed during write: ${path}`);
|
|
7987
8033
|
assertWriteable(dirname3(canonicalPath), "Managed global config directory");
|
|
7988
8034
|
const temporary = `${canonicalPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
7989
|
-
const descriptor =
|
|
8035
|
+
const descriptor = openSync2(temporary, "wx", 384);
|
|
7990
8036
|
try {
|
|
7991
8037
|
writeFileSync(descriptor, content);
|
|
7992
|
-
|
|
8038
|
+
closeSync2(descriptor);
|
|
7993
8039
|
if (!matchesSnapshot(canonicalPath, expected))
|
|
7994
8040
|
throw new Error(`Refusing to replace concurrently changed file: ${canonicalPath}`);
|
|
7995
8041
|
renameSync(temporary, canonicalPath);
|
|
7996
8042
|
} catch (error) {
|
|
7997
8043
|
try {
|
|
7998
|
-
|
|
8044
|
+
closeSync2(descriptor);
|
|
7999
8045
|
} catch {}
|
|
8000
8046
|
try {
|
|
8001
8047
|
unlinkSync(temporary);
|
|
@@ -8047,7 +8093,7 @@ function snapshot(path) {
|
|
|
8047
8093
|
const stat = lstatSync5(path);
|
|
8048
8094
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
8049
8095
|
throw new Error(`Managed global config snapshot target is unsafe: ${path}`);
|
|
8050
|
-
return Object.freeze({ path, exists: true, bytes:
|
|
8096
|
+
return Object.freeze({ path, exists: true, bytes: readFileSync5(path, "utf8"), dev: stat.dev, ino: stat.ino });
|
|
8051
8097
|
}
|
|
8052
8098
|
function preflightGlobalConfig(configRoot, schemaSource) {
|
|
8053
8099
|
const canonicalRoot = secureCanonicalPath(configRoot, "OpenCode config root");
|
|
@@ -8056,24 +8102,24 @@ function preflightGlobalConfig(configRoot, schemaSource) {
|
|
|
8056
8102
|
throw new Error(`OpenCode config root is not a safe directory: ${canonicalRoot}`);
|
|
8057
8103
|
}
|
|
8058
8104
|
assertWriteable(canonicalRoot, "OpenCode config root");
|
|
8059
|
-
const directory = secureCanonicalPath(
|
|
8105
|
+
const directory = secureCanonicalPath(join7(canonicalRoot, "gvozd"), "Global Gvozd directory");
|
|
8060
8106
|
const directoryStat = existsSync4(directory) ? lstatSync5(directory) : undefined;
|
|
8061
8107
|
if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
|
|
8062
8108
|
throw new Error(`Global Gvozd path is not a safe directory: ${directory}`);
|
|
8063
8109
|
}
|
|
8064
8110
|
assertWriteable(directory, "Global Gvozd directory");
|
|
8065
|
-
const configPath =
|
|
8066
|
-
const schemaPath =
|
|
8111
|
+
const configPath = join7(directory, "config.jsonc");
|
|
8112
|
+
const schemaPath = join7(directory, "schema.json");
|
|
8067
8113
|
if (existsSync4(configPath)) {
|
|
8068
8114
|
if (!isRegularFile(configPath))
|
|
8069
8115
|
throw new Error(`Global Gvozd config is not a safe file: ${configPath}`);
|
|
8070
|
-
assertValidJsonc(
|
|
8116
|
+
assertValidJsonc(readFileSync5(configPath, "utf8"), configPath);
|
|
8071
8117
|
assertWriteable(configPath, "Global Gvozd config");
|
|
8072
8118
|
}
|
|
8073
8119
|
if (existsSync4(schemaPath)) {
|
|
8074
8120
|
if (!isRegularFile(schemaPath))
|
|
8075
8121
|
throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
|
|
8076
|
-
const schema =
|
|
8122
|
+
const schema = readFileSync5(schemaPath, "utf8");
|
|
8077
8123
|
if (!hasGeneratedSchemaMarker(schema) && !legacySchemaMatches(schema, schemaSource)) {
|
|
8078
8124
|
throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
|
|
8079
8125
|
}
|
|
@@ -8086,9 +8132,9 @@ function writeGlobalConfig(input) {
|
|
|
8086
8132
|
const canonicalRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
|
|
8087
8133
|
if (state.configRoot !== canonicalRoot)
|
|
8088
8134
|
throw new Error("Global config snapshot belongs to another config root");
|
|
8089
|
-
const directory =
|
|
8090
|
-
const configPath =
|
|
8091
|
-
const schemaPath =
|
|
8135
|
+
const directory = join7(state.configRoot, "gvozd");
|
|
8136
|
+
const configPath = join7(directory, "config.jsonc");
|
|
8137
|
+
const schemaPath = join7(directory, "schema.json");
|
|
8092
8138
|
const base = state.config.exists ? state.config.bytes : `{
|
|
8093
8139
|
"$schema": "./schema.json",
|
|
8094
8140
|
"agents": {}
|
|
@@ -8108,6 +8154,51 @@ function writeGlobalConfig(input) {
|
|
|
8108
8154
|
return { configPath, schemaPath, config };
|
|
8109
8155
|
}
|
|
8110
8156
|
|
|
8157
|
+
// src/cli/agents.ts
|
|
8158
|
+
var formattingOptions2 = { insertSpaces: true, tabSize: 2, eol: `
|
|
8159
|
+
` };
|
|
8160
|
+
function assertValidJsonc2(source, label) {
|
|
8161
|
+
const errors = [];
|
|
8162
|
+
const value = parse2(source, errors, { allowTrailingComma: true, disallowComments: false });
|
|
8163
|
+
if (errors.length > 0 || value === undefined || value === null || Array.isArray(value) || typeof value !== "object") {
|
|
8164
|
+
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
8165
|
+
throw new Error(`Invalid JSONC in ${label}${details ? `: ${details}` : ""}`);
|
|
8166
|
+
}
|
|
8167
|
+
}
|
|
8168
|
+
function listAgents(cwd) {
|
|
8169
|
+
const config = loadConfig(cwd, { configRoot: resolveOpenCodeConfigRoot() });
|
|
8170
|
+
return Object.entries(config.agents).map(([id, agent]) => ({
|
|
8171
|
+
id,
|
|
8172
|
+
mode: agent.mode,
|
|
8173
|
+
lease: agent.fileLease,
|
|
8174
|
+
disabled: agent.disabled,
|
|
8175
|
+
model: agent.models[0] ?? ""
|
|
8176
|
+
}));
|
|
8177
|
+
}
|
|
8178
|
+
function setAgentDisabled(source, agentID, disabled) {
|
|
8179
|
+
if (!ALL_AGENT_IDS.includes(agentID)) {
|
|
8180
|
+
throw new Error(`Unknown agent: ${agentID}`);
|
|
8181
|
+
}
|
|
8182
|
+
assertValidJsonc2(source, "global Gvozd config");
|
|
8183
|
+
return applyEdits(source, modify(source, ["agents", agentID, "disabled"], disabled, { formattingOptions: formattingOptions2 }));
|
|
8184
|
+
}
|
|
8185
|
+
function readGlobalConfig(configRoot) {
|
|
8186
|
+
const path = join8(configRoot, "gvozd", "config.jsonc");
|
|
8187
|
+
if (!existsSync5(path))
|
|
8188
|
+
return `{
|
|
8189
|
+
"$schema": "./schema.json",
|
|
8190
|
+
"agents": {}
|
|
8191
|
+
}
|
|
8192
|
+
`;
|
|
8193
|
+
const source = readFileSync6(path, "utf8");
|
|
8194
|
+
assertValidJsonc2(source, path);
|
|
8195
|
+
return source;
|
|
8196
|
+
}
|
|
8197
|
+
|
|
8198
|
+
// src/cli/setup.ts
|
|
8199
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
8200
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
8201
|
+
|
|
8111
8202
|
// src/cli/configure.ts
|
|
8112
8203
|
function availableProfile(profile, catalog) {
|
|
8113
8204
|
if (!profile)
|
|
@@ -8177,9 +8268,9 @@ function profileFromAgents(agents, catalog) {
|
|
|
8177
8268
|
}
|
|
8178
8269
|
|
|
8179
8270
|
// src/cli/global-sync.ts
|
|
8180
|
-
import { accessSync as accessSync2, closeSync as
|
|
8271
|
+
import { accessSync as accessSync2, closeSync as closeSync3, constants as fsConstants2, existsSync as existsSync6, lstatSync as lstatSync6, mkdirSync as mkdirSync2, openSync as openSync3, readFileSync as readFileSync7, readdirSync as readdirSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8181
8272
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
8182
|
-
import { dirname as dirname4, join as
|
|
8273
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
8183
8274
|
function stat(path) {
|
|
8184
8275
|
try {
|
|
8185
8276
|
return lstatSync6(path);
|
|
@@ -8191,7 +8282,7 @@ function stat(path) {
|
|
|
8191
8282
|
}
|
|
8192
8283
|
function assertWriteable2(path, label) {
|
|
8193
8284
|
let candidate = path;
|
|
8194
|
-
while (!
|
|
8285
|
+
while (!existsSync6(candidate)) {
|
|
8195
8286
|
const parent = dirname4(candidate);
|
|
8196
8287
|
if (parent === candidate)
|
|
8197
8288
|
break;
|
|
@@ -8213,11 +8304,11 @@ function createFile(path, content) {
|
|
|
8213
8304
|
const canonicalPath = secureCanonicalPath(path, "Managed global agent path");
|
|
8214
8305
|
if (canonicalPath !== path)
|
|
8215
8306
|
throw new Error(`Managed global agent path changed: ${path}`);
|
|
8216
|
-
const descriptor =
|
|
8307
|
+
const descriptor = openSync3(canonicalPath, "wx", 384);
|
|
8217
8308
|
try {
|
|
8218
8309
|
writeFileSync2(descriptor, content);
|
|
8219
8310
|
} finally {
|
|
8220
|
-
|
|
8311
|
+
closeSync3(descriptor);
|
|
8221
8312
|
}
|
|
8222
8313
|
}
|
|
8223
8314
|
function replaceFile(path, content) {
|
|
@@ -8236,7 +8327,7 @@ function replaceFile(path, content) {
|
|
|
8236
8327
|
}
|
|
8237
8328
|
function writeManagedAgents(input) {
|
|
8238
8329
|
const configRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
|
|
8239
|
-
const agentsDirectory = secureCanonicalPath(
|
|
8330
|
+
const agentsDirectory = secureCanonicalPath(join9(configRoot, "agents"), "OpenCode agents path");
|
|
8240
8331
|
const result = { created: [], updated: [], unchanged: [], conflicts: [], removed: [] };
|
|
8241
8332
|
const writes = [];
|
|
8242
8333
|
const removals = new Map;
|
|
@@ -8255,21 +8346,21 @@ function writeManagedAgents(input) {
|
|
|
8255
8346
|
for (const entry of readdirSync4(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
8256
8347
|
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
8257
8348
|
continue;
|
|
8258
|
-
const path =
|
|
8349
|
+
const path = join9(agentsDirectory, entry.name);
|
|
8259
8350
|
const currentStat = stat(path);
|
|
8260
8351
|
if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile())
|
|
8261
8352
|
continue;
|
|
8262
|
-
if (hasGeneratedAgentMarker(
|
|
8353
|
+
if (hasGeneratedAgentMarker(readFileSync7(path, "utf8"))) {
|
|
8263
8354
|
assertWriteable2(path, "Managed global agent");
|
|
8264
8355
|
result.removed.push(path);
|
|
8265
|
-
removals.set(path,
|
|
8356
|
+
removals.set(path, readFileSync7(path, "utf8"));
|
|
8266
8357
|
}
|
|
8267
8358
|
}
|
|
8268
8359
|
}
|
|
8269
8360
|
for (const [id, agent] of Object.entries(input.agents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
8270
8361
|
if (agent.disabled)
|
|
8271
8362
|
continue;
|
|
8272
|
-
const path =
|
|
8363
|
+
const path = join9(agentsDirectory, `${id}.md`);
|
|
8273
8364
|
const content = renderAgent(agent);
|
|
8274
8365
|
const currentStat = stat(path);
|
|
8275
8366
|
if (!currentStat) {
|
|
@@ -8281,7 +8372,7 @@ function writeManagedAgents(input) {
|
|
|
8281
8372
|
result.conflicts.push(path);
|
|
8282
8373
|
continue;
|
|
8283
8374
|
}
|
|
8284
|
-
const current =
|
|
8375
|
+
const current = readFileSync7(path, "utf8");
|
|
8285
8376
|
if (!hasGeneratedAgentMarker(current)) {
|
|
8286
8377
|
result.conflicts.push(path);
|
|
8287
8378
|
continue;
|
|
@@ -8319,7 +8410,7 @@ function writeManagedAgents(input) {
|
|
|
8319
8410
|
if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile()) {
|
|
8320
8411
|
throw new Error(`Refusing to remove a changed or unmanaged global agent: ${path}`);
|
|
8321
8412
|
}
|
|
8322
|
-
const current =
|
|
8413
|
+
const current = readFileSync7(path, "utf8");
|
|
8323
8414
|
if (!hasGeneratedAgentMarker(current) || current !== removals.get(path)) {
|
|
8324
8415
|
throw new Error(`Refusing to remove a concurrently changed global agent: ${path}`);
|
|
8325
8416
|
}
|
|
@@ -8328,7 +8419,7 @@ function writeManagedAgents(input) {
|
|
|
8328
8419
|
for (const write of writes) {
|
|
8329
8420
|
if (write.replace) {
|
|
8330
8421
|
const currentStat = stat(write.path);
|
|
8331
|
-
const current = currentStat?.isFile() && !currentStat.isSymbolicLink() ?
|
|
8422
|
+
const current = currentStat?.isFile() && !currentStat.isSymbolicLink() ? readFileSync7(write.path, "utf8") : undefined;
|
|
8332
8423
|
if (!current || !hasGeneratedAgentMarker(current) || current !== write.previous) {
|
|
8333
8424
|
throw new Error(`Refusing to replace a concurrently changed global agent: ${write.path}`);
|
|
8334
8425
|
}
|
|
@@ -8341,8 +8432,30 @@ function writeManagedAgents(input) {
|
|
|
8341
8432
|
|
|
8342
8433
|
// src/file-lock.ts
|
|
8343
8434
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8344
|
-
import { accessSync as accessSync3, closeSync as
|
|
8435
|
+
import { accessSync as accessSync3, closeSync as closeSync4, constants, existsSync as existsSync7, fstatSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync4, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
8345
8436
|
import { dirname as dirname5 } from "node:path";
|
|
8437
|
+
var STALE_LOCK_AGE_MS = 30 * 60 * 1000;
|
|
8438
|
+
function lockOwnerAlive(path) {
|
|
8439
|
+
let contents;
|
|
8440
|
+
try {
|
|
8441
|
+
contents = readFileSync8(path, "utf8");
|
|
8442
|
+
} catch {
|
|
8443
|
+
return true;
|
|
8444
|
+
}
|
|
8445
|
+
const pid = Number(contents.split(":")[0]);
|
|
8446
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
8447
|
+
return false;
|
|
8448
|
+
if (pid === process.pid)
|
|
8449
|
+
return true;
|
|
8450
|
+
try {
|
|
8451
|
+
process.kill(pid, 0);
|
|
8452
|
+
return true;
|
|
8453
|
+
} catch (error) {
|
|
8454
|
+
if (error.code === "ESRCH")
|
|
8455
|
+
return false;
|
|
8456
|
+
return true;
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8346
8459
|
function assertSecure(path) {
|
|
8347
8460
|
const stat = lstatSync7(path);
|
|
8348
8461
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
@@ -8364,22 +8477,48 @@ function acquire(path, operation) {
|
|
|
8364
8477
|
`;
|
|
8365
8478
|
let descriptor;
|
|
8366
8479
|
try {
|
|
8367
|
-
descriptor =
|
|
8480
|
+
descriptor = openSync4(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
8368
8481
|
} catch (error) {
|
|
8369
|
-
if (error.code
|
|
8482
|
+
if (error.code !== "EEXIST")
|
|
8483
|
+
throw error;
|
|
8484
|
+
if (removeStaleLock(canonicalPath)) {
|
|
8485
|
+
descriptor = openSync4(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
8486
|
+
} else {
|
|
8370
8487
|
throw new Error(`Another Gvozd ${operation} is active or left a lock at ${canonicalPath}; inspect it manually and do not delete it while work may be running`);
|
|
8371
8488
|
}
|
|
8372
|
-
throw error;
|
|
8373
8489
|
}
|
|
8374
8490
|
const created = fstatSync(descriptor);
|
|
8375
8491
|
writeFileSync3(descriptor, nonce);
|
|
8376
8492
|
return { path: canonicalPath, descriptor, nonce, dev: created.dev, ino: created.ino };
|
|
8377
8493
|
}
|
|
8494
|
+
function removeStaleLock(path) {
|
|
8495
|
+
let stat;
|
|
8496
|
+
try {
|
|
8497
|
+
stat = lstatSync7(path);
|
|
8498
|
+
} catch {
|
|
8499
|
+
return false;
|
|
8500
|
+
}
|
|
8501
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
8502
|
+
return false;
|
|
8503
|
+
if (Date.now() - stat.mtimeMs < STALE_LOCK_AGE_MS)
|
|
8504
|
+
return false;
|
|
8505
|
+
if (lockOwnerAlive(path))
|
|
8506
|
+
return false;
|
|
8507
|
+
try {
|
|
8508
|
+
const current = lstatSync7(path);
|
|
8509
|
+
if (current.isSymbolicLink() || !current.isFile() || current.ino !== stat.ino || current.dev !== stat.dev)
|
|
8510
|
+
return false;
|
|
8511
|
+
unlinkSync3(path);
|
|
8512
|
+
return true;
|
|
8513
|
+
} catch {
|
|
8514
|
+
return false;
|
|
8515
|
+
}
|
|
8516
|
+
}
|
|
8378
8517
|
function release(lock) {
|
|
8379
|
-
|
|
8380
|
-
if (
|
|
8518
|
+
closeSync4(lock.descriptor);
|
|
8519
|
+
if (existsSync7(lock.path)) {
|
|
8381
8520
|
const current = lstatSync7(lock.path);
|
|
8382
|
-
if (current.isFile() && !current.isSymbolicLink() && current.dev === lock.dev && current.ino === lock.ino &&
|
|
8521
|
+
if (current.isFile() && !current.isSymbolicLink() && current.dev === lock.dev && current.ino === lock.ino && readFileSync8(lock.path, "utf8") === lock.nonce)
|
|
8383
8522
|
unlinkSync3(lock.path);
|
|
8384
8523
|
}
|
|
8385
8524
|
}
|
|
@@ -8402,14 +8541,14 @@ function withExclusiveFileLockSync(path, callback, operation = "operation") {
|
|
|
8402
8541
|
|
|
8403
8542
|
// src/cli/setup.ts
|
|
8404
8543
|
function assertVersion(version) {
|
|
8405
|
-
if (parseOpenCodeVersion(version)
|
|
8544
|
+
if (!satisfiesOpenCodeRange(parseOpenCodeVersion(version), SUPPORTED_OPENCODE_VERSION)) {
|
|
8406
8545
|
throw new Error(`Unsupported OpenCode version. Gvozd ${PACKAGE_VERSION} requires ${SUPPORTED_OPENCODE_VERSION}.`);
|
|
8407
8546
|
}
|
|
8408
8547
|
}
|
|
8409
8548
|
async function selectProfile(input, client, configRoot) {
|
|
8410
8549
|
const catalog = parseModels(await client.models());
|
|
8411
8550
|
const config = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8412
|
-
const hasGlobalConfig =
|
|
8551
|
+
const hasGlobalConfig = existsSync8(join10(configRoot, "gvozd", "config.jsonc"));
|
|
8413
8552
|
return chooseModelProfile({
|
|
8414
8553
|
catalog,
|
|
8415
8554
|
ui: input.ui,
|
|
@@ -8435,6 +8574,22 @@ async function registeredPackageSpecs(client, packageName, target) {
|
|
|
8435
8574
|
const specs = [...new Set(output.match(specPattern) ?? [])];
|
|
8436
8575
|
return specs.filter((spec) => spec !== target);
|
|
8437
8576
|
}
|
|
8577
|
+
async function awaitRegisteredPlugin(client, timeoutMs = 15000) {
|
|
8578
|
+
const target = PACKAGE_SPEC;
|
|
8579
|
+
const started = Date.now();
|
|
8580
|
+
for (;; ) {
|
|
8581
|
+
try {
|
|
8582
|
+
const output = await client.pluginList();
|
|
8583
|
+
const escapedName = PACKAGE_NAME.replace(/[/@]/g, "\\$&");
|
|
8584
|
+
const registered = new RegExp(`${escapedName}(?:@|\\s+)v?${PACKAGE_VERSION}(?=$|[^0-9.])`, "m").test(output);
|
|
8585
|
+
if (registered)
|
|
8586
|
+
return;
|
|
8587
|
+
} catch {}
|
|
8588
|
+
if (Date.now() - started >= timeoutMs)
|
|
8589
|
+
throw new Error(`OpenCode did not report ${target} within ${timeoutMs}ms after the service restart`);
|
|
8590
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8438
8593
|
async function runSetup(input) {
|
|
8439
8594
|
const client = await (input.findClient ?? (() => findOpenCode()))();
|
|
8440
8595
|
const paths = await client.debugPaths();
|
|
@@ -8445,8 +8600,8 @@ async function runSetup(input) {
|
|
|
8445
8600
|
const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
|
|
8446
8601
|
assertVersion(await client.version());
|
|
8447
8602
|
const before = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8448
|
-
const schemaSource =
|
|
8449
|
-
const packagedSchema =
|
|
8603
|
+
const schemaSource = join10(dirname6(before.sources[0]), "schema.json");
|
|
8604
|
+
const packagedSchema = readFileSync9(schemaSource, "utf8");
|
|
8450
8605
|
const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
|
|
8451
8606
|
const preview = writeManagedAgents({ configRoot, agents: before.agents, check: true });
|
|
8452
8607
|
const profile = await selectProfile(input, client, configRoot);
|
|
@@ -8454,17 +8609,17 @@ async function runSetup(input) {
|
|
|
8454
8609
|
return { status: "cancelled" };
|
|
8455
8610
|
input.output?.([
|
|
8456
8611
|
`Register ${PACKAGE_SPEC}`,
|
|
8457
|
-
`Write ${
|
|
8458
|
-
`Write managed agents in ${
|
|
8612
|
+
`Write ${join10(configRoot, "gvozd", "config.jsonc")}`,
|
|
8613
|
+
`Write managed agents in ${join10(configRoot, "agents")}`,
|
|
8459
8614
|
...preview.removed.length > 0 ? [`Remove ${preview.removed.length} stale or disabled managed agent(s)`] : []
|
|
8460
8615
|
].join(`
|
|
8461
8616
|
`));
|
|
8462
8617
|
if (!await confirm2(input, "Run setup?"))
|
|
8463
8618
|
return { status: "cancelled" };
|
|
8464
|
-
return withExclusiveFileLock(
|
|
8619
|
+
return withExclusiveFileLock(join10(configRoot, "gvozd", "setup.lock"), async () => {
|
|
8465
8620
|
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
8466
8621
|
throw new Error("OpenCode config root changed while setup awaited the lock");
|
|
8467
|
-
const lockedSchema =
|
|
8622
|
+
const lockedSchema = readFileSync9(schemaSource, "utf8");
|
|
8468
8623
|
const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
|
|
8469
8624
|
if (!sameSnapshot(previewSnapshot, snapshot))
|
|
8470
8625
|
throw new Error("Global Gvozd configuration changed while setup awaited confirmation; review and rerun setup");
|
|
@@ -8486,6 +8641,7 @@ async function runSetup(input) {
|
|
|
8486
8641
|
const configured = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8487
8642
|
writeManagedAgents({ configRoot, agents: configured.agents });
|
|
8488
8643
|
await client.serviceRestart();
|
|
8644
|
+
await awaitRegisteredPlugin(client);
|
|
8489
8645
|
const report = await runDoctor({ client, configRoot, runtimeConfigRoot, cwd: input.cwd });
|
|
8490
8646
|
return { status: "complete", report };
|
|
8491
8647
|
} catch (error) {
|
|
@@ -8504,16 +8660,16 @@ async function runConfigure(input) {
|
|
|
8504
8660
|
const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
|
|
8505
8661
|
assertVersion(await client.version());
|
|
8506
8662
|
const config = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8507
|
-
const schemaSource =
|
|
8508
|
-
const packagedSchema =
|
|
8663
|
+
const schemaSource = join10(dirname6(config.sources[0]), "schema.json");
|
|
8664
|
+
const packagedSchema = readFileSync9(schemaSource, "utf8");
|
|
8509
8665
|
const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
|
|
8510
8666
|
const profile = await selectProfile(input, client, configRoot);
|
|
8511
8667
|
if (!profile || !await confirm2(input, "Apply model configuration?"))
|
|
8512
8668
|
return { status: "cancelled" };
|
|
8513
|
-
return withExclusiveFileLock(
|
|
8669
|
+
return withExclusiveFileLock(join10(configRoot, "gvozd", "setup.lock"), async () => {
|
|
8514
8670
|
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
8515
8671
|
throw new Error("OpenCode config root changed while configuration awaited the lock");
|
|
8516
|
-
const lockedSchema =
|
|
8672
|
+
const lockedSchema = readFileSync9(schemaSource, "utf8");
|
|
8517
8673
|
const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
|
|
8518
8674
|
if (!sameSnapshot(previewSnapshot, snapshot))
|
|
8519
8675
|
throw new Error("Global Gvozd configuration changed while configuration awaited confirmation; review and rerun");
|
|
@@ -8537,13 +8693,13 @@ function setupExitCode(result) {
|
|
|
8537
8693
|
}
|
|
8538
8694
|
|
|
8539
8695
|
// src/sync.ts
|
|
8540
|
-
import { closeSync as
|
|
8696
|
+
import { closeSync as closeSync5, lstatSync as lstatSync8, mkdirSync as mkdirSync4, openSync as openSync5, readFileSync as readFileSync10, readdirSync as readdirSync5, realpathSync as realpathSync4, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8541
8697
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
8542
|
-
import { basename, dirname as dirname7, join as
|
|
8698
|
+
import { basename, dirname as dirname7, join as join11, relative as relative4 } from "node:path";
|
|
8543
8699
|
function renderPluginEntrypoint(config, destination) {
|
|
8544
8700
|
let moduleSpecifier = "agent-gvozd/server";
|
|
8545
8701
|
if (realpathSync4(config.packageRoot) === realpathSync4(config.projectRoot)) {
|
|
8546
|
-
moduleSpecifier = relative4(destination,
|
|
8702
|
+
moduleSpecifier = relative4(destination, join11(config.packageRoot, "src", "index")).replaceAll("\\", "/");
|
|
8547
8703
|
if (!moduleSpecifier.startsWith("."))
|
|
8548
8704
|
moduleSpecifier = `./${moduleSpecifier}`;
|
|
8549
8705
|
}
|
|
@@ -8588,7 +8744,7 @@ function stat2(path) {
|
|
|
8588
8744
|
function safeDirectory(root, segments, create) {
|
|
8589
8745
|
let current = realpathSync4(root);
|
|
8590
8746
|
for (const segment of segments) {
|
|
8591
|
-
current =
|
|
8747
|
+
current = join11(current, segment);
|
|
8592
8748
|
const currentStat = stat2(current);
|
|
8593
8749
|
if (currentStat) {
|
|
8594
8750
|
if (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {
|
|
@@ -8610,11 +8766,11 @@ function assertRegularFile(path) {
|
|
|
8610
8766
|
return true;
|
|
8611
8767
|
}
|
|
8612
8768
|
function createFile2(path, content) {
|
|
8613
|
-
const descriptor =
|
|
8769
|
+
const descriptor = openSync5(path, "wx", 384);
|
|
8614
8770
|
try {
|
|
8615
8771
|
writeFileSync4(descriptor, content);
|
|
8616
8772
|
} finally {
|
|
8617
|
-
|
|
8773
|
+
closeSync5(descriptor);
|
|
8618
8774
|
}
|
|
8619
8775
|
}
|
|
8620
8776
|
function replaceFile2(path, content) {
|
|
@@ -8632,7 +8788,7 @@ function replaceFile2(path, content) {
|
|
|
8632
8788
|
function planProjectTemplate(config, result, check) {
|
|
8633
8789
|
const directory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], !check);
|
|
8634
8790
|
const writes = [];
|
|
8635
|
-
const configPath =
|
|
8791
|
+
const configPath = join11(directory, "config.jsonc");
|
|
8636
8792
|
if (!assertRegularFile(configPath)) {
|
|
8637
8793
|
result.created.push(configPath);
|
|
8638
8794
|
writes.push({
|
|
@@ -8649,14 +8805,14 @@ function planProjectTemplate(config, result, check) {
|
|
|
8649
8805
|
`)
|
|
8650
8806
|
});
|
|
8651
8807
|
}
|
|
8652
|
-
const schemaSource =
|
|
8653
|
-
const schemaTarget =
|
|
8654
|
-
const schema =
|
|
8808
|
+
const schemaSource = join11(dirname7(config.sources[0]), "schema.json");
|
|
8809
|
+
const schemaTarget = join11(directory, "schema.json");
|
|
8810
|
+
const schema = readFileSync10(schemaSource, "utf8");
|
|
8655
8811
|
if (!assertRegularFile(schemaTarget)) {
|
|
8656
8812
|
result.created.push(schemaTarget);
|
|
8657
8813
|
writes.push({ target: schemaTarget, content: schema, replace: false });
|
|
8658
8814
|
} else {
|
|
8659
|
-
const current =
|
|
8815
|
+
const current = readFileSync10(schemaTarget, "utf8");
|
|
8660
8816
|
if (current !== schema) {
|
|
8661
8817
|
if (!hasGeneratedSchemaMarker(current) && !isEquivalentLegacySchema(current, schema)) {
|
|
8662
8818
|
throw new Error(`Refusing to overwrite an unmanaged project schema: ${schemaTarget}`);
|
|
@@ -8681,8 +8837,8 @@ function syncAgentsUnlocked(config, options) {
|
|
|
8681
8837
|
for (const entry of readdirSync5(destination, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
8682
8838
|
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
8683
8839
|
continue;
|
|
8684
|
-
const target =
|
|
8685
|
-
const current =
|
|
8840
|
+
const target = join11(destination, entry.name);
|
|
8841
|
+
const current = readFileSync10(target, "utf8");
|
|
8686
8842
|
if (!hasGeneratedAgentMarker(current))
|
|
8687
8843
|
continue;
|
|
8688
8844
|
result.removed.push(target);
|
|
@@ -8693,14 +8849,14 @@ function syncAgentsUnlocked(config, options) {
|
|
|
8693
8849
|
for (const [id, agent] of Object.entries(config.agents)) {
|
|
8694
8850
|
if (agent.disabled)
|
|
8695
8851
|
continue;
|
|
8696
|
-
const target =
|
|
8852
|
+
const target = join11(destination, `${id}.md`);
|
|
8697
8853
|
const content = renderAgent(agent);
|
|
8698
8854
|
if (!assertRegularFile(target)) {
|
|
8699
8855
|
result.created.push(target);
|
|
8700
8856
|
writes.push({ target, content, replace: false });
|
|
8701
8857
|
continue;
|
|
8702
8858
|
}
|
|
8703
|
-
const current =
|
|
8859
|
+
const current = readFileSync10(target, "utf8");
|
|
8704
8860
|
if (current === content) {
|
|
8705
8861
|
result.unchanged.push(target);
|
|
8706
8862
|
continue;
|
|
@@ -8712,11 +8868,11 @@ function syncAgentsUnlocked(config, options) {
|
|
|
8712
8868
|
options.onDiff?.(renderDiff(target, current, content));
|
|
8713
8869
|
writes.push({ target, content, replace: true, previous: current });
|
|
8714
8870
|
}
|
|
8715
|
-
const pluginTarget =
|
|
8871
|
+
const pluginTarget = join11(pluginDestination, "index.ts");
|
|
8716
8872
|
const pluginContent = renderPluginEntrypoint(config, pluginDestination);
|
|
8717
8873
|
if (!options.devPlugin) {
|
|
8718
8874
|
if (assertRegularFile(pluginTarget)) {
|
|
8719
|
-
const current =
|
|
8875
|
+
const current = readFileSync10(pluginTarget, "utf8");
|
|
8720
8876
|
if (hasGeneratedPluginMarker(current)) {
|
|
8721
8877
|
result.removed.push(pluginTarget);
|
|
8722
8878
|
removals.set(pluginTarget, current);
|
|
@@ -8727,7 +8883,7 @@ function syncAgentsUnlocked(config, options) {
|
|
|
8727
8883
|
result.created.push(pluginTarget);
|
|
8728
8884
|
pluginWrite = { target: pluginTarget, content: pluginContent, replace: false };
|
|
8729
8885
|
} else {
|
|
8730
|
-
const current =
|
|
8886
|
+
const current = readFileSync10(pluginTarget, "utf8");
|
|
8731
8887
|
if (current === pluginContent) {
|
|
8732
8888
|
result.unchanged.push(pluginTarget);
|
|
8733
8889
|
} else {
|
|
@@ -8746,7 +8902,7 @@ function syncAgentsUnlocked(config, options) {
|
|
|
8746
8902
|
if (!assertRegularFile(target)) {
|
|
8747
8903
|
throw new Error(`Refusing to remove a changed or unsafe generated file: ${target}`);
|
|
8748
8904
|
}
|
|
8749
|
-
const current =
|
|
8905
|
+
const current = readFileSync10(target, "utf8");
|
|
8750
8906
|
const generated = target.endsWith("index.ts") ? hasGeneratedPluginMarker(current) : hasGeneratedAgentMarker(current);
|
|
8751
8907
|
if (!generated || current !== removals.get(target)) {
|
|
8752
8908
|
throw new Error(`Refusing to remove a concurrently changed generated file: ${target}`);
|
|
@@ -8755,27 +8911,27 @@ function syncAgentsUnlocked(config, options) {
|
|
|
8755
8911
|
}
|
|
8756
8912
|
for (const write of writes) {
|
|
8757
8913
|
if (!write.replace) {
|
|
8758
|
-
createFile2(
|
|
8914
|
+
createFile2(join11(writableDestination, basename(write.target)), write.content);
|
|
8759
8915
|
continue;
|
|
8760
8916
|
}
|
|
8761
8917
|
if (!assertRegularFile(write.target)) {
|
|
8762
8918
|
throw new Error(`Refusing to replace a changed or unsafe agent file: ${write.target}`);
|
|
8763
8919
|
}
|
|
8764
|
-
const current =
|
|
8920
|
+
const current = readFileSync10(write.target, "utf8");
|
|
8765
8921
|
if (!hasGeneratedAgentMarker(current) || current !== write.previous) {
|
|
8766
8922
|
throw new Error(`Refusing to replace a concurrently changed agent file: ${write.target}`);
|
|
8767
8923
|
}
|
|
8768
8924
|
replaceFile2(write.target, write.content);
|
|
8769
8925
|
}
|
|
8770
8926
|
if (pluginWrite) {
|
|
8771
|
-
const target =
|
|
8927
|
+
const target = join11(writablePluginDestination, basename(pluginWrite.target));
|
|
8772
8928
|
if (!pluginWrite.replace) {
|
|
8773
8929
|
createFile2(target, pluginWrite.content);
|
|
8774
8930
|
} else {
|
|
8775
8931
|
if (!assertRegularFile(target)) {
|
|
8776
8932
|
throw new Error(`Refusing to replace a changed or unsafe plugin entrypoint: ${target}`);
|
|
8777
8933
|
}
|
|
8778
|
-
const current =
|
|
8934
|
+
const current = readFileSync10(target, "utf8");
|
|
8779
8935
|
if (!hasGeneratedPluginMarker(current) || current !== pluginWrite.previous) {
|
|
8780
8936
|
throw new Error(`Refusing to replace a concurrently changed plugin entrypoint: ${target}`);
|
|
8781
8937
|
}
|
|
@@ -8784,11 +8940,11 @@ function syncAgentsUnlocked(config, options) {
|
|
|
8784
8940
|
}
|
|
8785
8941
|
const templateDirectory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], true);
|
|
8786
8942
|
for (const write of templateWrites) {
|
|
8787
|
-
const target =
|
|
8943
|
+
const target = join11(templateDirectory, basename(write.target));
|
|
8788
8944
|
if (!write.replace)
|
|
8789
8945
|
createFile2(target, write.content);
|
|
8790
8946
|
else {
|
|
8791
|
-
if (!assertRegularFile(target) ||
|
|
8947
|
+
if (!assertRegularFile(target) || readFileSync10(target, "utf8") !== write.previous) {
|
|
8792
8948
|
throw new Error(`Refusing to replace a concurrently changed project schema: ${target}`);
|
|
8793
8949
|
}
|
|
8794
8950
|
replaceFile2(target, write.content);
|
|
@@ -8803,7 +8959,7 @@ function syncAgents(config, options = {}) {
|
|
|
8803
8959
|
if (options.check)
|
|
8804
8960
|
return syncAgentsUnlocked(config, options);
|
|
8805
8961
|
const root = realpathSync4(config.projectRoot);
|
|
8806
|
-
return withExclusiveFileLockSync(
|
|
8962
|
+
return withExclusiveFileLockSync(join11(root, ".agent-gvozd-sync.lock"), () => syncAgentsUnlocked(config, options), "sync");
|
|
8807
8963
|
}
|
|
8808
8964
|
function formatSyncResult(result, check) {
|
|
8809
8965
|
const lines = [check ? "agent-gvozd sync check" : "agent-gvozd sync complete"];
|
|
@@ -8838,6 +8994,8 @@ var HELP = [
|
|
|
8838
8994
|
"",
|
|
8839
8995
|
"Commands:",
|
|
8840
8996
|
" setup [--yes] Install or upgrade the global agent team",
|
|
8997
|
+
" agents [list] Show the resolved agent team",
|
|
8998
|
+
" agents disable <id> | enable <id> Toggle an agent in the global config",
|
|
8841
8999
|
" config [--yes] Configure model preferences",
|
|
8842
9000
|
" doctor [--json] Diagnose the global installation",
|
|
8843
9001
|
" sync [--check] [--dev-plugin] Maintain the project-local installation",
|
|
@@ -8922,6 +9080,32 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
|
|
|
8922
9080
|
io.stdout(formatSyncResult(result, check));
|
|
8923
9081
|
return check && result.created.length + result.updated.length + result.removed.length > 0 ? 1 : 0;
|
|
8924
9082
|
}
|
|
9083
|
+
if (command === "agents") {
|
|
9084
|
+
const [action, agentID] = rest;
|
|
9085
|
+
if (!action || action === "list") {
|
|
9086
|
+
if (rest.length > 1)
|
|
9087
|
+
return usage(io);
|
|
9088
|
+
const rows = listAgents(io.cwd());
|
|
9089
|
+
for (const row of rows) {
|
|
9090
|
+
io.stdout(`${row.disabled ? "✗" : " "} ${row.id.padEnd(12)} ${row.mode.padEnd(8)} ${row.lease.padEnd(12)} ${row.model}`);
|
|
9091
|
+
}
|
|
9092
|
+
return 0;
|
|
9093
|
+
}
|
|
9094
|
+
if (action === "disable" || action === "enable") {
|
|
9095
|
+
if (agentID === undefined || agentID.startsWith("--") || rest.length > 2)
|
|
9096
|
+
return usage(io);
|
|
9097
|
+
const configRoot = resolveOpenCodeConfigRoot();
|
|
9098
|
+
await withExclusiveFileLock(join12(configRoot, "gvozd", "setup.lock"), async () => {
|
|
9099
|
+
preflightGlobalConfig(configRoot);
|
|
9100
|
+
const snapshot2 = snapshot(join12(configRoot, "gvozd", "config.jsonc"));
|
|
9101
|
+
const next = setAgentDisabled(readGlobalConfig(configRoot), agentID, action === "disable");
|
|
9102
|
+
writeManagedGlobalFile(join12(configRoot, "gvozd", "config.jsonc"), next, snapshot2);
|
|
9103
|
+
});
|
|
9104
|
+
io.stdout(`${action}d ${agentID}; run gvozd sync and gvozd doctor to apply`);
|
|
9105
|
+
return 0;
|
|
9106
|
+
}
|
|
9107
|
+
return usage(io);
|
|
9108
|
+
}
|
|
8925
9109
|
if (command === "trust-project") {
|
|
8926
9110
|
const parsed = parseFlags(rest, []);
|
|
8927
9111
|
if (!parsed || parsed.positional.length > 1)
|
|
@@ -8936,8 +9120,13 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
|
|
|
8936
9120
|
}
|
|
8937
9121
|
}
|
|
8938
9122
|
var invokedPath = process.argv[1];
|
|
8939
|
-
|
|
8940
|
-
|
|
9123
|
+
try {
|
|
9124
|
+
if (invokedPath && realpathSync5(invokedPath) === realpathSync5(fileURLToPath2(import.meta.url))) {
|
|
9125
|
+
process.exitCode = await runCli(process.argv.slice(2));
|
|
9126
|
+
}
|
|
9127
|
+
} catch (error) {
|
|
9128
|
+
if (error.code !== "ENOENT")
|
|
9129
|
+
throw error;
|
|
8941
9130
|
}
|
|
8942
9131
|
export {
|
|
8943
9132
|
runCli
|