@agjs/tsforge 0.5.0 → 0.5.1
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
CHANGED
|
@@ -16,6 +16,7 @@ export const TOOL_NAME = {
|
|
|
16
16
|
typeAt: "type_at",
|
|
17
17
|
diagnostics: "diagnostics",
|
|
18
18
|
renameSymbol: "rename_symbol",
|
|
19
|
+
moveFile: "move_file",
|
|
19
20
|
organizeImports: "organize_imports",
|
|
20
21
|
scaffoldUi: "scaffold_ui",
|
|
21
22
|
scaffoldRoutes: "scaffold_routes",
|
|
@@ -379,4 +380,17 @@ export const LSP_TOOLS = [
|
|
|
379
380
|
},
|
|
380
381
|
},
|
|
381
382
|
},
|
|
383
|
+
{
|
|
384
|
+
type: "function",
|
|
385
|
+
function: {
|
|
386
|
+
name: TOOL_NAME.moveFile,
|
|
387
|
+
description:
|
|
388
|
+
"Move/rename a FILE and rewrite every import that points at it (and its own relative imports) in one step — compiler-accurate, no manual edits. Rejected if the source, destination, or any importer is read-only/out-of-scope.",
|
|
389
|
+
parameters: {
|
|
390
|
+
type: "object",
|
|
391
|
+
properties: { from: { type: "string" }, to: { type: "string" } },
|
|
392
|
+
required: ["from", "to"],
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
382
396
|
];
|
|
@@ -30,6 +30,7 @@ const HANDLERS: Record<ToolName, ToolHandler> = {
|
|
|
30
30
|
[TOOL_NAME.typeAt]: (a, c) => doLsp(TOOL_NAME.typeAt, a, c),
|
|
31
31
|
[TOOL_NAME.diagnostics]: (a, c) => doLsp(TOOL_NAME.diagnostics, a, c),
|
|
32
32
|
[TOOL_NAME.renameSymbol]: (a, c) => doLsp(TOOL_NAME.renameSymbol, a, c),
|
|
33
|
+
[TOOL_NAME.moveFile]: (a, c) => doLsp(TOOL_NAME.moveFile, a, c),
|
|
33
34
|
[TOOL_NAME.organizeImports]: (a, c) => doLsp(TOOL_NAME.organizeImports, a, c),
|
|
34
35
|
[TOOL_NAME.scaffoldUi]: doScaffoldUi,
|
|
35
36
|
[TOOL_NAME.scaffoldRoutes]: doScaffoldRoutes,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative } from "node:path";
|
|
2
2
|
import { fileArg, TOOL_NAME, type ToolName } from "../../agent";
|
|
3
3
|
import { runArgvCommand } from "../../lib/fs";
|
|
4
|
-
import { writable } from "../../lib/scope";
|
|
4
|
+
import { writable, isVendored } from "../../lib/scope";
|
|
5
5
|
import { LOOP_LIMITS } from "../loop.constants";
|
|
6
6
|
import { str, reject, type IToolContext } from "./tool-context";
|
|
7
7
|
|
|
@@ -137,10 +137,57 @@ export function doLsp(
|
|
|
137
137
|
return `organize_imports: ${n} change(s) in ${file}`;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// move_file takes {from, to} (not {file, symbol}) — handle before doSymbolLsp's
|
|
141
|
+
// symbol guard. Scope-enforced: a move rewrites importers across the project; it
|
|
142
|
+
// must NOT touch read-only/out-of-scope/vendored files.
|
|
143
|
+
if (name === TOOL_NAME.moveFile) {
|
|
144
|
+
return doMoveFile(svc, args, ctx, rel);
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
// The remaining tools address a symbol by name within a file.
|
|
141
148
|
return doSymbolLsp(name, svc, file, args, ctx, rel);
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
/** move_file: relocate a file and rewrite every importer's specifier. */
|
|
152
|
+
function doMoveFile(
|
|
153
|
+
svc: LspService,
|
|
154
|
+
args: Record<string, unknown>,
|
|
155
|
+
ctx: IToolContext,
|
|
156
|
+
rel: (abs: string) => string
|
|
157
|
+
): string {
|
|
158
|
+
const from = str(args, "from");
|
|
159
|
+
const to = str(args, "to");
|
|
160
|
+
|
|
161
|
+
if (from.length === 0 || to.length === 0) {
|
|
162
|
+
return "move_file: need {from, to}";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const targets = svc.moveTargets(from, to).map(rel);
|
|
166
|
+
const blocked = targets.filter(
|
|
167
|
+
(t) => !writable(t, ctx.files) || isVendored(t, ctx.vendored ?? [])
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (blocked.length > 0) {
|
|
171
|
+
return reject(
|
|
172
|
+
ctx,
|
|
173
|
+
"move_file",
|
|
174
|
+
`move '${from}' → '${to}' REJECTED: would touch out-of-scope/read-only file(s): ${blocked.join(", ")}. Move files only when the source, destination, and every importer are in your editable scope.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const changed = svc.moveFile(from, to);
|
|
179
|
+
|
|
180
|
+
ctx.report({
|
|
181
|
+
kind: "tool",
|
|
182
|
+
task: ctx.task,
|
|
183
|
+
message: `move_file ${from}→${to} (${changed ?? 0})`,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return changed === null
|
|
187
|
+
? `move_file: source '${from}' not found`
|
|
188
|
+
: `moved '${from}' → '${to}', updated imports across ${changed} file(s)`;
|
|
189
|
+
}
|
|
190
|
+
|
|
144
191
|
type LspService = NonNullable<IToolContext["tsService"]>;
|
|
145
192
|
|
|
146
193
|
/** The LSP tools that resolve a symbol position first (type_at, find_references,
|
package/src/lsp/service.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { join, isAbsolute } from "node:path";
|
|
1
|
+
import { join, isAbsolute, dirname } from "node:path";
|
|
2
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
2
3
|
import ts from "typescript";
|
|
3
4
|
import type {
|
|
4
5
|
ITsDiagnostic,
|
|
@@ -451,6 +452,73 @@ export class TsService {
|
|
|
451
452
|
return [...new Set((locs ?? []).map((l) => l.fileName))];
|
|
452
453
|
}
|
|
453
454
|
|
|
455
|
+
/** Edits a file move would produce (rewriting importers + the file's own
|
|
456
|
+
* relative imports). Absolute paths. */
|
|
457
|
+
private moveEdits(
|
|
458
|
+
fromAbs: string,
|
|
459
|
+
toAbs: string
|
|
460
|
+
): readonly ts.FileTextChanges[] {
|
|
461
|
+
return this.service.getEditsForFileRename(
|
|
462
|
+
fromAbs,
|
|
463
|
+
toAbs,
|
|
464
|
+
ts.getDefaultFormatCodeSettings("\n"),
|
|
465
|
+
{}
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Which files a move would touch — every importer plus the source and
|
|
470
|
+
* destination — for scope-checking BEFORE applying. Absolute paths. */
|
|
471
|
+
moveTargets(fromRel: string, toRel: string): string[] {
|
|
472
|
+
const fromAbs = this.toAbs(fromRel);
|
|
473
|
+
const toAbs = this.toAbs(toRel);
|
|
474
|
+
const touched = new Set(
|
|
475
|
+
this.moveEdits(fromAbs, toAbs).map((e) => e.fileName)
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
touched.add(fromAbs);
|
|
479
|
+
touched.add(toAbs);
|
|
480
|
+
|
|
481
|
+
return [...touched];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Move a file and rewrite every import that points at it (and its own relative
|
|
486
|
+
* imports), compiler-accurately via getEditsForFileRename. Returns the number
|
|
487
|
+
* of files changed (incl. the moved file), or null if the source can't be read.
|
|
488
|
+
* Callers MUST enforce scope first (a move can touch read-only files).
|
|
489
|
+
*/
|
|
490
|
+
moveFile(fromRel: string, toRel: string): number | null {
|
|
491
|
+
const fromAbs = this.toAbs(fromRel);
|
|
492
|
+
const toAbs = this.toAbs(toRel);
|
|
493
|
+
const original = ts.sys.readFile(fromAbs);
|
|
494
|
+
|
|
495
|
+
if (original === undefined) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const edits = this.moveEdits(fromAbs, toAbs);
|
|
500
|
+
|
|
501
|
+
// Apply the import rewrites (importers + the moved file's own imports) while
|
|
502
|
+
// the file still lives at its old path, THEN relocate the edited content.
|
|
503
|
+
this.applyChanges(edits);
|
|
504
|
+
|
|
505
|
+
const moved = ts.sys.readFile(fromAbs) ?? original;
|
|
506
|
+
|
|
507
|
+
mkdirSync(dirname(toAbs), { recursive: true });
|
|
508
|
+
ts.sys.writeFile(toAbs, moved);
|
|
509
|
+
rmSync(fromAbs, { force: true });
|
|
510
|
+
|
|
511
|
+
const stale = this.files.indexOf(fromAbs);
|
|
512
|
+
|
|
513
|
+
if (stale >= 0) {
|
|
514
|
+
this.files.splice(stale, 1);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
this.refresh(toRel);
|
|
518
|
+
|
|
519
|
+
return new Set([...edits.map((e) => e.fileName), fromAbs]).size;
|
|
520
|
+
}
|
|
521
|
+
|
|
454
522
|
/** Organize imports (dedupe/sort/drop unused) for one file. Returns edits made. */
|
|
455
523
|
organizeImports(file: string): number {
|
|
456
524
|
const changes = this.service.organizeImports(
|