Skip to content

CLI: Add remote API commands for Data API and Metadata API#1059

Merged
hotlong merged 8 commits intomainfrom
claude/add-remote-api-commands
Apr 2, 2026
Merged

CLI: Add remote API commands for Data API and Metadata API#1059
hotlong merged 8 commits intomainfrom
claude/add-remote-api-commands

Conversation

@Claude
Copy link
Copy Markdown
Contributor

@Claude Claude AI commented Apr 2, 2026

The CLI had 16+ local commands but only 1 remote API call (plugin publish). This adds 12 P0 commands to interact with running ObjectStack servers via REST APIs.

Commands Added

Authentication (3)

  • os auth login - Store credentials in ~/.objectstack/credentials.json (mode 0600)
  • os auth logout - Clear stored credentials
  • os auth whoami - Show current session via GET /auth/get-session

Data API (5)

  • os data query <object> - Query with filters, sorting, pagination
  • os data get <object> <id> - Fetch single record
  • os data create <object> - Create from JSON string or file
  • os data update <object> <id> - Partial update
  • os data delete <object> <id> - Delete record

Metadata API (4)

  • os meta list [type] - List types or items of a type
  • os meta get <type> <name> - Fetch metadata definition
  • os meta register <type> - Register from JSON file
  • os meta delete <type> <name> - Delete metadata item

Infrastructure

Common utilities

  • utils/api-client.ts - Factory for @objectstack/client with auth resolution
  • utils/auth-config.ts - Credential storage management
  • utils/output-formatter.ts - JSON/table/YAML formatting

Shared patterns

  • --url / OBJECTSTACK_URL for server configuration (default: http://localhost:3000)
  • --token / OBJECTSTACK_TOKEN for auth, or read from stored credentials
  • --format json|table|yaml for output formatting
  • Consistent error handling with helpful messages

Example Usage

# Login and store credentials
os auth login --url https://api.example.com
Email: user@example.com
Password: ********

# Query data with filters
os data query project_task --filter '{"status":"open"}' --limit 10 --format table

# Create record from file
os data create project_task --data task.json

# Register metadata
os meta register object --data my-object.json

# CI/CD usage
export OBJECTSTACK_TOKEN=${{ secrets.TOKEN }}
os meta list object --format json

Testing

  • 40+ unit tests covering command structure, flags, and utilities
  • Mocked tests for file I/O and API client operations
  • Convention compliance tests (env vars, output formats, examples)

Dependencies

  • Added @objectstack/client workspace dependency
  • Added yaml for YAML output support

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Apr 2, 2026 9:18am
spec Ready Ready Preview, Comment Apr 2, 2026 9:18am

Request Review

@Claude Claude AI linked an issue Apr 2, 2026 that may be closed by this pull request
31 tasks
@github-actions github-actions bot added dependencies Pull requests that update a dependency file size/xl labels Apr 2, 2026
@github-actions github-actions bot added the documentation Improvements or additions to documentation label Apr 2, 2026
@github-actions github-actions bot added the tests label Apr 2, 2026
@Claude Claude AI changed the title [WIP] Add remote API commands for Data API and Metadata API in CLI CLI: Add remote API commands for Data API and Metadata API Apr 2, 2026
@Claude Claude AI requested a review from hotlong April 2, 2026 08:03
Comment on lines +63 to +66
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 2, 2026

@anthropic-code-agent fix all ci build and test errors

@hotlong hotlong marked this pull request as ready for review April 2, 2026 08:20
Copilot AI review requested due to automatic review settings April 2, 2026 08:20
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 2, 2026

@anthropic-code-agent fix all ci build and test errors

@copilot

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

@anthropic-code-agent fix all ci build and test errors

@copilot

Fixed in commit b1b8cc9. The CI failures had two root causes:

  1. ERR_PNPM_OUTDATED_LOCKFILEpnpm-lock.yaml wasn't regenerated after adding @objectstack/client and yaml to packages/cli/package.json. Regenerated it with pnpm install.
  2. Unused imports (flagged by GitHub Advanced Security across 11 files) — removed Args, printHeader, and AuthConfig where they were imported but not used.
  3. URL injection in meta/delete.ts — applied encodeURIComponent() to user-supplied type/name args used in outbound network requests.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands the ObjectStack CLI with a set of remote-first commands that talk to a running ObjectStack server over the REST APIs, including auth, data CRUD/query, and metadata management, plus shared CLI utilities and tests.

Changes:

  • Added 12 new remote API CLI commands under auth, data, and meta topics.
  • Introduced shared utilities for API client creation, credential storage, and output formatting (json/table/yaml).
  • Added documentation, unit tests for command conventions/utilities, and updated CLI dependencies + changelog.

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
REMOTE_API_COMMANDS.md End-user documentation for remote API CLI workflows and examples.
packages/cli/src/utils/api-client.ts Creates @objectstack/client instances and enforces auth presence.
packages/cli/src/utils/auth-config.ts Reads/writes/deletes ~/.objectstack/credentials.json.
packages/cli/src/utils/output-formatter.ts Implements json / yaml / table output formatting.
packages/cli/src/commands/auth/login.ts Adds interactive/non-interactive login and credential persistence.
packages/cli/src/commands/auth/logout.ts Clears stored credentials.
packages/cli/src/commands/auth/whoami.ts Calls session endpoint and prints current user/session info.
packages/cli/src/commands/data/query.ts Queries records with filter/fields/sort/limit/offset and formatting.
packages/cli/src/commands/data/get.ts Fetches a single record by ID with formatting.
packages/cli/src/commands/data/create.ts Creates a record from JSON arg or JSON file.
packages/cli/src/commands/data/update.ts Partially updates a record from JSON arg or JSON file.
packages/cli/src/commands/data/delete.ts Deletes a record with json/table output.
packages/cli/src/commands/meta/list.ts Lists metadata types or items with json/table/yaml output.
packages/cli/src/commands/meta/get.ts Fetches a metadata item with json/table/yaml output.
packages/cli/src/commands/meta/register.ts Registers metadata from a JSON file (PUT).
packages/cli/src/commands/meta/delete.ts Deletes metadata (currently via direct fetch).
packages/cli/test/remote-api-utils.test.ts Unit tests for api-client/auth-config/output formatter utilities.
packages/cli/test/remote-api-commands.test.ts Convention/shape tests for the new oclif commands.
packages/cli/package.json Adds workspace dependency on @objectstack/client and yaml.
CHANGELOG.md Documents the new remote API commands and their flags/formats.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +34 to +51
// 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
}
}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createApiClient() never uses the stored credentials URL (authConfig.url). After os auth login --url ..., running commands without --url will still default to OBJECTSTACK_URL or http://localhost:3000, which contradicts the documented priority order and breaks the “stored credentials” workflow. Consider loading authConfig when token is missing (or when url is missing) and using authConfig.url as the fallback before defaulting to localhost.

