oh-my-opencode-slim 2.0.5 → 2.1.1
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 +64 -38
- package/README.ko-KR.md +62 -36
- package/README.md +82 -47
- package/README.zh-CN.md +63 -39
- 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/cli/custom-skills-registry.d.ts +18 -0
- package/dist/cli/custom-skills.d.ts +3 -19
- package/dist/cli/index.js +1130 -195
- package/dist/cli/providers.d.ts +12 -16
- package/dist/cli/skills.d.ts +2 -2
- package/dist/companion/manager.d.ts +10 -0
- package/dist/config/constants.d.ts +2 -1
- package/dist/config/council-schema.d.ts +37 -12
- package/dist/config/loader.d.ts +5 -2
- package/dist/config/schema.d.ts +17 -6
- package/dist/council/council-manager.d.ts +7 -3
- 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 +40 -2
- package/dist/hooks/image-hook.d.ts +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/loop-command/index.d.ts +13 -0
- package/dist/hooks/phase-reminder/index.d.ts +4 -3
- package/dist/hooks/post-file-tool-nudge/index.d.ts +15 -9
- package/dist/hooks/session-lifecycle.d.ts +11 -0
- package/dist/hooks/task-session-manager/index.d.ts +15 -4
- 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 +2890 -986
- package/dist/interview/dashboard-manager.d.ts +21 -0
- package/dist/interview/manager.d.ts +0 -14
- package/dist/interview/service.d.ts +9 -0
- package/dist/interview/session-server.d.ts +21 -0
- package/dist/interview/types.d.ts +3 -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 +33 -0
- package/dist/multiplexer/index.d.ts +1 -0
- package/dist/multiplexer/session-manager.d.ts +6 -5
- package/dist/multiplexer/shared.d.ts +24 -0
- package/dist/multiplexer/tmux/index.d.ts +0 -1
- package/dist/multiplexer/types.d.ts +4 -4
- package/dist/multiplexer/zellij/index.d.ts +0 -1
- package/dist/tools/cancel-task.d.ts +2 -2
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +123 -45
- package/dist/utils/background-job-board.d.ts +22 -2
- package/dist/utils/background-job-coordinator.d.ts +72 -0
- package/dist/utils/background-job-store.d.ts +46 -0
- package/dist/utils/councillor-models.d.ts +20 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/internal-initiator.d.ts +6 -1
- package/dist/utils/session.d.ts +2 -2
- package/oh-my-opencode-slim.schema.json +18 -1
- 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/oh-my-opencode-slim/SKILL.md +3 -3
- 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",
|
|
@@ -297,14 +293,43 @@ var ALL_AGENT_NAMES = ["orchestrator", ...SUBAGENT_NAMES];
|
|
|
297
293
|
var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
|
|
298
294
|
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
299
295
|
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
|
|
300
|
-
|
|
296
|
+
function formatSystemReminder(text) {
|
|
297
|
+
return `<system-reminder>
|
|
298
|
+
${text}
|
|
299
|
+
</system-reminder>`;
|
|
300
|
+
}
|
|
301
|
+
var PHASE_REMINDER = formatSystemReminder(PHASE_REMINDER_TEXT);
|
|
301
302
|
// src/config/council-schema.ts
|
|
302
303
|
import { z } from "zod";
|
|
303
|
-
|
|
304
|
+
|
|
305
|
+
// src/utils/councillor-models.ts
|
|
306
|
+
function normalizeCouncillorModels(model, fallbackVariant) {
|
|
307
|
+
const raw = Array.isArray(model) ? model : [model];
|
|
308
|
+
return raw.map((entry) => typeof entry === "string" ? { id: entry, variant: fallbackVariant } : { id: entry.id, variant: entry.variant ?? fallbackVariant });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/config/council-schema.ts
|
|
312
|
+
var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.6-luna")');
|
|
313
|
+
var CouncillorModelEntrySchema = z.object({
|
|
314
|
+
id: ModelIdSchema,
|
|
315
|
+
variant: z.string().optional()
|
|
316
|
+
});
|
|
317
|
+
var CouncillorModelSchema = z.union([
|
|
318
|
+
ModelIdSchema,
|
|
319
|
+
z.array(z.union([ModelIdSchema, CouncillorModelEntrySchema])).min(1)
|
|
320
|
+
]).describe('Model ID in provider/model format (e.g. "openai/gpt-5.6-luna"), or an ' + "ordered fallback chain (array of model IDs or { id, variant } entries) " + "tried in order until one responds.");
|
|
304
321
|
var CouncillorConfigSchema = z.object({
|
|
305
|
-
model:
|
|
322
|
+
model: CouncillorModelSchema,
|
|
306
323
|
variant: z.string().optional(),
|
|
307
324
|
prompt: z.string().optional().describe("Optional role/guidance injected into the councillor user prompt")
|
|
325
|
+
}).transform((c) => {
|
|
326
|
+
const models = normalizeCouncillorModels(c.model, c.variant);
|
|
327
|
+
return {
|
|
328
|
+
model: models[0].id,
|
|
329
|
+
variant: c.variant,
|
|
330
|
+
prompt: c.prompt,
|
|
331
|
+
models
|
|
332
|
+
};
|
|
308
333
|
});
|
|
309
334
|
var CouncilPresetSchema = z.record(z.string(), z.record(z.string(), z.unknown())).transform((entries, ctx) => {
|
|
310
335
|
const councillors = {};
|
|
@@ -344,7 +369,7 @@ var CouncilConfigSchema = z.object({
|
|
|
344
369
|
default_preset: z.string().default("default"),
|
|
345
370
|
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
371
|
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
|
|
372
|
+
master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
|
|
348
373
|
}).transform((data) => {
|
|
349
374
|
const deprecated = [];
|
|
350
375
|
if (data.master !== undefined)
|
|
@@ -410,7 +435,13 @@ var AgentOverrideConfigSchema = z2.object({
|
|
|
410
435
|
options: z2.record(z2.string(), z2.unknown()).optional(),
|
|
411
436
|
displayName: z2.string().min(1).optional()
|
|
412
437
|
}).strict();
|
|
413
|
-
var MultiplexerTypeSchema = z2.enum([
|
|
438
|
+
var MultiplexerTypeSchema = z2.enum([
|
|
439
|
+
"auto",
|
|
440
|
+
"tmux",
|
|
441
|
+
"zellij",
|
|
442
|
+
"herdr",
|
|
443
|
+
"none"
|
|
444
|
+
]);
|
|
414
445
|
var MultiplexerLayoutSchema = z2.enum([
|
|
415
446
|
"main-horizontal",
|
|
416
447
|
"main-vertical",
|
|
@@ -452,7 +483,9 @@ var FailoverConfigSchema = z2.object({
|
|
|
452
483
|
enabled: z2.boolean().default(true),
|
|
453
484
|
timeoutMs: z2.number().min(0).default(15000),
|
|
454
485
|
retryDelayMs: z2.number().min(0).default(500),
|
|
455
|
-
|
|
486
|
+
maxRetries: z2.number().int().min(0).default(3).describe("Number of consecutive 429/rate-limit responses tolerated on the " + "same model before aborting (or swapping to the next fallback " + "model when a chain is configured)."),
|
|
487
|
+
retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes."),
|
|
488
|
+
runtimeOverride: z2.boolean().default(true).describe("When true (default), a runtime model selected via /model that is " + "outside the configured fallback chain will still trigger the chain " + "on rate-limit errors. When false, out-of-chain runtime picks are " + "respected and the error surfaces instead of silently falling back " + "to the chain. Models that are members of the chain always fall back " + "regardless of this setting.")
|
|
456
489
|
}).strict();
|
|
457
490
|
var CompanionConfigSchema = z2.object({
|
|
458
491
|
enabled: z2.boolean().optional(),
|
|
@@ -479,24 +512,13 @@ var AcpAgentConfigSchema = z2.object({
|
|
|
479
512
|
permissionMode: AcpAgentPermissionModeSchema.default("ask")
|
|
480
513
|
}).strict();
|
|
481
514
|
var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
|
|
482
|
-
function
|
|
515
|
+
function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
|
|
483
516
|
for (const [name, override] of Object.entries(overrides)) {
|
|
484
|
-
|
|
485
|
-
if (!isBuiltInOrAlias) {
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
if (override.prompt !== undefined) {
|
|
489
|
-
ctx.addIssue({
|
|
490
|
-
code: z2.ZodIssueCode.custom,
|
|
491
|
-
path: [...pathPrefix, name, "prompt"],
|
|
492
|
-
message: "prompt is only supported for custom agents"
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
if (override.orchestratorPrompt !== undefined) {
|
|
517
|
+
if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
|
|
496
518
|
ctx.addIssue({
|
|
497
519
|
code: z2.ZodIssueCode.custom,
|
|
498
520
|
path: [...pathPrefix, name, "orchestratorPrompt"],
|
|
499
|
-
message: "orchestratorPrompt is
|
|
521
|
+
message: "orchestratorPrompt is not supported for the orchestrator agent"
|
|
500
522
|
});
|
|
501
523
|
}
|
|
502
524
|
}
|
|
@@ -504,6 +526,7 @@ function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
|
|
|
504
526
|
var PluginConfigSchema = z2.object({
|
|
505
527
|
preset: z2.string().optional(),
|
|
506
528
|
setDefaultAgent: z2.boolean().optional(),
|
|
529
|
+
compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout."),
|
|
507
530
|
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
508
531
|
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
509
532
|
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
@@ -522,11 +545,14 @@ var PluginConfigSchema = z2.object({
|
|
|
522
545
|
acpAgents: AcpAgentsConfigSchema.optional()
|
|
523
546
|
}).superRefine((value, ctx) => {
|
|
524
547
|
if (value.agents) {
|
|
525
|
-
|
|
548
|
+
rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
|
|
526
549
|
}
|
|
527
550
|
if (value.presets) {
|
|
528
551
|
for (const [presetName, preset] of Object.entries(value.presets)) {
|
|
529
|
-
|
|
552
|
+
rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
|
|
553
|
+
"presets",
|
|
554
|
+
presetName
|
|
555
|
+
]);
|
|
530
556
|
}
|
|
531
557
|
}
|
|
532
558
|
});
|
|
@@ -543,16 +569,7 @@ var DEFAULT_AGENT_MCPS = {
|
|
|
543
569
|
councillor: []
|
|
544
570
|
};
|
|
545
571
|
|
|
546
|
-
// src/cli/custom-skills.ts
|
|
547
|
-
import {
|
|
548
|
-
copyFileSync,
|
|
549
|
-
existsSync as existsSync2,
|
|
550
|
-
mkdirSync as mkdirSync2,
|
|
551
|
-
readdirSync,
|
|
552
|
-
statSync
|
|
553
|
-
} from "node:fs";
|
|
554
|
-
import { dirname as dirname2, join as join2 } from "node:path";
|
|
555
|
-
import { fileURLToPath } from "node:url";
|
|
572
|
+
// src/cli/custom-skills-registry.ts
|
|
556
573
|
var CUSTOM_SKILLS = [
|
|
557
574
|
{
|
|
558
575
|
name: "simplify",
|
|
@@ -590,6 +607,12 @@ var CUSTOM_SKILLS = [
|
|
|
590
607
|
allowedAgents: ["orchestrator"],
|
|
591
608
|
sourcePath: "src/skills/oh-my-opencode-slim"
|
|
592
609
|
},
|
|
610
|
+
{
|
|
611
|
+
name: "release-smoke-test",
|
|
612
|
+
description: "Validate packed release candidates and bugfixes before public publish",
|
|
613
|
+
allowedAgents: ["orchestrator"],
|
|
614
|
+
sourcePath: "src/skills/release-smoke-test"
|
|
615
|
+
},
|
|
593
616
|
{
|
|
594
617
|
name: "worktrees",
|
|
595
618
|
description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
|
|
@@ -597,57 +620,18 @@ var CUSTOM_SKILLS = [
|
|
|
597
620
|
sourcePath: "src/skills/worktrees"
|
|
598
621
|
}
|
|
599
622
|
];
|
|
600
|
-
function getCustomSkillsDir() {
|
|
601
|
-
return join2(getConfigDir(), "skills");
|
|
602
|
-
}
|
|
603
|
-
function copyDirRecursive(src, dest) {
|
|
604
|
-
if (!existsSync2(dest)) {
|
|
605
|
-
mkdirSync2(dest, { recursive: true });
|
|
606
|
-
}
|
|
607
|
-
const entries = readdirSync(src);
|
|
608
|
-
for (const entry of entries) {
|
|
609
|
-
const srcPath = join2(src, entry);
|
|
610
|
-
const destPath = join2(dest, entry);
|
|
611
|
-
const stat = statSync(srcPath);
|
|
612
|
-
if (stat.isDirectory()) {
|
|
613
|
-
copyDirRecursive(srcPath, destPath);
|
|
614
|
-
} else {
|
|
615
|
-
const destDir = dirname2(destPath);
|
|
616
|
-
if (!existsSync2(destDir)) {
|
|
617
|
-
mkdirSync2(destDir, { recursive: true });
|
|
618
|
-
}
|
|
619
|
-
copyFileSync(srcPath, destPath);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
function installCustomSkill(skill) {
|
|
624
|
-
try {
|
|
625
|
-
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
626
|
-
const sourcePath = join2(packageRoot, skill.sourcePath);
|
|
627
|
-
const targetPath = join2(getCustomSkillsDir(), skill.name);
|
|
628
|
-
if (!existsSync2(sourcePath)) {
|
|
629
|
-
console.error(`Custom skill source not found: ${sourcePath}`);
|
|
630
|
-
return false;
|
|
631
|
-
}
|
|
632
|
-
copyDirRecursive(sourcePath, targetPath);
|
|
633
|
-
return true;
|
|
634
|
-
} catch (error) {
|
|
635
|
-
console.error(`Failed to install custom skill: ${skill.name}`, error);
|
|
636
|
-
return false;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
623
|
|
|
640
624
|
// src/cli/providers.ts
|
|
641
625
|
var SCHEMA_URL = "https://unpkg.com/oh-my-opencode-slim@latest/oh-my-opencode-slim.schema.json";
|
|
642
626
|
var GENERATED_PRESETS = ["openai", "opencode-go"];
|
|
643
627
|
var MODEL_MAPPINGS = {
|
|
644
628
|
openai: {
|
|
645
|
-
orchestrator: { model: "openai/gpt-5.
|
|
646
|
-
oracle: { model: "openai/gpt-5.
|
|
647
|
-
librarian: { model: "openai/gpt-5.
|
|
648
|
-
explorer: { model: "openai/gpt-5.
|
|
649
|
-
designer: { model: "openai/gpt-5.
|
|
650
|
-
fixer: { model: "openai/gpt-5.
|
|
629
|
+
orchestrator: { model: "openai/gpt-5.6-terra", variant: "medium" },
|
|
630
|
+
oracle: { model: "openai/gpt-5.6-sol", variant: "high" },
|
|
631
|
+
librarian: { model: "openai/gpt-5.6-luna", variant: "low" },
|
|
632
|
+
explorer: { model: "openai/gpt-5.6-luna", variant: "low" },
|
|
633
|
+
designer: { model: "openai/gpt-5.6-luna", variant: "medium" },
|
|
634
|
+
fixer: { model: "openai/gpt-5.6-luna", variant: "medium" }
|
|
651
635
|
},
|
|
652
636
|
kimi: {
|
|
653
637
|
orchestrator: { model: "kimi-for-coding/k2p5" },
|
|
@@ -677,12 +661,11 @@ var MODEL_MAPPINGS = {
|
|
|
677
661
|
fixer: { model: "zai-coding-plan/glm-5", variant: "low" }
|
|
678
662
|
},
|
|
679
663
|
"opencode-go": {
|
|
680
|
-
orchestrator: { model: "opencode-go/glm-5.
|
|
681
|
-
oracle: { model: "opencode-go/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
designer: { model: "opencode-go/kimi-k2.6", variant: "medium" },
|
|
664
|
+
orchestrator: { model: "opencode-go/glm-5.2" },
|
|
665
|
+
oracle: { model: "opencode-go/qwen3.7-max", variant: "max" },
|
|
666
|
+
librarian: { model: "opencode-go/deepseek-v4-flash" },
|
|
667
|
+
explorer: { model: "opencode-go/deepseek-v4-flash" },
|
|
668
|
+
designer: { model: "opencode-go/kimi-k2.7-code", variant: "medium" },
|
|
686
669
|
fixer: { model: "opencode-go/deepseek-v4-flash", variant: "high" },
|
|
687
670
|
observer: { model: "opencode-go/kimi-k2.6" }
|
|
688
671
|
}
|
|
@@ -788,10 +771,10 @@ function normalizePathForMatch(path) {
|
|
|
788
771
|
return path.replaceAll("\\", "/");
|
|
789
772
|
}
|
|
790
773
|
function findPackageRoot(startPath) {
|
|
791
|
-
let currentPath =
|
|
774
|
+
let currentPath = dirname2(startPath);
|
|
792
775
|
while (true) {
|
|
793
|
-
const packageJsonPath =
|
|
794
|
-
if (
|
|
776
|
+
const packageJsonPath = join2(currentPath, "package.json");
|
|
777
|
+
if (existsSync2(packageJsonPath)) {
|
|
795
778
|
try {
|
|
796
779
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
797
780
|
if (packageJson.name === PACKAGE_NAME) {
|
|
@@ -799,7 +782,7 @@ function findPackageRoot(startPath) {
|
|
|
799
782
|
}
|
|
800
783
|
} catch {}
|
|
801
784
|
}
|
|
802
|
-
const parentPath =
|
|
785
|
+
const parentPath = dirname2(currentPath);
|
|
803
786
|
if (parentPath === currentPath) {
|
|
804
787
|
return null;
|
|
805
788
|
}
|
|
@@ -810,8 +793,8 @@ function isLocalPackageRootEntry(entry) {
|
|
|
810
793
|
if (!entry || entry.startsWith("file://")) {
|
|
811
794
|
return false;
|
|
812
795
|
}
|
|
813
|
-
const packageJsonPath =
|
|
814
|
-
if (!
|
|
796
|
+
const packageJsonPath = join2(entry, "package.json");
|
|
797
|
+
if (!existsSync2(packageJsonPath)) {
|
|
815
798
|
return false;
|
|
816
799
|
}
|
|
817
800
|
try {
|
|
@@ -883,8 +866,8 @@ function getRequestedPackageTag(packageRoot) {
|
|
|
883
866
|
}
|
|
884
867
|
function getVersionFromPackageRoot(packageRoot) {
|
|
885
868
|
try {
|
|
886
|
-
const packageJsonPath =
|
|
887
|
-
if (!
|
|
869
|
+
const packageJsonPath = join2(packageRoot, "package.json");
|
|
870
|
+
if (!existsSync2(packageJsonPath))
|
|
888
871
|
return;
|
|
889
872
|
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
890
873
|
return pkg.version;
|
|
@@ -893,13 +876,13 @@ function getVersionFromPackageRoot(packageRoot) {
|
|
|
893
876
|
}
|
|
894
877
|
}
|
|
895
878
|
function getOpenCodePluginCacheDir(version) {
|
|
896
|
-
const cacheDir = process.env.XDG_CACHE_HOME?.trim() ||
|
|
879
|
+
const cacheDir = process.env.XDG_CACHE_HOME?.trim() || join2(homedir2(), ".cache");
|
|
897
880
|
const suffix = version ? `${PACKAGE_NAME}@${version}` : `${PACKAGE_NAME}@latest`;
|
|
898
|
-
return
|
|
881
|
+
return join2(cacheDir, "opencode", "packages", suffix);
|
|
899
882
|
}
|
|
900
883
|
function writeOpenCodePluginCacheManifest(cacheDir, version = "latest") {
|
|
901
884
|
try {
|
|
902
|
-
writeFileSync(
|
|
885
|
+
writeFileSync(join2(cacheDir, "package.json"), JSON.stringify({
|
|
903
886
|
name: `${PACKAGE_NAME}-cache`,
|
|
904
887
|
private: true,
|
|
905
888
|
dependencies: {
|
|
@@ -916,16 +899,16 @@ function writeOpenCodePluginCacheManifest(cacheDir, version = "latest") {
|
|
|
916
899
|
}
|
|
917
900
|
}
|
|
918
901
|
function removeOpenCodePluginCacheArtifacts(cacheDir) {
|
|
919
|
-
rmSync(
|
|
902
|
+
rmSync(join2(cacheDir, "node_modules", PACKAGE_NAME), {
|
|
920
903
|
recursive: true,
|
|
921
904
|
force: true
|
|
922
905
|
});
|
|
923
|
-
rmSync(
|
|
924
|
-
rmSync(
|
|
906
|
+
rmSync(join2(cacheDir, "bun.lock"), { force: true });
|
|
907
|
+
rmSync(join2(cacheDir, "bun.lockb"), { force: true });
|
|
925
908
|
}
|
|
926
909
|
function verifyOpenCodePluginCache(cacheDir) {
|
|
927
|
-
const pluginPackageJsonPath =
|
|
928
|
-
if (!
|
|
910
|
+
const pluginPackageJsonPath = join2(cacheDir, "node_modules", PACKAGE_NAME, "package.json");
|
|
911
|
+
if (!existsSync2(pluginPackageJsonPath)) {
|
|
929
912
|
return {
|
|
930
913
|
success: false,
|
|
931
914
|
configPath: cacheDir,
|
|
@@ -965,7 +948,7 @@ async function warmOpenCodePluginCache() {
|
|
|
965
948
|
const cacheVersion = pinnedVersion ?? requestedTag ?? runningVersion;
|
|
966
949
|
const cacheDir = getOpenCodePluginCacheDir(cacheVersion);
|
|
967
950
|
try {
|
|
968
|
-
|
|
951
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
969
952
|
} catch (err) {
|
|
970
953
|
return {
|
|
971
954
|
success: false,
|
|
@@ -1011,9 +994,9 @@ function stripJsonComments(json) {
|
|
|
1011
994
|
}
|
|
1012
995
|
function parseConfigFile(path) {
|
|
1013
996
|
try {
|
|
1014
|
-
if (!
|
|
997
|
+
if (!existsSync2(path))
|
|
1015
998
|
return { config: null };
|
|
1016
|
-
const stat =
|
|
999
|
+
const stat = statSync(path);
|
|
1017
1000
|
if (stat.size === 0)
|
|
1018
1001
|
return { config: null };
|
|
1019
1002
|
const content = readFileSync(path, "utf-8");
|
|
@@ -1042,8 +1025,8 @@ function writeConfig(configPath, config) {
|
|
|
1042
1025
|
const bakPath = `${configPath}.bak`;
|
|
1043
1026
|
const content = `${JSON.stringify(config, null, 2)}
|
|
1044
1027
|
`;
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1028
|
+
if (existsSync2(configPath)) {
|
|
1029
|
+
copyFileSync(configPath, bakPath);
|
|
1047
1030
|
}
|
|
1048
1031
|
writeFileSync(tmpPath, content);
|
|
1049
1032
|
renameSync(tmpPath, configPath);
|
|
@@ -1129,8 +1112,8 @@ function writeLiteConfig(installConfig, targetPath) {
|
|
|
1129
1112
|
const bakPath = `${configPath}.bak`;
|
|
1130
1113
|
const content = `${JSON.stringify(config, null, 2)}
|
|
1131
1114
|
`;
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1115
|
+
if (existsSync2(configPath)) {
|
|
1116
|
+
copyFileSync(configPath, bakPath);
|
|
1134
1117
|
}
|
|
1135
1118
|
writeFileSync(tmpPath, content);
|
|
1136
1119
|
renameSync(tmpPath, configPath);
|
|
@@ -1288,6 +1271,7 @@ function mergePluginConfigs(base, override) {
|
|
|
1288
1271
|
...base,
|
|
1289
1272
|
...override,
|
|
1290
1273
|
agents: deepMerge(base.agents, override.agents),
|
|
1274
|
+
presets: deepMerge(base.presets, override.presets),
|
|
1291
1275
|
tmux: deepMerge(base.tmux, override.tmux),
|
|
1292
1276
|
multiplexer: deepMerge(base.multiplexer, override.multiplexer),
|
|
1293
1277
|
interview: deepMerge(base.interview, override.interview),
|
|
@@ -1513,9 +1497,949 @@ Options:
|
|
|
1513
1497
|
// src/cli/install.ts
|
|
1514
1498
|
import { existsSync as existsSync7 } from "node:fs";
|
|
1515
1499
|
import { createInterface } from "node:readline/promises";
|
|
1500
|
+
import { fileURLToPath } from "node:url";
|
|
1501
|
+
|
|
1502
|
+
// src/hooks/auto-update-checker/skill-sync.ts
|
|
1503
|
+
import * as crypto from "node:crypto";
|
|
1504
|
+
import {
|
|
1505
|
+
copyFileSync as copyFileSync2,
|
|
1506
|
+
existsSync as existsSync4,
|
|
1507
|
+
lstatSync,
|
|
1508
|
+
mkdirSync as mkdirSync3,
|
|
1509
|
+
readdirSync,
|
|
1510
|
+
readFileSync as readFileSync4,
|
|
1511
|
+
renameSync as renameSync2,
|
|
1512
|
+
rmSync as rmSync2,
|
|
1513
|
+
unlinkSync,
|
|
1514
|
+
writeFileSync as writeFileSync2
|
|
1515
|
+
} from "node:fs";
|
|
1516
|
+
import * as os from "node:os";
|
|
1517
|
+
import * as path2 from "node:path";
|
|
1518
|
+
|
|
1519
|
+
// src/utils/logger.ts
|
|
1520
|
+
import { appendFile } from "node:fs/promises";
|
|
1521
|
+
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1522
|
+
var logFile = null;
|
|
1523
|
+
var writeChain = Promise.resolve();
|
|
1524
|
+
function log(message, data) {
|
|
1525
|
+
const target = logFile;
|
|
1526
|
+
if (!target)
|
|
1527
|
+
return;
|
|
1528
|
+
try {
|
|
1529
|
+
const timestamp = new Date().toISOString();
|
|
1530
|
+
let dataStr = "";
|
|
1531
|
+
if (data !== undefined) {
|
|
1532
|
+
try {
|
|
1533
|
+
dataStr = JSON.stringify(data);
|
|
1534
|
+
} catch {
|
|
1535
|
+
dataStr = "[unserializable]";
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
1539
|
+
`;
|
|
1540
|
+
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
1541
|
+
} catch {}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// src/hooks/auto-update-checker/skill-sync.ts
|
|
1545
|
+
var localProcessToken = globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN;
|
|
1546
|
+
if (!localProcessToken) {
|
|
1547
|
+
localProcessToken = crypto.randomUUID();
|
|
1548
|
+
globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN = localProcessToken;
|
|
1549
|
+
}
|
|
1550
|
+
var PROCESS_TOKEN = localProcessToken;
|
|
1551
|
+
var ACQUIRED_LOCKS = new Set;
|
|
1552
|
+
var LEGACY_MANAGED_SKILL_HASHES = {};
|
|
1553
|
+
function validateManifest(data) {
|
|
1554
|
+
if (typeof data !== "object" || data === null)
|
|
1555
|
+
return false;
|
|
1556
|
+
const d = data;
|
|
1557
|
+
if (d.schemaVersion !== 1)
|
|
1558
|
+
return false;
|
|
1559
|
+
if (typeof d.skills !== "object" || d.skills === null)
|
|
1560
|
+
return false;
|
|
1561
|
+
const allowedStatuses = new Set([
|
|
1562
|
+
"managed",
|
|
1563
|
+
"customized",
|
|
1564
|
+
"deleted",
|
|
1565
|
+
"conflict"
|
|
1566
|
+
]);
|
|
1567
|
+
const skillsObj = d.skills;
|
|
1568
|
+
for (const key of Object.keys(skillsObj)) {
|
|
1569
|
+
const entry = skillsObj[key];
|
|
1570
|
+
if (typeof entry !== "object" || entry === null)
|
|
1571
|
+
return false;
|
|
1572
|
+
if (typeof entry.status !== "string" || !allowedStatuses.has(entry.status))
|
|
1573
|
+
return false;
|
|
1574
|
+
if (typeof entry.packageVersion !== "string")
|
|
1575
|
+
return false;
|
|
1576
|
+
if (typeof entry.sourceHash !== "string")
|
|
1577
|
+
return false;
|
|
1578
|
+
if (typeof entry.lastManagedHash !== "string")
|
|
1579
|
+
return false;
|
|
1580
|
+
if (typeof entry.lastSeenHash !== "string")
|
|
1581
|
+
return false;
|
|
1582
|
+
if (entry.stagedPath !== undefined && typeof entry.stagedPath !== "string")
|
|
1583
|
+
return false;
|
|
1584
|
+
if (typeof entry.updatedAt !== "string")
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
function copyDirRecursive(src, dest) {
|
|
1590
|
+
const stat = lstatSync(src);
|
|
1591
|
+
if (stat.isSymbolicLink()) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (stat.isDirectory()) {
|
|
1595
|
+
mkdirSync3(dest, { recursive: true });
|
|
1596
|
+
const entries = readdirSync(src);
|
|
1597
|
+
for (const entry of entries) {
|
|
1598
|
+
copyDirRecursive(path2.join(src, entry), path2.join(dest, entry));
|
|
1599
|
+
}
|
|
1600
|
+
} else if (stat.isFile()) {
|
|
1601
|
+
const destDir = path2.dirname(dest);
|
|
1602
|
+
if (!existsSync4(destDir)) {
|
|
1603
|
+
mkdirSync3(destDir, { recursive: true });
|
|
1604
|
+
}
|
|
1605
|
+
copyFileSync2(src, dest);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function computeDirectoryHash(dirPath) {
|
|
1609
|
+
const hash = crypto.createHash("sha256");
|
|
1610
|
+
const entriesToHash = [];
|
|
1611
|
+
function traverse(currentDir) {
|
|
1612
|
+
const entries = readdirSync(currentDir);
|
|
1613
|
+
for (const entry of entries) {
|
|
1614
|
+
const absolutePath = path2.join(currentDir, entry);
|
|
1615
|
+
const stat = lstatSync(absolutePath);
|
|
1616
|
+
const relativePath = path2.relative(dirPath, absolutePath);
|
|
1617
|
+
if (stat.isSymbolicLink()) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (stat.isDirectory()) {
|
|
1621
|
+
entriesToHash.push({
|
|
1622
|
+
relativePath,
|
|
1623
|
+
absolutePath,
|
|
1624
|
+
kind: "directory",
|
|
1625
|
+
mode: stat.mode
|
|
1626
|
+
});
|
|
1627
|
+
traverse(absolutePath);
|
|
1628
|
+
} else if (stat.isFile()) {
|
|
1629
|
+
entriesToHash.push({
|
|
1630
|
+
relativePath,
|
|
1631
|
+
absolutePath,
|
|
1632
|
+
kind: "file",
|
|
1633
|
+
mode: stat.mode
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
traverse(dirPath);
|
|
1639
|
+
entriesToHash.sort((a, b) => {
|
|
1640
|
+
if (a.relativePath < b.relativePath)
|
|
1641
|
+
return -1;
|
|
1642
|
+
if (a.relativePath > b.relativePath)
|
|
1643
|
+
return 1;
|
|
1644
|
+
return 0;
|
|
1645
|
+
});
|
|
1646
|
+
for (const entry of entriesToHash) {
|
|
1647
|
+
hash.update(entry.kind);
|
|
1648
|
+
hash.update("\x00");
|
|
1649
|
+
hash.update(entry.relativePath);
|
|
1650
|
+
hash.update("\x00");
|
|
1651
|
+
hash.update(String(entry.mode & 4095));
|
|
1652
|
+
hash.update("\x00");
|
|
1653
|
+
if (entry.kind === "file") {
|
|
1654
|
+
const content = readFileSync4(entry.absolutePath);
|
|
1655
|
+
hash.update(content);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return hash.digest("hex");
|
|
1659
|
+
}
|
|
1660
|
+
function isPidRunning(pid) {
|
|
1661
|
+
try {
|
|
1662
|
+
process.kill(pid, 0);
|
|
1663
|
+
return true;
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
return err.code === "EPERM";
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
var CROSS_HOST_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
1669
|
+
function acquireLock(lockDir) {
|
|
1670
|
+
const metadataPath = path2.join(lockDir, "owner.json");
|
|
1671
|
+
const currentHost = os.hostname();
|
|
1672
|
+
const currentPid = process.pid;
|
|
1673
|
+
const writeMetadata = () => {
|
|
1674
|
+
try {
|
|
1675
|
+
const metadata = {
|
|
1676
|
+
pid: currentPid,
|
|
1677
|
+
host: currentHost,
|
|
1678
|
+
time: Date.now(),
|
|
1679
|
+
token: PROCESS_TOKEN
|
|
1680
|
+
};
|
|
1681
|
+
writeFileSync2(metadataPath, JSON.stringify(metadata), "utf-8");
|
|
1682
|
+
} catch {}
|
|
1683
|
+
};
|
|
1684
|
+
try {
|
|
1685
|
+
mkdirSync3(lockDir);
|
|
1686
|
+
writeMetadata();
|
|
1687
|
+
ACQUIRED_LOCKS.add(path2.resolve(lockDir));
|
|
1688
|
+
return true;
|
|
1689
|
+
} catch (err) {
|
|
1690
|
+
if (err.code !== "EEXIST") {
|
|
1691
|
+
throw err;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
try {
|
|
1695
|
+
let shouldSteal = false;
|
|
1696
|
+
let ageMs = 0;
|
|
1697
|
+
if (existsSync4(metadataPath)) {
|
|
1698
|
+
try {
|
|
1699
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
1700
|
+
const metadata = JSON.parse(content);
|
|
1701
|
+
ageMs = Date.now() - metadata.time;
|
|
1702
|
+
if (metadata.host === currentHost) {
|
|
1703
|
+
if (!isPidRunning(metadata.pid)) {
|
|
1704
|
+
log(`[skill-sync] Lock owner process ${metadata.pid} is not running on this host. Recovery path.`);
|
|
1705
|
+
shouldSteal = true;
|
|
1706
|
+
}
|
|
1707
|
+
} else {
|
|
1708
|
+
if (ageMs > CROSS_HOST_LOCK_EXPIRY_MS) {
|
|
1709
|
+
log(`[skill-sync] Lock owned by different host ${metadata.host} has expired (${Math.round(ageMs / 1000)}s old). Reclaiming lock.`);
|
|
1710
|
+
shouldSteal = true;
|
|
1711
|
+
} else {
|
|
1712
|
+
log(`[skill-sync] Lock is owned by different host ${metadata.host}; failing closed.`);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
} catch {
|
|
1716
|
+
shouldSteal = true;
|
|
1717
|
+
}
|
|
1718
|
+
} else {
|
|
1719
|
+
const stat = lstatSync(lockDir);
|
|
1720
|
+
ageMs = Date.now() - stat.mtimeMs;
|
|
1721
|
+
if (ageMs > 30000) {
|
|
1722
|
+
shouldSteal = true;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (!shouldSteal)
|
|
1726
|
+
return false;
|
|
1727
|
+
log(`[skill-sync] Stealing/recovering lock directory.`);
|
|
1728
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
1729
|
+
mkdirSync3(lockDir);
|
|
1730
|
+
writeMetadata();
|
|
1731
|
+
ACQUIRED_LOCKS.add(path2.resolve(lockDir));
|
|
1732
|
+
return true;
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
log(`[skill-sync] Failed to check/recover lock at ${lockDir}:`, err);
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function releaseLock(lockDir) {
|
|
1739
|
+
const resolvedPath = path2.resolve(lockDir);
|
|
1740
|
+
try {
|
|
1741
|
+
let isOurLock = false;
|
|
1742
|
+
const metadataPath = path2.join(lockDir, "owner.json");
|
|
1743
|
+
if (existsSync4(metadataPath)) {
|
|
1744
|
+
try {
|
|
1745
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
1746
|
+
const metadata = JSON.parse(content);
|
|
1747
|
+
if (metadata.host === os.hostname() && metadata.pid === process.pid && metadata.token === PROCESS_TOKEN) {
|
|
1748
|
+
isOurLock = true;
|
|
1749
|
+
} else {
|
|
1750
|
+
isOurLock = false;
|
|
1751
|
+
}
|
|
1752
|
+
} catch (err) {
|
|
1753
|
+
log(`[skill-sync] Lock owner.json is unreadable/corrupt:`, err);
|
|
1754
|
+
isOurLock = false;
|
|
1755
|
+
}
|
|
1756
|
+
} else if (ACQUIRED_LOCKS.has(resolvedPath)) {
|
|
1757
|
+
isOurLock = true;
|
|
1758
|
+
}
|
|
1759
|
+
if (isOurLock) {
|
|
1760
|
+
if (existsSync4(lockDir)) {
|
|
1761
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
1762
|
+
}
|
|
1763
|
+
} else if (existsSync4(lockDir)) {
|
|
1764
|
+
log(`[skill-sync] Skipping lock directory removal: lock is not owned by this process/token or owner.json check failed.`);
|
|
1765
|
+
}
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
log(`[skill-sync] Failed to release lock at ${lockDir}:`, err);
|
|
1768
|
+
} finally {
|
|
1769
|
+
ACQUIRED_LOCKS.delete(resolvedPath);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function atomicReplaceDir(sourceDir, destDir) {
|
|
1773
|
+
const parentDir = path2.dirname(destDir);
|
|
1774
|
+
if (!existsSync4(parentDir)) {
|
|
1775
|
+
mkdirSync3(parentDir, { recursive: true });
|
|
1776
|
+
}
|
|
1777
|
+
const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1778
|
+
const stagingDir = path2.join(parentDir, `.staging-${path2.basename(destDir)}-${uniqueSuffix}`);
|
|
1779
|
+
const backupDir = path2.join(parentDir, `.backup-${path2.basename(destDir)}-${uniqueSuffix}`);
|
|
1780
|
+
let backupCreated = false;
|
|
1781
|
+
try {
|
|
1782
|
+
copyDirRecursive(sourceDir, stagingDir);
|
|
1783
|
+
if (existsSync4(destDir)) {
|
|
1784
|
+
renameSync2(destDir, backupDir);
|
|
1785
|
+
backupCreated = true;
|
|
1786
|
+
}
|
|
1787
|
+
renameSync2(stagingDir, destDir);
|
|
1788
|
+
if (backupCreated) {
|
|
1789
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
1790
|
+
}
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
log(`[skill-sync] Error during atomic replace for ${destDir}. Rolling back:`, err);
|
|
1793
|
+
if (backupCreated) {
|
|
1794
|
+
try {
|
|
1795
|
+
if (existsSync4(destDir)) {
|
|
1796
|
+
rmSync2(destDir, { recursive: true, force: true });
|
|
1797
|
+
}
|
|
1798
|
+
renameSync2(backupDir, destDir);
|
|
1799
|
+
} catch (rollbackErr) {
|
|
1800
|
+
log(`[skill-sync] Critical error during rollback for ${destDir}:`, rollbackErr);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
try {
|
|
1804
|
+
if (existsSync4(stagingDir)) {
|
|
1805
|
+
rmSync2(stagingDir, { recursive: true, force: true });
|
|
1806
|
+
}
|
|
1807
|
+
} catch {}
|
|
1808
|
+
throw err;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
function matchesArtifactPattern(entry, prefix, skillName) {
|
|
1812
|
+
if (!entry.startsWith(prefix))
|
|
1813
|
+
return false;
|
|
1814
|
+
const rest = entry.slice(prefix.length);
|
|
1815
|
+
if (!rest.startsWith(`${skillName}-`))
|
|
1816
|
+
return false;
|
|
1817
|
+
const suffix = rest.slice(skillName.length + 1);
|
|
1818
|
+
const firstPart = suffix.split("-")[0];
|
|
1819
|
+
const timestamp = Number(firstPart);
|
|
1820
|
+
if (Number.isNaN(timestamp) || timestamp <= 0)
|
|
1821
|
+
return false;
|
|
1822
|
+
return true;
|
|
1823
|
+
}
|
|
1824
|
+
function recoverOrphanArtifacts(destSkillsDir, skillName) {
|
|
1825
|
+
if (!existsSync4(destSkillsDir))
|
|
1826
|
+
return false;
|
|
1827
|
+
let hadArtifacts = false;
|
|
1828
|
+
let entries = [];
|
|
1829
|
+
try {
|
|
1830
|
+
entries = readdirSync(destSkillsDir);
|
|
1831
|
+
} catch {
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
const backups = [];
|
|
1835
|
+
const stagings = [];
|
|
1836
|
+
for (const entry of entries) {
|
|
1837
|
+
if (matchesArtifactPattern(entry, ".backup-", skillName)) {
|
|
1838
|
+
backups.push(path2.join(destSkillsDir, entry));
|
|
1839
|
+
hadArtifacts = true;
|
|
1840
|
+
} else if (matchesArtifactPattern(entry, ".staging-", skillName)) {
|
|
1841
|
+
stagings.push(path2.join(destSkillsDir, entry));
|
|
1842
|
+
hadArtifacts = true;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
const destPath = path2.join(destSkillsDir, skillName);
|
|
1846
|
+
if (backups.length > 0) {
|
|
1847
|
+
backups.sort();
|
|
1848
|
+
const mostRecentBackup = backups[backups.length - 1];
|
|
1849
|
+
if (!existsSync4(destPath)) {
|
|
1850
|
+
backups.pop();
|
|
1851
|
+
try {
|
|
1852
|
+
renameSync2(mostRecentBackup, destPath);
|
|
1853
|
+
log(`[skill-sync] Recovered backup for ${skillName} back to destination.`);
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
log(`[skill-sync] Failed to restore backup for ${skillName}:`, err);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
for (const backup of backups) {
|
|
1859
|
+
try {
|
|
1860
|
+
rmSync2(backup, { recursive: true, force: true });
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
log(`[skill-sync] Failed to clean up backup folder ${backup}:`, err);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
for (const staging of stagings) {
|
|
1867
|
+
try {
|
|
1868
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
log(`[skill-sync] Failed to clean up staging folder ${staging}:`, err);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return hadArtifacts;
|
|
1874
|
+
}
|
|
1875
|
+
function removeManagedStagedPath(stagedPath, manifestDir, skillName) {
|
|
1876
|
+
try {
|
|
1877
|
+
const absoluteStagedPath = path2.resolve(stagedPath);
|
|
1878
|
+
const absoluteAllowedRoot = path2.resolve(path2.join(manifestDir, "skill-updates"));
|
|
1879
|
+
const relative2 = path2.relative(absoluteAllowedRoot, absoluteStagedPath);
|
|
1880
|
+
const isUnderRoot = relative2 && !relative2.startsWith("..") && !path2.isAbsolute(relative2);
|
|
1881
|
+
if (isUnderRoot) {
|
|
1882
|
+
if (existsSync4(absoluteStagedPath)) {
|
|
1883
|
+
rmSync2(absoluteStagedPath, { recursive: true, force: true });
|
|
1884
|
+
log(`[skill-sync] Safely cleaned up staged path for ${skillName}: ${absoluteStagedPath}`);
|
|
1885
|
+
}
|
|
1886
|
+
} else {
|
|
1887
|
+
log(`[skill-sync] Refusing to delete staged path for ${skillName}: path ${absoluteStagedPath} is not under managed root ${absoluteAllowedRoot}`);
|
|
1888
|
+
}
|
|
1889
|
+
} catch (err) {
|
|
1890
|
+
log(`[skill-sync] Error while trying to verify and remove staged path for ${skillName} (${stagedPath}):`, err);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
function syncBundledSkillsFromPackage(packageRoot, options = {}) {
|
|
1894
|
+
const installed = [];
|
|
1895
|
+
const skippedExisting = [];
|
|
1896
|
+
const failed = [];
|
|
1897
|
+
const staged = [];
|
|
1898
|
+
const adopted = [];
|
|
1899
|
+
const customized = [];
|
|
1900
|
+
const sourceSkillsDir = path2.join(packageRoot, "src", "skills");
|
|
1901
|
+
try {
|
|
1902
|
+
const stat = lstatSync(sourceSkillsDir);
|
|
1903
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
1904
|
+
log(`[skill-sync] Source skills directory is not a valid directory: ${sourceSkillsDir}`);
|
|
1905
|
+
return {
|
|
1906
|
+
installed,
|
|
1907
|
+
skippedExisting,
|
|
1908
|
+
failed,
|
|
1909
|
+
staged,
|
|
1910
|
+
adopted,
|
|
1911
|
+
customized
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
} catch {
|
|
1915
|
+
log(`[skill-sync] Source skills directory does not exist or is unreadable: ${sourceSkillsDir}`);
|
|
1916
|
+
return {
|
|
1917
|
+
installed,
|
|
1918
|
+
skippedExisting,
|
|
1919
|
+
failed,
|
|
1920
|
+
staged,
|
|
1921
|
+
adopted,
|
|
1922
|
+
customized
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
let packageVersion = "unknown";
|
|
1926
|
+
try {
|
|
1927
|
+
const pkgJsonPath = path2.join(packageRoot, "package.json");
|
|
1928
|
+
if (existsSync4(pkgJsonPath)) {
|
|
1929
|
+
const content = readFileSync4(pkgJsonPath, "utf-8");
|
|
1930
|
+
const pkg = JSON.parse(content);
|
|
1931
|
+
if (pkg.version) {
|
|
1932
|
+
packageVersion = pkg.version;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
log(`[skill-sync] Failed to read package version from ${packageRoot}:`, err);
|
|
1937
|
+
}
|
|
1938
|
+
const manifestDir = path2.join(getConfigDir(), ".oh-my-opencode-slim");
|
|
1939
|
+
const lockDir = path2.join(manifestDir, "skills.lock");
|
|
1940
|
+
try {
|
|
1941
|
+
mkdirSync3(manifestDir, { recursive: true });
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
log(`[skill-sync] Failed to create manifest directory: ${manifestDir}`, err);
|
|
1944
|
+
}
|
|
1945
|
+
if (!acquireLock(lockDir)) {
|
|
1946
|
+
log("[skill-sync] Failed to acquire lock for skill synchronization. Skipping.");
|
|
1947
|
+
return {
|
|
1948
|
+
installed,
|
|
1949
|
+
skippedExisting,
|
|
1950
|
+
failed: ["__lock__"],
|
|
1951
|
+
staged,
|
|
1952
|
+
adopted,
|
|
1953
|
+
customized
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
const manifestPath = path2.join(manifestDir, "skills-manifest.json");
|
|
1958
|
+
let manifest = {
|
|
1959
|
+
schemaVersion: 1,
|
|
1960
|
+
updatedAt: new Date().toISOString(),
|
|
1961
|
+
skills: {}
|
|
1962
|
+
};
|
|
1963
|
+
let isManifestCorrupt = false;
|
|
1964
|
+
if (existsSync4(manifestPath)) {
|
|
1965
|
+
try {
|
|
1966
|
+
const content = readFileSync4(manifestPath, "utf-8");
|
|
1967
|
+
const parsed = JSON.parse(content);
|
|
1968
|
+
if (validateManifest(parsed)) {
|
|
1969
|
+
manifest = parsed;
|
|
1970
|
+
} else {
|
|
1971
|
+
throw new Error("Manifest validation failed");
|
|
1972
|
+
}
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
log("[skill-sync] Manifest is corrupt/unreadable. Failing closed.", err);
|
|
1975
|
+
isManifestCorrupt = true;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const destSkillsDir = path2.join(getConfigDir(), "skills");
|
|
1979
|
+
try {
|
|
1980
|
+
if (!existsSync4(destSkillsDir)) {
|
|
1981
|
+
mkdirSync3(destSkillsDir, { recursive: true });
|
|
1982
|
+
}
|
|
1983
|
+
} catch (err) {
|
|
1984
|
+
log(`[skill-sync] Failed to create destination skills directory: ${destSkillsDir}`, err);
|
|
1985
|
+
}
|
|
1986
|
+
const skillsToProcess = (options.skills ?? CUSTOM_SKILLS).map((s) => ({
|
|
1987
|
+
name: s.name,
|
|
1988
|
+
sourcePath: s.sourcePath
|
|
1989
|
+
}));
|
|
1990
|
+
for (const skill of skillsToProcess) {
|
|
1991
|
+
try {
|
|
1992
|
+
const sourcePath = path2.join(packageRoot, skill.sourcePath);
|
|
1993
|
+
try {
|
|
1994
|
+
const stat = lstatSync(sourcePath);
|
|
1995
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
const skillMdPath = path2.join(sourcePath, "SKILL.md");
|
|
1999
|
+
const skillMdStat = lstatSync(skillMdPath);
|
|
2000
|
+
if (skillMdStat.isSymbolicLink() || !skillMdStat.isFile()) {
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
} catch {
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const destPath = path2.join(destSkillsDir, skill.name);
|
|
2007
|
+
const hadArtifacts = recoverOrphanArtifacts(destSkillsDir, skill.name);
|
|
2008
|
+
let destExists = false;
|
|
2009
|
+
let destIsDir = false;
|
|
2010
|
+
try {
|
|
2011
|
+
const destStat = lstatSync(destPath);
|
|
2012
|
+
destExists = true;
|
|
2013
|
+
destIsDir = destStat.isDirectory() && !destStat.isSymbolicLink();
|
|
2014
|
+
} catch {}
|
|
2015
|
+
if (destExists && !destIsDir) {
|
|
2016
|
+
log(`[skill-sync] Skill ${skill.name} destination is a file or symlink (conflict). Skipping.`);
|
|
2017
|
+
skippedExisting.push(skill.name);
|
|
2018
|
+
const sourceHash2 = computeDirectoryHash(sourcePath);
|
|
2019
|
+
const entry2 = manifest.skills[skill.name];
|
|
2020
|
+
if (entry2?.stagedPath) {
|
|
2021
|
+
removeManagedStagedPath(entry2.stagedPath, manifestDir, skill.name);
|
|
2022
|
+
}
|
|
2023
|
+
manifest.skills[skill.name] = {
|
|
2024
|
+
status: "conflict",
|
|
2025
|
+
packageVersion,
|
|
2026
|
+
sourceHash: sourceHash2,
|
|
2027
|
+
lastManagedHash: "",
|
|
2028
|
+
lastSeenHash: "",
|
|
2029
|
+
updatedAt: new Date().toISOString()
|
|
2030
|
+
};
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
const sourceHash = computeDirectoryHash(sourcePath);
|
|
2034
|
+
if (isManifestCorrupt) {
|
|
2035
|
+
if (!destExists) {
|
|
2036
|
+
try {
|
|
2037
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2038
|
+
installed.push(skill.name);
|
|
2039
|
+
manifest.skills[skill.name] = {
|
|
2040
|
+
status: "managed",
|
|
2041
|
+
packageVersion,
|
|
2042
|
+
sourceHash,
|
|
2043
|
+
lastManagedHash: sourceHash,
|
|
2044
|
+
lastSeenHash: sourceHash,
|
|
2045
|
+
updatedAt: new Date().toISOString()
|
|
2046
|
+
};
|
|
2047
|
+
} catch (err) {
|
|
2048
|
+
log(`[skill-sync] Failed to install missing skill ${skill.name} (corrupt manifest mode):`, err);
|
|
2049
|
+
failed.push(skill.name);
|
|
2050
|
+
}
|
|
2051
|
+
} else {
|
|
2052
|
+
log(`[skill-sync] Skipping existing skill ${skill.name} because manifest is corrupt.`);
|
|
2053
|
+
skippedExisting.push(skill.name);
|
|
2054
|
+
const destHash2 = computeDirectoryHash(destPath);
|
|
2055
|
+
if (destHash2 === sourceHash) {
|
|
2056
|
+
manifest.skills[skill.name] = {
|
|
2057
|
+
status: "managed",
|
|
2058
|
+
packageVersion,
|
|
2059
|
+
sourceHash,
|
|
2060
|
+
lastManagedHash: sourceHash,
|
|
2061
|
+
lastSeenHash: sourceHash,
|
|
2062
|
+
updatedAt: new Date().toISOString()
|
|
2063
|
+
};
|
|
2064
|
+
} else {
|
|
2065
|
+
try {
|
|
2066
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2067
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2068
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2069
|
+
}
|
|
2070
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2071
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2072
|
+
manifest.skills[skill.name] = {
|
|
2073
|
+
status: "customized",
|
|
2074
|
+
packageVersion,
|
|
2075
|
+
sourceHash,
|
|
2076
|
+
lastManagedHash: "",
|
|
2077
|
+
lastSeenHash: destHash2,
|
|
2078
|
+
stagedPath: stagedSkillDir,
|
|
2079
|
+
updatedAt: new Date().toISOString()
|
|
2080
|
+
};
|
|
2081
|
+
staged.push(skill.name);
|
|
2082
|
+
customized.push(skill.name);
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name} during recovery:`, err);
|
|
2085
|
+
manifest.skills[skill.name] = {
|
|
2086
|
+
status: "customized",
|
|
2087
|
+
packageVersion: "unknown",
|
|
2088
|
+
sourceHash: "",
|
|
2089
|
+
lastManagedHash: "",
|
|
2090
|
+
lastSeenHash: destHash2,
|
|
2091
|
+
updatedAt: new Date().toISOString()
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
const entry = manifest.skills[skill.name];
|
|
2099
|
+
if (!destExists) {
|
|
2100
|
+
if (entry && entry.status === "deleted") {
|
|
2101
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user. Skipping.`);
|
|
2102
|
+
skippedExisting.push(skill.name);
|
|
2103
|
+
continue;
|
|
2104
|
+
}
|
|
2105
|
+
if (entry && entry.status !== "deleted") {
|
|
2106
|
+
if (hadArtifacts) {
|
|
2107
|
+
log(`[skill-sync] Managed skill ${skill.name} has backup/staging artifacts. Skipping delete, re-installing.`);
|
|
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
|
+
} catch (err) {
|
|
2120
|
+
log(`[skill-sync] Failed to re-install skill ${skill.name}:`, err);
|
|
2121
|
+
failed.push(skill.name);
|
|
2122
|
+
}
|
|
2123
|
+
continue;
|
|
2124
|
+
} else {
|
|
2125
|
+
if (entry.stagedPath) {
|
|
2126
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2127
|
+
delete entry.stagedPath;
|
|
2128
|
+
}
|
|
2129
|
+
const rawEntry = entry;
|
|
2130
|
+
delete rawEntry.stagedVersion;
|
|
2131
|
+
delete rawEntry.stagedHash;
|
|
2132
|
+
entry.status = "deleted";
|
|
2133
|
+
entry.updatedAt = new Date().toISOString();
|
|
2134
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user (detected now). Skipping.`);
|
|
2135
|
+
skippedExisting.push(skill.name);
|
|
2136
|
+
continue;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
try {
|
|
2140
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2141
|
+
installed.push(skill.name);
|
|
2142
|
+
manifest.skills[skill.name] = {
|
|
2143
|
+
status: "managed",
|
|
2144
|
+
packageVersion,
|
|
2145
|
+
sourceHash,
|
|
2146
|
+
lastManagedHash: sourceHash,
|
|
2147
|
+
lastSeenHash: sourceHash,
|
|
2148
|
+
updatedAt: new Date().toISOString()
|
|
2149
|
+
};
|
|
2150
|
+
log(`[skill-sync] Successfully installed missing skill: ${skill.name}`);
|
|
2151
|
+
} catch (err) {
|
|
2152
|
+
log(`[skill-sync] Failed to install skill ${skill.name}:`, err);
|
|
2153
|
+
failed.push(skill.name);
|
|
2154
|
+
}
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
const destHash = computeDirectoryHash(destPath);
|
|
2158
|
+
if (entry) {
|
|
2159
|
+
if (entry.status === "managed") {
|
|
2160
|
+
if (destHash === entry.lastManagedHash) {
|
|
2161
|
+
if (destHash === sourceHash) {
|
|
2162
|
+
entry.packageVersion = packageVersion;
|
|
2163
|
+
entry.sourceHash = sourceHash;
|
|
2164
|
+
entry.lastManagedHash = sourceHash;
|
|
2165
|
+
entry.lastSeenHash = sourceHash;
|
|
2166
|
+
entry.updatedAt = new Date().toISOString();
|
|
2167
|
+
skippedExisting.push(skill.name);
|
|
2168
|
+
} else {
|
|
2169
|
+
try {
|
|
2170
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2171
|
+
installed.push(skill.name);
|
|
2172
|
+
manifest.skills[skill.name] = {
|
|
2173
|
+
status: "managed",
|
|
2174
|
+
packageVersion,
|
|
2175
|
+
sourceHash,
|
|
2176
|
+
lastManagedHash: sourceHash,
|
|
2177
|
+
lastSeenHash: sourceHash,
|
|
2178
|
+
updatedAt: new Date().toISOString()
|
|
2179
|
+
};
|
|
2180
|
+
log(`[skill-sync] Updated managed skill: ${skill.name}`);
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
log(`[skill-sync] Failed to update managed skill ${skill.name}:`, err);
|
|
2183
|
+
failed.push(skill.name);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
} else {
|
|
2187
|
+
if (destHash === sourceHash) {
|
|
2188
|
+
manifest.skills[skill.name] = {
|
|
2189
|
+
status: "managed",
|
|
2190
|
+
packageVersion,
|
|
2191
|
+
sourceHash,
|
|
2192
|
+
lastManagedHash: sourceHash,
|
|
2193
|
+
lastSeenHash: sourceHash,
|
|
2194
|
+
updatedAt: new Date().toISOString()
|
|
2195
|
+
};
|
|
2196
|
+
skippedExisting.push(skill.name);
|
|
2197
|
+
} else {
|
|
2198
|
+
try {
|
|
2199
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2200
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2201
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2202
|
+
}
|
|
2203
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2204
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2205
|
+
}
|
|
2206
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2207
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2208
|
+
entry.status = "customized";
|
|
2209
|
+
entry.lastSeenHash = destHash;
|
|
2210
|
+
entry.stagedPath = stagedSkillDir;
|
|
2211
|
+
entry.sourceHash = sourceHash;
|
|
2212
|
+
entry.packageVersion = packageVersion;
|
|
2213
|
+
entry.updatedAt = new Date().toISOString();
|
|
2214
|
+
staged.push(skill.name);
|
|
2215
|
+
customized.push(skill.name);
|
|
2216
|
+
skippedExisting.push(skill.name);
|
|
2217
|
+
log(`[skill-sync] Skill ${skill.name} is customized. Staged update at ${stagedSkillDir}`);
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2220
|
+
failed.push(skill.name);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
} else if (entry.status === "customized") {
|
|
2225
|
+
if (destHash === sourceHash) {
|
|
2226
|
+
if (entry.stagedPath) {
|
|
2227
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2228
|
+
}
|
|
2229
|
+
entry.status = "managed";
|
|
2230
|
+
entry.lastManagedHash = sourceHash;
|
|
2231
|
+
entry.lastSeenHash = sourceHash;
|
|
2232
|
+
entry.sourceHash = sourceHash;
|
|
2233
|
+
entry.packageVersion = packageVersion;
|
|
2234
|
+
delete entry.stagedPath;
|
|
2235
|
+
entry.updatedAt = new Date().toISOString();
|
|
2236
|
+
adopted.push(skill.name);
|
|
2237
|
+
skippedExisting.push(skill.name);
|
|
2238
|
+
log(`[skill-sync] Customized skill ${skill.name} converged with current version. Adopted back to managed.`);
|
|
2239
|
+
} else {
|
|
2240
|
+
entry.lastSeenHash = destHash;
|
|
2241
|
+
entry.updatedAt = new Date().toISOString();
|
|
2242
|
+
if (destHash !== sourceHash && entry.sourceHash !== sourceHash) {
|
|
2243
|
+
try {
|
|
2244
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2245
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2246
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2247
|
+
}
|
|
2248
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2249
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2250
|
+
}
|
|
2251
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2252
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2253
|
+
entry.stagedPath = stagedSkillDir;
|
|
2254
|
+
entry.sourceHash = sourceHash;
|
|
2255
|
+
entry.packageVersion = packageVersion;
|
|
2256
|
+
staged.push(skill.name);
|
|
2257
|
+
customized.push(skill.name);
|
|
2258
|
+
skippedExisting.push(skill.name);
|
|
2259
|
+
log(`[skill-sync] Staged new update for customized skill ${skill.name} at ${stagedSkillDir}`);
|
|
2260
|
+
} catch (err) {
|
|
2261
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2262
|
+
failed.push(skill.name);
|
|
2263
|
+
}
|
|
2264
|
+
} else {
|
|
2265
|
+
customized.push(skill.name);
|
|
2266
|
+
skippedExisting.push(skill.name);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
} else if (entry.status === "deleted") {
|
|
2270
|
+
if (destHash === sourceHash) {
|
|
2271
|
+
entry.status = "managed";
|
|
2272
|
+
entry.packageVersion = packageVersion;
|
|
2273
|
+
entry.sourceHash = sourceHash;
|
|
2274
|
+
entry.lastManagedHash = sourceHash;
|
|
2275
|
+
entry.lastSeenHash = sourceHash;
|
|
2276
|
+
entry.updatedAt = new Date().toISOString();
|
|
2277
|
+
skippedExisting.push(skill.name);
|
|
2278
|
+
adopted.push(skill.name);
|
|
2279
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (matching current). Adopted as managed.`);
|
|
2280
|
+
} else {
|
|
2281
|
+
try {
|
|
2282
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2283
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2284
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2285
|
+
}
|
|
2286
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2287
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2288
|
+
}
|
|
2289
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2290
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2291
|
+
entry.status = "customized";
|
|
2292
|
+
entry.packageVersion = packageVersion;
|
|
2293
|
+
entry.sourceHash = sourceHash;
|
|
2294
|
+
entry.lastManagedHash = sourceHash;
|
|
2295
|
+
entry.lastSeenHash = destHash;
|
|
2296
|
+
entry.stagedPath = stagedSkillDir;
|
|
2297
|
+
entry.updatedAt = new Date().toISOString();
|
|
2298
|
+
staged.push(skill.name);
|
|
2299
|
+
customized.push(skill.name);
|
|
2300
|
+
skippedExisting.push(skill.name);
|
|
2301
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (custom). Marked customized and staged.`);
|
|
2302
|
+
} catch (err) {
|
|
2303
|
+
log(`[skill-sync] Failed to stage update for deleted/recreated skill ${skill.name}:`, err);
|
|
2304
|
+
failed.push(skill.name);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
} else if (entry.status === "conflict") {
|
|
2308
|
+
if (destHash === sourceHash) {
|
|
2309
|
+
if (entry.stagedPath) {
|
|
2310
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2311
|
+
}
|
|
2312
|
+
entry.status = "managed";
|
|
2313
|
+
entry.packageVersion = packageVersion;
|
|
2314
|
+
entry.sourceHash = sourceHash;
|
|
2315
|
+
entry.lastManagedHash = sourceHash;
|
|
2316
|
+
entry.lastSeenHash = sourceHash;
|
|
2317
|
+
delete entry.stagedPath;
|
|
2318
|
+
entry.updatedAt = new Date().toISOString();
|
|
2319
|
+
adopted.push(skill.name);
|
|
2320
|
+
} else {
|
|
2321
|
+
try {
|
|
2322
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2323
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2324
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2325
|
+
}
|
|
2326
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2327
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2328
|
+
}
|
|
2329
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2330
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2331
|
+
entry.status = "customized";
|
|
2332
|
+
entry.packageVersion = packageVersion;
|
|
2333
|
+
entry.sourceHash = sourceHash;
|
|
2334
|
+
entry.lastManagedHash = sourceHash;
|
|
2335
|
+
entry.lastSeenHash = destHash;
|
|
2336
|
+
entry.stagedPath = stagedSkillDir;
|
|
2337
|
+
entry.updatedAt = new Date().toISOString();
|
|
2338
|
+
staged.push(skill.name);
|
|
2339
|
+
customized.push(skill.name);
|
|
2340
|
+
skippedExisting.push(skill.name);
|
|
2341
|
+
log(`[skill-sync] Conflicted skill ${skill.name} recovered as customized and staged at ${stagedSkillDir}`);
|
|
2342
|
+
} catch (err) {
|
|
2343
|
+
log(`[skill-sync] Failed to stage update for conflicted skill ${skill.name}:`, err);
|
|
2344
|
+
failed.push(skill.name);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
} else {
|
|
2349
|
+
if (destHash === sourceHash) {
|
|
2350
|
+
manifest.skills[skill.name] = {
|
|
2351
|
+
status: "managed",
|
|
2352
|
+
packageVersion,
|
|
2353
|
+
sourceHash,
|
|
2354
|
+
lastManagedHash: sourceHash,
|
|
2355
|
+
lastSeenHash: sourceHash,
|
|
2356
|
+
updatedAt: new Date().toISOString()
|
|
2357
|
+
};
|
|
2358
|
+
skippedExisting.push(skill.name);
|
|
2359
|
+
adopted.push(skill.name);
|
|
2360
|
+
log(`[skill-sync] Adopted existing matching skill: ${skill.name}`);
|
|
2361
|
+
} else if (LEGACY_MANAGED_SKILL_HASHES[skill.name]?.includes(destHash)) {
|
|
2362
|
+
try {
|
|
2363
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2364
|
+
installed.push(skill.name);
|
|
2365
|
+
manifest.skills[skill.name] = {
|
|
2366
|
+
status: "managed",
|
|
2367
|
+
packageVersion,
|
|
2368
|
+
sourceHash,
|
|
2369
|
+
lastManagedHash: sourceHash,
|
|
2370
|
+
lastSeenHash: sourceHash,
|
|
2371
|
+
updatedAt: new Date().toISOString()
|
|
2372
|
+
};
|
|
2373
|
+
log(`[skill-sync] Adopted and updated legacy skill: ${skill.name}`);
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
log(`[skill-sync] Failed to update legacy skill ${skill.name}:`, err);
|
|
2376
|
+
failed.push(skill.name);
|
|
2377
|
+
}
|
|
2378
|
+
} else {
|
|
2379
|
+
try {
|
|
2380
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2381
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2382
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2383
|
+
}
|
|
2384
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2385
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2386
|
+
manifest.skills[skill.name] = {
|
|
2387
|
+
status: "customized",
|
|
2388
|
+
packageVersion,
|
|
2389
|
+
sourceHash,
|
|
2390
|
+
lastManagedHash: "",
|
|
2391
|
+
lastSeenHash: destHash,
|
|
2392
|
+
stagedPath: stagedSkillDir,
|
|
2393
|
+
updatedAt: new Date().toISOString()
|
|
2394
|
+
};
|
|
2395
|
+
staged.push(skill.name);
|
|
2396
|
+
customized.push(skill.name);
|
|
2397
|
+
skippedExisting.push(skill.name);
|
|
2398
|
+
log(`[skill-sync] Skill ${skill.name} is customized (no manifest entry). Staged update at ${stagedSkillDir}`);
|
|
2399
|
+
} catch (err) {
|
|
2400
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2401
|
+
failed.push(skill.name);
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
} catch (err) {
|
|
2406
|
+
log(`[skill-sync] Failed processing skill ${skill.name}:`, err);
|
|
2407
|
+
failed.push(skill.name);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
let manifestWriteFailed = false;
|
|
2411
|
+
manifest.updatedAt = new Date().toISOString();
|
|
2412
|
+
const tempManifestPath = `${manifestPath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
|
|
2413
|
+
try {
|
|
2414
|
+
writeFileSync2(tempManifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2415
|
+
renameSync2(tempManifestPath, manifestPath);
|
|
2416
|
+
} catch (err) {
|
|
2417
|
+
log("[skill-sync] Failed to write skills manifest atomically:", err);
|
|
2418
|
+
manifestWriteFailed = true;
|
|
2419
|
+
try {
|
|
2420
|
+
if (existsSync4(tempManifestPath)) {
|
|
2421
|
+
unlinkSync(tempManifestPath);
|
|
2422
|
+
}
|
|
2423
|
+
} catch {}
|
|
2424
|
+
}
|
|
2425
|
+
if (manifestWriteFailed) {
|
|
2426
|
+
failed.push("__manifest__");
|
|
2427
|
+
}
|
|
2428
|
+
} finally {
|
|
2429
|
+
releaseLock(lockDir);
|
|
2430
|
+
}
|
|
2431
|
+
return {
|
|
2432
|
+
installed,
|
|
2433
|
+
skippedExisting,
|
|
2434
|
+
failed,
|
|
2435
|
+
staged,
|
|
2436
|
+
adopted,
|
|
2437
|
+
customized
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
1516
2440
|
|
|
1517
2441
|
// src/cli/background-subagents.ts
|
|
1518
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as
|
|
2442
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1519
2443
|
import { homedir as homedir3 } from "node:os";
|
|
1520
2444
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
1521
2445
|
var ENV_NAME = "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS";
|
|
@@ -1604,57 +2528,30 @@ function upsertBackgroundSubagentsBlock(content, block) {
|
|
|
1604
2528
|
}
|
|
1605
2529
|
function writeBackgroundSubagentsBlock(targetPath) {
|
|
1606
2530
|
const block = getBackgroundSubagentsBlock(targetPath);
|
|
1607
|
-
const content = existsSync5(targetPath) ?
|
|
2531
|
+
const content = existsSync5(targetPath) ? readFileSync5(targetPath, "utf8") : "";
|
|
1608
2532
|
const nextContent = upsertBackgroundSubagentsBlock(content, block);
|
|
1609
2533
|
mkdirSync4(dirname4(targetPath), { recursive: true });
|
|
1610
|
-
|
|
2534
|
+
writeFileSync3(targetPath, nextContent);
|
|
1611
2535
|
}
|
|
1612
2536
|
|
|
1613
2537
|
// src/companion/updater.ts
|
|
1614
2538
|
init_compat();
|
|
1615
|
-
import { createHash } from "node:crypto";
|
|
2539
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1616
2540
|
import {
|
|
1617
2541
|
chmodSync,
|
|
1618
2542
|
copyFileSync as copyFileSync3,
|
|
1619
2543
|
existsSync as existsSync6,
|
|
1620
2544
|
mkdirSync as mkdirSync5,
|
|
1621
2545
|
mkdtempSync,
|
|
1622
|
-
readFileSync as
|
|
1623
|
-
renameSync as
|
|
1624
|
-
rmSync as
|
|
1625
|
-
statSync as
|
|
1626
|
-
writeFileSync as
|
|
2546
|
+
readFileSync as readFileSync6,
|
|
2547
|
+
renameSync as renameSync3,
|
|
2548
|
+
rmSync as rmSync3,
|
|
2549
|
+
statSync as statSync3,
|
|
2550
|
+
writeFileSync as writeFileSync4
|
|
1627
2551
|
} from "node:fs";
|
|
1628
2552
|
import { homedir as homedir4, platform, tmpdir } from "node:os";
|
|
1629
|
-
import * as
|
|
2553
|
+
import * as path3 from "node:path";
|
|
1630
2554
|
import { setTimeout as delay } from "node:timers/promises";
|
|
1631
|
-
|
|
1632
|
-
// src/utils/logger.ts
|
|
1633
|
-
import { appendFile } from "node:fs/promises";
|
|
1634
|
-
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1635
|
-
var logFile = null;
|
|
1636
|
-
var writeChain = Promise.resolve();
|
|
1637
|
-
function log(message, data) {
|
|
1638
|
-
const target = logFile;
|
|
1639
|
-
if (!target)
|
|
1640
|
-
return;
|
|
1641
|
-
try {
|
|
1642
|
-
const timestamp = new Date().toISOString();
|
|
1643
|
-
let dataStr = "";
|
|
1644
|
-
if (data !== undefined) {
|
|
1645
|
-
try {
|
|
1646
|
-
dataStr = JSON.stringify(data);
|
|
1647
|
-
} catch {
|
|
1648
|
-
dataStr = "[unserializable]";
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
1652
|
-
`;
|
|
1653
|
-
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
1654
|
-
} catch {}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
// src/companion/updater.ts
|
|
1658
2555
|
var DOWNLOAD_TIMEOUT_MS = 30000;
|
|
1659
2556
|
var LOCK_TIMEOUT_MS = 2000;
|
|
1660
2557
|
var STALE_LOCK_MS = 5 * 60000;
|
|
@@ -1692,8 +2589,8 @@ function getCompanionTarget() {
|
|
|
1692
2589
|
}
|
|
1693
2590
|
function getCompanionBinaryPath() {
|
|
1694
2591
|
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
1695
|
-
const base = xdg &&
|
|
1696
|
-
return
|
|
2592
|
+
const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(homedir4(), ".local", "share");
|
|
2593
|
+
return path3.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
|
|
1697
2594
|
}
|
|
1698
2595
|
async function ensureCompanionVersion(options) {
|
|
1699
2596
|
const { config, dryRun = false } = options;
|
|
@@ -1790,7 +2687,7 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1790
2687
|
} finally {
|
|
1791
2688
|
clearTimeout(timeout);
|
|
1792
2689
|
}
|
|
1793
|
-
const checksum =
|
|
2690
|
+
const checksum = createHash2("sha256").update(Buffer.from(buffer)).digest("hex");
|
|
1794
2691
|
if (checksum !== expectedChecksum) {
|
|
1795
2692
|
return {
|
|
1796
2693
|
status: "failed",
|
|
@@ -1800,10 +2697,10 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1800
2697
|
}
|
|
1801
2698
|
let tempDir = "";
|
|
1802
2699
|
try {
|
|
1803
|
-
tempDir = mkdtempSync(
|
|
1804
|
-
const archivePath =
|
|
1805
|
-
|
|
1806
|
-
const extractedDir =
|
|
2700
|
+
tempDir = mkdtempSync(path3.join(tmpdir(), "companion-install-"));
|
|
2701
|
+
const archivePath = path3.join(tempDir, archiveName);
|
|
2702
|
+
writeFileSync4(archivePath, Buffer.from(buffer));
|
|
2703
|
+
const extractedDir = path3.join(tempDir, "extracted");
|
|
1807
2704
|
mkdirSync5(extractedDir, { recursive: true });
|
|
1808
2705
|
if (isWindows) {
|
|
1809
2706
|
const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
|
|
@@ -1821,7 +2718,7 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1821
2718
|
}
|
|
1822
2719
|
}
|
|
1823
2720
|
const binaryName = isWindows ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion";
|
|
1824
|
-
const extractedBinaryPath =
|
|
2721
|
+
const extractedBinaryPath = path3.join(extractedDir, binaryName);
|
|
1825
2722
|
if (!existsSync6(extractedBinaryPath)) {
|
|
1826
2723
|
return {
|
|
1827
2724
|
status: "failed",
|
|
@@ -1829,14 +2726,14 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1829
2726
|
error: `Binary ${binaryName} not found in extracted archive`
|
|
1830
2727
|
};
|
|
1831
2728
|
}
|
|
1832
|
-
const binDir =
|
|
2729
|
+
const binDir = path3.dirname(finalBinaryPath);
|
|
1833
2730
|
mkdirSync5(binDir, { recursive: true });
|
|
1834
2731
|
const tmpFinalPath = `${finalBinaryPath}.tmp`;
|
|
1835
2732
|
copyFileSync3(extractedBinaryPath, tmpFinalPath);
|
|
1836
2733
|
if (!isWindows) {
|
|
1837
2734
|
chmodSync(tmpFinalPath, 493);
|
|
1838
2735
|
}
|
|
1839
|
-
|
|
2736
|
+
renameSync3(tmpFinalPath, finalBinaryPath);
|
|
1840
2737
|
writeInstallMetadata(finalBinaryPath, {
|
|
1841
2738
|
version: manifest.version,
|
|
1842
2739
|
tag: manifest.tag,
|
|
@@ -1859,14 +2756,14 @@ async function installCompanionArchive(finalBinaryPath, target, manifest, downlo
|
|
|
1859
2756
|
} finally {
|
|
1860
2757
|
if (tempDir) {
|
|
1861
2758
|
try {
|
|
1862
|
-
|
|
2759
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
1863
2760
|
} catch {}
|
|
1864
2761
|
}
|
|
1865
2762
|
}
|
|
1866
2763
|
}
|
|
1867
2764
|
function readInstallMetadata(binaryPath) {
|
|
1868
2765
|
try {
|
|
1869
|
-
const parsed = JSON.parse(
|
|
2766
|
+
const parsed = JSON.parse(readFileSync6(metadataPath(binaryPath), "utf8"));
|
|
1870
2767
|
if (parsed?.version && parsed.tag && parsed.target) {
|
|
1871
2768
|
return parsed;
|
|
1872
2769
|
}
|
|
@@ -1874,7 +2771,7 @@ function readInstallMetadata(binaryPath) {
|
|
|
1874
2771
|
return null;
|
|
1875
2772
|
}
|
|
1876
2773
|
function writeInstallMetadata(binaryPath, metadata) {
|
|
1877
|
-
|
|
2774
|
+
writeFileSync4(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
|
|
1878
2775
|
}
|
|
1879
2776
|
function metadataPath(binaryPath) {
|
|
1880
2777
|
return `${binaryPath}.json`;
|
|
@@ -1883,7 +2780,7 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
|
|
|
1883
2780
|
const lock = `${binaryPath}.lock`;
|
|
1884
2781
|
const deadline = Date.now() + (timeoutMs ?? LOCK_TIMEOUT_MS);
|
|
1885
2782
|
const staleAfterMs = staleMs ?? STALE_LOCK_MS;
|
|
1886
|
-
mkdirSync5(
|
|
2783
|
+
mkdirSync5(path3.dirname(binaryPath), { recursive: true });
|
|
1887
2784
|
while (Date.now() <= deadline) {
|
|
1888
2785
|
try {
|
|
1889
2786
|
mkdirSync5(lock);
|
|
@@ -1891,7 +2788,7 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
|
|
|
1891
2788
|
return await run();
|
|
1892
2789
|
} finally {
|
|
1893
2790
|
try {
|
|
1894
|
-
|
|
2791
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
1895
2792
|
} catch {}
|
|
1896
2793
|
}
|
|
1897
2794
|
} catch (err) {
|
|
@@ -1899,9 +2796,9 @@ async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
|
|
|
1899
2796
|
if (code !== "EEXIST")
|
|
1900
2797
|
throw err;
|
|
1901
2798
|
try {
|
|
1902
|
-
const ageMs = Date.now() -
|
|
2799
|
+
const ageMs = Date.now() - statSync3(lock).mtimeMs;
|
|
1903
2800
|
if (ageMs > staleAfterMs) {
|
|
1904
|
-
|
|
2801
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
1905
2802
|
log("[companion] removed stale install lock", lock);
|
|
1906
2803
|
continue;
|
|
1907
2804
|
}
|
|
@@ -1988,7 +2885,7 @@ async function installCompanion(config) {
|
|
|
1988
2885
|
// src/cli/system.ts
|
|
1989
2886
|
init_compat();
|
|
1990
2887
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1991
|
-
import { statSync as
|
|
2888
|
+
import { statSync as statSync4 } from "node:fs";
|
|
1992
2889
|
var cachedOpenCodePath = null;
|
|
1993
2890
|
function resolvePathCommand(command) {
|
|
1994
2891
|
try {
|
|
@@ -2061,7 +2958,7 @@ function resolveOpenCodePath() {
|
|
|
2061
2958
|
if (opencodePath === "opencode")
|
|
2062
2959
|
continue;
|
|
2063
2960
|
try {
|
|
2064
|
-
const stat =
|
|
2961
|
+
const stat = statSync4(opencodePath);
|
|
2065
2962
|
if (stat.isFile()) {
|
|
2066
2963
|
cachedOpenCodePath = opencodePath;
|
|
2067
2964
|
return opencodePath;
|
|
@@ -2110,8 +3007,8 @@ async function getOpenCodeVersion() {
|
|
|
2110
3007
|
return null;
|
|
2111
3008
|
}
|
|
2112
3009
|
function getOpenCodePath() {
|
|
2113
|
-
const
|
|
2114
|
-
return
|
|
3010
|
+
const path4 = resolveOpenCodePath();
|
|
3011
|
+
return path4 === "opencode" ? null : path4;
|
|
2115
3012
|
}
|
|
2116
3013
|
// src/cli/install.ts
|
|
2117
3014
|
var GREEN = "\x1B[32m";
|
|
@@ -2194,11 +3091,11 @@ async function checkOpenCodeInstalled() {
|
|
|
2194
3091
|
return { ok: false };
|
|
2195
3092
|
}
|
|
2196
3093
|
const version = await getOpenCodeVersion();
|
|
2197
|
-
const
|
|
3094
|
+
const path4 = getOpenCodePath();
|
|
2198
3095
|
const detectedVersion = version ?? "";
|
|
2199
|
-
const pathInfo =
|
|
3096
|
+
const pathInfo = path4 ? ` (${DIM}${path4}${RESET})` : "";
|
|
2200
3097
|
printSuccess(`OpenCode ${detectedVersion} detected${pathInfo}`);
|
|
2201
|
-
return { ok: true, version: version ?? undefined, path:
|
|
3098
|
+
return { ok: true, version: version ?? undefined, path: path4 ?? undefined };
|
|
2202
3099
|
}
|
|
2203
3100
|
async function configureBackgroundSubagents(config) {
|
|
2204
3101
|
if (isBackgroundSubagentsEnabled(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS)) {
|
|
@@ -2260,9 +3157,9 @@ async function shouldInstallCompanion(config) {
|
|
|
2260
3157
|
if (config.companion === "no")
|
|
2261
3158
|
return false;
|
|
2262
3159
|
if (config.dryRun) {
|
|
2263
|
-
printInfo("Dry run mode - would ask to install the desktop companion (default:
|
|
2264
|
-
config.companion = "
|
|
2265
|
-
return
|
|
3160
|
+
printInfo("Dry run mode - would ask to install the desktop companion (default: no).");
|
|
3161
|
+
config.companion = "no";
|
|
3162
|
+
return false;
|
|
2266
3163
|
}
|
|
2267
3164
|
if (!process.stdin.isTTY) {
|
|
2268
3165
|
printInfo("Skipped desktop companion prompt in non-TTY mode. Use --companion=yes to install it.");
|
|
@@ -2271,7 +3168,7 @@ async function shouldInstallCompanion(config) {
|
|
|
2271
3168
|
}
|
|
2272
3169
|
console.log();
|
|
2273
3170
|
printInfo("The optional desktop companion shows live agent activity.");
|
|
2274
|
-
const shouldInstall = await confirm("Install and enable the desktop companion?",
|
|
3171
|
+
const shouldInstall = await confirm("Install and enable the desktop companion?", false);
|
|
2275
3172
|
config.companion = shouldInstall ? "yes" : "no";
|
|
2276
3173
|
if (!shouldInstall) {
|
|
2277
3174
|
printInfo("Desktop companion install skipped.");
|
|
@@ -2390,25 +3287,63 @@ ${JSON.stringify(liteConfig, null, 2)}
|
|
|
2390
3287
|
}
|
|
2391
3288
|
}
|
|
2392
3289
|
if (config.installCustomSkills) {
|
|
2393
|
-
printStep(step++, totalSteps, "
|
|
3290
|
+
printStep(step++, totalSteps, "Synchronizing custom skills...");
|
|
2394
3291
|
if (config.dryRun) {
|
|
2395
|
-
printInfo("Dry run mode - would
|
|
3292
|
+
printInfo("Dry run mode - would synchronize custom skills:");
|
|
2396
3293
|
for (const skill of CUSTOM_SKILLS) {
|
|
2397
3294
|
printInfo(` - ${skill.name}`);
|
|
2398
3295
|
}
|
|
2399
3296
|
} else {
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
3297
|
+
try {
|
|
3298
|
+
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
3299
|
+
const result = syncBundledSkillsFromPackage(packageRoot);
|
|
3300
|
+
const categorizedSkipped = new Set([
|
|
3301
|
+
...result.staged,
|
|
3302
|
+
...result.adopted,
|
|
3303
|
+
...result.customized
|
|
3304
|
+
]);
|
|
3305
|
+
const preservedSkills = result.skippedExisting.filter((skill) => !categorizedSkipped.has(skill));
|
|
3306
|
+
if (result.installed.length > 0) {
|
|
3307
|
+
for (const skill of result.installed) {
|
|
3308
|
+
printSuccess(`Installed/Updated: ${skill}`);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
if (preservedSkills.length > 0) {
|
|
3312
|
+
for (const skill of preservedSkills) {
|
|
3313
|
+
printInfo(`Skipped/Preserved: ${skill}`);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
if (result.failed.length > 0) {
|
|
3317
|
+
for (const skill of result.failed) {
|
|
3318
|
+
if (skill === "__lock__") {
|
|
3319
|
+
printError("Lock acquisition failed");
|
|
3320
|
+
} else if (skill === "__manifest__") {
|
|
3321
|
+
printError("Manifest write failed");
|
|
3322
|
+
} else {
|
|
3323
|
+
printError(`Failed: ${skill}`);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
if (result.staged.length > 0) {
|
|
3328
|
+
for (const skill of result.staged) {
|
|
3329
|
+
printInfo(`Staged for review: ${skill}`);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (result.adopted.length > 0) {
|
|
3333
|
+
for (const skill of result.adopted) {
|
|
3334
|
+
printInfo(`Adopted: ${skill}`);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (result.customized.length > 0) {
|
|
3338
|
+
for (const skill of result.customized) {
|
|
3339
|
+
printInfo(`Customized: ${skill}`);
|
|
3340
|
+
}
|
|
2408
3341
|
}
|
|
3342
|
+
const realFailed = result.failed.filter((skill) => skill !== "__lock__" && skill !== "__manifest__");
|
|
3343
|
+
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.`);
|
|
3344
|
+
} catch (err) {
|
|
3345
|
+
printError(`Failed to synchronize custom skills: ${err}`);
|
|
2409
3346
|
}
|
|
2410
|
-
const totalCustom = CUSTOM_SKILLS.length;
|
|
2411
|
-
printSuccess(`${customSkillsInstalled}/${totalCustom} custom skills processed`);
|
|
2412
3347
|
}
|
|
2413
3348
|
}
|
|
2414
3349
|
const statusMsg = isUpdate ? "Configuration updated!" : "Installation complete!";
|
|
@@ -2532,7 +3467,7 @@ Usage:
|
|
|
2532
3467
|
Options:
|
|
2533
3468
|
--skills=yes|no Install bundled skills (default: yes)
|
|
2534
3469
|
--companion=ask|yes|no Install desktop companion binary and enable config
|
|
2535
|
-
(default: ask; prompt defaults to
|
|
3470
|
+
(default: ask; prompt defaults to no)
|
|
2536
3471
|
--preset=<name> Active generated config preset (default: openai)
|
|
2537
3472
|
--background-subagents=ask|yes|no
|
|
2538
3473
|
Persist required OpenCode background subagent env
|