Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/runnable-remedy-commands.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 5 additions & 3 deletions packages/cli/src/commands/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { withSurveyInvite } from "../survey/invite";
import { applyCliInvocation } from "../util/invocation";
import {
detectCliInvocation,
getCliPrefix,
processLauncherContext,
} from "../util/package-manager";

Expand Down Expand Up @@ -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 <topic>` for the full recipe (e.g. `taskless agent create-sg-rule`)."
`\nRun \`${prefix} agent <topic>\` for the full recipe (e.g. \`${prefix} agent create-sg-rule\`).`
);
return;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
},
Expand Down
9 changes: 6 additions & 3 deletions packages/cli/src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -174,15 +177,15 @@ 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;
}

const whoami = await fetchWhoami(token);
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;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
try {
Expand Down Expand Up @@ -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 <file>\`.`
);
}
return;
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
);
}
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/commands/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getTelemetry } from "../telemetry";
import { CLIError } from "../util/cli-error";
import {
detectCliInvocation,
getCliPrefix,
processLauncherContext,
} from "../util/package-manager";

Expand All @@ -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({
Expand Down Expand Up @@ -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;
}
Expand Down
13 changes: 6 additions & 7 deletions packages/cli/src/commands/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand All @@ -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"
);
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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"
);
}
Expand Down Expand Up @@ -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/<engine>/) ` +
`to \`taskless rule improve --from <file>\`, or use the local-only improve flow.`,
`to \`${getCliPrefix()} rule improve --from <file>\`, or use the local-only improve flow.`,
"RULE_META_UNAVAILABLE"
);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <path>`) so the agent command
// doesn't mis-parse them as positional topic names.
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/rules/deliver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/rules/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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");
}
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/wizard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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" });
}
Expand Down Expand Up @@ -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" });
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/test/agent-extensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,12 @@ describe("taskless agent <topic>", () => {
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
Expand Down
8 changes: 5 additions & 3 deletions packages/cli/test/anonymous-flag.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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`/
);
});
});
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/test/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file>`/
);
});

it("does not scaffold .taskless/ in a project that has none", async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/init-no-interactive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
Loading