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
package/src/cli.ts ADDED
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { Command } from "commander";
5
+ import { registerApiCommands } from "./actions/api/registerApiCommands";
6
+ import { runEnv } from "./actions/env";
7
+ import { fetchOrgInfo } from "./actions/env/fetchOrgInfo";
8
+ import { runInit } from "./actions/init/runInit";
9
+ import { runLogin } from "./actions/login";
10
+ import {
11
+ CONNECT_OPTIONS,
12
+ keylessDepsFor,
13
+ runClaim,
14
+ runKeylessLogin,
15
+ } from "./actions/login/keyless";
16
+ import { runPull } from "./actions/pull";
17
+ import { runPush } from "./actions/push";
18
+ import { runReset } from "./actions/reset/runReset";
19
+ import { runSandboxCreate } from "./actions/sandbox/createSandbox";
20
+ import { runSandboxDelete } from "./actions/sandbox/deleteSandbox";
21
+ import { runSandboxList } from "./actions/sandbox/listSandboxes";
22
+ import { runSandboxUse } from "./actions/sandbox/useSandbox";
23
+ import { withSandboxScopeHint } from "./actions/sandbox/withSandboxScopeHint";
24
+ import {
25
+ installSkills,
26
+ linkSkills,
27
+ printSkill,
28
+ renderSkillsList,
29
+ SKILLS_DIR_NAME,
30
+ staleSkillsHint,
31
+ updateSkills,
32
+ } from "./actions/skills/skills";
33
+ import { configPackageName } from "./config/configPackageName";
34
+ import { assertSandboxTarget } from "./env/assertSandboxTarget";
35
+ import { loadEnvFiles } from "./env/loadEnv";
36
+ import {
37
+ managementTarget,
38
+ requireSecretKey,
39
+ resolveTarget,
40
+ type Target,
41
+ type TargetFlags,
42
+ targetBaseUrl,
43
+ } from "./env/resolveTarget";
44
+ import type { CreateSandboxParams } from "./generated/client";
45
+ import { createClient } from "./generated/client";
46
+ import { autumnFetch } from "./http/autumnFetch";
47
+ import { type Project, resolveProject } from "./project/resolveProject";
48
+ import {
49
+ createPrompter,
50
+ hint,
51
+ NeedsInputError,
52
+ needs,
53
+ type Prompter,
54
+ releaseStdin,
55
+ } from "./prompt/prompt";
56
+ import { previewIsEmpty } from "./render/renderPreview";
57
+ import { version } from "./version";
58
+
59
+ /**
60
+ * Commander treats a lone `-v` as unknown, and `-V` as version. Rewriting the
61
+ * token keeps `-v`, `-V` and `--version` identical without a custom parser.
62
+ */
63
+ const normalizeVersionFlag = ({ argv }: { argv: string[] }): string[] =>
64
+ argv.map((token) => (token === "-v" ? "--version" : token));
65
+
66
+ /** The flags every command shares: where to send, which key, who the CLI is. */
67
+ const withTargetFlags = (program: Command): Command =>
68
+ program
69
+ .option("-p, --prod", "target production instead of sandbox")
70
+ .option("--sandbox <sandboxId>", "target a specific sandbox")
71
+ .option("-l, --local", "target a local server (default port 8080)")
72
+ // Long-only: -p is prod.
73
+ .option("--port <port>", "port of a local server (implies --local)")
74
+ .option("-b, --base-url <url>", "send to this URL instead")
75
+ .option("--client-id <id>", "OAuth client id the CLI identifies as")
76
+ .option(
77
+ "-c, --config <path>",
78
+ "autumn.config.ts, or the folder holding it (default: found from cwd or the root package.json)",
79
+ )
80
+ .option(
81
+ "--headless",
82
+ "never prompt: print what a command needs and stop (default when not a TTY)",
83
+ );
84
+
85
+ /**
86
+ * Env first: the key and AUTUMN_BASE_URL usually live in a .env beside the
87
+ * config, so reading them after building the client would never see them —
88
+ * which is what the "put it in your .env" error message promises.
89
+ */
90
+ const prepareTarget = ({ command }: { command: Command }): Target => {
91
+ loadEnvFiles({ dirs: projectOf({ command }).envDirs });
92
+ return resolveTarget(command.optsWithGlobals<TargetFlags>());
93
+ };
94
+
95
+ type GlobalFlags = TargetFlags & { config?: string; headless?: boolean };
96
+
97
+ const projectOf = ({ command }: { command: Command }): Project =>
98
+ resolveProject({
99
+ cwd: process.cwd(),
100
+ configFlag: command.optsWithGlobals<GlobalFlags>().config,
101
+ });
102
+
103
+ const configFlagOf = ({ command }: { command: Command }): string | undefined =>
104
+ command.optsWithGlobals<GlobalFlags>().config;
105
+
106
+ /** Skills sit beside the config; with no config and no --dir there is nowhere to put them. */
107
+ const skillsDirOf = ({
108
+ command,
109
+ dir,
110
+ }: {
111
+ command: Command;
112
+ dir: string | undefined;
113
+ }): string => {
114
+ if (dir !== undefined) return dir;
115
+ const project = projectOf({ command });
116
+ if (project.configPath === null) {
117
+ const flagHint = `Run ${configPackageName()} init, or pass --dir <dir>`;
118
+ process.stdout.write(
119
+ `${needs("No Autumn config folder found.")}\n${hint(flagHint)}\n`,
120
+ );
121
+ throw new NeedsInputError(flagHint);
122
+ }
123
+ return join(project.configDir, SKILLS_DIR_NAME);
124
+ };
125
+
126
+ const writeStaleSkillsHint = ({ command }: { command: Command }): void => {
127
+ const stale = staleSkillsHint({
128
+ dir: join(projectOf({ command }).configDir, SKILLS_DIR_NAME),
129
+ });
130
+ if (stale !== null) process.stdout.write(`${stale}\n`);
131
+ };
132
+
133
+ const PULL_EDIT_LINE = /^([+~-]) (\S+)$/;
134
+
135
+ /** `atmn pull` in a child, its edit lines read back. Only the pin travels:
136
+ * init always pulls the main sandbox. */
137
+ const pullInChildProcess = ({
138
+ configDir,
139
+ }: {
140
+ configDir: string;
141
+ }): { appended: string[]; replaced: string[]; deleted: string[] } => {
142
+ const result = spawnSync(
143
+ process.execPath,
144
+ [...process.execArgv, process.argv[1] ?? "", "--headless", "pull"],
145
+ {
146
+ cwd: configDir,
147
+ env: { ...process.env, AUTUMN_SANDBOX_ID: "" },
148
+ encoding: "utf8",
149
+ },
150
+ );
151
+ if (result.status !== 0)
152
+ throw new Error(`${result.stdout}${result.stderr}`.trim());
153
+ const edits = {
154
+ appended: [] as string[],
155
+ replaced: [] as string[],
156
+ deleted: [] as string[],
157
+ };
158
+ for (const raw of result.stdout.split("\n")) {
159
+ const match = PULL_EDIT_LINE.exec(raw.trim());
160
+ if (!match) continue;
161
+ const [, symbol, id] = match;
162
+ if (id === undefined) continue;
163
+ if (symbol === "+") edits.appended.push(id);
164
+ else if (symbol === "~") edits.replaced.push(id);
165
+ else edits.deleted.push(id);
166
+ }
167
+ return edits;
168
+ };
169
+
170
+ /** `--headless` forces the hint path; a pipe or CI log gets it by default. */
171
+ const prompterFor = ({ command }: { command: Command }): Prompter =>
172
+ createPrompter({
173
+ interactive:
174
+ command.optsWithGlobals<GlobalFlags>().headless !== true &&
175
+ process.stdout.isTTY === true &&
176
+ process.stdin.isTTY === true,
177
+ });
178
+
179
+ const clientFor = ({ target }: { target: Target }) =>
180
+ createClient({
181
+ secretKey: requireSecretKey({ target }),
182
+ fetch: autumnFetch,
183
+ ...(target.baseUrl ? { baseUrl: target.baseUrl } : {}),
184
+ });
185
+
186
+ /** `sandbox *` is an organization operation: never the sandbox's own key. */
187
+ const sandboxClientFor = ({ target }: { target: Target }) =>
188
+ clientFor({ target: managementTarget({ target }) });
189
+
190
+ /** The pinned sandbox's name for the copy; nothing when the main key cannot list. */
191
+ const pinnedSandboxName = async ({
192
+ target,
193
+ }: {
194
+ target: Target;
195
+ }): Promise<{ sandboxName?: string }> => {
196
+ if (target.sandboxId === undefined) return {};
197
+ try {
198
+ const { list } = await sandboxClientFor({ target }).listSandboxes({});
199
+ const name = list.find((row) => row.id === target.sandboxId)?.name;
200
+ return name === undefined ? {} : { sandboxName: name };
201
+ } catch {
202
+ return {};
203
+ }
204
+ };
205
+
206
+ /** `/organization/me` for the main key: what `sandbox use` and `init` report the org as. */
207
+ const mainOrgInfo = ({ target }: { target: Target }) => {
208
+ const main = managementTarget({ target });
209
+ return fetchOrgInfo({
210
+ baseUrl: targetBaseUrl({ target: main }),
211
+ secretKey: requireSecretKey({ target: main }),
212
+ });
213
+ };
214
+
215
+ export const buildProgram = (): Command => {
216
+ const program = new Command();
217
+
218
+ const name = configPackageName();
219
+ withTargetFlags(
220
+ program
221
+ .name(name)
222
+ .description("Autumn CLI")
223
+ .addHelpText(
224
+ "before",
225
+ `Getting started:
226
+ ${name} init set up this repo end to end (asks how to connect, or take --login / --keyless)
227
+ ${name} login ${CONNECT_OPTIONS.login}
228
+ ${name} login --keyless ${CONNECT_OPTIONS.keyless}
229
+ `,
230
+ )
231
+ .version(`${name} v${version}`, "-V, --version", "print the version")
232
+ .showHelpAfterError(),
233
+ );
234
+
235
+ program
236
+ .command("init")
237
+ .description(
238
+ "set up this repo: connect to Autumn, place the config, pull your catalog, install the skills",
239
+ )
240
+ .addHelpText(
241
+ "after",
242
+ "\nWith no AUTUMN_SECRET_KEY on disk, init asks how to connect in a terminal; headless it prints --login / --keyless and stops.",
243
+ )
244
+ .option(
245
+ "--path <dir>",
246
+ "folder for the config: autumn/ by default, packages/autumn/ in a monorepo",
247
+ )
248
+ .option("--name <name>", "the package's name (monorepos)")
249
+ .option("--login", CONNECT_OPTIONS.login)
250
+ .option("--keyless", CONNECT_OPTIONS.keyless)
251
+ .action(
252
+ async (
253
+ options: {
254
+ path?: string;
255
+ name?: string;
256
+ login?: boolean;
257
+ keyless?: boolean;
258
+ },
259
+ command: Command,
260
+ ) => {
261
+ if (options.login && options.keyless)
262
+ throw new Error("Pick one of --login and --keyless.");
263
+ const flags = command.optsWithGlobals<GlobalFlags>();
264
+ const mainTarget = resolveTarget({ ...flags, sandbox: undefined });
265
+ const baseUrl = targetBaseUrl({ target: mainTarget });
266
+ // Tests run the CLI from source, where the package is not published:
267
+ // they point new packages at the checkout instead.
268
+ const dependencySpec = process.env.ATMN_INIT_DEPENDENCY;
269
+ await runInit({
270
+ ...(dependencySpec === undefined ? {} : { dependencySpec }),
271
+ ...(options.path === undefined ? {} : { path: options.path }),
272
+ ...(options.name === undefined ? {} : { name: options.name }),
273
+ ...(options.login ? { connect: "login" as const } : {}),
274
+ ...(options.keyless ? { connect: "keyless" as const } : {}),
275
+ prompter: prompterFor({ command }),
276
+ deps: {
277
+ fetchOrgInfo: ({ secretKey }) =>
278
+ fetchOrgInfo({ baseUrl, secretKey }),
279
+ login: ({ envDirs }) =>
280
+ runLogin({ cwd: envDirs[0], target: mainTarget }),
281
+ keyless: keylessDepsFor({ target: mainTarget }),
282
+ install: async ({ manager, repoRoot }) =>
283
+ spawnSync(manager, ["install"], {
284
+ cwd: repoRoot,
285
+ stdio: "inherit",
286
+ }).status === 0,
287
+ // A fresh process: the config imports a package that was
288
+ // installed a moment ago, which this process cannot resolve.
289
+ pull: async ({ configDir }) => pullInChildProcess({ configDir }),
290
+ },
291
+ });
292
+ },
293
+ );
294
+
295
+ program
296
+ .command("login")
297
+ .description("connect to Autumn and write the org's keys to your .env")
298
+ .addHelpText(
299
+ "after",
300
+ `
301
+ Two ways in:
302
+ atmn login ${CONNECT_OPTIONS.login}
303
+ atmn login --keyless ${CONNECT_OPTIONS.keyless}
304
+ Linking a keyless org to an account:
305
+ atmn login --claim you@acme.com creates and emails a secure browser claim link`,
306
+ )
307
+ .option("--keyless", CONNECT_OPTIONS.keyless)
308
+ .option(
309
+ "--claim <email>",
310
+ "link the keyless org this key belongs to with an account",
311
+ )
312
+ .action(
313
+ async (
314
+ options: { keyless?: boolean; claim?: string },
315
+ command: Command,
316
+ ) => {
317
+ if (options.keyless && options.claim !== undefined)
318
+ throw new Error("Pick one of --keyless and --claim.");
319
+ const target = prepareTarget({ command });
320
+ const prompter = prompterFor({ command });
321
+ // A keyless org lives in sandbox, so its key is the main sandbox one
322
+ // whatever --prod or a sandbox pin says.
323
+ const mainTarget = managementTarget({
324
+ target: { ...target, secretKeyName: "AUTUMN_SECRET_KEY" },
325
+ });
326
+ if (options.keyless) {
327
+ const project = projectOf({ command });
328
+ await runKeylessLogin({
329
+ repoRoot: project.repoRoot,
330
+ envDirs: project.envDirs,
331
+ deps: keylessDepsFor({ target: mainTarget }),
332
+ prompter,
333
+ });
334
+ return;
335
+ }
336
+ if (options.claim !== undefined) {
337
+ await runClaim({
338
+ secretKey: requireSecretKey({ target: mainTarget }),
339
+ email: options.claim,
340
+ deps: keylessDepsFor({ target: mainTarget }),
341
+ prompter,
342
+ });
343
+ return;
344
+ }
345
+ await runLogin({ target, configPath: configFlagOf({ command }) });
346
+ },
347
+ );
348
+
349
+ registerApiCommands({
350
+ program,
351
+ targetOf: (command) => prepareTarget({ command }),
352
+ });
353
+
354
+ program
355
+ .command("env")
356
+ .description("show the org, environment and key your commands target")
357
+ .option("--json", "print the response instead of the summary")
358
+ .action(async (options: { json?: boolean }, command: Command) => {
359
+ const target = prepareTarget({ command });
360
+ await runEnv({
361
+ target,
362
+ fetchOrgInfo: () =>
363
+ fetchOrgInfo({
364
+ baseUrl: targetBaseUrl({ target }),
365
+ secretKey: requireSecretKey({ target }),
366
+ }),
367
+ json: options.json === true,
368
+ });
369
+ });
370
+
371
+ program
372
+ .command("push")
373
+ .description(
374
+ "preview autumn.config.ts against your catalog; --yes applies it",
375
+ )
376
+ .option("-y, --yes", "apply the changes the preview shows")
377
+ .option("-d, --dry-run", "preview only, even with --yes")
378
+ .action(
379
+ async (
380
+ options: { dryRun?: boolean; yes?: boolean },
381
+ command: Command,
382
+ ) => {
383
+ const target = prepareTarget({ command });
384
+ // Nothing is applied unless asked: a plain push is the preview, and the
385
+ // same command with --yes is the write. Deletions ride the same gate.
386
+ const apply = options.yes === true && options.dryRun !== true;
387
+ const result = await runPush({
388
+ client: clientFor({ target }),
389
+ configPath: configFlagOf({ command }),
390
+ dryRun: !apply,
391
+ });
392
+ if (!apply && !previewIsEmpty({ preview: result.preview }))
393
+ process.stdout.write(
394
+ options.dryRun === true
395
+ ? "\nDry run — nothing applied.\n"
396
+ : "\nNothing applied. Re-run with --yes to apply these changes.\n",
397
+ );
398
+ writeStaleSkillsHint({ command });
399
+ },
400
+ );
401
+
402
+ const skills = program
403
+ .command("skills")
404
+ .description("the skills this CLI carries: list, print, install, update")
405
+ .argument("[name]", "print this skill's SKILL.md")
406
+ .option("--ref <path>", "print one of the skill's references instead")
407
+ .option("--json", "print the skill as JSON")
408
+ .action(
409
+ (name: string | undefined, options: { ref?: string; json?: boolean }) => {
410
+ if (name === undefined) {
411
+ process.stdout.write(`${renderSkillsList()}\n`);
412
+ return;
413
+ }
414
+ printSkill({
415
+ name,
416
+ ...(options.ref === undefined ? {} : { ref: options.ref }),
417
+ json: options.json === true,
418
+ write: (text) => process.stdout.write(text),
419
+ });
420
+ },
421
+ );
422
+
423
+ skills
424
+ .command("install")
425
+ .description(
426
+ "write the skills next to your config; --link runs npx skills add",
427
+ )
428
+ .option("--dir <dir>", "write here instead of <config folder>/skills")
429
+ .option("--link", "run `npx skills add <dir> --all` afterwards")
430
+ .action(
431
+ async (options: { dir?: string; link?: boolean }, command: Command) => {
432
+ const dir = skillsDirOf({ command, dir: options.dir });
433
+ installSkills({ dir, write: (text) => process.stdout.write(text) });
434
+ if (options.link === true)
435
+ await linkSkills({
436
+ dir,
437
+ write: (text) => process.stdout.write(text),
438
+ });
439
+ },
440
+ );
441
+
442
+ skills
443
+ .command("update")
444
+ .description("rewrite installed skills older than this CLI's")
445
+ .option(
446
+ "--dir <dir>",
447
+ "the install to update; <config folder>/skills by default",
448
+ )
449
+ .action((options: { dir?: string }, command: Command) => {
450
+ const dir = skillsDirOf({ command, dir: options.dir });
451
+ updateSkills({ dir, write: (text) => process.stdout.write(text) });
452
+ });
453
+
454
+ program
455
+ .command("pull")
456
+ .description("write your remote catalog back into autumn.config.ts")
457
+ .option("--include-mappings", "keep processor mappings in pulled fixtures")
458
+ .option(
459
+ "--overwrite",
460
+ "rewrite the config from your org's catalog (e.g. after switching orgs)",
461
+ )
462
+ .option("-y, --yes", "overwrite it")
463
+ .action(
464
+ async (
465
+ options: {
466
+ includeMappings?: boolean;
467
+ overwrite?: boolean;
468
+ yes?: boolean;
469
+ },
470
+ command: Command,
471
+ ) => {
472
+ const target = prepareTarget({ command });
473
+ await runPull({
474
+ client: clientFor({ target }),
475
+ configPath: configFlagOf({ command }),
476
+ includeMappings: options.includeMappings === true,
477
+ overwrite: options.overwrite === true,
478
+ yes: options.yes === true,
479
+ prompter: prompterFor({ command }),
480
+ });
481
+ writeStaleSkillsHint({ command });
482
+ },
483
+ );
484
+
485
+ program
486
+ .command("reset")
487
+ .description("wipe this sandbox's catalog and customers; --yes applies it")
488
+ .option("-y, --yes", "wipe it")
489
+ .action(async (options: { yes?: boolean }, command: Command) => {
490
+ const target = prepareTarget({ command });
491
+ // Ahead of the client: --prod would otherwise fail on a missing prod
492
+ // key rather than on the refusal that matters.
493
+ assertSandboxTarget({ target });
494
+ try {
495
+ await runReset({
496
+ client: clientFor({ target }),
497
+ target,
498
+ ...(await pinnedSandboxName({ target })),
499
+ yes: options.yes === true,
500
+ });
501
+ } catch (error) {
502
+ throw withSandboxScopeHint({ error });
503
+ }
504
+ });
505
+
506
+ const sandbox = program
507
+ .command("sandbox")
508
+ .description("manage isolated sandboxes");
509
+
510
+ sandbox
511
+ .command("list")
512
+ .description("show your sandboxes, newest first")
513
+ .option("--json", "print the response instead of the table")
514
+ .action(async (options: { json?: boolean }, command: Command) => {
515
+ const target = prepareTarget({ command });
516
+ await runSandboxList({
517
+ client: sandboxClientFor({ target }),
518
+ ...(target.sandboxId === undefined
519
+ ? {}
520
+ : { currentSandboxId: target.sandboxId }),
521
+ json: options.json === true,
522
+ });
523
+ });
524
+
525
+ sandbox
526
+ .command("use")
527
+ .description(
528
+ "target a sandbox by name or id; --clear returns to the main one",
529
+ )
530
+ .argument("[query]", "the sandbox's name or id")
531
+ .option("--clear", "drop the pin so commands target the main sandbox")
532
+ .option("--json", "print the result as JSON")
533
+ .action(
534
+ async (
535
+ query: string | undefined,
536
+ options: { clear?: boolean; json?: boolean },
537
+ command: Command,
538
+ ) => {
539
+ const target = prepareTarget({ command });
540
+ // Clearing only edits .env: no key, no server, no reason to fail.
541
+ if (options.clear === true) {
542
+ await runSandboxUse({
543
+ clear: true,
544
+ json: options.json === true,
545
+ envDirs: projectOf({ command }).envDirs,
546
+ prompter: prompterFor({ command }),
547
+ });
548
+ return;
549
+ }
550
+ try {
551
+ await runSandboxUse({
552
+ client: sandboxClientFor({ target }),
553
+ org: await mainOrgInfo({ target }),
554
+ ...(query === undefined ? {} : { query }),
555
+ json: options.json === true,
556
+ envDirs: projectOf({ command }).envDirs,
557
+ prompter: prompterFor({ command }),
558
+ });
559
+ } catch (error) {
560
+ throw withSandboxScopeHint({ error });
561
+ }
562
+ },
563
+ );
564
+
565
+ sandbox
566
+ .command("create")
567
+ .description("mint a sandbox and write its key to your .env")
568
+ .argument("<name>", "name for the sandbox")
569
+ .option("--color <color>", "colour the dashboard labels it with")
570
+ .option("--icon <icon>", "icon the dashboard labels it with")
571
+ .option("--use", "pin AUTUMN_SANDBOX_ID so later commands target it")
572
+ .option("--json", "print the response instead of the summary")
573
+ .action(
574
+ async (
575
+ name: string,
576
+ options: {
577
+ color?: string;
578
+ icon?: string;
579
+ use?: boolean;
580
+ json?: boolean;
581
+ },
582
+ command: Command,
583
+ ) => {
584
+ const target = prepareTarget({ command });
585
+ try {
586
+ await runSandboxCreate({
587
+ client: sandboxClientFor({ target }),
588
+ name,
589
+ // The palette lives in the spec; an unknown colour is the
590
+ // server's error to give, not a list the CLI keeps in sync.
591
+ ...(options.color === undefined
592
+ ? {}
593
+ : { color: options.color as CreateSandboxParams["color"] }),
594
+ ...(options.icon === undefined ? {} : { icon: options.icon }),
595
+ use: options.use === true,
596
+ json: options.json === true,
597
+ envDirs: projectOf({ command }).envDirs,
598
+ });
599
+ } catch (error) {
600
+ throw withSandboxScopeHint({ error });
601
+ }
602
+ },
603
+ );
604
+
605
+ sandbox
606
+ .command("delete")
607
+ .description("delete a sandbox and drop its key from your .env")
608
+ .argument("<id>", "id of the sandbox to delete")
609
+ .option("-y, --yes", "delete it, catalog and customers included")
610
+ .action(
611
+ async (id: string, options: { yes?: boolean }, command: Command) => {
612
+ const target = prepareTarget({ command });
613
+ try {
614
+ await runSandboxDelete({
615
+ client: sandboxClientFor({ target }),
616
+ id,
617
+ yes: options.yes === true,
618
+ envDirs: projectOf({ command }).envDirs,
619
+ });
620
+ } catch (error) {
621
+ throw withSandboxScopeHint({ error });
622
+ }
623
+ },
624
+ );
625
+
626
+ return program;
627
+ };
628
+
629
+ export const run = async ({ argv }: { argv: string[] }): Promise<void> => {
630
+ try {
631
+ await buildProgram().parseAsync(normalizeVersionFlag({ argv }));
632
+ } catch (error) {
633
+ // A headless run stopping at a prompt has already printed what it
634
+ // needs; that is a normal end, not a failure.
635
+ if (error instanceof NeedsInputError) return;
636
+ throw error;
637
+ } finally {
638
+ releaseStdin();
639
+ }
640
+ };
641
+
642
+ if (import.meta.main) {
643
+ run({ argv: process.argv }).catch((error: unknown) => {
644
+ const message = error instanceof Error ? error.message : String(error);
645
+ process.stderr.write(`${message}\n`);
646
+ process.exit(1);
647
+ });
648
+ }
@@ -0,0 +1,9 @@
1
+ const PUBLISHED_PACKAGE = "atmn";
2
+
3
+ /**
4
+ * Specifier a scaffolded autumn.config.ts imports from.
5
+ */
6
+ export const configPackageName = (): string => {
7
+ const override = process.env.ATMN_CONFIG_PACKAGE?.trim();
8
+ return override && override.length > 0 ? override : PUBLISHED_PACKAGE;
9
+ };
@@ -0,0 +1,25 @@
1
+ import { basename } from "node:path";
2
+
3
+ /** The 1.x package exported an `item` builder; 2.x never did. */
4
+ const LEGACY_IMPORT =
5
+ /import\s*\{[^}]*\bitem\b[^}]*\}\s*from\s*["']atmn(?:-nightly)?["']/;
6
+ const DEFAULT_EXPORT = /^\s*export\s+default\b/m;
7
+
8
+ /** A config written for atmn 1.x: it imports `item`, or has no root export. */
9
+ export const isLegacyConfigText = ({ text }: { text: string }): boolean =>
10
+ LEGACY_IMPORT.test(text) || !DEFAULT_EXPORT.test(text);
11
+
12
+ export class LegacyConfigError extends Error {
13
+ constructor({ path }: { path: string }) {
14
+ super(
15
+ [
16
+ `${basename(path)} was written for atmn 1.x. atmn 2 uses a new config format.`,
17
+ "",
18
+ " 1. Take note of any pending changes you have made to your config",
19
+ " 2. Rebuild the existing config from your org: atmn pull --overwrite --yes",
20
+ " 3. Re-apply any changes you made before upgrading to v2 in the new format and push when ready",
21
+ ].join("\n"),
22
+ );
23
+ this.name = "LegacyConfigError";
24
+ }
25
+ }