@totoday/quinn-cli 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ # @totoday/quinn-cli
2
+
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 629fba0: Unify organization query semantics around `organizations current`:
8
+ - remove `organizations details` command from CLI
9
+ - make `organizations current` return organization details with aggregate stats
10
+ - align SDK and skill documentation with the simplified flow
11
+ - @totoday/quinn-sdk@0.1.2
12
+
13
+ ## 0.1.1
14
+
15
+ ### Patch Changes
16
+
17
+ - 2d4beb4: Improve `quinn login` password handling:
18
+ - default to interactive hidden password prompt
19
+ - add `--password-stdin` for script-friendly secure input
20
+ - deprecate plain-text `--password` with warning
21
+ - @totoday/quinn-sdk@0.1.1
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @totoday/quinn-cli
2
+
3
+ CLI for Quinn organization/member/role/competency queries.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g @totoday/quinn-cli
9
+ quinn --help
10
+ ```
11
+
12
+ One-off without global install:
13
+
14
+ ```bash
15
+ npx @totoday/quinn-cli --help
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ Login (recommended: hidden password prompt):
21
+
22
+ ```bash
23
+ quinn login --email <email>
24
+ ```
25
+
26
+ Login via stdin (scripts/password managers):
27
+
28
+ ```bash
29
+ echo "<password>" | quinn login --email <email> --password-stdin
30
+ ```
31
+
32
+ Check organization:
33
+
34
+ ```bash
35
+ quinn organizations current
36
+ ```
37
+
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>
54
+ ```
55
+
56
+ ## Config
57
+
58
+ CLI reads config from:
59
+
60
+ - `~/.config/quinn/config.json` (default)
61
+ - `QUINN_CONFIG_PATH` (override)
62
+
63
+ Runtime override order:
64
+
65
+ 1. command flags
66
+ 2. env vars (`QUINN_API_URL`, `QUINN_API_TOKEN`, `QUINN_ORG_ID`)
67
+ 3. config file
68
+
69
+ If `apiUrl` is missing, default is `https://api.lunapark.com`.
package/dist/index.js CHANGED
@@ -64,16 +64,13 @@ function writeConfig(configPath, config) {
64
64
  }
65
65
  function resolveRuntimeConfig(opts, fileConfig) {
66
66
  return {
67
- apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl,
67
+ apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl || quinn_sdk_1.DEFAULT_QUINN_API_URL,
68
68
  token: opts.apiToken || opts.token || process.env.QUINN_API_TOKEN || fileConfig.token,
69
69
  orgId: opts.orgId || process.env.QUINN_ORG_ID || fileConfig.orgId,
70
70
  };
71
71
  }
72
72
  function createClient(config) {
73
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
74
  if (!token) {
78
75
  throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
79
76
  }
@@ -82,6 +79,60 @@ function createClient(config) {
82
79
  }
83
80
  return new quinn_sdk_1.Quinn({ apiUrl, token, orgId });
84
81
  }
