@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.
Files changed (58) hide show
  1. package/README.md +100 -3
  2. package/dist/base-command.d.ts +27 -0
  3. package/dist/base-command.js +124 -1
  4. package/dist/commands/auth/index.d.ts +48 -1
  5. package/dist/commands/auth/index.js +259 -27
  6. package/dist/commands/function/run/index.d.ts +40 -0
  7. package/dist/commands/function/run/index.js +244 -0
  8. package/dist/commands/sandbox/pull/index.js +12 -2
  9. package/dist/commands/sandbox/push/index.d.ts +1 -1
  10. package/dist/commands/sandbox/push/index.js +17 -13
  11. package/dist/commands/static_host/build/create/index.d.ts +9 -1
  12. package/dist/commands/static_host/build/create/index.js +54 -4
  13. package/dist/commands/static_host/build/delete/index.d.ts +19 -0
  14. package/dist/commands/static_host/build/delete/index.js +114 -0
  15. package/dist/commands/static_host/build/get/index.d.ts +1 -1
  16. package/dist/commands/static_host/build/get/index.js +16 -10
  17. package/dist/commands/static_host/build/pull/index.d.ts +52 -0
  18. package/dist/commands/static_host/build/pull/index.js +300 -0
  19. package/dist/commands/static_host/build/push/index.d.ts +23 -0
  20. package/dist/commands/static_host/build/push/index.js +228 -0
  21. package/dist/commands/static_host/create/index.d.ts +17 -0
  22. package/dist/commands/static_host/create/index.js +86 -0
  23. package/dist/commands/static_host/deploy/index.d.ts +18 -0
  24. package/dist/commands/static_host/deploy/index.js +105 -0
  25. package/dist/commands/static_host/edit/index.d.ts +23 -0
  26. package/dist/commands/static_host/edit/index.js +151 -0
  27. package/dist/commands/static_host/get/index.d.ts +18 -0
  28. package/dist/commands/static_host/get/index.js +94 -0
  29. package/dist/commands/static_host/migrate/index.d.ts +44 -0
  30. package/dist/commands/static_host/migrate/index.js +205 -0
  31. package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
  32. package/dist/commands/tenant/snapshot/create/index.js +78 -0
  33. package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
  34. package/dist/commands/tenant/snapshot/delete/index.js +102 -0
  35. package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
  36. package/dist/commands/tenant/snapshot/list/index.js +96 -0
  37. package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
  38. package/dist/commands/tenant/snapshot/swap/index.js +103 -0
  39. package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
  40. package/dist/commands/tenant/unit_test/run/index.js +7 -0
  41. package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
  42. package/dist/commands/unit_test/run/index.d.ts +1 -0
  43. package/dist/commands/unit_test/run/index.js +8 -0
  44. package/dist/commands/unit_test/run_all/index.js +7 -3
  45. package/dist/commands/workspace/pull/index.js +13 -2
  46. package/dist/commands/workspace/push/index.d.ts +1 -1
  47. package/dist/commands/workspace/push/index.js +15 -5
  48. package/dist/utils/function-input.d.ts +77 -0
  49. package/dist/utils/function-input.js +148 -0
  50. package/dist/utils/knowledge-sync.d.ts +113 -0
  51. package/dist/utils/knowledge-sync.js +404 -0
  52. package/dist/utils/multidoc-push.d.ts +22 -0
  53. package/dist/utils/multidoc-push.js +262 -107
  54. package/dist/utils/reference-checker.js +2 -2
  55. package/dist/utils/static-host-files.d.ts +21 -0
  56. package/dist/utils/static-host-files.js +41 -0
  57. package/oclif.manifest.json +4179 -2636
  58. 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 and filter files ──────────────────────────────────────────
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
- if (files.length === 0) {
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
- const documentEntries = readDocuments(files);
350
- if (documentEntries.length === 0) {
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
- for (const entry of documentEntries) {
357
- const parsed = parseDocument(entry.content);
358
- if (parsed) {
359
- const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
360
- documentFileMap.set(key, entry.filePath);
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 (!dryRunResponse.ok) {
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
- 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;
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
- multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
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
- try {
572
- const response = await verboseFetch(apiUrl, {
573
- body: multidoc,
574
- headers: requestHeaders,
575
- method: 'POST',
576
- }, flags.verbose, accessToken);
577
- if (!response.ok) {
578
- handlePushError(response, await response.text(), documentEntries, inputDir, command);
579
- }
580
- // Parse response for GUID map
581
- const responseText = await response.text();
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
- // Write GUIDs back to local files
597
- if (flags.guids && guidMap.length > 0) {
598
- const baseKeyMap = new Map();
599
- for (const [key, fp] of documentFileMap) {
600
- const baseKey = key.split(':').slice(0, 2).join(':');
601
- if (baseKeyMap.has(baseKey)) {
602
- baseKeyMap.set(baseKey, ''); // Mark as ambiguous
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
- else {
605
- baseKeyMap.set(baseKey, fp);
692
+ catch {
693
+ if (flags.verbose) {
694
+ log('Server response is not JSON; skipping GUID sync');
695
+ }
606
696
  }
607
697
  }
608
- let updatedCount = 0;
609
- for (const entry of guidMap) {
610
- if (!entry.guid)
611
- continue;
612
- const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
613
- let filePath = documentFileMap.get(key);
614
- if (!filePath) {
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
- if (!filePath) {
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
- try {
628
- const updated = syncGuidToFile(filePath, entry.guid);
629
- if (updated)
630
- updatedCount++;
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
- catch (error) {
633
- command.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
738
+ if (updatedCount > 0) {
739
+ log(`Synced ${updatedCount} GUIDs to local files`);
634
740
  }
635
741
  }
636
- if (updatedCount > 0) {
637
- log(`Synced ${updatedCount} GUIDs to local files`);
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
- catch (error) {
642
- if (error instanceof Error && 'oclif' in error)
643
- throw error;
644
- const elapsedMs = Date.now() - startTime;
645
- command.error(`Failed to push multidoc: ${describeNetworkError(error, apiUrl, elapsedMs)}`);
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 pushedCount = multidoc.split('\n---\n').length;
649
- log(`Pushed ${pushedCount} documents to ${target.label} from ${relative(process.cwd(), inputDir) || inputDir} in ${elapsed}s`);
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 created_at are auto-added during import
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
+ }