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,334 @@
1
+ // Generated by @autumn/atmn-generator from packages/openapi/openapi-internal.yml.
2
+ // Do not edit — run `bun generate` in packages/atmn-generator instead.
3
+
4
+ import type { Feature } from "./features.js";
5
+ import { LINT_RULES } from "./lintRules.js";
6
+ import { ConfigError, lintDocument } from "./lintRuntime.js";
7
+ import type { Plan } from "./plans.js";
8
+ import type { ReferralProgram } from "./referralPrograms.js";
9
+ import type { Reward } from "./rewards.js";
10
+ import type { Settings } from "./settings.js";
11
+
12
+ /** Operators like `$startsWith` are literal API keys, not snake_case fields. */
13
+ const isOperatorKey = (key: string): boolean => key.startsWith("$");
14
+
15
+ /** Only `_` before a LETTER folds — `_1` has no uppercase form to restore. */
16
+ const toSnakeCase = (key: string): string =>
17
+ isOperatorKey(key)
18
+ ? key
19
+ : key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
20
+
21
+ const toCamelCase = (key: string): string =>
22
+ isOperatorKey(key)
23
+ ? key
24
+ : key.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
25
+
26
+ export type PathHints = {
27
+ recordPaths: Set<string>;
28
+ frozenPaths: Set<string>;
29
+ /** Fixture path -> wire key, where the overlay renamed a field. */
30
+ renamedPaths: Map<string, string>;
31
+ /** Wire path -> fixture name: the same renames, read from a response. */
32
+ renamedWirePaths: Map<string, string>;
33
+ };
34
+
35
+ export type WireDocument = Record<string, unknown>;
36
+
37
+ /** Stated relationship entries are complete desired state; no overlay means stock. */
38
+ const clearOmittedRelationshipCustomizes = <T>(row: T): T => {
39
+ if (row === null || typeof row !== "object") return row;
40
+ const source = row as Record<string, unknown>;
41
+ const normalize = (entries: unknown): unknown =>
42
+ Array.isArray(entries)
43
+ ? entries.map((entry) =>
44
+ entry !== null && typeof entry === "object"
45
+ ? {
46
+ ...(entry as Record<string, unknown>),
47
+ customize: (entry as Record<string, unknown>).customize ?? null,
48
+ }
49
+ : entry,
50
+ )
51
+ : entries;
52
+ return {
53
+ ...source,
54
+ ...(source.licenses !== undefined
55
+ ? { licenses: normalize(source.licenses) }
56
+ : {}),
57
+ ...(source.variants !== undefined
58
+ ? { variants: normalize(source.variants) }
59
+ : {}),
60
+ } as T;
61
+ };
62
+
63
+ /**
64
+ * Fixture (camelCase) -> wire (snake_case), stopping where the spec says the
65
+ * data stops being ours. Array indices are elided from the path, so one hint
66
+ * covers every element.
67
+ */
68
+ export const toWire = ({
69
+ value,
70
+ path,
71
+ hints,
72
+ }: {
73
+ value: unknown;
74
+ path: string;
75
+ hints: PathHints;
76
+ }): unknown => {
77
+ if (hints.frozenPaths.has(path)) return value;
78
+
79
+ if (Array.isArray(value)) {
80
+ return value.map((entry) => toWire({ value: entry, path, hints }));
81
+ }
82
+ if (value === null || typeof value !== "object") return value;
83
+
84
+ const source = value as Record<string, unknown>;
85
+
86
+ // A record's keys are the user's; its values are still ours.
87
+ if (hints.recordPaths.has(path)) {
88
+ return Object.fromEntries(
89
+ Object.entries(source).map(([key, entry]) => [
90
+ key,
91
+ toWire({ value: entry, path: `${path}.*`, hints }),
92
+ ]),
93
+ );
94
+ }
95
+
96
+ return Object.fromEntries(
97
+ Object.entries(source).map(([key, entry]) => {
98
+ const childPath = path ? `${path}.${key}` : key;
99
+ return [
100
+ hints.renamedPaths.get(childPath) ?? toSnakeCase(key),
101
+ toWire({ value: entry, path: childPath, hints }),
102
+ ];
103
+ }),
104
+ );
105
+ };
106
+
107
+ /**
108
+ * Wire (snake_case) -> fixture (camelCase), for responses. Hints are recorded
109
+ * in FIXTURE terms, so the path is built from the recased key.
110
+ */
111
+ export const toFixture = ({
112
+ value,
113
+ path,
114
+ hints,
115
+ }: {
116
+ value: unknown;
117
+ path: string;
118
+ hints: PathHints;
119
+ }): unknown => {
120
+ if (hints.frozenPaths.has(path)) return value;
121
+
122
+ if (Array.isArray(value)) {
123
+ return value.map((entry) => toFixture({ value: entry, path, hints }));
124
+ }
125
+ if (value === null || typeof value !== "object") return value;
126
+
127
+ const source = value as Record<string, unknown>;
128
+
129
+ if (hints.recordPaths.has(path)) {
130
+ return Object.fromEntries(
131
+ Object.entries(source).map(([key, entry]) => [
132
+ key,
133
+ toFixture({ value: entry, path: `${path}.*`, hints }),
134
+ ]),
135
+ );
136
+ }
137
+
138
+ return Object.fromEntries(
139
+ Object.entries(source).map(([key, entry]) => {
140
+ const name =
141
+ hints.renamedWirePaths.get(path ? `${path}.${key}` : key) ??
142
+ toCamelCase(key);
143
+ return [
144
+ name,
145
+ toFixture({
146
+ value: entry,
147
+ path: path ? `${path}.${name}` : name,
148
+ hints,
149
+ }),
150
+ ];
151
+ }),
152
+ );
153
+ };
154
+
155
+ export const hintsOf = (hints: {
156
+ recordPaths: readonly string[];
157
+ frozenPaths: readonly string[];
158
+ renamedPaths: Readonly<Record<string, string>>;
159
+ }): PathHints => ({
160
+ recordPaths: new Set(hints.recordPaths),
161
+ frozenPaths: new Set(hints.frozenPaths),
162
+ renamedPaths: new Map(Object.entries(hints.renamedPaths)),
163
+ renamedWirePaths: new Map(
164
+ Object.entries(hints.renamedPaths).map(([fixturePath, wireKey]) => {
165
+ const segments = fixturePath.split(".");
166
+ const parent = segments.slice(0, -1);
167
+ return [
168
+ [...parent, wireKey].join("."),
169
+ segments[segments.length - 1] ?? "",
170
+ ];
171
+ }),
172
+ ),
173
+ });
174
+
175
+ const CATALOG_HINTS = hintsOf({
176
+ recordPaths: [
177
+ "features.creditSchema.dimensions",
178
+ "features.creditSchema.dimensions.*.match",
179
+ "features.creditSchema.multipliers",
180
+ "features.creditSchema.multipliers.*.match",
181
+ "features.modelMarkups",
182
+ "features.providerMarkups",
183
+ "plans.billingControls.usageAlerts.filter.properties",
184
+ "plans.billingControls.usageLimits.filter.properties",
185
+ "plans.items.featureOverride.creditSchema.dimensions",
186
+ "plans.items.featureOverride.creditSchema.dimensions.*.match",
187
+ "plans.items.featureOverride.creditSchema.multipliers",
188
+ "plans.items.featureOverride.creditSchema.multipliers.*.match",
189
+ "plans.items.featureOverride.markups.modelMarkups",
190
+ "plans.items.featureOverride.markups.providerMarkups",
191
+ "plans.licenses.customize.addItems.featureOverride.creditSchema.dimensions",
192
+ "plans.licenses.customize.addItems.featureOverride.creditSchema.dimensions.*.match",
193
+ "plans.licenses.customize.addItems.featureOverride.creditSchema.multipliers",
194
+ "plans.licenses.customize.addItems.featureOverride.creditSchema.multipliers.*.match",
195
+ "plans.licenses.customize.addItems.featureOverride.markups.modelMarkups",
196
+ "plans.licenses.customize.addItems.featureOverride.markups.providerMarkups",
197
+ "plans.variants.customize.addItems.featureOverride.creditSchema.dimensions",
198
+ "plans.variants.customize.addItems.featureOverride.creditSchema.dimensions.*.match",
199
+ "plans.variants.customize.addItems.featureOverride.creditSchema.multipliers",
200
+ "plans.variants.customize.addItems.featureOverride.creditSchema.multipliers.*.match",
201
+ "plans.variants.customize.addItems.featureOverride.markups.modelMarkups",
202
+ "plans.variants.customize.addItems.featureOverride.markups.providerMarkups",
203
+ "plans.variants.customize.billingControls.usageAlerts.filter.properties",
204
+ "plans.variants.customize.billingControls.usageLimits.filter.properties",
205
+ "plans.variants.customize.items.featureOverride.creditSchema.dimensions",
206
+ "plans.variants.customize.items.featureOverride.creditSchema.dimensions.*.match",
207
+ "plans.variants.customize.items.featureOverride.creditSchema.multipliers",
208
+ "plans.variants.customize.items.featureOverride.creditSchema.multipliers.*.match",
209
+ "plans.variants.customize.items.featureOverride.markups.modelMarkups",
210
+ "plans.variants.customize.items.featureOverride.markups.providerMarkups",
211
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.dimensions",
212
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.dimensions.*.match",
213
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers",
214
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.creditSchema.multipliers.*.match",
215
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.modelMarkups",
216
+ "plans.variants.customize.upsertLicenses.customize.addItems.featureOverride.markups.providerMarkups",
217
+ ],
218
+ frozenPaths: [
219
+ "plans.licenses.metadata",
220
+ "plans.metadata",
221
+ "plans.variants.customize.upsertLicenses.metadata",
222
+ ],
223
+ renamedPaths: { "settings.paydownOverages": "persist_free_overage" },
224
+ });
225
+
226
+ export type AtmnConfig = {
227
+ /** Every features entry this catalog should have. `[]` means "mine, and
228
+ * empty"; omitted means "not mine". */
229
+ features?: Feature[];
230
+ /** Every plans entry this catalog should have. `[]` means "mine, and
231
+ * empty"; omitted means "not mine". */
232
+ plans?: Plan[];
233
+ /** Every rewards entry this catalog should have. `[]` means "mine, and
234
+ * empty"; omitted means "not mine". */
235
+ rewards?: Reward[];
236
+ /** Every referralPrograms entry this catalog should have. `[]` means "mine, and
237
+ * empty"; omitted means "not mine". */
238
+ referralPrograms?: ReferralProgram[];
239
+ /** The settings this config manages. Only the fields stated are written;
240
+ * an omitted field keeps its value, and an omitted block manages nothing. */
241
+ settings?: Settings;
242
+ };
243
+
244
+ const stated = (config: AtmnConfig): Record<string, unknown> => ({
245
+ ...(config.features !== undefined ? { features: config.features } : {}),
246
+ ...(config.plans !== undefined
247
+ ? {
248
+ plans: config.plans.map(clearOmittedRelationshipCustomizes),
249
+ // Stated plans are every version: the ones it omits are removed.
250
+ skip_version_deletions: false,
251
+ }
252
+ : {}),
253
+ ...(config.rewards !== undefined ? { rewards: config.rewards } : {}),
254
+ ...(config.referralPrograms !== undefined
255
+ ? { referralPrograms: config.referralPrograms }
256
+ : {}),
257
+ ...(config.settings !== undefined ? { settings: config.settings } : {}),
258
+ });
259
+
260
+ const SINGLETON_KEYS: readonly string[] = ["settings"];
261
+ const CONFIG_KEYS: readonly string[] = [
262
+ "features",
263
+ "plans",
264
+ "rewards",
265
+ "referralPrograms",
266
+ "settings",
267
+ ];
268
+
269
+ /**
270
+ * A key the type does not know is a config written for another version of
271
+ * this CLI, and the runtime loader does no type check: refused, never dropped.
272
+ */
273
+ const unknownKeyIssue = (key: string): { path: string; message: string } =>
274
+ key === "planVersions"
275
+ ? {
276
+ path: key,
277
+ message:
278
+ "planVersions is gone: every version is now a row in plans, with versionSlug and active. Rebuild the config from your org with `atmn pull --overwrite --yes` (commit first), or move the rows into plans.",
279
+ }
280
+ : {
281
+ path: key,
282
+ message: `${key} is not a config field. The fields are ${CONFIG_KEYS.join(", ")}.`,
283
+ };
284
+
285
+ /**
286
+ * The catalog document and each singleton's own request body, split from the
287
+ * one document `atmn()` returns: they go to different operations.
288
+ */
289
+ export const splitWire = (
290
+ document: WireDocument,
291
+ ): {
292
+ catalog: WireDocument;
293
+ singletons: Record<string, WireDocument | undefined>;
294
+ } => ({
295
+ catalog: Object.fromEntries(
296
+ Object.entries(document).filter(([key]) => !SINGLETON_KEYS.includes(key)),
297
+ ),
298
+ singletons: {
299
+ settings:
300
+ document.settings === undefined
301
+ ? undefined
302
+ : { config: document.settings },
303
+ },
304
+ });
305
+
306
+ export const atmn = (config: AtmnConfig): WireDocument => {
307
+ const unknownKeys = Object.keys(config).filter(
308
+ (key) => !CONFIG_KEYS.includes(key),
309
+ );
310
+ if (unknownKeys.length > 0)
311
+ throw new ConfigError(unknownKeys.map(unknownKeyIssue));
312
+ const document = stated(config);
313
+ // Linted before anything is sent, and every problem is reported at once —
314
+ // a round trip per mistake is what makes a config painful to write.
315
+ const issues = lintDocument({
316
+ document,
317
+ rules: LINT_RULES,
318
+ hints: CATALOG_HINTS,
319
+ });
320
+ if (issues.length > 0) throw new ConfigError(issues);
321
+
322
+ return {
323
+ ...(toWire({
324
+ value: document,
325
+ path: "",
326
+ hints: CATALOG_HINTS,
327
+ }) as WireDocument),
328
+ // The payload is the complete desired catalog, so omission is a removal.
329
+ skip_deletions: false,
330
+ // A constant, not a decision: "draft wherever one is warranted". The server
331
+ // works out which rows actually need one.
332
+ migration: { draft: true },
333
+ };
334
+ };
@@ -0,0 +1,30 @@
1
+ import { version } from "../version";
2
+
3
+ const runtime =
4
+ typeof Bun !== "undefined"
5
+ ? `bun ${Bun.version}`
6
+ : `node ${process.versions.node}`;
7
+
8
+ /** What the server logs as `user_agent` for every request this CLI makes. */
9
+ export const USER_AGENT = `atmn/${version} (${process.platform}; ${runtime})`;
10
+
11
+ /** A fetch that stamps the CLI's user agent unless the caller already set one. */
12
+ export const withUserAgent = (
13
+ base: typeof globalThis.fetch,
14
+ ): typeof globalThis.fetch => {
15
+ const stamped = (
16
+ input: Parameters<typeof globalThis.fetch>[0],
17
+ init?: Parameters<typeof globalThis.fetch>[1],
18
+ ) => {
19
+ const headers = new Headers(init?.headers);
20
+ if (!headers.has("user-agent")) headers.set("user-agent", USER_AGENT);
21
+ return base(input, { ...init, headers });
22
+ };
23
+ // Bun's fetch carries extras like `preconnect`; keep them on the wrapper.
24
+ return Object.assign(stamped, base);
25
+ };
26
+
27
+ /** The one transport every request to Autumn goes through. */
28
+ export const autumnFetch: typeof globalThis.fetch = withUserAgent(
29
+ globalThis.fetch,
30
+ );
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The package's import surface for a config: the builders and `atmn()`.
3
+ * Source-backed for workspace links; a built entry follows for npm.
4
+ */
5
+ export { type Feature, feature } from "./generated/features.js";
6
+ export { type License, license } from "./generated/licenses.js";
7
+ export { type Plan, plan } from "./generated/plans.js";
8
+ export {
9
+ type ReferralProgram,
10
+ referralProgram,
11
+ } from "./generated/referralPrograms.js";
12
+ export {
13
+ type Coupon,
14
+ coupon,
15
+ type FeatureGrant,
16
+ featureGrant,
17
+ type Reward,
18
+ } from "./generated/rewards.js";
19
+ export { type Variant, variant } from "./generated/variants.js";
20
+ export { type AtmnConfig, atmn } from "./generated/wire.js";
@@ -0,0 +1,41 @@
1
+ import { relative, resolve } from "node:path";
2
+ import { COLLECTION_FILES } from "../actions/pull/scaffoldConfig";
3
+ import { configPackageName } from "../config/configPackageName";
4
+ import { ask, done, type Prompter, soft } from "../prompt/prompt";
5
+ import { findRepoLayout } from "../repo/findRepoRoot";
6
+
7
+ /** The config folder when nothing states it: its own package in a workspace, else `autumn/`. */
8
+ export const defaultConfigDirName = ({ cwd }: { cwd: string }): string =>
9
+ findRepoLayout({ cwd }).hasWorkspaces ? "packages/autumn/" : "autumn/";
10
+
11
+ const configFiles = ["autumn.config.ts", ...Object.keys(COLLECTION_FILES)];
12
+
13
+ /**
14
+ * No flag, no config beside cwd, no root marker: say what is about to be
15
+ * created and ask where, before writing anything. Headless prints the `-c`
16
+ * hint and stops, so nothing lands in a folder the user never chose.
17
+ */
18
+ export const chooseConfigDir = async ({
19
+ cwd,
20
+ prompter,
21
+ }: {
22
+ cwd: string;
23
+ prompter: Prompter;
24
+ }): Promise<{ configDir: string; repoRoot: string }> => {
25
+ const { repoRoot } = findRepoLayout({ cwd });
26
+ const fallback = defaultConfigDirName({ cwd });
27
+ prompter.write(
28
+ `${soft("No autumn.config.ts found.")}\n atmn keeps your pricing in one folder: ${configFiles.join(", ")}.\n`,
29
+ );
30
+ const chosen = await ask({
31
+ prompter,
32
+ value: undefined,
33
+ question: "Where should that folder live?",
34
+ flag: `-c <dir> (or run ${configPackageName()} init)`,
35
+ example: `-c ${fallback}`,
36
+ defaultValue: fallback,
37
+ });
38
+ const configDir = resolve(repoRoot, chosen);
39
+ prompter.write(`${done(`Path ${relative(repoRoot, configDir) || "."}`)}\n`);
40
+ return { configDir, repoRoot };
41
+ };
@@ -0,0 +1,115 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { findRepoLayout } from "../repo/findRepoRoot";
4
+
5
+ const CONFIG_FILENAMES = ["autumn.config.ts", "autumn.config.js"] as const;
6
+
7
+ /** The root package.json field `atmn init` writes so later commands find the config from anywhere in the repo. */
8
+ export const MARKER_FIELD = "atmn";
9
+
10
+ export type ProjectMarker = { config: string };
11
+
12
+ export type Project = {
13
+ repoRoot: string;
14
+ /** The config file, when one could be found. */
15
+ configPath: string | null;
16
+ /** Where fixtures and skills/ live: the config's folder, else cwd. */
17
+ configDir: string;
18
+ /** Where `.env` is looked for, root first: an existing file anywhere here is
19
+ * reused, and a new one is created at the root so one file serves every package. */
20
+ envDirs: string[];
21
+ /** How the config was found; `hint` is printed when a command needs one and there is none. */
22
+ source: "flag" | "cwd" | "marker" | "none";
23
+ };
24
+
25
+ const configIn = ({ dir }: { dir: string }): string | null => {
26
+ for (const filename of CONFIG_FILENAMES) {
27
+ const path = join(dir, filename);
28
+ if (existsSync(path)) return path;
29
+ }
30
+ return null;
31
+ };
32
+
33
+ /** `-c` takes a file or a directory; a directory means the config it holds, or a new autumn.config.ts. */
34
+ export const configPathFromFlag = ({
35
+ cwd,
36
+ flag,
37
+ }: {
38
+ cwd: string;
39
+ flag: string;
40
+ }): string => {
41
+ const resolved = isAbsolute(flag) ? flag : resolve(cwd, flag);
42
+ const isFile = /\.(ts|js)$/.test(resolved);
43
+ if (isFile) return resolved;
44
+ if (existsSync(resolved) && !statSync(resolved).isDirectory())
45
+ return resolved;
46
+ // A folder means whichever config it holds; a new folder gets the .ts one.
47
+ return configIn({ dir: resolved }) ?? join(resolved, "autumn.config.ts");
48
+ };
49
+
50
+ export const readMarker = ({
51
+ repoRoot,
52
+ }: {
53
+ repoRoot: string;
54
+ }): ProjectMarker | null => {
55
+ const manifest = join(repoRoot, "package.json");
56
+ if (!existsSync(manifest)) return null;
57
+ try {
58
+ const parsed = JSON.parse(readFileSync(manifest, "utf8")) as Record<
59
+ string,
60
+ unknown
61
+ >;
62
+ const marker = parsed[MARKER_FIELD];
63
+ if (
64
+ typeof marker === "object" &&
65
+ marker !== null &&
66
+ typeof (marker as ProjectMarker).config === "string"
67
+ )
68
+ return { config: (marker as ProjectMarker).config };
69
+ return null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ };
74
+
75
+ /**
76
+ * Where this run's config and `.env` are. The flag wins, then a config beside
77
+ * cwd, then the root marker `atmn init` wrote: the common case of running
78
+ * `atmn push` from the repo root of a monorepo lands on the marker.
79
+ */
80
+ export const resolveProject = ({
81
+ cwd,
82
+ configFlag,
83
+ }: {
84
+ cwd: string;
85
+ configFlag?: string;
86
+ }): Project => {
87
+ const { repoRoot, packageRoot } = findRepoLayout({ cwd });
88
+
89
+ let configPath: string | null = null;
90
+ let source: Project["source"] = "none";
91
+ if (configFlag !== undefined) {
92
+ configPath = configPathFromFlag({ cwd, flag: configFlag });
93
+ source = "flag";
94
+ } else if (configIn({ dir: cwd }) !== null) {
95
+ configPath = configIn({ dir: cwd });
96
+ source = "cwd";
97
+ } else {
98
+ const marker = readMarker({ repoRoot });
99
+ if (marker !== null) {
100
+ configPath = resolve(repoRoot, marker.config);
101
+ source = "marker";
102
+ }
103
+ }
104
+
105
+ const configDir = configPath === null ? cwd : dirname(configPath);
106
+ return {
107
+ repoRoot,
108
+ configPath,
109
+ configDir,
110
+ envDirs: [
111
+ ...new Set([repoRoot, configDir, cwd, join(cwd, "atmn"), packageRoot]),
112
+ ],
113
+ source,
114
+ };
115
+ };
@@ -0,0 +1,40 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ import { configPackageName } from "../config/configPackageName";
4
+ import { MARKER_FIELD, readMarker } from "./resolveProject";
5
+
6
+ const readJson = (path: string): Record<string, unknown> =>
7
+ JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
8
+
9
+ const writeJson = (path: string, value: unknown): void => {
10
+ writeFileSync(path, `${JSON.stringify(value, null, "\t")}\n`, "utf8");
11
+ };
12
+
13
+ /** The root's marker and script, added beside whatever is already there. */
14
+ export const writeRootMarker = ({
15
+ repoRoot,
16
+ configPath,
17
+ }: {
18
+ repoRoot: string;
19
+ configPath: string;
20
+ }): boolean => {
21
+ const manifestPath = join(repoRoot, "package.json");
22
+ const manifest = existsSync(manifestPath) ? readJson(manifestPath) : {};
23
+ const config = relative(repoRoot, configPath);
24
+ const scripts = (manifest.scripts ?? {}) as Record<string, string>;
25
+ const next = {
26
+ ...manifest,
27
+ scripts: {
28
+ ...scripts,
29
+ atmn: `${configPackageName()} -c ${JSON.stringify(config)}`,
30
+ },
31
+ [MARKER_FIELD]: { config },
32
+ };
33
+ if (
34
+ readMarker({ repoRoot })?.config === config &&
35
+ scripts.atmn === next.scripts.atmn
36
+ )
37
+ return false;
38
+ writeJson(manifestPath, next);
39
+ return true;
40
+ };