@xano/cli 1.0.3 → 1.0.4-beta.10
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 +100 -3
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +48 -1
- package/dist/commands/auth/index.js +259 -27
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -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 +17 -13
- 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 +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 +262 -107
- 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 +4179 -2636
- package/package.json +4 -1
|
@@ -3,6 +3,7 @@ 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
8
|
// Minimum total operations before a workspace mismatch is treated as worth interrupting for.
|
|
8
9
|
// Small change sets (e.g., editing a single function) aren't worth a reset prompt.
|
|
@@ -140,12 +141,15 @@ export function renderBadIndexes(badIndexes, log) {
|
|
|
140
141
|
const TYPE_LABELS = {
|
|
141
142
|
addon: 'Addons',
|
|
142
143
|
agent: 'Agents',
|
|
144
|
+
'agents.md': 'Knowledge: agents.md',
|
|
143
145
|
api_group: 'API Groups',
|
|
146
|
+
doc: 'Knowledge: Docs',
|
|
144
147
|
function: 'Functions',
|
|
145
148
|
mcp_server: 'MCP Servers',
|
|
146
149
|
middleware: 'Middleware',
|
|
147
150
|
query: 'API Endpoints',
|
|
148
151
|
realtime_channel: 'Realtime Channels',
|
|
152
|
+
skill: 'Knowledge: Skills',
|
|
149
153
|
table: 'Tables',
|
|
150
154
|
task: 'Tasks',
|
|
151
155
|
tool: 'Tools',
|
|
@@ -329,6 +333,47 @@ function syncGuidToFile(filePath, guid) {
|
|
|
329
333
|
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
330
334
|
return true;
|
|
331
335
|
}
|
|
336
|
+
// ── Knowledge Preview Helpers ─────────────────────────────────────────────────
|
|
337
|
+
/**
|
|
338
|
+
* Compute a knowledge change preview: prefer the server's `dry_run` response,
|
|
339
|
+
* and fall back to a client-side diff (fetch current objects → compare) when the
|
|
340
|
+
* server doesn't support it — mirroring how multidoc tolerates a missing dry-run.
|
|
341
|
+
*/
|
|
342
|
+
async function computeKnowledgePreview(listUrl, objects, branch, shouldDelete, accessToken, verboseFetch, verbose) {
|
|
343
|
+
const items = toPushItems(objects);
|
|
344
|
+
try {
|
|
345
|
+
const serverResult = await pushKnowledge(listUrl, accessToken, verboseFetch, verbose, {
|
|
346
|
+
branch,
|
|
347
|
+
delete: shouldDelete,
|
|
348
|
+
// eslint-disable-next-line camelcase -- external Metadata API field name
|
|
349
|
+
dry_run: true,
|
|
350
|
+
items,
|
|
351
|
+
});
|
|
352
|
+
if (serverResult.operations && serverResult.summary) {
|
|
353
|
+
return { operations: serverResult.operations, summary: serverResult.summary };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// Server doesn't support dry_run yet; fall through to local diff.
|
|
358
|
+
}
|
|
359
|
+
const serverObjects = await fetchKnowledge(listUrl, branch, accessToken, verboseFetch, verbose);
|
|
360
|
+
return knowledgePreview(items, serverObjects, shouldDelete);
|
|
361
|
+
}
|
|
362
|
+
/** Fold a knowledge preview's summary + operations into the multidoc DryRunResult. */
|
|
363
|
+
function mergeKnowledgePreview(preview, knowledge) {
|
|
364
|
+
for (const [type, counts] of Object.entries(knowledge.summary)) {
|
|
365
|
+
preview.summary[type] = {
|
|
366
|
+
created: counts.created,
|
|
367
|
+
deleted: counts.deleted,
|
|
368
|
+
truncated: 0,
|
|
369
|
+
unchanged: counts.unchanged,
|
|
370
|
+
updated: counts.updated,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
for (const op of knowledge.operations) {
|
|
374
|
+
preview.operations.push({ action: op.action, details: '', name: op.name, type: op.type });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
332
377
|
// ── Main Push Logic ─────────────────────────────────────────────────────────
|
|
333
378
|
/**
|
|
334
379
|
* Execute a multidoc push with preview, validation, partial mode, and GUID sync.
|
|
@@ -337,27 +382,37 @@ function syncGuidToFile(filePath, guid) {
|
|
|
337
382
|
export async function executePush(ctx, target, flags) {
|
|
338
383
|
const { accessToken, command, inputDir, verboseFetch } = ctx;
|
|
339
384
|
const log = command.log.bind(command);
|
|
340
|
-
// ── Collect
|
|
385
|
+
// ── Collect knowledge entries (before file check so knowledge-only push works) ─
|
|
386
|
+
let knowledgeObjects = [];
|
|
387
|
+
if (ctx.knowledge) {
|
|
388
|
+
knowledgeObjects = collectKnowledgeObjects(ctx.knowledge.rootDir, flags.include, flags.exclude);
|
|
389
|
+
}
|
|
390
|
+
// ── Collect and filter .xs files ──────────────────────────────────────
|
|
341
391
|
const allFiles = collectFiles(inputDir);
|
|
342
392
|
const files = applyFilters(allFiles, inputDir, flags.include, flags.exclude, log);
|
|
343
|
-
|
|
393
|
+
const knowledgeOnly = files.length === 0 && (knowledgeObjects.length > 0 || ctx.knowledge !== undefined);
|
|
394
|
+
if (files.length === 0 && !knowledgeOnly) {
|
|
344
395
|
command.error(flags.include || flags.exclude
|
|
345
396
|
? `No .xs files remain after ${[flags.include ? `include ${flags.include.join(', ')}` : '', flags.exclude ? `exclude ${flags.exclude.join(', ')}` : ''].filter(Boolean).join(' and ')} in ${inputDir}`
|
|
346
397
|
: `No .xs files found in ${inputDir}`);
|
|
347
398
|
}
|
|
348
399
|
// ── 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) ───────────
|
|
400
|
+
let documentEntries = [];
|
|
401
|
+
let multidoc = '';
|
|
355
402
|
const documentFileMap = new Map();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
403
|
+
if (!knowledgeOnly) {
|
|
404
|
+
documentEntries = readDocuments(files);
|
|
405
|
+
if (documentEntries.length === 0) {
|
|
406
|
+
command.error(`All .xs files in ${inputDir} are empty`);
|
|
407
|
+
}
|
|
408
|
+
multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
409
|
+
// ── Build document key → file path map (for GUID writeback) ─────────
|
|
410
|
+
for (const entry of documentEntries) {
|
|
411
|
+
const parsed = parseDocument(entry.content);
|
|
412
|
+
if (parsed) {
|
|
413
|
+
const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
|
|
414
|
+
documentFileMap.set(key, entry.filePath);
|
|
415
|
+
}
|
|
361
416
|
}
|
|
362
417
|
}
|
|
363
418
|
// ── Resolve push mode ─────────────────────────────────────────────────
|
|
@@ -388,7 +443,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
388
443
|
};
|
|
389
444
|
// ── Dry-run / Preview ─────────────────────────────────────────────────
|
|
390
445
|
let dryRunPreview = null;
|
|
391
|
-
const dryRunUrl = target.buildDryRunUrl(queryParams);
|
|
446
|
+
const dryRunUrl = knowledgeOnly ? null : target.buildDryRunUrl(queryParams);
|
|
392
447
|
if (dryRunUrl && (flags['dry-run'] || !flags.force)) {
|
|
393
448
|
const dryRunParams = new URLSearchParams(queryParams);
|
|
394
449
|
// Always request delete info in dry-run to show remote-only items
|
|
@@ -401,15 +456,16 @@ export async function executePush(ctx, target, flags) {
|
|
|
401
456
|
headers: requestHeaders,
|
|
402
457
|
method: 'POST',
|
|
403
458
|
}, 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 {
|
|
459
|
+
if (dryRunResponse.ok) {
|
|
409
460
|
const dryRunText = await dryRunResponse.text();
|
|
410
461
|
const preview = JSON.parse(dryRunText);
|
|
411
462
|
dryRunPreview = preview;
|
|
412
463
|
if (preview && preview.summary) {
|
|
464
|
+
// ── Merge knowledge preview into the combined DryRunResult ──────
|
|
465
|
+
if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
|
|
466
|
+
const knowledgeDryRun = await computeKnowledgePreview(ctx.knowledge.listUrl(), knowledgeObjects, ctx.branch, shouldDelete, accessToken, verboseFetch, flags.verbose);
|
|
467
|
+
mergeKnowledgePreview(preview, knowledgeDryRun);
|
|
468
|
+
}
|
|
413
469
|
renderPreview(preview, shouldDelete, target, flags.verbose, isPartial, log);
|
|
414
470
|
// Check for bad cross-references using dry-run operations to avoid false positives
|
|
415
471
|
const badRefs = checkReferences(documentEntries, preview.operations);
|
|
@@ -443,7 +499,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
443
499
|
}
|
|
444
500
|
log(ux.colorize('yellow', 'Proceeding anyway due to --force flag.'));
|
|
445
501
|
}
|
|
446
|
-
// Check for actual changes
|
|
502
|
+
// Check for actual changes (multidoc + knowledge combined)
|
|
447
503
|
const hasChanges = Object.values(preview.summary).some((c) => c.created > 0 || c.updated > 0 || (shouldDelete && c.deleted > 0) || c.truncated > 0);
|
|
448
504
|
// Detect local records
|
|
449
505
|
const tablesWithRecords = flags.records
|
|
@@ -476,6 +532,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
476
532
|
}
|
|
477
533
|
// Warn when the sandbox currently holds a different workspace than the one being
|
|
478
534
|
// pushed and the change set is large enough that stale state is a real risk.
|
|
535
|
+
let mismatchConfirmed = false;
|
|
479
536
|
if (target.warnOnWorkspaceMismatch && preview.workspace_name) {
|
|
480
537
|
const localWorkspaceName = findLocalWorkspaceName(documentEntries);
|
|
481
538
|
const totalChanges = countSummaryChanges(preview.summary, shouldDelete);
|
|
@@ -494,29 +551,32 @@ export async function executePush(ctx, target, flags) {
|
|
|
494
551
|
log('Push cancelled. Run `xano sandbox reset` then retry.');
|
|
495
552
|
return;
|
|
496
553
|
}
|
|
554
|
+
mismatchConfirmed = true;
|
|
497
555
|
}
|
|
498
556
|
else {
|
|
499
557
|
command.error('Workspace mismatch detected in non-interactive mode. Run `xano sandbox reset` first to start clean.');
|
|
500
558
|
}
|
|
501
559
|
}
|
|
502
560
|
}
|
|
503
|
-
// Confirm with user
|
|
504
|
-
|
|
505
|
-
op.action === '
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
561
|
+
// Confirm with user (skip if workspace mismatch prompt already obtained confirmation)
|
|
562
|
+
if (!mismatchConfirmed) {
|
|
563
|
+
const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
|
|
564
|
+
op.action === 'truncate' ||
|
|
565
|
+
op.action === 'drop_field' ||
|
|
566
|
+
op.action === 'alter_field');
|
|
567
|
+
const message = hasDestructive
|
|
568
|
+
? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
|
|
569
|
+
: 'Proceed with push?';
|
|
570
|
+
if (process.stdin.isTTY) {
|
|
571
|
+
const confirmed = await confirm(message);
|
|
572
|
+
if (!confirmed) {
|
|
573
|
+
log('Push cancelled.');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
516
579
|
}
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
520
580
|
}
|
|
521
581
|
}
|
|
522
582
|
else {
|
|
@@ -527,6 +587,10 @@ export async function executePush(ctx, target, flags) {
|
|
|
527
587
|
await confirmOrAbort(command, log);
|
|
528
588
|
}
|
|
529
589
|
}
|
|
590
|
+
else {
|
|
591
|
+
await handleDryRunError(dryRunResponse, command, flags, target);
|
|
592
|
+
// If we get here, the user confirmed to proceed without preview
|
|
593
|
+
}
|
|
530
594
|
}
|
|
531
595
|
catch (error) {
|
|
532
596
|
// Ctrl+C or SIGINT
|
|
@@ -548,8 +612,39 @@ export async function executePush(ctx, target, flags) {
|
|
|
548
612
|
await confirmOrAbort(command, log);
|
|
549
613
|
}
|
|
550
614
|
}
|
|
615
|
+
else if (knowledgeOnly && (flags['dry-run'] || !flags.force)) {
|
|
616
|
+
// ── Knowledge-only dry-run / preview ──────────────────────────────────
|
|
617
|
+
const kPreview = await computeKnowledgePreview(ctx.knowledge.listUrl(), knowledgeObjects, ctx.branch, shouldDelete, accessToken, verboseFetch, flags.verbose);
|
|
618
|
+
const syntheticResult = {
|
|
619
|
+
operations: kPreview.operations.map((op) => ({ action: op.action, details: '', name: op.name, type: op.type })),
|
|
620
|
+
summary: Object.fromEntries(Object.entries(kPreview.summary).map(([type, counts]) => [
|
|
621
|
+
type,
|
|
622
|
+
{ created: counts.created, deleted: counts.deleted, truncated: 0, unchanged: counts.unchanged, updated: counts.updated },
|
|
623
|
+
])),
|
|
624
|
+
};
|
|
625
|
+
renderPreview(syntheticResult, shouldDelete, target, flags.verbose, true, log);
|
|
626
|
+
const hasChanges = Object.values(syntheticResult.summary).some((c) => c.created > 0 || c.updated > 0 || (shouldDelete && c.deleted > 0));
|
|
627
|
+
if (!hasChanges) {
|
|
628
|
+
log('');
|
|
629
|
+
log('No changes to push.');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (flags['dry-run']) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (process.stdin.isTTY) {
|
|
636
|
+
const confirmed = await confirm('Proceed with push?');
|
|
637
|
+
if (!confirmed) {
|
|
638
|
+
log('Push cancelled.');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
command.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
551
646
|
// ── Show bad references in force mode (preview mode shows them inline) ─
|
|
552
|
-
if (flags.force) {
|
|
647
|
+
if (flags.force && !knowledgeOnly) {
|
|
553
648
|
const badRefs = checkReferences(documentEntries);
|
|
554
649
|
if (badRefs.length > 0) {
|
|
555
650
|
log('');
|
|
@@ -557,96 +652,156 @@ export async function executePush(ctx, target, flags) {
|
|
|
557
652
|
}
|
|
558
653
|
}
|
|
559
654
|
// ── Partial push: filter to changed documents only ────────────────────
|
|
560
|
-
if (isPartial && dryRunPreview) {
|
|
655
|
+
if (!knowledgeOnly && isPartial && dryRunPreview) {
|
|
561
656
|
const filteredEntries = filterChangedEntries(documentEntries, dryRunPreview.operations, flags.records);
|
|
562
|
-
if (filteredEntries.length === 0) {
|
|
657
|
+
if (filteredEntries.length === 0 && knowledgeObjects.length === 0) {
|
|
563
658
|
log('No changes to push.');
|
|
564
659
|
return;
|
|
565
660
|
}
|
|
566
|
-
|
|
661
|
+
if (filteredEntries.length > 0) {
|
|
662
|
+
multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
multidoc = '';
|
|
666
|
+
}
|
|
567
667
|
}
|
|
568
668
|
// ── Execute the actual push ───────────────────────────────────────────
|
|
569
|
-
const apiUrl = target.buildPushUrl(queryParams);
|
|
570
669
|
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
|
-
}
|
|
670
|
+
let pushedDocCount = 0;
|
|
671
|
+
if (!knowledgeOnly && multidoc) {
|
|
672
|
+
const apiUrl = target.buildPushUrl(queryParams);
|
|
673
|
+
try {
|
|
674
|
+
const response = await verboseFetch(apiUrl, {
|
|
675
|
+
body: multidoc,
|
|
676
|
+
headers: requestHeaders,
|
|
677
|
+
method: 'POST',
|
|
678
|
+
}, flags.verbose, accessToken);
|
|
679
|
+
if (!response.ok) {
|
|
680
|
+
handlePushError(response, await response.text(), documentEntries, inputDir, command);
|
|
594
681
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
682
|
+
// Parse response for GUID map
|
|
683
|
+
const responseText = await response.text();
|
|
684
|
+
let guidMap = [];
|
|
685
|
+
if (responseText && responseText !== 'null') {
|
|
686
|
+
try {
|
|
687
|
+
const responseJson = JSON.parse(responseText);
|
|
688
|
+
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
689
|
+
guidMap = responseJson.guid_map;
|
|
690
|
+
}
|
|
603
691
|
}
|
|
604
|
-
|
|
605
|
-
|
|
692
|
+
catch {
|
|
693
|
+
if (flags.verbose) {
|
|
694
|
+
log('Server response is not JSON; skipping GUID sync');
|
|
695
|
+
}
|
|
606
696
|
}
|
|
607
697
|
}
|
|
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;
|
|
698
|
+
// Write GUIDs back to local files
|
|
699
|
+
if (flags.guids && guidMap.length > 0) {
|
|
700
|
+
const baseKeyMap = new Map();
|
|
701
|
+
for (const [key, fp] of documentFileMap) {
|
|
702
|
+
const baseKey = key.split(':').slice(0, 2).join(':');
|
|
703
|
+
if (baseKeyMap.has(baseKey)) {
|
|
704
|
+
baseKeyMap.set(baseKey, ''); // Mark as ambiguous
|
|
619
705
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (flags.verbose) {
|
|
623
|
-
log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
706
|
+
else {
|
|
707
|
+
baseKeyMap.set(baseKey, fp);
|
|
624
708
|
}
|
|
625
|
-
continue;
|
|
626
709
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
|
|
710
|
+
let updatedCount = 0;
|
|
711
|
+
for (const entry of guidMap) {
|
|
712
|
+
if (!entry.guid)
|
|
713
|
+
continue;
|
|
714
|
+
const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
|
|
715
|
+
let filePath = documentFileMap.get(key);
|
|
716
|
+
if (!filePath) {
|
|
717
|
+
const baseKey = `${entry.type}:${entry.name}`;
|
|
718
|
+
const basePath = baseKeyMap.get(baseKey);
|
|
719
|
+
if (basePath) {
|
|
720
|
+
filePath = basePath;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (!filePath) {
|
|
724
|
+
if (flags.verbose) {
|
|
725
|
+
log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
726
|
+
}
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
const updated = syncGuidToFile(filePath, entry.guid);
|
|
731
|
+
if (updated)
|
|
732
|
+
updatedCount++;
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
command.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
|
|
736
|
+
}
|
|
631
737
|
}
|
|
632
|
-
|
|
633
|
-
|
|
738
|
+
if (updatedCount > 0) {
|
|
739
|
+
log(`Synced ${updatedCount} GUIDs to local files`);
|
|
634
740
|
}
|
|
635
741
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
742
|
+
pushedDocCount = multidoc.split('\n---\n').length;
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (error instanceof Error && 'oclif' in error)
|
|
746
|
+
throw error;
|
|
747
|
+
const elapsedMs = Date.now() - startTime;
|
|
748
|
+
command.error(`Failed to push multidoc: ${describeNetworkError(error, apiUrl, elapsedMs)}`);
|
|
639
749
|
}
|
|
640
750
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
751
|
+
// ── Push knowledge ────────────────────────────────────────────────────
|
|
752
|
+
let knowledgeImported = 0;
|
|
753
|
+
let knowledgeDeleted = 0;
|
|
754
|
+
if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
|
|
755
|
+
const listUrl = ctx.knowledge.listUrl();
|
|
756
|
+
try {
|
|
757
|
+
const result = await pushKnowledge(listUrl, accessToken, verboseFetch, flags.verbose, {
|
|
758
|
+
branch: ctx.branch,
|
|
759
|
+
delete: shouldDelete,
|
|
760
|
+
force: false,
|
|
761
|
+
items: toPushItems(knowledgeObjects),
|
|
762
|
+
});
|
|
763
|
+
knowledgeImported = result.imported ?? 0;
|
|
764
|
+
knowledgeDeleted = result.deleted ?? 0;
|
|
765
|
+
// Write GUIDs back into local frontmatter, matching server entries by name.
|
|
766
|
+
if (flags.guids && result.guid_map && result.guid_map.length > 0) {
|
|
767
|
+
const fileByName = new Map(knowledgeObjects.map((o) => [o.name, o.filePath]));
|
|
768
|
+
let kGuidCount = 0;
|
|
769
|
+
for (const entry of result.guid_map) {
|
|
770
|
+
const filePath = entry.guid && entry.name ? fileByName.get(entry.name) : undefined;
|
|
771
|
+
if (!filePath)
|
|
772
|
+
continue;
|
|
773
|
+
try {
|
|
774
|
+
const updated = syncGuidToFrontmatter(filePath, entry.guid);
|
|
775
|
+
if (updated)
|
|
776
|
+
kGuidCount++;
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
command.warn(`Failed to sync knowledge GUID to ${filePath}: ${error.message}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (kGuidCount > 0) {
|
|
783
|
+
log(`Synced ${kGuidCount} knowledge GUIDs to local files`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
if (error instanceof Error && 'oclif' in error)
|
|
789
|
+
throw error;
|
|
790
|
+
const elapsedMs = Date.now() - startTime;
|
|
791
|
+
command.error(`Failed to push knowledge: ${describeNetworkError(error, listUrl, elapsedMs)}`);
|
|
792
|
+
}
|
|
646
793
|
}
|
|
647
794
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
648
|
-
const
|
|
649
|
-
|
|
795
|
+
const parts = [];
|
|
796
|
+
if (!knowledgeOnly)
|
|
797
|
+
parts.push(`${pushedDocCount} documents`);
|
|
798
|
+
if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
|
|
799
|
+
const kParts = [`${knowledgeImported} knowledge file${knowledgeImported === 1 ? '' : 's'}`];
|
|
800
|
+
if (shouldDelete && knowledgeDeleted > 0)
|
|
801
|
+
kParts.push(`${knowledgeDeleted} deleted`);
|
|
802
|
+
parts.push(kParts.join(', '));
|
|
803
|
+
}
|
|
804
|
+
log(`Pushed ${parts.join(' + ')} to ${target.label} from ${relative(process.cwd(), inputDir) || inputDir} in ${elapsed}s`);
|
|
650
805
|
}
|
|
651
806
|
// ── Error Handlers ──────────────────────────────────────────────────────────
|
|
652
807
|
/**
|
|
@@ -662,7 +817,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
662
817
|
* failing — a failure landing near a round boundary (e.g. ~300s) is a strong
|
|
663
818
|
* signal of a server-side or proxy timeout rather than a local network blip.
|
|
664
819
|
*/
|
|
665
|
-
function describeNetworkError(error, url, elapsedMs) {
|
|
820
|
+
export function describeNetworkError(error, url, elapsedMs) {
|
|
666
821
|
if (!(error instanceof Error))
|
|
667
822
|
return String(error);
|
|
668
823
|
let host = url;
|
|
@@ -170,8 +170,8 @@ export function checkTableIndexes(documents) {
|
|
|
170
170
|
return badIndexes;
|
|
171
171
|
}
|
|
172
172
|
function extractSchemaFields(content) {
|
|
173
|
-
// id and
|
|
174
|
-
const fields = new Set(['id', 'created_at']);
|
|
173
|
+
// id, created_at, and xdo are system fields not declared in the schema
|
|
174
|
+
const fields = new Set(['id', 'created_at', 'xdo']);
|
|
175
175
|
// Find the schema block by matching braces
|
|
176
176
|
const schemaStart = content.match(/\bschema\s*\{/);
|
|
177
177
|
if (!schemaStart || schemaStart.index === undefined)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface CollectStaticHostFilesOptions {
|
|
2
|
+
/**
|
|
3
|
+
* When true, skip files matched by the source directory's `.gitignore`.
|
|
4
|
+
* The `.git/` folder is always excluded regardless of this setting.
|
|
5
|
+
*/
|
|
6
|
+
respectGitignore: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Collect the files to upload for a static-host build, as POSIX-relative paths
|
|
10
|
+
* (sorted, for a deterministic archive).
|
|
11
|
+
*
|
|
12
|
+
* Honours the `.gitignore` at the root of `sourceDir` when `respectGitignore` is
|
|
13
|
+
* set, and always excludes the `.git/` directory — it is repo metadata, never
|
|
14
|
+
* deployable output.
|
|
15
|
+
*
|
|
16
|
+
* Directory rules in `.gitignore` carry a trailing slash (e.g. `node_modules/`),
|
|
17
|
+
* and the `ignore` package only matches those when the tested path also ends in a
|
|
18
|
+
* slash — so directories are tested as `${rel}/` (which also lets us prune the
|
|
19
|
+
* whole subtree) while files are tested as-is.
|
|
20
|
+
*/
|
|
21
|
+
export declare function collectStaticHostFiles(sourceDir: string, options: CollectStaticHostFilesOptions): string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import ignore from 'ignore';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { join, relative, sep } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Collect the files to upload for a static-host build, as POSIX-relative paths
|
|
6
|
+
* (sorted, for a deterministic archive).
|
|
7
|
+
*
|
|
8
|
+
* Honours the `.gitignore` at the root of `sourceDir` when `respectGitignore` is
|
|
9
|
+
* set, and always excludes the `.git/` directory — it is repo metadata, never
|
|
10
|
+
* deployable output.
|
|
11
|
+
*
|
|
12
|
+
* Directory rules in `.gitignore` carry a trailing slash (e.g. `node_modules/`),
|
|
13
|
+
* and the `ignore` package only matches those when the tested path also ends in a
|
|
14
|
+
* slash — so directories are tested as `${rel}/` (which also lets us prune the
|
|
15
|
+
* whole subtree) while files are tested as-is.
|
|
16
|
+
*/
|
|
17
|
+
export function collectStaticHostFiles(sourceDir, options) {
|
|
18
|
+
const ig = ignore().add('.git');
|
|
19
|
+
if (options.respectGitignore) {
|
|
20
|
+
const gitignorePath = join(sourceDir, '.gitignore');
|
|
21
|
+
if (fs.existsSync(gitignorePath)) {
|
|
22
|
+
ig.add(fs.readFileSync(gitignorePath, 'utf8'));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const files = [];
|
|
26
|
+
const walk = (dir) => {
|
|
27
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
+
const abs = join(dir, entry.name);
|
|
29
|
+
const rel = relative(sourceDir, abs).split(sep).join('/');
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
if (!ig.ignores(`${rel}/`))
|
|
32
|
+
walk(abs);
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile() && !ig.ignores(rel)) {
|
|
35
|
+
files.push(rel);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
walk(sourceDir);
|
|
40
|
+
return files.sort();
|
|
41
|
+
}
|