From bed870d2994f447a4736eb914384fd48fbe851bf Mon Sep 17 00:00:00 2001 From: Ayush Date: Mon, 29 Jun 2026 23:40:42 +0530 Subject: [PATCH 01/11] Registered the init, pull, genrate command --- sdks/urbackend-cli/src/index.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/sdks/urbackend-cli/src/index.ts b/sdks/urbackend-cli/src/index.ts index 6370dfc2e..0aafa31a6 100644 --- a/sdks/urbackend-cli/src/index.ts +++ b/sdks/urbackend-cli/src/index.ts @@ -9,6 +9,9 @@ import { collectionListCommand } from "./commands/collection/list.js"; import { collectionDeleteCommand } from "./commands/collection/delete.js"; import { statusCommand } from "./commands/status/index.js"; import { doctorCommand } from "./commands/doctor/index.js"; +import { initCommand } from "./commands/init/index.js"; +import { pullCommand } from "./commands/pull/index.js"; +import { generateCommand } from "./commands/generate/index.js"; const program = new Command(); @@ -93,6 +96,23 @@ program .option("--json", "Output results as JSON (useful for CI/AI agents)") .action((options) => doctorCommand(options)); +// ── Workspace ───────────────────────────────────────────────────────────────── + +program + .command("init") + .description("Initialize a local urBackend project workspace") + .action(initCommand); + +program + .command("pull") + .description("Fetch the latest schemas for the linked project") + .action(pullCommand); + +program + .command("generate") + .description("Generate TypeScript definitions from local schemas") + .action(generateCommand); + // ── Parse ───────────────────────────────────────────────────────────────────── program.parseAsync(process.argv).catch((err: Error) => { From a7b9a6eddaafd10d35bebc3fff5bb84309741e35 Mon Sep 17 00:00:00 2001 From: Ayush Date: Mon, 29 Jun 2026 23:56:56 +0530 Subject: [PATCH 02/11] added core utilities --- sdks/urbackend-cli/src/core/workspace.ts | 78 ++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 sdks/urbackend-cli/src/core/workspace.ts diff --git a/sdks/urbackend-cli/src/core/workspace.ts b/sdks/urbackend-cli/src/core/workspace.ts new file mode 100644 index 000000000..1f774e6f1 --- /dev/null +++ b/sdks/urbackend-cli/src/core/workspace.ts @@ -0,0 +1,78 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { WorkspaceConfig } from "../types/config.js"; +import { logger } from "./logger.js"; + +const WORKSPACE_DIR = ".ub"; +const CONFIG_FILE = "config.json"; +const SCHEMAS_DIR = "schemas"; + +export function getWorkspaceDir(): string { + return path.join(process.cwd(), WORKSPACE_DIR); +} + +export function getWorkspaceConfigPath(): string { + return path.join(getWorkspaceDir(), CONFIG_FILE); +} + +export function getSchemasDir(): string { + return path.join(getWorkspaceDir(), SCHEMAS_DIR); +} + +export function loadWorkspaceConfig(): WorkspaceConfig | null { + const configPath = getWorkspaceConfigPath(); + if (!fs.existsSync(configPath)) { + return null; + } + try { + const raw = fs.readFileSync(configPath, "utf8"); + return JSON.parse(raw) as WorkspaceConfig; + } catch { + return null; + } +} + +export function saveWorkspaceConfig(config: WorkspaceConfig): void { + const dir = getWorkspaceDir(); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const configPath = getWorkspaceConfigPath(); + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8"); +} + +export function saveSchemaFile(collectionName: string, schema: any): void { + const schemasDir = getSchemasDir(); + if (!fs.existsSync(schemasDir)) { + fs.mkdirSync(schemasDir, { recursive: true }); + } + + const filePath = path.join(schemasDir, `${collectionName}.json`); + fs.writeFileSync(filePath, JSON.stringify(schema, null, 2), "utf8"); +} + +export function getLocalSchemas(): { name: string; schema: any }[] { + const schemasDir = getSchemasDir(); + if (!fs.existsSync(schemasDir)) { + return []; + } + + const files = fs.readdirSync(schemasDir).filter((file) => file.endsWith(".json")); + const schemas = []; + + for (const file of files) { + try { + const filePath = path.join(schemasDir, file); + const raw = fs.readFileSync(filePath, "utf8"); + schemas.push({ + name: file.replace(".json", ""), + schema: JSON.parse(raw), + }); + } catch (err) { + logger.warn(`Failed to parse schema file: ${file}. Skipping.`); + } + } + + return schemas; +} From 4ab4de4bf08f09a28744bfb6c3d44922cbb4c48d Mon Sep 17 00:00:00 2001 From: Ayush Date: Mon, 29 Jun 2026 23:58:22 +0530 Subject: [PATCH 03/11] added interface for workspace config --- sdks/urbackend-cli/src/types/config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sdks/urbackend-cli/src/types/config.ts b/sdks/urbackend-cli/src/types/config.ts index 21fb68baf..4de510588 100644 --- a/sdks/urbackend-cli/src/types/config.ts +++ b/sdks/urbackend-cli/src/types/config.ts @@ -3,3 +3,8 @@ export interface CLIConfig { pat?: string; currentProject?: string; } + +export interface WorkspaceConfig { + projectId?: string; + projectName?: string; +} From 042214cd2d32575bfee4a6c164ba889fa0496978 Mon Sep 17 00:00:00 2001 From: Ayush Date: Tue, 30 Jun 2026 00:31:19 +0530 Subject: [PATCH 04/11] created genrate command which converts json file into TypeScript Definition file --- .../src/commands/generate/index.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 sdks/urbackend-cli/src/commands/generate/index.ts diff --git a/sdks/urbackend-cli/src/commands/generate/index.ts b/sdks/urbackend-cli/src/commands/generate/index.ts new file mode 100644 index 000000000..76558e3ca --- /dev/null +++ b/sdks/urbackend-cli/src/commands/generate/index.ts @@ -0,0 +1,89 @@ +import fs from "node:fs"; +import path from "node:path"; +import { getLocalSchemas } from "../../core/workspace.js"; +import { logger } from "../../core/logger.js"; +import type { CollectionField } from "../../types/project.js"; + +function mapType(field: CollectionField, indent = " "): string { + switch (field.type) { + case "String": + return "string"; + case "Number": + return "number"; + case "Boolean": + return "boolean"; + case "Date": + return "string | Date"; + case "Ref": + return "string"; + case "Array": + if (field.fields && field.fields.length > 0) { + // We only support arrays of objects for now, or array of singular types if it's a single field with empty key + if (field.fields.length === 1 && !field.fields[0].key) { + return `Array<${mapType(field.fields[0], indent)}>`; + } + return `Array<${parseFields(field.fields, indent + " ")}>`; + } + return "any[]"; + case "Object": + if (field.fields && field.fields.length > 0) { + return parseFields(field.fields, indent + " "); + } + return "Record"; + default: + return "any"; + } +} + +function parseFields(fields: CollectionField[], indent: string): string { + let out = "{\n"; + for (const f of fields) { + const isOptional = !f.required ? "?" : ""; + const typeStr = mapType(f, indent); + out += `${indent} ${f.key}${isOptional}: ${typeStr};\n`; + } + out += `${indent}}`; + return out; +} + +export async function generateCommand(): Promise { + const schemas = getLocalSchemas(); + + if (schemas.length === 0) { + logger.error("No schemas found in .ub/schemas/. Run 'ub pull' first."); + process.exitCode = 1; + return; + } + + let dtsContent = `// Auto-generated by urBackend CLI\n// Do not edit this file directly\n\n`; + dtsContent += `export interface UrBackendTypes {\n`; + + for (const { name, schema } of schemas) { + if (!schema.model || !Array.isArray(schema.model)) continue; + + dtsContent += ` "${name}": {\n`; + dtsContent += ` _id: string;\n`; + + for (const field of schema.model as CollectionField[]) { + const isOptional = !field.required ? "?" : ""; + const typeStr = mapType(field, " "); + dtsContent += ` ${field.key}${isOptional}: ${typeStr};\n`; + } + + dtsContent += ` createdAt: string | Date;\n`; + dtsContent += ` updatedAt: string | Date;\n`; + dtsContent += ` };\n`; + } + + dtsContent += `}\n`; + + const outputPath = path.join(process.cwd(), "urbackend.d.ts"); + + try { + fs.writeFileSync(outputPath, dtsContent, "utf8"); + logger.success(`Generated TypeScript definitions at ./urbackend.d.ts`); + } catch (error) { + logger.error(`Failed to write urbackend.d.ts: ${(error as Error).message}`); + process.exitCode = 1; + } +} From 95fe895fd17bc84a3f6fb4558cf737d580e26aac Mon Sep 17 00:00:00 2001 From: Ayush Date: Tue, 30 Jun 2026 00:53:50 +0530 Subject: [PATCH 05/11] created pull command which fetch the structure of exact schema --- sdks/urbackend-cli/src/commands/pull/index.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 sdks/urbackend-cli/src/commands/pull/index.ts diff --git a/sdks/urbackend-cli/src/commands/pull/index.ts b/sdks/urbackend-cli/src/commands/pull/index.ts new file mode 100644 index 000000000..39f1339ba --- /dev/null +++ b/sdks/urbackend-cli/src/commands/pull/index.ts @@ -0,0 +1,67 @@ +import { loadWorkspaceConfig, saveSchemaFile } from "../../core/workspace.js"; +import { getProject } from "../../services/project.service.js"; +import { getToken } from "../../core/config.js"; +import { APIError } from "../../core/errors.js"; +import { logger } from "../../core/logger.js"; +import { label } from "../../utils/format.js"; + +export async function pullCommand(): Promise { + const token = getToken(); + if (!token) { + logger.error("You are not logged in. Run 'ub login' first."); + process.exitCode = 1; + return; + } + + const workspaceConfig = loadWorkspaceConfig(); + if (!workspaceConfig || !workspaceConfig.projectId) { + logger.error( + "No project linked to this directory. Run 'ub init' to link a project first." + ); + process.exitCode = 1; + return; + } + + const { projectId, projectName } = workspaceConfig; + logger.info(`Fetching schemas for ${projectName ? projectName + " " : ""}(${projectId})...`); + + try { + const project = await getProject(projectId); + + if (!project.collections || project.collections.length === 0) { + logger.info("This project has no collections defined yet."); + return; + } + + let count = 0; + for (const collection of project.collections) { + saveSchemaFile(collection.name, { + name: collection.name, + model: collection.model, + rls: collection.rls, + }); + count++; + } + + logger.success(`Successfully pulled ${count} collection schema(s) into .ub/schemas/`); + console.log(); + for (const collection of project.collections) { + console.log(` ${label("schema")} ${collection.name}.json`); + } + console.log(`\nNext, run 'ub generate' to create your TypeScript types.`); + } catch (error) { + if (error instanceof APIError) { + if (error.status === 401) { + logger.error("Token is invalid or expired. Run 'ub login' to re-authenticate."); + } else if (error.status === 403) { + logger.error("You do not have permission to access this project."); + } else { + logger.error(error.message); + } + process.exitCode = 1; + return; + } + logger.error("Unable to connect to the urBackend API."); + process.exitCode = 1; + } +} From 744db21920e9f2c1e9e158b70ea6b659f210e052 Mon Sep 17 00:00:00 2001 From: Ayush Date: Tue, 30 Jun 2026 01:59:38 +0530 Subject: [PATCH 06/11] created init command --- sdks/urbackend-cli/src/commands/init/index.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 sdks/urbackend-cli/src/commands/init/index.ts diff --git a/sdks/urbackend-cli/src/commands/init/index.ts b/sdks/urbackend-cli/src/commands/init/index.ts new file mode 100644 index 000000000..60568c5f1 --- /dev/null +++ b/sdks/urbackend-cli/src/commands/init/index.ts @@ -0,0 +1,98 @@ +import fs from "node:fs"; +import path from "node:path"; +import { listProjects } from "../../services/project.service.js"; +import { getToken } from "../../core/config.js"; +import { saveWorkspaceConfig, getWorkspaceDir } from "../../core/workspace.js"; +import { prompt } from "../../utils/prompt.js"; +import { APIError } from "../../core/errors.js"; +import { logger } from "../../core/logger.js"; + +export async function initCommand(projectIdOrName?: string): Promise { + const token = getToken(); + if (!token) { + logger.error("You are not logged in. Run 'ub login' first."); + process.exitCode = 1; + return; + } + + try { + const projects = await listProjects(); + + if (projects.length === 0) { + logger.info("No projects found. Create one at dashboard.urbackend.bitbros.in"); + return; + } + + let selectedId: string | undefined; + let selectedName: string | undefined; + + if (projectIdOrName) { + const match = projects.find( + (p) => + p._id === projectIdOrName || + p.name.toLowerCase() === projectIdOrName.toLowerCase(), + ); + if (!match) { + logger.error(`No project found matching "${projectIdOrName}".`); + logger.info("Run 'ub project list' to see available projects."); + process.exitCode = 1; + return; + } + selectedId = match._id; + selectedName = match.name; + } else { + console.log("\nAvailable projects:\n"); + projects.forEach((p, i) => { + console.log(` [${i + 1}] ${p.name} (${p._id})`); + }); + console.log(); + + const answer = await prompt("Select a project to link this directory to: "); + if (!/^\d+$/.test(answer)) { + logger.error("Invalid selection."); + process.exitCode = 1; + return; + } + const index = Number(answer) - 1; + if (isNaN(index) || index < 0 || index >= projects.length) { + logger.error("Invalid selection."); + process.exitCode = 1; + return; + } + + selectedId = projects[index]._id; + selectedName = projects[index].name; + } + + saveWorkspaceConfig({ projectId: selectedId, projectName: selectedName }); + + // Update .gitignore if it exists + const gitignorePath = path.join(process.cwd(), ".gitignore"); + if (fs.existsSync(gitignorePath)) { + const gitignore = fs.readFileSync(gitignorePath, "utf8"); + if (!gitignore.includes(".ub")) { + fs.appendFileSync(gitignorePath, "\n# urBackend local workspace\n.ub\n", "utf8"); + logger.info("Added .ub to .gitignore"); + } + } else { + fs.writeFileSync(gitignorePath, "# urBackend local workspace\n.ub\n", "utf8"); + logger.info("Created .gitignore and added .ub"); + } + + logger.success(`Successfully initialized urBackend workspace in .ub/`); + logger.info(`Linked to project: ${selectedName} (${selectedId})`); + console.log(`\nNext, run 'ub pull' to download your schemas.`); + } catch (error) { + if (error instanceof APIError) { + if (error.status === 401) { + logger.error("Token is invalid or expired. Run 'ub login' to re-authenticate."); + } else { + logger.error(error.message); + } + process.exitCode = 1; + return; + } + logger.error("Unable to connect to the urBackend API."); + process.exitCode = 1; + } +} From 338de852e6fd22a3254edbc4195a70dd1d78f571 Mon Sep 17 00:00:00 2001 From: Ayush Date: Tue, 30 Jun 2026 02:34:03 +0530 Subject: [PATCH 07/11] swap lint eslint src with tsc --noEmit --- sdks/urbackend-cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/urbackend-cli/package.json b/sdks/urbackend-cli/package.json index c7b653bd4..c3ed8edfa 100644 --- a/sdks/urbackend-cli/package.json +++ b/sdks/urbackend-cli/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "tsup --config tsup.config.ts", "dev": "tsup --config tsup.config.ts --watch", - "lint": "eslint src", + "lint": "tsc --noEmit", "format": "prettier --write \"src/**/*.ts\"" }, "dependencies": { From bf1de711b52c8e079764d76a5cd7a158abe1da68 Mon Sep 17 00:00:00 2001 From: Ayush Date: Tue, 30 Jun 2026 14:32:08 +0530 Subject: [PATCH 08/11] Complete the review by CodeRabbit --- .../urbackend-cli/src/commands/generate/index.ts | 4 ++-- sdks/urbackend-cli/src/commands/init/index.ts | 3 ++- sdks/urbackend-cli/src/commands/pull/index.ts | 3 ++- sdks/urbackend-cli/src/core/workspace.ts | 16 ++++++++++++++++ sdks/urbackend-cli/src/index.ts | 4 ++-- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/sdks/urbackend-cli/src/commands/generate/index.ts b/sdks/urbackend-cli/src/commands/generate/index.ts index 76558e3ca..872e9f11f 100644 --- a/sdks/urbackend-cli/src/commands/generate/index.ts +++ b/sdks/urbackend-cli/src/commands/generate/index.ts @@ -40,7 +40,7 @@ function parseFields(fields: CollectionField[], indent: string): string { for (const f of fields) { const isOptional = !f.required ? "?" : ""; const typeStr = mapType(f, indent); - out += `${indent} ${f.key}${isOptional}: ${typeStr};\n`; + out += `${indent} "${f.key}"${isOptional}: ${typeStr};\n`; } out += `${indent}}`; return out; @@ -67,7 +67,7 @@ export async function generateCommand(): Promise { for (const field of schema.model as CollectionField[]) { const isOptional = !field.required ? "?" : ""; const typeStr = mapType(field, " "); - dtsContent += ` ${field.key}${isOptional}: ${typeStr};\n`; + dtsContent += ` "${field.key}"${isOptional}: ${typeStr};\n`; } dtsContent += ` createdAt: string | Date;\n`; diff --git a/sdks/urbackend-cli/src/commands/init/index.ts b/sdks/urbackend-cli/src/commands/init/index.ts index 60568c5f1..b457314e6 100644 --- a/sdks/urbackend-cli/src/commands/init/index.ts +++ b/sdks/urbackend-cli/src/commands/init/index.ts @@ -70,7 +70,8 @@ export async function initCommand(projectIdOrName?: string): Promise { const gitignorePath = path.join(process.cwd(), ".gitignore"); if (fs.existsSync(gitignorePath)) { const gitignore = fs.readFileSync(gitignorePath, "utf8"); - if (!gitignore.includes(".ub")) { + const lines = gitignore.split(/\r?\n/).map(line => line.trim()); + if (!lines.includes(".ub") && !lines.includes("/.ub") && !lines.includes(".ub/")) { fs.appendFileSync(gitignorePath, "\n# urBackend local workspace\n.ub\n", "utf8"); logger.info("Added .ub to .gitignore"); } diff --git a/sdks/urbackend-cli/src/commands/pull/index.ts b/sdks/urbackend-cli/src/commands/pull/index.ts index 39f1339ba..0b61f1a6a 100644 --- a/sdks/urbackend-cli/src/commands/pull/index.ts +++ b/sdks/urbackend-cli/src/commands/pull/index.ts @@ -1,4 +1,4 @@ -import { loadWorkspaceConfig, saveSchemaFile } from "../../core/workspace.js"; +import { loadWorkspaceConfig, saveSchemaFile, clearSchemaFiles } from "../../core/workspace.js"; import { getProject } from "../../services/project.service.js"; import { getToken } from "../../core/config.js"; import { APIError } from "../../core/errors.js"; @@ -34,6 +34,7 @@ export async function pullCommand(): Promise { } let count = 0; + clearSchemaFiles(); for (const collection of project.collections) { saveSchemaFile(collection.name, { name: collection.name, diff --git a/sdks/urbackend-cli/src/core/workspace.ts b/sdks/urbackend-cli/src/core/workspace.ts index 1f774e6f1..9f8a19059 100644 --- a/sdks/urbackend-cli/src/core/workspace.ts +++ b/sdks/urbackend-cli/src/core/workspace.ts @@ -43,6 +43,10 @@ export function saveWorkspaceConfig(config: WorkspaceConfig): void { } export function saveSchemaFile(collectionName: string, schema: any): void { + if (typeof collectionName !== "string" || collectionName.includes("/") || collectionName.includes("\\") || collectionName.includes("..")) { + throw new Error(`Invalid collection name: ${collectionName}`); + } + const schemasDir = getSchemasDir(); if (!fs.existsSync(schemasDir)) { fs.mkdirSync(schemasDir, { recursive: true }); @@ -76,3 +80,15 @@ export function getLocalSchemas(): { name: string; schema: any }[] { return schemas; } + +export function clearSchemaFiles(): void { + const schemasDir = getSchemasDir(); + if (!fs.existsSync(schemasDir)) return; + + const files = fs.readdirSync(schemasDir); + for (const file of files) { + if (file.endsWith(".json")) { + fs.unlinkSync(path.join(schemasDir, file)); + } + } +} diff --git a/sdks/urbackend-cli/src/index.ts b/sdks/urbackend-cli/src/index.ts index 0aafa31a6..4cc7482a3 100644 --- a/sdks/urbackend-cli/src/index.ts +++ b/sdks/urbackend-cli/src/index.ts @@ -99,9 +99,9 @@ program // ── Workspace ───────────────────────────────────────────────────────────────── program - .command("init") + .command("init [projectIdOrName]") .description("Initialize a local urBackend project workspace") - .action(initCommand); + .action((projectIdOrName) => initCommand(typeof projectIdOrName === "string" ? projectIdOrName : undefined)); program .command("pull") From 3f6e3cb7175c94a3e9f03269681dc6273141d589 Mon Sep 17 00:00:00 2001 From: Ayush Date: Tue, 30 Jun 2026 23:37:35 +0530 Subject: [PATCH 09/11] Added input for version cases --- sdks/urbackend-cli/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/urbackend-cli/src/index.ts b/sdks/urbackend-cli/src/index.ts index 4cc7482a3..061d92862 100644 --- a/sdks/urbackend-cli/src/index.ts +++ b/sdks/urbackend-cli/src/index.ts @@ -18,7 +18,7 @@ const program = new Command(); program .name("ub") .description("Official urBackend CLI — manage projects, schemas, and more") - .version("0.1.0"); + .version("0.2.0", "-v, -V, --version", "Output the current version"); // ── Authentication ────────────────────────────────────────────────────────── From 79e016b6cecc9edc3127cefc8db381d8de4c100c Mon Sep 17 00:00:00 2001 From: Ayush Date: Tue, 30 Jun 2026 23:38:46 +0530 Subject: [PATCH 10/11] Updated the version - 0.2.0 --- sdks/urbackend-cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdks/urbackend-cli/package.json b/sdks/urbackend-cli/package.json index c3ed8edfa..4cc760531 100644 --- a/sdks/urbackend-cli/package.json +++ b/sdks/urbackend-cli/package.json @@ -1,6 +1,6 @@ { "name": "@urbackend/cli", - "version": "0.1.1", + "version": "0.2.0", "description": "Official CLI for urBackend — manage projects, schemas, and more from your terminal", "publishConfig": { "access": "public" From f522177b4fc12b7d2aca7d6729891a489122cd4b Mon Sep 17 00:00:00 2001 From: Ayush Date: Wed, 1 Jul 2026 00:31:30 +0530 Subject: [PATCH 11/11] Make pull cmd to look through all the incoming remote collections name first --- sdks/urbackend-cli/src/commands/pull/index.ts | 10 +++++++++- sdks/urbackend-cli/src/core/workspace.ts | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sdks/urbackend-cli/src/commands/pull/index.ts b/sdks/urbackend-cli/src/commands/pull/index.ts index 0b61f1a6a..367d32b25 100644 --- a/sdks/urbackend-cli/src/commands/pull/index.ts +++ b/sdks/urbackend-cli/src/commands/pull/index.ts @@ -1,4 +1,4 @@ -import { loadWorkspaceConfig, saveSchemaFile, clearSchemaFiles } from "../../core/workspace.js"; +import { loadWorkspaceConfig, saveSchemaFile, clearSchemaFiles, isValidCollectionName } from "../../core/workspace.js"; import { getProject } from "../../services/project.service.js"; import { getToken } from "../../core/config.js"; import { APIError } from "../../core/errors.js"; @@ -34,6 +34,14 @@ export async function pullCommand(): Promise { } let count = 0; + + // Validate all remote collection names up front before clearing local schemas + for (const collection of project.collections) { + if (!isValidCollectionName(collection.name)) { + throw new Error(`Invalid collection name received from remote: ${collection.name}`); + } + } + clearSchemaFiles(); for (const collection of project.collections) { saveSchemaFile(collection.name, { diff --git a/sdks/urbackend-cli/src/core/workspace.ts b/sdks/urbackend-cli/src/core/workspace.ts index 9f8a19059..df598b144 100644 --- a/sdks/urbackend-cli/src/core/workspace.ts +++ b/sdks/urbackend-cli/src/core/workspace.ts @@ -42,8 +42,12 @@ export function saveWorkspaceConfig(config: WorkspaceConfig): void { fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8"); } +export function isValidCollectionName(collectionName: string): boolean { + return typeof collectionName === "string" && !collectionName.includes("/") && !collectionName.includes("\\") && !collectionName.includes(".."); +} + export function saveSchemaFile(collectionName: string, schema: any): void { - if (typeof collectionName !== "string" || collectionName.includes("/") || collectionName.includes("\\") || collectionName.includes("..")) { + if (!isValidCollectionName(collectionName)) { throw new Error(`Invalid collection name: ${collectionName}`); }