Suggested change
// 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
}
}
// 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 URL or token is missing, try to load from stored credentials
if (!baseUrl || !token) {
try {
const authConfig = await readAuthConfig();
if (!token && authConfig.token) {
token = authConfig.token;
}
if (!baseUrl && (authConfig as any).url) {
baseUrl = (authConfig as any).url;
}
} catch {
// No stored credentials - commands will fail if auth is required
}
}
// Apply final default for baseUrl if still not resolved
if (!baseUrl) {
baseUrl = 'http://localhost:3000';
}

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5
import { ObjectStackClient } from '@objectstack/client';
import { readAuthConfig } from './auth-config.js';

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module imports AuthConfig but never uses it. Removing the unused import avoids lint noise and keeps the file focused.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +58
// 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,
});
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many commands call requireAuth((client as any).token) because ObjectStackClient.token is a private field. This couples the CLI to internal @objectstack/client implementation details and may break on refactors. A more robust approach is to have createApiClient() return the resolved token (or expose a public getter on the client) so requireAuth can validate without as any casts.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +72
// Ensure directory exists
await mkdir(dir, { recursive: true });

// Write credentials file
await writeFile(path, JSON.stringify(config, null, 2), { mode: 0o600 });
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeAuthConfig() writes with { mode: 0o600 }, but if the credentials file already exists, Node won’t necessarily update permissions. That can leave credentials readable if the file previously had broader perms. Consider explicitly chmod’ing to 0600 after write (best-effort, platform-aware) to enforce the security guarantee.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +64
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: ');
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interactive password input is echoed to the terminal (readline.question('Password: ')). This exposes passwords in terminal scrollback/screen recordings and is generally unsafe for an auth command. Consider switching to a prompt implementation that supports masked/hidden input (or implement hidden input with readline keypress handling).

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +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';

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printHeader is imported but never used in this command. Please remove the unused import to avoid lint warnings and keep the module tidy.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +73
// 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}`);
}

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This command bypasses @objectstack/client and hard-codes a DELETE request using fetch, while also reaching into private client fields ((client as any).baseUrl/token). That loses the client’s standardized error handling (and discovered route support) and increases maintenance cost. Prefer adding a client.meta.deleteItem(type, name) API to @objectstack/client (or exposing a safe request helper) and using that here.

