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
11 changes: 10 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ Order irrelevant.
reads `tui.json`.
- `@mode` — `replace` (default) | `merge` | `merge-overwrite` | `append`.
- `@fetch: URL -> dest [sha256=hex]` — repeatable.
- `@archive: URL -> destDir tar-sha256=hex [subdir=path]` — repeatable.
`.tar.gz` unpacked into a cache dir (`src/fetch-archive.ts`): top-level dir
stripped, only `subdir` kept. Hash = sha256 of the tar after gunzip, not the
download: GitHub changed only gzip so far (Jan 2023), and
`git archive --format=tar --prefix=<repo>-<sha>/ <sha>` reproduces the tar
from any clone — that is the second source. Links, devices, `..`, absolute
paths → whole archive refused. Cache hit = marker in destDir with hash +
subdir. Tagless repo (`skill-diagram-design`): commit SHA in `@pins`, source
kind `github-branch` with `path` = what gets installed (head moves hourly).
- `@pins: name version` — repeatable, one per pinned third-party artifact.
Version must also appear in body or `@fetch` (tested).
- `@prompt: name | type | help | default | setup` — repeatable; type
Expand Down Expand Up @@ -213,7 +222,7 @@ wrong, not good news — lombok (since dropped) needed git tags because Maven
Central's index and projectlombok.org's download page each reported a stale
"latest".

`@fetch` → always `sha256=`, and version the dest filename; `fetchAsset` skips
`@fetch` → always `sha256=` (`@archive`: `tar-sha256=`), and version the dest filename; `fetchAsset` skips
an existing dest, so reusing the filename makes a bump a no-op. Verify the hash
against a second source before committing.

Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ something outside your opencode config say so in their description.
| `plugin-superpowers` | Plugin | append | Add the Superpowers OpenCode plugin from `obra/superpowers` (brainstorming, plans, TDD, review workflows; pins tag `v7.0.0`) |
| `agent-runaway-guard` | Agent | merge | Adds step limits to built-in agents to prevent runaway tool loops |
| `default-agent-plan` | Agent | replace | Sets the default agent to "plan" so opencode always starts in plan mode instead of build mode |
| `planify-german` | Bundle | — | The whole planify setup, which does nothing in parts: the `opencode-planify-german` plugin from npm (pinned to `1.0.1`, needs opencode 2), registering a `plan_render` tool that validates a plan JSON against a schema and renders a self-contained HTML file to `docs/plans/<TICKET>-<slug>.html`, and a `pr_summary` tool that writes a short task/problem/solution summary into the description of the Bitbucket Data Center PR of the current branch, opening it as a draft if none is open; the rules file that makes opencode answer in German with Swiss orthography (never the eszett character, always `ss`) and send every plan through `plan_render`; and the `planify` skill documenting the plan schema. Prompts for the Bitbucket URL and an HTTP access token with repository write, stored as plugin options in `opencode.json` (token in plain text; prefer `--set-env bitbucketToken=ENV_VAR`). Rules file and skill are fetched sha256-verified. Run `opencode service restart` after installing |
| `skill-diagram-design` | Skill | append | Needs a clone of the repo on disk first — `git clone https://github.com/cathrynlavery/diagram-design ~/src/diagram-design`. Registers the `diagram-design` skill (editorial diagram types as self-contained HTML + SVG) by appending your clone's `skills/` dir to `skills.paths`; answer with that clone's `skills/` dir (`--set skillsDir=/Users/you/src/diagram-design/skills`); the install refuses if the dir is not there. Tracks `main` — the repo ships no tags. One-way: `remove` cannot undo an append preset that prompts, so uninstall by deleting the one `skills.paths` entry by hand |
| `planify-german` | Bundle | — | The whole planify setup, which does nothing in parts: the `opencode-planify-german` plugin from npm (pinned to `1.0.1`, needs opencode 2), registering a `plan_render` tool that validates a plan JSON against a schema and renders a self-contained HTML file to `docs/plans/<TICKET>-<slug>.html`, and a `pr_summary` tool that writes a short task/problem/solution summary into the description of the Bitbucket Data Center PR of the current branch, opening it as a draft if none is open; the rules file that makes opencode answer in German with Swiss orthography (never the eszett character, always `ss`) and send every plan through `plan_render`; the `planify` skill documenting the plan schema; and `skill-diagram-design`, which draws the plan's diagrams. Prompts for the Bitbucket URL and an HTTP access token with repository write, stored as plugin options in `opencode.json` (token in plain text; prefer `--set-env bitbucketToken=ENV_VAR`). Rules file and skills are fetched sha256-verified. Run `opencode service restart` after installing |
| `skill-diagram-design` | Skill | append | Registers the `diagram-design` skill (editorial diagram types as self-contained HTML + SVG). Downloads the archive of one pinned commit of [cathrynlavery/diagram-design](https://github.com/cathrynlavery/diagram-design), verifies its tar sha256, unpacks its `skills/` dir to `<cache>/diagram-design-skills-<version>` and appends that to `skills.paths`. No clone. The repo ships no tags, so a new upstream commit means a new preset version. Part of `planify-german`, so `remove planify-german` removes it too. Coming from 0.3.x (clone + prompt): drop `--set skillsDir` from scripts, and delete the old clone's `skills.paths` entry by hand, or the skill is registered twice |

### Bundles

Expand Down Expand Up @@ -363,6 +363,14 @@ Presets write `opencode.json` only. `@target: tui` is rejected:
opencode 2 does not read `tui.json`.

`@fetch: <url> -> <dest> [sha256=hex]` downloads to the cache.
`@archive: <url> -> <dir> tar-sha256=hex [subdir=path]` downloads a
`.tar.gz` and unpacks it into the cache: the archive's top-level directory is
stripped, and with `subdir` only that part is kept. The hash is the sha256 of
the tar after gunzip, so a GitHub archive can be checked against
`git archive --format=tar --prefix=<repo>-<sha>/ <sha>` from any clone. Only
regular files and directories are accepted; a link, device or path escaping
the directory refuses the whole archive. Unpacked to a temp dir and renamed
into place, so an interrupted install never looks complete.
`@prompt: name | text|secret|dir | help | default | setup` collects input
at install time; everything after the type is optional. Both repeatable.
Reference fetched files as `{{cache}}/<name>` and prompt values as
Expand Down
6 changes: 3 additions & 3 deletions presets/instructions-planify-german.conf
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
// offered once the work is done. Appends a rules file to
// `instructions`, replacing no built-in prompt.
// @author: Jan <jan@trick77.com>
// @version: 1.0.1
// @version: 1.0.2
// @part-of: planify-german
// @path: instructions
// @mode: append
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/bb523af9b2679886d72b8d28c8f564e956813a29/instructions/planify.md -> {{cache}}/opencode-planify-german-rules-1.0.1.md sha256=f062418f13605f894e291f5b80dac97e373400e591bac166f1b554e629e92213
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/e235864efae408fe07752d6d3d53827281036647/instructions/planify.md -> {{cache}}/opencode-planify-german-rules-1.0.2.md sha256=31f2049890ad7316b76cf6ec144464ff339d2827eb79606ef17982db1b1c3cc6

[
"{{cache}}/opencode-planify-german-rules-1.0.1.md"
"{{cache}}/opencode-planify-german-rules-1.0.2.md"
]
11 changes: 6 additions & 5 deletions presets/planify-german.conf
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// @name: planify-german
// @description: Bundle: the whole planify setup — the plugin providing
// `plan_render` (plan JSON to HTML) and `pr_summary` (summary into a Bitbucket
// Data Center PR, opened as draft if needed), the German rules file that routes
// every plan through them, and the skill documenting the plan schema. Asks for
// the Bitbucket URL and a repo-write HTTP access token; the token lands in
// opencode.json in plain text. Install all three or the setup is inert.
// Data Center PR), the German rules file routing every plan through them, the
// plan schema skill, and the diagram-design skill (removed with the bundle).
// Asks for the Bitbucket URL and a repo-write HTTP access token; the token
// lands in opencode.json in plain text.
// @author: Jan <jan@trick77.com>
// @version: 1.0.1
// @version: 1.0.2
// @include: plugin-planify-german
// @include: instructions-planify-german
// @include: skill-planify-german
// @include: skill-diagram-design
18 changes: 9 additions & 9 deletions presets/skill-diagram-design.conf
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// @name: skill-diagram-design
// @description: Needs a clone of github.com/cathrynlavery/diagram-design on disk
// first — clone it yourself, then answer with the absolute path to its skills/
// dir; the install refuses if it is not there. Registers the diagram-design
// skill: editorial diagram types rendered as self-contained HTML + SVG. No tags
// upstream, so you track main: update with git pull, and review what you pull.
// Fetches and runs nothing. Uninstall by deleting the skills.paths entry.
// @description: Registers the diagram-design skill: editorial diagram types
// rendered as self-contained HTML + SVG. Downloads the archive of one pinned
// commit of github.com/cathrynlavery/diagram-design, verifies its tar sha256,
// unpacks its skills/ dir into the cache and appends that to `skills.paths` —
// no clone needed.
// @author: Jan <jan@trick77.com>
// @version: 0.3.1
// @version: 1.0.0
// @path: skills.paths
// @mode: append
// @prompt: skillsDir | dir | absolute path to your diagram-design clone's skills/ dir | | clone it first: git clone https://github.com/cathrynlavery/diagram-design, then answer with the skills/ dir inside that clone
// @pins: diagram-design 10cfad6a7be7f734a92d3d628e5fa344685d4f01
// @archive: https://github.com/cathrynlavery/diagram-design/archive/10cfad6a7be7f734a92d3d628e5fa344685d4f01.tar.gz -> {{cache}}/diagram-design-skills-1.0.0 tar-sha256=642c222b4135497d52bc67db854e371f46edcc6ec58edddb6c42aae3e0a17883 subdir=skills
[
"{{prompt:skillsDir}}"
"{{cache}}/diagram-design-skills-1.0.0"
]
10 changes: 5 additions & 5 deletions presets/skill-planify-german.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
// sha256-verified, and appends that directory to `skills.paths` — no clone needed.
// Uninstall by deleting the entry.
// @author: Jan <jan@trick77.com>
// @version: 1.0.1
// @version: 1.0.2
// @part-of: planify-german
// @path: skills.paths
// @mode: append
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/bb523af9b2679886d72b8d28c8f564e956813a29/skills/planify/SKILL.md -> {{cache}}/opencode-planify-german-skills-1.0.1/planify/SKILL.md sha256=e43bc633bacfccaac949500f55f7263dcbb9db0de95cecc5c73787a3218d7c60
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/bb523af9b2679886d72b8d28c8f564e956813a29/skills/planify/schema/plan.schema.json -> {{cache}}/opencode-planify-german-skills-1.0.1/planify/schema/plan.schema.json sha256=1617c2c2789d2ed2421145049b4a4dbe8614c6ef0e9027b85f31643ca6faee73
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/bb523af9b2679886d72b8d28c8f564e956813a29/skills/planify/references/beispiel-plan.json -> {{cache}}/opencode-planify-german-skills-1.0.1/planify/references/beispiel-plan.json sha256=f4507cf91ed84c54b323dede8050dcf9c15dfdb5ca9f6f1b78a2c1f6b4036bc3
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/e235864efae408fe07752d6d3d53827281036647/skills/planify/SKILL.md -> {{cache}}/opencode-planify-german-skills-1.0.2/planify/SKILL.md sha256=1b349de872d7c2d659ef729535ffadfcf2a10a4f428abeb6fd615ab2bc79ed75
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/e235864efae408fe07752d6d3d53827281036647/skills/planify/schema/plan.schema.json -> {{cache}}/opencode-planify-german-skills-1.0.2/planify/schema/plan.schema.json sha256=1617c2c2789d2ed2421145049b4a4dbe8614c6ef0e9027b85f31643ca6faee73
// @fetch: https://raw.githubusercontent.com/trick77/opencode-planify-german/e235864efae408fe07752d6d3d53827281036647/skills/planify/references/beispiel-plan.json -> {{cache}}/opencode-planify-german-skills-1.0.2/planify/references/beispiel-plan.json sha256=f4507cf91ed84c54b323dede8050dcf9c15dfdb5ca9f6f1b78a2c1f6b4036bc3

[
"{{cache}}/opencode-planify-german-skills-1.0.1"
"{{cache}}/opencode-planify-german-skills-1.0.2"
]
24 changes: 22 additions & 2 deletions scripts/check-pins.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,26 @@ async function latestFor(source) {
if (!latest) throw new Error(`${source.repo}: no version-shaped tags on the first page`);
return latest;
}
case 'github-branch': {
// Newest commit on the branch that touches the installed path.
const body = await getJson(
`https://api.github.com/repos/${source.repo}/commits?sha=${source.branch}&path=${source.path}&per_page=1`,
ghHeaders());
if (!body.length) throw new Error(`${source.repo}: no commits touch ${source.path} on ${source.branch}`);
return body[0].sha;
}
}
throw new Error(`unhandled pin source kind: ${source.kind}`);
}