82
+ function readPasswordFromStdin() {
83
+ const value = node_fs_1.default.readFileSync(0, 'utf8').trim();
84
+ if (!value) {
85
+ throw new Error('empty password from stdin');
86
+ }
87
+ return value;
88
+ }
89
+ async function promptPasswordHidden(prompt = 'Password: ') {
90
+ if (!process.stdin.isTTY) {
91
+ throw new Error('interactive password prompt requires TTY, use --password-stdin');
92
+ }
93
+ return await new Promise((resolve, reject) => {
94
+ const stdin = process.stdin;
95
+ const stdout = process.stdout;
96
+ let password = '';
97
+ const cleanup = () => {
98
+ stdin.off('data', onData);
99
+ if (stdin.isTTY) {
100
+ stdin.setRawMode(false);
101
+ }
102
+ stdin.pause();
103
+ };
104
+ const onData = (chunk) => {
105
+ const text = chunk.toString('utf8');
106
+ if (text === '\u0003') {
107
+ cleanup();
108
+ reject(new Error('login cancelled'));
109
+ return;
110
+ }
111
+ if (text === '\r' || text === '\n') {
112
+ cleanup();
113
+ stdout.write('\n');
114
+ const value = password.trim();
115
+ if (!value) {
116
+ reject(new Error('password is required'));
117
+ return;
118
+ }
119
+ resolve(value);
120
+ return;
121
+ }
122
+ if (text === '\u007f' || text === '\b' || text === '\x08') {
123
+ if (password.length > 0) {
124
+ password = password.slice(0, -1);
125
+ }
126
+ return;
127
+ }
128
+ password += text;
129
+ };
130
+ stdout.write(prompt);
131
+ stdin.resume();
132
+ stdin.setRawMode(true);
133
+ stdin.on('data', onData);
134
+ });
135
+ }
85
136
  function getContext(command) {
86
137
  const global = command.optsWithGlobals();
87
138
  const configPath = getConfigPath(global);
@@ -132,8 +183,10 @@ program
132
183
  .addHelpText('after', [
133
184
  '',
134
185
  'Examples:',
186
+ ' quinn login --email <email>',
187
+ ' echo "<password>" | quinn login --email <email> --password-stdin',
135
188
  ' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
136
- ' quinn organizations details',
189
+ ' quinn organizations current',
137
190
  ' quinn members find alice',
138
191
  ' quinn members get user-1,user-2,user@example.com',
139
192
  ].join('\n'))
@@ -202,34 +255,76 @@ configCmd.addHelpText('after', [
202
255
  ' quinn config get',
203
256
  ' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
204
257
  ].join('\n'));
258
+ program
259
+ .command('login')
260
+ .description('login and save token (+orgId) into config (password input is hidden by default)')
261
+ .requiredOption('--email <email>')
262
+ .option('--password <password>', 'DEPRECATED: avoid plain-text password in command history/process list')
263
+ .option('--password-stdin', 'read password from stdin')
264
+ .option('--org-id <id>', 'override org id if login response does not include one')
265
+ .action(function (opts) {
266
+ return withHandler(async () => {
267
+ const { configPath, fileConfig, runtimeConfig } = getContext(this);
268
+ if (opts.password && opts.passwordStdin) {
269
+ throw new Error('cannot use --password and --password-stdin together');
270
+ }
271
+ let password = '';
272
+ if (opts.passwordStdin) {
273
+ password = readPasswordFromStdin();
274
+ }
275
+ else if (opts.password) {
276
+ process.stderr.write('Warning: --password is deprecated and insecure. Use interactive prompt or --password-stdin.\n');
277
+ password = opts.password;
278
+ }
279
+ else {
280
+ password = await promptPasswordHidden();
281
+ }
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
+ });
290
+ const resolvedOrgId = opts.orgId || login.orgId || fileConfig.orgId;
291
+ if (!resolvedOrgId) {
292
+ throw new Error('orgId not found in login response, please pass --org-id');
293
+ }
294
+ const next = {
295
+ apiUrl: runtimeConfig.apiUrl || quinn_sdk_1.DEFAULT_QUINN_API_URL,
296
+ token: login.token,
297
+ orgId: resolvedOrgId,
298
+ };
299
+ writeConfig(configPath, next);
300
+ print({
301
+ ok: true,
302
+ configPath,
303
+ config: {
304
+ apiUrl: next.apiUrl,
305
+ token: maskToken(next.token),
306
+ orgId: next.orgId,
307
+ },
308
+ });
309
+ });
310
+ });
205
311
  const orgCmd = program
206
312
  .command('organizations')
207
313
  .description('organization metadata and high-level stats');
208
314
  orgCmd
209
315
  .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')
316
+ .description('get current organization details with aggregate stats')
221
317
  .action(function () {
222
318
  return withHandler(async () => {
223
319
  const { runtimeConfig } = getContext(this);
224
320
  const quinn = createClient(runtimeConfig);
225
- print(await quinn.organizations.getDetails());
321
+ print(await quinn.organizations.current());
226
322
  });
227
323
  });
