assistant-ui 0.0.112 → 0.0.114

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 (91) hide show
  1. package/dist/codemods/v0-12/assistant-api-to-aui.d.ts.map +1 -1
  2. package/dist/codemods/v0-12/assistant-api-to-aui.js +87 -62
  3. package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
  4. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
  5. package/dist/codemods/v0-12/primitive-if-to-aui-if.js +31 -45
  6. package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
  7. package/dist/commands/add.d.ts +1 -1
  8. package/dist/commands/add.d.ts.map +1 -1
  9. package/dist/commands/add.js +4 -2
  10. package/dist/commands/add.js.map +1 -1
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/create.js +33 -10
  13. package/dist/commands/create.js.map +1 -1
  14. package/dist/commands/info.d.ts.map +1 -1
  15. package/dist/commands/info.js +2 -1
  16. package/dist/commands/info.js.map +1 -1
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +4 -2
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/mcp.d.ts.map +1 -1
  21. package/dist/commands/mcp.js +52 -40
  22. package/dist/commands/mcp.js.map +1 -1
  23. package/dist/commands/update.js +6 -7
  24. package/dist/commands/update.js.map +1 -1
  25. package/dist/commands/upgrade.d.ts.map +1 -1
  26. package/dist/commands/upgrade.js +5 -2
  27. package/dist/commands/upgrade.js.map +1 -1
  28. package/dist/index.js +2 -6
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/agent-skill.d.ts +1 -1
  31. package/dist/lib/agent-skill.d.ts.map +1 -1
  32. package/dist/lib/agent-skill.js.map +1 -1
  33. package/dist/lib/create-project.d.ts +11 -3
  34. package/dist/lib/create-project.d.ts.map +1 -1
  35. package/dist/lib/create-project.js +127 -43
  36. package/dist/lib/create-project.js.map +1 -1
  37. package/dist/lib/handle-cli-error.d.ts +5 -0
  38. package/dist/lib/handle-cli-error.d.ts.map +1 -0
  39. package/dist/lib/handle-cli-error.js +17 -0
  40. package/dist/lib/handle-cli-error.js.map +1 -0
  41. package/dist/lib/install-ai-sdk-lib.js +8 -1
  42. package/dist/lib/install-ai-sdk-lib.js.map +1 -1
  43. package/dist/lib/install-edge-lib.js +4 -8
  44. package/dist/lib/install-edge-lib.js.map +1 -1
  45. package/dist/lib/run-spawn.d.ts +16 -2
  46. package/dist/lib/run-spawn.d.ts.map +1 -1
  47. package/dist/lib/run-spawn.js +87 -9
  48. package/dist/lib/run-spawn.js.map +1 -1
  49. package/dist/lib/transform.d.ts +1 -1
  50. package/dist/lib/transform.d.ts.map +1 -1
  51. package/dist/lib/transform.js +12 -25
  52. package/dist/lib/transform.js.map +1 -1
  53. package/dist/lib/upgrade.js +20 -16
  54. package/dist/lib/upgrade.js.map +1 -1
  55. package/dist/lib/utils/file-scanner.d.ts +9 -1
  56. package/dist/lib/utils/file-scanner.d.ts.map +1 -1
  57. package/dist/lib/utils/file-scanner.js +22 -20
  58. package/dist/lib/utils/file-scanner.js.map +1 -1
  59. package/dist/lib/utils/package-installer.js +1 -1
  60. package/dist/lib/utils/package-manager.d.ts +3 -1
  61. package/dist/lib/utils/package-manager.d.ts.map +1 -1
  62. package/dist/lib/utils/package-manager.js +22 -10
  63. package/dist/lib/utils/package-manager.js.map +1 -1
  64. package/package.json +7 -7
  65. package/plugin/skills/assistant-ui/SKILL.md +4 -4
  66. package/src/codemods/v0-12/__tests__/assistant-api-to-aui.test.ts +370 -0
  67. package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +100 -0
  68. package/src/codemods/v0-12/assistant-api-to-aui.ts +227 -134
  69. package/src/codemods/v0-12/primitive-if-to-aui-if.ts +44 -72
  70. package/src/commands/add.ts +5 -5
  71. package/src/commands/create.ts +41 -7
  72. package/src/commands/info.ts +1 -0
  73. package/src/commands/init.ts +4 -6
  74. package/src/commands/mcp.ts +57 -41
  75. package/src/commands/update.ts +6 -8
  76. package/src/commands/upgrade.ts +5 -2
  77. package/src/index.ts +2 -7
  78. package/src/lib/agent-skill.ts +2 -1
  79. package/src/lib/create-project.test.ts +175 -0
  80. package/src/lib/create-project.ts +189 -59
  81. package/src/lib/handle-cli-error.test.ts +67 -0
  82. package/src/lib/handle-cli-error.ts +17 -0
  83. package/src/lib/install-ai-sdk-lib.ts +13 -1
  84. package/src/lib/install-edge-lib.ts +7 -8
  85. package/src/lib/run-spawn.test.ts +148 -0
  86. package/src/lib/run-spawn.ts +121 -11
  87. package/src/lib/transform.test.ts +87 -0
  88. package/src/lib/transform.ts +21 -37
  89. package/src/lib/upgrade.ts +23 -19
  90. package/src/lib/utils/file-scanner.ts +26 -20
  91. package/src/lib/utils/package-manager.ts +29 -12
