Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
run: node --test scripts/validate.test.mjs
- name: Validate manifests + skills
run: node scripts/validate.mjs
- name: Validate install/recovery docs
run: node scripts/validate-install-docs.mjs
- name: Version consistency (plugin.json == package.json)
run: |
PJ=$(jq -r .version .codex-plugin/plugin.json)
Expand Down
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ All contributors must sign the [JFrog CLA](https://jfrog.com/cla/) before contri
3. **Validate** locally:

```bash
npm test # unit tests for the manifest/skill validator
npm run validate # lint the plugin manifests + skill frontmatter
npm test # unit tests for the manifest/skill validator
npm run validate # lint the plugin manifests + skill frontmatter
node scripts/validate-install-docs.mjs # check install/verify docs stay aligned
```

1. **Test** by loading your clone as the plugin. The repo root is the marketplace root (`.agents/plugins/marketplace.json` registers the `jfrog` plugin):
Expand All @@ -41,7 +42,8 @@ This downloads the pinned upstream tarball and replaces the contents of `skills/

## Pre-release checklist

- [ ] `npm test` and `npm run validate` pass.
- [ ] `npm test`, `npm run validate`, and `node scripts/validate-install-docs.mjs` pass.
- [ ] README includes **Verify** and **Recovery** and does not send readers to another plugin repository for those steps.
- [ ] `version` bumped and **identical** in both [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) and [`package.json`](package.json) (CI enforces the match).
- [ ] No secrets, credentials, or API keys committed.
- [ ] If the skill tree changed: `pin` in [`scripts/sync-skills-vendor.json`](scripts/sync-skills-vendor.json) matches the upstream tag the new tree was generated from, and the README Prerequisites link points at that tag.
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,32 @@ Restart Codex; the `jfrog` MCP server and its tools are now available (verify wi

---

## Verify

Verification is a required install step, not a troubleshooting fallback. After
restarting Codex, confirm all four:

1. `codex plugin list` — `jfrog@codex-plugin` is installed and enabled.
2. `/plugins` in the Codex TUI — the JFrog plugin lists its skills (`jfrog` and others).
3. `codex mcp list` — `jfrog` is connected (after `codex mcp login jfrog`).
4. `jf rt ping` — succeeds against your configured JFrog server.

If any check fails, see [Recovery](#recovery). Setting MCP environment variables
by hand does not repair a failed initialization — re-run `jfrog-init` instead.

---

## Recovery

| Symptom | Do this | Do **not** do this |
| --- | --- | --- |
| MCP missing after install | Run `jfrog-init`, edit the plugin `.mcp.json` host if needed, run `codex mcp login jfrog`, **restart Codex**, then `codex mcp list`. | Assume exporting `JFROG_PLATFORM_URL` will register MCP. Codex reads the host from `.mcp.json`. |
| `jfrog-init` stopped at CLI/auth | Follow the skill prompt (`jf config add`, web login, or token path), then **re-run `jfrog-init`**. | Skip init and only export env vars. |
| Placeholder still in `.mcp.json` | Set the host in `<install-path>/.mcp.json`, run `codex mcp login jfrog`, restart Codex. | Reinstall the plugin when only the host placeholder is wrong. |
| Plugin not listed | Re-run `codex plugin add jfrog@codex-plugin` from a regular terminal, then restart Codex. | Run install commands from inside the Codex TUI. |

---

## Usage

Once configured, interact with the JFrog plugin through natural language.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"scripts": {
"sync-skills": "node scripts/sync-skills.mjs",
"validate": "node scripts/validate.mjs",
"test": "node --test scripts/validate.test.mjs"
"test": "node --test scripts/validate.test.mjs scripts/validate-install-docs.test.mjs"
}
}
97 changes: 97 additions & 0 deletions scripts/validate-install-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node
// Copyright (c) JFrog Ltd. 2026
// Licensed under the Apache License, Version 2.0
// Validates install/recovery documentation invariants.

import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

const repoRoot = process.cwd();

const HARNESS_OWN_REPO = {
claude: 'claude-plugin',
codex: 'codex-plugin',
cursor: 'cursor-plugin',
devin: 'devin-plugin',
opencode: 'opencode-jfrog-plugin',
vscode: 'vscode-plugin',
};

const ALL_PLUGIN_REPOS = Object.values(HARNESS_OWN_REPO);

const REQUIRED_README_MARKERS = ['## Verify'];

const FORBIDDEN_PATTERNS = [
{
re: /setting\s+(?:the\s+)?environment\s+variables?\s+after\s+a\s+failed\s+init\s+may\s+repair/i,
message: 'must not claim env vars repair failed init',
},
{
re: /JFROG_URL/,
message: 'must not document the legacy JFROG_URL env var; use JFROG_PLATFORM_URL',
},
{
re: /atlassian\.net/i,
message: 'must not reference JFrog Jira (atlassian.net) in repo files',
},
{
re: /\b(?:AX|MLD)-\d+\b/,
message: 'must not include Jira ticket keys in repo files',
},
];

export function validateInstallDocs({ repoRoot: root, harness }) {
const errors = [];
const readmePath = join(root, 'README.md');
if (!existsSync(readmePath)) {
return [`${harness}: missing README.md`];
}
const files = [{ label: 'README.md', text: readFileSync(readmePath, 'utf8') }];

const readme = files[0].text;
for (const marker of REQUIRED_README_MARKERS) {
if (!readme.includes(marker)) {
errors.push(`${harness}: README.md missing required marker: ${marker}`);
}
}

const ownRepo = HARNESS_OWN_REPO[harness];
const otherRepos = ALL_PLUGIN_REPOS.filter((name) => name !== ownRepo);

for (const { label, text } of files) {
for (const { re, message } of FORBIDDEN_PATTERNS) {
if (re.test(text)) errors.push(`${harness}: ${label} ${message}`);
}
for (const other of otherRepos) {
if (text.includes(`github.com/jfrog/${other}`)) {
errors.push(`${harness}: ${label} must not link to github.com/jfrog/${other}`);
}
}
}

return errors;
}

function main() {
const harness = process.env.JFROG_PLUGIN_HARNESS ?? inferHarness(repoRoot);
const errors = validateInstallDocs({ repoRoot, harness });
if (errors.length) {
console.error('install-docs validation failed:');
for (const e of errors) console.error(` - ${e}`);
process.exit(1);
}
console.log('install-docs validation passed');
}

function inferHarness(root) {
if (existsSync(join(root, '.codex-plugin'))) return 'codex';
if (existsSync(join(root, '.devin-plugin'))) return 'devin';
if (existsSync(join(root, '.claude-plugin'))) return 'claude';
if (existsSync(join(root, 'plugins', 'jfrog', '.cursor-plugin'))) return 'cursor';
if (existsSync(join(root, 'plugin', '.claude-plugin'))) return 'vscode';
if (existsSync(join(root, 'package.json')) && root.endsWith('opencode-jfrog-plugin')) return 'opencode';
return 'unknown';
}

if (process.argv[1] === fileURLToPath(import.meta.url)) main();
61 changes: 61 additions & 0 deletions scripts/validate-install-docs.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) JFrog Ltd. 2026
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { validateInstallDocs } from './validate-install-docs.mjs';

function writeReadme(root, body) {
writeFileSync(join(root, 'README.md'), body);
}

test('validateInstallDocs passes when README has Verify and no other-plugin links', () => {
const root = mkdtempSync(join(tmpdir(), 'codex-docs-'));
writeReadme(root, '# Codex\n\n## Verify\n\n1. list plugins\n');
assert.deepEqual(validateInstallDocs({ repoRoot: root, harness: 'codex' }), []);
});

test('validateInstallDocs flags missing Verify section', () => {
const root = mkdtempSync(join(tmpdir(), 'codex-docs-'));
writeReadme(root, '# Codex\n\nInstall the plugin.\n');
const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' });
assert.ok(errors.some((e) => e.includes('## Verify')));
});

test('validateInstallDocs rejects contradictory failed-init env-var recovery claims', () => {
const root = mkdtempSync(join(tmpdir(), 'codex-docs-'));
writeReadme(
root,
'# x\n## Verify\nSetting environment variables after a failed init may repair MCP registration.'
);
const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' });
assert.ok(errors.some((e) => e.includes('env vars repair failed init')));
});

test('validateInstallDocs rejects the legacy JFROG_URL env var', () => {
const root = mkdtempSync(join(tmpdir(), 'codex-docs-'));
writeReadme(root, '# Codex\n## Verify\nSet `JFROG_URL` to your platform.\n');
const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' });
assert.ok(errors.some((e) => e.includes('JFROG_URL')));
});

test('validateInstallDocs rejects links to other plugin GitHub repos', () => {
const root = mkdtempSync(join(tmpdir(), 'codex-docs-'));
writeReadme(
root,
'# Codex\n## Verify\nSee https://github.com/jfrog/claude-plugin/blob/main/README.md\n'
);
const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' });
assert.ok(errors.some((e) => e.includes('claude-plugin')));
});

test('validateInstallDocs rejects Jira URLs and ticket keys', () => {
const root = mkdtempSync(join(tmpdir(), 'codex-docs-'));
const host = ['jfrog-int', 'atlassian', 'net'].join('.');
const key = ['AX', '1780'].join('-');
writeReadme(root, `# Codex\n## Verify\nSee [${key}](https://${host}/browse/${key}).\n`);
const errors = validateInstallDocs({ repoRoot: root, harness: 'codex' });
assert.ok(errors.some((e) => e.includes('atlassian.net')));
assert.ok(errors.some((e) => e.includes('Jira ticket keys')));
});
Loading