@skhema/cli 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +163 -0
  2. package/bin/skhema.js +2 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +92 -0
  6. package/dist/commands/agent.d.ts +3 -0
  7. package/dist/commands/agent.d.ts.map +1 -0
  8. package/dist/commands/agent.js +154 -0
  9. package/dist/commands/api.d.ts +19 -0
  10. package/dist/commands/api.d.ts.map +1 -0
  11. package/dist/commands/api.js +97 -0
  12. package/dist/commands/auth.d.ts +3 -0
  13. package/dist/commands/auth.d.ts.map +1 -0
  14. package/dist/commands/auth.js +403 -0
  15. package/dist/commands/component.d.ts +3 -0
  16. package/dist/commands/component.d.ts.map +1 -0
  17. package/dist/commands/component.js +221 -0
  18. package/dist/commands/contribute.d.ts +3 -0
  19. package/dist/commands/contribute.d.ts.map +1 -0
  20. package/dist/commands/contribute.js +318 -0
  21. package/dist/commands/element.d.ts +7 -0
  22. package/dist/commands/element.d.ts.map +1 -0
  23. package/dist/commands/element.js +167 -0
  24. package/dist/commands/export.d.ts +9 -0
  25. package/dist/commands/export.d.ts.map +1 -0
  26. package/dist/commands/export.js +42 -0
  27. package/dist/commands/init.d.ts +49 -0
  28. package/dist/commands/init.d.ts.map +1 -0
  29. package/dist/commands/init.js +275 -0
  30. package/dist/commands/link.d.ts +8 -0
  31. package/dist/commands/link.d.ts.map +1 -0
  32. package/dist/commands/link.js +70 -0
  33. package/dist/commands/resource.d.ts +3 -0
  34. package/dist/commands/resource.d.ts.map +1 -0
  35. package/dist/commands/resource.js +88 -0
  36. package/dist/commands/skills.d.ts +3 -0
  37. package/dist/commands/skills.d.ts.map +1 -0
  38. package/dist/commands/skills.js +113 -0
  39. package/dist/commands/strategy.d.ts +3 -0
  40. package/dist/commands/strategy.d.ts.map +1 -0
  41. package/dist/commands/strategy.js +143 -0
  42. package/dist/commands/webhook.d.ts +3 -0
  43. package/dist/commands/webhook.d.ts.map +1 -0
  44. package/dist/commands/webhook.js +77 -0
  45. package/dist/commands/workspace.d.ts +8 -0
  46. package/dist/commands/workspace.d.ts.map +1 -0
  47. package/dist/commands/workspace.js +219 -0
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/lib/api/client.d.ts +27 -0
  52. package/dist/lib/api/client.d.ts.map +1 -0
  53. package/dist/lib/api/client.js +71 -0
  54. package/dist/lib/api/command-helpers.d.ts +27 -0
  55. package/dist/lib/api/command-helpers.d.ts.map +1 -0
  56. package/dist/lib/api/command-helpers.js +77 -0
  57. package/dist/lib/api/credential-context.d.ts +3 -0
  58. package/dist/lib/api/credential-context.d.ts.map +1 -0
  59. package/dist/lib/api/credential-context.js +13 -0
  60. package/dist/lib/api/export-cli.d.ts +14 -0
  61. package/dist/lib/api/export-cli.d.ts.map +1 -0
  62. package/dist/lib/api/export-cli.js +26 -0
  63. package/dist/lib/api/formats.d.ts +12 -0
  64. package/dist/lib/api/formats.d.ts.map +1 -0
  65. package/dist/lib/api/formats.js +18 -0
  66. package/dist/lib/api/passthrough.d.ts +34 -0
  67. package/dist/lib/api/passthrough.d.ts.map +1 -0
  68. package/dist/lib/api/passthrough.js +106 -0
  69. package/dist/lib/api/payload.d.ts +16 -0
  70. package/dist/lib/api/payload.d.ts.map +1 -0
  71. package/dist/lib/api/payload.js +53 -0
  72. package/dist/lib/api/run-command.d.ts +69 -0
  73. package/dist/lib/api/run-command.d.ts.map +1 -0
  74. package/dist/lib/api/run-command.js +127 -0
  75. package/dist/lib/api/scope.d.ts +8 -0
  76. package/dist/lib/api/scope.d.ts.map +1 -0
  77. package/dist/lib/api/scope.js +16 -0
  78. package/dist/lib/api/transfer.d.ts +57 -0
  79. package/dist/lib/api/transfer.d.ts.map +1 -0
  80. package/dist/lib/api/transfer.js +153 -0
  81. package/dist/lib/auth/api-key-admin.d.ts +39 -0
  82. package/dist/lib/auth/api-key-admin.d.ts.map +1 -0
  83. package/dist/lib/auth/api-key-admin.js +66 -0
  84. package/dist/lib/auth/api-key-store.d.ts +8 -0
  85. package/dist/lib/auth/api-key-store.d.ts.map +1 -0
  86. package/dist/lib/auth/api-key-store.js +109 -0
  87. package/dist/lib/auth/device-flow.d.ts +4 -0
  88. package/dist/lib/auth/device-flow.d.ts.map +1 -0
  89. package/dist/lib/auth/device-flow.js +109 -0
  90. package/dist/lib/auth/entitlements.d.ts +16 -0
  91. package/dist/lib/auth/entitlements.d.ts.map +1 -0
  92. package/dist/lib/auth/entitlements.js +89 -0
  93. package/dist/lib/auth/localhost-flow.d.ts +4 -0
  94. package/dist/lib/auth/localhost-flow.d.ts.map +1 -0
  95. package/dist/lib/auth/localhost-flow.js +134 -0
  96. package/dist/lib/auth/oauth-client.d.ts +33 -0
  97. package/dist/lib/auth/oauth-client.d.ts.map +1 -0
  98. package/dist/lib/auth/oauth-client.js +96 -0
  99. package/dist/lib/auth/token-store.d.ts +6 -0
  100. package/dist/lib/auth/token-store.d.ts.map +1 -0
  101. package/dist/lib/auth/token-store.js +136 -0
  102. package/dist/lib/auth/types.d.ts +49 -0
  103. package/dist/lib/auth/types.d.ts.map +1 -0
  104. package/dist/lib/auth/types.js +1 -0
  105. package/dist/lib/banner.d.ts +2 -0
  106. package/dist/lib/banner.d.ts.map +1 -0
  107. package/dist/lib/banner.js +82 -0
  108. package/dist/lib/config-store.d.ts +23 -0
  109. package/dist/lib/config-store.d.ts.map +1 -0
  110. package/dist/lib/config-store.js +53 -0
  111. package/dist/lib/config.d.ts +20 -0
  112. package/dist/lib/config.d.ts.map +1 -0
  113. package/dist/lib/config.js +33 -0
  114. package/dist/lib/contribute/author-resolver.d.ts +13 -0
  115. package/dist/lib/contribute/author-resolver.d.ts.map +1 -0
  116. package/dist/lib/contribute/author-resolver.js +63 -0
  117. package/dist/lib/contribute/embed-generator.d.ts +4 -0
  118. package/dist/lib/contribute/embed-generator.d.ts.map +1 -0
  119. package/dist/lib/contribute/embed-generator.js +108 -0
  120. package/dist/lib/contribute/link-generator.d.ts +11 -0
  121. package/dist/lib/contribute/link-generator.d.ts.map +1 -0
  122. package/dist/lib/contribute/link-generator.js +119 -0
  123. package/dist/lib/contribute/method-schema.d.ts +239 -0
  124. package/dist/lib/contribute/method-schema.d.ts.map +1 -0
  125. package/dist/lib/contribute/method-schema.js +172 -0
  126. package/dist/lib/contribute/profile-resolver.d.ts +2 -0
  127. package/dist/lib/contribute/profile-resolver.d.ts.map +1 -0
  128. package/dist/lib/contribute/profile-resolver.js +25 -0
  129. package/dist/lib/contribute/types.d.ts +91 -0
  130. package/dist/lib/contribute/types.d.ts.map +1 -0
  131. package/dist/lib/contribute/types.js +1 -0
  132. package/dist/lib/mcp/registrar.d.ts +26 -0
  133. package/dist/lib/mcp/registrar.d.ts.map +1 -0
  134. package/dist/lib/mcp/registrar.js +69 -0
  135. package/dist/lib/mcp/registry.d.ts +54 -0
  136. package/dist/lib/mcp/registry.d.ts.map +1 -0
  137. package/dist/lib/mcp/registry.js +105 -0
  138. package/dist/lib/open.d.ts +2 -0
  139. package/dist/lib/open.d.ts.map +1 -0
  140. package/dist/lib/open.js +24 -0
  141. package/dist/lib/output.d.ts +25 -0
  142. package/dist/lib/output.d.ts.map +1 -0
  143. package/dist/lib/output.js +74 -0
  144. package/dist/lib/skills/installer.d.ts +4 -0
  145. package/dist/lib/skills/installer.d.ts.map +1 -0
  146. package/dist/lib/skills/installer.js +100 -0
  147. package/dist/lib/skills/lock.d.ts +4 -0
  148. package/dist/lib/skills/lock.d.ts.map +1 -0
  149. package/dist/lib/skills/lock.js +28 -0
  150. package/dist/lib/skills/platform-detect.d.ts +4 -0
  151. package/dist/lib/skills/platform-detect.d.ts.map +1 -0
  152. package/dist/lib/skills/platform-detect.js +89 -0
  153. package/dist/lib/skills/types.d.ts +29 -0
  154. package/dist/lib/skills/types.d.ts.map +1 -0
  155. package/dist/lib/skills/types.js +1 -0
  156. package/dist/postinstall.d.ts +2 -0
  157. package/dist/postinstall.d.ts.map +1 -0
  158. package/dist/postinstall.js +11 -0
  159. package/package.json +67 -0
  160. package/skills/.manifest.json +15 -0
  161. package/skills/skhema-challenge-framing/SKILL.md +28 -0
  162. package/skills/skhema-coherence-check/SKILL.md +28 -0
  163. package/skills/skhema-element-decomposition/SKILL.md +28 -0
  164. package/skills/skhema-element-writer/SKILL.md +20 -0
  165. package/skills/skhema-judgment-audit/SKILL.md +28 -0
  166. package/skills/skhema-semantic-sharpening/SKILL.md +28 -0
  167. package/skills/skhema-strategy-advisor/SKILL.md +20 -0
  168. package/skills/skhema-workspace-navigator/SKILL.md +20 -0
