atmn 1.1.25 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/dist/bin.js +23102 -0
- package/dist/index.js +2906 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +27 -74
- package/src/actions/api/callApi.ts +280 -0
- package/src/actions/api/registerApiCommands.ts +121 -0
- package/src/actions/env/fetchOrgInfo.ts +32 -0
- package/src/actions/env/types/orgInfo.ts +18 -0
- package/src/actions/env.ts +94 -0
- package/src/actions/init/runInit.ts +401 -0
- package/src/actions/login/keyless.ts +135 -0
- package/src/actions/login.ts +144 -0
- package/src/actions/pull/appendPlanVersionFixture.ts +373 -0
- package/src/actions/pull/applyPreview.ts +553 -0
- package/src/actions/pull/applySettingsPreview.ts +114 -0
- package/src/actions/pull/changedFixtureKeys.ts +88 -0
- package/src/actions/pull/listSourceFiles.ts +29 -0
- package/src/actions/pull/locateFixture.ts +78 -0
- package/src/actions/pull/resolveCollectionTarget.ts +198 -0
- package/src/actions/pull/rewriteConfig.ts +57 -0
- package/src/actions/pull/scaffoldConfig.ts +118 -0
- package/src/actions/pull.ts +399 -0
- package/src/actions/push/backfillInternalIds.ts +394 -0
- package/src/actions/push/deprecatedFields.ts +55 -0
- package/src/actions/push.ts +294 -0
- package/src/actions/reset/runReset.ts +58 -0
- package/src/actions/sandbox/createSandbox.ts +83 -0
- package/src/actions/sandbox/deleteSandbox.ts +91 -0
- package/src/actions/sandbox/listSandboxes.ts +28 -0
- package/src/actions/sandbox/types/sandboxClient.ts +16 -0
- package/src/actions/sandbox/useSandbox.ts +157 -0
- package/src/actions/sandbox/withSandboxScopeHint.ts +27 -0
- package/src/actions/skills/skills.ts +246 -0
- package/src/auth/announceAuthorizationUrl.ts +26 -0
- package/src/auth/browser/openSystemBrowser.ts +7 -0
- package/src/auth/browser/tryOpenBrowser.ts +17 -0
- package/src/auth/browser/watchLauncher.ts +46 -0
- package/src/auth/buildAuthorizationUrl.ts +36 -0
- package/src/auth/callbackPages.ts +126 -0
- package/src/auth/createOrgApiKeys.ts +46 -0
- package/src/auth/keyless.ts +119 -0
- package/src/auth/oauthConfig.ts +63 -0
- package/src/auth/runOAuthFlow.ts +230 -0
- package/src/auth/types/browserOpener.ts +5 -0
- package/src/auth/types/impersonationTokens.ts +23 -0
- package/src/auth/types/oauthTokens.ts +13 -0
- package/src/auth/types/orgApiKeys.ts +6 -0
- package/src/bin.ts +9 -0
- package/src/cli.ts +648 -0
- package/src/config/configPackageName.ts +9 -0
- package/src/config/legacyConfig.ts +25 -0
- package/src/config/loadConfig.ts +231 -0
- package/src/env/assertSandboxTarget.ts +20 -0
- package/src/env/loadEnv.ts +184 -0
- package/src/env/resolveTarget.ts +134 -0
- package/src/env/sandboxKeyName.ts +18 -0
- package/src/generated/apiRoutes.ts +3787 -0
- package/src/generated/client.ts +51564 -0
- package/src/generated/emit.ts +1163 -0
- package/src/generated/emitRuntime.ts +522 -0
- package/src/generated/features.ts +146 -0
- package/src/generated/labels.ts +29 -0
- package/src/generated/licenses.ts +287 -0
- package/src/generated/lintRules.ts +2207 -0
- package/src/generated/lintRuntime.ts +865 -0
- package/src/generated/plans.ts +1628 -0
- package/src/generated/referralPrograms.ts +22 -0
- package/src/generated/rewards.ts +58 -0
- package/src/generated/settings.ts +21 -0
- package/src/generated/skills.ts +305 -0
- package/src/generated/variants.ts +934 -0
- package/src/generated/wire.ts +334 -0
- package/src/http/autumnFetch.ts +30 -0
- package/src/index.ts +20 -0
- package/src/project/chooseConfigDir.ts +41 -0
- package/src/project/resolveProject.ts +115 -0
- package/src/project/rootMarker.ts +40 -0
- package/src/prompt/prompt.ts +186 -0
- package/src/prompt/select.ts +162 -0
- package/src/render/renderEnv.ts +77 -0
- package/src/render/renderPreview.ts +945 -0
- package/src/render/renderSandboxes.ts +92 -0
- package/src/render/stripTerminalControls.ts +19 -0
- package/src/repo/findRepoRoot.ts +79 -0
- package/src/surgery/appendPropertyEdit.ts +67 -0
- package/src/surgery/appendToArray.ts +87 -0
- package/src/surgery/appendToBinding.ts +19 -0
- package/src/surgery/appendToCollection.ts +40 -0
- package/src/surgery/appendToFixtureArray.ts +71 -0
- package/src/surgery/arrayBinding.ts +30 -0
- package/src/surgery/deleteFixtureLiteral.ts +81 -0
- package/src/surgery/deleteReference.ts +48 -0
- package/src/surgery/ensureBuilderImport.ts +65 -0
- package/src/surgery/findFixture.ts +238 -0
- package/src/surgery/fixtureEdit.ts +156 -0
- package/src/surgery/fixtureLocation.ts +32 -0
- package/src/surgery/insertCollection.ts +86 -0
- package/src/surgery/insertFirstProperty.ts +73 -0
- package/src/surgery/patchFixtureProperty.ts +152 -0
- package/src/surgery/patchSingletonProperty.ts +221 -0
- package/src/surgery/replaceFixture.ts +28 -0
- package/src/surgery/setFixtureProperty.ts +55 -0
- package/src/surgery/staticFixtureRule.ts +48 -0
- package/src/version.ts +5 -0
- package/dist/cli.js +0 -146296
- package/dist/compose/index.js +0 -122
- package/dist/src/compose/builders/builderFunctions.d.ts +0 -84
- package/dist/src/compose/builders/rewardFunctions.d.ts +0 -5
- package/dist/src/compose/builders/variantFunctions.d.ts +0 -2
- package/dist/src/compose/index.d.ts +0 -19
- package/dist/src/compose/models/featureModels.d.ts +0 -262
- package/dist/src/compose/models/index.d.ts +0 -3
- package/dist/src/compose/models/planModels.d.ts +0 -562
- package/dist/src/compose/models/rewardModels.d.ts +0 -52
- package/dist/src/compose/models/variantModels.d.ts +0 -34
- package/readme.md +0 -186
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { ListSandboxesResponse } from "../generated/client";
|
|
3
|
+
import { stripTerminalControls } from "./stripTerminalControls";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Headless rendering, like the preview: a table a terminal or a CI log can
|
|
7
|
+
* read. Nothing here decides anything — it reports what `sandboxes.list` said.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SandboxRow = ListSandboxesResponse["list"][number];
|
|
11
|
+
|
|
12
|
+
const MINUTE = 60_000;
|
|
13
|
+
const HOUR = 60 * MINUTE;
|
|
14
|
+
const DAY = 24 * HOUR;
|
|
15
|
+
const MONTH = 30 * DAY;
|
|
16
|
+
const YEAR = 365 * DAY;
|
|
17
|
+
|
|
18
|
+
export const NO_SANDBOXES =
|
|
19
|
+
"No sandboxes yet. Create one with atmn sandbox create <name>.";
|
|
20
|
+
|
|
21
|
+
const HEADERS = ["ID", "NAME", "CREATED"] as const;
|
|
22
|
+
const GAP = " ";
|
|
23
|
+
const CURRENT_MARKER = "← current";
|
|
24
|
+
|
|
25
|
+
/** How old, in the terms a reader thinks in; `--json` carries the exact stamp. */
|
|
26
|
+
export const relativeAge = ({
|
|
27
|
+
createdAt,
|
|
28
|
+
now,
|
|
29
|
+
}: {
|
|
30
|
+
createdAt: number;
|
|
31
|
+
now: number;
|
|
32
|
+
}): string => {
|
|
33
|
+
const elapsed = Math.max(0, now - createdAt);
|
|
34
|
+
if (elapsed < MINUTE) return "just now";
|
|
35
|
+
if (elapsed < HOUR) return `${Math.floor(elapsed / MINUTE)}m ago`;
|
|
36
|
+
if (elapsed < DAY) return `${Math.floor(elapsed / HOUR)}h ago`;
|
|
37
|
+
if (elapsed < MONTH) return `${Math.floor(elapsed / DAY)}d ago`;
|
|
38
|
+
if (elapsed < YEAR) return `${Math.floor(elapsed / MONTH)}mo ago`;
|
|
39
|
+
return `${Math.floor(elapsed / YEAR)}y ago`;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const columnWidths = ({ rows }: { rows: string[][] }): number[] =>
|
|
43
|
+
HEADERS.map((header, column) =>
|
|
44
|
+
Math.max(header.length, ...rows.map((row) => (row[column] ?? "").length)),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const tableLine = ({
|
|
48
|
+
cells,
|
|
49
|
+
widths,
|
|
50
|
+
marker,
|
|
51
|
+
}: {
|
|
52
|
+
cells: readonly string[];
|
|
53
|
+
widths: number[];
|
|
54
|
+
marker: string;
|
|
55
|
+
}): string =>
|
|
56
|
+
`${cells
|
|
57
|
+
.map((cell, column) => cell.padEnd(widths[column] ?? 0))
|
|
58
|
+
.join(GAP)}${marker}`.trimEnd();
|
|
59
|
+
|
|
60
|
+
export const renderSandboxes = ({
|
|
61
|
+
sandboxes,
|
|
62
|
+
currentSandboxId,
|
|
63
|
+
now = Date.now(),
|
|
64
|
+
}: {
|
|
65
|
+
sandboxes: SandboxRow[];
|
|
66
|
+
/** The sandbox `--sandbox` or AUTUMN_SANDBOX_ID points at, if any. */
|
|
67
|
+
currentSandboxId?: string;
|
|
68
|
+
now?: number;
|
|
69
|
+
}): string => {
|
|
70
|
+
if (sandboxes.length === 0) return chalk.dim(NO_SANDBOXES);
|
|
71
|
+
|
|
72
|
+
const rows = sandboxes.map((sandbox) => [
|
|
73
|
+
sandbox.id,
|
|
74
|
+
stripTerminalControls(sandbox.name),
|
|
75
|
+
relativeAge({ createdAt: sandbox.createdAt, now }),
|
|
76
|
+
]);
|
|
77
|
+
const widths = columnWidths({ rows });
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
chalk.dim(tableLine({ cells: HEADERS, widths, marker: "" })),
|
|
81
|
+
...rows.map((cells, index) =>
|
|
82
|
+
tableLine({
|
|
83
|
+
cells,
|
|
84
|
+
widths,
|
|
85
|
+
marker:
|
|
86
|
+
sandboxes[index]?.id === currentSandboxId
|
|
87
|
+
? chalk.dim(`${GAP}${CURRENT_MARKER}`)
|
|
88
|
+
: "",
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
91
|
+
].join("\n");
|
|
92
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESC-initiated sequences: CSI (`ESC [ ... final`), OSC (`ESC ] ... BEL|ST`),
|
|
3
|
+
* and the two-character escapes. Matched before the bare control characters,
|
|
4
|
+
* so a whole sequence goes rather than just its ESC.
|
|
5
|
+
*/
|
|
6
|
+
const ESCAPE_SEQUENCE =
|
|
7
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching them is the point
|
|
8
|
+
/\u001b(?:\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]|\][\s\S]*?(?:\u0007|\u001b\\|$)|[\x40-\x5a\x5c-\x5f])/g;
|
|
9
|
+
|
|
10
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: C0 controls, DEL and C1
|
|
11
|
+
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Text a terminal prints as text. A name comes from the server, which only
|
|
15
|
+
* requires its derived slug to hold a letter or a digit, so an escape sequence
|
|
16
|
+
* in one could otherwise repaint or disguise the output around it.
|
|
17
|
+
*/
|
|
18
|
+
export const stripTerminalControls = (text: string): string =>
|
|
19
|
+
text.replace(ESCAPE_SEQUENCE, "").replace(CONTROL_CHARACTER, "");
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, parse } from "node:path";
|
|
4
|
+
|
|
5
|
+
const MONOREPO_MARKERS = [
|
|
6
|
+
"turbo.json",
|
|
7
|
+
"pnpm-workspace.yaml",
|
|
8
|
+
"pnpm-workspace.yml",
|
|
9
|
+
"lerna.json",
|
|
10
|
+
"nx.json",
|
|
11
|
+
"rush.json",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type RepoLayout = {
|
|
15
|
+
/** Git toplevel, else the outermost directory holding a package.json. */
|
|
16
|
+
repoRoot: string;
|
|
17
|
+
/** Nearest ancestor with a package.json — where generated files belong. */
|
|
18
|
+
packageRoot: string;
|
|
19
|
+
/** True when repoRoot shows a workspace marker and differs from packageRoot. */
|
|
20
|
+
isMonorepo: boolean;
|
|
21
|
+
/** True when the root declares workspaces at all: where init offers a package of its own. */
|
|
22
|
+
hasWorkspaces: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const gitToplevel = ({ cwd }: { cwd: string }): string | null => {
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
28
|
+
cwd,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
31
|
+
}).trim();
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const nearestPackageDir = ({ from }: { from: string }): string | null => {
|
|
38
|
+
let current = from;
|
|
39
|
+
const { root } = parse(current);
|
|
40
|
+
while (true) {
|
|
41
|
+
if (existsSync(join(current, "package.json"))) return current;
|
|
42
|
+
if (current === root) return null;
|
|
43
|
+
current = dirname(current);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const declaresWorkspaces = ({ dir }: { dir: string }): boolean => {
|
|
48
|
+
const manifest = join(dir, "package.json");
|
|
49
|
+
if (!existsSync(manifest)) return false;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(manifest, "utf8")).workspaces !== undefined;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Cheap probes, not a package-graph resolver — enough to place generated files
|
|
59
|
+
* sensibly and to tell the user which package they are pointed at.
|
|
60
|
+
*/
|
|
61
|
+
export const findRepoLayout = ({
|
|
62
|
+
cwd = process.cwd(),
|
|
63
|
+
}: {
|
|
64
|
+
cwd?: string;
|
|
65
|
+
} = {}): RepoLayout => {
|
|
66
|
+
const packageRoot = nearestPackageDir({ from: cwd }) ?? cwd;
|
|
67
|
+
const repoRoot = gitToplevel({ cwd }) ?? packageRoot;
|
|
68
|
+
|
|
69
|
+
const hasMarker =
|
|
70
|
+
MONOREPO_MARKERS.some((marker) => existsSync(join(repoRoot, marker))) ||
|
|
71
|
+
declaresWorkspaces({ dir: repoRoot });
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
repoRoot,
|
|
75
|
+
packageRoot,
|
|
76
|
+
isMonorepo: hasMarker && repoRoot !== packageRoot,
|
|
77
|
+
hasWorkspaces: hasMarker,
|
|
78
|
+
};
|
|
79
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Edit, SgNode } from "@ast-grep/napi";
|
|
2
|
+
import { leadingIndentOfLine, lineStartOf } from "./fixtureEdit";
|
|
3
|
+
|
|
4
|
+
const isMember = (child: SgNode): boolean =>
|
|
5
|
+
child.kind() === "pair" || child.kind() === "spread_element";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Append a pair after the literal's last member, spreads counted. A spread
|
|
9
|
+
* that appears later overrides an earlier key at runtime, so a pair this
|
|
10
|
+
* rewriter means to take effect only wins from the end.
|
|
11
|
+
*/
|
|
12
|
+
export const appendPropertyEdit = ({
|
|
13
|
+
source,
|
|
14
|
+
object,
|
|
15
|
+
pair,
|
|
16
|
+
}: {
|
|
17
|
+
source: string;
|
|
18
|
+
object: SgNode;
|
|
19
|
+
pair: string;
|
|
20
|
+
}): Edit => {
|
|
21
|
+
const closing = object.range().end.index - 1;
|
|
22
|
+
const multiline = source
|
|
23
|
+
.slice(object.range().start.index, closing)
|
|
24
|
+
.includes("\n");
|
|
25
|
+
const members = object.children().filter(isMember);
|
|
26
|
+
const last = members[members.length - 1];
|
|
27
|
+
if (last === undefined) {
|
|
28
|
+
return multiline
|
|
29
|
+
? {
|
|
30
|
+
startPos: lineStartOf(source, closing),
|
|
31
|
+
endPos: lineStartOf(source, closing),
|
|
32
|
+
insertedText: `${leadingIndentOfLine(source, object.range().start.index)}\t${pair},\n`,
|
|
33
|
+
}
|
|
34
|
+
: {
|
|
35
|
+
startPos: object.range().start.index + 1,
|
|
36
|
+
endPos: closing,
|
|
37
|
+
insertedText: ` ${pair} `,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const lastEnd = last.range().end.index;
|
|
41
|
+
const between = source.slice(lastEnd, closing);
|
|
42
|
+
const trailingComma = between.trimStart().startsWith(",");
|
|
43
|
+
const afterComma = trailingComma
|
|
44
|
+
? lastEnd + between.indexOf(",") + 1
|
|
45
|
+
: lastEnd;
|
|
46
|
+
if (multiline) {
|
|
47
|
+
// Anchored to the last member itself, so a value whose closing brace shares
|
|
48
|
+
// the object's closing line can never push the insert outside the literal.
|
|
49
|
+
const indent = leadingIndentOfLine(source, last.range().start.index);
|
|
50
|
+
return {
|
|
51
|
+
startPos: afterComma,
|
|
52
|
+
endPos: afterComma,
|
|
53
|
+
insertedText: trailingComma
|
|
54
|
+
? `\n${indent}${pair},`
|
|
55
|
+
: `,\n${indent}${pair},`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
startPos: afterComma,
|
|
60
|
+
endPos: afterComma,
|
|
61
|
+
insertedText: trailingComma ? ` ${pair},` : `, ${pair}`,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Whether a literal is built from a spread, so an inserted pair has to go last. */
|
|
66
|
+
export const holdsSpread = ({ object }: { object: SgNode }): boolean =>
|
|
67
|
+
object.children().some((child) => child.kind() === "spread_element");
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { SgNode } from "@ast-grep/napi";
|
|
2
|
+
import { leadingIndentOfLine, lineStartOf } from "./fixtureEdit";
|
|
3
|
+
|
|
4
|
+
/** A function is built against the indent its first line will receive. */
|
|
5
|
+
export type AppendText = string | ((elementIndent: string) => string);
|
|
6
|
+
|
|
7
|
+
/** Append one element to an array literal, matching its siblings' indent and
|
|
8
|
+
* comma style; an empty array is reflowed onto its own lines. */
|
|
9
|
+
export const appendElementToArray = ({
|
|
10
|
+
source,
|
|
11
|
+
root,
|
|
12
|
+
array,
|
|
13
|
+
text,
|
|
14
|
+
}: {
|
|
15
|
+
source: string;
|
|
16
|
+
root: SgNode;
|
|
17
|
+
array: SgNode;
|
|
18
|
+
text: AppendText;
|
|
19
|
+
}): string => {
|
|
20
|
+
const elements = array.namedChildren();
|
|
21
|
+
if (elements.length === 0) {
|
|
22
|
+
const indent = leadingIndentOfLine(source, array.range().start.index);
|
|
23
|
+
// The seeded element's first line lands one tab deeper than the array.
|
|
24
|
+
const resolved = resolveText({ text, elementIndent: `${indent}\t` });
|
|
25
|
+
return root.commitEdits([
|
|
26
|
+
{
|
|
27
|
+
startPos: array.range().start.index,
|
|
28
|
+
endPos: array.range().end.index,
|
|
29
|
+
insertedText: `[\n${indent}\t${resolved},\n${indent}]`,
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
const last = elements[elements.length - 1];
|
|
34
|
+
const lastEnd = last.range().end.index;
|
|
35
|
+
const after = source.slice(lastEnd);
|
|
36
|
+
const commaAfter = after.indexOf(",");
|
|
37
|
+
const hasTrailingComma =
|
|
38
|
+
commaAfter !== -1 && after.slice(0, commaAfter).trim() === "";
|
|
39
|
+
const insertAt = hasTrailingComma ? lastEnd + commaAfter + 1 : lastEnd;
|
|
40
|
+
const missingComma = hasTrailingComma ? "" : ",";
|
|
41
|
+
const spansLines = source
|
|
42
|
+
.slice(array.range().start.index, last.range().start.index)
|
|
43
|
+
.includes("\n");
|
|
44
|
+
if (spansLines) {
|
|
45
|
+
// Sibling indent: the whitespace opening the last element's line.
|
|
46
|
+
const indent = leadingIndentOfLine(source, last.range().start.index);
|
|
47
|
+
const resolved = resolveText({ text, elementIndent: indent });
|
|
48
|
+
return root.commitEdits([
|
|
49
|
+
{
|
|
50
|
+
startPos: insertAt,
|
|
51
|
+
endPos: insertAt,
|
|
52
|
+
insertedText: `${missingComma}\n${indent}${resolved},`,
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
const lineIndent = leadingIndentOfLine(source, array.range().start.index);
|
|
57
|
+
const elementIndent = `${lineIndent}\t`;
|
|
58
|
+
const resolved = resolveText({ text, elementIndent });
|
|
59
|
+
if (!resolved.includes("\n")) {
|
|
60
|
+
return root.commitEdits([
|
|
61
|
+
{
|
|
62
|
+
startPos: insertAt,
|
|
63
|
+
endPos: insertAt,
|
|
64
|
+
insertedText: `${missingComma} ${resolved},`,
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
// A one-line array cannot hold a multi-line element inline: reflow it.
|
|
69
|
+
const lines = [...elements.map((element) => element.text()), resolved].map(
|
|
70
|
+
(element) => `${elementIndent}${element},`,
|
|
71
|
+
);
|
|
72
|
+
return root.commitEdits([
|
|
73
|
+
{
|
|
74
|
+
startPos: array.range().start.index,
|
|
75
|
+
endPos: array.range().end.index,
|
|
76
|
+
insertedText: `[\n${lines.join("\n")}\n${lineIndent}]`,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const resolveText = ({
|
|
82
|
+
text,
|
|
83
|
+
elementIndent,
|
|
84
|
+
}: {
|
|
85
|
+
text: AppendText;
|
|
86
|
+
elementIndent: string;
|
|
87
|
+
}): string => (typeof text === "function" ? text(elementIndent) : text);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
2
|
+
import { type AppendText, appendElementToArray } from "./appendToArray";
|
|
3
|
+
import { findArrayBinding } from "./arrayBinding";
|
|
4
|
+
|
|
5
|
+
/** Append to `const name = [...]`, exported or not; null when no such array literal exists. */
|
|
6
|
+
export const appendToBinding = ({
|
|
7
|
+
source,
|
|
8
|
+
name,
|
|
9
|
+
text,
|
|
10
|
+
}: {
|
|
11
|
+
source: string;
|
|
12
|
+
name: string;
|
|
13
|
+
text: AppendText;
|
|
14
|
+
}): string | null => {
|
|
15
|
+
const root = parse(Lang.TypeScript, source).root();
|
|
16
|
+
const array = findArrayBinding({ root, name });
|
|
17
|
+
if (array === null) return null;
|
|
18
|
+
return appendElementToArray({ source, root, array, text });
|
|
19
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SgNode } from "@ast-grep/napi";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
import { type AppendText, appendElementToArray } from "./appendToArray";
|
|
4
|
+
|
|
5
|
+
export const appendToCollection = ({
|
|
6
|
+
source,
|
|
7
|
+
collection,
|
|
8
|
+
text,
|
|
9
|
+
}: {
|
|
10
|
+
source: string;
|
|
11
|
+
collection: string;
|
|
12
|
+
text: AppendText;
|
|
13
|
+
}): string | null => {
|
|
14
|
+
const root = parse(Lang.TypeScript, source).root();
|
|
15
|
+
// A bare `collection: [$$$]` parses as a type annotation, so the anchor must
|
|
16
|
+
// be the enclosing atmn call; the leading/trailing $$$ allow sibling keys.
|
|
17
|
+
const anchor =
|
|
18
|
+
root.find(`atmn({ $$$, ${collection}: [$$$ITEMS], $$$ })`) ??
|
|
19
|
+
root.find(`atmn({ ${collection}: [$$$ITEMS] })`);
|
|
20
|
+
if (anchor === null) return null;
|
|
21
|
+
const array = collectionArray({ anchor, collection });
|
|
22
|
+
if (array === null) return null;
|
|
23
|
+
return appendElementToArray({ source, root, array, text });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const collectionArray = ({
|
|
27
|
+
anchor,
|
|
28
|
+
collection,
|
|
29
|
+
}: {
|
|
30
|
+
anchor: SgNode;
|
|
31
|
+
collection: string;
|
|
32
|
+
}): SgNode | null => {
|
|
33
|
+
for (const array of anchor.findAll({ rule: { kind: "array" } })) {
|
|
34
|
+
const pair = array.parent();
|
|
35
|
+
if (pair === null || pair.kind() !== "pair") continue;
|
|
36
|
+
const key = pair.namedChildren()[0];
|
|
37
|
+
if (key !== undefined && key.text() === collection) return array;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
2
|
+
import { appendPropertyEdit } from "./appendPropertyEdit";
|
|
3
|
+
import { type AppendText, appendElementToArray } from "./appendToArray";
|
|
4
|
+
import {
|
|
5
|
+
type FixtureConstraint,
|
|
6
|
+
type FixtureShape,
|
|
7
|
+
findFixture,
|
|
8
|
+
fixtureObjectOf,
|
|
9
|
+
} from "./findFixture";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Append one element to an array property of a fixture literal
|
|
13
|
+
* (`plan({ variants: [...] })`), seeding the property when absent.
|
|
14
|
+
* Null when the fixture is not there or the property is not an array.
|
|
15
|
+
*/
|
|
16
|
+
export const appendToFixtureArray = ({
|
|
17
|
+
source,
|
|
18
|
+
builder,
|
|
19
|
+
idField,
|
|
20
|
+
id,
|
|
21
|
+
where,
|
|
22
|
+
property,
|
|
23
|
+
text,
|
|
24
|
+
}: {
|
|
25
|
+
source: string;
|
|
26
|
+
builder: FixtureShape;
|
|
27
|
+
idField: string;
|
|
28
|
+
id: string;
|
|
29
|
+
where?: FixtureConstraint[];
|
|
30
|
+
property: string;
|
|
31
|
+
text: AppendText;
|
|
32
|
+
}): string | null => {
|
|
33
|
+
const call = findFixture({
|
|
34
|
+
source,
|
|
35
|
+
builder,
|
|
36
|
+
idField,
|
|
37
|
+
id,
|
|
38
|
+
where,
|
|
39
|
+
allowDynamic: true,
|
|
40
|
+
});
|
|
41
|
+
if (call === null) return null;
|
|
42
|
+
const object = fixtureObjectOf(call);
|
|
43
|
+
if (object === null) return null;
|
|
44
|
+
const root = parse(Lang.TypeScript, source).root();
|
|
45
|
+
|
|
46
|
+
const pair = object
|
|
47
|
+
.children()
|
|
48
|
+
.find(
|
|
49
|
+
(member) =>
|
|
50
|
+
member.kind() === "pair" &&
|
|
51
|
+
member.namedChildren()[0]?.text() === property,
|
|
52
|
+
);
|
|
53
|
+
if (pair === undefined) {
|
|
54
|
+
// Seed the key, then re-find: the edit shifts every node position.
|
|
55
|
+
const seeded = root.commitEdits([
|
|
56
|
+
appendPropertyEdit({ source, object, pair: `${property}: []` }),
|
|
57
|
+
]);
|
|
58
|
+
return appendToFixtureArray({
|
|
59
|
+
source: seeded,
|
|
60
|
+
builder,
|
|
61
|
+
idField,
|
|
62
|
+
id,
|
|
63
|
+
where,
|
|
64
|
+
property,
|
|
65
|
+
text,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const array = pair.namedChildren()[1];
|
|
69
|
+
if (array === undefined || array.kind() !== "array") return null;
|
|
70
|
+
return appendElementToArray({ source, root, array, text });
|
|
71
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SgNode } from "@ast-grep/napi";
|
|
2
|
+
|
|
3
|
+
/** The literal of `kind` initializing `const name` (typed or not, exported or
|
|
4
|
+
* not); null when no such declaration holds one. */
|
|
5
|
+
export const findLiteralBinding = ({
|
|
6
|
+
root,
|
|
7
|
+
name,
|
|
8
|
+
kind,
|
|
9
|
+
}: {
|
|
10
|
+
root: SgNode;
|
|
11
|
+
name: string;
|
|
12
|
+
kind: "array" | "object";
|
|
13
|
+
}): SgNode | null => {
|
|
14
|
+
for (const declarator of root.findAll({
|
|
15
|
+
rule: { kind: "variable_declarator" },
|
|
16
|
+
})) {
|
|
17
|
+
if (declarator.field("name")?.text() !== name) continue;
|
|
18
|
+
const value = declarator.field("value");
|
|
19
|
+
if (value !== null && value.kind() === kind) return value;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const findArrayBinding = ({
|
|
25
|
+
root,
|
|
26
|
+
name,
|
|
27
|
+
}: {
|
|
28
|
+
root: SgNode;
|
|
29
|
+
name: string;
|
|
30
|
+
}): SgNode | null => findLiteralBinding({ root, name, kind: "array" });
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { SgNode } from "@ast-grep/napi";
|
|
2
|
+
import { type FixtureConstraint, findFixture } from "./findFixture";
|
|
3
|
+
import { lineStartOf, removeArrayElementEdits } from "./fixtureEdit";
|
|
4
|
+
|
|
5
|
+
export const deleteFixtureLiteral = ({
|
|
6
|
+
source,
|
|
7
|
+
builder,
|
|
8
|
+
idField,
|
|
9
|
+
id,
|
|
10
|
+
where,
|
|
11
|
+
}: {
|
|
12
|
+
source: string;
|
|
13
|
+
builder: string;
|
|
14
|
+
idField: string;
|
|
15
|
+
id: string;
|
|
16
|
+
where?: FixtureConstraint[];
|
|
17
|
+
}): { source: string; exportedName?: string } | null => {
|
|
18
|
+
const call = findFixture({ source, builder, idField, id, where });
|
|
19
|
+
if (call === null) return null;
|
|
20
|
+
const root = call.getRoot().root();
|
|
21
|
+
// The nearer ancestor wins: an element of `export default atmn({ features: [
|
|
22
|
+
// ...] })` is still an array element, not an exported fixture statement.
|
|
23
|
+
let statement: SgNode | null = null;
|
|
24
|
+
let isArrayElement = false;
|
|
25
|
+
for (const ancestor of call.ancestors()) {
|
|
26
|
+
if (ancestor.kind() === "array") {
|
|
27
|
+
isArrayElement = true;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
if (ancestor.kind() === "export_statement") {
|
|
31
|
+
statement = ancestor;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (statement !== null && !isArrayElement) {
|
|
36
|
+
const declarator = statement.find({
|
|
37
|
+
rule: { kind: "variable_declarator" },
|
|
38
|
+
});
|
|
39
|
+
const name = declarator?.namedChildren()[0];
|
|
40
|
+
if (name === undefined || name.kind() !== "identifier") return null;
|
|
41
|
+
return {
|
|
42
|
+
source: root.commitEdits([exportRemovalEdit({ source, statement })]),
|
|
43
|
+
exportedName: name.text(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const parent = call.parent();
|
|
47
|
+
if (parent === null || parent.kind() !== "array") return null;
|
|
48
|
+
const elementEdits = removeArrayElementEdits({ source, element: call });
|
|
49
|
+
if (elementEdits === null) return null;
|
|
50
|
+
return { source: root.commitEdits(elementEdits) };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remove the whole statement line, a doc comment directly above it (exactly one
|
|
55
|
+
* newline between comment and statement), and the blank line above the comment.
|
|
56
|
+
*/
|
|
57
|
+
const exportRemovalEdit = ({
|
|
58
|
+
source,
|
|
59
|
+
statement,
|
|
60
|
+
}: {
|
|
61
|
+
source: string;
|
|
62
|
+
statement: SgNode;
|
|
63
|
+
}): { startPos: number; endPos: number; insertedText: string } => {
|
|
64
|
+
const statementLineStart = lineStartOf(source, statement.range().start.index);
|
|
65
|
+
let deleteStart = statementLineStart;
|
|
66
|
+
const comment = statement.prev();
|
|
67
|
+
if (
|
|
68
|
+
comment !== null &&
|
|
69
|
+
comment.kind() === "comment" &&
|
|
70
|
+
source.slice(comment.range().end.index, statementLineStart) === "\n"
|
|
71
|
+
) {
|
|
72
|
+
deleteStart = lineStartOf(source, comment.range().start.index);
|
|
73
|
+
const lineAbove = lineStartOf(source, deleteStart - 1);
|
|
74
|
+
if (deleteStart > 0 && source.slice(lineAbove, deleteStart).trim() === "") {
|
|
75
|
+
deleteStart = lineAbove;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const lineEnd = source.indexOf("\n", statement.range().end.index);
|
|
79
|
+
const deleteEnd = lineEnd === -1 ? source.length : lineEnd + 1;
|
|
80
|
+
return { startPos: deleteStart, endPos: deleteEnd, insertedText: "" };
|
|
81
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Edit } from "@ast-grep/napi";
|
|
2
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
3
|
+
import {
|
|
4
|
+
importStatementOf,
|
|
5
|
+
removeArrayElementEdits,
|
|
6
|
+
removeLineEdit,
|
|
7
|
+
removeSpecifierEdit,
|
|
8
|
+
} from "./fixtureEdit";
|
|
9
|
+
|
|
10
|
+
export const deleteReference = ({
|
|
11
|
+
source,
|
|
12
|
+
name,
|
|
13
|
+
}: {
|
|
14
|
+
source: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}): string => {
|
|
17
|
+
const root = parse(Lang.TypeScript, source).root();
|
|
18
|
+
const edits: Array<Edit> = [];
|
|
19
|
+
for (const identifier of root.findAll({
|
|
20
|
+
rule: { kind: "identifier", pattern: name },
|
|
21
|
+
})) {
|
|
22
|
+
const parent = identifier.parent();
|
|
23
|
+
if (parent === null || parent.kind() !== "array") continue;
|
|
24
|
+
const elementEdits = removeArrayElementEdits({
|
|
25
|
+
source,
|
|
26
|
+
element: identifier,
|
|
27
|
+
});
|
|
28
|
+
if (elementEdits !== null) edits.push(...elementEdits);
|
|
29
|
+
}
|
|
30
|
+
for (const specifier of root.findAll({
|
|
31
|
+
rule: { kind: "import_specifier" },
|
|
32
|
+
})) {
|
|
33
|
+
const names = specifier
|
|
34
|
+
.text()
|
|
35
|
+
.trim()
|
|
36
|
+
.split(/\s+as\s+/);
|
|
37
|
+
if (names[names.length - 1] !== name) continue;
|
|
38
|
+
const imports = specifier.parent();
|
|
39
|
+
if (imports !== null && imports.namedChildren().length === 1) {
|
|
40
|
+
const statement = importStatementOf(specifier);
|
|
41
|
+
if (statement !== null)
|
|
42
|
+
edits.push(removeLineEdit({ source, node: statement }));
|
|
43
|
+
} else {
|
|
44
|
+
edits.push(removeSpecifierEdit({ source, specifier }));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return root.commitEdits(edits);
|
|
48
|
+
};
|