@nail00749/agent-gvozd 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +918 -27
- package/defaults/agents/front-deep.jsonc +3 -1
- package/defaults/agents/front-fast.jsonc +2 -1
- package/defaults/agents/git.jsonc +1035 -36
- 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/agents/verifier.jsonc +873 -20
- 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/git.md +3 -1
- package/defaults/prompts/master.md +11 -2
- package/defaults/prompts/verifier.md +3 -1
- package/defaults/schema.json +11 -1
- package/dist/cli.js +1453 -258
- package/dist/index.js +758 -307
- 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
|
|
@@ -2463,6 +2462,14 @@ function cleanRegex(source) {
|
|
|
2463
2462
|
const end = source.endsWith("$") ? source.length - 1 : source.length;
|
|
2464
2463
|
return source.slice(start, end);
|
|
2465
2464
|
}
|
|
2465
|
+
function floatSafeRemainder(val, step) {
|
|
2466
|
+
const ratio = val / step;
|
|
2467
|
+
const roundedRatio = Math.round(ratio);
|
|
2468
|
+
const tolerance = Number.EPSILON * Math.max(Math.abs(ratio), 1);
|
|
2469
|
+
if (Math.abs(ratio - roundedRatio) < tolerance)
|
|
2470
|
+
return 0;
|
|
2471
|
+
return ratio - roundedRatio;
|
|
2472
|
+
}
|
|
2466
2473
|
var EVALUATING = /* @__PURE__ */ Symbol("evaluating");
|
|
2467
2474
|
function defineLazy(object, key, getter) {
|
|
2468
2475
|
let value = undefined;
|
|
@@ -3021,6 +3028,7 @@ var string = (params) => {
|
|
|
3021
3028
|
const regex = params ? `[\\s\\S]{${params?.minimum ?? 0},${params?.maximum ?? ""}}` : `[\\s\\S]*`;
|
|
3022
3029
|
return new RegExp(`^${regex}$`);
|
|
3023
3030
|
};
|
|
3031
|
+
var integer = /^-?\d+$/;
|
|
3024
3032
|
var number = /^-?\d+(?:\.\d+)?$/;
|
|
3025
3033
|
var boolean = /^(?:true|false)$/i;
|
|
3026
3034
|
var lowercase = /^[^A-Z]*$/;
|
|
@@ -3033,6 +3041,168 @@ var $ZodCheck = /* @__PURE__ */ $constructor("$ZodCheck", (inst, def) => {
|
|
|
3033
3041
|
inst._zod.def = def;
|
|
3034
3042
|
(_a = inst._zod).onattach ?? (_a.onattach = []);
|
|
3035
3043
|
});
|
|
3044
|
+
var numericOriginMap = {
|
|
3045
|
+
number: "number",
|
|
3046
|
+
bigint: "bigint",
|
|
3047
|
+
object: "date"
|
|
3048
|
+
};
|
|
3049
|
+
var $ZodCheckLessThan = /* @__PURE__ */ $constructor("$ZodCheckLessThan", (inst, def) => {
|
|
3050
|
+
$ZodCheck.init(inst, def);
|
|
3051
|
+
const origin = numericOriginMap[typeof def.value];
|
|
3052
|
+
inst._zod.onattach.push((inst) => {
|
|
3053
|
+
const bag = inst._zod.bag;
|
|
3054
|
+
const curr = (def.inclusive ? bag.maximum : bag.exclusiveMaximum) ?? Number.POSITIVE_INFINITY;
|
|
3055
|
+
if (def.value < curr) {
|
|
3056
|
+
if (def.inclusive)
|
|
3057
|
+
bag.maximum = def.value;
|
|
3058
|
+
else
|
|
3059
|
+
bag.exclusiveMaximum = def.value;
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
inst._zod.check = (payload) => {
|
|
3063
|
+
if (def.inclusive ? payload.value <= def.value : payload.value < def.value) {
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
payload.issues.push({
|
|
3067
|
+
origin,
|
|
3068
|
+
code: "too_big",
|
|
3069
|
+
maximum: typeof def.value === "object" ? def.value.getTime() : def.value,
|
|
3070
|
+
input: payload.value,
|
|
3071
|
+
inclusive: def.inclusive,
|
|
3072
|
+
inst,
|
|
3073
|
+
continue: !def.abort
|
|
3074
|
+
});
|
|
3075
|
+
};
|
|
3076
|
+
});
|
|
3077
|
+
var $ZodCheckGreaterThan = /* @__PURE__ */ $constructor("$ZodCheckGreaterThan", (inst, def) => {
|
|
3078
|
+
$ZodCheck.init(inst, def);
|
|
3079
|
+
const origin = numericOriginMap[typeof def.value];
|
|
3080
|
+
inst._zod.onattach.push((inst) => {
|
|
3081
|
+
const bag = inst._zod.bag;
|
|
3082
|
+
const curr = (def.inclusive ? bag.minimum : bag.exclusiveMinimum) ?? Number.NEGATIVE_INFINITY;
|
|
3083
|
+
if (def.value > curr) {
|
|
3084
|
+
if (def.inclusive)
|
|
3085
|
+
bag.minimum = def.value;
|
|
3086
|
+
else
|
|
3087
|
+
bag.exclusiveMinimum = def.value;
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
inst._zod.check = (payload) => {
|
|
3091
|
+
if (def.inclusive ? payload.value >= def.value : payload.value > def.value) {
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
payload.issues.push({
|
|
3095
|
+
origin,
|
|
3096
|
+
code: "too_small",
|
|
3097
|
+
minimum: typeof def.value === "object" ? def.value.getTime() : def.value,
|
|
3098
|
+
input: payload.value,
|
|
3099
|
+
inclusive: def.inclusive,
|
|
3100
|
+
inst,
|
|
3101
|
+
continue: !def.abort
|
|
3102
|
+
});
|
|
3103
|
+
};
|
|
3104
|
+
});
|
|
3105
|
+
var $ZodCheckMultipleOf = /* @__PURE__ */ $constructor("$ZodCheckMultipleOf", (inst, def) => {
|
|
3106
|
+
$ZodCheck.init(inst, def);
|
|
3107
|
+
inst._zod.onattach.push((inst) => {
|
|
3108
|
+
var _a;
|
|
3109
|
+
(_a = inst._zod.bag).multipleOf ?? (_a.multipleOf = def.value);
|
|
3110
|
+
});
|
|
3111
|
+
inst._zod.check = (payload) => {
|
|
3112
|
+
if (typeof payload.value !== typeof def.value)
|
|
3113
|
+
throw new Error("Cannot mix number and bigint in multiple_of check.");
|
|
3114
|
+
const isMultiple = typeof payload.value === "bigint" ? payload.value % def.value === BigInt(0) : floatSafeRemainder(payload.value, def.value) === 0;
|
|
3115
|
+
if (isMultiple)
|
|
3116
|
+
return;
|
|
3117
|
+
payload.issues.push({
|
|
3118
|
+
origin: typeof payload.value,
|
|
3119
|
+
code: "not_multiple_of",
|
|
3120
|
+
divisor: def.value,
|
|
3121
|
+
input: payload.value,
|
|
3122
|
+
inst,
|
|
3123
|
+
continue: !def.abort
|
|
3124
|
+
});
|
|
3125
|
+
};
|
|
3126
|
+
});
|
|
3127
|
+
var $ZodCheckNumberFormat = /* @__PURE__ */ $constructor("$ZodCheckNumberFormat", (inst, def) => {
|
|
3128
|
+
$ZodCheck.init(inst, def);
|
|
3129
|
+
def.format = def.format || "float64";
|
|
3130
|
+
const isInt = def.format?.includes("int");
|
|
3131
|
+
const origin = isInt ? "int" : "number";
|
|
3132
|
+
const [minimum, maximum] = NUMBER_FORMAT_RANGES[def.format];
|
|
3133
|
+
inst._zod.onattach.push((inst) => {
|
|
3134
|
+
const bag = inst._zod.bag;
|
|
3135
|
+
bag.format = def.format;
|
|
3136
|
+
bag.minimum = minimum;
|
|
3137
|
+
bag.maximum = maximum;
|
|
3138
|
+
if (isInt)
|
|
3139
|
+
bag.pattern = integer;
|
|
3140
|
+
});
|
|
3141
|
+
inst._zod.check = (payload) => {
|
|
3142
|
+
const input = payload.value;
|
|
3143
|
+
if (isInt) {
|
|
3144
|
+
if (!Number.isInteger(input)) {
|
|
3145
|
+
payload.issues.push({
|
|
3146
|
+
expected: origin,
|
|
3147
|
+
format: def.format,
|
|
3148
|
+
code: "invalid_type",
|
|
3149
|
+
continue: false,
|
|
3150
|
+
input,
|
|
3151
|
+
inst
|
|
3152
|
+
});
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
if (!Number.isSafeInteger(input)) {
|
|
3156
|
+
if (input > 0) {
|
|
3157
|
+
payload.issues.push({
|
|
3158
|
+
input,
|
|
3159
|
+
code: "too_big",
|
|
3160
|
+
maximum: Number.MAX_SAFE_INTEGER,
|
|
3161
|
+
note: "Integers must be within the safe integer range.",
|
|
3162
|
+
inst,
|
|
3163
|
+
origin,
|
|
3164
|
+
inclusive: true,
|
|
3165
|
+
continue: !def.abort
|
|
3166
|
+
});
|
|
3167
|
+
} else {
|
|
3168
|
+
payload.issues.push({
|
|
3169
|
+
input,
|
|
3170
|
+
code: "too_small",
|
|
3171
|
+
minimum: Number.MIN_SAFE_INTEGER,
|
|
3172
|
+
note: "Integers must be within the safe integer range.",
|
|
3173
|
+
inst,
|
|
3174
|
+
origin,
|
|
3175
|
+
inclusive: true,
|
|
3176
|
+
continue: !def.abort
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
if (input < minimum) {
|
|
3183
|
+
payload.issues.push({
|
|
3184
|
+
origin: "number",
|
|
3185
|
+
input,
|
|
3186
|
+
code: "too_small",
|
|
3187
|
+
minimum,
|
|
3188
|
+
inclusive: true,
|
|
3189
|
+
inst,
|
|
3190
|
+
continue: !def.abort
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
if (input > maximum) {
|
|
3194
|
+
payload.issues.push({
|
|
3195
|
+
origin: "number",
|
|
3196
|
+
input,
|
|
3197
|
+
code: "too_big",
|
|
3198
|
+
maximum,
|
|
3199
|
+
inclusive: true,
|
|
3200
|
+
inst,
|
|
3201
|
+
continue: !def.abort
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
};
|
|
3205
|
+
});
|
|
3036
3206
|
var $ZodCheckMaxLength = /* @__PURE__ */ $constructor("$ZodCheckMaxLength", (inst, def) => {
|
|
3037
3207
|
var _a;
|
|
3038
3208
|
$ZodCheck.init(inst, def);
|
|
@@ -3715,6 +3885,33 @@ var $ZodJWT = /* @__PURE__ */ $constructor("$ZodJWT", (inst, def) => {
|
|
|
3715
3885
|
});
|
|
3716
3886
|
};
|
|
3717
3887
|
});
|
|
3888
|
+
var $ZodNumber = /* @__PURE__ */ $constructor("$ZodNumber", (inst, def) => {
|
|
3889
|
+
$ZodType.init(inst, def);
|
|
3890
|
+
inst._zod.pattern = inst._zod.bag.pattern ?? number;
|
|
3891
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
3892
|
+
if (def.coerce)
|
|
3893
|
+
try {
|
|
3894
|
+
payload.value = Number(payload.value);
|
|
3895
|
+
} catch (_) {}
|
|
3896
|
+
const input = payload.value;
|
|
3897
|
+
if (typeof input === "number" && !Number.isNaN(input) && Number.isFinite(input)) {
|
|
3898
|
+
return payload;
|
|
3899
|
+
}
|
|
3900
|
+
const received = typeof input === "number" ? Number.isNaN(input) ? "NaN" : !Number.isFinite(input) ? "Infinity" : undefined : undefined;
|
|
3901
|
+
payload.issues.push({
|
|
3902
|
+
expected: "number",
|
|
3903
|
+
code: "invalid_type",
|
|
3904
|
+
input,
|
|
3905
|
+
inst,
|
|
3906
|
+
...received ? { received } : {}
|
|
3907
|
+
});
|
|
3908
|
+
return payload;
|
|
3909
|
+
};
|
|
3910
|
+
});
|
|
3911
|
+
var $ZodNumberFormat = /* @__PURE__ */ $constructor("$ZodNumberFormat", (inst, def) => {
|
|
3912
|
+
$ZodCheckNumberFormat.init(inst, def);
|
|
3913
|
+
$ZodNumber.init(inst, def);
|
|
3914
|
+
});
|
|
3718
3915
|
var $ZodBoolean = /* @__PURE__ */ $constructor("$ZodBoolean", (inst, def) => {
|
|
3719
3916
|
$ZodType.init(inst, def);
|
|
3720
3917
|
inst._zod.pattern = boolean;
|
|
@@ -4929,6 +5126,22 @@ function _isoDuration(Class, params) {
|
|
|
4929
5126
|
...normalizeParams(params)
|
|
4930
5127
|
});
|
|
4931
5128
|
}
|
|
5129
|
+
function _number(Class, params) {
|
|
5130
|
+
return new Class({
|
|
5131
|
+
type: "number",
|
|
5132
|
+
checks: [],
|
|
5133
|
+
...normalizeParams(params)
|
|
5134
|
+
});
|
|
5135
|
+
}
|
|
5136
|
+
function _int(Class, params) {
|
|
5137
|
+
return new Class({
|
|
5138
|
+
type: "number",
|
|
5139
|
+
check: "number_format",
|
|
5140
|
+
abort: false,
|
|
5141
|
+
format: "safeint",
|
|
5142
|
+
...normalizeParams(params)
|
|
5143
|
+
});
|
|
5144
|
+
}
|
|
4932
5145
|
function _boolean(Class, params) {
|
|
4933
5146
|
return new Class({
|
|
4934
5147
|
type: "boolean",
|
|
@@ -4946,6 +5159,45 @@ function _never(Class, params) {
|
|
|
4946
5159
|
...normalizeParams(params)
|
|
4947
5160
|
});
|
|
4948
5161
|
}
|
|
5162
|
+
function _lt(value, params) {
|
|
5163
|
+
return new $ZodCheckLessThan({
|
|
5164
|
+
check: "less_than",
|
|
5165
|
+
...normalizeParams(params),
|
|
5166
|
+
value,
|
|
5167
|
+
inclusive: false
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
function _lte(value, params) {
|
|
5171
|
+
return new $ZodCheckLessThan({
|
|
5172
|
+
check: "less_than",
|
|
5173
|
+
...normalizeParams(params),
|
|
5174
|
+
value,
|
|
5175
|
+
inclusive: true
|
|
5176
|
+
});
|
|
5177
|
+
}
|
|
5178
|
+
function _gt(value, params) {
|
|
5179
|
+
return new $ZodCheckGreaterThan({
|
|
5180
|
+
check: "greater_than",
|
|
5181
|
+
...normalizeParams(params),
|
|
5182
|
+
value,
|
|
5183
|
+
inclusive: false
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
5186
|
+
function _gte(value, params) {
|
|
5187
|
+
return new $ZodCheckGreaterThan({
|
|
5188
|
+
check: "greater_than",
|
|
5189
|
+
...normalizeParams(params),
|
|
5190
|
+
value,
|
|
5191
|
+
inclusive: true
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
function _multipleOf(value, params) {
|
|
5195
|
+
return new $ZodCheckMultipleOf({
|
|
5196
|
+
check: "multiple_of",
|
|
5197
|
+
...normalizeParams(params),
|
|
5198
|
+
value
|
|
5199
|
+
});
|
|
5200
|
+
}
|
|
4949
5201
|
function _maxLength(maximum, params) {
|
|
4950
5202
|
const ch = new $ZodCheckMaxLength({
|
|
4951
5203
|
check: "max_length",
|
|
@@ -5471,6 +5723,39 @@ var stringProcessor = (schema, ctx, _json, _params) => {
|
|
|
5471
5723
|
}
|
|
5472
5724
|
}
|
|
5473
5725
|
};
|
|
5726
|
+
var numberProcessor = (schema, ctx, _json, _params) => {
|
|
5727
|
+
const json = _json;
|
|
5728
|
+
const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
|
|
5729
|
+
if (typeof format === "string" && format.includes("int"))
|
|
5730
|
+
json.type = "integer";
|
|
5731
|
+
else
|
|
5732
|
+
json.type = "number";
|
|
5733
|
+
const exMin = typeof exclusiveMinimum === "number" && exclusiveMinimum >= (minimum ?? Number.NEGATIVE_INFINITY);
|
|
5734
|
+
const exMax = typeof exclusiveMaximum === "number" && exclusiveMaximum <= (maximum ?? Number.POSITIVE_INFINITY);
|
|
5735
|
+
const legacy = ctx.target === "draft-04" || ctx.target === "openapi-3.0";
|
|
5736
|
+
if (exMin) {
|
|
5737
|
+
if (legacy) {
|
|
5738
|
+
json.minimum = exclusiveMinimum;
|
|
5739
|
+
json.exclusiveMinimum = true;
|
|
5740
|
+
} else {
|
|
5741
|
+
json.exclusiveMinimum = exclusiveMinimum;
|
|
5742
|
+
}
|
|
5743
|
+
} else if (typeof minimum === "number") {
|
|
5744
|
+
json.minimum = minimum;
|
|
5745
|
+
}
|
|
5746
|
+
if (exMax) {
|
|
5747
|
+
if (legacy) {
|
|
5748
|
+
json.maximum = exclusiveMaximum;
|
|
5749
|
+
json.exclusiveMaximum = true;
|
|
5750
|
+
} else {
|
|
5751
|
+
json.exclusiveMaximum = exclusiveMaximum;
|
|
5752
|
+
}
|
|
5753
|
+
} else if (typeof maximum === "number") {
|
|
5754
|
+
json.maximum = maximum;
|
|
5755
|
+
}
|
|
5756
|
+
if (typeof multipleOf === "number")
|
|
5757
|
+
json.multipleOf = multipleOf;
|
|
5758
|
+
};
|
|
5474
5759
|
var booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
5475
5760
|
json.type = "boolean";
|
|
5476
5761
|
};
|
|
@@ -6094,6 +6379,74 @@ var ZodJWT = /* @__PURE__ */ $constructor("ZodJWT", (inst, def) => {
|
|
|
6094
6379
|
$ZodJWT.init(inst, def);
|
|
6095
6380
|
ZodStringFormat.init(inst, def);
|
|
6096
6381
|
});
|
|
6382
|
+
var ZodNumber = /* @__PURE__ */ $constructor("ZodNumber", (inst, def) => {
|
|
6383
|
+
$ZodNumber.init(inst, def);
|
|
6384
|
+
ZodType.init(inst, def);
|
|
6385
|
+
inst._zod.processJSONSchema = (ctx, json, params) => numberProcessor(inst, ctx, json, params);
|
|
6386
|
+
_installLazyMethods(inst, "ZodNumber", {
|
|
6387
|
+
gt(value, params) {
|
|
6388
|
+
return this.check(_gt(value, params));
|
|
6389
|
+
},
|
|
6390
|
+
gte(value, params) {
|
|
6391
|
+
return this.check(_gte(value, params));
|
|
6392
|
+
},
|
|
6393
|
+
min(value, params) {
|
|
6394
|
+
return this.check(_gte(value, params));
|
|
6395
|
+
},
|
|
6396
|
+
lt(value, params) {
|
|
6397
|
+
return this.check(_lt(value, params));
|
|
6398
|
+
},
|
|
6399
|
+
lte(value, params) {
|
|
6400
|
+
return this.check(_lte(value, params));
|
|
6401
|
+
},
|
|
6402
|
+
max(value, params) {
|
|
6403
|
+
return this.check(_lte(value, params));
|
|
6404
|
+
},
|
|
6405
|
+
int(params) {
|
|
6406
|
+
return this.check(int(params));
|
|
6407
|
+
},
|
|
6408
|
+
safe(params) {
|
|
6409
|
+
return this.check(int(params));
|
|
6410
|
+
},
|
|
6411
|
+
positive(params) {
|
|
6412
|
+
return this.check(_gt(0, params));
|
|
6413
|
+
},
|
|
6414
|
+
nonnegative(params) {
|
|
6415
|
+
return this.check(_gte(0, params));
|
|
6416
|
+
},
|
|
6417
|
+
negative(params) {
|
|
6418
|
+
return this.check(_lt(0, params));
|
|
6419
|
+
},
|
|
6420
|
+
nonpositive(params) {
|
|
6421
|
+
return this.check(_lte(0, params));
|
|
6422
|
+
},
|
|
6423
|
+
multipleOf(value, params) {
|
|
6424
|
+
return this.check(_multipleOf(value, params));
|
|
6425
|
+
},
|
|
6426
|
+
step(value, params) {
|
|
6427
|
+
return this.check(_multipleOf(value, params));
|
|
6428
|
+
},
|
|
6429
|
+
finite() {
|
|
6430
|
+
return this;
|
|
6431
|
+
}
|
|
6432
|
+
});
|
|
6433
|
+
const bag = inst._zod.bag;
|
|
6434
|
+
inst.minValue = Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null;
|
|
6435
|
+
inst.maxValue = Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null;
|
|
6436
|
+
inst.isInt = (bag.format ?? "").includes("int") || Number.isSafeInteger(bag.multipleOf ?? 0.5);
|
|
6437
|
+
inst.isFinite = true;
|
|
6438
|
+
inst.format = bag.format ?? null;
|
|
6439
|
+
});
|
|
6440
|
+
function number2(params) {
|
|
6441
|
+
return _number(ZodNumber, params);
|
|
6442
|
+
}
|
|
6443
|
+
var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def) => {
|
|
6444
|
+
$ZodNumberFormat.init(inst, def);
|
|
6445
|
+
ZodNumber.init(inst, def);
|
|
6446
|
+
});
|
|
6447
|
+
function int(params) {
|
|
6448
|
+
return _int(ZodNumberFormat, params);
|
|
6449
|
+
}
|
|
6097
6450
|
var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
|
|
6098
6451
|
$ZodBoolean.init(inst, def);
|
|
6099
6452
|
ZodType.init(inst, def);
|
|
@@ -6466,12 +6819,184 @@ function refine(fn, _params = {}) {
|
|
|
6466
6819
|
function superRefine(fn, params) {
|
|
6467
6820
|
return _superRefine(fn, params);
|
|
6468
6821
|
}
|
|
6822
|
+
// src/config-root.ts
|
|
6823
|
+
import { homedir } from "node:os";
|
|
6824
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6825
|
+
var GVOZD_CONFIG_ROOT_ENV = "GVOZD_OPENCODE_CONFIG_ROOT";
|
|
6826
|
+
function absolute(path, label) {
|
|
6827
|
+
if (!isAbsolute(path))
|
|
6828
|
+
throw new Error(`${label} must be an absolute path`);
|
|
6829
|
+
return resolve(path);
|
|
6830
|
+
}
|
|
6831
|
+
function resolveOpenCodeConfigRootContract(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
|
|
6832
|
+
if (explicitRoot)
|
|
6833
|
+
return { path: absolute(explicitRoot, "OpenCode config root"), source: "explicit" };
|
|
6834
|
+
const override = env[GVOZD_CONFIG_ROOT_ENV];
|
|
6835
|
+
if (override) {
|
|
6836
|
+
return { path: absolute(override, GVOZD_CONFIG_ROOT_ENV), source: GVOZD_CONFIG_ROOT_ENV };
|
|
6837
|
+
}
|
|
6838
|
+
if (env.XDG_CONFIG_HOME) {
|
|
6839
|
+
return { path: join(absolute(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME"), "opencode"), source: "XDG_CONFIG_HOME" };
|
|
6840
|
+
}
|
|
6841
|
+
if (platform === "win32" && env.APPDATA) {
|
|
6842
|
+
return { path: join(env.APPDATA, "opencode"), source: "APPDATA" };
|
|
6843
|
+
}
|
|
6844
|
+
return { path: join(home, ".config", "opencode"), source: "platform-default" };
|
|
6845
|
+
}
|
|
6846
|
+
function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
|
|
6847
|
+
return resolveOpenCodeConfigRootContract(env, platform, home, explicitRoot).path;
|
|
6848
|
+
}
|
|
6849
|
+
|
|
6850
|
+
// src/project-trust.ts
|
|
6851
|
+
import { createHash } from "node:crypto";
|
|
6852
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
6853
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "node:path";
|
|
6854
|
+
var PROJECT_TRUST_ENV = "GVOZD_TRUST_PROJECT_CONFIG";
|
|
6855
|
+
function findRoot(start) {
|
|
6856
|
+
let current = resolve2(start);
|
|
6857
|
+
while (true) {
|
|
6858
|
+
if (existsSync(join2(current, ".git")))
|
|
6859
|
+
return realpathSync(current);
|
|
6860
|
+
const parent = dirname(current);
|
|
6861
|
+
if (parent === current)
|
|
6862
|
+
return realpathSync(resolve2(start));
|
|
6863
|
+
current = parent;
|
|
6864
|
+
}
|
|
6865
|
+
}
|
|
6866
|
+
function within(base, target, label) {
|
|
6867
|
+
const child = relative(base, target);
|
|
6868
|
+
if (child === "" || !child.startsWith("..") && !isAbsolute2(child))
|
|
6869
|
+
return;
|
|
6870
|
+
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
6871
|
+
}
|
|
6872
|
+
function rejectSymlinkComponents(base, target, label) {
|
|
6873
|
+
within(base, target, label);
|
|
6874
|
+
const segments = relative(base, target).split(/[\\/]/).filter(Boolean);
|
|
6875
|
+
let current = base;
|
|
6876
|
+
for (const segment of segments) {
|
|
6877
|
+
current = join2(current, segment);
|
|
6878
|
+
if (!existsSync(current))
|
|
6879
|
+
continue;
|
|
6880
|
+
if (lstatSync(current).isSymbolicLink())
|
|
6881
|
+
throw new Error(`${label} must not contain symlink components: ${current}`);
|
|
6882
|
+
}
|
|
6883
|
+
}
|
|
6884
|
+
function readJsonc(path) {
|
|
6885
|
+
const bytes = readFileSync(path);
|
|
6886
|
+
const errors = [];
|
|
6887
|
+
const value = parse2(bytes.toString("utf8"), errors, { allowTrailingComma: true, disallowComments: false });
|
|
6888
|
+
if (errors.length > 0 || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
6889
|
+
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
6890
|
+
throw new Error(`Invalid JSONC in ${path}${details ? `: ${details}` : ""}`);
|
|
6891
|
+
}
|
|
6892
|
+
return { value, bytes };
|
|
6893
|
+
}
|
|
6894
|
+
function promptFromPatch(patch) {
|
|
6895
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch))
|
|
6896
|
+
return;
|
|
6897
|
+
const prompt = patch.prompt;
|
|
6898
|
+
return typeof prompt === "string" && prompt.length > 0 ? prompt : undefined;
|
|
6899
|
+
}
|
|
6900
|
+
function collectProjectTrustInputs(projectDirectory) {
|
|
6901
|
+
const canonicalRoot = findRoot(projectDirectory);
|
|
6902
|
+
const layer = join2(canonicalRoot, "docs", ".gvozd");
|
|
6903
|
+
const rootPath = join2(layer, "config.jsonc");
|
|
6904
|
+
if (!existsSync(rootPath))
|
|
6905
|
+
return {
|
|
6906
|
+
canonicalRoot,
|
|
6907
|
+
inputs: [{ identity: "docs/.gvozd/config.jsonc:<missing>", bytes: Buffer.alloc(0) }]
|
|
6908
|
+
};
|
|
6909
|
+
rejectSymlinkComponents(canonicalRoot, rootPath, "Project config path");
|
|
6910
|
+
const rootStat = lstatSync(rootPath);
|
|
6911
|
+
if (!rootStat.isFile() || rootStat.isSymbolicLink())
|
|
6912
|
+
throw new Error(`Project config must be a regular non-symlink file: ${rootPath}`);
|
|
6913
|
+
const root = readJsonc(rootPath);
|
|
6914
|
+
const files = new Map([[relative(canonicalRoot, rootPath).replaceAll("\\", "/"), root.bytes]]);
|
|
6915
|
+
const prompts = [];
|
|
6916
|
+
const inlineAgents = root.value.agents;
|
|
6917
|
+
if (inlineAgents && typeof inlineAgents === "object" && !Array.isArray(inlineAgents)) {
|
|
6918
|
+
for (const patch of Object.values(inlineAgents)) {
|
|
6919
|
+
const prompt = promptFromPatch(patch);
|
|
6920
|
+
if (prompt)
|
|
6921
|
+
prompts.push({ source: rootPath, value: prompt });
|
|
6922
|
+
}
|
|
6923
|
+
}
|
|
6924
|
+
const configuredDirectory = root.value.agentsDirectory;
|
|
6925
|
+
if (configuredDirectory !== undefined && (typeof configuredDirectory !== "string" || configuredDirectory.length === 0)) {
|
|
6926
|
+
throw new Error(`Invalid agentsDirectory in ${rootPath}`);
|
|
6927
|
+
}
|
|
6928
|
+
const agentsDirectory = resolve2(layer, configuredDirectory ?? "agents");
|
|
6929
|
+
within(layer, agentsDirectory, "agentsDirectory");
|
|
6930
|
+
if (existsSync(agentsDirectory)) {
|
|
6931
|
+
rejectSymlinkComponents(canonicalRoot, agentsDirectory, "agentsDirectory");
|
|
6932
|
+
const stat = lstatSync(agentsDirectory);
|
|
6933
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
6934
|
+
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
6935
|
+
within(realpathSync(layer), realpathSync(agentsDirectory), "agentsDirectory");
|
|
6936
|
+
for (const entry of readdirSync(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
6937
|
+
if (!entry.name.endsWith(".jsonc"))
|
|
6938
|
+
continue;
|
|
6939
|
+
const path = join2(agentsDirectory, entry.name);
|
|
6940
|
+
if (!entry.isFile())
|
|
6941
|
+
throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
|
|
6942
|
+
rejectSymlinkComponents(canonicalRoot, path, "Agent fragment path");
|
|
6943
|
+
const stat = lstatSync(path);
|
|
6944
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
6945
|
+
throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
|
|
6946
|
+
const fragment = readJsonc(path);
|
|
6947
|
+
files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), fragment.bytes);
|
|
6948
|
+
const prompt = promptFromPatch(fragment.value);
|
|
6949
|
+
if (prompt)
|
|
6950
|
+
prompts.push({ source: path, value: prompt });
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
for (const prompt of prompts) {
|
|
6954
|
+
const path = resolve2(dirname(prompt.source), prompt.value);
|
|
6955
|
+
within(layer, path, "Agent prompt");
|
|
6956
|
+
rejectSymlinkComponents(canonicalRoot, path, "Agent prompt");
|
|
6957
|
+
if (!existsSync(path))
|
|
6958
|
+
throw new Error(`Agent prompt is missing: ${path}`);
|
|
6959
|
+
const stat = lstatSync(path);
|
|
6960
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
6961
|
+
throw new Error(`Agent prompt must be a regular non-symlink file: ${path}`);
|
|
6962
|
+
const canonical = realpathSync(path);
|
|
6963
|
+
within(realpathSync(layer), canonical, "Agent prompt");
|
|
6964
|
+
files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), readFileSync(path));
|
|
6965
|
+
}
|
|
6966
|
+
return {
|
|
6967
|
+
canonicalRoot,
|
|
6968
|
+
inputs: [...files].map(([identity, bytes]) => ({ identity, bytes })).sort((left, right) => left.identity.localeCompare(right.identity))
|
|
6969
|
+
};
|
|
6970
|
+
}
|
|
6971
|
+
function computeProjectTrustToken(projectDirectory) {
|
|
6972
|
+
const collected = collectProjectTrustInputs(projectDirectory);
|
|
6973
|
+
const hash = createHash("sha256");
|
|
6974
|
+
hash.update("agent-gvozd-project-trust-v1\x00");
|
|
6975
|
+
hash.update(collected.canonicalRoot);
|
|
6976
|
+
hash.update("\x00");
|
|
6977
|
+
for (const input of collected.inputs) {
|
|
6978
|
+
hash.update(String(Buffer.byteLength(input.identity)));
|
|
6979
|
+
hash.update(":");
|
|
6980
|
+
hash.update(input.identity);
|
|
6981
|
+
hash.update(":");
|
|
6982
|
+
hash.update(String(input.bytes.length));
|
|
6983
|
+
hash.update(":");
|
|
6984
|
+
hash.update(input.bytes);
|
|
6985
|
+
hash.update("\x00");
|
|
6986
|
+
}
|
|
6987
|
+
return `sha256:${hash.digest("hex")}`;
|
|
6988
|
+
}
|
|
6989
|
+
|
|
6990
|
+
// src/file-leases.ts
|
|
6991
|
+
var DEFAULT_RESERVATION_TTL_MS = 5 * 60 * 1000;
|
|
6992
|
+
var DEFAULT_ACTIVE_TTL_MS = 30 * 60 * 1000;
|
|
6993
|
+
|
|
6469
6994
|
// src/config.ts
|
|
6470
6995
|
var permissionSchema = object({
|
|
6471
6996
|
action: string2().min(1),
|
|
6472
6997
|
resource: string2().min(1),
|
|
6473
6998
|
effect: _enum(["allow", "ask", "deny"])
|
|
6474
|
-
});
|
|
6999
|
+
}).strict();
|
|
6475
7000
|
var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
|
|
6476
7001
|
var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
|
|
6477
7002
|
var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
|
|
@@ -6485,12 +7010,18 @@ var agentPatchSchema = object({
|
|
|
6485
7010
|
permissions: array(permissionSchema).optional(),
|
|
6486
7011
|
fileLease: fileLeaseRoleSchema.optional(),
|
|
6487
7012
|
disabled: boolean2().optional()
|
|
6488
|
-
});
|
|
7013
|
+
}).strict();
|
|
7014
|
+
var leaseSchema = object({
|
|
7015
|
+
reservationTtlMinutes: number2().int().positive().max(24 * 60).optional(),
|
|
7016
|
+
activeTtlMinutes: number2().int().positive().max(24 * 60).optional()
|
|
7017
|
+
}).strict();
|
|
6489
7018
|
var rootPatchSchema = object({
|
|
7019
|
+
$schema: string2().min(1).optional(),
|
|
6490
7020
|
defaultAgent: agentIdSchema.optional(),
|
|
6491
7021
|
agentsDirectory: string2().min(1).optional(),
|
|
6492
|
-
agents: record(agentIdSchema, agentPatchSchema).optional()
|
|
6493
|
-
|
|
7022
|
+
agents: record(agentIdSchema, agentPatchSchema).optional(),
|
|
7023
|
+
lease: leaseSchema.optional()
|
|
7024
|
+
}).strict();
|
|
6494
7025
|
var resolvedAgentSchema = agentPatchSchema.extend({
|
|
6495
7026
|
description: string2().min(1),
|
|
6496
7027
|
mode: _enum(["primary", "subagent", "all"]),
|
|
@@ -6502,9 +7033,18 @@ var resolvedAgentSchema = agentPatchSchema.extend({
|
|
|
6502
7033
|
fileLease: fileLeaseRoleSchema,
|
|
6503
7034
|
disabled: boolean2()
|
|
6504
7035
|
});
|
|
6505
|
-
|
|
7036
|
+
var SAFE_UNTRUSTED_AGENT_FIELDS = new Set(["description"]);
|
|
7037
|
+
function assertTrustedProjectPatch(patch, sourcePath, id, trusted, knownAgents) {
|
|
7038
|
+
if (trusted)
|
|
7039
|
+
return;
|
|
7040
|
+
const fields = Object.keys(patch).filter((field) => !SAFE_UNTRUSTED_AGENT_FIELDS.has(field));
|
|
7041
|
+
if (fields.length === 0 && knownAgents.has(id))
|
|
7042
|
+
return;
|
|
7043
|
+
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.`);
|
|
7044
|
+
}
|
|
7045
|
+
function readJsonc2(path) {
|
|
6506
7046
|
const errors = [];
|
|
6507
|
-
const value = parse2(
|
|
7047
|
+
const value = parse2(readFileSync2(path, "utf8"), errors, {
|
|
6508
7048
|
allowTrailingComma: true,
|
|
6509
7049
|
disallowComments: false
|
|
6510
7050
|
});
|
|
@@ -6515,51 +7055,69 @@ function readJsonc(path) {
|
|
|
6515
7055
|
return value;
|
|
6516
7056
|
}
|
|
6517
7057
|
function assertWithin(base, target, label) {
|
|
6518
|
-
const child =
|
|
6519
|
-
if (child === "" || !child.startsWith("..") && !
|
|
7058
|
+
const child = relative2(base, target);
|
|
7059
|
+
if (child === "" || !child.startsWith("..") && !isAbsolute3(child))
|
|
6520
7060
|
return;
|
|
6521
7061
|
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
6522
7062
|
}
|
|
6523
7063
|
function resolvePrompt(patch, sourcePath, layerDirectory) {
|
|
6524
7064
|
if (!patch.prompt)
|
|
6525
7065
|
return patch;
|
|
6526
|
-
const prompt =
|
|
7066
|
+
const prompt = resolve3(dirname2(sourcePath), patch.prompt);
|
|
6527
7067
|
assertWithin(layerDirectory, prompt, "Agent prompt");
|
|
6528
|
-
if (!
|
|
7068
|
+
if (!existsSync2(prompt))
|
|
6529
7069
|
throw new Error(`Agent prompt is missing: ${prompt}`);
|
|
6530
|
-
const
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
const
|
|
6536
|
-
|
|
7070
|
+
const promptStat = lstatSync2(prompt);
|
|
7071
|
+
if (promptStat.isSymbolicLink() || !promptStat.isFile())
|
|
7072
|
+
throw new Error(`Agent prompt must be a regular non-symlink file: ${prompt}`);
|
|
7073
|
+
const canonical = realpathSync2(prompt);
|
|
7074
|
+
assertWithin(realpathSync2(layerDirectory), canonical, "Agent prompt");
|
|
7075
|
+
const promptContent = readFileSync2(canonical, "utf8");
|
|
7076
|
+
return { ...patch, prompt: canonical, promptContent };
|
|
7077
|
+
}
|
|
7078
|
+
function loadLayer(directory, rootFileName, required, projectPolicy) {
|
|
7079
|
+
const rootPath = join3(directory, rootFileName);
|
|
7080
|
+
if (!existsSync2(rootPath)) {
|
|
6537
7081
|
if (required)
|
|
6538
7082
|
throw new Error(`Required config is missing: ${rootPath}`);
|
|
6539
7083
|
return { agents: {}, sources: [] };
|
|
6540
7084
|
}
|
|
6541
|
-
const root = rootPatchSchema.parse(
|
|
7085
|
+
const root = rootPatchSchema.parse(readJsonc2(rootPath));
|
|
7086
|
+
if (projectPolicy && !projectPolicy.trusted) {
|
|
7087
|
+
const restricted = ["defaultAgent", "agentsDirectory", "lease"].filter((field) => Object.prototype.hasOwnProperty.call(root, field));
|
|
7088
|
+
if (restricted.length > 0)
|
|
7089
|
+
throw new Error(`Untrusted project config ${rootPath} cannot override ${restricted.join(", ")}`);
|
|
7090
|
+
}
|
|
6542
7091
|
const agents = {};
|
|
6543
7092
|
for (const [id, patch] of Object.entries(root.agents ?? {})) {
|
|
7093
|
+
if (projectPolicy)
|
|
7094
|
+
assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6544
7095
|
agents[id] = resolvePrompt(patch, rootPath, directory);
|
|
6545
7096
|
}
|
|
6546
|
-
const agentsDirectory =
|
|
7097
|
+
const agentsDirectory = resolve3(directory, root.agentsDirectory ?? "agents");
|
|
6547
7098
|
assertWithin(directory, agentsDirectory, "agentsDirectory");
|
|
6548
|
-
if (
|
|
6549
|
-
|
|
6550
|
-
|
|
7099
|
+
if (existsSync2(agentsDirectory)) {
|
|
7100
|
+
const directoryStat = lstatSync2(agentsDirectory);
|
|
7101
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
7102
|
+
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
7103
|
+
}
|
|
7104
|
+
assertWithin(realpathSync2(directory), realpathSync2(agentsDirectory), "agentsDirectory");
|
|
7105
|
+
for (const entry of readdirSync2(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6551
7106
|
if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
|
|
6552
7107
|
continue;
|
|
6553
7108
|
const id = entry.name.slice(0, -".jsonc".length);
|
|
6554
7109
|
agentIdSchema.parse(id);
|
|
6555
|
-
const agentPath =
|
|
6556
|
-
const patch = agentPatchSchema.parse(
|
|
7110
|
+
const agentPath = join3(agentsDirectory, entry.name);
|
|
7111
|
+
const patch = agentPatchSchema.parse(readJsonc2(agentPath));
|
|
7112
|
+
if (projectPolicy)
|
|
7113
|
+
assertTrustedProjectPatch(patch, agentPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6557
7114
|
agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
|
|
6558
7115
|
}
|
|
6559
7116
|
}
|
|
6560
7117
|
return {
|
|
6561
7118
|
defaultAgent: root.defaultAgent,
|
|
6562
7119
|
agents,
|
|
7120
|
+
lease: root.lease,
|
|
6563
7121
|
sources: [rootPath]
|
|
6564
7122
|
};
|
|
6565
7123
|
}
|
|
@@ -6567,22 +7125,24 @@ function mergeAgent(base, override) {
|
|
|
6567
7125
|
return { ...base ?? {}, ...override };
|
|
6568
7126
|
}
|
|
6569
7127
|
function resolveAgentConfig(patch) {
|
|
6570
|
-
const parsed = agentPatchSchema.parse(patch);
|
|
6571
|
-
|
|
7128
|
+
const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
|
|
7129
|
+
const { promptContent, ...agentPatch } = parsed;
|
|
7130
|
+
const resolved = resolvedAgentSchema.parse({
|
|
6572
7131
|
skills: [],
|
|
6573
7132
|
mcp: [],
|
|
6574
7133
|
permissions: [],
|
|
6575
7134
|
fileLease: "readonly",
|
|
6576
7135
|
disabled: false,
|
|
6577
|
-
...
|
|
7136
|
+
...agentPatch
|
|
6578
7137
|
});
|
|
7138
|
+
return promptContent === undefined ? resolved : { ...resolved, promptContent };
|
|
6579
7139
|
}
|
|
6580
7140
|
function findPackageRoot() {
|
|
6581
|
-
let current =
|
|
7141
|
+
let current = dirname2(fileURLToPath(import.meta.url));
|
|
6582
7142
|
while (true) {
|
|
6583
|
-
if (
|
|
7143
|
+
if (existsSync2(join3(current, "defaults", "default.jsonc")))
|
|
6584
7144
|
return current;
|
|
6585
|
-
const parent =
|
|
7145
|
+
const parent = dirname2(current);
|
|
6586
7146
|
if (parent === current)
|
|
6587
7147
|
break;
|
|
6588
7148
|
current = parent;
|
|
@@ -6590,34 +7150,49 @@ function findPackageRoot() {
|
|
|
6590
7150
|
throw new Error("Unable to locate agent-gvozd package defaults");
|
|
6591
7151
|
}
|
|
6592
7152
|
function findProjectRoot(start) {
|
|
6593
|
-
let current =
|
|
7153
|
+
let current = resolve3(start);
|
|
6594
7154
|
while (true) {
|
|
6595
|
-
const git =
|
|
6596
|
-
if (
|
|
7155
|
+
const git = join3(current, ".git");
|
|
7156
|
+
if (existsSync2(git))
|
|
6597
7157
|
return current;
|
|
6598
|
-
const parent =
|
|
7158
|
+
const parent = dirname2(current);
|
|
6599
7159
|
if (parent === current)
|
|
6600
|
-
return
|
|
7160
|
+
return resolve3(start);
|
|
6601
7161
|
current = parent;
|
|
6602
7162
|
}
|
|
6603
7163
|
}
|
|
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
7164
|
function loadConfig(projectDirectory, options = {}) {
|
|
6612
7165
|
const projectRoot = findProjectRoot(projectDirectory);
|
|
6613
7166
|
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)]
|
|
7167
|
+
const projectConfigDirectory = join3(projectRoot, "docs", ".gvozd");
|
|
7168
|
+
const globalConfigDirectory = join3(resolveOpenCodeConfigRoot(options.env, options.platform, options.home, options.configRoot), "gvozd");
|
|
7169
|
+
const baseLayers = [
|
|
7170
|
+
loadLayer(join3(packageRoot, "defaults"), "default.jsonc", true),
|
|
7171
|
+
loadLayer(globalConfigDirectory, "config.jsonc", false)
|
|
6620
7172
|
];
|
|
7173
|
+
const knownAgents = new Set(baseLayers.flatMap((layer) => Object.keys(layer.agents)));
|
|
7174
|
+
const includeProject = options.includeProject !== false;
|
|
7175
|
+
const suppliedToken = includeProject ? options.projectTrustToken ?? (options.env ?? process.env)[PROJECT_TRUST_ENV] : undefined;
|
|
7176
|
+
const trustProjectConfig = includeProject && suppliedToken !== undefined && suppliedToken === computeProjectTrustToken(projectRoot);
|
|
7177
|
+
const projectLayer = includeProject ? loadLayer(projectConfigDirectory, "config.jsonc", false, { trusted: trustProjectConfig, knownAgents }) : undefined;
|
|
7178
|
+
if (trustProjectConfig && suppliedToken !== computeProjectTrustToken(projectRoot)) {
|
|
7179
|
+
throw new Error("Project configuration changed while its trust token was being validated; review it and compute a new token");
|
|
7180
|
+
}
|
|
7181
|
+
const layers = [...baseLayers, ...projectLayer ? [projectLayer] : []];
|
|
7182
|
+
const lease = {
|
|
7183
|
+
reservationTtlMs: DEFAULT_RESERVATION_TTL_MS,
|
|
7184
|
+
activeTtlMs: DEFAULT_ACTIVE_TTL_MS
|
|
7185
|
+
};
|
|
7186
|
+
for (const layer of layers) {
|
|
7187
|
+
if (!layer.lease)
|
|
7188
|
+
continue;
|
|
7189
|
+
if (layer.lease.reservationTtlMinutes !== undefined) {
|
|
7190
|
+
lease.reservationTtlMs = layer.lease.reservationTtlMinutes * 60000;
|
|
7191
|
+
}
|
|
7192
|
+
if (layer.lease.activeTtlMinutes !== undefined) {
|
|
7193
|
+
lease.activeTtlMs = layer.lease.activeTtlMinutes * 60000;
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
6621
7196
|
let defaultAgent;
|
|
6622
7197
|
const agents = {};
|
|
6623
7198
|
for (const layer of layers) {
|
|
@@ -6630,7 +7205,12 @@ function loadConfig(projectDirectory, options = {}) {
|
|
|
6630
7205
|
throw new Error("defaultAgent is not configured");
|
|
6631
7206
|
const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
|
|
6632
7207
|
id,
|
|
6633
|
-
|
|
7208
|
+
(() => {
|
|
7209
|
+
const agent = resolveAgentConfig(patch);
|
|
7210
|
+
if (agent.promptContent === undefined)
|
|
7211
|
+
throw new Error(`Agent prompt snapshot is missing after configuration load: ${agent.prompt}`);
|
|
7212
|
+
return agent;
|
|
7213
|
+
})()
|
|
6634
7214
|
]));
|
|
6635
7215
|
const defaultConfig = resolvedAgents[defaultAgent];
|
|
6636
7216
|
if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
|
|
@@ -6639,6 +7219,7 @@ function loadConfig(projectDirectory, options = {}) {
|
|
|
6639
7219
|
return {
|
|
6640
7220
|
defaultAgent,
|
|
6641
7221
|
agents: resolvedAgents,
|
|
7222
|
+
lease,
|
|
6642
7223
|
packageRoot,
|
|
6643
7224
|
projectRoot,
|
|
6644
7225
|
projectConfigDirectory,
|
|
@@ -6648,15 +7229,157 @@ function loadConfig(projectDirectory, options = {}) {
|
|
|
6648
7229
|
}
|
|
6649
7230
|
|
|
6650
7231
|
// src/cli/doctor.ts
|
|
6651
|
-
import { lstatSync, readFileSync as readFileSync3 } from "node:fs";
|
|
6652
|
-
import { join as
|
|
7232
|
+
import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
|
|
7233
|
+
import { join as join4, resolve as resolve4 } from "node:path";
|
|
6653
7234
|
|
|
6654
|
-
// src/
|
|
6655
|
-
|
|
7235
|
+
// src/tool-permissions.ts
|
|
7236
|
+
function family(command, ...variants) {
|
|
7237
|
+
return [command, ...variants].map((entry) => ({
|
|
7238
|
+
exact: entry,
|
|
7239
|
+
wildcard: `${entry} *`
|
|
7240
|
+
}));
|
|
7241
|
+
}
|
|
7242
|
+
var INSPECTION_COMMANDS = [
|
|
7243
|
+
...family("pwd", "true", "test"),
|
|
7244
|
+
...family("cat", "head", "tail", "wc", "sort", "uniq"),
|
|
7245
|
+
...family("grep", "rg", "find", "diff", "cmp"),
|
|
7246
|
+
...family("ls", "du", "df", "stat", "file", "realpath", "basename", "dirname"),
|
|
7247
|
+
...family("shasum", "sha256sum", "md5sum"),
|
|
7248
|
+
...family("uname", "whoami", "hostname", "date", "printenv"),
|
|
7249
|
+
...family("which", "command -v"),
|
|
7250
|
+
...family("mktemp"),
|
|
7251
|
+
...family("tr", "cut", "paste", "column"),
|
|
7252
|
+
...family("node --version", "python3 --version", "python --version", "deno --version")
|
|
7253
|
+
];
|
|
7254
|
+
var TOOLCHAIN_COMMANDS = [
|
|
7255
|
+
...family("bun test", "bun run test", "bun --version"),
|
|
7256
|
+
...family("bun run typecheck", "bun run lint", "bun run build", "bun run check"),
|
|
7257
|
+
...family("tsc --noEmit", "npx tsc --noEmit"),
|
|
7258
|
+
...family("eslint", "biome check", "prettier --check"),
|
|
7259
|
+
...family("npm test", "npm run test", "npm run typecheck", "npm run lint", "npm run build"),
|
|
7260
|
+
...family("pnpm test", "pnpm run test", "pnpm run build"),
|
|
7261
|
+
...family("yarn test", "yarn build"),
|
|
7262
|
+
...family("vitest run", "jest", "playwright test"),
|
|
7263
|
+
...family("cargo check", "cargo test", "cargo build", "cargo clippy", "cargo fmt --check", "cargo --version"),
|
|
7264
|
+
...family("go build ./...", "go test ./...", "go vet ./...", "go version"),
|
|
7265
|
+
...family("pytest", "python3 -m pytest", "python -m pytest"),
|
|
7266
|
+
...family("ruff check", "mypy", "pyright"),
|
|
7267
|
+
...family("mvn test", "mvn verify", "gradle test", "gradle check", "./gradlew test", "./gradlew check"),
|
|
7268
|
+
...family("make test", "make check", "make build", "make --version"),
|
|
7269
|
+
...family("just --list")
|
|
7270
|
+
];
|
|
7271
|
+
var GIT_READONLY_COMMANDS = [
|
|
7272
|
+
...family("git status", "git status --short", "git status --short --branch", "git status --porcelain", "git status --porcelain=v1 --branch"),
|
|
7273
|
+
...family("git diff", "git diff --stat", "git diff --cached", "git diff --check"),
|
|
7274
|
+
...family("git log", "git show", "git reflog"),
|
|
7275
|
+
...family("git rev-parse", "git rev-list", "git show-ref", "git cat-file", "git symbolic-ref"),
|
|
7276
|
+
...family("git ls-files", "git ls-remote", "git grep"),
|
|
7277
|
+
...family("git branch", "git remote", "git stash list", "git tag", "git describe"),
|
|
7278
|
+
...family("git config --get", "git config --get-regexp"),
|
|
7279
|
+
...family("git -C")
|
|
7280
|
+
];
|
|
7281
|
+
var GIT_MUTATING_COMMANDS = [
|
|
7282
|
+
...family("git add", "git rm --cached"),
|
|
7283
|
+
...family("git commit", "git merge --ff-only", "git merge --no-ff"),
|
|
7284
|
+
...family("git push", "git fetch", "git pull --ff-only"),
|
|
7285
|
+
...family("git tag -a", "git tag -v", "git tag --list"),
|
|
7286
|
+
...family("git stash", "git cherry-pick", "git revert"),
|
|
7287
|
+
...family("git switch", "git checkout -b", "git worktree list", "git worktree add")
|
|
7288
|
+
];
|
|
7289
|
+
var GIT_ENV_PREFIXES = ["GIT_OPTIONAL_LOCKS=0"];
|
|
7290
|
+
function withEnvPrefixes(rule, prefixes = GIT_ENV_PREFIXES) {
|
|
7291
|
+
return prefixes.map((prefix) => ({
|
|
7292
|
+
action: rule.action,
|
|
7293
|
+
resource: `${prefix} ${rule.resource}`,
|
|
7294
|
+
effect: rule.effect
|
|
7295
|
+
}));
|
|
7296
|
+
}
|
|
7297
|
+
|
|
7298
|
+
// src/agent-permissions.ts
|
|
7299
|
+
function normalizeMcpName(name) {
|
|
7300
|
+
return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
|
|
7301
|
+
}
|
|
7302
|
+
function buildAgentPermissions(agent, mcpServers) {
|
|
7303
|
+
const result = [
|
|
7304
|
+
...agent.permissions,
|
|
7305
|
+
{ action: "skill", resource: "*", effect: "deny" },
|
|
7306
|
+
...agent.skills.map((skill) => ({ action: "skill", resource: skill, effect: "allow" }))
|
|
7307
|
+
];
|
|
7308
|
+
for (const server of mcpServers) {
|
|
7309
|
+
const prefix = `${normalizeMcpName(server)}_`;
|
|
7310
|
+
result.push({
|
|
7311
|
+
action: `${prefix}*`,
|
|
7312
|
+
resource: "*",
|
|
7313
|
+
effect: agent.mcp.includes(server) ? "allow" : "deny"
|
|
7314
|
+
});
|
|
7315
|
+
result.push(...agent.permissions.filter((rule) => rule.action.startsWith(prefix)));
|
|
7316
|
+
}
|
|
7317
|
+
for (const rule of agent.permissions) {
|
|
7318
|
+
if (rule.action !== "shell")
|
|
7319
|
+
continue;
|
|
7320
|
+
if (!/(^|\s|["'])git(?:$|\s)/.test(rule.resource))
|
|
7321
|
+
continue;
|
|
7322
|
+
if (rule.resource.startsWith("GIT_"))
|
|
7323
|
+
continue;
|
|
7324
|
+
result.push(...withEnvPrefixes(rule));
|
|
7325
|
+
}
|
|
7326
|
+
return result;
|
|
7327
|
+
}
|
|
7328
|
+
|
|
7329
|
+
// src/release-metadata.ts
|
|
7330
|
+
var PACKAGE_NAME = "@nail00749/agent-gvozd";
|
|
7331
|
+
var PACKAGE_VERSION = "0.1.3";
|
|
7332
|
+
var PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
7333
|
+
var SUPPORTED_OPENCODE_VERSION = "0.0.0-beta-19425";
|
|
7334
|
+
var CONFIG_SCHEMA_VERSION = 2;
|
|
6656
7335
|
|
|
6657
7336
|
// src/constants.ts
|
|
6658
7337
|
var GENERATED_MARKER = "# Generated by agent-gvozd sync. Do not edit this file directly.";
|
|
6659
7338
|
var GENERATED_PLUGIN_MARKER = "// Generated by agent-gvozd sync. Do not edit this file directly.";
|
|
7339
|
+
function hasGeneratedAgentMarker(content) {
|
|
7340
|
+
return content.startsWith(`---
|
|
7341
|
+
${GENERATED_MARKER}
|
|
7342
|
+
`);
|
|
7343
|
+
}
|
|
7344
|
+
function hasGeneratedPluginMarker(content) {
|
|
7345
|
+
return content.startsWith(`${GENERATED_PLUGIN_MARKER}
|
|
7346
|
+
`);
|
|
7347
|
+
}
|
|
7348
|
+
function hasGeneratedSchemaMarker(content) {
|
|
7349
|
+
const errors = [];
|
|
7350
|
+
const value = parse2(content, errors, { allowTrailingComma: true, disallowComments: false });
|
|
7351
|
+
if (errors.length > 0 || value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
7352
|
+
return false;
|
|
7353
|
+
}
|
|
7354
|
+
if (value.$comment !== GENERATED_PLUGIN_MARKER)
|
|
7355
|
+
return false;
|
|
7356
|
+
const version = value["x-agent-gvozd-schema-version"];
|
|
7357
|
+
return typeof version === "number" && Number.isSafeInteger(version) && version >= 1 && version <= CONFIG_SCHEMA_VERSION;
|
|
7358
|
+
}
|
|
7359
|
+
function stable(value) {
|
|
7360
|
+
if (Array.isArray(value))
|
|
7361
|
+
return value.map(stable);
|
|
7362
|
+
if (!value || typeof value !== "object")
|
|
7363
|
+
return value;
|
|
7364
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stable(entry)]));
|
|
7365
|
+
}
|
|
7366
|
+
function isEquivalentLegacySchema(content, generated) {
|
|
7367
|
+
try {
|
|
7368
|
+
const previous = JSON.parse(content);
|
|
7369
|
+
const expected = JSON.parse(generated);
|
|
7370
|
+
if (!previous || Array.isArray(previous) || previous.$comment !== undefined)
|
|
7371
|
+
return false;
|
|
7372
|
+
if (previous["x-agent-gvozd-schema-version"] !== undefined && previous["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION)
|
|
7373
|
+
return false;
|
|
7374
|
+
delete previous.$comment;
|
|
7375
|
+
delete previous["x-agent-gvozd-schema-version"];
|
|
7376
|
+
delete expected.$comment;
|
|
7377
|
+
delete expected["x-agent-gvozd-schema-version"];
|
|
7378
|
+
return JSON.stringify(stable(previous)) === JSON.stringify(stable(expected));
|
|
7379
|
+
} catch {
|
|
7380
|
+
return false;
|
|
7381
|
+
}
|
|
7382
|
+
}
|
|
6660
7383
|
|
|
6661
7384
|
// src/agent-generation.ts
|
|
6662
7385
|
function yamlString(value) {
|
|
@@ -6675,12 +7398,11 @@ function renderPermissions(rules) {
|
|
|
6675
7398
|
];
|
|
6676
7399
|
}
|
|
6677
7400
|
function renderAgent(agent) {
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
];
|
|
7401
|
+
if (agent.promptContent === undefined) {
|
|
7402
|
+
throw new Error(`Agent is missing its immutable prompt snapshot (${agent.prompt})`);
|
|
7403
|
+
}
|
|
7404
|
+
const prompt = agent.promptContent.trim();
|
|
7405
|
+
const permissions = buildAgentPermissions(agent, []);
|
|
6684
7406
|
return [
|
|
6685
7407
|
"---",
|
|
6686
7408
|
GENERATED_MARKER,
|
|
@@ -6695,9 +7417,15 @@ function renderAgent(agent) {
|
|
|
6695
7417
|
`);
|
|
6696
7418
|
}
|
|
6697
7419
|
|
|
7420
|
+
// src/runtime-events.ts
|
|
7421
|
+
function redactDiagnostic(error) {
|
|
7422
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7423
|
+
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);
|
|
7424
|
+
}
|
|
7425
|
+
|
|
6698
7426
|
// src/cli/opencode.ts
|
|
6699
7427
|
import { spawn } from "node:child_process";
|
|
6700
|
-
import { isAbsolute as
|
|
7428
|
+
import { isAbsolute as isAbsolute4 } from "node:path";
|
|
6701
7429
|
var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
6702
7430
|
var DEFAULT_TIMEOUT_MS = 15000;
|
|
6703
7431
|
function appendBounded(current, chunk) {
|
|
@@ -6709,43 +7437,96 @@ function appendBounded(current, chunk) {
|
|
|
6709
7437
|
var defaultProcessRunner = {
|
|
6710
7438
|
run(executable, args, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
6711
7439
|
return new Promise((resolve, reject) => {
|
|
7440
|
+
const grouped = process.platform !== "win32";
|
|
6712
7441
|
const child = spawn(executable, [...args], {
|
|
7442
|
+
detached: grouped,
|
|
6713
7443
|
shell: false,
|
|
6714
7444
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6715
7445
|
});
|
|
6716
7446
|
let stdout = "";
|
|
6717
7447
|
let stderr = "";
|
|
7448
|
+
let settled = false;
|
|
6718
7449
|
let timedOut = false;
|
|
6719
7450
|
let forceKill;
|
|
7451
|
+
let hardDeadline;
|
|
7452
|
+
const terminate = (signal) => {
|
|
7453
|
+
try {
|
|
7454
|
+
if (grouped && child.pid)
|
|
7455
|
+
process.kill(-child.pid, signal);
|
|
7456
|
+
else
|
|
7457
|
+
child.kill(signal);
|
|
7458
|
+
} catch {
|
|
7459
|
+
try {
|
|
7460
|
+
child.kill(signal);
|
|
7461
|
+
} catch {}
|
|
7462
|
+
}
|
|
7463
|
+
};
|
|
7464
|
+
const onStdout = (chunk) => {
|
|
7465
|
+
stdout = appendBounded(stdout, chunk);
|
|
7466
|
+
};
|
|
7467
|
+
const onStderr = (chunk) => {
|
|
7468
|
+
stderr = appendBounded(stderr, chunk);
|
|
7469
|
+
};
|
|
7470
|
+
const timeoutError = () => {
|
|
7471
|
+
const error = new Error(`OpenCode command timed out after ${timeoutMs}ms`);
|
|
7472
|
+
error.code = "ETIMEDOUT";
|
|
7473
|
+
return error;
|
|
7474
|
+
};
|
|
7475
|
+
const clearTimers = () => {
|
|
7476
|
+
clearTimeout(timer);
|
|
7477
|
+
if (forceKill)
|
|
7478
|
+
clearTimeout(forceKill);
|
|
7479
|
+
if (hardDeadline)
|
|
7480
|
+
clearTimeout(hardDeadline);
|
|
7481
|
+
};
|
|
7482
|
+
const settleTimeout = (destroy) => {
|
|
7483
|
+
if (settled)
|
|
7484
|
+
return;
|
|
7485
|
+
settled = true;
|
|
7486
|
+
clearTimers();
|
|
7487
|
+
if (destroy) {
|
|
7488
|
+
child.stdout.off("data", onStdout);
|
|
7489
|
+
child.stderr.off("data", onStderr);
|
|
7490
|
+
child.stdout.destroy();
|
|
7491
|
+
child.stderr.destroy();
|
|
7492
|
+
}
|
|
7493
|
+
reject(timeoutError());
|
|
7494
|
+
};
|
|
6720
7495
|
const timer = setTimeout(() => {
|
|
7496
|
+
if (settled)
|
|
7497
|
+
return;
|
|
6721
7498
|
timedOut = true;
|
|
6722
|
-
|
|
6723
|
-
forceKill = setTimeout(() =>
|
|
7499
|
+
terminate("SIGTERM");
|
|
7500
|
+
forceKill = setTimeout(() => terminate("SIGKILL"), 500);
|
|
6724
7501
|
forceKill.unref();
|
|
7502
|
+
hardDeadline = setTimeout(() => settleTimeout(true), 1750);
|
|
7503
|
+
hardDeadline.unref();
|
|
6725
7504
|
}, timeoutMs);
|
|
6726
7505
|
timer.unref();
|
|
6727
|
-
child.stdout.on("data",
|
|
6728
|
-
|
|
6729
|
-
});
|
|
6730
|
-
child.stderr.on("data", (chunk) => {
|
|
6731
|
-
stderr = appendBounded(stderr, chunk);
|
|
6732
|
-
});
|
|
7506
|
+
child.stdout.on("data", onStdout);
|
|
7507
|
+
child.stderr.on("data", onStderr);
|
|
6733
7508
|
child.once("error", (error) => {
|
|
6734
|
-
|
|
6735
|
-
if (
|
|
6736
|
-
|
|
7509
|
+
clearTimers();
|
|
7510
|
+
if (settled)
|
|
7511
|
+
return;
|
|
7512
|
+
settled = true;
|
|
7513
|
+
if (timedOut) {
|
|
7514
|
+
child.stdout.off("data", onStdout);
|
|
7515
|
+
child.stderr.off("data", onStderr);
|
|
7516
|
+
child.stdout.destroy();
|
|
7517
|
+
child.stderr.destroy();
|
|
7518
|
+
}
|
|
6737
7519
|
reject(error);
|
|
6738
7520
|
});
|
|
6739
7521
|
child.once("close", (code) => {
|
|
6740
|
-
|
|
6741
|
-
if (
|
|
6742
|
-
|
|
7522
|
+
clearTimers();
|
|
7523
|
+
if (settled)
|
|
7524
|
+
return;
|
|
6743
7525
|
if (timedOut) {
|
|
6744
|
-
|
|
6745
|
-
error.code = "ETIMEDOUT";
|
|
6746
|
-
reject(error);
|
|
7526
|
+
settleTimeout(false);
|
|
6747
7527
|
return;
|
|
6748
7528
|
}
|
|
7529
|
+
settled = true;
|
|
6749
7530
|
resolve({ code: code ?? 1, stdout, stderr });
|
|
6750
7531
|
});
|
|
6751
7532
|
});
|
|
@@ -6757,8 +7538,8 @@ function bounded(value) {
|
|
|
6757
7538
|
async function checked(runner, executable, args, timeoutMs) {
|
|
6758
7539
|
const result = await runner.run(executable, args, timeoutMs);
|
|
6759
7540
|
if (result.code !== 0) {
|
|
6760
|
-
const detail = bounded(result.stderr).trim();
|
|
6761
|
-
throw new Error(
|
|
7541
|
+
const detail = redactDiagnostic(bounded(result.stderr).trim());
|
|
7542
|
+
throw new Error(`OpenCode command exited ${result.code}${detail ? `: ${detail}` : ""}`);
|
|
6762
7543
|
}
|
|
6763
7544
|
return bounded(result.stdout);
|
|
6764
7545
|
}
|
|
@@ -6770,7 +7551,7 @@ function parseDebugPaths(output) {
|
|
|
6770
7551
|
continue;
|
|
6771
7552
|
paths[match[1]] = match[2];
|
|
6772
7553
|
}
|
|
6773
|
-
if (!paths.config || !
|
|
7554
|
+
if (!paths.config || !isAbsolute4(paths.config)) {
|
|
6774
7555
|
throw new Error("OpenCode did not report an absolute config path");
|
|
6775
7556
|
}
|
|
6776
7557
|
return paths;
|
|
@@ -6875,10 +7656,6 @@ function manualProfile(provider, fast, deep, catalog) {
|
|
|
6875
7656
|
|
|
6876
7657
|
// src/cli/doctor.ts
|
|
6877
7658
|
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
7659
|
function aggregate(checks) {
|
|
6883
7660
|
if (checks.some((check) => check.status === "fail"))
|
|
6884
7661
|
return "fail";
|
|
@@ -6888,7 +7665,7 @@ function aggregate(checks) {
|
|
|
6888
7665
|
}
|
|
6889
7666
|
function safeFile(path) {
|
|
6890
7667
|
try {
|
|
6891
|
-
const stat =
|
|
7668
|
+
const stat = lstatSync3(path);
|
|
6892
7669
|
return stat.isFile() && !stat.isSymbolicLink();
|
|
6893
7670
|
} catch {
|
|
6894
7671
|
return false;
|
|
@@ -6900,8 +7677,10 @@ function escapeRegExp(value) {
|
|
|
6900
7677
|
function hasAgentIdentifier(output, id) {
|
|
6901
7678
|
return new RegExp(`(?:^|[^A-Za-z0-9_-])${escapeRegExp(id)}(?=$|[^A-Za-z0-9_-])`, "m").test(output);
|
|
6902
7679
|
}
|
|
6903
|
-
function
|
|
6904
|
-
|
|
7680
|
+
function hasInstalledPluginVersion(output, name, version) {
|
|
7681
|
+
const boundary = `[\\s"'|│,}\\]]`;
|
|
7682
|
+
const pattern = `(?:^|${boundary})${escapeRegExp(name)}(?:@|\\s+)v?${escapeRegExp(version)}(?=$|${boundary})`;
|
|
7683
|
+
return new RegExp(pattern, "m").test(output);
|
|
6905
7684
|
}
|
|
6906
7685
|
function checkGlobalFiles(config, configRoot) {
|
|
6907
7686
|
const missing = [];
|
|
@@ -6909,27 +7688,43 @@ function checkGlobalFiles(config, configRoot) {
|
|
|
6909
7688
|
for (const [id, agent] of Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
6910
7689
|
if (agent.disabled)
|
|
6911
7690
|
continue;
|
|
6912
|
-
const path =
|
|
7691
|
+
const path = join4(configRoot, "agents", `${id}.md`);
|
|
6913
7692
|
if (!safeFile(path)) {
|
|
6914
7693
|
missing.push(id);
|
|
6915
7694
|
continue;
|
|
6916
7695
|
}
|
|
6917
7696
|
const content = readFileSync3(path, "utf8");
|
|
6918
|
-
if (!content
|
|
7697
|
+
if (!hasGeneratedAgentMarker(content) || content !== renderAgent(agent))
|
|
6919
7698
|
stale.push(id);
|
|
6920
7699
|
}
|
|
6921
|
-
|
|
6922
|
-
|
|
7700
|
+
const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
|
|
7701
|
+
const orphans = [];
|
|
7702
|
+
const directory = join4(configRoot, "agents");
|
|
7703
|
+
if (existsSync3(directory) && lstatSync3(directory).isDirectory() && !lstatSync3(directory).isSymbolicLink()) {
|
|
7704
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
7705
|
+
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
7706
|
+
continue;
|
|
7707
|
+
const content = readFileSync3(join4(directory, entry.name), "utf8");
|
|
7708
|
+
if (hasGeneratedAgentMarker(content))
|
|
7709
|
+
orphans.push(entry.name.slice(0, -3));
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
if (missing.length + stale.length + orphans.length === 0) {
|
|
7713
|
+
return { id: "global-agents", status: "pass", summary: `${enabled.size} managed global agents are current` };
|
|
6923
7714
|
}
|
|
6924
|
-
const details = [
|
|
7715
|
+
const details = [
|
|
7716
|
+
missing.length ? `missing: ${missing.join(", ")}` : "",
|
|
7717
|
+
stale.length ? `unmanaged or stale: ${stale.join(", ")}` : "",
|
|
7718
|
+
orphans.length ? `orphan managed agents: ${orphans.join(", ")}` : ""
|
|
7719
|
+
].filter(Boolean).join("; ");
|
|
6925
7720
|
return { id: "global-agents", status: "fail", summary: details, remediation: SETUP_COMMAND };
|
|
6926
7721
|
}
|
|
6927
7722
|
function checkLegacy(config) {
|
|
6928
7723
|
const duplicates = [];
|
|
6929
|
-
const plugin =
|
|
7724
|
+
const plugin = join4(config.projectRoot, ".opencode", "plugins", "agent-gvozd", "index.ts");
|
|
6930
7725
|
if (safeFile(plugin))
|
|
6931
7726
|
duplicates.push("local plugin");
|
|
6932
|
-
const agents = Object.keys(config.agents).filter((id) => safeFile(
|
|
7727
|
+
const agents = Object.keys(config.agents).filter((id) => safeFile(join4(config.projectRoot, ".opencode", "agents", `${id}.md`)));
|
|
6933
7728
|
if (agents.length > 0)
|
|
6934
7729
|
duplicates.push(`${agents.length} local agents`);
|
|
6935
7730
|
if (duplicates.length === 0)
|
|
@@ -6942,58 +7737,59 @@ function checkLegacy(config) {
|
|
|
6942
7737
|
};
|
|
6943
7738
|
}
|
|
6944
7739
|
async function runDoctor(input) {
|
|
6945
|
-
const packageName = input.packageName ??
|
|
6946
|
-
const packageVersion = input.packageVersion ??
|
|
6947
|
-
const supportedVersion = input.supportedOpenCodeVersion ??
|
|
7740
|
+
const packageName = input.packageName ?? PACKAGE_NAME;
|
|
7741
|
+
const packageVersion = input.packageVersion ?? PACKAGE_VERSION;
|
|
7742
|
+
const supportedVersion = input.supportedOpenCodeVersion ?? SUPPORTED_OPENCODE_VERSION;
|
|
6948
7743
|
const checks = [];
|
|
6949
7744
|
try {
|
|
6950
7745
|
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: ${
|
|
7746
|
+
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
7747
|
} catch (error) {
|
|
6953
|
-
checks.push({ id: "opencode-version", status: "fail", summary: `OpenCode version check failed: ${
|
|
7748
|
+
checks.push({ id: "opencode-version", status: "fail", summary: `OpenCode version check failed: ${redactDiagnostic(error)}`, remediation: `Install OpenCode ${supportedVersion}` });
|
|
6954
7749
|
}
|
|
6955
7750
|
try {
|
|
6956
7751
|
await input.client.serviceStatus();
|
|
6957
7752
|
checks.push({ id: "service", status: "pass", summary: "OpenCode service is reachable" });
|
|
6958
7753
|
} catch (error) {
|
|
6959
|
-
checks.push({ id: "service", status: "fail", summary: `OpenCode service is unavailable: ${
|
|
7754
|
+
checks.push({ id: "service", status: "fail", summary: `OpenCode service is unavailable: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} service restart` });
|
|
6960
7755
|
}
|
|
6961
7756
|
try {
|
|
6962
7757
|
const output = await input.client.pluginList();
|
|
6963
|
-
checks.push(
|
|
7758
|
+
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
7759
|
} catch (error) {
|
|
6965
|
-
checks.push({ id: "plugin", status: "fail", summary: `plugin list failed: ${
|
|
7760
|
+
checks.push({ id: "plugin", status: "fail", summary: `plugin list failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
|
|
6966
7761
|
}
|
|
6967
7762
|
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" });
|
|
7763
|
+
await input.client.pluginCheck(PACKAGE_SPEC);
|
|
7764
|
+
checks.push({ id: "plugin-check", status: "pass", summary: "Gvozd plugin check passed" });
|
|
6971
7765
|
} catch (error) {
|
|
6972
|
-
checks.push({ id: "plugin-check", status: "fail", summary: `plugin check failed: ${
|
|
7766
|
+
checks.push({ id: "plugin-check", status: "fail", summary: `plugin check failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
|
|
6973
7767
|
}
|
|
6974
7768
|
try {
|
|
6975
7769
|
const paths = await input.client.debugPaths();
|
|
6976
|
-
|
|
7770
|
+
const reported = resolve4(paths.config);
|
|
7771
|
+
const expected = resolve4(input.runtimeConfigRoot ?? input.configRoot);
|
|
7772
|
+
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
7773
|
} catch (error) {
|
|
6978
|
-
checks.push({ id: "config-root", status: "fail", summary: `config path check failed: ${
|
|
7774
|
+
checks.push({ id: "config-root", status: "fail", summary: `config path check failed: ${redactDiagnostic(error)}` });
|
|
6979
7775
|
}
|
|
6980
7776
|
let config;
|
|
6981
7777
|
let globalConfig;
|
|
6982
|
-
const configPath =
|
|
6983
|
-
const schemaPath =
|
|
7778
|
+
const configPath = join4(input.configRoot, "gvozd", "config.jsonc");
|
|
7779
|
+
const schemaPath = join4(input.configRoot, "gvozd", "schema.json");
|
|
6984
7780
|
try {
|
|
6985
7781
|
if (!safeFile(configPath) || !safeFile(schemaPath))
|
|
6986
7782
|
throw new Error("global config.jsonc or schema.json is missing");
|
|
6987
7783
|
const schemaErrors = [];
|
|
6988
7784
|
const schema = parse2(readFileSync3(schemaPath, "utf8"), schemaErrors);
|
|
6989
|
-
if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !==
|
|
7785
|
+
if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION || schema?.$comment !== GENERATED_PLUGIN_MARKER) {
|
|
6990
7786
|
throw new Error("global schema is invalid, incompatible, or unmanaged");
|
|
6991
7787
|
}
|
|
6992
7788
|
globalConfig = loadConfig(input.cwd, { configRoot: input.configRoot, includeProject: false });
|
|
6993
7789
|
config = loadConfig(input.cwd, { configRoot: input.configRoot });
|
|
6994
7790
|
checks.push({ id: "config", status: "pass", summary: "global Gvozd config and schema are valid" });
|
|
6995
7791
|
} catch (error) {
|
|
6996
|
-
checks.push({ id: "config", status: "fail", summary: `global config check failed: ${
|
|
7792
|
+
checks.push({ id: "config", status: "fail", summary: `global config check failed: ${redactDiagnostic(error)}`, remediation: SETUP_COMMAND });
|
|
6997
7793
|
}
|
|
6998
7794
|
let catalog = parseModels([]);
|
|
6999
7795
|
try {
|
|
@@ -7003,12 +7799,14 @@ async function runDoctor(input) {
|
|
|
7003
7799
|
if (!config) {
|
|
7004
7800
|
checks.push({ id: "models", status: "fail", summary: "configured models cannot be validated without a valid config", remediation: SETUP_COMMAND });
|
|
7005
7801
|
} else {
|
|
7006
|
-
const
|
|
7007
|
-
const
|
|
7008
|
-
|
|
7802
|
+
const available = new Set(catalog.models);
|
|
7803
|
+
const enabled = Object.entries(config.agents).filter(([, agent]) => !agent.disabled);
|
|
7804
|
+
const unavailableAgents = enabled.filter(([, agent]) => !agent.models.some((model) => available.has(model))).map(([id]) => id);
|
|
7805
|
+
const missingFallbacks = [...new Set(enabled.flatMap(([, agent]) => agent.models.filter((model) => !available.has(model))))].sort();
|
|
7806
|
+
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
7807
|
}
|
|
7010
7808
|
} catch (error) {
|
|
7011
|
-
checks.push({ id: "models", status: "fail", summary: `model catalog check failed: ${
|
|
7809
|
+
checks.push({ id: "models", status: "fail", summary: `model catalog check failed: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} auth` });
|
|
7012
7810
|
}
|
|
7013
7811
|
if (globalConfig)
|
|
7014
7812
|
checks.push(checkGlobalFiles(globalConfig, input.configRoot));
|
|
@@ -7019,10 +7817,10 @@ async function runDoctor(input) {
|
|
|
7019
7817
|
const missing = Object.entries(config?.agents ?? {}).filter(([, agent]) => !agent.disabled).map(([id]) => id).filter((id) => !hasAgentIdentifier(output, id));
|
|
7020
7818
|
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
7819
|
} catch (error) {
|
|
7022
|
-
checks.push({ id: "runtime-agents", status: "fail", summary: `runtime agent check failed: ${
|
|
7820
|
+
checks.push({ id: "runtime-agents", status: "fail", summary: `runtime agent check failed: ${redactDiagnostic(error)}`, remediation: `${input.client.executable} service restart` });
|
|
7023
7821
|
}
|
|
7024
7822
|
checks.push(config ? checkLegacy(config) : { id: "legacy-local", status: "warn", summary: "legacy duplicates could not be checked" });
|
|
7025
|
-
return { schemaVersion:
|
|
7823
|
+
return { schemaVersion: CONFIG_SCHEMA_VERSION, status: aggregate(checks), checks };
|
|
7026
7824
|
}
|
|
7027
7825
|
function doctorExitCode(report) {
|
|
7028
7826
|
return report.status === "fail" ? 1 : 0;
|
|
@@ -7044,20 +7842,84 @@ function doctorOperationalFailure(error) {
|
|
|
7044
7842
|
const checks = [{
|
|
7045
7843
|
id: "opencode-discovery",
|
|
7046
7844
|
status: "fail",
|
|
7047
|
-
summary: `OpenCode discovery failed: ${
|
|
7048
|
-
remediation:
|
|
7845
|
+
summary: `OpenCode discovery failed: ${redactDiagnostic(error)}`,
|
|
7846
|
+
remediation: `Install OpenCode ${SUPPORTED_OPENCODE_VERSION} and run gvozd doctor again`
|
|
7049
7847
|
}];
|
|
7050
|
-
return { schemaVersion:
|
|
7848
|
+
return { schemaVersion: CONFIG_SCHEMA_VERSION, status: "fail", checks };
|
|
7051
7849
|
}
|
|
7052
7850
|
|
|
7053
7851
|
// src/cli/setup.ts
|
|
7054
|
-
import { existsSync as
|
|
7055
|
-
import { dirname as
|
|
7852
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
7853
|
+
import { dirname as dirname6, join as join8 } from "node:path";
|
|
7056
7854
|
|
|
7057
7855
|
// src/cli/config-store.ts
|
|
7058
|
-
import { closeSync, existsSync as
|
|
7856
|
+
import { accessSync, closeSync, constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync5, mkdirSync, openSync, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7059
7857
|
import { randomUUID } from "node:crypto";
|
|
7060
|
-
import { dirname as
|
|
7858
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
7859
|
+
|
|
7860
|
+
// src/secure-path.ts
|
|
7861
|
+
import { lstatSync as lstatSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
7862
|
+
import { isAbsolute as isAbsolute5, join as join5, parse as parse6, relative as relative3, resolve as resolve5, sep } from "node:path";
|
|
7863
|
+
function secureCanonicalPath(target, label = "Path") {
|
|
7864
|
+
if (!isAbsolute5(target))
|
|
7865
|
+
throw new Error(`${label} must be absolute: ${target}`);
|
|
7866
|
+
const lexical = resolve5(target);
|
|
7867
|
+
const root = parse6(lexical).root;
|
|
7868
|
+
const components = relative3(root, lexical).split(sep).filter(Boolean);
|
|
7869
|
+
let current = root;
|
|
7870
|
+
let nearestExisting = root;
|
|
7871
|
+
const missingSuffix = [];
|
|
7872
|
+
let missing = false;
|
|
7873
|
+
const rootStat = lstatSync4(root);
|
|
7874
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
7875
|
+
throw new Error(`${label} has an unsafe filesystem root: ${root}`);
|
|
7876
|
+
const existingDirectories = [{ path: root, stat: rootStat }];
|
|
7877
|
+
for (const component of components) {
|
|
7878
|
+
current = join5(current, component);
|
|
7879
|
+
if (missing) {
|
|
7880
|
+
missingSuffix.push(component);
|
|
7881
|
+
continue;
|
|
7882
|
+
}
|
|
7883
|
+
let stat;
|
|
7884
|
+
try {
|
|
7885
|
+
stat = lstatSync4(current);
|
|
7886
|
+
} catch (error) {
|
|
7887
|
+
if (error.code !== "ENOENT")
|
|
7888
|
+
throw error;
|
|
7889
|
+
missing = true;
|
|
7890
|
+
missingSuffix.push(component);
|
|
7891
|
+
continue;
|
|
7892
|
+
}
|
|
7893
|
+
if (stat.isSymbolicLink())
|
|
7894
|
+
throw new Error(`${label} contains a symbolic-link component: ${current}`);
|
|
7895
|
+
if (current !== lexical && !stat.isDirectory()) {
|
|
7896
|
+
throw new Error(`${label} contains a non-directory ancestor: ${current}`);
|
|
7897
|
+
}
|
|
7898
|
+
if (stat.isDirectory())
|
|
7899
|
+
existingDirectories.push({ path: current, stat });
|
|
7900
|
+
nearestExisting = current;
|
|
7901
|
+
}
|
|
7902
|
+
if (process.platform !== "win32") {
|
|
7903
|
+
const uid = process.getuid?.();
|
|
7904
|
+
for (let index = 0;index < existingDirectories.length; index++) {
|
|
7905
|
+
const entry = existingDirectories[index];
|
|
7906
|
+
if ((entry.stat.mode & 18) === 0)
|
|
7907
|
+
continue;
|
|
7908
|
+
const sticky = (entry.stat.mode & 512) !== 0;
|
|
7909
|
+
const hasPrivateBoundary = sticky && uid !== undefined && existingDirectories.slice(index + 1).some((candidate) => candidate.stat.uid === uid && (candidate.stat.mode & 18) === 0);
|
|
7910
|
+
if (!hasPrivateBoundary) {
|
|
7911
|
+
throw new Error(`${label} contains a group/world-writable directory without an existing private boundary: ${entry.path}`);
|
|
7912
|
+
}
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
const canonicalAncestor = realpathSync3(nearestExisting);
|
|
7916
|
+
if (canonicalAncestor !== nearestExisting) {
|
|
7917
|
+
throw new Error(`${label} contains a non-canonical or changed ancestor: ${nearestExisting}`);
|
|
7918
|
+
}
|
|
7919
|
+
return resolve5(canonicalAncestor, ...missingSuffix);
|
|
7920
|
+
}
|
|
7921
|
+
|
|
7922
|
+
// src/cli/config-store.ts
|
|
7061
7923
|
var FAST_AGENT_IDS = ["back-fast", "front-fast", "review-fast", "researcher", "git", "docs", "verifier"];
|
|
7062
7924
|
var DEEP_AGENT_IDS = ["master", "planner", "back-deep", "front-deep", "review-deep", "debugger", "security", "devops"];
|
|
7063
7925
|
var ALL_AGENT_IDS = [...DEEP_AGENT_IDS, ...FAST_AGENT_IDS, "explorer"];
|
|
@@ -7084,14 +7946,30 @@ function applyModelProfile(source, profile) {
|
|
|
7084
7946
|
updated = setJsonc(updated, ["agents", "explorer", "models"], profile.agentOverrides.explorer ?? profile.fast);
|
|
7085
7947
|
return updated;
|
|
7086
7948
|
}
|
|
7087
|
-
function
|
|
7088
|
-
|
|
7089
|
-
|
|
7949
|
+
function matchesSnapshot(path, expected) {
|
|
7950
|
+
if (!expected.exists)
|
|
7951
|
+
return !existsSync4(path);
|
|
7952
|
+
if (!existsSync4(path))
|
|
7953
|
+
return false;
|
|
7954
|
+
const stat = lstatSync5(path);
|
|
7955
|
+
return stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino && readFileSync4(path, "utf8") === expected.bytes;
|
|
7956
|
+
}
|
|
7957
|
+
function atomicWrite(path, content, expected) {
|
|
7958
|
+
const canonicalPath = secureCanonicalPath(path, "Managed global config path");
|
|
7959
|
+
if (canonicalPath !== expected.path)
|
|
7960
|
+
throw new Error(`Global config path changed after preflight: ${path}`);
|
|
7961
|
+
mkdirSync(dirname3(canonicalPath), { recursive: true, mode: 448 });
|
|
7962
|
+
if (secureCanonicalPath(canonicalPath, "Managed global config path") !== canonicalPath)
|
|
7963
|
+
throw new Error(`Global config path changed during write: ${path}`);
|
|
7964
|
+
assertWriteable(dirname3(canonicalPath), "Managed global config directory");
|
|
7965
|
+
const temporary = `${canonicalPath}.tmp-${process.pid}-${randomUUID()}`;
|
|
7090
7966
|
const descriptor = openSync(temporary, "wx", 384);
|
|
7091
7967
|
try {
|
|
7092
7968
|
writeFileSync(descriptor, content);
|
|
7093
7969
|
closeSync(descriptor);
|
|
7094
|
-
|
|
7970
|
+
if (!matchesSnapshot(canonicalPath, expected))
|
|
7971
|
+
throw new Error(`Refusing to replace concurrently changed file: ${canonicalPath}`);
|
|
7972
|
+
renameSync(temporary, canonicalPath);
|
|
7095
7973
|
} catch (error) {
|
|
7096
7974
|
try {
|
|
7097
7975
|
closeSync(descriptor);
|
|
@@ -7103,51 +7981,107 @@ function atomicWrite(path, content) {
|
|
|
7103
7981
|
}
|
|
7104
7982
|
}
|
|
7105
7983
|
function isRegularFile(path) {
|
|
7106
|
-
const stat =
|
|
7984
|
+
const stat = lstatSync5(path);
|
|
7107
7985
|
return stat.isFile() && !stat.isSymbolicLink();
|
|
7108
7986
|
}
|
|
7109
|
-
function
|
|
7110
|
-
|
|
7987
|
+
function assertWriteable(path, label) {
|
|
7988
|
+
let candidate = path;
|
|
7989
|
+
while (!existsSync4(candidate)) {
|
|
7990
|
+
const parent = dirname3(candidate);
|
|
7991
|
+
if (parent === candidate)
|
|
7992
|
+
break;
|
|
7993
|
+
candidate = parent;
|
|
7994
|
+
}
|
|
7995
|
+
try {
|
|
7996
|
+
const stat = lstatSync5(candidate);
|
|
7997
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() && candidate !== path)
|
|
7998
|
+
throw new Error("unsafe parent");
|
|
7999
|
+
const uid = process.getuid?.();
|
|
8000
|
+
if (uid !== undefined && (stat.uid !== uid || (stat.mode & 18) !== 0))
|
|
8001
|
+
throw new Error("unsafe ownership or mode");
|
|
8002
|
+
accessSync(candidate, fsConstants.W_OK | (stat.isDirectory() ? fsConstants.X_OK : 0));
|
|
8003
|
+
} catch {
|
|
8004
|
+
throw new Error(`${label} is not writeable: ${path}`);
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
8007
|
+
function legacySchemaMatches(source, generated) {
|
|
8008
|
+
try {
|
|
8009
|
+
const previous = JSON.parse(source);
|
|
8010
|
+
const previousVersion = previous["x-agent-gvozd-schema-version"];
|
|
8011
|
+
if (previous.$comment !== undefined || previousVersion !== undefined && previousVersion !== CONFIG_SCHEMA_VERSION) {
|
|
8012
|
+
return false;
|
|
8013
|
+
}
|
|
8014
|
+
if (previous.$id !== "https://example.invalid/agent-gvozd.schema.json")
|
|
8015
|
+
return false;
|
|
8016
|
+
return generated ? isEquivalentLegacySchema(source, generated) : true;
|
|
8017
|
+
} catch {
|
|
8018
|
+
return false;
|
|
8019
|
+
}
|
|
8020
|
+
}
|
|
8021
|
+
function snapshot(path) {
|
|
8022
|
+
if (!existsSync4(path))
|
|
8023
|
+
return Object.freeze({ path, exists: false });
|
|
8024
|
+
const stat = lstatSync5(path);
|
|
8025
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
8026
|
+
throw new Error(`Managed global config snapshot target is unsafe: ${path}`);
|
|
8027
|
+
return Object.freeze({ path, exists: true, bytes: readFileSync4(path, "utf8"), dev: stat.dev, ino: stat.ino });
|
|
8028
|
+
}
|
|
8029
|
+
function preflightGlobalConfig(configRoot, schemaSource) {
|
|
8030
|
+
const canonicalRoot = secureCanonicalPath(configRoot, "OpenCode config root");
|
|
8031
|
+
const rootStat = existsSync4(canonicalRoot) ? lstatSync5(canonicalRoot) : undefined;
|
|
7111
8032
|
if (rootStat && (rootStat.isSymbolicLink() || !rootStat.isDirectory())) {
|
|
7112
|
-
throw new Error(`OpenCode config root is not a safe directory: ${
|
|
8033
|
+
throw new Error(`OpenCode config root is not a safe directory: ${canonicalRoot}`);
|
|
7113
8034
|
}
|
|
7114
|
-
|
|
7115
|
-
const
|
|
8035
|
+
assertWriteable(canonicalRoot, "OpenCode config root");
|
|
8036
|
+
const directory = secureCanonicalPath(join6(canonicalRoot, "gvozd"), "Global Gvozd directory");
|
|
8037
|
+
const directoryStat = existsSync4(directory) ? lstatSync5(directory) : undefined;
|
|
7116
8038
|
if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
|
|
7117
8039
|
throw new Error(`Global Gvozd path is not a safe directory: ${directory}`);
|
|
7118
8040
|
}
|
|
7119
|
-
|
|
7120
|
-
const
|
|
7121
|
-
|
|
8041
|
+
assertWriteable(directory, "Global Gvozd directory");
|
|
8042
|
+
const configPath = join6(directory, "config.jsonc");
|
|
8043
|
+
const schemaPath = join6(directory, "schema.json");
|
|
8044
|
+
if (existsSync4(configPath)) {
|
|
7122
8045
|
if (!isRegularFile(configPath))
|
|
7123
8046
|
throw new Error(`Global Gvozd config is not a safe file: ${configPath}`);
|
|
7124
8047
|
assertValidJsonc(readFileSync4(configPath, "utf8"), configPath);
|
|
8048
|
+
assertWriteable(configPath, "Global Gvozd config");
|
|
7125
8049
|
}
|
|
7126
|
-
if (
|
|
7127
|
-
|
|
8050
|
+
if (existsSync4(schemaPath)) {
|
|
8051
|
+
if (!isRegularFile(schemaPath))
|
|
8052
|
+
throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
|
|
8053
|
+
const schema = readFileSync4(schemaPath, "utf8");
|
|
8054
|
+
if (!hasGeneratedSchemaMarker(schema) && !legacySchemaMatches(schema, schemaSource)) {
|
|
8055
|
+
throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
|
|
8056
|
+
}
|
|
8057
|
+
assertWriteable(schemaPath, "Global Gvozd schema");
|
|
7128
8058
|
}
|
|
8059
|
+
return Object.freeze({ configRoot: canonicalRoot, config: snapshot(configPath), schema: snapshot(schemaPath) });
|
|
7129
8060
|
}
|
|
7130
8061
|
function writeGlobalConfig(input) {
|
|
7131
|
-
preflightGlobalConfig(input.configRoot);
|
|
7132
|
-
const
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
const
|
|
8062
|
+
const state = input.snapshot ?? preflightGlobalConfig(input.configRoot, input.schemaSource);
|
|
8063
|
+
const canonicalRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
|
|
8064
|
+
if (state.configRoot !== canonicalRoot)
|
|
8065
|
+
throw new Error("Global config snapshot belongs to another config root");
|
|
8066
|
+
const directory = join6(state.configRoot, "gvozd");
|
|
8067
|
+
const configPath = join6(directory, "config.jsonc");
|
|
8068
|
+
const schemaPath = join6(directory, "schema.json");
|
|
8069
|
+
const base = state.config.exists ? state.config.bytes : `{
|
|
7136
8070
|
"$schema": "./schema.json",
|
|
7137
8071
|
"agents": {}
|
|
7138
8072
|
}
|
|
7139
8073
|
`;
|
|
7140
|
-
if (!input.schemaSource
|
|
8074
|
+
if (!hasGeneratedSchemaMarker(input.schemaSource)) {
|
|
7141
8075
|
throw new Error("Package schema is missing the Gvozd ownership marker");
|
|
7142
8076
|
}
|
|
7143
8077
|
const config = applyModelProfile(base, input.profile);
|
|
7144
8078
|
assertValidJsonc(input.schemaSource, "package Gvozd schema");
|
|
7145
8079
|
atomicWrite(schemaPath, input.schemaSource.endsWith(`
|
|
7146
8080
|
`) ? input.schemaSource : `${input.schemaSource}
|
|
7147
|
-
|
|
8081
|
+
`, state.schema);
|
|
7148
8082
|
atomicWrite(configPath, config.endsWith(`
|
|
7149
8083
|
`) ? config : `${config}
|
|
7150
|
-
|
|
8084
|
+
`, state.config);
|
|
7151
8085
|
return { configPath, schemaPath, config };
|
|
7152
8086
|
}
|
|
7153
8087
|
|
|
@@ -7220,20 +8154,43 @@ function profileFromAgents(agents, catalog) {
|
|
|
7220
8154
|
}
|
|
7221
8155
|
|
|
7222
8156
|
// src/cli/global-sync.ts
|
|
7223
|
-
import { closeSync as closeSync2, lstatSync as
|
|
8157
|
+
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
8158
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7225
|
-
import { join as
|
|
8159
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
7226
8160
|
function stat(path) {
|
|
7227
8161
|
try {
|
|
7228
|
-
return
|
|
8162
|
+
return lstatSync6(path);
|
|
7229
8163
|
} catch (error) {
|
|
7230
8164
|
if (error.code === "ENOENT")
|
|
7231
8165
|
return;
|
|
7232
8166
|
throw error;
|
|
7233
8167
|
}
|
|
7234
8168
|
}
|
|
8169
|
+
function assertWriteable2(path, label) {
|
|
8170
|
+
let candidate = path;
|
|
8171
|
+
while (!existsSync5(candidate)) {
|
|
8172
|
+
const parent = dirname4(candidate);
|
|
8173
|
+
if (parent === candidate)
|
|
8174
|
+
break;
|
|
8175
|
+
candidate = parent;
|
|
8176
|
+
}
|
|
8177
|
+
try {
|
|
8178
|
+
const current = lstatSync6(candidate);
|
|
8179
|
+
if (current.isSymbolicLink() || !current.isDirectory() && candidate !== path)
|
|
8180
|
+
throw new Error("unsafe parent");
|
|
8181
|
+
const uid = process.getuid?.();
|
|
8182
|
+
if (uid !== undefined && (current.uid !== uid || (current.mode & 18) !== 0))
|
|
8183
|
+
throw new Error("unsafe ownership or mode");
|
|
8184
|
+
accessSync2(candidate, fsConstants2.W_OK | (current.isDirectory() ? fsConstants2.X_OK : 0));
|
|
8185
|
+
} catch {
|
|
8186
|
+
throw new Error(`${label} is not writeable: ${path}`);
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
7235
8189
|
function createFile(path, content) {
|
|
7236
|
-
const
|
|
8190
|
+
const canonicalPath = secureCanonicalPath(path, "Managed global agent path");
|
|
8191
|
+
if (canonicalPath !== path)
|
|
8192
|
+
throw new Error(`Managed global agent path changed: ${path}`);
|
|
8193
|
+
const descriptor = openSync2(canonicalPath, "wx", 384);
|
|
7237
8194
|
try {
|
|
7238
8195
|
writeFileSync2(descriptor, content);
|
|
7239
8196
|
} finally {
|
|
@@ -7241,6 +8198,8 @@ function createFile(path, content) {
|
|
|
7241
8198
|
}
|
|
7242
8199
|
}
|
|
7243
8200
|
function replaceFile(path, content) {
|
|
8201
|
+
if (secureCanonicalPath(path, "Managed global agent path") !== path)
|
|
8202
|
+
throw new Error(`Managed global agent path changed: ${path}`);
|
|
7244
8203
|
const temporary = `${path}.tmp-${process.pid}-${randomUUID2()}`;
|
|
7245
8204
|
createFile(temporary, content);
|
|
7246
8205
|
try {
|
|
@@ -7253,21 +8212,41 @@ function replaceFile(path, content) {
|
|
|
7253
8212
|
}
|
|
7254
8213
|
}
|
|
7255
8214
|
function writeManagedAgents(input) {
|
|
7256
|
-
const
|
|
7257
|
-
const
|
|
8215
|
+
const configRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
|
|
8216
|
+
const agentsDirectory = secureCanonicalPath(join7(configRoot, "agents"), "OpenCode agents path");
|
|
8217
|
+
const result = { created: [], updated: [], unchanged: [], conflicts: [], removed: [] };
|
|
7258
8218
|
const writes = [];
|
|
7259
|
-
const
|
|
8219
|
+
const removals = new Map;
|
|
8220
|
+
const enabled = new Set(Object.entries(input.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
|
|
8221
|
+
const rootStat = stat(configRoot);
|
|
7260
8222
|
if (rootStat && (rootStat.isSymbolicLink() || !rootStat.isDirectory())) {
|
|
7261
|
-
throw new Error(`OpenCode config root is not a safe directory: ${
|
|
8223
|
+
throw new Error(`OpenCode config root is not a safe directory: ${configRoot}`);
|
|
7262
8224
|
}
|
|
8225
|
+
assertWriteable2(configRoot, "OpenCode config root");
|
|
7263
8226
|
const directoryStat = stat(agentsDirectory);
|
|
7264
8227
|
if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
|
|
7265
8228
|
throw new Error(`OpenCode agents path is not a safe directory: ${agentsDirectory}`);
|
|
7266
8229
|
}
|
|
8230
|
+
assertWriteable2(agentsDirectory, "OpenCode agents directory");
|
|
8231
|
+
if (directoryStat) {
|
|
8232
|
+
for (const entry of readdirSync4(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
8233
|
+
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
8234
|
+
continue;
|
|
8235
|
+
const path = join7(agentsDirectory, entry.name);
|
|
8236
|
+
const currentStat = stat(path);
|
|
8237
|
+
if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile())
|
|
8238
|
+
continue;
|
|
8239
|
+
if (hasGeneratedAgentMarker(readFileSync5(path, "utf8"))) {
|
|
8240
|
+
assertWriteable2(path, "Managed global agent");
|
|
8241
|
+
result.removed.push(path);
|
|
8242
|
+
removals.set(path, readFileSync5(path, "utf8"));
|
|
8243
|
+
}
|
|
8244
|
+
}
|
|
8245
|
+
}
|
|
7267
8246
|
for (const [id, agent] of Object.entries(input.agents).sort(([left], [right]) => left.localeCompare(right))) {
|
|
7268
8247
|
if (agent.disabled)
|
|
7269
8248
|
continue;
|
|
7270
|
-
const path =
|
|
8249
|
+
const path = join7(agentsDirectory, `${id}.md`);
|
|
7271
8250
|
const content = renderAgent(agent);
|
|
7272
8251
|
const currentStat = stat(path);
|
|
7273
8252
|
if (!currentStat) {
|
|
@@ -7280,45 +8259,134 @@ function writeManagedAgents(input) {
|
|
|
7280
8259
|
continue;
|
|
7281
8260
|
}
|
|
7282
8261
|
const current = readFileSync5(path, "utf8");
|
|
7283
|
-
if (!current
|
|
8262
|
+
if (!hasGeneratedAgentMarker(current)) {
|
|
7284
8263
|
result.conflicts.push(path);
|
|
7285
8264
|
continue;
|
|
7286
8265
|
}
|
|
8266
|
+
assertWriteable2(path, "Managed global agent");
|
|
7287
8267
|
if (current === content) {
|
|
7288
8268
|
result.unchanged.push(path);
|
|
7289
8269
|
continue;
|
|
7290
8270
|
}
|
|
7291
8271
|
result.updated.push(path);
|
|
7292
|
-
writes.push({ path, content, replace: true });
|
|
8272
|
+
writes.push({ path, content, replace: true, previous: current });
|
|
7293
8273
|
}
|
|
7294
8274
|
if (result.conflicts.length > 0) {
|
|
7295
8275
|
throw new Error(`Refusing to overwrite unmanaged global agents: ${result.conflicts.join(", ")}`);
|
|
7296
8276
|
}
|
|
7297
8277
|
if (input.check)
|
|
7298
8278
|
return result;
|
|
7299
|
-
|
|
8279
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
8280
|
+
throw new Error("OpenCode config root changed before write");
|
|
8281
|
+
mkdirSync2(configRoot, { recursive: true, mode: 448 });
|
|
8282
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
8283
|
+
throw new Error("OpenCode config root changed during creation");
|
|
8284
|
+
assertWriteable2(configRoot, "OpenCode config root");
|
|
8285
|
+
if (secureCanonicalPath(agentsDirectory, "OpenCode agents path") !== agentsDirectory)
|
|
8286
|
+
throw new Error("OpenCode agents path changed before creation");
|
|
7300
8287
|
mkdirSync2(agentsDirectory, { recursive: true, mode: 448 });
|
|
8288
|
+
if (secureCanonicalPath(agentsDirectory, "OpenCode agents path") !== agentsDirectory)
|
|
8289
|
+
throw new Error("OpenCode agents path changed before write");
|
|
8290
|
+
assertWriteable2(configRoot, "OpenCode config root");
|
|
8291
|
+
assertWriteable2(agentsDirectory, "OpenCode agents directory");
|
|
8292
|
+
for (const path of result.removed) {
|
|
8293
|
+
if (secureCanonicalPath(path, "Managed global agent path") !== path)
|
|
8294
|
+
throw new Error(`Managed global agent path changed: ${path}`);
|
|
8295
|
+
const currentStat = stat(path);
|
|
8296
|
+
if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile()) {
|
|
8297
|
+
throw new Error(`Refusing to remove a changed or unmanaged global agent: ${path}`);
|
|
8298
|
+
}
|
|
8299
|
+
const current = readFileSync5(path, "utf8");
|
|
8300
|
+
if (!hasGeneratedAgentMarker(current) || current !== removals.get(path)) {
|
|
8301
|
+
throw new Error(`Refusing to remove a concurrently changed global agent: ${path}`);
|
|
8302
|
+
}
|
|
8303
|
+
unlinkSync2(path);
|
|
8304
|
+
}
|
|
7301
8305
|
for (const write of writes) {
|
|
7302
|
-
if (write.replace)
|
|
8306
|
+
if (write.replace) {
|
|
8307
|
+
const currentStat = stat(write.path);
|
|
8308
|
+
const current = currentStat?.isFile() && !currentStat.isSymbolicLink() ? readFileSync5(write.path, "utf8") : undefined;
|
|
8309
|
+
if (!current || !hasGeneratedAgentMarker(current) || current !== write.previous) {
|
|
8310
|
+
throw new Error(`Refusing to replace a concurrently changed global agent: ${write.path}`);
|
|
8311
|
+
}
|
|
7303
8312
|
replaceFile(write.path, write.content);
|
|
7304
|
-
else
|
|
8313
|
+
} else
|
|
7305
8314
|
createFile(write.path, write.content);
|
|
7306
8315
|
}
|
|
7307
8316
|
return result;
|
|
7308
8317
|
}
|
|
7309
8318
|
|
|
8319
|
+
// src/file-lock.ts
|
|
8320
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8321
|
+
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";
|
|
8322
|
+
import { dirname as dirname5 } from "node:path";
|
|
8323
|
+
function assertSecure(path) {
|
|
8324
|
+
const stat = lstatSync7(path);
|
|
8325
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
8326
|
+
throw new Error(`Lock directory is unsafe: ${path}`);
|
|
8327
|
+
const uid = process.getuid?.();
|
|
8328
|
+
if (uid !== undefined && (stat.uid !== uid || (stat.mode & 18) !== 0)) {
|
|
8329
|
+
throw new Error(`Lock directory must be owner-controlled and not group/world writable: ${path}`);
|
|
8330
|
+
}
|
|
8331
|
+
accessSync3(path, constants.W_OK | constants.X_OK);
|
|
8332
|
+
}
|
|
8333
|
+
function acquire(path, operation) {
|
|
8334
|
+
const canonicalPath = secureCanonicalPath(path, "Lock path");
|
|
8335
|
+
const directory = dirname5(canonicalPath);
|
|
8336
|
+
mkdirSync3(directory, { recursive: true, mode: 448 });
|
|
8337
|
+
if (secureCanonicalPath(canonicalPath, "Lock path") !== canonicalPath)
|
|
8338
|
+
throw new Error(`Lock path changed during creation: ${path}`);
|
|
8339
|
+
assertSecure(directory);
|
|
8340
|
+
const nonce = `${process.pid}:${randomUUID3()}
|
|
8341
|
+
`;
|
|
8342
|
+
let descriptor;
|
|
8343
|
+
try {
|
|
8344
|
+
descriptor = openSync3(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
8345
|
+
} catch (error) {
|
|
8346
|
+
if (error.code === "EEXIST") {
|
|
8347
|
+
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`);
|
|
8348
|
+
}
|
|
8349
|
+
throw error;
|
|
8350
|
+
}
|
|
8351
|
+
const created = fstatSync(descriptor);
|
|
8352
|
+
writeFileSync3(descriptor, nonce);
|
|
8353
|
+
return { path: canonicalPath, descriptor, nonce, dev: created.dev, ino: created.ino };
|
|
8354
|
+
}
|
|
8355
|
+
function release(lock) {
|
|
8356
|
+
closeSync3(lock.descriptor);
|
|
8357
|
+
if (existsSync6(lock.path)) {
|
|
8358
|
+
const current = lstatSync7(lock.path);
|
|
8359
|
+
if (current.isFile() && !current.isSymbolicLink() && current.dev === lock.dev && current.ino === lock.ino && readFileSync6(lock.path, "utf8") === lock.nonce)
|
|
8360
|
+
unlinkSync3(lock.path);
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
async function withExclusiveFileLock(path, callback) {
|
|
8364
|
+
const lock = acquire(path, "setup");
|
|
8365
|
+
try {
|
|
8366
|
+
return await callback();
|
|
8367
|
+
} finally {
|
|
8368
|
+
release(lock);
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
function withExclusiveFileLockSync(path, callback, operation = "operation") {
|
|
8372
|
+
const lock = acquire(path, operation);
|
|
8373
|
+
try {
|
|
8374
|
+
return callback();
|
|
8375
|
+
} finally {
|
|
8376
|
+
release(lock);
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
|
|
7310
8380
|
// src/cli/setup.ts
|
|
7311
|
-
var PACKAGE_SPEC = "@nail00749/agent-gvozd@^0.1.0";
|
|
7312
|
-
var SUPPORTED_VERSION = "0.0.0-beta-19425";
|
|
7313
8381
|
function assertVersion(version) {
|
|
7314
|
-
if (parseOpenCodeVersion(version) !==
|
|
7315
|
-
throw new Error(`Unsupported OpenCode version. Gvozd
|
|
8382
|
+
if (parseOpenCodeVersion(version) !== SUPPORTED_OPENCODE_VERSION) {
|
|
8383
|
+
throw new Error(`Unsupported OpenCode version. Gvozd ${PACKAGE_VERSION} requires ${SUPPORTED_OPENCODE_VERSION}.`);
|
|
7316
8384
|
}
|
|
7317
8385
|
}
|
|
7318
8386
|
async function selectProfile(input, client, configRoot) {
|
|
7319
8387
|
const catalog = parseModels(await client.models());
|
|
7320
8388
|
const config = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
7321
|
-
const hasGlobalConfig =
|
|
8389
|
+
const hasGlobalConfig = existsSync7(join8(configRoot, "gvozd", "config.jsonc"));
|
|
7322
8390
|
return chooseModelProfile({
|
|
7323
8391
|
catalog,
|
|
7324
8392
|
ui: input.ui,
|
|
@@ -7335,54 +8403,100 @@ async function confirm2(input, message) {
|
|
|
7335
8403
|
const answer = await input.ui.confirm({ message, initialValue: true });
|
|
7336
8404
|
return typeof answer === "symbol" ? false : answer;
|
|
7337
8405
|
}
|
|
8406
|
+
function sameSnapshot(left, right) {
|
|
8407
|
+
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;
|
|
8408
|
+
}
|
|
7338
8409
|
async function runSetup(input) {
|
|
7339
8410
|
const client = await (input.findClient ?? (() => findOpenCode()))();
|
|
7340
8411
|
const paths = await client.debugPaths();
|
|
7341
|
-
|
|
7342
|
-
if (!configRoot)
|
|
8412
|
+
if (!paths.config)
|
|
7343
8413
|
throw new Error("OpenCode did not report its config path");
|
|
7344
|
-
|
|
8414
|
+
const configRoot = secureCanonicalPath(paths.config, "OpenCode config root");
|
|
7345
8415
|
preflightGlobalConfig(configRoot);
|
|
8416
|
+
const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
|
|
8417
|
+
assertVersion(await client.version());
|
|
7346
8418
|
const before = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
7347
|
-
|
|
8419
|
+
const schemaSource = join8(dirname6(before.sources[0]), "schema.json");
|
|
8420
|
+
const packagedSchema = readFileSync7(schemaSource, "utf8");
|
|
8421
|
+
const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
|
|
8422
|
+
const preview = writeManagedAgents({ configRoot, agents: before.agents, check: true });
|
|
7348
8423
|
const profile = await selectProfile(input, client, configRoot);
|
|
7349
8424
|
if (!profile)
|
|
7350
8425
|
return { status: "cancelled" };
|
|
7351
|
-
input.output?.(
|
|
7352
|
-
|
|
7353
|
-
Write
|
|
8426
|
+
input.output?.([
|
|
8427
|
+
`Register ${PACKAGE_SPEC}`,
|
|
8428
|
+
`Write ${join8(configRoot, "gvozd", "config.jsonc")}`,
|
|
8429
|
+
`Write managed agents in ${join8(configRoot, "agents")}`,
|
|
8430
|
+
...preview.removed.length > 0 ? [`Remove ${preview.removed.length} stale or disabled managed agent(s)`] : []
|
|
8431
|
+
].join(`
|
|
8432
|
+
`));
|
|
7354
8433
|
if (!await confirm2(input, "Run setup?"))
|
|
7355
8434
|
return { status: "cancelled" };
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
const
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
const
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
8435
|
+
return withExclusiveFileLock(join8(configRoot, "gvozd", "setup.lock"), async () => {
|
|
8436
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
8437
|
+
throw new Error("OpenCode config root changed while setup awaited the lock");
|
|
8438
|
+
const lockedSchema = readFileSync7(schemaSource, "utf8");
|
|
8439
|
+
const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
|
|
8440
|
+
if (!sameSnapshot(previewSnapshot, snapshot))
|
|
8441
|
+
throw new Error("Global Gvozd configuration changed while setup awaited confirmation; review and rerun setup");
|
|
8442
|
+
const lockedBefore = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8443
|
+
writeManagedAgents({ configRoot, agents: lockedBefore.agents, check: true });
|
|
8444
|
+
const lockedProfile = input.yes ? await selectProfile(input, client, configRoot) : profile;
|
|
8445
|
+
if (!lockedProfile)
|
|
8446
|
+
throw new Error("Model profile changed while setup awaited the global lock; rerun setup");
|
|
8447
|
+
if (!sameSnapshot(snapshot, preflightGlobalConfig(configRoot, lockedSchema))) {
|
|
8448
|
+
throw new Error("Global Gvozd configuration changed during locked setup revalidation; review and rerun setup");
|
|
8449
|
+
}
|
|
8450
|
+
writeManagedAgents({ configRoot, agents: lockedBefore.agents, check: true });
|
|
8451
|
+
await client.pluginAdd(PACKAGE_SPEC);
|
|
8452
|
+
try {
|
|
8453
|
+
writeGlobalConfig({ configRoot, profile: lockedProfile, schemaSource: lockedSchema, snapshot });
|
|
8454
|
+
const configured = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8455
|
+
writeManagedAgents({ configRoot, agents: configured.agents });
|
|
8456
|
+
await client.serviceRestart();
|
|
8457
|
+
const report = await runDoctor({ client, configRoot, runtimeConfigRoot, cwd: input.cwd });
|
|
8458
|
+
return { status: "complete", report };
|
|
8459
|
+
} catch (error) {
|
|
8460
|
+
const detail = redactDiagnostic(error);
|
|
8461
|
+
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");
|
|
8462
|
+
}
|
|
8463
|
+
});
|
|
7368
8464
|
}
|
|
7369
8465
|
async function runConfigure(input) {
|
|
7370
8466
|
const client = await (input.findClient ?? (() => findOpenCode()))();
|
|
7371
8467
|
const paths = await client.debugPaths();
|
|
7372
|
-
|
|
7373
|
-
if (!configRoot)
|
|
8468
|
+
if (!paths.config)
|
|
7374
8469
|
throw new Error("OpenCode did not report its config path");
|
|
7375
|
-
|
|
8470
|
+
const configRoot = secureCanonicalPath(paths.config, "OpenCode config root");
|
|
7376
8471
|
preflightGlobalConfig(configRoot);
|
|
8472
|
+
const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
|
|
8473
|
+
assertVersion(await client.version());
|
|
8474
|
+
const config = loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8475
|
+
const schemaSource = join8(dirname6(config.sources[0]), "schema.json");
|
|
8476
|
+
const packagedSchema = readFileSync7(schemaSource, "utf8");
|
|
8477
|
+
const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
|
|
7377
8478
|
const profile = await selectProfile(input, client, configRoot);
|
|
7378
8479
|
if (!profile || !await confirm2(input, "Apply model configuration?"))
|
|
7379
8480
|
return { status: "cancelled" };
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
8481
|
+
return withExclusiveFileLock(join8(configRoot, "gvozd", "setup.lock"), async () => {
|
|
8482
|
+
if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
|
|
8483
|
+
throw new Error("OpenCode config root changed while configuration awaited the lock");
|
|
8484
|
+
const lockedSchema = readFileSync7(schemaSource, "utf8");
|
|
8485
|
+
const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
|
|
8486
|
+
if (!sameSnapshot(previewSnapshot, snapshot))
|
|
8487
|
+
throw new Error("Global Gvozd configuration changed while configuration awaited confirmation; review and rerun");
|
|
8488
|
+
loadConfig(input.cwd, { configRoot, includeProject: false });
|
|
8489
|
+
const lockedProfile = input.yes ? await selectProfile(input, client, configRoot) : profile;
|
|
8490
|
+
if (!lockedProfile)
|
|
8491
|
+
throw new Error("Model profile changed while configuration awaited the global lock; rerun configuration");
|
|
8492
|
+
if (!sameSnapshot(snapshot, preflightGlobalConfig(configRoot, lockedSchema))) {
|
|
8493
|
+
throw new Error("Global Gvozd configuration changed during locked configuration revalidation; review and rerun");
|
|
8494
|
+
}
|
|
8495
|
+
writeGlobalConfig({ configRoot, profile: lockedProfile, schemaSource: lockedSchema, snapshot });
|
|
8496
|
+
await client.serviceRestart();
|
|
8497
|
+
const report = await runDoctor({ client, configRoot, runtimeConfigRoot, cwd: input.cwd });
|
|
8498
|
+
return { status: "complete", report };
|
|
8499
|
+
});
|
|
7386
8500
|
}
|
|
7387
8501
|
function setupExitCode(result) {
|
|
7388
8502
|
if (result.status === "cancelled")
|
|
@@ -7391,13 +8505,13 @@ function setupExitCode(result) {
|
|
|
7391
8505
|
}
|
|
7392
8506
|
|
|
7393
8507
|
// src/sync.ts
|
|
7394
|
-
import { closeSync as
|
|
7395
|
-
import { randomUUID as
|
|
7396
|
-
import { basename, dirname as
|
|
8508
|
+
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";
|
|
8509
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
8510
|
+
import { basename, dirname as dirname7, join as join9, relative as relative4 } from "node:path";
|
|
7397
8511
|
function renderPluginEntrypoint(config, destination) {
|
|
7398
8512
|
let moduleSpecifier = "agent-gvozd/server";
|
|
7399
|
-
if (
|
|
7400
|
-
moduleSpecifier =
|
|
8513
|
+
if (realpathSync4(config.packageRoot) === realpathSync4(config.projectRoot)) {
|
|
8514
|
+
moduleSpecifier = relative4(destination, join9(config.packageRoot, "src", "index")).replaceAll("\\", "/");
|
|
7401
8515
|
if (!moduleSpecifier.startsWith("."))
|
|
7402
8516
|
moduleSpecifier = `./${moduleSpecifier}`;
|
|
7403
8517
|
}
|
|
@@ -7432,7 +8546,7 @@ function renderDiff(path, before, after) {
|
|
|
7432
8546
|
}
|
|
7433
8547
|
function stat2(path) {
|
|
7434
8548
|
try {
|
|
7435
|
-
return
|
|
8549
|
+
return lstatSync8(path);
|
|
7436
8550
|
} catch (error) {
|
|
7437
8551
|
if (error.code === "ENOENT")
|
|
7438
8552
|
return;
|
|
@@ -7440,9 +8554,9 @@ function stat2(path) {
|
|
|
7440
8554
|
}
|
|
7441
8555
|
}
|
|
7442
8556
|
function safeDirectory(root, segments, create) {
|
|
7443
|
-
let current =
|
|
8557
|
+
let current = realpathSync4(root);
|
|
7444
8558
|
for (const segment of segments) {
|
|
7445
|
-
current =
|
|
8559
|
+
current = join9(current, segment);
|
|
7446
8560
|
const currentStat = stat2(current);
|
|
7447
8561
|
if (currentStat) {
|
|
7448
8562
|
if (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {
|
|
@@ -7451,7 +8565,7 @@ function safeDirectory(root, segments, create) {
|
|
|
7451
8565
|
continue;
|
|
7452
8566
|
}
|
|
7453
8567
|
if (create)
|
|
7454
|
-
|
|
8568
|
+
mkdirSync4(current);
|
|
7455
8569
|
}
|
|
7456
8570
|
return current;
|
|
7457
8571
|
}
|
|
@@ -7464,145 +8578,191 @@ function assertRegularFile(path) {
|
|
|
7464
8578
|
return true;
|
|
7465
8579
|
}
|
|
7466
8580
|
function createFile2(path, content) {
|
|
7467
|
-
const descriptor =
|
|
8581
|
+
const descriptor = openSync4(path, "wx", 384);
|
|
7468
8582
|
try {
|
|
7469
|
-
|
|
8583
|
+
writeFileSync4(descriptor, content);
|
|
7470
8584
|
} finally {
|
|
7471
|
-
|
|
8585
|
+
closeSync4(descriptor);
|
|
7472
8586
|
}
|
|
7473
8587
|
}
|
|
7474
8588
|
function replaceFile2(path, content) {
|
|
7475
|
-
const temporary = `${path}.tmp-${process.pid}-${
|
|
8589
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID4()}`;
|
|
7476
8590
|
createFile2(temporary, content);
|
|
7477
8591
|
try {
|
|
7478
8592
|
renameSync3(temporary, path);
|
|
7479
8593
|
} catch (error) {
|
|
7480
8594
|
try {
|
|
7481
|
-
|
|
8595
|
+
unlinkSync4(temporary);
|
|
7482
8596
|
} catch {}
|
|
7483
8597
|
throw error;
|
|
7484
8598
|
}
|
|
7485
8599
|
}
|
|
7486
|
-
function
|
|
8600
|
+
function planProjectTemplate(config, result, check) {
|
|
7487
8601
|
const directory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], !check);
|
|
7488
|
-
const
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
8602
|
+
const writes = [];
|
|
8603
|
+
const configPath = join9(directory, "config.jsonc");
|
|
8604
|
+
if (!assertRegularFile(configPath)) {
|
|
8605
|
+
result.created.push(configPath);
|
|
8606
|
+
writes.push({
|
|
8607
|
+
target: configPath,
|
|
8608
|
+
replace: false,
|
|
8609
|
+
content: [
|
|
8610
|
+
"{",
|
|
8611
|
+
' "$schema": "./schema.json",',
|
|
8612
|
+
" // Project overrides are merged after built-in and global configuration.",
|
|
8613
|
+
' "agents": {}',
|
|
8614
|
+
"}",
|
|
8615
|
+
""
|
|
8616
|
+
].join(`
|
|
8617
|
+
`)
|
|
8618
|
+
});
|
|
7501
8619
|
}
|
|
7502
|
-
const schemaSource =
|
|
7503
|
-
const schemaTarget =
|
|
7504
|
-
const schema =
|
|
7505
|
-
if (assertRegularFile(schemaTarget))
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
8620
|
+
const schemaSource = join9(dirname7(config.sources[0]), "schema.json");
|
|
8621
|
+
const schemaTarget = join9(directory, "schema.json");
|
|
8622
|
+
const schema = readFileSync8(schemaSource, "utf8");
|
|
8623
|
+
if (!assertRegularFile(schemaTarget)) {
|
|
8624
|
+
result.created.push(schemaTarget);
|
|
8625
|
+
writes.push({ target: schemaTarget, content: schema, replace: false });
|
|
8626
|
+
} else {
|
|
8627
|
+
const current = readFileSync8(schemaTarget, "utf8");
|
|
8628
|
+
if (current !== schema) {
|
|
8629
|
+
if (!hasGeneratedSchemaMarker(current) && !isEquivalentLegacySchema(current, schema)) {
|
|
8630
|
+
throw new Error(`Refusing to overwrite an unmanaged project schema: ${schemaTarget}`);
|
|
8631
|
+
}
|
|
8632
|
+
result.updated.push(schemaTarget);
|
|
8633
|
+
writes.push({ target: schemaTarget, content: schema, replace: true, previous: current });
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
return writes;
|
|
7509
8637
|
}
|
|
7510
|
-
function
|
|
8638
|
+
function syncAgentsUnlocked(config, options) {
|
|
7511
8639
|
const check = options.check ?? false;
|
|
7512
8640
|
const destination = safeDirectory(config.projectRoot, [".opencode", "agents"], false);
|
|
7513
8641
|
const pluginDestination = safeDirectory(config.projectRoot, [".opencode", "plugins", "agent-gvozd"], false);
|
|
7514
8642
|
const result = { created: [], updated: [], removed: [], unchanged: [] };
|
|
7515
8643
|
const writes = [];
|
|
7516
8644
|
let pluginWrite;
|
|
8645
|
+
const removals = new Map;
|
|
8646
|
+
const templateWrites = planProjectTemplate(config, result, check);
|
|
7517
8647
|
const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
|
|
7518
8648
|
if (stat2(destination)) {
|
|
7519
|
-
for (const entry of
|
|
8649
|
+
for (const entry of readdirSync5(destination, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7520
8650
|
if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
|
|
7521
8651
|
continue;
|
|
7522
|
-
const target =
|
|
7523
|
-
const current =
|
|
7524
|
-
if (!current
|
|
8652
|
+
const target = join9(destination, entry.name);
|
|
8653
|
+
const current = readFileSync8(target, "utf8");
|
|
8654
|
+
if (!hasGeneratedAgentMarker(current))
|
|
7525
8655
|
continue;
|
|
7526
8656
|
result.removed.push(target);
|
|
8657
|
+
removals.set(target, current);
|
|
7527
8658
|
options.onDiff?.(renderDiff(target, current, ""));
|
|
7528
8659
|
}
|
|
7529
8660
|
}
|
|
7530
8661
|
for (const [id, agent] of Object.entries(config.agents)) {
|
|
7531
8662
|
if (agent.disabled)
|
|
7532
8663
|
continue;
|
|
7533
|
-
const target =
|
|
8664
|
+
const target = join9(destination, `${id}.md`);
|
|
7534
8665
|
const content = renderAgent(agent);
|
|
7535
8666
|
if (!assertRegularFile(target)) {
|
|
7536
8667
|
result.created.push(target);
|
|
7537
8668
|
writes.push({ target, content, replace: false });
|
|
7538
8669
|
continue;
|
|
7539
8670
|
}
|
|
7540
|
-
const current =
|
|
8671
|
+
const current = readFileSync8(target, "utf8");
|
|
7541
8672
|
if (current === content) {
|
|
7542
8673
|
result.unchanged.push(target);
|
|
7543
8674
|
continue;
|
|
7544
8675
|
}
|
|
7545
|
-
if (!current
|
|
8676
|
+
if (!hasGeneratedAgentMarker(current)) {
|
|
7546
8677
|
throw new Error(`Refusing to overwrite a non-generated agent file: ${target}`);
|
|
7547
8678
|
}
|
|
7548
8679
|
result.updated.push(target);
|
|
7549
8680
|
options.onDiff?.(renderDiff(target, current, content));
|
|
7550
|
-
writes.push({ target, content, replace: true });
|
|
8681
|
+
writes.push({ target, content, replace: true, previous: current });
|
|
7551
8682
|
}
|
|
7552
|
-
const pluginTarget =
|
|
8683
|
+
const pluginTarget = join9(pluginDestination, "index.ts");
|
|
7553
8684
|
const pluginContent = renderPluginEntrypoint(config, pluginDestination);
|
|
7554
8685
|
if (!assertRegularFile(pluginTarget)) {
|
|
7555
8686
|
result.created.push(pluginTarget);
|
|
7556
8687
|
pluginWrite = { target: pluginTarget, content: pluginContent, replace: false };
|
|
7557
8688
|
} else {
|
|
7558
|
-
const current =
|
|
8689
|
+
const current = readFileSync8(pluginTarget, "utf8");
|
|
7559
8690
|
if (current === pluginContent) {
|
|
7560
8691
|
result.unchanged.push(pluginTarget);
|
|
7561
8692
|
} else {
|
|
7562
|
-
if (!current
|
|
8693
|
+
if (!hasGeneratedPluginMarker(current)) {
|
|
7563
8694
|
throw new Error(`Refusing to overwrite a non-generated plugin entrypoint: ${pluginTarget}`);
|
|
7564
8695
|
}
|
|
7565
8696
|
result.updated.push(pluginTarget);
|
|
7566
8697
|
options.onDiff?.(renderDiff(pluginTarget, current, pluginContent));
|
|
7567
|
-
pluginWrite = { target: pluginTarget, content: pluginContent, replace: true };
|
|
8698
|
+
pluginWrite = { target: pluginTarget, content: pluginContent, replace: true, previous: current };
|
|
7568
8699
|
}
|
|
7569
8700
|
}
|
|
7570
8701
|
if (!check) {
|
|
7571
8702
|
const writableDestination = safeDirectory(config.projectRoot, [".opencode", "agents"], true);
|
|
7572
8703
|
const writablePluginDestination = safeDirectory(config.projectRoot, [".opencode", "plugins", "agent-gvozd"], true);
|
|
7573
8704
|
for (const target of result.removed) {
|
|
7574
|
-
if (!assertRegularFile(target)
|
|
8705
|
+
if (!assertRegularFile(target)) {
|
|
7575
8706
|
throw new Error(`Refusing to remove a changed or unsafe agent file: ${target}`);
|
|
7576
8707
|
}
|
|
7577
|
-
|
|
8708
|
+
const current = readFileSync8(target, "utf8");
|
|
8709
|
+
if (!hasGeneratedAgentMarker(current) || current !== removals.get(target)) {
|
|
8710
|
+
throw new Error(`Refusing to remove a concurrently changed agent file: ${target}`);
|
|
8711
|
+
}
|
|
8712
|
+
unlinkSync4(target);
|
|
7578
8713
|
}
|
|
7579
8714
|
for (const write of writes) {
|
|
7580
8715
|
if (!write.replace) {
|
|
7581
|
-
createFile2(
|
|
8716
|
+
createFile2(join9(writableDestination, basename(write.target)), write.content);
|
|
7582
8717
|
continue;
|
|
7583
8718
|
}
|
|
7584
|
-
if (!assertRegularFile(write.target)
|
|
8719
|
+
if (!assertRegularFile(write.target)) {
|
|
7585
8720
|
throw new Error(`Refusing to replace a changed or unsafe agent file: ${write.target}`);
|
|
7586
8721
|
}
|
|
8722
|
+
const current = readFileSync8(write.target, "utf8");
|
|
8723
|
+
if (!hasGeneratedAgentMarker(current) || current !== write.previous) {
|
|
8724
|
+
throw new Error(`Refusing to replace a concurrently changed agent file: ${write.target}`);
|
|
8725
|
+
}
|
|
7587
8726
|
replaceFile2(write.target, write.content);
|
|
7588
8727
|
}
|
|
7589
8728
|
if (pluginWrite) {
|
|
7590
|
-
const target =
|
|
8729
|
+
const target = join9(writablePluginDestination, basename(pluginWrite.target));
|
|
7591
8730
|
if (!pluginWrite.replace) {
|
|
7592
8731
|
createFile2(target, pluginWrite.content);
|
|
7593
8732
|
} else {
|
|
7594
|
-
if (!assertRegularFile(target)
|
|
8733
|
+
if (!assertRegularFile(target)) {
|
|
7595
8734
|
throw new Error(`Refusing to replace a changed or unsafe plugin entrypoint: ${target}`);
|
|
7596
8735
|
}
|
|
8736
|
+
const current = readFileSync8(target, "utf8");
|
|
8737
|
+
if (!hasGeneratedPluginMarker(current) || current !== pluginWrite.previous) {
|
|
8738
|
+
throw new Error(`Refusing to replace a concurrently changed plugin entrypoint: ${target}`);
|
|
8739
|
+
}
|
|
7597
8740
|
replaceFile2(target, pluginWrite.content);
|
|
7598
8741
|
}
|
|
7599
8742
|
}
|
|
8743
|
+
const templateDirectory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], true);
|
|
8744
|
+
for (const write of templateWrites) {
|
|
8745
|
+
const target = join9(templateDirectory, basename(write.target));
|
|
8746
|
+
if (!write.replace)
|
|
8747
|
+
createFile2(target, write.content);
|
|
8748
|
+
else {
|
|
8749
|
+
if (!assertRegularFile(target) || readFileSync8(target, "utf8") !== write.previous) {
|
|
8750
|
+
throw new Error(`Refusing to replace a concurrently changed project schema: ${target}`);
|
|
8751
|
+
}
|
|
8752
|
+
replaceFile2(target, write.content);
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
7600
8755
|
}
|
|
7601
|
-
ensureProjectTemplate(config, check);
|
|
7602
8756
|
if (!check)
|
|
7603
8757
|
safeDirectory(config.projectRoot, ["docs", ".gvozd", "tasks"], true);
|
|
7604
8758
|
return result;
|
|
7605
8759
|
}
|
|
8760
|
+
function syncAgents(config, options = {}) {
|
|
8761
|
+
if (options.check)
|
|
8762
|
+
return syncAgentsUnlocked(config, options);
|
|
8763
|
+
const root = realpathSync4(config.projectRoot);
|
|
8764
|
+
return withExclusiveFileLockSync(join9(root, ".agent-gvozd-sync.lock"), () => syncAgentsUnlocked(config, options), "sync");
|
|
8765
|
+
}
|
|
7606
8766
|
function formatSyncResult(result, check) {
|
|
7607
8767
|
const lines = [check ? "agent-gvozd sync check" : "agent-gvozd sync complete"];
|
|
7608
8768
|
for (const [label, paths] of [
|
|
@@ -7631,8 +8791,23 @@ var promptUI = {
|
|
|
7631
8791
|
intro,
|
|
7632
8792
|
outro
|
|
7633
8793
|
};
|
|
8794
|
+
var HELP = [
|
|
8795
|
+
"Usage: gvozd <setup|config|doctor|sync|trust-project> [options]",
|
|
8796
|
+
"",
|
|
8797
|
+
"Commands:",
|
|
8798
|
+
" setup [--yes] Install or upgrade the global agent team",
|
|
8799
|
+
" config [--yes] Configure model preferences",
|
|
8800
|
+
" doctor [--json] Diagnose the global installation",
|
|
8801
|
+
" sync [--check] Maintain the legacy project-local installation",
|
|
8802
|
+
" trust-project [directory] Print the current project trust token",
|
|
8803
|
+
"",
|
|
8804
|
+
"Options:",
|
|
8805
|
+
" --help Show this help",
|
|
8806
|
+
" --version Show the Gvozd version"
|
|
8807
|
+
].join(`
|
|
8808
|
+
`);
|
|
7634
8809
|
function usage(io) {
|
|
7635
|
-
io.stderr(
|
|
8810
|
+
io.stderr(HELP);
|
|
7636
8811
|
return 2;
|
|
7637
8812
|
}
|
|
7638
8813
|
function parseFlags(args, allowed) {
|
|
@@ -7644,6 +8819,14 @@ function parseFlags(args, allowed) {
|
|
|
7644
8819
|
async function runCli(args, io = defaultIO, commands = { setup: runSetup, configure: runConfigure }) {
|
|
7645
8820
|
const [command, ...rest] = args;
|
|
7646
8821
|
try {
|
|
8822
|
+
if ((command === "--help" || command === "help") && rest.length === 0) {
|
|
8823
|
+
io.stdout(HELP);
|
|
8824
|
+
return 0;
|
|
8825
|
+
}
|
|
8826
|
+
if (command === "--version" && rest.length === 0) {
|
|
8827
|
+
io.stdout(PACKAGE_VERSION);
|
|
8828
|
+
return 0;
|
|
8829
|
+
}
|
|
7647
8830
|
if (command === "setup" || command === "config") {
|
|
7648
8831
|
const parsed = parseFlags(rest, ["--yes"]);
|
|
7649
8832
|
if (!parsed || parsed.positional.length > 0)
|
|
@@ -7672,7 +8855,12 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
|
|
|
7672
8855
|
try {
|
|
7673
8856
|
const client = await (commands.findClient ?? findOpenCode)();
|
|
7674
8857
|
const paths = await client.debugPaths();
|
|
7675
|
-
report = await runDoctor({
|
|
8858
|
+
report = await runDoctor({
|
|
8859
|
+
client,
|
|
8860
|
+
configRoot: paths.config,
|
|
8861
|
+
runtimeConfigRoot: resolveOpenCodeConfigRoot(),
|
|
8862
|
+
cwd: io.cwd()
|
|
8863
|
+
});
|
|
7676
8864
|
} catch (error) {
|
|
7677
8865
|
report = doctorOperationalFailure(error);
|
|
7678
8866
|
}
|
|
@@ -7690,14 +8878,21 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
|
|
|
7690
8878
|
io.stdout(formatSyncResult(result, check));
|
|
7691
8879
|
return check && result.created.length + result.updated.length + result.removed.length > 0 ? 1 : 0;
|
|
7692
8880
|
}
|
|
8881
|
+
if (command === "trust-project") {
|
|
8882
|
+
const parsed = parseFlags(rest, []);
|
|
8883
|
+
if (!parsed || parsed.positional.length > 1)
|
|
8884
|
+
return usage(io);
|
|
8885
|
+
io.stdout(computeProjectTrustToken(parsed.positional[0] ?? io.cwd()));
|
|
8886
|
+
return 0;
|
|
8887
|
+
}
|
|
7693
8888
|
return usage(io);
|
|
7694
8889
|
} catch (error) {
|
|
7695
|
-
io.stderr(
|
|
8890
|
+
io.stderr(redactDiagnostic(error));
|
|
7696
8891
|
return 1;
|
|
7697
8892
|
}
|
|
7698
8893
|
}
|
|
7699
8894
|
var invokedPath = process.argv[1];
|
|
7700
|
-
if (invokedPath &&
|
|
8895
|
+
if (invokedPath && realpathSync5(invokedPath) === realpathSync5(fileURLToPath2(import.meta.url))) {
|
|
7701
8896
|
process.exitCode = await runCli(process.argv.slice(2));
|
|
7702
8897
|
}
|
|
7703
8898
|
export {
|