atmn 2.0.32 → 2.0.34

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 (40) hide show
  1. package/dist/bin.js +1408 -188
  2. package/dist/index.js +220 -19
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/package.json +2 -2
  5. package/src/actions/pull/webhooks/appendWebhook.ts +84 -0
  6. package/src/actions/pull/webhooks/applyWebhooksPull.ts +100 -0
  7. package/src/actions/pull/webhooks/locateWebhook.ts +22 -0
  8. package/src/actions/pull/webhooks/removeWebhookEnv.ts +141 -0
  9. package/src/actions/pull/webhooks/types.ts +20 -0
  10. package/src/actions/pull/webhooks/updateWebhook.ts +138 -0
  11. package/src/actions/pull.ts +32 -2
  12. package/src/actions/push/pushLanesError.ts +47 -0
  13. package/src/actions/push.ts +220 -60
  14. package/src/actions/webhooks/applyWebhooks.ts +52 -0
  15. package/src/actions/webhooks/isDashboardWebhook.ts +9 -0
  16. package/src/actions/webhooks/newWebhookId.ts +4 -0
  17. package/src/actions/webhooks/previewWebhooks.ts +40 -0
  18. package/src/actions/webhooks/resolveWebhookEnv.ts +34 -0
  19. package/src/actions/webhooks/resolveWebhooksForEnv.ts +29 -0
  20. package/src/actions/webhooks/types/webhookEnv.ts +9 -0
  21. package/src/actions/webhooks/webhookSecretName.ts +22 -0
  22. package/src/actions/webhooks/writeWebhookSecrets.ts +55 -0
  23. package/src/cli.ts +20 -1
  24. package/src/config/loadConfig.ts +3 -0
  25. package/src/generated/apiRoutes.ts +165 -1
  26. package/src/generated/client.ts +239 -1
  27. package/src/generated/emit.ts +20 -1
  28. package/src/generated/emitRuntime.ts +8 -0
  29. package/src/generated/isLocalWebhookUrl.ts +64 -0
  30. package/src/generated/lintRules.ts +95 -0
  31. package/src/generated/lintRuntime.ts +113 -10
  32. package/src/generated/sandboxName.ts +52 -0
  33. package/src/generated/skills.ts +10 -10
  34. package/src/generated/webhooks.ts +36 -0
  35. package/src/generated/wire.ts +32 -5
  36. package/src/index.ts +1 -0
  37. package/src/render/renderPreview.ts +17 -2
  38. package/src/render/renderWebhooks.ts +92 -0
  39. package/src/surgery/appendPropertyEdit.ts +9 -3
  40. package/src/surgery/patchFixturePaths.ts +57 -5
