@xano/cli 1.3.1-beta.0 → 1.3.1
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 +5 -269
- package/dist/base-command.d.ts +1 -12
- package/dist/base-command.js +22 -36
- package/dist/commands/branch/set_live/index.d.ts +0 -22
- package/dist/commands/branch/set_live/index.js +1 -60
- package/dist/commands/ephemeral/push/index.js +1 -6
- package/dist/commands/function/create/index.d.ts +0 -15
- package/dist/commands/function/create/index.js +0 -40
- package/dist/commands/function/edit/index.d.ts +0 -15
- package/dist/commands/function/edit/index.js +0 -40
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +10 -2
- package/dist/commands/release/create/index.js +0 -7
- package/dist/commands/release/deploy/index.d.ts +0 -28
- package/dist/commands/release/deploy/index.js +6 -90
- package/dist/commands/release/import/index.js +1 -5
- package/dist/commands/release/list/index.d.ts +0 -7
- package/dist/commands/release/list/index.js +10 -54
- package/dist/commands/release/push/index.js +1 -19
- package/dist/commands/sandbox/push/index.js +1 -6
- package/dist/commands/tenant/create/index.d.ts +0 -3
- package/dist/commands/tenant/create/index.js +0 -21
- package/dist/commands/tenant/deploy_release/index.d.ts +0 -40
- package/dist/commands/tenant/deploy_release/index.js +4 -186
- package/dist/commands/tenant/edit/index.d.ts +0 -2
- package/dist/commands/tenant/edit/index.js +4 -39
- package/dist/commands/workspace/pull/index.d.ts +0 -10
- package/dist/commands/workspace/pull/index.js +7 -70
- package/dist/commands/workspace/push/index.d.ts +0 -19
- package/dist/commands/workspace/push/index.js +5 -124
- package/dist/utils/document-parser.d.ts +0 -10
- package/dist/utils/document-parser.js +0 -21
- package/dist/utils/multidoc-push.d.ts +2 -55
- package/dist/utils/multidoc-push.js +27 -103
- package/oclif.manifest.json +3658 -6647
- package/package.json +1 -7
- package/dist/commands/policy/catalogue/index.d.ts +0 -15
- package/dist/commands/policy/catalogue/index.js +0 -29
- package/dist/commands/policy/create/index.d.ts +0 -18
- package/dist/commands/policy/create/index.js +0 -39
- package/dist/commands/policy/delete/index.d.ts +0 -19
- package/dist/commands/policy/delete/index.js +0 -61
- package/dist/commands/policy/evaluate/index.d.ts +0 -32
- package/dist/commands/policy/evaluate/index.js +0 -71
- package/dist/commands/policy/list/index.d.ts +0 -14
- package/dist/commands/policy/list/index.js +0 -21
- package/dist/commands/policy/parse/index.d.ts +0 -19
- package/dist/commands/policy/parse/index.js +0 -18
- package/dist/commands/policy/publish/index.d.ts +0 -20
- package/dist/commands/policy/publish/index.js +0 -38
- package/dist/commands/policy/runs/index.d.ts +0 -54
- package/dist/commands/policy/runs/index.js +0 -273
- package/dist/commands/policy/status/index.d.ts +0 -21
- package/dist/commands/policy/status/index.js +0 -54
- package/dist/commands/skills/pull/index.d.ts +0 -30
- package/dist/commands/skills/pull/index.js +0 -118
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +0 -31
- package/dist/commands/tenant_deploy_request/bypass/index.js +0 -109
- package/dist/commands/tenant_deploy_request/create/index.d.ts +0 -21
- package/dist/commands/tenant_deploy_request/create/index.js +0 -108
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +0 -22
- package/dist/commands/tenant_deploy_request/edit/index.js +0 -103
- package/dist/commands/tenant_deploy_request/get/index.d.ts +0 -25
- package/dist/commands/tenant_deploy_request/get/index.js +0 -116
- package/dist/commands/tenant_deploy_request/list/index.d.ts +0 -20
- package/dist/commands/tenant_deploy_request/list/index.js +0 -122
- package/dist/commands/tenant_deploy_request/revision/index.d.ts +0 -21
- package/dist/commands/tenant_deploy_request/revision/index.js +0 -83
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +0 -38
- package/dist/commands/tenant_deploy_request/set_status/index.js +0 -159
- package/dist/policy-command.d.ts +0 -90
- package/dist/policy-command.js +0 -94
- package/dist/utils/policy/catalogue.d.ts +0 -7
- package/dist/utils/policy/catalogue.js +0 -72
- package/dist/utils/policy/errors.d.ts +0 -12
- package/dist/utils/policy/errors.js +0 -61
- package/dist/utils/policy/feedback.d.ts +0 -68
- package/dist/utils/policy/feedback.js +0 -165
- package/dist/utils/policy/findings.d.ts +0 -23
- package/dist/utils/policy/findings.js +0 -65
- package/dist/utils/policy/gate.d.ts +0 -161
- package/dist/utils/policy/gate.js +0 -201
- package/dist/utils/policy/goal.d.ts +0 -46
- package/dist/utils/policy/goal.js +0 -95
- package/dist/utils/policy/permission.d.ts +0 -23
- package/dist/utils/policy/permission.js +0 -84
- package/dist/utils/policy/push-gate.d.ts +0 -5
- package/dist/utils/policy/push-gate.js +0 -29
- package/dist/utils/policy/release.d.ts +0 -85
- package/dist/utils/policy/release.js +0 -115
- package/dist/utils/policy/request.d.ts +0 -55
- package/dist/utils/policy/request.js +0 -115
- package/dist/utils/policy/runs.d.ts +0 -30
- package/dist/utils/policy/runs.js +0 -101
- package/dist/utils/policy/status.d.ts +0 -47
- package/dist/utils/policy/status.js +0 -117
- package/dist/utils/policy/types.d.ts +0 -185
- package/dist/utils/policy/types.js +0 -1
|
@@ -2,28 +2,7 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../base-command.js';
|
|
5
|
-
import {
|
|
6
|
-
import { executePush, FailedAfterImportError, } from '../../../utils/multidoc-push.js';
|
|
7
|
-
import { policyCodeGuidance } from '../../../utils/policy/errors.js';
|
|
8
|
-
import { policiesSkippedNotice, policyCheckWarning, policyDocumentSummary, policyExitCode, policySummary, pushEvidence, } from '../../../utils/policy/feedback.js';
|
|
9
|
-
import { BLANK_POLICY_OVERRIDE, blankPolicyOverride } from '../../../utils/policy/gate.js';
|
|
10
|
-
import { policyFilePushGuidance } from '../../../utils/policy/permission.js';
|
|
11
|
-
import { PushPolicyGateError } from '../../../utils/policy/push-gate.js';
|
|
12
|
-
/** The `-o json` document for a push whose import ran: the import response and what was sent. */
|
|
13
|
-
function importDocument(result) {
|
|
14
|
-
return { ...result.response, documents: result.sent.length, imported: true, knowledge: result.knowledge };
|
|
15
|
-
}
|
|
16
|
-
/** The policy check the import answered with, if any. */
|
|
17
|
-
function policyCheck(result) {
|
|
18
|
-
return result.response?.policy_check;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* The platform's one notice that it left the push's policy files out (the Policies feature is off):
|
|
22
|
-
* the import's, else its preview's, since a partial push may not send the files the preview named.
|
|
23
|
-
*/
|
|
24
|
-
function policiesSkipped(result) {
|
|
25
|
-
return policiesSkippedNotice(result.response) ?? policiesSkippedNotice(result.preview);
|
|
26
|
-
}
|
|
5
|
+
import { executePush } from '../../../utils/multidoc-push.js';
|
|
27
6
|
export default class Push extends BaseCommand {
|
|
28
7
|
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.';
|
|
29
8
|
static examples = [
|
|
@@ -77,9 +56,6 @@ Push functions but exclude test files
|
|
|
77
56
|
`,
|
|
78
57
|
`$ xano workspace push -i "knowledge/**"
|
|
79
58
|
Push only knowledge files (agents.md / skills / docs)
|
|
80
|
-
`,
|
|
81
|
-
`$ xano workspace push -m "Tightened the auth policies"
|
|
82
|
-
Label the Version History entry of each policy this push changes; other objects get no message
|
|
83
59
|
`,
|
|
84
60
|
`$ xano workspace push --sync --delete
|
|
85
61
|
Full sync including knowledge files; removes server objects not present locally
|
|
@@ -89,7 +65,7 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
89
65
|
...BaseCommand.baseFlags,
|
|
90
66
|
branch: Flags.string({
|
|
91
67
|
char: 'b',
|
|
92
|
-
description:
|
|
68
|
+
description: 'Branch name (optional if set in profile, defaults to live)',
|
|
93
69
|
required: false,
|
|
94
70
|
}),
|
|
95
71
|
delete: Flags.boolean({
|
|
@@ -136,19 +112,6 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
136
112
|
multiple: true,
|
|
137
113
|
required: false,
|
|
138
114
|
}),
|
|
139
|
-
message: Flags.string({
|
|
140
|
-
char: 'm',
|
|
141
|
-
description: 'Labels the Version History entry of each policy this push changes; other objects get no message',
|
|
142
|
-
required: false,
|
|
143
|
-
}),
|
|
144
|
-
output: Flags.string({
|
|
145
|
-
char: 'o',
|
|
146
|
-
default: 'summary',
|
|
147
|
-
description: 'Output format; JSON retains the complete import and policy feedback',
|
|
148
|
-
options: ['summary', 'json'],
|
|
149
|
-
required: false,
|
|
150
|
-
}),
|
|
151
|
-
'policy-override': Flags.string({ description: 'Override a blocking live-branch policy gate with an audited reason (requires workspace:policy update)' }),
|
|
152
115
|
records: Flags.boolean({
|
|
153
116
|
default: false,
|
|
154
117
|
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.',
|
|
@@ -176,20 +139,8 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
176
139
|
required: false,
|
|
177
140
|
}),
|
|
178
141
|
};
|
|
179
|
-
async catch(error) {
|
|
180
|
-
if (error instanceof PushPolicyGateError) {
|
|
181
|
-
if (this.isJsonOutput())
|
|
182
|
-
this.log(JSON.stringify({ imported: false, refused: error.payload }, null, 2));
|
|
183
|
-
this.error(error.message, { exit: 2 });
|
|
184
|
-
}
|
|
185
|
-
if (error instanceof FailedAfterImportError)
|
|
186
|
-
return this.catchAfterImport(error);
|
|
187
|
-
return this.catchAsOperational(error);
|
|
188
|
-
}
|
|
189
142
|
async run() {
|
|
190
143
|
const { flags } = await this.parse(Push);
|
|
191
|
-
if (blankPolicyOverride(flags['policy-override']))
|
|
192
|
-
this.error(BLANK_POLICY_OVERRIDE);
|
|
193
144
|
const { profile, profileName } = this.resolveProfile(flags);
|
|
194
145
|
// Determine workspace_id from flag or profile
|
|
195
146
|
let workspaceId;
|
|
@@ -211,23 +162,12 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
211
162
|
if (!fs.statSync(inputDir).isDirectory()) {
|
|
212
163
|
this.error(`Not a directory: ${inputDir}`);
|
|
213
164
|
}
|
|
214
|
-
const branch = flags.branch
|
|
165
|
+
const branch = flags.branch || profile.branch || '';
|
|
215
166
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}`;
|
|
216
|
-
const json = flags.output === 'json';
|
|
217
|
-
// Only the import writes Version History entries, so only the import carries the message.
|
|
218
|
-
const message = flags.message?.trim();
|
|
219
167
|
const target = {
|
|
220
168
|
buildDryRunUrl: (params) => `${baseUrl}/multidoc/dry-run?${params.toString()}`,
|
|
221
|
-
buildPushUrl(params) {
|
|
222
|
-
const query = new URLSearchParams(params);
|
|
223
|
-
if (message)
|
|
224
|
-
query.set('message', message);
|
|
225
|
-
if (flags['policy-override']?.trim())
|
|
226
|
-
query.set('override_reason', flags['policy-override'].trim());
|
|
227
|
-
return `${baseUrl}/multidoc?${query.toString()}`;
|
|
228
|
-
},
|
|
169
|
+
buildPushUrl: (params) => `${baseUrl}/multidoc?${params.toString()}`,
|
|
229
170
|
cliVersion: this.config.version,
|
|
230
|
-
explainRefusal: (status, payload) => (status === 403 ? policyFilePushGuidance(payload) : policyCodeGuidance(payload) || undefined),
|
|
231
171
|
instanceOrigin: profile.instance_origin,
|
|
232
172
|
label: `workspace ${workspaceId}`,
|
|
233
173
|
supportsBranches: true,
|
|
@@ -247,7 +187,7 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
247
187
|
truncate: flags.truncate,
|
|
248
188
|
verbose: flags.verbose,
|
|
249
189
|
};
|
|
250
|
-
|
|
190
|
+
await executePush({
|
|
251
191
|
accessToken: profile.access_token,
|
|
252
192
|
branch,
|
|
253
193
|
command: this,
|
|
@@ -256,66 +196,7 @@ Full sync including knowledge files; removes server objects not present locally
|
|
|
256
196
|
listUrl: () => `${baseUrl}/knowledge/sync`,
|
|
257
197
|
rootDir: inputDir,
|
|
258
198
|
},
|
|
259
|
-
// Under `-o json` stdout carries one JSON document, so progress goes to stderr.
|
|
260
|
-
log: json ? this.logToStderr.bind(this) : undefined,
|
|
261
199
|
verboseFetch: this.verboseFetch.bind(this),
|
|
262
200
|
}, target, pushFlags);
|
|
263
|
-
if (json) {
|
|
264
|
-
this.log(JSON.stringify(result.stopped
|
|
265
|
-
? { imported: false, preview: result.preview, reason: result.stopped }
|
|
266
|
-
: importDocument(result), null, 2));
|
|
267
|
-
}
|
|
268
|
-
// Policy feedback describes the multidoc import, so a push that imported none has none. Its
|
|
269
|
-
// preview may still say that the policy files are left out.
|
|
270
|
-
if (result.response) {
|
|
271
|
-
this.reportPolicyFeedback(result, json);
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
const skipped = policiesSkipped(result);
|
|
275
|
-
if (skipped)
|
|
276
|
-
this.warn(skipped);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* A failure after the import landed. The import stands, so its policy feedback is reported as for
|
|
281
|
-
* any push and a blocking finding still exits 2; otherwise the failure exits 1. Under `-o json`
|
|
282
|
-
* stdout holds the import's document with the failure as `error`.
|
|
283
|
-
*/
|
|
284
|
-
catchAfterImport(error) {
|
|
285
|
-
const json = this.isJsonOutput();
|
|
286
|
-
const exit = policyExitCode(policyCheck(error.imported)) || 1;
|
|
287
|
-
if (json)
|
|
288
|
-
this.log(JSON.stringify({ ...importDocument(error.imported), error: { exit, message: error.message } }, null, 2));
|
|
289
|
-
this.reportPolicyFeedback(error.imported, json);
|
|
290
|
-
this.error(error, { exit });
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* What happened to the policy documents, then the policy check the import answered with. When the
|
|
294
|
-
* platform left the policy files out, its notice says so instead of a count of what was sent.
|
|
295
|
-
*/
|
|
296
|
-
reportPolicyFeedback(result, json) {
|
|
297
|
-
const check = policyCheck(result);
|
|
298
|
-
const skipped = policiesSkipped(result);
|
|
299
|
-
if (skipped)
|
|
300
|
-
this.warn(skipped);
|
|
301
|
-
if (!json) {
|
|
302
|
-
if (!skipped) {
|
|
303
|
-
const sentPolicies = result.sent.filter((entry) => parseDocument(entry.content)?.type === 'policy').length;
|
|
304
|
-
for (const line of policyDocumentSummary(result.preview, sentPolicies))
|
|
305
|
-
this.log(line);
|
|
306
|
-
}
|
|
307
|
-
for (const line of policySummary(check, pushEvidence(check)))
|
|
308
|
-
this.log(line);
|
|
309
|
-
}
|
|
310
|
-
const warning = policyCheckWarning(check);
|
|
311
|
-
if (warning)
|
|
312
|
-
this.warn(warning);
|
|
313
|
-
const code = policyExitCode(check);
|
|
314
|
-
if (code) {
|
|
315
|
-
process.exitCode = code;
|
|
316
|
-
if (!json)
|
|
317
|
-
this.log('Next: `xano policy status --run-detail` for the current standing, or `xano policy runs` for this run.');
|
|
318
|
-
this.warn('Workspace import completed with blocking policy findings; the imported changes were not rolled back.');
|
|
319
|
-
}
|
|
320
201
|
}
|
|
321
202
|
}
|
|
@@ -11,16 +11,6 @@ export interface ParsedDocument {
|
|
|
11
11
|
type: string;
|
|
12
12
|
verb?: string;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
* The platform's policy key pattern, kept offline for file names. A command that already holds the
|
|
16
|
-
* catalogue validates a key against the catalogue's `document.key.pattern` instead.
|
|
17
|
-
*/
|
|
18
|
-
export declare const POLICY_KEY_PATTERN: RegExp;
|
|
19
|
-
/**
|
|
20
|
-
* The file name, without `.xs`, for a policy key. The pattern is the platform's own key pattern, so a
|
|
21
|
-
* key from the server can never name a path outside `policies/`.
|
|
22
|
-
*/
|
|
23
|
-
export declare function policyBaseName(key: string): string;
|
|
24
14
|
/**
|
|
25
15
|
* Parse a single XanoScript document to extract its type, name, and optional verb/api_group.
|
|
26
16
|
* Skips leading comment lines (starting with //) to find the first meaningful line.
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The platform's policy key pattern, kept offline for file names. A command that already holds the
|
|
3
|
-
* catalogue validates a key against the catalogue's `document.key.pattern` instead.
|
|
4
|
-
*/
|
|
5
|
-
export const POLICY_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
6
|
-
/**
|
|
7
|
-
* The file name, without `.xs`, for a policy key. The pattern is the platform's own key pattern, so a
|
|
8
|
-
* key from the server can never name a path outside `policies/`.
|
|
9
|
-
*/
|
|
10
|
-
export function policyBaseName(key) {
|
|
11
|
-
if (!POLICY_KEY_PATTERN.test(key))
|
|
12
|
-
throw new Error(`Invalid policy key: ${key}`);
|
|
13
|
-
return key;
|
|
14
|
-
}
|
|
15
1
|
/**
|
|
16
2
|
* Parse a single XanoScript document to extract its type, name, and optional verb/api_group.
|
|
17
3
|
* Skips leading comment lines (starting with //) to find the first meaningful line.
|
|
@@ -47,9 +33,6 @@ export function parseDocument(content) {
|
|
|
47
33
|
if (name.startsWith('"') && name.endsWith('"')) {
|
|
48
34
|
name = name.slice(1, -1);
|
|
49
35
|
}
|
|
50
|
-
// Policy bodies are opaque. Prose/parameters may mention guid or api_group; these are not document identity.
|
|
51
|
-
if (type === 'policy')
|
|
52
|
-
return { content, name, type };
|
|
53
36
|
// Extract verb if present (e.g., verb=GET)
|
|
54
37
|
let verb;
|
|
55
38
|
const verbMatch = rest.match(/verb=(\S+)/);
|
|
@@ -179,10 +162,6 @@ export function sanitizeDocumentName(name, snakeCaseFn) {
|
|
|
179
162
|
export function resolveDocumentPath(doc, outputDir, deps) {
|
|
180
163
|
const { getApiGroupFolder, getChannelServer, join, snakeCase } = deps;
|
|
181
164
|
const sanitize = (name) => sanitizeDocumentName(name, snakeCase);
|
|
182
|
-
if (doc.type === 'policy') {
|
|
183
|
-
// policy → policies/{key}.xs (the key is validated, never snake_cased)
|
|
184
|
-
return { baseName: policyBaseName(doc.name), typeDir: join(outputDir, 'policies') };
|
|
185
|
-
}
|
|
186
165
|
if (doc.type === 'workspace') {
|
|
187
166
|
// workspace → workspace/{name}.xs
|
|
188
167
|
return { baseName: sanitize(doc.name), typeDir: join(outputDir, 'workspace') };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
2
|
import { type BadIndex, type BadReference } from './reference-checker.js';
|
|
3
3
|
export interface PushFlags {
|
|
4
4
|
delete: boolean;
|
|
@@ -21,11 +21,6 @@ export interface PushTarget {
|
|
|
21
21
|
buildPushUrl: (queryParams: URLSearchParams) => string;
|
|
22
22
|
/** CLI version string */
|
|
23
23
|
cliVersion: string;
|
|
24
|
-
/**
|
|
25
|
-
* Guidance for a refusal this target recognises, given the HTTP status and the server's payload.
|
|
26
|
-
* A preview or import refused that way stops with the message and this guidance appended.
|
|
27
|
-
*/
|
|
28
|
-
explainRefusal?: (status: number, payload: unknown) => string | undefined;
|
|
29
24
|
/** Instance origin URL (e.g., "https://x123-abcd-1234.xano.io") */
|
|
30
25
|
instanceOrigin: string;
|
|
31
26
|
/** Human-readable label for log messages (e.g., "sandbox environment", "workspace 40") */
|
|
@@ -59,56 +54,8 @@ export interface PushContext {
|
|
|
59
54
|
inputDir: string;
|
|
60
55
|
/** Optional knowledge sync config. Only workspace push sets this. */
|
|
61
56
|
knowledge?: KnowledgeConfig;
|
|
62
|
-
/** Where progress and diagnostics go; defaults to the command's stdout. */
|
|
63
|
-
log?: (message: string) => void;
|
|
64
57
|
verboseFetch: (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
|
|
65
58
|
}
|
|
66
|
-
export interface DryRunSummary {
|
|
67
|
-
created: number;
|
|
68
|
-
deleted: number;
|
|
69
|
-
truncated: number;
|
|
70
|
-
unchanged: number;
|
|
71
|
-
updated: number;
|
|
72
|
-
}
|
|
73
|
-
export interface DryRunOperation {
|
|
74
|
-
action: string;
|
|
75
|
-
details: string;
|
|
76
|
-
name: string;
|
|
77
|
-
reason?: string;
|
|
78
|
-
type: string;
|
|
79
|
-
}
|
|
80
|
-
export interface DryRunResult {
|
|
81
|
-
operations: DryRunOperation[];
|
|
82
|
-
/** True when the sandbox currently holds a different source workspace than the one being pushed. */
|
|
83
|
-
source_workspace_mismatch?: boolean;
|
|
84
|
-
summary: Record<string, DryRunSummary>;
|
|
85
|
-
workspace_name?: string;
|
|
86
|
-
}
|
|
87
|
-
/** What a push did, for the calling command to report. */
|
|
88
|
-
export interface PushResult {
|
|
89
|
-
knowledge: {
|
|
90
|
-
deleted: number;
|
|
91
|
-
imported: number;
|
|
92
|
-
};
|
|
93
|
-
preview: DryRunResult | null;
|
|
94
|
-
/** The multidoc import's response body; absent when no multidoc was imported. */
|
|
95
|
-
response?: Record<string, unknown>;
|
|
96
|
-
/** The documents the multidoc import carried. */
|
|
97
|
-
sent: Array<{
|
|
98
|
-
content: string;
|
|
99
|
-
filePath: string;
|
|
100
|
-
}>;
|
|
101
|
-
/** Why the push stopped before writing anything, when it did. */
|
|
102
|
-
stopped?: 'blocked' | 'cancelled' | 'dry-run' | 'no-changes';
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* A push that failed after its multidoc import landed. The import is not rolled back, so the error
|
|
106
|
-
* carries what it did for the calling command to report beside the failure.
|
|
107
|
-
*/
|
|
108
|
-
export declare class FailedAfterImportError extends Errors.CLIError {
|
|
109
|
-
readonly imported: PushResult;
|
|
110
|
-
constructor(message: string, imported: PushResult);
|
|
111
|
-
}
|
|
112
59
|
/**
|
|
113
60
|
* Filter document entries down to the ones the dry-run preview reports as changed,
|
|
114
61
|
* used to send only the changed documents during a partial (non-`--sync`) push.
|
|
@@ -245,7 +192,7 @@ export declare function confirm(message: string): Promise<boolean>;
|
|
|
245
192
|
* Execute a multidoc push with preview, validation, partial mode, and GUID sync.
|
|
246
193
|
* Shared by both sandbox:push and workspace:push commands.
|
|
247
194
|
*/
|
|
248
|
-
export declare function executePush(ctx: PushContext, target: PushTarget, flags: PushFlags): Promise<
|
|
195
|
+
export declare function executePush(ctx: PushContext, target: PushTarget, flags: PushFlags): Promise<void>;
|
|
249
196
|
/**
|
|
250
197
|
* Turn a thrown fetch/network error into an actionable message.
|
|
251
198
|
*
|
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ux } from '@oclif/core';
|
|
2
2
|
import { minimatch } from 'minimatch';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { buildDocumentKey, findFilesWithGuid, parseDocument } from './document-parser.js';
|
|
6
6
|
import { flattenBundleFile } from './flatten.js';
|
|
7
7
|
import { collectKnowledgeObjects, fetchKnowledge, knowledgePreview, pushKnowledge, syncGuidToFrontmatter, toPushItems, } from './knowledge-sync.js';
|
|
8
|
-
import { policyWeakeningGuidance, WEAKENING_REFUSAL } from './policy/permission.js';
|
|
9
|
-
import { PushPolicyGateError } from './policy/push-gate.js';
|
|
10
8
|
import { checkReferences, checkTableIndexes } from './reference-checker.js';
|
|
11
|
-
/**
|
|
12
|
-
* A push that failed after its multidoc import landed. The import is not rolled back, so the error
|
|
13
|
-
* carries what it did for the calling command to report beside the failure.
|
|
14
|
-
*/
|
|
15
|
-
export class FailedAfterImportError extends Errors.CLIError {
|
|
16
|
-
imported;
|
|
17
|
-
constructor(message, imported) {
|
|
18
|
-
super(message);
|
|
19
|
-
this.imported = imported;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
9
|
/**
|
|
23
10
|
* Filter document entries down to the ones the dry-run preview reports as changed,
|
|
24
11
|
* used to send only the changed documents during a partial (non-`--sync`) push.
|
|
@@ -592,9 +579,7 @@ function mergeKnowledgePreview(preview, knowledge) {
|
|
|
592
579
|
*/
|
|
593
580
|
export async function executePush(ctx, target, flags) {
|
|
594
581
|
const { accessToken, command, inputDir, verboseFetch } = ctx;
|
|
595
|
-
const log =
|
|
596
|
-
let dryRunPreview = null;
|
|
597
|
-
const stop = (stopped) => ({ knowledge: { deleted: 0, imported: 0 }, preview: dryRunPreview, sent: [], stopped });
|
|
582
|
+
const log = command.log.bind(command);
|
|
598
583
|
// ── Collect knowledge entries (before file check so knowledge-only push works) ─
|
|
599
584
|
let knowledgeObjects = [];
|
|
600
585
|
if (ctx.knowledge) {
|
|
@@ -653,7 +638,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
653
638
|
const doFlatten = await confirm('Flatten these file(s) in place and continue the push?');
|
|
654
639
|
if (!doFlatten) {
|
|
655
640
|
log('Push cancelled. Run `xano flatten <file>` yourself, or re-run to be prompted again.');
|
|
656
|
-
return
|
|
641
|
+
return;
|
|
657
642
|
}
|
|
658
643
|
}
|
|
659
644
|
else {
|
|
@@ -756,6 +741,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
756
741
|
'Content-Type': 'text/x-xanoscript',
|
|
757
742
|
};
|
|
758
743
|
// ── Dry-run / Preview ─────────────────────────────────────────────────
|
|
744
|
+
let dryRunPreview = null;
|
|
759
745
|
const dryRunUrl = knowledgeOnly ? null : target.buildDryRunUrl(queryParams);
|
|
760
746
|
if (dryRunUrl && (flags['dry-run'] || !flags.force)) {
|
|
761
747
|
const dryRunParams = new URLSearchParams(queryParams);
|
|
@@ -847,7 +833,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
847
833
|
log('');
|
|
848
834
|
log(ux.colorize('red', `Push blocked: ${criticalOps.length} critical error(s) found.`));
|
|
849
835
|
if (!flags.force) {
|
|
850
|
-
return
|
|
836
|
+
return;
|
|
851
837
|
}
|
|
852
838
|
log(ux.colorize('yellow', 'Proceeding anyway due to --force flag.'));
|
|
853
839
|
}
|
|
@@ -877,10 +863,10 @@ export async function executePush(ctx, target, flags) {
|
|
|
877
863
|
if (!hasChanges && !hasLocalRecords) {
|
|
878
864
|
log('');
|
|
879
865
|
log('No changes to push.');
|
|
880
|
-
return
|
|
866
|
+
return;
|
|
881
867
|
}
|
|
882
868
|
if (flags['dry-run']) {
|
|
883
|
-
return
|
|
869
|
+
return;
|
|
884
870
|
}
|
|
885
871
|
// Warn when the sandbox currently holds a different source workspace than the one being
|
|
886
872
|
// pushed. The backend compares the source workspace id we sent against the id stored on
|
|
@@ -898,7 +884,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
898
884
|
const proceed = await confirm('Continue with push anyway?');
|
|
899
885
|
if (!proceed) {
|
|
900
886
|
log('Push cancelled. Run `xano sandbox reset` then retry.');
|
|
901
|
-
return
|
|
887
|
+
return;
|
|
902
888
|
}
|
|
903
889
|
mismatchConfirmed = true;
|
|
904
890
|
}
|
|
@@ -919,7 +905,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
919
905
|
const confirmed = await confirm(message);
|
|
920
906
|
if (!confirmed) {
|
|
921
907
|
log('Push cancelled.');
|
|
922
|
-
return
|
|
908
|
+
return;
|
|
923
909
|
}
|
|
924
910
|
}
|
|
925
911
|
else {
|
|
@@ -936,17 +922,15 @@ export async function executePush(ctx, target, flags) {
|
|
|
936
922
|
}
|
|
937
923
|
}
|
|
938
924
|
else {
|
|
939
|
-
await handleDryRunError(dryRunResponse, command,
|
|
925
|
+
await handleDryRunError(dryRunResponse, command, flags, target);
|
|
940
926
|
// If we get here, the user confirmed to proceed without preview
|
|
941
927
|
}
|
|
942
928
|
}
|
|
943
929
|
catch (error) {
|
|
944
930
|
// Ctrl+C or SIGINT
|
|
945
|
-
if (error instanceof PushPolicyGateError)
|
|
946
|
-
throw error;
|
|
947
931
|
if (error.name === 'AbortError' || error.code === 'ERR_USE_AFTER_CLOSE') {
|
|
948
932
|
log('\nPush cancelled.');
|
|
949
|
-
return
|
|
933
|
+
return;
|
|
950
934
|
}
|
|
951
935
|
// Re-throw oclif errors
|
|
952
936
|
if (error instanceof Error && 'oclif' in error) {
|
|
@@ -977,16 +961,16 @@ export async function executePush(ctx, target, flags) {
|
|
|
977
961
|
if (!hasChanges) {
|
|
978
962
|
log('');
|
|
979
963
|
log('No changes to push.');
|
|
980
|
-
return
|
|
964
|
+
return;
|
|
981
965
|
}
|
|
982
966
|
if (flags['dry-run']) {
|
|
983
|
-
return
|
|
967
|
+
return;
|
|
984
968
|
}
|
|
985
969
|
if (process.stdin.isTTY) {
|
|
986
970
|
const confirmed = await confirm('Proceed with push?');
|
|
987
971
|
if (!confirmed) {
|
|
988
972
|
log('Push cancelled.');
|
|
989
|
-
return
|
|
973
|
+
return;
|
|
990
974
|
}
|
|
991
975
|
}
|
|
992
976
|
else {
|
|
@@ -1002,19 +986,17 @@ export async function executePush(ctx, target, flags) {
|
|
|
1002
986
|
}
|
|
1003
987
|
}
|
|
1004
988
|
// ── Partial push: filter to changed documents only ────────────────────
|
|
1005
|
-
let sent = documentEntries;
|
|
1006
989
|
if (!knowledgeOnly && isPartial && dryRunPreview) {
|
|
1007
|
-
|
|
1008
|
-
if (
|
|
990
|
+
const filteredEntries = filterChangedEntries(documentEntries, dryRunPreview.operations, flags.records);
|
|
991
|
+
if (filteredEntries.length === 0 && knowledgeObjects.length === 0) {
|
|
1009
992
|
log('No changes to push.');
|
|
1010
|
-
return
|
|
993
|
+
return;
|
|
1011
994
|
}
|
|
1012
|
-
multidoc =
|
|
995
|
+
multidoc = filteredEntries.length > 0 ? filteredEntries.map((d) => d.content).join('\n---\n') : '';
|
|
1013
996
|
}
|
|
1014
997
|
// ── Execute the actual push ───────────────────────────────────────────
|
|
1015
998
|
const startTime = Date.now();
|
|
1016
999
|
let pushedDocCount = 0;
|
|
1017
|
-
let pushResponse;
|
|
1018
1000
|
if (!knowledgeOnly && multidoc) {
|
|
1019
1001
|
const apiUrl = target.buildPushUrl(queryParams);
|
|
1020
1002
|
try {
|
|
@@ -1024,19 +1006,14 @@ export async function executePush(ctx, target, flags) {
|
|
|
1024
1006
|
method: 'POST',
|
|
1025
1007
|
}, flags.verbose, accessToken);
|
|
1026
1008
|
if (!response.ok) {
|
|
1027
|
-
|
|
1028
|
-
refuseIfExplained(command, target, response.status, errorText);
|
|
1029
|
-
handlePushError(response, errorText, documentEntries, inputDir, command);
|
|
1009
|
+
handlePushError(response, await response.text(), documentEntries, inputDir, command);
|
|
1030
1010
|
}
|
|
1031
1011
|
// Parse response for GUID map
|
|
1032
1012
|
const responseText = await response.text();
|
|
1033
1013
|
let guidMap = [];
|
|
1034
|
-
pushResponse = {};
|
|
1035
1014
|
if (responseText && responseText !== 'null') {
|
|
1036
1015
|
try {
|
|
1037
1016
|
const responseJson = JSON.parse(responseText);
|
|
1038
|
-
if (responseJson && typeof responseJson === 'object')
|
|
1039
|
-
pushResponse = responseJson;
|
|
1040
1017
|
if (responseJson?.guid_map && Array.isArray(responseJson.guid_map)) {
|
|
1041
1018
|
guidMap = responseJson.guid_map;
|
|
1042
1019
|
}
|
|
@@ -1062,7 +1039,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
1062
1039
|
let updatedCount = 0;
|
|
1063
1040
|
let canonicalCount = 0;
|
|
1064
1041
|
for (const entry of guidMap) {
|
|
1065
|
-
if (!entry.guid
|
|
1042
|
+
if (!entry.guid)
|
|
1066
1043
|
continue;
|
|
1067
1044
|
const key = buildDocumentKey(entry.type, entry.name, entry.verb, entry.api_group);
|
|
1068
1045
|
let filePath = documentFileMap.get(key);
|
|
@@ -1108,8 +1085,6 @@ export async function executePush(ctx, target, flags) {
|
|
|
1108
1085
|
pushedDocCount = multidoc.split('\n---\n').length;
|
|
1109
1086
|
}
|
|
1110
1087
|
catch (error) {
|
|
1111
|
-
if (error instanceof PushPolicyGateError)
|
|
1112
|
-
throw error;
|
|
1113
1088
|
if (error instanceof Error && 'oclif' in error)
|
|
1114
1089
|
throw error;
|
|
1115
1090
|
const elapsedMs = Date.now() - startTime;
|
|
@@ -1119,12 +1094,6 @@ export async function executePush(ctx, target, flags) {
|
|
|
1119
1094
|
// ── Push knowledge ────────────────────────────────────────────────────
|
|
1120
1095
|
let knowledgeImported = 0;
|
|
1121
1096
|
let knowledgeDeleted = 0;
|
|
1122
|
-
const pushed = () => ({
|
|
1123
|
-
knowledge: { deleted: knowledgeDeleted, imported: knowledgeImported },
|
|
1124
|
-
preview: dryRunPreview,
|
|
1125
|
-
response: pushResponse,
|
|
1126
|
-
sent: pushResponse ? sent : [],
|
|
1127
|
-
});
|
|
1128
1097
|
if (ctx.knowledge && (knowledgeObjects.length > 0 || shouldDelete)) {
|
|
1129
1098
|
const listUrl = ctx.knowledge.listUrl();
|
|
1130
1099
|
try {
|
|
@@ -1162,10 +1131,7 @@ export async function executePush(ctx, target, flags) {
|
|
|
1162
1131
|
if (error instanceof Error && 'oclif' in error)
|
|
1163
1132
|
throw error;
|
|
1164
1133
|
const elapsedMs = Date.now() - startTime;
|
|
1165
|
-
|
|
1166
|
-
if (pushResponse)
|
|
1167
|
-
throw new FailedAfterImportError(message, pushed());
|
|
1168
|
-
command.error(message);
|
|
1134
|
+
command.error(`Failed to push knowledge: ${describeNetworkError(error, listUrl, elapsedMs)}`);
|
|
1169
1135
|
}
|
|
1170
1136
|
}
|
|
1171
1137
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -1179,7 +1145,6 @@ export async function executePush(ctx, target, flags) {
|
|
|
1179
1145
|
parts.push(kParts.join(', '));
|
|
1180
1146
|
}
|
|
1181
1147
|
log(`Pushed ${parts.join(' + ')} to ${target.label} from ${relative(process.cwd(), inputDir) || inputDir} in ${elapsed}s`);
|
|
1182
|
-
return pushed();
|
|
1183
1148
|
}
|
|
1184
1149
|
// ── Error Handlers ──────────────────────────────────────────────────────────
|
|
1185
1150
|
/**
|
|
@@ -1258,43 +1223,10 @@ function formatFailureDuration(elapsedMs) {
|
|
|
1258
1223
|
? ` (failed after ~${human}, near a common ${boundaries.find((b) => Math.abs(totalSeconds - b) <= b * 0.05)}s timeout — likely a server or proxy cutoff)`
|
|
1259
1224
|
: ` (failed after ${human})`;
|
|
1260
1225
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
let message = body;
|
|
1264
|
-
let payload;
|
|
1265
|
-
try {
|
|
1266
|
-
const parsed = JSON.parse(body);
|
|
1267
|
-
if (typeof parsed?.message === 'string')
|
|
1268
|
-
message = parsed.message;
|
|
1269
|
-
payload = parsed?.payload;
|
|
1270
|
-
}
|
|
1271
|
-
catch {
|
|
1272
|
-
// Not JSON
|
|
1273
|
-
}
|
|
1274
|
-
const refusal = payload;
|
|
1275
|
-
if (status === 403 && refusal?.code === 'policy_gate')
|
|
1276
|
-
throw new PushPolicyGateError(refusal);
|
|
1277
|
-
// Policy files that weaken a mandatory policy of the live branch, from a credential without
|
|
1278
|
-
// workspace:policy update: a permission refusal (exit 1), not a blocking finding. The platform's
|
|
1279
|
-
// sentence already reads "Push refused: … Nothing was imported.", so it is printed as it is.
|
|
1280
|
-
if (status === 403 && refusal?.code === WEAKENING_REFUSAL) {
|
|
1281
|
-
command.error(`${message}${target.explainRefusal?.(status, payload) ?? policyWeakeningGuidance(payload, [], true)}`);
|
|
1282
|
-
}
|
|
1283
|
-
// The plan's policy cap: the platform's message names the plan, its cap and the remedy, and the
|
|
1284
|
-
// push itself would be refused the same way, so the preview stops here rather than skipping.
|
|
1285
|
-
if (status === 403 && refusal?.code === 'policy_plan_limit')
|
|
1286
|
-
command.error(`Push refused (${status}): ${message}`);
|
|
1287
|
-
if (refusal?.code === 'policy_gate_transaction_required') {
|
|
1288
|
-
command.error('Push refused: the live branch policy gate requires a transaction. Remove --no-transaction and retry. Nothing was imported.');
|
|
1289
|
-
}
|
|
1290
|
-
const guidance = target.explainRefusal?.(status, payload);
|
|
1291
|
-
if (guidance)
|
|
1292
|
-
command.error(`Push refused (${status}): ${message}${guidance}`);
|
|
1293
|
-
}
|
|
1294
|
-
async function handleDryRunError(response, command, target, { log, verbose }) {
|
|
1295
|
-
const errorText = await response.text();
|
|
1296
|
-
refuseIfExplained(command, target, response.status, errorText);
|
|
1226
|
+
async function handleDryRunError(response, command, flags, target) {
|
|
1227
|
+
const log = command.log.bind(command);
|
|
1297
1228
|
if (response.status === 404) {
|
|
1229
|
+
const errorText = await response.text();
|
|
1298
1230
|
try {
|
|
1299
1231
|
const errorJson = JSON.parse(errorText);
|
|
1300
1232
|
if (errorJson.message) {
|
|
@@ -1312,6 +1244,7 @@ async function handleDryRunError(response, command, target, { log, verbose }) {
|
|
|
1312
1244
|
log('');
|
|
1313
1245
|
}
|
|
1314
1246
|
else {
|
|
1247
|
+
const errorText = await response.text();
|
|
1315
1248
|
// Check if push is disabled
|
|
1316
1249
|
try {
|
|
1317
1250
|
const errorJson = JSON.parse(errorText);
|
|
@@ -1334,17 +1267,8 @@ async function handleDryRunError(response, command, target, { log, verbose }) {
|
|
|
1334
1267
|
catch {
|
|
1335
1268
|
// Not JSON, fall through
|
|
1336
1269
|
}
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
const errorJson = JSON.parse(errorText);
|
|
1340
|
-
if (typeof errorJson?.message === 'string' && errorJson.message.trim())
|
|
1341
|
-
reason = `: ${errorJson.message.trim().replace(/\.$/, '')}`;
|
|
1342
|
-
}
|
|
1343
|
-
catch {
|
|
1344
|
-
// Not JSON
|
|
1345
|
-
}
|
|
1346
|
-
command.warn(`Push preview failed (${response.status})${reason}. Skipping preview.`);
|
|
1347
|
-
if (verbose) {
|
|
1270
|
+
command.warn(`Push preview failed (${response.status}). Skipping preview.`);
|
|
1271
|
+
if (flags.verbose) {
|
|
1348
1272
|
log(ux.colorize('dim', errorText));
|
|
1349
1273
|
}
|
|
1350
1274
|
}
|