Skip to content

Commit 4a4652b

Browse files
Claudehotlong
andauthored
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>
1 parent 4a2756a commit 4a4652b

16 files changed

Lines changed: 1416 additions & 0 deletions

File tree

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
},
4141
"dependencies": {
4242
"@ai-sdk/gateway": "^3.0.84",
43+
"@objectstack/client": "workspace:*",
4344
"@objectstack/core": "workspace:*",
4445
"@objectstack/driver-memory": "workspace:^",
4546
"@objectstack/objectql": "workspace:^",
@@ -54,6 +55,7 @@
5455
"chalk": "^5.6.2",
5556
"dotenv-flow": "^4.1.0",
5657
"tsx": "^4.21.0",
58+
"yaml": "^2.4.1",
5759
"zod": "^4.3.6"
5860
},
5961
"peerDependencies": {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { Args, Command, Flags } from '@oclif/core';
4+
import { printHeader, printSuccess, printError, printKV } from '../../utils/format.js';
5+
import { writeAuthConfig } from '../../utils/auth-config.js';
6+
import { ObjectStackClient } from '@objectstack/client';
7+
import * as readline from 'node:readline/promises';
8+
import { stdin as input, stdout as output } from 'node:process';
9+
10+
export default class AuthLogin extends Command {
11+
static override description = 'Authenticate and store session credentials';
12+
13+
static override examples = [
14+
'$ os auth login',
15+
'$ os auth login --url https://api.example.com',
16+
'$ os auth login --email user@example.com --password mypassword',
17+
];
18+
19+
static override flags = {
20+
url: Flags.string({
21+
char: 'u',
22+
description: 'Server URL',
23+
default: 'http://localhost:3000',
24+
env: 'OBJECTSTACK_URL',
25+
}),
26+
email: Flags.string({
27+
char: 'e',
28+
description: 'Email address',
29+
}),
30+
password: Flags.string({
31+
char: 'p',
32+
description: 'Password',
33+
}),
34+
json: Flags.boolean({
35+
description: 'Output as JSON',
36+
}),
37+
};
38+
39+
async run(): Promise<void> {
40+
const { flags } = await this.parse(AuthLogin);
41+
42+
try {
43+
if (!flags.json) {
44+
printHeader('ObjectStack Login');
45+
printKV('Server', flags.url);
46+
console.log('');
47+
}
48+
49+
// Prompt for credentials if not provided
50+
let email = flags.email;
51+
let password = flags.password;
52+
53+
if (!email || !password) {
54+
const rl = readline.createInterface({ input, output });
55+
56+
if (!email) {
57+
email = await rl.question('Email: ');
58+
}
59+
60+
if (!password) {
61+
// Note: This doesn't hide the password input in the terminal
62+
// For production use, consider using a library like 'inquirer' or 'prompts'
63+
password = await rl.question('Password: ');
64+
}
65+
66+
rl.close();
67+
}
68+
69+
if (!email || !password) {
70+
throw new Error('Email and password are required');
71+
}
72+
73+
// Create client and authenticate
74+
const client = new ObjectStackClient({
75+
baseUrl: flags.url,
76+
});
77+
78+
const response = await client.auth.login({
79+
email,
80+
password,
81+
});
82+
83+
// Check if login was successful
84+
if (!response.data?.token && !response.data?.user) {
85+
throw new Error('Login failed: Invalid response from server');
86+
}
87+
88+
// Extract token - it might be in different locations depending on the auth system
89+
const token = response.data?.token || (response as any).token;
90+
const user = response.data?.user;
91+
92+
if (!token) {
93+
throw new Error('Login failed: No token received from server');
94+
}
95+
96+
// Store credentials
97+
await writeAuthConfig({
98+
url: flags.url,
99+
token,
100+
email: user?.email || email,
101+
userId: user?.id,
102+
createdAt: new Date().toISOString(),
103+
});
104+
105+
if (flags.json) {
106+
console.log(JSON.stringify({
107+
success: true,
108+
email: user?.email || email,
109+
userId: user?.id,
110+
}, null, 2));
111+
} else {
112+
printSuccess('Authentication successful');
113+
printKV('Email', user?.email || email);
114+
if (user?.id) {
115+
printKV('User ID', user.id);
116+
}
117+
console.log('');
118+
console.log(' Credentials stored in ~/.objectstack/credentials.json');
119+
console.log('');
120+
}
121+
} catch (error: any) {
122+
if (flags.json) {
123+
console.log(JSON.stringify({
124+
success: false,
125+
error: error.message,
126+
}, null, 2));
127+
this.exit(1);
128+
}
129+
printError(error.message || String(error));
130+
this.exit(1);
131+
}
132+
}
133+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { Command, Flags } from '@oclif/core';
4+
import { printHeader, printSuccess, printError } from '../../utils/format.js';
5+
import { deleteAuthConfig } from '../../utils/auth-config.js';
6+
7+
export default class AuthLogout extends Command {
8+
static override description = 'Clear stored authentication credentials';
9+
10+
static override examples = [
11+
'$ os auth logout',
12+
];
13+
14+
static override flags = {
15+
json: Flags.boolean({
16+
description: 'Output as JSON',
17+
}),
18+
};
19+
20+
async run(): Promise<void> {
21+
const { flags } = await this.parse(AuthLogout);
22+
23+
try {
24+
if (!flags.json) {
25+
printHeader('ObjectStack Logout');
26+
}
27+
28+
await deleteAuthConfig();
29+
30+
if (flags.json) {
31+
console.log(JSON.stringify({
32+
success: true,
33+
message: 'Credentials cleared',
34+
}, null, 2));
35+
} else {
36+
printSuccess('Credentials cleared');
37+
console.log('');
38+
}
39+
} catch (error: any) {
40+
if (flags.json) {
41+
console.log(JSON.stringify({
42+
success: false,
43+
error: error.message,
44+
}, null, 2));
45+
this.exit(1);
46+
}
47+
printError(error.message || String(error));
48+
this.exit(1);
49+
}
50+
}
51+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { Command, Flags } from '@oclif/core';
4+
import { printHeader, printError, printKV } from '../../utils/format.js';
5+
import { createApiClient, requireAuth } from '../../utils/api-client.js';
6+
import { formatOutput } from '../../utils/output-formatter.js';
7+
8+
export default class AuthWhoami extends Command {
9+
static override description = 'Show current session information';
10+
11+
static override examples = [
12+
'$ os auth whoami',
13+
'$ os auth whoami --json',
14+
'$ os auth whoami --url https://api.example.com --token <token>',
15+
];
16+
17+
static override flags = {
18+
url: Flags.string({
19+
char: 'u',
20+
description: 'Server URL',
21+
env: 'OBJECTSTACK_URL',
22+
}),
23+
token: Flags.string({
24+
char: 't',
25+
description: 'Authentication token',
26+
env: 'OBJECTSTACK_TOKEN',
27+
}),
28+
format: Flags.string({
29+
char: 'f',
30+
description: 'Output format',
31+
options: ['json', 'table', 'yaml'],
32+
default: 'table',
33+
}),
34+
};
35+
36+
async run(): Promise<void> {
37+
const { flags } = await this.parse(AuthWhoami);
38+
39+
try {
40+
const client = await createApiClient({
41+
url: flags.url,
42+
token: flags.token,
43+
});
44+
45+
// Check if we have a token
46+
requireAuth((client as any).token);
47+
48+
// Get current session info
49+
const response = await client.auth.me();
50+
51+
const sessionData = response.data || response;
52+
53+
if (flags.format === 'json') {
54+
formatOutput(sessionData, 'json');
55+
} else if (flags.format === 'yaml') {
56+
formatOutput(sessionData, 'yaml');
57+
} else {
58+
printHeader('Current Session');
59+
60+
if (sessionData.user) {
61+
printKV('User ID', sessionData.user.id || '-');
62+
printKV('Email', sessionData.user.email || '-');
63+
printKV('Name', sessionData.user.name || '-');
64+
}
65+
66+
if (sessionData.session) {
67+
printKV('Session ID', sessionData.session.id || '-');
68+
printKV('Expires At', sessionData.session.expiresAt || '-');
69+
}
70+
71+
console.log('');
72+
}
73+
} catch (error: any) {
74+
if (flags.format === 'json') {
75+
console.log(JSON.stringify({
76+
success: false,
77+
error: error.message,
78+
}, null, 2));
79+
this.exit(1);
80+
}
81+
printError(error.message || String(error));
82+
this.exit(1);
83+
}
84+
}
85+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { Args, Command, Flags } from '@oclif/core';
4+
import { printHeader, printError, printSuccess } from '../../utils/format.js';
5+
import { createApiClient, requireAuth } from '../../utils/api-client.js';
6+
import { formatOutput } from '../../utils/output-formatter.js';
7+
8+
export default class DataCreate extends Command {
9+
static override description = 'Create a new record';
10+
11+
static override examples = [
12+
'$ os data create project_task \'{"name":"New Task","status":"open"}\'',
13+
'$ os data create project_task --data task-data.json',
14+
'$ os data create project_task --data task-data.json --format json',
15+
];
16+
17+
static override args = {
18+
object: Args.string({
19+
description: 'Object name (snake_case)',
20+
required: true,
21+
}),
22+
data: Args.string({
23+
description: 'Record data as JSON string (or use --data flag for file)',
24+
}),
25+
};
26+
27+
static override flags = {
28+
url: Flags.string({
29+
char: 'u',
30+
description: 'Server URL',
31+
env: 'OBJECTSTACK_URL',
32+
}),
33+
token: Flags.string({
34+
char: 't',
35+
description: 'Authentication token',
36+
env: 'OBJECTSTACK_TOKEN',
37+
}),
38+
data: Flags.string({
39+
char: 'd',
40+
description: 'Path to JSON file containing record data',
41+
}),
42+
format: Flags.string({
43+
char: 'f',
44+
description: 'Output format',
45+
options: ['json', 'table', 'yaml'],
46+
default: 'table',
47+
}),
48+
};
49+
50+
async run(): Promise<void> {
51+
const { args, flags } = await this.parse(DataCreate);
52+
53+
try {
54+
const client = await createApiClient({
55+
url: flags.url,
56+
token: flags.token,
57+
});
58+
59+
requireAuth((client as any).token);
60+
61+
// Parse record data
62+
let recordData: any;
63+
64+
if (flags.data) {
65+
// Read from file
66+
const { readFile } = await import('node:fs/promises');
67+
const fileContent = await readFile(flags.data, 'utf-8');
68+
try {
69+
recordData = JSON.parse(fileContent);
70+
} catch (e) {
71+
throw new Error(`Invalid JSON in file: ${(e as Error).message}`);
72+
}
73+
} else if (args.data) {
74+
// Parse from argument
75+
try {
76+
recordData = JSON.parse(args.data);
77+
} catch (e) {
78+
throw new Error(`Invalid JSON: ${(e as Error).message}`);
79+
}
80+
} else {
81+
throw new Error('Record data is required (provide JSON string or use --data flag)');
82+
}
83+
84+
// Create the record
85+
const result = await client.data.create(args.object, recordData);
86+
87+
if (flags.format === 'json') {
88+
formatOutput(result, 'json');
89+
} else if (flags.format === 'yaml') {
90+
formatOutput(result, 'yaml');
91+
} else {
92+
printSuccess(`Record created: ${result.id}`);
93+
if (result.record) {
94+
console.log('');
95+
formatOutput(result.record, 'table');
96+
}
97+
}
98+
} catch (error: any) {
99+
if (flags.format === 'json') {
100+
console.log(JSON.stringify({
101+
success: false,
102+
error: error.message,
103+
}, null, 2));
104+
this.exit(1);
105+
}
106+
printError(error.message || String(error));
107+
this.exit(1);
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)