@totoday/quinn-cli 0.1.1 → 0.1.3

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.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 16a1fe9: Move organization path handling into the shared HTTP client to simplify service construction.
8
+ - Updated dependencies [16a1fe9]
9
+ - @totoday/quinn-sdk@0.1.3
10
+
11
+ ## 0.1.2
12
+
13
+ ### Patch Changes
14
+
15
+ - 629fba0: Unify organization query semantics around `organizations current`:
16
+ - remove `organizations details` command from CLI
17
+ - make `organizations current` return organization details with aggregate stats
18
+ - align SDK and skill documentation with the simplified flow
19
+ - Updated dependencies [45948dc]
20
+ - @totoday/quinn-sdk@0.1.2
21
+
3
22
  ## 0.1.1
4
23
 
5
24
  ### Patch Changes
@@ -8,3 +27,4 @@
8
27
  - default to interactive hidden password prompt
9
28
  - add `--password-stdin` for script-friendly secure input
10
29
  - deprecate plain-text `--password` with warning
30
+ - @totoday/quinn-sdk@0.1.1
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @totoday/quinn-cli
2
+
3
+ Minimal CLI for Quinn auth/config/connectivity setup.
4
+
5
+ Business data operations should use `@totoday/quinn-sdk`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i -g @totoday/quinn-cli
11
+ quinn --help
12
+ ```
13
+
14
+ One-off without global install:
15
+
16
+ ```bash
17
+ npx @totoday/quinn-cli --help
18
+ ```
19
+
20
+ ## Commands
21
+
22
+ ```bash
23
+ # login (recommended: hidden password prompt)
24
+ quinn login --email <email>
25
+
26
+ # login via stdin (scripts/password managers)
27
+ echo "<password>" | quinn login --email <email> --password-stdin
28
+
29
+ # inspect local config
30
+ quinn config path
31
+ quinn config get
32
+
33
+ # update local config
34
+ quinn config set --org-id <orgId>
35
+ quinn config set --api-url <apiUrl> --api-token <token> --org-id <orgId>
36
+
37
+ # connectivity test (calls organizations.current via SDK)
38
+ quinn test
39
+ ```
40
+
41
+ ## Config
42
+
43
+ CLI reads config from:
44
+
45
+ - `~/.config/quinn/config.json` (default)
46
+ - `QUINN_CONFIG_PATH` (override)
47
+
48
+ Runtime override order:
49
+
50
+ 1. command flags
51
+ 2. env vars (`QUINN_API_URL`, `QUINN_API_TOKEN`, `QUINN_ORG_ID`)
52
+ 3. config file
53
+
54
+ If `apiUrl` is missing, default is `https://api.lunapark.com`.
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 details',
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,303 +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 basic info (id, name)')
317
- .action(function () {
318
- return withHandler(async () => {
319
- const { runtimeConfig } = getContext(this);
320
- const quinn = createClient(runtimeConfig);
321
- print(await quinn.organizations.current());
322
- });
323
- });
324
- orgCmd
325
- .command('details')
326
- .description('get organization details with aggregate stats')
277
+ program
278
+ .command('test')
279
+ .description('test connectivity and auth by calling organizations.current()')
327
280
  .action(function () {
328
281
  return withHandler(async () => {
329
282
  const { runtimeConfig } = getContext(this);
330
283
  const quinn = createClient(runtimeConfig);
331
- print(await quinn.organizations.getDetails());
284
+ const item = await quinn.organizations.current();
285
+ print({ ok: true, item });
332
286
  });
333
287
  });
