@xano/cli 1.1.0 → 1.3.0
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 +155 -4
- package/dist/base-command.d.ts +43 -36
- package/dist/base-command.js +223 -160
- package/dist/commands/auth/index.d.ts +1 -1
- package/dist/commands/auth/index.js +7 -7
- package/dist/commands/branch/create/index.d.ts +1 -1
- package/dist/commands/branch/create/index.js +1 -1
- package/dist/commands/ephemeral/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/create/index.js +103 -0
- package/dist/commands/ephemeral/delete/index.d.ts +18 -0
- package/dist/commands/ephemeral/delete/index.js +99 -0
- package/dist/commands/ephemeral/edit/index.d.ts +18 -0
- package/dist/commands/ephemeral/edit/index.js +109 -0
- package/dist/commands/ephemeral/get/index.d.ts +21 -0
- package/dist/commands/ephemeral/get/index.js +125 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
- package/dist/commands/ephemeral/impersonate/index.js +120 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +123 -0
- package/dist/commands/ephemeral/pull/index.d.ts +19 -0
- package/dist/commands/ephemeral/pull/index.js +163 -0
- package/dist/commands/ephemeral/push/index.d.ts +35 -0
- package/dist/commands/ephemeral/push/index.js +223 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
- package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/create/index.js +92 -0
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
- package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/get/index.js +101 -0
- package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
- package/dist/commands/ephemeral/static_host/list/index.js +132 -0
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/create/index.js +1 -1
- package/dist/commands/function/edit/index.js +26 -32
- package/dist/commands/function/get/index.js +2 -3
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +19 -6
- package/dist/commands/profile/create/index.d.ts +1 -1
- package/dist/commands/profile/create/index.js +7 -7
- package/dist/commands/profile/me/index.js +1 -1
- package/dist/commands/profile/use/index.js +0 -4
- package/dist/commands/profile/wizard/index.d.ts +1 -1
- package/dist/commands/profile/wizard/index.js +12 -10
- package/dist/commands/profile/workspace/set/index.js +1 -1
- package/dist/commands/release/edit/index.js +2 -2
- package/dist/commands/release/export/index.js +1 -2
- package/dist/commands/release/import/index.js +1 -1
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +13 -99
- package/dist/commands/release/push/index.js +2 -2
- package/dist/commands/sandbox/env/get_all/index.js +1 -1
- package/dist/commands/sandbox/env/set_all/index.js +1 -1
- package/dist/commands/sandbox/get/index.js +1 -1
- package/dist/commands/sandbox/license/get/index.js +1 -1
- package/dist/commands/sandbox/license/set/index.js +1 -1
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +13 -81
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +5 -5
- package/dist/commands/sandbox/unit_test/run/index.js +17 -11
- package/dist/commands/sandbox/unit_test/run_all/index.js +9 -1
- package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
- package/dist/commands/sandbox/workflow_test/run_all/index.js +9 -1
- package/dist/commands/static_host/build/create/index.d.ts +1 -1
- package/dist/commands/static_host/build/create/index.js +2 -2
- package/dist/commands/static_host/build/pull/index.js +1 -1
- package/dist/commands/static_host/build/push/index.js +1 -1
- package/dist/commands/static_host/edit/index.js +1 -1
- package/dist/commands/tenant/backup/export/index.js +1 -2
- package/dist/commands/tenant/backup/import/index.js +1 -1
- package/dist/commands/tenant/cluster/create/index.js +1 -1
- package/dist/commands/tenant/cluster/license/get/index.js +1 -1
- package/dist/commands/tenant/cluster/license/set/index.js +1 -1
- package/dist/commands/tenant/create/index.d.ts +1 -1
- package/dist/commands/tenant/create/index.js +6 -6
- package/dist/commands/tenant/deploy_platform/index.js +1 -1
- package/dist/commands/tenant/edit/index.js +7 -7
- package/dist/commands/tenant/env/get_all/index.js +1 -1
- package/dist/commands/tenant/env/set_all/index.js +1 -1
- package/dist/commands/tenant/impersonate/index.d.ts +1 -1
- package/dist/commands/tenant/impersonate/index.js +14 -14
- package/dist/commands/tenant/license/get/index.js +1 -1
- package/dist/commands/tenant/license/set/index.js +1 -1
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +14 -100
- package/dist/commands/tenant/unit_test/run/index.js +19 -11
- package/dist/commands/tenant/unit_test/run_all/index.js +13 -1
- package/dist/commands/tenant/workflow_test/run/index.js +14 -9
- package/dist/commands/tenant/workflow_test/run_all/index.js +13 -1
- package/dist/commands/unit_test/run/index.js +19 -11
- package/dist/commands/unit_test/run_all/index.js +13 -1
- package/dist/commands/workflow_test/run/index.js +15 -10
- package/dist/commands/workflow_test/run_all/index.js +15 -3
- package/dist/commands/workspace/git/pull/index.d.ts +4 -4
- package/dist/commands/workspace/git/pull/index.js +158 -97
- package/dist/commands/workspace/pull/index.d.ts +1 -7
- package/dist/commands/workspace/pull/index.js +17 -103
- package/dist/help.js +4 -2
- package/dist/update-check.d.ts +1 -1
- package/dist/update-check.js +1 -1
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +311 -4
- 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 -6
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +84 -0
- package/dist/utils/multidoc-push.d.ts +88 -0
- package/dist/utils/multidoc-push.js +378 -15
- package/dist/utils/paging.d.ts +2 -1
- package/dist/utils/paging.js +0 -1
- package/dist/utils/reference-checker.js +14 -3
- package/oclif.manifest.json +5898 -3804
- package/package.json +10 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../../base-command.js';
|
|
3
|
+
import { buildPagingJson, buildPagingParams, formatPagingFooter, normalizeListResponse, pagingFlags } from '../../../../utils/paging.js';
|
|
4
|
+
export default class EphemeralStaticHostList extends BaseCommand {
|
|
5
|
+
static args = {
|
|
6
|
+
tenant_name: Args.string({
|
|
7
|
+
description: 'Ephemeral tenant name',
|
|
8
|
+
required: true,
|
|
9
|
+
}),
|
|
10
|
+
};
|
|
11
|
+
static description = 'List all static hosts for an ephemeral tenant from the Xano Metadata API';
|
|
12
|
+
static examples = [
|
|
13
|
+
`$ xano ephemeral static_host list e4f2-9ab1-xyz1 -w 40
|
|
14
|
+
Available static hosts:
|
|
15
|
+
- my-static-host (ID: 1)
|
|
16
|
+
- another-host (ID: 2)
|
|
17
|
+
`,
|
|
18
|
+
`$ xano ephemeral static_host list e4f2-9ab1-xyz1 --profile production
|
|
19
|
+
Available static hosts:
|
|
20
|
+
- my-static-host (ID: 1)
|
|
21
|
+
- another-host (ID: 2)
|
|
22
|
+
`,
|
|
23
|
+
`$ xano ephemeral static_host list e4f2-9ab1-xyz1 -w 40 --output json
|
|
24
|
+
{
|
|
25
|
+
"count": 1,
|
|
26
|
+
"page": 1,
|
|
27
|
+
"total": 3,
|
|
28
|
+
"items": [
|
|
29
|
+
{
|
|
30
|
+
"id": 1,
|
|
31
|
+
"name": "my-static-host"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
`$ xano ephemeral static_host list e4f2-9ab1-xyz1 -p staging -o json --page 2
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
"id": 3,
|
|
40
|
+
"name": "static-host-3"
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
`,
|
|
44
|
+
];
|
|
45
|
+
static flags = {
|
|
46
|
+
...BaseCommand.baseFlags,
|
|
47
|
+
output: Flags.string({
|
|
48
|
+
char: 'o',
|
|
49
|
+
default: 'summary',
|
|
50
|
+
description: 'Output format',
|
|
51
|
+
options: ['summary', 'json'],
|
|
52
|
+
required: false,
|
|
53
|
+
}),
|
|
54
|
+
...pagingFlags('page-only-envelope', { fixedPerPage: 100 }),
|
|
55
|
+
workspace: Flags.string({
|
|
56
|
+
char: 'w',
|
|
57
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
58
|
+
required: false,
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
async run() {
|
|
62
|
+
const { args, flags } = await this.parse(EphemeralStaticHostList);
|
|
63
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
64
|
+
const tenantName = args.tenant_name;
|
|
65
|
+
// Determine workspace_id from flag or profile
|
|
66
|
+
let workspaceId;
|
|
67
|
+
if (flags.workspace) {
|
|
68
|
+
workspaceId = flags.workspace;
|
|
69
|
+
}
|
|
70
|
+
else if (profile.workspace) {
|
|
71
|
+
workspaceId = profile.workspace;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
75
|
+
` 1. Provide it as a flag: xano ephemeral static_host list <tenant_name> -w <workspace_id>\n` +
|
|
76
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
77
|
+
}
|
|
78
|
+
// Build query parameters
|
|
79
|
+
const queryParams = new URLSearchParams(buildPagingParams(flags, 'page-only-envelope'));
|
|
80
|
+
// Construct the API URL
|
|
81
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/static_host?${queryParams.toString()}`;
|
|
82
|
+
// Fetch static hosts from the API
|
|
83
|
+
try {
|
|
84
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
85
|
+
headers: {
|
|
86
|
+
'accept': 'application/json',
|
|
87
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
88
|
+
},
|
|
89
|
+
method: 'GET',
|
|
90
|
+
}, flags.verbose, profile.access_token);
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const errorText = await response.text();
|
|
93
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
94
|
+
}
|
|
95
|
+
const list = normalizeListResponse(await response.json(), ['static_hosts']);
|
|
96
|
+
const staticHosts = list.items;
|
|
97
|
+
// Output results
|
|
98
|
+
if (flags.output === 'json') {
|
|
99
|
+
this.log(JSON.stringify(buildPagingJson(list, { page: flags.page, tier: 'page-only-envelope' }), null, 2));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// summary format
|
|
103
|
+
if (staticHosts.length === 0) {
|
|
104
|
+
this.log('No static hosts found');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.log('Available static hosts:');
|
|
108
|
+
for (const host of staticHosts) {
|
|
109
|
+
if (host.id === undefined) {
|
|
110
|
+
this.log(` - ${host.name}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const domainInfo = host.domain ? ` - ${host.domain}` : '';
|
|
114
|
+
this.log(` - ${host.name} (ID: ${host.id})${domainInfo}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const footer = formatPagingFooter(list, { noun: 'static host', page: flags.page, tier: 'page-only-envelope' });
|
|
119
|
+
if (footer)
|
|
120
|
+
this.log(footer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof Error) {
|
|
125
|
+
this.error(`Failed to fetch static hosts: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
this.error(`Failed to fetch static hosts: ${String(error)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* Split a multi-document `.xs` bundle into the standard one-document-per-file
|
|
4
|
+
* layout that the push pipeline requires.
|
|
5
|
+
*
|
|
6
|
+
* A hand-authored bundle keeps every object (workspace, tables, queries,
|
|
7
|
+
* microservices, …) in a single file joined by `---`. That is convenient to
|
|
8
|
+
* edit but unsupported by `push`: the partial-diff filter and GUID writeback
|
|
9
|
+
* both parse only the first document in a file, so such a bundle silently
|
|
10
|
+
* pushes nothing (partial mode) or corrupts GUIDs (full mode). `flatten` turns
|
|
11
|
+
* it back into the exact tree `pull` would have produced, using the shared
|
|
12
|
+
* placement logic so the two never drift.
|
|
13
|
+
*
|
|
14
|
+
* Not a BaseCommand: this is a purely local, offline transform — no profile,
|
|
15
|
+
* token, or network — so it extends oclif's Command directly.
|
|
16
|
+
*/
|
|
17
|
+
export default class Flatten extends Command {
|
|
18
|
+
static args: {
|
|
19
|
+
input: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
20
|
+
};
|
|
21
|
+
static description: string;
|
|
22
|
+
static examples: string[];
|
|
23
|
+
static flags: {
|
|
24
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
25
|
+
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
26
|
+
'keep-source': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
27
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
|
+
};
|
|
29
|
+
run(): Promise<void>;
|
|
30
|
+
/** Recursively collect `.xs` files under a dir that actually hold >1 document. */
|
|
31
|
+
private collectMultiDocFiles;
|
|
32
|
+
/** Flatten a single bundle file. Returns the number of documents written. */
|
|
33
|
+
private flattenOne;
|
|
34
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Args, Command, Flags, ux } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
4
|
+
import { flattenBundleFile } from '../../utils/flatten.js';
|
|
5
|
+
/**
|
|
6
|
+
* Split a multi-document `.xs` bundle into the standard one-document-per-file
|
|
7
|
+
* layout that the push pipeline requires.
|
|
8
|
+
*
|
|
9
|
+
* A hand-authored bundle keeps every object (workspace, tables, queries,
|
|
10
|
+
* microservices, …) in a single file joined by `---`. That is convenient to
|
|
11
|
+
* edit but unsupported by `push`: the partial-diff filter and GUID writeback
|
|
12
|
+
* both parse only the first document in a file, so such a bundle silently
|
|
13
|
+
* pushes nothing (partial mode) or corrupts GUIDs (full mode). `flatten` turns
|
|
14
|
+
* it back into the exact tree `pull` would have produced, using the shared
|
|
15
|
+
* placement logic so the two never drift.
|
|
16
|
+
*
|
|
17
|
+
* Not a BaseCommand: this is a purely local, offline transform — no profile,
|
|
18
|
+
* token, or network — so it extends oclif's Command directly.
|
|
19
|
+
*/
|
|
20
|
+
export default class Flatten extends Command {
|
|
21
|
+
static args = {
|
|
22
|
+
input: Args.string({
|
|
23
|
+
description: 'A multi-document .xs file, or a directory containing them, to split',
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
static description = 'Split a multi-document .xs bundle (documents joined by `---`) into the standard ' +
|
|
28
|
+
'one-document-per-file layout that push requires. Deletes the original bundle by ' +
|
|
29
|
+
'default (use --keep-source to keep it). Purely local — no network.';
|
|
30
|
+
static examples = [
|
|
31
|
+
`$ xano flatten secret/pdf-micro/multidoc.xs
|
|
32
|
+
Split the bundle in place into per-document files (removes multidoc.xs)`,
|
|
33
|
+
`$ xano flatten ./bundle.xs -o ./workspace
|
|
34
|
+
Write the split files under ./workspace instead of alongside the bundle`,
|
|
35
|
+
`$ xano flatten secret/pdf-micro/multidoc.xs --dry-run
|
|
36
|
+
Preview the file layout without writing anything`,
|
|
37
|
+
`$ xano flatten ./dir-of-bundles --keep-source
|
|
38
|
+
Flatten every multi-doc .xs under a directory, keeping the originals`,
|
|
39
|
+
];
|
|
40
|
+
static flags = {
|
|
41
|
+
'dry-run': Flags.boolean({
|
|
42
|
+
default: false,
|
|
43
|
+
description: 'Show the resulting file layout without writing anything',
|
|
44
|
+
}),
|
|
45
|
+
force: Flags.boolean({
|
|
46
|
+
char: 'f',
|
|
47
|
+
default: false,
|
|
48
|
+
description: 'Overwrite existing destination files instead of erroring',
|
|
49
|
+
}),
|
|
50
|
+
'keep-source': Flags.boolean({
|
|
51
|
+
default: false,
|
|
52
|
+
description: 'Keep the original multi-document file(s) (default: delete after a successful split)',
|
|
53
|
+
}),
|
|
54
|
+
output: Flags.string({
|
|
55
|
+
char: 'o',
|
|
56
|
+
description: 'Output directory (default: the directory each bundle file lives in)',
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
async run() {
|
|
60
|
+
const { args, flags } = await this.parse(Flatten);
|
|
61
|
+
const inputPath = resolve(args.input);
|
|
62
|
+
if (!fs.existsSync(inputPath)) {
|
|
63
|
+
this.error(`Not found: ${inputPath}`);
|
|
64
|
+
}
|
|
65
|
+
// Resolve the set of bundle files to flatten.
|
|
66
|
+
const bundleFiles = fs.statSync(inputPath).isDirectory()
|
|
67
|
+
? this.collectMultiDocFiles(inputPath)
|
|
68
|
+
: [inputPath];
|
|
69
|
+
if (bundleFiles.length === 0) {
|
|
70
|
+
this.error(fs.statSync(inputPath).isDirectory()
|
|
71
|
+
? `No multi-document .xs files found under ${inputPath}`
|
|
72
|
+
: `${inputPath} is not a .xs file`);
|
|
73
|
+
}
|
|
74
|
+
let totalWritten = 0;
|
|
75
|
+
for (const bundleFile of bundleFiles) {
|
|
76
|
+
totalWritten += this.flattenOne(bundleFile, flags);
|
|
77
|
+
}
|
|
78
|
+
if (flags['dry-run']) {
|
|
79
|
+
this.log('');
|
|
80
|
+
this.log(ux.colorize('dim', `Dry run — ${totalWritten} file(s) would be written. Nothing changed.`));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.log('');
|
|
84
|
+
this.log(`Flattened ${bundleFiles.length} bundle(s) into ${totalWritten} file(s).`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Recursively collect `.xs` files under a dir that actually hold >1 document. */
|
|
88
|
+
collectMultiDocFiles(dir) {
|
|
89
|
+
const out = [];
|
|
90
|
+
const walk = (d) => {
|
|
91
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
92
|
+
const full = join(d, entry.name);
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
walk(full);
|
|
95
|
+
}
|
|
96
|
+
else if (entry.isFile() && entry.name.endsWith('.xs')) {
|
|
97
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
98
|
+
if (/^---$/m.test(content))
|
|
99
|
+
out.push(full);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
walk(dir);
|
|
104
|
+
return out.sort();
|
|
105
|
+
}
|
|
106
|
+
/** Flatten a single bundle file. Returns the number of documents written. */
|
|
107
|
+
flattenOne(bundleFile, flags) {
|
|
108
|
+
const rel = relative(process.cwd(), bundleFile) || bundleFile;
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
// Buffer the plan so we can print a header before it only when there's work.
|
|
112
|
+
const planLines = [];
|
|
113
|
+
result = flattenBundleFile(bundleFile, {
|
|
114
|
+
dryRun: flags['dry-run'],
|
|
115
|
+
force: flags.force,
|
|
116
|
+
keepSource: flags['keep-source'],
|
|
117
|
+
log: (msg) => planLines.push(msg),
|
|
118
|
+
outputDir: flags.output,
|
|
119
|
+
});
|
|
120
|
+
if (result.skipped === 'empty') {
|
|
121
|
+
this.warn(`No parseable documents in ${rel}; skipping.`);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (result.skipped === 'single') {
|
|
125
|
+
this.warn(`${rel} holds a single document; nothing to split. Skipping.`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
this.log('');
|
|
129
|
+
this.log(ux.colorize('bold', `${rel} → ${result.written.length} documents`));
|
|
130
|
+
for (const l of planLines) {
|
|
131
|
+
// Colorize the WRITE marker; dim the incidental notes.
|
|
132
|
+
this.log(l.startsWith(' WRITE') ? l.replace('WRITE', ux.colorize('green', 'WRITE')) : ux.colorize('dim', l));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
this.error(error.message);
|
|
137
|
+
}
|
|
138
|
+
return result.written.length;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import * as os from 'node:os';
|
|
5
|
-
import
|
|
5
|
+
import path from 'node:path';
|
|
6
6
|
import BaseCommand from '../../../base-command.js';
|
|
7
7
|
export default class FunctionCreate extends BaseCommand {
|
|
8
8
|
static args = {};
|
|
@@ -3,7 +3,7 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import * as os from 'node:os';
|
|
6
|
-
import
|
|
6
|
+
import path from 'node:path';
|
|
7
7
|
import BaseCommand, { buildUserAgent } from '../../../base-command.js';
|
|
8
8
|
export default class FunctionEdit extends BaseCommand {
|
|
9
9
|
static args = {
|
|
@@ -120,8 +120,7 @@ Name: my_function
|
|
|
120
120
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
121
121
|
}
|
|
122
122
|
// If function_id is not provided, prompt user to select from list
|
|
123
|
-
|
|
124
|
-
functionId = args.function_id ? args.function_id : await this.promptForFunctionId(profile, workspaceId);
|
|
123
|
+
const functionId = args.function_id || (await this.promptForFunctionId(profile, workspaceId));
|
|
125
124
|
// Read XanoScript content
|
|
126
125
|
let xanoscript;
|
|
127
126
|
if (flags.file) {
|
|
@@ -221,14 +220,14 @@ Name: my_function
|
|
|
221
220
|
// Get the EDITOR environment variable
|
|
222
221
|
const editor = process.env.EDITOR || process.env.VISUAL;
|
|
223
222
|
if (!editor) {
|
|
224
|
-
this.error('No editor configured. Please set the EDITOR or VISUAL environment variable.\
|
|
223
|
+
this.error('No editor configured. Please set the EDITOR or VISUAL environment variable.\nExample: export EDITOR=vim');
|
|
225
224
|
}
|
|
226
225
|
// Validate editor executable exists
|
|
227
226
|
try {
|
|
228
227
|
execSync(`which ${editor.split(' ')[0]}`, { stdio: 'ignore' });
|
|
229
228
|
}
|
|
230
229
|
catch {
|
|
231
|
-
this.error(`Editor '${editor}' not found. Please set EDITOR to a valid editor.\
|
|
230
|
+
this.error(`Editor '${editor}' not found. Please set EDITOR to a valid editor.\nExample: export EDITOR=vim`);
|
|
232
231
|
}
|
|
233
232
|
// Read the original file
|
|
234
233
|
let originalContent;
|
|
@@ -321,35 +320,30 @@ Name: my_function
|
|
|
321
320
|
include_xanoscript: 'true',
|
|
322
321
|
});
|
|
323
322
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/${functionId}?${queryParams.toString()}`;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
323
|
+
const response = await fetch(apiUrl, {
|
|
324
|
+
headers: {
|
|
325
|
+
accept: 'application/json',
|
|
326
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
327
|
+
'User-Agent': buildUserAgent(this.config.version),
|
|
328
|
+
},
|
|
329
|
+
method: 'GET',
|
|
330
|
+
});
|
|
331
|
+
if (!response.ok) {
|
|
332
|
+
const errorText = await response.text();
|
|
333
|
+
throw new Error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
334
|
+
}
|
|
335
|
+
const result = (await response.json());
|
|
336
|
+
// Handle xanoscript as an object with status and value
|
|
337
|
+
if (result.xanoscript) {
|
|
338
|
+
if (result.xanoscript.status === 'ok' && result.xanoscript.value !== undefined) {
|
|
339
|
+
return result.xanoscript.value;
|
|
336
340
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (result.xanoscript) {
|
|
340
|
-
if (result.xanoscript.status === 'ok' && result.xanoscript.value !== undefined) {
|
|
341
|
-
return result.xanoscript.value;
|
|
342
|
-
}
|
|
343
|
-
if (typeof result.xanoscript === 'string') {
|
|
344
|
-
return result.xanoscript;
|
|
345
|
-
}
|
|
346
|
-
throw new Error(`Invalid xanoscript format: ${JSON.stringify(result.xanoscript)}`);
|
|
341
|
+
if (typeof result.xanoscript === 'string') {
|
|
342
|
+
return result.xanoscript;
|
|
347
343
|
}
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
catch (error) {
|
|
351
|
-
throw error;
|
|
344
|
+
throw new Error(`Invalid xanoscript format: ${JSON.stringify(result.xanoscript)}`);
|
|
352
345
|
}
|
|
346
|
+
return '';
|
|
353
347
|
}
|
|
354
348
|
async promptForFunctionId(profile, workspaceId) {
|
|
355
349
|
try {
|
|
@@ -365,9 +359,9 @@ Name: my_function
|
|
|
365
359
|
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
|
|
366
360
|
const response = await fetch(listUrl, {
|
|
367
361
|
headers: {
|
|
368
|
-
'User-Agent': buildUserAgent(this.config.version),
|
|
369
362
|
accept: 'application/json',
|
|
370
363
|
Authorization: `Bearer ${profile.access_token}`,
|
|
364
|
+
'User-Agent': buildUserAgent(this.config.version),
|
|
371
365
|
},
|
|
372
366
|
method: 'GET',
|
|
373
367
|
});
|
|
@@ -89,8 +89,7 @@ function yo {
|
|
|
89
89
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
90
90
|
}
|
|
91
91
|
// If function_id is not provided, prompt user to select from list
|
|
92
|
-
|
|
93
|
-
functionId = args.function_id ? args.function_id : await this.promptForFunctionId(profile, workspaceId);
|
|
92
|
+
const functionId = args.function_id || (await this.promptForFunctionId(profile, workspaceId));
|
|
94
93
|
// Build query parameters
|
|
95
94
|
// Automatically set include_xanoscript to true if output format is xs
|
|
96
95
|
const includeXanoscript = flags.output === 'xs' ? true : flags.include_xanoscript;
|
|
@@ -178,9 +177,9 @@ function yo {
|
|
|
178
177
|
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${queryParams.toString()}`;
|
|
179
178
|
const response = await fetch(listUrl, {
|
|
180
179
|
headers: {
|
|
181
|
-
'User-Agent': buildUserAgent(this.config.version),
|
|
182
180
|
accept: 'application/json',
|
|
183
181
|
Authorization: `Bearer ${profile.access_token}`,
|
|
182
|
+
'User-Agent': buildUserAgent(this.config.version),
|
|
184
183
|
},
|
|
185
184
|
method: 'GET',
|
|
186
185
|
});
|
|
@@ -8,6 +8,7 @@ export default class FunctionRun extends BaseCommand {
|
|
|
8
8
|
static flags: {
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
data: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
datasource: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
json: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
logs: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -17,6 +17,8 @@ export default class FunctionRun extends BaseCommand {
|
|
|
17
17
|
`$ xano function:run calcScore --json @payload.json --data env=staging`,
|
|
18
18
|
`$ echo '{"email":"jo@x.com"}' | xano function:run calcScore --stdin -o json | jq .result`,
|
|
19
19
|
`$ xano function:run calcScore --branch dev --logs`,
|
|
20
|
+
`$ xano function:run calcScore --datasource test
|
|
21
|
+
# Runs against the 'test' data source instead of 'live'`,
|
|
20
22
|
];
|
|
21
23
|
static flags = {
|
|
22
24
|
...BaseCommand.baseFlags,
|
|
@@ -30,6 +32,10 @@ export default class FunctionRun extends BaseCommand {
|
|
|
30
32
|
multiple: true,
|
|
31
33
|
required: false,
|
|
32
34
|
}),
|
|
35
|
+
datasource: Flags.string({
|
|
36
|
+
description: 'Data source to run against, e.g. test (defaults to live)',
|
|
37
|
+
required: false,
|
|
38
|
+
}),
|
|
33
39
|
json: Flags.string({
|
|
34
40
|
description: "Input as a JSON object: inline, @file.json, or '-' for stdin",
|
|
35
41
|
exclusive: ['stdin'],
|
|
@@ -106,13 +112,21 @@ export default class FunctionRun extends BaseCommand {
|
|
|
106
112
|
const body = { input, name: functionName };
|
|
107
113
|
if (branch)
|
|
108
114
|
body.branch = branch;
|
|
115
|
+
const headers = {
|
|
116
|
+
'accept': 'application/json',
|
|
117
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
};
|
|
120
|
+
// The Metadata API resolves the workspace's tables against the data source
|
|
121
|
+
// named by X-Data-Source (defaulting to `live`), so the run reads and writes
|
|
122
|
+
// the same tables a request carrying that header would. An unknown label is
|
|
123
|
+
// rejected server-side with "Invalid data source."
|
|
124
|
+
const dataSource = flags.datasource?.trim();
|
|
125
|
+
if (dataSource)
|
|
126
|
+
headers['X-Data-Source'] = dataSource;
|
|
109
127
|
const response = await this.verboseFetch(apiUrl, {
|
|
110
128
|
body: JSON.stringify(body),
|
|
111
|
-
headers
|
|
112
|
-
'accept': 'application/json',
|
|
113
|
-
'Authorization': `Bearer ${profile.access_token}`,
|
|
114
|
-
'Content-Type': 'application/json',
|
|
115
|
-
},
|
|
129
|
+
headers,
|
|
116
130
|
method: 'POST',
|
|
117
131
|
}, flags.verbose, profile.access_token);
|
|
118
132
|
if (!response.ok) {
|
|
@@ -153,7 +167,6 @@ export default class FunctionRun extends BaseCommand {
|
|
|
153
167
|
*/
|
|
154
168
|
async fetchFunctionByName(opts) {
|
|
155
169
|
const { branch, name, profile, verbose, workspaceId } = opts;
|
|
156
|
-
// eslint-disable-next-line camelcase -- external Metadata API query param
|
|
157
170
|
const params = new URLSearchParams({ per_page: '100', search: name });
|
|
158
171
|
if (branch)
|
|
159
172
|
params.set('branch', branch);
|
|
@@ -6,10 +6,10 @@ export default class ProfileCreate extends Command {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
-
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
9
|
access_token: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
10
|
account_origin: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
11
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
default: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
14
|
insecure: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
instance_origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import * as yaml from 'js-yaml';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
-
import
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
import { resolveCredentialsPath } from '../../../base-command.js';
|
|
6
6
|
export default class ProfileCreate extends Command {
|
|
7
7
|
static args = {
|
|
@@ -30,12 +30,6 @@ Profile 'selfhosted' created successfully at ~/.xano/credentials.yaml
|
|
|
30
30
|
`,
|
|
31
31
|
];
|
|
32
32
|
static flags = {
|
|
33
|
-
config: Flags.string({
|
|
34
|
-
char: 'c',
|
|
35
|
-
description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
|
|
36
|
-
env: 'XANO_CONFIG',
|
|
37
|
-
required: false,
|
|
38
|
-
}),
|
|
39
33
|
access_token: Flags.string({
|
|
40
34
|
char: 't',
|
|
41
35
|
description: 'Access token for the Xano Metadata API',
|
|
@@ -51,6 +45,12 @@ Profile 'selfhosted' created successfully at ~/.xano/credentials.yaml
|
|
|
51
45
|
description: 'Branch name',
|
|
52
46
|
required: false,
|
|
53
47
|
}),
|
|
48
|
+
config: Flags.string({
|
|
49
|
+
char: 'c',
|
|
50
|
+
description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
|
|
51
|
+
env: 'XANO_CONFIG',
|
|
52
|
+
required: false,
|
|
53
|
+
}),
|
|
54
54
|
default: Flags.boolean({
|
|
55
55
|
default: false,
|
|
56
56
|
description: 'Set this profile as the default',
|
|
@@ -171,7 +171,7 @@ User Information:
|
|
|
171
171
|
const credentialsPath = this.getCredentialsPath();
|
|
172
172
|
// Check if credentials file exists
|
|
173
173
|
if (!fs.existsSync(credentialsPath)) {
|
|
174
|
-
this.error(`Credentials file not found at ${credentialsPath}\
|
|
174
|
+
this.error(`Credentials file not found at ${credentialsPath}\nCreate a profile using 'xano profile:create'`);
|
|
175
175
|
}
|
|
176
176
|
// Read credentials file
|
|
177
177
|
try {
|
|
@@ -68,7 +68,6 @@ Added profile.yaml to .gitignore
|
|
|
68
68
|
`,
|
|
69
69
|
];
|
|
70
70
|
static flags = {
|
|
71
|
-
// eslint-disable-next-line camelcase
|
|
72
71
|
account_origin: Flags.string({
|
|
73
72
|
char: 'a',
|
|
74
73
|
description: 'Override account origin for this project',
|
|
@@ -90,7 +89,6 @@ Added profile.yaml to .gitignore
|
|
|
90
89
|
description: 'Add (or skip adding) profile.yaml to .gitignore without prompting',
|
|
91
90
|
required: false,
|
|
92
91
|
}),
|
|
93
|
-
// eslint-disable-next-line camelcase
|
|
94
92
|
instance_origin: Flags.string({
|
|
95
93
|
char: 'i',
|
|
96
94
|
description: 'Override instance origin for this project',
|
|
@@ -108,10 +106,8 @@ Added profile.yaml to .gitignore
|
|
|
108
106
|
const config = { profile: args.name };
|
|
109
107
|
if (flags.workspace)
|
|
110
108
|
config.workspace = flags.workspace;
|
|
111
|
-
// eslint-disable-next-line camelcase
|
|
112
109
|
if (flags.instance_origin)
|
|
113
110
|
config.instance_origin = flags.instance_origin;
|
|
114
|
-
// eslint-disable-next-line camelcase
|
|
115
111
|
if (flags.account_origin)
|
|
116
112
|
config.account_origin = flags.account_origin;
|
|
117
113
|
if (flags.branch)
|
|
@@ -9,10 +9,10 @@ export default class ProfileWizard extends Command {
|
|
|
9
9
|
origin: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
};
|
|
11
11
|
run(): Promise<void>;
|
|
12
|
-
private getHeaders;
|
|
13
12
|
private fetchBranches;
|
|
14
13
|
private fetchInstances;
|
|
15
14
|
private fetchWorkspaces;
|
|
16
15
|
private getDefaultProfileName;
|
|
16
|
+
private getHeaders;
|
|
17
17
|
private saveProfile;
|
|
18
18
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { ExitPromptError } from '@inquirer/core';
|
|
2
1
|
import { Command, Flags } from '@oclif/core';
|
|
3
2
|
import inquirer from 'inquirer';
|
|
4
3
|
import * as yaml from 'js-yaml';
|
|
5
4
|
import * as fs from 'node:fs';
|
|
6
|
-
import
|
|
5
|
+
import path from 'node:path';
|
|
7
6
|
import { buildUserAgent, resolveCredentialsPath } from '../../../base-command.js';
|
|
8
7
|
export default class ProfileWizard extends Command {
|
|
9
8
|
static description = 'Create a new profile configuration using an interactive wizard';
|
|
@@ -188,20 +187,16 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
|
|
|
188
187
|
this.log(`✓ Profile '${profileName}' created successfully!`);
|
|
189
188
|
}
|
|
190
189
|
catch (error) {
|
|
191
|
-
|
|
190
|
+
// Ctrl+C at an inquirer prompt throws ExitPromptError. Match on the name
|
|
191
|
+
// rather than `instanceof`: inquirer bundles its own copy of
|
|
192
|
+
// @inquirer/core, so the thrown class won't match an imported one.
|
|
193
|
+
if (error?.name === 'ExitPromptError') {
|
|
192
194
|
this.log('Wizard cancelled.');
|
|
193
195
|
process.exit(0);
|
|
194
196
|
}
|
|
195
197
|
throw error;
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
|
-
getHeaders(accessToken) {
|
|
199
|
-
return {
|
|
200
|
-
'User-Agent': buildUserAgent(this.config.version),
|
|
201
|
-
accept: 'application/json',
|
|
202
|
-
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
200
|
async fetchBranches(accessToken, origin, workspaceId) {
|
|
206
201
|
const response = await fetch(`${origin}/api:meta/workspace/${workspaceId}/branch`, {
|
|
207
202
|
headers: this.getHeaders(accessToken),
|
|
@@ -325,6 +320,13 @@ Profile 'production' created successfully at ~/.xano/credentials.yaml
|
|
|
325
320
|
return 'default';
|
|
326
321
|
}
|
|
327
322
|
}
|
|
323
|
+
getHeaders(accessToken) {
|
|
324
|
+
return {
|
|
325
|
+
accept: 'application/json',
|
|
326
|
+
'User-Agent': buildUserAgent(this.config.version),
|
|
327
|
+
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
328
330
|
async saveProfile(profile, setAsDefault = false, configPath) {
|
|
329
331
|
const credentialsPath = resolveCredentialsPath(configPath);
|
|
330
332
|
const configDir = path.dirname(credentialsPath);
|