@@ -837,3 +837,103 @@ function MyComponent() {
837
837
  });
838
838
  });
839
839
  });
840
+
841
+ describe("elements that cannot be migrated stay intact", () => {
842
+ it.each([
843
+ [
844
+ "spread attributes",
845
+ `<ThreadPrimitive.If {...props}>
846
+ <div>Content</div>
847
+ </ThreadPrimitive.If>`,
848
+ ],
849
+ [
850
+ "unknown props",
851
+ `<MessagePrimitive.If user asChild>
852
+ <div>Content</div>
853
+ </MessagePrimitive.If>`,
854
+ ],
855
+ [
856
+ "no props",
857
+ `<ThreadPrimitive.If>
858
+ <div>Content</div>
859
+ </ThreadPrimitive.If>`,
860
+ ],
861
+ [
862
+ "dynamic prop values",
863
+ `<ThreadPrimitive.If running={someFlag}>
864
+ <div>Content</div>
865
+ </ThreadPrimitive.If>`,
866
+ ],
867
+ [
868
+ "namespaced attributes",
869
+ `<ThreadPrimitive.If empty xml:lang="en">
870
+ <div>Content</div>
871
+ </ThreadPrimitive.If>`,
872
+ ],
873
+ [
874
+ "fixed-condition components with props",
875
+ `<ThreadPrimitive.Empty asChild>
876
+ <div>Content</div>
877
+ </ThreadPrimitive.Empty>`,
878
+ ],
879
+ ])("leaves an element with %s unchanged", (_label, jsx) => {
880
+ const input = `
881
+ import { ThreadPrimitive, MessagePrimitive } from "@assistant-ui/react";
882
+
883
+ function MyComponent({ someFlag }: { someFlag: boolean }) {
884
+ return (
885
+ ${jsx}
886
+ );
887
+ }
888
+ `;
889
+
890
+ expect(applyTransform(input)).toBeNull();
891
+ });
892
+
893
+ it("converts only the migratable element and keeps the output parseable", () => {
894
+ const input = `
895
+ import { ThreadPrimitive } from "@assistant-ui/react";
896
+
897
+ function MyComponent(props: Record<string, unknown>) {
898
+ return (
899
+ <>
900
+ <ThreadPrimitive.If empty>
901
+ <div>Empty</div>
902
+ </ThreadPrimitive.If>
903
+ <ThreadPrimitive.If {...props}>
904
+ <div>Dynamic</div>
905
+ </ThreadPrimitive.If>
906
+ </>
907
+ );
908
+ }
909
+ `;
910
+
911
+ const output = applyTransform(input);
912
+ expect(output).not.toBeNull();
913
+ expect(() => j(output!)).not.toThrow();
914
+ expect(output).toContain("</AuiIf>");
915
+ expect(output).toContain("</ThreadPrimitive.If>");
916
+ expect(output).toContain("<ThreadPrimitive.If {...props}>");
917
+ });
918
+ });
919
+
920
+ describe("null literal prop values", () => {
921
+ it("maps submittedFeedback={null} to the null comparison", () => {
922
+ const input = `
923
+ import { MessagePrimitive } from "@assistant-ui/react";
924
+
925
+ function MyComponent() {
926
+ return (
927
+ <MessagePrimitive.If submittedFeedback={null}>
928
+ <div>Content</div>
929
+ </MessagePrimitive.If>
930
+ );
931
+ }
932
+ `;
933
+
934
+ const output = applyTransform(input);
935
+ expect(output).toContain(
936
+ "(s.message.metadata.submittedFeedback?.type ?? null) === null",
937
+ );
938
+ });
939
+ });
@@ -22,72 +22,8 @@ const isUseAuiCall = (j: any, node: any): boolean => {
22
22
  );
