@totoday/quinn-cli 0.1.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @totoday/quinn-cli
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7fc166f: Add Knowledge API client on `Quinn` (`documents`, `folders`, `search`), related types, and re-exports for Knowledge service classes.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [7fc166f]
12
+ - @totoday/quinn-sdk@0.2.0
13
+
14
+ ## 0.1.3
15
+
16
+ ### Patch Changes
17
+
18
+ - 16a1fe9: Move organization path handling into the shared HTTP client to simplify service construction.
19
+ - Updated dependencies [16a1fe9]
20
+ - @totoday/quinn-sdk@0.1.3
21
+
3
22
  ## 0.1.2
4
23
 
5
24
  ### Patch Changes
@@ -8,6 +27,7 @@
8
27
  - remove `organizations details` command from CLI
9
28
  - make `organizations current` return organization details with aggregate stats
10
29
  - align SDK and skill documentation with the simplified flow
30
+ - Updated dependencies [45948dc]
11
31
  - @totoday/quinn-sdk@0.1.2
12
32
 
13
33
  ## 0.1.1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @totoday/quinn-cli
2
2
 
3
- CLI for Quinn organization/member/role/competency queries.
3
+ Minimal CLI for Quinn auth/config/connectivity setup.
4
+
5
+ Business data operations should use `@totoday/quinn-sdk`.
4
6
 
5
7
  ## Install
6
8
 
@@ -15,42 +17,25 @@ One-off without global install:
15
17
  npx @totoday/quinn-cli --help
16
18
  ```
17
19
 
18
- ## Quick Start
19
-
20
- Login (recommended: hidden password prompt):
20
+ ## Commands
21
21
 
22
22
  ```bash
23
+ # login (recommended: hidden password prompt)
23
24
  quinn login --email <email>
24
- ```
25
25
 
26
- Login via stdin (scripts/password managers):
27
-
28
- ```bash
26
+ # login via stdin (scripts/password managers)
29
27
  echo "<password>" | quinn login --email <email> --password-stdin
