@skillguard/cli 0.2.1 → 0.5.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.
@@ -0,0 +1,143 @@
1
+ import { mkdir, access, writeFile } from 'node:fs/promises';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { dirname, relative, resolve } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ const DEFAULT_WORKFLOW_PATH = '.github/workflows/skillguard.yml';
6
+ const DEFAULT_COMMIT_MESSAGE = 'chore(ci): add skillguard workflow';
7
+ function buildWorkflowYaml(input) {
8
+ const scanPath = input.scanPath.trim() || '.';
9
+ return [
10
+ 'name: SkillGuard Gate',
11
+ 'on:',
12
+ ' pull_request:',
13
+ ' paths:',
14
+ " - '**/SKILL.md'",
15
+ " - '**/skill.md'",
16
+ ' push:',
17
+ ' paths:',
18
+ " - '**/SKILL.md'",
19
+ " - '**/skill.md'",
20
+ '',
21
+ 'jobs:',
22
+ ' scan:',
23
+ ' runs-on: ubuntu-latest',
24
+ ' steps:',
25
+ ' - uses: actions/checkout@v4',
26
+ ' - uses: actions/setup-node@v4',
27
+ ' with:',
28
+ " node-version: '20'",
29
+ ' - name: SkillGuard scan',
30
+ ` run: npx @skillguard/cli gate ${scanPath} --fail-on ${input.failOn}`,
31
+ ].join('\n');
32
+ }
33
+ function isCommandSafePath(value) {
34
+ return /^[./a-zA-Z0-9_-]+$/.test(value);
35
+ }
36
+ async function fileExists(path) {
37
+ try {
38
+ await access(path, fsConstants.F_OK);
39
+ return true;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ function createDefaultGitRunner(cwd) {
46
+ return (args) => {
47
+ const result = spawnSync('git', args, {
48
+ cwd,
49
+ encoding: 'utf8',
50
+ stdio: ['ignore', 'pipe', 'pipe'],
51
+ });
52
+ if (result.error) {
53
+ throw new Error('Git command failed.');
54
+ }
55
+ return {
56
+ stdout: typeof result.stdout === 'string' ? result.stdout : '',
57
+ stderr: typeof result.stderr === 'string' ? result.stderr : '',
58
+ status: typeof result.status === 'number' ? result.status : 1,
59
+ };
60
+ };
61
+ }
62
+ function ensureGitSuccess(result, fallbackMessage) {
63
+ if (result.status === 0) {
64
+ return;
65
+ }
66
+ const stderr = result.stderr.trim();
67
+ if (stderr) {
68
+ throw new Error(stderr);
69
+ }
70
+ throw new Error(fallbackMessage);
71
+ }
72
+ function resolveWorkflowPath(cwd, outputPath) {
73
+ return resolve(cwd, outputPath || DEFAULT_WORKFLOW_PATH);
74
+ }
75
+ function commitAndPushWorkflow(input) {
76
+ const git = input.gitRunner || createDefaultGitRunner(input.cwd);
77
+ const repoRootResult = git(['rev-parse', '--show-toplevel']);
78
+ ensureGitSuccess(repoRootResult, 'Not a git repository.');
79
+ const repoRoot = repoRootResult.stdout.trim();
80
+ const relativePath = relative(repoRoot, input.workflowPath);
81
+ if (!relativePath || relativePath.startsWith('..')) {
82
+ throw new Error('Workflow path must be inside the current git repository.');
83
+ }
84
+ ensureGitSuccess(git(['add', '--', relativePath]), 'Could not stage workflow file.');
85
+ const stagedResult = git(['diff', '--cached', '--name-only', '--', relativePath]);
86
+ ensureGitSuccess(stagedResult, 'Could not inspect staged changes.');
87
+ if (!stagedResult.stdout.trim()) {
88
+ return false;
89
+ }
90
+ ensureGitSuccess(git(['commit', '-m', input.commitMessage, '--', relativePath]), 'Could not create commit.');
91
+ ensureGitSuccess(git(['push']), 'Could not push commit.');
92
+ return true;
93
+ }
94
+ export async function runWorkflow(options, runtime) {
95
+ const cwd = runtime?.cwd || process.cwd();
96
+ const scanPath = (options.scanPath || '.').trim() || '.';
97
+ if (!isCommandSafePath(scanPath)) {
98
+ console.error('Invalid --scan-path value. Use simple relative paths only.');
99
+ return 4;
100
+ }
101
+ if (options.print && options.autoGhPush) {
102
+ console.error('Cannot use --auto-gh-push with --print.');
103
+ return 4;
104
+ }
105
+ const yaml = buildWorkflowYaml({
106
+ scanPath,
107
+ failOn: options.failOn,
108
+ });
109
+ if (options.print) {
110
+ console.log(yaml);
111
+ return 0;
112
+ }
113
+ const workflowPath = resolveWorkflowPath(cwd, options.outputPath);
114
+ const exists = await fileExists(workflowPath);
115
+ if (exists && !options.force) {
116
+ console.error(`Workflow file already exists: ${workflowPath}. Use --force to overwrite.`);
117
+ return 4;
118
+ }
119
+ await mkdir(dirname(workflowPath), { recursive: true });
120
+ await writeFile(workflowPath, `${yaml}\n`, 'utf8');
121
+ console.log(`Workflow written to ${workflowPath}`);
122
+ if (!options.autoGhPush) {
123
+ return 0;
124
+ }
125
+ try {
126
+ const pushed = commitAndPushWorkflow({
127
+ cwd,
128
+ workflowPath,
129
+ commitMessage: (options.commitMessage || DEFAULT_COMMIT_MESSAGE).trim() || DEFAULT_COMMIT_MESSAGE,
130
+ gitRunner: runtime?.gitRunner,
131
+ });
132
+ if (!pushed) {
133
+ console.log('Workflow already up to date. No commit created.');
134
+ return 0;
135
+ }
136
+ }
137
+ catch (error) {
138
+ console.error(`Auto push failed: ${error.message}`);
139
+ return 5;
140
+ }
141
+ console.log('Workflow committed and pushed.');
142
+ return 0;
143
+ }
package/dist/lib/api.d.ts CHANGED
@@ -14,6 +14,34 @@ export interface ScanApiResponse {
14
14
  signedSkillContent?: string;
15
15
  [key: string]: unknown;
16
16
  }
17
+ export interface LimitsApiResponse {
18
+ mode?: 'anonymous' | 'owner' | 'agent';
19
+ tokenBalance?: number;
20
+ ownerTrial?: {
21
+ scansLimit?: number;
22
+ scansUsed?: number;
23
+ remainingScans?: number;
24
+ expiresAt?: string | null;
25
+ active?: boolean;
26
+ };
27
+ apiKeyQuota?: {
28
+ keyType?: 'standard' | 'trial';
29
+ maxScans?: number | null;
30
+ scansUsed?: number;
31
+ remainingScans?: number | null;
32
+ expiresAt?: string | null;
33
+ unlimited?: boolean;
34
+ active?: boolean;
35
+ };
36
+ anonymousQuota?: {
37
+ windowMs?: number;
38
+ maxScans?: number;
39
+ scansUsed?: number;
40
+ remainingScans?: number;
41
+ resetAt?: string;
42
+ };
43
+ [key: string]: unknown;
44
+ }
17
45
  export interface SanitizedApiError {
18
46
  message: string;
19
47
  exitCode: 2 | 3;
@@ -26,6 +54,18 @@ export declare function scanContent(input: {
26
54
  timeoutMs: number;
27
55
  fetchImpl?: typeof fetch;
28
56
  }): Promise<ScanApiResponse>;
57
+ export declare function scanContentAnonymous(input: {
58
+ baseUrl: string;
59
+ content: string;
60
+ timeoutMs: number;
61
+ fetchImpl?: typeof fetch;
62
+ }): Promise<ScanApiResponse>;
63
+ export declare function fetchLimits(input: {
64
+ baseUrl: string;
65
+ timeoutMs: number;
66
+ apiKey?: string;
67
+ fetchImpl?: typeof fetch;
68
+ }): Promise<LimitsApiResponse>;
29
69
  export declare function fetchJwks(input: {
30
70
  baseUrl: string;
31
71
  timeoutMs: number;
package/dist/lib/api.js CHANGED
@@ -58,6 +58,47 @@ function mapStatusMessage(status) {
58
58
  }
59
59
  return 'SkillGuard API request failed.';
60
60
  }
61
+ function mapLimitsStatusMessage(status) {
62
+ if (status === 401) {
63
+ return "Invalid API key. Run 'skillguard init' or set SKILLGUARD_API_KEY.";
64
+ }
65
+ if (status === 403) {
66
+ return 'Access denied for this API key.';
67
+ }
68
+ if (status === 429) {
69
+ return 'Rate limit exceeded. Wait and retry.';
70
+ }
71
+ if (status >= 500) {
72
+ return 'SkillGuard API error. Try again later.';
73
+ }
74
+ return 'SkillGuard limits request failed.';
75
+ }
76
+ function detectCiSource(env = process.env) {
77
+ if (env.GITHUB_ACTIONS === 'true')
78
+ return 'github-actions';
79
+ if (env.GITLAB_CI === 'true')
80
+ return 'gitlab';
81
+ if (env.BUILDKITE === 'true')
82
+ return 'buildkite';
83
+ if (env.CIRCLECI === 'true')
84
+ return 'circleci';
85
+ if (env.JENKINS_URL)
86
+ return 'jenkins';
87
+ if (env.TF_BUILD === 'True' || env.AZURE_HTTP_USER_AGENT)
88
+ return 'azure-pipelines';
89
+ return null;
90
+ }
91
+ function buildCliHeaders(extra = {}) {
92
+ const headers = {
93
+ 'X-SkillGuard-Source': 'cli',
94
+ ...extra,
95
+ };
96
+ const ciSource = detectCiSource();
97
+ if (ciSource) {
98
+ headers['X-SkillGuard-CI'] = ciSource;
99
+ }
100
+ return headers;
101
+ }
61
102
  export async function scanContent(input) {
62
103
  const fetchFn = input.fetchImpl || fetch;
63
104
  const endpoint = `${input.baseUrl}/api/v1/scan`;
@@ -66,6 +107,7 @@ export async function scanContent(input) {
66
107
  const response = await fetchWithRetry(fetchFn, endpoint, {
67
108
  method: 'POST',
68
109
  headers: {
110
+ ...buildCliHeaders(),
69
111
  'X-API-Key': input.apiKey,
70
112
  'Content-Type': 'application/json',
71
113
  },
@@ -101,6 +143,91 @@ export async function scanContent(input) {
101
143
  timeout.cancel();
102
144
  }
103
145
  }
146
+ export async function scanContentAnonymous(input) {
147
+ const fetchFn = input.fetchImpl || fetch;
148
+ const endpoint = `${input.baseUrl}/api/v1/scan/anonymous`;
149
+ const timeout = withTimeout(input.timeoutMs);
150
+ try {
151
+ const response = await fetchWithRetry(fetchFn, endpoint, {
152
+ method: 'POST',
153
+ headers: {
154
+ ...buildCliHeaders(),
155
+ 'Content-Type': 'application/json',
156
+ },
157
+ body: JSON.stringify({ content: input.content }),
158
+ signal: timeout.signal,
159
+ }, 1);
160
+ if (!response.ok) {
161
+ throw new Error(mapStatusMessage(response.status));
162
+ }
163
+ const parsed = await parseJsonSafe(response);
164
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
165
+ throw new Error('Unexpected API response.');
166
+ }
167
+ return parsed;
168
+ }
169
+ catch (error) {
170
+ const typed = error;
171
+ if (typed.name === 'AbortError') {
172
+ throw new Error('Cannot reach skillguard.ai. Check your connection.');
173
+ }
174
+ if (typed.message === 'Unexpected API response.') {
175
+ throw typed;
176
+ }
177
+ if (typed.message.includes('Rate limit exceeded.') ||
178
+ typed.message.includes('SkillGuard API error.') ||
179
+ typed.message.includes('SkillGuard API request failed.')) {
180
+ throw typed;
181
+ }
182
+ throw new Error('Cannot reach skillguard.ai. Check your connection.');
183
+ }
184
+ finally {
185
+ timeout.cancel();
186
+ }
187
+ }
188
+ export async function fetchLimits(input) {
189
+ const fetchFn = input.fetchImpl || fetch;
190
+ const endpoint = `${input.baseUrl}/api/v1/limits`;
191
+ const timeout = withTimeout(input.timeoutMs);
192
+ try {
193
+ const response = await fetchWithRetry(fetchFn, endpoint, {
194
+ method: 'GET',
195
+ headers: {
196
+ ...buildCliHeaders(),
197
+ ...(input.apiKey ? { 'X-API-Key': input.apiKey } : {}),
198
+ },
199
+ signal: timeout.signal,
200
+ }, 1);
201
+ if (!response.ok) {
202
+ throw new Error(mapLimitsStatusMessage(response.status));
203
+ }
204
+ const parsed = await parseJsonSafe(response);
205
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
206
+ throw new Error('Unexpected API response.');
207
+ }
208
+ return parsed;
209
+ }
210
+ catch (error) {
211
+ const typed = error;
212
+ if (typed.name === 'AbortError') {
213
+ throw new Error('Cannot reach skillguard.ai. Check your connection.');
214
+ }
215
+ if (typed.message === 'Unexpected API response.') {
216
+ throw typed;
217
+ }
218
+ if (typed.message.includes('Invalid API key.') ||
219
+ typed.message.includes('Access denied') ||
220
+ typed.message.includes('Rate limit exceeded.') ||
221
+ typed.message.includes('SkillGuard API error.') ||
222
+ typed.message.includes('SkillGuard limits request failed.')) {
223
+ throw typed;
224
+ }
225
+ throw new Error('Cannot reach skillguard.ai. Check your connection.');
226
+ }
227
+ finally {
228
+ timeout.cancel();
229
+ }
230
+ }
104
231
  export async function fetchJwks(input) {
105
232
  const fetchFn = input.fetchImpl || fetch;
106
233
  const endpoint = `${input.baseUrl}/.well-known/skillguard-jwks.json`;
@@ -15,5 +15,6 @@ export interface ApiKeyResolutionInput {
15
15
  export declare function getConfigPath(home?: string): string;
16
16
  export declare function readConfig(configPath?: string): Promise<SkillguardCliConfig | null>;
17
17
  export declare function writeConfig(config: SkillguardCliConfig, configPath?: string): Promise<void>;
18
+ export declare function clearConfig(configPath?: string): Promise<boolean>;
18
19
  export declare function normalizeBaseUrl(value?: string): string;
19
20
  export declare function resolveApiKey(input: ApiKeyResolutionInput): ResolvedApiKey | null;
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, chmod, writeFile } from 'node:fs/promises';
1
+ import { access, chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  export const DEFAULT_BASE_URL = 'https://skillguard.ai';
@@ -39,6 +39,25 @@ export async function writeConfig(config, configPath = getConfigPath()) {
39
39
  await writeFile(configPath, `${body}\n`, { encoding: 'utf8', mode: 0o600 });
40
40
  await chmod(configPath, 0o600);
41
41
  }
42
+ export async function clearConfig(configPath = getConfigPath()) {
43
+ try {
44
+ await access(configPath);
45
+ }
46
+ catch (error) {
47
+ const typed = error;
48
+ if (typed?.code === 'ENOENT') {
49
+ return false;
50
+ }
51
+ throw new Error('Failed to clear CLI config.');
52
+ }
53
+ try {
54
+ await rm(configPath, { force: true });
55
+ return true;
56
+ }
57
+ catch {
58
+ throw new Error('Failed to clear CLI config.');
59
+ }
60
+ }
42
61
  export function normalizeBaseUrl(value) {
43
62
  const raw = (value || DEFAULT_BASE_URL).trim();
44
63
  const withoutTrailingSlash = raw.replace(/\/+$/, '');
@@ -0,0 +1,2 @@
1
+ export declare function renderGeneralHelp(version: string): string;
2
+ export declare function renderTopicHelp(command: string | undefined): string | null;
@@ -0,0 +1,267 @@
1
+ const COMMAND_ALIASES = {
2
+ man: 'help',
3
+ };
4
+ function normalizeTopic(topic) {
5
+ if (!topic) {
6
+ return null;
7
+ }
8
+ const normalized = topic.trim().toLowerCase();
9
+ const aliased = COMMAND_ALIASES[normalized] || normalized;
10
+ if (aliased === 'help' ||
11
+ aliased === 'auth' ||
12
+ aliased === 'init' ||
13
+ aliased === 'scan' ||
14
+ aliased === 'gate' ||
15
+ aliased === 'limits' ||
16
+ aliased === 'verify' ||
17
+ aliased === 'workflow') {
18
+ return aliased;
19
+ }
20
+ return null;
21
+ }
22
+ export function renderGeneralHelp(version) {
23
+ return `SkillGuard CLI ${version}
24
+
25
+ Usage:
26
+ skillguard <command> [options]
27
+
28
+ Core commands:
29
+ auth API-key auth commands (login/status/logout)
30
+ init Save API key in local CLI config
31
+ scan Scan one file or recursively scan a directory
32
+ gate CI gate mode (0 pass, 1 fail)
33
+ limits Show anonymous/owner/agent remaining limits
34
+ verify Verify signed SKILL.md via SkillGuard JWKS
35
+ workflow Generate GitHub Actions workflow file
36
+
37
+ Help commands:
38
+ help [command]
39
+ man [command]
40
+
41
+ Quick start:
42
+ skillguard scan SKILL.md
43
+ skillguard auth status
44
+ skillguard gate .
45
+ skillguard workflow
46
+ skillguard workflow --auto-gh-push
47
+
48
+ Global flags:
49
+ -h, --help Show help
50
+ -v, --version Show version
51
+ --workflow Alias for "workflow"
52
+
53
+ Run "skillguard help <command>" for full command details.`;
54
+ }
55
+ function renderHelpCommandHelp() {
56
+ return `NAME
57
+ skillguard help, skillguard man - show detailed command help
58
+
59
+ SYNOPSIS
60
+ skillguard help [command]
61
+ skillguard man [command]
62
+
63
+ DESCRIPTION
64
+ Shows command-level help in a man-page style format.
65
+ If no command is provided, prints general CLI help.
66
+
67
+ EXAMPLES
68
+ skillguard help scan
69
+ skillguard man workflow`;
70
+ }
71
+ function renderInitHelp() {
72
+ return `NAME
73
+ skillguard init - save API key in local config
74
+
75
+ SYNOPSIS
76
+ skillguard init [--api-key <key>] [--base-url <url>]
77
+
78
+ DESCRIPTION
79
+ Writes ~/.config/skillguard/config.json with apiKey and apiUrl.
80
+ If --api-key is omitted, prompts interactively.
81
+
82
+ OPTIONS
83
+ --api-key <key> API key to persist
84
+ --base-url <url> API base URL (default: https://skillguard.ai)
85
+
86
+ EXIT CODES
87
+ 0 success
88
+ 4 usage/input/config error
89
+ 5 external runtime error`;
90
+ }
91
+ function renderAuthHelp() {
92
+ return `NAME
93
+ skillguard auth - manage CLI authentication state
94
+
95
+ SYNOPSIS
96
+ skillguard auth login [--api-key <key>] [--base-url <url>]
97
+ skillguard auth status [--json] [--api-key <key>] [--base-url <url>]
98
+ skillguard auth logout
99
+
100
+ DESCRIPTION
101
+ login Save API key in local config (interactive if key omitted)
102
+ status Show effective auth source (flag/env/config/none)
103
+ logout Remove local CLI config key
104
+
105
+ NOTES
106
+ logout cannot unset SKILLGUARD_API_KEY environment variables from parent shell.
107
+
108
+ EXIT CODES
109
+ 0 success
110
+ 4 usage/input/config error
111
+ 5 external runtime error`;
112
+ }
113
+ function renderScanHelp() {
114
+ return `NAME
115
+ skillguard scan - scan SKILL.md content for risk
116
+
117
+ SYNOPSIS
118
+ skillguard scan [path] [options]
119
+
120
+ DESCRIPTION
121
+ Scans one file or recursively scans a directory for SKILL.md files.
122
+ If no path is provided, scans from current directory.
123
+ If no API key is configured, uses anonymous mode (rate-limited).
124
+
125
+ OPTIONS
126
+ --json Machine-readable JSON output
127
+ --fail-on <safe|warning|dangerous>
128
+ Explicit threshold mode (exit 0/1)
129
+ --timeout <ms> Request timeout (default: 30000)
130
+ --base-url <url> API base URL
131
+ --api-key <key> Override API key
132
+ --dry-run Show target files, skip API call
133
+ --quiet Suppress output, keep exit code
134
+ --no-color Disable ANSI colors
135
+ --skip-node-modules Skip node_modules (default: enabled)
136
+ --scan-all Disable skip filters for recursive scan
137
+
138
+ EXIT CODES
139
+ default mode (no --fail-on): safe=0, warning=1, dangerous=2
140
+ threshold mode (--fail-on): 0 below threshold, 1 threshold reached
141
+ 4 usage/input/config error
142
+ 5 network/API/runtime failure`;
143
+ }
144
+ function renderGateHelp() {
145
+ return `NAME
146
+ skillguard gate - CI gate mode for deterministic pass/fail
147
+
148
+ SYNOPSIS
149
+ skillguard gate [path] [options]
150
+
151
+ DESCRIPTION
152
+ Wrapper around scan with explicit threshold mode enabled.
153
+ Default fail-on is warning.
154
+
155
+ OPTIONS
156
+ Same as "skillguard scan".
157
+ --fail-on defaults to warning in gate mode.
158
+
159
+ EXIT CODES
160
+ 0 below threshold
161
+ 1 threshold reached
162
+ 4 usage/input/config error
163
+ 5 network/API/runtime failure`;
164
+ }
165
+ function renderLimitsHelp() {
166
+ return `NAME
167
+ skillguard limits - show remaining limits/quota
168
+
169
+ SYNOPSIS
170
+ skillguard limits [--json] [--timeout <ms>] [--base-url <url>] [--api-key <key>] [--no-color]
171
+
172
+ DESCRIPTION
173
+ Fetches /api/v1/limits and prints current request mode:
174
+ anonymous, owner, or agent.
175
+
176
+ OPTIONS
177
+ --json JSON output
178
+ --timeout <ms> Request timeout (default: 30000)
179
+ --base-url <url> API base URL
180
+ --api-key <key> Optional API key
181
+ --no-color Disable ANSI colors
182
+
183
+ EXIT CODES
184
+ 0 success
185
+ 4 usage/input/config error
186
+ 5 network/API/runtime failure`;
187
+ }
188
+ function renderVerifyHelp() {
189
+ return `NAME
190
+ skillguard verify - verify signed SKILL.md content
191
+
192
+ SYNOPSIS
193
+ skillguard verify <path> [--json] [--timeout <ms>] [--base-url <url>]
194
+
195
+ DESCRIPTION
196
+ Verifies SkillGuard Ed25519 signature and content hash using JWKS.
197
+
198
+ OPTIONS
199
+ --json JSON output
200
+ --timeout <ms> Request timeout (default: 30000)
201
+ --base-url <url> API base URL
202
+
203
+ EXIT CODES
204
+ 0 signature valid
205
+ 1 invalid/tampered/expired signature
206
+ 4 usage/input/config error
207
+ 5 network/API/runtime failure`;
208
+ }
209
+ function renderWorkflowHelp() {
210
+ return `NAME
211
+ skillguard workflow - generate GitHub Actions workflow for CI gate
212
+
213
+ SYNOPSIS
214
+ skillguard workflow [options]
215
+ skillguard --workflow [options]
216
+
217
+ DESCRIPTION
218
+ Writes .github/workflows/skillguard.yml configured to run:
219
+ npx @skillguard/cli gate . --fail-on warning
220
+
221
+ OPTIONS
222
+ --path <file> Output workflow path
223
+ (default: .github/workflows/skillguard.yml)
224
+ --scan-path <path> Path passed to gate command (default: .)
225
+ --fail-on <safe|warning|dangerous>
226
+ Gate threshold (default: warning)
227
+ --print Print YAML to stdout, do not write files
228
+ --force Overwrite existing file
229
+ --auto-gh-push Run git add/commit/push after writing
230
+
231
+ EXIT CODES
232
+ 0 success
233
+ 4 usage/input/config error
234
+ 5 runtime/git failure
235
+
236
+ EXAMPLES
237
+ skillguard workflow
238
+ skillguard workflow --print
239
+ skillguard workflow --auto-gh-push
240
+ skillguard --workflow --force`;
241
+ }
242
+ export function renderTopicHelp(command) {
243
+ const topic = normalizeTopic(command);
244
+ if (!topic) {
245
+ return null;
246
+ }
247
+ switch (topic) {
248
+ case 'help':
249
+ return renderHelpCommandHelp();
250
+ case 'auth':
251
+ return renderAuthHelp();
252
+ case 'init':
253
+ return renderInitHelp();
254
+ case 'scan':
255
+ return renderScanHelp();
256
+ case 'gate':
257
+ return renderGateHelp();
258
+ case 'limits':
259
+ return renderLimitsHelp();
260
+ case 'verify':
261
+ return renderVerifyHelp();
262
+ case 'workflow':
263
+ return renderWorkflowHelp();
264
+ default:
265
+ return null;
266
+ }
267
+ }
@@ -0,0 +1 @@
1
+ export declare const CLI_VERSION = "0.5.0";
@@ -0,0 +1 @@
1
+ export const CLI_VERSION = '0.5.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillguard/cli",
3
- "version": "0.2.1",
3
+ "version": "0.5.0",
4
4
  "description": "Security scanner for AI agent skill files",
5
5
  "type": "module",
6
6
  "bin": {