@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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gsk skills pull <ref> --out <dir>` — client-side writer for the backend
|
|
3
|
+
* `skills_pull` action.
|
|
4
|
+
*
|
|
5
|
+
* Why client-side: an HTTP/JSON tool can't materialise a file tree on the
|
|
6
|
+
* user's machine. The backend `skills_pull` action returns the skill's files
|
|
7
|
+
* as `{ path, content_base64 }` rows; this helper decodes them and writes the
|
|
8
|
+
* tree to disk — same split as `sb-git clone-url --execute` — plus a
|
|
9
|
+
* `.genspark-skill.json` provenance sidecar so the tree stays identifiable
|
|
10
|
+
* and refresh-able. Pull is an authoring/export flow (pull → edit → re-upload),
|
|
11
|
+
* not a runtime mount path.
|
|
12
|
+
*/
|
|
13
|
+
/** Skill-slug shape the backend enforces (single path component, no separators
|
|
14
|
+
* or `..`). Defined here — the lowest module in the skills-CLI import graph
|
|
15
|
+
* (skills-sync imports `safeJoin` from it) — and re-used by skills-sync so the
|
|
16
|
+
* client can't drift from the server's slug contract. */
|
|
17
|
+
export declare const SLUG_RE: RegExp;
|
|
18
|
+
/** One file row from the backend `skills_pull` response. */
|
|
19
|
+
export interface SkillFile {
|
|
20
|
+
path: string;
|
|
21
|
+
size?: number;
|
|
22
|
+
content_base64: string;
|
|
23
|
+
}
|
|
24
|
+
/** Shape of the `data` field returned by the backend `skills_pull` action. */
|
|
25
|
+
export interface SkillPullData {
|
|
26
|
+
slug: string;
|
|
27
|
+
ref?: string;
|
|
28
|
+
owner?: string;
|
|
29
|
+
publisher_type?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
commit?: string;
|
|
32
|
+
file_count?: number;
|
|
33
|
+
total_bytes?: number;
|
|
34
|
+
files: SkillFile[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Provenance sidecar written next to the pulled files. This is what turns a
|
|
38
|
+
* pulled tree from an anonymous file dump into a managed artifact: it records
|
|
39
|
+
* WHICH skill (owner/slug) at WHICH commit landed here, so a later pull of the
|
|
40
|
+
* same skill can refresh the directory in place instead of being refused.
|
|
41
|
+
*/
|
|
42
|
+
export declare const SIDECAR_FILENAME = ".genspark-skill.json";
|
|
43
|
+
export interface SkillSidecar {
|
|
44
|
+
ref?: string;
|
|
45
|
+
owner?: string;
|
|
46
|
+
slug?: string;
|
|
47
|
+
commit?: string;
|
|
48
|
+
publisher_type?: string;
|
|
49
|
+
pulled_at?: string;
|
|
50
|
+
file_count?: number;
|
|
51
|
+
content_hash?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Parse the sidecar in `dest`, or null when absent/unreadable. */
|
|
54
|
+
export declare function readSidecar(dest: string): SkillSidecar | null;
|
|
55
|
+
/** Hash the tree currently on disk under `dir`, excluding the sidecar file. */
|
|
56
|
+
export declare function hashTree(dir: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the destination directory and decide whether this pull is a
|
|
59
|
+
* sidecar-matched in-place REFRESH. Default dest: the slug in the current
|
|
60
|
+
* directory; an explicit `--out` is honored (with `~` expansion).
|
|
61
|
+
*
|
|
62
|
+
* A non-empty existing directory is refused UNLESS its sidecar attests it
|
|
63
|
+
* holds a previous pull of the SAME skill (slug match, and owner match when
|
|
64
|
+
* both sides carry one) AND that pull is PRISTINE — the on-disk tree still
|
|
65
|
+
* hashes to what the sidecar recorded. A pristine match refreshes in place; an
|
|
66
|
+
* edited tree (or a same-skill dir with no recorded hash) is refused so a
|
|
67
|
+
* re-pull never silently destroys local edits (the pull → edit → re-upload
|
|
68
|
+
* authoring flow). A non-matching dir is refused so a pull never merges into an
|
|
69
|
+
* unrelated tree.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolvePullDest(slug: string, explicit: string | undefined, pulled?: {
|
|
72
|
+
slug: string;
|
|
73
|
+
owner?: string;
|
|
74
|
+
}): {
|
|
75
|
+
dest: string;
|
|
76
|
+
refresh: boolean;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Resolve a server-provided relative file path against `dest`, rejecting any
|
|
80
|
+
* path that would escape the destination (absolute paths, `..` traversal).
|
|
81
|
+
* The backend already constrains paths to the skill subtree; this is
|
|
82
|
+
* defense-in-depth so a crafted response can't write outside `--out`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function safeJoin(dest: string, relPath: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Write a `skills_pull` response to disk. Returns 0 on success, non-zero on
|
|
87
|
+
* any failure (matches the exit-code contract the caller propagates via
|
|
88
|
+
* `process.exit`).
|
|
89
|
+
*/
|
|
90
|
+
export declare function writeSkillFiles(data: SkillPullData, explicitDest: string | undefined): number;
|
|
91
|
+
//# sourceMappingURL=skills-pull.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-pull.d.ts","sourceRoot":"","sources":["../../src/commands/skills-pull.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH;;;yDAGyD;AACzD,eAAO,MAAM,OAAO,QAAqC,CAAA;AAEzD,4DAA4D;AAC5D,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,SAAS,EAAE,CAAA;CACnB;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,yBAAyB,CAAA;AAEtD,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IAInB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAW7D;AAqCD,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAe5C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,CAAC,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAgCpC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAiB9D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,CAmHR"}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gsk skills pull <ref> --out <dir>` — client-side writer for the backend
|
|
3
|
+
* `skills_pull` action.
|
|
4
|
+
*
|
|
5
|
+
* Why client-side: an HTTP/JSON tool can't materialise a file tree on the
|
|
6
|
+
* user's machine. The backend `skills_pull` action returns the skill's files
|
|
7
|
+
* as `{ path, content_base64 }` rows; this helper decodes them and writes the
|
|
8
|
+
* tree to disk — same split as `sb-git clone-url --execute` — plus a
|
|
9
|
+
* `.genspark-skill.json` provenance sidecar so the tree stays identifiable
|
|
10
|
+
* and refresh-able. Pull is an authoring/export flow (pull → edit → re-upload),
|
|
11
|
+
* not a runtime mount path.
|
|
12
|
+
*/
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { info, error as logError } from '../logger.js';
|
|
18
|
+
/** Skill-slug shape the backend enforces (single path component, no separators
|
|
19
|
+
* or `..`). Defined here — the lowest module in the skills-CLI import graph
|
|
20
|
+
* (skills-sync imports `safeJoin` from it) — and re-used by skills-sync so the
|
|
21
|
+
* client can't drift from the server's slug contract. */
|
|
22
|
+
export const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
23
|
+
/**
|
|
24
|
+
* Provenance sidecar written next to the pulled files. This is what turns a
|
|
25
|
+
* pulled tree from an anonymous file dump into a managed artifact: it records
|
|
26
|
+
* WHICH skill (owner/slug) at WHICH commit landed here, so a later pull of the
|
|
27
|
+
* same skill can refresh the directory in place instead of being refused.
|
|
28
|
+
*/
|
|
29
|
+
export const SIDECAR_FILENAME = '.genspark-skill.json';
|
|
30
|
+
/** Parse the sidecar in `dest`, or null when absent/unreadable. */
|
|
31
|
+
export function readSidecar(dest) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = fs.readFileSync(path.join(dest, SIDECAR_FILENAME), 'utf8');
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// absent or corrupt → treated as "no sidecar" (caller refuses the dir)
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Content hash of a set of skill files, canonicalized so the write-time hash
|
|
46
|
+
* (over the payload) and the re-pull-time hash (over the on-disk tree) are
|
|
47
|
+
* directly comparable: POSIX-separator relative paths, sorted, each contributes
|
|
48
|
+
* `path\0<bytes>\0`. The sidecar itself is never part of the hash.
|
|
49
|
+
*/
|
|
50
|
+
function hashFiles(entries) {
|
|
51
|
+
const hash = crypto.createHash('sha256');
|
|
52
|
+
// Canonicalize the key BEFORE sorting so the write-time hash (payload paths)
|
|
53
|
+
// and the re-pull hash (on-disk paths) agree regardless of platform:
|
|
54
|
+
// - separator: on-disk paths join with the OS separator (`\` on Windows);
|
|
55
|
+
// payload paths are `/`. Sorting on the raw separator orders the two
|
|
56
|
+
// differently (`/` and `\` are different code points), so a pristine
|
|
57
|
+
// Windows tree would never match its own recorded hash.
|
|
58
|
+
// - Unicode form: a normalizing filesystem (legacy HFS+, some SMB mounts)
|
|
59
|
+
// returns filenames in NFD while the backend emits NFC, so the same name
|
|
60
|
+
// hashes differently on write vs re-pull → a pristine tree read as edited.
|
|
61
|
+
// Normalize both (POSIX separators + NFC) and sort AND hash on that key.
|
|
62
|
+
const normalized = entries
|
|
63
|
+
.map(e => ({
|
|
64
|
+
key: e.relPath.split(path.sep).join('/').normalize('NFC'),
|
|
65
|
+
bytes: e.bytes,
|
|
66
|
+
}))
|
|
67
|
+
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
68
|
+
for (const { key, bytes } of normalized) {
|
|
69
|
+
hash.update(key);
|
|
70
|
+
hash.update('\0');
|
|
71
|
+
hash.update(bytes);
|
|
72
|
+
hash.update('\0');
|
|
73
|
+
}
|
|
74
|
+
return hash.digest('hex');
|
|
75
|
+
}
|
|
76
|
+
/** Hash the tree currently on disk under `dir`, excluding the sidecar file. */
|
|
77
|
+
export function hashTree(dir) {
|
|
78
|
+
const entries = [];
|
|
79
|
+
const walk = (abs, rel) => {
|
|
80
|
+
for (const name of fs.readdirSync(abs)) {
|
|
81
|
+
const childAbs = path.join(abs, name);
|
|
82
|
+
const childRel = rel ? path.join(rel, name) : name;
|
|
83
|
+
if (fs.statSync(childAbs).isDirectory()) {
|
|
84
|
+
walk(childAbs, childRel);
|
|
85
|
+
}
|
|
86
|
+
else if (!(rel === '' && name === SIDECAR_FILENAME)) {
|
|
87
|
+
entries.push({ relPath: childRel, bytes: fs.readFileSync(childAbs) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
walk(dir, '');
|
|
92
|
+
return hashFiles(entries);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the destination directory and decide whether this pull is a
|
|
96
|
+
* sidecar-matched in-place REFRESH. Default dest: the slug in the current
|
|
97
|
+
* directory; an explicit `--out` is honored (with `~` expansion).
|
|
98
|
+
*
|
|
99
|
+
* A non-empty existing directory is refused UNLESS its sidecar attests it
|
|
100
|
+
* holds a previous pull of the SAME skill (slug match, and owner match when
|
|
101
|
+
* both sides carry one) AND that pull is PRISTINE — the on-disk tree still
|
|
102
|
+
* hashes to what the sidecar recorded. A pristine match refreshes in place; an
|
|
103
|
+
* edited tree (or a same-skill dir with no recorded hash) is refused so a
|
|
104
|
+
* re-pull never silently destroys local edits (the pull → edit → re-upload
|
|
105
|
+
* authoring flow). A non-matching dir is refused so a pull never merges into an
|
|
106
|
+
* unrelated tree.
|
|
107
|
+
*/
|
|
108
|
+
export function resolvePullDest(slug, explicit, pulled) {
|
|
109
|
+
const dest = explicit
|
|
110
|
+
? path.resolve(explicit.replace(/^~(?=\/|$)/, os.homedir()))
|
|
111
|
+
: path.resolve(process.cwd(), slug);
|
|
112
|
+
if (fs.existsSync(dest) && fs.readdirSync(dest).length > 0) {
|
|
113
|
+
const sidecar = readSidecar(dest);
|
|
114
|
+
const sameSkill = sidecar != null &&
|
|
115
|
+
sidecar.slug === (pulled?.slug ?? slug) &&
|
|
116
|
+
(!sidecar.owner || !pulled?.owner || sidecar.owner === pulled.owner);
|
|
117
|
+
if (!sameSkill) {
|
|
118
|
+
throw new Error(`Refusing to write into ${dest}: directory exists and is not empty ` +
|
|
119
|
+
`(no ${SIDECAR_FILENAME} matching this skill). ` +
|
|
120
|
+
`Remove it or pass --out <new-path>.`);
|
|
121
|
+
}
|
|
122
|
+
// Same skill, but only refresh a PRISTINE tree. An edited tree (or one
|
|
123
|
+
// written before content hashing) must not be silently wiped.
|
|
124
|
+
const pristine = typeof sidecar.content_hash === 'string' &&
|
|
125
|
+
sidecar.content_hash === hashTree(dest);
|
|
126
|
+
if (!pristine) {
|
|
127
|
+
throw new Error(`Refusing to overwrite ${dest}: it holds local changes since the last ` +
|
|
128
|
+
`pull of this skill (or was pulled before edit-detection). Save your ` +
|
|
129
|
+
`edits, remove the directory, or pass --out <new-path>.`);
|
|
130
|
+
}
|
|
131
|
+
return { dest, refresh: true };
|
|
132
|
+
}
|
|
133
|
+
return { dest, refresh: false };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve a server-provided relative file path against `dest`, rejecting any
|
|
137
|
+
* path that would escape the destination (absolute paths, `..` traversal).
|
|
138
|
+
* The backend already constrains paths to the skill subtree; this is
|
|
139
|
+
* defense-in-depth so a crafted response can't write outside `--out`.
|
|
140
|
+
*/
|
|
141
|
+
export function safeJoin(dest, relPath) {
|
|
142
|
+
// Backend emits POSIX git-tree paths, so a backslash or Windows
|
|
143
|
+
// drive/UNC prefix can only be a crafted payload — reject before
|
|
144
|
+
// path.normalize (whose semantics differ per platform) sees it.
|
|
145
|
+
if (relPath.includes('\\') || /^[a-zA-Z]:/.test(relPath)) {
|
|
146
|
+
throw new Error(`Unsafe file path in skill payload: ${relPath}`);
|
|
147
|
+
}
|
|
148
|
+
const normalized = path.normalize(relPath);
|
|
149
|
+
if (path.isAbsolute(normalized) || normalized.split(path.sep)[0] === '..') {
|
|
150
|
+
throw new Error(`Unsafe file path in skill payload: ${relPath}`);
|
|
151
|
+
}
|
|
152
|
+
const target = path.resolve(dest, normalized);
|
|
153
|
+
const destWithSep = dest.endsWith(path.sep) ? dest : dest + path.sep;
|
|
154
|
+
if (target !== dest && !target.startsWith(destWithSep)) {
|
|
155
|
+
throw new Error(`Unsafe file path in skill payload: ${relPath}`);
|
|
156
|
+
}
|
|
157
|
+
return target;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Write a `skills_pull` response to disk. Returns 0 on success, non-zero on
|
|
161
|
+
* any failure (matches the exit-code contract the caller propagates via
|
|
162
|
+
* `process.exit`).
|
|
163
|
+
*/
|
|
164
|
+
export function writeSkillFiles(data, explicitDest) {
|
|
165
|
+
if (!data || !Array.isArray(data.files) || data.files.length === 0) {
|
|
166
|
+
logError('skills pull: response contained no files.');
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
if (typeof data.slug !== 'string' || !data.slug.trim()) {
|
|
170
|
+
logError('skills pull: response is missing a skill slug.');
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
// The slug computes the default destination dir, so validate its shape the
|
|
174
|
+
// same way the backend does (single path component, no separators / `..`).
|
|
175
|
+
// safeJoin only guards the per-file paths; an unsanitised slug would let a
|
|
176
|
+
// crafted response escape the destination root before the per-file check.
|
|
177
|
+
if (!SLUG_RE.test(data.slug)) {
|
|
178
|
+
logError(`skills pull: response has an unsafe skill slug: ${data.slug}`);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
// The provenance sidecar owns the root SIDECAR_FILENAME. If the skill itself
|
|
182
|
+
// ships a root-level file by that name, writing the sidecar would clobber it
|
|
183
|
+
// and the pristine check (which excludes the root sidecar) would silently
|
|
184
|
+
// drop it — so refuse rather than corrupt the skill. A same-named file in a
|
|
185
|
+
// SUBDIR is fine (only the root sidecar is reserved).
|
|
186
|
+
if (data.files.some(f => f.path === SIDECAR_FILENAME)) {
|
|
187
|
+
logError(`skills pull: skill ships a reserved root file ${SIDECAR_FILENAME}; ` +
|
|
188
|
+
`cannot materialize it without clobbering the provenance sidecar.`);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
let dest;
|
|
192
|
+
let refresh;
|
|
193
|
+
try {
|
|
194
|
+
;
|
|
195
|
+
({ dest, refresh } = resolvePullDest(data.slug, explicitDest, {
|
|
196
|
+
slug: data.slug,
|
|
197
|
+
owner: data.owner,
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
logError(e.message);
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
const destExisted = fs.existsSync(dest);
|
|
205
|
+
// A sidecar-matched refresh REPLACES the previous pull: clear the directory
|
|
206
|
+
// first so files deleted upstream don't linger as stale strays. The old
|
|
207
|
+
// content is by definition a previous served snapshot of this same skill
|
|
208
|
+
// (that is what the sidecar attests), so wiping it loses nothing local.
|
|
209
|
+
if (refresh) {
|
|
210
|
+
try {
|
|
211
|
+
for (const entry of fs.readdirSync(dest)) {
|
|
212
|
+
fs.rmSync(path.join(dest, entry), { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
logError(`skills pull: failed to clear ${dest} for refresh: ${e.message}`);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Roll back partial writes on failure so a mid-loop error (a crafted
|
|
221
|
+
// traversal/NUL path, disk-full) never leaves a half-written tree that then
|
|
222
|
+
// permanently fails the next retry on resolvePullDest's non-empty-dir guard.
|
|
223
|
+
try {
|
|
224
|
+
const written = [];
|
|
225
|
+
for (const file of data.files) {
|
|
226
|
+
if (!file.path)
|
|
227
|
+
continue; // skip a degenerate empty path (would resolve to dest itself)
|
|
228
|
+
const target = safeJoin(dest, file.path);
|
|
229
|
+
const bytes = Buffer.from(file.content_base64, 'base64');
|
|
230
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
231
|
+
fs.writeFileSync(target, bytes);
|
|
232
|
+
written.push({ relPath: file.path, bytes });
|
|
233
|
+
}
|
|
234
|
+
// Sidecar last, after every file landed — a crash mid-write must never
|
|
235
|
+
// leave a sidecar attesting a complete pull over a partial tree. The
|
|
236
|
+
// content_hash pins this exact tree so a later re-pull can tell a pristine
|
|
237
|
+
// directory (safe to refresh) from one the user has edited.
|
|
238
|
+
const sidecar = {
|
|
239
|
+
ref: data.ref ?? (data.owner ? `${data.owner}/${data.slug}` : data.slug),
|
|
240
|
+
owner: data.owner,
|
|
241
|
+
slug: data.slug,
|
|
242
|
+
commit: data.commit,
|
|
243
|
+
publisher_type: data.publisher_type,
|
|
244
|
+
pulled_at: new Date().toISOString(),
|
|
245
|
+
file_count: data.files.length,
|
|
246
|
+
content_hash: hashFiles(written),
|
|
247
|
+
};
|
|
248
|
+
fs.writeFileSync(path.join(dest, SIDECAR_FILENAME), JSON.stringify(sidecar, null, 2) + '\n');
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
try {
|
|
252
|
+
if (!destExisted) {
|
|
253
|
+
fs.rmSync(dest, { recursive: true, force: true }); // we created it → remove fully
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// dest pre-existed as either EMPTY (guard-proven) or a refresh we
|
|
257
|
+
// already cleared — every entry now under it is ours. Clear it back to
|
|
258
|
+
// empty (leaving empty subdirs would trip the non-empty-dir guard and
|
|
259
|
+
// brick every retry). A failed refresh thus ends EMPTY, not restored —
|
|
260
|
+
// inherent to refresh-in-place; the next pull rebuilds it.
|
|
261
|
+
for (const entry of fs.readdirSync(dest)) {
|
|
262
|
+
fs.rmSync(path.join(dest, entry), { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// best-effort cleanup; the original write error below is what matters
|
|
268
|
+
}
|
|
269
|
+
logError(`skills pull: failed to write files: ${e.message}`);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
info(`${refresh ? 'Refreshed' : 'Pulled'} ${data.files.length} file(s) for ${data.slug} → ${dest}`);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=skills-pull.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-pull.js","sourceRoot":"","sources":["../../src/commands/skills-pull.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gsk skills save [path]` — client-side directory collector for the backend
|
|
3
|
+
* `skills_save` action.
|
|
4
|
+
*
|
|
5
|
+
* Why client-side: `skills_save` takes `files: [{path, content_base64}]` in
|
|
6
|
+
* its request body (the inverse of `skills_pull`'s response shape) — an
|
|
7
|
+
* HTTP/JSON tool has no way to walk a local directory itself, so the CLI
|
|
8
|
+
* reads the tree and ships the encoded bytes. This is the sentinel-protocol
|
|
9
|
+
* successor referenced in `gsk_tool_adaptor.py`'s `_action_save`: SAS used to
|
|
10
|
+
* drop a `.save_to_self` file for the sandbox host to notice and zip
|
|
11
|
+
* out-of-band; here the CLI does the same walk itself, synchronously, before
|
|
12
|
+
* the request goes out.
|
|
13
|
+
*/
|
|
14
|
+
export interface CollectedFiles {
|
|
15
|
+
files: Array<{
|
|
16
|
+
path: string;
|
|
17
|
+
content_base64: string;
|
|
18
|
+
}>;
|
|
19
|
+
totalBytes: number;
|
|
20
|
+
sidecarOwner?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Walk `dir` and collect every real file into a `skills_save` payload.
|
|
24
|
+
*
|
|
25
|
+
* - Symlinks are never followed (skipped outright) — a save must not escape
|
|
26
|
+
* `dir` or loop on a cyclic link.
|
|
27
|
+
* - At the root only, dotfiles are excluded except `.gitignore` (ordinary
|
|
28
|
+
* authoring content) and the pull provenance sidecar `.genspark-skill.json`
|
|
29
|
+
* (metadata, not a skill file). Dotfiles in subdirectories are kept as-is.
|
|
30
|
+
* - Requires a root `SKILL.md`: the server rejects a save without one
|
|
31
|
+
* anyway (frontmatter validation), so failing fast here saves a round trip.
|
|
32
|
+
* - Throws once the running byte total exceeds `maxBytes`.
|
|
33
|
+
*
|
|
34
|
+
* Returns the sidecar's recorded `owner` (if any) so the caller can warn the
|
|
35
|
+
* user before saving into someone else's pulled skill forks it into their
|
|
36
|
+
* own catalog.
|
|
37
|
+
*/
|
|
38
|
+
export declare function collectSkillDir(dir: string, maxBytes?: number): CollectedFiles;
|
|
39
|
+
//# sourceMappingURL=skills-save.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-save.d.ts","sourceRoot":"","sources":["../../src/commands/skills-save.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACtD,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAA0B,GACnC,cAAc,CA2DhB"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gsk skills save [path]` — client-side directory collector for the backend
|
|
3
|
+
* `skills_save` action.
|
|
4
|
+
*
|
|
5
|
+
* Why client-side: `skills_save` takes `files: [{path, content_base64}]` in
|
|
6
|
+
* its request body (the inverse of `skills_pull`'s response shape) — an
|
|
7
|
+
* HTTP/JSON tool has no way to walk a local directory itself, so the CLI
|
|
8
|
+
* reads the tree and ships the encoded bytes. This is the sentinel-protocol
|
|
9
|
+
* successor referenced in `gsk_tool_adaptor.py`'s `_action_save`: SAS used to
|
|
10
|
+
* drop a `.save_to_self` file for the sandbox host to notice and zip
|
|
11
|
+
* out-of-band; here the CLI does the same walk itself, synchronously, before
|
|
12
|
+
* the request goes out.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { readSidecar, SIDECAR_FILENAME } from './skills-pull.js';
|
|
17
|
+
/** Mirrors the backend's `_MAX_SAVE_BYTES` (gsk_tool_adaptor.py) so a save
|
|
18
|
+
* that would be rejected server-side fails locally without a round trip. */
|
|
19
|
+
const DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
|
|
20
|
+
/**
|
|
21
|
+
* Walk `dir` and collect every real file into a `skills_save` payload.
|
|
22
|
+
*
|
|
23
|
+
* - Symlinks are never followed (skipped outright) — a save must not escape
|
|
24
|
+
* `dir` or loop on a cyclic link.
|
|
25
|
+
* - At the root only, dotfiles are excluded except `.gitignore` (ordinary
|
|
26
|
+
* authoring content) and the pull provenance sidecar `.genspark-skill.json`
|
|
27
|
+
* (metadata, not a skill file). Dotfiles in subdirectories are kept as-is.
|
|
28
|
+
* - Requires a root `SKILL.md`: the server rejects a save without one
|
|
29
|
+
* anyway (frontmatter validation), so failing fast here saves a round trip.
|
|
30
|
+
* - Throws once the running byte total exceeds `maxBytes`.
|
|
31
|
+
*
|
|
32
|
+
* Returns the sidecar's recorded `owner` (if any) so the caller can warn the
|
|
33
|
+
* user before saving into someone else's pulled skill forks it into their
|
|
34
|
+
* own catalog.
|
|
35
|
+
*/
|
|
36
|
+
export function collectSkillDir(dir, maxBytes = DEFAULT_MAX_BYTES) {
|
|
37
|
+
const skillMdPath = path.join(dir, 'SKILL.md');
|
|
38
|
+
if (!fs.existsSync(skillMdPath) || !fs.statSync(skillMdPath).isFile()) {
|
|
39
|
+
throw new Error(`${dir} has no SKILL.md at its root — not a skill directory`);
|
|
40
|
+
}
|
|
41
|
+
const files = [];
|
|
42
|
+
let totalBytes = 0;
|
|
43
|
+
const walk = (abs, rel) => {
|
|
44
|
+
const isRoot = rel === '';
|
|
45
|
+
for (const dirent of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
46
|
+
if (dirent.isSymbolicLink())
|
|
47
|
+
continue;
|
|
48
|
+
if (isRoot &&
|
|
49
|
+
(dirent.name === SIDECAR_FILENAME ||
|
|
50
|
+
(dirent.name.startsWith('.') && dirent.name !== '.gitignore'))) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const childAbs = path.join(abs, dirent.name);
|
|
54
|
+
const childRel = rel ? path.join(rel, dirent.name) : dirent.name;
|
|
55
|
+
if (dirent.isDirectory()) {
|
|
56
|
+
// Never walk or ship dependency / VCS trees, at ANY depth — they are
|
|
57
|
+
// never part of a skill, can be enormous (blowing the byte cap on
|
|
58
|
+
// junk), and `.git` would leak history. Root `.git` is already caught
|
|
59
|
+
// by the dotfile filter above; this also covers nested ones and
|
|
60
|
+
// `node_modules` (not a dotfile, so otherwise walked).
|
|
61
|
+
if (dirent.name === 'node_modules' || dirent.name === '.git')
|
|
62
|
+
continue;
|
|
63
|
+
walk(childAbs, childRel);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!dirent.isFile())
|
|
67
|
+
continue;
|
|
68
|
+
// Cap-check on the stat size BEFORE reading: an over-cap file yields a
|
|
69
|
+
// clean cap error without ballooning memory or tripping Node's
|
|
70
|
+
// ERR_FS_FILE_TOO_LARGE on a huge readFileSync.
|
|
71
|
+
const size = fs.statSync(childAbs).size;
|
|
72
|
+
if (totalBytes + size > maxBytes) {
|
|
73
|
+
throw new Error(`skill directory exceeds the ${maxBytes} byte cap`);
|
|
74
|
+
}
|
|
75
|
+
const bytes = fs.readFileSync(childAbs);
|
|
76
|
+
totalBytes += bytes.length;
|
|
77
|
+
files.push({
|
|
78
|
+
path: childRel.split(path.sep).join('/'),
|
|
79
|
+
content_base64: bytes.toString('base64'),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
walk(dir, '');
|
|
84
|
+
if (files.length === 0) {
|
|
85
|
+
throw new Error(`${dir} contains no files to save`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
files,
|
|
89
|
+
totalBytes,
|
|
90
|
+
sidecarOwner: readSidecar(dir)?.owner,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=skills-save.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-save.js","sourceRoot":"","sources":["../../src/commands/skills-save.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAA;AACxB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAE5B,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEhE;4EAC4E;AAC5E,MAAM,iBAAiB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAA;AAQ1C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAW,EACX,WAAmB,iBAAiB;IAEpC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAA;IAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,sDAAsD,CAAC,CAAA;IAC/E,CAAC;IAED,MAAM,KAAK,GAAoD,EAAE,CAAA;IACjE,IAAI,UAAU,GAAG,CAAC,CAAA;IAElB,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,GAAW,EAAQ,EAAE;QAC9C,MAAM,MAAM,GAAG,GAAG,KAAK,EAAE,CAAA;QACzB,KAAK,MAAM,MAAM,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAClE,IAAI,MAAM,CAAC,cAAc,EAAE;gBAAE,SAAQ;YACrC,IACE,MAAM;gBACN,CAAC,MAAM,CAAC,IAAI,KAAK,gBAAgB;oBAC/B,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,EAChE,CAAC;gBACD,SAAQ;YACV,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAA;YAC5C,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAA;YAChE,IAAI,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;gBACzB,qEAAqE;gBACrE,kEAAkE;gBAClE,sEAAsE;gBACtE,gEAAgE;gBAChE,uDAAuD;gBACvD,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM;oBAAE,SAAQ;gBACtE,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;gBACxB,SAAQ;YACV,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;gBAAE,SAAQ;YAC9B,uEAAuE;YACvE,+DAA+D;YAC/D,gDAAgD;YAChD,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAA;YACvC,IAAI,UAAU,GAAG,IAAI,GAAG,QAAQ,EAAE,CAAC;gBACjC,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,WAAW,CAAC,CAAA;YACrE,CAAC;YACD,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAA;YACvC,UAAU,IAAI,KAAK,CAAC,MAAM,CAAA;YAC1B,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;gBACxC,cAAc,EAAE,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;aACzC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CAAA;IACD,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IAEb,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,4BAA4B,CAAC,CAAA;IACrD,CAAC;IAED,OAAO;QACL,KAAK;QACL,UAAU;QACV,YAAY,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,KAAK;KACtC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
/** One file row from a `skills_sync` `update` entry. */
|
|
19
|
+
export interface SyncSkillFile {
|
|
20
|
+
path: string;
|
|
21
|
+
content_base64: string;
|
|
22
|
+
}
|
|
23
|
+
/** One per-slug entry in a `skills_sync` response. */
|
|
24
|
+
export interface SyncSkillEntry {
|
|
25
|
+
slug: string;
|
|
26
|
+
sha?: string;
|
|
27
|
+
action: 'update' | 'unchanged' | 'removed' | 'error';
|
|
28
|
+
files?: SyncSkillFile[];
|
|
29
|
+
total_bytes?: number;
|
|
30
|
+
code?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Shape of the `data` field returned by the backend `skills_sync` action. */
|
|
33
|
+
export interface SyncResponseData {
|
|
34
|
+
synced?: boolean;
|
|
35
|
+
complete: boolean;
|
|
36
|
+
skills: SyncSkillEntry[];
|
|
37
|
+
}
|
|
38
|
+
export interface SyncOptions {
|
|
39
|
+
storeDir?: string;
|
|
40
|
+
mountDir?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ApplySyncResult {
|
|
43
|
+
state: Record<string, string>;
|
|
44
|
+
applied: string[];
|
|
45
|
+
removed: string[];
|
|
46
|
+
failed: string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Folds the per-batch results of the multi-batch sync loop into single deduped
|
|
50
|
+
* totals. A slug can recur across batches — a server `error` row carries no
|
|
51
|
+
* recordable sha, so the server re-emits it in EVERY batch — and concatenating
|
|
52
|
+
* per-batch arrays would multi-count it, making the summary counts and the
|
|
53
|
+
* exit-4 gate wrong. Each category is a Set (dedup by slug); a slug's outcome
|
|
54
|
+
* in a later batch supersedes an earlier one. It also aggregates the echoed
|
|
55
|
+
* skill rows so `-o json` reflects every batch, not just the final one.
|
|
56
|
+
*
|
|
57
|
+
* Generic over the echoed-row type so the CLI can pass its own row shape
|
|
58
|
+
* without a cast (only `slug` is required here).
|
|
59
|
+
*/
|
|
60
|
+
export declare class SyncAggregator<Row extends {
|
|
61
|
+
slug: string;
|
|
62
|
+
}> {
|
|
63
|
+
private readonly _applied;
|
|
64
|
+
private readonly _removed;
|
|
65
|
+
private readonly _failed;
|
|
66
|
+
private readonly _rows;
|
|
67
|
+
/** Fold one batch's apply result and its echoed skill rows into the totals. */
|
|
68
|
+
add(result: ApplySyncResult, rows: readonly Row[]): void;
|
|
69
|
+
private categorize;
|
|
70
|
+
get appliedCount(): number;
|
|
71
|
+
get removedCount(): number;
|
|
72
|
+
get failedCount(): number;
|
|
73
|
+
/** Aggregated echoed rows (deduped by slug, last-wins). */
|
|
74
|
+
get skills(): Row[];
|
|
75
|
+
}
|
|
76
|
+
export declare const STATE_FILENAME = "skills-sync-state.json";
|
|
77
|
+
export declare function defaultStoreDir(): string;
|
|
78
|
+
export declare function defaultMountDir(): string;
|
|
79
|
+
/** Load the persisted `{slug: sha}` digest map. Both unparseable JSON AND a
|
|
80
|
+
* valid-JSON-but-wrong-shape file self-heal to `{}` — a dirty state file
|
|
81
|
+
* forces a full re-diff on the next sync rather than risk skipping a change
|
|
82
|
+
* against a state we can't trust. The shape check matters because the server
|
|
83
|
+
* verb rejects the WHOLE sync (`bad state digest map`) on a single bad key,
|
|
84
|
+
* so without validating here a corrupt map would wedge every sync forever;
|
|
85
|
+
* resetting to `{}` lets it recover on the next run. */
|
|
86
|
+
export declare function loadState(storeDir: string): Record<string, string>;
|
|
87
|
+
/**
|
|
88
|
+
* Apply one `skills_sync` response to disk. Never called in `--check` mode
|
|
89
|
+
* (Task 6's caller diffs against `loadState` output itself for that path).
|
|
90
|
+
*/
|
|
91
|
+
export declare function applySyncResponse(data: SyncResponseData, opts?: SyncOptions): ApplySyncResult;
|
|
92
|
+
//# sourceMappingURL=skills-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-sync.d.ts","sourceRoot":"","sources":["../../src/commands/skills-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AASH,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAA;IACpD,KAAK,CAAC,EAAE,aAAa,EAAE,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,cAAc,EAAE,CAAA;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,cAAc,CAAC,GAAG,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAE/C,+EAA+E;IAC/E,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,GAAG,IAAI;IAOxD,OAAO,CAAC,UAAU;IAOlB,IAAI,YAAY,IAAI,MAAM,CAEzB;IACD,IAAI,YAAY,IAAI,MAAM,CAEzB;IACD,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,2DAA2D;IAC3D,IAAI,MAAM,IAAI,GAAG,EAAE,CAElB;CACF;AAED,eAAO,MAAM,cAAc,2BAA2B,CAAA;AAMtD,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;;;wDAMwD;AACxD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAelE;AAkJD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,IAAI,GAAE,WAAgB,GACrB,eAAe,CAiGjB"}
|