@xano/cli 1.2.0 → 1.3.1-beta.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 (192) hide show
  1. package/README.md +381 -4
  2. package/dist/base-command.d.ts +54 -36
  3. package/dist/base-command.js +246 -169
  4. package/dist/commands/auth/index.d.ts +1 -1
  5. package/dist/commands/auth/index.js +7 -7
  6. package/dist/commands/branch/create/index.d.ts +1 -1
  7. package/dist/commands/branch/create/index.js +1 -1
  8. package/dist/commands/branch/set_live/index.d.ts +22 -0
  9. package/dist/commands/branch/set_live/index.js +60 -1
  10. package/dist/commands/ephemeral/create/index.d.ts +18 -0
  11. package/dist/commands/ephemeral/create/index.js +103 -0
  12. package/dist/commands/ephemeral/delete/index.d.ts +18 -0
  13. package/dist/commands/ephemeral/delete/index.js +99 -0
  14. package/dist/commands/ephemeral/edit/index.d.ts +18 -0
  15. package/dist/commands/ephemeral/edit/index.js +109 -0
  16. package/dist/commands/ephemeral/get/index.d.ts +21 -0
  17. package/dist/commands/ephemeral/get/index.js +125 -0
  18. package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
  19. package/dist/commands/ephemeral/impersonate/index.js +120 -0
  20. package/dist/commands/ephemeral/list/index.d.ts +19 -0
  21. package/dist/commands/ephemeral/list/index.js +123 -0
  22. package/dist/commands/ephemeral/pull/index.d.ts +19 -0
  23. package/dist/commands/ephemeral/pull/index.js +163 -0
  24. package/dist/commands/ephemeral/push/index.d.ts +35 -0
  25. package/dist/commands/ephemeral/push/index.js +228 -0
  26. package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
  27. package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
  28. package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
  29. package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
  30. package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
  31. package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
  32. package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
  33. package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
  34. package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
  35. package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
  36. package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
  37. package/dist/commands/ephemeral/static_host/create/index.js +92 -0
  38. package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
  39. package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
  40. package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
  41. package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
  42. package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
  43. package/dist/commands/ephemeral/static_host/get/index.js +101 -0
  44. package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
  45. package/dist/commands/ephemeral/static_host/list/index.js +132 -0
  46. package/dist/commands/flatten/index.d.ts +34 -0
  47. package/dist/commands/flatten/index.js +140 -0
  48. package/dist/commands/function/create/index.d.ts +15 -0
  49. package/dist/commands/function/create/index.js +41 -1
  50. package/dist/commands/function/edit/index.d.ts +15 -0
  51. package/dist/commands/function/edit/index.js +66 -32
  52. package/dist/commands/function/get/index.js +2 -3
  53. package/dist/commands/function/run/index.d.ts +1 -0
  54. package/dist/commands/function/run/index.js +19 -6
  55. package/dist/commands/policy/catalogue/index.d.ts +15 -0
  56. package/dist/commands/policy/catalogue/index.js +29 -0
  57. package/dist/commands/policy/create/index.d.ts +18 -0
  58. package/dist/commands/policy/create/index.js +39 -0
  59. package/dist/commands/policy/delete/index.d.ts +19 -0
  60. package/dist/commands/policy/delete/index.js +61 -0
  61. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  62. package/dist/commands/policy/evaluate/index.js +71 -0
  63. package/dist/commands/policy/list/index.d.ts +14 -0
  64. package/dist/commands/policy/list/index.js +21 -0
  65. package/dist/commands/policy/parse/index.d.ts +19 -0
  66. package/dist/commands/policy/parse/index.js +18 -0
  67. package/dist/commands/policy/publish/index.d.ts +20 -0
  68. package/dist/commands/policy/publish/index.js +38 -0
  69. package/dist/commands/policy/runs/index.d.ts +54 -0
  70. package/dist/commands/policy/runs/index.js +273 -0
  71. package/dist/commands/policy/status/index.d.ts +21 -0
  72. package/dist/commands/policy/status/index.js +54 -0
  73. package/dist/commands/profile/create/index.d.ts +1 -1
  74. package/dist/commands/profile/create/index.js +7 -7
  75. package/dist/commands/profile/me/index.js +1 -1
  76. package/dist/commands/profile/use/index.js +0 -4
  77. package/dist/commands/profile/wizard/index.d.ts +1 -1
  78. package/dist/commands/profile/wizard/index.js +12 -10
  79. package/dist/commands/profile/workspace/set/index.js +1 -1
  80. package/dist/commands/release/create/index.js +7 -0
  81. package/dist/commands/release/deploy/index.d.ts +28 -0
  82. package/dist/commands/release/deploy/index.js +90 -6
  83. package/dist/commands/release/edit/index.js +2 -2
  84. package/dist/commands/release/export/index.js +1 -2
  85. package/dist/commands/release/import/index.js +6 -2
  86. package/dist/commands/release/list/index.d.ts +7 -0
  87. package/dist/commands/release/list/index.js +54 -10
  88. package/dist/commands/release/pull/index.d.ts +0 -6
  89. package/dist/commands/release/pull/index.js +13 -99
  90. package/dist/commands/release/push/index.js +21 -3
  91. package/dist/commands/sandbox/env/get_all/index.js +1 -1
  92. package/dist/commands/sandbox/env/set_all/index.js +1 -1
  93. package/dist/commands/sandbox/get/index.js +1 -1
  94. package/dist/commands/sandbox/license/get/index.js +1 -1
  95. package/dist/commands/sandbox/license/set/index.js +1 -1
  96. package/dist/commands/sandbox/pull/index.d.ts +0 -1
  97. package/dist/commands/sandbox/pull/index.js +13 -81
  98. package/dist/commands/sandbox/push/index.d.ts +1 -1
  99. package/dist/commands/sandbox/push/index.js +11 -6
  100. package/dist/commands/skills/pull/index.d.ts +30 -0
  101. package/dist/commands/skills/pull/index.js +118 -0
  102. package/dist/commands/static_host/build/create/index.d.ts +1 -1
  103. package/dist/commands/static_host/build/create/index.js +2 -2
  104. package/dist/commands/static_host/build/pull/index.js +1 -1
  105. package/dist/commands/static_host/build/push/index.js +1 -1
  106. package/dist/commands/static_host/edit/index.js +1 -1
  107. package/dist/commands/tenant/backup/export/index.js +1 -2
  108. package/dist/commands/tenant/backup/import/index.js +1 -1
  109. package/dist/commands/tenant/cluster/create/index.js +1 -1
  110. package/dist/commands/tenant/cluster/license/get/index.js +1 -1
  111. package/dist/commands/tenant/cluster/license/set/index.js +1 -1
  112. package/dist/commands/tenant/create/index.d.ts +4 -1
  113. package/dist/commands/tenant/create/index.js +26 -5
  114. package/dist/commands/tenant/deploy_platform/index.js +1 -1
  115. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  116. package/dist/commands/tenant/deploy_release/index.js +186 -4
  117. package/dist/commands/tenant/edit/index.d.ts +2 -0
  118. package/dist/commands/tenant/edit/index.js +46 -11
  119. package/dist/commands/tenant/env/get_all/index.js +1 -1
  120. package/dist/commands/tenant/env/set_all/index.js +1 -1
  121. package/dist/commands/tenant/impersonate/index.d.ts +1 -1
  122. package/dist/commands/tenant/impersonate/index.js +14 -14
  123. package/dist/commands/tenant/license/get/index.js +1 -1
  124. package/dist/commands/tenant/license/set/index.js +1 -1
  125. package/dist/commands/tenant/pull/index.d.ts +0 -6
  126. package/dist/commands/tenant/pull/index.js +14 -100
  127. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  128. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  129. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  130. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  131. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  132. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  133. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  134. package/dist/commands/tenant_deploy_request/get/index.js +116 -0
  135. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  136. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  137. package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
  138. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  139. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  140. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  141. package/dist/commands/workspace/git/pull/index.d.ts +4 -4
  142. package/dist/commands/workspace/git/pull/index.js +158 -97
  143. package/dist/commands/workspace/pull/index.d.ts +9 -5
  144. package/dist/commands/workspace/pull/index.js +83 -106
  145. package/dist/commands/workspace/push/index.d.ts +19 -0
  146. package/dist/commands/workspace/push/index.js +124 -5
  147. package/dist/help.js +4 -2
  148. package/dist/policy-command.d.ts +90 -0
  149. package/dist/policy-command.js +94 -0
  150. package/dist/update-check.d.ts +1 -1
  151. package/dist/update-check.js +1 -1
  152. package/dist/utils/document-parser.d.ts +126 -0
  153. package/dist/utils/document-parser.js +332 -4
  154. package/dist/utils/flatten.d.ts +36 -0
  155. package/dist/utils/flatten.js +68 -0
  156. package/dist/utils/knowledge-sync.d.ts +1 -0
  157. package/dist/utils/knowledge-sync.js +11 -6
  158. package/dist/utils/microservice-wait.d.ts +53 -0
  159. package/dist/utils/microservice-wait.js +84 -0
  160. package/dist/utils/multidoc-push.d.ts +143 -2
  161. package/dist/utils/multidoc-push.js +479 -40
  162. package/dist/utils/paging.d.ts +2 -1
  163. package/dist/utils/paging.js +0 -1
  164. package/dist/utils/policy/catalogue.d.ts +7 -0
  165. package/dist/utils/policy/catalogue.js +72 -0
  166. package/dist/utils/policy/errors.d.ts +12 -0
  167. package/dist/utils/policy/errors.js +61 -0
  168. package/dist/utils/policy/feedback.d.ts +68 -0
  169. package/dist/utils/policy/feedback.js +165 -0
  170. package/dist/utils/policy/findings.d.ts +23 -0
  171. package/dist/utils/policy/findings.js +65 -0
  172. package/dist/utils/policy/gate.d.ts +161 -0
  173. package/dist/utils/policy/gate.js +201 -0
  174. package/dist/utils/policy/goal.d.ts +46 -0
  175. package/dist/utils/policy/goal.js +95 -0
  176. package/dist/utils/policy/permission.d.ts +23 -0
  177. package/dist/utils/policy/permission.js +84 -0
  178. package/dist/utils/policy/push-gate.d.ts +5 -0
  179. package/dist/utils/policy/push-gate.js +29 -0
  180. package/dist/utils/policy/release.d.ts +85 -0
  181. package/dist/utils/policy/release.js +115 -0
  182. package/dist/utils/policy/request.d.ts +55 -0
  183. package/dist/utils/policy/request.js +115 -0
  184. package/dist/utils/policy/runs.d.ts +30 -0
  185. package/dist/utils/policy/runs.js +101 -0
  186. package/dist/utils/policy/status.d.ts +47 -0
  187. package/dist/utils/policy/status.js +117 -0
  188. package/dist/utils/policy/types.d.ts +185 -0
  189. package/dist/utils/policy/types.js +1 -0
  190. package/dist/utils/reference-checker.js +14 -3
  191. package/oclif.manifest.json +8538 -3445
  192. package/package.json +16 -1