// A pinned commit is current when it already contains the newest commit that
// touched the path: compare reports the pin identical to it or ahead of it.
async function branchCmp(source, pinned, latest) {
const body = await getJson(
`https://api.github.com/repos/${source.repo}/compare/${latest}...${pinned}`, ghHeaders());
return body.status === 'identical' || body.status === 'ahead' ? 0 : -1;
}

async function shippedPins() {
const files = (await readdir(PRESET_DIR)).filter(n => n.endsWith('.conf')).sort();
const pins = [];
Expand Down Expand Up @@ -96,7 +112,9 @@ for (const pin of pins) {

try {
const latest = await latestFor(source);
const cmp = compareVersions(pin.version, latest);
const cmp = source.kind === 'github-branch'
? await branchCmp(source, pin.version, latest)
: compareVersions(pin.version, latest);
// A pin ahead of upstream is not good news — it means this source is not
// authoritative for that artifact, so its "ok" verdicts are worthless too.
// Both sources first tried for lombok (since dropped) failed exactly that way.
Expand All @@ -110,9 +128,11 @@ for (const pin of pins) {
}

const w = (s, n) => String(s).padEnd(n);
// Commit SHAs shortened so the table keeps its columns.
const v = s => /^[0-9a-f]{40}$/.test(String(s)) ? String(s).slice(0, 10) : s;
console.log(`${w('PIN', 34)}${w('PINNED', 12)}${w('LATEST', 12)}${w('STATE', 15)}PRESET`);
for (const r of rows) {
console.log(`${w(r.name, 34)}${w(r.version, 12)}${w(r.latest, 12)}${w(r.state, 15)}${r.preset}`);
console.log(`${w(r.name, 34)}${w(v(r.version), 12)}${w(v(r.latest), 12)}${w(r.state, 15)}${r.preset}`);
if (r.detail) console.log(` ${r.detail}`);
}

Expand Down
15 changes: 14 additions & 1 deletion src/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ConfMeta, FetchDirective } from './parse-conf.js';
import { applyAtPath, removeAtPath, getAtPath } from './merge.js';
import type { ApplyStats, RemoveStats, MergeMode } from './merge.js';
import { fetchAsset } from './fetch-asset.js';
import { fetchArchive } from './fetch-archive.js';
import { checkDir, findOnPath, formatSetup } from './preconditions.js';
import { recoverPromptValues } from './recover-prompts.js';
import { backup } from './backup.js';
Expand Down Expand Up @@ -219,6 +220,16 @@ export async function runBatch(opts: RunBatchOpts): Promise<void> {
process.exit(1);
}
}
for (const a of m.meta.archive) {
a.dest = expandCacheStr(a.dest, cacheDir);
try {
const r = await fetchArchive(a);
console.log(' ' + c.ok('✓') + ' ' + (r.cached ? 'cached ' : `unpacked ${r.files} files to `) + c.meta(a.dest));
} catch (e) {
console.error(c.err('error: ') + (e instanceof Error ? e.message : String(e)));
process.exit(1);
}
}
}

