Skip to content

Commit 460272f

Browse files
authored
Add pnpm create-integration script (#513)
1 parent 30ead77 commit 460272f

File tree

3 files changed

+321
-1
lines changed

3 files changed

+321
-1
lines changed

typescript-sdk/apps/dojo/scripts/generate-content-json.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,10 @@ async function runGenerateContent() {
347347
// Use the parsed agent keys instead of executing the agents function
348348
const agentsPerFeatures = agentConfig.agentKeys;
349349

350-
const agentFilePaths = agentFilesMapper[agentConfig.id](agentConfig.agentKeys);
350+
const agentFilePaths = agentFilesMapper[agentConfig.id]?.(agentConfig.agentKeys);
351+
if (!agentFilePaths) {
352+
continue;
353+
}
351354

352355
// Per feature, assign all the frontend files like page.tsx as well as all agent files
353356
for (const featureId of agentsPerFeatures) {
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
#!/usr/bin/env node
2+
3+
const args = process.argv.slice(2);
4+
5+
const hasMiddleware = args.includes("--middleware");
6+
const hasServer = args.includes("--server");
7+
8+
function showHelp() {
9+
console.log(`
10+
Usage: pnpm create-integration [OPTIONS] <integration-name>
11+
12+
Create a new AG-UI integration
13+
14+
OPTIONS:
15+
--middleware Create a middleware-based integration
16+
--server Create a server-based integration
17+
18+
ARGUMENTS:
19+
<integration-name> Name of the integration in kebab-case (e.g., my-integration)
20+
21+
EXAMPLES:
22+
pnpm create-integration --middleware my-integration
23+
pnpm create-integration --server my-api-integration
24+
`);
25+
}
26+
27+
if (!hasMiddleware && !hasServer) {
28+
showHelp();
29+
process.exit(1);
30+
}
31+
32+
if (hasMiddleware && hasServer) {
33+
console.error("Error: Cannot specify both --middleware and --server");
34+
showHelp();
35+
process.exit(1);
36+
}
37+
38+
// Get the integration name (should be after the flag)
39+
const integrationName = args.find((arg) => !arg.startsWith("--"));
40+
41+
if (!integrationName) {
42+
console.error("Error: Integration name is required");
43+
showHelp();
44+
process.exit(1);
45+
}
46+
47+
// Validate kebab-case format: lowercase letters, numbers, and hyphens only
48+
// Must start with a letter, cannot start or end with hyphen, no consecutive hyphens
49+
const kebabCaseRegex = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
50+
51+
if (!kebabCaseRegex.test(integrationName)) {
52+
console.error(`Error: Integration name "${integrationName}" is not in valid kebab-case format`);
53+
console.error("Valid kebab-case examples: my-integration, api-client, my-api-123");
54+
showHelp();
55+
process.exit(1);
56+
}
57+
58+
if (hasMiddleware) {
59+
console.log(`Creating middleware-based integration: ${integrationName}`);
60+
61+
const { execSync } = require("child_process");
62+
const path = require("path");
63+
const fs = require("fs");
64+
65+
const integrationsDir = path.join(__dirname, "integrations");
66+
const sourceDir = path.join(integrationsDir, "middleware-starter");
67+
const targetDir = path.join(integrationsDir, integrationName);
68+
69+
// Check if source directory exists
70+
if (!fs.existsSync(sourceDir)) {
71+
console.error(`Error: Template directory not found: ${sourceDir}`);
72+
process.exit(1);
73+
}
74+
75+
// Check if target directory already exists
76+
if (fs.existsSync(targetDir)) {
77+
console.error(`Error: Integration directory already exists: ${targetDir}`);
78+
process.exit(1);
79+
}
80+
81+
try {
82+
console.log(
83+
`Copying template from integrations/middleware-starter to integrations/${integrationName}...`,
84+
);
85+
execSync(`cp -r "${sourceDir}" "${targetDir}"`, { stdio: "inherit" });
86+
console.log(`✓ Created integration at integrations/${integrationName}`);
87+
88+
// Update package.json
89+
const packageJsonPath = path.join(targetDir, "package.json");
90+
console.log(`Updating package.json...`);
91+
92+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
93+
packageJson.name = `@ag-ui/${integrationName}`;
94+
95+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
96+
console.log(`✓ Updated package name to @ag-ui/${integrationName}`);
97+
98+
// Convert kebab-case to PascalCase
99+
const pascalCaseName = integrationName
100+
.split("-")
101+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
102+
.join("");
103+
const agentClassName = `${pascalCaseName}Agent`;
104+
105+
// Update src/index.ts
106+
const indexPath = path.join(targetDir, "src", "index.ts");
107+
console.log(`Updating src/index.ts...`);
108+
109+
let indexContent = fs.readFileSync(indexPath, "utf8");
110+
indexContent = indexContent.replace(
111+
/MiddlewareStarterAgent/g,
112+
agentClassName
113+
);
114+
115+
fs.writeFileSync(indexPath, indexContent);
116+
console.log(`✓ Updated class name to ${agentClassName}`);
117+
118+
// Update apps/dojo/src/menu.ts
119+
const menuPath = path.join(__dirname, "apps", "dojo", "src", "menu.ts");
120+
console.log(`Updating apps/dojo/src/menu.ts...`);
121+
122+
let menuContent = fs.readFileSync(menuPath, "utf8");
123+
124+
const newIntegration = ` {
125+
id: "${integrationName}",
126+
name: "${pascalCaseName}",
127+
features: ["agentic_chat"],
128+
},\n`;
129+
130+
// Find the menuIntegrations array and prepend the new integration
131+
menuContent = menuContent.replace(
132+
/(export const menuIntegrations: MenuIntegrationConfig\[\] = \[\n)/,
133+
`$1${newIntegration}`
134+
);
135+
136+
fs.writeFileSync(menuPath, menuContent);
137+
console.log(`✓ Registered integration in dojo menu`);
138+
139+
// Update apps/dojo/src/agents.ts
140+
const agentsPath = path.join(__dirname, "apps", "dojo", "src", "agents.ts");
141+
console.log(`Updating apps/dojo/src/agents.ts...`);
142+
143+
let agentsContent = fs.readFileSync(agentsPath, "utf8");
144+
145+
// Add import statement at the top
146+
const importStatement = `import { ${agentClassName} } from "@ag-ui/${integrationName}";\n`;
147+
agentsContent = importStatement + agentsContent;
148+
149+
const newAgentIntegration = ` {
150+
id: "${integrationName}",
151+
agents: async () => {
152+
return {
153+
agentic_chat: new ${agentClassName}(),
154+
}
155+
},
156+
},\n`;
157+
158+
// Find the agentsIntegrations array and prepend the new integration
159+
agentsContent = agentsContent.replace(
160+
/(export const agentsIntegrations: AgentIntegrationConfig\[\] = \[\n)/,
161+
`$1${newAgentIntegration}`
162+
);
163+
164+
fs.writeFileSync(agentsPath, agentsContent);
165+
console.log(`✓ Registered agent in dojo agents`);
166+
167+
// Update apps/dojo/package.json
168+
const dojoPackageJsonPath = path.join(__dirname, "apps", "dojo", "package.json");
169+
console.log(`Updating apps/dojo/package.json...`);
170+
171+
const dojoPackageJson = JSON.parse(fs.readFileSync(dojoPackageJsonPath, "utf8"));
172+
173+
// Add the new integration as a dependency at the beginning
174+
const newDependencies = {
175+
[`@ag-ui/${integrationName}`]: "workspace:*",
176+
...dojoPackageJson.dependencies
177+
};
178+
dojoPackageJson.dependencies = newDependencies;
179+
180+
fs.writeFileSync(dojoPackageJsonPath, JSON.stringify(dojoPackageJson, null, 2) + "\n");
181+
console.log(`✓ Added @ag-ui/${integrationName} to dojo dependencies`);
182+
} catch (error) {
183+
console.error("Error creating integration:", error);
184+
process.exit(1);
185+
}
186+
}
187+
188+
if (hasServer) {
189+
console.log(`Creating server-based integration: ${integrationName}`);
190+
191+
const { execSync } = require("child_process");
192+
const path = require("path");
193+
const fs = require("fs");
194+
195+
const integrationsDir = path.join(__dirname, "integrations");
196+
const sourceDir = path.join(integrationsDir, "server-starter");
197+
const targetDir = path.join(integrationsDir, integrationName);
198+
199+
// Check if source directory exists
200+
if (!fs.existsSync(sourceDir)) {
201+
console.error(`Error: Template directory not found: ${sourceDir}`);
202+
process.exit(1);
203+
}
204+
205+
// Check if target directory already exists
206+
if (fs.existsSync(targetDir)) {
207+
console.error(`Error: Integration directory already exists: ${targetDir}`);
208+
process.exit(1);
209+
}
210+
211+
try {
212+
console.log(
213+
`Copying template from integrations/server-starter to integrations/${integrationName}...`,
214+
);
215+
execSync(`cp -r "${sourceDir}" "${targetDir}"`, { stdio: "inherit" });
216+
console.log(`✓ Created integration at integrations/${integrationName}`);
217+
218+
// Update package.json
219+
const packageJsonPath = path.join(targetDir, "package.json");
220+
console.log(`Updating package.json...`);
221+
222+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
223+
packageJson.name = `@ag-ui/${integrationName}`;
224+
225+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
226+
console.log(`✓ Updated package name to @ag-ui/${integrationName}`);
227+
228+
// Convert kebab-case to PascalCase
229+
const pascalCaseName = integrationName
230+
.split("-")
231+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
232+
.join("");
233+
const agentClassName = `${pascalCaseName}Agent`;
234+
235+
// Update src/index.ts
236+
const indexPath = path.join(targetDir, "src", "index.ts");
237+
console.log(`Updating src/index.ts...`);
238+
239+
let indexContent = fs.readFileSync(indexPath, "utf8");
240+
indexContent = indexContent.replace(
241+
/ServerStarterAgent/g,
242+
agentClassName
243+
);
244+
245+
fs.writeFileSync(indexPath, indexContent);
246+
console.log(`✓ Updated class name to ${agentClassName}`);
247+
248+
// Update apps/dojo/src/menu.ts
249+
const menuPath = path.join(__dirname, "apps", "dojo", "src", "menu.ts");
250+
console.log(`Updating apps/dojo/src/menu.ts...`);
251+
252+
let menuContent = fs.readFileSync(menuPath, "utf8");
253+
254+
const newIntegration = ` {
255+
id: "${integrationName}",
256+
name: "${pascalCaseName}",
257+
features: ["agentic_chat"],
258+
},\n`;
259+
260+
// Find the menuIntegrations array and prepend the new integration
261+
menuContent = menuContent.replace(
262+
/(export const menuIntegrations: MenuIntegrationConfig\[\] = \[\n)/,
263+
`$1${newIntegration}`
264+
);
265+
266+
fs.writeFileSync(menuPath, menuContent);
267+
console.log(`✓ Registered integration in dojo menu`);
268+
269+
// Update apps/dojo/src/agents.ts
270+
const agentsPath = path.join(__dirname, "apps", "dojo", "src", "agents.ts");
271+
console.log(`Updating apps/dojo/src/agents.ts...`);
272+
273+
let agentsContent = fs.readFileSync(agentsPath, "utf8");
274+
275+
// Add import statement at the top
276+
const importStatement = `import { ${agentClassName} } from "@ag-ui/${integrationName}";\n`;
277+
agentsContent = importStatement + agentsContent;
278+
279+
const newAgentIntegration = ` {
280+
id: "${integrationName}",
281+
agents: async () => {
282+
return {
283+
agentic_chat: new ${agentClassName}({ url: "http://localhost:8000" }),
284+
}
285+
},
286+
},\n`;
287+
288+
// Find the agentsIntegrations array and prepend the new integration
289+
agentsContent = agentsContent.replace(
290+
/(export const agentsIntegrations: AgentIntegrationConfig\[\] = \[\n)/,
291+
`$1${newAgentIntegration}`
292+
);
293+
294+
fs.writeFileSync(agentsPath, agentsContent);
295+
console.log(`✓ Registered agent in dojo agents`);
296+
297+
// Update apps/dojo/package.json
298+
const dojoPackageJsonPath = path.join(__dirname, "apps", "dojo", "package.json");
299+
console.log(`Updating apps/dojo/package.json...`);
300+
301+
const dojoPackageJson = JSON.parse(fs.readFileSync(dojoPackageJsonPath, "utf8"));
302+
303+
// Add the new integration as a dependency at the beginning
304+
const newDependencies = {
305+
[`@ag-ui/${integrationName}`]: "workspace:*",
306+
...dojoPackageJson.dependencies
307+
};
308+
dojoPackageJson.dependencies = newDependencies;
309+
310+
fs.writeFileSync(dojoPackageJsonPath, JSON.stringify(dojoPackageJson, null, 2) + "\n");
311+
console.log(`✓ Added @ag-ui/${integrationName} to dojo dependencies`);
312+
} catch (error) {
313+
console.error("Error creating integration:", error);
314+
process.exit(1);
315+
}
316+
}

typescript-sdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"format": "prettier --write \"**/*.{ts,tsx,md,mdx}\"",
1313
"check-types": "turbo run check-types",
1414
"test": "turbo run test",
15+
"create-integration": "pnpm dlx tsx create-integration.ts",
1516
"bump": "pnpm --filter './packages/*' exec -- pnpm version",
1617
"bump:alpha": "pnpm --filter './packages/*' exec -- pnpm version --preid alpha",
1718
"publish": "pnpm -r clean && pnpm install && turbo run build && pnpm publish -r --filter='./packages/*'",

0 commit comments

Comments
 (0)