@xano/cli 1.0.2 → 1.0.3-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -3
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- 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 +225 -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/utils/multidoc-push.js +1 -1
- package/dist/utils/reference-checker.js +2 -2
- package/oclif.manifest.json +2593 -1677
- package/package.json +3 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class StaticHostDeploy extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
static_host: Args.string({
|
|
6
|
+
description: 'Static Host name',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Deploy a static host build to an environment';
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano static_host deploy default --build_id 52 --env dev
|
|
13
|
+
Deployed build 52 to dev
|
|
14
|
+
URL: https://x1234-abcd.static.xano.io
|
|
15
|
+
`,
|
|
16
|
+
`$ xano static_host deploy default --build_id 52 --env prod
|
|
17
|
+
Deployed build 52 to prod
|
|
18
|
+
URL: https://x1234-abcd.static.xano.io
|
|
19
|
+
`,
|
|
20
|
+
`$ xano static_host deploy myhost --build_id 123 --env dev -w 40
|
|
21
|
+
Deployed build 123 to dev
|
|
22
|
+
`,
|
|
23
|
+
`$ xano static_host deploy default --build_id 52 --env prod -o json`,
|
|
24
|
+
];
|
|
25
|
+
static flags = {
|
|
26
|
+
...BaseCommand.baseFlags,
|
|
27
|
+
build_id: Flags.string({
|
|
28
|
+
description: 'Build ID to deploy',
|
|
29
|
+
required: true,
|
|
30
|
+
}),
|
|
31
|
+
env: Flags.string({
|
|
32
|
+
description: 'Target environment',
|
|
33
|
+
options: ['dev', 'prod'],
|
|
34
|
+
required: true,
|
|
35
|
+
}),
|
|
36
|
+
output: Flags.string({
|
|
37
|
+
char: 'o',
|
|
38
|
+
default: 'summary',
|
|
39
|
+
description: 'Output format',
|
|
40
|
+
options: ['summary', 'json'],
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
workspace: Flags.string({
|
|
44
|
+
char: 'w',
|
|
45
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
46
|
+
required: false,
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
async run() {
|
|
50
|
+
const { args, flags } = await this.parse(StaticHostDeploy);
|
|
51
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
52
|
+
let workspaceId;
|
|
53
|
+
if (flags.workspace) {
|
|
54
|
+
workspaceId = flags.workspace;
|
|
55
|
+
}
|
|
56
|
+
else if (profile.workspace) {
|
|
57
|
+
workspaceId = profile.workspace;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
61
|
+
` 1. Provide it as a flag: xano static_host deploy <static_host> --build_id <id> --env <env> -w <workspace_id>\n` +
|
|
62
|
+
` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
|
|
63
|
+
}
|
|
64
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/build/${flags.build_id}/env`;
|
|
65
|
+
try {
|
|
66
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
67
|
+
body: JSON.stringify({ env: flags.env }),
|
|
68
|
+
headers: {
|
|
69
|
+
'accept': 'application/json',
|
|
70
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
},
|
|
73
|
+
method: 'POST',
|
|
74
|
+
}, flags.verbose, profile.access_token);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const errorText = await response.text();
|
|
77
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
78
|
+
}
|
|
79
|
+
const result = await response.json();
|
|
80
|
+
if (!result || typeof result !== 'object') {
|
|
81
|
+
this.error('Unexpected API response format');
|
|
82
|
+
}
|
|
83
|
+
if (flags.output === 'json') {
|
|
84
|
+
this.log(JSON.stringify(result, null, 2));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.log(`Deployed build ${flags.build_id} to ${flags.env}`);
|
|
88
|
+
if (result.default_url) {
|
|
89
|
+
this.log(`URL: ${result.default_url}`);
|
|
90
|
+
}
|
|
91
|
+
if (result.custom_url) {
|
|
92
|
+
this.log(`Custom URL: ${result.custom_url}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
this.error(`Failed to deploy build: ${error.message}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.error(`Failed to deploy build: ${String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class StaticHostEdit extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
'git-private-key-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'git-public-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'git-repo': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
};
|
|
20
|
+
run(): Promise<void>;
|
|
21
|
+
/** Fetch the current host record so unspecified fields are preserved on edit. */
|
|
22
|
+
private fetchHost;
|
|
23
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import BaseCommand from '../../../base-command.js';
|
|
5
|
+
export default class StaticHostEdit extends BaseCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
static_host: Args.string({
|
|
8
|
+
description: 'Static Host name to edit',
|
|
9
|
+
required: true,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Update a static host\'s name, description, or git configuration';
|
|
13
|
+
static examples = [
|
|
14
|
+
`$ xano static_host edit newsite --description "Marketing site"
|
|
15
|
+
Updated static host 'newsite'
|
|
16
|
+
`,
|
|
17
|
+
`$ xano static_host edit newsite --name newsite-v2
|
|
18
|
+
Updated static host 'newsite' (renamed to 'newsite-v2')
|
|
19
|
+
`,
|
|
20
|
+
`$ xano static_host edit newsite --git-repo git@github.com:me/site.git --git-private-key-file ./deploy_key
|
|
21
|
+
Updated static host 'newsite'
|
|
22
|
+
`,
|
|
23
|
+
];
|
|
24
|
+
static flags = {
|
|
25
|
+
...BaseCommand.baseFlags,
|
|
26
|
+
description: Flags.string({
|
|
27
|
+
description: 'New description for the static host',
|
|
28
|
+
required: false,
|
|
29
|
+
}),
|
|
30
|
+
'git-private-key-file': Flags.string({
|
|
31
|
+
description: 'Path to a file containing the git SSH private key (read from disk; never passed on the command line)',
|
|
32
|
+
required: false,
|
|
33
|
+
}),
|
|
34
|
+
'git-public-key': Flags.string({
|
|
35
|
+
description: 'Git SSH public key',
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
'git-repo': Flags.string({
|
|
39
|
+
description: 'Git repository URL (e.g. git@github.com:org/repo.git)',
|
|
40
|
+
required: false,
|
|
41
|
+
}),
|
|
42
|
+
name: Flags.string({
|
|
43
|
+
description: 'New name for the static host (renaming changes the deployed hostname)',
|
|
44
|
+
required: false,
|
|
45
|
+
}),
|
|
46
|
+
output: Flags.string({
|
|
47
|
+
char: 'o',
|
|
48
|
+
default: 'summary',
|
|
49
|
+
description: 'Output format',
|
|
50
|
+
options: ['summary', 'json'],
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
53
|
+
workspace: Flags.string({
|
|
54
|
+
char: 'w',
|
|
55
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
56
|
+
required: false,
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
async run() {
|
|
60
|
+
const { args, flags } = await this.parse(StaticHostEdit);
|
|
61
|
+
const hasGitFlag = Boolean(flags['git-repo'] || flags['git-private-key-file'] || flags['git-public-key']);
|
|
62
|
+
if (!flags.name && flags.description === undefined && !hasGitFlag) {
|
|
63
|
+
this.error('Nothing to update. Provide at least one of --name, --description, or a --git-* flag.');
|
|
64
|
+
}
|
|
65
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
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 static_host edit <static_host> --name <name> -w <workspace_id>\n` +
|
|
76
|
+
` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
|
|
77
|
+
}
|
|
78
|
+
// The edit endpoint requires `name`, and merges git as a whole object —
|
|
79
|
+
// so fetch the current record first and only override the fields the user set.
|
|
80
|
+
const current = await this.fetchHost(profile, workspaceId, args.static_host, flags.verbose);
|
|
81
|
+
const git = { ...current.git };
|
|
82
|
+
if (flags['git-repo'] !== undefined)
|
|
83
|
+
git.repo = flags['git-repo'];
|
|
84
|
+
if (flags['git-public-key'] !== undefined)
|
|
85
|
+
git.public_key = flags['git-public-key'];
|
|
86
|
+
if (flags['git-private-key-file'] !== undefined) {
|
|
87
|
+
const keyPath = path.resolve(flags['git-private-key-file']);
|
|
88
|
+
if (!fs.existsSync(keyPath)) {
|
|
89
|
+
this.error(`Private key file not found: ${keyPath}`);
|
|
90
|
+
}
|
|
91
|
+
git.private_key = fs.readFileSync(keyPath, 'utf8').trim();
|
|
92
|
+
}
|
|
93
|
+
const body = {
|
|
94
|
+
description: flags.description ?? current.description ?? '',
|
|
95
|
+
git: {
|
|
96
|
+
private_key: git.private_key ?? '',
|
|
97
|
+
public_key: git.public_key ?? '',
|
|
98
|
+
repo: git.repo ?? '',
|
|
99
|
+
},
|
|
100
|
+
name: flags.name ?? current.name,
|
|
101
|
+
};
|
|
102
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}/edit`;
|
|
103
|
+
try {
|
|
104
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
105
|
+
body: JSON.stringify(body),
|
|
106
|
+
headers: {
|
|
107
|
+
accept: 'application/json',
|
|
108
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
},
|
|
111
|
+
method: 'POST',
|
|
112
|
+
}, flags.verbose, profile.access_token);
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const message = await this.parseApiError(response, `Failed to update static host '${args.static_host}'`);
|
|
115
|
+
this.error(message);
|
|
116
|
+
}
|
|
117
|
+
const updated = (await response.json());
|
|
118
|
+
if (flags.output === 'json') {
|
|
119
|
+
this.log(JSON.stringify(updated, null, 2));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const renamed = flags.name && flags.name !== args.static_host ? ` (renamed to '${flags.name}')` : '';
|
|
123
|
+
this.log(`Updated static host '${args.static_host}'${renamed}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof Error) {
|
|
128
|
+
this.error(`Failed to update static host: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.error(`Failed to update static host: ${String(error)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Fetch the current host record so unspecified fields are preserved on edit. */
|
|
136
|
+
async fetchHost(profile, workspaceId, staticHost, verbose) {
|
|
137
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}`;
|
|
138
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
139
|
+
headers: {
|
|
140
|
+
accept: 'application/json',
|
|
141
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
142
|
+
},
|
|
143
|
+
method: 'GET',
|
|
144
|
+
}, verbose, profile.access_token);
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const message = await this.parseApiError(response, `Static host '${staticHost}' not found`);
|
|
147
|
+
this.error(message);
|
|
148
|
+
}
|
|
149
|
+
return (await response.json());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class StaticHostGet extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
static_host: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
output: 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
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
/** Print a one-line summary for a dev/prod env if it has been deployed. */
|
|
17
|
+
private logEnv;
|
|
18
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class StaticHostGet extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
static_host: Args.string({
|
|
6
|
+
description: 'Static Host name',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Get a single static host\'s details (name, git config, dev/prod environments)';
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano static_host get newsite
|
|
13
|
+
Static Host: newsite
|
|
14
|
+
ID: 5
|
|
15
|
+
Dev: https://newsite-dev-....dev.xano.io (v2)
|
|
16
|
+
`,
|
|
17
|
+
`$ xano static_host get newsite -w 40 -o json`,
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
...BaseCommand.baseFlags,
|
|
21
|
+
output: Flags.string({
|
|
22
|
+
char: 'o',
|
|
23
|
+
default: 'summary',
|
|
24
|
+
description: 'Output format',
|
|
25
|
+
options: ['summary', 'json'],
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
28
|
+
workspace: Flags.string({
|
|
29
|
+
char: 'w',
|
|
30
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
async run() {
|
|
35
|
+
const { args, flags } = await this.parse(StaticHostGet);
|
|
36
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
37
|
+
let workspaceId;
|
|
38
|
+
if (flags.workspace) {
|
|
39
|
+
workspaceId = flags.workspace;
|
|
40
|
+
}
|
|
41
|
+
else if (profile.workspace) {
|
|
42
|
+
workspaceId = profile.workspace;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
46
|
+
` 1. Provide it as a flag: xano static_host get <static_host> -w <workspace_id>\n` +
|
|
47
|
+
` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
|
|
48
|
+
}
|
|
49
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${args.static_host}`;
|
|
50
|
+
try {
|
|
51
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
52
|
+
headers: {
|
|
53
|
+
accept: 'application/json',
|
|
54
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
55
|
+
},
|
|
56
|
+
method: 'GET',
|
|
57
|
+
}, flags.verbose, profile.access_token);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const message = await this.parseApiError(response, `Failed to get static host '${args.static_host}'`);
|
|
60
|
+
this.error(message);
|
|
61
|
+
}
|
|
62
|
+
const host = (await response.json());
|
|
63
|
+
if (flags.output === 'json') {
|
|
64
|
+
this.log(JSON.stringify(host, null, 2));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.log(`Static Host: ${host.name}`);
|
|
68
|
+
this.log(`ID: ${host.id}`);
|
|
69
|
+
if (host.description)
|
|
70
|
+
this.log(`Description: ${host.description}`);
|
|
71
|
+
if (host.git?.repo)
|
|
72
|
+
this.log(`Git: ${host.git.repo}`);
|
|
73
|
+
this.logEnv('Dev', host.dev);
|
|
74
|
+
this.logEnv('Prod', host.prod);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof Error) {
|
|
79
|
+
this.error(`Failed to get static host: ${error.message}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.error(`Failed to get static host: ${String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Print a one-line summary for a dev/prod env if it has been deployed. */
|
|
87
|
+
logEnv(label, env) {
|
|
88
|
+
if (!env?.default_url)
|
|
89
|
+
return;
|
|
90
|
+
const modeInfo = env.mode ? ` (${env.mode})` : '';
|
|
91
|
+
const custom = env.custom_url ? ` [custom: ${env.custom_url}]` : '';
|
|
92
|
+
this.log(`${label}: ${env.default_url}${modeInfo}${custom}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export interface StaticHostEnv {
|
|
3
|
+
canonical?: null | string;
|
|
4
|
+
mode?: null | string;
|
|
5
|
+
}
|
|
6
|
+
export interface StaticHost {
|
|
7
|
+
dev?: StaticHostEnv;
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
prod?: StaticHostEnv;
|
|
11
|
+
}
|
|
12
|
+
export default class StaticHostMigrate extends BaseCommand {
|
|
13
|
+
static args: {
|
|
14
|
+
static_host: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
15
|
+
};
|
|
16
|
+
static description: string;
|
|
17
|
+
static examples: string[];
|
|
18
|
+
static flags: {
|
|
19
|
+
all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
env: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
26
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
27
|
+
};
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
/** List all static hosts in the workspace (paginates until exhausted). */
|
|
30
|
+
private listHosts;
|
|
31
|
+
/** Discover v1 hosts and migrate each, with a per-host report. */
|
|
32
|
+
private migrateAll;
|
|
33
|
+
/** POST the meta migrate endpoint for one host. Throws on a non-OK response. */
|
|
34
|
+
private migrateHost;
|
|
35
|
+
/** Migrate a single named host and report the result. */
|
|
36
|
+
private migrateOne;
|
|
37
|
+
}
|
|
38
|
+
/** An env still needs migration when it's been deployed (has a canonical) but isn't yet v2. */
|
|
39
|
+
export declare function envNeedsMigration(e?: StaticHostEnv): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* A host still needs migration when an env of interest is v1. When --env is given,
|
|
42
|
+
* only that env is considered; otherwise either env being v1 qualifies the host.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isV1(host: StaticHost, env?: string): boolean;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class StaticHostMigrate extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
static_host: Args.string({
|
|
6
|
+
description: 'Static Host name to migrate (omit when using --all)',
|
|
7
|
+
required: false,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Migrate a static host to instance-managed (v2) hosting. Reparents the Ingress, verifies it, clears master, and marks the host v2.';
|
|
11
|
+
static examples = [
|
|
12
|
+
`$ xano static_host migrate newsite
|
|
13
|
+
Migrated 'newsite' to v2
|
|
14
|
+
`,
|
|
15
|
+
`$ xano static_host migrate newsite --env dev
|
|
16
|
+
Migrated 'newsite' (dev) to v2
|
|
17
|
+
`,
|
|
18
|
+
`$ xano static_host migrate --all
|
|
19
|
+
Migrating 3 v1 host(s)...
|
|
20
|
+
✓ newsite
|
|
21
|
+
✓ marketing
|
|
22
|
+
✗ legacy — <error>
|
|
23
|
+
`,
|
|
24
|
+
`$ xano static_host migrate --all --dry-run
|
|
25
|
+
Would migrate 3 v1 host(s): newsite, marketing, legacy
|
|
26
|
+
`,
|
|
27
|
+
];
|
|
28
|
+
static flags = {
|
|
29
|
+
...BaseCommand.baseFlags,
|
|
30
|
+
all: Flags.boolean({
|
|
31
|
+
default: false,
|
|
32
|
+
description: 'Migrate every host still on v1 in the workspace',
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
'dry-run': Flags.boolean({
|
|
36
|
+
default: false,
|
|
37
|
+
description: 'List the hosts that would be migrated without changing anything',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
env: Flags.string({
|
|
41
|
+
description: 'Which environment to migrate (migrates both if omitted)',
|
|
42
|
+
options: ['dev', 'prod'],
|
|
43
|
+
required: false,
|
|
44
|
+
}),
|
|
45
|
+
output: Flags.string({
|
|
46
|
+
char: 'o',
|
|
47
|
+
default: 'summary',
|
|
48
|
+
description: 'Output format',
|
|
49
|
+
options: ['summary', 'json'],
|
|
50
|
+
required: false,
|
|
51
|
+
}),
|
|
52
|
+
workspace: Flags.string({
|
|
53
|
+
char: 'w',
|
|
54
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
55
|
+
required: false,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
async run() {
|
|
59
|
+
const { args, flags } = await this.parse(StaticHostMigrate);
|
|
60
|
+
if (!flags.all && !args.static_host) {
|
|
61
|
+
this.error('Provide a static host name, or use --all to migrate every v1 host.');
|
|
62
|
+
}
|
|
63
|
+
if (flags.all && args.static_host) {
|
|
64
|
+
this.error('Use either a static host name or --all, not both.');
|
|
65
|
+
}
|
|
66
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
67
|
+
let workspaceId;
|
|
68
|
+
if (flags.workspace) {
|
|
69
|
+
workspaceId = flags.workspace;
|
|
70
|
+
}
|
|
71
|
+
else if (profile.workspace) {
|
|
72
|
+
workspaceId = profile.workspace;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
76
|
+
` 1. Provide it as a flag: xano static_host migrate <static_host> -w <workspace_id>\n` +
|
|
77
|
+
` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
|
|
78
|
+
}
|
|
79
|
+
if (flags.all) {
|
|
80
|
+
await this.migrateAll(profile, workspaceId, flags);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await this.migrateOne(profile, workspaceId, args.static_host, flags);
|
|
84
|
+
}
|
|
85
|
+
/** List all static hosts in the workspace (paginates until exhausted). */
|
|
86
|
+
async listHosts(profile, workspaceId, verbose) {
|
|
87
|
+
const hosts = [];
|
|
88
|
+
const perPage = 100;
|
|
89
|
+
for (let page = 1;; page++) {
|
|
90
|
+
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host?page=${page}&per_page=${perPage}`;
|
|
91
|
+
// eslint-disable-next-line no-await-in-loop
|
|
92
|
+
const response = await this.verboseFetch(listUrl, {
|
|
93
|
+
headers: {
|
|
94
|
+
accept: 'application/json',
|
|
95
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
96
|
+
},
|
|
97
|
+
method: 'GET',
|
|
98
|
+
}, verbose, profile.access_token);
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const errorText = await response.text(); // eslint-disable-line no-await-in-loop
|
|
101
|
+
this.error(`Failed to list static hosts: ${response.status} ${response.statusText}\n${errorText}`);
|
|
102
|
+
}
|
|
103
|
+
const data = (await response.json()); // eslint-disable-line no-await-in-loop
|
|
104
|
+
const pageHosts = Array.isArray(data)
|
|
105
|
+
? data
|
|
106
|
+
: data && typeof data === 'object' && 'items' in data && Array.isArray(data.items)
|
|
107
|
+
? data.items
|
|
108
|
+
: [];
|
|
109
|
+
hosts.push(...pageHosts);
|
|
110
|
+
if (pageHosts.length < perPage)
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return hosts;
|
|
114
|
+
}
|
|
115
|
+
/** Discover v1 hosts and migrate each, with a per-host report. */
|
|
116
|
+
async migrateAll(profile, workspaceId, flags) {
|
|
117
|
+
const hosts = await this.listHosts(profile, workspaceId, flags.verbose);
|
|
118
|
+
const v1Hosts = hosts.filter((h) => isV1(h, flags.env));
|
|
119
|
+
if (v1Hosts.length === 0) {
|
|
120
|
+
this.log('No v1 hosts to migrate — all hosts are already instance-managed (v2).');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (flags['dry-run']) {
|
|
124
|
+
const names = v1Hosts.map((h) => h.name);
|
|
125
|
+
if (flags.output === 'json') {
|
|
126
|
+
this.log(JSON.stringify({ dryRun: true, wouldMigrate: names }, null, 2));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.log(`Would migrate ${names.length} v1 host(s): ${names.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (flags.output !== 'json') {
|
|
134
|
+
this.log(`Migrating ${v1Hosts.length} v1 host(s)...`);
|
|
135
|
+
}
|
|
136
|
+
const results = [];
|
|
137
|
+
for (const host of v1Hosts) {
|
|
138
|
+
try {
|
|
139
|
+
// eslint-disable-next-line no-await-in-loop
|
|
140
|
+
await this.migrateHost(profile, workspaceId, host.name, flags.env, flags.verbose);
|
|
141
|
+
results.push({ migrated: true, static_host: host.name });
|
|
142
|
+
if (flags.output !== 'json')
|
|
143
|
+
this.log(` ✓ ${host.name}`);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
results.push({ error: message, migrated: false, static_host: host.name });
|
|
148
|
+
if (flags.output !== 'json')
|
|
149
|
+
this.log(` ✗ ${host.name} — ${message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (flags.output === 'json') {
|
|
153
|
+
this.log(JSON.stringify({ results }, null, 2));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const ok = results.filter((r) => r.migrated).length;
|
|
157
|
+
const failed = results.length - ok;
|
|
158
|
+
this.log(`Done: ${ok} migrated${failed > 0 ? `, ${failed} failed` : ''}.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** POST the meta migrate endpoint for one host. Throws on a non-OK response. */
|
|
162
|
+
async migrateHost(profile, workspaceId, staticHost, env, verbose) {
|
|
163
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/migrate`;
|
|
164
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
165
|
+
body: JSON.stringify(env ? { env } : {}),
|
|
166
|
+
headers: {
|
|
167
|
+
accept: 'application/json',
|
|
168
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
},
|
|
171
|
+
method: 'POST',
|
|
172
|
+
}, verbose, profile.access_token);
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
const message = await this.parseApiError(response, `Migrate failed for '${staticHost}'`);
|
|
175
|
+
throw new Error(message);
|
|
176
|
+
}
|
|
177
|
+
return (await response.json());
|
|
178
|
+
}
|
|
179
|
+
/** Migrate a single named host and report the result. */
|
|
180
|
+
async migrateOne(profile, workspaceId, staticHost, flags) {
|
|
181
|
+
const result = await this.migrateHost(profile, workspaceId, staticHost, flags.env, flags.verbose);
|
|
182
|
+
const envSuffix = flags.env ? ` (${flags.env})` : '';
|
|
183
|
+
if (flags.output === 'json') {
|
|
184
|
+
this.log(JSON.stringify({ env: flags.env ?? null, migrated: true, static_host: staticHost, ...result }, null, 2));
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
this.log(`Migrated '${staticHost}'${envSuffix} to v2`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** An env still needs migration when it's been deployed (has a canonical) but isn't yet v2. */
|
|
192
|
+
export function envNeedsMigration(e) {
|
|
193
|
+
return Boolean(e?.canonical) && e?.mode !== 'v2';
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A host still needs migration when an env of interest is v1. When --env is given,
|
|
197
|
+
* only that env is considered; otherwise either env being v1 qualifies the host.
|
|
198
|
+
*/
|
|
199
|
+
export function isV1(host, env) {
|
|
200
|
+
if (env === 'dev')
|
|
201
|
+
return envNeedsMigration(host.dev);
|
|
202
|
+
if (env === 'prod')
|
|
203
|
+
return envNeedsMigration(host.prod);
|
|
204
|
+
return envNeedsMigration(host.dev) || envNeedsMigration(host.prod);
|
|
205
|
+
}
|
|
@@ -589,7 +589,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
589
589
|
}
|
|
590
590
|
catch {
|
|
591
591
|
if (flags.verbose) {
|
|
592
|
-
log(
|
|
592
|
+
log(`Server response is not JSON; skipping GUID sync\n${responseText}`);
|
|
593
593
|
}
|
|
594
594
|
}
|
|
595
595
|
}
|
|
@@ -170,8 +170,8 @@ export function checkTableIndexes(documents) {
|
|
|
170
170
|
return badIndexes;
|
|
171
171
|
}
|
|
172
172
|
function extractSchemaFields(content) {
|
|
173
|
-
// id and
|
|
174
|
-
const fields = new Set(['id', 'created_at']);
|
|
173
|
+
// id, created_at, and xdo are system fields not declared in the schema
|
|
174
|
+
const fields = new Set(['id', 'created_at', 'xdo']);
|
|
175
175
|
// Find the schema block by matching braces
|
|
176
176
|
const schemaStart = content.match(/\bschema\s*\{/);
|
|
177
177
|
if (!schemaStart || schemaStart.index === undefined)
|