ardent-cli-beta 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,14 +4,22 @@
4
4
 
5
5
  The Ardent CLI is the customer command-line client for the control-plane API.
6
6
 
7
- The Oclif foundation is implemented, including help, version, autocomplete, and
8
- native JSON command behavior. Ardent product commands, authentication, local
9
- context, and API requests are not implemented yet.
7
+ The Oclif foundation and token authentication are implemented. `ardent login
8
+ --token <token>` validates and stores an API token; `ARDENT_TOKEN` supplies the
9
+ same flag non-interactively. `ardent status` and `ardent logout` inspect and
10
+ remove that local session. The CLI always talks to Ardent's production control
11
+ plane. Oclif provides help, version, autocomplete, and native JSON command
12
+ behavior. Resource commands and resource context are not implemented yet.
13
+
14
+ Set `ARDENT_API_URL` to point the CLI at a development control plane. Without
15
+ it, the CLI uses `https://production.tryardent.com`.
10
16
 
11
17
  ```bash
12
18
  npm ci
13
19
  npm run build
14
20
  ./bin/run.js --help
21
+ ./bin/run.js login --token <token>
22
+ ARDENT_API_URL=http://localhost:8080 ./bin/run.js login --token <token>
15
23
  ```
16
24
 
17
25
  Before changing this repository, read [ARDENT_CLI_DEVELOPMENT.md](ARDENT_CLI_DEVELOPMENT.md).
