@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
@@ -0,0 +1,94 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs';
3
+ import BaseCommand from './base-command.js';
4
+ import { POLICY_ROUTE, policyRequest, policyScope } from './utils/policy/request.js';
5
+ import { policyRunDetail } from './utils/policy/runs.js';
6
+ /** Shared flags, request helper and output for the `policy *` commands and `skills pull`. */
7
+ export default class PolicyCommand extends BaseCommand {
8
+ static policyFlags = {
9
+ ...BaseCommand.baseFlags,
10
+ branch: Flags.string({ char: 'b', description: "Branch label (defaults to profile branch or live; -b '' selects live)" }),
11
+ output: Flags.string({ char: 'o', default: 'summary', description: 'Output format', options: ['summary', 'json'] }),
12
+ workspace: Flags.string({ char: 'w', description: 'Workspace ID (defaults to profile workspace)' }),
13
+ };
14
+ /** `publish -m`: the message on the Version History entry the save creates. */
15
+ static publishFlags = {
16
+ message: Flags.string({ char: 'm', description: 'Message stored on the Version History entry this save creates' }),
17
+ };
18
+ /** One positional, as CLAUDE.md requires: the file `--file` would otherwise name. */
19
+ static sourceArgs = {
20
+ file: Args.string({ description: 'Policy XanoScript file (same as --file)', ignoreStdin: true, required: false }),
21
+ };
22
+ static sourceFlags = {
23
+ file: Flags.string({ char: 'f', description: 'Policy XanoScript file', exclusive: ['stdin'] }),
24
+ stdin: Flags.boolean({ default: false, description: 'Read policy XanoScript from stdin', exclusive: ['file'] }),
25
+ };
26
+ /** Any failure exits 1; an error that is not the CLI's own names the command it broke. */
27
+ async catch(error) {
28
+ if ('oclif' in error)
29
+ return this.catchAsOperational(error);
30
+ const action = this.id?.split(':').at(-1) ?? 'command';
31
+ return this.catchAsOperational(new Error(`Policy ${action} failed: ${error.message}`));
32
+ }
33
+ /**
34
+ * Print each rule's warnings (a name, tag or value that makes the rule check nothing) to stderr,
35
+ * so piped source stays clean; nothing when there are none.
36
+ */
37
+ logRuleWarnings(parsed) {
38
+ const warned = parsed.rule_warnings ?? [];
39
+ if (warned.length === 0)
40
+ return;
41
+ this.logToStderr('Rule warnings (the rule may check nothing or everything; fix them before relying on it):');
42
+ for (const rule of warned) {
43
+ for (const warning of rule.warnings)
44
+ this.logToStderr(` ${rule.rule_id || `rule[${rule.rule}]`}: ${warning}`);
45
+ }
46
+ }
47
+ /** Print a run's recorded detail, or say it evaluated no policy. */
48
+ logRunDetail(run) {
49
+ const detail = policyRunDetail(run);
50
+ for (const line of detail)
51
+ this.log(line);
52
+ if (detail.length === 0)
53
+ this.log(run.id ? `Run ${run.id} evaluated no policies.` : 'No policies were evaluated.');
54
+ }
55
+ /**
56
+ * Ask the parse route for the policy, its canonical source and each rule's warnings against the
57
+ * target branch, from XanoScript (`{source}`) or a policy document (`{data}`); the policy and source
58
+ * must come back.
59
+ */
60
+ async parseSource(request, input) {
61
+ const answer = (await request('/parse', 'POST', input));
62
+ if (!answer?.policy?.key || typeof answer.source !== 'string')
63
+ this.error('The platform did not return a parsed policy and canonical source.');
64
+ return { ...answer, rule_warnings: Array.isArray(answer.rule_warnings) ? answer.rule_warnings : [] };
65
+ }
66
+ /**
67
+ * The workspace and branch from the flags or the profile, and a request helper bound to them for
68
+ * a route gated by `workspace:policy`: `/policy` unless another is named.
69
+ */
70
+ policyTarget(flags, route = POLICY_ROUTE) {
71
+ const { profile } = this.resolveProfile(flags);
72
+ const { branch, workspace } = policyScope(flags, profile);
73
+ if (!workspace)
74
+ this.error('Workspace ID required. Use --workspace or set one in your profile.');
75
+ const request = policyRequest({ error: (message) => this.error(message), logToStderr: (message) => this.logToStderr(message), verboseFetch: (...args) => this.verboseFetch(...args) }, { ...route, branch, profile, verbose: flags.verbose, workspace });
76
+ return { branch, request, workspace };
77
+ }
78
+ /** Policy source from the positional, `--file` or `--stdin`; a file named twice must be named the same. */
79
+ readSource(flags, positional) {
80
+ const named = positional?.trim();
81
+ if (named && flags.stdin)
82
+ this.error('Provide policy source once: a file (positional or --file) or --stdin.');
83
+ if (named && flags.file && flags.file !== named)
84
+ this.error('Provide the file once: as a positional or as --file.');
85
+ const file = flags.file ?? named;
86
+ if (!file && !flags.stdin)
87
+ this.error('Provide the policy file (as an argument or --file) or --stdin.');
88
+ return fs.readFileSync(flags.stdin ? 0 : file, 'utf8');
89
+ }
90
+ /** `workspace 3 (feature)`, or just `workspace 3` for live. */
91
+ where({ branch, workspace }) {
92
+ return `workspace ${workspace}${branch ? ` (${branch})` : ''}`;
93
+ }
94
+ }
@@ -5,4 +5,4 @@ export declare function clearUpdateCache(): void;
5
5
  *
