@xano/cli 1.0.6-beta.1 → 1.0.6-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 +96 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
- package/dist/commands/ephemeral/impersonate/index.js +11 -3
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +0 -6
- package/dist/commands/ephemeral/pull/index.js +10 -95
- package/dist/commands/ephemeral/push/index.d.ts +10 -0
- package/dist/commands/ephemeral/push/index.js +77 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/build/list/index.js +19 -40
- package/dist/commands/ephemeral/static_host/list/index.d.ts +2 -3
- package/dist/commands/ephemeral/static_host/list/index.js +20 -40
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +10 -95
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +68 -40
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +67 -36
- package/dist/commands/workspace/git/pull/index.js +53 -3
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -95
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +274 -1
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -5
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +83 -0
- package/dist/utils/multidoc-push.d.ts +12 -0
- package/dist/utils/multidoc-push.js +84 -0
- package/dist/utils/paging.d.ts +145 -0
- package/dist/utils/paging.js +199 -0
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +2762 -2550
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
|
|
3
4
|
export default class StaticHostBuildList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
static_host: Args.string({
|
|
@@ -20,13 +21,16 @@ Available builds:
|
|
|
20
21
|
- staging (ID: 2) - Status: completed
|
|
21
22
|
`,
|
|
22
23
|
`$ xano static_host:build:list default -w 40 --output json
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
{
|
|
25
|
+
"count": 1,
|
|
26
|
+
"page": 1,
|
|
27
|
+
"total": 12,
|
|
28
|
+
"items": [
|
|
29
|
+
{
|
|
30
|
+
"id": 52
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
30
34
|
`,
|
|
31
35
|
`$ xano static_host:build:list default -p staging -o json --page 2
|
|
32
36
|
[
|
|
@@ -46,16 +50,7 @@ Available builds:
|
|
|
46
50
|
options: ['summary', 'json'],
|
|
47
51
|
required: false,
|
|
48
52
|
}),
|
|
49
|
-
page:
|
|
50
|
-
default: 1,
|
|
51
|
-
description: 'Page number for pagination',
|
|
52
|
-
required: false,
|
|
53
|
-
}),
|
|
54
|
-
per_page: Flags.integer({
|
|
55
|
-
default: 50,
|
|
56
|
-
description: 'Number of results per page',
|
|
57
|
-
required: false,
|
|
58
|
-
}),
|
|
53
|
+
...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
|
|
59
54
|
workspace: Flags.string({
|
|
60
55
|
char: 'w',
|
|
61
56
|
description: 'Workspace ID (optional if set in profile)',
|
|
@@ -79,13 +74,7 @@ Available builds:
|
|
|
79
74
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
80
75
|
}
|
|
81
76
|
// Build query parameters
|
|
82
|
-
const queryParams = new URLSearchParams(
|
|
83
|
-
page: flags.page.toString(),
|
|
84
|
-
});
|
|
85
|
-
// Only add per_page if it's not the default value
|
|
86
|
-
if (flags.per_page !== 50) {
|
|
87
|
-
queryParams.append('per_page', flags.per_page.toString());
|
|
88
|
-
}
|
|
77
|
+
const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
|
|
89
78
|
// Construct the API URL
|
|
90
79
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build?${queryParams.toString()}`;
|
|
91
80
|
// Fetch builds from the API
|
|
@@ -101,24 +90,11 @@ Available builds:
|
|
|
101
90
|
const errorText = await response.text();
|
|
102
91
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
103
92
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
let builds;
|
|
107
|
-
if (Array.isArray(data)) {
|
|
108
|
-
builds = data;
|
|
109
|
-
}
|
|
110
|
-
else if (data && typeof data === 'object' && 'builds' in data && Array.isArray(data.builds)) {
|
|
111
|
-
builds = data.builds;
|
|
112
|
-
}
|
|
113
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
114
|
-
builds = data.items;
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
this.error('Unexpected API response format');
|
|
118
|
-
}
|
|
93
|
+
const list = normalizeListResponse(await response.json(), ['builds']);
|
|
94
|
+
const builds = list.items;
|
|
119
95
|
// Output results
|
|
120
96
|
if (flags.output === 'json') {
|
|
121
|
-
this.log(JSON.stringify(
|
|
97
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
|
|
122
98
|
}
|
|
123
99
|
else {
|
|
124
100
|
// summary format
|
|
@@ -137,6 +113,9 @@ Available builds:
|
|
|
137
113
|
}
|
|
138
114
|
}
|
|
139
115
|
}
|
|
116
|
+
const footer = formatPagingFooter(list, { noun: 'build', page: flags.page, tier: 'page-only-envelope' });
|
|
117
|
+
if (footer)
|
|
118
|
+
this.log(footer);
|
|
140
119
|
}
|
|
141
120
|
}
|
|
142
121
|
catch (error) {
|
|
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../../base-command.js';
|
|
5
|
+
import { normalizeListResponse } from '../../../../utils/paging.js';
|
|
5
6
|
/**
|
|
6
7
|
* Find the deployed `canonical` for a static host's env from a list of hosts.
|
|
7
8
|
* Returns null when the host isn't present or nothing is deployed to that env.
|
|
@@ -240,9 +241,13 @@ Pulled the compiled/served output instead of the original source
|
|
|
240
241
|
this.error(`No build is deployed to the '${env}' environment of static host '${staticHost}'`);
|
|
241
242
|
}
|
|
242
243
|
// 2. Page through builds to find the one matching the deployed canonical.
|
|
243
|
-
|
|
244
|
+
// per_page is not an input on this endpoint (the server fixes the page
|
|
245
|
+
// size), so it is not sent. Termination uses the response's own itemsTotal
|
|
246
|
+
// rather than "the page came back full", which would stop early and report
|
|
247
|
+
// a spurious not-found if the server page size ever differed.
|
|
248
|
+
let seen = 0;
|
|
244
249
|
for (let page = 1;; page++) {
|
|
245
|
-
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build?page=${page}
|
|
250
|
+
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build?page=${page}`;
|
|
246
251
|
// eslint-disable-next-line no-await-in-loop
|
|
247
252
|
const response = await this.verboseFetch(listUrl, {
|
|
248
253
|
headers: {
|
|
@@ -257,17 +262,15 @@ Pulled the compiled/served output instead of the original source
|
|
|
257
262
|
this.error(`Failed to list builds: ${response.status} ${response.statusText}\n${errorText}`);
|
|
258
263
|
}
|
|
259
264
|
// eslint-disable-next-line no-await-in-loop
|
|
260
|
-
const
|
|
261
|
-
const builds =
|
|
262
|
-
? data
|
|
263
|
-
: data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)
|
|
264
|
-
? data.items
|
|
265
|
-
: [];
|
|
265
|
+
const list = normalizeListResponse(await response.json(), ['builds']);
|
|
266
|
+
const builds = list.items;
|
|
266
267
|
const match = findBuildByCanonical(builds, canonical);
|
|
267
268
|
if (match) {
|
|
268
269
|
return String(match.id);
|
|
269
270
|
}
|
|
270
|
-
|
|
271
|
+
seen += builds.length;
|
|
272
|
+
const exhausted = builds.length === 0 || (list.itemsTotal !== undefined && seen >= list.itemsTotal);
|
|
273
|
+
if (exhausted) {
|
|
271
274
|
// Reached the last page without a match.
|
|
272
275
|
this.error(`Could not find the build deployed to '${env}' (canonical ${canonical}) in the build list for '${staticHost}'`);
|
|
273
276
|
}
|
|
@@ -4,10 +4,9 @@ export default class StaticHostList extends BaseCommand {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
static flags: {
|
|
7
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
7
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
10
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
11
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
12
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../utils/paging.js';
|
|
3
4
|
export default class StaticHostList extends BaseCommand {
|
|
4
5
|
static args = {};
|
|
5
6
|
static description = 'List all static hosts in a workspace from the Xano Metadata API';
|
|
@@ -15,13 +16,17 @@ Available static hosts:
|
|
|
15
16
|
- another-host (ID: 2)
|
|
16
17
|
`,
|
|
17
18
|
`$ xano static_host:list -w 40 --output json
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
{
|
|
20
|
+
"count": 1,
|
|
21
|
+
"page": 1,
|
|
22
|
+
"total": 3,
|
|
23
|
+
"items": [
|
|
24
|
+
{
|
|
25
|
+
"id": 1,
|
|
26
|
+
"name": "my-static-host"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
25
30
|
`,
|
|
26
31
|
`$ xano static_host:list -p staging -o json --page 2
|
|
27
32
|
[
|
|
@@ -41,16 +46,7 @@ Available static hosts:
|
|
|
41
46
|
options: ['summary', 'json'],
|
|
42
47
|
required: false,
|
|
43
48
|
}),
|
|
44
|
-
page:
|
|
45
|
-
default: 1,
|
|
46
|
-
description: 'Page number for pagination',
|
|
47
|
-
required: false,
|
|
48
|
-
}),
|
|
49
|
-
per_page: Flags.integer({
|
|
50
|
-
default: 50,
|
|
51
|
-
description: 'Number of results per page',
|
|
52
|
-
required: false,
|
|
53
|
-
}),
|
|
49
|
+
...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
|
|
54
50
|
workspace: Flags.string({
|
|
55
51
|
char: 'w',
|
|
56
52
|
description: 'Workspace ID (optional if set in profile)',
|
|
@@ -74,13 +70,7 @@ Available static hosts:
|
|
|
74
70
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
75
71
|
}
|
|
76
72
|
// Build query parameters
|
|
77
|
-
const queryParams = new URLSearchParams(
|
|
78
|
-
page: flags.page.toString(),
|
|
79
|
-
});
|
|
80
|
-
// Only add per_page if it's not the default value
|
|
81
|
-
if (flags.per_page !== 50) {
|
|
82
|
-
queryParams.append('per_page', flags.per_page.toString());
|
|
83
|
-
}
|
|
73
|
+
const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
|
|
84
74
|
// Construct the API URL
|
|
85
75
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?${queryParams.toString()}`;
|
|
86
76
|
// Fetch static hosts from the API
|
|
@@ -96,24 +86,11 @@ Available static hosts:
|
|
|
96
86
|
const errorText = await response.text();
|
|
97
87
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
98
88
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
let staticHosts;
|
|
102
|
-
if (Array.isArray(data)) {
|
|
103
|
-
staticHosts = data;
|
|
104
|
-
}
|
|
105
|
-
else if (data && typeof data === 'object' && 'static_hosts' in data && Array.isArray(data.static_hosts)) {
|
|
106
|
-
staticHosts = data.static_hosts;
|
|
107
|
-
}
|
|
108
|
-
else if (data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)) {
|
|
109
|
-
staticHosts = data.items;
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
this.error('Unexpected API response format');
|
|
113
|
-
}
|
|
89
|
+
const list = normalizeListResponse(await response.json(), ['static_hosts']);
|
|
90
|
+
const staticHosts = list.items;
|
|
114
91
|
// Output results
|
|
115
92
|
if (flags.output === 'json') {
|
|
116
|
-
this.log(JSON.stringify(
|
|
93
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
|
|
117
94
|
}
|
|
118
95
|
else {
|
|
119
96
|
// summary format
|
|
@@ -132,6 +109,9 @@ Available static hosts:
|
|
|
132
109
|
}
|
|
133
110
|
}
|
|
134
111
|
}
|
|
112
|
+
const footer = formatPagingFooter(list, { noun: 'static host', page: flags.page, tier: 'page-only-envelope' });
|
|
113
|
+
if (footer)
|
|
114
|
+
this.log(footer);
|
|
135
115
|
}
|
|
136
116
|
}
|
|
137
117
|
catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { normalizeListResponse } from '../../../utils/paging.js';
|
|
3
4
|
export default class StaticHostMigrate extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
static_host: Args.string({
|
|
@@ -85,9 +86,12 @@ Would migrate 3 v1 host(s): newsite, marketing, legacy
|
|
|
85
86
|
/** List all static hosts in the workspace (paginates until exhausted). */
|
|
86
87
|
async listHosts(profile, workspaceId, verbose) {
|
|
87
88
|
const hosts = [];
|
|
88
|
-
const perPage = 100;
|
|
89
89
|
for (let page = 1;; page++) {
|
|
90
|
-
|
|
90
|
+
// per_page is not an input on this endpoint — the server fixes the page
|
|
91
|
+
// size — so it is deliberately not sent. Termination uses the response's
|
|
92
|
+
// own itemsTotal rather than "the page came back full", which would skip
|
|
93
|
+
// hosts if the server page size ever differed from a hardcoded guess.
|
|
94
|
+
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?page=${page}`;
|
|
91
95
|
// eslint-disable-next-line no-await-in-loop
|
|
92
96
|
const response = await this.verboseFetch(listUrl, {
|
|
93
97
|
headers: {
|
|
@@ -100,14 +104,14 @@ Would migrate 3 v1 host(s): newsite, marketing, legacy
|
|
|
100
104
|
const errorText = await response.text(); // eslint-disable-line no-await-in-loop
|
|
101
105
|
this.error(`Failed to list static hosts: ${response.status} ${response.statusText}\n${errorText}`);
|
|
102
106
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (
|
|
107
|
+
const list = normalizeListResponse(await response.json(), ['static_hosts']); // eslint-disable-line no-await-in-loop
|
|
108
|
+
hosts.push(...list.items);
|
|
109
|
+
// Stop on the server's own total when it gives us one. Fall back to an
|
|
110
|
+
// empty page otherwise — one extra request, but it cannot under-collect
|
|
111
|
+
// and silently migrate a subset.
|
|
112
|
+
if (list.items.length === 0)
|
|
113
|
+
break;
|
|
114
|
+
if (list.itemsTotal !== undefined && hosts.length >= list.itemsTotal)
|
|
111
115
|
break;
|
|
112
116
|
}
|
|
113
117
|
return hosts;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantBackupList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
tenant_name: Args.string({
|
|
@@ -70,23 +71,21 @@ Backups for tenant t1234-abcd-xyz1:
|
|
|
70
71
|
this.error('Unexpected API response format');
|
|
71
72
|
}
|
|
72
73
|
if (flags.output === 'json') {
|
|
73
|
-
this.log(JSON.stringify(backups, null, 2));
|
|
74
|
+
this.log(JSON.stringify(buildPagingJson({ items: backups }, { tier: 'none' }), null, 2));
|
|
75
|
+
}
|
|
76
|
+
else if (backups.length === 0) {
|
|
77
|
+
this.log(`No backups found for tenant ${tenantName}`);
|
|
74
78
|
}
|
|
75
79
|
else {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let date = 'unknown';
|
|
83
|
-
if (backup.created_at) {
|
|
84
|
-
const d = new Date(backup.created_at);
|
|
85
|
-
date = Number.isNaN(d.getTime()) ? backup.created_at : d.toISOString().split('T')[0];
|
|
86
|
-
}
|
|
87
|
-
const desc = backup.description || 'No description';
|
|
88
|
-
this.log(` - #${backup.id} - ${desc} (${date})`);
|
|
80
|
+
this.log(`Backups for tenant ${tenantName}:`);
|
|
81
|
+
for (const backup of backups) {
|
|
82
|
+
let date = 'unknown';
|
|
83
|
+
if (backup.created_at) {
|
|
84
|
+
const d = new Date(backup.created_at);
|
|
85
|
+
date = Number.isNaN(d.getTime()) ? backup.created_at : d.toISOString().split('T')[0];
|
|
89
86
|
}
|
|
87
|
+
const desc = backup.description || 'No description';
|
|
88
|
+
this.log(` - #${backup.id} - ${desc} (${date})`);
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantClusterList extends BaseCommand {
|
|
4
5
|
static description = 'List all tenant clusters';
|
|
5
6
|
static examples = [
|
|
@@ -48,18 +49,16 @@ Tenant clusters:
|
|
|
48
49
|
this.error('Unexpected API response format');
|
|
49
50
|
}
|
|
50
51
|
if (flags.output === 'json') {
|
|
51
|
-
this.log(JSON.stringify(clusters, null, 2));
|
|
52
|
+
this.log(JSON.stringify(buildPagingJson({ items: clusters }, { tier: 'none' }), null, 2));
|
|
53
|
+
}
|
|
54
|
+
else if (clusters.length === 0) {
|
|
55
|
+
this.log('No tenant clusters found');
|
|
52
56
|
}
|
|
53
57
|
else {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.log('Tenant clusters:');
|
|
59
|
-
for (const cluster of clusters) {
|
|
60
|
-
const type = cluster.type ? ` (${cluster.type})` : '';
|
|
61
|
-
this.log(` - ${cluster.name}${type} [ID: ${cluster.id}]`);
|
|
62
|
-
}
|
|
58
|
+
this.log('Tenant clusters:');
|
|
59
|
+
for (const cluster of clusters) {
|
|
60
|
+
const type = cluster.type ? ` (${cluster.type})` : '';
|
|
61
|
+
this.log(` - ${cluster.name}${type} [ID: ${cluster.id}]`);
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { formatPagingFooter } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantEnvList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
tenant_name: Args.string({
|
|
@@ -68,6 +69,9 @@ Environment variables for tenant my-tenant:
|
|
|
68
69
|
this.log(` - ${envVar.name}`);
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
const footer = formatPagingFooter({ items: envVars }, { noun: 'environment variable', tier: 'none' });
|
|
73
|
+
if (footer)
|
|
74
|
+
this.log(footer);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { buildPagingJson } from '../../../utils/paging.js';
|
|
3
4
|
export default class TenantList extends BaseCommand {
|
|
4
5
|
static description = 'List all tenants in a workspace';
|
|
5
6
|
static examples = [
|
|
@@ -65,28 +66,26 @@ Tenants in workspace 5:
|
|
|
65
66
|
this.error('Unexpected API response format');
|
|
66
67
|
}
|
|
67
68
|
if (flags.output === 'json') {
|
|
68
|
-
this.log(JSON.stringify(tenants, null, 2));
|
|
69
|
+
this.log(JSON.stringify(buildPagingJson({ items: tenants }, { tier: 'none' }), null, 2));
|
|
70
|
+
}
|
|
71
|
+
else if (tenants.length === 0) {
|
|
72
|
+
this.log('No tenants found');
|
|
69
73
|
}
|
|
70
74
|
else {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (releaseName)
|
|
86
|
-
this.log(` Release: ${releaseName} (ID: ${releaseId})`);
|
|
87
|
-
if (tenant.platform?.name)
|
|
88
|
-
this.log(` Platform: ${tenant.platform.name}`);
|
|
89
|
-
}
|
|
75
|
+
this.log(`Tenants in workspace ${workspaceId}:`);
|
|
76
|
+
for (const tenant of tenants) {
|
|
77
|
+
const state = tenant.state ? ` [${tenant.state}]` : '';
|
|
78
|
+
const license = tenant.license ? ` - ${tenant.license}` : '';
|
|
79
|
+
const typeLabel = tenant.type && tenant.type !== 'standard' ? ` [${tenant.type}]` : '';
|
|
80
|
+
this.log(` - ${tenant.display || tenant.name} (${tenant.name})${state}${license}${typeLabel}`);
|
|
81
|
+
if (tenant.cluster?.name)
|
|
82
|
+
this.log(` Cluster: ${tenant.cluster.name}`);
|
|
83
|
+
const releaseName = typeof tenant.release === 'string' ? tenant.release : tenant.release?.name;
|
|
84
|
+
const releaseId = typeof tenant.release === 'object' ? tenant.release?.id : undefined;
|
|
85
|
+
if (releaseName)
|
|
86
|
+
this.log(` Release: ${releaseName} (ID: ${releaseId})`);
|
|
87
|
+
if (tenant.platform?.name)
|
|
88
|
+
this.log(` Platform: ${tenant.platform.name}`);
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -14,10 +14,4 @@ export default class Pull extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Sanitize a document name for use as a filename.
|
|
19
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
20
|
-
* characters that are unsafe in filenames.
|
|
21
|
-
*/
|
|
22
|
-
private sanitizeFilename;
|
|
23
17
|
}
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class Pull extends BaseCommand {
|
|
8
8
|
static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
|
|
9
9
|
static examples = [
|
|
@@ -129,96 +129,19 @@ Pulled 58 documents from tenant my-tenant
|
|
|
129
129
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
130
130
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
131
131
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
132
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
133
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
134
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
132
135
|
// Track filenames per type to handle duplicates
|
|
133
136
|
const filenameCounters = new Map();
|
|
134
137
|
let writtenCount = 0;
|
|
135
138
|
for (const doc of documents) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
else if (doc.type === 'workspace_trigger') {
|
|
144
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
145
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
146
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
147
|
-
}
|
|
148
|
-
else if (doc.type === 'error_trigger') {
|
|
149
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
150
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
151
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
152
|
-
}
|
|
153
|
-
else if (doc.type === 'agent') {
|
|
154
|
-
// agent → ai/agent/{name}.xs
|
|
155
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
156
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
157
|
-
}
|
|
158
|
-
else if (doc.type === 'mcp_server') {
|
|
159
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
160
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
161
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
162
|
-
}
|
|
163
|
-
else if (doc.type === 'tool') {
|
|
164
|
-
// tool → ai/tool/{name}.xs
|
|
165
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
166
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
167
|
-
}
|
|
168
|
-
else if (doc.type === 'agent_trigger') {
|
|
169
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
170
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
171
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
172
|
-
}
|
|
173
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
174
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
175
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
176
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
177
|
-
}
|
|
178
|
-
else if (doc.type === 'table_trigger') {
|
|
179
|
-
// table_trigger → table/trigger/{name}.xs
|
|
180
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
181
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
182
|
-
}
|
|
183
|
-
else if (doc.type === 'realtime_channel') {
|
|
184
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
185
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
186
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
187
|
-
}
|
|
188
|
-
else if (doc.type === 'realtime_trigger') {
|
|
189
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
190
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
191
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
-
}
|
|
193
|
-
else if (doc.type === 'api_group') {
|
|
194
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
195
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
196
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
197
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
198
|
-
}
|
|
199
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
200
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
201
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
202
|
-
const nameParts = doc.name.split('/');
|
|
203
|
-
const leafName = nameParts.pop();
|
|
204
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
205
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
206
|
-
baseName = this.sanitizeFilename(leafName);
|
|
207
|
-
if (doc.verb) {
|
|
208
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
// Default: split folder path from name
|
|
213
|
-
const nameParts = doc.name.split('/');
|
|
214
|
-
const leafName = nameParts.pop();
|
|
215
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
216
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
217
|
-
baseName = this.sanitizeFilename(leafName);
|
|
218
|
-
if (doc.verb) {
|
|
219
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
139
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
140
|
+
getApiGroupFolder,
|
|
141
|
+
getChannelServer,
|
|
142
|
+
join: path.join,
|
|
143
|
+
snakeCase,
|
|
144
|
+
});
|
|
222
145
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
223
146
|
// Track duplicates per directory
|
|
224
147
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -237,12 +160,4 @@ Pulled 58 documents from tenant my-tenant
|
|
|
237
160
|
}
|
|
238
161
|
this.log(`Pulled ${writtenCount} documents from tenant ${tenantName} to ${flags.directory}`);
|
|
239
162
|
}
|
|
240
|
-
/**
|
|
241
|
-
* Sanitize a document name for use as a filename.
|
|
242
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
243
|
-
* characters that are unsafe in filenames.
|
|
244
|
-
*/
|
|
245
|
-
sanitizeFilename(name) {
|
|
246
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
247
|
-
}
|
|
248
163
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, formatPagingFooter } from '../../../../utils/paging.js';
|
|
3
4
|
export default class TenantSnapshotList extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
tenant_name: Args.string({
|
|
@@ -64,7 +65,7 @@ Snapshots for tenant t1234-abcd-xyz1:
|
|
|
64
65
|
this.error('Unexpected API response format');
|
|
65
66
|
}
|
|
66
67
|
if (flags.output === 'json') {
|
|
67
|
-
this.log(JSON.stringify(snapshots, null, 2));
|
|
68
|
+
this.log(JSON.stringify(buildPagingJson({ items: snapshots }, { tier: 'none' }), null, 2));
|
|
68
69
|
}
|
|
69
70
|
else if (snapshots.length === 0) {
|
|
70
71
|
this.log(`No snapshots found for tenant ${tenantName}`);
|
|
@@ -82,6 +83,9 @@ Snapshots for tenant t1234-abcd-xyz1:
|
|
|
82
83
|
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
83
84
|
this.log(` - ${snapshot.name} (${meta})${tagStr}`);
|
|
84
85
|
}
|
|
86
|
+
const footer = formatPagingFooter({ items: snapshots }, { noun: 'snapshot', tier: 'none' });
|
|
87
|
+
if (footer)
|
|
88
|
+
this.log(footer);
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
catch (error) {
|
|
@@ -3,11 +3,13 @@ export default class TenantUnitTestList extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
per_page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
10
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
11
|
'obj-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
12
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
-
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
13
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
14
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
15
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|