@totoday/quinn-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # @totoday/quinn-cli
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2d4beb4: Improve `quinn login` password handling:
8
+ - default to interactive hidden password prompt
9
+ - add `--password-stdin` for script-friendly secure input
10
+ - deprecate plain-text `--password` with warning
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,6 +183,8 @@ 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
189
  ' quinn organizations details',
137
190
  ' quinn members find alice',
@@ -202,6 +255,59 @@ 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');
@@ -212,7 +318,7 @@ orgCmd
212
318
  return withHandler(async () => {
213
319
  const { runtimeConfig } = getContext(this);
214
320
  const quinn = createClient(runtimeConfig);
215
- print(await quinn.organizations.get());
321
+ print(await quinn.organizations.current());
216
322
  });
217
323
  });
218
324
  orgCmd
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totoday/quinn-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "bin": {
5
5
  "quinn": "dist/index.js"
6
6
  },
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,6 +225,8 @@ 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
231
  ' quinn organizations details',
170
232
  ' quinn members find alice',
@@ -243,6 +305,60 @@ configCmd.addHelpText(
243
305
  ].join('\n')
244
306
  );
245
307
 
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 }) {
316
+ return withHandler(async () => {
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
+ });
359
+ });
360
+ });
361
+
246
362
  const orgCmd = program
247
363
  .command('organizations')
248
364
  .description('organization metadata and high-level stats');
@@ -253,7 +369,7 @@ orgCmd
253
369
  return withHandler(async () => {
254
370
  const { runtimeConfig } = getContext(this);
255
371
  const quinn = createClient(runtimeConfig);
256
- print(await quinn.organizations.get());
372
+ print(await quinn.organizations.current());
257
373
  });
258
374
  });
259
375