@@ -0,0 +1,92 @@
1
+ import chalk from "chalk";
2
+ import type { PreviewSyncWebhooksResponse } from "../generated/client";
3
+
4
+ type WebhookChange = PreviewSyncWebhooksResponse["changes"][number];
5
+ type WebhookState = Extract<WebhookChange, { action: "create" }>["webhook"];
6
+
7
+ /** One push's webhook lane: the server's diff for the target env. */
8
+ export type WebhooksPreview = {
9
+ changes: WebhookChange[];
10
+ };
11
+
12
+ export const webhooksHaveWork = ({
13
+ webhooks,
14
+ }: {
15
+ webhooks: WebhooksPreview | undefined;
16
+ }): boolean =>
17
+ (webhooks?.changes ?? []).some((change) => change.action !== "unmanaged");
18
+
19
+ const DETAIL_INDENT = " ";
20
+
21
+ const list = (values: readonly string[]): string =>
22
+ values.length === 0 ? "(none)" : values.join(", ");
23
+
24
+ /** Every field that moved, one line each; the URL first, since it is the one that bites. */
25
+ const updateDetails = ({
26
+ id,
27
+ before,
28
+ after,
29
+ }: {
30
+ id: string;
31
+ before: WebhookState;
32
+ after: WebhookState;
33
+ }): string[] => {
34
+ const lines: string[] = [];
35
+ if (before.url !== after.url)
36
+ lines.push(`${id} url: ${before.url} → ${after.url}`);
37
+ const events = (state: WebhookState) => [...state.events].sort();
38
+ if (events(before).join() !== events(after).join())
39
+ lines.push(`events: ${list(events(before))} → ${list(events(after))}`);
40
+ if ((before.description ?? "") !== (after.description ?? ""))
41
+ lines.push(
42
+ `description: ${JSON.stringify(before.description ?? "")} → ${JSON.stringify(after.description ?? "")}`,
43
+ );
44
+ if (before.disabled !== after.disabled)
45
+ lines.push(`disabled: ${before.disabled} → ${after.disabled}`);
46
+ return lines.map((line) => `${DETAIL_INDENT}${chalk.yellow(line)}`);
47
+ };
48
+
49
+ const renderChange = ({ change }: { change: WebhookChange }): string[] => {
50
+ if (change.action === "create")
51
+ return [
52
+ ` ${chalk.green(`+ ${change.id}`)}${chalk.dim(` ${change.webhook.url}`)}`,
53
+ `${DETAIL_INDENT}${chalk.dim(`events: ${list(change.webhook.events)}`)}`,
54
+ ];
55
+ if (change.action === "adopt")
56
+ return [
57
+ ` ${chalk.yellow(`~ ${change.id}`)} ${chalk.bold("adopt")} ${chalk.dim("existing dashboard webhook · signing secret unchanged")}`,
58
+ ...updateDetails({
59
+ id: change.id,
60
+ before: change.before,
61
+ after: change.after,
62
+ }),
63
+ ];
64
+ if (change.action === "update")
65
+ return [
66
+ ` ${chalk.yellow(`~ ${change.id}`)}`,
67
+ ...updateDetails({
68
+ id: change.id,
69
+ before: change.before,
70
+ after: change.after,
71
+ }),
72
+ ];
73
+ return [
74
+ chalk.dim(
75
+ ` · ${change.id} unmanaged (not in your config; atmn leaves it alone)`,
76
+ ),
77
+ ];
78
+ };
79
+
80
+ /** Null when the lane has nothing to say, so push prints no empty heading. */
81
+ export const renderWebhooks = ({
82
+ webhooks,
83
+ }: {
84
+ webhooks: WebhooksPreview;
85
+ }): string | null => {
86
+ const rows = webhooks.changes.flatMap((change) => renderChange({ change }));
87
+ if (rows.length === 0) return null;
88
+ const count = webhooks.changes.filter(
89
+ (change) => change.action !== "unmanaged",
90
+ ).length;
91
+ return [chalk.bold(`Webhooks (${count})`), ...rows].join("\n");
92
+ };
@@ -1,5 +1,5 @@
1
1
  import type { Edit, SgNode } from "@ast-grep/napi";
2
- import { leadingIndentOfLine, lineStartOf } from "./fixtureEdit";
2
+ import { leadingIndentOfLine, lineEndOf, lineStartOf } from "./fixtureEdit";
3
3
 
4
4
  const isMember = (child: SgNode): boolean =>
5
5
  child.kind() === "pair" ||
