@hyperdrive.bot/sign-cli 0.1.3
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/run.js +5 -0
- package/dist/commands/api.d.ts +28 -0
- package/dist/commands/api.js +82 -0
- package/dist/commands/modules/list.d.ts +16 -0
- package/dist/commands/modules/list.js +48 -0
- package/dist/hooks/command-not-found.d.ts +18 -0
- package/dist/hooks/command-not-found.js +129 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/manifest.d.ts +50 -0
- package/dist/lib/manifest.js +109 -0
- package/dist/lib/request.d.ts +26 -0
- package/dist/lib/request.js +76 -0
- package/dist/lib/sign-api.d.ts +26 -0
- package/dist/lib/sign-api.js +49 -0
- package/oclif.manifest.json +124 -0
- package/package.json +66 -0
package/bin/run.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sign api <METHOD> <path> [--body]
|
|
3
|
+
*
|
|
4
|
+
* Raw escape hatch — call any API endpoint directly.
|
|
5
|
+
* Bypasses manifest resolution for 100% endpoint coverage.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* sign api GET /folders
|
|
9
|
+
* sign api POST /documents --body '{"name":"test"}'
|
|
10
|
+
* sign api DELETE /folders/abc-123
|
|
11
|
+
*/
|
|
12
|
+
import { Command } from '@oclif/core';
|
|
13
|
+
export default class Api extends Command {
|
|
14
|
+
static description: string;
|
|
15
|
+
static examples: string[];
|
|
16
|
+
static args: {
|
|
17
|
+
method: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
18
|
+
path: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
19
|
+
};
|
|
20
|
+
static flags: {
|
|
21
|
+
body: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
|
+
module: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
|
+
table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
25
|
+
raw: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
26
|
+
};
|
|
27
|
+
run(): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sign api <METHOD> <path> [--body]
|
|
3
|
+
*
|
|
4
|
+
* Raw escape hatch — call any API endpoint directly.
|
|
5
|
+
* Bypasses manifest resolution for 100% endpoint coverage.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* sign api GET /folders
|
|
9
|
+
* sign api POST /documents --body '{"name":"test"}'
|
|
10
|
+
* sign api DELETE /folders/abc-123
|
|
11
|
+
*/
|
|
12
|
+
import { Command, Args, Flags } from '@oclif/core';
|
|
13
|
+
import { createSignClient } from '../lib/sign-api.js';
|
|
14
|
+
import { formatOutput } from '../lib/request.js';
|
|
15
|
+
export default class Api extends Command {
|
|
16
|
+
static description = 'Execute a raw API request (escape hatch)';
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> api GET /folders',
|
|
19
|
+
'<%= config.bin %> api POST /documents --body \'{"name":"My Doc"}\'',
|
|
20
|
+
'<%= config.bin %> api DELETE /folders/abc-123',
|
|
21
|
+
];
|
|
22
|
+
static args = {
|
|
23
|
+
method: Args.string({
|
|
24
|
+
description: 'HTTP method',
|
|
25
|
+
required: true,
|
|
26
|
+
options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
27
|
+
}),
|
|
28
|
+
path: Args.string({
|
|
29
|
+
description: 'API path (e.g., /folders, /documents/abc-123)',
|
|
30
|
+
required: true,
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
static flags = {
|
|
34
|
+
body: Flags.string({ description: 'Request body (JSON string)', char: 'b' }),
|
|
35
|
+
input: Flags.string({ description: 'Read body from file', char: 'i' }),
|
|
36
|
+
module: Flags.string({ description: 'Target module (for API Gateway routing)', char: 'm' }),
|
|
37
|
+
table: Flags.boolean({ description: 'Format output as table' }),
|
|
38
|
+
raw: Flags.boolean({ description: 'Raw output (no formatting)' }),
|
|
39
|
+
};
|
|
40
|
+
async run() {
|
|
41
|
+
const { args, flags } = await this.parse(Api);
|
|
42
|
+
const { method, path } = args;
|
|
43
|
+
try {
|
|
44
|
+
const client = createSignClient();
|
|
45
|
+
// If a module flag is provided, resolve from manifest for correct API Gateway
|
|
46
|
+
let fullUrl;
|
|
47
|
+
if (flags.module) {
|
|
48
|
+
const { getManifest } = await import('../lib/manifest.js');
|
|
49
|
+
const manifest = await getManifest(client);
|
|
50
|
+
const resource = Object.values(manifest.resources).find(r => r.module === flags.module);
|
|
51
|
+
if (resource) {
|
|
52
|
+
fullUrl = `${resource.apiGateway}${path}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Parse body
|
|
56
|
+
let body;
|
|
57
|
+
if (flags.body) {
|
|
58
|
+
body = JSON.parse(flags.body);
|
|
59
|
+
}
|
|
60
|
+
else if (flags.input) {
|
|
61
|
+
const { readFile } = await import('node:fs/promises');
|
|
62
|
+
body = JSON.parse(await readFile(flags.input, 'utf-8'));
|
|
63
|
+
}
|
|
64
|
+
let response;
|
|
65
|
+
if (fullUrl) {
|
|
66
|
+
response = await client.request(method, fullUrl, body);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Use the primary API URL (from stored credentials)
|
|
70
|
+
response = await client.get(path);
|
|
71
|
+
}
|
|
72
|
+
this.log(formatOutput(response, flags));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const err = error;
|
|
76
|
+
if (err.message.includes('Not authenticated') || err.message.includes('auth login')) {
|
|
77
|
+
this.error('Not authenticated. Run "sign auth login" first.');
|
|
78
|
+
}
|
|
79
|
+
this.error(err.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sign modules list
|
|
3
|
+
*
|
|
4
|
+
* Displays all discoverable API resources and their available actions.
|
|
5
|
+
* Fetches the manifest from the backend (or cache).
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from '@oclif/core';
|
|
8
|
+
export default class ModulesList extends Command {
|
|
9
|
+
static description: string;
|
|
10
|
+
static examples: string[];
|
|
11
|
+
static flags: {
|
|
12
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
refresh: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
};
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sign modules list
|
|
3
|
+
*
|
|
4
|
+
* Displays all discoverable API resources and their available actions.
|
|
5
|
+
* Fetches the manifest from the backend (or cache).
|
|
6
|
+
*/
|
|
7
|
+
import { Command, Flags } from '@oclif/core';
|
|
8
|
+
import { getManifest, listResources } from '../../lib/manifest.js';
|
|
9
|
+
import { createSignClient } from '../../lib/sign-api.js';
|
|
10
|
+
export default class ModulesList extends Command {
|
|
11
|
+
static description = 'List all available API resources and actions';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> modules list',
|
|
14
|
+
'<%= config.bin %> modules list --json',
|
|
15
|
+
'<%= config.bin %> modules list --refresh',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
json: Flags.boolean({ description: 'Output as JSON' }),
|
|
19
|
+
refresh: Flags.boolean({ description: 'Force refresh the manifest cache' }),
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { flags } = await this.parse(ModulesList);
|
|
23
|
+
try {
|
|
24
|
+
const client = createSignClient();
|
|
25
|
+
const manifest = await getManifest(client, flags.refresh);
|
|
26
|
+
const resources = listResources(manifest);
|
|
27
|
+
if (flags.json) {
|
|
28
|
+
this.log(JSON.stringify(resources, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.log(`\n Sign CLI — ${resources.length} resources discovered (${manifest.stage}/${manifest.region})\n`);
|
|
32
|
+
for (const res of resources) {
|
|
33
|
+
this.log(` ${res.name} (${res.module}) — ${res.actionCount} actions`);
|
|
34
|
+
this.log(` ${res.actions.join(', ')}`);
|
|
35
|
+
this.log('');
|
|
36
|
+
}
|
|
37
|
+
this.log(` Run "sign <resource> <action>" to execute.`);
|
|
38
|
+
this.log(` Run "sign <resource>" to see action details.\n`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const err = error;
|
|
42
|
+
if (err.message.includes('Not authenticated') || err.message.includes('auth login')) {
|
|
43
|
+
this.error('Not authenticated. Run "sign auth login" first.');
|
|
44
|
+
}
|
|
45
|
+
this.error(err.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command_not_found Hook — Dynamic Command Resolution
|
|
3
|
+
*
|
|
4
|
+
* This is the brain of the Sign CLI. When oclif can't find a static command
|
|
5
|
+
* (e.g., "sign folders list"), this hook intercepts, looks up the manifest,
|
|
6
|
+
* and executes the matching API call via SigV4.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Parse: "folders list" → resource="folders", action="list"
|
|
10
|
+
* 2. Load manifest (cached or fresh)
|
|
11
|
+
* 3. Resolve resource + action → method + path + apiGateway
|
|
12
|
+
* 4. Substitute path params from positional args
|
|
13
|
+
* 5. SigV4-sign and execute the request
|
|
14
|
+
* 6. Print response as JSON
|
|
15
|
+
*/
|
|
16
|
+
import { Hook } from '@oclif/core';
|
|
17
|
+
declare const hook: Hook.CommandNotFound;
|
|
18
|
+
export default hook;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command_not_found Hook — Dynamic Command Resolution
|
|
3
|
+
*
|
|
4
|
+
* This is the brain of the Sign CLI. When oclif can't find a static command
|
|
5
|
+
* (e.g., "sign folders list"), this hook intercepts, looks up the manifest,
|
|
6
|
+
* and executes the matching API call via SigV4.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Parse: "folders list" → resource="folders", action="list"
|
|
10
|
+
* 2. Load manifest (cached or fresh)
|
|
11
|
+
* 3. Resolve resource + action → method + path + apiGateway
|
|
12
|
+
* 4. Substitute path params from positional args
|
|
13
|
+
* 5. SigV4-sign and execute the request
|
|
14
|
+
* 6. Print response as JSON
|
|
15
|
+
*/
|
|
16
|
+
import { getManifest, resolveAction, listResources } from '../lib/manifest.js';
|
|
17
|
+
import { buildUrl, parseBody, formatOutput } from '../lib/request.js';
|
|
18
|
+
import { createSignClient } from '../lib/sign-api.js';
|
|
19
|
+
const hook = async function (opts) {
|
|
20
|
+
const parts = opts.id.split(':');
|
|
21
|
+
const argv = opts.argv ?? [];
|
|
22
|
+
// Need at least resource:action (oclif uses colons internally for topic:command)
|
|
23
|
+
if (parts.length < 2) {
|
|
24
|
+
const resource = parts[0];
|
|
25
|
+
try {
|
|
26
|
+
const client = createSignClient();
|
|
27
|
+
const manifest = await getManifest(client);
|
|
28
|
+
const res = manifest.resources[resource];
|
|
29
|
+
if (res) {
|
|
30
|
+
console.log(`\nResource: ${resource} (module: ${res.module})\n`);
|
|
31
|
+
console.log('Available actions:');
|
|
32
|
+
for (const action of res.actions) {
|
|
33
|
+
const argsStr = action.args.map(a => `<${a}>`).join(' ');
|
|
34
|
+
console.log(` sign ${resource} ${action.name}${argsStr ? ' ' + argsStr : ''} [${action.method}]`);
|
|
35
|
+
}
|
|
36
|
+
console.log(`\nUse --body '{...}' for POST/PUT/PATCH requests.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Fall through to error
|
|
42
|
+
}
|
|
43
|
+
console.error(`Unknown command: sign ${opts.id}`);
|
|
44
|
+
console.error('Run "sign modules list" to see available resources.');
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const [resource, action] = parts;
|
|
49
|
+
// Remaining colon-separated parts + non-flag argv become positional args
|
|
50
|
+
const positionalArgs = [...parts.slice(2), ...argv.filter(a => !a.startsWith('-'))];
|
|
51
|
+
// Parse flags from argv
|
|
52
|
+
const flags = parseFlags(argv);
|
|
53
|
+
try {
|
|
54
|
+
const client = createSignClient();
|
|
55
|
+
// Handle "manifest refresh" as a special case
|
|
56
|
+
if (resource === 'manifest' && action === 'refresh') {
|
|
57
|
+
const { clearManifestCache } = await import('../lib/manifest.js');
|
|
58
|
+
await clearManifestCache();
|
|
59
|
+
const manifest = await getManifest(client, true);
|
|
60
|
+
console.log(`Manifest refreshed: ${Object.keys(manifest.resources).length} resources discovered.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const manifest = await getManifest(client);
|
|
64
|
+
const match = resolveAction(manifest, resource, action);
|
|
65
|
+
if (!match) {
|
|
66
|
+
const res = manifest.resources[resource];
|
|
67
|
+
if (res) {
|
|
68
|
+
console.error(`Unknown action "${action}" for resource "${resource}".`);
|
|
69
|
+
console.error(`Available actions: ${res.actions.map(a => a.name).join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.error(`Unknown resource "${resource}".`);
|
|
73
|
+
const resources = listResources(manifest);
|
|
74
|
+
console.error(`Available resources: ${resources.map(r => r.name).join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Build the full URL with substituted path params
|
|
80
|
+
const url = buildUrl(match.resource, match.action, positionalArgs);
|
|
81
|
+
// Parse request body for POST/PUT/PATCH
|
|
82
|
+
const needsBody = ['POST', 'PUT', 'PATCH'].includes(match.action.method);
|
|
83
|
+
const body = needsBody ? await parseBody(flags) : undefined;
|
|
84
|
+
// Execute the signed request
|
|
85
|
+
const response = await client.request(match.action.method, url, body ? JSON.parse(body) : undefined);
|
|
86
|
+
// Output
|
|
87
|
+
const output = formatOutput(response, flags);
|
|
88
|
+
console.log(output);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const err = error;
|
|
92
|
+
if (err.message.includes('Missing required argument')) {
|
|
93
|
+
const match = resolveAction(await getManifest(createSignClient()), resource, action);
|
|
94
|
+
const argsStr = match?.action.args.map(a => `<${a}>`).join(' ') || '';
|
|
95
|
+
console.error(`Error: ${err.message}`);
|
|
96
|
+
console.error(`Usage: sign ${resource} ${action} ${argsStr}`);
|
|
97
|
+
}
|
|
98
|
+
else if (err.message.includes('Not authenticated') || err.message.includes('auth login')) {
|
|
99
|
+
console.error('Not authenticated. Run "sign auth login" first.');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.error(`Error: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Parse flags from argv
|
|
109
|
+
*/
|
|
110
|
+
function parseFlags(argv) {
|
|
111
|
+
const flags = {};
|
|
112
|
+
for (let i = 0; i < argv.length; i++) {
|
|
113
|
+
const arg = argv[i];
|
|
114
|
+
if (arg === '--body' && argv[i + 1]) {
|
|
115
|
+
flags.body = argv[++i];
|
|
116
|
+
}
|
|
117
|
+
else if (arg === '--input' && argv[i + 1]) {
|
|
118
|
+
flags.input = argv[++i];
|
|
119
|
+
}
|
|
120
|
+
else if (arg === '--table') {
|
|
121
|
+
flags.table = true;
|
|
122
|
+
}
|
|
123
|
+
else if (arg === '--raw') {
|
|
124
|
+
flags.raw = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return flags;
|
|
128
|
+
}
|
|
129
|
+
export default hook;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest Client
|
|
3
|
+
*
|
|
4
|
+
* Fetches and caches the API manifest from the backend.
|
|
5
|
+
* The manifest contains all discoverable resources + actions.
|
|
6
|
+
*/
|
|
7
|
+
export interface ManifestAction {
|
|
8
|
+
name: string;
|
|
9
|
+
method: string;
|
|
10
|
+
path: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface ManifestResource {
|
|
14
|
+
module: string;
|
|
15
|
+
apiGateway: string;
|
|
16
|
+
actions: ManifestAction[];
|
|
17
|
+
}
|
|
18
|
+
export interface Manifest {
|
|
19
|
+
version: string;
|
|
20
|
+
generatedAt: string;
|
|
21
|
+
stage: string;
|
|
22
|
+
region: string;
|
|
23
|
+
resources: Record<string, ManifestResource>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get the manifest — from cache if fresh, otherwise from the API
|
|
27
|
+
*/
|
|
28
|
+
export declare const getManifest: (apiClient: {
|
|
29
|
+
get: (path: string) => Promise<unknown>;
|
|
30
|
+
}, forceRefresh?: boolean) => Promise<Manifest>;
|
|
31
|
+
/**
|
|
32
|
+
* Clear the cached manifest
|
|
33
|
+
*/
|
|
34
|
+
export declare const clearManifestCache: () => Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Find a matching action in the manifest
|
|
37
|
+
*/
|
|
38
|
+
export declare const resolveAction: (manifest: Manifest, resource: string, action: string) => {
|
|
39
|
+
resource: ManifestResource;
|
|
40
|
+
action: ManifestAction;
|
|
41
|
+
} | null;
|
|
42
|
+
/**
|
|
43
|
+
* List all resources in the manifest
|
|
44
|
+
*/
|
|
45
|
+
export declare const listResources: (manifest: Manifest) => Array<{
|
|
46
|
+
name: string;
|
|
47
|
+
module: string;
|
|
48
|
+
actionCount: number;
|
|
49
|
+
actions: string[];
|
|
50
|
+
}>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest Client
|
|
3
|
+
*
|
|
4
|
+
* Fetches and caches the API manifest from the backend.
|
|
5
|
+
* The manifest contains all discoverable resources + actions.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
// --- Config ---
|
|
12
|
+
const CACHE_DIR = join(homedir(), '.sign');
|
|
13
|
+
const CACHE_FILE = join(CACHE_DIR, 'manifest.json');
|
|
14
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
15
|
+
/**
|
|
16
|
+
* Get the manifest — from cache if fresh, otherwise from the API
|
|
17
|
+
*/
|
|
18
|
+
export const getManifest = async (apiClient, forceRefresh = false) => {
|
|
19
|
+
if (!forceRefresh) {
|
|
20
|
+
const cached = await loadCachedManifest();
|
|
21
|
+
if (cached)
|
|
22
|
+
return cached;
|
|
23
|
+
}
|
|
24
|
+
const manifest = await fetchManifest(apiClient);
|
|
25
|
+
await cacheManifest(manifest);
|
|
26
|
+
return manifest;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Load manifest from local cache if it exists and is fresh
|
|
30
|
+
*/
|
|
31
|
+
const loadCachedManifest = async () => {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(CACHE_FILE))
|
|
34
|
+
return null;
|
|
35
|
+
const raw = await readFile(CACHE_FILE, 'utf-8');
|
|
36
|
+
const cached = JSON.parse(raw);
|
|
37
|
+
const age = Date.now() - new Date(cached.fetchedAt).getTime();
|
|
38
|
+
if (age > CACHE_TTL_MS)
|
|
39
|
+
return null;
|
|
40
|
+
return cached.manifest;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Fetch manifest from the API
|
|
48
|
+
*/
|
|
49
|
+
const fetchManifest = async (apiClient) => {
|
|
50
|
+
const response = await apiClient.get('/tenant/manifest');
|
|
51
|
+
return response;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Cache manifest to local filesystem
|
|
55
|
+
*/
|
|
56
|
+
const cacheManifest = async (manifest) => {
|
|
57
|
+
try {
|
|
58
|
+
if (!existsSync(CACHE_DIR)) {
|
|
59
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
const cached = {
|
|
62
|
+
fetchedAt: new Date().toISOString(),
|
|
63
|
+
manifest
|
|
64
|
+
};
|
|
65
|
+
await writeFile(CACHE_FILE, JSON.stringify(cached, null, 2));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Cache write failure is non-fatal
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Clear the cached manifest
|
|
73
|
+
*/
|
|
74
|
+
export const clearManifestCache = async () => {
|
|
75
|
+
try {
|
|
76
|
+
if (existsSync(CACHE_FILE)) {
|
|
77
|
+
const { unlink } = await import('node:fs/promises');
|
|
78
|
+
await unlink(CACHE_FILE);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Non-fatal
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Find a matching action in the manifest
|
|
87
|
+
*/
|
|
88
|
+
export const resolveAction = (manifest, resource, action) => {
|
|
89
|
+
const res = manifest.resources[resource];
|
|
90
|
+
if (!res)
|
|
91
|
+
return null;
|
|
92
|
+
const act = res.actions.find(a => a.name === action);
|
|
93
|
+
if (!act)
|
|
94
|
+
return null;
|
|
95
|
+
return { resource: res, action: act };
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* List all resources in the manifest
|
|
99
|
+
*/
|
|
100
|
+
export const listResources = (manifest) => {
|
|
101
|
+
return Object.entries(manifest.resources)
|
|
102
|
+
.map(([name, res]) => ({
|
|
103
|
+
name,
|
|
104
|
+
module: res.module,
|
|
105
|
+
actionCount: res.actions.length,
|
|
106
|
+
actions: res.actions.map(a => a.name)
|
|
107
|
+
}))
|
|
108
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
109
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Execution
|
|
3
|
+
*
|
|
4
|
+
* Executes SigV4-signed HTTP requests against API Gateway endpoints.
|
|
5
|
+
* Uses cli-auth's SigV4ApiClient under the hood.
|
|
6
|
+
*/
|
|
7
|
+
import type { ManifestAction, ManifestResource } from './manifest.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build the full URL for an action, substituting path params from positional args
|
|
10
|
+
*/
|
|
11
|
+
export declare const buildUrl: (resource: ManifestResource, action: ManifestAction, positionalArgs: string[]) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Parse request body from CLI flags
|
|
14
|
+
* Priority: --body > --input > stdin
|
|
15
|
+
*/
|
|
16
|
+
export declare const parseBody: (flags: {
|
|
17
|
+
body?: string;
|
|
18
|
+
input?: string;
|
|
19
|
+
}) => Promise<string | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Format response for output
|
|
22
|
+
*/
|
|
23
|
+
export declare const formatOutput: (data: unknown, flags: {
|
|
24
|
+
table?: boolean;
|
|
25
|
+
raw?: boolean;
|
|
26
|
+
}) => string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Execution
|
|
3
|
+
*
|
|
4
|
+
* Executes SigV4-signed HTTP requests against API Gateway endpoints.
|
|
5
|
+
* Uses cli-auth's SigV4ApiClient under the hood.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
/**
|
|
9
|
+
* Build the full URL for an action, substituting path params from positional args
|
|
10
|
+
*/
|
|
11
|
+
export const buildUrl = (resource, action, positionalArgs) => {
|
|
12
|
+
let path = action.path;
|
|
13
|
+
// Substitute path params in order
|
|
14
|
+
for (let i = 0; i < action.args.length; i++) {
|
|
15
|
+
const paramName = action.args[i];
|
|
16
|
+
const value = positionalArgs[i];
|
|
17
|
+
if (!value) {
|
|
18
|
+
throw new Error(`Missing required argument: <${paramName}> (argument ${i + 1})`);
|
|
19
|
+
}
|
|
20
|
+
path = path.replace(`{${paramName}}`, value);
|
|
21
|
+
}
|
|
22
|
+
return `${resource.apiGateway}${path}`;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Parse request body from CLI flags
|
|
26
|
+
* Priority: --body > --input > stdin
|
|
27
|
+
*/
|
|
28
|
+
export const parseBody = async (flags) => {
|
|
29
|
+
if (flags.body) {
|
|
30
|
+
return flags.body;
|
|
31
|
+
}
|
|
32
|
+
if (flags.input) {
|
|
33
|
+
return readFile(flags.input, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
// Check if stdin has data (non-TTY = piped input)
|
|
36
|
+
if (!process.stdin.isTTY) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
process.stdin.setEncoding('utf-8');
|
|
40
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
41
|
+
process.stdin.on('end', () => { resolve(data || undefined); });
|
|
42
|
+
// Timeout after 100ms if no data
|
|
43
|
+
setTimeout(() => { resolve(undefined); }, 100);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Format response for output
|
|
50
|
+
*/
|
|
51
|
+
export const formatOutput = (data, flags) => {
|
|
52
|
+
if (flags.raw) {
|
|
53
|
+
return typeof data === 'string' ? data : JSON.stringify(data);
|
|
54
|
+
}
|
|
55
|
+
if (flags.table && Array.isArray(data)) {
|
|
56
|
+
return formatTable(data);
|
|
57
|
+
}
|
|
58
|
+
return JSON.stringify(data, null, 2);
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Simple table formatter for array responses
|
|
62
|
+
*/
|
|
63
|
+
const formatTable = (data) => {
|
|
64
|
+
if (data.length === 0)
|
|
65
|
+
return '(empty)';
|
|
66
|
+
// Get columns from first item
|
|
67
|
+
const columns = Object.keys(data[0]);
|
|
68
|
+
// Calculate column widths
|
|
69
|
+
const widths = columns.map(col => Math.max(col.length, ...data.map(row => String(row[col] ?? '').length)));
|
|
70
|
+
// Header
|
|
71
|
+
const header = columns.map((col, i) => col.padEnd(widths[i])).join(' ');
|
|
72
|
+
const separator = widths.map(w => '─'.repeat(w)).join('──');
|
|
73
|
+
// Rows
|
|
74
|
+
const rows = data.map(row => columns.map((col, i) => String(row[col] ?? '').padEnd(widths[i])).join(' '));
|
|
75
|
+
return [header, separator, ...rows].join('\n');
|
|
76
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign API Client
|
|
3
|
+
*
|
|
4
|
+
* Extends SigV4ApiClient to provide generic request methods
|
|
5
|
+
* for the Sign CLI's dynamic command resolution.
|
|
6
|
+
*
|
|
7
|
+
* Domain-based: user runs "sign auth login --domain acme.vixsign.dev"
|
|
8
|
+
* and the bootstrap endpoint returns everything needed.
|
|
9
|
+
*/
|
|
10
|
+
import { SigV4ApiClient } from '@hyperdrive.bot/cli-auth';
|
|
11
|
+
export declare class SignApiClient extends SigV4ApiClient {
|
|
12
|
+
constructor(domain?: string);
|
|
13
|
+
/**
|
|
14
|
+
* GET request to the primary API (tenants gateway)
|
|
15
|
+
*/
|
|
16
|
+
get<T = unknown>(path: string): Promise<T>;
|
|
17
|
+
/**
|
|
18
|
+
* Make a signed request to an arbitrary full URL
|
|
19
|
+
* Used by the dynamic command resolver which already has the full apiGateway URL
|
|
20
|
+
*/
|
|
21
|
+
request<T = unknown>(method: string, fullUrl: string, body?: Record<string, unknown>): Promise<T>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a SignApiClient
|
|
25
|
+
*/
|
|
26
|
+
export declare const createSignClient: (domain?: string) => SignApiClient;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sign API Client
|
|
3
|
+
*
|
|
4
|
+
* Extends SigV4ApiClient to provide generic request methods
|
|
5
|
+
* for the Sign CLI's dynamic command resolution.
|
|
6
|
+
*
|
|
7
|
+
* Domain-based: user runs "sign auth login --domain acme.vixsign.dev"
|
|
8
|
+
* and the bootstrap endpoint returns everything needed.
|
|
9
|
+
*/
|
|
10
|
+
import { SigV4ApiClient } from '@hyperdrive.bot/cli-auth';
|
|
11
|
+
const CLI_VERSION = '0.1.0';
|
|
12
|
+
const SIGN_AUTH_CONFIG = {
|
|
13
|
+
appName: 'sign',
|
|
14
|
+
defaultRegion: process.env.SIGN_AWS_REGION || 'us-east-1',
|
|
15
|
+
};
|
|
16
|
+
export class SignApiClient extends SigV4ApiClient {
|
|
17
|
+
constructor(domain) {
|
|
18
|
+
super({
|
|
19
|
+
authConfig: SIGN_AUTH_CONFIG,
|
|
20
|
+
domain,
|
|
21
|
+
cliName: 'sign',
|
|
22
|
+
cliVersion: CLI_VERSION,
|
|
23
|
+
apiUrlOverride: process.env.SIGN_API_URL,
|
|
24
|
+
regionOverride: process.env.SIGN_AWS_REGION,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* GET request to the primary API (tenants gateway)
|
|
29
|
+
*/
|
|
30
|
+
async get(path) {
|
|
31
|
+
return this.makeSignedRequest('GET', path);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Make a signed request to an arbitrary full URL
|
|
35
|
+
* Used by the dynamic command resolver which already has the full apiGateway URL
|
|
36
|
+
*/
|
|
37
|
+
async request(method, fullUrl, body) {
|
|
38
|
+
const url = new URL(fullUrl);
|
|
39
|
+
const baseUrl = `${url.protocol}//${url.host}`;
|
|
40
|
+
const path = url.pathname + url.search;
|
|
41
|
+
return this.makeSignedRequest(method, path, body, baseUrl);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a SignApiClient
|
|
46
|
+
*/
|
|
47
|
+
export const createSignClient = (domain) => {
|
|
48
|
+
return new SignApiClient(domain);
|
|
49
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"commands": {
|
|
3
|
+
"api": {
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"args": {
|
|
6
|
+
"method": {
|
|
7
|
+
"description": "HTTP method",
|
|
8
|
+
"name": "method",
|
|
9
|
+
"options": [
|
|
10
|
+
"GET",
|
|
11
|
+
"POST",
|
|
12
|
+
"PUT",
|
|
13
|
+
"PATCH",
|
|
14
|
+
"DELETE"
|
|
15
|
+
],
|
|
16
|
+
"required": true
|
|
17
|
+
},
|
|
18
|
+
"path": {
|
|
19
|
+
"description": "API path (e.g., /folders, /documents/abc-123)",
|
|
20
|
+
"name": "path",
|
|
21
|
+
"required": true
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"description": "Execute a raw API request (escape hatch)",
|
|
25
|
+
"examples": [
|
|
26
|
+
"<%= config.bin %> api GET /folders",
|
|
27
|
+
"<%= config.bin %> api POST /documents --body '{\"name\":\"My Doc\"}'",
|
|
28
|
+
"<%= config.bin %> api DELETE /folders/abc-123"
|
|
29
|
+
],
|
|
30
|
+
"flags": {
|
|
31
|
+
"body": {
|
|
32
|
+
"char": "b",
|
|
33
|
+
"description": "Request body (JSON string)",
|
|
34
|
+
"name": "body",
|
|
35
|
+
"hasDynamicHelp": false,
|
|
36
|
+
"multiple": false,
|
|
37
|
+
"type": "option"
|
|
38
|
+
},
|
|
39
|
+
"input": {
|
|
40
|
+
"char": "i",
|
|
41
|
+
"description": "Read body from file",
|
|
42
|
+
"name": "input",
|
|
43
|
+
"hasDynamicHelp": false,
|
|
44
|
+
"multiple": false,
|
|
45
|
+
"type": "option"
|
|
46
|
+
},
|
|
47
|
+
"module": {
|
|
48
|
+
"char": "m",
|
|
49
|
+
"description": "Target module (for API Gateway routing)",
|
|
50
|
+
"name": "module",
|
|
51
|
+
"hasDynamicHelp": false,
|
|
52
|
+
"multiple": false,
|
|
53
|
+
"type": "option"
|
|
54
|
+
},
|
|
55
|
+
"table": {
|
|
56
|
+
"description": "Format output as table",
|
|
57
|
+
"name": "table",
|
|
58
|
+
"allowNo": false,
|
|
59
|
+
"type": "boolean"
|
|
60
|
+
},
|
|
61
|
+
"raw": {
|
|
62
|
+
"description": "Raw output (no formatting)",
|
|
63
|
+
"name": "raw",
|
|
64
|
+
"allowNo": false,
|
|
65
|
+
"type": "boolean"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"hasDynamicHelp": false,
|
|
69
|
+
"hiddenAliases": [],
|
|
70
|
+
"id": "api",
|
|
71
|
+
"pluginAlias": "@hyperdrive.bot/sign-cli",
|
|
72
|
+
"pluginName": "@hyperdrive.bot/sign-cli",
|
|
73
|
+
"pluginType": "core",
|
|
74
|
+
"strict": true,
|
|
75
|
+
"enableJsonFlag": false,
|
|
76
|
+
"isESM": true,
|
|
77
|
+
"relativePath": [
|
|
78
|
+
"dist",
|
|
79
|
+
"commands",
|
|
80
|
+
"api.js"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"modules:list": {
|
|
84
|
+
"aliases": [],
|
|
85
|
+
"args": {},
|
|
86
|
+
"description": "List all available API resources and actions",
|
|
87
|
+
"examples": [
|
|
88
|
+
"<%= config.bin %> modules list",
|
|
89
|
+
"<%= config.bin %> modules list --json",
|
|
90
|
+
"<%= config.bin %> modules list --refresh"
|
|
91
|
+
],
|
|
92
|
+
"flags": {
|
|
93
|
+
"json": {
|
|
94
|
+
"description": "Output as JSON",
|
|
95
|
+
"name": "json",
|
|
96
|
+
"allowNo": false,
|
|
97
|
+
"type": "boolean"
|
|
98
|
+
},
|
|
99
|
+
"refresh": {
|
|
100
|
+
"description": "Force refresh the manifest cache",
|
|
101
|
+
"name": "refresh",
|
|
102
|
+
"allowNo": false,
|
|
103
|
+
"type": "boolean"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"hasDynamicHelp": false,
|
|
107
|
+
"hiddenAliases": [],
|
|
108
|
+
"id": "modules:list",
|
|
109
|
+
"pluginAlias": "@hyperdrive.bot/sign-cli",
|
|
110
|
+
"pluginName": "@hyperdrive.bot/sign-cli",
|
|
111
|
+
"pluginType": "core",
|
|
112
|
+
"strict": true,
|
|
113
|
+
"enableJsonFlag": false,
|
|
114
|
+
"isESM": true,
|
|
115
|
+
"relativePath": [
|
|
116
|
+
"dist",
|
|
117
|
+
"commands",
|
|
118
|
+
"modules",
|
|
119
|
+
"list.js"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"version": "0.1.3"
|
|
124
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hyperdrive.bot/sign-cli",
|
|
3
|
+
"description": "Sign CLI — runtime-discovered API client for the Sign platform",
|
|
4
|
+
"version": "0.1.3",
|
|
5
|
+
"author": "marcelomarra",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sign": "./bin/run.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@hyperdrive.bot/auth-plugin": "^0.1.0",
|
|
11
|
+
"@hyperdrive.bot/cli-auth": "^1.1.3",
|
|
12
|
+
"@oclif/core": "^4",
|
|
13
|
+
"@oclif/plugin-help": "^6",
|
|
14
|
+
"@oclif/plugin-not-found": "^3.1.8",
|
|
15
|
+
"chalk": "^5.3.0",
|
|
16
|
+
"cli-table3": "^0.6.5",
|
|
17
|
+
"ora": "^8.0.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^18",
|
|
21
|
+
"oclif": "^4",
|
|
22
|
+
"ts-node": "^10",
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18.0.0"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"/dist",
|
|
31
|
+
"/oclif.manifest.json"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"main": "dist/index.js",
|
|
35
|
+
"type": "module",
|
|
36
|
+
"oclif": {
|
|
37
|
+
"bin": "sign",
|
|
38
|
+
"dirname": "sign",
|
|
39
|
+
"commands": "./dist/commands",
|
|
40
|
+
"plugins": [
|
|
41
|
+
"@oclif/plugin-help",
|
|
42
|
+
"@oclif/plugin-not-found",
|
|
43
|
+
"@hyperdrive.bot/auth-plugin"
|
|
44
|
+
],
|
|
45
|
+
"hooks": {
|
|
46
|
+
"command_not_found": "./dist/hooks/command-not-found"
|
|
47
|
+
},
|
|
48
|
+
"authPlugin": {
|
|
49
|
+
"appName": "sign",
|
|
50
|
+
"displayName": "Sign",
|
|
51
|
+
"defaultBootstrapUrl": "https://oo2wp0ax27.execute-api.us-east-1.amazonaws.com/dev/tenant/bootstrap-dev",
|
|
52
|
+
"envPrefix": "SIGN",
|
|
53
|
+
"ciTokenPrefix": "sign_sk_",
|
|
54
|
+
"primaryApiName": "sign"
|
|
55
|
+
},
|
|
56
|
+
"topicSeparator": " "
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc",
|
|
60
|
+
"dev": "tsc --watch",
|
|
61
|
+
"clean": "rm -rf dist",
|
|
62
|
+
"test": "echo 'No tests yet'",
|
|
63
|
+
"postpack": "rm -f oclif.manifest.json",
|
|
64
|
+
"prepack": "npm run build && oclif manifest"
|
|
65
|
+
}
|
|
66
|
+
}
|