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/share-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

`taskless share` prints the Taskless banner over a scannable QR code for taskless.io, with `npx @taskless/cli` beneath it, for showing Taskless to someone in person. The link carries `utm_source=cli`, `utm_medium=share`, and the CLI's base version as `utm_content`, so a visit is credited to the command and to the release that sent it. `--json` prints the URL instead.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ ignores:
# and makes the next one a decision rather than an inheritance.
- "packages/cli/src/schemas/verify-test.ts"
- "packages/cli/src/schemas/index.ts"
# `share` prints the invocation for SOMEONE ELSE to run, beside a QR code
# they scan. A nightly or local build advertising its own pinned launcher
# would send that person to a prerelease, so the release spelling is
# deliberate here on every build target.
- "packages/cli/src/commands/share.ts"
rule:
kind: string_fragment
regex: "@taskless/cli"
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"smol-toml": "^1.6.1",
"sprintf-js": "^1.1.3",
"tsx": "^4.21.0",
"uqr": "^0.1.3",
"yaml": "^2.8.2",
"zod": "^4.3.6"
},
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const SUBCOMMAND_NAMES = [
"init",
"onboard",
"rule",
"share",
"test",
"update",
"verify",
Expand Down
90 changes: 90 additions & 0 deletions packages/cli/src/commands/share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import process from "node:process";

import chalk from "chalk";
import { defineCommand } from "citty";
import { encode, renderUnicodeCompact } from "uqr";

import "../util/color";
import { CLI_VERSION } from "../version";
import { BANNER_WIDTH, renderBanner } from "../wizard/intro";

/**
* The bare minimum to attribute a visit: `source`/`medium` credit the share
* command, `content` carries the base version with any prerelease or build
* suffix cut, so every nightly of a release counts toward that release.
*/
const SHARE_URL = `https://www.taskless.io/?${new URLSearchParams({
utm_source: "cli",
utm_medium: "share",
utm_content: CLI_VERSION.split(/[-+]/)[0] ?? CLI_VERSION,
}).toString()}`;

/**
* The prod invocation, spelled out rather than taken from `buildInvocation()`:
* a nightly or local build would otherwise share its own pinned launcher,
* which is not what anyone being shown the code should run.
*/
const SHARE_CAPTION = "`npx @taskless/cli` // www.taskless.io";

/** Left-pad a block of uncolored text to center it under the banner. */
function center(
block: string,
style: (line: string) => string = (line) => line
): string {
return block
.split("\n")
.map((line) => {
const pad = Math.max(0, Math.floor((BANNER_WIDTH - line.length) / 2));
return " ".repeat(pad) + style(line);
})
.join("\n");
}

/**
* Two modules per line with half blocks, drawn dark-on-white with explicit
* colors so the code reads the same on a light or a dark terminal theme.
*/
function renderColored(url: string): string {
const { data } = encode(url, { ecc: "L", border: 2 });
const lines: string[] = [];
for (let y = 0; y < data.length; y += 2) {
const topRow = data[y] ?? [];
const bottomRow = data[y + 1] ?? [];
const line = topRow.map((top, x) => {
const bottom = bottomRow[x] ?? false;
return top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
});
lines.push(line.join(""));
}
return center(lines.join("\n"), (line) => chalk.bgWhiteBright.black(line));
}

export const shareCommand = defineCommand({
meta: {
name: "share",
description: "Show a QR code for taskless.io",
},
args: {
json: {
type: "boolean",
description: "Output as JSON",
default: false,
},
},
run({ args }) {
if (args.json) {
console.log(JSON.stringify({ url: SHARE_URL }));
return;
}

// Without color the half blocks fall back to the terminal's own
// foreground, which assumes a dark theme; better than no code at all.
const qr =
chalk.level > 0
? renderColored(SHARE_URL)
: center(renderUnicodeCompact(SHARE_URL, { ecc: "L", border: 2 }));
const caption = center(SHARE_CAPTION);

process.stdout.write(`\n${renderBanner()}\n\n${qr}\n\n\n${caption}\n\n`);
},
});
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { infoCommand } from "./commands/info";
import { type SubcommandName } from "./commands/names";
import { onboardCommand } from "./commands/onboard";
import { ruleCommand } from "./commands/rules";
import { shareCommand } from "./commands/share";
import {
getTelemetry,
resolveRunIdentity,
Expand Down Expand Up @@ -47,6 +48,7 @@ const subCommands = {
auth: authCommand,
onboard: onboardCommand,
rule: ruleCommand,
share: shareCommand,
verify: verifyCommand,
test: testCommand,
} satisfies Record<Exclude<SubcommandName, "agent">, unknown>;
Expand Down
15 changes: 12 additions & 3 deletions packages/cli/src/wizard/intro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,24 @@ const BANNER = `
* non-TTY contexts via chalk's built-in detection.
*/
export function renderIntro(): string {
const version = chalk.dim(`v${CLI_VERSION}`);
return `${renderBanner()}\n${version}`;
}

/** The wordmark alone, colored, for callers that lay out their own content. */
export function renderBanner(): string {
// Direct hex instead of chalk.dim(chalk.cyan(…)): the composed form
// emits reset codes at each newline and the reapplication flashes
// brighter on some quadrant chars. A single truecolor attribute
// stays flat across the whole banner.
const coloredBanner = chalk.hex("#2B7384")(BANNER);
const version = chalk.dim(`v${CLI_VERSION}`);
return `${coloredBanner}\n${version}`;
return chalk.hex("#2B7384")(BANNER);
}

/** Columns the widest banner line occupies; every glyph in it is one column. */
export const BANNER_WIDTH = Math.max(
...BANNER.split("\n").map((line) => line.length)
);

export function getCliVersion(): string {
return CLI_VERSION;
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading