@eide/foir-cli 0.1.34 → 0.1.35
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/dist/cli.js +10 -47
- package/dist/codegen/generators/model-types.js +2 -1
- package/dist/commands/playground.d.ts +4 -0
- package/dist/commands/playground.d.ts.map +1 -0
- package/dist/commands/playground.js +270 -0
- package/dist/commands/register-commands.d.ts +7 -0
- package/dist/commands/register-commands.d.ts.map +1 -0
- package/dist/commands/register-commands.js +259 -0
- package/dist/commands/select-project.js +2 -2
- package/dist/graphql/generated.d.ts +42 -2298
- package/dist/graphql/generated.d.ts.map +1 -1
- package/dist/graphql/generated.js +355 -135
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -9,37 +9,18 @@ import { createRequire } from 'module';
|
|
|
9
9
|
import { Command } from 'commander';
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const { version } = require('../package.json');
|
|
12
|
-
// Auth commands
|
|
12
|
+
// Auth commands (special — not GraphQL-driven)
|
|
13
13
|
import { registerLoginCommand } from './commands/login.js';
|
|
14
14
|
import { registerLogoutCommand } from './commands/logout.js';
|
|
15
15
|
import { registerSelectProjectCommand } from './commands/select-project.js';
|
|
16
16
|
import { registerWhoamiCommand } from './commands/whoami.js';
|
|
17
|
-
//
|
|
18
|
-
import { registerRecordsCommands } from './commands/records.js';
|
|
19
|
-
import { registerModelsCommands } from './commands/models.js';
|
|
20
|
-
import { registerSearchCommands } from './commands/search.js';
|
|
21
|
-
import { registerCustomersCommands } from './commands/customers.js';
|
|
22
|
-
import { registerCustomerProfilesCommands } from './commands/customer-profiles.js';
|
|
23
|
-
import { registerSegmentsCommands } from './commands/segments.js';
|
|
24
|
-
import { registerExperimentsCommands } from './commands/experiments.js';
|
|
25
|
-
import { registerSettingsCommands } from './commands/settings.js';
|
|
26
|
-
import { registerApiKeysCommands } from './commands/api-keys.js';
|
|
27
|
-
import { registerAuthProvidersCommands } from './commands/auth-providers.js';
|
|
28
|
-
import { registerExtensionsCommands } from './commands/extensions.js';
|
|
29
|
-
import { registerOperationsCommands } from './commands/operations.js';
|
|
30
|
-
import { registerHooksCommands } from './commands/hooks.js';
|
|
31
|
-
import { registerSchedulesCommands } from './commands/schedules.js';
|
|
17
|
+
// Special commands (REST, codegen, scaffolding, multi-type search)
|
|
32
18
|
import { registerMediaCommands } from './commands/media.js';
|
|
33
|
-
import { registerContextCommands } from './commands/context.js';
|
|
34
|
-
import { registerNotificationsCommands } from './commands/notifications.js';
|
|
35
|
-
import { registerLocalesCommands } from './commands/locales.js';
|
|
36
|
-
import { registerFilesCommands } from './commands/files.js';
|
|
37
|
-
import { registerNotesCommands } from './commands/notes.js';
|
|
38
|
-
import { registerVariantCatalogCommands } from './commands/variant-catalog.js';
|
|
39
|
-
import { registerAuthConfigCommands } from './commands/auth-config.js';
|
|
40
|
-
import { registerEmbeddingsCommands } from './commands/embeddings.js';
|
|
41
19
|
import { registerPullCommand } from './commands/pull.js';
|
|
42
20
|
import { registerCreateExtensionCommand } from './commands/create-extension.js';
|
|
21
|
+
import { registerSearchCommands } from './commands/search.js';
|
|
22
|
+
// Dynamic commands from command registry (replaces ~25 imperative command files)
|
|
23
|
+
import { registerDynamicCommands } from './commands/register-commands.js';
|
|
43
24
|
const program = new Command();
|
|
44
25
|
program
|
|
45
26
|
.name('foir')
|
|
@@ -58,36 +39,18 @@ function getGlobalOpts() {
|
|
|
58
39
|
quiet: !!opts.quiet,
|
|
59
40
|
};
|
|
60
41
|
}
|
|
61
|
-
//
|
|
42
|
+
// Auth commands
|
|
62
43
|
registerLoginCommand(program, getGlobalOpts);
|
|
63
44
|
registerLogoutCommand(program, getGlobalOpts);
|
|
64
45
|
registerSelectProjectCommand(program, getGlobalOpts);
|
|
65
46
|
registerWhoamiCommand(program, getGlobalOpts);
|
|
66
|
-
|
|
67
|
-
registerModelsCommands(program, getGlobalOpts);
|
|
68
|
-
registerSearchCommands(program, getGlobalOpts);
|
|
69
|
-
registerCustomersCommands(program, getGlobalOpts);
|
|
70
|
-
registerCustomerProfilesCommands(program, getGlobalOpts);
|
|
71
|
-
registerSegmentsCommands(program, getGlobalOpts);
|
|
72
|
-
registerExperimentsCommands(program, getGlobalOpts);
|
|
73
|
-
registerSettingsCommands(program, getGlobalOpts);
|
|
74
|
-
registerApiKeysCommands(program, getGlobalOpts);
|
|
75
|
-
registerAuthProvidersCommands(program, getGlobalOpts);
|
|
76
|
-
registerExtensionsCommands(program, getGlobalOpts);
|
|
77
|
-
registerOperationsCommands(program, getGlobalOpts);
|
|
78
|
-
registerHooksCommands(program, getGlobalOpts);
|
|
79
|
-
registerSchedulesCommands(program, getGlobalOpts);
|
|
47
|
+
// Special commands
|
|
80
48
|
registerMediaCommands(program, getGlobalOpts);
|
|
81
|
-
|
|
82
|
-
registerNotificationsCommands(program, getGlobalOpts);
|
|
83
|
-
registerLocalesCommands(program, getGlobalOpts);
|
|
84
|
-
registerFilesCommands(program, getGlobalOpts);
|
|
85
|
-
registerNotesCommands(program, getGlobalOpts);
|
|
86
|
-
registerVariantCatalogCommands(program, getGlobalOpts);
|
|
87
|
-
registerAuthConfigCommands(program, getGlobalOpts);
|
|
88
|
-
registerEmbeddingsCommands(program, getGlobalOpts);
|
|
49
|
+
registerSearchCommands(program, getGlobalOpts);
|
|
89
50
|
// Codegen
|
|
90
51
|
registerPullCommand(program, getGlobalOpts);
|
|
91
52
|
// Scaffolding
|
|
92
53
|
registerCreateExtensionCommand(program, getGlobalOpts);
|
|
54
|
+
// All GraphQL-driven commands (models, records, locales, etc.)
|
|
55
|
+
registerDynamicCommands(program, getGlobalOpts);
|
|
93
56
|
program.parse();
|
|
@@ -147,7 +147,8 @@ function generateDataInterface(model, fields, interfaceName, allModels) {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
// Only parameterize if all targets resolved — otherwise keep default generic
|
|
150
|
-
if (resolvedPreviewTypes.length === refTypes.length &&
|
|
150
|
+
if (resolvedPreviewTypes.length === refTypes.length &&
|
|
151
|
+
resolvedPreviewTypes.length > 0) {
|
|
151
152
|
fieldType = `ReferenceValue<${resolvedPreviewTypes.join(' | ')}>`;
|
|
152
153
|
}
|
|
153
154
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../src/commands/playground.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAMzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAgCtD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,MAAM,aAAa,QA8QnC"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as readline from 'readline';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { createClient } from '../lib/client.js';
|
|
7
|
+
import { getProjectContext } from '../auth/credentials.js';
|
|
8
|
+
import { ListModelsDocument } from '../graphql/generated.js';
|
|
9
|
+
const HISTORY_FILE = join(homedir(), '.foir', 'repl-history');
|
|
10
|
+
const MAX_HISTORY = 500;
|
|
11
|
+
function loadHistory() {
|
|
12
|
+
try {
|
|
13
|
+
if (existsSync(HISTORY_FILE)) {
|
|
14
|
+
return readFileSync(HISTORY_FILE, 'utf-8')
|
|
15
|
+
.split('\n')
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.slice(-MAX_HISTORY);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
function saveHistory(lines) {
|
|
26
|
+
try {
|
|
27
|
+
const dir = join(homedir(), '.foir');
|
|
28
|
+
if (!existsSync(dir))
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
writeFileSync(HISTORY_FILE, lines.slice(-MAX_HISTORY).join('\n') + '\n');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function registerPlaygroundCommand(program, getGlobalOpts) {
|
|
37
|
+
program
|
|
38
|
+
.command('playground')
|
|
39
|
+
.alias('repl')
|
|
40
|
+
.description('Interactive REPL with tab completion')
|
|
41
|
+
.action(async () => {
|
|
42
|
+
const opts = getGlobalOpts();
|
|
43
|
+
// Validate auth and create client
|
|
44
|
+
let client;
|
|
45
|
+
try {
|
|
46
|
+
client = await createClient(opts);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw new Error('Authentication required. Run `foir login` first.');
|
|
50
|
+
}
|
|
51
|
+
// Pre-fetch model keys for tab completion (don't block startup)
|
|
52
|
+
let modelKeys = [];
|
|
53
|
+
client
|
|
54
|
+
.request(ListModelsDocument, { limit: 200 })
|
|
55
|
+
.then((data) => {
|
|
56
|
+
modelKeys = data.models.items.map((m) => m.key);
|
|
57
|
+
})
|
|
58
|
+
.catch(() => {
|
|
59
|
+
/* ignore — completion just won't have model keys */
|
|
60
|
+
});
|
|
61
|
+
// Get project context for prompt
|
|
62
|
+
let projectName = 'no project';
|
|
63
|
+
try {
|
|
64
|
+
const ctx = await getProjectContext();
|
|
65
|
+
if (ctx?.name)
|
|
66
|
+
projectName = ctx.name;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// no project context
|
|
70
|
+
}
|
|
71
|
+
// Collect command names for completion
|
|
72
|
+
const commandNames = program.commands.map((c) => c.name());
|
|
73
|
+
const commandMap = new Map();
|
|
74
|
+
for (const cmd of program.commands) {
|
|
75
|
+
commandMap.set(cmd.name(), cmd);
|
|
76
|
+
for (const alias of cmd.aliases()) {
|
|
77
|
+
commandMap.set(alias, cmd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Build completer
|
|
81
|
+
const metaCommands = [
|
|
82
|
+
'.help',
|
|
83
|
+
'.clear',
|
|
84
|
+
'.exit',
|
|
85
|
+
'.commands',
|
|
86
|
+
'.context',
|
|
87
|
+
'.models',
|
|
88
|
+
];
|
|
89
|
+
function completer(line) {
|
|
90
|
+
const trimmed = line.trim();
|
|
91
|
+
const parts = trimmed.split(/\s+/);
|
|
92
|
+
// Meta commands
|
|
93
|
+
if (parts.length === 1 && trimmed.startsWith('.')) {
|
|
94
|
+
const hits = metaCommands.filter((c) => c.startsWith(trimmed));
|
|
95
|
+
return [hits.length ? hits : metaCommands, trimmed];
|
|
96
|
+
}
|
|
97
|
+
// First word: command names
|
|
98
|
+
if (parts.length <= 1) {
|
|
99
|
+
const hits = commandNames.filter((c) => c.startsWith(trimmed));
|
|
100
|
+
return [hits.length ? hits : commandNames, trimmed];
|
|
101
|
+
}
|
|
102
|
+
// Second word: subcommands
|
|
103
|
+
const firstPart = parts[0] ?? '';
|
|
104
|
+
const cmd = commandMap.get(firstPart);
|
|
105
|
+
if (cmd && parts.length === 2) {
|
|
106
|
+
const subNames = cmd.commands.map((c) => c.name());
|
|
107
|
+
const partial = parts[1] ?? '';
|
|
108
|
+
const hits = subNames.filter((s) => s.startsWith(partial));
|
|
109
|
+
return [hits.length ? hits : subNames, partial];
|
|
110
|
+
}
|
|
111
|
+
// Level 3: model keys for records/search commands
|
|
112
|
+
const cmdName = cmd?.name();
|
|
113
|
+
if ((cmdName === 'records' || cmdName === 'search') &&
|
|
114
|
+
parts.length === 3 &&
|
|
115
|
+
modelKeys.length > 0) {
|
|
116
|
+
const partial = parts[2] ?? '';
|
|
117
|
+
const hits = modelKeys.filter((k) => k.startsWith(partial));
|
|
118
|
+
return [hits.length ? hits : modelKeys, partial];
|
|
119
|
+
}
|
|
120
|
+
// Flags
|
|
121
|
+
if (cmd) {
|
|
122
|
+
const lastPart = parts[parts.length - 1] ?? '';
|
|
123
|
+
if (lastPart.startsWith('-')) {
|
|
124
|
+
const flags = cmd.options.map((o) => o.long).filter((f) => Boolean(f));
|
|
125
|
+
const hits = flags.filter((f) => f.startsWith(lastPart));
|
|
126
|
+
return [hits.length ? hits : [], lastPart];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [[], trimmed];
|
|
130
|
+
}
|
|
131
|
+
// Load history
|
|
132
|
+
const history = loadHistory();
|
|
133
|
+
const rl = readline.createInterface({
|
|
134
|
+
input: process.stdin,
|
|
135
|
+
output: process.stdout,
|
|
136
|
+
prompt: chalk.cyan(`foir (${projectName})> `),
|
|
137
|
+
completer,
|
|
138
|
+
history,
|
|
139
|
+
historySize: MAX_HISTORY,
|
|
140
|
+
});
|
|
141
|
+
console.log(chalk.bold('\nFoir Interactive Playground'));
|
|
142
|
+
console.log(chalk.dim('Type commands without the "foir" prefix. Use Tab for completion.'));
|
|
143
|
+
console.log(chalk.dim('Type .help for available meta-commands.\n'));
|
|
144
|
+
rl.prompt();
|
|
145
|
+
// Prevent Commander from calling process.exit()
|
|
146
|
+
program.exitOverride();
|
|
147
|
+
rl.on('line', async (line) => {
|
|
148
|
+
const input = line.trim();
|
|
149
|
+
if (!input) {
|
|
150
|
+
rl.prompt();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Meta commands
|
|
154
|
+
if (input === '.help') {
|
|
155
|
+
console.log(chalk.bold('\nMeta-commands:'));
|
|
156
|
+
console.log(' .help Show this help');
|
|
157
|
+
console.log(' .clear Clear terminal');
|
|
158
|
+
console.log(' .exit Exit playground');
|
|
159
|
+
console.log(' .commands List available commands');
|
|
160
|
+
console.log(' .context Show current auth/project context');
|
|
161
|
+
console.log(' .models Refresh and list model keys');
|
|
162
|
+
console.log(chalk.dim('\nRun any foir command without the "foir" prefix.'));
|
|
163
|
+
console.log(chalk.dim('Example: records list page\n'));
|
|
164
|
+
rl.prompt();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (input === '.clear') {
|
|
168
|
+
console.clear();
|
|
169
|
+
rl.prompt();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (input === '.exit' || input === 'exit' || input === 'quit') {
|
|
173
|
+
saveHistory(rl.history || []);
|
|
174
|
+
console.log(chalk.dim('Goodbye!'));
|
|
175
|
+
rl.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (input === '.commands') {
|
|
179
|
+
console.log(chalk.bold('\nAvailable commands:'));
|
|
180
|
+
for (const cmd of program.commands) {
|
|
181
|
+
if (cmd.name() === 'playground')
|
|
182
|
+
continue;
|
|
183
|
+
const desc = cmd.description() || '';
|
|
184
|
+
console.log(` ${chalk.green(cmd.name().padEnd(24))} ${chalk.dim(desc)}`);
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
rl.prompt();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (input === '.context') {
|
|
191
|
+
try {
|
|
192
|
+
const ctx = await getProjectContext();
|
|
193
|
+
console.log(chalk.bold('\nCurrent context:'));
|
|
194
|
+
if (ctx) {
|
|
195
|
+
console.log(` Project: ${chalk.green(ctx.name || 'unnamed')}`);
|
|
196
|
+
console.log(` ID: ${chalk.dim(ctx.id || 'n/a')}`);
|
|
197
|
+
console.log(` Tenant: ${chalk.dim(ctx.tenantId || 'n/a')}`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.log(chalk.yellow(' No project selected. Run: select-project'));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
console.log(chalk.yellow(' No project context available.'));
|
|
205
|
+
}
|
|
206
|
+
console.log('');
|
|
207
|
+
rl.prompt();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (input === '.models') {
|
|
211
|
+
try {
|
|
212
|
+
const data = await client.request(ListModelsDocument, {
|
|
213
|
+
limit: 200,
|
|
214
|
+
});
|
|
215
|
+
modelKeys = data.models.items.map((m) => m.key);
|
|
216
|
+
console.log(chalk.bold('\nModel keys:'));
|
|
217
|
+
for (const key of modelKeys) {
|
|
218
|
+
console.log(` ${chalk.green(key)}`);
|
|
219
|
+
}
|
|
220
|
+
if (modelKeys.length === 0) {
|
|
221
|
+
console.log(chalk.yellow(' No models found.'));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
console.log(chalk.yellow(' Failed to fetch models.'));
|
|
226
|
+
}
|
|
227
|
+
console.log('');
|
|
228
|
+
rl.prompt();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Regular command - parse and execute via Commander
|
|
232
|
+
const args = input.match(/(?:[^\s"']+|"[^"]*"|'[^']*')/g) || [];
|
|
233
|
+
// Strip quotes from arguments
|
|
234
|
+
const cleanArgs = args.map((a) => a.replace(/^["']|["']$/g, ''));
|
|
235
|
+
try {
|
|
236
|
+
await program.parseAsync(['node', 'foir', ...cleanArgs], {
|
|
237
|
+
from: 'user',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
// CommanderError is thrown by exitOverride instead of process.exit
|
|
242
|
+
if (err.code === 'commander.helpDisplayed' ||
|
|
243
|
+
err.code === 'commander.version') {
|
|
244
|
+
// Help/version output already printed
|
|
245
|
+
}
|
|
246
|
+
else if (err.code === 'commander.unknownCommand') {
|
|
247
|
+
console.error(chalk.red(`Unknown command: ${cleanArgs[0]}`));
|
|
248
|
+
console.log(chalk.dim('Type .commands to see available commands.'));
|
|
249
|
+
}
|
|
250
|
+
else if (err.exitCode !== undefined) {
|
|
251
|
+
// Commander error with exit code - already printed message
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Application error
|
|
255
|
+
console.error(chalk.red(err.message || String(err)));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
console.log(''); // spacing
|
|
259
|
+
rl.prompt();
|
|
260
|
+
});
|
|
261
|
+
rl.on('close', () => {
|
|
262
|
+
saveHistory(rl.history || []);
|
|
263
|
+
});
|
|
264
|
+
// Handle SIGINT gracefully
|
|
265
|
+
rl.on('SIGINT', () => {
|
|
266
|
+
console.log(chalk.dim('\n(Use .exit or Ctrl+D to quit)'));
|
|
267
|
+
rl.prompt();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { GlobalOptions } from '../lib/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register all declarative commands from the command registry.
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerDynamicCommands(program: Command, globalOpts: () => GlobalOptions): void;
|
|
7
|
+
//# sourceMappingURL=register-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-commands.d.ts","sourceRoot":"","sources":["../../src/commands/register-commands.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AA0GtD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,aAAa,GAC9B,IAAI,CA8LN"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { COMMANDS, createSchemaEngine, } from '@eide/command-registry';
|
|
5
|
+
import { withErrorHandler } from '../lib/errors.js';
|
|
6
|
+
import { createClient } from '../lib/client.js';
|
|
7
|
+
import { formatOutput, formatList, timeAgo, success, } from '../lib/output.js';
|
|
8
|
+
import { parseInputData, confirmAction, isUUID } from '../lib/input.js';
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
function loadSchemaSDL() {
|
|
12
|
+
// Try monorepo path first (development)
|
|
13
|
+
const monorepoPath = resolve(__dirname, '../../../../graphql-core/schema.graphql');
|
|
14
|
+
try {
|
|
15
|
+
return readFileSync(monorepoPath, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Fall back to relative path from dist (might differ)
|
|
19
|
+
const altPath = resolve(__dirname, '../../../graphql-core/schema.graphql');
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(altPath, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error('Could not find schema.graphql. Ensure you are running from within the EIDE monorepo.');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extract the useful data from a GraphQL result, unwrapping list wrappers.
|
|
30
|
+
*/
|
|
31
|
+
function extractResult(result, operationName) {
|
|
32
|
+
const raw = result[operationName];
|
|
33
|
+
if (!raw || typeof raw !== 'object')
|
|
34
|
+
return { data: raw };
|
|
35
|
+
const obj = raw;
|
|
36
|
+
// Check for list wrappers: { items: [...], total } or { entityName: [...], total }
|
|
37
|
+
if ('total' in obj) {
|
|
38
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
39
|
+
if (key !== 'total' && key !== '__typename' && Array.isArray(val)) {
|
|
40
|
+
return { data: val, total: obj.total };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { data: raw };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert registry ColumnDef (with string format) to CLI ColumnDef (with format function).
|
|
48
|
+
*/
|
|
49
|
+
function toCliColumns(columns) {
|
|
50
|
+
if (!columns)
|
|
51
|
+
return undefined;
|
|
52
|
+
return columns.map((col) => {
|
|
53
|
+
const cliCol = {
|
|
54
|
+
key: col.key,
|
|
55
|
+
header: col.header,
|
|
56
|
+
width: col.width,
|
|
57
|
+
};
|
|
58
|
+
switch (col.format) {
|
|
59
|
+
case 'timeAgo':
|
|
60
|
+
cliCol.format = (v) => timeAgo(v);
|
|
61
|
+
break;
|
|
62
|
+
case 'boolean':
|
|
63
|
+
cliCol.format = (v) => (v ? 'yes' : '');
|
|
64
|
+
break;
|
|
65
|
+
case 'truncate':
|
|
66
|
+
cliCol.format = (v) => {
|
|
67
|
+
const str = String(v ?? '');
|
|
68
|
+
return str.length > 40 ? str.slice(0, 39) + '\u2026' : str;
|
|
69
|
+
};
|
|
70
|
+
break;
|
|
71
|
+
case 'join':
|
|
72
|
+
cliCol.format = (v) => Array.isArray(v) ? v.join(', ') : String(v ?? '');
|
|
73
|
+
break;
|
|
74
|
+
case 'bytes':
|
|
75
|
+
cliCol.format = (v) => {
|
|
76
|
+
const num = Number(v);
|
|
77
|
+
if (num < 1024)
|
|
78
|
+
return `${num} B`;
|
|
79
|
+
if (num < 1024 * 1024)
|
|
80
|
+
return `${(num / 1024).toFixed(1)} KB`;
|
|
81
|
+
return `${(num / (1024 * 1024)).toFixed(1)} MB`;
|
|
82
|
+
};
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return cliCol;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Register all declarative commands from the command registry.
|
|
90
|
+
*/
|
|
91
|
+
export function registerDynamicCommands(program, globalOpts) {
|
|
92
|
+
let engine;
|
|
93
|
+
try {
|
|
94
|
+
const sdl = loadSchemaSDL();
|
|
95
|
+
engine = createSchemaEngine(sdl);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
// If schema can't be loaded, skip dynamic command registration
|
|
99
|
+
// (special commands still work)
|
|
100
|
+
console.error(`Warning: Could not load schema for dynamic commands: ${err instanceof Error ? err.message : String(err)}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Group commands by group name
|
|
104
|
+
const groups = new Map();
|
|
105
|
+
for (const cmd of COMMANDS) {
|
|
106
|
+
if (!groups.has(cmd.group))
|
|
107
|
+
groups.set(cmd.group, []);
|
|
108
|
+
groups.get(cmd.group).push(cmd);
|
|
109
|
+
}
|
|
110
|
+
for (const [groupName, entries] of groups) {
|
|
111
|
+
const group = program.command(groupName).description(`Manage ${groupName}`);
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
let cmd = group.command(entry.name).description(entry.description);
|
|
114
|
+
// Add positional args
|
|
115
|
+
for (const pos of entry.positionalArgs ?? []) {
|
|
116
|
+
cmd = cmd.argument(`<${pos.name}>`, pos.description ?? pos.name);
|
|
117
|
+
}
|
|
118
|
+
// Add flags from schema args (excluding positional args and input args)
|
|
119
|
+
const schemaArgs = engine.getOperationArgs(entry.operation, entry.operationType);
|
|
120
|
+
for (const arg of schemaArgs) {
|
|
121
|
+
// Skip if already a positional arg
|
|
122
|
+
if (entry.positionalArgs?.some((p) => p.graphqlArg === arg.name))
|
|
123
|
+
continue;
|
|
124
|
+
// Skip if it's the input arg
|
|
125
|
+
if (entry.acceptsInput && arg.name === (entry.inputArgName ?? 'input'))
|
|
126
|
+
continue;
|
|
127
|
+
const reqStr = arg.required ? ' (required)' : '';
|
|
128
|
+
cmd = cmd.option(`--${arg.name} <value>`, `${arg.name}${reqStr}`);
|
|
129
|
+
}
|
|
130
|
+
// Add --data/--file for input commands
|
|
131
|
+
if (entry.acceptsInput) {
|
|
132
|
+
cmd = cmd.option('-d, --data <json>', 'Data as JSON');
|
|
133
|
+
cmd = cmd.option('-f, --file <path>', 'Read data from file');
|
|
134
|
+
}
|
|
135
|
+
// Add --confirm for destructive commands
|
|
136
|
+
if (entry.requiresConfirmation) {
|
|
137
|
+
cmd = cmd.option('--confirm', 'Skip confirmation prompt');
|
|
138
|
+
}
|
|
139
|
+
// Action handler
|
|
140
|
+
cmd.action(withErrorHandler(globalOpts, async (...actionArgs) => {
|
|
141
|
+
const opts = globalOpts();
|
|
142
|
+
const client = await createClient(opts);
|
|
143
|
+
// Build variables from positional args + flags
|
|
144
|
+
const variables = {};
|
|
145
|
+
// Map positional args
|
|
146
|
+
const positionals = entry.positionalArgs ?? [];
|
|
147
|
+
for (let i = 0; i < positionals.length; i++) {
|
|
148
|
+
const posDef = positionals[i];
|
|
149
|
+
const value = actionArgs[i];
|
|
150
|
+
if (value) {
|
|
151
|
+
variables[posDef.graphqlArg] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// The flags/options object is the last argument from Commander
|
|
155
|
+
const flags = (actionArgs[positionals.length] ?? {});
|
|
156
|
+
// Map named flags to variables (coerce types)
|
|
157
|
+
const rawFlags = {};
|
|
158
|
+
for (const [key, val] of Object.entries(flags)) {
|
|
159
|
+
if (key === 'data' || key === 'file' || key === 'confirm')
|
|
160
|
+
continue;
|
|
161
|
+
rawFlags[key] = String(val);
|
|
162
|
+
}
|
|
163
|
+
const coerced = engine.coerceArgs(entry.operation, entry.operationType, rawFlags);
|
|
164
|
+
Object.assign(variables, coerced);
|
|
165
|
+
// Handle input data (--data, --file, stdin)
|
|
166
|
+
if (entry.acceptsInput && (flags.data || flags.file)) {
|
|
167
|
+
const inputData = await parseInputData({
|
|
168
|
+
data: flags.data,
|
|
169
|
+
file: flags.file,
|
|
170
|
+
});
|
|
171
|
+
const argName = entry.inputArgName ?? 'input';
|
|
172
|
+
variables[argName] = inputData;
|
|
173
|
+
}
|
|
174
|
+
// Handle alternate get (by key/code instead of UUID)
|
|
175
|
+
if (entry.alternateGet &&
|
|
176
|
+
positionals.length > 0 &&
|
|
177
|
+
variables[positionals[0].graphqlArg]) {
|
|
178
|
+
const firstArgValue = String(variables[positionals[0].graphqlArg]);
|
|
179
|
+
if (!isUUID(firstArgValue)) {
|
|
180
|
+
// Use alternate operation
|
|
181
|
+
const altEntry = {
|
|
182
|
+
...entry,
|
|
183
|
+
operation: entry.alternateGet.operation,
|
|
184
|
+
positionalArgs: [
|
|
185
|
+
{
|
|
186
|
+
name: positionals[0].name,
|
|
187
|
+
graphqlArg: entry.alternateGet.argName,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
// Remap the variable
|
|
192
|
+
delete variables[positionals[0].graphqlArg];
|
|
193
|
+
variables[entry.alternateGet.argName] = firstArgValue;
|
|
194
|
+
const queryStr = engine.buildQuery(altEntry, variables);
|
|
195
|
+
const result = (await client.request(queryStr, variables));
|
|
196
|
+
const { data } = extractResult(result, altEntry.operation);
|
|
197
|
+
formatOutput(data, opts);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Confirmation prompt for destructive actions
|
|
202
|
+
if (entry.requiresConfirmation) {
|
|
203
|
+
const confirmed = await confirmAction(`${entry.description}?`, {
|
|
204
|
+
confirm: !!flags.confirm,
|
|
205
|
+
});
|
|
206
|
+
if (!confirmed) {
|
|
207
|
+
console.log('Aborted.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Build and execute query
|
|
212
|
+
const queryStr = engine.buildQuery(entry, variables);
|
|
213
|
+
const result = (await client.request(queryStr, variables));
|
|
214
|
+
const { data, total } = extractResult(result, entry.operation);
|
|
215
|
+
// Format output
|
|
216
|
+
if (entry.scalarResult) {
|
|
217
|
+
if (!(opts.json || opts.jsonl || opts.quiet)) {
|
|
218
|
+
if (entry.successMessage) {
|
|
219
|
+
success(entry.successMessage);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
formatOutput(data, opts);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (Array.isArray(data)) {
|
|
227
|
+
const cliColumns = toCliColumns(entry.columns);
|
|
228
|
+
formatList(data, opts, {
|
|
229
|
+
columns: cliColumns ?? autoColumns(data),
|
|
230
|
+
total,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
formatOutput(data, opts);
|
|
235
|
+
if (entry.successMessage &&
|
|
236
|
+
!(opts.json || opts.jsonl || opts.quiet)) {
|
|
237
|
+
success(entry.successMessage);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Auto-generate column definitions from the first item in an array.
|
|
246
|
+
*/
|
|
247
|
+
function autoColumns(items) {
|
|
248
|
+
if (items.length === 0)
|
|
249
|
+
return [];
|
|
250
|
+
const first = items[0];
|
|
251
|
+
return Object.keys(first)
|
|
252
|
+
.filter((k) => k !== '__typename')
|
|
253
|
+
.slice(0, 6)
|
|
254
|
+
.map((key) => ({
|
|
255
|
+
key,
|
|
256
|
+
header: key,
|
|
257
|
+
width: 20,
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
@@ -38,8 +38,8 @@ async function fetchSessionContext(apiUrl, accessToken) {
|
|
|
38
38
|
return data.sessionContext;
|
|
39
39
|
}
|
|
40
40
|
async function fetchApiKeys(apiUrl, accessToken, projectId, tenantId) {
|
|
41
|
-
const data = await gqlRequest(apiUrl, accessToken, `query { listApiKeys(includeInactive: false, limit: 100) {
|
|
42
|
-
return data.listApiKeys?.
|
|
41
|
+
const data = await gqlRequest(apiUrl, accessToken, `query { listApiKeys(includeInactive: false, limit: 100) { items { id name isActive } } }`, undefined, { 'x-tenant-id': tenantId, 'x-project-id': projectId });
|
|
42
|
+
return data.listApiKeys?.items ?? [];
|
|
43
43
|
}
|
|
44
44
|
async function createApiKey(apiUrl, accessToken, projectId, tenantId) {
|
|
45
45
|
const data = await gqlRequest(apiUrl, accessToken, `mutation($input: CreateApiKeyInput!) { createApiKey(input: $input) { apiKey { id name isActive } plainKey } }`, {
|