@xano/cli 1.0.4-beta.3 → 1.0.4-beta.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 CHANGED
@@ -577,11 +577,14 @@ xano static_host build pull default --env prod -d ./prod-release
577
577
 
578
578
  # Push a build (name optional — auto-generated from the timestamp if omitted).
579
579
  # Accepts a directory (-d) or a zip file (-f). Defaults to the current directory.
580
+ # When pushing a directory, files matched by its .gitignore are skipped by default
581
+ # (the .git/ folder is always excluded); use --no-gitignore to push everything.
580
582
  # For package.json builds, the CLI waits for the build to finish (--no-wait to skip).
581
583
  xano static_host build push default -d ./dist -n "v1.0.0"
582
584
  xano static_host build push default # current dir, auto-name
583
585
  xano static_host build push default -f ./build.zip -n "v1.0.0" # from zip file
584
586
  xano static_host build push default -n "release" --description "Production build"
587
+ xano static_host build push default -d ./static --no-gitignore # push gitignored files too
585
588
 
586
589
  # Delete a build (prompts for confirmation; --force to skip)
587
590
  xano static_host build delete default --build_id 52
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import snakeCase from 'lodash.snakecase';
3
3
  import BaseCommand from '../../../base-command.js';
4
4
  import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
5
+ import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
5
6
  import * as fs from 'node:fs';
6
7
  import * as path from 'node:path';
7
8
  export default class SandboxPull extends BaseCommand {
@@ -87,7 +88,6 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
87
88
  }
88
89
  if (documents.length === 0) {
89
90
  this.log('No documents found in response');
90
- return;
91
91
  }
92
92
  const outputDir = path.resolve(flags.directory);
93
93
  fs.mkdirSync(outputDir, { recursive: true });
@@ -182,7 +182,17 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
182
182
  fs.writeFileSync(filePath, doc.content, 'utf8');
183
183
  writtenCount++;
184
184
  }
185
- this.log(`Pulled ${writtenCount} documents from sandbox environment to ${flags.directory}`);
185
+ // ── Pull knowledge ────────────────────────────────────────────────────
186
+ const knowledgeUrl = `${profile.instance_origin}/api:meta/sandbox/knowledge/sync`;
187
+ const knowledgeObjects = await fetchKnowledge(knowledgeUrl, '', profile.access_token, this.verboseFetch.bind(this), flags.verbose);
188
+ let knowledgeCount = 0;
189
+ if (knowledgeObjects.length > 0) {
190
+ knowledgeCount = writeKnowledge(knowledgeObjects, outputDir);
191
+ }
192
+ const parts = [`${writtenCount} documents`];
193
+ if (knowledgeCount > 0)
194
+ parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
195
+ this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
186
196
  }
187
197
  sanitizeFilename(name) {
188
198
  return snakeCase(name.replaceAll('"', ''));
@@ -19,6 +19,6 @@ export default class SandboxPush extends BaseCommand {
19
19
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
20
  };
21
21
  run(): Promise<void>;
22
- private openReview;
23
22
  private getFrontendUrl;
23
+ private openReview;
24
24
  }
@@ -130,12 +130,29 @@ Push and open sandbox review in the browser
130
130
  branch: '',
131
131
  command: this,
132
132
  inputDir,
133
+ knowledge: {
134
+ listUrl: () => `${baseUrl}/knowledge/sync`,
135
+ rootDir: inputDir,
136
+ },
133
137
  verboseFetch: this.verboseFetch.bind(this),
134
138
  }, target, pushFlags);
135
139
  if (flags.review) {
136
140
  await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
137
141
  }
138
142
  }
