Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,52 @@ jobs:
tags: |
ghcr.io/${{ github.repository }}:${{ steps.ver.outputs.v }}
${{ steps.ver.outputs.latest }}

dist:
name: Publish Homebrew + Scoop
needs: release # regenerate from the just-published npm tarball
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22

# Regenerate the formula + manifest from the published tarball (sha256).
- name: Generate dist files
id: gen
run: |
V=$(node -p "require('./package.json').version")
echo "v=$V" >> "$GITHUB_OUTPUT"
node scripts/gen-dist.mjs "$V"

# Cross-repo push needs a PAT (the default GITHUB_TOKEN is scoped to this
# repo). Add a fine-grained TAP_TOKEN secret with contents:write on
# wavyx/homebrew-tap and wavyx/scoop-pdcli to enable auto-publish; without
# it this step no-ops so the release never fails on a missing secret.
# Prereleases are skipped (they must not move the stable formula/manifest).
- name: Push to tap repos
env:
TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
V: ${{ steps.gen.outputs.v }}
run: |
case "$V" in *-*) echo "prerelease $V — skipping tap publish"; exit 0 ;; esac
if [ -z "$TAP_TOKEN" ]; then
echo "No TAP_TOKEN secret — skipping Homebrew/Scoop publish."
echo "Run 'node scripts/gen-dist.mjs $V' and push manually, or add the PAT."
exit 0
fi
git config --global user.name "pdcli-release"
git config --global user.email "release@wavyx.dev"
push_file() {
repo="$1"; src="$2"; dest="$3"
tmp=$(mktemp -d)
git clone --depth 1 "https://x-access-token:${TAP_TOKEN}@github.com/${repo}.git" "$tmp"
mkdir -p "$(dirname "$tmp/$dest")"
cp "$src" "$tmp/$dest"
git -C "$tmp" add "$dest"
git -C "$tmp" commit -m "pdcli $V" || { echo "no change for $repo"; return 0; }
git -C "$tmp" push
}
push_file wavyx/homebrew-tap packaging/homebrew/pdcli.rb Formula/pdcli.rb
push_file wavyx/scoop-pdcli packaging/scoop/pdcli.json bucket/pdcli.json
12 changes: 9 additions & 3 deletions packaging/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ This downloads the npm tarball, computes its sha256, and writes:

Both generated files are git-ignored here — they live in their own repos.

> Auto-bumping these on every release would need a cross-repo token (PAT); for
> now it's a one-line manual step. The Docker image and npm publish are fully
> automated in `.github/workflows/release.yml`.
## Automation

The `dist` job in `.github/workflows/release.yml` regenerates both files from the
just-published tarball and pushes them to the tap repos on every stable release —
**as long as a `TAP_TOKEN` secret exists**. It must be a fine-grained PAT with
`contents:write` on `wavyx/homebrew-tap` and `wavyx/scoop-pdcli`. Without the
secret the job no-ops (the release never fails), and you publish with the manual
`node scripts/gen-dist.mjs <version>` step above. npm publish and the Docker image
are always automated.

## Container security caveat (env-var tokens)

