@agentuity/cli 1.0.20 → 1.0.22

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 (44) hide show
  1. package/dist/cmd/build/vite/static-renderer.d.ts +27 -0
  2. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -0
  3. package/dist/cmd/build/vite/static-renderer.js +182 -0
  4. package/dist/cmd/build/vite/static-renderer.js.map +1 -0
  5. package/dist/cmd/build/vite/vite-builder.d.ts +5 -0
  6. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  7. package/dist/cmd/build/vite/vite-builder.js +16 -0
  8. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  9. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  10. package/dist/cmd/build/vite-bundler.js +3 -0
  11. package/dist/cmd/build/vite-bundler.js.map +1 -1
  12. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  13. package/dist/cmd/cloud/deploy.js +1 -1
  14. package/dist/cmd/cloud/deploy.js.map +1 -1
  15. package/dist/cmd/project/add/domain.d.ts.map +1 -1
  16. package/dist/cmd/project/add/domain.js +6 -4
  17. package/dist/cmd/project/add/domain.js.map +1 -1
  18. package/dist/cmd/project/domain/check.d.ts.map +1 -1
  19. package/dist/cmd/project/domain/check.js +10 -5
  20. package/dist/cmd/project/domain/check.js.map +1 -1
  21. package/dist/cmd/project/reconcile.d.ts.map +1 -1
  22. package/dist/cmd/project/reconcile.js +19 -7
  23. package/dist/cmd/project/reconcile.js.map +1 -1
  24. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  25. package/dist/cmd/project/template-flow.js +2 -1
  26. package/dist/cmd/project/template-flow.js.map +1 -1
  27. package/dist/domain.d.ts +3 -2
  28. package/dist/domain.d.ts.map +1 -1
  29. package/dist/domain.js +90 -21
  30. package/dist/domain.js.map +1 -1
  31. package/dist/types.d.ts +7 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/types.js.map +1 -1
  34. package/package.json +6 -6
  35. package/src/cmd/build/vite/static-renderer.ts +236 -0
  36. package/src/cmd/build/vite/vite-builder.ts +18 -0
  37. package/src/cmd/build/vite-bundler.ts +7 -0
  38. package/src/cmd/cloud/deploy.ts +1 -0
  39. package/src/cmd/project/add/domain.ts +10 -4
  40. package/src/cmd/project/domain/check.ts +16 -5
  41. package/src/cmd/project/reconcile.ts +31 -13
  42. package/src/cmd/project/template-flow.ts +2 -1
  43. package/src/domain.ts +99 -21
  44. package/src/types.ts +8 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Static Renderer