@@ -0,0 +1,403 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import readline from 'readline';
4
+ import { resolveClient } from '../lib/api/client.js';
5
+ import { AuthRequiredError, emitSuccess, runCommand, UsageError, } from '../lib/api/run-command.js';
6
+ import { createApiKey, listApiKeys, revokeApiKey, } from '../lib/auth/api-key-admin.js';
7
+ import { clearApiKey, getApiKeyStorageLocation, storeApiKey, } from '../lib/auth/api-key-store.js';
8
+ import { startDeviceFlow } from '../lib/auth/device-flow.js';
9
+ import { clearContributorStatusCache } from '../lib/auth/entitlements.js';
10
+ import { startLocalhostFlow } from '../lib/auth/localhost-flow.js';
11
+ import { ConsentRequiredError, fetchUserInfo, switchOrganization, } from '../lib/auth/oauth-client.js';
12
+ import { clearCredentials, getStoredCredentials, storeCredentials, } from '../lib/auth/token-store.js';
13
+ import { getGlobalOptions, log, logHeader, logTable, outputError, } from '../lib/output.js';
14
+ const VALID_PERMISSIONS = ['read', 'write', 'read-write'];
15
+ const VALID_SCOPES = ['org', 'user'];
16
+ /**
17
+ * Resolve the logged-in user's OAuth token + active org for key-management calls
18
+ * (create/list/revoke go through `api-key-manage`, which requires a user token).
19
+ */
20
+ async function requireOAuthContext() {
21
+ const creds = await getStoredCredentials();
22
+ if (!creds?.access_token) {
23
+ throw new AuthRequiredError('Managing API keys requires an interactive login. Run `skhema auth login` first.');
24
+ }
25
+ const organizationId = creds.organization?.id;
26
+ if (!organizationId) {
27
+ throw new UsageError('No active organization on your session. Run `skhema auth switch` to select one.');
28
+ }
29
+ return { accessToken: creds.access_token, organizationId };
30
+ }
31
+ export function registerAuthCommands(program) {
32
+ const auth = program.command('auth').description('Authentication management');
33
+ auth
34
+ .command('login')
35
+ .description('Authenticate with Skhema')
36
+ .option('--headless', 'Use device code flow (for headless environments)')
37
+ .action(async (options) => {
38
+ // Check if already logged in
39
+ const existing = await getStoredCredentials();
40
+ if (existing) {
41
+ log(chalk.yellow(`Already logged in as ${existing.user.email}. Run 'skhema auth logout' first.`));
42
+ return;
43
+ }
44
+ try {
45
+ if (options.headless) {
46
+ await startDeviceFlow();
47
+ }
48
+ else {
49
+ await startLocalhostFlow();
50
+ }
51
+ }
52
+ catch (error) {
53
+ outputError(error instanceof Error ? error.message : 'Login failed', 'auth login');
54
+ process.exit(1);
55
+ }
56
+ });
57
+ auth
58
+ .command('logout')
59
+ .description('Clear stored credentials')
60
+ .action(async () => {
61
+ try {
62
+ await clearCredentials();
63
+ clearContributorStatusCache();
64
+ log(chalk.green('Logged out successfully.'));
65
+ emitSuccess('auth logout', { loggedOut: true });
66
+ }
67
+ catch (error) {
68
+ outputError(error instanceof Error ? error.message : 'Logout failed', 'auth logout');
69
+ process.exit(1);
70
+ }
71
+ });
72
+ auth
73
+ .command('status')
74
+ .description('Show which credential the CLI will use, and for which org')
75
+ .action(async () => {
76
+ // `status` reports the resolved credential lane (spec §3.1 precedence).
77
+ // Absence of any credential is a normal, non-error outcome here.
78
+ let resolved;
79
+ try {
80
+ resolved = await resolveClient();
81
+ }
82
+ catch (error) {
83
+ if (error instanceof AuthRequiredError) {
84
+ log(chalk.yellow('Not authenticated. Run `skhema auth login`, or set SKHEMA_API_KEY / `skhema auth key use <key>`.'));
85
+ emitSuccess('auth status', { authenticated: false });
86
+ return;
87
+ }
88
+ outputError(error instanceof Error ? error.message : 'Failed to get status', 'auth status');
89
+ process.exit(1);
90
+ }
91
+ const cred = resolved.credential;
92
+ if (cred.kind === 'oauth') {
93
+ const creds = await getStoredCredentials();
94
+ const expiresAt = creds ? new Date(creds.expires_at) : null;
95
+ const isExpired = expiresAt ? expiresAt < new Date() : false;
96
+ if (getGlobalOptions().json) {
97
+ emitSuccess('auth status', {
98
+ authenticated: true,
99
+ credential: {
100
+ lane: cred.lane,
101
+ kind: cred.kind,
102
+ source: cred.source,
103
+ },
104
+ expired: isExpired,
105
+ user: creds?.user,
106
+ organization: creds?.organization,
107
+ expiresAt: expiresAt?.toISOString(),
108
+ scope: creds?.scope,
109
+ });
110
+ return;
111
+ }
112
+ logHeader('Authentication Status');
113
+ log(` Credential: ${chalk.cyan(cred.source)}`);
114
+ if (creds) {
115
+ log(` User: ${chalk.cyan(creds.user.name)} (${creds.user.email})`);
116
+ if (creds.organization) {
117
+ log(` Organization: ${chalk.cyan(creds.organization.name)}`);
118
+ }
119
+ log(` Token: ${isExpired ? chalk.red('Expired') : chalk.green('Valid')}`);
120
+ log(` Expires: ${expiresAt?.toLocaleString()}`);
121
+ log(` Scopes: ${creds.scope}`);
122
+ }
123
+ return;
124
+ }
125
+ // API-key lane: verify the key by exchanging it (health forces the
126
+ // exchange), then report the org + permission carried by the key.
127
+ let verified = false;
128
+ let permission;
129
+ let organizationId;
130
+ try {
131
+ await resolved.client.meta.health();
132
+ const ctx = resolved.client.getContext();
133
+ permission = ctx.permission;
134
+ organizationId = ctx.organizationId;
135
+ verified = true;
136
+ }
137
+ catch {
138
+ verified = false;
139
+ }
140
+ if (getGlobalOptions().json) {
141
+ emitSuccess('auth status', {
142
+ authenticated: true,
143
+ credential: {
144
+ lane: cred.lane,
145
+ kind: cred.kind,
146
+ source: cred.source,
147
+ maskedKey: cred.maskedKey,
148
+ },
149
+ verified,
150
+ permission,
151
+ organization: organizationId ? { id: organizationId } : undefined,
152
+ });
153
+ return;
154
+ }
155
+ logHeader('Authentication Status');
156
+ log(` Credential: ${chalk.cyan(cred.source)}`);
157
+ log(` Key: ${cred.maskedKey}`);
158
+ log(` Verified: ${verified ? chalk.green('Yes') : chalk.red('No (invalid/expired key or network error)')}`);
159
+ if (verified) {
160
+ log(` Permission: ${permission ?? 'unknown'}`);
161
+ log(` Organization: ${organizationId ?? 'unknown'}`);
162
+ }
163
+ });
164
+ auth
165
+ .command('switch [org-slug]')
166
+ .description('Switch active organization')
167
+ .option('--headless', 'Use device code flow for consent fallback')
168
+ .action(async (orgSlug, options) => {
169
+ try {
170
+ const creds = await getStoredCredentials();
171
+ if (!creds) {
172
+ outputError('Not logged in. Run `skhema auth login` to authenticate.', 'auth switch');
173
+ process.exit(1);
174
+ }
175
+ const spinner = ora('Fetching organizations...').start();
176
+ const userInfo = await fetchUserInfo(creds.access_token);
177
+ spinner.stop();
178
+ const orgs = userInfo.organizations ?? [];
179
+ if (orgs.length === 0) {
180
+ outputError('No organizations found for your account.', 'auth switch');
181
+ process.exit(1);
182
+ }
183
+ // If --json and no slug, output org list
184
+ if (getGlobalOptions().json && !orgSlug) {
185
+ emitSuccess('auth switch', {
186
+ current: creds.organization?.slug ?? null,
187
+ organizations: orgs,
188
+ });
189
+ return;
190
+ }
191
+ let targetOrg;
192
+ if (orgSlug) {
193
+ // Direct slug lookup
194
+ targetOrg = orgs.find((o) => o.slug === orgSlug);
195
+ if (!targetOrg) {
196
+ outputError(`Organization '${orgSlug}' not found. Available: ${orgs.map((o) => o.slug).join(', ')}`, 'auth switch');
197
+ process.exit(1);
198
+ }
199
+ }
200
+ else {
201
+ // Interactive picker
202
+ logHeader('Select Organization');
203
+ orgs.forEach((org, i) => {
204
+ const current = creds.organization?.id === org.id ? chalk.green(' (current)') : '';
205
+ log(` ${chalk.dim(`${i + 1}.`)} ${org.name} ${chalk.dim(`(${org.slug})`)}${current}`);
206
+ });
207
+ log('');
208
+ const rl = readline.createInterface({
209
+ input: process.stdin,
210
+ output: process.stdout,
211
+ });
212
+ const answer = await new Promise((resolve) => {
213
+ rl.question(` Enter number (1-${orgs.length}): `, (ans) => {
214
+ rl.close();
215
+ resolve(ans.trim());
216
+ });
217
+ });
218
+ const index = parseInt(answer, 10) - 1;
219
+ if (isNaN(index) || index < 0 || index >= orgs.length) {
220
+ outputError('Invalid selection.', 'auth switch');
221
+ process.exit(1);
222
+ }
223
+ targetOrg = orgs[index];
224
+ }
225
+ // Skip if already on the target org
226
+ if (creds.organization?.id === targetOrg.id) {
227
+ log(chalk.yellow(`Already on organization '${targetOrg.name}'.`));
228
+ return;
229
+ }
230
+ // Attempt fast switch via server endpoint
231
+ const switchSpinner = ora(`Switching to ${targetOrg.name}...`).start();
232
+ try {
233
+ const result = await switchOrganization(creds.access_token, targetOrg.id);
234
+ // Update stored credentials
235
+ await storeCredentials({
236
+ ...creds,
237
+ access_token: result.access_token,
238
+ refresh_token: result.refresh_token,
239
+ token_type: result.token_type,
240
+ expires_at: Date.now() + result.expires_in * 1000,
241
+ scope: result.scope,
242
+ organization: result.organization ?? {
243
+ id: targetOrg.id,
244
+ name: targetOrg.name,
245
+ slug: targetOrg.slug,
246
+ },
247
+ });
248
+ switchSpinner.succeed(`Switched to ${chalk.cyan(targetOrg.name)} (${targetOrg.slug})`);
249
+ emitSuccess('auth switch', {
250
+ switched: true,
251
+ organization: result.organization ?? targetOrg,
252
+ });
253
+ }
254
+ catch (err) {
255
+ if (err instanceof ConsentRequiredError) {
256
+ switchSpinner.warn('Consent required — opening browser for authorization...');
257
+ if (options.headless) {
258
+ await startDeviceFlow({ organizationId: targetOrg.id });
259
+ }
260
+ else {
261
+ await startLocalhostFlow({ organizationId: targetOrg.id });
262
+ }
263
+ }
264
+ else {
265
+ switchSpinner.fail('Switch failed');
266
+ throw err;
267
+ }
268
+ }
269
+ }
270
+ catch (error) {
271
+ if (!(error instanceof ConsentRequiredError) &&
272
+ !(error instanceof Error && error.message.includes('Logged in as'))) {
273
+ outputError(error instanceof Error
274
+ ? error.message
275
+ : 'Organization switch failed', 'auth switch');
276
+ process.exit(1);
277
+ }
278
+ }
279
+ });
280
+ registerAuthKeyCommands(auth);
281
+ }
282
+ /**
283
+ * `skhema auth key …` — the API-key credential lane for CI and agents.
284
+ * `use`/`remove` manage the locally stored key; `create`/`list`/`revoke` drive
285
+ * the server-side `api-key-manage` function with the logged-in user's token
286
+ * (issuance authorization is enforced there, unchanged).
287
+ */
288
+ function registerAuthKeyCommands(auth) {
289
+ const key = auth
290
+ .command('key')
291
+ .description('Manage API keys for headless / CI / agent use');
292
+ key
293
+ .command('use <api-key>')
294
+ .description('Store an API key (sk_live_…) as the CLI credential')
295
+ .action(async (apiKey) => {
296
+ await runCommand('auth key use', async () => {
297
+ if (!apiKey.startsWith('sk_live_')) {
298
+ throw new UsageError('Invalid API key: expected a key starting with "sk_live_".');
299
+ }
300
+ storeApiKey(apiKey);
301
+ const location = getApiKeyStorageLocation();
302
+ const maskedKey = `sk_live_...${apiKey.slice(-4)}`;
303
+ log(chalk.green(`Stored API key ${maskedKey}.`));
304
+ log(chalk.dim(` Location: ${location}`));
305
+ log(chalk.dim(' This key is used only when no --api-key flag or SKHEMA_API_KEY env is set.'));
306
+ return { stored: true, maskedKey, location };
307
+ });
308
+ });
309
+ key
310
+ .command('remove')
311
+ .description('Remove the stored API key')
312
+ .action(async () => {
313
+ await runCommand('auth key remove', async () => {
314
+ clearApiKey();
315
+ log(chalk.green('Removed stored API key.'));
316
+ return { removed: true };
317
+ });
318
+ });
319
+ key
320
+ .command('create')
321
+ .description('Create a new API key (requires an interactive login)')
322
+ .requiredOption('--name <name>', 'A label for the key')
323
+ .option('--permission <permission>', 'read | write | read-write', 'read-write')
324
+ .option('--scope <scope>', 'org | user', 'org')
325
+ .option('--use', 'Store the new key as the CLI credential immediately')
326
+ .action(async (options) => {
327
+ await runCommand('auth key create', async () => {
328
+ if (!VALID_PERMISSIONS.includes(options.permission)) {
329
+ throw new UsageError(`--permission must be one of: ${VALID_PERMISSIONS.join(', ')}`);
330
+ }
331
+ if (!VALID_SCOPES.includes(options.scope)) {
332
+ throw new UsageError(`--scope must be one of: ${VALID_SCOPES.join(', ')}`);
333
+ }
334
+ const { accessToken, organizationId } = await requireOAuthContext();
335
+ const created = await createApiKey(accessToken, {
336
+ organizationId,
337
+ name: options.name,
338
+ permission: options.permission,
339
+ scope: options.scope,
340
+ });
341
+ if (options.use)
342
+ storeApiKey(created.rawKey);
343
+ // The raw key is shown once — this is the only place it is ever printed.
344
+ logHeader('API key created');
345
+ log(` Name: ${chalk.cyan(created.name)}`);
346
+ log(` Permission: ${created.permission}`);
347
+ log(` Scope: ${created.scope}`);
348
+ log('');
349
+ log(` ${chalk.bold('Key (shown once):')} ${chalk.yellow(created.rawKey)}`);
350
+ log('');
351
+ log(chalk.dim(options.use
352
+ ? ' Stored as the CLI credential (--use).'
353
+ : ' Store it now: skhema auth key use ' + created.rawKey));
354
+ return {
355
+ id: created.id,
356
+ name: created.name,
357
+ permission: created.permission,
358
+ scope: created.scope,
359
+ rawKey: created.rawKey,
360
+ stored: Boolean(options.use),
361
+ };
362
+ });
363
+ });
364
+ key
365
+ .command('list')
366
+ .description('List the organization’s API keys (masked)')
367
+ .action(async () => {
368
+ await runCommand('auth key list', async () => {
369
+ const { accessToken, organizationId } = await requireOAuthContext();
370
+ const result = await listApiKeys(accessToken, organizationId);
371
+ if (!getGlobalOptions().json) {
372
+ logHeader(`API keys (${result.limits.used}/${result.limits.max} used)`);
373
+ if (result.keys.length === 0) {
374
+ log(chalk.dim(' No API keys.'));
375
+ }
376
+ else {
377
+ logTable(['Prefix', 'Name', 'Perm', 'Scope', 'Status', 'Last used'], result.keys.map((k) => [
378
+ k.keyPrefix,
379
+ k.name,
380
+ k.permission,
381
+ k.scope,
382
+ k.revokedAt ? 'revoked' : 'active',
383
+ k.lastUsedAt
384
+ ? new Date(k.lastUsedAt).toLocaleDateString()
385
+ : 'never',
386
+ ]));
387
+ }
388
+ }
389
+ return result;
390
+ });
391
+ });
392
+ key
393
+ .command('revoke <key-id>')
394
+ .description('Revoke an API key by id')
395
+ .action(async (keyId) => {
396
+ await runCommand('auth key revoke', async () => {
397
+ const { accessToken } = await requireOAuthContext();
398
+ const result = await revokeApiKey(accessToken, keyId);
399
+ log(chalk.green(`Revoked API key ${keyId}.`));
400
+ return result;
401
+ });
402
+ });
403
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerComponentCommands(program: Command): void;
3
+ //# sourceMappingURL=component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/commands/component.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAkEnC,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkKhE"}
@@ -0,0 +1,221 @@
1
+ import chalk from 'chalk';
2
+ import { resolveClient } from '../lib/api/client.js';
3
+ import { assertExportOutput, reportExport } from '../lib/api/export-cli.js';
4
+ import { assertFormat, DOCUMENT_FORMATS } from '../lib/api/formats.js';
5
+ import { resolveBody } from '../lib/api/payload.js';
6
+ import { runCommand } from '../lib/api/run-command.js';
7
+ import { resolveWorkspaceId } from '../lib/api/scope.js';
8
+ import { performExport } from '../lib/api/transfer.js';
9
+ import { log, logHeader, logTable } from '../lib/output.js';
10
+ /** Attach the shared `--workspace` selector to a workspace-scoped subcommand. */
11
+ function withWorkspace(cmd) {
12
+ return cmd.option('--workspace <id>', 'Workspace id (defaults to `skhema workspace use`)');
13
+ }
14
+ function renderComponents(components = []) {
15
+ if (components.length === 0) {
16
+ log(chalk.dim('No components.'));
17
+ return;
18
+ }
19
+ logHeader(`Components (${components.length})`);
20
+ logTable(['id', 'name', 'type', 'pos'], components.map((c) => [
21
+ c.id,
22
+ c.name ?? '',
23
+ c.componentType ?? '',
24
+ c.position === undefined ? '' : String(c.position),
25
+ ]));
26
+ }
27
+ function renderComponentLinks(links = []) {
28
+ if (links.length === 0) {
29
+ log(chalk.dim('No component links.'));
30
+ return;
31
+ }
32
+ logHeader(`Component links (${links.length})`);
33
+ logTable(['id', 'source', 'target', 'status'], links.map((l) => [
34
+ l.id,
35
+ l.sourceComponentId ?? '',
36
+ l.targetComponentId ?? '',
37
+ l.status ?? '',
38
+ ]));
39
+ }
40
+ export function registerComponentCommands(program) {
41
+ const component = program
42
+ .command('component')
43
+ .description('Component commands');
44
+ withWorkspace(component.command('list'))
45
+ .description('List components in a workspace')
46
+ .action(async (opts) => {
47
+ await runCommand('component list', async () => {
48
+ const { client } = await resolveClient();
49
+ const ws = resolveWorkspaceId(opts.workspace);
50
+ const data = await client.components.list(ws);
51
+ renderComponents(data.components);
52
+ return data;
53
+ });
54
+ });
55
+ withWorkspace(component.command('create'))
56
+ .description('Create a component')
57
+ .option('--type <componentType>', 'Component type (e.g. diagnosis)')
58
+ .option('--name <name>', 'Component name')
59
+ .option('--file <path>', 'JSON payload file (or - for stdin)')
60
+ .action(async (opts) => {
61
+ await runCommand('component create', async () => {
62
+ const { client } = await resolveClient();
63
+ const ws = resolveWorkspaceId(opts.workspace);
64
+ const body = resolveBody({
65
+ file: opts.file,
66
+ fields: { componentType: opts.type, name: opts.name },
67
+ });
68
+ const data = await client.components.create(ws, body);
69
+ log(chalk.green(`Created component ${chalk.cyan(data.component?.id)}`));
70
+ return data;
71
+ });
72
+ });
73
+ withWorkspace(component.command('update <componentId>'))
74
+ .description('Update a component')
75
+ .option('--name <name>', 'New name')
76
+ .option('--position <n>', 'New position')
77
+ .option('--file <path>', 'JSON payload file (or - for stdin)')
78
+ .action(async (componentId, opts) => {
79
+ await runCommand('component update', async () => {
80
+ const { client } = await resolveClient();
81
+ const ws = resolveWorkspaceId(opts.workspace);
82
+ const fields = {};
83
+ if (opts.name !== undefined)
84
+ fields.name = opts.name;
85
+ if (opts.position !== undefined)
86
+ fields.position = Number(opts.position);
87
+ const body = resolveBody({ file: opts.file, fields });
88
+ const data = await client.components.update(ws, componentId, body);
89
+ log(chalk.green(`Updated component ${chalk.cyan(componentId)}`));
90
+ return data;
91
+ });
92
+ });
93
+ withWorkspace(component.command('delete <componentId>'))
94
+ .description('Delete a component')
95
+ .action(async (componentId, opts) => {
96
+ await runCommand('component delete', async () => {
97
+ const { client } = await resolveClient();
98
+ const ws = resolveWorkspaceId(opts.workspace);
99
+ const data = await client.components.delete(ws, componentId);
100
+ log(chalk.green(`Deleted component ${chalk.cyan(componentId)}`));
101
+ return data;
102
+ });
103
+ });
104
+ withWorkspace(component.command('duplicate <componentId>'))
105
+ .description('Duplicate a component')
106
+ .action(async (componentId, opts) => {
107
+ await runCommand('component duplicate', async () => {
108
+ const { client } = await resolveClient();
109
+ const ws = resolveWorkspaceId(opts.workspace);
110
+ const data = await client.components.duplicate(ws, componentId);
111
+ log(chalk.green(`Duplicated component ${chalk.cyan(componentId)}`));
112
+ return data;
113
+ });
114
+ });
115
+ withWorkspace(component.command('reorder'))
116
+ .description('Reorder components (payload: { positions: [{ componentId, position }] })')
117
+ .requiredOption('--file <path>', 'JSON payload file (or - for stdin)')
118
+ .action(async (opts) => {
119
+ await runCommand('component reorder', async () => {
120
+ const { client } = await resolveClient();
121
+ const ws = resolveWorkspaceId(opts.workspace);
122
+ const body = resolveBody({ file: opts.file });
123
+ const data = await client.components.reorder(ws, body);
124
+ log(chalk.green('Reordered components'));
125
+ return data;
126
+ });
127
+ });
128
+ withWorkspace(component.command('export <componentId>'))
129
+ .description('Export a component (pdf/docx/md/json)')
130
+ .requiredOption('--format <format>', 'pdf | docx | md | json')
131
+ .option('--out <path>', 'Write to this path (- for stdout; default: server filename in cwd)')
132
+ .option('--email <recipient>', 'Email the export to an org member instead of downloading')
133
+ .action(async (componentId, opts) => {
134
+ await runCommand('component export', async () => {
135
+ const { client } = await resolveClient();
136
+ const ws = resolveWorkspaceId(opts.workspace);
137
+ const format = assertFormat(opts.format, DOCUMENT_FORMATS);
138
+ assertExportOutput(opts);
139
+ const result = await performExport({
140
+ out: opts.email ? undefined : opts.out,
141
+ exec: () => client.components.export(ws, componentId, {
142
+ format,
143
+ ...(opts.email
144
+ ? { delivery: 'email', recipientEmail: opts.email }
145
+ : {}),
146
+ }),
147
+ });
148
+ reportExport(result);
149
+ return result;
150
+ });
151
+ });
152
+ registerComponentLinkCommands(program);
153
+ }
154
+ function registerComponentLinkCommands(program) {
155
+ const link = program
156
+ .command('component-link')
157
+ .description('Component-to-component link commands');
158
+ withWorkspace(link.command('list'))
159
+ .description('List component links')
160
+ .action(async (opts) => {
161
+ await runCommand('component-link list', async () => {
162
+ const { client } = await resolveClient();
163
+ const ws = resolveWorkspaceId(opts.workspace);
164
+ const data = await client.components.links.list(ws);
165
+ renderComponentLinks(data.componentLinks ?? data.links);
166
+ return data;
167
+ });
168
+ });
169
+ withWorkspace(link.command('create'))
170
+ .description('Create a component link')
171
+ .option('--source <id>', 'Source component id')
172
+ .option('--target <id>', 'Target component id')
173
+ .option('--notes <text>', 'Notes')
174
+ .option('--file <path>', 'JSON payload file (or - for stdin)')
175
+ .action(async (opts) => {
176
+ await runCommand('component-link create', async () => {
177
+ const { client } = await resolveClient();
178
+ const ws = resolveWorkspaceId(opts.workspace);
179
+ const body = resolveBody({
180
+ file: opts.file,
181
+ fields: {
182
+ sourceComponentId: opts.source,
183
+ targetComponentId: opts.target,
184
+ notes: opts.notes,
185
+ },
186
+ });
187
+ const data = await client.components.links.create(ws, body);
188
+ log(chalk.green('Created component link'));
189
+ return data;
190
+ });
191
+ });
192
+ withWorkspace(link.command('update <componentLinkId>'))
193
+ .description('Update a component link')
194
+ .option('--notes <text>', 'Notes')
195
+ .option('--status <status>', 'Status')
196
+ .option('--file <path>', 'JSON payload file (or - for stdin)')
197
+ .action(async (componentLinkId, opts) => {
198
+ await runCommand('component-link update', async () => {
199
+ const { client } = await resolveClient();
200
+ const ws = resolveWorkspaceId(opts.workspace);
201
+ const body = resolveBody({
202
+ file: opts.file,
203
+ fields: { notes: opts.notes, status: opts.status },
204
+ });
205
+ const data = await client.components.links.update(ws, componentLinkId, body);
206
+ log(chalk.green(`Updated component link ${chalk.cyan(componentLinkId)}`));
207
+ return data;
208
+ });
209
+ });
210
+ withWorkspace(link.command('delete <componentLinkId>'))
211
+ .description('Delete a component link')
212
+ .action(async (componentLinkId, opts) => {
213
+ await runCommand('component-link delete', async () => {
214
+ const { client } = await resolveClient();
215
+ const ws = resolveWorkspaceId(opts.workspace);
216
+ const data = await client.components.links.delete(ws, componentLinkId);
217
+ log(chalk.green(`Deleted component link ${chalk.cyan(componentLinkId)}`));
218
+ return data;
219
+ });
220
+ });
221
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerContributeCommands(program: Command): void;
3
+ //# sourceMappingURL=contribute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contribute.d.ts","sourceRoot":"","sources":["../../src/commands/contribute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAmFnC,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8VjE"}