6
6
  * The check hits npm at most once every 24 hours and caches the result.
7
7
  */
8
- export declare function checkForUpdate(currentVersion: string, forceCheck?: boolean): string | null;
8
+ export declare function checkForUpdate(currentVersion: string, forceCheck?: boolean): null | string;
@@ -1,7 +1,7 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import * as fs from 'node:fs';
3
- import * as path from 'node:path';
4
3
  import * as os from 'node:os';
4
+ import path from 'node:path';
5
5
  const UPDATE_CHECK_FILE = path.join(os.homedir(), '.xano', 'update-check.json');
6
6
  const CHECK_INTERVAL_MS = 8 * 60 * 60 * 1000; // 8 hours
7
7
  function isBeta(version) {
@@ -1,17 +1,112 @@
1
1
  export interface ParsedDocument {
2
2
  apiGroup?: string;
3
3
  canonical?: string;
4
+ /** Owning channel path for realtime v2 `message` documents. */
5
+ channel?: string;
4
6
  content: string;
5
7
  guid?: string;
6
8
  name: string;
9
+ /** Owning realtime_server name for realtime v2 `channel` documents. */
10
+ server?: string;
7
11
  type: string;
8
12
  verb?: string;
9
13
  }
14
+ /**
15
+ * The platform's policy key pattern, kept offline for file names. A command that already holds the
16
+ * catalogue validates a key against the catalogue's `document.key.pattern` instead.
17
+ */
18
+ export declare const POLICY_KEY_PATTERN: RegExp;
19
+ /**
20
+ * The file name, without `.xs`, for a policy key. The pattern is the platform's own key pattern, so a
21
+ * key from the server can never name a path outside `policies/`.
22
+ */
23
+ export declare function policyBaseName(key: string): string;
10
24
  /**
11
25
  * Parse a single XanoScript document to extract its type, name, and optional verb/api_group.
12
26
  * Skips leading comment lines (starting with //) to find the first meaningful line.
13
27
  */
14
28
  export declare function parseDocument(content: string): null | ParsedDocument;
29
+ /**
30
+ * Map a realtime v2 channel path onto directory segments.
31
+ *
32
+ * A channel name IS a path template — "rooms/{room_id}" — so the directory tree
33
+ * mirrors it rather than flattening it. That is the whole reason messages nest
34
+ * under channels: the tree should show the structure, and
35
+ * "channel/rooms/[room_id]/post.xs" says immediately that `post` lives on a
36
+ * parameterized rooms channel.
37
+ *
38
+ * "rooms" -> ["rooms"]
39
+ * "rooms/{room_id}" -> ["rooms", "[room_id]"]
40
+ * "org/{org_id}/room/{id}" -> ["org", "[org_id]", "room", "[id]"]
41
+ *
42
+ * Braces become brackets because "{" and "}" are awkward in shells, while "["
43
+ * and "]" are legal on every filesystem. They are still glob character classes,
44
+ * so scripting against a checkout needs the path quoted.
45
+ *
46
+ * Escaping the braces rather than dropping them is what keeps a parameterized
47
+ * channel distinct from a literal one: without it, "rooms/{room_id}" and a
48
+ * literal channel named "rooms/room_id" collide on the same directory.
49
+ *
50
+ * Mirrors Vfs::channelSegment() on the server.
51
+ */
52
+ export declare function channelPathSegments(name: string, snakeCaseFn: (s: string) => string): string[];
53
+ /**
54
+ * Build a map of realtime v2 channel path -> owning realtime_server name.
55
+ *
56
+ * This is the cross-document lookup the on-disk nesting needs: a `message`
57
+ * document names its channel (`channel = "..."`), while a `channel` document
58
+ * names its server (`realtime_server = "..."`). To place a message under
59
+ * `realtime/server/<server>/channel/<path>/...` we resolve the message's channel
60
+ * to that channel's server — which the channel document carries. (Messages now
61
+ * also carry `realtime_server` directly, but resolving via the channel keeps a
62
+ * single source of truth for the channel→server mapping.)
63
+ *
64
+ * Because every pull path parses the WHOLE multidoc into memory before writing,
65
+ * we can build this map from the `channel` docs in the same batch (a two-pass
66
+ * resolve), keyed by the channel's own name (its path). A channel with no
67
+ * `realtime_server` (e.g. a pre-server export) simply isn't in the map, and
68
+ * callers fall back accordingly.
69
+ */
70
+ export declare function buildChannelServerResolver(documents: ParsedDocument[]): (channelName: string) => string | undefined;
71
+ /**
72
+ * Sanitize a document name for use as a single on-disk filename segment.
73
+ * Strips quotes and snake_cases the rest. Mirrors the per-command
74
+ * `sanitizeFilename` the pull commands used to each define privately.
75
+ */
76
+ export declare function sanitizeDocumentName(name: string, snakeCaseFn: (s: string) => string): string;
77
+ /**
78
+ * The directory + base filename (no extension, no duplicate suffix) a document
79
+ * is written to on disk. `resolveDocumentPath` returns this; callers append the
80
+ * `.xs` extension and any `_N` duplicate suffix.
81
+ */
82
+ export interface DocumentPlacement {
83
+ baseName: string;
84
+ typeDir: string;
85
+ }
86
+ /**
87
+ * Resolve where a parsed document lands on disk — the SINGLE source of truth for
88
+ * the pull layout, shared by every pull command (workspace, tenant, release,
89
+ * sandbox, ephemeral). Previously each command inlined a ~130-line copy of this
90
+ * decision; the copies drifted (the realtime v2 realtime_server/channel/message
91
+ * branches existed only in workspace/tenant), so an ephemeral pull dumped v2
92
+ * objects into flat top-level channel/, message/ and realtime_server/ folders —
93
+ * detaching messages from their channels and risking name collisions (message
94
+ * names are unique only WITHIN a channel).
95
+ *
96
+ * @param doc - the parsed document
97
+ * @param outputDir - the resolved pull output root
98
+ * @param deps - injected dependencies
99
+ * @param deps.getApiGroupFolder - resolves an api_group name to its unique folder
100
+ * @param deps.getChannelServer - resolves a v2 channel path to its owning realtime_server
101
+ * @param deps.join - the path.join implementation (injectable for testing)
102
+ * @param deps.snakeCase - the snakeCase implementation the caller uses
103
+ */
104
+ export declare function resolveDocumentPath(doc: ParsedDocument, outputDir: string, deps: {
105
+ getApiGroupFolder: (groupName: string) => string;
106
+ getChannelServer: (channelName: string) => string | undefined;
107
+ join: (...parts: string[]) => string;
108
+ snakeCase: (s: string) => string;
109
+ }): DocumentPlacement;
15
110
  /**
16
111
  * Build a unique key for a document based on its type, name, verb, and api_group.
17
112
  * Used to match server GUID map entries back to local files.
@@ -37,3 +132,34 @@ export declare function findFilesWithGuid(entries: Array<{
37
132
  content: string;
38
133
  filePath: string;
39
134
  }>, guid: string): string[];
135
+ /**
136
+ * Split a multi-document XanoScript blob (documents joined by `\n---\n`) into
137
+ * parsed documents, skipping empties and any fragment that fails to parse.
138
+ *
139
+ * This is the same split the pull commands apply to the server's multidoc
140
+ * response. A hand-authored bundle is one file of many `---`-separated
141
+ * documents; this turns it back into the per-document units every downstream
142
+ * step (placement, GUID writeback, partial-diff matching) assumes.
143
+ */
144
+ export declare function splitMultidoc(blob: string): ParsedDocument[];
145
+ /** A document's resolved on-disk location relative to the output root, plus its content. */
146
+ export interface PlacedDocument {
147
+ content: string;
148
+ /** POSIX-style path relative to the output root, e.g. "api/pdf/documents_GET.xs". */
149
+ relPath: string;
150
+ }
151
+ /**
152
+ * Compute where each document lands on disk — a PURE function (no filesystem)
153
+ * mirroring the split-and-place loop the pull commands run. It resolves each
154
+ * document via `resolveDocumentPath`, appends `.xs`, and disambiguates
155
+ * collisions within a directory with a `_N` suffix (the second file named
156
+ * `say.xs` in a dir becomes `say_2.xs`), exactly as the pull writer does.
157
+ *
158
+ * Kept filesystem-free so it can be unit-tested and reused by both the pull
159
+ * commands and `flatten` without duplicating the placement rules.
160
+ */
161
+ export declare function placeDocuments(documents: ParsedDocument[], deps: {
162
+ join: (...parts: string[]) => string;
163
+ relative: (from: string, to: string) => string;
164
+ snakeCase: (s: string) => string;
165
+ }): PlacedDocument[];
@@ -1,3 +1,17 @@
1
+ /**
2
+ * The platform's policy key pattern, kept offline for file names. A command that already holds the
3
+ * catalogue validates a key against the catalogue's `document.key.pattern` instead.
4
+ */
5
+ export const POLICY_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
6
+ /**
7
+ * The file name, without `.xs`, for a policy key. The pattern is the platform's own key pattern, so a
8
+ * key from the server can never name a path outside `policies/`.
9
+ */
10
+ export function policyBaseName(key) {
11
+ if (!POLICY_KEY_PATTERN.test(key))
12
+ throw new Error(`Invalid policy key: ${key}`);
13
+ return key;
14
+ }
1
15
  /**
2
16
  * Parse a single XanoScript document to extract its type, name, and optional verb/api_group.
3
17
  * Skips leading comment lines (starting with //) to find the first meaningful line.
@@ -33,6 +47,9 @@ export function parseDocument(content) {
33
47
  if (name.startsWith('"') && name.endsWith('"')) {
34
48
  name = name.slice(1, -1);
35
49
  }
50
+ // Policy bodies are opaque. Prose/parameters may mention guid or api_group; these are not document identity.
51
+ if (type === 'policy')
52
+ return { content, name, type };
36
53
  // Extract verb if present (e.g., verb=GET)
37
54
  let verb;
38
55
  const verbMatch = rest.match(/verb=(\S+)/);
@@ -45,6 +62,25 @@ export function parseDocument(content) {
45
62
  if (apiGroupMatch) {
46
63
  apiGroup = apiGroupMatch[1];
47
64
  }
65
+ // Extract channel if present (e.g., channel = "rooms/{room_id}").
66
+ // Realtime v2 messages reference their owning channel by PATH, which is what
67
+ // nests them under it on disk.
68
+ let channel;
69
+ const channelMatch = content.match(/^\s*channel\s*=\s*"([^"]*)"/m);
70
+ if (channelMatch) {
71
+ channel = channelMatch[1];
72
+ }
73
+ // Extract realtime_server if present (e.g., realtime_server = "chat").
74
+ // Realtime v2 channels/messages reference their owning realtime_server by NAME
75
+ // via a top-level `realtime_server = "..."`, which is what nests them under it
76
+ // on disk (mirrors how a message names its channel via `channel = "..."`).
77
+ // The keyword is `realtime_server` (renamed from a bare `server` to match the
78
+ // api_group/mcp_server convention — reference the container by its full type).
79
+ let server;
80
+ const serverMatch = content.match(/^\s*realtime_server\s*=\s*"([^"]*)"/m);
81
+ if (serverMatch) {
82
+ server = serverMatch[1];
83
+ }
48
84
  // Extract canonical if present (e.g., canonical = "abc123")
49
85
  let canonical;
50
86
  const canonicalMatch = content.match(/canonical\s*=\s*"([^"]*)"/);
@@ -57,7 +93,241 @@ export function parseDocument(content) {
57
93
  if (guidMatch) {
58
94
  guid = guidMatch[1];
59
95
  }
60
- return { apiGroup, canonical, content, guid, name, type, verb };
96
+ return { apiGroup, canonical, channel, content, guid, name, server, type, verb };
97
+ }
98
+ /**
99
+ * Map a realtime v2 channel path onto directory segments.
100
+ *
101
+ * A channel name IS a path template — "rooms/{room_id}" — so the directory tree
102
+ * mirrors it rather than flattening it. That is the whole reason messages nest
103
+ * under channels: the tree should show the structure, and
104
+ * "channel/rooms/[room_id]/post.xs" says immediately that `post` lives on a
105
+ * parameterized rooms channel.
106
+ *
107
+ * "rooms" -> ["rooms"]
108
+ * "rooms/{room_id}" -> ["rooms", "[room_id]"]
109
+ * "org/{org_id}/room/{id}" -> ["org", "[org_id]", "room", "[id]"]
110
+ *
111
+ * Braces become brackets because "{" and "}" are awkward in shells, while "["
112
+ * and "]" are legal on every filesystem. They are still glob character classes,
113
+ * so scripting against a checkout needs the path quoted.
114
+ *
115
+ * Escaping the braces rather than dropping them is what keeps a parameterized
116
+ * channel distinct from a literal one: without it, "rooms/{room_id}" and a
117
+ * literal channel named "rooms/room_id" collide on the same directory.
118
+ *
119
+ * Mirrors Vfs::channelSegment() on the server.
120
+ */
121
+ export function channelPathSegments(name, snakeCaseFn) {
122
+ // A parameter keeps the author's name verbatim so it matches the `input`
123
+ // block; only the delimiters are swapped.
124
+ const isParam = /^\{.*\}$/;
125
+ return name.split('/').map((segment) => (isParam.test(segment) ? `[${segment.slice(1, -1)}]` : snakeCaseFn(segment)));
126
+ }
127
+ /**
128
+ * Build a map of realtime v2 channel path -> owning realtime_server name.
129
+ *
130
+ * This is the cross-document lookup the on-disk nesting needs: a `message`
131
+ * document names its channel (`channel = "..."`), while a `channel` document
132
+ * names its server (`realtime_server = "..."`). To place a message under
133
+ * `realtime/server/<server>/channel/<path>/...` we resolve the message's channel
134
+ * to that channel's server — which the channel document carries. (Messages now
135
+ * also carry `realtime_server` directly, but resolving via the channel keeps a
136
+ * single source of truth for the channel→server mapping.)
137
+ *
138
+ * Because every pull path parses the WHOLE multidoc into memory before writing,
139
+ * we can build this map from the `channel` docs in the same batch (a two-pass
140
+ * resolve), keyed by the channel's own name (its path). A channel with no
141
+ * `realtime_server` (e.g. a pre-server export) simply isn't in the map, and
142
+ * callers fall back accordingly.
143
+ */
144
+ export function buildChannelServerResolver(documents) {
145
+ const channelToServer = new Map();
146
+ for (const doc of documents) {
147
+ if (doc.type === 'channel' && doc.server) {
148
+ channelToServer.set(doc.name, doc.server);
149
+ }
150
+ }
151
+ return (channelName) => channelToServer.get(channelName);
152
+ }
153
+ /**
154
+ * Sanitize a document name for use as a single on-disk filename segment.
155
+ * Strips quotes and snake_cases the rest. Mirrors the per-command
156
+ * `sanitizeFilename` the pull commands used to each define privately.
157
+ */
158
+ export function sanitizeDocumentName(name, snakeCaseFn) {
159
+ return snakeCaseFn(name.replaceAll('"', ''));
160
+ }
161
+ /**
162
+ * Resolve where a parsed document lands on disk — the SINGLE source of truth for
163
+ * the pull layout, shared by every pull command (workspace, tenant, release,
164
+ * sandbox, ephemeral). Previously each command inlined a ~130-line copy of this
165
+ * decision; the copies drifted (the realtime v2 realtime_server/channel/message
166
+ * branches existed only in workspace/tenant), so an ephemeral pull dumped v2
167
+ * objects into flat top-level channel/, message/ and realtime_server/ folders —
168
+ * detaching messages from their channels and risking name collisions (message
169
+ * names are unique only WITHIN a channel).
170
+ *
171
+ * @param doc - the parsed document
172
+ * @param outputDir - the resolved pull output root
173
+ * @param deps - injected dependencies
174
+ * @param deps.getApiGroupFolder - resolves an api_group name to its unique folder
175
+ * @param deps.getChannelServer - resolves a v2 channel path to its owning realtime_server
176
+ * @param deps.join - the path.join implementation (injectable for testing)
177
+ * @param deps.snakeCase - the snakeCase implementation the caller uses
178
+ */
179
+ export function resolveDocumentPath(doc, outputDir, deps) {
180
+ const { getApiGroupFolder, getChannelServer, join, snakeCase } = deps;
181
+ const sanitize = (name) => sanitizeDocumentName(name, snakeCase);
182
+ if (doc.type === 'policy') {
183
+ // policy → policies/{key}.xs (the key is validated, never snake_cased)
184
+ return { baseName: policyBaseName(doc.name), typeDir: join(outputDir, 'policies') };
185
+ }
186
+ if (doc.type === 'workspace') {
187
+ // workspace → workspace/{name}.xs
188
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'workspace') };
189
+ }
190
+ if (doc.type === 'workspace_trigger' || doc.type === 'error_trigger') {
191
+ // workspace_trigger / error_trigger → workspace/trigger/{name}.xs
192
+ // (error_trigger is a singleton, colocated with workspace triggers)
193
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'workspace', 'trigger') };
194
+ }
195
+ if (doc.type === 'agent') {
196
+ // agent → ai/agent/{name}.xs
197
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'ai', 'agent') };
198
+ }
199
+ if (doc.type === 'mcp_server') {
200
+ // mcp_server → ai/mcp_server/{name}.xs
201
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'ai', 'mcp_server') };
202
+ }
203
+ if (doc.type === 'tool') {
204
+ // tool → ai/tool/{name}.xs
205
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'ai', 'tool') };
206
+ }
207
+ if (doc.type === 'agent_trigger') {
208
+ // agent_trigger → ai/agent/trigger/{name}.xs
209
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'ai', 'agent', 'trigger') };
210
+ }
211
+ if (doc.type === 'mcp_server_trigger') {
212
+ // mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
213
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'ai', 'mcp_server', 'trigger') };
214
+ }
215
+ if (doc.type === 'table_trigger') {
216
+ // table_trigger → table/trigger/{name}.xs
217
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'table', 'trigger') };
218
+ }
219
+ if (doc.type === 'realtime_channel') {
220
+ // Realtime v1: realtime_channel → realtime/channel/{name}.xs
221
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'realtime', 'channel') };
222
+ }
223
+ if (doc.type === 'realtime_trigger') {
224
+ // Realtime v1: realtime_trigger → realtime/trigger/{name}.xs
225
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'realtime', 'trigger') };
226
+ }
227
+ if (doc.type === 'realtime_server_trigger') {
228
+ // Realtime v2. A realtime_server's connect/disconnect trigger. It references its
229
+ // owning server by name via `realtime_server = "..."`, so it nests under that
230
+ // server's folder as `<server>/trigger/`, mirroring how the server's own document
231
+ // and its channels already nest under realtime/server/<server>/ (DEV-7712). Keeping
232
+ // it there means every realtime document stays under realtime/ instead of landing at
233
+ // the output root. A server-less doc (a malformed/legacy export) falls back to a flat
234
+ // realtime/server_trigger/ under realtime/ rather than the repo root.
235
+ return {
236
+ baseName: sanitize(doc.name),
237
+ typeDir: doc.server
238
+ ? join(outputDir, 'realtime', 'server', sanitize(doc.server), 'trigger')
239
+ : join(outputDir, 'realtime', 'server_trigger'),
240
+ };
241
+ }
242
+ if (doc.type === 'channel_trigger') {
243
+ // Realtime v2. A channel's join/leave/deliver trigger. It references BOTH its owning
244
+ // server (`realtime_server = "..."`) and its channel (`channel = "..."`), so it nests
245
+ // beside that channel as `<server>/channel/<channel>/trigger/` — the same path the
246
+ // channel's own document and messages already use (DEV-7712). Falls back gracefully:
247
+ // channel but no resolvable server → the legacy flat channel/<path>/trigger/ layout;
248
+ // neither → realtime/channel_trigger/ under realtime/ (never the output root).
249
+ if (doc.server && doc.channel) {
250
+ return {
251
+ baseName: sanitize(doc.name),
252
+ typeDir: join(outputDir, 'realtime', 'server', sanitize(doc.server), 'channel', snakeCase(doc.channel), 'trigger'),
253
+ };
254
+ }
255
+ if (doc.channel) {
256
+ return {
257
+ baseName: sanitize(doc.name),
258
+ typeDir: join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase), 'trigger'),
259
+ };
260
+ }
261
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'realtime', 'channel_trigger') };
262
+ }
263
+ if (doc.type === 'realtime_server') {
264
+ // Realtime v2. The realtime_server is the top-level container that owns
265
+ // channels (which own messages). Its own document is named after itself
266
+ // inside its name directory, mirroring api_group (api/<group>/<group>.xs).
267
+ // realtime_server "chat" → realtime/server/chat/chat.xs
268
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'realtime', 'server', sanitize(doc.name)) };
269
+ }
270
+ if (doc.type === 'channel') {
271
+ // Realtime v2. The channel owns a directory named after itself (the full
272
+ // channel name, snake_cased into a single flat segment), and its messages
273
+ // live in a message/ subfolder inside it. It nests under its owning
274
+ // realtime_server (from `realtime_server = "..."`):
275
+ // channel "rooms/{room_id}" (server "chat")
276
+ // → realtime/server/chat/channel/rooms_room_id/rooms_room_id.xs
277
+ //
278
+ // A channel with no resolvable server (e.g. a pre-server export) falls back
279
+ // to the legacy flat channel/<path>/_channel.xs layout.
280
+ if (doc.server) {
281
+ return {
282
+ baseName: snakeCase(doc.name),
283
+ typeDir: join(outputDir, 'realtime', 'server', sanitize(doc.server), 'channel', snakeCase(doc.name)),
284
+ };
285
+ }
286
+ return { baseName: '_channel', typeDir: join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase)) };
287
+ }
288
+ if (doc.type === 'message' && doc.channel) {
289
+ // Realtime v2 message → nests in a message/ subfolder under its channel,
290
+ // under that channel's server (resolved via the two-pass channel→server map).
291
+ // message "post" on channel "rooms/{room_id}" (server "chat")
292
+ // → realtime/server/chat/channel/rooms_room_id/message/post.xs
293
+ //
294
+ // Nesting matters beyond tidiness: message names are unique only WITHIN a
295
+ // channel, so a flat message/ directory would collide when two channels
296
+ // both define e.g. "say". A channel whose server can't be resolved falls
297
+ // back to the legacy flat channel/ layout.
298
+ const messageServer = getChannelServer(doc.channel);
299
+ return {
300
+ baseName: sanitize(doc.name),
301
+ typeDir: messageServer
302
+ ? join(outputDir, 'realtime', 'server', sanitize(messageServer), 'channel', snakeCase(doc.channel), 'message')
303
+ : join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase)),
304
+ };
305
+ }
306
+ if (doc.type === 'api_group') {
307
+ // api_group "test" → api/{resolved_folder}/{name}.xs
308
+ return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'api', getApiGroupFolder(doc.name)) };
309
+ }
310
+ if (doc.type === 'query' && doc.apiGroup) {
311
+ // query in group "test" → api/{resolved_folder}/{folders}/{query_name}[_verb].xs
312
+ const groupFolder = getApiGroupFolder(doc.apiGroup);
313
+ const nameParts = doc.name.split('/');
314
+ const leafName = nameParts.pop();
315
+ const folderParts = nameParts.map((part) => snakeCase(part));
316
+ const baseName = sanitize(leafName);
317
+ return {
318
+ baseName: doc.verb ? `${baseName}_${doc.verb}` : baseName,
319
+ typeDir: join(outputDir, 'api', groupFolder, ...folderParts),
320
+ };
321
+ }
322
+ // Default: split folder path from name
323
+ const nameParts = doc.name.split('/');
324
+ const leafName = nameParts.pop();
325
+ const folderParts = nameParts.map((part) => snakeCase(part));
326
+ const baseName = sanitize(leafName);
327
+ return {
328
+ baseName: doc.verb ? `${baseName}_${doc.verb}` : baseName,
329
+ typeDir: join(outputDir, doc.type, ...folderParts),
330
+ };
61
331
  }
