ardent-cli-beta 0.0.7 → 0.0.8

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
@@ -11,7 +11,8 @@ API token for CI and automation; `ARDENT_TOKEN` supplies the same flag
11
11
  non-interactively. `ardent status` and `ardent logout` inspect and remove that
12
12
  local session. The CLI always talks to Ardent's production control plane. Oclif
13
13
  provides help, version, autocomplete, and native JSON command behavior. Resource
14
- commands and resource context are not implemented yet.
14
+ context is retained from login. Project listing, creation, and local context
15
+ switching are implemented; other resource commands are not implemented yet.
15
16
 
16
17
  Set `ARDENT_API_URL` to point the CLI at a development control plane. Without
17
18
  it, the CLI uses `https://production.tryardent.com`.
@@ -22,6 +23,9 @@ npm run build
22
23
  ./bin/run.js --help
23
24
  ./bin/run.js login
24
25
  ./bin/run.js login --token <token>
26
+ ./bin/run.js project list
27
+ ./bin/run.js project create "My project"
28
+ ./bin/run.js project switch "My project"
25
29
  ARDENT_API_URL=http://localhost:8080 ./bin/run.js login --token <token>
26
30
  ```
27
31
 
@@ -1,6 +1,16 @@
1
1
  export interface Session {
2
+ organization?: {
3
+ id: string;
4
+ name: string;
5
+ };
6
+ projects?: ProjectIdentity[];
7
+ selectedProject?: ProjectIdentity;
2
8
  token: string;
3
9
  }
10
+ export interface ProjectIdentity {
11
+ id: string;
12
+ name: string;
13
+ }
4
14
  export declare function loadSession(directory: string): Promise<Session | undefined>;
5
15
  export declare function saveSession(directory: string, session: Session): Promise<void>;
6
16
  export declare function clearSession(directory: string): Promise<void>;
@@ -16,12 +16,54 @@ export async function loadSession(directory) {
16
16
  const session = JSON.parse(contents);
17
17
  if (typeof session.token !== 'string' || session.token.length === 0)
18
18
  throw new Error();
19
- return { token: session.token };
19
+ if (session.organization === undefined) {
20
+ if (session.projects !== undefined || session.selectedProject !== undefined)
21
+ throw new Error();
22
+ return { token: session.token };
23
+ }
24
+ if (typeof session.organization !== 'object' ||
25
+ session.organization === null ||
26
+ typeof session.organization.id !== 'string' ||
27
+ session.organization.id.length === 0 ||
28
+ typeof session.organization.name !== 'string' ||
29
+ session.organization.name.length === 0) {
30
+ throw new Error();
31
+ }
32
+ let projects;
33
+ if (session.projects !== undefined) {
34
+ if (!Array.isArray(session.projects) || session.projects.some((project) => !isProjectIdentity(project)))
35
+ throw new Error();
36
+ projects = session.projects.map(({ id, name }) => ({ id, name }));
37
+ if (new Set(projects.map(({ id }) => id)).size !== projects.length)
38
+ throw new Error();
39
+ if (new Set(projects.map(({ name }) => name)).size !== projects.length)
40
+ throw new Error();
41
+ }
42
+ let selectedProject;
43
+ if (session.selectedProject !== undefined) {
44
+ if (!isProjectIdentity(session.selectedProject) || projects === undefined)
45
+ throw new Error();
46
+ selectedProject = projects.find(({ id, name }) => id === session.selectedProject?.id && name === session.selectedProject.name);
47
+ if (!selectedProject)
48
+ throw new Error();
49
+ }
50
+ return {
51
+ organization: session.organization,
52
+ ...(projects === undefined ? {} : { projects }),
53
+ ...(selectedProject === undefined ? {} : { selectedProject }),
54
+ token: session.token,
55
+ };
20
56
  }
21
57
  catch {
22
58
  throw new Error(`Ardent configuration at ${configPath(directory)} is invalid. Run ardent logout, then log in again.`);
23
59
  }
24
60
  }
61
+ function isProjectIdentity(value) {
62
+ if (typeof value !== 'object' || value === null)
63
+ return false;
64
+ const project = value;
65
+ return typeof project.id === 'string' && project.id.length > 0 && typeof project.name === 'string' && project.name.length > 0;
66
+ }
25
67
  export async function saveSession(directory, session) {
26
68
  await mkdir(directory, { mode: 0o700, recursive: true });
27
69
  await chmod(directory, 0o700);
@@ -9,10 +9,14 @@ export async function loginWithToken(directory, value, signal) {
9
9
  organizations.some((organization) => typeof organization !== 'object' ||
10
10
  organization === null ||
11
11
  typeof organization.id !== 'string' ||
12
- typeof organization.name !== 'string')) {
12
+ organization.id.length === 0 ||
13
+ typeof organization.name !== 'string' ||
14
+ organization.name.length === 0)) {
13
15
  throw new Error('Ardent API returned an invalid organization response.');
14
16
  }
17
+ if (organizations.length !== 1)
18
+ throw new Error('Ardent API token must belong to exactly one organization.');
15
19
  signal?.throwIfAborted();
16
- await saveSession(directory, { token });
20
+ await saveSession(directory, { organization: organizations[0], token });
17
21
  return organizations;
18
22
  }
@@ -0,0 +1,10 @@
1
+ import { ArdentCommand } from '../../command.js';
2
+ import { type Project } from '../../projects/client.js';
3
+ export default class ProjectCreate extends ArdentCommand {
4
+ static args: {
5
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static helpGroup: string;
9
+ run(): Promise<Project>;
10
+ }
@@ -0,0 +1,29 @@
1
+ import { Args } from '@oclif/core';
2
+ import { loadSession } from '../../auth/credentials.js';
3
+ import { ArdentCommand } from '../../command.js';
4
+ import { createProject } from '../../projects/client.js';
5
+ import { selectProject } from '../../projects/context.js';
6
+ import { terminalText } from '../../terminal.js';
7
+ export default class ProjectCreate extends ArdentCommand {
8
+ static args = {
9
+ name: Args.string({ description: 'Project name', required: true }),
10
+ };
11
+ static description = 'Create a project';
12
+ static helpGroup = 'Projects';
13
+ async run() {
14
+ const { args } = await this.parse(ProjectCreate);
15
+ const name = args.name.trim();
16
+ if (!name)
17
+ this.error('Project name cannot be empty.');
18
+ const session = await loadSession(this.config.configDir);
19
+ if (!session)
20
+ this.error('Not logged in to Ardent. Run ardent login.');
21
+ if (!session.organization)
22
+ this.error('You must be logged in to run this command. Run ardent login.');
23
+ const project = await createProject(session.token, session.organization.id, name);
24
+ await selectProject(this.config.configDir, session, project);
25
+ if (!this.jsonEnabled())
26
+ this.log(`✓ Created project ${terminalText(project.name)}`);
27
+ return project;
28
+ }
29
+ }
@@ -0,0 +1,9 @@
1
+ import { ArdentCommand } from '../../command.js';
2
+ import { type Project } from '../../projects/client.js';
3
+ export default class ProjectList extends ArdentCommand {
4
+ static description: string;
5
+ static helpGroup: string;
6
+ run(): Promise<{
7
+ projects: Project[];
8
+ }>;
9
+ }
@@ -0,0 +1,29 @@
1
+ import { ux } from '@oclif/core';
2
+ import { loadSession } from '../../auth/credentials.js';
3
+ import { ArdentCommand } from '../../command.js';
4
+ import { listProjects } from '../../projects/client.js';
5
+ import { replaceProjectCache } from '../../projects/context.js';
6
+ import { terminalText } from '../../terminal.js';
7
+ export default class ProjectList extends ArdentCommand {
8
+ static description = 'List projects';
9
+ static helpGroup = 'Projects';
10
+ async run() {
11
+ const session = await loadSession(this.config.configDir);
12
+ if (!session)
13
+ this.error('Not logged in to Ardent. Run ardent login.');
14
+ if (!session.organization)
15
+ this.error('You must be logged in to run this command. Run ardent login.');
16
+ const projects = await listProjects(session.token, session.organization.id);
17
+ const refreshedSession = await replaceProjectCache(this.config.configDir, session, projects);
18
+ if (!this.jsonEnabled()) {
19
+ if (projects.length === 0)
20
+ this.log('No projects found.');
21
+ for (const project of projects) {
22
+ const name = terminalText(project.name);
23
+ const line = project.id === refreshedSession.selectedProject?.id ? `● ${name}` : ` ${name}`;
24
+ this.log(project.id === refreshedSession.selectedProject?.id ? ux.colorize('green', line) : line);
25
+ }
26
+ }
27
+ return { projects };
28
+ }
29
+ }
@@ -0,0 +1,12 @@
1
+ import { ArdentCommand } from '../../command.js';
2
+ export default class ProjectSwitch extends ArdentCommand {
3
+ static args: {
4
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static helpGroup: string;
8
+ run(): Promise<{
9
+ id: string;
10
+ name: string;
11
+ }>;
12
+ }
@@ -0,0 +1,28 @@
1
+ import { Args } from '@oclif/core';
2
+ import { loadSession } from '../../auth/credentials.js';
3
+ import { ArdentCommand } from '../../command.js';
4
+ import { cachedProjectNamed, selectProject } from '../../projects/context.js';
5
+ import { terminalText } from '../../terminal.js';
6
+ export default class ProjectSwitch extends ArdentCommand {
7
+ static args = {
8
+ name: Args.string({ description: 'Project name', required: true }),
9
+ };
10
+ static description = 'Switch to a project from the local project list';
11
+ static helpGroup = 'Projects';
12
+ async run() {
13
+ const { args } = await this.parse(ProjectSwitch);
14
+ const name = args.name.trim();
15
+ if (!name)
16
+ this.error('Project name cannot be empty.');
17
+ const session = await loadSession(this.config.configDir);
18
+ if (!session?.organization)
19
+ this.error('You must be logged in to run this command. Run ardent login.');
20
+ const project = cachedProjectNamed(session, name);
21
+ if (!project)
22
+ this.error(`Project ${JSON.stringify(name)} not found. Run ardent project list.`);
23
+ await selectProject(this.config.configDir, session, project);
24
+ if (!this.jsonEnabled())
25
+ this.log(`✓ Switched to project ${terminalText(project.name)}`);
26
+ return project;
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ export interface Project {
2
+ created_at: string;
3
+ id: string;
4
+ name: string;
5
+ organization_id: string;
6
+ updated_at: string;
7
+ }
8
+ export declare function listProjects(token: string, organizationID: string): Promise<Project[]>;
9
+ export declare function createProject(token: string, organizationID: string, name: string): Promise<Project>;
@@ -0,0 +1,30 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { request } from '../api/client.js';
3
+ export async function listProjects(token, organizationID) {
4
+ const projects = await request(token, `/projects?organization_id=${encodeURIComponent(organizationID)}`);
5
+ if (!Array.isArray(projects) || projects.some((project) => !isProject(project, organizationID))) {
6
+ throw new Error('Ardent API returned an invalid projects response.');
7
+ }
8
+ return projects;
9
+ }
10
+ export async function createProject(token, organizationID, name) {
11
+ const project = await request(token, '/projects', {
12
+ body: JSON.stringify({ name, organization_id: organizationID }),
13
+ headers: { 'Idempotency-Key': randomUUID() },
14
+ method: 'POST',
15
+ });
16
+ if (!isProject(project, organizationID))
17
+ throw new Error('Ardent API returned an invalid project response.');
18
+ return project;
19
+ }
20
+ function isProject(value, organizationID) {
21
+ if (typeof value !== 'object' || value === null)
22
+ return false;
23
+ const project = value;
24
+ return (typeof project.id === 'string' &&
25
+ project.id.length > 0 &&
26
+ project.organization_id === organizationID &&
27
+ typeof project.name === 'string' &&
28
+ typeof project.created_at === 'string' &&
29
+ typeof project.updated_at === 'string');
30
+ }
@@ -0,0 +1,5 @@
1
+ import { type ProjectIdentity, type Session } from '../auth/credentials.js';
2
+ import type { Project } from './client.js';
3
+ export declare function replaceProjectCache(directory: string, session: Session, projects: Project[]): Promise<Session>;
4
+ export declare function selectProject(directory: string, session: Session, project: ProjectIdentity): Promise<Session>;
5
+ export declare function cachedProjectNamed(session: Session, name: string): ProjectIdentity | undefined;
@@ -0,0 +1,29 @@
1
+ import { saveSession } from '../auth/credentials.js';
2
+ export async function replaceProjectCache(directory, session, projects) {
3
+ const identities = projects.map(projectIdentity);
4
+ const selectedProject = identities.find(({ id }) => id === session.selectedProject?.id) ??
5
+ (identities.length === 1 ? identities[0] : undefined);
6
+ const updated = { ...session, projects: identities, selectedProject };
7
+ if (selectedProject === undefined)
8
+ delete updated.selectedProject;
9
+ await saveSession(directory, updated);
10
+ return updated;
11
+ }
12
+ export async function selectProject(directory, session, project) {
13
+ const selectedProject = projectIdentity(project);
14
+ const projects = [...(session.projects ?? [])];
15
+ const existingIndex = projects.findIndex(({ id }) => id === selectedProject.id);
16
+ if (existingIndex === -1)
17
+ projects.push(selectedProject);
18
+ else
19
+ projects[existingIndex] = selectedProject;
20
+ const updated = { ...session, projects, selectedProject };
21
+ await saveSession(directory, updated);
22
+ return updated;
23
+ }
24
+ export function cachedProjectNamed(session, name) {
25
+ return session.projects?.find((project) => project.name === name);
26
+ }
27
+ function projectIdentity(project) {
28
+ return { id: project.id, name: project.name };
29
+ }
@@ -0,0 +1 @@
1
+ export declare function terminalText(value: string): string;
@@ -0,0 +1,9 @@
1
+ const terminalControlCharacter = /[\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/g;
2
+ export function terminalText(value) {
3
+ return value.replace(terminalControlCharacter, (character) => {
4
+ const codePoint = character.codePointAt(0);
5
+ if (codePoint === undefined)
6
+ return '';
7
+ return `\\u${codePoint.toString(16).padStart(4, '0')}`;
8
+ });
9
+ }
@@ -94,7 +94,109 @@
94
94
  "commands",
95
95
  "status.js"
96
96
  ]
97
+ },
98
+ "project:create": {
99
+ "aliases": [],
100
+ "args": {
101
+ "name": {
102
+ "description": "Project name",
103
+ "name": "name",
104
+ "required": true
105
+ }
106
+ },
107
+ "description": "Create a project",
108
+ "flags": {
109
+ "json": {
110
+ "description": "Format output as json.",
111
+ "helpGroup": "GLOBAL",
112
+ "name": "json",
113
+ "allowNo": false,
114
+ "type": "boolean"
115
+ }
116
+ },
117
+ "hasDynamicHelp": false,
118
+ "hiddenAliases": [],
119
+ "id": "project:create",
120
+ "pluginAlias": "ardent-cli-beta",
121
+ "pluginName": "ardent-cli-beta",
122
+ "pluginType": "core",
123
+ "strict": true,
124
+ "enableJsonFlag": true,
125
+ "helpGroup": "Projects",
126
+ "isESM": true,
127
+ "relativePath": [
128
+ "dist",
129
+ "commands",
130
+ "project",
131
+ "create.js"
132
+ ]
133
+ },
134
+ "project:list": {
135
+ "aliases": [],
136
+ "args": {},
137
+ "description": "List projects",
138
+ "flags": {
139
+ "json": {
140
+ "description": "Format output as json.",
141
+ "helpGroup": "GLOBAL",
142
+ "name": "json",
143
+ "allowNo": false,
144
+ "type": "boolean"
145
+ }
146
+ },
147
+ "hasDynamicHelp": false,
148
+ "hiddenAliases": [],
149
+ "id": "project:list",
150
+ "pluginAlias": "ardent-cli-beta",
151
+ "pluginName": "ardent-cli-beta",
152
+ "pluginType": "core",
153
+ "strict": true,
154
+ "enableJsonFlag": true,
155
+ "helpGroup": "Projects",
156
+ "isESM": true,
157
+ "relativePath": [
158
+ "dist",
159
+ "commands",
160
+ "project",
161
+ "list.js"
162
+ ]
163
+ },
164
+ "project:switch": {
165
+ "aliases": [],
166
+ "args": {
167
+ "name": {
168
+ "description": "Project name",
169
+ "name": "name",
170
+ "required": true
171
+ }
172
+ },
173
+ "description": "Switch to a project from the local project list",
174
+ "flags": {
175
+ "json": {
176
+ "description": "Format output as json.",
177
+ "helpGroup": "GLOBAL",
178
+ "name": "json",
179
+ "allowNo": false,
180
+ "type": "boolean"
181
+ }
182
+ },
183
+ "hasDynamicHelp": false,
184
+ "hiddenAliases": [],
185
+ "id": "project:switch",
186
+ "pluginAlias": "ardent-cli-beta",
187
+ "pluginName": "ardent-cli-beta",
188
+ "pluginType": "core",
189
+ "strict": true,
190
+ "enableJsonFlag": true,
191
+ "helpGroup": "Projects",
192
+ "isESM": true,
193
+ "relativePath": [
194
+ "dist",
195
+ "commands",
196
+ "project",
197
+ "switch.js"
198
+ ]
97
199
  }
98
200
  },
99
- "version": "0.0.7"
201
+ "version": "0.0.8"
100
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli-beta",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "The Ardent command-line interface",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",