@genspark/cli 1.0.24 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/skills-pull.d.ts +91 -0
- package/dist/commands/skills-pull.d.ts.map +1 -0
- package/dist/commands/skills-pull.js +275 -0
- package/dist/commands/skills-pull.js.map +1 -0
- package/dist/commands/skills-save.d.ts +39 -0
- package/dist/commands/skills-save.d.ts.map +1 -0
- package/dist/commands/skills-save.js +93 -0
- package/dist/commands/skills-save.js.map +1 -0
- package/dist/commands/skills-sync.d.ts +92 -0
- package/dist/commands/skills-sync.d.ts.map +1 -0
- package/dist/commands/skills-sync.js +329 -0
- package/dist/commands/skills-sync.js.map +1 -0
- package/dist/index.js +189 -4
- package/dist/index.js.map +1 -1
- package/docs/skills.md +208 -1
- package/package.json +2 -1
- package/skills/gsk-aidrive/SKILL.md +8 -5
- package/skills/gsk-crawler/SKILL.md +3 -1
- package/skills/gsk-github/SKILL.md +17 -10
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gsk skills sync` — client-side materializer for the backend `skills_sync`
|
|
3
|
+
* action.
|
|
4
|
+
*
|
|
5
|
+
* Why client-side: `skills_sync` returns a digest-diffed bundle (base64 file
|
|
6
|
+
* bytes for changed skills, a removal list, per-slug errors); this module
|
|
7
|
+
* applies one such response to disk. Deliberately NOT parity with
|
|
8
|
+
* `setup.sh` (which unconditionally `rm -rf`s then `ln -sfn`s over the mount
|
|
9
|
+
* path — the synced skill always wins): here a pre-existing REAL directory
|
|
10
|
+
* under `mountDir` (an inline-tar builtin, or user content) is never
|
|
11
|
+
* clobbered by a same-named synced skill; the real dir wins and the slug is
|
|
12
|
+
* reported failed instead. Chosen because this materializer runs on a
|
|
13
|
+
* user's laptop, not a disposable sandbox — never `rm -rf`ing directories we
|
|
14
|
+
* didn't create is worth a failed sync entry. Index.ts (Task 6) drives the
|
|
15
|
+
* poll-until-`complete` loop and owns `--check`; this module only ever
|
|
16
|
+
* applies (never diffs) a response.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { error as logError } from '../logger.js';
|
|
22
|
+
import { safeJoin, SLUG_RE } from './skills-pull.js';
|
|
23
|
+
/**
|
|
24
|
+
* Folds the per-batch results of the multi-batch sync loop into single deduped
|
|
25
|
+
* totals. A slug can recur across batches — a server `error` row carries no
|
|
26
|
+
* recordable sha, so the server re-emits it in EVERY batch — and concatenating
|
|
27
|
+
* per-batch arrays would multi-count it, making the summary counts and the
|
|
28
|
+
* exit-4 gate wrong. Each category is a Set (dedup by slug); a slug's outcome
|
|
29
|
+
* in a later batch supersedes an earlier one. It also aggregates the echoed
|
|
30
|
+
* skill rows so `-o json` reflects every batch, not just the final one.
|
|
31
|
+
*
|
|
32
|
+
* Generic over the echoed-row type so the CLI can pass its own row shape
|
|
33
|
+
* without a cast (only `slug` is required here).
|
|
34
|
+
*/
|
|
35
|
+
export class SyncAggregator {
|
|
36
|
+
_applied = new Set();
|
|
37
|
+
_removed = new Set();
|
|
38
|
+
_failed = new Set();
|
|
39
|
+
_rows = new Map();
|
|
40
|
+
/** Fold one batch's apply result and its echoed skill rows into the totals. */
|
|
41
|
+
add(result, rows) {
|
|
42
|
+
for (const slug of result.applied)
|
|
43
|
+
this.categorize(slug, this._applied);
|
|
44
|
+
for (const slug of result.removed)
|
|
45
|
+
this.categorize(slug, this._removed);
|
|
46
|
+
for (const slug of result.failed)
|
|
47
|
+
this.categorize(slug, this._failed);
|
|
48
|
+
for (const row of rows)
|
|
49
|
+
this._rows.set(row.slug, row);
|
|
50
|
+
}
|
|
51
|
+
categorize(slug, into) {
|
|
52
|
+
this._applied.delete(slug);
|
|
53
|
+
this._removed.delete(slug);
|
|
54
|
+
this._failed.delete(slug);
|
|
55
|
+
into.add(slug);
|
|
56
|
+
}
|
|
57
|
+
get appliedCount() {
|
|
58
|
+
return this._applied.size;
|
|
59
|
+
}
|
|
60
|
+
get removedCount() {
|
|
61
|
+
return this._removed.size;
|
|
62
|
+
}
|
|
63
|
+
get failedCount() {
|
|
64
|
+
return this._failed.size;
|
|
65
|
+
}
|
|
66
|
+
/** Aggregated echoed rows (deduped by slug, last-wins). */
|
|
67
|
+
get skills() {
|
|
68
|
+
return Array.from(this._rows.values());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export const STATE_FILENAME = 'skills-sync-state.json';
|
|
72
|
+
function expandHome(p) {
|
|
73
|
+
return p.replace(/^~(?=\/|$)/, os.homedir());
|
|
74
|
+
}
|
|
75
|
+
export function defaultStoreDir() {
|
|
76
|
+
return expandHome('~/.gsk/skills-store');
|
|
77
|
+
}
|
|
78
|
+
export function defaultMountDir() {
|
|
79
|
+
return expandHome('~/.opencode/skills');
|
|
80
|
+
}
|
|
81
|
+
/** Load the persisted `{slug: sha}` digest map. Both unparseable JSON AND a
|
|
82
|
+
* valid-JSON-but-wrong-shape file self-heal to `{}` — a dirty state file
|
|
83
|
+
* forces a full re-diff on the next sync rather than risk skipping a change
|
|
84
|
+
* against a state we can't trust. The shape check matters because the server
|
|
85
|
+
* verb rejects the WHOLE sync (`bad state digest map`) on a single bad key,
|
|
86
|
+
* so without validating here a corrupt map would wedge every sync forever;
|
|
87
|
+
* resetting to `{}` lets it recover on the next run. */
|
|
88
|
+
export function loadState(storeDir) {
|
|
89
|
+
try {
|
|
90
|
+
const raw = fs.readFileSync(path.join(storeDir, STATE_FILENAME), 'utf8');
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
93
|
+
const obj = parsed;
|
|
94
|
+
const wellShaped = Object.entries(obj).every(([k, v]) => SLUG_RE.test(k) && typeof v === 'string');
|
|
95
|
+
if (wellShaped)
|
|
96
|
+
return obj;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// absent or unparseable → treated as empty state
|
|
101
|
+
}
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
/** True when `<slug>`'s files are downloaded on disk. The STORE dir is the
|
|
105
|
+
* single source of truth for "materialized" — NOT the mount symlink. The mount
|
|
106
|
+
* is a best-effort, separately-reconciled concern (see `reconcileMount`): on a
|
|
107
|
+
* filesystem that can't hold a symlink (Windows without Developer Mode,
|
|
108
|
+
* exFAT/FAT/SMB — `symlinkSync` throws EPERM/ENOSYS) the mount is never created,
|
|
109
|
+
* yet the store IS. Keying materialization on the mount would then read the
|
|
110
|
+
* slug as "not materialized" on every `unchanged` re-sync, drop its sha, and
|
|
111
|
+
* re-download the whole store forever. `writeSlugAtomic` renames the tree into
|
|
112
|
+
* place in one step, so a present store dir is a complete one. A hand-deleted
|
|
113
|
+
* store dir (store missing) still reads false → the caller drops the sha and
|
|
114
|
+
* the next sync re-fetches (the F3 self-heal). */
|
|
115
|
+
function isSlugMaterialized(storeDir, slug) {
|
|
116
|
+
return fs.existsSync(path.join(storeDir, slug));
|
|
117
|
+
}
|
|
118
|
+
/** Best-effort reconcile of the mount symlink for an already-stored slug, run
|
|
119
|
+
* every sync (never touches the store, so it can't trigger a re-download):
|
|
120
|
+
* - `'mounted'` — the symlink now points at (or already pointed at) the
|
|
121
|
+
* live store dir.
|
|
122
|
+
* - `'blocked'` — a REAL dir/file occupies the mount path; the builtin /
|
|
123
|
+
* user content wins and we never clobber it (the module's core deviation
|
|
124
|
+
* from setup.sh). A benign, terminal state — not retried into a symlink.
|
|
125
|
+
* - `'unsupported'` — `symlinkSync` threw (filesystem can't hold a symlink),
|
|
126
|
+
* or the mount path can't be stat'd (a non-ENOENT fault). The store is
|
|
127
|
+
* intact; the caller keeps the sha but reports the slug not-mounted.
|
|
128
|
+
* The whole body is guarded: the initial `lstatSync` can itself throw a
|
|
129
|
+
* non-ENOENT error (e.g. ENOTDIR when `mountDir` is a file), which must be
|
|
130
|
+
* caught here rather than escape the batch before `saveState`. */
|
|
131
|
+
function reconcileMount(storeDir, mountDir, slug) {
|
|
132
|
+
const linkPath = path.join(mountDir, slug);
|
|
133
|
+
const targetPath = path.join(storeDir, slug);
|
|
134
|
+
try {
|
|
135
|
+
const stat = fs.lstatSync(linkPath, { throwIfNoEntry: false });
|
|
136
|
+
if (stat && !stat.isSymbolicLink())
|
|
137
|
+
return 'blocked';
|
|
138
|
+
if (stat &&
|
|
139
|
+
path.resolve(mountDir, fs.readlinkSync(linkPath)) === targetPath) {
|
|
140
|
+
return 'mounted'; // already our symlink → no churn
|
|
141
|
+
}
|
|
142
|
+
fs.mkdirSync(mountDir, { recursive: true });
|
|
143
|
+
if (stat)
|
|
144
|
+
fs.unlinkSync(linkPath); // stale / foreign symlink → repoint
|
|
145
|
+
fs.symlinkSync(targetPath, linkPath, 'dir');
|
|
146
|
+
return 'mounted';
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return 'unsupported';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Human-facing reason a stored slug isn't mounted, per `reconcileMount`. */
|
|
153
|
+
function mountFailureMessage(slug, result) {
|
|
154
|
+
const why = result === 'blocked'
|
|
155
|
+
? 'a real directory occupies the mount path (builtin/user content wins)'
|
|
156
|
+
: 'the filesystem does not support symlinks (Windows without Developer Mode, exFAT/FAT/SMB)';
|
|
157
|
+
return `skills sync: ${slug} stored but not mounted — ${why}`;
|
|
158
|
+
}
|
|
159
|
+
/** Sweep orphan `.tmp-<slug>-<pid>` staging dirs left by a killed run. Each
|
|
160
|
+
* `writeSlugAtomic` only clears its OWN pid's tmp dir, so a process killed
|
|
161
|
+
* mid-write leaks one; this best-effort sweep runs once per apply. Guarded to
|
|
162
|
+
* the exact tmp-dir shape so it can never touch a real skill dir. */
|
|
163
|
+
function sweepStaleTmpDirs(storeDir) {
|
|
164
|
+
let entries;
|
|
165
|
+
try {
|
|
166
|
+
entries = fs.readdirSync(storeDir);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const tmpRe = /^\.tmp-[A-Za-z0-9][A-Za-z0-9_-]*-\d+$/;
|
|
172
|
+
for (const name of entries) {
|
|
173
|
+
if (!tmpRe.test(name))
|
|
174
|
+
continue;
|
|
175
|
+
try {
|
|
176
|
+
fs.rmSync(path.join(storeDir, name), { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// best-effort — a concurrent writer or locked dir just stays
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function saveState(storeDir, state) {
|
|
184
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
185
|
+
fs.writeFileSync(path.join(storeDir, STATE_FILENAME), JSON.stringify(state, null, 2) + '\n');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Write one `update` entry's files into `<storeDir>/<slug>` atomically: build
|
|
189
|
+
* the tree in a sibling `.tmp-` dir first, then remove any existing target
|
|
190
|
+
* and `renameSync` the tmp dir over it. A crash or crafted traversal path
|
|
191
|
+
* partway through the write leaves the tmp dir orphaned but the previous
|
|
192
|
+
* (or absent) target untouched — never a half-written skill on disk.
|
|
193
|
+
*/
|
|
194
|
+
function writeSlugAtomic(storeDir, slug, files) {
|
|
195
|
+
const tmpDir = path.join(storeDir, `.tmp-${slug}-${process.pid}`);
|
|
196
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
197
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
198
|
+
try {
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
const target = safeJoin(tmpDir, file.path);
|
|
201
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
202
|
+
fs.writeFileSync(target, Buffer.from(file.content_base64, 'base64'));
|
|
203
|
+
}
|
|
204
|
+
const finalDir = path.join(storeDir, slug);
|
|
205
|
+
fs.rmSync(finalDir, { recursive: true, force: true });
|
|
206
|
+
fs.renameSync(tmpDir, finalDir);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Remove a slug's store dir and mount symlink. Only unlinks the symlink
|
|
214
|
+
* when it actually points into `storeDir` — a repointed or foreign symlink
|
|
215
|
+
* (or a real directory) is left alone rather than risk deleting user data. */
|
|
216
|
+
function removeSlug(storeDir, mountDir, slug) {
|
|
217
|
+
const linkPath = path.join(mountDir, slug);
|
|
218
|
+
const stat = fs.lstatSync(linkPath, { throwIfNoEntry: false });
|
|
219
|
+
if (stat?.isSymbolicLink()) {
|
|
220
|
+
const resolved = path.resolve(mountDir, fs.readlinkSync(linkPath));
|
|
221
|
+
const storeSlugDir = path.join(storeDir, slug);
|
|
222
|
+
if (resolved === storeSlugDir) {
|
|
223
|
+
fs.unlinkSync(linkPath);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
fs.rmSync(path.join(storeDir, slug), { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Apply one `skills_sync` response to disk. Never called in `--check` mode
|
|
230
|
+
* (Task 6's caller diffs against `loadState` output itself for that path).
|
|
231
|
+
*/
|
|
232
|
+
export function applySyncResponse(data, opts = {}) {
|
|
233
|
+
// Absolutize (mirrors resolvePullDest in skills-pull.ts): a relative
|
|
234
|
+
// storeDir/mountDir would otherwise reach safeJoin's containment check as
|
|
235
|
+
// a relative `dest` while its computed `target` is always absolute
|
|
236
|
+
// (path.resolve), so `target.startsWith(dest + sep)` could never match
|
|
237
|
+
// and every `update` entry would be rejected as an unsafe path.
|
|
238
|
+
const storeDir = path.resolve(expandHome(opts.storeDir ?? defaultStoreDir()));
|
|
239
|
+
const mountDir = path.resolve(expandHome(opts.mountDir ?? defaultMountDir()));
|
|
240
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
241
|
+
sweepStaleTmpDirs(storeDir);
|
|
242
|
+
const state = loadState(storeDir);
|
|
243
|
+
const applied = [];
|
|
244
|
+
const removed = [];
|
|
245
|
+
const failed = [];
|
|
246
|
+
for (const entry of data.skills) {
|
|
247
|
+
if (!SLUG_RE.test(entry.slug)) {
|
|
248
|
+
failed.push(entry.slug);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (entry.action === 'update') {
|
|
252
|
+
try {
|
|
253
|
+
writeSlugAtomic(storeDir, entry.slug, entry.files ?? []);
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
logError(`skills sync: ${entry.slug} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
257
|
+
failed.push(entry.slug);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// Record the synced sha the moment the STORE write succeeds — the content
|
|
261
|
+
// is materialized and sha-addressed regardless of whether the mount step
|
|
262
|
+
// then succeeds. The mount is reconciled separately (best-effort); its
|
|
263
|
+
// failure must never drop the sha, or the server would re-send this slug
|
|
264
|
+
// every sync (on a symlink-unsupported filesystem that re-downloads
|
|
265
|
+
// everything each run, and a large re-sent skill sorting early could
|
|
266
|
+
// starve later slugs out of the byte budget until the batch guard bailed).
|
|
267
|
+
if (entry.sha)
|
|
268
|
+
state[entry.slug] = entry.sha;
|
|
269
|
+
const mount = reconcileMount(storeDir, mountDir, entry.slug);
|
|
270
|
+
if (mount === 'mounted') {
|
|
271
|
+
applied.push(entry.slug);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Store is materialized and the sha recorded (no re-download); only the
|
|
275
|
+
// mount could not be made. Surface it once for this content change.
|
|
276
|
+
logError(mountFailureMessage(entry.slug, mount));
|
|
277
|
+
failed.push(entry.slug);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else if (entry.action === 'unchanged') {
|
|
281
|
+
// The server said "unchanged" only because our digest matched — but the
|
|
282
|
+
// store may be gone from disk (dir hand-deleted, or a state file restored
|
|
283
|
+
// from backup without its store). Trust the STORE, not the state file:
|
|
284
|
+
// carry the sha forward only when the store dir is present; otherwise DROP
|
|
285
|
+
// it so the next sync sees a digest miss and re-fetches (self-heal). The
|
|
286
|
+
// mount symlink is NOT part of this decision — see isSlugMaterialized.
|
|
287
|
+
if (entry.sha && isSlugMaterialized(storeDir, entry.slug)) {
|
|
288
|
+
state[entry.slug] = entry.sha;
|
|
289
|
+
// Retry the mount every sync (a transient/absent mount can recover),
|
|
290
|
+
// but never let it drop the sha or re-download. A real dir winning the
|
|
291
|
+
// mount ('blocked') is benign and self-heals to exit 0; a
|
|
292
|
+
// symlink-unsupported filesystem ('unsupported') keeps the slug failed
|
|
293
|
+
// (persistent exit 4 — the store is downloaded but can't be mounted).
|
|
294
|
+
if (reconcileMount(storeDir, mountDir, entry.slug) === 'unsupported') {
|
|
295
|
+
logError(mountFailureMessage(entry.slug, 'unsupported'));
|
|
296
|
+
failed.push(entry.slug);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
delete state[entry.slug];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (entry.action === 'removed') {
|
|
304
|
+
try {
|
|
305
|
+
removeSlug(storeDir, mountDir, entry.slug);
|
|
306
|
+
delete state[entry.slug];
|
|
307
|
+
removed.push(entry.slug);
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
// Keep the state entry so the next sync retries this removal —
|
|
311
|
+
// a failed delete here (e.g. a locked store dir) must not be
|
|
312
|
+
// reported as done, and the batch's already-applied updates
|
|
313
|
+
// still need saveState() below to land.
|
|
314
|
+
logError(`skills sync: ${entry.slug} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
315
|
+
failed.push(entry.slug);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (entry.action === 'error') {
|
|
319
|
+
// Surface the server's per-slug code (NOT_FOUND / TOO_LARGE) like the
|
|
320
|
+
// update/removed branches log their failures — otherwise a server-side
|
|
321
|
+
// sync error was silently swallowed into the failed count with no reason.
|
|
322
|
+
logError(`skills sync: ${entry.slug} failed${entry.code ? ` (${entry.code})` : ''}`);
|
|
323
|
+
failed.push(entry.slug);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
saveState(storeDir, state);
|
|
327
|
+
return { state, applied, removed, failed };
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=skills-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-sync.js","sourceRoot":"","sources":["../../src/commands/skills-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAE5B,OAAO,EAAE,KAAK,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAqCpD;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,cAAc;IACR,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAA;IAC5B,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAA;IAC5B,OAAO,GAAG,IAAI,GAAG,EAAU,CAAA;IAC3B,KAAK,GAAG,IAAI,GAAG,EAAe,CAAA;IAE/C,+EAA+E;IAC/E,GAAG,CAAC,MAAuB,EAAE,IAAoB;QAC/C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QACvE,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;QACvE,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;QACrE,KAAK,MAAM,GAAG,IAAI,IAAI;YAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IACvD,CAAC;IAEO,UAAU,CAAC,IAAY,EAAE,IAAiB;QAChD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAC1B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACzB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAA;IAC3B,CAAC;IACD,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAA;IAC3B,CAAC;IACD,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;IAED,2DAA2D;IAC3D,IAAI,MAAM;QACR,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;IACxC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,wBAAwB,CAAA;AAEtD,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;AAC9C,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,CAAC,qBAAqB,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,CAAC,oBAAoB,CAAC,CAAA;AACzC,CAAC;AAED;;;;;;wDAMwD;AACxD,MAAM,UAAU,SAAS,CAAC,QAAgB;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAA;QACxE,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACvC,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnE,MAAM,GAAG,GAAG,MAAiC,CAAA;YAC7C,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAC1C,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CACrD,CAAA;YACD,IAAI,UAAU;gBAAE,OAAO,GAA6B,CAAA;QACtD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,iDAAiD;IACnD,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED;;;;;;;;;;kDAUkD;AAClD,SAAS,kBAAkB,CAAC,QAAgB,EAAE,IAAY;IACxD,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAA;AACjD,CAAC;AAKD;;;;;;;;;;;;kEAYkE;AAClE,SAAS,cAAc,CACrB,QAAgB,EAChB,QAAgB,EAChB,IAAY;IAEZ,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC5C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAA;QAC9D,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE;YAAE,OAAO,SAAS,CAAA;QACpD,IACE,IAAI;YACJ,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,KAAK,UAAU,EAChE,CAAC;YACD,OAAO,SAAS,CAAA,CAAC,iCAAiC;QACpD,CAAC;QACD,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3C,IAAI,IAAI;YAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA,CAAC,oCAAoC;QACtE,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;QAC3C,OAAO,SAAS,CAAA;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,aAAa,CAAA;IACtB,CAAC;AACH,CAAC;AAED,6EAA6E;AAC7E,SAAS,mBAAmB,CAC1B,IAAY,EACZ,MAAuC;IAEvC,MAAM,GAAG,GACP,MAAM,KAAK,SAAS;QAClB,CAAC,CAAC,sEAAsE;QACxE,CAAC,CAAC,0FAA0F,CAAA;IAChG,OAAO,gBAAgB,IAAI,6BAA6B,GAAG,EAAE,CAAA;AAC/D,CAAC;AAED;;;qEAGqE;AACrE,SAAS,iBAAiB,CAAC,QAAgB;IACzC,IAAI,OAAiB,CAAA;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAA;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAM;IACR,CAAC;IACD,MAAM,KAAK,GAAG,uCAAuC,CAAA;IACrD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAQ;QAC/B,IAAI,CAAC;YACH,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,6DAA6D;QAC/D,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB,EAAE,KAA6B;IAChE,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3C,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,EACnC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CACtC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,eAAe,CACtB,QAAgB,EAChB,IAAY,EACZ,KAAsB;IAEtB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,IAAI,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;IACjE,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACnD,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACzC,IAAI,CAAC;QACH,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;YAC1C,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YACvD,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC,CAAA;QACtE,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;QAC1C,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACrD,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACjC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACnD,MAAM,CAAC,CAAA;IACT,CAAC;AACH,CAAC;AAED;;8EAE8E;AAC9E,SAAS,UAAU,CAAC,QAAgB,EAAE,QAAgB,EAAE,IAAY;IAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAA;IAC9D,IAAI,IAAI,EAAE,cAAc,EAAE,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;QAClE,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;QAC9C,IAAI,QAAQ,KAAK,YAAY,EAAE,CAAC;YAC9B,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;IACD,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACxE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAsB,EACtB,OAAoB,EAAE;IAEtB,qEAAqE;IACrE,0EAA0E;IAC1E,mEAAmE;IACnE,uEAAuE;IACvE,gEAAgE;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,IAAI,eAAe,EAAE,CAAC,CAAC,CAAA;IAC7E,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,IAAI,eAAe,EAAE,CAAC,CAAC,CAAA;IAC7E,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3C,iBAAiB,CAAC,QAAQ,CAAC,CAAA;IAE3B,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;IACjC,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAa,EAAE,CAAA;IAE3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACvB,SAAQ;QACV,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC9B,IAAI,CAAC;gBACH,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;YAC1D,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,QAAQ,CACN,gBAAgB,KAAK,CAAC,IAAI,YAAY,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CACnF,CAAA;gBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBACvB,SAAQ;YACV,CAAC;YACD,0EAA0E;YAC1E,yEAAyE;YACzE,uEAAuE;YACvE,yEAAyE;YACzE,oEAAoE;YACpE,qEAAqE;YACrE,2EAA2E;YAC3E,IAAI,KAAK,CAAC,GAAG;gBAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAA;YAC5C,MAAM,KAAK,GAAG,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;YAC5D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YAC1B,CAAC;iBAAM,CAAC;gBACN,wEAAwE;gBACxE,oEAAoE;gBACpE,QAAQ,CAAC,mBAAmB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAA;gBAChD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YACxC,wEAAwE;YACxE,0EAA0E;YAC1E,uEAAuE;YACvE,2EAA2E;YAC3E,yEAAyE;YACzE,uEAAuE;YACvE,IAAI,KAAK,CAAC,GAAG,IAAI,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1D,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAA;gBAC7B,qEAAqE;gBACrE,uEAAuE;gBACvE,0DAA0D;gBAC1D,uEAAuE;gBACvE,sEAAsE;gBACtE,IAAI,cAAc,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,aAAa,EAAE,CAAC;oBACrE,QAAQ,CAAC,mBAAmB,CAAC,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAA;oBACxD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBACzB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YAC1B,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAA;gBAC1C,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBACxB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YAC1B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,+DAA+D;gBAC/D,6DAA6D;gBAC7D,4DAA4D;gBAC5D,wCAAwC;gBACxC,QAAQ,CACN,gBAAgB,KAAK,CAAC,IAAI,YAAY,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CACnF,CAAA;gBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;QACH,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;YACpC,sEAAsE;YACtE,uEAAuE;YACvE,0EAA0E;YAC1E,QAAQ,CACN,gBAAgB,KAAK,CAAC,IAAI,UAAU,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAC3E,CAAA;YACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;IAED,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC1B,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC5C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { Command } from 'commander';
|
|
|
22
22
|
import * as fs from 'fs';
|
|
23
23
|
import * as http from 'http';
|
|
24
24
|
import * as https from 'https';
|
|
25
|
+
import * as os from 'os';
|
|
25
26
|
import * as pathModule from 'path';
|
|
26
27
|
import { createRequire } from 'module';
|
|
27
28
|
import { spawn } from 'child_process';
|
|
@@ -30,7 +31,7 @@ import { setDebugEnabled, setOutputFormat, debug, info, error as logError, outpu
|
|
|
30
31
|
import { loadConfigFile, saveConfigFile, getConfigPath, loadToolsCache, saveToolsCache, setConfigPathOverride, } from './config.js';
|
|
31
32
|
import { checkForUpdates } from './updater.js';
|
|
32
33
|
import { registerDesignCommand } from './commands/design.js';
|
|
33
|
-
import { registerMeshCommand, getMeshInvocation, runMesh } from './commands/mesh.js';
|
|
34
|
+
import { registerMeshCommand, getMeshInvocation, runMesh, } from './commands/mesh.js';
|
|
34
35
|
import { executeCloneUrl, normalizeCloneUrlForGit, } from './commands/sb-git-clone.js';
|
|
35
36
|
const require = createRequire(import.meta.url);
|
|
36
37
|
const { version: VERSION } = require('../package.json');
|
|
@@ -74,6 +75,12 @@ const program = new Command();
|
|
|
74
75
|
program
|
|
75
76
|
.name('gsk')
|
|
76
77
|
.description('Genspark Tool CLI - Search, crawl, analyze, and generate media')
|
|
78
|
+
// Agents drive this CLI blind (no schema in context): on a bad invocation,
|
|
79
|
+
// print the full usage + a did-you-mean hint so the caller can self-correct
|
|
80
|
+
// on the next attempt instead of burning a round on `--help`. Subcommands
|
|
81
|
+
// inherit both settings via Commander's copyInheritedSettings.
|
|
82
|
+
.showHelpAfterError()
|
|
83
|
+
.showSuggestionAfterError()
|
|
77
84
|
.version(VERSION, '-v, --version')
|
|
78
85
|
.option('--api-key <key>', 'API key (or set GSK_API_KEY env var)')
|
|
79
86
|
.option('--base-url <url>', `Base URL for API (default: ${DEFAULT_BASE_URL})`, DEFAULT_BASE_URL)
|
|
@@ -451,6 +458,16 @@ function extractFileUrl(data, keys) {
|
|
|
451
458
|
function resolveVdProjectId() {
|
|
452
459
|
return process.env.GSK_VD_PROJECT_ID || fileConfig.vd_project_id;
|
|
453
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Expand a leading `~` to the user's home directory. `skills-sync.ts`
|
|
463
|
+
* applies this same expansion internally on the paths it defaults to, but
|
|
464
|
+
* a user-supplied `--store-dir`/`--mount-dir` needs it applied here too —
|
|
465
|
+
* `loadState()` is called from this file before the sync response comes
|
|
466
|
+
* back, and it does no expansion of its own.
|
|
467
|
+
*/
|
|
468
|
+
function expandHome(p) {
|
|
469
|
+
return p.replace(/^~(?=\/|$)/, os.homedir());
|
|
470
|
+
}
|
|
454
471
|
/**
|
|
455
472
|
* Register a dynamic tool command from a server-provided schema.
|
|
456
473
|
*
|
|
@@ -506,8 +523,15 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
506
523
|
if (name === tool.cli.primary_arg)
|
|
507
524
|
continue;
|
|
508
525
|
// For create_task, make params optional so --acp mode works without them.
|
|
509
|
-
// Validation of required params happens in the action handler.
|
|
510
|
-
|
|
526
|
+
// Validation of required params happens in the action handler. Likewise
|
|
527
|
+
// skills_save's `files` is server-required but CLI-populated (from the
|
|
528
|
+
// [path] directory collector, never typed by hand) — a Commander
|
|
529
|
+
// requiredOption would block every invocation before the action handler
|
|
530
|
+
// (and its collector) ever runs.
|
|
531
|
+
const isRequired = tool.name === 'create_task' ||
|
|
532
|
+
(tool.name === 'skills_save' && name === 'files')
|
|
533
|
+
? false
|
|
534
|
+
: required.includes(name);
|
|
511
535
|
const paramType = Array.isArray(param.type) ? param.type[0] : param.type;
|
|
512
536
|
// Single-char aliases are short flags (-n); multi-char aliases must be
|
|
513
537
|
// long flags (--page) — commander treats `-page` as a literal short flag,
|
|
@@ -594,6 +618,27 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
594
618
|
if (tool.name === 'sb-git') {
|
|
595
619
|
cmd.option('--execute [dest]', 'For action=clone-url: run `git clone` locally to [dest] (default: ./<repo>) instead of just printing the snippet');
|
|
596
620
|
}
|
|
621
|
+
// For skills pull: `--out [dir]` writes the returned skill files to disk.
|
|
622
|
+
// Client-side because an HTTP/JSON tool can't materialise a tree on the
|
|
623
|
+
// user's machine — same shape as sb-git's `--execute`. Default dest is
|
|
624
|
+
// ./<slug>. The option is CLI-only and is stripped before the API call.
|
|
625
|
+
if (tool.name === 'skills_pull') {
|
|
626
|
+
cmd.option('--out [dir]', 'Directory to write the skill files into (default: ./<slug>)');
|
|
627
|
+
}
|
|
628
|
+
// For skills sync: the digest-diff materializer loop lives entirely on
|
|
629
|
+
// the client (Task 5's skills-sync.ts), so these are CLI-only knobs —
|
|
630
|
+
// stripped before the API call, never forwarded to the server.
|
|
631
|
+
if (tool.name === 'skills_sync') {
|
|
632
|
+
cmd.option('--check', 'report differences without writing');
|
|
633
|
+
cmd.option('--mount-dir <dir>', 'symlink mount directory (default ~/.opencode/skills)');
|
|
634
|
+
cmd.option('--store-dir <dir>', 'content store directory (default ~/.gsk/skills-store)');
|
|
635
|
+
}
|
|
636
|
+
// For skills save: the server's `files` param is an inline array with no
|
|
637
|
+
// `primary_arg`, so add our own positional for the local directory to
|
|
638
|
+
// read — same shape as `skills pull <ref> --out <dir>` in reverse.
|
|
639
|
+
if (tool.name === 'skills_save') {
|
|
640
|
+
cmd.argument('[path]', 'Local skill directory to save (default: current directory)');
|
|
641
|
+
}
|
|
597
642
|
// Generic action handler
|
|
598
643
|
cmd.action(async (primaryArgValue, opts) => {
|
|
599
644
|
// Commander passes opts as second arg when there's an argument,
|
|
@@ -738,6 +783,56 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
738
783
|
}
|
|
739
784
|
delete args.execute;
|
|
740
785
|
}
|
|
786
|
+
// skills pull --out is client-side only; never forward. `pull` always
|
|
787
|
+
// materialises to disk (default ./<slug>); --out just overrides where.
|
|
788
|
+
let skillsPullOut;
|
|
789
|
+
if (tool.name === 'skills_pull') {
|
|
790
|
+
const outVal = args.out;
|
|
791
|
+
skillsPullOut =
|
|
792
|
+
typeof outVal === 'string' && outVal.length > 0 ? outVal : undefined;
|
|
793
|
+
delete args.out;
|
|
794
|
+
}
|
|
795
|
+
// skills sync: --check/--mount-dir/--store-dir are CLI-only. The
|
|
796
|
+
// digest `state` sent to the server is always computed here (from
|
|
797
|
+
// the on-disk state file the materializer loop owns), never
|
|
798
|
+
// accepted as user input.
|
|
799
|
+
let skillsSyncCheck = false;
|
|
800
|
+
let skillsSyncStoreDir = '';
|
|
801
|
+
let skillsSyncMountDir = '';
|
|
802
|
+
if (tool.name === 'skills_sync') {
|
|
803
|
+
const { loadState, defaultStoreDir, defaultMountDir } = await import('./commands/skills-sync.js');
|
|
804
|
+
skillsSyncCheck = !!args.check;
|
|
805
|
+
skillsSyncStoreDir = args.storeDir
|
|
806
|
+
? expandHome(args.storeDir)
|
|
807
|
+
: defaultStoreDir();
|
|
808
|
+
skillsSyncMountDir = args.mountDir
|
|
809
|
+
? expandHome(args.mountDir)
|
|
810
|
+
: defaultMountDir();
|
|
811
|
+
delete args.check;
|
|
812
|
+
delete args.storeDir;
|
|
813
|
+
delete args.mountDir;
|
|
814
|
+
args.state = JSON.stringify(loadState(skillsSyncStoreDir));
|
|
815
|
+
}
|
|
816
|
+
// skills save: read the local directory (default cwd) and ship its
|
|
817
|
+
// files inline — the inverse of `skills pull`. Client-side because
|
|
818
|
+
// only the local machine can walk a directory tree.
|
|
819
|
+
if (tool.name === 'skills_save') {
|
|
820
|
+
const { collectSkillDir } = await import('./commands/skills-save.js');
|
|
821
|
+
const dir = pathModule.resolve(primaryArgValue || '.');
|
|
822
|
+
let collected;
|
|
823
|
+
try {
|
|
824
|
+
collected = collectSkillDir(dir);
|
|
825
|
+
}
|
|
826
|
+
catch (e) {
|
|
827
|
+
logError(`skills save: ${e.message}`);
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
if (collected.sidecarOwner) {
|
|
831
|
+
info(`Warning: ${dir} carries provenance for ${collected.sidecarOwner} — ` +
|
|
832
|
+
`saving will fork this skill into your own catalog.`);
|
|
833
|
+
}
|
|
834
|
+
args.files = collected.files;
|
|
835
|
+
}
|
|
741
836
|
// aidrive: handle --local_file for the upload action.
|
|
742
837
|
// Stream the file directly to AI Drive via the dedicated tool_cli
|
|
743
838
|
// endpoint (POST /api/tool_cli/aidrive/upload) — no blob middleman.
|
|
@@ -809,7 +904,7 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
809
904
|
const after = normalizeCloneUrlForGit(before);
|
|
810
905
|
data.url = after;
|
|
811
906
|
if (Array.isArray(data.commands)) {
|
|
812
|
-
data.commands = data.commands.map(
|
|
907
|
+
data.commands = data.commands.map(cmd => typeof cmd === 'string' ? cmd.replace(before, after) : cmd);
|
|
813
908
|
}
|
|
814
909
|
}
|
|
815
910
|
}
|
|
@@ -833,6 +928,96 @@ function registerToolCommand(parentProgram, tool, clientFactory, vdProjectIdDefa
|
|
|
833
928
|
process.exit(code);
|
|
834
929
|
return;
|
|
835
930
|
}
|
|
931
|
+
// skills pull: decode the returned file rows + write the skill tree to
|
|
932
|
+
// disk (default ./<slug>, or --out <dir>). Client-side because the
|
|
933
|
+
// backend returns JSON, not a filesystem. Pull always materialises;
|
|
934
|
+
// the base64 blobs are dropped from the echoed result below.
|
|
935
|
+
if (tool.name === 'skills_pull' &&
|
|
936
|
+
result.status === 'ok' &&
|
|
937
|
+
result.data &&
|
|
938
|
+
typeof result.data === 'object') {
|
|
939
|
+
const { writeSkillFiles } = await import('./commands/skills-pull.js');
|
|
940
|
+
const code = writeSkillFiles(result.data, skillsPullOut);
|
|
941
|
+
if (code !== 0)
|
|
942
|
+
process.exit(code);
|
|
943
|
+
// Drop the (large, base64) file blobs from the echoed result — the
|
|
944
|
+
// user asked to write to disk, not to dump bytes to stdout.
|
|
945
|
+
const data = result.data;
|
|
946
|
+
delete data.files;
|
|
947
|
+
output(result);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
// skills sync: `--check` reports a diff without writing. Otherwise
|
|
951
|
+
// materialize the response via applySyncResponse (Task 5), looping
|
|
952
|
+
// while the server signals a truncated bundle (`complete: false`) —
|
|
953
|
+
// only the local machine holds the digest state across calls and
|
|
954
|
+
// can write the resulting file tree / symlinks.
|
|
955
|
+
if (tool.name === 'skills_sync' &&
|
|
956
|
+
result.status === 'ok' &&
|
|
957
|
+
result.data &&
|
|
958
|
+
typeof result.data === 'object') {
|
|
959
|
+
if (skillsSyncCheck) {
|
|
960
|
+
const data = result.data;
|
|
961
|
+
for (const entry of data.skills) {
|
|
962
|
+
const suffix = entry.code ? ` (${entry.code})` : '';
|
|
963
|
+
info(`${entry.slug}: ${entry.action}${suffix}`);
|
|
964
|
+
}
|
|
965
|
+
const dirty = data.skills.some(entry => entry.action !== 'unchanged');
|
|
966
|
+
for (const entry of data.skills)
|
|
967
|
+
delete entry.files;
|
|
968
|
+
output(result);
|
|
969
|
+
if (dirty)
|
|
970
|
+
process.exit(3);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const { applySyncResponse, SyncAggregator } = await import('./commands/skills-sync.js');
|
|
974
|
+
const syncOpts = {
|
|
975
|
+
storeDir: skillsSyncStoreDir,
|
|
976
|
+
mountDir: skillsSyncMountDir,
|
|
977
|
+
};
|
|
978
|
+
let current = result;
|
|
979
|
+
// Fold every batch's outcome into deduped totals (see SyncAggregator):
|
|
980
|
+
// a server `error` row re-appears each batch, so concatenating would
|
|
981
|
+
// multi-count it and the summary + exit code would lie.
|
|
982
|
+
const agg = new SyncAggregator();
|
|
983
|
+
let iterations = 0;
|
|
984
|
+
for (;;) {
|
|
985
|
+
const data = current.data;
|
|
986
|
+
const applyResult = applySyncResponse(data, syncOpts);
|
|
987
|
+
agg.add(applyResult, current.data.skills);
|
|
988
|
+
if (data.complete !== false)
|
|
989
|
+
break;
|
|
990
|
+
iterations += 1;
|
|
991
|
+
if (iterations >= 20) {
|
|
992
|
+
logError('skills sync: exceeded 20 batches without completing — aborting');
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
current = await client.executeTool('skills_sync', {
|
|
996
|
+
state: JSON.stringify(applyResult.state),
|
|
997
|
+
});
|
|
998
|
+
if (current.status !== 'ok' || !current.data) {
|
|
999
|
+
logError(`skills sync: batch ${iterations} failed — ${current.message}`);
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
info(`synced ${agg.appliedCount} updated / ${agg.removedCount} removed / ${agg.failedCount} failed`);
|
|
1004
|
+
// Emit the aggregated (all-batch, deduped) skill rows rather than only
|
|
1005
|
+
// the final batch's, stripping the base64 file blobs first.
|
|
1006
|
+
const aggregatedSkills = agg.skills;
|
|
1007
|
+
for (const entry of aggregatedSkills)
|
|
1008
|
+
delete entry.files;
|
|
1009
|
+
current.data.skills = aggregatedSkills;
|
|
1010
|
+
output(current);
|
|
1011
|
+
// A boot script runs `sync` every startup and keys off the exit
|
|
1012
|
+
// code; a per-slug failure (mount refused, write error, server
|
|
1013
|
+
// `error` row) must not read as success. Exit 4 (distinct from 3 =
|
|
1014
|
+
// `--check` found a diff, and 1 = the sync loop itself aborted) so
|
|
1015
|
+
// the caller can tell "some skills didn't materialize" from a clean
|
|
1016
|
+
// run. Exit 0 stays reserved for fully-clean.
|
|
1017
|
+
if (agg.failedCount > 0)
|
|
1018
|
+
process.exit(4);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
836
1021
|
// If -o was specified and tool succeeded, download the output file.
|
|
837
1022
|
// Skip for create_task — it has its own export logic below.
|
|
838
1023
|
if (tool.name !== 'create_task' &&
|