@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 +10 -0
- package/dist/index.js +111 -5
- package/package.json +1 -1
- package/src/index.ts +122 -6
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.
|
|
321
|
+
print(await quinn.organizations.current());
|
|
216
322
|
});
|
|
217
323
|
});
|
|
218
324
|
orgCmd
|
package/package.json
CHANGED
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.
|
|
372
|
+
print(await quinn.organizations.current());
|
|
257
373
|
});
|
|
258
374
|
});
|
|
259
375
|
|