@xano/cli 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -4
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +45 -1
- package/dist/commands/auth/index.js +197 -29
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -0
- package/dist/commands/knowledge/{pull → get}/index.d.ts +6 -2
- package/dist/commands/knowledge/get/index.js +126 -0
- package/dist/commands/knowledge/{push → list}/index.d.ts +6 -8
- package/dist/commands/knowledge/list/index.js +135 -0
- 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 +19 -14
- package/dist/commands/sandbox/reset/index.js +12 -0
- package/dist/commands/static_host/build/create/index.d.ts +9 -1
- package/dist/commands/static_host/build/create/index.js +54 -4
- package/dist/commands/static_host/build/delete/index.d.ts +19 -0
- package/dist/commands/static_host/build/delete/index.js +114 -0
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +16 -10
- package/dist/commands/static_host/build/pull/index.d.ts +52 -0
- package/dist/commands/static_host/build/pull/index.js +300 -0
- package/dist/commands/static_host/build/push/index.d.ts +23 -0
- package/dist/commands/static_host/build/push/index.js +228 -0
- package/dist/commands/static_host/create/index.d.ts +17 -0
- package/dist/commands/static_host/create/index.js +86 -0
- package/dist/commands/static_host/deploy/index.d.ts +18 -0
- package/dist/commands/static_host/deploy/index.js +105 -0
- package/dist/commands/static_host/edit/index.d.ts +23 -0
- package/dist/commands/static_host/edit/index.js +151 -0
- package/dist/commands/static_host/get/index.d.ts +18 -0
- package/dist/commands/static_host/get/index.js +94 -0
- package/dist/commands/static_host/migrate/index.d.ts +44 -0
- package/dist/commands/static_host/migrate/index.js +205 -0
- package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
- package/dist/commands/tenant/snapshot/create/index.js +78 -0
- package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/delete/index.js +102 -0
- package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
- package/dist/commands/tenant/snapshot/list/index.js +96 -0
- package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/swap/index.js +103 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +7 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +8 -0
- package/dist/commands/unit_test/run_all/index.js +7 -3
- 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/function-input.d.ts +77 -0
- package/dist/utils/function-input.js +148 -0
- package/dist/utils/knowledge-sync.d.ts +84 -79
- package/dist/utils/knowledge-sync.js +376 -323
- package/dist/utils/multidoc-push.d.ts +30 -18
- package/dist/utils/multidoc-push.js +284 -148
- package/dist/utils/reference-checker.js +2 -2
- package/dist/utils/static-host-files.d.ts +21 -0
- package/dist/utils/static-host-files.js +41 -0
- package/oclif.manifest.json +4574 -3045
- package/package.json +7 -1
- package/dist/commands/knowledge/pull/index.js +0 -86
- package/dist/commands/knowledge/push/index.js +0 -126
|
@@ -25,35 +25,37 @@ export interface PushTarget {
|
|
|
25
25
|
instanceOrigin: string;
|
|
26
26
|
/** Human-readable label for log messages (e.g., "sandbox environment", "workspace 40") */
|
|
27
27
|
label: string;
|
|
28
|
+
/**
|
|
29
|
+
* The id of the source workspace being pushed (from the active profile). Sent to the sandbox
|
|
30
|
+
* so the backend can flag a mismatch when it differs from the workspace the sandbox last held.
|
|
31
|
+
*/
|
|
32
|
+
sourceWorkspaceId?: string;
|
|
28
33
|
/** Does this target support branches? */
|
|
29
34
|
supportsBranches: boolean;
|
|
30
35
|
/** Does this target support the partial query param? */
|
|
31
36
|
supportsPartial: boolean;
|
|
32
37
|
/**
|
|
33
|
-
* Warn when the
|
|
34
|
-
*
|
|
35
|
-
* across workspaces and pushing onto a different
|
|
38
|
+
* Warn when the sandbox currently holds a different source workspace than the one being pushed
|
|
39
|
+
* (per the dry-run's `source_workspace_mismatch` flag). Used by sandbox push because the sandbox
|
|
40
|
+
* is shared across workspaces and pushing onto a different one can leave stale state behind.
|
|
36
41
|
*/
|
|
37
42
|
warnOnWorkspaceMismatch?: boolean;
|
|
38
43
|
}
|
|
44
|
+
export interface KnowledgeConfig {
|
|
45
|
+
/** Build the knowledge list URL (no query params). */
|
|
46
|
+
listUrl: () => string;
|
|
47
|
+
/** Root directory containing the `knowledge/` folder. */
|
|
48
|
+
rootDir: string;
|
|
49
|
+
}
|
|
39
50
|
export interface PushContext {
|
|
40
51
|
accessToken: string;
|
|
41
52
|
branch: string;
|
|
42
53
|
command: Command;
|
|
43
54
|
inputDir: string;
|
|
55
|
+
/** Optional knowledge sync config. Only workspace push sets this. */
|
|
56
|
+
knowledge?: KnowledgeConfig;
|
|
44
57
|
verboseFetch: (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
|
|
45
58
|
}
|
|
46
|
-
export declare const WORKSPACE_MISMATCH_THRESHOLD = 10;
|
|
47
|
-
/**
|
|
48
|
-
* Sum all impactful operations in a dry-run summary. `deleted` is only counted when the
|
|
49
|
-
* caller actually intends to apply deletions (sync mode), matching what the user will see.
|
|
50
|
-
*/
|
|
51
|
-
export declare function countSummaryChanges(summary: Record<string, {
|
|
52
|
-
created: number;
|
|
53
|
-
deleted: number;
|
|
54
|
-
truncated: number;
|
|
55
|
-
updated: number;
|
|
56
|
-
}>, shouldDelete: boolean): number;
|
|
57
59
|
/**
|
|
58
60
|
* Filter document entries down to the ones the dry-run preview reports as changed,
|
|
59
61
|
* used to send only the changed documents during a partial (non-`--sync`) push.
|
|
@@ -97,13 +99,23 @@ export declare function readDocuments(files: string[]): Array<{
|
|
|
97
99
|
}>;
|
|
98
100
|
export declare function renderBadReferences(badRefs: BadReference[], log: (msg: string) => void): void;
|
|
99
101
|
export declare function renderBadIndexes(badIndexes: BadIndex[], log: (msg: string) => void): void;
|
|
100
|
-
export declare function findLocalWorkspaceName(entries: Array<{
|
|
101
|
-
content: string;
|
|
102
|
-
filePath: string;
|
|
103
|
-
}>): null | string;
|
|
104
102
|
export declare function confirm(message: string): Promise<boolean>;
|
|
105
103
|
/**
|
|
106
104
|
* Execute a multidoc push with preview, validation, partial mode, and GUID sync.
|
|
107
105
|
* Shared by both sandbox:push and workspace:push commands.
|
|
108
106
|
*/
|
|
109
107
|
export declare function executePush(ctx: PushContext, target: PushTarget, flags: PushFlags): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Turn a thrown fetch/network error into an actionable message.
|
|
110
|
+
*
|
|
111
|
+
* Node's native fetch throws a TypeError with the unhelpful message "fetch
|
|
112
|
+
* failed" for all transport-level failures (DNS, connection refused, TLS,
|
|
113
|
+
* resets, timeouts). The real reason lives in `error.cause` as a system error
|
|
114
|
+
* with a `code` (ECONNREFUSED, ENOTFOUND, ETIMEDOUT, etc.). This unwraps it so
|
|
115
|
+
* the user sees what actually went wrong and where.
|
|
116
|
+
*
|
|
117
|
+
* `elapsedMs` is appended so the user can see how long the request ran before
|
|
118
|
+
* failing — a failure landing near a round boundary (e.g. ~300s) is a strong
|
|
119
|
+
* signal of a server-side or proxy timeout rather than a local network blip.
|
|
120
|
+
*/
|
|
121
|
+
export declare function describeNetworkError(error: unknown, url: string, elapsedMs?: number): string;
|
|
@@ -3,17 +3,8 @@ import { minimatch } from 'minimatch';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { buildDocumentKey, findFilesWithGuid, parseDocument } from './document-parser.js';
|
|
6
|
+
import { collectKnowledgeObjects, fetchKnowledge, knowledgePreview, pushKnowledge, syncGuidToFrontmatter, toPushItems, } from './knowledge-sync.js';
|
|
6
7
|
import { checkReferences, checkTableIndexes } from './reference-checker.js';
|
|
7
|
-
// Minimum total operations before a workspace mismatch is treated as worth interrupting for.
|
|
8
|
-
// Small change sets (e.g., editing a single function) aren't worth a reset prompt.
|
|
9
|
-
export const WORKSPACE_MISMATCH_THRESHOLD = 10;
|
|
10
|
-
/**
|
|
11
|
-
* Sum all impactful operations in a dry-run summary. `deleted` is only counted when the
|
|
12
|
-
* caller actually intends to apply deletions (sync mode), matching what the user will see.
|
|
13
|
-
*/
|
|
14
|
-
export function countSummaryChanges(summary, shouldDelete) {
|
|
15
|
-
return Object.values(summary).reduce((sum, c) => sum + c.created + c.updated + (shouldDelete ? c.deleted : 0) + c.truncated, 0);
|
|
16
|
-
}
|
|
17
8
|
/**
|
|
18
9
|
* Filter document entries down to the ones the dry-run preview reports as changed,
|
|
19
10
|
* used to send only the changed documents during a partial (non-`--sync`) push.
|
|
@@ -140,12 +131,15 @@ export function renderBadIndexes(badIndexes, log) {
|
|
|
140
131
|
const TYPE_LABELS = {
|
|
141
132
|
addon: 'Addons',
|
|
142
133
|
agent: 'Agents',
|
|
134
|
+
'agents.md': 'Knowledge: agents.md',
|
|
143
135
|
api_group: 'API Groups',
|
|
136
|
+
doc: 'Knowledge: Docs',
|
|
144
137
|
function: 'Functions',
|
|
145
138
|
mcp_server: 'MCP Servers',
|
|
146
139
|
middleware: 'Middleware',
|
|
147
140
|
query: 'API Endpoints',
|
|
148
141
|
realtime_channel: 'Realtime Channels',
|
|
142
|
+
skill: 'Knowledge: Skills',
|
|
149
143
|
table: 'Tables',
|
|
150
144
|
task: 'Tasks',
|
|
151
145
|
tool: 'Tools',
|
|
@@ -259,14 +253,6 @@ function renderPreview(result, willDelete, target, verbose, partial, log) {
|
|
|
259
253
|
}
|
|
260
254
|
log('');
|
|
261
255
|
}
|
|
262
|
-
export function findLocalWorkspaceName(entries) {
|
|
263
|
-
for (const entry of entries) {
|
|
264
|
-
const parsed = parseDocument(entry.content);
|
|
265
|
-
if (parsed?.type === 'workspace')
|
|
266
|
-
return parsed.name;
|
|
267
|
-
}
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
256
|
// ── Confirmation ────────────────────────────────────────────────────────────
|
|
271
257
|
export async function confirm(message) {
|
|
272
258
|
const readline = await import('node:readline');
|
|
@@ -329,6 +315,47 @@ function syncGuidToFile(filePath, guid) {
|
|
|
329
315
|
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
330
316
|
return true;
|
|
331
317
|
}
|
|
318
|
+
// ── Knowledge Preview Helpers ─────────────────────────────────────────────────
|
|
319
|
+
/**
|
|
320
|
+
* Compute a knowledge change preview: prefer the server's `dry_run` response,
|
|
321
|
+
* and fall back to a client-side diff (fetch current objects → compare) when the
|
|
322
|
+
* server doesn't support it — mirroring how multidoc tolerates a missing dry-run.
|
|
323
|
+
*/
|
|
324
|
+
async function computeKnowledgePreview(listUrl, objects, branch, shouldDelete, accessToken, verboseFetch, verbose) {
|
|
325
|
+
const items = toPushItems(objects);
|
|
326
|
+
try {
|
|
327
|
+
const serverResult = await pushKnowledge(listUrl, accessToken, verboseFetch, verbose, {
|
|
328
|
+
branch,
|
|
329
|
+
delete: shouldDelete,
|
|
330
|
+
// eslint-disable-next-line camelcase -- external Metadata API field name
|
|
331
|
+
dry_run: true,
|
|
332
|
+
items,
|
|
333
|
+
});
|
|
334
|
+
if (serverResult.operations && serverResult.summary) {
|
|
335
|
+
return { operations: serverResult.operations, summary: serverResult.summary };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Server doesn't support dry_run yet; fall through to local diff.
|
|
340
|
+
}
|
|
341
|
+
const serverObjects = await fetchKnowledge(listUrl, branch, accessToken, verboseFetch, verbose);
|
|
342
|
+
return knowledgePreview(items, serverObjects, shouldDelete);
|
|
343
|
+
}
|
|
344
|
+
/** Fold a knowledge preview's summary + operations into the multidoc DryRunResult. */
|
|
345
|
+
function mergeKnowledgePreview(preview, knowledge) {
|
|
346
|
+
for (const [type, counts] of Object.entries(knowledge.summary)) {
|
|
347
|
+
preview.summary[type] = {
|
|
348
|
+
created: counts.created,
|
|
349
|
+
deleted: counts.deleted,
|
|
350
|
+
truncated: 0,
|
|
351
|
+
unchanged: counts.unchanged,
|
|
352
|
+
updated: counts.updated,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
for (const op of knowledge.operations) {
|
|
356
|
+
preview.operations.push({ action: op.action, details: '', name: op.name, type: op.type });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
332
359
|
// ── Main Push Logic ─────────────────────────────────────────────────────────
|
|
333
360
|
/**
|
|
334
361
|
* Execute a multidoc push with preview, validation, partial mode, and GUID sync.
|
|
@@ -337,27 +364,37 @@ function syncGuidToFile(filePath, guid) {
|
|
|
337
364
|
export async function executePush(ctx, target, flags) {
|
|
338
365
|
const { accessToken, command, inputDir, verboseFetch } = ctx;
|
|
339
366
|
const log = command.log.bind(command);
|
|
340
|
-
// ── Collect
|
|
367
|
+
// ── Collect knowledge entries (before file check so knowledge-only push works) ─
|
|
368
|
+
let knowledgeObjects = [];
|
|
369
|
+
if (ctx.knowledge) {
|
|
370
|
+
knowledgeObjects = collectKnowledgeObjects(ctx.knowledge.rootDir, flags.include, flags.exclude);
|
|
371
|
+
}
|
|
372
|
+
// ── Collect and filter .xs files ──────────────────────────────────────
|
|
341
373
|
const allFiles = collectFiles(inputDir);
|
|
342
374
|
const files = applyFilters(allFiles, inputDir, flags.include, flags.exclude, log);
|
|
343
|
-
|
|
375
|
+
const knowledgeOnly = files.length === 0 && (knowledgeObjects.length > 0 || ctx.knowledge !== undefined);
|
|
376
|
+
if (files.length === 0 && !knowledgeOnly) {
|
|
344
377
|
command.error(flags.include || flags.exclude
|
|
345
378
|
? `No .xs files remain after ${[flags.include ? `include ${flags.include.join(', ')}` : '', flags.exclude ? `exclude ${flags.exclude.join(', ')}` : ''].filter(Boolean).join(' and ')} in ${inputDir}`
|
|
346
379
|
: `No .xs files found in ${inputDir}`);
|
|
347
380
|
}
|
|
348
381
|
// ── Read documents ────────────────────────────────────────────────────
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
command.error(`All .xs files in ${inputDir} are empty`);
|
|
352
|
-
}
|
|
353
|
-
let multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
354
|
-
// ── Build document key → file path map (for GUID writeback) ───────────
|
|
382
|
+
let documentEntries = [];
|
|
383
|
+
let multidoc = '';
|
|
355
384
|
const documentFileMap = new Map();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
385
|
+
if (!knowledgeOnly) {
|
|
386
|
+
documentEntries = readDocuments(files);
|
|
387
|
+
if (documentEntries.length === 0) {
|
|
388
|
+
command.error(`All .xs files in ${inputDir} are empty`);
|
|
389
|
+
}
|
|
390
|
+
multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
391
|
+
// ── Build document key → file path map (for GUID writeback) ─────────
|
|
392
|
+
for (const entry of documentEntries) {
|
|
393
|
+
const parsed = parseDocument(entry.content);
|
|
394
|
+
if (parsed) {
|
|
395
|
+
const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
|
|
396
|
+
documentFileMap.set(key, entry.filePath);
|
|
397
|
+
}
|
|
361
398
|
}
|
|
362
399
|
}
|
|
363
400
|
// ── Resolve push mode ─────────────────────────────────────────────────
|
|
@@ -380,6 +417,9 @@ export async function executePush(ctx, target, flags) {
|
|
|
380
417
|
if (target.supportsPartial) {
|
|
381
418
|
queryParams.set('partial', isPartial.toString());
|
|
382
419
|
}
|
|
420
|
+
if (target.sourceWorkspaceId) {
|
|
421
|
+
queryParams.set('source_workspace_id', target.sourceWorkspaceId);
|
|
422
|
+
}
|
|
383
423
|
// ── Request headers ───────────────────────────────────────────────────
|
|
384
424
|
const requestHeaders = {
|
|
385
425
|
accept: 'application/json',
|
|
@@ -388,7 +428,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
388
428
|
};
|
|
389
429
|
// ── Dry-run / Preview ─────────────────────────────────────────────────
|
|
390
430
|
let dryRunPreview = null;
|
|
391
|
-
const dryRunUrl = target.buildDryRunUrl(queryParams);
|
|
431
|
+
const dryRunUrl = knowledgeOnly ? null : target.buildDryRunUrl(queryParams);
|
|
392
432
|
if (dryRunUrl && (flags['dry-run'] || !flags.force)) {
|
|
393
433
|
const dryRunParams = new URLSearchParams(queryParams);
|
|
394
434
|
// Always request delete info in dry-run to show remote-only items
|
|
@@ -401,15 +441,16 @@ export async function executePush(ctx, target, flags) {
|
|
|
401
441
|
headers: requestHeaders,
|
|
402
442
|
method: 'POST',
|
|
403
443
|
}, flags.verbose, accessToken);
|
|
404
|
-
if (
|
|
405
|
-
await handleDryRunError(dryRunResponse, command, flags, target);
|
|
406
|
-
// If we get here, the user confirmed to proceed without preview
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
444
|
+
if (dryRunResponse.ok) {
|
|
409
445
|
const dryRunText = await dryRunResponse.text();
|
|
410
446
|
const preview = JSON.parse(dryRunText);
|
|
411
447
|
dryRunPreview = preview;
|
|
412
448
|
if (preview && preview.summary) {
|
|
449
|
+
// ── Merge knowledge preview into the combined DryRunResult ──────
|
|
450
|
+
if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
|
|
451
|
+
const knowledgeDryRun = await computeKnowledgePreview(ctx.knowledge.listUrl(), knowledgeObjects, ctx.branch, shouldDelete, accessToken, verboseFetch, flags.verbose);
|
|
452
|
+
mergeKnowledgePreview(preview, knowledgeDryRun);
|
|
453
|
+
}
|
|
413
454
|
renderPreview(preview, shouldDelete, target, flags.verbose, isPartial, log);
|
|
414
455
|
// Check for bad cross-references using dry-run operations to avoid false positives
|
|
415
456
|
const badRefs = checkReferences(documentEntries, preview.operations);
|
|
@@ -443,7 +484,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
443
484
|
}
|
|
444
485
|
log(ux.colorize('yellow', 'Proceeding anyway due to --force flag.'));
|
|
445
486
|
}
|
|
446
|
-
// Check for actual changes
|
|
487
|
+
// Check for actual changes (multidoc + knowledge combined)
|
|
447
488
|
const hasChanges = Object.values(preview.summary).some((c) => c.created > 0 || c.updated > 0 || (shouldDelete && c.deleted > 0) || c.truncated > 0);
|
|
448
489
|
// Detect local records
|
|
449
490
|
const tablesWithRecords = flags.records
|
|
@@ -474,49 +515,49 @@ export async function executePush(ctx, target, flags) {
|
|
|
474
515
|
if (flags['dry-run']) {
|
|
475
516
|
return;
|
|
476
517
|
}
|
|
477
|
-
// Warn when the sandbox currently holds a different workspace than the one being
|
|
478
|
-
// pushed
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
log('Push cancelled. Run `xano sandbox reset` then retry.');
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
command.error('Workspace mismatch detected in non-interactive mode. Run `xano sandbox reset` first to start clean.');
|
|
518
|
+
// Warn when the sandbox currently holds a different source workspace than the one being
|
|
519
|
+
// pushed. The backend compares the source workspace id we sent against the id stored on
|
|
520
|
+
// the sandbox from its last push, so this is reliable regardless of whether the push
|
|
521
|
+
// includes a workspace-settings document (unlike the old name comparison).
|
|
522
|
+
let mismatchConfirmed = false;
|
|
523
|
+
if (target.warnOnWorkspaceMismatch && preview.source_workspace_mismatch) {
|
|
524
|
+
log('');
|
|
525
|
+
log(ux.colorize('yellow', ux.colorize('bold', '=== Workspace Mismatch ===')));
|
|
526
|
+
log('');
|
|
527
|
+
log(ux.colorize('yellow', "This sandbox currently holds a different workspace than the one you're pushing."));
|
|
528
|
+
log(ux.colorize('yellow', 'Pushing on top of it can leave stale data behind. Run `xano sandbox reset` first to start clean.'));
|
|
529
|
+
log('');
|
|
530
|
+
if (process.stdin.isTTY) {
|
|
531
|
+
const proceed = await confirm('Continue with push anyway?');
|
|
532
|
+
if (!proceed) {
|
|
533
|
+
log('Push cancelled. Run `xano sandbox reset` then retry.');
|
|
534
|
+
return;
|
|
500
535
|
}
|
|
536
|
+
mismatchConfirmed = true;
|
|
501
537
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
|
|
505
|
-
op.action === 'truncate' ||
|
|
506
|
-
op.action === 'drop_field' ||
|
|
507
|
-
op.action === 'alter_field');
|
|
508
|
-
const message = hasDestructive
|
|
509
|
-
? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
|
|
510
|
-
: 'Proceed with push?';
|
|
511
|
-
if (process.stdin.isTTY) {
|
|
512
|
-
const confirmed = await confirm(message);
|
|
513
|
-
if (!confirmed) {
|
|
514
|
-
log('Push cancelled.');
|
|
515
|
-
return;
|
|
538
|
+
else {
|
|
539
|
+
command.error('Workspace mismatch detected in non-interactive mode. Run `xano sandbox reset` first to start clean.');
|
|
516
540
|
}
|
|
517
541
|
}
|
|
518
|
-
|
|
519
|
-
|
|
542
|
+
// Confirm with user (skip if workspace mismatch prompt already obtained confirmation)
|
|
543
|
+
if (!mismatchConfirmed) {
|
|
544
|
+
const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
|
|
545
|
+
op.action === 'truncate' ||
|
|
546
|
+
op.action === 'drop_field' ||
|
|
547
|
+
op.action === 'alter_field');
|
|
548
|
+
const message = hasDestructive
|
|
549
|
+
? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
|
|
550
|
+
: 'Proceed with push?';
|
|
551
|
+
if (process.stdin.isTTY) {
|
|
552
|
+
const confirmed = await confirm(message);
|
|
553
|
+
if (!confirmed) {
|
|
554
|
+
log('Push cancelled.');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
560
|
+
}
|
|
520
561
|
}
|
|
521
562
|
}
|
|
522
563
|
else {
|
|
@@ -527,6 +568,10 @@ export async function executePush(ctx, target, flags) {
|
|
|
527
568
|
await confirmOrAbort(command, log);
|
|
528
569
|
}
|
|
529
570
|
}
|
|
571
|
+
else {
|
|
572
|
+
await handleDryRunError(dryRunResponse, command, flags, target);
|
|
573
|
+
// If we get here, the user confirmed to proceed without preview
|
|
574
|
+
}
|
|
530
575
|
}
|
|
531
576
|
catch (error) {
|
|
532
577
|
// Ctrl+C or SIGINT
|
|
@@ -548,8 +593,39 @@ export async function executePush(ctx, target, flags) {
|
|
|
548
593
|
await confirmOrAbort(command, log);
|
|
549
594
|
}
|
|
550
595
|
}
|
|
596
|
+
else if (knowledgeOnly && (flags['dry-run'] || !flags.force)) {
|
|
597
|
+
// ── Knowledge-only dry-run / preview ──────────────────────────────────
|
|
598
|
+
const kPreview = await computeKnowledgePreview(ctx.knowledge.listUrl(), knowledgeObjects, ctx.branch, shouldDelete, accessToken, verboseFetch, flags.verbose);
|
|
599
|
+
const syntheticResult = {
|
|
600
|
+
operations: kPreview.operations.map((op) => ({ action: op.action, details: '', name: op.name, type: op.type })),
|
|
601
|
+
summary: Object.fromEntries(Object.entries(kPreview.summary).map(([type, counts]) => [
|
|
602
|
+
type,
|
|
603
|
+
{ created: counts.created, deleted: counts.deleted, truncated: 0, unchanged: counts.unchanged, updated: counts.updated },
|
|
604
|
+
])),
|
|
605
|
+
};
|
|
606
|
+
renderPreview(syntheticResult, shouldDelete, target, flags.verbose, true, log);
|
|
607
|
+
const hasChanges = Object.values(syntheticResult.summary).some((c) => c.created > 0 || c.updated > 0 || (shouldDelete && c.deleted > 0));
|
|
608
|
+
if (!hasChanges) {
|
|
609
|
+
log('');
|
|
610
|
+
log('No changes to push.');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (flags['dry-run']) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (process.stdin.isTTY) {
|
|
617
|
+
const confirmed = await confirm('Proceed with push?');
|
|
618
|
+
if (!confirmed) {
|
|
619
|
+
log('Push cancelled.');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
551
627
|
// ── Show bad references in force mode (preview mode shows them inline) ─
|
|
552
|
-
if (flags.force) {
|
|
628
|
+
if (flags.force && !knowledgeOnly) {
|
|
553
629
|
const badRefs = checkReferences(documentEntries);
|
|
554
630
|
if (badRefs.length > 0) {
|
|
555
631
|
log('');
|
|
@@ -557,96 +633,156 @@ export async function executePush(ctx, target, flags) {
|
|
|
557
633
|
}
|
|
558
634
|
}
|
|
559
635
|
// ── Partial push: filter to changed documents only ────────────────────
|
|
560
|
-
if (isPartial && dryRunPreview) {
|
|
636
|
+
if (!knowledgeOnly && isPartial && dryRunPreview) {
|
|
561
637
|
const filteredEntries = filterChangedEntries(documentEntries, dryRunPreview.operations, flags.records);
|
|
562
|
-
if (filteredEntries.length === 0) {
|
|
638
|
+
if (filteredEntries.length === 0 && knowledgeObjects.length === 0) {
|
|
563
639
|
log('No changes to push.');
|
|
564
640
|
return;
|
|
565
641
|
}
|
|
566
|
-
|
|
642
|
+
if (filteredEntries.length > 0) {
|
|
643
|
+
multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
multidoc = '';
|
|
647
|
+
}
|
|
567
648
|
}
|
|
568
649
|
// ── Execute the actual push ───────────────────────────────────────────
|
|
569
|
-
const apiUrl = target.buildPushUrl(queryParams);
|
|
570
650
|
const startTime = Date.now();
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
let guidMap = [];
|
|
583
|
-
if (responseText && responseText !== 'null') {
|
|
584
|
-
try {
|
|
585
|
-
const responseJson = JSON.parse(responseText);
|
|
586
|
-
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
587
|
-
guidMap = responseJson.guid_map;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
catch {
|
|
591
|
-
if (flags.verbose) {
|
|
592
|
-
log('Server response is not JSON; skipping GUID sync');
|
|
593
|
-
}
|
|
651
|
+
let pushedDocCount = 0;
|
|
652
|
+
if (!knowledgeOnly && multidoc) {
|
|
653
|
+
const apiUrl = target.buildPushUrl(queryParams);
|
|
654
|
+
try {
|
|
655
|
+
const response = await verboseFetch(apiUrl, {
|
|
656
|
+
body: multidoc,
|
|
657
|
+
headers: requestHeaders,
|
|
658
|
+
method: 'POST',
|
|
659
|
+
}, flags.verbose, accessToken);
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
handlePushError(response, await response.text(), documentEntries, inputDir, command);
|
|
594
662
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
663
|
+
// Parse response for GUID map
|
|
664
|
+
const responseText = await response.text();
|
|
665
|
+
let guidMap = [];
|
|
666
|
+
if (responseText && responseText !== 'null') {
|
|
667
|
+
try {
|
|
668
|
+
const responseJson = JSON.parse(responseText);
|
|
669
|
+
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
670
|
+
guidMap = responseJson.guid_map;
|
|
671
|
+
}
|
|
603
672
|
}
|
|
604
|
-
|
|
605
|
-
|
|
673
|
+
catch {
|
|
674
|
+
if (flags.verbose) {
|
|
675
|
+
log('Server response is not JSON; skipping GUID sync');
|
|
676
|
+
}
|
|
606
677
|
}
|
|
607
678
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const baseKey = `${entry.type}:${entry.name}`;
|
|
616
|
-
const basePath = baseKeyMap.get(baseKey);
|
|
617
|
-
if (basePath) {
|
|
618
|
-
filePath = basePath;
|
|
679
|
+
// Write GUIDs back to local files
|
|
680
|
+
if (flags.guids && guidMap.length > 0) {
|
|
681
|
+
const baseKeyMap = new Map();
|
|
682
|
+
for (const [key, fp] of documentFileMap) {
|
|
683
|
+
const baseKey = key.split(':').slice(0, 2).join(':');
|
|
684
|
+
if (baseKeyMap.has(baseKey)) {
|
|
685
|
+
baseKeyMap.set(baseKey, ''); // Mark as ambiguous
|
|
619
686
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (flags.verbose) {
|
|
623
|
-
log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
687
|
+
else {
|
|
688
|
+
baseKeyMap.set(baseKey, fp);
|
|
624
689
|
}
|
|
625
|
-
continue;
|
|
626
690
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
|
|
691
|
+
let updatedCount = 0;
|
|
692
|
+
for (const entry of guidMap) {
|
|
693
|
+
if (!entry.guid)
|
|
694
|
+
continue;
|
|
695
|
+
const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
|
|
696
|
+
let filePath = documentFileMap.get(key);
|
|
697
|
+
if (!filePath) {
|
|
698
|
+
const baseKey = `${entry.type}:${entry.name}`;
|
|
699
|
+
const basePath = baseKeyMap.get(baseKey);
|
|
700
|
+
if (basePath) {
|
|
701
|
+
filePath = basePath;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!filePath) {
|
|
705
|
+
if (flags.verbose) {
|
|
706
|
+
log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
707
|
+
}
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
const updated = syncGuidToFile(filePath, entry.guid);
|
|
712
|
+
if (updated)
|
|
713
|
+
updatedCount++;
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
command.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
|
|
717
|
+
}
|
|
631
718
|
}
|
|
632
|
-
|
|
633
|
-
|
|
719
|
+
if (updatedCount > 0) {
|
|
720
|
+
log(`Synced ${updatedCount} GUIDs to local files`);
|
|
634
721
|
}
|
|
635
722
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
723
|
+
pushedDocCount = multidoc.split('\n---\n').length;
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
if (error instanceof Error && 'oclif' in error)
|
|
727
|
+
throw error;
|
|
728
|
+
const elapsedMs = Date.now() - startTime;
|
|
729
|
+
command.error(`Failed to push multidoc: ${describeNetworkError(error, apiUrl, elapsedMs)}`);
|
|
639
730
|
}
|
|
640
731
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
732
|
+
// ── Push knowledge ────────────────────────────────────────────────────
|
|
733
|
+
let knowledgeImported = 0;
|
|
734
|
+
let knowledgeDeleted = 0;
|
|
735
|
+
if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
|
|
736
|
+
const listUrl = ctx.knowledge.listUrl();
|
|
737
|
+
try {
|
|
738
|
+
const result = await pushKnowledge(listUrl, accessToken, verboseFetch, flags.verbose, {
|
|
739
|
+
branch: ctx.branch,
|
|
740
|
+
delete: shouldDelete,
|
|
741
|
+
force: false,
|
|
742
|
+
items: toPushItems(knowledgeObjects),
|
|
743
|
+
});
|
|
744
|
+
knowledgeImported = result.imported ?? 0;
|
|
745
|
+
knowledgeDeleted = result.deleted ?? 0;
|
|
746
|
+
// Write GUIDs back into local frontmatter, matching server entries by name.
|
|
747
|
+
if (flags.guids && result.guid_map && result.guid_map.length > 0) {
|
|
748
|
+
const fileByName = new Map(knowledgeObjects.map((o) => [o.name, o.filePath]));
|
|
749
|
+
let kGuidCount = 0;
|
|
750
|
+
for (const entry of result.guid_map) {
|
|
751
|
+
const filePath = entry.guid && entry.name ? fileByName.get(entry.name) : undefined;
|
|
752
|
+
if (!filePath)
|
|
753
|
+
continue;
|
|
754
|
+
try {
|
|
755
|
+
const updated = syncGuidToFrontmatter(filePath, entry.guid);
|
|
756
|
+
if (updated)
|
|
757
|
+
kGuidCount++;
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
command.warn(`Failed to sync knowledge GUID to ${filePath}: ${error.message}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (kGuidCount > 0) {
|
|
764
|
+
log(`Synced ${kGuidCount} knowledge GUIDs to local files`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (error instanceof Error && 'oclif' in error)
|
|
770
|
+
throw error;
|
|
771
|
+
const elapsedMs = Date.now() - startTime;
|
|
772
|
+
command.error(`Failed to push knowledge: ${describeNetworkError(error, listUrl, elapsedMs)}`);
|
|
773
|
+
}
|
|
646
774
|
}
|
|
647
775
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
648
|
-
const
|
|
649
|
-
|
|
776
|
+
const parts = [];
|
|
777
|
+
if (!knowledgeOnly)
|
|
778
|
+
parts.push(`${pushedDocCount} documents`);
|
|
779
|
+
if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
|
|
780
|
+
const kParts = [`${knowledgeImported} knowledge file${knowledgeImported === 1 ? '' : 's'}`];
|
|
781
|
+
if (shouldDelete && knowledgeDeleted > 0)
|
|
782
|
+
kParts.push(`${knowledgeDeleted} deleted`);
|
|
783
|
+
parts.push(kParts.join(', '));
|
|
784
|
+
}
|
|
785
|
+
log(`Pushed ${parts.join(' + ')} to ${target.label} from ${relative(process.cwd(), inputDir) || inputDir} in ${elapsed}s`);
|
|
650
786
|
}
|
|
651
787
|
// ── Error Handlers ──────────────────────────────────────────────────────────
|
|
652
788
|
/**
|
|
@@ -662,7 +798,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
662
798
|
* failing — a failure landing near a round boundary (e.g. ~300s) is a strong
|
|
663
799
|
* signal of a server-side or proxy timeout rather than a local network blip.
|
|
664
800
|
*/
|
|
665
|
-
function describeNetworkError(error, url, elapsedMs) {
|
|
801
|
+
export function describeNetworkError(error, url, elapsedMs) {
|
|
666
802
|
if (!(error instanceof Error))
|
|
667
803
|
return String(error);
|
|
668
804
|
let host = url;
|