@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.
- package/README.md +68 -1
- 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 +3532 -3450
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,6 +199,7 @@ xano workspace pull -d ./my-workspace # Specify output direct
|
|
|
199
199
|
xano workspace pull -b dev # Specific branch
|
|
200
200
|
xano workspace pull --env --records # Include env vars and table records
|
|
201
201
|
xano workspace pull --draft # Include draft changes
|
|
202
|
+
xano workspace pull --sync-state # Write .xano/sync_state.json; later pulls keep it current
|
|
202
203
|
|
|
203
204
|
# Push local files to workspace (defaults to current directory, only changed files)
|
|
204
205
|
xano workspace push
|
|
@@ -212,6 +213,11 @@ xano workspace push --env # Include environment v
|
|
|
212
213
|
xano workspace push --truncate # Truncate tables before import
|
|
213
214
|
xano workspace push --no-transaction # Disable database transaction wrapping
|
|
214
215
|
xano workspace push --no-guids # Skip writing GUIDs back to local files
|
|
216
|
+
xano workspace push --fast # Push only files that changed since last sync
|
|
217
|
+
xano workspace push --fast --refresh # Re-compare against the server export, then push
|
|
218
|
+
xano workspace push --no-fast # Skip --fast even when profile.yaml sets fast: true
|
|
219
|
+
xano workspace push --writeback # Write server-formatted XanoScript back to pushed files
|
|
220
|
+
xano workspace push --writeback --writeback-diff ./diffs # Also save local vs server copies for linter work
|
|
215
221
|
xano workspace push --force # Skip preview and confirmation (for CI/CD)
|
|
216
222
|
xano workspace push -i "function/*" # Push only matching files
|
|
217
223
|
xano workspace push -e "table/*" # Push all files except tables
|
|
@@ -244,6 +250,44 @@ directory *adds* a file rather than overwriting, and renaming a workspace leaves
|
|
|
244
250
|
the old-name file behind. Delete the stale ones, keeping the single document that
|
|
245
251
|
matches your target workspace.
|
|
246
252
|
|
|
253
|
+
### `.xano/sync_state.json`
|
|
254
|
+
|
|
255
|
+
`--fast` skips files whose contents still match a local sync
|
|
256
|
+
record at `<push dir>/.xano/sync_state.json`. The file is per-working-copy
|
|
257
|
+
state: it should not be committed. Ignoring it is up to you (the CLI never
|
|
258
|
+
edits `.gitignore` or any other VCS ignore file).
|
|
259
|
+
|
|
260
|
+
The record is created by `workspace pull --sync-state`, `sandbox pull --sync-state`,
|
|
261
|
+
`workspace push --fast`, or `sandbox push --fast`.
|
|
262
|
+
Once it exists, every successful workspace or sandbox push or pull updates it. A successful
|
|
263
|
+
`workspace push --sync --delete` or `sandbox push --sync --delete` also removes entries for
|
|
264
|
+
files deleted locally, which clears the "deleted locally" note. A default push or pull does
|
|
265
|
+
not create it. `--dry-run` never writes it, and neither does `pull --draft`, because
|
|
266
|
+
`--fast` compares against published versions. Push records the exact bytes it sent, so a
|
|
267
|
+
file edited while a push runs is sent again by the next `--fast` push. Delete the file to turn this off; the next
|
|
268
|
+
`--fast` push compares against the server export once (~99 seconds on a
|
|
269
|
+
~1,100-document workspace) and rebuilds it. If that export fails, the CLI falls
|
|
270
|
+
back to a full dry-run.
|
|
271
|
+
|
|
272
|
+
Sandbox `--fast` also keeps a per-user marker at `sandbox_sync.json` next to your
|
|
273
|
+
credentials file (normally `~/.xano/sandbox_sync.json`). A successful sandbox push
|
|
274
|
+
or `sandbox pull --sync-state` writes a new token there and into the directory's
|
|
275
|
+
record. `sandbox reset` and `sandbox delete` clear the host's entries, as does a
|
|
276
|
+
sandbox push from another directory. If the tokens do not match, `--fast` compares
|
|
277
|
+
against the sandbox export once instead of trusting the record. After a reset or
|
|
278
|
+
push done outside this CLI or on another machine, use `--refresh`.
|
|
279
|
+
|
|
280
|
+
`--refresh` (requires `--fast`) runs that export comparison even when a record
|
|
281
|
+
already exists. Use it after edits in the Xano UI, a teammate's push, or when
|
|
282
|
+
the record looks wrong. `--fast` will not overwrite remote-only changes; a push
|
|
283
|
+
without `--fast` still will.
|
|
284
|
+
|
|
285
|
+
`--fast` cannot be combined with `--sync`. It works with `--force` and still
|
|
286
|
+
sends only the files the record (or export comparison) marked as changed.
|
|
287
|
+
With `--records`, tables that have `items` are always sent, as they are without
|
|
288
|
+
`--fast`. With `--env`, `--fast` compares every file with the dry-run, because the
|
|
289
|
+
record does not track environment variables.
|
|
290
|
+
|
|
247
291
|
### Flatten
|
|
248
292
|
|
|
249
293
|
A hand-authored `.xs` file can hold several documents joined by `---`. That is
|
|
@@ -820,6 +864,7 @@ xano sandbox get -o json
|
|
|
820
864
|
xano sandbox pull
|
|
821
865
|
xano sandbox pull -d ./my-sandbox
|
|
822
866
|
xano sandbox pull --env --records
|
|
867
|
+
xano sandbox pull --sync-state # Write .xano/sync_state.json; later pulls keep it current
|
|
823
868
|
|
|
824
869
|
# Push local files to sandbox (defaults to current directory, only changed files)
|
|
825
870
|
xano sandbox push
|
|
@@ -827,6 +872,11 @@ xano sandbox push -d ./my-workspace # Push from a specific
|
|
|
827
872
|
xano sandbox push --sync # Full push — send all files
|
|
828
873
|
xano sandbox push --sync --delete # Full push + delete remote objects not included
|
|
829
874
|
xano sandbox push --dry-run # Preview changes without pushing
|
|
875
|
+
xano sandbox push --fast # Skip unchanged files using .xano/sync_state.json
|
|
876
|
+
xano sandbox push --fast --refresh # Re-compare against the sandbox export, then push
|
|
877
|
+
xano sandbox push --no-fast # Push without --fast even when profile.yaml sets fast: true
|
|
878
|
+
xano sandbox push --writeback # Write server-formatted XanoScript back after push
|
|
879
|
+
xano sandbox push --writeback --writeback-diff ./diffs # Also save local vs server copies
|
|
830
880
|
xano sandbox push --records --env # Include records and environment variables
|
|
831
881
|
xano sandbox push --truncate # Truncate tables before import
|
|
832
882
|
xano sandbox push --no-guids # Skip writing GUIDs back to local files
|
|
@@ -972,11 +1022,28 @@ workspace: 110 # optional override
|
|
|
972
1022
|
instance_origin: https://your-instance.xano.io # optional override
|
|
973
1023
|
account_origin: https://app.xano.com # optional override
|
|
974
1024
|
branch: main # optional override
|
|
1025
|
+
fast: true # optional: default workspace/sandbox push --fast
|
|
1026
|
+
writeback: true # optional: default workspace/sandbox push --writeback
|
|
1027
|
+
writeback_object_limit: 50 # optional: workspace push --writeback per-object fetch limit (default 25)
|
|
975
1028
|
```
|
|
976
1029
|
|
|
977
1030
|
When a `profile.yaml` is in effect, every command prints the active target,
|
|
978
1031
|
e.g. `Using profile 'staging' (workspace 110) · profile.yaml` (suppressed for
|
|
979
|
-
`--output json`).
|
|
1032
|
+
`--output json`). `workspace push` and `sandbox push` also log
|
|
1033
|
+
`Using --fast --writeback · profile.yaml` when those keys turn the flags on.
|
|
1034
|
+
`--no-fast` and `--no-writeback` override them for one run. `--sync` ignores a
|
|
1035
|
+
`fast: true` rather than erroring.
|
|
1036
|
+
|
|
1037
|
+
`workspace push --writeback` fetches each pushed object individually when the push
|
|
1038
|
+
has at most `writeback_object_limit` documents (default 25), and otherwise downloads
|
|
1039
|
+
the full workspace export. Per-object requests grow with the number of documents;
|
|
1040
|
+
the export costs about the same no matter how many you pushed. Raise the limit for a
|
|
1041
|
+
large workspace where the export is slow; `0` always uses the export. If a per-object
|
|
1042
|
+
request fails, writeback falls back to the export. Sandbox push always uses the export.
|
|
1043
|
+
Writeback skips, with a warning, any file that changed on disk after the push read it.
|
|
1044
|
+
|
|
1045
|
+
An explicit `-p/--profile` or `XANO_PROFILE` ignores `profile.yaml` entirely,
|
|
1046
|
+
including `fast`, `writeback`, and `writeback_object_limit`.
|
|
980
1047
|
|
|
981
1048
|
Generate one with `xano profile use`. It writes a self-documenting
|
|
982
1049
|
`profile.yaml` (every overridable field is included as a commented example, so
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
import BaseCommand from '../../../base-command.js';
|
|
4
|
+
import { invalidateSandboxMarkerForOrigin } from '../../../utils/sandbox-marker.js';
|
|
3
5
|
export default class SandboxDelete extends BaseCommand {
|
|
4
6
|
static description = '[CRITICAL] NEVER run without explicit user confirmation; this destroys all sandbox data. Deletes your sandbox environment completely (debugging only — it will be re-created on next access).';
|
|
5
7
|
static examples = [
|
|
@@ -43,6 +45,12 @@ Sandbox environment deleted.
|
|
|
43
45
|
this.error(message);
|
|
44
46
|
}
|
|
45
47
|
this.log('Sandbox environment deleted.');
|
|
48
|
+
try {
|
|
49
|
+
invalidateSandboxMarkerForOrigin(dirname(this.getCredentialsPath()), profile.instance_origin);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.warn(`Skipped invalidating sandbox sync marker: ${error.message}`);
|
|
53
|
+
}
|
|
46
54
|
}
|
|
47
55
|
catch (error) {
|
|
48
56
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether pull should write `.xano/sync_state.json`: only when asked to, or to keep an existing
|
|
4
|
+
* record current. A `--draft` pull never writes it, because `--fast` compares against published
|
|
5
|
+
* versions and recording draft bodies as synced would stop those drafts from being pushed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function syncStateAction(flag: boolean, dir: string, draft: boolean): 'draft' | 'skip' | 'write';
|
|
2
8
|
export default class SandboxPull extends BaseCommand {
|
|
3
9
|
static description: string;
|
|
4
10
|
static examples: string[];
|
|
@@ -7,6 +13,7 @@ export default class SandboxPull extends BaseCommand {
|
|
|
7
13
|
draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
14
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
15
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
|
+
'sync-state': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
17
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
18
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
19
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -5,6 +5,18 @@ import path from 'node:path';
|
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
8
|
+
import { writeMarkerToken } from '../../../utils/sandbox-marker.js';
|
|
9
|
+
import { instanceHost, recordEntriesForPaths, sandboxTargetIdentity, saveSyncState, syncStateExists, } from '../../../utils/sync-state.js';
|
|
10
|
+
/**
|
|
11
|
+
* Whether pull should write `.xano/sync_state.json`: only when asked to, or to keep an existing
|
|
12
|
+
* record current. A `--draft` pull never writes it, because `--fast` compares against published
|
|
13
|
+
* versions and recording draft bodies as synced would stop those drafts from being pushed.
|
|
14
|
+
*/
|
|
15
|
+
export function syncStateAction(flag, dir, draft) {
|
|
16
|
+
if (!flag && !syncStateExists(dir))
|
|
17
|
+
return 'skip';
|
|
18
|
+
return draft ? 'draft' : 'write';
|
|
19
|
+
}
|
|
8
20
|
export default class SandboxPull extends BaseCommand {
|
|
9
21
|
static description = 'Pull documents from your sandbox environment and split into individual files';
|
|
10
22
|
static examples = [
|
|
@@ -15,6 +27,9 @@ Pulled 42 documents from sandbox environment to current directory
|
|
|
15
27
|
Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
16
28
|
`,
|
|
17
29
|
`$ xano sandbox pull --env --records`,
|
|
30
|
+
`$ xano sandbox pull --sync-state
|
|
31
|
+
Write .xano/sync_state.json after pulling; later pulls keep it current while it exists
|
|
32
|
+
`,
|
|
18
33
|
];
|
|
19
34
|
static flags = {
|
|
20
35
|
...BaseCommand.baseFlags,
|
|
@@ -39,6 +54,11 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
39
54
|
description: 'Include records',
|
|
40
55
|
required: false,
|
|
41
56
|
}),
|
|
57
|
+
'sync-state': Flags.boolean({
|
|
58
|
+
default: false,
|
|
59
|
+
description: 'Write .xano/sync_state.json after a successful pull. Needed only the first time; later pulls keep the record current while it exists.',
|
|
60
|
+
required: false,
|
|
61
|
+
}),
|
|
42
62
|
};
|
|
43
63
|
async run() {
|
|
44
64
|
const { flags } = await this.parse(SandboxPull);
|
|
@@ -97,6 +117,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
97
117
|
const getChannelServer = buildChannelServerResolver(documents);
|
|
98
118
|
const filenameCounters = new Map();
|
|
99
119
|
let writtenCount = 0;
|
|
120
|
+
const writtenPaths = [];
|
|
100
121
|
for (const doc of documents) {
|
|
101
122
|
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
102
123
|
getApiGroupFolder,
|
|
@@ -115,6 +136,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
115
136
|
const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
116
137
|
const filePath = path.join(typeDir, filename);
|
|
117
138
|
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
139
|
+
writtenPaths.push(filePath);
|
|
118
140
|
writtenCount++;
|
|
119
141
|
}
|
|
120
142
|
// ── Pull knowledge ────────────────────────────────────────────────────
|
|
@@ -128,5 +150,32 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
128
150
|
if (knowledgeCount > 0)
|
|
129
151
|
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
130
152
|
this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
|
|
153
|
+
const syncState = syncStateAction(flags['sync-state'], outputDir, flags.draft);
|
|
154
|
+
if (syncState === 'skip') {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (syncState === 'draft') {
|
|
158
|
+
this.warn('Skipped writing .xano/sync_state.json: --draft pulls draft versions, which --fast does not compare against');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const tenant = await this.getOrCreateSandbox(profile, flags.verbose);
|
|
163
|
+
if (typeof tenant.created_at !== 'string') {
|
|
164
|
+
this.warn('Could not resolve sandbox fingerprints; skipped writing .xano/sync_state.json');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const target = sandboxTargetIdentity(instanceHost(profile.instance_origin), {
|
|
168
|
+
created_at: tenant.created_at,
|
|
169
|
+
id: tenant.id,
|
|
170
|
+
});
|
|
171
|
+
const sandboxToken = writeMarkerToken(path.dirname(this.getCredentialsPath()), target.instance_host, target.branch_created_at);
|
|
172
|
+
saveSyncState(outputDir, target, recordEntriesForPaths(outputDir, writtenPaths), {
|
|
173
|
+
replace: true,
|
|
174
|
+
sandboxToken,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
this.warn(`Skipped writing .xano/sync_state.json: ${error.message}`);
|
|
179
|
+
}
|
|
131
180
|
}
|
|
132
181
|
}
|
|
@@ -7,13 +7,17 @@ export default class SandboxPush extends BaseCommand {
|
|
|
7
7
|
directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
8
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
fast: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
refresh: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
15
|
review: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
16
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
17
|
transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
18
|
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
writeback: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
'writeback-diff': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
21
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
22
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
23
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, relative, resolve } from 'node:path';
|
|
4
4
|
import open from 'open';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
+
import { formatWorkspacePushDefaultsBanner, resolvePushDefaults } from '../../../utils/local-config.js';
|
|
6
7
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
7
8
|
export default class SandboxPush extends BaseCommand {
|
|
8
9
|
static description = '[IMPORTANT] ALWAYS run --dry-run first and show the user the output before pushing. Push local documents to your sandbox environment via multidoc import. 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. Include/exclude glob filters are intentionally not supported on sandbox push — partial pushes can hide deletions during review and lead to data loss when promoted to the workspace. Pushing into a sandbox that currently holds a different workspace will prompt for confirmation; run `xano sandbox reset` first to start clean.';
|
|
@@ -24,6 +25,18 @@ Preview changes without pushing
|
|
|
24
25
|
`,
|
|
25
26
|
`$ xano sandbox push --force
|
|
26
27
|
Skip preview and push immediately
|
|
28
|
+
`,
|
|
29
|
+
`$ xano sandbox push --fast
|
|
30
|
+
Push only files that changed since the last sync, and write .xano/sync_state.json
|
|
31
|
+
`,
|
|
32
|
+
`$ xano sandbox push --fast --refresh
|
|
33
|
+
Re-compare local files against the sandbox export, then push changes
|
|
34
|
+
`,
|
|
35
|
+
`$ xano sandbox push --no-fast
|
|
36
|
+
Push without --fast even when profile.yaml sets fast: true
|
|
37
|
+
`,
|
|
38
|
+
`$ xano sandbox push --writeback
|
|
39
|
+
Push and write server-formatted XanoScript back to local files
|
|
27
40
|
`,
|
|
28
41
|
`$ xano sandbox push --records --env`,
|
|
29
42
|
`$ xano sandbox push --truncate`,
|
|
@@ -54,6 +67,11 @@ Push and open sandbox review in the browser
|
|
|
54
67
|
description: 'Include environment variables in import',
|
|
55
68
|
required: false,
|
|
56
69
|
}),
|
|
70
|
+
fast: Flags.boolean({
|
|
71
|
+
allowNo: true,
|
|
72
|
+
description: 'Skip files whose contents match .xano/sync_state.json, and write or update that record. Without a record, compares against the sandbox export once. Can be set as `fast: true` in profile.yaml.',
|
|
73
|
+
required: false,
|
|
74
|
+
}),
|
|
57
75
|
force: Flags.boolean({
|
|
58
76
|
default: false,
|
|
59
77
|
description: 'Skip preview and confirmation prompt (for CI/CD pipelines)',
|
|
@@ -70,6 +88,11 @@ Push and open sandbox review in the browser
|
|
|
70
88
|
description: 'Include records in import',
|
|
71
89
|
required: false,
|
|
72
90
|
}),
|
|
91
|
+
refresh: Flags.boolean({
|
|
92
|
+
default: false,
|
|
93
|
+
description: 'With --fast, re-compare local files against the sandbox export even when a sync record exists',
|
|
94
|
+
required: false,
|
|
95
|
+
}),
|
|
73
96
|
review: Flags.boolean({
|
|
74
97
|
default: false,
|
|
75
98
|
description: 'Open sandbox review in the browser after pushing',
|
|
@@ -91,6 +114,15 @@ Push and open sandbox review in the browser
|
|
|
91
114
|
description: '[CRITICAL] STOP and confirm with the user; this truncates live tables before importing.',
|
|
92
115
|
required: false,
|
|
93
116
|
}),
|
|
117
|
+
writeback: Flags.boolean({
|
|
118
|
+
allowNo: true,
|
|
119
|
+
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.',
|
|
120
|
+
required: false,
|
|
121
|
+
}),
|
|
122
|
+
'writeback-diff': Flags.string({
|
|
123
|
+
description: 'With --writeback, save local vs server copies of rewritten files under this directory',
|
|
124
|
+
required: false,
|
|
125
|
+
}),
|
|
94
126
|
};
|
|
95
127
|
async run() {
|
|
96
128
|
const { flags } = await this.parse(SandboxPush);
|
|
@@ -103,6 +135,17 @@ Push and open sandbox review in the browser
|
|
|
103
135
|
this.error(`Not a directory: ${inputDir}`);
|
|
104
136
|
}
|
|
105
137
|
const baseUrl = `${profile.instance_origin}/api:meta/sandbox`;
|
|
138
|
+
const pushDefaults = resolvePushDefaults({
|
|
139
|
+
flagFast: flags.fast,
|
|
140
|
+
flagWriteback: flags.writeback,
|
|
141
|
+
localFast: this.localProfile?.config.fast,
|
|
142
|
+
localWriteback: this.localProfile?.config.writeback,
|
|
143
|
+
sync: flags.sync,
|
|
144
|
+
});
|
|
145
|
+
if (pushDefaults.fromLocal.length > 0 && this.localProfile) {
|
|
146
|
+
const relativePath = relative(process.cwd(), this.localProfile.path) || basename(this.localProfile.path);
|
|
147
|
+
this.log(formatWorkspacePushDefaultsBanner(pushDefaults.fromLocal, relativePath));
|
|
148
|
+
}
|
|
106
149
|
const target = {
|
|
107
150
|
buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
|
|
108
151
|
buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
|
|
@@ -118,13 +161,17 @@ Push and open sandbox review in the browser
|
|
|
118
161
|
delete: flags.delete,
|
|
119
162
|
'dry-run': flags['dry-run'],
|
|
120
163
|
env: flags.env,
|
|
164
|
+
fast: pushDefaults.fast,
|
|
121
165
|
force: flags.force,
|
|
122
166
|
guids: flags.guids,
|
|
123
167
|
records: flags.records,
|
|
168
|
+
refresh: flags.refresh,
|
|
124
169
|
sync: flags.sync,
|
|
125
170
|
transaction: flags.transaction,
|
|
126
171
|
truncate: flags.truncate,
|
|
127
172
|
verbose: flags.verbose,
|
|
173
|
+
writeback: pushDefaults.writeback,
|
|
174
|
+
writebackDiff: flags['writeback-diff'],
|
|
128
175
|
};
|
|
129
176
|
await executePush({
|
|
130
177
|
accessToken: profile.access_token,
|
|
@@ -135,6 +182,11 @@ Push and open sandbox review in the browser
|
|
|
135
182
|
listUrl: () => `${baseUrl}/knowledge/sync`,
|
|
136
183
|
rootDir: inputDir,
|
|
137
184
|
},
|
|
185
|
+
syncTarget: {
|
|
186
|
+
instanceOrigin: profile.instance_origin,
|
|
187
|
+
kind: 'sandbox',
|
|
188
|
+
markerDir: dirname(this.getCredentialsPath()),
|
|
189
|
+
},
|
|
138
190
|
verboseFetch: this.verboseFetch.bind(this),
|
|
139
191
|
}, target, pushFlags);
|
|
140
192
|
if (flags.review) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
2
3
|
import BaseCommand from '../../../base-command.js';
|
|
3
4
|
import { pushKnowledge } from '../../../utils/knowledge-sync.js';
|
|
5
|
+
import { invalidateSandboxMarkerForOrigin } from '../../../utils/sandbox-marker.js';
|
|
4
6
|
export default class SandboxReset extends BaseCommand {
|
|
5
7
|
static description = '[CRITICAL] NEVER run without explicit user confirmation; this clears all workspace data and drafts. Resets your sandbox environment.';
|
|
6
8
|
static examples = [
|
|
@@ -55,6 +57,12 @@ Sandbox environment has been reset.
|
|
|
55
57
|
this.warn(`Sandbox environment was reset, but clearing knowledge files failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
56
58
|
}
|
|
57
59
|
this.log('Sandbox environment has been reset.');
|
|
60
|
+
try {
|
|
61
|
+
invalidateSandboxMarkerForOrigin(dirname(this.getCredentialsPath()), profile.instance_origin);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
this.warn(`Skipped invalidating sandbox sync marker: ${error.message}`);
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
catch (error) {
|
|
60
68
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether pull should write `.xano/sync_state.json`: only when asked to, or to keep an existing
|
|
4
|
+
* record current. A `--draft` pull never writes it, because `--fast` compares against published
|
|
5
|
+
* versions and recording draft bodies as synced would stop those drafts from being pushed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function syncStateAction(flag: boolean, dir: string, draft: boolean): 'draft' | 'skip' | 'write';
|
|
2
8
|
export default class Pull extends BaseCommand {
|
|
3
9
|
static description: string;
|
|
4
10
|
static examples: string[];
|
|
@@ -8,6 +14,7 @@ export default class Pull extends BaseCommand {
|
|
|
8
14
|
draft: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
15
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
16
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
'sync-state': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
18
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
19
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
20
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -5,6 +5,17 @@ import path from 'node:path';
|
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
8
|
+
import { recordEntriesForPaths, resolveTarget, saveSyncState, syncStateExists } from '../../../utils/sync-state.js';
|
|
9
|
+
/**
|
|
10
|
+
* Whether pull should write `.xano/sync_state.json`: only when asked to, or to keep an existing
|
|
11
|
+
* record current. A `--draft` pull never writes it, because `--fast` compares against published
|
|
12
|
+
* versions and recording draft bodies as synced would stop those drafts from being pushed.
|
|
13
|
+
*/
|
|
14
|
+
export function syncStateAction(flag, dir, draft) {
|
|
15
|
+
if (!flag && !syncStateExists(dir))
|
|
16
|
+
return 'skip';
|
|
17
|
+
return draft ? 'draft' : 'write';
|
|
18
|
+
}
|
|
8
19
|
export default class Pull extends BaseCommand {
|
|
9
20
|
static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
|
|
10
21
|
static examples = [
|
|
@@ -22,6 +33,9 @@ Pulled 58 documents
|
|
|
22
33
|
`,
|
|
23
34
|
`$ xano workspace pull --draft`,
|
|
24
35
|
`$ xano workspace pull -b dev`,
|
|
36
|
+
`$ xano workspace pull --sync-state
|
|
37
|
+
Write .xano/sync_state.json after pulling; later pulls keep it current while it exists
|
|
38
|
+
`,
|
|
25
39
|
];
|
|
26
40
|
static flags = {
|
|
27
41
|
...BaseCommand.baseFlags,
|
|
@@ -51,6 +65,11 @@ Pulled 58 documents
|
|
|
51
65
|
description: 'Include records',
|
|
52
66
|
required: false,
|
|
53
67
|
}),
|
|
68
|
+
'sync-state': Flags.boolean({
|
|
69
|
+
default: false,
|
|
70
|
+
description: 'Write .xano/sync_state.json after a successful pull. Needed only the first time; later pulls keep the record current while it exists.',
|
|
71
|
+
required: false,
|
|
72
|
+
}),
|
|
54
73
|
workspace: Flags.string({
|
|
55
74
|
char: 'w',
|
|
56
75
|
description: 'Workspace ID (optional if set in profile)',
|
|
@@ -140,6 +159,7 @@ Pulled 58 documents
|
|
|
140
159
|
// Track filenames per type to handle duplicates
|
|
141
160
|
const filenameCounters = new Map();
|
|
142
161
|
let writtenCount = 0;
|
|
162
|
+
const writtenPaths = [];
|
|
143
163
|
for (const doc of documents) {
|
|
144
164
|
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
145
165
|
getApiGroupFolder,
|
|
@@ -160,6 +180,7 @@ Pulled 58 documents
|
|
|
160
180
|
const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
161
181
|
const filePath = path.join(typeDir, filename);
|
|
162
182
|
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
183
|
+
writtenPaths.push(filePath);
|
|
163
184
|
writtenCount++;
|
|
164
185
|
}
|
|
165
186
|
// ── Pull knowledge ────────────────────────────────────────────────────
|
|
@@ -173,5 +194,38 @@ Pulled 58 documents
|
|
|
173
194
|
if (knowledgeCount > 0)
|
|
174
195
|
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
175
196
|
this.log(`Pulled ${parts.join(' + ')} to ${flags.directory}`);
|
|
197
|
+
const syncState = syncStateAction(flags['sync-state'], outputDir, flags.draft);
|
|
198
|
+
if (syncState === 'skip') {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (syncState === 'draft') {
|
|
202
|
+
this.warn('Skipped writing .xano/sync_state.json: --draft pulls draft versions, which --fast does not compare against');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const origin = profile.instance_origin.replace(/\/$/, '');
|
|
206
|
+
try {
|
|
207
|
+
const target = await resolveTarget(profile.instance_origin, workspaceId, branch, async (pathname) => {
|
|
208
|
+
const response = await this.verboseFetch(`${origin}/api:meta${pathname}`, {
|
|
209
|
+
headers: {
|
|
210
|
+
accept: 'application/json',
|
|
211
|
+
Authorization: `Bearer ${profile.access_token}`,
|
|
212
|
+
},
|
|
213
|
+
method: 'GET',
|
|
214
|
+
}, flags.verbose, profile.access_token);
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
throw new Error(`HTTP ${response.status}`);
|
|
217
|
+
}
|
|
218
|
+
return response.json();
|
|
219
|
+
});
|
|
220
|
+
if (!target) {
|
|
221
|
+
this.warn('Could not resolve workspace/branch fingerprints; skipped writing .xano/sync_state.json');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const entries = recordEntriesForPaths(outputDir, writtenPaths);
|
|
225
|
+
saveSyncState(outputDir, target, entries, { replace: true });
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
this.warn(`Skipped writing .xano/sync_state.json: ${error.message}`);
|
|
229
|
+
}
|
|
176
230
|
}
|
|
177
231
|
}
|
|
@@ -9,14 +9,18 @@ export default class Push extends BaseCommand {
|
|
|
9
9
|
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
fast: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
include: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
refresh: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
18
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
19
|
transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
20
|
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
21
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
writeback: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
+
'writeback-diff': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
24
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
25
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
26
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -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
|
}
|