62
332
  /**
63
333
  * Build a unique key for a document based on its type, name, verb, and api_group.
@@ -101,9 +371,7 @@ export function buildApiGroupFolderResolver(documents, snakeCaseFn) {
101
371
  apiGroupFolderMap.set(names[i], `${folder}_${i + 1}`);
102
372
  }
103
373
  }
104
- return (groupName) => {
105
- return apiGroupFolderMap.get(groupName) ?? snakeCaseFn(groupName);
106
- };
374
+ return (groupName) => apiGroupFolderMap.get(groupName) ?? snakeCaseFn(groupName);
107
375
  }
108
376
  /**
109
377
  * Find local .xs files that contain a specific GUID.
@@ -112,3 +380,63 @@ export function buildApiGroupFolderResolver(documents, snakeCaseFn) {
112
380
  export function findFilesWithGuid(entries, guid) {
113
381
  return entries.filter((e) => e.content.includes(guid)).map((e) => e.filePath);
114
382
  }
383
+ /**
384
+ * Split a multi-document XanoScript blob (documents joined by `\n---\n`) into
385
+ * parsed documents, skipping empties and any fragment that fails to parse.
386
+ *
387
+ * This is the same split the pull commands apply to the server's multidoc
388
+ * response. A hand-authored bundle is one file of many `---`-separated
389
+ * documents; this turns it back into the per-document units every downstream
390
+ * step (placement, GUID writeback, partial-diff matching) assumes.
391
+ */
392
+ export function splitMultidoc(blob) {
393
+ const documents = [];
394
+ for (const raw of blob.split('\n---\n')) {
395
+ const trimmed = raw.trim();
396
+ if (!trimmed)
397
+ continue;
398
+ const parsed = parseDocument(trimmed);
399
+ if (parsed)
400
+ documents.push(parsed);
401
+ }
402
+ return documents;
403
+ }
404
+ /**
405
+ * Compute where each document lands on disk — a PURE function (no filesystem)
406
+ * mirroring the split-and-place loop the pull commands run. It resolves each
407
+ * document via `resolveDocumentPath`, appends `.xs`, and disambiguates
408
+ * collisions within a directory with a `_N` suffix (the second file named
409
+ * `say.xs` in a dir becomes `say_2.xs`), exactly as the pull writer does.
410
+ *
411
+ * Kept filesystem-free so it can be unit-tested and reused by both the pull
412
+ * commands and `flatten` without duplicating the placement rules.
413
+ */
414
+ export function placeDocuments(documents, deps) {
415
+ const { join, relative, snakeCase } = deps;
416
+ const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
417
+ const getChannelServer = buildChannelServerResolver(documents);
418
+ // Per-directory filename counters, so duplicate leaf names get a _N suffix.
419
+ const filenameCounters = new Map();
420
+ const placed = [];
421
+ for (const doc of documents) {
422
+ const { baseName, typeDir } = resolveDocumentPath(doc, '', {
423
+ getApiGroupFolder,
424
+ getChannelServer,
425
+ join,
426
+ snakeCase,
427
+ });
428
+ const dirKey = typeDir;
429
+ if (!filenameCounters.has(dirKey))
430
+ filenameCounters.set(dirKey, new Map());
431
+ const typeCounters = filenameCounters.get(dirKey);
432
+ const count = typeCounters.get(baseName) || 0;
433
+ typeCounters.set(baseName, count + 1);
434
+ const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
435
+ const abs = join(typeDir, filename);
436
+ // typeDir was built off an empty root, so `abs` is already root-relative;
437
+ // normalize any leading separator via relative() for a clean POSIX path.
438
+ const relPath = relative('', abs);
439
+ placed.push({ content: doc.content, relPath });
440
+ }
441
+ return placed;
442
+ }
@@ -0,0 +1,36 @@
1
+ /** Outcome of flattening one bundle file. */
2
+ export interface FlattenResult {
3
+ /** True when the original bundle was removed. */
4
+ removedSource: boolean;
5
+ /** Why the file was skipped, if it was (empty / single-document). */
6
+ skipped?: 'empty' | 'single';
7
+ /** Absolute paths written (or that would be written, in dry-run). */
8
+ written: string[];
9
+ }
10
+ export interface FlattenOptions {
11
+ /** Preview only — compute the plan and write nothing, delete nothing. */
12
+ dryRun?: boolean;
13
+ /** Overwrite existing destination files instead of throwing on a clash. */
14
+ force?: boolean;
15
+ /** Keep the original bundle file (default: delete after a successful split). */
16
+ keepSource?: boolean;
17
+ /** Optional line logger (e.g. command.log) for a per-file plan. */
18
+ log?: (msg: string) => void;
19
+ /** Output root (default: the bundle file's own directory). */
20
+ outputDir?: string;
21
+ }
22
+ /**
23
+ * Split ONE multi-document `.xs` bundle (documents joined by `---`) into the
24
+ * standard per-document tree — the exact layout `pull` produces — using the
25
+ * shared `placeDocuments` placement so the two never drift.
26
+ *
27
+ * Filesystem-touching, but self-contained: it reads the bundle, resolves every
28
+ * document's destination, checks for collisions, writes, and (unless
29
+ * `keepSource`) removes the original. Throws on a destination collision when
30
+ * `force` is not set, and never deletes the source when a written document
31
+ * landed on the source's own path.
32
+ *
33
+ * Shared by the `flatten` command and the push-time prompt, so a user who
34
+ * confirms "flatten now?" gets byte-identical output to running `xano flatten`.
35
+ */
36
+ export declare function flattenBundleFile(bundleFile: string, opts?: FlattenOptions): FlattenResult;