@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 ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {execute} from '@oclif/core'
4
+
5
+ await execute({dir: import.meta.url})
@@ -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;
@@ -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
+ }