228
324
  orgCmd.addHelpText('after', [
229
325
  '',
230
326
  'Examples:',
231
327
  ' quinn organizations current',
232
- ' quinn organizations details',
233
328
  ].join('\n'));
234
329
  const membersCmd = program.command('members').description('member operations');
235
330
  membersCmd
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@totoday/quinn-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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.2"
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 { Quinn, Privilege } from '@totoday/quinn-sdk';
7
+ import { DEFAULT_QUINN_API_URL, Quinn, QuinnAuth, Privilege } from '@totoday/quinn-sdk';
8
8
 
9
9
  type QuinnCliConfig = {
10
10
  apiUrl?: string;
@@ -83,7 +83,7 @@ function writeConfig(configPath: string, config: QuinnCliConfig): void {
83
83
 
84
84
  function resolveRuntimeConfig(opts: GlobalOptions, fileConfig: QuinnCliConfig): QuinnCliConfig {
85
85
  return {
86
- apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl,
86
+ apiUrl: opts.apiUrl || process.env.QUINN_API_URL || fileConfig.apiUrl || DEFAULT_QUINN_API_URL,
87
87
  token: opts.apiToken || opts.token || process.env.QUINN_API_TOKEN || fileConfig.token,
88
88
  orgId: opts.orgId || process.env.QUINN_ORG_ID || fileConfig.orgId,
89
89
  };
@@ -91,9 +91,6 @@ function resolveRuntimeConfig(opts: GlobalOptions, fileConfig: QuinnCliConfig):
91
91
 
92
92
  function createClient(config: QuinnCliConfig): Quinn {
93
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
94
  if (!token) {
98
95
  throw new Error('missing token: set --api-token/--token or QUINN_API_TOKEN or config');
99
96
  }
@@ -103,6 +100,69 @@ function createClient(config: QuinnCliConfig): Quinn {
103
100
  return new Quinn({ apiUrl, token, orgId });
104
101
  }
105
102
 
103
+ function readPasswordFromStdin(): string {
104
+ const value = fs.readFileSync(0, 'utf8').trim();
105
+ if (!value) {
106
+ throw new Error('empty password from stdin');
107
+ }
108
+ return value;
109
+ }
110
+
111
+ async function promptPasswordHidden(prompt = 'Password: '): Promise<string> {
112
+ if (!process.stdin.isTTY) {
113
+ throw new Error('interactive password prompt requires TTY, use --password-stdin');
114
+ }
115
+ return await new Promise<string>((resolve, reject) => {
116
+ const stdin = process.stdin;
117
+ const stdout = process.stdout;
118
+ let password = '';
119
+
120
+ const cleanup = () => {
121
+ stdin.off('data', onData);
122
+ if (stdin.isTTY) {
123
+ stdin.setRawMode(false);
124
+ }
125
+ stdin.pause();
126
+ };
127
+
128
+ const onData = (chunk: Buffer) => {
129
+ const text = chunk.toString('utf8');
130
+
131
+ if (text === '\u0003') {
132
+ cleanup();
133
+ reject(new Error('login cancelled'));
134
+ return;
135
+ }
136
+
137
+ if (text === '\r' || text === '\n') {
138
+ cleanup();
139
+ stdout.write('\n');
140
+ const value = password.trim();
141
+ if (!value) {
142
+ reject(new Error('password is required'));
143
+ return;
144
+ }
145
+ resolve(value);
146
+ return;
147
+ }
148
+
149
+ if (text === '\u007f' || text === '\b' || text === '\x08') {
150
+ if (password.length > 0) {
151
+ password = password.slice(0, -1);
152
+ }
153
+ return;
154
+ }
155
+
156
+ password += text;
157
+ };
158
+
159
+ stdout.write(prompt);
160
+ stdin.resume();
161
+ stdin.setRawMode(true);
162
+ stdin.on('data', onData);
163
+ });
164
+ }
165
+
106
166
  function getContext(command: Command): {
107
167
  global: GlobalOptions;
108
168
  configPath: string;
@@ -165,8 +225,10 @@ program
165
225
  [
166
226
  '',
167
227
  'Examples:',
228
+ ' quinn login --email <email>',
229
+ ' echo "<password>" | quinn login --email <email> --password-stdin',
168
230
  ' quinn config set --api-url http://localhost:8090 --api-token <token> --org-id <orgId>',
169
- ' quinn organizations details',
231
+ ' quinn organizations current',
170
232
  ' quinn members find alice',
171
233
  ' quinn members get user-1,user-2,user@example.com',
172
234
  ].join('\n')
@@ -243,28 +305,71 @@ configCmd.addHelpText(
243
305
  ].join('\n')
244
306
  );
245
307
 
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 () {
308
+ program
309
+ .command('login')
310
+ .description('login and save token (+orgId) into config (password input is hidden by default)')
311
+ .requiredOption('--email <email>')
312
+ .option('--password <password>', 'DEPRECATED: avoid plain-text password in command history/process list')
313
+ .option('--password-stdin', 'read password from stdin')
314
+ .option('--org-id <id>', 'override org id if login response does not include one')
315
+ .action(function (opts: { email: string; password?: string; passwordStdin?: boolean; orgId?: string }) {
253
316
  return withHandler(async () => {
254
- const { runtimeConfig } = getContext(this);
255
- const quinn = createClient(runtimeConfig);
256
- print(await quinn.organizations.get());
317
+ const { configPath, fileConfig, runtimeConfig } = getContext(this);
318
+ if (opts.password && opts.passwordStdin) {
319
+ throw new Error('cannot use --password and --password-stdin together');
320
+ }
321
+ let password = '';
322
+ if (opts.passwordStdin) {
323
+ password = readPasswordFromStdin();
324
+ } else if (opts.password) {
325
+ process.stderr.write(
326
+ 'Warning: --password is deprecated and insecure. Use interactive prompt or --password-stdin.\n'
327
+ );
328
+ password = opts.password;
329
+ } else {
330
+ password = await promptPasswordHidden();
331
+ }
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
+ });
340
+ const resolvedOrgId = opts.orgId || login.orgId || fileConfig.orgId;
341
+ if (!resolvedOrgId) {
342
+ throw new Error('orgId not found in login response, please pass --org-id');
343
+ }
344
+ const next: QuinnCliConfig = {
345
+ apiUrl: runtimeConfig.apiUrl || DEFAULT_QUINN_API_URL,
346
+ token: login.token,
347
+ orgId: resolvedOrgId,
348
+ };
349
+ writeConfig(configPath, next);
350
+ print({
351
+ ok: true,
352
+ configPath,
353
+ config: {
354
+ apiUrl: next.apiUrl,
355
+ token: maskToken(next.token),
356
+ orgId: next.orgId,
357
+ },
358
+ });
257
359
  });
258
360
  });
259
361
 
362
+ const orgCmd = program
363
+ .command('organizations')
364
+ .description('organization metadata and high-level stats');
260
365
  orgCmd
261
- .command('details')
262
- .description('get organization details with aggregate stats')
366
+ .command('current')
367
+ .description('get current organization details with aggregate stats')
263
368
  .action(function () {
264
369
  return withHandler(async () => {
265
370
  const { runtimeConfig } = getContext(this);
266
371
  const quinn = createClient(runtimeConfig);
267
- print(await quinn.organizations.getDetails());
372
+ print(await quinn.organizations.current());
268
373
  });
269
374
  });
270
375
  orgCmd.addHelpText(
@@ -273,7 +378,6 @@ orgCmd.addHelpText(
273
378
  '',
274
379
  'Examples:',
275
380
  ' quinn organizations current',
276
- ' quinn organizations details',
277
381
  ].join('\n')
278
382
  );
279
383