@xano/cli 1.0.4-beta.1 → 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 +32 -1
- package/dist/commands/auth/index.d.ts +38 -1
- package/dist/commands/auth/index.js +135 -40
- 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/push/index.d.ts +1 -1
- package/dist/commands/static_host/build/push/index.js +20 -17
- 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 +242 -91
- package/dist/utils/static-host-files.d.ts +21 -0
- package/dist/utils/static-host-files.js +41 -0
- package/oclif.manifest.json +2106 -1910
- package/package.json +2 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import BaseCommand from '../../../base-command.js';
|
|
4
|
+
import { assembleInput, loadJsonSource, parseDataPairs, validateAgainstSchema, } from '../../../utils/function-input.js';
|
|
5
|
+
export default class FunctionRun extends BaseCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
name: Args.string({
|
|
8
|
+
description: 'Name of the function to run (interactive if omitted with --name unset)',
|
|
9
|
+
required: false,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Run (execute) a named function in a workspace and print its result';
|
|
13
|
+
static examples = [
|
|
14
|
+
`$ xano function:run calcScore -w 40
|
|
15
|
+
# Prompts for any declared inputs, then runs the function`,
|
|
16
|
+
`$ xano function:run calcScore --data email=jo@x.com --data age:=30 --data active:=true`,
|
|
17
|
+
`$ xano function:run calcScore --json @payload.json --data env=staging`,
|
|
18
|
+
`$ echo '{"email":"jo@x.com"}' | xano function:run calcScore --stdin -o json | jq .result`,
|
|
19
|
+
`$ xano function:run calcScore --branch dev --logs`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
branch: Flags.string({
|
|
24
|
+
description: 'Branch to run from (defaults to profile branch, then main)',
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
data: Flags.string({
|
|
28
|
+
char: 'd',
|
|
29
|
+
description: 'Input field as key=value (string), key:=json (raw JSON), or key@file (file contents). Repeatable.',
|
|
30
|
+
multiple: true,
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
json: Flags.string({
|
|
34
|
+
description: "Input as a JSON object: inline, @file.json, or '-' for stdin",
|
|
35
|
+
exclusive: ['stdin'],
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
logs: Flags.boolean({
|
|
39
|
+
default: false,
|
|
40
|
+
description: 'Print the execution logs returned by the debugger',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
'name': Flags.string({
|
|
44
|
+
char: 'n',
|
|
45
|
+
description: 'Name of the function to run (alternative to the positional argument)',
|
|
46
|
+
required: false,
|
|
47
|
+
}),
|
|
48
|
+
'no-input-check': Flags.boolean({
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Skip local schema validation and interactive prompting; send the payload as-is',
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
53
|
+
output: Flags.string({
|
|
54
|
+
char: 'o',
|
|
55
|
+
default: 'json',
|
|
56
|
+
description: 'Output format',
|
|
57
|
+
options: ['json', 'summary'],
|
|
58
|
+
required: false,
|
|
59
|
+
}),
|
|
60
|
+
stdin: Flags.boolean({
|
|
61
|
+
char: 's',
|
|
62
|
+
default: false,
|
|
63
|
+
description: "Read the input JSON object from stdin (same as --json -)",
|
|
64
|
+
exclusive: ['json'],
|
|
65
|
+
required: false,
|
|
66
|
+
}),
|
|
67
|
+
workspace: Flags.string({
|
|
68
|
+
char: 'w',
|
|
69
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
70
|
+
required: false,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
async run() {
|
|
74
|
+
const { args, flags } = await this.parse(FunctionRun);
|
|
75
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
76
|
+
const functionName = args.name ?? flags.name;
|
|
77
|
+
if (!functionName) {
|
|
78
|
+
this.error('A function name is required. Provide it as an argument or with --name.');
|
|
79
|
+
}
|
|
80
|
+
const workspaceId = this.resolveWorkspaceId(flags, profile, profileName);
|
|
81
|
+
const branch = flags.branch ?? profile.branch ?? '';
|
|
82
|
+
// 1) Assemble the input object from JSON base + --data overrides.
|
|
83
|
+
const jsonSource = flags.stdin ? '-' : flags.json;
|
|
84
|
+
const jsonBase = loadJsonSource(jsonSource, { stdin: () => this.readStdinSync() });
|
|
85
|
+
let input = assembleInput({ dataPairs: parseDataPairs(flags.data ?? []), jsonBase });
|
|
86
|
+
// 2) Introspect the function's declared inputs (best-effort) to validate,
|
|
87
|
+
// warn on obvious mismatches, and prompt for missing required params.
|
|
88
|
+
if (!flags['no-input-check']) {
|
|
89
|
+
const fn = await this.fetchFunctionByName({
|
|
90
|
+
branch,
|
|
91
|
+
name: functionName,
|
|
92
|
+
profile,
|
|
93
|
+
verbose: flags.verbose,
|
|
94
|
+
workspaceId,
|
|
95
|
+
});
|
|
96
|
+
const schema = fn?.input;
|
|
97
|
+
const { missingRequired, warnings } = validateAgainstSchema(input, schema);
|
|
98
|
+
for (const w of warnings)
|
|
99
|
+
this.warn(w);
|
|
100
|
+
if (missingRequired.length > 0) {
|
|
101
|
+
input = await this.promptForMissing(input, missingRequired);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 3) Execute via the meta run endpoint.
|
|
105
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/run`;
|
|
106
|
+
const body = { input, name: functionName };
|
|
107
|
+
if (branch)
|
|
108
|
+
body.branch = branch;
|
|
109
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
headers: {
|
|
112
|
+
'accept': 'application/json',
|
|
113
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
method: 'POST',
|
|
117
|
+
}, flags.verbose, profile.access_token);
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const message = await this.parseApiError(response, 'Failed to run function');
|
|
120
|
+
this.error(message);
|
|
121
|
+
}
|
|
122
|
+
const result = (await response.json());
|
|
123
|
+
this.emitResult(result, flags.output, flags.logs);
|
|
124
|
+
// 4) Compose in scripts/CI: non-zero exit when the function errored.
|
|
125
|
+
if (result.status && result.status !== 'ok') {
|
|
126
|
+
this.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
emitResult(result, output, showLogs) {
|
|
130
|
+
if (output === 'json') {
|
|
131
|
+
// Raw result object so `| jq .result` works predictably.
|
|
132
|
+
this.log(JSON.stringify(result.result ?? null, null, 2));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const status = result.status ?? 'ok';
|
|
136
|
+
this.log(`Status: ${status}`);
|
|
137
|
+
this.log('Result:');
|
|
138
|
+
this.log(JSON.stringify(result.result ?? null, null, 2));
|
|
139
|
+
}
|
|
140
|
+
if (showLogs && Array.isArray(result.logs) && result.logs.length > 0) {
|
|
141
|
+
this.log('');
|
|
142
|
+
this.log('Logs:');
|
|
143
|
+
for (const entry of result.logs) {
|
|
144
|
+
this.log(typeof entry === 'string' ? entry : JSON.stringify(entry));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Fetch a single function record by name via the list endpoint's search,
|
|
150
|
+
* returning the exact-name match (so `calc` doesn't accidentally match
|
|
151
|
+
* `calcScore`). Best-effort: returns undefined and warns on any failure so a
|
|
152
|
+
* transient introspection problem never blocks an otherwise-valid run.
|
|
153
|
+
*/
|
|
154
|
+
async fetchFunctionByName(opts) {
|
|
155
|
+
const { branch, name, profile, verbose, workspaceId } = opts;
|
|
156
|
+
// eslint-disable-next-line camelcase -- external Metadata API query param
|
|
157
|
+
const params = new URLSearchParams({ per_page: '100', search: name });
|
|
158
|
+
if (branch)
|
|
159
|
+
params.set('branch', branch);
|
|
160
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${params.toString()}`;
|
|
161
|
+
try {
|
|
162
|
+
const response = await this.verboseFetch(url, { headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` }, method: 'GET' }, verbose, profile.access_token);
|
|
163
|
+
if (!response.ok)
|
|
164
|
+
return undefined;
|
|
165
|
+
const page = (await response.json());
|
|
166
|
+
const items = Array.isArray(page) ? page : (page.items ?? []);
|
|
167
|
+
const exact = items.filter((f) => f.name === name);
|
|
168
|
+
if (exact.length > 1) {
|
|
169
|
+
this.warn(`Multiple functions named '${name}' found; skipping input validation.`);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return exact[0];
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Introspection is a convenience, not a requirement.
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Prompt for each missing required input on an interactive TTY. Values are
|
|
181
|
+
* read as raw JSON when they parse as such (so numbers/bools/objects work),
|
|
182
|
+
* otherwise treated as a string — mirroring the `key:=`/`key=` distinction.
|
|
183
|
+
* Non-TTY: fail fast with the list of missing params (never hang on a prompt).
|
|
184
|
+
*/
|
|
185
|
+
async promptForMissing(input, missing) {
|
|
186
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
187
|
+
const names = missing.map((p) => `${p.name}${p.type ? ` (${p.type})` : ''}`).join(', ');
|
|
188
|
+
this.error(`Missing required input: ${names}. ` +
|
|
189
|
+
`Provide via --data/--json, or run interactively in a terminal.`);
|
|
190
|
+
}
|
|
191
|
+
const readline = await import('node:readline');
|
|
192
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
193
|
+
const ask = (message) => new Promise((resolve) => {
|
|
194
|
+
rl.question(message, resolve);
|
|
195
|
+
});
|
|
196
|
+
const filled = { ...input };
|
|
197
|
+
try {
|
|
198
|
+
for (const param of missing) {
|
|
199
|
+
const hint = [param.type, param.description].filter(Boolean).join(' — ');
|
|
200
|
+
// eslint-disable-next-line no-await-in-loop
|
|
201
|
+
const raw = await ask(`${param.name}${hint ? ` (${hint})` : ''}: `);
|
|
202
|
+
filled[param.name] = coercePromptValue(raw, param);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
rl.close();
|
|
207
|
+
}
|
|
208
|
+
return filled;
|
|
209
|
+
}
|
|
210
|
+
readStdinSync() {
|
|
211
|
+
// Synchronous read of all of stdin (fd 0); used for --json - / --stdin.
|
|
212
|
+
try {
|
|
213
|
+
return readFileSync(0, 'utf8');
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
this.error(`Failed to read from stdin: ${error instanceof Error ? error.message : String(error)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
resolveWorkspaceId(flags, profile, profileName) {
|
|
220
|
+
if (flags.workspace)
|
|
221
|
+
return flags.workspace;
|
|
222
|
+
if (profile.workspace)
|
|
223
|
+
return profile.workspace;
|
|
224
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
225
|
+
` 1. Provide it as a flag: xano function:run <name> -w <workspace_id>\n` +
|
|
226
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Coerce a prompted string into the value the parameter likely expects. Non-text
|
|
231
|
+
* params attempt a JSON parse (so `30`, `true`, `["a"]` become typed values);
|
|
232
|
+
* text params keep the raw string. Falls back to the raw string on parse errors.
|
|
233
|
+
*/
|
|
234
|
+
function coercePromptValue(raw, param) {
|
|
235
|
+
const isTextLike = !param.type || param.type === 'text' || param.type === 'uuid';
|
|
236
|
+
if (isTextLike)
|
|
237
|
+
return raw;
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(raw);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return raw;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -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
|
-
|
|
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('"', ''));
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
for (const rel of files) {
|
|
223
|
+
archive.file(path.join(sourceDir, rel), { name: rel });
|
|
224
|
+
}
|
|
222
225
|
archive.finalize();
|
|
223
226
|
});
|
|
224
227
|
}
|
|
@@ -6,6 +6,7 @@ export default class TenantUnitTestRun extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -17,6 +17,11 @@ Result: PASS
|
|
|
17
17
|
];
|
|
18
18
|
static flags = {
|
|
19
19
|
...BaseCommand.baseFlags,
|
|
20
|
+
branch: Flags.string({
|
|
21
|
+
char: 'b',
|
|
22
|
+
description: 'Branch the unit test belongs to (uses profile branch if not provided, then the live branch)',
|
|
23
|
+
required: false,
|
|
24
|
+
}),
|
|
20
25
|
output: Flags.string({
|
|
21
26
|
char: 'o',
|
|
22
27
|
default: 'summary',
|
|
@@ -43,12 +48,14 @@ Result: PASS
|
|
|
43
48
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
44
49
|
}
|
|
45
50
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
51
|
+
const branch = flags.branch ?? profile.branch;
|
|
46
52
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test/${encodeURIComponent(args.unit_test_id)}/run`;
|
|
47
53
|
try {
|
|
48
54
|
if (flags.output === 'summary') {
|
|
49
55
|
this.log(`Running unit test ${args.unit_test_id}...`);
|
|
50
56
|
}
|
|
51
57
|
const response = await this.verboseFetch(apiUrl, {
|
|
58
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
52
59
|
headers: {
|
|
53
60
|
accept: 'application/json',
|
|
54
61
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -18,7 +18,7 @@ Results: 4 passed, 1 failed
|
|
|
18
18
|
...BaseCommand.baseFlags,
|
|
19
19
|
branch: Flags.string({
|
|
20
20
|
char: 'b',
|
|
21
|
-
description: '
|
|
21
|
+
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
22
22
|
required: false,
|
|
23
23
|
}),
|
|
24
24
|
'obj-type': Flags.string({
|
|
@@ -53,11 +53,14 @@ Results: 4 passed, 1 failed
|
|
|
53
53
|
}
|
|
54
54
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
55
55
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test`;
|
|
56
|
+
// Resolve the branch once so the listed tests and the tests we run come
|
|
57
|
+
// from the same branch.
|
|
58
|
+
const branch = flags.branch ?? profile.branch;
|
|
56
59
|
try {
|
|
57
60
|
const listParams = new URLSearchParams();
|
|
58
61
|
listParams.set('per_page', '10000');
|
|
59
|
-
if (
|
|
60
|
-
listParams.set('branch',
|
|
62
|
+
if (branch)
|
|
63
|
+
listParams.set('branch', branch);
|
|
61
64
|
if (flags['obj-type'])
|
|
62
65
|
listParams.set('obj_type', flags['obj-type']);
|
|
63
66
|
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
@@ -94,6 +97,7 @@ Results: 4 passed, 1 failed
|
|
|
94
97
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
95
98
|
try {
|
|
96
99
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
100
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
97
101
|
headers: {
|
|
98
102
|
accept: 'application/json',
|
|
99
103
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -6,6 +6,7 @@ export default class UnitTestRun extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -14,9 +14,15 @@ Running unit test abc-123...
|
|
|
14
14
|
Result: PASS
|
|
15
15
|
`,
|
|
16
16
|
`$ xano unit-test run abc-123 -o json`,
|
|
17
|
+
`$ xano unit-test run abc-123 --branch dev`,
|
|
17
18
|
];
|
|
18
19
|
static flags = {
|
|
19
20
|
...BaseCommand.baseFlags,
|
|
21
|
+
branch: Flags.string({
|
|
22
|
+
char: 'b',
|
|
23
|
+
description: 'Branch the unit test belongs to (uses profile branch if not provided, then the live branch)',
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
20
26
|
output: Flags.string({
|
|
21
27
|
char: 'o',
|
|
22
28
|
default: 'summary',
|
|
@@ -37,12 +43,14 @@ Result: PASS
|
|
|
37
43
|
if (!workspaceId) {
|
|
38
44
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
39
45
|
}
|
|
46
|
+
const branch = flags.branch ?? profile.branch;
|
|
40
47
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/unit_test/${args.unit_test_id}/run`;
|
|
41
48
|
try {
|
|
42
49
|
if (flags.output === 'summary') {
|
|
43
50
|
this.log(`Running unit test ${args.unit_test_id}...`);
|
|
44
51
|
}
|
|
45
52
|
const response = await this.verboseFetch(apiUrl, {
|
|
53
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
46
54
|
headers: {
|
|
47
55
|
'accept': 'application/json',
|
|
48
56
|
'Authorization': `Bearer ${profile.access_token}`,
|
|
@@ -19,7 +19,7 @@ Results: 2 passed, 1 failed
|
|
|
19
19
|
...BaseCommand.baseFlags,
|
|
20
20
|
branch: Flags.string({
|
|
21
21
|
char: 'b',
|
|
22
|
-
description: '
|
|
22
|
+
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
23
23
|
required: false,
|
|
24
24
|
}),
|
|
25
25
|
'obj-type': Flags.string({
|
|
@@ -48,12 +48,15 @@ Results: 2 passed, 1 failed
|
|
|
48
48
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
49
49
|
}
|
|
50
50
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/unit_test`;
|
|
51
|
+
// Resolve the branch once so the listed tests and the tests we run come
|
|
52
|
+
// from the same branch.
|
|
53
|
+
const branch = flags.branch ?? profile.branch;
|
|
51
54
|
try {
|
|
52
55
|
// Step 1: List all unit tests
|
|
53
56
|
const listParams = new URLSearchParams();
|
|
54
57
|
listParams.set('per_page', '10000');
|
|
55
|
-
if (
|
|
56
|
-
listParams.set('branch',
|
|
58
|
+
if (branch) {
|
|
59
|
+
listParams.set('branch', branch);
|
|
57
60
|
}
|
|
58
61
|
if (flags['obj-type']) {
|
|
59
62
|
listParams.set('obj_type', flags['obj-type']);
|
|
@@ -93,6 +96,7 @@ Results: 2 passed, 1 failed
|
|
|
93
96
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
94
97
|
try {
|
|
95
98
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
99
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
96
100
|
headers: {
|
|
97
101
|
accept: 'application/json',
|
|
98
102
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -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
|
-
|
|
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>;
|