@xano/cli 1.0.4 → 1.0.6-beta.0
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 +139 -4
- package/dist/base-command.d.ts +27 -0
- package/dist/base-command.js +124 -1
- package/dist/commands/auth/index.d.ts +45 -1
- package/dist/commands/auth/index.js +197 -29
- package/dist/commands/ephemeral/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/create/index.js +103 -0
- package/dist/commands/ephemeral/delete/index.d.ts +18 -0
- package/dist/commands/ephemeral/delete/index.js +99 -0
- package/dist/commands/ephemeral/edit/index.d.ts +18 -0
- package/dist/commands/ephemeral/edit/index.js +109 -0
- package/dist/commands/ephemeral/get/index.d.ts +21 -0
- package/dist/commands/ephemeral/get/index.js +125 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +19 -0
- package/dist/commands/ephemeral/impersonate/index.js +112 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +122 -0
- package/dist/commands/ephemeral/pull/index.d.ts +23 -0
- package/dist/commands/ephemeral/pull/index.js +248 -0
- package/dist/commands/{knowledge → ephemeral}/push/index.d.ts +8 -5
- package/dist/commands/ephemeral/push/index.js +143 -0
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -0
- package/dist/commands/knowledge/{pull → get}/index.d.ts +6 -2
- package/dist/commands/knowledge/get/index.js +126 -0
- package/dist/commands/knowledge/list/index.d.ts +18 -0
- package/dist/commands/knowledge/list/index.js +135 -0
- package/dist/commands/sandbox/pull/index.js +12 -2
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +19 -14
- package/dist/commands/sandbox/reset/index.js +12 -0
- package/dist/commands/static_host/build/create/index.d.ts +9 -1
- package/dist/commands/static_host/build/create/index.js +54 -4
- package/dist/commands/static_host/build/delete/index.d.ts +19 -0
- package/dist/commands/static_host/build/delete/index.js +114 -0
- package/dist/commands/static_host/build/get/index.d.ts +1 -1
- package/dist/commands/static_host/build/get/index.js +16 -10
- package/dist/commands/static_host/build/pull/index.d.ts +52 -0
- package/dist/commands/static_host/build/pull/index.js +300 -0
- package/dist/commands/static_host/build/push/index.d.ts +23 -0
- package/dist/commands/static_host/build/push/index.js +228 -0
- package/dist/commands/static_host/create/index.d.ts +17 -0
- package/dist/commands/static_host/create/index.js +86 -0
- package/dist/commands/static_host/deploy/index.d.ts +18 -0
- package/dist/commands/static_host/deploy/index.js +105 -0
- package/dist/commands/static_host/edit/index.d.ts +23 -0
- package/dist/commands/static_host/edit/index.js +151 -0
- package/dist/commands/static_host/get/index.d.ts +18 -0
- package/dist/commands/static_host/get/index.js +94 -0
- package/dist/commands/static_host/migrate/index.d.ts +44 -0
- package/dist/commands/static_host/migrate/index.js +205 -0
- package/dist/commands/tenant/snapshot/create/index.d.ts +17 -0
- package/dist/commands/tenant/snapshot/create/index.js +78 -0
- package/dist/commands/tenant/snapshot/delete/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/delete/index.js +102 -0
- package/dist/commands/tenant/snapshot/list/index.d.ts +16 -0
- package/dist/commands/tenant/snapshot/list/index.js +96 -0
- package/dist/commands/tenant/snapshot/swap/index.d.ts +19 -0
- package/dist/commands/tenant/snapshot/swap/index.js +103 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +7 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +8 -0
- package/dist/commands/unit_test/run_all/index.js +7 -3
- package/dist/commands/workspace/pull/index.js +13 -2
- package/dist/commands/workspace/push/index.d.ts +1 -1
- package/dist/commands/workspace/push/index.js +15 -5
- package/dist/utils/function-input.d.ts +77 -0
- package/dist/utils/function-input.js +148 -0
- package/dist/utils/knowledge-sync.d.ts +84 -79
- package/dist/utils/knowledge-sync.js +376 -323
- package/dist/utils/multidoc-push.d.ts +30 -18
- package/dist/utils/multidoc-push.js +284 -148
- package/dist/utils/reference-checker.js +2 -2
- package/dist/utils/static-host-files.d.ts +21 -0
- package/dist/utils/static-host-files.js +41 -0
- package/oclif.manifest.json +5046 -2691
- package/package.json +10 -1
- package/dist/commands/knowledge/pull/index.js +0 -86
- package/dist/commands/knowledge/push/index.js +0 -126
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import BaseCommand from '../../../base-command.js';
|
|
4
|
+
import { assembleInput, loadJsonSource, parseDataPairs, validateAgainstSchema, } from '../../../utils/function-input.js';
|
|
5
|
+
export default class FunctionRun extends BaseCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
name: Args.string({
|
|
8
|
+
description: 'Name of the function to run (interactive if omitted with --name unset)',
|
|
9
|
+
required: false,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Run (execute) a named function in a workspace and print its result';
|
|
13
|
+
static examples = [
|
|
14
|
+
`$ xano function:run calcScore -w 40
|
|
15
|
+
# Prompts for any declared inputs, then runs the function`,
|
|
16
|
+
`$ xano function:run calcScore --data email=jo@x.com --data age:=30 --data active:=true`,
|
|
17
|
+
`$ xano function:run calcScore --json @payload.json --data env=staging`,
|
|
18
|
+
`$ echo '{"email":"jo@x.com"}' | xano function:run calcScore --stdin -o json | jq .result`,
|
|
19
|
+
`$ xano function:run calcScore --branch dev --logs`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
branch: Flags.string({
|
|
24
|
+
description: 'Branch to run from (defaults to profile branch, then main)',
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
data: Flags.string({
|
|
28
|
+
char: 'd',
|
|
29
|
+
description: 'Input field as key=value (string), key:=json (raw JSON), or key@file (file contents). Repeatable.',
|
|
30
|
+
multiple: true,
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
json: Flags.string({
|
|
34
|
+
description: "Input as a JSON object: inline, @file.json, or '-' for stdin",
|
|
35
|
+
exclusive: ['stdin'],
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
logs: Flags.boolean({
|
|
39
|
+
default: false,
|
|
40
|
+
description: 'Print the execution logs returned by the debugger',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
'name': Flags.string({
|
|
44
|
+
char: 'n',
|
|
45
|
+
description: 'Name of the function to run (alternative to the positional argument)',
|
|
46
|
+
required: false,
|
|
47
|
+
}),
|
|
48
|
+
'no-input-check': Flags.boolean({
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Skip local schema validation and interactive prompting; send the payload as-is',
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
53
|
+
output: Flags.string({
|
|
54
|
+
char: 'o',
|
|
55
|
+
default: 'json',
|
|
56
|
+
description: 'Output format',
|
|
57
|
+
options: ['json', 'summary'],
|
|
58
|
+
required: false,
|
|
59
|
+
}),
|
|
60
|
+
stdin: Flags.boolean({
|
|
61
|
+
char: 's',
|
|
62
|
+
default: false,
|
|
63
|
+
description: "Read the input JSON object from stdin (same as --json -)",
|
|
64
|
+
exclusive: ['json'],
|
|
65
|
+
required: false,
|
|
66
|
+
}),
|
|
67
|
+
workspace: Flags.string({
|
|
68
|
+
char: 'w',
|
|
69
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
70
|
+
required: false,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
async run() {
|
|
74
|
+
const { args, flags } = await this.parse(FunctionRun);
|
|
75
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
76
|
+
const functionName = args.name ?? flags.name;
|
|
77
|
+
if (!functionName) {
|
|
78
|
+
this.error('A function name is required. Provide it as an argument or with --name.');
|
|
79
|
+
}
|
|
80
|
+
const workspaceId = this.resolveWorkspaceId(flags, profile, profileName);
|
|
81
|
+
const branch = flags.branch ?? profile.branch ?? '';
|
|
82
|
+
// 1) Assemble the input object from JSON base + --data overrides.
|
|
83
|
+
const jsonSource = flags.stdin ? '-' : flags.json;
|
|
84
|
+
const jsonBase = loadJsonSource(jsonSource, { stdin: () => this.readStdinSync() });
|
|
85
|
+
let input = assembleInput({ dataPairs: parseDataPairs(flags.data ?? []), jsonBase });
|
|
86
|
+
// 2) Introspect the function's declared inputs (best-effort) to validate,
|
|
87
|
+
// warn on obvious mismatches, and prompt for missing required params.
|
|
88
|
+
if (!flags['no-input-check']) {
|
|
89
|
+
const fn = await this.fetchFunctionByName({
|
|
90
|
+
branch,
|
|
91
|
+
name: functionName,
|
|
92
|
+
profile,
|
|
93
|
+
verbose: flags.verbose,
|
|
94
|
+
workspaceId,
|
|
95
|
+
});
|
|
96
|
+
const schema = fn?.input;
|
|
97
|
+
const { missingRequired, warnings } = validateAgainstSchema(input, schema);
|
|
98
|
+
for (const w of warnings)
|
|
99
|
+
this.warn(w);
|
|
100
|
+
if (missingRequired.length > 0) {
|
|
101
|
+
input = await this.promptForMissing(input, missingRequired);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 3) Execute via the meta run endpoint.
|
|
105
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/run`;
|
|
106
|
+
const body = { input, name: functionName };
|
|
107
|
+
if (branch)
|
|
108
|
+
body.branch = branch;
|
|
109
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
headers: {
|
|
112
|
+
'accept': 'application/json',
|
|
113
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
method: 'POST',
|
|
117
|
+
}, flags.verbose, profile.access_token);
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const message = await this.parseApiError(response, 'Failed to run function');
|
|
120
|
+
this.error(message);
|
|
121
|
+
}
|
|
122
|
+
const result = (await response.json());
|
|
123
|
+
this.emitResult(result, flags.output, flags.logs);
|
|
124
|
+
// 4) Compose in scripts/CI: non-zero exit when the function errored.
|
|
125
|
+
if (result.status && result.status !== 'ok') {
|
|
126
|
+
this.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
emitResult(result, output, showLogs) {
|
|
130
|
+
if (output === 'json') {
|
|
131
|
+
// Raw result object so `| jq .result` works predictably.
|
|
132
|
+
this.log(JSON.stringify(result.result ?? null, null, 2));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const status = result.status ?? 'ok';
|
|
136
|
+
this.log(`Status: ${status}`);
|
|
137
|
+
this.log('Result:');
|
|
138
|
+
this.log(JSON.stringify(result.result ?? null, null, 2));
|
|
139
|
+
}
|
|
140
|
+
if (showLogs && Array.isArray(result.logs) && result.logs.length > 0) {
|
|
141
|
+
this.log('');
|
|
142
|
+
this.log('Logs:');
|
|
143
|
+
for (const entry of result.logs) {
|
|
144
|
+
this.log(typeof entry === 'string' ? entry : JSON.stringify(entry));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Fetch a single function record by name via the list endpoint's search,
|
|
150
|
+
* returning the exact-name match (so `calc` doesn't accidentally match
|
|
151
|
+
* `calcScore`). Best-effort: returns undefined and warns on any failure so a
|
|
152
|
+
* transient introspection problem never blocks an otherwise-valid run.
|
|
153
|
+
*/
|
|
154
|
+
async fetchFunctionByName(opts) {
|
|
155
|
+
const { branch, name, profile, verbose, workspaceId } = opts;
|
|
156
|
+
// eslint-disable-next-line camelcase -- external Metadata API query param
|
|
157
|
+
const params = new URLSearchParams({ per_page: '100', search: name });
|
|
158
|
+
if (branch)
|
|
159
|
+
params.set('branch', branch);
|
|
160
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${params.toString()}`;
|
|
161
|
+
try {
|
|
162
|
+
const response = await this.verboseFetch(url, { headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` }, method: 'GET' }, verbose, profile.access_token);
|
|
163
|
+
if (!response.ok)
|
|
164
|
+
return undefined;
|
|
165
|
+
const page = (await response.json());
|
|
166
|
+
const items = Array.isArray(page) ? page : (page.items ?? []);
|
|
167
|
+
const exact = items.filter((f) => f.name === name);
|
|
168
|
+
if (exact.length > 1) {
|
|
169
|
+
this.warn(`Multiple functions named '${name}' found; skipping input validation.`);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return exact[0];
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Introspection is a convenience, not a requirement.
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Prompt for each missing required input on an interactive TTY. Values are
|
|
181
|
+
* read as raw JSON when they parse as such (so numbers/bools/objects work),
|
|
182
|
+
* otherwise treated as a string — mirroring the `key:=`/`key=` distinction.
|
|
183
|
+
* Non-TTY: fail fast with the list of missing params (never hang on a prompt).
|
|
184
|
+
*/
|
|
185
|
+
async promptForMissing(input, missing) {
|
|
186
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
187
|
+
const names = missing.map((p) => `${p.name}${p.type ? ` (${p.type})` : ''}`).join(', ');
|
|
188
|
+
this.error(`Missing required input: ${names}. ` +
|
|
189
|
+
`Provide via --data/--json, or run interactively in a terminal.`);
|
|
190
|
+
}
|
|
191
|
+
const readline = await import('node:readline');
|
|
192
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
193
|
+
const ask = (message) => new Promise((resolve) => {
|
|
194
|
+
rl.question(message, resolve);
|
|
195
|
+
});
|
|
196
|
+
const filled = { ...input };
|
|
197
|
+
try {
|
|
198
|
+
for (const param of missing) {
|
|
199
|
+
const hint = [param.type, param.description].filter(Boolean).join(' — ');
|
|
200
|
+
// eslint-disable-next-line no-await-in-loop
|
|
201
|
+
const raw = await ask(`${param.name}${hint ? ` (${hint})` : ''}: `);
|
|
202
|
+
filled[param.name] = coercePromptValue(raw, param);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
rl.close();
|
|
207
|
+
}
|
|
208
|
+
return filled;
|
|
209
|
+
}
|
|
210
|
+
readStdinSync() {
|
|
211
|
+
// Synchronous read of all of stdin (fd 0); used for --json - / --stdin.
|
|
212
|
+
try {
|
|
213
|
+
return readFileSync(0, 'utf8');
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
this.error(`Failed to read from stdin: ${error instanceof Error ? error.message : String(error)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
resolveWorkspaceId(flags, profile, profileName) {
|
|
220
|
+
if (flags.workspace)
|
|
221
|
+
return flags.workspace;
|
|
222
|
+
if (profile.workspace)
|
|
223
|
+
return profile.workspace;
|
|
224
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
225
|
+
` 1. Provide it as a flag: xano function:run <name> -w <workspace_id>\n` +
|
|
226
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Coerce a prompted string into the value the parameter likely expects. Non-text
|
|
231
|
+
* params attempt a JSON parse (so `30`, `true`, `["a"]` become typed values);
|
|
232
|
+
* text params keep the raw string. Falls back to the raw string on parse errors.
|
|
233
|
+
*/
|
|
234
|
+
function coercePromptValue(raw, param) {
|
|
235
|
+
const isTextLike = !param.type || param.type === 'text' || param.type === 'uuid';
|
|
236
|
+
if (isTextLike)
|
|
237
|
+
return raw;
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(raw);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return raw;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import BaseCommand from '../../../base-command.js';
|
|
2
|
-
export default class
|
|
2
|
+
export default class KnowledgeGet extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
3
6
|
static description: string;
|
|
4
7
|
static examples: string[];
|
|
5
8
|
static flags: {
|
|
6
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
|
|
10
|
+
file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
13
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
14
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class KnowledgeGet extends BaseCommand {
|
|
4
|
+
static args = {
|
|
5
|
+
name: Args.string({
|
|
6
|
+
description: 'Knowledge item name',
|
|
7
|
+
required: true,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Get a knowledge item by name, or one of its reference files';
|
|
11
|
+
static examples = [
|
|
12
|
+
'$ xano knowledge get "deploy-runbook" -w 40',
|
|
13
|
+
'$ xano knowledge get "deploy-runbook" -w 40 --output json',
|
|
14
|
+
'$ xano knowledge get "deploy-runbook" -w 40 --file checklist.md',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
...BaseCommand.baseFlags,
|
|
18
|
+
branch: Flags.string({
|
|
19
|
+
char: 'b',
|
|
20
|
+
description: 'Branch ID',
|
|
21
|
+
required: false,
|
|
22
|
+
}),
|
|
23
|
+
file: Flags.string({
|
|
24
|
+
char: 'f',
|
|
25
|
+
description: 'Path of a reference file to fetch instead of the main content',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
28
|
+
output: Flags.string({
|
|
29
|
+
char: 'o',
|
|
30
|
+
default: 'text',
|
|
31
|
+
description: 'Output format',
|
|
32
|
+
options: ['text', 'json'],
|
|
33
|
+
required: false,
|
|
34
|
+
}),
|
|
35
|
+
workspace: Flags.string({
|
|
36
|
+
char: 'w',
|
|
37
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
38
|
+
required: false,
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
async run() {
|
|
42
|
+
const { args, flags } = await this.parse(KnowledgeGet);
|
|
43
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
44
|
+
let workspaceId;
|
|
45
|
+
if (flags.workspace) {
|
|
46
|
+
workspaceId = flags.workspace;
|
|
47
|
+
}
|
|
48
|
+
else if (profile.workspace) {
|
|
49
|
+
workspaceId = profile.workspace;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
53
|
+
` 1. Provide it as a flag: xano knowledge get <name> -w <workspace_id>\n` +
|
|
54
|
+
` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
|
|
55
|
+
}
|
|
56
|
+
const queryParams = new URLSearchParams();
|
|
57
|
+
queryParams.set('enabled_only', 'false');
|
|
58
|
+
queryParams.set('include_content', 'true');
|
|
59
|
+
if (flags.branch) {
|
|
60
|
+
queryParams.set('branch_id', flags.branch);
|
|
61
|
+
}
|
|
62
|
+
const listUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/knowledge?${queryParams.toString()}`;
|
|
63
|
+
try {
|
|
64
|
+
const listResponse = await this.verboseFetch(listUrl, {
|
|
65
|
+
headers: {
|
|
66
|
+
'accept': 'application/json',
|
|
67
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
68
|
+
},
|
|
69
|
+
method: 'GET',
|
|
70
|
+
}, flags.verbose, profile.access_token);
|
|
71
|
+
if (!listResponse.ok) {
|
|
72
|
+
const errorText = await listResponse.text();
|
|
73
|
+
this.error(`API request failed with status ${listResponse.status}: ${listResponse.statusText}\n${errorText}`);
|
|
74
|
+
}
|
|
75
|
+
const data = await listResponse.json();
|
|
76
|
+
const items = Array.isArray(data) ? data : (data.items ?? []);
|
|
77
|
+
const targetName = args.name.toLowerCase();
|
|
78
|
+
const item = items.find((i) => i.name?.toLowerCase() === targetName);
|
|
79
|
+
if (!item) {
|
|
80
|
+
const available = items.map((i) => i.name).filter(Boolean).join(', ');
|
|
81
|
+
this.error(`Knowledge item not found: "${args.name}"\n` +
|
|
82
|
+
(available ? `Available items: ${available}` : 'No knowledge items exist in this workspace.'));
|
|
83
|
+
}
|
|
84
|
+
if (flags.file) {
|
|
85
|
+
const fileParams = new URLSearchParams({ path: flags.file });
|
|
86
|
+
const fileUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/knowledge/${item.id}/file?${fileParams.toString()}`;
|
|
87
|
+
const fileResponse = await this.verboseFetch(fileUrl, {
|
|
88
|
+
headers: {
|
|
89
|
+
'accept': 'application/json',
|
|
90
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
91
|
+
},
|
|
92
|
+
method: 'GET',
|
|
93
|
+
}, flags.verbose, profile.access_token);
|
|
94
|
+
if (!fileResponse.ok) {
|
|
95
|
+
const errorText = await fileResponse.text();
|
|
96
|
+
this.error(`Failed to fetch file "${flags.file}" from knowledge item "${item.name}": ${fileResponse.status} ${fileResponse.statusText}\n${errorText}`);
|
|
97
|
+
}
|
|
98
|
+
const fileJson = await fileResponse.json();
|
|
99
|
+
const fileContent = fileJson.file?.content ?? '';
|
|
100
|
+
if (flags.output === 'json') {
|
|
101
|
+
this.log(JSON.stringify({ content: fileContent, name: item.name, path: flags.file }, null, 2));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.log(fileContent);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (flags.output === 'json') {
|
|
109
|
+
this.log(JSON.stringify(item, null, 2));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
this.log(item.content ?? '');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof Error && 'oclif' in error)
|
|
117
|
+
throw error;
|
|
118
|
+
if (error instanceof Error) {
|
|
119
|
+
this.error(`Failed to fetch knowledge: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.error(`Failed to fetch knowledge: ${String(error)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class KnowledgeList extends BaseCommand {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
'enabled-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private formatMarkdown;
|
|
18
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
export default class KnowledgeList extends BaseCommand {
|
|
4
|
+
static args = {};
|
|
5
|
+
static description = 'List workspace knowledge and skills as plain-text markdown';
|
|
6
|
+
static examples = [
|
|
7
|
+
`$ xano knowledge list -w 40
|
|
8
|
+
# Always-on Knowledge
|
|
9
|
+
|
|
10
|
+
## deploy-runbook
|
|
11
|
+
|
|
12
|
+
# Deploy Runbook
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
# On-demand Knowledge
|
|
16
|
+
|
|
17
|
+
- **code-style**: Our team coding conventions
|
|
18
|
+
`,
|
|
19
|
+
'$ xano knowledge list -w 40 --output json',
|
|
20
|
+
'$ xano knowledge list -w 40 --type skill',
|
|
21
|
+
'$ xano knowledge list --no-enabled-only',
|
|
22
|
+
];
|
|
23
|
+
static flags = {
|
|
24
|
+
...BaseCommand.baseFlags,
|
|
25
|
+
branch: Flags.string({
|
|
26
|
+
char: 'b',
|
|
27
|
+
description: 'Branch ID',
|
|
28
|
+
required: false,
|
|
29
|
+
}),
|
|
30
|
+
'enabled-only': Flags.boolean({
|
|
31
|
+
allowNo: true,
|
|
32
|
+
default: true,
|
|
33
|
+
description: 'Only show enabled knowledge (use --no-enabled-only to include disabled)',
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
output: Flags.string({
|
|
37
|
+
char: 'o',
|
|
38
|
+
default: 'markdown',
|
|
39
|
+
description: 'Output format',
|
|
40
|
+
options: ['markdown', 'json'],
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
type: Flags.string({
|
|
44
|
+
char: 't',
|
|
45
|
+
description: 'Filter by knowledge type',
|
|
46
|
+
options: ['skill', 'doc', 'agents.md'],
|
|
47
|
+
required: false,
|
|
48
|
+
}),
|
|
49
|
+
workspace: Flags.string({
|
|
50
|
+
char: 'w',
|
|
51
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
52
|
+
required: false,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
async run() {
|
|
56
|
+
const { flags } = await this.parse(KnowledgeList);
|
|
57
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
58
|
+
let workspaceId;
|
|
59
|
+
if (flags.workspace) {
|
|
60
|
+
workspaceId = flags.workspace;
|
|
61
|
+
}
|
|
62
|
+
else if (profile.workspace) {
|
|
63
|
+
workspaceId = profile.workspace;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
67
|
+
` 1. Provide it as a flag: xano knowledge list -w <workspace_id>\n` +
|
|
68
|
+
` 2. Set it in your profile using: xano profile edit ${profileName} -w <workspace_id>`);
|
|
69
|
+
}
|
|
70
|
+
const queryParams = new URLSearchParams();
|
|
71
|
+
queryParams.set('enabled_only', flags['enabled-only'].toString());
|
|
72
|
+
queryParams.set('include_content', 'true');
|
|
73
|
+
if (flags.branch) {
|
|
74
|
+
queryParams.set('branch_id', flags.branch);
|
|
75
|
+
}
|
|
76
|
+
if (flags.type) {
|
|
77
|
+
queryParams.set('type', flags.type);
|
|
78
|
+
}
|
|
79
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/knowledge?${queryParams.toString()}`;
|
|
80
|
+
try {
|
|
81
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
82
|
+
headers: {
|
|
83
|
+
'accept': 'application/json',
|
|
84
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
85
|
+
},
|
|
86
|
+
method: 'GET',
|
|
87
|
+
}, flags.verbose, profile.access_token);
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const errorText = await response.text();
|
|
90
|
+
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
91
|
+
}
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
const items = Array.isArray(data) ? data : (data.items ?? []);
|
|
94
|
+
if (flags.output === 'json') {
|
|
95
|
+
this.log(JSON.stringify(items, null, 2));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.log(this.formatMarkdown(items));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error && 'oclif' in error)
|
|
102
|
+
throw error;
|
|
103
|
+
if (error instanceof Error) {
|
|
104
|
+
this.error(`Failed to fetch knowledge: ${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.error(`Failed to fetch knowledge: ${String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
formatMarkdown(items) {
|
|
112
|
+
if (items.length === 0) {
|
|
113
|
+
return 'No knowledge items found.';
|
|
114
|
+
}
|
|
115
|
+
const alwaysOn = items.filter((item) => item.mode === 'always');
|
|
116
|
+
const onDemand = items.filter((item) => item.mode !== 'always');
|
|
117
|
+
const lines = [];
|
|
118
|
+
if (alwaysOn.length > 0) {
|
|
119
|
+
lines.push('# Always-on Knowledge');
|
|
120
|
+
for (const item of alwaysOn) {
|
|
121
|
+
lines.push('', `## ${item.name ?? '(unnamed)'}`, '', item.content ?? '', '', '---');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (onDemand.length > 0) {
|
|
125
|
+
if (lines.length > 0)
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push('# On-demand Knowledge', '');
|
|
128
|
+
for (const item of onDemand) {
|
|
129
|
+
const desc = item.description || '(no description)';
|
|
130
|
+
lines.push(`- **${item.name ?? '(unnamed)'}**: ${desc}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import snakeCase from 'lodash.snakecase';
|
|
3
3
|
import BaseCommand from '../../../base-command.js';
|
|
4
4
|
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
5
|
+
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
5
6
|
import * as fs from 'node:fs';
|
|
6
7
|
import * as path from 'node:path';
|
|
7
8
|
export default class SandboxPull extends BaseCommand {
|
|
@@ -87,7 +88,6 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
87
88
|
}
|
|
88
89
|
if (documents.length === 0) {
|
|
89
90
|
this.log('No documents found in response');
|
|
90
|
-
return;
|
|
91
91
|
}
|
|
92
92
|
const outputDir = path.resolve(flags.directory);
|
|
93
93
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -182,7 +182,17 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
182
182
|
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
183
183
|
writtenCount++;
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
// ── Pull knowledge ────────────────────────────────────────────────────
|
|
186
|
+
const knowledgeUrl = `${profile.instance_origin}/api:meta/sandbox/knowledge/sync`;
|
|
187
|
+
const knowledgeObjects = await fetchKnowledge(knowledgeUrl, '', profile.access_token, this.verboseFetch.bind(this), flags.verbose);
|
|
188
|
+
let knowledgeCount = 0;
|
|
189
|
+
if (knowledgeObjects.length > 0) {
|
|
190
|
+
knowledgeCount = writeKnowledge(knowledgeObjects, outputDir);
|
|
191
|
+
}
|
|
192
|
+
const parts = [`${writtenCount} documents`];
|
|
193
|
+
if (knowledgeCount > 0)
|
|
194
|
+
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
195
|
+
this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
|
|
186
196
|
}
|
|
187
197
|
sanitizeFilename(name) {
|
|
188
198
|
return snakeCase(name.replaceAll('"', ''));
|
|
@@ -5,7 +5,7 @@ import open from 'open';
|
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
6
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
7
7
|
export default class SandboxPush extends BaseCommand {
|
|
8
|
-
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.
|
|
8
|
+
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.';
|
|
9
9
|
static examples = [
|
|
10
10
|
`$ xano sandbox push
|
|
11
11
|
Push from current directory (default partial mode)
|
|
@@ -109,6 +109,7 @@ Push and open sandbox review in the browser
|
|
|
109
109
|
cliVersion: this.config.version,
|
|
110
110
|
instanceOrigin: profile.instance_origin,
|
|
111
111
|
label: 'sandbox environment',
|
|
112
|
+
sourceWorkspaceId: profile.workspace,
|
|
112
113
|
supportsBranches: false,
|
|
113
114
|
supportsPartial: true,
|
|
114
115
|
warnOnWorkspaceMismatch: true,
|
|
@@ -130,12 +131,29 @@ Push and open sandbox review in the browser
|
|
|
130
131
|
branch: '',
|
|
131
132
|
command: this,
|
|
132
133
|
inputDir,
|
|
134
|
+
knowledge: {
|
|
135
|
+
listUrl: () => `${baseUrl}/knowledge/sync`,
|
|
136
|
+
rootDir: inputDir,
|
|
137
|
+
},
|
|
133
138
|
verboseFetch: this.verboseFetch.bind(this),
|
|
134
139
|
}, target, pushFlags);
|
|
135
140
|
if (flags.review) {
|
|
136
141
|
await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
|
|
137
142
|
}
|
|
138
143
|
}
|
|
144
|
+
getFrontendUrl(instanceOrigin) {
|
|
145
|
+
try {
|
|
146
|
+
const url = new URL(instanceOrigin);
|
|
147
|
+
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
|
148
|
+
url.port = '4200';
|
|
149
|
+
return url.origin;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// fall through
|
|
154
|
+
}
|
|
155
|
+
return instanceOrigin;
|
|
156
|
+
}
|
|
139
157
|
async openReview(instanceOrigin, accessToken, verbose) {
|
|
140
158
|
const response = await this.verboseFetch(`${instanceOrigin}/api:meta/sandbox/impersonate`, {
|
|
141
159
|
headers: {
|
|
@@ -158,17 +176,4 @@ Push and open sandbox review in the browser
|
|
|
158
176
|
this.log('Opening sandbox review...');
|
|
159
177
|
await open(reviewUrl);
|
|
160
178
|
}
|
|
161
|
-
getFrontendUrl(instanceOrigin) {
|
|
162
|
-
try {
|
|
163
|
-
const url = new URL(instanceOrigin);
|
|
164
|
-
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
|
165
|
-
url.port = '4200';
|
|
166
|
-
return url.origin;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
// fall through
|
|
171
|
-
}
|
|
172
|
-
return instanceOrigin;
|
|
173
|
-
}
|
|
174
179
|
}
|