@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.
@@ -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,5 @@
1
+ /**
2
+ * Config module exports
3
+ */
4
+ export * from './types';
5
+ export * from './config-manager';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Configuration types for the CNC execution engine
3
+ */
4
+ /**
5
+ * Default global settings
6
+ */
7
+ export const DEFAULT_SETTINGS = {};
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SDK module exports
3
+ */
4
+ export * from './client';
5
+ export * from './executor';