Metadata-Version: 2.5
Name: insightfactory-cli
Version: 1.0.3.dev17
Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
Project-URL: Homepage, https://insightfactory.ai
Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Keywords: cli,insightfactory,oauth,pkce
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# insightfactory-cli

Profile-based authentication CLI for the InsightFactory Interfaces API. It
installs the `if-cli` command, which logs in through the browser with OAuth
authorization code and PKCE, refreshes tokens, and makes authenticated API
calls. Profiles live in `~/.insightfactory` in the same layout the Node
`@insightfactory-ai/if-cli` uses, so both implementations can share one config
directory.

This is the production Python CLI, published to public PyPI. It needs Python
3.10 or newer.

## Install

```bash
uv tool install insightfactory-cli   # persistent install, on PATH
uvx insightfactory-cli profiles      # or zero-install, run-once
uv tool upgrade insightfactory-cli   # updates
```

`uv` fetches a managed Python if the machine lacks one. Fallbacks:

```bash
pipx install insightfactory-cli
pip install insightfactory-cli
```

Develop-branch builds are published as PEP 440 `.devN` pre-releases. Install
the latest one with:

```bash
pip install --pre insightfactory-cli
uv tool install --prerelease allow insightfactory-cli
```

PEP 440 sorts `1.0.0.devN` before `1.0.0`, so a dependency spec of
`insightfactory-cli>=1.0.0` does not match a develop-channel dev build. An
in-process consumer that wants those builds needs a spec that admits
pre-releases, such as `insightfactory-cli>=1.0.0.dev0`. That spec matches both
the `.devN` builds and the final `1.0.0` release.

## Usage

```bash
# Create a profile and authenticate through the browser.
if-cli login -p example-dev --host https://factory.example

# Inspect profiles and cached-token status. Statuses are colour-coded on a TTY
# (green valid, yellow refreshable-but-expired, red missing or broken); set
# NO_COLOR=1 to disable or FORCE_COLOR=1 to keep colour when piping.
if-cli profiles
if-cli profiles --json

# Read a single non-secret profile value for scripting.
if-cli config get host -p example-dev

# Print a valid token, refreshing it when a refresh token is available.
if-cli token -p example-dev
if-cli token -p example-dev --env
if-cli token -p example-dev --env-name INSIGHTFACTORY_ACCESS_TOKEN_DEV

# Make an authenticated API request.
if-cli api -p example-dev /api/agent-projects
if-cli api -p example-dev -X POST -d '{"name":"example"}' /api/example

# Wait longer than the 30-second default for a long-running endpoint.
if-cli api -p example-dev --timeout 300 -X PUT -d '{"productionLineCodes":["PL001"]}' \
  /api/orchestration/run

# Discover API routes from the factory's OpenAPI document.
if-cli api routes -p example-dev
if-cli api routes -p example-dev agent-projects
if-cli api describe -p example-dev GET /api/agent-projects/{id}

# Remove the cached credential for a profile.
if-cli logout -p example-dev

# Fallback for factories where browser OAuth is not available.
if-cli set-token -p example-dev

if-cli --version
```

Profile selection tries `-p` first, then `INSIGHTFACTORY_CONFIG_PROFILE`, then
`[DEFAULT]`. Route discovery reads `{host}/swagger/v1/swagger.json` and does
not need a login. Authenticated requests only go to the selected factory's
origin, so a profile token cannot be forwarded to another host. A profile host
must be a bare origin, not a URL with an application path prefix.

Requests time out after 30 seconds by default. Raise the deadline for one
invocation with `--timeout <seconds>`, or for a whole session with
`INSIGHTFACTORY_REQUEST_TIMEOUT`. The flag wins over the environment variable,
and the CLI validates both before it touches the network. There is no upper
bound, because the right ceiling depends on the endpoint.

Both settings apply to the API request itself. OAuth discovery and token
refresh keep the 30-second default, so `if-cli token` and in-process callers of
`if_cli.oauth.get_valid_token` always wait a bounded time that no ambient
environment value can change.

