@xano/cli 0.0.95-beta.9 → 1.0.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 +111 -70
- package/dist/base-command.d.ts +16 -1
- package/dist/base-command.js +57 -5
- package/dist/commands/auth/index.d.ts +1 -0
- package/dist/commands/auth/index.js +15 -10
- package/dist/commands/branch/create/index.d.ts +4 -1
- package/dist/commands/branch/create/index.js +22 -21
- package/dist/commands/branch/delete/index.d.ts +1 -0
- package/dist/commands/branch/delete/index.js +1 -4
- package/dist/commands/branch/edit/index.d.ts +1 -0
- package/dist/commands/branch/edit/index.js +1 -4
- package/dist/commands/branch/get/index.d.ts +1 -0
- package/dist/commands/branch/get/index.js +1 -4
- package/dist/commands/branch/list/index.d.ts +2 -6
- package/dist/commands/branch/list/index.js +13 -17
- package/dist/commands/branch/set_live/index.d.ts +1 -0
- package/dist/commands/branch/set_live/index.js +1 -4
- package/dist/commands/function/create/index.d.ts +1 -0
- package/dist/commands/function/create/index.js +1 -2
- package/dist/commands/function/edit/index.d.ts +1 -0
- package/dist/commands/function/edit/index.js +1 -2
- package/dist/commands/function/get/index.d.ts +1 -0
- package/dist/commands/function/get/index.js +1 -4
- package/dist/commands/function/list/index.d.ts +1 -0
- package/dist/commands/function/list/index.js +1 -4
- package/dist/commands/platform/get/index.d.ts +1 -0
- package/dist/commands/platform/get/index.js +1 -4
- package/dist/commands/platform/list/index.d.ts +1 -0
- package/dist/commands/platform/list/index.js +1 -4
- package/dist/commands/profile/create/index.d.ts +1 -0
- package/dist/commands/profile/create/index.js +10 -4
- package/dist/commands/profile/delete/index.d.ts +1 -0
- package/dist/commands/profile/delete/index.js +8 -4
- package/dist/commands/profile/edit/index.d.ts +1 -0
- package/dist/commands/profile/edit/index.js +1 -4
- package/dist/commands/profile/get/index.d.ts +3 -0
- package/dist/commands/profile/get/index.js +12 -5
- package/dist/commands/profile/list/index.d.ts +1 -0
- package/dist/commands/profile/list/index.js +8 -4
- package/dist/commands/profile/me/index.d.ts +1 -0
- package/dist/commands/profile/me/index.js +1 -4
- package/dist/commands/profile/set/index.d.ts +3 -0
- package/dist/commands/profile/set/index.js +12 -6
- package/dist/commands/profile/token/index.d.ts +3 -0
- package/dist/commands/profile/token/index.js +12 -5
- package/dist/commands/profile/wizard/index.d.ts +1 -0
- package/dist/commands/profile/wizard/index.js +13 -9
- package/dist/commands/profile/workspace/index.d.ts +3 -0
- package/dist/commands/profile/workspace/index.js +12 -5
- package/dist/commands/profile/workspace/set/index.d.ts +1 -0
- package/dist/commands/profile/workspace/set/index.js +1 -3
- package/dist/commands/release/create/index.d.ts +4 -1
- package/dist/commands/release/create/index.js +12 -14
- package/dist/commands/release/delete/index.d.ts +1 -0
- package/dist/commands/release/delete/index.js +1 -4
- package/dist/commands/release/deploy/index.d.ts +3 -0
- package/dist/commands/release/deploy/index.js +31 -1
- package/dist/commands/release/edit/index.d.ts +1 -0
- package/dist/commands/release/edit/index.js +1 -4
- package/dist/commands/release/export/index.d.ts +1 -0
- package/dist/commands/release/export/index.js +1 -3
- package/dist/commands/release/get/index.d.ts +1 -0
- package/dist/commands/release/get/index.js +1 -4
- package/dist/commands/release/import/index.d.ts +1 -0
- package/dist/commands/release/import/index.js +1 -3
- package/dist/commands/release/list/index.d.ts +1 -0
- package/dist/commands/release/list/index.js +1 -4
- package/dist/commands/release/pull/index.d.ts +2 -3
- package/dist/commands/release/pull/index.js +19 -18
- package/dist/commands/release/push/index.d.ts +2 -3
- package/dist/commands/release/push/index.js +19 -22
- package/dist/commands/sandbox/delete/index.d.ts +13 -0
- package/dist/commands/sandbox/delete/index.js +71 -0
- package/dist/commands/sandbox/env/delete/index.d.ts +1 -0
- package/dist/commands/sandbox/env/delete/index.js +4 -2
- package/dist/commands/sandbox/env/get/index.d.ts +1 -0
- package/dist/commands/sandbox/env/get/index.js +4 -2
- package/dist/commands/sandbox/env/get_all/index.d.ts +1 -0
- package/dist/commands/sandbox/env/get_all/index.js +4 -2
- package/dist/commands/sandbox/env/list/index.d.ts +1 -0
- package/dist/commands/sandbox/env/list/index.js +4 -2
- package/dist/commands/sandbox/env/set/index.d.ts +1 -0
- package/dist/commands/sandbox/env/set/index.js +4 -2
- package/dist/commands/sandbox/env/set_all/index.d.ts +1 -0
- package/dist/commands/sandbox/env/set_all/index.js +4 -2
- package/dist/commands/sandbox/get/index.d.ts +1 -0
- package/dist/commands/sandbox/get/index.js +2 -0
- package/dist/commands/sandbox/license/get/index.d.ts +1 -0
- package/dist/commands/sandbox/license/get/index.js +4 -2
- package/dist/commands/sandbox/license/set/index.d.ts +1 -0
- package/dist/commands/sandbox/license/set/index.js +4 -2
- package/dist/commands/sandbox/pull/index.d.ts +2 -3
- package/dist/commands/sandbox/pull/index.js +19 -14
- package/dist/commands/sandbox/push/index.d.ts +12 -4
- package/dist/commands/sandbox/push/index.js +150 -95
- package/dist/commands/sandbox/reset/index.d.ts +1 -0
- package/dist/commands/sandbox/reset/index.js +4 -2
- package/dist/commands/sandbox/review/index.d.ts +1 -0
- package/dist/commands/sandbox/review/index.js +4 -2
- package/dist/commands/sandbox/unit_test/list/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/list/index.js +4 -2
- package/dist/commands/sandbox/unit_test/run/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run/index.js +4 -2
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +4 -0
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +4 -2
- package/dist/commands/sandbox/workflow_test/run/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run/index.js +4 -2
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +4 -0
- package/dist/commands/static_host/build/create/index.d.ts +1 -0
- package/dist/commands/static_host/build/create/index.js +1 -3
- package/dist/commands/static_host/build/get/index.d.ts +1 -0
- package/dist/commands/static_host/build/get/index.js +1 -4
- package/dist/commands/static_host/build/list/index.d.ts +1 -0
- package/dist/commands/static_host/build/list/index.js +1 -4
- package/dist/commands/static_host/list/index.d.ts +1 -0
- package/dist/commands/static_host/list/index.js +1 -4
- package/dist/commands/tenant/backup/create/index.d.ts +1 -0
- package/dist/commands/tenant/backup/create/index.js +1 -4
- package/dist/commands/tenant/backup/delete/index.d.ts +1 -0
- package/dist/commands/tenant/backup/delete/index.js +1 -4
- package/dist/commands/tenant/backup/export/index.d.ts +1 -0
- package/dist/commands/tenant/backup/export/index.js +1 -3
- package/dist/commands/tenant/backup/import/index.d.ts +1 -0
- package/dist/commands/tenant/backup/import/index.js +1 -3
- package/dist/commands/tenant/backup/list/index.d.ts +1 -0
- package/dist/commands/tenant/backup/list/index.js +1 -4
- package/dist/commands/tenant/backup/restore/index.d.ts +1 -0
- package/dist/commands/tenant/backup/restore/index.js +1 -4
- package/dist/commands/tenant/cluster/create/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/create/index.js +1 -3
- package/dist/commands/tenant/cluster/delete/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/delete/index.js +1 -4
- package/dist/commands/tenant/cluster/edit/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/edit/index.js +1 -4
- package/dist/commands/tenant/cluster/get/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/get/index.js +1 -4
- package/dist/commands/tenant/cluster/license/get/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/license/get/index.js +1 -3
- package/dist/commands/tenant/cluster/license/set/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/license/set/index.js +1 -3
- package/dist/commands/tenant/cluster/list/index.d.ts +1 -0
- package/dist/commands/tenant/cluster/list/index.js +1 -4
- package/dist/commands/tenant/create/index.d.ts +1 -0
- package/dist/commands/tenant/create/index.js +1 -3
- package/dist/commands/tenant/delete/index.d.ts +1 -0
- package/dist/commands/tenant/delete/index.js +1 -4
- package/dist/commands/tenant/deploy_platform/index.d.ts +1 -0
- package/dist/commands/tenant/deploy_platform/index.js +1 -3
- package/dist/commands/tenant/deploy_release/index.d.ts +1 -0
- package/dist/commands/tenant/deploy_release/index.js +1 -4
- package/dist/commands/tenant/edit/index.d.ts +1 -0
- package/dist/commands/tenant/edit/index.js +1 -4
- package/dist/commands/tenant/env/delete/index.d.ts +1 -0
- package/dist/commands/tenant/env/delete/index.js +1 -4
- package/dist/commands/tenant/env/get/index.d.ts +1 -0
- package/dist/commands/tenant/env/get/index.js +1 -4
- package/dist/commands/tenant/env/get_all/index.d.ts +1 -0
- package/dist/commands/tenant/env/get_all/index.js +1 -3
- package/dist/commands/tenant/env/list/index.d.ts +1 -0
- package/dist/commands/tenant/env/list/index.js +1 -4
- package/dist/commands/tenant/env/set/index.d.ts +1 -0
- package/dist/commands/tenant/env/set/index.js +1 -4
- package/dist/commands/tenant/env/set_all/index.d.ts +1 -0
- package/dist/commands/tenant/env/set_all/index.js +1 -3
- package/dist/commands/tenant/get/index.d.ts +1 -0
- package/dist/commands/tenant/get/index.js +1 -4
- package/dist/commands/tenant/impersonate/index.d.ts +1 -0
- package/dist/commands/tenant/impersonate/index.js +1 -4
- package/dist/commands/tenant/license/get/index.d.ts +1 -0
- package/dist/commands/tenant/license/get/index.js +1 -3
- package/dist/commands/tenant/license/set/index.d.ts +1 -0
- package/dist/commands/tenant/license/set/index.js +1 -3
- package/dist/commands/tenant/list/index.d.ts +1 -0
- package/dist/commands/tenant/list/index.js +1 -4
- package/dist/commands/tenant/pull/index.d.ts +2 -3
- package/dist/commands/tenant/pull/index.js +20 -21
- package/dist/commands/tenant/push/index.d.ts +2 -22
- package/dist/commands/tenant/push/index.js +7 -225
- package/dist/commands/tenant/unit_test/list/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/list/index.js +2 -27
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +2 -27
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +2 -27
- package/dist/commands/tenant/workflow_test/list/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/list/index.js +2 -27
- package/dist/commands/tenant/workflow_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run/index.js +2 -27
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +2 -27
- package/dist/commands/unit_test/list/index.d.ts +1 -0
- package/dist/commands/unit_test/list/index.js +1 -4
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +1 -4
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +1 -4
- package/dist/commands/update/index.d.ts +1 -0
- package/dist/commands/workflow_test/delete/index.d.ts +1 -0
- package/dist/commands/workflow_test/delete/index.js +1 -4
- package/dist/commands/workflow_test/get/index.d.ts +1 -0
- package/dist/commands/workflow_test/get/index.js +1 -4
- package/dist/commands/workflow_test/list/index.d.ts +1 -0
- package/dist/commands/workflow_test/list/index.js +1 -4
- package/dist/commands/workflow_test/run/index.d.ts +1 -0
- package/dist/commands/workflow_test/run/index.js +1 -4
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +1 -4
- package/dist/commands/workspace/create/index.d.ts +1 -0
- package/dist/commands/workspace/create/index.js +1 -4
- package/dist/commands/workspace/delete/index.d.ts +2 -6
- package/dist/commands/workspace/delete/index.js +17 -16
- package/dist/commands/workspace/edit/index.d.ts +2 -6
- package/dist/commands/workspace/edit/index.js +16 -20
- package/dist/commands/workspace/get/index.d.ts +2 -6
- package/dist/commands/workspace/get/index.js +14 -18
- package/dist/commands/workspace/git/pull/index.d.ts +2 -3
- package/dist/commands/workspace/git/pull/index.js +18 -17
- package/dist/commands/workspace/list/index.d.ts +1 -0
- package/dist/commands/workspace/list/index.js +1 -4
- package/dist/commands/workspace/pull/index.d.ts +2 -3
- package/dist/commands/workspace/pull/index.js +21 -24
- package/dist/commands/workspace/push/index.d.ts +7 -16
- package/dist/commands/workspace/push/index.js +85 -700
- package/dist/utils/multidoc-push.d.ts +63 -0
- package/dist/utils/multidoc-push.js +690 -0
- package/dist/utils/reference-checker.d.ts +57 -0
- package/dist/utils/reference-checker.js +232 -0
- package/oclif.manifest.json +3562 -2647
- package/package.json +1 -1
- package/dist/commands/sandbox/workflow_test/delete/index.d.ts +0 -17
- package/dist/commands/sandbox/workflow_test/delete/index.js +0 -59
- package/dist/commands/sandbox/workflow_test/get/index.d.ts +0 -17
- package/dist/commands/sandbox/workflow_test/get/index.js +0 -58
- package/dist/commands/tenant/workflow_test/delete/index.d.ts +0 -19
- package/dist/commands/tenant/workflow_test/delete/index.js +0 -110
- package/dist/commands/tenant/workflow_test/get/index.d.ts +0 -19
- package/dist/commands/tenant/workflow_test/get/index.js +0 -112
|
@@ -1,63 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as yaml from 'js-yaml';
|
|
3
|
-
import { minimatch } from 'minimatch';
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
4
2
|
import * as fs from 'node:fs';
|
|
5
|
-
import
|
|
6
|
-
import * as path from 'node:path';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
7
4
|
import BaseCommand from '../../../base-command.js';
|
|
8
|
-
import {
|
|
5
|
+
import { executePush } from '../../../utils/multidoc-push.js';
|
|
9
6
|
export default class Push extends BaseCommand {
|
|
10
|
-
static args = {
|
|
11
|
-
directory: Args.string({
|
|
12
|
-
description: 'Directory containing documents to push (as produced by workspace pull)',
|
|
13
|
-
required: true,
|
|
14
|
-
}),
|
|
15
|
-
};
|
|
16
7
|
static description = 'Push local documents to a workspace. By default, only changed files are pushed (partial mode). Use --sync to push all files. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only.';
|
|
17
8
|
static examples = [
|
|
18
|
-
`$ xano workspace push
|
|
19
|
-
Push
|
|
9
|
+
`$ xano workspace push
|
|
10
|
+
Push from current directory (default partial mode)
|
|
11
|
+
`,
|
|
12
|
+
`$ xano workspace push -d ./my-workspace
|
|
13
|
+
Push from a specific directory
|
|
20
14
|
`,
|
|
21
|
-
`$ xano workspace push
|
|
15
|
+
`$ xano workspace push --sync
|
|
22
16
|
Push all files to the workspace
|
|
23
17
|
`,
|
|
24
|
-
`$ xano workspace push
|
|
18
|
+
`$ xano workspace push --sync --delete
|
|
25
19
|
Push all files and delete remote objects not included
|
|
26
20
|
`,
|
|
27
|
-
`$ xano workspace push
|
|
21
|
+
`$ xano workspace push --dry-run
|
|
28
22
|
Preview changes without pushing
|
|
29
23
|
`,
|
|
30
|
-
`$ xano workspace push
|
|
24
|
+
`$ xano workspace push --force
|
|
31
25
|
Skip preview and push immediately (for CI/CD)
|
|
32
26
|
`,
|
|
33
|
-
`$ xano workspace push ./output -w 40
|
|
27
|
+
`$ xano workspace push -d ./output -w 40
|
|
34
28
|
Pushed 15 documents from ./output
|
|
35
29
|
`,
|
|
36
|
-
`$ xano workspace push
|
|
37
|
-
Pushed 58 documents
|
|
30
|
+
`$ xano workspace push --profile production
|
|
31
|
+
Pushed 58 documents
|
|
38
32
|
`,
|
|
39
|
-
`$ xano workspace push
|
|
40
|
-
Pushed 42 documents
|
|
33
|
+
`$ xano workspace push -b dev
|
|
34
|
+
Pushed 42 documents
|
|
41
35
|
`,
|
|
42
|
-
`$ xano workspace push
|
|
36
|
+
`$ xano workspace push --no-records
|
|
43
37
|
Push schema only, skip importing table records
|
|
44
38
|
`,
|
|
45
|
-
`$ xano workspace push
|
|
39
|
+
`$ xano workspace push --no-env
|
|
46
40
|
Push without overwriting environment variables
|
|
47
41
|
`,
|
|
48
|
-
`$ xano workspace push
|
|
42
|
+
`$ xano workspace push --truncate
|
|
49
43
|
Truncate all table records before importing
|
|
50
44
|
`,
|
|
51
|
-
`$ xano workspace push
|
|
45
|
+
`$ xano workspace push -i "**/func*"
|
|
52
46
|
Push only files matching the glob pattern
|
|
53
47
|
`,
|
|
54
|
-
`$ xano workspace push
|
|
48
|
+
`$ xano workspace push -i "function/*" -i "table/*"
|
|
55
49
|
Push files matching multiple patterns
|
|
56
50
|
`,
|
|
57
|
-
`$ xano workspace push
|
|
51
|
+
`$ xano workspace push -e "table/*"
|
|
58
52
|
Push all files except tables
|
|
59
53
|
`,
|
|
60
|
-
`$ xano workspace push
|
|
54
|
+
`$ xano workspace push -i "function/*" -e "**/test*"
|
|
61
55
|
Push functions but exclude test files
|
|
62
56
|
`,
|
|
63
57
|
];
|
|
@@ -68,6 +62,12 @@ Push functions but exclude test files
|
|
|
68
62
|
description: 'Branch name (optional if set in profile, defaults to live)',
|
|
69
63
|
required: false,
|
|
70
64
|
}),
|
|
65
|
+
directory: Flags.string({
|
|
66
|
+
char: 'd',
|
|
67
|
+
default: '.',
|
|
68
|
+
description: 'Directory containing documents to push (defaults to current directory)',
|
|
69
|
+
required: false,
|
|
70
|
+
}),
|
|
71
71
|
delete: Flags.boolean({
|
|
72
72
|
default: false,
|
|
73
73
|
description: 'Delete workspace objects not included in the push (requires --sync)',
|
|
@@ -83,14 +83,15 @@ Push functions but exclude test files
|
|
|
83
83
|
description: 'Include environment variables in import',
|
|
84
84
|
required: false,
|
|
85
85
|
}),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
description: '
|
|
86
|
+
exclude: Flags.string({
|
|
87
|
+
char: 'e',
|
|
88
|
+
description: 'Glob pattern to exclude files (e.g. "table/*", "**/test*"). Matched against relative paths from the push directory.',
|
|
89
|
+
multiple: true,
|
|
89
90
|
required: false,
|
|
90
91
|
}),
|
|
91
|
-
|
|
92
|
+
force: Flags.boolean({
|
|
92
93
|
default: false,
|
|
93
|
-
description: '
|
|
94
|
+
description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
|
|
94
95
|
required: false,
|
|
95
96
|
}),
|
|
96
97
|
guids: Flags.boolean({
|
|
@@ -99,6 +100,22 @@ Push functions but exclude test files
|
|
|
99
100
|
description: 'Write server-assigned GUIDs back to local files (use --no-guids to skip)',
|
|
100
101
|
required: false,
|
|
101
102
|
}),
|
|
103
|
+
include: Flags.string({
|
|
104
|
+
char: 'i',
|
|
105
|
+
description: 'Glob pattern to include files (e.g. "**/func*", "table/*.xs"). Matched against relative paths from the push directory.',
|
|
106
|
+
multiple: true,
|
|
107
|
+
required: false,
|
|
108
|
+
}),
|
|
109
|
+
records: Flags.boolean({
|
|
110
|
+
default: false,
|
|
111
|
+
description: 'Include records in import',
|
|
112
|
+
required: false,
|
|
113
|
+
}),
|
|
114
|
+
sync: Flags.boolean({
|
|
115
|
+
default: false,
|
|
116
|
+
description: 'Full push — send all files, not just changed ones. Required for --delete.',
|
|
117
|
+
required: false,
|
|
118
|
+
}),
|
|
102
119
|
transaction: Flags.boolean({
|
|
103
120
|
allowNo: true,
|
|
104
121
|
default: true,
|
|
@@ -115,43 +132,10 @@ Push functions but exclude test files
|
|
|
115
132
|
description: 'Workspace ID (optional if set in profile)',
|
|
116
133
|
required: false,
|
|
117
134
|
}),
|
|
118
|
-
exclude: Flags.string({
|
|
119
|
-
char: 'e',
|
|
120
|
-
description: 'Glob pattern to exclude files (e.g. "table/*", "**/test*"). Matched against relative paths from the push directory.',
|
|
121
|
-
multiple: true,
|
|
122
|
-
required: false,
|
|
123
|
-
}),
|
|
124
|
-
include: Flags.string({
|
|
125
|
-
char: 'i',
|
|
126
|
-
description: 'Glob pattern to include files (e.g. "**/func*", "table/*.xs"). Matched against relative paths from the push directory.',
|
|
127
|
-
multiple: true,
|
|
128
|
-
required: false,
|
|
129
|
-
}),
|
|
130
|
-
force: Flags.boolean({
|
|
131
|
-
default: false,
|
|
132
|
-
description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
|
|
133
|
-
required: false,
|
|
134
|
-
}),
|
|
135
135
|
};
|
|
136
136
|
async run() {
|
|
137
|
-
const {
|
|
138
|
-
|
|
139
|
-
const profileName = flags.profile || this.getDefaultProfile();
|
|
140
|
-
// Load credentials
|
|
141
|
-
const credentials = this.loadCredentials();
|
|
142
|
-
// Get the profile configuration
|
|
143
|
-
if (!(profileName in credentials.profiles)) {
|
|
144
|
-
this.error(`Profile '${profileName}' not found. Available profiles: ${Object.keys(credentials.profiles).join(', ')}\n` +
|
|
145
|
-
`Create a profile using 'xano profile:create'`);
|
|
146
|
-
}
|
|
147
|
-
const profile = credentials.profiles[profileName];
|
|
148
|
-
// Validate required fields
|
|
149
|
-
if (!profile.instance_origin) {
|
|
150
|
-
this.error(`Profile '${profileName}' is missing instance_origin`);
|
|
151
|
-
}
|
|
152
|
-
if (!profile.access_token) {
|
|
153
|
-
this.error(`Profile '${profileName}' is missing access_token`);
|
|
154
|
-
}
|
|
137
|
+
const { flags } = await this.parse(Push);
|
|
138
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
155
139
|
// Determine workspace_id from flag or profile
|
|
156
140
|
let workspaceId;
|
|
157
141
|
if (flags.workspace) {
|
|
@@ -162,646 +146,47 @@ Push functions but exclude test files
|
|
|
162
146
|
}
|
|
163
147
|
else {
|
|
164
148
|
this.error(`Workspace ID is required. Either:\n` +
|
|
165
|
-
` 1. Provide it as a flag: xano workspace push
|
|
149
|
+
` 1. Provide it as a flag: xano workspace push -w <workspace_id>\n` +
|
|
166
150
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
167
151
|
}
|
|
168
|
-
|
|
169
|
-
const inputDir = path.resolve(args.directory);
|
|
152
|
+
const inputDir = resolve(flags.directory);
|
|
170
153
|
if (!fs.existsSync(inputDir)) {
|
|
171
154
|
this.error(`Directory not found: ${inputDir}`);
|
|
172
155
|
}
|
|
173
156
|
if (!fs.statSync(inputDir).isDirectory()) {
|
|
174
157
|
this.error(`Not a directory: ${inputDir}`);
|
|
175
158
|
}
|
|
176
|
-
// Collect all .xs files from the directory tree
|
|
177
|
-
const allFiles = this.collectFiles(inputDir);
|
|
178
|
-
let files = allFiles;
|
|
179
|
-
// Apply glob include(s) if specified
|
|
180
|
-
if (flags.include && flags.include.length > 0) {
|
|
181
|
-
files = files.filter((f) => {
|
|
182
|
-
const rel = path.relative(inputDir, f);
|
|
183
|
-
return flags.include.some((pattern) => minimatch(rel, pattern, { matchBase: true }));
|
|
184
|
-
});
|
|
185
|
-
this.log('');
|
|
186
|
-
this.log(` ${ux.colorize('dim', 'Include:')} ${flags.include.map((p) => ux.colorize('cyan', p)).join(', ')}`);
|
|
187
|
-
this.log(` ${ux.colorize('dim', 'Matched:')} ${ux.colorize('bold', String(files.length))} of ${allFiles.length} files`);
|
|
188
|
-
}
|
|
189
|
-
// Apply glob exclude(s) if specified
|
|
190
|
-
if (flags.exclude && flags.exclude.length > 0) {
|
|
191
|
-
const beforeCount = files.length;
|
|
192
|
-
files = files.filter((f) => {
|
|
193
|
-
const rel = path.relative(inputDir, f);
|
|
194
|
-
return !flags.exclude.some((pattern) => minimatch(rel, pattern, { matchBase: true }));
|
|
195
|
-
});
|
|
196
|
-
this.log('');
|
|
197
|
-
this.log(` ${ux.colorize('dim', 'Exclude:')} ${flags.exclude.map((p) => ux.colorize('cyan', p)).join(', ')}`);
|
|
198
|
-
this.log(` ${ux.colorize('dim', 'Kept:')} ${ux.colorize('bold', String(files.length))} of ${beforeCount} files (excluded ${beforeCount - files.length})`);
|
|
199
|
-
}
|
|
200
|
-
if (files.length === 0) {
|
|
201
|
-
this.error(flags.include || flags.exclude
|
|
202
|
-
? `No .xs files remain after ${[flags.include ? `include ${flags.include.join(', ')}` : '', flags.exclude ? `exclude ${flags.exclude.join(', ')}` : ''].filter(Boolean).join(' and ')} in ${args.directory}`
|
|
203
|
-
: `No .xs files found in ${args.directory}`);
|
|
204
|
-
}
|
|
205
|
-
// Read each file and track file path alongside content
|
|
206
|
-
const documentEntries = [];
|
|
207
|
-
for (const filePath of files) {
|
|
208
|
-
const content = fs.readFileSync(filePath, 'utf8').trim();
|
|
209
|
-
if (content) {
|
|
210
|
-
documentEntries.push({ content, filePath });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (documentEntries.length === 0) {
|
|
214
|
-
this.error(`All .xs files in ${args.directory} are empty`);
|
|
215
|
-
}
|
|
216
|
-
let multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
217
|
-
// Build lookup map from document key to file path (for GUID writeback)
|
|
218
|
-
const documentFileMap = new Map();
|
|
219
|
-
for (const entry of documentEntries) {
|
|
220
|
-
const parsed = parseDocument(entry.content);
|
|
221
|
-
if (parsed) {
|
|
222
|
-
const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
|
|
223
|
-
documentFileMap.set(key, entry.filePath);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
// Determine branch from flag or profile
|
|
227
159
|
const branch = flags.branch || profile.branch || '';
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
env: flags.env.toString(),
|
|
238
|
-
partial: isPartial.toString(),
|
|
239
|
-
records: flags.records.toString(),
|
|
240
|
-
transaction: flags.transaction.toString(),
|
|
241
|
-
truncate: flags.truncate.toString(),
|
|
242
|
-
});
|
|
243
|
-
// POST the multidoc to the API
|
|
244
|
-
const requestHeaders = {
|
|
245
|
-
accept: 'application/json',
|
|
246
|
-
Authorization: `Bearer ${profile.access_token}`,
|
|
247
|
-
'Content-Type': 'text/x-xanoscript',
|
|
160
|
+
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}`;
|
|
161
|
+
const target = {
|
|
162
|
+
buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
|
|
163
|
+
buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
|
|
164
|
+
cliVersion: this.config.version,
|
|
165
|
+
instanceOrigin: profile.instance_origin,
|
|
166
|
+
label: `workspace ${workspaceId}`,
|
|
167
|
+
supportsBranches: true,
|
|
168
|
+
supportsPartial: true,
|
|
248
169
|
};
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
method: 'POST',
|
|
263
|
-
}, flags.verbose, profile.access_token);
|
|
264
|
-
if (!dryRunResponse.ok) {
|
|
265
|
-
if (dryRunResponse.status === 404) {
|
|
266
|
-
// Check if the workspace itself doesn't exist
|
|
267
|
-
const wsCheckUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}`;
|
|
268
|
-
const wsCheckResponse = await this.verboseFetch(wsCheckUrl, {
|
|
269
|
-
headers: {
|
|
270
|
-
accept: 'application/json',
|
|
271
|
-
Authorization: `Bearer ${profile.access_token}`,
|
|
272
|
-
},
|
|
273
|
-
method: 'GET',
|
|
274
|
-
}, flags.verbose, profile.access_token);
|
|
275
|
-
if (!wsCheckResponse.ok) {
|
|
276
|
-
this.error(`Workspace ${workspaceId} not found on this instance.`);
|
|
277
|
-
}
|
|
278
|
-
// Workspace exists — dry-run endpoint just not available
|
|
279
|
-
this.log('');
|
|
280
|
-
this.log(ux.colorize('dim', 'Push preview not yet available on this instance.'));
|
|
281
|
-
this.log('');
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
const errorText = await dryRunResponse.text();
|
|
285
|
-
// Check if push is disabled on this workspace
|
|
286
|
-
try {
|
|
287
|
-
const errorJson = JSON.parse(errorText);
|
|
288
|
-
if (errorJson.message?.includes('Push is disabled')) {
|
|
289
|
-
this.log('');
|
|
290
|
-
this.log(ux.colorize('red', ux.colorize('bold', 'Direct push to this workspace is disabled.')));
|
|
291
|
-
this.log(ux.colorize('dim', 'To apply changes to the workspace, use the sandbox review flow:'));
|
|
292
|
-
this.log(` ${ux.colorize('cyan', 'xano sandbox push')} ${ux.colorize('dim', '— push changes to your sandbox')}`);
|
|
293
|
-
this.log(` ${ux.colorize('cyan', 'xano sandbox review')} ${ux.colorize('dim', '— edit any logic, inspect the snapshot diff, and promote changes to the workspace')}`);
|
|
294
|
-
this.log('');
|
|
295
|
-
this.log(ux.colorize('dim', 'To enable direct push, go to Workspace Settings → CLI → Allow Direct Workspace Push.'));
|
|
296
|
-
this.log('');
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
// Not JSON, fall through
|
|
302
|
-
}
|
|
303
|
-
this.warn(`Push preview failed (${dryRunResponse.status}). Skipping preview.`);
|
|
304
|
-
if (flags.verbose) {
|
|
305
|
-
this.log(ux.colorize('dim', errorText));
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (process.stdin.isTTY) {
|
|
309
|
-
const confirmed = await this.confirm('Proceed with push?');
|
|
310
|
-
if (!confirmed) {
|
|
311
|
-
this.log('Push cancelled.');
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
this.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
317
|
-
}
|
|
318
|
-
// Skip the rest of preview logic
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
const dryRunText = await dryRunResponse.text();
|
|
322
|
-
const preview = JSON.parse(dryRunText);
|
|
323
|
-
dryRunPreview = preview;
|
|
324
|
-
// Check if the server returned a valid dry-run response
|
|
325
|
-
if (preview && preview.summary) {
|
|
326
|
-
this.renderPreview(preview, shouldDelete, workspaceId, flags.verbose, isPartial);
|
|
327
|
-
// Check for critical errors that must block the push
|
|
328
|
-
const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
|
|
329
|
-
if (criticalOps.length > 0) {
|
|
330
|
-
this.log('');
|
|
331
|
-
this.log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL ERRORS ===')));
|
|
332
|
-
this.log('');
|
|
333
|
-
this.log(ux.colorize('red', 'The following items contain syntax errors or unresolved placeholder statements'));
|
|
334
|
-
this.log(ux.colorize('red', 'that would corrupt data if pushed. These must be resolved first:'));
|
|
335
|
-
this.log('');
|
|
336
|
-
for (const op of criticalOps) {
|
|
337
|
-
this.log(` ${ux.colorize('red', 'BLOCKED'.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
338
|
-
if (op.details) {
|
|
339
|
-
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
this.log('');
|
|
343
|
-
this.log(ux.colorize('red', `Push blocked: ${criticalOps.length} critical error(s) found.`));
|
|
344
|
-
if (!flags.force) {
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
this.log(ux.colorize('yellow', 'Proceeding anyway due to --force flag.'));
|
|
348
|
-
}
|
|
349
|
-
// Check if there are any actual changes (exclude deletes when --delete is off)
|
|
350
|
-
const hasChanges = Object.values(preview.summary).some((c) => c.created > 0 || c.updated > 0 || (shouldDelete && c.deleted > 0) || c.truncated > 0);
|
|
351
|
-
// Detect if local files contain records that would be imported
|
|
352
|
-
const tablesWithRecords = flags.records
|
|
353
|
-
? documentEntries
|
|
354
|
-
.filter((d) => /^table\s+/m.test(d.content) && /\bitems\s*=\s*\[/m.test(d.content))
|
|
355
|
-
.map((d) => {
|
|
356
|
-
const nameMatch = d.content.match(/^table\s+(\S+)/m);
|
|
357
|
-
const itemsMatch = d.content.match(/\bitems\s*=\s*\[([\s\S]*?)\n\s*\]/);
|
|
358
|
-
const itemCount = itemsMatch ? (itemsMatch[1].match(/^\s*\{/gm) || []).length : 0;
|
|
359
|
-
return { name: nameMatch ? nameMatch[1] : 'unknown', records: itemCount };
|
|
360
|
-
})
|
|
361
|
-
: [];
|
|
362
|
-
const hasLocalRecords = tablesWithRecords.length > 0;
|
|
363
|
-
if (hasLocalRecords) {
|
|
364
|
-
this.log('');
|
|
365
|
-
this.log(ux.colorize('bold', '--- Records ---'));
|
|
366
|
-
this.log('');
|
|
367
|
-
for (const t of tablesWithRecords) {
|
|
368
|
-
this.log(` ${ux.colorize('yellow', 'UPSERT'.padEnd(16))} ${'table'.padEnd(18)} ${t.name} (${t.records} records)`);
|
|
369
|
-
}
|
|
370
|
-
this.log('');
|
|
371
|
-
}
|
|
372
|
-
if (!hasChanges && !hasLocalRecords) {
|
|
373
|
-
this.log('');
|
|
374
|
-
this.log('No changes to push.');
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
if (flags['dry-run']) {
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
const hasDestructive = preview.operations.some((op) => (shouldDelete && (op.action === 'delete' || op.action === 'cascade_delete')) ||
|
|
381
|
-
op.action === 'truncate' ||
|
|
382
|
-
op.action === 'drop_field' ||
|
|
383
|
-
op.action === 'alter_field');
|
|
384
|
-
const message = hasDestructive
|
|
385
|
-
? 'Proceed with push? This includes DESTRUCTIVE operations listed above.'
|
|
386
|
-
: 'Proceed with push?';
|
|
387
|
-
if (process.stdin.isTTY) {
|
|
388
|
-
const confirmed = await this.confirm(message);
|
|
389
|
-
if (!confirmed) {
|
|
390
|
-
this.log('Push cancelled.');
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
this.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
// Server returned unexpected response (older version)
|
|
400
|
-
this.log('');
|
|
401
|
-
this.log(ux.colorize('dim', 'Push preview not yet available on this instance.'));
|
|
402
|
-
this.log('');
|
|
403
|
-
if (process.stdin.isTTY) {
|
|
404
|
-
const confirmed = await this.confirm('Proceed with push?');
|
|
405
|
-
if (!confirmed) {
|
|
406
|
-
this.log('Push cancelled.');
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
this.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
catch (error) {
|
|
417
|
-
// Ctrl+C or SIGINT — exit cleanly
|
|
418
|
-
if (error.name === 'AbortError' || error.code === 'ERR_USE_AFTER_CLOSE') {
|
|
419
|
-
this.log('\nPush cancelled.');
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
// Re-throw oclif errors (e.g. from this.error()) so they exit properly
|
|
423
|
-
if (error instanceof Error && 'oclif' in error) {
|
|
424
|
-
throw error;
|
|
425
|
-
}
|
|
426
|
-
// If dry-run fails unexpectedly, proceed without preview
|
|
427
|
-
this.log('');
|
|
428
|
-
this.log(ux.colorize('dim', 'Push preview not yet available on this instance.'));
|
|
429
|
-
if (flags.verbose) {
|
|
430
|
-
this.log(ux.colorize('dim', ` ${error.message}`));
|
|
431
|
-
}
|
|
432
|
-
this.log('');
|
|
433
|
-
if (process.stdin.isTTY) {
|
|
434
|
-
const confirmed = await this.confirm('Proceed with push?');
|
|
435
|
-
if (!confirmed) {
|
|
436
|
-
this.log('Push cancelled.');
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
else {
|
|
441
|
-
this.error('Non-interactive environment detected. Use --force to skip confirmation.');
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
// For partial pushes, filter to only changed documents
|
|
446
|
-
if (isPartial && dryRunPreview) {
|
|
447
|
-
const changedKeys = new Set(dryRunPreview.operations
|
|
448
|
-
.filter((op) => op.action !== 'unchanged' && op.action !== 'delete' && op.action !== 'cascade_delete')
|
|
449
|
-
.map((op) => `${op.type}:${op.name}`));
|
|
450
|
-
const filteredEntries = documentEntries.filter((entry) => {
|
|
451
|
-
const parsed = parseDocument(entry.content);
|
|
452
|
-
if (!parsed)
|
|
453
|
-
return true;
|
|
454
|
-
// For queries, operation name includes verb (e.g., "path/{id} DELETE")
|
|
455
|
-
const opName = parsed.verb ? `${parsed.name} ${parsed.verb}` : parsed.name;
|
|
456
|
-
if (changedKeys.has(`${parsed.type}:${opName}`))
|
|
457
|
-
return true;
|
|
458
|
-
// Keep table documents that contain records when --records is active
|
|
459
|
-
if (flags.records && parsed.type === 'table' && /\bitems\s*=\s*\[/m.test(entry.content))
|
|
460
|
-
return true;
|
|
461
|
-
return false;
|
|
462
|
-
});
|
|
463
|
-
if (filteredEntries.length === 0) {
|
|
464
|
-
this.log('No changes to push.');
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
|
|
468
|
-
}
|
|
469
|
-
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc?${queryParams.toString()}`;
|
|
470
|
-
const startTime = Date.now();
|
|
471
|
-
try {
|
|
472
|
-
const response = await this.verboseFetch(apiUrl, {
|
|
473
|
-
body: multidoc,
|
|
474
|
-
headers: requestHeaders,
|
|
475
|
-
method: 'POST',
|
|
476
|
-
}, flags.verbose, profile.access_token);
|
|
477
|
-
if (!response.ok) {
|
|
478
|
-
const errorText = await response.text();
|
|
479
|
-
let errorMessage = `Push failed (${response.status})`;
|
|
480
|
-
try {
|
|
481
|
-
const errorJson = JSON.parse(errorText);
|
|
482
|
-
errorMessage += `: ${errorJson.message}`;
|
|
483
|
-
if (errorJson.payload?.param) {
|
|
484
|
-
errorMessage += `\n Parameter: ${errorJson.payload.param}`;
|
|
485
|
-
}
|
|
486
|
-
// Provide clear guidance when push is disabled
|
|
487
|
-
if (errorJson.message?.includes('Push is disabled')) {
|
|
488
|
-
this.error(`Push is disabled for this workspace.\n\n` +
|
|
489
|
-
`To enable, go to Workspace Settings and turn on "Allow Push".\n\n` +
|
|
490
|
-
`Alternatively, use sandbox commands:\n` +
|
|
491
|
-
` xano sandbox push ${args.directory}\n` +
|
|
492
|
-
` xano sandbox impersonate`);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
catch {
|
|
496
|
-
errorMessage += `\n${errorText}`;
|
|
497
|
-
}
|
|
498
|
-
// Surface local files involved in duplicate GUID errors
|
|
499
|
-
const guidMatch = errorMessage.match(/Duplicate \w+ guid: (\S+)/);
|
|
500
|
-
if (guidMatch) {
|
|
501
|
-
const dupeFiles = findFilesWithGuid(documentEntries, guidMatch[1]);
|
|
502
|
-
if (dupeFiles.length > 0) {
|
|
503
|
-
const relPaths = dupeFiles.map((f) => path.relative(inputDir, f));
|
|
504
|
-
errorMessage += `\n Local files with this GUID:\n${relPaths.map((f) => ` ${f}`).join('\n')}`;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
this.error(errorMessage);
|
|
508
|
-
}
|
|
509
|
-
// Parse the response for GUID map
|
|
510
|
-
const responseText = await response.text();
|
|
511
|
-
let guidMap = [];
|
|
512
|
-
if (responseText && responseText !== 'null') {
|
|
513
|
-
try {
|
|
514
|
-
const responseJson = JSON.parse(responseText);
|
|
515
|
-
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
516
|
-
guidMap = responseJson.guid_map;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
catch {
|
|
520
|
-
// Response is not JSON (e.g., older server version)
|
|
521
|
-
if (flags.verbose) {
|
|
522
|
-
this.log('Server response is not JSON; skipping GUID sync');
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
// Write GUIDs back to local files
|
|
527
|
-
if (flags.guids && guidMap.length > 0) {
|
|
528
|
-
// Build a secondary lookup by type:name only (without verb/api_group)
|
|
529
|
-
// for cases where the server omits those fields
|
|
530
|
-
const baseKeyMap = new Map();
|
|
531
|
-
for (const [key, fp] of documentFileMap) {
|
|
532
|
-
const baseKey = key.split(':').slice(0, 2).join(':');
|
|
533
|
-
// Only use base key if there's no ambiguity (single entry per base key)
|
|
534
|
-
if (baseKeyMap.has(baseKey)) {
|
|
535
|
-
baseKeyMap.set(baseKey, ''); // Mark as ambiguous
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
baseKeyMap.set(baseKey, fp);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
let updatedCount = 0;
|
|
542
|
-
for (const entry of guidMap) {
|
|
543
|
-
if (!entry.guid)
|
|
544
|
-
continue;
|
|
545
|
-
const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
|
|
546
|
-
let filePath = documentFileMap.get(key);
|
|
547
|
-
// Fallback: try type:name only if full key didn't match
|
|
548
|
-
if (!filePath) {
|
|
549
|
-
const baseKey = `${entry.type}:${entry.name}`;
|
|
550
|
-
const basePath = baseKeyMap.get(baseKey);
|
|
551
|
-
if (basePath) {
|
|
552
|
-
filePath = basePath;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
if (!filePath) {
|
|
556
|
-
if (flags.verbose) {
|
|
557
|
-
this.log(` No local file found for ${entry.type} "${entry.name}", skipping GUID sync`);
|
|
558
|
-
}
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
try {
|
|
562
|
-
const updated = syncGuidToFile(filePath, entry.guid);
|
|
563
|
-
if (updated)
|
|
564
|
-
updatedCount++;
|
|
565
|
-
}
|
|
566
|
-
catch (error) {
|
|
567
|
-
this.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (updatedCount > 0) {
|
|
571
|
-
this.log(`Synced ${updatedCount} GUIDs to local files`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
catch (error) {
|
|
576
|
-
if (error instanceof Error) {
|
|
577
|
-
this.error(`Failed to push multidoc: ${error.message}`);
|
|
578
|
-
}
|
|
579
|
-
else {
|
|
580
|
-
this.error(`Failed to push multidoc: ${String(error)}`);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
584
|
-
const pushedCount = multidoc.split('\n---\n').length;
|
|
585
|
-
this.log(`Pushed ${pushedCount} documents from ${args.directory} in ${elapsed}s`);
|
|
586
|
-
}
|
|
587
|
-
async confirm(message) {
|
|
588
|
-
const readline = await import('node:readline');
|
|
589
|
-
const rl = readline.createInterface({
|
|
590
|
-
input: process.stdin,
|
|
591
|
-
output: process.stdout,
|
|
592
|
-
});
|
|
593
|
-
return new Promise((resolve) => {
|
|
594
|
-
let answered = false;
|
|
595
|
-
rl.on('close', () => {
|
|
596
|
-
if (!answered)
|
|
597
|
-
resolve(false);
|
|
598
|
-
});
|
|
599
|
-
rl.question(`${message} (y/N) `, (answer) => {
|
|
600
|
-
answered = true;
|
|
601
|
-
rl.close();
|
|
602
|
-
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
renderPreview(result, willDelete, workspaceId, verbose = false, partial = false) {
|
|
607
|
-
const typeLabels = {
|
|
608
|
-
addon: 'Addons',
|
|
609
|
-
agent: 'Agents',
|
|
610
|
-
api_group: 'API Groups',
|
|
611
|
-
function: 'Functions',
|
|
612
|
-
mcp_server: 'MCP Servers',
|
|
613
|
-
middleware: 'Middleware',
|
|
614
|
-
query: 'API Endpoints',
|
|
615
|
-
realtime_channel: 'Realtime Channels',
|
|
616
|
-
table: 'Tables',
|
|
617
|
-
task: 'Tasks',
|
|
618
|
-
tool: 'Tools',
|
|
619
|
-
toolset: 'Toolsets',
|
|
620
|
-
trigger: 'Triggers',
|
|
621
|
-
workflow_test: 'Workflow Tests',
|
|
622
|
-
workspace: 'Workspace Settings',
|
|
170
|
+
const pushFlags = {
|
|
171
|
+
delete: flags.delete,
|
|
172
|
+
'dry-run': flags['dry-run'],
|
|
173
|
+
env: flags.env,
|
|
174
|
+
exclude: flags.exclude,
|
|
175
|
+
force: flags.force,
|
|
176
|
+
guids: flags.guids,
|
|
177
|
+
include: flags.include,
|
|
178
|
+
records: flags.records,
|
|
179
|
+
sync: flags.sync,
|
|
180
|
+
transaction: flags.transaction,
|
|
181
|
+
truncate: flags.truncate,
|
|
182
|
+
verbose: flags.verbose,
|
|
623
183
|
};
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
for (const [type, counts] of Object.entries(result.summary)) {
|
|
632
|
-
const label = typeLabels[type] || type;
|
|
633
|
-
const parts = [];
|
|
634
|
-
if (counts.created > 0) {
|
|
635
|
-
parts.push(ux.colorize('green', `+${counts.created} created`));
|
|
636
|
-
}
|
|
637
|
-
if (counts.updated > 0) {
|
|
638
|
-
parts.push(ux.colorize('yellow', `~${counts.updated} updated`));
|
|
639
|
-
}
|
|
640
|
-
if (willDelete && counts.deleted > 0) {
|
|
641
|
-
parts.push(ux.colorize('red', `-${counts.deleted} deleted`));
|
|
642
|
-
}
|
|
643
|
-
if (counts.truncated > 0) {
|
|
644
|
-
parts.push(ux.colorize('yellow', `${counts.truncated} truncated`));
|
|
645
|
-
}
|
|
646
|
-
if (parts.length > 0) {
|
|
647
|
-
this.log(` ${label.padEnd(20)} ${parts.join(' ')}`);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
const changes = result.operations.filter((op) => op.action === 'create' || op.action === 'update' || op.action === 'add_field' || op.action === 'update_field');
|
|
651
|
-
const destructive = result.operations.filter((op) => op.action === 'delete' ||
|
|
652
|
-
op.action === 'cascade_delete' ||
|
|
653
|
-
op.action === 'truncate' ||
|
|
654
|
-
op.action === 'drop_field' ||
|
|
655
|
-
op.action === 'alter_field');
|
|
656
|
-
if (changes.length > 0) {
|
|
657
|
-
this.log('');
|
|
658
|
-
this.log(ux.colorize('bold', '--- Changes ---'));
|
|
659
|
-
this.log('');
|
|
660
|
-
for (const op of changes) {
|
|
661
|
-
const color = op.action === 'update' || op.action === 'update_field' ? 'yellow' : 'green';
|
|
662
|
-
const actionLabel = op.action.toUpperCase();
|
|
663
|
-
this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
664
|
-
if (verbose && op.details) {
|
|
665
|
-
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
666
|
-
}
|
|
667
|
-
if (verbose && op.reason) {
|
|
668
|
-
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `reason: ${op.reason}`)}`);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
// Split destructive ops by category
|
|
673
|
-
const deleteOps = destructive.filter((op) => op.action === 'delete' || op.action === 'cascade_delete');
|
|
674
|
-
const alwaysDestructive = destructive.filter((op) => op.action === 'truncate' || op.action === 'drop_field' || op.action === 'alter_field');
|
|
675
|
-
// Show destructive operations (deletes only when --delete, truncates/drop_field always)
|
|
676
|
-
const shownDestructive = [...(willDelete ? deleteOps : []), ...alwaysDestructive];
|
|
677
|
-
if (shownDestructive.length > 0) {
|
|
678
|
-
this.log('');
|
|
679
|
-
this.log(ux.colorize('bold', '--- Destructive Operations ---'));
|
|
680
|
-
this.log('');
|
|
681
|
-
for (const op of shownDestructive) {
|
|
682
|
-
const color = op.action === 'truncate' || op.action === 'alter_field' ? 'yellow' : 'red';
|
|
683
|
-
const actionLabel = op.action.toUpperCase();
|
|
684
|
-
this.log(` ${ux.colorize(color, actionLabel.padEnd(16))} ${op.type.padEnd(18)} ${op.name}`);
|
|
685
|
-
if (verbose && op.details) {
|
|
686
|
-
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', op.details)}`);
|
|
687
|
-
}
|
|
688
|
-
if (verbose && op.reason) {
|
|
689
|
-
this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `reason: ${op.reason}`)}`);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
// Warn about potential field renames (add + drop on same table)
|
|
694
|
-
const addFieldTables = new Set(result.operations
|
|
695
|
-
.filter((op) => op.action === 'add_field')
|
|
696
|
-
.map((op) => op.name));
|
|
697
|
-
const dropFieldTables = new Set(result.operations
|
|
698
|
-
.filter((op) => op.action === 'drop_field')
|
|
699
|
-
.map((op) => op.name));
|
|
700
|
-
const renameCandidates = [...addFieldTables].filter((t) => dropFieldTables.has(t));
|
|
701
|
-
if (renameCandidates.length > 0) {
|
|
702
|
-
this.log('');
|
|
703
|
-
this.log(ux.colorize('yellow', ` Note: Table(s) ${renameCandidates.map((t) => `"${t}"`).join(', ')} have both added and dropped fields.`));
|
|
704
|
-
this.log(ux.colorize('yellow', ' If this is intended to be a field rename, use the Xano Admin — renaming is not'));
|
|
705
|
-
this.log(ux.colorize('yellow', ' currently available through the CLI or Metadata API.'));
|
|
706
|
-
}
|
|
707
|
-
// Show remote-only items when not using --delete (skip for partial pushes)
|
|
708
|
-
if (!willDelete && !partial && deleteOps.length > 0) {
|
|
709
|
-
this.log('');
|
|
710
|
-
this.log(ux.colorize('dim', '--- Remote Only (not included in push) ---'));
|
|
711
|
-
this.log('');
|
|
712
|
-
for (const op of deleteOps) {
|
|
713
|
-
this.log(ux.colorize('dim', ` ${op.type.padEnd(18)} ${op.name}`));
|
|
714
|
-
}
|
|
715
|
-
this.log('');
|
|
716
|
-
this.log(ux.colorize('dim', ` Use --delete to remove these ${deleteOps.length} item(s) from remote.`));
|
|
717
|
-
}
|
|
718
|
-
this.log('');
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Recursively collect all .xs files from a directory, sorted by
|
|
722
|
-
* type subdirectory name then filename for deterministic ordering.
|
|
723
|
-
*/
|
|
724
|
-
collectFiles(dir) {
|
|
725
|
-
const files = [];
|
|
726
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
727
|
-
for (const entry of entries) {
|
|
728
|
-
const fullPath = path.join(dir, entry.name);
|
|
729
|
-
if (entry.isDirectory()) {
|
|
730
|
-
files.push(...this.collectFiles(fullPath));
|
|
731
|
-
}
|
|
732
|
-
else if (entry.isFile() && entry.name.endsWith('.xs')) {
|
|
733
|
-
files.push(fullPath);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
return files.sort();
|
|
737
|
-
}
|
|
738
|
-
loadCredentials() {
|
|
739
|
-
const configDir = path.join(os.homedir(), '.xano');
|
|
740
|
-
const credentialsPath = path.join(configDir, 'credentials.yaml');
|
|
741
|
-
// Check if credentials file exists
|
|
742
|
-
if (!fs.existsSync(credentialsPath)) {
|
|
743
|
-
this.error(`Credentials file not found at ${credentialsPath}\n` + `Create a profile using 'xano profile:create'`);
|
|
744
|
-
}
|
|
745
|
-
// Read credentials file
|
|
746
|
-
try {
|
|
747
|
-
const fileContent = fs.readFileSync(credentialsPath, 'utf8');
|
|
748
|
-
const parsed = yaml.load(fileContent);
|
|
749
|
-
if (!parsed || typeof parsed !== 'object' || !('profiles' in parsed)) {
|
|
750
|
-
this.error('Credentials file has invalid format.');
|
|
751
|
-
}
|
|
752
|
-
return parsed;
|
|
753
|
-
}
|
|
754
|
-
catch (error) {
|
|
755
|
-
this.error(`Failed to parse credentials file: ${error}`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
const GUID_REGEX = /guid\s*=\s*(["'])([^"']*)\1/;
|
|
760
|
-
/**
|
|
761
|
-
* Sync a GUID into a local .xs file. Returns true if the file was modified.
|
|
762
|
-
*
|
|
763
|
-
* - If the file already has a matching GUID, returns false (no change).
|
|
764
|
-
* - If the file has a different GUID, updates it.
|
|
765
|
-
* - If the file has no GUID, inserts one before the final closing brace.
|
|
766
|
-
*/
|
|
767
|
-
function syncGuidToFile(filePath, guid) {
|
|
768
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
769
|
-
const existingMatch = content.match(GUID_REGEX);
|
|
770
|
-
if (existingMatch) {
|
|
771
|
-
// Already has a GUID
|
|
772
|
-
if (existingMatch[2] === guid) {
|
|
773
|
-
return false; // Already matches
|
|
774
|
-
}
|
|
775
|
-
// Update existing GUID
|
|
776
|
-
const updated = content.replace(GUID_REGEX, `guid = "${guid}"`);
|
|
777
|
-
fs.writeFileSync(filePath, updated, 'utf8');
|
|
778
|
-
return true;
|
|
779
|
-
}
|
|
780
|
-
// No GUID line exists — insert before the final closing brace of the top-level block
|
|
781
|
-
const lines = content.split('\n');
|
|
782
|
-
let insertIndex = -1;
|
|
783
|
-
// Find the last closing brace (top-level block end)
|
|
784
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
785
|
-
if (lines[i].trim() === '}') {
|
|
786
|
-
insertIndex = i;
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
if (insertIndex === -1) {
|
|
791
|
-
return false; // Could not find insertion point
|
|
792
|
-
}
|
|
793
|
-
// Determine indentation from the line above the closing brace
|
|
794
|
-
let indent = ' ';
|
|
795
|
-
for (let i = insertIndex - 1; i >= 0; i--) {
|
|
796
|
-
if (lines[i].trim()) {
|
|
797
|
-
const indentMatch = lines[i].match(/^(\s+)/);
|
|
798
|
-
if (indentMatch) {
|
|
799
|
-
indent = indentMatch[1];
|
|
800
|
-
}
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
184
|
+
await executePush({
|
|
185
|
+
accessToken: profile.access_token,
|
|
186
|
+
branch,
|
|
187
|
+
command: this,
|
|
188
|
+
inputDir,
|
|
189
|
+
verboseFetch: this.verboseFetch.bind(this),
|
|
190
|
+
}, target, pushFlags);
|
|
803
191
|
}
|
|
804
|
-
lines.splice(insertIndex, 0, `${indent}guid = "${guid}"`);
|
|
805
|
-
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
806
|
-
return true;
|
|
807
192
|
}
|