143
+ getFrontendUrl(instanceOrigin) {
144
+ try {
145
+ const url = new URL(instanceOrigin);
146
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
147
+ url.port = '4200';
148
+ return url.origin;
149
+ }
150
+ }
151
+ catch {
152
+ // fall through
153
+ }
154
+ return instanceOrigin;
155
+ }
139
156
  async openReview(instanceOrigin, accessToken, verbose) {
140
157
  const response = await this.verboseFetch(`${instanceOrigin}/api:meta/sandbox/impersonate`, {
141
158
  headers: {
@@ -158,17 +175,4 @@ Push and open sandbox review in the browser
158
175
  this.log('Opening sandbox review...');
159
176
  await open(reviewUrl);
160
177
  }
161
- getFrontendUrl(instanceOrigin) {
162
- try {
163
- const url = new URL(instanceOrigin);
164
- if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
165
- url.port = '4200';
166
- return url.origin;
167
- }
168
- }
169
- catch {
170
- // fall through
171
- }
172
- return instanceOrigin;
173
- }
174
178
  }
@@ -10,6 +10,7 @@ export default class StaticHostBuildPush extends BaseCommand {
10
10
  directory: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'no-gitignore': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
15
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
16
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -18,6 +19,5 @@ export default class StaticHostBuildPush extends BaseCommand {
18
19
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
20
  };
20
21
  run(): Promise<void>;
21
- private countFiles;
22
22
  private createZipBuffer;
23
23
  }
@@ -3,6 +3,7 @@ import archiver from 'archiver';
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
5
  import BaseCommand from '../../../../base-command.js';
6
+ import { collectStaticHostFiles } from '../../../../utils/static-host-files.js';
6
7
  import { generateBuildName } from '../create/index.js';
7
8
  export default class StaticHostBuildPush extends BaseCommand {
8
9
  static args = {
@@ -27,6 +28,9 @@ ID: 124
27
28
  `$ xano static_host build push myhost -n "production" --description "Production build" -w 40
28
29
  Pushed 22 files as build "production"
29
30
  ID: 125
31
+ `,
32
+ `$ xano static_host build push default -d ./static --no-gitignore
33
+ Pushed 30 files as build "20260531-143022"
30
34
  `,
31
35
  ];
32
36
  static flags = {
@@ -52,6 +56,11 @@ ID: 125
52
56
  description: 'Build name (auto-generated from the current timestamp if omitted)',
53
57
  required: false,
54
58
  }),
59
+ 'no-gitignore': Flags.boolean({
60
+ default: false,
61
+ description: 'Push every file in the directory, including those matched by .gitignore (the .git/ folder is always excluded)',
62
+ required: false,
63
+ }),
55
64
  'no-wait': Flags.boolean({
56
65
  default: false,
57
66
  description: 'Return immediately after upload instead of waiting for the build to finish',
@@ -113,10 +122,15 @@ ID: 125
113
122
  if (!dirStats.isDirectory()) {
114
123
  this.error(`Path is not a directory: ${sourceDir}`);
115
124
  }
116
- fileCount = this.countFiles(sourceDir);
125
+ const files = collectStaticHostFiles(sourceDir, { respectGitignore: !flags['no-gitignore'] });
126
+ if (files.length === 0) {
127
+ this.error(`No files to push from ${sourceDir} — everything is excluded by .gitignore. ` +
128
+ `Use --no-gitignore to push the entire directory.`);
129
+ }
130
+ fileCount = files.length;
117
131
  if (animate)
118
132
  ux.action.start('Packaging', `${fileCount} files`);
119
- zipBuffer = await this.createZipBuffer(sourceDir);
133
+ zipBuffer = await this.createZipBuffer(sourceDir, files);
120
134
  if (animate) {
121
135
  ux.action.stop(`${fileCount} files (${(zipBuffer.length / (1024 * 1024)).toFixed(1)} MB)`);
122
136
  ux.action.start('Uploading');
@@ -192,20 +206,7 @@ ID: 125
192
206
  }
193
207
  }
194
208
  }
195
- countFiles(dir) {
196
- let count = 0;
197
- const entries = fs.readdirSync(dir, { withFileTypes: true });
198
- for (const entry of entries) {
199
- if (entry.isDirectory()) {
200
- count += this.countFiles(path.join(dir, entry.name));
201
- }
202
- else if (entry.isFile()) {
203
- count++;
204
- }
205
- }
206
- return count;
207
- }
208
- async createZipBuffer(sourceDir) {
209
+ async createZipBuffer(sourceDir, files) {
209
210
  return new Promise((resolve, reject) => {
210
211
  const chunks = [];
211
212
  const archive = archiver('zip', { zlib: { level: 9 } });
@@ -218,7 +219,9 @@ ID: 125
218
219
  archive.on('error', (err) => {
219
220
  reject(err);
220
221
  });
221
- archive.directory(sourceDir, false);
222
+ for (const rel of files) {
223
+ archive.file(path.join(sourceDir, rel), { name: rel });
224
+ }
222
225
  archive.finalize();
223
226
  });
224
227
  }
@@ -4,11 +4,12 @@ import * as path from 'node:path';
4
4
  import snakeCase from 'lodash.snakecase';
5
5
  import BaseCommand from '../../../base-command.js';
6
6
  import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
7
+ import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
7
8
  export default class Pull extends BaseCommand {
8
9
  static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
9
10
  static examples = [
10
11
  `$ xano workspace pull
11
- Pulled 42 documents to current directory
12
+ Pulled 42 documents + 5 knowledge files to current directory
12
13
  `,
13
14
  `$ xano workspace pull -d ./my-workspace
14
15
  Pulled 42 documents to ./my-workspace
@@ -239,7 +240,17 @@ Pulled 58 documents
239
240
  fs.writeFileSync(filePath, doc.content, 'utf8');
240
241
  writtenCount++;
241
242
  }
242
- this.log(`Pulled ${writtenCount} documents to ${flags.directory}`);
243
+ // ── Pull knowledge ────────────────────────────────────────────────────
244
+ const knowledgeUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/knowledge/sync`;
245
+ const knowledgeObjects = await fetchKnowledge(knowledgeUrl, branch, profile.access_token, this.verboseFetch.bind(this), flags.verbose);
246
+ let knowledgeCount = 0;
247
+ if (knowledgeObjects.length > 0) {
248
+ knowledgeCount = writeKnowledge(knowledgeObjects, outputDir);
249
+ }
250
+ const parts = [`${writtenCount} documents`];
251
+ if (knowledgeCount > 0)
252
+ parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
253
+ this.log(`Pulled ${parts.join(' + ')} to ${flags.directory}`);
243
254
  }
244
255
  /**
245
256
  * Sanitize a document name for use as a filename.
@@ -4,8 +4,8 @@ export default class Push extends BaseCommand {
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
- directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
7
  delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
9
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -53,6 +53,12 @@ Push all files except tables
53
53
  `,
54
54
  `$ xano workspace push -i "function/*" -e "**/test*"
55
55
  Push functions but exclude test files
56
+ `,
57
+ `$ xano workspace push -i "knowledge/**"
58
+ Push only knowledge files (agents.md / skills / docs)
59
+ `,
60
+ `$ xano workspace push --sync --delete
61
+ Full sync including knowledge files; removes server objects not present locally
56
62
  `,
57
63
  ];
58
64
  static flags = {
@@ -62,17 +68,17 @@ Push functions but exclude test files
62
68
  description: 'Branch name (optional if set in profile, defaults to live)',
63
69
  required: false,
64
70
  }),
71
+ delete: Flags.boolean({
72
+ default: false,
73
+ description: '[CRITICAL] STOP and confirm with the user before running. Delete workspace objects not included in the push (requires --sync).',
74
+ required: false,
75
+ }),
65
76
  directory: Flags.string({
66
77
  char: 'd',
67
78
  default: '.',
68
79
  description: 'Directory containing documents to push (defaults to current directory)',
69
80
  required: false,
70
81
  }),
71
- delete: Flags.boolean({
72
- default: false,
73
- description: '[CRITICAL] STOP and confirm with the user before running. Delete workspace objects not included in the push (requires --sync).',
74
- required: false,
75
- }),
76
82
  'dry-run': Flags.boolean({
77
83
  default: false,
78
84
  description: 'Show preview of changes without pushing (exit after preview)',
@@ -186,6 +192,10 @@ Push functions but exclude test files
186
192
  branch,
187
193
  command: this,
188
194
  inputDir,
195
+ knowledge: {
196
+ listUrl: () => `${baseUrl}/knowledge/sync`,
197
+ rootDir: inputDir,
198
+ },
189
199
  verboseFetch: this.verboseFetch.bind(this),
190
200
  }, target, pushFlags);
191
201
  }
@@ -0,0 +1,113 @@
1
+ export declare const KNOWLEDGE_DIR = "knowledge";
2
+ export type KnowledgeType = 'agents.md' | 'doc' | 'skill';
3
+ /** A reference file attached to a skill (knowledge_file). */
4
+ export interface KnowledgeFile {
5
+ content: string;
6
+ /** Path relative to the skill's `references/` folder, POSIX-separated. */
7
+ name: string;
8
+ }
9
+ /** A workspace-scoped knowledge object as exchanged with the API. */
10
+ export interface KnowledgeObject {
11
+ content: string;
12
+ description?: string;
13
+ enabled?: boolean;
14
+ files?: KnowledgeFile[];
15
+ guid?: string;
16
+ knowledge_type: KnowledgeType;
17
+ mode?: string;
18
+ name: string;
19
+ scope?: string;
20
+ }
21
+ /** A locally-collected object that also remembers its primary file on disk (for GUID writeback). */
22
+ export interface LocalKnowledgeObject extends KnowledgeObject {
23
+ filePath: string;
24
+ }
25
+ export interface KnowledgePushBody {
26
+ branch?: string;
27
+ delete?: boolean;
28
+ dry_run?: boolean;
29
+ force?: boolean;
30
+ items: KnowledgeObject[];
31
+ }
32
+ export interface KnowledgePushResult {
33
+ deleted?: number;
34
+ guid_map?: Array<{
35
+ guid: string;
36
+ name: string;
37
+ }>;
38
+ imported?: number;
39
+ operations?: KnowledgeDryRunOperation[];
40
+ summary?: Record<string, KnowledgeDryRunSummary>;
41
+ }
42
+ export interface KnowledgeDryRunOperation {
43
+ action: string;
44
+ name: string;
45
+ type: string;
46
+ }
47
+ export interface KnowledgeDryRunSummary {
48
+ created: number;
49
+ deleted: number;
50
+ unchanged: number;
51
+ updated: number;
52
+ }
53
+ export interface KnowledgeDryRunResult {
54
+ operations: KnowledgeDryRunOperation[];
55
+ summary: Record<string, KnowledgeDryRunSummary>;
56
+ }
57
+ type VerboseFetch = (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
58
+ /**
59
+ * Build a primary `.md` file body: YAML frontmatter (built from the object's
60
+ * structured fields) followed by the markdown content.
61
+ */
62
+ export declare function buildPrimaryContent(obj: KnowledgeObject): string;
63
+ /**
64
+ * Split a primary `.md` file into its frontmatter fields and markdown body.
65
+ * Returns `{meta: {}, body: <raw>}` when there is no leading `---` block.
66
+ */
67
+ export declare function parsePrimaryContent(raw: string): {
68
+ body: string;
69
+ meta: Record<string, unknown>;
70
+ };
71
+ /** Read just `guid`/`name` from frontmatter (used by GUID matching). */
72
+ export declare function parseFrontmatter(content: string): {
73
+ guid?: string;
74
+ name?: string;
75
+ };
76
+ /**
77
+ * Write knowledge objects under `<outputDir>/knowledge/`, building frontmatter
78
+ * and laying out skill reference files under `<skill>/references/`.
79
+ * Returns the number of files written (primaries + references).
80
+ */
81
+ export declare function writeKnowledge(objects: KnowledgeObject[], outputDir: string): number;
82
+ /**
83
+ * Walk `<inputDir>/knowledge/`, reconstructing structured knowledge objects from
84
+ * primary `.md` files (frontmatter → fields, body → content) and attaching each
85
+ * skill's `references/` files. Include/exclude globs are matched against
86
+ * `knowledge/<path>` (relative to inputDir), consistent with multidoc filtering.
87
+ * Returns `[]` when the directory is absent.
88
+ */
89
+ export declare function collectKnowledgeObjects(inputDir: string, include?: string[], exclude?: string[]): LocalKnowledgeObject[];
90
+ /** Strip the local-only `filePath` before sending objects to the API. */
91
+ export declare function toPushItems(objects: LocalKnowledgeObject[]): KnowledgeObject[];
92
+ /**
93
+ * GET workspace knowledge objects (with content). Returns `[]` on 404 (instance
94
+ * without the feature) or any network error, so pull degrades gracefully.
95
+ */
96
+ export declare function fetchKnowledge(baseUrl: string, branch: string, accessToken: string, verboseFetch: VerboseFetch, verbose: boolean): Promise<KnowledgeObject[]>;
97
+ /**
98
+ * POST knowledge objects to the workspace. Throws on non-2xx. When `body.dry_run`
99
+ * is set the result may carry `operations`/`summary` instead of `imported`.
100
+ */
101
+ export declare function pushKnowledge(url: string, accessToken: string, verboseFetch: VerboseFetch, verbose: boolean, body: KnowledgePushBody): Promise<KnowledgePushResult>;
102
+ /**
103
+ * Compute a dry-run-style preview by diffing local objects against server
104
+ * objects (match by `guid`, then `name`). Used when the server doesn't honor
105
+ * the `dry_run` flag.
106
+ */
107
+ export declare function knowledgePreview(local: KnowledgeObject[], server: KnowledgeObject[], willDelete: boolean): KnowledgeDryRunResult;
108
+ /**
109
+ * Set or update `guid:` in a primary `.md` file's YAML frontmatter.
110
+ * Returns true if the file was modified.
111
+ */
112
+ export declare function syncGuidToFrontmatter(filePath: string, guid: string): boolean;
113
+ export {};