@gabaltech/cli 0.1.0
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/bin/gabal.js +37 -0
- package/bin/gabaltech.js +27 -0
- package/package.json +39 -0
- package/src/commands/api-keys.js +52 -0
- package/src/commands/blueprints.js +37 -0
- package/src/commands/config.js +46 -0
- package/src/commands/deploy.js +47 -0
- package/src/commands/docs.js +29 -0
- package/src/commands/generate.js +39 -0
- package/src/commands/init.js +27 -0
- package/src/commands/login.js +26 -0
- package/src/commands/review.js +41 -0
- package/src/commands/users.js +84 -0
- package/src/config.js +55 -0
- package/src/idp-client.js +68 -0
- package/src/utils/table.js +11 -0
package/bin/gabal.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
|
|
6
|
+
const configCmd = require('../src/commands/config');
|
|
7
|
+
const usersCmd = require('../src/commands/users');
|
|
8
|
+
const deployCmd = require('../src/commands/deploy');
|
|
9
|
+
const blueprintsCmd = require('../src/commands/blueprints');
|
|
10
|
+
const apiKeysCmd = require('../src/commands/api-keys');
|
|
11
|
+
const loginCmd = require('../src/commands/login');
|
|
12
|
+
const generateCmd = require('../src/commands/generate');
|
|
13
|
+
const reviewCmd = require('../src/commands/review');
|
|
14
|
+
const docsCmd = require('../src/commands/docs');
|
|
15
|
+
const initCmd = require('../src/commands/init');
|
|
16
|
+
|
|
17
|
+
const program = new Command();
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.name('gabal')
|
|
21
|
+
.description('Gabaltech platform CLI — design, generate, deploy, and manage AWS infrastructure')
|
|
22
|
+
.version('0.1.0')
|
|
23
|
+
.addCommand(loginCmd)
|
|
24
|
+
.addCommand(configCmd)
|
|
25
|
+
.addCommand(usersCmd)
|
|
26
|
+
.addCommand(deployCmd)
|
|
27
|
+
.addCommand(blueprintsCmd)
|
|
28
|
+
.addCommand(apiKeysCmd)
|
|
29
|
+
.addCommand(generateCmd)
|
|
30
|
+
.addCommand(reviewCmd)
|
|
31
|
+
.addCommand(docsCmd)
|
|
32
|
+
.addCommand(initCmd);
|
|
33
|
+
|
|
34
|
+
program.parseAsync(process.argv).catch(err => {
|
|
35
|
+
console.error('Error:', err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
package/bin/gabaltech.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
|
|
6
|
+
const configCmd = require('../src/commands/config');
|
|
7
|
+
const usersCmd = require('../src/commands/users');
|
|
8
|
+
const deployCmd = require('../src/commands/deploy');
|
|
9
|
+
const blueprintsCmd = require('../src/commands/blueprints');
|
|
10
|
+
const apiKeysCmd = require('../src/commands/api-keys');
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('gabaltech')
|
|
16
|
+
.description('Gabaltech platform CLI')
|
|
17
|
+
.version('0.1.0')
|
|
18
|
+
.addCommand(configCmd)
|
|
19
|
+
.addCommand(usersCmd)
|
|
20
|
+
.addCommand(deployCmd)
|
|
21
|
+
.addCommand(blueprintsCmd)
|
|
22
|
+
.addCommand(apiKeysCmd);
|
|
23
|
+
|
|
24
|
+
program.parseAsync(process.argv).catch(err => {
|
|
25
|
+
console.error('Error:', err.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gabaltech/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gabaltech platform CLI — design, generate, deploy, and manage AWS infrastructure",
|
|
5
|
+
"bin": {
|
|
6
|
+
"gabal": "./bin/gabal.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "jest --forceExit",
|
|
12
|
+
"lint": "eslint src/ bin/"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@aws-sdk/client-cognito-identity-provider": "^3.0.0",
|
|
16
|
+
"chalk": "^4.1.2",
|
|
17
|
+
"cli-table3": "^0.6.5",
|
|
18
|
+
"commander": "^12.0.0",
|
|
19
|
+
"js-yaml": "^4.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"jest": "^29.0.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://gitlab.com/gabaltech1/cli.git"
|
|
30
|
+
},
|
|
31
|
+
"license": "UNLICENSED",
|
|
32
|
+
"files": [
|
|
33
|
+
"bin/",
|
|
34
|
+
"src/"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const idp = require('../idp-client');
|
|
5
|
+
const { printTable } = require('../utils/table');
|
|
6
|
+
|
|
7
|
+
const apiKeysCmd = new Command('api-keys').description('Manage IDP API keys');
|
|
8
|
+
|
|
9
|
+
apiKeysCmd
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List all API keys (hashes not shown)')
|
|
12
|
+
.option('--json', 'Output raw JSON')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const { keys } = await idp.listApiKeys();
|
|
15
|
+
if (opts.json) { console.log(JSON.stringify(keys, null, 2)); return; }
|
|
16
|
+
printTable(
|
|
17
|
+
['ID', 'Name', 'Scopes', 'Created By', 'Last Used'],
|
|
18
|
+
keys.map(k => [
|
|
19
|
+
k.keyId.slice(0, 8),
|
|
20
|
+
k.name,
|
|
21
|
+
(k.scopes ?? []).join(', '),
|
|
22
|
+
k.createdBy,
|
|
23
|
+
k.lastUsed ? new Date(k.lastUsed).toLocaleString() : 'never',
|
|
24
|
+
]),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
apiKeysCmd
|
|
29
|
+
.command('create <name>')
|
|
30
|
+
.description('Create a new API key — the key is shown once, save it immediately')
|
|
31
|
+
.option('--scope <scope>', 'Add scope (repeatable)', (v, acc) => [...acc, v], [])
|
|
32
|
+
.action(async (name, opts) => {
|
|
33
|
+
const scopes = opts.scope.length ? opts.scope : ['admin'];
|
|
34
|
+
const result = await idp.createApiKey(name, scopes);
|
|
35
|
+
console.log('\nAPI key created. Save this — it will not be shown again:\n');
|
|
36
|
+
console.log(` ${result.key}\n`);
|
|
37
|
+
console.log(` ID: ${result.keyId}`);
|
|
38
|
+
console.log(` Name: ${result.name}`);
|
|
39
|
+
console.log(` Scopes: ${result.scopes.join(', ')}`);
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(`Run to store locally: gabaltech config set api-key ${result.key}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
apiKeysCmd
|
|
45
|
+
.command('revoke <keyId>')
|
|
46
|
+
.description('Revoke an API key by ID')
|
|
47
|
+
.action(async (keyId) => {
|
|
48
|
+
await idp.revokeApiKey(keyId);
|
|
49
|
+
console.log(`Revoked: ${keyId}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
module.exports = apiKeysCmd;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const idp = require('../idp-client');
|
|
5
|
+
const { printTable } = require('../utils/table');
|
|
6
|
+
|
|
7
|
+
const blueprintsCmd = new Command('blueprints').description('Manage and provision service blueprints');
|
|
8
|
+
|
|
9
|
+
blueprintsCmd
|
|
10
|
+
.command('list')
|
|
11
|
+
.description('List available blueprint catalogs')
|
|
12
|
+
.option('--json', 'Output raw JSON')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const { catalogs } = await idp.listCatalogs();
|
|
15
|
+
if (opts.json) { console.log(JSON.stringify(catalogs, null, 2)); return; }
|
|
16
|
+
printTable(
|
|
17
|
+
['ID', 'Name', 'Description'],
|
|
18
|
+
catalogs.map(c => [c.id, c.name, c.description ?? '']),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
blueprintsCmd
|
|
23
|
+
.command('provision <catalogId> <serviceName> <environment>')
|
|
24
|
+
.description('Provision a new service from a blueprint (e.g. dynamodb my-table dev)')
|
|
25
|
+
.option('--field <kv>', 'Field value in key=value format (repeatable)', (v, acc) => [...acc, v], [])
|
|
26
|
+
.option('--json', 'Output raw JSON')
|
|
27
|
+
.action(async (catalogId, serviceName, environment, opts) => {
|
|
28
|
+
const fields = Object.fromEntries(opts.field.map(kv => {
|
|
29
|
+
const idx = kv.indexOf('=');
|
|
30
|
+
return [kv.slice(0, idx), kv.slice(idx + 1)];
|
|
31
|
+
}));
|
|
32
|
+
const { deployment } = await idp.provision(catalogId, serviceName, environment, fields);
|
|
33
|
+
if (opts.json) { console.log(JSON.stringify(deployment, null, 2)); return; }
|
|
34
|
+
console.log(`Provisioned: ${deployment?.deploymentId ?? '?'} [${deployment?.status ?? '?'}]`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
module.exports = blueprintsCmd;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const cfg = require('../config');
|
|
5
|
+
|
|
6
|
+
const configCmd = new Command('config')
|
|
7
|
+
.description('Manage CLI configuration (~/.gabaltech/config.json)');
|
|
8
|
+
|
|
9
|
+
configCmd
|
|
10
|
+
.command('set <key> <value>')
|
|
11
|
+
.description('Set a config value')
|
|
12
|
+
.action((key, value) => {
|
|
13
|
+
cfg.set(key, value);
|
|
14
|
+
console.log(`Set ${key}`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
configCmd
|
|
18
|
+
.command('get <key>')
|
|
19
|
+
.description('Get a config value')
|
|
20
|
+
.action((key) => {
|
|
21
|
+
const v = cfg.get(key);
|
|
22
|
+
if (v === undefined) { console.error(`Key not found: ${key}`); process.exit(1); }
|
|
23
|
+
console.log(v);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
configCmd
|
|
27
|
+
.command('unset <key>')
|
|
28
|
+
.description('Remove a config value')
|
|
29
|
+
.action((key) => {
|
|
30
|
+
cfg.unset(key);
|
|
31
|
+
console.log(`Unset ${key}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
configCmd
|
|
35
|
+
.command('list')
|
|
36
|
+
.description('Show all config values')
|
|
37
|
+
.action(() => {
|
|
38
|
+
const data = cfg.load();
|
|
39
|
+
if (!Object.keys(data).length) { console.log('No config set.'); return; }
|
|
40
|
+
for (const [k, v] of Object.entries(data)) {
|
|
41
|
+
const display = k === 'api-key' ? v.slice(0, 12) + '...' : v;
|
|
42
|
+
console.log(`${k}=${display}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
module.exports = configCmd;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const idp = require('../idp-client');
|
|
5
|
+
const { printTable } = require('../utils/table');
|
|
6
|
+
|
|
7
|
+
const deployCmd = new Command('deploy').description('Trigger and list deployments');
|
|
8
|
+
|
|
9
|
+
deployCmd
|
|
10
|
+
.command('trigger <service> <environment>')
|
|
11
|
+
.description('Trigger a deployment (e.g. website prod)')
|
|
12
|
+
.option('--version <tag>', 'Image tag / version', 'latest')
|
|
13
|
+
.option('--reason <reason>', 'Reason for deployment', 'CLI trigger')
|
|
14
|
+
.action(async (service, environment, opts) => {
|
|
15
|
+
const { deployment } = await idp.triggerDeploy(service, environment, opts.version, opts.reason);
|
|
16
|
+
console.log(`Deployment queued: ${deployment?.deploymentId ?? '?'} [${deployment?.status ?? '?'}]`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
deployCmd
|
|
20
|
+
.command('list')
|
|
21
|
+
.description('List recent deployments')
|
|
22
|
+
.option('--service <service>', 'Filter by service')
|
|
23
|
+
.option('--env <env>', 'Filter by environment')
|
|
24
|
+
.option('--limit <n>', 'Max results', '20')
|
|
25
|
+
.option('--json', 'Output raw JSON')
|
|
26
|
+
.action(async (opts) => {
|
|
27
|
+
const { deployments } = await idp.listDeployments({
|
|
28
|
+
service: opts.service,
|
|
29
|
+
environment: opts.env,
|
|
30
|
+
limit: opts.limit,
|
|
31
|
+
});
|
|
32
|
+
if (opts.json) { console.log(JSON.stringify(deployments, null, 2)); return; }
|
|
33
|
+
printTable(
|
|
34
|
+
['ID', 'Service', 'Env', 'Version', 'Status', 'By', 'At'],
|
|
35
|
+
deployments.map(d => [
|
|
36
|
+
(d.deploymentId ?? '').slice(0, 8),
|
|
37
|
+
d.service ?? '',
|
|
38
|
+
d.environment ?? '',
|
|
39
|
+
d.version ?? '',
|
|
40
|
+
d.status ?? '',
|
|
41
|
+
d.requestedBy ?? '',
|
|
42
|
+
d.requestedAt ? new Date(d.requestedAt).toLocaleString() : '',
|
|
43
|
+
]),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
module.exports = deployCmd;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const idp = require('../idp-client');
|
|
7
|
+
|
|
8
|
+
const docsCmd = new Command('docs').description('Generate architecture docs from a design');
|
|
9
|
+
|
|
10
|
+
docsCmd
|
|
11
|
+
.option('--design <designId>', 'Design ID from the IDP')
|
|
12
|
+
.option('--output <dir>', 'Output directory', './gabaltech-docs')
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
if (!opts.design) {
|
|
15
|
+
console.error('Error: --design <designId> is required');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { docs } = await idp.generateDocs(opts.design);
|
|
20
|
+
|
|
21
|
+
const outDir = path.resolve(opts.output);
|
|
22
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(outDir, 'architecture.md'), docs.architecture ?? '', 'utf8');
|
|
24
|
+
fs.writeFileSync(path.join(outDir, 'runbook.md'), docs.runbook ?? '', 'utf8');
|
|
25
|
+
|
|
26
|
+
console.log(`Documentation generated → ${opts.output}/`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
module.exports = docsCmd;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const idp = require('../idp-client');
|
|
7
|
+
|
|
8
|
+
const generateCmd = new Command('generate').description('Generate Terraform from a design or prompt');
|
|
9
|
+
|
|
10
|
+
generateCmd
|
|
11
|
+
.option('--design <designId>', 'Design ID from the IDP')
|
|
12
|
+
.option('--prompt <text>', 'Describe your architecture in plain text')
|
|
13
|
+
.option('--output <dir>', 'Output directory', './gabaltech-infra')
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
if (!opts.design && !opts.prompt) {
|
|
16
|
+
console.error('Error: provide --design <id> or --prompt "<text>"');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let payload;
|
|
21
|
+
if (opts.design) {
|
|
22
|
+
const design = await idp.getDesign(opts.design);
|
|
23
|
+
payload = { design };
|
|
24
|
+
} else {
|
|
25
|
+
payload = { prompt: opts.prompt };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { terraform } = await idp.generateTerraform(payload);
|
|
29
|
+
|
|
30
|
+
const outDir = path.resolve(opts.output);
|
|
31
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(path.join(outDir, 'main.tf'), terraform.main ?? '', 'utf8');
|
|
33
|
+
fs.writeFileSync(path.join(outDir, 'variables.tf'), terraform.variables ?? '', 'utf8');
|
|
34
|
+
fs.writeFileSync(path.join(outDir, 'outputs.tf'), terraform.outputs ?? '', 'utf8');
|
|
35
|
+
|
|
36
|
+
console.log(`Terraform generated → ${opts.output}/`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
module.exports = generateCmd;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const cfg = require('../config');
|
|
7
|
+
|
|
8
|
+
const initCmd = new Command('init').description('Scaffold a new Gabaltech project');
|
|
9
|
+
|
|
10
|
+
initCmd
|
|
11
|
+
.option('--name <project>', 'Project name', 'my-project')
|
|
12
|
+
.option('--type <type>', 'Project type: serverless|container|platform', 'container')
|
|
13
|
+
.action((opts) => {
|
|
14
|
+
const root = path.resolve(opts.name);
|
|
15
|
+
fs.mkdirSync(path.join(root, 'infra'), { recursive: true });
|
|
16
|
+
fs.mkdirSync(path.join(root, 'docs'), { recursive: true });
|
|
17
|
+
fs.writeFileSync(path.join(root, 'infra', '.gitkeep'), '', 'utf8');
|
|
18
|
+
fs.writeFileSync(path.join(root, 'docs', '.gitkeep'), '', 'utf8');
|
|
19
|
+
fs.writeFileSync(
|
|
20
|
+
path.join(root, 'gabaltech.json'),
|
|
21
|
+
JSON.stringify({ name: opts.name, type: opts.type, idpUrl: cfg.getIdpUrl() }, null, 2) + '\n',
|
|
22
|
+
'utf8',
|
|
23
|
+
);
|
|
24
|
+
console.log(`Project scaffolded → ./${opts.name}/`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
module.exports = initCmd;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const cfg = require('../config');
|
|
5
|
+
|
|
6
|
+
const loginCmd = new Command('login').description('Authenticate with the Gabaltech IDP');
|
|
7
|
+
|
|
8
|
+
loginCmd
|
|
9
|
+
.option('--token <api-key>', 'API key to save')
|
|
10
|
+
.option('--url <url>', 'IDP URL (default: https://idp.gabaltech.co.uk)')
|
|
11
|
+
.action((opts) => {
|
|
12
|
+
if (opts.url) {
|
|
13
|
+
cfg.set('idp-url', opts.url);
|
|
14
|
+
}
|
|
15
|
+
if (opts.token) {
|
|
16
|
+
cfg.set('api-key', opts.token);
|
|
17
|
+
console.log('Logged in. API key saved.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log('To authenticate:');
|
|
21
|
+
console.log(' 1. Open https://idp.gabaltech.co.uk/admin/api-keys');
|
|
22
|
+
console.log(' 2. Create a new API key with scope: cli');
|
|
23
|
+
console.log(' 3. Run: gabal login --token <your-api-key>');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
module.exports = loginCmd;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const idp = require('../idp-client');
|
|
7
|
+
|
|
8
|
+
const SEVERITY_ORDER = { CRITICAL: 0, WARNING: 1, INFO: 2 };
|
|
9
|
+
|
|
10
|
+
const reviewCmd = new Command('review').description('AI review of Terraform files');
|
|
11
|
+
|
|
12
|
+
reviewCmd
|
|
13
|
+
.option('--file <path>', 'Single .tf file to review')
|
|
14
|
+
.option('--dir <path>', 'Directory of .tf files to review')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
if (!opts.file && !opts.dir) {
|
|
17
|
+
console.error('Error: provide --file <path> or --dir <path>');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let content;
|
|
22
|
+
if (opts.file) {
|
|
23
|
+
content = fs.readFileSync(path.resolve(opts.file), 'utf8');
|
|
24
|
+
} else {
|
|
25
|
+
const dir = path.resolve(opts.dir);
|
|
26
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.tf'));
|
|
27
|
+
if (!files.length) { console.error('No .tf files found in', dir); process.exit(1); }
|
|
28
|
+
content = files.map(f => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { findings } = await idp.reviewTerraform(content);
|
|
32
|
+
|
|
33
|
+
if (!findings?.length) { console.log('No findings.'); return; }
|
|
34
|
+
|
|
35
|
+
findings
|
|
36
|
+
.slice()
|
|
37
|
+
.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99))
|
|
38
|
+
.forEach(f => console.log(`[${f.severity ?? 'INFO'}] ${f.message ?? f}`));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
module.exports = reviewCmd;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
const idp = require('../idp-client');
|
|
7
|
+
const { printTable } = require('../utils/table');
|
|
8
|
+
|
|
9
|
+
const usersCmd = new Command('users').description('Manage Cognito users via IDP API');
|
|
10
|
+
|
|
11
|
+
usersCmd
|
|
12
|
+
.command('list')
|
|
13
|
+
.description('List all users')
|
|
14
|
+
.option('--json', 'Output raw JSON')
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
const { users } = await idp.listUsers();
|
|
17
|
+
if (opts.json) { console.log(JSON.stringify(users, null, 2)); return; }
|
|
18
|
+
printTable(
|
|
19
|
+
['Email', 'Name', 'Status', 'Enabled'],
|
|
20
|
+
users.map(u => [u.email, u.name || '', u.status, u.enabled ? 'yes' : 'no']),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
usersCmd
|
|
25
|
+
.command('invite <email>')
|
|
26
|
+
.description('Invite a new user')
|
|
27
|
+
.option('--name <name>', 'Display name')
|
|
28
|
+
.option('--admin', 'Make user an approver')
|
|
29
|
+
.action(async (email, opts) => {
|
|
30
|
+
const result = await idp.inviteUser(email, opts.name, !!opts.admin);
|
|
31
|
+
console.log(`Invited: ${result.user?.username ?? email}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
usersCmd
|
|
35
|
+
.command('delete <username>')
|
|
36
|
+
.description('Delete a user by username')
|
|
37
|
+
.action(async (username) => {
|
|
38
|
+
await idp.deleteUser(username);
|
|
39
|
+
console.log(`Deleted: ${username}`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
usersCmd
|
|
43
|
+
.command('set-admin <username>')
|
|
44
|
+
.description('Grant or revoke admin (approver) role')
|
|
45
|
+
.option('--revoke', 'Remove admin role instead')
|
|
46
|
+
.action(async (username, opts) => {
|
|
47
|
+
const is_admin = !opts.revoke;
|
|
48
|
+
await idp.patchUser(username, { is_admin });
|
|
49
|
+
console.log(`${is_admin ? 'Granted' : 'Revoked'} admin for ${username}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
usersCmd
|
|
53
|
+
.command('sync')
|
|
54
|
+
.description('Sync users from a YAML or JSON file')
|
|
55
|
+
.requiredOption('--file <path>', 'Path to users file')
|
|
56
|
+
.option('--dry-run', 'Show what would happen without making changes')
|
|
57
|
+
.option('--json', 'Output raw JSON result')
|
|
58
|
+
.action(async (opts) => {
|
|
59
|
+
const raw = fs.readFileSync(opts.file, 'utf8');
|
|
60
|
+
const data = opts.file.endsWith('.json') ? JSON.parse(raw) : yaml.load(raw);
|
|
61
|
+
const users = Array.isArray(data) ? data : data.users;
|
|
62
|
+
|
|
63
|
+
if (!users?.length) { console.error('No users found in file'); process.exit(1); }
|
|
64
|
+
|
|
65
|
+
if (opts.dryRun) {
|
|
66
|
+
console.log(`Dry run — would sync ${users.length} users:`);
|
|
67
|
+
for (const u of users) console.log(` ${u.email}${u.is_admin ? ' [admin]' : ''}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await idp.syncUsers(users);
|
|
72
|
+
|
|
73
|
+
if (opts.json) { console.log(JSON.stringify(result, null, 2)); return; }
|
|
74
|
+
if (result.invited.length) console.log(`Invited (${result.invited.length}):`, result.invited.map(u => u.email).join(', '));
|
|
75
|
+
if (result.updated.length) console.log(`Updated (${result.updated.length}):`, result.updated.map(u => u.email).join(', '));
|
|
76
|
+
if (result.skipped.length) console.log(`Skipped (${result.skipped.length}):`, result.skipped.map(u => u.email).join(', '));
|
|
77
|
+
if (result.errors.length) {
|
|
78
|
+
console.error(`Errors (${result.errors.length}):`);
|
|
79
|
+
for (const e of result.errors) console.error(` ${e.email}: ${e.error}`);
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
module.exports = usersCmd;
|
package/src/config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.gabaltech');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
function load() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function save(data) {
|
|
19
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function get(key) {
|
|
24
|
+
return load()[key];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function set(key, value) {
|
|
28
|
+
const cfg = load();
|
|
29
|
+
cfg[key] = value;
|
|
30
|
+
save(cfg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function unset(key) {
|
|
34
|
+
const cfg = load();
|
|
35
|
+
delete cfg[key];
|
|
36
|
+
save(cfg);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getIdpUrl() {
|
|
40
|
+
return process.env.GABALTECH_IDP_URL || get('idp-url') || 'https://idp.gabaltech.co.uk';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getApiKey() {
|
|
44
|
+
return process.env.GABALTECH_API_KEY || get('api-key');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getAwsProfile() {
|
|
48
|
+
return process.env.AWS_PROFILE || get('aws-profile') || 'gabaltech';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getCognitoPoolId() {
|
|
52
|
+
return process.env.COGNITO_USER_POOL_ID || get('cognito-pool-id');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { load, save, get, set, unset, getIdpUrl, getApiKey, getAwsProfile, getCognitoPoolId };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cfg = require('./config');
|
|
4
|
+
|
|
5
|
+
async function request(method, path, body) {
|
|
6
|
+
const apiKey = cfg.getApiKey();
|
|
7
|
+
if (!apiKey) throw new Error('No API key configured. Run: gabaltech config set api-key <key>');
|
|
8
|
+
|
|
9
|
+
const url = `${cfg.getIdpUrl()}${path}`;
|
|
10
|
+
const headers = {
|
|
11
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
'Accept': 'application/json',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const res = await fetch(url, {
|
|
17
|
+
method,
|
|
18
|
+
headers,
|
|
19
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const text = await res.text();
|
|
23
|
+
let data;
|
|
24
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
throw new Error(data.error || `HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const idp = {
|
|
33
|
+
// Users
|
|
34
|
+
listUsers: () => request('GET', '/api/admin/users'),
|
|
35
|
+
inviteUser: (email, name, is_admin) => request('POST', '/api/admin/users/invite', { email, name, is_admin }),
|
|
36
|
+
syncUsers: (users) => request('POST', '/api/admin/users/sync', { users }),
|
|
37
|
+
deleteUser: (username) => request('DELETE', `/api/admin/users/${encodeURIComponent(username)}`),
|
|
38
|
+
patchUser: (username, patch) => request('PATCH', `/api/admin/users/${encodeURIComponent(username)}`, patch),
|
|
39
|
+
|
|
40
|
+
// API keys
|
|
41
|
+
listApiKeys: () => request('GET', '/api/admin/api-keys'),
|
|
42
|
+
createApiKey: (name, scopes) => request('POST', '/api/admin/api-keys', { name, scopes }),
|
|
43
|
+
revokeApiKey: (keyId) => request('DELETE', `/api/admin/api-keys/${keyId}`),
|
|
44
|
+
|
|
45
|
+
// Deployments
|
|
46
|
+
listDeployments: (opts = {}) => {
|
|
47
|
+
const qs = new URLSearchParams(Object.entries(opts).filter(([, v]) => v)).toString();
|
|
48
|
+
return request('GET', `/api/deployments${qs ? '?' + qs : ''}`);
|
|
49
|
+
},
|
|
50
|
+
triggerDeploy: (service, environment, version, reason) =>
|
|
51
|
+
request('POST', '/api/deployments', { service, environment, version, reason }),
|
|
52
|
+
|
|
53
|
+
// Blueprints
|
|
54
|
+
listCatalogs: () => request('GET', '/api/catalogs'),
|
|
55
|
+
provision: (catalogId, serviceName, environment, fields) =>
|
|
56
|
+
request('POST', '/api/blueprints/provision', { catalogId, serviceName, environment, fields }),
|
|
57
|
+
|
|
58
|
+
// Auth
|
|
59
|
+
verifyApiKey: () => request('GET', '/api/auth/verify'),
|
|
60
|
+
|
|
61
|
+
// Designs + generation
|
|
62
|
+
getDesign: (id) => request('GET', `/api/designs/${encodeURIComponent(id)}`),
|
|
63
|
+
generateTerraform: (payload) => request('POST', '/api/generate/terraform', payload),
|
|
64
|
+
reviewTerraform: (content) => request('POST', '/api/review/terraform', { content }),
|
|
65
|
+
generateDocs: (designId) => request('POST', '/api/generate/docs', { designId }),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
module.exports = idp;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Table = require('cli-table3');
|
|
4
|
+
|
|
5
|
+
function printTable(headers, rows) {
|
|
6
|
+
const t = new Table({ head: headers, style: { head: ['cyan'] } });
|
|
7
|
+
for (const row of rows) t.push(row);
|
|
8
|
+
console.log(t.toString());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { printTable };
|