@onyx-robotics/agent 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 +202 -0
- package/README.md +72 -0
- package/bin/onyx.ts +4 -0
- package/package.json +52 -0
- package/scripts/install.sh +115 -0
- package/skills/onyx/SKILL.md +150 -0
- package/src/commands/agent.ts +23 -0
- package/src/commands/branch.ts +96 -0
- package/src/commands/exp.ts +432 -0
- package/src/commands/listen.ts +327 -0
- package/src/commands/login.ts +198 -0
- package/src/commands/profile.ts +112 -0
- package/src/commands/sync.ts +88 -0
- package/src/install.test.ts +38 -0
- package/src/lib/api.ts +227 -0
- package/src/lib/args.ts +68 -0
- package/src/lib/config.ts +148 -0
- package/src/lib/events.ts +97 -0
- package/src/lib/git.ts +57 -0
- package/src/lib/history.ts +272 -0
- package/src/lib/login.ts +233 -0
- package/src/lib/markdown.ts +148 -0
- package/src/lib/metrics.ts +41 -0
- package/src/lib/outbox.ts +173 -0
- package/src/lib/process.ts +73 -0
- package/src/lib/project.ts +42 -0
- package/src/lib/skill-content.ts +1 -0
- package/src/lib/skill.ts +50 -0
- package/src/lib/sync.ts +294 -0
- package/src/lib/tui.ts +364 -0
- package/src/main.ts +84 -0
- package/src/onyx.test.ts +952 -0
- package/src/onyx.ts +92 -0
- package/src/profile.test.ts +472 -0
- package/src/protocol/index.ts +2 -0
- package/src/protocol/local-research.ts +152 -0
- package/src/protocol/research.ts +75 -0
package/src/onyx.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Public surface barrel: keeps `bin/onyx.ts` and tests importing from "./onyx"
|
|
2
|
+
// while the implementation lives in focused modules under lib/ and commands/.
|
|
3
|
+
export { parseArgs, type Args } from "./lib/args"
|
|
4
|
+
export { parseMetricLines, primaryMetric, summarizeOutput } from "./lib/metrics"
|
|
5
|
+
export {
|
|
6
|
+
branchMetadata,
|
|
7
|
+
resolveBranchName,
|
|
8
|
+
type BranchMetadata,
|
|
9
|
+
type MetricDirection,
|
|
10
|
+
} from "./lib/markdown"
|
|
11
|
+
export {
|
|
12
|
+
branchStateKey,
|
|
13
|
+
normalizeProjectPath,
|
|
14
|
+
onyxPath,
|
|
15
|
+
resolveProjectPath,
|
|
16
|
+
scopedRoot,
|
|
17
|
+
} from "./lib/project"
|
|
18
|
+
export {
|
|
19
|
+
appendOutbox,
|
|
20
|
+
clearLastRun,
|
|
21
|
+
clientRunRef,
|
|
22
|
+
lastRunPath,
|
|
23
|
+
onyxStateDir,
|
|
24
|
+
outboxPath,
|
|
25
|
+
readLastRun,
|
|
26
|
+
readOutbox,
|
|
27
|
+
readState,
|
|
28
|
+
rewriteOutbox,
|
|
29
|
+
statePath,
|
|
30
|
+
writeLastRun,
|
|
31
|
+
writeState,
|
|
32
|
+
type CliState,
|
|
33
|
+
type LastRunRecord,
|
|
34
|
+
} from "./lib/outbox"
|
|
35
|
+
export {
|
|
36
|
+
appendHistory,
|
|
37
|
+
apiExperimentToHistory,
|
|
38
|
+
applyHistorySyncUpdates,
|
|
39
|
+
experimentRecordToHistory,
|
|
40
|
+
historyPath,
|
|
41
|
+
hydrateHistoryFromApi,
|
|
42
|
+
mergeHistory,
|
|
43
|
+
readHistory,
|
|
44
|
+
rewriteHistory,
|
|
45
|
+
type HistorySyncUpdate,
|
|
46
|
+
type HydrateResult,
|
|
47
|
+
} from "./lib/history"
|
|
48
|
+
export { emitEvent, eventsPath, readEvents, truncateEvents } from "./lib/events"
|
|
49
|
+
export {
|
|
50
|
+
formatAge,
|
|
51
|
+
formatMetricCell,
|
|
52
|
+
formatMetricValue,
|
|
53
|
+
pad,
|
|
54
|
+
renderExperimentTable,
|
|
55
|
+
renderFrame,
|
|
56
|
+
spinnerChar,
|
|
57
|
+
stripAnsi,
|
|
58
|
+
truncate,
|
|
59
|
+
truncateAnsi,
|
|
60
|
+
type ExperimentRow,
|
|
61
|
+
type ListenModel,
|
|
62
|
+
} from "./lib/tui"
|
|
63
|
+
export { flushOutbox, type FlushResult } from "./lib/sync"
|
|
64
|
+
export {
|
|
65
|
+
gitBranchForName,
|
|
66
|
+
currentBranch,
|
|
67
|
+
currentCommit,
|
|
68
|
+
gitDir,
|
|
69
|
+
nameFromGitBranch,
|
|
70
|
+
normalizeRepositoryUrl,
|
|
71
|
+
repoRoot,
|
|
72
|
+
} from "./lib/git"
|
|
73
|
+
export {
|
|
74
|
+
defaultSkillInstallRoot,
|
|
75
|
+
displaySkillPath,
|
|
76
|
+
installOnyxSkill,
|
|
77
|
+
packagedSkillPath,
|
|
78
|
+
} from "./lib/skill"
|
|
79
|
+
export * from "./protocol"
|
|
80
|
+
export { commandAgent } from "./commands/agent"
|
|
81
|
+
export { commandLogin } from "./commands/login"
|
|
82
|
+
export {
|
|
83
|
+
commandProfile,
|
|
84
|
+
commandProfileList,
|
|
85
|
+
commandProfileSetApiKeyEnv,
|
|
86
|
+
commandProfileUse,
|
|
87
|
+
} from "./commands/profile"
|
|
88
|
+
export { commandBranchCreate } from "./commands/branch"
|
|
89
|
+
export { commandExpList, commandExpLog, commandExpRun } from "./commands/exp"
|
|
90
|
+
export { commandListen } from "./commands/listen"
|
|
91
|
+
export { commandPush, commandStatus, commandSync } from "./commands/sync"
|
|
92
|
+
export { USAGE, main } from "./main"
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { mkdtemp } from "node:fs/promises"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, test } from "bun:test"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_API_URL,
|
|
9
|
+
apiBaseUrl,
|
|
10
|
+
apiKey,
|
|
11
|
+
readConfig,
|
|
12
|
+
writeConfig,
|
|
13
|
+
type CliProfile,
|
|
14
|
+
} from "./lib/config"
|
|
15
|
+
import { cliLoginCompleteHtml } from "./lib/login"
|
|
16
|
+
import {
|
|
17
|
+
buildCliLoginUrl,
|
|
18
|
+
loginProfileManifest,
|
|
19
|
+
profileNameForTeam,
|
|
20
|
+
saveLoginProfile,
|
|
21
|
+
} from "./commands/login"
|
|
22
|
+
import {
|
|
23
|
+
commandProfileList,
|
|
24
|
+
commandProfileSetApiKeyEnv,
|
|
25
|
+
commandProfileUse,
|
|
26
|
+
} from "./commands/profile"
|
|
27
|
+
|
|
28
|
+
const ALPHA_API_KEY_ENV = "ONYX_ALPHA_API_KEY"
|
|
29
|
+
const BETA_API_KEY_ENV = "ONYX_BETA_API_KEY"
|
|
30
|
+
|
|
31
|
+
function profile(overrides: Partial<CliProfile> = {}): CliProfile {
|
|
32
|
+
return {
|
|
33
|
+
apiUrl: "https://app.onyx.test",
|
|
34
|
+
apiKey: "onyx_key",
|
|
35
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
36
|
+
teamName: "Alpha Team",
|
|
37
|
+
updatedAt: "2026-06-06T12:00:00.000Z",
|
|
38
|
+
...overrides,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function captureLogs(fn: () => Promise<void>) {
|
|
43
|
+
const previous = console.log
|
|
44
|
+
const logs: string[] = []
|
|
45
|
+
console.log = (...args: unknown[]) => {
|
|
46
|
+
logs.push(args.map(String).join(" "))
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await fn()
|
|
50
|
+
} finally {
|
|
51
|
+
console.log = previous
|
|
52
|
+
}
|
|
53
|
+
return logs.join("\n")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("CLI profiles", () => {
|
|
57
|
+
beforeEach(async () => {
|
|
58
|
+
process.env.XDG_CONFIG_HOME = await mkdtemp(
|
|
59
|
+
join(tmpdir(), "onyx-cli-config-test-")
|
|
60
|
+
)
|
|
61
|
+
delete process.env.ONYX_API_KEY
|
|
62
|
+
delete process.env.ONYX_API_URL
|
|
63
|
+
delete process.env[ALPHA_API_KEY_ENV]
|
|
64
|
+
delete process.env[BETA_API_KEY_ENV]
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("stores login results as the current profile", async () => {
|
|
68
|
+
const saved = await saveLoginProfile({
|
|
69
|
+
baseUrl: "http://localhost:3000",
|
|
70
|
+
result: {
|
|
71
|
+
apiKey: "onyx_secret",
|
|
72
|
+
apiKeyId: "44444444-4444-4444-8444-444444444444",
|
|
73
|
+
apiUrl: "https://app.onyx.test",
|
|
74
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
75
|
+
teamName: "Alpha Team",
|
|
76
|
+
alreadyConfigured: false,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(saved).toEqual({
|
|
81
|
+
profileName: "alpha",
|
|
82
|
+
alreadyConfigured: false,
|
|
83
|
+
})
|
|
84
|
+
const config = await readConfig()
|
|
85
|
+
expect(config.currentProfile).toBe("alpha")
|
|
86
|
+
expect(config.profiles.alpha).toMatchObject({
|
|
87
|
+
apiUrl: "https://app.onyx.test",
|
|
88
|
+
apiKey: "onyx_secret",
|
|
89
|
+
apiKeyId: "44444444-4444-4444-8444-444444444444",
|
|
90
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
91
|
+
teamName: "Alpha Team",
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("builds the browser login URL with a non-secret profile manifest", async () => {
|
|
96
|
+
await writeConfig({
|
|
97
|
+
currentProfile: "alpha",
|
|
98
|
+
profiles: {
|
|
99
|
+
alpha: profile({
|
|
100
|
+
apiKey: "alpha-secret",
|
|
101
|
+
apiKeyId: "44444444-4444-4444-8444-444444444444",
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const config = await readConfig()
|
|
107
|
+
const loginUrl = buildCliLoginUrl({
|
|
108
|
+
baseUrl: "https://app.onyx.test",
|
|
109
|
+
redirectUri: "http://127.0.0.1:8765/callback",
|
|
110
|
+
state: "state-123",
|
|
111
|
+
profiles: loginProfileManifest(config),
|
|
112
|
+
refresh: true,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(loginUrl.pathname).toBe("/cli/login")
|
|
116
|
+
expect(loginUrl.searchParams.get("redirect_uri")).toBe(
|
|
117
|
+
"http://127.0.0.1:8765/callback"
|
|
118
|
+
)
|
|
119
|
+
expect(loginUrl.searchParams.get("refresh")).toBe("true")
|
|
120
|
+
const manifest = JSON.parse(
|
|
121
|
+
Buffer.from(
|
|
122
|
+
loginUrl.searchParams.get("profiles") ?? "",
|
|
123
|
+
"base64url"
|
|
124
|
+
).toString("utf8")
|
|
125
|
+
)
|
|
126
|
+
expect(manifest).toEqual([
|
|
127
|
+
{
|
|
128
|
+
profileName: "alpha",
|
|
129
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
130
|
+
apiUrl: "https://app.onyx.test",
|
|
131
|
+
apiKeyId: "44444444-4444-4444-8444-444444444444",
|
|
132
|
+
},
|
|
133
|
+
])
|
|
134
|
+
expect(JSON.stringify(manifest)).not.toContain("alpha-secret")
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test("renders a branded login completion page", () => {
|
|
138
|
+
const html = cliLoginCompleteHtml()
|
|
139
|
+
|
|
140
|
+
expect(html).toContain("Onyx CLI login complete. You can close this tab.")
|
|
141
|
+
expect(html).toContain("<span>Onyx</span>")
|
|
142
|
+
expect(html).toContain('<section class="card"')
|
|
143
|
+
expect(html).toContain("--background: oklch(0.96 0 0)")
|
|
144
|
+
expect(html).toContain("width: 60px")
|
|
145
|
+
expect(html).toContain("font-size: 48px")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test("generates first-word profile names and handles empty names", () => {
|
|
149
|
+
expect(profileNameForTeam("Acme Research Lab")).toBe("acme")
|
|
150
|
+
expect(profileNameForTeam("Acme-Research Lab")).toBe("acme-research")
|
|
151
|
+
expect(profileNameForTeam("!!!")).toBe("team")
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test("reuses existing team and origin profiles", async () => {
|
|
155
|
+
await writeConfig({
|
|
156
|
+
currentProfile: "custom",
|
|
157
|
+
profiles: {
|
|
158
|
+
custom: profile({
|
|
159
|
+
apiKey: "old-key",
|
|
160
|
+
teamName: "Original Team",
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const saved = await saveLoginProfile({
|
|
166
|
+
baseUrl: "https://app.onyx.test",
|
|
167
|
+
result: {
|
|
168
|
+
apiKey: "new-key",
|
|
169
|
+
apiUrl: "https://app.onyx.test",
|
|
170
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
171
|
+
teamName: "Renamed Team",
|
|
172
|
+
alreadyConfigured: false,
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(saved.profileName).toBe("custom")
|
|
177
|
+
const config = await readConfig()
|
|
178
|
+
expect(config.profiles.custom?.apiKey).toBe("new-key")
|
|
179
|
+
expect(config.profiles.custom?.teamName).toBe("Renamed Team")
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("suffixes generated profile names when another team owns the name", async () => {
|
|
183
|
+
await writeConfig({
|
|
184
|
+
currentProfile: "alpha",
|
|
185
|
+
profiles: {
|
|
186
|
+
alpha: profile({
|
|
187
|
+
teamId: "33333333-3333-4333-8333-333333333333",
|
|
188
|
+
teamName: "Alpha Other",
|
|
189
|
+
}),
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const saved = await saveLoginProfile({
|
|
194
|
+
baseUrl: "https://app.onyx.test",
|
|
195
|
+
result: {
|
|
196
|
+
apiKey: "new-key",
|
|
197
|
+
apiUrl: "https://app.onyx.test",
|
|
198
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
199
|
+
teamName: "Alpha Team",
|
|
200
|
+
alreadyConfigured: false,
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
expect(saved.profileName).toBe("alpha-2")
|
|
205
|
+
expect((await readConfig()).currentProfile).toBe("alpha-2")
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("selects already configured profiles without overwriting credentials", async () => {
|
|
209
|
+
await writeConfig({
|
|
210
|
+
currentProfile: "beta",
|
|
211
|
+
profiles: {
|
|
212
|
+
alpha: profile({ apiKey: "old-key" }),
|
|
213
|
+
beta: profile({
|
|
214
|
+
apiKey: "beta-key",
|
|
215
|
+
teamId: "33333333-3333-4333-8333-333333333333",
|
|
216
|
+
teamName: "Beta Team",
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const saved = await saveLoginProfile({
|
|
222
|
+
baseUrl: "https://app.onyx.test",
|
|
223
|
+
result: {
|
|
224
|
+
apiUrl: "https://app.onyx.test",
|
|
225
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
226
|
+
teamName: "Alpha Team",
|
|
227
|
+
profileName: "alpha",
|
|
228
|
+
alreadyConfigured: true,
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const config = await readConfig()
|
|
233
|
+
expect(saved).toEqual({
|
|
234
|
+
profileName: "alpha",
|
|
235
|
+
alreadyConfigured: true,
|
|
236
|
+
})
|
|
237
|
+
expect(config.currentProfile).toBe("alpha")
|
|
238
|
+
expect(config.profiles.alpha?.apiKey).toBe("old-key")
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test("refresh replaces env-backed profile credentials with the returned stored key", async () => {
|
|
242
|
+
await writeConfig({
|
|
243
|
+
currentProfile: "alpha",
|
|
244
|
+
profiles: {
|
|
245
|
+
alpha: profile({
|
|
246
|
+
apiKey: undefined,
|
|
247
|
+
apiKeyEnv: ALPHA_API_KEY_ENV,
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
await saveLoginProfile({
|
|
253
|
+
baseUrl: "https://app.onyx.test",
|
|
254
|
+
result: {
|
|
255
|
+
apiKey: "new-stored-key",
|
|
256
|
+
apiKeyId: "44444444-4444-4444-8444-444444444444",
|
|
257
|
+
apiUrl: "https://app.onyx.test",
|
|
258
|
+
teamId: "22222222-2222-4222-8222-222222222222",
|
|
259
|
+
teamName: "Alpha Team",
|
|
260
|
+
alreadyConfigured: false,
|
|
261
|
+
},
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const alpha = (await readConfig()).profiles.alpha
|
|
265
|
+
expect(alpha?.apiKey).toBe("new-stored-key")
|
|
266
|
+
expect(alpha?.apiKeyId).toBe("44444444-4444-4444-8444-444444444444")
|
|
267
|
+
expect(alpha?.apiKeyEnv).toBeUndefined()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test("resolves current and overridden profiles for API calls", async () => {
|
|
271
|
+
await writeConfig({
|
|
272
|
+
currentProfile: "alpha",
|
|
273
|
+
profiles: {
|
|
274
|
+
alpha: profile({ apiKey: "alpha-key" }),
|
|
275
|
+
beta: profile({
|
|
276
|
+
apiUrl: "https://beta.onyx.test",
|
|
277
|
+
apiKey: "beta-key",
|
|
278
|
+
teamName: "Beta Team",
|
|
279
|
+
}),
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
expect(await apiBaseUrl()).toBe("https://app.onyx.test")
|
|
284
|
+
expect(await apiKey()).toBe("alpha-key")
|
|
285
|
+
expect(
|
|
286
|
+
await apiBaseUrl({
|
|
287
|
+
positional: ["status"],
|
|
288
|
+
options: { profile: "beta" },
|
|
289
|
+
})
|
|
290
|
+
).toBe("https://beta.onyx.test")
|
|
291
|
+
expect(
|
|
292
|
+
await apiKey({
|
|
293
|
+
positional: ["status"],
|
|
294
|
+
options: { profile: "beta" },
|
|
295
|
+
})
|
|
296
|
+
).toBe("beta-key")
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test("uses environment credentials before profiles", async () => {
|
|
300
|
+
process.env.ONYX_API_KEY = "env-key"
|
|
301
|
+
process.env.ONYX_API_URL = "https://env.onyx.test"
|
|
302
|
+
|
|
303
|
+
expect(await apiBaseUrl()).toBe("https://env.onyx.test")
|
|
304
|
+
expect(await apiKey()).toBe("env-key")
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test("defaults API URL to the hosted app for standalone installs", async () => {
|
|
308
|
+
process.env.ONYX_API_KEY = "env-key"
|
|
309
|
+
|
|
310
|
+
expect(await apiBaseUrl()).toBe(DEFAULT_API_URL)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test("resolves profile API keys from apiKeyEnv before stored profile keys", async () => {
|
|
314
|
+
process.env[ALPHA_API_KEY_ENV] = "alpha-env-key"
|
|
315
|
+
await writeConfig({
|
|
316
|
+
currentProfile: "alpha",
|
|
317
|
+
profiles: {
|
|
318
|
+
alpha: profile({
|
|
319
|
+
apiKey: "alpha-stored-key",
|
|
320
|
+
apiKeyEnv: ALPHA_API_KEY_ENV,
|
|
321
|
+
}),
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
expect(await apiKey()).toBe("alpha-env-key")
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("does not fall back to a stored key when apiKeyEnv is missing", async () => {
|
|
329
|
+
await writeConfig({
|
|
330
|
+
currentProfile: "alpha",
|
|
331
|
+
profiles: {
|
|
332
|
+
alpha: profile({
|
|
333
|
+
apiKey: "alpha-stored-key",
|
|
334
|
+
apiKeyEnv: ALPHA_API_KEY_ENV,
|
|
335
|
+
}),
|
|
336
|
+
},
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
await expect(apiKey()).rejects.toThrow(
|
|
340
|
+
'Profile "alpha" expects API key in ONYX_ALPHA_API_KEY'
|
|
341
|
+
)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test("treats an empty profile API key environment variable as missing", async () => {
|
|
345
|
+
process.env[ALPHA_API_KEY_ENV] = " "
|
|
346
|
+
await writeConfig({
|
|
347
|
+
currentProfile: "alpha",
|
|
348
|
+
profiles: {
|
|
349
|
+
alpha: profile({
|
|
350
|
+
apiKey: "alpha-stored-key",
|
|
351
|
+
apiKeyEnv: ALPHA_API_KEY_ENV,
|
|
352
|
+
}),
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
await expect(apiKey()).rejects.toThrow(
|
|
357
|
+
'Profile "alpha" expects API key in ONYX_ALPHA_API_KEY'
|
|
358
|
+
)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test("global environment API key overrides a missing profile apiKeyEnv", async () => {
|
|
362
|
+
process.env.ONYX_API_KEY = "global-env-key"
|
|
363
|
+
await writeConfig({
|
|
364
|
+
currentProfile: "alpha",
|
|
365
|
+
profiles: {
|
|
366
|
+
alpha: profile({
|
|
367
|
+
apiKey: "alpha-stored-key",
|
|
368
|
+
apiKeyEnv: ALPHA_API_KEY_ENV,
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
expect(await apiKey()).toBe("global-env-key")
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test("lists profiles and switches the active profile", async () => {
|
|
377
|
+
await writeConfig({
|
|
378
|
+
currentProfile: "alpha",
|
|
379
|
+
profiles: {
|
|
380
|
+
alpha: profile({ apiKey: "alpha-key" }),
|
|
381
|
+
beta: profile({
|
|
382
|
+
apiKey: "beta-key",
|
|
383
|
+
teamId: "33333333-3333-4333-8333-333333333333",
|
|
384
|
+
teamName: "Beta Team",
|
|
385
|
+
}),
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const output = await captureLogs(commandProfileList)
|
|
390
|
+
expect(output).toContain("* alpha")
|
|
391
|
+
expect(output).toContain("stored key")
|
|
392
|
+
expect(output).toContain(" beta")
|
|
393
|
+
|
|
394
|
+
await captureLogs(() =>
|
|
395
|
+
commandProfileUse({
|
|
396
|
+
positional: ["profile", "use", "beta"],
|
|
397
|
+
options: {},
|
|
398
|
+
})
|
|
399
|
+
)
|
|
400
|
+
expect((await readConfig()).currentProfile).toBe("beta")
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
test("lists env-backed credential sources", async () => {
|
|
404
|
+
process.env[ALPHA_API_KEY_ENV] = "alpha-env-key"
|
|
405
|
+
await writeConfig({
|
|
406
|
+
currentProfile: "alpha",
|
|
407
|
+
profiles: {
|
|
408
|
+
alpha: profile({ apiKeyEnv: ALPHA_API_KEY_ENV }),
|
|
409
|
+
beta: profile({
|
|
410
|
+
apiKey: undefined,
|
|
411
|
+
apiKeyEnv: BETA_API_KEY_ENV,
|
|
412
|
+
teamName: "Beta Team",
|
|
413
|
+
}),
|
|
414
|
+
},
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const output = await captureLogs(commandProfileList)
|
|
418
|
+
expect(output).toContain("env:ONYX_ALPHA_API_KEY (set)")
|
|
419
|
+
expect(output).toContain("env:ONYX_BETA_API_KEY (missing)")
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
test("sets a profile API key environment variable and removes stored key", async () => {
|
|
423
|
+
await writeConfig({
|
|
424
|
+
currentProfile: "alpha",
|
|
425
|
+
profiles: {
|
|
426
|
+
alpha: profile({ apiKey: "alpha-stored-key" }),
|
|
427
|
+
},
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
await captureLogs(() =>
|
|
431
|
+
commandProfileSetApiKeyEnv({
|
|
432
|
+
positional: ["profile", "set-api-key-env", "alpha", ALPHA_API_KEY_ENV],
|
|
433
|
+
options: {},
|
|
434
|
+
})
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
const config = await readConfig()
|
|
438
|
+
const alpha = config.profiles.alpha
|
|
439
|
+
expect(alpha).toBeDefined()
|
|
440
|
+
expect(alpha?.apiKey).toBeUndefined()
|
|
441
|
+
expect(alpha?.apiKeyEnv).toBe(ALPHA_API_KEY_ENV)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
test("rejects unsafe API key environment variable names", async () => {
|
|
445
|
+
await writeConfig({
|
|
446
|
+
currentProfile: "alpha",
|
|
447
|
+
profiles: {
|
|
448
|
+
alpha: profile({ apiKey: "alpha-stored-key" }),
|
|
449
|
+
},
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
await expect(
|
|
453
|
+
commandProfileSetApiKeyEnv({
|
|
454
|
+
positional: ["profile", "set-api-key-env", "alpha", "onyx_alpha"],
|
|
455
|
+
options: {},
|
|
456
|
+
})
|
|
457
|
+
).rejects.toThrow("Environment variable name must match")
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test("errors when a selected profile is missing", async () => {
|
|
461
|
+
await expect(apiKey()).rejects.toThrow("No Onyx CLI profile selected")
|
|
462
|
+
await expect(
|
|
463
|
+
apiKey({ positional: ["status"], options: { profile: "missing" } })
|
|
464
|
+
).rejects.toThrow('Unknown Onyx CLI profile "missing"')
|
|
465
|
+
await expect(
|
|
466
|
+
commandProfileUse({
|
|
467
|
+
positional: ["profile", "use", "missing"],
|
|
468
|
+
options: {},
|
|
469
|
+
})
|
|
470
|
+
).rejects.toThrow('Unknown Onyx CLI profile "missing"')
|
|
471
|
+
})
|
|
472
|
+
})
|