diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..0ace3f6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build + +on: + push: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + build: + name: build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm check + - run: pnpm build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index bc16c8e..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - -jobs: - check: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version-file: package.json - cache: pnpm - - - run: pnpm install --frozen-lockfile - - run: pnpm check - - run: pnpm test - - run: pnpm build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..c7463d2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,34 @@ +name: CodeQL + +on: + schedule: + - cron: 0 4 * * 1 + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: + - javascript-typescript + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Initializes the CodeQL tools for scanning. + - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality + + - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..770de49 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: Publish + +on: + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + registry-url: https://registry.npmjs.org + cache: pnpm + + - run: pnpm install --frozen-lockfile + - name: determine dist-tag + id: dist-tag + run: | + VERSION=$(node -p "require('./package.json').version") + if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then + echo "tag=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + - name: publish + env: + NPM_TAG: ${{ steps.dist-tag.outputs.tag }} + run: pnpm publish --provenance --access public --no-git-checks --tag "$NPM_TAG" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a88a177..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Release - -on: - push: - tags: - - "[0-9]*" - -permissions: - contents: read - id-token: write - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - persist-credentials: false - - uses: pnpm/action-setup@71c92474e7e4f5bca283fb17ef80fba9cdb2b4b1 # v6 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 - with: - node-version-file: package.json - cache: pnpm - registry-url: https://registry.npmjs.org - - - name: Verify tag matches the package version - env: - REF: ${{ github.ref }} - run: | - tag="${REF#refs/tags/}" - version=$(node -p "require('./package.json').version") - if [ "$tag" != "$version" ]; then - echo "tag ($tag) does not match package.json version ($version)" >&2 - exit 1 - fi - - - run: pnpm install --frozen-lockfile - - run: pnpm check - - run: pnpm test - - run: pnpm build - - - name: Publish to npm - run: pnpm publish --access public --provenance --no-git-checks diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..22678e7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Test + +on: + push: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + test: + name: test on ${{ matrix.os-release }} + runs-on: ${{ matrix.os-release }} + + strategy: + matrix: + os-release: + - ubuntu-latest + - windows-latest + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm test:coverage diff --git a/.gitignore b/.gitignore index b947077..c2bad67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,25 @@ -node_modules/ -dist/ +## Windows +Thumbs.db +Desktop.ini +$RECYCLE.BIN/ + +## OS X +.DS_Store +._* + +## Editors and IDE +*.sublime* +*.code-workspace + +## Node.js +node_modules +*.log + +## Build +build + +## Coverage reports +coverage + +## API keys and secrets +.env diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..1a9e507 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["oxc.oxc-vscode", "editorconfig.editorconfig", "vitest.explorer"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5cb5074 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll": "always", + "source.organizeImports": "always" + }, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "js/ts.preferences.importModuleSpecifierEnding": "js" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..463e36d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2026-09-12 + +### Added + +- First release: projects, tasks, search, create, update, move, archive, delete, changes, status and the MCP bridge over stdio diff --git a/README.md b/README.md index f6d1f88..052ce94 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ The same tools and resources are served either way. ## Contributing -Bug reports and feature requests go in [issues](https://github.com/dotpm/cli/issues). The package builds with `pnpm build`, and `pnpm check` and `pnpm test` are what CI runs. The API contract it talks to is [`@dotpm/api`](https://github.com/dotpm/obsidian-pm/tree/main/packages/api), released from the plugin repository. +Bug reports and feature requests go in [issues](https://github.com/dotpm/cli/issues). The package builds with `pnpm build` into `build/`, and `pnpm check` and `pnpm test:coverage` are what CI runs on Linux and Windows. The API contract it talks to is [`@dotpm/api`](https://github.com/dotpm/obsidian-pm/tree/main/packages/api), released from the plugin repository. ## License diff --git a/oxlint.config.ts b/oxlint.config.ts index 9871951..5d3d8b3 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -3,14 +3,8 @@ import { defineConfig } from 'oxlint' export default defineConfig({ extends: [axiom], - ignorePatterns: ['dist/**'], rules: { - 'no-new': 'off', - 'import/no-default-export': 'off', 'jsdoc/require-param': 'off', - 'jsdoc/require-returns': 'off', - 'typescript/no-non-null-assertion': 'warn', - 'promise/prefer-await-to-then': 'warn', - 'promise/prefer-await-to-callbacks': 'warn' + 'jsdoc/require-returns': 'off' } }) diff --git a/package.json b/package.json index ec1bd74..f826f7b 100644 --- a/package.json +++ b/package.json @@ -2,39 +2,67 @@ "name": "@dotpm/cli", "version": "0.1.0", "description": "Command line for dotpm, the Obsidian project manager: list, search and edit tasks from a terminal or an agent", - "keywords": ["cli", "mcp", "obsidian", "project-management", "tasks"], + "keywords": [ + "cli", + "mcp", + "obsidian", + "project-management", + "tasks" + ], "homepage": "https://github.com/dotpm/cli#readme", + "bugs": { + "url": "https://github.com/dotpm/cli/issues" + }, "license": "MIT", - "repository": "github:dotpm/cli", - "bin": { "dotpm": "./dist/dotpm.js" }, - "files": ["dist"], + "author": "dotpm (https://dotpm.pm)", + "repository": { + "type": "git", + "url": "git+https://github.com/dotpm/cli.git" + }, + "bin": { + "dotpm": "./build/dotpm.js" + }, + "files": [ + "build" + ], "type": "module", - "publishConfig": { "access": "public" }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "tsdown", "check": "run-p check:**", - "check:code": "oxlint ./src", - "check:format": "oxfmt --check ./src", - "check:types": "tsc -noEmit -skipLibCheck", + "check:code": "oxlint ./source", + "check:format": "oxfmt --check ./source", + "check:types": "tsc --noEmit", "fix": "run-p fix:**", - "fix:code": "oxlint --fix ./src", - "fix:format": "oxfmt ./src", + "fix:code": "oxlint --fix ./source", + "fix:format": "oxfmt ./source", + "prepublishOnly": "run-s build", + "preversion": "run-s check test build", "test": "vitest run", - "test:watch": "vitest" + "test:coverage": "vitest run --coverage" }, "devDependencies": { "@2bad/axiom": "2.1.0", "@2bad/tsconfig": "4.0.0", "@dotpm/api": "0.1.0", - "@types/node": "26.4.1", + "@types/node": "26.5.1", + "@vitest/coverage-v8": "5.0.0", "npm-run-all2": "9.0.3", - "oxfmt": "0.66.0", - "oxlint": "1.81.0", + "oxfmt": "0.68.0", + "oxlint": "1.83.0", + "publint": "0.3.24", "tsdown": "0.23.0", - "typescript": "6.0.3", + "typescript": "7.0.2", "vitest": "5.0.0" }, - "packageManager": "pnpm@11.5.3", - "engines": { "node": ">=22" }, - "volta": { "node": "24.15.0", "pnpm": "11.5.3" } + "engines": { + "node": ">=22" + }, + "volta": { + "node": "26.8.1", + "pnpm": "11.27.0" + }, + "packageManager": "pnpm@11.27.0" } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..74be9be --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +minimumReleaseAgeExclude: + - '@2bad/*' + - '@dotpm/*' diff --git a/src/cli.test.ts b/source/cli.test.ts similarity index 99% rename from src/cli.test.ts rename to source/cli.test.ts index 45bfa62..6976511 100644 --- a/src/cli.test.ts +++ b/source/cli.test.ts @@ -1,7 +1,7 @@ import { createRouter } from '@dotpm/api' import { FakeApi } from '@dotpm/api/testing' import { beforeEach, describe, expect, it } from 'vitest' -import { runCli, type CliDeps } from './cli' +import { runCli, type CliDeps } from './cli.ts' const TOKEN = 'secret-token-0123456789' const VAULT = '/vault' diff --git a/src/cli.ts b/source/cli.ts similarity index 99% rename from src/cli.ts rename to source/cli.ts index b8e4b2d..4d43dbd 100644 --- a/src/cli.ts +++ b/source/cli.ts @@ -1,9 +1,9 @@ import { parseArgs, type ParseArgsConfig } from 'node:util' import { ApiRequestError, type ApiErrorCode } from '@dotpm/api/contract' import { HttpApi } from '@dotpm/api/client' -import { ConnectionError, resolveConnection, type Connection } from './connection' -import { bridge } from './mcp' -import { render, type Output } from './render' +import { ConnectionError, resolveConnection, type Connection } from './connection.ts' +import { bridge } from './mcp.ts' +import { render, type Output } from './render.ts' export interface CliDeps { env: Record diff --git a/src/connection.test.ts b/source/connection.test.ts similarity index 99% rename from src/connection.test.ts rename to source/connection.test.ts index 07ebd54..a9e323d 100644 --- a/src/connection.test.ts +++ b/source/connection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { resolveConnection, type ConnectionInput } from './connection' +import { resolveConnection, type ConnectionInput } from './connection.ts' const VAULT = '/home/me/Notes' const SETTINGS = `${VAULT}/.obsidian/plugins/project-manager/data.json` diff --git a/src/connection.ts b/source/connection.ts similarity index 100% rename from src/connection.ts rename to source/connection.ts diff --git a/src/env.d.ts b/source/env.d.ts similarity index 100% rename from src/env.d.ts rename to source/env.d.ts diff --git a/src/main.ts b/source/main.ts similarity index 85% rename from src/main.ts rename to source/main.ts index 7fb1f51..9047310 100644 --- a/src/main.ts +++ b/source/main.ts @@ -3,7 +3,7 @@ import { existsSync, readFileSync } from 'node:fs' import { createInterface } from 'node:readline' import { setTimeout as delay } from 'node:timers/promises' -import { runCli } from './cli' +import { runCli } from './cli.ts' function readFile(path: string): string | null { try { @@ -35,6 +35,12 @@ process.exitCode = await runCli(process.argv.slice(2), { stdout: (text) => process.stdout.write(`${text}\n`), stderr: (text) => process.stderr.write(`${text}\n`), fetch: (request) => fetch(request), - sleep: (ms) => delay(ms, undefined, { signal: interrupt.signal }).catch(() => undefined), + sleep: async (ms) => { + try { + await delay(ms, undefined, { signal: interrupt.signal }) + } catch { + return undefined + } + }, signal: interrupt.signal }) diff --git a/src/mcp.test.ts b/source/mcp.test.ts similarity index 98% rename from src/mcp.test.ts rename to source/mcp.test.ts index d55a147..1d6408b 100644 --- a/src/mcp.test.ts +++ b/source/mcp.test.ts @@ -1,7 +1,7 @@ import { createRouter } from '@dotpm/api' import { FakeApi } from '@dotpm/api/testing' import { describe, expect, it } from 'vitest' -import { bridge } from './mcp' +import { bridge } from './mcp.ts' const TOKEN = 'secret-token-0123456789' diff --git a/src/mcp.ts b/source/mcp.ts similarity index 100% rename from src/mcp.ts rename to source/mcp.ts diff --git a/src/render.ts b/source/render.ts similarity index 100% rename from src/render.ts rename to source/render.ts diff --git a/tsconfig.json b/tsconfig.json index 565d5b1..128c9b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,15 @@ { "extends": "@2bad/tsconfig", - "include": ["src/**/*.ts"], + "include": ["source/**/*", "vitest.config.ts", "tsdown.config.ts"], "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022"], "types": ["node"], - "isolatedModules": true, - "erasableSyntaxOnly": false, "noEmit": true, - "noUnusedLocals": false, - "noUnusedParameters": false, + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": false, "exactOptionalPropertyTypes": false, "noUncheckedIndexedAccess": false, "noPropertyAccessFromIndexSignature": false, - "noImplicitOverride": false + "noUnusedLocals": false, + "noUnusedParameters": false } } diff --git a/tsdown.config.ts b/tsdown.config.ts index c801bca..986c366 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -4,16 +4,14 @@ import { defineConfig } from 'tsdown' const { version } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) as { version: string } export default defineConfig({ - entry: { dotpm: 'src/main.ts' }, - format: 'esm', + entry: { dotpm: 'source/main.ts' }, + outDir: 'build', + format: 'es', platform: 'node', target: 'node22', - outDir: 'dist', + fixedExtension: false, dts: false, - minify: false, - sourcemap: false, - clean: true, + publint: true, hash: false, - outExtensions: () => ({ js: '.js' }), define: { __CLI_VERSION__: JSON.stringify(version) } }) diff --git a/vitest.config.ts b/vitest.config.ts index faa5010..0ee0dc6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,20 @@ -import { defineConfig } from 'vitest/config' +import { coverageConfigDefaults, defineConfig } from 'vitest/config' export default defineConfig({ define: { __CLI_VERSION__: JSON.stringify('0.0.0-test') }, - test: { include: ['src/**/*.test.ts'] } + test: { + exclude: ['build', 'node_modules'], + coverage: { + include: ['source/**/*.ts'], + exclude: ['build', ...coverageConfigDefaults.exclude], + provider: 'v8', + thresholds: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + }, + testTimeout: 30000 + } })