@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.
Files changed (240) hide show
  1. package/README.md +111 -70
  2. package/dist/base-command.d.ts +16 -1
  3. package/dist/base-command.js +57 -5
  4. package/dist/commands/auth/index.d.ts +1 -0
  5. package/dist/commands/auth/index.js +15 -10
  6. package/dist/commands/branch/create/index.d.ts +4 -1
  7. package/dist/commands/branch/create/index.js +22 -21
  8. package/dist/commands/branch/delete/index.d.ts +1 -0
  9. package/dist/commands/branch/delete/index.js +1 -4
  10. package/dist/commands/branch/edit/index.d.ts +1 -0
  11. package/dist/commands/branch/edit/index.js +1 -4
  12. package/dist/commands/branch/get/index.d.ts +1 -0
  13. package/dist/commands/branch/get/index.js +1 -4
  14. package/dist/commands/branch/list/index.d.ts +2 -6
  15. package/dist/commands/branch/list/index.js +13 -17
  16. package/dist/commands/branch/set_live/index.d.ts +1 -0
  17. package/dist/commands/branch/set_live/index.js +1 -4
  18. package/dist/commands/function/create/index.d.ts +1 -0
  19. package/dist/commands/function/create/index.js +1 -2
  20. package/dist/commands/function/edit/index.d.ts +1 -0
  21. package/dist/commands/function/edit/index.js +1 -2
  22. package/dist/commands/function/get/index.d.ts +1 -0
  23. package/dist/commands/function/get/index.js +1 -4
  24. package/dist/commands/function/list/index.d.ts +1 -0
  25. package/dist/commands/function/list/index.js +1 -4
  26. package/dist/commands/platform/get/index.d.ts +1 -0
  27. package/dist/commands/platform/get/index.js +1 -4
  28. package/dist/commands/platform/list/index.d.ts +1 -0
  29. package/dist/commands/platform/list/index.js +1 -4
  30. package/dist/commands/profile/create/index.d.ts +1 -0
  31. package/dist/commands/profile/create/index.js +10 -4
  32. package/dist/commands/profile/delete/index.d.ts +1 -0
  33. package/dist/commands/profile/delete/index.js +8 -4
  34. package/dist/commands/profile/edit/index.d.ts +1 -0
  35. package/dist/commands/profile/edit/index.js +1 -4
  36. package/dist/commands/profile/get/index.d.ts +3 -0
  37. package/dist/commands/profile/get/index.js +12 -5
  38. package/dist/commands/profile/list/index.d.ts +1 -0
  39. package/dist/commands/profile/list/index.js +8 -4
  40. package/dist/commands/profile/me/index.d.ts +1 -0
  41. package/dist/commands/profile/me/index.js +1 -4
  42. package/dist/commands/profile/set/index.d.ts +3 -0
  43. package/dist/commands/profile/set/index.js +12 -6
  44. package/dist/commands/profile/token/index.d.ts +3 -0
  45. package/dist/commands/profile/token/index.js +12 -5
  46. package/dist/commands/profile/wizard/index.d.ts +1 -0
  47. package/dist/commands/profile/wizard/index.js +13 -9
  48. package/dist/commands/profile/workspace/index.d.ts +3 -0
  49. package/dist/commands/profile/workspace/index.js +12 -5
  50. package/dist/commands/profile/workspace/set/index.d.ts +1 -0
  51. package/dist/commands/profile/workspace/set/index.js +1 -3
  52. package/dist/commands/release/create/index.d.ts +4 -1
  53. package/dist/commands/release/create/index.js +12 -14
  54. package/dist/commands/release/delete/index.d.ts +1 -0
  55. package/dist/commands/release/delete/index.js +1 -4
  56. package/dist/commands/release/deploy/index.d.ts +3 -0
  57. package/dist/commands/release/deploy/index.js +31 -1
  58. package/dist/commands/release/edit/index.d.ts +1 -0
  59. package/dist/commands/release/edit/index.js +1 -4
  60. package/dist/commands/release/export/index.d.ts +1 -0
  61. package/dist/commands/release/export/index.js +1 -3
  62. package/dist/commands/release/get/index.d.ts +1 -0
  63. package/dist/commands/release/get/index.js +1 -4
  64. package/dist/commands/release/import/index.d.ts +1 -0
  65. package/dist/commands/release/import/index.js +1 -3
  66. package/dist/commands/release/list/index.d.ts +1 -0
  67. package/dist/commands/release/list/index.js +1 -4
  68. package/dist/commands/release/pull/index.d.ts +2 -3
  69. package/dist/commands/release/pull/index.js +19 -18
  70. package/dist/commands/release/push/index.d.ts +2 -3
  71. package/dist/commands/release/push/index.js +19 -22
  72. package/dist/commands/sandbox/delete/index.d.ts +13 -0
  73. package/dist/commands/sandbox/delete/index.js +71 -0
  74. package/dist/commands/sandbox/env/delete/index.d.ts +1 -0
  75. package/dist/commands/sandbox/env/delete/index.js +4 -2
  76. package/dist/commands/sandbox/env/get/index.d.ts +1 -0
  77. package/dist/commands/sandbox/env/get/index.js +4 -2
  78. package/dist/commands/sandbox/env/get_all/index.d.ts +1 -0
  79. package/dist/commands/sandbox/env/get_all/index.js +4 -2
  80. package/dist/commands/sandbox/env/list/index.d.ts +1 -0
  81. package/dist/commands/sandbox/env/list/index.js +4 -2
  82. package/dist/commands/sandbox/env/set/index.d.ts +1 -0
  83. package/dist/commands/sandbox/env/set/index.js +4 -2
  84. package/dist/commands/sandbox/env/set_all/index.d.ts +1 -0
  85. package/dist/commands/sandbox/env/set_all/index.js +4 -2
  86. package/dist/commands/sandbox/get/index.d.ts +1 -0
  87. package/dist/commands/sandbox/get/index.js +2 -0
  88. package/dist/commands/sandbox/license/get/index.d.ts +1 -0
  89. package/dist/commands/sandbox/license/get/index.js +4 -2
  90. package/dist/commands/sandbox/license/set/index.d.ts +1 -0
  91. package/dist/commands/sandbox/license/set/index.js +4 -2
  92. package/dist/commands/sandbox/pull/index.d.ts +2 -3
  93. package/dist/commands/sandbox/pull/index.js +19 -14
  94. package/dist/commands/sandbox/push/index.d.ts +12 -4
  95. package/dist/commands/sandbox/push/index.js +150 -95
  96. package/dist/commands/sandbox/reset/index.d.ts +1 -0
  97. package/dist/commands/sandbox/reset/index.js +4 -2
  98. package/dist/commands/sandbox/review/index.d.ts +1 -0
  99. package/dist/commands/sandbox/review/index.js +4 -2
  100. package/dist/commands/sandbox/unit_test/list/index.d.ts +1 -0
  101. package/dist/commands/sandbox/unit_test/list/index.js +4 -2
  102. package/dist/commands/sandbox/unit_test/run/index.d.ts +1 -0
  103. package/dist/commands/sandbox/unit_test/run/index.js +4 -2
  104. package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
  105. package/dist/commands/sandbox/unit_test/run_all/index.js +4 -0
  106. package/dist/commands/sandbox/workflow_test/list/index.d.ts +1 -0
  107. package/dist/commands/sandbox/workflow_test/list/index.js +4 -2
  108. package/dist/commands/sandbox/workflow_test/run/index.d.ts +1 -0
  109. package/dist/commands/sandbox/workflow_test/run/index.js +4 -2
  110. package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
  111. package/dist/commands/sandbox/workflow_test/run_all/index.js +4 -0
  112. package/dist/commands/static_host/build/create/index.d.ts +1 -0
  113. package/dist/commands/static_host/build/create/index.js +1 -3
  114. package/dist/commands/static_host/build/get/index.d.ts +1 -0
  115. package/dist/commands/static_host/build/get/index.js +1 -4
  116. package/dist/commands/static_host/build/list/index.d.ts +1 -0
  117. package/dist/commands/static_host/build/list/index.js +1 -4
  118. package/dist/commands/static_host/list/index.d.ts +1 -0
  119. package/dist/commands/static_host/list/index.js +1 -4
  120. package/dist/commands/tenant/backup/create/index.d.ts +1 -0
  121. package/dist/commands/tenant/backup/create/index.js +1 -4
  122. package/dist/commands/tenant/backup/delete/index.d.ts +1 -0
  123. package/dist/commands/tenant/backup/delete/index.js +1 -4
  124. package/dist/commands/tenant/backup/export/index.d.ts +1 -0
  125. package/dist/commands/tenant/backup/export/index.js +1 -3
  126. package/dist/commands/tenant/backup/import/index.d.ts +1 -0
  127. package/dist/commands/tenant/backup/import/index.js +1 -3
  128. package/dist/commands/tenant/backup/list/index.d.ts +1 -0
  129. package/dist/commands/tenant/backup/list/index.js +1 -4
  130. package/dist/commands/tenant/backup/restore/index.d.ts +1 -0
  131. package/dist/commands/tenant/backup/restore/index.js +1 -4
  132. package/dist/commands/tenant/cluster/create/index.d.ts +1 -0
  133. package/dist/commands/tenant/cluster/create/index.js +1 -3
  134. package/dist/commands/tenant/cluster/delete/index.d.ts +1 -0
  135. package/dist/commands/tenant/cluster/delete/index.js +1 -4
  136. package/dist/commands/tenant/cluster/edit/index.d.ts +1 -0
  137. package/dist/commands/tenant/cluster/edit/index.js +1 -4
  138. package/dist/commands/tenant/cluster/get/index.d.ts +1 -0
  139. package/dist/commands/tenant/cluster/get/index.js +1 -4
  140. package/dist/commands/tenant/cluster/license/get/index.d.ts +1 -0
  141. package/dist/commands/tenant/cluster/license/get/index.js +1 -3
  142. package/dist/commands/tenant/cluster/license/set/index.d.ts +1 -0
  143. package/dist/commands/tenant/cluster/license/set/index.js +1 -3
  144. package/dist/commands/tenant/cluster/list/index.d.ts +1 -0
  145. package/dist/commands/tenant/cluster/list/index.js +1 -4
  146. package/dist/commands/tenant/create/index.d.ts +1 -0
  147. package/dist/commands/tenant/create/index.js +1 -3
  148. package/dist/commands/tenant/delete/index.d.ts +1 -0
  149. package/dist/commands/tenant/delete/index.js +1 -4
  150. package/dist/commands/tenant/deploy_platform/index.d.ts +1 -0
  151. package/dist/commands/tenant/deploy_platform/index.js +1 -3
  152. package/dist/commands/tenant/deploy_release/index.d.ts +1 -0
  153. package/dist/commands/tenant/deploy_release/index.js +1 -4
  154. package/dist/commands/tenant/edit/index.d.ts +1 -0
  155. package/dist/commands/tenant/edit/index.js +1 -4
  156. package/dist/commands/tenant/env/delete/index.d.ts +1 -0
  157. package/dist/commands/tenant/env/delete/index.js +1 -4
  158. package/dist/commands/tenant/env/get/index.d.ts +1 -0
  159. package/dist/commands/tenant/env/get/index.js +1 -4
  160. package/dist/commands/tenant/env/get_all/index.d.ts +1 -0
  161. package/dist/commands/tenant/env/get_all/index.js +1 -3
  162. package/dist/commands/tenant/env/list/index.d.ts +1 -0
  163. package/dist/commands/tenant/env/list/index.js +1 -4
  164. package/dist/commands/tenant/env/set/index.d.ts +1 -0
  165. package/dist/commands/tenant/env/set/index.js +1 -4
  166. package/dist/commands/tenant/env/set_all/index.d.ts +1 -0
  167. package/dist/commands/tenant/env/set_all/index.js +1 -3
  168. package/dist/commands/tenant/get/index.d.ts +1 -0
  169. package/dist/commands/tenant/get/index.js +1 -4
  170. package/dist/commands/tenant/impersonate/index.d.ts +1 -0
  171. package/dist/commands/tenant/impersonate/index.js +1 -4
  172. package/dist/commands/tenant/license/get/index.d.ts +1 -0
  173. package/dist/commands/tenant/license/get/index.js +1 -3
  174. package/dist/commands/tenant/license/set/index.d.ts +1 -0
  175. package/dist/commands/tenant/license/set/index.js +1 -3
  176. package/dist/commands/tenant/list/index.d.ts +1 -0
  177. package/dist/commands/tenant/list/index.js +1 -4
  178. package/dist/commands/tenant/pull/index.d.ts +2 -3
  179. package/dist/commands/tenant/pull/index.js +20 -21
  180. package/dist/commands/tenant/push/index.d.ts +2 -22
  181. package/dist/commands/tenant/push/index.js +7 -225
  182. package/dist/commands/tenant/unit_test/list/index.d.ts +1 -0
  183. package/dist/commands/tenant/unit_test/list/index.js +2 -27
  184. package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
  185. package/dist/commands/tenant/unit_test/run/index.js +2 -27
  186. package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
  187. package/dist/commands/tenant/unit_test/run_all/index.js +2 -27
  188. package/dist/commands/tenant/workflow_test/list/index.d.ts +1 -0
  189. package/dist/commands/tenant/workflow_test/list/index.js +2 -27
  190. package/dist/commands/tenant/workflow_test/run/index.d.ts +1 -0
  191. package/dist/commands/tenant/workflow_test/run/index.js +2 -27
  192. package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
  193. package/dist/commands/tenant/workflow_test/run_all/index.js +2 -27
  194. package/dist/commands/unit_test/list/index.d.ts +1 -0
  195. package/dist/commands/unit_test/list/index.js +1 -4
  196. package/dist/commands/unit_test/run/index.d.ts +1 -0
  197. package/dist/commands/unit_test/run/index.js +1 -4
  198. package/dist/commands/unit_test/run_all/index.d.ts +1 -0
  199. package/dist/commands/unit_test/run_all/index.js +1 -4
  200. package/dist/commands/update/index.d.ts +1 -0
  201. package/dist/commands/workflow_test/delete/index.d.ts +1 -0
  202. package/dist/commands/workflow_test/delete/index.js +1 -4
  203. package/dist/commands/workflow_test/get/index.d.ts +1 -0
  204. package/dist/commands/workflow_test/get/index.js +1 -4
  205. package/dist/commands/workflow_test/list/index.d.ts +1 -0
  206. package/dist/commands/workflow_test/list/index.js +1 -4
  207. package/dist/commands/workflow_test/run/index.d.ts +1 -0
  208. package/dist/commands/workflow_test/run/index.js +1 -4
  209. package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
  210. package/dist/commands/workflow_test/run_all/index.js +1 -4
  211. package/dist/commands/workspace/create/index.d.ts +1 -0
  212. package/dist/commands/workspace/create/index.js +1 -4
  213. package/dist/commands/workspace/delete/index.d.ts +2 -6
  214. package/dist/commands/workspace/delete/index.js +17 -16
  215. package/dist/commands/workspace/edit/index.d.ts +2 -6
  216. package/dist/commands/workspace/edit/index.js +16 -20
  217. package/dist/commands/workspace/get/index.d.ts +2 -6
  218. package/dist/commands/workspace/get/index.js +14 -18
  219. package/dist/commands/workspace/git/pull/index.d.ts +2 -3
  220. package/dist/commands/workspace/git/pull/index.js +18 -17
  221. package/dist/commands/workspace/list/index.d.ts +1 -0
  222. package/dist/commands/workspace/list/index.js +1 -4
  223. package/dist/commands/workspace/pull/index.d.ts +2 -3
  224. package/dist/commands/workspace/pull/index.js +21 -24
  225. package/dist/commands/workspace/push/index.d.ts +7 -16
  226. package/dist/commands/workspace/push/index.js +85 -700
  227. package/dist/utils/multidoc-push.d.ts +63 -0
  228. package/dist/utils/multidoc-push.js +690 -0
  229. package/dist/utils/reference-checker.d.ts +57 -0
  230. package/dist/utils/reference-checker.js +232 -0
  231. package/oclif.manifest.json +3562 -2647
  232. package/package.json +1 -1
  233. package/dist/commands/sandbox/workflow_test/delete/index.d.ts +0 -17
  234. package/dist/commands/sandbox/workflow_test/delete/index.js +0 -59
  235. package/dist/commands/sandbox/workflow_test/get/index.d.ts +0 -17
  236. package/dist/commands/sandbox/workflow_test/get/index.js +0 -58
  237. package/dist/commands/tenant/workflow_test/delete/index.d.ts +0 -19
  238. package/dist/commands/tenant/workflow_test/delete/index.js +0 -110
  239. package/dist/commands/tenant/workflow_test/get/index.d.ts +0 -19
  240. package/dist/commands/tenant/workflow_test/get/index.js +0 -112
