@xano/cli 1.3.1-beta.1 → 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 +1 -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/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 +3142 -6141
- 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
|
@@ -14,14 +14,4 @@ export default class Pull extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
|
-
/**
|
|
18
|
-
* Policy keys are unique on the server without regard to case, but an older export or a local file
|
|
19
|
-
* can still differ only in case, and file names are not case-sensitive on every checkout. The
|
|
20
|
-
* policies whose file name differs only in case from another exported policy, or from a local file
|
|
21
|
-
* it would overwrite, are left out with a warning; everything else is written. Local policy files
|
|
22
|
-
* absent from the export are kept. They are reported as stale only when this credential reads the
|
|
23
|
-
* branch's policies, because an export leaves out the policies its credential cannot read, and
|
|
24
|
-
* every policy while the Policies feature is off.
|
|
25
|
-
*/
|
|
26
|
-
private policyClashes;
|
|
27
17
|
}
|
|
@@ -3,9 +3,8 @@ import snakeCase from 'lodash.snakecase';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument,
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
8
|
-
import { policyListing } from '../../../utils/policy/request.js';
|
|
9
8
|
export default class Pull extends BaseCommand {
|
|
10
9
|
static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
|
|
11
10
|
static examples = [
|
|
@@ -28,7 +27,7 @@ Pulled 58 documents
|
|
|
28
27
|
...BaseCommand.baseFlags,
|
|
29
28
|
branch: Flags.string({
|
|
30
29
|
char: 'b',
|
|
31
|
-
description:
|
|
30
|
+
description: 'Branch name (optional if set in profile, defaults to live)',
|
|
32
31
|
required: false,
|
|
33
32
|
}),
|
|
34
33
|
directory: Flags.string({
|
|
@@ -74,8 +73,8 @@ Pulled 58 documents
|
|
|
74
73
|
` 1. Provide it as a flag: xano workspace pull -w <workspace_id>\n` +
|
|
75
74
|
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
76
75
|
}
|
|
77
|
-
// Determine branch from flag or profile
|
|
78
|
-
const branch = flags.branch
|
|
76
|
+
// Determine branch from flag or profile
|
|
77
|
+
const branch = flags.branch || profile.branch || '';
|
|
79
78
|
// Build query parameters
|
|
80
79
|
const queryParams = new URLSearchParams({
|
|
81
80
|
branch,
|
|
@@ -124,13 +123,12 @@ Pulled 58 documents
|
|
|
124
123
|
documents.push(parsed);
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
|
-
// Resolve the output directory
|
|
128
|
-
const outputDir = path.resolve(flags.directory);
|
|
129
|
-
const clashing = await this.policyClashes(documents, outputDir, () => policyListing({ logToStderr: (message) => this.logToStderr(message), verboseFetch: (...args) => this.verboseFetch(...args) }, { branch, profile, verbose: flags.verbose, workspace: workspaceId }));
|
|
130
126
|
if (documents.length === 0) {
|
|
131
127
|
this.log('No documents found in response');
|
|
132
128
|
return;
|
|
133
129
|
}
|
|
130
|
+
// Resolve the output directory
|
|
131
|
+
const outputDir = path.resolve(flags.directory);
|
|
134
132
|
// Create the output directory if it doesn't exist
|
|
135
133
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
136
134
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
@@ -142,7 +140,7 @@ Pulled 58 documents
|
|
|
142
140
|
// Track filenames per type to handle duplicates
|
|
143
141
|
const filenameCounters = new Map();
|
|
144
142
|
let writtenCount = 0;
|
|
145
|
-
for (const doc of documents
|
|
143
|
+
for (const doc of documents) {
|
|
146
144
|
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
147
145
|
getApiGroupFolder,
|
|
148
146
|
getChannelServer,
|
|
@@ -175,66 +173,5 @@ Pulled 58 documents
|
|
|
175
173
|
if (knowledgeCount > 0)
|
|
176
174
|
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
177
175
|
this.log(`Pulled ${parts.join(' + ')} to ${flags.directory}`);
|
|
178
|
-
// Policy files state the rules; the skill the instance generates tells an agent how to follow them.
|
|
179
|
-
// Only a pull that carried policy files says so: an instance with the Policies feature off exports
|
|
180
|
-
// none, and its skill route answers 403.
|
|
181
|
-
if (documents.some((doc) => doc.type === 'policy')) {
|
|
182
|
-
this.log('Run `xano skills pull` to install the policies skill for your coding agent.');
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Policy keys are unique on the server without regard to case, but an older export or a local file
|
|
187
|
-
* can still differ only in case, and file names are not case-sensitive on every checkout. The
|
|
188
|
-
* policies whose file name differs only in case from another exported policy, or from a local file
|
|
189
|
-
* it would overwrite, are left out with a warning; everything else is written. Local policy files
|
|
190
|
-
* absent from the export are kept. They are reported as stale only when this credential reads the
|
|
191
|
-
* branch's policies, because an export leaves out the policies its credential cannot read, and
|
|
192
|
-
* every policy while the Policies feature is off.
|
|
193
|
-
*/
|
|
194
|
-
async policyClashes(documents, outputDir, readsPolicies) {
|
|
195
|
-
const exported = new Map();
|
|
196
|
-
for (const doc of documents.filter(doc => doc.type === 'policy')) {
|
|
197
|
-
const filename = `${policyBaseName(doc.name)}.xs`;
|
|
198
|
-
exported.set(filename.toLowerCase(), [...(exported.get(filename.toLowerCase()) ?? []), filename]);
|
|
199
|
-
}
|
|
200
|
-
const clashes = new Map();
|
|
201
|
-
for (const [normalized, names] of exported) {
|
|
202
|
-
if (names.length > 1)
|
|
203
|
-
clashes.set(normalized, names.map(name => `policies/${name}`).join(' and '));
|
|
204
|
-
}
|
|
205
|
-
const policyDir = path.join(outputDir, 'policies');
|
|
206
|
-
const localFiles = fs.existsSync(policyDir)
|
|
207
|
-
? fs.readdirSync(policyDir, { withFileTypes: true })
|
|
208
|
-
.filter(entry => entry.isFile() && entry.name.toLowerCase().endsWith('.xs'))
|
|
209
|
-
.map(entry => entry.name)
|
|
210
|
-
.sort()
|
|
211
|
-
: [];
|
|
212
|
-
const stale = [];
|
|
213
|
-
for (const filename of localFiles) {
|
|
214
|
-
const names = exported.get(filename.toLowerCase());
|
|
215
|
-
if (!names)
|
|
216
|
-
stale.push(`policies/${filename}`);
|
|
217
|
-
else if (!names.includes(filename) && !clashes.has(filename.toLowerCase())) {
|
|
218
|
-
clashes.set(filename.toLowerCase(), `policies/${names[0]} (local policies/${filename})`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (clashes.size > 0) {
|
|
222
|
-
this.warn(`Policy files that differ only in case are left out of this pull; everything else is written:\n ${[...clashes.values()].join('\n ')}\n` +
|
|
223
|
-
'Rename one of the policies, or move the local file aside, then pull again.');
|
|
224
|
-
}
|
|
225
|
-
if (stale.length > 0) {
|
|
226
|
-
// An export that carries a policy was made by a credential that reads them.
|
|
227
|
-
const listing = exported.size > 0 ? 'listed' : await readsPolicies();
|
|
228
|
-
if (listing === 'listed') {
|
|
229
|
-
this.warn(`Stale local policy files are absent from this export and were kept:\n ${stale.join('\n ')}\nReview them before pushing; a push can publish these policies again.`);
|
|
230
|
-
}
|
|
231
|
-
else if (listing === 'feature_off') {
|
|
232
|
-
this.log('Local policy files were kept: Policies are not enabled on this instance, so the export carries none.');
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
this.log(`Local policy files were kept: this credential cannot list this workspace's policies, and the export carries none.`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return new Set(documents.filter(doc => doc.type === 'policy' && clashes.has(`${doc.name}.xs`.toLowerCase())));
|
|
239
176
|
}
|
|
240
177
|
}
|
|
@@ -12,9 +12,6 @@ export default class Push extends BaseCommand {
|
|
|
12
12
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
13
|
guids: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
14
|
include: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
-
message: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
-
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
-
'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
15
|
records: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
16
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
17
|
transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
@@ -24,21 +21,5 @@ export default class Push extends BaseCommand {
|
|
|
24
21
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
22
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
26
23
|
};
|
|
27
|
-
protected catch(error: Error & {
|
|
28
|
-
oclif?: {
|
|
29
|
-
exit?: number;
|
|
30
|
-
};
|
|
31
|
-
}): Promise<void>;
|
|
32
24
|
run(): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* A failure after the import landed. The import stands, so its policy feedback is reported as for
|
|
35
|
-
* any push and a blocking finding still exits 2; otherwise the failure exits 1. Under `-o json`
|
|
36
|
-
* stdout holds the import's document with the failure as `error`.
|
|
37
|
-
*/
|
|
38
|
-
private catchAfterImport;
|
|
39
|
-
/**
|
|
40
|
-
* What happened to the policy documents, then the policy check the import answered with. When the
|
|
41
|
-
* platform left the policy files out, its notice says so instead of a count of what was sent.
|
|
42
|
-
*/
|
|
43
|
-
private reportPolicyFeedback;
|
|
44
25
|
}
|
|
@@ -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
|
*
|