feat(cli): add PostHog tracking and improve runner hooks
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -23,8 +23,11 @@ import {
|
|||||||
validateNonTuiArgs,
|
validateNonTuiArgs,
|
||||||
} from "./install-validators"
|
} from "./install-validators"
|
||||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||||
|
import { createCliPostHog, getPostHogDistinctId } from "../shared/posthog"
|
||||||
|
|
||||||
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
||||||
|
const posthog = createCliPostHog()
|
||||||
|
const distinctId = getPostHogDistinctId()
|
||||||
const validation = validateNonTuiArgs(args)
|
const validation = validateNonTuiArgs(args)
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
printHeader(false)
|
printHeader(false)
|
||||||
@@ -62,6 +65,8 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
|||||||
const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion)
|
const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion)
|
||||||
if (unsupportedVersionMessage) {
|
if (unsupportedVersionMessage) {
|
||||||
printWarning(unsupportedVersionMessage)
|
printWarning(unsupportedVersionMessage)
|
||||||
|
posthog.capture({ distinctId, event: "install_failed", properties: { reason: "unsupported_opencode_version", is_update: isUpdate } })
|
||||||
|
await posthog.shutdown()
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +82,8 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
|||||||
const pluginResult = await addPluginToOpenCodeConfig(version)
|
const pluginResult = await addPluginToOpenCodeConfig(version)
|
||||||
if (!pluginResult.success) {
|
if (!pluginResult.success) {
|
||||||
printError(`Failed: ${pluginResult.error}`)
|
printError(`Failed: ${pluginResult.error}`)
|
||||||
|
posthog.capture({ distinctId, event: "install_failed", properties: { reason: "plugin_config_write_failed", is_update: isUpdate } })
|
||||||
|
await posthog.shutdown()
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
printSuccess(
|
printSuccess(
|
||||||
@@ -87,6 +94,8 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
|||||||
const omoResult = writeOmoConfig(config)
|
const omoResult = writeOmoConfig(config)
|
||||||
if (!omoResult.success) {
|
if (!omoResult.success) {
|
||||||
printError(`Failed: ${omoResult.error}`)
|
printError(`Failed: ${omoResult.error}`)
|
||||||
|
posthog.capture({ distinctId, event: "install_failed", properties: { reason: "omo_config_write_failed", is_update: isUpdate } })
|
||||||
|
await posthog.shutdown()
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
|
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
|
||||||
@@ -129,6 +138,20 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
|||||||
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
||||||
console.log()
|
console.log()
|
||||||
|
|
||||||
|
posthog.capture({
|
||||||
|
distinctId,
|
||||||
|
event: "install_completed",
|
||||||
|
properties: {
|
||||||
|
is_update: isUpdate,
|
||||||
|
has_claude: config.hasClaude,
|
||||||
|
has_openai: config.hasOpenAI,
|
||||||
|
has_gemini: config.hasGemini,
|
||||||
|
has_copilot: config.hasCopilot,
|
||||||
|
has_opencode_zen: config.hasOpencodeZen,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await posthog.shutdown()
|
||||||
|
|
||||||
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||||
printBox(
|
printBox(
|
||||||
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
|
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test"
|
import { describe, it, expect, spyOn, beforeEach, afterEach, mock } from "bun:test"
|
||||||
import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hide"
|
import * as spawnWithWindowsHideModule from "../../shared/spawn-with-windows-hide"
|
||||||
import * as loggerModule from "../../shared/logger"
|
import * as loggerModule from "../../shared/logger"
|
||||||
import { executeOnCompleteHook } from "./on-complete-hook"
|
|
||||||
|
type OnCompleteHookModule = typeof import("./on-complete-hook")
|
||||||
|
|
||||||
describe("executeOnCompleteHook", () => {
|
describe("executeOnCompleteHook", () => {
|
||||||
let originalPlatform: NodeJS.Platform
|
let originalPlatform: NodeJS.Platform
|
||||||
@@ -31,16 +32,27 @@ describe("executeOnCompleteHook", () => {
|
|||||||
} satisfies ReturnType<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
} satisfies ReturnType<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
||||||
}
|
}
|
||||||
|
|
||||||
let logSpy: ReturnType<typeof spyOn<typeof loggerModule, "log">>
|
let logCalls: Array<Parameters<typeof loggerModule.log>>
|
||||||
|
|
||||||
|
async function importFreshExecuteOnCompleteHook(): Promise<
|
||||||
|
OnCompleteHookModule["executeOnCompleteHook"]
|
||||||
|
> {
|
||||||
|
const onCompleteHookModule = await import(`./on-complete-hook?test=${Date.now()}-${Math.random()}`)
|
||||||
|
return onCompleteHookModule.executeOnCompleteHook
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mock.restore()
|
||||||
originalPlatform = process.platform
|
originalPlatform = process.platform
|
||||||
originalEnv = {
|
originalEnv = {
|
||||||
SHELL: process.env.SHELL,
|
SHELL: process.env.SHELL,
|
||||||
PSModulePath: process.env.PSModulePath,
|
PSModulePath: process.env.PSModulePath,
|
||||||
ComSpec: process.env.ComSpec,
|
ComSpec: process.env.ComSpec,
|
||||||
}
|
}
|
||||||
logSpy = spyOn(loggerModule, "log").mockImplementation(() => {})
|
logCalls = []
|
||||||
|
spyOn(loggerModule, "log").mockImplementation((message: string, data?: unknown) => {
|
||||||
|
logCalls.push([message, data])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -52,7 +64,7 @@ describe("executeOnCompleteHook", () => {
|
|||||||
delete process.env[key]
|
delete process.env[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logSpy.mockRestore()
|
mock.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("uses sh on unix shells and passes correct env vars", async () => {
|
it("uses sh on unix shells and passes correct env vars", async () => {
|
||||||
@@ -60,32 +72,36 @@ describe("executeOnCompleteHook", () => {
|
|||||||
Object.defineProperty(process, "platform", { value: "linux" })
|
Object.defineProperty(process, "platform", { value: "linux" })
|
||||||
process.env.SHELL = "/bin/bash"
|
process.env.SHELL = "/bin/bash"
|
||||||
delete process.env.PSModulePath
|
delete process.env.PSModulePath
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
const spawnCalls: Array<Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>> = []
|
||||||
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation((
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
spawnCalls.push([command, options])
|
||||||
|
return createProc(0)
|
||||||
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: "echo test",
|
||||||
command: "echo test",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 0,
|
||||||
exitCode: 0,
|
durationMs: 5000,
|
||||||
durationMs: 5000,
|
messageCount: 10,
|
||||||
messageCount: 10,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(spawnSpy).toHaveBeenCalledTimes(1)
|
expect(spawnCalls).toHaveLength(1)
|
||||||
const [args, options] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
const [args, options] = spawnCalls[0]
|
||||||
|
|
||||||
expect(args).toEqual(["sh", "-c", "echo test"])
|
expect(args).toEqual(["sh", "-c", "echo test"])
|
||||||
expect(options?.env?.SESSION_ID).toBe("session-123")
|
expect(options?.env?.SESSION_ID).toBe("session-123")
|
||||||
expect(options?.env?.EXIT_CODE).toBe("0")
|
expect(options?.env?.EXIT_CODE).toBe("0")
|
||||||
expect(options?.env?.DURATION_MS).toBe("5000")
|
expect(options?.env?.DURATION_MS).toBe("5000")
|
||||||
expect(options?.env?.MESSAGE_COUNT).toBe("10")
|
expect(options?.env?.MESSAGE_COUNT).toBe("10")
|
||||||
expect(options?.stdout).toBe("pipe")
|
expect(options?.stdout).toBe("pipe")
|
||||||
expect(options?.stderr).toBe("pipe")
|
expect(options?.stderr).toBe("pipe")
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("uses powershell when PowerShell is detected on Windows", async () => {
|
it("uses powershell when PowerShell is detected on Windows", async () => {
|
||||||
@@ -93,24 +109,28 @@ describe("executeOnCompleteHook", () => {
|
|||||||
Object.defineProperty(process, "platform", { value: "win32" })
|
Object.defineProperty(process, "platform", { value: "win32" })
|
||||||
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
||||||
delete process.env.SHELL
|
delete process.env.SHELL
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
const spawnCalls: Array<Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>> = []
|
||||||
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation((
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
spawnCalls.push([command, options])
|
||||||
|
return createProc(0)
|
||||||
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: "Write-Host done",
|
||||||
command: "Write-Host done",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 0,
|
||||||
exitCode: 0,
|
durationMs: 5000,
|
||||||
durationMs: 5000,
|
messageCount: 10,
|
||||||
messageCount: 10,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const [args] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
const [args] = spawnCalls[0]
|
||||||
expect(args).toEqual(["powershell.exe", "-NoProfile", "-Command", "Write-Host done"])
|
expect(args).toEqual(["powershell.exe", "-NoProfile", "-Command", "Write-Host done"])
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("uses pwsh when PowerShell is detected on non-Windows platforms", async () => {
|
it("uses pwsh when PowerShell is detected on non-Windows platforms", async () => {
|
||||||
@@ -118,24 +138,28 @@ describe("executeOnCompleteHook", () => {
|
|||||||
Object.defineProperty(process, "platform", { value: "linux" })
|
Object.defineProperty(process, "platform", { value: "linux" })
|
||||||
process.env.PSModulePath = "/usr/local/share/powershell/Modules"
|
process.env.PSModulePath = "/usr/local/share/powershell/Modules"
|
||||||
delete process.env.SHELL
|
delete process.env.SHELL
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
const spawnCalls: Array<Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>> = []
|
||||||
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation((
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
spawnCalls.push([command, options])
|
||||||
|
return createProc(0)
|
||||||
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: "Write-Host done",
|
||||||
command: "Write-Host done",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 0,
|
||||||
exitCode: 0,
|
durationMs: 5000,
|
||||||
durationMs: 5000,
|
messageCount: 10,
|
||||||
messageCount: 10,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const [args] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
const [args] = spawnCalls[0]
|
||||||
expect(args).toEqual(["pwsh", "-NoProfile", "-Command", "Write-Host done"])
|
expect(args).toEqual(["pwsh", "-NoProfile", "-Command", "Write-Host done"])
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("falls back to cmd.exe on Windows when PowerShell is not detected", async () => {
|
it("falls back to cmd.exe on Windows when PowerShell is not detected", async () => {
|
||||||
@@ -144,179 +168,182 @@ describe("executeOnCompleteHook", () => {
|
|||||||
delete process.env.PSModulePath
|
delete process.env.PSModulePath
|
||||||
delete process.env.SHELL
|
delete process.env.SHELL
|
||||||
process.env.ComSpec = "C:\\Windows\\System32\\cmd.exe"
|
process.env.ComSpec = "C:\\Windows\\System32\\cmd.exe"
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
const spawnCalls: Array<Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>> = []
|
||||||
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation((
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
spawnCalls.push([command, options])
|
||||||
|
return createProc(0)
|
||||||
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: "echo done",
|
||||||
command: "echo done",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 0,
|
||||||
exitCode: 0,
|
durationMs: 5000,
|
||||||
durationMs: 5000,
|
messageCount: 10,
|
||||||
messageCount: 10,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const [args] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
const [args] = spawnCalls[0]
|
||||||
expect(args).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/s", "/c", "echo done"])
|
expect(args).toEqual(["C:\\Windows\\System32\\cmd.exe", "/d", "/s", "/c", "echo done"])
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("env var values are strings", async () => {
|
it("env var values are strings", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
const spawnCalls: Array<Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>> = []
|
||||||
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation((
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
spawnCalls.push([command, options])
|
||||||
|
return createProc(0)
|
||||||
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: "echo test",
|
||||||
command: "echo test",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 1,
|
||||||
exitCode: 1,
|
durationMs: 12345,
|
||||||
durationMs: 12345,
|
messageCount: 42,
|
||||||
messageCount: 42,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const [_, options] = spawnSpy.mock.calls[0] as Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>
|
const [, options] = spawnCalls[0]
|
||||||
|
|
||||||
expect(options?.env?.EXIT_CODE).toBe("1")
|
expect(options?.env?.EXIT_CODE).toBe("1")
|
||||||
expect(options?.env?.EXIT_CODE).toBeTypeOf("string")
|
expect(options?.env?.EXIT_CODE).toBeTypeOf("string")
|
||||||
expect(options?.env?.DURATION_MS).toBe("12345")
|
expect(options?.env?.DURATION_MS).toBe("12345")
|
||||||
expect(options?.env?.DURATION_MS).toBeTypeOf("string")
|
expect(options?.env?.DURATION_MS).toBeTypeOf("string")
|
||||||
expect(options?.env?.MESSAGE_COUNT).toBe("42")
|
expect(options?.env?.MESSAGE_COUNT).toBe("42")
|
||||||
expect(options?.env?.MESSAGE_COUNT).toBeTypeOf("string")
|
expect(options?.env?.MESSAGE_COUNT).toBeTypeOf("string")
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("empty command string is no-op", async () => {
|
it("empty command string is no-op", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
const spawnCalls: Array<Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>> = []
|
||||||
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation((
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
spawnCalls.push([command, options])
|
||||||
|
return createProc(0)
|
||||||
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: "",
|
||||||
command: "",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 0,
|
||||||
exitCode: 0,
|
durationMs: 5000,
|
||||||
durationMs: 5000,
|
messageCount: 10,
|
||||||
messageCount: 10,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(spawnSpy).not.toHaveBeenCalled()
|
expect(spawnCalls).toHaveLength(0)
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("whitespace-only command is no-op", async () => {
|
it("whitespace-only command is no-op", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(0))
|
const spawnCalls: Array<Parameters<typeof spawnWithWindowsHideModule.spawnWithWindowsHide>> = []
|
||||||
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation((
|
||||||
|
command,
|
||||||
|
options,
|
||||||
|
) => {
|
||||||
|
spawnCalls.push([command, options])
|
||||||
|
return createProc(0)
|
||||||
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: " ",
|
||||||
command: " ",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 0,
|
||||||
exitCode: 0,
|
durationMs: 5000,
|
||||||
durationMs: 5000,
|
messageCount: 10,
|
||||||
messageCount: 10,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(spawnSpy).not.toHaveBeenCalled()
|
expect(spawnCalls).toHaveLength(0)
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("command failure logs warning but does not throw", async () => {
|
it("command failure logs warning but does not throw", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(1))
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(createProc(1))
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
expect(
|
command: "false",
|
||||||
executeOnCompleteHook({
|
sessionId: "session-123",
|
||||||
command: "false",
|
exitCode: 0,
|
||||||
sessionId: "session-123",
|
durationMs: 5000,
|
||||||
exitCode: 0,
|
messageCount: 10,
|
||||||
durationMs: 5000,
|
})
|
||||||
messageCount: 10,
|
|
||||||
})
|
|
||||||
).resolves.toBeUndefined()
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const warningCall = logSpy.mock.calls.find(
|
const warningCall = logCalls.find(
|
||||||
(call) => call[0] === "On-complete hook exited with non-zero code"
|
(call) => call[0] === "On-complete hook exited with non-zero code"
|
||||||
)
|
)
|
||||||
expect(warningCall).toBeDefined()
|
expect(warningCall).toBeDefined()
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("spawn error logs warning but does not throw", async () => {
|
it("spawn error logs warning but does not throw", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnError = new Error("Command not found")
|
const spawnError = new Error("Command not found")
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation(() => {
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockImplementation(() => {
|
||||||
throw spawnError
|
throw spawnError
|
||||||
})
|
})
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
expect(
|
command: "nonexistent-command",
|
||||||
executeOnCompleteHook({
|
sessionId: "session-123",
|
||||||
command: "nonexistent-command",
|
exitCode: 0,
|
||||||
sessionId: "session-123",
|
durationMs: 5000,
|
||||||
exitCode: 0,
|
messageCount: 10,
|
||||||
durationMs: 5000,
|
})
|
||||||
messageCount: 10,
|
|
||||||
})
|
|
||||||
).resolves.toBeUndefined()
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const errorCall = logSpy.mock.calls.find(
|
const errorCall = logCalls.find(
|
||||||
(call) => call[0] === "Failed to execute on-complete hook"
|
(call) => call[0] === "Failed to execute on-complete hook"
|
||||||
)
|
)
|
||||||
expect(errorCall).toBeDefined()
|
expect(errorCall).toBeDefined()
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("hook stdout and stderr are logged to file logger", async () => {
|
it("hook stdout and stderr are logged to file logger", async () => {
|
||||||
// given
|
// given
|
||||||
const spawnSpy = spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(
|
spyOn(spawnWithWindowsHideModule, "spawnWithWindowsHide").mockReturnValue(
|
||||||
createProc(0, { stdout: "hook output\n", stderr: "hook warning\n" })
|
createProc(0, { stdout: "hook output\n", stderr: "hook warning\n" })
|
||||||
)
|
)
|
||||||
|
const executeOnCompleteHook = await importFreshExecuteOnCompleteHook()
|
||||||
|
|
||||||
try {
|
// when
|
||||||
// when
|
await executeOnCompleteHook({
|
||||||
await executeOnCompleteHook({
|
command: "echo test",
|
||||||
command: "echo test",
|
sessionId: "session-123",
|
||||||
sessionId: "session-123",
|
exitCode: 0,
|
||||||
exitCode: 0,
|
durationMs: 5000,
|
||||||
durationMs: 5000,
|
messageCount: 10,
|
||||||
messageCount: 10,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const stdoutCall = logSpy.mock.calls.find(
|
const stdoutCall = logCalls.find(
|
||||||
(call) => call[0] === "On-complete hook stdout"
|
(call) => call[0] === "On-complete hook stdout"
|
||||||
)
|
)
|
||||||
const stderrCall = logSpy.mock.calls.find(
|
const stderrCall = logCalls.find(
|
||||||
(call) => call[0] === "On-complete hook stderr"
|
(call) => call[0] === "On-complete hook stderr"
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(stdoutCall?.[1]).toEqual({ command: "echo test", stdout: "hook output" })
|
expect(stdoutCall?.[1]).toEqual({ command: "echo test", stdout: "hook output" })
|
||||||
expect(stderrCall?.[1]).toEqual({ command: "echo test", stderr: "hook warning" })
|
expect(stderrCall?.[1]).toEqual({ command: "echo test", stderr: "hook warning" })
|
||||||
} finally {
|
|
||||||
spawnSpy.mockRestore()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
|
||||||
import { detectShellType, log } from "../../shared"
|
import { detectShellType } from "../../shared"
|
||||||
|
import { log } from "../../shared/logger"
|
||||||
|
|
||||||
async function readOutput(
|
async function readOutput(
|
||||||
stream: ReadableStream<Uint8Array> | undefined,
|
stream: ReadableStream<Uint8Array> | undefined,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { pollForCompletion } from "./poll-for-completion"
|
|||||||
import { loadAgentProfileColors } from "./agent-profile-colors"
|
import { loadAgentProfileColors } from "./agent-profile-colors"
|
||||||
import { suppressRunInput } from "./stdin-suppression"
|
import { suppressRunInput } from "./stdin-suppression"
|
||||||
import { createTimestampedStdoutController } from "./timestamp-output"
|
import { createTimestampedStdoutController } from "./timestamp-output"
|
||||||
|
import { createCliPostHog, getPostHogDistinctId } from "../../shared/posthog"
|
||||||
|
|
||||||
export { resolveRunAgent }
|
export { resolveRunAgent }
|
||||||
|
|
||||||
@@ -50,6 +51,18 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
const resolvedAgent = resolveRunAgent(options, pluginConfig)
|
const resolvedAgent = resolveRunAgent(options, pluginConfig)
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
|
||||||
|
const posthog = createCliPostHog()
|
||||||
|
const distinctId = getPostHogDistinctId()
|
||||||
|
posthog.capture({
|
||||||
|
distinctId,
|
||||||
|
event: "run_started",
|
||||||
|
properties: {
|
||||||
|
agent: resolvedAgent,
|
||||||
|
has_model: !!options.model,
|
||||||
|
has_session_id: !!options.sessionId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resolvedModel = resolveRunModel(options.model)
|
const resolvedModel = resolveRunModel(options.model)
|
||||||
|
|
||||||
@@ -141,6 +154,28 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exitCode === 0) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId,
|
||||||
|
event: "run_completed",
|
||||||
|
properties: {
|
||||||
|
agent: resolvedAgent,
|
||||||
|
duration_ms: durationMs,
|
||||||
|
message_count: eventState.messageCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else if (exitCode === 1) {
|
||||||
|
posthog.capture({
|
||||||
|
distinctId,
|
||||||
|
event: "run_failed",
|
||||||
|
properties: {
|
||||||
|
agent: resolvedAgent,
|
||||||
|
exit_code: exitCode,
|
||||||
|
duration_ms: durationMs,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return exitCode
|
return exitCode
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
cleanup()
|
cleanup()
|
||||||
@@ -155,9 +190,20 @@ export async function run(options: RunOptions): Promise<number> {
|
|||||||
if (err instanceof Error && err.name === "AbortError") {
|
if (err instanceof Error && err.name === "AbortError") {
|
||||||
return 130
|
return 130
|
||||||
}
|
}
|
||||||
|
posthog.captureException(err, distinctId)
|
||||||
|
posthog.capture({
|
||||||
|
distinctId,
|
||||||
|
event: "run_failed",
|
||||||
|
properties: {
|
||||||
|
agent: resolvedAgent,
|
||||||
|
error: serializeError(err),
|
||||||
|
duration_ms: Date.now() - startTime,
|
||||||
|
},
|
||||||
|
})
|
||||||
console.error(pc.red(`Error: ${serializeError(err)}`))
|
console.error(pc.red(`Error: ${serializeError(err)}`))
|
||||||
return 1
|
return 1
|
||||||
} finally {
|
} finally {
|
||||||
|
await posthog.shutdown()
|
||||||
timestampOutput?.restore()
|
timestampOutput?.restore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user