@@ -1,63 +1,57 @@
1
- import { Args, Flags, ux } from '@oclif/core';
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 * as os from 'node:os';
6
- import * as path from 'node:path';
3
+ import { resolve } from 'node:path';
7
4
  import BaseCommand from '../../../base-command.js';
8
- import { buildDocumentKey, findFilesWithGuid, parseDocument } from '../../../utils/document-parser.js';
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 ./my-workspace
19
- Push only changed files (default partial mode)
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 ./my-workspace --sync
15
+ `$ xano workspace push --sync
22
16
  Push all files to the workspace
23
17
  `,
24
- `$ xano workspace push ./my-workspace --sync --delete
18
+ `$ xano workspace push --sync --delete
25
19
  Push all files and delete remote objects not included
26
20
  `,
27
- `$ xano workspace push ./my-workspace --dry-run
21
+ `$ xano workspace push --dry-run
28
22
  Preview changes without pushing
29
23
  `,
30
- `$ xano workspace push ./my-workspace --force
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 ./backup --profile production
37
- Pushed 58 documents from ./backup
30
+ `$ xano workspace push --profile production
31
+ Pushed 58 documents
38
32
  `,
39
- `$ xano workspace push ./my-workspace -b dev
40
- Pushed 42 documents from ./my-workspace
33
+ `$ xano workspace push -b dev
34
+ Pushed 42 documents
41
35
  `,
