@brainervirus/workit-core 2.9.0 → 3.0.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/README.md +2 -1
- package/package.json +1 -1
- package/scripts/bundled-deps.json +1 -7
- package/scripts/install-codex-plugin.sh +1 -2
- package/scripts/install-cursor-plugin.sh +3 -4
- package/scripts/install-opencode-plugin.sh +2 -3
- package/scripts/install-pi-package.sh +2 -3
- package/scripts/sync-runtime.sh +0 -13
- package/src/core/init.ts +1 -1
- package/src/core/support-matrix.ts +4 -4
- package/src/core/workspace-branch-policy.ts +123 -0
- package/src/hooks/hosts/opencode.ts +2 -2
- package/scripts/doctor-check.ts +0 -37
- package/src/core/config-conversion.ts +0 -180
- package/src/core/cutover.ts +0 -1789
- package/src/core/detect-hosts.ts +0 -115
- package/src/core/doctor.ts +0 -1882
- package/src/core/host-install.ts +0 -617
- package/src/core/legacy-ownership.ts +0 -47
- package/src/core/registration.ts +0 -306
- package/src/core/setup-state.ts +0 -61
- package/src/core/setup.ts +0 -1582
- package/src/core/uninstall.ts +0 -544
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@ Adapters map host-native surfaces to `src/core/`; they never re-implement core l
|
|
|
30
30
|
## Exports
|
|
31
31
|
|
|
32
32
|
- Main entry: `./src/core.ts`.
|
|
33
|
-
- Subpath exports: `./src/*.ts` (e.g. `@brainervirus/workit-core/src/core/
|
|
33
|
+
- Subpath exports: `./src/*.ts` (e.g. `@brainervirus/workit-core/src/core/task-store`), plus `./package.json`.
|
|
34
|
+
- Setup, doctor, upgrade, uninstall and host-install code lives in `@brainervirus/workit-cli` (`src/admin/`), not here, so host hooks and plugins never bundle it.
|
|
34
35
|
|
|
35
36
|
## Package scripts
|
|
36
37
|
|
package/package.json
CHANGED
|
@@ -56,13 +56,7 @@
|
|
|
56
56
|
"yoga-layout",
|
|
57
57
|
"zod"
|
|
58
58
|
],
|
|
59
|
-
"workit-opencode": [
|
|
60
|
-
"@openclaw/fs-safe",
|
|
61
|
-
"@opencode-ai/plugin",
|
|
62
|
-
"@opencode-ai/plugin/zod",
|
|
63
|
-
"@opencode/plugin",
|
|
64
|
-
"zod"
|
|
65
|
-
],
|
|
59
|
+
"workit-opencode": ["@openclaw/fs-safe", "@opencode/plugin", "zod"],
|
|
66
60
|
"workit-cursor": [
|
|
67
61
|
"@modelcontextprotocol/sdk",
|
|
68
62
|
"@openclaw/fs-safe",
|
|
@@ -19,10 +19,9 @@ fi
|
|
|
19
19
|
mkdir -p "$PLUGIN_DIR"
|
|
20
20
|
rsync -a --delete "$SOURCE/packages/workit-codex/" "$PLUGIN_DIR/"
|
|
21
21
|
|
|
22
|
-
if ! bun "$ROOT/packages/workit-
|
|
22
|
+
if ! bun "$ROOT/packages/workit-cli/scripts/doctor-check.ts" cli; then
|
|
23
23
|
echo "FATAL: post-install doctor found problems after Codex plugin sync" >&2
|
|
24
24
|
exit 1
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
27
|
echo "Codex plugin installed at $PLUGIN_DIR"
|
|
28
|
-
echo "Complete an explicit v1 cutover before treating this as a generation switch."
|
|
@@ -50,7 +50,7 @@ fi
|
|
|
50
50
|
# The check reads only workit-owned files, so a healthy install is byte-untouched;
|
|
51
51
|
# a registry-unreachable comparison reports registry_unreachable, never stale
|
|
52
52
|
# (CA-04) — no false stale_install and no install failure.
|
|
53
|
-
if bun "$ROOT/packages/workit-
|
|
53
|
+
if bun "$ROOT/packages/workit-cli/scripts/doctor-check.ts" cursor --stale; then
|
|
54
54
|
:
|
|
55
55
|
elif [ $? -eq 2 ]; then
|
|
56
56
|
echo "workit: stale Cursor plugin install detected — self-healing (refresh + canonical re-registration)" >&2
|
|
@@ -70,7 +70,7 @@ if [ -d "$SKILLS_DIR" ]; then
|
|
|
70
70
|
fi
|
|
71
71
|
|
|
72
72
|
PLUGIN_DIR="$HOME/.cursor/plugins/local/workit"
|
|
73
|
-
REGISTRATION_TS="$ROOT/packages/workit-
|
|
73
|
+
REGISTRATION_TS="$ROOT/packages/workit-cli/src/admin/registration.ts"
|
|
74
74
|
CURSOR_SETTINGS="$HOME/.cursor/settings.json" CURSOR_MCP="$HOME/.cursor/mcp.json" PLUGIN_DIR="$PLUGIN_DIR" REGISTRATION_TS="$REGISTRATION_TS" LOCAL_DIST="$LOCAL_DIST" bun -e '
|
|
75
75
|
import fs from "node:fs";
|
|
76
76
|
import path from "node:path";
|
|
@@ -120,7 +120,7 @@ if (localDist) {
|
|
|
120
120
|
'
|
|
121
121
|
|
|
122
122
|
# DG-09: verify the just-written Cursor registration with the shared offline doctor.
|
|
123
|
-
if ! bun "$ROOT/packages/workit-
|
|
123
|
+
if ! bun "$ROOT/packages/workit-cli/scripts/doctor-check.ts" cursor; then
|
|
124
124
|
echo "FATAL: post-install doctor found an unhealthy Cursor registration" >&2
|
|
125
125
|
exit 1
|
|
126
126
|
fi
|
|
@@ -140,6 +140,5 @@ if [ -d "$LEGACY_PLUGIN_DIR" ] && [ "$LEGACY_PLUGIN_DIR" != "$PLUGIN_DIR" ]; the
|
|
|
140
140
|
fi
|
|
141
141
|
|
|
142
142
|
echo "Cursor plugin installed + auto-sync enabled (sessionStart)."
|
|
143
|
-
echo "Generation switches require an explicit cutover preview/apply — install alone is not a cutover."
|
|
144
143
|
echo "Share: $SHARE"
|
|
145
144
|
ls "$HOME/.cursor/plugins/local/workit/skills" | grep '^wk-' || true
|
|
@@ -38,7 +38,7 @@ mkdir -p "$(dirname "$CONFIG")"
|
|
|
38
38
|
|
|
39
39
|
# Merge/dedupe registrations via the shared core helper (RR-06): one dev pin,
|
|
40
40
|
# no legacy/current duplicates, unrelated user settings preserved.
|
|
41
|
-
CONFIG_PATH="$CONFIG" PIN_PATH="$PIN" REGISTRATION_TS="$ROOT/packages/workit-
|
|
41
|
+
CONFIG_PATH="$CONFIG" PIN_PATH="$PIN" REGISTRATION_TS="$ROOT/packages/workit-cli/src/admin/registration.ts" bun -e '
|
|
42
42
|
import fs from "node:fs";
|
|
43
43
|
const { mergeOpenCodeConfig } = await import(process.env.REGISTRATION_TS!);
|
|
44
44
|
const path = process.env.CONFIG_PATH!;
|
|
@@ -71,10 +71,9 @@ if [ ! -s "$PLUGIN_ENTRY" ]; then
|
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
73
|
# DG-09: verify the just-written registration with the shared offline doctor.
|
|
74
|
-
if ! bun "$ROOT/packages/workit-
|
|
74
|
+
if ! bun "$ROOT/packages/workit-cli/scripts/doctor-check.ts" opencode; then
|
|
75
75
|
echo "FATAL: post-install doctor found an unhealthy OpenCode registration" >&2
|
|
76
76
|
exit 1
|
|
77
77
|
fi
|
|
78
78
|
|
|
79
79
|
echo "OpenCode install done. Fully quit all opencode processes, then restart."
|
|
80
|
-
echo "Generation switches require an explicit cutover preview/apply — install alone is not a cutover."
|
|
@@ -19,8 +19,8 @@ fi
|
|
|
19
19
|
# pi 0.85.1 loads extensions from the settings `packages` list, not from
|
|
20
20
|
# ~/.pi/config.json below. Register the absolute package dir there (honoring
|
|
21
21
|
# PI_CODING_AGENT_DIR) so `pi list` picks it up; the config.json write stays
|
|
22
|
-
# because uninstall
|
|
23
|
-
# those
|
|
22
|
+
# because uninstall and detection still key pi state off it — migrating
|
|
23
|
+
# those two off config.json is a separate contract change.
|
|
24
24
|
AGENT_DIR="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
25
25
|
SETTINGS="$AGENT_DIR/settings.json"
|
|
26
26
|
mkdir -p "$AGENT_DIR"
|
|
@@ -53,4 +53,3 @@ console.log("Registered Pi extension:", process.env.EXT);
|
|
|
53
53
|
'
|
|
54
54
|
|
|
55
55
|
echo "Pi package registered in $SETTINGS (and legacy $PI_CONFIG)"
|
|
56
|
-
echo "Complete an explicit v1 cutover before treating this as a generation switch."
|
package/scripts/sync-runtime.sh
CHANGED
|
@@ -153,19 +153,6 @@ fi
|
|
|
153
153
|
# Remove broken TLA live-loader if present (OpenCode ignored it; /wk-* vanished)
|
|
154
154
|
rm -f "${OPENCODE_PLUGINS}/workflow-toolkit.ts"
|
|
155
155
|
|
|
156
|
-
# Ensure OpenCode has plugin peer dep
|
|
157
|
-
PKG="${HOME}/.config/opencode/package.json"
|
|
158
|
-
if [ -f "$PKG" ]; then
|
|
159
|
-
PKG_PATH="$PKG" "$BUN_BIN" -e '
|
|
160
|
-
import fs from "node:fs";
|
|
161
|
-
const path = process.env.PKG_PATH!;
|
|
162
|
-
const data = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
163
|
-
data.dependencies = data.dependencies ?? {};
|
|
164
|
-
data.dependencies["@opencode-ai/plugin"] = data.dependencies["@opencode-ai/plugin"] ?? "1.18.30";
|
|
165
|
-
fs.writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
166
|
-
' || true
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
156
|
# Drop bun package cache so old github/file installs cannot shadow file:// plugin.ts
|
|
170
157
|
rm -rf "${HOME}/.cache/opencode/packages/workflow-toolkit-opencode@"* 2>/dev/null || true
|
|
171
158
|
|
package/src/core/init.ts
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { configDir, isConfigObject } from "./config";
|
|
5
5
|
import { writeFileExclusive } from "./safe-write";
|
|
6
|
-
import { applyWorkspaceBranchPolicy } from "./
|
|
6
|
+
import { applyWorkspaceBranchPolicy } from "./workspace-branch-policy";
|
|
7
7
|
import { youTrackTokenCreateUrl } from "./youtrack";
|
|
8
8
|
|
|
9
9
|
const TOKEN_PLACEHOLDER = "YOUR_TOKEN_HERE";
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
export const SUPPORT_MATRIX = {
|
|
8
8
|
bun: "1.4.1",
|
|
9
9
|
node: { minimum: "24", current: "24.20.0" },
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
opencode: { minimum: "
|
|
10
|
+
// OpenCode V2 plugin API only: the entry exports `setup()` (Workit 3.0
|
|
11
|
+
// retired the V1 `server()` adapter), so 1.x hosts cannot load it. 2.0.18 is
|
|
12
|
+
// the `@opencode/plugin` contract the adapter is built and tested against.
|
|
13
|
+
opencode: { minimum: "2.0.18", current: "2.0.18" },
|
|
14
14
|
// Codex is a qualification host, not a runtime dependency: the CLI version
|
|
15
15
|
// below is the one live qualification evidence covers. The doctor warns when
|
|
16
16
|
// an installed CLI drifts ahead so a fresh install never silently outruns
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { detectBranchPolicy } from "./branch-policy";
|
|
5
|
+
import { configDir } from "./config";
|
|
6
|
+
import {
|
|
7
|
+
readWorkspacesResult,
|
|
8
|
+
resolveWorkspaceFrom,
|
|
9
|
+
validateWorkspaceGlob,
|
|
10
|
+
validateWorkspacesDocument,
|
|
11
|
+
workspacesRevision,
|
|
12
|
+
type WorkspaceConfig,
|
|
13
|
+
} from "./workspaces";
|
|
14
|
+
|
|
15
|
+
const readFileSafe = (p: string): string | null => {
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(p, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The one proposal→write path for a workspace branch policy, shared by the
|
|
24
|
+
// OpenCode `workit_init_apply` branch_policy action (init.ts) and the CLI
|
|
25
|
+
// wizard, so both produce byte-identical workspaces.json writes.
|
|
26
|
+
export function applyWorkspaceBranchPolicy(opts: {
|
|
27
|
+
workspace_root: string;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
}): Record<string, any> {
|
|
30
|
+
const { workspace_root, env = process.env } = opts;
|
|
31
|
+
const dir = path.join(env.WORKFLOW_TOOLKIT_CONFIG ?? configDir());
|
|
32
|
+
const {
|
|
33
|
+
status,
|
|
34
|
+
path: wsPath,
|
|
35
|
+
entries,
|
|
36
|
+
document,
|
|
37
|
+
revision,
|
|
38
|
+
error: workspacesError,
|
|
39
|
+
} = readWorkspacesResult(dir);
|
|
40
|
+
if (status === "malformed" || status === "invalid")
|
|
41
|
+
return { ok: false, error: workspacesError ?? `invalid workspaces.json: ${wsPath}` };
|
|
42
|
+
const detection = detectBranchPolicy(workspace_root);
|
|
43
|
+
const name = String(env.WORKFLOW_BP_NAME ?? path.basename(workspace_root));
|
|
44
|
+
const integration = (env.WORKFLOW_BP_INTEGRATION ?? detection.integration) as "pr" | "merge";
|
|
45
|
+
const policy = {
|
|
46
|
+
preset: detection.preset,
|
|
47
|
+
developBranch: env.WORKFLOW_BP_DEVELOP ?? detection.developBranch ?? undefined,
|
|
48
|
+
prefixes: detection.prefixes,
|
|
49
|
+
allowed: detection.allowed,
|
|
50
|
+
protected: detection.protected,
|
|
51
|
+
integration,
|
|
52
|
+
};
|
|
53
|
+
const glob = `${workspace_root.replace(/[\\/]+$/, "")}/**`;
|
|
54
|
+
if (!validateWorkspaceGlob(glob).ok)
|
|
55
|
+
return { ok: false, error: `invalid workspace glob: ${glob}` };
|
|
56
|
+
let existing: WorkspaceConfig | null;
|
|
57
|
+
try {
|
|
58
|
+
existing = resolveWorkspaceFrom(workspace_root, dir);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const selectedName = env.WORKFLOW_BP_NAME?.trim();
|
|
61
|
+
if (!selectedName)
|
|
62
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
63
|
+
try {
|
|
64
|
+
existing = resolveWorkspaceFrom(workspace_root, dir, selectedName);
|
|
65
|
+
} catch (selectionError) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: selectionError instanceof Error ? selectionError.message : String(selectionError),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (existing?.branchPolicy && isDeepStrictEqual(existing.branchPolicy, policy)) {
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
status: "already-configured",
|
|
76
|
+
workspace: existing,
|
|
77
|
+
policy,
|
|
78
|
+
config_path: wsPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const next = existing
|
|
82
|
+
? entries.map((w) => (w.name === existing?.name ? { ...w, branchPolicy: policy } : w))
|
|
83
|
+
: [...entries, { name, glob, branchPolicy: policy }];
|
|
84
|
+
if (!existing && entries.some((entry) => entry.name === name))
|
|
85
|
+
return { ok: false, error: `workspace name ${JSON.stringify(name)} is already configured` };
|
|
86
|
+
const proposed = validateWorkspacesDocument({ ...document, workspaces: next }, wsPath);
|
|
87
|
+
if (proposed.status !== "valid") return { ok: false, error: proposed.error };
|
|
88
|
+
if (workspacesRevision(readFileSafe(wsPath)) !== revision)
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: `workspaces.json changed while applying the branch policy: ${wsPath}`,
|
|
92
|
+
};
|
|
93
|
+
mkdirSync(path.dirname(wsPath), { recursive: true });
|
|
94
|
+
const tmp = `${wsPath}.${process.pid}.tmp`;
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(tmp, JSON.stringify({ ...document, workspaces: next }, null, 2) + "\n", "utf8");
|
|
97
|
+
if (workspacesRevision(readFileSafe(wsPath)) !== revision) {
|
|
98
|
+
rmSync(tmp, { force: true });
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: `workspaces.json changed while applying the branch policy: ${wsPath}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
renameSync(tmp, wsPath);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
try {
|
|
107
|
+
rmSync(tmp, { force: true });
|
|
108
|
+
} catch {}
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: `failed to write ${wsPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
status: existing ? "updated" : "configured",
|
|
117
|
+
workspace: existing
|
|
118
|
+
? { ...existing, branchPolicy: policy }
|
|
119
|
+
: { name, glob, branchPolicy: policy },
|
|
120
|
+
policy,
|
|
121
|
+
config_path: wsPath,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -4,8 +4,8 @@ import type { HostDescriptor } from "../descriptor";
|
|
|
4
4
|
export const OPENCODE_DESCRIPTOR: HostDescriptor = {
|
|
5
5
|
host: "opencode",
|
|
6
6
|
label: "OpenCode",
|
|
7
|
-
verifiedAgainst: "@opencode
|
|
8
|
-
docs: ["https://opencode.ai/docs/plugins/"],
|
|
7
|
+
verifiedAgainst: "@opencode/plugin 2.0.18",
|
|
8
|
+
docs: ["https://opencode.ai/v2/docs/build/plugins/"],
|
|
9
9
|
transport: "in-process-plugin",
|
|
10
10
|
events: {
|
|
11
11
|
// No separate start event: context is injected on every agent-loop call.
|
package/scripts/doctor-check.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// DG-09: installers run the shared offline doctor after writing configuration and
|
|
3
|
-
// return nonzero when a required registration/config check fails. `installer:
|
|
4
|
-
// true` limits the enforced checks to the ones the installer just guaranteed, so
|
|
5
|
-
// a dev/stub checkout without built assets does not fail a post-install run.
|
|
6
|
-
// CA-02: `cursor --stale` is the installer's self-heal pre-check — it exits 2
|
|
7
|
-
// when the doctor's stale_install check fails and 0 otherwise. A
|
|
8
|
-
// registry-unreachable comparison warns as registry_unreachable and is NOT
|
|
9
|
-
// stale (CA-04): no false stale_install and no install failure. Other check
|
|
10
|
-
// failures are left to the post-install gate.
|
|
11
|
-
import { runDoctor } from "../src/core/doctor";
|
|
12
|
-
|
|
13
|
-
const hostArg = process.argv[2];
|
|
14
|
-
const host = hostArg === "cursor" ? "cursor" : hostArg === "cli" ? "cli" : "opencode";
|
|
15
|
-
const staleOnly = process.argv.includes("--stale");
|
|
16
|
-
const report = runDoctor({ host, installer: true });
|
|
17
|
-
|
|
18
|
-
if (staleOnly) {
|
|
19
|
-
const stale = report.checks.find((c) => c.id === "stale_install");
|
|
20
|
-
if (stale?.status === "fail") {
|
|
21
|
-
process.stderr.write(`STALE: ${stale.detail}\n`);
|
|
22
|
-
if (stale.fix) process.stderr.write(` fix: ${stale.fix}\n`);
|
|
23
|
-
process.exit(2);
|
|
24
|
-
}
|
|
25
|
-
process.exit(0);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (report.exitCode !== 0) {
|
|
29
|
-
for (const check of report.checks) {
|
|
30
|
-
if (check.status === "fail") {
|
|
31
|
-
process.stderr.write(`FATAL: workit doctor ${check.id}: ${check.detail}\n`);
|
|
32
|
-
if (check.fix) process.stderr.write(` fix: ${check.fix}\n`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
process.exit(0);
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { isConfigObject, mergePreset, type BranchPreset, type ToolkitConfig } from "./config";
|
|
5
|
-
import { writeFileAtomic } from "./safe-write";
|
|
6
|
-
|
|
7
|
-
export type ConversionInput = {
|
|
8
|
-
configDir: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type ConversionMapping = {
|
|
12
|
-
key: string;
|
|
13
|
-
from: unknown;
|
|
14
|
-
to: unknown;
|
|
15
|
-
note?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type ConversionUnresolved = {
|
|
19
|
-
key: string;
|
|
20
|
-
reason: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type ConversionPreview = {
|
|
24
|
-
mappings: ConversionMapping[];
|
|
25
|
-
preserved: string[];
|
|
26
|
-
unresolved: ConversionUnresolved[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const REDACT_KEYS = /token|secret|password|credential/i;
|
|
30
|
-
|
|
31
|
-
const readJsonObject = (file: string): Record<string, unknown> | null => {
|
|
32
|
-
if (!existsSync(file)) return null;
|
|
33
|
-
try {
|
|
34
|
-
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
35
|
-
return isConfigObject(parsed) ? parsed : null;
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const redactValue = (key: string, value: unknown): unknown => {
|
|
42
|
-
if (REDACT_KEYS.test(key)) return "[redacted]";
|
|
43
|
-
if (typeof value === "string" && value.length > 8 && !value.includes(" ")) return "[redacted]";
|
|
44
|
-
return value;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const redactConversionPreview = (preview: ConversionPreview): ConversionPreview => ({
|
|
48
|
-
mappings: preview.mappings.map((m) => ({
|
|
49
|
-
...m,
|
|
50
|
-
from: redactValue(m.key, m.from),
|
|
51
|
-
to: redactValue(m.key, m.to),
|
|
52
|
-
})),
|
|
53
|
-
preserved: preview.preserved,
|
|
54
|
-
unresolved: preview.unresolved,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
export function previewConversion(input: ConversionInput): ConversionPreview {
|
|
58
|
-
const configFile = path.join(input.configDir, "config.json");
|
|
59
|
-
const config = readJsonObject(configFile);
|
|
60
|
-
const mappings: ConversionMapping[] = [];
|
|
61
|
-
const unresolved: ConversionUnresolved[] = [];
|
|
62
|
-
const preserved: string[] = [];
|
|
63
|
-
|
|
64
|
-
if (config) {
|
|
65
|
-
if (config.locale !== undefined) {
|
|
66
|
-
mappings.push({ key: "locale", from: config.locale, to: config.locale });
|
|
67
|
-
}
|
|
68
|
-
if (config.branchPolicy !== undefined && isConfigObject(config.branchPolicy)) {
|
|
69
|
-
const bp = config.branchPolicy as ToolkitConfig["branchPolicy"] & {
|
|
70
|
-
allowed?: string[];
|
|
71
|
-
protected?: string[];
|
|
72
|
-
};
|
|
73
|
-
const preset = bp.preset ?? "gitflow";
|
|
74
|
-
const merged = mergePreset(
|
|
75
|
-
preset,
|
|
76
|
-
{
|
|
77
|
-
allowed: Array.isArray(bp.allowed) ? bp.allowed : undefined,
|
|
78
|
-
protectedNames: Array.isArray(bp.protected) ? bp.protected : undefined,
|
|
79
|
-
},
|
|
80
|
-
{ branchPolicy: bp },
|
|
81
|
-
);
|
|
82
|
-
mappings.push({
|
|
83
|
-
key: "branchPolicy",
|
|
84
|
-
from: config.branchPolicy,
|
|
85
|
-
to: merged,
|
|
86
|
-
note: "preset-derived allowed/protected reset through mergePreset",
|
|
87
|
-
});
|
|
88
|
-
if (preset === "custom" && Array.isArray(bp.allowed)) {
|
|
89
|
-
if (bp.allowed.includes("*")) {
|
|
90
|
-
unresolved.push({
|
|
91
|
-
key: "branchPolicy.allowed",
|
|
92
|
-
reason: 'unsupported permissive value "*" requires an explicit v1 choice',
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (config.workflowMode !== undefined) {
|
|
98
|
-
unresolved.push({
|
|
99
|
-
key: "legacyWorkflowMode",
|
|
100
|
-
reason: "0.x workflow mode has no automatic v1 equivalent; choose how to continue",
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
for (const name of ["youtrack.json", "vcs.json", "workspaces.json"]) {
|
|
106
|
-
const file = path.join(input.configDir, name);
|
|
107
|
-
if (!existsSync(file)) continue;
|
|
108
|
-
const parsed = readJsonObject(file);
|
|
109
|
-
if (parsed)
|
|
110
|
-
mappings.push({ key: name, from: parsed, to: parsed, note: "carry forward unchanged" });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
for (const token of ["youtrack.token", "gitlab.token", "github.token"]) {
|
|
114
|
-
const file = path.join(input.configDir, token);
|
|
115
|
-
if (existsSync(file)) preserved.push(file);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return { mappings, preserved, unresolved };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export const conversionDigest = (preview: ConversionPreview): string =>
|
|
122
|
-
createHash("sha256")
|
|
123
|
-
.update(JSON.stringify(redactConversionPreview(preview)))
|
|
124
|
-
.digest("hex");
|
|
125
|
-
|
|
126
|
-
export type ConversionApplyResult = {
|
|
127
|
-
configPath: string;
|
|
128
|
-
choicesPath: string;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/** Apply approved conversion mappings and record explicit resolution choices. */
|
|
132
|
-
export function applyConversionConfig(
|
|
133
|
-
configDir: string,
|
|
134
|
-
preview: ConversionPreview,
|
|
135
|
-
resolutions: Record<string, string>,
|
|
136
|
-
): ConversionApplyResult {
|
|
137
|
-
const configFile = path.join(configDir, "config.json");
|
|
138
|
-
const raw = readJsonObject(configFile) ?? {};
|
|
139
|
-
const next: Record<string, unknown> = { ...raw };
|
|
140
|
-
|
|
141
|
-
for (const mapping of preview.mappings) {
|
|
142
|
-
if (mapping.key === "locale") {
|
|
143
|
-
next[mapping.key] = mapping.to;
|
|
144
|
-
}
|
|
145
|
-
if (mapping.key === "branchPolicy" && !resolutions["branchPolicy.allowed"]) {
|
|
146
|
-
next.branchPolicy = mapping.to;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (resolutions["branchPolicy.allowed"]) {
|
|
151
|
-
const bp = isConfigObject(next.branchPolicy)
|
|
152
|
-
? (next.branchPolicy as ToolkitConfig["branchPolicy"])
|
|
153
|
-
: { preset: "custom" as BranchPreset, allowed: [], protected: [] };
|
|
154
|
-
const preset = bp.preset ?? "custom";
|
|
155
|
-
next.branchPolicy = mergePreset(
|
|
156
|
-
preset,
|
|
157
|
-
{
|
|
158
|
-
allowed: resolutions["branchPolicy.allowed"]
|
|
159
|
-
.split(",")
|
|
160
|
-
.map((s) => s.trim())
|
|
161
|
-
.filter(Boolean),
|
|
162
|
-
},
|
|
163
|
-
{ branchPolicy: bp },
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (resolutions["legacyWorkflowMode"]) {
|
|
168
|
-
delete next.workflowMode;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
writeFileAtomic(configFile, JSON.stringify(next, null, 2) + "\n");
|
|
172
|
-
|
|
173
|
-
const choicesPath = path.join(configDir, "cutover-choices.json");
|
|
174
|
-
writeFileAtomic(
|
|
175
|
-
choicesPath,
|
|
176
|
-
JSON.stringify({ resolutions, applied_at: new Date().toISOString() }, null, 2) + "\n",
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return { configPath: configFile, choicesPath };
|
|
180
|
-
}
|