tirtc-devtools-cli 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # TiRTC DevTool CLI
1
+ # TiRTC DevTools CLI
2
2
 
3
- `devtools/cli/` 承接 `TiRTC DevTool CLI`。它是当前唯一用户界面的产品门面 (command client),为用户提供了一套操作 `Matrix Host` 控制面的终端工具。
3
+ `devtools/cli/` 承接 `TiRTC DevTools CLI`。它是当前唯一用户界面的产品门面 (command client),为用户提供了一套操作 `Matrix Host` 控制面的终端工具。
4
4
 
5
5
  ## 使用说明 (C-Lite)
6
6
 
package/USAGE.md CHANGED
@@ -1,4 +1,4 @@
1
- # TiRTC DevTool CLI(用户手册)
1
+ # TiRTC DevTools CLI(用户手册)
2
2
 
3
3
  `tirtc-devtools-cli` 是基于 `app-server/host` 的命令行门面,用来控制会话、服务、连接、流、命令通道与调试工具。
4
4
 
@@ -118,27 +118,46 @@ node devtools/cli/bin/tirtc-devtools-cli.js --help
118
118
 
119
119
  用途:直接签发 token,并把调试所需的核心信息一次性输出到控制台,同时在本地生成二维码 PNG。
120
120
 
121
- - `tirtc-devtools-cli token issue <accessId> <secretKey> <localId> <peerId>`:签发 token。
121
+ - `tirtc-devtools-cli token issue <peerId>`:签发 token;默认从环境变量读取凭据,并使用 `peerId` 作为默认 `localId`。
122
122
 
123
123
  可选参数:
124
124
 
125
+ - `--access-id <accessId>`:显式传 access id;不传时读取 `TIRTC_CONN_ACCESS_ID`。
126
+ - `--secret-key <secretKey>`:显式传 secret key;不传时读取 `TIRTC_CONN_SECRET_KEY`。
127
+ - `--local-id <localId>`:显式传 local id;不传时默认使用 `peerId`。
125
128
  - `--openapi-entry <url>`:覆盖默认 openapi entry。
126
129
  - `--service-entry <entry>`:附加到组合 payload,便于扫码后直接连到目标服务。
127
130
  - `--user-ttl-seconds <seconds>`:覆盖 user token ttl。
128
131
  - `--channel-ttl-seconds <seconds>`:覆盖 channel token ttl。
132
+ - `--qr-error-correction-level <level>`:设置二维码纠错级别,可选 `L/M/Q/H`,默认 `M`。
133
+ - `--ascii-max-columns <columns>`:显式限制 ASCII 二维码最大宽度;不传时优先读取当前终端宽度或 `COLUMNS`。
129
134
 
130
135
  默认输出四部分:
131
136
 
132
137
  - `Issued Token Summary`:面向人类阅读的摘要,快速确认 local/peer/entry/ttl。
133
138
  - `Token`:单独罗列 token 字符串,便于复制粘贴。
134
139
  - `Payload JSON`:组合 JSON,对方或 Agent 可直接复制使用。
140
+ - `QR Code ASCII`:非 `--json` 模式下尽量输出 ASCII 二维码;如果终端宽度不足,会提示改看 PNG。
135
141
  - `QR Code PNG`:基于组合 JSON 生成的本地 PNG 文件绝对路径。适合人工打开图片后扫码。
136
142
 
137
143
  说明:
138
144
 
139
145
  - 组合 JSON 的 `type` 固定为 `tirtc-connect-token`。
140
146
  - PNG 二维码承载的是组合 JSON,不再暴露 `secret_key`。
141
- - `--json` 模式下会输出 `payload`、`payloadJson`、`token` 和 `qrCodePngPath`。
147
+ - CLI 不会在最终 JSON、控制台输出或二维码内容里打印 `access_id` 和 `secret_key`。
148
+ - `--json` 模式下只输出 `payload`、`payloadJson`、`token` 和 `qrCodePngPath`,不输出 ASCII 二维码。
149
+ - 如果觉得 ASCII 二维码太密,可把纠错级别从默认 `M` 调低到 `L`;如果希望更耐遮挡,可调高到 `Q` 或 `H`,但二维码会更密。
150
+
151
+ 推荐默认做法:
152
+
153
+ ```bash
154
+ export TIRTC_CONN_ACCESS_ID="<ACCESS_ID>"
155
+ export TIRTC_CONN_SECRET_KEY="<SECRET_KEY>"
156
+ tirtc-devtools-cli --json token issue "<PEER_ID>"
157
+ ```
158
+
159
+ 只有在你明确需要覆盖环境变量时,才额外传 `--access-id` 和 `--secret-key`。
160
+ 只有在你明确需要不同的本地用户标识时,才额外传 `--local-id`。
142
161
 
