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,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
|
+
};
|