@uniqbit/mate-core 0.18.3-canary.0 → 0.18.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/claude-plugin/hooks/hooks.json +10 -1
- package/claude-plugin/hooks/session-banner.mjs +4 -3
- package/claude-plugin/hooks/session-prepare.mjs +9 -0
- package/opencode-plugin/src/companion.ts +47 -12
- package/opencode-plugin/src/server.ts +39 -4
- package/package.json +3 -3
- package/src/cli/commands/artifact/finish/openspec.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +60 -21
- package/src/cli/commands/companion/link.ts +16 -11
- package/src/cli/commands/companion/list.ts +4 -5
- package/src/cli/commands/companion/open.ts +4 -5
- package/src/cli/commands/companion/tui.ts +1 -1
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/report/renderer.ts +9 -19
- package/src/cli/commands/session/prepare.ts +119 -0
- package/src/cli/commands/shared/companion-selection.ts +2 -2
- package/src/cli/commands/studio/reports.ts +0 -1
- package/src/cli/commands/studio/server.ts +6 -2
- package/src/cli/commands/unwrap.ts +28 -3
- package/src/cli/commands/wrap.ts +53 -59
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/main.ts +17 -0
- package/src/cli/usage.ts +3 -1
- package/src/hooks/session-banner.ts +19 -10
- package/src/hooks/session-prepare.ts +101 -0
- package/src/lib/fs-utils.ts +13 -0
- package/src/lib/orchestrator/adapters/base.ts +43 -8
- package/src/lib/orchestrator/adapters/claude.ts +130 -29
- package/src/lib/orchestrator/adapters/opencode.ts +17 -19
- package/src/lib/orchestrator/companion-git-sync.ts +28 -1
- package/src/lib/orchestrator/companion-hub.ts +2 -1
- package/src/lib/orchestrator/config-store.ts +56 -2
- package/src/lib/orchestrator/framework-context.ts +5 -29
- package/src/lib/orchestrator/launcher.ts +36 -171
- package/src/lib/orchestrator/projection-claude-entry.ts +17 -6
- package/src/lib/orchestrator/projection-claude-skills.ts +54 -22
- package/src/lib/orchestrator/projection-entries.ts +78 -11
- package/src/lib/orchestrator/projection-opencode-copies.ts +230 -0
- package/src/lib/orchestrator/projection-record.ts +7 -1
- package/src/lib/orchestrator/projection-runtime-documents.ts +83 -130
- package/src/lib/orchestrator/projection-types.ts +38 -33
- package/src/lib/orchestrator/repository-link.ts +473 -0
- package/src/lib/orchestrator/root-context.ts +6 -2
- package/src/lib/orchestrator/session-prepare.ts +425 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +3 -1
- package/src/lib/orchestrator/types.ts +4 -1
- package/src/lib/orchestrator/working-repo-projection.ts +71 -159
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/orchestrator/wrap-pass.ts +131 -0
- package/src/lib/runtime-directory-refresh.ts +186 -0
- package/src/lib/update-checker.ts +6 -0
- package/src/opencode/companion-hooks.ts +24 -132
- package/src/opencode/companion-policy.ts +26 -10
- package/src/opencode/index.ts +1 -7
- package/src/opencode/session-notes.ts +76 -0
- package/src/opencode/session-prepare.ts +139 -0
- package/src/opencode/tui.tsx +56 -9
- package/src/runtime/claude-delivery-plan.ts +73 -0
- package/src/runtime/claude-hook-group.ts +21 -0
- package/src/runtime/companion-lock.ts +121 -0
- package/src/runtime/companion-sync.ts +66 -9
- package/src/runtime/runtime-directory.ts +43 -0
- package/src/runtime/runtime-document-manifest.ts +335 -0
- package/src/runtime/session-prepare-runner.ts +148 -0
- package/src/runtime/wrap-state.ts +90 -0
- package/src/runtime/write-atomic.ts +27 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +2 -1
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +5 -0
- package/src/templates/capabilities/react-doctor/mate-react-doctor/SKILL.md +84 -0
- package/src/templates/capabilities/react-doctor/{skill → mate-react-doctor}/references/explain.md +18 -15
- package/src/templates/mate-skills/agents/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +1 -1
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +45 -29
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +1 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +850 -499
- package/src/tools/setup/capabilities/react-doctor.ts +34 -50
- package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -1
- package/src/tools/setup/engine.ts +15 -4
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/providers/claude-format.ts +11 -18
- package/src/tools/setup/providers/claude-plugin-hooks.ts +61 -24
- package/src/tools/setup/providers/claude.ts +23 -6
- package/src/tools/setup/runtime-documents.ts +152 -32
- package/src/tools/setup/working-repo-local-state.ts +82 -13
- package/src/tools/setup.ts +2 -2
- package/src/lib/orchestrator/companion-registry-reader.ts +0 -37
- package/src/lib/orchestrator/companion-resolver.ts +0 -179
- package/src/lib/orchestrator/companion-store.ts +0 -94
- package/src/lib/orchestrator/repo-local-registry.ts +0 -143
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +0 -144
- package/src/templates/capabilities/react-doctor/skill/SKILL.md +0 -51
- package/src/templates/report-assets/README.md +0 -32
- package/src/templates/report-assets/mermaid.LICENSE +0 -21
- package/src/templates/report-assets/mermaid.min.js +0 -4376
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
|
-
import { CompanionRegistryStore } from "./companion-registry-store";
|
|
6
|
-
import { ConfigStore } from "./config-store";
|
|
7
|
-
import { pathIsDirectory, writeRepoLocalRegistryEntry } from "./repo-local-registry";
|
|
8
|
-
import { type CompanionSource, ConfigError, type LinkedRepository } from "./types";
|
|
9
|
-
|
|
10
|
-
/** Builds a {@link CompanionStore} whose config/registry files live under `<companionPath>/.mate/config`, regardless of `process.cwd()`. */
|
|
11
|
-
export function companionRootedStore(companionPath: string): CompanionStore {
|
|
12
|
-
const configDir = path.join(path.resolve(companionPath), `.${FRAMEWORK_NAME}`, "config");
|
|
13
|
-
return new CompanionStore(
|
|
14
|
-
new ConfigStore(path.join(configDir, "framework.yaml")),
|
|
15
|
-
new CompanionRegistryStore(path.join(configDir, "registry.yaml")),
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class CompanionStore {
|
|
20
|
-
constructor(
|
|
21
|
-
private readonly configStore = new ConfigStore(),
|
|
22
|
-
private readonly companionRegistryStore = new CompanionRegistryStore(),
|
|
23
|
-
) {}
|
|
24
|
-
|
|
25
|
-
async registerRepository(
|
|
26
|
-
repository: LinkedRepository,
|
|
27
|
-
options: { companionSource?: CompanionSource } = {},
|
|
28
|
-
): Promise<LinkedRepository> {
|
|
29
|
-
const stats = await fs.stat(repository.path).catch(() => null);
|
|
30
|
-
if (!stats?.isDirectory()) {
|
|
31
|
-
throw new ConfigError(
|
|
32
|
-
`Linked repository path must exist and be a directory: ${repository.path}`,
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// A companion's registry.yaml may not exist yet (its first-ever link):
|
|
37
|
-
// that is not a real error here, unlike a working repo reading its own
|
|
38
|
-
// missing config, so fall back to an empty registry instead of throwing.
|
|
39
|
-
const working = await this.companionRegistryStore.load().catch((error) => {
|
|
40
|
-
if (error instanceof ConfigError) return CompanionRegistryStore.defaultConfig();
|
|
41
|
-
throw error;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// Prune dead entries (path no longer a directory) as part of this same
|
|
45
|
-
// write, piggybacking on the load/save that registration already does
|
|
46
|
-
// rather than adding a separate scan; never runs on a read-only list.
|
|
47
|
-
const checked = await Promise.all(
|
|
48
|
-
working.repos.map(async (repo) => ({ repo, exists: await pathIsDirectory(repo.path) })),
|
|
49
|
-
);
|
|
50
|
-
const survivors: LinkedRepository[] = [];
|
|
51
|
-
for (const entry of checked) {
|
|
52
|
-
if (entry.exists) survivors.push(entry.repo);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const existing = survivors.findIndex((r) => r.id === repository.id);
|
|
56
|
-
if (existing >= 0) {
|
|
57
|
-
survivors[existing] = repository;
|
|
58
|
-
} else {
|
|
59
|
-
survivors.push(repository);
|
|
60
|
-
}
|
|
61
|
-
working.repos = survivors;
|
|
62
|
-
await this.companionRegistryStore.save(working);
|
|
63
|
-
|
|
64
|
-
// Repo-local registry write is a cache, not the source of truth: the
|
|
65
|
-
// companion-side write above already succeeded, so a failure here (e.g. a
|
|
66
|
-
// read-only working tree) must not fail the overall link operation.
|
|
67
|
-
try {
|
|
68
|
-
const companionRoot = path.resolve(path.dirname(this.configStore.configPath), "..", "..");
|
|
69
|
-
await writeRepoLocalRegistryEntry(
|
|
70
|
-
repository.path,
|
|
71
|
-
companionRoot,
|
|
72
|
-
repository,
|
|
73
|
-
options.companionSource ?? "git",
|
|
74
|
-
);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
-
console.error(
|
|
78
|
-
`mate: warning: failed to write repo-local registry for ${repository.path}: ${message}`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return repository;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async getRepository(repositoryId: string): Promise<LinkedRepository | undefined> {
|
|
86
|
-
const working = await this.companionRegistryStore.load();
|
|
87
|
-
return working.repos.find((r) => r.id === repositoryId);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async listRepositories(): Promise<LinkedRepository[]> {
|
|
91
|
-
const working = await this.companionRegistryStore.load();
|
|
92
|
-
return working.repos;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { parse } from "yaml";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
ancestorDirectories,
|
|
8
|
-
repoLocalDirName,
|
|
9
|
-
repoLocalRegistryPath,
|
|
10
|
-
type RepoLocalRegistryFile,
|
|
11
|
-
} from "../../runtime/repo-local";
|
|
12
|
-
import { firstFailure } from "./projection-types";
|
|
13
|
-
import { normalizeLinkedRepository } from "./repo-local-store";
|
|
14
|
-
import { project } from "./working-repo-projection";
|
|
15
|
-
import type { RepoLocalRegistry } from "./repo-local-store";
|
|
16
|
-
import type { CompanionSource, LinkedRepository } from "./types";
|
|
17
|
-
|
|
18
|
-
export {
|
|
19
|
-
repoLocalDirPath,
|
|
20
|
-
repoLocalFrameworkPath,
|
|
21
|
-
repoLocalRegistryPath,
|
|
22
|
-
} from "../../runtime/repo-local";
|
|
23
|
-
export { pathIsDirectory } from "../fs-utils";
|
|
24
|
-
export {
|
|
25
|
-
RepoLocalRegistryStore,
|
|
26
|
-
upsertRepoLocalCompanionPointer,
|
|
27
|
-
writeRepoLocalFrameworkConfig,
|
|
28
|
-
type RepoLocalCompanionPointer,
|
|
29
|
-
type RepoLocalRegistry,
|
|
30
|
-
} from "./repo-local-store";
|
|
31
|
-
|
|
32
|
-
const repoLocalScanSkipDirNames = () =>
|
|
33
|
-
new Set([
|
|
34
|
-
".git",
|
|
35
|
-
repoLocalDirName(),
|
|
36
|
-
"node_modules",
|
|
37
|
-
"dist",
|
|
38
|
-
"build",
|
|
39
|
-
".next",
|
|
40
|
-
".turbo",
|
|
41
|
-
".venv",
|
|
42
|
-
"venv",
|
|
43
|
-
"target",
|
|
44
|
-
".cache",
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Records a Repository Link. Deliberately writes no projection file: wrapping is
|
|
49
|
-
* an explicit act, which is why the scope and not the inputs picks the entries.
|
|
50
|
-
* Throws on the first failed entry — this caller's own callers warn on it, so
|
|
51
|
-
* the owner's per-entry best-effort report must not silently become success.
|
|
52
|
-
*/
|
|
53
|
-
export async function writeRepoLocalRegistryEntry(
|
|
54
|
-
repoPath: string,
|
|
55
|
-
companionPath: string,
|
|
56
|
-
repository: LinkedRepository,
|
|
57
|
-
source: CompanionSource,
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
const { outcomes } = await project("link", { repoPath, companionPath, repository, source });
|
|
60
|
-
const failure = firstFailure(outcomes);
|
|
61
|
-
if (failure) throw failure.error ?? new Error(`failed to project ${failure.path}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function findRepoLocalLinkedRepository(cwd: string): Promise<LinkedRepository | null> {
|
|
65
|
-
const found = await findRepoLocalRegistryFile(cwd);
|
|
66
|
-
if (!found) return null;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const raw = await fs.readFile(found.registryPath, "utf8");
|
|
70
|
-
const parsed = parse(raw) as Partial<RepoLocalRegistry> | null;
|
|
71
|
-
if (!parsed?.repository) return null;
|
|
72
|
-
return normalizeLinkedRepository(parsed.repository);
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Walks up from `cwd` looking for the nearest ancestor holding a repo-local registry file. */
|
|
79
|
-
export async function findRepoLocalRegistryFile(
|
|
80
|
-
cwd: string,
|
|
81
|
-
): Promise<RepoLocalRegistryFile | null> {
|
|
82
|
-
for (const dir of ancestorDirectories(cwd)) {
|
|
83
|
-
const candidate = repoLocalRegistryPath(dir);
|
|
84
|
-
try {
|
|
85
|
-
if ((await fs.stat(candidate)).isFile()) return { repoRoot: dir, registryPath: candidate };
|
|
86
|
-
} catch {
|
|
87
|
-
// keep walking up
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Finds repo-local registries strictly beneath `rootPath`, skipping noisy directories. */
|
|
94
|
-
export async function findDescendantRepoLocalRegistries(rootPath: string): Promise<string[]> {
|
|
95
|
-
const resolvedRootPath = path.resolve(rootPath);
|
|
96
|
-
const pending = [resolvedRootPath];
|
|
97
|
-
const registryCandidates: Array<{ registryPath: string; shadowedPath: string }> = [];
|
|
98
|
-
|
|
99
|
-
while (pending.length > 0) {
|
|
100
|
-
const currentPath = pending.pop();
|
|
101
|
-
if (!currentPath) continue;
|
|
102
|
-
|
|
103
|
-
const entries = await (async () => {
|
|
104
|
-
try {
|
|
105
|
-
return await fs.readdir(currentPath, { withFileTypes: true });
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
})();
|
|
110
|
-
if (!entries) continue;
|
|
111
|
-
|
|
112
|
-
for (const entry of entries) {
|
|
113
|
-
if (!entry.isDirectory()) continue;
|
|
114
|
-
|
|
115
|
-
const entryPath = path.join(currentPath, entry.name);
|
|
116
|
-
if (entry.name === repoLocalDirName()) {
|
|
117
|
-
if (currentPath !== resolvedRootPath) {
|
|
118
|
-
registryCandidates.push({
|
|
119
|
-
registryPath: path.join(entryPath, "config", "registry.yaml"),
|
|
120
|
-
shadowedPath: currentPath,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (repoLocalScanSkipDirNames().has(entry.name)) continue;
|
|
126
|
-
|
|
127
|
-
pending.push(entryPath);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const shadowedPaths = await Promise.all(
|
|
132
|
-
registryCandidates.map(async ({ registryPath, shadowedPath }) => {
|
|
133
|
-
try {
|
|
134
|
-
const stats = await fs.stat(registryPath);
|
|
135
|
-
return stats.isFile() ? shadowedPath : null;
|
|
136
|
-
} catch {
|
|
137
|
-
// A missing or unreadable registry is not a shadowed link.
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
}),
|
|
141
|
-
);
|
|
142
|
-
return shadowedPaths.filter((candidate): candidate is string => candidate !== null).sort();
|
|
143
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -u
|
|
3
|
-
|
|
4
|
-
input_file=$(mktemp "${TMPDIR:-/tmp}/react-doctor-agent-hook.XXXXXX")
|
|
5
|
-
output_file=$(mktemp "${TMPDIR:-/tmp}/react-doctor-agent-hook-output.XXXXXX")
|
|
6
|
-
trap 'rm -f "$input_file" "$output_file"' EXIT
|
|
7
|
-
cat > "$input_file"
|
|
8
|
-
|
|
9
|
-
script_dir=$(CDPATH='' cd "$(dirname "$0")" && pwd)
|
|
10
|
-
project_root=${CLAUDE_PROJECT_DIR:-}
|
|
11
|
-
if [ -z "$project_root" ] && command -v node >/dev/null 2>&1; then
|
|
12
|
-
project_root=$(node - "$input_file" <<'NODE'
|
|
13
|
-
const fs = require('node:fs');
|
|
14
|
-
try {
|
|
15
|
-
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{}');
|
|
16
|
-
if (typeof input.cwd === 'string') process.stdout.write(input.cwd);
|
|
17
|
-
} catch {
|
|
18
|
-
// Fall back to the current working directory or the hook location below.
|
|
19
|
-
}
|
|
20
|
-
NODE
|
|
21
|
-
)
|
|
22
|
-
fi
|
|
23
|
-
if [ -z "$project_root" ]; then
|
|
24
|
-
project_root=${PWD:-}
|
|
25
|
-
fi
|
|
26
|
-
if [ -z "$project_root" ]; then
|
|
27
|
-
project_root=$(CDPATH='' cd "$script_dir/../.." && pwd)
|
|
28
|
-
fi
|
|
29
|
-
if ! cd "$project_root"; then
|
|
30
|
-
exit 0
|
|
31
|
-
fi
|
|
32
|
-
|
|
33
|
-
should_scan() {
|
|
34
|
-
if ! command -v node >/dev/null 2>&1; then
|
|
35
|
-
return 1
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
node - "$input_file" "$project_root" <<'NODE'
|
|
39
|
-
const crypto = require('node:crypto');
|
|
40
|
-
const { spawnSync } = require('node:child_process');
|
|
41
|
-
const fs = require('node:fs');
|
|
42
|
-
const os = require('node:os');
|
|
43
|
-
const path = require('node:path');
|
|
44
|
-
const inputPath = process.argv[2];
|
|
45
|
-
const projectRoot = process.argv[3];
|
|
46
|
-
const editToolNames = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ApplyPatch']);
|
|
47
|
-
try {
|
|
48
|
-
const input = JSON.parse(fs.readFileSync(inputPath, 'utf8') || '{}');
|
|
49
|
-
const eventName = input.hook_event_name || input.eventName || input.event_name;
|
|
50
|
-
const sessionId = input.session_id || input.sessionId;
|
|
51
|
-
if (!sessionId) process.exit(10);
|
|
52
|
-
|
|
53
|
-
const markerDir = path.join(process.env.TMPDIR || os.tmpdir(), 'mate-react-doctor-hooks');
|
|
54
|
-
const markerKey = crypto
|
|
55
|
-
.createHash('sha256')
|
|
56
|
-
.update(`${projectRoot}\0${sessionId}`)
|
|
57
|
-
.digest('hex');
|
|
58
|
-
const markerPath = path.join(markerDir, markerKey);
|
|
59
|
-
|
|
60
|
-
const hasWorkingTreeChanges = () => {
|
|
61
|
-
const result = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'], {
|
|
62
|
-
cwd: projectRoot,
|
|
63
|
-
encoding: 'utf8',
|
|
64
|
-
});
|
|
65
|
-
// Preserve the existing scan behavior when git is unavailable or the
|
|
66
|
-
// project is not a git repository; React Doctor will handle that case.
|
|
67
|
-
if (result.error || result.status !== 0) return true;
|
|
68
|
-
return Boolean(result.stdout.trim());
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
if (eventName === 'PostToolUse') {
|
|
72
|
-
const toolName = input.tool_name || input.toolName || input.tool;
|
|
73
|
-
if (!editToolNames.has(toolName)) process.exit(10);
|
|
74
|
-
fs.mkdirSync(markerDir, { recursive: true, mode: 0o700 });
|
|
75
|
-
fs.writeFileSync(markerPath, '', { mode: 0o600 });
|
|
76
|
-
process.exit(10);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (eventName !== 'Stop' || !fs.existsSync(markerPath)) process.exit(10);
|
|
80
|
-
fs.unlinkSync(markerPath);
|
|
81
|
-
if (!hasWorkingTreeChanges()) process.exit(10);
|
|
82
|
-
process.exit(0);
|
|
83
|
-
} catch {
|
|
84
|
-
process.exit(10);
|
|
85
|
-
}
|
|
86
|
-
NODE
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
REACT_DOCTOR_VERSION=${REACT_DOCTOR_VERSION:-0.9.13}
|
|
90
|
-
|
|
91
|
-
run_react_doctor() {
|
|
92
|
-
if [ -n "${MATE_REACT_DOCTOR_BIN_PATH:-}" ] && [ -x "$MATE_REACT_DOCTOR_BIN_PATH" ]; then
|
|
93
|
-
"$MATE_REACT_DOCTOR_BIN_PATH" --yes --verbose --scope changed --base HEAD --blocking warning --no-score --max-duration 30
|
|
94
|
-
return
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
if [ -x ./node_modules/.bin/react-doctor ]; then
|
|
98
|
-
./node_modules/.bin/react-doctor --yes --verbose --scope changed --base HEAD --blocking warning --no-score --max-duration 30
|
|
99
|
-
return
|
|
100
|
-
fi
|
|
101
|
-
|
|
102
|
-
if command -v react-doctor >/dev/null 2>&1; then
|
|
103
|
-
react-doctor --yes --verbose --scope changed --base HEAD --blocking warning --no-score --max-duration 30
|
|
104
|
-
return
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
printf '%s\n' "react-doctor: executable unavailable; install react-doctor@${REACT_DOCTOR_VERSION} in Mate or the project, then retry."
|
|
108
|
-
return 127
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if ! should_scan; then
|
|
112
|
-
exit 0
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
if run_react_doctor > "$output_file" 2>&1; then
|
|
116
|
-
exit 0
|
|
117
|
-
fi
|
|
118
|
-
|
|
119
|
-
node - "$input_file" "$output_file" <<'NODE'
|
|
120
|
-
const fs = require('node:fs');
|
|
121
|
-
const outputPath = process.argv[3];
|
|
122
|
-
const scanOutput = fs.readFileSync(outputPath, 'utf8').trim();
|
|
123
|
-
if (!scanOutput) process.exit(0);
|
|
124
|
-
|
|
125
|
-
const NON_LINT_FAILURES = [
|
|
126
|
-
/No React dependency found/i,
|
|
127
|
-
/Could not resolve config/i,
|
|
128
|
-
/is not a git repository/i,
|
|
129
|
-
];
|
|
130
|
-
if (NON_LINT_FAILURES.some((re) => re.test(scanOutput))) process.exit(0);
|
|
131
|
-
|
|
132
|
-
if (/^react-doctor: executable unavailable/i.test(scanOutput)) {
|
|
133
|
-
console.log(JSON.stringify({
|
|
134
|
-
hookSpecificOutput: {
|
|
135
|
-
hookEventName: 'Stop',
|
|
136
|
-
additionalContext: `${scanOutput}\nReact Doctor was not run. Install the pinned Mate runtime dependency with \`bun install\` in the Mate checkout, or install \`react-doctor@0.9.13\` in the project root, then retry.`,
|
|
137
|
-
},
|
|
138
|
-
}));
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const message = `React Doctor found issues in the changed files. Review this output and fix the regressions before finishing. For confirmed issues that cannot be fixed now, create GitHub issues with the rule, file/line, confidence, impact, and proposed fix.\n\n${scanOutput}`;
|
|
143
|
-
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: message } }));
|
|
144
|
-
NODE
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: react-doctor
|
|
3
|
-
description: Use when finishing a feature, fixing a bug, before committing React code, or when the user types `/doctor`, asks to scan, triage, or clean up React diagnostics. Covers lint, accessibility, bundle size, architecture. Includes a regression check and a full local-triage workflow that fetches the canonical playbook.
|
|
4
|
-
version: "1.2.0"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# React Doctor
|
|
8
|
-
|
|
9
|
-
Scans React codebases for security, performance, correctness, and architecture issues. Outputs a 0–100 health score.
|
|
10
|
-
|
|
11
|
-
## After making React code changes:
|
|
12
|
-
|
|
13
|
-
Run `npx react-doctor@latest --verbose --scope changed` and check the score did not regress.
|
|
14
|
-
|
|
15
|
-
If the score dropped, fix the regressions before committing.
|
|
16
|
-
|
|
17
|
-
## For general cleanup or code improvement:
|
|
18
|
-
|
|
19
|
-
Run `npx react-doctor@latest --verbose` (the default `--scope full`) to scan the full codebase. Fix issues by severity — errors first, then warnings.
|
|
20
|
-
|
|
21
|
-
## /doctor — full local triage workflow
|
|
22
|
-
|
|
23
|
-
When the user types `/doctor`, says "run react doctor", or asks for a full triage / cleanup pass (not just a regression check), fetch the canonical local-triage playbook and follow every step in it:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
curl --fail --silent --show-error \
|
|
27
|
-
--header 'Cache-Control: no-cache' \
|
|
28
|
-
https://www.react.doctor/prompts/react-doctor-agent.md
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
The playbook is the single source of truth — a scan → filter → triage → fix → validate loop that edits the working tree directly (never commits, never opens PRs). Updating the prompt at its source updates every agent on its next fetch — no skill reinstall needed.
|
|
32
|
-
|
|
33
|
-
Pair it with the matching per-rule prompts at `https://www.react.doctor/prompts/rules/<plugin>/<rule>.md` (fetched on demand inside the playbook) so each fix uses the canonical, reviewer-tested recipe.
|
|
34
|
-
|
|
35
|
-
## Configuring or explaining rules
|
|
36
|
-
|
|
37
|
-
When the user wants to understand a rule, disagrees with one, or wants to disable / tune which rules run (not fix code), read [references/explain.md](references/explain.md) and follow it. Start with `npx react-doctor@latest rules explain <rule>`, then apply the narrowest control via `npx react-doctor@latest rules disable|set|category|ignore-tag …`, which edits your `doctor.config.*` (or `package.json#reactDoctor`).
|
|
38
|
-
|
|
39
|
-
## Command
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npx react-doctor@latest --verbose --scope changed
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
| Flag | Purpose |
|
|
46
|
-
| ----------------- | ---------------------------------------------------------------- |
|
|
47
|
-
| `.` | Scan current directory |
|
|
48
|
-
| `--verbose` | Show affected files and line numbers per rule |
|
|
49
|
-
| `--scope changed` | Only report issues introduced vs the base branch (default: full) |
|
|
50
|
-
| `--scope lines` | Only report issues on the changed lines |
|
|
51
|
-
| `--score` | Output only the numeric score |
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Report assets
|
|
2
|
-
|
|
3
|
-
Vendored browser assets inlined into `mate report` HTML output. Nothing here is
|
|
4
|
-
imported as a module or linted, formatted, or typechecked — these files are read
|
|
5
|
-
as strings at render time.
|
|
6
|
-
|
|
7
|
-
## `mermaid.min.js`
|
|
8
|
-
|
|
9
|
-
| | |
|
|
10
|
-
| -------- | ------------------------------------------------------------------------ |
|
|
11
|
-
| Upstream | [`mermaid`](https://www.npmjs.com/package/mermaid) `dist/mermaid.min.js` |
|
|
12
|
-
| Version | `12.0.0` |
|
|
13
|
-
| License | MIT — see `mermaid.LICENSE` |
|
|
14
|
-
| Form | esbuild IIFE, self-contained, zero dynamic `import()` calls |
|
|
15
|
-
| Size | 5.58 MB raw |
|
|
16
|
-
|
|
17
|
-
The IIFE build is vendored rather than installed because `@uniqbit/mate-core`
|
|
18
|
-
publishes raw `src/` with no build step, and only mermaid's ESM build lazily
|
|
19
|
-
loads per-diagram chunks. It is inlined into a report document only when that
|
|
20
|
-
report carries a mermaid payload.
|
|
21
|
-
|
|
22
|
-
### Bumping
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
npm pack mermaid@<version>
|
|
26
|
-
tar -xzf mermaid-<version>.tgz package/dist/mermaid.min.js package/LICENSE
|
|
27
|
-
cp package/dist/mermaid.min.js packages/mate-core/src/templates/report-assets/mermaid.min.js
|
|
28
|
-
cp package/LICENSE packages/mate-core/src/templates/report-assets/mermaid.LICENSE
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Update the version above, confirm `grep -c 'import('` on the bundle stays `0`,
|
|
32
|
-
then re-run the report renderer tests.
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
The MIT License (MIT)
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2014 - 2022 Knut Sveidqvist
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|