diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..03822b9 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npm run type-check + + - name: Lint and format check + run: npx biome check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b0bf8af --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Run tests + run: npm test diff --git a/.gitignore b/.gitignore index b947077..aa59d08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ dist/ +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c6315ec --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,55 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```sh +npm install # install dependencies +npm run build # compile TypeScript to dist/ +npm run dev # watch mode (rebuild on changes) +npm run type-check # typecheck without emitting +npm run test # run all tests +npx vitest run src/__tests__/output.test.ts # run a single test file +npm run format # biome lint + format (auto-fix) +``` + +After building, `npm link` makes the `ol` binary available globally. + +## Architecture + +ESM-only TypeScript CLI using Commander.js. Entry point: `src/index.ts` creates the program and registers four command modules. + +### Command pattern + +Each file in `src/commands/` exports a `registerXxxCommand(program)` function that defines subcommands with Commander's chaining API. Commands use async action handlers. + +### Lib modules + +- **api.ts**: Single `apiRequest(path, body)` function. All Outline API calls are POST with Bearer auth. Wraps requests in spinner with per-endpoint messages (SPINNER_MESSAGES map). +- **auth.ts**: Config stored at `~/.config/outline-cli/config.json`. Token/URL resolution: env var → config file → default. +- **output.ts**: `outputItem()` and `outputList()` handle three output modes (human/json/ndjson). Commands define a `humanFormatter` function and an `essentialKeys` array for JSON field filtering. +- **spinner.ts**: `withSpinner(opts, fn)` wrapper using yocto-spinner. Auto-disables on non-TTY, JSON output, CI, `--no-spinner` flag. +- **markdown.ts**: Terminal markdown rendering via marked + marked-terminal. + +### Key patterns + +- Document IDs are resolved from full URLs or slugs via regex extraction in `resolveId()`. +- Text input for create/update supports `--text` inline or `--file` path, with auto title extraction from markdown headings. +- Delete operations require `--confirm` flag. + +## Testing + +Vitest with module mocking. Tests live in `src/__tests__/`. Common patterns: +- Mock `apiRequest` with `vi.mock()` +- Stub `fetch` globally for API tests +- Capture `console.log` calls in an array for output assertions +- Use `program.exitOverride()` + `program.parseAsync()` to test command parsing +- Auth tests use tmpdir with `process.pid` for filesystem isolation + +## Style + +- Biome: tabs for indentation, auto-sorted imports +- TypeScript strict mode, target ES2022, NodeNext modules +- Avoid `any` types and forced typecasts +- Prefer `for` loops over `.forEach()` diff --git a/README.md b/README.md index 6e7d452..74d75b6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,11 @@ CLI for the [Outline](https://www.getoutline.com) wiki/knowledge base API. ## Install ```sh -npm install -g outline-cli +git clone https://github.com/gnapse/outline-cli.git +cd outline-cli +npm install +npm run build +npm link ``` ## Auth @@ -26,11 +30,13 @@ Self-hosted instances: provide your instance URL during `ol auth login` or set ` ```sh # Search -ol search "query" --limit 10 --collection +ol search "query" --limit 10 --collection --status published -# Documents +# Documents (alias: ol doc) ol document list --collection --sort updatedAt --direction DESC -ol document get +ol document get # renders markdown for terminal +ol document get --raw # outputs raw markdown +ol document open # opens in browser ol document create --title "Title" --collection --file doc.md --publish ol document update --file updated.md ol document delete --confirm @@ -38,7 +44,7 @@ ol document move --collection ol document archive ol document unarchive -# Collections +# Collections (alias: ol col) ol collection list ol collection get ol collection create --name "Engineering" --color "#4CAF50" diff --git a/biome.json b/biome.json index 2e4eff2..8a5225e 100644 --- a/biome.json +++ b/biome.json @@ -1,12 +1,12 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", - "organizeImports": { "enabled": true }, - "linter": { - "enabled": true, - "rules": { "recommended": true } - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - } + "$schema": "https://biomejs.dev/schemas/2.3.12/schema.json", + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { + "enabled": true, + "rules": { "recommended": true } + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + } } diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..41ccee8 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,13 @@ +pre-commit: + commands: + type-check: + run: npm run type-check + biome: + glob: '*.{ts,js,json}' + run: npx biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} + stage_fixed: true + +pre-push: + commands: + test: + run: npm test diff --git a/package-lock.json b/package-lock.json index 3fd0ac0..0526b55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "devDependencies": { "@biomejs/biome": "2.3.12", "@types/node": "25.0.10", + "lefthook": "2.0.15", "typescript": "5.9.3", "vitest": "4.0.18" } @@ -1512,6 +1513,169 @@ "node": ">=8" } }, + "node_modules/lefthook": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.0.15.tgz", + "integrity": "sha512-sl5rePO6UUOLKp6Ci+MMKOc86zicBaPUCvSw2Cq4gCAgTmxpxhIjhz7LOu2ObYerVRPpTq3gvzPTjI71UotjnA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "lefthook": "bin/index.js" + }, + "optionalDependencies": { + "lefthook-darwin-arm64": "2.0.15", + "lefthook-darwin-x64": "2.0.15", + "lefthook-freebsd-arm64": "2.0.15", + "lefthook-freebsd-x64": "2.0.15", + "lefthook-linux-arm64": "2.0.15", + "lefthook-linux-x64": "2.0.15", + "lefthook-openbsd-arm64": "2.0.15", + "lefthook-openbsd-x64": "2.0.15", + "lefthook-windows-arm64": "2.0.15", + "lefthook-windows-x64": "2.0.15" + } + }, + "node_modules/lefthook-darwin-arm64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-darwin-arm64/-/lefthook-darwin-arm64-2.0.15.tgz", + "integrity": "sha512-ygAqG/NzOgY9bEiqeQtiOmCRTtp9AmOd3eyrpEaSrRB9V9f3RHRgWDrWbde9BiHSsCzcbeY9/X2NuKZ69eUsNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/lefthook-darwin-x64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-darwin-x64/-/lefthook-darwin-x64-2.0.15.tgz", + "integrity": "sha512-3wA30CzdSL5MFKD6dk7v8BMq7ScWQivpLbmIn3Pv67AaBavN57N/hcdGqOFnDDFI5WazVwDY7UqDfMIk5HZjEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/lefthook-freebsd-arm64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-arm64/-/lefthook-freebsd-arm64-2.0.15.tgz", + "integrity": "sha512-FbYBBLVbX8BjdO+icN1t/pC3TOW3FAvTKv/zggBKNihv6jHNn/3s/0j2xIS0k0Pw9oOE7MVmEni3qp2j5vqHrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/lefthook-freebsd-x64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-freebsd-x64/-/lefthook-freebsd-x64-2.0.15.tgz", + "integrity": "sha512-udHMjh1E8TfC0Z7Y249XZMATJOyj1Jxlj9JoEinkoBvAsePFKDEQg5teuXuTGhjsHYpqVekfSvLNNfHKUUbbjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/lefthook-linux-arm64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-linux-arm64/-/lefthook-linux-arm64-2.0.15.tgz", + "integrity": "sha512-1HAPmdYhfcOlubv63sTnWtW2rFuC+kT1MvC3JvdrS5V6zrOImbBSnYZMJX/Dd3w4pm0x2ZJb9T+uef8a0jUQkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/lefthook-linux-x64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-linux-x64/-/lefthook-linux-x64-2.0.15.tgz", + "integrity": "sha512-Pho87mlNFH47zc4fPKzQSp8q9sWfIFW/KMMZfx/HZNmX25aUUTOqMyRwaXxtdAo/hNJ9FX4JeuZWq9Y3iyM5VA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/lefthook-openbsd-arm64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-arm64/-/lefthook-openbsd-arm64-2.0.15.tgz", + "integrity": "sha512-pet03Edlj1QeFUgxcIK1xu8CeZA+ejYplvPgdfe//69+vQFGSDaEx3H2mVx8RqzWfmMbijM2/WfkZXR2EVw3bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/lefthook-openbsd-x64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-openbsd-x64/-/lefthook-openbsd-x64-2.0.15.tgz", + "integrity": "sha512-i+a364CcSAeIO5wQzLMHsthHt/v6n3XwhKmRq/VBzPOUv9KutNeF55yCE/6lvuvzwxpdEfBjh6cXPERC0yp98w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/lefthook-windows-arm64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-windows-arm64/-/lefthook-windows-arm64-2.0.15.tgz", + "integrity": "sha512-69u5GdVOT4QIxc2TK5ce0cTXLzwB55Pk9ZnnJNFf1XsyZTGcg9bUWYYTyD12CIIXbVTa0RVXIIrbU9UgP8O1AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/lefthook-windows-x64": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/lefthook-windows-x64/-/lefthook-windows-x64-2.0.15.tgz", + "integrity": "sha512-/zYEndCUgj8XK+4wvLYLRk3AcfKU6zWf2GHx+tcZ4K2bLaQdej4m+OqmQsVpUlF8N2tN9hfwlj1D50uz75LUuQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/package.json b/package.json index c97d29b..cbbb8a9 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,38 @@ { - "name": "outline-cli", - "version": "0.1.0", - "description": "CLI for the Outline wiki/knowledge base API", - "type": "module", - "bin": { - "ol": "./dist/index.js" - }, - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "type-check": "tsc --noEmit", - "format": "biome check --write", - "test": "vitest run", - "test:watch": "vitest" - }, - "keywords": [ - "outline", - "wiki", - "cli" - ], - "license": "MIT", - "dependencies": { - "chalk": "5.6.2", - "commander": "14.0.2", - "marked": "15.0.12", - "marked-terminal": "7.3.0", - "yocto-spinner": "1.0.0" - }, - "devDependencies": { - "@biomejs/biome": "2.3.12", - "@types/node": "25.0.10", - "typescript": "5.9.3", - "vitest": "4.0.18" - } + "name": "outline-cli", + "version": "0.1.0", + "description": "CLI for the Outline wiki/knowledge base API", + "type": "module", + "bin": { + "ol": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "type-check": "tsc --noEmit", + "format": "biome check --write", + "test": "vitest run", + "test:watch": "vitest", + "prepare": "lefthook install" + }, + "keywords": [ + "outline", + "wiki", + "cli" + ], + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "commander": "14.0.2", + "marked": "15.0.12", + "marked-terminal": "7.3.0", + "yocto-spinner": "1.0.0" + }, + "devDependencies": { + "@biomejs/biome": "2.3.12", + "@types/node": "25.0.10", + "lefthook": "2.0.15", + "typescript": "5.9.3", + "vitest": "4.0.18" + } } diff --git a/src/__tests__/api.test.ts b/src/__tests__/api.test.ts index ae41fb6..9b82606 100644 --- a/src/__tests__/api.test.ts +++ b/src/__tests__/api.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../lib/auth.js", () => ({ getApiToken: () => "test-token", @@ -59,12 +59,17 @@ describe("apiRequest", () => { ok: false, status: 401, statusText: "Unauthorized", - json: async () => ({ error: "auth_required", message: "Authentication required" }), + json: async () => ({ + error: "auth_required", + message: "Authentication required", + }), }; (fetch as ReturnType).mockResolvedValue(mockResponse); const { apiRequest } = await import("../lib/api.js"); - await expect(apiRequest("auth.info")).rejects.toThrow("Authentication required"); + await expect(apiRequest("auth.info")).rejects.toThrow( + "Authentication required", + ); }); it("throws generic message when no API error body", async () => { @@ -72,11 +77,15 @@ describe("apiRequest", () => { ok: false, status: 500, statusText: "Internal Server Error", - json: async () => { throw new Error("not json"); }, + json: async () => { + throw new Error("not json"); + }, }; (fetch as ReturnType).mockResolvedValue(mockResponse); const { apiRequest } = await import("../lib/api.js"); - await expect(apiRequest("documents.list")).rejects.toThrow("API error: 500 Internal Server Error"); + await expect(apiRequest("documents.list")).rejects.toThrow( + "API error: 500 Internal Server Error", + ); }); }); diff --git a/src/__tests__/auth.test.ts b/src/__tests__/auth.test.ts index a6c0aeb..c188056 100644 --- a/src/__tests__/auth.test.ts +++ b/src/__tests__/auth.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs"; -import { join } from "node:path"; +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const TEST_HOME = join(tmpdir(), "outline-cli-test-" + process.pid); +const TEST_HOME = join(tmpdir(), `outline-cli-test-${process.pid}`); const TEST_CONFIG_DIR = join(TEST_HOME, ".config", "outline-cli"); const TEST_CONFIG_PATH = join(TEST_CONFIG_DIR, "config.json"); @@ -11,7 +11,7 @@ vi.mock("node:os", async () => { const actual = await vi.importActual("node:os"); return { ...actual, - homedir: () => join(tmpdir(), "outline-cli-test-" + process.pid), + homedir: () => join(tmpdir(), `outline-cli-test-${process.pid}`), }; }); @@ -36,7 +36,10 @@ describe("auth", () => { }); it("getApiToken reads from config file", async () => { - writeFileSync(TEST_CONFIG_PATH, JSON.stringify({ api_token: "file-token" })); + writeFileSync( + TEST_CONFIG_PATH, + JSON.stringify({ api_token: "file-token" }), + ); const { getApiToken } = await import("../lib/auth.js"); expect(getApiToken()).toBe("file-token"); }); @@ -64,7 +67,9 @@ describe("auth", () => { }); it("saveConfig and clearConfig work", async () => { - const { saveConfig, clearConfig, getApiToken } = await import("../lib/auth.js"); + const { saveConfig, clearConfig, getApiToken } = await import( + "../lib/auth.js" + ); saveConfig("test-token", "https://wiki.test.com"); expect(getApiToken()).toBe("test-token"); clearConfig(); diff --git a/src/__tests__/commands.test.ts b/src/__tests__/commands.test.ts index 35fb533..b357c71 100644 --- a/src/__tests__/commands.test.ts +++ b/src/__tests__/commands.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { Command } from "commander"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../lib/auth.js", () => ({ getApiToken: () => "test-token", @@ -32,7 +32,12 @@ describe("search command", () => { (apiRequest as ReturnType).mockResolvedValue({ data: [ { - document: { id: "1", title: "Test Doc", urlId: "test-doc-abc", collectionId: "c1" }, + document: { + id: "1", + title: "Test Doc", + urlId: "test-doc-abc", + collectionId: "c1", + }, context: "Some context here", ranking: 0.9, }, @@ -45,7 +50,14 @@ describe("search command", () => { program.exitOverride(); registerSearchCommand(program); - await program.parseAsync(["node", "ol", "search", "test query", "--limit", "10"]); + await program.parseAsync([ + "node", + "ol", + "search", + "test query", + "--limit", + "10", + ]); expect(apiRequest).toHaveBeenCalledWith("documents.search", { query: "test query", @@ -58,7 +70,12 @@ describe("search command", () => { (apiRequest as ReturnType).mockResolvedValue({ data: [ { - document: { id: "1", title: "Test", urlId: "test-abc", collectionId: "c1" }, + document: { + id: "1", + title: "Test", + urlId: "test-abc", + collectionId: "c1", + }, context: "snippet", ranking: 0.9, }, @@ -109,7 +126,13 @@ describe("document commands", () => { program.exitOverride(); registerDocumentCommand(program); - await program.parseAsync(["node", "ol", "document", "get", "my-doc-abc123"]); + await program.parseAsync([ + "node", + "ol", + "document", + "get", + "my-doc-abc123", + ]); expect(apiRequest).toHaveBeenCalledWith("documents.info", { id: "abc123" }); expect(logs[0]).toContain("# My Doc"); @@ -127,7 +150,16 @@ describe("document commands", () => { program.exitOverride(); registerDocumentCommand(program); - await program.parseAsync(["node", "ol", "document", "list", "--limit", "10", "--offset", "5"]); + await program.parseAsync([ + "node", + "ol", + "document", + "list", + "--limit", + "10", + "--offset", + "5", + ]); expect(apiRequest).toHaveBeenCalledWith("documents.list", { limit: 10, @@ -158,7 +190,9 @@ describe("collection commands", () => { data: [{ id: "c1", name: "Engineering", documentCount: 42 }], }); - const { registerCollectionCommand } = await import("../commands/collection.js"); + const { registerCollectionCommand } = await import( + "../commands/collection.js" + ); const program = new Command(); program.exitOverride(); registerCollectionCommand(program); diff --git a/src/__tests__/output.test.ts b/src/__tests__/output.test.ts index f9b6016..57915e1 100644 --- a/src/__tests__/output.test.ts +++ b/src/__tests__/output.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { outputItem, outputList, getOutputOptions } from "../lib/output.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { getOutputOptions, outputItem, outputList } from "../lib/output.js"; describe("output", () => { let logs: string[]; @@ -44,10 +44,12 @@ describe("output", () => { }); it("getOutputOptions parses flags", () => { - expect(getOutputOptions({ json: true, full: true, ndjson: false })).toEqual({ - json: true, - ndjson: false, - full: true, - }); + expect(getOutputOptions({ json: true, full: true, ndjson: false })).toEqual( + { + json: true, + ndjson: false, + full: true, + }, + ); }); }); diff --git a/src/__tests__/spinner.test.ts b/src/__tests__/spinner.test.ts index a567378..0ed6f9b 100644 --- a/src/__tests__/spinner.test.ts +++ b/src/__tests__/spinner.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { withSpinner, LoadingSpinner } from "../lib/spinner.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { LoadingSpinner, withSpinner } from "../lib/spinner.js"; const mockSpinnerInstance = { start: vi.fn().mockReturnThis(), diff --git a/src/commands/auth.ts b/src/commands/auth.ts index de2d7fd..e4cc5c8 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -1,9 +1,14 @@ import { createInterface } from "node:readline/promises"; import { Writable } from "node:stream"; -import type { Command } from "commander"; import chalk from "chalk"; -import { saveConfig, clearConfig, getTokenSource, getBaseUrl } from "../lib/auth.js"; +import type { Command } from "commander"; import { apiRequest } from "../lib/api.js"; +import { + clearConfig, + getBaseUrl, + getTokenSource, + saveConfig, +} from "../lib/auth.js"; interface TeamInfo { name: string; @@ -26,8 +31,16 @@ async function prompt(question: string): Promise { async function promptSecret(question: string): Promise { process.stdout.write(question); - const mutedOutput = new Writable({ write(_, __, cb) { cb(); } }); - const rl = createInterface({ input: process.stdin, output: mutedOutput, terminal: true }); + const mutedOutput = new Writable({ + write(_, __, cb) { + cb(); + }, + }); + const rl = createInterface({ + input: process.stdin, + output: mutedOutput, + terminal: true, + }); try { const answer = await rl.question(""); process.stdout.write("\n"); @@ -60,10 +73,15 @@ export function registerAuthCommand(program: Command): void { try { const { data } = await apiRequest("auth.info"); console.log( - chalk.green(`Authenticated to ${data.team.name} as ${data.user.name}`), + chalk.green( + `Authenticated to ${data.team.name} as ${data.user.name}`, + ), ); } catch (err) { - console.log(chalk.yellow("Token saved, but could not verify:"), (err as Error).message); + console.log( + chalk.yellow("Token saved, but could not verify:"), + (err as Error).message, + ); } }); @@ -85,7 +103,10 @@ export function registerAuthCommand(program: Command): void { console.log(`Team: ${chalk.bold(data.team.name)}`); console.log(`User: ${data.user.name} (${data.user.email})`); } catch (err) { - console.error(chalk.red("Could not fetch auth info:"), (err as Error).message); + console.error( + chalk.red("Could not fetch auth info:"), + (err as Error).message, + ); process.exit(1); } }); diff --git a/src/commands/collection.ts b/src/commands/collection.ts index 32455a5..89e82ae 100644 --- a/src/commands/collection.ts +++ b/src/commands/collection.ts @@ -1,5 +1,5 @@ -import type { Command } from "commander"; import chalk from "chalk"; +import type { Command } from "commander"; import { apiRequest } from "../lib/api.js"; import { getOutputOptions, outputItem, outputList } from "../lib/output.js"; @@ -30,7 +30,10 @@ function formatCollection(col: Collection): string { } export function registerCollectionCommand(program: Command): void { - const col = program.command("collection").alias("col").description("Manage collections"); + const col = program + .command("collection") + .alias("col") + .description("Manage collections"); col .command("list") @@ -49,7 +52,13 @@ export function registerCollectionCommand(program: Command): void { }, ); - outputList(data, formatCollection, essentialKeys, getOutputOptions(opts), pagination); + outputList( + data, + formatCollection, + essentialKeys, + getOutputOptions(opts), + pagination, + ); }); col diff --git a/src/commands/document.ts b/src/commands/document.ts index f5f9958..9c15d5f 100644 --- a/src/commands/document.ts +++ b/src/commands/document.ts @@ -1,7 +1,7 @@ import { exec } from "node:child_process"; import { readFileSync } from "node:fs"; -import type { Command } from "commander"; import chalk from "chalk"; +import type { Command } from "commander"; import { apiRequest } from "../lib/api.js"; import { getBaseUrl } from "../lib/auth.js"; import { renderMarkdown } from "../lib/markdown.js"; @@ -51,7 +51,10 @@ function formatDocFull(doc: Document): string { return `# ${doc.title}\n\n${doc.text}`; } -function readTextInput(opts: { text?: string; file?: string }): string | undefined { +function readTextInput(opts: { + text?: string; + file?: string; +}): string | undefined { if (opts.file) return readFileSync(opts.file, "utf-8"); return opts.text; } @@ -79,7 +82,10 @@ function extractTitleFromText(text: string): { title?: string; body: string } { } export function registerDocumentCommand(program: Command): void { - const doc = program.command("document").alias("doc").description("Manage documents"); + const doc = program + .command("document") + .alias("doc") + .description("Manage documents"); doc .command("list") @@ -87,7 +93,11 @@ export function registerDocumentCommand(program: Command): void { .option("--collection ", "Filter by collection ID") .option("--limit ", "Max results", "25") .option("--offset ", "Pagination offset", "0") - .option("--sort ", "Sort by field (title|updatedAt|createdAt)", "updatedAt") + .option( + "--sort ", + "Sort by field (title|updatedAt|createdAt)", + "updatedAt", + ) .option("--direction ", "Sort direction (ASC|DESC)", "DESC") .option("--json", "Output JSON") .option("--ndjson", "Output NDJSON") @@ -106,7 +116,13 @@ export function registerDocumentCommand(program: Command): void { body, ); - outputList(data, formatDoc, essentialKeys, getOutputOptions(opts), pagination); + outputList( + data, + formatDoc, + essentialKeys, + getOutputOptions(opts), + pagination, + ); }); doc @@ -165,7 +181,10 @@ export function registerDocumentCommand(program: Command): void { if (opts.json) { outputItem(data, formatDoc, essentialKeys, { json: true }); } else { - console.log(chalk.green(`Created: ${data.title}`), chalk.dim(data.urlId)); + console.log( + chalk.green(`Created: ${data.title}`), + chalk.dim(data.urlId), + ); } }); @@ -194,7 +213,10 @@ export function registerDocumentCommand(program: Command): void { if (opts.json) { outputItem(data, formatDoc, essentialKeys, { json: true }); } else { - console.log(chalk.green(`Updated: ${data.title}`), chalk.dim(data.urlId)); + console.log( + chalk.green(`Updated: ${data.title}`), + chalk.dim(data.urlId), + ); } }); diff --git a/src/commands/search.ts b/src/commands/search.ts index d4f3b59..5fa7a5b 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -1,5 +1,5 @@ -import type { Command } from "commander"; import chalk from "chalk"; +import type { Command } from "commander"; import { apiRequest } from "../lib/api.js"; import { getBaseUrl } from "../lib/auth.js"; import { getOutputOptions, outputList } from "../lib/output.js"; diff --git a/src/index.ts b/src/index.ts index c14f510..fdb5705 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node import { Command } from "commander"; import { registerAuthCommand } from "./commands/auth.js"; -import { registerSearchCommand } from "./commands/search.js"; -import { registerDocumentCommand } from "./commands/document.js"; import { registerCollectionCommand } from "./commands/collection.js"; +import { registerDocumentCommand } from "./commands/document.js"; +import { registerSearchCommand } from "./commands/search.js"; const program = new Command(); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index e8c9114..e0a0aa8 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,12 @@ -import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs"; +import { + existsSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { homedir } from "node:os"; -import { join, dirname } from "node:path"; +import { join } from "node:path"; interface Config { api_token?: string; @@ -59,7 +65,7 @@ export function saveConfig(token: string, baseUrl?: string): void { if (baseUrl) { config.base_url = baseUrl.replace(/\/$/, ""); } - writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n"); + writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`); } export function clearConfig(): void { diff --git a/src/lib/spinner.ts b/src/lib/spinner.ts index e386371..1696ab7 100644 --- a/src/lib/spinner.ts +++ b/src/lib/spinner.ts @@ -1,5 +1,5 @@ -import yoctoSpinner from "yocto-spinner"; import chalk from "chalk"; +import yoctoSpinner from "yocto-spinner"; export interface SpinnerOptions { text: string; diff --git a/tsconfig.json b/tsconfig.json index 4785300..c060b79 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "outDir": "dist", - "rootDir": "src", - "declaration": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/__tests__"] + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/__tests__"] }