@xano/cli 1.0.6-beta.9 → 1.2.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 +46 -62
- package/dist/base-command.d.ts +0 -2
- package/dist/base-command.js +4 -10
- package/dist/commands/release/pull/index.d.ts +6 -0
- package/dist/commands/release/pull/index.js +95 -10
- package/dist/commands/sandbox/pull/index.d.ts +1 -0
- package/dist/commands/sandbox/pull/index.js +77 -10
- package/dist/commands/sandbox/unit_test/run/index.js +17 -11
- package/dist/commands/sandbox/unit_test/run_all/index.js +9 -1
- package/dist/commands/sandbox/workflow_test/run/index.js +12 -9
- package/dist/commands/sandbox/workflow_test/run_all/index.js +9 -1
- package/dist/commands/tenant/pull/index.d.ts +6 -0
- package/dist/commands/tenant/pull/index.js +95 -10
- package/dist/commands/tenant/unit_test/run/index.js +19 -11
- package/dist/commands/tenant/unit_test/run_all/index.js +13 -1
- package/dist/commands/tenant/workflow_test/run/index.js +14 -9
- package/dist/commands/tenant/workflow_test/run_all/index.js +13 -1
- package/dist/commands/unit_test/run/index.js +19 -11
- package/dist/commands/unit_test/run_all/index.js +13 -1
- package/dist/commands/workflow_test/run/index.js +15 -10
- package/dist/commands/workflow_test/run_all/index.js +15 -3
- package/dist/commands/workspace/git/pull/index.js +3 -53
- package/dist/commands/workspace/pull/index.d.ts +6 -0
- package/dist/commands/workspace/pull/index.js +95 -10
- package/dist/utils/document-parser.d.ts +0 -85
- package/dist/utils/document-parser.js +1 -214
- package/dist/utils/knowledge-sync.d.ts +0 -1
- package/dist/utils/knowledge-sync.js +5 -11
- package/dist/utils/paging.d.ts +1 -2
- package/dist/utils/reference-checker.js +0 -13
- package/oclif.manifest.json +2796 -4820
- package/package.json +1 -10
- package/dist/commands/ephemeral/create/index.d.ts +0 -18
- package/dist/commands/ephemeral/create/index.js +0 -103
- package/dist/commands/ephemeral/delete/index.d.ts +0 -18
- package/dist/commands/ephemeral/delete/index.js +0 -99
- package/dist/commands/ephemeral/edit/index.d.ts +0 -18
- package/dist/commands/ephemeral/edit/index.js +0 -109
- package/dist/commands/ephemeral/get/index.d.ts +0 -21
- package/dist/commands/ephemeral/get/index.js +0 -125
- package/dist/commands/ephemeral/impersonate/index.d.ts +0 -20
- package/dist/commands/ephemeral/impersonate/index.js +0 -120
- package/dist/commands/ephemeral/list/index.d.ts +0 -19
- package/dist/commands/ephemeral/list/index.js +0 -123
- package/dist/commands/ephemeral/pull/index.d.ts +0 -19
- package/dist/commands/ephemeral/pull/index.js +0 -164
- package/dist/commands/ephemeral/push/index.d.ts +0 -35
- package/dist/commands/ephemeral/push/index.js +0 -221
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +0 -20
- package/dist/commands/ephemeral/static_host/build/delete/index.js +0 -121
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +0 -18
- package/dist/commands/ephemeral/static_host/build/get/index.js +0 -137
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +0 -18
- package/dist/commands/ephemeral/static_host/build/list/index.js +0 -137
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +0 -53
- package/dist/commands/ephemeral/static_host/build/pull/index.js +0 -308
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +0 -24
- package/dist/commands/ephemeral/static_host/build/push/index.js +0 -250
- package/dist/commands/ephemeral/static_host/create/index.d.ts +0 -18
- package/dist/commands/ephemeral/static_host/create/index.js +0 -92
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +0 -19
- package/dist/commands/ephemeral/static_host/deploy/index.js +0 -112
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +0 -24
- package/dist/commands/ephemeral/static_host/edit/index.js +0 -158
- package/dist/commands/ephemeral/static_host/get/index.d.ts +0 -19
- package/dist/commands/ephemeral/static_host/get/index.js +0 -101
- package/dist/commands/ephemeral/static_host/list/index.d.ts +0 -17
- package/dist/commands/ephemeral/static_host/list/index.js +0 -132
- package/dist/utils/microservice-wait.d.ts +0 -53
- package/dist/utils/microservice-wait.js +0 -83
package/README.md
CHANGED
|
@@ -46,16 +46,16 @@ The Metadata API pages unevenly, so `list` commands differ in what they expose.
|
|
|
46
46
|
| Commands | Flags | Footer |
|
|
47
47
|
|---|---|---|
|
|
48
48
|
| `function list`, `unit_test list`, `workflow_test list` (plus `sandbox` / `tenant` variants) | `--page`, `--per_page` | `Page 2 · 50 shown · next: --page 3` |
|
|
49
|
-
| `static_host list`, `static_host build list`
|
|
49
|
+
| `static_host list`, `static_host build list` | `--page` only | `Page 2 · 100 shown · 340 total` |
|
|
50
50
|
| `branch list`, `workspace list`, `tenant snapshot list`, `sandbox env list`, `tenant env list` | none | `12 branches` |
|
|
51
|
-
| `tenant list`, `release list`, `platform list`, `tenant cluster list
|
|
51
|
+
| `tenant list`, `release list`, `platform list`, `tenant cluster list` | none | none |
|
|
52
52
|
| `tenant backup list` | `--page` only (pre-existing) | none |
|
|
53
53
|
|
|
54
54
|
Notes:
|
|
55
55
|
|
|
56
56
|
- **`--per_page` is only offered where the endpoint accepts it.** Static-host endpoints hardcode 100 items per page server-side, so those commands take `--page` alone. `xano static_host list --per_page 10` previously parsed but did nothing; it is now rejected rather than silently ignored.
|
|
57
57
|
- **All paged commands default `--per_page` to 50.** The test-list commands previously requested 10000 internally so nothing was ever cut off; now that the footer and the JSON envelope both report position, a page that stops at 50 is visible rather than silent. Raise it (up to 10000) when you want everything in one call.
|
|
58
|
-
- **The last group has no CLI paging.** Those endpoints page server-side but return a plain array with no page or total metadata, so the CLI cannot tell you where you are. Rather than show a page number with no context — or infer "more available" from a full page, which is wrong exactly when the result count is a multiple of the page size — these commands are left as-is. **They return at most 25–50 items** (50 for tenants
|
|
58
|
+
- **The last group has no CLI paging.** Those endpoints page server-side but return a plain array with no page or total metadata, so the CLI cannot tell you where you are. Rather than show a page number with no context — or infer "more available" from a full page, which is wrong exactly when the result count is a multiple of the page size — these commands are left as-is. **They return at most 25–50 items** (50 for tenants, 25 for releases, platforms, and tenant backups). If you have more than that, query the Metadata API directly until those endpoints return paging metadata.
|
|
59
59
|
- **`--output json` returns an envelope, not a bare array.** Every list command emits `{items, ...}` using the Metadata API's own field names — `curPage`, `nextPage`, `prevPage`, `itemsTotal` — each included only when the endpoint actually reports it. `perPage` is the one field the CLI adds, since the API never echoes it back and it is otherwise invisible at its default. This is a **breaking change** for scripts that parsed the previous bare array — read `.items` instead.
|
|
60
60
|
|
|
61
61
|
```jsonc
|
|
@@ -487,7 +487,7 @@ Every `run_all` command takes `--concurrency` (default `1`):
|
|
|
487
487
|
|
|
488
488
|
```bash
|
|
489
489
|
xano unit_test run_all --concurrency 4
|
|
490
|
-
xano tenant unit_test run_all my-tenant --concurrency 8
|
|
490
|
+
xano tenant unit_test run_all --tenant my-tenant --concurrency 8
|
|
491
491
|
```
|
|
492
492
|
|
|
493
493
|
Output stays in test order regardless of concurrency, so a parallel run is still diffable against a sequential one, and the JSON `results` array keeps a stable order.
|
|
@@ -496,6 +496,48 @@ Output stays in test order regardless of concurrency, so a parallel run is still
|
|
|
496
496
|
|
|
497
497
|
**The default is 1 on purpose.** These tests execute against a shared workspace database, so tests that touch the same tables or records can interfere with each other when run at the same time — producing failures that look like flakiness rather than a real regression. Raise `--concurrency` once you know your tests are independent.
|
|
498
498
|
|
|
499
|
+
#### Exit codes
|
|
500
|
+
|
|
501
|
+
Every `run` and `run_all` command — on all four surfaces (`unit_test`, `workflow_test`, `sandbox`, `tenant`) — uses the same exit codes:
|
|
502
|
+
|
|
503
|
+
| Code | Meaning |
|
|
504
|
+
|------|---------|
|
|
505
|
+
| `0` | All tests passed, **or** there were no tests to run |
|
|
506
|
+
| `1` | At least one test failed |
|
|
507
|
+
| `2` | The command could not run the tests at all — bad workspace, auth failure, unreadable profile, or the test-list request failed |
|
|
508
|
+
|
|
509
|
+
**This is identical for `-o json` and summary output.** Adding `-o json` to get machine-readable output does not change the exit code, so a CI job can rely on it in either mode.
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
xano workflow_test run_all -o json || echo "tests failed or the command errored"
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**One asymmetry worth knowing about.** In `run`, a non-2xx response from the run endpoint is a CLI error and exits `2`. In `run_all`, a non-2xx response for an *individual* test is recorded as a failed test result with an `API error <status>` message and contributes to exit `1` — because `run_all`'s job is to finish the batch and report a roll-up rather than abort on one test's transport error. Exit `2` in `run_all` is reserved for failures that stop the batch from starting at all.
|
|
516
|
+
|
|
517
|
+
If you need to tell a backend outage apart from a genuine assertion failure, the exit code alone will not do it — inspect `results[].message` for the `API error` prefix:
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
xano workflow_test run_all -o json | jq '[.results[] | select(.message // "" | startswith("API error"))] | length'
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### JSON output
|
|
524
|
+
|
|
525
|
+
`run` returns the API's result object unchanged. Treat **any** `status` other than `"ok"` as a failure — several distinct non-`ok` values are returned in practice, so match on "not `ok`" rather than enumerating known failure values:
|
|
526
|
+
|
|
527
|
+
```json
|
|
528
|
+
{ "status": "exception", "timing": 0.02, "message": "Precondition failed." }
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
`run_all` returns a roll-up. A branch with no tests is a normal state — freshly created branches routinely have none — and still emits valid JSON rather than plain text, so `JSON.parse` is always safe:
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
{ "passed": 0, "failed": 0, "results": [], "total_timing": 0 }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
The `total_timing` field is currently emitted by `workflow_test run_all` only; the `unit_test`, `tenant`, and `sandbox` variants omit it. The empty-suite envelope always carries the same keys as that command's populated envelope.
|
|
538
|
+
|
|
539
|
+
> **Changed in this release:** a failing `run` in summary mode previously exited `2` and printed a spurious `EEXIT: 1` line; it now exits `1` with clean output. A failing `run` with `-o json` previously exited `0`, which silently hid failures from CI. If you match on exit code `2` specifically to detect test failures, switch to a non-zero check.
|
|
540
|
+
|
|
499
541
|
### Tenants
|
|
500
542
|
|
|
501
543
|
Manage tenants, their environment variables, backups, deployments, and clusters.
|
|
@@ -652,64 +694,6 @@ xano tenant cluster license set <cluster_id>
|
|
|
652
694
|
xano tenant cluster license set <cluster_id> --file ./kubeconfig.yaml
|
|
653
695
|
```
|
|
654
696
|
|
|
655
|
-
### Ephemeral Tenants
|
|
656
|
-
|
|
657
|
-
Manage ephemeral tenants — short-lived, auto-expiring tenants scoped to a workspace (default TTL 1h, max 72h). Unlike a sandbox, an ephemeral tenant requires a workspace.
|
|
658
|
-
|
|
659
|
-
```bash
|
|
660
|
-
# List ephemeral tenants in the current workspace
|
|
661
|
-
xano ephemeral list
|
|
662
|
-
xano ephemeral list -w 5
|
|
663
|
-
|
|
664
|
-
# List ephemeral tenants across every workspace you can access
|
|
665
|
-
xano ephemeral list --global
|
|
666
|
-
|
|
667
|
-
# Create an ephemeral tenant (workspace required)
|
|
668
|
-
xano ephemeral create "PR preview"
|
|
669
|
-
xano ephemeral create "Demo" --expires-hours 24 -w 5
|
|
670
|
-
xano ephemeral create "Load test" -d "48h soak" --expires-hours 48
|
|
671
|
-
|
|
672
|
-
# Get / edit / delete
|
|
673
|
-
xano ephemeral get <tenant_name>
|
|
674
|
-
xano ephemeral edit <tenant_name> --display "New Name" -d "New description"
|
|
675
|
-
xano ephemeral delete <tenant_name> --force
|
|
676
|
-
|
|
677
|
-
# Pull to / push from local files (multidoc)
|
|
678
|
-
xano ephemeral pull <tenant_name> -d ./my-ephemeral
|
|
679
|
-
xano ephemeral push <tenant_name> -d ./my-ephemeral --dry-run # preview first
|
|
680
|
-
xano ephemeral push <tenant_name> -d ./my-ephemeral
|
|
681
|
-
|
|
682
|
-
# Open in the browser (or print the URL)
|
|
683
|
-
xano ephemeral impersonate <tenant_name>
|
|
684
|
-
xano ephemeral impersonate <tenant_name> --url-only
|
|
685
|
-
xano ephemeral impersonate <tenant_name> --guest # read-only session (browse only)
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
#### Static hosting for an ephemeral tenant
|
|
689
|
-
|
|
690
|
-
An ephemeral tenant can host static sites, scoped to that tenant. These commands mirror
|
|
691
|
-
`xano static_host *` but take the tenant name as the first argument (the tenant's static
|
|
692
|
-
hosting is isolated inside the tenant's own database).
|
|
693
|
-
|
|
694
|
-
```bash
|
|
695
|
-
# List / create / inspect a tenant's static hosts
|
|
696
|
-
xano ephemeral static_host list <tenant_name>
|
|
697
|
-
xano ephemeral static_host create <tenant_name> --name marketing --description "Marketing site"
|
|
698
|
-
xano ephemeral static_host get <tenant_name> -H marketing
|
|
699
|
-
xano ephemeral static_host edit <tenant_name> -H marketing --description "Updated"
|
|
700
|
-
|
|
701
|
-
# Builds: push a directory, list, inspect, deploy to an env, pull, delete
|
|
702
|
-
xano ephemeral static_host build push <tenant_name> -H default -f ./site
|
|
703
|
-
xano ephemeral static_host build list <tenant_name> -H default
|
|
704
|
-
xano ephemeral static_host build get <tenant_name> -H default --build_id 52
|
|
705
|
-
xano ephemeral static_host deploy <tenant_name> -H default --build_id 52 --env prod
|
|
706
|
-
xano ephemeral static_host build pull <tenant_name> -H default --latest
|
|
707
|
-
xano ephemeral static_host build delete <tenant_name> -H default --build_id 52
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
> Static hosting is currently available for **local** tenants. Remote (tier2/tier3)
|
|
711
|
-
> tenants are not yet supported and will return an error.
|
|
712
|
-
|
|
713
697
|
### Sandbox
|
|
714
698
|
|
|
715
699
|
Manage your sandbox tenant. Each user has a single sandbox tenant that is auto-provisioned on first use.
|
package/dist/base-command.d.ts
CHANGED
|
@@ -117,7 +117,6 @@ export default abstract class BaseCommand extends Command {
|
|
|
117
117
|
protected logStaticHostUrls(opts: {
|
|
118
118
|
profile: ProfileConfig;
|
|
119
119
|
staticHost: string;
|
|
120
|
-
tenantName?: string;
|
|
121
120
|
verbose: boolean;
|
|
122
121
|
workspaceId: string;
|
|
123
122
|
}): Promise<void>;
|
|
@@ -127,7 +126,6 @@ export default abstract class BaseCommand extends Command {
|
|
|
127
126
|
profile: ProfileConfig;
|
|
128
127
|
quiet?: boolean;
|
|
129
128
|
staticHost: string;
|
|
130
|
-
tenantName?: string;
|
|
131
129
|
timeoutMs?: number;
|
|
132
130
|
verbose: boolean;
|
|
133
131
|
workspaceId: string;
|
package/dist/base-command.js
CHANGED
|
@@ -370,11 +370,8 @@ export default class BaseCommand extends Command {
|
|
|
370
370
|
* Returns the final status. Resolves to the last-seen status on timeout.
|
|
371
371
|
*/
|
|
372
372
|
async logStaticHostUrls(opts) {
|
|
373
|
-
const { profile, staticHost,
|
|
374
|
-
|
|
375
|
-
// workspace-level static host (default, unchanged behavior).
|
|
376
|
-
const scope = tenantName ? `/tenant/${tenantName}` : '';
|
|
377
|
-
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}${scope}/static_host/${staticHost}`;
|
|
373
|
+
const { profile, staticHost, verbose, workspaceId } = opts;
|
|
374
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}`;
|
|
378
375
|
try {
|
|
379
376
|
const response = await this.verboseFetch(url, {
|
|
380
377
|
headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` },
|
|
@@ -397,14 +394,11 @@ export default class BaseCommand extends Command {
|
|
|
397
394
|
}
|
|
398
395
|
}
|
|
399
396
|
async waitForBuild(opts) {
|
|
400
|
-
const { buildId, profile, quiet, staticHost,
|
|
397
|
+
const { buildId, profile, quiet, staticHost, verbose, workspaceId } = opts;
|
|
401
398
|
const intervalMs = opts.intervalMs ?? 2000;
|
|
402
399
|
const timeoutMs = opts.timeoutMs ?? 600_000; // 10 min
|
|
403
400
|
const terminal = new Set(['error', 'ok']);
|
|
404
|
-
|
|
405
|
-
// workspace-level static host (default, unchanged behavior).
|
|
406
|
-
const scope = tenantName ? `/tenant/${tenantName}` : '';
|
|
407
|
-
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}${scope}/static_host/${staticHost}/build/${buildId}`;
|
|
401
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/static_host/${staticHost}/build/${buildId}`;
|
|
408
402
|
// Spinner only on an interactive TTY and when not emitting JSON. Verbose mode
|
|
409
403
|
// also disables it (the spinner would interleave with request/response logs).
|
|
410
404
|
const animate = Boolean(process.stdout.isTTY) && !quiet && !verbose;
|
|
@@ -14,4 +14,10 @@ export default class ReleasePull extends BaseCommand {
|
|
|
14
14
|
};
|
|
15
15
|
run(): Promise<void>;
|
|
16
16
|
private resolveReleaseName;
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize a document name for use as a filename.
|
|
19
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
20
|
+
* characters that are unsafe in filenames.
|
|
21
|
+
*/
|
|
22
|
+
private sanitizeFilename;
|
|
17
23
|
}
|
|
@@ -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,
|
|
6
|
+
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class ReleasePull extends BaseCommand {
|
|
8
8
|
static description = 'Pull a release multidoc from the Xano Metadata API and split into individual files';
|
|
9
9
|
static examples = [
|
|
@@ -123,19 +123,96 @@ Pulled 58 documents from release 'v1.0'
|
|
|
123
123
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
124
124
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
125
125
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
126
|
-
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
127
|
-
// messages can nest under their channel's server (see resolver docs).
|
|
128
|
-
const getChannelServer = buildChannelServerResolver(documents);
|
|
129
126
|
// Track filenames per type to handle duplicates
|
|
130
127
|
const filenameCounters = new Map();
|
|
131
128
|
let writtenCount = 0;
|
|
132
129
|
for (const doc of documents) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
let typeDir;
|
|
131
|
+
let baseName;
|
|
132
|
+
if (doc.type === 'workspace') {
|
|
133
|
+
// workspace → workspace/{name}.xs
|
|
134
|
+
typeDir = path.join(outputDir, 'workspace');
|
|
135
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
136
|
+
}
|
|
137
|
+
else if (doc.type === 'workspace_trigger') {
|
|
138
|
+
// workspace_trigger → workspace/trigger/{name}.xs
|
|
139
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
140
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
141
|
+
}
|
|
142
|
+
else if (doc.type === 'error_trigger') {
|
|
143
|
+
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
144
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
145
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
146
|
+
}
|
|
147
|
+
else if (doc.type === 'agent') {
|
|
148
|
+
// agent → ai/agent/{name}.xs
|
|
149
|
+
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
150
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
151
|
+
}
|
|
152
|
+
else if (doc.type === 'mcp_server') {
|
|
153
|
+
// mcp_server → ai/mcp_server/{name}.xs
|
|
154
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
155
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
156
|
+
}
|
|
157
|
+
else if (doc.type === 'tool') {
|
|
158
|
+
// tool → ai/tool/{name}.xs
|
|
159
|
+
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
160
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
161
|
+
}
|
|
162
|
+
else if (doc.type === 'agent_trigger') {
|
|
163
|
+
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
164
|
+
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
165
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
166
|
+
}
|
|
167
|
+
else if (doc.type === 'mcp_server_trigger') {
|
|
168
|
+
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
169
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
170
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
171
|
+
}
|
|
172
|
+
else if (doc.type === 'table_trigger') {
|
|
173
|
+
// table_trigger → table/trigger/{name}.xs
|
|
174
|
+
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
175
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
176
|
+
}
|
|
177
|
+
else if (doc.type === 'realtime_channel') {
|
|
178
|
+
// realtime_channel → realtime/channel/{name}.xs
|
|
179
|
+
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
180
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
181
|
+
}
|
|
182
|
+
else if (doc.type === 'realtime_trigger') {
|
|
183
|
+
// realtime_trigger → realtime/trigger/{name}.xs
|
|
184
|
+
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
185
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
186
|
+
}
|
|
187
|
+
else if (doc.type === 'api_group') {
|
|
188
|
+
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
189
|
+
const groupFolder = getApiGroupFolder(doc.name);
|
|
190
|
+
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
191
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
+
}
|
|
193
|
+
else if (doc.type === 'query' && doc.apiGroup) {
|
|
194
|
+
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
195
|
+
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
196
|
+
const nameParts = doc.name.split('/');
|
|
197
|
+
const leafName = nameParts.pop();
|
|
198
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
199
|
+
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
200
|
+
baseName = this.sanitizeFilename(leafName);
|
|
201
|
+
if (doc.verb) {
|
|
202
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Default: split folder path from name
|
|
207
|
+
const nameParts = doc.name.split('/');
|
|
208
|
+
const leafName = nameParts.pop();
|
|
209
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
210
|
+
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
211
|
+
baseName = this.sanitizeFilename(leafName);
|
|
212
|
+
if (doc.verb) {
|
|
213
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
139
216
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
140
217
|
// Track duplicates per directory
|
|
141
218
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -180,4 +257,12 @@ Pulled 58 documents from release 'v1.0'
|
|
|
180
257
|
}
|
|
181
258
|
return match.id;
|
|
182
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Sanitize a document name for use as a filename.
|
|
262
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
263
|
+
* characters that are unsafe in filenames.
|
|
264
|
+
*/
|
|
265
|
+
sanitizeFilename(name) {
|
|
266
|
+
return snakeCase(name.replaceAll('"', ''));
|
|
267
|
+
}
|
|
183
268
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import snakeCase from 'lodash.snakecase';
|
|
3
3
|
import BaseCommand from '../../../base-command.js';
|
|
4
|
-
import { buildApiGroupFolderResolver,
|
|
4
|
+
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
5
5
|
import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
@@ -92,18 +92,82 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
92
92
|
const outputDir = path.resolve(flags.directory);
|
|
93
93
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
94
94
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
95
|
-
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
96
|
-
// messages can nest under their channel's server (see resolver docs).
|
|
97
|
-
const getChannelServer = buildChannelServerResolver(documents);
|
|
98
95
|
const filenameCounters = new Map();
|
|
99
96
|
let writtenCount = 0;
|
|
100
97
|
for (const doc of documents) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
98
|
+
let typeDir;
|
|
99
|
+
let baseName;
|
|
100
|
+
if (doc.type === 'workspace') {
|
|
101
|
+
typeDir = path.join(outputDir, 'workspace');
|
|
102
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
103
|
+
}
|
|
104
|
+
else if (doc.type === 'workspace_trigger') {
|
|
105
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
106
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
107
|
+
}
|
|
108
|
+
else if (doc.type === 'error_trigger') {
|
|
109
|
+
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
110
|
+
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
111
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
112
|
+
}
|
|
113
|
+
else if (doc.type === 'agent') {
|
|
114
|
+
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
115
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
116
|
+
}
|
|
117
|
+
else if (doc.type === 'mcp_server') {
|
|
118
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
119
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
120
|
+
}
|
|
121
|
+
else if (doc.type === 'tool') {
|
|
122
|
+
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
123
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
124
|
+
}
|
|
125
|
+
else if (doc.type === 'agent_trigger') {
|
|
126
|
+
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
127
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
128
|
+
}
|
|
129
|
+
else if (doc.type === 'mcp_server_trigger') {
|
|
130
|
+
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
131
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
132
|
+
}
|
|
133
|
+
else if (doc.type === 'table_trigger') {
|
|
134
|
+
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
135
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
136
|
+
}
|
|
137
|
+
else if (doc.type === 'realtime_channel') {
|
|
138
|
+
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
139
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
140
|
+
}
|
|
141
|
+
else if (doc.type === 'realtime_trigger') {
|
|
142
|
+
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
143
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
144
|
+
}
|
|
145
|
+
else if (doc.type === 'api_group') {
|
|
146
|
+
const groupFolder = getApiGroupFolder(doc.name);
|
|
147
|
+
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
148
|
+
baseName = this.sanitizeFilename(doc.name);
|
|
149
|
+
}
|
|
150
|
+
else if (doc.type === 'query' && doc.apiGroup) {
|
|
151
|
+
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
152
|
+
const nameParts = doc.name.split('/');
|
|
153
|
+
const leafName = nameParts.pop();
|
|
154
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
155
|
+
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
156
|
+
baseName = this.sanitizeFilename(leafName);
|
|
157
|
+
if (doc.verb) {
|
|
158
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const nameParts = doc.name.split('/');
|
|
163
|
+
const leafName = nameParts.pop();
|
|
164
|
+
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
165
|
+
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
166
|
+
baseName = this.sanitizeFilename(leafName);
|
|
167
|
+
if (doc.verb) {
|
|
168
|
+
baseName = `${baseName}_${doc.verb}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
107
171
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
108
172
|
const dirKey = path.relative(outputDir, typeDir);
|
|
109
173
|
if (!filenameCounters.has(dirKey)) {
|
|
@@ -130,4 +194,7 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
|
|
|
130
194
|
parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
|
|
131
195
|
this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
|
|
132
196
|
}
|
|
197
|
+
sanitizeFilename(name) {
|
|
198
|
+
return snakeCase(name.replaceAll('"', ''));
|
|
199
|
+
}
|
|
133
200
|
}
|
|
@@ -46,24 +46,30 @@ Result: PASS
|
|
|
46
46
|
this.error(message);
|
|
47
47
|
}
|
|
48
48
|
const result = (await response.json());
|
|
49
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
50
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
51
|
+
const passed = result.status === 'ok';
|
|
49
52
|
if (flags.output === 'json') {
|
|
50
53
|
this.log(JSON.stringify(result, null, 2));
|
|
51
54
|
}
|
|
55
|
+
else if (passed) {
|
|
56
|
+
this.log('Result: PASS');
|
|
57
|
+
}
|
|
52
58
|
else {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
59
|
-
for (const expect of failedExpects) {
|
|
60
|
-
if (expect.message) {
|
|
61
|
-
this.log(` Error: ${expect.message}`);
|
|
62
|
-
}
|
|
59
|
+
this.log('Result: FAIL');
|
|
60
|
+
const failedExpects = result.results?.filter((r) => r.status === 'fail') ?? [];
|
|
61
|
+
for (const expect of failedExpects) {
|
|
62
|
+
if (expect.message) {
|
|
63
|
+
this.log(` Error: ${expect.message}`);
|
|
63
64
|
}
|
|
64
|
-
this.exit(1);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
68
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
69
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
70
|
+
if (!passed) {
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
|
67
73
|
}
|
|
68
74
|
catch (error) {
|
|
69
75
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -78,7 +78,15 @@ Results: 4 passed, 1 failed
|
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
80
|
if (tests.length === 0) {
|
|
81
|
-
|
|
81
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
82
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
83
|
+
// with the same keys the populated path emits below.
|
|
84
|
+
if (flags.output === 'json') {
|
|
85
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.log('No unit tests found');
|
|
89
|
+
}
|
|
82
90
|
return;
|
|
83
91
|
}
|
|
84
92
|
if (flags.output === 'summary') {
|
|
@@ -46,22 +46,25 @@ Result: PASS (0.25s)
|
|
|
46
46
|
this.error(message);
|
|
47
47
|
}
|
|
48
48
|
const result = (await response.json());
|
|
49
|
+
// Anything other than 'ok' is a failure -- the API has been observed
|
|
50
|
+
// returning several distinct non-ok statuses, so never enumerate them.
|
|
51
|
+
const passed = result.status === 'ok';
|
|
49
52
|
if (flags.output === 'json') {
|
|
50
53
|
this.log(JSON.stringify(result, null, 2));
|
|
51
54
|
}
|
|
52
55
|
else {
|
|
53
56
|
const timing = result.timing ? ` (${result.timing}s)` : '';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else {
|
|
58
|
-
this.log(`Result: FAIL${timing}`);
|
|
59
|
-
if (result.message) {
|
|
60
|
-
this.log(` Error: ${result.message}`);
|
|
61
|
-
}
|
|
62
|
-
this.exit(1);
|
|
57
|
+
this.log(`Result: ${passed ? 'PASS' : 'FAIL'}${timing}`);
|
|
58
|
+
if (!passed && result.message) {
|
|
59
|
+
this.log(` Error: ${result.message}`);
|
|
63
60
|
}
|
|
64
61
|
}
|
|
62
|
+
// Set the code rather than throwing via this.exit(): the exit must apply
|
|
63
|
+
// in both output modes, and process.exitCode cannot be swallowed by the
|
|
64
|
+
// surrounding catch. Mirrors what run_all already does.
|
|
65
|
+
if (!passed) {
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
70
|
if (error instanceof Error && 'oclif' in error)
|
|
@@ -71,7 +71,15 @@ Results: 2 passed, 1 failed
|
|
|
71
71
|
},
|
|
72
72
|
});
|
|
73
73
|
if (tests.length === 0) {
|
|
74
|
-
|
|
74
|
+
// An empty suite is a normal state (fresh branches have no tests) and
|
|
75
|
+
// is a success, not a failure. Under -o json it must still be parseable,
|
|
76
|
+
// with the same keys the populated path emits below.
|
|
77
|
+
if (flags.output === 'json') {
|
|
78
|
+
this.log(JSON.stringify({ failed: 0, passed: 0, results: [] }, null, 2));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.log('No workflow tests found');
|
|
82
|
+
}
|
|
75
83
|
return;
|
|
76
84
|
}
|
|
77
85
|
if (flags.output === 'summary') {
|
|
@@ -14,4 +14,10 @@ export default class Pull extends BaseCommand {
|
|
|
14
14
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
15
|
};
|
|
16
16
|
run(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize a document name for use as a filename.
|
|
19
|
+
* Strips quotes, replaces spaces with underscores, and removes
|
|
20
|
+
* characters that are unsafe in filenames.
|
|
21
|
+
*/
|
|
22
|
+
private sanitizeFilename;
|
|
17
23
|
}
|