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.
Files changed (117) hide show
  1. package/README.md +120 -0
  2. package/dist/bin.js +23102 -0
  3. package/dist/index.js +2906 -0
  4. package/dist/tsconfig.tsbuildinfo +1 -1
  5. package/package.json +27 -74
  6. package/src/actions/api/callApi.ts +280 -0
  7. package/src/actions/api/registerApiCommands.ts +121 -0
  8. package/src/actions/env/fetchOrgInfo.ts +32 -0
  9. package/src/actions/env/types/orgInfo.ts +18 -0
  10. package/src/actions/env.ts +94 -0
  11. package/src/actions/init/runInit.ts +401 -0
  12. package/src/actions/login/keyless.ts +135 -0
  13. package/src/actions/login.ts +144 -0
  14. package/src/actions/pull/appendPlanVersionFixture.ts +373 -0
  15. package/src/actions/pull/applyPreview.ts +553 -0
  16. package/src/actions/pull/applySettingsPreview.ts +114 -0
  17. package/src/actions/pull/changedFixtureKeys.ts +88 -0
  18. package/src/actions/pull/listSourceFiles.ts +29 -0
  19. package/src/actions/pull/locateFixture.ts +78 -0
  20. package/src/actions/pull/resolveCollectionTarget.ts +198 -0
  21. package/src/actions/pull/rewriteConfig.ts +57 -0
  22. package/src/actions/pull/scaffoldConfig.ts +118 -0
  23. package/src/actions/pull.ts +399 -0
  24. package/src/actions/push/backfillInternalIds.ts +394 -0
  25. package/src/actions/push/deprecatedFields.ts +55 -0
  26. package/src/actions/push.ts +294 -0
  27. package/src/actions/reset/runReset.ts +58 -0
  28. package/src/actions/sandbox/createSandbox.ts +83 -0
  29. package/src/actions/sandbox/deleteSandbox.ts +91 -0
  30. package/src/actions/sandbox/listSandboxes.ts +28 -0
  31. package/src/actions/sandbox/types/sandboxClient.ts +16 -0
  32. package/src/actions/sandbox/useSandbox.ts +157 -0
  33. package/src/actions/sandbox/withSandboxScopeHint.ts +27 -0
  34. package/src/actions/skills/skills.ts +246 -0
  35. package/src/auth/announceAuthorizationUrl.ts +26 -0
  36. package/src/auth/browser/openSystemBrowser.ts +7 -0
  37. package/src/auth/browser/tryOpenBrowser.ts +17 -0
  38. package/src/auth/browser/watchLauncher.ts +46 -0
  39. package/src/auth/buildAuthorizationUrl.ts +36 -0
  40. package/src/auth/callbackPages.ts +126 -0
  41. package/src/auth/createOrgApiKeys.ts +46 -0
  42. package/src/auth/keyless.ts +119 -0
  43. package/src/auth/oauthConfig.ts +63 -0
  44. package/src/auth/runOAuthFlow.ts +230 -0
  45. package/src/auth/types/browserOpener.ts +5 -0
  46. package/src/auth/types/impersonationTokens.ts +23 -0
  47. package/src/auth/types/oauthTokens.ts +13 -0
  48. package/src/auth/types/orgApiKeys.ts +6 -0
  49. package/src/bin.ts +9 -0
  50. package/src/cli.ts +648 -0
  51. package/src/config/configPackageName.ts +9 -0
  52. package/src/config/legacyConfig.ts +25 -0
  53. package/src/config/loadConfig.ts +231 -0
  54. package/src/env/assertSandboxTarget.ts +20 -0
  55. package/src/env/loadEnv.ts +184 -0
  56. package/src/env/resolveTarget.ts +134 -0
  57. package/src/env/sandboxKeyName.ts +18 -0
  58. package/src/generated/apiRoutes.ts +3787 -0
  59. package/src/generated/client.ts +51564 -0
  60. package/src/generated/emit.ts +1163 -0
  61. package/src/generated/emitRuntime.ts +522 -0
  62. package/src/generated/features.ts +146 -0
  63. package/src/generated/labels.ts +29 -0
  64. package/src/generated/licenses.ts +287 -0
  65. package/src/generated/lintRules.ts +2207 -0
  66. package/src/generated/lintRuntime.ts +865 -0
  67. package/src/generated/plans.ts +1628 -0
  68. package/src/generated/referralPrograms.ts +22 -0
  69. package/src/generated/rewards.ts +58 -0
  70. package/src/generated/settings.ts +21 -0
  71. package/src/generated/skills.ts +305 -0
  72. package/src/generated/variants.ts +934 -0
  73. package/src/generated/wire.ts +334 -0
  74. package/src/http/autumnFetch.ts +30 -0
  75. package/src/index.ts +20 -0
  76. package/src/project/chooseConfigDir.ts +41 -0
  77. package/src/project/resolveProject.ts +115 -0
  78. package/src/project/rootMarker.ts +40 -0
  79. package/src/prompt/prompt.ts +186 -0
  80. package/src/prompt/select.ts +162 -0
  81. package/src/render/renderEnv.ts +77 -0
  82. package/src/render/renderPreview.ts +945 -0
  83. package/src/render/renderSandboxes.ts +92 -0
  84. package/src/render/stripTerminalControls.ts +19 -0
  85. package/src/repo/findRepoRoot.ts +79 -0
  86. package/src/surgery/appendPropertyEdit.ts +67 -0
  87. package/src/surgery/appendToArray.ts +87 -0
  88. package/src/surgery/appendToBinding.ts +19 -0
  89. package/src/surgery/appendToCollection.ts +40 -0
  90. package/src/surgery/appendToFixtureArray.ts +71 -0
  91. package/src/surgery/arrayBinding.ts +30 -0
  92. package/src/surgery/deleteFixtureLiteral.ts +81 -0
  93. package/src/surgery/deleteReference.ts +48 -0
  94. package/src/surgery/ensureBuilderImport.ts +65 -0
  95. package/src/surgery/findFixture.ts +238 -0
  96. package/src/surgery/fixtureEdit.ts +156 -0
  97. package/src/surgery/fixtureLocation.ts +32 -0
  98. package/src/surgery/insertCollection.ts +86 -0
  99. package/src/surgery/insertFirstProperty.ts +73 -0
  100. package/src/surgery/patchFixtureProperty.ts +152 -0
  101. package/src/surgery/patchSingletonProperty.ts +221 -0
  102. package/src/surgery/replaceFixture.ts +28 -0
  103. package/src/surgery/setFixtureProperty.ts +55 -0
  104. package/src/surgery/staticFixtureRule.ts +48 -0
  105. package/src/version.ts +5 -0
  106. package/dist/cli.js +0 -146296
  107. package/dist/compose/index.js +0 -122
  108. package/dist/src/compose/builders/builderFunctions.d.ts +0 -84
  109. package/dist/src/compose/builders/rewardFunctions.d.ts +0 -5
  110. package/dist/src/compose/builders/variantFunctions.d.ts +0 -2
  111. package/dist/src/compose/index.d.ts +0 -19
  112. package/dist/src/compose/models/featureModels.d.ts +0 -262
  113. package/dist/src/compose/models/index.d.ts +0 -3
  114. package/dist/src/compose/models/planModels.d.ts +0 -562
  115. package/dist/src/compose/models/rewardModels.d.ts +0 -52
  116. package/dist/src/compose/models/variantModels.d.ts +0 -34
  117. package/readme.md +0 -186
