@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.
- package/README.md +163 -0
- package/bin/skhema.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +92 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +154 -0
- package/dist/commands/api.d.ts +19 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +97 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +403 -0
- package/dist/commands/component.d.ts +3 -0
- package/dist/commands/component.d.ts.map +1 -0
- package/dist/commands/component.js +221 -0
- package/dist/commands/contribute.d.ts +3 -0
- package/dist/commands/contribute.d.ts.map +1 -0
- package/dist/commands/contribute.js +318 -0
- package/dist/commands/element.d.ts +7 -0
- package/dist/commands/element.d.ts.map +1 -0
- package/dist/commands/element.js +167 -0
- package/dist/commands/export.d.ts +9 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +42 -0
- package/dist/commands/init.d.ts +49 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +275 -0
- package/dist/commands/link.d.ts +8 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +70 -0
- package/dist/commands/resource.d.ts +3 -0
- package/dist/commands/resource.d.ts.map +1 -0
- package/dist/commands/resource.js +88 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +113 -0
- package/dist/commands/strategy.d.ts +3 -0
- package/dist/commands/strategy.d.ts.map +1 -0
- package/dist/commands/strategy.js +143 -0
- package/dist/commands/webhook.d.ts +3 -0
- package/dist/commands/webhook.d.ts.map +1 -0
- package/dist/commands/webhook.js +77 -0
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +219 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/api/client.d.ts +27 -0
- package/dist/lib/api/client.d.ts.map +1 -0
- package/dist/lib/api/client.js +71 -0
- package/dist/lib/api/command-helpers.d.ts +27 -0
- package/dist/lib/api/command-helpers.d.ts.map +1 -0
- package/dist/lib/api/command-helpers.js +77 -0
- package/dist/lib/api/credential-context.d.ts +3 -0
- package/dist/lib/api/credential-context.d.ts.map +1 -0
- package/dist/lib/api/credential-context.js +13 -0
- package/dist/lib/api/export-cli.d.ts +14 -0
- package/dist/lib/api/export-cli.d.ts.map +1 -0
- package/dist/lib/api/export-cli.js +26 -0
- package/dist/lib/api/formats.d.ts +12 -0
- package/dist/lib/api/formats.d.ts.map +1 -0
- package/dist/lib/api/formats.js +18 -0
- package/dist/lib/api/passthrough.d.ts +34 -0
- package/dist/lib/api/passthrough.d.ts.map +1 -0
- package/dist/lib/api/passthrough.js +106 -0
- package/dist/lib/api/payload.d.ts +16 -0
- package/dist/lib/api/payload.d.ts.map +1 -0
- package/dist/lib/api/payload.js +53 -0
- package/dist/lib/api/run-command.d.ts +69 -0
- package/dist/lib/api/run-command.d.ts.map +1 -0
- package/dist/lib/api/run-command.js +127 -0
- package/dist/lib/api/scope.d.ts +8 -0
- package/dist/lib/api/scope.d.ts.map +1 -0
- package/dist/lib/api/scope.js +16 -0
- package/dist/lib/api/transfer.d.ts +57 -0
- package/dist/lib/api/transfer.d.ts.map +1 -0
- package/dist/lib/api/transfer.js +153 -0
- package/dist/lib/auth/api-key-admin.d.ts +39 -0
- package/dist/lib/auth/api-key-admin.d.ts.map +1 -0
- package/dist/lib/auth/api-key-admin.js +66 -0
- package/dist/lib/auth/api-key-store.d.ts +8 -0
- package/dist/lib/auth/api-key-store.d.ts.map +1 -0
- package/dist/lib/auth/api-key-store.js +109 -0
- package/dist/lib/auth/device-flow.d.ts +4 -0
- package/dist/lib/auth/device-flow.d.ts.map +1 -0
- package/dist/lib/auth/device-flow.js +109 -0
- package/dist/lib/auth/entitlements.d.ts +16 -0
- package/dist/lib/auth/entitlements.d.ts.map +1 -0
- package/dist/lib/auth/entitlements.js +89 -0
- package/dist/lib/auth/localhost-flow.d.ts +4 -0
- package/dist/lib/auth/localhost-flow.d.ts.map +1 -0
- package/dist/lib/auth/localhost-flow.js +134 -0
- package/dist/lib/auth/oauth-client.d.ts +33 -0
- package/dist/lib/auth/oauth-client.d.ts.map +1 -0
- package/dist/lib/auth/oauth-client.js +96 -0
- package/dist/lib/auth/token-store.d.ts +6 -0
- package/dist/lib/auth/token-store.d.ts.map +1 -0
- package/dist/lib/auth/token-store.js +136 -0
- package/dist/lib/auth/types.d.ts +49 -0
- package/dist/lib/auth/types.d.ts.map +1 -0
- package/dist/lib/auth/types.js +1 -0
- package/dist/lib/banner.d.ts +2 -0
- package/dist/lib/banner.d.ts.map +1 -0
- package/dist/lib/banner.js +82 -0
- package/dist/lib/config-store.d.ts +23 -0
- package/dist/lib/config-store.d.ts.map +1 -0
- package/dist/lib/config-store.js +53 -0
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +33 -0
- package/dist/lib/contribute/author-resolver.d.ts +13 -0
- package/dist/lib/contribute/author-resolver.d.ts.map +1 -0
- package/dist/lib/contribute/author-resolver.js +63 -0
- package/dist/lib/contribute/embed-generator.d.ts +4 -0
- package/dist/lib/contribute/embed-generator.d.ts.map +1 -0
- package/dist/lib/contribute/embed-generator.js +108 -0
- package/dist/lib/contribute/link-generator.d.ts +11 -0
- package/dist/lib/contribute/link-generator.d.ts.map +1 -0
- package/dist/lib/contribute/link-generator.js +119 -0
- package/dist/lib/contribute/method-schema.d.ts +239 -0
- package/dist/lib/contribute/method-schema.d.ts.map +1 -0
- package/dist/lib/contribute/method-schema.js +172 -0
- package/dist/lib/contribute/profile-resolver.d.ts +2 -0
- package/dist/lib/contribute/profile-resolver.d.ts.map +1 -0
- package/dist/lib/contribute/profile-resolver.js +25 -0
- package/dist/lib/contribute/types.d.ts +91 -0
- package/dist/lib/contribute/types.d.ts.map +1 -0
- package/dist/lib/contribute/types.js +1 -0
- package/dist/lib/mcp/registrar.d.ts +26 -0
- package/dist/lib/mcp/registrar.d.ts.map +1 -0
- package/dist/lib/mcp/registrar.js +69 -0
- package/dist/lib/mcp/registry.d.ts +54 -0
- package/dist/lib/mcp/registry.d.ts.map +1 -0
- package/dist/lib/mcp/registry.js +105 -0
- package/dist/lib/open.d.ts +2 -0
- package/dist/lib/open.d.ts.map +1 -0
- package/dist/lib/open.js +24 -0
- package/dist/lib/output.d.ts +25 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +74 -0
- package/dist/lib/skills/installer.d.ts +4 -0
- package/dist/lib/skills/installer.d.ts.map +1 -0
- package/dist/lib/skills/installer.js +100 -0
- package/dist/lib/skills/lock.d.ts +4 -0
- package/dist/lib/skills/lock.d.ts.map +1 -0
- package/dist/lib/skills/lock.js +28 -0
- package/dist/lib/skills/platform-detect.d.ts +4 -0
- package/dist/lib/skills/platform-detect.d.ts.map +1 -0
- package/dist/lib/skills/platform-detect.js +89 -0
- package/dist/lib/skills/types.d.ts +29 -0
- package/dist/lib/skills/types.d.ts.map +1 -0
- package/dist/lib/skills/types.js +1 -0
- package/dist/postinstall.d.ts +2 -0
- package/dist/postinstall.d.ts.map +1 -0
- package/dist/postinstall.js +11 -0
- package/package.json +67 -0
- package/skills/.manifest.json +15 -0
- package/skills/skhema-challenge-framing/SKILL.md +28 -0
- package/skills/skhema-coherence-check/SKILL.md +28 -0
- package/skills/skhema-element-decomposition/SKILL.md +28 -0
- package/skills/skhema-element-writer/SKILL.md +20 -0
- package/skills/skhema-judgment-audit/SKILL.md +28 -0
- package/skills/skhema-semantic-sharpening/SKILL.md +28 -0
- package/skills/skhema-strategy-advisor/SKILL.md +20 -0
- 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 @@
|
|
|
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 @@
|
|
|
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"}
|