@xano/cli 1.3.3 → 1.3.4

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.
@@ -0,0 +1,52 @@
1
+ import { type ParsedDocument } from './document-parser.js';
2
+ export declare const ITEMS_BLOCK_REGEX: RegExp;
3
+ /**
4
+ * Split a GET /multidoc response into parsed documents, skipping empty or invalid segments.
5
+ */
6
+ export declare function parseExportDocuments(multidoc: string): ParsedDocument[];
7
+ /**
8
+ * Reinsert a local table `items = [...]` block when the server export omitted records.
9
+ * If the export already has an items block, the server copy wins.
10
+ */
11
+ export declare function preserveLocalTableRecords(localContent: string, serverContent: string): string;
12
+ export interface DocumentIndex {
13
+ byGuid: Map<string, string>;
14
+ byKey: Map<string, string>;
15
+ byKeyLower: Map<string, string>;
16
+ }
17
+ /**
18
+ * Index local files by document key (and GUID) so an export document can be paired
19
+ * with the file it belongs to. Ambiguous case-insensitive keys are marked empty
20
+ * rather than guessed.
21
+ */
22
+ export declare function indexLocalDocuments(files: Map<string, string>, localContents: Map<string, string>): DocumentIndex;
23
+ export declare function resolveMatchedPath(doc: ParsedDocument, index: DocumentIndex): string | undefined;
24
+ export declare function selectWritebacks(exported: ParsedDocument[], pushedFiles: Map<string, string>, localContents: Map<string, string>, options: {
25
+ includeRecords: boolean;
26
+ }): Array<{
27
+ content: string;
28
+ filePath: string;
29
+ }>;
30
+ export interface ExportCompareResult {
31
+ candidates: Array<{
32
+ content: string;
33
+ filePath: string;
34
+ relPath: string;
35
+ }>;
36
+ identical: Array<{
37
+ content: string;
38
+ filePath: string;
39
+ relPath: string;
40
+ }>;
41
+ serverOnly: ParsedDocument[];
42
+ unmatchedLocal: Array<{
43
+ content: string;
44
+ filePath: string;
45
+ relPath: string;
46
+ }>;
47
+ }
48
+ export declare function compareLocalToExport(localEntries: Array<{
49
+ content: string;
50
+ filePath: string;
51
+ relPath: string;
52
+ }>, exported: ParsedDocument[]): ExportCompareResult;
@@ -0,0 +1,134 @@
1
+ import { buildDocumentKey, parseDocument } from './document-parser.js';
2
+ export const ITEMS_BLOCK_REGEX = /^([ \t]*)items\s*=\s*\[[\s\S]*?\n\1\]/m;
3
+ /**
4
+ * Split a GET /multidoc response into parsed documents, skipping empty or invalid segments.
5
+ */
6
+ export function parseExportDocuments(multidoc) {
7
+ const documents = [];
8
+ for (const raw of multidoc.split('\n---\n')) {
9
+ const trimmed = raw.trim();
10
+ if (!trimmed)
11
+ continue;
12
+ const parsed = parseDocument(trimmed);
13
+ if (parsed)
14
+ documents.push(parsed);
15
+ }
16
+ return documents;
17
+ }
18
+ /**
19
+ * Reinsert a local table `items = [...]` block when the server export omitted records.
20
+ * If the export already has an items block, the server copy wins.
21
+ */
22
+ export function preserveLocalTableRecords(localContent, serverContent) {
23
+ if (ITEMS_BLOCK_REGEX.test(serverContent)) {
24
+ return serverContent;
25
+ }
26
+ const localMatch = localContent.match(ITEMS_BLOCK_REGEX);
27
+ if (!localMatch) {
28
+ return serverContent;
29
+ }
30
+ const lines = serverContent.split('\n');
31
+ let insertIndex = -1;
32
+ for (let i = lines.length - 1; i >= 0; i--) {
33
+ if (lines[i].trim() === '}') {
34
+ insertIndex = i;
35
+ break;
36
+ }
37
+ }
38
+ if (insertIndex === -1) {
39
+ return serverContent;
40
+ }
41
+ lines.splice(insertIndex, 0, ...localMatch[0].split('\n'));
42
+ return lines.join('\n');
43
+ }
44
+ /**
45
+ * Index local files by document key (and GUID) so an export document can be paired
46
+ * with the file it belongs to. Ambiguous case-insensitive keys are marked empty
47
+ * rather than guessed.
48
+ */
49
+ export function indexLocalDocuments(files, localContents) {
50
+ const byKey = new Map(files);
51
+ const byKeyLower = new Map();
52
+ for (const [key, filePath] of files) {
53
+ const lower = key.toLowerCase();
54
+ if (byKeyLower.has(lower) && byKeyLower.get(lower) !== filePath) {
55
+ byKeyLower.set(lower, '');
56
+ }
57
+ else if (!byKeyLower.has(lower)) {
58
+ byKeyLower.set(lower, filePath);
59
+ }
60
+ }
61
+ const byGuid = new Map();
62
+ for (const [filePath, local] of localContents) {
63
+ const parsed = parseDocument(local);
64
+ if (parsed?.guid)
65
+ byGuid.set(parsed.guid, filePath);
66
+ }
67
+ return { byGuid, byKey, byKeyLower };
68
+ }
69
+ export function resolveMatchedPath(doc, index) {
70
+ const key = buildDocumentKey(doc.type, doc.name, doc.verb, doc.apiGroup);
71
+ return index.byKey.get(key) || index.byKeyLower.get(key.toLowerCase()) || (doc.guid ? index.byGuid.get(doc.guid) : undefined);
72
+ }
73
+ export function selectWritebacks(exported, pushedFiles, localContents, options) {
74
+ const result = [];
75
+ const index = indexLocalDocuments(pushedFiles, localContents);
76
+ for (const doc of exported) {
77
+ const filePath = resolveMatchedPath(doc, index);
78
+ if (!filePath)
79
+ continue;
80
+ const local = localContents.get(filePath) ?? '';
81
+ let { content } = doc;
82
+ if (!options.includeRecords && doc.type === 'table') {
83
+ content = preserveLocalTableRecords(local, content);
84
+ }
85
+ if (local.trim() === content.trim())
86
+ continue;
87
+ result.push({ content, filePath });
88
+ }
89
+ return result;
90
+ }
91
+ export function compareLocalToExport(localEntries, exported) {
92
+ const pushedFiles = new Map();
93
+ const localContents = new Map();
94
+ const byPath = new Map();
95
+ for (const entry of localEntries) {
96
+ const parsed = parseDocument(entry.content);
97
+ if (!parsed)
98
+ continue;
99
+ const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
100
+ pushedFiles.set(key, entry.filePath);
101
+ localContents.set(entry.filePath, entry.content);
102
+ byPath.set(entry.filePath, entry);
103
+ }
104
+ const index = indexLocalDocuments(pushedFiles, localContents);
105
+ const matchedLocal = new Set();
106
+ const candidates = [];
107
+ const identical = [];
108
+ const serverOnly = [];
109
+ for (const doc of exported) {
110
+ const filePath = resolveMatchedPath(doc, index);
111
+ if (!filePath) {
112
+ serverOnly.push(doc);
113
+ continue;
114
+ }
115
+ matchedLocal.add(filePath);
116
+ const local = byPath.get(filePath);
117
+ if (!local)
118
+ continue;
119
+ if (local.content.trim() === doc.content.trim()) {
120
+ identical.push(local);
121
+ }
122
+ else {
123
+ candidates.push(local);
124
+ }
125
+ }
126
+ const unmatchedLocal = [];
127
+ for (const entry of localEntries) {
128
+ if (matchedLocal.has(entry.filePath))
129
+ continue;
130
+ unmatchedLocal.push(entry);
131
+ candidates.push(entry);
132
+ }
133
+ return { candidates, identical, serverOnly, unmatchedLocal };
134
+ }
@@ -4,9 +4,12 @@ export declare const LOCAL_PROFILE_FILENAME = "profile.yaml";
4
4
  export interface LocalProfileConfig {
5
5
  account_origin?: string;
6
6
  branch?: string;
7
+ fast?: boolean;
7
8
  instance_origin?: string;
8
9
  profile?: string;
9
10
  workspace?: string;
11
+ writeback?: boolean;
12
+ writeback_object_limit?: number;
10
13
  }
