@constructive-io/cli 6.0.5 → 6.1.1
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/commands/auth.d.ts +6 -0
- package/commands/auth.js +207 -0
- package/commands/codegen.js +3 -0
- package/commands/context.d.ts +7 -0
- package/commands/context.js +233 -0
- package/commands/execute.d.ts +6 -0
- package/commands/execute.js +136 -0
- package/commands.js +6 -0
- package/config/config-manager.d.ts +72 -0
- package/config/config-manager.js +280 -0
- package/config/index.d.ts +5 -0
- package/config/index.js +21 -0
- package/config/types.d.ts +46 -0
- package/config/types.js +10 -0
- package/esm/commands/auth.js +202 -0
- package/esm/commands/codegen.js +3 -0
- package/esm/commands/context.js +228 -0
- package/esm/commands/execute.js +98 -0
- package/esm/commands.js +6 -0
- package/esm/config/config-manager.js +229 -0
- package/esm/config/index.js +5 -0
- package/esm/config/types.js +7 -0
- package/esm/sdk/client.js +66 -0
- package/esm/sdk/executor.js +44 -0
- package/esm/sdk/index.js +5 -0
- package/esm/utils/display.js +10 -0
- package/package.json +5 -4
- package/sdk/client.d.ts +33 -0
- package/sdk/client.js +70 -0
- package/sdk/executor.d.ts +21 -0
- package/sdk/executor.js +48 -0
- package/sdk/index.d.ts +5 -0
- package/sdk/index.js +21 -0
- package/utils/display.d.ts +1 -1
- package/utils/display.js +10 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context management commands for the CNC execution engine
|
|
3
|
+
* Similar to kubectl contexts - manages named endpoint + credential configurations
|
|
4
|
+
*/
|
|
5
|
+
import { extractFirst } from 'inquirerer';
|
|
6
|
+
import chalk from 'yanse';
|
|
7
|
+
import { createContext, listContexts, loadContext, deleteContext, getCurrentContext, setCurrentContext, loadSettings, saveSettings, getContextCredentials, hasValidCredentials, } from '../config';
|
|
8
|
+
const usage = `
|
|
9
|
+
Constructive Context Management:
|
|
10
|
+
|
|
11
|
+
cnc context <command> [OPTIONS]
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
create <name> Create a new context
|
|
15
|
+
list List all contexts
|
|
16
|
+
use <name> Set the active context
|
|
17
|
+
current Show current context
|
|
18
|
+
delete <name> Delete a context
|
|
19
|
+
|
|
20
|
+
Create Options:
|
|
21
|
+
--endpoint <url> GraphQL endpoint URL
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
cnc context create my-api --endpoint https://api.example.com/graphql
|
|
25
|
+
cnc context list
|
|
26
|
+
cnc context use my-api
|
|
27
|
+
cnc context current
|
|
28
|
+
cnc context delete my-api
|
|
29
|
+
|
|
30
|
+
--help, -h Show this help message
|
|
31
|
+
`;
|
|
32
|
+
export default async (argv, prompter, _options) => {
|
|
33
|
+
if (argv.help || argv.h) {
|
|
34
|
+
console.log(usage);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
const { first: subcommand, newArgv } = extractFirst(argv);
|
|
38
|
+
if (!subcommand) {
|
|
39
|
+
const answer = await prompter.prompt(argv, [
|
|
40
|
+
{
|
|
41
|
+
type: 'autocomplete',
|
|
42
|
+
name: 'subcommand',
|
|
43
|
+
message: 'What do you want to do?',
|
|
44
|
+
options: ['create', 'list', 'use', 'current', 'delete'],
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
return handleSubcommand(answer.subcommand, newArgv, prompter);
|
|
48
|
+
}
|
|
49
|
+
return handleSubcommand(subcommand, newArgv, prompter);
|
|
50
|
+
};
|
|
51
|
+
async function handleSubcommand(subcommand, argv, prompter) {
|
|
52
|
+
switch (subcommand) {
|
|
53
|
+
case 'create':
|
|
54
|
+
return handleCreate(argv, prompter);
|
|
55
|
+
case 'list':
|
|
56
|
+
return handleList();
|
|
57
|
+
case 'use':
|
|
58
|
+
return handleUse(argv, prompter);
|
|
59
|
+
case 'current':
|
|
60
|
+
return handleCurrent();
|
|
61
|
+
case 'delete':
|
|
62
|
+
return handleDelete(argv, prompter);
|
|
63
|
+
default:
|
|
64
|
+
console.log(usage);
|
|
65
|
+
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function handleCreate(argv, prompter) {
|
|
70
|
+
const { first: name, newArgv } = extractFirst(argv);
|
|
71
|
+
const settings = loadSettings();
|
|
72
|
+
const answers = await prompter.prompt({ name, ...newArgv }, [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
name: 'name',
|
|
76
|
+
message: 'Context name',
|
|
77
|
+
required: true,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'text',
|
|
81
|
+
name: 'endpoint',
|
|
82
|
+
message: 'GraphQL endpoint URL',
|
|
83
|
+
required: true,
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
const answersRecord = answers;
|
|
87
|
+
const contextName = answersRecord.name;
|
|
88
|
+
const endpoint = answersRecord.endpoint;
|
|
89
|
+
const existing = loadContext(contextName);
|
|
90
|
+
if (existing) {
|
|
91
|
+
console.error(chalk.red(`Context "${contextName}" already exists.`));
|
|
92
|
+
console.log(chalk.gray(`Use "cnc context delete ${contextName}" to remove it first.`));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const context = createContext(contextName, endpoint);
|
|
96
|
+
if (!settings.currentContext) {
|
|
97
|
+
setCurrentContext(contextName);
|
|
98
|
+
console.log(chalk.green(`Created and activated context: ${contextName}`));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(chalk.green(`Created context: ${contextName}`));
|
|
102
|
+
}
|
|
103
|
+
console.log();
|
|
104
|
+
console.log(` Endpoint: ${context.endpoint}`);
|
|
105
|
+
console.log();
|
|
106
|
+
console.log(chalk.gray(`Next: Run "cnc auth set-token <token>" to configure authentication.`));
|
|
107
|
+
}
|
|
108
|
+
function handleList() {
|
|
109
|
+
const contexts = listContexts();
|
|
110
|
+
const settings = loadSettings();
|
|
111
|
+
if (contexts.length === 0) {
|
|
112
|
+
console.log(chalk.gray('No contexts configured.'));
|
|
113
|
+
console.log(chalk.gray('Run "cnc context create <name>" to create one.'));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.log(chalk.bold('Contexts:'));
|
|
117
|
+
console.log();
|
|
118
|
+
for (const context of contexts) {
|
|
119
|
+
const isCurrent = context.name === settings.currentContext;
|
|
120
|
+
const hasAuth = hasValidCredentials(context.name);
|
|
121
|
+
const marker = isCurrent ? chalk.green('*') : ' ';
|
|
122
|
+
const authStatus = hasAuth ? chalk.green('[authenticated]') : chalk.yellow('[no token]');
|
|
123
|
+
console.log(`${marker} ${chalk.bold(context.name)} ${authStatus}`);
|
|
124
|
+
console.log(` Endpoint: ${context.endpoint}`);
|
|
125
|
+
console.log();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function handleUse(argv, prompter) {
|
|
129
|
+
const { first: name } = extractFirst(argv);
|
|
130
|
+
const contexts = listContexts();
|
|
131
|
+
if (contexts.length === 0) {
|
|
132
|
+
console.log(chalk.gray('No contexts configured.'));
|
|
133
|
+
console.log(chalk.gray('Run "cnc context create <name>" to create one.'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let contextName = name;
|
|
137
|
+
if (!contextName) {
|
|
138
|
+
const answer = await prompter.prompt(argv, [
|
|
139
|
+
{
|
|
140
|
+
type: 'autocomplete',
|
|
141
|
+
name: 'name',
|
|
142
|
+
message: 'Select context',
|
|
143
|
+
options: contexts.map(c => c.name),
|
|
144
|
+
},
|
|
145
|
+
]);
|
|
146
|
+
contextName = answer.name;
|
|
147
|
+
}
|
|
148
|
+
if (setCurrentContext(contextName)) {
|
|
149
|
+
console.log(chalk.green(`Switched to context: ${contextName}`));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.error(chalk.red(`Context "${contextName}" not found.`));
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function handleCurrent() {
|
|
157
|
+
const current = getCurrentContext();
|
|
158
|
+
if (!current) {
|
|
159
|
+
console.log(chalk.gray('No current context set.'));
|
|
160
|
+
console.log(chalk.gray('Run "cnc context use <name>" to set one.'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const creds = getContextCredentials(current.name);
|
|
164
|
+
const hasAuth = hasValidCredentials(current.name);
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(chalk.bold(`Current context: ${current.name}`));
|
|
167
|
+
console.log();
|
|
168
|
+
console.log(` Endpoint: ${current.endpoint}`);
|
|
169
|
+
console.log(` Created: ${current.createdAt}`);
|
|
170
|
+
console.log(` Updated: ${current.updatedAt}`);
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(chalk.bold('Authentication:'));
|
|
173
|
+
if (hasAuth) {
|
|
174
|
+
console.log(` Status: ${chalk.green('Authenticated')}`);
|
|
175
|
+
if (creds?.expiresAt) {
|
|
176
|
+
console.log(` Expires: ${creds.expiresAt}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log(` Status: ${chalk.yellow('Not authenticated')}`);
|
|
181
|
+
console.log(chalk.gray(` Run "cnc auth set-token <token>" to configure.`));
|
|
182
|
+
}
|
|
183
|
+
console.log();
|
|
184
|
+
}
|
|
185
|
+
async function handleDelete(argv, prompter) {
|
|
186
|
+
const { first: name } = extractFirst(argv);
|
|
187
|
+
const contexts = listContexts();
|
|
188
|
+
if (contexts.length === 0) {
|
|
189
|
+
console.log(chalk.gray('No contexts configured.'));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
let contextName = name;
|
|
193
|
+
if (!contextName) {
|
|
194
|
+
const answer = await prompter.prompt(argv, [
|
|
195
|
+
{
|
|
196
|
+
type: 'autocomplete',
|
|
197
|
+
name: 'name',
|
|
198
|
+
message: 'Select context to delete',
|
|
199
|
+
options: contexts.map(c => c.name),
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
contextName = answer.name;
|
|
203
|
+
}
|
|
204
|
+
const confirm = await prompter.prompt(argv, [
|
|
205
|
+
{
|
|
206
|
+
type: 'confirm',
|
|
207
|
+
name: 'confirm',
|
|
208
|
+
message: `Are you sure you want to delete context "${contextName}"?`,
|
|
209
|
+
default: false,
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
if (!confirm.confirm) {
|
|
213
|
+
console.log(chalk.gray('Cancelled.'));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (deleteContext(contextName)) {
|
|
217
|
+
const settings = loadSettings();
|
|
218
|
+
if (settings.currentContext === contextName) {
|
|
219
|
+
settings.currentContext = undefined;
|
|
220
|
+
saveSettings(settings);
|
|
221
|
+
}
|
|
222
|
+
console.log(chalk.green(`Deleted context: ${contextName}`));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.error(chalk.red(`Context "${contextName}" not found.`));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute command for running GraphQL queries
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import chalk from 'yanse';
|
|
6
|
+
import { execute, getExecutionContext } from '../sdk';
|
|
7
|
+
const usage = `
|
|
8
|
+
Constructive Execute - Run GraphQL Queries:
|
|
9
|
+
|
|
10
|
+
cnc execute [OPTIONS]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--query <graphql> GraphQL query/mutation string
|
|
14
|
+
--file <path> Path to file containing GraphQL query
|
|
15
|
+
--variables <json> Variables as JSON string
|
|
16
|
+
--context <name> Context to use (defaults to current)
|
|
17
|
+
|
|
18
|
+
Examples:
|
|
19
|
+
# Execute inline query
|
|
20
|
+
cnc execute --query 'query { databases { nodes { id name } } }'
|
|
21
|
+
|
|
22
|
+
# Execute from file
|
|
23
|
+
cnc execute --file query.graphql
|
|
24
|
+
|
|
25
|
+
# With variables
|
|
26
|
+
cnc execute --query 'query($id: UUID!) { database(id: $id) { name } }' --variables '{"id":"..."}'
|
|
27
|
+
|
|
28
|
+
--help, -h Show this help message
|
|
29
|
+
`;
|
|
30
|
+
export default async (argv, prompter, _options) => {
|
|
31
|
+
if (argv.help || argv.h) {
|
|
32
|
+
console.log(usage);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
let query;
|
|
36
|
+
let variables;
|
|
37
|
+
if (argv.file) {
|
|
38
|
+
const filePath = argv.file;
|
|
39
|
+
if (!fs.existsSync(filePath)) {
|
|
40
|
+
console.error(chalk.red(`File not found: ${filePath}`));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
query = fs.readFileSync(filePath, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
else if (argv.query) {
|
|
46
|
+
query = argv.query;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const answers = await prompter.prompt(argv, [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
name: 'query',
|
|
53
|
+
message: 'GraphQL query',
|
|
54
|
+
required: true,
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
query = answers.query;
|
|
58
|
+
}
|
|
59
|
+
if (argv.variables) {
|
|
60
|
+
try {
|
|
61
|
+
variables = JSON.parse(argv.variables);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.error(chalk.red('Invalid JSON in --variables'));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let execContext;
|
|
69
|
+
try {
|
|
70
|
+
execContext = await getExecutionContext(argv.context);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error(chalk.red(error instanceof Error ? error.message : 'Failed to get execution context'));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.gray(`Context: ${execContext.context.name}`));
|
|
77
|
+
console.log(chalk.gray(`Endpoint: ${execContext.context.endpoint}`));
|
|
78
|
+
console.log();
|
|
79
|
+
const result = await execute(query, variables, execContext);
|
|
80
|
+
if (result.ok) {
|
|
81
|
+
console.log(chalk.green('Success!'));
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.error(chalk.red('Failed!'));
|
|
87
|
+
console.log();
|
|
88
|
+
if (result.errors) {
|
|
89
|
+
for (const error of result.errors) {
|
|
90
|
+
console.error(chalk.red(` - ${error.message}`));
|
|
91
|
+
if (error.path) {
|
|
92
|
+
console.error(chalk.gray(` Path: ${error.path.join('.')}`));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
};
|
package/esm/commands.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { checkForUpdates } from '@inquirerer/utils';
|
|
2
2
|
import { cliExitWithError, extractFirst, getPackageJson } from 'inquirerer';
|
|
3
|
+
import auth from './commands/auth';
|
|
3
4
|
import codegen from './commands/codegen';
|
|
5
|
+
import context from './commands/context';
|
|
6
|
+
import execute from './commands/execute';
|
|
4
7
|
import explorer from './commands/explorer';
|
|
5
8
|
import getGraphqlSchema from './commands/get-graphql-schema';
|
|
6
9
|
import jobs from './commands/jobs';
|
|
@@ -13,6 +16,9 @@ const createCommandMap = () => {
|
|
|
13
16
|
'get-graphql-schema': getGraphqlSchema,
|
|
14
17
|
codegen,
|
|
15
18
|
jobs,
|
|
19
|
+
context,
|
|
20
|
+
auth,
|
|
21
|
+
execute,
|
|
16
22
|
};
|
|
17
23
|
};
|
|
18
24
|
export const commands = async (argv, prompter, options) => {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration manager for the CNC execution engine
|
|
3
|
+
* Uses appstash for directory resolution
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { appstash, resolve } from 'appstash';
|
|
8
|
+
import { DEFAULT_SETTINGS } from './types';
|
|
9
|
+
const TOOL_NAME = 'cnc';
|
|
10
|
+
/**
|
|
11
|
+
* Get the appstash directories for cnc
|
|
12
|
+
*/
|
|
13
|
+
export function getAppDirs() {
|
|
14
|
+
return appstash(TOOL_NAME, { ensure: true });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get path to a config file
|
|
18
|
+
*/
|
|
19
|
+
function getConfigPath(filename) {
|
|
20
|
+
const dirs = getAppDirs();
|
|
21
|
+
return resolve(dirs, 'config', filename);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get path to a context config file
|
|
25
|
+
*/
|
|
26
|
+
function getContextConfigPath(contextName) {
|
|
27
|
+
const dirs = getAppDirs();
|
|
28
|
+
const contextsDir = resolve(dirs, 'config', 'contexts');
|
|
29
|
+
if (!fs.existsSync(contextsDir)) {
|
|
30
|
+
fs.mkdirSync(contextsDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
return path.join(contextsDir, `${contextName}.json`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Load global settings
|
|
36
|
+
*/
|
|
37
|
+
export function loadSettings() {
|
|
38
|
+
const settingsPath = getConfigPath('settings.json');
|
|
39
|
+
if (fs.existsSync(settingsPath)) {
|
|
40
|
+
try {
|
|
41
|
+
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
42
|
+
return { ...DEFAULT_SETTINGS, ...JSON.parse(content) };
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return DEFAULT_SETTINGS;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return DEFAULT_SETTINGS;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Save global settings
|
|
52
|
+
*/
|
|
53
|
+
export function saveSettings(settings) {
|
|
54
|
+
const settingsPath = getConfigPath('settings.json');
|
|
55
|
+
const configDir = path.dirname(settingsPath);
|
|
56
|
+
if (!fs.existsSync(configDir)) {
|
|
57
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Load credentials
|
|
63
|
+
*/
|
|
64
|
+
export function loadCredentials() {
|
|
65
|
+
const credentialsPath = getConfigPath('credentials.json');
|
|
66
|
+
if (fs.existsSync(credentialsPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const content = fs.readFileSync(credentialsPath, 'utf8');
|
|
69
|
+
return JSON.parse(content);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { tokens: {} };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { tokens: {} };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Save credentials
|
|
79
|
+
*/
|
|
80
|
+
export function saveCredentials(credentials) {
|
|
81
|
+
const credentialsPath = getConfigPath('credentials.json');
|
|
82
|
+
const configDir = path.dirname(credentialsPath);
|
|
83
|
+
if (!fs.existsSync(configDir)) {
|
|
84
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2), {
|
|
87
|
+
mode: 0o600, // Read/write for owner only
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Load a context configuration
|
|
92
|
+
*/
|
|
93
|
+
export function loadContext(contextName) {
|
|
94
|
+
const contextPath = getContextConfigPath(contextName);
|
|
95
|
+
if (fs.existsSync(contextPath)) {
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
98
|
+
return JSON.parse(content);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Save a context configuration
|
|
108
|
+
*/
|
|
109
|
+
export function saveContext(context) {
|
|
110
|
+
const contextPath = getContextConfigPath(context.name);
|
|
111
|
+
fs.writeFileSync(contextPath, JSON.stringify(context, null, 2));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Delete a context configuration
|
|
115
|
+
*/
|
|
116
|
+
export function deleteContext(contextName) {
|
|
117
|
+
const contextPath = getContextConfigPath(contextName);
|
|
118
|
+
if (fs.existsSync(contextPath)) {
|
|
119
|
+
fs.unlinkSync(contextPath);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* List all context configurations
|
|
126
|
+
*/
|
|
127
|
+
export function listContexts() {
|
|
128
|
+
const dirs = getAppDirs();
|
|
129
|
+
const contextsDir = resolve(dirs, 'config', 'contexts');
|
|
130
|
+
if (!fs.existsSync(contextsDir)) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
const files = fs.readdirSync(contextsDir).filter(f => f.endsWith('.json'));
|
|
134
|
+
const contexts = [];
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
try {
|
|
137
|
+
const content = fs.readFileSync(path.join(contextsDir, file), 'utf8');
|
|
138
|
+
contexts.push(JSON.parse(content));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Skip invalid files
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return contexts;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get the current active context
|
|
148
|
+
*/
|
|
149
|
+
export function getCurrentContext() {
|
|
150
|
+
const settings = loadSettings();
|
|
151
|
+
if (settings.currentContext) {
|
|
152
|
+
return loadContext(settings.currentContext);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Set the current active context
|
|
158
|
+
*/
|
|
159
|
+
export function setCurrentContext(contextName) {
|
|
160
|
+
const context = loadContext(contextName);
|
|
161
|
+
if (!context) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const settings = loadSettings();
|
|
165
|
+
settings.currentContext = contextName;
|
|
166
|
+
saveSettings(settings);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a new context configuration
|
|
171
|
+
*/
|
|
172
|
+
export function createContext(name, endpoint) {
|
|
173
|
+
const now = new Date().toISOString();
|
|
174
|
+
const context = {
|
|
175
|
+
name,
|
|
176
|
+
endpoint,
|
|
177
|
+
createdAt: now,
|
|
178
|
+
updatedAt: now,
|
|
179
|
+
};
|
|
180
|
+
saveContext(context);
|
|
181
|
+
return context;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get credentials for a context
|
|
185
|
+
*/
|
|
186
|
+
export function getContextCredentials(contextName) {
|
|
187
|
+
const credentials = loadCredentials();
|
|
188
|
+
return credentials.tokens[contextName] || null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Set credentials for a context
|
|
192
|
+
*/
|
|
193
|
+
export function setContextCredentials(contextName, token, options) {
|
|
194
|
+
const credentials = loadCredentials();
|
|
195
|
+
credentials.tokens[contextName] = {
|
|
196
|
+
token,
|
|
197
|
+
expiresAt: options?.expiresAt,
|
|
198
|
+
refreshToken: options?.refreshToken,
|
|
199
|
+
};
|
|
200
|
+
saveCredentials(credentials);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove credentials for a context
|
|
204
|
+
*/
|
|
205
|
+
export function removeContextCredentials(contextName) {
|
|
206
|
+
const credentials = loadCredentials();
|
|
207
|
+
if (credentials.tokens[contextName]) {
|
|
208
|
+
delete credentials.tokens[contextName];
|
|
209
|
+
saveCredentials(credentials);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Check if a context has valid credentials
|
|
216
|
+
*/
|
|
217
|
+
export function hasValidCredentials(contextName) {
|
|
218
|
+
const creds = getContextCredentials(contextName);
|
|
219
|
+
if (!creds || !creds.token) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (creds.expiresAt) {
|
|
223
|
+
const expiresAt = new Date(creds.expiresAt);
|
|
224
|
+
if (expiresAt <= new Date()) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple GraphQL client for the CNC execution engine
|
|
3
|
+
* Uses native fetch - no external dependencies
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Execute a GraphQL query/mutation against an endpoint
|
|
7
|
+
*/
|
|
8
|
+
export async function executeGraphQL(endpoint, query, variables, headers) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(endpoint, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
Accept: 'application/json',
|
|
15
|
+
...headers,
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
query,
|
|
19
|
+
variables: variables ?? {},
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
data: null,
|
|
26
|
+
errors: [
|
|
27
|
+
{ message: `HTTP ${response.status}: ${response.statusText}` },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const json = (await response.json());
|
|
32
|
+
if (json.errors && json.errors.length > 0) {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
data: json.data ?? null,
|
|
36
|
+
errors: json.errors,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
data: json.data,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
data: null,
|
|
48
|
+
errors: [
|
|
49
|
+
{
|
|
50
|
+
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create a configured client for a specific endpoint
|
|
58
|
+
*/
|
|
59
|
+
export function createClient(config) {
|
|
60
|
+
return {
|
|
61
|
+
execute: (query, variables) => {
|
|
62
|
+
return executeGraphQL(config.endpoint, query, variables, config.headers);
|
|
63
|
+
},
|
|
64
|
+
config,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple GraphQL executor for the CNC execution engine
|
|
3
|
+
* Executes raw GraphQL queries against configured endpoints
|
|
4
|
+
*/
|
|
5
|
+
import { executeGraphQL } from './client';
|
|
6
|
+
import { getCurrentContext, loadContext, getContextCredentials, hasValidCredentials, } from '../config';
|
|
7
|
+
/**
|
|
8
|
+
* Get execution context for the current or specified context
|
|
9
|
+
*/
|
|
10
|
+
export async function getExecutionContext(contextName) {
|
|
11
|
+
let context;
|
|
12
|
+
if (contextName) {
|
|
13
|
+
context = loadContext(contextName);
|
|
14
|
+
if (!context) {
|
|
15
|
+
throw new Error(`Context "${contextName}" not found.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
context = getCurrentContext();
|
|
20
|
+
if (!context) {
|
|
21
|
+
throw new Error('No active context. Run "cnc context create" or "cnc context use" first.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!hasValidCredentials(context.name)) {
|
|
25
|
+
throw new Error(`No valid credentials for context "${context.name}". Run "cnc auth set-token" first.`);
|
|
26
|
+
}
|
|
27
|
+
const creds = getContextCredentials(context.name);
|
|
28
|
+
if (!creds || !creds.token) {
|
|
29
|
+
throw new Error(`No token found for context "${context.name}". Run "cnc auth set-token" first.`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
context,
|
|
33
|
+
token: creds.token,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Execute a raw GraphQL query/mutation
|
|
38
|
+
*/
|
|
39
|
+
export async function execute(query, variables, execContext) {
|
|
40
|
+
const ctx = execContext || (await getExecutionContext());
|
|
41
|
+
return executeGraphQL(ctx.context.endpoint, query, variables, {
|
|
42
|
+
Authorization: `Bearer ${ctx.token}`,
|
|
43
|
+
});
|
|
44
|
+
}
|