harumi 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ ---
2
+ name: harumi-cli
3
+ description: Guide for using the `harumi` CLI to run local optimization/solver code (Gurobi, OR-Tools, plain Python) on Harumi's infrastructure via the project's self-hosted Gitea repo, to select the backend environment (production vs internal staging), create/manage projects, import a downloaded project export as a new project, browse and edit the project's git repo, inspect and cancel runs, manage datasources, schedules, secrets, organizations, and your profile. Use when the user wants to run, push, or debug a local script against a Harumi project, mentions `harumi init`, `harumi import`, `harumi run`, `harumi runs`, `harumi repo`, `harumi env`, `harumi projects`, `harumi datasources`, `harumi schedules`, `harumi secrets`, `harumi org`, `harumi profile`, asks about switching between production and staging, creating a new Harumi project, importing/uploading a downloaded project zip to the CLI, Harumi kernel specs, Gitea remotes, scratch branches, database connections, running SQL queries against a project datasource, scheduling/cron runs, managing environment variables/secrets, organization members, or needs to fetch/download results or files from a Harumi run/repo.
4
+ ---
5
+
6
+ # Harumi CLI
7
+
8
+ Drives the `harumi` CLI (source: `harumi-cli`, package `harumi`). Every run is git-ref based: code lives in a per-project Harumi Git (Gitea) repo. The CLI auto-manages a scratch branch so users can iterate without manually committing.
9
+
10
+ The CLI targets one of two environments (see [Config & environments](#config--environments)): `production` (default) and `staging` (internal, VPN-only, `git.dev.harumi.io`). Each has its own Supabase, so each has its own login. The git-first `run`/`repo` flow currently only works on `staging` — production Gitea (`git.harumi.io`) is not live yet, so on production only the non-git commands (`projects`/`datasources`/`secrets`/`org`/`profile`) work.
11
+
12
+ For the full flag-by-flag reference and troubleshooting table, see [references/commands.md](references/commands.md).
13
+
14
+ ## VPN requirement
15
+
16
+ The staging endpoints (`api.dev.harumi.io`, `git.dev.harumi.io`) are internal ALBs — anything on the `staging` environment (login, `harumi init`, git push, runs) only works over VPN. Surface a clear network error if the user isn't on VPN.
17
+
18
+ ## Preflight
19
+
20
+ 1. Check CLI is installed: `harumi --version`. Install with `pip install harumi` (or `pip install -e .` from this repo for an unreleased fix).
21
+ 2. Check the user is authenticated. Any command raises `Not logged in. Run harumi login first.` if not. **Never try to automate the OTP flow** — it emails a one-time code and needs interactive input. Ask the user to run it:
22
+ - `harumi login` for an existing account.
23
+ - `harumi login --signup` for a brand-new email (plain login 422s with "Signups not allowed for otp" on new emails).
24
+ 3. `harumi login` also provisions a per-user Gitea token via `POST /api/git/credentials`. The token is stored in `~/.harumi/credentials.json`.
25
+
26
+ ## The always-bound-repo invariant
27
+
28
+ Every `harumi run` (and every command that accepts `--project`) requires either an explicit `--project <ID>`, or the working directory (or a parent) to be bound to a Harumi project via `harumi init`. Without either, the command exits with a clear "provide --project or run `harumi init`" error.
29
+
30
+ ## Workflow
31
+
32
+ ### 1. Bind a directory to a project
33
+
34
+ Run once per project directory:
35
+
36
+ ```bash
37
+ harumi init --project <PROJECT_ID>
38
+ ```
39
+
40
+ This fetches the Gitea repo metadata (`GET /projects/{id}/repo`), writes `.harumi/config.json`, and configures the `harumi` git remote for authenticated HTTPS pushes.
41
+
42
+ Find project IDs with: `harumi projects list` (or `harumi notebooks` for the legacy notebook-centric view).
43
+
44
+ **Creating a brand-new project instead of binding to an existing one:**
45
+
46
+ ```bash
47
+ harumi projects create "My Project" [--customer-id ID] [--template-id ID]
48
+ ```
49
+
50
+ Calls `POST /projects`, then fetches its repo and binds the current directory the same way `harumi init` does (pass `--no-bind` to skip that). If the backend hasn't provisioned a Gitea repo for the project, the CLI still creates the bare project and prints a warning instead of failing.
51
+
52
+ **Importing a downloaded project export (e.g. from the web app's "Download project" button) as a new project:**
53
+
54
+ ```bash
55
+ harumi import [PATH] [--project-name NAME] [--from-git URL]
56
+ ```
57
+
58
+ `PATH` must be an **unzipped folder** (default: current directory) — unzip the export first. Creates a project, then pushes the whole folder as the repo's initial commit and binds the directory, same as `projects create` above. `--from-git URL` additionally clones an old GitHub repo flat into the folder before pushing (exported files win on any filename collision). See [references/commands.md#import](references/commands.md#import) for the full flag/behavior breakdown.
59
+
60
+ ### 2. Run code
61
+
62
+ **Default — scratch branch (for uncommitted/unpushed work):**
63
+
64
+ ```bash
65
+ harumi run
66
+ ```
67
+
68
+ The CLI detects a dirty or unpushed tree, transparently pushes a throwaway branch (`harumi-scratch/<user>/<timestamp>`) to Gitea, queues the run against that ref, and cleans up the scratch branch when done. The user's real branches are never touched.
69
+
70
+ If the tree is clean and fully pushed, it runs the current branch directly — no scratch branch needed.
71
+
72
+ **Run a specific branch or commit:**
73
+
74
+ ```bash
75
+ harumi run --branch feature/solver-v2
76
+ harumi run --commit abc123f
77
+ ```
78
+
79
+ **Override the `harumi.toml` command or kernel:**
80
+
81
+ ```bash
82
+ harumi run --command "python solver.py" --kernel gurobi_python_medium
83
+ ```
84
+
85
+ **Block until done and download output artifacts:**
86
+
87
+ ```bash
88
+ harumi run --watch --output-dir ./out
89
+ ```
90
+
91
+ ### 3. Inspect and manage runs
92
+
93
+ ```bash
94
+ harumi runs list # table of runs for the bound project, newest first
95
+ harumi runs get <RUN_ID> # status, git ref, exit code, stdout/stderr/error
96
+ harumi runs cancel <RUN_ID> # cancel an in-flight run
97
+ ```
98
+
99
+ `harumi outputs` still works as a thin backwards-compatible wrapper (`--latest`, `--download <RUN_ID>`), but prefer `harumi runs` for new usage.
100
+
101
+ ## Manage the project's repo directly
102
+
103
+ `harumi repo` reads and writes the project's Gitea repo through harumi-api's git router — no local git clone required for file-level edits. Every write lands in a single commit via the batch changes endpoint.
104
+
105
+ ```bash
106
+ harumi repo ls [--ref BRANCH] # list every file (flat, recursive)
107
+ harumi repo cat <path> [--ref BRANCH] [--output FILE] # print or save a file's content
108
+ harumi repo put <local_file> <repo_path> [-m MSG] [--branch B] # create/update as one commit
109
+ harumi repo rm <path> [-m MSG] [--branch B] # delete a file or folder, one commit
110
+ harumi repo mv <from> <to> [-m MSG] [--branch B] # rename/move, one commit
111
+ harumi repo download -o out.zip [--path DIR] [--ref REF] # download repo/folder as a zip
112
+ harumi repo branches # list versions; live branch flagged
113
+ harumi repo branch-create <name> [--from BRANCH]
114
+ harumi repo branch-rm <name>
115
+ harumi repo promote <name> [--title T] [--delete-after] # merge a version into live
116
+ ```
117
+
118
+ All `repo` subcommands accept `--project` to override the `.harumi` binding.
119
+
120
+ ## Manage datasources
121
+
122
+ `harumi datasources` manages project-scoped database connections. Credentials are **always prompted interactively with hidden input** — the CLI never accepts them as a flag, and the server never returns them back (stored in AWS SSM).
123
+
124
+ ```bash
125
+ harumi datasources list # table of datasources for the bound project
126
+ harumi datasources get <name> # detail view (no credentials)
127
+ harumi datasources add <name> --type postgresql --host ... --port 5432 --database ... --username ...
128
+ harumi datasources update <name> --host newhost --set-credentials
129
+ harumi datasources remove <name>
130
+ harumi datasources test --type postgresql --host ... --port 5432 --database ... --username ...
131
+ harumi datasources query <name> --sql "SELECT * FROM orders LIMIT 10"
132
+ ```
133
+
134
+ `query` is the most useful command for iteration: it runs SQL against the real datasource through a server-side proxy that **only allows `SELECT`/`WITH`** (any destructive keyword is rejected with a 403) and **caps rows** (default limit 10000, server max 100000). Use it to validate a query before hardcoding it into solver code. Add `--csv <path>` to save results instead of printing a table.
135
+
136
+ All `datasources` subcommands accept `--project` to override the `.harumi` binding.
137
+
138
+ ## Schedule runs
139
+
140
+ `harumi schedules` manages project-scoped cron schedules for git-ref runs (`/projects/{project_id}/schedules`).
141
+
142
+ ```bash
143
+ harumi schedules list # table of schedules for the bound project
144
+ harumi schedules get <SCHEDULE_ID> # detail view
145
+ harumi schedules add --cron "0 9 * * *" --git-branch main [--start-at ISO] [--kernel ...] [--email-to ...]
146
+ harumi schedules update <SCHEDULE_ID> --cron "0 */6 * * *"
147
+ harumi schedules remove <SCHEDULE_ID>
148
+ ```
149
+
150
+ Key semantics:
151
+
152
+ - **Cron is a raw 5-field expression, interpreted in UTC.** The CLI does not validate it client-side — the server validates with `croniter` and returns a clear 400 on a bad expression. Don't try to build a calendar/human-friendly UI; just pass the cron string.
153
+ - **No pause/enable flag exists.** The only way to stop a schedule from firing is `harumi schedules remove`.
154
+ - **No separate "run now" for a schedule.** Immediate execution is already covered by `harumi run` (git-ref based); schedules only control recurring runs.
155
+
156
+ All `schedules` subcommands accept `--project` to override the `.harumi` binding.
157
+
158
+ ## Manage secrets
159
+
160
+ `harumi secrets` manages project-scoped environment variables, injected into kernels/apps. Values are stored as SSM SecureStrings; there is no update endpoint — `set` on an existing name overwrites it.
161
+
162
+ ```bash
163
+ harumi secrets list # names only, never values
164
+ harumi secrets set <NAME> # prompts for the value (hidden input)
165
+ harumi secrets rm <NAME>
166
+ ```
167
+
168
+ ## Manage organizations and your profile
169
+
170
+ ```bash
171
+ harumi org list
172
+ harumi org create <BUSINESS_NAME>
173
+ harumi org rename <ORG_ID> <NEW_NAME>
174
+ harumi org delete <ORG_ID>
175
+ harumi org members <ORG_ID>
176
+ harumi org invite <ORG_ID> --email a@b.com --role member
177
+ harumi org role <ORG_ID> <USER_ID> --role admin
178
+ harumi org remove <ORG_ID> <USER_ID>
179
+
180
+ harumi profile show
181
+ harumi profile set --first-name Ana --bio "..."
182
+ ```
183
+
184
+ ## Config & environments
185
+
186
+ There are two built-in environments, selectable in the CLI:
187
+
188
+ | Env | API | Gitea | Access |
189
+ |---|---|---|---|
190
+ | `production` (default) | `https://api.harumi.io/api` | `https://git.harumi.io` | public |
191
+ | `staging` | `https://api.dev.harumi.io/api` | `https://git.dev.harumi.io` | internal, VPN-only |
192
+
193
+ Auth flows through harumi-api (`/users/otp`, `/users/refresh`), and each environment has its own Supabase — so **each environment has its own stored session**. You must `harumi login` once per environment; switching does not log you out of the other.
194
+
195
+ ```bash
196
+ harumi env list # production only (staging is hidden by default)
197
+ harumi env list --all # include internal/VPN-only envs (or set HARUMI_INTERNAL=1)
198
+ harumi env current # show the active env + endpoints
199
+ harumi env use staging # persist the default (internal devs, VPN required)
200
+ harumi --env staging run # override for a single command
201
+ ```
202
+
203
+ Staging is **internal-only**: it's hidden from `env list`/help for regular users, but the real gate is needing a staging Supabase account and the VPN — anyone internal can `harumi env use staging` (or pass `--env staging`).
204
+
205
+ Environment selection precedence: `--env` > `HARUMI_ENV` > `harumi env use` (saved default) > `production`.
206
+
207
+ - Per-command URL overrides: `--api-url` / `HARUMI_API_URL`, `--git-url` / `HARUMI_GIT_URL` (e.g. for a local harumi-api). These override the active environment's endpoints without changing which env you're on.
208
+ - Org: `harumi config set-org <ORG_ID>` / `--org` / `HARUMI_ORG` (scoped per environment).
209
+ - Stored under `~/.harumi/`: global `config.json` (just the selected environment) + per-env `environments/<env>/{credentials,config}.json`. Override the home dir with `HARUMI_HOME`.
@@ -0,0 +1,457 @@
1
+ # harumi CLI — Command Reference
2
+
3
+ Detailed flag reference, config/credential storage, troubleshooting, and the Python `Client` SDK. Loaded on demand from [SKILL.md](../SKILL.md).
4
+
5
+ ## Contents
6
+
7
+ - [Auth commands](#auth-commands)
8
+ - [env](#env)
9
+ - [profile](#profile)
10
+ - [config set-org](#config-set-org)
11
+ - [specs](#specs)
12
+ - [notebooks](#notebooks)
13
+ - [projects](#projects)
14
+ - [init](#init)
15
+ - [import](#import)
16
+ - [run](#run)
17
+ - [runs](#runs)
18
+ - [outputs](#outputs)
19
+ - [repo](#repo)
20
+ - [datasources](#datasources)
21
+ - [schedules](#schedules)
22
+ - [secrets](#secrets)
23
+ - [org](#org)
24
+ - [Config & credential files](#config--credential-files)
25
+ - [Troubleshooting](#troubleshooting)
26
+ - [Python library (`Client`) alternative](#python-library-client-alternative)
27
+
28
+ ## Auth commands
29
+
30
+ ### `harumi login`
31
+
32
+ ```
33
+ harumi login [--email EMAIL] [--signup] [--api-url URL] [--git-url URL]
34
+ ```
35
+
36
+ Logs into the **active environment** (see [env](#env)) — pass `harumi --env staging login` to log into staging. Each environment has its own Supabase and its own stored session.
37
+
38
+ - Prompts for email if `--email` omitted, then prompts for the OTP code emailed by Supabase.
39
+ - `--signup`: creates the Supabase account first. Required the *first* time a new email logs in.
40
+ - On success, stores `access_token`/`refresh_token` at `~/.harumi/credentials.json` (mode `0600`), then calls `POST /git/credentials` to provision a per-user Gitea personal access token (`git_token` + `git_url`), used by `harumi init` and `harumi run` for git-over-HTTPS.
41
+ - Best-effort org resolution: if exactly one org, stored automatically; if multiple, prints a table and instructs `harumi config set-org`.
42
+
43
+ ### `harumi logout`
44
+
45
+ Clears `~/.harumi/credentials.json`. No flags.
46
+
47
+ ### `harumi whoami`
48
+
49
+ ```
50
+ harumi whoami [--api-url URL] [--org ORG]
51
+ ```
52
+
53
+ Prints the email and id of the currently logged-in user (`GET /users/profile`), plus the active environment.
54
+
55
+ ## `env`
56
+
57
+ ```
58
+ harumi env list [--all]
59
+ harumi env current
60
+ harumi env use NAME
61
+ ```
62
+
63
+ Selects the backend environment. Two are built in:
64
+
65
+ | Env | API | Gitea | Access |
66
+ |---|---|---|---|
67
+ | `production` (default) | `https://api.harumi.io/api` | `https://git.harumi.io` | public |
68
+ | `staging` | `https://api.dev.harumi.io/api` | `https://git.dev.harumi.io` | internal, VPN-only |
69
+
70
+ - **`list`**: shows selectable environments with the active one flagged. Internal (VPN-only) environments are hidden unless `--all` is passed or `HARUMI_INTERNAL=1` is set.
71
+ - **`current`**: shows the active environment and its endpoints.
72
+ - **`use NAME`**: persists the default environment in `~/.harumi/config.json`. Warns if the target is internal (VPN required) and if you're not yet logged in on it.
73
+
74
+ Selection precedence: `--env` (top-level flag) > `HARUMI_ENV` > saved default from `env use` > `production`.
75
+
76
+ **Each environment has its own Supabase, so each has its own stored session** (`~/.harumi/environments/<env>/credentials.json`). Switching environments never logs you out of the other — but you must `harumi login` at least once per environment. `--api-url`/`--git-url` (and `HARUMI_API_URL`/`HARUMI_GIT_URL`) still override the active environment's endpoints for local development without changing which environment you're on.
77
+
78
+ Staging is hidden from regular users as a UX convenience only — the real access gate is needing an account in staging's Supabase plus the VPN. Any internal dev can `harumi env use staging` or pass `--env staging`.
79
+
80
+ ## `profile`
81
+
82
+ ```
83
+ harumi profile show [--api-url URL] [--org ORG]
84
+ harumi profile set [--first-name N] [--last-name N] [--bio TEXT] [--api-url URL] [--org ORG]
85
+ ```
86
+
87
+ `show` prints `id`/`email`/`first_name`/`last_name`/`bio` (`GET /users/profile`). `set` sends only the flags you pass as a partial update (`PUT /users/profile`); errors locally with "No fields to update" if none are given.
88
+
89
+ ## `config set-org`
90
+
91
+ ```
92
+ harumi config set-org <ORG_ID>
93
+ ```
94
+
95
+ Persists `org_id` in `~/.harumi/config.json`; every subsequent request sends it as `X-Organization`.
96
+
97
+ ## `specs`
98
+
99
+ ```
100
+ harumi specs [--api-url URL] [--org ORG]
101
+ ```
102
+
103
+ Lists kernel specs (`name`, `display_name`, `cpu`, `memory`, `subscription_required`) from `GET /sandbox/specs`. `name` is what you pass to `run --kernel`.
104
+
105
+ ## `notebooks`
106
+
107
+ ```
108
+ harumi notebooks [--project PROJECT_ID] [--api-url URL] [--org ORG]
109
+ ```
110
+
111
+ Lists every project and its notebooks (legacy notebook-centric view; most projects have exactly one). Useful for finding a `PROJECT_ID`, though `harumi projects list` is the more direct way to do that today.
112
+
113
+ ## `projects`
114
+
115
+ ```
116
+ harumi projects create NAME [--customer-id ID] [--template-id ID] [--bind/--no-bind]
117
+ [--api-url URL] [--git-url URL] [--org ORG]
118
+ harumi projects list [--api-url URL] [--org ORG]
119
+ harumi projects get PROJECT_ID [--api-url URL] [--org ORG]
120
+ harumi projects rename PROJECT_ID NAME [--api-url URL] [--org ORG]
121
+ harumi projects delete PROJECT_ID [--yes] [--api-url URL] [--org ORG]
122
+ ```
123
+
124
+ - **`create`**: `POST /projects`, then `GET /projects/{id}/repo` to fetch the Gitea repo and (unless `--no-bind`) bind the current directory the same way `harumi init` does. If the repo fetch 404s (Harumi Git not configured for this backend), the project is still created — the CLI prints a warning and skips binding instead of failing.
125
+ - **`list`**: `GET /projects` → table of `id`, `name`, `kernel_spec`, `role`.
126
+ - **`get`**: `GET /projects/{id}` → detail view.
127
+ - **`rename`**: `PUT /projects/{id}` with `{name}`.
128
+ - **`delete`**: `DELETE /projects/{id}`. Prompts you to type the exact project name to confirm unless `--yes`.
129
+
130
+ ## `init`
131
+
132
+ ```
133
+ harumi init --project PROJECT_ID [--api-url URL] [--git-url URL] [--org ORG]
134
+ ```
135
+
136
+ **Run once per project directory.** Binds the current working directory to a Harumi project:
137
+
138
+ 1. Calls `GET /projects/{id}/repo`.
139
+ 2. Writes `.harumi/config.json` in the current directory with `project_id` + repo metadata.
140
+ 3. Configures the `harumi` git remote with an authenticated HTTPS URL (requires a `git_token` in credentials from `harumi login` and the repo to be a git working tree).
141
+
142
+ After `harumi init`, `harumi run`, `harumi runs`, `harumi repo`, `harumi outputs`, `harumi datasources`, `harumi schedules`, and `harumi secrets` all work without any `--project` flag.
143
+
144
+ **Note:** `.harumi/config.json` is searched upward from cwd, so these commands work from subdirectories.
145
+
146
+ ## `import`
147
+
148
+ ```
149
+ harumi import [PATH] [--project-name NAME] [--from-git URL] [--bind/--no-bind]
150
+ [--api-url URL] [--git-url URL] [--org ORG]
151
+ ```
152
+
153
+ Turns a downloaded/unzipped project export (e.g. from the web app's "Download
154
+ project" button) into a brand-new git-based Harumi project. `PATH` defaults to
155
+ the current directory and **must be a directory** — unzip the export first
156
+ (`import` fails with `Not a directory: <path>` on a `.zip`).
157
+
158
+ | Flag | Meaning |
159
+ |---|---|
160
+ | `PATH` | Folder to import (positional). Default: current directory. |
161
+ | `--project-name` | Name for the new project. Default: the folder's name. |
162
+ | `--from-git` | Also clone this git URL (e.g. the project's old GitHub repo) flat into the folder — `.git` stripped, files copied alongside the exported ones — before importing. On a filename collision the exported file wins; the CLI warns and lists the first few colliding paths. |
163
+ | `--bind / --no-bind` | Bind the folder to the new project afterward, like `harumi init`. Default: `--bind`. |
164
+
165
+ Sequence:
166
+
167
+ 1. `POST /projects` to create the project (`name` = `--project-name` or the folder name).
168
+ 2. If `--from-git` is set, shallow-clones that URL into a temp dir and copies its tree (minus `.git`) flat into the folder first.
169
+ 3. If the backend didn't provision a Gitea repo for the project (`project.repo is None`), prints a warning and stops — nothing is pushed, no binding happens.
170
+ 4. Otherwise, requires a Gitea token from `harumi login` (prints a warning and stops if missing — never fails hard), then commits and pushes the **entire folder** as one commit ("Import project") to the new repo's default branch.
171
+ 5. If the folder contains a `HARUMI_IMPORT.md` (part of the export, with follow-ups like re-adding datasource credentials or the old GitHub URL), prints a pointer to it.
172
+ 6. Unless `--no-bind`, binds the folder the same way `harumi init` does.
173
+
174
+ ## `run`
175
+
176
+ ```
177
+ harumi run [--branch B] [--commit SHA] [--command C] [--kernel K]
178
+ [--watch] [--output-dir DIR]
179
+ [--api-url URL] [--git-url URL] [--org ORG]
180
+ ```
181
+
182
+ Requires the directory (or a parent) to be bound via `harumi init`.
183
+
184
+ | Flag | Meaning |
185
+ |---|---|
186
+ | `--branch, -b` | Run a specific branch. Default: current branch (or scratch branch if dirty/unpushed). |
187
+ | `--commit` | Run a specific commit SHA. |
188
+ | `--command, -c` | Override the command in `harumi.toml`. |
189
+ | `--kernel, -k` | Override the kernel spec (e.g. `or_python_small`, `gurobi_python_medium`). |
190
+ | `--watch, -w` | Block until the run reaches a terminal status. |
191
+ | `--output-dir, -o` | With `--watch`: download output zip here on success. |
192
+
193
+ **Scratch-branch flow (default when tree is dirty or has unpushed commits):**
194
+
195
+ The CLI detects local changes, creates a temporary branch `harumi-scratch/<user>/<yyyymmdd-HHMMSS>` from HEAD, commits the full working tree to it using a throwaway git index (the user's real index/HEAD are untouched), pushes it to the `harumi` remote, queues the run, then deletes the remote scratch branch when finished (best-effort cleanup). The user never has to commit manually for a quick iteration.
196
+
197
+ **Calls:** `POST /projects/{id}/execute` with `{ branch, commit?, command?, kernel_spec? }`, which returns `{execution_log_id, status, workflow_run_id?, project_run_id?}`. With `--watch`, the CLI then polls `GET /projects/{id}/runs/{run_id}` until it reaches a terminal status.
198
+
199
+ ## `runs`
200
+
201
+ ```
202
+ harumi runs list [--project ID] [--api-url URL] [--org ORG]
203
+ harumi runs get RUN_ID [--project ID] [--api-url URL] [--org ORG]
204
+ harumi runs cancel RUN_ID [--project ID] [--api-url URL] [--org ORG]
205
+ ```
206
+
207
+ - **`list`**: `GET /projects/{id}/runs` → table of `id`, `status`, `source`, `git_branch`, `started`, `ended`, newest first.
208
+ - **`get`**: `GET /projects/{id}/runs/{run_id}` → detail view plus `stdout`/`stderr`/`error` if present.
209
+ - **`cancel`**: `POST /projects/{id}/runs/{run_id}/cancel` on an in-flight run.
210
+
211
+ `--project` on every subcommand overrides the `.harumi` binding.
212
+
213
+ ## `outputs`
214
+
215
+ ```
216
+ harumi outputs [--project ID] [--latest] [--download RUN_ID] [--output-dir DIR]
217
+ [--api-url URL] [--org ORG]
218
+ ```
219
+
220
+ Deprecated alias kept for backwards compatibility — prefer `harumi runs` for new usage.
221
+
222
+ - `--project` optional if run from a bound directory.
223
+ - No extra flags: table of all runs (`id`, `status`, `started`, `ended`, `git_branch`).
224
+ - `--latest`: only the most recently started run.
225
+ - `--download <run_id> [--output-dir DIR]`: downloads the run's committed output via the repo archive endpoint.
226
+
227
+ ## `repo`
228
+
229
+ ```
230
+ harumi repo ls [--ref REF] [--project ID] [--api-url URL] [--org ORG]
231
+ harumi repo cat PATH [--ref REF] [--output FILE] [--project ID]
232
+ harumi repo put LOCAL_PATH REPO_PATH [-m MSG] [--branch B] [--project ID]
233
+ harumi repo rm PATH [-m MSG] [--branch B] [--yes] [--project ID]
234
+ harumi repo mv FROM TO [-m MSG] [--branch B] [--project ID]
235
+ harumi repo download -o OUT.zip [--path DIR] [--ref REF] [--project ID]
236
+ harumi repo branches [--project ID]
237
+ harumi repo branch-create NAME [--from BRANCH] [--project ID]
238
+ harumi repo branch-rm NAME [--yes] [--project ID]
239
+ harumi repo promote NAME [--title T] [--delete-after] [--project ID]
240
+ ```
241
+
242
+ Real endpoints on harumi-api's git router. All writes go through the batch `POST /projects/{id}/repo/changes` endpoint, so every `put`/`rm`/`mv` is exactly one commit.
243
+
244
+ - **`ls`**: `GET /projects/{id}/repo/files[?ref=]` → flat, recursive file list.
245
+ - **`cat`**: `GET /projects/{id}/repo/file-content?path=...[&ref=]`, base64-decodes `content`. Prints to stdout, or writes bytes to `--output` (required for binary files — the CLI refuses to print non-UTF-8 content without `--output`).
246
+ - **`put`**: probes `get_repo_file` first to decide `create` vs `update`, then sends one `repo/changes` operation with base64-encoded file content.
247
+ - **`rm`**: sends a `delete` operation for the path (file or folder — deletes everything under a folder prefix). Prompts for confirmation unless `--yes`.
248
+ - **`mv`**: sends a `move` operation (`from_path` → `path`).
249
+ - **`download`**: `GET /projects/{id}/repo/archive?path=&ref=`, streamed to the `--output` zip path.
250
+ - **`branches`**: `GET /projects/{id}/repo/branches` → table with the live branch flagged.
251
+ - **`branch-create`**: `POST /projects/{id}/repo/branches` with `{name, from_branch?}`.
252
+ - **`branch-rm`**: `DELETE /projects/{id}/repo/branches/{name}`. Refuses (server-side) to delete the live branch.
253
+ - **`promote`**: `POST /projects/{id}/repo/branches/{name}/promote` with `{title?, delete_after}`; merges the version into the live branch. On a merge conflict the response's `conflict=true` and the CLI surfaces `message` as an error instead of a fake success.
254
+
255
+ `--project` on every subcommand overrides the `.harumi` binding.
256
+
257
+ ## `datasources`
258
+
259
+ Real endpoints, live today (`harumi-api/src/api/datasources/router.py`). Scoped per-project by `(project_id, name)`.
260
+
261
+ ```
262
+ harumi datasources list [--project ID] [--api-url URL] [--org ORG]
263
+ harumi datasources get NAME [--project ID]
264
+ harumi datasources add NAME --type TYPE [--host H] [--port P] [--database D] [--username U]
265
+ [--use-proxy] [--proxy-host H] [--proxy-port P] [--proxy-server-name N]
266
+ [--project ID]
267
+ harumi datasources update NAME [--name NEW_NAME] [--type T] [--host H] [--port P] [--database D]
268
+ [--username U] [--set-credentials] [--use-proxy/--no-use-proxy]
269
+ [--proxy-host H] [--proxy-port P] [--proxy-server-name N] [--project ID]
270
+ harumi datasources remove NAME [--yes] [--project ID]
271
+ harumi datasources test --type TYPE --host H --port P --database D --username U
272
+ [--use-proxy] [--proxy-host H] [--proxy-port P] [--proxy-server-name N]
273
+ harumi datasources query NAME --sql "SELECT ..." [--limit N] [--csv PATH] [--project ID]
274
+ ```
275
+
276
+ `--project` on every subcommand overrides the `.harumi` binding.
277
+
278
+ **Credentials are always prompted, never a flag.** `add`, `update --set-credentials`, and `test` each prompt with `typer.prompt(hide_input=True)`. This is deliberate — secrets must never land in shell history, process listings, or `--help` output. The server stores credentials in AWS SSM (SecureString) and never returns them; `get`/`list` responses have no credential field.
279
+
280
+ **`type`** must be one of `postgresql | mysql | sqlserver | oracle`.
281
+
282
+ **`add`** calls `POST /datasources/{project_id}` — the backend tests the connection before persisting, so a bad host/credentials fails the `add` with the same error `test` would surface.
283
+
284
+ **`update`** calls `PUT /datasources/{project_id}/{name}` with only the fields you pass (partial update). Passing `--name` renames the datasource (and its SSM parameter, server-side). Omit `--set-credentials` to leave the stored credentials untouched.
285
+
286
+ **`remove`** calls `DELETE /datasources/{project_id}/{name}`, prompting for confirmation unless `--yes`. Deletes the DB row and the SSM parameter.
287
+
288
+ **`test`** calls `POST /datasources/test-connection` — validates without persisting. Useful to sanity-check credentials before `add`.
289
+
290
+ **`query`** calls `POST /datasources/{project_id}/{name}/execute`, the read-only proxy:
291
+
292
+ - Server validates the SQL is **SELECT/WITH-only**; any of `INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE|EXEC|EXECUTE|CALL|MERGE|UPSERT` (as a whole word, case-insensitive) → **403** with a message naming the forbidden keyword.
293
+ - Rows are capped server-side at `--limit` (default 10000, hard max 100000). If actual rows exceed the cap, the response sets `wasLimited=true` and the CLI prints a yellow warning.
294
+ - Response shape: `{ columns: string[], data: any[][], rowCount, wasLimited, maxRows, dataframe_name }`. The CLI renders `columns`/`data` as a Rich table, or writes them as CSV with `--csv <path>`.
295
+ - Datasource not found → 404. Query execution error (bad SQL, connection issue) → 400.
296
+
297
+ ## `schedules`
298
+
299
+ Real, project-scoped endpoints (`/projects/{project_id}/schedules`).
300
+
301
+ ```
302
+ harumi schedules list [--project ID] [--api-url URL] [--org ORG]
303
+ harumi schedules get SCHEDULE_ID [--project ID]
304
+ harumi schedules add --cron CRON --git-branch BRANCH [--start-at ISO] [--git-commit SHA]
305
+ [--command C] [--kernel K] [--output-format F] [--email-to E] [--project ID]
306
+ harumi schedules update SCHEDULE_ID [--cron CRON] [--start-at ISO] [--git-branch B] [--git-commit SHA]
307
+ [--command C] [--kernel K] [--output-format F] [--email-to E] [--project ID]
308
+ harumi schedules remove SCHEDULE_ID [--yes] [--project ID]
309
+ ```
310
+
311
+ `--project` on every subcommand overrides the `.harumi` binding.
312
+
313
+ **`--cron`** is a raw 5-field cron expression (e.g. `"0 9 * * *"`), **interpreted in UTC**. The CLI does not validate it — the server validates with `croniter` and returns **400 Invalid cron expression** on a bad value. There is no calendar/builder UX; pass the cron string directly.
314
+
315
+ **`--start-at`** is an ISO-8601 datetime; defaults to "now" (UTC) if omitted on `add`.
316
+
317
+ **`--email-to`** accepts `only-me` | `team` | `everyone` | a comma-separated list of email addresses (resolved server-side).
318
+
319
+ **`add`** calls `POST /projects/{project_id}/schedules` with `{cron, start_at, git_branch, git_commit?, command?, kernel_spec?, output_format?, email_to?}` → `Schedule`.
320
+
321
+ **`update`** calls `PUT /projects/{project_id}/schedules/{schedule_id}` with only the fields you pass (partial update); errors locally with "No fields to update" if no flags are given.
322
+
323
+ **`remove`** calls `DELETE /projects/{project_id}/schedules/{schedule_id}`, prompting for confirmation unless `--yes`. **This is the only way to stop a schedule** — there is no pause/enable flag.
324
+
325
+ **No separate "run now."** Immediate execution is `harumi run` (git-ref based); schedules only manage recurring cron runs.
326
+
327
+ ## `secrets`
328
+
329
+ Project-scoped environment variables, stored as SSM SecureStrings and injected into kernels/apps at run time.
330
+
331
+ ```
332
+ harumi secrets list [--project ID] [--api-url URL] [--org ORG]
333
+ harumi secrets set NAME [--project ID]
334
+ harumi secrets rm NAME [--yes] [--project ID]
335
+ ```
336
+
337
+ - **`list`**: `GET /projects/{id}/secrets` → names only. Values are never printed.
338
+ - **`set`**: prompts for the value with hidden input, then `POST /projects/{id}/secrets` with `{name, value}`. There is no update endpoint — `set` on an existing name overwrites it.
339
+ - **`rm`**: `DELETE /projects/{id}/secrets/{name}`, prompting for confirmation unless `--yes`.
340
+
341
+ ## `org`
342
+
343
+ ```
344
+ harumi org list [--api-url URL]
345
+ harumi org create BUSINESS_NAME [--api-url URL]
346
+ harumi org rename ORG_ID BUSINESS_NAME [--api-url URL]
347
+ harumi org delete ORG_ID [--yes] [--api-url URL]
348
+ harumi org members ORG_ID [--api-url URL]
349
+ harumi org invite ORG_ID --email EMAIL [--role ROLE] [--api-url URL]
350
+ harumi org role ORG_ID USER_ID --role ROLE [--api-url URL]
351
+ harumi org remove ORG_ID USER_ID [--yes] [--api-url URL]
352
+ ```
353
+
354
+ `--role` is one of `owner | admin | member | viewer`.
355
+
356
+ - **`list`**: `GET /users/organizations`.
357
+ - **`create`**: `POST /users/organizations` with `{business_name}`.
358
+ - **`rename`**: `PUT /users/organizations/{id}` with `{business_name}`.
359
+ - **`delete`**: `DELETE /users/organizations/{id}`, prompting for confirmation unless `--yes`.
360
+ - **`members`**: `GET /users/organizations/{id}/users`.
361
+ - **`invite`**: `POST /users/organizations/{id}/users` with `{email, role}`.
362
+ - **`role`**: `PUT /users/organizations/{id}/users/{user_id}` with `{role}`.
363
+ - **`remove`**: `DELETE /users/organizations/{id}/users/{user_id}`, prompting for confirmation unless `--yes`.
364
+
365
+ ## Config & credential files
366
+
367
+ Environment selection precedence (highest first): **`--env` > `HARUMI_ENV` > saved default (`harumi env use`) > `production`**. See [env](#env) for the environment table.
368
+
369
+ Within the active environment, URL/org overrides (highest first): **CLI flags > env vars > per-env `config.json` > the environment's built-in endpoints**.
370
+
371
+ | Setting | Env var | Per-env config key | Default (per environment) |
372
+ |---|---|---|---|
373
+ | API base URL | `HARUMI_API_URL` | `api_url` | environment's `api_url` |
374
+ | Gitea URL | `HARUMI_GIT_URL` | `git_url` | environment's `git_url` |
375
+ | Org id (`X-Organization`) | `HARUMI_ORG` | `org_id` | none (from login) |
376
+
377
+ - `~/.harumi/config.json` — global; stores only the selected `environment`.
378
+ - `~/.harumi/environments/<env>/credentials.json` — per-environment `access_token`, `refresh_token`, `git_token`, `git_url`, `user_id`, `email`, `expires_at`; mode `0600`.
379
+ - `~/.harumi/environments/<env>/config.json` — per-environment `org_id` (and any local `api_url`/`git_url` overrides).
380
+ - `.harumi/config.json` (per-project) — `project_id`, `repo.owner/name/clone_url/default_branch`; written by `harumi init` / `harumi projects create`, searched upward from cwd.
381
+ - Override the home dir with `HARUMI_HOME`.
382
+ - **Upgrading from a pre-environments install:** the old flat `~/.harumi/credentials.json` + `config.json` are migrated automatically into the `production` environment on first run.
383
+
384
+ ## Troubleshooting
385
+
386
+ | Symptom / error | Cause | Fix |
387
+ |---|---|---|
388
+ | `Error: Not logged in. Run harumi login first.` | No/expired session | Ask user to run `harumi login` |
389
+ | `harumi-api returned HTTP 422: ... Signups not allowed for otp` | New email, no account | Re-run `harumi login --signup` |
390
+ | `Provide --project or run from a directory with a .harumi binding` | No `--project` and `.harumi/config.json` missing in cwd + parents | `harumi init --project <ID>` or pass `--project` |
391
+ | `No Gitea token found. Run harumi login` | `git_token` absent in credentials | `harumi login` again |
392
+ | `git push failed: ...` | Network (VPN not connected) or bad credentials | Check VPN; re-run `harumi login` to refresh token |
393
+ | `git not found` | git missing from PATH | Install git |
394
+ | `Run ended with status: failed` | Solver code raised or exited non-zero | `harumi runs get <RUN_ID>` for stdout/stderr/error |
395
+ | `No run id returned; cannot watch this run` | Backend didn't return a `project_run_id` | Check `harumi runs list` manually |
396
+ | `harumi-api returned HTTP 403: Only SELECT queries are allowed ...` | `datasources query` SQL contains a destructive keyword or doesn't start with SELECT/WITH | Rewrite the query as a read-only SELECT/WITH |
397
+ | `harumi-api returned HTTP 404: ...` (on `datasources`/`repo`/`schedules`/`secrets` get/update/remove) | Resource name/id doesn't exist for this project | List the resource first to check the exact name/id |
398
+ | `[yellow]Result was truncated at the server-side row cap` | Query returned more rows than `--limit` (or the 100000 hard max) | Narrow the query (add a `WHERE`/`LIMIT`) or raise `--limit` |
399
+ | `No fields to update.` | An `update`/`set` command called with no flags | Pass at least one field flag |
400
+ | `harumi-api returned HTTP 400: Invalid cron expression: ...` | `schedules add/update --cron` failed server-side `croniter` validation | Fix the cron string (5 fields: minute hour day month weekday) |
401
+ | `{path!r} is not valid UTF-8 text.` | `repo cat` on a binary file without `--output` | Re-run with `--output <local_path>` |
402
+
403
+ ## Python library (`Client`) alternative
404
+
405
+ When scripting is better than shelling out:
406
+
407
+ ```python
408
+ from harumi import Client
409
+ from harumi.config import ProjectBinding
410
+
411
+ binding = ProjectBinding.load() # reads .harumi/config.json
412
+ client = Client() # loads ~/.harumi/credentials.json
413
+
414
+ # Queue a git-ref run
415
+ response = client.execute_project(
416
+ binding.project_id,
417
+ branch="feature/solver-v2",
418
+ command="python main.py",
419
+ )
420
+
421
+ # Poll until done
422
+ from harumi.execution import wait_for_run, download_run_output
423
+ result = wait_for_run(client.api, binding.project_id, response.project_run_id)
424
+ print(result.status, result.succeeded)
425
+
426
+ # Download artifacts
427
+ download_run_output(client.api, binding.project_id, result, "./out")
428
+
429
+ # Repo file operations
430
+ files = client.list_repo_files(binding.project_id)
431
+ client.apply_repo_changes(
432
+ binding.project_id,
433
+ operations=[{"action": "update", "path": "config.yaml", "content": "..."}], # base64
434
+ )
435
+
436
+ # Datasources
437
+ result = client.execute_datasource_query(binding.project_id, "sales_db", "SELECT * FROM orders LIMIT 10")
438
+ print(result.columns, result.row_count, result.was_limited)
439
+
440
+ # Schedules
441
+ schedule = client.create_schedule(
442
+ binding.project_id,
443
+ {"cron": "0 9 * * *", "start_at": "2026-01-22T09:00:00Z", "git_branch": "main"},
444
+ )
445
+
446
+ # Secrets
447
+ client.create_secret(binding.project_id, "API_KEY", "s3cr3t")
448
+
449
+ # Organizations
450
+ orgs = client.list_organizations()
451
+
452
+ # Create a project
453
+ project = client.create_project("New Project") # project.repo is None if unprovisioned
454
+ ```
455
+
456
+ `Client(api_url=..., git_url=..., org_id=...)` accepts the same overrides as the CLI flags. Requires the user to have run `harumi login` at least once.
457
+