docks-kit 0.17.1 → 0.17.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 +36 -32
- package/cli/docs/omp-models.md +88 -53
- 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 +57 -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/ompSync.ts +64 -408
- 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/package.json +9 -2
- package/cli/src/engine-native/claudePlugins.ts +0 -510
|
@@ -2,162 +2,165 @@
|
|
|
2
2
|
* Direct modes for `model` and `toolchain`. Same public argv vocabulary,
|
|
3
3
|
* golden-tested messages, same exit codes.
|
|
4
4
|
*/
|
|
5
|
-
import { p } from "./exec"
|
|
6
|
-
import { readFileSync } from "node:fs"
|
|
7
|
-
import { payloadText } from "../payload"
|
|
5
|
+
import { p } from "./exec";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { payloadText } from "../payload";
|
|
8
8
|
|
|
9
|
-
import { syncClaudeModel } from "./claudeSettingsModifiers"
|
|
10
|
-
import { syncCodexModel } from "./codexToml"
|
|
11
|
-
import type { Ctx } from "./index"
|
|
12
|
-
import { isObject, parseJson, type Json } from "./jq"
|
|
13
|
-
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
14
|
-
import { bunBootstrap } from "./bun"
|
|
15
|
-
import { installedVersion, present, report } from "./toolchain"
|
|
9
|
+
import { syncClaudeModel } from "./claudeSettingsModifiers";
|
|
10
|
+
import { syncCodexModel } from "./codexToml";
|
|
11
|
+
import type { Ctx } from "./index";
|
|
12
|
+
import { isObject, parseJson, type Json } from "./jq";
|
|
13
|
+
import { printModels, validateClaudeModel, validateCodexModel } from "./models";
|
|
14
|
+
import { bunBootstrap } from "./bun";
|
|
15
|
+
import { installedVersion, present, report } from "./toolchain";
|
|
16
16
|
|
|
17
17
|
export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
18
|
-
const { echo, err, warn } = ctx.services.logger
|
|
19
|
-
let tool = ""
|
|
20
|
-
let value = ""
|
|
18
|
+
const { echo, err, warn } = ctx.services.logger;
|
|
19
|
+
let tool = "";
|
|
20
|
+
let value = "";
|
|
21
21
|
for (const arg of args) {
|
|
22
|
-
if (arg === "--dry-run") ctx.dryRun = true
|
|
22
|
+
if (arg === "--dry-run") ctx.dryRun = true;
|
|
23
23
|
else if (arg === "--verbose") {
|
|
24
|
-
ctx.verbose = true
|
|
25
|
-
} else if (arg === "claude" || arg === "codex") tool = arg
|
|
24
|
+
ctx.verbose = true;
|
|
25
|
+
} else if (arg === "claude" || arg === "codex") tool = arg;
|
|
26
26
|
else if (arg.startsWith("-")) {
|
|
27
|
-
err(`Unknown flag for model: ${arg}`)
|
|
28
|
-
return 2
|
|
29
|
-
} else value = arg
|
|
27
|
+
err(`Unknown flag for model: ${arg}`);
|
|
28
|
+
return 2;
|
|
29
|
+
} else value = arg;
|
|
30
30
|
}
|
|
31
31
|
if (tool === "") {
|
|
32
|
-
err("Usage: model <claude|codex> [value] [--dry-run]")
|
|
33
|
-
return 2
|
|
32
|
+
err("Usage: model <claude|codex> [value] [--dry-run]");
|
|
33
|
+
return 2;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
if (value === "") {
|
|
37
37
|
if (tool === "claude") {
|
|
38
|
-
const deployed = p(ctx.home, ".claude", "settings.json")
|
|
39
|
-
const result = readConfig(deployed)
|
|
38
|
+
const deployed = p(ctx.home, ".claude", "settings.json");
|
|
39
|
+
const result = readConfig(deployed);
|
|
40
40
|
if (result.kind === "missing") {
|
|
41
|
-
warn("~/.claude/settings.json missing")
|
|
42
|
-
return 0
|
|
41
|
+
warn("~/.claude/settings.json missing");
|
|
42
|
+
return 0;
|
|
43
43
|
}
|
|
44
44
|
if (result.kind === "read-error") {
|
|
45
|
-
err(`Failed to read ~/.claude/settings.json: ${String(result.error)}`)
|
|
46
|
-
return 1
|
|
45
|
+
err(`Failed to read ~/.claude/settings.json: ${String(result.error)}`);
|
|
46
|
+
return 1;
|
|
47
47
|
}
|
|
48
|
-
echo(`deployed: ${jsonModelText(result.data)}`)
|
|
49
|
-
echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`)
|
|
48
|
+
echo(`deployed: ${jsonModelText(result.data)}`);
|
|
49
|
+
echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`);
|
|
50
50
|
} else {
|
|
51
|
-
const deployed = p(ctx.home, ".codex", "config.toml")
|
|
52
|
-
const result = readConfig(deployed)
|
|
51
|
+
const deployed = p(ctx.home, ".codex", "config.toml");
|
|
52
|
+
const result = readConfig(deployed);
|
|
53
53
|
if (result.kind === "missing") {
|
|
54
|
-
warn("~/.codex/config.toml missing")
|
|
55
|
-
return 0
|
|
54
|
+
warn("~/.codex/config.toml missing");
|
|
55
|
+
return 0;
|
|
56
56
|
}
|
|
57
57
|
if (result.kind === "read-error") {
|
|
58
|
-
err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`)
|
|
59
|
-
return 1
|
|
58
|
+
err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`);
|
|
59
|
+
return 1;
|
|
60
60
|
}
|
|
61
|
-
echo(`deployed: ${tomlModelText(result.data)}`)
|
|
62
|
-
echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`)
|
|
61
|
+
echo(`deployed: ${tomlModelText(result.data)}`);
|
|
62
|
+
echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`);
|
|
63
63
|
}
|
|
64
|
-
printModels(ctx, tool)
|
|
65
|
-
return 0
|
|
64
|
+
printModels(ctx, tool);
|
|
65
|
+
return 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (tool === "claude") {
|
|
69
69
|
if (!validateClaudeModel(ctx, value)) {
|
|
70
|
-
printModels(ctx, "claude")
|
|
71
|
-
err(`Invalid Claude model '${value}'`)
|
|
72
|
-
return 2
|
|
70
|
+
printModels(ctx, "claude");
|
|
71
|
+
err(`Invalid Claude model '${value}'`);
|
|
72
|
+
return 2;
|
|
73
73
|
}
|
|
74
|
-
syncClaudeModel(ctx, value)
|
|
74
|
+
syncClaudeModel(ctx, value);
|
|
75
75
|
} else {
|
|
76
76
|
if (!validateCodexModel(ctx, value)) {
|
|
77
|
-
printModels(ctx, "codex")
|
|
78
|
-
err(`Invalid Codex model '${value}'`)
|
|
79
|
-
return 2
|
|
77
|
+
printModels(ctx, "codex");
|
|
78
|
+
err(`Invalid Codex model '${value}'`);
|
|
79
|
+
return 2;
|
|
80
80
|
}
|
|
81
|
-
syncCodexModel(ctx, value)
|
|
81
|
+
syncCodexModel(ctx, value);
|
|
82
82
|
}
|
|
83
|
-
return 0
|
|
83
|
+
return 0;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
type ConfigReadResult =
|
|
87
87
|
| { readonly kind: "missing" }
|
|
88
88
|
| { readonly kind: "read-error"; readonly error: unknown }
|
|
89
|
-
| { readonly kind: "data"; readonly data: string }
|
|
89
|
+
| { readonly kind: "data"; readonly data: string };
|
|
90
90
|
|
|
91
91
|
function readConfig(file: string): ConfigReadResult {
|
|
92
92
|
try {
|
|
93
|
-
return { kind: "data", data: readFileSync(file, "utf8") }
|
|
93
|
+
return { kind: "data", data: readFileSync(file, "utf8") };
|
|
94
94
|
} catch (error) {
|
|
95
95
|
const code =
|
|
96
|
-
typeof error === "object" &&
|
|
96
|
+
typeof error === "object" &&
|
|
97
|
+
error !== null &&
|
|
98
|
+
"code" in error &&
|
|
99
|
+
typeof error.code === "string"
|
|
97
100
|
? error.code
|
|
98
|
-
: undefined
|
|
99
|
-
return code === "ENOENT" ? { kind: "missing" } : { kind: "read-error", error }
|
|
101
|
+
: undefined;
|
|
102
|
+
return code === "ENOENT" ? { kind: "missing" } : { kind: "read-error", error };
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
function jsonModelText(text: string): string {
|
|
104
|
-
const doc = parseJson(text)
|
|
105
|
-
if (doc === undefined) return ""
|
|
106
|
-
const v: Json | undefined = isObject(doc) ? doc["model"] : undefined
|
|
107
|
-
if (v === undefined || v === null || v === false) return "default (unset)"
|
|
108
|
-
return typeof v === "string" ? v : JSON.stringify(v)
|
|
107
|
+
const doc = parseJson(text);
|
|
108
|
+
if (doc === undefined) return "";
|
|
109
|
+
const v: Json | undefined = isObject(doc) ? doc["model"] : undefined;
|
|
110
|
+
if (v === undefined || v === null || v === false) return "default (unset)";
|
|
111
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
/** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
|
|
112
115
|
|
|
113
116
|
function tomlModelText(text: string): string {
|
|
114
117
|
for (const line of text.split("\n")) {
|
|
115
|
-
if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? ""
|
|
118
|
+
if (/^model[ \t]*=/.test(line)) return line.split('"')[1] ?? "";
|
|
116
119
|
}
|
|
117
|
-
return ""
|
|
120
|
+
return "";
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
121
|
-
const { echo, err, verbose } = ctx.services.logger
|
|
122
|
-
const words = args.filter((arg) => !arg.startsWith("--"))
|
|
123
|
-
const op = words[0] ?? "check"
|
|
124
|
-
const tool = words[1] ?? ""
|
|
124
|
+
const { echo, err, verbose } = ctx.services.logger;
|
|
125
|
+
const words = args.filter((arg) => !arg.startsWith("--"));
|
|
126
|
+
const op = words[0] ?? "check";
|
|
127
|
+
const tool = words[1] ?? "";
|
|
125
128
|
for (const arg of args) {
|
|
126
|
-
if (arg === "--verbose") ctx.verbose = true
|
|
129
|
+
if (arg === "--verbose") ctx.verbose = true;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
if (op === "check") {
|
|
130
|
-
await report(ctx)
|
|
131
|
-
return 0
|
|
133
|
+
await report(ctx);
|
|
134
|
+
return 0;
|
|
132
135
|
}
|
|
133
136
|
if (op !== "ensure") {
|
|
134
|
-
err("Usage: toolchain [check|ensure <tool>]")
|
|
135
|
-
return 2
|
|
137
|
+
err("Usage: toolchain [check|ensure <tool>]");
|
|
138
|
+
return 2;
|
|
136
139
|
}
|
|
137
140
|
if (tool === "") {
|
|
138
|
-
err("Usage: toolchain ensure <tool>")
|
|
139
|
-
return 2
|
|
141
|
+
err("Usage: toolchain ensure <tool>");
|
|
142
|
+
return 2;
|
|
140
143
|
}
|
|
141
144
|
switch (tool) {
|
|
142
145
|
case "bun": {
|
|
143
146
|
// `bun` is the one managed tool, and its bootstrap is silent when Bun is
|
|
144
147
|
// already installed. Probe first so the no-op confirmation the `--verbose`
|
|
145
148
|
// contract promises is not mistaken for a fresh install.
|
|
146
|
-
const alreadyInstalled = present(ctx, "bun")
|
|
147
|
-
if ((await bunBootstrap(ctx, ctx.services)).kind !== "ready") return 1
|
|
149
|
+
const alreadyInstalled = present(ctx, "bun");
|
|
150
|
+
if ((await bunBootstrap(ctx, ctx.services)).kind !== "ready") return 1;
|
|
148
151
|
if (alreadyInstalled) {
|
|
149
152
|
// A present tool whose --version cannot be read reports `unknown` in the
|
|
150
153
|
// doctor table; keep the same vocabulary rather than an empty pair of
|
|
151
154
|
// parentheses.
|
|
152
|
-
const probed = await installedVersion(ctx, "bun")
|
|
153
|
-
const installed = probed === "" ? "version unknown" : probed
|
|
154
|
-
if (ctx.dryRun) echo(`[dry-run] bun up to date (${installed})`)
|
|
155
|
-
else verbose(`bun up to date (${installed})`)
|
|
155
|
+
const probed = await installedVersion(ctx, "bun");
|
|
156
|
+
const installed = probed === "" ? "version unknown" : probed;
|
|
157
|
+
if (ctx.dryRun) echo(`[dry-run] bun up to date (${installed})`);
|
|
158
|
+
else verbose(`bun up to date (${installed})`);
|
|
156
159
|
}
|
|
157
|
-
return 0
|
|
160
|
+
return 0;
|
|
158
161
|
}
|
|
159
162
|
default:
|
|
160
|
-
err("toolchain ensure supports managed tools only (bun)")
|
|
161
|
-
return 2
|
|
163
|
+
err("toolchain ensure supports managed tools only (bun)");
|
|
164
|
+
return 2;
|
|
162
165
|
}
|
|
163
166
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative omp file deploy: private file modes, whole-file sync, and
|
|
3
|
+
* merged-YAML sync for the agent directory. Split from ompSync.ts; the
|
|
4
|
+
* orchestration still lives there.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { payloadDisplayPath, payloadText, type PayloadPath } from "../payload";
|
|
18
|
+
import type { Ctx } from "./index";
|
|
19
|
+
|
|
20
|
+
export type OmpTextPayloadPath = Extract<PayloadPath, `SoT/.omp/${string}`>;
|
|
21
|
+
|
|
22
|
+
// ------------------------------------------------------------ file modes ----
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `mkdirSync` and `writeFileSync` apply their `mode` only when they create the
|
|
26
|
+
* path, so an existing world-readable directory or file would keep its mode.
|
|
27
|
+
* The explicit chmod reproduces `install -d -m 0700` / `install -m 0600`.
|
|
28
|
+
*/
|
|
29
|
+
export function ensureDirectory(directory: string): void {
|
|
30
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
31
|
+
chmodSync(directory, 0o700);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writePrivateFile(target: string, content: string): void {
|
|
35
|
+
writeFileSync(target, content, { mode: 0o600 });
|
|
36
|
+
chmodSync(target, 0o600);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function backupPrivateFile(target: string): void {
|
|
40
|
+
copyFileSync(target, `${target}.bak`);
|
|
41
|
+
chmodSync(`${target}.bak`, 0o600);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --------------------------------------------------------------- config ----
|
|
45
|
+
|
|
46
|
+
export function syncWholeFile(
|
|
47
|
+
ctx: Ctx,
|
|
48
|
+
sourcePath: OmpTextPayloadPath,
|
|
49
|
+
target: string,
|
|
50
|
+
alreadyMessage: string,
|
|
51
|
+
syncedMessage: string,
|
|
52
|
+
): void {
|
|
53
|
+
const { change, echo, verbose } = ctx.services.logger;
|
|
54
|
+
const source = payloadDisplayPath(sourcePath);
|
|
55
|
+
const content = payloadText(sourcePath);
|
|
56
|
+
|
|
57
|
+
if (ctx.dryRun) {
|
|
58
|
+
echo(`[dry-run] cp ${source} -> ${target}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (existsSync(target) && readFileSync(target, "utf8") === content) {
|
|
63
|
+
verbose(alreadyMessage);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (existsSync(target)) backupPrivateFile(target);
|
|
67
|
+
writePrivateFile(target, content);
|
|
68
|
+
change(syncedMessage);
|
|
69
|
+
ctx.nextStepTriggers.ompRestart = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function syncMergedYaml(
|
|
73
|
+
ctx: Ctx,
|
|
74
|
+
sourcePath: OmpTextPayloadPath,
|
|
75
|
+
target: string,
|
|
76
|
+
merge: (sotText: string, deployedText: string) => string,
|
|
77
|
+
): void {
|
|
78
|
+
const { change, echo, verbose } = ctx.services.logger;
|
|
79
|
+
const source = payloadDisplayPath(sourcePath);
|
|
80
|
+
const sotText = payloadText(sourcePath);
|
|
81
|
+
const name = basename(sourcePath);
|
|
82
|
+
|
|
83
|
+
if (!existsSync(target)) {
|
|
84
|
+
if (ctx.dryRun) {
|
|
85
|
+
echo(`[dry-run] cp ${source} -> ${target}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
writePrivateFile(target, sotText);
|
|
89
|
+
change(`omp ${name} installed`);
|
|
90
|
+
ctx.nextStepTriggers.ompRestart = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const deployedText = readFileSync(target, "utf8");
|
|
95
|
+
const merged = merge(sotText, deployedText);
|
|
96
|
+
if (merged === deployedText) {
|
|
97
|
+
verbose(`omp ${name} already in sync`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (ctx.dryRun) {
|
|
101
|
+
echo(`[dry-run] merge ${source} -> ${target} (backup at ${target}.bak)`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
backupPrivateFile(target);
|
|
106
|
+
writePrivateFile(`${target}.tmp`, merged);
|
|
107
|
+
renameSync(`${target}.tmp`, target);
|
|
108
|
+
chmodSync(target, 0o600);
|
|
109
|
+
change(`omp ${name} merged (backup at ${name}.bak)`);
|
|
110
|
+
ctx.nextStepTriggers.ompRestart = true;
|
|
111
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative omp marketplace pass: registry detection plus register, adopt,
|
|
3
|
+
* and refresh flows. Split from ompSync.ts; the orchestration still lives
|
|
4
|
+
* there. `firstOutputLine` is shared with the plugin pass in ompPlugins.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
import { spawnProcess, type AsyncProcessResult } from "./exec";
|
|
9
|
+
import { recordFailure } from "./failures";
|
|
10
|
+
import type { Ctx } from "./index";
|
|
11
|
+
import { isObject, parseJson } from "./jq";
|
|
12
|
+
|
|
13
|
+
const MARKETPLACE_NAME = "docks";
|
|
14
|
+
const MARKETPLACE_SOURCE = "https://github.com/DocksDocks/docks.git";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------- marketplace ----
|
|
17
|
+
|
|
18
|
+
function registryHasDocks(registryFile: string): boolean {
|
|
19
|
+
if (!existsSync(registryFile)) return false;
|
|
20
|
+
|
|
21
|
+
let registryText: string;
|
|
22
|
+
try {
|
|
23
|
+
registryText = readFileSync(registryFile, "utf8");
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const registry = parseJson(registryText);
|
|
28
|
+
if (registry === undefined) return false;
|
|
29
|
+
if (isObject(registry) && Object.hasOwn(registry, MARKETPLACE_NAME)) return true;
|
|
30
|
+
|
|
31
|
+
const entries = Array.isArray(registry)
|
|
32
|
+
? registry
|
|
33
|
+
: isObject(registry) && Array.isArray(registry["marketplaces"])
|
|
34
|
+
? registry["marketplaces"]
|
|
35
|
+
: [];
|
|
36
|
+
return entries.some((entry) => isObject(entry) && entry["name"] === MARKETPLACE_NAME);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function firstOutputLine(result: AsyncProcessResult): string {
|
|
40
|
+
const output = `${result.stdout}${result.stderr}`;
|
|
41
|
+
return output.split("\n")[0] || "unknown error";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Three states, because upstream `getMarketplacesRegistryPath` copies a legacy
|
|
46
|
+
* `configRoot` registry forward the first time it resolves an XDG data root:
|
|
47
|
+
* the active registry lists docks, only the legacy registry lists it, or
|
|
48
|
+
* neither does. In the middle state the kit never copies user data itself; it
|
|
49
|
+
* takes the refresh path, whose own resolution inside omp performs that
|
|
50
|
+
* adoption and leaves the active registry present.
|
|
51
|
+
*/
|
|
52
|
+
export async function syncMarketplace(
|
|
53
|
+
ctx: Ctx,
|
|
54
|
+
registryFile: string,
|
|
55
|
+
legacyRegistryFile?: string,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
const { change, clearProgress, echo, progress, verbose } = ctx.services.logger;
|
|
58
|
+
const registered = registryHasDocks(registryFile);
|
|
59
|
+
const adoptable =
|
|
60
|
+
!registered && legacyRegistryFile !== undefined && registryHasDocks(legacyRegistryFile);
|
|
61
|
+
|
|
62
|
+
if (ctx.dryRun) {
|
|
63
|
+
if (adoptable) {
|
|
64
|
+
echo(
|
|
65
|
+
ctx.skipPluginRefresh === true
|
|
66
|
+
? "[dry-run] omp plugin marketplace list"
|
|
67
|
+
: `[dry-run] omp plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
68
|
+
);
|
|
69
|
+
} else if (registered) verbose("omp docks marketplace already registered");
|
|
70
|
+
else echo(`[dry-run] omp plugin marketplace add ${MARKETPLACE_SOURCE}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// syncPlugins owns the single skip message for both missing CLIs, and it runs
|
|
75
|
+
// right after this pass. Without omp no marketplace command can run at all,
|
|
76
|
+
// and without git a marketplace clone cannot resolve, so both are deliberate
|
|
77
|
+
// skips: return silently instead of spawning and recording a failure.
|
|
78
|
+
if (ctx.services.deps.probe("omp").state === "missing") return;
|
|
79
|
+
if (ctx.services.deps.probe("git").state === "missing") return;
|
|
80
|
+
|
|
81
|
+
if (!registered && !adoptable) {
|
|
82
|
+
progress("Registering omp docks marketplace...");
|
|
83
|
+
const result = await spawnProcess("omp", ["plugin", "marketplace", "add", MARKETPLACE_SOURCE], {
|
|
84
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
85
|
+
});
|
|
86
|
+
clearProgress();
|
|
87
|
+
if (result.error === undefined && result.exitCode === 0) {
|
|
88
|
+
change("omp docks marketplace registered");
|
|
89
|
+
} else {
|
|
90
|
+
recordFailure(
|
|
91
|
+
ctx,
|
|
92
|
+
`omp docks marketplace registration failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace add ${MARKETPLACE_SOURCE}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (ctx.skipPluginRefresh === true) {
|
|
99
|
+
if (!adoptable) {
|
|
100
|
+
verbose("omp docks marketplace already registered; refresh-only update skipped");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Adoption only needs omp to resolve the registry path. `marketplace list`
|
|
105
|
+
// does that and fetches nothing, so it stays inside the flag's contract
|
|
106
|
+
// while leaving the active registry present.
|
|
107
|
+
progress("Adopting omp docks marketplace registry...");
|
|
108
|
+
const listed = await spawnProcess("omp", ["plugin", "marketplace", "list"], {
|
|
109
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
110
|
+
});
|
|
111
|
+
clearProgress();
|
|
112
|
+
if (listed.error === undefined && listed.exitCode === 0) {
|
|
113
|
+
change("omp docks marketplace registry adopted; refresh-only update skipped");
|
|
114
|
+
} else {
|
|
115
|
+
recordFailure(
|
|
116
|
+
ctx,
|
|
117
|
+
`omp docks marketplace adoption failed: ${firstOutputLine(listed)}; run manually: omp plugin marketplace list`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
progress("Updating omp docks marketplace...");
|
|
124
|
+
const result = await spawnProcess("omp", ["plugin", "marketplace", "update", MARKETPLACE_NAME], {
|
|
125
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
126
|
+
});
|
|
127
|
+
clearProgress();
|
|
128
|
+
if (result.error === undefined && result.exitCode === 0) {
|
|
129
|
+
verbose("omp docks marketplace refreshed");
|
|
130
|
+
} else {
|
|
131
|
+
recordFailure(
|
|
132
|
+
ctx,
|
|
133
|
+
`omp docks marketplace update failed: ${firstOutputLine(result)}; run manually: omp plugin marketplace update ${MARKETPLACE_NAME}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|