// ── Compute cumulative new root ──
Expand Down Expand Up @@ -441,11 +452,13 @@ function renderSummary(
for (const line of agentOverrideWarnings(modules, existing)) lines.push(line);

const totalFetches = modules.reduce((n, m) => n + m.meta.fetch.length, 0);
const totalArchives = modules.reduce((n, m) => n + m.meta.archive.length, 0);
const totalPrompts = modules.reduce((n, m) => n + m.meta.prompts.length, 0);
const totalPins = modules.reduce((n, m) => n + m.meta.pins.length, 0);
if (totalFetches > 0 || totalPrompts > 0 || totalPins > 0) {
if (totalFetches > 0 || totalArchives > 0 || totalPrompts > 0 || totalPins > 0) {
const bits: string[] = [];
if (totalFetches > 0) bits.push(`${totalFetches} fetch${totalFetches === 1 ? '' : 'es'}`);
if (totalArchives > 0) bits.push(`${totalArchives} archive${totalArchives === 1 ? '' : 's'}`);
if (totalPrompts > 0) bits.push(`${totalPrompts} prompt${totalPrompts === 1 ? '' : 's'}`);
if (totalPins > 0) bits.push(`${totalPins} pin${totalPins === 1 ? '' : 's'}`);
lines.push(' ' + c.dim(bits.join(', ')));
Expand Down
Loading
Loading