Skip to content

Commit ad1fc44

Browse files
authored
fix: registry (#150)
2 parents cbc8725 + bc0d509 commit ad1fc44

File tree

8 files changed

+203
-76
lines changed

8 files changed

+203
-76
lines changed

.changeset/bumpy-moons-hear.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@calycode/core": patch
3+
"@calycode/cli": patch
4+
---
5+
6+
fix: fixing default registry item .xs function to match new syntax

.changeset/many-moments-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@calycode/cli": patch
3+
---
4+
5+
fix: several fixes for the registry related command, updated scaffolded registry directory structure, fixed url processing from env, fixed multiple issues with adding items to the remote Xano instance

.changeset/olive-animals-attend.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@calycode/core": patch
3+
"@calycode/cli": patch
4+
---
5+
6+
chore: added test-config.schema, now it's easier to actually know what a test config should look like

.changeset/stupid-views-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@calycode/cli": patch
3+
---
4+
5+
chore: updated the output of the registry-add command for better readibility and to expose the errors that Xano returns --> thus allowing actually remote linting of .xs files as well

packages/cli/src/commands/registry.ts

Lines changed: 113 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
sortFilesByType,
1111
withErrorHandler,
1212
} from '../utils/index';
13+
import { resolveConfigs } from '../utils/index';
1314

