@heart-of-gold/toolkit 0.1.41 → 0.1.43
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/README.md +3 -1
- package/package.json +1 -1
- package/src/commands/install.ts +31 -12
- package/src/index.ts +1 -1
- package/src/utils/pi-package-detection.js +81 -0
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ bunx @heart-of-gold/toolkit install --to pi
|
|
|
36
36
|
**Important:** choose one Pi install path or the other.
|
|
37
37
|
Do **not** use both the Pi package install and `install --to pi` at the same time, or Pi will report duplicate skill collisions on reload.
|
|
38
38
|
|
|
39
|
+
The CLI now refuses `install --to pi` when your Pi settings already reference `@heart-of-gold/toolkit` as a package. If you intentionally want both paths for debugging, rerun with `--force`.
|
|
40
|
+
|
|
39
41
|
Pi also discovers skills from the shared `~/.agents/skills/` location, so installs done with the OpenCode target are usable from Pi too.
|
|
40
42
|
|
|
41
43
|
When installed as a Pi package, Heart of Gold exposes Pi-native extension commands for the flagship workflows:
|
|
@@ -168,7 +170,7 @@ The toolkit ships as an npm package with a CLI for installing skills into any su
|
|
|
168
170
|
|
|
169
171
|
- `--to pi` installs to Pi's native `~/.pi/agent/skills/`
|
|
170
172
|
- `--to opencode` installs to shared `~/.agents/skills/`, which Pi also discovers
|
|
171
|
-
- `pi install npm:@heart-of-gold/toolkit`
|
|
173
|
+
- `pi install npm:@heart-of-gold/toolkit` adds the package to Pi settings, then Pi installs and loads it as a package with shared skills plus pi-native extensions
|
|
172
174
|
|
|
173
175
|
```bash
|
|
174
176
|
# Install all plugins into Codex
|
package/package.json
CHANGED
package/src/commands/install.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { loadAllPlugins, loadPlugin } from "../parsers/claude";
|
|
3
3
|
import { targets } from "../targets/index";
|
|
4
|
-
import { resolve
|
|
5
|
-
import {
|
|
6
|
-
import { execSync } from "child_process";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import { findHeartOfGoldPiPackageInstalls } from "../utils/pi-package-detection.js";
|
|
7
6
|
|
|
8
7
|
export const installCommand = defineCommand({
|
|
9
8
|
meta: {
|
|
@@ -26,21 +25,41 @@ export const installCommand = defineCommand({
|
|
|
26
25
|
description: "Override output root directory",
|
|
27
26
|
required: false,
|
|
28
27
|
},
|
|
28
|
+
force: {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
description: "Proceed even if Pi already loads @heart-of-gold/toolkit as a package",
|
|
31
|
+
required: false,
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
29
34
|
},
|
|
30
35
|
async run({ args }) {
|
|
31
36
|
const targetName = args.to;
|
|
32
37
|
|
|
33
38
|
if (targetName === "pi") {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const matches = findHeartOfGoldPiPackageInstalls(process.cwd());
|
|
40
|
+
if (matches.length > 0) {
|
|
41
|
+
const locations = matches
|
|
42
|
+
.map(({ settingsPath, source }) => ` - ${settingsPath} → ${source}`)
|
|
43
|
+
.join("\n");
|
|
44
|
+
|
|
45
|
+
const message = [
|
|
46
|
+
"Refusing to install duplicate Pi skills.",
|
|
47
|
+
"Pi is already configured to load @heart-of-gold/toolkit as a package from:",
|
|
48
|
+
locations,
|
|
49
|
+
"",
|
|
50
|
+
"Choose one Pi install path:",
|
|
51
|
+
" 1. keep the Pi package: pi install npm:@heart-of-gold/toolkit",
|
|
52
|
+
" 2. keep native skill copy: bunx @heart-of-gold/toolkit install --to pi",
|
|
53
|
+
"",
|
|
54
|
+
"Remove one side before continuing, or rerun with --force if you really want both.",
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
if (!args.force) {
|
|
58
|
+
console.error(message);
|
|
59
|
+
process.exit(1);
|
|
41
60
|
}
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
|
|
62
|
+
console.warn(`${message}\n`);
|
|
44
63
|
}
|
|
45
64
|
}
|
|
46
65
|
const target = targets[targetName];
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { shareServerCommand } from "./commands/share-server";
|
|
|
8
8
|
const main = defineCommand({
|
|
9
9
|
meta: {
|
|
10
10
|
name: "heart-of-gold",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.43",
|
|
12
12
|
description:
|
|
13
13
|
"Cross-platform installer for Heart of Gold skills — Codex, OpenCode, Pi, Claude Code, and more",
|
|
14
14
|
},
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
const HEART_OF_GOLD_PACKAGE_NAME = "@heart-of-gold/toolkit";
|
|
6
|
+
const HEART_OF_GOLD_REPO_HINT = /(^|[/:])heart-of-gold-toolkit(?:$|[@/.#?])/i;
|
|
7
|
+
|
|
8
|
+
function readJson(path) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isLocalSource(source) {
|
|
17
|
+
return source.startsWith(".") || source.startsWith("/");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function localSourceIsHeartOfGold(source, settingsDir) {
|
|
21
|
+
const resolved = resolve(settingsDir, source);
|
|
22
|
+
const packageJsonPath = existsSync(resolved) && resolved.endsWith("package.json")
|
|
23
|
+
? resolved
|
|
24
|
+
: join(resolved, "package.json");
|
|
25
|
+
|
|
26
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
27
|
+
|
|
28
|
+
const manifest = readJson(packageJsonPath);
|
|
29
|
+
return manifest?.name === HEART_OF_GOLD_PACKAGE_NAME;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sourceMatchesHeartOfGold(source, settingsDir) {
|
|
33
|
+
if (typeof source !== "string" || source.length === 0) return false;
|
|
34
|
+
|
|
35
|
+
if (source === HEART_OF_GOLD_PACKAGE_NAME) return true;
|
|
36
|
+
if (source.startsWith(`npm:${HEART_OF_GOLD_PACKAGE_NAME}`)) return true;
|
|
37
|
+
if (isLocalSource(source) && localSourceIsHeartOfGold(source, settingsDir)) return true;
|
|
38
|
+
if ((source.startsWith("git:") || source.startsWith("http://") || source.startsWith("https://") || source.startsWith("ssh://")) && HEART_OF_GOLD_REPO_HINT.test(source)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function collectMatchingPackageSources(settingsPath) {
|
|
46
|
+
if (!existsSync(settingsPath)) return [];
|
|
47
|
+
|
|
48
|
+
const settings = readJson(settingsPath);
|
|
49
|
+
const packages = Array.isArray(settings?.packages) ? settings.packages : [];
|
|
50
|
+
const settingsDir = dirname(settingsPath);
|
|
51
|
+
const matches = [];
|
|
52
|
+
|
|
53
|
+
for (const entry of packages) {
|
|
54
|
+
const source = typeof entry === "string" ? entry : entry?.source;
|
|
55
|
+
if (sourceMatchesHeartOfGold(source, settingsDir)) {
|
|
56
|
+
matches.push({ settingsPath, source });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return matches;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function findNearestProjectSettings(startDir = process.cwd()) {
|
|
64
|
+
let current = resolve(startDir);
|
|
65
|
+
const root = parse(current).root;
|
|
66
|
+
|
|
67
|
+
while (true) {
|
|
68
|
+
const candidate = join(current, ".pi", "settings.json");
|
|
69
|
+
if (existsSync(candidate)) return candidate;
|
|
70
|
+
if (current === root) return null;
|
|
71
|
+
current = dirname(current);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findHeartOfGoldPiPackageInstalls(startDir = process.cwd()) {
|
|
76
|
+
const globalSettings = join(homedir(), ".pi", "agent", "settings.json");
|
|
77
|
+
const projectSettings = findNearestProjectSettings(startDir);
|
|
78
|
+
const settingsPaths = [globalSettings, projectSettings].filter(Boolean);
|
|
79
|
+
|
|
80
|
+
return settingsPaths.flatMap((settingsPath) => collectMatchingPackageSources(settingsPath));
|
|
81
|
+
}
|