From fd171fb3c0dbf23c3c4f0e63d49156db6207b78e Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Mon, 5 Oct 2026 20:33:07 -0700 Subject: [PATCH] fix(cli): print remedy commands through getCliPrefix() --- .changeset/runnable-remedy-commands.md | 5 +++++ packages/cli/src/commands/agent.ts | 8 +++++--- packages/cli/src/commands/auth.ts | 9 ++++++--- packages/cli/src/commands/check.ts | 3 ++- packages/cli/src/commands/feedback.ts | 3 ++- packages/cli/src/commands/onboard.ts | 7 ++++--- packages/cli/src/commands/rules.ts | 13 ++++++------- packages/cli/src/index.ts | 6 ++++-- packages/cli/src/rules/deliver.ts | 3 ++- packages/cli/src/rules/dispatch.ts | 3 ++- packages/cli/src/wizard/index.ts | 5 +++-- packages/cli/test/agent-extensions.test.ts | 7 +++++-- packages/cli/test/anonymous-flag.test.ts | 8 +++++--- packages/cli/test/check.test.ts | 5 +++++ packages/cli/test/init-no-interactive.test.ts | 4 ++-- 15 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 .changeset/runnable-remedy-commands.md diff --git a/.changeset/runnable-remedy-commands.md b/.changeset/runnable-remedy-commands.md new file mode 100644 index 00000000..85d58e6f --- /dev/null +++ b/.changeset/runnable-remedy-commands.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +Messages that tell you what to run now print a command you can run as shown. They used to hard-code `taskless`, which is usually not on `PATH` when the CLI was launched with `npx @taskless/cli` or `pnpm dlx`. They now use the same launcher-aware prefix the authentication and rule-recovery errors already used, for example `npx @taskless/cli@latest auth login`. This covers `auth`, `check`, `rule create`/`improve`/`meta`, `onboard`, `agent`, `feedback send`, the install wizard, the non-interactive banner, and the Vale and missing-fixture notices. `check` with no rules now also names the `--from` that `rule create` requires. diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts index 46cc893c..32f99075 100644 --- a/packages/cli/src/commands/agent.ts +++ b/packages/cli/src/commands/agent.ts @@ -19,6 +19,7 @@ import { withSurveyInvite } from "../survey/invite"; import { applyCliInvocation } from "../util/invocation"; import { detectCliInvocation, + getCliPrefix, processLauncherContext, } from "../util/package-manager"; @@ -162,8 +163,9 @@ export function createAgentCommand(subCommands: SubCommandsDef) { "\nAppend `--anonymous` to any rule/check command to skip the Taskless API" ); console.log("and use local-only behavior."); + const prefix = getCliPrefix(); console.log( - "\nRun `taskless agent ` for the full recipe (e.g. `taskless agent create-sg-rule`)." + `\nRun \`${prefix} agent \` for the full recipe (e.g. \`${prefix} agent create-sg-rule\`).` ); return; } @@ -177,7 +179,7 @@ export function createAgentCommand(subCommands: SubCommandsDef) { telemetry.capture("cli_agent", { topic: positionals.join(" ") }); console.error(`Too many arguments: ${positionals.join(" ")}`); console.error( - "A topic is a single token. Run `taskless agent` for the topic index." + `A topic is a single token. Run \`${getCliPrefix()} agent\` for the topic index.` ); process.exitCode = 1; return; @@ -238,7 +240,7 @@ export function createAgentCommand(subCommands: SubCommandsDef) { // cli_agent for an unknown topic — still the attempted topic string. telemetry.capture("cli_agent", { topic: key }); console.error(`Unknown command: ${key}`); - console.error("Run `taskless agent` for available topics."); + console.error(`Run \`${getCliPrefix()} agent\` for available topics.`); process.exitCode = 1; } }, diff --git a/packages/cli/src/commands/auth.ts b/packages/cli/src/commands/auth.ts index eca52cf4..f1c9759f 100644 --- a/packages/cli/src/commands/auth.ts +++ b/packages/cli/src/commands/auth.ts @@ -8,6 +8,7 @@ import { fetchWhoami } from "../auth/whoami"; import { getTelemetry } from "../telemetry"; import { type CLIErrorCode, writeJsonError } from "../types/errors"; import { splitRawArguments } from "../util/argv"; +import { getCliPrefix } from "../util/package-manager"; const loginCommand = defineCommand({ meta: { @@ -70,7 +71,9 @@ const loginCommand = defineCommand({ case "already_logged_in": { if (!args.json) { console.log("You are already logged in."); - console.log("Run `taskless auth logout` first to re-authenticate."); + console.log( + `Run \`${getCliPrefix()} auth logout\` first to re-authenticate.` + ); } return; } @@ -174,7 +177,7 @@ export const authCommand = defineCommand({ const token = await getToken(cwd); if (!token) { console.log("Not logged in."); - console.log("Run `taskless auth login` to authenticate."); + console.log(`Run \`${getCliPrefix()} auth login\` to authenticate.`); return; } @@ -182,7 +185,7 @@ export const authCommand = defineCommand({ if (!whoami) { console.log("Logged in, but unable to verify identity."); console.log( - "Your token may be invalid or expired. Run `taskless auth login` to re-authenticate." + `Your token may be invalid or expired. Run \`${getCliPrefix()} auth login\` to re-authenticate.` ); return; } diff --git a/packages/cli/src/commands/check.ts b/packages/cli/src/commands/check.ts index 534c6417..48d0240b 100644 --- a/packages/cli/src/commands/check.ts +++ b/packages/cli/src/commands/check.ts @@ -19,6 +19,7 @@ import { planCheck } from "../rules/plan-check"; import { openRun } from "../rules/run-directory"; import { fromProjectRoot } from "../rules/snapshot"; import { markNotice } from "../util/notices"; +import { getCliPrefix } from "../util/package-manager"; async function pathExists(absolutePath: string): Promise { try { @@ -334,7 +335,7 @@ export const checkCommand = defineCommand({ ); } else { console.log( - "No rules configured. Create one with `taskless rule create`." + `No rules configured. Create one with \`${getCliPrefix()} rule create --from \`.` ); } return; diff --git a/packages/cli/src/commands/feedback.ts b/packages/cli/src/commands/feedback.ts index bbf6028e..ec474b62 100644 --- a/packages/cli/src/commands/feedback.ts +++ b/packages/cli/src/commands/feedback.ts @@ -15,6 +15,7 @@ import { import { getTelemetry, isTelemetryEnabled } from "../telemetry"; import { type CLIErrorCode, writeJsonError } from "../types/errors"; import { CLIError } from "../util/cli-error"; +import { getCliPrefix } from "../util/package-manager"; /** * What both verbs say under the telemetry opt-out. An agent should never @@ -111,7 +112,7 @@ const sendCommand = defineCommand({ // whether or not anything would be sent, and the agent should hear so. if (!args.from) { fail( - "--from is required. Provide a path to a JSON file.\n Example: taskless feedback send --from .taskless/.tmp-feedback.json", + `--from is required. Provide a path to a JSON file.\n Example: ${getCliPrefix()} feedback send --from .taskless/.tmp-feedback.json`, "INVALID_INPUT" ); } diff --git a/packages/cli/src/commands/onboard.ts b/packages/cli/src/commands/onboard.ts index 45e9c6f6..f2fa5533 100644 --- a/packages/cli/src/commands/onboard.ts +++ b/packages/cli/src/commands/onboard.ts @@ -12,6 +12,7 @@ import { getTelemetry } from "../telemetry"; import { CLIError } from "../util/cli-error"; import { detectCliInvocation, + getCliPrefix, processLauncherContext, } from "../util/package-manager"; @@ -31,9 +32,9 @@ export function getOnboardTrailer(args: { commandsInstalled: boolean; }): string { if (args.commandsInstalled) { - return "Next: in your AI tool, run /tskl onboard or ask it to use the Taskless skill (or run `taskless onboard` from your terminal) to discover rule candidates from your codebase."; + return `Next: in your AI tool, run /tskl onboard or ask it to use the Taskless skill (or run \`${getCliPrefix()} onboard\` from your terminal) to discover rule candidates from your codebase.`; } - return "Next: in your AI tool, ask it to use the Taskless skill (or run `taskless onboard` from your terminal) to discover rule candidates from your codebase."; + return `Next: in your AI tool, ask it to use the Taskless skill (or run \`${getCliPrefix()} onboard\` from your terminal) to discover rule candidates from your codebase.`; } export const onboardCommand = defineCommand({ @@ -97,7 +98,7 @@ export const onboardCommand = defineCommand({ if (alreadyOnboarded && !args.force) { console.log("Taskless onboarding is already marked complete."); console.log( - "Run `taskless onboard --force` to re-run the discovery recipe." + `Run \`${getCliPrefix()} onboard --force\` to re-run the discovery recipe.` ); return; } diff --git a/packages/cli/src/commands/rules.ts b/packages/cli/src/commands/rules.ts index 0adcc14b..189e71c5 100644 --- a/packages/cli/src/commands/rules.ts +++ b/packages/cli/src/commands/rules.ts @@ -43,6 +43,7 @@ import { import { getTelemetry } from "../telemetry"; import { CLIError } from "../util/cli-error"; import { type CLIErrorCode, writeJsonError } from "../types/errors"; +import { getCliPrefix } from "../util/package-manager"; /** Why submitting a request or an iteration failed, as a message and a code. */ function describeSubmitFailure( @@ -251,8 +252,7 @@ const createCommand = defineCommand({ // `create-sg-rule`: authoring an ast-grep rule on-device with no service // call is exactly what anonymous mode asks for, so it is the destination // rather than an `--anonymous` variant of the service recipe. - const message = - "Anonymous rule generation runs in the agent. Run `taskless agent create-sg-rule` to fetch the local-only recipe."; + const message = `Anonymous rule generation runs in the agent. Run \`${getCliPrefix()} agent create-sg-rule\` to fetch the local-only recipe.`; if (args.json) { writeJsonError("INVALID_INPUT", message); } else { @@ -269,7 +269,7 @@ const createCommand = defineCommand({ // 1. Read and validate --from file if (!args.from) { fail( - "--from is required. Provide a path to a JSON file.\n Example: taskless rule create --from request.json", + `--from is required. Provide a path to a JSON file.\n Example: ${getCliPrefix()} rule create --from request.json`, "INVALID_INPUT" ); } @@ -398,8 +398,7 @@ const improveCommand = defineCommand({ } if (args.anonymous) { - const message = - "Anonymous rule improvement runs in the agent. Run `taskless agent improve-rule --anonymous` to fetch the local-only recipe."; + const message = `Anonymous rule improvement runs in the agent. Run \`${getCliPrefix()} agent improve-rule --anonymous\` to fetch the local-only recipe.`; if (args.json) { writeJsonError("INVALID_INPUT", message); } else { @@ -416,7 +415,7 @@ const improveCommand = defineCommand({ // 1. Read and validate --from file if (!args.from) { fail( - "--from is required. Provide a path to a JSON file.\n Example: taskless rule improve --from request.json", + `--from is required. Provide a path to a JSON file.\n Example: ${getCliPrefix()} rule improve --from request.json`, "INVALID_INPUT" ); } @@ -558,7 +557,7 @@ const metaCommand = defineCommand({ `the rule service does not return the metadata block that ` + `.taskless/rule-metadata/ is written from. This is not specific to "${args.id}". ` + `To iterate on a rule, pass its id (the rule's directory name under .taskless/rules//) ` + - `to \`taskless rule improve --from \`, or use the local-only improve flow.`, + `to \`${getCliPrefix()} rule improve --from \`, or use the local-only improve flow.`, "RULE_META_UNAVAILABLE" ); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index b8ba41a4..edded0c3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -31,6 +31,7 @@ import { } from "./util/argv"; import { shouldLaunchWizard } from "./util/interactive"; import { showResolvedUsage } from "./util/help"; +import { getCliPrefix } from "./util/package-manager"; import { CLIError } from "./util/cli-error"; // `satisfies` against the name list (minus `agent`, which is constructed from @@ -140,11 +141,12 @@ const main = defineCommand({ return; } + const prefix = getCliPrefix(); console.error( "Taskless CLI — non-interactive context detected.\n" + " For interactive install, run from a terminal.\n" + - " For scripted install, run `taskless init`.\n" + - " For agent recipes, run `taskless agent` (no args) for the topic index.\n" + ` For scripted install, run \`${prefix} init\`.\n` + + ` For agent recipes, run \`${prefix} agent\` (no args) for the topic index.\n` ); // Forward the parent's rawArgs (e.g. `-d `) so the agent command // doesn't mis-parse them as positional topic names. diff --git a/packages/cli/src/rules/deliver.ts b/packages/cli/src/rules/deliver.ts index c6296635..7b4aafcd 100644 --- a/packages/cli/src/rules/deliver.ts +++ b/packages/cli/src/rules/deliver.ts @@ -8,6 +8,7 @@ import { } from "./layout"; import { ruleDirectory } from "./engines"; import { isMissingDirectory } from "./errno"; +import { getCliPrefix } from "../util/package-manager"; /** One file of a delivered rule, its path relative to the rule directory. */ export interface DeliveredFile { @@ -140,7 +141,7 @@ export function describeMissingFixtures( if (hasFixture) return undefined; return ( `was delivered with no ${prefix} fixtures, so nothing exercises it: ` + - "`taskless test` reports a pass over zero cases, which is " + + `\`${getCliPrefix()} test\` reports a pass over zero cases, which is ` + "indistinguishable from a rule proven to work" ); } diff --git a/packages/cli/src/rules/dispatch.ts b/packages/cli/src/rules/dispatch.ts index 833b3a53..33bf4b7e 100644 --- a/packages/cli/src/rules/dispatch.ts +++ b/packages/cli/src/rules/dispatch.ts @@ -10,6 +10,7 @@ import type { RuntimeRule } from "./runtime/discover"; import { runAstGrepScan } from "./scan"; import { runVale } from "./vale/run"; import { collectNotices } from "../util/notices"; +import { getCliPrefix } from "../util/package-manager"; /** * Whether `.taskless/rules/vale/` holds anything to run. @@ -286,7 +287,7 @@ function describeValeRefusal(refused: RefusedValeConfig): string { ); return [ `Vale did not run: the config of ${rules} was rejected by the config schema. ` + - `Run \`taskless verify\` for the constraint behind each line.`, + `Run \`${getCliPrefix()} verify\` for the constraint behind each line.`, ...lines, ].join("\n"); } diff --git a/packages/cli/src/wizard/index.ts b/packages/cli/src/wizard/index.ts index 993b7476..6628aad7 100644 --- a/packages/cli/src/wizard/index.ts +++ b/packages/cli/src/wizard/index.ts @@ -14,6 +14,7 @@ import { getReloadNotice } from "../install/reload-notice"; import { computeInstallDiff, readInstallState } from "../install/state"; import { getTelemetry } from "../telemetry"; import { CLIError } from "../util/cli-error"; +import { getCliPrefix } from "../util/package-manager"; import { WizardCancelled } from "./ask"; import { getCliVersion, renderIntro } from "./intro"; @@ -64,7 +65,7 @@ export async function runWizard( const proceed = await renderSummaryAndConfirm(diff); if (!proceed) { - cancel("Install cancelled. Run `taskless init` to try again."); + cancel(`Install cancelled. Run \`${getCliPrefix()} init\` to try again.`); cancelledStep = "summary"; return finish({ status: "cancelled" }); } @@ -98,7 +99,7 @@ export async function runWizard( } catch (error) { if (error instanceof WizardCancelled) { cancel( - `Wizard cancelled at step "${error.step}". Run \`taskless init\` to try again.` + `Wizard cancelled at step "${error.step}". Run \`${getCliPrefix()} init\` to try again.` ); cancelledStep = error.step; return finish({ status: "cancelled" }); diff --git a/packages/cli/test/agent-extensions.test.ts b/packages/cli/test/agent-extensions.test.ts index 2d3c1f0d..072a3046 100644 --- a/packages/cli/test/agent-extensions.test.ts +++ b/packages/cli/test/agent-extensions.test.ts @@ -246,9 +246,12 @@ describe("taskless agent ", () => { expect(result.stderr).toContain("Unknown command"); }); - it("points an unknown topic at `taskless agent`, not the removed command", async () => { + it("points an unknown topic at `agent`, not the removed command", async () => { const result = await runCli(["agent", "totally-unknown", "-d", cwd]); - expect(result.stderr).toContain("Run `taskless agent` for available"); + // Through the launcher, not a bare `taskless` that may not be on PATH. + expect(result.stderr).toMatch( + /Run `(?:npx|pnpm dlx) @taskless\/cli@latest agent` for available/ + ); }); // A topic is one token. The old resolver joined positionals, so diff --git a/packages/cli/test/anonymous-flag.test.ts b/packages/cli/test/anonymous-flag.test.ts index 32113487..cd0be778 100644 --- a/packages/cli/test/anonymous-flag.test.ts +++ b/packages/cli/test/anonymous-flag.test.ts @@ -95,7 +95,9 @@ describe("--anonymous flag (per-command behavior matrix)", () => { // file validation. const result = await runCli(["rule", "create", "--anonymous", "-d", cwd]); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("taskless agent create-sg-rule"); + expect(result.stderr).toMatch( + /`(?:npx|pnpm dlx) @taskless\/cli@latest agent create-sg-rule`/ + ); }); it("with --json, emits the standardized envelope", async () => { @@ -124,8 +126,8 @@ describe("--anonymous flag (per-command behavior matrix)", () => { cwd, ]); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain( - "taskless agent improve-rule --anonymous" + expect(result.stderr).toMatch( + /`(?:npx|pnpm dlx) @taskless\/cli@latest agent improve-rule --anonymous`/ ); }); }); diff --git a/packages/cli/test/check.test.ts b/packages/cli/test/check.test.ts index a3fca0a8..84c48f7c 100644 --- a/packages/cli/test/check.test.ts +++ b/packages/cli/test/check.test.ts @@ -56,6 +56,11 @@ describe("check", () => { ]); expect(exitCode).toBe(0); expect(stdout).toContain("No rules configured"); + // The remedy must run as printed: through the launcher, with the + // `--from` that `rule create` refuses to start without. + expect(stdout).toMatch( + /`(?:npx|pnpm dlx) @taskless\/cli@latest rule create --from `/ + ); }); it("does not scaffold .taskless/ in a project that has none", async () => { diff --git a/packages/cli/test/init-no-interactive.test.ts b/packages/cli/test/init-no-interactive.test.ts index 1c79eaad..c060f220 100644 --- a/packages/cli/test/init-no-interactive.test.ts +++ b/packages/cli/test/init-no-interactive.test.ts @@ -188,7 +188,7 @@ describe("taskless init (the batch install)", () => { expect(stdout).toMatch(/Next:.*\/tskl onboard/); expect(stdout).toMatch(/Taskless skill/); - expect(stdout).toMatch(/`taskless onboard`/); + expect(stdout).toMatch(/`(?:npx|pnpm dlx) @taskless\/cli@latest onboard`/); }); it("prints a skill-only trailer when no commands were installed", async () => { @@ -203,7 +203,7 @@ describe("taskless init (the batch install)", () => { expect(stdout).not.toContain("/tskl onboard"); expect(stdout).toMatch(/Taskless skill/); - expect(stdout).toMatch(/`taskless onboard`/); + expect(stdout).toMatch(/`(?:npx|pnpm dlx) @taskless\/cli@latest onboard`/); }); it("prints an upgrade trailer naming the changed directories, before the onboarding trailer", async () => {