A timeout is a client-side deadline, not a rejection. The factory may have
accepted and completed the request after `if-cli` gave up, so re-issuing a
`POST`, `PUT`, `PATCH`, or `DELETE` can apply it twice. For those methods the
timeout message tells you to confirm the current state before retrying rather
than repeat the call.

Override the config directory with `INSIGHTFACTORY_CONFIG_DIR`, which is useful
in tests and CI. The default is `~/.insightfactory`.

The request method goes in `-X`. `if-cli api` takes exactly one positional
argument and reads it as the path, so the `curl`-shaped
`if-cli api GET /api/schedules` parses `GET` as the path. Write
`if-cli api -X GET /api/schedules` instead. The error names the misplaced verb
rather than reporting a problem with `/api/schedules`.

On Git Bash (MSYS2) on Windows, path conversion rewrites a leading-slash
argument such as `/api/agent-projects` into a Windows path. Prefix the command
with `MSYS_NO_PATHCONV=1`:

```bash
MSYS_NO_PATHCONV=1 if-cli api -p example-dev /api/agent-projects
```

### Scripting against profiles

`profiles --json` and `config get` are the supported machine-readable
interfaces. The padded `profiles` columns are for humans and are not a
contract, and neither is the layout of `~/.insightfactory/config`.

`if-cli profiles --json` prints an array with one entry per configured profile,
in file order, each holding `name`, `host`, and `token`:

```json
[
  {
    "name": "example-dev",
    "host": "https://factory.example",
    "token": { "status": "valid", "expires_at": 1785918336, "refreshable": true }
  },
  { "name": "other", "host": "https://other.factory.example", "token": { "status": "none" } }
]
```

`token.status` is `valid`, `expired`, `none`, or `unknown`. `expires_at` and
`refreshable` appear only when a cached token exists. `valid` means
`if-cli token` will hand back the cached token as-is. A token inside the
60-second slack window that command refreshes within is therefore reported as
`expired`, so read the raw `expires_at` if you need the literal expiry instant.
A profile that cannot be resolved has a `host` of `null`, or its raw configured
value, plus a top-level `error` describing the problem. It does not stop the
other profiles from being listed. `token.error` is set only when the token
cache itself could not be read.

`if-cli config get <key> -p <profile>` prints one newline-terminated value and
exits non-zero if the profile cannot be resolved or the key is not configured
in it. A profile with no host cannot be resolved at all, so every key on it
fails, not just `host`. A key that is present but empty, such as `client_id =`,
counts as not configured:

```bash
curl -H "Authorization: Bearer $(if-cli token -p example-dev)" \
  "$(if-cli config get host -p example-dev)/api/agent-projects"
```

The readable keys are `host`, `audience`, `callback_port`, `client_id`, and
`organization`. All are non-secret and resolved the same way the CLI resolves
them, so `audience` and `callback_port` return their defaults when the profile
omits them. Tokens are deliberately not readable this way. Use `if-cli token`.

## Programmatic API

You can import `insightfactory-cli` in-process to resolve a profile to a
factory host and a fresh access token. These four symbols are supported for
in-process consumers:

- `if_cli.config.load_config`
- `if_cli.config.get_profile`
- `if_cli.oauth.get_valid_token`
- `if_cli.runtime.CliError`

They raise `CliError` on failure, never `SystemExit` or `sys.exit`. They never
write to stdout, never launch a browser or block on interactive input, and only
use bounded waits. Changing or removing any of them is a breaking change and
needs a major version bump.

## How authentication works

- Profiles live in `~/.insightfactory/config`, one per customer and environment.
- The CLI manages the profile file and regenerates it on updates. Comments and
  empty sections are not preserved.
- Tokens are cached by host in `~/.insightfactory/token-cache.json` with file
  mode `0600`.
- The configuration directory is `0700`. Configuration and cache updates are
  atomic.
- Remote factory, authorization, and token endpoints must use HTTPS. HTTP is
  accepted only for loopback development.
- Login discovers OAuth metadata from
  `{host}/.well-known/oauth-authorization-server`.