@@ -15,6 +15,11 @@ export interface CredentialsFile {
15
15
  };
16
16
  }
17
17
  export declare function buildUserAgent(version: string): string;
18
+ /**
19
+ * True when `error` is the "foreign dispatcher rejected by built-in fetch"
20
+ * failure (Node 26+). The real code lives on `error.cause.code`.
21
+ */
22
+ export declare function isInvalidDispatcherError(error: unknown): boolean;
18
23
  export interface SandboxTenant {
19
24
  created_at?: string;
20
25
  description?: string;
@@ -22,7 +27,7 @@ export interface SandboxTenant {
22
27
  ephemeral?: boolean;
23
28
  id: number;
24
29
  name: string;
25
- sandbox_expires_at?: string | number;
30
+ sandbox_expires_at?: number | string;
26
31
  state?: string;
27
32
  xano_domain?: string;
28
33
  }
@@ -52,57 +57,40 @@ export default abstract class BaseCommand extends Command {
52
57
  config: LocalProfileConfig;
53
58
  path: string;
54
59
  };
55
- protected updateNotice: string | null;
56
- init(): Promise<void>;
57
- /**
58
- * Find and parse the nearest project-local profile.yaml, unless an explicit
59
- * -p/XANO_PROFILE was given (in which case the local file is ignored).
60
- */
61
- private loadLocalProfile;
62
- /** Print the one-line target banner when a local profile.yaml is in effect. */
63
- private maybePrintLocalProfileBanner;
64
- finally(_: Error | undefined): Promise<void>;
65
- private isJsonOutput;
60
+ protected updateNotice: null | string;
66
61
  /**
67
62
  * Apply insecure TLS mode if the active profile has insecure: true.
68
63
  * Sets NODE_TLS_REJECT_UNAUTHORIZED=0 so all fetch() calls skip cert verification.
69
64
  */
70
65
  protected applyInsecureFromProfile(): void;
71
- protected getDefaultProfile(): string;
72
- protected getProfile(): string | undefined;
66
+ /**
67
+ * Report a failure as exit 1 for commands that reserve exit 2 for findings: operational errors,
68
+ * flag errors and oclif's own default exit 2 all become 1, and under `-o json` the failure is also
69
+ * written to stdout as `{"error": {"exit": 1, "message"}}`. A deliberate exit 0 passes through.
70
+ */
71
+ protected catchAsOperational(error: Error & {
72
+ oclif?: {
73
+ exit?: number;
74
+ };
75
+ }): Promise<void>;
76
+ finally(_: Error | undefined): Promise<void>;
73
77
  /**
74
78
  * Get the resolved credentials file path, respecting --config flag and XANO_CONFIG env var.
75
79
  * Reads -c/--config from process.argv directly because oclif doesn't set this.flags
76
80
  * from parsed results — the static flags property is the flag definition, not parsed values.
77
81
  */
78
82
  protected getCredentialsPath(): string;
79
- protected loadCredentialsFile(): CredentialsFile | null;
83
+ protected getDefaultProfile(): string;
80
84
  /**
81
85
  * Get or create the singleton sandbox environment for the authenticated user.
82
86
  * Returns the sandbox object (existing or newly created).
83
87
  */
84
88
  protected getOrCreateSandbox(profile: ProfileConfig, verbose: boolean): Promise<SandboxTenant>;
85
- /**
86
- * Resolve the profile from flags and any project-local profile.yaml,
87
- * validating instance_origin and access_token exist.
88
- * Precedence: -p/XANO_PROFILE > profile.yaml > credentials default.
89
- */
90
- protected resolveProfile(flags: {
91
- profile?: string;
92
- }): {
93
- profile: ProfileConfig;
94
- profileName: string;
95
- };
96
- /**
97
- * Parse an API error response and return a clean error message.
98
- * Extracts the message from JSON responses and adds context for common errors.
99
- */
100
- protected parseApiError(response: Response, fallbackPrefix: string): Promise<string>;
101
- /**
102
- * Make an HTTP request with optional verbose logging.
103
- * Use this for all Metadata API calls to support the --verbose flag.
104
- */
105
- protected verboseFetch(url: string, options: RequestInit, verbose: boolean, authToken?: string): Promise<Response>;
89
+ protected getProfile(): string | undefined;
90
+ init(): Promise<void>;
91
+ /** Raw-argv check: the parsed flags are not available yet when the banner prints. */
92
+ protected isJsonOutput(): boolean;
93
+ protected loadCredentialsFile(): CredentialsFile | null;
106
94
  /**
107
95
  * Poll a static-host build until it reaches a terminal status (ok | error),
108
96
  * showing a live, ticking spinner with the current stage and elapsed time —
@@ -117,17 +105,47 @@ export default abstract class BaseCommand extends Command {
117
105
  protected logStaticHostUrls(opts: {
118
106
  profile: ProfileConfig;
119
107
  staticHost: string;
108
+ tenantName?: string;
120
109
  verbose: boolean;
121
110
  workspaceId: string;
122
111
  }): Promise<void>;
112
+ /**
113
+ * Parse an API error response and return a clean error message.
114
+ * Extracts the message from JSON responses and adds context for common errors.
115
+ */
116
+ protected parseApiError(response: Response, fallbackPrefix: string): Promise<string>;
117
+ /**
118
+ * Resolve the profile from flags and any project-local profile.yaml,
119
+ * validating instance_origin and access_token exist.
120
+ * Precedence: -p/XANO_PROFILE > profile.yaml > credentials default.
121
+ */
122
+ protected resolveProfile(flags: {
123
+ profile?: string;
124
+ }): {
125
+ profile: ProfileConfig;
126
+ profileName: string;
127
+ };
128
+ /**
129
+ * Make an HTTP request with optional verbose logging.
130
+ * Use this for all Metadata API calls to support the --verbose flag.
131
+ */
132
+ protected verboseFetch(url: string, options: RequestInit, verbose: boolean, authToken?: string): Promise<Response>;
123
133
  protected waitForBuild(opts: {
124
134
  buildId: number | string;
125
135
  intervalMs?: number;
126
136
  profile: ProfileConfig;
127
137
  quiet?: boolean;
128
138
  staticHost: string;
139
+ tenantName?: string;
129
140
  timeoutMs?: number;
130
141
  verbose: boolean;
131
142
  workspaceId: string;
132
143
  }): Promise<string>;
144
+ /**
145
+ * Find and parse the nearest project-local profile.yaml, unless an explicit
146
+ * -p/XANO_PROFILE was given (in which case the local file is ignored).
147
+ */
148
+ private loadLocalProfile;
149
+ /** Print the one-line target banner when a local profile.yaml is in effect. */
150
+ private maybePrintLocalProfileBanner;
133
151
  }
