docks-kit 0.17.1 → 0.17.3
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 +41 -33
- package/cli/docs/omp-context.md +104 -0
- package/cli/docs/omp-models.md +106 -56
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +62 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +132 -112
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +67 -64
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompRemovals.ts +159 -0
- package/cli/src/engine-native/ompSync.ts +69 -409
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/docks-kit +13 -2
- package/docks-kit.ps1 +21 -6
- package/package.json +10 -2
- package/cli/src/engine-native/claudePlugins.ts +0 -510
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative `sync omp` retired-key pruning. `ompYaml.ts mergeOmpConfig` is
|
|
3
|
+
* additive: its `mergeMappings, deployed-key retention loop` returns every
|
|
4
|
+
* deployed key absent from the SoT to the merged result. Removing a key from
|
|
5
|
+
* `SoT/.omp/config.yml` therefore never removes it from an existing
|
|
6
|
+
* `~/.omp/agent/config.yml`. This pass force-prunes an inventory of retired
|
|
7
|
+
* kit-owned keys on every sync, without `--reconcile`. Message strings and
|
|
8
|
+
* prune semantics are part of the contract.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { isMap, isScalar, parseDocument, type YAMLMap } from "yaml";
|
|
12
|
+
import type { Ctx } from "./index";
|
|
13
|
+
|
|
14
|
+
type RetiredScalar = string | number | boolean;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Keys the kit used to deploy, each paired with the exact value it deployed. A
|
|
18
|
+
* key is pruned only while the deployed value still equals that value, so a
|
|
19
|
+
* user who changed the setting keeps it. Every recorded value also equals omp's
|
|
20
|
+
* current default, so pruning changes no behavior today and lets a later
|
|
21
|
+
* upstream default change take effect.
|
|
22
|
+
*/
|
|
23
|
+
const OMP_RETIRED_VALUES: ReadonlyArray<readonly [string, RetiredScalar]> = [
|
|
24
|
+
["symbolPreset", "unicode"],
|
|
25
|
+
["theme.dark", "titanium"],
|
|
26
|
+
["statusLine.preset", "default"],
|
|
27
|
+
["statusLine.showHookStatus", true],
|
|
28
|
+
["statusLine.sessionAccent", true],
|
|
29
|
+
["statusLine.transparent", false],
|
|
30
|
+
["terminal.showProgress", false],
|
|
31
|
+
["tui.textSizing", false],
|
|
32
|
+
["tui.tight", false],
|
|
33
|
+
["display.shimmer", "classic"],
|
|
34
|
+
["omitThinking", false],
|
|
35
|
+
["hideThinkingBlock", false],
|
|
36
|
+
["autocompleteMaxVisible", 10],
|
|
37
|
+
["emojiAutocomplete", true],
|
|
38
|
+
["readLineNumbers", false],
|
|
39
|
+
["interruptMode", "immediate"],
|
|
40
|
+
["autoResume", false],
|
|
41
|
+
["startup.changelogMode", "summary"],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Keys retired outright and pruned at any value, because the key itself no
|
|
46
|
+
* longer means anything. omp migrates `providers.webSearchOrder` in memory into
|
|
47
|
+
* `modelRoles.web` plus `retry.fallbackChains.web` and then drops it, but never
|
|
48
|
+
* writes that expansion back to the file.
|
|
49
|
+
*/
|
|
50
|
+
const OMP_RETIRED_KEYS: ReadonlyArray<string> = ["providers.webSearchOrder"];
|
|
51
|
+
|
|
52
|
+
function indexOfKey(mapping: YAMLMap, key: string): number {
|
|
53
|
+
return mapping.items.findIndex((pair) => {
|
|
54
|
+
const name: unknown = pair.key;
|
|
55
|
+
if (typeof name === "string") return name === key;
|
|
56
|
+
return isScalar(name) && name.value === key;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Deletes `segments` when `matches` accepts the leaf value, then removes every
|
|
62
|
+
* ancestor mapping the deletion left empty. An ancestor holding another key
|
|
63
|
+
* survives, so `statusLine` keeps `compactThinkingLevel` while `tui` disappears
|
|
64
|
+
* once both of its keys are gone.
|
|
65
|
+
*/
|
|
66
|
+
function deletePath(
|
|
67
|
+
root: YAMLMap,
|
|
68
|
+
segments: ReadonlyArray<string>,
|
|
69
|
+
matches: (value: unknown) => boolean,
|
|
70
|
+
): boolean {
|
|
71
|
+
const owners: Array<YAMLMap> = [root];
|
|
72
|
+
let cursor: YAMLMap = root;
|
|
73
|
+
for (const segment of segments.slice(0, -1)) {
|
|
74
|
+
const index = indexOfKey(cursor, segment);
|
|
75
|
+
const pair = index < 0 ? undefined : cursor.items[index];
|
|
76
|
+
if (pair === undefined || !isMap(pair.value)) return false;
|
|
77
|
+
cursor = pair.value;
|
|
78
|
+
owners.push(cursor);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const leaf = segments[segments.length - 1];
|
|
82
|
+
if (leaf === undefined) return false;
|
|
83
|
+
const leafIndex = indexOfKey(cursor, leaf);
|
|
84
|
+
if (leafIndex < 0) return false;
|
|
85
|
+
const leafPair = cursor.items[leafIndex];
|
|
86
|
+
if (leafPair === undefined || !matches(leafPair.value)) return false;
|
|
87
|
+
|
|
88
|
+
// `commentBefore` can hold several blocks separated by a blank line. Only the
|
|
89
|
+
// final block sits against this key; anything above it is a file or section
|
|
90
|
+
// header that happens to precede the key, so it survives the removal instead
|
|
91
|
+
// of leaving with it.
|
|
92
|
+
const leafKey: unknown = leafPair.key;
|
|
93
|
+
const priorComment =
|
|
94
|
+
isScalar(leafKey) && typeof leafKey.commentBefore === "string"
|
|
95
|
+
? leafKey.commentBefore.slice(0, Math.max(0, leafKey.commentBefore.lastIndexOf("\n\n")))
|
|
96
|
+
: "";
|
|
97
|
+
cursor.items.splice(leafIndex, 1);
|
|
98
|
+
if (priorComment !== "") {
|
|
99
|
+
const heir: unknown = cursor.items[leafIndex]?.key;
|
|
100
|
+
if (isScalar(heir)) {
|
|
101
|
+
heir.commentBefore =
|
|
102
|
+
typeof heir.commentBefore === "string"
|
|
103
|
+
? `${priorComment}\n\n${heir.commentBefore}`
|
|
104
|
+
: priorComment;
|
|
105
|
+
} else {
|
|
106
|
+
// The key was last in its mapping, so there is no later key to carry the
|
|
107
|
+
// header. A trailing mapping comment keeps the text in the file.
|
|
108
|
+
cursor.comment =
|
|
109
|
+
typeof cursor.comment === "string" ? `${cursor.comment}\n${priorComment}` : priorComment;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let depth = owners.length - 1; depth >= 1; depth--) {
|
|
114
|
+
const mapping = owners[depth];
|
|
115
|
+
if (mapping === undefined || mapping.items.length > 0) break;
|
|
116
|
+
const owner = owners[depth - 1];
|
|
117
|
+
const segment = segments[depth - 1];
|
|
118
|
+
if (owner === undefined || segment === undefined) break;
|
|
119
|
+
const index = indexOfKey(owner, segment);
|
|
120
|
+
if (index >= 0) owner.items.splice(index, 1);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Prunes retired kit-owned keys from a deployed omp config.yml. */
|
|
126
|
+
export function syncOmpRemovals(ctx: Ctx, configFile: string): number {
|
|
127
|
+
const { change, echo, verbose, warn } = ctx.services.logger;
|
|
128
|
+
if (!existsSync(configFile)) return 0;
|
|
129
|
+
|
|
130
|
+
const doc = parseDocument(readFileSync(configFile, "utf8"));
|
|
131
|
+
const parseError = doc.errors[0];
|
|
132
|
+
if (parseError !== undefined) {
|
|
133
|
+
warn(`omp config.yml unreadable, retired keys not pruned: ${parseError.message}`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
const contents = doc.contents;
|
|
137
|
+
if (!isMap(contents)) return 0;
|
|
138
|
+
|
|
139
|
+
const pruned: Array<string> = [];
|
|
140
|
+
for (const [path, value] of OMP_RETIRED_VALUES) {
|
|
141
|
+
const matches = (node: unknown): boolean => isScalar(node) && node.value === value;
|
|
142
|
+
if (deletePath(contents, path.split("."), matches)) pruned.push(path);
|
|
143
|
+
}
|
|
144
|
+
for (const path of OMP_RETIRED_KEYS) {
|
|
145
|
+
if (deletePath(contents, path.split("."), () => true)) pruned.push(path);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (pruned.length === 0) {
|
|
149
|
+
verbose("omp config.yml carries no retired keys");
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (ctx.dryRun) {
|
|
153
|
+
echo(`[dry-run] prune ${configFile}: ${pruned.join(", ")}`);
|
|
154
|
+
return pruned.length;
|
|
155
|
+
}
|
|
156
|
+
writeFileSync(configFile, String(doc));
|
|
157
|
+
change(`omp config.yml pruned ${pruned.length} retired key(s): ${pruned.join(", ")}`);
|
|
158
|
+
return pruned.length;
|
|
159
|
+
}
|
|
@@ -5,437 +5,97 @@
|
|
|
5
5
|
* PI_CODING_AGENT_DIR, and XDG roots each move them.
|
|
6
6
|
* Resolution stays within the environment and filesystem probes so no omp
|
|
7
7
|
* subcommand runs under ctx.dryRun.
|
|
8
|
+
*
|
|
9
|
+
* Split: file deploy lives in `ompFileDeploy.ts`, the marketplace pass in
|
|
10
|
+
* `ompMarketplace.ts`, the plugin pass in `ompPlugins.ts`, and the retired-key
|
|
11
|
+
* prune in `ompRemovals.ts`. This module keeps the orchestration plus the
|
|
12
|
+
* summary surface.
|
|
8
13
|
*/
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { field } from "./toolchain"
|
|
21
|
-
|
|
22
|
-
const MARKETPLACE_NAME = "docks"
|
|
23
|
-
const MARKETPLACE_SOURCE = "https://github.com/DocksDocks/docks.git"
|
|
24
|
-
const MARKETPLACE_PLUGIN_IDS = ["docks@docks", "plan-lifecycle@docks"] as const
|
|
25
|
-
type OmpTextPayloadPath = Extract<PayloadPath, `SoT/.omp/${string}`>
|
|
14
|
+
import { isAbsolute, resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
import { bunBootstrap } from "./bun";
|
|
17
|
+
import { p } from "./exec";
|
|
18
|
+
import type { Ctx } from "./index";
|
|
19
|
+
import { ompPaths } from "./ompPaths";
|
|
20
|
+
import { mergeOmpConfig, mergeOmpModels } from "./ompYaml";
|
|
21
|
+
import { ensureDirectory, syncMergedYaml, syncWholeFile } from "./ompFileDeploy";
|
|
22
|
+
import { syncMarketplace } from "./ompMarketplace";
|
|
23
|
+
import { syncPlugins } from "./ompPlugins";
|
|
24
|
+
import { syncOmpRemovals } from "./ompRemovals";
|
|
26
25
|
|
|
27
26
|
export interface OmpState {
|
|
28
|
-
readonly pluginsInstalled: number
|
|
27
|
+
readonly pluginsInstalled: number;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
export async function ompSync(ctx: Ctx): Promise<OmpState> {
|
|
32
|
-
await bunBootstrap(ctx, ctx.services)
|
|
33
|
-
|
|
34
|
-
const paths = ompPaths({ home: ctx.home, env: process.env, platform: ctx.services.platform.raw() })
|
|
35
|
-
const agentDir = paths.agentDir
|
|
36
|
-
if (!ctx.dryRun) ensureDirectory(agentDir)
|
|
31
|
+
await bunBootstrap(ctx, ctx.services);
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
const paths = ompPaths({
|
|
34
|
+
home: ctx.home,
|
|
35
|
+
env: process.env,
|
|
36
|
+
platform: ctx.services.platform.raw(),
|
|
37
|
+
});
|
|
38
|
+
const agentDir = paths.agentDir;
|
|
39
|
+
if (!ctx.dryRun) ensureDirectory(agentDir);
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
syncWholeFile(
|
|
42
|
+
ctx,
|
|
43
|
+
"SoT/.omp/AGENTS.md",
|
|
44
|
+
p(agentDir, "AGENTS.md"),
|
|
45
|
+
"omp AGENTS.md already in sync",
|
|
46
|
+
"omp AGENTS.md synced",
|
|
47
|
+
);
|
|
48
|
+
syncWholeFile(
|
|
49
|
+
ctx,
|
|
50
|
+
"SoT/.omp/mcp.json",
|
|
51
|
+
p(agentDir, "mcp.json"),
|
|
52
|
+
"omp mcp.json already in sync",
|
|
53
|
+
"omp mcp.json synced",
|
|
54
|
+
);
|
|
55
|
+
syncMergedYaml(ctx, "SoT/.omp/config.yml", p(agentDir, "config.yml"), mergeOmpConfig);
|
|
56
|
+
// mergeOmpConfig is additive, so the prune must run on the merged result.
|
|
57
|
+
syncOmpRemovals(ctx, p(agentDir, "config.yml"));
|
|
58
|
+
syncMergedYaml(ctx, "SoT/.omp/models.yml", p(agentDir, "models.yml"), mergeOmpModels);
|
|
59
|
+
|
|
60
|
+
const intercomRootSetting = process.env["PI_CODING_AGENT_DIR"];
|
|
61
|
+
const intercomRoot =
|
|
62
|
+
intercomRootSetting !== undefined && intercomRootSetting !== ""
|
|
63
|
+
? isAbsolute(intercomRootSetting)
|
|
64
|
+
? intercomRootSetting
|
|
65
|
+
: resolve(process.cwd(), intercomRootSetting)
|
|
66
|
+
: p(ctx.home, ".pi", "agent");
|
|
67
|
+
const intercomDir = p(intercomRoot, "intercom");
|
|
68
|
+
if (!ctx.dryRun) ensureDirectory(intercomDir);
|
|
51
69
|
syncWholeFile(
|
|
52
70
|
ctx,
|
|
53
71
|
"SoT/.omp/intercom.json",
|
|
54
72
|
p(intercomDir, "config.json"),
|
|
55
73
|
"omp intercom configuration already in sync",
|
|
56
|
-
"omp intercom configuration synced"
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
const legacyRegistryFile = paths.dataRoot === paths.configRoot
|
|
60
|
-
? undefined
|
|
61
|
-
: p(paths.configRoot, "marketplaces.json")
|
|
62
|
-
await syncMarketplace(ctx, p(paths.dataRoot, "marketplaces.json"), legacyRegistryFile)
|
|
63
|
-
const pluginsInstalled = await syncPlugins(ctx)
|
|
64
|
-
return { pluginsInstalled }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ------------------------------------------------------------ file modes ----
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* `mkdirSync` and `writeFileSync` apply their `mode` only when they create the
|
|
71
|
-
* path, so an existing world-readable directory or file would keep its mode.
|
|
72
|
-
* The explicit chmod reproduces `install -d -m 0700` / `install -m 0600`.
|
|
73
|
-
*/
|
|
74
|
-
function ensureDirectory(directory: string): void {
|
|
75
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
76
|
-
chmodSync(directory, 0o700)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function writePrivateFile(target: string, content: string): void {
|
|
80
|
-
writeFileSync(target, content, { mode: 0o600 })
|
|
81
|
-
chmodSync(target, 0o600)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function backupPrivateFile(target: string): void {
|
|
85
|
-
copyFileSync(target, `${target}.bak`)
|
|
86
|
-
chmodSync(`${target}.bak`, 0o600)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// --------------------------------------------------------------- config ----
|
|
90
|
-
|
|
91
|
-
function syncWholeFile(
|
|
92
|
-
ctx: Ctx,
|
|
93
|
-
sourcePath: OmpTextPayloadPath,
|
|
94
|
-
target: string,
|
|
95
|
-
alreadyMessage: string,
|
|
96
|
-
syncedMessage: string
|
|
97
|
-
): void {
|
|
98
|
-
const { change, echo, verbose } = ctx.services.logger
|
|
99
|
-
const source = payloadDisplayPath(sourcePath)
|
|
100
|
-
const content = payloadText(sourcePath)
|
|
101
|
-
|
|
102
|
-
if (ctx.dryRun) {
|
|
103
|
-
echo(`[dry-run] cp ${source} -> ${target}`)
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (existsSync(target) && readFileSync(target, "utf8") === content) {
|
|
108
|
-
verbose(alreadyMessage)
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
if (existsSync(target)) backupPrivateFile(target)
|
|
112
|
-
writePrivateFile(target, content)
|
|
113
|
-
change(syncedMessage)
|
|
114
|
-
ctx.nextStepTriggers.ompRestart = true
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function syncMergedYaml(
|
|
118
|
-
ctx: Ctx,
|
|
119
|
-
sourcePath: OmpTextPayloadPath,
|
|
120
|
-
target: string,
|
|
121
|
-
merge: (sotText: string, deployedText: string) => string
|
|
122
|
-
): void {
|
|
123
|
-
const { change, echo, verbose } = ctx.services.logger
|
|
124
|
-
const source = payloadDisplayPath(sourcePath)
|
|
125
|
-
const sotText = payloadText(sourcePath)
|
|
126
|
-
const name = basename(sourcePath)
|
|
127
|
-
|
|
128
|
-
if (!existsSync(target)) {
|
|
129
|
-
if (ctx.dryRun) {
|
|
130
|
-
echo(`[dry-run] cp ${source} -> ${target}`)
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
writePrivateFile(target, sotText)
|
|
134
|
-
change(`omp ${name} installed`)
|
|
135
|
-
ctx.nextStepTriggers.ompRestart = true
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const deployedText = readFileSync(target, "utf8")
|
|
140
|
-
const merged = merge(sotText, deployedText)
|
|
141
|
-
if (merged === deployedText) {
|
|
142
|
-
verbose(`omp ${name} already in sync`)
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
if (ctx.dryRun) {
|
|
146
|
-
echo(`[dry-run] merge ${source} -> ${target} (backup at ${target}.bak)`)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
backupPrivateFile(target)
|
|
151
|
-
writePrivateFile(`${target}.tmp`, merged)
|
|
152
|
-
renameSync(`${target}.tmp`, target)
|
|
153
|
-
chmodSync(target, 0o600)
|
|
154
|
-
change(`omp ${name} merged (backup at ${name}.bak)`)
|
|
155
|
-
ctx.nextStepTriggers.ompRestart = true
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---------------------------------------------------------- marketplace ----
|
|
159
|
-
|
|
160
|
-
function registryHasDocks(registryFile: string): boolean {
|
|
161
|
-
if (!existsSync(registryFile)) return false
|
|
162
|
-
|
|
163
|
-
let registryText: string
|
|
164
|
-
try {
|
|
165
|
-
registryText = readFileSync(registryFile, "utf8")
|
|
166
|
-
} catch {
|
|
167
|
-
return false
|
|
168
|
-
}
|
|
169
|
-
const registry = parseJson(registryText)
|
|
170
|
-
if (registry === undefined) return false
|
|
171
|
-
if (isObject(registry) && Object.hasOwn(registry, MARKETPLACE_NAME)) return true
|
|
172
|
-
|
|
173
|
-
const entries = Array.isArray(registry)
|
|
174
|
-
? registry
|
|
175
|
-
: isObject(registry) && Array.isArray(registry["marketplaces"])
|
|
176
|
-
? registry["marketplaces"]
|
|
177
|
-
: []
|
|
178
|
-
return entries.some((entry) => isObject(entry) && entry["name"] === MARKETPLACE_NAME)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function firstOutputLine(result: AsyncProcessResult): string {
|
|
182
|
-
const output = `${result.stdout}${result.stderr}`
|
|
183
|
-
return output.split("\n")[0] || "unknown error"
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Three states, because upstream `getMarketplacesRegistryPath` copies a legacy
|
|
188
|
-
* `configRoot` registry forward the first time it resolves an XDG data root:
|
|
189
|
-
* the active registry lists docks, only the legacy registry lists it, or
|
|
190
|
-
* neither does. In the middle state the kit never copies user data itself; it
|
|
191
|
-
* takes the refresh path, whose own resolution inside omp performs that
|
|
192
|
-
* adoption and leaves the active registry present.
|
|
193
|
-
*/
|
|
194
|
-
async function syncMarketplace(ctx: Ctx, registryFile: string, legacyRegistryFile?: string): Promise<void> {
|
|
195
|
-
const { change, clearProgress, echo, progress, verbose } = ctx.services.logger
|
|
196
|
-
const registered = registryHasDocks(registryFile)
|
|
197
|
-
const adoptable = !registered && legacyRegistryFile !== undefined && registryHasDocks(legacyRegistryFile)
|
|
198
|
-
|
|
199
|
-
if (ctx.dryRun) {
|
|
200
|
-
if (adoptable) {
|
|
201
|
-
echo(
|
|
202
|
-
ctx.skipPluginRefresh === true
|
|
203
|
-
? "[dry-run] omp plugin marketplace list"
|
|
204
|
-
: `[dry-run] omp plugin marketplace update ${MARKETPLACE_NAME}`
|
|
205
|
-
)
|
|
206
|
-
} else if (registered) verbose("omp docks marketplace already registered")
|
|
207
|
-
else echo(`[dry-run] omp plugin marketplace add ${MARKETPLACE_SOURCE}`)
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// syncPlugins owns the single skip message for both missing CLIs, and it runs
|
|
212
|
-
// right after this pass. Without omp no marketplace command can run at all,
|
|
213
|
-
// and without git a marketplace clone cannot resolve, so both are deliberate
|
|
214
|
-
// skips: return silently instead of spawning and recording a failure.
|
|
215
|
-
if (ctx.services.deps.probe("omp").state === "missing") return
|
|
216
|
-
if (ctx.services.deps.probe("git").state === "missing") return
|
|
217
|
-
|
|
218
|
-
if (!registered && !adoptable) {
|
|
219
|
-
progress("Registering omp docks marketplace...")
|
|
220
|
-
const result = await spawnProcess("omp", ["plugin", "marketplace", "add", MARKETPLACE_SOURCE], {
|
|
221
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
222
|
-
})
|
|
223
|
-
clearProgress()
|
|
224
|
-
if (result.error === undefined && result.exitCode === 0) {
|
|
225
|
-
change("omp docks marketplace registered")
|
|
226
|
-
} else {
|
|
227
|
-
recordFailure(
|
|
228
|
-
ctx,
|
|
229
|
-
`omp docks marketplace registration failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace add ${MARKETPLACE_SOURCE}`
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (ctx.skipPluginRefresh === true) {
|
|
236
|
-
if (!adoptable) {
|
|
237
|
-
verbose("omp docks marketplace already registered; refresh-only update skipped")
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Adoption only needs omp to resolve the registry path. `marketplace list`
|
|
242
|
-
// does that and fetches nothing, so it stays inside the flag's contract
|
|
243
|
-
// while leaving the active registry present.
|
|
244
|
-
progress("Adopting omp docks marketplace registry...")
|
|
245
|
-
const listed = await spawnProcess("omp", ["plugin", "marketplace", "list"], {
|
|
246
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
247
|
-
})
|
|
248
|
-
clearProgress()
|
|
249
|
-
if (listed.error === undefined && listed.exitCode === 0) {
|
|
250
|
-
change("omp docks marketplace registry adopted; refresh-only update skipped")
|
|
251
|
-
} else {
|
|
252
|
-
recordFailure(
|
|
253
|
-
ctx,
|
|
254
|
-
`omp docks marketplace adoption failed: ${firstOutputLine(listed)}; run manually: omp plugin marketplace list`
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
progress("Updating omp docks marketplace...")
|
|
261
|
-
const result = await spawnProcess("omp", ["plugin", "marketplace", "update", MARKETPLACE_NAME], {
|
|
262
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
263
|
-
})
|
|
264
|
-
clearProgress()
|
|
265
|
-
if (result.error === undefined && result.exitCode === 0) {
|
|
266
|
-
verbose("omp docks marketplace refreshed")
|
|
267
|
-
} else {
|
|
268
|
-
recordFailure(
|
|
269
|
-
ctx,
|
|
270
|
-
`omp docks marketplace update failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace update ${MARKETPLACE_NAME}`
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// -------------------------------------------------------------- plugins ----
|
|
276
|
-
|
|
277
|
-
interface InstalledPlugins {
|
|
278
|
-
readonly marketplace: Set<string>
|
|
279
|
-
readonly npm: Map<string, string>
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async function installedPluginIdsFromCli(): Promise<InstalledPlugins | undefined> {
|
|
283
|
-
const result = await spawnProcess("omp", ["plugin", "list", "--json"], {
|
|
284
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
285
|
-
})
|
|
286
|
-
if (result.error !== undefined || result.exitCode !== 0) return undefined
|
|
287
|
-
|
|
288
|
-
const value = parseJson(result.stdout)
|
|
289
|
-
if (
|
|
290
|
-
value === undefined ||
|
|
291
|
-
!isObject(value) ||
|
|
292
|
-
!Array.isArray(value["marketplace"]) ||
|
|
293
|
-
!Array.isArray(value["npm"])
|
|
294
|
-
) {
|
|
295
|
-
return undefined
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// `omp plugin list --json` reports marketplace rows as
|
|
299
|
-
// `{ id: "<plugin>@<marketplace>", scope, entries: [...] }` - the composite id
|
|
300
|
-
// is already the token `omp plugin install/upgrade` takes. omp emits one row
|
|
301
|
-
// per scope holding the plugin, so a match found only in a `project` row
|
|
302
|
-
// means the user scope is empty and `upgrade --scope user` would fail; only a
|
|
303
|
-
// `user` row counts as installed for this pipeline.
|
|
304
|
-
const marketplace = new Set<string>()
|
|
305
|
-
for (const row of value["marketplace"]) {
|
|
306
|
-
if (!isObject(row) || typeof row["id"] !== "string" || row["scope"] !== "user") continue
|
|
307
|
-
marketplace.add(row["id"])
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const npm = new Map<string, string>()
|
|
311
|
-
for (const row of value["npm"]) {
|
|
312
|
-
if (!isObject(row) || typeof row["name"] !== "string" || typeof row["version"] !== "string") continue
|
|
313
|
-
npm.set(row["name"], row["version"])
|
|
314
|
-
}
|
|
315
|
-
return { marketplace, npm }
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function runPluginCommand(
|
|
319
|
-
ctx: Ctx,
|
|
320
|
-
plugin: string,
|
|
321
|
-
args: ReadonlyArray<string>
|
|
322
|
-
): Promise<boolean> {
|
|
323
|
-
const { clearProgress, progress } = ctx.services.logger
|
|
324
|
-
progress(`Updating omp plugin ${plugin}...`)
|
|
325
|
-
const result = await spawnProcess("omp", args, { stdio: ["ignore", "pipe", "pipe"] })
|
|
326
|
-
clearProgress()
|
|
327
|
-
if (result.error === undefined && result.exitCode === 0) return true
|
|
328
|
-
|
|
329
|
-
recordFailure(
|
|
330
|
-
ctx,
|
|
331
|
-
`omp plugin operation failed for ${plugin}: ${firstOutputLine(result)}; run manually: omp ${args.join(" ")}`
|
|
332
|
-
)
|
|
333
|
-
return false
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async function syncPlugins(ctx: Ctx): Promise<number> {
|
|
337
|
-
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
338
|
-
|
|
339
|
-
if (ctx.dryRun) {
|
|
340
|
-
const piIntercomPin = field("pi-intercom", "verified")
|
|
341
|
-
for (const pluginId of MARKETPLACE_PLUGIN_IDS) {
|
|
342
|
-
echo(`[dry-run] omp plugin install --scope user ${pluginId}`)
|
|
343
|
-
}
|
|
344
|
-
if (piIntercomPin === "") {
|
|
345
|
-
warn("pi-intercom install skipped because SoT/toolchain.json has no verified pi-intercom pin")
|
|
346
|
-
} else {
|
|
347
|
-
echo(`[dry-run] omp install pi-intercom@${piIntercomPin}`)
|
|
348
|
-
}
|
|
349
|
-
return 0
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (ctx.services.deps.probe("omp").state === "missing") {
|
|
353
|
-
ctx.services.deps.warnMissing(
|
|
354
|
-
"omp",
|
|
355
|
-
ctx.services.logger,
|
|
356
|
-
"deployed omp config only — marketplace and plugin passes skipped; re-run sync after installing"
|
|
357
|
-
)
|
|
358
|
-
return 0
|
|
359
|
-
}
|
|
360
|
-
if (ctx.services.deps.probe("git").state === "missing") {
|
|
361
|
-
ctx.services.deps.warnMissing(
|
|
362
|
-
"git",
|
|
363
|
-
ctx.services.logger,
|
|
364
|
-
"plugin marketplaces are git repos — omp plugin refresh skipped; re-run sync after installing"
|
|
365
|
-
)
|
|
366
|
-
return 0
|
|
367
|
-
}
|
|
368
|
-
const piIntercomPin = field("pi-intercom", "verified")
|
|
369
|
-
|
|
370
|
-
progress("Checking installed omp plugins...")
|
|
371
|
-
const installed = await installedPluginIdsFromCli()
|
|
372
|
-
clearProgress()
|
|
373
|
-
if (installed === undefined) {
|
|
374
|
-
warn("omp plugin inventory unavailable — falling back to the full refresh path")
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
let pluginsInstalled = installed === undefined
|
|
378
|
-
? 0
|
|
379
|
-
: MARKETPLACE_PLUGIN_IDS.filter((pluginId) => installed.marketplace.has(pluginId)).length +
|
|
380
|
-
(piIntercomPin !== "" && installed.npm.has("pi-intercom") ? 1 : 0)
|
|
381
|
-
let operationsSucceeded = 0
|
|
382
|
-
|
|
383
|
-
for (const pluginId of MARKETPLACE_PLUGIN_IDS) {
|
|
384
|
-
const present = installed?.marketplace.has(pluginId) === true
|
|
385
|
-
if (present && ctx.skipPluginRefresh === true) {
|
|
386
|
-
verbose(`omp plugin ${pluginId} already installed; refresh-only update skipped`)
|
|
387
|
-
continue
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const args = present
|
|
391
|
-
? ["plugin", "upgrade", "--scope", "user", pluginId]
|
|
392
|
-
: ["plugin", "install", "--scope", "user", pluginId]
|
|
393
|
-
if (await runPluginCommand(ctx, pluginId, args)) {
|
|
394
|
-
operationsSucceeded++
|
|
395
|
-
if (!present) pluginsInstalled++
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (piIntercomPin === "") {
|
|
400
|
-
warn("pi-intercom install skipped because SoT/toolchain.json has no verified pi-intercom pin")
|
|
401
|
-
} else {
|
|
402
|
-
const installedVersion = installed?.npm.get("pi-intercom")
|
|
403
|
-
const present = installedVersion !== undefined
|
|
404
|
-
if (installedVersion === piIntercomPin) {
|
|
405
|
-
verbose(`omp npm plugin pi-intercom already installed at ${piIntercomPin}`)
|
|
406
|
-
} else if (present && ctx.skipPluginRefresh === true) {
|
|
407
|
-
verbose("omp npm plugin pi-intercom already installed; refresh-only update skipped")
|
|
408
|
-
} else {
|
|
409
|
-
const args = present
|
|
410
|
-
? ["install", "--force", `pi-intercom@${piIntercomPin}`]
|
|
411
|
-
: ["install", `pi-intercom@${piIntercomPin}`]
|
|
412
|
-
if (await runPluginCommand(ctx, "pi-intercom", args)) {
|
|
413
|
-
operationsSucceeded++
|
|
414
|
-
if (!present) pluginsInstalled++
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
74
|
+
"omp intercom configuration synced",
|
|
75
|
+
);
|
|
418
76
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
return pluginsInstalled
|
|
77
|
+
const legacyRegistryFile =
|
|
78
|
+
paths.dataRoot === paths.configRoot ? undefined : p(paths.configRoot, "marketplaces.json");
|
|
79
|
+
await syncMarketplace(ctx, p(paths.dataRoot, "marketplaces.json"), legacyRegistryFile);
|
|
80
|
+
const pluginsInstalled = await syncPlugins(ctx);
|
|
81
|
+
return { pluginsInstalled };
|
|
426
82
|
}
|
|
427
83
|
|
|
428
84
|
// -------------------------------------------------------------- summary ----
|
|
429
85
|
|
|
430
86
|
export function ompSummary(ctx: Ctx, state: OmpState): void {
|
|
431
|
-
const { echo } = ctx.services.logger
|
|
432
|
-
const agentDir = ompPaths({
|
|
433
|
-
|
|
434
|
-
|
|
87
|
+
const { echo } = ctx.services.logger;
|
|
88
|
+
const agentDir = ompPaths({
|
|
89
|
+
home: ctx.home,
|
|
90
|
+
env: process.env,
|
|
91
|
+
platform: ctx.services.platform.raw(),
|
|
92
|
+
}).agentDir;
|
|
93
|
+
echo(`omp: ${agentDir}`);
|
|
94
|
+
if (!ctx.dryRun) echo(`omp plugins: ${state.pluginsInstalled} installed`);
|
|
435
95
|
}
|
|
436
96
|
|
|
437
97
|
export function ompNextSteps(ctx: Ctx): Array<string> {
|
|
438
98
|
return ctx.verbose || ctx.nextStepTriggers.ompRestart
|
|
439
99
|
? ["Restart omp to load any refreshed plugins, skills, or tools."]
|
|
440
|
-
: []
|
|
100
|
+
: [];
|
|
441
101
|
}
|