@xano/cli 1.0.4-beta.2 → 1.0.4-beta.4
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/auth/index.d.ts +38 -1
- package/dist/commands/auth/index.js +134 -42
- package/dist/commands/sandbox/pull/index.js +12 -2
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +17 -13
- package/dist/commands/workspace/pull/index.js +13 -2
- package/dist/commands/workspace/push/index.d.ts +1 -1
- package/dist/commands/workspace/push/index.js +15 -5
- package/dist/utils/knowledge-sync.d.ts +113 -0
- package/dist/utils/knowledge-sync.js +404 -0
- package/dist/utils/multidoc-push.d.ts +22 -0
- package/dist/utils/multidoc-push.js +242 -91
- package/oclif.manifest.json +1855 -1836
- package/package.json +1 -1
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/* eslint-disable camelcase -- knowledge_type / dry_run / guid_map are external Metadata API field names */
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import snakeCase from 'lodash.snakecase';
|
|
4
|
+
import { minimatch } from 'minimatch';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
7
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// Knowledge mirrors the multidoc model: the server returns self-describing
|
|
10
|
+
// *objects* and the CLI owns the on-disk folder layout + YAML frontmatter
|
|
11
|
+
// (see workspace/pull for the equivalent `.xs` mapping). We deliberately do NOT
|
|
12
|
+
// let the server dictate file paths.
|
|
13
|
+
export const KNOWLEDGE_DIR = 'knowledge';
|
|
14
|
+
// ── Frontmatter ──────────────────────────────────────────────────────────────
|
|
15
|
+
const FRONTMATTER_ORDER = ['name', 'description', 'knowledge_type', 'scope', 'mode', 'enabled', 'guid'];
|
|
16
|
+
/**
|
|
17
|
+
* Build a primary `.md` file body: YAML frontmatter (built from the object's
|
|
18
|
+
* structured fields) followed by the markdown content.
|
|
19
|
+
*/
|
|
20
|
+
export function buildPrimaryContent(obj) {
|
|
21
|
+
const meta = {};
|
|
22
|
+
meta.name = obj.name;
|
|
23
|
+
if (obj.description !== undefined)
|
|
24
|
+
meta.description = obj.description;
|
|
25
|
+
meta.knowledge_type = obj.knowledge_type;
|
|
26
|
+
if (obj.scope !== undefined)
|
|
27
|
+
meta.scope = obj.scope;
|
|
28
|
+
if (obj.mode !== undefined)
|
|
29
|
+
meta.mode = obj.mode;
|
|
30
|
+
if (obj.enabled !== undefined)
|
|
31
|
+
meta.enabled = obj.enabled;
|
|
32
|
+
if (obj.guid !== undefined)
|
|
33
|
+
meta.guid = obj.guid;
|
|
34
|
+
// Emit keys in a stable, human-friendly order.
|
|
35
|
+
const ordered = {};
|
|
36
|
+
for (const key of FRONTMATTER_ORDER) {
|
|
37
|
+
if (key in meta)
|
|
38
|
+
ordered[key] = meta[key];
|
|
39
|
+
}
|
|
40
|
+
const fm = yaml.dump(ordered, { lineWidth: -1 }).trimEnd();
|
|
41
|
+
const body = obj.content ?? '';
|
|
42
|
+
return `---\n${fm}\n---\n${body}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Split a primary `.md` file into its frontmatter fields and markdown body.
|
|
46
|
+
* Returns `{meta: {}, body: <raw>}` when there is no leading `---` block.
|
|
47
|
+
*/
|
|
48
|
+
export function parsePrimaryContent(raw) {
|
|
49
|
+
const lines = raw.split('\n');
|
|
50
|
+
if (lines[0]?.trim() !== '---')
|
|
51
|
+
return { body: raw, meta: {} };
|
|
52
|
+
let close = -1;
|
|
53
|
+
for (let i = 1; i < lines.length; i++) {
|
|
54
|
+
if (lines[i].trim() === '---') {
|
|
55
|
+
close = i;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (close === -1)
|
|
60
|
+
return { body: raw, meta: {} };
|
|
61
|
+
const fmText = lines.slice(1, close).join('\n');
|
|
62
|
+
const body = lines.slice(close + 1).join('\n');
|
|
63
|
+
let meta = {};
|
|
64
|
+
try {
|
|
65
|
+
meta = yaml.load(fmText) ?? {};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
meta = {};
|
|
69
|
+
}
|
|
70
|
+
return { body, meta };
|
|
71
|
+
}
|
|
72
|
+
/** Read just `guid`/`name` from frontmatter (used by GUID matching). */
|
|
73
|
+
export function parseFrontmatter(content) {
|
|
74
|
+
const { meta } = parsePrimaryContent(content);
|
|
75
|
+
const result = {};
|
|
76
|
+
if (typeof meta.guid === 'string')
|
|
77
|
+
result.guid = meta.guid;
|
|
78
|
+
if (typeof meta.name === 'string')
|
|
79
|
+
result.name = meta.name;
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
// ── Path mapping (CLI-owned layout) ────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Allocate a unique snake_case folder/file segment per call, suffixing
|
|
85
|
+
* collisions with `_2`, `_3`, … . Each object instance gets its own segment —
|
|
86
|
+
* we deliberately do NOT memoize by name, because two distinct objects can
|
|
87
|
+
* share a name (identity travels via the frontmatter `guid`, not the path), and
|
|
88
|
+
* collapsing them onto one path would silently overwrite files.
|
|
89
|
+
*/
|
|
90
|
+
function makeNameResolver() {
|
|
91
|
+
const taken = new Set();
|
|
92
|
+
return (name) => {
|
|
93
|
+
const base = snakeCase(name) || 'untitled';
|
|
94
|
+
let candidate = base;
|
|
95
|
+
let n = 2;
|
|
96
|
+
while (taken.has(candidate)) {
|
|
97
|
+
candidate = `${base}_${n}`;
|
|
98
|
+
n++;
|
|
99
|
+
}
|
|
100
|
+
taken.add(candidate);
|
|
101
|
+
return candidate;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** POSIX path (relative to `knowledge/`) of an object's primary `.md` file. */
|
|
105
|
+
function primaryRelPath(obj, resolvers) {
|
|
106
|
+
switch (obj.knowledge_type) {
|
|
107
|
+
case 'agents.md': {
|
|
108
|
+
return 'agents.md';
|
|
109
|
+
}
|
|
110
|
+
case 'doc': {
|
|
111
|
+
return `docs/${resolvers.doc(obj.name)}.md`;
|
|
112
|
+
}
|
|
113
|
+
case 'skill': {
|
|
114
|
+
return `skills/${resolvers.skill(obj.name)}/SKILL.md`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Infer the knowledge type from a primary file's POSIX path. */
|
|
119
|
+
function inferType(posixPath) {
|
|
120
|
+
if (posixPath.startsWith('skills/'))
|
|
121
|
+
return 'skill';
|
|
122
|
+
if (posixPath.startsWith('docs/'))
|
|
123
|
+
return 'doc';
|
|
124
|
+
return 'agents.md';
|
|
125
|
+
}
|
|
126
|
+
// ── Pull: write objects to disk ────────────────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Write knowledge objects under `<outputDir>/knowledge/`, building frontmatter
|
|
129
|
+
* and laying out skill reference files under `<skill>/references/`.
|
|
130
|
+
* Returns the number of files written (primaries + references).
|
|
131
|
+
*/
|
|
132
|
+
export function writeKnowledge(objects, outputDir) {
|
|
133
|
+
const resolvers = { doc: makeNameResolver(), skill: makeNameResolver() };
|
|
134
|
+
let count = 0;
|
|
135
|
+
for (const obj of objects) {
|
|
136
|
+
const rel = primaryRelPath(obj, resolvers);
|
|
137
|
+
const primaryAbs = join(outputDir, KNOWLEDGE_DIR, ...rel.split('/'));
|
|
138
|
+
fs.mkdirSync(dirname(primaryAbs), { recursive: true });
|
|
139
|
+
fs.writeFileSync(primaryAbs, buildPrimaryContent(obj), 'utf8');
|
|
140
|
+
count++;
|
|
141
|
+
if (obj.knowledge_type === 'skill' && obj.files?.length) {
|
|
142
|
+
const refsDir = join(dirname(primaryAbs), 'references');
|
|
143
|
+
for (const file of obj.files) {
|
|
144
|
+
const refAbs = join(refsDir, ...file.name.split('/'));
|
|
145
|
+
fs.mkdirSync(dirname(refAbs), { recursive: true });
|
|
146
|
+
fs.writeFileSync(refAbs, file.content, 'utf8');
|
|
147
|
+
count++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return count;
|
|
152
|
+
}
|
|
153
|
+
// ── Push: collect objects from disk ────────────────────────────────────────────
|
|
154
|
+
function listFilesRecursive(baseDir, currentDir, out) {
|
|
155
|
+
for (const item of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
156
|
+
const fullPath = join(currentDir, item.name);
|
|
157
|
+
if (item.isDirectory()) {
|
|
158
|
+
listFilesRecursive(baseDir, fullPath, out);
|
|
159
|
+
}
|
|
160
|
+
else if (item.isFile()) {
|
|
161
|
+
out.push(relative(baseDir, fullPath).split(sep).join('/'));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** True if `posixPath` is a primary file (not a reference). */
|
|
166
|
+
function isPrimary(posixPath) {
|
|
167
|
+
if (posixPath === 'agents.md')
|
|
168
|
+
return true;
|
|
169
|
+
if (/^skills\/[^/]+\/SKILL\.md$/.test(posixPath))
|
|
170
|
+
return true;
|
|
171
|
+
if (/^docs\/[^/]+\.md$/.test(posixPath))
|
|
172
|
+
return true;
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Walk `<inputDir>/knowledge/`, reconstructing structured knowledge objects from
|
|
177
|
+
* primary `.md` files (frontmatter → fields, body → content) and attaching each
|
|
178
|
+
* skill's `references/` files. Include/exclude globs are matched against
|
|
179
|
+
* `knowledge/<path>` (relative to inputDir), consistent with multidoc filtering.
|
|
180
|
+
* Returns `[]` when the directory is absent.
|
|
181
|
+
*/
|
|
182
|
+
export function collectKnowledgeObjects(inputDir, include, exclude) {
|
|
183
|
+
const knowledgeDir = join(inputDir, KNOWLEDGE_DIR);
|
|
184
|
+
if (!fs.existsSync(knowledgeDir))
|
|
185
|
+
return [];
|
|
186
|
+
const allPaths = [];
|
|
187
|
+
listFilesRecursive(knowledgeDir, knowledgeDir, allPaths);
|
|
188
|
+
const allowed = (posixPath) => {
|
|
189
|
+
const rel = `${KNOWLEDGE_DIR}/${posixPath}`;
|
|
190
|
+
if (include?.length && !include.some((p) => minimatch(rel, p, { matchBase: true })))
|
|
191
|
+
return false;
|
|
192
|
+
if (exclude?.length && exclude.some((p) => minimatch(rel, p, { matchBase: true })))
|
|
193
|
+
return false;
|
|
194
|
+
return true;
|
|
195
|
+
};
|
|
196
|
+
const objects = [];
|
|
197
|
+
for (const posixPath of allPaths.sort()) {
|
|
198
|
+
if (!isPrimary(posixPath) || !allowed(posixPath))
|
|
199
|
+
continue;
|
|
200
|
+
const absPath = join(knowledgeDir, ...posixPath.split('/'));
|
|
201
|
+
const raw = fs.readFileSync(absPath, 'utf8');
|
|
202
|
+
const { body, meta } = parsePrimaryContent(raw);
|
|
203
|
+
const knowledgeType = meta.knowledge_type === 'agents.md' || meta.knowledge_type === 'doc' || meta.knowledge_type === 'skill'
|
|
204
|
+
? meta.knowledge_type
|
|
205
|
+
: inferType(posixPath);
|
|
206
|
+
// The API requires description/scope/mode/enabled on every item, so default
|
|
207
|
+
// them when a hand-authored file omits them (pulled files always carry them).
|
|
208
|
+
const obj = {
|
|
209
|
+
content: body,
|
|
210
|
+
description: typeof meta.description === 'string' ? meta.description : '',
|
|
211
|
+
enabled: typeof meta.enabled === 'boolean' ? meta.enabled : true,
|
|
212
|
+
filePath: absPath,
|
|
213
|
+
knowledge_type: knowledgeType,
|
|
214
|
+
mode: typeof meta.mode === 'string' ? meta.mode : 'auto',
|
|
215
|
+
name: typeof meta.name === 'string' ? meta.name : deriveName(posixPath),
|
|
216
|
+
scope: typeof meta.scope === 'string' ? meta.scope : 'workspace',
|
|
217
|
+
};
|
|
218
|
+
if (typeof meta.guid === 'string')
|
|
219
|
+
obj.guid = meta.guid;
|
|
220
|
+
if (knowledgeType === 'skill') {
|
|
221
|
+
const skillFolder = posixPath.slice('skills/'.length, posixPath.indexOf('/SKILL.md'));
|
|
222
|
+
const refPrefix = `skills/${skillFolder}/references/`;
|
|
223
|
+
const files = [];
|
|
224
|
+
for (const candidate of allPaths) {
|
|
225
|
+
if (candidate.startsWith(refPrefix) && allowed(candidate)) {
|
|
226
|
+
files.push({
|
|
227
|
+
content: fs.readFileSync(join(knowledgeDir, ...candidate.split('/')), 'utf8'),
|
|
228
|
+
name: candidate.slice(refPrefix.length),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (files.length > 0)
|
|
233
|
+
obj.files = files;
|
|
234
|
+
}
|
|
235
|
+
objects.push(obj);
|
|
236
|
+
}
|
|
237
|
+
return objects;
|
|
238
|
+
}
|
|
239
|
+
function deriveName(posixPath) {
|
|
240
|
+
if (posixPath === 'agents.md')
|
|
241
|
+
return 'agents.md';
|
|
242
|
+
const parts = posixPath.split('/');
|
|
243
|
+
if (posixPath.startsWith('skills/'))
|
|
244
|
+
return parts[1];
|
|
245
|
+
return parts.at(-1).replace(/\.md$/, '');
|
|
246
|
+
}
|
|
247
|
+
/** Strip the local-only `filePath` before sending objects to the API. */
|
|
248
|
+
export function toPushItems(objects) {
|
|
249
|
+
return objects.map(({ filePath: _filePath, ...rest }) => rest);
|
|
250
|
+
}
|
|
251
|
+
// ── API calls ──────────────────────────────────────────────────────────────────
|
|
252
|
+
/**
|
|
253
|
+
* GET workspace knowledge objects (with content). Returns `[]` on 404 (instance
|
|
254
|
+
* without the feature) or any network error, so pull degrades gracefully.
|
|
255
|
+
*/
|
|
256
|
+
export async function fetchKnowledge(baseUrl, branch, accessToken, verboseFetch, verbose) {
|
|
257
|
+
const params = new URLSearchParams({ branch });
|
|
258
|
+
const url = `${baseUrl}?${params.toString()}`;
|
|
259
|
+
try {
|
|
260
|
+
const response = await verboseFetch(url, { headers: { accept: 'application/json', Authorization: `Bearer ${accessToken}` }, method: 'GET' }, verbose, accessToken);
|
|
261
|
+
if (!response.ok)
|
|
262
|
+
return [];
|
|
263
|
+
const json = (await response.json());
|
|
264
|
+
if (Array.isArray(json))
|
|
265
|
+
return json;
|
|
266
|
+
return json.items ?? [];
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* POST knowledge objects to the workspace. Throws on non-2xx. When `body.dry_run`
|
|
274
|
+
* is set the result may carry `operations`/`summary` instead of `imported`.
|
|
275
|
+
*/
|
|
276
|
+
export async function pushKnowledge(url, accessToken, verboseFetch, verbose, body) {
|
|
277
|
+
const response = await verboseFetch(url, {
|
|
278
|
+
body: JSON.stringify(body),
|
|
279
|
+
headers: {
|
|
280
|
+
accept: 'application/json',
|
|
281
|
+
Authorization: `Bearer ${accessToken}`,
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
},
|
|
284
|
+
method: 'POST',
|
|
285
|
+
}, verbose, accessToken);
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
const errorText = await response.text();
|
|
288
|
+
let msg = `Knowledge push failed (${response.status})`;
|
|
289
|
+
try {
|
|
290
|
+
const json = JSON.parse(errorText);
|
|
291
|
+
if (json.message)
|
|
292
|
+
msg += `: ${json.message}`;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
msg += `\n${errorText}`;
|
|
296
|
+
}
|
|
297
|
+
throw new Error(msg);
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
return (await response.json());
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return {};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// ── Preview (client-side diff fallback) ─────────────────────────────────────────
|
|
307
|
+
/** Canonical form for change detection: content + description + metadata + references. */
|
|
308
|
+
function canonical(obj) {
|
|
309
|
+
const files = (obj.files ?? [])
|
|
310
|
+
.map((f) => ({ content: f.content, name: f.name }))
|
|
311
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
312
|
+
return JSON.stringify({
|
|
313
|
+
content: obj.content,
|
|
314
|
+
description: obj.description ?? '',
|
|
315
|
+
enabled: obj.enabled ?? null,
|
|
316
|
+
files,
|
|
317
|
+
mode: obj.mode ?? '',
|
|
318
|
+
scope: obj.scope ?? '',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Compute a dry-run-style preview by diffing local objects against server
|
|
323
|
+
* objects (match by `guid`, then `name`). Used when the server doesn't honor
|
|
324
|
+
* the `dry_run` flag.
|
|
325
|
+
*/
|
|
326
|
+
export function knowledgePreview(local, server, willDelete) {
|
|
327
|
+
const operations = [];
|
|
328
|
+
const summary = {};
|
|
329
|
+
const ensure = (type) => {
|
|
330
|
+
summary[type] ??= { created: 0, deleted: 0, unchanged: 0, updated: 0 };
|
|
331
|
+
};
|
|
332
|
+
const serverByGuid = new Map();
|
|
333
|
+
const serverByName = new Map();
|
|
334
|
+
for (const s of server) {
|
|
335
|
+
if (s.guid)
|
|
336
|
+
serverByGuid.set(s.guid, s);
|
|
337
|
+
serverByName.set(s.name, s);
|
|
338
|
+
}
|
|
339
|
+
const matched = new Set();
|
|
340
|
+
for (const obj of local) {
|
|
341
|
+
const type = obj.knowledge_type;
|
|
342
|
+
ensure(type);
|
|
343
|
+
const match = (obj.guid ? serverByGuid.get(obj.guid) : undefined) ?? serverByName.get(obj.name);
|
|
344
|
+
if (match) {
|
|
345
|
+
matched.add(match);
|
|
346
|
+
if (canonical(match) === canonical(obj)) {
|
|
347
|
+
summary[type].unchanged++;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
operations.push({ action: 'update', name: obj.name, type });
|
|
351
|
+
summary[type].updated++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
operations.push({ action: 'create', name: obj.name, type });
|
|
356
|
+
summary[type].created++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (willDelete) {
|
|
360
|
+
for (const s of server) {
|
|
361
|
+
if (!matched.has(s)) {
|
|
362
|
+
const type = s.knowledge_type;
|
|
363
|
+
ensure(type);
|
|
364
|
+
operations.push({ action: 'delete', name: s.name, type });
|
|
365
|
+
summary[type].deleted++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return { operations, summary };
|
|
370
|
+
}
|
|
371
|
+
// ── GUID writeback ──────────────────────────────────────────────────────────────
|
|
372
|
+
/**
|
|
373
|
+
* Set or update `guid:` in a primary `.md` file's YAML frontmatter.
|
|
374
|
+
* Returns true if the file was modified.
|
|
375
|
+
*/
|
|
376
|
+
export function syncGuidToFrontmatter(filePath, guid) {
|
|
377
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
378
|
+
const lines = content.split('\n');
|
|
379
|
+
if (lines[0]?.trim() !== '---')
|
|
380
|
+
return false;
|
|
381
|
+
let close = -1;
|
|
382
|
+
for (let i = 1; i < lines.length; i++) {
|
|
383
|
+
if (lines[i].trim() === '---') {
|
|
384
|
+
close = i;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (close === -1)
|
|
389
|
+
return false;
|
|
390
|
+
for (let i = 1; i < close; i++) {
|
|
391
|
+
const match = lines[i].match(/^guid\s*:\s*(.+)$/);
|
|
392
|
+
if (match) {
|
|
393
|
+
const existing = match[1].trim().replaceAll(/^["']|["']$/g, '');
|
|
394
|
+
if (existing === guid)
|
|
395
|
+
return false;
|
|
396
|
+
lines[i] = `guid: ${guid}`;
|
|
397
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
lines.splice(close, 0, `guid: ${guid}`);
|
|
402
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
@@ -36,11 +36,19 @@ export interface PushTarget {
|
|
|
36
36
|
*/
|
|
37
37
|
warnOnWorkspaceMismatch?: boolean;
|
|
38
38
|
}
|
|
39
|
+
export interface KnowledgeConfig {
|
|
40
|
+
/** Build the knowledge list URL (no query params). */
|
|
41
|
+
listUrl: () => string;
|
|
42
|
+
/** Root directory containing the `knowledge/` folder. */
|
|
43
|
+
rootDir: string;
|
|
44
|
+
}
|
|
39
45
|
export interface PushContext {
|
|
40
46
|
accessToken: string;
|
|
41
47
|
branch: string;
|
|
42
48
|
command: Command;
|
|
43
49
|
inputDir: string;
|
|
50
|
+
/** Optional knowledge sync config. Only workspace push sets this. */
|
|
51
|
+
knowledge?: KnowledgeConfig;
|
|
44
52
|
verboseFetch: (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
|
|
45
53
|
}
|
|
46
54
|
export declare const WORKSPACE_MISMATCH_THRESHOLD = 10;
|
|
@@ -107,3 +115,17 @@ export declare function confirm(message: string): Promise<boolean>;
|
|
|
107
115
|
* Shared by both sandbox:push and workspace:push commands.
|
|
108
116
|
*/
|
|
109
117
|
export declare function executePush(ctx: PushContext, target: PushTarget, flags: PushFlags): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Turn a thrown fetch/network error into an actionable message.
|
|
120
|
+
*
|
|
121
|
+
* Node's native fetch throws a TypeError with the unhelpful message "fetch
|
|
122
|
+
* failed" for all transport-level failures (DNS, connection refused, TLS,
|
|
123
|
+
* resets, timeouts). The real reason lives in `error.cause` as a system error
|
|
124
|
+
* with a `code` (ECONNREFUSED, ENOTFOUND, ETIMEDOUT, etc.). This unwraps it so
|
|
125
|
+
* the user sees what actually went wrong and where.
|
|
126
|
+
*
|
|
127
|
+
* `elapsedMs` is appended so the user can see how long the request ran before
|
|
128
|
+
* failing — a failure landing near a round boundary (e.g. ~300s) is a strong
|
|
129
|
+
* signal of a server-side or proxy timeout rather than a local network blip.
|
|
130
|
+
*/
|
|
131
|
+
export declare function describeNetworkError(error: unknown, url: string, elapsedMs?: number): string;
|