ast-mcp-server 0.9.2 → 0.11.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/CHANGELOG.md +33 -0
- package/README.md +46 -14
- package/SECURITY.md +1 -1
- package/dist/batch/schema.js +2 -0
- package/dist/cli.js +67 -1
- package/dist/server.js +2 -0
- package/dist/services/agent-setup.js +11 -5
- package/dist/services/agent-targets.js +1 -0
- package/dist/services/call-spines.js +101 -0
- package/dist/services/cli-project-discovery.js +97 -0
- package/dist/services/context-builder.js +83 -24
- package/dist/services/doctor-continuation.js +37 -0
- package/dist/services/doctor-model.js +77 -0
- package/dist/services/doctor.js +111 -0
- package/dist/services/explore-presentation.js +160 -0
- package/dist/services/managed-bundle.js +145 -0
- package/dist/services/managed-file.js +37 -0
- package/dist/services/project.js +103 -24
- package/dist/services/public-errors.js +7 -1
- package/dist/services/relationships.js +104 -0
- package/dist/services/skill-installer.js +74 -56
- package/dist/services/test-candidates.js +15 -8
- package/dist/services/upgrade-command.js +77 -0
- package/dist/services/upgrade-reconcile.js +41 -0
- package/dist/services/upgrade-runtime.js +157 -0
- package/dist/services/upgrade.js +232 -0
- package/dist/tools/explore.js +73 -2
- package/dist/tools/find_test_candidates.js +124 -0
- package/dist/tools/get_impact.js +3 -30
- package/dist/tools/relationship-schema.js +31 -0
- package/docs/support.md +13 -13
- package/package.json +2 -2
- package/skills/structural-code-editing/SKILL.md +30 -158
- package/skills/structural-code-editing/references/operations.md +53 -0
- package/skills/structural-code-editing/releases.json +22 -3
|
@@ -2,7 +2,8 @@ import { readFile, realpath, stat } from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { canonicalizeFuturePath, createManagedFileApplyContext, } from "./managed-file.js";
|
|
6
|
+
import { applyManagedBundle, ManagedBundleConflictError, planManagedBundle, } from "./managed-bundle.js";
|
|
6
7
|
const SKILL_NAME = "structural-code-editing";
|
|
7
8
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
8
9
|
const ReleaseRecordSchema = z
|
|
@@ -12,17 +13,24 @@ const ReleaseRecordSchema = z
|
|
|
12
13
|
npm_versions: z.array(z.string().min(1)).min(1),
|
|
13
14
|
})
|
|
14
15
|
.strict();
|
|
15
|
-
const
|
|
16
|
+
const SkillFileSchema = z
|
|
17
|
+
.object({ path: z.string().min(1), sha256: z.string().regex(SHA256_PATTERN) })
|
|
18
|
+
.strict();
|
|
19
|
+
const BundleReleaseSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
version: z.string().min(1),
|
|
22
|
+
files: z.array(SkillFileSchema).min(1),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
const BundleSkillReleaseManifestSchema = z
|
|
16
26
|
.object({
|
|
17
|
-
schema_version: z.literal(
|
|
27
|
+
schema_version: z.literal(2),
|
|
18
28
|
algorithm: z.literal("sha256"),
|
|
19
|
-
current:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.strict(),
|
|
25
|
-
predecessors: z.array(ReleaseRecordSchema),
|
|
29
|
+
current: BundleReleaseSchema,
|
|
30
|
+
predecessors: z.array(z.union([
|
|
31
|
+
ReleaseRecordSchema,
|
|
32
|
+
BundleReleaseSchema.extend({ npm_versions: z.array(z.string().min(1)).min(1) }),
|
|
33
|
+
])),
|
|
26
34
|
})
|
|
27
35
|
.strict();
|
|
28
36
|
export class SkillConflictError extends Error {
|
|
@@ -101,77 +109,87 @@ async function readReleaseManifest(manifestPath) {
|
|
|
101
109
|
catch (error) {
|
|
102
110
|
throw new Error(`Skill release manifest is unreadable or invalid JSON: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
103
111
|
}
|
|
104
|
-
const result =
|
|
112
|
+
const result = BundleSkillReleaseManifestSchema.safeParse(parsed);
|
|
105
113
|
if (!result.success) {
|
|
106
114
|
throw new Error(`Skill release manifest is invalid: ${result.error.issues[0]?.message ?? "unknown error"}`);
|
|
107
115
|
}
|
|
108
|
-
const
|
|
109
|
-
result.data.current
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
const manifest = {
|
|
117
|
+
current: result.data.current,
|
|
118
|
+
predecessors: result.data.predecessors.map((item) => "files" in item
|
|
119
|
+
? item
|
|
120
|
+
: {
|
|
121
|
+
version: item.version,
|
|
122
|
+
npm_versions: item.npm_versions,
|
|
123
|
+
files: [{ path: "SKILL.md", sha256: item.sha256 }],
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
if (!manifest.current.files.some((file) => file.path === "SKILL.md")) {
|
|
127
|
+
throw new Error("Skill release manifest current bundle must include SKILL.md.");
|
|
114
128
|
}
|
|
115
129
|
const predecessorVersions = result.data.predecessors.flatMap((item) => item.npm_versions);
|
|
116
130
|
if (new Set(predecessorVersions).size !== predecessorVersions.length) {
|
|
117
131
|
throw new Error("Skill release manifest contains a duplicate npm version provenance entry.");
|
|
118
132
|
}
|
|
119
|
-
return
|
|
133
|
+
return manifest;
|
|
120
134
|
}
|
|
121
135
|
export async function planBundledSkillInstallation(options) {
|
|
122
136
|
const sourcePath = path.resolve(options.sourceSkillPath);
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const manifestPath = path.resolve(options.releaseManifestPath ?? path.join(
|
|
137
|
+
const sourceRoot = path.dirname(sourcePath);
|
|
138
|
+
const canonicalSourceRoot = await realpath(sourceRoot);
|
|
139
|
+
const manifestPath = path.resolve(options.releaseManifestPath ?? path.join(sourceRoot, "releases.json"));
|
|
126
140
|
const manifest = await readReleaseManifest(manifestPath);
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
const sources = [];
|
|
142
|
+
for (const file of manifest.current.files) {
|
|
143
|
+
const assetPath = path.resolve(sourceRoot, file.path);
|
|
144
|
+
const assetRealPath = await realpath(assetPath).catch(() => undefined);
|
|
145
|
+
if (assetRealPath === undefined ||
|
|
146
|
+
(assetRealPath !== canonicalSourceRoot &&
|
|
147
|
+
!assetRealPath.startsWith(`${canonicalSourceRoot}${path.sep}`)) ||
|
|
148
|
+
!(await stat(assetPath)).isFile()) {
|
|
149
|
+
throw new Error(`Skill release manifest asset is missing or escapes the bundle: ${file.path}`);
|
|
150
|
+
}
|
|
151
|
+
sources.push({ ...file, content: await readFile(assetPath) });
|
|
129
152
|
}
|
|
130
|
-
const admittedPredecessors = new Set(manifest.predecessors.map((item) => item.sha256));
|
|
131
153
|
const logical = await destinations(options);
|
|
132
154
|
const filesByPath = new Map();
|
|
155
|
+
const statusesByRoot = new Map();
|
|
133
156
|
for (const item of logical) {
|
|
134
|
-
|
|
157
|
+
const destinationRoot = path.dirname(item.path);
|
|
158
|
+
if (statusesByRoot.has(destinationRoot))
|
|
135
159
|
continue;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
let plan;
|
|
161
|
+
try {
|
|
162
|
+
plan = await planManagedBundle({
|
|
163
|
+
destinationRoot,
|
|
164
|
+
current: { files: sources },
|
|
165
|
+
predecessors: manifest.predecessors,
|
|
166
|
+
force: options.force,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof ManagedBundleConflictError)
|
|
171
|
+
throw new SkillConflictError(error.destination);
|
|
172
|
+
if (error instanceof Error && error.message.startsWith("Managed bundle")) {
|
|
173
|
+
throw new Error(error.message.replace("Managed bundle", "Skill release manifest"), {
|
|
174
|
+
cause: error,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
statusesByRoot.set(destinationRoot, plan.status);
|
|
180
|
+
for (const file of plan.files)
|
|
181
|
+
filesByPath.set(file.path, file);
|
|
153
182
|
}
|
|
154
183
|
return {
|
|
155
184
|
installations: logical.map((item) => ({
|
|
156
185
|
...item,
|
|
157
|
-
status:
|
|
186
|
+
status: statusesByRoot.get(path.dirname(item.path)),
|
|
158
187
|
})),
|
|
159
188
|
files: [...filesByPath.values()],
|
|
160
189
|
};
|
|
161
190
|
}
|
|
162
|
-
export async function applySkillInstallationPlan(plan, onApplied, context = createManagedFileApplyContext()) {
|
|
163
|
-
|
|
164
|
-
for (const file of plan.files) {
|
|
165
|
-
for (const current of authenticated) {
|
|
166
|
-
await verifyManagedFilePostimage(current, context);
|
|
167
|
-
}
|
|
168
|
-
await applyManagedFilePlan(file, context);
|
|
169
|
-
authenticated.push(file);
|
|
170
|
-
await onApplied?.(file);
|
|
171
|
-
}
|
|
172
|
-
for (const current of authenticated) {
|
|
173
|
-
await verifyManagedFilePostimage(current, context);
|
|
174
|
-
}
|
|
191
|
+
export async function applySkillInstallationPlan(plan, onApplied, context = createManagedFileApplyContext(), hooks = {}) {
|
|
192
|
+
await applyManagedBundle(plan, onApplied, context, hooks);
|
|
175
193
|
}
|
|
176
194
|
export async function installBundledSkill(options) {
|
|
177
195
|
const plan = await planBundledSkillInstallation(options);
|
|
@@ -4,21 +4,22 @@ export const DEFAULT_TEST_DIRECTORIES = Object.freeze(["test", "tests", "__tests
|
|
|
4
4
|
export const TEST_CANDIDATE_REASONS = Object.freeze([
|
|
5
5
|
"direct_compiler_reference",
|
|
6
6
|
"transitive_compiler_reference",
|
|
7
|
+
"convention_match",
|
|
7
8
|
]);
|
|
8
9
|
export const TEST_CANDIDATE_CONFIDENCES = Object.freeze(["exact", "high"]);
|
|
9
|
-
const
|
|
10
|
-
const
|
|
10
|
+
export const MAX_TEST_CANDIDATE_CONVENTION_ITEMS = 32;
|
|
11
|
+
export const MAX_TEST_CANDIDATE_CONVENTION_LENGTH = 256;
|
|
11
12
|
function normalizeConventionValues(values, defaults, label) {
|
|
12
13
|
const selected = values ?? defaults;
|
|
13
|
-
if (!Array.isArray(selected) || selected.length >
|
|
14
|
-
throw new Error(`${label} must contain at most ${
|
|
14
|
+
if (!Array.isArray(selected) || selected.length > MAX_TEST_CANDIDATE_CONVENTION_ITEMS) {
|
|
15
|
+
throw new Error(`${label} must contain at most ${MAX_TEST_CANDIDATE_CONVENTION_ITEMS} entries.`);
|
|
15
16
|
}
|
|
16
17
|
const normalized = selected.map((value) => {
|
|
17
18
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
18
19
|
throw new Error(`${label} entries must be non-empty strings.`);
|
|
19
20
|
}
|
|
20
|
-
if (value.length >
|
|
21
|
-
throw new Error(`${label} entries must not exceed ${
|
|
21
|
+
if (value.length > MAX_TEST_CANDIDATE_CONVENTION_LENGTH) {
|
|
22
|
+
throw new Error(`${label} entries must not exceed ${MAX_TEST_CANDIDATE_CONVENTION_LENGTH} characters.`);
|
|
22
23
|
}
|
|
23
24
|
return value.trim().replaceAll("\\", "/");
|
|
24
25
|
});
|
|
@@ -112,7 +113,13 @@ function edgeOrder(left, right) {
|
|
|
112
113
|
left.target.file.localeCompare(right.target.file) ||
|
|
113
114
|
left.target.selector.localeCompare(right.target.selector));
|
|
114
115
|
}
|
|
115
|
-
function candidateReason(depth) {
|
|
116
|
+
function candidateReason(file, depth) {
|
|
117
|
+
if (!isTestFile(file, {
|
|
118
|
+
patterns: DEFAULT_TEST_FILE_PATTERNS,
|
|
119
|
+
directories: DEFAULT_TEST_DIRECTORIES,
|
|
120
|
+
})) {
|
|
121
|
+
return "convention_match";
|
|
122
|
+
}
|
|
116
123
|
return depth === 1 ? "direct_compiler_reference" : "transitive_compiler_reference";
|
|
117
124
|
}
|
|
118
125
|
function candidateConfidence(depth) {
|
|
@@ -194,7 +201,7 @@ export function findTestCandidates(impact, conventions = {}) {
|
|
|
194
201
|
const depth = candidate.depth;
|
|
195
202
|
return {
|
|
196
203
|
file,
|
|
197
|
-
reason: candidateReason(depth),
|
|
204
|
+
reason: candidateReason(file, depth),
|
|
198
205
|
confidence: candidateConfidence(depth),
|
|
199
206
|
evidence: {
|
|
200
207
|
depth,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { inspectUpgrade, performUpgrade, UpgradeError } from "./upgrade.js";
|
|
2
|
+
import { ReconciliationError } from "./upgrade-reconcile.js";
|
|
3
|
+
import { UpgradeCleanupError } from "./upgrade-runtime.js";
|
|
4
|
+
export class UpgradeCommandError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code = "UPGRADE_USAGE") {
|
|
7
|
+
super("Usage: ast-tool upgrade [--check]");
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "UpgradeCommandError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function runUpgradeCommand(args, runtime, reconcile) {
|
|
13
|
+
if (args.length > 1 || (args.length === 1 && args[0] !== "--check")) {
|
|
14
|
+
throw new UpgradeCommandError();
|
|
15
|
+
}
|
|
16
|
+
if (args[0] === "--check")
|
|
17
|
+
return inspectUpgrade(runtime);
|
|
18
|
+
const updated = await performUpgrade(runtime);
|
|
19
|
+
const { new_cli: newCli, ...result } = updated;
|
|
20
|
+
if (updated.status === "current")
|
|
21
|
+
return { ...result, reconciliation: "not_required" };
|
|
22
|
+
try {
|
|
23
|
+
await reconcile(newCli);
|
|
24
|
+
return { ...result, reconciliation: "completed" };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const skillConflict = error instanceof ReconciliationError && error.code === "AGENT_SKILL_CONFLICT";
|
|
28
|
+
return {
|
|
29
|
+
...result,
|
|
30
|
+
status: "partial",
|
|
31
|
+
package_status: "updated",
|
|
32
|
+
reconciliation: "required",
|
|
33
|
+
continuation: skillConflict
|
|
34
|
+
? "ast-tool setup --agents all --yes --force-skill"
|
|
35
|
+
: "ast-tool setup --agents all --yes",
|
|
36
|
+
restart_required: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function primaryCode(error) {
|
|
41
|
+
return error instanceof UpgradeError || error instanceof UpgradeCommandError
|
|
42
|
+
? error.code
|
|
43
|
+
: "UPGRADE_ERROR";
|
|
44
|
+
}
|
|
45
|
+
export async function runUpgradeLease(args, lease, reconcile) {
|
|
46
|
+
let result;
|
|
47
|
+
let primary;
|
|
48
|
+
try {
|
|
49
|
+
result = await runUpgradeCommand(args, lease.runtime, reconcile);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
primary = error;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await lease.dispose();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
if (primary)
|
|
59
|
+
throw new UpgradeCleanupError(primaryCode(primary));
|
|
60
|
+
const record = result;
|
|
61
|
+
if (record.status === "updated" || record.package_status === "updated") {
|
|
62
|
+
return {
|
|
63
|
+
...record,
|
|
64
|
+
status: "partial",
|
|
65
|
+
package_status: "updated",
|
|
66
|
+
cleanup: "required",
|
|
67
|
+
cleanup_code: "UPGRADE_CLEANUP_FAILED",
|
|
68
|
+
continuation: record.continuation ?? "ast-tool upgrade --check",
|
|
69
|
+
restart_required: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
throw new UpgradeCleanupError();
|
|
73
|
+
}
|
|
74
|
+
if (primary)
|
|
75
|
+
throw primary;
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
const executeFile = promisify(execFile);
|
|
5
|
+
export class ReconciliationError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code) {
|
|
8
|
+
super("The updated package requires managed setup reconciliation.");
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "ReconciliationError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function reconcileUpgrade(executable, options = {}) {
|
|
14
|
+
const execute = options.execute ?? executeFile;
|
|
15
|
+
try {
|
|
16
|
+
await execute(options.nodeExecutable ?? process.execPath, [executable, "setup", "--agents", "all", "--yes"], {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
env: options.environment ?? process.env,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const stderr = typeof error === "object" && error !== null && "stderr" in error
|
|
24
|
+
? String(error.stderr ?? "")
|
|
25
|
+
: "";
|
|
26
|
+
try {
|
|
27
|
+
const failure = JSON.parse(stderr);
|
|
28
|
+
if (typeof failure === "object" &&
|
|
29
|
+
failure !== null &&
|
|
30
|
+
"code" in failure &&
|
|
31
|
+
failure.code === "AGENT_SKILL_CONFLICT") {
|
|
32
|
+
throw new ReconciliationError("AGENT_SKILL_CONFLICT");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (parsed) {
|
|
36
|
+
if (parsed instanceof ReconciliationError)
|
|
37
|
+
throw parsed;
|
|
38
|
+
}
|
|
39
|
+
throw new ReconciliationError("UPGRADE_RECONCILIATION_FAILED");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
export class UpgradeCleanupError extends Error {
|
|
8
|
+
primaryCode;
|
|
9
|
+
code = "UPGRADE_CLEANUP_FAILED";
|
|
10
|
+
constructor(primaryCode) {
|
|
11
|
+
super("Upgrade temporary files could not be removed safely.");
|
|
12
|
+
this.primaryCode = primaryCode;
|
|
13
|
+
this.name = "UpgradeCleanupError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const executeFile = promisify(execFile);
|
|
17
|
+
async function npmCliFor(node, platform) {
|
|
18
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
19
|
+
const root = platform === "win32" ? p.dirname(node) : p.resolve(p.dirname(node), "..");
|
|
20
|
+
const candidates = platform === "win32"
|
|
21
|
+
? [p.join(root, "node_modules", "npm", "bin", "npm-cli.js")]
|
|
22
|
+
: [
|
|
23
|
+
p.join(p.dirname(node), "npm"),
|
|
24
|
+
p.join(root, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
25
|
+
];
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
const resolved = await realpath(candidate);
|
|
29
|
+
if ((await lstat(resolved)).isFile())
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Try the next layout owned by this Node installation.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
async function copyUserConfig(environment, destination) {
|
|
39
|
+
const source = environment.npm_config_userconfig ??
|
|
40
|
+
environment.NPM_CONFIG_USERCONFIG ??
|
|
41
|
+
path.join(environment.HOME ?? os.homedir(), ".npmrc");
|
|
42
|
+
try {
|
|
43
|
+
await copyFile(source, destination);
|
|
44
|
+
await chmod(destination, 0o600);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
await writeFile(destination, "", { encoding: "utf8", mode: 0o600 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function restoreOwnerAccess(value) {
|
|
51
|
+
const info = await lstat(value);
|
|
52
|
+
if (info.isSymbolicLink())
|
|
53
|
+
return;
|
|
54
|
+
await chmod(value, info.isDirectory() ? 0o700 : 0o600);
|
|
55
|
+
if (info.isDirectory())
|
|
56
|
+
for (const child of await readdir(value))
|
|
57
|
+
await restoreOwnerAccess(path.join(value, child));
|
|
58
|
+
}
|
|
59
|
+
async function cleanupBoundary(boundary, removeTree = (value) => rm(value, { recursive: true, force: true })) {
|
|
60
|
+
try {
|
|
61
|
+
await restoreOwnerAccess(boundary);
|
|
62
|
+
await removeTree(boundary);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new UpgradeCleanupError();
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await lstat(boundary);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code === "ENOENT")
|
|
72
|
+
return;
|
|
73
|
+
throw new UpgradeCleanupError();
|
|
74
|
+
}
|
|
75
|
+
throw new UpgradeCleanupError();
|
|
76
|
+
}
|
|
77
|
+
export async function createUpgradeRuntime(executable, options = {}) {
|
|
78
|
+
const environment = options.environment ?? process.env;
|
|
79
|
+
const platform = options.platform ?? process.platform;
|
|
80
|
+
const nodeExecutable = options.nodeExecutable ?? process.execPath;
|
|
81
|
+
const execute = options.execute ?? executeFile;
|
|
82
|
+
const boundary = await mkdtemp(path.join(options.temporaryParent ?? os.tmpdir(), "ast-tool-upgrade-"));
|
|
83
|
+
try {
|
|
84
|
+
const cache = path.join(boundary, "cache");
|
|
85
|
+
const logs = path.join(boundary, "logs");
|
|
86
|
+
const userConfig = path.join(boundary, "npmrc");
|
|
87
|
+
await Promise.all([mkdir(cache), mkdir(logs), copyUserConfig(environment, userConfig)]);
|
|
88
|
+
const npmEnvironment = {
|
|
89
|
+
...environment,
|
|
90
|
+
npm_config_cache: cache,
|
|
91
|
+
npm_config_logs_dir: logs,
|
|
92
|
+
npm_config_userconfig: userConfig,
|
|
93
|
+
npm_config_update_notifier: "false",
|
|
94
|
+
npm_config_audit: "false",
|
|
95
|
+
npm_config_fund: "false",
|
|
96
|
+
};
|
|
97
|
+
const run = async (file, args) => (await execute(file, args, { encoding: "utf8", windowsHide: true, env: npmEnvironment }))
|
|
98
|
+
.stdout;
|
|
99
|
+
const cliExecutable = await realpath(executable);
|
|
100
|
+
const packageRoot = path.resolve(path.dirname(cliExecutable), "..");
|
|
101
|
+
const npmCliExecutable = await npmCliFor(nodeExecutable, platform);
|
|
102
|
+
const p = platform === "win32" ? path.win32 : path.posix;
|
|
103
|
+
const voltaExecutable = environment.VOLTA_HOME
|
|
104
|
+
? p.resolve(environment.VOLTA_HOME, "bin", platform === "win32" ? "volta.exe" : "volta")
|
|
105
|
+
: undefined;
|
|
106
|
+
const runtime = {
|
|
107
|
+
packageRoot,
|
|
108
|
+
cliExecutable,
|
|
109
|
+
nodeExecutable,
|
|
110
|
+
...(npmCliExecutable ? { npmCliExecutable } : {}),
|
|
111
|
+
...(voltaExecutable ? { voltaExecutable } : {}),
|
|
112
|
+
platform,
|
|
113
|
+
realpath,
|
|
114
|
+
directDirectory: async (value) => {
|
|
115
|
+
const info = await lstat(value).catch(() => undefined);
|
|
116
|
+
return info?.isDirectory() === true && info.isSymbolicLink() === false;
|
|
117
|
+
},
|
|
118
|
+
readPackage: async (root) => JSON.parse(await readFile(path.join(root, "package.json"), "utf8")),
|
|
119
|
+
latestVersion: async (provenance) => {
|
|
120
|
+
const stdout = provenance === "volta"
|
|
121
|
+
? await run(voltaExecutable, [
|
|
122
|
+
"run",
|
|
123
|
+
"npm",
|
|
124
|
+
"view",
|
|
125
|
+
"ast-mcp-server@latest",
|
|
126
|
+
"version",
|
|
127
|
+
"--json",
|
|
128
|
+
])
|
|
129
|
+
: await run(nodeExecutable, [
|
|
130
|
+
npmCliExecutable,
|
|
131
|
+
"view",
|
|
132
|
+
"ast-mcp-server@latest",
|
|
133
|
+
"version",
|
|
134
|
+
"--json",
|
|
135
|
+
]);
|
|
136
|
+
const value = JSON.parse(stdout);
|
|
137
|
+
if (typeof value !== "string")
|
|
138
|
+
throw new Error("invalid registry response");
|
|
139
|
+
return value;
|
|
140
|
+
},
|
|
141
|
+
run,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
runtime,
|
|
145
|
+
dispose: () => cleanupBoundary(boundary, options.removeTree),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
try {
|
|
150
|
+
await cleanupBoundary(boundary, options.removeTree);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new UpgradeCleanupError("UPGRADE_RUNTIME_CREATION_FAILED");
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|