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
19 changes: 19 additions & 0 deletions .github/workflows/deploy-site.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ jobs:
--llms-full-output ./docs/site/llms-full.txt \
$PREV_FLAG

- name: Stamp build freshness (date + version) into SEO assets
# index.html (JSON-LD dateModified/softwareVersion) and sitemap.xml
# (<lastmod>) carry __BUILD_DATE__/__VERSION__ placeholders in the repo.
# Substitute them here, in the throwaway runner checkout, so deployed
# structured data signals freshness without committing volatile values.
# Version comes from commands.json, which the generator just wrote using
# the same cleaning logic as the on-page version badge (single source).
run: |
BUILD_DATE="$(date -u +%Y-%m-%d)"
VERSION="$(python3 -c "import json; print(json.load(open('docs/site/commands.json'))['version'])")"
COUNT="$(python3 -c "import json; print(format(json.load(open('docs/site/commands.json'))['commandCount'], ','))")"
sed -i "s/__BUILD_DATE__/${BUILD_DATE}/g; s/__VERSION__/${VERSION}/g; s/__COMMAND_COUNT__/${COUNT}/g" \
docs/site/index.html docs/site/sitemap.xml
if grep -REn '__BUILD_DATE__|__VERSION__|__COMMAND_COUNT__' docs/site/index.html docs/site/sitemap.xml; then
echo "::error::Unsubstituted SEO placeholder(s) remain after stamping." >&2
exit 1
fi
echo "Stamped freshness: version=${VERSION} date=${BUILD_DATE} commands=${COUNT}"

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/site-preflight.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,5 @@ jobs:
--commands-json docs/site/commands.json \
--llms-txt docs/site/llms.txt \
--llms-full-txt docs/site/llms-full.txt \
--binary ./bin/jamf-cli \
--site-dir docs/site
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ verify-site-output: build
--commands-json /tmp/jamf-cli-site/commands.json \
--llms-txt /tmp/jamf-cli-site/llms.txt \
--llms-full-txt /tmp/jamf-cli-site/llms-full.txt \
--binary ./bin/jamf-cli \
--site-dir docs/site

# Verify generated code is up to date (CI-safe)
Expand Down
110 changes: 107 additions & 3 deletions docs/site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Explore all jamf-cli commands — unified CLI for Jamf Platform API Gateway, Jamf Pro, Jamf Protect, and Jamf School">
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large">
<title>jamf-cli — Your fleet, one command away</title>
<link rel="canonical" href="https://jamf-concepts.github.io/jamf-cli/">
<meta name="theme-color" content="#fbfbfa" media="(prefers-color-scheme: light)">
Expand All @@ -16,17 +17,20 @@
<!-- Open Graph / Social -->
<meta property="og:type" content="website">
<meta property="og:title" content="jamf-cli — Your fleet, one command away">
<meta property="og:description" content="Unified CLI for Jamf Platform API Gateway, Jamf Pro, Jamf Protect, and Jamf School. 1,251 commands. Full API coverage. Zero clicks.">
<meta property="og:description" content="Unified CLI for Jamf Platform API Gateway, Jamf Pro, Jamf Protect, and Jamf School. __COMMAND_COUNT__ commands. Full API coverage. Zero clicks.">
<meta property="og:url" content="https://jamf-concepts.github.io/jamf-cli/">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="jamf-cli — Your fleet, one command away">
<meta name="twitter:description" content="Unified CLI for Jamf Platform API Gateway, Jamf Pro, Jamf Protect, and Jamf School. 1,251 commands. Full API coverage. Zero clicks.">
<meta name="twitter:description" content="Unified CLI for Jamf Platform API Gateway, Jamf Pro, Jamf Protect, and Jamf School. __COMMAND_COUNT__ commands. Full API coverage. Zero clicks.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<!-- Schema.org SoftwareApplication — eligibility for Google rich results
and structured data for AI/search consumers. -->
and structured data for AI/search consumers.
__VERSION__ and __BUILD_DATE__ are substituted with the release version
and deploy date by the "Stamp build freshness" step in deploy-site.yaml.
They stay as literal placeholders in the repo (valid JSON strings). -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
Expand All @@ -37,6 +41,8 @@
"applicationCategory": "DeveloperApplication",
"applicationSubCategory": "Command Line Interface",
"operatingSystem": "macOS, Linux, Windows",
"softwareVersion": "__VERSION__",
"dateModified": "__BUILD_DATE__",
"url": "https://jamf-concepts.github.io/jamf-cli/",
"downloadUrl": "https://github.com/Jamf-Concepts/jamf-cli/releases",
"softwareHelp": "https://github.com/Jamf-Concepts/jamf-cli/wiki",
Expand Down Expand Up @@ -299,6 +305,104 @@ <h3>📦 Binary</h3>
</div>
</section>

