From 4a2756a83a2314eeadd6e9dbdb1382e12bbd6363 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:53:48 +0000 Subject: [PATCH 1/8] Initial plan From 4a4652b0347b5208505b563c3be2787c5f29c042 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 2 Apr 2026 07:59:19 +0000 Subject: [PATCH 2/8] Add P0 remote API commands for auth, data, and metadata Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/205a6eab-27f7-46cf-aee8-b628c23b3490 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/cli/package.json | 2 + packages/cli/src/commands/auth/login.ts | 133 +++++++++++++++++++++ packages/cli/src/commands/auth/logout.ts | 51 ++++++++ packages/cli/src/commands/auth/whoami.ts | 85 +++++++++++++ packages/cli/src/commands/data/create.ts | 110 +++++++++++++++++ packages/cli/src/commands/data/delete.ts | 81 +++++++++++++ packages/cli/src/commands/data/get.ts | 84 +++++++++++++ packages/cli/src/commands/data/query.ts | 127 ++++++++++++++++++++ packages/cli/src/commands/data/update.ts | 114 ++++++++++++++++++ packages/cli/src/commands/meta/delete.ts | 95 +++++++++++++++ packages/cli/src/commands/meta/get.ts | 73 +++++++++++ packages/cli/src/commands/meta/list.ts | 105 ++++++++++++++++ packages/cli/src/commands/meta/register.ts | 95 +++++++++++++++ packages/cli/src/utils/api-client.ts | 70 +++++++++++ packages/cli/src/utils/auth-config.ts | 100 ++++++++++++++++ packages/cli/src/utils/output-formatter.ts | 91 ++++++++++++++ 16 files changed, 1416 insertions(+) create mode 100644 packages/cli/src/commands/auth/login.ts create mode 100644 packages/cli/src/commands/auth/logout.ts create mode 100644 packages/cli/src/commands/auth/whoami.ts create mode 100644 packages/cli/src/commands/data/create.ts create mode 100644 packages/cli/src/commands/data/delete.ts create mode 100644 packages/cli/src/commands/data/get.ts create mode 100644 packages/cli/src/commands/data/query.ts create mode 100644 packages/cli/src/commands/data/update.ts create mode 100644 packages/cli/src/commands/meta/delete.ts create mode 100644 packages/cli/src/commands/meta/get.ts create mode 100644 packages/cli/src/commands/meta/list.ts create mode 100644 packages/cli/src/commands/meta/register.ts create mode 100644 packages/cli/src/utils/api-client.ts create mode 100644 packages/cli/src/utils/auth-config.ts create mode 100644 packages/cli/src/utils/output-formatter.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 015ee8dc7..33c488fd2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@ai-sdk/gateway": "^3.0.84", + "@objectstack/client": "workspace:*", "@objectstack/core": "workspace:*", "@objectstack/driver-memory": "workspace:^", "@objectstack/objectql": "workspace:^", @@ -54,6 +55,7 @@ "chalk": "^5.6.2", "dotenv-flow": "^4.1.0", "tsx": "^4.21.0", + "yaml": "^2.4.1", "zod": "^4.3.6" }, "peerDependencies": { diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts new file mode 100644 index 000000000..b84e9c3dd --- /dev/null +++ b/packages/cli/src/commands/auth/login.ts @@ -0,0 +1,133 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printSuccess, printError, printKV } from '../../utils/format.js'; +import { writeAuthConfig } from '../../utils/auth-config.js'; +import { ObjectStackClient } from '@objectstack/client'; +import * as readline from 'node:readline/promises'; +import { stdin as input, stdout as output } from 'node:process'; + +export default class AuthLogin extends Command { + static override description = 'Authenticate and store session credentials'; + + static override examples = [ + '$ os auth login', + '$ os auth login --url https://api.example.com', + '$ os auth login --email user@example.com --password mypassword', + ]; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + default: 'http://localhost:3000', + env: 'OBJECTSTACK_URL', + }), + email: Flags.string({ + char: 'e', + description: 'Email address', + }), + password: Flags.string({ + char: 'p', + description: 'Password', + }), + json: Flags.boolean({ + description: 'Output as JSON', + }), + }; + + async run(): Promise { + const { flags } = await this.parse(AuthLogin); + + try { + if (!flags.json) { + printHeader('ObjectStack Login'); + printKV('Server', flags.url); + console.log(''); + } + + // Prompt for credentials if not provided + let email = flags.email; + let password = flags.password; + + if (!email || !password) { + const rl = readline.createInterface({ input, output }); + + if (!email) { + email = await rl.question('Email: '); + } + + if (!password) { + // Note: This doesn't hide the password input in the terminal + // For production use, consider using a library like 'inquirer' or 'prompts' + password = await rl.question('Password: '); + } + + rl.close(); + } + + if (!email || !password) { + throw new Error('Email and password are required'); + } + + // Create client and authenticate + const client = new ObjectStackClient({ + baseUrl: flags.url, + }); + + const response = await client.auth.login({ + email, + password, + }); + + // Check if login was successful + if (!response.data?.token && !response.data?.user) { + throw new Error('Login failed: Invalid response from server'); + } + + // Extract token - it might be in different locations depending on the auth system + const token = response.data?.token || (response as any).token; + const user = response.data?.user; + + if (!token) { + throw new Error('Login failed: No token received from server'); + } + + // Store credentials + await writeAuthConfig({ + url: flags.url, + token, + email: user?.email || email, + userId: user?.id, + createdAt: new Date().toISOString(), + }); + + if (flags.json) { + console.log(JSON.stringify({ + success: true, + email: user?.email || email, + userId: user?.id, + }, null, 2)); + } else { + printSuccess('Authentication successful'); + printKV('Email', user?.email || email); + if (user?.id) { + printKV('User ID', user.id); + } + console.log(''); + console.log(' Credentials stored in ~/.objectstack/credentials.json'); + console.log(''); + } + } catch (error: any) { + if (flags.json) { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/auth/logout.ts b/packages/cli/src/commands/auth/logout.ts new file mode 100644 index 000000000..45888e00b --- /dev/null +++ b/packages/cli/src/commands/auth/logout.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Command, Flags } from '@oclif/core'; +import { printHeader, printSuccess, printError } from '../../utils/format.js'; +import { deleteAuthConfig } from '../../utils/auth-config.js'; + +export default class AuthLogout extends Command { + static override description = 'Clear stored authentication credentials'; + + static override examples = [ + '$ os auth logout', + ]; + + static override flags = { + json: Flags.boolean({ + description: 'Output as JSON', + }), + }; + + async run(): Promise { + const { flags } = await this.parse(AuthLogout); + + try { + if (!flags.json) { + printHeader('ObjectStack Logout'); + } + + await deleteAuthConfig(); + + if (flags.json) { + console.log(JSON.stringify({ + success: true, + message: 'Credentials cleared', + }, null, 2)); + } else { + printSuccess('Credentials cleared'); + console.log(''); + } + } catch (error: any) { + if (flags.json) { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/auth/whoami.ts b/packages/cli/src/commands/auth/whoami.ts new file mode 100644 index 000000000..b8fed3423 --- /dev/null +++ b/packages/cli/src/commands/auth/whoami.ts @@ -0,0 +1,85 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Command, Flags } from '@oclif/core'; +import { printHeader, printError, printKV } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class AuthWhoami extends Command { + static override description = 'Show current session information'; + + static override examples = [ + '$ os auth whoami', + '$ os auth whoami --json', + '$ os auth whoami --url https://api.example.com --token ', + ]; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table', 'yaml'], + default: 'table', + }), + }; + + async run(): Promise { + const { flags } = await this.parse(AuthWhoami); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + // Check if we have a token + requireAuth((client as any).token); + + // Get current session info + const response = await client.auth.me(); + + const sessionData = response.data || response; + + if (flags.format === 'json') { + formatOutput(sessionData, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(sessionData, 'yaml'); + } else { + printHeader('Current Session'); + + if (sessionData.user) { + printKV('User ID', sessionData.user.id || '-'); + printKV('Email', sessionData.user.email || '-'); + printKV('Name', sessionData.user.name || '-'); + } + + if (sessionData.session) { + printKV('Session ID', sessionData.session.id || '-'); + printKV('Expires At', sessionData.session.expiresAt || '-'); + } + + console.log(''); + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/data/create.ts b/packages/cli/src/commands/data/create.ts new file mode 100644 index 000000000..b0a66eebd --- /dev/null +++ b/packages/cli/src/commands/data/create.ts @@ -0,0 +1,110 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class DataCreate extends Command { + static override description = 'Create a new record'; + + static override examples = [ + '$ os data create project_task \'{"name":"New Task","status":"open"}\'', + '$ os data create project_task --data task-data.json', + '$ os data create project_task --data task-data.json --format json', + ]; + + static override args = { + object: Args.string({ + description: 'Object name (snake_case)', + required: true, + }), + data: Args.string({ + description: 'Record data as JSON string (or use --data flag for file)', + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + data: Flags.string({ + char: 'd', + description: 'Path to JSON file containing record data', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table', 'yaml'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(DataCreate); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Parse record data + let recordData: any; + + if (flags.data) { + // Read from file + const { readFile } = await import('node:fs/promises'); + const fileContent = await readFile(flags.data, 'utf-8'); + try { + recordData = JSON.parse(fileContent); + } catch (e) { + throw new Error(`Invalid JSON in file: ${(e as Error).message}`); + } + } else if (args.data) { + // Parse from argument + try { + recordData = JSON.parse(args.data); + } catch (e) { + throw new Error(`Invalid JSON: ${(e as Error).message}`); + } + } else { + throw new Error('Record data is required (provide JSON string or use --data flag)'); + } + + // Create the record + const result = await client.data.create(args.object, recordData); + + if (flags.format === 'json') { + formatOutput(result, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(result, 'yaml'); + } else { + printSuccess(`Record created: ${result.id}`); + if (result.record) { + console.log(''); + formatOutput(result.record, 'table'); + } + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/data/delete.ts b/packages/cli/src/commands/data/delete.ts new file mode 100644 index 000000000..04054974b --- /dev/null +++ b/packages/cli/src/commands/data/delete.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; + +export default class DataDelete extends Command { + static override description = 'Delete a record'; + + static override examples = [ + '$ os data delete project_task abc123', + '$ os data delete project_task abc123 --format json', + ]; + + static override args = { + object: Args.string({ + description: 'Object name (snake_case)', + required: true, + }), + id: Args.string({ + description: 'Record ID', + required: true, + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(DataDelete); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Delete the record + const result = await client.data.delete(args.object, args.id); + + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: true, + object: result.object, + id: result.id, + deleted: result.deleted, + }, null, 2)); + } else { + printSuccess(`Record deleted: ${result.id}`); + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/data/get.ts b/packages/cli/src/commands/data/get.ts new file mode 100644 index 000000000..1dc2a56ae --- /dev/null +++ b/packages/cli/src/commands/data/get.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class DataGet extends Command { + static override description = 'Get a single record by ID'; + + static override examples = [ + '$ os data get project_task abc123', + '$ os data get project_task abc123 --format json', + ]; + + static override args = { + object: Args.string({ + description: 'Object name (snake_case)', + required: true, + }), + id: Args.string({ + description: 'Record ID', + required: true, + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table', 'yaml'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(DataGet); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Get the record + const result = await client.data.get(args.object, args.id); + + if (flags.format === 'json') { + formatOutput(result, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(result, 'yaml'); + } else { + // Table format - show the record + if (result.record) { + formatOutput(result.record, 'table'); + } else { + console.log('Record not found.'); + } + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/data/query.ts b/packages/cli/src/commands/data/query.ts new file mode 100644 index 000000000..a7043b57a --- /dev/null +++ b/packages/cli/src/commands/data/query.ts @@ -0,0 +1,127 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class DataQuery extends Command { + static override description = 'Query records from an object'; + + static override examples = [ + '$ os data query project_task', + '$ os data query project_task --filter \'{"status":"open"}\'', + '$ os data query project_task --limit 10 --offset 0', + '$ os data query project_task --fields name,status,created_at', + '$ os data query project_task --sort -created_at', + '$ os data query project_task --format json', + ]; + + static override args = { + object: Args.string({ + description: 'Object name (snake_case)', + required: true, + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + filter: Flags.string({ + description: 'Filter criteria as JSON object', + }), + fields: Flags.string({ + description: 'Comma-separated list of fields to retrieve', + }), + sort: Flags.string({ + description: 'Sort field (prefix with - for descending)', + }), + limit: Flags.integer({ + description: 'Maximum number of records to return', + default: 50, + }), + offset: Flags.integer({ + description: 'Number of records to skip', + default: 0, + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table', 'yaml'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(DataQuery); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Build query options + const queryOptions: any = { + limit: flags.limit, + offset: flags.offset, + }; + + if (flags.filter) { + try { + queryOptions.where = JSON.parse(flags.filter); + } catch (e) { + throw new Error(`Invalid filter JSON: ${(e as Error).message}`); + } + } + + if (flags.fields) { + queryOptions.fields = flags.fields.split(',').map(f => f.trim()); + } + + if (flags.sort) { + queryOptions.orderBy = flags.sort; + } + + // Execute query + const result = await client.data.query(args.object, queryOptions); + + if (flags.format === 'json') { + formatOutput(result, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(result, 'yaml'); + } else { + // Table format + if (result.records && result.records.length > 0) { + formatOutput(result.records, 'table'); + } else { + console.log('No records found.'); + } + + if (result.total !== undefined) { + console.log(`\nTotal: ${result.total} record(s)`); + } + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/data/update.ts b/packages/cli/src/commands/data/update.ts new file mode 100644 index 000000000..3d9c0fcb7 --- /dev/null +++ b/packages/cli/src/commands/data/update.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class DataUpdate extends Command { + static override description = 'Update an existing record'; + + static override examples = [ + '$ os data update project_task abc123 \'{"status":"completed"}\'', + '$ os data update project_task abc123 --data update-data.json', + '$ os data update project_task abc123 --data update-data.json --format json', + ]; + + static override args = { + object: Args.string({ + description: 'Object name (snake_case)', + required: true, + }), + id: Args.string({ + description: 'Record ID', + required: true, + }), + data: Args.string({ + description: 'Update data as JSON string (or use --data flag for file)', + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + data: Flags.string({ + char: 'd', + description: 'Path to JSON file containing update data', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table', 'yaml'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(DataUpdate); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Parse update data + let updateData: any; + + if (flags.data) { + // Read from file + const { readFile } = await import('node:fs/promises'); + const fileContent = await readFile(flags.data, 'utf-8'); + try { + updateData = JSON.parse(fileContent); + } catch (e) { + throw new Error(`Invalid JSON in file: ${(e as Error).message}`); + } + } else if (args.data) { + // Parse from argument + try { + updateData = JSON.parse(args.data); + } catch (e) { + throw new Error(`Invalid JSON: ${(e as Error).message}`); + } + } else { + throw new Error('Update data is required (provide JSON string or use --data flag)'); + } + + // Update the record + const result = await client.data.update(args.object, args.id, updateData); + + if (flags.format === 'json') { + formatOutput(result, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(result, 'yaml'); + } else { + printSuccess(`Record updated: ${result.id}`); + if (result.record) { + console.log(''); + formatOutput(result.record, 'table'); + } + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/meta/delete.ts b/packages/cli/src/commands/meta/delete.ts new file mode 100644 index 000000000..991304117 --- /dev/null +++ b/packages/cli/src/commands/meta/delete.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; + +export default class MetaDelete extends Command { + static override description = 'Delete a metadata item'; + + static override examples = [ + '$ os meta delete object my_custom_object', + '$ os meta delete plugin my-plugin', + ]; + + static override args = { + type: Args.string({ + description: 'Metadata type', + required: true, + }), + name: Args.string({ + description: 'Item name (snake_case)', + required: true, + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(MetaDelete); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Note: The current client doesn't have a direct delete method for metadata + // We'll need to use fetch directly with the proper endpoint + const baseUrl = (client as any).baseUrl; + const token = (client as any).token; + + const response = await fetch(`${baseUrl}/api/v1/meta/${args.type}/${args.name}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorBody = await response.text().catch(() => 'Unknown error'); + throw new Error(`Delete failed (${response.status}): ${errorBody}`); + } + + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: true, + type: args.type, + name: args.name, + }, null, 2)); + } else { + printSuccess(`Metadata deleted: ${args.type}/${args.name}`); + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/meta/get.ts b/packages/cli/src/commands/meta/get.ts new file mode 100644 index 000000000..3228e85cb --- /dev/null +++ b/packages/cli/src/commands/meta/get.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class MetaGet extends Command { + static override description = 'Get a metadata item'; + + static override examples = [ + '$ os meta get object project_task', + '$ os meta get plugin my-plugin --format json', + ]; + + static override args = { + type: Args.string({ + description: 'Metadata type', + required: true, + }), + name: Args.string({ + description: 'Item name (snake_case)', + required: true, + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table', 'yaml'], + default: 'json', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(MetaGet); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Get the metadata item + const item = await client.meta.getItem(args.type, args.name); + + formatOutput(item, flags.format as any); + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/meta/list.ts b/packages/cli/src/commands/meta/list.ts new file mode 100644 index 000000000..c62368b62 --- /dev/null +++ b/packages/cli/src/commands/meta/list.ts @@ -0,0 +1,105 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class MetaList extends Command { + static override description = 'List metadata types or items'; + + static override examples = [ + '$ os meta list', + '$ os meta list object', + '$ os meta list plugin --format json', + ]; + + static override args = { + type: Args.string({ + description: 'Metadata type (object, plugin, view, etc.)', + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table', 'yaml'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(MetaList); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + if (!args.type) { + // List all metadata types + const types = await client.meta.getTypes(); + + if (flags.format === 'json') { + formatOutput(types, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(types, 'yaml'); + } else { + console.log('\nAvailable metadata types:\n'); + if (Array.isArray(types)) { + types.forEach(type => console.log(` • ${type}`)); + } else { + console.log('No types available'); + } + console.log(''); + } + } else { + // List items of a specific type + const items = await client.meta.getItems(args.type); + + if (flags.format === 'json') { + formatOutput(items, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(items, 'yaml'); + } else { + console.log(`\n${args.type} items:\n`); + if (Array.isArray(items)) { + if (items.length === 0) { + console.log(' (no items)'); + } else { + items.forEach(item => { + const name = item.name || item.id || JSON.stringify(item); + console.log(` • ${name}`); + }); + } + } + console.log(''); + } + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/commands/meta/register.ts b/packages/cli/src/commands/meta/register.ts new file mode 100644 index 000000000..c0ff81f70 --- /dev/null +++ b/packages/cli/src/commands/meta/register.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { Args, Command, Flags } from '@oclif/core'; +import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; + +export default class MetaRegister extends Command { + static override description = 'Register metadata (create or update)'; + + static override examples = [ + '$ os meta register object --data object-def.json', + '$ os meta register plugin --data plugin-manifest.json', + ]; + + static override args = { + type: Args.string({ + description: 'Metadata type', + required: true, + }), + }; + + static override flags = { + url: Flags.string({ + char: 'u', + description: 'Server URL', + env: 'OBJECTSTACK_URL', + }), + token: Flags.string({ + char: 't', + description: 'Authentication token', + env: 'OBJECTSTACK_TOKEN', + }), + data: Flags.string({ + char: 'd', + description: 'Path to JSON file containing metadata definition', + required: true, + }), + format: Flags.string({ + char: 'f', + description: 'Output format', + options: ['json', 'table'], + default: 'table', + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(MetaRegister); + + try { + const client = await createApiClient({ + url: flags.url, + token: flags.token, + }); + + requireAuth((client as any).token); + + // Read metadata from file + const { readFile } = await import('node:fs/promises'); + const fileContent = await readFile(flags.data, 'utf-8'); + let metadata: any; + + try { + metadata = JSON.parse(fileContent); + } catch (e) { + throw new Error(`Invalid JSON in file: ${(e as Error).message}`); + } + + // Extract name from metadata + const name = metadata.name; + if (!name) { + throw new Error('Metadata definition must include a "name" field'); + } + + // Register the metadata + const result = await client.meta.saveItem(args.type, name, metadata); + + if (flags.format === 'json') { + formatOutput(result, 'json'); + } else { + printSuccess(`Metadata registered: ${args.type}/${name}`); + } + } catch (error: any) { + if (flags.format === 'json') { + console.log(JSON.stringify({ + success: false, + error: error.message, + }, null, 2)); + this.exit(1); + } + printError(error.message || String(error)); + this.exit(1); + } + } +} diff --git a/packages/cli/src/utils/api-client.ts b/packages/cli/src/utils/api-client.ts new file mode 100644 index 000000000..c903a6047 --- /dev/null +++ b/packages/cli/src/utils/api-client.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { ObjectStackClient } from '@objectstack/client'; +import { readAuthConfig, AuthConfig } from './auth-config.js'; + +/** + * API client configuration options for CLI commands + */ +export interface ApiClientOptions { + /** + * Server URL (defaults to OBJECTSTACK_URL env var or http://localhost:3000) + */ + url?: string; + /** + * Authentication token (defaults to stored credentials or OBJECTSTACK_TOKEN env var) + */ + token?: string; + /** + * Enable debug logging + */ + debug?: boolean; +} + +/** + * Create an authenticated ObjectStack API client for CLI commands. + * + * Resolves configuration in this priority order: + * 1. Explicit options passed to the function + * 2. Environment variables (OBJECTSTACK_URL, OBJECTSTACK_TOKEN) + * 3. Stored credentials from `os auth login` + * 4. Defaults (http://localhost:3000) + */ +export async function createApiClient(options: ApiClientOptions = {}): Promise { + // Resolve server URL + const baseUrl = options.url || + process.env.OBJECTSTACK_URL || + 'http://localhost:3000'; + + // Resolve authentication token + let token = options.token || process.env.OBJECTSTACK_TOKEN; + + // If no token provided via options or env, try to load from stored credentials + if (!token) { + try { + const authConfig = await readAuthConfig(); + token = authConfig.token; + } catch { + // No stored credentials - commands will fail if auth is required + } + } + + // Create and return the client + return new ObjectStackClient({ + baseUrl, + token, + debug: options.debug || false, + }); +} + +/** + * Ensure authentication is present, throwing an error if not. + * Use this in commands that require authentication. + */ +export function requireAuth(token?: string): void { + if (!token) { + throw new Error( + 'Authentication required. Please run `os auth login` or set OBJECTSTACK_TOKEN environment variable.' + ); + } +} diff --git a/packages/cli/src/utils/auth-config.ts b/packages/cli/src/utils/auth-config.ts new file mode 100644 index 000000000..98741ed0a --- /dev/null +++ b/packages/cli/src/utils/auth-config.ts @@ -0,0 +1,100 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { homedir } from 'node:os'; +import { join } from 'node:path'; +import { readFile, writeFile, mkdir } from 'node:fs/promises'; + +/** + * Authentication configuration stored in ~/.objectstack/credentials.json + */ +export interface AuthConfig { + /** + * Server URL (base URL for the ObjectStack instance) + */ + url: string; + /** + * Authentication token (Bearer token) + */ + token: string; + /** + * User email (for display purposes) + */ + email?: string; + /** + * User ID + */ + userId?: string; + /** + * Timestamp when credentials were created + */ + createdAt: string; + /** + * Timestamp when credentials were last used + */ + lastUsedAt?: string; +} + +/** + * Get the path to the credentials file + */ +export function getCredentialsPath(): string { + return join(homedir(), '.objectstack', 'credentials.json'); +} + +/** + * Read stored authentication configuration + */ +export async function readAuthConfig(): Promise { + const path = getCredentialsPath(); + try { + const content = await readFile(path, 'utf-8'); + return JSON.parse(content) as AuthConfig; + } catch (error: any) { + if (error.code === 'ENOENT') { + throw new Error('No stored credentials found. Please run `os auth login` first.'); + } + throw new Error(`Failed to read credentials: ${error.message}`); + } +} + +/** + * Write authentication configuration + */ +export async function writeAuthConfig(config: AuthConfig): Promise { + const path = getCredentialsPath(); + const dir = join(homedir(), '.objectstack'); + + // Ensure directory exists + await mkdir(dir, { recursive: true }); + + // Write credentials file + await writeFile(path, JSON.stringify(config, null, 2), { mode: 0o600 }); +} + +/** + * Delete stored authentication configuration + */ +export async function deleteAuthConfig(): Promise { + const path = getCredentialsPath(); + try { + const { unlink } = await import('node:fs/promises'); + await unlink(path); + } catch (error: any) { + if (error.code !== 'ENOENT') { + throw new Error(`Failed to delete credentials: ${error.message}`); + } + } +} + +/** + * Update last used timestamp + */ +export async function touchAuthConfig(): Promise { + try { + const config = await readAuthConfig(); + config.lastUsedAt = new Date().toISOString(); + await writeAuthConfig(config); + } catch { + // Ignore errors - this is best-effort + } +} diff --git a/packages/cli/src/utils/output-formatter.ts b/packages/cli/src/utils/output-formatter.ts new file mode 100644 index 000000000..0788b50c6 --- /dev/null +++ b/packages/cli/src/utils/output-formatter.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import chalk from 'chalk'; +import yaml from 'yaml'; + +/** + * Output format options for CLI commands + */ +export type OutputFormat = 'json' | 'table' | 'yaml'; + +/** + * Format and output data according to the specified format + */ +export function formatOutput(data: any, format: OutputFormat = 'json'): void { + switch (format) { + case 'json': + console.log(JSON.stringify(data, null, 2)); + break; + + case 'yaml': + console.log(yaml.stringify(data)); + break; + + case 'table': + // For table format, handle different data structures + if (Array.isArray(data)) { + printTable(data); + } else if (data && typeof data === 'object') { + // For single objects, print as key-value pairs + printKeyValue(data); + } else { + console.log(String(data)); + } + break; + + default: + console.log(JSON.stringify(data, null, 2)); + } +} + +/** + * Print data as a table (for arrays of objects) + */ +function printTable(data: any[]): void { + if (data.length === 0) { + console.log(chalk.dim('(no data)')); + return; + } + + // Get all unique keys from all objects + const keys = Array.from( + new Set(data.flatMap(item => Object.keys(item))) + ); + + // Print header + console.log(chalk.bold(keys.join(' | '))); + console.log(chalk.dim('─'.repeat(keys.join(' | ').length))); + + // Print rows + for (const item of data) { + const values = keys.map(key => { + const value = item[key]; + if (value === null || value === undefined) return chalk.dim('-'); + if (typeof value === 'object') return chalk.dim('[object]'); + return String(value); + }); + console.log(values.join(' | ')); + } + + console.log(chalk.dim(`\n${data.length} row(s)`)); +} + +/** + * Print object as key-value pairs + */ +function printKeyValue(data: Record, indent = 0): void { + const prefix = ' '.repeat(indent); + + for (const [key, value] of Object.entries(data)) { + if (value === null || value === undefined) { + console.log(`${prefix}${chalk.dim(key + ':')} ${chalk.dim('null')}`); + } else if (typeof value === 'object' && !Array.isArray(value)) { + console.log(`${prefix}${chalk.bold(key + ':')}`); + printKeyValue(value, indent + 1); + } else if (Array.isArray(value)) { + console.log(`${prefix}${chalk.dim(key + ':')} ${chalk.dim(`[${value.length} items]`)}`); + } else { + console.log(`${prefix}${chalk.dim(key + ':')} ${chalk.white(String(value))}`); + } + } +} From 978289b430d47940fcafb8405c72fef2a79ab1aa Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:00:19 +0000 Subject: [PATCH 3/8] Add documentation for remote API commands Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/205a6eab-27f7-46cf-aee8-b628c23b3490 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- CHANGELOG.md | 11 +++ REMOTE_API_COMMANDS.md | 197 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 REMOTE_API_COMMANDS.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c41339fad..047624de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **CLI: Remote API Commands** - Added 12 new CLI commands for interacting with remote ObjectStack servers: + - **Authentication**: `os auth login`, `os auth logout`, `os auth whoami` + - **Data API**: `os data query`, `os data get`, `os data create`, `os data update`, `os data delete` + - **Metadata API**: `os meta list`, `os meta get`, `os meta register`, `os meta delete` + - All commands support `--url` and `--token` flags, or use stored credentials from `~/.objectstack/credentials.json` + - Multiple output formats: `--format json|table|yaml` + - Environment variable support: `OBJECTSTACK_URL`, `OBJECTSTACK_TOKEN` + - See [REMOTE_API_COMMANDS.md](./REMOTE_API_COMMANDS.md) for full documentation + ### Changed - **i18n: `I18nLabelSchema` now accepts `string` only** — `label`, `description`, `title`, and other display-text fields across all UI schemas (`AppSchema`, `NavigationArea`, diff --git a/REMOTE_API_COMMANDS.md b/REMOTE_API_COMMANDS.md new file mode 100644 index 000000000..8a55c2939 --- /dev/null +++ b/REMOTE_API_COMMANDS.md @@ -0,0 +1,197 @@ +# Remote API Commands + +The ObjectStack CLI now includes commands to interact with a running ObjectStack server via its REST APIs. + +## Authentication + +Before using remote API commands, you need to authenticate: + +```bash +# Login and store credentials +os auth login --url https://api.example.com + +# Show current session +os auth whoami + +# Logout (clear stored credentials) +os auth logout +``` + +Credentials are stored in `~/.objectstack/credentials.json` and automatically used for subsequent commands. + +Alternatively, you can provide credentials via environment variables or flags: + +```bash +# Using environment variables +export OBJECTSTACK_URL=https://api.example.com +export OBJECTSTACK_TOKEN=your-token-here + +# Using flags +os data query project_task --url https://api.example.com --token your-token-here +``` + +## Data API Commands + +### Query Records + +```bash +# Query all records +os data query project_task + +# Filter records +os data query project_task --filter '{"status":"open"}' + +# Limit and pagination +os data query project_task --limit 10 --offset 0 + +# Select specific fields +os data query project_task --fields name,status,created_at + +# Sort results +os data query project_task --sort -created_at # descending +os data query project_task --sort created_at # ascending + +# Output formats +os data query project_task --format json +os data query project_task --format yaml +os data query project_task --format table # default +``` + +### Get a Single Record + +```bash +os data get project_task abc123 +os data get project_task abc123 --format json +``` + +### Create a Record + +```bash +# From JSON string +os data create project_task '{"name":"New Task","status":"open"}' + +# From JSON file +os data create project_task --data task.json +``` + +### Update a Record + +```bash +# From JSON string +os data update project_task abc123 '{"status":"completed"}' + +# From JSON file +os data update project_task abc123 --data update.json +``` + +### Delete a Record + +```bash +os data delete project_task abc123 +``` + +## Metadata API Commands + +### List Metadata Types + +```bash +# List all available metadata types +os meta list + +# List items of a specific type +os meta list object +os meta list plugin +os meta list view +``` + +### Get Metadata Item + +```bash +os meta get object project_task +os meta get plugin my-plugin --format json +``` + +### Register Metadata + +```bash +# Register from JSON file +os meta register object --data object-definition.json +os meta register plugin --data plugin-manifest.json +``` + +The metadata file must include a `name` field: + +```json +{ + "name": "my_custom_object", + "label": "My Custom Object", + "fields": { + "name": { + "type": "text", + "label": "Name" + } + } +} +``` + +### Delete Metadata + +```bash +os meta delete object my_custom_object +os meta delete plugin my-plugin +``` + +## Output Formats + +Most commands support multiple output formats via the `--format` flag: + +- `json` - Machine-readable JSON output +- `yaml` - Human-readable YAML output +- `table` - Formatted table output (default for most commands) + +## Environment Variables + +The following environment variables are supported: + +- `OBJECTSTACK_URL` - Default server URL (default: `http://localhost:3000`) +- `OBJECTSTACK_TOKEN` - Authentication token (alternative to `os auth login`) + +## Examples + +### Complete Workflow + +```bash +# 1. Login +os auth login --url https://api.example.com +Email: user@example.com +Password: ******** + +# 2. Query data +os data query project_task --filter '{"status":"open"}' --limit 5 + +# 3. Create a new record +os data create project_task '{"name":"Implement feature","status":"open","priority":"high"}' + +# 4. Update the record +os data update project_task abc123 '{"status":"in_progress"}' + +# 5. List metadata +os meta list object + +# 6. Get object definition +os meta get object project_task --format yaml +``` + +### CI/CD Integration + +```bash +# Use token authentication in CI/CD pipelines +export OBJECTSTACK_URL=https://api.production.com +export OBJECTSTACK_TOKEN=${{ secrets.OBJECTSTACK_TOKEN }} + +# Deploy metadata +os meta register object --data objects/project_task.json + +# Verify deployment +os meta get object project_task --format json +``` From aefd8d671998494eb33a0c9733f34763f14a5cd4 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:01:27 +0000 Subject: [PATCH 4/8] Add unit tests for remote API commands and utilities Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/205a6eab-27f7-46cf-aee8-b628c23b3490 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/cli/test/remote-api-commands.test.ts | 188 +++++++++++++++++ packages/cli/test/remote-api-utils.test.ts | 196 ++++++++++++++++++ 2 files changed, 384 insertions(+) create mode 100644 packages/cli/test/remote-api-commands.test.ts create mode 100644 packages/cli/test/remote-api-utils.test.ts diff --git a/packages/cli/test/remote-api-commands.test.ts b/packages/cli/test/remote-api-commands.test.ts new file mode 100644 index 000000000..9935958ae --- /dev/null +++ b/packages/cli/test/remote-api-commands.test.ts @@ -0,0 +1,188 @@ +import { describe, it, expect } from 'vitest'; +import AuthLogin from '../src/commands/auth/login'; +import AuthLogout from '../src/commands/auth/logout'; +import AuthWhoami from '../src/commands/auth/whoami'; +import DataQuery from '../src/commands/data/query'; +import DataGet from '../src/commands/data/get'; +import DataCreate from '../src/commands/data/create'; +import DataUpdate from '../src/commands/data/update'; +import DataDelete from '../src/commands/data/delete'; +import MetaList from '../src/commands/meta/list'; +import MetaGet from '../src/commands/meta/get'; +import MetaRegister from '../src/commands/meta/register'; +import MetaDelete from '../src/commands/meta/delete'; + +describe('Remote API Commands (oclif)', () => { + describe('Auth Commands', () => { + it('should have auth login command', () => { + expect(AuthLogin.description).toContain('Authenticate'); + expect(AuthLogin.flags).toHaveProperty('url'); + expect(AuthLogin.flags).toHaveProperty('email'); + expect(AuthLogin.flags).toHaveProperty('password'); + expect(AuthLogin.flags).toHaveProperty('json'); + }); + + it('should have auth logout command', () => { + expect(AuthLogout.description).toContain('Clear'); + expect(AuthLogout.flags).toHaveProperty('json'); + }); + + it('should have auth whoami command', () => { + expect(AuthWhoami.description).toContain('session'); + expect(AuthWhoami.flags).toHaveProperty('url'); + expect(AuthWhoami.flags).toHaveProperty('token'); + expect(AuthWhoami.flags).toHaveProperty('format'); + }); + + it('auth commands should have examples', () => { + expect(AuthLogin.examples).toBeDefined(); + expect(AuthLogin.examples.length).toBeGreaterThan(0); + expect(AuthLogout.examples).toBeDefined(); + expect(AuthWhoami.examples).toBeDefined(); + }); + }); + + describe('Data Commands', () => { + it('should have data query command', () => { + expect(DataQuery.description).toContain('Query'); + expect(DataQuery.args).toHaveProperty('object'); + expect(DataQuery.flags).toHaveProperty('filter'); + expect(DataQuery.flags).toHaveProperty('fields'); + expect(DataQuery.flags).toHaveProperty('sort'); + expect(DataQuery.flags).toHaveProperty('limit'); + expect(DataQuery.flags).toHaveProperty('offset'); + expect(DataQuery.flags).toHaveProperty('format'); + }); + + it('should have data get command', () => { + expect(DataGet.description).toContain('single record'); + expect(DataGet.args).toHaveProperty('object'); + expect(DataGet.args).toHaveProperty('id'); + expect(DataGet.flags).toHaveProperty('format'); + }); + + it('should have data create command', () => { + expect(DataCreate.description).toContain('Create'); + expect(DataCreate.args).toHaveProperty('object'); + expect(DataCreate.flags).toHaveProperty('data'); + expect(DataCreate.flags).toHaveProperty('format'); + }); + + it('should have data update command', () => { + expect(DataUpdate.description).toContain('Update'); + expect(DataUpdate.args).toHaveProperty('object'); + expect(DataUpdate.args).toHaveProperty('id'); + expect(DataUpdate.flags).toHaveProperty('data'); + expect(DataUpdate.flags).toHaveProperty('format'); + }); + + it('should have data delete command', () => { + expect(DataDelete.description).toContain('Delete'); + expect(DataDelete.args).toHaveProperty('object'); + expect(DataDelete.args).toHaveProperty('id'); + expect(DataDelete.flags).toHaveProperty('format'); + }); + + it('data commands should support common flags', () => { + const commands = [DataQuery, DataGet, DataCreate, DataUpdate, DataDelete]; + commands.forEach(cmd => { + expect(cmd.flags).toHaveProperty('url'); + expect(cmd.flags).toHaveProperty('token'); + }); + }); + + it('data commands should have examples', () => { + expect(DataQuery.examples).toBeDefined(); + expect(DataQuery.examples.length).toBeGreaterThan(0); + expect(DataGet.examples).toBeDefined(); + expect(DataCreate.examples).toBeDefined(); + expect(DataUpdate.examples).toBeDefined(); + expect(DataDelete.examples).toBeDefined(); + }); + }); + + describe('Metadata Commands', () => { + it('should have meta list command', () => { + expect(MetaList.description).toContain('List metadata'); + expect(MetaList.args).toHaveProperty('type'); + expect(MetaList.flags).toHaveProperty('format'); + }); + + it('should have meta get command', () => { + expect(MetaGet.description).toContain('Get'); + expect(MetaGet.args).toHaveProperty('type'); + expect(MetaGet.args).toHaveProperty('name'); + expect(MetaGet.flags).toHaveProperty('format'); + }); + + it('should have meta register command', () => { + expect(MetaRegister.description).toContain('Register'); + expect(MetaRegister.args).toHaveProperty('type'); + expect(MetaRegister.flags).toHaveProperty('data'); + expect(MetaRegister.flags).toHaveProperty('format'); + }); + + it('should have meta delete command', () => { + expect(MetaDelete.description).toContain('Delete'); + expect(MetaDelete.args).toHaveProperty('type'); + expect(MetaDelete.args).toHaveProperty('name'); + expect(MetaDelete.flags).toHaveProperty('format'); + }); + + it('meta commands should support common flags', () => { + const commands = [MetaList, MetaGet, MetaRegister, MetaDelete]; + commands.forEach(cmd => { + expect(cmd.flags).toHaveProperty('url'); + expect(cmd.flags).toHaveProperty('token'); + }); + }); + + it('meta commands should have examples', () => { + expect(MetaList.examples).toBeDefined(); + expect(MetaList.examples.length).toBeGreaterThan(0); + expect(MetaGet.examples).toBeDefined(); + expect(MetaRegister.examples).toBeDefined(); + expect(MetaDelete.examples).toBeDefined(); + }); + }); + + describe('Command Conventions', () => { + it('all remote commands should support --url flag with OBJECTSTACK_URL env var', () => { + const commands = [ + AuthLogin, AuthWhoami, + DataQuery, DataGet, DataCreate, DataUpdate, DataDelete, + MetaList, MetaGet, MetaRegister, MetaDelete + ]; + + commands.forEach(cmd => { + expect(cmd.flags).toHaveProperty('url'); + expect(cmd.flags.url).toHaveProperty('env', 'OBJECTSTACK_URL'); + }); + }); + + it('authenticated commands should support --token flag with OBJECTSTACK_TOKEN env var', () => { + const commands = [ + AuthWhoami, + DataQuery, DataGet, DataCreate, DataUpdate, DataDelete, + MetaList, MetaGet, MetaRegister, MetaDelete + ]; + + commands.forEach(cmd => { + expect(cmd.flags).toHaveProperty('token'); + expect(cmd.flags.token).toHaveProperty('env', 'OBJECTSTACK_TOKEN'); + }); + }); + + it('all commands should support output formatting', () => { + const commands = [ + AuthWhoami, + DataQuery, DataGet, DataCreate, DataUpdate, DataDelete, + MetaList, MetaGet, MetaRegister, MetaDelete + ]; + + commands.forEach(cmd => { + expect(cmd.flags).toHaveProperty('format'); + }); + }); + }); +}); diff --git a/packages/cli/test/remote-api-utils.test.ts b/packages/cli/test/remote-api-utils.test.ts new file mode 100644 index 000000000..9597050e5 --- /dev/null +++ b/packages/cli/test/remote-api-utils.test.ts @@ -0,0 +1,196 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createApiClient, requireAuth } from '../src/utils/api-client'; +import { readAuthConfig, writeAuthConfig, deleteAuthConfig, getCredentialsPath } from '../src/utils/auth-config'; +import { formatOutput } from '../src/utils/output-formatter'; +import * as fs from 'node:fs/promises'; + +// Mock fs module +vi.mock('node:fs/promises'); + +describe('API Client Utilities', () => { + describe('createApiClient', () => { + it('should use provided URL and token', async () => { + const client = await createApiClient({ + url: 'https://test.example.com', + token: 'test-token', + }); + + expect(client).toBeDefined(); + expect((client as any).baseUrl).toBe('https://test.example.com'); + expect((client as any).token).toBe('test-token'); + }); + + it('should default to localhost when no URL provided', async () => { + const client = await createApiClient({}); + + expect(client).toBeDefined(); + expect((client as any).baseUrl).toBe('http://localhost:3000'); + }); + + it('should use environment variables if no options provided', async () => { + const originalUrl = process.env.OBJECTSTACK_URL; + const originalToken = process.env.OBJECTSTACK_TOKEN; + + process.env.OBJECTSTACK_URL = 'https://env.example.com'; + process.env.OBJECTSTACK_TOKEN = 'env-token'; + + const client = await createApiClient({}); + + expect((client as any).baseUrl).toBe('https://env.example.com'); + expect((client as any).token).toBe('env-token'); + + // Restore + process.env.OBJECTSTACK_URL = originalUrl; + process.env.OBJECTSTACK_TOKEN = originalToken; + }); + }); + + describe('requireAuth', () => { + it('should not throw when token is provided', () => { + expect(() => requireAuth('valid-token')).not.toThrow(); + }); + + it('should throw when token is missing', () => { + expect(() => requireAuth(undefined)).toThrow(/Authentication required/); + }); + + it('should throw when token is empty string', () => { + expect(() => requireAuth('')).toThrow(/Authentication required/); + }); + }); +}); + +describe('Auth Config Utilities', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('getCredentialsPath', () => { + it('should return path to credentials file', () => { + const path = getCredentialsPath(); + expect(path).toContain('.objectstack'); + expect(path).toContain('credentials.json'); + }); + }); + + describe('writeAuthConfig', () => { + it('should write credentials to file with correct permissions', async () => { + const mockMkdir = vi.mocked(fs.mkdir); + const mockWriteFile = vi.mocked(fs.writeFile); + + const config = { + url: 'https://test.example.com', + token: 'test-token', + email: 'user@example.com', + createdAt: '2024-01-01T00:00:00.000Z', + }; + + await writeAuthConfig(config); + + expect(mockMkdir).toHaveBeenCalledWith( + expect.stringContaining('.objectstack'), + { recursive: true } + ); + + expect(mockWriteFile).toHaveBeenCalledWith( + expect.stringContaining('credentials.json'), + expect.stringContaining('test-token'), + { mode: 0o600 } + ); + }); + }); + + describe('readAuthConfig', () => { + it('should read and parse credentials file', async () => { + const mockConfig = { + url: 'https://test.example.com', + token: 'test-token', + email: 'user@example.com', + createdAt: '2024-01-01T00:00:00.000Z', + }; + + const mockReadFile = vi.mocked(fs.readFile); + mockReadFile.mockResolvedValue(JSON.stringify(mockConfig)); + + const config = await readAuthConfig(); + + expect(config).toEqual(mockConfig); + }); + + it('should throw helpful error when file does not exist', async () => { + const mockReadFile = vi.mocked(fs.readFile); + mockReadFile.mockRejectedValue({ code: 'ENOENT' }); + + await expect(readAuthConfig()).rejects.toThrow(/No stored credentials/); + }); + }); + + describe('deleteAuthConfig', () => { + it('should delete credentials file', async () => { + const mockUnlink = vi.fn().mockResolvedValue(undefined); + vi.doMock('node:fs/promises', () => ({ + unlink: mockUnlink, + })); + + await deleteAuthConfig(); + + // Should not throw + }); + + it('should not throw if file does not exist', async () => { + const mockUnlink = vi.fn().mockRejectedValue({ code: 'ENOENT' }); + vi.doMock('node:fs/promises', () => ({ + unlink: mockUnlink, + })); + + await expect(deleteAuthConfig()).resolves.not.toThrow(); + }); + }); +}); + +describe('Output Formatter Utilities', () => { + beforeEach(() => { + // Spy on console.log + vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + it('should format JSON output', () => { + const data = { name: 'test', value: 123 }; + formatOutput(data, 'json'); + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('"name": "test"') + ); + }); + + it('should format YAML output', () => { + const data = { name: 'test', value: 123 }; + formatOutput(data, 'yaml'); + + expect(console.log).toHaveBeenCalled(); + }); + + it('should format table output for arrays', () => { + const data = [ + { name: 'item1', value: 1 }, + { name: 'item2', value: 2 }, + ]; + formatOutput(data, 'table'); + + expect(console.log).toHaveBeenCalled(); + }); + + it('should format table output for single object', () => { + const data = { name: 'test', value: 123 }; + formatOutput(data, 'table'); + + expect(console.log).toHaveBeenCalled(); + }); + + it('should handle empty arrays', () => { + const data: any[] = []; + formatOutput(data, 'table'); + + expect(console.log).toHaveBeenCalled(); + }); +}); From b1b8cc98d2d6df645f771ff915c4a0f502838a22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:24:55 +0000 Subject: [PATCH 5/8] Fix CI: remove unused imports, fix URL injection, and update pnpm-lock.yaml Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/cc3d4634-2e71-4d72-ab01-bf11a371eef3 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/cli/src/commands/auth/login.ts | 2 +- packages/cli/src/commands/data/create.ts | 2 +- packages/cli/src/commands/data/delete.ts | 2 +- packages/cli/src/commands/data/get.ts | 2 +- packages/cli/src/commands/data/query.ts | 2 +- packages/cli/src/commands/data/update.ts | 2 +- packages/cli/src/commands/meta/delete.ts | 4 +- packages/cli/src/commands/meta/get.ts | 2 +- packages/cli/src/commands/meta/list.ts | 2 +- packages/cli/src/commands/meta/register.ts | 2 +- packages/cli/src/utils/api-client.ts | 2 +- pnpm-lock.yaml | 165 +++++++++++---------- 12 files changed, 102 insertions(+), 87 deletions(-) diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index b84e9c3dd..3f3f8ca54 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -1,6 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { Args, Command, Flags } from '@oclif/core'; +import { Command, Flags } from '@oclif/core'; import { printHeader, printSuccess, printError, printKV } from '../../utils/format.js'; import { writeAuthConfig } from '../../utils/auth-config.js'; import { ObjectStackClient } from '@objectstack/client'; diff --git a/packages/cli/src/commands/data/create.ts b/packages/cli/src/commands/data/create.ts index b0a66eebd..a61cb8225 100644 --- a/packages/cli/src/commands/data/create.ts +++ b/packages/cli/src/commands/data/create.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { printError, printSuccess } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; diff --git a/packages/cli/src/commands/data/delete.ts b/packages/cli/src/commands/data/delete.ts index 04054974b..7677f805f 100644 --- a/packages/cli/src/commands/data/delete.ts +++ b/packages/cli/src/commands/data/delete.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { printError, printSuccess } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; export default class DataDelete extends Command { diff --git a/packages/cli/src/commands/data/get.ts b/packages/cli/src/commands/data/get.ts index 1dc2a56ae..0485ab6f0 100644 --- a/packages/cli/src/commands/data/get.ts +++ b/packages/cli/src/commands/data/get.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError } from '../../utils/format.js'; +import { printError } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; diff --git a/packages/cli/src/commands/data/query.ts b/packages/cli/src/commands/data/query.ts index a7043b57a..a88f00340 100644 --- a/packages/cli/src/commands/data/query.ts +++ b/packages/cli/src/commands/data/query.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError } from '../../utils/format.js'; +import { printError } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; diff --git a/packages/cli/src/commands/data/update.ts b/packages/cli/src/commands/data/update.ts index 3d9c0fcb7..f4cef212a 100644 --- a/packages/cli/src/commands/data/update.ts +++ b/packages/cli/src/commands/data/update.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { printError, printSuccess } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; diff --git a/packages/cli/src/commands/meta/delete.ts b/packages/cli/src/commands/meta/delete.ts index 991304117..7f9898631 100644 --- a/packages/cli/src/commands/meta/delete.ts +++ b/packages/cli/src/commands/meta/delete.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { printError, printSuccess } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; export default class MetaDelete extends Command { @@ -58,7 +58,7 @@ export default class MetaDelete extends Command { const baseUrl = (client as any).baseUrl; const token = (client as any).token; - const response = await fetch(`${baseUrl}/api/v1/meta/${args.type}/${args.name}`, { + const response = await fetch(`${baseUrl}/api/v1/meta/${encodeURIComponent(args.type)}/${encodeURIComponent(args.name)}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, diff --git a/packages/cli/src/commands/meta/get.ts b/packages/cli/src/commands/meta/get.ts index 3228e85cb..a0939fac9 100644 --- a/packages/cli/src/commands/meta/get.ts +++ b/packages/cli/src/commands/meta/get.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError } from '../../utils/format.js'; +import { printError } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; diff --git a/packages/cli/src/commands/meta/list.ts b/packages/cli/src/commands/meta/list.ts index c62368b62..71acb164d 100644 --- a/packages/cli/src/commands/meta/list.ts +++ b/packages/cli/src/commands/meta/list.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError } from '../../utils/format.js'; +import { printError } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; diff --git a/packages/cli/src/commands/meta/register.ts b/packages/cli/src/commands/meta/register.ts index c0ff81f70..3343af735 100644 --- a/packages/cli/src/commands/meta/register.ts +++ b/packages/cli/src/commands/meta/register.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { Args, Command, Flags } from '@oclif/core'; -import { printHeader, printError, printSuccess } from '../../utils/format.js'; +import { printError, printSuccess } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; import { formatOutput } from '../../utils/output-formatter.js'; diff --git a/packages/cli/src/utils/api-client.ts b/packages/cli/src/utils/api-client.ts index c903a6047..e3ec661d7 100644 --- a/packages/cli/src/utils/api-client.ts +++ b/packages/cli/src/utils/api-client.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { ObjectStackClient } from '@objectstack/client'; -import { readAuthConfig, AuthConfig } from './auth-config.js'; +import { readAuthConfig } from './auth-config.js'; /** * API client configuration options for CLI commands diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a8612cb2..d88dd933e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,7 +50,7 @@ importers: version: 25.5.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -68,7 +68,7 @@ importers: version: 16.7.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.7.0(react@19.2.4))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) fumadocs-mdx: specifier: 14.2.11 - version: 14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.7.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.7.0(react@19.2.4))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.7.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.7.0(react@19.2.4))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) fumadocs-ui: specifier: 16.7.7 version: 16.7.7(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.7.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.7.0(react@19.2.4))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(shiki@4.0.2)(tailwindcss@4.2.2) @@ -265,7 +265,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 6.0.1(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) autoprefixer: specifier: ^10.4.27 version: 10.4.27(postcss@8.5.8) @@ -289,10 +289,10 @@ importers: version: 6.0.2 vite: specifier: ^8.0.3 - version: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + version: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) examples/app-crm: dependencies: @@ -404,7 +404,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/adapters/fastify: devDependencies: @@ -419,7 +419,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/adapters/hono: devDependencies: @@ -434,7 +434,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/adapters/nestjs: devDependencies: @@ -452,7 +452,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/adapters/nextjs: devDependencies: @@ -473,7 +473,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/adapters/nuxt: devDependencies: @@ -488,7 +488,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/adapters/sveltekit: devDependencies: @@ -497,19 +497,22 @@ importers: version: link:../../runtime '@sveltejs/kit': specifier: ^2.55.0 - version: 2.55.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.50.3)(typescript@6.0.2)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 2.55.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.50.3)(typescript@6.0.2)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: ^6.0.2 version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/cli: dependencies: '@ai-sdk/gateway': specifier: ^3.0.84 version: 3.0.84(zod@4.3.6) + '@objectstack/client': + specifier: workspace:* + version: link:../client '@objectstack/core': specifier: workspace:* version: link:../core @@ -552,6 +555,9 @@ importers: tsx: specifier: ^4.21.0 version: 4.21.0 + yaml: + specifier: ^2.4.1 + version: 2.8.3 zod: specifier: ^4.3.6 version: 4.3.6 @@ -567,13 +573,13 @@ importers: version: 25.5.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) typescript: specifier: ^6.0.2 version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/client: dependencies: @@ -613,7 +619,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/client-react: dependencies: @@ -660,7 +666,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/create-objectstack: dependencies: @@ -676,7 +682,7 @@ importers: version: 25.5.0 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) typescript: specifier: ^6.0.2 version: 6.0.2 @@ -716,7 +722,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/objectql: dependencies: @@ -735,7 +741,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/driver-memory: dependencies: @@ -757,7 +763,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/driver-sql: dependencies: @@ -785,7 +791,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/driver-turso: dependencies: @@ -816,7 +822,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/plugin-audit: dependencies: @@ -835,7 +841,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/plugin-auth: dependencies: @@ -847,7 +853,7 @@ importers: version: link:../../spec better-auth: specifier: ^1.5.6 - version: 1.5.6(17c5f2326fac0df6c6e66da26192619c) + version: 1.5.6(43a6435ce489d5f947f3ba61921932cd) devDependencies: '@objectstack/cli': specifier: workspace:* @@ -860,7 +866,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/plugin-dev: dependencies: @@ -906,7 +912,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/plugin-hono-server: dependencies: @@ -931,7 +937,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/plugin-msw: dependencies: @@ -962,7 +968,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/plugin-security: dependencies: @@ -981,7 +987,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/plugins/plugin-setup: dependencies: @@ -1000,7 +1006,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/rest: dependencies: @@ -1019,7 +1025,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/runtime: dependencies: @@ -1044,7 +1050,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-ai: dependencies: @@ -1069,7 +1075,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-analytics: dependencies: @@ -1088,7 +1094,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-automation: dependencies: @@ -1107,7 +1113,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-cache: dependencies: @@ -1126,7 +1132,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-feed: dependencies: @@ -1145,7 +1151,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-i18n: dependencies: @@ -1164,7 +1170,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-job: dependencies: @@ -1183,7 +1189,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-queue: dependencies: @@ -1202,7 +1208,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-realtime: dependencies: @@ -1221,7 +1227,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/services/service-storage: dependencies: @@ -1240,7 +1246,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/spec: dependencies: @@ -1256,7 +1262,7 @@ importers: version: 25.5.0 '@vitest/coverage-v8': specifier: ^4.1.2 - version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) + version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -1265,7 +1271,7 @@ importers: version: 6.0.2 vitest: specifier: ^4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) packages/types: dependencies: @@ -7259,6 +7265,11 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -9178,11 +9189,11 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/kit@2.55.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.50.3)(typescript@6.0.2)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@sveltejs/kit@2.55.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.50.3)(typescript@6.0.2)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -9194,27 +9205,27 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.50.3 - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: '@opentelemetry/api': 1.9.0 typescript: 6.0.2 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) obug: 2.1.1 svelte: 5.50.3 - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 svelte: 5.50.3 - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - vitefu: 1.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@swc/helpers@0.5.15': dependencies: @@ -9484,12 +9495,12 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))': + '@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.2 @@ -9501,7 +9512,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@4.1.2': dependencies: @@ -9512,14 +9523,14 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.14(@types/node@25.5.0)(typescript@6.0.2) - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@4.1.2': dependencies: @@ -9746,7 +9757,7 @@ snapshots: baseline-browser-mapping@2.10.11: {} - better-auth@1.5.6(17c5f2326fac0df6c6e66da26192619c): + better-auth@1.5.6(43a6435ce489d5f947f3ba61921932cd): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.41.0(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(better-sqlite3@12.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2))(typescript@6.0.2))(better-sqlite3@12.8.0)(knex@3.2.7(better-sqlite3@12.8.0)(mysql2@3.15.3))(kysely@0.28.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(better-sqlite3@12.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2))) @@ -9767,7 +9778,7 @@ snapshots: zod: 4.3.6 optionalDependencies: '@prisma/client': 7.4.2(prisma@7.4.2(@types/react@19.2.14)(better-sqlite3@12.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2))(typescript@6.0.2) - '@sveltejs/kit': 2.55.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.50.3)(typescript@6.0.2)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/kit': 2.55.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.50.3)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.50.3)(typescript@6.0.2)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) better-sqlite3: 12.8.0 drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(@libsql/client@0.17.2)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(better-sqlite3@12.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2))(typescript@6.0.2))(better-sqlite3@12.8.0)(knex@3.2.7(better-sqlite3@12.8.0)(mysql2@3.15.3))(kysely@0.28.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(better-sqlite3@12.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2)) mongodb: 7.1.0 @@ -9777,7 +9788,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) svelte: 5.50.3 - vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -10627,7 +10638,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.7.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.7.0(react@19.2.4))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): + fumadocs-mdx@14.2.11(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.7.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@1.7.0(react@19.2.4))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -10653,7 +10664,7 @@ snapshots: '@types/react': 19.2.14 next: 16.2.1(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -12261,13 +12272,14 @@ snapshots: pluralize@8.0.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.8 tsx: 4.21.0 + yaml: 2.8.3 postcss-selector-parser@7.1.1: dependencies: @@ -13160,7 +13172,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 @@ -13171,7 +13183,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.60.0 source-map: 0.7.6 @@ -13368,7 +13380,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): + vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -13381,15 +13393,16 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 tsx: 4.21.0 + yaml: 2.8.3 - vitefu@1.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): + vitefu@1.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@25.5.0)(typescript@6.0.2))(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.2 '@vitest/runner': 4.1.2 '@vitest/snapshot': 4.1.2 @@ -13406,7 +13419,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -13493,6 +13506,8 @@ snapshots: yallist@4.0.0: {} + yaml@2.8.3: {} + yargs-parser@21.1.1: {} yargs@17.7.2: From b967addcc7425b5edcfb1d03da2ae56a6e8af4bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:43:55 +0000 Subject: [PATCH 6/8] Address all PR review feedback: client URL fallback, token exposure, chmod, password masking, yaml formats, tests Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/cbe33622-849e-44e4-a736-3622d7348646 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- CHANGELOG.md | 2 +- packages/cli/src/commands/auth/login.ts | 64 ++++++++++++++++++++-- packages/cli/src/commands/auth/whoami.ts | 8 +-- packages/cli/src/commands/data/create.ts | 4 +- packages/cli/src/commands/data/delete.ts | 9 ++- packages/cli/src/commands/data/get.ts | 4 +- packages/cli/src/commands/data/query.ts | 4 +- packages/cli/src/commands/data/update.ts | 4 +- packages/cli/src/commands/meta/delete.ts | 34 +++--------- packages/cli/src/commands/meta/get.ts | 4 +- packages/cli/src/commands/meta/list.ts | 4 +- packages/cli/src/commands/meta/register.ts | 8 ++- packages/cli/src/utils/api-client.ts | 38 +++++++++---- packages/cli/src/utils/auth-config.ts | 9 ++- packages/cli/test/remote-api-utils.test.ts | 28 +++++----- packages/client/src/index.ts | 13 +++++ 16 files changed, 159 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 047624de0..9d8ef59ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Data API**: `os data query`, `os data get`, `os data create`, `os data update`, `os data delete` - **Metadata API**: `os meta list`, `os meta get`, `os meta register`, `os meta delete` - All commands support `--url` and `--token` flags, or use stored credentials from `~/.objectstack/credentials.json` - - Multiple output formats: `--format json|table|yaml` + - Multiple output formats: `--format json|table|yaml` (yaml available on all commands) - Environment variable support: `OBJECTSTACK_URL`, `OBJECTSTACK_TOKEN` - See [REMOTE_API_COMMANDS.md](./REMOTE_API_COMMANDS.md) for full documentation diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index 3f3f8ca54..e32cce6a3 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -7,6 +7,62 @@ import { ObjectStackClient } from '@objectstack/client'; import * as readline from 'node:readline/promises'; import { stdin as input, stdout as output } from 'node:process'; +/** + * Prompt for a password with masked input (shows * per character). + * Falls back to plain readline.question() in non-TTY environments. + */ +async function promptPassword(promptText: string): Promise { + if (!process.stdin.isTTY) { + const rl = readline.createInterface({ input, output }); + const answer = await rl.question(promptText); + rl.close(); + return answer; + } + + return new Promise((resolve) => { + const chars: string[] = []; + process.stdout.write(promptText); + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + + const cleanup = () => { + process.stdin.setRawMode(false); + process.stdin.pause(); + process.stdin.removeListener('data', handler); + process.stdout.write('\n'); + }; + + const handler = (char: string) => { + switch (char) { + case '\u0003': // Ctrl+C + cleanup(); + process.exit(1); + break; + case '\r': + case '\n': // Enter + cleanup(); + resolve(chars.join('')); + break; + case '\u007f': // Backspace + if (chars.length > 0) { + chars.pop(); + process.stdout.clearLine(0); + process.stdout.cursorTo(0); + process.stdout.write(promptText + '*'.repeat(chars.length)); + } + break; + default: + chars.push(char); + process.stdout.write('*'); + break; + } + }; + + process.stdin.on('data', handler); + }); +} + export default class AuthLogin extends Command { static override description = 'Authenticate and store session credentials'; @@ -57,13 +113,11 @@ export default class AuthLogin extends Command { email = await rl.question('Email: '); } + rl.close(); + if (!password) { - // Note: This doesn't hide the password input in the terminal - // For production use, consider using a library like 'inquirer' or 'prompts' - password = await rl.question('Password: '); + password = await promptPassword('Password: '); } - - rl.close(); } if (!email || !password) { diff --git a/packages/cli/src/commands/auth/whoami.ts b/packages/cli/src/commands/auth/whoami.ts index b8fed3423..cc848aebf 100644 --- a/packages/cli/src/commands/auth/whoami.ts +++ b/packages/cli/src/commands/auth/whoami.ts @@ -10,7 +10,7 @@ export default class AuthWhoami extends Command { static override examples = [ '$ os auth whoami', - '$ os auth whoami --json', + '$ os auth whoami --format json', '$ os auth whoami --url https://api.example.com --token ', ]; @@ -37,14 +37,14 @@ export default class AuthWhoami extends Command { const { flags } = await this.parse(AuthWhoami); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - // Check if we have a token - requireAuth((client as any).token); + requireAuth(token); + // Check if we have a token // Get current session info const response = await client.auth.me(); diff --git a/packages/cli/src/commands/data/create.ts b/packages/cli/src/commands/data/create.ts index a61cb8225..b99b1e86c 100644 --- a/packages/cli/src/commands/data/create.ts +++ b/packages/cli/src/commands/data/create.ts @@ -51,12 +51,12 @@ export default class DataCreate extends Command { const { args, flags } = await this.parse(DataCreate); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); // Parse record data let recordData: any; diff --git a/packages/cli/src/commands/data/delete.ts b/packages/cli/src/commands/data/delete.ts index 7677f805f..b247ee731 100644 --- a/packages/cli/src/commands/data/delete.ts +++ b/packages/cli/src/commands/data/delete.ts @@ -37,7 +37,7 @@ export default class DataDelete extends Command { format: Flags.string({ char: 'f', description: 'Output format', - options: ['json', 'table'], + options: ['json', 'table', 'yaml'], default: 'table', }), }; @@ -46,12 +46,12 @@ export default class DataDelete extends Command { const { args, flags } = await this.parse(DataDelete); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); // Delete the record const result = await client.data.delete(args.object, args.id); @@ -63,6 +63,9 @@ export default class DataDelete extends Command { id: result.id, deleted: result.deleted, }, null, 2)); + } else if (flags.format === 'yaml') { + const { formatOutput } = await import('../../utils/output-formatter.js'); + formatOutput({ success: true, object: result.object, id: result.id, deleted: result.deleted }, 'yaml'); } else { printSuccess(`Record deleted: ${result.id}`); } diff --git a/packages/cli/src/commands/data/get.ts b/packages/cli/src/commands/data/get.ts index 0485ab6f0..8bf145f76 100644 --- a/packages/cli/src/commands/data/get.ts +++ b/packages/cli/src/commands/data/get.ts @@ -47,12 +47,12 @@ export default class DataGet extends Command { const { args, flags } = await this.parse(DataGet); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); // Get the record const result = await client.data.get(args.object, args.id); diff --git a/packages/cli/src/commands/data/query.ts b/packages/cli/src/commands/data/query.ts index a88f00340..1bb4d1068 100644 --- a/packages/cli/src/commands/data/query.ts +++ b/packages/cli/src/commands/data/query.ts @@ -64,12 +64,12 @@ export default class DataQuery extends Command { const { args, flags } = await this.parse(DataQuery); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); // Build query options const queryOptions: any = { diff --git a/packages/cli/src/commands/data/update.ts b/packages/cli/src/commands/data/update.ts index f4cef212a..1aaed1cf2 100644 --- a/packages/cli/src/commands/data/update.ts +++ b/packages/cli/src/commands/data/update.ts @@ -55,12 +55,12 @@ export default class DataUpdate extends Command { const { args, flags } = await this.parse(DataUpdate); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); // Parse update data let updateData: any; diff --git a/packages/cli/src/commands/meta/delete.ts b/packages/cli/src/commands/meta/delete.ts index 7f9898631..7d25bd504 100644 --- a/packages/cli/src/commands/meta/delete.ts +++ b/packages/cli/src/commands/meta/delete.ts @@ -3,6 +3,7 @@ import { Args, Command, Flags } from '@oclif/core'; import { printError, printSuccess } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; export default class MetaDelete extends Command { static override description = 'Delete a metadata item'; @@ -10,6 +11,7 @@ export default class MetaDelete extends Command { static override examples = [ '$ os meta delete object my_custom_object', '$ os meta delete plugin my-plugin', + '$ os meta delete object my_custom_object --format json', ]; static override args = { @@ -37,7 +39,7 @@ export default class MetaDelete extends Command { format: Flags.string({ char: 'f', description: 'Output format', - options: ['json', 'table'], + options: ['json', 'table', 'yaml'], default: 'table', }), }; @@ -46,37 +48,19 @@ export default class MetaDelete extends Command { const { args, flags } = await this.parse(MetaDelete); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); - // Note: The current client doesn't have a direct delete method for metadata - // We'll need to use fetch directly with the proper endpoint - const baseUrl = (client as any).baseUrl; - const token = (client as any).token; - - const response = await fetch(`${baseUrl}/api/v1/meta/${encodeURIComponent(args.type)}/${encodeURIComponent(args.name)}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const errorBody = await response.text().catch(() => 'Unknown error'); - throw new Error(`Delete failed (${response.status}): ${errorBody}`); - } + const result = await client.meta.deleteItem(args.type, args.name); if (flags.format === 'json') { - console.log(JSON.stringify({ - success: true, - type: args.type, - name: args.name, - }, null, 2)); + formatOutput({ success: true, type: args.type, name: args.name, deleted: result.deleted }, 'json'); + } else if (flags.format === 'yaml') { + formatOutput({ success: true, type: args.type, name: args.name, deleted: result.deleted }, 'yaml'); } else { printSuccess(`Metadata deleted: ${args.type}/${args.name}`); } diff --git a/packages/cli/src/commands/meta/get.ts b/packages/cli/src/commands/meta/get.ts index a0939fac9..9c9c157b9 100644 --- a/packages/cli/src/commands/meta/get.ts +++ b/packages/cli/src/commands/meta/get.ts @@ -47,12 +47,12 @@ export default class MetaGet extends Command { const { args, flags } = await this.parse(MetaGet); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); // Get the metadata item const item = await client.meta.getItem(args.type, args.name); diff --git a/packages/cli/src/commands/meta/list.ts b/packages/cli/src/commands/meta/list.ts index 71acb164d..83d5baa2d 100644 --- a/packages/cli/src/commands/meta/list.ts +++ b/packages/cli/src/commands/meta/list.ts @@ -43,12 +43,12 @@ export default class MetaList extends Command { const { args, flags } = await this.parse(MetaList); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); if (!args.type) { // List all metadata types diff --git a/packages/cli/src/commands/meta/register.ts b/packages/cli/src/commands/meta/register.ts index 3343af735..6ab0d1778 100644 --- a/packages/cli/src/commands/meta/register.ts +++ b/packages/cli/src/commands/meta/register.ts @@ -39,7 +39,7 @@ export default class MetaRegister extends Command { format: Flags.string({ char: 'f', description: 'Output format', - options: ['json', 'table'], + options: ['json', 'table', 'yaml'], default: 'table', }), }; @@ -48,12 +48,12 @@ export default class MetaRegister extends Command { const { args, flags } = await this.parse(MetaRegister); try { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: flags.url, token: flags.token, }); - requireAuth((client as any).token); + requireAuth(token); // Read metadata from file const { readFile } = await import('node:fs/promises'); @@ -77,6 +77,8 @@ export default class MetaRegister extends Command { if (flags.format === 'json') { formatOutput(result, 'json'); + } else if (flags.format === 'yaml') { + formatOutput(result, 'yaml'); } else { printSuccess(`Metadata registered: ${args.type}/${name}`); } diff --git a/packages/cli/src/utils/api-client.ts b/packages/cli/src/utils/api-client.ts index e3ec661d7..12f47c431 100644 --- a/packages/cli/src/utils/api-client.ts +++ b/packages/cli/src/utils/api-client.ts @@ -21,6 +21,15 @@ export interface ApiClientOptions { debug?: boolean; } +/** + * Result returned by createApiClient — exposes the resolved token so commands + * can call requireAuth() without accessing private client fields. + */ +export interface ApiClientResult { + client: ObjectStackClient; + token?: string; +} + /** * Create an authenticated ObjectStack API client for CLI commands. * @@ -30,31 +39,40 @@ export interface ApiClientOptions { * 3. Stored credentials from `os auth login` * 4. Defaults (http://localhost:3000) */ -export async function createApiClient(options: ApiClientOptions = {}): Promise { - // Resolve server URL - const baseUrl = options.url || - process.env.OBJECTSTACK_URL || - 'http://localhost:3000'; +export async function createApiClient(options: ApiClientOptions = {}): Promise { + // Resolve server URL (without applying defaults yet) + let baseUrl = options.url || process.env.OBJECTSTACK_URL; // Resolve authentication token let token = options.token || process.env.OBJECTSTACK_TOKEN; - // If no token provided via options or env, try to load from stored credentials - if (!token) { + // If URL or token is missing, try to load from stored credentials + if (!baseUrl || !token) { try { const authConfig = await readAuthConfig(); - token = authConfig.token; + if (!token && authConfig.token) { + token = authConfig.token; + } + if (!baseUrl && authConfig.url) { + baseUrl = authConfig.url; + } } catch { // No stored credentials - commands will fail if auth is required } } - // Create and return the client - return new ObjectStackClient({ + // Apply final default for baseUrl if still not resolved + if (!baseUrl) { + baseUrl = 'http://localhost:3000'; + } + + const client = new ObjectStackClient({ baseUrl, token, debug: options.debug || false, }); + + return { client, token }; } /** diff --git a/packages/cli/src/utils/auth-config.ts b/packages/cli/src/utils/auth-config.ts index 98741ed0a..bb98c5066 100644 --- a/packages/cli/src/utils/auth-config.ts +++ b/packages/cli/src/utils/auth-config.ts @@ -2,7 +2,7 @@ import { homedir } from 'node:os'; import { join } from 'node:path'; -import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises'; /** * Authentication configuration stored in ~/.objectstack/credentials.json @@ -69,6 +69,13 @@ export async function writeAuthConfig(config: AuthConfig): Promise { // Write credentials file await writeFile(path, JSON.stringify(config, null, 2), { mode: 0o600 }); + + // Explicitly enforce permissions in case the file already existed with broader perms + try { + await chmod(path, 0o600); + } catch { + // Best-effort — platforms that don't support chmod will silently continue + } } /** diff --git a/packages/cli/test/remote-api-utils.test.ts b/packages/cli/test/remote-api-utils.test.ts index 9597050e5..515814978 100644 --- a/packages/cli/test/remote-api-utils.test.ts +++ b/packages/cli/test/remote-api-utils.test.ts @@ -10,18 +10,18 @@ vi.mock('node:fs/promises'); describe('API Client Utilities', () => { describe('createApiClient', () => { it('should use provided URL and token', async () => { - const client = await createApiClient({ + const { client, token } = await createApiClient({ url: 'https://test.example.com', token: 'test-token', }); expect(client).toBeDefined(); expect((client as any).baseUrl).toBe('https://test.example.com'); - expect((client as any).token).toBe('test-token'); + expect(token).toBe('test-token'); }); it('should default to localhost when no URL provided', async () => { - const client = await createApiClient({}); + const { client } = await createApiClient({}); expect(client).toBeDefined(); expect((client as any).baseUrl).toBe('http://localhost:3000'); @@ -34,10 +34,10 @@ describe('API Client Utilities', () => { process.env.OBJECTSTACK_URL = 'https://env.example.com'; process.env.OBJECTSTACK_TOKEN = 'env-token'; - const client = await createApiClient({}); + const { client, token } = await createApiClient({}); expect((client as any).baseUrl).toBe('https://env.example.com'); - expect((client as any).token).toBe('env-token'); + expect(token).toBe('env-token'); // Restore process.env.OBJECTSTACK_URL = originalUrl; @@ -127,23 +127,23 @@ describe('Auth Config Utilities', () => { describe('deleteAuthConfig', () => { it('should delete credentials file', async () => { - const mockUnlink = vi.fn().mockResolvedValue(undefined); - vi.doMock('node:fs/promises', () => ({ - unlink: mockUnlink, - })); + const mockUnlink = vi.mocked(fs.unlink); + mockUnlink.mockResolvedValue(undefined as any); await deleteAuthConfig(); - // Should not throw + expect(mockUnlink).toHaveBeenCalled(); + expect(mockUnlink).toHaveBeenCalledWith( + expect.stringContaining('credentials.json') + ); }); it('should not throw if file does not exist', async () => { - const mockUnlink = vi.fn().mockRejectedValue({ code: 'ENOENT' }); - vi.doMock('node:fs/promises', () => ({ - unlink: mockUnlink, - })); + const mockUnlink = vi.mocked(fs.unlink); + mockUnlink.mockRejectedValue({ code: 'ENOENT' } as any); await expect(deleteAuthConfig()).resolves.not.toThrow(); + expect(mockUnlink).toHaveBeenCalled(); }); }); }); diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 93fb191f4..d9fc886f7 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -385,6 +385,19 @@ export class ObjectStackClient { }); return this.unwrapResponse(res); }, + + /** + * Delete a metadata item + * @param type - Metadata type (e.g., 'object', 'plugin') + * @param name - Item name (snake_case identifier) + */ + deleteItem: async (type: string, name: string): Promise<{ type: string; name: string; deleted: boolean }> => { + const route = this.getRoute('metadata'); + const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}`, { + method: 'DELETE', + }); + return this.unwrapResponse(res); + }, /** * Get object metadata with cache support From bfda7e0218e3e6f4b1ad3369bfc22664da6891af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:46:24 +0000 Subject: [PATCH 7/8] Fix Ctrl+C cleanup in login.ts and static import in data/delete.ts Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/cbe33622-849e-44e4-a736-3622d7348646 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/cli/src/commands/auth/login.ts | 2 +- packages/cli/src/commands/data/delete.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index e32cce6a3..f32f1f73f 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -37,7 +37,7 @@ async function promptPassword(promptText: string): Promise { switch (char) { case '\u0003': // Ctrl+C cleanup(); - process.exit(1); + process.kill(process.pid, 'SIGINT'); break; case '\r': case '\n': // Enter diff --git a/packages/cli/src/commands/data/delete.ts b/packages/cli/src/commands/data/delete.ts index b247ee731..88b81f932 100644 --- a/packages/cli/src/commands/data/delete.ts +++ b/packages/cli/src/commands/data/delete.ts @@ -3,6 +3,7 @@ import { Args, Command, Flags } from '@oclif/core'; import { printError, printSuccess } from '../../utils/format.js'; import { createApiClient, requireAuth } from '../../utils/api-client.js'; +import { formatOutput } from '../../utils/output-formatter.js'; export default class DataDelete extends Command { static override description = 'Delete a record'; @@ -64,7 +65,6 @@ export default class DataDelete extends Command { deleted: result.deleted, }, null, 2)); } else if (flags.format === 'yaml') { - const { formatOutput } = await import('../../utils/output-formatter.js'); formatOutput({ success: true, object: result.object, id: result.id, deleted: result.deleted }, 'yaml'); } else { printSuccess(`Record deleted: ${result.id}`); From 820ae34eae0d575be1de86cef2cb048813b4f4a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:05:01 +0000 Subject: [PATCH 8/8] Fix TS2345: add required type: 'email' to client.auth.login() call in auth/login.ts Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/a0bd0f1a-f742-48c7-a74e-41aed3aecf26 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- packages/cli/src/commands/auth/login.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index f32f1f73f..17c0cc5e5 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -130,6 +130,7 @@ export default class AuthLogin extends Command { }); const response = await client.auth.login({ + type: 'email', email, password, });