@xano/cli 1.3.3 → 1.3.5-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.
Files changed (118) hide show
  1. package/README.md +137 -3
  2. package/dist/base-command.d.ts +12 -1
  3. package/dist/base-command.js +36 -22
  4. package/dist/commands/branch/set_live/index.d.ts +14 -0
  5. package/dist/commands/branch/set_live/index.js +38 -0
  6. package/dist/commands/ephemeral/push/index.js +6 -1
  7. package/dist/commands/function/create/index.d.ts +15 -0
  8. package/dist/commands/function/create/index.js +39 -0
  9. package/dist/commands/function/edit/index.d.ts +15 -0
  10. package/dist/commands/function/edit/index.js +39 -0
  11. package/dist/commands/function/run/index.js +5 -11
  12. package/dist/commands/policy/catalogue/index.d.ts +16 -0
  13. package/dist/commands/policy/catalogue/index.js +60 -0
  14. package/dist/commands/policy/coverage/index.d.ts +17 -0
  15. package/dist/commands/policy/coverage/index.js +33 -0
  16. package/dist/commands/policy/create/index.d.ts +18 -0
  17. package/dist/commands/policy/create/index.js +44 -0
  18. package/dist/commands/policy/delete/index.d.ts +19 -0
  19. package/dist/commands/policy/delete/index.js +61 -0
  20. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  21. package/dist/commands/policy/evaluate/index.js +71 -0
  22. package/dist/commands/policy/list/index.d.ts +14 -0
  23. package/dist/commands/policy/list/index.js +21 -0
  24. package/dist/commands/policy/parse/index.d.ts +19 -0
  25. package/dist/commands/policy/parse/index.js +18 -0
  26. package/dist/commands/policy/publish/index.d.ts +20 -0
  27. package/dist/commands/policy/publish/index.js +38 -0
  28. package/dist/commands/policy/runs/index.d.ts +54 -0
  29. package/dist/commands/policy/runs/index.js +273 -0
  30. package/dist/commands/policy/status/index.d.ts +21 -0
  31. package/dist/commands/policy/status/index.js +54 -0
  32. package/dist/commands/release/create/index.js +4 -0
  33. package/dist/commands/release/deploy/index.d.ts +19 -0
  34. package/dist/commands/release/deploy/index.js +68 -6
  35. package/dist/commands/release/import/index.js +5 -1
  36. package/dist/commands/release/list/index.d.ts +7 -0
  37. package/dist/commands/release/list/index.js +46 -9
  38. package/dist/commands/release/push/index.js +19 -1
  39. package/dist/commands/sandbox/delete/index.js +8 -0
  40. package/dist/commands/sandbox/pull/index.d.ts +7 -0
  41. package/dist/commands/sandbox/pull/index.js +49 -0
  42. package/dist/commands/sandbox/push/index.d.ts +4 -0
  43. package/dist/commands/sandbox/push/index.js +59 -2
  44. package/dist/commands/sandbox/reset/index.js +8 -0
  45. package/dist/commands/skills/pull/index.d.ts +30 -0
  46. package/dist/commands/skills/pull/index.js +118 -0
  47. package/dist/commands/tenant/create/index.d.ts +3 -0
  48. package/dist/commands/tenant/create/index.js +21 -0
  49. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  50. package/dist/commands/tenant/deploy_release/index.js +182 -4
  51. package/dist/commands/tenant/edit/index.d.ts +2 -0
  52. package/dist/commands/tenant/edit/index.js +39 -4
  53. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  54. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  55. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  56. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  57. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  58. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  59. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  60. package/dist/commands/tenant_deploy_request/get/index.js +113 -0
  61. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  62. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  63. package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
  64. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  65. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  66. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  67. package/dist/commands/workspace/pull/index.d.ts +17 -0
  68. package/dist/commands/workspace/pull/index.js +124 -7
  69. package/dist/commands/workspace/push/index.d.ts +23 -0
  70. package/dist/commands/workspace/push/index.js +188 -6
  71. package/dist/policy-command.d.ts +91 -0
  72. package/dist/policy-command.js +95 -0
  73. package/dist/utils/document-parser.d.ts +10 -0
  74. package/dist/utils/document-parser.js +21 -0
  75. package/dist/utils/export-compare.d.ts +52 -0
  76. package/dist/utils/export-compare.js +134 -0
  77. package/dist/utils/local-config.d.ts +23 -0
  78. package/dist/utils/local-config.js +62 -2
  79. package/dist/utils/multidoc-push.d.ts +124 -4
  80. package/dist/utils/multidoc-push.js +670 -49
  81. package/dist/utils/object-fetch.d.ts +86 -0
  82. package/dist/utils/object-fetch.js +265 -0
  83. package/dist/utils/policy/catalogue.d.ts +85 -0
  84. package/dist/utils/policy/catalogue.js +196 -0
  85. package/dist/utils/policy/coverage.d.ts +19 -0
  86. package/dist/utils/policy/coverage.js +93 -0
  87. package/dist/utils/policy/errors.d.ts +20 -0
  88. package/dist/utils/policy/errors.js +93 -0
  89. package/dist/utils/policy/feedback.d.ts +90 -0
  90. package/dist/utils/policy/feedback.js +210 -0
  91. package/dist/utils/policy/findings.d.ts +29 -0
  92. package/dist/utils/policy/findings.js +82 -0
  93. package/dist/utils/policy/gate.d.ts +138 -0
  94. package/dist/utils/policy/gate.js +193 -0
  95. package/dist/utils/policy/permission.d.ts +7 -0
  96. package/dist/utils/policy/permission.js +55 -0
  97. package/dist/utils/policy/push-gate.d.ts +5 -0
  98. package/dist/utils/policy/push-gate.js +43 -0
  99. package/dist/utils/policy/release.d.ts +85 -0
  100. package/dist/utils/policy/release.js +117 -0
  101. package/dist/utils/policy/request.d.ts +55 -0
  102. package/dist/utils/policy/request.js +119 -0
  103. package/dist/utils/policy/runs.d.ts +30 -0
  104. package/dist/utils/policy/runs.js +101 -0
  105. package/dist/utils/policy/status.d.ts +55 -0
  106. package/dist/utils/policy/status.js +127 -0
  107. package/dist/utils/policy/template.d.ts +66 -0
  108. package/dist/utils/policy/template.js +152 -0
  109. package/dist/utils/policy/types.d.ts +295 -0
  110. package/dist/utils/policy/types.js +1 -0
  111. package/dist/utils/reference-checker.d.ts +3 -0
  112. package/dist/utils/reference-checker.js +3 -2
  113. package/dist/utils/sandbox-marker.d.ts +9 -0
  114. package/dist/utils/sandbox-marker.js +119 -0
  115. package/dist/utils/sync-state.d.ts +71 -0
  116. package/dist/utils/sync-state.js +353 -0
  117. package/oclif.manifest.json +6472 -3181
  118. package/package.json +9 -1