11
14
  /**
12
15
  * Parse the raw contents of a profile.yaml.
@@ -34,8 +37,28 @@ export declare function resolveProfileSelection(params: {
34
37
  applyLocal: boolean;
35
38
  profileName: string;
36
39
  };
40
+ export type WorkspacePushDefaultKey = 'fast' | 'writeback';
41
+ /**
42
+ * Resolve push --fast/--writeback from the explicit flag, then
43
+ * profile.yaml, then false. `--sync` ignores a profile.yaml `fast: true`
44
+ * instead of erroring; an explicit `--fast --sync` is left as-is for executePush.
45
+ * Used by both workspace push and sandbox push.
46
+ */
47
+ export declare function resolvePushDefaults(args: {
48
+ flagFast?: boolean;
49
+ flagWriteback?: boolean;
50
+ localFast?: boolean;
51
+ localWriteback?: boolean;
52
+ sync: boolean;
53
+ }): {
54
+ fast: boolean;
55
+ fromLocal: WorkspacePushDefaultKey[];
56
+ writeback: boolean;
57
+ };
37
58
  /** Format the one-line banner shown when a local profile.yaml is in effect. */
38
59
  export declare function formatLocalProfileBanner(profileName: string, workspace: string | undefined, relativePath: string): string;
60
+ /** Format the one-line banner when profile.yaml turns on workspace push flags. */
61
+ export declare function formatWorkspacePushDefaultsBanner(fromLocal: WorkspacePushDefaultKey[], relativePath: string): string;
39
62
  /**
40
63
  * Walk up from `startDir` to the filesystem root, returning the path of the
41
64
  * first profile.yaml found, or null if none exists.
@@ -2,7 +2,9 @@ import * as yaml from 'js-yaml';
2
2
  import * as fs from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  export const LOCAL_PROFILE_FILENAME = 'profile.yaml';
5
- const RECOGNIZED_KEYS = ['profile', 'workspace', 'instance_origin', 'account_origin', 'branch'];
5
+ const RECOGNIZED_STRING_KEYS = ['profile', 'workspace', 'instance_origin', 'account_origin', 'branch'];
6
+ const RECOGNIZED_BOOLEAN_KEYS = ['fast', 'writeback'];
7
+ const RECOGNIZED_NON_NEGATIVE_INTEGER_KEYS = ['writeback_object_limit'];
6
8
  /**
7
9
  * Fields that may be layered onto a resolved profile (everything except the profile pointer).
8
10
  * NOTE: `insecure` is intentionally NOT listed here — a project file must never silently
@@ -26,11 +28,28 @@ export function parseLocalProfile(raw) {
26
28
  `Tokens belong in credentials.yaml — reference a profile by name instead.`);
27
29
  }
28
30
  const config = {};
29
- for (const key of RECOGNIZED_KEYS) {
31
+ for (const key of RECOGNIZED_STRING_KEYS) {
30
32
  if (obj[key] !== undefined && obj[key] !== null) {
31
33
  config[key] = String(obj[key]);
32
34
  }
33
35
  }
36
+ for (const key of RECOGNIZED_BOOLEAN_KEYS) {
37
+ if (obj[key] === undefined || obj[key] === null)
38
+ continue;
39
+ if (typeof obj[key] !== 'boolean') {
40
+ throw new TypeError(`profile.yaml: ${key} must be a boolean`);
41
+ }
42
+ config[key] = obj[key];
43
+ }
44
+ for (const key of RECOGNIZED_NON_NEGATIVE_INTEGER_KEYS) {
45
+ if (obj[key] === undefined || obj[key] === null)
46
+ continue;
47
+ const value = obj[key];
48
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
49
+ throw new TypeError(`profile.yaml: ${key} must be a non-negative integer`);
50
+ }
51
+ config[key] = value;
52
+ }
34
53
  if (Object.keys(config).length === 0) {
35
54
  return null;
36
55
  }
@@ -63,11 +82,52 @@ export function resolveProfileSelection(params) {
63
82
  }
64
83
  return { applyLocal: false, profileName: params.defaultProfile };
65
84
  }
85
+ /**
86
+ * Resolve push --fast/--writeback from the explicit flag, then
87
+ * profile.yaml, then false. `--sync` ignores a profile.yaml `fast: true`
88
+ * instead of erroring; an explicit `--fast --sync` is left as-is for executePush.
89
+ * Used by both workspace push and sandbox push.
90
+ */
91
+ export function resolvePushDefaults(args) {
92
+ const fromLocal = [];
93
+ let fast;
94
+ if (args.flagFast !== undefined) {
95
+ fast = args.flagFast;
96
+ }
97
+ else if (args.sync) {
98
+ fast = false;
99
+ }
100
+ else if (args.localFast === undefined) {
101
+ fast = false;
102
+ }
103
+ else {
104
+ fast = args.localFast;
105
+ if (args.localFast)
106
+ fromLocal.push('fast');
107
+ }
108
+ let writeback;
109
+ if (args.flagWriteback !== undefined) {
110
+ writeback = args.flagWriteback;
111
+ }
112
+ else if (args.localWriteback === undefined) {
113
+ writeback = false;
114
+ }
115
+ else {
116
+ writeback = args.localWriteback;
117
+ if (args.localWriteback)
118
+ fromLocal.push('writeback');
119
+ }
120
+ return { fast, fromLocal, writeback };
121
+ }
66
122
  /** Format the one-line banner shown when a local profile.yaml is in effect. */