42
- `$ xano workspace push ./my-workspace --no-records
36
+ `$ xano workspace push --no-records
43
37
  Push schema only, skip importing table records
44
38
  `,
45
- `$ xano workspace push ./my-workspace --no-env
39
+ `$ xano workspace push --no-env
46
40
  Push without overwriting environment variables
47
41
  `,
48
- `$ xano workspace push ./my-workspace --truncate
42
+ `$ xano workspace push --truncate
49
43
  Truncate all table records before importing
50
44
  `,
51
- `$ xano workspace push ./my-workspace -i "**/func*"
45
+ `$ xano workspace push -i "**/func*"
52
46
  Push only files matching the glob pattern
53
47
  `,
54
- `$ xano workspace push ./my-workspace -i "function/*" -i "table/*"
48
+ `$ xano workspace push -i "function/*" -i "table/*"
55
49
  Push files matching multiple patterns
56
50
  `,
57
- `$ xano workspace push ./my-workspace -e "table/*"
51
+ `$ xano workspace push -e "table/*"
58
52
  Push all files except tables
59
53
  `,
60
- `$ xano workspace push ./my-workspace -i "function/*" -e "**/test*"
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
- sync: Flags.boolean({
87
- default: false,
88
- description: 'Full push send all files, not just changed ones. Required for --delete.',
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
- records: Flags.boolean({
92
+ force: Flags.boolean({
92
93
  default: false,
93
- description: 'Include records in import',
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 { args, flags } = await this.parse(Push);
138
- // Get profile name (default or from flag/env)
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 <directory> -w <workspace_id>\n` +
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
- // Resolve the input directory
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 isPartial = !flags.sync;
229
- if (flags.delete && isPartial) {
230
- this.error('Cannot use --delete without --sync');
231
- }
232
- const shouldDelete = isPartial ? false : flags.delete;
233
- // Construct the API URL
234
- const queryParams = new URLSearchParams({
235
- branch,
236
- delete: shouldDelete.toString(),
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
- // Preview mode: show what would change before pushing
250
- let dryRunPreview = null;
251
- if (flags['dry-run'] || !flags.force) {
252
- const dryRunParams = new URLSearchParams(queryParams);
253
- // Request delete info in dry-run so we can show remote-only items (skip for partial)
254
- if (!isPartial) {
255
- dryRunParams.set('delete', 'true');
256
- }
257
- const dryRunUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/multidoc/dry-run?${dryRunParams.toString()}`;
258
- try {
259
- const dryRunResponse = await this.verboseFetch(dryRunUrl, {
260
- body: multidoc,
261
- headers: requestHeaders,
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
- this.log('');
625
- const wsLabel = result.workspace_name ? `${result.workspace_name} (${workspaceId})` : `Workspace ${workspaceId}`;
626
- this.log(ux.colorize('bold', `=== Push Preview: ${wsLabel} ===`));
627
- if (!partial) {
628
- this.log(ux.colorize('red', ' --sync: all documents will be sent, including unchanged'));
629
- }
630
- this.log('');
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
  }