@xano/cli 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -4
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +45 -1
- package/dist/commands/auth/index.js +197 -29
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -0
- package/dist/commands/knowledge/{pull → get}/index.d.ts +6 -2
- package/dist/commands/knowledge/get/index.js +126 -0
- package/dist/commands/knowledge/{push → list}/index.d.ts +6 -8
- package/dist/commands/knowledge/list/index.js +135 -0
- package/dist/commands/sandbox/pull/index.js +12 -2
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +19 -14
- package/dist/commands/sandbox/reset/index.js +12 -0
- package/dist/commands/static_host/build/create/index.d.ts +9 -1
- package/dist/commands/static_host/build/create/index.js +54 -4
- package/dist/commands/static_host/build/delete/index.d.ts +19 -0
- package/dist/commands/static_host/build/delete/index.js +114 -0
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +16 -10
- package/dist/commands/static_host/build/pull/index.d.ts +52 -0
- package/dist/commands/static_host/build/pull/index.js +300 -0
- package/dist/commands/static_host/build/push/index.d.ts +23 -0
- package/dist/commands/static_host/build/push/index.js +228 -0
- package/dist/commands/static_host/create/index.d.ts +17 -0
- package/dist/commands/static_host/create/index.js +86 -0
- package/dist/commands/static_host/deploy/index.d.ts +18 -0
- package/dist/commands/static_host/deploy/index.js +105 -0
- package/dist/commands/static_host/edit/index.d.ts +23 -0
- package/dist/commands/static_host/edit/index.js +151 -0
- package/dist/commands/static_host/get/index.d.ts +18 -0
- package/dist/commands/static_host/get/index.js +94 -0
- package/dist/commands/static_host/migrate/index.d.ts +44 -0
- package/dist/commands/static_host/migrate/index.js +205 -0
- package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
- package/dist/commands/tenant/snapshot/create/index.js +78 -0
- package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/delete/index.js +102 -0
- package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
- package/dist/commands/tenant/snapshot/list/index.js +96 -0
- package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/swap/index.js +103 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +7 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +8 -0
- package/dist/commands/unit_test/run_all/index.js +7 -3
- package/dist/commands/workspace/pull/index.js +13 -2
- package/dist/commands/workspace/push/index.d.ts +1 -1
- package/dist/commands/workspace/push/index.js +15 -5
- package/dist/utils/function-input.d.ts +77 -0
- package/dist/utils/function-input.js +148 -0
- package/dist/utils/knowledge-sync.d.ts +84 -79
- package/dist/utils/knowledge-sync.js +376 -323
- package/dist/utils/multidoc-push.d.ts +30 -18
- package/dist/utils/multidoc-push.js +284 -148
- package/dist/utils/reference-checker.js +2 -2
- package/dist/utils/static-host-files.d.ts +21 -0
- package/dist/utils/static-host-files.js +41 -0
- package/oclif.manifest.json +4574 -3045
- package/package.json +7 -1
- package/dist/commands/knowledge/pull/index.js +0 -86
- package/dist/commands/knowledge/push/index.js +0 -126
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import BaseCommand from '../../../../base-command.js';
|
|
2
|
+
export default class TenantSnapshotSwap extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
tenant_name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
snapshot: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<void>;
|
|
18
|
+
private confirm;
|
|
19
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as readline from 'node:readline';
|
|
3
|
+
import BaseCommand from '../../../../base-command.js';
|
|
4
|
+
export default class TenantSnapshotSwap extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
tenant_name: Args.string({
|
|
7
|
+
description: 'Tenant name to swap',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
static description = "Swap a tenant's live database to a snapshot. This repoints the tenant; the current live database is left untouched, so you can swap back. To roll back, swap to the original database name.";
|
|
12
|
+
static examples = [
|
|
13
|
+
`$ xano tenant snapshot swap t1234-abcd-xyz1 --snapshot t1234-abcd-xyz1_bk_20260603_203614
|
|
14
|
+
Swapped tenant t1234-abcd-xyz1 from t1234-abcd-xyz1 to t1234-abcd-xyz1_bk_20260603_203614
|
|
15
|
+
`,
|
|
16
|
+
`$ xano tenant snapshot swap t1234-abcd-xyz1 --snapshot t1234-abcd-xyz1 --force`,
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
...BaseCommand.baseFlags,
|
|
20
|
+
force: Flags.boolean({
|
|
21
|
+
char: 'f',
|
|
22
|
+
default: false,
|
|
23
|
+
description: 'Skips the confirmation prompt.',
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
26
|
+
output: Flags.string({
|
|
27
|
+
char: 'o',
|
|
28
|
+
default: 'summary',
|
|
29
|
+
description: 'Output format',
|
|
30
|
+
options: ['summary', 'json'],
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
snapshot: Flags.string({
|
|
34
|
+
description: 'Snapshot database name to swap to (use the original tenant name to roll back)',
|
|
35
|
+
required: true,
|
|
36
|
+
}),
|
|
37
|
+
workspace: Flags.string({
|
|
38
|
+
char: 'w',
|
|
39
|
+
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
40
|
+
required: false,
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
async run() {
|
|
44
|
+
const { args, flags } = await this.parse(TenantSnapshotSwap);
|
|
45
|
+
const { profile } = this.resolveProfile(flags);
|
|
46
|
+
const workspaceId = flags.workspace || profile.workspace;
|
|
47
|
+
if (!workspaceId) {
|
|
48
|
+
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
49
|
+
}
|
|
50
|
+
const tenantName = args.tenant_name;
|
|
51
|
+
const { snapshot } = flags;
|
|
52
|
+
if (!flags.force) {
|
|
53
|
+
const confirmed = await this.confirm(`Swap tenant ${tenantName} to "${snapshot}"? This repoints the tenant's live database.`);
|
|
54
|
+
if (!confirmed) {
|
|
55
|
+
this.log('Swap cancelled.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/snapshot/swap`;
|
|
60
|
+
try {
|
|
61
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
62
|
+
body: JSON.stringify({ snapshot }),
|
|
63
|
+
headers: {
|
|
64
|
+
'accept': 'application/json',
|
|
65
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
method: 'POST',
|
|
69
|
+
}, flags.verbose, profile.access_token);
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const errorText = await response.text();
|
|
72
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
73
|
+
}
|
|
74
|
+
const result = (await response.json());
|
|
75
|
+
if (flags.output === 'json') {
|
|
76
|
+
this.log(JSON.stringify(result, null, 2));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this.log(`Swapped tenant ${tenantName} from ${result.from ?? 'unknown'} to ${result.to ?? snapshot}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof Error) {
|
|
84
|
+
this.error(`Failed to swap snapshot: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.error(`Failed to swap snapshot: ${String(error)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async confirm(message) {
|
|
92
|
+
const rl = readline.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stdout,
|
|
95
|
+
});
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
rl.question(`${message} (y/N) `, (answer) => {
|
|
98
|
+
rl.close();
|
|
99
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -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>;
|
|
@@ -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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input assembly for `xano function run`.
|
|
3
|
+
*
|
|
4
|
+
* A run's `input` is a single JSON object sent to the Metadata API. Users can
|
|
5
|
+
* build that object from several sources, which this module normalizes and
|
|
6
|
+
* merges:
|
|
7
|
+
*
|
|
8
|
+
* --json <inline | @file | -> a base JSON object
|
|
9
|
+
* --data key=value a string field (httpie convention)
|
|
10
|
+
* --data key:=<json> a raw-JSON field (numbers, bools, arrays, ...)
|
|
11
|
+
* --data key@<file> a field whose value is a file's contents (string)
|
|
12
|
+
*
|
|
13
|
+
* Merge order is: JSON base first, then --data pairs override — so the common
|
|
14
|
+
* "reuse a payload file, tweak one field" flow works without editing the file.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is pure and free of oclif/network deps so it is unit-testable
|
|
17
|
+
* in isolation; the command layer handles fetching, prompting and output.
|
|
18
|
+
*/
|
|
19
|
+
export type JsonObject = Record<string, unknown>;
|
|
20
|
+
/** A single declared function input parameter, as returned by the meta API. */
|
|
21
|
+
export interface FunctionInputParam {
|
|
22
|
+
children?: FunctionInputParam[];
|
|
23
|
+
default?: unknown;
|
|
24
|
+
description?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
nullable?: boolean;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
style?: {
|
|
29
|
+
type?: string;
|
|
30
|
+
};
|
|
31
|
+
type?: string;
|
|
32
|
+
values?: unknown[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Split a `--data` token into its key, operator and value.
|
|
36
|
+
* Recognizes, in priority order: `:=` (raw json), `@` (file), `=` (string).
|
|
37
|
+
* The operator is the FIRST occurrence scanned left-to-right, but `:=` is
|
|
38
|
+
* detected before a bare `=` at the same position so `k:=1` is raw-json, not
|
|
39
|
+
* a string key `k:` set to `=1`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function splitDataToken(token: string): {
|
|
42
|
+
key: string;
|
|
43
|
+
op: ':=' | '=' | '@';
|
|
44
|
+
value: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Parse `--data` tokens into an object, applying httpie-style typing.
|
|
48
|
+
* Later tokens override earlier ones with the same key.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseDataPairs(pairs: string[], readFile?: (p: string) => string): JsonObject;
|
|
51
|
+
/**
|
|
52
|
+
* Load the `--json` source: inline JSON, `@file`, or `-` (stdin, supplied by
|
|
53
|
+
* the caller via `stdin`). Must parse to a plain object; arrays/scalars are
|
|
54
|
+
* rejected because a function's input is always a keyed object.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadJsonSource(source: string | undefined, opts?: {
|
|
57
|
+
readFile?: (p: string) => string;
|
|
58
|
+
stdin?: () => string;
|
|
59
|
+
}): JsonObject | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Merge a JSON base with --data overrides into the final input object.
|
|
62
|
+
* Precedence: base first, then dataPairs override (shallow).
|
|
63
|
+
*/
|
|
64
|
+
export declare function assembleInput(opts: {
|
|
65
|
+
dataPairs?: JsonObject;
|
|
66
|
+
jsonBase?: JsonObject;
|
|
67
|
+
}): JsonObject;
|
|
68
|
+
/**
|
|
69
|
+
* Validate a supplied input object against a function's declared input schema.
|
|
70
|
+
* Non-destructive: returns lists the caller can act on (prompt for missing
|
|
71
|
+
* required params on a TTY, warn on enum mismatches, etc.). The server remains
|
|
72
|
+
* the source of truth, so type mismatches are warnings, not hard failures.
|
|
73
|
+
*/
|
|
74
|
+
export declare function validateAgainstSchema(input: JsonObject, schema: FunctionInputParam[] | undefined): {
|
|
75
|
+
missingRequired: FunctionInputParam[];
|
|
76
|
+
warnings: string[];
|
|
77
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Split a `--data` token into its key, operator and value.
|
|
4
|
+
* Recognizes, in priority order: `:=` (raw json), `@` (file), `=` (string).
|
|
5
|
+
* The operator is the FIRST occurrence scanned left-to-right, but `:=` is
|
|
6
|
+
* detected before a bare `=` at the same position so `k:=1` is raw-json, not
|
|
7
|
+
* a string key `k:` set to `=1`.
|
|
8
|
+
*/
|
|
9
|
+
export function splitDataToken(token) {
|
|
10
|
+
// Find the earliest `=` and `@`; `:=` is an `=` preceded by `:`.
|
|
11
|
+
const eq = token.indexOf('=');
|
|
12
|
+
const at = token.indexOf('@');
|
|
13
|
+
// Neither operator present -> invalid.
|
|
14
|
+
if (eq === -1 && at === -1) {
|
|
15
|
+
throw new Error(`Invalid --data '${token}'. Expected key=value, key:=json, or key@file.`);
|
|
16
|
+
}
|
|
17
|
+
// Choose whichever operator appears first. On a tie an '=' can't tie with '@'
|
|
18
|
+
// at the same index, so a simple "smallest index wins" is unambiguous.
|
|
19
|
+
const useAt = at !== -1 && (eq === -1 || at < eq);
|
|
20
|
+
if (useAt) {
|
|
21
|
+
const key = token.slice(0, at);
|
|
22
|
+
if (!key)
|
|
23
|
+
throw new Error(`Invalid --data '${token}': missing key before '@'.`);
|
|
24
|
+
return { key, op: '@', value: token.slice(at + 1) };
|
|
25
|
+
}
|
|
26
|
+
// `=` path — check for the `:=` raw-json form (a ':' immediately before '=').
|
|
27
|
+
if (eq > 0 && token[eq - 1] === ':') {
|
|
28
|
+
const key = token.slice(0, eq - 1);
|
|
29
|
+
if (!key)
|
|
30
|
+
throw new Error(`Invalid --data '${token}': missing key before ':='.`);
|
|
31
|
+
return { key, op: ':=', value: token.slice(eq + 1) };
|
|
32
|
+
}
|
|
33
|
+
const key = token.slice(0, eq);
|
|
34
|
+
if (!key)
|
|
35
|
+
throw new Error(`Invalid --data '${token}': missing key before '='.`);
|
|
36
|
+
return { key, op: '=', value: token.slice(eq + 1) };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse `--data` tokens into an object, applying httpie-style typing.
|
|
40
|
+
* Later tokens override earlier ones with the same key.
|
|
41
|
+
*/
|
|
42
|
+
export function parseDataPairs(pairs, readFile = defaultReadFile) {
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const token of pairs) {
|
|
45
|
+
const { key, op, value } = splitDataToken(token);
|
|
46
|
+
switch (op) {
|
|
47
|
+
case ':=': {
|
|
48
|
+
try {
|
|
49
|
+
out[key] = JSON.parse(value);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new Error(`Invalid JSON for --data '${key}:=': ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case '@': {
|
|
57
|
+
out[key] = readFile(value);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
default: {
|
|
61
|
+
out[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load the `--json` source: inline JSON, `@file`, or `-` (stdin, supplied by
|
|
69
|
+
* the caller via `stdin`). Must parse to a plain object; arrays/scalars are
|
|
70
|
+
* rejected because a function's input is always a keyed object.
|
|
71
|
+
*/
|
|
72
|
+
export function loadJsonSource(source, opts = {}) {
|
|
73
|
+
if (source === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const readFile = opts.readFile ?? defaultReadFile;
|
|
76
|
+
let raw;
|
|
77
|
+
if (source === '-') {
|
|
78
|
+
if (!opts.stdin)
|
|
79
|
+
throw new Error('stdin reader not available for --json -');
|
|
80
|
+
raw = opts.stdin();
|
|
81
|
+
}
|
|
82
|
+
else if (source.startsWith('@')) {
|
|
83
|
+
raw = readFile(source.slice(1));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
raw = source;
|
|
87
|
+
}
|
|
88
|
+
if (raw.trim() === '')
|
|
89
|
+
return undefined;
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(raw);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new Error(`Invalid JSON input: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
98
|
+
throw new Error('Function input must be a JSON object (e.g. {"name":"..."}), not an array or scalar.');
|
|
99
|
+
}
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Merge a JSON base with --data overrides into the final input object.
|
|
104
|
+
* Precedence: base first, then dataPairs override (shallow).
|
|
105
|
+
*/
|
|
106
|
+
export function assembleInput(opts) {
|
|
107
|
+
return { ...opts.jsonBase, ...opts.dataPairs };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate a supplied input object against a function's declared input schema.
|
|
111
|
+
* Non-destructive: returns lists the caller can act on (prompt for missing
|
|
112
|
+
* required params on a TTY, warn on enum mismatches, etc.). The server remains
|
|
113
|
+
* the source of truth, so type mismatches are warnings, not hard failures.
|
|
114
|
+
*/
|
|
115
|
+
export function validateAgainstSchema(input, schema) {
|
|
116
|
+
const missingRequired = [];
|
|
117
|
+
const warnings = [];
|
|
118
|
+
if (!schema)
|
|
119
|
+
return { missingRequired, warnings };
|
|
120
|
+
for (const param of schema) {
|
|
121
|
+
const present = Object.hasOwn(input, param.name);
|
|
122
|
+
if (!present) {
|
|
123
|
+
// A required param with no default must be supplied.
|
|
124
|
+
if (param.required && param.default === undefined) {
|
|
125
|
+
missingRequired.push(param);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const value = input[param.name];
|
|
130
|
+
if (value === null && param.nullable === false) {
|
|
131
|
+
warnings.push(`Input '${param.name}' is null but the parameter is not nullable.`);
|
|
132
|
+
}
|
|
133
|
+
// Enum membership check (only for non-null scalar values).
|
|
134
|
+
if (Array.isArray(param.values) && param.values.length > 0 && value !== null && !param.values.includes(value)) {
|
|
135
|
+
warnings.push(`Input '${param.name}' = ${JSON.stringify(value)} is not one of the allowed values: ` +
|
|
136
|
+
param.values.map((v) => JSON.stringify(v)).join(', '));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { missingRequired, warnings };
|
|
140
|
+
}
|
|
141
|
+
function defaultReadFile(p) {
|
|
142
|
+
try {
|
|
143
|
+
return fs.readFileSync(p, 'utf8');
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new Error(`Failed to read file '${p}': ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|