docks-kit 0.15.5 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +106 -36
- package/README.md +21 -14
- package/cli/docs/sync-layers.md +32 -2
- package/cli/src/commands/harnesses.ts +64 -0
- package/cli/src/commands/sync.ts +2 -2
- package/cli/src/engine-native/claudeRetired.ts +165 -0
- package/cli/src/engine-native/claudeSync.ts +11 -4
- package/cli/src/engine-native/codexSync.ts +69 -0
- package/cli/src/engine-native/deps.ts +7 -0
- package/cli/src/engine-native/harnesses.ts +75 -0
- package/cli/src/engine-native/index.ts +31 -4
- package/cli/src/engine-native/ompPaths.ts +101 -0
- package/cli/src/engine-native/ompSync.ts +421 -0
- package/cli/src/engine-native/ompYaml.ts +107 -0
- package/cli/src/engine-native/parseArgs.ts +36 -11
- package/cli/src/engine-native/toolchain.ts +3 -1
- package/cli/src/generated/sotPayload.ts +14 -6
- package/cli/src/main.ts +4 -1
- package/package.json +3 -2
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
syncClaudeModel
|
|
23
23
|
} from "./claudeSettingsModifiers"
|
|
24
24
|
import { claudeRuntimePaths, materializeClaudeSettings, type ClaudeRuntimePaths } from "./claudeRuntime"
|
|
25
|
+
import { RETIRED_PERMISSION_RULES } from "./claudeRetired"
|
|
25
26
|
import { p, spawnProcess, writeBytesIfChanged, writeTextIfChanged } from "./exec"
|
|
26
27
|
import type { Ctx } from "./index"
|
|
27
28
|
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
@@ -398,8 +399,14 @@ const REMOVED_MANIFEST = {
|
|
|
398
399
|
"hooks.PreToolUse"
|
|
399
400
|
],
|
|
400
401
|
permissionRules: {
|
|
401
|
-
allow: ["Write(./)", "Bash(rtk *)"],
|
|
402
|
-
deny: [
|
|
402
|
+
allow: ["Write(./)", "Bash(rtk *)", ...RETIRED_PERMISSION_RULES.allow],
|
|
403
|
+
deny: [
|
|
404
|
+
"Write(**/.env)",
|
|
405
|
+
"Write(**/.env.local)",
|
|
406
|
+
"Write(**/secrets/**)",
|
|
407
|
+
...RETIRED_PERMISSION_RULES.deny
|
|
408
|
+
],
|
|
409
|
+
ask: [...RETIRED_PERMISSION_RULES.ask]
|
|
403
410
|
},
|
|
404
411
|
claudeJsonKeys: [] as Array<string>,
|
|
405
412
|
/** Home-relative artifacts the kit installed outside ~/.claude. */
|
|
@@ -447,14 +454,14 @@ function pruneJsonKeys(ctx: Ctx, file: string, keys: Array<string>): number {
|
|
|
447
454
|
function prunePermissionRules(
|
|
448
455
|
ctx: Ctx,
|
|
449
456
|
file: string,
|
|
450
|
-
rules: Readonly<Record<"allow" | "deny", ReadonlyArray<string>>>
|
|
457
|
+
rules: Readonly<Record<"allow" | "deny" | "ask", ReadonlyArray<string>>>
|
|
451
458
|
): number {
|
|
452
459
|
if (!existsSync(file)) return 0
|
|
453
460
|
const doc = parseJson(readFileSync(file, "utf8"))
|
|
454
461
|
if (doc === undefined || !isObject(doc) || !isObject(doc["permissions"])) return 0
|
|
455
462
|
const permissions = doc["permissions"]
|
|
456
463
|
let present = 0
|
|
457
|
-
for (const key of ["allow", "deny"] as const) {
|
|
464
|
+
for (const key of ["allow", "deny", "ask"] as const) {
|
|
458
465
|
const values = permissions[key]
|
|
459
466
|
if (!Array.isArray(values)) continue
|
|
460
467
|
const removed = new Set(rules[key])
|
|
@@ -20,6 +20,7 @@ export async function codexSync(ctx: Ctx): Promise<void> {
|
|
|
20
20
|
await ensureBubblewrap(ctx)
|
|
21
21
|
if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
|
|
22
22
|
syncConfig(ctx, sotConfig, userConfig)
|
|
23
|
+
removeRetiredImportedHooks(ctx, codexDir)
|
|
23
24
|
syncCodexModel(ctx, ctx.codexModel)
|
|
24
25
|
syncCodexEffort(ctx, ctx.codexEffort)
|
|
25
26
|
syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
|
|
@@ -29,6 +30,74 @@ export async function codexSync(ctx: Ctx): Promise<void> {
|
|
|
29
30
|
await syncPlugins(ctx, sotConfig)
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
const RETIRED_CONTEXT_HOOK = `echo "[CONTEXT] Current date: $(date '+%A, %Y-%m-%d %H:%M:%S %Z')"`
|
|
34
|
+
const RETIRED_SKILLS_HOOK = `SKILL_COUNT=$(find .claude/skills -name 'SKILL.md' -mindepth 2 -maxdepth 2 2>/dev/null | wc -l); [ "$SKILL_COUNT" -gt 0 ] && echo "[SKILLS] $SKILL_COUNT project skills available in .claude/skills/. Claude Code loads them on demand via Skill tool. After code changes affecting documented patterns, update the relevant skill and its metadata.updated field." || true`
|
|
35
|
+
const RETIRED_CONFIG_HOOKS = new Set([
|
|
36
|
+
`echo "[CONFIG] Context: $([ \\\"\${CLAUDE_CODE_DISABLE_1M_CONTEXT:-0}\\\" = \\\"1\\\" ] && echo '200K' || echo '1M') | Compact-window: \${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-full} | Effort: \${CLAUDE_CODE_EFFORT_LEVEL:-high} | Thinking: adaptive | Subagent: \${CLAUDE_CODE_SUBAGENT_MODEL:-default}"`,
|
|
37
|
+
`echo "[CONFIG] Context: $([ \\\"\${CLAUDE_CODE_DISABLE_1M_CONTEXT:-0}\\\" = \\\"1\\\" ] && echo '200K' || echo '1M') | Compact-window: \${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-full} | Effort: \${CLAUDE_CODE_EFFORT_LEVEL:-high} | Thinking: adaptive | Model: \${ANTHROPIC_DEFAULT_OPUS_MODEL:-default} | Subagent: \${CLAUDE_CODE_SUBAGENT_MODEL:-default}"`,
|
|
38
|
+
`echo "[CONFIG] Context: $([ \\\"\${CLAUDE_CODE_DISABLE_1M_CONTEXT:-0}\\\" = \\\"1\\\" ] && echo '200K' || echo '1M') | Compact-window: \${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-full} | Effort: \${CLAUDE_CODE_EFFORT_LEVEL:-$(jq -r .effortLevel $HOME/.claude/settings.json 2>/dev/null || echo default)} | Thinking: adaptive | Subagent: \${CLAUDE_CODE_SUBAGENT_MODEL:-default}"`
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
function isRetiredImportedSessionStartHook(handler: Json, codexDir: string): boolean {
|
|
42
|
+
if (!isObject(handler) || handler["type"] !== "command" || typeof handler["command"] !== "string") return false
|
|
43
|
+
const command = handler["command"]
|
|
44
|
+
const connectorScript = p(codexDir, "hooks", "disable-claudeai-connectors.sh")
|
|
45
|
+
const retiredConnectorHook = `[ -x '${connectorScript}' ] && '${connectorScript}' || true`
|
|
46
|
+
return command === RETIRED_CONTEXT_HOOK || command === RETIRED_SKILLS_HOOK || RETIRED_CONFIG_HOOKS.has(command) || command === retiredConnectorHook
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function withoutRetiredImportedHooks(doc: { [key: string]: Json }, codexDir: string): { readonly value: Json; readonly removed: number } {
|
|
50
|
+
const hooks = doc["hooks"]
|
|
51
|
+
if (!isObject(hooks) || !Array.isArray(hooks["SessionStart"])) return { value: doc, removed: 0 }
|
|
52
|
+
|
|
53
|
+
let removed = 0
|
|
54
|
+
const groups: Array<Json> = []
|
|
55
|
+
for (const group of hooks["SessionStart"]) {
|
|
56
|
+
if (!isObject(group) || !Array.isArray(group["hooks"])) {
|
|
57
|
+
groups.push(group)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
const kept = group["hooks"].filter((handler) => {
|
|
61
|
+
if (!isRetiredImportedSessionStartHook(handler, codexDir)) return true
|
|
62
|
+
removed++
|
|
63
|
+
return false
|
|
64
|
+
})
|
|
65
|
+
if (kept.length > 0) groups.push({ ...group, hooks: kept })
|
|
66
|
+
}
|
|
67
|
+
if (removed === 0) return { value: doc, removed: 0 }
|
|
68
|
+
|
|
69
|
+
const nextHooks = { ...hooks }
|
|
70
|
+
if (groups.length > 0) nextHooks["SessionStart"] = groups
|
|
71
|
+
else delete nextHooks["SessionStart"]
|
|
72
|
+
return { value: { ...doc, hooks: nextHooks }, removed }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Remove only Claude hooks that docks-kit retired before Codex could import them safely. */
|
|
76
|
+
function removeRetiredImportedHooks(ctx: Ctx, codexDir: string): void {
|
|
77
|
+
const { change, echo, warn } = ctx.services.logger
|
|
78
|
+
const hooksFile = p(codexDir, "hooks.json")
|
|
79
|
+
if (!existsSync(hooksFile)) return
|
|
80
|
+
const before = readFileSync(hooksFile, "utf8")
|
|
81
|
+
const doc = parseJson(before)
|
|
82
|
+
if (doc === undefined || !isObject(doc)) {
|
|
83
|
+
warn(`Codex hooks file is not a valid JSON object; retired hook cleanup skipped: ${hooksFile}`)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const result = withoutRetiredImportedHooks(doc, codexDir)
|
|
87
|
+
if (result.removed === 0) return
|
|
88
|
+
|
|
89
|
+
if (ctx.dryRun) {
|
|
90
|
+
echo(`[dry-run] remove ${result.removed} retired imported docks-kit SessionStart hook(s) from ${hooksFile}`)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
copyFileSync(hooksFile, `${hooksFile}.bak`)
|
|
95
|
+
writeFileSync(`${hooksFile}.tmp`, jqStringify(result.value))
|
|
96
|
+
renameSync(`${hooksFile}.tmp`, hooksFile)
|
|
97
|
+
change(`Codex: removed ${result.removed} retired imported docks-kit SessionStart hook(s) (backup at hooks.json.bak)`)
|
|
98
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
99
|
+
}
|
|
100
|
+
|
|
32
101
|
// ---------------------------------------------------------- bubblewrap ----
|
|
33
102
|
|
|
34
103
|
async function ensureBubblewrap(ctx: Ctx): Promise<void> {
|
|
@@ -23,6 +23,7 @@ export type ToolId =
|
|
|
23
23
|
| "npx"
|
|
24
24
|
| "claude"
|
|
25
25
|
| "codex"
|
|
26
|
+
| "omp"
|
|
26
27
|
| "bun"
|
|
27
28
|
| "bwrap"
|
|
28
29
|
| "ffplay"
|
|
@@ -181,6 +182,12 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
181
182
|
(pf = rawPlatform()) => hostOs(platformName(pf)).installHint("codex"),
|
|
182
183
|
{ version: versionProbe("codex") }
|
|
183
184
|
),
|
|
185
|
+
omp: spec(
|
|
186
|
+
"omp",
|
|
187
|
+
"optional",
|
|
188
|
+
() => "install omp from https://github.com/can1357/oh-my-pi (an existing install self-updates with `omp update`)",
|
|
189
|
+
{ version: versionProbe("omp", ["--version"], (out) => out.trim().replace(/^omp[/v]/, "")) }
|
|
190
|
+
),
|
|
184
191
|
bun: spec(
|
|
185
192
|
"bun",
|
|
186
193
|
"optional",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-machine harness selection at ~/.docks-kit/state.json. The selection keeps
|
|
3
|
+
* the omp harness opt-in. A missing or unreadable state file is represented by
|
|
4
|
+
* undefined so callers resolve it to LEGACY_SELECTION and existing machines
|
|
5
|
+
* keep today's behavior.
|
|
6
|
+
*/
|
|
7
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
8
|
+
import { homedir } from "node:os"
|
|
9
|
+
|
|
10
|
+
import { p } from "./exec"
|
|
11
|
+
|
|
12
|
+
export type Harness = "claude" | "codex" | "agents" | "omp"
|
|
13
|
+
|
|
14
|
+
export const HARNESSES: ReadonlyArray<Harness> = ["claude", "codex", "agents", "omp"]
|
|
15
|
+
export const LEGACY_SELECTION: ReadonlyArray<Harness> = ["claude", "codex", "agents"]
|
|
16
|
+
|
|
17
|
+
function isHarness(value: unknown): value is Harness {
|
|
18
|
+
return value === "claude" || value === "codex" || value === "agents" || value === "omp"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeHarnesses(selection: ReadonlyArray<unknown>): Array<Harness> {
|
|
22
|
+
const selected = new Set<Harness>()
|
|
23
|
+
for (const value of selection) {
|
|
24
|
+
if (isHarness(value)) selected.add(value)
|
|
25
|
+
}
|
|
26
|
+
return HARNESSES.filter((harness) => selected.has(harness))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve the engine home root from HOME with the platform home as fallback. */
|
|
30
|
+
export function engineHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
31
|
+
const home = env["HOME"]
|
|
32
|
+
return home !== undefined && home !== "" ? home : homedir()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function harnessStateFile(home: string): string {
|
|
36
|
+
return p(home, ".docks-kit", "state.json")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read valid local state without allowing corruption to make sync unusable. */
|
|
40
|
+
export function readHarnessSelection(home: string): ReadonlyArray<Harness> | undefined {
|
|
41
|
+
let parsed: unknown
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(readFileSync(harnessStateFile(home), "utf8")) as unknown
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined
|
|
49
|
+
const state = parsed as Record<string, unknown>
|
|
50
|
+
if (state["version"] !== 1 || !Array.isArray(state["harnesses"])) return undefined
|
|
51
|
+
|
|
52
|
+
const selection = normalizeHarnesses(state["harnesses"])
|
|
53
|
+
return selection.length > 0 ? selection : undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function writeHarnessSelection(home: string, selection: ReadonlyArray<Harness>): void {
|
|
57
|
+
if (selection.length === 0) {
|
|
58
|
+
throw new Error("Cannot write an empty harness selection because sync would become a no-op")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const harnesses = normalizeHarnesses(selection)
|
|
62
|
+
if (harnesses.length === 0) {
|
|
63
|
+
throw new Error("Harness selection must contain at least one known harness name")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const directory = p(home, ".docks-kit")
|
|
67
|
+
const file = harnessStateFile(home)
|
|
68
|
+
const text = `${JSON.stringify({ version: 1, harnesses }, null, 2)}\n`
|
|
69
|
+
// `mode` applies only when mkdir creates the path, so an existing permissive
|
|
70
|
+
// ~/.docks-kit would keep its mode.
|
|
71
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
72
|
+
chmodSync(directory, 0o700)
|
|
73
|
+
writeFileSync(file, text, { mode: 0o600 })
|
|
74
|
+
chmodSync(file, 0o600)
|
|
75
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* vocabulary directly.
|
|
7
7
|
*/
|
|
8
8
|
import { p } from "./exec"
|
|
9
|
-
import {
|
|
9
|
+
import { engineHome } from "./harnesses"
|
|
10
10
|
|
|
11
11
|
import { kitHome } from "../kitHome"
|
|
12
12
|
import { payloadText } from "../payload"
|
|
@@ -15,6 +15,7 @@ import type { TerminalLease } from "./logger"
|
|
|
15
15
|
import type { BunRuntimeState } from "./bun"
|
|
16
16
|
import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync"
|
|
17
17
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
18
|
+
import { ompNextSteps, ompSummary, ompSync, type OmpState } from "./ompSync"
|
|
18
19
|
import { normalizeManifest, skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
|
|
19
20
|
import { modeModel, modeToolchain } from "./modes"
|
|
20
21
|
import { ExitError, parseArgs, parseClaudePlugin, parseCompactWindow, validateModifierFlags } from "./parseArgs"
|
|
@@ -81,6 +82,7 @@ export interface Ctx {
|
|
|
81
82
|
readonly repoDir: string
|
|
82
83
|
readonly home: string
|
|
83
84
|
readonly agentsDir: string
|
|
85
|
+
readonly interactive: boolean
|
|
84
86
|
dryRun: boolean
|
|
85
87
|
verbose: boolean
|
|
86
88
|
skipBubblewrap: boolean
|
|
@@ -106,19 +108,21 @@ export interface Ctx {
|
|
|
106
108
|
syncClaude: boolean
|
|
107
109
|
syncCodex: boolean
|
|
108
110
|
syncAgents: boolean
|
|
111
|
+
syncOmp: boolean
|
|
109
112
|
/** Per-run next-step triggers (Output Policy): advice prints only when its trigger changed or --verbose. */
|
|
110
113
|
readonly nextStepTriggers: {
|
|
111
114
|
claudePlugins: boolean
|
|
112
115
|
claudeRestart: boolean
|
|
113
116
|
codexRestart: boolean
|
|
114
117
|
skillsRestart: boolean
|
|
118
|
+
ompRestart: boolean
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
/** Globals default from env using the historical ${VAR:-default} contract. */
|
|
119
123
|
function makeCtx(services: EngineServices): Ctx {
|
|
120
124
|
const env = process.env
|
|
121
|
-
const home = env
|
|
125
|
+
const home = engineHome(env)
|
|
122
126
|
const compactWindowSource = env["CLAUDE_COMPACT_WINDOW"] ?? ""
|
|
123
127
|
const claudeCompactWindow = compactWindowSource === "" ? "" : parseCompactWindow(compactWindowSource)
|
|
124
128
|
if (claudeCompactWindow === undefined) {
|
|
@@ -133,6 +137,11 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
133
137
|
repoDir: kitHome(),
|
|
134
138
|
home,
|
|
135
139
|
agentsDir: env["AGENTS_DIR"] !== undefined && env["AGENTS_DIR"] !== "" ? env["AGENTS_DIR"] : p(home, ".agents"),
|
|
140
|
+
interactive:
|
|
141
|
+
env["DOCKS_KIT_INTERACTIVE"] === "1" ||
|
|
142
|
+
(env["DOCKS_KIT_INTERACTIVE"] !== "0" &&
|
|
143
|
+
process.stdout.isTTY === true &&
|
|
144
|
+
process.stdin.isTTY === true),
|
|
136
145
|
dryRun: env["DRY_RUN"] === "1",
|
|
137
146
|
verbose: env["DOCKS_KIT_VERBOSE"] === "1",
|
|
138
147
|
skipBubblewrap: env["SKIP_BUBBLEWRAP"] === "1",
|
|
@@ -154,7 +163,14 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
154
163
|
syncClaude: false,
|
|
155
164
|
syncCodex: false,
|
|
156
165
|
syncAgents: false,
|
|
157
|
-
|
|
166
|
+
syncOmp: false,
|
|
167
|
+
nextStepTriggers: {
|
|
168
|
+
claudePlugins: false,
|
|
169
|
+
claudeRestart: false,
|
|
170
|
+
codexRestart: false,
|
|
171
|
+
skillsRestart: false,
|
|
172
|
+
ompRestart: false
|
|
173
|
+
}
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
176
|
|
|
@@ -181,6 +197,7 @@ async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number
|
|
|
181
197
|
| { readonly kind: "claude"; readonly runtime: ClaudeRuntimeState }
|
|
182
198
|
| { readonly kind: "codex" }
|
|
183
199
|
| { readonly kind: "skills"; readonly state: SkillsState }
|
|
200
|
+
| { readonly kind: "omp"; readonly state: OmpState }
|
|
184
201
|
interface SelectedPipeline {
|
|
185
202
|
readonly name: string
|
|
186
203
|
readonly run: SyncTask<PipelineResult>
|
|
@@ -208,6 +225,12 @@ async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number
|
|
|
208
225
|
run: async () => ({ kind: "skills", state: await skillsSync(ctx) })
|
|
209
226
|
})
|
|
210
227
|
}
|
|
228
|
+
if (ctx.syncOmp) {
|
|
229
|
+
selected.push({
|
|
230
|
+
name: "omp",
|
|
231
|
+
run: async () => ({ kind: "omp", state: await ompSync(ctx) })
|
|
232
|
+
})
|
|
233
|
+
}
|
|
211
234
|
|
|
212
235
|
// A populated skills manifest deploys with `-a claude-code codex`, and symlink healing also writes into Claude's tree.
|
|
213
236
|
ctx.syncConcurrency = syncConcurrencyForManifest(
|
|
@@ -243,9 +266,11 @@ async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number
|
|
|
243
266
|
const codexRan = ctx.syncCodex
|
|
244
267
|
let claudeRuntime: ClaudeRuntimeState | undefined
|
|
245
268
|
let skillsState: SkillsState | undefined
|
|
269
|
+
let ompState: OmpState | undefined
|
|
246
270
|
for (const result of results) {
|
|
247
271
|
if (result.kind === "claude") claudeRuntime = result.runtime
|
|
248
272
|
else if (result.kind === "skills") skillsState = result.state
|
|
273
|
+
else if (result.kind === "omp") ompState = result.state
|
|
249
274
|
}
|
|
250
275
|
|
|
251
276
|
echo("")
|
|
@@ -254,11 +279,13 @@ async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number
|
|
|
254
279
|
if (claudeRuntime !== undefined) claudeSummary(ctx, claudeRuntime)
|
|
255
280
|
if (codexRan) codexSummary(ctx)
|
|
256
281
|
if (skillsState !== undefined) skillsSummary(ctx, skillsState)
|
|
282
|
+
if (ompState !== undefined) ompSummary(ctx, ompState)
|
|
257
283
|
|
|
258
284
|
const advice = [
|
|
259
285
|
...(claudeRan ? claudeNextSteps(ctx) : []),
|
|
260
286
|
...(codexRan ? codexNextSteps(ctx) : []),
|
|
261
|
-
...(skillsState !== undefined ? skillsNextSteps(ctx) : [])
|
|
287
|
+
...(skillsState !== undefined ? skillsNextSteps(ctx) : []),
|
|
288
|
+
...(ompState !== undefined ? ompNextSteps(ctx) : [])
|
|
262
289
|
]
|
|
263
290
|
if (advice.length > 0) {
|
|
264
291
|
echo("")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* omp path resolution, mirroring upstream `packages/utils/src/dirs.ts`
|
|
3
|
+
* `DirResolver`. The kit deploys into two different roots and must not guess
|
|
4
|
+
* either one: the agent directory holds `AGENTS.md`, `config.yml`, and
|
|
5
|
+
* `mcp.json`; the data root holds `marketplaces.json` and `plugins/`.
|
|
6
|
+
*
|
|
7
|
+
* Resolution reads the environment and probes directory existence only, so a
|
|
8
|
+
* dry run stays free of omp subcommands.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs"
|
|
11
|
+
import { isAbsolute, resolve } from "node:path"
|
|
12
|
+
|
|
13
|
+
import { p } from "./exec"
|
|
14
|
+
|
|
15
|
+
/** Upstream CONFIG_DIR_NAME; PI_CONFIG_DIR renames this directory under home. */
|
|
16
|
+
const DEFAULT_CONFIG_DIR_NAME = ".omp"
|
|
17
|
+
/** Upstream APP_NAME, the fixed segment under an XDG category root. */
|
|
18
|
+
const APP_NAME = "omp"
|
|
19
|
+
/** Upstream PROFILE_NAME_RE. An invalid name degrades to the default profile. */
|
|
20
|
+
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/
|
|
21
|
+
/** Windows device aliases upstream rejects, bare or with any extension. */
|
|
22
|
+
const WINDOWS_RESERVED_BASENAME_RE = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i
|
|
23
|
+
|
|
24
|
+
export interface OmpPaths {
|
|
25
|
+
/** undefined for the default profile, else the normalized profile name */
|
|
26
|
+
readonly profile: string | undefined
|
|
27
|
+
/** config root for the active profile */
|
|
28
|
+
readonly configRoot: string
|
|
29
|
+
/** directory holding AGENTS.md, config.yml, mcp.json - never XDG-redirected */
|
|
30
|
+
readonly agentDir: string
|
|
31
|
+
/** root holding marketplaces.json and plugins/ - XDG-redirected when adopted */
|
|
32
|
+
readonly dataRoot: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OmpPathInputs {
|
|
36
|
+
readonly home: string
|
|
37
|
+
readonly env: Record<string, string | undefined>
|
|
38
|
+
readonly platform: NodeJS.Platform
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Upstream `normalizeProfileName`, minus the throw: an invalid value reaches
|
|
43
|
+
* `readProfileFromEnvSafe`, which degrades to the default profile so a bad env
|
|
44
|
+
* var cannot crash a bare import.
|
|
45
|
+
*/
|
|
46
|
+
function normalizeProfile(value: string | undefined): string | undefined {
|
|
47
|
+
const name = value?.trim()
|
|
48
|
+
if (name === undefined || name === "" || name === "default") return undefined
|
|
49
|
+
if (name === "." || name === ".." || name.endsWith(".")) return undefined
|
|
50
|
+
if (!PROFILE_NAME_RE.test(name) || WINDOWS_RESERVED_BASENAME_RE.test(name)) return undefined
|
|
51
|
+
return name
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Upstream applies `path.resolve` to PI_CODING_AGENT_DIR, so cwd anchors a relative value. */
|
|
55
|
+
function resolveAgentOverride(value: string): string {
|
|
56
|
+
return isAbsolute(value) ? value : resolve(process.cwd(), value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ompPaths({ home, env, platform }: OmpPathInputs): OmpPaths {
|
|
60
|
+
// OMP_PROFILE wins whenever it is defined, including when explicitly empty;
|
|
61
|
+
// PI_PROFILE is only the legacy fallback.
|
|
62
|
+
const profile = normalizeProfile(env["OMP_PROFILE"] !== undefined ? env["OMP_PROFILE"] : env["PI_PROFILE"])
|
|
63
|
+
|
|
64
|
+
// PI_CONFIG_DIR is a config root dirname joined under home, not a path.
|
|
65
|
+
const configDirName = env["PI_CONFIG_DIR"]
|
|
66
|
+
const baseRoot = p(home, configDirName !== undefined && configDirName !== "" ? configDirName : DEFAULT_CONFIG_DIR_NAME)
|
|
67
|
+
const configRoot = profile === undefined ? baseRoot : p(baseRoot, "profiles", profile)
|
|
68
|
+
|
|
69
|
+
// A named profile derives its own agent directory and ignores the override.
|
|
70
|
+
const agentOverride = env["PI_CODING_AGENT_DIR"]
|
|
71
|
+
const defaultAgentDir = p(configRoot, "agent")
|
|
72
|
+
const agentDir = profile === undefined && agentOverride !== undefined && agentOverride !== ""
|
|
73
|
+
? resolveAgentOverride(agentOverride)
|
|
74
|
+
: defaultAgentDir
|
|
75
|
+
|
|
76
|
+
return { profile, configRoot, agentDir, dataRoot: xdgDataRoot(env, platform, profile, agentDir === defaultAgentDir) ?? configRoot }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* XDG is a Linux and macOS convention upstream disables whenever an agent-dir
|
|
81
|
+
* override is active, and adopts a category only once its omp directory
|
|
82
|
+
* exists - `omp config init-xdg` creates it without moving existing data. A
|
|
83
|
+
* named profile keys on its own `profiles/<name>` path and adopts that path,
|
|
84
|
+
* so a profile stays where it was first activated.
|
|
85
|
+
*/
|
|
86
|
+
function xdgDataRoot(
|
|
87
|
+
env: Record<string, string | undefined>,
|
|
88
|
+
platform: NodeJS.Platform,
|
|
89
|
+
profile: string | undefined,
|
|
90
|
+
agentDirIsDefault: boolean
|
|
91
|
+
): string | undefined {
|
|
92
|
+
if (!agentDirIsDefault) return undefined
|
|
93
|
+
if (platform !== "linux" && platform !== "darwin") return undefined
|
|
94
|
+
|
|
95
|
+
const dataHome = env["XDG_DATA_HOME"]
|
|
96
|
+
if (dataHome === undefined || dataHome === "") return undefined
|
|
97
|
+
|
|
98
|
+
const appRoot = p(dataHome, APP_NAME)
|
|
99
|
+
const candidate = profile === undefined ? appRoot : p(appRoot, "profiles", profile)
|
|
100
|
+
return existsSync(candidate) ? candidate : undefined
|
|
101
|
+
}
|