@@ -0,0 +1,399 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ unlinkSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { basename, dirname, join } from "node:path";
9
+ import { ConfigNotFoundError, loadConfig } from "../config/loadConfig";
10
+ import { loadEnvFiles } from "../env/loadEnv";
11
+ import { AutumnApiError, type AutumnClient } from "../generated/client";
12
+ import { COLLECTIONS, NESTED_FIXTURES, SINGLETONS } from "../generated/emit";
13
+ import { splitWire, type WireDocument } from "../generated/wire";
14
+ import { chooseConfigDir } from "../project/chooseConfigDir";
15
+ import { resolveProject } from "../project/resolveProject";
16
+ import { writeRootMarker } from "../project/rootMarker";
17
+ import { createPrompter, done, type Prompter } from "../prompt/prompt";
18
+ import type { SettingsPreview } from "../render/renderPreview";
19
+ import { applyPreview, type PreviewEntry } from "./pull/applyPreview";
20
+ import { applySettingsPreview } from "./pull/applySettingsPreview";
21
+ import { listSourceFiles } from "./pull/listSourceFiles";
22
+ import { rewriteConfig } from "./pull/rewriteConfig";
23
+ import {
24
+ type ConfigImports,
25
+ packageImports,
26
+ scaffoldConfig,
27
+ } from "./pull/scaffoldConfig";
28
+ import { configSearchDirs } from "./push";
29
+ import {
30
+ backfillInternalIds,
31
+ identityRowsFromCatalog,
32
+ } from "./push/backfillInternalIds";
33
+
34
+ export type PullResult = {
35
+ configPath: string;
36
+ appended: string[];
37
+ replaced: string[];
38
+ deleted: string[];
39
+ };
40
+
41
+ export type PullOptions = {
42
+ client: AutumnClient;
43
+ cwd?: string;
44
+ /** `-c`: the config file or its folder; found from cwd or the root marker otherwise. */
45
+ configPath?: string;
46
+ /** Keep processor mappings (Stripe product/meter ids) in pulled fixtures. */
47
+ includeMappings?: boolean;
48
+ /** Where to write progress. Injected so tests can capture it. */
49
+ write?: (text: string) => void;
50
+ /** Module specifiers a scaffolded config imports from; the package by default. */
51
+ imports?: ConfigImports;
52
+ /**
53
+ * Rewrite the config and its collection files from the server, as if from
54
+ * an empty directory. Files that do not import the package are left alone.
55
+ * The way out when the file describes a different org than the key targets.
56
+ */
57
+ overwrite?: boolean;
58
+ /** Confirm replacing the local config when overwrite is set. */
59
+ yes?: boolean;
60
+ /** Asks where a first pull should put the config; headless by default. */
61
+ prompter?: Prompter;
62
+ };
63
+
64
+ const OVERWRITE_HINT =
65
+ "Your config no longer matches this org's catalog. To replace it with the server's catalog: atmn pull --overwrite";
66
+
67
+ /** Only a rejected catalog diff earns the overwrite hint. */
68
+ const diffOrExplain = async ({
69
+ client,
70
+ wire,
71
+ }: {
72
+ client: AutumnClient;
73
+ wire: WireDocument;
74
+ }) => {
75
+ try {
76
+ return await client.diff(wire);
77
+ } catch (error) {
78
+ if (error instanceof AutumnApiError && error.status === 400) {
79
+ throw new Error(`${error.message}\n ${OVERWRITE_HINT}`);
80
+ }
81
+ throw error;
82
+ }
83
+ };
84
+
85
+ /** No config means a first pull: scaffold one at `cwd`, then pull into it. */
86
+ const loadOrScaffold = async ({
87
+ dirs,
88
+ cwd,
89
+ configPath,
90
+ existingConfigPath,
91
+ imports,
92
+ overwrite,
93
+ write,
94
+ }: {
95
+ dirs: string[];
96
+ cwd: string;
97
+ /** `-c`: an exact file; scaffolded there when missing. */
98
+ configPath?: string;
99
+ /** The config on disk, when there is one. */
100
+ existingConfigPath: string | null;
101
+ imports: ConfigImports | undefined;
102
+ overwrite: boolean;
103
+ write: (text: string) => void;
104
+ }) => {
105
+ const scaffold = () => {
106
+ const scaffolded = scaffoldConfig({
107
+ directory: cwd,
108
+ ...(configPath === undefined ? {} : { configPath }),
109
+ imports,
110
+ });
111
+ write(`Scaffolded ${scaffolded}\n`);
112
+ return loadConfig({ dirs: [cwd], configPath: scaffolded });
113
+ };
114
+
115
+ // Overwrite never deletes: with no config it is a first pull, otherwise the
116
+ // config and the collection files it owns are rewritten as a fresh shell.
117
+ if (overwrite && existingConfigPath === null) return scaffold();
118
+ if (overwrite && existingConfigPath !== null) {
119
+ const { rewritten, kept } = rewriteConfig({
120
+ configPath: existingConfigPath,
121
+ imports: imports ?? packageImports(),
122
+ });
123
+ write(`Rewrote ${rewritten.map((file) => basename(file)).join(", ")}\n`);
124
+ if (kept.length > 0)
125
+ write(
126
+ `Left alone (not atmn files): ${kept.map((file) => basename(file)).join(", ")}\n`,
127
+ );
128
+ return loadConfig({ dirs: [cwd], configPath: existingConfigPath });
129
+ }
130
+ try {
131
+ return await loadConfig({
132
+ dirs,
133
+ ...(configPath === undefined ? {} : { configPath }),
134
+ });
135
+ } catch (error) {
136
+ if (!(error instanceof ConfigNotFoundError)) throw error;
137
+ return scaffold();
138
+ }
139
+ };
140
+
141
+ /** Preview rows plus the sibling versions nested under them: a sibling has no
142
+ * action of its own, so it is walked as an update and acted on only when its
143
+ * active flag contradicts what the config states. */
144
+ const entriesOf = (value: unknown): PreviewEntry[] => {
145
+ if (!Array.isArray(value)) return [];
146
+ const rows = value as PreviewEntry[];
147
+ return rows.flatMap((row) => {
148
+ const siblings = Array.isArray(row.siblingVersions)
149
+ ? (row.siblingVersions as PreviewEntry[])
150
+ : [];
151
+ return [
152
+ row,
153
+ ...siblings.map((sibling) => ({
154
+ ...sibling,
155
+ action: "update",
156
+ siblingOf: row.planId,
157
+ })),
158
+ ];
159
+ });
160
+ };
161
+
162
+ const rowsOf = (value: unknown): Record<string, unknown>[] =>
163
+ Array.isArray(value) ? (value as Record<string, unknown>[]) : [];
164
+
165
+ const featureTypesOf = ({
166
+ rows,
167
+ }: {
168
+ rows: Record<string, unknown>[];
169
+ }): Readonly<Record<string, string>> => {
170
+ const featureTypes: Record<string, string> = {};
171
+ for (const row of rows) {
172
+ if (typeof row.id === "string" && typeof row.type === "string")
173
+ featureTypes[row.id] = row.type;
174
+ }
175
+ return featureTypes;
176
+ };
177
+
178
+ type VariantEdge = Record<string, unknown> & {
179
+ plan?: { internalId?: unknown; versionSlug?: unknown } | null;
180
+ };
181
+
182
+ /**
183
+ * A variant edge names its identity on the resolved plan, which the fixture
184
+ * never carries; hoisted onto the edge, the written entry always states its
185
+ * stable id and slug, so no pulled variant is left version-less.
186
+ */
187
+ const withVariantIdentity = (
188
+ rows: Record<string, unknown>[],
189
+ ): Record<string, unknown>[] =>
190
+ rows.map((row) => {
191
+ if (!Array.isArray(row.variants)) return row;
192
+ return {
193
+ ...row,
194
+ variants: (row.variants as VariantEdge[]).map((edge) => {
195
+ const { internalId, versionSlug, ...rest } = edge;
196
+ const stableId = internalId ?? edge.plan?.internalId;
197
+ const slug = versionSlug ?? edge.plan?.versionSlug;
198
+ return {
199
+ ...(typeof stableId === "string" ? { internalId: stableId } : {}),
200
+ ...rest,
201
+ ...(typeof slug === "string" ? { versionSlug: slug } : {}),
202
+ };
203
+ }),
204
+ };
205
+ });
206
+
207
+ /**
208
+ * Pull rides preview: the server's diff drives every edit, and the CLI never
209
+ * diffs anything itself. Config sources are held in memory and only files
210
+ * whose text changed go back to disk.
211
+ */
212
+ export const runPull = async ({
213
+ client,
214
+ cwd = process.cwd(),
215
+ configPath: configFlag,
216
+ includeMappings = false,
217
+ write = (text) => process.stdout.write(text),
218
+ imports,
219
+ overwrite = false,
220
+ yes = false,
221
+ prompter = createPrompter({ interactive: false, write }),
222
+ }: PullOptions): Promise<PullResult> => {
223
+ const project = resolveProject({ cwd, configFlag });
224
+ if (overwrite && !yes) {
225
+ write(
226
+ "This rewrites autumn.config.ts and the features.ts, plans.ts and rewards.ts beside it from your org's catalog. Other files are left alone. Re-run with --yes to overwrite.\n",
227
+ );
228
+ return {
229
+ configPath:
230
+ project.configPath ?? join(project.configDir, "autumn.config.ts"),
231
+ appended: [],
232
+ replaced: [],
233
+ deleted: [],
234
+ };
235
+ }
236
+ const dirs = configSearchDirs({ cwd, configPath: configFlag });
237
+ loadEnvFiles({ dirs: project.envDirs });
238
+
239
+ // Nothing states where the config lives: ask, and never scaffold into cwd
240
+ // unasked. A folder other than cwd gets the root marker so later commands
241
+ // find it from anywhere in the repo.
242
+ let scaffoldDir = project.configDir;
243
+ let scaffoldPath = project.configPath ?? undefined;
244
+ if (project.configPath === null) {
245
+ const chosen = await chooseConfigDir({ cwd, prompter });
246
+ scaffoldDir = chosen.configDir;
247
+ scaffoldPath = join(scaffoldDir, "autumn.config.ts");
248
+ if (
249
+ scaffoldDir !== cwd &&
250
+ writeRootMarker({ repoRoot: chosen.repoRoot, configPath: scaffoldPath })
251
+ )
252
+ prompter.write(
253
+ `${done("Recorded in package.json so atmn finds it from anywhere")}\n`,
254
+ );
255
+ }
256
+
257
+ const { path: configPath, wire: document } = await loadOrScaffold({
258
+ dirs,
259
+ cwd: scaffoldDir,
260
+ ...(scaffoldPath === undefined ? {} : { configPath: scaffoldPath }),
261
+ existingConfigPath: project.configPath,
262
+ imports,
263
+ overwrite,
264
+ write,
265
+ });
266
+ const { catalog: wire, singletons } = splitWire(document);
267
+
268
+ const [preview, catalog, settingsPreview] = await Promise.all([
269
+ diffOrExplain({ client, wire }),
270
+ // Every version: each is a row in plans, with `active` on it.
271
+ client.get({ include_versions: true }),
272
+ // Always asked, even with no `settings` stated: a non-default flag the
273
+ // config omits is exactly what a first pull should write.
274
+ client.previewUpdateOrganization(
275
+ singletons.settings ?? { config: {} },
276
+ ) as Promise<SettingsPreview>,
277
+ ]);
278
+
279
+ const files = new Map<string, string>();
280
+ files.set(configPath, readFileSync(configPath, "utf8"));
281
+ for (const file of listSourceFiles({ directory: dirname(configPath) })) {
282
+ // The config is already in the map and is always tried first.
283
+ if (file === configPath || files.has(file)) continue;
284
+ files.set(file, readFileSync(file, "utf8"));
285
+ }
286
+ const originals = new Map(files);
287
+
288
+ const appended: string[] = [];
289
+ const replaced: string[] = [];
290
+ const deleted: string[] = [];
291
+ const lines: string[] = [];
292
+ const unlocated: { collection: string; id: string; action: string }[] = [];
293
+ const previewRows = preview as unknown as Record<string, unknown>;
294
+ const catalogRows = catalog as unknown as Record<string, unknown>;
295
+ const featureTypes = featureTypesOf({ rows: rowsOf(catalogRows.features) });
296
+ const nestedBuilders = Object.fromEntries(
297
+ Object.values(NESTED_FIXTURES).map(({ path, builder }) => [path, builder]),
298
+ );
299
+
300
+ for (const [collection, spec] of Object.entries(COLLECTIONS)) {
301
+ // Versions share an id; until internal_id lands, pull cannot address them.
302
+ if (!spec.pull) continue;
303
+ const applied = applyPreview({
304
+ collection,
305
+ spec,
306
+ entries: entriesOf(previewRows[collection]),
307
+ catalogRows: withVariantIdentity(rowsOf(catalogRows[collection])),
308
+ configPath,
309
+ files,
310
+ includeMappings,
311
+ featureTypes,
312
+ nestedBuilders,
313
+ });
314
+ appended.push(...applied.appended);
315
+ replaced.push(...applied.replaced);
316
+ deleted.push(...applied.deleted);
317
+ lines.push(...applied.lines);
318
+ unlocated.push(
319
+ ...applied.unlocated.map((entry) => ({ collection, ...entry })),
320
+ );
321
+ }
322
+
323
+ for (const [singleton, spec] of Object.entries(SINGLETONS)) {
324
+ const applied = applySettingsPreview({
325
+ singleton,
326
+ spec,
327
+ changes: settingsPreview.config?.changes ?? [],
328
+ stated: singletons[singleton]?.[spec.wireKey] as
329
+ | Record<string, unknown>
330
+ | undefined,
331
+ configPath,
332
+ files,
333
+ });
334
+ lines.push(...applied.lines);
335
+ unlocated.push(
336
+ ...applied.unlocated.map((key) => ({
337
+ collection: singleton,
338
+ id: key,
339
+ action: `set by hand: \`${singleton}\` is not an object literal`,
340
+ })),
341
+ );
342
+ }
343
+
344
+ // A fixture that is not a plain literal cannot be edited in place; saying
345
+ // so before writing anything beats a half-applied pull.
346
+ if (unlocated.length > 0) throw new UnlocatableFixturesError({ unlocated });
347
+
348
+ for (const [file, source] of files) {
349
+ if (source === originals.get(file)) continue;
350
+ mkdirSync(dirname(file), { recursive: true });
351
+ writeFileSync(file, source, "utf8");
352
+ if (!originals.has(file)) {
353
+ const keep = join(dirname(file), ".gitkeep");
354
+ if (existsSync(keep)) unlinkSync(keep);
355
+ }
356
+ }
357
+
358
+ // Fixtures the catalog already knows get their stable id and slug, even
359
+ // when nothing else about them changed: no row is left slug-less.
360
+ const { backfilled, slugged } = backfillInternalIds({
361
+ rows: identityRowsFromCatalog({ catalog: catalogRows }),
362
+ configPath,
363
+ });
364
+ if (backfilled.length > 0)
365
+ lines.push(
366
+ `↳ wrote internalId into ${backfilled.length} fixture${backfilled.length === 1 ? "" : "s"}`,
367
+ );
368
+ if (slugged.length > 0)
369
+ lines.push(
370
+ `↳ wrote versionSlug into ${slugged.length} fixture${slugged.length === 1 ? "" : "s"}`,
371
+ );
372
+
373
+ write(
374
+ lines.length === 0
375
+ ? "Nothing to pull.\n"
376
+ : `${lines.join("\n")}\nPulled.\n`,
377
+ );
378
+
379
+ return { configPath, appended, replaced, deleted };
380
+ };
381
+
382
+ export class UnlocatableFixturesError extends Error {
383
+ constructor({
384
+ unlocated,
385
+ }: {
386
+ unlocated: { collection: string; id: string; action: string }[];
387
+ }) {
388
+ super(
389
+ [
390
+ `atmn pull cannot edit ${unlocated.length} fixture${unlocated.length === 1 ? "" : "s"} — each must be a plain literal (no spreads, helpers or .map()):`,
391
+ ...unlocated.map(
392
+ ({ collection, id, action }) =>
393
+ ` ${collection} ${JSON.stringify(id)}: ${action}`,
394
+ ),
395
+ ].join("\n"),
396
+ );
397
+ this.name = "UnlocatableFixturesError";
398
+ }
399
+ }