@@ -0,0 +1,86 @@
1
+ import { type SyncFileEntry, type SyncTargetIdentity, type SyncTargetState } from './sync-state.js';
2
+ export declare const PER_OBJECT_FETCH_LIMIT = 25;
3
+ export declare const PER_OBJECT_CONCURRENCY = 6;
4
+ export type ObjectType = 'addon' | 'agent' | 'function' | 'query' | 'table' | 'task' | 'tool';
5
+ export interface TypeSpec {
6
+ hasBranchField: boolean;
7
+ listPath: (workspaceId: string, extra?: {
8
+ groupId?: number;
9
+ }) => string;
10
+ objectPath: (workspaceId: string, id: number, extra?: {
11
+ groupId?: number;
12
+ }) => string;
13
+ }
14
+ export declare const OBJECT_TYPE_SPECS: Record<ObjectType, TypeSpec>;
15
+ export declare function isObjectType(type: string): type is ObjectType;
16
+ export declare function typesWithoutBranchField(types: string[]): string[];
17
+ /**
18
+ * Insert a guid line before the final `}`, matching `syncGuidToFile`.
19
+ * A blank line is inserted before it when the previous non-empty line is a
20
+ * closing `}` or `]` (the multidoc export's layout, measured in Phase 0b).
21
+ */
22
+ export declare function insertGuidLine(content: string, guid: string): string;
23
+ export declare function restoreGuidFromLocal(remoteContent: string, localContent: string): string;
24
+ export declare function readXanoscript(body: unknown): {
25
+ branch?: string;
26
+ status: string;
27
+ text: string;
28
+ } | {
29
+ error: string;
30
+ };
31
+ export interface ResolvedObjectId {
32
+ groupId?: number;
33
+ id: number;
34
+ }
35
+ export type ObjectGet = (pathname: string, query?: Record<string, string>) => Promise<{
36
+ json: unknown;
37
+ ok: boolean;
38
+ status: number;
39
+ }>;
40
+ export declare function lookupObjectId(get: ObjectGet, workspaceId: string, parsed: {
41
+ apiGroup?: string;
42
+ name: string;
43
+ type: string;
44
+ verb?: string;
45
+ }): Promise<null | ResolvedObjectId>;
46
+ export interface PushedObject {
47
+ content: string;
48
+ filePath: string;
49
+ relPath: string;
50
+ }
51
+ export interface FetchFormattedResult {
52
+ content: string;
53
+ filePath: string;
54
+ id: number;
55
+ relPath: string;
56
+ }
57
+ export type FetchFormattedFailure = {
58
+ error: string;
59
+ filePath: string;
60
+ };
61
+ export declare function canUsePerObjectFetch(args: {
62
+ isLiveBranch: boolean;
63
+ limit?: number;
64
+ pushed: PushedObject[];
65
+ }): {
66
+ ok: false;
67
+ reason: string;
68
+ } | {
69
+ ok: true;
70
+ };
71
+ export declare function fetchFormattedObjects(args: {
72
+ get: ObjectGet;
73
+ isLiveBranch: boolean;
74
+ limit?: number;
75
+ pushed: PushedObject[];
76
+ state: null | SyncTargetState;
77
+ target: SyncTargetIdentity;
78
+ workspaceId: string;
79
+ }): Promise<{
80
+ ids: Record<string, SyncFileEntry>;
81
+ ok: true;
82
+ results: FetchFormattedResult[];
83
+ } | {
84
+ ok: false;
85
+ reason: string;
86
+ }>;
@@ -0,0 +1,265 @@
1
+ import { mapWithConcurrency } from './concurrency.js';
2
+ import { buildDocumentKey, parseDocument } from './document-parser.js';
3
+ import { normalizeListResponse } from './paging.js';
4
+ export const PER_OBJECT_FETCH_LIMIT = 25;
5
+ export const PER_OBJECT_CONCURRENCY = 6;
6
+ export const OBJECT_TYPE_SPECS = {
7
+ addon: {
8
+ hasBranchField: true,
9
+ listPath: (workspaceId) => `/workspace/${workspaceId}/addon`,
10
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/addon/${id}`,
11
+ },
12
+ agent: {
13
+ hasBranchField: false,
14
+ listPath: (workspaceId) => `/workspace/${workspaceId}/agent`,
15
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/agent/${id}`,
16
+ },
17
+ function: {
18
+ hasBranchField: true,
19
+ listPath: (workspaceId) => `/workspace/${workspaceId}/function`,
20
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/function/${id}`,
21
+ },
22
+ query: {
23
+ hasBranchField: false,
24
+ listPath: (workspaceId, extra) => extra?.groupId === undefined
25
+ ? `/workspace/${workspaceId}/apigroup`
26
+ : `/workspace/${workspaceId}/apigroup/${extra.groupId}/api`,
27
+ objectPath: (workspaceId, id, extra) => `/workspace/${workspaceId}/apigroup/${extra?.groupId}/api/${id}`,
28
+ },
29
+ table: {
30
+ hasBranchField: false,
31
+ listPath: (workspaceId) => `/workspace/${workspaceId}/table`,
32
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/table/${id}`,
33
+ },
34
+ task: {
35
+ hasBranchField: true,
36
+ listPath: (workspaceId) => `/workspace/${workspaceId}/task`,
37
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/task/${id}`,
38
+ },
39
+ tool: {
40
+ hasBranchField: true,
41
+ listPath: (workspaceId) => `/workspace/${workspaceId}/tool`,
42
+ objectPath: (workspaceId, id) => `/workspace/${workspaceId}/tool/${id}`,
43
+ },
44
+ };
45
+ const GUID_REGEX = /guid\s*=\s*(["'])([^"']*)\1/;
46
+ export function isObjectType(type) {
47
+ return type in OBJECT_TYPE_SPECS;
48
+ }
49
+ export function typesWithoutBranchField(types) {
50
+ return types.filter((type) => isObjectType(type) && !OBJECT_TYPE_SPECS[type].hasBranchField);
51
+ }
52
+ /**
53
+ * Insert a guid line before the final `}`, matching `syncGuidToFile`.
54
+ * A blank line is inserted before it when the previous non-empty line is a
55
+ * closing `}` or `]` (the multidoc export's layout, measured in Phase 0b).
56
+ */
57
+ export function insertGuidLine(content, guid) {
58
+ if (GUID_REGEX.test(content)) {
59
+ return content.replace(GUID_REGEX, `guid = "${guid}"`);
60
+ }
61
+ const lines = content.split('\n');
62
+ let insertIndex = -1;
63
+ for (let i = lines.length - 1; i >= 0; i--) {
64
+ if (lines[i].trim() === '}') {
65
+ insertIndex = i;
66
+ break;
67
+ }
68
+ }
69
+ if (insertIndex === -1)
70
+ return content;
71
+ let indent = ' ';
72
+ let previousNonEmpty = '';
73
+ for (let i = insertIndex - 1; i >= 0; i--) {
74
+ if (lines[i].trim()) {
75
+ previousNonEmpty = lines[i].trim();
76
+ const indentMatch = lines[i].match(/^(\s+)/);
77
+ if (indentMatch)
78
+ indent = indentMatch[1];
79
+ break;
80
+ }
81
+ }
82
+ const guidLine = `${indent}guid = "${guid}"`;
83
+ if (previousNonEmpty === '}' || previousNonEmpty === ']') {
84
+ lines.splice(insertIndex, 0, '', guidLine);
85
+ }
86
+ else {
87
+ lines.splice(insertIndex, 0, guidLine);
88
+ }
89
+ return lines.join('\n');
90
+ }
91
+ export function restoreGuidFromLocal(remoteContent, localContent) {
92
+ const localGuid = localContent.match(GUID_REGEX);
93
+ if (!localGuid)
94
+ return remoteContent;
95
+ if (GUID_REGEX.test(remoteContent))
96
+ return remoteContent;
97
+ return insertGuidLine(remoteContent, localGuid[2]);
98
+ }
99
+ export function readXanoscript(body) {
100
+ if (!body || typeof body !== 'object')
101
+ return { error: 'response is not an object' };
102
+ const record = body;
103
+ const xs = record.xanoscript;
104
+ if (!xs || typeof xs !== 'object')
105
+ return { error: 'xanoscript is missing' };
106
+ const script = xs;
107
+ if (script.status !== 'ok' || typeof script.value !== 'string') {
108
+ return { error: `xanoscript.status is ${String(script.status)}` };
109
+ }
110
+ return {
111
+ branch: typeof record.branch === 'string' ? record.branch : undefined,
112
+ status: 'ok',
113
+ text: script.value,
114
+ };
115
+ }
116
+ function itemsOf(json) {
117
+ try {
118
+ return normalizeListResponse(json, [
119
+ 'functions',
120
+ 'tables',
121
+ 'tasks',
122
+ 'agents',
123
+ 'tools',
124
+ 'addons',
125
+ 'apigroups',
126
+ 'apis',
127
+ ]).items.filter((item) => typeof item === 'object' && item !== null);
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ }
133
+ async function searchExact(get, pathname, name, extraMatch) {
134
+ const response = await get(pathname, { include_draft: 'false', page: '1', per_page: '50', search: name });
135
+ if (!response.ok)
136
+ return null;
137
+ const matches = itemsOf(response.json).filter((item) => item.name === name && (extraMatch ? extraMatch(item) : true));
138
+ if (matches.length !== 1 || typeof matches[0].id !== 'number')
139
+ return null;
140
+ return matches[0];
141
+ }
142
+ export async function lookupObjectId(get, workspaceId, parsed) {
143
+ if (!isObjectType(parsed.type))
144
+ return null;
145
+ const spec = OBJECT_TYPE_SPECS[parsed.type];
146
+ if (parsed.type === 'query') {
147
+ if (!parsed.apiGroup)
148
+ return null;
149
+ const group = await searchExact(get, spec.listPath(workspaceId), parsed.apiGroup);
150
+ if (!group || typeof group.id !== 'number')
151
+ return null;
152
+ const verb = parsed.verb ? String(parsed.verb).toUpperCase() : undefined;
153
+ const api = await searchExact(get, spec.listPath(workspaceId, { groupId: group.id }), parsed.name, (item) => verb ? String(item.verb ?? '').toUpperCase() === verb : true);
154
+ if (!api || typeof api.id !== 'number')
155
+ return null;
156
+ return { groupId: group.id, id: api.id };
157
+ }
158
+ const found = await searchExact(get, spec.listPath(workspaceId), parsed.name);
159
+ if (!found || typeof found.id !== 'number')
160
+ return null;
161
+ return { id: found.id };
162
+ }
163
+ export function canUsePerObjectFetch(args) {
164
+ const limit = args.limit ?? PER_OBJECT_FETCH_LIMIT;
165
+ if (args.pushed.length === 0)
166
+ return { ok: false, reason: 'no pushed documents' };
167
+ if (args.pushed.length > limit) {
168
+ return { ok: false, reason: `push has ${args.pushed.length} documents (limit ${limit})` };
169
+ }
170
+ const types = new Set();
171
+ for (const entry of args.pushed) {
172
+ const parsed = parseDocument(entry.content);
173
+ if (!parsed || !isObjectType(parsed.type)) {
174
+ return { ok: false, reason: `type ${parsed?.type ?? 'unknown'} is not in the per-object table` };
175
+ }
176
+ types.add(parsed.type);
177
+ }
178
+ if (!args.isLiveBranch) {
179
+ const missing = typesWithoutBranchField([...types]);
180
+ if (missing.length > 0) {
181
+ return { ok: false, reason: `types without a branch field on a non-live branch: ${missing.join(', ')}` };
182
+ }
183
+ }
184
+ return { ok: true };
185
+ }
186
+ export async function fetchFormattedObjects(args) {
187
+ const gate = canUsePerObjectFetch({ isLiveBranch: args.isLiveBranch, limit: args.limit, pushed: args.pushed });
188
+ if (!gate.ok)
189
+ return gate;
190
+ const ids = {};
191
+ const resolved = [];
192
+ for (const entry of args.pushed) {
193
+ const parsed = parseDocument(entry.content);
194
+ if (!parsed || !isObjectType(parsed.type)) {
195
+ return { ok: false, reason: `unparseable document ${entry.relPath}` };
196
+ }
197
+ const key = buildDocumentKey(parsed.type, parsed.name, parsed.verb, parsed.apiGroup);
198
+ const recorded = args.state?.files[entry.relPath];
199
+ let id;
200
+ let groupId;
201
+ const cachedId = recorded?.id !== undefined && recorded.key === key ? recorded.id : undefined;
202
+ if (parsed.type === 'query' || cachedId === undefined) {
203
+ // Sequential: stop on the first miss instead of firing every lookup.
204
+ // eslint-disable-next-line no-await-in-loop
205
+ const lookedUp = await lookupObjectId(args.get, args.workspaceId, parsed);
206
+ if (!lookedUp)
207
+ return { ok: false, reason: `could not look up id for ${entry.relPath}` };
208
+ id = cachedId ?? lookedUp.id;
209
+ groupId = lookedUp.groupId;
210
+ }
211
+ else {
212
+ id = cachedId;
213
+ }
214
+ resolved.push({ ...entry, groupId, id, key });
215
+ }
216
+ const fetched = await mapWithConcurrency(resolved, PER_OBJECT_CONCURRENCY, async (entry) => {
217
+ const parsed = parseDocument(entry.content);
218
+ const spec = OBJECT_TYPE_SPECS[parsed.type];
219
+ const pathname = spec.objectPath(args.workspaceId, entry.id, { groupId: entry.groupId });
220
+ const response = await args.get(pathname, { include_draft: 'false', include_xanoscript: 'true' });
221
+ if (!response.ok) {
222
+ return { error: `HTTP ${response.status} for ${entry.relPath}`, filePath: entry.filePath };
223
+ }
224
+ const xs = readXanoscript(response.json);
225
+ if ('error' in xs) {
226
+ return { error: `${entry.relPath}: ${xs.error}`, filePath: entry.filePath };
227
+ }
228
+ if (spec.hasBranchField && xs.branch !== args.target.branch) {
229
+ return {
230
+ error: `${entry.relPath}: branch ${xs.branch ?? '(missing)'} != ${args.target.branch}`,
231
+ filePath: entry.filePath,
232
+ };
233
+ }
234
+ const restored = restoreGuidFromLocal(xs.text, entry.content);
235
+ const restoredParsed = parseDocument(restored);
236
+ if (!restoredParsed) {
237
+ return { error: `${entry.relPath}: fetched document did not parse`, filePath: entry.filePath };
238
+ }
239
+ const restoredKey = buildDocumentKey(restoredParsed.type, restoredParsed.name, restoredParsed.verb, restoredParsed.apiGroup);
240
+ if (restoredKey !== entry.key) {
241
+ return { error: `${entry.relPath}: fetched key ${restoredKey} != ${entry.key}`, filePath: entry.filePath };
242
+ }
243
+ return {
244
+ content: restored,
245
+ filePath: entry.filePath,
246
+ id: entry.id,
247
+ relPath: entry.relPath,
248
+ };
249
+ });
250
+ const results = [];
251
+ for (const item of fetched) {
252
+ if ('error' in item) {
253
+ return { ok: false, reason: item.error ?? 'unknown fetch error' };
254
+ }
255
+ results.push(item);
256
+ }
257
+ for (const entry of resolved) {
258
+ ids[entry.relPath] = {
259
+ id: entry.id,
260
+ key: entry.key,
261
+ sha256: '',
262
+ };
263
+ }
264
+ return { ids, ok: true, results };
265
+ }
@@ -0,0 +1,85 @@
1
+ import type { PolicyTemplate } from './template.js';
2
+ import type { PolicyCatalogueEntry } from './types.js';
3
+ /**
4
+ * `--check` narrows the catalogue to one check. An id that is not in it names the closest matches
5
+ * instead of reprinting the whole list: the usual mistake is a typo one character from a real id.
6
+ */
7
+ export declare function selectCatalogueCheck(checks: PolicyCatalogueEntry[], id: string): PolicyCatalogueEntry[];
8
+ /** `--recipe` narrows the catalogue's recipes to one, refusing an unknown id as `--check` does. */
9
+ export declare function selectCatalogueRecipe(recipes: PolicyRecipe[], id: string): PolicyRecipe[];
10
+ /**
11
+ * The checks as a table of id, label and description, object kinds and required params, each row
12
+ * followed by the check's `uses`: a goal, the params that achieve it (one line of JSON to copy) and
13
+ * its note.
14
+ */
15
+ export declare function policyCatalogueSummary(checks: PolicyCatalogueEntry[]): string[];
16
+ /** The catalogue's `template_categories`: the groups templates are filed under, in the order to offer them. */
17
+ export interface PolicyTemplateCategory {
18
+ id: string;
19
+ label: string;
20
+ }
21
+ /** The catalogue's `template_frameworks`: the framework tags a template can carry, in order, with their labels. */
22
+ export interface PolicyTemplateFramework {
23
+ id: string;
24
+ label: string;
25
+ }
26
+ /**
27
+ * The catalogue's templates, grouped under their `category` in the order `template_categories`
28
+ * lists them, each group named by its label. A category the list does not name follows in the order
29
+ * first seen, named by its id; a template without one (an older instance) is listed under "Other".
30
+ * Each row names the id `policy create --template` takes, the title, what it would create and its
31
+ * framework tags by label (an unknown id as itself). When any row has a tag, the catalogue's
32
+ * `template_frameworks_note` follows the heading, worded by the platform rather than here; an
33
+ * instance that serves no note gets no tags, since a tag must never show without it.
34
+ */
35
+ export declare function policyTemplateSummary(templates: PolicyTemplate[], categories?: PolicyTemplateCategory[], frameworks?: PolicyTemplateFramework[], frameworksNote?: string): string[];
36
+ /**
37
+ * One of the catalogue's `recipes`: a goal spanning several templates, served as data. There is no
38
+ * recipe object and no create action: an author creates its `templates` in order, one policy each,
39
+ * then its `rules` together in a policy of their own.
40
+ */
41
+ export interface PolicyRecipe {
42
+ /** Framework tag ids, as a template's: topics it relates to, never a compliance claim. */
43
+ frameworks?: string[];
44
+ id: string;
45
+ keywords?: string[];
46
+ /** `not_checkable` ids: what no rule in the recipe can see. */
47
+ limits?: string[];
48
+ /** What the author does first: the tags, fields and functions the rules depend on. */
49
+ prerequisites?: string[];
50
+ /** Rules no template covers, shaped like a template's; `needs` are the params the author fills. */
51
+ rules?: Array<{
52
+ check: string;
53
+ needs?: string[];
54
+ params?: Record<string, unknown> | unknown[];
55
+ title: string;
56
+ }>;
57
+ summary?: string;
58
+ /** Template ids, in the order to create them. */
59
+ templates?: string[];
60
+ /** The goal, never a framework name. */
61
+ title: string;
62
+ }
63
+ /** One of the catalogue's `not_checkable` entries: a topic no check can see, and why. */
64
+ export interface PolicyNotCheckable {
65
+ id: string;
66
+ keywords?: string[];
67
+ topic: string;
68
+ why: string;
69
+ }
70
+ /** What the recipe section reads beside the recipes: templates by id, tags, and the not-checkable topics. */
71
+ export interface PolicyRecipeContext {
72
+ frameworks?: PolicyTemplateFramework[];
73
+ frameworksNote?: string;
74
+ notCheckable?: PolicyNotCheckable[];
75
+ templates?: PolicyTemplate[];
76
+ }
77
+ /**
78
+ * The catalogue's recipes, each with its title, tags, summary, what to do first, its templates in
79
+ * order (each with its key and title, so `policy create --template` can take it from here), the rules
80
+ * no template covers (their params as JSON and what the author fills in) and the topics no rule in it
81
+ * can see. The framework note follows the heading when any recipe shows a tag, as for templates.
82
+ */
83
+ export declare function policyRecipeSummary(recipes: PolicyRecipe[], context?: PolicyRecipeContext): string[];
84
+ /** The catalogue's `not_checkable`: each topic no check can see, by id, with why. */
85
+ export declare function policyNotCheckableSummary(entries: PolicyNotCheckable[]): string[];
@@ -0,0 +1,196 @@
1
+ /**
2
+ * `--check` narrows the catalogue to one check. An id that is not in it names the closest matches
3
+ * instead of reprinting the whole list: the usual mistake is a typo one character from a real id.
4
+ */
5
+ export function selectCatalogueCheck(checks, id) {
6
+ return selectById(checks, id, 'a policy check');
7
+ }
8
+ /** `--recipe` narrows the catalogue's recipes to one, refusing an unknown id as `--check` does. */
9
+ export function selectCatalogueRecipe(recipes, id) {
10
+ return selectById(recipes, id, 'a recipe');
11
+ }
12
+ function selectById(entries, id, noun) {
13
+ const wanted = id.trim().toLowerCase();
14
+ const exact = entries.filter(entry => entry.id.toLowerCase() === wanted);
15
+ if (exact.length > 0)
16
+ return exact;
17
+ const near = entries.map(entry => entry.id).filter(known => {
18
+ const lower = known.toLowerCase();
19
+ return lower.includes(wanted) || wanted.includes(lower) || editDistanceWithin(lower, wanted, 2);
20
+ }).sort();
21
+ throw new Error(`"${id}" is not ${noun}.${near.length > 0
22
+ ? ` Did you mean ${near.join(', ')}?`
23
+ : ' Run `xano policy catalogue` for the full list.'}`);
24
+ }
25
+ /** Cheap bounded Levenshtein: true when `a` and `b` are at most `max` edits apart. */
26
+ function editDistanceWithin(a, b, max) {
27
+ if (Math.abs(a.length - b.length) > max)
28
+ return false;
29
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
30
+ for (let i = 1; i <= a.length; i++) {
31
+ const current = [i];
32
+ for (let j = 1; j <= b.length; j++) {
33
+ current[j] = a[i - 1] === b[j - 1]
34
+ ? previous[j - 1]
35
+ : 1 + Math.min(previous[j - 1], previous[j], current[j - 1]);
36
+ }
37
+ if (Math.min(...current) > max)
38
+ return false;
39
+ previous = current;
40
+ }
41
+ return previous[b.length] <= max;
42
+ }
43
+ /** How wide a full-width line (a use, a recipe) is wrapped; the check table is its own columns. */
44
+ const LINE_WIDTH = 120;
45
+ /**
46
+ * The checks as a table of id, label and description, object kinds and required params, each row
47
+ * followed by the check's `uses`: a goal, the params that achieve it (one line of JSON to copy) and
48
+ * its note.
49
+ */
50
+ export function policyCatalogueSummary(checks) {
51
+ if (checks.length === 0)
52
+ return ['No policy checks found.'];
53
+ const rows = [
54
+ ['Check ID', 'Label / Description', 'Object kinds', 'Required params'],
55
+ ...checks.map(check => [
56
+ check.id,
57
+ [check.label.trim(), check.description, check.fix_hint && `Fix hint: ${check.fix_hint}`].filter(Boolean).join('\n'),
58
+ (check.object_kinds ?? []).join(', ') || '—',
59
+ requiredParams(check),
60
+ ]),
61
+ ];
62
+ const widths = rows[0].map((_, column) => Math.min([36, 46, 24, 32][column], Math.max(...rows.map(row => row[column].length))));
63
+ return rows.flatMap((row, index) => {
64
+ const cells = row.map((cell, column) => cell.split('\n').flatMap(paragraph => wrap(paragraph, widths[column])));
65
+ const lines = Array.from({ length: Math.max(...cells.map(cell => cell.length)) }, (_, line) => cells.map((cell, column) => (cell[line] ?? '').padEnd(widths[column])).join(' ').trimEnd());
66
+ return index === 0 ? lines : [...lines, ...useLines(checks[index - 1].uses)];
67
+ });
68
+ }
69
+ /** A check's uses under its row: `Use: goal`, the params as JSON, then `Note: …` when it has one. */
70
+ function useLines(uses) {
71
+ if (!Array.isArray(uses))
72
+ return [];
73
+ return uses.flatMap(use => [
74
+ ...hanging(' Use: ', use.goal),
75
+ ` ${JSON.stringify(Array.isArray(use.params) ? {} : use.params ?? {})}`,
76
+ ...(use.note ? hanging(' Note: ', use.note, ' ') : []),
77
+ ]);
78
+ }
79
+ /** Words wrapped at `width`; a word longer than the width keeps a line of its own. */
80
+ function wrap(text, width) {
81
+ const lines = [''];
82
+ for (const word of text.split(/\s+/)) {
83
+ const last = lines.length - 1;
84
+ if (lines[last] && lines[last].length + word.length + 1 > width)
85
+ lines.push(word);
86
+ else
87
+ lines[last] += `${lines[last] ? ' ' : ''}${word}`;
88
+ }
89
+ return lines;
90
+ }
91
+ /** `text` after `first`, wrapped at the line width, its later lines indented as `rest` (default: as wide as `first`). */
92
+ function hanging(first, text, rest = ' '.repeat(first.length)) {
93
+ return wrap(text, LINE_WIDTH - first.length).map((line, index) => `${index === 0 ? first : rest}${line}`);
94
+ }
95
+ /** The params a rule must set: each required one, and any "one of" group. */
96
+ function requiredParams(check) {
97
+ const required = Object.entries(check.params ?? {}).filter(([, schema]) => schema.required)
98
+ .map(([name, schema]) => `${name}: ${schema.type ?? 'any'}`);
99
+ const oneOf = check.requires_one_of ?? [];
100
+ if (oneOf.length > 0)
101
+ required.push(`one of: ${oneOf.join(' | ')}`);
102
+ return required.join(', ') || 'none';
103
+ }
104
+ /**
105
+ * The catalogue's templates, grouped under their `category` in the order `template_categories`
106
+ * lists them, each group named by its label. A category the list does not name follows in the order
107
+ * first seen, named by its id; a template without one (an older instance) is listed under "Other".
108
+ * Each row names the id `policy create --template` takes, the title, what it would create and its
109
+ * framework tags by label (an unknown id as itself). When any row has a tag, the catalogue's
110
+ * `template_frameworks_note` follows the heading, worded by the platform rather than here; an
111
+ * instance that serves no note gets no tags, since a tag must never show without it.
112
+ */
113
+ export function policyTemplateSummary(templates, categories = [], frameworks = [], frameworksNote = '') {
114
+ if (templates.length === 0)
115
+ return [];
116
+ const labels = new Map(categories.map(category => [category.id, category.label]));
117
+ const tags = frameworkTags(frameworks, frameworksNote);
118
+ const note = templates.some(template => tags(template).length > 0) ? [frameworksNote.trim()] : [];
119
+ const groups = new Map(categories.map(category => [category.id, []]));
120
+ for (const template of templates) {
121
+ const category = template.category?.trim() || '';
122
+ groups.set(category, [...(groups.get(category) ?? []), template]);
123
+ }
124
+ const width = Math.min(36, Math.max(...templates.map(template => template.id.length)));
125
+ return [
126
+ 'Templates (xano policy create --template <id>):',
127
+ ...note,
128
+ ...[...groups].filter(([, members]) => members.length > 0).flatMap(([category, members]) => [
129
+ '',
130
+ labels.get(category) ?? (category || 'Other'),
131
+ ...members.map(template => {
132
+ const rules = template.rules.length === 1 ? '1 rule' : `${template.rules.length} rules`;
133
+ const tagged = tags(template).length > 0 ? ` [${tags(template).join(', ')}]` : '';
134
+ return ` ${template.id.padEnd(width)} ${template.title} (${template.key}, ${template.severity}, ${rules})${tagged}`;
135
+ }),
136
+ ]),
137
+ ];
138
+ }
139
+ /**
140
+ * Framework tag labels for a template or recipe (an unknown id as itself). Tags are shown only
141
+ * beside the platform's sentence on what they mean: an instance that serves no note gets none.
142
+ */
143
+ function frameworkTags(frameworks, frameworksNote) {
144
+ const labels = new Map(frameworks.map(framework => [framework.id, framework.label]));
145
+ return entry => frameworksNote.trim() === '' || !Array.isArray(entry.frameworks) ? [] : entry.frameworks.map(id => labels.get(id) ?? id);
146
+ }
147
+ /**
148
+ * The catalogue's recipes, each with its title, tags, summary, what to do first, its templates in
149
+ * order (each with its key and title, so `policy create --template` can take it from here), the rules
150
+ * no template covers (their params as JSON and what the author fills in) and the topics no rule in it
151
+ * can see. The framework note follows the heading when any recipe shows a tag, as for templates.
152
+ */
153
+ export function policyRecipeSummary(recipes, context = {}) {
154
+ if (recipes.length === 0)
155
+ return [];
156
+ const { frameworks = [], frameworksNote = '', notCheckable = [], templates = [] } = context;
157
+ const tags = frameworkTags(frameworks, frameworksNote);
158
+ const note = recipes.some(recipe => tags(recipe).length > 0) ? [frameworksNote.trim()] : [];
159
+ const byId = new Map(templates.map(template => [template.id, template]));
160
+ const topics = new Map(notCheckable.map(entry => [entry.id, entry.topic]));
161
+ return [
162
+ 'Recipes (goals that span several templates; nothing creates a recipe: create its templates, then its rules):',
163
+ ...note,
164
+ ...recipes.flatMap(recipe => {
165
+ const tagged = tags(recipe).length > 0 ? ` [${tags(recipe).join(', ')}]` : '';
166
+ const steps = recipe.templates ?? [];
167
+ const rules = recipe.rules ?? [];
168
+ const limits = recipe.limits ?? [];
169
+ return [
170
+ '',
171
+ ` ${recipe.id} ${recipe.title}${tagged}`,
172
+ ...(recipe.summary ? hanging(' ', recipe.summary) : []),
173
+ ...((recipe.prerequisites ?? []).length > 0 ? [' First:', ...(recipe.prerequisites ?? []).flatMap(step => hanging(' - ', step))] : []),
174
+ ...(steps.length > 0 ? [' Templates, in order (xano policy create --template <id>, one policy each):', ...steps.map((id, index) => {
175
+ const template = byId.get(id);
176
+ return ` ${index + 1}. ${id}${template ? ` ${template.title} (${template.key})` : ''}`;
177
+ })] : []),
178
+ ...(rules.length > 0 ? [' Rules no template covers, together in one policy of your own:', ...rules.flatMap(rule => [
179
+ ...hanging(' - ', `${rule.title}: ${rule.check}`),
180
+ ` ${JSON.stringify(Array.isArray(rule.params) ? {} : rule.params ?? {})}`,
181
+ ...((rule.needs ?? []).length > 0 ? [` You fill in: ${(rule.needs ?? []).join(', ')}`] : []),
182
+ ])] : []),
183
+ ...(limits.length > 0 ? hanging(' No rule here can see: ', `${limits.map(id => topics.get(id) ?? id).join('; ')}.`, ' ') : []),
184
+ ];
185
+ }),
186
+ ];
187
+ }
188
+ /** The catalogue's `not_checkable`: each topic no check can see, by id, with why. */
189
+ export function policyNotCheckableSummary(entries) {
190
+ if (entries.length === 0)
191
+ return [];
192
+ return [
193
+ 'What no check can see (recipes name these under "No rule here can see"):',
194
+ ...entries.flatMap(entry => ['', ` ${entry.id} ${entry.topic}`, ...hanging(' ', entry.why)]),
195
+ ];
196
+ }
@@ -0,0 +1,19 @@
1
+ import type { ObjectCoverage } from './types.js';
2
+ /** An object as findings name it, `type:id` (`query:42`), or `null` when the text is not one. */
3
+ export declare function parseObjectRef(text: string): null | {
4
+ id: number;
5
+ type: string;
6
+ };
7
+ /** The object coverage chip, worded as Studio's object panel words it. */
8
+ export declare function coverageChip(summary: ObjectCoverage['summary']): string;
9
+ /**
10
+ * The policies that apply with no finding here, counted from the summary as Studio's panel words its
11
+ * line under the findings: "6 passing · 1 couldn't check · 1 not checked yet". Empty when it counts none.
12
+ */
13
+ export declare function coverageCounts(summary: ObjectCoverage['summary']): string;
14
+ /**
15
+ * An object's coverage on stdout: the chip, the run it reads, the findings Blocking then Advisory,
16
+ * each with its fix, the policies that apply (counted, then each with its status and why), and the
17
+ * platform's note.
18
+ */
19
+ export declare function coverageLines(answer: ObjectCoverage): string[];