@volcengine/tls-observer-pi-install 0.0.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.
@@ -0,0 +1,808 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { execFile, execFileSync } from 'node:child_process';
4
+ import { homedir } from 'node:os';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { promisify } from 'node:util';
7
+ import readline from 'node:readline/promises';
8
+ import { randomInt } from 'node:crypto';
9
+
10
+ type AnyRecord = Record<string, any>;
11
+
12
+ type TlsServiceClient = {
13
+ isAkSk: boolean;
14
+ DescribeProjects: (payload: AnyRecord) => Promise<AnyRecord>;
15
+ CreateProject: (payload: AnyRecord) => Promise<AnyRecord>;
16
+ DescribeApps: (payload: AnyRecord) => Promise<AnyRecord>;
17
+ CreateApp: (payload: AnyRecord) => Promise<AnyRecord>;
18
+ DescribeLogApps: (payload: AnyRecord) => Promise<AnyRecord>;
19
+ DescribeTemplates: (payload: AnyRecord) => Promise<AnyRecord>;
20
+ };
21
+
22
+ type OpenapiModule = {
23
+ tlsOpenapi?: {
24
+ TlsService?: new (options: AnyRecord) => AnyRecord;
25
+ };
26
+ };
27
+
28
+ type InstallOptions = {
29
+ help: boolean;
30
+ nonInteractive: boolean;
31
+ force: boolean;
32
+ beta: boolean;
33
+ dryRun: boolean;
34
+ skipTlsConfig: boolean;
35
+ piBin: string;
36
+ registry: string;
37
+ pluginPackage: string;
38
+ pluginVersion?: string;
39
+ envTarget: string;
40
+ dataRoot: string;
41
+ region?: string;
42
+ traceTopicId?: string;
43
+ apiKey?: string;
44
+ ak?: string;
45
+ sk?: string;
46
+ authMode: string;
47
+ exportTimeoutMs: string;
48
+ captureContent: string;
49
+ tlsEndpoint?: string;
50
+ projectId?: string;
51
+ projectName?: string;
52
+ logAppId?: string;
53
+ logAppName?: string;
54
+ logAppType: string;
55
+ templateName: string;
56
+ templateId?: string;
57
+ };
58
+
59
+ type PluginInstallResult = {
60
+ source: string;
61
+ target: string;
62
+ packageSpec?: string;
63
+ version?: string;
64
+ dryRun?: boolean;
65
+ };
66
+
67
+ const execFileAsync = promisify(execFile);
68
+ const PLUGIN_PACKAGE_NAME = '@volcengine/tls-observer-pi';
69
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
70
+ const DEFAULT_ENV_TARGET = path.join(homedir(), '.pi', 'agent', 'tls-observer-pi.env');
71
+ const DEFAULT_DATA_ROOT = path.join(homedir(), '.pi', 'agent', 'tls-observer-pi');
72
+ const DEFAULT_REGION = 'cn-beijing';
73
+ const DEFAULT_PROJECT_NAME = 'pi_observability';
74
+ const DEFAULT_APP_NAME_PREFIX = 'pi-observability';
75
+ const DEFAULT_TLS_ENDPOINT = 'tls-cn-beijing.volces.com';
76
+ const DEFAULT_LOG_APP_TYPE = 'Pi';
77
+ const DEFAULT_TEMPLATE_NAME = 'Pi';
78
+ const LOCAL_PLUGIN_SOURCE_ENV_KEYS = [
79
+ 'PI_PLUGIN_SOURCE',
80
+ 'PI_TLS_PLUGIN_SOURCE',
81
+ 'TRAE_TLS_PLUGIN_SOURCE',
82
+ ] as const;
83
+
84
+ function readEnvString(key: string): string | undefined {
85
+ const value = process.env[key];
86
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
87
+ }
88
+
89
+ function readEnvBool(key: string): boolean {
90
+ const value = readEnvString(key);
91
+ return value ? /^(1|true|yes|y|on)$/i.test(value) : false;
92
+ }
93
+
94
+ function takeArg(argv: string[], index: number, flag: string): string {
95
+ const value = argv[index + 1];
96
+ if (typeof value !== 'string' || value.startsWith('--')) {
97
+ throw new Error(`${flag} requires a value`);
98
+ }
99
+ return value;
100
+ }
101
+
102
+ function defaultTlsEndpointForRegion(region?: string): string {
103
+ return region ? `tls-${region}.volces.com` : DEFAULT_TLS_ENDPOINT;
104
+ }
105
+
106
+ function randomSuffix(): string {
107
+ const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
108
+ let value = '';
109
+ for (let index = 0; index < 8; index += 1) value += alphabet[randomInt(alphabet.length)];
110
+ return value;
111
+ }
112
+
113
+ function defaultAppName(): string {
114
+ return `${DEFAULT_APP_NAME_PREFIX}_${randomSuffix()}`;
115
+ }
116
+
117
+ function applyDerivedDefaults(options: InstallOptions): void {
118
+ if (options.region && !options.tlsEndpoint) {
119
+ options.tlsEndpoint = defaultTlsEndpointForRegion(options.region);
120
+ }
121
+ if (!options.projectName && !options.projectId) options.projectName = DEFAULT_PROJECT_NAME;
122
+ if (!options.logAppName) options.logAppName = defaultAppName();
123
+ }
124
+
125
+ function parseCliOptions(argv: string[] = process.argv.slice(2)): InstallOptions {
126
+ const options = {
127
+ help: false,
128
+ nonInteractive: readEnvBool('PI_TLS_INSTALL_NON_INTERACTIVE'),
129
+ force: readEnvBool('PI_TLS_INSTALL_FORCE'),
130
+ beta: readEnvBool('PI_TLS_INSTALL_BETA'),
131
+ dryRun: readEnvBool('PI_TLS_INSTALL_DRY_RUN'),
132
+ skipTlsConfig: readEnvBool('PI_TLS_INSTALL_SKIP_TLS_CONFIG'),
133
+ piBin: readEnvString('PI_TLS_PI_BIN') || readEnvString('PI_BIN') || 'pi',
134
+ registry:
135
+ readEnvString('PI_TLS_NPM_REGISTRY') ||
136
+ readEnvString('npm_config_registry') ||
137
+ readEnvString('NPM_CONFIG_REGISTRY') ||
138
+ DEFAULT_REGISTRY,
139
+ pluginPackage: readEnvString('PI_TLS_PLUGIN_PACKAGE') || PLUGIN_PACKAGE_NAME,
140
+ pluginVersion: readEnvString('PI_TLS_PLUGIN_VERSION'),
141
+ envTarget: readEnvString('PI_TLS_ENV_TARGET') || DEFAULT_ENV_TARGET,
142
+ dataRoot: readEnvString('PI_TLS_DATA_ROOT') || DEFAULT_DATA_ROOT,
143
+ region: readEnvString('PI_TLS_REGION') || DEFAULT_REGION,
144
+ traceTopicId: readEnvString('PI_TLS_TRACE_TOPIC_ID'),
145
+ apiKey: readEnvString('PI_TLS_API_KEY'),
146
+ ak: readEnvString('PI_TLS_AK'),
147
+ sk: readEnvString('PI_TLS_SK'),
148
+ authMode: readEnvString('PI_TLS_AUTH_MODE') || 'header',
149
+ exportTimeoutMs: readEnvString('PI_TLS_EXPORT_TIMEOUT_MS') || '20000',
150
+ captureContent: readEnvString('PI_TLS_CAPTURE_CONTENT') || '1',
151
+ tlsEndpoint: readEnvString('PI_TLS_ENDPOINT'),
152
+ projectId: readEnvString('PI_TLS_PROJECT_ID'),
153
+ projectName: readEnvString('PI_TLS_PROJECT_NAME'),
154
+ logAppId: readEnvString('PI_TLS_LOG_APP_ID'),
155
+ logAppName: readEnvString('PI_TLS_LOG_APP_NAME'),
156
+ logAppType: readEnvString('PI_TLS_LOG_APP_TYPE') || DEFAULT_LOG_APP_TYPE,
157
+ templateName: readEnvString('PI_TLS_TEMPLATE_NAME') || DEFAULT_TEMPLATE_NAME,
158
+ templateId: readEnvString('PI_TLS_TEMPLATE_ID'),
159
+ };
160
+
161
+ for (let index = 0; index < argv.length; index += 1) {
162
+ const current = argv[index];
163
+ if (current === '--help' || current === '-h') options.help = true;
164
+ else if (current === '--non-interactive') options.nonInteractive = true;
165
+ else if (current === '--force') options.force = true;
166
+ else if (current === '--beta') options.beta = true;
167
+ else if (current === '--dry-run') options.dryRun = true;
168
+ else if (current === '--skip-tls-config') options.skipTlsConfig = true;
169
+ else if (current === '--pi-bin') options.piBin = takeArg(argv, index++, current);
170
+ else if (current === '--registry') options.registry = takeArg(argv, index++, current);
171
+ else if (current === '--plugin-package') options.pluginPackage = takeArg(argv, index++, current);
172
+ else if (current === '--plugin-version') options.pluginVersion = takeArg(argv, index++, current);
173
+ else if (current === '--env-target') options.envTarget = takeArg(argv, index++, current);
174
+ else if (current === '--data-root') options.dataRoot = takeArg(argv, index++, current);
175
+ else if (current === '--region') options.region = takeArg(argv, index++, current);
176
+ else if (current === '--trace-topic-id') options.traceTopicId = takeArg(argv, index++, current);
177
+ else if (current === '--api-key') options.apiKey = takeArg(argv, index++, current);
178
+ else if (current === '--ak') options.ak = takeArg(argv, index++, current);
179
+ else if (current === '--sk') options.sk = takeArg(argv, index++, current);
180
+ else if (current === '--auth-mode') options.authMode = takeArg(argv, index++, current);
181
+ else if (current === '--export-timeout-ms') options.exportTimeoutMs = takeArg(argv, index++, current);
182
+ else if (current === '--capture-content') options.captureContent = takeArg(argv, index++, current);
183
+ else if (current === '--tls-endpoint') options.tlsEndpoint = takeArg(argv, index++, current);
184
+ else if (current === '--project-id') options.projectId = takeArg(argv, index++, current);
185
+ else if (current === '--project-name') options.projectName = takeArg(argv, index++, current);
186
+ else if (current === '--app-name' || current === '--log-app-name')
187
+ options.logAppName = takeArg(argv, index++, current);
188
+ else if (current === '--log-app-id') options.logAppId = takeArg(argv, index++, current);
189
+ else if (current === '--log-app-type') options.logAppType = takeArg(argv, index++, current);
190
+ else if (current === '--template-name') options.templateName = takeArg(argv, index++, current);
191
+ else if (current === '--template-id') options.templateId = takeArg(argv, index++, current);
192
+ else throw new Error(`Unknown option: ${current}`);
193
+ }
194
+
195
+ return options;
196
+ }
197
+
198
+ function printHelp(): void {
199
+ console.log(`TLS Observer Pi installer
200
+
201
+ Usage:
202
+ tls-observer-pi-install [options]
203
+ npm exec -y --package=@volcengine/tls-observer-pi-install -- tls-observer-pi-install [options]
204
+
205
+ Options:
206
+ --non-interactive Read required inputs from flags/env
207
+ --force Overwrite existing env file
208
+ --beta Install @volcengine/tls-observer-pi@beta instead of @latest
209
+ --dry-run Write env but skip running pi install
210
+ --skip-tls-config Install plugin without writing TLS env
211
+ --pi-bin <path> Pi CLI executable. Default: pi
212
+ --registry <url> NPM registry for plugin package. Default: https://registry.npmjs.org
213
+ --plugin-version <version> Exact plugin version or dist-tag. Overrides --beta
214
+ --region <region> TLS region. Default: cn-beijing
215
+ --tls-endpoint <host> TLS Producer endpoint. Derived from region by default
216
+ --trace-topic-id <id> TLS trace topic ID
217
+ --api-key <value> TLS API Key
218
+ --ak <value> TLS Access Key
219
+ --sk <value> TLS Secret Key
220
+ --project-name <name> TLS Project name for AK/SK auto creation
221
+ --app-name <name> TLS LogApp name for AK/SK auto creation
222
+ --export-timeout-ms <ms> TLS export timeout. Default: 20000
223
+ --capture-content <0|1> Default: 1
224
+ --env-target <path> Default: ~/.pi/agent/tls-observer-pi.env
225
+ --data-root <path> Default: ~/.pi/agent/tls-observer-pi
226
+
227
+ Environment:
228
+ PI_PLUGIN_SOURCE Install a local built plugin package instead of installing from npm
229
+ PI_TLS_PI_BIN Pi CLI executable path when pi is not on PATH
230
+ `);
231
+ }
232
+
233
+ function createReadline(): readline.Interface {
234
+ return readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
235
+ }
236
+
237
+ async function promptText(rl: readline.Interface, message: string, defaultValue = ''): Promise<string> {
238
+ const suffix = defaultValue ? `\n default: ${defaultValue}\n> ` : '\n> ';
239
+ const answer = await rl.question(`${message}${suffix}`);
240
+ return answer.trim() || defaultValue;
241
+ }
242
+
243
+ async function promptRequired(rl: readline.Interface, message: string, defaultValue = ''): Promise<string> {
244
+ while (true) {
245
+ const value = await promptText(rl, message, defaultValue);
246
+ if (value.trim()) return value.trim();
247
+ console.log('This value is required.');
248
+ }
249
+ }
250
+
251
+ function setTerminalEcho(enabled: boolean): boolean {
252
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
253
+ try {
254
+ execFileSync('stty', [enabled ? 'echo' : '-echo'], {
255
+ stdio: ['inherit', 'ignore', 'ignore'],
256
+ });
257
+ return true;
258
+ } catch {
259
+ return false;
260
+ }
261
+ }
262
+
263
+ async function promptSecret(rl: readline.Interface, message: string, defaultValue = ''): Promise<string> {
264
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
265
+ return promptRequired(rl, message, defaultValue);
266
+ }
267
+
268
+ rl.pause();
269
+ return new Promise<string>((resolve, reject) => {
270
+ const input = process.stdin;
271
+ let value = '';
272
+ let finished = false;
273
+ const previousRawMode = input.isRaw;
274
+ let echoDisabled = false;
275
+ const suffix = defaultValue ? '\n default: 已设置,回车保留\n> ' : '\n> ';
276
+
277
+ function cleanup(): void {
278
+ input.off('data', onData);
279
+ input.setRawMode(Boolean(previousRawMode));
280
+ if (echoDisabled) setTerminalEcho(true);
281
+ input.pause();
282
+ rl.resume();
283
+ }
284
+
285
+ function finish(): void {
286
+ if (finished) return;
287
+ finished = true;
288
+ cleanup();
289
+ process.stdout.write('\n');
290
+ const resolvedValue = value.trim() || defaultValue;
291
+ if (resolvedValue) {
292
+ resolve(resolvedValue);
293
+ } else {
294
+ console.log('This value is required.');
295
+ resolve(promptSecret(rl, message, defaultValue));
296
+ }
297
+ }
298
+
299
+ function onData(buffer: Buffer): void {
300
+ for (const char of buffer.toString('utf8')) {
301
+ if (finished) return;
302
+ if (char === '\u0003') {
303
+ finished = true;
304
+ cleanup();
305
+ reject(new Error('Interrupted'));
306
+ return;
307
+ }
308
+ if (char === '\r' || char === '\n') {
309
+ finish();
310
+ return;
311
+ }
312
+ if (char === '\u007f' || char === '\b') {
313
+ if (value.length > 0) {
314
+ value = [...value].slice(0, -1).join('');
315
+ process.stdout.write('\b \b');
316
+ }
317
+ continue;
318
+ }
319
+ value += char;
320
+ process.stdout.write('*');
321
+ }
322
+ }
323
+
324
+ input.resume();
325
+ input.setRawMode(true);
326
+ input.on('data', onData);
327
+ echoDisabled = setTerminalEcho(false);
328
+ process.stdout.write(`${message}${suffix}`);
329
+ });
330
+ }
331
+
332
+ function detectAuthType(options: InstallOptions): 'apiKey' | 'aksk' | undefined {
333
+ if (options.apiKey) return 'apiKey';
334
+ if (options.ak || options.sk) return 'aksk';
335
+ return undefined;
336
+ }
337
+
338
+ function validateBaseTlsConfig(options: InstallOptions): void {
339
+ const missing: string[] = [];
340
+ if (!options.region) missing.push('--region or PI_TLS_REGION');
341
+ const authType = detectAuthType(options);
342
+ if (!authType) missing.push('--api-key or --ak/--sk');
343
+ if (authType === 'apiKey' && (options.ak || options.sk)) {
344
+ throw new Error('API Key and AK/SK cannot be used together');
345
+ }
346
+ if (authType === 'aksk' && (!options.ak || !options.sk)) missing.push('--ak and --sk');
347
+ if (missing.length > 0) throw new Error(`Missing required TLS config: ${missing.join(', ')}`);
348
+ }
349
+
350
+ function validateTlsConfig(options: InstallOptions): void {
351
+ validateBaseTlsConfig(options);
352
+ const missing: string[] = [];
353
+ if (!options.tlsEndpoint) missing.push('--tls-endpoint or --region');
354
+ if (!options.traceTopicId) missing.push('--trace-topic-id or PI_TLS_TRACE_TOPIC_ID');
355
+ if (missing.length > 0) throw new Error(`Missing required TLS config: ${missing.join(', ')}`);
356
+ }
357
+
358
+ async function collectInteractiveOptions(options: InstallOptions): Promise<InstallOptions> {
359
+ if (options.nonInteractive) {
360
+ if (!options.skipTlsConfig) {
361
+ applyDerivedDefaults(options);
362
+ validateBaseTlsConfig(options);
363
+ }
364
+ return options;
365
+ }
366
+
367
+ const rl = createReadline();
368
+ try {
369
+ console.log('TLS Observer Pi installer');
370
+ options.force = true;
371
+ if (!options.skipTlsConfig) {
372
+ options.region = await promptRequired(rl, 'TLS region', options.region || DEFAULT_REGION);
373
+ applyDerivedDefaults(options);
374
+ const authType = await promptRequired(
375
+ rl,
376
+ 'Auth type, apiKey or aksk',
377
+ detectAuthType(options) || 'apiKey',
378
+ );
379
+ if (authType === 'apiKey') {
380
+ options.traceTopicId = await promptRequired(rl, 'Trace topic ID', options.traceTopicId || '');
381
+ options.apiKey = await promptSecret(rl, 'TLS API Key', options.apiKey || '');
382
+ options.ak = undefined;
383
+ options.sk = undefined;
384
+ } else if (authType === 'aksk') {
385
+ options.projectName = await promptRequired(
386
+ rl,
387
+ 'TLS Project name',
388
+ options.projectName || DEFAULT_PROJECT_NAME,
389
+ );
390
+ options.logAppName = await promptRequired(
391
+ rl,
392
+ 'TLS LogApp name',
393
+ options.logAppName || defaultAppName(),
394
+ );
395
+ options.ak = await promptSecret(rl, 'TLS AK', options.ak || '');
396
+ options.sk = await promptSecret(rl, 'TLS SK', options.sk || '');
397
+ options.apiKey = undefined;
398
+ } else {
399
+ throw new Error('Auth type must be apiKey or aksk');
400
+ }
401
+ }
402
+ return options;
403
+ } finally {
404
+ rl.close();
405
+ }
406
+ }
407
+
408
+ function moduleSpecifier(specifier: string): string {
409
+ if (specifier.startsWith('.') || specifier.startsWith('/') || /^[A-Za-z]:[\\/]/.test(specifier)) {
410
+ return pathToFileURL(path.resolve(specifier)).href;
411
+ }
412
+ return specifier;
413
+ }
414
+
415
+ async function loadOpenapiModule(): Promise<OpenapiModule> {
416
+ return import(moduleSpecifier(readEnvString('PI_TLS_OPENAPI_MODULE') || '@volcengine/openapi'));
417
+ }
418
+
419
+ function createTlsService(options: InstallOptions, openapiModule: OpenapiModule): TlsServiceClient {
420
+ if (!openapiModule?.tlsOpenapi?.TlsService) {
421
+ throw new Error('@volcengine/openapi must export tlsOpenapi.TlsService');
422
+ }
423
+ const isAkSk = !options.apiKey;
424
+ const tlsService = new openapiModule.tlsOpenapi.TlsService({
425
+ host: options.tlsEndpoint || defaultTlsEndpointForRegion(options.region),
426
+ region: options.region,
427
+ accessKeyId: isAkSk ? options.ak : 'anonymous',
428
+ secretKey: isAkSk ? options.sk : 'anonymous',
429
+ protocol: 'https:',
430
+ version: '0.3.0',
431
+ });
432
+ const axiosConfig = options.apiKey
433
+ ? { proxy: false, headers: { 'x-tls-anonymous-identity': options.apiKey } }
434
+ : { proxy: false };
435
+
436
+ return {
437
+ isAkSk,
438
+ DescribeProjects: (payload: AnyRecord) => tlsService.DescribeProjects(payload, axiosConfig),
439
+ CreateProject: (payload: AnyRecord) => tlsService.CreateProject(payload, axiosConfig),
440
+ DescribeApps: (payload: AnyRecord) =>
441
+ tlsService.createAPI('DescribeApps', { method: 'GET' })(payload, axiosConfig),
442
+ CreateApp: (payload: AnyRecord) =>
443
+ tlsService.createAPI('CreateApp', { method: 'POST' })(payload, axiosConfig),
444
+ DescribeLogApps: (payload: AnyRecord) =>
445
+ tlsService.createAPI('DescribeLogApps', { method: 'GET' })(payload, axiosConfig),
446
+ DescribeTemplates: (payload: AnyRecord) =>
447
+ tlsService.createAPI('DescribeTemplates', { method: 'GET' })(payload, axiosConfig),
448
+ };
449
+ }
450
+
451
+ async function ensureTemplateId(tlsService: TlsServiceClient, options: InstallOptions): Promise<string> {
452
+ if (options.templateId) return options.templateId;
453
+ const response = await tlsService.DescribeTemplates({
454
+ TemplateName: options.templateName,
455
+ PageNumber: 1,
456
+ PageSize: 20,
457
+ });
458
+ const template = response.Templates?.find((item: AnyRecord) => item.TemplateName === options.templateName);
459
+ const templateId = String(template?.TemplateId || '').trim();
460
+ if (!templateId) {
461
+ throw new Error(
462
+ `DescribeTemplates did not return TemplateId for ${options.templateName}. Pass --template-id or --trace-topic-id.`,
463
+ );
464
+ }
465
+ return templateId;
466
+ }
467
+
468
+ async function ensureProjectId(tlsService: TlsServiceClient, options: InstallOptions): Promise<string> {
469
+ if (options.projectId) return options.projectId;
470
+ const response = await tlsService.DescribeProjects({
471
+ ProjectName: options.projectName,
472
+ PageSize: 100,
473
+ PageNumber: 1,
474
+ });
475
+ const project = response.Projects?.find((item: AnyRecord) => item.ProjectName === options.projectName);
476
+ const existingProjectId = String(project?.ProjectId || '').trim();
477
+ if (existingProjectId) return existingProjectId;
478
+ const created = await tlsService.CreateProject({
479
+ ProjectName: options.projectName,
480
+ Description: 'Pi observability',
481
+ Region: options.region,
482
+ });
483
+ const projectId = String(created.ProjectId || '').trim();
484
+ if (!projectId) throw new Error('CreateProject succeeded but did not return ProjectId');
485
+ return projectId;
486
+ }
487
+
488
+ async function ensureLogApp(
489
+ tlsService: TlsServiceClient,
490
+ options: InstallOptions,
491
+ projectId: string,
492
+ ): Promise<string> {
493
+ if (options.logAppId) return options.logAppId;
494
+ const appResponse = await tlsService.DescribeApps({
495
+ AppName: options.logAppName,
496
+ AppType: 'LogApp',
497
+ });
498
+ const total = appResponse.total ?? appResponse.Total ?? 0;
499
+ if (total > 0) {
500
+ const app = appResponse.apps?.[0] || appResponse.Apps?.[0];
501
+ const logAppId = String(app?.Resources?.[0]?.Id || app?.Sources?.[0]?.Id || '').trim();
502
+ if (logAppId) return logAppId;
503
+ }
504
+ const templateId = await ensureTemplateId(tlsService, options);
505
+ const created = await tlsService.CreateApp({
506
+ AppName: options.logAppName,
507
+ AppType: 'LogApp',
508
+ LogAppReq: {
509
+ LogAppName: options.logAppName,
510
+ LogAppType: options.logAppType,
511
+ ProjectId: projectId,
512
+ },
513
+ TemplateId: templateId,
514
+ });
515
+ const logAppId = String(created.Sources?.[0]?.Id || '').trim();
516
+ if (!logAppId) throw new Error('CreateApp succeeded but did not return Sources[0].Id');
517
+ return logAppId;
518
+ }
519
+
520
+ function topicMapFromLogApps(response: AnyRecord): Record<string, string> {
521
+ const topics = response?.LogApps?.[0]?.RelatedResourceList || [];
522
+ const topicMap: Record<string, string> = {};
523
+ for (const topic of topics) {
524
+ const resourceName = String(topic?.ResourceName || '');
525
+ const topicName = resourceName.split('_').filter(Boolean).at(-1);
526
+ const resourceId = String(topic?.ResourceID || '').trim();
527
+ if (topicName && resourceId) topicMap[topicName] = resourceId;
528
+ }
529
+ return topicMap;
530
+ }
531
+
532
+ async function ensureTlsResources(
533
+ options: InstallOptions,
534
+ ): Promise<{ created: boolean; logAppId?: string; traceTopicId?: string }> {
535
+ applyDerivedDefaults(options);
536
+ if (
537
+ options.skipTlsConfig ||
538
+ options.dryRun ||
539
+ options.apiKey ||
540
+ options.traceTopicId ||
541
+ !options.ak ||
542
+ !options.sk
543
+ ) {
544
+ return { created: false };
545
+ }
546
+ const openapiModule = await loadOpenapiModule();
547
+ const tlsService = createTlsService(options, openapiModule);
548
+ const projectId = await ensureProjectId(tlsService, options);
549
+
550
+ const logAppId = await ensureLogApp(tlsService, options, projectId);
551
+ options.logAppId = logAppId;
552
+ const logApps = await tlsService.DescribeLogApps({
553
+ PageNumber: 1,
554
+ PageSize: 20,
555
+ LogAppId: logAppId,
556
+ LogAppType: options.logAppType,
557
+ });
558
+
559
+ const topics = topicMapFromLogApps(logApps);
560
+ if (!options.traceTopicId) options.traceTopicId = topics.trace;
561
+ return { created: true, logAppId, traceTopicId: options.traceTopicId };
562
+ }
563
+
564
+ async function pathExists(filePath: string): Promise<boolean> {
565
+ try {
566
+ await fs.stat(filePath);
567
+ return true;
568
+ } catch (error) {
569
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
570
+ throw error;
571
+ }
572
+ }
573
+
574
+ async function assertPluginRuntime(pluginSource: string): Promise<void> {
575
+ const entry = path.join(pluginSource, 'dist', 'index.js');
576
+ const manifest = path.join(pluginSource, 'package.json');
577
+ if ((await pathExists(manifest)) && (await pathExists(entry))) return;
578
+ throw new Error(
579
+ `Plugin package is missing runtime files: ${pluginSource}. Expected dist/index.js and package.json.`,
580
+ );
581
+ }
582
+
583
+ function pluginPackageSpec(options: InstallOptions): string {
584
+ const version = options.pluginVersion || (options.beta ? 'beta' : 'latest');
585
+ return `${options.pluginPackage}@${version}`;
586
+ }
587
+
588
+ function readLocalPluginSourceEnv(): { key: string; value: string } | undefined {
589
+ for (const key of LOCAL_PLUGIN_SOURCE_ENV_KEYS) {
590
+ const value = readEnvString(key);
591
+ if (value) return { key, value };
592
+ }
593
+ return undefined;
594
+ }
595
+
596
+ async function findLocalPluginSource(): Promise<string | undefined> {
597
+ const explicit = readLocalPluginSourceEnv();
598
+ if (!explicit) return undefined;
599
+ const manifest = await readJsonFile<{ publishConfig?: { directory?: string } }>(
600
+ path.join(explicit.value, 'package.json'),
601
+ {},
602
+ );
603
+ const publishDirectory = manifest.publishConfig?.directory;
604
+ const candidates = publishDirectory ? [path.join(explicit.value, publishDirectory)] : [explicit.value];
605
+ for (const candidate of candidates) {
606
+ try {
607
+ await assertPluginRuntime(candidate);
608
+ return candidate;
609
+ } catch {
610
+ // Continue so the final error includes the env var value.
611
+ }
612
+ }
613
+ throw new Error(
614
+ `${explicit.key} does not point to a built Pi runtime package: ${explicit.value}. Run the plugin build first.`,
615
+ );
616
+ }
617
+
618
+ async function readPluginVersion(pluginSource: string): Promise<string | undefined> {
619
+ const manifest = await readJsonFile<{ version?: string }>(path.join(pluginSource, 'package.json'), {});
620
+ return manifest.version;
621
+ }
622
+
623
+ function piCliEnv(options: InstallOptions): NodeJS.ProcessEnv {
624
+ return {
625
+ ...process.env,
626
+ npm_config_registry: options.registry,
627
+ NPM_CONFIG_REGISTRY: options.registry,
628
+ };
629
+ }
630
+
631
+ function commandErrorDetail(error: unknown): string {
632
+ if (error && typeof error === 'object' && 'stderr' in error) {
633
+ const stderr = String((error as { stderr?: unknown }).stderr || '').trim();
634
+ if (stderr) return stderr;
635
+ }
636
+ return error instanceof Error ? error.message : String(error);
637
+ }
638
+
639
+ async function execPiCli(options: InstallOptions, args: string[]): Promise<void> {
640
+ try {
641
+ await execFileAsync(options.piBin, args, { env: piCliEnv(options) });
642
+ } catch (error) {
643
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
644
+ throw new Error(
645
+ `Pi CLI executable not found: ${options.piBin}. Install Pi CLI, make it available on PATH, or pass --pi-bin <path> / set PI_TLS_PI_BIN. The installer needs to run "${options.piBin} ${args.join(' ')}".`,
646
+ );
647
+ }
648
+ throw new Error(
649
+ `Pi CLI command failed: ${options.piBin} ${args.join(' ')}\n${commandErrorDetail(error)}`,
650
+ );
651
+ }
652
+ }
653
+
654
+ async function installPlugin(options: InstallOptions): Promise<PluginInstallResult> {
655
+ const localSource = await findLocalPluginSource();
656
+ const packageSpec = pluginPackageSpec(options);
657
+ const source = localSource || `npm:${packageSpec}`;
658
+ if (options.dryRun) {
659
+ return {
660
+ source,
661
+ target: source,
662
+ packageSpec: localSource ? undefined : packageSpec,
663
+ version: localSource ? await readPluginVersion(localSource) : undefined,
664
+ dryRun: true,
665
+ };
666
+ }
667
+ if (localSource) {
668
+ await execPiCli(options, ['install', localSource]);
669
+ return {
670
+ source: localSource,
671
+ target: localSource,
672
+ version: await readPluginVersion(localSource),
673
+ };
674
+ }
675
+ await execPiCli(options, ['install', source]);
676
+ return { source, target: source, packageSpec };
677
+ }
678
+
679
+ async function readJsonFile<T>(filePath: string, fallback: T): Promise<T> {
680
+ try {
681
+ return JSON.parse(await fs.readFile(filePath, 'utf8')) as T;
682
+ } catch (error) {
683
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return fallback;
684
+ throw error;
685
+ }
686
+ }
687
+
688
+ async function writeJsonFile(filePath: string, value: unknown, mode?: number): Promise<void> {
689
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
690
+ if (mode) {
691
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode });
692
+ } else {
693
+ await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
694
+ }
695
+ }
696
+
697
+ function envLine(key: string, value?: string | null): string | undefined {
698
+ if (value === undefined || value === null || value === '') return undefined;
699
+ return `${key}=${String(value)}`;
700
+ }
701
+
702
+ function buildTlsEnv(options: InstallOptions): string {
703
+ const lines = [
704
+ '# Generated by tls-observer-pi installer.',
705
+ '# Keep this file local because it can contain credentials.',
706
+ envLine('PI_TLS_REGION', options.region),
707
+ envLine('PI_TLS_ENDPOINT', options.tlsEndpoint),
708
+ envLine('PI_TLS_TRACE_TOPIC_ID', options.traceTopicId),
709
+ envLine('PI_TLS_API_KEY', options.apiKey),
710
+ envLine('PI_TLS_AK', options.ak),
711
+ envLine('PI_TLS_SK', options.sk),
712
+ envLine('PI_TLS_AUTH_MODE', options.authMode),
713
+ envLine('PI_TLS_EXPORT_TIMEOUT_MS', options.exportTimeoutMs),
714
+ envLine('PI_TLS_CAPTURE_CONTENT', options.captureContent),
715
+ envLine('PI_TLS_DATA_ROOT', options.dataRoot),
716
+ ].filter(Boolean);
717
+ return `${lines.join('\n')}\n`;
718
+ }
719
+
720
+ async function writeTlsEnv(options: InstallOptions): Promise<string | null> {
721
+ if (options.skipTlsConfig) return null;
722
+ const envTargetExists = await pathExists(options.envTarget);
723
+ if (envTargetExists && !options.force) {
724
+ throw new Error(`TLS env target exists: ${options.envTarget}. Re-run with --force.`);
725
+ }
726
+ await writeJsonFile(path.join(options.dataRoot, 'state', 'install.json'), {
727
+ installed_at: new Date().toISOString(),
728
+ plugin_id: PLUGIN_PACKAGE_NAME,
729
+ log_app_id: options.logAppId,
730
+ env_target: options.envTarget,
731
+ });
732
+ await fs.mkdir(path.dirname(options.envTarget), { recursive: true, mode: 0o700 });
733
+ if (envTargetExists) {
734
+ await fs.chmod(options.envTarget, 0o600).catch(() => undefined);
735
+ await fs.rm(options.envTarget, { force: true });
736
+ }
737
+ await fs.writeFile(options.envTarget, buildTlsEnv(options), { flag: 'wx', mode: 0o600 });
738
+ await fs.chmod(options.envTarget, 0o600);
739
+ return options.envTarget;
740
+ }
741
+
742
+ function printCompletion({
743
+ plugin,
744
+ envTarget,
745
+ }: {
746
+ plugin: PluginInstallResult;
747
+ envTarget: string | null;
748
+ }): void {
749
+ console.log('');
750
+ console.log(plugin.dryRun ? 'Dry run complete' : 'Install complete');
751
+ console.log(` Plugin: ${plugin.target}`);
752
+ if (plugin.packageSpec) console.log(` Plugin package: ${plugin.packageSpec}`);
753
+ if (plugin.version) console.log(` Plugin version: ${plugin.version}`);
754
+ console.log(` TLS env: ${envTarget || 'skipped'}`);
755
+ console.log('');
756
+ console.log('数据访问说明');
757
+ console.log('安装后,插件会读取 Pi 运行过程中的会话和生命周期事件');
758
+ console.log('用户请求、模型回复、工具调用记录和 token 用量会整理成诊断数据,上传到你配置的火山引擎 TLS');
759
+ console.log('如配置 API Key 或 AK/SK,凭据只写入本机 env 文件,用于上传鉴权');
760
+ console.log('');
761
+ console.log('Next steps:');
762
+ console.log(' 1. Restart Pi environment.');
763
+ console.log(' 2. Run one Pi turn.');
764
+ console.log(' 3. Check TLS Trace Topic for agent.turn.');
765
+ }
766
+
767
+ async function run(argv: string[] = process.argv.slice(2)): Promise<void> {
768
+ const parsed = parseCliOptions(argv);
769
+ if (parsed.help) {
770
+ printHelp();
771
+ return;
772
+ }
773
+ const options = await collectInteractiveOptions(parsed);
774
+ if (!options.skipTlsConfig) {
775
+ const resourceResult = await ensureTlsResources(options);
776
+ if (resourceResult.created && !options.traceTopicId) {
777
+ throw new Error('TLS LogApp was created but trace topic discovery failed. Pass --trace-topic-id.');
778
+ }
779
+ applyDerivedDefaults(options);
780
+ validateTlsConfig(options);
781
+ }
782
+ const plugin = await installPlugin(options);
783
+ const envTarget = await writeTlsEnv(options);
784
+ printCompletion({ plugin, envTarget });
785
+ }
786
+
787
+ async function writeEnvFile(options: InstallOptions): Promise<void> {
788
+ await writeTlsEnv(options);
789
+ }
790
+
791
+ const runInstaller = run;
792
+ const installPiPackage = installPlugin;
793
+
794
+ export {
795
+ applyDerivedDefaults,
796
+ buildTlsEnv,
797
+ findLocalPluginSource,
798
+ installPiPackage,
799
+ installPlugin,
800
+ parseCliOptions,
801
+ pluginPackageSpec,
802
+ run,
803
+ runInstaller,
804
+ validateBaseTlsConfig,
805
+ validateTlsConfig,
806
+ writeEnvFile,
807
+ writeTlsEnv,
808
+ };