@@ -49,9 +49,15 @@ export const appendPropertyEdit = ({
49
49
  // Anchored to the last member itself, so a value whose closing brace shares
50
50
  // the object's closing line can never push the insert outside the literal.
51
51
  const indent = leadingIndentOfLine(source, last.range().start.index);
52
+ // A trailing `// comment` belongs to the member it follows: insert after it.
53
+ const restOfLine = source.slice(afterComma, lineEndOf(source, afterComma));
54
+ const at =
55
+ trailingComma && /^\s*\/\/.*$/.test(restOfLine)
56
+ ? afterComma + restOfLine.length
57
+ : afterComma;
52
58
  return {
53
- startPos: afterComma,
54
- endPos: afterComma,
59
+ startPos: at,
60
+ endPos: at,
55
61
  insertedText: trailingComma
56
62
  ? `\n${indent}${pair},`
57
63
  : `,\n${indent}${pair},`,
@@ -7,6 +7,7 @@ import {
7
7
  findFixtures,
8
8
  fixtureObjectOf,
9
9
  } from "./findFixture";
10
+ import { lineEndOf, lineStartOf } from "./fixtureEdit";
10
11
  import {
11
12
  DYNAMIC_VALUE,
12
13
  fixtureArrayElements,
@@ -47,7 +48,48 @@ const matchesLiteral = ({
47
48
  return !isDynamic(proposed) && isDeepStrictEqual(value, proposed);
48
49
  };
49
50
 
50
- const removeMemberEdits = ({ member }: { member: SgNode }): Edit[] => {
51
+ const PLAIN_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
52
+
53
+ /** Inserted keys are quoted unless the caller asks for identifiers where they are valid. */
54
+ const keyText = ({
55
+ key,
56
+ bareKeys,
57
+ }: {
58
+ key: string;
59
+ bareKeys: boolean;
60
+ }): string =>
61
+ bareKeys && PLAIN_IDENTIFIER.test(key) ? key : JSON.stringify(key);
62
+
63
+ /** A member alone on its line goes with the whole line, its own trailing comment included. */
64
+ const wholeLineEdit = ({
65
+ source,
66
+ member,
67
+ comma,
68
+ }: {
69
+ source: string;
70
+ member: SgNode;
71
+ comma: SgNode | null;
72
+ }): Edit | null => {
73
+ const start = member.range().start.index;
74
+ const lineStart = lineStartOf(source, start);
75
+ if (source.slice(lineStart, start).trim() !== "") return null;
76
+ const end = Math.max(member.range().end.index, comma?.range().end.index ?? 0);
77
+ const lineEnd = lineEndOf(source, end);
78
+ if (!/^\s*(\/\/.*)?$/.test(source.slice(end, lineEnd))) return null;
79
+ return {
80
+ startPos: lineStart,
81
+ endPos: Math.min(lineEnd + 1, source.length),
82
+ insertedText: "",
83
+ };
84
+ };
85
+
86
+ const removeMemberEdits = ({
87
+ source,
88
+ member,
89
+ }: {
90
+ source: string;
91
+ member: SgNode;
92
+ }): Edit[] => {
51
93
  const siblings = member.parent()?.children() ?? [];
52
94
  const index = siblings.findIndex(
53
95
  (sibling) => sibling.range().start.index === member.range().start.index,
@@ -61,6 +103,11 @@ const removeMemberEdits = ({ member }: { member: SgNode }): Edit[] => {
61
103
  .find((sibling) => sibling.kind() !== "comment");
62
104
  const comma =
63
105
  next?.kind() === "," ? next : previous?.kind() === "," ? previous : null;
106
+ const line =
107
+ comma === next || comma === null
108
+ ? wholeLineEdit({ source, member, comma })
109
+ : null;
110
+ if (line !== null) return [line];
64
111
  return [member, ...(comma === null ? [] : [comma])].map((node) => ({
65
112
  startPos: node.range().start.index,
66
113
  endPos: node.range().end.index,
@@ -74,10 +121,12 @@ const pathEdits = ({
74
121
  path,
75
122
  text,
76
123
  builders,
124
+ bareKeys,
77
125
  }: {
78
126
  source: string;
79
127
  object: SgNode;
80
128
  builders: readonly string[];
129
+ bareKeys: boolean;
81
130
  } & Assignment): Edit[] | string => {
82
131
  let current = object;
83
132
  for (let index = 0; index < path.length; index += 1) {
@@ -124,8 +173,8 @@ const pathEdits = ({
124
173
  return "missing-array-element";
125
174
  let inserted = text;
126
175
  for (const key of remaining.reverse())
127
- inserted = `{ ${JSON.stringify(key)}: ${inserted} }`;
128
- const pair = `${JSON.stringify(segment)}: ${inserted}`;
176
+ inserted = `{ ${keyText({ key: String(key), bareKeys })}: ${inserted} }`;
177
+ const pair = `${keyText({ key: segment, bareKeys })}: ${inserted}`;
129
178
  const members = current
130
179
  .namedChildren()
131
180
  .filter((child) => child.kind() !== "comment");
@@ -149,7 +198,7 @@ const pathEdits = ({
149
198
  if (text === null)
150
199
  return member === undefined
151
200
  ? "array-element-removal"
152
- : removeMemberEdits({ member });
201
+ : removeMemberEdits({ source, member });
153
202
  if (matchesLiteral({ value: currentValue, text })) return [];
154
203
  return [
155
204
  {
@@ -170,6 +219,7 @@ export const patchFixturePaths = ({
170
219
  where,
171
220
  assignments,
172
221
  builders = [],
222
+ bareKeys = false,
173
223
  }: {
174
224
  source: string;
175
225
  builder: FixtureShape;
@@ -178,6 +228,8 @@ export const patchFixturePaths = ({
178
228
  where?: FixtureConstraint[];
179
229
  assignments: Assignment[];
180
230
  builders?: readonly string[];
231
+ /** Write an inserted key bare when it is a valid identifier. */
232
+ bareKeys?: boolean;
181
233
  }): PatchResult | null => {
182
234
  const selector = { builder, idField, id, where, allowDynamic: true };
183
235
  const initial = findFixtures({ source, ...selector });
@@ -193,7 +245,7 @@ export const patchFixturePaths = ({
193
245
  const edits =
194
246
  object === null
195
247
  ? "ambiguous-fixture"
196
- : pathEdits({ source, object, builders, ...assignment });
248
+ : pathEdits({ source, object, builders, bareKeys, ...assignment });
197
249
  if (typeof edits === "string") {
198
250
  skipped.push({ path: assignment.path, reason: edits });
199
251
  continue;