23
23
  };
24
24
 
25
- // Check if a scope node directly contains an 'api' variable declaration
26
- const scopeHasDirectApiDeclaration = (j: any, scopeNode: any): boolean => {
27
- // Get the body of the scope
28
- let body = null;
29
- if (
30
- j.FunctionDeclaration.check(scopeNode) ||
31
- j.FunctionExpression.check(scopeNode)
32
- ) {
33
- body = scopeNode.body?.body;
34
- } else if (j.ArrowFunctionExpression.check(scopeNode)) {
35
- // Arrow functions might have block or expression body
36
- if (j.BlockStatement.check(scopeNode.body)) {
37
- body = scopeNode.body.body;
38
- }
39
- } else if (j.BlockStatement.check(scopeNode)) {
40
- body = scopeNode.body;
41
- }
42
-
43
- if (!Array.isArray(body)) return false;
44
-
45
- // Check only direct statements in this scope's body
46
- for (const statement of body) {
47
- if (j.VariableDeclaration.check(statement)) {
48
- for (const declarator of statement.declarations) {
49
- if (j.Identifier.check(declarator.id) && declarator.id.name === "api") {
50
- // Don't count it as shadowing if it's from useAui
51
- if (!isUseAuiCall(j, declarator.init)) {
52
- return true;
53
- }
54
- }
55
- }
56
- }
57
- }
58
-
59
- return false;
60
- };
61
-
62
- // Check if a path is inside a scope that shadows the api variable
63
- const isInsideShadowingScope = (j: any, identifierPath: any): boolean => {
64
- let currentPath = identifierPath.parent;
65
-
66
- while (currentPath) {
67
- const node = currentPath.value;
68
-
69
- // Check if this is a scope-creating node (function, arrow function, block)
70
- if (
71
- j.FunctionDeclaration.check(node) ||
72
- j.FunctionExpression.check(node) ||
73
- j.ArrowFunctionExpression.check(node) ||
74
- j.BlockStatement.check(node)
75
- ) {
76
- if (scopeHasDirectApiDeclaration(j, node)) {
77
- return true;
78
- }
79
- }
80
-
81
- currentPath = currentPath.parent;
82
- }
83
-
84
- return false;
85
- };
86
-
87
25
  const migrateAssistantApiToAui = createTransformer(
88
26
  ({ j, root, markAsChanged }) => {
89
- let hasApiFromUseAui = false;
90
-
91
27
  // 1. Update imports
92
28
  root.find(j.ImportDeclaration).forEach((path: any) => {
93
29
  const source = path.value.source.value;
@@ -122,91 +58,248 @@ const migrateAssistantApiToAui = createTransformer(
122
58
  }
123
59
  });
124
60
 
125
- // 2. Find and rename variable declarations from useAui (or useAssistantApi)
61
+ // 2. Collect `api` declarators initialized from useAui / useAssistantApi.
62
+ // References are renamed by binding resolution, so an `api` bound
63
+ // elsewhere (function params, `const { api } = other()`) is never touched.
64
+ const renamedDeclaratorIds = new Set<any>();
126
65
  root.find(j.VariableDeclarator).forEach((path: any) => {
127
- const init = path.value.init;
128
-
129
- // Check if this is a call to useAui or useAssistantApi
130
- if (isUseAuiCall(j, init)) {
131
- if (j.Identifier.check(path.value.id)) {
132
- const oldVarName = path.value.id.name;
133
-
134
- // Only rename if it's called 'api'
135
- if (oldVarName === "api") {
136
- path.value.id.name = "aui";
137
- hasApiFromUseAui = true;
138
- markAsChanged();
139
- }
140
- }
66
+ if (
67
+ isUseAuiCall(j, path.value.init) &&
68
+ j.Identifier.check(path.value.id) &&
69
+ path.value.id.name === "api"
70
+ ) {
71
+ renamedDeclaratorIds.add(path.value.id);
141
72
  }
142
73
  });
143
74
 
144
- // 3. Rename all references to 'api' if we found it from useAui
145
- if (hasApiFromUseAui) {
146
- root.find(j.Identifier).forEach((path: any) => {
147
- if (path.value.name === "api") {
148
- // Skip if this is part of an import
149
- if (j.ImportSpecifier.check(path.parent.value)) {
150
- return;
151
- }
152
-
153
- // Skip if this is a variable declarator id
154
- if (j.VariableDeclarator.check(path.parent.value)) {
155
- const declarator = path.parent.value;
156
- if (declarator.id === path.value) {
157
- return;
158
- }
159
- }
160
-
161
- // Skip if this is a property key in an object (e.g., { api: true })
162
- if (j.Property.check(path.parent.value)) {
163
- const prop = path.parent.value;
164
- if (prop.key === path.value && !prop.shorthand && !prop.computed) {
165
- return;
166
- }
167
- }
168
-
169
- if (j.ObjectProperty.check(path.parent.value)) {
170
- const prop = path.parent.value;
171
- if (prop.key === path.value && !prop.shorthand && !prop.computed) {
172
- return;
173
- }
75
+ // 3. Rename references governed by one of those declarators. Resolution
76
+ // is lexical (nearest enclosing declaration wins) rather than via
77
+ // ast-types scopes, which have no block granularity: a block-scoped
78
+ // `const api = other()` inside the same function must shadow.
79
+ if (renamedDeclaratorIds.size > 0) {
80
+ const patternBindsApi = (id: any): boolean => {
81
+ if (
82
+ id &&
83
+ (id.type === "TSParameterProperty" ||
84
+ j.TSParameterProperty?.check?.(id))
85
+ ) {
86
+ return patternBindsApi(id.parameter);
87
+ }
88
+ if (j.Identifier.check(id)) return id.name === "api";
89
+ if (j.ObjectPattern.check(id)) {
90
+ return id.properties.some((prop: any) =>
91
+ patternBindsApi(prop.value ?? prop.argument ?? prop),
92
+ );
93
+ }
94
+ if (j.ArrayPattern.check(id)) {
95
+ return id.elements.some((el: any) => el && patternBindsApi(el));
96
+ }
97
+ if (j.AssignmentPattern.check(id)) return patternBindsApi(id.left);
98
+ if (j.RestElement.check(id)) return patternBindsApi(id.argument);
99
+ return false;
100
+ };
101
+
102
+ // What a statement-level node declares for `api`: the declarator id
103
+ // node when it is a plain `const/let/var api = ...`, "foreign" for any
104
+ // other binding of the name (patterns, functions, classes, enums), or
105
+ // undefined when it does not bind `api` at all.
106
+ const declaredApi = (statement: any): any => {
107
+ if (!statement) return undefined;
108
+ if (
109
+ j.ExportNamedDeclaration.check(statement) ||
110
+ j.ExportDefaultDeclaration.check(statement)
111
+ ) {
112
+ return declaredApi(statement.declaration);
113
+ }
114
+ if (j.VariableDeclaration.check(statement)) {
115
+ for (const declarator of statement.declarations) {
116
+ if (
117
+ j.Identifier.check(declarator.id) &&
118
+ declarator.id.name === "api"
119
+ )
120
+ return declarator.id;
121
+ if (patternBindsApi(declarator.id)) return "foreign";
174
122
  }
175
-
176
- // Skip if this is a property in a member expression (e.g., foo.api)
123
+ return undefined;
124
+ }
125
+ // Type-only declarations do not shadow the value binding.
126
+ if (
127
+ statement.type === "TSTypeAliasDeclaration" ||
128
+ statement.type === "TSInterfaceDeclaration" ||
129
+ statement.type === "TSDeclareFunction"
130
+ )
131
+ return undefined;
132
+ // FunctionDeclaration, ClassDeclaration, TS enums/namespaces, …
133
+ if (
134
+ statement.id &&
135
+ j.Identifier.check(statement.id) &&
136
+ statement.id.name === "api"
137
+ )
138
+ return "foreign";
139
+ return undefined;
140
+ };
141
+
142
+ const scanStatements = (statements: any[]): any => {
143
+ for (const statement of statements) {
144
+ const found = declaredApi(statement);
145
+ if (found !== undefined) return found;
146
+ }
147
+ return undefined;
148
+ };
149
+
150
+ // Returns the declarator id node governing `api` here, or "foreign"
151
+ // when any other binding of the name shadows it first.
152
+ const governingApiBinding = (path: any): any => {
153
+ let current = path.parent;
154
+ while (current) {
155
+ const node = current.value;
156
+
157
+ // Anything function-like (declarations, expressions, arrows,
158
+ // object/class methods) binds its params.
159
+ if (Array.isArray(node.params) && node.params.some(patternBindsApi))
160
+ return "foreign";
161
+ // A named function/class expression binds its own name in its body.
177
162
  if (
178
- j.MemberExpression.check(path.parent.value) &&
179
- path.parent.value.property === path.value &&
180
- !path.parent.value.computed
163
+ (j.FunctionExpression.check(node) ||
164
+ j.ClassExpression.check(node)) &&
165
+ node.id?.name === "api"
166
+ )
167
+ return "foreign";
168
+ if (
169
+ j.CatchClause.check(node) &&
170
+ node.param &&
171
+ patternBindsApi(node.param)
172
+ )
173
+ return "foreign";
174
+
175
+ let found: any;
176
+ if (j.BlockStatement.check(node) || j.Program.check(node)) {
177
+ found = scanStatements(node.body);
178
+ } else if (j.ForStatement.check(node)) {
179
+ found = declaredApi(node.init);
180
+ } else if (
181
+ j.ForOfStatement.check(node) ||
182
+ j.ForInStatement.check(node)
181
183
  ) {
182
- return;
184
+ found = declaredApi(node.left);
185
+ } else if (j.SwitchStatement.check(node)) {
186
+ found = scanStatements(
187
+ node.cases.flatMap((c: any) => c.consequent),
188
+ );
189
+ } else if (j.StaticBlock?.check?.(node)) {
190
+ found = scanStatements(node.body);
183
191
  }
192
+ if (found !== undefined) return found;
184
193
 
185
- // Skip if this is a JSX attribute name
186
- if (j.JSXAttribute.check(path.parent.value)) {
194
+ current = current.parent;
195
+ }
196
+ return undefined;
197
+ };
198
+
199
+ const bindsToRenamedApi = (path: any): boolean => {
200
+ const governing = governingApiBinding(path);
201
+ return governing !== "foreign" && renamedDeclaratorIds.has(governing);
202
+ };
203
+
204
+ const referencePaths: any[] = [];
205
+ root.find(j.Identifier, { name: "api" }).forEach((path: any) => {
206
+ const parent = path.parent.value;
207
+ if (j.ImportSpecifier.check(parent)) return;
208
+ // Declaration names (variable, function, class, type alias,
209
+ // interface) and TS type positions are not value references.
210
+ if (parent.id === path.value) return;
211
+ if (j.TSTypeReference?.check?.(parent)) return;
212
+ if (j.TSQualifiedName?.check?.(parent)) return;
213
+ // Any non-computed key position is a name, not a reference: object
214
+ // properties, object/class methods, class properties, TS signatures.
215
+ // Esprima-style shorthand reuses one node as key and value, so the
216
+ // value position must survive the guard.
217
+ if (
218
+ parent.key === path.value &&
219
+ !parent.computed &&
220
+ parent.value !== path.value
221
+ )
222
+ return;
223
+ if (
224
+ j.MemberExpression.check(parent) &&
225
+ parent.property === path.value &&
226
+ !parent.computed
227
+ )
228
+ return;
229
+ // JSXIdentifier extends Identifier, so JSX positions land here too:
230
+ // member properties (<config.api/>), namespace names, and lowercase
231
+ // element names (<api/> is an intrinsic tag) are not references.
232
+ if (
233
+ j.JSXMemberExpression?.check?.(parent) &&
234
+ parent.property === path.value
235
+ )
236
+ return;
237
+ if (j.JSXNamespacedName?.check?.(parent)) return;
238
+ if (
239
+ (j.JSXOpeningElement?.check?.(parent) ||
240
+ j.JSXClosingElement?.check?.(parent)) &&
241
+ parent.name === path.value
242
+ )
243
+ return;
244
+ if (j.JSXAttribute.check(parent)) return;
245
+ // The exported name of `export { api }` is the public alias, not a
246
+ // reference; only the local side is renamed (to `aui as api`). A
247
+ // source-bearing re-export binds in the other module, never here.
248
+ if (j.ExportSpecifier.check(parent)) {
249
+ const grandparent = path.parent.parent?.value;
250
+ if (
251
+ j.ExportNamedDeclaration.check(grandparent) &&
252
+ grandparent.source != null
253
+ )
187
254
  return;
188
- }
189
-
190
- // Skip if this identifier is inside a scope that shadows the api variable
191
- if (isInsideShadowingScope(j, path)) {
255
+ if (
256
+ grandparent?.exportKind === "type" ||
257
+ parent.exportKind === "type"
258
+ )
259
+ return;
260
+ if (parent.exported === path.value && parent.local !== path.value)
192
261
  return;
193
- }
194
-
195
- // Update the reference
196
- path.value.name = "aui";
197
- markAsChanged();
198
262
  }
263
+ if (!bindsToRenamedApi(path)) return;
264
+ referencePaths.push(path);
199
265
  });
200
266
 
201
- // Also handle JSX identifiers
202
- root.find(j.JSXIdentifier).forEach((path: any) => {
203
- if (path.value.name === "api") {
204
- if (!isInsideShadowingScope(j, path)) {
205
- path.value.name = "aui";
206
- markAsChanged();
207
- }
267
+ for (const path of referencePaths) {
268
+ const parent = path.parent.value;
269
+ if (
270
+ (j.Property.check(parent) || j.ObjectProperty.check(parent)) &&
271
+ parent.shorthand &&
272
+ parent.value === path.value
273
+ ) {
274
+ // `{ api }` in an object literal: keep the key, rename the value
275
+ parent.shorthand = false;
276
+ parent.key = j.identifier("api");
277
+ parent.value = j.identifier("aui");
278
+ } else if (
279
+ j.ExportSpecifier.check(parent) &&
280
+ parent.local === path.value
281
+ ) {
282
+ // `export { api }` / `export { api as name }`: rename the local
283
+ // binding, keep the public name. Replaced wholesale — recast keeps
284
+ // the shorthand form (dropping the alias) when only the fields of
285
+ // the original node change.
286
+ path.parent.replace(
287
+ j.exportSpecifier.from({
288
+ local: j.identifier("aui"),
289
+ exported: j.Identifier.check(parent.exported)
290
+ ? j.identifier(parent.exported.name)
291
+ : parent.exported,
292
+ }),
293
+ );
294
+ } else {
295
+ path.value.name = "aui";
208
296
  }
209
- });
297
+ markAsChanged();
298
+ }
299
+ for (const idNode of renamedDeclaratorIds) {
300
+ idNode.name = "aui";
301
+ markAsChanged();
302
+ }
210
303
  }
211
304
 
212
305
  // 4. Update hook call references (in case they're used as values)