@xano/cli 1.0.6-beta.0 → 1.0.6-beta.10
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 +124 -7
- package/dist/base-command.d.ts +2 -0
- package/dist/base-command.js +10 -4
- package/dist/commands/branch/list/index.js +5 -1
- package/dist/commands/ephemeral/impersonate/index.d.ts +1 -0
- package/dist/commands/ephemeral/impersonate/index.js +11 -3
- package/dist/commands/ephemeral/list/index.js +2 -1
- package/dist/commands/ephemeral/pull/index.d.ts +3 -7
- package/dist/commands/ephemeral/pull/index.js +25 -109
- package/dist/commands/ephemeral/push/index.d.ts +13 -1
- package/dist/commands/ephemeral/push/index.js +94 -16
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
- package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/create/index.js +92 -0
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
- package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/get/index.js +101 -0
- package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
- package/dist/commands/ephemeral/static_host/list/index.js +132 -0
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/list/index.d.ts +4 -4
- package/dist/commands/function/list/index.js +30 -38
- package/dist/commands/knowledge/list/index.js +2 -1
- package/dist/commands/platform/list/index.js +13 -14
- package/dist/commands/release/list/index.js +13 -14
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +10 -95
- package/dist/commands/sandbox/env/list/index.js +4 -0
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +10 -77
- package/dist/commands/sandbox/unit_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/unit_test/list/index.js +9 -14
- package/dist/commands/sandbox/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/unit_test/run_all/index.js +67 -37
- package/dist/commands/sandbox/workflow_test/list/index.d.ts +2 -0
- package/dist/commands/sandbox/workflow_test/list/index.js +9 -14
- package/dist/commands/sandbox/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/sandbox/workflow_test/run_all/index.js +65 -35
- package/dist/commands/static_host/build/list/index.d.ts +2 -3
- package/dist/commands/static_host/build/list/index.js +19 -40
- package/dist/commands/static_host/build/pull/index.js +12 -9
- package/dist/commands/static_host/list/index.d.ts +2 -3
- package/dist/commands/static_host/list/index.js +20 -40
- package/dist/commands/static_host/migrate/index.js +14 -10
- package/dist/commands/tenant/backup/list/index.js +13 -14
- package/dist/commands/tenant/cluster/list/index.js +9 -10
- package/dist/commands/tenant/env/list/index.js +4 -0
- package/dist/commands/tenant/list/index.js +19 -20
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +10 -95
- package/dist/commands/tenant/snapshot/list/index.js +5 -1
- package/dist/commands/tenant/unit_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/unit_test/list/index.js +9 -14
- package/dist/commands/tenant/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +67 -37
- package/dist/commands/tenant/workflow_test/list/index.d.ts +4 -2
- package/dist/commands/tenant/workflow_test/list/index.js +9 -14
- package/dist/commands/tenant/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/tenant/workflow_test/run_all/index.js +65 -35
- package/dist/commands/unit_test/list/index.d.ts +3 -1
- package/dist/commands/unit_test/list/index.js +9 -14
- package/dist/commands/unit_test/run_all/index.d.ts +1 -0
- package/dist/commands/unit_test/run_all/index.js +68 -40
- package/dist/commands/workflow_test/list/index.d.ts +3 -1
- package/dist/commands/workflow_test/list/index.js +10 -13
- package/dist/commands/workflow_test/run_all/index.d.ts +1 -0
- package/dist/commands/workflow_test/run_all/index.js +67 -36
- package/dist/commands/workspace/git/pull/index.js +53 -3
- package/dist/commands/workspace/list/index.js +7 -13
- package/dist/commands/workspace/pull/index.d.ts +0 -6
- package/dist/commands/workspace/pull/index.js +10 -95
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.js +54 -0
- package/dist/utils/document-parser.d.ts +116 -0
- package/dist/utils/document-parser.js +274 -1
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -5
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +83 -0
- package/dist/utils/multidoc-push.d.ts +12 -0
- package/dist/utils/multidoc-push.js +84 -0
- package/dist/utils/paging.d.ts +145 -0
- package/dist/utils/paging.js +199 -0
- package/dist/utils/reference-checker.js +13 -0
- package/oclif.manifest.json +5038 -3635
- package/package.json +7 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Outcome of flattening one bundle file. */
|
|
2
|
+
export interface FlattenResult {
|
|
3
|
+
/** True when the original bundle was removed. */
|
|
4
|
+
removedSource: boolean;
|
|
5
|
+
/** Why the file was skipped, if it was (empty / single-document). */
|
|
6
|
+
skipped?: 'empty' | 'single';
|
|
7
|
+
/** Absolute paths written (or that would be written, in dry-run). */
|
|
8
|
+
written: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface FlattenOptions {
|
|
11
|
+
/** Preview only — compute the plan and write nothing, delete nothing. */
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
/** Overwrite existing destination files instead of throwing on a clash. */
|
|
14
|
+
force?: boolean;
|
|
15
|
+
/** Keep the original bundle file (default: delete after a successful split). */
|
|
16
|
+
keepSource?: boolean;
|
|
17
|
+
/** Optional line logger (e.g. command.log) for a per-file plan. */
|
|
18
|
+
log?: (msg: string) => void;
|
|
19
|
+
/** Output root (default: the bundle file's own directory). */
|
|
20
|
+
outputDir?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Split ONE multi-document `.xs` bundle (documents joined by `---`) into the
|
|
24
|
+
* standard per-document tree — the exact layout `pull` produces — using the
|
|
25
|
+
* shared `placeDocuments` placement so the two never drift.
|
|
26
|
+
*
|
|
27
|
+
* Filesystem-touching, but self-contained: it reads the bundle, resolves every
|
|
28
|
+
* document's destination, checks for collisions, writes, and (unless
|
|
29
|
+
* `keepSource`) removes the original. Throws on a destination collision when
|
|
30
|
+
* `force` is not set, and never deletes the source when a written document
|
|
31
|
+
* landed on the source's own path.
|
|
32
|
+
*
|
|
33
|
+
* Shared by the `flatten` command and the push-time prompt, so a user who
|
|
34
|
+
* confirms "flatten now?" gets byte-identical output to running `xano flatten`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function flattenBundleFile(bundleFile: string, opts?: FlattenOptions): FlattenResult;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import snakeCase from 'lodash.snakecase';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { placeDocuments, splitMultidoc } from './document-parser.js';
|
|
5
|
+
/**
|
|
6
|
+
* Split ONE multi-document `.xs` bundle (documents joined by `---`) into the
|
|
7
|
+
* standard per-document tree — the exact layout `pull` produces — using the
|
|
8
|
+
* shared `placeDocuments` placement so the two never drift.
|
|
9
|
+
*
|
|
10
|
+
* Filesystem-touching, but self-contained: it reads the bundle, resolves every
|
|
11
|
+
* document's destination, checks for collisions, writes, and (unless
|
|
12
|
+
* `keepSource`) removes the original. Throws on a destination collision when
|
|
13
|
+
* `force` is not set, and never deletes the source when a written document
|
|
14
|
+
* landed on the source's own path.
|
|
15
|
+
*
|
|
16
|
+
* Shared by the `flatten` command and the push-time prompt, so a user who
|
|
17
|
+
* confirms "flatten now?" gets byte-identical output to running `xano flatten`.
|
|
18
|
+
*/
|
|
19
|
+
export function flattenBundleFile(bundleFile, opts = {}) {
|
|
20
|
+
const { dryRun = false, force = false, keepSource = false, log } = opts;
|
|
21
|
+
const content = fs.readFileSync(bundleFile, 'utf8').trim();
|
|
22
|
+
const documents = splitMultidoc(content);
|
|
23
|
+
if (documents.length === 0)
|
|
24
|
+
return { removedSource: false, skipped: 'empty', written: [] };
|
|
25
|
+
if (documents.length === 1)
|
|
26
|
+
return { removedSource: false, skipped: 'single', written: [] };
|
|
27
|
+
const outputDir = opts.outputDir ? resolve(opts.outputDir) : dirname(bundleFile);
|
|
28
|
+
const placed = placeDocuments(documents, { join, relative, snakeCase });
|
|
29
|
+
// Pre-check destination collisions (unless --force / dry-run). A written file
|
|
30
|
+
// landing on the source's own path is not a clash — it replaces the bundle.
|
|
31
|
+
if (!dryRun && !force) {
|
|
32
|
+
const clashes = placed
|
|
33
|
+
.map((p) => join(outputDir, p.relPath))
|
|
34
|
+
.filter((abs) => fs.existsSync(abs) && resolve(abs) !== resolve(bundleFile));
|
|
35
|
+
if (clashes.length > 0) {
|
|
36
|
+
throw new Error(`Destination file(s) already exist (use --force to overwrite):\n` +
|
|
37
|
+
clashes.map((c) => ` ${relative(process.cwd(), c) || c}`).join('\n'));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const written = [];
|
|
41
|
+
for (const p of placed) {
|
|
42
|
+
const abs = join(outputDir, p.relPath);
|
|
43
|
+
if (log)
|
|
44
|
+
log(` WRITE ${relative(process.cwd(), abs) || p.relPath}`);
|
|
45
|
+
if (!dryRun) {
|
|
46
|
+
fs.mkdirSync(dirname(abs), { recursive: true });
|
|
47
|
+
fs.writeFileSync(abs, p.content.endsWith('\n') ? p.content : `${p.content}\n`, 'utf8');
|
|
48
|
+
}
|
|
49
|
+
written.push(abs);
|
|
50
|
+
}
|
|
51
|
+
// Remove the original bundle unless asked to keep it, and never when a split
|
|
52
|
+
// document was written over the source's own path.
|
|
53
|
+
let removedSource = false;
|
|
54
|
+
if (!dryRun && !keepSource) {
|
|
55
|
+
const wroteOverSource = placed.some((p) => resolve(join(outputDir, p.relPath)) === resolve(bundleFile));
|
|
56
|
+
if (wroteOverSource) {
|
|
57
|
+
if (log)
|
|
58
|
+
log(` (source ${basename(bundleFile)} was overwritten by a split document; not deleting)`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
fs.rmSync(bundleFile);
|
|
62
|
+
removedSource = true;
|
|
63
|
+
if (log)
|
|
64
|
+
log(` removed ${relative(process.cwd(), bundleFile) || bundleFile}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { removedSource, written };
|
|
68
|
+
}
|
|
@@ -17,6 +17,7 @@ export interface KnowledgeObject {
|
|
|
17
17
|
mode?: string;
|
|
18
18
|
name: string;
|
|
19
19
|
scope?: string;
|
|
20
|
+
tag?: string[];
|
|
20
21
|
}
|
|
21
22
|
/** A locally-collected object that also remembers its primary file on disk (for GUID writeback). */
|
|
22
23
|
export interface LocalKnowledgeObject extends KnowledgeObject {
|
|
@@ -12,7 +12,7 @@ import { dirname, join, relative, sep } from 'node:path';
|
|
|
12
12
|
// let the server dictate file paths.
|
|
13
13
|
export const KNOWLEDGE_DIR = 'knowledge';
|
|
14
14
|
// ── Frontmatter ──────────────────────────────────────────────────────────────
|
|
15
|
-
const FRONTMATTER_ORDER = ['name', 'description', 'knowledge_type', '
|
|
15
|
+
const FRONTMATTER_ORDER = ['name', 'description', 'knowledge_type', 'inclusion', 'tags', 'guid'];
|
|
16
16
|
/**
|
|
17
17
|
* Build a primary `.md` file body: YAML frontmatter (built from the object's
|
|
18
18
|
* structured fields) followed by the markdown content.
|
|
@@ -23,12 +23,10 @@ export function buildPrimaryContent(obj) {
|
|
|
23
23
|
if (obj.description !== undefined)
|
|
24
24
|
meta.description = obj.description;
|
|
25
25
|
meta.knowledge_type = obj.knowledge_type;
|
|
26
|
-
if (obj.scope !== undefined)
|
|
27
|
-
meta.scope = obj.scope;
|
|
28
26
|
if (obj.mode !== undefined)
|
|
29
27
|
meta.inclusion = modeToInclusion(obj.mode);
|
|
30
|
-
if (obj.
|
|
31
|
-
meta.
|
|
28
|
+
if (obj.tag !== undefined && obj.tag.length > 0)
|
|
29
|
+
meta.tags = obj.tag;
|
|
32
30
|
if (obj.guid !== undefined)
|
|
33
31
|
meta.guid = obj.guid;
|
|
34
32
|
// Emit keys in a stable, human-friendly order.
|
|
@@ -107,6 +105,10 @@ const MODE_TO_INCLUSION = {
|
|
|
107
105
|
function modeToInclusion(mode) {
|
|
108
106
|
return MODE_TO_INCLUSION[mode] ?? mode;
|
|
109
107
|
}
|
|
108
|
+
/** Parse the on-disk `tags:` frontmatter value into a string array, or `undefined` if absent/malformed. */
|
|
109
|
+
function parseTags(tags) {
|
|
110
|
+
return Array.isArray(tags) && tags.every((t) => typeof t === 'string') ? tags : undefined;
|
|
111
|
+
}
|
|
110
112
|
// ── Path mapping (CLI-owned layout) ────────────────────────────────────────────
|
|
111
113
|
/**
|
|
112
114
|
* Allocate a unique snake_case folder/file segment per call, suffixing
|
|
@@ -246,6 +248,9 @@ export function collectKnowledgeObjects(inputDir, include, exclude) {
|
|
|
246
248
|
};
|
|
247
249
|
if (typeof meta.guid === 'string')
|
|
248
250
|
obj.guid = meta.guid;
|
|
251
|
+
const tags = parseTags(meta.tags);
|
|
252
|
+
if (tags)
|
|
253
|
+
obj.tag = tags;
|
|
249
254
|
if (knowledgeType === 'skill') {
|
|
250
255
|
const skillFolder = posixPath.slice('skills/'.length, posixPath.indexOf('/SKILL.md'));
|
|
251
256
|
const refPrefix = `skills/${skillFolder}/references/`;
|
|
@@ -345,6 +350,7 @@ function canonical(obj) {
|
|
|
345
350
|
files,
|
|
346
351
|
mode: obj.mode ?? '',
|
|
347
352
|
scope: obj.scope ?? '',
|
|
353
|
+
tag: [...(obj.tag ?? [])].sort(),
|
|
348
354
|
});
|
|
349
355
|
}
|
|
350
356
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll a tenant's microservice deployment status until every auto-deployed
|
|
3
|
+
* microservice settles (ok/error) or a timeout elapses. Backs `ephemeral push
|
|
4
|
+
* --wait`: after a multidoc push auto-deploys microservices (tenant_deploy=
|
|
5
|
+
* "auto"), the CLI waits here for them to become Ready and reports the outcome.
|
|
6
|
+
*
|
|
7
|
+
* The backend route (GET .../tenant/{name}/microservice, statement
|
|
8
|
+
* mvp:microservice_status_all) returns one entry per microservice with a live
|
|
9
|
+
* k8s readiness read. `manual`/`disabled` microservices are reported but not
|
|
10
|
+
* waited on (they were never deployed by the push).
|
|
11
|
+
*/
|
|
12
|
+
/** One microservice's status as returned by the tenant status route. */
|
|
13
|
+
export interface MicroserviceStatusEntry {
|
|
14
|
+
desired?: number;
|
|
15
|
+
detail?: string;
|
|
16
|
+
microservice_id: number;
|
|
17
|
+
name: string;
|
|
18
|
+
ready?: number;
|
|
19
|
+
status: string;
|
|
20
|
+
tenant_deploy?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface WaitOptions {
|
|
23
|
+
/** Bearer token for the authed status GET. */
|
|
24
|
+
accessToken: string;
|
|
25
|
+
/** Called once per poll with the latest entries, for progress rendering. */
|
|
26
|
+
onPoll?: (entries: MicroserviceStatusEntry[], elapsedMs: number) => void;
|
|
27
|
+
/** Poll cadence in ms (default 2000). */
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
/** Absolute URL of the tenant microservice status route. */
|
|
30
|
+
statusUrl: string;
|
|
31
|
+
/** Overall wait budget in ms (default 300000 = 300s). */
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
/** Authed fetch (command.verboseFetch bound), so TLS/dispatcher settings apply. */
|
|
34
|
+
verboseFetch: (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
|
|
35
|
+
/** Verbose request logging passthrough. */
|
|
36
|
+
verbose?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface WaitResult {
|
|
39
|
+
/** The final status entries observed. */
|
|
40
|
+
entries: MicroserviceStatusEntry[];
|
|
41
|
+
/** True if any awaited (auto) microservice ended in "error". */
|
|
42
|
+
hadError: boolean;
|
|
43
|
+
/** True if the wait budget elapsed before all awaited microservices settled. */
|
|
44
|
+
timedOut: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** An awaited microservice is one that was auto-deployed and is not skipped/disabled. */
|
|
47
|
+
export declare function isAwaited(entry: MicroserviceStatusEntry): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Poll until all awaited microservices settle or the timeout elapses. Transient
|
|
50
|
+
* poll failures (network blips, a brief 5xx while a pod restarts) are tolerated —
|
|
51
|
+
* they don't abort the wait; only the overall timeout does.
|
|
52
|
+
*/
|
|
53
|
+
export declare function waitForMicroservices(opts: WaitOptions): Promise<WaitResult>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll a tenant's microservice deployment status until every auto-deployed
|
|
3
|
+
* microservice settles (ok/error) or a timeout elapses. Backs `ephemeral push
|
|
4
|
+
* --wait`: after a multidoc push auto-deploys microservices (tenant_deploy=
|
|
5
|
+
* "auto"), the CLI waits here for them to become Ready and reports the outcome.
|
|
6
|
+
*
|
|
7
|
+
* The backend route (GET .../tenant/{name}/microservice, statement
|
|
8
|
+
* mvp:microservice_status_all) returns one entry per microservice with a live
|
|
9
|
+
* k8s readiness read. `manual`/`disabled` microservices are reported but not
|
|
10
|
+
* waited on (they were never deployed by the push).
|
|
11
|
+
*/
|
|
12
|
+
/** Statuses that mean an awaited microservice has settled (no longer in flight). */
|
|
13
|
+
const SETTLED = new Set(['ok', 'error']);
|
|
14
|
+
/** Statuses that mean we don't wait on this microservice at all. */
|
|
15
|
+
const NOT_AWAITED = new Set(['skipped', 'disabled']);
|
|
16
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
|
+
/** An awaited microservice is one that was auto-deployed and is not skipped/disabled. */
|
|
18
|
+
export function isAwaited(entry) {
|
|
19
|
+
return !NOT_AWAITED.has(entry.status) && (entry.tenant_deploy ?? 'auto') !== 'manual';
|
|
20
|
+
}
|
|
21
|
+
/** True once every awaited microservice has settled (ok/error). */
|
|
22
|
+
function allSettled(entries) {
|
|
23
|
+
const awaited = entries.filter((e) => isAwaited(e));
|
|
24
|
+
// No auto microservices at all → nothing to wait for.
|
|
25
|
+
if (awaited.length === 0)
|
|
26
|
+
return true;
|
|
27
|
+
return awaited.every((e) => SETTLED.has(e.status));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fetch the status route once. Throws on a non-2xx so the caller can decide
|
|
31
|
+
* whether a transient failure is fatal (it isn't — we keep polling).
|
|
32
|
+
*/
|
|
33
|
+
async function fetchStatus(opts) {
|
|
34
|
+
const res = await opts.verboseFetch(opts.statusUrl, { headers: { Authorization: `Bearer ${opts.accessToken}` }, method: 'GET' }, opts.verbose ?? false, opts.accessToken);
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`status ${res.status} ${res.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
const body = (await res.json());
|
|
39
|
+
if (!Array.isArray(body)) {
|
|
40
|
+
throw new Error('unexpected status response shape');
|
|
41
|
+
}
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Poll until all awaited microservices settle or the timeout elapses. Transient
|
|
46
|
+
* poll failures (network blips, a brief 5xx while a pod restarts) are tolerated —
|
|
47
|
+
* they don't abort the wait; only the overall timeout does.
|
|
48
|
+
*/
|
|
49
|
+
export async function waitForMicroservices(opts) {
|
|
50
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 2000;
|
|
51
|
+
const timeoutMs = opts.timeoutMs ?? 300000;
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
let entries = [];
|
|
54
|
+
let consecutiveErrors = 0;
|
|
55
|
+
// Poll loop. We always do at least one fetch so `--wait` reports even when the
|
|
56
|
+
// deployment already settled between the push returning and the first poll.
|
|
57
|
+
for (;;) {
|
|
58
|
+
const elapsed = Date.now() - start;
|
|
59
|
+
try {
|
|
60
|
+
entries = await fetchStatus(opts);
|
|
61
|
+
consecutiveErrors = 0;
|
|
62
|
+
opts.onPoll?.(entries, elapsed);
|
|
63
|
+
if (allSettled(entries)) {
|
|
64
|
+
return { entries, hadError: entries.some((e) => isAwaited(e) && e.status === 'error'), timedOut: false };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// A transient status read failure is non-fatal — keep polling until the
|
|
69
|
+
// overall timeout. (Mirrors the frontend poller's tolerate-with-cap.) We
|
|
70
|
+
// don't surface each blip; the timeout is the real failure signal.
|
|
71
|
+
consecutiveErrors++;
|
|
72
|
+
}
|
|
73
|
+
if (Date.now() - start + pollIntervalMs > timeoutMs) {
|
|
74
|
+
// Out of budget. Report whatever we last saw as timed out.
|
|
75
|
+
return {
|
|
76
|
+
entries,
|
|
77
|
+
hadError: entries.some((e) => isAwaited(e) && e.status === 'error'),
|
|
78
|
+
timedOut: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
await sleep(pollIntervalMs);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -97,6 +97,18 @@ export declare function readDocuments(files: string[]): Array<{
|
|
|
97
97
|
content: string;
|
|
98
98
|
filePath: string;
|
|
99
99
|
}>;
|
|
100
|
+
/**
|
|
101
|
+
* Return the entries whose file holds more than one document (a `---`
|
|
102
|
+
* separator). Used to refuse multi-doc files before the push pipeline's
|
|
103
|
+
* single-doc-per-file assumptions can drop or corrupt them.
|
|
104
|
+
*/
|
|
105
|
+
export declare function findMultiDocEntries(entries: Array<{
|
|
106
|
+
content: string;
|
|
107
|
+
filePath: string;
|
|
108
|
+
}>): Array<{
|
|
109
|
+
count: number;
|
|
110
|
+
filePath: string;
|
|
111
|
+
}>;
|
|
100
112
|
export declare function renderBadReferences(badRefs: BadReference[], log: (msg: string) => void): void;
|
|
101
113
|
export declare function renderBadIndexes(badIndexes: BadIndex[], log: (msg: string) => void): void;
|
|
102
114
|
export declare function confirm(message: string): Promise<boolean>;
|
|
@@ -3,6 +3,7 @@ import { minimatch } from 'minimatch';
|
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
4
|
import { join, relative } from 'node:path';
|
|
5
5
|
import { buildDocumentKey, findFilesWithGuid, parseDocument } from './document-parser.js';
|
|
6
|
+
import { flattenBundleFile } from './flatten.js';
|
|
6
7
|
import { collectKnowledgeObjects, fetchKnowledge, knowledgePreview, pushKnowledge, syncGuidToFrontmatter, toPushItems, } from './knowledge-sync.js';
|
|
7
8
|
import { checkReferences, checkTableIndexes } from './reference-checker.js';
|
|
8
9
|
/**
|
|
@@ -102,6 +103,31 @@ export function readDocuments(files) {
|
|
|
102
103
|
}
|
|
103
104
|
return entries;
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* A `.xs` document separator: a line that is exactly `---`. The whole push
|
|
108
|
+
* pipeline (partial-diff filtering, the document→file map, and GUID writeback)
|
|
109
|
+
* assumes ONE document per file — it only ever parses the first document in a
|
|
110
|
+
* file and rewrites the first `guid =` line. A multi-document file therefore
|
|
111
|
+
* silently drops on a partial push and corrupts GUIDs on a full one, so it is
|
|
112
|
+
* refused up front (with a pointer to `xano flatten`).
|
|
113
|
+
*/
|
|
114
|
+
const DOC_SEPARATOR = /^---$/m;
|
|
115
|
+
/**
|
|
116
|
+
* Return the entries whose file holds more than one document (a `---`
|
|
117
|
+
* separator). Used to refuse multi-doc files before the push pipeline's
|
|
118
|
+
* single-doc-per-file assumptions can drop or corrupt them.
|
|
119
|
+
*/
|
|
120
|
+
export function findMultiDocEntries(entries) {
|
|
121
|
+
const offenders = [];
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (DOC_SEPARATOR.test(entry.content)) {
|
|
124
|
+
// Count documents = separators + 1 (trailing/empty fragments don't change the "multi" verdict).
|
|
125
|
+
const count = entry.content.split('\n').filter((l) => l.trim() === '---').length + 1;
|
|
126
|
+
offenders.push({ count, filePath: entry.filePath });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return offenders;
|
|
130
|
+
}
|
|
105
131
|
// ── Validation Rendering ────────────────────────────────────────────────────
|
|
106
132
|
export function renderBadReferences(badRefs, log) {
|
|
107
133
|
log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
|
|
@@ -387,6 +413,64 @@ export async function executePush(ctx, target, flags) {
|
|
|
387
413
|
if (documentEntries.length === 0) {
|
|
388
414
|
command.error(`All .xs files in ${inputDir} are empty`);
|
|
389
415
|
}
|
|
416
|
+
// ── Handle multi-document files ───────────────────────────────────────
|
|
417
|
+
// The push pipeline assumes one document per file: the partial-diff filter
|
|
418
|
+
// and GUID-writeback both parse only the first document in a file, so a
|
|
419
|
+
// bundle like a single hand-authored multidoc.xs silently pushes nothing
|
|
420
|
+
// (partial) or clobbers a single guid line across every document (full).
|
|
421
|
+
// Rather than fail, offer to flatten each bundle in place into the
|
|
422
|
+
// per-document layout `pull` produces, then re-read and continue the push.
|
|
423
|
+
const multiDocFiles = findMultiDocEntries(documentEntries);
|
|
424
|
+
if (multiDocFiles.length > 0) {
|
|
425
|
+
const list = multiDocFiles
|
|
426
|
+
.map((f) => ` ${relative(inputDir, f.filePath) || f.filePath} (${f.count} documents)`)
|
|
427
|
+
.join('\n');
|
|
428
|
+
log('');
|
|
429
|
+
log(ux.colorize('yellow', ux.colorize('bold', '=== Multi-document file(s) detected ===')));
|
|
430
|
+
log('');
|
|
431
|
+
log(ux.colorize('yellow', 'These .xs files hold more than one document:'));
|
|
432
|
+
log(list);
|
|
433
|
+
log('');
|
|
434
|
+
log(ux.colorize('dim', 'Push needs one document per file (partial diff + GUID writeback operate per file),\n' +
|
|
435
|
+
'so a multi-doc bundle silently pushes nothing or corrupts GUIDs. I can split each\n' +
|
|
436
|
+
'into the standard per-document layout (the same tree `pull` produces) and continue.'));
|
|
437
|
+
log('');
|
|
438
|
+
// Flattening DELETES the source bundle, so never do it unattended without an
|
|
439
|
+
// explicit opt-in. --force means "just do it"; an interactive TTY gets a
|
|
440
|
+
// prompt; a bare non-interactive run refuses (rather than silently rewriting
|
|
441
|
+
// and deleting the user's files in CI).
|
|
442
|
+
if (flags.force) {
|
|
443
|
+
log(ux.colorize('dim', 'Flattening (--force)…'));
|
|
444
|
+
}
|
|
445
|
+
else if (process.stdin.isTTY) {
|
|
446
|
+
const doFlatten = await confirm('Flatten these file(s) in place and continue the push?');
|
|
447
|
+
if (!doFlatten) {
|
|
448
|
+
log('Push cancelled. Run `xano flatten <file>` yourself, or re-run to be prompted again.');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
command.error('Multi-document .xs file(s) cannot be pushed. Run `xano flatten <file>` to split them ' +
|
|
454
|
+
'into the standard per-document layout first, or re-run with --force to flatten automatically.');
|
|
455
|
+
}
|
|
456
|
+
for (const offender of multiDocFiles) {
|
|
457
|
+
try {
|
|
458
|
+
const result = flattenBundleFile(offender.filePath, { log: (m) => flags.verbose && log(ux.colorize('dim', m)) });
|
|
459
|
+
log(ux.colorize('dim', ` flattened ${relative(inputDir, offender.filePath) || offender.filePath} → ${result.written.length} files`));
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
command.error(`Failed to flatten ${offender.filePath}: ${error.message}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
log('');
|
|
466
|
+
// Re-collect and re-read: the bundle files are gone, replaced by the split
|
|
467
|
+
// per-document tree, so the rest of the push sees a clean one-doc-per-file set.
|
|
468
|
+
const refreshed = readDocuments(applyFilters(collectFiles(inputDir), inputDir, flags.include, flags.exclude, () => { }));
|
|
469
|
+
documentEntries = refreshed;
|
|
470
|
+
if (documentEntries.length === 0) {
|
|
471
|
+
command.error('After flattening, no .xs documents remain to push.');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
390
474
|
multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
391
475
|
// ── Build document key → file path map (for GUID writeback) ─────────
|
|
392
476
|
for (const entry of documentEntries) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much paging information the backing Metadata API endpoint gives us.
|
|
3
|
+
*
|
|
4
|
+
* The Metadata API pages unevenly, and what the CLI can honestly tell the user
|
|
5
|
+
* depends entirely on what the response carries:
|
|
6
|
+
*
|
|
7
|
+
* - `envelope` accepts page + per_page and returns
|
|
8
|
+
* {curPage, nextPage, prevPage, items[]}. `nextPage` is
|
|
9
|
+
* server-computed and null on the last page, so it is the
|
|
10
|
+
* one place a "there is more" hint is guaranteed correct.
|
|
11
|
+
* - `page-only-envelope` accepts page only (per_page is hardcoded server-side)
|
|
12
|
+
* and returns an object carrying `itemsTotal`. A true
|
|
13
|
+
* total, so we can say where the user sits — but we offer
|
|
14
|
+
* no next-page affordance.
|
|
15
|
+
* - `none` no server-side paging at all. The full set comes back,
|
|
16
|
+
* so the item count IS the total.
|
|
17
|
+
*
|
|
18
|
+
* There is deliberately no tier for endpoints that page but return a bare array
|
|
19
|
+
* with no metadata (tenants, releases, platforms, clusters, ephemerals, tenant
|
|
20
|
+
* backups). Those commands are left alone: the only has-more signal available
|
|
21
|
+
* would be "the page came back full", which is a guaranteed false positive when
|
|
22
|
+
* the result set is an exact multiple of the page size — sending the user to an
|
|
23
|
+
* empty page. Better to offer nothing than to offer a lie.
|
|
24
|
+
*/
|
|
25
|
+
export type PagingTier = 'envelope' | 'none' | 'page-only-envelope';
|
|
26
|
+
export interface PagingFlagValues {
|
|
27
|
+
page?: number;
|
|
28
|
+
per_page?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface NormalizedList<T> {
|
|
31
|
+
curPage?: number;
|
|
32
|
+
items: T[];
|
|
33
|
+
itemsTotal?: number;
|
|
34
|
+
nextPage?: number;
|
|
35
|
+
prevPage?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface PagingFlagOptions {
|
|
38
|
+
/** Page size the endpoint hardcodes server-side; surfaced in the page flag description. */
|
|
39
|
+
fixedPerPage?: number;
|
|
40
|
+
/** Maximum per_page the endpoint accepts; surfaced in the per_page flag description. */
|
|
41
|
+
maxPerPage?: number;
|
|
42
|
+
}
|
|
43
|
+
declare function makePageFlag(description: string): import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
44
|
+
declare function makePerPageFlag(defaultValue: number, description: string): import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
45
|
+
type PageFlag = ReturnType<typeof makePageFlag>;
|
|
46
|
+
type PerPageFlag = ReturnType<typeof makePerPageFlag>;
|
|
47
|
+
/**
|
|
48
|
+
* Build the oclif flag definitions appropriate to an endpoint's paging tier.
|
|
49
|
+
* Spread the result into a command's `static flags`.
|
|
50
|
+
*
|
|
51
|
+
* A flag the server ignores is worse than no flag, so `per_page` is only
|
|
52
|
+
* produced for tiers whose endpoint actually accepts it. The overloads keep
|
|
53
|
+
* oclif's flag-type inference intact so `flags.page` stays typed at call sites.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pagingFlags(tier: 'envelope', options?: PagingFlagOptions): {
|
|
56
|
+
page: PageFlag;
|
|
57
|
+
per_page: PerPageFlag;
|
|
58
|
+
};
|
|
59
|
+
export declare function pagingFlags(tier: 'page-only-envelope', options?: PagingFlagOptions): {
|
|
60
|
+
page: PageFlag;
|
|
61
|
+
};
|
|
62
|
+
export declare function pagingFlags(tier: 'none', options?: PagingFlagOptions): Record<string, never>;
|
|
63
|
+
export declare function pagingFlags(tier: PagingTier, options?: PagingFlagOptions): Record<string, unknown>;
|
|
64
|
+
/**
|
|
65
|
+
* Translate parsed paging flags into query-string entries, emitting only the
|
|
66
|
+
* params the endpoint actually declares.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildPagingParams(flags: PagingFlagValues | Record<string, unknown>, tier: PagingTier): Array<[string, string]>;
|
|
69
|
+
/**
|
|
70
|
+
* Absorb the response shapes the Metadata API returns for list endpoints:
|
|
71
|
+
* a bare array, a paging envelope keyed on `items`, or an object keyed on the
|
|
72
|
+
* resource name (e.g. `{workspaces: [...]}`).
|
|
73
|
+
*
|
|
74
|
+
* `resourceKeys` names any resource-keyed fallbacks to check, in order.
|
|
75
|
+
*/
|
|
76
|
+
export declare function normalizeListResponse<T>(data: unknown, resourceKeys?: string[]): NormalizedList<T>;
|
|
77
|
+
/** Page size used when the CLI pages an endpoint on the user's behalf. */
|
|
78
|
+
export declare const INTERNAL_PAGE_SIZE = 100;
|
|
79
|
+
/**
|
|
80
|
+
* Walk an envelope-tier endpoint page by page and collect every item.
|
|
81
|
+
*
|
|
82
|
+
* Termination is the server's own `nextPage`, never "the page came back full" —
|
|
83
|
+
* the same rule the footer follows. An empty page also stops the walk, which
|
|
84
|
+
* covers an endpoint that omits `nextPage` entirely rather than looping forever.
|
|
85
|
+
*
|
|
86
|
+
* `maxPages` is a runaway backstop, not a limit users are expected to hit; when
|
|
87
|
+
* it trips, `onTruncate` fires so the caller can say so out loud instead of
|
|
88
|
+
* silently processing a subset.
|
|
89
|
+
*/
|
|
90
|
+
export declare function collectAllPages<T>(fetchPage: (page: number) => Promise<NormalizedList<T>>, options?: {
|
|
91
|
+
maxPages?: number;
|
|
92
|
+
onTruncate?: (collected: number) => void;
|
|
93
|
+
}): Promise<T[]>;
|
|
94
|
+
export interface PagingJson<T> {
|
|
95
|
+
curPage?: number;
|
|
96
|
+
items: T[];
|
|
97
|
+
itemsTotal?: number;
|
|
98
|
+
nextPage?: number;
|
|
99
|
+
perPage?: number;
|
|
100
|
+
prevPage?: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the `--output json` payload for a list command.
|
|
104
|
+
*
|
|
105
|
+
* Field names mirror the Metadata API envelope exactly — `curPage`, `nextPage`,
|
|
106
|
+
* `prevPage`, `itemsTotal`, `items` — so a script can move between the CLI and
|
|
107
|
+
* the raw API without a translation layer. `perPage` is the one field the CLI
|
|
108
|
+
* injects: the API never echoes it back, and it is otherwise invisible when the
|
|
109
|
+
* user leaves the flag at its default.
|
|
110
|
+
*
|
|
111
|
+
* Emits only fields the response actually proves, mirroring the footer's
|
|
112
|
+
* honesty rule: `itemsTotal` appears only when the server sent it, `nextPage`
|
|
113
|
+
* only when the server computed it.
|
|
114
|
+
*
|
|
115
|
+
* Without this envelope, a script driving `--output json` sees a bare array and
|
|
116
|
+
* its only available stop condition is `length < perPage` — precisely the
|
|
117
|
+
* page-fullness inference this module refuses to make in the footer.
|
|
118
|
+
*/
|
|
119
|
+
export declare function buildPagingJson<T>(list: NormalizedList<T>, options: {
|
|
120
|
+
page?: number;
|
|
121
|
+
perPage?: number;
|
|
122
|
+
tier: PagingTier;
|
|
123
|
+
}): PagingJson<T>;
|
|
124
|
+
export interface PagingFooterOptions {
|
|
125
|
+
/** Singular noun for the listed resource, used by the `none` tier. */
|
|
126
|
+
noun: string;
|
|
127
|
+
/** Plural form; defaults to `noun + 's'`. */
|
|
128
|
+
nounPlural?: string;
|
|
129
|
+
/** Page the user requested, used when the response omits `curPage`. */
|
|
130
|
+
page?: number;
|
|
131
|
+
/** Page size the user requested. Recorded for context; never used to infer a next page. */
|
|
132
|
+
perPage?: number;
|
|
133
|
+
tier: PagingTier;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Render the paging footer for a list command, reporting only facts the response
|
|
137
|
+
* actually proves. Returns null when there is nothing worth saying — an empty
|
|
138
|
+
* result set already prints its own "No X found" line.
|
|
139
|
+
*
|
|
140
|
+
* Deliberately asymmetric across tiers: `envelope` shows a next-page hint but no
|
|
141
|
+
* total, `page-only-envelope` shows a total but no next-page hint, and `none`
|
|
142
|
+
* shows a bare count. See PagingTier for why.
|
|
143
|
+
*/
|
|
144
|
+
export declare function formatPagingFooter<T>(list: NormalizedList<T>, options: PagingFooterOptions): null | string;
|
|
145
|
+
export {};
|