opencode-thrum-pro 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/commands/thrum-compact-extended.md +14 -12
- package/assets/commands/thrum-compact.md +17 -15
- package/assets/commands/thrum-prime.md +1 -0
- package/assets/skills/configure-roles/SKILL.md +0 -1
- package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +15 -6
- package/assets/skills/coordinator-merging-code/SKILL.md +21 -7
- package/assets/skills/coordinator-merging-code/resources/overbuild-check-prompt.md +60 -0
- package/assets/skills/coordinator-philosophy-merge-gate/SKILL.md +5 -4
- package/assets/skills/persistent-watcher-archetype/SKILL.md +142 -60
- package/assets/skills/project-hotpath-gate/SKILL.md +8 -0
- package/assets/skills/thrum/resources/CLI_REFERENCE.md +14 -6
- package/assets/skills/thrum/resources/TMUX_SESSIONS.md +16 -8
- package/assets/skills/using-tmux-ssh/resources/tmux-ssh +23 -2
- package/assets/skills/watcher-archetype/SKILL.md +13 -5
- package/dist/installer-assets.test.js +5 -2
- package/dist/installer.d.ts +5 -1
- package/dist/installer.js +95 -7
- package/dist/installer.test.js +102 -0
- package/package.json +4 -1
- package/dist/permission-fixture.integration.test.js +0 -506
- /package/dist/{permission-fixture.integration.test.d.ts → installer.test.d.ts} +0 -0
package/dist/installer.js
CHANGED
|
@@ -5,16 +5,32 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const ASSETS_DIR = path.resolve(__dirname, "..", "assets");
|
|
7
7
|
const VERSION_FILE = ".thrum-plugin-version";
|
|
8
|
-
|
|
8
|
+
// Written next to the version file so a later release can tell pro from
|
|
9
|
+
// open-core installs and clean up safely.
|
|
10
|
+
const FLAVOR_FILE = ".thrum-plugin-flavor";
|
|
11
|
+
const DEFAULT_SKILLS_SUBDIR = "thrum";
|
|
12
|
+
function readPackageJson() {
|
|
9
13
|
const pkgPath = path.resolve(__dirname, "..", "package.json");
|
|
10
14
|
try {
|
|
11
|
-
|
|
12
|
-
return pkg.version ?? "0.0.0";
|
|
15
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
13
16
|
}
|
|
14
17
|
catch {
|
|
15
|
-
return
|
|
18
|
+
return {};
|
|
16
19
|
}
|
|
17
20
|
}
|
|
21
|
+
function getPluginVersion() {
|
|
22
|
+
return readPackageJson().version ?? "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
// The pro build sets thrum.skillsSubdir in the packaged package.json to
|
|
25
|
+
// "thrum-pro" so it never shares a directory with the open-core package.
|
|
26
|
+
export function sanitizeSkillsSubdir(sub) {
|
|
27
|
+
return sub && /^[A-Za-z0-9._-]+$/.test(sub) && sub !== "." && sub !== ".."
|
|
28
|
+
? sub
|
|
29
|
+
: DEFAULT_SKILLS_SUBDIR;
|
|
30
|
+
}
|
|
31
|
+
export function getSkillsSubdir() {
|
|
32
|
+
return sanitizeSkillsSubdir(readPackageJson().thrum?.skillsSubdir);
|
|
33
|
+
}
|
|
18
34
|
export function getConfigDir() {
|
|
19
35
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
20
36
|
if (xdg)
|
|
@@ -34,11 +50,77 @@ function copyDirRecursive(src, dest) {
|
|
|
34
50
|
}
|
|
35
51
|
}
|
|
36
52
|
}
|
|
37
|
-
|
|
53
|
+
// Older pro releases (0.1.x) installed flat (skills/<name>) with the marker in
|
|
54
|
+
// skills/thrum, the same layout open-core still uses. OpenCode resolves
|
|
55
|
+
// duplicate skill names to the flat copy, so stale flat pro skills would shadow
|
|
56
|
+
// the nested ones. Remove them only when the install is positively identified
|
|
57
|
+
// as pro; anything else (open-core, unknown version, unreadable) is left alone.
|
|
58
|
+
function isLegacyProInstall(markerDir) {
|
|
59
|
+
const read = (name) => {
|
|
60
|
+
try {
|
|
61
|
+
return fs.readFileSync(path.join(markerDir, name), "utf8").trim();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const flavor = read(FLAVOR_FILE);
|
|
68
|
+
if (flavor !== undefined)
|
|
69
|
+
return flavor === "pro";
|
|
70
|
+
const version = read(VERSION_FILE);
|
|
71
|
+
return version !== undefined && /^0\.1\.\d+([-+].*)?$/.test(version);
|
|
72
|
+
}
|
|
73
|
+
async function cleanLegacyFlatInstall(skillsDir, log) {
|
|
74
|
+
const markerDir = path.join(skillsDir, DEFAULT_SKILLS_SUBDIR);
|
|
75
|
+
if (!fs.existsSync(path.join(markerDir, VERSION_FILE)))
|
|
76
|
+
return;
|
|
77
|
+
if (!isLegacyProInstall(markerDir)) {
|
|
78
|
+
await log(`Legacy flat install found at ${markerDir}; not removed (not identified as an old pro install). If it is one, delete it and its sibling skill dirs manually.`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const assetsSkillsDir = path.join(ASSETS_DIR, "skills");
|
|
82
|
+
let removed = 0;
|
|
83
|
+
if (fs.existsSync(assetsSkillsDir)) {
|
|
84
|
+
for (const entry of fs.readdirSync(assetsSkillsDir, { withFileTypes: true })) {
|
|
85
|
+
if (!entry.isDirectory())
|
|
86
|
+
continue;
|
|
87
|
+
const flat = path.join(skillsDir, entry.name);
|
|
88
|
+
if (!fs.existsSync(flat))
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
fs.rmSync(flat, { recursive: true });
|
|
92
|
+
removed++;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// best-effort; a locked dir must not block the install
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const marker of [VERSION_FILE, FLAVOR_FILE]) {
|
|
100
|
+
try {
|
|
101
|
+
fs.rmSync(path.join(markerDir, marker), { force: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// best-effort
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
fs.rmdirSync(markerDir); // only succeeds when empty
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// not empty or already gone
|
|
112
|
+
}
|
|
113
|
+
await log(`Removed ${removed} legacy flat pro skill dirs under ${skillsDir}`);
|
|
114
|
+
}
|
|
115
|
+
export async function installAssets(ctx, opts = {}) {
|
|
38
116
|
const configDir = getConfigDir();
|
|
39
117
|
const skillsDir = path.join(configDir, "skills");
|
|
40
118
|
const commandsDir = path.join(configDir, "commands");
|
|
41
|
-
const
|
|
119
|
+
const subdir = opts.skillsSubdir !== undefined
|
|
120
|
+
? sanitizeSkillsSubdir(opts.skillsSubdir)
|
|
121
|
+
: getSkillsSubdir();
|
|
122
|
+
const installRoot = path.join(skillsDir, subdir);
|
|
123
|
+
const versionPath = path.join(installRoot, VERSION_FILE);
|
|
42
124
|
const currentVersion = getPluginVersion();
|
|
43
125
|
// Check if already installed at this version
|
|
44
126
|
try {
|
|
@@ -52,6 +134,9 @@ export async function installAssets(ctx) {
|
|
|
52
134
|
const log = (msg) => ctx.client.app.log({
|
|
53
135
|
body: { service: "opencode-thrum", level: "info", message: msg },
|
|
54
136
|
});
|
|
137
|
+
if (subdir !== DEFAULT_SKILLS_SUBDIR) {
|
|
138
|
+
await cleanLegacyFlatInstall(skillsDir, log);
|
|
139
|
+
}
|
|
55
140
|
// Copy each skill subdirectory
|
|
56
141
|
const assetsSkillsDir = path.join(ASSETS_DIR, "skills");
|
|
57
142
|
if (fs.existsSync(assetsSkillsDir)) {
|
|
@@ -61,7 +146,9 @@ export async function installAssets(ctx) {
|
|
|
61
146
|
if (!entry.isDirectory())
|
|
62
147
|
continue;
|
|
63
148
|
const src = path.join(assetsSkillsDir, entry.name);
|
|
64
|
-
|
|
149
|
+
// Open-core keeps its historical flat layout (skills/<name>); only a
|
|
150
|
+
// non-default subdir (pro) nests skills under skills/<subdir>/.
|
|
151
|
+
const dest = path.join(subdir === DEFAULT_SKILLS_SUBDIR ? skillsDir : installRoot, entry.name);
|
|
65
152
|
copyDirRecursive(src, dest);
|
|
66
153
|
await log(`Installed skill: ${entry.name}`);
|
|
67
154
|
}
|
|
@@ -82,6 +169,7 @@ export async function installAssets(ctx) {
|
|
|
82
169
|
// Write version marker
|
|
83
170
|
fs.mkdirSync(path.dirname(versionPath), { recursive: true });
|
|
84
171
|
fs.writeFileSync(versionPath, currentVersion);
|
|
172
|
+
fs.writeFileSync(path.join(installRoot, FLAVOR_FILE), subdir === DEFAULT_SKILLS_SUBDIR ? "open-core" : "pro");
|
|
85
173
|
await log(`opencode-thrum v${currentVersion} assets installed`);
|
|
86
174
|
}
|
|
87
175
|
const ALLOWLIST_ASSET_PATH = path.join(ASSETS_DIR, "thrum_allowlist.json");
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { installAssets, sanitizeSkillsSubdir } from "./installer.js";
|
|
7
|
+
const ctx = { client: { app: { log: async () => { } } } };
|
|
8
|
+
function withConfig(fn) {
|
|
9
|
+
return async () => {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-inst-"));
|
|
11
|
+
const prev = process.env.XDG_CONFIG_HOME;
|
|
12
|
+
process.env.XDG_CONFIG_HOME = dir;
|
|
13
|
+
try {
|
|
14
|
+
await fn(path.join(dir, "opencode"));
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
if (prev === undefined)
|
|
18
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
19
|
+
else
|
|
20
|
+
process.env.XDG_CONFIG_HOME = prev;
|
|
21
|
+
fs.rmSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const pkgVersion = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "..", "package.json"), "utf8")).version;
|
|
26
|
+
test("open-core: flat skills/<name>, marker in skills/thrum", withConfig(async (cfg) => {
|
|
27
|
+
await installAssets(ctx, { skillsSubdir: "thrum" });
|
|
28
|
+
assert.ok(fs.existsSync(path.join(cfg, "skills", "thrum", ".thrum-plugin-version")));
|
|
29
|
+
assert.ok(fs.existsSync(path.join(cfg, "skills", "configure-roles")));
|
|
30
|
+
assert.ok(!fs.existsSync(path.join(cfg, "skills", "thrum-pro")));
|
|
31
|
+
assert.equal(fs.readFileSync(path.join(cfg, "skills", "thrum", ".thrum-plugin-flavor"), "utf8"), "open-core");
|
|
32
|
+
}));
|
|
33
|
+
test("pro: skills nested under skills/thrum-pro, marker there", withConfig(async (cfg) => {
|
|
34
|
+
await installAssets(ctx, { skillsSubdir: "thrum-pro" });
|
|
35
|
+
const v = fs.readFileSync(path.join(cfg, "skills", "thrum-pro", ".thrum-plugin-version"), "utf8");
|
|
36
|
+
assert.equal(v, pkgVersion);
|
|
37
|
+
assert.ok(fs.existsSync(path.join(cfg, "skills", "thrum-pro", "configure-roles")));
|
|
38
|
+
assert.ok(!fs.existsSync(path.join(cfg, "skills", "configure-roles")));
|
|
39
|
+
assert.equal(fs.readFileSync(path.join(cfg, "skills", "thrum-pro", ".thrum-plugin-flavor"), "utf8"), "pro");
|
|
40
|
+
}));
|
|
41
|
+
test("pro: leaves an existing skills/thrum install untouched", withConfig(async (cfg) => {
|
|
42
|
+
const legacy = path.join(cfg, "skills", "thrum");
|
|
43
|
+
fs.mkdirSync(legacy, { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(legacy, ".thrum-plugin-version"), "9.9.9");
|
|
45
|
+
await installAssets(ctx, { skillsSubdir: "thrum-pro" });
|
|
46
|
+
assert.equal(fs.readFileSync(path.join(legacy, ".thrum-plugin-version"), "utf8"), "9.9.9");
|
|
47
|
+
}));
|
|
48
|
+
test("invalid subdir falls back to default", () => {
|
|
49
|
+
for (const bad of ["../evil", "a/b", "", "..", ".", undefined]) {
|
|
50
|
+
assert.equal(sanitizeSkillsSubdir(bad), "thrum");
|
|
51
|
+
}
|
|
52
|
+
assert.equal(sanitizeSkillsSubdir("thrum-pro"), "thrum-pro");
|
|
53
|
+
});
|
|
54
|
+
function seedLegacy(cfg, marker) {
|
|
55
|
+
const skills = path.join(cfg, "skills");
|
|
56
|
+
const thrumDir = path.join(skills, "thrum");
|
|
57
|
+
fs.mkdirSync(thrumDir, { recursive: true });
|
|
58
|
+
if (marker.version !== undefined)
|
|
59
|
+
fs.writeFileSync(path.join(thrumDir, ".thrum-plugin-version"), marker.version);
|
|
60
|
+
if (marker.flavor !== undefined)
|
|
61
|
+
fs.writeFileSync(path.join(thrumDir, ".thrum-plugin-flavor"), marker.flavor);
|
|
62
|
+
for (const n of ["configure-roles", "adversarial-critique", "user-own-skill"]) {
|
|
63
|
+
fs.mkdirSync(path.join(skills, n), { recursive: true });
|
|
64
|
+
fs.writeFileSync(path.join(skills, n, "SKILL.md"), "stale");
|
|
65
|
+
}
|
|
66
|
+
return skills;
|
|
67
|
+
}
|
|
68
|
+
test("pro: legacy flavor=pro removes matching flat dirs, keeps others", withConfig(async (cfg) => {
|
|
69
|
+
const skills = seedLegacy(cfg, { version: "0.1.16", flavor: "pro" });
|
|
70
|
+
await installAssets(ctx, { skillsSubdir: "thrum-pro" });
|
|
71
|
+
assert.ok(!fs.existsSync(path.join(skills, "configure-roles")));
|
|
72
|
+
assert.ok(!fs.existsSync(path.join(skills, "adversarial-critique")));
|
|
73
|
+
assert.ok(fs.existsSync(path.join(skills, "user-own-skill", "SKILL.md")));
|
|
74
|
+
assert.ok(!fs.existsSync(path.join(skills, "thrum", ".thrum-plugin-version")));
|
|
75
|
+
assert.ok(!fs.existsSync(path.join(skills, "thrum", ".thrum-plugin-flavor")));
|
|
76
|
+
assert.ok(fs.existsSync(path.join(skills, "thrum-pro", "configure-roles")));
|
|
77
|
+
}));
|
|
78
|
+
test("pro: legacy 0.1.16 without flavor is removed", withConfig(async (cfg) => {
|
|
79
|
+
const skills = seedLegacy(cfg, { version: "0.1.16" });
|
|
80
|
+
await installAssets(ctx, { skillsSubdir: "thrum-pro" });
|
|
81
|
+
assert.ok(!fs.existsSync(path.join(skills, "configure-roles")));
|
|
82
|
+
assert.ok(fs.existsSync(path.join(skills, "user-own-skill")));
|
|
83
|
+
assert.ok(!fs.existsSync(path.join(skills, "thrum", ".thrum-plugin-version")));
|
|
84
|
+
}));
|
|
85
|
+
test("pro: legacy 0.3.105 without flavor removes nothing", withConfig(async (cfg) => {
|
|
86
|
+
const skills = seedLegacy(cfg, { version: "0.3.105" });
|
|
87
|
+
await installAssets(ctx, { skillsSubdir: "thrum-pro" });
|
|
88
|
+
assert.ok(fs.existsSync(path.join(skills, "configure-roles", "SKILL.md")));
|
|
89
|
+
assert.ok(fs.existsSync(path.join(skills, "thrum", ".thrum-plugin-version")));
|
|
90
|
+
}));
|
|
91
|
+
test("pro: legacy flavor=open-core removes nothing", withConfig(async (cfg) => {
|
|
92
|
+
const skills = seedLegacy(cfg, { version: "0.1.16", flavor: "open-core" });
|
|
93
|
+
await installAssets(ctx, { skillsSubdir: "thrum-pro" });
|
|
94
|
+
assert.ok(fs.existsSync(path.join(skills, "configure-roles", "SKILL.md")));
|
|
95
|
+
assert.ok(fs.existsSync(path.join(skills, "thrum", ".thrum-plugin-flavor")));
|
|
96
|
+
}));
|
|
97
|
+
test("open-core build: existing flat install is not cleaned", withConfig(async (cfg) => {
|
|
98
|
+
const skills = seedLegacy(cfg, { version: "0.1.16", flavor: "pro" });
|
|
99
|
+
await installAssets(ctx, { skillsSubdir: "thrum" });
|
|
100
|
+
assert.ok(fs.existsSync(path.join(skills, "user-own-skill", "SKILL.md")));
|
|
101
|
+
assert.equal(fs.readFileSync(path.join(skills, "thrum", ".thrum-plugin-flavor"), "utf8"), "open-core");
|
|
102
|
+
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-thrum-pro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Thrum multi-agent coordination plugin for Open Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,5 +46,8 @@
|
|
|
46
46
|
"@opencode-ai/plugin": "^1.4.0",
|
|
47
47
|
"@types/node": "^25.5.2",
|
|
48
48
|
"typescript": "^5.3.0"
|
|
49
|
+
},
|
|
50
|
+
"thrum": {
|
|
51
|
+
"skillsSubdir": "thrum-pro"
|
|
49
52
|
}
|
|
50
53
|
}
|