334
- orgCmd.addHelpText('after', [
335
- '',
336
- 'Examples:',
337
- ' quinn organizations current',
338
- ' quinn organizations details',
339
- ].join('\n'));
340
- const membersCmd = program.command('members').description('member operations');
341
- membersCmd
342
- .command('list')
343
- .description('list members with structured filters (no keyword search)')
344
- .option('--privilege <value>', 'filter by privilege: owner|admin|member, supports comma-separated')
345
- .option('--manager-uid <uid>', 'filter by manager UID')
346
- .option('--limit <n>', 'page size')
347
- .option('--page-token <token>', 'pagination token from previous page')
348
- .action(function (opts) {
349
- return withHandler(async () => {
350
- const { runtimeConfig } = getContext(this);
351
- const quinn = createClient(runtimeConfig);
352
- print(await quinn.members.list({
353
- privilege: asPrivilege(opts.privilege),
354
- managerUid: opts.managerUid,
355
- limit: opts.limit ? Number(opts.limit) : undefined,
356
- token: opts.pageToken,
357
- }));
358
- });
359
- });
360
- membersCmd
361
- .command('find')
362
- .description('find members by keyword (name/email fuzzy search)')
363
- .argument('<query>')
364
- .option('--limit <n>', 'page size')
365
- .option('--page-token <token>', 'pagination token from previous page')
366
- .action(function (query, opts) {
367
- return withHandler(async () => {
368
- const { runtimeConfig } = getContext(this);
369
- const quinn = createClient(runtimeConfig);
370
- print(await quinn.members.list({
371
- search: query,
372
- limit: opts.limit ? Number(opts.limit) : undefined,
373
- token: opts.pageToken,
374
- }));
375
- });
376
- });
377
- membersCmd
378
- .command('list-managers')
379
- .description('list unique manager members in the organization')
380
- .option('--search <text>', 'optional keyword filter on manager name/email')
381
- .option('--limit <n>', 'page size')
382
- .option('--page-token <token>', 'pagination token from previous page')
383
- .action(function (opts) {
384
- return withHandler(async () => {
385
- const { runtimeConfig } = getContext(this);
386
- const quinn = createClient(runtimeConfig);
387
- print(await quinn.members.listManagers({
388
- search: opts.search,
389
- limit: opts.limit ? Number(opts.limit) : undefined,
390
- token: opts.pageToken,
391
- }));
392
- });
393
- });
394
- membersCmd
395
- .command('get')
396
- .description('get one member by ID/email, or many members by comma-separated values')
397
- .argument('<memberIdOrEmail>')
398
- .action(function (memberIdOrEmail) {
399
- return withHandler(async () => {
400
- const { runtimeConfig } = getContext(this);
401
- const quinn = createClient(runtimeConfig);
402
- const values = asCsv(memberIdOrEmail);
403
- if (values.length <= 1) {
404
- print(await quinn.members.get(memberIdOrEmail));
405
- return;
406
- }
407
- const ids = [];
408
- const emails = [];
409
- for (const value of values) {
410
- if (value.includes('@')) {
411
- emails.push(value);
412
- }
413
- else {
414
- ids.push(value);
415
- }
416
- }
417
- print(await quinn.members.batchGet({
418
- ids: ids.length > 0 ? ids : undefined,
419
- emails: emails.length > 0 ? emails : undefined,
420
- }));
421
- });
422
- });
423
- membersCmd.addHelpText('after', [
424
- '',
425
- 'Examples:',
426
- ' quinn members list --privilege owner,admin',
427
- ' quinn members list --manager-uid <uid>',
428
- ' quinn members find alice',
429
- ' quinn members list-managers --search bob',
430
- ' quinn members get <memberId>',
431
- ' quinn members get <memberId1,memberId2,user@example.com>',
432
- ].join('\n'));
433
- const rolesCmd = program.command('roles').description('role operations');
434
- rolesCmd.command('list').description('list all roles in the organization').action(function () {
435
- return withHandler(async () => {
436
- const { runtimeConfig } = getContext(this);
437
- const quinn = createClient(runtimeConfig);
438
- print(await quinn.roles.list());
439
- });
440
- });
441
- rolesCmd
442
- .command('get')
443
- .description('get one role by ID, or many roles by comma-separated values')
444
- .argument('<roleId>')
445
- .action(function (roleId) {
446
- return withHandler(async () => {
447
- const { runtimeConfig } = getContext(this);
448
- const quinn = createClient(runtimeConfig);
449
- const values = asCsv(roleId);
450
- if (values.length <= 1) {
451
- print(await quinn.roles.get(roleId));
452
- return;
453
- }
454
- print(await quinn.roles.batchGet(values));
455
- });
456
- });
457
- rolesCmd.addHelpText('after', [
458
- '',
459
- 'Examples:',
460
- ' quinn roles list',
461
- ' quinn roles get <roleId>',
462
- ' quinn roles get <roleId1,roleId2>',
463
- ].join('\n'));
464
- const levelsCmd = program.command('levels').description('level operations');
465
- levelsCmd
466
- .command('list')
467
- .description('list levels under a role')
468
- .requiredOption('--role-id <id>', 'role ID to scope the query')
469
- .option('--limit <n>', 'page size')
470
- .option('--page-token <token>', 'pagination token from previous page')
471
- .action(function (opts) {
472
- return withHandler(async () => {
473
- const { runtimeConfig } = getContext(this);
474
- const quinn = createClient(runtimeConfig);
475
- print(await quinn.levels.list({
476
- roleId: opts.roleId,
477
- limit: opts.limit ? Number(opts.limit) : undefined,
478
- token: opts.pageToken,
479
- }));
480
- });
481
- });
482
- levelsCmd
483
- .command('get')
484
- .description('get one level by ID, or many levels by comma-separated values')
485
- .argument('<levelId>')
486
- .action(function (levelId) {
487
- return withHandler(async () => {
488
- const { runtimeConfig } = getContext(this);
489
- const quinn = createClient(runtimeConfig);
490
- const values = asCsv(levelId);
491
- if (values.length <= 1) {
492
- print(await quinn.levels.get(levelId));
493
- return;
494
- }
495
- print(await quinn.levels.batchGet(values));
496
- });
497
- });
498
- levelsCmd.addHelpText('after', [
499
- '',
500
- 'Examples:',
501
- ' quinn levels list --role-id <roleId>',
502
- ' quinn levels get <levelId>',
503
- ' quinn levels get <levelId1,levelId2>',
504
- ].join('\n'));
505
- const compsCmd = program.command('competencies').description('competency operations');
506
- compsCmd
507
- .command('list')
508
- .description('list competencies scoped by role + level')
509
- .requiredOption('--role-id <id>', 'role ID')
510
- .requiredOption('--level-id <id>', 'level ID')
511
- .option('--search <text>', 'optional keyword filter')
512
- .option('--limit <n>', 'page size')
513
- .option('--page-token <token>', 'pagination token from previous page')
514
- .action(function (opts) {
515
- return withHandler(async () => {
516
- const { runtimeConfig } = getContext(this);
517
- const quinn = createClient(runtimeConfig);
518
- print(await quinn.competencies.list({
519
- roleId: opts.roleId,
520
- levelId: opts.levelId,
521
- search: opts.search,
522
- limit: opts.limit ? Number(opts.limit) : undefined,
523
- token: opts.pageToken,
524
- }));
525
- });
526
- });
527
- compsCmd
528
- .command('get')
529
- .description('get one competency by ID, or many competencies by comma-separated values')
530
- .argument('<competencyId>')
531
- .action(function (competencyId) {
532
- return withHandler(async () => {
533
- const { runtimeConfig } = getContext(this);
534
- const quinn = createClient(runtimeConfig);
535
- const values = asCsv(competencyId);
536
- if (values.length <= 1) {
537
- print(await quinn.competencies.get(competencyId));
538
- return;
539
- }
540
- print(await quinn.competencies.batchGet(values));
541
- });
542
- });
543
- compsCmd
544
- .command('courses')
545
- .description('list courses under a competency')
546
- .argument('<competencyId>')
547
- .action(function (competencyId) {
548
- return withHandler(async () => {
549
- const { runtimeConfig } = getContext(this);
550
- const quinn = createClient(runtimeConfig);
551
- print(await quinn.competencies.listCourses(competencyId));
552
- });
553
- });
554
- compsCmd.addHelpText('after', [
555
- '',
556
- 'Examples:',
557
- ' quinn competencies list --role-id <roleId> --level-id <levelId>',
558
- ' quinn competencies get <competencyId>',
559
- ' quinn competencies get <id1,id2>',
560
- ' quinn competencies courses <competencyId>',
561
- ].join('\n'));
562
- const endorseCmd = program.command('endorsements').description('endorsement operations');
563
- endorseCmd
564
- .command('get')
565
- .description('get one endorsement by ID')
566
- .argument('<endorsementId>')
567
- .action(function (endorsementId) {
568
- return withHandler(async () => {
569
- const { runtimeConfig } = getContext(this);
570
- const quinn = createClient(runtimeConfig);
571
- print(await quinn.endorsements.get(endorsementId));
572
- });
573
- });
574
- endorseCmd
575
- .command('find')
576
- .description('find one endorsement by user + competency')
577
- .requiredOption('--uid <uid>', 'user ID')
578
- .requiredOption('--competency-id <id>', 'competency ID')
579
- .action(function (opts) {
580
- return withHandler(async () => {
581
- const { runtimeConfig } = getContext(this);
582
- const quinn = createClient(runtimeConfig);
583
- print(await quinn.endorsements.find(opts.uid, opts.competencyId));
584
- });
585
- });
586
- endorseCmd
587
- .command('list')
588
- .description('list endorsements by user IDs and competency IDs')
589
- .requiredOption('--uids <uids>', 'comma-separated user IDs')
590
- .requiredOption('--competency-ids <ids>', 'comma-separated competency IDs')
591
- .action(function (opts) {
592
- return withHandler(async () => {
593
- const { runtimeConfig } = getContext(this);
594
- const quinn = createClient(runtimeConfig);
595
- print(await quinn.endorsements.list({
596
- uids: asCsv(opts.uids),
597
- competencyIds: asCsv(opts.competencyIds),
598
- }));
599
- });
600
- });
601
- endorseCmd.addHelpText('after', [
602
- '',
603
- 'Examples:',
604
- ' quinn endorsements get <endorsementId>',
605
- ' quinn endorsements find --uid <uid> --competency-id <competencyId>',
606
- ' quinn endorsements list --uids <u1,u2> --competency-ids <c1,c2>',
607
- ].join('\n'));
608
288
  program.parseAsync(process.argv).catch((error) => {
609
289
  const message = error instanceof Error ? error.message : String(error);
610
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.1",
3
+ "version": "0.1.3",
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.0"
9
+ "@totoday/quinn-sdk": "0.1.3"
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 details',
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,350 +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 basic info (id, name)')
368
- .action(function () {
369
- return withHandler(async () => {
370
- const { runtimeConfig } = getContext(this);
371
- const quinn = createClient(runtimeConfig);
372
- print(await quinn.organizations.current());
373
- });
374
- });
375
-
376
- orgCmd
377
- .command('details')
378
- .description('get organization details with aggregate stats')
322
+ program
323
+ .command('test')
324
+ .description('test connectivity and auth by calling organizations.current()')
379
325
  .action(function () {
380
326
  return withHandler(async () => {
381
327
  const { runtimeConfig } = getContext(this);
382
328
  const quinn = createClient(runtimeConfig);
383
- print(await quinn.organizations.getDetails());
384
- });
385
- });
386
- orgCmd.addHelpText(
387
- 'after',
388
- [
389
- '',
390
- 'Examples:',
391
- ' quinn organizations current',
392
- ' quinn organizations details',
393
- ].join('\n')
394
- );
395
-
396
- const membersCmd = program.command('members').description('member operations');
397
- membersCmd
398
- .command('list')
399
- .description('list members with structured filters (no keyword search)')
400
- .option('--privilege <value>', 'filter by privilege: owner|admin|member, supports comma-separated')
401
- .option('--manager-uid <uid>', 'filter by manager UID')
402
- .option('--limit <n>', 'page size')
403
- .option('--page-token <token>', 'pagination token from previous page')
404
- .action(function (opts: { privilege?: string; managerUid?: string; limit?: string; pageToken?: string }) {
405
- return withHandler(async () => {
406
- const { runtimeConfig } = getContext(this);
407
- const quinn = createClient(runtimeConfig);
408
- print(
409
- await quinn.members.list({
410
- privilege: asPrivilege(opts.privilege),
411
- managerUid: opts.managerUid,
412
- limit: opts.limit ? Number(opts.limit) : undefined,
413
- token: opts.pageToken,
414
- })
415
- );
416
- });
417
- });
418
-
419
- membersCmd
420
- .command('find')
421
- .description('find members by keyword (name/email fuzzy search)')
422
- .argument('<query>')
423
- .option('--limit <n>', 'page size')
424
- .option('--page-token <token>', 'pagination token from previous page')
425
- .action(function (query: string, opts: { limit?: string; pageToken?: string }) {
426
- return withHandler(async () => {
427
- const { runtimeConfig } = getContext(this);
428
- const quinn = createClient(runtimeConfig);
429
- print(
430
- await quinn.members.list({
431
- search: query,
432
- limit: opts.limit ? Number(opts.limit) : undefined,
433
- token: opts.pageToken,
434
- })
435
- );
436
- });
437
- });
438
-
439
- membersCmd
440
- .command('list-managers')
441
- .description('list unique manager members in the organization')
442
- .option('--search <text>', 'optional keyword filter on manager name/email')
443
- .option('--limit <n>', 'page size')
444
- .option('--page-token <token>', 'pagination token from previous page')
445
- .action(function (opts: { search?: string; limit?: string; pageToken?: string }) {
446
- return withHandler(async () => {
447
- const { runtimeConfig } = getContext(this);
448
- const quinn = createClient(runtimeConfig);
449
- print(
450
- await quinn.members.listManagers({
451
- search: opts.search,
452
- limit: opts.limit ? Number(opts.limit) : undefined,
453
- token: opts.pageToken,
454
- })
455
- );
456
- });
457
- });
458
-
459
- membersCmd
460
- .command('get')
461
- .description('get one member by ID/email, or many members by comma-separated values')
462
- .argument('<memberIdOrEmail>')
463
- .action(function (memberIdOrEmail: string) {
464
- return withHandler(async () => {
465
- const { runtimeConfig } = getContext(this);
466
- const quinn = createClient(runtimeConfig);
467
- const values = asCsv(memberIdOrEmail);
468
- if (values.length <= 1) {
469
- print(await quinn.members.get(memberIdOrEmail));
470
- return;
471
- }
472
-
473
- const ids: string[] = [];
474
- const emails: string[] = [];
475
- for (const value of values) {
476
- if (value.includes('@')) {
477
- emails.push(value);
478
- } else {
479
- ids.push(value);
480
- }
481
- }
482
- print(
483
- await quinn.members.batchGet({
484
- ids: ids.length > 0 ? ids : undefined,
485
- emails: emails.length > 0 ? emails : undefined,
486
- })
487
- );
488
- });
489
- });
490
- membersCmd.addHelpText(
491
- 'after',
492
- [
493
- '',
494
- 'Examples:',
495
- ' quinn members list --privilege owner,admin',
496
- ' quinn members list --manager-uid <uid>',
497
- ' quinn members find alice',
498
- ' quinn members list-managers --search bob',
499
- ' quinn members get <memberId>',
500
- ' quinn members get <memberId1,memberId2,user@example.com>',
501
- ].join('\n')
502
- );
503
-
504
- const rolesCmd = program.command('roles').description('role operations');
505
- rolesCmd.command('list').description('list all roles in the organization').action(function () {
506
- return withHandler(async () => {
507
- const { runtimeConfig } = getContext(this);
508
- const quinn = createClient(runtimeConfig);
509
- print(await quinn.roles.list());
510
- });
511
- });
512
-
513
- rolesCmd
514
- .command('get')
515
- .description('get one role by ID, or many roles by comma-separated values')
516
- .argument('<roleId>')
517
- .action(function (roleId: string) {
518
- return withHandler(async () => {
519
- const { runtimeConfig } = getContext(this);
520
- const quinn = createClient(runtimeConfig);
521
- const values = asCsv(roleId);
522
- if (values.length <= 1) {
523
- print(await quinn.roles.get(roleId));
524
- return;
525
- }
526
- print(await quinn.roles.batchGet(values));
527
- });
528
- });
529
- rolesCmd.addHelpText(
530
- 'after',
531
- [
532
- '',
533
- 'Examples:',
534
- ' quinn roles list',
535
- ' quinn roles get <roleId>',
536
- ' quinn roles get <roleId1,roleId2>',
537
- ].join('\n')
538
- );
539
-
540
- const levelsCmd = program.command('levels').description('level operations');
541
- levelsCmd
542
- .command('list')
543
- .description('list levels under a role')
544
- .requiredOption('--role-id <id>', 'role ID to scope the query')
545
- .option('--limit <n>', 'page size')
546
- .option('--page-token <token>', 'pagination token from previous page')
547
- .action(function (opts: { roleId: string; limit?: string; pageToken?: string }) {
548
- return withHandler(async () => {
549
- const { runtimeConfig } = getContext(this);
550
- const quinn = createClient(runtimeConfig);
551
- print(
552
- await quinn.levels.list({
553
- roleId: opts.roleId,
554
- limit: opts.limit ? Number(opts.limit) : undefined,
555
- token: opts.pageToken,
556
- })
557
- );
558
- });
559
- });
560
-
561
- levelsCmd
562
- .command('get')
563
- .description('get one level by ID, or many levels by comma-separated values')
564
- .argument('<levelId>')
565
- .action(function (levelId: string) {
566
- return withHandler(async () => {
567
- const { runtimeConfig } = getContext(this);
568
- const quinn = createClient(runtimeConfig);
569
- const values = asCsv(levelId);
570
- if (values.length <= 1) {
571
- print(await quinn.levels.get(levelId));
572
- return;
573
- }
574
- print(await quinn.levels.batchGet(values));
575
- });
576
- });
577
- levelsCmd.addHelpText(
578
- 'after',
579
- [
580
- '',
581
- 'Examples:',
582
- ' quinn levels list --role-id <roleId>',
583
- ' quinn levels get <levelId>',
584
- ' quinn levels get <levelId1,levelId2>',
585
- ].join('\n')
586
- );
587
-
588
- const compsCmd = program.command('competencies').description('competency operations');
589
- compsCmd
590
- .command('list')
591
- .description('list competencies scoped by role + level')
592
- .requiredOption('--role-id <id>', 'role ID')
593
- .requiredOption('--level-id <id>', 'level ID')
594
- .option('--search <text>', 'optional keyword filter')
595
- .option('--limit <n>', 'page size')
596
- .option('--page-token <token>', 'pagination token from previous page')
597
- .action(function (opts: { roleId: string; levelId: string; search?: string; limit?: string; pageToken?: string }) {
598
- return withHandler(async () => {
599
- const { runtimeConfig } = getContext(this);
600
- const quinn = createClient(runtimeConfig);
601
- print(
602
- await quinn.competencies.list({
603
- roleId: opts.roleId,
604
- levelId: opts.levelId,
605
- search: opts.search,
606
- limit: opts.limit ? Number(opts.limit) : undefined,
607
- token: opts.pageToken,
608
- })
609
- );
610
- });
611
- });
612
-
613
- compsCmd
614
- .command('get')
615
- .description('get one competency by ID, or many competencies by comma-separated values')
616
- .argument('<competencyId>')
617
- .action(function (competencyId: string) {
618
- return withHandler(async () => {
619
- const { runtimeConfig } = getContext(this);
620
- const quinn = createClient(runtimeConfig);
621
- const values = asCsv(competencyId);
622
- if (values.length <= 1) {
623
- print(await quinn.competencies.get(competencyId));
624
- return;
625
- }
626
- print(await quinn.competencies.batchGet(values));
627
- });
628
- });
629
-
630
- compsCmd
631
- .command('courses')
632
- .description('list courses under a competency')
633
- .argument('<competencyId>')
634
- .action(function (competencyId: string) {
635
- return withHandler(async () => {
636
- const { runtimeConfig } = getContext(this);
637
- const quinn = createClient(runtimeConfig);
638
- print(await quinn.competencies.listCourses(competencyId));
639
- });
640
- });
641
- compsCmd.addHelpText(
642
- 'after',
643
- [
644
- '',
645
- 'Examples:',
646
- ' quinn competencies list --role-id <roleId> --level-id <levelId>',
647
- ' quinn competencies get <competencyId>',
648
- ' quinn competencies get <id1,id2>',
649
- ' quinn competencies courses <competencyId>',
650
- ].join('\n')
651
- );
652
-
653
- const endorseCmd = program.command('endorsements').description('endorsement operations');
654
- endorseCmd
655
- .command('get')
656
- .description('get one endorsement by ID')
657
- .argument('<endorsementId>')
658
- .action(function (endorsementId: string) {
659
- return withHandler(async () => {
660
- const { runtimeConfig } = getContext(this);
661
- const quinn = createClient(runtimeConfig);
662
- print(await quinn.endorsements.get(endorsementId));
663
- });
664
- });
665
-
666
- endorseCmd
667
- .command('find')
668
- .description('find one endorsement by user + competency')
669
- .requiredOption('--uid <uid>', 'user ID')
670
- .requiredOption('--competency-id <id>', 'competency ID')
671
- .action(function (opts: { uid: string; competencyId: string }) {
672
- return withHandler(async () => {
673
- const { runtimeConfig } = getContext(this);
674
- const quinn = createClient(runtimeConfig);
675
- print(await quinn.endorsements.find(opts.uid, opts.competencyId));
676
- });
677
- });
678
-
679
- endorseCmd
680
- .command('list')
681
- .description('list endorsements by user IDs and competency IDs')
682
- .requiredOption('--uids <uids>', 'comma-separated user IDs')
683
- .requiredOption('--competency-ids <ids>', 'comma-separated competency IDs')
684
- .action(function (opts: { uids: string; competencyIds: string }) {
685
- return withHandler(async () => {
686
- const { runtimeConfig } = getContext(this);
687
- const quinn = createClient(runtimeConfig);
688
- print(
689
- await quinn.endorsements.list({
690
- uids: asCsv(opts.uids),
691
- competencyIds: asCsv(opts.competencyIds),
692
- })
693
- );
329
+ const item = await quinn.organizations.current();
330
+ print({ ok: true, item });
694
331
  });
695
332
  });
696
- endorseCmd.addHelpText(
697
- 'after',
698
- [
699
- '',
700
- 'Examples:',
701
- ' quinn endorsements get <endorsementId>',
702
- ' quinn endorsements find --uid <uid> --competency-id <competencyId>',
703
- ' quinn endorsements list --uids <u1,u2> --competency-ids <c1,c2>',
704
- ].join('\n')
705
- );
706
333
 
707
334
  program.parseAsync(process.argv).catch((error: unknown) => {
708
335
  const message = error instanceof Error ? error.message : String(error);