@@ -0,0 +1,7 @@
1
+ export declare const API_URL: string;
2
+ export declare class APIError extends Error {
3
+ readonly status: number;
4
+ readonly code?: string | undefined;
5
+ constructor(message: string, status: number, code?: string | undefined);
6
+ }
7
+ export declare function request<T>(token: string, path: string, init?: RequestInit): Promise<T>;
@@ -0,0 +1,46 @@
1
+ const DEFAULT_TIMEOUT_MS = 10_000;
2
+ export const API_URL = process.env.ARDENT_API_URL || 'https://production.tryardent.com';
3
+ export class APIError extends Error {
4
+ status;
5
+ code;
6
+ constructor(message, status, code) {
7
+ super(message);
8
+ this.status = status;
9
+ this.code = code;
10
+ }
11
+ }
12
+ export async function request(token, path, init = {}) {
13
+ let response;
14
+ try {
15
+ response = await fetch(new URL(path, `${API_URL}/`), {
16
+ ...init,
17
+ headers: {
18
+ Accept: 'application/json',
19
+ Authorization: `Bearer ${token}`,
20
+ ...(init.body === undefined ? {} : { 'Content-Type': 'application/json' }),
21
+ ...init.headers,
22
+ },
23
+ signal: init.signal ?? AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
24
+ });
25
+ }
26
+ catch (error) {
27
+ throw new Error(`Could not reach the Ardent API: ${error instanceof Error ? error.message : String(error)}`);
28
+ }
29
+ const text = await response.text();
30
+ if (!response.ok) {
31
+ let detail = {};
32
+ try {
33
+ detail = JSON.parse(text);
34
+ }
35
+ catch {
36
+ // Plain-text errors still receive the status-based fallback below.
37
+ }
38
+ throw new APIError(detail.message ?? `Ardent API request failed with HTTP ${response.status}.`, response.status, detail.error);
39
+ }
40
+ try {
41
+ return JSON.parse(text);
42
+ }
43
+ catch {
44
+ throw new Error('Ardent API returned an invalid JSON response.');
45
+ }
46
+ }
@@ -0,0 +1,6 @@
1
+ export interface Session {
2
+ token: string;
3
+ }
4
+ export declare function loadSession(directory: string): Promise<Session | undefined>;
5
+ export declare function saveSession(directory: string, session: Session): Promise<void>;
6
+ export declare function clearSession(directory: string): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ const configPath = (directory) => join(directory, 'config.json');
5
+ export async function loadSession(directory) {
6
+ let contents;
7
+ try {
8
+ contents = await readFile(configPath(directory), 'utf8');
9
+ }
10
+ catch (error) {
11
+ if (error.code === 'ENOENT')
12
+ return undefined;
13
+ throw error;
14
+ }
15
+ try {
16
+ const session = JSON.parse(contents);
17
+ if (typeof session.token !== 'string' || session.token.length === 0)
18
+ throw new Error();
19
+ return { token: session.token };
20
+ }
21
+ catch {
22
+ throw new Error(`Ardent configuration at ${configPath(directory)} is invalid. Run ardent logout, then log in again.`);
23
+ }
24
+ }
25
+ export async function saveSession(directory, session) {
26
+ await mkdir(directory, { mode: 0o700, recursive: true });
27
+ await chmod(directory, 0o700);
28
+ const destination = configPath(directory);
29
+ const temporary = `${destination}.${process.pid}.${randomUUID()}.tmp`;
30
+ try {
31
+ await writeFile(temporary, `${JSON.stringify(session, undefined, 2)}\n`, { flag: 'wx', mode: 0o600 });
32
+ await rename(temporary, destination);
33
+ }
34
+ catch (error) {
35
+ await unlink(temporary).catch(() => { });
36
+ throw error;
37
+ }
38
+ }
39
+ export async function clearSession(directory) {
40
+ await unlink(configPath(directory)).catch((error) => {
41
+ if (error.code !== 'ENOENT')
42
+ throw error;
43
+ });
44
+ }
@@ -0,0 +1,5 @@
1
+ export interface Organization {
2
+ id: string;
3
+ name: string;
4
+ }
5
+ export declare function loginWithToken(directory: string, value: string): Promise<Organization[]>;
@@ -0,0 +1,17 @@
1
+ import { request } from '../api/client.js';
2
+ import { saveSession } from './credentials.js';
3
+ export async function loginWithToken(directory, value) {
4
+ const token = value.trim();
5
+ if (!token)
6
+ throw new Error('Ardent API token cannot be empty.');
7
+ const organizations = await request(token, '/organizations');
8
+ if (!Array.isArray(organizations) ||
9
+ organizations.some((organization) => typeof organization !== 'object' ||
10
+ organization === null ||
11
+ typeof organization.id !== 'string' ||
12
+ typeof organization.name !== 'string')) {
13
+ throw new Error('Ardent API returned an invalid organization response.');
14
+ }
15
+ await saveSession(directory, { token });
16
+ return organizations;
17
+ }
@@ -0,0 +1,12 @@
1
+ import { type Organization } from '../auth/token.js';
2
+ import { ArdentCommand } from '../command.js';
3
+ export default class Login extends ArdentCommand {
4
+ static description: string;
5
+ static flags: {
6
+ token: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<{
9
+ authenticated: true;
10
+ organizations: Organization[];
11
+ }>;
12
+ }
@@ -0,0 +1,21 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { loginWithToken } from '../auth/token.js';
3
+ import { ArdentCommand } from '../command.js';
4
+ export default class Login extends ArdentCommand {
5
+ static description = 'Log in with an existing Ardent token';
6
+ static flags = {
7
+ token: Flags.string({ description: 'Existing Ardent API token', env: 'ARDENT_TOKEN', required: true }),
8
+ };
9
+ async run() {
10
+ const { flags } = await this.parse(Login);
11
+ const organizations = await loginWithToken(this.config.configDir, flags.token);
12
+ if (!this.jsonEnabled()) {
13
+ this.log('✓ Logged in to Ardent');
14
+ if (organizations.length === 1)
15
+ this.log(` Organization: ${organizations[0].name} (${organizations[0].id})`);
16
+ else
17
+ this.log(` Organizations: ${organizations.length}`);
18
+ }
19
+ return { authenticated: true, organizations };
20
+ }
21
+ }
@@ -0,0 +1,7 @@
1
+ import { ArdentCommand } from '../command.js';
2
+ export default class Logout extends ArdentCommand {
3
+ static description: string;
4
+ run(): Promise<{
5
+ authenticated: false;
6
+ }>;
7
+ }
@@ -0,0 +1,11 @@
1
+ import { clearSession } from '../auth/credentials.js';
2
+ import { ArdentCommand } from '../command.js';
3
+ export default class Logout extends ArdentCommand {
4
+ static description = 'Remove the locally stored Ardent token';
5
+ async run() {
6
+ await clearSession(this.config.configDir);
7
+ if (!this.jsonEnabled())
8
+ this.log('✓ Logged out of Ardent');
9
+ return { authenticated: false };
10
+ }
11
+ }
@@ -0,0 +1,7 @@
1
+ import { ArdentCommand } from '../command.js';
2
+ export default class Status extends ArdentCommand {
3
+ static description: string;
4
+ run(): Promise<{
5
+ authenticated: boolean;
6
+ }>;
7
+ }
@@ -0,0 +1,18 @@
1
+ import { loadSession } from '../auth/credentials.js';
2
+ import { ArdentCommand } from '../command.js';
3
+ export default class Status extends ArdentCommand {
4
+ static description = 'Show the local Ardent login status';
5
+ async run() {
6
+ const session = await loadSession(this.config.configDir);
7
+ const result = { authenticated: session !== undefined };
8
+ if (!this.jsonEnabled()) {
9
+ if (session === undefined) {
10
+ this.log('Not logged in to Ardent');
11
+ }
12
+ else {
13
+ this.log('✓ Logged in to Ardent');
14
+ }
15
+ }
16
+ return result;
17
+ }
18
+ }
@@ -1,4 +1,98 @@
1
1
  {
2
- "commands": {},
3
- "version": "0.0.1"
2
+ "commands": {
3
+ "login": {
4
+ "aliases": [],
5
+ "args": {},
6
+ "description": "Log in with an existing Ardent token",
7
+ "flags": {
8
+ "json": {
9
+ "description": "Format output as json.",
10
+ "helpGroup": "GLOBAL",
11
+ "name": "json",
12
+ "allowNo": false,
13
+ "type": "boolean"
14
+ },
15
+ "token": {
16
+ "description": "Existing Ardent API token",
17
+ "env": "ARDENT_TOKEN",
18
+ "name": "token",
19
+ "required": true,
20
+ "hasDynamicHelp": false,
21
+ "multiple": false,
22
+ "type": "option"
23
+ }
24
+ },
25
+ "hasDynamicHelp": false,
26
+ "hiddenAliases": [],
27
+ "id": "login",
28
+ "pluginAlias": "ardent-cli-beta",
29
+ "pluginName": "ardent-cli-beta",
30
+ "pluginType": "core",
31
+ "strict": true,
32
+ "enableJsonFlag": true,
33
+ "isESM": true,
34
+ "relativePath": [
35
+ "dist",
36
+ "commands",
37
+ "login.js"
38
+ ]
39
+ },
40
+ "logout": {
41
+ "aliases": [],
42
+ "args": {},
43
+ "description": "Remove the locally stored Ardent token",
44
+ "flags": {
45
+ "json": {
46
+ "description": "Format output as json.",
47
+ "helpGroup": "GLOBAL",
48
+ "name": "json",
49
+ "allowNo": false,
50
+ "type": "boolean"
51
+ }
52
+ },
53
+ "hasDynamicHelp": false,
54
+ "hiddenAliases": [],
55
+ "id": "logout",
56
+ "pluginAlias": "ardent-cli-beta",
57
+ "pluginName": "ardent-cli-beta",
58
+ "pluginType": "core",
59
+ "strict": true,
60
+ "enableJsonFlag": true,
61
+ "isESM": true,
62
+ "relativePath": [
63
+ "dist",
64
+ "commands",
65
+ "logout.js"
66
+ ]
67
+ },
68
+ "status": {
69
+ "aliases": [],
70
+ "args": {},
71
+ "description": "Show the local Ardent login status",
72
+ "flags": {
73
+ "json": {
74
+ "description": "Format output as json.",
75
+ "helpGroup": "GLOBAL",
76
+ "name": "json",
77
+ "allowNo": false,
78
+ "type": "boolean"
79
+ }
80
+ },
81
+ "hasDynamicHelp": false,
82
+ "hiddenAliases": [],
83
+ "id": "status",
84
+ "pluginAlias": "ardent-cli-beta",
85
+ "pluginName": "ardent-cli-beta",
86
+ "pluginType": "core",
87
+ "strict": true,
88
+ "enableJsonFlag": true,
89
+ "isESM": true,
90
+ "relativePath": [
91
+ "dist",
92
+ "commands",
93
+ "status.js"
94
+ ]
95
+ }
96
+ },
97
+ "version": "0.0.2"
4
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli-beta",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "The Ardent command-line interface",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -32,10 +32,11 @@
32
32
  "topicSeparator": " "
33
33
  },
34
34
  "scripts": {
35
- "build": "tsc -b --clean && tsc -b",
35
+ "build": "npm run clean && tsc -b",
36
+ "clean": "node -e \"const {rmSync} = require('node:fs'); for (const path of ['dist', 'tsconfig.tsbuildinfo']) rmSync(path, {force: true, recursive: true})\"",
36
37
  "manifest": "oclif manifest",
37
38
  "prepack": "npm run build && npm run manifest",
38
- "test": "npm run build && node --test test/*.test.js",
39
+ "test": "npm run build && npm run manifest && node --test \"test/**/*.test.js\"",
39
40
  "typecheck": "tsc --noEmit"
40
41
  },
41
42
  "dependencies": {