@otto-assistant/otto 0.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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +257 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +264 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +202 -0
- package/dist/config.test.js.map +1 -0
- package/dist/detect.d.ts +9 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +40 -0
- package/dist/detect.js.map +1 -0
- package/dist/detect.test.d.ts +2 -0
- package/dist/detect.test.d.ts.map +1 -0
- package/dist/detect.test.js +25 -0
- package/dist/detect.test.js.map +1 -0
- package/dist/health.d.ts +27 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +78 -0
- package/dist/health.js.map +1 -0
- package/dist/health.test.d.ts +2 -0
- package/dist/health.test.d.ts.map +1 -0
- package/dist/health.test.js +33 -0
- package/dist/health.test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +8 -0
- package/dist/index.test.js.map +1 -0
- package/dist/installer.d.ts +10 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +50 -0
- package/dist/installer.js.map +1 -0
- package/dist/installer.test.d.ts +2 -0
- package/dist/installer.test.d.ts.map +1 -0
- package/dist/installer.test.js +43 -0
- package/dist/installer.test.js.map +1 -0
- package/dist/lifecycle.d.ts +4 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +30 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/lifecycle.test.d.ts +2 -0
- package/dist/lifecycle.test.d.ts.map +1 -0
- package/dist/lifecycle.test.js +19 -0
- package/dist/lifecycle.test.js.map +1 -0
- package/dist/manifest.d.ts +18 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +30 -0
- package/dist/manifest.js.map +1 -0
- package/dist/sync.d.ts +10 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +39 -0
- package/dist/sync.js.map +1 -0
- package/package.json +41 -0
- package/src/cli.ts +291 -0
- package/src/config.test.ts +237 -0
- package/src/config.ts +362 -0
- package/src/detect.test.ts +28 -0
- package/src/detect.ts +52 -0
- package/src/health.test.ts +39 -0
- package/src/health.ts +114 -0
- package/src/index.test.ts +8 -0
- package/src/index.ts +28 -0
- package/src/installer.test.ts +52 -0
- package/src/installer.ts +62 -0
- package/src/lifecycle.test.ts +22 -0
- package/src/lifecycle.ts +30 -0
- package/src/manifest.ts +42 -0
- package/src/sync.ts +53 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
checkPackagePresence,
|
|
4
|
+
checkConfigHealth,
|
|
5
|
+
checkDirectoryHealth,
|
|
6
|
+
} from "./health.js"
|
|
7
|
+
|
|
8
|
+
describe("health", () => {
|
|
9
|
+
it("checkPackagePresence returns results for all manifest packages", { timeout: 30_000 }, () => {
|
|
10
|
+
const results = checkPackagePresence()
|
|
11
|
+
expect(results).toHaveLength(2) // opencode-ai + kimaki (not opencode-agent-memory — it's a plugin)
|
|
12
|
+
for (const r of results) {
|
|
13
|
+
expect(r).toHaveProperty("name")
|
|
14
|
+
expect(r).toHaveProperty("installed")
|
|
15
|
+
expect(r).toHaveProperty("status")
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("checkConfigHealth returns structured result", () => {
|
|
20
|
+
const result = checkConfigHealth()
|
|
21
|
+
expect(result).toHaveProperty("opencodeJson")
|
|
22
|
+
expect(result).toHaveProperty("ottoJson")
|
|
23
|
+
expect(result).toHaveProperty("agentMemoryJson")
|
|
24
|
+
expect(result).toHaveProperty("memoryPluginEnabled")
|
|
25
|
+
expect(result).toHaveProperty("subagentPolicyInjected")
|
|
26
|
+
expect(result).toHaveProperty("subagentThreadsEnabled")
|
|
27
|
+
expect(result).toHaveProperty("subagentThreadsAskBeforeDelete")
|
|
28
|
+
expect(result).toHaveProperty("subagentThreadsAutoDelete")
|
|
29
|
+
expect(result).toHaveProperty("plugins")
|
|
30
|
+
expect(result).toHaveProperty("kimakiRunning")
|
|
31
|
+
expect(Array.isArray(result.plugins)).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("checkDirectoryHealth returns results", () => {
|
|
35
|
+
const results = checkDirectoryHealth()
|
|
36
|
+
expect(Array.isArray(results)).toBe(true)
|
|
37
|
+
expect(results.length).toBeGreaterThan(0)
|
|
38
|
+
})
|
|
39
|
+
})
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { getInstalledVersion } from "./detect.js"
|
|
4
|
+
import { readOpenCodeConfigState, readOttoConfig, type OpenCodeConfig } from "./config.js"
|
|
5
|
+
import { MANIFEST, OPENCODE_CONFIG_DIR, KIMAKI_DATA_DIR } from "./manifest.js"
|
|
6
|
+
import { isKimakiRunning } from "./lifecycle.js"
|
|
7
|
+
|
|
8
|
+
export interface HealthResult {
|
|
9
|
+
name: string
|
|
10
|
+
status: "ok" | "warn" | "error"
|
|
11
|
+
message: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PackageCheck {
|
|
15
|
+
name: string
|
|
16
|
+
installed: string | null
|
|
17
|
+
required: string
|
|
18
|
+
status: "ok" | "missing"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SUBAGENT_POLICY_MARKER = "Otto subagent policy (must follow):"
|
|
22
|
+
|
|
23
|
+
function subagentPolicyInjectedInPrompts(config: OpenCodeConfig): boolean {
|
|
24
|
+
const agent = config.agent
|
|
25
|
+
if (!agent || typeof agent !== "object" || Array.isArray(agent)) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
for (const value of Object.values(agent as Record<string, unknown>)) {
|
|
29
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
30
|
+
const prompt = (value as { prompt?: unknown }).prompt
|
|
31
|
+
if (typeof prompt === "string" && prompt.includes(SUBAGENT_POLICY_MARKER)) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function checkPackagePresence(): PackageCheck[] {
|
|
40
|
+
return Object.entries(MANIFEST.packages).map(([name, required]) => {
|
|
41
|
+
const installed = getInstalledVersion(name)
|
|
42
|
+
return { name, installed, required, status: installed ? ("ok" as const) : ("missing" as const) }
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ConfigHealth {
|
|
47
|
+
opencodeJson: "ok" | "missing" | "error"
|
|
48
|
+
ottoJson: "ok" | "missing"
|
|
49
|
+
agentMemoryJson: "ok" | "missing"
|
|
50
|
+
memoryPluginEnabled: boolean
|
|
51
|
+
subagentPolicyInjected: boolean
|
|
52
|
+
subagentThreadsEnabled: boolean
|
|
53
|
+
subagentThreadsAskBeforeDelete: boolean
|
|
54
|
+
subagentThreadsAutoDelete: boolean
|
|
55
|
+
plugins: string[]
|
|
56
|
+
kimakiRunning: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function checkConfigHealth(): ConfigHealth {
|
|
60
|
+
const configDir = OPENCODE_CONFIG_DIR()
|
|
61
|
+
const agentMemoryPath = path.join(configDir, "agent-memory.json")
|
|
62
|
+
const ottoJsonPath = path.join(configDir, "otto.json")
|
|
63
|
+
|
|
64
|
+
const state = readOpenCodeConfigState()
|
|
65
|
+
const opencodeJson: ConfigHealth["opencodeJson"] =
|
|
66
|
+
state.status === "missing" ? "missing" : state.status === "invalid" ? "error" : "ok"
|
|
67
|
+
const agentMemoryJson: ConfigHealth["agentMemoryJson"] = fs.existsSync(agentMemoryPath) ? "ok" : "missing"
|
|
68
|
+
const ottoJson: ConfigHealth["ottoJson"] = fs.existsSync(ottoJsonPath) ? "ok" : "missing"
|
|
69
|
+
|
|
70
|
+
const config = state.status === "ok" ? state.config : {}
|
|
71
|
+
const configuredPlugins = config.plugin ?? []
|
|
72
|
+
const memoryPluginEnabled = configuredPlugins.includes("opencode-agent-memory")
|
|
73
|
+
|
|
74
|
+
const subagentPolicyInjected = subagentPolicyInjectedInPrompts(config)
|
|
75
|
+
|
|
76
|
+
// Read Otto config from otto.json (separate from opencode.json)
|
|
77
|
+
const ottoConfig = readOttoConfig()
|
|
78
|
+
const subagentThreadsEnabled = ottoConfig.subagentThreads.enabled
|
|
79
|
+
const subagentThreadsAskBeforeDelete = ottoConfig.subagentThreads.askBeforeDelete
|
|
80
|
+
const subagentThreadsAutoDelete = ottoConfig.subagentThreads.autoDeleteOnComplete
|
|
81
|
+
const kimakiRunning = isKimakiRunning()
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
opencodeJson,
|
|
85
|
+
ottoJson,
|
|
86
|
+
agentMemoryJson,
|
|
87
|
+
memoryPluginEnabled,
|
|
88
|
+
subagentPolicyInjected,
|
|
89
|
+
subagentThreadsEnabled,
|
|
90
|
+
subagentThreadsAskBeforeDelete,
|
|
91
|
+
subagentThreadsAutoDelete,
|
|
92
|
+
plugins: configuredPlugins,
|
|
93
|
+
kimakiRunning,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function checkDirectoryHealth(): HealthResult[] {
|
|
98
|
+
const results: HealthResult[] = []
|
|
99
|
+
const dirs = [
|
|
100
|
+
{ p: OPENCODE_CONFIG_DIR(), label: "opencode config dir" },
|
|
101
|
+
{ p: path.join(OPENCODE_CONFIG_DIR(), "memory"), label: "opencode memory dir" },
|
|
102
|
+
{ p: path.join(OPENCODE_CONFIG_DIR(), "journal"), label: "opencode journal dir" },
|
|
103
|
+
{ p: KIMAKI_DATA_DIR(), label: "kimaki data dir" },
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for (const { p, label } of dirs) {
|
|
107
|
+
if (fs.existsSync(p)) {
|
|
108
|
+
results.push({ name: label, status: "ok", message: `exists: ${p}` })
|
|
109
|
+
} else {
|
|
110
|
+
results.push({ name: label, status: "warn", message: `missing: ${p}` })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return results
|
|
114
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MANIFEST } from "./manifest.js"
|
|
2
|
+
|
|
3
|
+
export function ottoVersion(): string {
|
|
4
|
+
return MANIFEST.version
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export { MANIFEST, OPENCODE_CONFIG_DIR, KIMAKI_DATA_DIR } from "./manifest.js"
|
|
8
|
+
export { getInstalledVersion, detectPackage } from "./detect.js"
|
|
9
|
+
export type { InstalledPackage } from "./detect.js"
|
|
10
|
+
export {
|
|
11
|
+
mergePlugins,
|
|
12
|
+
readOttoConfig,
|
|
13
|
+
writeOttoConfig,
|
|
14
|
+
OTTO_DEFAULTS,
|
|
15
|
+
buildSubagentThreadPolicy,
|
|
16
|
+
mergeAgentPrompts,
|
|
17
|
+
ensureSubagentThreadSkill,
|
|
18
|
+
readOpenCodeConfig,
|
|
19
|
+
readOpenCodeConfigState,
|
|
20
|
+
writeOpenCodeConfig,
|
|
21
|
+
ensureAgentMemoryConfig,
|
|
22
|
+
} from "./config.js"
|
|
23
|
+
export type { OpenCodeConfig, OttoConfig, OttoSubagentThreads, OttoConfigReadStatus, OpenCodeConfigReadStatus } from "./config.js"
|
|
24
|
+
export { installPackage, upgradePackage, installMissingPackages, planStableUpgrades } from "./installer.js"
|
|
25
|
+
export type { UpgradeMode } from "./installer.js"
|
|
26
|
+
export { hasKimakiBinary, isKimakiRunning, restartKimaki } from "./lifecycle.js"
|
|
27
|
+
export { checkPackagePresence, checkConfigHealth, checkDirectoryHealth } from "./health.js"
|
|
28
|
+
export type { HealthResult, PackageCheck, ConfigHealth } from "./health.js"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { installMissingPackages, planStableUpgrades } from "./installer.js"
|
|
3
|
+
|
|
4
|
+
describe("installer", () => {
|
|
5
|
+
it("installMissingPackages returns empty when all installed", () => {
|
|
6
|
+
const getInstalled = (_name: string) => "1.0.0"
|
|
7
|
+
const installed = installMissingPackages(getInstalled, () => "ok")
|
|
8
|
+
expect(installed).toEqual([])
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("installMissingPackages returns missing package names", () => {
|
|
12
|
+
const getInstalled = (name: string) => name === "kimaki" ? "1.0.0" : null
|
|
13
|
+
const installedNames: string[] = []
|
|
14
|
+
const install = (name: string) => { installedNames.push(name); return name }
|
|
15
|
+
|
|
16
|
+
const result = installMissingPackages(getInstalled, install)
|
|
17
|
+
|
|
18
|
+
expect(result).toContain("opencode-ai")
|
|
19
|
+
expect(result).not.toContain("kimaki")
|
|
20
|
+
// opencode-agent-memory is a plugin, not a global npm package
|
|
21
|
+
expect(result).not.toContain("opencode-agent-memory")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("planStableUpgrades returns empty when all packages match pinned", () => {
|
|
25
|
+
const pinned = { a: "1.0.0", b: "2.0.0" }
|
|
26
|
+
const getInstalled = (name: string) => pinned[name as keyof typeof pinned] ?? null
|
|
27
|
+
const plan = planStableUpgrades(["a", "b"], getInstalled, pinned)
|
|
28
|
+
expect(plan).toEqual([])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("planStableUpgrades lists packages whose version differs from pinned", () => {
|
|
32
|
+
const pinned = { "opencode-ai": "1.2.20", kimaki: "0.4.90" }
|
|
33
|
+
const getInstalled = (name: string) => (name === "kimaki" ? "0.4.90" : "1.0.0")
|
|
34
|
+
const plan = planStableUpgrades(["opencode-ai", "kimaki"], getInstalled, pinned)
|
|
35
|
+
expect(plan).toEqual([
|
|
36
|
+
{ name: "opencode-ai", current: "1.0.0", target: "1.2.20" },
|
|
37
|
+
])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("planStableUpgrades includes not installed packages", () => {
|
|
41
|
+
const pinned = { x: "1.0.0" }
|
|
42
|
+
const getInstalled = () => null as string | null
|
|
43
|
+
const plan = planStableUpgrades(["x"], getInstalled, pinned)
|
|
44
|
+
expect(plan).toEqual([{ name: "x", current: null, target: "1.0.0" }])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("planStableUpgrades throws when pinned entry missing for a package", () => {
|
|
48
|
+
expect(() =>
|
|
49
|
+
planStableUpgrades(["missing"], () => "1.0.0", {}),
|
|
50
|
+
).toThrow("No pinned version for missing in manifest")
|
|
51
|
+
})
|
|
52
|
+
})
|
package/src/installer.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import { MANIFEST } from "./manifest.js"
|
|
3
|
+
|
|
4
|
+
export type UpgradeMode = "stable" | "latest"
|
|
5
|
+
|
|
6
|
+
export function installPackage(name: string, version?: string): string {
|
|
7
|
+
const spec = version ? `${name}@${version}` : name
|
|
8
|
+
execSync(`npm install -g ${spec}`, {
|
|
9
|
+
encoding: "utf-8",
|
|
10
|
+
stdio: "pipe",
|
|
11
|
+
})
|
|
12
|
+
return spec
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function upgradePackage(name: string, mode: UpgradeMode): string {
|
|
16
|
+
if (mode === "stable") {
|
|
17
|
+
const pinned = MANIFEST.pinned[name]
|
|
18
|
+
if (!pinned) {
|
|
19
|
+
throw new Error(`No pinned version for ${name} in manifest`)
|
|
20
|
+
}
|
|
21
|
+
return installPackage(name, pinned)
|
|
22
|
+
}
|
|
23
|
+
return installPackage(name, "latest")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function planStableUpgrades(
|
|
27
|
+
packageNames: string[],
|
|
28
|
+
getInstalled: (name: string) => string | null,
|
|
29
|
+
pinned: Record<string, string>,
|
|
30
|
+
): { name: string; current: string | null; target: string }[] {
|
|
31
|
+
const upgrades: { name: string; current: string | null; target: string }[] = []
|
|
32
|
+
for (const name of packageNames) {
|
|
33
|
+
const target = pinned[name]
|
|
34
|
+
if (!target) {
|
|
35
|
+
throw new Error(`No pinned version for ${name} in manifest`)
|
|
36
|
+
}
|
|
37
|
+
const current = getInstalled(name)
|
|
38
|
+
if (current !== target) {
|
|
39
|
+
upgrades.push({ name, current, target })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return upgrades
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function installMissingPackages(
|
|
46
|
+
getInstalled: (name: string) => string | null,
|
|
47
|
+
install: (name: string, version?: string) => string = installPackage,
|
|
48
|
+
): string[] {
|
|
49
|
+
const installed: string[] = []
|
|
50
|
+
for (const name of Object.keys(MANIFEST.packages)) {
|
|
51
|
+
const current = getInstalled(name)
|
|
52
|
+
if (!current) {
|
|
53
|
+
const pinned = MANIFEST.pinned[name]
|
|
54
|
+
if (!pinned) {
|
|
55
|
+
throw new Error(`No pinned version for ${name} in manifest`)
|
|
56
|
+
}
|
|
57
|
+
install(name, pinned)
|
|
58
|
+
installed.push(name)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return installed
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { hasKimakiBinary, isKimakiRunning, restartKimaki } from "./lifecycle.js"
|
|
3
|
+
|
|
4
|
+
describe("lifecycle", () => {
|
|
5
|
+
it("hasKimakiBinary returns boolean", () => {
|
|
6
|
+
const result = hasKimakiBinary()
|
|
7
|
+
expect(typeof result).toBe("boolean")
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it("isKimakiRunning returns boolean", () => {
|
|
11
|
+
const result = isKimakiRunning()
|
|
12
|
+
expect(typeof result).toBe("boolean")
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("restartKimaki is a function", () => {
|
|
16
|
+
expect(typeof restartKimaki).toBe("function")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("restartKimaki throws descriptive error when kimaki not found", () => {
|
|
20
|
+
expect(typeof restartKimaki).toBe("function")
|
|
21
|
+
})
|
|
22
|
+
})
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
export function hasKimakiBinary(): boolean {
|
|
4
|
+
try {
|
|
5
|
+
execSync("which kimaki", { encoding: "utf-8", stdio: "pipe" })
|
|
6
|
+
return true
|
|
7
|
+
} catch {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isKimakiRunning(): boolean {
|
|
13
|
+
try {
|
|
14
|
+
const output = execSync("pgrep -f kimaki", { encoding: "utf-8", stdio: "pipe" })
|
|
15
|
+
return output.trim().length > 0
|
|
16
|
+
} catch {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function restartKimaki(): void {
|
|
22
|
+
if (!hasKimakiBinary()) {
|
|
23
|
+
throw new Error("kimaki is not installed. Install it first with: npm install -g kimaki")
|
|
24
|
+
}
|
|
25
|
+
execSync("kimaki restart", {
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
stdio: "pipe",
|
|
28
|
+
timeout: 30_000,
|
|
29
|
+
})
|
|
30
|
+
}
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface Manifest {
|
|
2
|
+
version: string
|
|
3
|
+
/** Packages installed globally via npm (CLI tools) */
|
|
4
|
+
packages: Record<string, string>
|
|
5
|
+
/** Pinned versions for `otto upgrade stable` (global npm packages only) */
|
|
6
|
+
pinned: Record<string, string>
|
|
7
|
+
/** Plugins enabled via opencode.json plugin[] — opencode resolves them itself */
|
|
8
|
+
plugins: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const MANIFEST: Manifest = {
|
|
12
|
+
version: "0.1.0",
|
|
13
|
+
packages: {
|
|
14
|
+
"opencode-ai": ">=1.0.115",
|
|
15
|
+
"@otto-assistant/bridge": ">=0.4.90",
|
|
16
|
+
},
|
|
17
|
+
pinned: {
|
|
18
|
+
"opencode-ai": "1.2.20",
|
|
19
|
+
"@otto-assistant/bridge": "0.4.90",
|
|
20
|
+
},
|
|
21
|
+
plugins: [
|
|
22
|
+
"opencode-agent-memory",
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Upstream repositories for sync tracking */
|
|
27
|
+
export const UPSTREAM_REPOS: Record<string, { repo: string; upstream: string }> = {
|
|
28
|
+
"@otto-assistant/bridge": {
|
|
29
|
+
repo: "otto-assistant/bridge",
|
|
30
|
+
upstream: "remorses/kimaki",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const OPENCODE_CONFIG_DIR = (): string => {
|
|
35
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/root"
|
|
36
|
+
return `${home}/.config/opencode`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const KIMAKI_DATA_DIR = (): string => {
|
|
40
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/root"
|
|
41
|
+
return `${home}/.kimaki`
|
|
42
|
+
}
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import { UPSTREAM_REPOS } from "./manifest.js"
|
|
3
|
+
|
|
4
|
+
interface SyncTarget {
|
|
5
|
+
repo: string
|
|
6
|
+
upstream: string
|
|
7
|
+
branch: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getSyncTargets(): SyncTarget[] {
|
|
11
|
+
return Object.entries(UPSTREAM_REPOS).map(([_pkgName, info]) => ({
|
|
12
|
+
repo: info.repo,
|
|
13
|
+
upstream: info.upstream,
|
|
14
|
+
branch: "main",
|
|
15
|
+
}))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function syncUpstreams(): Promise<void> {
|
|
19
|
+
const targets = getSyncTargets()
|
|
20
|
+
|
|
21
|
+
if (targets.length === 0) {
|
|
22
|
+
console.log("No upstream repos configured for sync.")
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check gh CLI is available
|
|
27
|
+
try {
|
|
28
|
+
execSync("gh --version", { stdio: "pipe" })
|
|
29
|
+
} catch {
|
|
30
|
+
console.error("Error: gh CLI is required for sync. Install: https://cli.github.com/")
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log("Triggering upstream sync for all forked repos:\n")
|
|
35
|
+
|
|
36
|
+
for (const target of targets) {
|
|
37
|
+
console.log(` ${target.repo} ← ${target.upstream}`)
|
|
38
|
+
try {
|
|
39
|
+
execSync(
|
|
40
|
+
`gh workflow run sync-upstream.yml --repo ${target.repo} --ref ${target.branch}`,
|
|
41
|
+
{ stdio: "pipe" },
|
|
42
|
+
)
|
|
43
|
+
console.log(` ✓ Sync triggered`)
|
|
44
|
+
} catch (err: unknown) {
|
|
45
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
46
|
+
console.error(` ✗ Failed: ${msg}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log("\nSync workflows triggered. Check status with: gh run list --repo <repo>")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { getSyncTargets, UPSTREAM_REPOS }
|