@@ -2,7 +2,7 @@ import { Command, Flags, ux } from '@oclif/core';
2
2
  import * as yaml from 'js-yaml';
3
3
  import * as fs from 'node:fs';
4
4
  import * as os from 'node:os';
5
- import * as path from 'node:path';
5
+ import path from 'node:path';
6
6
  import { Agent } from 'undici';
7
7
  import { checkForUpdate } from './update-check.js';
8
8
  import { applyLocalOverrides, findLocalProfilePath, formatLocalProfileBanner, parseLocalProfile, resolveProfileSelection, } from './utils/local-config.js';
@@ -29,13 +29,46 @@ function resolveRequestTimeoutMs(env = process.env) {
29
29
  return DEFAULT_REQUEST_TIMEOUT_MS;
30
30
  return parsed;
31
31
  }
32
+ /**
33
+ * Whether the built-in `fetch` on this runtime accepts a dispatcher built from
34
+ * our bundled userland `undici`. Node <= 24 accepts it; Node 26 rejects a
35
+ * foreign dispatcher with UND_ERR_INVALID_ARG, which `fetch` surfaces only as
36
+ * the generic "fetch failed" — breaking *every* command (DEV-7773).
37
+ *
38
+ * Starts optimistic (`true`); flipped to `false` the first time a request fails
39
+ * with UND_ERR_INVALID_ARG (see `verboseFetch`, which then retries without the
40
+ * dispatcher). Once flipped, every later request skips the dispatcher outright.
41
+ */
42
+ let dispatcherSupported = true;
43
+ /**
44
+ * True when `error` is the "foreign dispatcher rejected by built-in fetch"
45
+ * failure (Node 26+). The real code lives on `error.cause.code`.
46
+ */
47
+ export function isInvalidDispatcherError(error) {
48
+ if (!(error instanceof Error))
49
+ return false;
50
+ const { cause } = error;
51
+ const code = cause && typeof cause === 'object' && 'code' in cause
52
+ ? cause.code
53
+ : error.code;
54
+ return code === 'UND_ERR_INVALID_ARG';
55
+ }
32
56
  /**
33
57
  * Lazily-built undici dispatcher whose header/body inactivity timeouts match our
34
58
  * request timeout, so undici's internal 300s default never fires first. Built
35
59
  * once and reused across requests. `timeout === 0` means "no bound".
60
+ *
61
+ * Returns `undefined` when this runtime has been found to reject a foreign
62
+ * dispatcher (Node 26+, DEV-7773). Dropping the dispatcher does NOT regress the
63
+ * large-push timeout: the per-request `AbortSignal.timeout(timeoutMs)` in
64
+ * `verboseFetch` already bounds the whole request (headers included) to the same
65
+ * 15-min ceiling the dispatcher enforced, so undici's hidden 300s
66
+ * `headersTimeout` is subsumed by it.
36
67
  */
