assistant-ui 0.0.116 → 0.0.118

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 (127) hide show
  1. package/dist/codemods/utils/createTransformer.d.ts +13 -16
  2. package/dist/codemods/utils/createTransformer.d.ts.map +1 -1
  3. package/dist/codemods/utils/resolveBinding.d.ts +3 -0
  4. package/dist/codemods/utils/resolveBinding.d.ts.map +1 -0
  5. package/dist/codemods/utils/resolveBinding.js +78 -0
  6. package/dist/codemods/utils/resolveBinding.js.map +1 -0
  7. package/dist/codemods/v0-11/content-part-to-message-part.d.ts +1 -3
  8. package/dist/codemods/v0-11/content-part-to-message-part.d.ts.map +1 -1
  9. package/dist/codemods/v0-12/assistant-api-to-aui.d.ts +1 -3
  10. package/dist/codemods/v0-12/assistant-api-to-aui.d.ts.map +1 -1
  11. package/dist/codemods/v0-12/assistant-api-to-aui.js +51 -99
  12. package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
  13. package/dist/codemods/v0-12/event-names-to-camelcase.d.ts +1 -3
  14. package/dist/codemods/v0-12/event-names-to-camelcase.d.ts.map +1 -1
  15. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts +1 -3
  16. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
  17. package/dist/codemods/v0-15/aui-accessor-calls-to-properties.d.ts +1 -3
  18. package/dist/codemods/v0-15/aui-accessor-calls-to-properties.d.ts.map +1 -1
  19. package/dist/codemods/v0-15/aui-accessor-calls-to-properties.js +33 -5
  20. package/dist/codemods/v0-15/aui-accessor-calls-to-properties.js.map +1 -1
  21. package/dist/codemods/v0-8/ui-package-split.d.ts +1 -3
  22. package/dist/codemods/v0-8/ui-package-split.d.ts.map +1 -1
  23. package/dist/codemods/v0-9/edge-package-split.d.ts +1 -3
  24. package/dist/codemods/v0-9/edge-package-split.d.ts.map +1 -1
  25. package/dist/commands/add.d.ts +13 -16
  26. package/dist/commands/add.d.ts.map +1 -1
  27. package/dist/commands/add.js +3 -3
  28. package/dist/commands/agent.d.ts +1 -4
  29. package/dist/commands/agent.d.ts.map +1 -1
  30. package/dist/commands/agent.js +16 -13
  31. package/dist/commands/agent.js.map +1 -1
  32. package/dist/commands/create.d.ts +43 -39
  33. package/dist/commands/create.d.ts.map +1 -1
  34. package/dist/commands/create.js +40 -26
  35. package/dist/commands/create.js.map +1 -1
  36. package/dist/commands/doctor.d.ts +17 -20
  37. package/dist/commands/doctor.d.ts.map +1 -1
  38. package/dist/commands/info.d.ts +4 -6
  39. package/dist/commands/info.d.ts.map +1 -1
  40. package/dist/commands/info.js +3 -3
  41. package/dist/commands/init.d.ts +8 -10
  42. package/dist/commands/init.d.ts.map +1 -1
  43. package/dist/commands/init.js +3 -4
  44. package/dist/commands/init.js.map +1 -1
  45. package/dist/commands/mcp.d.ts +1 -4
  46. package/dist/commands/mcp.d.ts.map +1 -1
  47. package/dist/commands/mcp.js +2 -2
  48. package/dist/commands/update.d.ts +1 -4
  49. package/dist/commands/update.d.ts.map +1 -1
  50. package/dist/commands/update.js +1 -1
  51. package/dist/commands/upgrade.d.ts +7 -10
  52. package/dist/commands/upgrade.d.ts.map +1 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/lib/agent-skill.d.ts +11 -10
  56. package/dist/lib/agent-skill.d.ts.map +1 -1
  57. package/dist/lib/agent-skill.js +78 -2
  58. package/dist/lib/agent-skill.js.map +1 -1
  59. package/dist/lib/create-project.d.ts +33 -32
  60. package/dist/lib/create-project.d.ts.map +1 -1
  61. package/dist/lib/create-project.js +91 -5
  62. package/dist/lib/create-project.js.map +1 -1
  63. package/dist/lib/handle-cli-error.d.ts +1 -4
  64. package/dist/lib/handle-cli-error.d.ts.map +1 -1
  65. package/dist/lib/install-ai-sdk-lib.d.ts +1 -4
  66. package/dist/lib/install-ai-sdk-lib.d.ts.map +1 -1
  67. package/dist/lib/install-edge-lib.d.ts +1 -4
  68. package/dist/lib/install-edge-lib.d.ts.map +1 -1
  69. package/dist/lib/run-spawn.d.ts +17 -19
  70. package/dist/lib/run-spawn.d.ts.map +1 -1
  71. package/dist/lib/run-spawn.js +25 -10
  72. package/dist/lib/run-spawn.js.map +1 -1
  73. package/dist/lib/transform-options.d.ts +5 -8
  74. package/dist/lib/transform-options.d.ts.map +1 -1
  75. package/dist/lib/transform.d.ts +13 -16
  76. package/dist/lib/transform.d.ts.map +1 -1
  77. package/dist/lib/transform.js +19 -35
  78. package/dist/lib/transform.js.map +1 -1
  79. package/dist/lib/upgrade.d.ts +3 -6
  80. package/dist/lib/upgrade.d.ts.map +1 -1
  81. package/dist/lib/upgrade.js +6 -11
  82. package/dist/lib/upgrade.js.map +1 -1
  83. package/dist/lib/utils/config.d.ts +18 -21
  84. package/dist/lib/utils/config.d.ts.map +1 -1
  85. package/dist/lib/utils/file-scanner.d.ts +12 -15
  86. package/dist/lib/utils/file-scanner.d.ts.map +1 -1
  87. package/dist/lib/utils/logger.d.ts +7 -10
  88. package/dist/lib/utils/logger.d.ts.map +1 -1
  89. package/dist/lib/utils/package-installer.d.ts +7 -10
  90. package/dist/lib/utils/package-installer.d.ts.map +1 -1
  91. package/dist/lib/utils/package-json.d.ts +1 -4
  92. package/dist/lib/utils/package-json.d.ts.map +1 -1
  93. package/dist/lib/utils/package-manager.d.ts +9 -12
  94. package/dist/lib/utils/package-manager.d.ts.map +1 -1
  95. package/dist/lib/utils/package-manager.js +13 -2
  96. package/dist/lib/utils/package-manager.js.map +1 -1
  97. package/dist/lib/utils/registry.d.ts +5 -8
  98. package/dist/lib/utils/registry.d.ts.map +1 -1
  99. package/dist/lib/utils/workspace.d.ts +2 -5
  100. package/dist/lib/utils/workspace.d.ts.map +1 -1
  101. package/dist/program.d.ts +1 -4
  102. package/dist/program.d.ts.map +1 -1
  103. package/dist/program.js +6 -6
  104. package/dist/run.d.ts +1 -4
  105. package/dist/run.d.ts.map +1 -1
  106. package/package.json +8 -8
  107. package/src/codemods/utils/resolveBinding.test.ts +107 -0
  108. package/src/codemods/utils/resolveBinding.ts +130 -0
  109. package/src/codemods/v0-12/__tests__/assistant-api-to-aui.test.ts +256 -0
  110. package/src/codemods/v0-12/assistant-api-to-aui.ts +125 -225
  111. package/src/codemods/v0-15/__tests__/aui-accessor-calls-to-properties.test.ts +101 -0
  112. package/src/codemods/v0-15/aui-accessor-calls-to-properties.ts +60 -5
  113. package/src/commands/agent.ts +15 -22
  114. package/src/commands/create.ts +53 -20
  115. package/src/commands/init.ts +1 -2
  116. package/src/lib/agent-skill.ts +103 -1
  117. package/src/lib/create-project.ts +99 -4
  118. package/src/lib/run-spawn.test.ts +47 -0
  119. package/src/lib/run-spawn.ts +29 -17
  120. package/src/lib/transform-files.test.ts +71 -0
  121. package/src/lib/transform.test.ts +69 -1
  122. package/src/lib/transform.ts +23 -38
  123. package/src/lib/upgrade.ts +9 -13
  124. package/src/lib/utils/package-manager.ts +24 -1
  125. package/plugin/.claude-plugin/plugin.json +0 -8
  126. package/plugin/skills/assistant-ui/SKILL.md +0 -199
  127. package/plugin/skills/assistant-ui/shipables.json +0 -22
