docks-kit 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/AGENTS.md +99 -0
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/SoT/.agents/skills.txt +14 -0
- package/SoT/.claude/CLAUDE.md +146 -0
- package/SoT/.claude/fetch-usage.sh +66 -0
- package/SoT/.claude/hooks/notify.sh +14 -0
- package/SoT/.claude/mcp-servers.json +10 -0
- package/SoT/.claude/settings.json +258 -0
- package/SoT/.claude/statusline.sh +175 -0
- package/SoT/.codex/AGENTS.md +75 -0
- package/SoT/.codex/agents/.gitkeep +1 -0
- package/SoT/.codex/config.toml +45 -0
- package/SoT/.codex/plugins/marketplace.json +50 -0
- package/SoT/.codex/rules/docks.rules +116 -0
- package/SoT/models.json +28 -0
- package/SoT/toolchain.json +26 -0
- package/cli/docs/flags.md +48 -0
- package/cli/docs/install.md +56 -0
- package/cli/docs/models.md +42 -0
- package/cli/docs/modifiers.md +33 -0
- package/cli/docs/overview.md +37 -0
- package/cli/docs/platforms.md +31 -0
- package/cli/docs/plugins.md +44 -0
- package/cli/docs/sync-layers.md +43 -0
- package/cli/docs/toolchain.md +54 -0
- package/cli/src/commands/docs.ts +65 -0
- package/cli/src/commands/model.ts +60 -0
- package/cli/src/commands/models.ts +46 -0
- package/cli/src/commands/plugins.ts +34 -0
- package/cli/src/commands/skills.ts +37 -0
- package/cli/src/commands/status.ts +79 -0
- package/cli/src/commands/sync.ts +134 -0
- package/cli/src/commands/toolchain.ts +42 -0
- package/cli/src/engine.ts +36 -0
- package/cli/src/kitHome.ts +31 -0
- package/cli/src/main.ts +60 -0
- package/cli/src/manifests.ts +105 -0
- package/cli/src/md.d.ts +4 -0
- package/cli/tsconfig.json +14 -0
- package/docks-kit +61 -0
- package/lib/claude.sh +846 -0
- package/lib/codex.sh +518 -0
- package/lib/common.sh +229 -0
- package/lib/engine.sh +153 -0
- package/lib/skills.sh +373 -0
- package/lib/toolchain.sh +222 -0
- package/notification.mp3 +0 -0
- package/package.json +39 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Args, Command, Options, Prompt } from "@effect/cli"
|
|
2
|
+
import { Effect, Option } from "effect"
|
|
3
|
+
import { bail, engine } from "../engine"
|
|
4
|
+
import { modelCatalog, type Tool } from "../manifests"
|
|
5
|
+
|
|
6
|
+
const tool = Args.text({ name: "tool" }).pipe(
|
|
7
|
+
Args.withDescription("Which tool: claude | codex")
|
|
8
|
+
)
|
|
9
|
+
const value = Args.text({ name: "value" }).pipe(
|
|
10
|
+
Args.withDescription("Model to set (omit to view current + pick interactively on a TTY)"),
|
|
11
|
+
Args.optional
|
|
12
|
+
)
|
|
13
|
+
const dryRun = Options.boolean("dry-run").pipe(
|
|
14
|
+
Options.withDescription("Preview without applying")
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const KEEP = "__keep__"
|
|
18
|
+
|
|
19
|
+
export const modelCommand = Command.make(
|
|
20
|
+
"model",
|
|
21
|
+
{ tool, value, dryRun },
|
|
22
|
+
(config) =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
if (config.tool !== "claude" && config.tool !== "codex") {
|
|
25
|
+
return yield* bail(`Unknown tool '${config.tool}' (valid: claude, codex)`)
|
|
26
|
+
}
|
|
27
|
+
const t = config.tool as Tool
|
|
28
|
+
const dry = config.dryRun ? ["--dry-run"] : []
|
|
29
|
+
|
|
30
|
+
if (Option.isSome(config.value)) {
|
|
31
|
+
return yield* engine(["model", t, config.value.value, ...dry])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// No value: show current (engine prints deployed + SoT + catalog) …
|
|
35
|
+
yield* engine(["model", t])
|
|
36
|
+
|
|
37
|
+
// … and offer an interactive picker when attached to a terminal.
|
|
38
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return
|
|
39
|
+
|
|
40
|
+
const catalog = modelCatalog(t)
|
|
41
|
+
const chosen = yield* Prompt.select({
|
|
42
|
+
message: `Set the deployed ${t} model (deployed config only; a flag-less sync reverts to SoT)`,
|
|
43
|
+
choices: [
|
|
44
|
+
{ title: "(keep current)", value: KEEP },
|
|
45
|
+
...catalog.models.map((m) => ({
|
|
46
|
+
title: m.id,
|
|
47
|
+
value: m.id,
|
|
48
|
+
description: m.note ?? ""
|
|
49
|
+
}))
|
|
50
|
+
]
|
|
51
|
+
})
|
|
52
|
+
if (chosen !== KEEP) {
|
|
53
|
+
yield* engine(["model", t, chosen, ...dry])
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
).pipe(
|
|
57
|
+
Command.withDescription(
|
|
58
|
+
"Get or set the DEPLOYED model for one tool without a full sync (deploy-time modifier semantics: SoT untouched; flag-less sync reverts)."
|
|
59
|
+
)
|
|
60
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli"
|
|
2
|
+
import { Console, Effect, Option } from "effect"
|
|
3
|
+
import { bail } from "../engine"
|
|
4
|
+
import { modelCatalog, type Tool } from "../manifests"
|
|
5
|
+
|
|
6
|
+
const tool = Args.text({ name: "tool" }).pipe(
|
|
7
|
+
Args.withDescription("claude | codex (omit for both)"),
|
|
8
|
+
Args.optional
|
|
9
|
+
)
|
|
10
|
+
const json = Options.boolean("json").pipe(
|
|
11
|
+
Options.withDescription("Machine-readable output")
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const renderTool = (t: Tool) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const catalog = modelCatalog(t)
|
|
17
|
+
yield* Console.log(`${t} models (kit-verified ${catalog.verified}):`)
|
|
18
|
+
for (const m of catalog.models) {
|
|
19
|
+
yield* Console.log(` ${m.id.padEnd(28)} ${m.kind.padEnd(6)} ${m.note ?? ""}`)
|
|
20
|
+
}
|
|
21
|
+
yield* Console.log("")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const modelsCommand = Command.make("models", { tool, json }, (config) =>
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const requested = Option.getOrUndefined(config.tool)
|
|
27
|
+
if (requested !== undefined && requested !== "claude" && requested !== "codex") {
|
|
28
|
+
return yield* bail(`Unknown tool '${requested}' (valid: claude, codex)`)
|
|
29
|
+
}
|
|
30
|
+
const tools: Array<Tool> = requested !== undefined ? [requested as Tool] : ["claude", "codex"]
|
|
31
|
+
|
|
32
|
+
if (config.json) {
|
|
33
|
+
const out = Object.fromEntries(tools.map((t) => [t, modelCatalog(t)]))
|
|
34
|
+
return yield* Console.log(JSON.stringify(out, null, 2))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const t of tools) {
|
|
38
|
+
yield* renderTool(t)
|
|
39
|
+
}
|
|
40
|
+
yield* Console.log(
|
|
41
|
+
"Catalog: SoT/models.json (research-verified). Well-formed IDs outside it apply with a warning."
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
).pipe(
|
|
45
|
+
Command.withDescription("List the kit-verified model catalog (SoT/models.json) for claude/codex.")
|
|
46
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli"
|
|
2
|
+
import { Console, Effect, Option } from "effect"
|
|
3
|
+
import { bail } from "../engine"
|
|
4
|
+
import { pluginsView } from "../manifests"
|
|
5
|
+
|
|
6
|
+
const action = Args.text({ name: "action" }).pipe(
|
|
7
|
+
Args.withDescription("list (default)"),
|
|
8
|
+
Args.optional
|
|
9
|
+
)
|
|
10
|
+
const json = Options.boolean("json").pipe(
|
|
11
|
+
Options.withDescription("Machine-readable output")
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export const pluginsCommand = Command.make("plugins", { action, json }, (config) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const act = Option.getOrElse(config.action, () => "list")
|
|
17
|
+
if (act !== "list") {
|
|
18
|
+
return yield* bail(
|
|
19
|
+
`Unknown plugins action '${act}' (valid: list). Install/removal runs through sync: --claude-plugin=<name> opts in, --prune reconciles.`
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
const view = pluginsView()
|
|
23
|
+
if (config.json) {
|
|
24
|
+
return yield* Console.log(JSON.stringify(view, null, 2))
|
|
25
|
+
}
|
|
26
|
+
yield* Console.log("SoT tri-state: true = enabled everywhere, false = installed-but-disabled (per-project enable), absent = not kit-managed\n")
|
|
27
|
+
yield* Console.log(`${"PLUGIN".padEnd(44)} ${"SOT".padEnd(7)} INSTALLED`)
|
|
28
|
+
for (const p of view) {
|
|
29
|
+
yield* Console.log(`${p.plugin.padEnd(44)} ${p.sot.padEnd(7)} ${p.installed ? "yes" : "no"}`)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
).pipe(
|
|
33
|
+
Command.withDescription("Installed plugins vs SoT enabledPlugins tri-state (read-only).")
|
|
34
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli"
|
|
2
|
+
import { Console, Effect, Option } from "effect"
|
|
3
|
+
import { bail } from "../engine"
|
|
4
|
+
import { skillsView } from "../manifests"
|
|
5
|
+
|
|
6
|
+
const action = Args.text({ name: "action" }).pipe(
|
|
7
|
+
Args.withDescription("list (default)"),
|
|
8
|
+
Args.optional
|
|
9
|
+
)
|
|
10
|
+
const json = Options.boolean("json").pipe(
|
|
11
|
+
Options.withDescription("Machine-readable output")
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export const skillsCommand = Command.make("skills", { action, json }, (config) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const act = Option.getOrElse(config.action, () => "list")
|
|
17
|
+
if (act !== "list") {
|
|
18
|
+
return yield* bail(
|
|
19
|
+
`Unknown skills action '${act}' (valid: list). Declare skills in SoT/.agents/skills.txt and run sync; --prune removes undeclared kit-managed ones.`
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
const view = skillsView()
|
|
23
|
+
if (config.json) {
|
|
24
|
+
return yield* Console.log(JSON.stringify(view, null, 2))
|
|
25
|
+
}
|
|
26
|
+
yield* Console.log(`${"SKILL".padEnd(28)} ${"DECLARED".padEnd(9)} ${"INSTALLED".padEnd(10)} CLAUDE-SYMLINK`)
|
|
27
|
+
for (const s of view) {
|
|
28
|
+
yield* Console.log(
|
|
29
|
+
`${s.skill.padEnd(28)} ${(s.declared ? "yes" : "no").padEnd(9)} ${(s.installed ? "yes" : "no").padEnd(10)} ${s.claudeSymlink ? "ok" : "-"}`
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
).pipe(
|
|
34
|
+
Command.withDescription(
|
|
35
|
+
"Universal agent skills: SoT/.agents/skills.txt vs ~/.agents/skills + the ~/.claude/skills symlink health (read-only)."
|
|
36
|
+
)
|
|
37
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Command, Options } from "@effect/cli"
|
|
2
|
+
import { Console, Effect } from "effect"
|
|
3
|
+
import { engineCapture } from "../engine"
|
|
4
|
+
import {
|
|
5
|
+
deployedClaudeSettings,
|
|
6
|
+
deployedCodexModel,
|
|
7
|
+
pluginsView,
|
|
8
|
+
skillsView,
|
|
9
|
+
sotClaudeSettings,
|
|
10
|
+
sotCodexModel
|
|
11
|
+
} from "../manifests"
|
|
12
|
+
import { kitHome } from "../kitHome"
|
|
13
|
+
|
|
14
|
+
const json = Options.boolean("json").pipe(
|
|
15
|
+
Options.withDescription("Machine-readable output")
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
interface Drift {
|
|
19
|
+
readonly setting: string
|
|
20
|
+
readonly deployed: string
|
|
21
|
+
readonly sot: string
|
|
22
|
+
readonly drifted: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const gatherDrift = (): Array<Drift> => {
|
|
26
|
+
const sot = sotClaudeSettings()
|
|
27
|
+
const dep = deployedClaudeSettings() ?? {}
|
|
28
|
+
const row = (setting: string, deployed: unknown, sotVal: unknown): Drift => {
|
|
29
|
+
const d = String(deployed ?? "(unset)")
|
|
30
|
+
const s = String(sotVal ?? "(unset)")
|
|
31
|
+
return { setting, deployed: d, sot: s, drifted: d !== s }
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
row("claude.model", dep.model, sot.model),
|
|
35
|
+
row("claude.effortLevel", dep.effortLevel, sot.effortLevel),
|
|
36
|
+
row(
|
|
37
|
+
"claude.compactWindow",
|
|
38
|
+
dep.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW,
|
|
39
|
+
sot.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW
|
|
40
|
+
),
|
|
41
|
+
row("codex.model", deployedCodexModel(), sotCodexModel())
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const statusCommand = Command.make("status", { json }, (config) =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const drift = gatherDrift()
|
|
48
|
+
const plugins = pluginsView()
|
|
49
|
+
const skills = skillsView()
|
|
50
|
+
const toolchainTable = yield* engineCapture(["toolchain", "check"])
|
|
51
|
+
|
|
52
|
+
if (config.json) {
|
|
53
|
+
return yield* Console.log(
|
|
54
|
+
JSON.stringify({ kitHome: kitHome(), drift, plugins, skills, toolchainTable }, null, 2)
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
yield* Console.log(`Kit home: ${kitHome()}\n`)
|
|
59
|
+
yield* Console.log("Deployed vs SoT (drift is expected for deploy-time modifiers):")
|
|
60
|
+
for (const d of drift) {
|
|
61
|
+
const mark = d.drifted ? "≠" : "="
|
|
62
|
+
yield* Console.log(` ${d.setting.padEnd(22)} deployed=${d.deployed} ${mark} SoT=${d.sot}`)
|
|
63
|
+
}
|
|
64
|
+
yield* Console.log("\nToolchain:")
|
|
65
|
+
yield* Console.log(toolchainTable.trimEnd())
|
|
66
|
+
const enabled = plugins.filter((p) => p.sot === "true").length
|
|
67
|
+
yield* Console.log(
|
|
68
|
+
`\nPlugins: ${plugins.length} known (${enabled} SoT-enabled) — details: docks-kit plugins list`
|
|
69
|
+
)
|
|
70
|
+
const installed = skills.filter((s) => s.installed).length
|
|
71
|
+
yield* Console.log(
|
|
72
|
+
`Skills: ${skills.length} known (${installed} installed) — details: docks-kit skills list`
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
).pipe(
|
|
76
|
+
Command.withDescription(
|
|
77
|
+
"Doctor view: deployed-vs-SoT drift (model, effort, compact window), toolchain table, plugin/skill counts."
|
|
78
|
+
)
|
|
79
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli"
|
|
2
|
+
import { Effect, Option } from "effect"
|
|
3
|
+
import { bail, engine } from "../engine"
|
|
4
|
+
import { modelCatalog, type Tool } from "../manifests"
|
|
5
|
+
|
|
6
|
+
const VALID_TARGETS = ["claude", "codex", "agents"]
|
|
7
|
+
|
|
8
|
+
// Renamed sync.sh-era flags: @effect/cli routes unknown flags into the excess
|
|
9
|
+
// positional args, so the rename hints (lib/common.sh's exit-2 arms) are
|
|
10
|
+
// mirrored here to keep the parser contract identical across both front-ends.
|
|
11
|
+
const LEGACY_HINTS: Record<string, string> = {
|
|
12
|
+
"--force": "--force was renamed to --reconcile",
|
|
13
|
+
"--remove-plugins":
|
|
14
|
+
"--remove-plugins was renamed to --prune (it also removes marketplaces + kit-managed skills)",
|
|
15
|
+
"--680k": "--680k was renamed to --claude-compact-window=680k",
|
|
16
|
+
"--permissive": "--permissive was renamed to --claude-permissive",
|
|
17
|
+
"--supabase": "--supabase was renamed to --claude-plugin=supabase",
|
|
18
|
+
"--n8n": "--n8n was renamed to --claude-plugin=n8n",
|
|
19
|
+
"--no-rtk": "--no-rtk was renamed to --skip-rtk",
|
|
20
|
+
"--claude": "--claude was renamed: pass the target as a word, e.g. 'sync claude'",
|
|
21
|
+
"--codex": "--codex was renamed: pass the target as a word, e.g. 'sync codex'",
|
|
22
|
+
"--agents": "--agents was renamed: pass the target as a word, e.g. 'sync agents'"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const catalogHint = (t: Tool): string => {
|
|
26
|
+
const c = modelCatalog(t)
|
|
27
|
+
const list = c.models
|
|
28
|
+
.map((m) => ` ${m.id}${m.note !== undefined ? ` — ${m.note}` : ""}`)
|
|
29
|
+
.join("\n")
|
|
30
|
+
return `Available ${t} models (kit-verified ${c.verified} — SoT/models.json):\n${list}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const targets = Args.text({ name: "target" }).pipe(
|
|
34
|
+
Args.withDescription("Sync targets: claude, codex, agents (default: all three)"),
|
|
35
|
+
Args.repeated
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const dryRun = Options.boolean("dry-run").pipe(
|
|
39
|
+
Options.withDescription("Preview without applying")
|
|
40
|
+
)
|
|
41
|
+
const reconcile = Options.boolean("reconcile").pipe(
|
|
42
|
+
Options.withDescription("Reconcile kit-owned settings with SoT (SoT keys win; user-only keys preserved; permissions arrays replaced)")
|
|
43
|
+
)
|
|
44
|
+
const prune = Options.boolean("prune").pipe(
|
|
45
|
+
Options.withDescription("Uninstall kit-managed installs not in SoT (plugins, marketplaces, universal skills)")
|
|
46
|
+
)
|
|
47
|
+
const skipRtk = Options.boolean("skip-rtk").pipe(
|
|
48
|
+
Options.withDescription("Skip optional tool bootstrap (RTK, bubblewrap)")
|
|
49
|
+
)
|
|
50
|
+
const yes = Options.boolean("yes").pipe(
|
|
51
|
+
Options.withDescription("Auto-accept toolchain prompts (containers/CI)")
|
|
52
|
+
)
|
|
53
|
+
const claudeModel = Options.text("claude-model").pipe(
|
|
54
|
+
Options.withDescription("Deploy-time modifier: set deployed Claude model (see `docks-kit models claude`)"),
|
|
55
|
+
Options.optional
|
|
56
|
+
)
|
|
57
|
+
const claudeCompactWindow = Options.text("claude-compact-window").pipe(
|
|
58
|
+
Options.withDescription("Deploy-time modifier: set deployed autocompact window in tokens (e.g. 680000 or 680k)"),
|
|
59
|
+
Options.optional
|
|
60
|
+
)
|
|
61
|
+
const claudePermissive = Options.boolean("claude-permissive").pipe(
|
|
62
|
+
Options.withDescription("Deploy-time modifier: empty permissions.ask/deny in deployed settings (sandboxes)")
|
|
63
|
+
)
|
|
64
|
+
const claudePlugin = Options.text("claude-plugin").pipe(
|
|
65
|
+
Options.withDescription(
|
|
66
|
+
"Sticky opt-in plugin(s); repeatable and/or comma-separated (known: supabase, n8n)"
|
|
67
|
+
),
|
|
68
|
+
Options.repeated
|
|
69
|
+
)
|
|
70
|
+
const codexModel = Options.text("codex-model").pipe(
|
|
71
|
+
Options.withDescription("Deploy-time modifier: set deployed Codex model (see `docks-kit models codex`)"),
|
|
72
|
+
Options.optional
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
export const syncCommand = Command.make(
|
|
76
|
+
"sync",
|
|
77
|
+
{
|
|
78
|
+
targets,
|
|
79
|
+
dryRun,
|
|
80
|
+
reconcile,
|
|
81
|
+
prune,
|
|
82
|
+
skipRtk,
|
|
83
|
+
yes,
|
|
84
|
+
claudeModel,
|
|
85
|
+
claudeCompactWindow,
|
|
86
|
+
claudePermissive,
|
|
87
|
+
claudePlugin,
|
|
88
|
+
codexModel
|
|
89
|
+
},
|
|
90
|
+
(config) =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
for (const t of config.targets) {
|
|
93
|
+
if (VALID_TARGETS.includes(t)) continue
|
|
94
|
+
if (t === "--claude-model" || t === "--codex-model") {
|
|
95
|
+
const tool: Tool = t === "--claude-model" ? "claude" : "codex"
|
|
96
|
+
return yield* bail(`${catalogHint(tool)}\n${t} requires a value: ${t}=<model>`)
|
|
97
|
+
}
|
|
98
|
+
const hint = LEGACY_HINTS[t]
|
|
99
|
+
if (hint !== undefined) {
|
|
100
|
+
return yield* bail(hint)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const bad = config.targets.filter((t) => !VALID_TARGETS.includes(t))
|
|
104
|
+
if (bad.length > 0) {
|
|
105
|
+
return yield* bail(
|
|
106
|
+
`Unknown sync target(s): ${bad.join(", ")} (valid: ${VALID_TARGETS.join(", ")})`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const args: Array<string> = ["sync", ...config.targets]
|
|
111
|
+
if (config.dryRun) args.push("--dry-run")
|
|
112
|
+
if (config.reconcile) args.push("--reconcile")
|
|
113
|
+
if (config.prune) args.push("--prune")
|
|
114
|
+
if (config.skipRtk) args.push("--skip-rtk")
|
|
115
|
+
if (config.yes) args.push("--yes")
|
|
116
|
+
if (config.claudePermissive) args.push("--claude-permissive")
|
|
117
|
+
Option.map(config.claudeModel, (m) => args.push(`--claude-model=${m}`))
|
|
118
|
+
Option.map(config.claudeCompactWindow, (w) => args.push(`--claude-compact-window=${w}`))
|
|
119
|
+
Option.map(config.codexModel, (m) => args.push(`--codex-model=${m}`))
|
|
120
|
+
for (const occurrence of config.claudePlugin) {
|
|
121
|
+
occurrence
|
|
122
|
+
.split(",")
|
|
123
|
+
.map((p) => p.trim())
|
|
124
|
+
.filter((p) => p.length > 0)
|
|
125
|
+
.forEach((p) => args.push(`--claude-plugin=${p}`))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
yield* engine(args)
|
|
129
|
+
})
|
|
130
|
+
).pipe(
|
|
131
|
+
Command.withDescription(
|
|
132
|
+
"Deploy the SoT to this machine (engine: lib/engine.sh — same flags, zero-dependency escape hatch). Deploy-time modifiers touch deployed config only; a later flag-less sync reverts them to SoT."
|
|
133
|
+
)
|
|
134
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Args, Command, Options } from "@effect/cli"
|
|
2
|
+
import { Effect, Option } from "effect"
|
|
3
|
+
import { bail, engine } from "../engine"
|
|
4
|
+
|
|
5
|
+
const MANAGED = ["rtk", "bun", "effect-solutions", "agent-browser"]
|
|
6
|
+
|
|
7
|
+
const op = Args.text({ name: "op" }).pipe(
|
|
8
|
+
Args.withDescription("check (default) | ensure <tool>"),
|
|
9
|
+
Args.optional
|
|
10
|
+
)
|
|
11
|
+
const tool = Args.text({ name: "tool" }).pipe(
|
|
12
|
+
Args.withDescription(`Managed tool for ensure: ${MANAGED.join(", ")}`),
|
|
13
|
+
Args.optional
|
|
14
|
+
)
|
|
15
|
+
const yes = Options.boolean("yes").pipe(
|
|
16
|
+
Options.withDescription("Auto-accept above-verified installs")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export const toolchainCommand = Command.make("toolchain", { op, tool, yes }, (config) =>
|
|
20
|
+
Effect.gen(function* () {
|
|
21
|
+
const operation = Option.getOrElse(config.op, () => "check")
|
|
22
|
+
const flags = config.yes ? ["--yes"] : []
|
|
23
|
+
|
|
24
|
+
switch (operation) {
|
|
25
|
+
case "check":
|
|
26
|
+
return yield* engine(["toolchain", "check"])
|
|
27
|
+
case "ensure": {
|
|
28
|
+
const t = Option.getOrUndefined(config.tool)
|
|
29
|
+
if (t === undefined || !MANAGED.includes(t)) {
|
|
30
|
+
return yield* bail(`toolchain ensure needs a managed tool: ${MANAGED.join(", ")}`)
|
|
31
|
+
}
|
|
32
|
+
return yield* engine(["toolchain", "ensure", t, ...flags])
|
|
33
|
+
}
|
|
34
|
+
default:
|
|
35
|
+
return yield* bail(`Unknown toolchain op '${operation}' (valid: check, ensure)`)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
).pipe(
|
|
39
|
+
Command.withDescription(
|
|
40
|
+
"Verified-version floors for external tools (SoT/toolchain.json): check prints the doctor table; ensure installs/upgrades one managed tool per policy (above-verified versions prompt; --yes accepts)."
|
|
41
|
+
)
|
|
42
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Command as Subprocess } from "@effect/platform"
|
|
2
|
+
import { Console, Effect } from "effect"
|
|
3
|
+
import { kitHome } from "./kitHome"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The single seam between the typed CLI and the bash engine. All mutation runs
|
|
7
|
+
* through lib/engine.sh (same flag vocabulary as this CLI), so the engine stays
|
|
8
|
+
* independently usable as the zero-dependency escape hatch.
|
|
9
|
+
*/
|
|
10
|
+
export const engine = (args: ReadonlyArray<string>) =>
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
const code = yield* Subprocess.make("bash", join("lib/engine.sh"), ...args).pipe(
|
|
13
|
+
Subprocess.workingDirectory(kitHome()),
|
|
14
|
+
Subprocess.stdin("inherit"),
|
|
15
|
+
Subprocess.stdout("inherit"),
|
|
16
|
+
Subprocess.stderr("inherit"),
|
|
17
|
+
Subprocess.exitCode
|
|
18
|
+
)
|
|
19
|
+
if (code !== 0) {
|
|
20
|
+
yield* Effect.sync(() => process.exit(code))
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** Run the engine capturing stdout (engine logs/warns go to stderr and pass through). */
|
|
25
|
+
export const engineCapture = (args: ReadonlyArray<string>) =>
|
|
26
|
+
Subprocess.make("bash", join("lib/engine.sh"), ...args).pipe(
|
|
27
|
+
Subprocess.workingDirectory(kitHome()),
|
|
28
|
+
Subprocess.stderr("inherit"),
|
|
29
|
+
Subprocess.string
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const join = (rel: string): string => `${kitHome()}/${rel}`
|
|
33
|
+
|
|
34
|
+
/** Print a message to stderr and exit — for CLI-side validation failures. */
|
|
35
|
+
export const bail = (message: string, code = 2) =>
|
|
36
|
+
Console.error(message).pipe(Effect.andThen(Effect.sync(() => process.exit(code))))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { dirname, join, resolve } from "node:path"
|
|
3
|
+
|
|
4
|
+
const isKitHome = (dir: string): boolean =>
|
|
5
|
+
existsSync(join(dir, "SoT")) && existsSync(join(dir, "lib", "engine.sh"))
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the kit home (the directory holding SoT/ + lib/engine.sh):
|
|
9
|
+
* DOCKS_KIT_HOME env → nearest ancestor of cwd (repo-checkout usage) →
|
|
10
|
+
* the package's own root (bunx / bun add -g usage: main.ts lives at
|
|
11
|
+
* <root>/cli/src, and the npm package bundles SoT/ + lib/ alongside).
|
|
12
|
+
*/
|
|
13
|
+
export const kitHome = (): string => {
|
|
14
|
+
const env = process.env["DOCKS_KIT_HOME"]
|
|
15
|
+
if (env !== undefined && env !== "") {
|
|
16
|
+
if (isKitHome(env)) return env
|
|
17
|
+
throw new Error(`DOCKS_KIT_HOME=${env} does not contain SoT/ + lib/engine.sh`)
|
|
18
|
+
}
|
|
19
|
+
let dir = process.cwd()
|
|
20
|
+
for (;;) {
|
|
21
|
+
if (isKitHome(dir)) return dir
|
|
22
|
+
const parent = dirname(dir)
|
|
23
|
+
if (parent === dir) break
|
|
24
|
+
dir = parent
|
|
25
|
+
}
|
|
26
|
+
const packageRoot = resolve(import.meta.dir, "..", "..")
|
|
27
|
+
if (isKitHome(packageRoot)) return packageRoot
|
|
28
|
+
throw new Error(
|
|
29
|
+
"docks-kit home not found — run inside the kit repo or set DOCKS_KIT_HOME"
|
|
30
|
+
)
|
|
31
|
+
}
|
package/cli/src/main.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from "@effect/cli"
|
|
3
|
+
import { BunContext, BunRuntime } from "@effect/platform-bun"
|
|
4
|
+
import { Console, Effect } from "effect"
|
|
5
|
+
import { docsCommand } from "./commands/docs"
|
|
6
|
+
import { modelCommand } from "./commands/model"
|
|
7
|
+
import { modelsCommand } from "./commands/models"
|
|
8
|
+
import { pluginsCommand } from "./commands/plugins"
|
|
9
|
+
import { skillsCommand } from "./commands/skills"
|
|
10
|
+
import { statusCommand } from "./commands/status"
|
|
11
|
+
import { syncCommand } from "./commands/sync"
|
|
12
|
+
import { toolchainCommand } from "./commands/toolchain"
|
|
13
|
+
|
|
14
|
+
const root = Command.make("docks-kit", {}, () =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
yield* Console.log("docks-kit — portable AI coding agent config kit")
|
|
17
|
+
yield* Console.log("")
|
|
18
|
+
yield* Console.log(" docks-kit sync [claude] [codex] [agents] deploy the SoT to this machine")
|
|
19
|
+
yield* Console.log(" docks-kit model <claude|codex> [value] get/set the deployed model")
|
|
20
|
+
yield* Console.log(" docks-kit models [tool] kit-verified model catalog")
|
|
21
|
+
yield* Console.log(" docks-kit toolchain [check|ensure <tool>] verified-version floors")
|
|
22
|
+
yield* Console.log(" docks-kit status deployed-vs-SoT doctor view")
|
|
23
|
+
yield* Console.log(" docks-kit plugins list plugin tri-state")
|
|
24
|
+
yield* Console.log(" docks-kit skills list universal skills")
|
|
25
|
+
yield* Console.log(" docks-kit docs [topic] self-documentation")
|
|
26
|
+
yield* Console.log("")
|
|
27
|
+
yield* Console.log("Run 'docks-kit --help' for full option listings (also: --wizard, --completions).")
|
|
28
|
+
yield* Console.log("Zero-dependency escape hatch: bash lib/engine.sh <same subcommands/flags>")
|
|
29
|
+
})
|
|
30
|
+
).pipe(
|
|
31
|
+
Command.withDescription(
|
|
32
|
+
"Portable AI coding agent config kit — SoT sync engine + helpers for Claude Code, Codex, and universal agent skills."
|
|
33
|
+
),
|
|
34
|
+
Command.withSubcommands([
|
|
35
|
+
syncCommand,
|
|
36
|
+
modelCommand,
|
|
37
|
+
modelsCommand,
|
|
38
|
+
toolchainCommand,
|
|
39
|
+
statusCommand,
|
|
40
|
+
pluginsCommand,
|
|
41
|
+
skillsCommand,
|
|
42
|
+
docsCommand
|
|
43
|
+
])
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const cli = Command.run(root, {
|
|
47
|
+
name: "docks-kit",
|
|
48
|
+
version: "0.1.0"
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// @effect/cli's Options.repeated does not accept `--flag=value` syntax (only
|
|
52
|
+
// `--flag value`), but the documented form for the repeatable opt-in is
|
|
53
|
+
// `--claude-plugin=<name>` — normalize it here so both spellings work.
|
|
54
|
+
const argv = process.argv.flatMap((a) =>
|
|
55
|
+
a.startsWith("--claude-plugin=")
|
|
56
|
+
? ["--claude-plugin", a.slice("--claude-plugin=".length)]
|
|
57
|
+
: [a]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
cli(argv).pipe(Effect.provide(BunContext.layer), BunRuntime.runMain)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, readlinkSync, existsSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { kitHome } from "./kitHome"
|
|
4
|
+
|
|
5
|
+
export interface ModelEntry {
|
|
6
|
+
readonly id: string
|
|
7
|
+
readonly kind: "alias" | "id"
|
|
8
|
+
readonly note?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ModelCatalog {
|
|
12
|
+
readonly verified: string
|
|
13
|
+
readonly models: ReadonlyArray<ModelEntry>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type Tool = "claude" | "codex"
|
|
17
|
+
|
|
18
|
+
const readJson = (path: string): any => JSON.parse(readFileSync(path, "utf8"))
|
|
19
|
+
|
|
20
|
+
export const modelCatalog = (tool: Tool): ModelCatalog =>
|
|
21
|
+
readJson(join(kitHome(), "SoT", "models.json"))[tool]
|
|
22
|
+
|
|
23
|
+
export const toolchainManifest = (): Record<string, any> =>
|
|
24
|
+
readJson(join(kitHome(), "SoT", "toolchain.json")).tools
|
|
25
|
+
|
|
26
|
+
/** SoT settings (claude) — model/effort/env for drift display. */
|
|
27
|
+
export const sotClaudeSettings = (): any =>
|
|
28
|
+
readJson(join(kitHome(), "SoT", ".claude", "settings.json"))
|
|
29
|
+
|
|
30
|
+
export const deployedClaudeSettings = (): any | undefined => {
|
|
31
|
+
const p = join(homedir(), ".claude", "settings.json")
|
|
32
|
+
return existsSync(p) ? readJson(p) : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const tomlModel = (path: string): string | undefined => {
|
|
36
|
+
if (!existsSync(path)) return undefined
|
|
37
|
+
const m = readFileSync(path, "utf8").match(/^model\s*=\s*"([^"]+)"/m)
|
|
38
|
+
return m?.[1]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const sotCodexModel = (): string | undefined =>
|
|
42
|
+
tomlModel(join(kitHome(), "SoT", ".codex", "config.toml"))
|
|
43
|
+
|
|
44
|
+
export const deployedCodexModel = (): string | undefined =>
|
|
45
|
+
tomlModel(join(homedir(), ".codex", "config.toml"))
|
|
46
|
+
|
|
47
|
+
export const homedir = (): string => process.env["HOME"] ?? "~"
|
|
48
|
+
|
|
49
|
+
/** Installed plugins (user scope) vs SoT enabledPlugins tri-state. */
|
|
50
|
+
export const pluginsView = (): Array<{
|
|
51
|
+
plugin: string
|
|
52
|
+
sot: "true" | "false" | "absent"
|
|
53
|
+
installed: boolean
|
|
54
|
+
}> => {
|
|
55
|
+
const sot: Record<string, boolean> = sotClaudeSettings().enabledPlugins ?? {}
|
|
56
|
+
const installedPath = join(homedir(), ".claude", "plugins", "installed_plugins.json")
|
|
57
|
+
const installed: Record<string, unknown> = existsSync(installedPath)
|
|
58
|
+
? readJson(installedPath).plugins ?? {}
|
|
59
|
+
: {}
|
|
60
|
+
const names = new Set([...Object.keys(sot), ...Object.keys(installed)])
|
|
61
|
+
return [...names].sort().map((plugin) => ({
|
|
62
|
+
plugin,
|
|
63
|
+
sot: plugin in sot ? (sot[plugin] ? "true" : "false") : "absent",
|
|
64
|
+
installed: plugin in installed
|
|
65
|
+
}))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Universal skills: SoT manifest slugs vs ~/.agents/skills contents. */
|
|
69
|
+
export const skillsView = (): Array<{
|
|
70
|
+
skill: string
|
|
71
|
+
declared: boolean
|
|
72
|
+
installed: boolean
|
|
73
|
+
claudeSymlink: boolean
|
|
74
|
+
}> => {
|
|
75
|
+
const manifestPath = join(kitHome(), "SoT", ".agents", "skills.txt")
|
|
76
|
+
const declared = existsSync(manifestPath)
|
|
77
|
+
? readFileSync(manifestPath, "utf8")
|
|
78
|
+
.split("\n")
|
|
79
|
+
.map((l) => l.replace(/#.*$/, "").trim())
|
|
80
|
+
.filter((l) => l.length > 0)
|
|
81
|
+
.map((slug) => slug.split("/").pop() as string)
|
|
82
|
+
: []
|
|
83
|
+
const skillsDir = join(homedir(), ".agents", "skills")
|
|
84
|
+
const installed = existsSync(skillsDir)
|
|
85
|
+
? readdirSync(skillsDir, { withFileTypes: true })
|
|
86
|
+
.filter((e) => e.isDirectory())
|
|
87
|
+
.map((e) => e.name)
|
|
88
|
+
: []
|
|
89
|
+
const names = new Set([...declared, ...installed])
|
|
90
|
+
return [...names].sort().map((skill) => {
|
|
91
|
+
const link = join(homedir(), ".claude", "skills", skill)
|
|
92
|
+
let claudeSymlink = false
|
|
93
|
+
try {
|
|
94
|
+
claudeSymlink = readlinkSync(link).includes(".agents/skills")
|
|
95
|
+
} catch {
|
|
96
|
+
/* not a symlink or missing */
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
skill,
|
|
100
|
+
declared: declared.includes(skill),
|
|
101
|
+
installed: installed.includes(skill),
|
|
102
|
+
claudeSymlink
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
package/cli/src/md.d.ts
ADDED