@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,86 @@
1
+ import { type SyncFileEntry, type SyncTargetIdentity, type SyncTargetState } from './sync-state.js';
2
+ export declare const PER_OBJECT_FETCH_LIMIT = 25;
3
+ export declare const PER_OBJECT_CONCURRENCY = 6;
4
+ export type ObjectType = 'addon' | 'agent' | 'function' | 'query' | 'table' | 'task' | 'tool';
5
+ export interface TypeSpec {
6
+ hasBranchField: boolean;
7
+ listPath: (workspaceId: string, extra?: {
8
+ groupId?: number;
9
+ }) => string;
10
+ objectPath: (workspaceId: string, id: number, extra?: {
11
+ groupId?: number;
12
+ }) => string;
13
+ }
14
+ export declare const OBJECT_TYPE_SPECS: Record<ObjectType, TypeSpec>;
15
+ export declare function isObjectType(type: string): type is ObjectType;
16
+ export declare function typesWithoutBranchField(types: string[]): string[];
17
+ /**
18
+ * Insert a guid line before the final `}`, matching `syncGuidToFile`.
19
+ * A blank line is inserted before it when the previous non-empty line is a
20
+ * closing `}` or `]` (the multidoc export's layout, measured in Phase 0b).
21
+ */
22
+ export declare function insertGuidLine(content: string, guid: string): string;
23
+ export declare function restoreGuidFromLocal(remoteContent: string, localContent: string): string;
24
+ export declare function readXanoscript(body: unknown): {
25
+ branch?: string;
26
+ status: string;
27
+ text: string;
28
+ } | {
29
+ error: string;
30
+ };
31
+ export interface ResolvedObjectId {
32
+ groupId?: number;
33
+ id: number;
34
+ }
35
+ export type ObjectGet = (pathname: string, query?: Record<string, string>) => Promise<{
36
+ json: unknown;
37
+ ok: boolean;
38
+ status: number;
39
+ }>;
40
+ export declare function lookupObjectId(get: ObjectGet, workspaceId: string, parsed: {
41
+ apiGroup?: string;
42
+ name: string;
43
+ type: string;
44
+ verb?: string;
45
+ }): Promise<null | ResolvedObjectId>;
46
+ export interface PushedObject {
47
+ content: string;
48
+ filePath: string;
49
+ relPath: string;
50
+ }
51
+ export interface FetchFormattedResult {
52
+ content: string;
53
+ filePath: string;
54
+ id: number;
55
+ relPath: string;
56
+ }
57
+ export type FetchFormattedFailure = {
58
+ error: string;
59
+ filePath: string;
60
+ };
61
+ export declare function canUsePerObjectFetch(args: {
62
+ isLiveBranch: boolean;
63
+ limit?: number;
64
+ pushed: PushedObject[];
65
+ }): {
66
+ ok: false;
67
+ reason: string;
68
+ } | {
69
+ ok: true;
70
+ };
71
+ export declare function fetchFormattedObjects(args: {
72
+ get: ObjectGet;
73
+ isLiveBranch: boolean;
74
+ limit?: number;
75
+ pushed: PushedObject[];
76
+ state: null | SyncTargetState;
77
+ target: SyncTargetIdentity;
78
+ workspaceId: string;
79
+ }): Promise<{
80
+ ids: Record<string, SyncFileEntry>;
81
+ ok: true;
82
+ results: FetchFormattedResult[];
83
+ } | {
84
+ ok: false;
85
+ reason: string;
86
+ }>;
@@ -0,0 +1,265 @@
1
+ import { mapWithConcurrency } from './concurrency.js';
2
+ import { buildDocumentKey, parseDocument } from './document-parser.js';
3
+ import { normalizeListResponse } from './paging.js';
4
+ export const PER_OBJECT_FETCH_LIMIT = 25;
5
+ export const PER_OBJECT_CONCURRENCY = 6;
6
+ export const OBJECT_TYPE_SPECS = {
7
+ addon: {
8
+ hasBranchField: true,
9
+ listPath: (workspaceId) => `/workspace/${workspaceId}/addon`,
10
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/addon/${id}`,
11
+ },
12
+ agent: {
13
+ hasBranchField: false,
14
+ listPath: (workspaceId) => `/workspace/${workspaceId}/agent`,
15
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/agent/${id}`,
16
+ },
17
+ function: {
18
+ hasBranchField: true,
19
+ listPath: (workspaceId) => `/workspace/${workspaceId}/function`,
20
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/function/${id}`,
21
+ },
22
+ query: {
23
+ hasBranchField: false,
24
+ listPath: (workspaceId, extra) => extra?.groupId === undefined
25
+ ? `/workspace/${workspaceId}/apigroup`
26
+ : `/workspace/${workspaceId}/apigroup/${extra.groupId}/api`,
27
+ objectPath: (workspaceId, id, extra) => `/workspace/${workspaceId}/apigroup/${extra?.groupId}/api/${id}`,
28
+ },
29
+ table: {
30
+ hasBranchField: false,
31
+ listPath: (workspaceId) => `/workspace/${workspaceId}/table`,
32
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/table/${id}`,
33
+ },
34
+ task: {
35
+ hasBranchField: true,
36
+ listPath: (workspaceId) => `/workspace/${workspaceId}/task`,
37
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/task/${id}`,
38
+ },
39
+ tool: {
40
+ hasBranchField: true,
41
+ listPath: (workspaceId) => `/workspace/${workspaceId}/tool`,
42
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/tool/${id}`,
43
+ },
44
+ };
45
+ const GUID_REGEX = /guid\s*=\s*(["'])([^"']*)\1/;
46
+ export function isObjectType(type) {
47
+ return type in OBJECT_TYPE_SPECS;
48
+ }
49
+ export function typesWithoutBranchField(types) {
50
+ return types.filter((type) => isObjectType(type) && !OBJECT_TYPE_SPECS[type].hasBranchField);
51
+ }
52
+ /**
53
+ * Insert a guid line before the final `}`, matching `syncGuidToFile`.
54
+ * A blank line is inserted before it when the previous non-empty line is a
55
+ * closing `}` or `]` (the multidoc export's layout, measured in Phase 0b).
56
+ */
57
+ export function insertGuidLine(content, guid) {
58
+ if (GUID_REGEX.test(content)) {
59
+ return content.replace(GUID_REGEX, `guid = "${guid}"`);
60
+ }
61
+ const lines = content.split('\n');
62
+ let insertIndex = -1;
63
+ for (let i = lines.length - 1; i >= 0; i--) {
64
+ if (lines[i].trim() === '}') {
65
+ insertIndex = i;
66
+ break;
67
+ }
68
+ }
69
+ if (insertIndex === -1)
70
+ return content;
71
+ let indent = ' ';
72
+ let previousNonEmpty = '';
73
+ for (let i = insertIndex - 1; i >= 0; i--) {
74
+ if (lines[i].trim()) {
75
+ previousNonEmpty = lines[i].trim();
76
+ const indentMatch = lines[i].match(/^(\s+)/);
77
+ if (indentMatch)
78
+ indent = indentMatch[1];
79
+ break;
80
+ }
81
+ }
82
+ const guidLine = `${indent}guid = "${guid}"`;
83
+ if (previousNonEmpty === '}' || previousNonEmpty === ']') {
84
+ lines.splice(insertIndex, 0, '', guidLine);
85
+ }
86
+ else {
87
+ lines.splice(insertIndex, 0, guidLine);
88
+ }
89
+ return lines.join('\n');
90
+ }
91
+ export function restoreGuidFromLocal(remoteContent, localContent) {
92
+ const localGuid = localContent.match(GUID_REGEX);
93
+ if (!localGuid)
94
+ return remoteContent;
95
+ if (GUID_REGEX.test(remoteContent))
96
+ return remoteContent;
97
+ return insertGuidLine(remoteContent, localGuid[2]);
98
+ }
99
+ export function readXanoscript(body) {
100
+ if (!body || typeof body !== 'object')
101
+ return { error: 'response is not an object' };
102
+ const record = body;
103
+ const xs = record.xanoscript;
104
+ if (!xs || typeof xs !== 'object')
105
+ return { error: 'xanoscript is missing' };
106
+ const script = xs;
107
+ if (script.status !== 'ok' || typeof script.value !== 'string') {
108
+ return { error: `xanoscript.status is ${String(script.status)}` };
109
+ }
110
+ return {
111
+ branch: typeof record.branch === 'string' ? record.branch : undefined,
112
+ status: 'ok',
113
+ text: script.value,
114
+ };
115
+ }
116
+ function itemsOf(json) {
117
+ try {
118
+ return normalizeListResponse(json, [
119
+ 'functions',
120
+ 'tables',
121
+ 'tasks',
122
+ 'agents',
123
+ 'tools',
124
+ 'addons',
125
+ 'apigroups',
126
+ 'apis',
127
+ ]).items.filter((item) => typeof item === 'object' && item !== null);
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ }
133
+ async function searchExact(get, pathname, name, extraMatch) {
134
+ const response = await get(pathname, { include_draft: 'false', page: '1', per_page: '50', search: name });
135
+ if (!response.ok)
136
+ return null;
137
+ const matches = itemsOf(response.json).filter((item) => item.name === name && (extraMatch ? extraMatch(item) : true));
138
+ if (matches.length !== 1 || typeof matches[0].id !== 'number')
139
+ return null;
140
+ return matches[0];
141
+ }
142
+ export async function lookupObjectId(get, workspaceId, parsed) {
143
+ if (!isObjectType(parsed.type))
144
+ return null;
145
+ const spec = OBJECT_TYPE_SPECS[parsed.type];
146
+ if (parsed.type === 'query') {
147
+ if (!parsed.apiGroup)
148
+ return null;
149
+ const group = await searchExact(get, spec.listPath(workspaceId), parsed.apiGroup);
150
+ if (!group || typeof group.id !== 'number')
151
+ return null;
152
+ const verb = parsed.verb ? String(parsed.verb).toUpperCase() : undefined;
153
+ const api = await searchExact(get, spec.listPath(workspaceId, { groupId: group.id }), parsed.name, (item) => verb ? String(item.verb ?? '').toUpperCase() === verb : true);
154
+ if (!api || typeof api.id !== 'number')
155
+ return null;
156
+ return { groupId: group.id, id: api.id };
157
+ }
158
+ const found = await searchExact(get, spec.listPath(workspaceId), parsed.name);
159
+ if (!found || typeof found.id !== 'number')
160
+ return null;
161
+ return { id: found.id };
162
+ }
163
+ export function canUsePerObjectFetch(args) {
164
+ const limit = args.limit ?? PER_OBJECT_FETCH_LIMIT;
165
+ if (args.pushed.length === 0)
166
+ return { ok: false, reason: 'no pushed documents' };
167
+ if (args.pushed.length > limit) {
168
+ return { ok: false, reason: `push has ${args.pushed.length} documents (limit ${limit})` };
169
+ }
170
+ const types = new Set();
171
+ for (const entry of args.pushed) {
172
+ const parsed = parseDocument(entry.content);
173
+ if (!parsed || !isObjectType(parsed.type)) {
174
+ return { ok: false, reason: `type ${parsed?.type ?? 'unknown'} is not in the per-object table` };
175
+ }
176
+ types.add(parsed.type);
177
+ }
178
+ if (!args.isLiveBranch) {
179
+ const missing = typesWithoutBranchField([...types]);
180
+ if (missing.length > 0) {
181
+ return { ok: false, reason: `types without a branch field on a non-live branch: ${missing.join(', ')}` };
182
+ }
183
+ }
184
+ return { ok: true };
185
+ }
186
+ export async function fetchFormattedObjects(args) {
187
+ const gate = canUsePerObjectFetch({ isLiveBranch: args.isLiveBranch, limit: args.limit, pushed: args.pushed });
188
+ if (!gate.ok)
189
+ return gate;
190
+ const ids = {};
191
+ const resolved = [];
192
+ for (const entry of args.pushed) {
193
+ const parsed = parseDocument(entry.content);
194
+ if (!parsed || !isObjectType(parsed.type)) {
195
+ return { ok: false, reason: `unparseable document ${entry.relPath}` };
196
+ }
197
+ const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
198
+ const recorded = args.state?.files[entry.relPath];
199
+ let id;
200
+ let groupId;
201
+ const cachedId = recorded?.id !== undefined && recorded.key === key ? recorded.id : undefined;
202
+ if (parsed.type === 'query' || cachedId === undefined) {
203
+ // Sequential: stop on the first miss instead of firing every lookup.
204
+ // eslint-disable-next-line no-await-in-loop
205
+ const lookedUp = await lookupObjectId(args.get, args.workspaceId, parsed);
206
+ if (!lookedUp)
207
+ return { ok: false, reason: `could not look up id for ${entry.relPath}` };
208
+ id = cachedId ?? lookedUp.id;
209
+ groupId = lookedUp.groupId;
210
+ }
211
+ else {
212
+ id = cachedId;
213
+ }
214
+ resolved.push({ ...entry, groupId, id, key });
215
+ }
216
+ const fetched = await mapWithConcurrency(resolved, PER_OBJECT_CONCURRENCY, async (entry) => {
217
+ const parsed = parseDocument(entry.content);
218
+ const spec = OBJECT_TYPE_SPECS[parsed.type];
219
+ const pathname = spec.objectPath(args.workspaceId, entry.id, { groupId: entry.groupId });
220
+ const response = await args.get(pathname, { include_draft: 'false', include_xanoscript: 'true' });
221
+ if (!response.ok) {
222
+ return { error: `HTTP ${response.status} for ${entry.relPath}`, filePath: entry.filePath };
223
+ }
224
+ const xs = readXanoscript(response.json);
225
+ if ('error' in xs) {
226
+ return { error: `${entry.relPath}: ${xs.error}`, filePath: entry.filePath };
227
+ }
228
+ if (spec.hasBranchField && xs.branch !== args.target.branch) {
229
+ return {
230
+ error: `${entry.relPath}: branch ${xs.branch ?? '(missing)'} != ${args.target.branch}`,
231
+ filePath: entry.filePath,
232
+ };
233
+ }
234
+ const restored = restoreGuidFromLocal(xs.text, entry.content);
235
+ const restoredParsed = parseDocument(restored);
236
+ if (!restoredParsed) {
237
+ return { error: `${entry.relPath}: fetched document did not parse`, filePath: entry.filePath };
238
+ }
239
+ const restoredKey = buildDocumentKey(restoredParsed.type, restoredParsed.name, restoredParsed.verb, restoredParsed.apiGroup);
240
+ if (restoredKey !== entry.key) {
241
+ return { error: `${entry.relPath}: fetched key ${restoredKey} != ${entry.key}`, filePath: entry.filePath };
242
+ }
243
+ return {
244
+ content: restored,
245
+ filePath: entry.filePath,
246
+ id: entry.id,
247
+ relPath: entry.relPath,
248
+ };
249
+ });
250
+ const results = [];
251
+ for (const item of fetched) {
252
+ if ('error' in item) {
253
+ return { ok: false, reason: item.error ?? 'unknown fetch error' };
254
+ }
255
+ results.push(item);
256
+ }
257
+ for (const entry of resolved) {
258
+ ids[entry.relPath] = {
259
+ id: entry.id,
260
+ key: entry.key,
261
+ sha256: '',
262
+ };
263
+ }
264
+ return { ids, ok: true, results };
265
+ }
@@ -42,6 +42,9 @@ export declare function checkReferences(documents: Array<{
42
42
  }>, serverOperations?: Array<{
43
43
  name: string;
44
44
  type: string;
45
+ }>, scanDocuments?: Array<{
46
+ content: string;
47
+ filePath: string;
45
48
  }>): BadReference[];
46
49
  export interface BadIndex {
47
50
  field: string;
@@ -107,7 +107,7 @@ export function buildServerRegistry(operations) {
107
107
  * both local files AND server-known objects, eliminating false positives for objects
108
108
  * that exist on the server but aren't in the push set.
109
109
  */
110
- export function checkReferences(documents, serverOperations) {
110
+ export function checkReferences(documents, serverOperations, scanDocuments) {
111
111
  const registry = buildRegistry(documents);
112
112
  // Merge server-known names into the registry
113
113
  if (serverOperations) {
@@ -122,7 +122,8 @@ export function checkReferences(documents, serverOperations) {
122
122
  }
123
123
  }
124
124
  const badRefs = [];
125
- for (const doc of documents) {
125
+ const toScan = scanDocuments ?? documents;
126
+ for (const doc of toScan) {
126
127
  const parsed = parseDocument(doc.content);
127
128
  if (!parsed)
128
129
  continue;
@@ -0,0 +1,9 @@
1
+ export declare const SANDBOX_MARKER_VERSION = 1;
2
+ export declare const SANDBOX_MARKER_FILENAME = "sandbox_sync.json";
3
+ export declare function sandboxMarkerPath(dir: string): string;
4
+ export declare function sandboxMarkerExists(dir: string): boolean;
5
+ export declare function sandboxMarkerKey(host: string, tenantId: number | string): string;
6
+ export declare function readMarkerToken(dir: string, host: string, tenantId: number | string): null | string;
7
+ export declare function writeMarkerToken(dir: string, host: string, tenantId: number | string): string;
8
+ export declare function invalidateMarkerHost(dir: string, host: string): void;
9
+ export declare function invalidateSandboxMarkerForOrigin(dir: string, instanceOrigin: string): void;
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { instanceHost } from './sync-state.js';
5
+ export const SANDBOX_MARKER_VERSION = 1;
6
+ export const SANDBOX_MARKER_FILENAME = 'sandbox_sync.json';
7
+ const TEMP_PREFIX = `${SANDBOX_MARKER_FILENAME}.tmp-`;
8
+ export function sandboxMarkerPath(dir) {
9
+ return join(dir, SANDBOX_MARKER_FILENAME);
10
+ }
11
+ export function sandboxMarkerExists(dir) {
12
+ return fs.existsSync(sandboxMarkerPath(dir));
13
+ }
14
+ export function sandboxMarkerKey(host, tenantId) {
15
+ return `${host}#${tenantId}`;
16
+ }
17
+ function isRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ function parseMarkerFile(raw) {
21
+ if (!isRecord(raw))
22
+ return null;
23
+ if (raw.version !== SANDBOX_MARKER_VERSION)
24
+ return null;
25
+ if (!isRecord(raw.sandboxes))
26
+ return null;
27
+ const sandboxes = {};
28
+ for (const [key, value] of Object.entries(raw.sandboxes)) {
29
+ if (!isRecord(value) || typeof value.token !== 'string' || value.token.length === 0) {
30
+ return null;
31
+ }
32
+ sandboxes[key] = { token: value.token };
33
+ }
34
+ return { sandboxes, version: SANDBOX_MARKER_VERSION };
35
+ }
36
+ function readMarkerFile(dir) {
37
+ const filePath = sandboxMarkerPath(dir);
38
+ if (!fs.existsSync(filePath))
39
+ return null;
40
+ try {
41
+ return parseMarkerFile(JSON.parse(fs.readFileSync(filePath, 'utf8')));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function cleanupTempFiles(dir, keep) {
48
+ let names = [];
49
+ try {
50
+ names = fs.readdirSync(dir);
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ for (const name of names) {
56
+ if (!name.startsWith(TEMP_PREFIX))
57
+ continue;
58
+ const full = join(dir, name);
59
+ if (keep && full === keep)
60
+ continue;
61
+ try {
62
+ fs.unlinkSync(full);
63
+ }
64
+ catch {
65
+ // Best-effort cleanup.
66
+ }
67
+ }
68
+ }
69
+ function atomicWrite(dir, payload) {
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ const dest = sandboxMarkerPath(dir);
72
+ const tmp = join(dir, `${TEMP_PREFIX}${process.pid}`);
73
+ try {
74
+ fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
75
+ fs.renameSync(tmp, dest);
76
+ }
77
+ catch (error) {
78
+ try {
79
+ fs.unlinkSync(tmp);
80
+ }
81
+ catch {
82
+ // Already gone.
83
+ }
84
+ throw error;
85
+ }
86
+ finally {
87
+ cleanupTempFiles(dir);
88
+ }
89
+ }
90
+ export function readMarkerToken(dir, host, tenantId) {
91
+ const file = readMarkerFile(dir);
92
+ if (!file)
93
+ return null;
94
+ return file.sandboxes[sandboxMarkerKey(host, tenantId)]?.token ?? null;
95
+ }
96
+ export function writeMarkerToken(dir, host, tenantId) {
97
+ const token = randomUUID();
98
+ const existing = readMarkerFile(dir);
99
+ const sandboxes = existing?.sandboxes ? { ...existing.sandboxes } : {};
100
+ sandboxes[sandboxMarkerKey(host, tenantId)] = { token };
101
+ atomicWrite(dir, { sandboxes, version: SANDBOX_MARKER_VERSION });
102
+ return token;
103
+ }
104
+ export function invalidateMarkerHost(dir, host) {
105
+ const file = readMarkerFile(dir);
106
+ if (!file)
107
+ return;
108
+ const prefix = `${host}#`;
109
+ const sandboxes = {};
110
+ for (const [key, entry] of Object.entries(file.sandboxes)) {
111
+ if (!key.startsWith(prefix)) {
112
+ sandboxes[key] = entry;
113
+ }
114
+ }
115
+ atomicWrite(dir, { sandboxes, version: SANDBOX_MARKER_VERSION });
116
+ }
117
+ export function invalidateSandboxMarkerForOrigin(dir, instanceOrigin) {
118
+ invalidateMarkerHost(dir, instanceHost(instanceOrigin));
119
+ }
@@ -0,0 +1,71 @@
1
+ export declare const SYNC_STATE_VERSION = 1;
2
+ export declare const SYNC_STATE_DIR = ".xano";
3
+ export declare const SYNC_STATE_FILENAME = "sync_state.json";
4
+ export interface SyncFileEntry {
5
+ id?: number;
6
+ key: string;
7
+ sha256: string;
8
+ }
9
+ export interface SyncTargetIdentity {
10
+ branch: string;
11
+ branch_created_at: string;
12
+ instance_host: string;
13
+ workspace_created_at: string;
14
+ workspace_id: string;
15
+ }
16
+ export interface SyncTargetState extends SyncTargetIdentity {
17
+ files: Record<string, SyncFileEntry>;
18
+ sandbox_token?: string;
19
+ }
20
+ export interface SyncStateFile {
21
+ targets: SyncTargetState[];
22
+ version: number;
23
+ }
24
+ export interface LocalFileHash {
25
+ relPath: string;
26
+ sha256: string;
27
+ }
28
+ export interface CandidateResult {
29
+ candidates: LocalFileHash[];
30
+ deletedLocally: Array<{
31
+ key: string;
32
+ relPath: string;
33
+ }>;
34
+ unchanged: LocalFileHash[];
35
+ }
36
+ export type MetaGet = (pathname: string) => Promise<unknown>;
37
+ export declare function instanceHost(instanceOrigin: string): string;
38
+ export declare function toRelPath(dir: string, filePath: string): string;
39
+ export declare function hashBytes(data: Buffer | string): string;
40
+ export declare function hashFile(filePath: string): string;
41
+ export declare function syncStatePath(dir: string): string;
42
+ export declare function syncStateExists(dir: string): boolean;
43
+ export declare function recordEntriesForPaths(dir: string, filePaths: string[]): Record<string, SyncFileEntry>;
44
+ /**
45
+ * Build record entries from file bytes the caller already holds. Push uses this to record
46
+ * exactly what it sent, so an edit made while the push ran is not recorded as synced.
47
+ */
48
+ export declare function recordEntriesForContents(dir: string, files: Array<{
49
+ content: string;
50
+ filePath: string;
51
+ }>): Record<string, SyncFileEntry>;
52
+ export declare function sandboxTargetIdentity(host: string, tenant: {
53
+ created_at: string;
54
+ id: number;
55
+ }): SyncTargetIdentity;
56
+ export declare function sameTargetIdentity(a: SyncTargetIdentity, b: SyncTargetIdentity): boolean;
57
+ export declare function sameTargetFingerprints(a: SyncTargetIdentity, b: SyncTargetIdentity): boolean;
58
+ export declare function readSyncStateFile(dir: string, warn?: (message: string) => void): null | SyncStateFile;
59
+ export declare function loadSyncState(dir: string, target: SyncTargetIdentity, warn?: (message: string) => void): null | SyncTargetState;
60
+ export interface ResolvedTarget extends SyncTargetIdentity {
61
+ live_branch: string;
62
+ }
63
+ export declare function resolveTarget(instanceOrigin: string, workspaceId: string, branch: string, fetchJson: MetaGet): Promise<null | ResolvedTarget>;
64
+ export declare function computeCandidates(files: LocalFileHash[], state: null | SyncTargetState): CandidateResult;
65
+ export declare function recordFiles(state: SyncTargetState, entries: Record<string, SyncFileEntry>): SyncTargetState;
66
+ export declare function saveSyncState(dir: string, target: SyncTargetIdentity, entries: Record<string, SyncFileEntry>, options?: {
67
+ pruneMissing?: boolean;
68
+ replace?: boolean;
69
+ sandboxToken?: string;
70
+ }): void;
71
+ export declare function listSyncDir(dir: string): string[];