@@ -1,4 +1,5 @@
1
1
  import { createTransformer } from "../utils/createTransformer";
2
+ import { resolveBinding } from "../utils/resolveBinding";
2
3
 
3
4
  // Map of old hook names to new hook names
4
5
  const hookRenamingMap: Record<string, string> = {
@@ -13,62 +14,136 @@ const componentRenamingMap: Record<string, string> = {
13
14
  AssistantProvider: "AuiProvider",
14
15
  };
15
16
 
16
- const isUseAuiCall = (j: any, node: any): boolean => {
17
- return (
18
- node &&
19
- j.CallExpression.check(node) &&
20
- j.Identifier.check(node.callee) &&
21
- (node.callee.name === "useAui" || node.callee.name === "useAssistantApi")
22
- );
23
- };
24
-
25
17
  const migrateAssistantApiToAui = createTransformer(
26
18
  ({ j, root, markAsChanged }) => {
27
- // 1. Update imports
28
19
  root.find(j.ImportDeclaration).forEach((path: any) => {
29
20
  const source = path.value.source.value;
30
-
31
- // Only process imports from @assistant-ui packages
32
- if (typeof source === "string" && source.startsWith("@assistant-ui/")) {
33
- path.value.specifiers?.forEach((specifier: any) => {
34
- if (j.ImportSpecifier.check(specifier)) {
35
- const oldName = specifier.imported.name as string;
36
-
37
- // Rename hooks
38
- if (hookRenamingMap[oldName]) {
39
- const newName = hookRenamingMap[oldName];
40
- specifier.imported.name = newName;
41
- if (specifier.local && specifier.local.name === oldName) {
42
- specifier.local.name = newName;
21
+ if (typeof source !== "string" || !source.startsWith("@assistant-ui/"))
22
+ return;
23
+ path.value.specifiers?.forEach((specifier: any, index: number) => {
24
+ if (
25
+ !j.ImportSpecifier.check(specifier) ||
26
+ !j.Identifier.check(specifier.imported)
27
+ )
28
+ return;
29
+ const oldName = specifier.imported.name;
30
+ const importKind = (specifier as { importKind?: string }).importKind;
31
+ const newName =
32
+ hookRenamingMap[oldName] ?? componentRenamingMap[oldName];
33
+ if (!newName) return;
34
+ if (
35
+ specifier.local?.name === oldName &&
36
+ path.value.importKind !== "type" &&
37
+ importKind !== "type"
38
+ ) {
39
+ const references = root
40
+ .find(j.Identifier, { name: oldName })
41
+ .paths()
42
+ .filter((reference: any) => {
43
+ const parent = reference.parent.value;
44
+ const node = reference.value;
45
+ if (parent.type.startsWith("Import") || parent.id === node)
46
+ return false;
47
+ if (
48
+ parent.key === node &&
49
+ !parent.computed &&
50
+ parent.value !== node
51
+ )
52
+ return false;
53
+ if (parent.property === node && !parent.computed) return false;
54
+ if (j.TSQualifiedName.check(parent) && parent.right === node)
55
+ return false;
56
+ if (
57
+ j.JSXAttribute.check(parent) ||
58
+ j.JSXNamespacedName.check(parent)
59
+ )
60
+ return false;
61
+ if (j.ExportSpecifier.check(parent)) {
62
+ if (
63
+ reference.parent.parent.value.source ||
64
+ parent.local !== node
65
+ )
66
+ return false;
43
67
  }
44
- markAsChanged();
45
- }
46
-
47
- // Rename components
48
- if (componentRenamingMap[oldName]) {
49
- const newName = componentRenamingMap[oldName];
50
- specifier.imported.name = newName;
51
- if (specifier.local && specifier.local.name === oldName) {
52
- specifier.local.name = newName;
68
+ return resolveBinding(j, reference, oldName) === specifier.local;
69
+ });
70
+ const canRename =
71
+ !resolveBinding(j, path, newName) &&
72
+ references.every(
73
+ (reference: any) => !resolveBinding(j, reference, newName),
74
+ );
75
+ if (canRename) {
76
+ for (const reference of references) {
77
+ const parent = reference.parent.value;
78
+ if (
79
+ (j.Property.check(parent) || j.ObjectProperty.check(parent)) &&
80
+ parent.shorthand
81
+ ) {
82
+ parent.shorthand = false;
83
+ parent.key = j.identifier(oldName);
84
+ parent.value = j.identifier(newName);
85
+ } else if (j.ExportSpecifier.check(parent)) {
86
+ reference.parent.replace(
87
+ j.exportSpecifier.from({
88
+ local: j.identifier(newName),
89
+ exported: j.Identifier.check(parent.exported)
90
+ ? j.identifier(parent.exported.name)
91
+ : parent.exported,
92
+ }),
93
+ );
94
+ } else {
95
+ reference.value.name = newName;
53
96
  }
54
- markAsChanged();
55
97
  }
98
+ specifier.local.name = newName;
56
99
  }
57
- });
100
+ }
101
+ const replacement: any = j.importSpecifier(
102
+ j.identifier(newName),
103
+ j.Identifier.check(specifier.local)
104
+ ? j.identifier(specifier.local.name)
105
+ : null,
106
+ );
107
+ replacement.importKind = importKind;
108
+ replacement.comments = specifier.comments;
109
+ path.get("specifiers", index).replace(replacement);
110
+ markAsChanged();
111
+ });
112
+ });
113
+
114
+ const hookBindings = new Set<any>();
115
+ root.find(j.ImportDeclaration).forEach((path) => {
116
+ if (
117
+ !String(path.value.source.value).startsWith("@assistant-ui/") ||
118
+ path.value.importKind === "type"
119
+ )
120
+ return;
121
+ for (const specifier of path.value.specifiers ?? []) {
122
+ if (
123
+ j.ImportSpecifier.check(specifier) &&
124
+ j.Identifier.check(specifier.imported) &&
125
+ specifier.imported.name === "useAui" &&
126
+ (specifier as { importKind?: string }).importKind !== "type" &&
127
+ j.Identifier.check(specifier.local)
128
+ )
129
+ hookBindings.add(specifier.local);
58
130
  }
59
131
  });
60
132
 
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
133
  const renamedDeclaratorIds = new Set<any>();
65
134
  root.find(j.VariableDeclarator).forEach((path: any) => {
135
+ const { id, init } = path.value;
66
136
  if (
67
- isUseAuiCall(j, path.value.init) &&
68
- j.Identifier.check(path.value.id) &&
69
- path.value.id.name === "api"
137
+ j.Identifier.check(id) &&
138
+ id.name === "api" &&
139
+ j.CallExpression.check(init) &&
140
+ j.Identifier.check(init.callee) &&
141
+ hookBindings.has(
142
+ resolveBinding(j, path.get("init", "callee"), init.callee.name),
143
+ ) &&
144
+ !resolveBinding(j, path, "aui")
70
145
  ) {
71
- renamedDeclaratorIds.add(path.value.id);
146
+ renamedDeclaratorIds.add(id);
72
147
  }
73
148
  });
74
149
 
@@ -77,130 +152,8 @@ const migrateAssistantApiToAui = createTransformer(
77
152
  // ast-types scopes, which have no block granularity: a block-scoped
78
153
  // `const api = other()` inside the same function must shadow.
79
154
  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 (!j.VariableDeclarator.check(declarator)) continue;
117
- if (
118
- j.Identifier.check(declarator.id) &&
119
- declarator.id.name === "api"
120
- )
121
- return declarator.id;
122
- if (patternBindsApi(declarator.id)) return "foreign";
123
- }
124
- return undefined;
125
- }
126
- // Type-only declarations do not shadow the value binding.
127
- if (
128
- statement.type === "TSTypeAliasDeclaration" ||
129
- statement.type === "TSInterfaceDeclaration" ||
130
- statement.type === "TSDeclareFunction"
131
- )
132
- return undefined;
133
- // FunctionDeclaration, ClassDeclaration, TS enums/namespaces, …
134
- if (
135
- statement.id &&
136
- j.Identifier.check(statement.id) &&
137
- statement.id.name === "api"
138
- )
139
- return "foreign";
140
- return undefined;
141
- };
142
-
143
- const scanStatements = (statements: any[]): any => {
144
- for (const statement of statements) {
145
- const found = declaredApi(statement);
146
- if (found !== undefined) return found;
147
- }
148
- return undefined;
149
- };
150
-
151
- // Returns the declarator id node governing `api` here, or "foreign"
152
- // when any other binding of the name shadows it first.
153
- const governingApiBinding = (path: any): any => {
154
- let current = path.parent;
155
- while (current) {
156
- const node = current.value;
157
-
158
- // Anything function-like (declarations, expressions, arrows,
159
- // object/class methods) binds its params.
160
- if (Array.isArray(node.params) && node.params.some(patternBindsApi))
161
- return "foreign";
162
- // A named function/class expression binds its own name in its body.
163
- if (
164
- (j.FunctionExpression.check(node) ||
165
- j.ClassExpression.check(node)) &&
166
- node.id?.name === "api"
167
- )
168
- return "foreign";
169
- if (
170
- j.CatchClause.check(node) &&
171
- node.param &&
172
- patternBindsApi(node.param)
173
- )
174
- return "foreign";
175
-
176
- let found: any;
177
- if (j.BlockStatement.check(node) || j.Program.check(node)) {
178
- found = scanStatements(node.body);
179
- } else if (j.ForStatement.check(node)) {
180
- found = declaredApi(node.init);
181
- } else if (
182
- j.ForOfStatement.check(node) ||
183
- j.ForInStatement.check(node)
184
- ) {
185
- found = declaredApi(node.left);
186
- } else if (j.SwitchStatement.check(node)) {
187
- found = scanStatements(
188
- node.cases.flatMap((c: any) => c.consequent),
189
- );
190
- } else if (j.StaticBlock?.check?.(node)) {
191
- found = scanStatements(node.body);
192
- }
193
- if (found !== undefined) return found;
194
-
195
- current = current.parent;
196
- }
197
- return undefined;
198
- };
199
-
200
- const bindsToRenamedApi = (path: any): boolean => {
201
- const governing = governingApiBinding(path);
202
- return governing !== "foreign" && renamedDeclaratorIds.has(governing);
203
- };
155
+ const bindsToRenamedApi = (path: any): boolean =>
156
+ renamedDeclaratorIds.has(resolveBinding(j, path, "api"));
204
157
 
205
158
  const referencePaths: any[] = [];
206
159
  root.find(j.Identifier, { name: "api" }).forEach((path: any) => {
@@ -268,6 +221,13 @@ const migrateAssistantApiToAui = createTransformer(
268
221
  });
269
222
 
270
223
  for (const path of referencePaths) {
224
+ if (resolveBinding(j, path, "aui")) {
225
+ renamedDeclaratorIds.delete(resolveBinding(j, path, "api"));
226
+ }
227
+ }
228
+
229
+ for (const path of referencePaths) {
230
+ if (!bindsToRenamedApi(path)) continue;
271
231
  const parent = path.parent.value;
272
232
  if (
273
233
  (j.Property.check(parent) || j.ObjectProperty.check(parent)) &&
@@ -304,66 +264,6 @@ const migrateAssistantApiToAui = createTransformer(
304
264
  markAsChanged();
305
265
  }
306
266
  }
307
-
308
- // 4. Update hook call references (in case they're used as values)
309
- Object.entries(hookRenamingMap).forEach(([oldName, newName]) => {
310
- root.find(j.Identifier).forEach((path: any) => {
311
- if (path.value.name === oldName) {
312
- // Skip if already handled in imports
313
- if (j.ImportSpecifier.check(path.parent.value)) {
314
- return;
315
- }
316
-
317
- // This might be a reference to the hook as a value
318
- path.value.name = newName;
319
- markAsChanged();
320
- }
321
- });
322
- });
323
-
324
- // 5. Update JSX component names
325
- Object.entries(componentRenamingMap).forEach(([oldName, newName]) => {
326
- // Update JSX opening elements
327
- root.find(j.JSXOpeningElement).forEach((path: any) => {
328
- if (
329
- j.JSXIdentifier.check(path.value.name) &&
330
- path.value.name.name === oldName
331
- ) {
332
- path.value.name.name = newName;
333
- markAsChanged();
334
- }
335
- });
336
-
337
- // Update JSX closing elements
338
- root.find(j.JSXClosingElement).forEach((path: any) => {
339
- if (
340
- j.JSXIdentifier.check(path.value.name) &&
341
- path.value.name.name === oldName
342
- ) {
343
- path.value.name.name = newName;
344
- markAsChanged();
345
- }
346
- });
347
-
348
- // Update regular identifier references (for component references)
349
- root.find(j.Identifier).forEach((path: any) => {
350
- if (path.value.name === oldName) {
351
- // Skip if already handled in imports
352
- if (j.ImportSpecifier.check(path.parent.value)) {
353
- return;
354
- }
355
-
356
- // Skip JSX identifiers (already handled above)
357
- if (j.JSXIdentifier.check(path.value)) {
358
- return;
359
- }
360
-
361
- // This might be a reference to the component as a value
362
- path.value.name = newName;
363
- markAsChanged();
364
- }
365
- });
366
- });
367
267
  },
368
268
  );
369
269
 
@@ -21,6 +21,86 @@ function applyTransform(source: string): string | null {
21
21
  }
22
22
 
23
23
  describe("aui-accessor-calls-to-properties", () => {
24
+ it("resolves parameter defaults outside the function body", () => {
25
+ const output = applyTransform(`const client = useAui();
26
+ function worker(value = client.thread()) { var client = other(); return client.thread(); }`);
27
+ expect(output).toContain("value = client.thread)");
28
+ expect(output).toContain("return client.thread();");
29
+ });
30
+
31
+ it("does not let a body client capture an unrelated parameter default", () => {
32
+ const output = applyTransform(`const client = other();
33
+ function worker(value = client.thread()) { var client = useAui(); return client.thread(); }`);
34
+ expect(output).toContain("value = client.thread())");
35
+ expect(output).toContain("return client.thread;");
36
+ });
37
+
38
+ it("preserves unrelated namespace bindings", () => {
39
+ const output = applyTransform(`const client = useAui();
40
+ namespace Local { export const client = other(); client.thread(); }
41
+ client.thread();`);
42
+ expect(output).toContain("export const client = other(); client.thread();");
43
+ expect(output).toContain("\nclient.thread;");
44
+ });
45
+
46
+ it.each([
47
+ "const read = ({ aui }) => aui.thread();",
48
+ "const read = (aui = fallback) => aui.thread();",
49
+ "class Reader { constructor(private aui: AssistantClient) { aui.thread(); } }",
50
+ ])("retains supported parameter wrappers: %s", (source) => {
51
+ expect(applyTransform(source)).toContain("aui.thread;");
52
+ });
53
+ it.each([
54
+ "function worker(client: { thread(): string }) { return client.thread(); }",
55
+ "function worker() { const client = other(); return client.thread(); }",
56
+ "{ const client = other(); client.thread(); }",
57
+ "{ const { client } = other(); client.thread(); }",
58
+ "for (const client of others) { client.thread(); }",
59
+ "for (const client of client.thread()) { client.thread(); }",
60
+ "for (let client of client.thread()) { client.thread(); }",
61
+ "for (const client in client.thread()) { client.thread(); }",
62
+ "for (let client in client.thread()) { client.thread(); }",
63
+ "try {} catch (client) { client.thread(); }",
64
+ "const worker = { run(client) { return client.thread(); } };",
65
+ "function worker() { if (ready) { var client = other(); } return client.thread(); }",
66
+ "switch (kind) { case 1: const client = other(); client.thread(); }",
67
+ ])("preserves a different binding: %s", (unrelated) => {
68
+ const output = applyTransform(
69
+ `const client = useAui();\n${unrelated}\nclient.thread();`,
70
+ );
71
+ expect(output).toContain(unrelated);
72
+ expect(output).toContain("\nclient.thread;");
73
+ });
74
+
75
+ it("does not treat a known unrelated aui binding as an implicit client", () => {
76
+ expect(applyTransform("const aui = other(); aui.thread();")).toBeNull();
77
+ expect(
78
+ applyTransform(
79
+ "function worker(aui: OtherClient) { return aui.thread(); }",
80
+ ),
81
+ ).toBeNull();
82
+ });
83
+
84
+ it("keeps references to a recognized client in nested closures", () => {
85
+ expect(
86
+ applyTransform(
87
+ "const client = useAui(); const read = () => client.thread();",
88
+ ),
89
+ ).toContain("() => client.thread;");
90
+ });
91
+
92
+ it("recognizes an aliased assistant hook without matching a foreign hook", () => {
93
+ const input = `import { useAui as useClient } from "@assistant-ui/react";
94
+ import { useAui } from "./other";
95
+ const client = useClient();
96
+ const other = useAui();
97
+ client.thread();
98
+ other.thread();`;
99
+ const output = applyTransform(input);
100
+ expect(output).toContain("client.thread;");
101
+ expect(output).toContain("other.thread();");
102
+ });
103
+
24
104
  it("rewrites nullary accessor calls on a useAui variable", () => {
25
105
  const input = `
26
106
  const client = useAui();
@@ -33,6 +113,27 @@ client.thread.cancelRun();
33
113
  const state = client.composer.getState();
34
114
  `;
35
115
  expect(applyTransform(input)?.trim()).toBe(expected.trim());
116
+ expect(applyTransform(expected)).toBeNull();
117
+ });
118
+
119
+ it("resolves the switch discriminant outside the case declarations", () => {
120
+ const output = applyTransform(`const client = useAui();
121
+ switch (client.thread()) { case 1: const client = other(); client.thread(); }`);
122
+ expect(output).toContain("switch (client.thread)");
123
+ expect(output).toContain("const client = other(); client.thread();");
124
+ });
125
+
126
+ it("migrates typed method parameters without touching another method", () => {
127
+ const output = applyTransform(`const readers = {
128
+ chat(client: AssistantClient) { return client.thread(); },
129
+ other(client: Other) { return client.thread(); },
130
+ };`);
131
+ expect(output).toContain(
132
+ "chat(client: AssistantClient) { return client.thread; }",
133
+ );
134
+ expect(output).toContain(
135
+ "other(client: Other) { return client.thread(); }",
136
+ );
36
137
  });
37
138
 
38
139
  it("rewrites only the accessor call in chained expressions", () => {
@@ -1,4 +1,5 @@
1
1
  import { createTransformer } from "../utils/createTransformer";
2
+ import { resolveBinding } from "../utils/resolveBinding";
2
3
 
3
4
  // Nullary scope accessors that became properties in v0.15. Parameterized
4
5
  // lookups (e.g. `aui.thread.message({ id })`) stay as real calls.
@@ -28,7 +29,23 @@ const AUI_HOOKS = new Set(["useAui", "useAssistantApi"]);
28
29
 
29
30
  const auiAccessorCallsToProperties = createTransformer(
30
31
  ({ j, root, markAsChanged }) => {
31
- const auiNames = new Set(["aui"]);
32
+ const auiBindings = new Set<any>();
33
+ const hookBindings = new Set<any>();
34
+ const hookNames = new Set(AUI_HOOKS);
35
+ root.find(j.ImportDeclaration).forEach((path) => {
36
+ if (!String(path.value.source.value).startsWith("@assistant-ui/")) return;
37
+ for (const specifier of path.value.specifiers ?? []) {
38
+ if (
39
+ j.ImportSpecifier.check(specifier) &&
40
+ j.Identifier.check(specifier.imported) &&
41
+ AUI_HOOKS.has(specifier.imported.name)
42
+ ) {
43
+ hookBindings.add(specifier.local);
44
+ if (j.Identifier.check(specifier.local))
45
+ hookNames.add(specifier.local.name);
46
+ }
47
+ }
48
+ });
32
49
 
33
50
  root.find(j.VariableDeclarator).forEach((path: any) => {
34
51
  const { id, init } = path.value;
@@ -37,13 +54,37 @@ const auiAccessorCallsToProperties = createTransformer(
37
54
  init &&
38
55
  j.CallExpression.check(init) &&
39
56
  j.Identifier.check(init.callee) &&
40
- AUI_HOOKS.has(init.callee.name)
57
+ hookNames.has(init.callee.name) &&
58
+ (() => {
59
+ const binding = resolveBinding(
60
+ j,
61
+ path.get("init", "callee"),
62
+ init.callee.name,
63
+ );
64
+ return binding
65
+ ? hookBindings.has(binding)
66
+ : AUI_HOOKS.has(init.callee.name);
67
+ })()
41
68
  ) {
42
- auiNames.add(id.name);
69
+ auiBindings.add(id);
43
70
  }
44
71
  });
45
72
 
46
73
  const collectParam = (param: any) => {
74
+ if (param?.type === "TSParameterProperty")
75
+ return collectParam(param.parameter);
76
+ if (j.AssignmentPattern.check(param)) return collectParam(param.left);
77
+ if (j.RestElement.check(param)) return collectParam(param.argument);
78
+ if (j.ObjectPattern.check(param)) {
79
+ param.properties.forEach((p: any) =>
80
+ collectParam(p.value ?? p.argument),
81
+ );
82
+ return;
83
+ }
84
+ if (j.ArrayPattern.check(param)) {
85
+ param.elements.forEach(collectParam);
86
+ return;
87
+ }
47
88
  const annotation = param?.typeAnnotation?.typeAnnotation;
48
89
  if (
49
90
  j.Identifier.check(param) &&
@@ -52,13 +93,21 @@ const auiAccessorCallsToProperties = createTransformer(
52
93
  j.Identifier.check(annotation.typeName) &&
53
94
  annotation.typeName.name === "AssistantClient"
54
95
  ) {
55
- auiNames.add(param.name);
96
+ auiBindings.add(param);
97
+ } else if (
98
+ j.Identifier.check(param) &&
99
+ param.name === "aui" &&
100
+ !annotation
101
+ ) {
102
+ auiBindings.add(param);
56
103
  }
57
104
  };
58
105
  for (const fnType of [
59
106
  j.FunctionDeclaration,
60
107
  j.FunctionExpression,
61
108
  j.ArrowFunctionExpression,
109
+ j.ObjectMethod,
110
+ j.ClassMethod,
62
111
  ] as const) {
63
112
  root.find(fnType as typeof j.FunctionDeclaration).forEach((path: any) => {
64
113
  path.value.params.forEach(collectParam);
@@ -73,7 +122,13 @@ const auiAccessorCallsToProperties = createTransformer(
73
122
  if (!j.Identifier.check(callee.property)) return;
74
123
  if (!NULLARY_SCOPES.has(callee.property.name)) return;
75
124
  if (!j.Identifier.check(callee.object)) return;
76
- if (!auiNames.has(callee.object.name)) return;
125
+ const binding = resolveBinding(
126
+ j,
127
+ path.get("callee", "object"),
128
+ callee.object.name,
129
+ );
130
+ if (binding ? !auiBindings.has(binding) : callee.object.name !== "aui")
131
+ return;
77
132
  j(path).replaceWith(callee);
78
133
  markAsChanged();
79
134
  });
@@ -1,24 +1,16 @@
1
1
  import { Command } from "commander";
2
- import { resolve, dirname } from "node:path";
3
- import { existsSync } from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
2
  import { launch } from "@assistant-ui/agent-launcher";
3
+ import { ensureSkillsPlugin, skillsPluginDir } from "../lib/agent-skill";
4
+ import { logger } from "../lib/utils/logger";
6
5
 
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
-
9
- function getPluginPath(): string {
10
- // In dist/, plugin is at ../../plugin relative to dist/commands/agent.js
11
- // In dev (src/), plugin is at ../../plugin relative to src/commands/
12
- const candidates = [
13
- resolve(__dirname, "..", "..", "plugin"),
14
- resolve(__dirname, "..", "plugin"),
15
- ];
16
- for (const candidate of candidates) {
17
- if (existsSync(candidate)) return candidate;
6
+ async function resolvePluginDir(dry: boolean): Promise<string> {
7
+ if (dry) return skillsPluginDir();
8
+ try {
9
+ return await ensureSkillsPlugin();
10
+ } catch (error) {
11
+ logger.error(error instanceof Error ? error.message : String(error));
12
+ process.exit(1);
18
13
  }
19
- throw new Error(
20
- `Could not locate the assistant-ui plugin directory. Checked:\n${candidates.map((c) => ` ${c}`).join("\n")}`,
21
- );
22
14
  }
23
15
 
24
16
  export const agent = new Command()
@@ -26,13 +18,14 @@ export const agent = new Command()
26
18
  .description("launch Claude Code with assistant-ui skills")
27
19
  .argument("<prompt...>", "prompt for the agent")
28
20
  .option("--dry", "print the command instead of running it")
29
- .action((promptParts: string[], opts) => {
30
- const prompt = promptParts.join(" ");
21
+ .action(async (promptParts: string[], opts: { dry?: boolean }) => {
22
+ const dry = opts.dry === true;
23
+ const pluginDir = await resolvePluginDir(dry);
31
24
 
32
25
  launch({
33
- pluginDir: getPluginPath(),
26
+ pluginDir,
34
27
  skillName: "assistant-ui",
35
- prompt,
36
- dry: opts.dry,
28
+ prompt: promptParts.join(" "),
29
+ dry,
37
30
  });
38
31
  });