oh-my-opencode-slim 2.0.4 → 2.1.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.ja-JP.md +61 -21
- package/README.ko-KR.md +59 -19
- package/README.md +73 -20
- package/README.zh-CN.md +62 -24
- package/dist/agents/council.d.ts +1 -1
- package/dist/agents/index.d.ts +7 -2
- package/dist/agents/orchestrator.d.ts +1 -1
- package/dist/agents/permissions.d.ts +10 -0
- package/dist/cli/custom-skills-registry.d.ts +18 -0
- package/dist/cli/custom-skills.d.ts +3 -19
- package/dist/cli/index.js +1093 -186
- package/dist/cli/providers.d.ts +5 -9
- package/dist/cli/skills.d.ts +3 -3
- package/dist/companion/manager.d.ts +7 -0
- package/dist/config/constants.d.ts +3 -2
- package/dist/config/loader.d.ts +5 -2
- package/dist/config/schema.d.ts +7 -0
- package/dist/council/council-manager.d.ts +1 -1
- package/dist/hooks/auto-update-checker/skill-sync.d.ts +59 -1
- package/dist/hooks/filter-available-skills/index.d.ts +1 -2
- package/dist/hooks/foreground-fallback/index.d.ts +5 -1
- package/dist/hooks/image-hook.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/loop-command/index.d.ts +13 -0
- package/dist/hooks/phase-reminder/index.d.ts +1 -2
- package/dist/hooks/task-session-manager/index.d.ts +1 -2
- package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
- package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
- package/dist/hooks/types.d.ts +3 -1
- package/dist/index.js +3491 -876
- package/dist/interview/dashboard-manager.d.ts +21 -0
- package/dist/interview/dashboard.d.ts +5 -0
- package/dist/interview/document.d.ts +4 -1
- package/dist/interview/manager.d.ts +0 -14
- package/dist/interview/server.d.ts +2 -0
- package/dist/interview/service.d.ts +9 -0
- package/dist/interview/session-server.d.ts +21 -0
- package/dist/interview/types.d.ts +16 -1
- package/dist/loop/loop-session.d.ts +64 -0
- package/dist/multiplexer/factory.d.ts +5 -5
- package/dist/multiplexer/herdr/index.d.ts +31 -0
- package/dist/multiplexer/index.d.ts +1 -0
- package/dist/multiplexer/session-manager.d.ts +3 -0
- package/dist/multiplexer/types.d.ts +4 -4
- package/dist/tools/acp-run.d.ts +1 -1
- package/dist/tui-state.d.ts +3 -0
- package/dist/tui.d.ts +4 -1
- package/dist/tui.js +142 -55
- package/dist/utils/background-job-board.d.ts +20 -0
- package/dist/utils/env.d.ts +3 -0
- package/dist/utils/session.d.ts +1 -1
- package/oh-my-opencode-slim.schema.json +24 -4
- package/package.json +1 -1
- package/src/skills/clonedeps/SKILL.md +2 -2
- package/src/skills/clonedeps/codemap.md +23 -32
- package/src/skills/codemap/SKILL.md +2 -2
- package/src/skills/codemap.md +63 -36
- package/src/skills/loop-engineering/SKILL.md +30 -0
- package/src/skills/reflect/SKILL.md +133 -0
- package/src/skills/release-smoke-test/SKILL.md +159 -0
- package/src/skills/simplify/SKILL.md +6 -6
package/dist/cli/index.js
CHANGED
|
@@ -90,8 +90,8 @@ function isPwshAvailable() {
|
|
|
90
90
|
});
|
|
91
91
|
return result.status === 0;
|
|
92
92
|
}
|
|
93
|
-
function escapePowerShellPath(
|
|
94
|
-
return
|
|
93
|
+
function escapePowerShellPath(path3) {
|
|
94
|
+
return path3.replace(/'/g, "''");
|
|
95
95
|
}
|
|
96
96
|
function getWindowsZipExtractor() {
|
|
97
97
|
const buildNumber = getWindowsBuildNumber();
|
|
@@ -163,17 +163,17 @@ import * as path from "node:path";
|
|
|
163
163
|
// src/cli/config-io.ts
|
|
164
164
|
init_compat();
|
|
165
165
|
import {
|
|
166
|
-
copyFileSync
|
|
167
|
-
existsSync as
|
|
168
|
-
mkdirSync as
|
|
166
|
+
copyFileSync,
|
|
167
|
+
existsSync as existsSync2,
|
|
168
|
+
mkdirSync as mkdirSync2,
|
|
169
169
|
readFileSync,
|
|
170
170
|
renameSync,
|
|
171
171
|
rmSync,
|
|
172
|
-
statSync
|
|
172
|
+
statSync,
|
|
173
173
|
writeFileSync
|
|
174
174
|
} from "node:fs";
|
|
175
175
|
import { homedir as homedir2 } from "node:os";
|
|
176
|
-
import { dirname as
|
|
176
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
177
177
|
|
|
178
178
|
// src/cli/paths.ts
|
|
179
179
|
import { existsSync, mkdirSync } from "node:fs";
|
|
@@ -279,10 +279,6 @@ function ensureOpenCodeConfigDir() {
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
// src/config/constants.ts
|
|
282
|
-
var AGENT_ALIASES = {
|
|
283
|
-
explore: "explorer",
|
|
284
|
-
"frontend-ui-ux-engineer": "designer"
|
|
285
|
-
};
|
|
286
282
|
var SUBAGENT_NAMES = [
|
|
287
283
|
"explorer",
|
|
288
284
|
"librarian",
|
|
@@ -344,7 +340,7 @@ var CouncilConfigSchema = z.object({
|
|
|
344
340
|
default_preset: z.string().default("default"),
|
|
345
341
|
councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
|
|
346
342
|
councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
|
|
347
|
-
master: z.unknown().optional().describe("DEPRECATED
|
|
343
|
+
master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
|
|
348
344
|
}).transform((data) => {
|
|
349
345
|
const deprecated = [];
|
|
350
346
|
if (data.master !== undefined)
|
|
@@ -410,7 +406,13 @@ var AgentOverrideConfigSchema = z2.object({
|
|
|
410
406
|
options: z2.record(z2.string(), z2.unknown()).optional(),
|
|
411
407
|
displayName: z2.string().min(1).optional()
|
|
412
408
|
}).strict();
|
|
413
|
-
var MultiplexerTypeSchema = z2.enum([
|
|
409
|
+
var MultiplexerTypeSchema = z2.enum([
|
|
410
|
+
"auto",
|
|
411
|
+
"tmux",
|
|
412
|
+
"zellij",
|
|
413
|
+
"herdr",
|
|
414
|
+
"none"
|
|
415
|
+
]);
|
|
414
416
|
var MultiplexerLayoutSchema = z2.enum([
|
|
415
417
|
"main-horizontal",
|
|
416
418
|
"main-vertical",
|
|
@@ -465,6 +467,7 @@ var CompanionConfigSchema = z2.object({
|
|
|
465
467
|
debug: z2.boolean().optional().describe("Enable verbose native companion debug logs.")
|
|
466
468
|
});
|
|
467
469
|
var AcpAgentPermissionModeSchema = z2.enum(["ask", "allow", "reject"]);
|
|
470
|
+
var MAX_ACP_TIMEOUT_MS = 2147483647;
|
|
468
471
|
var AcpAgentConfigSchema = z2.object({
|
|
469
472
|
command: z2.string().min(1),
|
|
470
473
|
args: z2.array(z2.string()).default([]),
|
|
@@ -474,28 +477,17 @@ var AcpAgentConfigSchema = z2.object({
|
|
|
474
477
|
prompt: z2.string().min(1).optional(),
|
|
475
478
|
orchestratorPrompt: z2.string().min(1).optional(),
|
|
476
479
|
wrapperModel: ProviderModelIdSchema.optional(),
|
|
477
|
-
timeoutMs: z2.number().int().min(
|
|
480
|
+
timeoutMs: z2.number().int().min(0).max(MAX_ACP_TIMEOUT_MS).default(0).describe("Timeout for a single ACP run in milliseconds. Set to 0 to disable the timeout."),
|
|
478
481
|
permissionMode: AcpAgentPermissionModeSchema.default("ask")
|
|
479
482
|
}).strict();
|
|
480
483
|
var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
|
|
481
|
-
function
|
|
484
|
+
function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
|
|
482
485
|
for (const [name, override] of Object.entries(overrides)) {
|
|
483
|
-
|
|
484
|
-
if (!isBuiltInOrAlias) {
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
if (override.prompt !== undefined) {
|
|
488
|
-
ctx.addIssue({
|
|
489
|
-
code: z2.ZodIssueCode.custom,
|
|
490
|
-
path: [...pathPrefix, name, "prompt"],
|
|
491
|
-
message: "prompt is only supported for custom agents"
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
if (override.orchestratorPrompt !== undefined) {
|
|
486
|
+
if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
|
|
495
487
|
ctx.addIssue({
|
|
496
488
|
code: z2.ZodIssueCode.custom,
|
|
497
489
|
path: [...pathPrefix, name, "orchestratorPrompt"],
|
|
498
|
-
message: "orchestratorPrompt is
|
|
490
|
+
message: "orchestratorPrompt is not supported for the orchestrator agent"
|
|
499
491
|
});
|
|
500
492
|
}
|
|
501
493
|
}
|
|
@@ -503,11 +495,14 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
|
|
|
503
495
|
var PluginConfigSchema = z2.object({
|
|
504
496
|
preset: z2.string().optional(),
|
|
505
497
|
setDefaultAgent: z2.boolean().optional(),
|
|
498
|
+
compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout when enabled."),
|
|
506
499
|
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
507
500
|
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
508
501
|
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
509
502
|
disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
|
|
510
503
|
disabled_mcps: z2.array(z2.string()).optional(),
|
|
504
|
+
disabled_tools: z2.array(z2.string()).optional().describe("Tool names to disable completely. Disabled tools are not registered with OpenCode and cannot be used by agents."),
|
|
505
|
+
disabled_skills: z2.array(z2.string()).optional().describe("Skill names to disable completely. Disabled skills are not granted to agents, even when referenced by presets or agent overrides."),
|
|
511
506
|
multiplexer: MultiplexerConfigSchema.optional(),
|
|
512
507
|
tmux: TmuxConfigSchema.optional(),
|
|
513
508
|
websearch: WebsearchConfigSchema.optional(),
|
|
@@ -519,11 +514,14 @@ var PluginConfigSchema = z2.object({
|
|
|
519
514
|
acpAgents: AcpAgentsConfigSchema.optional()
|
|
520
515
|
}).superRefine((value, ctx) => {
|
|
521
516
|
if (value.agents) {
|
|
522
|
-
|
|
517
|
+
rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
|
|
523
518
|
}
|
|
524
519
|
if (value.presets) {
|
|
525
520
|
for (const [presetName, preset] of Object.entries(value.presets)) {
|
|
526
|
-
|
|
521
|
+
rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
|
|
522
|
+
"presets",
|
|
523
|
+
presetName
|
|
524
|
+
]);
|
|
527
525
|
}
|
|
528
526
|
}
|
|
529
527
|
});
|
|
@@ -540,16 +538,7 @@ var DEFAULT_AGENT_MCPS = {
|
|
|
540
538
|
councillor: []
|
|
541
539
|
};
|
|
542
540
|
|
|
543
|
-
// src/cli/custom-skills.ts
|
|
544
|
-
import {
|
|
545
|
-
copyFileSync,
|
|
546
|
-
existsSync as existsSync2,
|
|
547
|
-
mkdirSync as mkdirSync2,
|
|
548
|
-
readdirSync,
|
|
549
|
-
statSync
|
|
550
|
-
} from "node:fs";
|
|
551
|
-
import { dirname as dirname2, join as join2 } from "node:path";
|
|
552
|
-
import { fileURLToPath } from "node:url";
|
|
541
|
+
// src/cli/custom-skills-registry.ts
|
|
553
542
|
var CUSTOM_SKILLS = [
|
|
554
543
|
{
|
|
555
544
|
name: "simplify",
|
|
@@ -587,6 +576,12 @@ var CUSTOM_SKILLS = [
|
|
|
587
576
|
allowedAgents: ["orchestrator"],
|
|
588
577
|
sourcePath: "src/skills/oh-my-opencode-slim"
|
|
589
578
|
},
|
|
579
|
+
{
|
|
580
|
+
name: "release-smoke-test",
|
|
581
|
+
description: "Validate packed release candidates and bugfixes before public publish",
|
|
582
|
+
allowedAgents: ["orchestrator"],
|
|
583
|
+
sourcePath: "src/skills/release-smoke-test"
|
|
584
|
+
},
|
|
590
585
|
{
|
|
591
586
|
name: "worktrees",
|
|
592
587
|
description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
|
|
@@ -594,45 +589,6 @@ var CUSTOM_SKILLS = [
|
|
|
594
589
|
sourcePath: "src/skills/worktrees"
|
|
595
590
|
}
|
|
596
591
|
];
|
|
597
|
-
function getCustomSkillsDir() {
|
|
598
|
-
return join2(getConfigDir(), "skills");
|
|
599
|
-
}
|
|
600
|
-
function copyDirRecursive(src, dest) {
|
|
601
|
-
if (!existsSync2(dest)) {
|
|
602
|
-
mkdirSync2(dest, { recursive: true });
|
|
603
|
-
}
|
|
604
|
-
const entries = readdirSync(src);
|
|
605
|
-
for (const entry of entries) {
|
|
606
|
-
const srcPath = join2(src, entry);
|
|
607
|
-
const destPath = join2(dest, entry);
|
|
608
|
-
const stat = statSync(srcPath);
|
|
609
|
-
if (stat.isDirectory()) {
|
|
610
|
-
copyDirRecursive(srcPath, destPath);
|
|
611
|
-
} else {
|
|
612
|
-
const destDir = dirname2(destPath);
|
|
613
|
-
if (!existsSync2(destDir)) {
|
|
614
|
-
mkdirSync2(destDir, { recursive: true });
|
|
615
|
-
}
|
|
616
|
-
copyFileSync(srcPath, destPath);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
function installCustomSkill(skill) {
|
|
621
|
-
try {
|
|
622
|
-
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
623
|
-
const sourcePath = join2(packageRoot, skill.sourcePath);
|
|
624
|
-
const targetPath = join2(getCustomSkillsDir(), skill.name);
|
|
625
|
-
if (!existsSync2(sourcePath)) {
|
|
626
|
-
console.error(`Custom skill source not found: ${sourcePath}`);
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
copyDirRecursive(sourcePath, targetPath);
|
|
630
|
-
return true;
|
|
631
|
-
} catch (error) {
|
|
632
|
-
console.error(`Failed to install custom skill: ${skill.name}`, error);
|
|
633
|
-
return false;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
592
|
|
|
637
593
|
// src/cli/providers.ts
|
|
638
594
|
var SCHEMA_URL = "https://unpkg.com/oh-my-opencode-slim@latest/oh-my-opencode-slim.schema.json";
|
|
@@ -674,12 +630,11 @@ var MODEL_MAPPINGS = {
|
|
|
674
630
|
fixer: { model: "zai-coding-plan/glm-5", variant: "low" }
|
|
675
631
|
},
|
|
676
632
|
"opencode-go": {
|
|
677
|
-
orchestrator: { model: "opencode-go/glm-5.
|
|
678
|
-
oracle: { model: "opencode-go/
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
designer: { model: "opencode-go/kimi-k2.6", variant: "medium" },
|
|
633
|
+
orchestrator: { model: "opencode-go/glm-5.2" },
|
|
634
|
+
oracle: { model: "opencode-go/qwen3.7-max", variant: "max" },
|
|
635
|
+
librarian: { model: "opencode-go/deepseek-v4-flash" },
|
|
636
|
+
explorer: { model: "opencode-go/deepseek-v4-flash" },
|
|
637
|
+
designer: { model: "opencode-go/kimi-k2.7-code", variant: "medium" },
|
|
683
638
|
fixer: { model: "opencode-go/deepseek-v4-flash", variant: "high" },
|
|
684
639
|
observer: { model: "opencode-go/kimi-k2.6" }
|
|
685
640
|
}
|
|
@@ -785,10 +740,10 @@ function normalizePathForMatch(path) {
|
|
|
785
740
|
return path.replaceAll("\\", "/");
|
|
786
741
|
}
|
|
787
742
|
function findPackageRoot(startPath) {
|
|
788
|
-
let currentPath =
|
|
743
|
+
let currentPath = dirname2(startPath);
|
|
789
744
|
while (true) {
|
|
790
|
-
const packageJsonPath =
|
|
791
|
-
if (
|
|
745
|
+
const packageJsonPath = join2(currentPath, "package.json");
|
|
746
|
+
if (existsSync2(packageJsonPath)) {
|
|
792
747
|
try {
|
|
793
748
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
794
749
|
if (packageJson.name === PACKAGE_NAME) {
|
|
@@ -796,7 +751,7 @@ function findPackageRoot(startPath) {
|
|
|
796
751
|
}
|
|
797
752
|
} catch {}
|
|
798
753
|
}
|
|
799
|
-
const parentPath =
|
|
754
|
+
const parentPath = dirname2(currentPath);
|
|
800
755
|
if (parentPath === currentPath) {
|
|
801
756
|
return null;
|
|
802
757
|
}
|
|
@@ -807,8 +762,8 @@ function isLocalPackageRootEntry(entry) {
|
|
|
807
762
|
if (!entry || entry.startsWith("file://")) {
|
|
808
763
|
return false;
|
|
809
764
|
}
|
|
810
|
-
const packageJsonPath =
|
|
811
|
-
if (!
|
|
765
|
+
const packageJsonPath = join2(entry, "package.json");
|
|
766
|
+
if (!existsSync2(packageJsonPath)) {
|
|
812
767
|
return false;
|
|
813
768
|
}
|
|
814
769
|
try {
|
|
@@ -880,8 +835,8 @@ function getRequestedPackageTag(packageRoot) {
|
|
|
880
835
|
}
|
|
881
836
|
function getVersionFromPackageRoot(packageRoot) {
|
|
882
837
|
try {
|
|
883
|
-
const packageJsonPath =
|
|
884
|
-
if (!
|
|
838
|
+
const packageJsonPath = join2(packageRoot, "package.json");
|
|
839
|
+
if (!existsSync2(packageJsonPath))
|
|
885
840
|
return;
|
|
886
841
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
887
842
|
return pkg.version;
|
|
@@ -890,13 +845,13 @@ function getVersionFromPackageRoot(packageRoot) {
|
|
|
890
845
|
}
|
|
891
846
|
}
|
|
892
847
|
function getOpenCodePluginCacheDir(version) {
|
|
893
|
-
const cacheDir = process.env.XDG_CACHE_HOME?.trim() ||
|
|
848
|
+
const cacheDir = process.env.XDG_CACHE_HOME?.trim() || join2(homedir2(), ".cache");
|
|
894
849
|
const suffix = version ? `${PACKAGE_NAME}@${version}` : `${PACKAGE_NAME}@latest`;
|
|
895
|
-
return
|
|
850
|
+
return join2(cacheDir, "opencode", "packages", suffix);
|
|
896
851
|
}
|
|
897
852
|
function writeOpenCodePluginCacheManifest(cacheDir, version = "latest") {
|
|
898
853
|
try {
|
|
899
|
-
writeFileSync(
|
|
854
|
+
writeFileSync(join2(cacheDir, "package.json"), JSON.stringify({
|
|
900
855
|
name: `${PACKAGE_NAME}-cache`,
|
|
901
856
|
private: true,
|
|
902
857
|
dependencies: {
|
|
@@ -913,16 +868,16 @@ function writeOpenCodePluginCacheManifest(cacheDir, version = "latest") {
|
|
|
913
868
|
}
|
|
914
869
|
}
|
|
915
870
|
function removeOpenCodePluginCacheArtifacts(cacheDir) {
|
|
916
|
-
rmSync(
|
|
871
|
+
rmSync(join2(cacheDir, "node_modules", PACKAGE_NAME), {
|
|
917
872
|
recursive: true,
|
|
918
873
|
force: true
|
|
919
874
|
});
|
|
920
|
-
rmSync(
|
|
921
|
-
rmSync(
|
|
875
|
+
rmSync(join2(cacheDir, "bun.lock"), { force: true });
|
|
876
|
+
rmSync(join2(cacheDir, "bun.lockb"), { force: true });
|
|
922
877
|
}
|
|
923
878
|
function verifyOpenCodePluginCache(cacheDir) {
|
|
924
|
-
const pluginPackageJsonPath =
|
|
925
|
-
if (!
|
|
879
|
+
const pluginPackageJsonPath = join2(cacheDir, "node_modules", PACKAGE_NAME, "package.json");
|
|
880
|
+
if (!existsSync2(pluginPackageJsonPath)) {
|
|
926
881
|
return {
|
|
927
882
|
success: false,
|
|
928
883
|
configPath: cacheDir,
|
|
@@ -962,7 +917,7 @@ async function warmOpenCodePluginCache() {
|
|
|
962
917
|
const cacheVersion = pinnedVersion ?? requestedTag ?? runningVersion;
|
|
963
918
|
const cacheDir = getOpenCodePluginCacheDir(cacheVersion);
|
|
964
919
|
try {
|
|
965
|
-
|
|
920
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
966
921
|
} catch (err) {
|
|
967
922
|
return {
|
|
968
923
|
success: false,
|
|
@@ -1008,9 +963,9 @@ function stripJsonComments(json) {
|
|
|
1008
963
|
}
|
|
1009
964
|
function parseConfigFile(path) {
|
|
1010
965
|
try {
|
|
1011
|
-
if (!
|
|
966
|
+
if (!existsSync2(path))
|
|
1012
967
|
return { config: null };
|
|
1013
|
-
const stat =
|
|
968
|
+
const stat = statSync(path);
|
|
1014
969
|
if (stat.size === 0)
|
|
1015
970
|
return { config: null };
|
|
1016
971
|
const content = readFileSync(path, "utf-8");
|
|
@@ -1039,8 +994,8 @@ function writeConfig(configPath, config) {
|
|
|
1039
994
|
const bakPath = `${configPath}.bak`;
|
|
1040
995
|
const content = `${JSON.stringify(config, null, 2)}
|
|
1041
996
|
`;
|
|
1042
|
-
if (
|
|
1043
|
-
|
|
997
|
+
if (existsSync2(configPath)) {
|
|
998
|
+
copyFileSync(configPath, bakPath);
|
|
1044
999
|
}
|
|
1045
1000
|
writeFileSync(tmpPath, content);
|
|
1046
1001
|
renameSync(tmpPath, configPath);
|
|
@@ -1126,8 +1081,8 @@ function writeLiteConfig(installConfig, targetPath) {
|
|
|
1126
1081
|
const bakPath = `${configPath}.bak`;
|
|
1127
1082
|
const content = `${JSON.stringify(config, null, 2)}
|
|
1128
1083
|
`;
|
|
1129
|
-
if (
|
|
1130
|
-
|
|
1084
|
+
if (existsSync2(configPath)) {
|
|
1085
|
+
copyFileSync(configPath, bakPath);
|
|
1131
1086
|
}
|
|
1132
1087
|
writeFileSync(tmpPath, content);
|
|
1133
1088
|
renameSync(tmpPath, configPath);
|
|
@@ -1285,6 +1240,7 @@ function mergePluginConfigs(base, override) {
|
|
|
1285
1240
|
...base,
|
|
1286
1241
|
...override,
|
|
1287
1242
|
agents: deepMerge(base.agents, override.agents),
|
|
1243
|
+
presets: deepMerge(base.presets, override.presets),
|
|
1288
1244
|
tmux: deepMerge(base.tmux, override.tmux),
|
|
1289
1245
|
multiplexer: deepMerge(base.multiplexer, override.multiplexer),
|
|
1290
1246
|
interview: deepMerge(base.interview, override.interview),
|
|
@@ -1510,9 +1466,949 @@ Options:
|
|
|
1510
1466
|
// src/cli/install.ts
|
|
1511
1467
|
import { existsSync as existsSync7 } from "node:fs";
|
|
1512
1468
|
import { createInterface } from "node:readline/promises";
|
|
1469
|
+
import { fileURLToPath } from "node:url";
|
|
1470
|
+
|
|
1471
|
+
// src/hooks/auto-update-checker/skill-sync.ts
|
|
1472
|
+
import * as crypto from "node:crypto";
|
|
1473
|
+
import {
|
|
1474
|
+
copyFileSync as copyFileSync2,
|
|
1475
|
+
existsSync as existsSync4,
|
|
1476
|
+
lstatSync,
|
|
1477
|
+
mkdirSync as mkdirSync3,
|
|
1478
|
+
readdirSync,
|
|
1479
|
+
readFileSync as readFileSync4,
|
|
1480
|
+
renameSync as renameSync2,
|
|
1481
|
+
rmSync as rmSync2,
|
|
1482
|
+
unlinkSync,
|
|
1483
|
+
writeFileSync as writeFileSync2
|
|
1484
|
+
} from "node:fs";
|
|
1485
|
+
import * as os from "node:os";
|
|
1486
|
+
import * as path2 from "node:path";
|
|
1487
|
+
|
|
1488
|
+
// src/utils/logger.ts
|
|
1489
|
+
import { appendFile } from "node:fs/promises";
|
|
1490
|
+
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1491
|
+
var logFile = null;
|
|
1492
|
+
var writeChain = Promise.resolve();
|
|
1493
|
+
function log(message, data) {
|
|
1494
|
+
const target = logFile;
|
|
1495
|
+
if (!target)
|
|
1496
|
+
return;
|
|
1497
|
+
try {
|
|
1498
|
+
const timestamp = new Date().toISOString();
|
|
1499
|
+
let dataStr = "";
|
|
1500
|
+
if (data !== undefined) {
|
|
1501
|
+
try {
|
|
1502
|
+
dataStr = JSON.stringify(data);
|
|
1503
|
+
} catch {
|
|
1504
|
+
dataStr = "[unserializable]";
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
1508
|
+
`;
|
|
1509
|
+
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
1510
|
+
} catch {}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/hooks/auto-update-checker/skill-sync.ts
|
|
1514
|
+
var localProcessToken = globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN;
|
|
1515
|
+
if (!localProcessToken) {
|
|
1516
|
+
localProcessToken = crypto.randomUUID();
|
|
1517
|
+
globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN = localProcessToken;
|
|
1518
|
+
}
|
|
1519
|
+
var PROCESS_TOKEN = localProcessToken;
|
|
1520
|
+
var ACQUIRED_LOCKS = new Set;
|
|
1521
|
+
var LEGACY_MANAGED_SKILL_HASHES = {};
|
|
1522
|
+
function validateManifest(data) {
|
|
1523
|
+
if (typeof data !== "object" || data === null)
|
|
1524
|
+
return false;
|
|
1525
|
+
const d = data;
|
|
1526
|
+
if (d.schemaVersion !== 1)
|
|
1527
|
+
return false;
|
|
1528
|
+
if (typeof d.skills !== "object" || d.skills === null)
|
|
1529
|
+
return false;
|
|
1530
|
+
const allowedStatuses = new Set([
|
|
1531
|
+
"managed",
|
|
1532
|
+
"customized",
|
|
1533
|
+
"deleted",
|
|
1534
|
+
"conflict"
|
|
1535
|
+
]);
|
|
1536
|
+
const skillsObj = d.skills;
|
|
1537
|
+
for (const key of Object.keys(skillsObj)) {
|
|
1538
|
+
const entry = skillsObj[key];
|
|
1539
|
+
if (typeof entry !== "object" || entry === null)
|
|
1540
|
+
return false;
|
|
1541
|
+
if (typeof entry.status !== "string" || !allowedStatuses.has(entry.status))
|
|
1542
|
+
return false;
|
|
1543
|
+
if (typeof entry.packageVersion !== "string")
|
|
1544
|
+
return false;
|
|
1545
|
+
if (typeof entry.sourceHash !== "string")
|
|
1546
|
+
return false;
|
|
1547
|
+
if (typeof entry.lastManagedHash !== "string")
|
|
1548
|
+
return false;
|
|
1549
|
+
if (typeof entry.lastSeenHash !== "string")
|
|
1550
|
+
return false;
|
|
1551
|
+
if (entry.stagedPath !== undefined && typeof entry.stagedPath !== "string")
|
|
1552
|
+
return false;
|
|
1553
|
+
if (typeof entry.updatedAt !== "string")
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
return true;
|
|
1557
|
+
}
|
|
1558
|
+
function copyDirRecursive(src, dest) {
|
|
1559
|
+
const stat = lstatSync(src);
|
|
1560
|
+
if (stat.isSymbolicLink()) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (stat.isDirectory()) {
|
|
1564
|
+
mkdirSync3(dest, { recursive: true });
|
|
1565
|
+
const entries = readdirSync(src);
|
|
1566
|
+
for (const entry of entries) {
|
|
1567
|
+
copyDirRecursive(path2.join(src, entry), path2.join(dest, entry));
|
|
1568
|
+
}
|
|
1569
|
+
} else if (stat.isFile()) {
|
|
1570
|
+
const destDir = path2.dirname(dest);
|
|
1571
|
+
if (!existsSync4(destDir)) {
|
|
1572
|
+
mkdirSync3(destDir, { recursive: true });
|
|
1573
|
+
}
|
|
1574
|
+
copyFileSync2(src, dest);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function computeDirectoryHash(dirPath) {
|
|
1578
|
+
const hash = crypto.createHash("sha256");
|
|
1579
|
+
const entriesToHash = [];
|
|
1580
|
+
function traverse(currentDir) {
|
|
1581
|
+
const entries = readdirSync(currentDir);
|
|
1582
|
+
for (const entry of entries) {
|
|
1583
|
+
const absolutePath = path2.join(currentDir, entry);
|
|
1584
|
+
const stat = lstatSync(absolutePath);
|
|
1585
|
+
const relativePath = path2.relative(dirPath, absolutePath);
|
|
1586
|
+
if (stat.isSymbolicLink()) {
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (stat.isDirectory()) {
|
|
1590
|
+
entriesToHash.push({
|
|
1591
|
+
relativePath,
|
|
1592
|
+
absolutePath,
|
|
1593
|
+
kind: "directory",
|
|
1594
|
+
mode: stat.mode
|
|
1595
|
+
});
|
|
1596
|
+
traverse(absolutePath);
|
|
1597
|
+
} else if (stat.isFile()) {
|
|
1598
|
+
entriesToHash.push({
|
|
1599
|
+
relativePath,
|
|
1600
|
+
absolutePath,
|
|
1601
|
+
kind: "file",
|
|
1602
|
+
mode: stat.mode
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
traverse(dirPath);
|
|
1608
|
+
entriesToHash.sort((a, b) => {
|
|
1609
|
+
if (a.relativePath < b.relativePath)
|
|
1610
|
+
return -1;
|
|
1611
|
+
if (a.relativePath > b.relativePath)
|
|
1612
|
+
return 1;
|
|
1613
|
+
return 0;
|
|
1614
|
+
});
|
|
1615
|
+
for (const entry of entriesToHash) {
|
|
1616
|
+
hash.update(entry.kind);
|
|
1617
|
+
hash.update("\x00");
|
|
1618
|
+
hash.update(entry.relativePath);
|
|
1619
|
+
hash.update("\x00");
|
|
1620
|
+
hash.update(String(entry.mode & 4095));
|
|
1621
|
+
hash.update("\x00");
|
|
1622
|
+
if (entry.kind === "file") {
|
|
1623
|
+
const content = readFileSync4(entry.absolutePath);
|
|
1624
|
+
hash.update(content);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return hash.digest("hex");
|
|
1628
|
+
}
|
|
1629
|
+
function isPidRunning(pid) {
|
|
1630
|
+
try {
|
|
1631
|
+
process.kill(pid, 0);
|
|
1632
|
+
return true;
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
return err.code === "EPERM";
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
var CROSS_HOST_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
1638
|
+
function acquireLock(lockDir) {
|
|
1639
|
+
const metadataPath = path2.join(lockDir, "owner.json");
|
|
1640
|
+
const currentHost = os.hostname();
|
|
1641
|
+
const currentPid = process.pid;
|
|
1642
|
+
const writeMetadata = () => {
|
|
1643
|
+
try {
|
|
1644
|
+
const metadata = {
|
|
1645
|
+
pid: currentPid,
|
|
1646
|
+
host: currentHost,
|
|
1647
|
+
time: Date.now(),
|
|
1648
|
+
token: PROCESS_TOKEN
|
|
1649
|
+
};
|
|
1650
|
+
writeFileSync2(metadataPath, JSON.stringify(metadata), "utf-8");
|
|
1651
|
+
} catch {}
|
|
1652
|
+
};
|
|
1653
|
+
try {
|
|
1654
|
+
mkdirSync3(lockDir);
|
|
1655
|
+
writeMetadata();
|
|
1656
|
+
ACQUIRED_LOCKS.add(path2.resolve(lockDir));
|
|
1657
|
+
return true;
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
if (err.code !== "EEXIST") {
|
|
1660
|
+
throw err;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
try {
|
|
1664
|
+
let shouldSteal = false;
|
|
1665
|
+
let ageMs = 0;
|
|
1666
|
+
if (existsSync4(metadataPath)) {
|
|
1667
|
+
try {
|
|
1668
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
1669
|
+
const metadata = JSON.parse(content);
|
|
1670
|
+
ageMs = Date.now() - metadata.time;
|
|
1671
|
+
if (metadata.host === currentHost) {
|
|
1672
|
+
if (!isPidRunning(metadata.pid)) {
|
|
1673
|
+
log(`[skill-sync] Lock owner process ${metadata.pid} is not running on this host. Recovery path.`);
|
|
1674
|
+
shouldSteal = true;
|
|
1675
|
+
}
|
|
1676
|
+
} else {
|
|
1677
|
+
if (ageMs > CROSS_HOST_LOCK_EXPIRY_MS) {
|
|
1678
|
+
log(`[skill-sync] Lock owned by different host ${metadata.host} has expired (${Math.round(ageMs / 1000)}s old). Reclaiming lock.`);
|
|
1679
|
+
shouldSteal = true;
|
|
1680
|
+
} else {
|
|
1681
|
+
log(`[skill-sync] Lock is owned by different host ${metadata.host}; failing closed.`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
} catch {
|
|
1685
|
+
shouldSteal = true;
|
|
1686
|
+
}
|
|
1687
|
+
} else {
|
|
1688
|
+
const stat = lstatSync(lockDir);
|
|
1689
|
+
ageMs = Date.now() - stat.mtimeMs;
|
|
1690
|
+
if (ageMs > 30000) {
|
|
1691
|
+
shouldSteal = true;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (!shouldSteal)
|
|
1695
|
+
return false;
|
|
1696
|
+
log(`[skill-sync] Stealing/recovering lock directory.`);
|
|
1697
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
1698
|
+
mkdirSync3(lockDir);
|
|
1699
|
+
writeMetadata();
|
|
1700
|
+
ACQUIRED_LOCKS.add(path2.resolve(lockDir));
|
|
1701
|
+
return true;
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
log(`[skill-sync] Failed to check/recover lock at ${lockDir}:`, err);
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function releaseLock(lockDir) {
|
|
1708
|
+
const resolvedPath = path2.resolve(lockDir);
|
|
1709
|
+
try {
|
|
1710
|
+
let isOurLock = false;
|
|
1711
|
+
const metadataPath = path2.join(lockDir, "owner.json");
|
|
1712
|
+
if (existsSync4(metadataPath)) {
|
|
1713
|
+
try {
|
|
1714
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
1715
|
+
const metadata = JSON.parse(content);
|
|
1716
|
+
if (metadata.host === os.hostname() && metadata.pid === process.pid && metadata.token === PROCESS_TOKEN) {
|
|
1717
|
+
isOurLock = true;
|
|
1718
|
+
} else {
|
|
1719
|
+
isOurLock = false;
|
|
1720
|
+
}
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
log(`[skill-sync] Lock owner.json is unreadable/corrupt:`, err);
|
|
1723
|
+
isOurLock = false;
|
|
1724
|
+
}
|
|
1725
|
+
} else if (ACQUIRED_LOCKS.has(resolvedPath)) {
|
|
1726
|
+
isOurLock = true;
|
|
1727
|
+
}
|
|
1728
|
+
if (isOurLock) {
|
|
1729
|
+
if (existsSync4(lockDir)) {
|
|
1730
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
1731
|
+
}
|
|
1732
|
+
} else if (existsSync4(lockDir)) {
|
|
1733
|
+
log(`[skill-sync] Skipping lock directory removal: lock is not owned by this process/token or owner.json check failed.`);
|
|
1734
|
+
}
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
log(`[skill-sync] Failed to release lock at ${lockDir}:`, err);
|
|
1737
|
+
} finally {
|
|
1738
|
+
ACQUIRED_LOCKS.delete(resolvedPath);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function atomicReplaceDir(sourceDir, destDir) {
|
|
1742
|
+
const parentDir = path2.dirname(destDir);
|
|
1743
|
+
if (!existsSync4(parentDir)) {
|
|
1744
|
+
mkdirSync3(parentDir, { recursive: true });
|
|
1745
|
+
}
|
|
1746
|
+
const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1747
|
+
const stagingDir = path2.join(parentDir, `.staging-${path2.basename(destDir)}-${uniqueSuffix}`);
|
|
1748
|
+
const backupDir = path2.join(parentDir, `.backup-${path2.basename(destDir)}-${uniqueSuffix}`);
|
|
1749
|
+
let backupCreated = false;
|
|
1750
|
+
try {
|
|
1751
|
+
copyDirRecursive(sourceDir, stagingDir);
|
|
1752
|
+
if (existsSync4(destDir)) {
|
|
1753
|
+
renameSync2(destDir, backupDir);
|
|
1754
|
+
backupCreated = true;
|
|
1755
|
+
}
|
|
1756
|
+
renameSync2(stagingDir, destDir);
|
|
1757
|
+
if (backupCreated) {
|
|
1758
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
1759
|
+
}
|
|
1760
|
+
} catch (err) {
|
|
1761
|
+
log(`[skill-sync] Error during atomic replace for ${destDir}. Rolling back:`, err);
|
|
1762
|
+
if (backupCreated) {
|
|
1763
|
+
try {
|
|
1764
|
+
if (existsSync4(destDir)) {
|
|
1765
|
+
rmSync2(destDir, { recursive: true, force: true });
|
|
1766
|
+
}
|
|
1767
|
+
renameSync2(backupDir, destDir);
|
|
1768
|
+
} catch (rollbackErr) {
|
|
1769
|
+
log(`[skill-sync] Critical error during rollback for ${destDir}:`, rollbackErr);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
try {
|
|
1773
|
+
if (existsSync4(stagingDir)) {
|
|
1774
|
+
rmSync2(stagingDir, { recursive: true, force: true });
|
|
1775
|
+
}
|
|
1776
|
+
} catch {}
|
|
1777
|
+
throw err;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function matchesArtifactPattern(entry, prefix, skillName) {
|
|
1781
|
+
if (!entry.startsWith(prefix))
|
|
1782
|
+
return false;
|
|
1783
|
+
const rest = entry.slice(prefix.length);
|
|
1784
|
+
if (!rest.startsWith(`${skillName}-`))
|
|
1785
|
+
return false;
|
|
1786
|
+
const suffix = rest.slice(skillName.length + 1);
|
|
1787
|
+
const firstPart = suffix.split("-")[0];
|
|
1788
|
+
const timestamp = Number(firstPart);
|
|
1789
|
+
if (Number.isNaN(timestamp) || timestamp <= 0)
|
|
1790
|
+
return false;
|
|
1791
|
+
return true;
|
|
1792
|
+
}
|
|
1793
|
+
function recoverOrphanArtifacts(destSkillsDir, skillName) {
|
|
1794
|
+
if (!existsSync4(destSkillsDir))
|
|
1795
|
+
return false;
|
|
1796
|
+
let hadArtifacts = false;
|
|
1797
|
+
let entries = [];
|
|
1798
|
+
try {
|
|
1799
|
+
entries = readdirSync(destSkillsDir);
|
|
1800
|
+
} catch {
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1803
|
+
const backups = [];
|
|
1804
|
+
const stagings = [];
|
|
1805
|
+
for (const entry of entries) {
|
|
1806
|
+
if (matchesArtifactPattern(entry, ".backup-", skillName)) {
|
|
1807
|
+
backups.push(path2.join(destSkillsDir, entry));
|
|
1808
|
+
hadArtifacts = true;
|
|
1809
|
+
} else if (matchesArtifactPattern(entry, ".staging-", skillName)) {
|
|
1810
|
+
stagings.push(path2.join(destSkillsDir, entry));
|
|
1811
|
+
hadArtifacts = true;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
const destPath = path2.join(destSkillsDir, skillName);
|
|
1815
|
+
if (backups.length > 0) {
|
|
1816
|
+
backups.sort();
|
|
1817
|
+
const mostRecentBackup = backups[backups.length - 1];
|
|
1818
|
+
if (!existsSync4(destPath)) {
|
|
1819
|
+
backups.pop();
|
|
1820
|
+
try {
|
|
1821
|
+
renameSync2(mostRecentBackup, destPath);
|
|
1822
|
+
log(`[skill-sync] Recovered backup for ${skillName} back to destination.`);
|
|
1823
|
+
} catch (err) {
|
|
1824
|
+
log(`[skill-sync] Failed to restore backup for ${skillName}:`, err);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
for (const backup of backups) {
|
|
1828
|
+
try {
|
|
1829
|
+
rmSync2(backup, { recursive: true, force: true });
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
log(`[skill-sync] Failed to clean up backup folder ${backup}:`, err);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
for (const staging of stagings) {
|
|
1836
|
+
try {
|
|
1837
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
log(`[skill-sync] Failed to clean up staging folder ${staging}:`, err);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
return hadArtifacts;
|
|
1843
|
+
}
|
|
1844
|
+
function removeManagedStagedPath(stagedPath, manifestDir, skillName) {
|
|
1845
|
+
try {
|
|
1846
|
+
const absoluteStagedPath = path2.resolve(stagedPath);
|
|
1847
|
+
const absoluteAllowedRoot = path2.resolve(path2.join(manifestDir, "skill-updates"));
|
|
1848
|
+
const relative2 = path2.relative(absoluteAllowedRoot, absoluteStagedPath);
|
|
1849
|
+
const isUnderRoot = relative2 && !relative2.startsWith("..") && !path2.isAbsolute(relative2);
|
|
1850
|
+
if (isUnderRoot) {
|
|
1851
|
+
if (existsSync4(absoluteStagedPath)) {
|
|
1852
|
+
rmSync2(absoluteStagedPath, { recursive: true, force: true });
|
|
1853
|
+
log(`[skill-sync] Safely cleaned up staged path for ${skillName}: ${absoluteStagedPath}`);
|
|
1854
|
+
}
|
|
1855
|
+
} else {
|
|
1856
|
+
log(`[skill-sync] Refusing to delete staged path for ${skillName}: path ${absoluteStagedPath} is not under managed root ${absoluteAllowedRoot}`);
|
|
1857
|
+
}
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
log(`[skill-sync] Error while trying to verify and remove staged path for ${skillName} (${stagedPath}):`, err);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
function syncBundledSkillsFromPackage(packageRoot, options = {}) {
|
|
1863
|
+
const installed = [];
|
|
1864
|
+
const skippedExisting = [];
|
|
1865
|
+
const failed = [];
|
|
1866
|
+
const staged = [];
|
|
1867
|
+
const adopted = [];
|
|
1868
|
+
const customized = [];
|
|
1869
|
+
const sourceSkillsDir = path2.join(packageRoot, "src", "skills");
|
|
1870
|
+
try {
|
|
1871
|
+
const stat = lstatSync(sourceSkillsDir);
|
|
1872
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
1873
|
+
log(`[skill-sync] Source skills directory is not a valid directory: ${sourceSkillsDir}`);
|
|
1874
|
+
return {
|
|
1875
|
+
installed,
|
|
1876
|
+
skippedExisting,
|
|
1877
|
+
failed,
|
|
1878
|
+
staged,
|
|
1879
|
+
adopted,
|
|
1880
|
+
customized
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
} catch {
|
|
1884
|
+
log(`[skill-sync] Source skills directory does not exist or is unreadable: ${sourceSkillsDir}`);
|
|
1885
|
+
return {
|
|
1886
|
+
installed,
|
|
1887
|
+
skippedExisting,
|
|
1888
|
+
failed,
|
|
1889
|
+
staged,
|
|
1890
|
+
adopted,
|
|
1891
|
+
customized
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
let packageVersion = "unknown";
|
|
1895
|
+
try {
|
|
1896
|
+
const pkgJsonPath = path2.join(packageRoot, "package.json");
|
|
1897
|
+
if (existsSync4(pkgJsonPath)) {
|
|
1898
|
+
const content = readFileSync4(pkgJsonPath, "utf-8");
|
|
1899
|
+
const pkg = JSON.parse(content);
|
|
1900
|
+
if (pkg.version) {
|
|
1901
|
+
packageVersion = pkg.version;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
} catch (err) {
|
|
1905
|
+
log(`[skill-sync] Failed to read package version from ${packageRoot}:`, err);
|
|
1906
|
+
}
|
|
1907
|
+
const manifestDir = path2.join(getConfigDir(), ".oh-my-opencode-slim");
|
|
1908
|
+
const lockDir = path2.join(manifestDir, "skills.lock");
|
|
1909
|
+
try {
|
|
1910
|
+
mkdirSync3(manifestDir, { recursive: true });
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
log(`[skill-sync] Failed to create manifest directory: ${manifestDir}`, err);
|
|
1913
|
+
}
|
|
1914
|
+
if (!acquireLock(lockDir)) {
|
|
1915
|
+
log("[skill-sync] Failed to acquire lock for skill synchronization. Skipping.");
|
|
1916
|
+
return {
|
|
1917
|
+
installed,
|
|
1918
|
+
skippedExisting,
|
|
1919
|
+
failed: ["__lock__"],
|
|
1920
|
+
staged,
|
|
1921
|
+
adopted,
|
|
1922
|
+
customized
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
try {
|
|
1926
|
+
const manifestPath = path2.join(manifestDir, "skills-manifest.json");
|
|
1927
|
+
let manifest = {
|
|
1928
|
+
schemaVersion: 1,
|
|
1929
|
+
updatedAt: new Date().toISOString(),
|
|
1930
|
+
skills: {}
|
|
1931
|
+
};
|
|
1932
|
+
let isManifestCorrupt = false;
|
|
1933
|
+
if (existsSync4(manifestPath)) {
|
|
1934
|
+
try {
|
|
1935
|
+
const content = readFileSync4(manifestPath, "utf-8");
|
|
1936
|
+
const parsed = JSON.parse(content);
|
|
1937
|
+
if (validateManifest(parsed)) {
|
|
1938
|
+
manifest = parsed;
|
|
1939
|
+
} else {
|
|
1940
|
+
throw new Error("Manifest validation failed");
|
|
1941
|
+
}
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
log("[skill-sync] Manifest is corrupt/unreadable. Failing closed.", err);
|
|
1944
|
+
isManifestCorrupt = true;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const destSkillsDir = path2.join(getConfigDir(), "skills");
|
|
1948
|
+
try {
|
|
1949
|
+
if (!existsSync4(destSkillsDir)) {
|
|
1950
|
+
mkdirSync3(destSkillsDir, { recursive: true });
|
|
1951
|
+
}
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
log(`[skill-sync] Failed to create destination skills directory: ${destSkillsDir}`, err);
|
|
1954
|
+
}
|
|
1955
|
+
const skillsToProcess = (options.skills ?? CUSTOM_SKILLS).map((s) => ({
|
|
1956
|
+
name: s.name,
|
|
1957
|
+
sourcePath: s.sourcePath
|
|
1958
|
+
}));
|
|
1959
|
+
for (const skill of skillsToProcess) {
|
|
1960
|
+
try {
|
|
1961
|
+
const sourcePath = path2.join(packageRoot, skill.sourcePath);
|
|
1962
|
+
try {
|
|
1963
|
+
const stat = lstatSync(sourcePath);
|
|
1964
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
const skillMdPath = path2.join(sourcePath, "SKILL.md");
|
|
1968
|
+
const skillMdStat = lstatSync(skillMdPath);
|
|
1969
|
+
if (skillMdStat.isSymbolicLink() || !skillMdStat.isFile()) {
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
} catch {
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
const destPath = path2.join(destSkillsDir, skill.name);
|
|
1976
|
+
const hadArtifacts = recoverOrphanArtifacts(destSkillsDir, skill.name);
|
|
1977
|
+
let destExists = false;
|
|
1978
|
+
let destIsDir = false;
|
|
1979
|
+
try {
|
|
1980
|
+
const destStat = lstatSync(destPath);
|
|
1981
|
+
destExists = true;
|
|
1982
|
+
destIsDir = destStat.isDirectory() && !destStat.isSymbolicLink();
|
|
1983
|
+
} catch {}
|
|
1984
|
+
if (destExists && !destIsDir) {
|
|
1985
|
+
log(`[skill-sync] Skill ${skill.name} destination is a file or symlink (conflict). Skipping.`);
|
|
1986
|
+
skippedExisting.push(skill.name);
|
|
1987
|
+
const sourceHash2 = computeDirectoryHash(sourcePath);
|
|
1988
|
+
const entry2 = manifest.skills[skill.name];
|
|
1989
|
+
if (entry2?.stagedPath) {
|
|
1990
|
+
removeManagedStagedPath(entry2.stagedPath, manifestDir, skill.name);
|
|
1991
|
+
}
|
|
1992
|
+
manifest.skills[skill.name] = {
|
|
1993
|
+
status: "conflict",
|
|
1994
|
+
packageVersion,
|
|
1995
|
+
sourceHash: sourceHash2,
|
|
1996
|
+
lastManagedHash: "",
|
|
1997
|
+
lastSeenHash: "",
|
|
1998
|
+
updatedAt: new Date().toISOString()
|
|
1999
|
+
};
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
const sourceHash = computeDirectoryHash(sourcePath);
|
|
2003
|
+
if (isManifestCorrupt) {
|
|
2004
|
+
if (!destExists) {
|
|
2005
|
+
try {
|
|
2006
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2007
|
+
installed.push(skill.name);
|
|
2008
|
+
manifest.skills[skill.name] = {
|
|
2009
|
+
status: "managed",
|
|
2010
|
+
packageVersion,
|
|
2011
|
+
sourceHash,
|
|
2012
|
+
lastManagedHash: sourceHash,
|
|
2013
|
+
lastSeenHash: sourceHash,
|
|
2014
|
+
updatedAt: new Date().toISOString()
|
|
2015
|
+
};
|
|
2016
|
+
} catch (err) {
|
|
2017
|
+
log(`[skill-sync] Failed to install missing skill ${skill.name} (corrupt manifest mode):`, err);
|
|
2018
|
+
failed.push(skill.name);
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
log(`[skill-sync] Skipping existing skill ${skill.name} because manifest is corrupt.`);
|
|
2022
|
+
skippedExisting.push(skill.name);
|
|
2023
|
+
const destHash2 = computeDirectoryHash(destPath);
|
|
2024
|
+
if (destHash2 === sourceHash) {
|
|
2025
|
+
manifest.skills[skill.name] = {
|
|
2026
|
+
status: "managed",
|
|
2027
|
+
packageVersion,
|
|
2028
|
+
sourceHash,
|
|
2029
|
+
lastManagedHash: sourceHash,
|
|
2030
|
+
lastSeenHash: sourceHash,
|
|
2031
|
+
updatedAt: new Date().toISOString()
|
|
2032
|
+
};
|
|
2033
|
+
} else {
|
|
2034
|
+
try {
|
|
2035
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2036
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2037
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2038
|
+
}
|
|
2039
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2040
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2041
|
+
manifest.skills[skill.name] = {
|
|
2042
|
+
status: "customized",
|
|
2043
|
+
packageVersion,
|
|
2044
|
+
sourceHash,
|
|
2045
|
+
lastManagedHash: "",
|
|
2046
|
+
lastSeenHash: destHash2,
|
|
2047
|
+
stagedPath: stagedSkillDir,
|
|
2048
|
+
updatedAt: new Date().toISOString()
|
|
2049
|
+
};
|
|
2050
|
+
staged.push(skill.name);
|
|
2051
|
+
customized.push(skill.name);
|
|
2052
|
+
} catch (err) {
|
|
2053
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name} during recovery:`, err);
|
|
2054
|
+
manifest.skills[skill.name] = {
|
|
2055
|
+
status: "customized",
|
|
2056
|
+
packageVersion: "unknown",
|
|
2057
|
+
sourceHash: "",
|
|
2058
|
+
lastManagedHash: "",
|
|
2059
|
+
lastSeenHash: destHash2,
|
|
2060
|
+
updatedAt: new Date().toISOString()
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
const entry = manifest.skills[skill.name];
|
|
2068
|
+
if (!destExists) {
|
|
2069
|
+
if (entry && entry.status === "deleted") {
|
|
2070
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user. Skipping.`);
|
|
2071
|
+
skippedExisting.push(skill.name);
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
if (entry && entry.status !== "deleted") {
|
|
2075
|
+
if (hadArtifacts) {
|
|
2076
|
+
log(`[skill-sync] Managed skill ${skill.name} has backup/staging artifacts. Skipping delete, re-installing.`);
|
|
2077
|
+
try {
|
|
2078
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2079
|
+
installed.push(skill.name);
|
|
2080
|
+
manifest.skills[skill.name] = {
|
|
2081
|
+
status: "managed",
|
|
2082
|
+
packageVersion,
|
|
2083
|
+
sourceHash,
|
|
2084
|
+
lastManagedHash: sourceHash,
|
|
2085
|
+
lastSeenHash: sourceHash,
|
|
2086
|
+
updatedAt: new Date().toISOString()
|
|
2087
|
+
};
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
log(`[skill-sync] Failed to re-install skill ${skill.name}:`, err);
|
|
2090
|
+
failed.push(skill.name);
|
|
2091
|
+
}
|
|
2092
|
+
continue;
|
|
2093
|
+
} else {
|
|
2094
|
+
if (entry.stagedPath) {
|
|
2095
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2096
|
+
delete entry.stagedPath;
|
|
2097
|
+
}
|
|
2098
|
+
const rawEntry = entry;
|
|
2099
|
+
delete rawEntry.stagedVersion;
|
|
2100
|
+
delete rawEntry.stagedHash;
|
|
2101
|
+
entry.status = "deleted";
|
|
2102
|
+
entry.updatedAt = new Date().toISOString();
|
|
2103
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user (detected now). Skipping.`);
|
|
2104
|
+
skippedExisting.push(skill.name);
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
try {
|
|
2109
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2110
|
+
installed.push(skill.name);
|
|
2111
|
+
manifest.skills[skill.name] = {
|
|
2112
|
+
status: "managed",
|
|
2113
|
+
packageVersion,
|
|
2114
|
+
sourceHash,
|
|
2115
|
+
lastManagedHash: sourceHash,
|
|
2116
|
+
lastSeenHash: sourceHash,
|
|
2117
|
+
updatedAt: new Date().toISOString()
|
|
2118
|
+
};
|
|
2119
|
+
log(`[skill-sync] Successfully installed missing skill: ${skill.name}`);
|
|
2120
|
+
} catch (err) {
|
|
2121
|
+
log(`[skill-sync] Failed to install skill ${skill.name}:`, err);
|
|
2122
|
+
failed.push(skill.name);
|
|
2123
|
+
}
|
|
2124
|
+
continue;
|
|
2125
|
+
}
|
|
2126
|
+
const destHash = computeDirectoryHash(destPath);
|
|
2127
|
+
if (entry) {
|
|
2128
|
+
if (entry.status === "managed") {
|
|
2129
|
+
if (destHash === entry.lastManagedHash) {
|
|
2130
|
+
if (destHash === sourceHash) {
|
|
2131
|
+
entry.packageVersion = packageVersion;
|
|
2132
|
+
entry.sourceHash = sourceHash;
|
|
2133
|
+
entry.lastManagedHash = sourceHash;
|
|
2134
|
+
entry.lastSeenHash = sourceHash;
|
|
2135
|
+
entry.updatedAt = new Date().toISOString();
|
|
2136
|
+
skippedExisting.push(skill.name);
|
|
2137
|
+
} else {
|
|
2138
|
+
try {
|
|
2139
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2140
|
+
installed.push(skill.name);
|
|
2141
|
+
manifest.skills[skill.name] = {
|
|
2142
|
+
status: "managed",
|
|
2143
|
+
packageVersion,
|
|
2144
|
+
sourceHash,
|
|
2145
|
+
lastManagedHash: sourceHash,
|
|
2146
|
+
lastSeenHash: sourceHash,
|
|
2147
|
+
updatedAt: new Date().toISOString()
|
|
2148
|
+
};
|
|
2149
|
+
log(`[skill-sync] Updated managed skill: ${skill.name}`);
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
log(`[skill-sync] Failed to update managed skill ${skill.name}:`, err);
|
|
2152
|
+
failed.push(skill.name);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
} else {
|
|
2156
|
+
if (destHash === sourceHash) {
|
|
2157
|
+
manifest.skills[skill.name] = {
|
|
2158
|
+
status: "managed",
|
|
2159
|
+
packageVersion,
|
|
2160
|
+
sourceHash,
|
|
2161
|
+
lastManagedHash: sourceHash,
|
|
2162
|
+
lastSeenHash: sourceHash,
|
|
2163
|
+
updatedAt: new Date().toISOString()
|
|
2164
|
+
};
|
|
2165
|
+
skippedExisting.push(skill.name);
|
|
2166
|
+
} else {
|
|
2167
|
+
try {
|
|
2168
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2169
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2170
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2171
|
+
}
|
|
2172
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2173
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2174
|
+
}
|
|
2175
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2176
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2177
|
+
entry.status = "customized";
|
|
2178
|
+
entry.lastSeenHash = destHash;
|
|
2179
|
+
entry.stagedPath = stagedSkillDir;
|
|
2180
|
+
entry.sourceHash = sourceHash;
|
|
2181
|
+
entry.packageVersion = packageVersion;
|
|
2182
|
+
entry.updatedAt = new Date().toISOString();
|
|
2183
|
+
staged.push(skill.name);
|
|
2184
|
+
customized.push(skill.name);
|
|
2185
|
+
skippedExisting.push(skill.name);
|
|
2186
|
+
log(`[skill-sync] Skill ${skill.name} is customized. Staged update at ${stagedSkillDir}`);
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2189
|
+
failed.push(skill.name);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
} else if (entry.status === "customized") {
|
|
2194
|
+
if (destHash === sourceHash) {
|
|
2195
|
+
if (entry.stagedPath) {
|
|
2196
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2197
|
+
}
|
|
2198
|
+
entry.status = "managed";
|
|
2199
|
+
entry.lastManagedHash = sourceHash;
|
|
2200
|
+
entry.lastSeenHash = sourceHash;
|
|
2201
|
+
entry.sourceHash = sourceHash;
|
|
2202
|
+
entry.packageVersion = packageVersion;
|
|
2203
|
+
delete entry.stagedPath;
|
|
2204
|
+
entry.updatedAt = new Date().toISOString();
|
|
2205
|
+
adopted.push(skill.name);
|
|
2206
|
+
skippedExisting.push(skill.name);
|
|
2207
|
+
log(`[skill-sync] Customized skill ${skill.name} converged with current version. Adopted back to managed.`);
|
|
2208
|
+
} else {
|
|
2209
|
+
entry.lastSeenHash = destHash;
|
|
2210
|
+
entry.updatedAt = new Date().toISOString();
|
|
2211
|
+
if (destHash !== sourceHash && entry.sourceHash !== sourceHash) {
|
|
2212
|
+
try {
|
|
2213
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2214
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2215
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2216
|
+
}
|
|
2217
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2218
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2219
|
+
}
|
|
2220
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2221
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2222
|
+
entry.stagedPath = stagedSkillDir;
|
|
2223
|
+
entry.sourceHash = sourceHash;
|
|
2224
|
+
entry.packageVersion = packageVersion;
|
|
2225
|
+
staged.push(skill.name);
|
|
2226
|
+
customized.push(skill.name);
|
|
2227
|
+
skippedExisting.push(skill.name);
|
|
2228
|
+
log(`[skill-sync] Staged new update for customized skill ${skill.name} at ${stagedSkillDir}`);
|
|
2229
|
+
} catch (err) {
|
|
2230
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2231
|
+
failed.push(skill.name);
|
|
2232
|
+
}
|
|
2233
|
+
} else {
|
|
2234
|
+
customized.push(skill.name);
|
|
2235
|
+
skippedExisting.push(skill.name);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
} else if (entry.status === "deleted") {
|
|
2239
|
+
if (destHash === sourceHash) {
|
|
2240
|
+
entry.status = "managed";
|
|
2241
|
+
entry.packageVersion = packageVersion;
|
|
2242
|
+
entry.sourceHash = sourceHash;
|
|
2243
|
+
entry.lastManagedHash = sourceHash;
|
|
2244
|
+
entry.lastSeenHash = sourceHash;
|
|
2245
|
+
entry.updatedAt = new Date().toISOString();
|
|
2246
|
+
skippedExisting.push(skill.name);
|
|
2247
|
+
adopted.push(skill.name);
|
|
2248
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (matching current). Adopted as managed.`);
|
|
2249
|
+
} else {
|
|
2250
|
+
try {
|
|
2251
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2252
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2253
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2254
|
+
}
|
|
2255
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2256
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2257
|
+
}
|
|
2258
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2259
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2260
|
+
entry.status = "customized";
|
|
2261
|
+
entry.packageVersion = packageVersion;
|
|
2262
|
+
entry.sourceHash = sourceHash;
|
|
2263
|
+
entry.lastManagedHash = sourceHash;
|
|
2264
|
+
entry.lastSeenHash = destHash;
|
|
2265
|
+
entry.stagedPath = stagedSkillDir;
|
|
2266
|
+
entry.updatedAt = new Date().toISOString();
|
|
2267
|
+
staged.push(skill.name);
|
|
2268
|
+
customized.push(skill.name);
|
|
2269
|
+
skippedExisting.push(skill.name);
|
|
2270
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (custom). Marked customized and staged.`);
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
log(`[skill-sync] Failed to stage update for deleted/recreated skill ${skill.name}:`, err);
|
|
2273
|
+
failed.push(skill.name);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
} else if (entry.status === "conflict") {
|
|
2277
|
+
if (destHash === sourceHash) {
|
|
2278
|
+
if (entry.stagedPath) {
|
|
2279
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2280
|
+
}
|
|
2281
|
+
entry.status = "managed";
|
|
2282
|
+
entry.packageVersion = packageVersion;
|
|
2283
|
+
entry.sourceHash = sourceHash;
|
|
2284
|
+
entry.lastManagedHash = sourceHash;
|
|
2285
|
+
entry.lastSeenHash = sourceHash;
|
|
2286
|
+
delete entry.stagedPath;
|
|
2287
|
+
entry.updatedAt = new Date().toISOString();
|
|
2288
|
+
adopted.push(skill.name);
|
|
2289
|
+
} else {
|
|
2290
|
+
try {
|
|
2291
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2292
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2293
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2294
|
+
}
|
|
2295
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2296
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2297
|
+
}
|
|
2298
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2299
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2300
|
+
entry.status = "customized";
|
|
2301
|
+
entry.packageVersion = packageVersion;
|
|
2302
|
+
entry.sourceHash = sourceHash;
|
|
2303
|
+
entry.lastManagedHash = sourceHash;
|
|
2304
|
+
entry.lastSeenHash = destHash;
|
|
2305
|
+
entry.stagedPath = stagedSkillDir;
|
|
2306
|
+
entry.updatedAt = new Date().toISOString();
|
|
2307
|
+
staged.push(skill.name);
|
|
2308
|
+
customized.push(skill.name);
|
|
2309
|
+
skippedExisting.push(skill.name);
|
|
2310
|
+
log(`[skill-sync] Conflicted skill ${skill.name} recovered as customized and staged at ${stagedSkillDir}`);
|
|
2311
|
+
} catch (err) {
|
|
2312
|
+
log(`[skill-sync] Failed to stage update for conflicted skill ${skill.name}:`, err);
|
|
2313
|
+
failed.push(skill.name);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
} else {
|
|
2318
|
+
if (destHash === sourceHash) {
|
|
2319
|
+
manifest.skills[skill.name] = {
|
|
2320
|
+
status: "managed",
|
|
2321
|
+
packageVersion,
|
|
2322
|
+
sourceHash,
|
|
2323
|
+
lastManagedHash: sourceHash,
|
|
2324
|
+
lastSeenHash: sourceHash,
|
|
2325
|
+
updatedAt: new Date().toISOString()
|
|
2326
|
+
};
|
|
2327
|
+
skippedExisting.push(skill.name);
|
|
2328
|
+
adopted.push(skill.name);
|
|
2329
|
+
log(`[skill-sync] Adopted existing matching skill: ${skill.name}`);
|
|
2330
|
+
} else if (LEGACY_MANAGED_SKILL_HASHES[skill.name]?.includes(destHash)) {
|
|
2331
|
+
try {
|
|
2332
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2333
|
+
installed.push(skill.name);
|
|
2334
|
+
manifest.skills[skill.name] = {
|
|
2335
|
+
status: "managed",
|
|
2336
|
+
packageVersion,
|
|
2337
|
+
sourceHash,
|
|
2338
|
+
lastManagedHash: sourceHash,
|
|
2339
|
+
lastSeenHash: sourceHash,
|
|
2340
|
+
updatedAt: new Date().toISOString()
|
|
2341
|
+
};
|
|
2342
|
+
log(`[skill-sync] Adopted and updated legacy skill: ${skill.name}`);
|
|
2343
|
+
} catch (err) {
|
|
2344
|
+
log(`[skill-sync] Failed to update legacy skill ${skill.name}:`, err);
|
|
2345
|
+
failed.push(skill.name);
|
|
2346
|
+
}
|
|
2347
|
+
} else {
|
|
2348
|
+
try {
|
|
2349
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2350
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2351
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2352
|
+
}
|
|
2353
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2354
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2355
|
+
manifest.skills[skill.name] = {
|
|
2356
|
+
status: "customized",
|
|
2357
|
+
packageVersion,
|
|
2358
|
+
sourceHash,
|
|
2359
|
+
lastManagedHash: "",
|
|
2360
|
+
lastSeenHash: destHash,
|
|
2361
|
+
stagedPath: stagedSkillDir,
|
|
2362
|
+
updatedAt: new Date().toISOString()
|
|
2363
|
+
};
|
|
2364
|
+
staged.push(skill.name);
|
|
2365
|
+
customized.push(skill.name);
|
|
2366
|
+
skippedExisting.push(skill.name);
|
|
2367
|
+
log(`[skill-sync] Skill ${skill.name} is customized (no manifest entry). Staged update at ${stagedSkillDir}`);
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2370
|
+
failed.push(skill.name);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
log(`[skill-sync] Failed processing skill ${skill.name}:`, err);
|
|
2376
|
+
failed.push(skill.name);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
let manifestWriteFailed = false;
|
|
2380
|
+
manifest.updatedAt = new Date().toISOString();
|
|
2381
|
+
const tempManifestPath = `${manifestPath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
|
|
2382
|
+
try {
|
|
2383
|
+
writeFileSync2(tempManifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2384
|
+
renameSync2(tempManifestPath, manifestPath);
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
log("[skill-sync] Failed to write skills manifest atomically:", err);
|
|
2387
|
+
manifestWriteFailed = true;
|
|
2388
|
+
try {
|
|
2389
|
+
if (existsSync4(tempManifestPath)) {
|
|
2390
|
+
unlinkSync(tempManifestPath);
|
|
2391
|
+
}
|
|
2392
|
+
} catch {}
|
|
2393
|
+
}
|
|
2394
|
+
if (manifestWriteFailed) {
|
|
2395
|
+
failed.push("__manifest__");
|
|
2396
|
+
}
|
|
2397
|
+
} finally {
|
|
2398
|
+
releaseLock(lockDir);
|
|
2399
|
+
}
|
|
2400
|
+
return {
|
|
2401
|
+
installed,
|
|
2402
|
+
skippedExisting,
|
|
2403
|
+
failed,
|
|
2404
|
+
staged,
|
|
2405
|
+
adopted,
|
|
2406
|
+
customized
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
1513
2409
|
|
|
1514
2410
|
// src/cli/background-subagents.ts
|
|
1515
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as
|
|
2411
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1516
2412
|
import { homedir as homedir3 } from "node:os";
|
|
1517
2413
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
1518
2414
|
var ENV_NAME = "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS";
|
|
@@ -1601,57 +2497,30 @@ function upsertBackgroundSubagentsBlock(content, block) {
|
|
|
1601
2497
|
}
|
|
1602
2498
|
function writeBackgroundSubagentsBlock(targetPath) {
|
|
1603
2499
|
const block = getBackgroundSubagentsBlock(targetPath);
|
|
1604
|
-
const content = existsSync5(targetPath) ?
|
|
2500
|
+
const content = existsSync5(targetPath) ? readFileSync5(targetPath, "utf8") : "";
|
|
1605
2501
|
const nextContent = upsertBackgroundSubagentsBlock(content, block);
|
|
1606
2502
|
mkdirSync4(dirname4(targetPath), { recursive: true });
|
|
1607
|
-
|
|
2503
|
+
writeFileSync3(targetPath, nextContent);
|
|
1608
2504
|
}
|
|
1609
2505
|
|
|
1610
2506
|
// src/companion/updater.ts
|
|
1611
2507
|
init_compat();
|
|
1612
|
-
import { createHash } from "node:crypto";
|
|
2508
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1613
2509
|
import {
|
|
1614
2510
|
chmodSync,
|
|
1615
2511
|
copyFileSync as copyFileSync3,
|
|
1616
2512
|
existsSync as existsSync6,
|
|
1617
2513
|
mkdirSync as mkdirSync5,
|
|
1618
2514
|
mkdtempSync,
|
|
1619
|
-
readFileSync as
|
|
1620
|
-
renameSync as
|
|
1621
|
-
rmSync as
|
|
1622
|
-
statSync as
|
|
1623
|
-
writeFileSync as
|
|
2515
|
+
readFileSync as readFileSync6,
|
|
2516
|
+
renameSync as renameSync3,
|
|
2517
|
+
rmSync as rmSync3,
|
|
2518
|
+
statSync as statSync3,
|
|
2519
|
+
writeFileSync as writeFileSync4
|
|
1624
2520
|
} from "node:fs";
|
|
1625
2521
|
import { homedir as homedir4, platform, tmpdir } from "node:os";
|
|
1626
|
-
import * as
|
|
2522
|
+
import * as path3 from "node:path";
|
|
1627
2523
|
import { setTimeout as delay } from "node:timers/promises";
|
|
1628
|
-
|
|
1629
|
-
// src/utils/logger.ts
|
|
1630
|
-
import { appendFile } from "node:fs/promises";
|
|
1631
|
-
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1632
|
-
var logFile = null;
|
|
1633
|
-
var writeChain = Promise.resolve();
|
|
1634
|
-
function log(message, data) {
|
|
1635
|
-
const target = logFile;
|
|
1636
|
-
if (!target)
|
|
1637
|
-
return;
|
|
1638
|
-
try {
|
|
1639
|
-
const timestamp = new Date().toISOString();
|
|
1640
|
-
let dataStr = "";
|
|
1641
|
-
if (data !== undefined) {
|
|
1642
|
-
try {
|
|
1643
|
-
dataStr = JSON.stringify(data);
|
|
1644
|
-
} catch {
|
|
1645
|
-
dataStr = "[unserializable]";
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
1649
|
-
`;
|
|
1650
|
-
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
1651
|
-
} catch {}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
// src/companion/updater.ts
|
|
1655
2524
|
var DOWNLOAD_TIMEOUT_MS = 30000;
|
|
1656
2525
|
var LOCK_TIMEOUT_MS = 2000;
|
|
1657
2526
|
var STALE_LOCK_MS = 5 * 60000;
|
|
@@ -1689,8 +2558,8 @@ function getCompanionTarget() {
|
|
|
1689
2558
|
}
|
|
1690
2559
|
function getCompanionBinaryPath() {
|
|
1691
2560
|
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
1692
|
-
const base = xdg &&
|
|
1693
|
-
return
|
|
2561
|
+
const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(homedir4(), ".local", "share");
|
|
2562
|
+
return path3.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
|
|
1694
2563
|
}
|
|
1695
2564
|
async function ensureCompanionVersion(options) {
|
|
1696
2565
|
const { config, dryRun = false } = options;
|
|
@@ -1787,7 +2656,7 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1787
2656
|
} finally {
|
|
1788
2657
|
clearTimeout(timeout);
|
|
1789
2658
|
}
|
|
1790
|
-
const checksum =
|
|
2659
|
+
const checksum = createHash2("sha256").update(Buffer.from(buffer)).digest("hex");
|
|
1791
2660
|
if (checksum !== expectedChecksum) {
|
|
1792
2661
|
return {
|
|
1793
2662
|
status: "failed",
|
|
@@ -1797,10 +2666,10 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1797
2666
|
}
|
|
1798
2667
|
let tempDir = "";
|
|
1799
2668
|
try {
|
|
1800
|
-
tempDir = mkdtempSync(
|
|
1801
|
-
const archivePath =
|
|
1802
|
-
|
|
1803
|
-
const extractedDir =
|
|
2669
|
+
tempDir = mkdtempSync(path3.join(tmpdir(), "companion-install-"));
|
|
2670
|
+
const archivePath = path3.join(tempDir, archiveName);
|
|
2671
|
+
writeFileSync4(archivePath, Buffer.from(buffer));
|
|
2672
|
+
const extractedDir = path3.join(tempDir, "extracted");
|
|
1804
2673
|
mkdirSync5(extractedDir, { recursive: true });
|
|
1805
2674
|
if (isWindows) {
|
|
1806
2675
|
const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
|
|
@@ -1818,7 +2687,7 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1818
2687
|
}
|
|
1819
2688
|
}
|
|
1820
2689
|
const binaryName = isWindows ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion";
|
|
1821
|
-
const extractedBinaryPath =
|
|
2690
|
+
const extractedBinaryPath = path3.join(extractedDir, binaryName);
|
|
1822
2691
|
if (!existsSync6(extractedBinaryPath)) {
|
|
1823
2692
|
return {
|
|
1824
2693
|
status: "failed",
|
|
@@ -1826,14 +2695,14 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1826
2695
|
error: `Binary ${binaryName} not found in extracted archive`
|
|
1827
2696
|
};
|
|
1828
2697
|
}
|
|
1829
|
-
const binDir =
|
|
2698
|
+
const binDir = path3.dirname(finalBinaryPath);
|
|
1830
2699
|
mkdirSync5(binDir, { recursive: true });
|
|
1831
2700
|
const tmpFinalPath = `${finalBinaryPath}.tmp`;
|
|
1832
2701
|
copyFileSync3(extractedBinaryPath, tmpFinalPath);
|
|
1833
2702
|
if (!isWindows) {
|
|
1834
2703
|
chmodSync(tmpFinalPath, 493);
|
|
1835
2704
|
}
|
|
1836
|
-
|
|
2705
|
+
renameSync3(tmpFinalPath, finalBinaryPath);
|
|
1837
2706
|
writeInstallMetadata(finalBinaryPath, {
|
|
1838
2707
|
version: manifest.version,
|
|
1839
2708
|
tag: manifest.tag,
|
|
@@ -1856,14 +2725,14 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1856
2725
|
} finally {
|
|
1857
2726
|
if (tempDir) {
|
|
1858
2727
|
try {
|
|
1859
|
-
|
|
2728
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
1860
2729
|
} catch {}
|
|
1861
2730
|
}
|
|
1862
2731
|
}
|
|
1863
2732
|
}
|
|
1864
2733
|
function readInstallMetadata(binaryPath) {
|
|
1865
2734
|
try {
|
|
1866
|
-
const parsed = JSON.parse(
|
|
2735
|
+
const parsed = JSON.parse(readFileSync6(metadataPath(binaryPath), "utf8"));
|
|
1867
2736
|
if (parsed?.version && parsed.tag && parsed.target) {
|
|
1868
2737
|
return parsed;
|
|
1869
2738
|
}
|
|
@@ -1871,7 +2740,7 @@ function readInstallMetadata(binaryPath) {
|
|
|
1871
2740
|
return null;
|
|
1872
2741
|
}
|
|
1873
2742
|
function writeInstallMetadata(binaryPath, metadata) {
|
|
1874
|
-
|
|
2743
|
+
writeFileSync4(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
|
|
1875
2744
|
}
|
|
1876
2745
|
function metadataPath(binaryPath) {
|
|
1877
2746
|
return `${binaryPath}.json`;
|
|
@@ -1880,7 +2749,7 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
|
|
|
1880
2749
|
const lock = `${binaryPath}.lock`;
|
|
1881
2750
|
const deadline = Date.now() + (timeoutMs ?? LOCK_TIMEOUT_MS);
|
|
1882
2751
|
const staleAfterMs = staleMs ?? STALE_LOCK_MS;
|
|
1883
|
-
mkdirSync5(
|
|
2752
|
+
mkdirSync5(path3.dirname(binaryPath), { recursive: true });
|
|
1884
2753
|
while (Date.now() <= deadline) {
|
|
1885
2754
|
try {
|
|
1886
2755
|
mkdirSync5(lock);
|
|
@@ -1888,7 +2757,7 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
|
|
|
1888
2757
|
return await run();
|
|
1889
2758
|
} finally {
|
|
1890
2759
|
try {
|
|
1891
|
-
|
|
2760
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
1892
2761
|
} catch {}
|
|
1893
2762
|
}
|
|
1894
2763
|
} catch (err) {
|
|
@@ -1896,9 +2765,9 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
|
|
|
1896
2765
|
if (code !== "EEXIST")
|
|
1897
2766
|
throw err;
|
|
1898
2767
|
try {
|
|
1899
|
-
const ageMs = Date.now() -
|
|
2768
|
+
const ageMs = Date.now() - statSync3(lock).mtimeMs;
|
|
1900
2769
|
if (ageMs > staleAfterMs) {
|
|
1901
|
-
|
|
2770
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
1902
2771
|
log("[companion] removed stale install lock", lock);
|
|
1903
2772
|
continue;
|
|
1904
2773
|
}
|
|
@@ -1985,7 +2854,7 @@ async function installCompanion(config) {
|
|
|
1985
2854
|
// src/cli/system.ts
|
|
1986
2855
|
init_compat();
|
|
1987
2856
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1988
|
-
import { statSync as
|
|
2857
|
+
import { statSync as statSync4 } from "node:fs";
|
|
1989
2858
|
var cachedOpenCodePath = null;
|
|
1990
2859
|
function resolvePathCommand(command) {
|
|
1991
2860
|
try {
|
|
@@ -2058,7 +2927,7 @@ function resolveOpenCodePath() {
|
|
|
2058
2927
|
if (opencodePath === "opencode")
|
|
2059
2928
|
continue;
|
|
2060
2929
|
try {
|
|
2061
|
-
const stat =
|
|
2930
|
+
const stat = statSync4(opencodePath);
|
|
2062
2931
|
if (stat.isFile()) {
|
|
2063
2932
|
cachedOpenCodePath = opencodePath;
|
|
2064
2933
|
return opencodePath;
|
|
@@ -2107,8 +2976,8 @@ async function getOpenCodeVersion() {
|
|
|
2107
2976
|
return null;
|
|
2108
2977
|
}
|
|
2109
2978
|
function getOpenCodePath() {
|
|
2110
|
-
const
|
|
2111
|
-
return
|
|
2979
|
+
const path4 = resolveOpenCodePath();
|
|
2980
|
+
return path4 === "opencode" ? null : path4;
|
|
2112
2981
|
}
|
|
2113
2982
|
// src/cli/install.ts
|
|
2114
2983
|
var GREEN = "\x1B[32m";
|
|
@@ -2191,11 +3060,11 @@ async function checkOpenCodeInstalled() {
|
|
|
2191
3060
|
return { ok: false };
|
|
2192
3061
|
}
|
|
2193
3062
|
const version = await getOpenCodeVersion();
|
|
2194
|
-
const
|
|
3063
|
+
const path4 = getOpenCodePath();
|
|
2195
3064
|
const detectedVersion = version ?? "";
|
|
2196
|
-
const pathInfo =
|
|
3065
|
+
const pathInfo = path4 ? ` (${DIM}${path4}${RESET})` : "";
|
|
2197
3066
|
printSuccess(`OpenCode ${detectedVersion} detected${pathInfo}`);
|
|
2198
|
-
return { ok: true, version: version ?? undefined, path:
|
|
3067
|
+
return { ok: true, version: version ?? undefined, path: path4 ?? undefined };
|
|
2199
3068
|
}
|
|
2200
3069
|
async function configureBackgroundSubagents(config) {
|
|
2201
3070
|
if (isBackgroundSubagentsEnabled(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS)) {
|
|
@@ -2257,9 +3126,9 @@ async function shouldInstallCompanion(config) {
|
|
|
2257
3126
|
if (config.companion === "no")
|
|
2258
3127
|
return false;
|
|
2259
3128
|
if (config.dryRun) {
|
|
2260
|
-
printInfo("Dry run mode - would ask to install the desktop companion (default:
|
|
2261
|
-
config.companion = "
|
|
2262
|
-
return
|
|
3129
|
+
printInfo("Dry run mode - would ask to install the desktop companion (default: no).");
|
|
3130
|
+
config.companion = "no";
|
|
3131
|
+
return false;
|
|
2263
3132
|
}
|
|
2264
3133
|
if (!process.stdin.isTTY) {
|
|
2265
3134
|
printInfo("Skipped desktop companion prompt in non-TTY mode. Use --companion=yes to install it.");
|
|
@@ -2268,7 +3137,7 @@ async function shouldInstallCompanion(config) {
|
|
|
2268
3137
|
}
|
|
2269
3138
|
console.log();
|
|
2270
3139
|
printInfo("The optional desktop companion shows live agent activity.");
|
|
2271
|
-
const shouldInstall = await confirm("Install and enable the desktop companion?",
|
|
3140
|
+
const shouldInstall = await confirm("Install and enable the desktop companion?", false);
|
|
2272
3141
|
config.companion = shouldInstall ? "yes" : "no";
|
|
2273
3142
|
if (!shouldInstall) {
|
|
2274
3143
|
printInfo("Desktop companion install skipped.");
|
|
@@ -2387,25 +3256,63 @@ ${JSON.stringify(liteConfig, null, 2)}
|
|
|
2387
3256
|
}
|
|
2388
3257
|
}
|
|
2389
3258
|
if (config.installCustomSkills) {
|
|
2390
|
-
printStep(step++, totalSteps, "
|
|
3259
|
+
printStep(step++, totalSteps, "Synchronizing custom skills...");
|
|
2391
3260
|
if (config.dryRun) {
|
|
2392
|
-
printInfo("Dry run mode - would
|
|
3261
|
+
printInfo("Dry run mode - would synchronize custom skills:");
|
|
2393
3262
|
for (const skill of CUSTOM_SKILLS) {
|
|
2394
3263
|
printInfo(` - ${skill.name}`);
|
|
2395
3264
|
}
|
|
2396
3265
|
} else {
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
3266
|
+
try {
|
|
3267
|
+
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
3268
|
+
const result = syncBundledSkillsFromPackage(packageRoot);
|
|
3269
|
+
const categorizedSkipped = new Set([
|
|
3270
|
+
...result.staged,
|
|
3271
|
+
...result.adopted,
|
|
3272
|
+
...result.customized
|
|
3273
|
+
]);
|
|
3274
|
+
const preservedSkills = result.skippedExisting.filter((skill) => !categorizedSkipped.has(skill));
|
|
3275
|
+
if (result.installed.length > 0) {
|
|
3276
|
+
for (const skill of result.installed) {
|
|
3277
|
+
printSuccess(`Installed/Updated: ${skill}`);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
if (preservedSkills.length > 0) {
|
|
3281
|
+
for (const skill of preservedSkills) {
|
|
3282
|
+
printInfo(`Skipped/Preserved: ${skill}`);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
if (result.failed.length > 0) {
|
|
3286
|
+
for (const skill of result.failed) {
|
|
3287
|
+
if (skill === "__lock__") {
|
|
3288
|
+
printError("Lock acquisition failed");
|
|
3289
|
+
} else if (skill === "__manifest__") {
|
|
3290
|
+
printError("Manifest write failed");
|
|
3291
|
+
} else {
|
|
3292
|
+
printError(`Failed: ${skill}`);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
if (result.staged.length > 0) {
|
|
3297
|
+
for (const skill of result.staged) {
|
|
3298
|
+
printInfo(`Staged for review: ${skill}`);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
if (result.adopted.length > 0) {
|
|
3302
|
+
for (const skill of result.adopted) {
|
|
3303
|
+
printInfo(`Adopted: ${skill}`);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (result.customized.length > 0) {
|
|
3307
|
+
for (const skill of result.customized) {
|
|
3308
|
+
printInfo(`Customized: ${skill}`);
|
|
3309
|
+
}
|
|
2405
3310
|
}
|
|
3311
|
+
const realFailed = result.failed.filter((skill) => skill !== "__lock__" && skill !== "__manifest__");
|
|
3312
|
+
printSuccess(`Skill synchronization complete: ${result.installed.length} installed/updated, ${preservedSkills.length} skipped/preserved, ${result.staged.length} staged, ${result.adopted.length} adopted, ${result.customized.length} customized, ${realFailed.length} failed.`);
|
|
3313
|
+
} catch (err) {
|
|
3314
|
+
printError(`Failed to synchronize custom skills: ${err}`);
|
|
2406
3315
|
}
|
|
2407
|
-
const totalCustom = CUSTOM_SKILLS.length;
|
|
2408
|
-
printSuccess(`${customSkillsInstalled}/${totalCustom} custom skills processed`);
|
|
2409
3316
|
}
|
|
2410
3317
|
}
|
|
2411
3318
|
const statusMsg = isUpdate ? "Configuration updated!" : "Installation complete!";
|
|
@@ -2529,7 +3436,7 @@ Usage:
|
|
|
2529
3436
|
Options:
|
|
2530
3437
|
--skills=yes|no Install bundled skills (default: yes)
|
|
2531
3438
|
--companion=ask|yes|no Install desktop companion binary and enable config
|
|
2532
|
-
(default: ask; prompt defaults to
|
|
3439
|
+
(default: ask; prompt defaults to no)
|
|
2533
3440
|
--preset=<name> Active generated config preset (default: openai)
|
|
2534
3441
|
--background-subagents=ask|yes|no
|
|
2535
3442
|
Persist required OpenCode background subagent env
|