Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bumpy-moons-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@calycode/core": patch
"@calycode/cli": patch
---

fix: fixing default registry item .xs function to match new syntax
5 changes: 5 additions & 0 deletions .changeset/many-moments-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@calycode/cli": patch
---

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
6 changes: 6 additions & 0 deletions .changeset/olive-animals-attend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@calycode/core": patch
"@calycode/cli": patch
---

chore: added test-config.schema, now it's easier to actually know what a test config should look like
5 changes: 5 additions & 0 deletions .changeset/stupid-views-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@calycode/cli": patch
---

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
174 changes: 113 additions & 61 deletions packages/cli/src/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
sortFilesByType,
withErrorHandler,
} from '../utils/index';
import { resolveConfigs } from '../utils/index';

async function addToXano({
componentNames,
Expand All @@ -19,17 +20,17 @@ async function addToXano({
componentNames: string[];
context: CoreContext;
core: any;
}) {
// [ ] !!! fix: to use the context resolver !!!!
const startDir = process.cwd();
const { instanceConfig, workspaceConfig, branchConfig } = await core.loadAndValidateContext({
instance: context.instance,
workspace: context.workspace,
branch: context.branch,
startDir,
}): Promise<{
installed: Array<{ component: string; file: string; response: any }>;
failed: Array<{ component: string; file: string; error: string; response?: any }>;
skipped: Array<any>;
}> {
const { instanceConfig, workspaceConfig, branchConfig } = await resolveConfigs({
cliContext: context,
core,
});

intro('Add components to your Xano instance');
intro('Adding components to your Xano instance:');

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

Expand All @@ -40,39 +41,58 @@ async function addToXano({
const registryItem = await getRegistryItem(componentName);
const sortedFiles = sortFilesByType(registryItem.files);
for (const file of sortedFiles) {
const success = await installComponentToXano(
const installResult = await installComponentToXano(
file,
{
instanceConfig,
workspaceConfig,
branchConfig,
},
{ instanceConfig, workspaceConfig, branchConfig },
core
);
if (success)
results.installed.push({ component: componentName, file: file.target || file.path });
else
if (installResult.success) {
results.installed.push({
component: componentName,
file: file.target || file.path,
response: installResult.body,
});
} else {
results.failed.push({
component: componentName,
file: file.target || file.path,
error: 'Installation failed',
error: installResult.error || 'Installation failed',
response: installResult.body,
});
}
}
log.step(`Installed: ${componentName}`);
} catch (error) {
results.failed.push({ component: componentName, error: error.message });
}
}

// --- Output summary table ---
if (results.installed.length) {
log.success('Installed components:');
results.installed.forEach(({ component, file }) => {
log.info(`${component}\nFile: ${file}\n---`);
});
}
if (results.failed.length) {
log.error('Failed components:');
results.failed.forEach(({ component, file, error }) => {
log.warn(`${component}\nFile: ${file}\nError: ${error}\n---`);
});
}
if (!results.installed.length && !results.failed.length) {
log.info('\nNo components were installed.');
}

return results;
}

// [ ] CORE
/**
* Function that creates the required components in Xano.
* Installs a component file to Xano.
*
* @param {*} file
* @param {*} resolvedContext
* @returns {Boolean} - success: true, failure: false
* @param {Object} file - The component file metadata.
* @param {Object} resolvedContext - The resolved context configs.
* @param {any} core - Core utilities.
* @returns {Promise<{ success: boolean, error?: string, body?: any }>}
*/
async function installComponentToXano(file, resolvedContext, core) {
const { instanceConfig, workspaceConfig, branchConfig } = resolvedContext;
Expand All @@ -82,18 +102,12 @@ async function installComponentToXano(file, resolvedContext, core) {
'registry:table': `workspace/${workspaceConfig.id}/table`,
};

// If query, extend the default urlMapping with the populated query creation API group.
if (file.type === 'registry:query') {
const targetApiGroup = await getApiGroupByName(
file['api-group-name'],
{
instanceConfig,
workspaceConfig,
branchConfig,
},
{ instanceConfig, workspaceConfig, branchConfig },
core
);

urlMapping[
'registry:query'
] = `workspace/${workspaceConfig.id}/apigroup/${targetApiGroup.id}/api?branch=${branchConfig.label}`;
Expand All @@ -103,11 +117,7 @@ async function installComponentToXano(file, resolvedContext, core) {
const xanoApiUrl = `${instanceConfig.url}/api:meta`;

try {
// [ ] TODO: implement override checking. For now just try the POST and Xano will throw error anyways...

// Fetch the text content of the registry file (xano-script)
const content = await fetchRegistryFileContent(file.path);

const response = await fetch(`${xanoApiUrl}/${urlMapping[file.type]}`, {
method: 'POST',
headers: {
Expand All @@ -116,11 +126,51 @@ async function installComponentToXano(file, resolvedContext, core) {
},
body: content,
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
return true;

let body;
try {
body = await response.json();
} catch (jsonErr) {
// If response is not JSON, treat as failure
return {
success: false,
error: `Invalid JSON response: ${jsonErr.message}`,
};
}

// 1. If HTTP error, always fail
if (!response.ok) {
return {
success: false,
error: `HTTP ${response.status}: ${response.statusText} - ${body?.message || ''}`,
body,
};
}

// 2. If "code" and "message" fields are present, treat as error (API-level error)
if (body && body.code && body.message) {
return {
success: false,
error: `${body.code}: ${body.message}`,
body,
};
}

// 3. If "xanoscript" is present and has a non-ok status, treat as error
if (body && body.xanoscript && body.xanoscript.status !== 'ok') {
return {
success: false,
error: `XanoScript error: ${body.xanoscript.message || 'Unknown error'}`,
body,
};
}

// If all checks pass, treat as success
return { success: true, body };
} catch (error) {
// Only catch truly unexpected errors (network, programming, etc.)
console.error(`Failed to install ${file.target || file.path}:`, error);
return false;
return { success: false, error: error.message };
}
}

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

addFullContextOptions(cmd);
cmd.option('--components', 'Comma-separated list of components to add')
.option(
'--registry <url>',
'URL to the component registry. Default: http://localhost:5500/registry/definitions'
)
.action(
withErrorHandler(async (options) => {
if (options.registry) {
process.env.Caly_REGISTRY_URL = options.registry;
}

await addToXano({
componentNames: options.components,
context: {
instance: options.instance,
workspace: options.workspace,
branch: options.branch,
},
core,
});
})
);
cmd.argument(
'<components...>',
'Space delimited list of components to add to your Xano instance.'
);
cmd.option(
'--registry <url>',
'URL to the component registry. Default: http://localhost:5500/registry/definitions'
).action(
withErrorHandler(async (components, options) => {
if (options.registry) {
console.log('command registry option: ', options.registry);
process.env.CALY_REGISTRY_URL = options.registry;
}
await addToXano({
componentNames: components,
context: {
instance: options.instance,
workspace: options.workspace,
branch: options.branch,
},
core,
});
})
);
}

function registerRegistryScaffoldCommand(program, core) {
Expand Down
20 changes: 13 additions & 7 deletions packages/cli/src/utils/feature-focused/registry/api.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
const registryCache = new Map();
const REGISTRY_URL = process.env.Caly_REGISTRY_URL || 'http://localhost:5500/registry-definitions';

// [ ] CLI, whole file
/**
* Fetch one or more registry paths, with caching.
*/
async function fetchRegistry(paths) {
const REGISTRY_URL = process.env.CALY_REGISTRY_URL || 'http://localhost:5500/registry';
const results = [];
for (const path of paths) {
if (registryCache.has(path)) {
results.push(await registryCache.get(path));
continue;
}
const promise = fetch(`${REGISTRY_URL}/${path}`).then(async (res) => {
if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
return res.json();
});
const promise = fetch(`${REGISTRY_URL}/${path}`)
.then(async (res) => {
if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
return res.json();
})
.catch((err) => {
registryCache.delete(path);
throw err;
});
registryCache.set(path, promise);
results.push(await promise);
const resolvedPromise = await promise;
results.push(resolvedPromise);
}
return results;
}
Expand Down Expand Up @@ -45,6 +50,7 @@ async function getRegistryItem(name) {
* Get a registry item content by path.
*/
async function fetchRegistryFileContent(path) {
const REGISTRY_URL = process.env.CALY_REGISTRY_URL || 'http://localhost:5500/registry';
// Remove leading slash if present
const normalized = path.replace(/^\/+/, '');
const url = `${REGISTRY_URL}/${normalized}`;
Expand Down
15 changes: 7 additions & 8 deletions packages/cli/src/utils/feature-focused/registry/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ async function scaffoldRegistry(
}
) {
const componentsRoot = 'components';
const definitionPath = join(registryRoot, 'definitions');
const definitionPath = join(registryRoot);
const functionName = 'hello-world';
const functionRelPath = `functions/${functionName}`;
const functionFileName = `${functionName}.xs`;
Expand All @@ -22,10 +22,12 @@ async function scaffoldRegistry(
const functionFilePath = join(registryRoot, componentsRoot, 'functions', functionFileName);
const functionDefPath = join(definitionPath, 'functions', `${functionName}.json`);
const indexPath = join(definitionPath, 'index.json');
const extensionLessFileName = functionFileName.endsWith('.xs')
? functionFileName.slice(0, -3)
: functionFileName;

// Sample content
const sampleFunctionContent = `
function ${functionFileName} {
const sampleFunctionContent = `function "${extensionLessFileName}" {
input {
int score
}
Expand All @@ -34,11 +36,8 @@ async function scaffoldRegistry(
value = $input.score + 1
}
}
response {
value = $x1
}
}
`;
response = $x1
}`;

// Descriptor
const sampleRegistryItem = {
Expand Down
Loading