@xano/cli 1.3.2 → 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.
- package/README.md +72 -1
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +16 -2
- package/dist/commands/sandbox/delete/index.js +8 -0
- package/dist/commands/sandbox/pull/index.d.ts +7 -0
- package/dist/commands/sandbox/pull/index.js +49 -0
- package/dist/commands/sandbox/push/index.d.ts +4 -0
- package/dist/commands/sandbox/push/index.js +53 -1
- package/dist/commands/sandbox/reset/index.js +8 -0
- package/dist/commands/workspace/pull/index.d.ts +7 -0
- package/dist/commands/workspace/pull/index.js +54 -0
- package/dist/commands/workspace/push/index.d.ts +4 -0
- package/dist/commands/workspace/push/index.js +54 -1
- package/dist/utils/export-compare.d.ts +52 -0
- package/dist/utils/export-compare.js +134 -0
- package/dist/utils/local-config.d.ts +23 -0
- package/dist/utils/local-config.js +62 -2
- package/dist/utils/multidoc-push.d.ts +67 -2
- package/dist/utils/multidoc-push.js +577 -25
- package/dist/utils/object-fetch.d.ts +86 -0
- package/dist/utils/object-fetch.js +265 -0
- package/dist/utils/reference-checker.d.ts +3 -0
- package/dist/utils/reference-checker.js +3 -2
- package/dist/utils/sandbox-marker.d.ts +9 -0
- package/dist/utils/sandbox-marker.js +119 -0
- package/dist/utils/sync-state.d.ts +71 -0
- package/dist/utils/sync-state.js +353 -0
- package/oclif.manifest.json +2485 -2393
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
3
|
+
import { basename, relative, resolve } from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../base-command.js';
|
|
5
|
+
import { formatWorkspacePushDefaultsBanner, resolvePushDefaults } from '../../../utils/local-config.js';
|
|
5
6
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
6
7
|
export default class Push extends BaseCommand {
|
|
7
8
|
static description = '[IMPORTANT] ALWAYS run --dry-run first and show the user the output before pushing. Push local documents to a workspace. By default, only changed files are pushed (partial mode). Use --sync to push all files. Shows a preview of changes before pushing unless --force is specified. Use --dry-run to preview only.';
|
|
@@ -23,6 +24,18 @@ Preview changes without pushing
|
|
|
23
24
|
`,
|
|
24
25
|
`$ xano workspace push --force
|
|
25
26
|
Skip preview and push immediately (for CI/CD)
|
|
27
|
+
`,
|
|
28
|
+
`$ xano workspace push --fast
|
|
29
|
+
Push only files that changed since the last sync, and write .xano/sync_state.json
|
|
30
|
+
`,
|
|
31
|
+
`$ xano workspace push --fast --refresh
|
|
32
|
+
Re-compare local files against the server export, then push changes
|
|
33
|
+
`,
|
|
34
|
+
`$ xano workspace push --no-fast
|
|
35
|
+
Push without --fast even when profile.yaml sets fast: true
|
|
36
|
+
`,
|
|
37
|
+
`$ xano workspace push --writeback
|
|
38
|
+
Push and write server-formatted XanoScript back to local files
|
|
26
39
|
`,
|
|
27
40
|
`$ xano workspace push -d ./output -w 40
|
|
28
41
|
Pushed 15 documents from ./output
|
|
@@ -95,6 +108,11 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
95
108
|
multiple: true,
|
|
96
109
|
required: false,
|
|
97
110
|
}),
|
|
111
|
+
fast: Flags.boolean({
|
|
112
|
+
allowNo: true,
|
|
113
|
+
description: 'Skip files whose contents match .xano/sync_state.json, and write or update that record. Without a record, compares against the server export once. Can be set as `fast: true` in profile.yaml.',
|
|
114
|
+
required: false,
|
|
115
|
+
}),
|
|
98
116
|
force: Flags.boolean({
|
|
99
117
|
default: false,
|
|
100
118
|
description: '[CRITICAL] NEVER run without explicit user confirmation. Skips preview and confirmation prompt (for CI/CD pipelines).',
|
|
@@ -117,6 +135,11 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
117
135
|
description: '[CRITICAL] STOP and ALWAYS run --dry-run first to show the user a preview before pushing live table records. Includes table records in import.',
|
|
118
136
|
required: false,
|
|
119
137
|
}),
|
|
138
|
+
refresh: Flags.boolean({
|
|
139
|
+
default: false,
|
|
140
|
+
description: 'With --fast, re-compare local files against the server export even when a sync record exists',
|
|
141
|
+
required: false,
|
|
142
|
+
}),
|
|
120
143
|
sync: Flags.boolean({
|
|
121
144
|
default: false,
|
|
122
145
|
description: 'Full push — send all files, not just changed ones. Required for --delete.',
|
|
@@ -138,6 +161,15 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
138
161
|
description: 'Workspace ID (optional if set in profile)',
|
|
139
162
|
required: false,
|
|
140
163
|
}),
|
|
164
|
+
writeback: Flags.boolean({
|
|
165
|
+
allowNo: true,
|
|
166
|
+
description: 'After a successful push, write server-formatted XanoScript back to the files that were pushed. Can be set as `writeback: true` in profile.yaml.',
|
|
167
|
+
required: false,
|
|
168
|
+
}),
|
|
169
|
+
'writeback-diff': Flags.string({
|
|
170
|
+
description: 'With --writeback, save local vs server copies of rewritten files under this directory',
|
|
171
|
+
required: false,
|
|
172
|
+
}),
|
|
141
173
|
};
|
|
142
174
|
async run() {
|
|
143
175
|
const { flags } = await this.parse(Push);
|
|
@@ -164,6 +196,17 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
164
196
|
}
|
|
165
197
|
const branch = flags.branch || profile.branch || '';
|
|
166
198
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}`;
|
|
199
|
+
const pushDefaults = resolvePushDefaults({
|
|
200
|
+
flagFast: flags.fast,
|
|
201
|
+
flagWriteback: flags.writeback,
|
|
202
|
+
localFast: this.localProfile?.config.fast,
|
|
203
|
+
localWriteback: this.localProfile?.config.writeback,
|
|
204
|
+
sync: flags.sync,
|
|
205
|
+
});
|
|
206
|
+
if (pushDefaults.fromLocal.length > 0 && this.localProfile) {
|
|
207
|
+
const relativePath = relative(process.cwd(), this.localProfile.path) || basename(this.localProfile.path);
|
|
208
|
+
this.log(formatWorkspacePushDefaultsBanner(pushDefaults.fromLocal, relativePath));
|
|
209
|
+
}
|
|
167
210
|
const target = {
|
|
168
211
|
buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
|
|
169
212
|
buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
|
|
@@ -178,14 +221,19 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
178
221
|
'dry-run': flags['dry-run'],
|
|
179
222
|
env: flags.env,
|
|
180
223
|
exclude: flags.exclude,
|
|
224
|
+
fast: pushDefaults.fast,
|
|
181
225
|
force: flags.force,
|
|
182
226
|
guids: flags.guids,
|
|
183
227
|
include: flags.include,
|
|
184
228
|
records: flags.records,
|
|
229
|
+
refresh: flags.refresh,
|
|
185
230
|
sync: flags.sync,
|
|
186
231
|
transaction: flags.transaction,
|
|
187
232
|
truncate: flags.truncate,
|
|
188
233
|
verbose: flags.verbose,
|
|
234
|
+
writeback: pushDefaults.writeback,
|
|
235
|
+
writebackDiff: flags['writeback-diff'],
|
|
236
|
+
writebackObjectLimit: this.localProfile?.config.writeback_object_limit,
|
|
189
237
|
};
|
|
190
238
|
await executePush({
|
|
191
239
|
accessToken: profile.access_token,
|
|
@@ -196,6 +244,11 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
196
244
|
listUrl: () => `${baseUrl}/knowledge/sync`,
|
|
197
245
|
rootDir: inputDir,
|
|
198
246
|
},
|
|
247
|
+
syncTarget: {
|
|
248
|
+
instanceOrigin: profile.instance_origin,
|
|
249
|
+
kind: 'workspace',
|
|
250
|
+
workspace: workspaceId,
|
|
251
|
+
},
|
|
199
252
|
verboseFetch: this.verboseFetch.bind(this),
|
|
200
253
|
}, target, pushFlags);
|
|
201
254
|
}
|
|
@@ -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
|
|
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
|
|
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';
|