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,88 @@
|
|
|
1
|
+
import type { CollectionSpec } from "../../generated/emitRuntime";
|
|
2
|
+
import type { PreviewEntry } from "./applyPreview";
|
|
3
|
+
|
|
4
|
+
const camelOf = (snake: string): string =>
|
|
5
|
+
snake.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
6
|
+
|
|
7
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
8
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
|
|
10
|
+
const NOOP_ACTIONS = new Set(["none", "skip"]);
|
|
11
|
+
|
|
12
|
+
/** A nested lane (licenses, variants) is work when any row in it acts. */
|
|
13
|
+
const laneHasWork = (value: unknown): boolean =>
|
|
14
|
+
Array.isArray(value) &&
|
|
15
|
+
value.some((row) =>
|
|
16
|
+
Object.entries(row as Record<string, unknown>).some(
|
|
17
|
+
([key, entry]) =>
|
|
18
|
+
key.endsWith("ction") &&
|
|
19
|
+
typeof entry === "string" &&
|
|
20
|
+
!NOOP_ACTIONS.has(entry),
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The top-level fixture keys a preview update touches, in patch order:
|
|
26
|
+
* content first, then version slug, then the id, so each lookup still finds
|
|
27
|
+
* the fixture by what the config said before. Null when the preview does not
|
|
28
|
+
* name its fields, which means the whole fixture is rewritten instead.
|
|
29
|
+
*/
|
|
30
|
+
export const changedFixtureKeys = ({
|
|
31
|
+
spec,
|
|
32
|
+
entry,
|
|
33
|
+
includeMappings,
|
|
34
|
+
fixtureId,
|
|
35
|
+
rowId,
|
|
36
|
+
}: {
|
|
37
|
+
spec: CollectionSpec;
|
|
38
|
+
entry: PreviewEntry;
|
|
39
|
+
includeMappings: boolean;
|
|
40
|
+
/** The public id the located fixture states; null when not a literal string. */
|
|
41
|
+
fixtureId: string | null;
|
|
42
|
+
/** The id the server row carries now. */
|
|
43
|
+
rowId: unknown;
|
|
44
|
+
}): string[] | null => {
|
|
45
|
+
const keys = new Set<string>();
|
|
46
|
+
const known = new Set(spec.keys);
|
|
47
|
+
const addAttribute = (attribute: string): boolean => {
|
|
48
|
+
const key = camelOf(attribute);
|
|
49
|
+
if (key === "id" || key === spec.idField || key === "internalId")
|
|
50
|
+
return true;
|
|
51
|
+
// Mappings ride include_mappings, never a diff.
|
|
52
|
+
if (key === "processors" && !includeMappings) return true;
|
|
53
|
+
if (!known.has(key)) return false;
|
|
54
|
+
keys.add(key);
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const planChange = entry.planChange;
|
|
59
|
+
if (isObject(planChange)) {
|
|
60
|
+
const previous = planChange.previousAttributes;
|
|
61
|
+
if (isObject(previous)) {
|
|
62
|
+
for (const attribute of Object.keys(previous)) {
|
|
63
|
+
if (!addAttribute(attribute)) return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (planChange.priceChange !== undefined) keys.add("price");
|
|
67
|
+
if (planChange.freeTrialChange !== undefined) keys.add("freeTrial");
|
|
68
|
+
if (Array.isArray(planChange.itemChanges) && planChange.itemChanges.length)
|
|
69
|
+
keys.add("items");
|
|
70
|
+
if (planChange.customize !== undefined) return null;
|
|
71
|
+
}
|
|
72
|
+
if (isObject(entry.previousAttributes)) {
|
|
73
|
+
for (const attribute of Object.keys(entry.previousAttributes)) {
|
|
74
|
+
if (!addAttribute(attribute)) return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (laneHasWork(entry.licenses) && known.has("licenses"))
|
|
78
|
+
keys.add("licenses");
|
|
79
|
+
if (laneHasWork(entry.variants) && known.has("variants"))
|
|
80
|
+
keys.add("variants");
|
|
81
|
+
|
|
82
|
+
const ordered = [...keys].filter((key) => key !== "versionSlug");
|
|
83
|
+
if (entry.newVersionSlug !== undefined || keys.has("versionSlug"))
|
|
84
|
+
ordered.push("versionSlug");
|
|
85
|
+
if (typeof rowId === "string" && fixtureId !== null && rowId !== fixtureId)
|
|
86
|
+
ordered.push(spec.idField);
|
|
87
|
+
return ordered.length === 0 ? null : ordered;
|
|
88
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every `.ts` file under `directory`, recursively. Generated and scratch
|
|
6
|
+
* directories are skipped: a pull must never rewrite its own inputs, and a
|
|
7
|
+
* fixture literal inside `node_modules` or a `.tmp` folder is not the user's.
|
|
8
|
+
*/
|
|
9
|
+
export const listSourceFiles = ({
|
|
10
|
+
directory,
|
|
11
|
+
}: {
|
|
12
|
+
directory: string;
|
|
13
|
+
}): string[] => {
|
|
14
|
+
const files: string[] = [];
|
|
15
|
+
const walk = (dir: string): void => {
|
|
16
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
17
|
+
const path = join(dir, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
if (entry.name === "node_modules" || entry.name.startsWith("."))
|
|
20
|
+
continue;
|
|
21
|
+
walk(path);
|
|
22
|
+
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
23
|
+
files.push(path);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
walk(directory);
|
|
28
|
+
return files;
|
|
29
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { SgNode } from "@ast-grep/napi";
|
|
2
|
+
import {
|
|
3
|
+
type FixtureConstraint,
|
|
4
|
+
type FixtureShape,
|
|
5
|
+
findFixture,
|
|
6
|
+
} from "../../surgery/findFixture";
|
|
7
|
+
|
|
8
|
+
/** The file, its source, the node, and the lookup that found it — reuse the
|
|
9
|
+
* same lookup for the edit, or two versions sharing an id get confused. */
|
|
10
|
+
export type LocatedFixture = {
|
|
11
|
+
file: string;
|
|
12
|
+
source: string;
|
|
13
|
+
node: SgNode;
|
|
14
|
+
builder: FixtureShape;
|
|
15
|
+
idField: string;
|
|
16
|
+
id: string;
|
|
17
|
+
where?: FixtureConstraint[];
|
|
18
|
+
};
|
|
19
|
+
export type { FixtureConstraint };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Where a fixture literal lives: the config file first (the common case), then
|
|
23
|
+
* every other walked source file. A stable id wins over the public one, so a
|
|
24
|
+
* renamed row is still found; `where` narrows rows that share an id, like
|
|
25
|
+
* plan versions. `files` maps absolute paths to their current source, so later
|
|
26
|
+
* edits are seen by later lookups. Several `builder` shapes are tried in order
|
|
27
|
+
* for each lookup, so a stable id in any shape beats a public id in the first.
|
|
28
|
+
*/
|
|
29
|
+
export const locateFixture = ({
|
|
30
|
+
configPath,
|
|
31
|
+
files,
|
|
32
|
+
builder,
|
|
33
|
+
idField,
|
|
34
|
+
id,
|
|
35
|
+
internalId,
|
|
36
|
+
where,
|
|
37
|
+
allowDynamic = false,
|
|
38
|
+
}: {
|
|
39
|
+
configPath: string;
|
|
40
|
+
files: Map<string, string>;
|
|
41
|
+
builder: FixtureShape | FixtureShape[];
|
|
42
|
+
idField: string;
|
|
43
|
+
id: string;
|
|
44
|
+
internalId?: string | null;
|
|
45
|
+
where?: FixtureConstraint[];
|
|
46
|
+
allowDynamic?: boolean;
|
|
47
|
+
}): LocatedFixture | null => {
|
|
48
|
+
const others = [...files.keys()].filter((file) => file !== configPath);
|
|
49
|
+
const ordered = [configPath, ...others];
|
|
50
|
+
const attempts: {
|
|
51
|
+
idField: string;
|
|
52
|
+
id: string;
|
|
53
|
+
where?: FixtureConstraint[];
|
|
54
|
+
}[] = [
|
|
55
|
+
...(typeof internalId === "string"
|
|
56
|
+
? [{ idField: "internalId", id: internalId }]
|
|
57
|
+
: []),
|
|
58
|
+
{ idField, id, where },
|
|
59
|
+
];
|
|
60
|
+
const shapes = Array.isArray(builder) ? builder : [builder];
|
|
61
|
+
for (const attempt of attempts) {
|
|
62
|
+
for (const file of ordered) {
|
|
63
|
+
const source = files.get(file);
|
|
64
|
+
if (source === undefined) continue;
|
|
65
|
+
for (const shape of shapes) {
|
|
66
|
+
const node = findFixture({
|
|
67
|
+
source,
|
|
68
|
+
builder: shape,
|
|
69
|
+
allowDynamic,
|
|
70
|
+
...attempt,
|
|
71
|
+
});
|
|
72
|
+
if (node !== null)
|
|
73
|
+
return { file, source, node, builder: shape, ...attempt };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import type { SgNode } from "@ast-grep/napi";
|
|
3
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
4
|
+
import { findLiteralBinding } from "../../surgery/arrayBinding";
|
|
5
|
+
|
|
6
|
+
export type CollectionTarget =
|
|
7
|
+
| { kind: "inline"; file: string }
|
|
8
|
+
| { kind: "binding"; file: string; name: string };
|
|
9
|
+
|
|
10
|
+
export const collectionTargetReferenceName = ({
|
|
11
|
+
target,
|
|
12
|
+
files,
|
|
13
|
+
collection,
|
|
14
|
+
name,
|
|
15
|
+
fixtureFile,
|
|
16
|
+
}: {
|
|
17
|
+
target: CollectionTarget;
|
|
18
|
+
files: Map<string, string>;
|
|
19
|
+
collection: string;
|
|
20
|
+
name: string;
|
|
21
|
+
fixtureFile: string;
|
|
22
|
+
}): string | null => {
|
|
23
|
+
const source = files.get(target.file);
|
|
24
|
+
if (source === undefined) return null;
|
|
25
|
+
const root = parse(Lang.TypeScript, source).root();
|
|
26
|
+
const array =
|
|
27
|
+
target.kind === "binding"
|
|
28
|
+
? findLiteralBinding({ root, name: target.name, kind: "array" })
|
|
29
|
+
: collectionValue({ root, collection });
|
|
30
|
+
if (array?.kind() !== "array") return null;
|
|
31
|
+
for (const element of array.namedChildren()) {
|
|
32
|
+
if (element.kind() !== "identifier") continue;
|
|
33
|
+
const localName = element.text();
|
|
34
|
+
const imported = importedFrom({ root, name: localName });
|
|
35
|
+
if (imported !== null) {
|
|
36
|
+
const importedFile = moduleFileOf({
|
|
37
|
+
from: target.file,
|
|
38
|
+
specifier: imported.specifier,
|
|
39
|
+
files,
|
|
40
|
+
});
|
|
41
|
+
if (importedFile === fixtureFile && imported.exportedName === name)
|
|
42
|
+
return localName;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (target.file === fixtureFile && localName === name) return localName;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const NAMED_IMPORT =
|
|
51
|
+
/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*["']([^"']+)["']/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Where a collection's array literal lives: inline in the atmn call, a const
|
|
55
|
+
* the call names, or the const behind the array's last `...spread`; a named
|
|
56
|
+
* const may be local or imported from a relative `.ts` file. Null when the
|
|
57
|
+
* value is something this pull cannot append to.
|
|
58
|
+
*/
|
|
59
|
+
export const resolveCollectionTarget = ({
|
|
60
|
+
configPath,
|
|
61
|
+
files,
|
|
62
|
+
collection,
|
|
63
|
+
kind = "array",
|
|
64
|
+
}: {
|
|
65
|
+
configPath: string;
|
|
66
|
+
files: Map<string, string>;
|
|
67
|
+
collection: string;
|
|
68
|
+
/** What the key holds: a collection's array, or a singleton's object. */
|
|
69
|
+
kind?: "array" | "object";
|
|
70
|
+
}): CollectionTarget | null => {
|
|
71
|
+
const source = files.get(configPath);
|
|
72
|
+
if (source === undefined) return null;
|
|
73
|
+
const root = parse(Lang.TypeScript, source).root();
|
|
74
|
+
const value = collectionValue({ root, collection });
|
|
75
|
+
if (value === null) return null;
|
|
76
|
+
if (value.kind() === kind) {
|
|
77
|
+
const spread = kind === "array" ? trailingSpreadName(value) : null;
|
|
78
|
+
return spread === null
|
|
79
|
+
? { kind: "inline", file: configPath }
|
|
80
|
+
: resolveBinding({ root, name: spread, configPath, files, kind });
|
|
81
|
+
}
|
|
82
|
+
// `atmn({ plans })` names the binding without a pair.
|
|
83
|
+
const named =
|
|
84
|
+
value.kind() === "identifier" ||
|
|
85
|
+
value.kind() === "shorthand_property_identifier";
|
|
86
|
+
if (!named) return null;
|
|
87
|
+
return resolveBinding({ root, name: value.text(), configPath, files, kind });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** `[...a, ...b]` appends into `b`; a literal element last means inline. */
|
|
91
|
+
const trailingSpreadName = (array: SgNode): string | null => {
|
|
92
|
+
const elements = array.namedChildren();
|
|
93
|
+
const last = elements[elements.length - 1];
|
|
94
|
+
if (last === undefined || last.kind() !== "spread_element") return null;
|
|
95
|
+
const argument = last.namedChildren()[0];
|
|
96
|
+
return argument?.kind() === "identifier" ? argument.text() : null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const resolveBinding = ({
|
|
100
|
+
root,
|
|
101
|
+
name,
|
|
102
|
+
configPath,
|
|
103
|
+
files,
|
|
104
|
+
kind,
|
|
105
|
+
}: {
|
|
106
|
+
root: SgNode;
|
|
107
|
+
name: string;
|
|
108
|
+
configPath: string;
|
|
109
|
+
files: Map<string, string>;
|
|
110
|
+
kind: "array" | "object";
|
|
111
|
+
}): CollectionTarget | null => {
|
|
112
|
+
if (findLiteralBinding({ root, name, kind }) !== null)
|
|
113
|
+
return { kind: "binding", file: configPath, name };
|
|
114
|
+
const imported = importedFrom({ root, name });
|
|
115
|
+
if (imported === null) return null;
|
|
116
|
+
const file = moduleFileOf({
|
|
117
|
+
from: configPath,
|
|
118
|
+
specifier: imported.specifier,
|
|
119
|
+
files,
|
|
120
|
+
});
|
|
121
|
+
if (file === null) return null;
|
|
122
|
+
const module = parse(Lang.TypeScript, files.get(file) ?? "").root();
|
|
123
|
+
return findLiteralBinding({
|
|
124
|
+
root: module,
|
|
125
|
+
name: imported.exportedName,
|
|
126
|
+
kind,
|
|
127
|
+
}) !== null
|
|
128
|
+
? { kind: "binding", file, name: imported.exportedName }
|
|
129
|
+
: null;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const collectionValue = ({
|
|
133
|
+
root,
|
|
134
|
+
collection,
|
|
135
|
+
}: {
|
|
136
|
+
root: SgNode;
|
|
137
|
+
collection: string;
|
|
138
|
+
}): SgNode | null => {
|
|
139
|
+
const object = root.find("atmn($ARG)")?.getMatch("ARG") ?? null;
|
|
140
|
+
if (object === null || object.kind() !== "object") return null;
|
|
141
|
+
for (const member of object.children()) {
|
|
142
|
+
if (
|
|
143
|
+
member.kind() === "shorthand_property_identifier" &&
|
|
144
|
+
member.text() === collection
|
|
145
|
+
)
|
|
146
|
+
return member;
|
|
147
|
+
if (member.kind() !== "pair") continue;
|
|
148
|
+
const [key, value] = member.namedChildren();
|
|
149
|
+
if (key?.text() === collection && value !== undefined) return value;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** The import binding `name`, with the name it had in the module it came from. */
|
|
155
|
+
const importedFrom = ({
|
|
156
|
+
root,
|
|
157
|
+
name,
|
|
158
|
+
}: {
|
|
159
|
+
root: SgNode;
|
|
160
|
+
name: string;
|
|
161
|
+
}): { specifier: string; exportedName: string } | null => {
|
|
162
|
+
for (const statement of root.findAll({
|
|
163
|
+
rule: { kind: "import_statement" },
|
|
164
|
+
})) {
|
|
165
|
+
const match = NAMED_IMPORT.exec(statement.text());
|
|
166
|
+
if (match === null) continue;
|
|
167
|
+
for (const entry of match[1].split(",")) {
|
|
168
|
+
const [exportedName, localName = exportedName] = entry
|
|
169
|
+
.trim()
|
|
170
|
+
.split(/\s+as\s+/);
|
|
171
|
+
if (localName === name && exportedName !== "")
|
|
172
|
+
return { specifier: match[2], exportedName };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** A relative specifier against the walked files: bare, `.ts`, or a folder index. */
|
|
179
|
+
const moduleFileOf = ({
|
|
180
|
+
from,
|
|
181
|
+
specifier,
|
|
182
|
+
files,
|
|
183
|
+
}: {
|
|
184
|
+
from: string;
|
|
185
|
+
specifier: string;
|
|
186
|
+
files: Map<string, string>;
|
|
187
|
+
}): string | null => {
|
|
188
|
+
if (!specifier.startsWith(".")) return null;
|
|
189
|
+
const byResolved = new Map(
|
|
190
|
+
[...files.keys()].map((file) => [resolve(file), file] as const),
|
|
191
|
+
);
|
|
192
|
+
const base = resolve(dirname(from), specifier);
|
|
193
|
+
for (const candidate of [base, `${base}.ts`, `${base}/index.ts`]) {
|
|
194
|
+
const file = byResolved.get(candidate);
|
|
195
|
+
if (file !== undefined) return file;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
COLLECTION_FILES,
|
|
5
|
+
type ConfigImports,
|
|
6
|
+
collectionFileSource,
|
|
7
|
+
rootSource,
|
|
8
|
+
} from "./scaffoldConfig";
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAMES = ["atmn", "atmn-nightly"];
|
|
11
|
+
|
|
12
|
+
/** A file is ours when it imports the config package; anything else is the user's. */
|
|
13
|
+
export const importsConfigPackage = ({
|
|
14
|
+
text,
|
|
15
|
+
imports,
|
|
16
|
+
}: {
|
|
17
|
+
text: string;
|
|
18
|
+
imports: ConfigImports;
|
|
19
|
+
}): boolean =>
|
|
20
|
+
[imports.atmn, imports.builders, ...PACKAGE_NAMES].some(
|
|
21
|
+
(specifier) =>
|
|
22
|
+
text.includes(`from "${specifier}"`) ||
|
|
23
|
+
text.includes(`from '${specifier}'`),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Overwrite without deleting: the config becomes a fresh shell, and each
|
|
28
|
+
* collection file beside it is rewritten only when it is ours. A collection
|
|
29
|
+
* whose file belongs to the user is kept inline in the config instead.
|
|
30
|
+
*/
|
|
31
|
+
export const rewriteConfig = ({
|
|
32
|
+
configPath,
|
|
33
|
+
imports,
|
|
34
|
+
}: {
|
|
35
|
+
configPath: string;
|
|
36
|
+
imports: ConfigImports;
|
|
37
|
+
}): { rewritten: string[]; kept: string[] } => {
|
|
38
|
+
const configDir = dirname(configPath);
|
|
39
|
+
const importedFiles = new Set<string>();
|
|
40
|
+
const rewritten: string[] = [];
|
|
41
|
+
const kept: string[] = [];
|
|
42
|
+
for (const [file, collections] of Object.entries(COLLECTION_FILES)) {
|
|
43
|
+
const path = join(configDir, file);
|
|
44
|
+
const isUsers =
|
|
45
|
+
existsSync(path) &&
|
|
46
|
+
!importsConfigPackage({ text: readFileSync(path, "utf8"), imports });
|
|
47
|
+
if (isUsers) {
|
|
48
|
+
kept.push(path);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(path, collectionFileSource({ collections, imports }), "utf8");
|
|
52
|
+
importedFiles.add(file);
|
|
53
|
+
rewritten.push(path);
|
|
54
|
+
}
|
|
55
|
+
writeFileSync(configPath, rootSource({ imports, importedFiles }), "utf8");
|
|
56
|
+
return { rewritten: [configPath, ...rewritten], kept };
|
|
57
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { configPackageName } from "../../config/configPackageName";
|
|
4
|
+
import { COLLECTIONS } from "../../generated/emit";
|
|
5
|
+
|
|
6
|
+
/** Where a config imports from: the published package, or generated files in-repo. */
|
|
7
|
+
export type ConfigImports = { atmn: string; builders: string };
|
|
8
|
+
|
|
9
|
+
export const packageImports = (): ConfigImports => {
|
|
10
|
+
const name = configPackageName();
|
|
11
|
+
return { atmn: name, builders: name };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One file per catalog surface. `rewards.ts` holds both reward collections:
|
|
16
|
+
* a referral program names a reward, so they read as one thing.
|
|
17
|
+
*/
|
|
18
|
+
export const COLLECTION_FILES: Readonly<Record<string, readonly string[]>> = {
|
|
19
|
+
"features.ts": ["features"],
|
|
20
|
+
"plans.ts": ["plans"],
|
|
21
|
+
"rewards.ts": ["rewards", "referralPrograms"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const builderImportsFor = ({
|
|
25
|
+
collections,
|
|
26
|
+
specifier,
|
|
27
|
+
}: {
|
|
28
|
+
collections: readonly string[];
|
|
29
|
+
specifier: string;
|
|
30
|
+
}): string => {
|
|
31
|
+
const builders = collections
|
|
32
|
+
.flatMap((name) => {
|
|
33
|
+
const spec = COLLECTIONS[name];
|
|
34
|
+
if (spec === undefined) return [];
|
|
35
|
+
return spec.branches
|
|
36
|
+
? spec.branches.map((branch) => branch.builder)
|
|
37
|
+
: [spec.builder];
|
|
38
|
+
})
|
|
39
|
+
.sort();
|
|
40
|
+
return `import { ${builders.join(", ")} } from "${specifier}";`;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const collectionFileSource = ({
|
|
44
|
+
collections,
|
|
45
|
+
imports,
|
|
46
|
+
}: {
|
|
47
|
+
collections: readonly string[];
|
|
48
|
+
imports: ConfigImports;
|
|
49
|
+
}): string =>
|
|
50
|
+
[
|
|
51
|
+
builderImportsFor({ collections, specifier: imports.builders }),
|
|
52
|
+
"",
|
|
53
|
+
...collections.map((name) => `export const ${name} = [];`),
|
|
54
|
+
"",
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
/** A collection whose file is not imported stays inline as an empty array. */
|
|
58
|
+
export const rootSource = ({
|
|
59
|
+
imports,
|
|
60
|
+
importedFiles = new Set(Object.keys(COLLECTION_FILES)),
|
|
61
|
+
}: {
|
|
62
|
+
imports: ConfigImports;
|
|
63
|
+
importedFiles?: ReadonlySet<string>;
|
|
64
|
+
}): string => {
|
|
65
|
+
const collectionImports: string[] = [];
|
|
66
|
+
const collectionKeys: string[] = [];
|
|
67
|
+
for (const [file, collections] of Object.entries(COLLECTION_FILES)) {
|
|
68
|
+
if (importedFiles.has(file)) {
|
|
69
|
+
collectionImports.push(
|
|
70
|
+
`import { ${collections.join(", ")} } from "./${file.replace(/\.ts$/, "")}";`,
|
|
71
|
+
);
|
|
72
|
+
collectionKeys.push(...collections.map((name) => `\t${name},`));
|
|
73
|
+
} else {
|
|
74
|
+
collectionKeys.push(...collections.map((name) => `\t${name}: [],`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [
|
|
78
|
+
`import { atmn } from "${imports.atmn}";`,
|
|
79
|
+
...collectionImports,
|
|
80
|
+
"",
|
|
81
|
+
"/**",
|
|
82
|
+
" * Your catalog, as code. `atmn pull` fills the collection files from the",
|
|
83
|
+
" * server; `atmn push` makes the server match them.",
|
|
84
|
+
" */",
|
|
85
|
+
"export default atmn({",
|
|
86
|
+
...collectionKeys,
|
|
87
|
+
"\tsettings: {},",
|
|
88
|
+
"});",
|
|
89
|
+
"",
|
|
90
|
+
].join("\n");
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The first pull's starting point: a root config that imports one empty
|
|
95
|
+
* array per collection from its own file, so the pull that follows appends
|
|
96
|
+
* the whole catalog into them.
|
|
97
|
+
*/
|
|
98
|
+
export const scaffoldConfig = ({
|
|
99
|
+
directory,
|
|
100
|
+
configPath = join(directory, "autumn.config.ts"),
|
|
101
|
+
imports = packageImports(),
|
|
102
|
+
}: {
|
|
103
|
+
directory: string;
|
|
104
|
+
/** `-c` may name the file; the folder is created when missing. */
|
|
105
|
+
configPath?: string;
|
|
106
|
+
imports?: ConfigImports;
|
|
107
|
+
}): string => {
|
|
108
|
+
mkdirSync(directory, { recursive: true });
|
|
109
|
+
writeFileSync(configPath, rootSource({ imports }), "utf8");
|
|
110
|
+
// Collection files sit beside the config, wherever `-c` put it.
|
|
111
|
+
const configDir = dirname(configPath);
|
|
112
|
+
for (const [file, collections] of Object.entries(COLLECTION_FILES)) {
|
|
113
|
+
const path = join(configDir, file);
|
|
114
|
+
if (existsSync(path)) continue;
|
|
115
|
+
writeFileSync(path, collectionFileSource({ collections, imports }), "utf8");
|
|
116
|
+
}
|
|
117
|
+
return configPath;
|
|
118
|
+
};
|