@nail00749/agent-gvozd 0.1.1 → 0.1.2
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 +172 -11
- package/defaults/agents/back-deep.jsonc +3 -1
- package/defaults/agents/back-fast.jsonc +2 -1
- package/defaults/agents/devops.jsonc +2 -0
- package/defaults/agents/front-deep.jsonc +3 -1
- package/defaults/agents/front-fast.jsonc +2 -1
- package/defaults/agents/master.jsonc +2 -1
- package/defaults/agents/planner.jsonc +2 -1
- package/defaults/agents/review-deep.jsonc +2 -1
- package/defaults/agents/review-fast.jsonc +2 -1
- package/defaults/default.jsonc +2 -1
- package/defaults/prompts/back-deep.md +1 -1
- package/defaults/prompts/devops.md +1 -1
- package/defaults/prompts/explorer.md +1 -1
- package/defaults/prompts/front-deep.md +1 -1
- package/defaults/prompts/master.md +2 -2
- package/dist/cli.js +991 -255
- package/dist/index.js +544 -177
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -987,13 +987,12 @@ ${b}
|
|
|
987
987
|
var i = `${styleText("gray", S_BAR)} `;
|
|
988
988
|
|
|
989
989
|
// src/cli.ts
|
|
990
|
-
import { realpathSync as
|
|
990
|
+
import { realpathSync as realpathSync5 } from "node:fs";
|
|
991
991
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
992
992
|
|
|
993
993
|
// src/config.ts
|
|
994
|
-
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
995
|
-
import {
|
|
996
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
994
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
995
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
997
996
|
import { fileURLToPath } from "node:url";
|
|
998
997
|
|
|
999
998
|
// node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
@@ -6466,12 +6465,180 @@ function refine(fn, _params = {}) {
|
|
|
6466
6465
|
function superRefine(fn, params) {
|
|
6467
6466
|
return _superRefine(fn, params);
|
|
6468
6467
|
}
|
|
6468
|
+
// src/config-root.ts
|
|
6469
|
+
import { homedir } from "node:os";
|
|
6470
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6471
|
+
var GVOZD_CONFIG_ROOT_ENV = "GVOZD_OPENCODE_CONFIG_ROOT";
|
|
6472
|
+
function absolute(path, label) {
|
|
6473
|
+
if (!isAbsolute(path))
|
|
6474
|
+
throw new Error(`${label} must be an absolute path`);
|
|
6475
|
+
return resolve(path);
|
|
6476
|
+
}
|
|
6477
|
+
function resolveOpenCodeConfigRootContract(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
|
|
6478
|
+
if (explicitRoot)
|
|
6479
|
+
return { path: absolute(explicitRoot, "OpenCode config root"), source: "explicit" };
|
|
6480
|
+
const override = env[GVOZD_CONFIG_ROOT_ENV];
|
|
6481
|
+
if (override) {
|
|
6482
|
+
return { path: absolute(override, GVOZD_CONFIG_ROOT_ENV), source: GVOZD_CONFIG_ROOT_ENV };
|
|
6483
|
+
}
|
|
6484
|
+
if (env.XDG_CONFIG_HOME) {
|
|
6485
|
+
return { path: join(absolute(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME"), "opencode"), source: "XDG_CONFIG_HOME" };
|
|
6486
|
+
}
|
|
6487
|
+
if (platform === "win32" && env.APPDATA) {
|
|
6488
|
+
return { path: join(env.APPDATA, "opencode"), source: "APPDATA" };
|
|
6489
|
+
}
|
|
6490
|
+
return { path: join(home, ".config", "opencode"), source: "platform-default" };
|
|
6491
|
+
}
|
|
6492
|
+
function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
|
|
6493
|
+
return resolveOpenCodeConfigRootContract(env, platform, home, explicitRoot).path;
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6496
|
+
// src/project-trust.ts
|
|
6497
|
+
import { createHash } from "node:crypto";
|
|
6498
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
6499
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
6500
|
+
var PROJECT_TRUST_ENV = "GVOZD_TRUST_PROJECT_CONFIG";
|
|
6501
|
+
function findRoot(start) {
|
|
6502
|
+
let current = resolve2(start);
|
|
6503
|
+
while (true) {
|
|
6504
|
+
if (existsSync(join2(current, ".git")))
|
|
6505
|
+
return realpathSync(current);
|
|
6506
|
+
const parent = dirname(current);
|
|
6507
|
+
if (parent === current)
|
|
6508
|
+
return realpathSync(resolve2(start));
|
|
6509
|
+
current = parent;
|
|
6510
|
+
}
|
|
6511
|
+
}
|
|
6512
|
+
function within(base, target, label) {
|
|
6513
|
+
const child = relative(base, target);
|
|
6514
|
+
if (child === "" || !child.startsWith("..") && !isAbsolute2(child))
|
|
6515
|
+
return;
|
|
6516
|
+
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
6517
|
+
}
|
|
6518
|
+
function rejectSymlinkComponents(base, target, label) {
|
|
6519
|
+
within(base, target, label);
|
|
6520
|
+
const segments = relative(base, target).split(/[\\/]/).filter(Boolean);
|
|
6521
|
+
let current = base;
|
|
6522
|
+
for (const segment of segments) {
|
|
6523
|
+
current = join2(current, segment);
|
|
6524
|
+
if (!existsSync(current))
|
|
6525
|
+
continue;
|
|
6526
|
+
if (lstatSync(current).isSymbolicLink())
|
|
6527
|
+
throw new Error(`${label} must not contain symlink components: ${current}`);
|
|
6528
|
+
}
|
|
6529
|
+
}
|
|
6530
|
+
function readJsonc(path) {
|
|
6531
|
+
const bytes = readFileSync(path);
|
|
6532
|
+
const errors = [];
|
|
6533
|
+
const value = parse2(bytes.toString("utf8"), errors, { allowTrailingComma: true, disallowComments: false });
|
|
6534
|
+
if (errors.length > 0 || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
6535
|
+
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
6536
|
+
throw new Error(`Invalid JSONC in ${path}${details ? `: ${details}` : ""}`);
|
|
6537
|
+
}
|
|
6538
|
+
return { value, bytes };
|
|
6539
|
+
}
|
|
6540
|
+
function promptFromPatch(patch) {
|
|
6541
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch))
|
|
6542
|
+
return;
|
|
6543
|
+
const prompt = patch.prompt;
|
|
6544
|
+
return typeof prompt === "string" && prompt.length > 0 ? prompt : undefined;
|
|
6545
|
+
}
|
|
6546
|
+
function collectProjectTrustInputs(projectDirectory) {
|
|
6547
|
+
const canonicalRoot = findRoot(projectDirectory);
|
|
6548
|
+
const layer = join2(canonicalRoot, "docs", ".gvozd");
|
|
6549
|
+
const rootPath = join2(layer, "config.jsonc");
|
|
6550
|
+
if (!existsSync(rootPath))
|
|
6551
|
+
return {
|
|
6552
|
+
canonicalRoot,
|
|
6553
|
+
inputs: [{ identity: "docs/.gvozd/config.jsonc:<missing>", bytes: Buffer.alloc(0) }]
|
|
6554
|
+
};
|
|
6555
|
+
rejectSymlinkComponents(canonicalRoot, rootPath, "Project config path");
|
|
6556
|
+
const rootStat = lstatSync(rootPath);
|
|
6557
|
+
if (!rootStat.isFile() || rootStat.isSymbolicLink())
|
|
6558
|
+
throw new Error(`Project config must be a regular non-symlink file: ${rootPath}`);
|
|
6559
|
+
const root = readJsonc(rootPath);
|
|
6560
|
+
const files = new Map([[relative(canonicalRoot, rootPath).replaceAll("\\", "/"), root.bytes]]);
|
|
6561
|
+
const prompts = [];
|
|
6562
|
+
const inlineAgents = root.value.agents;
|
|
6563
|
+
if (inlineAgents && typeof inlineAgents === "object" && !Array.isArray(inlineAgents)) {
|
|
6564
|
+
for (const patch of Object.values(inlineAgents)) {
|
|
6565
|
+
const prompt = promptFromPatch(patch);
|
|
6566
|
+
if (prompt)
|
|
6567
|
+
prompts.push({ source: rootPath, value: prompt });
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6570
|
+
const configuredDirectory = root.value.agentsDirectory;
|
|
6571
|
+
if (configuredDirectory !== undefined && (typeof configuredDirectory !== "string" || configuredDirectory.length === 0)) {
|
|
6572
|
+
throw new Error(`Invalid agentsDirectory in ${rootPath}`);
|
|
6573
|
+
}
|
|
6574
|
+
const agentsDirectory = resolve2(layer, configuredDirectory ?? "agents");
|
|
6575
|
+
within(layer, agentsDirectory, "agentsDirectory");
|
|
6576
|
+
if (existsSync(agentsDirectory)) {
|
|
6577
|
+
rejectSymlinkComponents(canonicalRoot, agentsDirectory, "agentsDirectory");
|
|
6578
|
+
const stat = lstatSync(agentsDirectory);
|
|
6579
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
6580
|
+
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
6581
|
+
within(realpathSync(layer), realpathSync(agentsDirectory), "agentsDirectory");
|
|
6582
|
+
for (const entry of readdirSync(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
6583
|
+
if (!entry.name.endsWith(".jsonc"))
|
|
6584
|
+
continue;
|
|
6585
|
+
const path = join2(agentsDirectory, entry.name);
|
|
6586
|
+
if (!entry.isFile())
|
|
6587
|
+
throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
|
|
6588
|
+
rejectSymlinkComponents(canonicalRoot, path, "Agent fragment path");
|
|
6589
|
+
const stat = lstatSync(path);
|
|
6590
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
6591
|
+
throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
|
|
6592
|
+
const fragment = readJsonc(path);
|
|
6593
|
+
files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), fragment.bytes);
|
|
6594
|
+
const prompt = promptFromPatch(fragment.value);
|
|
6595
|
+
if (prompt)
|
|
6596
|
+
prompts.push({ source: path, value: prompt });
|
|
6597
|
+
}
|
|
6598
|
+
}
|
|
6599
|
+
for (const prompt of prompts) {
|
|
6600
|
+
const path = resolve2(dirname(prompt.source), prompt.value);
|
|
6601
|
+
within(layer, path, "Agent prompt");
|
|
6602
|
+
rejectSymlinkComponents(canonicalRoot, path, "Agent prompt");
|
|
6603
|
+
if (!existsSync(path))
|
|
6604
|
+
throw new Error(`Agent prompt is missing: ${path}`);
|
|
6605
|
+
const stat = lstatSync(path);
|
|
6606
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
6607
|
+
throw new Error(`Agent prompt must be a regular non-symlink file: ${path}`);
|
|
6608
|
+
const canonical = realpathSync(path);
|
|
6609
|
+
within(realpathSync(layer), canonical, "Agent prompt");
|
|
6610
|
+
files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), readFileSync(path));
|
|
6611
|
+
}
|
|
6612
|
+
return {
|
|
6613
|
+
canonicalRoot,
|
|
6614
|
+
inputs: [...files].map(([identity, bytes]) => ({ identity, bytes })).sort((left, right) => left.identity.localeCompare(right.identity))
|
|
6615
|
+
};
|
|
6616
|
+
}
|
|
6617
|
+
function computeProjectTrustToken(projectDirectory) {
|
|
6618
|
+
const collected = collectProjectTrustInputs(projectDirectory);
|
|
6619
|
+
const hash = createHash("sha256");
|
|
6620
|
+
hash.update("agent-gvozd-project-trust-v1\x00");
|
|
6621
|
+
hash.update(collected.canonicalRoot);
|
|
6622
|
+
hash.update("\x00");
|
|
6623
|
+
for (const input of collected.inputs) {
|
|
6624
|
+
hash.update(String(Buffer.byteLength(input.identity)));
|
|
6625
|
+
hash.update(":");
|
|
6626
|
+
hash.update(input.identity);
|
|
6627
|
+
hash.update(":");
|
|
6628
|
+
hash.update(String(input.bytes.length));
|
|
6629
|
+
hash.update(":");
|
|
6630
|
+
hash.update(input.bytes);
|
|
6631
|
+
hash.update("\x00");
|
|
6632
|
+
}
|
|
6633
|
+
return `sha256:${hash.digest("hex")}`;
|
|
6634
|
+
}
|
|
6635
|
+
|
|
6469
6636
|
// src/config.ts
|
|
6470
6637
|
var permissionSchema = object({
|
|
6471
6638
|
action: string2().min(1),
|
|
6472
6639
|
resource: string2().min(1),
|
|
6473
6640
|
effect: _enum(["allow", "ask", "deny"])
|
|
6474
|
-
});
|
|
6641
|
+
}).strict();
|
|
6475
6642
|
var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
|
|
6476
6643
|
var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
|
|
6477
6644
|
var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
|
|
@@ -6485,12 +6652,13 @@ var agentPatchSchema = object({
|
|
|
6485
6652
|
permissions: array(permissionSchema).optional(),
|
|
6486
6653
|
fileLease: fileLeaseRoleSchema.optional(),
|
|
6487
6654
|
disabled: boolean2().optional()
|
|
6488
|
-
});
|
|
6655
|
+
}).strict();
|
|
6489
6656
|
var rootPatchSchema = object({
|
|
6657
|
+
$schema: string2().min(1).optional(),
|
|
6490
6658
|
defaultAgent: agentIdSchema.optional(),
|
|
6491
6659
|
agentsDirectory: string2().min(1).optional(),
|
|
6492
6660
|
agents: record(agentIdSchema, agentPatchSchema).optional()
|
|
6493
|
-
});
|
|
6661
|
+
}).strict();
|
|
6494
6662
|
var resolvedAgentSchema = agentPatchSchema.extend({
|
|
6495
6663
|
description: string2().min(1),
|
|
6496
6664
|
mode: _enum(["primary", "subagent", "all"]),
|
|
@@ -6502,9 +6670,18 @@ var resolvedAgentSchema = agentPatchSchema.extend({
|
|
|
6502
6670
|
fileLease: fileLeaseRoleSchema,
|
|
6503
6671
|
disabled: boolean2()
|
|
6504
6672
|
});
|
|
6505
|
-
|
|
6673
|
+
var SAFE_UNTRUSTED_AGENT_FIELDS = new Set(["description"]);
|
|
6674
|
+
function assertTrustedProjectPatch(patch, sourcePath, id, trusted, knownAgents) {
|
|
6675
|
+
if (trusted)
|
|
6676
|
+
return;
|
|
6677
|
+
const fields = Object.keys(patch).filter((field) => !SAFE_UNTRUSTED_AGENT_FIELDS.has(field));
|
|
6678
|
+
if (fields.length === 0 && knownAgents.has(id))
|
|
6679
|
+
return;
|
|
6680
|
+
throw new Error(`Untrusted project config ${sourcePath} cannot override ${fields.join(", ") || `unknown agent ${id}`}. ` + `Set ${PROJECT_TRUST_ENV} to the exact token returned by computeProjectTrustToken() after reviewing these files.`);
|
|
6681
|
+
}
|
|
6682
|
+
function readJsonc2(path) {
|
|
6506
6683
|
const errors = [];
|
|
6507
|
-
const value = parse2(
|
|
6684
|
+
const value = parse2(readFileSync2(path, "utf8"), errors, {
|
|
6508
6685
|
allowTrailingComma: true,
|
|
6509
6686
|
disallowComments: false
|
|
6510
6687
|
});
|
|
@@ -6515,45 +6692,62 @@ function readJsonc(path) {
|
|
|
6515
6692
|
return value;
|
|
6516
6693
|
}
|
|
6517
6694
|
function assertWithin(base, target, label) {
|
|
6518
|
-
const child =
|
|
6519
|
-
if (child === "" || !child.startsWith("..") && !
|
|
6695
|
+
const child = relative2(base, target);
|
|
6696
|
+
if (child === "" || !child.startsWith("..") && !isAbsolute3(child))
|
|
6520
6697
|
return;
|
|
6521
6698
|
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
6522
6699
|
}
|
|
6523
6700
|
function resolvePrompt(patch, sourcePath, layerDirectory) {
|
|
6524
6701
|
if (!patch.prompt)
|
|
6525
6702
|
return patch;
|
|
6526
|
-
const prompt =
|
|
6703
|
+
const prompt = resolve3(dirname2(sourcePath), patch.prompt);
|
|
6527
6704
|
assertWithin(layerDirectory, prompt, "Agent prompt");
|
|
6528
|
-
if (!
|
|
6705
|
+
if (!existsSync2(prompt))
|
|
6529
6706
|
throw new Error(`Agent prompt is missing: ${prompt}`);
|
|
6530
|
-
const
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
const
|
|
6536
|
-
|
|
6707
|
+
const promptStat = lstatSync2(prompt);
|
|
6708
|
+
if (promptStat.isSymbolicLink() || !promptStat.isFile())
|
|
6709
|
+
throw new Error(`Agent prompt must be a regular non-symlink file: ${prompt}`);
|
|
6710
|
+
const canonical = realpathSync2(prompt);
|
|
6711
|
+
assertWithin(realpathSync2(layerDirectory), canonical, "Agent prompt");
|
|
6712
|
+
const promptContent = readFileSync2(canonical, "utf8");
|
|
6713
|
+
return { ...patch, prompt: canonical, promptContent };
|
|
6714
|
+
}
|
|
6715
|
+
function loadLayer(directory, rootFileName, required, projectPolicy) {
|
|
6716
|
+
const rootPath = join3(directory, rootFileName);
|
|
6717
|
+
if (!existsSync2(rootPath)) {
|
|
6537
6718
|
if (required)
|
|
6538
6719
|
throw new Error(`Required config is missing: ${rootPath}`);
|
|
6539
6720
|
return { agents: {}, sources: [] };
|
|
6540
6721
|
}
|
|
6541
|
-
const root = rootPatchSchema.parse(
|
|
6722
|
+
const root = rootPatchSchema.parse(readJsonc2(rootPath));
|
|
6723
|
+
if (projectPolicy && !projectPolicy.trusted) {
|
|
6724
|
+
const restricted = ["defaultAgent", "agentsDirectory"].filter((field) => Object.prototype.hasOwnProperty.call(root, field));
|
|
6725
|
+
if (restricted.length > 0)
|
|
6726
|
+
throw new Error(`Untrusted project config ${rootPath} cannot override ${restricted.join(", ")}`);
|
|
6727
|
+
}
|
|
6542
6728
|
const agents = {};
|
|
6543
6729
|
for (const [id, patch] of Object.entries(root.agents ?? {})) {
|
|
6730
|
+
if (projectPolicy)
|
|
6731
|
+
assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6544
6732
|
agents[id] = resolvePrompt(patch, rootPath, directory);
|
|
6545
6733
|
}
|
|
6546
|
-
const agentsDirectory =
|
|
6734
|
+
const agentsDirectory = resolve3(directory, root.agentsDirectory ?? "agents");
|
|
6547
6735
|
assertWithin(directory, agentsDirectory, "agentsDirectory");
|
|
6548
|
-
if (
|
|
6549
|
-
|
|
6550
|
-
|
|
6736
|
+
if (existsSync2(agentsDirectory)) {
|
|
6737
|
+
const directoryStat = lstatSync2(agentsDirectory);
|
|
6738
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
6739
|
+
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
6740
|
+
}
|
|
6741
|
+
assertWithin(realpathSync2(directory), realpathSync2(agentsDirectory), "agentsDirectory");
|
|
6742
|
+
for (const entry of readdirSync2(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6551
6743
|
if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
|
|
6552
6744
|
continue;
|
|
6553
6745
|
const id = entry.name.slice(0, -".jsonc".length);
|
|
6554
6746
|
agentIdSchema.parse(id);
|
|
6555
|
-
const agentPath =
|
|
6556
|
-
const patch = agentPatchSchema.parse(
|
|
6747
|
+
const agentPath = join3(agentsDirectory, entry.name);
|
|
6748
|
+
const patch = agentPatchSchema.parse(readJsonc2(agentPath));
|
|
6749
|
+
if (projectPolicy)
|
|
6750
|
+
assertTrustedProjectPatch(patch, agentPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6557
6751
|
agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
|
|
6558
6752
|
}
|
|
6559
6753
|
}
|
|
@@ -6567,22 +6761,24 @@ function mergeAgent(base, override) {
|
|
|
6567
6761
|
return { ...base ?? {}, ...override };
|
|
6568
6762
|
}
|
|
6569
6763
|
function resolveAgentConfig(patch) {
|
|
6570
|
-
const parsed = agentPatchSchema.parse(patch);
|
|
6571
|
-
|
|
6764
|
+
const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
|
|
6765
|
+
const { promptContent, ...agentPatch } = parsed;
|
|
6766
|
+
const resolved = resolvedAgentSchema.parse({
|
|
6572
6767
|
skills: [],
|
|
6573
6768
|
mcp: [],
|
|
6574
6769
|
permissions: [],
|
|
6575
6770
|
fileLease: "readonly",
|
|
6576
6771
|
disabled: false,
|
|
6577
|
-
...
|
|
6772
|
+
...agentPatch
|
|
6578
6773
|
});
|
|
6774
|
+
return promptContent === undefined ? resolved : { ...resolved, promptContent };
|
|
6579
6775
|
}
|
|
6580
6776
|
function findPackageRoot() {
|
|
6581
|
-
let current =
|
|
6777
|
+
let current = dirname2(fileURLToPath(import.meta.url));
|
|
6582
6778
|
while (true) {
|
|
6583
|
-
if (
|
|
6779
|
+
if (existsSync2(join3(current, "defaults", "default.jsonc")))
|
|
6584
6780
|
return current;
|
|
6585
|
-
const parent =
|
|
6781
|
+
const parent = dirname2(current);
|
|
6586
6782
|
if (parent === current)
|
|
6587
6783
|
break;
|
|
6588
6784
|
current = parent;
|
|
@@ -6590,34 +6786,35 @@ function findPackageRoot() {
|
|
|
6590
6786
|
throw new Error("Unable to locate agent-gvozd package defaults");
|
|
6591
6787
|
}
|
|
6592
6788
|
function findProjectRoot(start) {
|
|
6593
|
-
let current =
|
|
6789
|
+
let current = resolve3(start);
|
|
6594
6790
|
while (true) {
|
|
6595
|
-
const git =
|
|
6596
|
-
if (
|
|
6791
|
+
const git = join3(current, ".git");
|
|
6792
|
+
if (existsSync2(git))
|
|
6597
6793
|
return current;
|
|
6598
|
-
const parent =
|
|
6794
|
+
const parent = dirname2(current);
|
|
6599
6795
|
if (parent === current)
|
|
6600
|
-
return
|
|
6796
|
+
return resolve3(start);
|
|
6601
6797
|
current = parent;
|
|
6602
6798
|
}
|
|
6603
6799
|
}
|
|
6604
|
-
function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir()) {
|
|
6605
|
-
if (env.XDG_CONFIG_HOME)
|
|
6606
|
-
return join(env.XDG_CONFIG_HOME, "opencode");
|
|
6607
|
-
if (platform === "win32" && env.APPDATA)
|
|
6608
|
-
return join(env.APPDATA, "opencode");
|
|
6609
|
-
return join(home, ".config", "opencode");
|
|
6610
|
-
}
|
|
6611
6800
|
function loadConfig(projectDirectory, options = {}) {
|
|
6612
6801
|
const projectRoot = findProjectRoot(projectDirectory);
|
|
6613
6802
|
const packageRoot = findPackageRoot();
|
|
6614
|
-
const projectConfigDirectory =
|
|
6615
|
-
const globalConfigDirectory =
|
|
6616
|
-
const
|
|
6617
|
-
loadLayer(
|
|
6618
|
-
loadLayer(globalConfigDirectory, "config.jsonc", false)
|
|
6619
|
-
...options.includeProject === false ? [] : [loadLayer(projectConfigDirectory, "config.jsonc", false)]
|
|
6803
|
+
const projectConfigDirectory = join3(projectRoot, "docs", ".gvozd");
|
|
6804
|
+
const globalConfigDirectory = join3(resolveOpenCodeConfigRoot(options.env, options.platform, options.home, options.configRoot), "gvozd");
|
|
6805
|
+
const baseLayers = [
|
|
6806
|
+
loadLayer(join3(packageRoot, "defaults"), "default.jsonc", true),
|
|
6807
|
+
loadLayer(globalConfigDirectory, "config.jsonc", false)
|
|
6620
6808
|
];
|
|
6809
|
+
const knownAgents = new Set(baseLayers.flatMap((layer) => Object.keys(layer.agents)));
|
|
6810
|
+
const includeProject = options.includeProject !== false;
|
|
6811
|
+
const suppliedToken = includeProject ? options.projectTrustToken ?? (options.env ?? process.env)[PROJECT_TRUST_ENV] : undefined;
|
|
6812
|
+
const trustProjectConfig = includeProject && suppliedToken !== undefined && suppliedToken === computeProjectTrustToken(projectRoot);
|
|
6813
|
+
const projectLayer = includeProject ? loadLayer(projectConfigDirectory, "config.jsonc", false, { trusted: trustProjectConfig, knownAgents }) : undefined;
|
|
6814
|
+
if (trustProjectConfig && suppliedToken !== computeProjectTrustToken(projectRoot)) {
|
|
6815
|
+
throw new Error("Project configuration changed while its trust token was being validated; review it and compute a new token");
|
|
6816
|
+
}
|
|
6817
|
+
const layers = [...baseLayers, ...projectLayer ? [projectLayer] : []];
|
|
6621
6818
|
let defaultAgent;
|
|
6622
6819
|
const agents = {};
|
|
6623
6820
|
for (const layer of layers) {
|
|
@@ -6630,7 +6827,12 @@ function loadConfig(projectDirectory, options = {}) {
|
|
|
6630
6827
|
throw new Error("defaultAgent is not configured");
|
|
6631
6828
|
const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
|
|
6632
6829
|
id,
|
|
6633
|
-
|
|
6830
|
+
(() => {
|
|
6831
|
+
const agent = resolveAgentConfig(patch);
|
|
6832
|
+
if (agent.promptContent === undefined)
|
|
6833
|
+
throw new Error(`Agent prompt snapshot is missing after configuration load: ${agent.prompt}`);
|
|
6834
|
+
return agent;
|
|
6835
|
+
})()
|
|
6634
6836
|
]));
|
|
6635
6837
|
const defaultConfig = resolvedAgents[defaultAgent];
|
|
6636
6838
|
if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
|
|
@@ -6648,15 +6850,79 @@ function loadConfig(projectDirectory, options = {}) {
|
|
|
6648
6850
|
}
|
|
6649
6851
|
|
|
6650
6852
|
// src/cli/doctor.ts
|
|
6651
|
-
import { lstatSync, readFileSync as readFileSync3 } from "node:fs";
|
|
6652
|
-
import { join as
|
|
6853
|
+
import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
|
|
6854
|
+
import { join as join4, resolve as resolve4 } from "node:path";
|
|
6653
6855
|
|
|
6654
|
-
// src/agent-
|
|
6655
|
-
|
|
6856
|
+
// src/agent-permissions.ts
|
|
6857
|
+
function normalizeMcpName(name) {
|
|
6858
|
+
return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
|
|
6859
|
+
}
|
|
6860
|
+
function buildAgentPermissions(agent, mcpServers) {
|
|
6861
|
+
const result = [
|
|
6862
|
+
...agent.permissions,
|
|
6863
|
+
{ action: "skill", resource: "*", effect: "deny" },
|
|
6864
|
+
...agent.skills.map((skill) => ({ action: "skill", resource: skill, effect: "allow" }))
|
|
6865
|
+
];
|
|
6866
|
+
for (const server of mcpServers) {
|
|
6867
|
+
const prefix = `${normalizeMcpName(server)}_`;
|
|
6868
|
+
result.push({
|
|
6869
|
+
action: `${prefix}*`,
|
|
6870
|
+
resource: "*",
|
|
6871
|
+
effect: agent.mcp.includes(server) ? "allow" : "deny"
|
|
6872
|
+
});
|
|
6873
|
+
result.push(...agent.permissions.filter((rule) => rule.action.startsWith(prefix)));
|
|
6874
|
+
}
|
|
6875
|
+
return result;
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
// src/release-metadata.ts
|
|
6879
|
+
var PACKAGE_NAME = "@nail00749/agent-gvozd";
|
|
6880
|
+
var PACKAGE_VERSION = "0.1.2";
|
|
6881
|
+
var PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
6882
|
+
var SUPPORTED_OPENCODE_VERSION = "0.0.0-beta-19425";
|
|
6883
|
+
var CONFIG_SCHEMA_VERSION = 1;
|
|
6656
6884
|
|
|
6657
6885
|
// src/constants.ts
|
|
6658
6886
|
var GENERATED_MARKER = "# Generated by agent-gvozd sync. Do not edit this file directly.";
|
|
6659
6887
|
var GENERATED_PLUGIN_MARKER = "// Generated by agent-gvozd sync. Do not edit this file directly.";
|
|
6888
|
+
function hasGeneratedAgentMarker(content) {
|
|
6889
|
+
return content.startsWith(`---
|
|
6890
|
+
${GENERATED_MARKER}
|
|
6891
|
+
`);
|
|
6892
|
+
}
|
|
6893
|
+
function hasGeneratedPluginMarker(content) {
|
|
6894
|
+
return content.startsWith(`${GENERATED_PLUGIN_MARKER}
|
|
6895
|
+
`);
|
|
6896
|
+
}
|
|
6897
|
+
function hasGeneratedSchemaMarker(content) {
|
|
6898
|
+
const errors = [];
|
|
6899
|
+
const value = parse2(content, errors, { allowTrailingComma: true, disallowComments: false });
|
|
6900
|
+
return errors.length === 0 && value !== null && typeof value === "object" && !Array.isArray(value) && value.$comment === GENERATED_PLUGIN_MARKER && value["x-agent-gvozd-schema-version"] === CONFIG_SCHEMA_VERSION;
|
|
6901
|
+
}
|
|
6902
|
+
function stable(value) {
|
|
6903
|
+
if (Array.isArray(value))
|
|
6904
|
+
return value.map(stable);
|
|
6905
|
+
if (!value || typeof value !== "object")
|
|
6906
|
+
return value;
|
|
6907
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stable(entry)]));
|
|
6908
|
+
}
|
|
6909
|
+
function isEquivalentLegacySchema(content, generated) {
|
|
6910
|
+
try {
|
|
6911
|
+
const previous = JSON.parse(content);
|
|
6912
|
+
const expected = JSON.parse(generated);
|
|
6913
|
+
if (!previous || Array.isArray(previous) || previous.$comment !== undefined)
|
|
6914
|
+
return false;
|
|
6915
|
+
if (previous["x-agent-gvozd-schema-version"] !== undefined && previous["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION)
|
|
6916
|
+
return false;
|
|
6917
|
+
delete previous.$comment;
|
|
6918
|
+
delete previous["x-agent-gvozd-schema-version"];
|
|
6919
|
+
delete expected.$comment;
|
|
6920
|
+
delete expected["x-agent-gvozd-schema-version"];
|
|
6921
|
+
return JSON.stringify(stable(previous)) === JSON.stringify(stable(expected));
|
|
6922
|
+
} catch {
|
|
6923
|
+
return false;
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6660
6926
|
|
|
6661
6927
|
// src/agent-generation.ts
|
|
6662
6928
|
function yamlString(value) {
|
|
@@ -6675,12 +6941,11 @@ function renderPermissions(rules) {
|
|
|
6675
6941
|
];
|
|
6676
6942
|
}
|
|
6677
6943
|
function renderAgent(agent) {
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
];
|
|
6944
|
+
if (agent.promptContent === undefined) {
|
|
6945
|
+
throw new Error(`Agent is missing its immutable prompt snapshot (${agent.prompt})`);
|
|
6946
|
+
}
|
|
6947
|
+
const prompt = agent.promptContent.trim();
|
|
6948
|
+
const permissions = buildAgentPermissions(agent, []);
|
|
6684
6949
|
return [
|
|
6685
6950
|
"---",
|
|
6686
6951
|
GENERATED_MARKER,
|
|
@@ -6695,9 +6960,15 @@ function renderAgent(agent) {
|
|
|
6695
6960
|
`);
|
|
6696
6961
|
}
|
|
6697
6962
|
|
|
6963
|
+
// src/runtime-events.ts
|
|
6964
|
+
function redactDiagnostic(error) {
|
|
6965
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6966
|
+
return message.replace(/-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi, "[redacted private key]").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+:[^\s/@]*@/gi, "$1[redacted]@").replace(/([?&](?:access_?token|auth(?:orization)?|api_?key|cookie|credential|password|secret|token)=)[^&#\s]*/gi, "$1[redacted]").replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b([A-Za-z0-9_]*(?:token|password|authorization|api_?key|secret|credential|cookie)[A-Za-z0-9_]*)\s*[:=]\s*["']?[^\s,;}"']+/gi, "$1=[redacted]").replace(/\s+/g, " ").slice(0, 300);
|
|
6967
|
+
}
|
|
6968
|
+
|
|
6698
6969
|
// src/cli/opencode.ts
|
|
6699
6970
|
import { spawn } from "node:child_process";
|
|
6700
|
-
import { isAbsolute as
|
|
6971
|
+
import { isAbsolute as isAbsolute4 } from "node:path";
|
|
6701
6972
|
var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
6702
6973
|
var DEFAULT_TIMEOUT_MS = 15000;
|
|
6703
6974
|
function appendBounded(current, chunk) {
|
|
@@ -6709,43 +6980,96 @@ function appendBounded(current, chunk) {
|
|
|
6709
6980
|
var defaultProcessRunner = {
|
|
6710
6981
|
run(executable, args, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
6711
6982
|
return new Promise((resolve, reject) => {
|
|
6983
|
+
const grouped = process.platform !== "win32";
|
|
6712
6984
|
const child = spawn(executable, [...args], {
|
|
6985
|
+
detached: grouped,
|
|
6713
6986
|
shell: false,
|
|
6714
6987
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6715
6988
|
});
|
|
6716
6989
|
let stdout = "";
|
|
6717
6990
|
let stderr = "";
|
|
6991
|
+
let settled = false;
|
|
6718
6992
|
let timedOut = false;
|
|
6719
6993
|
let forceKill;
|
|
6994
|
+
let hardDeadline;
|
|
6995
|
+
const terminate = (signal) => {
|
|
6996
|
+
try {
|
|
6997
|
+
if (grouped && child.pid)
|
|
6998
|
+
process.kill(-child.pid, signal);
|
|
6999
|
+
else
|
|
7000
|
+
child.kill(signal);
|
|
7001
|
+
} catch {
|
|
7002
|
+
try {
|
|
7003
|
+
child.kill(signal);
|
|
7004
|
+
} catch {}
|
|
7005
|
+
}
|
|
7006
|
+
};
|
|
7007
|
+
const onStdout = (chunk) => {
|
|
7008
|
+
stdout = appendBounded(stdout, chunk);
|
|
7009
|
+
};
|
|
7010
|
+
const onStderr = (chunk) => {
|
|
7011
|
+
stderr = appendBounded(stderr, chunk);
|
|
7012
|
+
};
|
|
7013
|
+
const timeoutError = () => {
|
|
7014
|
+
const error = new Error(`OpenCode command timed out after ${timeoutMs}ms`);
|
|
7015
|
+
error.code = "ETIMEDOUT";
|
|
7016
|
+
return error;
|
|
7017
|
+
};
|
|
7018
|
+
const clearTimers = () => {
|
|
7019
|
+
clearTimeout(timer);
|
|
7020
|
+
if (forceKill)
|
|
7021
|
+
clearTimeout(forceKill);
|
|
7022
|
+
if (hardDeadline)
|
|
7023
|
+
clearTimeout(hardDeadline);
|
|
7024
|
+
};
|
|
7025
|
+
const settleTimeout = (destroy) => {
|
|
7026
|
+
if (settled)
|
|
7027
|
+
return;
|
|
7028
|
+
settled = true;
|
|
7029
|
+
clearTimers();
|
|
7030
|
+
if (destroy) {
|
|
7031
|
+
child.stdout.off("data", onStdout);
|
|
7032
|
+
child.stderr.off("data", onStderr);
|
|
7033
|
+
child.stdout.destroy();
|
|
7034
|
+
child.stderr.destroy();
|
|
7035
|
+
}
|
|
7036
|
+
reject(timeoutError());
|
|
7037
|
+
};
|
|
6720
7038
|
const timer = setTimeout(() => {
|
|
7039
|
+
if (settled)
|
|
7040
|
+
return;
|
|
6721
7041
|
timedOut = true;
|
|
6722
|
-
|
|
6723
|
-
forceKill = setTimeout(() =>
|
|
7042
|
+
terminate("SIGTERM");
|
|
7043
|
+
forceKill = setTimeout(() => terminate("SIGKILL"), 500);
|
|
6724
7044
|
forceKill.unref();
|
|
7045
|
+
hardDeadline = setTimeout(() => settleTimeout(true), 1750);
|
|
7046
|
+
hardDeadline.unref();
|
|
6725
7047
|
}, timeoutMs);
|
|
6726
7048
|
timer.unref();
|
|
6727
|
-
child.stdout.on("data",
|
|
6728
|
-
|
|
6729
|
-
});
|
|
6730
|
-
child.stderr.on("data", (chunk) => {
|
|
6731
|
-
stderr = appendBounded(stderr, chunk);
|
|
6732
|
-
});
|
|
7049
|
+
child.stdout.on("data", onStdout);
|
|
7050
|
+
child.stderr.on("data", onStderr);
|
|
6733
7051
|
child.once("error", (error) => {
|
|
6734
|
-
|
|
6735
|
-
if (
|
|
6736
|
-
|
|
7052
|
+
clearTimers();
|
|
7053
|
+
if (settled)
|
|
7054
|
+
return;
|
|
7055
|
+
settled = true;
|
|
7056
|
+
if (timedOut) {
|
|
7057
|
+
child.stdout.off("data", onStdout);
|
|
7058
|
+
child.stderr.off("data", onStderr);
|
|
7059
|
+
child.stdout.destroy();
|
|
7060
|
+
child.stderr.destroy();
|
|
7061
|
+
}
|
|
6737
7062
|
reject(error);
|
|
6738
7063
|
});
|
|
6739
7064
|
child.once("close", (code) => {
|
|
6740
|
-
|
|
6741
|
-
if (
|
|
6742
|
-
|
|
7065
|
+
clearTimers();
|
|
7066
|
+
if (settled)
|
|
7067
|
+
return;
|
|
6743
7068
|
if (timedOut) {
|
|
6744
|
-
|
|
6745
|
-
error.code = "ETIMEDOUT";
|
|
6746
|
-
reject(error);
|
|
7069
|
+
settleTimeout(false);
|
|
6747
7070
|
return;
|
|
6748
7071
|
}
|
|
7072
|
+
settled = true;
|
|
6749
7073
|
resolve({ code: code ?? 1, stdout, stderr });
|
|
6750
7074
|
});
|
|
6751
7075
|
});
|
|
@@ -6757,8 +7081,8 @@ function bounded(value) {
|
|
|
6757
7081
|
async function checked(runner, executable, args, timeoutMs) {
|
|
6758
7082
|
const result = await runner.run(executable, args, timeoutMs);
|
|
6759
7083
|
if (result.code !== 0) {
|
|
6760
|
-
const detail = bounded(result.stderr).trim();
|
|
6761
|
-
throw new Error(
|
|
7084
|
+
const detail = redactDiagnostic(bounded(result.stderr).trim());
|
|
7085
|
+
throw new Error(`OpenCode command exited ${result.code}${detail ? `: ${detail}` : ""}`);
|
|
6762
7086
|
}
|
|
6763
7087
|
return bounded(result.stdout);
|
|
6764
7088
|
}
|
|
@@ -6770,7 +7094,7 @@ function parseDebugPaths(output) {
|
|
|
6770
7094
|
continue;
|
|
6771
7095
|
paths[match[1]] = match[2];
|
|
6772
7096
|
}
|
|
6773
|
-
if (!paths.config || !
|
|
7097
|
+
if (!paths.config || !isAbsolute4(paths.config)) {
|
|
6774
7098
|
throw new Error("OpenCode did not report an absolute config path");
|
|
6775
7099
|
}
|
|
6776
7100
|
return paths;
|
|
@@ -6875,10 +7199,6 @@ function manualProfile(provider, fast, deep, catalog) {
|
|
|
6875
7199
|
|
|
6876
7200
|
// src/cli/doctor.ts
|
|
6877
7201
|
var SETUP_COMMAND = "gvozd setup";
|
|
6878
|
-
function redact(value) {
|
|
6879
|
-
const message = value instanceof Error ? value.message : String(value);
|
|
6880
|
-
return message.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b([A-Za-z0-9_]*(?:token|password|authorization|api_?key|secret|credential|cookie)[A-Za-z0-9_]*)\s*[:=]\s*["']?[^\s,;}"']+/gi, "$1=[redacted]").replace(/\s+/g, " ").slice(0, 300);
|
|
6881
|
-
}
|
|
6882
7202
|
function aggregate(checks) {
|
|
6883
7203
|
if (checks.some((check) => check.status === "fail"))
|
|
6884
7204
|
return "fail";
|
|
@@ -6888,7 +7208,7 @@ function aggregate(checks) {
|
|
|
6888
7208
|
}
|
|
6889
7209
|
function safeFile(path) {
|
|
6890
7210
|
try {
|
|
6891
|
-
const stat =
|
|
7211
|
+
const stat = lstatSync3(path);
|
|
6892
7212
|
return stat.isFile() && !stat.isSymbolicLink();
|
|
6893
7213
|
} catch {
|
|
6894
7214
|
return false;
|
|
@@ -6900,8 +7220,10 @@ function escapeRegExp(value) {
|
|
|
6900
7220
|
function hasAgentIdentifier(output, id) {
|
|
6901
7221
|
return new RegExp(`(?:^|[^A-Za-z0-9_-])${escapeRegExp(id)}(?=$|[^A-Za-z0-9_-])`, "m").test(output);
|
|
6902
7222
|
}
|
|
6903
|
-
function
|
|
6904
|
-
|
|
7223
|
+
function hasInstalledPluginVersion(output, name, version) {
|
|
7224
|
+
const boundary = `[\\s"'|│,}\\]]`;
|
|
7225
|
+
const pattern = `(?:^|${boundary})${escapeRegExp(name)}(?:@|\\s+)v?${escapeRegExp(version)}(?=$|${boundary})`;
|
|
7226
|
+
return new RegExp(pattern, "m").test(output);
|
|
6905
7227
|
}
|
|
6906
7228
|
function checkGlobalFiles(config, configRoot) {
|
|
6907
7229
|
const missing = [];
|
|
@@ -6909,27 +7231,43 @@ function checkGlobalFiles(config, configRoot) {
|
|
|
6909
7231
|
for (const [id, agent] of Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
6910
7232
|
if (agent.disabled)
|
|
6911
7233
|
continue;
|
|
6912
|
-
const path =
|
|
7234
|
+
const path = join4(configRoot, "agents", `${id}.md`);
|
|
6913
7235
|
if (!safeFile(path)) {
|
|
6914
7236
|
missing.push(id);
|
|
6915
7237
|
continue;
|
|
6916
7238
|
}
|
|
6917
7239
|
const content = readFileSync3(path, "utf8");
|
|
6918
|
-
if (!content
|
|
7240
|
+
if (!hasGeneratedAgentMarker(content) || content !== renderAgent(agent))
|
|
6919
7241
|
stale.push(id);
|
|
6920
7242
|
}
|
|
6921
|
-
|
|
6922
|
-
|
|
7243
|
+
const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
|
|
7244
|
+
const orphans = [];
|
|
7245
|
+
const directory = join4(configRoot, "agents");
|
|
7246
|
+
if (existsSync3(directory) && lstatSync3(directory).isDirectory() && !lstatSync3(directory).isSymbolicLink()) {
|
|
7247
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
7248
|
+
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
7249
|
+
continue;
|
|
7250
|
+
const content = readFileSync3(join4(directory, entry.name), "utf8");
|
|
7251
|
+
if (hasGeneratedAgentMarker(content))
|
|
7252
|
+
orphans.push(entry.name.slice(0, -3));
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
if (missing.length + stale.length + orphans.length === 0) {
|
|
7256
|
+
return { id: "global-agents", status: "pass", summary: `${enabled.size} managed global agents are current` };
|
|
6923
7257
|
}
|
|
6924
|
-
const details = [
|
|
7258
|
+
const details = [
|
|
7259
|
+
missing.length ? `missing: ${missing.join(", ")}` : "",
|
|
7260
|
+
stale.length ? `unmanaged or stale: ${stale.join(", ")}` : "",
|
|
7261
|
+
orphans.length ? `orphan managed agents: ${orphans.join(", ")}` : ""
|
|
7262
|
+
].filter(Boolean).join("; ");
|
|
6925
7263
|
return { id: "global-agents", status: "fail", summary: details, remediation: SETUP_COMMAND };
|
|
6926
7264
|
}
|
|
6927
7265
|
function checkLegacy(config) {
|
|
6928
7266
|
const duplicates = [];
|
|
6929
|
-
const plugin =
|
|
7267
|
+
const plugin = join4(config.projectRoot, ".opencode", "plugins", "agent-gvozd", "index.ts");
|
|
6930
7268
|
if (safeFile(plugin))
|
|
6931
7269
|
duplicates.push("local plugin");
|
|
6932
|
-
const agents = Object.keys(config.agents).filter((id) => safeFile(
|
|
7270
|
+
const agents = Object.keys(config.agents).filter((id) => safeFile(join4(config.projectRoot, ".opencode", "agents", `${id}.md`)));
|
|
6933
7271
|
if (agents.length > 0)
|
|
6934
7272
|
duplicates.push(`${agents.length} local agents`);
|
|
6935
7273
|
if (duplicates.length === 0)
|
|
@@ -6942,58 +7280,59 @@ function checkLegacy(config) {
|
|
|
6942
7280
|
};
|
|
6943
7281
|
}
|
|
6944
7282
|
async function runDoctor(input) {
|
|
6945
|
-
const packageName = input.packageName ??
|
|
6946
|
-
const packageVersion = input.packageVersion ??
|
|
6947
|
-
const supportedVersion = input.supportedOpenCodeVersion ??
|
|
7283
|
+
const packageName = input.packageName ?? PACKAGE_NAME;
|
|
7284
|
+
const packageVersion = input.packageVersion ?? PACKAGE_VERSION;
|
|
7285
|
+
const supportedVersion = input.supportedOpenCodeVersion ?? SUPPORTED_OPENCODE_VERSION;
|
|
6948
7286
|
const checks = [];
|
|
6949
7287
|
try {
|
|
6950
7288
|
const version = await input.client.version();
|
|
6951
|
-
checks.push(parseOpenCodeVersion(version) === supportedVersion ? { id: "opencode-version", status: "pass", summary: `OpenCode ${supportedVersion} is available` } : { id: "opencode-version", status: "fail", summary: `unsupported OpenCode version: ${
|
|
7289
|
+
checks.push(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}` });
|
|
6952
7290
|
} catch (error) {
|
|
6953
|
-
checks.push({ id: "opencode-version", status: "fail", summary: `OpenCode version check failed: ${
|
|
7291
|
+
checks.push({ id: "opencode-version", status: "fail", summary: `OpenCode version check failed: ${redactDiagnostic(error)}`, remediation: `Install OpenCode ${supportedVersion}` });
|
|
6954
7292
|
}
|
|
6955
7293
|
try {
|
|
6956
7294
|
await input.client.serviceStatus();
|
|
6957
7295
|
checks.push({ id: "service", status: "pass", summary: "OpenCode service is reachable" });
|
|
6958
7296
|
} catch (error) {
|
|
6959
|
-
checks.push({ id: "service", status: "fail", summary: `OpenCode service is unavailable: ${
|
|
7297
|
+
checks.push({ id: "service", status: "fail", summary: `OpenCode service is unavailable: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} service restart` });
|
|
6960
7298
|
}
|
|
6961
7299
|
try {
|
|
6962
7300
|
const output = await input.client.pluginList();
|
|
6963
|
-
checks.push(
|
|
7301
|
+
checks.push(hasInstalledPluginVersion(output, packageName, packageVersion) ? { id: "plugin", status: "pass", summary: `${packageName} ${packageVersion} is registered` } : { id: "plugin", status: "fail", summary: `${packageName} ${packageVersion} is not registered`, remediation: SETUP_COMMAND });
|
|
6964
7302
|
} catch (error) {
|
|
6965
|
-
checks.push({ id: "plugin", status: "fail", summary: `plugin list failed: ${
|
|
7303
|
+
checks.push({ id: "plugin", status: "fail", summary: `plugin list failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
|
|
6966
7304
|
}
|
|
6967
7305
|
try {
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
checks.push(unhealthy ? { id: "plugin-check", status: "fail", summary: "OpenCode reports an unhealthy Gvozd plugin", remediation: SETUP_COMMAND } : { id: "plugin-check", status: "pass", summary: "Gvozd plugin check passed" });
|
|
7306
|
+
await input.client.pluginCheck(PACKAGE_SPEC);
|
|
7307
|
+
checks.push({ id: "plugin-check", status: "pass", summary: "Gvozd plugin check passed" });
|
|
6971
7308
|
} catch (error) {
|
|
6972
|
-
checks.push({ id: "plugin-check", status: "fail", summary: `plugin check failed: ${
|
|
7309
|
+
checks.push({ id: "plugin-check", status: "fail", summary: `plugin check failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
|
|
6973
7310
|
}
|
|
6974
7311
|
try {
|
|
6975
7312
|
const paths = await input.client.debugPaths();
|
|
6976
|
-
|
|
7313
|
+
const reported = resolve4(paths.config);
|
|
7314
|
+
const expected = resolve4(input.runtimeConfigRoot ?? input.configRoot);
|
|
7315
|
+
checks.push(reported === resolve4(input.configRoot) && reported === expected ? { id: "config-root", status: "pass", summary: "runtime and CLI config roots match" } : { id: "config-root", status: "fail", summary: "OpenCode debug path and independently resolved runtime config root differ", remediation: `${input.client.executable} debug paths; set GVOZD_OPENCODE_CONFIG_ROOT to the reported config path before starting OpenCode` });
|
|
6977
7316
|
} catch (error) {
|
|
6978
|
-
checks.push({ id: "config-root", status: "fail", summary: `config path check failed: ${
|
|
7317
|
+
checks.push({ id: "config-root", status: "fail", summary: `config path check failed: ${redactDiagnostic(error)}` });
|
|
6979
7318
|
}
|
|
6980
7319
|
let config;
|
|
6981
7320
|
let globalConfig;
|
|
6982
|
-
const configPath =
|
|
6983
|
-
const schemaPath =
|
|
7321
|
+
const configPath = join4(input.configRoot, "gvozd", "config.jsonc");
|
|
7322
|
+
const schemaPath = join4(input.configRoot, "gvozd", "schema.json");
|
|
6984
7323
|
try {
|
|
6985
7324
|
if (!safeFile(configPath) || !safeFile(schemaPath))
|
|
6986
7325
|
throw new Error("global config.jsonc or schema.json is missing");
|
|
6987
7326
|
const schemaErrors = [];
|
|
6988
7327
|
const schema = parse2(readFileSync3(schemaPath, "utf8"), schemaErrors);
|
|
6989
|
-
if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !==
|
|
7328
|
+
if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION || schema?.$comment !== GENERATED_PLUGIN_MARKER) {
|
|
6990
7329
|
throw new Error("global schema is invalid, incompatible, or unmanaged");
|
|
6991
7330
|
}
|
|
6992
7331
|
globalConfig = loadConfig(input.cwd, { configRoot: input.configRoot, includeProject: false });
|
|
6993
7332
|
config = loadConfig(input.cwd, { configRoot: input.configRoot });
|
|
6994
7333
|
checks.push({ id: "config", status: "pass", summary: "global Gvozd config and schema are valid" });
|
|
6995
7334
|
} catch (error) {
|
|
6996
|
-
checks.push({ id: "config", status: "fail", summary: `global config check failed: ${
|
|
7335
|
+
checks.push({ id: "config", status: "fail", summary: `global config check failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
|
|
6997
7336
|
}
|
|
6998
7337
|
let catalog = parseModels([]);
|
|
6999
7338
|
try {
|
|
@@ -7003,12 +7342,14 @@ async function runDoctor(input) {
|
|
|
7003
7342
|
if (!config) {
|
|
7004
7343
|
checks.push({ id: "models", status: "fail", summary: "configured models cannot be validated without a valid config", remediation: SETUP_COMMAND });
|
|
7005
7344
|
} else {
|
|
7006
|
-
const
|
|
7007
|
-
const
|
|
7008
|
-
|
|
7345
|
+
const available = new Set(catalog.models);
|
|
7346
|
+
const enabled = Object.entries(config.agents).filter(([, agent]) => !agent.disabled);
|
|
7347
|
+
const unavailableAgents = enabled.filter(([, agent]) => !agent.models.some((model) => available.has(model))).map(([id]) => id);
|
|
7348
|
+
const missingFallbacks = [...new Set(enabled.flatMap(([, agent]) => agent.models.filter((model) => !available.has(model))))].sort();
|
|
7349
|
+
checks.push(unavailableAgents.length > 0 ? { id: "models", status: "fail", summary: `no configured model is available for: ${unavailableAgents.join(", ")}`, remediation: "gvozd config" } : missingFallbacks.length > 0 ? { id: "models", status: "warn", summary: `primary coverage is available; unavailable fallback models: ${missingFallbacks.join(", ")}`, remediation: "gvozd config" } : { id: "models", status: "pass", summary: `${catalog.models.length} available models cover the Gvozd profile` });
|
|
7009
7350
|
}
|
|
7010
7351
|
} catch (error) {
|
|
7011
|
-
checks.push({ id: "models", status: "fail", summary: `model catalog check failed: ${
|
|
7352
|
+
checks.push({ id: "models", status: "fail", summary: `model catalog check failed: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} auth` });
|
|
7012
7353
|
}
|
|
7013
7354
|
if (globalConfig)
|
|
7014
7355
|
checks.push(checkGlobalFiles(globalConfig, input.configRoot));
|
|
@@ -7019,7 +7360,7 @@ async function runDoctor(input) {
|
|
|
7019
7360
|
const missing = Object.entries(config?.agents ?? {}).filter(([, agent]) => !agent.disabled).map(([id]) => id).filter((id) => !hasAgentIdentifier(output, id));
|
|
7020
7361
|
checks.push(missing.length === 0 && config ? { id: "runtime-agents", status: "pass", summary: "all enabled Gvozd agents are visible to OpenCode" } : { id: "runtime-agents", status: "fail", summary: `runtime agents are missing: ${missing.join(", ") || "config unavailable"}`, remediation: `${input.client.executable} service restart` });
|
|
7021
7362
|
} catch (error) {
|
|
7022
|
-
checks.push({ id: "runtime-agents", status: "fail", summary: `runtime agent check failed: ${
|
|
7363
|
+
checks.push({ id: "runtime-agents", status: "fail", summary: `runtime agent check failed: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} service restart` });
|
|
7023
7364
|
}
|
|
7024
7365
|
checks.push(config ? checkLegacy(config) : { id: "legacy-local", status: "warn", summary: "legacy duplicates could not be checked" });
|
|
7025
7366
|
return { schemaVersion: 1, status: aggregate(checks), checks };
|
|
@@ -7044,20 +7385,84 @@ function doctorOperationalFailure(error) {
|
|
|
7044
7385
|
const checks = [{
|
|
7045
7386
|
id: "opencode-discovery",
|
|
7046
7387
|
status: "fail",
|
|
7047
|
-
summary: `OpenCode discovery failed: ${
|
|
7048
|
-
remediation:
|
|
7388
|
+
summary: `OpenCode discovery failed: ${redactDiagnostic(error)}`,
|
|
7389
|
+
remediation: `Install OpenCode ${SUPPORTED_OPENCODE_VERSION} and run gvozd doctor again`
|
|
7049
7390
|
}];
|
|
7050
7391
|
return { schemaVersion: 1, status: "fail", checks };
|
|
7051
7392
|
}
|
|
7052
7393
|
|
|
7053
7394
|
// src/cli/setup.ts
|
|
7054
|
-
import { existsSync as
|
|
7055
|
-
import { dirname as
|
|
7395
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
7396
|
+
import { dirname as dirname6, join as join8 } from "node:path";
|
|
7056
7397
|
|
|
7057
7398
|
// src/cli/config-store.ts
|
|
7058
|
-
import { closeSync, existsSync as
|
|
7399
|
+
import { accessSync, closeSync, constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync5, mkdirSync, openSync, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7059
7400
|
import { randomUUID } from "node:crypto";
|
|
7060
|
-
import { dirname as
|
|
7401
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
7402
|
+
|
|
7403
|
+
// src/secure-path.ts
|
|
7404
|
+
import { lstatSync as lstatSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
7405
|
+
import { isAbsolute as isAbsolute5, join as join5, parse as parse6, relative as relative3, resolve as resolve5, sep } from "node:path";
|
|
7406
|
+
function secureCanonicalPath(target, label = "Path") {
|
|
7407
|
+
if (!isAbsolute5(target))
|
|
7408
|
+
throw new Error(`${label} must be absolute: ${target}`);
|
|
7409
|
+
const lexical = resolve5(target);
|
|
7410
|
+
const root = parse6(lexical).root;
|
|
7411
|
+
const components = relative3(root, lexical).split(sep).filter(Boolean);
|
|
7412
|
+
let current = root;
|
|
7413
|
+
let nearestExisting = root;
|
|
7414
|
+
const missingSuffix = [];
|
|
7415
|
+
let missing = false;
|
|
7416
|
+
const rootStat = lstatSync4(root);
|
|
7417
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
7418
|
+
throw new Error(`${label} has an unsafe filesystem root: ${root}`);
|
|
7419
|
+
const existingDirectories = [{ path: root, stat: rootStat }];
|
|
7420
|
+
for (const component of components) {
|
|
7421
|
+
current = join5(current, component);
|
|
7422
|
+
if (missing) {
|
|
7423
|
+
missingSuffix.push(component);
|
|
7424
|
+
continue;
|
|
7425
|
+
}
|
|
7426
|
+
let stat;
|
|
7427
|
+
try {
|
|
7428
|
+
stat = lstatSync4(current);
|
|
7429
|
+
} catch (error) {
|
|
7430
|
+
if (error.code !== "ENOENT")
|
|
7431
|
+
throw error;
|
|
7432
|
+
missing = true;
|
|
7433
|
+
missingSuffix.push(component);
|
|
7434
|
+
continue;
|
|
7435
|
+
}
|
|
7436
|
+
if (stat.isSymbolicLink())
|
|
7437
|
+
throw new Error(`${label} contains a symbolic-link component: ${current}`);
|
|
7438
|
+
if (current !== lexical && !stat.isDirectory()) {
|
|
7439
|
+
throw new Error(`${label} contains a non-directory ancestor: ${current}`);
|
|
7440
|
+
}
|
|
7441
|
+
if (stat.isDirectory())
|
|
7442
|
+
existingDirectories.push({ path: current, stat });
|
|
7443
|
+
nearestExisting = current;
|
|
7444
|
+
}
|
|
7445
|
+
if (process.platform !== "win32") {
|
|
7446
|
+
const uid = process.getuid?.();
|
|
7447
|
+
for (let index = 0;index < existingDirectories.length; index++) {
|
|
7448
|
+
const entry = existingDirectories[index];
|
|
7449
|
+
if ((entry.stat.mode & 18) === 0)
|
|
7450
|
+
continue;
|
|
7451
|
+
const sticky = (entry.stat.mode & 512) !== 0;
|
|
7452
|
+
const hasPrivateBoundary = sticky && uid !== undefined && existingDirectories.slice(index + 1).some((candidate) => candidate.stat.uid === uid && (candidate.stat.mode & 18) === 0);
|
|
7453
|
+
if (!hasPrivateBoundary) {
|
|
7454
|
+
throw new Error(`${label} contains a group/world-writable directory without an existing private boundary: ${entry.path}`);
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7458
|
+
const canonicalAncestor = realpathSync3(nearestExisting);
|
|
7459
|
+
if (canonicalAncestor !== nearestExisting) {
|
|
7460
|
+
throw new Error(`${label} contains a non-canonical or changed ancestor: ${nearestExisting}`);
|
|
7461
|
+
}
|
|
7462
|
+
return resolve5(canonicalAncestor, ...missingSuffix);
|
|
7463
|
+
}
|
|
7464
|
+
|
|
7465
|
+
// src/cli/config-store.ts
|
|
7061
7466
|
var FAST_AGENT_IDS = ["back-fast", "front-fast", "review-fast", "researcher", "git", "docs", "verifier"];
|
|
7062
7467
|
var DEEP_AGENT_IDS = ["master", "planner", "back-deep", "front-deep", "review-deep", "debugger", "security", "devops"];
|
|
7063
7468
|
var ALL_AGENT_IDS = [...DEEP_AGENT_IDS, ...FAST_AGENT_IDS, "explorer"];
|
|
@@ -7084,14 +7489,30 @@ function applyModelProfile(source, profile) {
|
|
|
7084
7489
|
updated = setJsonc(updated, ["agents", "explorer", "models"], profile.agentOverrides.explorer ?? profile.fast);
|
|
7085
7490
|
return updated;
|
|
7086
7491
|
}
|
|
7087
|
-
function
|
|
7088
|
-
|
|
7089
|
-
|
|
7492
|
+
function matchesSnapshot(path, expected) {
|
|
7493
|
+
if (!expected.exists)
|
|
7494
|
+
return !existsSync4(path);
|
|
7495
|
+
if (!existsSync4(path))
|
|
7496
|
+
return false;
|
|
7497
|
+
const stat = lstatSync5(path);
|
|
7498
|
+
return stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino && readFileSync4(path, "utf8") === expected.bytes;
|
|
7499
|
+
}
|
|
7500
|
+
function atomicWrite(path, content, expected) {
|
|
7501
|
+
const canonicalPath = secureCanonicalPath(path, "Managed global config path");
|
|
7502
|
+
if (canonicalPath !== expected.path)
|
|
7503
|
+
throw new Error(`Global config path changed after preflight: ${path}`);
|
|
7504
|
+
mkdirSync(dirname3(canonicalPath), { recursive: true, mode: 448 });
|
|
7505
|
+
if (secureCanonicalPath(canonicalPath, "Managed global config path") !== canonicalPath)
|
|
7506
|
+
throw new Error(`Global config path changed during write: ${path}`);
|
|
7507
|
+
assertWriteable(dirname3(canonicalPath), "Managed global config directory");
|
|
7508
|
+
const temporary = `${canonicalPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
7090
7509
|
const descriptor = openSync(temporary, "wx", 384);
|
|
7091
7510
|
try {
|
|
7092
7511
|
writeFileSync(descriptor, content);
|
|
7093
7512
|
closeSync(descriptor);
|
|
7094
|
-
|
|
7513
|
+
if (!matchesSnapshot(canonicalPath, expected))
|
|
7514
|
+
throw new Error(`Refusing to replace concurrently changed file: ${canonicalPath}`);
|
|
7515
|
+
renameSync(temporary, canonicalPath);
|
|
7095
7516
|
} catch (error) {
|
|
7096
7517
|
try {
|
|
7097
7518
|
closeSync(descriptor);
|
|
@@ -7103,51 +7524,105 @@ function atomicWrite(path, content) {
|
|
|
7103
7524
|
}
|
|
7104
7525
|
}
|
|
7105
7526
|
function isRegularFile(path) {
|
|
7106
|
-
const stat =
|
|
7527
|
+
const stat = lstatSync5(path);
|
|
7107
7528
|
return stat.isFile() && !stat.isSymbolicLink();
|
|
7108
7529
|
}
|
|
7109
|
-
function
|
|
7110
|
-
|
|
7530
|
+
function assertWriteable(path, label) {
|
|
7531
|
+
let candidate = path;
|
|
7532
|
+
while (!existsSync4(candidate)) {
|
|
7533
|
+
const parent = dirname3(candidate);
|
|
7534
|
+
if (parent === candidate)
|
|
7535
|
+
break;
|
|
7536
|
+
candidate = parent;
|
|
7537
|
+
}
|
|
7538
|
+
try {
|
|
7539
|
+
const stat = lstatSync5(candidate);
|
|
7540
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() && candidate !== path)
|
|
7541
|
+
throw new Error("unsafe parent");
|
|
7542
|
+
const uid = process.getuid?.();
|
|
7543
|
+
if (uid !== undefined && (stat.uid !== uid || (stat.mode & 18) !== 0))
|
|
7544
|
+
throw new Error("unsafe ownership or mode");
|
|
7545
|
+
accessSync(candidate, fsConstants.W_OK | (stat.isDirectory() ? fsConstants.X_OK : 0));
|
|
7546
|
+
} catch {
|
|
7547
|
+
throw new Error(`${label} is not writeable: ${path}`);
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
function legacySchemaMatches(source, generated) {
|
|
7551
|
+
try {
|
|
7552
|
+
const previous = JSON.parse(source);
|
|
7553
|
+
if (previous.$comment !== undefined || ![undefined, 1].includes(previous["x-agent-gvozd-schema-version"]))
|
|
7554
|
+
return false;
|
|
7555
|
+
if (previous.$id !== "https://example.invalid/agent-gvozd.schema.json")
|
|
7556
|
+
return false;
|
|
7557
|
+
return generated ? isEquivalentLegacySchema(source, generated) : true;
|
|
7558
|
+
} catch {
|
|
7559
|
+
return false;
|
|
7560
|
+
}
|
|
7561
|
+
}
|
|
7562
|
+
function snapshot(path) {
|
|
7563
|
+
if (!existsSync4(path))
|
|
7564
|
+
return Object.freeze({ path, exists: false });
|
|
7565
|
+
const stat = lstatSync5(path);
|
|
7566
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
7567
|
+
throw new Error(`Managed global config snapshot target is unsafe: ${path}`);
|
|
7568
|
+
return Object.freeze({ path, exists: true, bytes: readFileSync4(path, "utf8"), dev: stat.dev, ino: stat.ino });
|
|
7569
|
+
}
|
|
7570
|
+
function preflightGlobalConfig(configRoot, schemaSource) {
|
|
7571
|
+
const canonicalRoot = secureCanonicalPath(configRoot, "OpenCode config root");
|
|
7572
|
+
const rootStat = existsSync4(canonicalRoot) ? lstatSync5(canonicalRoot) : undefined;
|
|
7111
7573
|
if (rootStat && (rootStat.isSymbolicLink() || !rootStat.isDirectory())) {
|
|
7112
|
-
throw new Error(`OpenCode config root is not a safe directory: ${
|
|
7574
|
+
throw new Error(`OpenCode config root is not a safe directory: ${canonicalRoot}`);
|
|
7113
7575
|
}
|
|
7114
|
-
|
|
7115
|
-
const
|
|
7576
|
+
assertWriteable(canonicalRoot, "OpenCode config root");
|
|
7577
|
+
const directory = secureCanonicalPath(join6(canonicalRoot, "gvozd"), "Global Gvozd directory");
|
|
7578
|
+
const directoryStat = existsSync4(directory) ? lstatSync5(directory) : undefined;
|
|
7116
7579
|
if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
|
|
7117
7580
|
throw new Error(`Global Gvozd path is not a safe directory: ${directory}`);
|
|
7118
7581
|
}
|
|
7119
|
-
|
|
7120
|
-
const
|
|
7121
|
-
|
|
7582
|
+
assertWriteable(directory, "Global Gvozd directory");
|
|
7583
|
+
const configPath = join6(directory, "config.jsonc");
|
|
7584
|
+
const schemaPath = join6(directory, "schema.json");
|
|
7585
|
+
if (existsSync4(configPath)) {
|
|
7122
7586
|
if (!isRegularFile(configPath))
|
|
7123
7587
|
throw new Error(`Global Gvozd config is not a safe file: ${configPath}`);
|
|
7124
7588
|
assertValidJsonc(readFileSync4(configPath, "utf8"), configPath);
|
|
7589
|
+
assertWriteable(configPath, "Global Gvozd config");
|
|
7125
7590
|
}
|
|
7126
|
-
if (
|
|
7127
|
-
|
|
7591
|
+
if (existsSync4(schemaPath)) {
|
|
7592
|
+
if (!isRegularFile(schemaPath))
|
|
7593
|
+
throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
|
|
7594
|
+
const schema = readFileSync4(schemaPath, "utf8");
|
|
7595
|
+
if (!hasGeneratedSchemaMarker(schema) && !legacySchemaMatches(schema, schemaSource)) {
|
|
7596
|
+
throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
|
|
7597
|
+
}
|
|
7598
|
+
assertWriteable(schemaPath, "Global Gvozd schema");
|
|
7128
7599
|
}
|
|
7600
|
+
return Object.freeze({ configRoot: canonicalRoot, config: snapshot(configPath), schema: snapshot(schemaPath) });
|
|
7129
7601
|
}
|
|
7130
7602
|
function writeGlobalConfig(input) {
|
|
7131
|
-
preflightGlobalConfig(input.configRoot);
|
|
7132
|
-
const
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
const
|
|
7603
|
+
const state = input.snapshot ?? preflightGlobalConfig(input.configRoot, input.schemaSource);
|
|
7604
|
+
const canonicalRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
|
|
7605
|
+
if (state.configRoot !== canonicalRoot)
|
|
7606
|
+
throw new Error("Global config snapshot belongs to another config root");
|
|
7607
|
+
const directory = join6(state.configRoot, "gvozd");
|
|
7608
|
+
const configPath = join6(directory, "config.jsonc");
|
|
7609
|
+
const schemaPath = join6(directory, "schema.json");
|
|
7610
|
+
const base = state.config.exists ? state.config.bytes : `{
|
|
7136
7611
|
"$schema": "./schema.json",
|
|
7137
7612
|
"agents": {}
|
|
7138
7613
|
}
|
|
7139
7614
|
`;
|
|
7140
|
-
if (!input.schemaSource
|
|
7615
|
+
if (!hasGeneratedSchemaMarker(input.schemaSource)) {
|
|
7141
7616
|
throw new Error("Package schema is missing the Gvozd ownership marker");
|
|
7142
7617
|
}
|
|
7143
7618
|
const config = applyModelProfile(base, input.profile);
|
|
7144
7619
|
assertValidJsonc(input.schemaSource, "package Gvozd schema");
|
|
7145
7620
|
atomicWrite(schemaPath, input.schemaSource.endsWith(`
|
|
7146
7621
|
`) ? input.schemaSource : `${input.schemaSource}
|
|
7147
|
-
|
|
7622
|
+
`, state.schema);
|
|
7148
7623
|
atomicWrite(configPath, config.endsWith(`
|
|
7149
7624
|
`) ? config : `${config}
|
|
7150
|
-
|
|
7625
|
+
`, state.config);
|
|
7151
7626
|
return { configPath, schemaPath, config };
|
|
7152
7627
|
}
|
|
7153
7628
|
|
|
@@ -7220,20 +7695,43 @@ function profileFromAgents(agents, catalog) {
|
|
|
7220
7695
|
}
|
|
7221
7696
|
|
|
7222
7697
|
// src/cli/global-sync.ts
|
|
7223
|
-
import { closeSync as closeSync2, lstatSync as
|
|
7698
|
+
import { accessSync as accessSync2, closeSync as closeSync2, constants as fsConstants2, existsSync as existsSync5, lstatSync as lstatSync6, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync5, readdirSync as readdirSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7224
7699
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7225
|
-
import { join as
|
|
7700
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
7226
7701
|
function stat(path) {
|
|
7227
7702
|
try {
|
|
7228
|
-
return
|
|
7703
|
+
return lstatSync6(path);
|
|
7229
7704
|
} catch (error) {
|
|
7230
7705
|
if (error.code === "ENOENT")
|
|
7231
7706
|
return;
|
|
7232
7707
|
throw error;
|
|
7233
7708
|
}
|
|
7234
7709
|
}
|
|
7710
|
+
function assertWriteable2(path, label) {
|
|
7711
|
+
let candidate = path;
|
|
7712
|
+
while (!existsSync5(candidate)) {
|
|
7713
|
+
const parent = dirname4(candidate);
|
|
7714
|
+
if (parent === candidate)
|
|
7715
|
+
break;
|
|
7716
|
+
candidate = parent;
|
|
7717
|
+
}
|
|
7718
|
+
try {
|
|
7719
|
+
const current = lstatSync6(candidate);
|
|
7720
|
+
if (current.isSymbolicLink() || !current.isDirectory() && candidate !== path)
|
|
7721
|
+
throw new Error("unsafe parent");
|
|
7722
|
+
const uid = process.getuid?.();
|
|
7723
|
+
if (uid !== undefined && (current.uid !== uid || (current.mode & 18) !== 0))
|
|
7724
|
+
throw new Error("unsafe ownership or mode");
|
|
7725
|
+
accessSync2(candidate, fsConstants2.W_OK | (current.isDirectory() ? fsConstants2.X_OK : 0));
|
|
7726
|
+
} catch {
|
|
7727
|
+
throw new Error(`${label} is not writeable: ${path}`);
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7235
7730
|
function createFile(path, content) {
|
|
7236
|
-
const
|
|
7731
|
+
const canonicalPath = secureCanonicalPath(path, "Managed global agent path");
|
|
7732
|
+
if (canonicalPath !== path)
|
|
7733
|
+
throw new Error(`Managed global agent path changed: ${path}`);
|
|
7734
|
+
const descriptor = openSync2(canonicalPath, "wx", 384);
|
|
7237
7735
|
try {
|
|
7238
7736
|
writeFileSync2(descriptor, content);
|
|
7239
7737
|
} finally {
|
|
@@ -7241,6 +7739,8 @@ function createFile(path, content) {
|
|
|
7241
7739
|
}
|
|
7242
7740
|
}
|
|
7243
7741
|
function replaceFile(path, content) {
|
|
7742
|
+
if (secureCanonicalPath(path, "Managed global agent path") !== path)
|
|
7743
|
+
throw new Error(`Managed global agent path changed: ${path}`);
|
|
7244
7744
|
const temporary = `${path}.tmp-${process.pid}-${randomUUID2()}`;
|
|
7245
7745
|
createFile(temporary, content);
|
|
7246
7746
|
try {
|
|
@@ -7253,21 +7753,41 @@ function replaceFile(path, content) {
|
|
|
7253
7753
|
}
|
|
7254
7754
|
}
|
|
7255
7755
|
function writeManagedAgents(input) {
|
|
7256
|
-
const
|
|
7257
|
-
const
|
|
7756
|
+
const configRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
|
|
7757
|
+
const agentsDirectory = secureCanonicalPath(join7(configRoot, "agents"), "OpenCode agents path");
|
|
7758
|
+
const result = { created: [], updated: [], unchanged: [], conflicts: [], removed: [] };
|
|
7258
7759
|
const writes = [];
|
|
7259
|
-
const
|
|
7760
|
+
const removals = new Map;
|
|
7761
|
+
const enabled = new Set(Object.entries(input.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
|
|
7762
|
+
const rootStat = stat(configRoot);
|
|
7260
7763
|
if (rootStat && (rootStat.isSymbolicLink() || !rootStat.isDirectory())) {
|
|
7261
|
-
throw new Error(`OpenCode config root is not a safe directory: ${
|
|
7764
|
+
throw new Error(`OpenCode config root is not a safe directory: ${configRoot}`);
|
|
7262
7765
|
}
|
|
7766
|
+
assertWriteable2(configRoot, "OpenCode config root");
|
|
7263
7767
|
const directoryStat = stat(agentsDirectory);
|
|
7264
7768
|
if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
|
|
7265
7769
|
throw new Error(`OpenCode agents path is not a safe directory: ${agentsDirectory}`);
|
|
7266
7770
|
}
|
|
7771
|
+
assertWriteable2(agentsDirectory, "OpenCode agents directory");
|
|
7772
|
+
if (directoryStat) {
|
|
7773
|
+
for (const entry of readdirSync4(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
7774
|
+
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
7775
|
+
continue;
|
|
7776
|
+
const path = join7(agentsDirectory, entry.name);
|
|
7777
|
+
const currentStat = stat(path);
|
|
7778
|
+
if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile())
|
|
7779
|
+
continue;
|
|
7780
|
+
if (hasGeneratedAgentMarker(readFileSync5(path, "utf8"))) {
|
|
7781
|
+
assertWriteable2(path, "Managed global agent");
|
|
7782
|
+
result.removed.push(path);
|
|
7783
|
+
removals.set(path, readFileSync5(path, "utf8"));
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
}
|
|
7267
7787
|
for (const [id, agent] of Object.entries(input.agents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
7268
7788
|
if (agent.disabled)
|
|
7269
7789
|
continue;
|
|
7270
|
-
const path =
|
|
7790
|
+
const path = join7(agentsDirectory, `${id}.md`);
|
|
7271
7791
|
const content = renderAgent(agent);
|
|
7272
7792
|
const currentStat = stat(path);
|
|
7273
7793
|
if (!currentStat) {
|
|
@@ -7280,45 +7800,134 @@ function writeManagedAgents(input) {
|
|
|
7280
7800
|
continue;
|
|
7281
7801
|
}
|
|
7282
7802
|
const current = readFileSync5(path, "utf8");
|
|
7283
|
-
if (!current
|
|
7803
|
+
if (!hasGeneratedAgentMarker(current)) {
|
|
7284
7804
|
result.conflicts.push(path);
|
|
7285
7805
|
continue;
|
|
7286
7806
|
}
|
|
7807
|
+
assertWriteable2(path, "Managed global agent");
|
|
7287
7808
|
if (current === content) {
|
|
7288
7809
|
result.unchanged.push(path);
|
|
7289
7810
|
continue;
|
|
7290
7811
|
}
|
|
7291
7812
|
result.updated.push(path);
|
|
7292
|
-
writes.push({ path, content, replace: true });
|
|
7813
|
+
writes.push({ path, content, replace: true, previous: current });
|
|
7293
7814
|
}
|
|
7294
7815
|
if (result.conflicts.length > 0) {
|
|
7295
7816
|
throw new Error(`Refusing to overwrite unmanaged global agents: ${result.conflicts.join(", ")}`);
|
|
7296
7817
|
}
|
|
7297
7818
|
if (input.check)
|
|
7298
7819
|
return result;
|
|
7299
|
-
|
|
7820
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
7821
|
+
throw new Error("OpenCode config root changed before write");
|
|
7822
|
+
mkdirSync2(configRoot, { recursive: true, mode: 448 });
|
|
7823
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
7824
|
+
throw new Error("OpenCode config root changed during creation");
|
|
7825
|
+
assertWriteable2(configRoot, "OpenCode config root");
|
|
7826
|
+
if (secureCanonicalPath(agentsDirectory, "OpenCode agents path") !== agentsDirectory)
|
|
7827
|
+
throw new Error("OpenCode agents path changed before creation");
|
|
7300
7828
|
mkdirSync2(agentsDirectory, { recursive: true, mode: 448 });
|
|
7829
|
+
if (secureCanonicalPath(agentsDirectory, "OpenCode agents path") !== agentsDirectory)
|
|
7830
|
+
throw new Error("OpenCode agents path changed before write");
|
|
7831
|
+
assertWriteable2(configRoot, "OpenCode config root");
|
|
7832
|
+
assertWriteable2(agentsDirectory, "OpenCode agents directory");
|
|
7833
|
+
for (const path of result.removed) {
|
|
7834
|
+
if (secureCanonicalPath(path, "Managed global agent path") !== path)
|
|
7835
|
+
throw new Error(`Managed global agent path changed: ${path}`);
|
|
7836
|
+
const currentStat = stat(path);
|
|
7837
|
+
if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile()) {
|
|
7838
|
+
throw new Error(`Refusing to remove a changed or unmanaged global agent: ${path}`);
|
|
7839
|
+
}
|
|
7840
|
+
const current = readFileSync5(path, "utf8");
|
|
7841
|
+
if (!hasGeneratedAgentMarker(current) || current !== removals.get(path)) {
|
|
7842
|
+
throw new Error(`Refusing to remove a concurrently changed global agent: ${path}`);
|
|
7843
|
+
}
|
|
7844
|
+
unlinkSync2(path);
|
|
7845
|
+
}
|
|
7301
7846
|
for (const write of writes) {
|
|
7302
|
-
if (write.replace)
|
|
7847
|
+
if (write.replace) {
|
|
7848
|
+
const currentStat = stat(write.path);
|
|
7849
|
+
const current = currentStat?.isFile() && !currentStat.isSymbolicLink() ? readFileSync5(write.path, "utf8") : undefined;
|
|
7850
|
+
if (!current || !hasGeneratedAgentMarker(current) || current !== write.previous) {
|
|
7851
|
+
throw new Error(`Refusing to replace a concurrently changed global agent: ${write.path}`);
|
|
7852
|
+
}
|
|
7303
7853
|
replaceFile(write.path, write.content);
|
|
7304
|
-
else
|
|
7854
|
+
} else
|
|
7305
7855
|
createFile(write.path, write.content);
|
|
7306
7856
|
}
|
|
7307
7857
|
return result;
|
|
7308
7858
|
}
|
|
7309
7859
|
|
|
7860
|
+
// src/file-lock.ts
|
|
7861
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
7862
|
+
import { accessSync as accessSync3, closeSync as closeSync3, constants, existsSync as existsSync6, fstatSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7863
|
+
import { dirname as dirname5 } from "node:path";
|
|
7864
|
+
function assertSecure(path) {
|
|
7865
|
+
const stat = lstatSync7(path);
|
|
7866
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
7867
|
+
throw new Error(`Lock directory is unsafe: ${path}`);
|
|
7868
|
+
const uid = process.getuid?.();
|
|
7869
|
+
if (uid !== undefined && (stat.uid !== uid || (stat.mode & 18) !== 0)) {
|
|
7870
|
+
throw new Error(`Lock directory must be owner-controlled and not group/world writable: ${path}`);
|
|
7871
|
+
}
|
|
7872
|
+
accessSync3(path, constants.W_OK | constants.X_OK);
|
|
7873
|
+
}
|
|
7874
|
+
function acquire(path, operation) {
|
|
7875
|
+
const canonicalPath = secureCanonicalPath(path, "Lock path");
|
|
7876
|
+
const directory = dirname5(canonicalPath);
|
|
7877
|
+
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
7878
|
+
if (secureCanonicalPath(canonicalPath, "Lock path") !== canonicalPath)
|
|
7879
|
+
throw new Error(`Lock path changed during creation: ${path}`);
|
|
7880
|
+
assertSecure(directory);
|
|
7881
|
+
const nonce = `${process.pid}:${randomUUID3()}
|
|
7882
|
+
`;
|
|
7883
|
+
let descriptor;
|
|
7884
|
+
try {
|
|
7885
|
+
descriptor = openSync3(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
7886
|
+
} catch (error) {
|
|
7887
|
+
if (error.code === "EEXIST") {
|
|
7888
|
+
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`);
|
|
7889
|
+
}
|
|
7890
|
+
throw error;
|
|
7891
|
+
}
|
|
7892
|
+
const created = fstatSync(descriptor);
|
|
7893
|
+
writeFileSync3(descriptor, nonce);
|
|
7894
|
+
return { path: canonicalPath, descriptor, nonce, dev: created.dev, ino: created.ino };
|
|
7895
|
+
}
|
|
7896
|
+
function release(lock) {
|
|
7897
|
+
closeSync3(lock.descriptor);
|
|
7898
|
+
if (existsSync6(lock.path)) {
|
|
7899
|
+
const current = lstatSync7(lock.path);
|
|
7900
|
+
if (current.isFile() && !current.isSymbolicLink() && current.dev === lock.dev && current.ino === lock.ino && readFileSync6(lock.path, "utf8") === lock.nonce)
|
|
7901
|
+
unlinkSync3(lock.path);
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
async function withExclusiveFileLock(path, callback) {
|
|
7905
|
+
const lock = acquire(path, "setup");
|
|
7906
|
+
try {
|
|
7907
|
+
return await callback();
|
|
7908
|
+
} finally {
|
|
7909
|
+
release(lock);
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
function withExclusiveFileLockSync(path, callback, operation = "operation") {
|
|
7913
|
+
const lock = acquire(path, operation);
|
|
7914
|
+
try {
|
|
7915
|
+
return callback();
|
|
7916
|
+
} finally {
|
|
7917
|
+
release(lock);
|
|
7918
|
+
}
|
|
7919
|
+
}
|
|
7920
|
+
|
|
7310
7921
|
// src/cli/setup.ts
|
|
7311
|
-
var PACKAGE_SPEC = "@nail00749/agent-gvozd@^0.1.0";
|
|
7312
|
-
var SUPPORTED_VERSION = "0.0.0-beta-19425";
|
|
7313
7922
|
function assertVersion(version) {
|
|
7314
|
-
if (parseOpenCodeVersion(version) !==
|
|
7315
|
-
throw new Error(`Unsupported OpenCode version. Gvozd
|
|
7923
|
+
if (parseOpenCodeVersion(version) !== SUPPORTED_OPENCODE_VERSION) {
|
|
7924
|
+
throw new Error(`Unsupported OpenCode version. Gvozd ${PACKAGE_VERSION} requires ${SUPPORTED_OPENCODE_VERSION}.`);
|
|
7316
7925
|
}
|
|
7317
7926
|
}
|
|
7318
7927
|
async function selectProfile(input, client, configRoot) {
|
|
7319
7928
|
const catalog = parseModels(await client.models());
|
|
7320
7929
|
const config = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
7321
|
-
const hasGlobalConfig =
|
|
7930
|
+
const hasGlobalConfig = existsSync7(join8(configRoot, "gvozd", "config.jsonc"));
|
|
7322
7931
|
return chooseModelProfile({
|
|
7323
7932
|
catalog,
|
|
7324
7933
|
ui: input.ui,
|
|
@@ -7335,54 +7944,100 @@ async function confirm2(input, message) {
|
|
|
7335
7944
|
const answer = await input.ui.confirm({ message, initialValue: true });
|
|
7336
7945
|
return typeof answer === "symbol" ? false : answer;
|
|
7337
7946
|
}
|
|
7947
|
+
function sameSnapshot(left, right) {
|
|
7948
|
+
return left.configRoot === right.configRoot && left.config.exists === right.config.exists && left.config.dev === right.config.dev && left.config.ino === right.config.ino && left.config.bytes === right.config.bytes && left.schema.exists === right.schema.exists && left.schema.dev === right.schema.dev && left.schema.ino === right.schema.ino && left.schema.bytes === right.schema.bytes;
|
|
7949
|
+
}
|
|
7338
7950
|
async function runSetup(input) {
|
|
7339
7951
|
const client = await (input.findClient ?? (() => findOpenCode()))();
|
|
7340
7952
|
const paths = await client.debugPaths();
|
|
7341
|
-
|
|
7342
|
-
if (!configRoot)
|
|
7953
|
+
if (!paths.config)
|
|
7343
7954
|
throw new Error("OpenCode did not report its config path");
|
|
7344
|
-
|
|
7955
|
+
const configRoot = secureCanonicalPath(paths.config, "OpenCode config root");
|
|
7345
7956
|
preflightGlobalConfig(configRoot);
|
|
7957
|
+
const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
|
|
7958
|
+
assertVersion(await client.version());
|
|
7346
7959
|
const before = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
7347
|
-
|
|
7960
|
+
const schemaSource = join8(dirname6(before.sources[0]), "schema.json");
|
|
7961
|
+
const packagedSchema = readFileSync7(schemaSource, "utf8");
|
|
7962
|
+
const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
|
|
7963
|
+
const preview = writeManagedAgents({ configRoot, agents: before.agents, check: true });
|
|
7348
7964
|
const profile = await selectProfile(input, client, configRoot);
|
|
7349
7965
|
if (!profile)
|
|
7350
7966
|
return { status: "cancelled" };
|
|
7351
|
-
input.output?.(
|
|
7352
|
-
|
|
7353
|
-
Write
|
|
7967
|
+
input.output?.([
|
|
7968
|
+
`Register ${PACKAGE_SPEC}`,
|
|
7969
|
+
`Write ${join8(configRoot, "gvozd", "config.jsonc")}`,
|
|
7970
|
+
`Write managed agents in ${join8(configRoot, "agents")}`,
|
|
7971
|
+
...preview.removed.length > 0 ? [`Remove ${preview.removed.length} stale or disabled managed agent(s)`] : []
|
|
7972
|
+
].join(`
|
|
7973
|
+
`));
|
|
7354
7974
|
if (!await confirm2(input, "Run setup?"))
|
|
7355
7975
|
return { status: "cancelled" };
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
const
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
const
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7976
|
+
return withExclusiveFileLock(join8(configRoot, "gvozd", "setup.lock"), async () => {
|
|
7977
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
7978
|
+
throw new Error("OpenCode config root changed while setup awaited the lock");
|
|
7979
|
+
const lockedSchema = readFileSync7(schemaSource, "utf8");
|
|
7980
|
+
const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
|
|
7981
|
+
if (!sameSnapshot(previewSnapshot, snapshot))
|
|
7982
|
+
throw new Error("Global Gvozd configuration changed while setup awaited confirmation; review and rerun setup");
|
|
7983
|
+
const lockedBefore = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
7984
|
+
writeManagedAgents({ configRoot, agents: lockedBefore.agents, check: true });
|
|
7985
|
+
const lockedProfile = input.yes ? await selectProfile(input, client, configRoot) : profile;
|
|
7986
|
+
if (!lockedProfile)
|
|
7987
|
+
throw new Error("Model profile changed while setup awaited the global lock; rerun setup");
|
|
7988
|
+
if (!sameSnapshot(snapshot, preflightGlobalConfig(configRoot, lockedSchema))) {
|
|
7989
|
+
throw new Error("Global Gvozd configuration changed during locked setup revalidation; review and rerun setup");
|
|
7990
|
+
}
|
|
7991
|
+
writeManagedAgents({ configRoot, agents: lockedBefore.agents, check: true });
|
|
7992
|
+
await client.pluginAdd(PACKAGE_SPEC);
|
|
7993
|
+
try {
|
|
7994
|
+
writeGlobalConfig({ configRoot, profile: lockedProfile, schemaSource: lockedSchema, snapshot });
|
|
7995
|
+
const configured = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
7996
|
+
writeManagedAgents({ configRoot, agents: configured.agents });
|
|
7997
|
+
await client.serviceRestart();
|
|
7998
|
+
const report = await runDoctor({ client, configRoot, runtimeConfigRoot, cwd: input.cwd });
|
|
7999
|
+
return { status: "complete", report };
|
|
8000
|
+
} catch (error) {
|
|
8001
|
+
const detail = redactDiagnostic(error);
|
|
8002
|
+
throw new Error(`${PACKAGE_NAME} remains registered, but setup is incomplete: ${detail}. ` + `Managed files under ${configRoot} may be partially updated; no automatic rollback was attempted because concurrent or user changes cannot be distinguished safely. ` + "Fix the reported cause, then rerun: gvozd setup");
|
|
8003
|
+
}
|
|
8004
|
+
});
|
|
7368
8005
|
}
|
|
7369
8006
|
async function runConfigure(input) {
|
|
7370
8007
|
const client = await (input.findClient ?? (() => findOpenCode()))();
|
|
7371
8008
|
const paths = await client.debugPaths();
|
|
7372
|
-
|
|
7373
|
-
if (!configRoot)
|
|
8009
|
+
if (!paths.config)
|
|
7374
8010
|
throw new Error("OpenCode did not report its config path");
|
|
7375
|
-
|
|
8011
|
+
const configRoot = secureCanonicalPath(paths.config, "OpenCode config root");
|
|
7376
8012
|
preflightGlobalConfig(configRoot);
|
|
8013
|
+
const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
|
|
8014
|
+
assertVersion(await client.version());
|
|
8015
|
+
const config = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8016
|
+
const schemaSource = join8(dirname6(config.sources[0]), "schema.json");
|
|
8017
|
+
const packagedSchema = readFileSync7(schemaSource, "utf8");
|
|
8018
|
+
const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
|
|
7377
8019
|
const profile = await selectProfile(input, client, configRoot);
|
|
7378
8020
|
if (!profile || !await confirm2(input, "Apply model configuration?"))
|
|
7379
8021
|
return { status: "cancelled" };
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
8022
|
+
return withExclusiveFileLock(join8(configRoot, "gvozd", "setup.lock"), async () => {
|
|
8023
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
8024
|
+
throw new Error("OpenCode config root changed while configuration awaited the lock");
|
|
8025
|
+
const lockedSchema = readFileSync7(schemaSource, "utf8");
|
|
8026
|
+
const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
|
|
8027
|
+
if (!sameSnapshot(previewSnapshot, snapshot))
|
|
8028
|
+
throw new Error("Global Gvozd configuration changed while configuration awaited confirmation; review and rerun");
|
|
8029
|
+
loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8030
|
+
const lockedProfile = input.yes ? await selectProfile(input, client, configRoot) : profile;
|
|
8031
|
+
if (!lockedProfile)
|
|
8032
|
+
throw new Error("Model profile changed while configuration awaited the global lock; rerun configuration");
|
|
8033
|
+
if (!sameSnapshot(snapshot, preflightGlobalConfig(configRoot, lockedSchema))) {
|
|
8034
|
+
throw new Error("Global Gvozd configuration changed during locked configuration revalidation; review and rerun");
|
|
8035
|
+
}
|
|
8036
|
+
writeGlobalConfig({ configRoot, profile: lockedProfile, schemaSource: lockedSchema, snapshot });
|
|
8037
|
+
await client.serviceRestart();
|
|
8038
|
+
const report = await runDoctor({ client, configRoot, runtimeConfigRoot, cwd: input.cwd });
|
|
8039
|
+
return { status: "complete", report };
|
|
8040
|
+
});
|
|
7386
8041
|
}
|
|
7387
8042
|
function setupExitCode(result) {
|
|
7388
8043
|
if (result.status === "cancelled")
|
|
@@ -7391,13 +8046,13 @@ function setupExitCode(result) {
|
|
|
7391
8046
|
}
|
|
7392
8047
|
|
|
7393
8048
|
// src/sync.ts
|
|
7394
|
-
import { closeSync as
|
|
7395
|
-
import { randomUUID as
|
|
7396
|
-
import { basename, dirname as
|
|
8049
|
+
import { closeSync as closeSync4, lstatSync as lstatSync8, mkdirSync as mkdirSync4, openSync as openSync4, readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync as realpathSync4, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8050
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
8051
|
+
import { basename, dirname as dirname7, join as join9, relative as relative4 } from "node:path";
|
|
7397
8052
|
function renderPluginEntrypoint(config, destination) {
|
|
7398
8053
|
let moduleSpecifier = "agent-gvozd/server";
|
|
7399
|
-
if (
|
|
7400
|
-
moduleSpecifier =
|
|
8054
|
+
if (realpathSync4(config.packageRoot) === realpathSync4(config.projectRoot)) {
|
|
8055
|
+
moduleSpecifier = relative4(destination, join9(config.packageRoot, "src", "index")).replaceAll("\\", "/");
|
|
7401
8056
|
if (!moduleSpecifier.startsWith("."))
|
|
7402
8057
|
moduleSpecifier = `./${moduleSpecifier}`;
|
|
7403
8058
|
}
|
|
@@ -7432,7 +8087,7 @@ function renderDiff(path, before, after) {
|
|
|
7432
8087
|
}
|
|
7433
8088
|
function stat2(path) {
|
|
7434
8089
|
try {
|
|
7435
|
-
return
|
|
8090
|
+
return lstatSync8(path);
|
|
7436
8091
|
} catch (error) {
|
|
7437
8092
|
if (error.code === "ENOENT")
|
|
7438
8093
|
return;
|
|
@@ -7440,9 +8095,9 @@ function stat2(path) {
|
|
|
7440
8095
|
}
|
|
7441
8096
|
}
|
|
7442
8097
|
function safeDirectory(root, segments, create) {
|
|
7443
|
-
let current =
|
|
8098
|
+
let current = realpathSync4(root);
|
|
7444
8099
|
for (const segment of segments) {
|
|
7445
|
-
current =
|
|
8100
|
+
current = join9(current, segment);
|
|
7446
8101
|
const currentStat = stat2(current);
|
|
7447
8102
|
if (currentStat) {
|
|
7448
8103
|
if (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {
|
|
@@ -7451,7 +8106,7 @@ function safeDirectory(root, segments, create) {
|
|
|
7451
8106
|
continue;
|
|
7452
8107
|
}
|
|
7453
8108
|
if (create)
|
|
7454
|
-
|
|
8109
|
+
mkdirSync4(current);
|
|
7455
8110
|
}
|
|
7456
8111
|
return current;
|
|
7457
8112
|
}
|
|
@@ -7464,145 +8119,191 @@ function assertRegularFile(path) {
|
|
|
7464
8119
|
return true;
|
|
7465
8120
|
}
|
|
7466
8121
|
function createFile2(path, content) {
|
|
7467
|
-
const descriptor =
|
|
8122
|
+
const descriptor = openSync4(path, "wx", 384);
|
|
7468
8123
|
try {
|
|
7469
|
-
|
|
8124
|
+
writeFileSync4(descriptor, content);
|
|
7470
8125
|
} finally {
|
|
7471
|
-
|
|
8126
|
+
closeSync4(descriptor);
|
|
7472
8127
|
}
|
|
7473
8128
|
}
|
|
7474
8129
|
function replaceFile2(path, content) {
|
|
7475
|
-
const temporary = `${path}.tmp-${process.pid}-${
|
|
8130
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID4()}`;
|
|
7476
8131
|
createFile2(temporary, content);
|
|
7477
8132
|
try {
|
|
7478
8133
|
renameSync3(temporary, path);
|
|
7479
8134
|
} catch (error) {
|
|
7480
8135
|
try {
|
|
7481
|
-
|
|
8136
|
+
unlinkSync4(temporary);
|
|
7482
8137
|
} catch {}
|
|
7483
8138
|
throw error;
|
|
7484
8139
|
}
|
|
7485
8140
|
}
|
|
7486
|
-
function
|
|
8141
|
+
function planProjectTemplate(config, result, check) {
|
|
7487
8142
|
const directory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], !check);
|
|
7488
|
-
const
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
8143
|
+
const writes = [];
|
|
8144
|
+
const configPath = join9(directory, "config.jsonc");
|
|
8145
|
+
if (!assertRegularFile(configPath)) {
|
|
8146
|
+
result.created.push(configPath);
|
|
8147
|
+
writes.push({
|
|
8148
|
+
target: configPath,
|
|
8149
|
+
replace: false,
|
|
8150
|
+
content: [
|
|
8151
|
+
"{",
|
|
8152
|
+
' "$schema": "./schema.json",',
|
|
8153
|
+
" // Project overrides are merged after built-in and global configuration.",
|
|
8154
|
+
' "agents": {}',
|
|
8155
|
+
"}",
|
|
8156
|
+
""
|
|
8157
|
+
].join(`
|
|
8158
|
+
`)
|
|
8159
|
+
});
|
|
7501
8160
|
}
|
|
7502
|
-
const schemaSource =
|
|
7503
|
-
const schemaTarget =
|
|
7504
|
-
const schema =
|
|
7505
|
-
if (assertRegularFile(schemaTarget))
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
8161
|
+
const schemaSource = join9(dirname7(config.sources[0]), "schema.json");
|
|
8162
|
+
const schemaTarget = join9(directory, "schema.json");
|
|
8163
|
+
const schema = readFileSync8(schemaSource, "utf8");
|
|
8164
|
+
if (!assertRegularFile(schemaTarget)) {
|
|
8165
|
+
result.created.push(schemaTarget);
|
|
8166
|
+
writes.push({ target: schemaTarget, content: schema, replace: false });
|
|
8167
|
+
} else {
|
|
8168
|
+
const current = readFileSync8(schemaTarget, "utf8");
|
|
8169
|
+
if (current !== schema) {
|
|
8170
|
+
if (!hasGeneratedSchemaMarker(current) && !isEquivalentLegacySchema(current, schema)) {
|
|
8171
|
+
throw new Error(`Refusing to overwrite an unmanaged project schema: ${schemaTarget}`);
|
|
8172
|
+
}
|
|
8173
|
+
result.updated.push(schemaTarget);
|
|
8174
|
+
writes.push({ target: schemaTarget, content: schema, replace: true, previous: current });
|
|
8175
|
+
}
|
|
8176
|
+
}
|
|
8177
|
+
return writes;
|
|
7509
8178
|
}
|
|
7510
|
-
function
|
|
8179
|
+
function syncAgentsUnlocked(config, options) {
|
|
7511
8180
|
const check = options.check ?? false;
|
|
7512
8181
|
const destination = safeDirectory(config.projectRoot, [".opencode", "agents"], false);
|
|
7513
8182
|
const pluginDestination = safeDirectory(config.projectRoot, [".opencode", "plugins", "agent-gvozd"], false);
|
|
7514
8183
|
const result = { created: [], updated: [], removed: [], unchanged: [] };
|
|
7515
8184
|
const writes = [];
|
|
7516
8185
|
let pluginWrite;
|
|
8186
|
+
const removals = new Map;
|
|
8187
|
+
const templateWrites = planProjectTemplate(config, result, check);
|
|
7517
8188
|
const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
|
|
7518
8189
|
if (stat2(destination)) {
|
|
7519
|
-
for (const entry of
|
|
8190
|
+
for (const entry of readdirSync5(destination, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7520
8191
|
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
7521
8192
|
continue;
|
|
7522
|
-
const target =
|
|
7523
|
-
const current =
|
|
7524
|
-
if (!current
|
|
8193
|
+
const target = join9(destination, entry.name);
|
|
8194
|
+
const current = readFileSync8(target, "utf8");
|
|
8195
|
+
if (!hasGeneratedAgentMarker(current))
|
|
7525
8196
|
continue;
|
|
7526
8197
|
result.removed.push(target);
|
|
8198
|
+
removals.set(target, current);
|
|
7527
8199
|
options.onDiff?.(renderDiff(target, current, ""));
|
|
7528
8200
|
}
|
|
7529
8201
|
}
|
|
7530
8202
|
for (const [id, agent] of Object.entries(config.agents)) {
|
|
7531
8203
|
if (agent.disabled)
|
|
7532
8204
|
continue;
|
|
7533
|
-
const target =
|
|
8205
|
+
const target = join9(destination, `${id}.md`);
|
|
7534
8206
|
const content = renderAgent(agent);
|
|
7535
8207
|
if (!assertRegularFile(target)) {
|
|
7536
8208
|
result.created.push(target);
|
|
7537
8209
|
writes.push({ target, content, replace: false });
|
|
7538
8210
|
continue;
|
|
7539
8211
|
}
|
|
7540
|
-
const current =
|
|
8212
|
+
const current = readFileSync8(target, "utf8");
|
|
7541
8213
|
if (current === content) {
|
|
7542
8214
|
result.unchanged.push(target);
|
|
7543
8215
|
continue;
|
|
7544
8216
|
}
|
|
7545
|
-
if (!current
|
|
8217
|
+
if (!hasGeneratedAgentMarker(current)) {
|
|
7546
8218
|
throw new Error(`Refusing to overwrite a non-generated agent file: ${target}`);
|
|
7547
8219
|
}
|
|
7548
8220
|
result.updated.push(target);
|
|
7549
8221
|
options.onDiff?.(renderDiff(target, current, content));
|
|
7550
|
-
writes.push({ target, content, replace: true });
|
|
8222
|
+
writes.push({ target, content, replace: true, previous: current });
|
|
7551
8223
|
}
|
|
7552
|
-
const pluginTarget =
|
|
8224
|
+
const pluginTarget = join9(pluginDestination, "index.ts");
|
|
7553
8225
|
const pluginContent = renderPluginEntrypoint(config, pluginDestination);
|
|
7554
8226
|
if (!assertRegularFile(pluginTarget)) {
|
|
7555
8227
|
result.created.push(pluginTarget);
|
|
7556
8228
|
pluginWrite = { target: pluginTarget, content: pluginContent, replace: false };
|
|
7557
8229
|
} else {
|
|
7558
|
-
const current =
|
|
8230
|
+
const current = readFileSync8(pluginTarget, "utf8");
|
|
7559
8231
|
if (current === pluginContent) {
|
|
7560
8232
|
result.unchanged.push(pluginTarget);
|
|
7561
8233
|
} else {
|
|
7562
|
-
if (!current
|
|
8234
|
+
if (!hasGeneratedPluginMarker(current)) {
|
|
7563
8235
|
throw new Error(`Refusing to overwrite a non-generated plugin entrypoint: ${pluginTarget}`);
|
|
7564
8236
|
}
|
|
7565
8237
|
result.updated.push(pluginTarget);
|
|
7566
8238
|
options.onDiff?.(renderDiff(pluginTarget, current, pluginContent));
|
|
7567
|
-
pluginWrite = { target: pluginTarget, content: pluginContent, replace: true };
|
|
8239
|
+
pluginWrite = { target: pluginTarget, content: pluginContent, replace: true, previous: current };
|
|
7568
8240
|
}
|
|
7569
8241
|
}
|
|
7570
8242
|
if (!check) {
|
|
7571
8243
|
const writableDestination = safeDirectory(config.projectRoot, [".opencode", "agents"], true);
|
|
7572
8244
|
const writablePluginDestination = safeDirectory(config.projectRoot, [".opencode", "plugins", "agent-gvozd"], true);
|
|
7573
8245
|
for (const target of result.removed) {
|
|
7574
|
-
if (!assertRegularFile(target)
|
|
8246
|
+
if (!assertRegularFile(target)) {
|
|
7575
8247
|
throw new Error(`Refusing to remove a changed or unsafe agent file: ${target}`);
|
|
7576
8248
|
}
|
|
7577
|
-
|
|
8249
|
+
const current = readFileSync8(target, "utf8");
|
|
8250
|
+
if (!hasGeneratedAgentMarker(current) || current !== removals.get(target)) {
|
|
8251
|
+
throw new Error(`Refusing to remove a concurrently changed agent file: ${target}`);
|
|
8252
|
+
}
|
|
8253
|
+
unlinkSync4(target);
|
|
7578
8254
|
}
|
|
7579
8255
|
for (const write of writes) {
|
|
7580
8256
|
if (!write.replace) {
|
|
7581
|
-
createFile2(
|
|
8257
|
+
createFile2(join9(writableDestination, basename(write.target)), write.content);
|
|
7582
8258
|
continue;
|
|
7583
8259
|
}
|
|
7584
|
-
if (!assertRegularFile(write.target)
|
|
8260
|
+
if (!assertRegularFile(write.target)) {
|
|
7585
8261
|
throw new Error(`Refusing to replace a changed or unsafe agent file: ${write.target}`);
|
|
7586
8262
|
}
|
|
8263
|
+
const current = readFileSync8(write.target, "utf8");
|
|
8264
|
+
if (!hasGeneratedAgentMarker(current) || current !== write.previous) {
|
|
8265
|
+
throw new Error(`Refusing to replace a concurrently changed agent file: ${write.target}`);
|
|
8266
|
+
}
|
|
7587
8267
|
replaceFile2(write.target, write.content);
|
|
7588
8268
|
}
|
|
7589
8269
|
if (pluginWrite) {
|
|
7590
|
-
const target =
|
|
8270
|
+
const target = join9(writablePluginDestination, basename(pluginWrite.target));
|
|
7591
8271
|
if (!pluginWrite.replace) {
|
|
7592
8272
|
createFile2(target, pluginWrite.content);
|
|
7593
8273
|
} else {
|
|
7594
|
-
if (!assertRegularFile(target)
|
|
8274
|
+
if (!assertRegularFile(target)) {
|
|
7595
8275
|
throw new Error(`Refusing to replace a changed or unsafe plugin entrypoint: ${target}`);
|
|
7596
8276
|
}
|
|
8277
|
+
const current = readFileSync8(target, "utf8");
|
|
8278
|
+
if (!hasGeneratedPluginMarker(current) || current !== pluginWrite.previous) {
|
|
8279
|
+
throw new Error(`Refusing to replace a concurrently changed plugin entrypoint: ${target}`);
|
|
8280
|
+
}
|
|
7597
8281
|
replaceFile2(target, pluginWrite.content);
|
|
7598
8282
|
}
|
|
7599
8283
|
}
|
|
8284
|
+
const templateDirectory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], true);
|
|
8285
|
+
for (const write of templateWrites) {
|
|
8286
|
+
const target = join9(templateDirectory, basename(write.target));
|
|
8287
|
+
if (!write.replace)
|
|
8288
|
+
createFile2(target, write.content);
|
|
8289
|
+
else {
|
|
8290
|
+
if (!assertRegularFile(target) || readFileSync8(target, "utf8") !== write.previous) {
|
|
8291
|
+
throw new Error(`Refusing to replace a concurrently changed project schema: ${target}`);
|
|
8292
|
+
}
|
|
8293
|
+
replaceFile2(target, write.content);
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
7600
8296
|
}
|
|
7601
|
-
ensureProjectTemplate(config, check);
|
|
7602
8297
|
if (!check)
|
|
7603
8298
|
safeDirectory(config.projectRoot, ["docs", ".gvozd", "tasks"], true);
|
|
7604
8299
|
return result;
|
|
7605
8300
|
}
|
|
8301
|
+
function syncAgents(config, options = {}) {
|
|
8302
|
+
if (options.check)
|
|
8303
|
+
return syncAgentsUnlocked(config, options);
|
|
8304
|
+
const root = realpathSync4(config.projectRoot);
|
|
8305
|
+
return withExclusiveFileLockSync(join9(root, ".agent-gvozd-sync.lock"), () => syncAgentsUnlocked(config, options), "sync");
|
|
8306
|
+
}
|
|
7606
8307
|
function formatSyncResult(result, check) {
|
|
7607
8308
|
const lines = [check ? "agent-gvozd sync check" : "agent-gvozd sync complete"];
|
|
7608
8309
|
for (const [label, paths] of [
|
|
@@ -7631,8 +8332,23 @@ var promptUI = {
|
|
|
7631
8332
|
intro,
|
|
7632
8333
|
outro
|
|
7633
8334
|
};
|
|
8335
|
+
var HELP = [
|
|
8336
|
+
"Usage: gvozd <setup|config|doctor|sync|trust-project> [options]",
|
|
8337
|
+
"",
|
|
8338
|
+
"Commands:",
|
|
8339
|
+
" setup [--yes] Install or upgrade the global agent team",
|
|
8340
|
+
" config [--yes] Configure model preferences",
|
|
8341
|
+
" doctor [--json] Diagnose the global installation",
|
|
8342
|
+
" sync [--check] Maintain the legacy project-local installation",
|
|
8343
|
+
" trust-project [directory] Print the current project trust token",
|
|
8344
|
+
"",
|
|
8345
|
+
"Options:",
|
|
8346
|
+
" --help Show this help",
|
|
8347
|
+
" --version Show the Gvozd version"
|
|
8348
|
+
].join(`
|
|
8349
|
+
`);
|
|
7634
8350
|
function usage(io) {
|
|
7635
|
-
io.stderr(
|
|
8351
|
+
io.stderr(HELP);
|
|
7636
8352
|
return 2;
|
|
7637
8353
|
}
|
|
7638
8354
|
function parseFlags(args, allowed) {
|
|
@@ -7644,6 +8360,14 @@ function parseFlags(args, allowed) {
|
|
|
7644
8360
|
async function runCli(args, io = defaultIO, commands = { setup: runSetup, configure: runConfigure }) {
|
|
7645
8361
|
const [command, ...rest] = args;
|
|
7646
8362
|
try {
|
|
8363
|
+
if ((command === "--help" || command === "help") && rest.length === 0) {
|
|
8364
|
+
io.stdout(HELP);
|
|
8365
|
+
return 0;
|
|
8366
|
+
}
|
|
8367
|
+
if (command === "--version" && rest.length === 0) {
|
|
8368
|
+
io.stdout(PACKAGE_VERSION);
|
|
8369
|
+
return 0;
|
|
8370
|
+
}
|
|
7647
8371
|
if (command === "setup" || command === "config") {
|
|
7648
8372
|
const parsed = parseFlags(rest, ["--yes"]);
|
|
7649
8373
|
if (!parsed || parsed.positional.length > 0)
|
|
@@ -7672,7 +8396,12 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
|
|
|
7672
8396
|
try {
|
|
7673
8397
|
const client = await (commands.findClient ?? findOpenCode)();
|
|
7674
8398
|
const paths = await client.debugPaths();
|
|
7675
|
-
report = await runDoctor({
|
|
8399
|
+
report = await runDoctor({
|
|
8400
|
+
client,
|
|
8401
|
+
configRoot: paths.config,
|
|
8402
|
+
runtimeConfigRoot: resolveOpenCodeConfigRoot(),
|
|
8403
|
+
cwd: io.cwd()
|
|
8404
|
+
});
|
|
7676
8405
|
} catch (error) {
|
|
7677
8406
|
report = doctorOperationalFailure(error);
|
|
7678
8407
|
}
|
|
@@ -7690,14 +8419,21 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
|
|
|
7690
8419
|
io.stdout(formatSyncResult(result, check));
|
|
7691
8420
|
return check && result.created.length + result.updated.length + result.removed.length > 0 ? 1 : 0;
|
|
7692
8421
|
}
|
|
8422
|
+
if (command === "trust-project") {
|
|
8423
|
+
const parsed = parseFlags(rest, []);
|
|
8424
|
+
if (!parsed || parsed.positional.length > 1)
|
|
8425
|
+
return usage(io);
|
|
8426
|
+
io.stdout(computeProjectTrustToken(parsed.positional[0] ?? io.cwd()));
|
|
8427
|
+
return 0;
|
|
8428
|
+
}
|
|
7693
8429
|
return usage(io);
|
|
7694
8430
|
} catch (error) {
|
|
7695
|
-
io.stderr(
|
|
8431
|
+
io.stderr(redactDiagnostic(error));
|
|
7696
8432
|
return 1;
|
|
7697
8433
|
}
|
|
7698
8434
|
}
|
|
7699
8435
|
var invokedPath = process.argv[1];
|
|
7700
|
-
if (invokedPath &&
|
|
8436
|
+
if (invokedPath && realpathSync5(invokedPath) === realpathSync5(fileURLToPath2(import.meta.url))) {
|
|
7701
8437
|
process.exitCode = await runCli(process.argv.slice(2));
|
|
7702
8438
|
}
|
|
7703
8439
|
export {
|