Skip to content

Commit a1207cb

Browse files
authored
Merge pull request #657 from constructive-io/devin/1769265674-cnc-execution-engine
feat(cli): add execution engine with context, auth, and execute commands
2 parents feaeaae + abb9ef6 commit a1207cb

File tree

13 files changed

+3643
-7006
lines changed

13 files changed

+3643
-7006
lines changed

packages/cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@
5555
"@pgpmjs/logger": "workspace:^",
5656
"@pgpmjs/server-utils": "workspace:^",
5757
"@pgpmjs/types": "workspace:^",
58+
"appstash": "^0.3.0",
5859
"find-and-require-package-json": "^0.9.0",
59-
"inquirerer": "^4.4.0",
60+
"inquirerer": "^4.5.0",
6061
"js-yaml": "^4.1.0",
6162
"pg-cache": "workspace:^",
6263
"pg-env": "workspace:^",

packages/cli/src/commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { checkForUpdates } from '@inquirerer/utils';
22
import { CLIOptions, Inquirerer, ParsedArgs, cliExitWithError, extractFirst, getPackageJson } from 'inquirerer';
33

4+
import auth from './commands/auth';
45
import codegen from './commands/codegen';
6+
import context from './commands/context';
7+
import execute from './commands/execute';
58
import explorer from './commands/explorer';
69
import getGraphqlSchema from './commands/get-graphql-schema';
710
import jobs from './commands/jobs';
@@ -15,6 +18,9 @@ const createCommandMap = (): Record<string, Function> => {
1518
'get-graphql-schema': getGraphqlSchema,
1619
codegen,
1720
jobs,
21+
context,
22+
auth,
23+
execute,
1824
};
1925
};
2026