Expand Down
2 changes: 1 addition & 1 deletion scripts/gen-dist.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function renderHomebrewFormula({ url, sha256 }) {
# Homebrew build sandbox blocks network, so point node-jq at the Homebrew jq
# instead (node-jq honors $JQ_PATH at runtime).
(bin/"pdcli").write_env_script libexec/"bin/pdcli",
JQ_PATH: Formula["jq"].opt_bin/"jq"
JQ_PATH: formula_opt_bin("jq")/"jq"
end

test do
Expand Down
3 changes: 2 additions & 1 deletion website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ export default defineConfig({
label: 'Start here',
items: [
{ label: 'Installation', slug: 'start/installation' },
{ label: 'Distribution (all channels)', slug: 'start/distribution' },
{ label: 'Quickstart', slug: 'start/quickstart' },
{ label: 'Which command do I use?', slug: 'start/tasks' },
{ label: 'Distribution (all channels)', slug: 'start/distribution' },
],
},
{
Expand Down
19 changes: 19 additions & 0 deletions website/public/demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 45 additions & 21 deletions website/src/components/Home.astro
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const steps: [string, string, string, string][] = [
],
];

const features: { t: string; b: string; d: string }[] = [
const features: { t: string; b: string; d: string; href?: string }[] = [
{
t: 'Backup & data export',
b: 'Stream your entire account to JSON with pdcli backup --resume, or export any resource to CSV. Resumable, scriptable, and perfect for archives or migrations.',
Expand All @@ -63,6 +63,7 @@ const features: { t: string; b: string; d: string }[] = [
t: 'Built for AI agents',
b: 'Runs as an MCP server — pdcli mcp serve — read-only by default, giving Claude and other hosts a safe, typed tool surface. For shell-driven agents there is also self-documenting --help, clean JSON, and deterministic exit codes.',
d: '<rect x="4" y="6" width="16" height="12" rx="2"/><path d="M9 2v4M15 2v4M9 12h0M15 12h0M9.5 15.5a3 3 0 005 0"/>',
href: 'start/agents/',
},
{
t: 'Scriptable by design',
Expand Down Expand Up @@ -170,6 +171,20 @@ const showcaseTerm = `<span class="dim">$</span> <span class="g">pdcli</span> <s
<pre class="clr-term__body" id="clr-aterm-body" aria-hidden="true"><span
class="prompt">❯</span> <span class="c" id="clr-aterm-cmd"></span><span
class="cur">▋</span><span id="clr-aterm-out"></span></pre>
<noscript>
<img
src={withBase('demo.svg')}
alt="Animated demo of pdcli: pipeline health, a winning deal update, and a data-hygiene audit"
width="880"
height="382"
style="display:block; width:100%; height:auto"
/>
</noscript>
<a
class="clr-aterm__link"
href={withBase('guides/mcp/')}
aria-label="See how pdcli runs as an MCP server"
style="position:absolute; inset:0; z-index:2"></a>
</div>
</section>

Expand Down Expand Up @@ -271,26 +286,35 @@ const showcaseTerm = `<span class="dim">$</span> <span class="g">pdcli</span> <s
</div>
<div class="clr-grid">
{
features.map((f) => (
<div class="clr-feat">
<div class="clr-feat__ic">
<svg
aria-hidden="true"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
set:html={f.d}
/>
</div>
<h3>{f.t}</h3>
<p>{f.b}</p>
</div>
))
features.map((f) => {
const inner = (
<>
<div class="clr-feat__ic">
<svg
aria-hidden="true"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
stroke-linejoin="round"
set:html={f.d}
/>
</div>
<h3>{f.t}</h3>
<p>{f.b}</p>
</>
);
return f.href ? (
<a class="clr-feat" href={withBase(f.href)} style="color: inherit">
{inner}
</a>
) : (
<div class="clr-feat">{inner}</div>
);
})
}
</div>
</section>
Expand Down
29 changes: 10 additions & 19 deletions website/src/content/docs/automation/ci.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,20 @@ running `pdcli auth login`. The token never touches disk and stays out of shell
PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=xxxxxxxx pdcli deal list
```

Store the token as an encrypted secret and inject it per step. In GitHub Actions:
Store the token as an encrypted secret and inject it as env vars per job — in GitHub
Actions that's a two-line `env:` block wired to a repository secret:

```yaml
name: pipedrive-audit
on:
schedule:
- cron: '0 6 * * 1' # Mondays 06:00 UTC
workflow_dispatch:

jobs:
audit:
runs-on: ubuntu-latest
env:
PDCLI_COMPANY_DOMAIN: acme
PDCLI_API_TOKEN: ${{ secrets.PIPEDRIVE_API_TOKEN }}
steps:
- uses: actions/setup-node@v5
with:
node-version: 20
- run: npm install -g @wavyx/pdcli
- run: pdcli audit --strict
env:
PDCLI_COMPANY_DOMAIN: acme
PDCLI_API_TOKEN: ${{ secrets.PIPEDRIVE_API_TOKEN }}
```

The complete GitHub Actions workflow — a pinned install, the `pdcli doctor --offline`
preflight, and exit-code gating — lives in
[GitHub Actions](/pdcli/automation/github-actions/). This page stays platform-agnostic:
the flags and scheduled-job patterns below work on any CI.

Env auth means no OS keychain is required on the runner — env vars take precedence over the
keychain, so writes (login) are never attempted. See
[Security model](/pdcli/concepts/security/).
Expand Down
7 changes: 7 additions & 0 deletions website/src/content/docs/contributing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ npx vitest run test/deal/list.test.js # a single file
npm run test:coverage # suite + coverage report
```

## Docs site

The documentation site lives in `website/` (Astro + Starlight). Preview it locally with
`cd website && npm install && npm run dev`. The command reference at
`reference/commands.mdx` is **generated** from the oclif manifest by
`scripts/gen-commands.mjs` (`npm run docs:commands`) — don't hand-edit it, regenerate.

## Release flow

Releases are tag-driven and automated (maintainers only): bump the version and
Expand Down
34 changes: 34 additions & 0 deletions website/src/content/docs/guides/analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@ stages/activities or goals where needed) and computes the numbers locally, so th
against any account without extra setup. All accept the global `--output`/`--jq`/`--fields`
flags.

## Which command answers what

Most weeks, start with [`digest`](#digest): a single fetch fanned into velocity, pipeline
health, coverage, funnel and forecast, so you rarely run the rest by hand. Reach for an
individual command when you want one number in depth.

| Command | Answers |
| ---------------------------------------------------- | ----------------------------------------------------------- |
| [`digest`](#digest) | **Start here** — the whole Monday packet in one call. |
| [`metrics velocity`](#metrics-velocity) | How much deal value/day is the pipeline producing? |
| [`deal summary`](#deal-summary) | What's my pipeline worth right now? |
| [`funnel`](#funnel) | Where do deals drop off, stage to stage? |
| [`metrics aging`](#metrics-aging) | Where are deals rotting (days in current stage)? |
| [`metrics slippage`](#metrics-slippage) | Which close dates keep slipping? |
| [`metrics conversion-matrix`](#metrics-conversion-matrix) | The real stage-transition graph, backward edges and all. |
| [`metrics coverage`](#metrics-coverage) | Am I carrying enough pipeline to hit quota? |
| [`pipeline health`](#pipeline-health) | What's neglected per stage right now? |
| [`metrics forecast`](#metrics-forecast) | What closes by month (commit / best case / weighted)? |
| [`rep scorecard`](#rep-scorecard) | How is each rep performing? |

:::caution[Currencies are never summed]
`deal summary`, `metrics coverage`, `metrics forecast` and `digest` keep each currency on its
own line — a USD deal and a EUR deal are different units. Because `coverage` collapses open
value into a single ratio, a mixed-currency pipeline exits `64` until you scope it to one with
`--currency`.
:::

## `metrics velocity`

The Sales Velocity Equation: how much deal value your pipeline produces per day.
Expand Down Expand Up @@ -134,6 +161,13 @@ a deal whose changelog can't be fetched is skipped, counted, and reported on std
than aborting the run. `--pipeline <id>` is required when the account has more than one
pipeline, inferred otherwise.

:::caution[These mine the changelog — one request per deal]
`metrics aging`, `slippage` and `conversion-matrix` — plus [`funnel --exact`](#exact-transitions---exact)
and [`digest --deep`](#digest) — reconstruct history from each deal's changelog: one 20-token
request per deal, with a stderr warning above 100 deals. Cheap on a small pipeline, a real
cost on a large one, so scope with `--pipeline` / `--period` before running them wide.
:::

### `metrics aging`

Days-in-current-stage for every open deal, bucketed, so you can see at a glance how much
Expand Down
4 changes: 3 additions & 1 deletion website/src/content/docs/guides/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ relative path like `/api/v2/deals`.
## Request bodies

For `POST`, `PUT`, and `PATCH`, supply a JSON body with `--body`, which takes either an
inline string or an `@file` path (`GET` and `DELETE` ignore it):
inline string or an `@file` path — or omit `--body` and pipe the JSON on stdin (`GET` and
`DELETE` ignore the body):

```bash
pdcli api POST /api/v2/deals --body '{"title":"Inline JSON"}' # inline string
pdcli api POST /api/v2/deals --body @new-deal.json # @file
echo '{"title":"Piped"}' | pdcli api POST /api/v2/deals # piped stdin
```

The body is parsed as JSON before sending; invalid JSON fails before any request.
Expand Down
2 changes: 2 additions & 0 deletions website/src/content/docs/guides/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ they take precedence over the environment.)
| `PDCLI_PROFILE` | Active profile name (same as `--profile`). |
| `PDCLI_CLIENT_ID` | OAuth app client ID, read by `auth login --oauth`. |
| `PDCLI_CLIENT_SECRET` | OAuth app client secret, read by `auth login --oauth`. |
| `PDCLI_CONFIG_DIR` | Override the config-store directory — isolates profiles for tests, CI, or sandboxes so a run never touches your real profiles. Defaults to the OS config path. |
| `PDCLI_BASE_URL` | Point pdcli at a localhost mock endpoint instead of the live host. Host-locked to `localhost`/`127.0.0.1`/`::1` and requires `PDCLI_API_TOKEN`. See [Local mock endpoint](/pdcli/guides/mock/). |
| `NO_COLOR` | Any value disables colored output (same as `--no-color`). |

Together, `PDCLI_COMPANY_DOMAIN` + `PDCLI_API_TOKEN` are all you need for a fully
Expand Down
2 changes: 1 addition & 1 deletion website/src/content/docs/guides/custom-fields.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Error: Unknown field "Dela Size". Run: pdcli field list <entity>

The exact same name-and-label resolution runs on single writes (`create`/`update`), on
[`deal bulk-update`](/pdcli/guides/bulk/), and on
[CSV imports](/pdcli/guides/bulk/) — where each CSV header is treated as a field name. So a
[CSV imports](/pdcli/guides/bulk/#importing-persons-and-organizations-from-csv) — where each CSV header is treated as a field name. So a
`Deal Size` column in your spreadsheet resolves the same way `--field "Deal Size=Large"`
does on the command line.

Expand Down
Loading
Loading