diff --git a/.gitignore b/.gitignore index a14702c..f907b21 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules # output out dist +bin *.tgz # code coverage diff --git a/README.md b/README.md index a0e0e47..9e3b285 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,28 @@ To install dependencies: bun install ``` -To run: +Run tests: ```bash -bun run index.ts +bun test ``` -This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +Build single-file executable: + +```bash +bun run build +``` + +Compile + smoke test the binary: + +```bash +bun run test:binary +``` + +CLI help: + +```bash +./bin/job-tracker --help +./bin/job-tracker help add +./bin/job-tracker exists --help +``` diff --git a/SPEC.md b/SPEC.md index 9094d0d..ae72233 100644 --- a/SPEC.md +++ b/SPEC.md @@ -37,7 +37,7 @@ Out of scope: Build command: ```bash -bun build ./src/cli.ts --compile --outfile ./job-tracker +bun build ./src/cli.ts --compile --outfile ./bin/job-tracker ``` ## 4. Package Identity @@ -353,7 +353,7 @@ Build steps: ```bash bun install bun test -bun build ./src/cli.ts --compile --outfile ./job-tracker +bun build ./src/cli.ts --compile --outfile ./bin/job-tracker ``` Package artifacts: diff --git a/docs/IMPLEMENTATION_HANDOFF_PLAN.md b/docs/IMPLEMENTATION_HANDOFF_PLAN.md index 9f330b2..1834843 100644 --- a/docs/IMPLEMENTATION_HANDOFF_PLAN.md +++ b/docs/IMPLEMENTATION_HANDOFF_PLAN.md @@ -84,5 +84,5 @@ ## Build Verification - `bun test` -- `bun build ./src/cli.ts --compile --outfile ./job-tracker` +- `bun build ./src/cli.ts --compile --outfile ./bin/job-tracker` - Smoke run commands against fixture vault. diff --git a/index.ts b/index.ts deleted file mode 100644 index f67b2c6..0000000 --- a/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/job-tracker b/job-tracker index e6f1fe78a5723c708a917324f41b2024ad552cc3..27b6f7df3ca77f15a04861eab72149e49b0ad940 100755 Binary files a/job-tracker and b/job-tracker differ diff --git a/package.json b/package.json index 14a7c37..65215e4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,13 @@ "module": "index.ts", "type": "module", "private": true, + "scripts": { + "test": "bun test", + "build": "mkdir -p ./bin && bun build ./src/cli.ts --compile --outfile ./bin/job-tracker", + "typecheck": "bunx tsc --noEmit", + "smoke:binary": "bun test tests/compiled-binary.smoke.test.ts", + "test:binary": "bun run build && bun run smoke:binary" + }, "devDependencies": { "@types/bun": "latest" }, diff --git a/skill/SKILL.md b/skill/SKILL.md index cf83b0f..1d93827 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -127,5 +127,5 @@ Update flow: - Build command: ```bash -bun build ./src/cli.ts --compile --outfile ./job-tracker +bun build ./src/cli.ts --compile --outfile ./bin/job-tracker ``` diff --git a/src/cli.ts b/src/cli.ts index 068c12d..6eb7fb2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -12,8 +12,110 @@ import { runUpdate } from "./commands/update"; import { CliError, toExitCode } from "./core/errors"; import { printError, printSuccess } from "./output/print"; +function globalHelp(): string { + return [ + "Usage: job-tracker [options]", + "", + "Commands:", + " exists Check for an existing job by Job Spec URL", + " add Add a new job record", + " update Update an existing job record by id", + " find Search job records", + " help Show help for command", + "", + "Global options:", + " --vault-root Vault root path (default: ~/obsidian/crabpot)", + " --format json|text Output format (default: json)", + " --help, -h Show help", + ].join("\n"); +} + +function commandHelp(command: string): string { + if (command === "exists") { + return [ + "Usage: job-tracker exists --job-spec-url [options]", + "", + "Options:", + " --job-spec-url ", + " --canonical-source-url Deprecated alias", + " --vault-root ", + " --format json|text", + ].join("\n"); + } + + if (command === "add") { + return [ + "Usage: job-tracker add --input [options]", + "", + "Options:", + " --input ", + " --vault-root ", + " --format json|text", + ].join("\n"); + } + + if (command === "update") { + return [ + "Usage: job-tracker update --id --patch [options]", + "", + "Options:", + " --id ", + " --patch ", + " --vault-root ", + " --format json|text", + ].join("\n"); + } + + if (command === "find") { + return [ + "Usage: job-tracker find --query [options]", + "", + "Options:", + " --query ", + " --status ", + " --limit ", + " --vault-root ", + " --format json|text", + ].join("\n"); + } + + return globalHelp(); +} + +function parseHelpRequest(argv: string[]): { show: boolean; command?: string } { + if (argv.length === 0) { + return { show: true }; + } + + const first = argv[0]; + const second = argv[1]; + const hasHelpFlag = argv.includes("--help") || argv.includes("-h"); + + if (first === "help") { + return { show: true, command: second }; + } + + if (first === "--help" || first === "-h") { + return { show: true, command: second }; + } + + if (hasHelpFlag && first) { + return { show: true, command: first }; + } + + return { show: false }; +} + async function main(): Promise { - const args = parseArgs(Bun.argv.slice(2)); + const rawArgs = Bun.argv.slice(2); + const help = parseHelpRequest(rawArgs); + + if (help.show) { + process.stdout.write(`${help.command ? commandHelp(help.command) : globalHelp()}\n`); + return 0; + } + + const args = parseArgs(rawArgs); const format = getFormat(args.options); const vaultRoot = getVaultRoot(args.options); @@ -87,7 +189,14 @@ main() process.exit(exitCode); }) .catch((error) => { - const format = getFormat(parseArgs(Bun.argv.slice(2)).options); + let format: "json" | "text" = "json"; + + try { + format = getFormat(parseArgs(Bun.argv.slice(2)).options); + } catch { + format = "json"; + } + printError(format, error); process.exit(toExitCode(error)); }); diff --git a/tests/cli.integration.test.ts b/tests/cli.integration.test.ts index 0fb7200..6a4691c 100644 --- a/tests/cli.integration.test.ts +++ b/tests/cli.integration.test.ts @@ -435,3 +435,23 @@ test("concurrent updates do not corrupt markdown", async () => { await vault.cleanup(); } }); + +test("root help prints usage and exits successfully", () => { + const result = runCli(["--help"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.includes("Usage: job-tracker [options]")).toBe(true); + expect(result.stdout.includes("Commands:")).toBe(true); + expect(result.stdout.includes("exists")).toBe(true); + expect(result.stdout.includes("add")).toBe(true); + expect(result.stdout.includes("update")).toBe(true); + expect(result.stdout.includes("find")).toBe(true); +}); + +test("subcommand help prints command-specific usage", () => { + const result = runCli(["add", "--help"]); + + expect(result.exitCode).toBe(0); + expect(result.stdout.includes("Usage: job-tracker add --input ")).toBe(true); + expect(result.stdout.includes("--input")).toBe(true); +}); diff --git a/tests/compiled-binary.smoke.test.ts b/tests/compiled-binary.smoke.test.ts new file mode 100644 index 0000000..eed0750 --- /dev/null +++ b/tests/compiled-binary.smoke.test.ts @@ -0,0 +1,74 @@ +import { expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createTempVault } from "../src/testing/tempVault"; + +const TEST_FILE_DIR = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = join(TEST_FILE_DIR, ".."); + +function decode(bytes: Uint8Array): string { + return new TextDecoder().decode(bytes).trim(); +} + +test("compiled binary supports add/exists workflow", async () => { + const compileDir = await mkdtemp(join(REPO_ROOT, ".tmp-job-tracker-binary-test-")); + const binaryPath = join(compileDir, "job-tracker-smoke"); + const vault = await createTempVault(); + + try { + const build = Bun.spawnSync({ + cmd: ["bun", "build", "./src/cli.ts", "--compile", "--outfile", binaryPath], + cwd: REPO_ROOT, + stdout: "pipe", + stderr: "pipe", + }); + + expect(build.exitCode).toBe(0); + + const add = Bun.spawnSync({ + cmd: [ + binaryPath, + "add", + "--vault-root", + vault.rootPath, + "--input", + JSON.stringify({ + Company: "Binary Smoke Co", + Role: "Engineer", + "Job Spec": "https://example.com/jobs/binary-smoke-1?utm_source=test", + }), + ], + cwd: REPO_ROOT, + stdout: "pipe", + stderr: "pipe", + }); + + expect(add.exitCode).toBe(0); + const addPayload = JSON.parse(decode(add.stdout)); + expect(addPayload.ok).toBe(true); + + const exists = Bun.spawnSync({ + cmd: [ + binaryPath, + "exists", + "--vault-root", + vault.rootPath, + "--job-spec-url", + "https://example.com/jobs/binary-smoke-1", + ], + cwd: REPO_ROOT, + stdout: "pipe", + stderr: "pipe", + }); + + expect(exists.exitCode).toBe(0); + const existsPayload = JSON.parse(decode(exists.stdout)); + expect(existsPayload.ok).toBe(true); + expect(existsPayload.exists).toBe(true); + expect(existsPayload.match.company).toBe("Binary Smoke Co"); + } finally { + await vault.cleanup(); + await rm(compileDir, { recursive: true, force: true }); + } +});