@xano/cli 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -3
- package/dist/base-command.d.ts +43 -36
- package/dist/base-command.js +223 -160
- package/dist/commands/auth/index.d.ts +1 -1
- package/dist/commands/auth/index.js +7 -7
- package/dist/commands/branch/create/index.d.ts +1 -1
- package/dist/commands/branch/create/index.js +1 -1
- package/dist/commands/ephemeral/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/create/index.js +103 -0
- package/dist/commands/ephemeral/delete/index.d.ts +18 -0
- package/dist/commands/ephemeral/delete/index.js +99 -0
- package/dist/commands/ephemeral/edit/index.d.ts +18 -0
- package/dist/commands/ephemeral/edit/index.js +109 -0
- package/dist/commands/ephemeral/get/index.d.ts +21 -0
- package/dist/commands/ephemeral/get/index.js +125 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
- package/dist/commands/ephemeral/impersonate/index.js +120 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +123 -0
- package/dist/commands/ephemeral/pull/index.d.ts +19 -0
- package/dist/commands/ephemeral/pull/index.js +163 -0
- package/dist/commands/ephemeral/push/index.d.ts +35 -0
- package/dist/commands/ephemeral/push/index.js +223 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
- package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/create/index.js +92 -0
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
- package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/get/index.js +101 -0
- package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
- package/dist/commands/ephemeral/static_host/list/index.js +132 -0
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/create/index.js +1 -1
- package/dist/commands/function/edit/index.js +26 -32
- package/dist/commands/function/get/index.js +2 -3
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +19 -6
- package/dist/commands/profile/create/index.d.ts +1 -1
- package/dist/commands/profile/create/index.js +7 -7
- package/dist/commands/profile/me/index.js +1 -1
- package/dist/commands/profile/use/index.js +0 -4
- package/dist/commands/profile/wizard/index.d.ts +1 -1
- package/dist/commands/profile/wizard/index.js +12 -10
- package/dist/commands/profile/workspace/set/index.js +1 -1
- package/dist/commands/release/edit/index.js +2 -2
- package/dist/commands/release/export/index.js +1 -2
- package/dist/commands/release/import/index.js +1 -1
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +13 -99
- package/dist/commands/release/push/index.js +2 -2
- package/dist/commands/sandbox/env/get_all/index.js +1 -1
- package/dist/commands/sandbox/env/set_all/index.js +1 -1
- package/dist/commands/sandbox/get/index.js +1 -1
- package/dist/commands/sandbox/license/get/index.js +1 -1
- package/dist/commands/sandbox/license/set/index.js +1 -1
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +13 -81
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +5 -5
- package/dist/commands/static_host/build/create/index.d.ts +1 -1
- package/dist/commands/static_host/build/create/index.js +2 -2
- package/dist/commands/static_host/build/pull/index.js +1 -1
- package/dist/commands/static_host/build/push/index.js +1 -1
- package/dist/commands/static_host/edit/index.js +1 -1
- package/dist/commands/tenant/backup/export/index.js +1 -2
- package/dist/commands/tenant/backup/import/index.js +1 -1
- package/dist/commands/tenant/cluster/create/index.js +1 -1
- package/dist/commands/tenant/cluster/license/get/index.js +1 -1
- package/dist/commands/tenant/cluster/license/set/index.js +1 -1
- package/dist/commands/tenant/create/index.d.ts +1 -1
- package/dist/commands/tenant/create/index.js +6 -6
- package/dist/commands/tenant/deploy_platform/index.js +1 -1
- package/dist/commands/tenant/edit/index.js +7 -7
- package/dist/commands/tenant/env/get_all/index.js +1 -1
- package/dist/commands/tenant/env/set_all/index.js +1 -1
- package/dist/commands/tenant/impersonate/index.d.ts +1 -1
- package/dist/commands/tenant/impersonate/index.js +14 -14
- package/dist/commands/tenant/license/get/index.js +1 -1
- package/dist/commands/tenant/license/set/index.js +1 -1
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +14 -100
- package/dist/commands/workspace/git/pull/index.d.ts +4 -4
- package/dist/commands/workspace/git/pull/index.js +158 -97
- package/dist/commands/workspace/pull/index.d.ts +1 -7
- package/dist/commands/workspace/pull/index.js +17 -103
- package/dist/help.js +4 -2
- package/dist/update-check.d.ts +1 -1
- package/dist/update-check.js +1 -1
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +311 -4
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -6
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +84 -0
- package/dist/utils/multidoc-push.d.ts +88 -0
- package/dist/utils/multidoc-push.js +378 -15
- package/dist/utils/paging.d.ts +2 -1
- package/dist/utils/paging.js +0 -1
- package/dist/utils/reference-checker.js +14 -3
- package/oclif.manifest.json +5264 -3170
- package/package.json +10 -1
|
@@ -89,7 +89,61 @@ export declare function collectFiles(dir: string): string[];
|
|
|
89
89
|
* Apply include/exclude glob filters to a file list. Logs filter results.
|
|
90
90
|
* Returns the filtered file list.
|
|
91
91
|
*/
|
|
92
|
+
/**
|
|
93
|
+
* Normalize a filter pattern into one minimatch can actually match against a
|
|
94
|
+
* repo-relative FILE path.
|
|
95
|
+
*
|
|
96
|
+
* minimatch has no notion of "this pattern names a directory", and the two
|
|
97
|
+
* spellings a person reaches for first both silently match NOTHING:
|
|
98
|
+
*
|
|
99
|
+
* "table/" a trailing slash is not directory syntax; it matches nothing
|
|
100
|
+
* "table" no slash, so `matchBase` compares it to the file's BASENAME,
|
|
101
|
+
* i.e. it asks "is this file called `table`", not "is it under
|
|
102
|
+
* `table/`"
|
|
103
|
+
*
|
|
104
|
+
* Silently matching nothing is the dangerous half of this: combined with
|
|
105
|
+
* `--delete`, a filter that fails open pushes more than intended, and a filter
|
|
106
|
+
* that succeeds deletes what it was meant to protect. So a pattern that names a
|
|
107
|
+
* real directory is expanded to `<dir>/**` rather than left to match nothing.
|
|
108
|
+
*/
|
|
109
|
+
export declare function normalizeFilterPattern(pattern: string, inputDir: string): string;
|
|
92
110
|
export declare function applyFilters(files: string[], inputDir: string, include: string[] | undefined, exclude: string[] | undefined, log: (msg: string) => void): string[];
|
|
111
|
+
/** A document the filter removed from the push, and therefore must protect. */
|
|
112
|
+
export interface FilteredOutDocument {
|
|
113
|
+
guid?: string;
|
|
114
|
+
key: string;
|
|
115
|
+
name: string;
|
|
116
|
+
relPath: string;
|
|
117
|
+
type: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Describe the documents a --include/--exclude filter removed.
|
|
121
|
+
*
|
|
122
|
+
* These are the objects the user asked NOT to touch, and they are exactly the
|
|
123
|
+
* ones a `--delete` sweep would remove: a filtered-out document never reaches
|
|
124
|
+
* the payload, so the server reads it as deleted from the tree. Their GUIDs are
|
|
125
|
+
* sent as `protect_guids` so the server spares them, and their keys are used to
|
|
126
|
+
* VERIFY the server actually did.
|
|
127
|
+
*/
|
|
128
|
+
export declare function describeFilteredOut(allFiles: string[], keptFiles: string[], inputDir: string): FilteredOutDocument[];
|
|
129
|
+
/**
|
|
130
|
+
* Delete operations that would remove something the filter excluded.
|
|
131
|
+
*
|
|
132
|
+
* This is the guard's actual test, and it is deliberately performed against the
|
|
133
|
+
* dry-run the SERVER returned rather than trusting that `protect_guids` was
|
|
134
|
+
* honoured: an older instance ignores the parameter entirely, and a document
|
|
135
|
+
* with no GUID cannot be protected at all. Either way the answer here is the
|
|
136
|
+
* truth about what the push is about to do.
|
|
137
|
+
*/
|
|
138
|
+
export declare function deletesHittingFilteredOut(operations: Array<{
|
|
139
|
+
action: string;
|
|
140
|
+
name: string;
|
|
141
|
+
type: string;
|
|
142
|
+
}>, filteredOut: FilteredOutDocument[]): Array<{
|
|
143
|
+
name: string;
|
|
144
|
+
relPath: string;
|
|
145
|
+
type: string;
|
|
146
|
+
}>;
|
|
93
147
|
/**
|
|
94
148
|
* Read .xs files into document entries, skipping empty files.
|
|
95
149
|
*/
|
|
@@ -97,6 +151,40 @@ export declare function readDocuments(files: string[]): Array<{
|
|
|
97
151
|
content: string;
|
|
98
152
|
filePath: string;
|
|
99
153
|
}>;
|
|
154
|
+
/**
|
|
155
|
+
* Return the entries whose file holds more than one document (a `---`
|
|
156
|
+
* separator). Used to refuse multi-doc files before the push pipeline's
|
|
157
|
+
* single-doc-per-file assumptions can drop or corrupt them.
|
|
158
|
+
*/
|
|
159
|
+
export declare function findMultiDocEntries(entries: Array<{
|
|
160
|
+
content: string;
|
|
161
|
+
filePath: string;
|
|
162
|
+
}>): Array<{
|
|
163
|
+
count: number;
|
|
164
|
+
filePath: string;
|
|
165
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* Return every `workspace` document in the push set, with the name it declares.
|
|
168
|
+
*
|
|
169
|
+
* A tree must carry at most ONE. The server applies the first workspace
|
|
170
|
+
* document it encounters to the workspace being pushed into and silently
|
|
171
|
+
* discards the rest, with no check that the document actually describes that
|
|
172
|
+
* workspace — so a second one renames the destination to a foreign name while
|
|
173
|
+
* leaving its content untouched, and which name wins flips with the contents of
|
|
174
|
+
* the pushed set.
|
|
175
|
+
*
|
|
176
|
+
* Trees accumulate a second one easily, because `pull` names the file after the
|
|
177
|
+
* workspace's own name (`workspace/{name}.xs`): pulling a different workspace
|
|
178
|
+
* into the same directory ADDS a file rather than overwriting, and renaming a
|
|
179
|
+
* workspace leaves the old-name file behind.
|
|
180
|
+
*/
|
|
181
|
+
export declare function findWorkspaceEntries(entries: Array<{
|
|
182
|
+
content: string;
|
|
183
|
+
filePath: string;
|
|
184
|
+
}>): Array<{
|
|
185
|
+
filePath: string;
|
|
186
|
+
name: string;
|
|
187
|
+
}>;
|
|
100
188
|
export declare function renderBadReferences(badRefs: BadReference[], log: (msg: string) => void): void;
|
|
101
189
|
export declare function renderBadIndexes(badIndexes: BadIndex[], log: (msg: string) => void): void;
|
|
102
190
|
export declare function confirm(message: string): Promise<boolean>;
|
|
@@ -3,6 +3,7 @@ import { minimatch } from 'minimatch';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { buildDocumentKey, findFilesWithGuid, parseDocument } from './document-parser.js';
|
|
6
|
+
import { flattenBundleFile } from './flatten.js';
|
|
6
7
|
import { collectKnowledgeObjects, fetchKnowledge, knowledgePreview, pushKnowledge, syncGuidToFrontmatter, toPushItems, } from './knowledge-sync.js';
|
|
7
8
|
import { checkReferences, checkTableIndexes } from './reference-checker.js';
|
|
8
9
|
/**
|
|
@@ -65,30 +66,151 @@ export function collectFiles(dir) {
|
|
|
65
66
|
* Apply include/exclude glob filters to a file list. Logs filter results.
|
|
66
67
|
* Returns the filtered file list.
|
|
67
68
|
*/
|
|
69
|
+
/**
|
|
70
|
+
* Normalize a filter pattern into one minimatch can actually match against a
|
|
71
|
+
* repo-relative FILE path.
|
|
72
|
+
*
|
|
73
|
+
* minimatch has no notion of "this pattern names a directory", and the two
|
|
74
|
+
* spellings a person reaches for first both silently match NOTHING:
|
|
75
|
+
*
|
|
76
|
+
* "table/" a trailing slash is not directory syntax; it matches nothing
|
|
77
|
+
* "table" no slash, so `matchBase` compares it to the file's BASENAME,
|
|
78
|
+
* i.e. it asks "is this file called `table`", not "is it under
|
|
79
|
+
* `table/`"
|
|
80
|
+
*
|
|
81
|
+
* Silently matching nothing is the dangerous half of this: combined with
|
|
82
|
+
* `--delete`, a filter that fails open pushes more than intended, and a filter
|
|
83
|
+
* that succeeds deletes what it was meant to protect. So a pattern that names a
|
|
84
|
+
* real directory is expanded to `<dir>/**` rather than left to match nothing.
|
|
85
|
+
*/
|
|
86
|
+
export function normalizeFilterPattern(pattern, inputDir) {
|
|
87
|
+
const trimmed = pattern.replace(/[/\\]+$/, '');
|
|
88
|
+
if (trimmed === '')
|
|
89
|
+
return pattern;
|
|
90
|
+
// Anything with a glob character is taken exactly as written.
|
|
91
|
+
if (trimmed.includes('*') || trimmed.includes('?') || trimmed.includes('[')) {
|
|
92
|
+
return trimmed === pattern ? pattern : trimmed;
|
|
93
|
+
}
|
|
94
|
+
// A trailing slash is an unambiguous "this is a directory".
|
|
95
|
+
if (trimmed !== pattern)
|
|
96
|
+
return `${trimmed}/**`;
|
|
97
|
+
try {
|
|
98
|
+
if (fs.statSync(join(inputDir, trimmed)).isDirectory())
|
|
99
|
+
return `${trimmed}/**`;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Not a path in this tree - leave it alone (it may be a bare filename,
|
|
103
|
+
// which matchBase handles).
|
|
104
|
+
}
|
|
105
|
+
return pattern;
|
|
106
|
+
}
|
|
68
107
|
export function applyFilters(files, inputDir, include, exclude, log) {
|
|
69
108
|
let filtered = files;
|
|
70
109
|
const totalCount = files.length;
|
|
110
|
+
// Report each pattern's own match count. The aggregate line alone hides a dead
|
|
111
|
+
// pattern among live ones: a run with three -e patterns where one is a typo
|
|
112
|
+
// still prints a plausible number and says nothing about which one matched
|
|
113
|
+
// nothing.
|
|
114
|
+
const describe = (patterns, rels) => patterns
|
|
115
|
+
.map((raw) => {
|
|
116
|
+
const pattern = normalizeFilterPattern(raw, inputDir);
|
|
117
|
+
const hits = rels.filter((rel) => minimatch(rel, pattern, { matchBase: true })).length;
|
|
118
|
+
const shown = pattern === raw ? raw : `${raw} -> ${pattern}`;
|
|
119
|
+
return hits === 0
|
|
120
|
+
? `${ux.colorize('yellow', shown)} ${ux.colorize('yellow', '(matched 0 files)')}`
|
|
121
|
+
: `${ux.colorize('cyan', shown)} ${ux.colorize('dim', `(${hits})`)}`;
|
|
122
|
+
})
|
|
123
|
+
.join(', ');
|
|
71
124
|
if (include && include.length > 0) {
|
|
125
|
+
const rels = filtered.map((f) => relative(inputDir, f));
|
|
126
|
+
const patterns = include.map((p) => normalizeFilterPattern(p, inputDir));
|
|
127
|
+
log('');
|
|
128
|
+
log(` ${ux.colorize('dim', 'Include:')} ${describe(include, rels)}`);
|
|
72
129
|
filtered = filtered.filter((f) => {
|
|
73
130
|
const rel = relative(inputDir, f);
|
|
74
|
-
return
|
|
131
|
+
return patterns.some((pattern) => minimatch(rel, pattern, { matchBase: true }));
|
|
75
132
|
});
|
|
76
|
-
log('');
|
|
77
|
-
log(` ${ux.colorize('dim', 'Include:')} ${include.map((p) => ux.colorize('cyan', p)).join(', ')}`);
|
|
78
133
|
log(` ${ux.colorize('dim', 'Matched:')} ${ux.colorize('bold', String(filtered.length))} of ${totalCount} files`);
|
|
79
134
|
}
|
|
80
135
|
if (exclude && exclude.length > 0) {
|
|
81
136
|
const beforeCount = filtered.length;
|
|
137
|
+
const rels = filtered.map((f) => relative(inputDir, f));
|
|
138
|
+
const patterns = exclude.map((p) => normalizeFilterPattern(p, inputDir));
|
|
139
|
+
log('');
|
|
140
|
+
log(` ${ux.colorize('dim', 'Exclude:')} ${describe(exclude, rels)}`);
|
|
82
141
|
filtered = filtered.filter((f) => {
|
|
83
142
|
const rel = relative(inputDir, f);
|
|
84
|
-
return !
|
|
143
|
+
return !patterns.some((pattern) => minimatch(rel, pattern, { matchBase: true }));
|
|
85
144
|
});
|
|
86
|
-
log('');
|
|
87
|
-
log(` ${ux.colorize('dim', 'Exclude:')} ${exclude.map((p) => ux.colorize('cyan', p)).join(', ')}`);
|
|
88
145
|
log(` ${ux.colorize('dim', 'Kept:')} ${ux.colorize('bold', String(filtered.length))} of ${beforeCount} files (excluded ${beforeCount - filtered.length})`);
|
|
89
146
|
}
|
|
90
147
|
return filtered;
|
|
91
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Describe the documents a --include/--exclude filter removed.
|
|
151
|
+
*
|
|
152
|
+
* These are the objects the user asked NOT to touch, and they are exactly the
|
|
153
|
+
* ones a `--delete` sweep would remove: a filtered-out document never reaches
|
|
154
|
+
* the payload, so the server reads it as deleted from the tree. Their GUIDs are
|
|
155
|
+
* sent as `protect_guids` so the server spares them, and their keys are used to
|
|
156
|
+
* VERIFY the server actually did.
|
|
157
|
+
*/
|
|
158
|
+
export function describeFilteredOut(allFiles, keptFiles, inputDir) {
|
|
159
|
+
const kept = new Set(keptFiles);
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const filePath of allFiles) {
|
|
162
|
+
if (kept.has(filePath))
|
|
163
|
+
continue;
|
|
164
|
+
let content;
|
|
165
|
+
try {
|
|
166
|
+
content = fs.readFileSync(filePath, 'utf8').trim();
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!content)
|
|
172
|
+
continue;
|
|
173
|
+
const parsed = parseDocument(content);
|
|
174
|
+
if (!parsed)
|
|
175
|
+
continue;
|
|
176
|
+
const opName = parsed.verb ? `${parsed.name} ${parsed.verb}` : parsed.name;
|
|
177
|
+
out.push({
|
|
178
|
+
guid: parsed.guid,
|
|
179
|
+
// The preview buckets every trigger subtype under the generic `trigger`
|
|
180
|
+
// type, so key triggers that way to match (DEV-7084, same as
|
|
181
|
+
// filterChangedEntries).
|
|
182
|
+
key: `${parsed.type.endsWith('_trigger') ? 'trigger' : parsed.type}:${opName}`,
|
|
183
|
+
name: opName,
|
|
184
|
+
relPath: relative(inputDir, filePath),
|
|
185
|
+
type: parsed.type,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Delete operations that would remove something the filter excluded.
|
|
192
|
+
*
|
|
193
|
+
* This is the guard's actual test, and it is deliberately performed against the
|
|
194
|
+
* dry-run the SERVER returned rather than trusting that `protect_guids` was
|
|
195
|
+
* honoured: an older instance ignores the parameter entirely, and a document
|
|
196
|
+
* with no GUID cannot be protected at all. Either way the answer here is the
|
|
197
|
+
* truth about what the push is about to do.
|
|
198
|
+
*/
|
|
199
|
+
export function deletesHittingFilteredOut(operations, filteredOut) {
|
|
200
|
+
if (filteredOut.length === 0)
|
|
201
|
+
return [];
|
|
202
|
+
const byKey = new Map(filteredOut.map((d) => [d.key, d]));
|
|
203
|
+
const hits = [];
|
|
204
|
+
for (const op of operations) {
|
|
205
|
+
if (op.action !== 'delete' && op.action !== 'cascade_delete')
|
|
206
|
+
continue;
|
|
207
|
+
const match = byKey.get(`${op.type}:${op.name}`);
|
|
208
|
+
if (match) {
|
|
209
|
+
hits.push({ name: op.name, relPath: match.relPath, type: op.type });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return hits;
|
|
213
|
+
}
|
|
92
214
|
/**
|
|
93
215
|
* Read .xs files into document entries, skipping empty files.
|
|
94
216
|
*/
|
|
@@ -102,6 +224,58 @@ export function readDocuments(files) {
|
|
|
102
224
|
}
|
|
103
225
|
return entries;
|
|
104
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* A `.xs` document separator: a line that is exactly `---`. The whole push
|
|
229
|
+
* pipeline (partial-diff filtering, the document→file map, and GUID writeback)
|
|
230
|
+
* assumes ONE document per file — it only ever parses the first document in a
|
|
231
|
+
* file and rewrites the first `guid =` line. A multi-document file therefore
|
|
232
|
+
* silently drops on a partial push and corrupts GUIDs on a full one, so it is
|
|
233
|
+
* refused up front (with a pointer to `xano flatten`).
|
|
234
|
+
*/
|
|
235
|
+
const DOC_SEPARATOR = /^---$/m;
|
|
236
|
+
/**
|
|
237
|
+
* Return the entries whose file holds more than one document (a `---`
|
|
238
|
+
* separator). Used to refuse multi-doc files before the push pipeline's
|
|
239
|
+
* single-doc-per-file assumptions can drop or corrupt them.
|
|
240
|
+
*/
|
|
241
|
+
export function findMultiDocEntries(entries) {
|
|
242
|
+
const offenders = [];
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (DOC_SEPARATOR.test(entry.content)) {
|
|
245
|
+
// Count documents = separators + 1 (trailing/empty fragments don't change the "multi" verdict).
|
|
246
|
+
const count = entry.content.split('\n').filter((l) => l.trim() === '---').length + 1;
|
|
247
|
+
offenders.push({ count, filePath: entry.filePath });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return offenders;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Return every `workspace` document in the push set, with the name it declares.
|
|
254
|
+
*
|
|
255
|
+
* A tree must carry at most ONE. The server applies the first workspace
|
|
256
|
+
* document it encounters to the workspace being pushed into and silently
|
|
257
|
+
* discards the rest, with no check that the document actually describes that
|
|
258
|
+
* workspace — so a second one renames the destination to a foreign name while
|
|
259
|
+
* leaving its content untouched, and which name wins flips with the contents of
|
|
260
|
+
* the pushed set.
|
|
261
|
+
*
|
|
262
|
+
* Trees accumulate a second one easily, because `pull` names the file after the
|
|
263
|
+
* workspace's own name (`workspace/{name}.xs`): pulling a different workspace
|
|
264
|
+
* into the same directory ADDS a file rather than overwriting, and renaming a
|
|
265
|
+
* workspace leaves the old-name file behind.
|
|
266
|
+
*/
|
|
267
|
+
export function findWorkspaceEntries(entries) {
|
|
268
|
+
const found = [];
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
const parsed = parseDocument(entry.content);
|
|
271
|
+
// Exact match: `workspace_trigger` is a different document type that also
|
|
272
|
+
// lives under workspace/ and must not be counted here.
|
|
273
|
+
if (parsed && parsed.type === 'workspace') {
|
|
274
|
+
found.push({ filePath: entry.filePath, name: parsed.name });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return found;
|
|
278
|
+
}
|
|
105
279
|
// ── Validation Rendering ────────────────────────────────────────────────────
|
|
106
280
|
export function renderBadReferences(badRefs, log) {
|
|
107
281
|
log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
|
|
@@ -148,7 +322,7 @@ const TYPE_LABELS = {
|
|
|
148
322
|
workflow_test: 'Workflow Tests',
|
|
149
323
|
workspace: 'Workspace Settings',
|
|
150
324
|
};
|
|
151
|
-
function renderPreview(result, willDelete, target, verbose, partial, log) {
|
|
325
|
+
function renderPreview(result, willDelete, target, verbose, partial, log, filteredOutCount = 0) {
|
|
152
326
|
log('');
|
|
153
327
|
log(ux.colorize('bold', `=== Push Preview: ${target.label} ===`));
|
|
154
328
|
let instanceHost = target.instanceOrigin;
|
|
@@ -212,6 +386,17 @@ function renderPreview(result, willDelete, target, verbose, partial, log) {
|
|
|
212
386
|
// Split destructive ops by category
|
|
213
387
|
const deleteOps = destructive.filter((op) => op.action === 'delete' || op.action === 'cascade_delete');
|
|
214
388
|
const alwaysDestructive = destructive.filter((op) => op.action === 'truncate' || op.action === 'drop_field' || op.action === 'alter_field');
|
|
389
|
+
// --include/--exclude and --delete interact in a way the preview cannot show
|
|
390
|
+
// on its own: a filtered-out document is absent from the payload, so without
|
|
391
|
+
// the protection the push declares, the sweep would read it as removed. The
|
|
392
|
+
// objects it defines are spared (and the push is refused outright if they
|
|
393
|
+
// cannot be), but anything ELSE missing from the tree is still deleted below,
|
|
394
|
+
// which is worth saying while a filter is active.
|
|
395
|
+
if (willDelete && filteredOutCount > 0 && deleteOps.length > 0) {
|
|
396
|
+
log('');
|
|
397
|
+
log(ux.colorize('yellow', ` Note: ${filteredOutCount} file(s) were filtered out by --include/--exclude, and --delete is on.`));
|
|
398
|
+
log(ux.colorize('yellow', ' What they define is protected from the sweep; the deletes below are objects missing from the tree entirely.'));
|
|
399
|
+
}
|
|
215
400
|
// Show destructive operations (deletes only when --delete, truncates/drop_field always)
|
|
216
401
|
const shownDestructive = [...(willDelete ? deleteOps : []), ...alwaysDestructive];
|
|
217
402
|
if (shownDestructive.length > 0) {
|
|
@@ -274,6 +459,12 @@ export async function confirm(message) {
|
|
|
274
459
|
});
|
|
275
460
|
}
|
|
276
461
|
// ── GUID Sync ───────────────────────────────────────────────────────────────
|
|
462
|
+
/**
|
|
463
|
+
* Ceiling for the `protect_guids` query parameter. The whole request LINE must
|
|
464
|
+
* fit in one nginx buffer (8k by default), and the URL already carries the rest
|
|
465
|
+
* of the push parameters, so stay well inside it.
|
|
466
|
+
*/
|
|
467
|
+
const PROTECT_GUIDS_MAX_CHARS = 4000;
|
|
277
468
|
const GUID_REGEX = /guid\s*=\s*(["'])([^"']*)\1/;
|
|
278
469
|
/**
|
|
279
470
|
* Sync a GUID into a local .xs file. Returns true if the file was modified.
|
|
@@ -315,6 +506,32 @@ function syncGuidToFile(filePath, guid) {
|
|
|
315
506
|
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
316
507
|
return true;
|
|
317
508
|
}
|
|
509
|
+
const CANONICAL_REGEX = /canonical\s*=\s*(["'])([^"']*)\1/;
|
|
510
|
+
/**
|
|
511
|
+
* Sync a server-assigned `canonical` into a local .xs file. Returns true if the
|
|
512
|
+
* file was modified.
|
|
513
|
+
*
|
|
514
|
+
* Like the GUID, a canonical is minted by the instance and the document can only
|
|
515
|
+
* ASK for one: it is unique across every workspace on the instance, so a tree
|
|
516
|
+
* cloned from another workspace names canonicals that are already taken and the
|
|
517
|
+
* import keeps the existing value instead. Without this writeback the local file
|
|
518
|
+
* kept asking for a value the server kept refusing, so every push reported the
|
|
519
|
+
* same objects as changed forever.
|
|
520
|
+
*
|
|
521
|
+
* Unlike the GUID this NEVER inserts a missing line: only the container kinds
|
|
522
|
+
* (api_group, realtime_server, mcp_server/agent/toolset) have the field, and the
|
|
523
|
+
* server omits the key for everything else, so an absent line means "this kind
|
|
524
|
+
* has no canonical" rather than "not yet assigned".
|
|
525
|
+
*/
|
|
526
|
+
function syncCanonicalToFile(filePath, canonical) {
|
|
527
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
528
|
+
const existingMatch = content.match(CANONICAL_REGEX);
|
|
529
|
+
if (!existingMatch || existingMatch[2] === canonical) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
fs.writeFileSync(filePath, content.replace(CANONICAL_REGEX, `canonical = "${canonical}"`), 'utf8');
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
318
535
|
// ── Knowledge Preview Helpers ─────────────────────────────────────────────────
|
|
319
536
|
/**
|
|
320
537
|
* Compute a knowledge change preview: prefer the server's `dry_run` response,
|
|
@@ -327,7 +544,6 @@ async function computeKnowledgePreview(listUrl, objects, branch, shouldDelete, a
|
|
|
327
544
|
const serverResult = await pushKnowledge(listUrl, accessToken, verboseFetch, verbose, {
|
|
328
545
|
branch,
|
|
329
546
|
delete: shouldDelete,
|
|
330
|
-
// eslint-disable-next-line camelcase -- external Metadata API field name
|
|
331
547
|
dry_run: true,
|
|
332
548
|
items,
|
|
333
549
|
});
|
|
@@ -372,6 +588,8 @@ export async function executePush(ctx, target, flags) {
|
|
|
372
588
|
// ── Collect and filter .xs files ──────────────────────────────────────
|
|
373
589
|
const allFiles = collectFiles(inputDir);
|
|
374
590
|
const files = applyFilters(allFiles, inputDir, flags.include, flags.exclude, log);
|
|
591
|
+
const filteredOut = describeFilteredOut(allFiles, files, inputDir);
|
|
592
|
+
const filteredOutCount = allFiles.length - files.length;
|
|
375
593
|
const knowledgeOnly = files.length === 0 && (knowledgeObjects.length > 0 || ctx.knowledge !== undefined);
|
|
376
594
|
if (files.length === 0 && !knowledgeOnly) {
|
|
377
595
|
command.error(flags.include || flags.exclude
|
|
@@ -387,6 +605,91 @@ export async function executePush(ctx, target, flags) {
|
|
|
387
605
|
if (documentEntries.length === 0) {
|
|
388
606
|
command.error(`All .xs files in ${inputDir} are empty`);
|
|
389
607
|
}
|
|
608
|
+
// ── Handle multi-document files ───────────────────────────────────────
|
|
609
|
+
// The push pipeline assumes one document per file: the partial-diff filter
|
|
610
|
+
// and GUID-writeback both parse only the first document in a file, so a
|
|
611
|
+
// bundle like a single hand-authored multidoc.xs silently pushes nothing
|
|
612
|
+
// (partial) or clobbers a single guid line across every document (full).
|
|
613
|
+
// Rather than fail, offer to flatten each bundle in place into the
|
|
614
|
+
// per-document layout `pull` produces, then re-read and continue the push.
|
|
615
|
+
const multiDocFiles = findMultiDocEntries(documentEntries);
|
|
616
|
+
if (multiDocFiles.length > 0) {
|
|
617
|
+
const list = multiDocFiles
|
|
618
|
+
.map((f) => ` ${relative(inputDir, f.filePath) || f.filePath} (${f.count} documents)`)
|
|
619
|
+
.join('\n');
|
|
620
|
+
log('');
|
|
621
|
+
log(ux.colorize('yellow', ux.colorize('bold', '=== Multi-document file(s) detected ===')));
|
|
622
|
+
log('');
|
|
623
|
+
log(ux.colorize('yellow', 'These .xs files hold more than one document:'));
|
|
624
|
+
log(list);
|
|
625
|
+
log('');
|
|
626
|
+
log(ux.colorize('dim', 'Push needs one document per file (partial diff + GUID writeback operate per file),\n' +
|
|
627
|
+
'so a multi-doc bundle silently pushes nothing or corrupts GUIDs. I can split each\n' +
|
|
628
|
+
'into the standard per-document layout (the same tree `pull` produces) and continue.'));
|
|
629
|
+
log('');
|
|
630
|
+
// Flattening DELETES the source bundle, so never do it unattended without an
|
|
631
|
+
// explicit opt-in. --force means "just do it"; an interactive TTY gets a
|
|
632
|
+
// prompt; a bare non-interactive run refuses (rather than silently rewriting
|
|
633
|
+
// and deleting the user's files in CI).
|
|
634
|
+
if (flags.force) {
|
|
635
|
+
log(ux.colorize('dim', 'Flattening (--force)…'));
|
|
636
|
+
}
|
|
637
|
+
else if (process.stdin.isTTY) {
|
|
638
|
+
const doFlatten = await confirm('Flatten these file(s) in place and continue the push?');
|
|
639
|
+
if (!doFlatten) {
|
|
640
|
+
log('Push cancelled. Run `xano flatten <file>` yourself, or re-run to be prompted again.');
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
command.error('Multi-document .xs file(s) cannot be pushed. Run `xano flatten <file>` to split them ' +
|
|
646
|
+
'into the standard per-document layout first, or re-run with --force to flatten automatically.');
|
|
647
|
+
}
|
|
648
|
+
for (const offender of multiDocFiles) {
|
|
649
|
+
try {
|
|
650
|
+
const result = flattenBundleFile(offender.filePath, { log: (m) => flags.verbose && log(ux.colorize('dim', m)) });
|
|
651
|
+
log(ux.colorize('dim', ` flattened ${relative(inputDir, offender.filePath) || offender.filePath} → ${result.written.length} files`));
|
|
652
|
+
}
|
|
653
|
+
catch (error) {
|
|
654
|
+
command.error(`Failed to flatten ${offender.filePath}: ${error.message}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
log('');
|
|
658
|
+
// Re-collect and re-read: the bundle files are gone, replaced by the split
|
|
659
|
+
// per-document tree, so the rest of the push sees a clean one-doc-per-file set.
|
|
660
|
+
const refreshed = readDocuments(applyFilters(collectFiles(inputDir), inputDir, flags.include, flags.exclude, () => { }));
|
|
661
|
+
documentEntries = refreshed;
|
|
662
|
+
if (documentEntries.length === 0) {
|
|
663
|
+
command.error('After flattening, no .xs documents remain to push.');
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// ── Refuse a tree carrying more than one workspace document ───────────
|
|
667
|
+
// The server applies the FIRST workspace document it sees to the workspace
|
|
668
|
+
// being pushed into and silently drops the rest, with no check that the
|
|
669
|
+
// document describes that workspace. A second one therefore renames the
|
|
670
|
+
// destination to a foreign name while leaving all of its content intact,
|
|
671
|
+
// and which name wins flips with whatever happens to be in the pushed set.
|
|
672
|
+
//
|
|
673
|
+
// Checked against the WHOLE tree rather than the partial changed-set, because
|
|
674
|
+
// the flip-flop case is exactly the one where only the foreign document is
|
|
675
|
+
// "changed" and would be the sole workspace document actually sent.
|
|
676
|
+
//
|
|
677
|
+
// No --force escape and no auto-fix: unlike the multi-doc bundle above there
|
|
678
|
+
// is no safe repair, since only the user knows which workspace this tree is
|
|
679
|
+
// meant to be. Picking one is the bug.
|
|
680
|
+
const workspaceDocs = findWorkspaceEntries(documentEntries);
|
|
681
|
+
if (workspaceDocs.length > 1) {
|
|
682
|
+
const list = workspaceDocs
|
|
683
|
+
.map((w) => ` ${relative(inputDir, w.filePath) || w.filePath} → workspace "${w.name}"`)
|
|
684
|
+
.join('\n');
|
|
685
|
+
command.error(`${inputDir} holds ${workspaceDocs.length} workspace documents:\n${list}\n\n` +
|
|
686
|
+
'A push targets one workspace, and the server applies only the first of these and ' +
|
|
687
|
+
'silently discards the rest — renaming the destination to a foreign name while leaving ' +
|
|
688
|
+
'its content untouched.\n\n' +
|
|
689
|
+
'Delete the ones that do not describe the workspace you are pushing to, keeping a single ' +
|
|
690
|
+
'workspace/*.xs. (`pull` names this file after the workspace, so pulling a different ' +
|
|
691
|
+
'workspace into this tree, or renaming one, leaves the old file behind.)');
|
|
692
|
+
}
|
|
390
693
|
multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
391
694
|
// ── Build document key → file path map (for GUID writeback) ─────────
|
|
392
695
|
for (const entry of documentEntries) {
|
|
@@ -420,6 +723,17 @@ export async function executePush(ctx, target, flags) {
|
|
|
420
723
|
if (target.sourceWorkspaceId) {
|
|
421
724
|
queryParams.set('source_workspace_id', target.sourceWorkspaceId);
|
|
422
725
|
}
|
|
726
|
+
// Tell the server which objects the filter put out of scope, so its delete
|
|
727
|
+
// sweep spares them. Only meaningful with --delete; without it nothing is
|
|
728
|
+
// swept. The parameter travels in the query string (the body is the multidoc
|
|
729
|
+
// itself), so it has a length ceiling: past PROTECT_GUIDS_MAX_CHARS we send
|
|
730
|
+
// nothing and let the verification below refuse the push rather than issue a
|
|
731
|
+
// request the server may truncate or reject.
|
|
732
|
+
const protectableGuids = filteredOut.map((d) => d.guid).filter((g) => typeof g === 'string' && g.length > 0);
|
|
733
|
+
const protectGuidsParam = protectableGuids.join(',');
|
|
734
|
+
if (shouldDelete && protectGuidsParam && protectGuidsParam.length <= PROTECT_GUIDS_MAX_CHARS) {
|
|
735
|
+
queryParams.set('protect_guids', protectGuidsParam);
|
|
736
|
+
}
|
|
423
737
|
// ── Request headers ───────────────────────────────────────────────────
|
|
424
738
|
const requestHeaders = {
|
|
425
739
|
accept: 'application/json',
|
|
@@ -451,7 +765,46 @@ export async function executePush(ctx, target, flags) {
|
|
|
451
765
|
const knowledgeDryRun = await computeKnowledgePreview(ctx.knowledge.listUrl(), knowledgeObjects, ctx.branch, shouldDelete, accessToken, verboseFetch, flags.verbose);
|
|
452
766
|
mergeKnowledgePreview(preview, knowledgeDryRun);
|
|
453
767
|
}
|
|
454
|
-
renderPreview(preview, shouldDelete, target, flags.verbose, isPartial, log);
|
|
768
|
+
renderPreview(preview, shouldDelete, target, flags.verbose, isPartial, log, filteredOutCount);
|
|
769
|
+
// GUARD: --include/--exclude must never cost you the objects it
|
|
770
|
+
// filtered out.
|
|
771
|
+
//
|
|
772
|
+
// A filtered-out document is absent from the payload, so a --delete
|
|
773
|
+
// sweep reads it as removed from the tree and deletes it - the exact
|
|
774
|
+
// opposite of what -e means to the person typing it. The server is
|
|
775
|
+
// told to spare them (protect_guids above), but this checks the
|
|
776
|
+
// SERVER'S OWN preview rather than assuming that worked: an older
|
|
777
|
+
// instance ignores the parameter, a document that has never been
|
|
778
|
+
// pushed has no GUID to protect, and a very large filter exceeds what
|
|
779
|
+
// the query string can carry. In every one of those cases the push is
|
|
780
|
+
// refused rather than allowed to delete.
|
|
781
|
+
if (shouldDelete) {
|
|
782
|
+
const endangered = deletesHittingFilteredOut(preview.operations, filteredOut);
|
|
783
|
+
if (endangered.length > 0) {
|
|
784
|
+
log('');
|
|
785
|
+
log(ux.colorize('red', '--- Refusing to push ---'));
|
|
786
|
+
log('');
|
|
787
|
+
log(` --delete would remove ${endangered.length} object(s) that --include/--exclude filtered out:`);
|
|
788
|
+
log('');
|
|
789
|
+
for (const hit of endangered.slice(0, 10)) {
|
|
790
|
+
log(` ${ux.colorize('red', 'DELETE'.padEnd(8))} ${hit.type.padEnd(18)} ${hit.name} ${ux.colorize('dim', `(${hit.relPath})`)}`);
|
|
791
|
+
}
|
|
792
|
+
if (endangered.length > 10) {
|
|
793
|
+
log(ux.colorize('dim', ` ... and ${endangered.length - 10} more`));
|
|
794
|
+
}
|
|
795
|
+
log('');
|
|
796
|
+
const unprotectable = filteredOut.filter((d) => !d.guid).length;
|
|
797
|
+
if (unprotectable > 0) {
|
|
798
|
+
log(ux.colorize('dim', ` ${unprotectable} filtered-out document(s) have no guid yet, so the server cannot be told to spare them.`));
|
|
799
|
+
}
|
|
800
|
+
if (protectGuidsParam.length > PROTECT_GUIDS_MAX_CHARS) {
|
|
801
|
+
log(ux.colorize('dim', ' Too many filtered-out documents to declare in one request.'));
|
|
802
|
+
}
|
|
803
|
+
log(ux.colorize('dim', ' This instance may also predate protected pushes.'));
|
|
804
|
+
log('');
|
|
805
|
+
command.error('Re-run without --delete, or without the --include/--exclude filter.');
|
|
806
|
+
}
|
|
807
|
+
}
|
|
455
808
|
// Check for bad cross-references using dry-run operations to avoid false positives
|
|
456
809
|
const badRefs = checkReferences(documentEntries, preview.operations);
|
|
457
810
|
if (badRefs.length > 0) {
|
|
@@ -639,12 +992,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
639
992
|
log('No changes to push.');
|
|
640
993
|
return;
|
|
641
994
|
}
|
|
642
|
-
|
|
643
|
-
multidoc = filteredEntries.map((d) => d.content).join('\n---\n');
|
|
644
|
-
}
|
|
645
|
-
else {
|
|
646
|
-
multidoc = '';
|
|
647
|
-
}
|
|
995
|
+
multidoc = filteredEntries.length > 0 ? filteredEntries.map((d) => d.content).join('\n---\n') : '';
|
|
648
996
|
}
|
|
649
997
|
// ── Execute the actual push ───────────────────────────────────────────
|
|
650
998
|
const startTime = Date.now();
|
|
@@ -689,6 +1037,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
689
1037
|
}
|
|
690
1038
|
}
|
|
691
1039
|
let updatedCount = 0;
|
|
1040
|
+
let canonicalCount = 0;
|
|
692
1041
|
for (const entry of guidMap) {
|
|
693
1042
|
if (!entry.guid)
|
|
694
1043
|
continue;
|
|
@@ -715,10 +1064,23 @@ export async function executePush(ctx, target, flags) {
|
|
|
715
1064
|
catch (error) {
|
|
716
1065
|
command.warn(`Failed to sync GUID to ${filePath}: ${error.message}`);
|
|
717
1066
|
}
|
|
1067
|
+
if (entry.canonical) {
|
|
1068
|
+
try {
|
|
1069
|
+
const updated = syncCanonicalToFile(filePath, entry.canonical);
|
|
1070
|
+
if (updated)
|
|
1071
|
+
canonicalCount++;
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
command.warn(`Failed to sync canonical to ${filePath}: ${error.message}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
718
1077
|
}
|
|
719
1078
|
if (updatedCount > 0) {
|
|
720
1079
|
log(`Synced ${updatedCount} GUIDs to local files`);
|
|
721
1080
|
}
|
|
1081
|
+
if (canonicalCount > 0) {
|
|
1082
|
+
log(`Synced ${canonicalCount} canonicals to local files (the instance assigned its own)`);
|
|
1083
|
+
}
|
|
722
1084
|
}
|
|
723
1085
|
pushedDocCount = multidoc.split('\n---\n').length;
|
|
724
1086
|
}
|
|
@@ -820,6 +1182,7 @@ export function describeNetworkError(error, url, elapsedMs) {
|
|
|
820
1182
|
ETIMEDOUT: `Connection to ${host} timed out. Check your network or VPN, then retry.`,
|
|
821
1183
|
UND_ERR_CONNECT_TIMEOUT: `Connection to ${host} timed out. Check your network or VPN, then retry.`,
|
|
822
1184
|
UND_ERR_HEADERS_TIMEOUT: `${host} accepted the connection but did not respond in time. The push may be too large; try splitting it or retrying.`,
|
|
1185
|
+
UND_ERR_INVALID_ARG: `Your Node runtime rejected the CLI's HTTP dispatcher. Update the CLI, or run it under Node 24 (see DEV-7773).`,
|
|
823
1186
|
};
|
|
824
1187
|
let base;
|
|
825
1188
|
if (code && hints[code]) {
|
package/dist/utils/paging.d.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* so the item count IS the total.
|
|
17
17
|
*
|
|
18
18
|
* There is deliberately no tier for endpoints that page but return a bare array
|
|
19
|
-
* with no metadata (tenants, releases, platforms, clusters, tenant
|
|
19
|
+
* with no metadata (tenants, releases, platforms, clusters, ephemerals, tenant
|
|
20
|
+
* backups). Those commands are left alone: the only has-more signal available
|
|
20
21
|
* would be "the page came back full", which is a guaranteed false positive when
|
|
21
22
|
* the result set is an exact multiple of the page size — sending the user to an
|
|
22
23
|
* empty page. Better to offer nothing than to offer a lie.
|
package/dist/utils/paging.js
CHANGED