1415
async function addToXano({
1516
componentNames,
@@ -19,17 +20,17 @@ async function addToXano({
1920
componentNames: string[];
2021
context: CoreContext;
2122
core: any;
22-
}) {
23-
// [ ] !!! fix: to use the context resolver !!!!
24-
const startDir = process.cwd();
25-
const { instanceConfig, workspaceConfig, branchConfig } = await core.loadAndValidateContext({
26-
instance: context.instance,
27-
workspace: context.workspace,
28-
branch: context.branch,
29-
startDir,
23+
}): Promise<{
24+
installed: Array<{ component: string; file: string; response: any }>;
25+
failed: Array<{ component: string; file: string; error: string; response?: any }>;
26+
skipped: Array<any>;
27+
}> {
28+
const { instanceConfig, workspaceConfig, branchConfig } = await resolveConfigs({
29+
cliContext: context,
30+
core,
3031
});
3132

32-
intro('Add components to your Xano instance');
33+
intro('Adding components to your Xano instance:');
3334

3435
if (!componentNames?.length) componentNames = (await promptForComponents()) as string[];
3536

@@ -40,39 +41,58 @@ async function addToXano({
4041
const registryItem = await getRegistryItem(componentName);
4142
const sortedFiles = sortFilesByType(registryItem.files);
4243
for (const file of sortedFiles) {
43-
const success = await installComponentToXano(
44+
const installResult = await installComponentToXano(
4445
file,
45-
{
46-
instanceConfig,
47-
workspaceConfig,
48-
branchConfig,
49-
},
46+
{ instanceConfig, workspaceConfig, branchConfig },
5047
core
5148
);
52-
if (success)
53-
results.installed.push({ component: componentName, file: file.target || file.path });
54-
else
49+
if (installResult.success) {
50+
results.installed.push({
51+
component: componentName,
52+
file: file.target || file.path,
53+
response: installResult.body,
54+
});
55+
} else {
5556
results.failed.push({
5657
component: componentName,
5758
file: file.target || file.path,
58-
error: 'Installation failed',
59+
error: installResult.error || 'Installation failed',
60+
response: installResult.body,
5961
});
62+
}
6063
}
61-
log.step(`Installed: ${componentName}`);
6264
} catch (error) {
6365
results.failed.push({ component: componentName, error: error.message });
6466
}
6567
}
68+
69+
// --- Output summary table ---
70+
if (results.installed.length) {
71+
log.success('Installed components:');
72+
results.installed.forEach(({ component, file }) => {
73+
log.info(`${component}\nFile: ${file}\n---`);
74+
});
75+
}
76+
if (results.failed.length) {
77+
log.error('Failed components:');
78+
results.failed.forEach(({ component, file, error }) => {
79+
log.warn(`${component}\nFile: ${file}\nError: ${error}\n---`);
80+
});
81+
}
82+
if (!results.installed.length && !results.failed.length) {
83+
log.info('\nNo components were installed.');
84+
}
85+
6686
return results;
6787
}
6888

69-
// [ ] CORE
7089
/**
71-
* Function that creates the required components in Xano.
90+
* Installs a component file to Xano.
7291
*
73-
* @param {*} file
74-
* @param {*} resolvedContext
75-
* @returns {Boolean} - success: true, failure: false
92+
* @param {Object} file - The component file metadata.
93+
* @param {Object} resolvedContext - The resolved context configs.
94+
* @param {any} core - Core utilities.
95+
* @returns {Promise<{ success: boolean, error?: string, body?: any }>}
7696
*/
7797
async function installComponentToXano(file, resolvedContext, core) {
7898
const { instanceConfig, workspaceConfig, branchConfig } = resolvedContext;
@@ -82,18 +102,12 @@ async function installComponentToXano(file, resolvedContext, core) {
82102
'registry:table': `workspace/${workspaceConfig.id}/table`,
83103
};
84104

85-
// If query, extend the default urlMapping with the populated query creation API group.
86105
if (file.type === 'registry:query') {
87106
const targetApiGroup = await getApiGroupByName(
88107
file['api-group-name'],
89-
{
90-
instanceConfig,
91-
workspaceConfig,
92-
branchConfig,
93-
},
108+
{ instanceConfig, workspaceConfig, branchConfig },
94109
core
95110
);
96-
97111
urlMapping[
98112
'registry:query'
99113
] = `workspace/${workspaceConfig.id}/apigroup/${targetApiGroup.id}/api?branch=${branchConfig.label}`;
@@ -103,11 +117,7 @@ async function installComponentToXano(file, resolvedContext, core) {
103117
const xanoApiUrl = `${instanceConfig.url}/api:meta`;
104118

105119
try {
106-
// [ ] TODO: implement override checking. For now just try the POST and Xano will throw error anyways...
107-
108-
// Fetch the text content of the registry file (xano-script)
109120
const content = await fetchRegistryFileContent(file.path);
110-
111121
const response = await fetch(`${xanoApiUrl}/${urlMapping[file.type]}`, {
112122
method: 'POST',
113123
headers: {
@@ -116,11 +126,51 @@ async function installComponentToXano(file, resolvedContext, core) {
116126
},
117127
body: content,
118128
});
119-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
120-
return true;
129+
130+
let body;
131+
try {
132+
body = await response.json();
133+
} catch (jsonErr) {
134+
// If response is not JSON, treat as failure
135+
return {
136+
success: false,
137+
error: `Invalid JSON response: ${jsonErr.message}`,
138+
};
139+
}
140+
141+
// 1. If HTTP error, always fail
142+
if (!response.ok) {
143+
return {
144+
success: false,
145+
error: `HTTP ${response.status}: ${response.statusText} - ${body?.message || ''}`,
146+
body,
147+
};
148+
}
149+
150+
// 2. If "code" and "message" fields are present, treat as error (API-level error)
151+
if (body && body.code && body.message) {
152+
return {
153+
success: false,
154+
error: `${body.code}: ${body.message}`,
155+
body,
156+
};
157+
}
158+
159+
// 3. If "xanoscript" is present and has a non-ok status, treat as error
160+
if (body && body.xanoscript && body.xanoscript.status !== 'ok') {
161+
return {
162+
success: false,
163+
error: `XanoScript error: ${body.xanoscript.message || 'Unknown error'}`,
164+
body,
165+
};
166+
}
167+
168+
// If all checks pass, treat as success
169+
return { success: true, body };
121170
} catch (error) {
171+
// Only catch truly unexpected errors (network, programming, etc.)
122172
console.error(`Failed to install ${file.target || file.path}:`, error);
123-
return false;
173+
return { success: false, error: error.message };
124174
}
125175
}
126176

@@ -132,28 +182,30 @@ function registerRegistryAddCommand(program, core) {
132182
);
133183

134184
addFullContextOptions(cmd);
135-
cmd.option('--components', 'Comma-separated list of components to add')
136-
.option(
137-
'--registry <url>',
138-
'URL to the component registry. Default: http://localhost:5500/registry/definitions'
139-
)
140-
.action(
141-
withErrorHandler(async (options) => {
142-
if (options.registry) {
143-
process.env.Caly_REGISTRY_URL = options.registry;
144-
}
145-
146-
await addToXano({
147-
componentNames: options.components,
148-
context: {
149-
instance: options.instance,
150-
workspace: options.workspace,
151-
branch: options.branch,
152-
},
153-
core,
154-
});
155-
})
156-
);
185+
cmd.argument(
186+
'<components...>',
187+
'Space delimited list of components to add to your Xano instance.'
188+
);
189+
cmd.option(
190+
'--registry <url>',
191+
'URL to the component registry. Default: http://localhost:5500/registry/definitions'
192+
).action(
193+
withErrorHandler(async (components, options) => {
194+
if (options.registry) {
195+
console.log('command registry option: ', options.registry);
196+
process.env.CALY_REGISTRY_URL = options.registry;
197+
}
198+
await addToXano({
199+
componentNames: components,
200+
context: {
201+
instance: options.instance,
202+
workspace: options.workspace,
203+
branch: options.branch,
204+
},
205+
core,
206+
});
207+
})
208+
);
157209
}
158210

159211
function registerRegistryScaffoldCommand(program, core) {

packages/cli/src/utils/feature-focused/registry/api.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
11
const registryCache = new Map();
2-
const REGISTRY_URL = process.env.Caly_REGISTRY_URL || 'http://localhost:5500/registry-definitions';
32

4-
// [ ] CLI, whole file
53
/**
64
* Fetch one or more registry paths, with caching.
75
*/
86
async function fetchRegistry(paths) {
7+
const REGISTRY_URL = process.env.CALY_REGISTRY_URL || 'http://localhost:5500/registry';
98
const results = [];
109
for (const path of paths) {
1110
if (registryCache.has(path)) {
1211
results.push(await registryCache.get(path));
1312
continue;
1413
}
15-
const promise = fetch(`${REGISTRY_URL}/${path}`).then(async (res) => {
16-
if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
17-
return res.json();
18-
});
14+
const promise = fetch(`${REGISTRY_URL}/${path}`)
15+
.then(async (res) => {
16+
if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
17+
return res.json();
18+
})
19+
.catch((err) => {
20+
registryCache.delete(path);
21+
throw err;
22+
});
1923
registryCache.set(path, promise);
20-
results.push(await promise);
24+
const resolvedPromise = await promise;
25+
results.push(resolvedPromise);
2126
}
2227
return results;
2328
}
@@ -45,6 +50,7 @@ async function getRegistryItem(name) {
4550
* Get a registry item content by path.
4651
*/
4752
async function fetchRegistryFileContent(path) {
53+
const REGISTRY_URL = process.env.CALY_REGISTRY_URL || 'http://localhost:5500/registry';
4854
// Remove leading slash if present
4955
const normalized = path.replace(/^\/+/, '');
5056
const url = `${REGISTRY_URL}/${normalized}`;

packages/cli/src/utils/feature-focused/registry/scaffold.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ async function scaffoldRegistry(
1313
}
1414
) {
1515
const componentsRoot = 'components';
16-
const definitionPath = join(registryRoot, 'definitions');
16+
const definitionPath = join(registryRoot);
1717
const functionName = 'hello-world';
1818
const functionRelPath = `functions/${functionName}`;
1919
const functionFileName = `${functionName}.xs`;
@@ -22,10 +22,12 @@ async function scaffoldRegistry(
2222
const functionFilePath = join(registryRoot, componentsRoot, 'functions', functionFileName);
2323
const functionDefPath = join(definitionPath, 'functions', `${functionName}.json`);
2424
const indexPath = join(definitionPath, 'index.json');
25+
const extensionLessFileName = functionFileName.endsWith('.xs')
26+
? functionFileName.slice(0, -3)
27+
: functionFileName;
2528

2629
// Sample content
27-
const sampleFunctionContent = `
28-
function ${functionFileName} {
30+
const sampleFunctionContent = `function "${extensionLessFileName}" {
2931
input {
3032
int score
3133
}
@@ -34,11 +36,8 @@ async function scaffoldRegistry(
3436
value = $input.score + 1
3537
}
3638
}
37-
response {
38-
value = $x1
39-
}
40-
}
41-
`;
39+
response = $x1
40+
}`;
4241

4342
// Descriptor
4443
const sampleRegistryItem = {

0 commit comments

Comments
 (0)