- Authorization uses PKCE through the factory `/authorize` proxy with a
  loopback-only `127.0.0.1` callback. The CLI is a public OAuth client. It reads
  the client ID from the factory's discovery document at runtime and holds no
  client secret.

## Development

```bash
uv sync
uv run pytest
uv run ruff check .
uv run ty check
```

Smoke-test against a throwaway config directory, never `~/.insightfactory`:

```bash
uv run if-cli --help
uv run if-cli --version
INSIGHTFACTORY_CONFIG_DIR=$(mktemp -d) uv run if-cli profiles
```

Python 3.10 or newer is required. Runtime code is stdlib-only.

## Release

A release is cut by pushing a tag, not by merging. Merging to `main` runs CI
and publishes nothing.

```bash
git tag v1.0.1 && git push origin v1.0.1
```

Verify and build live in the reusable workflow
[`if_s_insightfactory_cli.yml`](https://github.com/insightfactory-ai/if_sre_github_actions/blob/main/.github/workflows/if_s_insightfactory_cli.yml)
in `insightfactory-ai/if_sre_github_actions`. This repository's
`.github/workflows/release.yml` keeps only the OIDC publish job, because PyPI
Trusted Publishing is registered for this repo, workflow `release.yml`, and
environment `pypi`. A reusable workflow cannot be registered.

The PyPI project needs a one-time trusted-publisher registration for that
triple. The publish job downloads the `dist-release` artifact staged by the
reusable workflow and uploads it with `pypa/gh-action-pypi-publish`.

| Trigger | Published version | PyPI role |
|---|---|---|
| Push / merge to `develop` | `1.0.1.dev{N}` | pre-release (`pip install --pre`) |
| Push of a `v*` tag | the version in `pyproject.toml`, which the tag must match | latest |
| Merge to `main` | nothing, CI only | none |

`N` is `github.run_number` of the reusable workflow. It is a per-workflow
integer that goes up on every run and stays the same across re-runs of that
run, so a failed publish can be retried under the same version. It does not
depend on git history or clone depth. After a release, bump `version` in
`pyproject.toml` on `develop` so later `.devN` builds sort after what just
landed. PEP 440 puts `1.0.1.devN` before `1.0.1`, which is also why a spec of
`insightfactory-cli>=1.0.0` skips develop-channel builds and an in-process
consumer that wants them must ask for `insightfactory-cli>=1.0.0.dev0`.

A `v*` tag whose name does not match `pyproject.toml` fails before publishing.

The two channels treat an already-published version differently, on purpose:

- **Release channel, a tag.** Publishing fails and names the fix. A tag is a
  request to release that version. If it cannot be honoured, the run must say
  so rather than report success having published nothing. Git also refuses to
  push a tag that already exists, so this is hard to reach.
- **Dev channel, `develop`.** Publishing is skipped quietly. `.dev{N}` uses
  `github.run_number`, which is stable across re-runs of one run, so an
  already-present version there can only mean a re-run of a run that already
  published. That is idempotency, not a swallowed mistake.

This split is why releasing moved off `main`. While a merge was the trigger, an
already-published version had to be tolerated as a no-op for merges to stay
green. A merge that released nothing then looked the same as one that
released, and a forgotten version bump shipped nothing, silently.

## Differences from the Node CLI

Behaviour matches `@insightfactory-ai/if-cli` except where the platforms
differ:

- **Unknown-option wording** is aligned to `Unknown option '--flag'`. Other
  argparse messages, such as a missing option value, still differ from Node's
  `util.parseArgs`.
- **Expiry timestamps** in human-readable output use the C-locale `%c` format
  rather than JavaScript `Date#toLocaleString()`.
- **`--version`** reads the installed package metadata from `1.0.0` onward
  rather than an npm `package.json`.

On-disk profile and token-cache paths, JSON shapes, and file permissions stay
compatible with the Node CLI, so you can switch implementations without
re-authenticating.

## Licence

Copyright 2026 insightfactory.ai. All rights reserved. This package is proprietary
and may be used only under a separate written agreement with insightfactory.ai.