67
123
  export function formatLocalProfileBanner(profileName, workspace, relativePath) {
68
124
  const workspaceClause = workspace ? ` (workspace ${workspace})` : '';
69
125
  return `Using profile '${profileName}'${workspaceClause} · ${relativePath}`;
70
126
  }
127
+ /** Format the one-line banner when profile.yaml turns on workspace push flags. */
128
+ export function formatWorkspacePushDefaultsBanner(fromLocal, relativePath) {
129
+ return `Using ${fromLocal.map((key) => `--${key}`).join(' ')} · ${relativePath}`;
130
+ }
71
131
  /**
72
132
  * Walk up from `startDir` to the filesystem root, returning the path of the
73
133
  * first profile.yaml found, or null if none exists.
@@ -1,18 +1,24 @@
1
1
  import { Command } from '@oclif/core';
2
2
  import { type BadIndex, type BadReference } from './reference-checker.js';
3
+ import { type SyncFileEntry, type SyncTargetIdentity, type SyncTargetState } from './sync-state.js';
3
4
  export interface PushFlags {
4
5
  delete: boolean;
5
6
  'dry-run': boolean;
6
7
  env: boolean;
7
8
  exclude?: string[];
9
+ fast?: boolean;
8
10
  force: boolean;
9
11
  guids: boolean;
10
12
  include?: string[];
11
13
  records: boolean;
14
+ refresh?: boolean;
12
15
  sync: boolean;
13
16
  transaction: boolean;
14
17
  truncate: boolean;
15
18
  verbose: boolean;
19
+ writeback?: boolean;
20
+ writebackDiff?: string;
21
+ writebackObjectLimit?: number;
16
22
  }
17
23
  export interface PushTarget {
18
24
  /** Build the dry-run URL. Return null if dry-run is not supported for this target. */