37
68
  let sharedDispatcher;
38
69
  function getRequestDispatcher(timeoutMs) {
70
+ if (!dispatcherSupported)
71
+ return undefined;
39
72
  if (timeoutMs === 0) {
40
73
  sharedDispatcher ??= new Agent({ bodyTimeout: 0, headersTimeout: 0 });
41
74
  return sharedDispatcher;
@@ -103,76 +136,6 @@ export default class BaseCommand extends Command {
103
136
  // Null when none was found or when an explicit -p/XANO_PROFILE overrides it.
104
137
  localProfile = null;
105
138
  updateNotice = null;
106
- async init() {
107
- await super.init();
108
- this.localProfile = this.loadLocalProfile();
109
- this.applyInsecureFromProfile();
110
- this.maybePrintLocalProfileBanner();
111
- const forceUpdateCheck = process.env.XANO_FORCE_UPDATE_CHECK === '1';
112
- this.updateNotice = checkForUpdate(this.config.version, forceUpdateCheck);
113
- }
114
- /**
115
- * Find and parse the nearest project-local profile.yaml, unless an explicit
116
- * -p/XANO_PROFILE was given (in which case the local file is ignored).
117
- */
118
- loadLocalProfile() {
119
- if (argvHasProfileFlag(process.argv, process.env)) {
120
- return null;
121
- }
122
- // Walks up to the filesystem root (git-style). parseLocalProfile returns
123
- // null for a profile.yaml with no recognized keys, so an unrelated file
124
- // belonging to another tool is ignored rather than hijacked.
125
- const filePath = findLocalProfilePath(process.cwd());
126
- if (!filePath) {
127
- return null;
128
- }
129
- let config;
130
- try {
131
- config = parseLocalProfile(fs.readFileSync(filePath, 'utf8'));
132
- }
133
- catch (error) {
134
- this.error(`${filePath}: ${error.message}`);
135
- }
136
- if (!config) {
137
- this.warn(`Ignoring ${filePath}: no recognized profile keys found.`);
138
- return null;
139
- }
140
- return { config, path: filePath };
141
- }
142
- /** Print the one-line target banner when a local profile.yaml is in effect. */
143
- maybePrintLocalProfileBanner() {
144
- if (!this.localProfile || this.isJsonOutput()) {
145
- return;
146
- }
147
- // Credential-management commands (the `profile` topic) operate on the
148
- // credentials store directly and intentionally ignore the project-local
149
- // pin, so the banner would be misleading for them.
150
- if (this.id?.startsWith('profile')) {
151
- return;
152
- }
153
- const { config, path: filePath } = this.localProfile;
154
- const profileName = config.profile ?? this.getDefaultProfile();
155
- const relativePath = path.relative(process.cwd(), filePath) || path.basename(filePath);
156
- this.log(formatLocalProfileBanner(profileName, config.workspace, relativePath));
157
- }
158
- async finally(_) {
159
- if (this.updateNotice && !this.isJsonOutput()) {
160
- this.log(this.updateNotice);
161
- }
162
- await super.finally(_);
163
- }
164
- isJsonOutput() {
165
- const args = process.argv;
166
- for (let i = 0; i < args.length; i++) {
167
- if (args[i] === '--output' && args[i + 1] === 'json')
168
- return true;
169
- if (args[i] === '-o' && args[i + 1] === 'json')
170
- return true;
171
- if (args[i] === '--output=json' || args[i] === '-o=json')
172
- return true;
173
- }
174
- return false;
175
- }
176
139
  /**
177
140
  * Apply insecure TLS mode if the active profile has insecure: true.
178
141
  * Sets NODE_TLS_REJECT_UNAUTHORIZED=0 so all fetch() calls skip cert verification.
@@ -193,22 +156,23 @@ export default class BaseCommand extends Command {
193
156
  // Don't fail the command if we can't read the profile for insecure check
194
157
  }
195
158
  }
196
- // Helper method to get the default profile from credentials file
197
- getDefaultProfile() {
198
- try {
199
- const credentials = this.loadCredentialsFile();
200
- if (credentials?.default) {
201
- return credentials.default;
202
- }
203
- return 'default';
204
- }
205
- catch {
206
- return 'default';
207
- }
159
+ /**
160
+ * Report a failure as exit 1 for commands that reserve exit 2 for findings: operational errors,
161
+ * flag errors and oclif's own default exit 2 all become 1, and under `-o json` the failure is also
162
+ * written to stdout as `{"error": {"exit": 1, "message"}}`. A deliberate exit 0 passes through.
163
+ */
164
+ async catchAsOperational(error) {
165
+ if (error.oclif?.exit === 0)
166
+ return super.catch(error);
167
+ if (this.isJsonOutput())
168
+ this.log(JSON.stringify({ error: { exit: 1, message: error.message } }, null, 2));
169
+ this.error(error, { exit: 1 });
208
170
  }
209
- // Helper method to get the profile flag value
210
- getProfile() {
211
- return this.flags?.profile;
171
+ async finally(_) {
172
+ if (this.updateNotice && !this.isJsonOutput()) {
173
+ this.log(this.updateNotice);
174
+ }
175
+ await super.finally(_);
212
176
  }
213
177
  /**
214
178
  * Get the resolved credentials file path, respecting --config flag and XANO_CONFIG env var.
@@ -225,17 +189,18 @@ export default class BaseCommand extends Command {
225
189
  }
226
190
  return resolveCredentialsPath();
227
191
  }
228
- loadCredentialsFile() {
229
- const credentialsPath = this.getCredentialsPath();
230
- if (!fs.existsSync(credentialsPath)) {
231
- return null;
192
+ // Helper method to get the default profile from credentials file
193
+ getDefaultProfile() {
194
+ try {
195
+ const credentials = this.loadCredentialsFile();
196
+ if (credentials?.default) {
197
+ return credentials.default;
198
+ }
199
+ return 'default';
232
200
  }
233
- const fileContent = fs.readFileSync(credentialsPath, 'utf8');
234
- const parsed = yaml.load(fileContent);
235
- if (parsed && typeof parsed === 'object' && 'profiles' in parsed) {
236
- return parsed;
201
+ catch {
202
+ return 'default';
237
203
  }
238
- return null;
239
204
  }
240
205
  /**
241
206
  * Get or create the singleton sandbox environment for the authenticated user.
@@ -256,6 +221,105 @@ export default class BaseCommand extends Command {
256
221
  }
257
222
  return (await response.json());
258
223
  }
224
+ // Helper method to get the profile flag value
225
+ getProfile() {
226
+ return this.flags?.profile;
227
+ }
228
+ async init() {
229
+ await super.init();
230
+ this.localProfile = this.loadLocalProfile();
231
+ this.applyInsecureFromProfile();
232
+ this.maybePrintLocalProfileBanner();
233
+ const forceUpdateCheck = process.env.XANO_FORCE_UPDATE_CHECK === '1';
234
+ this.updateNotice = checkForUpdate(this.config.version, forceUpdateCheck);
235
+ }
236
+ /** Raw-argv check: the parsed flags are not available yet when the banner prints. */
237
+ isJsonOutput() {
238
+ const args = this.argv;
239
+ for (let i = 0; i < args.length; i++) {
240
+ if (args[i] === '--output' && args[i + 1] === 'json')
241
+ return true;
242
+ if (args[i] === '-o' && args[i + 1] === 'json')
243
+ return true;
244
+ if (args[i] === '--output=json' || args[i] === '-o=json')
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ loadCredentialsFile() {
250
+ const credentialsPath = this.getCredentialsPath();
251
+ if (!fs.existsSync(credentialsPath)) {
252
+ return null;
253
+ }
254
+ const fileContent = fs.readFileSync(credentialsPath, 'utf8');
255
+ const parsed = yaml.load(fileContent);
256
+ if (parsed && typeof parsed === 'object' && 'profiles' in parsed) {
257
+ return parsed;
258
+ }
259
+ return null;
260
+ }
261
+ /**
262
+ * Poll a static-host build until it reaches a terminal status (ok | error),
263
+ * showing a live, ticking spinner with the current stage and elapsed time —
264
+ * mirroring the UI's build progress for async (package.json) builds, which
265
+ * keep running after the upload returns.
266
+ *
267
+ * On a TTY it renders an animated spinner via ux.action; when quiet (JSON
268
+ * output) or non-interactive it falls back to plain one-line status updates.
269
+ *
270
+ * Returns the final status. Resolves to the last-seen status on timeout.
271
+ */
272
+ async logStaticHostUrls(opts) {
273
+ const { profile, staticHost, tenantName, verbose, workspaceId } = opts;
274
+ // When a tenant is scoped, target the tenant's static hosting; otherwise the
275
+ // workspace-level static host (default, unchanged behavior).
276
+ const scope = tenantName ? `/tenant/${tenantName}` : '';
277
+ const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}${scope}/static_host/${staticHost}`;
278
+ try {
279
+ const response = await this.verboseFetch(url, {
280
+ headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
281
+ method: 'GET',
282
+ }, verbose, profile.access_token);
283
+ if (!response.ok)
284
+ return;
285
+ const host = (await response.json());
286
+ if (host.dev?.default_url)
287
+ this.log(`Dev URL: ${host.dev.default_url}`);
288
+ if (host.dev?.custom_url)
289
+ this.log(`Dev Custom URL: ${host.dev.custom_url}`);
290
+ if (host.prod?.default_url)
291
+ this.log(`Prod URL: ${host.prod.default_url}`);
292
+ if (host.prod?.custom_url)
293
+ this.log(`Prod Custom URL: ${host.prod.custom_url}`);
294
+ }
295
+ catch {
296
+ // Non-fatal — the build succeeded, we just can't show the URL.
297
+ }
298
+ }
299
+ /**
300
+ * Parse an API error response and return a clean error message.
301
+ * Extracts the message from JSON responses and adds context for common errors.
302
+ */
303
+ async parseApiError(response, fallbackPrefix) {
304
+ const errorText = await response.text();
305
+ let message = `${fallbackPrefix} (${response.status})`;
306
+ try {
307
+ const errorJson = JSON.parse(errorText);
308
+ if (errorJson.message) {
309
+ message = errorJson.message;
310
+ }
311
+ }
312
+ catch {
313
+ if (errorText) {
314
+ message += `\n${errorText}`;
315
+ }
316
+ }
317
+ // Provide guidance when sandbox access is denied (free plan restriction)
318
+ if (response.status === 500 && message === 'Access Denied.') {
319
+ message = 'Sandbox is not available on the Free plan. Upgrade your plan to use sandbox features.';
320
+ }
321
+ return message;
322
+ }
259
323
  /**
260
324
  * Resolve the profile from flags and any project-local profile.yaml,
261
325
  * validating instance_origin and access_token exist.
@@ -289,30 +353,6 @@ export default class BaseCommand extends Command {
289
353
  }
290
354
  return { profile, profileName };
291
355
  }
292
- /**
293
- * Parse an API error response and return a clean error message.
294
- * Extracts the message from JSON responses and adds context for common errors.
295
- */
296
- async parseApiError(response, fallbackPrefix) {
297
- const errorText = await response.text();
298
- let message = `${fallbackPrefix} (${response.status})`;
299
- try {
300
- const errorJson = JSON.parse(errorText);
301
- if (errorJson.message) {
302
- message = errorJson.message;
303
- }
304
- }
305
- catch {
306
- if (errorText) {
307
- message += `\n${errorText}`;
308
- }
309
- }
310
- // Provide guidance when sandbox access is denied (free plan restriction)
311
- if (response.status === 500 && message === 'Access Denied.') {
312
- message = 'Sandbox is not available on the Free plan. Upgrade your plan to use sandbox features.';
313
- }
314
- return message;
315
- }
316
356
  /**
317
357
  * Make an HTTP request with optional verbose logging.
318
358
  * Use this for all Metadata API calls to support the --verbose flag.
@@ -324,81 +364,74 @@ export default class BaseCommand extends Command {
324
364
  ...options.headers,
325
365
  };
326
366
  const timeoutMs = resolveRequestTimeoutMs();
327
- const fetchOptions = {
367
+ // Build the fetch options fresh each attempt: a retry needs a new
368
+ // AbortSignal (the first attempt's may already be aborted/consumed) and,
369
+ // when the dispatcher is the culprit, no dispatcher at all.
370
+ const buildFetchOptions = (withDispatcher) => ({
328
371
  ...options,
329
372
  // undici dispatcher: lifts the hidden 300s headers/body timeout to our bound
330
- dispatcher: getRequestDispatcher(timeoutMs),
373
+ ...(withDispatcher ? { dispatcher: getRequestDispatcher(timeoutMs) } : {}),
331
374
  headers,
332
375
  // belt-and-suspenders hard ceiling that also covers DNS/connect stalls;
333
376
  // surfaces as a clean AbortError (see describeNetworkError). 0 = no bound.
377
+ // This alone bounds the whole request, so dropping the dispatcher (Node 26)
378
+ // does not lose the large-push timeout protection.
334
379
  ...(timeoutMs > 0 && !options.signal ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
335
- };
380
+ });
336
381
  const contentType = headers['Content-Type'] || 'application/json';
382
+ const logDiagnostic = this.isJsonOutput() ? this.logToStderr.bind(this) : this.log.bind(this);
337
383
  if (verbose) {
338
- this.log('');
339
- this.log('─'.repeat(60));
340
- this.log(`→ ${method} ${url}`);
341
- this.log(` Content-Type: ${contentType}`);
384
+ logDiagnostic('');
385
+ logDiagnostic('─'.repeat(60));
386
+ logDiagnostic(`→ ${method} ${url}`);
387
+ logDiagnostic(` Content-Type: ${contentType}`);
342
388
  if (authToken) {
343
- this.log(` Authorization: Bearer ${authToken.slice(0, 8)}...${authToken.slice(-4)}`);
389
+ logDiagnostic(` Authorization: Bearer ${authToken.slice(0, 8)}...${authToken.slice(-4)}`);
344
390
  }
345
391
  if (options.body) {
346
392
  const bodyStr = typeof options.body === 'string' ? options.body : String(options.body);
347
393
  const bodyPreview = bodyStr.length > 500 ? bodyStr.slice(0, 500) + '...' : bodyStr;
348
- this.log(` Body: ${bodyPreview}`);
394
+ logDiagnostic(` Body: ${bodyPreview}`);
349
395
  }
350
396
  }
351
397
  const startTime = Date.now();
352
- const response = await fetch(url, fetchOptions);
398
+ let response;
399
+ try {
400
+ response = await fetch(url, buildFetchOptions(dispatcherSupported));
401
+ }
402
+ catch (error) {
403
+ // Node 26+ rejects our userland-undici dispatcher with UND_ERR_INVALID_ARG,
404
+ // surfaced only as "fetch failed" (DEV-7773). Disable it for the rest of the
405
+ // process and retry once without it; the AbortSignal.timeout ceiling above
406
+ // still bounds the request, so the large-push protection is preserved.
407
+ if (dispatcherSupported && isInvalidDispatcherError(error)) {
408
+ dispatcherSupported = false;
409
+ if (verbose) {
410
+ logDiagnostic(' (this Node runtime rejected undici dispatcher; retrying without it — see DEV-7773)');
411
+ }
412
+ response = await fetch(url, buildFetchOptions(false));
413
+ }
414
+ else {
415
+ throw error;
416
+ }
417
+ }
353
418
  const elapsed = Date.now() - startTime;
354
419
  if (verbose) {
355
- this.log(`← ${response.status} ${response.statusText} (${elapsed}ms)`);
356
- this.log('─'.repeat(60));
357
- this.log('');
420
+ logDiagnostic(`← ${response.status} ${response.statusText} (${elapsed}ms)`);
421
+ logDiagnostic('─'.repeat(60));
422
+ logDiagnostic('');
358
423
  }
359
424
  return response;
360
425
  }
361
- /**
362
- * Poll a static-host build until it reaches a terminal status (ok | error),
363
- * showing a live, ticking spinner with the current stage and elapsed time —
364
- * mirroring the UI's build progress for async (package.json) builds, which
365
- * keep running after the upload returns.
366
- *
367
- * On a TTY it renders an animated spinner via ux.action; when quiet (JSON
368
- * output) or non-interactive it falls back to plain one-line status updates.
369
- *
370
- * Returns the final status. Resolves to the last-seen status on timeout.
371
- */
372
- async logStaticHostUrls(opts) {
373
- const { profile, staticHost, verbose, workspaceId } = opts;
374
- const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}`;
375
- try {
376
- const response = await this.verboseFetch(url, {
377
- headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
378
- method: 'GET',
379
- }, verbose, profile.access_token);
380
- if (!response.ok)
381
- return;
382
- const host = (await response.json());
383
- if (host.dev?.default_url)
384
- this.log(`Dev URL: ${host.dev.default_url}`);
385
- if (host.dev?.custom_url)
386
- this.log(`Dev Custom URL: ${host.dev.custom_url}`);
387
- if (host.prod?.default_url)
388
- this.log(`Prod URL: ${host.prod.default_url}`);
389
- if (host.prod?.custom_url)
390
- this.log(`Prod Custom URL: ${host.prod.custom_url}`);
391
- }
392
- catch {
393
- // Non-fatal — the build succeeded, we just can't show the URL.
394
- }
395
- }
396
426
  async waitForBuild(opts) {
397
- const { buildId, profile, quiet, staticHost, verbose, workspaceId } = opts;
427
+ const { buildId, profile, quiet, staticHost, tenantName, verbose, workspaceId } = opts;
398
428
  const intervalMs = opts.intervalMs ?? 2000;
399
429
  const timeoutMs = opts.timeoutMs ?? 600_000; // 10 min
400
430
  const terminal = new Set(['error', 'ok']);
401
- const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build/${buildId}`;
431
+ // When a tenant is scoped, target the tenant's static hosting; otherwise the
432
+ // workspace-level static host (default, unchanged behavior).
433
+ const scope = tenantName ? `/tenant/${tenantName}` : '';
434
+ const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}${scope}/static_host/${staticHost}/build/${buildId}`;
402
435
  // Spinner only on an interactive TTY and when not emitting JSON. Verbose mode
403
436
  // also disables it (the spinner would interleave with request/response logs).
404
437
  const animate = Boolean(process.stdout.isTTY) && !quiet && !verbose;
@@ -461,6 +494,50 @@ export default class BaseCommand extends Command {
461
494
  conclude(`stopped waiting after ${Math.round(timeoutMs / 1000)}s (last status: ${stage || 'unknown'})`);
462
495
  return stage;
463
496
  }
497
+ /**
498
+ * Find and parse the nearest project-local profile.yaml, unless an explicit
499
+ * -p/XANO_PROFILE was given (in which case the local file is ignored).
500
+ */
501
+ loadLocalProfile() {
502
+ if (argvHasProfileFlag(process.argv, process.env)) {
503
+ return null;
504
+ }
505
+ // Walks up to the filesystem root (git-style). parseLocalProfile returns
506
+ // null for a profile.yaml with no recognized keys, so an unrelated file
507
+ // belonging to another tool is ignored rather than hijacked.
508
+ const filePath = findLocalProfilePath(process.cwd());
509
+ if (!filePath) {
510
+ return null;
511
+ }
512
+ let config;
513
+ try {
514
+ config = parseLocalProfile(fs.readFileSync(filePath, 'utf8'));
515
+ }
516
+ catch (error) {
517
+ this.error(`${filePath}: ${error.message}`);
518
+ }
519
+ if (!config) {
520
+ this.warn(`Ignoring ${filePath}: no recognized profile keys found.`);
521
+ return null;
522
+ }
523
+ return { config, path: filePath };
524
+ }
525
+ /** Print the one-line target banner when a local profile.yaml is in effect. */
526
+ maybePrintLocalProfileBanner() {
527
+ if (!this.localProfile || this.isJsonOutput()) {
528
+ return;
529
+ }
530
+ // Credential-management commands (the `profile` topic) operate on the
531
+ // credentials store directly and intentionally ignore the project-local
532
+ // pin, so the banner would be misleading for them.
533
+ if (this.id?.startsWith('profile')) {
534
+ return;
535
+ }
536
+ const { config, path: filePath } = this.localProfile;
537
+ const profileName = config.profile ?? this.getDefaultProfile();
538
+ const relativePath = path.relative(process.cwd(), filePath) || path.basename(filePath);
539
+ this.log(formatLocalProfileBanner(profileName, config.workspace, relativePath));
540
+ }
464
541
  }
465
542
  /** Human-friendly label for a build status stage. */
466
543
  function stageLabel(status) {
@@ -48,10 +48,10 @@ export default class Auth extends Command {
48
48
  run(): Promise<AuthResult>;
49
49
  protected toErrorJson(err: unknown): unknown;
50
50
  private acquireToken;
51
- private getHeaders;
52
51
  private fetchBranches;
53
52
  private fetchInstances;
54
53
  private fetchWorkspaces;
54
+ private getHeaders;
55
55
  private promptForToken;
56
56
  private promptProfileName;
57
57
  private readTokenFromStdin;