From 35e7de4c3297523d3df4abd2519ac5ed84e661a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1lmi=20=C3=9E=C3=B3r=20Valgeirsson?= Date: Thu, 9 Apr 2026 17:57:30 +0000 Subject: [PATCH 1/2] chore: initial project setup with CLI, CI/CD, and conventions Sets up the kronan-cli project from scratch including source code, tooling config, and automation: - CLI entrypoint and checkout command with tests (commander + clack/prompts) - Conventional Commits guideline added to CLAUDE.md - oxlint + oxfmt for linting and formatting - bun lockfile and tsconfig - GitHub Actions workflows for CI and releases - OpenAPI spec (kronan-openapi.yaml) used to generate typed API client - install.sh for local binary installation - README with usage documentation - kronan/SKILL.md agent skill definition --- .github/workflows/ci.yml | 35 + .github/workflows/release.yml | 81 ++ .gitignore | 41 + .oxlintrc.json | 3 + CLAUDE.md | 115 ++ README.md | 54 + bun.lock | 155 +++ install.sh | 74 ++ kronan-openapi.yaml | 2171 +++++++++++++++++++++++++++++++++ kronan/SKILL.md | 63 + package.json | 39 + src/commands/checkout.test.ts | 29 + src/commands/checkout.ts | 64 + src/index.ts | 24 + tsconfig.json | 26 + 15 files changed, 2974 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .oxlintrc.json create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 bun.lock create mode 100644 install.sh create mode 100644 kronan-openapi.yaml create mode 100644 kronan/SKILL.md create mode 100644 package.json create mode 100644 src/commands/checkout.test.ts create mode 100644 src/commands/checkout.ts create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..aaa7a4d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + +jobs: + ci: + runs-on: ubuntu-slim + steps: + - name: ⬇️ Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: 📦 Install dependencies + run: bun install + + - name: ⚙️ Generate + run: bun run generate + + - name: 🔎 Typecheck + run: bun run typecheck + + - name: 💅 Check formatting + run: bun format:check + + - name: 🔍 Lint + run: bun lint + + - name: 🧪 Test + run: bun test + + - name: 🔨 Build + run: bun run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..766c43c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,81 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + build: + strategy: + matrix: + include: + - os: ubuntu-24.04-slim + target: bun-linux-x64 + artifact: kronan-linux-x64 + - os: ubuntu-24.04-slim + target: bun-linux-arm64 + artifact: kronan-linux-arm64 + - os: macos-latest + target: bun-darwin-arm64 + artifact: kronan-darwin-arm64 + - os: macos-latest + target: bun-darwin-x64 + artifact: kronan-darwin-x64 + + runs-on: ${{ matrix.os }} + + steps: + - name: ⬇️ Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + + - name: 📦 Install dependencies + run: bun install + + - name: 🔨 Build binary + env: + TARGET: ${{ matrix.target }} + OUTFILE: dist/${{ matrix.artifact }} + run: bun run build + + - name: ⬆️ Upload artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ matrix.artifact }} + path: dist/${{ matrix.artifact }} + + release: + needs: build + runs-on: ubuntu-24.04-slim + + steps: + - name: ⬇️ Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: ⬇️ Download artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: artifacts + + - name: 🚀 Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p release + for dir in artifacts/kronan-*; do + name=$(basename "$dir") + cp "$dir/$name" "release/$name" + chmod +x "release/$name" + done + + gh release create "${{ github.ref_name }}" \ + release/kronan-* \ + --title "kronan-cli ${{ github.ref_name }}" \ + --generate-notes \ + --latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e8c153 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# dependencies (bun install) +node_modules + +# generated +generated/ +COMMANDS.md + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +*.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +# Claude local settings +.claude/settings.local.json diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..0d234e9 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "ignorePatterns": ["generated/"] +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f74558c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,115 @@ +--- +description: Use Bun instead of Node.js, npm, pnpm, or vite. +globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json" +alwaysApply: false +--- + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. + +## Git Commits + +Always use [Conventional Commits](https://www.conventionalcommits.org/) format: `[optional scope]: ` diff --git a/README.md b/README.md new file mode 100644 index 0000000..ada4e0c --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# kronan-cli + +A command-line interface for [Kronan](https://kronan.is). Built with [Umbra](https://github.com/ihs7/umbra), it auto-generates commands directly from the [Kronan Public API](https://api.kronan.is/api/v1/schema/redoc/)'s OpenAPI specification. + +## Installation + +### From GitHub Releases + +```bash +curl -fsSL https://raw.githubusercontent.com/thor-coding-cowboys/kronan-cli/main/install.sh | bash +``` + +Supports **macOS** (Intel & Apple Silicon) and **Linux** (x64 & ARM64). + +### Build from source + +Requires [Bun](https://bun.sh) v1.3+: + +```bash +bun install +bun run build +bun run install-local # installs to ~/.local/bin/kronan +``` + +## Authentication + +1. Generate an API token from your [Kronan account settings](https://kronan.is/adgangur/adgangslyklar) +2. Run `kronan auth login` and paste the token when prompted + +The token is stored securely in your system keychain. You can update it at any time by running `kronan auth login` again. + +## Usage + +```bash +kronan --help # list all available commands +kronan --version # show version + +# Examples +kronan me list +kronan products retrieve --sku ABC123 +kronan checkout add --sku ABC123 --quantity 2 +kronan orders list +``` + +Every resource in the [Kronan Public API](https://api.kronan.is/api/v1/schema/redoc/) is available as a CLI command. See [COMMANDS.md](COMMANDS.md) for the full reference. + +## Development + +```bash +bun run generate # regenerate CLI from OpenAPI spec (https://api.kronan.is/api/v1/schema/) +bun run lint # run oxlint +bun run format # format with oxfmt +bun test # run tests +``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..1d061ae --- /dev/null +++ b/bun.lock @@ -0,0 +1,155 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "kronan-cli", + "dependencies": { + "@clack/prompts": "^1.2.0", + "@ihs7/umbra": "^0.5.0", + "commander": "^14.0.3", + "yaml": "^2.8.3", + "zod": "^4.3.6", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^25.5.2", + "@typescript/native-preview": "^7.0.0-dev.20260409.1", + "oxfmt": "^0.44.0", + "oxlint": "^1.59.0", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], + + "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], + + "@ihs7/umbra": ["@ihs7/umbra@0.5.0", "", { "peerDependencies": { "typescript": "^5", "yaml": "^2", "zod": "^4" }, "bin": { "umbra": "dist/generate.js" } }, "sha512-RXj3h9rSYV/u45GrJlHGZU5/1Nc5smsgWzpgUCAIZT2Ld35GAPHMwx4QgQH8h98NhGqlWjrdmn3bx3oihRegHw=="], + + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.44.0", "", { "os": "android", "cpu": "arm" }, "sha512-5UvghMd9SA/yvKTWCAxMAPXS1d2i054UeOf4iFjZjfayTwCINcC3oaSXjtbZfCaEpxgJod7XiOjTtby5yEv/BQ=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.44.0", "", { "os": "android", "cpu": "arm64" }, "sha512-IVudM1BWfvrYO++Khtzr8q9n5Rxu7msUvoFMqzGJVdX7HfUXUDHwaH2zHZNB58svx2J56pmCUzophyaPFkcG/A=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eWCLAIKAHfx88EqEP1Ga2yz7qVcqDU5lemn4xck+07bH182hDdprOHjbogyk0In1Djys3T0/pO2JepFnRJ41Mg=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-eHTBznHLM49++dwz07MblQ2cOXyIgeedmE3Wgy4ptUESj38/qYZyRi1MPwC9olQJWssMeY6WI3UZ7YmU5ggvyQ=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.44.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jLMmbj0u0Ft43QpkUVr/0v1ZfQCGWAvU+WznEHcN3wZC/q6ox7XeSJtk9P36CCpiDSUf3sGnzbIuG1KdEMEDJQ=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-n+A/u/ByK1qV8FVGOwyaSpw5NPNl0qlZfgTBqHeGIqr8Qzq1tyWZ4lAaxPoe5mZqE3w88vn3+jZtMxriHPE7tg=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-5eax+FkxyCqAi3Rw0mrZFr7+KTt/XweFsbALR+B5ljWBLBl8nHe4ADrUnb1gLEfQCJLl+Ca5FIVD4xEt95AwIw=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-58l8JaHxSGOmOMOG2CIrNsnkRJAj0YcHQCmvNACniOa/vd1iRHhlPajczegzS5jwMENlqgreyiTR9iNlke8qCw=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AlObQIXyVRZ96LbtVljtFq0JqH5B92NU+BQeDFrXWBUWlCKAM0wF5GLfIhCLT5kQ3Sl+U0YjRJ7Alqj5hGQaCg=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.44.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-YcFE8/q/BbrCiIiM5piwbkA6GwJc5QqhMQp2yDrqQ2fuVkZ7CInb1aIijZ/k8EXc72qXMSwKpVlBv1w/MsGO/A=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-eOdzs6RqkRzuqNHUX5C8ISN5xfGh4xDww8OEd9YAmc3OWN8oAe5bmlIqQ+rrHLpv58/0BuU48bxkhnIGjA/ATQ=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-YBgNTxntD/QvlFUfgvh8bEdwOhXiquX8gaofZJAwYa/Xp1S1DQrFVZEeck7GFktr24DztsSp8N8WtWCBwxs0Hw=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.44.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-GLIh1R6WHWshl/i4QQDNgj0WtT25aRO4HNUWEoitxiywyRdhTFmFEYT2rXlcl9U6/26vhmOqG5cRlMLG3ocaIA=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gZOpgTlOsLcLfAF9qgpTr7FIIFSKnQN3hDf/0JvQ4CIwMY7h+eilNjxq/CorqvYcEOu+LRt1W4ZS7KccEHLOdA=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1CyS9JTB+pCUFYFI6pkQGGZaT/AY5gnhHVrQQLhFba6idP9AzVYm1xbdWfywoldTYvjxQJV6x4SuduCIfP3W+A=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.44.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bmEv70Ak6jLr1xotCbF5TxIKjsmQaiX+jFRtnGtfA03tJPf6VG3cKh96S21boAt3JZc+Vjx8PYcDuLj39vM2Pw=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWzB+oCpSnP/dmw85eFLAT5o35Ve5pkGS2uF/UCISpIwDqf1xa7OpmtomiqY/Vzg8VyvMbuf6vroF2khF/+1Vg=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-TcWpo18xEIE3AmIG2kpr3kz5IEhQgnx0lazl2+8L+3eTopOAUevQcmlr4nhguImNWz0OMeOZrYZOhJNCf16nlQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-oj8aLkPJZppIM4CMQNsyir9ybM1Xw/CfGPTSsTnzpVGyljgfbdP0EVUlURiGM0BDrmw5psQ6ArmGCcUY/yABaQ=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-etYDw/UaEv936AQUd/CRMBVd+e+XuuU6wC+VzOv1STvsTyZenLChepLWqLtnyTTp4YMlM22ypzogDDwqYxv5cg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TgLc7XVLKH2a4h8j3vn1MDjfK33i9MY60f/bKhRGWyVzbk5LCZ4X01VZG7iHrMmi5vYbAp8//Ponigx03CLsdw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DXyFPf5ZKldMLloRHx/B9fsxsiTQomaw7cmEW3YIJko2HgCh+GUhp9gGYwHrqlLJPsEe3dYj9JebjX92D3j3AA=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-LgvrsdgVLX1qWqIEmNsSmMXJhpAWdtUQ0M+oR0CySwi+9IHWyOGuIL8w8+u/kbZNMyZr4WUyYB5i0+D+AKgkLg=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-bOJhqX/ny4hrFuTPlyk8foSRx/vLRpxJh0jOOKN2NWW6FScXHPAA5rQbrwdQPcgGB5V8Ua51RS03fke8ssBcug=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vVUXxYMF9trXCsz4m9H6U0IjehosVHxBzVgJUxly1uz4W1PdDyicaBnpC0KRXsHYretLVe+uS9pJy8iM57Kujw=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-TULQW8YBPGRWg5yZpFPL54HLOnJ3/HiX6VenDPi6YfxB/jlItwSMFh3/hCeSNbh+DAMaE1Py0j5MOaivHkI/9Q=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gt54Y4eqSgYJ90xipm24xeyaPV854706o/kiT8oZvUt3VDY7qqxdqyGqchMaujd87ib+/MXvnl9WkK8Cc1BExg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3CtsKp7NFB3OfqQzbuAecrY7GIZeiv7AD+xutU4tefVQzlfmTI7/ygWLrvkzsDEjTlMq41rYHxgsn6Yh8tybmA=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-K0diOpT3ncDmOfl9I1HuvpEsAuTxkts0VYwIv/w6Xiy9CdwyPBVX88Ga9l8VlGgMrwBMnSY4xIvVlVY/fkQk7Q=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-xAU7+QDU6kTJJ7mJLOGgo7oOjtAtkKyFZ0Yjdb5cEo3DiCCPFLvyr08rWiQh6evZ7RiUTf+o65NY/bqttzJiQQ=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-KUmZmKlTTyauOnvUNVxK7G40sSSx0+w5l1UhaGsC6KPpOYHenx2oqJTnabmpLJicok7IC+3Y6fXAUOMyexaeJQ=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-4usRxC8gS0PGdkHnRmwJt/4zrQNZyk6vL0trCxwZSsAKM+OxhB8nKiR+mhjdBbl8lbMh2gc3bZpNN/ik8c4c2A=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-s/rNE2gDmbwAOOP493xk2X7M8LZfI1LJFSSW1+yanz3vuQCFPiHkx4GY+O1HuLUDtkzGlhtMrIcxxzyYLv308w=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+yYj1udJa2UvvIUmEm0IcKgc0UlPMgz0nsSTvkPL2y6n0uU5LgIHSwVu4AHhrve6j9BpVSoRksnz8c9QcvITJA=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-bUplUb48LYsB3hHlQXP2ZMOenpieWoOyppLAnnAhuPag3MGPnt+7caxE3w/Vl9wpQsTA3gzLntQi9rxWrs7Xqg=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-/HLsLuz42rWl7h7ePdmMTpHm2HIDmPtcEMYgm5BBEHiEiuNOrzMaUpd2z7UnNni5LGN9obJy2YoAYBLXQwazrA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-rUPy+JnanpPwV/aJCPnxAD1fW50+XPI0VkWr7f0vEbqcdsS8NpB24Rw6RsS7SdpFv8Dw+8ugCwao5nCFbqOUSg=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xkE7puteDS/vUyRngLXW0t8WgdWoS/tfxXjhP/P7SMqPDx+hs44SpssO3h3qmTqECYEuXBUPzcAw5257Ka+ofA=="], + + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + + "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="], + + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260409.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260409.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260409.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260409.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CV1HEMGo1xCySwUJbCQOF+mmrTue8KTJ1Od2kKWhcbOpu8fPBfaqIpbAM6tGLcNEykEjMMTYHc/VTLbMgxdScQ=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260409.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GcRRnaoeZVrbC47woQ/2t3vPoQcTSjsWPEAQGtwNSdw7Z9TKxG4ES22ghJIQXd3ncTRCMJ+XELnnuqxVutkJ9w=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260409.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-7s8DXAa0Xpu/8PEjYIc4I36Ju7eVpoz9k3E+3WQdOF8pIPWYohiOj+zi68m9XYQck+rnkjUFo26ThVKqVetoMA=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "arm" }, "sha512-fOa07JBUXQpEPq+024g346inYZ2xp63ELuoRq6J0jwDWQ/ftCCuvdQNMncwFhsm1qlMdKT3S68NrnSxX16hiaw=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-cGTzTUqRGlIDwdtkDy6qTrvrqpe27W4CdgnFn0FpxpiWnaIi3wqjlzQ1grtqrqainw/yuPy5hn/I86sQgN6nvA=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260409.1", "", { "os": "linux", "cpu": "x64" }, "sha512-lQrbc/BJKBxQrR1ttBDU5sYY1Hb2moFQgHL20T6nbapNqGpK4pzy64p+NK39O93D4omiCSk04pkchBCVrMPSAg=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260409.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-kmCafMo1xZlYx+9WnfpeZJ2tnB/CcJdR8QPX7j9vqcpe51D7b7Intmr921dD48KGpVh5YgjQ1MEFE5mjGqGMaA=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260409.1", "", { "os": "win32", "cpu": "x64" }, "sha512-WRd+JpQipTsE15QgYr3w7J0f1NKvGcq2QEgmcq8hB0WZA1X2WhQopNu+MpPQ3tdDD42VjMhm8ZoB8HpuOoXK5w=="], + + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], + + "fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="], + + "fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="], + + "oxfmt": ["oxfmt@0.44.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.44.0", "@oxfmt/binding-android-arm64": "0.44.0", "@oxfmt/binding-darwin-arm64": "0.44.0", "@oxfmt/binding-darwin-x64": "0.44.0", "@oxfmt/binding-freebsd-x64": "0.44.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.44.0", "@oxfmt/binding-linux-arm-musleabihf": "0.44.0", "@oxfmt/binding-linux-arm64-gnu": "0.44.0", "@oxfmt/binding-linux-arm64-musl": "0.44.0", "@oxfmt/binding-linux-ppc64-gnu": "0.44.0", "@oxfmt/binding-linux-riscv64-gnu": "0.44.0", "@oxfmt/binding-linux-riscv64-musl": "0.44.0", "@oxfmt/binding-linux-s390x-gnu": "0.44.0", "@oxfmt/binding-linux-x64-gnu": "0.44.0", "@oxfmt/binding-linux-x64-musl": "0.44.0", "@oxfmt/binding-openharmony-arm64": "0.44.0", "@oxfmt/binding-win32-arm64-msvc": "0.44.0", "@oxfmt/binding-win32-ia32-msvc": "0.44.0", "@oxfmt/binding-win32-x64-msvc": "0.44.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-lnncqvHewyRvaqdrnntVIrZV2tEddz8lbvPsQzG/zlkfvgZkwy0HP1p/2u1aCDToeg1jb9zBpbJdfkV73Itw+w=="], + + "oxlint": ["oxlint@1.59.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.59.0", "@oxlint/binding-android-arm64": "1.59.0", "@oxlint/binding-darwin-arm64": "1.59.0", "@oxlint/binding-darwin-x64": "1.59.0", "@oxlint/binding-freebsd-x64": "1.59.0", "@oxlint/binding-linux-arm-gnueabihf": "1.59.0", "@oxlint/binding-linux-arm-musleabihf": "1.59.0", "@oxlint/binding-linux-arm64-gnu": "1.59.0", "@oxlint/binding-linux-arm64-musl": "1.59.0", "@oxlint/binding-linux-ppc64-gnu": "1.59.0", "@oxlint/binding-linux-riscv64-gnu": "1.59.0", "@oxlint/binding-linux-riscv64-musl": "1.59.0", "@oxlint/binding-linux-s390x-gnu": "1.59.0", "@oxlint/binding-linux-x64-gnu": "1.59.0", "@oxlint/binding-linux-x64-musl": "1.59.0", "@oxlint/binding-openharmony-arm64": "1.59.0", "@oxlint/binding-win32-arm64-msvc": "1.59.0", "@oxlint/binding-win32-ia32-msvc": "1.59.0", "@oxlint/binding-win32-x64-msvc": "1.59.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-0xBLeGGjP4vD9pygRo8iuOkOzEU1MqOnfiOl7KYezL/QvWL8NUg6n03zXc7ZVqltiOpUxBk2zgHI3PnRIEdAvw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + } +} diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..1f62848 --- /dev/null +++ b/install.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO="thor-coding-cowboys/kronan-cli" +INSTALL_DIR="${HOME}/.local/bin" + +detect_platform() { + local os arch + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + + case "$arch" in + x86_64) arch="x64" ;; + aarch64 | arm64) arch="arm64" ;; + *) + echo "Unsupported architecture: $arch" >&2 + exit 1 + ;; + esac + + case "$os" in + linux | darwin) ;; + *) + echo "Unsupported OS: $os" >&2 + exit 1 + ;; + esac + + echo "${os}-${arch}" +} + +main() { + local platform asset download_url + + if ! command -v curl &>/dev/null; then + echo "Error: curl is required." >&2 + exit 1 + fi + + platform="$(detect_platform)" + asset="kronan-${platform}" + + echo "Fetching latest release of kronan (${platform})..." + + local api_response + api_response="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest")" + + if command -v jq &>/dev/null; then + download_url="$(echo "$api_response" | jq -r ".assets[] | select(.name == \"${asset}\") | .browser_download_url")" + else + download_url="$(echo "$api_response" | grep -o "\"browser_download_url\": \"[^\"]*${asset}\"" | grep -o 'https://[^"]*')" + fi + + if [[ -z "$download_url" ]]; then + echo "Error: could not find asset ${asset} in the latest release." >&2 + exit 1 + fi + + mkdir -p "$INSTALL_DIR" + + echo "Downloading ${asset}..." + curl -fsSL "$download_url" -o "${INSTALL_DIR}/kronan" + chmod +x "${INSTALL_DIR}/kronan" + + local version + version="$("${INSTALL_DIR}/kronan" --version 2>/dev/null || echo "unknown")" + echo "Installed kronan ${version} to ${INSTALL_DIR}/kronan" + + if [[ ":$PATH:" != *":${INSTALL_DIR}:"* ]]; then + echo "Add ${INSTALL_DIR} to your PATH if not already present." + fi +} + +main diff --git a/kronan-openapi.yaml b/kronan-openapi.yaml new file mode 100644 index 0000000..330eccf --- /dev/null +++ b/kronan-openapi.yaml @@ -0,0 +1,2171 @@ +openapi: 3.0.3 +info: + title: Kronan Public API + version: 1.0.0 + description: | + Public API for Kronan SmartStore. + + This API is designed for external systems, mobile apps, and third-party integrations to interact with the Kronan e-commerce platform. + + This API is in BETA and in active development. + + ## Authentication + All endpoints require an `AccessToken` header: `Authorization: AccessToken `. + Tokens are scoped to a user or a customer group. + + Only Users with login type Audkenni can create access tokens. + + Access tokens can be created in settings in User or Customer group pages. + + ## Available resources + - **Me** — Current authenticated identity (user or customer group) + - **Products** — Browse products, search in smart store product selection, view details + - **Categories** — Category tree and paginated product listings + - **Orders** — Order history, details, and line modifications + - **Checkout** — Smart checkout with line management + - **Shopping Notes** — Freeform shopping lists with product linking. Used in Scan and Go + Note: App does not support SKUs in notes yet. + - **Product Lists** — Saved product collections + - **Purchase Stats** — Personal purchase history and frequency data + + ## Coming soon + - Delivery slot browsing and reservation + - Address management + - Payment initiation with gift cards as we can't integrate with 3DS programmatically + - Special search for shopping notes to be able to search for all products in certain stores + + ## Rate limiting + All endpoints are rate limited to 200 requests per 200 seconds per user. Exceeding the limit returns `429 Too Many Requests`. + + ## Conventions + - All monetary values are integers (ISK, no decimals) + - Timestamps are ISO-8601 UTC with trailing `Z` + - Empty arrays return `[]`, never `null` + - Paginated endpoints use `limit`/`offset` parameters + - Response field names are `camelCase` +servers: + - url: https://api.kronan.is +paths: + /api/v1/categories/: + get: + operationId: categories_list + description: Returns the full category tree (3 levels). + summary: List categories + tags: + - categories + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PublicCategory" + description: "" + /api/v1/categories/{slug}/products/: + get: + operationId: categories_products_retrieve + description: Returns a paginated product listing for a category (48 products + per page). + summary: Get category products + parameters: + - in: query + name: page + schema: + type: integer + description: "Page number (default: 1)" + - in: path + name: slug + schema: + type: string + required: true + tags: + - categories + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicCategoryProductList" + description: "" + /api/v1/checkout/: + get: + operationId: checkout_list + description: Returns the active smart checkout for the authenticated user or + customer group. A checkout is auto-created if none exists. + summary: Get active checkout + tags: + - checkout + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PublicCheckout" + description: "" + /api/v1/checkout/lines/: + post: + operationId: checkout_lines_create + description: "Add product lines to the active checkout. Set `replace: true` + to replace all existing lines. Each line requires a SKU and quantity. Products + are validated for availability" + summary: Add or replace checkout lines + tags: + - checkout + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicLinesAddInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicLinesAddInput" + application/json: + schema: + $ref: "#/components/schemas/PublicLinesAddInput" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicCheckout" + description: "" + /api/v1/me/: + get: + operationId: me_list + description: Returns the identity behind the access token. `type` is either + `user` or `customer_group`. `name` is the user's full name or the customer + group name. + summary: Get current identity + tags: + - me + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PublicMe" + description: "" + /api/v1/orders/: + get: + operationId: orders_list + description: Returns a paginated list of orders ordered by most recent first. + summary: List orders + parameters: + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer + - in: query + name: type + schema: + type: string + description: "Filter by order type: delivery, pickup, scan_n_go, digital" + tags: + - orders + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedPublicOrderListResponseList" + description: "" + /api/v1/orders/{token}/: + get: + operationId: orders_retrieve + description: Returns full order details including lines with product thumbnails. + summary: Get order details + parameters: + - in: path + name: token + schema: + type: string + required: true + tags: + - orders + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicOrder" + description: "" + /api/v1/orders/{token}/delete-lines/: + post: + operationId: orders_delete_lines_create + description: Remove specific lines from an order by their IDs. Service lines + and the last remaining line cannot be deleted. Lines where picking has started + are also protected. Returns the updated order. + summary: Delete order lines + parameters: + - in: path + name: token + schema: + type: string + required: true + tags: + - orders + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicLinesDeleteInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicLinesDeleteInput" + application/json: + schema: + $ref: "#/components/schemas/PublicLinesDeleteInput" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicOrder" + description: "" + /api/v1/orders/{token}/lines-toggle-substitution/: + post: + operationId: orders_lines_toggle_substitution_create + description: Toggle whether substitution is allowed for specific order lines. + Only works on orders where modifications are still allowed. Returns the updated + order. + summary: Toggle substitution on order lines + parameters: + - in: path + name: token + schema: + type: string + required: true + tags: + - orders + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicLinesToggleSubstitutionInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicLinesToggleSubstitutionInput" + application/json: + schema: + $ref: "#/components/schemas/PublicLinesToggleSubstitutionInput" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicOrder" + description: "" + /api/v1/orders/{token}/lower-quantity-lines/: + post: + operationId: orders_lower_quantity_lines_create + description: Reduce the quantity of specific order lines. Quantity can only + be lowered, not increased. Set to 0 to remove the line. Lines where picking + has started cannot be modified. Returns the updated order. + summary: Lower quantity on order lines + parameters: + - in: path + name: token + schema: + type: string + required: true + tags: + - orders + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicLinesLowerQuantityInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicLinesLowerQuantityInput" + application/json: + schema: + $ref: "#/components/schemas/PublicLinesLowerQuantityInput" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicOrder" + description: "" + /api/v1/product-lists/: + get: + operationId: product_lists_list + description: Returns a paginated list of product lists owned by the authenticated + user or customer group. + summary: List product lists + parameters: + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer + tags: + - product-lists + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedPublicProductListListResponseList" + description: "" + post: + operationId: product_lists_create + description: Create a new product list with an optional name and description. + summary: Create product list + tags: + - product-lists + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicProductListCreate" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicProductListCreate" + application/json: + schema: + $ref: "#/components/schemas/PublicProductListCreate" + required: true + security: + - AccessTokenAuth: [] + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicProductList" + description: "" + /api/v1/product-lists/{token}/: + get: + operationId: product_lists_retrieve + description: Returns a product list with all items including product details, + pricing, discounts. + summary: Get product list details + parameters: + - in: path + name: token + schema: + type: string + format: uuid + required: true + tags: + - product-lists + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicProductListDetail" + description: "" + patch: + operationId: product_lists_partial_update + description: Update the name or description of a product list. + summary: Update product list + parameters: + - in: path + name: token + schema: + type: string + format: uuid + required: true + tags: + - product-lists + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PatchedPublicProductListCreate" + multipart/form-data: + schema: + $ref: "#/components/schemas/PatchedPublicProductListCreate" + application/json: + schema: + $ref: "#/components/schemas/PatchedPublicProductListCreate" + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicProductList" + description: "" + delete: + operationId: product_lists_destroy + description: Permanently delete a product list and all its items. + summary: Delete product list + parameters: + - in: path + name: token + schema: + type: string + format: uuid + required: true + tags: + - product-lists + security: + - AccessTokenAuth: [] + responses: + "204": + description: No response body + /api/v1/product-lists/{token}/delete-all-items/: + delete: + operationId: product_lists_delete_all_items_destroy + description: Remove all items from a product list without deleting the list + itself. + summary: Delete all items from product list + parameters: + - in: path + name: token + schema: + type: string + format: uuid + required: true + tags: + - product-lists + security: + - AccessTokenAuth: [] + responses: + "204": + description: No response body + /api/v1/product-lists/{token}/sort-items/: + post: + operationId: product_lists_sort_items_create + description: Sort items in the product list by store departments. Returns the + updated list. + summary: Sort product list items + parameters: + - in: path + name: token + schema: + type: string + format: uuid + required: true + tags: + - product-lists + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicProductList" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicProductList" + application/json: + schema: + $ref: "#/components/schemas/PublicProductList" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicProductListDetail" + description: "" + /api/v1/product-lists/{token}/update-item/: + post: + operationId: product_lists_update_item_create + description: Add a product by SKU or update its quantity. Set quantity to 0 + to remove the item. Returns the updated list with full product details. + summary: Add or update item in product list + parameters: + - in: path + name: token + schema: + type: string + format: uuid + required: true + tags: + - product-lists + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicProductListAddItemInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicProductListAddItemInput" + application/json: + schema: + $ref: "#/components/schemas/PublicProductListAddItemInput" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicProductListDetail" + description: "" + /api/v1/product-purchase-stats/: + get: + operationId: product_purchase_stats_list + description: Returns a paginated list of products the user or customer group + has previously purchased, ordered by most recent purchase date. Ignored products + are excluded by default. + summary: List purchase history + parameters: + - in: query + name: include_ignored + schema: + type: boolean + description: Set to true to include ignored products in the results. + - name: limit + required: false + in: query + description: Number of results to return per page. + schema: + type: integer + - name: offset + required: false + in: query + description: The initial index from which to return the results. + schema: + type: integer + tags: + - product-purchase-stats + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PaginatedPublicProductPurchaseStatsListResponseList" + description: "" + /api/v1/product-purchase-stats/{id}/set-ignored/: + patch: + operationId: product_purchase_stats_set_ignored_partial_update + description: Set whether a product is hidden from purchase history. Ignored + products will not appear in the default list. + summary: Set ignored status + parameters: + - in: path + name: id + schema: + type: integer + description: A unique integer value identifying this product purchase stats + v2. + required: true + tags: + - product-purchase-stats + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PatchedPublicToggleIgnoredInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PatchedPublicToggleIgnoredInput" + application/json: + schema: + $ref: "#/components/schemas/PatchedPublicToggleIgnoredInput" + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicProductPurchaseStats" + description: "" + /api/v1/products/{sku}/: + get: + operationId: products_retrieve + description: Returns full product details including price, discounts, tags, + availability. + summary: Get product details + parameters: + - in: path + name: sku + schema: + type: string + description: Product SKU + required: true + tags: + - products + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicProductDetail" + description: "" + /api/v1/products/search/: + post: + operationId: products_search_create + description: "Search for products with pagination and sorting. Only returns + products in the smart store product selection that are available for home + delivery. Set `with_detail: true` for enriched results including discounts + and tags." + summary: Search products + tags: + - products + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicSearchInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicSearchInput" + application/json: + schema: + $ref: "#/components/schemas/PublicSearchInput" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicPaginatedSearchResult" + description: "" + /api/v1/schema/: + get: + operationId: schema_retrieve + description: |- + OpenApi3 schema for this API. Format can be selected via content negotiation. + + - YAML: application/vnd.oai.openapi + - JSON: application/vnd.oai.openapi+json + parameters: + - in: query + name: format + schema: + type: string + enum: + - json + - yaml + - in: query + name: lang + schema: + type: string + enum: + - de + - en + - is + - nb + - sv + tags: + - schema + security: + - AccessTokenAuth: [] + - {} + responses: + "200": + content: + application/vnd.oai.openapi: + schema: + type: object + additionalProperties: {} + application/yaml: + schema: + type: object + additionalProperties: {} + application/vnd.oai.openapi+json: + schema: + type: object + additionalProperties: {} + application/json: + schema: + type: object + additionalProperties: {} + description: "" + /api/v1/shopping-notes/{token}/: + get: + operationId: shopping_notes_retrieve + description: Returns the shopping note for the authenticated user or customer + group. Auto-creates one if none exists. + summary: Get shopping note + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + description: "" + /api/v1/shopping-notes/{token}/add-line/: + post: + operationId: shopping_notes_add_line_create + description: Add a new line to the shopping note. Provide either a `text` (freeform + item) or a `sku` (linked product), with an optional quantity. + summary: Add line to shopping note + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicShoppingNoteLineInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicShoppingNoteLineInput" + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNoteLineInput" + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + description: "" + /api/v1/shopping-notes/{token}/change-line/: + patch: + operationId: shopping_notes_change_line_partial_update + description: Update the text or quantity of an existing shopping note line. + summary: Change shopping note line + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNoteLineChangeInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNoteLineChangeInput" + application/json: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNoteLineChangeInput" + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + description: "" + /api/v1/shopping-notes/{token}/change-placement/: + patch: + operationId: shopping_notes_change_placement_partial_update + description: Change the display order of lines by passing their tokens in the + desired order via `lines_tokens` query parameter. + summary: Reorder shopping note lines + parameters: + - in: query + name: lines_tokens + schema: + type: array + items: + type: string + format: uuid + required: true + explode: true + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNote" + multipart/form-data: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNote" + application/json: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNote" + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + description: "" + /api/v1/shopping-notes/{token}/delete-line/: + delete: + operationId: shopping_notes_delete_line_destroy + description: Remove a line from the shopping note by its token. + summary: Delete shopping note line + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + - in: query + name: token + schema: + type: string + format: uuid + required: true + tags: + - shopping-notes + security: + - AccessTokenAuth: [] + responses: + "204": + description: No response body + /api/v1/shopping-notes/{token}/delete-line-archived/: + delete: + operationId: shopping_notes_delete_line_archived_destroy + description: Remove a specific archived line by its token. Returns the remaining + archived lines. + summary: Delete archived line + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + - in: query + name: token + schema: + type: string + format: uuid + required: true + tags: + - shopping-notes + security: + - AccessTokenAuth: [] + responses: + "204": + description: No response body + /api/v1/shopping-notes/{token}/delete-shopping-note/: + delete: + operationId: shopping_notes_delete_shopping_note_destroy + description: Delete all lines from the shopping note (the note itself is preserved). + summary: Clear shopping note + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + security: + - AccessTokenAuth: [] + responses: + "204": + description: No response body + /api/v1/shopping-notes/{token}/is-eligible-for-store-product-order/: + get: + operationId: shopping_notes_is_eligible_for_store_product_order_retrieve + description: Check if the shopping note contains products that can be ordered + from a store. Returns 204 if eligible, 404 if no matching products found. + summary: Check store product order eligibility + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + security: + - AccessTokenAuth: [] + responses: + "204": + description: No response body + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNoteEligibilityError" + description: "" + /api/v1/shopping-notes/{token}/lines-archived/: + get: + operationId: shopping_notes_lines_archived_list + description: Returns previously completed and archived shopping note lines. + summary: List archived lines + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/PublicShoppingNoteLineArchived" + description: "" + /api/v1/shopping-notes/{token}/store-product-order/: + post: + operationId: shopping_notes_store_product_order_create + description: Reorder shopping note lines to match the store's product aisle + layout for efficient in-store shopping. + summary: Apply store product order + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + multipart/form-data: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + required: true + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + description: "" + /api/v1/shopping-notes/{token}/toggle-complete-on-line/: + patch: + operationId: shopping_notes_toggle_complete_on_line_partial_update + description: Mark a shopping note line as completed or uncompleted. + summary: Toggle line completion + parameters: + - in: path + name: token + schema: + type: string + format: uuid + description: A UUID string identifying this shopping note. + required: true + tags: + - shopping-notes + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNoteLineTokenInput" + multipart/form-data: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNoteLineTokenInput" + application/json: + schema: + $ref: "#/components/schemas/PatchedPublicShoppingNoteLineTokenInput" + security: + - AccessTokenAuth: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/PublicShoppingNote" + description: "" +components: + schemas: + BlankEnum: + enum: + - "" + NullEnum: + enum: + - null + OrderStatusEnum: + enum: + - draft + - unfulfilled + - partially fulfilled + - fulfilled + - canceled + - cloned + type: string + description: |- + * `draft` - Draft + * `unfulfilled` - Unfulfilled + * `partially fulfilled` - Partially fulfilled + * `fulfilled` - Fulfilled + * `canceled` - Canceled + * `cloned` - Cloned + OrderTypeEnum: + enum: + - delivery + - pickup + - scan_n_go + - digital + - digital_card_batch + - dropp + - navision + type: string + description: |- + * `delivery` - Delivery + * `pickup` - Pickup + * `scan_n_go` - Scan'n Go + * `digital` - Digital + * `digital_card_batch` - Digital Card Batch + * `dropp` - Dropp + * `navision` - Navision + PaginatedPublicOrderListResponseList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=200&limit=100 + results: + type: array + items: + $ref: "#/components/schemas/PublicOrderListResponse" + PaginatedPublicProductListListResponseList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=200&limit=100 + results: + type: array + items: + $ref: "#/components/schemas/PublicProductListListResponse" + PaginatedPublicProductPurchaseStatsListResponseList: + type: object + required: + - count + - results + properties: + count: + type: integer + example: 123 + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=400&limit=100 + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?offset=200&limit=100 + results: + type: array + items: + $ref: "#/components/schemas/PublicProductPurchaseStatsListResponse" + PatchedPublicProductListCreate: + type: object + properties: + name: + type: string + maxLength: 100 + description: + type: string + PatchedPublicShoppingNote: + type: object + properties: + token: + type: string + format: uuid + readOnly: true + name: + type: string + maxLength: 255 + lines: + type: array + items: + $ref: "#/components/schemas/PublicShoppingNoteLine" + PatchedPublicShoppingNoteLineChangeInput: + type: object + properties: + token: + type: string + format: uuid + text: + type: string + maxLength: 255 + quantity: + type: integer + minimum: 0 + PatchedPublicShoppingNoteLineTokenInput: + type: object + properties: + token: + type: string + format: uuid + PatchedPublicToggleIgnoredInput: + type: object + properties: + isIgnored: + type: boolean + PublicCategory: + type: object + properties: + slug: + type: string + maxLength: 128 + pattern: ^[-a-zA-Z0-9_]+$ + name: + type: string + maxLength: 128 + backgroundImage: + type: string + format: uri + nullable: true + icon: + type: string + format: uri + nullable: true + children: + type: array + items: + $ref: "#/components/schemas/PublicCategoryLevel1" + readOnly: true + required: + - children + - name + - slug + PublicCategoryLevel1: + type: object + properties: + slug: + type: string + maxLength: 128 + pattern: ^[-a-zA-Z0-9_]+$ + name: + type: string + maxLength: 128 + children: + type: array + items: + $ref: "#/components/schemas/PublicCategoryLevel2" + readOnly: true + required: + - children + - name + - slug + PublicCategoryLevel2: + type: object + properties: + slug: + type: string + maxLength: 128 + pattern: ^[-a-zA-Z0-9_]+$ + name: + type: string + maxLength: 128 + required: + - name + - slug + PublicCategoryProductList: + type: object + properties: + name: + type: string + count: + type: integer + page: + type: integer + pageCount: + type: integer + hasNextPage: + type: boolean + products: + type: array + items: + $ref: "#/components/schemas/PublicProduct" + readOnly: true + required: + - count + - hasNextPage + - name + - page + - pageCount + - products + PublicCheckout: + type: object + properties: + token: + type: string + format: uuid + readOnly: true + lines: + type: array + items: + $ref: "#/components/schemas/PublicCheckoutLine" + readOnly: true + total: + type: integer + readOnly: true + subtotal: + type: integer + readOnly: true + baggingFee: + type: integer + readOnly: true + serviceFee: + type: integer + readOnly: true + shippingFee: + type: integer + readOnly: true + shippingFeeCutoff: + type: integer + readOnly: true + required: + - baggingFee + - lines + - serviceFee + - shippingFee + - shippingFeeCutoff + - subtotal + - token + - total + PublicCheckoutLine: + type: object + properties: + id: + type: integer + readOnly: true + quantity: + type: integer + maximum: 2147483647 + minimum: 1 + product: + allOf: + - $ref: "#/components/schemas/PublicProduct" + readOnly: true + total: + type: integer + readOnly: true + price: + type: integer + readOnly: true + substitution: + type: boolean + required: + - id + - price + - product + - quantity + - total + PublicLineInput: + type: object + properties: + sku: + type: string + maxLength: 40 + quantity: + type: integer + maximum: 500 + minimum: 0 + default: 1 + substitution: + type: boolean + description: Whether substitution is allowed if this product is unavailable. + required: + - sku + PublicLinesAddInput: + type: object + properties: + lines: + type: array + items: + $ref: "#/components/schemas/PublicLineInput" + replace: + type: boolean + default: true + description: If true, replaces all existing checkout lines. If false, adds + to existing lines. + required: + - lines + PublicLinesDeleteInput: + type: object + properties: + lineIds: + type: array + items: + type: integer + required: + - lineIds + PublicLinesLowerQuantityInput: + type: object + properties: + lineIds: + type: array + items: + type: integer + quantity: + type: integer + minimum: 0 + description: New total quantity for the lines. Must be lower than current + quantity. Set to 0 to remove. + required: + - lineIds + - quantity + PublicLinesToggleSubstitutionInput: + type: object + properties: + lineIds: + type: array + items: + type: integer + required: + - lineIds + PublicMe: + type: object + properties: + type: + $ref: "#/components/schemas/PublicMeTypeEnum" + name: + type: string + required: + - name + - type + PublicMeTypeEnum: + enum: + - user + - customer_group + type: string + description: |- + * `user` - user + * `customer_group` - customer_group + PublicOrder: + type: object + description: Full order representation with lines for detail/mutation endpoints. + properties: + token: + type: string + maxLength: 36 + created: + type: string + format: date-time + readOnly: true + status: + $ref: "#/components/schemas/OrderStatusEnum" + type: + nullable: true + oneOf: + - $ref: "#/components/schemas/OrderTypeEnum" + - $ref: "#/components/schemas/BlankEnum" + - $ref: "#/components/schemas/NullEnum" + total: + type: integer + readOnly: true + discount: + type: integer + readOnly: true + deliveryDate: + type: string + format: date + nullable: true + allowAlterOrderLines: + type: boolean + readOnly: true + description: Whether the customer can still modify or remove lines from + this order. + lines: + type: array + items: + $ref: "#/components/schemas/PublicOrderLine" + readOnly: true + required: + - allowAlterOrderLines + - created + - discount + - lines + - total + PublicOrderLine: + type: object + properties: + id: + type: integer + readOnly: true + productName: + type: string + maxLength: 386 + sku: + type: string + quantity: + type: integer + maximum: 2147483647 + minimum: 1 + quantityOrdered: + type: integer + maximum: 2147483647 + minimum: 0 + unitPrice: + type: integer + readOnly: true + substitution: + type: boolean + substitutionForLineId: + type: integer + nullable: true + readOnly: true + description: ID of the original line this line substitutes for, or null. + isMutable: + type: boolean + readOnly: true + description: Whether this line can be modified or deleted. + isLastChance: + type: boolean + thumbnail: + type: string + format: uri + readOnly: true + total: + type: integer + readOnly: true + required: + - id + - isMutable + - productName + - quantity + - sku + - substitutionForLineId + - thumbnail + - total + - unitPrice + PublicOrderListResponse: + type: object + properties: + count: + type: integer + next: + type: string + format: uri + nullable: true + previous: + type: string + format: uri + nullable: true + results: + type: array + items: + $ref: "#/components/schemas/PublicOrderSummary" + required: + - count + - next + - previous + - results + PublicOrderSummary: + type: object + description: Lightweight order representation for list endpoints. + properties: + token: + type: string + maxLength: 36 + created: + type: string + format: date-time + readOnly: true + status: + $ref: "#/components/schemas/OrderStatusEnum" + type: + nullable: true + oneOf: + - $ref: "#/components/schemas/OrderTypeEnum" + - $ref: "#/components/schemas/BlankEnum" + - $ref: "#/components/schemas/NullEnum" + total: + type: integer + readOnly: true + discount: + type: integer + readOnly: true + deliveryDate: + type: string + format: date + nullable: true + allowAlterOrderLines: + type: boolean + readOnly: true + description: Whether the customer can still modify or remove lines from + this order. + required: + - allowAlterOrderLines + - created + - discount + - total + PublicPaginatedSearchResult: + type: object + properties: + count: + type: integer + page: + type: integer + pageCount: + type: integer + hasNextPage: + type: boolean + hits: + type: array + items: + $ref: "#/components/schemas/PublicSearchHit" + required: + - count + - hasNextPage + - hits + - page + - pageCount + PublicProduct: + type: object + description: Standard product representation used across list/search/category + endpoints. + properties: + sku: + type: string + readOnly: true + name: + type: string + maxLength: 128 + thumbnail: + type: string + format: uri + readOnly: true + price: + type: integer + readOnly: true + discountedPrice: + type: integer + readOnly: true + discountPercent: + type: integer + readOnly: true + onSale: + type: boolean + readOnly: true + priceInfo: + type: string + nullable: true + readOnly: true + chargedByWeight: + type: boolean + pricePerKilo: + type: integer + nullable: true + readOnly: true + baseComparisonUnit: + type: string + nullable: true + maxLength: 128 + temporaryShortage: + type: boolean + readOnly: true + required: + - discountPercent + - discountedPrice + - name + - onSale + - price + - priceInfo + - pricePerKilo + - sku + - temporaryShortage + - thumbnail + PublicProductDetail: + type: object + description: Extended product representation with description, full image, and + tags. + properties: + sku: + type: string + readOnly: true + name: + type: string + maxLength: 128 + thumbnail: + type: string + format: uri + readOnly: true + price: + type: integer + readOnly: true + discountedPrice: + type: integer + readOnly: true + discountPercent: + type: integer + readOnly: true + onSale: + type: boolean + readOnly: true + priceInfo: + type: string + nullable: true + readOnly: true + chargedByWeight: + type: boolean + pricePerKilo: + type: integer + nullable: true + readOnly: true + baseComparisonUnit: + type: string + nullable: true + maxLength: 128 + temporaryShortage: + type: boolean + readOnly: true + description: + type: string + image: + type: string + format: uri + readOnly: true + qtyPerBaseCompUnit: + type: number + format: double + nullable: true + countryOfOrigin: + type: string + nullable: true + maxLength: 128 + tags: + type: array + items: + $ref: "#/components/schemas/PublicProductTag" + readOnly: true + required: + - discountPercent + - discountedPrice + - image + - name + - onSale + - price + - priceInfo + - pricePerKilo + - sku + - tags + - temporaryShortage + - thumbnail + PublicProductList: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + token: + type: string + format: uuid + readOnly: true + description: + type: string + required: + - id + - name + - token + PublicProductListAddItemInput: + type: object + properties: + sku: + type: string + maxLength: 40 + quantity: + type: integer + minimum: 0 + required: + - quantity + - sku + PublicProductListCreate: + type: object + properties: + name: + type: string + maxLength: 100 + description: + type: string + required: + - name + PublicProductListDetail: + type: object + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + token: + type: string + format: uuid + readOnly: true + description: + type: string + items: + type: array + items: + $ref: "#/components/schemas/PublicProductListItem" + readOnly: true + required: + - id + - items + - name + - token + PublicProductListItem: + type: object + properties: + id: + type: integer + readOnly: true + quantity: + type: integer + maximum: 2147483647 + minimum: 0 + product: + allOf: + - $ref: "#/components/schemas/PublicProduct" + readOnly: true + required: + - id + - product + - quantity + PublicProductListListResponse: + type: object + properties: + count: + type: integer + next: + type: string + format: uri + nullable: true + previous: + type: string + format: uri + nullable: true + results: + type: array + items: + $ref: "#/components/schemas/PublicProductListWithCount" + required: + - count + - next + - previous + - results + PublicProductListWithCount: + type: object + description: Used in list endpoint where has_products is annotated on the queryset. + properties: + id: + type: integer + readOnly: true + name: + type: string + maxLength: 100 + token: + type: string + format: uuid + readOnly: true + description: + type: string + hasProducts: + type: boolean + readOnly: true + required: + - hasProducts + - id + - name + - token + PublicProductPurchaseStats: + type: object + properties: + id: + type: integer + readOnly: true + product: + allOf: + - $ref: "#/components/schemas/PublicProduct" + readOnly: true + purchaseCount: + type: integer + maximum: 2147483647 + minimum: 0 + quantityPurchased: + type: integer + maximum: 2147483647 + minimum: 0 + averagePurchaseQuantity: + type: number + format: double + nullable: true + lastPurchaseQuantity: + type: integer + maximum: 2147483647 + minimum: 0 + averagePurchaseIntervalDays: + type: number + format: double + nullable: true + firstPurchaseDate: + type: string + format: date + nullable: true + lastPurchaseDate: + type: string + format: date + nullable: true + isIgnored: + type: boolean + required: + - id + - product + PublicProductPurchaseStatsListResponse: + type: object + properties: + count: + type: integer + next: + type: string + format: uri + nullable: true + previous: + type: string + format: uri + nullable: true + results: + type: array + items: + $ref: "#/components/schemas/PublicProductPurchaseStats" + required: + - count + - next + - previous + - results + PublicProductTag: + type: object + properties: + slug: + type: string + name: + type: string + required: + - name + - slug + PublicSearchDetail: + type: object + properties: + discountedPrice: + type: integer + readOnly: true + discountPercent: + type: integer + readOnly: true + onSale: + type: boolean + readOnly: true + tags: + type: array + items: + $ref: "#/components/schemas/PublicProductTag" + readOnly: true + required: + - discountPercent + - discountedPrice + - onSale + - tags + PublicSearchHit: + type: object + properties: + sku: + type: string + name: + type: string + price: + type: integer + readOnly: true + thumbnail: + type: string + nullable: true + readOnly: true + temporaryShortage: + type: boolean + readOnly: true + priceInfo: + type: string + nullable: true + readOnly: true + chargedByWeight: + type: boolean + readOnly: true + pricePerKilo: + type: integer + nullable: true + readOnly: true + baseComparisonUnit: + type: string + nullable: true + readOnly: true + detail: + allOf: + - $ref: "#/components/schemas/PublicSearchDetail" + nullable: true + readOnly: true + required: + - baseComparisonUnit + - chargedByWeight + - detail + - name + - price + - priceInfo + - pricePerKilo + - sku + - temporaryShortage + - thumbnail + PublicSearchInput: + type: object + properties: + query: + type: string + maxLength: 64 + page: + type: integer + minimum: 1 + pageSize: + type: integer + sortBy: + type: string + description: Sort field (e.g. 'price', 'name'). + withDetail: + type: boolean + default: false + description: If true, each hit includes discounted price, discount percent, + and tags. Slower. + required: + - query + PublicShoppingNote: + type: object + properties: + token: + type: string + format: uuid + readOnly: true + name: + type: string + maxLength: 255 + lines: + type: array + items: + $ref: "#/components/schemas/PublicShoppingNoteLine" + required: + - lines + - name + - token + PublicShoppingNoteEligibilityError: + type: object + properties: + detail: + type: string + required: + - detail + PublicShoppingNoteLine: + type: object + properties: + token: + type: string + format: uuid + readOnly: true + text: + type: string + nullable: true + maxLength: 255 + quantity: + type: integer + maximum: 2147483647 + minimum: 0 + nullable: true + product: + allOf: + - $ref: "#/components/schemas/PublicShoppingNoteProduct" + nullable: true + placement: + type: integer + maximum: 2147483647 + minimum: 0 + isCompleted: + type: boolean + required: + - token + PublicShoppingNoteLineArchived: + type: object + properties: + token: + type: string + format: uuid + readOnly: true + text: + type: string + readOnly: true + completedCount: + type: integer + maximum: 2147483647 + minimum: 0 + required: + - text + - token + PublicShoppingNoteLineInput: + type: object + properties: + text: + type: string + description: Freeform text for the line. Either text or sku must be provided. + maxLength: 3000 + sku: + type: string + description: Product SKU to link. Either sku or text must be provided. + maxLength: 32 + quantity: + type: integer + minimum: 0 + default: 0 + PublicShoppingNoteProduct: + type: object + properties: + sku: + type: string + nullable: true + maxLength: 32 + name: + type: string + maxLength: 128 + description: + type: string + thumbnail: + type: string + readOnly: true + required: + - name + - thumbnail + securitySchemes: + AccessTokenAuth: + type: apiKey + in: header + name: Authorization + description: "Use prefix: AccessToken " diff --git a/kronan/SKILL.md b/kronan/SKILL.md new file mode 100644 index 0000000..9f7602a --- /dev/null +++ b/kronan/SKILL.md @@ -0,0 +1,63 @@ +--- +name: kronan +description: Use when the user wants to interact with the Krónan grocery store — searching for products, managing their cart, or browsing orders. Triggers on requests like "add X to my cart", "remove Y", "what's in my cart", "search for X at Krónan", or any grocery shopping task. +--- + +# Krónan CLI Skill + +The `kronan` CLI is available on PATH and provides access to the Krónan grocery store API. + +## Key Commands + +### Cart Management + +**Always use these custom commands for adding and removing items — not `kronan checkout lines`:** + +```sh +# Add a product (quantity defaults to 1) +kronan checkout add --sku +kronan checkout add --sku --quantity + +# Remove a product +kronan checkout remove --sku + +# View current cart +kronan checkout list +``` + +### Product Search + +```sh +kronan products search --query "" +``` + +- If the Icelandic name yields no results, try the English equivalent (e.g. "digestive" instead of "meltingarkex") +- Results include `sku`, `name`, `price`, and `priceInfo` + +### Orders + +```sh +kronan orders list +kronan orders get --token +``` + +## Workflow + +### Adding an item to the cart + +1. Search for the product with `kronan products search` +2. If multiple matches, ask the user which one they want +3. Add with `kronan checkout add --sku ` + +### Removing an item from the cart + +1. If the SKU is already known (e.g. from a previous search or cart listing), remove directly +2. Otherwise fetch the cart with `kronan checkout list` to find the SKU +3. Remove with `kronan checkout remove --sku ` + +## Notes + +- All prices are in ISK (integers, no decimals) +- `kronan checkout remove` works by sending all current lines with the target item set to `quantity: 0` — this is the correct removal mechanism for this API +- `kronan checkout lines` with `replace: true` does NOT work as a removal mechanism — the API ignores it +- Prefer Icelandic product names in responses to the user diff --git a/package.json b/package.json new file mode 100644 index 0000000..b42c1d5 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "kronan-cli", + "version": "0.1.0", + "private": true, + "bin": { + "kronan": "./src/index.ts" + }, + "type": "module", + "scripts": { + "generate": "umbra --openapi kronan-openapi.yaml --out generated/spec.ts --strip-prefix /api/v1", + "generate:docs": "umbra docs --openapi kronan-openapi.yaml --out COMMANDS.md --name kronan --strip-prefix /api/v1", + "prebuild": "bun run generate", + "build": "bun build --compile --sourcemap --define \"KRONAN_CLI_VERSION='$(jq -r .version package.json)'\" ./src/index.ts --outfile ${OUTFILE:-dist/kronan} ${TARGET:+--target=$TARGET}", + "typecheck": "bunx tsgo", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "format": "oxfmt", + "format:check": "oxfmt --check", + "test": "bun test", + "install-local": "bun run build && mkdir -p ~/.local/bin && mv dist/kronan ~/.local/bin/kronan" + }, + "dependencies": { + "@clack/prompts": "^1.2.0", + "@ihs7/umbra": "^0.5.0", + "commander": "^14.0.3", + "yaml": "^2.8.3", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^25.5.2", + "@typescript/native-preview": "^7.0.0-dev.20260409.1", + "oxfmt": "^0.44.0", + "oxlint": "^1.59.0" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/src/commands/checkout.test.ts b/src/commands/checkout.test.ts new file mode 100644 index 0000000..4169bcb --- /dev/null +++ b/src/commands/checkout.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test"; +import { checkoutCommand } from "./checkout"; + +describe("checkoutCommand", () => { + test("returns a valid CLI resource", () => { + const cmd = checkoutCommand(); + expect(cmd.name).toBe("checkout"); + expect(cmd.actions.add).toBeDefined(); + expect(cmd.actions.remove).toBeDefined(); + }); + + test("add action has correct params", () => { + const cmd = checkoutCommand(); + const params = cmd.actions.add!.params!; + expect(params).toHaveLength(2); + const skuParam = params.find((p) => p.name === "sku"); + const quantityParam = params.find((p) => p.name === "quantity"); + expect(skuParam?.required).toBe(true); + expect(quantityParam?.required).toBe(false); + }); + + test("remove action has correct params", () => { + const cmd = checkoutCommand(); + const params = cmd.actions.remove!.params!; + expect(params).toHaveLength(1); + const skuParam = params.find((p) => p.name === "sku"); + expect(skuParam?.required).toBe(true); + }); +}); diff --git a/src/commands/checkout.ts b/src/commands/checkout.ts new file mode 100644 index 0000000..c639cce --- /dev/null +++ b/src/commands/checkout.ts @@ -0,0 +1,64 @@ +import { checkout_lines_create, checkout_list } from "../../generated/spec"; +import type { CliResource } from "@ihs7/umbra"; + +export function checkoutCommand(): CliResource { + return { + name: "checkout", + actions: { + add: { + description: "Add a product to the checkout cart", + params: [ + { + name: "sku", + location: "body", + kind: "string", + required: true, + description: "Product SKU", + }, + { + name: "quantity", + location: "body", + kind: "number", + required: false, + description: "Quantity to add (default: 1)", + }, + ], + handler: async (args) => { + const body = args.body as { sku: string; quantity?: number }; + return checkout_lines_create.handler({ + body: { + lines: [{ sku: body.sku, quantity: body.quantity ?? 1 }], + replace: false, + }, + }); + }, + }, + remove: { + description: "Remove a product from the checkout cart", + params: [ + { + name: "sku", + location: "body", + kind: "string", + required: true, + description: "Product SKU", + }, + ], + handler: async (args) => { + const { sku } = args.body as { sku: string }; + const checkout = (await checkout_list.handler({})) as { + lines: { product: { sku: string }; quantity: number }[]; + }; + const lines = (checkout.lines ?? []).map((l) => ({ + sku: l.product.sku, + quantity: l.product.sku === sku ? 0 : l.quantity, + source: "checkout", + })); + return checkout_lines_create.handler({ + body: { lines, force: false }, + }); + }, + }, + }, + }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..653db0d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env bun + +import { createCli, configure } from "../generated/spec"; +import { checkoutCommand } from "./commands/checkout"; + +declare const KRONAN_CLI_VERSION: string; +const version = typeof KRONAN_CLI_VERSION !== "undefined" ? KRONAN_CLI_VERSION : "dev"; + +const baseUrl = process.env.KRONAN_API_BASE_URL; +if (baseUrl) { + configure({ baseUrl }); +} + +const auth = { + keychain: "kronan-cli", + header: (token: string) => ({ Authorization: `AccessToken ${token}` }), +}; + +await createCli({ + name: "kronan", + version, + auth, + commands: [checkoutCommand()], +}).run(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..15f366f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "allowJs": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + "types": ["bun-types"] + } +} From e46c93be40bd05e3efa4da1a16f864e91a32dc22 Mon Sep 17 00:00:00 2001 From: Ivar Haukur Saevarsson Date: Thu, 9 Apr 2026 19:22:46 +0000 Subject: [PATCH 2/2] chore: update @ihs7/umbra to 0.6.1 (#2) --- bun.lock | 20 ++------------------ package.json | 6 ++---- src/commands/checkout.test.ts | 9 ++++++--- src/commands/checkout.ts | 21 +++++++++------------ src/index.ts | 14 +++++--------- 5 files changed, 24 insertions(+), 46 deletions(-) diff --git a/bun.lock b/bun.lock index 1d061ae..4503854 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,7 @@ "": { "name": "kronan-cli", "dependencies": { - "@clack/prompts": "^1.2.0", - "@ihs7/umbra": "^0.5.0", - "commander": "^14.0.3", + "@ihs7/umbra": "^0.6.1", "yaml": "^2.8.3", "zod": "^4.3.6", }, @@ -24,11 +22,7 @@ }, }, "packages": { - "@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="], - - "@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], - - "@ihs7/umbra": ["@ihs7/umbra@0.5.0", "", { "peerDependencies": { "typescript": "^5", "yaml": "^2", "zod": "^4" }, "bin": { "umbra": "dist/generate.js" } }, "sha512-RXj3h9rSYV/u45GrJlHGZU5/1Nc5smsgWzpgUCAIZT2Ld35GAPHMwx4QgQH8h98NhGqlWjrdmn3bx3oihRegHw=="], + "@ihs7/umbra": ["@ihs7/umbra@0.6.1", "", { "peerDependencies": { "typescript": "^5", "yaml": "^2", "zod": "^4" }, "bin": { "umbra": "dist/generate.js" } }, "sha512-gCwNjUkfVUsU4zjzAJj0khMj73vGPVdH70j/hFmUZfOCul5Uj68MhC2/y5B5tyfx1w2VrLlMKB4lM3gJ5WkrsQ=="], "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.44.0", "", { "os": "android", "cpu": "arm" }, "sha512-5UvghMd9SA/yvKTWCAxMAPXS1d2i054UeOf4iFjZjfayTwCINcC3oaSXjtbZfCaEpxgJod7XiOjTtby5yEv/BQ=="], @@ -128,20 +122,10 @@ "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - - "fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="], - - "fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="], - - "fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="], - "oxfmt": ["oxfmt@0.44.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.44.0", "@oxfmt/binding-android-arm64": "0.44.0", "@oxfmt/binding-darwin-arm64": "0.44.0", "@oxfmt/binding-darwin-x64": "0.44.0", "@oxfmt/binding-freebsd-x64": "0.44.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.44.0", "@oxfmt/binding-linux-arm-musleabihf": "0.44.0", "@oxfmt/binding-linux-arm64-gnu": "0.44.0", "@oxfmt/binding-linux-arm64-musl": "0.44.0", "@oxfmt/binding-linux-ppc64-gnu": "0.44.0", "@oxfmt/binding-linux-riscv64-gnu": "0.44.0", "@oxfmt/binding-linux-riscv64-musl": "0.44.0", "@oxfmt/binding-linux-s390x-gnu": "0.44.0", "@oxfmt/binding-linux-x64-gnu": "0.44.0", "@oxfmt/binding-linux-x64-musl": "0.44.0", "@oxfmt/binding-openharmony-arm64": "0.44.0", "@oxfmt/binding-win32-arm64-msvc": "0.44.0", "@oxfmt/binding-win32-ia32-msvc": "0.44.0", "@oxfmt/binding-win32-x64-msvc": "0.44.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-lnncqvHewyRvaqdrnntVIrZV2tEddz8lbvPsQzG/zlkfvgZkwy0HP1p/2u1aCDToeg1jb9zBpbJdfkV73Itw+w=="], "oxlint": ["oxlint@1.59.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.59.0", "@oxlint/binding-android-arm64": "1.59.0", "@oxlint/binding-darwin-arm64": "1.59.0", "@oxlint/binding-darwin-x64": "1.59.0", "@oxlint/binding-freebsd-x64": "1.59.0", "@oxlint/binding-linux-arm-gnueabihf": "1.59.0", "@oxlint/binding-linux-arm-musleabihf": "1.59.0", "@oxlint/binding-linux-arm64-gnu": "1.59.0", "@oxlint/binding-linux-arm64-musl": "1.59.0", "@oxlint/binding-linux-ppc64-gnu": "1.59.0", "@oxlint/binding-linux-riscv64-gnu": "1.59.0", "@oxlint/binding-linux-riscv64-musl": "1.59.0", "@oxlint/binding-linux-s390x-gnu": "1.59.0", "@oxlint/binding-linux-x64-gnu": "1.59.0", "@oxlint/binding-linux-x64-musl": "1.59.0", "@oxlint/binding-openharmony-arm64": "1.59.0", "@oxlint/binding-win32-arm64-msvc": "1.59.0", "@oxlint/binding-win32-ia32-msvc": "1.59.0", "@oxlint/binding-win32-x64-msvc": "1.59.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-0xBLeGGjP4vD9pygRo8iuOkOzEU1MqOnfiOl7KYezL/QvWL8NUg6n03zXc7ZVqltiOpUxBk2zgHI3PnRIEdAvw=="], - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/package.json b/package.json index b42c1d5..07cef88 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "type": "module", "scripts": { "generate": "umbra --openapi kronan-openapi.yaml --out generated/spec.ts --strip-prefix /api/v1", - "generate:docs": "umbra docs --openapi kronan-openapi.yaml --out COMMANDS.md --name kronan --strip-prefix /api/v1", + "docs": "umbra docs --openapi kronan-openapi.yaml --out COMMANDS.md --name kronan --strip-prefix /api/v1", "prebuild": "bun run generate", "build": "bun build --compile --sourcemap --define \"KRONAN_CLI_VERSION='$(jq -r .version package.json)'\" ./src/index.ts --outfile ${OUTFILE:-dist/kronan} ${TARGET:+--target=$TARGET}", "typecheck": "bunx tsgo", @@ -20,9 +20,7 @@ "install-local": "bun run build && mkdir -p ~/.local/bin && mv dist/kronan ~/.local/bin/kronan" }, "dependencies": { - "@clack/prompts": "^1.2.0", - "@ihs7/umbra": "^0.5.0", - "commander": "^14.0.3", + "@ihs7/umbra": "^0.6.1", "yaml": "^2.8.3", "zod": "^4.3.6" }, diff --git a/src/commands/checkout.test.ts b/src/commands/checkout.test.ts index 4169bcb..ac6feb2 100644 --- a/src/commands/checkout.test.ts +++ b/src/commands/checkout.test.ts @@ -13,17 +13,20 @@ describe("checkoutCommand", () => { const cmd = checkoutCommand(); const params = cmd.actions.add!.params!; expect(params).toHaveLength(2); - const skuParam = params.find((p) => p.name === "sku"); - const quantityParam = params.find((p) => p.name === "quantity"); + const skuParam = params.find((p: { name: string }) => p.name === "sku"); + const quantityParam = params.find((p: { name: string }) => p.name === "quantity"); expect(skuParam?.required).toBe(true); + expect(skuParam?.positional).toBe(true); expect(quantityParam?.required).toBe(false); + expect(quantityParam?.default).toBe(1); }); test("remove action has correct params", () => { const cmd = checkoutCommand(); const params = cmd.actions.remove!.params!; expect(params).toHaveLength(1); - const skuParam = params.find((p) => p.name === "sku"); + const skuParam = params.find((p: { name: string }) => p.name === "sku"); expect(skuParam?.required).toBe(true); + expect(skuParam?.positional).toBe(true); }); }); diff --git a/src/commands/checkout.ts b/src/commands/checkout.ts index c639cce..d53b738 100644 --- a/src/commands/checkout.ts +++ b/src/commands/checkout.ts @@ -1,7 +1,7 @@ import { checkout_lines_create, checkout_list } from "../../generated/spec"; -import type { CliResource } from "@ihs7/umbra"; +import type { CliCommandEntry } from "@ihs7/umbra"; -export function checkoutCommand(): CliResource { +export function checkoutCommand(): CliCommandEntry { return { name: "checkout", actions: { @@ -13,6 +13,7 @@ export function checkoutCommand(): CliResource { location: "body", kind: "string", required: true, + positional: true, description: "Product SKU", }, { @@ -20,16 +21,14 @@ export function checkoutCommand(): CliResource { location: "body", kind: "number", required: false, - description: "Quantity to add (default: 1)", + default: 1, + description: "Quantity to add", }, ], handler: async (args) => { - const body = args.body as { sku: string; quantity?: number }; + const { sku, quantity } = args.body as { sku: string; quantity: number }; return checkout_lines_create.handler({ - body: { - lines: [{ sku: body.sku, quantity: body.quantity ?? 1 }], - replace: false, - }, + body: { lines: [{ sku, quantity }], replace: false }, }); }, }, @@ -41,6 +40,7 @@ export function checkoutCommand(): CliResource { location: "body", kind: "string", required: true, + positional: true, description: "Product SKU", }, ], @@ -52,11 +52,8 @@ export function checkoutCommand(): CliResource { const lines = (checkout.lines ?? []).map((l) => ({ sku: l.product.sku, quantity: l.product.sku === sku ? 0 : l.quantity, - source: "checkout", })); - return checkout_lines_create.handler({ - body: { lines, force: false }, - }); + return checkout_lines_create.handler({ body: { lines, force: false } }); }, }, }, diff --git a/src/index.ts b/src/index.ts index 653db0d..961513d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,18 +7,14 @@ declare const KRONAN_CLI_VERSION: string; const version = typeof KRONAN_CLI_VERSION !== "undefined" ? KRONAN_CLI_VERSION : "dev"; const baseUrl = process.env.KRONAN_API_BASE_URL; -if (baseUrl) { - configure({ baseUrl }); -} - -const auth = { - keychain: "kronan-cli", - header: (token: string) => ({ Authorization: `AccessToken ${token}` }), -}; +if (baseUrl) configure({ baseUrl }); await createCli({ name: "kronan", version, - auth, + auth: { + keychain: "kronan-cli", + header: (token) => ({ Authorization: `AccessToken ${token}` }), + }, commands: [checkoutCommand()], }).run();