toolfactory 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/dist/commands.d.ts +1 -1
- package/dist/distribution/native.d.ts +50 -0
- package/dist/distribution/native.js +414 -0
- package/dist/distribution/native.test.d.ts +1 -0
- package/dist/distribution/native.test.js +127 -0
- package/dist/distribution/system.d.ts +57 -0
- package/dist/distribution/system.js +637 -0
- package/dist/distribution/system.test.d.ts +1 -0
- package/dist/distribution/system.test.js +116 -0
- package/dist/e2e.test.js +7 -1
- package/dist/hosts/openclaw.js +6 -2
- package/dist/hosts/vscode.d.ts +1 -0
- package/dist/hosts/vscode.js +67 -0
- package/dist/model.d.ts +62 -2
- package/dist/model.js +9 -0
- package/dist/ops.d.ts +52 -0
- package/dist/project/apply.d.ts +1 -1
- package/dist/project/apply.js +194 -17
- package/dist/project/apply.test.js +103 -0
- package/dist/project/gate.d.ts +17 -2
- package/dist/project/gate.js +68 -3
- package/dist/project/history.d.ts +29 -1
- package/dist/project/lock.d.ts +5 -0
- package/dist/project/lock.js +9 -0
- package/dist/surfaces/agent-plugins.js +5 -4
- package/dist/surfaces/agents.js +21 -3
- package/dist/surfaces/agents.test.js +12 -0
- package/dist/surfaces/claude.js +8 -3
- package/dist/surfaces/codex.js +8 -3
- package/dist/surfaces/codex.test.js +8 -8
- package/dist/surfaces/cursor.js +2 -2
- package/dist/surfaces/distribution.test.js +3 -3
- package/dist/surfaces/dsh.js +27 -7
- package/dist/surfaces/dsh.test.js +37 -3
- package/dist/surfaces/gemini.js +2 -2
- package/dist/surfaces/gemini.test.js +4 -4
- package/dist/surfaces/hermes-native.js +38 -9
- package/dist/surfaces/hermes-native.test.js +32 -11
- package/dist/surfaces/mcp-registry.js +43 -5
- package/dist/surfaces/mcp-registry.test.js +18 -0
- package/dist/surfaces/openclaw-native.js +10 -8
- package/dist/surfaces/openclaw-native.test.js +30 -0
- package/dist/surfaces/readme.d.ts +16 -0
- package/dist/surfaces/readme.js +7 -0
- package/dist/surfaces/registry.js +6 -0
- package/dist/surfaces/vscode-extension.d.ts +14 -0
- package/dist/surfaces/vscode-extension.js +312 -0
- package/dist/surfaces/vscode-extension.test.d.ts +1 -0
- package/dist/surfaces/vscode-extension.test.js +81 -0
- package/dist/surfaces/vscode-scaffold.d.ts +43 -0
- package/dist/surfaces/vscode-scaffold.js +43 -0
- package/dist/surfaces/web.d.ts +1 -1
- package/dist/surfaces/web.js +1 -1
- package/dist/surfaces/workflows.js +49 -10
- package/dist/surfaces/workflows.test.js +3 -2
- package/dist/toolfactory/cli.js +1 -1
- package/dist/toolfactory/mcp.js +1 -1
- package/package.json +2 -1
- package/src/distribution/native.test.ts +161 -0
- package/src/distribution/native.ts +460 -0
- package/src/distribution/system.test.ts +136 -0
- package/src/distribution/system.ts +688 -0
- package/src/e2e.test.ts +7 -1
- package/src/hosts/openclaw.ts +4 -2
- package/src/hosts/vscode.ts +79 -0
- package/src/model.ts +15 -1
- package/src/project/apply.test.ts +120 -0
- package/src/project/apply.ts +257 -15
- package/src/project/gate.ts +96 -4
- package/src/project/lock.ts +12 -0
- package/src/surfaces/agent-plugins.ts +5 -4
- package/src/surfaces/agents.test.ts +16 -0
- package/src/surfaces/agents.ts +31 -3
- package/src/surfaces/claude.ts +7 -3
- package/src/surfaces/codex.test.ts +7 -7
- package/src/surfaces/codex.ts +8 -3
- package/src/surfaces/cursor.ts +2 -10
- package/src/surfaces/distribution.test.ts +2 -2
- package/src/surfaces/dsh.test.ts +38 -2
- package/src/surfaces/dsh.ts +28 -7
- package/src/surfaces/gemini.test.ts +3 -3
- package/src/surfaces/gemini.ts +1 -2
- package/src/surfaces/hermes-native.test.ts +39 -10
- package/src/surfaces/hermes-native.ts +41 -11
- package/src/surfaces/mcp-registry.test.ts +20 -0
- package/src/surfaces/mcp-registry.ts +46 -6
- package/src/surfaces/openclaw-native.test.ts +37 -0
- package/src/surfaces/openclaw-native.ts +49 -47
- package/src/surfaces/readme.ts +10 -0
- package/src/surfaces/registry.ts +6 -0
- package/src/surfaces/vscode-extension.test.ts +89 -0
- package/src/surfaces/vscode-extension.ts +345 -0
- package/src/surfaces/vscode-scaffold.ts +55 -0
- package/src/surfaces/web.ts +1 -1
- package/src/surfaces/workflows.test.ts +3 -4
- package/src/surfaces/workflows.ts +49 -10
- package/src/toolfactory/cli.ts +1 -1
- package/src/toolfactory/mcp.ts +1 -1
- package/web/dist/assets/{index-Spapkkvh.js → index-BHR6gCfm.js} +1 -1
- package/web/dist/index.html +1 -1
package/dist/project/apply.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
11
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
11
12
|
import { LOCK_PATH, readLock, serializeLock, sha256 } from "./lock.js";
|
|
12
13
|
function locate(text, region) {
|
|
13
14
|
const start = text.indexOf(region.begin);
|
|
@@ -70,13 +71,156 @@ export function pickPatch(document, patch, owned = [], prefix = "") {
|
|
|
70
71
|
}
|
|
71
72
|
return out;
|
|
72
73
|
}
|
|
74
|
+
function valueAt(document, path) {
|
|
75
|
+
return path
|
|
76
|
+
.split(".")
|
|
77
|
+
.reduce((value, part) => (isRecord(value) ? value[part] : undefined), document);
|
|
78
|
+
}
|
|
79
|
+
function setAt(document, path, value) {
|
|
80
|
+
const parts = path.split(".");
|
|
81
|
+
let target = document;
|
|
82
|
+
for (const part of parts.slice(0, -1)) {
|
|
83
|
+
const next = target[part];
|
|
84
|
+
if (isRecord(next))
|
|
85
|
+
target = next;
|
|
86
|
+
else {
|
|
87
|
+
const created = {};
|
|
88
|
+
target[part] = created;
|
|
89
|
+
target = created;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
target[parts.at(-1)] = value;
|
|
93
|
+
}
|
|
94
|
+
function omitKeyedArrays(patch, keyedArrays, prefix = "") {
|
|
95
|
+
const out = {};
|
|
96
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
97
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
98
|
+
if (path in keyedArrays)
|
|
99
|
+
continue;
|
|
100
|
+
out[key] = isRecord(value) ? omitKeyedArrays(value, keyedArrays, path) : value;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function keyedArrayEntries(value, id, path) {
|
|
105
|
+
if (!Array.isArray(value) ||
|
|
106
|
+
value.some((entry) => !isRecord(entry) || typeof entry[id] !== "string")) {
|
|
107
|
+
throw new Error(`${path} must be an array of objects with a string ${id} field.`);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
function keyedArrayState(file) {
|
|
112
|
+
if (!file.keyedArrays || Object.keys(file.keyedArrays).length === 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
return Object.fromEntries(Object.entries(file.keyedArrays).map(([path, id]) => [
|
|
115
|
+
path,
|
|
116
|
+
{
|
|
117
|
+
id,
|
|
118
|
+
values: keyedArrayEntries(valueAt(file.patch, path), id, path).map((entry) => entry[id]),
|
|
119
|
+
entries: Object.fromEntries(keyedArrayEntries(valueAt(file.patch, path), id, path).map((entry) => [
|
|
120
|
+
entry[id],
|
|
121
|
+
entry,
|
|
122
|
+
])),
|
|
123
|
+
},
|
|
124
|
+
]));
|
|
125
|
+
}
|
|
126
|
+
function removeProjectedLeaves(entry, previous, next) {
|
|
127
|
+
const out = { ...entry };
|
|
128
|
+
for (const [key, oldValue] of Object.entries(previous)) {
|
|
129
|
+
if (!(key in next))
|
|
130
|
+
delete out[key];
|
|
131
|
+
else if (isRecord(oldValue) && isRecord(next[key]) && isRecord(out[key]))
|
|
132
|
+
out[key] = removeProjectedLeaves(out[key], oldValue, next[key]);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function mergeKeyedArrays(document, file, previous) {
|
|
137
|
+
let changed = false;
|
|
138
|
+
for (const [path, id] of Object.entries(file.keyedArrays ?? {})) {
|
|
139
|
+
const desired = keyedArrayEntries(valueAt(file.patch, path), id, path);
|
|
140
|
+
const previousIds = new Set(previous.files[file.path]?.keyedArrays?.[path]?.values ?? []);
|
|
141
|
+
const current = valueAt(document, path);
|
|
142
|
+
const entries = current === undefined ? [] : keyedArrayEntries(current, id, path);
|
|
143
|
+
const desiredById = new Map(desired.map((entry) => [entry[id], entry]));
|
|
144
|
+
const retained = [];
|
|
145
|
+
const generated = [];
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
const key = entry[id];
|
|
148
|
+
const replacement = desiredById.get(key);
|
|
149
|
+
if (previousIds.has(key)) {
|
|
150
|
+
if (replacement) {
|
|
151
|
+
const previousEntry = previous.files[file.path]?.keyedArrays?.[path]?.entries?.[key];
|
|
152
|
+
generated.push(deepMerge(previousEntry ? removeProjectedLeaves(entry, previousEntry, replacement) : entry, replacement));
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// Migration from a formerly whole generated file: an identical entry is ours already.
|
|
157
|
+
if (replacement && JSON.stringify(entry) === JSON.stringify(replacement)) {
|
|
158
|
+
generated.push(entry);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (replacement)
|
|
162
|
+
throw new Error(`${file.path} ${path} already has author entry ${id}=${key}.`);
|
|
163
|
+
retained.push(entry);
|
|
164
|
+
}
|
|
165
|
+
const next = [
|
|
166
|
+
...retained,
|
|
167
|
+
...generated,
|
|
168
|
+
// A generated row may have been deleted by hand. It remains owned, so put its desired
|
|
169
|
+
// replacement back even when the previous lock knows its id already.
|
|
170
|
+
...desired.filter((entry) => !generated.some((current) => current[id] === entry[id])),
|
|
171
|
+
];
|
|
172
|
+
if (JSON.stringify(entries) !== JSON.stringify(next))
|
|
173
|
+
changed = true;
|
|
174
|
+
setAt(document, path, next);
|
|
175
|
+
}
|
|
176
|
+
return changed;
|
|
177
|
+
}
|
|
178
|
+
/** Select only generated leaves from an entry, leaving authored extension fields out of drift. */
|
|
179
|
+
function pickProjectedEntry(current, projected) {
|
|
180
|
+
if (!current)
|
|
181
|
+
return undefined;
|
|
182
|
+
const picked = {};
|
|
183
|
+
for (const [key, value] of Object.entries(projected)) {
|
|
184
|
+
const actual = current[key];
|
|
185
|
+
picked[key] = isRecord(value) && isRecord(actual) ? pickProjectedEntry(actual, value) : actual;
|
|
186
|
+
}
|
|
187
|
+
return picked;
|
|
188
|
+
}
|
|
189
|
+
function removeKeyedArrayEntries(document, keyedArrays) {
|
|
190
|
+
let changed = false;
|
|
191
|
+
for (const [path, state] of Object.entries(keyedArrays ?? {})) {
|
|
192
|
+
const current = valueAt(document, path);
|
|
193
|
+
if (!Array.isArray(current))
|
|
194
|
+
continue;
|
|
195
|
+
const next = current.filter((entry) => !isRecord(entry) || !state.values.includes(entry[state.id]));
|
|
196
|
+
if (next.length !== current.length) {
|
|
197
|
+
setAt(document, path, next);
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return changed;
|
|
202
|
+
}
|
|
203
|
+
function pickMerge(document, file) {
|
|
204
|
+
const picked = pickPatch(document, file.patch, file.owned);
|
|
205
|
+
for (const [path, id] of Object.entries(file.keyedArrays ?? {})) {
|
|
206
|
+
const desired = keyedArrayEntries(valueAt(file.patch, path), id, path);
|
|
207
|
+
const current = Array.isArray(valueAt(document, path))
|
|
208
|
+
? valueAt(document, path)
|
|
209
|
+
: [];
|
|
210
|
+
const byId = new Map(current.filter(isRecord).map((entry) => [entry[id], entry]));
|
|
211
|
+
setAt(picked, path, desired.map((entry) => pickProjectedEntry(byId.get(entry[id]), entry)));
|
|
212
|
+
}
|
|
213
|
+
return picked;
|
|
214
|
+
}
|
|
73
215
|
/** The dotted paths a patch writes: one per leaf value, and one per object it owns whole. */
|
|
74
|
-
export function patchKeys(patch, owned = [], prefix = "") {
|
|
216
|
+
export function patchKeys(patch, owned = [], keyedArrays = {}, prefix = "") {
|
|
75
217
|
const keys = [];
|
|
76
218
|
for (const [key, value] of Object.entries(patch)) {
|
|
77
219
|
const path = prefix ? `${prefix}.${key}` : key;
|
|
220
|
+
if (path in keyedArrays)
|
|
221
|
+
continue;
|
|
78
222
|
if (isRecord(value) && !owned.includes(path) && Object.keys(value).length > 0) {
|
|
79
|
-
keys.push(...patchKeys(value, owned, path));
|
|
223
|
+
keys.push(...patchKeys(value, owned, keyedArrays, path));
|
|
80
224
|
}
|
|
81
225
|
else {
|
|
82
226
|
keys.push(path);
|
|
@@ -119,12 +263,14 @@ export function removeKeys(document, keys) {
|
|
|
119
263
|
return removed;
|
|
120
264
|
}
|
|
121
265
|
function parseDocument(text, format) {
|
|
122
|
-
return (format === "toml" ? parseToml(text) : JSON.parse(text));
|
|
266
|
+
return (format === "toml" ? parseToml(text) : format === "yaml" ? parseYaml(text) : JSON.parse(text));
|
|
123
267
|
}
|
|
124
268
|
function serializeDocument(document, format) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
269
|
+
if (format === "toml")
|
|
270
|
+
return `${stringifyToml(document)}\n`;
|
|
271
|
+
if (format === "yaml")
|
|
272
|
+
return stringifyYaml(document);
|
|
273
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
128
274
|
}
|
|
129
275
|
/** The content toolfactory manages, as one string for hashing and comparison. */
|
|
130
276
|
export function managedContent(file) {
|
|
@@ -152,11 +298,15 @@ function currentManagedContent(root, file) {
|
|
|
152
298
|
return text;
|
|
153
299
|
if (file.kind === "region")
|
|
154
300
|
return extractRegions(text, file)?.join(" ");
|
|
155
|
-
return JSON.stringify(
|
|
301
|
+
return JSON.stringify(pickMerge(parseDocument(text, file.format), file));
|
|
156
302
|
}
|
|
157
303
|
/** The two structured formats merge files use; the extension is the only discriminator needed. */
|
|
158
304
|
function documentFormat(path) {
|
|
159
|
-
return path.endsWith(".toml")
|
|
305
|
+
return path.endsWith(".toml")
|
|
306
|
+
? "toml"
|
|
307
|
+
: path.endsWith(".yaml") || path.endsWith(".yml")
|
|
308
|
+
? "yaml"
|
|
309
|
+
: "json";
|
|
160
310
|
}
|
|
161
311
|
/** The keys the lock recorded for a path that the current patch no longer writes. */
|
|
162
312
|
function staleKeys(lock, path, current = []) {
|
|
@@ -169,6 +319,16 @@ function stranded(root, path, keys) {
|
|
|
169
319
|
const document = parseDocument(readFileSync(join(root, path), "utf8"), documentFormat(path));
|
|
170
320
|
return keys.some((key) => hasKey(document, key));
|
|
171
321
|
}
|
|
322
|
+
function keyedEntriesRemain(root, path, keyedArrays) {
|
|
323
|
+
if (!keyedArrays || !existsSync(join(root, path)))
|
|
324
|
+
return false;
|
|
325
|
+
const document = parseDocument(readFileSync(join(root, path), "utf8"), documentFormat(path));
|
|
326
|
+
return Object.entries(keyedArrays).some(([arrayPath, state]) => {
|
|
327
|
+
const current = valueAt(document, arrayPath);
|
|
328
|
+
return (Array.isArray(current) &&
|
|
329
|
+
current.some((entry) => isRecord(entry) && state.values.includes(entry[state.id])));
|
|
330
|
+
});
|
|
331
|
+
}
|
|
172
332
|
/** The marker pairs the lock recorded for a path that the current plan no longer writes. */
|
|
173
333
|
function staleRegions(lock, path, current = []) {
|
|
174
334
|
return (lock.files[path]?.regions ?? []).filter((region) => !current.some((planned) => planned.begin === region.begin));
|
|
@@ -219,7 +379,9 @@ export function check(root, plan, toolfactoryVersion) {
|
|
|
219
379
|
if (planned.has(path) || entry?.state !== "generated" || !present(root, path))
|
|
220
380
|
continue;
|
|
221
381
|
// A merge file toolfactory stops writing loses its keys, not its existence.
|
|
222
|
-
if (entry.keys
|
|
382
|
+
if ((entry.keys || entry.keyedArrays) &&
|
|
383
|
+
!stranded(root, path, entry.keys ?? []) &&
|
|
384
|
+
!keyedEntriesRemain(root, path, entry.keyedArrays))
|
|
223
385
|
continue;
|
|
224
386
|
// A region file likewise loses its regions: once they are empty, nothing of ours is stranded.
|
|
225
387
|
if (entry.regions && !filled(root, path, entry.regions))
|
|
@@ -240,25 +402,36 @@ function render(root, file, previous) {
|
|
|
240
402
|
const stale = staleRegions(previous, file.path, file.regions);
|
|
241
403
|
const replaced = replaceRegions(emptyRegions(text, stale) ?? text, file);
|
|
242
404
|
if (replaced === undefined) {
|
|
405
|
+
const prior = previous.files[file.path];
|
|
406
|
+
// A generated full file can become a region file without forcing an author to recreate
|
|
407
|
+
// markers. Its lock hash proves every byte is still the old generator's, so replacing it
|
|
408
|
+
// cannot discard authored work. Manual or changed files still require `adopt`.
|
|
409
|
+
if (prior?.state === "generated" && sha256(text) === prior.sha256)
|
|
410
|
+
return replaceRegions(file.template, file) ?? file.template;
|
|
243
411
|
throw new Error(`${file.path} exists but is missing a toolfactory region marker; restore the markers or run \`toolfactory adopt ${file.path}\`.`);
|
|
244
412
|
}
|
|
245
413
|
return replaced;
|
|
246
414
|
}
|
|
247
415
|
return replaceRegions(file.template, file) ?? file.template;
|
|
248
416
|
}
|
|
249
|
-
if (!existsSync(path))
|
|
250
|
-
|
|
417
|
+
if (!existsSync(path)) {
|
|
418
|
+
const document = deepMerge({}, omitKeyedArrays(file.patch, file.keyedArrays ?? {}), file.owned);
|
|
419
|
+
mergeKeyedArrays(document, file, previous);
|
|
420
|
+
return serializeDocument(document, file.format);
|
|
421
|
+
}
|
|
251
422
|
const text = readFileSync(path, "utf8");
|
|
252
423
|
const document = parseDocument(text, file.format);
|
|
253
424
|
// The patch's inverse first: keys a previous patch wrote and this one dropped are uninstalled.
|
|
254
|
-
const uninstalled = removeKeys(document, staleKeys(previous, file.path, patchKeys(file.patch, file.owned)));
|
|
425
|
+
const uninstalled = removeKeys(document, staleKeys(previous, file.path, patchKeys(file.patch, file.owned, file.keyedArrays)));
|
|
426
|
+
const arraysChanged = mergeKeyedArrays(document, file, previous);
|
|
255
427
|
// A document that already carries the patch is left byte-for-byte alone, so a rebuild never
|
|
256
428
|
// reserializes the author's file (and, for TOML, never drops their comments).
|
|
257
429
|
if (!uninstalled &&
|
|
258
|
-
|
|
430
|
+
!arraysChanged &&
|
|
431
|
+
JSON.stringify(pickMerge(document, file)) === JSON.stringify(file.patch)) {
|
|
259
432
|
return text;
|
|
260
433
|
}
|
|
261
|
-
return serializeDocument(deepMerge(document, file.patch, file.owned), file.format);
|
|
434
|
+
return serializeDocument(deepMerge(document, omitKeyedArrays(file.patch, file.keyedArrays ?? {}), file.owned), file.format);
|
|
262
435
|
}
|
|
263
436
|
/** Write a plan to the tree and refresh the lock. */
|
|
264
437
|
export function apply(root, plan, toolfactoryVersion) {
|
|
@@ -279,7 +452,8 @@ export function apply(root, plan, toolfactoryVersion) {
|
|
|
279
452
|
result.manual.push(file.path);
|
|
280
453
|
continue;
|
|
281
454
|
}
|
|
282
|
-
const keys = file.kind === "merge" ? patchKeys(file.patch, file.owned) : undefined;
|
|
455
|
+
const keys = file.kind === "merge" ? patchKeys(file.patch, file.owned, file.keyedArrays) : undefined;
|
|
456
|
+
const keyedArrays = file.kind === "merge" ? keyedArrayState(file) : undefined;
|
|
283
457
|
const regions = file.kind === "region" ? file.regions.map(({ begin, end }) => ({ begin, end })) : undefined;
|
|
284
458
|
if (isLink(file)) {
|
|
285
459
|
if (currentManagedContent(root, file) === managedContent(file)) {
|
|
@@ -308,6 +482,7 @@ export function apply(root, plan, toolfactoryVersion) {
|
|
|
308
482
|
sha256: sha256(managedContent(file)),
|
|
309
483
|
state: "generated",
|
|
310
484
|
keys,
|
|
485
|
+
keyedArrays,
|
|
311
486
|
regions,
|
|
312
487
|
};
|
|
313
488
|
}
|
|
@@ -320,11 +495,13 @@ export function apply(root, plan, toolfactoryVersion) {
|
|
|
320
495
|
}
|
|
321
496
|
if (!present(root, path))
|
|
322
497
|
continue;
|
|
323
|
-
if (entry.keys) {
|
|
498
|
+
if (entry.keys || entry.keyedArrays) {
|
|
324
499
|
// The inverse of a merge file is its keys: the author keeps the file and everything else in it.
|
|
325
500
|
const format = documentFormat(path);
|
|
326
501
|
const document = parseDocument(readFileSync(join(root, path), "utf8"), format);
|
|
327
|
-
|
|
502
|
+
const removedKeys = removeKeys(document, entry.keys ?? []);
|
|
503
|
+
const removedEntries = removeKeyedArrayEntries(document, entry.keyedArrays);
|
|
504
|
+
if (removedKeys || removedEntries) {
|
|
328
505
|
writeFileSync(join(root, path), serializeDocument(document, format));
|
|
329
506
|
result.written.push(path);
|
|
330
507
|
}
|
|
@@ -226,8 +226,111 @@ describe("apply / check — merge files", () => {
|
|
|
226
226
|
expect(text).toContain('dependencies = [ "zod" ]');
|
|
227
227
|
expect(check(root, plan, "0.1.0")).toEqual([]);
|
|
228
228
|
});
|
|
229
|
+
it("merges YAML and keeps authored keyed-array entries while removing stale generated ones", () => {
|
|
230
|
+
const root = tmp();
|
|
231
|
+
const plan = (commands) => [
|
|
232
|
+
{
|
|
233
|
+
kind: "merge",
|
|
234
|
+
path: "extension.yaml",
|
|
235
|
+
format: "yaml",
|
|
236
|
+
patch: {
|
|
237
|
+
name: "hello",
|
|
238
|
+
commands: commands.map((command) => ({ command, title: command })),
|
|
239
|
+
},
|
|
240
|
+
keyedArrays: { commands: "command" },
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
apply(root, plan(["hello.run", "hello.old"]), "0.1.0");
|
|
244
|
+
writeFileSync(join(root, "extension.yaml"), `${readFileSync(join(root, "extension.yaml"), "utf8")}commands:\n - command: author.run\n title: Author\n - command: hello.run\n title: hello.run\n - command: hello.old\n title: hello.old\n`);
|
|
245
|
+
// Recreate the document with a custom entry and the generated entries, as a hand-built host
|
|
246
|
+
// manifest would; the generated ids remain Toolfactory-owned from the lock.
|
|
247
|
+
writeFileSync(join(root, "extension.yaml"), "name: hello\ncommands:\n - command: author.run\n title: Author\n - command: hello.run\n title: hello.run\n - command: hello.old\n title: hello.old\n");
|
|
248
|
+
apply(root, plan(["hello.run", "hello.new"]), "0.1.0");
|
|
249
|
+
const text = readFileSync(join(root, "extension.yaml"), "utf8");
|
|
250
|
+
expect(text).toContain("author.run");
|
|
251
|
+
expect(text).toContain("hello.new");
|
|
252
|
+
expect(text).not.toContain("hello.old");
|
|
253
|
+
expect(check(root, plan(["hello.run", "hello.new"]), "0.1.0")).toEqual([]);
|
|
254
|
+
expect(() => apply(root, plan(["author.run"]), "0.1.0")).toThrow(/already has author entry/);
|
|
255
|
+
});
|
|
256
|
+
it("preserves unknown keyed-entry fields while removing stale projected leaves", () => {
|
|
257
|
+
const root = tmp();
|
|
258
|
+
const plan = (title, category) => [
|
|
259
|
+
{
|
|
260
|
+
kind: "merge",
|
|
261
|
+
path: "package.json",
|
|
262
|
+
format: "json",
|
|
263
|
+
patch: {
|
|
264
|
+
contributes: {
|
|
265
|
+
commands: [{ command: "hello.run", title, ...(category ? { category } : {}) }],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
keyedArrays: { "contributes.commands": "command" },
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
apply(root, plan("Run", "Toolfactory"), "0.1.0");
|
|
272
|
+
const path = join(root, "package.json");
|
|
273
|
+
const document = JSON.parse(readFileSync(path, "utf8"));
|
|
274
|
+
document.contributes.commands[0].icon = "$(rocket)";
|
|
275
|
+
writeFileSync(path, JSON.stringify(document));
|
|
276
|
+
// Native command details remain authored: they are not drift merely because Toolfactory
|
|
277
|
+
// owns the command's id and projected leaves.
|
|
278
|
+
expect(check(root, plan("Run", "Toolfactory"), "0.1.0")).toEqual([]);
|
|
279
|
+
apply(root, plan("Run tool"), "0.1.0");
|
|
280
|
+
expect(JSON.parse(readFileSync(path, "utf8")).contributes.commands).toEqual([
|
|
281
|
+
{ command: "hello.run", title: "Run tool", icon: "$(rocket)" },
|
|
282
|
+
]);
|
|
283
|
+
expect(check(root, plan("Run tool"), "0.1.0")).toEqual([]);
|
|
284
|
+
});
|
|
285
|
+
it("upgrades an untouched generated full file to a region but protects changed files", () => {
|
|
286
|
+
const root = tmp();
|
|
287
|
+
const old = [{ kind: "file", path: "entry.ts", content: "old generated\n" }];
|
|
288
|
+
const region = {
|
|
289
|
+
kind: "region",
|
|
290
|
+
path: "entry.ts",
|
|
291
|
+
regions: [{ begin: "// tf:entry", end: "// /tf:entry", content: "new generated\n" }],
|
|
292
|
+
template: "// tf:entry\n// /tf:entry\n\n// author tail\n",
|
|
293
|
+
};
|
|
294
|
+
apply(root, old, "0.1.0");
|
|
295
|
+
apply(root, [region], "0.2.0");
|
|
296
|
+
expect(readFileSync(join(root, "entry.ts"), "utf8")).toContain("new generated");
|
|
297
|
+
const changed = tmp();
|
|
298
|
+
apply(changed, old, "0.1.0");
|
|
299
|
+
writeFileSync(join(changed, "entry.ts"), "author changed\n");
|
|
300
|
+
expect(() => apply(changed, [region], "0.2.0")).toThrow(/missing a toolfactory region marker/);
|
|
301
|
+
});
|
|
229
302
|
});
|
|
230
303
|
describe("apply / check — inverses and outputs", () => {
|
|
304
|
+
it("restores deleted keyed rows, leaves steady YAML bytes alone, and uninstalls keys and rows", () => {
|
|
305
|
+
const root = tmp();
|
|
306
|
+
const plan = [
|
|
307
|
+
{
|
|
308
|
+
kind: "merge",
|
|
309
|
+
path: "extension.yaml",
|
|
310
|
+
format: "yaml",
|
|
311
|
+
patch: {
|
|
312
|
+
generated: true,
|
|
313
|
+
commands: [{ command: "hello.run", title: "Run" }],
|
|
314
|
+
},
|
|
315
|
+
keyedArrays: { commands: "command" },
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
apply(root, plan, "0.1.0");
|
|
319
|
+
writeFileSync(join(root, "extension.yaml"), "# author comment\nauthor: true\ngenerated: true\ncommands:\n - command: author.run\n title: Author\n");
|
|
320
|
+
// The prior lock owns hello.run, so removing it by hand does not turn it into an omission.
|
|
321
|
+
apply(root, plan, "0.1.0");
|
|
322
|
+
expect(readFileSync(join(root, "extension.yaml"), "utf8")).toContain("hello.run");
|
|
323
|
+
// Once current, no rewrite means YAML comments and formatting survive a rebuild.
|
|
324
|
+
const path = join(root, "extension.yaml");
|
|
325
|
+
writeFileSync(path, `# preserved comment\n${readFileSync(path, "utf8")}`);
|
|
326
|
+
const before = readFileSync(path, "utf8");
|
|
327
|
+
expect(apply(root, plan, "0.1.0").unchanged).toContain("extension.yaml");
|
|
328
|
+
expect(readFileSync(path, "utf8")).toBe(before);
|
|
329
|
+
apply(root, [], "0.1.0");
|
|
330
|
+
expect(readFileSync(path, "utf8")).toContain("author.run");
|
|
331
|
+
expect(readFileSync(path, "utf8")).not.toContain("hello.run");
|
|
332
|
+
expect(readFileSync(path, "utf8")).not.toContain("generated:");
|
|
333
|
+
});
|
|
231
334
|
it("uninstalls the keys a patch stops writing, keeping every other key", () => {
|
|
232
335
|
const root = tmp();
|
|
233
336
|
mkdirSync(root, { recursive: true });
|
package/dist/project/gate.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate and the release assets, as data.
|
|
3
|
+
*
|
|
4
|
+
* The gate is a command; CI is its projection. One list of steps is rendered three ways — the
|
|
5
|
+
* `test` job of `ci.yml`, the `gate`/`package` jobs of `release.yml`, and a shell script a local
|
|
6
|
+
* `toolfactory gate` / `toolfactory package` spawns — so "what green means" has exactly one
|
|
7
|
+
* definition whether or not the project ever sees GitHub.
|
|
8
|
+
*
|
|
9
|
+
* A step here is a command and nothing else. GitHub Actions `uses:` steps are runner provisioning
|
|
10
|
+
* (checkout, toolchains, caches) and stay in `src/surfaces/workflows.ts`, which wraps this list.
|
|
11
|
+
*/
|
|
12
|
+
import { type NativePackageId } from "../distribution/native.js";
|
|
13
|
+
import { type SystemPackageId } from "../distribution/system.js";
|
|
1
14
|
import { type PackageManager, type Project, type SurfaceId } from "../model.js";
|
|
2
15
|
export interface GateStep {
|
|
3
16
|
/** Step label: the CI step name, and the banner the shell script echoes. */
|
|
@@ -53,7 +66,9 @@ export declare function tagVersionAssert(project: Project): GateStep | undefined
|
|
|
53
66
|
* the release workflow's `package` job, which uploads the directory as one artifact. Publishing
|
|
54
67
|
* to a registry stays a CI concern; producing the artifacts never is.
|
|
55
68
|
*/
|
|
56
|
-
export declare function packageSteps(project: Project
|
|
69
|
+
export declare function packageSteps(project: Project, options?: {
|
|
70
|
+
system?: boolean;
|
|
71
|
+
}): GateStep[];
|
|
57
72
|
/** npm's filename for the root package, including its flattened scope. */
|
|
58
73
|
export declare function npmTarball(project: Project): string;
|
|
59
74
|
/** The ClawHub tarball `npm pack ./hosts/openclaw` writes, by name, inside the release artifact. */
|
|
@@ -65,7 +80,7 @@ export declare function openclawTarball(project: Project): string;
|
|
|
65
80
|
*/
|
|
66
81
|
export interface Registry {
|
|
67
82
|
/** Stable id: the release job suffix, the `secrets status` row, the unpublish step name. */
|
|
68
|
-
id: "npm" | "pypi" | "mcp-registry" | "oci" | "clawhub-package" | "clawhub-skill" | "pages" | "chrome" | "firefox" | "edge" | "safari";
|
|
83
|
+
id: NativePackageId | SystemPackageId | "npm" | "pypi" | "mcp-registry" | "oci" | "clawhub-package" | "clawhub-skill" | "pages" | "chrome" | "firefox" | "edge" | "safari" | "vscode-marketplace" | "open-vsx";
|
|
69
84
|
/** The surfaces whose selection publishes here; all must be selected. */
|
|
70
85
|
surfaces: SurfaceId[];
|
|
71
86
|
/** Environment names the publish leg consumes (repository scope); [] for OIDC/GITHUB_TOKEN legs. */
|
package/dist/project/gate.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* A step here is a command and nothing else. GitHub Actions `uses:` steps are runner provisioning
|
|
10
10
|
* (checkout, toolchains, caches) and stay in `src/surfaces/workflows.ts`, which wraps this list.
|
|
11
11
|
*/
|
|
12
|
+
import { nativePackageSteps, nativeRegistryRows, nativeToolchainSteps, } from "../distribution/native.js";
|
|
13
|
+
import { systemPackageSteps, systemRegistryRows, } from "../distribution/system.js";
|
|
12
14
|
import { githubSlug } from "../hosts/github.js";
|
|
13
15
|
import { githubOwner, projectName } from "../identity/name.js";
|
|
14
16
|
import { isInstructionOnly } from "../model.js";
|
|
@@ -17,6 +19,7 @@ import { HOST_DIR as DSH_HOST_DIR } from "../surfaces/dsh.js";
|
|
|
17
19
|
import { MANIFEST_PATH as MCPB_MANIFEST_PATH, MCPB_PIN } from "../surfaces/mcpb.js";
|
|
18
20
|
import { HOST_DIR as OPENCLAW_HOST_DIR } from "../surfaces/openclaw-native.js";
|
|
19
21
|
import { configProperties, envName, has, isSensitive, npmName, pypiName, } from "../surfaces/shared.js";
|
|
22
|
+
import { extensionName, OVSX_VERSION, publisher, VSCE_VERSION, HOST_DIR as VSCODE_HOST_DIR, vsixName, } from "../surfaces/vscode-extension.js";
|
|
20
23
|
/** Where `toolfactory package` and the release `package` job leave every asset. */
|
|
21
24
|
export const RELEASE_DIR = "dist/release";
|
|
22
25
|
/** The web app's build, shared by the release tarball and the Pages job (which sets `PAGES_BASE`). */
|
|
@@ -107,7 +110,9 @@ export function gateSteps(project) {
|
|
|
107
110
|
when: "ci",
|
|
108
111
|
});
|
|
109
112
|
}
|
|
110
|
-
if (has(project, "web") ||
|
|
113
|
+
if (has(project, "web") ||
|
|
114
|
+
has(project, "browser-extension") ||
|
|
115
|
+
has(project, "vscode-extension")) {
|
|
111
116
|
// The web smoke and the browser extension's own Playwright smoke both drive Chromium;
|
|
112
117
|
// their own npm installs fetch the browser, the runner needs its system libraries.
|
|
113
118
|
steps.push({
|
|
@@ -116,6 +121,7 @@ export function gateSteps(project) {
|
|
|
116
121
|
when: "ci",
|
|
117
122
|
});
|
|
118
123
|
}
|
|
124
|
+
steps.push(...nativeToolchainSteps(project));
|
|
119
125
|
steps.push({
|
|
120
126
|
name: "toolfactory validate",
|
|
121
127
|
run: `${cli} validate`,
|
|
@@ -128,6 +134,8 @@ export function gateSteps(project) {
|
|
|
128
134
|
name: "author tests",
|
|
129
135
|
run: typescript ? pm.test : "uv run --with pytest pytest -q",
|
|
130
136
|
});
|
|
137
|
+
if (typescript)
|
|
138
|
+
steps.push({ name: "author integration tests", run: pm.run("test:integration") });
|
|
131
139
|
if (openclaw) {
|
|
132
140
|
// T3, credential-free: one real OpenClaw agent turn against a scripted model. The suite only
|
|
133
141
|
// exists when `tool.json` `tests.examples` names an operation the plugin carries, which is
|
|
@@ -190,13 +198,16 @@ const LEGAL_BUNDLE_PATHS = [
|
|
|
190
198
|
* the release workflow's `package` job, which uploads the directory as one artifact. Publishing
|
|
191
199
|
* to a registry stays a CI concern; producing the artifacts never is.
|
|
192
200
|
*/
|
|
193
|
-
export function packageSteps(project) {
|
|
201
|
+
export function packageSteps(project, options = {}) {
|
|
194
202
|
const cli = toolfactoryCli(project);
|
|
195
203
|
const name = project.identity.name;
|
|
196
204
|
const steps = [
|
|
197
205
|
{ name: "release directory", run: `rm -rf ${RELEASE_DIR} && mkdir -p ${RELEASE_DIR}` },
|
|
198
206
|
...bootstrapSteps(project),
|
|
199
207
|
outputsStep(project),
|
|
208
|
+
...nativeToolchainSteps(project),
|
|
209
|
+
...nativePackageSteps(project),
|
|
210
|
+
...systemPackageSteps(project, { include: options.system !== false }),
|
|
200
211
|
];
|
|
201
212
|
if (has(project, "web")) {
|
|
202
213
|
steps.push({
|
|
@@ -295,6 +306,12 @@ export function packageSteps(project) {
|
|
|
295
306
|
});
|
|
296
307
|
}
|
|
297
308
|
// COVERAGE.md is tracked; coverage.json is a build output that need not be, so recompute it.
|
|
309
|
+
if (has(project, "vscode-extension")) {
|
|
310
|
+
steps.push({
|
|
311
|
+
name: "VS Code extension VSIX",
|
|
312
|
+
run: `npm --prefix ${VSCODE_HOST_DIR} install && npm --prefix ${VSCODE_HOST_DIR} run vsix && cp ${VSCODE_HOST_DIR}/extension.vsix ${RELEASE_DIR}/${vsixName(project)}`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
298
315
|
steps.push({
|
|
299
316
|
name: "coverage report",
|
|
300
317
|
run: `cp COVERAGE.md ${RELEASE_DIR}/ && ${cli} coverage > ${RELEASE_DIR}/coverage.json`,
|
|
@@ -355,6 +372,22 @@ const ASC_SECRETS = ["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_PRIVATE_KEY"];
|
|
|
355
372
|
export const RELEASE_SECRET_NAMES = [
|
|
356
373
|
"NPM_TOKEN",
|
|
357
374
|
"CLAWHUB_TOKEN",
|
|
375
|
+
"VSCE_PAT",
|
|
376
|
+
"OVSX_PAT",
|
|
377
|
+
"CARGO_REGISTRY_TOKEN",
|
|
378
|
+
"NUGET_API_KEY",
|
|
379
|
+
"MAVEN_CENTRAL_USERNAME",
|
|
380
|
+
"MAVEN_CENTRAL_PASSWORD",
|
|
381
|
+
"MAVEN_GPG_PRIVATE_KEY",
|
|
382
|
+
"MAVEN_GPG_PASSPHRASE",
|
|
383
|
+
"RUBYGEMS_API_KEY",
|
|
384
|
+
"HOMEBREW_TAP_TOKEN",
|
|
385
|
+
"SCOOP_BUCKET_TOKEN",
|
|
386
|
+
"CHOCOLATEY_API_KEY",
|
|
387
|
+
"APT_GPG_PRIVATE_KEY",
|
|
388
|
+
"APT_GPG_KEY_ID",
|
|
389
|
+
"COPR_LOGIN",
|
|
390
|
+
"COPR_TOKEN",
|
|
358
391
|
...CHROME_SECRETS,
|
|
359
392
|
...FIREFOX_SECRETS,
|
|
360
393
|
...EDGE_SECRETS,
|
|
@@ -379,6 +412,29 @@ export function registries(project) {
|
|
|
379
412
|
const browserZip = (browser) => `${BROWSER_HOST_DIR}/.output/${zipName(project, browser)}`;
|
|
380
413
|
const wxtSubmit = `npm --prefix ${BROWSER_HOST_DIR} exec --no -- wxt submit --dry-run`;
|
|
381
414
|
const rows = [
|
|
415
|
+
...nativeRegistryRows(project),
|
|
416
|
+
...systemRegistryRows(project),
|
|
417
|
+
{
|
|
418
|
+
id: "vscode-marketplace",
|
|
419
|
+
surfaces: ["vscode-extension"],
|
|
420
|
+
secrets: ["VSCE_PAT"],
|
|
421
|
+
url: "https://marketplace.visualstudio.com/manage",
|
|
422
|
+
probe: `npx --yes @vscode/vsce@${VSCE_VERSION} verify-pat ${publisher(project)}`,
|
|
423
|
+
exists: `npx --yes @vscode/vsce@${VSCE_VERSION} show ${publisher(project)}.${extensionName(project)} --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.exit(JSON.parse(s).versions.some(v=>v.version==="${version}")?0:1))'`,
|
|
424
|
+
gate: allSet(["VSCE_PAT"]),
|
|
425
|
+
retractUrl: "https://marketplace.visualstudio.com/manage",
|
|
426
|
+
retractHard: `npx --yes @vscode/vsce@${VSCE_VERSION} unpublish ${publisher(project)}.${extensionName(project)} --force`,
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "open-vsx",
|
|
430
|
+
surfaces: ["vscode-extension"],
|
|
431
|
+
secrets: ["OVSX_PAT"],
|
|
432
|
+
url: "https://open-vsx.org/user-settings/tokens",
|
|
433
|
+
probe: `npx --yes ovsx@${OVSX_VERSION} verify-pat ${publisher(project)}`,
|
|
434
|
+
exists: `curl -fsS -o /dev/null https://open-vsx.org/api/${publisher(project)}/${extensionName(project)}/${version}`,
|
|
435
|
+
gate: allSet(["OVSX_PAT"]),
|
|
436
|
+
retractUrl: "https://open-vsx.org/user-settings/extensions",
|
|
437
|
+
},
|
|
382
438
|
{
|
|
383
439
|
id: "npm",
|
|
384
440
|
surfaces: ["npm"],
|
|
@@ -527,7 +583,9 @@ export function registries(project) {
|
|
|
527
583
|
},
|
|
528
584
|
];
|
|
529
585
|
const selected = rows.filter((row) => row.surfaces.every((surface) => has(project, surface)) &&
|
|
530
|
-
(row.id !== "safari" || project.tool.browserExtension?.safari === true)
|
|
586
|
+
(row.id !== "safari" || project.tool.browserExtension?.safari === true) &&
|
|
587
|
+
(row.id !== "vscode-marketplace" || project.tool.vscode?.marketplace !== false) &&
|
|
588
|
+
(row.id !== "open-vsx" || project.tool.vscode?.openvsx !== false));
|
|
531
589
|
const mcpRegistry = selected.find((row) => row.id === "mcp-registry");
|
|
532
590
|
if (mcpRegistry) {
|
|
533
591
|
const packages = selected.filter((row) => row.id === "npm" || row.id === "pypi" || row.id === "oci");
|
|
@@ -535,6 +593,11 @@ export function registries(project) {
|
|
|
535
593
|
? packages.map((row) => `[ "$${gateVariable(row)}" = true ]`).join(" && ")
|
|
536
594
|
: "true";
|
|
537
595
|
}
|
|
596
|
+
const runtime = selected.find((row) => row.id === (project.tool.binding === "python" ? "pypi" : "npm"));
|
|
597
|
+
for (const row of selected.filter((row) => row.id === "vscode-marketplace" || row.id === "open-vsx")) {
|
|
598
|
+
if (runtime)
|
|
599
|
+
row.gate = `(${row.gate}) && { ${runtime.exists} || { ${runtime.gate}; }; }`;
|
|
600
|
+
}
|
|
538
601
|
return selected;
|
|
539
602
|
}
|
|
540
603
|
/** The shell variable the release gate holds a row's presence in; the job output takes the same name. */
|
|
@@ -558,6 +621,8 @@ export function manualSteps(project) {
|
|
|
558
621
|
const has_ = (id) => rows.some((row) => row.id === id);
|
|
559
622
|
const slug = githubSlug(project.identity.repository);
|
|
560
623
|
const steps = [];
|
|
624
|
+
if (has_("vscode-marketplace") || has_("open-vsx"))
|
|
625
|
+
steps.push("VS Code: register the publisher in Visual Studio Marketplace; create the Open VSX namespace and accept its publisher agreement before setting VSCE_PAT / OVSX_PAT. tool.vscode.publisher selects that identity.");
|
|
561
626
|
if (has_("oci")) {
|
|
562
627
|
steps.push(`ghcr.io: the first push publishes a private image whatever the repository's visibility, and GitHub has no visibility API — flip it once at ${rows.find((row) => row.id === "oci")?.url}.`);
|
|
563
628
|
}
|
|
@@ -10,7 +10,7 @@ export declare function toolAtRef(root: string, ref: string): {
|
|
|
10
10
|
schemaVersion: 1;
|
|
11
11
|
identity: string;
|
|
12
12
|
binding: "typescript" | "python";
|
|
13
|
-
surfaces: ("skill" | "agent-plugins" | "claude" | "codex" | "cursor" | "mcp" | "mcp-registry" | "cli" | "npm" | "pypi" | "openclaw-native" | "hermes-native" | "clawhub" | "web" | "dsh" | "workflows" | "agents" | "readme" | "gemini" | "mcpb" | "browser-extension")[];
|
|
13
|
+
surfaces: ("homebrew" | "winget" | "scoop" | "chocolatey" | "apt" | "rpm" | "skill" | "agent-plugins" | "claude" | "codex" | "cursor" | "mcp" | "mcp-registry" | "cli" | "npm" | "pypi" | "openclaw-native" | "hermes-native" | "clawhub" | "web" | "dsh" | "workflows" | "agents" | "readme" | "gemini" | "mcpb" | "browser-extension" | "vscode-extension" | "cargo" | "nuget" | "maven-central" | "rubygems" | "packagist" | "go-module")[];
|
|
14
14
|
bundle: {
|
|
15
15
|
runtime: "package" | "bundled";
|
|
16
16
|
};
|
|
@@ -46,6 +46,34 @@ export declare function toolAtRef(root: string, ref: string): {
|
|
|
46
46
|
hermes?: {
|
|
47
47
|
toolset?: string | undefined;
|
|
48
48
|
} | undefined;
|
|
49
|
+
vscode?: {
|
|
50
|
+
marketplace: boolean;
|
|
51
|
+
openvsx: boolean;
|
|
52
|
+
publisher?: string | undefined;
|
|
53
|
+
} | undefined;
|
|
54
|
+
nativePackages?: {
|
|
55
|
+
id: "cargo" | "nuget" | "maven-central" | "rubygems" | "packagist" | "go-module";
|
|
56
|
+
path: string;
|
|
57
|
+
identity: string;
|
|
58
|
+
name: string;
|
|
59
|
+
versionCommand?: string | undefined;
|
|
60
|
+
}[] | undefined;
|
|
61
|
+
systemPackages?: {
|
|
62
|
+
id: "homebrew" | "winget" | "scoop" | "chocolatey" | "apt" | "rpm";
|
|
63
|
+
path: string;
|
|
64
|
+
identity: string;
|
|
65
|
+
name: string;
|
|
66
|
+
versionCommand: string;
|
|
67
|
+
buildDependencies: string[];
|
|
68
|
+
buildCommand?: string | undefined;
|
|
69
|
+
asset?: string | undefined;
|
|
70
|
+
tap?: string | undefined;
|
|
71
|
+
bucket?: string | undefined;
|
|
72
|
+
bucketName?: string | undefined;
|
|
73
|
+
catalogPath?: string | undefined;
|
|
74
|
+
ppa?: string | undefined;
|
|
75
|
+
coprProject?: string | undefined;
|
|
76
|
+
}[] | undefined;
|
|
49
77
|
openclaw?: {
|
|
50
78
|
registers?: {
|
|
51
79
|
api: string;
|