3
+ *
4
+ * When `render: 'static'` is set in agentuity.config.ts, this module:
5
+ * 1. Runs a Vite SSR build to create a server-side entry point
6
+ * 2. Imports the built entry-server.js
7
+ * 3. Discovers routes to pre-render:
8
+ * - If `routeTree` is exported: auto-discovers all non-parameterized routes
9
+ * - If `getStaticPaths()` is exported: merges those paths in (for parameterized routes)
10
+ * - If neither: throws an error
11
+ * 4. Calls render(url) for each route
12
+ * 5. Replaces <!--app-html--> in the client template with rendered HTML
13
+ * 6. Writes pre-rendered HTML files to .agentuity/client/
14
+ */
15
+
16
+ import { join } from 'node:path';
17
+ import { createRequire } from 'node:module';
18
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
19
+ import type { Logger } from '../../../types';
20
+ import { hasFrameworkPlugin } from './config-loader';
21
+
22
+ /** Minimal shape of a TanStack Router route tree node. */
23
+ interface RouteTreeNode {
24
+ path?: string;
25
+ options?: { path?: string };
26
+ children?: Record<string, RouteTreeNode>;
27
+ }
28
+
29
+ /**
30
+ * Walks a TanStack Router route tree and extracts all non-parameterized paths.
31
+ * Skips layout routes (no path) and parameterized routes (containing $).
32
+ */
33
+ function extractRoutePaths(node: RouteTreeNode): string[] {
34
+ const paths = new Set<string>();
35
+
36
+ function walk(route: RouteTreeNode) {
37
+ const path: string | undefined = route.path ?? route.options?.path;
38
+ if (path && !path.includes('$')) {
39
+ // Normalize: strip trailing slashes, ensure leading slash
40
+ const normalized = path === '/' ? '/' : path.replace(/\/+$/, '');
41
+ if (normalized) {
42
+ paths.add(normalized);
43
+ }
44
+ }
45
+
46
+ // Recurse into children (TanStack Router stores them as an object)
47
+ const children = route.children;
48
+ if (children && typeof children === 'object') {
49
+ for (const child of Object.values(children)) {
50
+ if (child) walk(child);
51
+ }
52
+ }
53
+ }
54
+
55
+ walk(node);
56
+ return [...paths].sort();
57
+ }
58
+
59
+ export interface StaticRenderOptions {
60
+ rootDir: string;
61
+ logger: Logger;
62
+ /** User plugins from agentuity.config.ts */
63
+ userPlugins: import('vite').PluginOption[];
64
+ }
65
+
66
+ export interface StaticRenderResult {
67
+ routes: number;
68
+ duration: number;
69
+ }
70
+
71
+ export async function runStaticRender(options: StaticRenderOptions): Promise<StaticRenderResult> {
72
+ const { rootDir, logger, userPlugins } = options;
73
+ const started = Date.now();
74
+
75
+ const clientDir = join(rootDir, '.agentuity/client');
76
+ const ssrOutDir = join(rootDir, '.agentuity/ssr');
77
+ const entryServerPath = join(rootDir, 'src/web/entry-server.tsx');
78
+ const templatePath = join(clientDir, 'index.html');
79
+
80
+ // Verify prerequisites
81
+ if (!existsSync(entryServerPath)) {
82
+ throw new Error(
83
+ 'Static rendering requires src/web/entry-server.tsx. ' +
84
+ 'This file must export a render(url: string) function and either ' +
85
+ 'a routeTree for auto-discovery or getStaticPaths() for explicit paths.'
86
+ );
87
+ }
88
+
89
+ if (!existsSync(templatePath)) {
90
+ throw new Error(
91
+ 'Client build must complete before static rendering. ' +
92
+ 'No index.html found in .agentuity/client/'
93
+ );
94
+ }
95
+
96
+ // Step 1: Vite SSR build
97
+ // This resolves import.meta.glob, MDX imports, and other Vite-specific APIs
98
+ logger.debug('Running Vite SSR build for static rendering...');
99
+
100
+ const projectRequire = createRequire(join(rootDir, 'package.json'));
101
+ let vitePath = 'vite';
102
+ let reactPluginPath = '@vitejs/plugin-react';
103
+ try {
104
+ vitePath = projectRequire.resolve('vite');
105
+ reactPluginPath = projectRequire.resolve('@vitejs/plugin-react');
106
+ } catch {
107
+ // Use CLI's bundled version
108
+ }
109
+
110
+ const { build: viteBuild } = await import(vitePath);
111
+ const reactModule = await import(reactPluginPath);
112
+ const react = reactModule.default;
113
+
114
+ // Build plugin list: auto-add React if no framework plugin present
115
+ const plugins = [...(userPlugins || [])];
116
+ if (plugins.length === 0 || !hasFrameworkPlugin(plugins)) {
117
+ plugins.unshift(react());
118
+ }
119
+
120
+ await viteBuild({
121
+ root: rootDir,
122
+ plugins,
123
+ build: {
124
+ ssr: entryServerPath,
125
+ outDir: ssrOutDir,
126
+ rollupOptions: {
127
+ output: {
128
+ format: 'esm',
129
+ },
130
+ },
131
+ },
132
+ ssr: {
133
+ // Bundle all dependencies for SSR — we need import.meta.glob, MDX, etc.
134
+ // resolved at build time. Node built-ins are still externalized.
135
+ noExternal: true,
136
+ },
137
+ logLevel: 'warn',
138
+ });
139
+
140
+ // Steps 2–4: wrapped in try-finally so SSR artifacts are always cleaned up,
141
+ // even if an exception is thrown during module import, validation, or rendering.
142
+ let routeCount = 0;
143
+ try {
144
+ // Step 2: Import the built SSR entry
145
+ const ssrEntryPath = join(ssrOutDir, 'entry-server.js');
146
+ if (!existsSync(ssrEntryPath)) {
147
+ throw new Error(`SSR build did not produce entry-server.js at ${ssrEntryPath}`);
148
+ }
149
+
150
+ const ssrModule = await import(ssrEntryPath);
151
+
152
+ if (typeof ssrModule.render !== 'function') {
153
+ throw new Error(
154
+ 'entry-server.tsx must export a render(url: string) function that returns HTML string'
155
+ );
156
+ }
157
+
158
+ // Step 3: Discover routes
159
+ // Priority: auto-discover from routeTree + merge getStaticPaths() if present
160
+ const discovered = new Set<string>();
161
+
162
+ // 3a. Auto-discover from exported routeTree (skips parameterized routes)
163
+ if (ssrModule.routeTree) {
164
+ const autoRoutes = extractRoutePaths(ssrModule.routeTree);
165
+ for (const r of autoRoutes) {
166
+ discovered.add(r);
167
+ }
168
+ logger.debug(`Auto-discovered ${autoRoutes.length} routes from route tree`);
169
+ }
170
+
171
+ // 3b. Merge paths from getStaticPaths() if exported (for parameterized routes, etc.)
172
+ if (typeof ssrModule.getStaticPaths === 'function') {
173
+ const extraRoutes = await ssrModule.getStaticPaths();
174
+ if (!Array.isArray(extraRoutes)) {
175
+ throw new Error(
176
+ 'getStaticPaths() must return an array of URL paths (e.g., ["/", "/about"])'
177
+ );
178
+ }
179
+ for (const r of extraRoutes) {
180
+ discovered.add(r);
181
+ }
182
+ logger.debug(`getStaticPaths() added ${extraRoutes.length} paths`);
183
+ }
184
+
185
+ // Must have at least one source of routes
186
+ if (discovered.size === 0) {
187
+ throw new Error(
188
+ 'No routes to pre-render. Export routeTree from entry-server.tsx for auto-discovery, ' +
189
+ 'or export getStaticPaths() returning an array of URL paths.'
190
+ );
191
+ }
192
+
193
+ const routes: string[] = [...discovered].sort();
194
+ routeCount = routes.length;
195
+ logger.debug(`Total: ${routes.length} routes for pre-rendering`);
196
+
197
+ // Step 4: Read template and pre-render each route
198
+ const template = readFileSync(templatePath, 'utf-8');
199
+ if (!template.includes('<!--app-html-->')) {
200
+ logger.warn(
201
+ 'index.html is missing the <!--app-html--> placeholder; ' +
202
+ 'pre-rendered content will not be injected into the page.'
203
+ );
204
+ }
205
+
206
+ for (const route of routes) {
207
+ try {
208
+ const html = await ssrModule.render(route);
209
+ const page = template.replace('<!--app-html-->', html);
210
+
211
+ let outPath: string;
212
+ if (route === '/') {
213
+ outPath = join(clientDir, 'index.html');
214
+ } else {
215
+ const dir = join(clientDir, route.slice(1));
216
+ mkdirSync(dir, { recursive: true });
217
+ outPath = join(dir, 'index.html');
218
+ }
219
+
220
+ writeFileSync(outPath, page, 'utf-8');
221
+ } catch (err) {
222
+ const message = err instanceof Error ? err.message : String(err);
223
+ logger.warn(`Failed to render route ${route}: ${message}`);
224
+ // Continue rendering other routes
225
+ }
226
+ }
227
+ } finally {
228
+ // Step 5: Clean up SSR build artifacts (always runs, even on error)
229
+ rmSync(ssrOutDir, { recursive: true, force: true });
230
+ }
231
+
232
+ const duration = Date.now() - started;
233
+ logger.debug(`Static rendering complete: ${routeCount} routes in ${duration}ms`);
234
+
235
+ return { routes: routeCount, duration };
236
+ }
@@ -302,6 +302,7 @@ interface BuildResult {
302
302
  workbench: { included: boolean; duration: number };
303
303
  client: { included: boolean; duration: number };
304
304
  server: { included: boolean; duration: number };
305
+ static: { included: boolean; duration: number; routes: number };
305
306
  }