@@ -47,6 +53,15 @@ export interface KnowledgeConfig {
47
53
  /** Root directory containing the `knowledge/` folder. */
48
54
  rootDir: string;
49
55
  }
56
+ export type PushSyncTarget = {
57
+ instanceOrigin: string;
58
+ kind: 'sandbox';
59
+ markerDir: string;
60
+ } | {
61
+ instanceOrigin: string;
62
+ kind: 'workspace';
63
+ workspace: string;
64
+ };
50
65
  export interface PushContext {
51
66
  accessToken: string;
52
67
  branch: string;
@@ -54,8 +69,16 @@ export interface PushContext {
54
69
  inputDir: string;
55
70
  /** Optional knowledge sync config. Only workspace push sets this. */
56
71
  knowledge?: KnowledgeConfig;
72
+ /** Optional sync-record target. Workspace and sandbox push set this when they want a record. */
73
+ syncTarget?: PushSyncTarget;
57
74
  verboseFetch: (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
58
75
  }
76
+ /** The fetch fields writeback reads. */
77
+ export interface WritebackHttpResponse {
78
+ ok: boolean;
79
+ status: number;
80
+ text: () => Promise<string>;
81
+ }
59
82
  /**
60
83
  * Filter document entries down to the ones the dry-run preview reports as changed,
61
84
  * used to send only the changed documents during a partial (non-`--sync`) push.
@@ -144,10 +167,12 @@ export declare function deletesHittingFilteredOut(operations: Array<{
144
167
  relPath: string;
145
168
  type: string;
146
169
  }>;
170
+ export declare function pathMatchesFilters(relPath: string, inputDir: string, include: string[] | undefined, exclude: string[] | undefined): boolean;
147
171
  /**
148
- * Read .xs files into document entries, skipping empty files.
172
+ * Read .xs files into document entries, skipping empty files. When `raw` is given, the
173
+ * untrimmed bytes of each returned file are stored in it, keyed by file path.
149
174
  */
150
- export declare function readDocuments(files: string[]): Array<{
175
+ export declare function readDocuments(files: string[], raw?: Map<string, string>): Array<{
151
176
  content: string;
152
177
  filePath: string;
153
178
  }>;
@@ -193,6 +218,45 @@ export declare function confirm(message: string): Promise<boolean>;
193
218
  * Shared by both sandbox:push and workspace:push commands.
194
219
  */
195
220
  export declare function executePush(ctx: PushContext, target: PushTarget, flags: PushFlags): Promise<void>;
221
+ export interface WritebackResult {
222
+ elapsedMs: number;
223
+ /** Object ids resolved by per-object writeback, keyed by relative path. */
224
+ ids: Record<string, SyncFileEntry>;
225
+ /** The exact bytes written to each rewritten file. */
226
+ written: Array<{
227
+ content: string;
228
+ filePath: string;
229
+ }>;
230
+ }
231
+ export declare function writeBackFormattedDocuments(args: {
232
+ accessToken: string;
233
+ branch: string;
234
+ command: Command;
235
+ includeEnv: boolean;
236
+ includeRecords: boolean;
237
+ inputDir?: string;
238
+ isLiveBranch?: boolean;
239
+ /**
240
+ * The bytes each file held when it was pushed (after GUID sync). A file whose disk content
241
+ * no longer matches was edited during the push, so writeback leaves it alone.
242
+ */
243
+ pushedBytes?: Map<string, string>;
244
+ pushedEntries: Array<{
245
+ content: string;
246
+ filePath: string;
247
+ }>;
248
+ resolvedTarget?: null | SyncTargetIdentity;
249
+ syncState?: null | SyncTargetState;
250
+ target: PushTarget;
251
+ verbose: boolean;
252
+ verboseFetch: (url: string, options: {
253
+ headers?: Record<string, string>;
254
+ method?: string;
255
+ }, verbose: boolean, authToken?: string) => Promise<WritebackHttpResponse>;
256
+ workspaceId?: string;
257
+ writebackDiff?: string;
258
+ writebackObjectLimit?: number;
259
+ }): Promise<WritebackResult>;
196
260
  /**
197
261
  * Turn a thrown fetch/network error into an actionable message.
198
262
  *
@@ -207,3 +271,4 @@ export declare function executePush(ctx: PushContext, target: PushTarget, flags:
207
271
  * signal of a server-side or proxy timeout rather than a local network blip.
208
272
  */
209
273
  export declare function describeNetworkError(error: unknown, url: string, elapsedMs?: number): string;
274
+ export { parseExportDocuments, preserveLocalTableRecords, selectWritebacks } from './export-compare.js';