@uniqbit/mate-core 0.15.0-canary.2 → 0.15.0-canary.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/package.json +1 -1
- package/src/cli/commands/cap/index.ts +6 -3
- package/src/cli/commands/launch/claude.ts +2 -23
- package/src/cli/commands/launch/opencode.ts +2 -23
- package/src/cli/commands/launch/shared.ts +25 -0
- package/src/cli/commands/update.ts +21 -8
- package/src/cli/main.ts +11 -3
- package/src/cli/plugin-commands.ts +104 -0
- package/src/create-mate.ts +1 -1
- package/src/distribution.ts +9 -1
- package/src/framework.ts +0 -3
- package/src/index.ts +3 -0
- package/src/lib/fs-utils.ts +16 -0
- package/src/lib/opencode-plugin-package.ts +5 -3
- package/src/lib/orchestrator/adapters/base.ts +1 -17
- package/src/lib/orchestrator/launcher.ts +0 -15
- package/src/lib/public-npm.ts +39 -10
- package/src/lib/update-checker.ts +12 -3
- package/src/templates/providers/opencode/opencode.json +3 -4
- package/src/tools/setup/capabilities/graphify.ts +2 -14
- package/src/tools/setup/capabilities/headroom.ts +2 -15
- package/src/tools/setup/package-managers/uv.ts +14 -0
- package/src/tools/setup/plugin.ts +17 -0
- package/src/tools/setup/utils.ts +1 -13
- package/src/lib/lcov-to-cobertura.ts +0 -174
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { runGraphifyCapCommand } from "./graphify";
|
|
|
2
2
|
import { runHeadroomCapCommand } from "./headroom";
|
|
3
3
|
import { runOpenspecCapCommand } from "./openspec";
|
|
4
4
|
import { runIndexCapCommand } from "./index-cmd";
|
|
5
|
+
import { runPluginCliCommand } from "../../plugin-commands";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @command mate cap <capability> [...args]
|
|
@@ -9,9 +10,10 @@ import { runIndexCapCommand } from "./index-cmd";
|
|
|
9
10
|
* `headroom`, `openspec` (each forwards `[...args]` to the underlying CLI tool,
|
|
10
11
|
* scoped to the resolved companion/working repo), or `index` (re-syncs
|
|
11
12
|
* capability indexes such as tokensave/graphify after code changes; `sync` is
|
|
12
|
-
* accepted as a compatibility alias for `index`).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* accepted as a compatibility alias for `index`). Names not claimed by the
|
|
14
|
+
* framework dispatch to plugin-declared CLI commands as
|
|
15
|
+
* `mate cap <namespace> <command>`. Prints usage and sets a non-zero exit code
|
|
16
|
+
* when no capability name or an unknown one is given.
|
|
15
17
|
*/
|
|
16
18
|
export async function runCapCommand(
|
|
17
19
|
capabilityName: string | undefined,
|
|
@@ -40,6 +42,7 @@ export async function runCapCommand(
|
|
|
40
42
|
await runIndexCapCommand(args);
|
|
41
43
|
return;
|
|
42
44
|
default:
|
|
45
|
+
if (await runPluginCliCommand(capabilityName, args[0], args.slice(1))) return;
|
|
43
46
|
process.stderr.write(`mate: unknown capability command: ${capabilityName}\n`);
|
|
44
47
|
process.exitCode = 1;
|
|
45
48
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ensureUnambiguousCompanion,
|
|
3
|
-
parseDirectLaunchArgs,
|
|
4
|
-
parseLaunchArgs,
|
|
5
|
-
runLaunchToolCommand,
|
|
6
|
-
} from "./shared";
|
|
1
|
+
import { makeLaunchCommand } from "./shared";
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* @command mate claude [-- ...agentArgs]
|
|
@@ -17,20 +12,4 @@ import {
|
|
|
17
12
|
* alias), all args before `--` are treated as agent args rather than being
|
|
18
13
|
* parsed as launch options.
|
|
19
14
|
*/
|
|
20
|
-
export
|
|
21
|
-
args: string[],
|
|
22
|
-
options: { directPassthrough?: boolean } = {},
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
const parsed = options.directPassthrough ? parseDirectLaunchArgs(args) : parseLaunchArgs(args);
|
|
25
|
-
if (!parsed) return;
|
|
26
|
-
|
|
27
|
-
if (!(await ensureUnambiguousCompanion())) {
|
|
28
|
-
process.exitCode = 1;
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
await runLaunchToolCommand("claude", parsed.agentArgs, {
|
|
33
|
-
skipConfirmation: !!process.stdin.isTTY,
|
|
34
|
-
skipGit: parsed.skipGit,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
15
|
+
export const runLaunchClaudeCommand = makeLaunchCommand("claude");
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ensureUnambiguousCompanion,
|
|
3
|
-
parseDirectLaunchArgs,
|
|
4
|
-
parseLaunchArgs,
|
|
5
|
-
runLaunchToolCommand,
|
|
6
|
-
} from "./shared";
|
|
1
|
+
import { makeLaunchCommand } from "./shared";
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* @command mate opencode [-- ...agentArgs]
|
|
@@ -17,20 +12,4 @@ import {
|
|
|
17
12
|
* opencode` alias), all args before `--` are treated as agent args rather
|
|
18
13
|
* than being parsed as launch options.
|
|
19
14
|
*/
|
|
20
|
-
export
|
|
21
|
-
args: string[],
|
|
22
|
-
options: { directPassthrough?: boolean } = {},
|
|
23
|
-
): Promise<void> {
|
|
24
|
-
const parsed = options.directPassthrough ? parseDirectLaunchArgs(args) : parseLaunchArgs(args);
|
|
25
|
-
if (!parsed) return;
|
|
26
|
-
|
|
27
|
-
if (!(await ensureUnambiguousCompanion())) {
|
|
28
|
-
process.exitCode = 1;
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
await runLaunchToolCommand("opencode", parsed.agentArgs, {
|
|
33
|
-
skipConfirmation: !!process.stdin.isTTY,
|
|
34
|
-
skipGit: parsed.skipGit,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
15
|
+
export const runLaunchOpenCodeCommand = makeLaunchCommand("opencode");
|
|
@@ -128,3 +128,28 @@ export async function runLaunchToolCommand(
|
|
|
128
128
|
throw error;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Builds the `mate <tool>` command handler: parses launch args (direct
|
|
134
|
+
* passthrough or flagged), guards against ambiguous companions, then launches
|
|
135
|
+
* via {@link runLaunchToolCommand} with confirmation skipped in a TTY.
|
|
136
|
+
*/
|
|
137
|
+
export function makeLaunchCommand(tool: LaunchTarget) {
|
|
138
|
+
return async function runLaunchCommand(
|
|
139
|
+
args: string[],
|
|
140
|
+
options: { directPassthrough?: boolean } = {},
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const parsed = options.directPassthrough ? parseDirectLaunchArgs(args) : parseLaunchArgs(args);
|
|
143
|
+
if (!parsed) return;
|
|
144
|
+
|
|
145
|
+
if (!(await ensureUnambiguousCompanion())) {
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await runLaunchToolCommand(tool, parsed.agentArgs, {
|
|
151
|
+
skipConfirmation: !!process.stdin.isTTY,
|
|
152
|
+
skipGit: parsed.skipGit,
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -7,20 +7,30 @@ import {
|
|
|
7
7
|
warmOpenCodePluginCache,
|
|
8
8
|
} from "../../lib/opencode-plugin-package";
|
|
9
9
|
import { installPublicPackageSync, isPackageInstalledAtNpmGlobalRoot } from "../../lib/public-npm";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
fetchLatestVersion,
|
|
12
|
+
getCurrentVersion,
|
|
13
|
+
getUpdateConfig,
|
|
14
|
+
isNewer,
|
|
15
|
+
} from "../../lib/update-checker";
|
|
11
16
|
import { confirmPrompt } from "../../lib/components/confirm-prompt";
|
|
12
17
|
|
|
13
18
|
type InstallResult = ReturnType<typeof installPublicPackageSync>;
|
|
14
19
|
|
|
15
20
|
function writeNpmOnlyRecoveryMessage(): void {
|
|
21
|
+
const { packageName, registry } = getUpdateConfig();
|
|
16
22
|
process.stderr.write(
|
|
17
23
|
`${frameworkConfig.name}: self-update is only supported for npm-installed Mate.\n`,
|
|
18
24
|
);
|
|
25
|
+
if (packageName.startsWith("@uniqbit/")) {
|
|
26
|
+
process.stderr.write(
|
|
27
|
+
"If you previously configured the legacy GitLab registry override, remove it first:\n",
|
|
28
|
+
);
|
|
29
|
+
process.stderr.write(" npm config delete @uniqbit:registry --global\n");
|
|
30
|
+
}
|
|
19
31
|
process.stderr.write(
|
|
20
|
-
|
|
32
|
+
`Then reinstall with: npm install -g ${packageName} --registry ${registry}\n`,
|
|
21
33
|
);
|
|
22
|
-
process.stderr.write(" npm config delete @uniqbit:registry --global\n");
|
|
23
|
-
process.stderr.write(`Then reinstall with: npm install -g @uniqbit/${frameworkConfig.name}\n`);
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
export const updateCommandDeps = {
|
|
@@ -31,10 +41,12 @@ export const updateCommandDeps = {
|
|
|
31
41
|
isNpmManagedInstall: () =>
|
|
32
42
|
isPackageInstalledAtNpmGlobalRoot(
|
|
33
43
|
path.resolve(import.meta.dirname, "../../.."),
|
|
34
|
-
|
|
44
|
+
getUpdateConfig().packageName,
|
|
35
45
|
),
|
|
36
|
-
installLatest: (latest: string): InstallResult =>
|
|
37
|
-
|
|
46
|
+
installLatest: (latest: string): InstallResult => {
|
|
47
|
+
const { packageName, registry } = getUpdateConfig();
|
|
48
|
+
return installPublicPackageSync(`${packageName}@${latest}`, registry);
|
|
49
|
+
},
|
|
38
50
|
saveUpdateState: async (latest: string) => {
|
|
39
51
|
const { UpdateStateStore } = await import("../../lib/update-checker");
|
|
40
52
|
const store = new UpdateStateStore();
|
|
@@ -51,7 +63,8 @@ export const updateCommandDeps = {
|
|
|
51
63
|
const args = [entrypoint, "install", ...(skipConfirm ? ["--yes"] : [])];
|
|
52
64
|
return spawnSync(process.execPath, args, { stdio: "inherit" }) as InstallResult;
|
|
53
65
|
},
|
|
54
|
-
warmOpenCodePluginCache: (latest: string) =>
|
|
66
|
+
warmOpenCodePluginCache: (latest: string) =>
|
|
67
|
+
warmOpenCodePluginCache(latest, process.env, getUpdateConfig().registry),
|
|
55
68
|
};
|
|
56
69
|
|
|
57
70
|
/**
|
package/src/cli/main.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { runUpdateCommand } from "./commands/update";
|
|
|
16
16
|
import { runInstallCommand } from "./commands/install";
|
|
17
17
|
import { inspectInstallPreflight } from "../lib/install";
|
|
18
18
|
import { ensureUnambiguousCompanion } from "./commands/shared/companion-selection";
|
|
19
|
+
import { findPluginCliCommand } from "./plugin-commands";
|
|
19
20
|
import { usage } from "./usage";
|
|
20
21
|
|
|
21
22
|
export function isInstallRecoveryCommand(command?: string, subcommand?: string): boolean {
|
|
@@ -48,9 +49,16 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// Plugin commands (`mate cap <namespace> <command>`) own their stdout (an
|
|
53
|
+
// MCP server speaks JSON-RPC over it), so banners and background chatter
|
|
54
|
+
// are suppressed for them.
|
|
55
|
+
const isPluginCommand =
|
|
56
|
+
command === "cap" && findPluginCliCommand(subcommand, rest[0]) !== undefined;
|
|
57
|
+
if (!isPluginCommand) {
|
|
58
|
+
const updateStore = new UpdateStateStore();
|
|
59
|
+
await showUpdateBannerIfAvailable(updateStore);
|
|
60
|
+
scheduleBackgroundCheck(updateStore);
|
|
61
|
+
}
|
|
54
62
|
|
|
55
63
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
56
64
|
console.log(usage());
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { getActiveDistribution } from "../distribution";
|
|
2
|
+
import { resolveInstallContext } from "../lib/install";
|
|
3
|
+
import type { FrameworkConfig } from "../lib/orchestrator/types";
|
|
4
|
+
import type { Plugin, PluginCliCommand } from "../tools/setup/plugin";
|
|
5
|
+
|
|
6
|
+
function namespaceOf(plugin: Plugin): string {
|
|
7
|
+
return plugin.cliNamespace ?? plugin.id;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function pluginForNamespace(namespace: string): Plugin | undefined {
|
|
11
|
+
return getActiveDistribution()
|
|
12
|
+
.registry.getAll()
|
|
13
|
+
.find((candidate) => namespaceOf(candidate) === namespace);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dispatch `<distribution> cap <namespace> <name>` to a plugin-declared CLI
|
|
18
|
+
* command. Returns true when the namespace belongs to a plugin that declares
|
|
19
|
+
* CLI commands (the invocation is then fully handled here, including
|
|
20
|
+
* unknown-subcommand errors); false when no plugin claims the namespace so
|
|
21
|
+
* the caller can report an unknown command. Framework `cap` subcommands are
|
|
22
|
+
* dispatched before this is consulted, so they can never be shadowed.
|
|
23
|
+
*
|
|
24
|
+
* Commands are mounted independently of plugin selection: the MCP descriptors
|
|
25
|
+
* written during setup must stay launchable even when enablement is evaluated
|
|
26
|
+
* lazily. Commands guard their own preconditions.
|
|
27
|
+
*/
|
|
28
|
+
export async function runPluginCliCommand(
|
|
29
|
+
namespace: string,
|
|
30
|
+
commandName: string | undefined,
|
|
31
|
+
argv: string[],
|
|
32
|
+
): Promise<boolean> {
|
|
33
|
+
const plugin = pluginForNamespace(namespace);
|
|
34
|
+
const commands = plugin?.cliCommands;
|
|
35
|
+
if (!plugin || !commands || commands.length === 0) return false;
|
|
36
|
+
|
|
37
|
+
const command = findPluginCliCommand(namespace, commandName);
|
|
38
|
+
if (!command) {
|
|
39
|
+
console.error(
|
|
40
|
+
commandName
|
|
41
|
+
? `Unknown command: cap ${namespace} ${commandName}`
|
|
42
|
+
: `Missing subcommand for: cap ${namespace}`,
|
|
43
|
+
);
|
|
44
|
+
console.error(usageFor(plugin));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await command.run(argv);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Look up a plugin-declared CLI command without running it. Used by `main` to
|
|
55
|
+
* detect plugin-owned invocations early — plugin commands own their stdout
|
|
56
|
+
* (e.g. an MCP server speaking JSON-RPC), so the CLI must not print banners
|
|
57
|
+
* before dispatching to one.
|
|
58
|
+
*/
|
|
59
|
+
export function findPluginCliCommand(
|
|
60
|
+
namespace: string | undefined,
|
|
61
|
+
commandName: string | undefined,
|
|
62
|
+
): PluginCliCommand | undefined {
|
|
63
|
+
if (!namespace || !commandName) return undefined;
|
|
64
|
+
return pluginForNamespace(namespace)?.cliCommands?.find(
|
|
65
|
+
(candidate) => candidate.name === commandName,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface EnsureCapabilityEnabledDeps {
|
|
70
|
+
loadConfig?: () => Promise<FrameworkConfig>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Guard for plugin CLI commands: verifies the owning plugin's capability is
|
|
75
|
+
* enabled in the resolved companion configuration. On failure it prints a
|
|
76
|
+
* message naming the capability and how to enable it, sets a non-zero exit
|
|
77
|
+
* code, and returns false so the command can bail out without side effects.
|
|
78
|
+
*/
|
|
79
|
+
export async function ensureCapabilityEnabled(
|
|
80
|
+
pluginId: string,
|
|
81
|
+
deps: EnsureCapabilityEnabledDeps = {},
|
|
82
|
+
): Promise<boolean> {
|
|
83
|
+
const distribution = getActiveDistribution();
|
|
84
|
+
const plugin = distribution.registry.getAll().find((candidate) => candidate.id === pluginId);
|
|
85
|
+
const loadConfig = deps.loadConfig ?? (async () => (await resolveInstallContext()).config);
|
|
86
|
+
if (plugin?.isEnabled(await loadConfig())) return true;
|
|
87
|
+
|
|
88
|
+
const name = distribution.config.name;
|
|
89
|
+
console.error(
|
|
90
|
+
`${name}: the "${pluginId}" capability is not enabled for this companion. ` +
|
|
91
|
+
`Enable it via \`${name} companion setup\`.`,
|
|
92
|
+
);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function usageFor(plugin: Plugin): string {
|
|
98
|
+
const distributionName = getActiveDistribution().config.name;
|
|
99
|
+
const namespace = namespaceOf(plugin);
|
|
100
|
+
const lines = (plugin.cliCommands ?? []).map(
|
|
101
|
+
(command) => ` ${distributionName} cap ${namespace} ${command.name} ${command.description}`,
|
|
102
|
+
);
|
|
103
|
+
return ["Available commands:", ...lines].join("\n");
|
|
104
|
+
}
|
package/src/create-mate.ts
CHANGED
|
@@ -26,9 +26,9 @@ export interface MateCli {
|
|
|
26
26
|
*/
|
|
27
27
|
export function createMate({ config, plugins }: CreateMateOptions): MateCli {
|
|
28
28
|
const registry = new PluginRegistry([
|
|
29
|
+
...plugins,
|
|
29
30
|
{ plugin: createBunPlugin(), policy: "required" },
|
|
30
31
|
{ plugin: createUvPlugin(), policy: "required" },
|
|
31
|
-
...plugins,
|
|
32
32
|
createGitignorePlugin(config.name),
|
|
33
33
|
]);
|
|
34
34
|
setActiveDistribution({ config, registry });
|
package/src/distribution.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { PluginRegistry } from "./tools/setup/registry";
|
|
2
2
|
|
|
3
|
+
export interface DistributionUpdateConfig {
|
|
4
|
+
/** npm package whose versions are checked and installed by `mate update`. */
|
|
5
|
+
packageName: string;
|
|
6
|
+
/** npm registry used for update checks and downloads. */
|
|
7
|
+
registry: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
/** Identity and version of the distribution assembled through `createMate`. */
|
|
4
11
|
export interface DistributionConfig {
|
|
5
12
|
name: string;
|
|
6
13
|
legacyNames?: string[];
|
|
7
14
|
runtime: "bun";
|
|
8
|
-
supportedAdapters: string[];
|
|
9
15
|
version: string;
|
|
10
16
|
/** Asset directories that override the framework defaults, highest precedence first. */
|
|
11
17
|
assetRoots?: string[];
|
|
18
|
+
/** Optional update source; defaults to `@uniqbit/<name>` on public npm. */
|
|
19
|
+
update?: DistributionUpdateConfig;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export interface ActiveDistribution {
|
package/src/framework.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -13,18 +13,21 @@ export {
|
|
|
13
13
|
setFallbackDistribution,
|
|
14
14
|
type ActiveDistribution,
|
|
15
15
|
type DistributionConfig,
|
|
16
|
+
type DistributionUpdateConfig,
|
|
16
17
|
} from "./distribution";
|
|
17
18
|
export {
|
|
18
19
|
normalizeRegistration,
|
|
19
20
|
PluginRegistry,
|
|
20
21
|
type NormalizedRegistration,
|
|
21
22
|
} from "./tools/setup/registry";
|
|
23
|
+
export { ensureCapabilityEnabled, type EnsureCapabilityEnabledDeps } from "./cli/plugin-commands";
|
|
22
24
|
export type {
|
|
23
25
|
CapabilityPlugin,
|
|
24
26
|
InstructionsService,
|
|
25
27
|
McpServerDescriptor,
|
|
26
28
|
McpService,
|
|
27
29
|
Plugin,
|
|
30
|
+
PluginCliCommand,
|
|
28
31
|
PluginPolicy,
|
|
29
32
|
PluginRegistration,
|
|
30
33
|
ProviderHosting,
|
package/src/lib/fs-utils.ts
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import { accessSync, constants } from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function isCommandOnPath(command: string, pathValue: string): boolean {
|
|
6
|
+
if (!pathValue) return false;
|
|
7
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
8
|
+
if (!dir) continue;
|
|
9
|
+
try {
|
|
10
|
+
accessSync(path.join(dir, command), constants.X_OK);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
// continue
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
export async function fileExists(filePath: string): Promise<boolean> {
|
|
4
20
|
try {
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
+
import { PUBLIC_NPM_REGISTRY } from "./public-npm";
|
|
6
7
|
import { getCurrentVersion } from "./update-checker";
|
|
7
8
|
|
|
8
9
|
export const OPENCODE_PLUGIN_PACKAGE_NAME = "@uniqbit/mate-opencode-plugin";
|
|
@@ -35,8 +36,8 @@ export interface WarmPluginCacheResult {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export const opencodePluginCacheDeps = {
|
|
38
|
-
runInstall: (cwd: string) =>
|
|
39
|
-
spawnSync("npm", ["install", "--no-audit", "--no-fund", "--silent"], {
|
|
39
|
+
runInstall: (cwd: string, registry: string) =>
|
|
40
|
+
spawnSync("npm", ["install", "--no-audit", "--no-fund", "--silent", "--registry", registry], {
|
|
40
41
|
cwd,
|
|
41
42
|
encoding: "utf8" as const,
|
|
42
43
|
stdio: ["ignore", "pipe", "pipe"] as const,
|
|
@@ -52,6 +53,7 @@ export const opencodePluginCacheDeps = {
|
|
|
52
53
|
export async function warmOpenCodePluginCache(
|
|
53
54
|
version: string = getCurrentVersion(),
|
|
54
55
|
env: NodeJS.ProcessEnv = process.env,
|
|
56
|
+
registry = PUBLIC_NPM_REGISTRY,
|
|
55
57
|
): Promise<WarmPluginCacheResult> {
|
|
56
58
|
// Escape hatch for tests and intentionally offline environments: skip the
|
|
57
59
|
// pre-fetch entirely instead of attempting a registry install.
|
|
@@ -70,7 +72,7 @@ export async function warmOpenCodePluginCache(
|
|
|
70
72
|
"utf8",
|
|
71
73
|
);
|
|
72
74
|
|
|
73
|
-
const result = opencodePluginCacheDeps.runInstall(specDir);
|
|
75
|
+
const result = opencodePluginCacheDeps.runInstall(specDir, registry);
|
|
74
76
|
if (result.error) {
|
|
75
77
|
return { ok: false, detail: result.error.message };
|
|
76
78
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { accessSync, constants } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
5
4
|
import { version } from "../../../../package.json";
|
|
5
|
+
import { isCommandOnPath } from "../../fs-utils";
|
|
6
6
|
import { frameworkConfig } from "../../../framework";
|
|
7
7
|
import { getReactDoctorBinPath, getWrapperBinPath } from "../../package-paths";
|
|
8
8
|
import {
|
|
@@ -229,19 +229,3 @@ export abstract class LaunchAdapter {
|
|
|
229
229
|
});
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
-
|
|
233
|
-
function isCommandOnPath(command: string, pathValue: string): boolean {
|
|
234
|
-
if (!pathValue) return false;
|
|
235
|
-
|
|
236
|
-
for (const dir of pathValue.split(path.delimiter)) {
|
|
237
|
-
if (!dir) continue;
|
|
238
|
-
try {
|
|
239
|
-
accessSync(path.join(dir, command), constants.X_OK);
|
|
240
|
-
return true;
|
|
241
|
-
} catch {
|
|
242
|
-
// continue
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
@@ -81,21 +81,6 @@ export class FrameworkLauncher {
|
|
|
81
81
|
return (await this.prepare(request)).execute();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
async getAllowedTargets(): Promise<Set<string> | null> {
|
|
85
|
-
try {
|
|
86
|
-
const { configStore, workingRepoStore, repositoryId, repository } =
|
|
87
|
-
await this.resolveConfig();
|
|
88
|
-
const config = await configStore.load();
|
|
89
|
-
const store = new CompanionStore(configStore, workingRepoStore);
|
|
90
|
-
const policy = repository
|
|
91
|
-
? resolvePolicyFromConfig(config, [repository], repository.id)
|
|
92
|
-
: await store.resolvePolicy(repositoryId);
|
|
93
|
-
return new Set(policy.allowedAgents);
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
84
|
private async resolveLaunchState(request: LaunchRequest): Promise<ResolvedLaunchState> {
|
|
100
85
|
const {
|
|
101
86
|
configStore,
|
package/src/lib/public-npm.ts
CHANGED
|
@@ -27,15 +27,33 @@ export const publicNpmDeps = {
|
|
|
27
27
|
rmSync: fs.rmSync,
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
function
|
|
31
|
-
|
|
30
|
+
function packageNameFromSpec(packageSpec: string): string {
|
|
31
|
+
const separator = packageSpec.startsWith("@")
|
|
32
|
+
? packageSpec.indexOf("@", 1)
|
|
33
|
+
: packageSpec.indexOf("@");
|
|
34
|
+
return separator === -1 ? packageSpec : packageSpec.slice(0, separator);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
function getPublicNpmUserConfig(packageName: string, registry: string): string {
|
|
38
|
+
const scope = packageName.startsWith("@") ? packageName.slice(0, packageName.indexOf("/")) : null;
|
|
39
|
+
return [`registry=${registry}`, ...(scope ? [`${scope}:registry=${registry}`] : []), ""].join(
|
|
40
|
+
"\n",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function withPublicNpmConfig<T>(
|
|
45
|
+
packageName: string,
|
|
46
|
+
registry: string,
|
|
47
|
+
run: (env: NodeJS.ProcessEnv) => Promise<T>,
|
|
48
|
+
): Promise<T> {
|
|
35
49
|
const tempDir = await publicNpmDeps.mkdtemp(path.join(os.tmpdir(), "mate-public-npm-"));
|
|
36
50
|
const userConfigPath = path.join(tempDir, ".npmrc");
|
|
37
51
|
|
|
38
|
-
await publicNpmDeps.writeFile(
|
|
52
|
+
await publicNpmDeps.writeFile(
|
|
53
|
+
userConfigPath,
|
|
54
|
+
getPublicNpmUserConfig(packageName, registry),
|
|
55
|
+
"utf8",
|
|
56
|
+
);
|
|
39
57
|
|
|
40
58
|
try {
|
|
41
59
|
return await run({
|
|
@@ -47,11 +65,19 @@ async function withPublicNpmConfig<T>(run: (env: NodeJS.ProcessEnv) => Promise<T
|
|
|
47
65
|
}
|
|
48
66
|
}
|
|
49
67
|
|
|
50
|
-
function withPublicNpmConfigSync<T>(
|
|
68
|
+
function withPublicNpmConfigSync<T>(
|
|
69
|
+
packageName: string,
|
|
70
|
+
registry: string,
|
|
71
|
+
run: (env: NodeJS.ProcessEnv) => T,
|
|
72
|
+
): T {
|
|
51
73
|
const tempDir = publicNpmDeps.mkdtempSync(path.join(os.tmpdir(), "mate-public-npm-"));
|
|
52
74
|
const userConfigPath = path.join(tempDir, ".npmrc");
|
|
53
75
|
|
|
54
|
-
publicNpmDeps.writeFileSync(
|
|
76
|
+
publicNpmDeps.writeFileSync(
|
|
77
|
+
userConfigPath,
|
|
78
|
+
getPublicNpmUserConfig(packageName, registry),
|
|
79
|
+
"utf8",
|
|
80
|
+
);
|
|
55
81
|
|
|
56
82
|
try {
|
|
57
83
|
return run({
|
|
@@ -63,8 +89,11 @@ function withPublicNpmConfigSync<T>(run: (env: NodeJS.ProcessEnv) => T): T {
|
|
|
63
89
|
}
|
|
64
90
|
}
|
|
65
91
|
|
|
66
|
-
export async function fetchPublicPackageVersion(
|
|
67
|
-
|
|
92
|
+
export async function fetchPublicPackageVersion(
|
|
93
|
+
packageName: string,
|
|
94
|
+
registry = PUBLIC_NPM_REGISTRY,
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
const { stdout } = await withPublicNpmConfig(packageName, registry, (env) =>
|
|
68
97
|
publicNpmDeps.execFile("npm", ["view", packageName, "version"], {
|
|
69
98
|
timeout: 10_000,
|
|
70
99
|
env,
|
|
@@ -74,8 +103,8 @@ export async function fetchPublicPackageVersion(packageName: string): Promise<st
|
|
|
74
103
|
return stdout.trim();
|
|
75
104
|
}
|
|
76
105
|
|
|
77
|
-
export function installPublicPackageSync(packageSpec: string) {
|
|
78
|
-
return withPublicNpmConfigSync((env) =>
|
|
106
|
+
export function installPublicPackageSync(packageSpec: string, registry = PUBLIC_NPM_REGISTRY) {
|
|
107
|
+
return withPublicNpmConfigSync(packageNameFromSpec(packageSpec), registry, (env) =>
|
|
79
108
|
publicNpmDeps.spawnSync("npm", ["install", "-g", packageSpec], {
|
|
80
109
|
stdio: "inherit",
|
|
81
110
|
env,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { getActiveDistribution } from "../distribution";
|
|
4
|
+
import { getActiveDistribution, type DistributionUpdateConfig } from "../distribution";
|
|
5
5
|
import { frameworkConfig } from "../framework";
|
|
6
|
-
import { fetchPublicPackageVersion } from "./public-npm";
|
|
6
|
+
import { fetchPublicPackageVersion, PUBLIC_NPM_REGISTRY } from "./public-npm";
|
|
7
7
|
import { YamlFileStore } from "./orchestrator/yaml-file-store";
|
|
8
8
|
|
|
9
9
|
const parse = (v: string): number[] => v.split(".").slice(0, 3).map(Number);
|
|
@@ -34,6 +34,14 @@ export function getCurrentVersion(): string {
|
|
|
34
34
|
return getActiveDistribution().config.version;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function getUpdateConfig(): DistributionUpdateConfig {
|
|
38
|
+
const { name, update } = getActiveDistribution().config;
|
|
39
|
+
return {
|
|
40
|
+
packageName: update?.packageName ?? `@uniqbit/${name}`,
|
|
41
|
+
registry: update?.registry ?? PUBLIC_NPM_REGISTRY,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
export function isCanaryVersion(version: string = getCurrentVersion()): boolean {
|
|
38
46
|
return version.includes("-canary");
|
|
39
47
|
}
|
|
@@ -62,7 +70,8 @@ export async function showUpdateBannerIfAvailable(store: UpdateStateStore): Prom
|
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
export async function fetchLatestVersion(): Promise<string> {
|
|
65
|
-
|
|
73
|
+
const { packageName, registry } = getUpdateConfig();
|
|
74
|
+
return fetchPublicPackageVersion(packageName, registry);
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
export function scheduleBackgroundCheck(store: UpdateStateStore): void {
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
"compaction": {
|
|
4
4
|
"auto": true,
|
|
5
5
|
"prune": true,
|
|
6
|
-
"
|
|
7
|
-
"reserved": 8000
|
|
6
|
+
"reserved": 15000
|
|
8
7
|
},
|
|
9
8
|
"tool_output": {
|
|
10
|
-
"max_lines":
|
|
11
|
-
"max_bytes":
|
|
9
|
+
"max_lines": 500,
|
|
10
|
+
"max_bytes": 20000
|
|
12
11
|
},
|
|
13
12
|
"references": {
|
|
14
13
|
"mate": ".."
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import fs from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
|
|
@@ -12,18 +11,7 @@ import {
|
|
|
12
11
|
} from "../utils";
|
|
13
12
|
import { confirm } from "../../../cli/confirm";
|
|
14
13
|
import { frameworkConfig } from "../../../framework";
|
|
15
|
-
|
|
16
|
-
function defaultIsInstalledViaUvTool(pkgName: string): boolean {
|
|
17
|
-
try {
|
|
18
|
-
const output = execFileSync("uv", ["tool", "list"], {
|
|
19
|
-
encoding: "utf-8",
|
|
20
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
-
});
|
|
22
|
-
return output.includes(pkgName);
|
|
23
|
-
} catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
14
|
+
import { isInstalledViaUvTool } from "../package-managers/uv";
|
|
27
15
|
|
|
28
16
|
// Storage contract: <companionPath>/.graphify/<repositoryId>/graphify-out/
|
|
29
17
|
export const GRAPHIFY_STORE_SEGMENT = ".graphify";
|
|
@@ -336,7 +324,7 @@ export interface GraphifyPluginDeps {
|
|
|
336
324
|
export function createGraphifyPlugin(deps: GraphifyPluginDeps = {}): CapabilityPlugin {
|
|
337
325
|
const askConfirm = deps.confirm ?? confirm;
|
|
338
326
|
const checkPath = deps.isCommandOnPath ?? isCommandOnPath;
|
|
339
|
-
const checkUvTool = deps.isInstalledViaUvTool ??
|
|
327
|
+
const checkUvTool = deps.isInstalledViaUvTool ?? isInstalledViaUvTool;
|
|
340
328
|
const runCommand = deps.runCommand ?? runShellCommand;
|
|
341
329
|
const runProviderInstall = deps.runProviderInstall ?? runCommandSilently;
|
|
342
330
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
|
|
3
1
|
import type { CapabilityPlugin, SetupContext } from "../plugin";
|
|
4
2
|
import type { InstallRequirement } from "../install-contract";
|
|
5
3
|
import { isCommandOnPath, runShellCommand, runShellCommandSilently } from "../utils";
|
|
6
4
|
import { confirm } from "../../../cli/confirm";
|
|
5
|
+
import { isInstalledViaUvTool } from "../package-managers/uv";
|
|
7
6
|
|
|
8
7
|
const HEADROOM_GITIGNORE_ENTRIES = ["# headroom", ".headroom/"];
|
|
9
8
|
|
|
@@ -32,18 +31,6 @@ const RTK_UNINSTALL_COMMANDS: Record<string, string> = {
|
|
|
32
31
|
opencode: "rtk init -g --uninstall --opencode",
|
|
33
32
|
};
|
|
34
33
|
|
|
35
|
-
function defaultIsInstalledViaUvTool(pkgName: string): boolean {
|
|
36
|
-
try {
|
|
37
|
-
const output = execFileSync("uv", ["tool", "list"], {
|
|
38
|
-
encoding: "utf-8",
|
|
39
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
-
});
|
|
41
|
-
return output.includes(pkgName);
|
|
42
|
-
} catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
34
|
type HeadroomDeps = {
|
|
48
35
|
confirm?: typeof confirm;
|
|
49
36
|
isCommandOnPath?: typeof isCommandOnPath;
|
|
@@ -57,7 +44,7 @@ type HeadroomDeps = {
|
|
|
57
44
|
export function createHeadroomPlugin(deps: HeadroomDeps = {}): CapabilityPlugin {
|
|
58
45
|
const askConfirm = deps.confirm ?? confirm;
|
|
59
46
|
const checkPath = deps.isCommandOnPath ?? isCommandOnPath;
|
|
60
|
-
const checkUvTool = deps.isInstalledViaUvTool ??
|
|
47
|
+
const checkUvTool = deps.isInstalledViaUvTool ?? isInstalledViaUvTool;
|
|
61
48
|
const checkRtkPath = deps.isRtkOnPath ?? defaultIsRtkOnPath;
|
|
62
49
|
const checkBrew = deps.isBrewAvailable ?? defaultIsBrewAvailable;
|
|
63
50
|
const runRtkInstall = deps.runRtkInstallCmd ?? runShellCommand;
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
1
3
|
import type { Plugin, SetupContext } from "../plugin";
|
|
2
4
|
import type { InstallRequirement } from "../install-contract";
|
|
3
5
|
import { confirm } from "../../../cli/confirm";
|
|
4
6
|
import { isCommandOnPath, runCommand, runShellCommand } from "../utils";
|
|
5
7
|
|
|
8
|
+
export function isInstalledViaUvTool(pkgName: string): boolean {
|
|
9
|
+
try {
|
|
10
|
+
const output = execFileSync("uv", ["tool", "list"], {
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
return output.includes(pkgName);
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
export interface PackageManagerSetupDeps {
|
|
7
21
|
confirm?: typeof confirm;
|
|
8
22
|
isCommandOnPath?: (command: string, pathValue: string) => boolean;
|
|
@@ -60,6 +60,20 @@ export interface ProviderPlugin extends Plugin {
|
|
|
60
60
|
hosting?: ProviderHosting;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A CLI subcommand contributed by a plugin. `createMate` mounts each entry on
|
|
65
|
+
* the distribution binary as `<distribution> cap <namespace> <name>` (the
|
|
66
|
+
* namespace defaults to the plugin id, overridable via `cliNamespace`),
|
|
67
|
+
* independent of plugin selection — commands guard their own preconditions
|
|
68
|
+
* (see `ensureCapabilityEnabled`). Framework `cap` subcommands always win
|
|
69
|
+
* over plugin namespaces.
|
|
70
|
+
*/
|
|
71
|
+
export interface PluginCliCommand {
|
|
72
|
+
name: string;
|
|
73
|
+
description: string;
|
|
74
|
+
run(argv: string[]): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
export interface Plugin {
|
|
64
78
|
id: string;
|
|
65
79
|
kind: "provider" | "capability" | "packageManager" | "integration" | "root";
|
|
@@ -72,6 +86,9 @@ export interface Plugin {
|
|
|
72
86
|
gitignoreEntries?(ctx: SetupContext): string[];
|
|
73
87
|
persistGitignoreEntries?: boolean;
|
|
74
88
|
getInstallRequirements?(ctx: InstallRequirementContext): InstallRequirement[];
|
|
89
|
+
cliCommands?: PluginCliCommand[];
|
|
90
|
+
/** CLI namespace for `cliCommands` under `<distribution> cap`; defaults to `id`. */
|
|
91
|
+
cliNamespace?: string;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
/**
|
package/src/tools/setup/utils.ts
CHANGED
|
@@ -35,19 +35,7 @@ export async function pruneEmptyAncestors(dir: string, stopAt: string): Promise<
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export
|
|
39
|
-
if (!pathValue) return false;
|
|
40
|
-
for (const dir of pathValue.split(path.delimiter)) {
|
|
41
|
-
if (!dir) continue;
|
|
42
|
-
try {
|
|
43
|
-
accessSync(path.join(dir, command), constants.X_OK);
|
|
44
|
-
return true;
|
|
45
|
-
} catch {
|
|
46
|
-
// continue
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
38
|
+
export { isCommandOnPath } from "../../lib/fs-utils";
|
|
51
39
|
|
|
52
40
|
export function resolveCommandOnPath(command: string, pathValue: string): string | undefined {
|
|
53
41
|
if (!pathValue) return undefined;
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
type CoverageLine = {
|
|
4
|
-
hits: number;
|
|
5
|
-
lineNumber: number;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type CoverageFile = {
|
|
9
|
-
filePath: string;
|
|
10
|
-
lines: CoverageLine[];
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type CoberturaOptions = {
|
|
14
|
-
projectRoot?: string;
|
|
15
|
-
timestamp?: number;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function escapeXml(value: string): string {
|
|
19
|
-
return value
|
|
20
|
-
.replaceAll("&", "&")
|
|
21
|
-
.replaceAll("<", "<")
|
|
22
|
-
.replaceAll(">", ">")
|
|
23
|
-
.replaceAll('"', """)
|
|
24
|
-
.replaceAll("'", "'");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function normalizeCoveragePath(filePath: string, projectRoot: string): string {
|
|
28
|
-
if (!path.isAbsolute(filePath)) {
|
|
29
|
-
return filePath.split(path.sep).join("/");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const relativePath = path.relative(projectRoot, filePath);
|
|
33
|
-
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
34
|
-
return relativePath.split(path.sep).join("/");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return filePath.split(path.sep).join("/");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function finalizeRecord(
|
|
41
|
-
records: CoverageFile[],
|
|
42
|
-
activePath: string | null,
|
|
43
|
-
activeLines: Map<number, number>,
|
|
44
|
-
projectRoot: string,
|
|
45
|
-
) {
|
|
46
|
-
if (!activePath) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const lines = [...activeLines.entries()]
|
|
51
|
-
.toSorted(([left], [right]) => left - right)
|
|
52
|
-
.map(([lineNumber, hits]) => ({ hits, lineNumber }));
|
|
53
|
-
|
|
54
|
-
records.push({
|
|
55
|
-
filePath: normalizeCoveragePath(activePath, projectRoot),
|
|
56
|
-
lines,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function parseLcov(lcov: string, projectRoot = process.cwd()): CoverageFile[] {
|
|
61
|
-
const records: CoverageFile[] = [];
|
|
62
|
-
let activePath: string | null = null;
|
|
63
|
-
let activeLines = new Map<number, number>();
|
|
64
|
-
|
|
65
|
-
for (const rawLine of lcov.split(/\r?\n/)) {
|
|
66
|
-
if (rawLine.startsWith("SF:")) {
|
|
67
|
-
finalizeRecord(records, activePath, activeLines, projectRoot);
|
|
68
|
-
activePath = rawLine.slice(3).trim();
|
|
69
|
-
activeLines = new Map<number, number>();
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (rawLine.startsWith("DA:")) {
|
|
74
|
-
const [lineNumberText, hitsText] = rawLine.slice(3).split(",");
|
|
75
|
-
const lineNumber = Number.parseInt(lineNumberText ?? "", 10);
|
|
76
|
-
const hits = Number.parseInt(hitsText ?? "", 10);
|
|
77
|
-
|
|
78
|
-
if (Number.isFinite(lineNumber) && Number.isFinite(hits)) {
|
|
79
|
-
activeLines.set(lineNumber, hits);
|
|
80
|
-
}
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (rawLine === "end_of_record") {
|
|
85
|
-
finalizeRecord(records, activePath, activeLines, projectRoot);
|
|
86
|
-
activePath = null;
|
|
87
|
-
activeLines = new Map<number, number>();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
finalizeRecord(records, activePath, activeLines, projectRoot);
|
|
92
|
-
return records;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function lcovToCobertura(
|
|
96
|
-
lcov: string,
|
|
97
|
-
{ projectRoot = process.cwd(), timestamp = Date.now() }: CoberturaOptions = {},
|
|
98
|
-
): string {
|
|
99
|
-
const files = parseLcov(lcov, projectRoot);
|
|
100
|
-
const packages = new Map<string, CoverageFile[]>();
|
|
101
|
-
|
|
102
|
-
for (const file of files) {
|
|
103
|
-
const packageName = path.posix.dirname(file.filePath);
|
|
104
|
-
const bucket = packages.get(packageName) ?? [];
|
|
105
|
-
bucket.push(file);
|
|
106
|
-
packages.set(packageName, bucket);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const totalLinesValid = files.reduce((sum, file) => sum + file.lines.length, 0);
|
|
110
|
-
const totalLinesCovered = files.reduce(
|
|
111
|
-
(sum, file) => sum + file.lines.filter((line) => line.hits > 0).length,
|
|
112
|
-
0,
|
|
113
|
-
);
|
|
114
|
-
const totalLineRate = totalLinesValid === 0 ? 0 : totalLinesCovered / totalLinesValid;
|
|
115
|
-
|
|
116
|
-
const packageXml = [...packages.entries()]
|
|
117
|
-
.toSorted(([left], [right]) => left.localeCompare(right))
|
|
118
|
-
.map(([packageName, packageFiles]) => {
|
|
119
|
-
const packageLinesValid = packageFiles.reduce((sum, file) => sum + file.lines.length, 0);
|
|
120
|
-
const packageLinesCovered = packageFiles.reduce(
|
|
121
|
-
(sum, file) => sum + file.lines.filter((line) => line.hits > 0).length,
|
|
122
|
-
0,
|
|
123
|
-
);
|
|
124
|
-
const packageLineRate = packageLinesValid === 0 ? 0 : packageLinesCovered / packageLinesValid;
|
|
125
|
-
|
|
126
|
-
const classesXml = packageFiles
|
|
127
|
-
.toSorted((left, right) => left.filePath.localeCompare(right.filePath))
|
|
128
|
-
.map((file) => {
|
|
129
|
-
const fileLinesCovered = file.lines.filter((line) => line.hits > 0).length;
|
|
130
|
-
const fileLineRate = file.lines.length === 0 ? 0 : fileLinesCovered / file.lines.length;
|
|
131
|
-
const linesXml = file.lines
|
|
132
|
-
.map(
|
|
133
|
-
(line) =>
|
|
134
|
-
` <line number="${line.lineNumber}" hits="${line.hits}" branch="false"/>`,
|
|
135
|
-
)
|
|
136
|
-
.join("\n");
|
|
137
|
-
|
|
138
|
-
return [
|
|
139
|
-
` <class name="${escapeXml(path.posix.basename(file.filePath))}" filename="${escapeXml(file.filePath)}" line-rate="${fileLineRate.toFixed(4)}" branch-rate="0" complexity="0">`,
|
|
140
|
-
" <methods/>",
|
|
141
|
-
" <lines>",
|
|
142
|
-
linesXml,
|
|
143
|
-
" </lines>",
|
|
144
|
-
" </class>",
|
|
145
|
-
].join("\n");
|
|
146
|
-
})
|
|
147
|
-
.join("\n");
|
|
148
|
-
|
|
149
|
-
return [
|
|
150
|
-
` <package name="${escapeXml(packageName)}" line-rate="${packageLineRate.toFixed(4)}" branch-rate="0" complexity="0">`,
|
|
151
|
-
" <classes>",
|
|
152
|
-
classesXml,
|
|
153
|
-
" </classes>",
|
|
154
|
-
" </package>",
|
|
155
|
-
].join("\n");
|
|
156
|
-
})
|
|
157
|
-
.join("\n");
|
|
158
|
-
|
|
159
|
-
return [
|
|
160
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
161
|
-
'<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">',
|
|
162
|
-
`<coverage lines-covered="${totalLinesCovered}" lines-valid="${totalLinesValid}" line-rate="${totalLineRate.toFixed(4)}" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0" version="mate-lcov-to-cobertura" timestamp="${Math.floor(
|
|
163
|
-
timestamp / 1000,
|
|
164
|
-
)}">`,
|
|
165
|
-
" <sources>",
|
|
166
|
-
" <source>.</source>",
|
|
167
|
-
" </sources>",
|
|
168
|
-
" <packages>",
|
|
169
|
-
packageXml,
|
|
170
|
-
" </packages>",
|
|
171
|
-
"</coverage>",
|
|
172
|
-
"",
|
|
173
|
-
].join("\n");
|
|
174
|
-
}
|