@trustgraph/config 0.0.1 → 0.0.2

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.
Files changed (2) hide show
  1. package/cli.js +385 -0
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -1,2 +1,387 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import * as p from '@clack/prompts';
4
+ import { writeFileSync } from 'fs';
5
+ import yaml from 'js-yaml';
6
+ import jsonata from 'jsonata';
7
+ import { parseArgs } from 'util';
8
+
9
+ const DEFAULT_API_BASE = 'https://config-svc.app.trustgraph.ai/api';
10
+
11
+ // Parse command line arguments
12
+ const { values: args } = parseArgs({
13
+ options: {
14
+ api: {
15
+ type: 'string',
16
+ short: 'a',
17
+ default: DEFAULT_API_BASE,
18
+ },
19
+ help: {
20
+ type: 'boolean',
21
+ short: 'h',
22
+ default: false,
23
+ },
24
+ },
25
+ });
26
+
27
+ if (args.help) {
28
+ console.log(`
29
+ TrustGraph Configuration CLI
30
+
31
+ Usage: tg-config [options]
32
+
33
+ Options:
34
+ -a, --api <url> API base URL (default: ${DEFAULT_API_BASE})
35
+ -h, --help Show this help message
36
+ `);
37
+ process.exit(0);
38
+ }
39
+
40
+ const apiBase = args.api.replace(/\/$/, '');
41
+
42
+ // Fetch helpers
43
+ const fetchYaml = async (endpoint) => {
44
+ const url = `${apiBase}${endpoint}`;
45
+ const response = await fetch(url);
46
+ if (!response.ok) {
47
+ throw new Error(`Failed to fetch ${url}: ${response.status}`);
48
+ }
49
+ return yaml.load(await response.text());
50
+ };
51
+
52
+ const fetchText = async (endpoint) => {
53
+ const url = `${apiBase}${endpoint}`;
54
+ const response = await fetch(url);
55
+ if (!response.ok) {
56
+ throw new Error(`Failed to fetch ${url}: ${response.status}`);
57
+ }
58
+ return response.text();
59
+ };
60
+
61
+ const fetchDoc = async (path) => {
62
+ const url = `${apiBase}/docs/${path}`;
63
+ try {
64
+ const response = await fetch(url);
65
+ if (!response.ok) {
66
+ return `*Documentation file not found: ${path}*`;
67
+ }
68
+ return response.text();
69
+ } catch {
70
+ return `*Documentation file not found: ${path}*`;
71
+ }
72
+ };
73
+
74
+ // Evaluate JSONata condition
75
+ const evaluateCondition = async (condition, state) => {
76
+ if (!condition) return true;
77
+ try {
78
+ const expr = jsonata(condition);
79
+ return Boolean(await expr.evaluate(state));
80
+ } catch {
81
+ return false;
82
+ }
83
+ };
84
+
85
+ // Find next step based on transitions
86
+ const findNextStep = async (step, state) => {
87
+ for (const transition of step.transitions || []) {
88
+ if (await evaluateCondition(transition.when, state)) {
89
+ return transition.next || null;
90
+ }
91
+ }
92
+ return null;
93
+ };
94
+
95
+ // Set nested value in state
96
+ const setValue = (state, key, value) => {
97
+ const parts = key.split('.');
98
+ let obj = state;
99
+ for (let i = 0; i < parts.length - 1; i++) {
100
+ if (!obj[parts[i]]) obj[parts[i]] = {};
101
+ obj = obj[parts[i]];
102
+ }
103
+ obj[parts[parts.length - 1]] = value;
104
+ };
105
+
106
+ // Prompt for a step based on input type
107
+ const promptStep = async (step) => {
108
+ const input = step.input || {};
109
+ const type = input.type || 'select';
110
+
111
+ if (type === 'select') {
112
+ const options = input.options.map(opt => ({
113
+ value: opt.value,
114
+ label: opt.label + (opt.recommended ? ' (Recommended)' : ''),
115
+ hint: opt.description,
116
+ }));
117
+ const defaultOpt = input.options.find(opt => opt.recommended);
118
+ return await p.select({
119
+ message: step.title,
120
+ options,
121
+ initialValue: defaultOpt?.value,
122
+ });
123
+ }
124
+
125
+ if (type === 'toggle') {
126
+ return await p.confirm({
127
+ message: step.title,
128
+ initialValue: input.default ?? false,
129
+ });
130
+ }
131
+
132
+ if (type === 'number') {
133
+ const result = await p.text({
134
+ message: step.title,
135
+ initialValue: String(input.default ?? ''),
136
+ validate: (val) => {
137
+ const num = parseInt(val, 10);
138
+ if (isNaN(num)) return 'Please enter a valid number';
139
+ if (input.min !== undefined && num < input.min) return `Value must be at least ${input.min}`;
140
+ if (input.max !== undefined && num > input.max) return `Value must be at most ${input.max}`;
141
+ },
142
+ });
143
+ return parseInt(result, 10);
144
+ }
145
+
146
+ if (type === 'text') {
147
+ return await p.text({
148
+ message: step.title,
149
+ initialValue: input.default ?? '',
150
+ placeholder: input.placeholder,
151
+ });
152
+ }
153
+
154
+ return null;
155
+ };
156
+
157
+ // Generate installation documentation
158
+ const generateDocs = async (state, docsManifest) => {
159
+ const { documentation } = docsManifest;
160
+ const { categories, instructions } = documentation;
161
+
162
+ // Build category map
163
+ const categoryMap = Object.fromEntries(categories.map(c => [c.id, c]));
164
+
165
+ // Evaluate conditions and collect matching instructions
166
+ const matching = [];
167
+ const seenIds = new Set();
168
+
169
+ for (const instruction of instructions) {
170
+ // Check condition
171
+ let matches = false;
172
+ if (instruction.always) {
173
+ matches = true;
174
+ } else if (instruction.when) {
175
+ matches = await evaluateCondition(instruction.when, state);
176
+ }
177
+
178
+ if (matches && !seenIds.has(instruction.id)) {
179
+ seenIds.add(instruction.id);
180
+ const cat = categoryMap[instruction.category];
181
+ matching.push({
182
+ ...instruction,
183
+ categoryPriority: cat?.priority ?? 99,
184
+ categoryTitle: cat?.title ?? instruction.category,
185
+ });
186
+ }
187
+ }
188
+
189
+ // Sort by category priority, then instruction priority
190
+ matching.sort((a, b) => {
191
+ if (a.categoryPriority !== b.categoryPriority) {
192
+ return a.categoryPriority - b.categoryPriority;
193
+ }
194
+ return (a.priority ?? 99) - (b.priority ?? 99);
195
+ });
196
+
197
+ // Group by category
198
+ const grouped = {};
199
+ for (const item of matching) {
200
+ if (!grouped[item.category]) {
201
+ grouped[item.category] = {
202
+ title: item.categoryTitle,
203
+ priority: item.categoryPriority,
204
+ items: [],
205
+ };
206
+ }
207
+ grouped[item.category].items.push(item);
208
+ }
209
+
210
+ // Build output
211
+ const output = [`# ${documentation.title}\n`];
212
+
213
+ for (const cat of Object.values(grouped).sort((a, b) => a.priority - b.priority)) {
214
+ output.push(`\n## ${cat.title}\n`);
215
+
216
+ for (const item of cat.items) {
217
+ if (item.goal) {
218
+ output.push(`\n### ${item.goal}\n`);
219
+ }
220
+ if (item.file) {
221
+ const content = await fetchDoc(item.file);
222
+ output.push(content);
223
+ }
224
+ }
225
+ }
226
+
227
+ return output.join('\n');
228
+ };
229
+
230
+ // Format state for review display
231
+ const formatState = (obj, prefix = '') => {
232
+ const lines = [];
233
+ for (const [key, value] of Object.entries(obj)) {
234
+ const fullKey = prefix ? `${prefix}.${key}` : key;
235
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
236
+ lines.push(...formatState(value, fullKey));
237
+ } else {
238
+ const displayValue = typeof value === 'boolean' ? (value ? 'Yes' : 'No') : String(value);
239
+ lines.push(` ${fullKey}: ${displayValue}`);
240
+ }
241
+ }
242
+ return lines;
243
+ };
244
+
245
+ // Main wizard
246
+ const main = async () => {
247
+ console.clear();
248
+
249
+ p.intro('TrustGraph Configuration');
250
+
251
+ // Load resources from API
252
+ const s = p.spinner();
253
+ s.start(`Loading from ${apiBase}...`);
254
+
255
+ let flowData, outputTemplate, docsManifest;
256
+ try {
257
+ flowData = await fetchYaml('/dialog-flow');
258
+ const outputTemplateText = await fetchText('/config-prepare');
259
+ outputTemplate = jsonata(outputTemplateText);
260
+ docsManifest = await fetchYaml('/docs-manifest');
261
+ s.stop('Resources loaded');
262
+ } catch (err) {
263
+ s.stop('Failed to load resources');
264
+ p.log.error(err.message);
265
+ process.exit(1);
266
+ }
267
+
268
+ p.log.info(flowData.flow.title);
269
+
270
+ const state = {};
271
+ const history = []; // Track human-readable question/answer pairs
272
+ let currentStepId = flowData.flow.start;
273
+
274
+ // Walk through steps
275
+ while (currentStepId) {
276
+ const step = flowData.steps[currentStepId];
277
+
278
+ if (!step) {
279
+ p.log.error(`Unknown step: ${currentStepId}`);
280
+ break;
281
+ }
282
+
283
+ // Review step
284
+ if (step.type === 'review') {
285
+ const lines = history.map(h => ` ${h.question} ${h.answer}`);
286
+ p.log.info('Configuration Summary:\n' + lines.join('\n'));
287
+
288
+ // Generate config
289
+ const s = p.spinner();
290
+ s.start('Generating configuration...');
291
+
292
+ const config = await outputTemplate.evaluate(state);
293
+
294
+ s.stop('Configuration generated');
295
+
296
+ // Prompt for filename
297
+ const filename = await p.text({
298
+ message: 'Save deployment package as:',
299
+ initialValue: 'deploy.zip',
300
+ });
301
+
302
+ if (p.isCancel(filename)) {
303
+ p.cancel('Cancelled');
304
+ process.exit(0);
305
+ }
306
+
307
+ const s2 = p.spinner();
308
+ s2.start('Downloading...');
309
+
310
+ try {
311
+ const response = await fetch(config.api_url, {
312
+ method: 'POST',
313
+ headers: { 'Content-Type': 'application/json' },
314
+ body: JSON.stringify(config.templates),
315
+ });
316
+
317
+ if (!response.ok) {
318
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
319
+ }
320
+
321
+ const buffer = await response.arrayBuffer();
322
+ writeFileSync(filename, Buffer.from(buffer));
323
+ s2.stop(`Saved to ${filename}`);
324
+ } catch (err) {
325
+ s2.stop('Download failed');
326
+ p.log.error(err.message);
327
+ }
328
+
329
+ // Generate documentation
330
+ const s3 = p.spinner();
331
+ s3.start('Generating installation guide...');
332
+
333
+ const docs = await generateDocs(state, docsManifest);
334
+
335
+ s3.stop('Installation guide generated');
336
+
337
+ // Prompt for docs filename
338
+ const docsFilename = await p.text({
339
+ message: 'Save installation guide as:',
340
+ initialValue: 'INSTALLATION.md',
341
+ });
342
+
343
+ if (p.isCancel(docsFilename)) {
344
+ p.cancel('Cancelled');
345
+ process.exit(0);
346
+ }
347
+
348
+ writeFileSync(docsFilename, docs);
349
+ p.log.success(`Saved to ${docsFilename}`);
350
+
351
+ break;
352
+ }
353
+
354
+ // Regular step - prompt user
355
+ const value = await promptStep(step);
356
+
357
+ if (p.isCancel(value)) {
358
+ p.cancel('Cancelled');
359
+ process.exit(0);
360
+ }
361
+
362
+ // Save to state
363
+ if (step.state_key) {
364
+ setValue(state, step.state_key, value);
365
+
366
+ // Record human-readable question/answer
367
+ const input = step.input || {};
368
+ let displayValue;
369
+ if (input.type === 'select') {
370
+ const opt = input.options.find(o => o.value === value);
371
+ displayValue = opt?.label ?? value;
372
+ } else if (input.type === 'toggle') {
373
+ displayValue = value ? 'Yes' : 'No';
374
+ } else {
375
+ displayValue = String(value);
376
+ }
377
+ history.push({ question: step.title, answer: displayValue });
378
+ }
379
+
380
+ // Find next step
381
+ currentStepId = await findNextStep(step, state);
382
+ }
383
+
384
+ p.outro('Done!');
385
+ };
386
+
387
+ main().catch(console.error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustgraph/config",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "TrustGraph Configuration CLI - Interactive terminal wizard",
5
5
  "type": "module",
6
6
  "main": "cli.js",