@jgordijn/opencode-remote-config 0.1.0 → 0.3.0
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 +89 -26
- package/dist/index.js +461 -196
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# opencode-remote-config
|
|
2
2
|
|
|
3
|
-
An OpenCode plugin that syncs skills and
|
|
3
|
+
An OpenCode plugin that syncs skills, agents, commands, and instructions from Git repositories, making them available to OpenCode without polluting your local configuration.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -8,8 +8,10 @@ An OpenCode plugin that syncs skills and agents from Git repositories, making th
|
|
|
8
8
|
- **Local directories**: Use `file://` URLs for local directories (great for development)
|
|
9
9
|
- **Skills import**: Import skill definitions from `skill/` directory
|
|
10
10
|
- **Agents import**: Import agent definitions from `agent/` directory
|
|
11
|
+
- **Commands import**: Import slash commands from `command/` directory
|
|
11
12
|
- **Plugins import**: Import OpenCode hook plugins from `plugin/` directory
|
|
12
|
-
- **
|
|
13
|
+
- **Instructions import**: Import AGENTS.md instructions via manifest.json
|
|
14
|
+
- **Selective import**: Use `include` or `exclude` filters for fine-grained control
|
|
13
15
|
- **Ref pinning**: Pin to branch, tag, or commit SHA
|
|
14
16
|
- **Priority handling**: User config > first repository > subsequent repositories
|
|
15
17
|
- **Conflict handling**: Local definitions take precedence, warns on conflicts
|
|
@@ -71,9 +73,9 @@ bun install && bun run build
|
|
|
71
73
|
{
|
|
72
74
|
"url": "git@github.com:company/shared-skills.git",
|
|
73
75
|
"ref": "main",
|
|
74
|
-
"skills": ["code-review", "kotlin-pro"],
|
|
75
|
-
"agents": ["code-reviewer", "specialized/db-expert"],
|
|
76
|
-
"plugins": ["notify", "utils-logger"]
|
|
76
|
+
"skills": { "include": ["code-review", "kotlin-pro"] },
|
|
77
|
+
"agents": { "include": ["code-reviewer", "specialized/db-expert"] },
|
|
78
|
+
"plugins": { "include": ["notify", "utils-logger"] }
|
|
77
79
|
},
|
|
78
80
|
{
|
|
79
81
|
"url": "git@github.com:team/team-skills.git",
|
|
@@ -83,7 +85,7 @@ bun install && bun run build
|
|
|
83
85
|
}
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
**Restart OpenCode** to load the plugin.
|
|
87
89
|
|
|
88
90
|
### Configuration
|
|
89
91
|
|
|
@@ -98,12 +100,59 @@ The plugin reads its configuration from a separate JSON file (not `opencode.json
|
|
|
98
100
|
|
|
99
101
|
| Option | Type | Default | Description |
|
|
100
102
|
|--------|------|---------|-------------|
|
|
103
|
+
| `installMethod` | `"link"` or `"copy"` | `"link"` | How to install skills/plugins (symlinks or copies) |
|
|
101
104
|
| `repositories` | Array | `[]` | List of repositories to sync |
|
|
102
105
|
| `repositories[].url` | String | Required | Git URL, HTTPS URL, or `file://` path |
|
|
103
106
|
| `repositories[].ref` | String | Default branch | Branch, tag, or commit SHA (git only) |
|
|
104
|
-
| `repositories[].skills` |
|
|
105
|
-
| `repositories[].agents` |
|
|
106
|
-
| `repositories[].
|
|
107
|
+
| `repositories[].skills` | `"*"` or `{ include: [...] }` or `{ exclude: [...] }` | All skills | Skills to import |
|
|
108
|
+
| `repositories[].agents` | `"*"` or `{ include: [...] }` or `{ exclude: [...] }` | All agents | Agents to import |
|
|
109
|
+
| `repositories[].commands` | `"*"` or `{ include: [...] }` or `{ exclude: [...] }` | All commands | Slash commands to import |
|
|
110
|
+
| `repositories[].plugins` | `"*"` or `{ include: [...] }` or `{ exclude: [...] }` | All plugins | Plugins to import |
|
|
111
|
+
| `repositories[].instructions` | `"*"` or `{ include: [...] }` or `{ exclude: [...] }` | All instructions | Instructions from manifest to import |
|
|
112
|
+
|
|
113
|
+
### Installation Method
|
|
114
|
+
|
|
115
|
+
By default, skills and plugins are installed using symlinks. This is fast and efficient but may not work in all environments.
|
|
116
|
+
|
|
117
|
+
#### Configuration
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"installMethod": "link"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Value | Description |
|
|
126
|
+
|-------|-------------|
|
|
127
|
+
| `link` | Create symlinks (default). Fast, but may not work in containers or on Windows. |
|
|
128
|
+
| `copy` | Copy files. Works everywhere. Uses rsync if available for efficiency. |
|
|
129
|
+
|
|
130
|
+
#### When to Use Copy Mode
|
|
131
|
+
|
|
132
|
+
Use `"installMethod": "copy"` when:
|
|
133
|
+
|
|
134
|
+
- **Dev Containers**: Symlinks break because paths differ between host and container
|
|
135
|
+
- **Windows**: Symlinks require admin privileges or developer mode
|
|
136
|
+
- **Network filesystems**: Some NFS/CIFS mounts don't support symlinks properly
|
|
137
|
+
|
|
138
|
+
#### Example
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"$schema": "https://raw.githubusercontent.com/jgordijn/opencode-remote-config/main/remote-skills.schema.json",
|
|
143
|
+
"installMethod": "copy",
|
|
144
|
+
"repositories": [
|
|
145
|
+
{
|
|
146
|
+
"url": "git@github.com:your-org/shared-skills.git"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
When using copy mode:
|
|
153
|
+
- rsync is preferred if available (efficient incremental updates)
|
|
154
|
+
- Falls back to Node.js file copy if rsync is unavailable
|
|
155
|
+
- Files are kept in sync with the source repository
|
|
107
156
|
|
|
108
157
|
### Local Directories
|
|
109
158
|
|
|
@@ -129,7 +178,7 @@ For development or local skill repositories, use `file://` URLs:
|
|
|
129
178
|
Skills are cloned to a cache directory and symlinked into the OpenCode skill directory:
|
|
130
179
|
|
|
131
180
|
```
|
|
132
|
-
~/.cache/opencode/remote-
|
|
181
|
+
~/.cache/opencode/remote-config/repos/
|
|
133
182
|
└── github.com-company-shared-skills/
|
|
134
183
|
└── <full git clone>
|
|
135
184
|
|
|
@@ -200,20 +249,20 @@ Nested paths are converted to dashes: `plugin/foo/bar/baz.ts` becomes `foo-bar-b
|
|
|
200
249
|
|
|
201
250
|
**Plugin changes require restart:** Unlike skills, plugins are loaded at OpenCode startup. When plugin changes are detected, you'll see:
|
|
202
251
|
```
|
|
203
|
-
[remote-
|
|
252
|
+
[remote-config] Plugin changes detected. Restart OpenCode to apply.
|
|
204
253
|
```
|
|
205
254
|
|
|
206
255
|
### Example Output
|
|
207
256
|
|
|
208
257
|
```
|
|
209
|
-
[remote-
|
|
210
|
-
[remote-
|
|
211
|
-
[remote-
|
|
212
|
-
[remote-
|
|
213
|
-
[remote-
|
|
214
|
-
[remote-
|
|
215
|
-
[remote-
|
|
216
|
-
[remote-
|
|
258
|
+
[remote-config] Syncing 2 repositories...
|
|
259
|
+
[remote-config] ✓ company/shared-skills (main) - 5 skills
|
|
260
|
+
[remote-config] ✓ team/team-skills (v1.2.0) - 3 skills
|
|
261
|
+
[remote-config] ⚠ Conflict: 'git-commit' exists locally, skipping
|
|
262
|
+
[remote-config] Discovered 4 remote agents
|
|
263
|
+
[remote-config] Discovered 2 remote plugins
|
|
264
|
+
[remote-config] 7 skills, 2 plugins available
|
|
265
|
+
[remote-config] Injected 4 remote agents into config
|
|
217
266
|
```
|
|
218
267
|
|
|
219
268
|
### Authentication
|
|
@@ -305,7 +354,7 @@ This section contains instructions for LLM agents helping users set up the openc
|
|
|
305
354
|
**Problem: Updates not reflected**
|
|
306
355
|
- In background mode, restart OpenCode to apply updates
|
|
307
356
|
- Check that the `ref` (branch/tag) is correct
|
|
308
|
-
- Try removing the cached repo: `rm -rf ~/.cache/opencode/remote-
|
|
357
|
+
- Try removing the cached repo: `rm -rf ~/.cache/opencode/remote-config/repos/<repo-id>`
|
|
309
358
|
|
|
310
359
|
### Repository Structure Requirements
|
|
311
360
|
|
|
@@ -412,8 +461,8 @@ import * as fs from "fs"
|
|
|
412
461
|
{
|
|
413
462
|
"url": "git@github.com:company/shared-skills.git",
|
|
414
463
|
"ref": "main",
|
|
415
|
-
"skills": ["code-review", "testing"],
|
|
416
|
-
"agents": ["code-reviewer"]
|
|
464
|
+
"skills": { "include": ["code-review", "testing"] },
|
|
465
|
+
"agents": { "include": ["code-reviewer"] }
|
|
417
466
|
},
|
|
418
467
|
{
|
|
419
468
|
"url": "git@github.com:team/team-skills.git",
|
|
@@ -424,13 +473,27 @@ import * as fs from "fs"
|
|
|
424
473
|
}
|
|
425
474
|
```
|
|
426
475
|
|
|
427
|
-
**Skills only (
|
|
476
|
+
**Skills only (exclude all agents):**
|
|
477
|
+
```jsonc
|
|
478
|
+
{
|
|
479
|
+
"repositories": [
|
|
480
|
+
{
|
|
481
|
+
"url": "git@github.com:company/skills.git",
|
|
482
|
+
"agents": { "include": ["__none__"] } // Include a non-existent name to import no agents
|
|
483
|
+
}
|
|
484
|
+
]
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Note: To effectively import nothing, include a name that doesn't exist in the repository. The include/exclude arrays require at least one item.
|
|
489
|
+
|
|
490
|
+
**Exclude specific items:**
|
|
428
491
|
```jsonc
|
|
429
492
|
{
|
|
430
493
|
"repositories": [
|
|
431
494
|
{
|
|
432
495
|
"url": "git@github.com:company/skills.git",
|
|
433
|
-
"
|
|
496
|
+
"skills": { "exclude": ["deprecated-skill", "experimental"] }
|
|
434
497
|
}
|
|
435
498
|
]
|
|
436
499
|
}
|
|
@@ -443,8 +506,8 @@ import * as fs from "fs"
|
|
|
443
506
|
{
|
|
444
507
|
"url": "git@github.com:company/shared-skills.git",
|
|
445
508
|
"skills": "*",
|
|
446
|
-
"agents": ["code-reviewer"],
|
|
447
|
-
"plugins": ["notify", "analytics"]
|
|
509
|
+
"agents": { "include": ["code-reviewer"] },
|
|
510
|
+
"plugins": { "include": ["notify", "analytics"] }
|
|
448
511
|
}
|
|
449
512
|
]
|
|
450
513
|
}
|
package/dist/index.js
CHANGED
|
@@ -7475,8 +7475,49 @@ var coerce = {
|
|
|
7475
7475
|
var NEVER = INVALID;
|
|
7476
7476
|
// src/config.ts
|
|
7477
7477
|
import { existsSync, readFileSync } from "fs";
|
|
7478
|
+
import { join as join2 } from "path";
|
|
7479
|
+
import { homedir as homedir2 } from "os";
|
|
7480
|
+
|
|
7481
|
+
// src/logging.ts
|
|
7482
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
7478
7483
|
import { join } from "path";
|
|
7479
7484
|
import { homedir } from "os";
|
|
7485
|
+
var DEFAULT_LOG_DIR = join(homedir(), ".cache", "opencode", "remote-config");
|
|
7486
|
+
var currentLogDir = DEFAULT_LOG_DIR;
|
|
7487
|
+
var dirEnsured = false;
|
|
7488
|
+
var LOG_PREFIX = "[remote-config]";
|
|
7489
|
+
function getLogFile() {
|
|
7490
|
+
return join(currentLogDir, "plugin.log");
|
|
7491
|
+
}
|
|
7492
|
+
function timestamp() {
|
|
7493
|
+
return new Date().toISOString();
|
|
7494
|
+
}
|
|
7495
|
+
function writeLog(level, message) {
|
|
7496
|
+
try {
|
|
7497
|
+
if (!dirEnsured) {
|
|
7498
|
+
mkdirSync(currentLogDir, { recursive: true });
|
|
7499
|
+
dirEnsured = true;
|
|
7500
|
+
}
|
|
7501
|
+
appendFileSync(getLogFile(), `${timestamp()} [${level}] ${LOG_PREFIX} ${message}
|
|
7502
|
+
`);
|
|
7503
|
+
} catch {
|
|
7504
|
+
dirEnsured = false;
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
function log(message) {
|
|
7508
|
+
writeLog("INFO", message);
|
|
7509
|
+
}
|
|
7510
|
+
function logError(message) {
|
|
7511
|
+
writeLog("ERROR", message);
|
|
7512
|
+
}
|
|
7513
|
+
function logDebug(message) {
|
|
7514
|
+
writeLog("DEBUG", message);
|
|
7515
|
+
}
|
|
7516
|
+
function logWarn(message) {
|
|
7517
|
+
writeLog("WARN", message);
|
|
7518
|
+
}
|
|
7519
|
+
|
|
7520
|
+
// src/config.ts
|
|
7480
7521
|
var CONFIG_FILENAME = "remote-config.json";
|
|
7481
7522
|
var FilterConfigSchema = exports_external.union([
|
|
7482
7523
|
exports_external.object({
|
|
@@ -7496,21 +7537,36 @@ var RepositoryConfigSchema = exports_external.object({
|
|
|
7496
7537
|
skills: ImportConfigSchema.optional(),
|
|
7497
7538
|
agents: ImportConfigSchema.optional(),
|
|
7498
7539
|
commands: ImportConfigSchema.optional(),
|
|
7499
|
-
plugins: ImportConfigSchema.optional()
|
|
7540
|
+
plugins: ImportConfigSchema.optional(),
|
|
7541
|
+
instructions: ImportConfigSchema.optional()
|
|
7500
7542
|
}).strict();
|
|
7543
|
+
function shouldImport(name, config) {
|
|
7544
|
+
if (config === undefined || config === "*") {
|
|
7545
|
+
return true;
|
|
7546
|
+
}
|
|
7547
|
+
if ("include" in config) {
|
|
7548
|
+
return config.include.includes(name);
|
|
7549
|
+
}
|
|
7550
|
+
if ("exclude" in config) {
|
|
7551
|
+
return !config.exclude.includes(name);
|
|
7552
|
+
}
|
|
7553
|
+
return true;
|
|
7554
|
+
}
|
|
7501
7555
|
var RemoteSkillsConfigSchema = exports_external.object({
|
|
7502
7556
|
$schema: exports_external.string().optional(),
|
|
7503
7557
|
repositories: exports_external.array(RepositoryConfigSchema).default([]),
|
|
7504
|
-
sync: exports_external.enum(["blocking", "background"]).default("blocking")
|
|
7558
|
+
sync: exports_external.enum(["blocking", "background"]).default("blocking"),
|
|
7559
|
+
installMethod: exports_external.enum(["link", "copy"]).default("link")
|
|
7505
7560
|
}).strict();
|
|
7506
7561
|
var DEFAULT_CONFIG = {
|
|
7507
7562
|
repositories: [],
|
|
7508
|
-
sync: "blocking"
|
|
7563
|
+
sync: "blocking",
|
|
7564
|
+
installMethod: "link"
|
|
7509
7565
|
};
|
|
7510
7566
|
function getConfigPaths() {
|
|
7511
7567
|
return [
|
|
7512
|
-
|
|
7513
|
-
|
|
7568
|
+
join2(process.cwd(), ".opencode", CONFIG_FILENAME),
|
|
7569
|
+
join2(homedir2(), ".config", "opencode", CONFIG_FILENAME)
|
|
7514
7570
|
];
|
|
7515
7571
|
}
|
|
7516
7572
|
function loadConfig() {
|
|
@@ -7525,13 +7581,13 @@ function loadConfigWithLocation() {
|
|
|
7525
7581
|
const parsed = JSON.parse(content);
|
|
7526
7582
|
const result = RemoteSkillsConfigSchema.safeParse(parsed);
|
|
7527
7583
|
if (!result.success) {
|
|
7528
|
-
|
|
7584
|
+
logError(`Invalid configuration in ${configPath}: ${JSON.stringify(result.error.format())}`);
|
|
7529
7585
|
continue;
|
|
7530
7586
|
}
|
|
7531
|
-
const configDir =
|
|
7587
|
+
const configDir = join2(configPath, "..");
|
|
7532
7588
|
return { config: result.data, configDir };
|
|
7533
7589
|
} catch (error) {
|
|
7534
|
-
|
|
7590
|
+
logError(`Error reading ${configPath}: ${error}`);
|
|
7535
7591
|
continue;
|
|
7536
7592
|
}
|
|
7537
7593
|
}
|
|
@@ -7596,8 +7652,95 @@ var CommandConfigSchema = exports_external.object({
|
|
|
7596
7652
|
subtask: exports_external.boolean().optional()
|
|
7597
7653
|
}).passthrough();
|
|
7598
7654
|
|
|
7655
|
+
// src/instruction.ts
|
|
7656
|
+
import { existsSync as existsSync3 } from "fs";
|
|
7657
|
+
import { join as join4, normalize, resolve, sep } from "path";
|
|
7658
|
+
|
|
7659
|
+
// src/manifest.ts
|
|
7660
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
7661
|
+
import { join as join3, isAbsolute } from "path";
|
|
7662
|
+
var MANIFEST_FILENAME = "manifest.json";
|
|
7663
|
+
function containsPathTraversal(path) {
|
|
7664
|
+
return path.split("/").some((segment) => segment === ".." || segment === ".");
|
|
7665
|
+
}
|
|
7666
|
+
var instructionPathSchema = exports_external.string().transform((path) => path.trim()).transform((path) => path.startsWith("./") ? path.slice(2) : path).refine((path) => path.length > 0, { message: "Instruction path must not be empty" }).refine((path) => path.endsWith(".md"), { message: "Instruction path must end with .md" }).refine((path) => {
|
|
7667
|
+
const filename = path.includes("/") ? path.split("/").pop() : path;
|
|
7668
|
+
return filename.length > 3;
|
|
7669
|
+
}, { message: "Instruction path must have a filename before .md extension" }).refine((path) => !isAbsolute(path), { message: "Instruction path must be relative (absolute paths not allowed)" }).refine((path) => !path.includes("\\"), { message: "Instruction path must use forward slashes (POSIX-style)" }).refine((path) => !containsPathTraversal(path), { message: "Instruction path must not contain path traversal (. or ..)" }).refine((path) => !path.includes("//"), { message: "Instruction path must not contain consecutive slashes" }).refine((path) => !path.endsWith("/"), { message: "Instruction path must not have a trailing slash" });
|
|
7670
|
+
var ManifestSchema = exports_external.object({
|
|
7671
|
+
$schema: exports_external.string().optional(),
|
|
7672
|
+
instructions: exports_external.array(instructionPathSchema).optional().default([])
|
|
7673
|
+
});
|
|
7674
|
+
function loadManifest(repoPath) {
|
|
7675
|
+
const manifestPath = join3(repoPath, MANIFEST_FILENAME);
|
|
7676
|
+
if (!existsSync2(manifestPath)) {
|
|
7677
|
+
return { status: "not-found" };
|
|
7678
|
+
}
|
|
7679
|
+
try {
|
|
7680
|
+
const content = readFileSync2(manifestPath, "utf-8");
|
|
7681
|
+
const parsed = JSON.parse(content);
|
|
7682
|
+
const result = ManifestSchema.safeParse(parsed);
|
|
7683
|
+
if (!result.success) {
|
|
7684
|
+
const errorMessage = JSON.stringify(result.error.format());
|
|
7685
|
+
console.warn(`[remote-config] Invalid manifest.json in ${repoPath}:`, result.error.format());
|
|
7686
|
+
return { status: "invalid", error: errorMessage };
|
|
7687
|
+
}
|
|
7688
|
+
return { status: "found", manifest: result.data };
|
|
7689
|
+
} catch (error) {
|
|
7690
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
7691
|
+
console.warn(`[remote-config] Error reading manifest.json in ${repoPath}:`, error);
|
|
7692
|
+
return { status: "invalid", error: errorMessage };
|
|
7693
|
+
}
|
|
7694
|
+
}
|
|
7695
|
+
|
|
7696
|
+
// src/instruction.ts
|
|
7697
|
+
var INSTRUCTION_LIMITS = {
|
|
7698
|
+
maxInstructions: 100,
|
|
7699
|
+
maxPathLength: 500
|
|
7700
|
+
};
|
|
7701
|
+
function discoverInstructions(repoPath) {
|
|
7702
|
+
const result = loadManifest(repoPath);
|
|
7703
|
+
if (result.status === "not-found") {
|
|
7704
|
+
return [];
|
|
7705
|
+
}
|
|
7706
|
+
if (result.status === "invalid") {
|
|
7707
|
+
console.warn(`[remote-config] Skipping instructions for ${repoPath}: invalid manifest.json`);
|
|
7708
|
+
return [];
|
|
7709
|
+
}
|
|
7710
|
+
const manifest = result.manifest;
|
|
7711
|
+
const instructions = [];
|
|
7712
|
+
const instructionsToProcess = manifest.instructions.slice(0, INSTRUCTION_LIMITS.maxInstructions);
|
|
7713
|
+
if (manifest.instructions.length > INSTRUCTION_LIMITS.maxInstructions) {
|
|
7714
|
+
console.warn(`[remote-config] Limiting instructions to ${INSTRUCTION_LIMITS.maxInstructions} (manifest has ${manifest.instructions.length})`);
|
|
7715
|
+
}
|
|
7716
|
+
for (const instructionName of instructionsToProcess) {
|
|
7717
|
+
if (instructionName.length > INSTRUCTION_LIMITS.maxPathLength) {
|
|
7718
|
+
console.warn(`[remote-config] Skipping instruction with path exceeding ${INSTRUCTION_LIMITS.maxPathLength} chars`);
|
|
7719
|
+
continue;
|
|
7720
|
+
}
|
|
7721
|
+
if (containsPathTraversal(instructionName)) {
|
|
7722
|
+
console.warn(`[remote-config] Skipping instruction with path traversal: ${instructionName}`);
|
|
7723
|
+
continue;
|
|
7724
|
+
}
|
|
7725
|
+
const absolutePath = join4(repoPath, instructionName);
|
|
7726
|
+
const resolvedPath = normalize(resolve(absolutePath));
|
|
7727
|
+
const resolvedRepoPath = normalize(resolve(repoPath));
|
|
7728
|
+
if (!resolvedPath.startsWith(resolvedRepoPath + sep)) {
|
|
7729
|
+
console.warn(`[remote-config] Skipping instruction outside repository: ${instructionName}`);
|
|
7730
|
+
continue;
|
|
7731
|
+
}
|
|
7732
|
+
if (existsSync3(absolutePath)) {
|
|
7733
|
+
instructions.push({
|
|
7734
|
+
name: instructionName,
|
|
7735
|
+
path: absolutePath
|
|
7736
|
+
});
|
|
7737
|
+
}
|
|
7738
|
+
}
|
|
7739
|
+
return instructions;
|
|
7740
|
+
}
|
|
7741
|
+
|
|
7599
7742
|
// src/git.ts
|
|
7600
|
-
var CACHE_BASE = path.join(process.env.HOME || "~", ".cache", "opencode", "remote-
|
|
7743
|
+
var CACHE_BASE = path.join(process.env.HOME || "~", ".cache", "opencode", "remote-config", "repos");
|
|
7601
7744
|
function isFileUrl(url) {
|
|
7602
7745
|
return url.startsWith("file://");
|
|
7603
7746
|
}
|
|
@@ -7617,7 +7760,10 @@ async function cloneRepo(url, repoPath) {
|
|
|
7617
7760
|
const result = await $`git clone ${url} ${repoPath}`.quiet();
|
|
7618
7761
|
if (result.exitCode !== 0) {
|
|
7619
7762
|
const stderr = result.stderr.toString().trim();
|
|
7620
|
-
|
|
7763
|
+
const stdout = result.stdout.toString().trim();
|
|
7764
|
+
const output = [stderr, stdout].filter(Boolean).join(`
|
|
7765
|
+
`) || `exit code ${result.exitCode}`;
|
|
7766
|
+
throw new Error(`git clone failed: ${output}`);
|
|
7621
7767
|
}
|
|
7622
7768
|
}
|
|
7623
7769
|
async function fetchAndCheckout(repoPath, ref) {
|
|
@@ -7626,20 +7772,29 @@ async function fetchAndCheckout(repoPath, ref) {
|
|
|
7626
7772
|
const fetchResult = await $`git -C ${repoPath} fetch --all --prune`.quiet();
|
|
7627
7773
|
if (fetchResult.exitCode !== 0) {
|
|
7628
7774
|
const stderr = fetchResult.stderr.toString().trim();
|
|
7629
|
-
|
|
7775
|
+
const stdout = fetchResult.stdout.toString().trim();
|
|
7776
|
+
const output = [stderr, stdout].filter(Boolean).join(`
|
|
7777
|
+
`) || `exit code ${fetchResult.exitCode}`;
|
|
7778
|
+
throw new Error(`git fetch failed: ${output}`);
|
|
7630
7779
|
}
|
|
7631
7780
|
if (ref) {
|
|
7632
7781
|
const checkoutResult = await $`git -C ${repoPath} checkout ${ref}`.quiet();
|
|
7633
7782
|
if (checkoutResult.exitCode !== 0) {
|
|
7634
7783
|
const stderr = checkoutResult.stderr.toString().trim();
|
|
7635
|
-
|
|
7784
|
+
const stdout = checkoutResult.stdout.toString().trim();
|
|
7785
|
+
const output = [stderr, stdout].filter(Boolean).join(`
|
|
7786
|
+
`) || `exit code ${checkoutResult.exitCode}`;
|
|
7787
|
+
throw new Error(`git checkout ${ref} failed: ${output}`);
|
|
7636
7788
|
}
|
|
7637
7789
|
const isBranch = await $`git -C ${repoPath} symbolic-ref -q HEAD`.quiet();
|
|
7638
7790
|
if (isBranch.exitCode === 0) {
|
|
7639
7791
|
const pullResult = await $`git -C ${repoPath} pull --ff-only`.quiet();
|
|
7640
7792
|
if (pullResult.exitCode !== 0) {
|
|
7641
7793
|
const stderr = pullResult.stderr.toString().trim();
|
|
7642
|
-
|
|
7794
|
+
const stdout = pullResult.stdout.toString().trim();
|
|
7795
|
+
const output = [stderr, stdout].filter(Boolean).join(`
|
|
7796
|
+
`) || `exit code ${pullResult.exitCode}`;
|
|
7797
|
+
throw new Error(`git pull failed: ${output}`);
|
|
7643
7798
|
}
|
|
7644
7799
|
}
|
|
7645
7800
|
} else {
|
|
@@ -7723,14 +7878,14 @@ async function discoverAgents(repoPath) {
|
|
|
7723
7878
|
const findAgents = (dir, depth) => {
|
|
7724
7879
|
if (depth > DISCOVERY_LIMITS.maxDepth) {
|
|
7725
7880
|
if (!limitsWarned) {
|
|
7726
|
-
|
|
7881
|
+
logWarn(`Skipping deep directories (max depth: ${DISCOVERY_LIMITS.maxDepth})`);
|
|
7727
7882
|
limitsWarned = true;
|
|
7728
7883
|
}
|
|
7729
7884
|
return;
|
|
7730
7885
|
}
|
|
7731
7886
|
if (filesProcessed >= DISCOVERY_LIMITS.maxFiles) {
|
|
7732
7887
|
if (!limitsWarned) {
|
|
7733
|
-
|
|
7888
|
+
logWarn(`Stopping discovery (max files: ${DISCOVERY_LIMITS.maxFiles})`);
|
|
7734
7889
|
limitsWarned = true;
|
|
7735
7890
|
}
|
|
7736
7891
|
return;
|
|
@@ -7748,7 +7903,7 @@ async function discoverAgents(repoPath) {
|
|
|
7748
7903
|
try {
|
|
7749
7904
|
const stats = fs.statSync(fullPath);
|
|
7750
7905
|
if (stats.size > DISCOVERY_LIMITS.maxFileSize) {
|
|
7751
|
-
|
|
7906
|
+
logWarn(`Skipping large file (${Math.round(stats.size / 1024)}KB): ${entry.name}`);
|
|
7752
7907
|
continue;
|
|
7753
7908
|
}
|
|
7754
7909
|
filesProcessed++;
|
|
@@ -7758,7 +7913,7 @@ async function discoverAgents(repoPath) {
|
|
|
7758
7913
|
agents.push(parsed);
|
|
7759
7914
|
}
|
|
7760
7915
|
} catch (err) {
|
|
7761
|
-
|
|
7916
|
+
logError(`Failed to parse agent ${fullPath}: ${err}`);
|
|
7762
7917
|
}
|
|
7763
7918
|
}
|
|
7764
7919
|
}
|
|
@@ -7784,7 +7939,7 @@ function parseAgentMarkdown(filePath, content, agentDir) {
|
|
|
7784
7939
|
});
|
|
7785
7940
|
} catch (err) {
|
|
7786
7941
|
const relativeToRepo = path.relative(path.dirname(agentDir), filePath);
|
|
7787
|
-
|
|
7942
|
+
logError(`Failed to parse frontmatter in ${relativeToRepo}: ${err}`);
|
|
7788
7943
|
return null;
|
|
7789
7944
|
}
|
|
7790
7945
|
if (!md.data || Object.keys(md.data).length === 0) {
|
|
@@ -7794,7 +7949,7 @@ function parseAgentMarkdown(filePath, content, agentDir) {
|
|
|
7794
7949
|
const agentName = relativePath.replace(/\.md$/i, "");
|
|
7795
7950
|
if (!/^[a-zA-Z0-9_/-]+$/.test(agentName)) {
|
|
7796
7951
|
const relativeToRepo = path.relative(path.dirname(agentDir), filePath);
|
|
7797
|
-
|
|
7952
|
+
logWarn(`Skipping agent with invalid name characters: ${relativeToRepo}`);
|
|
7798
7953
|
return null;
|
|
7799
7954
|
}
|
|
7800
7955
|
const rawConfig = {
|
|
@@ -7803,7 +7958,7 @@ function parseAgentMarkdown(filePath, content, agentDir) {
|
|
|
7803
7958
|
};
|
|
7804
7959
|
const result = AgentConfigSchema.safeParse(rawConfig);
|
|
7805
7960
|
if (!result.success) {
|
|
7806
|
-
|
|
7961
|
+
logError(`Invalid agent config in ${filePath}: ${JSON.stringify(result.error.format())}`);
|
|
7807
7962
|
return null;
|
|
7808
7963
|
}
|
|
7809
7964
|
return {
|
|
@@ -7826,14 +7981,14 @@ async function discoverCommands(repoPath) {
|
|
|
7826
7981
|
const findCommands = (dir, depth) => {
|
|
7827
7982
|
if (depth > DISCOVERY_LIMITS.maxDepth) {
|
|
7828
7983
|
if (!limitsWarned) {
|
|
7829
|
-
|
|
7984
|
+
logWarn(`Skipping deep directories (max depth: ${DISCOVERY_LIMITS.maxDepth})`);
|
|
7830
7985
|
limitsWarned = true;
|
|
7831
7986
|
}
|
|
7832
7987
|
return;
|
|
7833
7988
|
}
|
|
7834
7989
|
if (filesProcessed >= DISCOVERY_LIMITS.maxFiles) {
|
|
7835
7990
|
if (!limitsWarned) {
|
|
7836
|
-
|
|
7991
|
+
logWarn(`Stopping discovery (max files: ${DISCOVERY_LIMITS.maxFiles})`);
|
|
7837
7992
|
limitsWarned = true;
|
|
7838
7993
|
}
|
|
7839
7994
|
return;
|
|
@@ -7851,7 +8006,7 @@ async function discoverCommands(repoPath) {
|
|
|
7851
8006
|
try {
|
|
7852
8007
|
const stats = fs.statSync(fullPath);
|
|
7853
8008
|
if (stats.size > DISCOVERY_LIMITS.maxFileSize) {
|
|
7854
|
-
|
|
8009
|
+
logWarn(`Skipping large file (${Math.round(stats.size / 1024)}KB): ${entry.name}`);
|
|
7855
8010
|
continue;
|
|
7856
8011
|
}
|
|
7857
8012
|
filesProcessed++;
|
|
@@ -7861,7 +8016,7 @@ async function discoverCommands(repoPath) {
|
|
|
7861
8016
|
commands.push(parsed);
|
|
7862
8017
|
}
|
|
7863
8018
|
} catch (err) {
|
|
7864
|
-
|
|
8019
|
+
logError(`Failed to parse command ${fullPath}: ${err}`);
|
|
7865
8020
|
}
|
|
7866
8021
|
}
|
|
7867
8022
|
}
|
|
@@ -7887,14 +8042,14 @@ function parseCommandMarkdown(filePath, content, commandDir) {
|
|
|
7887
8042
|
});
|
|
7888
8043
|
} catch (err) {
|
|
7889
8044
|
const relativeToRepo = path.relative(path.dirname(commandDir), filePath);
|
|
7890
|
-
|
|
8045
|
+
logError(`Failed to parse frontmatter in ${relativeToRepo}: ${err}`);
|
|
7891
8046
|
return null;
|
|
7892
8047
|
}
|
|
7893
8048
|
const relativePath = path.relative(commandDir, filePath).replace(/\\/g, "/");
|
|
7894
8049
|
const commandName = relativePath.replace(/\.md$/i, "");
|
|
7895
8050
|
if (!/^[a-zA-Z0-9_/-]+$/.test(commandName)) {
|
|
7896
8051
|
const relativeToRepo = path.relative(path.dirname(commandDir), filePath);
|
|
7897
|
-
|
|
8052
|
+
logWarn(`Skipping command with invalid name characters: ${relativeToRepo}`);
|
|
7898
8053
|
return null;
|
|
7899
8054
|
}
|
|
7900
8055
|
const rawConfig = {
|
|
@@ -7903,7 +8058,7 @@ function parseCommandMarkdown(filePath, content, commandDir) {
|
|
|
7903
8058
|
};
|
|
7904
8059
|
const result = CommandConfigSchema.safeParse(rawConfig);
|
|
7905
8060
|
if (!result.success) {
|
|
7906
|
-
|
|
8061
|
+
logError(`Invalid command config in ${filePath}: ${JSON.stringify(result.error.format())}`);
|
|
7907
8062
|
return null;
|
|
7908
8063
|
}
|
|
7909
8064
|
return {
|
|
@@ -7926,14 +8081,14 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
7926
8081
|
const findPlugins = (dir, depth) => {
|
|
7927
8082
|
if (depth > DISCOVERY_LIMITS.maxDepth) {
|
|
7928
8083
|
if (!limitsWarned) {
|
|
7929
|
-
|
|
8084
|
+
logWarn(`Skipping deep directories (max depth: ${DISCOVERY_LIMITS.maxDepth})`);
|
|
7930
8085
|
limitsWarned = true;
|
|
7931
8086
|
}
|
|
7932
8087
|
return;
|
|
7933
8088
|
}
|
|
7934
8089
|
if (filesProcessed >= DISCOVERY_LIMITS.maxFiles) {
|
|
7935
8090
|
if (!limitsWarned) {
|
|
7936
|
-
|
|
8091
|
+
logWarn(`Stopping discovery (max files: ${DISCOVERY_LIMITS.maxFiles})`);
|
|
7937
8092
|
limitsWarned = true;
|
|
7938
8093
|
}
|
|
7939
8094
|
return;
|
|
@@ -7951,7 +8106,7 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
7951
8106
|
try {
|
|
7952
8107
|
const stats = fs.statSync(fullPath);
|
|
7953
8108
|
if (stats.size > DISCOVERY_LIMITS.maxFileSize) {
|
|
7954
|
-
|
|
8109
|
+
logWarn(`Skipping large file (${Math.round(stats.size / 1024)}KB): ${entry.name}`);
|
|
7955
8110
|
continue;
|
|
7956
8111
|
}
|
|
7957
8112
|
filesProcessed++;
|
|
@@ -7960,7 +8115,7 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
7960
8115
|
const pluginName = relativePath.slice(0, -ext.length).replace(/\//g, "-");
|
|
7961
8116
|
if (!/^[a-zA-Z0-9_-]+$/.test(pluginName)) {
|
|
7962
8117
|
const relativeToRepo = path.relative(path.dirname(pluginDir), fullPath);
|
|
7963
|
-
|
|
8118
|
+
logWarn(`Skipping plugin with invalid name characters: ${relativeToRepo}`);
|
|
7964
8119
|
continue;
|
|
7965
8120
|
}
|
|
7966
8121
|
plugins.push({
|
|
@@ -7970,7 +8125,7 @@ async function discoverPlugins(repoPath, repoShortName) {
|
|
|
7970
8125
|
extension: ext
|
|
7971
8126
|
});
|
|
7972
8127
|
} catch (err) {
|
|
7973
|
-
|
|
8128
|
+
logError(`Failed to process plugin ${fullPath}: ${err}`);
|
|
7974
8129
|
}
|
|
7975
8130
|
}
|
|
7976
8131
|
}
|
|
@@ -7993,31 +8148,22 @@ async function syncLocalDirectory(config) {
|
|
|
7993
8148
|
let agents = [];
|
|
7994
8149
|
let commands = [];
|
|
7995
8150
|
let plugins = [];
|
|
8151
|
+
let instructions = [];
|
|
7996
8152
|
if (!fs.existsSync(localPath)) {
|
|
7997
8153
|
error = `Local directory not found: ${localPath}`;
|
|
7998
8154
|
} else if (!fs.statSync(localPath).isDirectory()) {
|
|
7999
8155
|
error = `Not a directory: ${localPath}`;
|
|
8000
8156
|
} else {
|
|
8001
8157
|
skills = await discoverSkills(localPath);
|
|
8002
|
-
|
|
8003
|
-
const requestedSkills = new Set(config.skills);
|
|
8004
|
-
skills = skills.filter((s) => requestedSkills.has(s.name));
|
|
8005
|
-
}
|
|
8158
|
+
skills = skills.filter((s) => shouldImport(s.name, config.skills));
|
|
8006
8159
|
agents = await discoverAgents(localPath);
|
|
8007
|
-
|
|
8008
|
-
const requestedAgents = new Set(config.agents);
|
|
8009
|
-
agents = agents.filter((a) => requestedAgents.has(a.name));
|
|
8010
|
-
}
|
|
8160
|
+
agents = agents.filter((a) => shouldImport(a.name, config.agents));
|
|
8011
8161
|
commands = await discoverCommands(localPath);
|
|
8012
|
-
|
|
8013
|
-
const requestedCommands = new Set(config.commands);
|
|
8014
|
-
commands = commands.filter((c) => requestedCommands.has(c.name));
|
|
8015
|
-
}
|
|
8162
|
+
commands = commands.filter((c) => shouldImport(c.name, config.commands));
|
|
8016
8163
|
plugins = await discoverPlugins(localPath, shortName);
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
}
|
|
8164
|
+
plugins = plugins.filter((p) => shouldImport(p.name, config.plugins));
|
|
8165
|
+
instructions = discoverInstructions(localPath);
|
|
8166
|
+
instructions = instructions.filter((i) => shouldImport(i.name, config.instructions));
|
|
8021
8167
|
}
|
|
8022
8168
|
return {
|
|
8023
8169
|
repoId,
|
|
@@ -8028,6 +8174,7 @@ async function syncLocalDirectory(config) {
|
|
|
8028
8174
|
agents,
|
|
8029
8175
|
commands,
|
|
8030
8176
|
plugins,
|
|
8177
|
+
instructions,
|
|
8031
8178
|
updated: false,
|
|
8032
8179
|
error
|
|
8033
8180
|
};
|
|
@@ -8055,29 +8202,20 @@ async function syncGitRepository(config) {
|
|
|
8055
8202
|
let agents = [];
|
|
8056
8203
|
let commands = [];
|
|
8057
8204
|
let plugins = [];
|
|
8205
|
+
let instructions = [];
|
|
8058
8206
|
let currentRef = config.ref || "default";
|
|
8059
8207
|
if (isCloned(repoPath)) {
|
|
8060
8208
|
skills = await discoverSkills(repoPath);
|
|
8061
8209
|
currentRef = await getCurrentRef(repoPath);
|
|
8062
|
-
|
|
8063
|
-
const requestedSkills = new Set(config.skills);
|
|
8064
|
-
skills = skills.filter((s) => requestedSkills.has(s.name));
|
|
8065
|
-
}
|
|
8210
|
+
skills = skills.filter((s) => shouldImport(s.name, config.skills));
|
|
8066
8211
|
agents = await discoverAgents(repoPath);
|
|
8067
|
-
|
|
8068
|
-
const requestedAgents = new Set(config.agents);
|
|
8069
|
-
agents = agents.filter((a) => requestedAgents.has(a.name));
|
|
8070
|
-
}
|
|
8212
|
+
agents = agents.filter((a) => shouldImport(a.name, config.agents));
|
|
8071
8213
|
commands = await discoverCommands(repoPath);
|
|
8072
|
-
|
|
8073
|
-
const requestedCommands = new Set(config.commands);
|
|
8074
|
-
commands = commands.filter((c) => requestedCommands.has(c.name));
|
|
8075
|
-
}
|
|
8214
|
+
commands = commands.filter((c) => shouldImport(c.name, config.commands));
|
|
8076
8215
|
plugins = await discoverPlugins(repoPath, shortName);
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
}
|
|
8216
|
+
plugins = plugins.filter((p) => shouldImport(p.name, config.plugins));
|
|
8217
|
+
instructions = discoverInstructions(repoPath);
|
|
8218
|
+
instructions = instructions.filter((i) => shouldImport(i.name, config.instructions));
|
|
8081
8219
|
}
|
|
8082
8220
|
return {
|
|
8083
8221
|
repoId,
|
|
@@ -8088,6 +8226,7 @@ async function syncGitRepository(config) {
|
|
|
8088
8226
|
agents,
|
|
8089
8227
|
commands,
|
|
8090
8228
|
plugins,
|
|
8229
|
+
instructions,
|
|
8091
8230
|
updated,
|
|
8092
8231
|
error
|
|
8093
8232
|
};
|
|
@@ -8101,20 +8240,122 @@ async function syncRepositories(configs) {
|
|
|
8101
8240
|
return results;
|
|
8102
8241
|
}
|
|
8103
8242
|
|
|
8104
|
-
// src/
|
|
8105
|
-
import * as
|
|
8106
|
-
import * as
|
|
8243
|
+
// src/install.ts
|
|
8244
|
+
import * as path3 from "path";
|
|
8245
|
+
import * as fs3 from "fs";
|
|
8246
|
+
var {$: $3 } = globalThis.Bun;
|
|
8247
|
+
|
|
8248
|
+
// src/copy.ts
|
|
8107
8249
|
var {$: $2 } = globalThis.Bun;
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8250
|
+
import * as fs2 from "fs";
|
|
8251
|
+
import * as path2 from "path";
|
|
8252
|
+
var rsyncAvailable = null;
|
|
8253
|
+
async function detectRsync() {
|
|
8254
|
+
if (rsyncAvailable !== null) {
|
|
8255
|
+
return rsyncAvailable;
|
|
8256
|
+
}
|
|
8257
|
+
try {
|
|
8258
|
+
const result = await $2`which rsync`.quiet();
|
|
8259
|
+
rsyncAvailable = result.exitCode === 0;
|
|
8260
|
+
} catch {
|
|
8261
|
+
rsyncAvailable = false;
|
|
8262
|
+
}
|
|
8263
|
+
return rsyncAvailable;
|
|
8264
|
+
}
|
|
8265
|
+
async function copyWithRsync(source, target) {
|
|
8266
|
+
fs2.mkdirSync(path2.dirname(target), { recursive: true });
|
|
8267
|
+
const result = await $2`rsync -a --delete ${source}/ ${target}/`.quiet();
|
|
8268
|
+
if (result.exitCode !== 0) {
|
|
8269
|
+
throw new Error(`rsync failed: ${result.stderr.toString()}`);
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8272
|
+
function copyWithNodeFs(source, target) {
|
|
8273
|
+
if (fs2.existsSync(target)) {
|
|
8274
|
+
const stat = fs2.lstatSync(target);
|
|
8275
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
8276
|
+
fs2.unlinkSync(target);
|
|
8277
|
+
} else {
|
|
8278
|
+
fs2.rmSync(target, { recursive: true, force: true });
|
|
8279
|
+
}
|
|
8280
|
+
}
|
|
8281
|
+
fs2.mkdirSync(path2.dirname(target), { recursive: true });
|
|
8282
|
+
fs2.cpSync(source, target, { recursive: true });
|
|
8283
|
+
}
|
|
8284
|
+
async function syncDirectory(source, target) {
|
|
8285
|
+
let sourceStat;
|
|
8286
|
+
try {
|
|
8287
|
+
sourceStat = fs2.statSync(source);
|
|
8288
|
+
} catch {
|
|
8289
|
+
throw new Error(`Source does not exist: ${source}`);
|
|
8290
|
+
}
|
|
8291
|
+
if (!sourceStat.isDirectory()) {
|
|
8292
|
+
throw new Error(`Source is not a directory: ${source}`);
|
|
8293
|
+
}
|
|
8294
|
+
const resolvedSource = path2.resolve(source);
|
|
8295
|
+
const resolvedTarget = path2.resolve(target);
|
|
8296
|
+
if (resolvedTarget.startsWith(resolvedSource + path2.sep)) {
|
|
8297
|
+
throw new Error(`Target cannot be inside source: ${target} is inside ${source}`);
|
|
8298
|
+
}
|
|
8299
|
+
if (resolvedSource.startsWith(resolvedTarget + path2.sep)) {
|
|
8300
|
+
throw new Error(`Source cannot be inside target: ${source} is inside ${target}`);
|
|
8301
|
+
}
|
|
8302
|
+
const hasRsync = await detectRsync();
|
|
8303
|
+
if (hasRsync) {
|
|
8304
|
+
try {
|
|
8305
|
+
await copyWithRsync(source, target);
|
|
8306
|
+
logDebug(`Copied ${source} \u2192 ${target} using rsync`);
|
|
8307
|
+
return { method: "rsync" };
|
|
8308
|
+
} catch (err) {
|
|
8309
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
8310
|
+
logError(`rsync failed, falling back to fs: ${errorMessage}`);
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
try {
|
|
8314
|
+
copyWithNodeFs(source, target);
|
|
8315
|
+
logDebug(`Copied ${source} \u2192 ${target} using fs.cpSync`);
|
|
8316
|
+
return { method: "fs" };
|
|
8317
|
+
} catch (err) {
|
|
8318
|
+
try {
|
|
8319
|
+
if (fs2.existsSync(target)) {
|
|
8320
|
+
const stat = fs2.lstatSync(target);
|
|
8321
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
8322
|
+
fs2.unlinkSync(target);
|
|
8323
|
+
} else {
|
|
8324
|
+
fs2.rmSync(target, { recursive: true, force: true });
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
} catch {}
|
|
8328
|
+
throw err;
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
|
|
8332
|
+
// src/install.ts
|
|
8333
|
+
var SKILL_BASE = path3.join(process.env.HOME || "~", ".config", "opencode", "skill");
|
|
8334
|
+
var PLUGINS_DIR = path3.join(SKILL_BASE, "_plugins");
|
|
8335
|
+
function getInstallPath(repoShortName, skillName) {
|
|
8336
|
+
return path3.join(PLUGINS_DIR, repoShortName, skillName);
|
|
8112
8337
|
}
|
|
8113
8338
|
function ensurePluginsDir() {
|
|
8114
|
-
|
|
8339
|
+
fs3.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
8340
|
+
}
|
|
8341
|
+
function createSymlinkSync(sourcePath, targetPath) {
|
|
8342
|
+
if (fs3.existsSync(targetPath)) {
|
|
8343
|
+
const stats = fs3.lstatSync(targetPath);
|
|
8344
|
+
if (stats.isSymbolicLink()) {
|
|
8345
|
+
const existingTarget = fs3.readlinkSync(targetPath);
|
|
8346
|
+
if (existingTarget === sourcePath) {
|
|
8347
|
+
return { created: false };
|
|
8348
|
+
}
|
|
8349
|
+
fs3.unlinkSync(targetPath);
|
|
8350
|
+
} else {
|
|
8351
|
+
return { created: false, error: `Path exists and is not a symlink: ${targetPath}` };
|
|
8352
|
+
}
|
|
8353
|
+
}
|
|
8354
|
+
fs3.symlinkSync(sourcePath, targetPath, "dir");
|
|
8355
|
+
return { created: true };
|
|
8115
8356
|
}
|
|
8116
|
-
function
|
|
8117
|
-
const targetPath =
|
|
8357
|
+
async function createSkillInstall(skill, repoShortName, installMethod = "link") {
|
|
8358
|
+
const targetPath = getInstallPath(repoShortName, skill.name);
|
|
8118
8359
|
const result = {
|
|
8119
8360
|
skillName: skill.name,
|
|
8120
8361
|
sourcePath: skill.path,
|
|
@@ -8122,78 +8363,81 @@ function createSkillSymlink(skill, repoShortName) {
|
|
|
8122
8363
|
created: false
|
|
8123
8364
|
};
|
|
8124
8365
|
try {
|
|
8125
|
-
|
|
8126
|
-
if (
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
} else {
|
|
8135
|
-
result.error = `Path exists and is not a symlink: ${targetPath}`;
|
|
8136
|
-
return result;
|
|
8366
|
+
fs3.mkdirSync(path3.dirname(targetPath), { recursive: true });
|
|
8367
|
+
if (installMethod === "copy") {
|
|
8368
|
+
await syncDirectory(skill.path, targetPath);
|
|
8369
|
+
result.created = true;
|
|
8370
|
+
} else {
|
|
8371
|
+
const symlinkResult = createSymlinkSync(skill.path, targetPath);
|
|
8372
|
+
result.created = symlinkResult.created;
|
|
8373
|
+
if (symlinkResult.error) {
|
|
8374
|
+
result.error = symlinkResult.error;
|
|
8137
8375
|
}
|
|
8138
8376
|
}
|
|
8139
|
-
fs2.symlinkSync(skill.path, targetPath, "dir");
|
|
8140
|
-
result.created = true;
|
|
8141
8377
|
} catch (err) {
|
|
8142
8378
|
result.error = err instanceof Error ? err.message : String(err);
|
|
8143
8379
|
}
|
|
8144
8380
|
return result;
|
|
8145
8381
|
}
|
|
8146
|
-
function
|
|
8382
|
+
async function createInstallsForRepo(syncResult, installMethod = "link") {
|
|
8147
8383
|
ensurePluginsDir();
|
|
8148
8384
|
const results = [];
|
|
8149
8385
|
for (const skill of syncResult.skills) {
|
|
8150
|
-
const result =
|
|
8386
|
+
const result = await createSkillInstall(skill, syncResult.shortName, installMethod);
|
|
8151
8387
|
results.push(result);
|
|
8152
8388
|
}
|
|
8153
8389
|
return results;
|
|
8154
8390
|
}
|
|
8155
|
-
function
|
|
8156
|
-
const
|
|
8157
|
-
if (!
|
|
8158
|
-
return
|
|
8159
|
-
}
|
|
8160
|
-
const
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8391
|
+
function getExistingInstalls() {
|
|
8392
|
+
const installs = new Map;
|
|
8393
|
+
if (!fs3.existsSync(PLUGINS_DIR)) {
|
|
8394
|
+
return installs;
|
|
8395
|
+
}
|
|
8396
|
+
const repoEntries = fs3.readdirSync(PLUGINS_DIR, { withFileTypes: true });
|
|
8397
|
+
for (const repoEntry of repoEntries) {
|
|
8398
|
+
if (repoEntry.name.startsWith(".") || !repoEntry.isDirectory())
|
|
8399
|
+
continue;
|
|
8400
|
+
const repoPath = path3.join(PLUGINS_DIR, repoEntry.name);
|
|
8401
|
+
const skillEntries = fs3.readdirSync(repoPath, { withFileTypes: true });
|
|
8402
|
+
for (const skillEntry of skillEntries) {
|
|
8403
|
+
if (skillEntry.name.startsWith("."))
|
|
8164
8404
|
continue;
|
|
8165
|
-
const fullPath =
|
|
8166
|
-
const relativePath =
|
|
8167
|
-
if (
|
|
8168
|
-
const target =
|
|
8169
|
-
|
|
8170
|
-
} else if (
|
|
8171
|
-
|
|
8405
|
+
const fullPath = path3.join(repoPath, skillEntry.name);
|
|
8406
|
+
const relativePath = `${repoEntry.name}/${skillEntry.name}`;
|
|
8407
|
+
if (skillEntry.isSymbolicLink()) {
|
|
8408
|
+
const target = fs3.readlinkSync(fullPath);
|
|
8409
|
+
installs.set(relativePath, target);
|
|
8410
|
+
} else if (skillEntry.isDirectory()) {
|
|
8411
|
+
installs.set(relativePath, fullPath);
|
|
8172
8412
|
}
|
|
8173
8413
|
}
|
|
8174
|
-
}
|
|
8175
|
-
|
|
8176
|
-
return symlinks;
|
|
8414
|
+
}
|
|
8415
|
+
return installs;
|
|
8177
8416
|
}
|
|
8178
|
-
function
|
|
8417
|
+
function cleanupStaleInstalls(currentSkills) {
|
|
8179
8418
|
const result = {
|
|
8180
8419
|
removed: [],
|
|
8181
8420
|
errors: []
|
|
8182
8421
|
};
|
|
8183
|
-
const
|
|
8184
|
-
for (const [relativePath] of
|
|
8422
|
+
const existingInstalls = getExistingInstalls();
|
|
8423
|
+
for (const [relativePath] of existingInstalls) {
|
|
8185
8424
|
if (!currentSkills.has(relativePath)) {
|
|
8186
|
-
const fullPath =
|
|
8425
|
+
const fullPath = path3.join(PLUGINS_DIR, relativePath);
|
|
8187
8426
|
try {
|
|
8188
|
-
|
|
8427
|
+
const stats = fs3.lstatSync(fullPath);
|
|
8428
|
+
if (stats.isSymbolicLink()) {
|
|
8429
|
+
fs3.unlinkSync(fullPath);
|
|
8430
|
+
} else if (stats.isDirectory()) {
|
|
8431
|
+
fs3.rmSync(fullPath, { recursive: true, force: true });
|
|
8432
|
+
}
|
|
8189
8433
|
result.removed.push(relativePath);
|
|
8190
|
-
let parentDir =
|
|
8434
|
+
let parentDir = path3.dirname(fullPath);
|
|
8191
8435
|
while (parentDir !== PLUGINS_DIR && parentDir.startsWith(PLUGINS_DIR)) {
|
|
8192
8436
|
try {
|
|
8193
|
-
const entries =
|
|
8437
|
+
const entries = fs3.readdirSync(parentDir);
|
|
8194
8438
|
if (entries.length === 0) {
|
|
8195
|
-
|
|
8196
|
-
parentDir =
|
|
8439
|
+
fs3.rmdirSync(parentDir);
|
|
8440
|
+
parentDir = path3.dirname(parentDir);
|
|
8197
8441
|
} else {
|
|
8198
8442
|
break;
|
|
8199
8443
|
}
|
|
@@ -8209,16 +8453,16 @@ function cleanupStaleSymlinks(currentSkills) {
|
|
|
8209
8453
|
return result;
|
|
8210
8454
|
}
|
|
8211
8455
|
function hasLocalConflict(skillName) {
|
|
8212
|
-
const localSkillPath =
|
|
8213
|
-
if (
|
|
8214
|
-
const realPath =
|
|
8456
|
+
const localSkillPath = path3.join(SKILL_BASE, skillName);
|
|
8457
|
+
if (fs3.existsSync(localSkillPath)) {
|
|
8458
|
+
const realPath = fs3.realpathSync(localSkillPath);
|
|
8215
8459
|
return !realPath.includes("_plugins");
|
|
8216
8460
|
}
|
|
8217
8461
|
return false;
|
|
8218
8462
|
}
|
|
8219
8463
|
async function findGitRoot(startPath) {
|
|
8220
8464
|
try {
|
|
8221
|
-
const result = await $
|
|
8465
|
+
const result = await $3`git -C ${startPath} rev-parse --show-toplevel`.quiet();
|
|
8222
8466
|
if (result.exitCode === 0) {
|
|
8223
8467
|
return result.stdout.toString().trim();
|
|
8224
8468
|
}
|
|
@@ -8230,14 +8474,14 @@ async function ensureGitignore() {
|
|
|
8230
8474
|
if (!gitRoot) {
|
|
8231
8475
|
return { modified: false, gitRoot: null };
|
|
8232
8476
|
}
|
|
8233
|
-
const gitignorePath =
|
|
8477
|
+
const gitignorePath = path3.join(gitRoot, ".gitignore");
|
|
8234
8478
|
const pluginsEntry = "_plugins/";
|
|
8235
|
-
const relativePath =
|
|
8479
|
+
const relativePath = path3.relative(gitRoot, PLUGINS_DIR);
|
|
8236
8480
|
const gitignoreEntry = relativePath ? `${relativePath}/` : pluginsEntry;
|
|
8237
8481
|
let content = "";
|
|
8238
8482
|
let exists = false;
|
|
8239
8483
|
try {
|
|
8240
|
-
content =
|
|
8484
|
+
content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
8241
8485
|
exists = true;
|
|
8242
8486
|
} catch {}
|
|
8243
8487
|
const lines = content.split(`
|
|
@@ -8259,27 +8503,41 @@ ${gitignoreEntry}
|
|
|
8259
8503
|
` : `# OpenCode remote skills plugin
|
|
8260
8504
|
${gitignoreEntry}
|
|
8261
8505
|
`;
|
|
8262
|
-
|
|
8506
|
+
fs3.writeFileSync(gitignorePath, newContent);
|
|
8263
8507
|
return { modified: true, gitRoot };
|
|
8264
8508
|
}
|
|
8265
8509
|
|
|
8266
|
-
// src/plugin-
|
|
8267
|
-
import * as
|
|
8268
|
-
import * as
|
|
8269
|
-
import { homedir as
|
|
8270
|
-
var DEFAULT_PLUGIN_DIR =
|
|
8510
|
+
// src/plugin-install.ts
|
|
8511
|
+
import * as fs4 from "fs";
|
|
8512
|
+
import * as path4 from "path";
|
|
8513
|
+
import { homedir as homedir3 } from "os";
|
|
8514
|
+
var DEFAULT_PLUGIN_DIR = path4.join(homedir3(), ".config", "opencode", "plugin");
|
|
8271
8515
|
var REMOTE_PREFIX = "_remote_";
|
|
8516
|
+
function removePathIfExists(targetPath) {
|
|
8517
|
+
try {
|
|
8518
|
+
const stats = fs4.lstatSync(targetPath);
|
|
8519
|
+
if (stats.isDirectory()) {
|
|
8520
|
+
fs4.rmSync(targetPath, { recursive: true });
|
|
8521
|
+
} else {
|
|
8522
|
+
fs4.unlinkSync(targetPath);
|
|
8523
|
+
}
|
|
8524
|
+
} catch (err) {
|
|
8525
|
+
if (err.code !== "ENOENT") {
|
|
8526
|
+
throw err;
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8272
8530
|
function getPluginSymlinkName(plugin) {
|
|
8273
8531
|
return `${REMOTE_PREFIX}${plugin.repoShortName}_${plugin.name}${plugin.extension}`;
|
|
8274
8532
|
}
|
|
8275
8533
|
function ensurePluginDir(pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
8276
|
-
if (!
|
|
8277
|
-
|
|
8534
|
+
if (!fs4.existsSync(pluginDir)) {
|
|
8535
|
+
fs4.mkdirSync(pluginDir, { recursive: true });
|
|
8278
8536
|
}
|
|
8279
8537
|
}
|
|
8280
|
-
function
|
|
8538
|
+
function createPluginInstall(plugin, pluginDir = DEFAULT_PLUGIN_DIR, installMethod = "link") {
|
|
8281
8539
|
const symlinkName = getPluginSymlinkName(plugin);
|
|
8282
|
-
const symlinkPath =
|
|
8540
|
+
const symlinkPath = path4.join(pluginDir, symlinkName);
|
|
8283
8541
|
const result = {
|
|
8284
8542
|
pluginName: plugin.name,
|
|
8285
8543
|
symlinkName,
|
|
@@ -8288,40 +8546,42 @@ function createPluginSymlink(plugin, pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
|
8288
8546
|
};
|
|
8289
8547
|
try {
|
|
8290
8548
|
ensurePluginDir(pluginDir);
|
|
8291
|
-
|
|
8292
|
-
|
|
8549
|
+
removePathIfExists(symlinkPath);
|
|
8550
|
+
if (installMethod === "copy") {
|
|
8551
|
+
fs4.cpSync(plugin.path, symlinkPath);
|
|
8552
|
+
} else {
|
|
8553
|
+
fs4.symlinkSync(plugin.path, symlinkPath);
|
|
8293
8554
|
}
|
|
8294
|
-
fs3.symlinkSync(plugin.path, symlinkPath);
|
|
8295
8555
|
} catch (err) {
|
|
8296
8556
|
result.error = err instanceof Error ? err.message : String(err);
|
|
8297
8557
|
}
|
|
8298
8558
|
return result;
|
|
8299
8559
|
}
|
|
8300
|
-
function
|
|
8301
|
-
return plugins.map((
|
|
8560
|
+
function createPluginInstalls(plugins, pluginDir = DEFAULT_PLUGIN_DIR, installMethod = "link") {
|
|
8561
|
+
return plugins.map((plugin) => createPluginInstall(plugin, pluginDir, installMethod));
|
|
8302
8562
|
}
|
|
8303
|
-
function
|
|
8304
|
-
if (!
|
|
8563
|
+
function getRemotePluginInstalls(pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
8564
|
+
if (!fs4.existsSync(pluginDir)) {
|
|
8305
8565
|
return [];
|
|
8306
8566
|
}
|
|
8307
8567
|
try {
|
|
8308
|
-
const entries =
|
|
8568
|
+
const entries = fs4.readdirSync(pluginDir);
|
|
8309
8569
|
return entries.filter((name) => name.startsWith(REMOTE_PREFIX));
|
|
8310
8570
|
} catch {
|
|
8311
8571
|
return [];
|
|
8312
8572
|
}
|
|
8313
8573
|
}
|
|
8314
|
-
function
|
|
8574
|
+
function cleanupStalePluginInstalls(currentInstalls, pluginDir = DEFAULT_PLUGIN_DIR) {
|
|
8315
8575
|
const result = {
|
|
8316
8576
|
removed: [],
|
|
8317
8577
|
errors: []
|
|
8318
8578
|
};
|
|
8319
|
-
const existing =
|
|
8579
|
+
const existing = getRemotePluginInstalls(pluginDir);
|
|
8320
8580
|
for (const name of existing) {
|
|
8321
|
-
if (!
|
|
8322
|
-
const
|
|
8581
|
+
if (!currentInstalls.has(name)) {
|
|
8582
|
+
const installPath = path4.join(pluginDir, name);
|
|
8323
8583
|
try {
|
|
8324
|
-
|
|
8584
|
+
removePathIfExists(installPath);
|
|
8325
8585
|
result.removed.push(name);
|
|
8326
8586
|
} catch (err) {
|
|
8327
8587
|
result.errors.push(`Failed to remove ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -8332,36 +8592,10 @@ function cleanupStalePluginSymlinks(currentSymlinks, pluginDir = DEFAULT_PLUGIN_
|
|
|
8332
8592
|
}
|
|
8333
8593
|
|
|
8334
8594
|
// src/index.ts
|
|
8335
|
-
import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
|
|
8336
|
-
import { join as join5 } from "path";
|
|
8337
|
-
import { homedir as homedir3 } from "os";
|
|
8338
|
-
var LOG_PREFIX = "[remote-skills]";
|
|
8339
|
-
var LOG_DIR = join5(homedir3(), ".cache", "opencode", "remote-skills");
|
|
8340
|
-
var LOG_FILE = join5(LOG_DIR, "plugin.log");
|
|
8341
8595
|
var initialized = false;
|
|
8342
|
-
function timestamp() {
|
|
8343
|
-
return new Date().toISOString();
|
|
8344
|
-
}
|
|
8345
|
-
function writeLog(level, message) {
|
|
8346
|
-
try {
|
|
8347
|
-
mkdirSync4(LOG_DIR, { recursive: true });
|
|
8348
|
-
appendFileSync(LOG_FILE, `${timestamp()} [${level}] ${message}
|
|
8349
|
-
`);
|
|
8350
|
-
} catch {}
|
|
8351
|
-
}
|
|
8352
|
-
function log(message) {
|
|
8353
|
-
const fullMessage = `${LOG_PREFIX} ${message}`;
|
|
8354
|
-
console.log(fullMessage);
|
|
8355
|
-
writeLog("INFO", message);
|
|
8356
|
-
}
|
|
8357
|
-
function logError(message) {
|
|
8358
|
-
const fullMessage = `${LOG_PREFIX} ${message}`;
|
|
8359
|
-
console.error(fullMessage);
|
|
8360
|
-
writeLog("ERROR", message);
|
|
8361
|
-
}
|
|
8362
8596
|
async function performSync(config) {
|
|
8363
8597
|
if (config.repositories.length === 0) {
|
|
8364
|
-
return { results: [], skippedConflicts: [], totalSkills: 0, remoteAgents: new Map, remoteCommands: new Map, pluginsChanged: false, totalPlugins: 0 };
|
|
8598
|
+
return { results: [], skippedConflicts: [], totalSkills: 0, remoteAgents: new Map, remoteCommands: new Map, remoteInstructions: [], pluginsChanged: false, totalPlugins: 0 };
|
|
8365
8599
|
}
|
|
8366
8600
|
log(`Syncing ${config.repositories.length} repositories...`);
|
|
8367
8601
|
const results = await syncRepositories(config.repositories);
|
|
@@ -8373,30 +8607,30 @@ async function performSync(config) {
|
|
|
8373
8607
|
logError(`\u2717 Failed to sync ${result.shortName}: ${result.error}`);
|
|
8374
8608
|
continue;
|
|
8375
8609
|
}
|
|
8376
|
-
const
|
|
8610
|
+
const skillsToInstall = result.skills.filter((skill) => {
|
|
8377
8611
|
if (hasLocalConflict(skill.name)) {
|
|
8378
8612
|
skippedConflicts.push(skill.name);
|
|
8379
8613
|
return false;
|
|
8380
8614
|
}
|
|
8381
8615
|
return true;
|
|
8382
8616
|
});
|
|
8383
|
-
const filteredResult = { ...result, skills:
|
|
8384
|
-
const
|
|
8385
|
-
for (const sr of
|
|
8617
|
+
const filteredResult = { ...result, skills: skillsToInstall };
|
|
8618
|
+
const installResults = await createInstallsForRepo(filteredResult, config.installMethod);
|
|
8619
|
+
for (const sr of installResults) {
|
|
8386
8620
|
if (!sr.error) {
|
|
8387
8621
|
currentSkills.add(`${result.shortName}/${sr.skillName}`);
|
|
8388
8622
|
totalSkills++;
|
|
8389
8623
|
} else {
|
|
8390
|
-
logError(`\u2717 Failed to
|
|
8624
|
+
logError(`\u2717 Failed to install skill ${sr.skillName}: ${sr.error}`);
|
|
8391
8625
|
}
|
|
8392
8626
|
}
|
|
8393
|
-
const skillCount =
|
|
8627
|
+
const skillCount = skillsToInstall.length;
|
|
8394
8628
|
const status = result.updated ? "\u2713" : "\u2713";
|
|
8395
8629
|
log(`${status} ${result.shortName} (${result.ref}) - ${skillCount} skills`);
|
|
8396
8630
|
}
|
|
8397
|
-
const cleanup =
|
|
8631
|
+
const cleanup = cleanupStaleInstalls(currentSkills);
|
|
8398
8632
|
if (cleanup.removed.length > 0) {
|
|
8399
|
-
log(`Cleaned up ${cleanup.removed.length} stale
|
|
8633
|
+
log(`Cleaned up ${cleanup.removed.length} stale skill installs`);
|
|
8400
8634
|
}
|
|
8401
8635
|
for (const conflict of skippedConflicts) {
|
|
8402
8636
|
log(`\u26A0 Conflict: '${conflict}' exists locally, skipping`);
|
|
@@ -8439,32 +8673,43 @@ async function performSync(config) {
|
|
|
8439
8673
|
if (remoteCommands.size > 0) {
|
|
8440
8674
|
log(`Discovered ${remoteCommands.size} remote commands`);
|
|
8441
8675
|
}
|
|
8676
|
+
const remoteInstructions = [];
|
|
8677
|
+
for (const result of results) {
|
|
8678
|
+
if (result.error)
|
|
8679
|
+
continue;
|
|
8680
|
+
for (const instruction of result.instructions) {
|
|
8681
|
+
remoteInstructions.push(instruction.path);
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
if (remoteInstructions.length > 0) {
|
|
8685
|
+
log(`Discovered ${remoteInstructions.length} remote instructions`);
|
|
8686
|
+
}
|
|
8442
8687
|
const allPlugins = [];
|
|
8443
8688
|
for (const result of results) {
|
|
8444
8689
|
if (result.error)
|
|
8445
8690
|
continue;
|
|
8446
8691
|
allPlugins.push(...result.plugins);
|
|
8447
8692
|
}
|
|
8448
|
-
const
|
|
8449
|
-
const
|
|
8693
|
+
const existingPluginInstalls = new Set(getRemotePluginInstalls());
|
|
8694
|
+
const newPluginInstalls = new Set;
|
|
8450
8695
|
if (allPlugins.length > 0) {
|
|
8451
|
-
const
|
|
8452
|
-
for (const sr of
|
|
8696
|
+
const installResults = createPluginInstalls(allPlugins, undefined, config.installMethod);
|
|
8697
|
+
for (const sr of installResults) {
|
|
8453
8698
|
if (!sr.error) {
|
|
8454
|
-
|
|
8699
|
+
newPluginInstalls.add(sr.symlinkName);
|
|
8455
8700
|
} else {
|
|
8456
|
-
logError(`\u2717 Failed to
|
|
8701
|
+
logError(`\u2717 Failed to install plugin ${sr.pluginName}: ${sr.error}`);
|
|
8457
8702
|
}
|
|
8458
8703
|
}
|
|
8459
8704
|
log(`Discovered ${allPlugins.length} remote plugins`);
|
|
8460
8705
|
}
|
|
8461
|
-
const pluginCleanup =
|
|
8706
|
+
const pluginCleanup = cleanupStalePluginInstalls(newPluginInstalls);
|
|
8462
8707
|
if (pluginCleanup.removed.length > 0) {
|
|
8463
|
-
log(`Cleaned up ${pluginCleanup.removed.length} stale plugin
|
|
8708
|
+
log(`Cleaned up ${pluginCleanup.removed.length} stale plugin installs`);
|
|
8464
8709
|
}
|
|
8465
|
-
const pluginsChanged = !setsEqual(
|
|
8466
|
-
const totalPlugins =
|
|
8467
|
-
return { results, skippedConflicts, totalSkills, remoteAgents, remoteCommands, pluginsChanged, totalPlugins };
|
|
8710
|
+
const pluginsChanged = !setsEqual(existingPluginInstalls, newPluginInstalls);
|
|
8711
|
+
const totalPlugins = newPluginInstalls.size;
|
|
8712
|
+
return { results, skippedConflicts, totalSkills, remoteAgents, remoteCommands, remoteInstructions, pluginsChanged, totalPlugins };
|
|
8468
8713
|
}
|
|
8469
8714
|
function setsEqual(a, b) {
|
|
8470
8715
|
if (a.size !== b.size)
|
|
@@ -8484,12 +8729,17 @@ var RemoteSkillsPlugin = async (ctx) => {
|
|
|
8484
8729
|
if (pluginConfig.repositories.length === 0) {
|
|
8485
8730
|
return {};
|
|
8486
8731
|
}
|
|
8732
|
+
if (pluginConfig.installMethod === "copy") {
|
|
8733
|
+
logDebug("Using copy mode for skill/plugin installation");
|
|
8734
|
+
} else {
|
|
8735
|
+
logDebug("Using symlink mode for skill/plugin installation");
|
|
8736
|
+
}
|
|
8487
8737
|
ensurePluginsDir();
|
|
8488
8738
|
const gitignoreResult = await ensureGitignore();
|
|
8489
8739
|
if (gitignoreResult.modified) {
|
|
8490
8740
|
log(`Added _plugins/ to .gitignore at ${gitignoreResult.gitRoot}`);
|
|
8491
8741
|
}
|
|
8492
|
-
const { totalSkills, skippedConflicts, remoteAgents, remoteCommands, pluginsChanged, totalPlugins } = await performSync(pluginConfig);
|
|
8742
|
+
const { totalSkills, skippedConflicts, remoteAgents, remoteCommands, remoteInstructions, pluginsChanged, totalPlugins } = await performSync(pluginConfig);
|
|
8493
8743
|
if (pluginsChanged) {
|
|
8494
8744
|
log("\u26A0 Plugin changes detected. Restart OpenCode to apply.");
|
|
8495
8745
|
}
|
|
@@ -8499,9 +8749,11 @@ var RemoteSkillsPlugin = async (ctx) => {
|
|
|
8499
8749
|
parts.push(`${totalSkills} skills`);
|
|
8500
8750
|
if (totalPlugins > 0)
|
|
8501
8751
|
parts.push(`${totalPlugins} plugins`);
|
|
8752
|
+
if (remoteInstructions.length > 0)
|
|
8753
|
+
parts.push(`${remoteInstructions.length} instructions`);
|
|
8502
8754
|
let message;
|
|
8503
8755
|
if (parts.length === 0) {
|
|
8504
|
-
message = "No remote
|
|
8756
|
+
message = "No remote config found";
|
|
8505
8757
|
} else {
|
|
8506
8758
|
message = `${parts.join(", ")} available`;
|
|
8507
8759
|
if (skippedCount > 0) {
|
|
@@ -8547,6 +8799,19 @@ var RemoteSkillsPlugin = async (ctx) => {
|
|
|
8547
8799
|
}
|
|
8548
8800
|
}
|
|
8549
8801
|
}
|
|
8802
|
+
if (remoteInstructions.length > 0) {
|
|
8803
|
+
if (config.instructions !== undefined && typeof config.instructions !== "string" && !Array.isArray(config.instructions)) {
|
|
8804
|
+
logError("config.instructions is not a string or array, skipping instruction injection");
|
|
8805
|
+
} else if (Array.isArray(config.instructions) && !config.instructions.every((x) => typeof x === "string")) {
|
|
8806
|
+
logError("config.instructions contains non-string elements, skipping instruction injection");
|
|
8807
|
+
} else {
|
|
8808
|
+
if (!Array.isArray(config.instructions)) {
|
|
8809
|
+
config.instructions = config.instructions ? [config.instructions] : [];
|
|
8810
|
+
}
|
|
8811
|
+
config.instructions.push(...remoteInstructions);
|
|
8812
|
+
log(`Appended ${remoteInstructions.length} remote instructions`);
|
|
8813
|
+
}
|
|
8814
|
+
}
|
|
8550
8815
|
},
|
|
8551
8816
|
event: async ({ event }) => {}
|
|
8552
8817
|
};
|