30
- ```
31
28
 
32
- Check organization:
29
+ # inspect local config
30
+ quinn config path
31
+ quinn config get
33
32
 
34
- ```bash
35
- quinn organizations current
36
- ```
33
+ # update local config
34
+ quinn config set --org-id <orgId>
35
+ quinn config set --api-url <apiUrl> --api-token <token> --org-id <orgId>
37
36
 
38
- ## Common Commands
39
-
40
- ```bash
41
- # members
42
- quinn members find alice
43
- quinn members list --privilege owner,admin
44
- quinn members get <memberId1,memberId2,user@example.com>
45
-
46
- # roles / levels / competencies
47
- quinn roles list
48
- quinn levels list --role-id <roleId>
49
- quinn competencies list --role-id <roleId> --level-id <levelId>
50
-
51
- # endorsements
52
- quinn endorsements find --uid <uid> --competency-id <competencyId>
53
- quinn endorsements list --uids <u1,u2> --competency-ids <c1,c2>
37
+ # connectivity test (calls organizations.current via SDK)
38
+ quinn test
54
39
  ```
55
40
 
56
41
  ## Config
package/dist/index.js CHANGED
@@ -12,25 +12,6 @@ const quinn_sdk_1 = require("@totoday/quinn-sdk");
12
12
  function print(data) {
13
13
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
14
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
15
  function maskToken(token) {
35
16
  if (!token) {
36
17
  return null;
@@ -40,45 +21,6 @@ function maskToken(token) {
40
21
  }
41
22
  return `${token.slice(0, 6)}...${token.slice(-4)}`;
42
23
  }
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 || quinn_sdk_1.DEFAULT_QUINN_API_URL,
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 (!token) {
75
- throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
76
- }
77
- if (!orgId) {
78
- throw new Error('missing orgId: set --org-id or QUINN_ORG_ID or config');
79
- }
80
- return new quinn_sdk_1.Quinn({ apiUrl, token, orgId });
81
- }
82
24
  function readPasswordFromStdin() {
83
25
  const value = node_fs_1.default.readFileSync(0, 'utf8').trim();
84
26
  if (!value) {
@@ -133,12 +75,51 @@ async function promptPasswordHidden(prompt = 'Password: ') {
133
75
  stdin.on('data', onData);
134
76
  });
135
77
  }
78
+ function getConfigPath(opts) {
79
+ return (opts.configPath ||
80
+ process.env.QUINN_CONFIG_PATH ||
81
+ node_path_1.default.join(node_os_1.default.homedir(), '.config', 'quinn', 'config.json'));
82
+ }
83
+ function readConfig(configPath) {
84
+ if (!node_fs_1.default.existsSync(configPath)) {
85
+ return {};
86
+ }
87
+ try {
88
+ const raw = node_fs_1.default.readFileSync(configPath, 'utf8');
89
+ const parsed = JSON.parse(raw);
90
+ return parsed ?? {};
91
+ }
92
+ catch {
93
+ return {};
94
+ }
95
+ }
96
+ function writeConfig(configPath, config) {
97
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(configPath), { recursive: true });
98
+ node_fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
99
+ }
100
+ function resolveRuntimeConfig(opts, fileConfig) {
101
+ return {
102
+ apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl || quinn_sdk_1.DEFAULT_QUINN_API_URL,
103
+ token: opts.apiToken || opts.token || process.env.QUINN_API_TOKEN || fileConfig.token,
104
+ orgId: opts.orgId || process.env.QUINN_ORG_ID || fileConfig.orgId,
105
+ };
106
+ }
107
+ function createClient(config) {
108
+ const { apiUrl, token, orgId } = config;
109
+ if (!token) {
110
+ throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
111
+ }
112
+ if (!orgId) {
113
+ throw new Error('missing orgId: set --org-id or QUINN_ORG_ID or config');
114
+ }
115
+ return new quinn_sdk_1.Quinn({ apiUrl, token, orgId });
116
+ }
136
117
  function getContext(command) {
137
118
  const global = command.optsWithGlobals();
138
119
  const configPath = getConfigPath(global);
139
120
  const fileConfig = readConfig(configPath);
140
121
  const runtimeConfig = resolveRuntimeConfig(global, fileConfig);
141
- return { global, configPath, fileConfig, runtimeConfig };
122
+ return { configPath, fileConfig, runtimeConfig };
142
123
  }
143
124
  async function withHandler(fn) {
144
125
  try {
@@ -179,16 +160,14 @@ async function withHandler(fn) {
179
160
  const program = new commander_1.Command();
180
161
  program
181
162
  .name('quinn')
182
- .description('Quinn CLI')
163
+ .description('Quinn CLI (minimal setup utility, SDK-first runtime)')
183
164
  .addHelpText('after', [
184
165
  '',
185
166
  'Examples:',
186
167
  ' quinn login --email <email>',
187
168
  ' echo "<password>" | quinn login --email <email> --password-stdin',
188
- ' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
189
- ' quinn organizations current',
190
- ' quinn members find alice',
191
- ' quinn members get user-1,user-2,user@example.com',
169
+ ' quinn config get',
170
+ ' quinn test',
192
171
  ].join('\n'))
193
172
  .showHelpAfterError()
194
173
  .option('--config-path <path>', 'config file path (default ~/.config/quinn/config.json)')
@@ -248,13 +227,6 @@ configCmd
248
227
  },
249
228
  });
250
229
  });
251
- configCmd.addHelpText('after', [
252
- '',
253
- 'Examples:',
254
- ' quinn config path',
255
- ' quinn config get',
256
- ' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
257
- ].join('\n'));
258
230
  program
259
231
  .command('login')
260
232
  .description('login and save token (+orgId) into config (password input is hidden by default)')
@@ -279,14 +251,8 @@ program
279
251
  else {
280
252
  password = await promptPasswordHidden();
281
253
  }
282
- const auth = new quinn_sdk_1.QuinnAuth({
283
- apiUrl: runtimeConfig.apiUrl,
284
- configPath,
285
- });
286
- const login = await auth.login({
287
- email: opts.email,
288
- password,
289
- });
254
+ const auth = new quinn_sdk_1.QuinnAuth({ apiUrl: runtimeConfig.apiUrl, configPath });
255
+ const login = await auth.login({ email: opts.email, password });
290
256
  const resolvedOrgId = opts.orgId || login.orgId || fileConfig.orgId;
291
257
  if (!resolvedOrgId) {
292
258
  throw new Error('orgId not found in login response, please pass --org-id');
@@ -308,292 +274,17 @@ program
308
274
  });
309
275
  });
310
276
  });
311
- const orgCmd = program
312
- .command('organizations')
313
- .description('organization metadata and high-level stats');
314
- orgCmd
315
- .command('current')
316
- .description('get current organization details with aggregate stats')
277
+ program
278
+ .command('test')
279
+ .description('test connectivity and auth by calling organizations.current()')
317
280
  .action(function () {
318
281
  return withHandler(async () => {
319
282
  const { runtimeConfig } = getContext(this);
320
283
  const quinn = createClient(runtimeConfig);
321
- print(await quinn.organizations.current());
284
+ const item = await quinn.organizations.current();
285
+ print({ ok: true, item });
322
286
  });
323
287
  });
324
- orgCmd.addHelpText('after', [
325
- '',
326
- 'Examples:',
327
- ' quinn organizations current',
328
- ].join('\n'));
329
- const membersCmd = program.command('members').description('member operations');
330
- membersCmd
331
- .command('list')
332
- .description('list members with structured filters (no keyword search)')
333
- .option('--privilege <value>', 'filter by privilege: owner|admin|member, supports comma-separated')
334
- .option('--manager-uid <uid>', 'filter by manager UID')
335
- .option('--limit <n>', 'page size')
336
- .option('--page-token <token>', 'pagination token from previous page')
337
- .action(function (opts) {
338
- return withHandler(async () => {
339
- const { runtimeConfig } = getContext(this);
340
- const quinn = createClient(runtimeConfig);
341
- print(await quinn.members.list({
342
- privilege: asPrivilege(opts.privilege),
343
- managerUid: opts.managerUid,
344
- limit: opts.limit ? Number(opts.limit) : undefined,
345
- token: opts.pageToken,
346
- }));
347
- });
348
- });
349
- membersCmd
350
- .command('find')
351
- .description('find members by keyword (name/email fuzzy search)')
352
- .argument('<query>')
353
- .option('--limit <n>', 'page size')
354
- .option('--page-token <token>', 'pagination token from previous page')
355
- .action(function (query, opts) {
356
- return withHandler(async () => {
357
- const { runtimeConfig } = getContext(this);
358
- const quinn = createClient(runtimeConfig);
359
- print(await quinn.members.list({
360
- search: query,
361
- limit: opts.limit ? Number(opts.limit) : undefined,
362
- token: opts.pageToken,
363
- }));
364
- });
365
- });
366
- membersCmd
367
- .command('list-managers')
368
- .description('list unique manager members in the organization')
369
- .option('--search <text>', 'optional keyword filter on manager name/email')
370
- .option('--limit <n>', 'page size')
371
- .option('--page-token <token>', 'pagination token from previous page')
372
- .action(function (opts) {
373
- return withHandler(async () => {
374
- const { runtimeConfig } = getContext(this);
375
- const quinn = createClient(runtimeConfig);
376
- print(await quinn.members.listManagers({
377
- search: opts.search,
378
- limit: opts.limit ? Number(opts.limit) : undefined,
379
- token: opts.pageToken,
380
- }));
381
- });
382
- });
383
- membersCmd
384
- .command('get')
385
- .description('get one member by ID/email, or many members by comma-separated values')
386
- .argument('<memberIdOrEmail>')
387
- .action(function (memberIdOrEmail) {
388
- return withHandler(async () => {
389
- const { runtimeConfig } = getContext(this);
390
- const quinn = createClient(runtimeConfig);
391
- const values = asCsv(memberIdOrEmail);
392
- if (values.length <= 1) {
393
- print(await quinn.members.get(memberIdOrEmail));
394
- return;
395
- }
396
- const ids = [];
397
- const emails = [];
398
- for (const value of values) {
399
- if (value.includes('@')) {
400
- emails.push(value);
401
- }
402
- else {
403
- ids.push(value);
404
- }
405
- }
406
- print(await quinn.members.batchGet({
407
- ids: ids.length > 0 ? ids : undefined,
408
- emails: emails.length > 0 ? emails : undefined,
409
- }));
410
- });
411
- });
412
- membersCmd.addHelpText('after', [
413
- '',
414
- 'Examples:',
415
- ' quinn members list --privilege owner,admin',
416
- ' quinn members list --manager-uid <uid>',
417
- ' quinn members find alice',
418
- ' quinn members list-managers --search bob',
419
- ' quinn members get <memberId>',
420
- ' quinn members get <memberId1,memberId2,user@example.com>',
421
- ].join('\n'));
422
- const rolesCmd = program.command('roles').description('role operations');
423
- rolesCmd.command('list').description('list all roles in the organization').action(function () {
424
- return withHandler(async () => {
425
- const { runtimeConfig } = getContext(this);
426
- const quinn = createClient(runtimeConfig);
427
- print(await quinn.roles.list());
428
- });
429
- });
430
- rolesCmd
431
- .command('get')
432
- .description('get one role by ID, or many roles by comma-separated values')
433
- .argument('<roleId>')
434
- .action(function (roleId) {
435
- return withHandler(async () => {
436
- const { runtimeConfig } = getContext(this);
437
- const quinn = createClient(runtimeConfig);
438
- const values = asCsv(roleId);
439
- if (values.length <= 1) {
440
- print(await quinn.roles.get(roleId));
441
- return;
442
- }
443
- print(await quinn.roles.batchGet(values));
444
- });
445
- });
446
- rolesCmd.addHelpText('after', [
447
- '',
448
- 'Examples:',
449
- ' quinn roles list',
450
- ' quinn roles get <roleId>',
451
- ' quinn roles get <roleId1,roleId2>',
452
- ].join('\n'));
453
- const levelsCmd = program.command('levels').description('level operations');
454
- levelsCmd
455
- .command('list')
456
- .description('list levels under a role')
457
- .requiredOption('--role-id <id>', 'role ID to scope the query')
458
- .option('--limit <n>', 'page size')
459
- .option('--page-token <token>', 'pagination token from previous page')
460
- .action(function (opts) {
461
- return withHandler(async () => {
462
- const { runtimeConfig } = getContext(this);
463
- const quinn = createClient(runtimeConfig);
464
- print(await quinn.levels.list({
465
- roleId: opts.roleId,
466
- limit: opts.limit ? Number(opts.limit) : undefined,
467
- token: opts.pageToken,
468
- }));
469
- });
470
- });
471
- levelsCmd
472
- .command('get')
473
- .description('get one level by ID, or many levels by comma-separated values')
474
- .argument('<levelId>')
475
- .action(function (levelId) {
476
- return withHandler(async () => {
477
- const { runtimeConfig } = getContext(this);
478
- const quinn = createClient(runtimeConfig);
479
- const values = asCsv(levelId);
480
- if (values.length <= 1) {
481
- print(await quinn.levels.get(levelId));
482
- return;
483
- }
484
- print(await quinn.levels.batchGet(values));
485
- });
486
- });
487
- levelsCmd.addHelpText('after', [
488
- '',
489
- 'Examples:',
490
- ' quinn levels list --role-id <roleId>',
491
- ' quinn levels get <levelId>',
492
- ' quinn levels get <levelId1,levelId2>',
493
- ].join('\n'));
494
- const compsCmd = program.command('competencies').description('competency operations');
495
- compsCmd
496
- .command('list')
497
- .description('list competencies scoped by role + level')
498
- .requiredOption('--role-id <id>', 'role ID')
499
- .requiredOption('--level-id <id>', 'level ID')
500
- .option('--search <text>', 'optional keyword filter')
501
- .option('--limit <n>', 'page size')
502
- .option('--page-token <token>', 'pagination token from previous page')
503
- .action(function (opts) {
504
- return withHandler(async () => {
505
- const { runtimeConfig } = getContext(this);
506
- const quinn = createClient(runtimeConfig);
507
- print(await quinn.competencies.list({
508
- roleId: opts.roleId,
509
- levelId: opts.levelId,
510
- search: opts.search,
511
- limit: opts.limit ? Number(opts.limit) : undefined,
512
- token: opts.pageToken,
513
- }));
514
- });
515
- });
516
- compsCmd
517
- .command('get')
518
- .description('get one competency by ID, or many competencies by comma-separated values')
519
- .argument('<competencyId>')
520
- .action(function (competencyId) {
521
- return withHandler(async () => {
522
- const { runtimeConfig } = getContext(this);
523
- const quinn = createClient(runtimeConfig);
524
- const values = asCsv(competencyId);
525
- if (values.length <= 1) {
526
- print(await quinn.competencies.get(competencyId));
527
- return;
528
- }
529
- print(await quinn.competencies.batchGet(values));
530
- });
531
- });
532
- compsCmd
533
- .command('courses')
534
- .description('list courses under a competency')
535
- .argument('<competencyId>')
536
- .action(function (competencyId) {
537
- return withHandler(async () => {
538
- const { runtimeConfig } = getContext(this);
539
- const quinn = createClient(runtimeConfig);
540
- print(await quinn.competencies.listCourses(competencyId));
541
- });
542
- });
543
- compsCmd.addHelpText('after', [
544
- '',
545
- 'Examples:',
546
- ' quinn competencies list --role-id <roleId> --level-id <levelId>',
547
- ' quinn competencies get <competencyId>',
548
- ' quinn competencies get <id1,id2>',
549
- ' quinn competencies courses <competencyId>',
550
- ].join('\n'));
551
- const endorseCmd = program.command('endorsements').description('endorsement operations');
552
- endorseCmd
553
- .command('get')
554
- .description('get one endorsement by ID')
555
- .argument('<endorsementId>')
556
- .action(function (endorsementId) {
557
- return withHandler(async () => {
558
- const { runtimeConfig } = getContext(this);
559
- const quinn = createClient(runtimeConfig);
560
- print(await quinn.endorsements.get(endorsementId));
561
- });
562
- });
563
- endorseCmd
564
- .command('find')
565
- .description('find one endorsement by user + competency')
566
- .requiredOption('--uid <uid>', 'user ID')
567
- .requiredOption('--competency-id <id>', 'competency ID')
568
- .action(function (opts) {
569
- return withHandler(async () => {
570
- const { runtimeConfig } = getContext(this);
571
- const quinn = createClient(runtimeConfig);
572
- print(await quinn.endorsements.find(opts.uid, opts.competencyId));
573
- });
574
- });
575
- endorseCmd
576
- .command('list')
577
- .description('list endorsements by user IDs and competency IDs')
578
- .requiredOption('--uids <uids>', 'comma-separated user IDs')
579
- .requiredOption('--competency-ids <ids>', 'comma-separated competency IDs')
580
- .action(function (opts) {
581
- return withHandler(async () => {
582
- const { runtimeConfig } = getContext(this);
583
- const quinn = createClient(runtimeConfig);
584
- print(await quinn.endorsements.list({
585
- uids: asCsv(opts.uids),
586
- competencyIds: asCsv(opts.competencyIds),
587
- }));
588
- });
589
- });
590
- endorseCmd.addHelpText('after', [
591
- '',
592
- 'Examples:',
593
- ' quinn endorsements get <endorsementId>',
594
- ' quinn endorsements find --uid <uid> --competency-id <competencyId>',
595
- ' quinn endorsements list --uids <u1,u2> --competency-ids <c1,c2>',
596
- ].join('\n'));
597
288
  program.parseAsync(process.argv).catch((error) => {
598
289
  const message = error instanceof Error ? error.message : String(error);
599
290
  process.stderr.write(`${message}\n`);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@totoday/quinn-cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "bin": {
5
5
  "quinn": "dist/index.js"
6
6
  },
7
7
  "dependencies": {
8
8
  "commander": "^13.1.0",
9
- "@totoday/quinn-sdk": "0.1.2"
9
+ "@totoday/quinn-sdk": "0.2.0"
10
10
  },
11
11
  "devDependencies": {
12
12
  "@types/node": "^22.13.10",
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { Command } from 'commander';
7
- import { DEFAULT_QUINN_API_URL, Quinn, QuinnAuth, Privilege } from '@totoday/quinn-sdk';
7
+ import { DEFAULT_QUINN_API_URL, Quinn, QuinnAuth } from '@totoday/quinn-sdk';
8
8
 
9
9
  type QuinnCliConfig = {
10
10
  apiUrl?: string;
@@ -24,27 +24,6 @@ function print(data: unknown): void {
24
24
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
25
25
  }
26
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
27
  function maskToken(token?: string): string | null {
49
28
  if (!token) {
50
29
  return null;
@@ -55,51 +34,6 @@ function maskToken(token?: string): string | null {
55
34
  return `${token.slice(0, 6)}...${token.slice(-4)}`;
56
35
  }
57
36
 
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 || DEFAULT_QUINN_API_URL,
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 (!token) {
95
- throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
96
- }
97
- if (!orgId) {
98
- throw new Error('missing orgId: set --org-id or QUINN_ORG_ID or config');
99
- }
100
- return new Quinn({ apiUrl, token, orgId });
101
- }
102
-
103
37
  function readPasswordFromStdin(): string {
104
38
  const value = fs.readFileSync(0, 'utf8').trim();
105
39
  if (!value) {
@@ -163,8 +97,52 @@ async function promptPasswordHidden(prompt = 'Password: '): Promise<string> {
163
97
  });
164
98
  }
165
99
 
100
+ function getConfigPath(opts: GlobalOptions): string {
101
+ return (
102
+ opts.configPath ||
103
+ process.env.QUINN_CONFIG_PATH ||
104
+ path.join(os.homedir(), '.config', 'quinn', 'config.json')
105
+ );
106
+ }
107
+
108
+ function readConfig(configPath: string): QuinnCliConfig {
109
+ if (!fs.existsSync(configPath)) {
110
+ return {};
111
+ }
112
+ try {
113
+ const raw = fs.readFileSync(configPath, 'utf8');
114
+ const parsed = JSON.parse(raw) as QuinnCliConfig;
115
+ return parsed ?? {};
116
+ } catch {
117
+ return {};
118
+ }
119
+ }
120
+
121
+ function writeConfig(configPath: string, config: QuinnCliConfig): void {
122
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
123
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
124
+ }
125
+
126
+ function resolveRuntimeConfig(opts: GlobalOptions, fileConfig: QuinnCliConfig): QuinnCliConfig {
127
+ return {
128
+ apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl || DEFAULT_QUINN_API_URL,
129
+ token: opts.apiToken || opts.token || process.env.QUINN_API_TOKEN || fileConfig.token,
130
+ orgId: opts.orgId || process.env.QUINN_ORG_ID || fileConfig.orgId,
131
+ };
132
+ }
133
+
134
+ function createClient(config: QuinnCliConfig): Quinn {
135
+ const { apiUrl, token, orgId } = config;
136
+ if (!token) {
137
+ throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
138
+ }
139
+ if (!orgId) {
140
+ throw new Error('missing orgId: set --org-id or QUINN_ORG_ID or config');
141
+ }
142
+ return new Quinn({ apiUrl, token, orgId });
143
+ }
144
+
166
145
  function getContext(command: Command): {
167
- global: GlobalOptions;
168
146
  configPath: string;
169
147
  fileConfig: QuinnCliConfig;
170
148
  runtimeConfig: QuinnCliConfig;
@@ -173,7 +151,7 @@ function getContext(command: Command): {
173
151
  const configPath = getConfigPath(global);
174
152
  const fileConfig = readConfig(configPath);
175
153
  const runtimeConfig = resolveRuntimeConfig(global, fileConfig);
176
- return { global, configPath, fileConfig, runtimeConfig };
154
+ return { configPath, fileConfig, runtimeConfig };
177
155
  }
178
156
 
179
157
  async function withHandler(fn: () => Promise<void>): Promise<void> {
@@ -219,7 +197,7 @@ async function withHandler(fn: () => Promise<void>): Promise<void> {
219
197
  const program = new Command();
220
198
  program
221
199
  .name('quinn')
222
- .description('Quinn CLI')
200
+ .description('Quinn CLI (minimal setup utility, SDK-first runtime)')
223
201
  .addHelpText(
224
202
  'after',
225
203
  [
@@ -227,10 +205,8 @@ program
227
205
  'Examples:',
228
206
  ' quinn login --email <email>',
229
207
  ' echo "<password>" | quinn login --email <email> --password-stdin',
230
- ' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
231
- ' quinn organizations current',
232
- ' quinn members find alice',
233
- ' quinn members get user-1,user-2,user@example.com',
208
+ ' quinn config get',
209
+ ' quinn test',
234
210
  ].join('\n')
235
211
  )
236
212
  .showHelpAfterError()
@@ -294,16 +270,6 @@ configCmd
294
270
  },
295
271
  });
296
272
  });
297
- configCmd.addHelpText(
298
- 'after',
299
- [
300
- '',
301
- 'Examples:',
302
- ' quinn config path',
303
- ' quinn config get',
304
- ' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
305
- ].join('\n')
306
- );
307
273
 
308
274
  program
309
275
  .command('login')
@@ -329,14 +295,8 @@ program
329
295
  } else {
330
296
  password = await promptPasswordHidden();
331
297
  }
332
- const auth = new QuinnAuth({
333
- apiUrl: runtimeConfig.apiUrl,
334
- configPath,
335
- });
336
- const login = await auth.login({
337
- email: opts.email,
338
- password,
339
- });
298
+ const auth = new QuinnAuth({ apiUrl: runtimeConfig.apiUrl, configPath });
299
+ const login = await auth.login({ email: opts.email, password });
340
300
  const resolvedOrgId = opts.orgId || login.orgId || fileConfig.orgId;
341
301
  if (!resolvedOrgId) {
342
302
  throw new Error('orgId not found in login response, please pass --org-id');
@@ -359,338 +319,17 @@ program
359
319
  });
360
320
  });
361
321
 
362
- const orgCmd = program
363
- .command('organizations')
364
- .description('organization metadata and high-level stats');
365
- orgCmd
366
- .command('current')
367
- .description('get current organization details with aggregate stats')
322
+ program
323
+ .command('test')
324
+ .description('test connectivity and auth by calling organizations.current()')
368
325
  .action(function () {
369
326
  return withHandler(async () => {
370
327
  const { runtimeConfig } = getContext(this);
371
328
  const quinn = createClient(runtimeConfig);
372
- print(await quinn.organizations.current());
373
- });
374
- });
375
- orgCmd.addHelpText(
376
- 'after',
377
- [
378
- '',
379
- 'Examples:',
380
- ' quinn organizations current',
381
- ].join('\n')
382
- );
383
-
384
- const membersCmd = program.command('members').description('member operations');
385
- membersCmd
386
- .command('list')
387
- .description('list members with structured filters (no keyword search)')
388
- .option('--privilege <value>', 'filter by privilege: owner|admin|member, supports comma-separated')
389
- .option('--manager-uid <uid>', 'filter by manager UID')
390
- .option('--limit <n>', 'page size')
391
- .option('--page-token <token>', 'pagination token from previous page')
392
- .action(function (opts: { privilege?: string; managerUid?: string; limit?: string; pageToken?: string }) {
393
- return withHandler(async () => {
394
- const { runtimeConfig } = getContext(this);
395
- const quinn = createClient(runtimeConfig);
396
- print(
397
- await quinn.members.list({
398
- privilege: asPrivilege(opts.privilege),
399
- managerUid: opts.managerUid,
400
- limit: opts.limit ? Number(opts.limit) : undefined,
401
- token: opts.pageToken,
402
- })
403
- );
404
- });
405
- });
406
-
407
- membersCmd
408
- .command('find')
409
- .description('find members by keyword (name/email fuzzy search)')
410
- .argument('<query>')
411
- .option('--limit <n>', 'page size')
412
- .option('--page-token <token>', 'pagination token from previous page')
413
- .action(function (query: string, opts: { limit?: string; pageToken?: string }) {
414
- return withHandler(async () => {
415
- const { runtimeConfig } = getContext(this);
416
- const quinn = createClient(runtimeConfig);
417
- print(
418
- await quinn.members.list({
419
- search: query,
420
- limit: opts.limit ? Number(opts.limit) : undefined,
421
- token: opts.pageToken,
422
- })
423
- );
424
- });
425
- });
426
-
427
- membersCmd
428
- .command('list-managers')
429
- .description('list unique manager members in the organization')
430
- .option('--search <text>', 'optional keyword filter on manager name/email')
431
- .option('--limit <n>', 'page size')
432
- .option('--page-token <token>', 'pagination token from previous page')
433
- .action(function (opts: { search?: string; limit?: string; pageToken?: string }) {
434
- return withHandler(async () => {
435
- const { runtimeConfig } = getContext(this);
436
- const quinn = createClient(runtimeConfig);
437
- print(
438
- await quinn.members.listManagers({
439
- search: opts.search,
440
- limit: opts.limit ? Number(opts.limit) : undefined,
441
- token: opts.pageToken,
442
- })
443
- );
444
- });
445
- });
446
-
447
- membersCmd
448
- .command('get')
449
- .description('get one member by ID/email, or many members by comma-separated values')
450
- .argument('<memberIdOrEmail>')
451
- .action(function (memberIdOrEmail: string) {
452
- return withHandler(async () => {
453
- const { runtimeConfig } = getContext(this);
454
- const quinn = createClient(runtimeConfig);
455
- const values = asCsv(memberIdOrEmail);
456
- if (values.length <= 1) {
457
- print(await quinn.members.get(memberIdOrEmail));
458
- return;
459
- }
460
-
461
- const ids: string[] = [];
462
- const emails: string[] = [];
463
- for (const value of values) {
464
- if (value.includes('@')) {
465
- emails.push(value);
466
- } else {
467
- ids.push(value);
468
- }
469
- }
470
- print(
471
- await quinn.members.batchGet({
472
- ids: ids.length > 0 ? ids : undefined,
473
- emails: emails.length > 0 ? emails : undefined,
474
- })
475
- );
476
- });
477
- });
478
- membersCmd.addHelpText(
479
- 'after',
480
- [
481
- '',
482
- 'Examples:',
483
- ' quinn members list --privilege owner,admin',
484
- ' quinn members list --manager-uid <uid>',
485
- ' quinn members find alice',
486
- ' quinn members list-managers --search bob',
487
- ' quinn members get <memberId>',
488
- ' quinn members get <memberId1,memberId2,user@example.com>',
489
- ].join('\n')
490
- );
491
-
492
- const rolesCmd = program.command('roles').description('role operations');
493
- rolesCmd.command('list').description('list all roles in the organization').action(function () {
494
- return withHandler(async () => {
495
- const { runtimeConfig } = getContext(this);
496
- const quinn = createClient(runtimeConfig);
497
- print(await quinn.roles.list());
498
- });
499
- });
500
-
501
- rolesCmd
502
- .command('get')
503
- .description('get one role by ID, or many roles by comma-separated values')
504
- .argument('<roleId>')
505
- .action(function (roleId: string) {
506
- return withHandler(async () => {
507
- const { runtimeConfig } = getContext(this);
508
- const quinn = createClient(runtimeConfig);
509
- const values = asCsv(roleId);
510
- if (values.length <= 1) {
511
- print(await quinn.roles.get(roleId));
512
- return;
513
- }
514
- print(await quinn.roles.batchGet(values));
515
- });
516
- });
517
- rolesCmd.addHelpText(
518
- 'after',
519
- [
520
- '',
521
- 'Examples:',
522
- ' quinn roles list',
523
- ' quinn roles get <roleId>',
524
- ' quinn roles get <roleId1,roleId2>',
525
- ].join('\n')
526
- );
527
-
528
- const levelsCmd = program.command('levels').description('level operations');
529
- levelsCmd
530
- .command('list')
531
- .description('list levels under a role')
532
- .requiredOption('--role-id <id>', 'role ID to scope the query')
533
- .option('--limit <n>', 'page size')
534
- .option('--page-token <token>', 'pagination token from previous page')
535
- .action(function (opts: { roleId: string; limit?: string; pageToken?: string }) {
536
- return withHandler(async () => {
537
- const { runtimeConfig } = getContext(this);
538
- const quinn = createClient(runtimeConfig);
539
- print(
540
- await quinn.levels.list({
541
- roleId: opts.roleId,
542
- limit: opts.limit ? Number(opts.limit) : undefined,
543
- token: opts.pageToken,
544
- })
545
- );
546
- });
547
- });
548
-
549
- levelsCmd
550
- .command('get')
551
- .description('get one level by ID, or many levels by comma-separated values')
552
- .argument('<levelId>')
553
- .action(function (levelId: string) {
554
- return withHandler(async () => {
555
- const { runtimeConfig } = getContext(this);
556
- const quinn = createClient(runtimeConfig);
557
- const values = asCsv(levelId);
558
- if (values.length <= 1) {
559
- print(await quinn.levels.get(levelId));
560
- return;
561
- }
562
- print(await quinn.levels.batchGet(values));
563
- });
564
- });
565
- levelsCmd.addHelpText(
566
- 'after',
567
- [
568
- '',
569
- 'Examples:',
570
- ' quinn levels list --role-id <roleId>',
571
- ' quinn levels get <levelId>',
572
- ' quinn levels get <levelId1,levelId2>',
573
- ].join('\n')
574
- );
575
-
576
- const compsCmd = program.command('competencies').description('competency operations');
577
- compsCmd
578
- .command('list')
579
- .description('list competencies scoped by role + level')
580
- .requiredOption('--role-id <id>', 'role ID')
581
- .requiredOption('--level-id <id>', 'level ID')
582
- .option('--search <text>', 'optional keyword filter')
583
- .option('--limit <n>', 'page size')
584
- .option('--page-token <token>', 'pagination token from previous page')
585
- .action(function (opts: { roleId: string; levelId: string; search?: string; limit?: string; pageToken?: string }) {
586
- return withHandler(async () => {
587
- const { runtimeConfig } = getContext(this);
588
- const quinn = createClient(runtimeConfig);
589
- print(
590
- await quinn.competencies.list({
591
- roleId: opts.roleId,
592
- levelId: opts.levelId,
593
- search: opts.search,
594
- limit: opts.limit ? Number(opts.limit) : undefined,
595
- token: opts.pageToken,
596
- })
597
- );
598
- });
599
- });
600
-
601
- compsCmd
602
- .command('get')
603
- .description('get one competency by ID, or many competencies by comma-separated values')
604
- .argument('<competencyId>')
605
- .action(function (competencyId: string) {
606
- return withHandler(async () => {
607
- const { runtimeConfig } = getContext(this);
608
- const quinn = createClient(runtimeConfig);
609
- const values = asCsv(competencyId);
610
- if (values.length <= 1) {
611
- print(await quinn.competencies.get(competencyId));
612
- return;
613
- }
614
- print(await quinn.competencies.batchGet(values));
615
- });
616
- });
617
-
618
- compsCmd
619
- .command('courses')
620
- .description('list courses under a competency')
621
- .argument('<competencyId>')
622
- .action(function (competencyId: string) {
623
- return withHandler(async () => {
624
- const { runtimeConfig } = getContext(this);
625
- const quinn = createClient(runtimeConfig);
626
- print(await quinn.competencies.listCourses(competencyId));
627
- });
628
- });
629
- compsCmd.addHelpText(
630
- 'after',
631
- [
632
- '',
633
- 'Examples:',
634
- ' quinn competencies list --role-id <roleId> --level-id <levelId>',
635
- ' quinn competencies get <competencyId>',
636
- ' quinn competencies get <id1,id2>',
637
- ' quinn competencies courses <competencyId>',
638
- ].join('\n')
639
- );
640
-
641
- const endorseCmd = program.command('endorsements').description('endorsement operations');
642
- endorseCmd
643
- .command('get')
644
- .description('get one endorsement by ID')
645
- .argument('<endorsementId>')
646
- .action(function (endorsementId: string) {
647
- return withHandler(async () => {
648
- const { runtimeConfig } = getContext(this);
649
- const quinn = createClient(runtimeConfig);
650
- print(await quinn.endorsements.get(endorsementId));
651
- });
652
- });
653
-
654
- endorseCmd
655
- .command('find')
656
- .description('find one endorsement by user + competency')
657
- .requiredOption('--uid <uid>', 'user ID')
658
- .requiredOption('--competency-id <id>', 'competency ID')
659
- .action(function (opts: { uid: string; competencyId: string }) {
660
- return withHandler(async () => {
661
- const { runtimeConfig } = getContext(this);
662
- const quinn = createClient(runtimeConfig);
663
- print(await quinn.endorsements.find(opts.uid, opts.competencyId));
664
- });
665
- });
666
-
667
- endorseCmd
668
- .command('list')
669
- .description('list endorsements by user IDs and competency IDs')
670
- .requiredOption('--uids <uids>', 'comma-separated user IDs')
671
- .requiredOption('--competency-ids <ids>', 'comma-separated competency IDs')
672
- .action(function (opts: { uids: string; competencyIds: string }) {
673
- return withHandler(async () => {
674
- const { runtimeConfig } = getContext(this);
675
- const quinn = createClient(runtimeConfig);
676
- print(
677
- await quinn.endorsements.list({
678
- uids: asCsv(opts.uids),
679
- competencyIds: asCsv(opts.competencyIds),
680
- })
681
- );
329
+ const item = await quinn.organizations.current();
330
+ print({ ok: true, item });
682
331
  });
683
332
  });
684
- endorseCmd.addHelpText(
685
- 'after',
686
- [
687
- '',
688
- 'Examples:',
689
- ' quinn endorsements get <endorsementId>',
690
- ' quinn endorsements find --uid <uid> --competency-id <competencyId>',
691
- ' quinn endorsements list --uids <u1,u2> --competency-ids <c1,c2>',
692
- ].join('\n')
693
- );
694
333
 
695
334
  program.parseAsync(process.argv).catch((error: unknown) => {
696
335
  const message = error instanceof Error ? error.message : String(error);