@totoday/quinn-cli 0.1.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.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +506 -0
- package/package.json +20 -0
- package/src/index.ts +595 -0
- package/tsconfig.json +9 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const commander_1 = require("commander");
|
|
11
|
+
const quinn_sdk_1 = require("@totoday/quinn-sdk");
|
|
12
|
+
function print(data) {
|
|
13
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
function asCsv(raw) {
|
|
16
|
+
if (!raw) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return raw
|
|
20
|
+
.split(',')
|
|
21
|
+
.map((item) => item.trim())
|
|
22
|
+
.filter((item) => item.length > 0);
|
|
23
|
+
}
|
|
24
|
+
function asPrivilege(raw) {
|
|
25
|
+
const values = asCsv(raw);
|
|
26
|
+
if (values.length === 0) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
if (values.length === 1) {
|
|
30
|
+
return values[0];
|
|
31
|
+
}
|
|
32
|
+
return values;
|
|
33
|
+
}
|
|
34
|
+
function maskToken(token) {
|
|
35
|
+
if (!token) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (token.length <= 12) {
|
|
39
|
+
return '***';
|
|
40
|
+
}
|
|
41
|
+
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
42
|
+
}
|
|
43
|
+
function getConfigPath(opts) {
|
|
44
|
+
return (opts.configPath ||
|
|
45
|
+
process.env.QUINN_CONFIG_PATH ||
|
|
46
|
+
node_path_1.default.join(node_os_1.default.homedir(), '.config', 'quinn', 'config.json'));
|
|
47
|
+
}
|
|
48
|
+
function readConfig(configPath) {
|
|
49
|
+
if (!node_fs_1.default.existsSync(configPath)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const raw = node_fs_1.default.readFileSync(configPath, 'utf8');
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
return parsed ?? {};
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeConfig(configPath, config) {
|
|
62
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
|
|
63
|
+
node_fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
64
|
+
}
|
|
65
|
+
function resolveRuntimeConfig(opts, fileConfig) {
|
|
66
|
+
return {
|
|
67
|
+
apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl,
|
|
68
|
+
token: opts.apiToken || opts.token || process.env.QUINN_API_TOKEN || fileConfig.token,
|
|
69
|
+
orgId: opts.orgId || process.env.QUINN_ORG_ID || fileConfig.orgId,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createClient(config) {
|
|
73
|
+
const { apiUrl, token, orgId } = config;
|
|
74
|
+
if (!apiUrl) {
|
|
75
|
+
throw new Error('missing apiUrl: set --api-url or QUINN_API_URL or config');
|
|
76
|
+
}
|
|
77
|
+
if (!token) {
|
|
78
|
+
throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
|
|
79
|
+
}
|
|
80
|
+
if (!orgId) {
|
|
81
|
+
throw new Error('missing orgId: set --org-id or QUINN_ORG_ID or config');
|
|
82
|
+
}
|
|
83
|
+
return new quinn_sdk_1.Quinn({ apiUrl, token, orgId });
|
|
84
|
+
}
|
|
85
|
+
function getContext(command) {
|
|
86
|
+
const global = command.optsWithGlobals();
|
|
87
|
+
const configPath = getConfigPath(global);
|
|
88
|
+
const fileConfig = readConfig(configPath);
|
|
89
|
+
const runtimeConfig = resolveRuntimeConfig(global, fileConfig);
|
|
90
|
+
return { global, configPath, fileConfig, runtimeConfig };
|
|
91
|
+
}
|
|
92
|
+
async function withHandler(fn) {
|
|
93
|
+
try {
|
|
94
|
+
await fn();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
let message = '';
|
|
98
|
+
if (typeof error === 'object' &&
|
|
99
|
+
error !== null &&
|
|
100
|
+
'isAxiosError' in error &&
|
|
101
|
+
error.isAxiosError) {
|
|
102
|
+
const axiosError = error;
|
|
103
|
+
const status = axiosError.response?.status;
|
|
104
|
+
const responseData = axiosError.response?.data;
|
|
105
|
+
const details = typeof responseData === 'string'
|
|
106
|
+
? responseData
|
|
107
|
+
: responseData
|
|
108
|
+
? JSON.stringify(responseData)
|
|
109
|
+
: '';
|
|
110
|
+
message = [
|
|
111
|
+
axiosError.message || 'HTTP request failed',
|
|
112
|
+
status ? `(status: ${status})` : '',
|
|
113
|
+
details ? `response: ${details}` : '',
|
|
114
|
+
]
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join(' ');
|
|
117
|
+
}
|
|
118
|
+
else if (error instanceof Error) {
|
|
119
|
+
message = error.message;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
message = String(error);
|
|
123
|
+
}
|
|
124
|
+
process.stderr.write(`${message || 'Unknown error'}\n`);
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const program = new commander_1.Command();
|
|
129
|
+
program
|
|
130
|
+
.name('quinn')
|
|
131
|
+
.description('Quinn CLI')
|
|
132
|
+
.addHelpText('after', [
|
|
133
|
+
'',
|
|
134
|
+
'Examples:',
|
|
135
|
+
' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
|
|
136
|
+
' quinn organizations details',
|
|
137
|
+
' quinn members find alice',
|
|
138
|
+
' quinn members get user-1,user-2,user@example.com',
|
|
139
|
+
].join('\n'))
|
|
140
|
+
.showHelpAfterError()
|
|
141
|
+
.option('--config-path <path>', 'config file path (default ~/.config/quinn/config.json)')
|
|
142
|
+
.option('--api-url <url>', 'Quinn API base url')
|
|
143
|
+
.option('--api-token <token>', 'Quinn API token')
|
|
144
|
+
.option('--token <token>', 'alias of --api-token')
|
|
145
|
+
.option('--org-id <id>', 'Quinn org id');
|
|
146
|
+
const configCmd = program.command('config').description('manage local CLI config');
|
|
147
|
+
configCmd
|
|
148
|
+
.command('path')
|
|
149
|
+
.description('print config file path')
|
|
150
|
+
.action(function () {
|
|
151
|
+
const { configPath } = getContext(this);
|
|
152
|
+
print({ configPath });
|
|
153
|
+
});
|
|
154
|
+
configCmd
|
|
155
|
+
.command('get')
|
|
156
|
+
.description('show config and resolved runtime values')
|
|
157
|
+
.action(function () {
|
|
158
|
+
const { configPath, fileConfig, runtimeConfig } = getContext(this);
|
|
159
|
+
print({
|
|
160
|
+
configPath,
|
|
161
|
+
config: {
|
|
162
|
+
apiUrl: fileConfig.apiUrl ?? null,
|
|
163
|
+
token: maskToken(fileConfig.token),
|
|
164
|
+
orgId: fileConfig.orgId ?? null,
|
|
165
|
+
},
|
|
166
|
+
runtime: {
|
|
167
|
+
apiUrl: runtimeConfig.apiUrl ?? null,
|
|
168
|
+
token: maskToken(runtimeConfig.token),
|
|
169
|
+
orgId: runtimeConfig.orgId ?? null,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
configCmd
|
|
174
|
+
.command('set')
|
|
175
|
+
.description('write apiUrl/token/orgId into config file')
|
|
176
|
+
.option('--api-url <url>')
|
|
177
|
+
.option('--api-token <token>')
|
|
178
|
+
.option('--token <token>')
|
|
179
|
+
.option('--org-id <id>')
|
|
180
|
+
.action(function (opts) {
|
|
181
|
+
const { configPath, fileConfig } = getContext(this);
|
|
182
|
+
const next = {
|
|
183
|
+
apiUrl: opts.apiUrl || fileConfig.apiUrl,
|
|
184
|
+
token: opts.apiToken || opts.token || fileConfig.token,
|
|
185
|
+
orgId: opts.orgId || fileConfig.orgId,
|
|
186
|
+
};
|
|
187
|
+
writeConfig(configPath, next);
|
|
188
|
+
print({
|
|
189
|
+
ok: true,
|
|
190
|
+
configPath,
|
|
191
|
+
config: {
|
|
192
|
+
apiUrl: next.apiUrl ?? null,
|
|
193
|
+
token: maskToken(next.token),
|
|
194
|
+
orgId: next.orgId ?? null,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
configCmd.addHelpText('after', [
|
|
199
|
+
'',
|
|
200
|
+
'Examples:',
|
|
201
|
+
' quinn config path',
|
|
202
|
+
' quinn config get',
|
|
203
|
+
' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
|
|
204
|
+
].join('\n'));
|
|
205
|
+
const orgCmd = program
|
|
206
|
+
.command('organizations')
|
|
207
|
+
.description('organization metadata and high-level stats');
|
|
208
|
+
orgCmd
|
|
209
|
+
.command('current')
|
|
210
|
+
.description('get current organization basic info (id, name)')
|
|
211
|
+
.action(function () {
|
|
212
|
+
return withHandler(async () => {
|
|
213
|
+
const { runtimeConfig } = getContext(this);
|
|
214
|
+
const quinn = createClient(runtimeConfig);
|
|
215
|
+
print(await quinn.organizations.get());
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
orgCmd
|
|
219
|
+
.command('details')
|
|
220
|
+
.description('get organization details with aggregate stats')
|
|
221
|
+
.action(function () {
|
|
222
|
+
return withHandler(async () => {
|
|
223
|
+
const { runtimeConfig } = getContext(this);
|
|
224
|
+
const quinn = createClient(runtimeConfig);
|
|
225
|
+
print(await quinn.organizations.getDetails());
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
orgCmd.addHelpText('after', [
|
|
229
|
+
'',
|
|
230
|
+
'Examples:',
|
|
231
|
+
' quinn organizations current',
|
|
232
|
+
' quinn organizations details',
|
|
233
|
+
].join('\n'));
|
|
234
|
+
const membersCmd = program.command('members').description('member operations');
|
|
235
|
+
membersCmd
|
|
236
|
+
.command('list')
|
|
237
|
+
.description('list members with structured filters (no keyword search)')
|
|
238
|
+
.option('--privilege <value>', 'filter by privilege: owner|admin|member, supports comma-separated')
|
|
239
|
+
.option('--manager-uid <uid>', 'filter by manager UID')
|
|
240
|
+
.option('--limit <n>', 'page size')
|
|
241
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
242
|
+
.action(function (opts) {
|
|
243
|
+
return withHandler(async () => {
|
|
244
|
+
const { runtimeConfig } = getContext(this);
|
|
245
|
+
const quinn = createClient(runtimeConfig);
|
|
246
|
+
print(await quinn.members.list({
|
|
247
|
+
privilege: asPrivilege(opts.privilege),
|
|
248
|
+
managerUid: opts.managerUid,
|
|
249
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
250
|
+
token: opts.pageToken,
|
|
251
|
+
}));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
membersCmd
|
|
255
|
+
.command('find')
|
|
256
|
+
.description('find members by keyword (name/email fuzzy search)')
|
|
257
|
+
.argument('<query>')
|
|
258
|
+
.option('--limit <n>', 'page size')
|
|
259
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
260
|
+
.action(function (query, opts) {
|
|
261
|
+
return withHandler(async () => {
|
|
262
|
+
const { runtimeConfig } = getContext(this);
|
|
263
|
+
const quinn = createClient(runtimeConfig);
|
|
264
|
+
print(await quinn.members.list({
|
|
265
|
+
search: query,
|
|
266
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
267
|
+
token: opts.pageToken,
|
|
268
|
+
}));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
membersCmd
|
|
272
|
+
.command('list-managers')
|
|
273
|
+
.description('list unique manager members in the organization')
|
|
274
|
+
.option('--search <text>', 'optional keyword filter on manager name/email')
|
|
275
|
+
.option('--limit <n>', 'page size')
|
|
276
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
277
|
+
.action(function (opts) {
|
|
278
|
+
return withHandler(async () => {
|
|
279
|
+
const { runtimeConfig } = getContext(this);
|
|
280
|
+
const quinn = createClient(runtimeConfig);
|
|
281
|
+
print(await quinn.members.listManagers({
|
|
282
|
+
search: opts.search,
|
|
283
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
284
|
+
token: opts.pageToken,
|
|
285
|
+
}));
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
membersCmd
|
|
289
|
+
.command('get')
|
|
290
|
+
.description('get one member by ID/email, or many members by comma-separated values')
|
|
291
|
+
.argument('<memberIdOrEmail>')
|
|
292
|
+
.action(function (memberIdOrEmail) {
|
|
293
|
+
return withHandler(async () => {
|
|
294
|
+
const { runtimeConfig } = getContext(this);
|
|
295
|
+
const quinn = createClient(runtimeConfig);
|
|
296
|
+
const values = asCsv(memberIdOrEmail);
|
|
297
|
+
if (values.length <= 1) {
|
|
298
|
+
print(await quinn.members.get(memberIdOrEmail));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const ids = [];
|
|
302
|
+
const emails = [];
|
|
303
|
+
for (const value of values) {
|
|
304
|
+
if (value.includes('@')) {
|
|
305
|
+
emails.push(value);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
ids.push(value);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
print(await quinn.members.batchGet({
|
|
312
|
+
ids: ids.length > 0 ? ids : undefined,
|
|
313
|
+
emails: emails.length > 0 ? emails : undefined,
|
|
314
|
+
}));
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
membersCmd.addHelpText('after', [
|
|
318
|
+
'',
|
|
319
|
+
'Examples:',
|
|
320
|
+
' quinn members list --privilege owner,admin',
|
|
321
|
+
' quinn members list --manager-uid <uid>',
|
|
322
|
+
' quinn members find alice',
|
|
323
|
+
' quinn members list-managers --search bob',
|
|
324
|
+
' quinn members get <memberId>',
|
|
325
|
+
' quinn members get <memberId1,memberId2,user@example.com>',
|
|
326
|
+
].join('\n'));
|
|
327
|
+
const rolesCmd = program.command('roles').description('role operations');
|
|
328
|
+
rolesCmd.command('list').description('list all roles in the organization').action(function () {
|
|
329
|
+
return withHandler(async () => {
|
|
330
|
+
const { runtimeConfig } = getContext(this);
|
|
331
|
+
const quinn = createClient(runtimeConfig);
|
|
332
|
+
print(await quinn.roles.list());
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
rolesCmd
|
|
336
|
+
.command('get')
|
|
337
|
+
.description('get one role by ID, or many roles by comma-separated values')
|
|
338
|
+
.argument('<roleId>')
|
|
339
|
+
.action(function (roleId) {
|
|
340
|
+
return withHandler(async () => {
|
|
341
|
+
const { runtimeConfig } = getContext(this);
|
|
342
|
+
const quinn = createClient(runtimeConfig);
|
|
343
|
+
const values = asCsv(roleId);
|
|
344
|
+
if (values.length <= 1) {
|
|
345
|
+
print(await quinn.roles.get(roleId));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
print(await quinn.roles.batchGet(values));
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
rolesCmd.addHelpText('after', [
|
|
352
|
+
'',
|
|
353
|
+
'Examples:',
|
|
354
|
+
' quinn roles list',
|
|
355
|
+
' quinn roles get <roleId>',
|
|
356
|
+
' quinn roles get <roleId1,roleId2>',
|
|
357
|
+
].join('\n'));
|
|
358
|
+
const levelsCmd = program.command('levels').description('level operations');
|
|
359
|
+
levelsCmd
|
|
360
|
+
.command('list')
|
|
361
|
+
.description('list levels under a role')
|
|
362
|
+
.requiredOption('--role-id <id>', 'role ID to scope the query')
|
|
363
|
+
.option('--limit <n>', 'page size')
|
|
364
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
365
|
+
.action(function (opts) {
|
|
366
|
+
return withHandler(async () => {
|
|
367
|
+
const { runtimeConfig } = getContext(this);
|
|
368
|
+
const quinn = createClient(runtimeConfig);
|
|
369
|
+
print(await quinn.levels.list({
|
|
370
|
+
roleId: opts.roleId,
|
|
371
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
372
|
+
token: opts.pageToken,
|
|
373
|
+
}));
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
levelsCmd
|
|
377
|
+
.command('get')
|
|
378
|
+
.description('get one level by ID, or many levels by comma-separated values')
|
|
379
|
+
.argument('<levelId>')
|
|
380
|
+
.action(function (levelId) {
|
|
381
|
+
return withHandler(async () => {
|
|
382
|
+
const { runtimeConfig } = getContext(this);
|
|
383
|
+
const quinn = createClient(runtimeConfig);
|
|
384
|
+
const values = asCsv(levelId);
|
|
385
|
+
if (values.length <= 1) {
|
|
386
|
+
print(await quinn.levels.get(levelId));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
print(await quinn.levels.batchGet(values));
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
levelsCmd.addHelpText('after', [
|
|
393
|
+
'',
|
|
394
|
+
'Examples:',
|
|
395
|
+
' quinn levels list --role-id <roleId>',
|
|
396
|
+
' quinn levels get <levelId>',
|
|
397
|
+
' quinn levels get <levelId1,levelId2>',
|
|
398
|
+
].join('\n'));
|
|
399
|
+
const compsCmd = program.command('competencies').description('competency operations');
|
|
400
|
+
compsCmd
|
|
401
|
+
.command('list')
|
|
402
|
+
.description('list competencies scoped by role + level')
|
|
403
|
+
.requiredOption('--role-id <id>', 'role ID')
|
|
404
|
+
.requiredOption('--level-id <id>', 'level ID')
|
|
405
|
+
.option('--search <text>', 'optional keyword filter')
|
|
406
|
+
.option('--limit <n>', 'page size')
|
|
407
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
408
|
+
.action(function (opts) {
|
|
409
|
+
return withHandler(async () => {
|
|
410
|
+
const { runtimeConfig } = getContext(this);
|
|
411
|
+
const quinn = createClient(runtimeConfig);
|
|
412
|
+
print(await quinn.competencies.list({
|
|
413
|
+
roleId: opts.roleId,
|
|
414
|
+
levelId: opts.levelId,
|
|
415
|
+
search: opts.search,
|
|
416
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
417
|
+
token: opts.pageToken,
|
|
418
|
+
}));
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
compsCmd
|
|
422
|
+
.command('get')
|
|
423
|
+
.description('get one competency by ID, or many competencies by comma-separated values')
|
|
424
|
+
.argument('<competencyId>')
|
|
425
|
+
.action(function (competencyId) {
|
|
426
|
+
return withHandler(async () => {
|
|
427
|
+
const { runtimeConfig } = getContext(this);
|
|
428
|
+
const quinn = createClient(runtimeConfig);
|
|
429
|
+
const values = asCsv(competencyId);
|
|
430
|
+
if (values.length <= 1) {
|
|
431
|
+
print(await quinn.competencies.get(competencyId));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
print(await quinn.competencies.batchGet(values));
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
compsCmd
|
|
438
|
+
.command('courses')
|
|
439
|
+
.description('list courses under a competency')
|
|
440
|
+
.argument('<competencyId>')
|
|
441
|
+
.action(function (competencyId) {
|
|
442
|
+
return withHandler(async () => {
|
|
443
|
+
const { runtimeConfig } = getContext(this);
|
|
444
|
+
const quinn = createClient(runtimeConfig);
|
|
445
|
+
print(await quinn.competencies.listCourses(competencyId));
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
compsCmd.addHelpText('after', [
|
|
449
|
+
'',
|
|
450
|
+
'Examples:',
|
|
451
|
+
' quinn competencies list --role-id <roleId> --level-id <levelId>',
|
|
452
|
+
' quinn competencies get <competencyId>',
|
|
453
|
+
' quinn competencies get <id1,id2>',
|
|
454
|
+
' quinn competencies courses <competencyId>',
|
|
455
|
+
].join('\n'));
|
|
456
|
+
const endorseCmd = program.command('endorsements').description('endorsement operations');
|
|
457
|
+
endorseCmd
|
|
458
|
+
.command('get')
|
|
459
|
+
.description('get one endorsement by ID')
|
|
460
|
+
.argument('<endorsementId>')
|
|
461
|
+
.action(function (endorsementId) {
|
|
462
|
+
return withHandler(async () => {
|
|
463
|
+
const { runtimeConfig } = getContext(this);
|
|
464
|
+
const quinn = createClient(runtimeConfig);
|
|
465
|
+
print(await quinn.endorsements.get(endorsementId));
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
endorseCmd
|
|
469
|
+
.command('find')
|
|
470
|
+
.description('find one endorsement by user + competency')
|
|
471
|
+
.requiredOption('--uid <uid>', 'user ID')
|
|
472
|
+
.requiredOption('--competency-id <id>', 'competency ID')
|
|
473
|
+
.action(function (opts) {
|
|
474
|
+
return withHandler(async () => {
|
|
475
|
+
const { runtimeConfig } = getContext(this);
|
|
476
|
+
const quinn = createClient(runtimeConfig);
|
|
477
|
+
print(await quinn.endorsements.find(opts.uid, opts.competencyId));
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
endorseCmd
|
|
481
|
+
.command('list')
|
|
482
|
+
.description('list endorsements by user IDs and competency IDs')
|
|
483
|
+
.requiredOption('--uids <uids>', 'comma-separated user IDs')
|
|
484
|
+
.requiredOption('--competency-ids <ids>', 'comma-separated competency IDs')
|
|
485
|
+
.action(function (opts) {
|
|
486
|
+
return withHandler(async () => {
|
|
487
|
+
const { runtimeConfig } = getContext(this);
|
|
488
|
+
const quinn = createClient(runtimeConfig);
|
|
489
|
+
print(await quinn.endorsements.list({
|
|
490
|
+
uids: asCsv(opts.uids),
|
|
491
|
+
competencyIds: asCsv(opts.competencyIds),
|
|
492
|
+
}));
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
endorseCmd.addHelpText('after', [
|
|
496
|
+
'',
|
|
497
|
+
'Examples:',
|
|
498
|
+
' quinn endorsements get <endorsementId>',
|
|
499
|
+
' quinn endorsements find --uid <uid> --competency-id <competencyId>',
|
|
500
|
+
' quinn endorsements list --uids <u1,u2> --competency-ids <c1,c2>',
|
|
501
|
+
].join('\n'));
|
|
502
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
503
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
504
|
+
process.stderr.write(`${message}\n`);
|
|
505
|
+
process.exitCode = 1;
|
|
506
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@totoday/quinn-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"quinn": "dist/index.js"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"commander": "^13.1.0",
|
|
9
|
+
"@totoday/quinn-sdk": "0.1.0"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/node": "^22.13.10",
|
|
13
|
+
"typescript": "^5.8.2"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"quinn": "node dist/index.js",
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"clean": "rm -rf dist"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { Quinn, Privilege } from '@totoday/quinn-sdk';
|
|
8
|
+
|
|
9
|
+
type QuinnCliConfig = {
|
|
10
|
+
apiUrl?: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
orgId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type GlobalOptions = {
|
|
16
|
+
configPath?: string;
|
|
17
|
+
apiUrl?: string;
|
|
18
|
+
apiToken?: string;
|
|
19
|
+
token?: string;
|
|
20
|
+
orgId?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function print(data: unknown): void {
|
|
24
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function asCsv(raw?: string): string[] {
|
|
28
|
+
if (!raw) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
return raw
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((item) => item.trim())
|
|
34
|
+
.filter((item) => item.length > 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function asPrivilege(raw?: string): Privilege | Privilege[] | undefined {
|
|
38
|
+
const values = asCsv(raw);
|
|
39
|
+
if (values.length === 0) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (values.length === 1) {
|
|
43
|
+
return values[0] as Privilege;
|
|
44
|
+
}
|
|
45
|
+
return values as Privilege[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function maskToken(token?: string): string | null {
|
|
49
|
+
if (!token) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (token.length <= 12) {
|
|
53
|
+
return '***';
|
|
54
|
+
}
|
|
55
|
+
return `${token.slice(0, 6)}...${token.slice(-4)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getConfigPath(opts: GlobalOptions): string {
|
|
59
|
+
return (
|
|
60
|
+
opts.configPath ||
|
|
61
|
+
process.env.QUINN_CONFIG_PATH ||
|
|
62
|
+
path.join(os.homedir(), '.config', 'quinn', 'config.json')
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readConfig(configPath: string): QuinnCliConfig {
|
|
67
|
+
if (!fs.existsSync(configPath)) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
72
|
+
const parsed = JSON.parse(raw) as QuinnCliConfig;
|
|
73
|
+
return parsed ?? {};
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeConfig(configPath: string, config: QuinnCliConfig): void {
|
|
80
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
81
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveRuntimeConfig(opts: GlobalOptions, fileConfig: QuinnCliConfig): QuinnCliConfig {
|
|
85
|
+
return {
|
|
86
|
+
apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl,
|
|
87
|
+
token: opts.apiToken || opts.token || process.env.QUINN_API_TOKEN || fileConfig.token,
|
|
88
|
+
orgId: opts.orgId || process.env.QUINN_ORG_ID || fileConfig.orgId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createClient(config: QuinnCliConfig): Quinn {
|
|
93
|
+
const { apiUrl, token, orgId } = config;
|
|
94
|
+
if (!apiUrl) {
|
|
95
|
+
throw new Error('missing apiUrl: set --api-url or QUINN_API_URL or config');
|
|
96
|
+
}
|
|
97
|
+
if (!token) {
|
|
98
|
+
throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
|
|
99
|
+
}
|
|
100
|
+
if (!orgId) {
|
|
101
|
+
throw new Error('missing orgId: set --org-id or QUINN_ORG_ID or config');
|
|
102
|
+
}
|
|
103
|
+
return new Quinn({ apiUrl, token, orgId });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getContext(command: Command): {
|
|
107
|
+
global: GlobalOptions;
|
|
108
|
+
configPath: string;
|
|
109
|
+
fileConfig: QuinnCliConfig;
|
|
110
|
+
runtimeConfig: QuinnCliConfig;
|
|
111
|
+
} {
|
|
112
|
+
const global = command.optsWithGlobals<GlobalOptions>();
|
|
113
|
+
const configPath = getConfigPath(global);
|
|
114
|
+
const fileConfig = readConfig(configPath);
|
|
115
|
+
const runtimeConfig = resolveRuntimeConfig(global, fileConfig);
|
|
116
|
+
return { global, configPath, fileConfig, runtimeConfig };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function withHandler(fn: () => Promise<void>): Promise<void> {
|
|
120
|
+
try {
|
|
121
|
+
await fn();
|
|
122
|
+
} catch (error: unknown) {
|
|
123
|
+
let message = '';
|
|
124
|
+
if (
|
|
125
|
+
typeof error === 'object' &&
|
|
126
|
+
error !== null &&
|
|
127
|
+
'isAxiosError' in error &&
|
|
128
|
+
(error as { isAxiosError?: boolean }).isAxiosError
|
|
129
|
+
) {
|
|
130
|
+
const axiosError = error as {
|
|
131
|
+
message?: string;
|
|
132
|
+
response?: { status?: number; data?: unknown };
|
|
133
|
+
};
|
|
134
|
+
const status = axiosError.response?.status;
|
|
135
|
+
const responseData = axiosError.response?.data;
|
|
136
|
+
const details =
|
|
137
|
+
typeof responseData === 'string'
|
|
138
|
+
? responseData
|
|
139
|
+
: responseData
|
|
140
|
+
? JSON.stringify(responseData)
|
|
141
|
+
: '';
|
|
142
|
+
message = [
|
|
143
|
+
axiosError.message || 'HTTP request failed',
|
|
144
|
+
status ? `(status: ${status})` : '',
|
|
145
|
+
details ? `response: ${details}` : '',
|
|
146
|
+
]
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.join(' ');
|
|
149
|
+
} else if (error instanceof Error) {
|
|
150
|
+
message = error.message;
|
|
151
|
+
} else {
|
|
152
|
+
message = String(error);
|
|
153
|
+
}
|
|
154
|
+
process.stderr.write(`${message || 'Unknown error'}\n`);
|
|
155
|
+
process.exitCode = 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const program = new Command();
|
|
160
|
+
program
|
|
161
|
+
.name('quinn')
|
|
162
|
+
.description('Quinn CLI')
|
|
163
|
+
.addHelpText(
|
|
164
|
+
'after',
|
|
165
|
+
[
|
|
166
|
+
'',
|
|
167
|
+
'Examples:',
|
|
168
|
+
' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
|
|
169
|
+
' quinn organizations details',
|
|
170
|
+
' quinn members find alice',
|
|
171
|
+
' quinn members get user-1,user-2,user@example.com',
|
|
172
|
+
].join('\n')
|
|
173
|
+
)
|
|
174
|
+
.showHelpAfterError()
|
|
175
|
+
.option('--config-path <path>', 'config file path (default ~/.config/quinn/config.json)')
|
|
176
|
+
.option('--api-url <url>', 'Quinn API base url')
|
|
177
|
+
.option('--api-token <token>', 'Quinn API token')
|
|
178
|
+
.option('--token <token>', 'alias of --api-token')
|
|
179
|
+
.option('--org-id <id>', 'Quinn org id');
|
|
180
|
+
|
|
181
|
+
const configCmd = program.command('config').description('manage local CLI config');
|
|
182
|
+
configCmd
|
|
183
|
+
.command('path')
|
|
184
|
+
.description('print config file path')
|
|
185
|
+
.action(function () {
|
|
186
|
+
const { configPath } = getContext(this);
|
|
187
|
+
print({ configPath });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
configCmd
|
|
191
|
+
.command('get')
|
|
192
|
+
.description('show config and resolved runtime values')
|
|
193
|
+
.action(function () {
|
|
194
|
+
const { configPath, fileConfig, runtimeConfig } = getContext(this);
|
|
195
|
+
print({
|
|
196
|
+
configPath,
|
|
197
|
+
config: {
|
|
198
|
+
apiUrl: fileConfig.apiUrl ?? null,
|
|
199
|
+
token: maskToken(fileConfig.token),
|
|
200
|
+
orgId: fileConfig.orgId ?? null,
|
|
201
|
+
},
|
|
202
|
+
runtime: {
|
|
203
|
+
apiUrl: runtimeConfig.apiUrl ?? null,
|
|
204
|
+
token: maskToken(runtimeConfig.token),
|
|
205
|
+
orgId: runtimeConfig.orgId ?? null,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
configCmd
|
|
211
|
+
.command('set')
|
|
212
|
+
.description('write apiUrl/token/orgId into config file')
|
|
213
|
+
.option('--api-url <url>')
|
|
214
|
+
.option('--api-token <token>')
|
|
215
|
+
.option('--token <token>')
|
|
216
|
+
.option('--org-id <id>')
|
|
217
|
+
.action(function (opts: GlobalOptions) {
|
|
218
|
+
const { configPath, fileConfig } = getContext(this);
|
|
219
|
+
const next: QuinnCliConfig = {
|
|
220
|
+
apiUrl: opts.apiUrl || fileConfig.apiUrl,
|
|
221
|
+
token: opts.apiToken || opts.token || fileConfig.token,
|
|
222
|
+
orgId: opts.orgId || fileConfig.orgId,
|
|
223
|
+
};
|
|
224
|
+
writeConfig(configPath, next);
|
|
225
|
+
print({
|
|
226
|
+
ok: true,
|
|
227
|
+
configPath,
|
|
228
|
+
config: {
|
|
229
|
+
apiUrl: next.apiUrl ?? null,
|
|
230
|
+
token: maskToken(next.token),
|
|
231
|
+
orgId: next.orgId ?? null,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
configCmd.addHelpText(
|
|
236
|
+
'after',
|
|
237
|
+
[
|
|
238
|
+
'',
|
|
239
|
+
'Examples:',
|
|
240
|
+
' quinn config path',
|
|
241
|
+
' quinn config get',
|
|
242
|
+
' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
|
|
243
|
+
].join('\n')
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const orgCmd = program
|
|
247
|
+
.command('organizations')
|
|
248
|
+
.description('organization metadata and high-level stats');
|
|
249
|
+
orgCmd
|
|
250
|
+
.command('current')
|
|
251
|
+
.description('get current organization basic info (id, name)')
|
|
252
|
+
.action(function () {
|
|
253
|
+
return withHandler(async () => {
|
|
254
|
+
const { runtimeConfig } = getContext(this);
|
|
255
|
+
const quinn = createClient(runtimeConfig);
|
|
256
|
+
print(await quinn.organizations.get());
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
orgCmd
|
|
261
|
+
.command('details')
|
|
262
|
+
.description('get organization details with aggregate stats')
|
|
263
|
+
.action(function () {
|
|
264
|
+
return withHandler(async () => {
|
|
265
|
+
const { runtimeConfig } = getContext(this);
|
|
266
|
+
const quinn = createClient(runtimeConfig);
|
|
267
|
+
print(await quinn.organizations.getDetails());
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
orgCmd.addHelpText(
|
|
271
|
+
'after',
|
|
272
|
+
[
|
|
273
|
+
'',
|
|
274
|
+
'Examples:',
|
|
275
|
+
' quinn organizations current',
|
|
276
|
+
' quinn organizations details',
|
|
277
|
+
].join('\n')
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const membersCmd = program.command('members').description('member operations');
|
|
281
|
+
membersCmd
|
|
282
|
+
.command('list')
|
|
283
|
+
.description('list members with structured filters (no keyword search)')
|
|
284
|
+
.option('--privilege <value>', 'filter by privilege: owner|admin|member, supports comma-separated')
|
|
285
|
+
.option('--manager-uid <uid>', 'filter by manager UID')
|
|
286
|
+
.option('--limit <n>', 'page size')
|
|
287
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
288
|
+
.action(function (opts: { privilege?: string; managerUid?: string; limit?: string; pageToken?: string }) {
|
|
289
|
+
return withHandler(async () => {
|
|
290
|
+
const { runtimeConfig } = getContext(this);
|
|
291
|
+
const quinn = createClient(runtimeConfig);
|
|
292
|
+
print(
|
|
293
|
+
await quinn.members.list({
|
|
294
|
+
privilege: asPrivilege(opts.privilege),
|
|
295
|
+
managerUid: opts.managerUid,
|
|
296
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
297
|
+
token: opts.pageToken,
|
|
298
|
+
})
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
membersCmd
|
|
304
|
+
.command('find')
|
|
305
|
+
.description('find members by keyword (name/email fuzzy search)')
|
|
306
|
+
.argument('<query>')
|
|
307
|
+
.option('--limit <n>', 'page size')
|
|
308
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
309
|
+
.action(function (query: string, opts: { limit?: string; pageToken?: string }) {
|
|
310
|
+
return withHandler(async () => {
|
|
311
|
+
const { runtimeConfig } = getContext(this);
|
|
312
|
+
const quinn = createClient(runtimeConfig);
|
|
313
|
+
print(
|
|
314
|
+
await quinn.members.list({
|
|
315
|
+
search: query,
|
|
316
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
317
|
+
token: opts.pageToken,
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
membersCmd
|
|
324
|
+
.command('list-managers')
|
|
325
|
+
.description('list unique manager members in the organization')
|
|
326
|
+
.option('--search <text>', 'optional keyword filter on manager name/email')
|
|
327
|
+
.option('--limit <n>', 'page size')
|
|
328
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
329
|
+
.action(function (opts: { search?: string; limit?: string; pageToken?: string }) {
|
|
330
|
+
return withHandler(async () => {
|
|
331
|
+
const { runtimeConfig } = getContext(this);
|
|
332
|
+
const quinn = createClient(runtimeConfig);
|
|
333
|
+
print(
|
|
334
|
+
await quinn.members.listManagers({
|
|
335
|
+
search: opts.search,
|
|
336
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
337
|
+
token: opts.pageToken,
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
membersCmd
|
|
344
|
+
.command('get')
|
|
345
|
+
.description('get one member by ID/email, or many members by comma-separated values')
|
|
346
|
+
.argument('<memberIdOrEmail>')
|
|
347
|
+
.action(function (memberIdOrEmail: string) {
|
|
348
|
+
return withHandler(async () => {
|
|
349
|
+
const { runtimeConfig } = getContext(this);
|
|
350
|
+
const quinn = createClient(runtimeConfig);
|
|
351
|
+
const values = asCsv(memberIdOrEmail);
|
|
352
|
+
if (values.length <= 1) {
|
|
353
|
+
print(await quinn.members.get(memberIdOrEmail));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const ids: string[] = [];
|
|
358
|
+
const emails: string[] = [];
|
|
359
|
+
for (const value of values) {
|
|
360
|
+
if (value.includes('@')) {
|
|
361
|
+
emails.push(value);
|
|
362
|
+
} else {
|
|
363
|
+
ids.push(value);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
print(
|
|
367
|
+
await quinn.members.batchGet({
|
|
368
|
+
ids: ids.length > 0 ? ids : undefined,
|
|
369
|
+
emails: emails.length > 0 ? emails : undefined,
|
|
370
|
+
})
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
membersCmd.addHelpText(
|
|
375
|
+
'after',
|
|
376
|
+
[
|
|
377
|
+
'',
|
|
378
|
+
'Examples:',
|
|
379
|
+
' quinn members list --privilege owner,admin',
|
|
380
|
+
' quinn members list --manager-uid <uid>',
|
|
381
|
+
' quinn members find alice',
|
|
382
|
+
' quinn members list-managers --search bob',
|
|
383
|
+
' quinn members get <memberId>',
|
|
384
|
+
' quinn members get <memberId1,memberId2,user@example.com>',
|
|
385
|
+
].join('\n')
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const rolesCmd = program.command('roles').description('role operations');
|
|
389
|
+
rolesCmd.command('list').description('list all roles in the organization').action(function () {
|
|
390
|
+
return withHandler(async () => {
|
|
391
|
+
const { runtimeConfig } = getContext(this);
|
|
392
|
+
const quinn = createClient(runtimeConfig);
|
|
393
|
+
print(await quinn.roles.list());
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
rolesCmd
|
|
398
|
+
.command('get')
|
|
399
|
+
.description('get one role by ID, or many roles by comma-separated values')
|
|
400
|
+
.argument('<roleId>')
|
|
401
|
+
.action(function (roleId: string) {
|
|
402
|
+
return withHandler(async () => {
|
|
403
|
+
const { runtimeConfig } = getContext(this);
|
|
404
|
+
const quinn = createClient(runtimeConfig);
|
|
405
|
+
const values = asCsv(roleId);
|
|
406
|
+
if (values.length <= 1) {
|
|
407
|
+
print(await quinn.roles.get(roleId));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
print(await quinn.roles.batchGet(values));
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
rolesCmd.addHelpText(
|
|
414
|
+
'after',
|
|
415
|
+
[
|
|
416
|
+
'',
|
|
417
|
+
'Examples:',
|
|
418
|
+
' quinn roles list',
|
|
419
|
+
' quinn roles get <roleId>',
|
|
420
|
+
' quinn roles get <roleId1,roleId2>',
|
|
421
|
+
].join('\n')
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const levelsCmd = program.command('levels').description('level operations');
|
|
425
|
+
levelsCmd
|
|
426
|
+
.command('list')
|
|
427
|
+
.description('list levels under a role')
|
|
428
|
+
.requiredOption('--role-id <id>', 'role ID to scope the query')
|
|
429
|
+
.option('--limit <n>', 'page size')
|
|
430
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
431
|
+
.action(function (opts: { roleId: string; limit?: string; pageToken?: string }) {
|
|
432
|
+
return withHandler(async () => {
|
|
433
|
+
const { runtimeConfig } = getContext(this);
|
|
434
|
+
const quinn = createClient(runtimeConfig);
|
|
435
|
+
print(
|
|
436
|
+
await quinn.levels.list({
|
|
437
|
+
roleId: opts.roleId,
|
|
438
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
439
|
+
token: opts.pageToken,
|
|
440
|
+
})
|
|
441
|
+
);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
levelsCmd
|
|
446
|
+
.command('get')
|
|
447
|
+
.description('get one level by ID, or many levels by comma-separated values')
|
|
448
|
+
.argument('<levelId>')
|
|
449
|
+
.action(function (levelId: string) {
|
|
450
|
+
return withHandler(async () => {
|
|
451
|
+
const { runtimeConfig } = getContext(this);
|
|
452
|
+
const quinn = createClient(runtimeConfig);
|
|
453
|
+
const values = asCsv(levelId);
|
|
454
|
+
if (values.length <= 1) {
|
|
455
|
+
print(await quinn.levels.get(levelId));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
print(await quinn.levels.batchGet(values));
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
levelsCmd.addHelpText(
|
|
462
|
+
'after',
|
|
463
|
+
[
|
|
464
|
+
'',
|
|
465
|
+
'Examples:',
|
|
466
|
+
' quinn levels list --role-id <roleId>',
|
|
467
|
+
' quinn levels get <levelId>',
|
|
468
|
+
' quinn levels get <levelId1,levelId2>',
|
|
469
|
+
].join('\n')
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const compsCmd = program.command('competencies').description('competency operations');
|
|
473
|
+
compsCmd
|
|
474
|
+
.command('list')
|
|
475
|
+
.description('list competencies scoped by role + level')
|
|
476
|
+
.requiredOption('--role-id <id>', 'role ID')
|
|
477
|
+
.requiredOption('--level-id <id>', 'level ID')
|
|
478
|
+
.option('--search <text>', 'optional keyword filter')
|
|
479
|
+
.option('--limit <n>', 'page size')
|
|
480
|
+
.option('--page-token <token>', 'pagination token from previous page')
|
|
481
|
+
.action(function (opts: { roleId: string; levelId: string; search?: string; limit?: string; pageToken?: string }) {
|
|
482
|
+
return withHandler(async () => {
|
|
483
|
+
const { runtimeConfig } = getContext(this);
|
|
484
|
+
const quinn = createClient(runtimeConfig);
|
|
485
|
+
print(
|
|
486
|
+
await quinn.competencies.list({
|
|
487
|
+
roleId: opts.roleId,
|
|
488
|
+
levelId: opts.levelId,
|
|
489
|
+
search: opts.search,
|
|
490
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
491
|
+
token: opts.pageToken,
|
|
492
|
+
})
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
compsCmd
|
|
498
|
+
.command('get')
|
|
499
|
+
.description('get one competency by ID, or many competencies by comma-separated values')
|
|
500
|
+
.argument('<competencyId>')
|
|
501
|
+
.action(function (competencyId: string) {
|
|
502
|
+
return withHandler(async () => {
|
|
503
|
+
const { runtimeConfig } = getContext(this);
|
|
504
|
+
const quinn = createClient(runtimeConfig);
|
|
505
|
+
const values = asCsv(competencyId);
|
|
506
|
+
if (values.length <= 1) {
|
|
507
|
+
print(await quinn.competencies.get(competencyId));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
print(await quinn.competencies.batchGet(values));
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
compsCmd
|
|
515
|
+
.command('courses')
|
|
516
|
+
.description('list courses under a competency')
|
|
517
|
+
.argument('<competencyId>')
|
|
518
|
+
.action(function (competencyId: string) {
|
|
519
|
+
return withHandler(async () => {
|
|
520
|
+
const { runtimeConfig } = getContext(this);
|
|
521
|
+
const quinn = createClient(runtimeConfig);
|
|
522
|
+
print(await quinn.competencies.listCourses(competencyId));
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
compsCmd.addHelpText(
|
|
526
|
+
'after',
|
|
527
|
+
[
|
|
528
|
+
'',
|
|
529
|
+
'Examples:',
|
|
530
|
+
' quinn competencies list --role-id <roleId> --level-id <levelId>',
|
|
531
|
+
' quinn competencies get <competencyId>',
|
|
532
|
+
' quinn competencies get <id1,id2>',
|
|
533
|
+
' quinn competencies courses <competencyId>',
|
|
534
|
+
].join('\n')
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const endorseCmd = program.command('endorsements').description('endorsement operations');
|
|
538
|
+
endorseCmd
|
|
539
|
+
.command('get')
|
|
540
|
+
.description('get one endorsement by ID')
|
|
541
|
+
.argument('<endorsementId>')
|
|
542
|
+
.action(function (endorsementId: string) {
|
|
543
|
+
return withHandler(async () => {
|
|
544
|
+
const { runtimeConfig } = getContext(this);
|
|
545
|
+
const quinn = createClient(runtimeConfig);
|
|
546
|
+
print(await quinn.endorsements.get(endorsementId));
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
endorseCmd
|
|
551
|
+
.command('find')
|
|
552
|
+
.description('find one endorsement by user + competency')
|
|
553
|
+
.requiredOption('--uid <uid>', 'user ID')
|
|
554
|
+
.requiredOption('--competency-id <id>', 'competency ID')
|
|
555
|
+
.action(function (opts: { uid: string; competencyId: string }) {
|
|
556
|
+
return withHandler(async () => {
|
|
557
|
+
const { runtimeConfig } = getContext(this);
|
|
558
|
+
const quinn = createClient(runtimeConfig);
|
|
559
|
+
print(await quinn.endorsements.find(opts.uid, opts.competencyId));
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
endorseCmd
|
|
564
|
+
.command('list')
|
|
565
|
+
.description('list endorsements by user IDs and competency IDs')
|
|
566
|
+
.requiredOption('--uids <uids>', 'comma-separated user IDs')
|
|
567
|
+
.requiredOption('--competency-ids <ids>', 'comma-separated competency IDs')
|
|
568
|
+
.action(function (opts: { uids: string; competencyIds: string }) {
|
|
569
|
+
return withHandler(async () => {
|
|
570
|
+
const { runtimeConfig } = getContext(this);
|
|
571
|
+
const quinn = createClient(runtimeConfig);
|
|
572
|
+
print(
|
|
573
|
+
await quinn.endorsements.list({
|
|
574
|
+
uids: asCsv(opts.uids),
|
|
575
|
+
competencyIds: asCsv(opts.competencyIds),
|
|
576
|
+
})
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
endorseCmd.addHelpText(
|
|
581
|
+
'after',
|
|
582
|
+
[
|
|
583
|
+
'',
|
|
584
|
+
'Examples:',
|
|
585
|
+
' quinn endorsements get <endorsementId>',
|
|
586
|
+
' quinn endorsements find --uid <uid> --competency-id <competencyId>',
|
|
587
|
+
' quinn endorsements list --uids <u1,u2> --competency-ids <c1,c2>',
|
|
588
|
+
].join('\n')
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
program.parseAsync(process.argv).catch((error: unknown) => {
|
|
592
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
593
|
+
process.stderr.write(`${message}\n`);
|
|
594
|
+
process.exitCode = 1;
|
|
595
|
+
});
|