docks-kit 0.2.0 → 0.3.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 +1 -1
- package/README.md +6 -4
- package/cli/docs/install.md +9 -11
- package/cli/docs/overview.md +3 -0
- package/cli/docs/platforms.md +3 -5
- package/cli/src/engine-native/DESIGN.md +4 -0
- package/cli/src/engine-native/claudeSync.ts +23 -25
- package/cli/src/engine-native/codexSync.ts +42 -36
- package/cli/src/engine-native/exec.ts +8 -23
- package/cli/src/engine-native/index.ts +3 -4
- package/cli/src/engine-native/models.ts +14 -16
- package/cli/src/engine-native/modes.ts +13 -4
- package/cli/src/engine-native/skillsSync.ts +7 -10
- package/cli/src/engine-native/toolchain.ts +6 -6
- package/cli/src/generated/sotPayload.ts +41 -0
- package/cli/src/kitHome.ts +15 -11
- package/cli/src/manifests.ts +17 -15
- package/cli/src/payload.ts +28 -0
- package/docks-kit +6 -6
- package/package.json +2 -3
- package/SoT/.agents/skills.txt +0 -14
- package/SoT/.claude/CLAUDE.md +0 -146
- package/SoT/.claude/fetch-usage.sh +0 -66
- package/SoT/.claude/hooks/notify.sh +0 -14
- package/SoT/.claude/mcp-servers.json +0 -10
- package/SoT/.claude/settings.json +0 -235
- package/SoT/.claude/statusline.sh +0 -175
- package/SoT/.codex/AGENTS.md +0 -75
- package/SoT/.codex/agents/.gitkeep +0 -1
- package/SoT/.codex/config.toml +0 -45
- package/SoT/.codex/plugins/marketplace.json +0 -50
- package/SoT/.codex/rules/docks.rules +0 -116
- package/SoT/models.json +0 -31
- package/SoT/toolchain.json +0 -27
- package/notification.mp3 +0 -0
package/AGENTS.md
CHANGED
|
@@ -22,7 +22,7 @@ Tool-specific instructions live alongside this file:
|
|
|
22
22
|
| `install.sh` | Global installer |
|
|
23
23
|
| `.github/workflows/release-cli.yml` | `cli-v*` release binaries + npm publish |
|
|
24
24
|
| `README.md` | Front door |
|
|
25
|
-
| `package.json` / `bun.lock` | npm package: `bin` = `cli/src/main.ts`; bundles `cli/`
|
|
25
|
+
| `package.json` / `bun.lock` | npm package: `bin` = `cli/src/main.ts`; bundles `cli/` with the generated in-memory SoT payload |
|
|
26
26
|
| `SoT/.agents/skills.txt` | Universal-skill manifest. One [agentskills.io](https://agentskills.io) slug per line; EngineNative runs `npx skills add` for each missing entry into `~/.agents/skills/`, where Codex et al. discover it natively and Claude Code follows a symlink at `~/.claude/skills/` |
|
|
27
27
|
| `notification.mp3` | Audio asset for Notification hooks (consumed by Claude Code today; tool-agnostic file) |
|
|
28
28
|
| `docs/plans/` | Multi-commit work-item plans (`active/` with status in frontmatter, plus `finished/` archive). Convention: `docs/plans/AGENTS.md` |
|
package/README.md
CHANGED
|
@@ -25,8 +25,9 @@ see `./docks-kit docs install`.
|
|
|
25
25
|
|
|
26
26
|
**No-Bun recovery path**:
|
|
27
27
|
|
|
28
|
-
Download the platform release binary from GitHub Releases and run it
|
|
29
|
-
|
|
28
|
+
Download the platform release binary from GitHub Releases and run it directly.
|
|
29
|
+
The executable carries the generated sync payload; no checkout or adjacent
|
|
30
|
+
`SoT/` directory is required.
|
|
30
31
|
|
|
31
32
|
Prerequisites for source/global installs: Bun plus `jq` and `curl` for sync
|
|
32
33
|
preflight; Node/npm for npm-global tools.
|
|
@@ -93,6 +94,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
|
|
|
93
94
|
| `SoT/models.json` | Kit-verified model catalog |
|
|
94
95
|
| `SoT/toolchain.json` | Verified-version floors |
|
|
95
96
|
| `cli/src/engine-native/` | EngineNative sync/model/toolchain implementation |
|
|
97
|
+
| `cli/src/generated/sotPayload.ts` | Generated in-memory payload used by standalone and npm installs |
|
|
96
98
|
| `cli/` | docks-kit CLI (Effect-TS on Bun) + bundled docs topics |
|
|
97
99
|
| `docks-kit` | Launcher (binary → bun-from-source) |
|
|
98
100
|
| `install.sh` | Global installer (Bun bootstrap + `bun add -g`) |
|
|
@@ -113,8 +115,8 @@ Details: `docks-kit docs platforms`.
|
|
|
113
115
|
|
|
114
116
|
Tagging `cli-v*` builds five standalone binaries (+ SHA256SUMS) and attaches
|
|
115
117
|
them to the GitHub release; npm publish runs when the `NPM_TOKEN` secret is
|
|
116
|
-
configured. Package bundles the CLI +
|
|
117
|
-
snapshots.
|
|
118
|
+
configured. Package bundles the CLI + generated payload, so npm releases are
|
|
119
|
+
versioned config snapshots without shipping the authoring `SoT/` tree.
|
|
118
120
|
|
|
119
121
|
## Deeper docs
|
|
120
122
|
|
package/cli/docs/install.md
CHANGED
|
@@ -21,10 +21,10 @@ docks-kit sync
|
|
|
21
21
|
bunx docks-kit status
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The npm package bundles the CLI and
|
|
25
|
-
snapshots
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
The npm package bundles the CLI and its generated in-memory payload — releases
|
|
25
|
+
are versioned config snapshots without publishing the authoring `SoT/` tree.
|
|
26
|
+
Kit-home resolution remains available for checkout/package update behavior and
|
|
27
|
+
display paths, but sync reads do not depend on it.
|
|
28
28
|
|
|
29
29
|
## 3. curl installer (Unix-only)
|
|
30
30
|
|
|
@@ -44,12 +44,10 @@ kit before. The installer bootstraps Bun when absent, runs
|
|
|
44
44
|
Two supported paths (CI-verified on windows-2025, native PowerShell):
|
|
45
45
|
|
|
46
46
|
- **Compiled binary** — `docks-kit-windows-x64.exe` release asset. No Bun,
|
|
47
|
-
no Git Bash: the exe embeds the runtime and
|
|
48
|
-
|
|
49
|
-
point `DOCKS_KIT_HOME` at one.
|
|
47
|
+
no Git Bash: the exe embeds the runtime and generated payload, and
|
|
48
|
+
EngineNative runs in-process from any working directory.
|
|
50
49
|
- **`bun add -g docks-kit`** — bun creates a working shim for the
|
|
51
|
-
`#!/usr/bin/env bun` bin;
|
|
52
|
-
SoT is used.
|
|
50
|
+
`#!/usr/bin/env bun` bin; the package carries the same generated payload.
|
|
53
51
|
|
|
54
52
|
`install.sh` is not a Windows path.
|
|
55
53
|
|
|
@@ -72,8 +70,8 @@ nudges when the checkout is stale (silent offline / detached / no git).
|
|
|
72
70
|
## No-Bun recovery
|
|
73
71
|
|
|
74
72
|
No Bun or constrained sandbox: download the platform release binary from GitHub
|
|
75
|
-
Releases and run it
|
|
76
|
-
|
|
73
|
+
Releases and run it directly. No checkout or `DOCKS_KIT_HOME` is required for
|
|
74
|
+
sync/config reads.
|
|
77
75
|
|
|
78
76
|
## Prerequisites
|
|
79
77
|
|
package/cli/docs/overview.md
CHANGED
|
@@ -14,6 +14,7 @@ AI-assisted dev environment on every machine.
|
|
|
14
14
|
| `SoT/.agents/` | Universal agent skills manifest (agentskills.io standard) |
|
|
15
15
|
| `SoT/models.json` | Kit-verified model catalog (see `docks-kit docs models`) |
|
|
16
16
|
| `SoT/toolchain.json` | Verified-version floors for external tools (see `docks-kit docs toolchain`) |
|
|
17
|
+
| `cli/src/generated/sotPayload.ts` | Deterministic generated payload embedded in standalone/npm execution |
|
|
17
18
|
| `cli/src/engine-native/` | EngineNative mutation logic for sync/model/toolchain |
|
|
18
19
|
| `cli/` | This CLI (Effect-TS on Bun) plus bundled docs |
|
|
19
20
|
| `docks-kit` | Launcher: compiled binary → bun-from-source, with Bun auto-install |
|
|
@@ -31,6 +32,8 @@ AI-assisted dev environment on every machine.
|
|
|
31
32
|
changes, and a later flag-less sync reverts them (`docks-kit docs modifiers`).
|
|
32
33
|
- **Engine/CLI split**: the CLI adds UX (typed flags, pickers, docs, JSON);
|
|
33
34
|
EngineNative owns mutation. No-Bun recovery is a platform release binary.
|
|
35
|
+
- **Authoring/runtime split**: changes begin in `SoT/`; build/prepack freshness
|
|
36
|
+
checks keep the generated in-memory payload byte-identical for every runtime.
|
|
34
37
|
|
|
35
38
|
## Where to go next
|
|
36
39
|
|
package/cli/docs/platforms.md
CHANGED
|
@@ -16,12 +16,10 @@ Supported entrypoints (both verified in CI on windows-2025 under native
|
|
|
16
16
|
PowerShell — see `.github/workflows/windows-entrypoints.yml`):
|
|
17
17
|
|
|
18
18
|
- **`docks-kit-windows-x64.exe`** (release asset) — the no-toolchain path.
|
|
19
|
-
No Bun, no Git Bash; the exe embeds the runtime and
|
|
20
|
-
in-process
|
|
21
|
-
set `DOCKS_KIT_HOME`.
|
|
19
|
+
No Bun, no Git Bash; the exe embeds the runtime and generated payload, and
|
|
20
|
+
EngineNative runs in-process from any working directory.
|
|
22
21
|
- **`bun add -g docks-kit`** — bun creates a working Windows shim for the
|
|
23
|
-
`#!/usr/bin/env bun` bin;
|
|
24
|
-
package's own bundled SoT.
|
|
22
|
+
`#!/usr/bin/env bun` bin; the package carries the same generated payload.
|
|
25
23
|
- `install.sh` is **Unix-only** and not a Windows path.
|
|
26
24
|
|
|
27
25
|
The managed tools all run natively on Windows: Claude Code (requires Git
|
|
@@ -36,6 +36,9 @@ explicit removed-engine diagnostic and exits 2 with the recovery tag message.
|
|
|
36
36
|
shell script.
|
|
37
37
|
- **Backups precede mutation.** Deployed settings/config files write `.bak`
|
|
38
38
|
backups before replacement.
|
|
39
|
+
- **Runtime payload is in memory.** `SoT/` remains the reviewed authoring tree;
|
|
40
|
+
generated payload freshness gates build/package production, and EngineNative
|
|
41
|
+
never needs an adjacent runtime `SoT/` directory.
|
|
39
42
|
|
|
40
43
|
## Output Policy (log UX contract)
|
|
41
44
|
|
|
@@ -113,6 +116,7 @@ active logger binding.
|
|
|
113
116
|
|---|---|
|
|
114
117
|
| `parseArgs.ts` | engine usage, target selection, flag parsing, legacy rename hints, preflight, model flag validation |
|
|
115
118
|
| `index.ts` | sync orchestration, target dispatch, run summary and next-step blocks |
|
|
119
|
+
| `../payload.ts` | generated text/byte payload reads and presentation-only source labels |
|
|
116
120
|
| `claudeSync.ts` | Claude pipeline: RTK, assets, settings merge, deploy-time modifiers, `~/.claude.json`, connector env, removed artifacts, plugins, optional plugins, LSP binaries |
|
|
117
121
|
| `settings.ts` | pure Claude settings merge/reconcile semantics and permission-array union |
|
|
118
122
|
| `claudeModel.ts` | deployed Claude model modifier and direct `model claude` write path |
|
|
@@ -17,13 +17,14 @@ import {
|
|
|
17
17
|
} from "node:fs"
|
|
18
18
|
import { tmpdir } from "node:os"
|
|
19
19
|
import { syncClaudeModel } from "./claudeModel"
|
|
20
|
-
import {
|
|
20
|
+
import { ensureExecutable, p, writeBytesIfChanged, writeFileIfChanged, writeTextIfChanged } from "./exec"
|
|
21
21
|
import type { Ctx } from "./index"
|
|
22
22
|
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
23
23
|
import type { EngineServices } from "./services"
|
|
24
24
|
import { ExitError } from "./parseArgs"
|
|
25
25
|
import { mergeSettings, reconcileSettings } from "./settings"
|
|
26
26
|
import { ensure, field } from "./toolchain"
|
|
27
|
+
import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
|
|
27
28
|
|
|
28
29
|
export function claudeSync(ctx: Ctx): void {
|
|
29
30
|
const { warn } = ctx.services.logger
|
|
@@ -127,15 +128,15 @@ function syncScripts(ctx: Ctx, claudeDir: string): void {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
let changed = false
|
|
130
|
-
for (const script of [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
131
|
+
for (const [script, source] of [
|
|
132
|
+
["statusline.sh", "SoT/.claude/statusline.sh"],
|
|
133
|
+
["fetch-usage.sh", "SoT/.claude/fetch-usage.sh"]
|
|
134
|
+
] as const) {
|
|
135
|
+
const path = p(claudeDir, script)
|
|
136
|
+
if (writeTextIfChanged(path, payloadText(source))) changed = true
|
|
137
|
+
if (ensureExecutable(path)) changed = true
|
|
138
|
+
}
|
|
139
|
+
if (writeBytesIfChanged(p(claudeDir, "notification.mp3"), payloadBytes("notification.mp3"))) changed = true
|
|
139
140
|
if (changed) {
|
|
140
141
|
change("Scripts synced (statusline, fetch-usage, notification)")
|
|
141
142
|
ctx.nextStepTriggers.claudeRestart = true
|
|
@@ -152,8 +153,7 @@ function shellScriptCount(hooksDir: string): number {
|
|
|
152
153
|
|
|
153
154
|
function syncHooks(ctx: Ctx, claudeDir: string): void {
|
|
154
155
|
const { change, echo, verbose } = ctx.services.logger
|
|
155
|
-
const sotHooks =
|
|
156
|
-
if (!existsSync(sotHooks)) return
|
|
156
|
+
const sotHooks = payloadDisplayPath("SoT/.claude/hooks/notify.sh", ctx.repoDir).replace(/\/notify\.sh$/, "")
|
|
157
157
|
|
|
158
158
|
if (ctx.dryRun) {
|
|
159
159
|
echo(`[dry-run] cp -R ${sotHooks}/. ${claudeDir}/hooks/`)
|
|
@@ -162,7 +162,7 @@ function syncHooks(ctx: Ctx, claudeDir: string): void {
|
|
|
162
162
|
|
|
163
163
|
const hooksDir = p(claudeDir, "hooks")
|
|
164
164
|
mkdirSync(hooksDir, { recursive: true })
|
|
165
|
-
let changed =
|
|
165
|
+
let changed = writeTextIfChanged(p(hooksDir, "notify.sh"), payloadText("SoT/.claude/hooks/notify.sh"))
|
|
166
166
|
for (const e of readdirSync(hooksDir, { withFileTypes: true })) {
|
|
167
167
|
if (e.isFile() && e.name.endsWith(".sh")) {
|
|
168
168
|
if (ensureExecutable(p(hooksDir, e.name))) changed = true
|
|
@@ -193,10 +193,10 @@ function syncClaudeMd(ctx: Ctx, claudeDir: string): void {
|
|
|
193
193
|
return
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
const
|
|
196
|
+
const source = payloadText("SoT/.claude/CLAUDE.md")
|
|
197
197
|
const stripReason = ctx.skipRtk ? "--skip-rtk" : rtkMdAbsent ? "~/.claude/RTK.md absent (rtk not initialized)" : ""
|
|
198
198
|
if (stripReason !== "") {
|
|
199
|
-
const stripped =
|
|
199
|
+
const stripped = source
|
|
200
200
|
.split("\n")
|
|
201
201
|
.filter((l) => l !== "@RTK.md")
|
|
202
202
|
.join("\n")
|
|
@@ -205,7 +205,7 @@ function syncClaudeMd(ctx: Ctx, claudeDir: string): void {
|
|
|
205
205
|
} else {
|
|
206
206
|
verbose("CLAUDE.md already in sync")
|
|
207
207
|
}
|
|
208
|
-
} else if (
|
|
208
|
+
} else if (writeTextIfChanged(p(claudeDir, "CLAUDE.md"), source)) {
|
|
209
209
|
change("CLAUDE.md synced")
|
|
210
210
|
} else {
|
|
211
211
|
verbose("CLAUDE.md already in sync")
|
|
@@ -216,7 +216,8 @@ function syncClaudeMd(ctx: Ctx, claudeDir: string): void {
|
|
|
216
216
|
|
|
217
217
|
function syncSettings(ctx: Ctx, claudeDir: string): void {
|
|
218
218
|
const { change, echo, err, verbose } = ctx.services.logger
|
|
219
|
-
const repoSettings =
|
|
219
|
+
const repoSettings = payloadDisplayPath("SoT/.claude/settings.json", ctx.repoDir)
|
|
220
|
+
const repoSettingsText = payloadText("SoT/.claude/settings.json")
|
|
220
221
|
const userSettings = p(claudeDir, "settings.json")
|
|
221
222
|
|
|
222
223
|
if (ctx.dryRun) {
|
|
@@ -231,7 +232,7 @@ function syncSettings(ctx: Ctx, claudeDir: string): void {
|
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
if (!existsSync(userSettings)) {
|
|
234
|
-
|
|
235
|
+
writeFileSync(userSettings, repoSettingsText)
|
|
235
236
|
change("Settings installed")
|
|
236
237
|
ctx.nextStepTriggers.claudeRestart = true
|
|
237
238
|
return
|
|
@@ -243,7 +244,7 @@ function syncSettings(ctx: Ctx, claudeDir: string): void {
|
|
|
243
244
|
err(`Skipping settings sync: ${userSettings} is not valid JSON. Fix it manually or delete it to reinstall.`)
|
|
244
245
|
throw new ExitError(1)
|
|
245
246
|
}
|
|
246
|
-
const repo = parseJson(
|
|
247
|
+
const repo = parseJson(repoSettingsText)!
|
|
247
248
|
|
|
248
249
|
const merged = ctx.reconcile ? reconcileSettings(repo, user) : mergeSettings(repo, user)
|
|
249
250
|
const out = jqStringify(merged)
|
|
@@ -334,10 +335,8 @@ function syncPermissive(ctx: Ctx, claudeDir: string): void {
|
|
|
334
335
|
function syncClaudeJson(ctx: Ctx): void {
|
|
335
336
|
const { change, echo, err, verbose } = ctx.services.logger
|
|
336
337
|
const claudeJson = p(ctx.home, ".claude.json")
|
|
337
|
-
const mcpSot = p(ctx.repoDir, "SoT", ".claude", "mcp-servers.json")
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
if (existsSync(mcpSot)) mcp = parseJson(readFileSync(mcpSot, "utf8"))
|
|
339
|
+
const mcp: Json | undefined = parseJson(payloadText("SoT/.claude/mcp-servers.json"))
|
|
341
340
|
const haveMcp = mcp !== undefined
|
|
342
341
|
|
|
343
342
|
if (ctx.dryRun) {
|
|
@@ -564,7 +563,6 @@ function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): b
|
|
|
564
563
|
|
|
565
564
|
function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
566
565
|
const { change, echo, verbose, warn } = ctx.services.logger
|
|
567
|
-
const repoSettingsFile = p(ctx.repoDir, "SoT", ".claude", "settings.json")
|
|
568
566
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
569
567
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
570
568
|
|
|
@@ -589,7 +587,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
589
587
|
return
|
|
590
588
|
}
|
|
591
589
|
|
|
592
|
-
const repoSettings =
|
|
590
|
+
const repoSettings = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
593
591
|
const repoObj = repoSettings !== undefined && isObject(repoSettings) ? repoSettings : {}
|
|
594
592
|
const sotMarketplaces = isObject(repoObj["extraKnownMarketplaces"]) ? repoObj["extraKnownMarketplaces"] : {}
|
|
595
593
|
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
@@ -799,7 +797,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
|
799
797
|
|
|
800
798
|
function syncLspServers(ctx: Ctx): void {
|
|
801
799
|
const { change, echo, verbose, warn } = ctx.services.logger
|
|
802
|
-
const sot =
|
|
800
|
+
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
803
801
|
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
804
802
|
if (enabled === undefined) return
|
|
805
803
|
const hasPhp = Object.prototype.hasOwnProperty.call(enabled, "php-lsp@claude-plugins-official")
|
|
@@ -4,25 +4,26 @@
|
|
|
4
4
|
* change. Guard order, message strings, and backup behavior are golden-tested.
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from "node:child_process"
|
|
7
|
-
import { copyFileSync, existsSync, mkdirSync,
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
8
8
|
|
|
9
9
|
import { syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
|
|
10
10
|
import { p } from "./exec"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
13
|
+
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
13
14
|
|
|
14
15
|
export function codexSync(ctx: Ctx): void {
|
|
15
16
|
const codexDir = p(ctx.home, ".codex")
|
|
16
|
-
const sotConfig =
|
|
17
|
+
const sotConfig = payloadText("SoT/.codex/config.toml")
|
|
17
18
|
const userConfig = p(codexDir, "config.toml")
|
|
18
19
|
|
|
19
20
|
ensureBubblewrap(ctx)
|
|
20
21
|
if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
|
|
21
22
|
syncConfig(ctx, sotConfig, userConfig)
|
|
22
23
|
syncCodexModel(ctx, ctx.codexModel)
|
|
23
|
-
syncRules(ctx,
|
|
24
|
-
syncAgentsMd(ctx,
|
|
25
|
-
syncMarketplace(ctx,
|
|
24
|
+
syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
|
|
25
|
+
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
26
|
+
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
26
27
|
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
27
28
|
syncPlugins(ctx, sotConfig)
|
|
28
29
|
}
|
|
@@ -95,9 +96,9 @@ function bwrapDetectPmInstallCmd(ctx: Ctx): string {
|
|
|
95
96
|
|
|
96
97
|
// --------------------------------------------------------------- config ----
|
|
97
98
|
|
|
98
|
-
function syncConfig(ctx: Ctx,
|
|
99
|
+
function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
99
100
|
const { change, echo, verbose } = ctx.services.logger
|
|
100
|
-
|
|
101
|
+
const sotConfig = payloadDisplayPath("SoT/.codex/config.toml", ctx.repoDir)
|
|
101
102
|
|
|
102
103
|
if (ctx.dryRun) {
|
|
103
104
|
echo(`[dry-run] merge ${sotConfig} -> ${userConfig}`)
|
|
@@ -105,7 +106,7 @@ function syncConfig(ctx: Ctx, sotConfig: string, userConfig: string): void {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
if (!existsSync(userConfig)) {
|
|
108
|
-
|
|
109
|
+
writeFileSync(userConfig, sotConfigText)
|
|
109
110
|
change("Codex config installed")
|
|
110
111
|
ctx.nextStepTriggers.codexRestart = true
|
|
111
112
|
return
|
|
@@ -119,8 +120,8 @@ function syncConfig(ctx: Ctx, sotConfig: string, userConfig: string): void {
|
|
|
119
120
|
writeFileSync(staging, before)
|
|
120
121
|
|
|
121
122
|
scrubDeprecatedFeatures(ctx, staging)
|
|
122
|
-
mergeTopLevelSettings(
|
|
123
|
-
mergeTableSettings(
|
|
123
|
+
mergeTopLevelSettings(sotConfigText, staging)
|
|
124
|
+
mergeTableSettings(sotConfigText, staging)
|
|
124
125
|
|
|
125
126
|
if (readFileSync(staging, "utf8") === before) {
|
|
126
127
|
rmSync(staging, { force: true })
|
|
@@ -179,8 +180,8 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
|
179
180
|
change("Codex: scrubbed deprecated [features].use_legacy_landlock")
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
function mergeTopLevelSettings(
|
|
183
|
-
for (const line of
|
|
183
|
+
function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
|
|
184
|
+
for (const line of sotConfigText.split("\n")) {
|
|
184
185
|
if (line.startsWith("[")) break
|
|
185
186
|
if (/^[ \t]*($|#)/.test(line)) continue
|
|
186
187
|
if (!/^[A-Za-z0-9_.-]+[ \t]*=/.test(line)) continue
|
|
@@ -189,8 +190,8 @@ function mergeTopLevelSettings(sotConfig: string, userConfig: string): void {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
function mergeTableSettings(
|
|
193
|
-
const sotLines =
|
|
193
|
+
function mergeTableSettings(sotConfigText: string, userConfig: string): void {
|
|
194
|
+
const sotLines = sotConfigText.split("\n")
|
|
194
195
|
for (const tableHeader of sotLines.filter((l) => /^\[[^\]]+\]/.test(l))) {
|
|
195
196
|
// Extract the SoT block: from the exact header line to (excluding) the
|
|
196
197
|
// next table header; `$(...)` strips trailing newlines.
|
|
@@ -225,9 +226,12 @@ function mergeTableSettings(sotConfig: string, userConfig: string): void {
|
|
|
225
226
|
|
|
226
227
|
// ------------------------------------------------------- rules + agents ----
|
|
227
228
|
|
|
228
|
-
function syncRules(ctx: Ctx,
|
|
229
|
+
function syncRules(ctx: Ctx, sotRules: ReadonlyArray<PayloadPath>, userRulesDir: string): void {
|
|
229
230
|
const { change, echo, verbose } = ctx.services.logger
|
|
230
|
-
|
|
231
|
+
const firstRule = sotRules[0]
|
|
232
|
+
if (firstRule === undefined) return
|
|
233
|
+
const firstDisplay = payloadDisplayPath(firstRule, ctx.repoDir)
|
|
234
|
+
const sotRulesDir = firstDisplay.slice(0, firstDisplay.lastIndexOf("/"))
|
|
231
235
|
|
|
232
236
|
if (ctx.dryRun) {
|
|
233
237
|
echo(`[dry-run] cp ${sotRulesDir}/*.rules -> ${userRulesDir}/`)
|
|
@@ -237,17 +241,15 @@ function syncRules(ctx: Ctx, sotRulesDir: string, userRulesDir: string): void {
|
|
|
237
241
|
mkdirSync(userRulesDir, { recursive: true })
|
|
238
242
|
let sawRules = false
|
|
239
243
|
let rulesChanged = false
|
|
240
|
-
const ruleFiles =
|
|
241
|
-
.filter((e) => e.isFile() && e.name.endsWith(".rules"))
|
|
242
|
-
.map((e) => p(sotRulesDir, e.name))
|
|
243
|
-
.sort(compareCodepoints)
|
|
244
|
+
const ruleFiles = sotRules.filter((path) => path.endsWith(".rules")).sort(compareCodepoints)
|
|
244
245
|
for (const ruleFile of ruleFiles) {
|
|
245
246
|
sawRules = true
|
|
246
247
|
const userRuleFile = p(userRulesDir, ruleFile.slice(ruleFile.lastIndexOf("/") + 1))
|
|
247
|
-
const
|
|
248
|
+
const content = payloadBytes(ruleFile)
|
|
249
|
+
const identical = existsSync(userRuleFile) && readFileSync(userRuleFile).equals(content)
|
|
248
250
|
if (identical) continue
|
|
249
251
|
if (existsSync(userRuleFile)) copyFileSync(userRuleFile, `${userRuleFile}.bak`)
|
|
250
|
-
|
|
252
|
+
writeFileSync(userRuleFile, content)
|
|
251
253
|
rulesChanged = true
|
|
252
254
|
}
|
|
253
255
|
if (rulesChanged) {
|
|
@@ -256,30 +258,30 @@ function syncRules(ctx: Ctx, sotRulesDir: string, userRulesDir: string): void {
|
|
|
256
258
|
} else if (sawRules) verbose("Codex rules already in sync")
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
function syncAgentsMd(ctx: Ctx,
|
|
261
|
+
function syncAgentsMd(ctx: Ctx, sotAgentsMdText: string, userAgentsMd: string): void {
|
|
260
262
|
const { change, echo, verbose } = ctx.services.logger
|
|
261
|
-
|
|
263
|
+
const sotAgentsMd = payloadDisplayPath("SoT/.codex/AGENTS.md", ctx.repoDir)
|
|
262
264
|
|
|
263
265
|
if (ctx.dryRun) {
|
|
264
266
|
echo(`[dry-run] cp ${sotAgentsMd} -> ${userAgentsMd}`)
|
|
265
267
|
return
|
|
266
268
|
}
|
|
267
269
|
|
|
268
|
-
if (existsSync(userAgentsMd) && readFileSync(userAgentsMd)
|
|
270
|
+
if (existsSync(userAgentsMd) && readFileSync(userAgentsMd, "utf8") === sotAgentsMdText) {
|
|
269
271
|
verbose("Codex AGENTS.md already in sync")
|
|
270
272
|
return
|
|
271
273
|
}
|
|
272
274
|
if (existsSync(userAgentsMd)) copyFileSync(userAgentsMd, `${userAgentsMd}.bak`)
|
|
273
|
-
|
|
275
|
+
writeFileSync(userAgentsMd, sotAgentsMdText)
|
|
274
276
|
change("Codex AGENTS.md synced")
|
|
275
277
|
ctx.nextStepTriggers.codexRestart = true
|
|
276
278
|
}
|
|
277
279
|
|
|
278
280
|
// ---------------------------------------------------------- marketplace ----
|
|
279
281
|
|
|
280
|
-
function syncMarketplace(ctx: Ctx,
|
|
282
|
+
function syncMarketplace(ctx: Ctx, sotMarketplaceText: string, userMarketplace: string): void {
|
|
281
283
|
const { change, echo, err, verbose } = ctx.services.logger
|
|
282
|
-
|
|
284
|
+
const sotMarketplace = payloadDisplayPath("SoT/.codex/plugins/marketplace.json", ctx.repoDir)
|
|
283
285
|
|
|
284
286
|
if (ctx.dryRun) {
|
|
285
287
|
echo(`[dry-run] cp ${sotMarketplace} -> ${userMarketplace}`)
|
|
@@ -287,7 +289,7 @@ function syncMarketplace(ctx: Ctx, sotMarketplace: string, userMarketplace: stri
|
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
mkdirSync(p(ctx.agentsDir, "plugins"), { recursive: true })
|
|
290
|
-
const repo = parseJson(
|
|
292
|
+
const repo = parseJson(sotMarketplaceText)
|
|
291
293
|
if (repo === undefined) throw new Error(`invalid SoT marketplace JSON: ${sotMarketplace}`)
|
|
292
294
|
|
|
293
295
|
if (existsSync(userMarketplace)) {
|
|
@@ -307,7 +309,7 @@ function syncMarketplace(ctx: Ctx, sotMarketplace: string, userMarketplace: stri
|
|
|
307
309
|
change("Codex marketplace merged (backup at marketplace.json.bak)")
|
|
308
310
|
ctx.nextStepTriggers.codexRestart = true
|
|
309
311
|
} else {
|
|
310
|
-
|
|
312
|
+
writeFileSync(userMarketplace, sotMarketplaceText)
|
|
311
313
|
change("Codex marketplace installed")
|
|
312
314
|
ctx.nextStepTriggers.codexRestart = true
|
|
313
315
|
}
|
|
@@ -390,13 +392,17 @@ const standaloneInstallCommand = (ctx: Ctx): string => ctx.services.deps.spec("c
|
|
|
390
392
|
/** codex::_enabled_plugin_ids — [plugins."<id>"] tables with enabled = true. */
|
|
391
393
|
export function enabledPluginIds(configFile: string): Array<string> {
|
|
392
394
|
if (!existsSync(configFile)) return []
|
|
395
|
+
return enabledPluginIdsFromText(readFileSync(configFile, "utf8"))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function enabledPluginIdsFromText(configText: string): Array<string> {
|
|
393
399
|
const ids: Array<string> = []
|
|
394
400
|
let plugin = ""
|
|
395
401
|
let enabled = false
|
|
396
402
|
const flush = (): void => {
|
|
397
403
|
if (plugin !== "" && enabled) ids.push(plugin)
|
|
398
404
|
}
|
|
399
|
-
for (const line of
|
|
405
|
+
for (const line of configText.split("\n")) {
|
|
400
406
|
const m = /^\[plugins\."([^"]+)"\][ \t]*$/.exec(line)
|
|
401
407
|
if (m !== null) {
|
|
402
408
|
flush()
|
|
@@ -418,12 +424,12 @@ export function enabledPluginIds(configFile: string): Array<string> {
|
|
|
418
424
|
return ids
|
|
419
425
|
}
|
|
420
426
|
|
|
421
|
-
function manualPluginRefreshCommand(
|
|
422
|
-
const first =
|
|
427
|
+
function manualPluginRefreshCommand(sotConfigText: string): string {
|
|
428
|
+
const first = enabledPluginIdsFromText(sotConfigText)[0]
|
|
423
429
|
return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
|
|
424
430
|
}
|
|
425
431
|
|
|
426
|
-
function syncPlugins(ctx: Ctx,
|
|
432
|
+
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
427
433
|
const { change, echo, warn } = ctx.services.logger
|
|
428
434
|
if (ctx.dryRun) {
|
|
429
435
|
echo("[dry-run] add enabled Codex plugins from SoT")
|
|
@@ -432,7 +438,7 @@ function syncPlugins(ctx: Ctx, sotConfig: string): void {
|
|
|
432
438
|
|
|
433
439
|
if (ctx.services.deps.probe("codex").state === "missing") {
|
|
434
440
|
warn(
|
|
435
|
-
`codex CLI not in PATH - deployed config/marketplace only; install Codex with: ${standaloneInstallCommand(ctx)} | docs: https://developers.openai.com/codex/cli; then run: ${manualPluginRefreshCommand(
|
|
441
|
+
`codex CLI not in PATH - deployed config/marketplace only; install Codex with: ${standaloneInstallCommand(ctx)} | docs: https://developers.openai.com/codex/cli; then run: ${manualPluginRefreshCommand(sotConfigText)}`
|
|
436
442
|
)
|
|
437
443
|
return
|
|
438
444
|
}
|
|
@@ -447,7 +453,7 @@ function syncPlugins(ctx: Ctx, sotConfig: string): void {
|
|
|
447
453
|
|
|
448
454
|
let refreshed = 0
|
|
449
455
|
let failed = 0
|
|
450
|
-
for (const pluginId of
|
|
456
|
+
for (const pluginId of enabledPluginIdsFromText(sotConfigText)) {
|
|
451
457
|
const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
452
458
|
const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
|
|
453
459
|
if (res.error === undefined && res.status === 0) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* substitution: stdout with trailing newlines stripped, empty on failure.
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from "node:child_process"
|
|
7
|
-
import { accessSync, chmodSync, constants,
|
|
7
|
+
import { accessSync, chmodSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
8
8
|
import { delimiter, isAbsolute, join } from "node:path"
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -69,34 +69,19 @@ export function ensureExecutable(path: string): boolean {
|
|
|
69
69
|
return true
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function
|
|
72
|
+
export function writeTextIfChanged(path: string, content: string): boolean {
|
|
73
73
|
if (existsSync(path) && readFileSync(path, "utf8") === content) return false
|
|
74
74
|
writeFileSync(path, content)
|
|
75
75
|
return true
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (existsSync(
|
|
81
|
-
|
|
78
|
+
export function writeBytesIfChanged(path: string, content: Uint8Array): boolean {
|
|
79
|
+
const bytes = Buffer.from(content)
|
|
80
|
+
if (existsSync(path) && readFileSync(path).equals(bytes)) return false
|
|
81
|
+
writeFileSync(path, bytes)
|
|
82
82
|
return true
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let changed = false
|
|
88
|
-
for (const e of readdirSync(srcDir, { withFileTypes: true })) {
|
|
89
|
-
const src = p(srcDir, e.name)
|
|
90
|
-
const dest = p(destDir, e.name)
|
|
91
|
-
if (e.isDirectory()) {
|
|
92
|
-
if (!existsSync(dest)) {
|
|
93
|
-
mkdirSync(dest, { recursive: true })
|
|
94
|
-
changed = true
|
|
95
|
-
}
|
|
96
|
-
if (copyTreeIfChanged(src, dest)) changed = true
|
|
97
|
-
} else if (copyFileIfChanged(src, dest)) {
|
|
98
|
-
changed = true
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return changed
|
|
85
|
+
export function writeFileIfChanged(path: string, content: string): boolean {
|
|
86
|
+
return writeTextIfChanged(path, content)
|
|
102
87
|
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* vocabulary directly.
|
|
7
7
|
*/
|
|
8
8
|
import { p } from "./exec"
|
|
9
|
-
import { existsSync } from "node:fs"
|
|
10
9
|
import { homedir } from "node:os"
|
|
11
10
|
|
|
12
11
|
import { kitHome } from "../kitHome"
|
|
@@ -81,13 +80,13 @@ function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
81
80
|
preflight(ctx)
|
|
82
81
|
validateModelFlags(ctx)
|
|
83
82
|
|
|
84
|
-
const claudeRan = ctx.syncClaude
|
|
83
|
+
const claudeRan = ctx.syncClaude
|
|
85
84
|
if (claudeRan) claudeSync(ctx)
|
|
86
85
|
|
|
87
|
-
const codexRan = ctx.syncCodex
|
|
86
|
+
const codexRan = ctx.syncCodex
|
|
88
87
|
if (codexRan) codexSync(ctx)
|
|
89
88
|
|
|
90
|
-
const skillsState = ctx.syncAgents
|
|
89
|
+
const skillsState = ctx.syncAgents ? skillsSync(ctx) : undefined
|
|
91
90
|
|
|
92
91
|
echo("")
|
|
93
92
|
echo("--- Sync complete ---")
|