feat(installer): prompt before starring repos
This commit is contained in:
@@ -153,7 +153,7 @@ describe("runCliInstaller platform branching", () => {
|
|||||||
expect(result).toBe(0)
|
expect(result).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("prints star commands for OpenAgent and LazyCodex", async () => {
|
test("does not print star commands in noninteractive installs", async () => {
|
||||||
// given
|
// given
|
||||||
stubOpenCodeSuccess()
|
stubOpenCodeSuccess()
|
||||||
spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
|
spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
|
||||||
@@ -164,7 +164,7 @@ describe("runCliInstaller platform branching", () => {
|
|||||||
// then
|
// then
|
||||||
const output = consoleLogMock.mock.calls.map((call) => call.join(" ")).join("\n")
|
const output = consoleLogMock.mock.calls.map((call) => call.join(" ")).join("\n")
|
||||||
expect(result).toBe(0)
|
expect(result).toBe(0)
|
||||||
expect(output).toContain("/user/starred/code-yeongyu/oh-my-openagent")
|
expect(output).not.toContain("/user/starred/code-yeongyu/oh-my-openagent")
|
||||||
expect(output).toContain("/user/starred/code-yeongyu/lazycodex")
|
expect(output).not.toContain("/user/starred/code-yeongyu/lazycodex")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createInterface } from "node:readline/promises"
|
||||||
import color from "picocolors"
|
import color from "picocolors"
|
||||||
import { PLUGIN_NAME, PUBLISHED_PACKAGE_NAME } from "../shared"
|
import { PLUGIN_NAME, PUBLISHED_PACKAGE_NAME } from "../shared"
|
||||||
import type { InstallArgs } from "./types"
|
import type { InstallArgs } from "./types"
|
||||||
@@ -24,7 +25,7 @@ import {
|
|||||||
} from "./install-validators"
|
} from "./install-validators"
|
||||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||||
import { runCodexInstaller } from "./install-codex"
|
import { runCodexInstaller } from "./install-codex"
|
||||||
import { STAR_REPOSITORIES, formatGitHubStarCommand } from "./star-request"
|
import { starGitHubRepositories } from "./star-request"
|
||||||
|
|
||||||
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
||||||
const validation = validateNonTuiArgs(args)
|
const validation = validateNonTuiArgs(args)
|
||||||
@@ -168,11 +169,7 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
|||||||
"The Magic Word",
|
"The Magic Word",
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`)
|
await maybePromptForGitHubStars()
|
||||||
for (const repository of STAR_REPOSITORIES) {
|
|
||||||
console.log(` ${color.dim(formatGitHubStarCommand(repository))}`)
|
|
||||||
}
|
|
||||||
console.log()
|
|
||||||
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
||||||
console.log()
|
console.log()
|
||||||
|
|
||||||
@@ -188,3 +185,34 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function maybePromptForGitHubStars(): Promise<void> {
|
||||||
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return
|
||||||
|
|
||||||
|
const readline = createInterface({ input: process.stdin, output: process.stdout })
|
||||||
|
try {
|
||||||
|
const answer = await readline.question(`${SYMBOLS.star} ${color.yellow("Star the repos on GitHub?")} ${color.dim("[y/N]")} `)
|
||||||
|
if (!isYes(answer)) return
|
||||||
|
} finally {
|
||||||
|
readline.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await starGitHubRepositories()
|
||||||
|
const failed = results.filter((result) => !result.ok)
|
||||||
|
if (failed.length === 0) {
|
||||||
|
printSuccess("Starred GitHub repositories")
|
||||||
|
console.log()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
printWarning("Could not star every repository. Make sure GitHub CLI is installed and authenticated.")
|
||||||
|
for (const result of failed) {
|
||||||
|
console.log(` ${SYMBOLS.bullet} ${result.repository}`)
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isYes(value: string): boolean {
|
||||||
|
const normalized = value.trim().toLowerCase()
|
||||||
|
return normalized === "y" || normalized === "yes"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { STAR_REPOSITORIES, formatGitHubStarCommand, starGitHubRepositories } from "./star-request"
|
||||||
|
|
||||||
|
describe("star-request", () => {
|
||||||
|
test("formats the legacy GitHub CLI command for manual fallback output", () => {
|
||||||
|
// given
|
||||||
|
const repository = "code-yeongyu/oh-my-openagent"
|
||||||
|
|
||||||
|
// when
|
||||||
|
const command = formatGitHubStarCommand(repository)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(command).toBe("gh api --silent --method PUT /user/starred/code-yeongyu/oh-my-openagent >/dev/null 2>&1 || true")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("runs GitHub star requests for every repository", async () => {
|
||||||
|
// given
|
||||||
|
const starred: string[] = []
|
||||||
|
|
||||||
|
// when
|
||||||
|
const results = await starGitHubRepositories(STAR_REPOSITORIES, async (repository) => {
|
||||||
|
starred.push(repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(starred).toEqual([...STAR_REPOSITORIES])
|
||||||
|
expect(results).toEqual(STAR_REPOSITORIES.map((repository) => ({ repository, ok: true })))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps going when one repository cannot be starred", async () => {
|
||||||
|
// given
|
||||||
|
const repositories = ["code-yeongyu/oh-my-openagent", "code-yeongyu/lazycodex"] as const
|
||||||
|
|
||||||
|
// when
|
||||||
|
const results = await starGitHubRepositories(repositories, async (repository) => {
|
||||||
|
if (repository === "code-yeongyu/lazycodex") throw new Error("gh auth missing")
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(results).toEqual([
|
||||||
|
{ repository: "code-yeongyu/oh-my-openagent", ok: true },
|
||||||
|
{ repository: "code-yeongyu/lazycodex", ok: false, error: "gh auth missing" },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,8 +1,41 @@
|
|||||||
|
import { execFile } from "node:child_process"
|
||||||
|
import { promisify } from "node:util"
|
||||||
|
|
||||||
export const STAR_REPOSITORIES = [
|
export const STAR_REPOSITORIES = [
|
||||||
"code-yeongyu/oh-my-openagent",
|
"code-yeongyu/oh-my-openagent",
|
||||||
"code-yeongyu/lazycodex",
|
"code-yeongyu/lazycodex",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
|
export interface GitHubStarResult {
|
||||||
|
readonly repository: string
|
||||||
|
readonly ok: boolean
|
||||||
|
readonly error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GitHubStarCommandRunner = (repository: string) => Promise<void>
|
||||||
|
|
||||||
export function formatGitHubStarCommand(repository: string): string {
|
export function formatGitHubStarCommand(repository: string): string {
|
||||||
return `gh api --silent --method PUT /user/starred/${repository} >/dev/null 2>&1 || true`
|
return `gh api --silent --method PUT /user/starred/${repository} >/dev/null 2>&1 || true`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function runGitHubStarCommand(repository: string): Promise<void> {
|
||||||
|
await execFileAsync("gh", ["api", "--silent", "--method", "PUT", `/user/starred/${repository}`])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function starGitHubRepositories(
|
||||||
|
repositories: readonly string[] = STAR_REPOSITORIES,
|
||||||
|
runCommand: GitHubStarCommandRunner = runGitHubStarCommand,
|
||||||
|
): Promise<readonly GitHubStarResult[]> {
|
||||||
|
const results: GitHubStarResult[] = []
|
||||||
|
for (const repository of repositories) {
|
||||||
|
try {
|
||||||
|
await runCommand(repository)
|
||||||
|
results.push({ repository, ok: true })
|
||||||
|
} catch (error) {
|
||||||
|
results.push({ repository, ok: false, error: error instanceof Error ? error.message : String(error) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"
|
||||||
import * as p from "@clack/prompts"
|
import * as p from "@clack/prompts"
|
||||||
import * as configManager from "./config-manager"
|
import * as configManager from "./config-manager"
|
||||||
|
import * as starRequest from "./star-request"
|
||||||
import * as tuiInstallPrompts from "./tui-install-prompts"
|
import * as tuiInstallPrompts from "./tui-install-prompts"
|
||||||
import { runTuiInstaller } from "./tui-installer"
|
import { runTuiInstaller } from "./tui-installer"
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ describe("runTuiInstaller", () => {
|
|||||||
spyOn(p.log, "success").mockImplementation(() => undefined),
|
spyOn(p.log, "success").mockImplementation(() => undefined),
|
||||||
spyOn(p.log, "message").mockImplementation(() => undefined),
|
spyOn(p.log, "message").mockImplementation(() => undefined),
|
||||||
spyOn(p, "note").mockImplementation(() => undefined),
|
spyOn(p, "note").mockImplementation(() => undefined),
|
||||||
|
spyOn(p, "confirm").mockResolvedValue(false),
|
||||||
spyOn(p, "outro").mockImplementation(() => undefined),
|
spyOn(p, "outro").mockImplementation(() => undefined),
|
||||||
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("opencode"),
|
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("opencode"),
|
||||||
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
|
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
|
||||||
@@ -152,6 +154,7 @@ describe("runTuiInstaller", () => {
|
|||||||
spyOn(p.log, "success").mockImplementation(() => undefined),
|
spyOn(p.log, "success").mockImplementation(() => undefined),
|
||||||
spyOn(p.log, "message").mockImplementation(() => undefined),
|
spyOn(p.log, "message").mockImplementation(() => undefined),
|
||||||
spyOn(p, "note").mockImplementation(() => undefined),
|
spyOn(p, "note").mockImplementation(() => undefined),
|
||||||
|
spyOn(p, "confirm").mockResolvedValue(false),
|
||||||
spyOn(p, "outro").mockImplementation(() => undefined),
|
spyOn(p, "outro").mockImplementation(() => undefined),
|
||||||
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("codex"),
|
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("codex"),
|
||||||
spyOn(tuiInstallPrompts, "promptInstallConfig").mockResolvedValue({
|
spyOn(tuiInstallPrompts, "promptInstallConfig").mockResolvedValue({
|
||||||
@@ -197,4 +200,77 @@ describe("runTuiInstaller", () => {
|
|||||||
addPluginSpy.mockRestore()
|
addPluginSpy.mockRestore()
|
||||||
writeConfigSpy.mockRestore()
|
writeConfigSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("stars GitHub repositories when the user confirms", async () => {
|
||||||
|
// given
|
||||||
|
const restoreSpies = [
|
||||||
|
spyOn(p, "spinner").mockReturnValue(createMockSpinner()),
|
||||||
|
spyOn(p, "intro").mockImplementation(() => undefined),
|
||||||
|
spyOn(p.log, "info").mockImplementation(() => undefined),
|
||||||
|
spyOn(p.log, "warn").mockImplementation(() => undefined),
|
||||||
|
spyOn(p.log, "success").mockImplementation(() => undefined),
|
||||||
|
spyOn(p.log, "message").mockImplementation(() => undefined),
|
||||||
|
spyOn(p, "note").mockImplementation(() => undefined),
|
||||||
|
spyOn(p, "confirm").mockResolvedValue(true),
|
||||||
|
spyOn(p, "outro").mockImplementation(() => undefined),
|
||||||
|
spyOn(tuiInstallPrompts, "promptInstallPlatform").mockResolvedValue("opencode"),
|
||||||
|
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
|
||||||
|
isInstalled: false,
|
||||||
|
installedVersion: null,
|
||||||
|
hasClaude: false,
|
||||||
|
isMax20: false,
|
||||||
|
hasOpenAI: false,
|
||||||
|
hasGemini: false,
|
||||||
|
hasCopilot: false,
|
||||||
|
hasCodex: false,
|
||||||
|
hasOpencodeZen: false,
|
||||||
|
hasZaiCodingPlan: false,
|
||||||
|
hasKimiForCoding: false,
|
||||||
|
hasOpencodeGo: false,
|
||||||
|
hasVercelAiGateway: false,
|
||||||
|
}),
|
||||||
|
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true),
|
||||||
|
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"),
|
||||||
|
spyOn(tuiInstallPrompts, "promptInstallConfig").mockResolvedValue({
|
||||||
|
platform: "opencode",
|
||||||
|
hasOpenCode: true,
|
||||||
|
hasClaude: false,
|
||||||
|
isMax20: false,
|
||||||
|
hasOpenAI: false,
|
||||||
|
hasGemini: false,
|
||||||
|
hasCopilot: false,
|
||||||
|
hasCodex: false,
|
||||||
|
hasOpencodeZen: false,
|
||||||
|
hasZaiCodingPlan: false,
|
||||||
|
hasKimiForCoding: false,
|
||||||
|
hasOpencodeGo: false,
|
||||||
|
hasVercelAiGateway: false,
|
||||||
|
codexAutonomous: false,
|
||||||
|
}),
|
||||||
|
spyOn(configManager, "addPluginToOpenCodeConfig").mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
configPath: "/tmp/opencode.jsonc",
|
||||||
|
}),
|
||||||
|
spyOn(configManager, "writeOmoConfig").mockReturnValue({
|
||||||
|
success: true,
|
||||||
|
configPath: "/tmp/oh-my-opencode.jsonc",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
const starSpy = spyOn(starRequest, "starGitHubRepositories").mockResolvedValue([
|
||||||
|
{ repository: "code-yeongyu/oh-my-openagent", ok: true },
|
||||||
|
{ repository: "code-yeongyu/lazycodex", ok: true },
|
||||||
|
])
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await runTuiInstaller({ tui: true }, "3.16.0")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBe(0)
|
||||||
|
expect(starSpy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
for (const spy of restoreSpies) {
|
||||||
|
spy.mockRestore()
|
||||||
|
}
|
||||||
|
starSpy.mockRestore()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { detectedToInitialValues, formatConfigSummary, SYMBOLS } from "./install
|
|||||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||||
import { promptInstallConfig, promptInstallPlatform } from "./tui-install-prompts"
|
import { promptInstallConfig, promptInstallPlatform } from "./tui-install-prompts"
|
||||||
import { runCodexInstaller } from "./install-codex"
|
import { runCodexInstaller } from "./install-codex"
|
||||||
import { STAR_REPOSITORIES, formatGitHubStarCommand } from "./star-request"
|
import { starGitHubRepositories } from "./star-request"
|
||||||
|
|
||||||
export async function runTuiInstaller(args: InstallArgs, version: string): Promise<number> {
|
export async function runTuiInstaller(args: InstallArgs, version: string): Promise<number> {
|
||||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||||
@@ -140,9 +140,20 @@ export async function runTuiInstaller(args: InstallArgs, version: string): Promi
|
|||||||
"The Magic Word",
|
"The Magic Word",
|
||||||
)
|
)
|
||||||
|
|
||||||
p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`)
|
const shouldStar = await p.confirm({
|
||||||
for (const repository of STAR_REPOSITORIES) {
|
message: "Star the repos on GitHub?",
|
||||||
p.log.message(` ${color.dim(formatGitHubStarCommand(repository))}`)
|
initialValue: false,
|
||||||
|
})
|
||||||
|
if (!p.isCancel(shouldStar) && shouldStar) {
|
||||||
|
spinner.start("Starring GitHub repositories")
|
||||||
|
const results = await starGitHubRepositories()
|
||||||
|
const failed = results.filter((result) => !result.ok)
|
||||||
|
if (failed.length === 0) {
|
||||||
|
spinner.stop("GitHub repositories starred")
|
||||||
|
} else {
|
||||||
|
spinner.stop("Could not star every repository")
|
||||||
|
p.log.warn("Make sure GitHub CLI is installed and authenticated.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.outro(color.green("oMoMoMoMo... Enjoy!"))
|
p.outro(color.green("oMoMoMoMo... Enjoy!"))
|
||||||
|
|||||||
Reference in New Issue
Block a user