ardent-cli-beta 0.0.1 → 0.0.3
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 +11 -3
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +46 -0
- package/dist/auth/credentials.d.ts +6 -0
- package/dist/auth/credentials.js +44 -0
- package/dist/auth/token.d.ts +5 -0
- package/dist/auth/token.js +17 -0
- package/dist/commands/login.d.ts +13 -0
- package/dist/commands/login.js +22 -0
- package/dist/commands/logout.d.ts +8 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.js +19 -0
- package/dist/help.d.ts +6 -0
- package/dist/help.js +52 -0
- package/oclif.manifest.json +99 -2
- package/package.json +7 -7
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
|
|
8
|
-
|
|
9
|
-
|
|
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,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,13 @@
|
|
|
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 helpGroup: string;
|
|
6
|
+
static flags: {
|
|
7
|
+
token: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<{
|
|
10
|
+
authenticated: true;
|
|
11
|
+
organizations: Organization[];
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 to Ardent';
|
|
6
|
+
static helpGroup = 'Authentication';
|
|
7
|
+
static flags = {
|
|
8
|
+
token: Flags.string({ description: 'Existing Ardent API token', env: 'ARDENT_TOKEN', required: true }),
|
|
9
|
+
};
|
|
10
|
+
async run() {
|
|
11
|
+
const { flags } = await this.parse(Login);
|
|
12
|
+
const organizations = await loginWithToken(this.config.configDir, flags.token);
|
|
13
|
+
if (!this.jsonEnabled()) {
|
|
14
|
+
this.log('✓ Logged in to Ardent');
|
|
15
|
+
if (organizations.length === 1)
|
|
16
|
+
this.log(` Organization: ${organizations[0].name} (${organizations[0].id})`);
|
|
17
|
+
else
|
|
18
|
+
this.log(` Organizations: ${organizations.length}`);
|
|
19
|
+
}
|
|
20
|
+
return { authenticated: true, organizations };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { clearSession } from '../auth/credentials.js';
|
|
2
|
+
import { ArdentCommand } from '../command.js';
|
|
3
|
+
export default class Logout extends ArdentCommand {
|
|
4
|
+
static description = 'Log out of Ardent on this device';
|
|
5
|
+
static helpGroup = 'Authentication';
|
|
6
|
+
async run() {
|
|
7
|
+
await clearSession(this.config.configDir);
|
|
8
|
+
if (!this.jsonEnabled())
|
|
9
|
+
this.log('✓ Logged out of Ardent');
|
|
10
|
+
return { authenticated: false };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadSession } from '../auth/credentials.js';
|
|
2
|
+
import { ArdentCommand } from '../command.js';
|
|
3
|
+
export default class Status extends ArdentCommand {
|
|
4
|
+
static description = 'Show your Ardent login status';
|
|
5
|
+
static helpGroup = 'Authentication';
|
|
6
|
+
async run() {
|
|
7
|
+
const session = await loadSession(this.config.configDir);
|
|
8
|
+
const result = { authenticated: session !== undefined };
|
|
9
|
+
if (!this.jsonEnabled()) {
|
|
10
|
+
if (session === undefined) {
|
|
11
|
+
this.log('Not logged in to Ardent');
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
this.log('✓ Logged in to Ardent');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/help.d.ts
ADDED
package/dist/help.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ux } from '@oclif/core';
|
|
2
|
+
import { getHelpFlagAdditions, Help } from '@oclif/core/help';
|
|
3
|
+
import { loadSession } from './auth/credentials.js';
|
|
4
|
+
export default class ArdentHelp extends Help {
|
|
5
|
+
async showHelp(arguments_) {
|
|
6
|
+
const rootHelp = arguments_.every((argument) => getHelpFlagAdditions(this.config).includes(argument));
|
|
7
|
+
if (!rootHelp)
|
|
8
|
+
return super.showHelp(arguments_);
|
|
9
|
+
const authenticated = await this.authenticated();
|
|
10
|
+
if (arguments_.length > 0 || authenticated) {
|
|
11
|
+
this.showArdentHelp(authenticated);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
this.log('Get started:');
|
|
15
|
+
this.log('');
|
|
16
|
+
this.log(ux.colorize('green', ' ardent login Standard CLI login'));
|
|
17
|
+
this.log(' ardent login --token <token> Non-interactive login (CI and automation)');
|
|
18
|
+
this.log('');
|
|
19
|
+
this.log('For all commands:');
|
|
20
|
+
this.log('');
|
|
21
|
+
this.log(' ardent --help');
|
|
22
|
+
}
|
|
23
|
+
async authenticated() {
|
|
24
|
+
try {
|
|
25
|
+
return (await loadSession(this.config.configDir)) !== undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
showArdentHelp(authenticated) {
|
|
32
|
+
this.log(`Ardent CLI ${ux.colorize('dim', `v${this.config.version}`)}`);
|
|
33
|
+
this.log('');
|
|
34
|
+
this.log(this.section('USAGE', '$ ardent <command>'));
|
|
35
|
+
const groups = new Map();
|
|
36
|
+
for (const command of this.sortedCommands) {
|
|
37
|
+
if (command.hidden || command.pluginName !== this.config.name || typeof command.helpGroup !== 'string')
|
|
38
|
+
continue;
|
|
39
|
+
const commands = groups.get(command.helpGroup) ?? [];
|
|
40
|
+
commands.push(command);
|
|
41
|
+
groups.set(command.helpGroup, commands);
|
|
42
|
+
}
|
|
43
|
+
for (const [group, commands] of groups) {
|
|
44
|
+
const rows = commands.map((command) => {
|
|
45
|
+
const name = `ardent ${command.id.replaceAll(':', ' ')}`;
|
|
46
|
+
return [!authenticated && command.id === 'login' ? ux.colorize('green', name) : name, this.summary(command)];
|
|
47
|
+
});
|
|
48
|
+
this.log('');
|
|
49
|
+
this.log(this.section(group.toUpperCase(), rows));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1,4 +1,101 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commands": {
|
|
3
|
-
|
|
2
|
+
"commands": {
|
|
3
|
+
"login": {
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"args": {},
|
|
6
|
+
"description": "Log in to Ardent",
|
|
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
|
+
"helpGroup": "Authentication",
|
|
34
|
+
"isESM": true,
|
|
35
|
+
"relativePath": [
|
|
36
|
+
"dist",
|
|
37
|
+
"commands",
|
|
38
|
+
"login.js"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"logout": {
|
|
42
|
+
"aliases": [],
|
|
43
|
+
"args": {},
|
|
44
|
+
"description": "Log out of Ardent on this device",
|
|
45
|
+
"flags": {
|
|
46
|
+
"json": {
|
|
47
|
+
"description": "Format output as json.",
|
|
48
|
+
"helpGroup": "GLOBAL",
|
|
49
|
+
"name": "json",
|
|
50
|
+
"allowNo": false,
|
|
51
|
+
"type": "boolean"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"hasDynamicHelp": false,
|
|
55
|
+
"hiddenAliases": [],
|
|
56
|
+
"id": "logout",
|
|
57
|
+
"pluginAlias": "ardent-cli-beta",
|
|
58
|
+
"pluginName": "ardent-cli-beta",
|
|
59
|
+
"pluginType": "core",
|
|
60
|
+
"strict": true,
|
|
61
|
+
"enableJsonFlag": true,
|
|
62
|
+
"helpGroup": "Authentication",
|
|
63
|
+
"isESM": true,
|
|
64
|
+
"relativePath": [
|
|
65
|
+
"dist",
|
|
66
|
+
"commands",
|
|
67
|
+
"logout.js"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"status": {
|
|
71
|
+
"aliases": [],
|
|
72
|
+
"args": {},
|
|
73
|
+
"description": "Show your Ardent login status",
|
|
74
|
+
"flags": {
|
|
75
|
+
"json": {
|
|
76
|
+
"description": "Format output as json.",
|
|
77
|
+
"helpGroup": "GLOBAL",
|
|
78
|
+
"name": "json",
|
|
79
|
+
"allowNo": false,
|
|
80
|
+
"type": "boolean"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"hasDynamicHelp": false,
|
|
84
|
+
"hiddenAliases": [],
|
|
85
|
+
"id": "status",
|
|
86
|
+
"pluginAlias": "ardent-cli-beta",
|
|
87
|
+
"pluginName": "ardent-cli-beta",
|
|
88
|
+
"pluginType": "core",
|
|
89
|
+
"strict": true,
|
|
90
|
+
"enableJsonFlag": true,
|
|
91
|
+
"helpGroup": "Authentication",
|
|
92
|
+
"isESM": true,
|
|
93
|
+
"relativePath": [
|
|
94
|
+
"dist",
|
|
95
|
+
"commands",
|
|
96
|
+
"status.js"
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"version": "0.0.3"
|
|
4
101
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ardent-cli-beta",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "The Ardent command-line interface",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -25,23 +25,23 @@
|
|
|
25
25
|
"bin": "ardent",
|
|
26
26
|
"dirname": "ardent",
|
|
27
27
|
"commands": "./dist/commands",
|
|
28
|
+
"helpClass": "./dist/help",
|
|
28
29
|
"plugins": [
|
|
29
|
-
"@oclif/plugin-autocomplete"
|
|
30
|
-
"@oclif/plugin-help"
|
|
30
|
+
"@oclif/plugin-autocomplete"
|
|
31
31
|
],
|
|
32
32
|
"topicSeparator": " "
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"build": "
|
|
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
|
|
39
|
+
"test": "npm run build && npm run manifest && node --test \"test/**/*.test.js\"",
|
|
39
40
|
"typecheck": "tsc --noEmit"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@oclif/core": "^5.0.0",
|
|
43
|
-
"@oclif/plugin-autocomplete": "^4.0.0"
|
|
44
|
-
"@oclif/plugin-help": "^7.0.0"
|
|
44
|
+
"@oclif/plugin-autocomplete": "^4.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.0.0",
|