<!-- ===== FAQ ===== -->
<!-- The visible Q&A below and the FAQPage JSON-LD that follows share the
same text on purpose (one is for humans, one for answer engines).
scripts/verify-site-output.sh fails if a FAQPage question is missing
from this visible section, so they can't silently drift apart. -->
<section id="faq">
<div class="faq-inner">
<h2>FAQ</h2>

<div class="faq-item">
<h3>What is jamf-cli?</h3>
<p>jamf-cli is a unified command-line interface for the Jamf platform. A single binary manages Jamf Platform API Gateway, Jamf Pro, Jamf Protect, and Jamf School — over 1,250 commands covering the full API surface, with structured output built for scripting and automation.</p>
</div>

<div class="faq-item">
<h3>How do I install jamf-cli?</h3>
<p>Install it with Homebrew (<code>brew install Jamf-Concepts/tap/jamf-cli</code>), with Go (<code>go install github.com/Jamf-Concepts/jamf-cli/cmd/jamf-cli@latest</code>), or by downloading a pre-built binary for macOS, Linux, or Windows from the GitHub releases page.</p>
</div>

<div class="faq-item">
<h3>How do I authenticate jamf-cli without exposing credentials?</h3>
<p>jamf-cli supports OAuth2 client credentials, bearer tokens, and Jamf Platform Gateway authentication. Credentials are never accepted through command-line flags or stdin, so they stay out of shell history and process listings — use interactive prompts, JAMF_* environment variables, or keychain-backed configuration profiles.</p>
</div>

<div class="faq-item">
<h3>Does jamf-cli support Jamf Pro, Jamf Protect, and Jamf School?</h3>
<p>Yes. One binary covers Jamf Pro (both the classic and modern APIs), Jamf Protect, Jamf School, and cross-product Jamf Platform API Gateway commands, each under its own namespace (pro, protect, school).</p>
</div>

<div class="faq-item">
<h3>Can I use jamf-cli in CI/CD pipelines and with AI agents?</h3>
<p>Yes. Every command supports structured output (<code>-o json</code>, <code>yaml</code>, or <code>csv</code>), stable exit codes, and unattended flags (<code>--no-input</code>, <code>--quiet</code>). It also publishes an llms.txt and a machine-readable command index so scripts and AI agents can discover and call commands reliably.</p>
</div>

<div class="faq-item">
<h3>Is jamf-cli free and open source?</h3>
<p>Yes. jamf-cli is free and open source under its repository license, built and maintained by Jamf Concepts.</p>
</div>
</div>
</section>