Suggested change
// 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}`);
}
// Delegate to the client metadata API so we use standardized routing and error handling
const metaClient = (client as any).meta;
if (!metaClient || typeof metaClient.deleteItem !== 'function') {
throw new Error('Metadata delete is not supported by the current client version.');
}
await metaClient.deleteItem(args.type, args.name);

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +6
import { Args, Command, Flags } from '@oclif/core';
import { printError, printSuccess } from '../../utils/format.js';
import { createApiClient, requireAuth } from '../../utils/api-client.js';

Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printHeader is imported but never used in this command. Please remove the unused import to avoid lint warnings and keep the module tidy.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +18
- 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`
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CHANGELOG claims multiple output formats --format json|table|yaml, but several new commands only allow json|table (e.g., data delete, meta register, meta delete). Either expand those commands to support yaml or adjust this entry to reflect the actual supported formats to avoid misleading users.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +146
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();
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleteAuthConfig tests use vi.doMock('node:fs/promises', ...) after node:fs/promises has already been imported/mocked at the top of the file. This doesn’t reliably affect the module instance used by deleteAuthConfig()’s dynamic import, and the tests currently don’t assert that unlink was called or that ENOENT is handled. Prefer configuring vi.mocked(fs.unlink) directly (and asserting calls) or using vi.resetModules() + re-import to ensure the mock applies.

Suggested change
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();
const mockUnlink = vi.mocked(fs.unlink);
mockUnlink.mockResolvedValue(undefined as any);
await deleteAuthConfig();
expect(mockUnlink).toHaveBeenCalled();
expect(mockUnlink).toHaveBeenCalledWith(
expect.stringContaining('credentials.json')
);
});
it('should not throw if file does not exist', async () => {
const mockUnlink = vi.mocked(fs.unlink);
mockUnlink.mockRejectedValue({ code: 'ENOENT' } as any);
await expect(deleteAuthConfig()).resolves.not.toThrow();
expect(mockUnlink).toHaveBeenCalled();

Copilot uses AI. Check for mistakes.
@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 2, 2026

@copilot fix all

Copilot AI and others added 2 commits April 2, 2026 08:43
…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>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

@copilot fix all

All review feedback addressed in commits b967add and bfda7e0:

  • @objectstack/client: Added meta.deleteItem()meta/delete.ts no longer uses raw fetch or private field access
  • createApiClient(): Now reads authConfig.url from stored credentials as a URL fallback, and returns { client, token } — all 10 command callers updated to avoid (client as any).token
  • auth-config.ts: Added explicit chmod(0o600) after writeFile to enforce permissions on pre-existing files
  • auth/whoami.ts: Fixed example --json--format json
  • auth/login.ts: Password input now masked with * using raw TTY mode, with backspace support and proper SIGINT on Ctrl+C; falls back to plain readline in non-TTY environments
  • data/delete, meta/register, meta/delete: Added --format yaml option for consistency across all commands
  • CHANGELOG.md: Updated to reflect yaml support on all commands
  • Tests: deleteAuthConfig tests now use vi.mocked(fs.unlink) with proper assertions instead of vi.doMock

@hotlong
Copy link
Copy Markdown
Contributor

hotlong commented Apr 2, 2026

@copilot 16:52:28.520 @objectstack/spec:build: ✓ data/TenantDatabaseLifecycle.json
16:53:51.528 @objectstack/cli:build: src/commands/auth/login.ts(132,48): error TS2345: Argument of type '{ email: string; password: string; }' is not assignable to parameter of type '{ type: "email" | "phone" | "username" | "magic-link" | "social"; email?: string | undefined; username?: string | undefined; password?: string | undefined; provider?: string | undefined; redirectTo?: string | undefined; }'.
16:53:51.528 @objectstack/cli:build: Property 'type' is missing in type '{ email: string; password: string; }' but required in type '{ type: "email" | "phone" | "username" | "magic-link" | "social"; email?: string | undefined; username?: string | undefined; password?: string | undefined; provider?: string | undefined; redirectTo?: string | undefined; }'.
16:53:51.581 @objectstack/cli:build:  ELIFECYCLE  Command failed with exit code 2.
16:53:51.596 ERROR @objectstack/cli#build: command (/vercel/path0/packages/cli) /vercel/.local/share/pnpm/.tools/pnpm/10.31.0/bin/pnpm run build exited (2)
16:53:51.608 ERROR run failed: command exited (2)
16:53:51.630 Error: Command "bash scripts/build-vercel.sh" exited with 2

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

@copilot 16:52:28.520 @objectstack/spec:build: ✓ data/TenantDatabaseLifecycle.json
16:53:51.528 @objectstack/cli:build: src/commands/auth/login.ts(132,48): error TS2345: Argument of type '{ email:...

Fixed in commit 820ae34. The LoginRequest type (from z.infer<typeof LoginRequestSchema>) requires the type field — added type: 'email' to the client.auth.login() call in auth/login.ts.

@hotlong hotlong merged commit 83964b8 into main Apr 2, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl size/xs tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI: add remote API commands for Data API and Metadata API

5 participants