306
307
 
307
308
  /**
@@ -318,6 +319,7 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
318
319
  workbench: { included: false, duration: 0 },
319
320
  client: { included: false, duration: 0 },
320
321
  server: { included: false, duration: 0 },
322
+ static: { included: false, duration: 0, routes: 0 },
321
323
  };
322
324
 
323
325
  // Load config to check if workbench is enabled (dev mode only)
@@ -382,6 +384,22 @@ export async function runAllBuilds(options: Omit<ViteBuildOptions, 'mode'>): Pro
382
384
  logger.debug('Skipping client build - no src/web/index.html found');
383
385
  }
384
386
 
387
+ // 2b. Static rendering (if configured)
388
+ if (config?.render === 'static' && hasWebFrontend) {
389
+ logger.debug('Running static rendering (pre-rendering all routes)...');
390
+ const endStaticDiagnostic = collector?.startDiagnostic('static-render');
391
+ const { runStaticRender } = await import('./static-renderer');
392
+ const staticResult = await runStaticRender({
393
+ rootDir,
394
+ logger,
395
+ userPlugins: config?.plugins || [],
396
+ });
397
+ result.static.included = true;
398
+ result.static.duration = staticResult.duration;
399
+ result.static.routes = staticResult.routes;
400
+ endStaticDiagnostic?.();
401
+ }
402
+
385
403
  // 3. Build workbench (if enabled in config)
386
404
  if (workbenchConfig.enabled) {
387
405
  logger.debug('Building workbench assets...');
@@ -113,6 +113,13 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
113
113
  if (result.workbench.included) {
114
114
  output.push(tui.muted(`✓ Workbench built in ${result.workbench.duration}ms`));
115
115
  }
116
+ if (result.static.included) {
117
+ output.push(
118
+ tui.muted(
119
+ `✓ ${result.static.routes} routes pre-rendered in ${result.static.duration}ms`
120
+ )
121
+ );
122
+ }
116
123
  if (result.server.included) {
117
124
  output.push(tui.muted(`✓ Server built in ${result.server.duration}ms`));
118
125
  }
@@ -553,6 +553,7 @@ export const deploySubcommand = createSubcommand({
553
553
  await domain.promptForDNS(
554
554
  project.projectId,
555
555
  project.deployment.domains,
556
+ project.region,
556
557
  config!,
557
558
  () => pauseStepUI(true)
558
559
  );
@@ -95,7 +95,7 @@ export const domainSubcommand = createSubcommand({
95
95
  message: `Checking DNS for ${domain}`,
96
96
  clearOnSuccess: true,
97
97
  callback: async () => {
98
- return checkCustomDomainForDNS(project.projectId, [domain], config);
98
+ return checkCustomDomainForDNS(project.projectId, [domain], project.region, config);
99
99
  },
100
100
  });
101
101
 
@@ -117,11 +117,17 @@ export const domainSubcommand = createSubcommand({
117
117
  }
118
118
 
119
119
  tui.newline();
120
- tui.warning('DNS record not yet configured. Please add the following CNAME record:');
120
+ tui.warning(
121
+ 'DNS record not yet configured. Please add one of the following DNS records:'
122
+ );
121
123
  tui.newline();
122
124
  tui.output(` ${tui.colorInfo('Domain:')} ${tui.colorPrimary(result.domain)}`);
123
- tui.output(` ${tui.colorInfo('Type:')} ${tui.colorPrimary(result.recordType)}`);
124
- tui.output(` ${tui.colorInfo('Target:')} ${tui.colorPrimary(result.target)}`);
125
+ tui.output(` ${tui.colorInfo('CNAME:')} ${tui.colorPrimary(result.target)}`);
126
+ if (result.aRecordTarget) {
127
+ tui.output(
128
+ ` ${tui.colorInfo('A:')} ${tui.colorPrimary(result.aRecordTarget)}`
129
+ );
130
+ }
125
131
  tui.newline();
126
132
 
127
133
  if (isMisconfigured(result)) {
@@ -39,6 +39,7 @@ export const checkSubcommand = createSubcommand({
39
39
  domain: z.string(),
40
40
  recordType: z.string(),
41
41
  target: z.string(),
42
+ aRecordTarget: z.string().optional(),
42
43
  status: z.string(),
43
44
  success: z.boolean(),
44
45
  })
@@ -69,11 +70,17 @@ export const checkSubcommand = createSubcommand({
69
70
  }
70
71
 
71
72
  const results = jsonMode
72
- ? await checkCustomDomainForDNS(project.projectId, domainsToCheck, config)
73
+ ? await checkCustomDomainForDNS(project.projectId, domainsToCheck, project.region, config)
73
74
  : await tui.spinner({
74
75
  message: `Checking DNS for ${domainsToCheck.length} ${tui.plural(domainsToCheck.length, 'domain', 'domains')}`,
75
76
  clearOnSuccess: true,
76
- callback: () => checkCustomDomainForDNS(project.projectId, domainsToCheck, config),
77
+ callback: () =>
78
+ checkCustomDomainForDNS(
79
+ project.projectId,
80
+ domainsToCheck,
81
+ project.region,
82
+ config
83
+ ),
77
84
  });
78
85
 
79
86
  const domainResults = results.map((r) => {
@@ -106,6 +113,7 @@ export const checkSubcommand = createSubcommand({
106
113
  domain: r.domain,
107
114
  recordType: r.recordType,
108
115
  target: r.target,
116
+ aRecordTarget: r.aRecordTarget,
109
117
  status,
110
118
  statusRaw,
111
119
  success,
@@ -116,8 +124,10 @@ export const checkSubcommand = createSubcommand({
116
124
  tui.newline();
117
125
  for (const r of domainResults) {
118
126
  console.log(` ${tui.colorInfo('Domain:')} ${tui.colorPrimary(r.domain)}`);
119
- console.log(` ${tui.colorInfo('Type:')} ${tui.colorPrimary(r.recordType)}`);
120
- console.log(` ${tui.colorInfo('Target:')} ${tui.colorPrimary(r.target)}`);
127
+ console.log(` ${tui.colorInfo('CNAME:')} ${tui.colorPrimary(r.target)}`);
128
+ if (r.aRecordTarget) {
129
+ console.log(` ${tui.colorInfo('A:')} ${tui.colorPrimary(r.aRecordTarget)}`);
130
+ }
121
131
  console.log(` ${tui.colorInfo('Status:')} ${r.status}`);
122
132
  console.log();
123
133
  }
@@ -128,7 +138,7 @@ export const checkSubcommand = createSubcommand({
128
138
  } else {
129
139
  const failCount = domainResults.filter((r) => !r.success).length;
130
140
  tui.warning(
131
- `${failCount} ${tui.plural(failCount, 'domain has', 'domains have')} DNS issues — add a CNAME record pointing to the target shown above`
141
+ `${failCount} ${tui.plural(failCount, 'domain has', 'domains have')} DNS issues — add a CNAME or A record pointing to one of the targets shown above`
132
142
  );
133
143
  }
134
144
  }
@@ -138,6 +148,7 @@ export const checkSubcommand = createSubcommand({
138
148
  domain: r.domain,
139
149
  recordType: r.recordType,
140
150
  target: r.target,
151
+ aRecordTarget: r.aRecordTarget,
141
152
  status: r.statusRaw,
142
153
  success: r.success,
143
154
  })),
@@ -310,25 +310,28 @@ async function textPrompt(options: {
310
310
  async function importExistingProject(
311
311
  opts: ReconcileOptions,
312
312
  existingConfig: Project,
313
- orgs: OrganizationList
313
+ orgs: OrganizationList,
314
+ options?: { skipPrompt?: boolean }
314
315
  ): Promise<ReconcileResult> {
315
316
  const { dir, apiClient, config, logger } = opts;
316
317
 
317
- tui.warning(
318
- "You don't have access to this project. It may have been deleted or transferred to another organization."
319
- );
320
- tui.newline();
318
+ if (!options?.skipPrompt) {
319
+ tui.warning(
320
+ "You don't have access to this project. It may have been deleted or transferred to another organization."
321
+ );
322
+ tui.newline();
321
323
 
322
- const shouldImport = await tui.confirm(
323
- 'Would you like to import this project to your organization?',
324
- true
325
- );
324
+ const shouldImport = await tui.confirm(
325
+ 'Would you like to import this project to your organization?',
326
+ true
327
+ );
326
328
 
327
- if (!shouldImport) {
328
- return { status: 'skipped', message: 'Project import cancelled.' };
329
- }
329
+ if (!shouldImport) {
330
+ return { status: 'skipped', message: 'Project import cancelled.' };
331
+ }
330
332
 
331
- tui.newline();
333
+ tui.newline();
334
+ }
332
335
 
333
336
  // Select org
334
337
  const orgId = await selectOrg(orgs, config, existingConfig.orgId);
@@ -605,6 +608,21 @@ export async function runProjectImport(opts: ReconcileOptions): Promise<Reconcil
605
608
 
606
609
  if (hasAccess) {
607
610
  tui.info('This project is already registered and you have access to it.');
611
+
612
+ if (interactive) {
613
+ tui.newline();
614
+ const shouldReimport = await tui.confirm(
615
+ 'Would you like to import it to a different organization?',
616
+ false
617
+ );
618
+ if (shouldReimport) {
619
+ tui.newline();
620
+ return await importExistingProject(opts, projectConfig, userOrgs, {
621
+ skipPrompt: true,
622
+ });
623
+ }
624
+ }
625
+
608
626
  return { status: 'valid', project: projectConfig };
609
627
  }
610
628
 
@@ -768,7 +768,8 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<CreateF
768
768
  const ok = await tui.confirm('Would you like to configure DNS now?', true);
769
769
  if (ok) {
770
770
  tui.newline();
771
- await promptForDNS(projectId, _domains, config);
771
+ const cloudRegion = region ?? process.env.AGENTUITY_REGION ?? 'usc';
772
+ await promptForDNS(projectId, _domains, cloudRegion, config);
772
773
  }
773
774
  }
774
775