<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is jamf-cli?",
"acceptedAnswer": {
"@type": "Answer",
"text": "jamf-cli is a unified command-line interface for the Jamf platform. A single binary manages Jamf Platform API Gateway, Jamf Pro, Jamf Protect, and Jamf School — over 1,250 commands covering the full API surface, with structured output built for scripting and automation."
}
},
{
"@type": "Question",
"name": "How do I install jamf-cli?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Install it with Homebrew (brew install Jamf-Concepts/tap/jamf-cli), with Go (go install github.com/Jamf-Concepts/jamf-cli/cmd/jamf-cli@latest), or by downloading a pre-built binary for macOS, Linux, or Windows from the GitHub releases page."
}
},
{
"@type": "Question",
"name": "How do I authenticate jamf-cli without exposing credentials?",
"acceptedAnswer": {
"@type": "Answer",
"text": "jamf-cli supports OAuth2 client credentials, bearer tokens, and Jamf Platform Gateway authentication. Credentials are never accepted through command-line flags or stdin, so they stay out of shell history and process listings — use interactive prompts, JAMF_* environment variables, or keychain-backed configuration profiles."
}
},
{
"@type": "Question",
"name": "Does jamf-cli support Jamf Pro, Jamf Protect, and Jamf School?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. One binary covers Jamf Pro (both the classic and modern APIs), Jamf Protect, Jamf School, and cross-product Jamf Platform API Gateway commands, each under its own namespace (pro, protect, school)."
}
},
{
"@type": "Question",
"name": "Can I use jamf-cli in CI/CD pipelines and with AI agents?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. Every command supports structured output (-o json, yaml, or csv), stable exit codes, and unattended flags (--no-input, --quiet). It also publishes an llms.txt and a machine-readable command index so scripts and AI agents can discover and call commands reliably."
}
},
{
"@type": "Question",
"name": "Is jamf-cli free and open source?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes. jamf-cli is free and open source under its repository license, built and maintained by Jamf Concepts."
}
}
]
}
</script>

<!-- ===== Keyboard Shortcuts Modal ===== -->
<div class="kbd-modal" id="kbd-modal">
<div class="kbd-modal-content">
Expand Down
7 changes: 7 additions & 0 deletions docs/site/sitemap.xml
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- __BUILD_DATE__ is replaced with the deploy date by the "Stamp build
freshness" step in deploy-site.yaml. It stays a literal placeholder in
the repo. -->
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://jamf-concepts.github.io/jamf-cli/</loc>
<lastmod>__BUILD_DATE__</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://jamf-concepts.github.io/jamf-cli/llms.txt</loc>
<lastmod>__BUILD_DATE__</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://jamf-concepts.github.io/jamf-cli/llms-full.txt</loc>
<lastmod>__BUILD_DATE__</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://jamf-concepts.github.io/jamf-cli/commands.json</loc>
<lastmod>__BUILD_DATE__</lastmod>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
Expand Down
47 changes: 47 additions & 0 deletions docs/site/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,53 @@ pre.example-command.copied::after {
text-align: center;
}

#faq {
scroll-margin-top: 60px;
padding: 4rem 2rem;
}

.faq-inner {
max-width: 760px;
margin: 0 auto;
}

#faq h2 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 2rem;
text-align: center;
}

.faq-item {
border-top: 1px solid var(--border);
padding: 1.25rem 0;
}

.faq-item:last-child {
border-bottom: 1px solid var(--border);
}

.faq-item h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}

.faq-item p {
color: var(--text-secondary);
line-height: 1.6;
font-size: 0.9rem;
}