143
162
  ### Stream
144
163
 
@@ -31,9 +31,12 @@ function printQuickstartGuide() {
31
31
  ' - 持 token + peer_id 连接服务端;连接成功后请求 audio_stream_id/video_stream_id',
32
32
  '',
33
33
  '额外:如果你只想本地直接签发并拿到可扫码 payload,可执行:',
34
- ' node devtools/cli/bin/tirtc-devtools-cli.js token issue "<ACCESS_ID>" "<SECRET_KEY>" "<LOCAL_ID>" "<PEER_ID>" \\',
34
+ ' export TIRTC_CONN_ACCESS_ID="<ACCESS_ID>"',
35
+ ' export TIRTC_CONN_SECRET_KEY="<SECRET_KEY>"',
36
+ ' node devtools/cli/bin/tirtc-devtools-cli.js token issue "<LOCAL_ID>" "<PEER_ID>" \\',
35
37
  ' --service-entry "<SERVICE_ENTRY>"',
36
38
  ' # CLI 会直接打印摘要、payload JSON、token,并生成本地二维码 PNG 路径',
39
+ ' # 如需临时覆盖,也可显式传 --access-id / --secret-key',
37
40
  '',
38
41
  '步骤 5:服务端观测',
39
42
  ' node devtools/cli/bin/tirtc-devtools-cli.js --session <SESSION_ID> events tail',
@@ -9,9 +9,11 @@ const session_manager_1 = require("./session_manager");
9
9
  const media_assets_1 = require("./media_assets");
10
10
  const transport_1 = require("./transport");
11
11
  const token_tool_1 = require("./token_tool");
12
- const CLI_VERSION = '0.0.3';
12
+ const CLI_VERSION = '0.0.5';
13
13
  const HOST_VERSION = '1.0.0';
14
14
  const PROTOCOL_VERSION = '1.0.0';
15
+ const kTokenIssueAccessIdEnvVar = 'TIRTC_CONN_ACCESS_ID';
16
+ const kTokenIssueSecretKeyEnvVar = 'TIRTC_CONN_SECRET_KEY';
15
17
  if (process.argv.includes('--version') || process.argv.includes('-V')) {
16
18
  console.log('CLI Version: ' + CLI_VERSION);
17
19
  console.log('Host Version: ' + HOST_VERSION);
@@ -325,6 +327,60 @@ async function runTokenIssue(params, options) {
325
327
  return exitCode;
326
328
  }
327
329
  }
330
+ async function runTokenIssueFromCli(peerId, commandOptions, options) {
331
+ const parsePositiveInt = (name, raw) => {
332
+ if (raw === undefined) {
333
+ return undefined;
334
+ }
335
+ const parsed = Number.parseInt(raw, 10);
336
+ if (!Number.isInteger(parsed) || parsed <= 0) {
337
+ throw new Error(name + ' must be a positive integer');
338
+ }
339
+ return parsed;
340
+ };
341
+ const parseQrErrorCorrectionLevel = (raw) => {
342
+ if (raw === undefined) {
343
+ return undefined;
344
+ }
345
+ const normalized = raw.trim().toUpperCase();
346
+ if (normalized !== 'L' && normalized !== 'M' && normalized !== 'Q' && normalized !== 'H') {
347
+ throw new Error('qr-error-correction-level must be one of: L, M, Q, H');
348
+ }
349
+ return normalized;
350
+ };
351
+ try {
352
+ const accessId = resolveTokenIssueCredential(commandOptions.accessId, kTokenIssueAccessIdEnvVar, '--access-id');
353
+ const secretKey = resolveTokenIssueCredential(commandOptions.secretKey, kTokenIssueSecretKeyEnvVar, '--secret-key');
354
+ return await runTokenIssue({
355
+ accessId,
356
+ secretKey,
357
+ localId: commandOptions.localId?.trim() || peerId,
358
+ peerId,
359
+ openapiEntry: commandOptions.openapiEntry,
360
+ serviceEntry: commandOptions.serviceEntry,
361
+ userTtlSeconds: parsePositiveInt('user-ttl-seconds', commandOptions.userTtlSeconds),
362
+ channelTtlSeconds: parsePositiveInt('channel-ttl-seconds', commandOptions.channelTtlSeconds),
363
+ qrErrorCorrectionLevel: parseQrErrorCorrectionLevel(commandOptions.qrErrorCorrectionLevel),
364
+ asciiMaxColumns: parsePositiveInt('ascii-max-columns', commandOptions.asciiMaxColumns),
365
+ }, options);
366
+ }
367
+ catch (error) {
368
+ const normalized = normalizeError(error);
369
+ const reasonCode = normalized.reasonCode;
370
+ const exitCode = facade_1.ErrorReasonCodeMapping[reasonCode] ?? 1;
371
+ if (options.json) {
372
+ console.log(JSON.stringify({
373
+ code: exitCode,
374
+ message: normalized.message,
375
+ data: normalized.data,
376
+ }));
377
+ }
378
+ else {
379
+ console.error('Error (' + reasonCode + '): ' + normalized.message);
380
+ }
381
+ return exitCode;
382
+ }
383
+ }
328
384
  function runAndExit(promise) {
329
385
  promise.then((code) => {
330
386
  process.exit(code);
@@ -339,6 +395,18 @@ function mustParseJsonPayload(input) {
339
395
  throw new Error('payloadJson must be valid JSON text');
340
396
  }
341
397
  }
398
+ function resolveTokenIssueCredential(explicitValue, envVarName, optionName) {
399
+ const normalizedExplicit = explicitValue?.trim();
400
+ if (normalizedExplicit) {
401
+ return normalizedExplicit;
402
+ }
403
+ const normalizedEnv = process.env[envVarName]?.trim();
404
+ if (normalizedEnv) {
405
+ return normalizedEnv;
406
+ }
407
+ throw new Error('missing credential: set environment variable ' + envVarName +
408
+ ' or pass ' + optionName + ' explicitly');
409
+ }
342
410
  program.name('tirtc-devtools-cli')
343
411
  .description('TiRTC DevTools CLI')
344
412
  .option('--config <path>', '配置文件路径(TOML)')
@@ -545,33 +613,19 @@ connection.command('disconnect').description('断开当前连接').action(() =>
545
613
  runAndExit(runCommand('connection disconnect', {}, getCliOptions()));
546
614
  });
547
615
  const token = program.command('token').description('Token 工具:签发 token,并输出可直接使用的 JSON 与本地二维码 PNG');
548
- token.command('issue <access_id> <secret_key> <local_id> <peer_id>')
549
- .description('使用 access/secret/local/peer 签发 token;输出摘要、payload JSON、token 和二维码 PNG 路径')
616
+ token.command('issue <peer_id>')
617
+ .description('默认从环境变量读取 access/secret,并以 peer_id 作为默认 local_id 来签发 token')
618
+ .option('--access-id <accessId>', '显式 access id;不传时读取 ' + kTokenIssueAccessIdEnvVar)
619
+ .option('--secret-key <secretKey>', '显式 secret key;不传时读取 ' + kTokenIssueSecretKeyEnvVar)
620
+ .option('--local-id <localId>', '显式 local id;不传时默认使用 peer_id')
550
621
  .option('--openapi-entry <url>', '可选 openapi entry;留空时走 runtime 默认值')
551
622
  .option('--service-entry <entry>', '可选 service entry;用于组合连接 payload 与二维码 PNG')
552
623
  .option('--user-ttl-seconds <seconds>', '可选 user token ttl(秒)')
553
624
  .option('--channel-ttl-seconds <seconds>', '可选 channel token ttl(秒)')
554
- .action((access_id, secret_key, local_id, peer_id, commandOptions) => {
555
- const parsePositiveInt = (name, raw) => {
556
- if (raw === undefined) {
557
- return undefined;
558
- }
559
- const parsed = Number.parseInt(raw, 10);
560
- if (!Number.isInteger(parsed) || parsed <= 0) {
561
- throw new Error(name + ' must be a positive integer');
562
- }
563
- return parsed;
564
- };
565
- runAndExit(runTokenIssue({
566
- accessId: access_id,
567
- secretKey: secret_key,
568
- localId: local_id,
569
- peerId: peer_id,
570
- openapiEntry: commandOptions.openapiEntry,
571
- serviceEntry: commandOptions.serviceEntry,
572
- userTtlSeconds: parsePositiveInt('user-ttl-seconds', commandOptions.userTtlSeconds),
573
- channelTtlSeconds: parsePositiveInt('channel-ttl-seconds', commandOptions.channelTtlSeconds),
574
- }, getCliOptions()));
625
+ .option('--qr-error-correction-level <level>', '二维码纠错级别:L/M/Q/H;默认 M')
626
+ .option('--ascii-max-columns <columns>', 'ASCII 二维码最大宽度;不传时优先读取当前终端宽度或 COLUMNS')
627
+ .action((peer_id, commandOptions) => {
628
+ runAndExit(runTokenIssueFromCli(peer_id, commandOptions, getCliOptions()));
575
629
  });
576
630
  const stream = program.command('stream').description('流控制:发送/接收/请求策略');
577
631
  stream.command('list').description('查看流快照列表').action(() => {
@@ -7,6 +7,8 @@ export type TokenIssueInput = {
7
7
  serviceEntry?: string;
8
8
  userTtlSeconds?: number;
9
9
  channelTtlSeconds?: number;
10
+ qrErrorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
11
+ asciiMaxColumns?: number;
10
12
  };
11
13
  export type IssuedTokenPayload = {
12
14
  version: 1;
@@ -26,10 +28,11 @@ export type TokenIssueOutput = {
26
28
  token: string;
27
29
  qrCodePngPath: string;
28
30
  qrCodeAscii: string;
31
+ qrCodeAsciiIncluded: boolean;
29
32
  };
30
33
  export declare function issueToken(input: TokenIssueInput): Promise<string>;
31
34
  export declare function buildIssuedTokenPayload(input: TokenIssueInput, token: string): IssuedTokenPayload;
32
- export declare function writePngQrcode(payloadJson: string, outputPath: string): Promise<string>;
33
- export declare function buildAsciiQrcode(payloadJson: string): Promise<string>;
35
+ export declare function writePngQrcode(payloadJson: string, outputPath: string, errorCorrectionLevel: 'L' | 'M' | 'Q' | 'H'): Promise<string>;
36
+ export declare function buildAsciiQrcode(payloadJson: string, terminalColumns?: number, errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H'): Promise<string>;
34
37
  export declare function issueTokenWithQrcode(input: TokenIssueInput): Promise<TokenIssueOutput>;
35
38
  export declare function formatTokenIssueConsoleOutput(output: TokenIssueOutput): string;
@@ -17,8 +17,6 @@ const embedded_paths_1 = require("./embedded_paths");
17
17
  const kDefaultOpenapiEntry = 'http://api-test-tirtc.tange365.com';
18
18
  const kDefaultUserTtlSeconds = 86400;
19
19
  const kDefaultChannelTtlSeconds = 300;
20
- const kAsciiQrBlack = '██';
21
- const kAsciiQrWhite = ' ';
22
20
  const kAsciiQrQuietZoneModules = 2;
23
21
  function pathExists(filePath) {
24
22
  return fs_1.default.existsSync(filePath);
@@ -176,6 +174,58 @@ function issueTokenViaSubprocess(addonPath, input) {
176
174
  function readQrModule(modules, row, column) {
177
175
  return Boolean(modules.data[row * modules.size + column]);
178
176
  }
177
+ function resolveTerminalColumns() {
178
+ if (typeof process.stdout.columns === 'number' && Number.isInteger(process.stdout.columns) && process.stdout.columns > 0) {
179
+ return process.stdout.columns;
180
+ }
181
+ const raw = process.env.COLUMNS;
182
+ if (!raw) {
183
+ return undefined;
184
+ }
185
+ const parsed = Number.parseInt(raw, 10);
186
+ if (!Number.isInteger(parsed) || parsed <= 0) {
187
+ return undefined;
188
+ }
189
+ return parsed;
190
+ }
191
+ function resolveAsciiMaxColumns(explicit) {
192
+ if (explicit !== undefined) {
193
+ return explicit;
194
+ }
195
+ return resolveTerminalColumns();
196
+ }
197
+ function readQrModuleOrWhite(modules, row, column) {
198
+ if (row < 0 || column < 0 || row >= modules.size || column >= modules.size) {
199
+ return false;
200
+ }
201
+ return readQrModule(modules, row, column);
202
+ }
203
+ function renderHalfBlockQr(modules) {
204
+ const lines = [];
205
+ const start = -kAsciiQrQuietZoneModules;
206
+ const end = modules.size + kAsciiQrQuietZoneModules;
207
+ for (let row = start; row < end; row += 2) {
208
+ let line = '';
209
+ for (let column = start; column < end; column += 1) {
210
+ const top = readQrModuleOrWhite(modules, row, column);
211
+ const bottom = readQrModuleOrWhite(modules, row + 1, column);
212
+ if (top && bottom) {
213
+ line += '█';
214
+ }
215
+ else if (top) {
216
+ line += '▀';
217
+ }
218
+ else if (bottom) {
219
+ line += '▄';
220
+ }
221
+ else {
222
+ line += ' ';
223
+ }
224
+ }
225
+ lines.push(line);
226
+ }
227
+ return lines.join('\n');
228
+ }
179
229
  async function issueToken(input) {
180
230
  const runtimePlatform = resolveRuntimePlatform();
181
231
  const runtimeBundleRoot = resolveRuntimeBundleRoot(runtimePlatform);
@@ -204,50 +254,45 @@ function buildIssuedTokenPayload(input, token) {
204
254
  generated_at: new Date().toISOString(),
205
255
  };
206
256
  }
207
- async function writePngQrcode(payloadJson, outputPath) {
257
+ async function writePngQrcode(payloadJson, outputPath, errorCorrectionLevel) {
208
258
  const resolvedPath = path_1.default.resolve(outputPath);
209
259
  ensureDirectory(path_1.default.dirname(resolvedPath));
210
260
  await qrcode_1.default.toFile(resolvedPath, payloadJson, {
211
- errorCorrectionLevel: 'M',
261
+ errorCorrectionLevel,
212
262
  margin: 2,
213
263
  type: 'png',
214
264
  width: 960,
215
265
  });
216
266
  return resolvedPath;
217
267
  }
218
- async function buildAsciiQrcode(payloadJson) {
219
- const qr = qrcode_1.default.create(payloadJson, { errorCorrectionLevel: 'M' });
220
- const quietZone = kAsciiQrWhite.repeat(kAsciiQrQuietZoneModules);
221
- const fullQuietLine = quietZone + kAsciiQrWhite.repeat(qr.modules.size) + quietZone;
222
- const lines = [];
223
- for (let index = 0; index < kAsciiQrQuietZoneModules; index += 1) {
224
- lines.push(fullQuietLine);
268
+ async function buildAsciiQrcode(payloadJson, terminalColumns, errorCorrectionLevel = 'M') {
269
+ const qr = qrcode_1.default.create(payloadJson, { errorCorrectionLevel });
270
+ const requiredColumns = qr.modules.size + (kAsciiQrQuietZoneModules * 2);
271
+ const availableColumns = resolveAsciiMaxColumns(terminalColumns);
272
+ if (availableColumns !== undefined && availableColumns < requiredColumns) {
273
+ return [
274
+ '(omitted: terminal width too narrow for ASCII QR)',
275
+ `required_columns: ${requiredColumns}`,
276
+ `available_columns: ${availableColumns}`,
277
+ 'open the QR Code PNG path below instead.',
278
+ ].join('\n');
225
279
  }
226
- for (let row = 0; row < qr.modules.size; row += 1) {
227
- let line = quietZone;
228
- for (let column = 0; column < qr.modules.size; column += 1) {
229
- line += readQrModule(qr.modules, row, column) ? kAsciiQrBlack : kAsciiQrWhite;
230
- }
231
- line += quietZone;
232
- lines.push(line);
233
- }
234
- for (let index = 0; index < kAsciiQrQuietZoneModules; index += 1) {
235
- lines.push(fullQuietLine);
236
- }
237
- return lines.join('\n');
280
+ return renderHalfBlockQr(qr.modules);
238
281
  }
239
282
  async function issueTokenWithQrcode(input) {
240
283
  const token = await issueToken(input);
241
284
  const payload = buildIssuedTokenPayload(input, token);
242
285
  const payloadJson = JSON.stringify(payload);
243
- const qrCodePngPath = await writePngQrcode(payloadJson, buildQrCodePngPath(payload));
244
- const qrCodeAscii = await buildAsciiQrcode(payloadJson);
286
+ const qrErrorCorrectionLevel = input.qrErrorCorrectionLevel ?? 'M';
287
+ const qrCodePngPath = await writePngQrcode(payloadJson, buildQrCodePngPath(payload), qrErrorCorrectionLevel);
288
+ const qrCodeAscii = await buildAsciiQrcode(payloadJson, input.asciiMaxColumns, qrErrorCorrectionLevel);
245
289
  return {
246
290
  payload,
247
291
  payloadJson,
248
292
  token,
249
293
  qrCodePngPath,
250
294
  qrCodeAscii,
295
+ qrCodeAsciiIncluded: !qrCodeAscii.startsWith('(omitted:'),
251
296
  };
252
297
  }
253
298
  function formatTokenIssueConsoleOutput(output) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tirtc-devtools-cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "bin": {