@cephalization/phoenix-insight 0.1.0
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/LICENSE +201 -0
- package/README.md +620 -0
- package/dist/agent/index.js +230 -0
- package/dist/cli.js +640 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/px-fetch-more-spans.js +98 -0
- package/dist/commands/px-fetch-more-trace.js +110 -0
- package/dist/config/index.js +165 -0
- package/dist/config/loader.js +141 -0
- package/dist/config/schema.js +53 -0
- package/dist/index.js +1 -0
- package/dist/modes/index.js +17 -0
- package/dist/modes/local.js +134 -0
- package/dist/modes/sandbox.js +121 -0
- package/dist/modes/types.js +1 -0
- package/dist/observability/index.js +65 -0
- package/dist/progress.js +209 -0
- package/dist/prompts/index.js +1 -0
- package/dist/prompts/system.js +30 -0
- package/dist/snapshot/client.js +74 -0
- package/dist/snapshot/context.js +332 -0
- package/dist/snapshot/datasets.js +68 -0
- package/dist/snapshot/experiments.js +135 -0
- package/dist/snapshot/index.js +262 -0
- package/dist/snapshot/projects.js +44 -0
- package/dist/snapshot/prompts.js +199 -0
- package/dist/snapshot/spans.js +80 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +75 -0
- package/src/agent/index.ts +323 -0
- package/src/cli.ts +782 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/px-fetch-more-spans.ts +174 -0
- package/src/commands/px-fetch-more-trace.ts +183 -0
- package/src/config/index.ts +225 -0
- package/src/config/loader.ts +173 -0
- package/src/config/schema.ts +66 -0
- package/src/index.ts +1 -0
- package/src/modes/index.ts +21 -0
- package/src/modes/local.ts +163 -0
- package/src/modes/sandbox.ts +144 -0
- package/src/modes/types.ts +31 -0
- package/src/observability/index.ts +90 -0
- package/src/progress.ts +239 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/system.ts +31 -0
- package/src/snapshot/client.ts +129 -0
- package/src/snapshot/context.ts +462 -0
- package/src/snapshot/datasets.ts +132 -0
- package/src/snapshot/experiments.ts +246 -0
- package/src/snapshot/index.ts +403 -0
- package/src/snapshot/projects.ts +58 -0
- package/src/snapshot/prompts.ts +267 -0
- package/src/snapshot/spans.ts +142 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { PhoenixClient } from "@arizeai/phoenix-client";
|
|
2
|
+
import type { ExecutionMode } from "../modes/types.js";
|
|
3
|
+
import { withErrorHandling } from "../snapshot/client.js";
|
|
4
|
+
|
|
5
|
+
export interface FetchMoreSpansOptions {
|
|
6
|
+
/** Project name to fetch spans for */
|
|
7
|
+
project: string;
|
|
8
|
+
/** Number of additional spans to fetch */
|
|
9
|
+
limit: number;
|
|
10
|
+
/** Inclusive lower bound time for filtering spans */
|
|
11
|
+
startTime?: Date | string | null;
|
|
12
|
+
/** Exclusive upper bound time for filtering spans */
|
|
13
|
+
endTime?: Date | string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SpanData {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
context: {
|
|
20
|
+
trace_id: string;
|
|
21
|
+
span_id: string;
|
|
22
|
+
};
|
|
23
|
+
span_kind: string;
|
|
24
|
+
parent_id: string | null;
|
|
25
|
+
start_time: string;
|
|
26
|
+
end_time: string;
|
|
27
|
+
status_code: string;
|
|
28
|
+
status_message: string;
|
|
29
|
+
attributes: Record<string, unknown>;
|
|
30
|
+
events: Array<unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SpansMetadata {
|
|
34
|
+
project: string;
|
|
35
|
+
spanCount: number;
|
|
36
|
+
startTime: string | null;
|
|
37
|
+
endTime: string | null;
|
|
38
|
+
snapshotTime: string;
|
|
39
|
+
lastCursor?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fetches additional spans for a specific project on-demand
|
|
44
|
+
*
|
|
45
|
+
* @param client - Phoenix client instance
|
|
46
|
+
* @param mode - Execution mode for file operations
|
|
47
|
+
* @param options - Options for fetching spans
|
|
48
|
+
*/
|
|
49
|
+
export async function fetchMoreSpans(
|
|
50
|
+
client: PhoenixClient,
|
|
51
|
+
mode: ExecutionMode,
|
|
52
|
+
options: FetchMoreSpansOptions
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const { project, limit, startTime, endTime } = options;
|
|
55
|
+
|
|
56
|
+
await withErrorHandling(async () => {
|
|
57
|
+
// Try to read existing metadata to get the last cursor
|
|
58
|
+
let existingMetadata: SpansMetadata | null = null;
|
|
59
|
+
let existingSpans: SpanData[] = [];
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const metadataResult = await mode.exec(
|
|
63
|
+
`cat /phoenix/projects/${project}/spans/metadata.json`
|
|
64
|
+
);
|
|
65
|
+
if (metadataResult.exitCode === 0 && metadataResult.stdout) {
|
|
66
|
+
existingMetadata = JSON.parse(metadataResult.stdout);
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// Metadata doesn't exist, that's okay
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Try to read existing spans
|
|
73
|
+
try {
|
|
74
|
+
const spansResult = await mode.exec(
|
|
75
|
+
`cat /phoenix/projects/${project}/spans/index.jsonl`
|
|
76
|
+
);
|
|
77
|
+
if (spansResult.exitCode === 0 && spansResult.stdout) {
|
|
78
|
+
existingSpans = spansResult.stdout
|
|
79
|
+
.trim()
|
|
80
|
+
.split("\n")
|
|
81
|
+
.filter((line) => line.length > 0)
|
|
82
|
+
.map((line) => JSON.parse(line) as SpanData);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Spans file doesn't exist, that's okay
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fetch new spans
|
|
89
|
+
const newSpans: SpanData[] = [];
|
|
90
|
+
let cursor: string | null = existingMetadata?.lastCursor ?? null;
|
|
91
|
+
let totalFetched = 0;
|
|
92
|
+
|
|
93
|
+
while (totalFetched < limit) {
|
|
94
|
+
const query: Record<string, any> = {
|
|
95
|
+
limit: Math.min(100, limit - totalFetched), // Fetch in chunks of 100
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (cursor) {
|
|
99
|
+
query.cursor = cursor;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (startTime) {
|
|
103
|
+
query.start_time =
|
|
104
|
+
startTime instanceof Date ? startTime.toISOString() : startTime;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (endTime) {
|
|
108
|
+
query.end_time =
|
|
109
|
+
endTime instanceof Date ? endTime.toISOString() : endTime;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response = await client.GET(
|
|
113
|
+
"/v1/projects/{project_identifier}/spans",
|
|
114
|
+
{
|
|
115
|
+
params: {
|
|
116
|
+
path: {
|
|
117
|
+
project_identifier: project,
|
|
118
|
+
},
|
|
119
|
+
query,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (response.error) throw response.error;
|
|
125
|
+
|
|
126
|
+
const data = response.data?.data ?? [];
|
|
127
|
+
newSpans.push(...(data as SpanData[]));
|
|
128
|
+
totalFetched += data.length;
|
|
129
|
+
|
|
130
|
+
cursor = response.data?.next_cursor ?? null;
|
|
131
|
+
|
|
132
|
+
// Stop if there's no more data
|
|
133
|
+
if (!cursor || data.length === 0) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Combine existing and new spans
|
|
139
|
+
const allSpans = [...existingSpans, ...newSpans];
|
|
140
|
+
|
|
141
|
+
// Write updated spans to JSONL file
|
|
142
|
+
const jsonlContent = allSpans
|
|
143
|
+
.map((span) => JSON.stringify(span))
|
|
144
|
+
.join("\n");
|
|
145
|
+
await mode.writeFile(
|
|
146
|
+
`/phoenix/projects/${project}/spans/index.jsonl`,
|
|
147
|
+
jsonlContent
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Update metadata
|
|
151
|
+
const metadata: SpansMetadata = {
|
|
152
|
+
project,
|
|
153
|
+
spanCount: allSpans.length,
|
|
154
|
+
startTime:
|
|
155
|
+
startTime instanceof Date
|
|
156
|
+
? startTime.toISOString()
|
|
157
|
+
: (startTime ?? null),
|
|
158
|
+
endTime:
|
|
159
|
+
endTime instanceof Date ? endTime.toISOString() : (endTime ?? null),
|
|
160
|
+
snapshotTime: new Date().toISOString(),
|
|
161
|
+
lastCursor: cursor,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
await mode.writeFile(
|
|
165
|
+
`/phoenix/projects/${project}/spans/metadata.json`,
|
|
166
|
+
JSON.stringify(metadata, null, 2)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
console.log(
|
|
170
|
+
`Fetched ${newSpans.length} additional spans for project "${project}"`
|
|
171
|
+
);
|
|
172
|
+
console.log(`Total spans for project: ${allSpans.length}`);
|
|
173
|
+
}, `fetching more spans for project ${project}`);
|
|
174
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { PhoenixClient } from "@arizeai/phoenix-client";
|
|
2
|
+
import type { ExecutionMode } from "../modes/types.js";
|
|
3
|
+
import { withErrorHandling } from "../snapshot/client.js";
|
|
4
|
+
|
|
5
|
+
export interface FetchMoreTraceOptions {
|
|
6
|
+
/** Trace ID to fetch all spans for */
|
|
7
|
+
traceId: string;
|
|
8
|
+
/** Project name to search in */
|
|
9
|
+
project: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SpanData {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
context: {
|
|
16
|
+
trace_id: string;
|
|
17
|
+
span_id: string;
|
|
18
|
+
};
|
|
19
|
+
span_kind: string;
|
|
20
|
+
parent_id: string | null;
|
|
21
|
+
start_time: string;
|
|
22
|
+
end_time: string;
|
|
23
|
+
status_code: string;
|
|
24
|
+
status_message: string;
|
|
25
|
+
attributes: Record<string, unknown>;
|
|
26
|
+
events: Array<unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fetches all spans for a specific trace ID
|
|
31
|
+
*
|
|
32
|
+
* @param client - Phoenix client instance
|
|
33
|
+
* @param mode - Execution mode for file operations
|
|
34
|
+
* @param options - Options for fetching trace
|
|
35
|
+
*/
|
|
36
|
+
export async function fetchMoreTrace(
|
|
37
|
+
client: PhoenixClient,
|
|
38
|
+
mode: ExecutionMode,
|
|
39
|
+
options: FetchMoreTraceOptions
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const { traceId, project } = options;
|
|
42
|
+
|
|
43
|
+
await withErrorHandling(async () => {
|
|
44
|
+
// First, check if the project exists in our snapshot
|
|
45
|
+
const projectsResult = await mode.exec("cat /phoenix/projects/index.jsonl");
|
|
46
|
+
if (projectsResult.exitCode !== 0 || !projectsResult.stdout) {
|
|
47
|
+
console.error("No projects found in snapshot. Run a snapshot first.");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const projectNames = projectsResult.stdout
|
|
52
|
+
.trim()
|
|
53
|
+
.split("\n")
|
|
54
|
+
.filter((line) => line.length > 0)
|
|
55
|
+
.map((line) => JSON.parse(line).name);
|
|
56
|
+
|
|
57
|
+
if (!projectNames.includes(project)) {
|
|
58
|
+
console.error(
|
|
59
|
+
`Project "${project}" not found. Available projects: ${projectNames.join(
|
|
60
|
+
", "
|
|
61
|
+
)}`
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fetch all spans that belong to this trace
|
|
67
|
+
const traceSpans: SpanData[] = [];
|
|
68
|
+
let cursor: string | null = null;
|
|
69
|
+
let totalFetched = 0;
|
|
70
|
+
|
|
71
|
+
console.log(`Fetching trace ${traceId} from project "${project}"...`);
|
|
72
|
+
|
|
73
|
+
// We need to fetch spans in batches and filter by trace_id
|
|
74
|
+
// Since the API doesn't support direct trace_id filtering
|
|
75
|
+
while (true) {
|
|
76
|
+
const query: Record<string, any> = {
|
|
77
|
+
limit: 100, // Fetch in chunks
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (cursor) {
|
|
81
|
+
query.cursor = cursor;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const response = await client.GET(
|
|
85
|
+
"/v1/projects/{project_identifier}/spans",
|
|
86
|
+
{
|
|
87
|
+
params: {
|
|
88
|
+
path: {
|
|
89
|
+
project_identifier: project,
|
|
90
|
+
},
|
|
91
|
+
query,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (response.error) throw response.error;
|
|
97
|
+
|
|
98
|
+
const data = response.data?.data ?? [];
|
|
99
|
+
totalFetched += data.length;
|
|
100
|
+
|
|
101
|
+
// Filter spans that belong to our trace
|
|
102
|
+
const matchingSpans = (data as SpanData[]).filter(
|
|
103
|
+
(span) => span.context.trace_id === traceId
|
|
104
|
+
);
|
|
105
|
+
traceSpans.push(...matchingSpans);
|
|
106
|
+
|
|
107
|
+
cursor = response.data?.next_cursor ?? null;
|
|
108
|
+
|
|
109
|
+
// Stop if we found spans for the trace or no more data
|
|
110
|
+
if (traceSpans.length > 0 || !cursor || data.length === 0) {
|
|
111
|
+
// If we found some spans, continue until we have all spans from the trace
|
|
112
|
+
// This is because trace spans might be spread across multiple pages
|
|
113
|
+
if (traceSpans.length > 0 && cursor && data.length > 0) {
|
|
114
|
+
console.log(
|
|
115
|
+
`Found ${traceSpans.length} spans so far, continuing search...`
|
|
116
|
+
);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Show progress for large datasets
|
|
123
|
+
if (totalFetched % 1000 === 0) {
|
|
124
|
+
console.log(`Searched ${totalFetched} spans so far...`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (traceSpans.length === 0) {
|
|
129
|
+
console.log(
|
|
130
|
+
`No spans found for trace ${traceId} in project "${project}"`
|
|
131
|
+
);
|
|
132
|
+
console.log(
|
|
133
|
+
`Searched through ${totalFetched} spans. The trace might not exist or might be in a different project.`
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Sort spans by start_time to show them in order
|
|
139
|
+
traceSpans.sort(
|
|
140
|
+
(a, b) =>
|
|
141
|
+
new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Write trace spans to a dedicated file
|
|
145
|
+
const jsonlContent = traceSpans
|
|
146
|
+
.map((span) => JSON.stringify(span))
|
|
147
|
+
.join("\n");
|
|
148
|
+
|
|
149
|
+
const traceDir = `/phoenix/traces/${traceId}`;
|
|
150
|
+
await mode.writeFile(`${traceDir}/spans.jsonl`, jsonlContent);
|
|
151
|
+
|
|
152
|
+
// Create trace metadata
|
|
153
|
+
const rootSpan = traceSpans.find((span) => !span.parent_id);
|
|
154
|
+
const firstSpan = traceSpans[0];
|
|
155
|
+
const lastSpan = traceSpans[traceSpans.length - 1];
|
|
156
|
+
const metadata = {
|
|
157
|
+
traceId,
|
|
158
|
+
project,
|
|
159
|
+
spanCount: traceSpans.length,
|
|
160
|
+
rootSpan: rootSpan ? { id: rootSpan.id, name: rootSpan.name } : null,
|
|
161
|
+
startTime: firstSpan?.start_time || null,
|
|
162
|
+
endTime: lastSpan?.end_time || null,
|
|
163
|
+
duration:
|
|
164
|
+
firstSpan && lastSpan
|
|
165
|
+
? new Date(lastSpan.end_time).getTime() -
|
|
166
|
+
new Date(firstSpan.start_time).getTime()
|
|
167
|
+
: 0,
|
|
168
|
+
snapshotTime: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
await mode.writeFile(
|
|
172
|
+
`${traceDir}/metadata.json`,
|
|
173
|
+
JSON.stringify(metadata, null, 2)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
console.log(`\nSuccessfully fetched trace ${traceId}:`);
|
|
177
|
+
console.log(`- Project: ${project}`);
|
|
178
|
+
console.log(`- Spans: ${traceSpans.length}`);
|
|
179
|
+
console.log(`- Root span: ${rootSpan?.name || "Unknown"}`);
|
|
180
|
+
console.log(`- Duration: ${(metadata.duration / 1000).toFixed(2)} seconds`);
|
|
181
|
+
console.log(`\nTrace data saved to: ${traceDir}/`);
|
|
182
|
+
}, `fetching trace ${traceId}`);
|
|
183
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config singleton module for Phoenix Insight CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides centralized configuration management with priority-based merging:
|
|
5
|
+
* 1. Config file (lowest priority)
|
|
6
|
+
* 2. Environment variables
|
|
7
|
+
* 3. CLI arguments (highest priority)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { configSchema, type Config, getDefaultConfig } from "./schema.js";
|
|
11
|
+
import {
|
|
12
|
+
getConfigPath,
|
|
13
|
+
loadConfigFile,
|
|
14
|
+
validateConfig,
|
|
15
|
+
createDefaultConfig,
|
|
16
|
+
setCliConfigPath,
|
|
17
|
+
} from "./loader.js";
|
|
18
|
+
|
|
19
|
+
// Re-export Config type for convenience
|
|
20
|
+
export type { Config } from "./schema.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Module-level storage for the initialized config singleton
|
|
24
|
+
*/
|
|
25
|
+
let configInstance: Config | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* CLI arguments that can override config values
|
|
29
|
+
*/
|
|
30
|
+
export interface CliArgs {
|
|
31
|
+
/** Custom config file path */
|
|
32
|
+
config?: string;
|
|
33
|
+
/** Phoenix server base URL */
|
|
34
|
+
baseUrl?: string;
|
|
35
|
+
/** Phoenix API key */
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
/** Maximum spans to fetch per project */
|
|
38
|
+
limit?: number;
|
|
39
|
+
/** Enable streaming responses */
|
|
40
|
+
stream?: boolean;
|
|
41
|
+
/** Execution mode: sandbox or local */
|
|
42
|
+
local?: boolean;
|
|
43
|
+
/** Force refresh of snapshot data */
|
|
44
|
+
refresh?: boolean;
|
|
45
|
+
/** Enable tracing */
|
|
46
|
+
trace?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Environment variable mappings to config keys
|
|
51
|
+
*/
|
|
52
|
+
const ENV_VAR_MAPPINGS: Record<string, keyof Config> = {
|
|
53
|
+
PHOENIX_BASE_URL: "baseUrl",
|
|
54
|
+
PHOENIX_API_KEY: "apiKey",
|
|
55
|
+
PHOENIX_INSIGHT_LIMIT: "limit",
|
|
56
|
+
PHOENIX_INSIGHT_STREAM: "stream",
|
|
57
|
+
PHOENIX_INSIGHT_MODE: "mode",
|
|
58
|
+
PHOENIX_INSIGHT_REFRESH: "refresh",
|
|
59
|
+
PHOENIX_INSIGHT_TRACE: "trace",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse environment variable value to appropriate type
|
|
64
|
+
*/
|
|
65
|
+
function parseEnvValue(
|
|
66
|
+
key: keyof Config,
|
|
67
|
+
value: string
|
|
68
|
+
): string | number | boolean | undefined {
|
|
69
|
+
switch (key) {
|
|
70
|
+
case "baseUrl":
|
|
71
|
+
case "apiKey":
|
|
72
|
+
return value;
|
|
73
|
+
case "limit":
|
|
74
|
+
const num = parseInt(value, 10);
|
|
75
|
+
return isNaN(num) ? undefined : num;
|
|
76
|
+
case "stream":
|
|
77
|
+
case "refresh":
|
|
78
|
+
case "trace":
|
|
79
|
+
return value.toLowerCase() === "true" || value === "1";
|
|
80
|
+
case "mode":
|
|
81
|
+
if (value === "sandbox" || value === "local") {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
default:
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get config values from environment variables
|
|
92
|
+
*/
|
|
93
|
+
function getEnvConfig(): Partial<Config> {
|
|
94
|
+
const envConfig: Partial<Config> = {};
|
|
95
|
+
|
|
96
|
+
for (const [envVar, configKey] of Object.entries(ENV_VAR_MAPPINGS)) {
|
|
97
|
+
const value = process.env[envVar];
|
|
98
|
+
if (value !== undefined) {
|
|
99
|
+
const parsed = parseEnvValue(configKey, value);
|
|
100
|
+
if (parsed !== undefined) {
|
|
101
|
+
(envConfig as any)[configKey] = parsed;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return envConfig;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert CLI args to config format
|
|
111
|
+
*/
|
|
112
|
+
function cliArgsToConfig(cliArgs: CliArgs): Partial<Config> {
|
|
113
|
+
const config: Partial<Config> = {};
|
|
114
|
+
|
|
115
|
+
if (cliArgs.baseUrl !== undefined) {
|
|
116
|
+
config.baseUrl = cliArgs.baseUrl;
|
|
117
|
+
}
|
|
118
|
+
if (cliArgs.apiKey !== undefined) {
|
|
119
|
+
config.apiKey = cliArgs.apiKey;
|
|
120
|
+
}
|
|
121
|
+
if (cliArgs.limit !== undefined) {
|
|
122
|
+
config.limit = cliArgs.limit;
|
|
123
|
+
}
|
|
124
|
+
if (cliArgs.stream !== undefined) {
|
|
125
|
+
config.stream = cliArgs.stream;
|
|
126
|
+
}
|
|
127
|
+
if (cliArgs.local !== undefined) {
|
|
128
|
+
// CLI uses --local flag, config uses mode
|
|
129
|
+
config.mode = cliArgs.local ? "local" : "sandbox";
|
|
130
|
+
}
|
|
131
|
+
if (cliArgs.refresh !== undefined) {
|
|
132
|
+
config.refresh = cliArgs.refresh;
|
|
133
|
+
}
|
|
134
|
+
if (cliArgs.trace !== undefined) {
|
|
135
|
+
config.trace = cliArgs.trace;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Initialize the configuration singleton
|
|
143
|
+
*
|
|
144
|
+
* Merges configuration from multiple sources with the following priority:
|
|
145
|
+
* 1. Config file (lowest priority)
|
|
146
|
+
* 2. Environment variables
|
|
147
|
+
* 3. CLI arguments (highest priority)
|
|
148
|
+
*
|
|
149
|
+
* @param cliArgs - CLI arguments from Commander
|
|
150
|
+
* @returns The initialized configuration
|
|
151
|
+
*/
|
|
152
|
+
export async function initializeConfig(cliArgs: CliArgs = {}): Promise<Config> {
|
|
153
|
+
// Set CLI config path if provided (for getConfigPath to use)
|
|
154
|
+
if (cliArgs.config) {
|
|
155
|
+
setCliConfigPath(cliArgs.config);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Get config file path
|
|
159
|
+
const { path: configPath, isDefault } = getConfigPath();
|
|
160
|
+
|
|
161
|
+
// Try to create default config if it doesn't exist (only for default path)
|
|
162
|
+
if (isDefault) {
|
|
163
|
+
await createDefaultConfig(configPath, isDefault);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Load config file
|
|
167
|
+
const fileConfig = await loadConfigFile(configPath);
|
|
168
|
+
|
|
169
|
+
// Validate file config (returns defaults if null/invalid)
|
|
170
|
+
const validatedFileConfig = validateConfig(fileConfig);
|
|
171
|
+
|
|
172
|
+
// Get environment config
|
|
173
|
+
const envConfig = getEnvConfig();
|
|
174
|
+
|
|
175
|
+
// Get CLI config
|
|
176
|
+
const cliConfig = cliArgsToConfig(cliArgs);
|
|
177
|
+
|
|
178
|
+
// Merge configs: file < env < cli
|
|
179
|
+
const mergedConfig = {
|
|
180
|
+
...validatedFileConfig,
|
|
181
|
+
...envConfig,
|
|
182
|
+
...cliConfig,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Final validation with Zod
|
|
186
|
+
const result = configSchema.safeParse(mergedConfig);
|
|
187
|
+
|
|
188
|
+
if (result.success) {
|
|
189
|
+
configInstance = result.data;
|
|
190
|
+
} else {
|
|
191
|
+
// Log validation issues as warnings
|
|
192
|
+
result.error.issues.forEach((issue) => {
|
|
193
|
+
console.warn(
|
|
194
|
+
`Warning: Config validation error at '${issue.path.join(".")}': ${issue.message}`
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
// Fall back to defaults
|
|
198
|
+
configInstance = getDefaultConfig();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return configInstance;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get the initialized configuration
|
|
206
|
+
*
|
|
207
|
+
* @throws Error if config has not been initialized via initializeConfig()
|
|
208
|
+
* @returns The configuration object
|
|
209
|
+
*/
|
|
210
|
+
export function getConfig(): Config {
|
|
211
|
+
if (configInstance === null) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
"Config not initialized. Call initializeConfig() first before using getConfig()."
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return configInstance;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Reset the config singleton (useful for testing)
|
|
221
|
+
*/
|
|
222
|
+
export function resetConfig(): void {
|
|
223
|
+
configInstance = null;
|
|
224
|
+
setCliConfigPath(undefined);
|
|
225
|
+
}
|