.faq-item code {
font-family: 'Geist Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.82em;
background: var(--page-bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.1em 0.35em;
}

.install-grid {
display: flex;
flex-direction: column;
Expand Down
9 changes: 8 additions & 1 deletion generator/site/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,14 @@ func renderLLMSTxt(d siteData) string {
fmt.Fprintf(&b, "- Credentials never accepted via CLI flags or stdin (shell-history safe). Interactive prompts, env vars (`JAMF_*`, `JAMFPROTECT_*`), or keychain-backed config profiles.\n\n")

fmt.Fprintf(&b, "## Output Formats\n\n")
fmt.Fprintf(&b, "Every command supports `-o {table,plain,json,yaml,csv}` plus `--field <path>` for jq-free scalar extraction. Pipe-friendly, scriptable, NO_COLOR aware.\n\n")
fmt.Fprintf(&b, "Every command supports `-o {table,plain,json,yaml,csv}` (default `json`). Shrink large responses with `--select id,general.name` (project dot-path fields), `--field general.name` (extract one scalar), or `--compact` (drop arrays and nested objects). Lists over 50 rows print a `hint:` to stderr suggesting how to narrow. Pipe-friendly, scriptable, NO_COLOR aware.\n\n")

fmt.Fprintf(&b, "## For AI Agents\n\n")
fmt.Fprintf(&b, "**Discover commands at runtime.** `jamf-cli commands -o json` emits the full command tree (path, description, flags, aliases, product, group). Prefer it over guessing command names.\n\n")
fmt.Fprintf(&b, "**Run unattended.** Pass `--no-input` (fail instead of prompting) and `--quiet`. Authenticate with `JAMF_*` / `JAMFPROTECT_*` env vars — never flags. Set `JAMF_CLI_ARGS='--quiet --no-input'` to apply both to every invocation.\n\n")
fmt.Fprintf(&b, "**Parse outcomes.** Default output is JSON. Exit codes are stable: 0 success · 1 general · 2 usage · 3 auth · 4 not-found · 5 permission · 6 rate-limited. With `-o json`, failures print `{\"error\",\"message\",\"exitCode\"}`, so success and failure are both machine-readable.\n\n")
fmt.Fprintf(&b, "**Stay within context.** Use `--select`, `--field`, and `--compact` to avoid pulling multi-thousand-line payloads you don't need.\n\n")
fmt.Fprintf(&b, "**Mutate safely.** `apply` is a name-based upsert — prefer it over create/update for idempotency. Destructive commands require `--yes` (bulk also requires `--confirm-destructive`). Preview any write with `--dry-run`/`-n`.\n\n")

fmt.Fprintf(&b, "## Optional\n\n")
fmt.Fprintf(&b, "- [Source code (Go)](https://github.com/Jamf-Concepts/jamf-cli)\n")
Expand Down
68 changes: 68 additions & 0 deletions generator/site/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ package main

import (
"encoding/json"
"fmt"
"strings"
"testing"

"github.com/Jamf-Concepts/jamf-cli/internal/exitcode"
)

func TestTransformCommands(t *testing.T) {
Expand Down Expand Up @@ -280,6 +284,26 @@ func TestParseVersion(t *testing.T) {
input: "jamf-cli v2.0.0-beta.1\n commit: abc1234\n",
want: "2.0.0-beta.1",
},
{
name: "json release (default -o json output)",
input: `{
"version": "1.18.0",
"commit": "c71ad8a",
"built": "2026-05-31T05:22:01Z",
"specProVersion": "unknown"
}`,
want: "1.18.0",
},
{
name: "json dirty dev build strips git-describe suffix",
input: `{"version":"v1.17.0-25-g74846ff-dirty","commit":"74846ff"}`,
want: "1.17.0",
},
{
name: "json pre-release preserved",
input: `{"version":"v2.0.0-beta.1"}`,
want: "2.0.0-beta.1",
},
{
name: "empty output",
input: "",
Expand Down Expand Up @@ -321,3 +345,47 @@ func TestParseVersion(t *testing.T) {
})
}
}

// TestRenderLLMSTxt_AgentContract guards the "For AI Agents" section. The
// exit-code table is the load-bearing part: it must stay in lockstep with
// internal/exitcode. We interpolate each constant's value here, so renaming
// or renumbering a code in exitcode.go without updating llms.txt fails CI.
func TestRenderLLMSTxt_AgentContract(t *testing.T) {
out := renderLLMSTxt(siteData{Version: "1.0.0", CommandCount: 1200})

if !strings.Contains(out, "## For AI Agents") {
t.Fatalf("llms.txt is missing the \"For AI Agents\" section")
}

exitCodes := []struct {
code int
label string
}{
{exitcode.Success, "success"},
{exitcode.General, "general"},
{exitcode.Usage, "usage"},
{exitcode.Authentication, "auth"},
{exitcode.NotFound, "not-found"},
{exitcode.PermissionDenied, "permission"},
{exitcode.RateLimited, "rate-limited"},
}
for _, ec := range exitCodes {
want := fmt.Sprintf("%d %s", ec.code, ec.label)
if !strings.Contains(out, want) {
t.Errorf("exit-code table out of sync with internal/exitcode: missing %q", want)
}
}

// Spot-check the rest of the contract so a silent deletion is caught.
for _, want := range []string{
"jamf-cli commands -o json",
"--no-input",
"--select",
"--confirm-destructive",
"apply",
} {
if !strings.Contains(out, want) {
t.Errorf("agent contract missing %q", want)
}
}
}
Loading