packages/cli/src/commands/auth.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/**
2+
* Authentication commands for the CNC execution engine
3+
*/
4+
5+
import { CLIOptions, Inquirerer, extractFirst } from 'inquirerer';
6+
import chalk from 'yanse';
7+
import {
8+
getCurrentContext,
9+
loadContext,
10+
listContexts,
11+
getContextCredentials,
12+
setContextCredentials,
13+
removeContextCredentials,
14+
hasValidCredentials,
15+
loadSettings,
16+
} from '../config';
17+
18+
const usage = `
19+
Constructive Authentication:
20+
21+
cnc auth <command> [OPTIONS]
22+
23+
Commands:
24+
set-token <token> Set API token for the current context
25+
status Show authentication status
26+
logout Remove credentials for the current context
27+
28+
Options:
29+
--context <name> Specify context (defaults to current context)
30+
--expires <date> Token expiration date (ISO format)
31+
32+
Examples:
33+
cnc auth set-token eyJhbGciOiJIUzI1NiIs...
34+
cnc auth status
35+
cnc auth logout
36+
cnc auth set-token <token> --context my-api
37+
38+
--help, -h Show this help message
39+
`;
40+
41+
export default async (
42+
argv: Partial<Record<string, unknown>>,
43+
prompter: Inquirerer,
44+
_options: CLIOptions
45+
) => {
46+
if (argv.help || argv.h) {
47+
console.log(usage);
48+
process.exit(0);
49+
}
50+
51+
const { first: subcommand, newArgv } = extractFirst(argv);
52+
53+
if (!subcommand) {
54+
const answer = await prompter.prompt(argv, [
55+
{
56+
type: 'autocomplete',
57+
name: 'subcommand',
58+
message: 'What do you want to do?',
59+
options: ['set-token', 'status', 'logout'],
60+
},
61+
]);
62+
return handleSubcommand(answer.subcommand as string, newArgv, prompter);
63+
}
64+
65+
return handleSubcommand(subcommand, newArgv, prompter);
66+
};
67+
68+
async function handleSubcommand(
69+
subcommand: string,
70+
argv: Partial<Record<string, unknown>>,
71+
prompter: Inquirerer
72+
) {
73+
switch (subcommand) {
74+
case 'set-token':
75+
return handleSetToken(argv, prompter);
76+
case 'status':
77+
return handleStatus(argv);
78+
case 'logout':
79+
return handleLogout(argv, prompter);
80+
default:
81+
console.log(usage);
82+
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
83+
process.exit(1);
84+
}
85+
}
86+
87+
async function getTargetContext(
88+
argv: Partial<Record<string, unknown>>,
89+
prompter: Inquirerer
90+
): Promise<string> {
91+
if (argv.context && typeof argv.context === 'string') {
92+
const context = loadContext(argv.context);
93+
if (!context) {
94+
console.error(chalk.red(`Context "${argv.context}" not found.`));
95+
process.exit(1);
96+
}
97+
return argv.context;
98+
}
99+
100+
const current = getCurrentContext();
101+
if (current) {
102+
return current.name;
103+
}
104+
105+
const contexts = listContexts();
106+
if (contexts.length === 0) {
107+
console.error(chalk.red('No contexts configured.'));
108+
console.log(chalk.gray('Run "cnc context create <name>" to create one first.'));
109+
process.exit(1);
110+
}
111+
112+
const answer = await prompter.prompt(argv, [
113+
{
114+
type: 'autocomplete',
115+
name: 'context',
116+
message: 'Select context',
117+
options: contexts.map(c => c.name),
118+
},
119+
]);
120+
121+
return answer.context as string;
122+
}
123+
124+
async function handleSetToken(
125+
argv: Partial<Record<string, unknown>>,
126+
prompter: Inquirerer
127+
) {
128+
const contextName = await getTargetContext(argv, prompter);
129+
const { first: token, newArgv } = extractFirst(argv);
130+
131+
let tokenValue = token as string;
132+
133+
if (!tokenValue) {
134+
const answer = await prompter.prompt(newArgv, [
135+
{
136+
type: 'password',
137+
name: 'token',
138+
message: 'API Token',
139+
required: true,
140+
},
141+
]);
142+
tokenValue = (answer as Record<string, unknown>).token as string;
143+
}
144+
145+
if (!tokenValue || tokenValue.trim() === '') {
146+
console.error(chalk.red('Token cannot be empty.'));
147+
process.exit(1);
148+
}
149+
150+
const expiresAt = argv.expires as string | undefined;
151+
152+
setContextCredentials(contextName, tokenValue.trim(), { expiresAt });
153+
154+
console.log(chalk.green(`Token saved for context: ${contextName}`));
155+
if (expiresAt) {
156+
console.log(chalk.gray(`Expires: ${expiresAt}`));
157+
}
158+
}
159+
160+
function handleStatus(argv: Partial<Record<string, unknown>>) {
161+
const settings = loadSettings();
162+
const contexts = listContexts();
163+
164+
if (contexts.length === 0) {
165+
console.log(chalk.gray('No contexts configured.'));
166+
return;
167+
}
168+
169+
if (argv.context && typeof argv.context === 'string') {
170+
const context = loadContext(argv.context);
171+
if (!context) {
172+
console.error(chalk.red(`Context "${argv.context}" not found.`));
173+
process.exit(1);
174+
}
175+
showContextAuthStatus(context.name, settings.currentContext === context.name);
176+
return;
177+
}
178+
179+
console.log(chalk.bold('Authentication Status:'));
180+
console.log();
181+
182+
for (const context of contexts) {
183+
const isCurrent = context.name === settings.currentContext;
184+
showContextAuthStatus(context.name, isCurrent);
185+
}
186+
}
187+
188+
function showContextAuthStatus(contextName: string, isCurrent: boolean) {
189+
const creds = getContextCredentials(contextName);
190+
const hasAuth = hasValidCredentials(contextName);
191+
const marker = isCurrent ? chalk.green('*') : ' ';
192+
193+
console.log(`${marker} ${chalk.bold(contextName)}`);
194+
195+
if (hasAuth && creds) {
196+
console.log(` Status: ${chalk.green('Authenticated')}`);
197+
console.log(` Token: ${maskToken(creds.token)}`);
198+
if (creds.expiresAt) {
199+
const expiresAt = new Date(creds.expiresAt);
200+
const now = new Date();
201+
if (expiresAt <= now) {
202+
console.log(` Expires: ${chalk.red(creds.expiresAt + ' (expired)')}`);
203+
} else {
204+
console.log(` Expires: ${creds.expiresAt}`);
205+
}
206+
}
207+
} else if (creds && creds.token) {
208+
console.log(` Status: ${chalk.red('Expired')}`);
209+
console.log(` Token: ${maskToken(creds.token)}`);
210+
if (creds.expiresAt) {
211+
console.log(` Expired: ${creds.expiresAt}`);
212+
}
213+
} else {
214+
console.log(` Status: ${chalk.yellow('Not authenticated')}`);
215+
}
216+
console.log();
217+
}
218+
219+
function maskToken(token: string): string {
220+
if (token.length <= 10) {
221+
return '****';
222+
}
223+
return token.substring(0, 6) + '...' + token.substring(token.length - 4);
224+
}
225+
226+
async function handleLogout(
227+
argv: Partial<Record<string, unknown>>,
228+
prompter: Inquirerer
229+
) {
230+
const contextName = await getTargetContext(argv, prompter);
231+
232+
const creds = getContextCredentials(contextName);
233+
if (!creds) {
234+
console.log(chalk.gray(`No credentials found for context: ${contextName}`));
235+
return;
236+
}
237+
238+
const confirm = await prompter.prompt(argv, [
239+
{
240+
type: 'confirm',
241+
name: 'confirm',
242+
message: `Remove credentials for context "${contextName}"?`,
243+
default: false,
244+
},
245+
]);
246+
247+
if (!confirm.confirm) {
248+
console.log(chalk.gray('Cancelled.'));
249+
return;
250+
}
251+
252+
if (removeContextCredentials(contextName)) {
253+
console.log(chalk.green(`Credentials removed for context: ${contextName}`));
254+
} else {
255+
console.log(chalk.gray(`No credentials found for context: ${contextName}`));
256+
}
257+
}

0 commit comments

Comments
 (0)