docks-kit 0.1.0 → 0.1.2
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 +14 -15
- package/README.md +12 -11
- package/SoT/.agents/skills.txt +3 -3
- package/SoT/.claude/settings.json +3 -26
- package/SoT/models.json +1 -1
- package/SoT/toolchain.json +3 -3
- package/cli/docs/install.md +39 -12
- package/cli/docs/overview.md +3 -4
- package/cli/docs/platforms.md +35 -22
- package/cli/src/commands/sync.ts +32 -4
- package/cli/src/commands/update.ts +116 -0
- package/cli/src/engine-native/DESIGN.md +89 -0
- package/cli/src/engine-native/claudeModel.ts +48 -0
- package/cli/src/engine-native/claudeSync.ts +771 -0
- package/cli/src/engine-native/codexSync.ts +439 -0
- package/cli/src/engine-native/codexToml.ts +67 -0
- package/cli/src/engine-native/exec.ts +54 -0
- package/cli/src/engine-native/index.ts +109 -0
- package/cli/src/engine-native/jq.ts +63 -0
- package/cli/src/engine-native/models.ts +73 -0
- package/cli/src/engine-native/modes.ts +133 -0
- package/cli/src/engine-native/output.ts +20 -0
- package/cli/src/engine-native/parseArgs.ts +223 -0
- package/cli/src/engine-native/settings.ts +41 -0
- package/cli/src/engine-native/skillsSync.ts +369 -0
- package/cli/src/engine-native/toolchain.ts +257 -0
- package/cli/src/engine.ts +35 -18
- package/cli/src/kitHome.ts +4 -4
- package/cli/src/main.ts +14 -1
- package/cli/src/manifests.ts +3 -2
- package/cli/tsconfig.json +1 -1
- package/docks-kit +6 -3
- package/package.json +8 -4
- package/lib/claude.sh +0 -846
- package/lib/codex.sh +0 -518
- package/lib/common.sh +0 -229
- package/lib/engine.sh +0 -153
- package/lib/skills.sh +0 -373
- package/lib/toolchain.sh +0 -222
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jq-semantics primitives for EngineNative.
|
|
3
|
+
*
|
|
4
|
+
* Some deployed JSON contracts were defined by jq behavior, so EngineNative
|
|
5
|
+
* reproduces three jq behaviors exactly:
|
|
6
|
+
* - `*` (recursive object merge): right side wins; objects merge
|
|
7
|
+
* recursively; arrays and scalars are REPLACED, never concatenated;
|
|
8
|
+
* merged key order = left object's order, then right-only keys appended.
|
|
9
|
+
* - `unique`: sorts (jq's total order — for our string arrays, Unicode
|
|
10
|
+
* codepoint order) and deduplicates.
|
|
11
|
+
* - output formatting: 2-space indent, UTF-8 raw, trailing newline.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type Json = null | boolean | number | string | Array<Json> | { [key: string]: Json }
|
|
15
|
+
|
|
16
|
+
export function isObject(v: Json): v is { [key: string]: Json } {
|
|
17
|
+
return typeof v === "object" && v !== null && !Array.isArray(v)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** jq `$left * $right`. */
|
|
21
|
+
export function deepMerge(left: Json, right: Json): Json {
|
|
22
|
+
if (!isObject(left) || !isObject(right)) return right
|
|
23
|
+
const out: { [key: string]: Json } = { ...left }
|
|
24
|
+
for (const [k, v] of Object.entries(right)) {
|
|
25
|
+
const lv = out[k]
|
|
26
|
+
out[k] = lv !== undefined && isObject(lv) && isObject(v) ? deepMerge(lv, v) : v
|
|
27
|
+
}
|
|
28
|
+
return out
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** jq `unique` over an array of strings: codepoint sort + dedup. */
|
|
32
|
+
export function uniqueStrings(arr: Array<string>): Array<string> {
|
|
33
|
+
return [...new Set(arr)].sort(compareCodepoints)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compareCodepoints(a: string, b: string): number {
|
|
37
|
+
const ia = a[Symbol.iterator]()
|
|
38
|
+
const ib = b[Symbol.iterator]()
|
|
39
|
+
for (;;) {
|
|
40
|
+
const ra = ia.next()
|
|
41
|
+
const rb = ib.next()
|
|
42
|
+
if (ra.done && rb.done) return 0
|
|
43
|
+
if (ra.done) return -1
|
|
44
|
+
if (rb.done) return 1
|
|
45
|
+
const ca = ra.value.codePointAt(0)!
|
|
46
|
+
const cb = rb.value.codePointAt(0)!
|
|
47
|
+
if (ca !== cb) return ca - cb
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** jq's default pretty-printer for the documents this kit writes. */
|
|
52
|
+
export function jqStringify(v: Json): string {
|
|
53
|
+
return `${JSON.stringify(v, null, 2)}\n`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** jq `empty` validation: parse or null. */
|
|
57
|
+
export function parseJson(text: string): Json | undefined {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(text) as Json
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-catalog helpers: manifest listing plus Claude/Codex model validation.
|
|
3
|
+
* Message strings are covered by the golden suites.
|
|
4
|
+
*/
|
|
5
|
+
import { p } from "./exec"
|
|
6
|
+
import { readFileSync } from "node:fs"
|
|
7
|
+
|
|
8
|
+
import { isObject, parseJson, type Json } from "./jq"
|
|
9
|
+
import { warn } from "./output"
|
|
10
|
+
|
|
11
|
+
function catalog(repoDir: string): Json | undefined {
|
|
12
|
+
try {
|
|
13
|
+
return parseJson(readFileSync(p(repoDir, "SoT", "models.json"), "utf8"))
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toolEntry(repoDir: string, tool: string): { [k: string]: Json } | undefined {
|
|
20
|
+
const doc = catalog(repoDir)
|
|
21
|
+
if (doc === undefined || !isObject(doc)) return undefined
|
|
22
|
+
const entry = doc[tool]
|
|
23
|
+
return entry !== undefined && isObject(entry) ? entry : undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function modelEntries(repoDir: string, tool: string): Array<{ [k: string]: Json }> {
|
|
27
|
+
const entry = toolEntry(repoDir, tool)
|
|
28
|
+
const models = entry?.["models"]
|
|
29
|
+
return Array.isArray(models) ? models.filter(isObject) : []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function modelsFromManifest(repoDir: string, tool: string): Array<string> {
|
|
33
|
+
return modelEntries(repoDir, tool)
|
|
34
|
+
.map((m) => m["id"])
|
|
35
|
+
.filter((id): id is string => typeof id === "string")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function printModels(repoDir: string, tool: string): void {
|
|
39
|
+
const entry = toolEntry(repoDir, tool)
|
|
40
|
+
if (entry === undefined) {
|
|
41
|
+
warn(`Model catalog unavailable (${p(repoDir, "SoT", "models.json")})`)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
const verified = typeof entry["verified"] === "string" ? entry["verified"] : "?"
|
|
45
|
+
const lines = [`Available ${tool} models (kit-verified ${verified} — SoT/models.json):`]
|
|
46
|
+
for (const m of modelEntries(repoDir, tool)) {
|
|
47
|
+
const note = typeof m["note"] === "string" ? ` — ${m["note"]}` : ""
|
|
48
|
+
lines.push(` ${String(m["id"] ?? "")}${note}`)
|
|
49
|
+
}
|
|
50
|
+
if (tool === "claude") lines.push(" (full claude-* model IDs outside the catalog are accepted with a warning)")
|
|
51
|
+
if (tool === "codex") lines.push(" (well-formed IDs outside the catalog are accepted with a warning)")
|
|
52
|
+
process.stderr.write(`${lines.join("\n")}\n`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function validateClaudeModel(repoDir: string, m: string): boolean {
|
|
56
|
+
if (m === "") return false
|
|
57
|
+
if (modelsFromManifest(repoDir, "claude").includes(m)) return true
|
|
58
|
+
if (m.startsWith("claude-")) {
|
|
59
|
+
warn(`Claude model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway`)
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateCodexModel(repoDir: string, m: string): boolean {
|
|
66
|
+
if (!/^[A-Za-z0-9._-]+$/.test(m)) return false
|
|
67
|
+
if (!modelsFromManifest(repoDir, "codex").includes(m)) {
|
|
68
|
+
warn(
|
|
69
|
+
`Codex model '${m}' is not in the kit-verified catalog (SoT/models.json) — applying anyway (check ~/.codex/config.toml if Codex rejects it)`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct modes for `model` and `toolchain`. Same public argv vocabulary,
|
|
3
|
+
* golden-tested messages, same exit codes.
|
|
4
|
+
*/
|
|
5
|
+
import { p } from "./exec"
|
|
6
|
+
import { readFileSync } from "node:fs"
|
|
7
|
+
|
|
8
|
+
import { syncClaudeModel } from "./claudeModel"
|
|
9
|
+
import { syncCodexModel } from "./codexToml"
|
|
10
|
+
import type { Ctx } from "./index"
|
|
11
|
+
import { isObject, parseJson, type Json } from "./jq"
|
|
12
|
+
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
13
|
+
import { echo, err, warn } from "./output"
|
|
14
|
+
import { rtkInstall } from "./claudeSync"
|
|
15
|
+
import { agentBrowserInstall, bunBootstrap, effectSolutionsInstall } from "./skillsSync"
|
|
16
|
+
import { ensure, report } from "./toolchain"
|
|
17
|
+
|
|
18
|
+
export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
19
|
+
let tool = ""
|
|
20
|
+
let value = ""
|
|
21
|
+
for (const arg of args) {
|
|
22
|
+
if (arg === "--dry-run") ctx.dryRun = true
|
|
23
|
+
else if (arg === "claude" || arg === "codex") tool = arg
|
|
24
|
+
else if (arg.startsWith("-")) {
|
|
25
|
+
err(`Unknown flag for model: ${arg}`)
|
|
26
|
+
return 2
|
|
27
|
+
} else value = arg
|
|
28
|
+
}
|
|
29
|
+
if (tool === "") {
|
|
30
|
+
err("Usage: model <claude|codex> [value] [--dry-run]")
|
|
31
|
+
return 2
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (value === "") {
|
|
35
|
+
if (tool === "claude") {
|
|
36
|
+
const deployed = p(ctx.home, ".claude", "settings.json")
|
|
37
|
+
if (!fileReadable(deployed)) {
|
|
38
|
+
warn("~/.claude/settings.json missing")
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
echo(`deployed: ${jsonModelField(deployed)}`)
|
|
42
|
+
echo(`SoT: ${jsonModelField(p(ctx.repoDir, "SoT", ".claude", "settings.json"))}`)
|
|
43
|
+
} else {
|
|
44
|
+
const deployed = p(ctx.home, ".codex", "config.toml")
|
|
45
|
+
if (!fileReadable(deployed)) {
|
|
46
|
+
warn("~/.codex/config.toml missing")
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
49
|
+
echo(`deployed: ${tomlModelField(deployed)}`)
|
|
50
|
+
echo(`SoT: ${tomlModelField(p(ctx.repoDir, "SoT", ".codex", "config.toml"))}`)
|
|
51
|
+
}
|
|
52
|
+
printModels(ctx.repoDir, tool)
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (tool === "claude") {
|
|
57
|
+
if (!validateClaudeModel(ctx.repoDir, value)) {
|
|
58
|
+
printModels(ctx.repoDir, "claude")
|
|
59
|
+
err(`Invalid Claude model '${value}'`)
|
|
60
|
+
return 2
|
|
61
|
+
}
|
|
62
|
+
syncClaudeModel(ctx, value)
|
|
63
|
+
} else {
|
|
64
|
+
if (!validateCodexModel(ctx.repoDir, value)) {
|
|
65
|
+
printModels(ctx.repoDir, "codex")
|
|
66
|
+
err(`Invalid Codex model '${value}'`)
|
|
67
|
+
return 2
|
|
68
|
+
}
|
|
69
|
+
syncCodexModel(ctx, value)
|
|
70
|
+
}
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fileReadable(p: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
readFileSync(p)
|
|
77
|
+
return true
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `jq -r '.model // "default (unset)"'` — empty on unparseable input. */
|
|
84
|
+
function jsonModelField(file: string): string {
|
|
85
|
+
const doc = parseJson(readFileSync(file, "utf8"))
|
|
86
|
+
if (doc === undefined) return ""
|
|
87
|
+
const v: Json | undefined = isObject(doc) ? doc["model"] : undefined
|
|
88
|
+
if (v === undefined || v === null || v === false) return "default (unset)"
|
|
89
|
+
return typeof v === "string" ? v : JSON.stringify(v)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
|
|
93
|
+
function tomlModelField(file: string): string {
|
|
94
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
95
|
+
if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? ""
|
|
96
|
+
}
|
|
97
|
+
return ""
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
101
|
+
const op = args[0] ?? "check"
|
|
102
|
+
const tool = args[1] ?? ""
|
|
103
|
+
for (const arg of args) {
|
|
104
|
+
if (arg === "--yes") ctx.assumeYes = true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (op === "check") {
|
|
108
|
+
report(ctx)
|
|
109
|
+
return 0
|
|
110
|
+
}
|
|
111
|
+
if (op !== "ensure") {
|
|
112
|
+
err("Usage: toolchain [check|ensure <tool>] [--yes]")
|
|
113
|
+
return 2
|
|
114
|
+
}
|
|
115
|
+
if (tool === "" || tool === "--yes") {
|
|
116
|
+
err("Usage: toolchain ensure <tool> [--yes]")
|
|
117
|
+
return 2
|
|
118
|
+
}
|
|
119
|
+
switch (tool) {
|
|
120
|
+
case "rtk":
|
|
121
|
+
return ensure(ctx, "rtk", rtkInstall(ctx))
|
|
122
|
+
case "bun":
|
|
123
|
+
// skills::_bun_bootstrap >/dev/null — the found-bun stdout is discarded.
|
|
124
|
+
return bunBootstrap(ctx) !== "" ? 0 : 1
|
|
125
|
+
case "effect-solutions":
|
|
126
|
+
return ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
127
|
+
case "agent-browser":
|
|
128
|
+
return ensure(ctx, "agent-browser", agentBrowserInstall)
|
|
129
|
+
default:
|
|
130
|
+
err("toolchain ensure supports managed tools only (rtk, bun, effect-solutions, agent-browser)")
|
|
131
|
+
return 2
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared log/warn/err and dry-run emitters. Prefixes go to stderr with stable
|
|
3
|
+
* ANSI codes; dry-run lines go to stdout.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function log(msg: string): void {
|
|
7
|
+
process.stderr.write(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function warn(msg: string): void {
|
|
11
|
+
process.stderr.write(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function err(msg: string): void {
|
|
15
|
+
process.stderr.write(`\x1b[1;31m[err]\x1b[0m ${msg}\n`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function echo(line: string): void {
|
|
19
|
+
process.stdout.write(`${line}\n`)
|
|
20
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative flag layer: usage / target selection / compact-window parsing /
|
|
3
|
+
* optional-plugin parsing / model validation / preflight. ExitError mirrors an
|
|
4
|
+
* early parser exit and is caught once in runEngineNative.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { commandExists } from "./exec"
|
|
8
|
+
import type { Ctx } from "./index"
|
|
9
|
+
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
10
|
+
import { echo, err, warn } from "./output"
|
|
11
|
+
|
|
12
|
+
export class ExitError extends Error {
|
|
13
|
+
constructor(readonly code: number) {
|
|
14
|
+
super(`exit ${code}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
|
|
19
|
+
|
|
20
|
+
function usage(ctx: Ctx): void {
|
|
21
|
+
const argv0 = "docks-kit sync"
|
|
22
|
+
echo(`Usage: ${argv0} [claude] [codex] [agents] [flags]`)
|
|
23
|
+
echo("")
|
|
24
|
+
echo("Targets (positional; default: all three)")
|
|
25
|
+
echo(" claude sync the Claude Code SoT")
|
|
26
|
+
echo(" codex sync the Codex SoT")
|
|
27
|
+
echo(" agents sync universal agent skills")
|
|
28
|
+
echo("")
|
|
29
|
+
echo("Global flags")
|
|
30
|
+
echo(" --dry-run preview without applying")
|
|
31
|
+
echo(
|
|
32
|
+
" --reconcile reconcile kit-owned settings with SoT (SoT keys win; user-only keys preserved; permissions arrays replaced)"
|
|
33
|
+
)
|
|
34
|
+
echo(
|
|
35
|
+
" --prune uninstall kit-managed installs not in SoT (plugins, marketplaces, skills in SoT/.agents/skills.txt)"
|
|
36
|
+
)
|
|
37
|
+
echo(" --skip-rtk skip optional tool bootstrap (RTK, bubblewrap)")
|
|
38
|
+
echo(" --yes auto-accept toolchain prompts (containers/CI)")
|
|
39
|
+
echo("")
|
|
40
|
+
echo("Deploy-time modifiers (deployed config only; SoT untouched; a later flag-less sync reverts)")
|
|
41
|
+
echo(
|
|
42
|
+
" --claude-model=<m> set deployed ~/.claude/settings.json model (aliases: best|opus|fable|sonnet|haiku, full claude-* IDs, or 'default' to unset)"
|
|
43
|
+
)
|
|
44
|
+
echo(
|
|
45
|
+
" --claude-compact-window=<n> set deployed autocompact window in tokens (e.g. 680000 or 680k) for disposable sessions"
|
|
46
|
+
)
|
|
47
|
+
echo(
|
|
48
|
+
" --claude-permissive empty permissions.ask/deny in deployed settings (sandboxes/containers; unattended commits + pushes)"
|
|
49
|
+
)
|
|
50
|
+
echo(" --codex-model=<m> set deployed ~/.codex/config.toml model (e.g. gpt-5.5)")
|
|
51
|
+
echo("")
|
|
52
|
+
echo("Sticky opt-ins (installed + enabled until --prune)")
|
|
53
|
+
echo(
|
|
54
|
+
` --claude-plugin=<name> opt an optional plugin into this machine (known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}; repeatable)`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** common::parse_compact_window — normalized tokens or undefined on junk. */
|
|
59
|
+
export function parseCompactWindow(v: string): string | undefined {
|
|
60
|
+
if (/[kK]$/.test(v)) {
|
|
61
|
+
const n = v.slice(0, -1)
|
|
62
|
+
if (!/^[0-9]+$/.test(n)) return undefined
|
|
63
|
+
return String(parseInt(n, 10) * 1000)
|
|
64
|
+
}
|
|
65
|
+
return /^[0-9]+$/.test(v) ? v : undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function addClaudePlugin(ctx: Ctx, name: string): void {
|
|
69
|
+
if (!KNOWN_CLAUDE_OPTIN_PLUGINS.includes(name)) {
|
|
70
|
+
err(`Unknown opt-in plugin '${name}'. Known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}`)
|
|
71
|
+
throw new ExitError(2)
|
|
72
|
+
}
|
|
73
|
+
ctx.claudePlugins.push(name)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function selectTarget(ctx: Ctx, target: string): void {
|
|
77
|
+
if (target === "claude") ctx.syncClaude = true
|
|
78
|
+
else if (target === "codex") ctx.syncCodex = true
|
|
79
|
+
else ctx.syncAgents = true
|
|
80
|
+
ctx.targetFilterSet = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
84
|
+
for (const arg of args) {
|
|
85
|
+
switch (arg) {
|
|
86
|
+
case "claude":
|
|
87
|
+
case "codex":
|
|
88
|
+
case "agents":
|
|
89
|
+
selectTarget(ctx, arg)
|
|
90
|
+
continue
|
|
91
|
+
case "--dry-run":
|
|
92
|
+
ctx.dryRun = true
|
|
93
|
+
continue
|
|
94
|
+
case "--skip-rtk":
|
|
95
|
+
ctx.skipRtk = true
|
|
96
|
+
continue
|
|
97
|
+
case "--reconcile":
|
|
98
|
+
ctx.reconcile = true
|
|
99
|
+
continue
|
|
100
|
+
case "--prune":
|
|
101
|
+
ctx.prune = true
|
|
102
|
+
continue
|
|
103
|
+
case "--yes":
|
|
104
|
+
ctx.assumeYes = true
|
|
105
|
+
continue
|
|
106
|
+
case "--claude-model":
|
|
107
|
+
printModels(ctx.repoDir, "claude")
|
|
108
|
+
err("--claude-model requires a value: --claude-model=<model>")
|
|
109
|
+
throw new ExitError(2)
|
|
110
|
+
case "--codex-model":
|
|
111
|
+
printModels(ctx.repoDir, "codex")
|
|
112
|
+
err("--codex-model requires a value: --codex-model=<model>")
|
|
113
|
+
throw new ExitError(2)
|
|
114
|
+
case "--claude-compact-window":
|
|
115
|
+
err("--claude-compact-window requires a value: --claude-compact-window=<tokens> (e.g. 680k)")
|
|
116
|
+
throw new ExitError(2)
|
|
117
|
+
case "--claude-permissive":
|
|
118
|
+
ctx.claudePermissive = true
|
|
119
|
+
continue
|
|
120
|
+
case "--claude-plugin":
|
|
121
|
+
err(`--claude-plugin requires a value: --claude-plugin=<${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}>`)
|
|
122
|
+
throw new ExitError(2)
|
|
123
|
+
case "--claude":
|
|
124
|
+
case "--codex":
|
|
125
|
+
case "--agents":
|
|
126
|
+
err(`${arg} was renamed: pass the target as a word, e.g. 'sync ${arg.slice(2)}'`)
|
|
127
|
+
throw new ExitError(2)
|
|
128
|
+
case "--force":
|
|
129
|
+
err("--force was renamed to --reconcile")
|
|
130
|
+
throw new ExitError(2)
|
|
131
|
+
case "--remove-plugins":
|
|
132
|
+
err("--remove-plugins was renamed to --prune (it also removes marketplaces + kit-managed skills)")
|
|
133
|
+
throw new ExitError(2)
|
|
134
|
+
case "--680k":
|
|
135
|
+
err("--680k was renamed to --claude-compact-window=680k")
|
|
136
|
+
throw new ExitError(2)
|
|
137
|
+
case "--permissive":
|
|
138
|
+
err("--permissive was renamed to --claude-permissive")
|
|
139
|
+
throw new ExitError(2)
|
|
140
|
+
case "--supabase":
|
|
141
|
+
err("--supabase was renamed to --claude-plugin=supabase")
|
|
142
|
+
throw new ExitError(2)
|
|
143
|
+
case "--n8n":
|
|
144
|
+
err("--n8n was renamed to --claude-plugin=n8n")
|
|
145
|
+
throw new ExitError(2)
|
|
146
|
+
case "--no-rtk":
|
|
147
|
+
err("--no-rtk was renamed to --skip-rtk")
|
|
148
|
+
throw new ExitError(2)
|
|
149
|
+
case "-h":
|
|
150
|
+
case "--help":
|
|
151
|
+
usage(ctx)
|
|
152
|
+
throw new ExitError(0)
|
|
153
|
+
default:
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
if (arg.startsWith("--claude-model=")) {
|
|
157
|
+
ctx.claudeModel = arg.slice("--claude-model=".length)
|
|
158
|
+
} else if (arg.startsWith("--codex-model=")) {
|
|
159
|
+
ctx.codexModel = arg.slice("--codex-model=".length)
|
|
160
|
+
} else if (arg.startsWith("--claude-compact-window=")) {
|
|
161
|
+
const parsed = parseCompactWindow(arg.slice("--claude-compact-window=".length))
|
|
162
|
+
if (parsed === undefined) {
|
|
163
|
+
err("--claude-compact-window expects a token count (e.g. 680000 or 680k)")
|
|
164
|
+
throw new ExitError(2)
|
|
165
|
+
}
|
|
166
|
+
ctx.claudeCompactWindow = parsed
|
|
167
|
+
} else if (arg.startsWith("--claude-plugin=")) {
|
|
168
|
+
addClaudePlugin(ctx, arg.slice("--claude-plugin=".length))
|
|
169
|
+
} else {
|
|
170
|
+
err(`Unknown arg: ${arg}`)
|
|
171
|
+
throw new ExitError(2)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!ctx.targetFilterSet) {
|
|
176
|
+
ctx.syncClaude = true
|
|
177
|
+
ctx.syncCodex = true
|
|
178
|
+
ctx.syncAgents = true
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function preflight(ctx: Ctx): void {
|
|
183
|
+
if (ctx.syncClaude || ctx.syncCodex) {
|
|
184
|
+
if (!commandExists("jq")) {
|
|
185
|
+
const hint = process.platform === "win32"
|
|
186
|
+
? "winget install jqlang.jq (then open a new terminal)"
|
|
187
|
+
: process.platform === "darwin"
|
|
188
|
+
? "brew install jq"
|
|
189
|
+
: "sudo apt install -y jq"
|
|
190
|
+
err(`jq is required (deployed statusline/hooks call it). Install: ${hint}`)
|
|
191
|
+
throw new ExitError(1)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (ctx.syncClaude) {
|
|
195
|
+
if (!commandExists("curl")) {
|
|
196
|
+
err("curl is required.")
|
|
197
|
+
throw new ExitError(1)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function validateModelFlags(ctx: Ctx): void {
|
|
203
|
+
if (ctx.claudeModel !== "") {
|
|
204
|
+
if (!ctx.syncClaude) {
|
|
205
|
+
warn("--claude-model ignored: claude target not selected")
|
|
206
|
+
ctx.claudeModel = ""
|
|
207
|
+
} else if (!validateClaudeModel(ctx.repoDir, ctx.claudeModel)) {
|
|
208
|
+
printModels(ctx.repoDir, "claude")
|
|
209
|
+
err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`)
|
|
210
|
+
throw new ExitError(2)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (ctx.codexModel !== "") {
|
|
214
|
+
if (!ctx.syncCodex) {
|
|
215
|
+
warn("--codex-model ignored: codex target not selected")
|
|
216
|
+
ctx.codexModel = ""
|
|
217
|
+
} else if (!validateCodexModel(ctx.repoDir, ctx.codexModel)) {
|
|
218
|
+
printModels(ctx.repoDir, "codex")
|
|
219
|
+
err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`)
|
|
220
|
+
throw new ExitError(2)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `~/.claude/settings.json` merge helpers. Pure functions take the two parsed
|
|
3
|
+
* documents and return the merged document; the FS step (backup, tmp+rename,
|
|
4
|
+
* log line) lives in the sync orchestrator. jq-oracle equivalence is covered by
|
|
5
|
+
* cli/test/unit/settings.test.ts and deployed behavior by the golden suites.
|
|
6
|
+
*/
|
|
7
|
+
import { deepMerge, isObject, uniqueStrings, type Json } from "./jq"
|
|
8
|
+
|
|
9
|
+
/** `$user * $repo` — SoT keys win, permissions arrays replaced wholesale. */
|
|
10
|
+
export function reconcileSettings(repo: Json, user: Json): Json {
|
|
11
|
+
return deepMerge(user, repo)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `($user * $repo)` + permissions.{allow,deny,ask} unioned (user + repo,
|
|
16
|
+
* `unique` — i.e. codepoint-sorted and deduplicated, matching jq).
|
|
17
|
+
*/
|
|
18
|
+
export function mergeSettings(repo: Json, user: Json): Json {
|
|
19
|
+
const merged = deepMerge(user, repo)
|
|
20
|
+
if (!isObject(merged)) return merged
|
|
21
|
+
const permissions = isObject(merged["permissions"]) ? merged["permissions"] : {}
|
|
22
|
+
merged["permissions"] = {
|
|
23
|
+
...permissions,
|
|
24
|
+
allow: unionPermissions(user, repo, "allow"),
|
|
25
|
+
deny: unionPermissions(user, repo, "deny"),
|
|
26
|
+
ask: unionPermissions(user, repo, "ask")
|
|
27
|
+
}
|
|
28
|
+
return merged
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function unionPermissions(user: Json, repo: Json, key: string): Array<string> {
|
|
32
|
+
return uniqueStrings([...permissionArray(user, key), ...permissionArray(repo, key)])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function permissionArray(doc: Json, key: string): Array<string> {
|
|
36
|
+
if (!isObject(doc)) return []
|
|
37
|
+
const permissions = doc["permissions"]
|
|
38
|
+
if (!isObject(permissions)) return []
|
|
39
|
+
const arr = permissions[key]
|
|
40
|
+
return Array.isArray(arr) ? arr.filter((x): x is string => typeof x === "string") : []
|
|
41
|
+
}
|