@xano/cli 1.0.6-beta.1 → 1.0.6-beta.2
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 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
- package/dist/commands/ephemeral/impersonate/index.js +11 -3
- package/dist/commands/tenant/pull/index.js +12 -1
- package/dist/commands/workspace/git/pull/index.js +10 -1
- package/dist/commands/workspace/pull/index.js +21 -1
- package/dist/utils/document-parser.d.ts +26 -0
- package/dist/utils/document-parser.js +38 -1
- package/oclif.manifest.json +2993 -2984
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -594,6 +594,7 @@ xano ephemeral push <tenant_name> -d ./my-ephemeral
|
|
|
594
594
|
# Open in the browser (or print the URL)
|
|
595
595
|
xano ephemeral impersonate <tenant_name>
|
|
596
596
|
xano ephemeral impersonate <tenant_name> --url-only
|
|
597
|
+
xano ephemeral impersonate <tenant_name> --guest # read-only session (browse only)
|
|
597
598
|
```
|
|
598
599
|
|
|
599
600
|
#### Static hosting for an ephemeral tenant
|
|
@@ -6,6 +6,7 @@ export default class EphemeralImpersonate extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
guest: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
'url-only': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -15,9 +15,16 @@ Opening browser...
|
|
|
15
15
|
Impersonation successful!
|
|
16
16
|
`,
|
|
17
17
|
`$ xano ephemeral impersonate e4f2-9ab1-xyz1 -o json`,
|
|
18
|
+
`$ xano ephemeral impersonate e4f2-9ab1-xyz1 --guest --url-only`,
|
|
18
19
|
];
|
|
19
20
|
static flags = {
|
|
20
21
|
...BaseCommand.baseFlags,
|
|
22
|
+
guest: Flags.boolean({
|
|
23
|
+
char: 'g',
|
|
24
|
+
default: false,
|
|
25
|
+
description: 'Mint a read-only guest session (browse only; no schema/data/config changes)',
|
|
26
|
+
required: false,
|
|
27
|
+
}),
|
|
21
28
|
output: Flags.string({
|
|
22
29
|
char: 'o',
|
|
23
30
|
default: 'summary',
|
|
@@ -46,7 +53,7 @@ Impersonation successful!
|
|
|
46
53
|
}
|
|
47
54
|
const tenantName = args.tenant_name;
|
|
48
55
|
try {
|
|
49
|
-
const response = await this.getImpersonateResponse(profile, workspaceId, tenantName);
|
|
56
|
+
const response = await this.getImpersonateResponse(profile, workspaceId, tenantName, flags.guest);
|
|
50
57
|
if (flags.output === 'json') {
|
|
51
58
|
this.log(JSON.stringify(response, null, 2));
|
|
52
59
|
}
|
|
@@ -76,8 +83,9 @@ Impersonation successful!
|
|
|
76
83
|
}
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
|
-
async getImpersonateResponse(profile, workspaceId, tenantName) {
|
|
80
|
-
const
|
|
86
|
+
async getImpersonateResponse(profile, workspaceId, tenantName, guest = false) {
|
|
87
|
+
const query = guest ? '?guest_read_only=true' : '';
|
|
88
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(tenantName)}/impersonate${query}`;
|
|
81
89
|
const { verbose } = await this.parse(EphemeralImpersonate).then((r) => r.flags);
|
|
82
90
|
const response = await this.verboseFetch(apiUrl, {
|
|
83
91
|
headers: {
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, channelPathSegments, parseDocument, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class Pull extends BaseCommand {
|
|
8
8
|
static description = 'Pull a tenant multidoc from the Xano Metadata API and split into individual files';
|
|
9
9
|
static examples = [
|
|
@@ -190,6 +190,17 @@ Pulled 58 documents from tenant my-tenant
|
|
|
190
190
|
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
191
191
|
baseName = this.sanitizeFilename(doc.name);
|
|
192
192
|
}
|
|
193
|
+
else if (doc.type === 'channel') {
|
|
194
|
+
// Realtime v2 — see workspace/pull for the full rationale.
|
|
195
|
+
// channel "rooms/{room_id}" → channel/rooms/[room_id]/_channel.xs
|
|
196
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
197
|
+
baseName = '_channel';
|
|
198
|
+
}
|
|
199
|
+
else if (doc.type === 'message' && doc.channel) {
|
|
200
|
+
// Realtime v2 message → channel/{channel_path}/{message_name}.xs
|
|
201
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
202
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
203
|
+
}
|
|
193
204
|
else if (doc.type === 'api_group') {
|
|
194
205
|
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
195
206
|
const groupFolder = getApiGroupFolder(doc.name);
|
|
@@ -5,7 +5,7 @@ import * as os from 'node:os';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import snakeCase from 'lodash.snakecase';
|
|
7
7
|
import BaseCommand, { buildUserAgent } from '../../../../base-command.js';
|
|
8
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../../utils/document-parser.js';
|
|
8
|
+
import { buildApiGroupFolderResolver, channelPathSegments, parseDocument, } from '../../../../utils/document-parser.js';
|
|
9
9
|
export default class GitPull extends BaseCommand {
|
|
10
10
|
static description = 'Pull XanoScript files from a git repository into a local directory';
|
|
11
11
|
static examples = [
|
|
@@ -373,6 +373,15 @@ export default class GitPull extends BaseCommand {
|
|
|
373
373
|
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
374
374
|
baseName = this.sanitizeFilename(doc.name);
|
|
375
375
|
}
|
|
376
|
+
else if (doc.type === 'channel') {
|
|
377
|
+
// Realtime v2 — see workspace/pull for the full rationale.
|
|
378
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
379
|
+
baseName = '_channel';
|
|
380
|
+
}
|
|
381
|
+
else if (doc.type === 'message' && doc.channel) {
|
|
382
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
383
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
384
|
+
}
|
|
376
385
|
else if (doc.type === 'api_group') {
|
|
377
386
|
const groupFolder = getApiGroupFolder(doc.name);
|
|
378
387
|
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import snakeCase from 'lodash.snakecase';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, channelPathSegments, parseDocument, } from '../../../utils/document-parser.js';
|
|
7
7
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
8
8
|
export default class Pull extends BaseCommand {
|
|
9
9
|
static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
|
|
@@ -195,6 +195,26 @@ Pulled 58 documents
|
|
|
195
195
|
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
196
196
|
baseName = this.sanitizeFilename(doc.name);
|
|
197
197
|
}
|
|
198
|
+
else if (doc.type === 'channel') {
|
|
199
|
+
// Realtime v2. The channel owns a directory mirroring its path and its
|
|
200
|
+
// messages live inside it, so ownership is visible in the tree.
|
|
201
|
+
// channel "rooms/{room_id}" → channel/rooms/[room_id]/_channel.xs
|
|
202
|
+
//
|
|
203
|
+
// The document uses a FIXED "_channel.xs" leaf: naming it after the
|
|
204
|
+
// last segment yields "[room_id].xs", which names the parameter rather
|
|
205
|
+
// than the channel. A fixed leaf also sorts first, making "the channel
|
|
206
|
+
// vs its messages" obvious.
|
|
207
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.name, snakeCase));
|
|
208
|
+
baseName = '_channel';
|
|
209
|
+
}
|
|
210
|
+
else if (doc.type === 'message' && doc.channel) {
|
|
211
|
+
// Realtime v2 message → channel/{channel_path}/{message_name}.xs
|
|
212
|
+
// Nesting matters beyond tidiness: message names are unique only WITHIN
|
|
213
|
+
// a channel, so a flat message/ directory would collide when two
|
|
214
|
+
// channels both define e.g. "say".
|
|
215
|
+
typeDir = path.join(outputDir, 'channel', ...channelPathSegments(doc.channel, snakeCase));
|
|
216
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
217
|
+
}
|
|
198
218
|
else if (doc.type === 'api_group') {
|
|
199
219
|
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
200
220
|
const groupFolder = getApiGroupFolder(doc.name);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface ParsedDocument {
|
|
2
2
|
apiGroup?: string;
|
|
3
3
|
canonical?: string;
|
|
4
|
+
/** Owning channel path for realtime v2 `message` documents. */
|
|
5
|
+
channel?: string;
|
|
4
6
|
content: string;
|
|
5
7
|
guid?: string;
|
|
6
8
|
name: string;
|
|
@@ -12,6 +14,30 @@ export interface ParsedDocument {
|
|
|
12
14
|
* Skips leading comment lines (starting with //) to find the first meaningful line.
|
|
13
15
|
*/
|
|
14
16
|
export declare function parseDocument(content: string): null | ParsedDocument;
|
|
17
|
+
/**
|
|
18
|
+
* Map a realtime v2 channel path onto directory segments.
|
|
19
|
+
*
|
|
20
|
+
* A channel name IS a path template — "rooms/{room_id}" — so the directory tree
|
|
21
|
+
* mirrors it rather than flattening it. That is the whole reason messages nest
|
|
22
|
+
* under channels: the tree should show the structure, and
|
|
23
|
+
* "channel/rooms/[room_id]/post.xs" says immediately that `post` lives on a
|
|
24
|
+
* parameterized rooms channel.
|
|
25
|
+
*
|
|
26
|
+
* "rooms" -> ["rooms"]
|
|
27
|
+
* "rooms/{room_id}" -> ["rooms", "[room_id]"]
|
|
28
|
+
* "org/{org_id}/room/{id}" -> ["org", "[org_id]", "room", "[id]"]
|
|
29
|
+
*
|
|
30
|
+
* Braces become brackets because "{" and "}" are awkward in shells, while "["
|
|
31
|
+
* and "]" are legal on every filesystem. They are still glob character classes,
|
|
32
|
+
* so scripting against a checkout needs the path quoted.
|
|
33
|
+
*
|
|
34
|
+
* Escaping the braces rather than dropping them is what keeps a parameterized
|
|
35
|
+
* channel distinct from a literal one: without it, "rooms/{room_id}" and a
|
|
36
|
+
* literal channel named "rooms/room_id" collide on the same directory.
|
|
37
|
+
*
|
|
38
|
+
* Mirrors Vfs::channelSegment() on the server.
|
|
39
|
+
*/
|
|
40
|
+
export declare function channelPathSegments(name: string, snakeCaseFn: (s: string) => string): string[];
|
|
15
41
|
/**
|
|
16
42
|
* Build a unique key for a document based on its type, name, verb, and api_group.
|
|
17
43
|
* Used to match server GUID map entries back to local files.
|
|
@@ -45,6 +45,14 @@ export function parseDocument(content) {
|
|
|
45
45
|
if (apiGroupMatch) {
|
|
46
46
|
apiGroup = apiGroupMatch[1];
|
|
47
47
|
}
|
|
48
|
+
// Extract channel if present (e.g., channel = "rooms/{room_id}").
|
|
49
|
+
// Realtime v2 messages reference their owning channel by PATH, which is what
|
|
50
|
+
// nests them under it on disk.
|
|
51
|
+
let channel;
|
|
52
|
+
const channelMatch = content.match(/^\s*channel\s*=\s*"([^"]*)"/m);
|
|
53
|
+
if (channelMatch) {
|
|
54
|
+
channel = channelMatch[1];
|
|
55
|
+
}
|
|
48
56
|
// Extract canonical if present (e.g., canonical = "abc123")
|
|
49
57
|
let canonical;
|
|
50
58
|
const canonicalMatch = content.match(/canonical\s*=\s*"([^"]*)"/);
|
|
@@ -57,7 +65,36 @@ export function parseDocument(content) {
|
|
|
57
65
|
if (guidMatch) {
|
|
58
66
|
guid = guidMatch[1];
|
|
59
67
|
}
|
|
60
|
-
return { apiGroup, canonical, content, guid, name, type, verb };
|
|
68
|
+
return { apiGroup, canonical, channel, content, guid, name, type, verb };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Map a realtime v2 channel path onto directory segments.
|
|
72
|
+
*
|
|
73
|
+
* A channel name IS a path template — "rooms/{room_id}" — so the directory tree
|
|
74
|
+
* mirrors it rather than flattening it. That is the whole reason messages nest
|
|
75
|
+
* under channels: the tree should show the structure, and
|
|
76
|
+
* "channel/rooms/[room_id]/post.xs" says immediately that `post` lives on a
|
|
77
|
+
* parameterized rooms channel.
|
|
78
|
+
*
|
|
79
|
+
* "rooms" -> ["rooms"]
|
|
80
|
+
* "rooms/{room_id}" -> ["rooms", "[room_id]"]
|
|
81
|
+
* "org/{org_id}/room/{id}" -> ["org", "[org_id]", "room", "[id]"]
|
|
82
|
+
*
|
|
83
|
+
* Braces become brackets because "{" and "}" are awkward in shells, while "["
|
|
84
|
+
* and "]" are legal on every filesystem. They are still glob character classes,
|
|
85
|
+
* so scripting against a checkout needs the path quoted.
|
|
86
|
+
*
|
|
87
|
+
* Escaping the braces rather than dropping them is what keeps a parameterized
|
|
88
|
+
* channel distinct from a literal one: without it, "rooms/{room_id}" and a
|
|
89
|
+
* literal channel named "rooms/room_id" collide on the same directory.
|
|
90
|
+
*
|
|
91
|
+
* Mirrors Vfs::channelSegment() on the server.
|
|
92
|
+
*/
|
|
93
|
+
export function channelPathSegments(name, snakeCaseFn) {
|
|
94
|
+
// A parameter keeps the author's name verbatim so it matches the `input`
|
|
95
|
+
// block; only the delimiters are swapped.
|
|
96
|
+
const isParam = /^\{.*\}$/;
|
|
97
|
+
return name.split('/').map((segment) => (isParam.test(segment) ? `[${segment.slice(1, -1)}]` : snakeCaseFn(segment)));
|
|
61
98
|
}
|
|
62
99
|
/**
|
|
63
100
|
* Build a unique key for a document based on its type, name, verb, and api_group.
|