@mars-stack/cli 3.0.2 → 4.0.0

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.
package/dist/index.js CHANGED
@@ -2808,6 +2808,8 @@ async function generateOnboarding(projectRoot) {
2808
2808
  }
2809
2809
  await addUserRelation(projectRoot, "onboardingProgress OnboardingProgress?", ctx);
2810
2810
  await registerRoute(projectRoot, "onboarding", "/onboarding", ctx);
2811
+ await patchDashboardWithOnboardingRedirect(projectRoot, ctx);
2812
+ await patchProxyWithOnboardingRoute(projectRoot, ctx);
2811
2813
  await setConfigFlag6(projectRoot, ctx);
2812
2814
  await ctx.commit();
2813
2815
  log.success(`Generated onboarding feature with ${count} files`);
@@ -2816,10 +2818,11 @@ async function generateOnboarding(projectRoot) {
2816
2818
  log.step("src/app/(protected)/onboarding/ \u2014 onboarding page");
2817
2819
  log.step("src/app/api/protected/onboarding/ \u2014 progress and step API routes");
2818
2820
  log.step("prisma/schema/onboarding.prisma \u2014 OnboardingProgress model");
2821
+ log.step("src/app/(protected)/dashboard/page.tsx \u2014 onboarding redirect for new users");
2822
+ log.step("src/proxy.ts \u2014 /onboarding added to protected routes");
2819
2823
  log.blank();
2820
2824
  log.warn("Next steps:");
2821
2825
  log.step("Run `yarn db:push` to sync the Prisma schema");
2822
- log.step("Add onboarding redirect check to your protected layout");
2823
2826
  log.step("Customize steps in `src/features/onboarding/config.ts`");
2824
2827
  log.blank();
2825
2828
  } catch (error) {
@@ -2827,6 +2830,66 @@ async function generateOnboarding(projectRoot) {
2827
2830
  throw error;
2828
2831
  }
2829
2832
  }
2833
+ async function patchDashboardWithOnboardingRedirect(projectRoot, ctx) {
2834
+ const dashboardPath = path13.join(
2835
+ projectRoot,
2836
+ "src",
2837
+ "app",
2838
+ "(protected)",
2839
+ "dashboard",
2840
+ "page.tsx"
2841
+ );
2842
+ if (!await fs13.pathExists(dashboardPath)) return;
2843
+ await ctx.trackModifiedFile(dashboardPath);
2844
+ let content = await fs13.readFile(dashboardPath, "utf-8");
2845
+ if (content.includes("isOnboardingComplete")) return;
2846
+ const redirectImport = `import { redirect } from 'next/navigation';
2847
+ `;
2848
+ const onboardingImport = `import { isOnboardingComplete } from '@/features/onboarding/server';
2849
+ `;
2850
+ if (!content.includes("from 'next/navigation'")) {
2851
+ const firstImport = content.indexOf("import ");
2852
+ if (firstImport >= 0) {
2853
+ content = content.slice(0, firstImport) + redirectImport + content.slice(firstImport);
2854
+ }
2855
+ }
2856
+ const configImportIdx = content.indexOf("from '@/config/app.config'");
2857
+ if (configImportIdx >= 0) {
2858
+ const lineEnd = content.indexOf("\n", configImportIdx);
2859
+ content = content.slice(0, lineEnd + 1) + onboardingImport + content.slice(lineEnd + 1);
2860
+ } else {
2861
+ const firstImport = content.indexOf("import ");
2862
+ if (firstImport >= 0) {
2863
+ content = content.slice(0, firstImport) + onboardingImport + content.slice(firstImport);
2864
+ }
2865
+ }
2866
+ const sessionLine = content.match(/const session = await verifySession\(\);?\n/);
2867
+ if (sessionLine && sessionLine.index !== void 0) {
2868
+ const insertAt = sessionLine.index + sessionLine[0].length;
2869
+ const redirectBlock = `
2870
+ if (appConfig.features.onboarding) {
2871
+ const complete = await isOnboardingComplete(session.userId);
2872
+ if (!complete) {
2873
+ redirect('/onboarding');
2874
+ }
2875
+ }
2876
+ `;
2877
+ content = content.slice(0, insertAt) + redirectBlock + content.slice(insertAt);
2878
+ }
2879
+ await fs13.writeFile(dashboardPath, content);
2880
+ }
2881
+ async function patchProxyWithOnboardingRoute(projectRoot, ctx) {
2882
+ const proxyPath = path13.join(projectRoot, "src", "proxy.ts");
2883
+ if (!await fs13.pathExists(proxyPath)) return;
2884
+ await ctx.trackModifiedFile(proxyPath);
2885
+ let content = await fs13.readFile(proxyPath, "utf-8");
2886
+ if (content.includes("routes.onboarding")) return;
2887
+ content = content.replace(
2888
+ /const protectedRoutes = \[([^\]]*)\]/,
2889
+ "const protectedRoutes = [$1, routes.onboarding]"
2890
+ );
2891
+ await fs13.writeFile(proxyPath, content);
2892
+ }
2830
2893
  async function setConfigFlag6(projectRoot, ctx) {
2831
2894
  const configPath = path13.join(projectRoot, "src", "config", "app.config.ts");
2832
2895
  if (!await fs13.pathExists(configPath)) return;
@@ -4171,8 +4234,13 @@ async function generateAI(projectRoot) {
4171
4234
  "src/features/ai/server/anthropic-provider.ts": anthropicProvider(),
4172
4235
  "src/features/ai/validation/schemas.ts": schemas5(),
4173
4236
  "src/features/ai/hooks/use-chat.ts": useChatHook(),
4237
+ "src/features/ai/components/ChatMessages.tsx": chatMessagesComponent(),
4238
+ "src/features/ai/components/ChatInput.tsx": chatInputComponent(),
4239
+ "src/features/ai/components/Chat.tsx": chatComponent(),
4240
+ "src/features/ai/components/index.ts": componentBarrel(),
4174
4241
  "src/features/ai/index.ts": barrelExports2(),
4175
- "src/app/api/protected/ai/chat/route.ts": chatRoute()
4242
+ "src/app/api/protected/ai/chat/route.ts": chatRoute(),
4243
+ "src/app/(protected)/ai/page.tsx": aiPage()
4176
4244
  };
4177
4245
  let count = 0;
4178
4246
  for (const [filePath, content] of Object.entries(files)) {
@@ -4186,18 +4254,20 @@ async function generateAI(projectRoot) {
4186
4254
  openai: "^4.0.0",
4187
4255
  "@anthropic-ai/sdk": "^0.30.0"
4188
4256
  });
4257
+ await registerRoute(projectRoot, "ai", "/ai", ctx);
4258
+ await wireProtectedNav4(projectRoot, ctx);
4189
4259
  await setConfigFlag9(projectRoot, ctx);
4190
4260
  await ctx.commit();
4191
4261
  log.success(`Generated AI feature with ${count} files`);
4192
4262
  log.blank();
4193
- log.step("src/features/ai/ \u2014 types, provider factory, hooks, validation");
4263
+ log.step("src/features/ai/ \u2014 types, provider factory, hooks, validation, chat UI");
4264
+ log.step("src/features/ai/components/ \u2014 Chat, ChatMessages, ChatInput");
4265
+ log.step("src/app/(protected)/ai/ \u2014 AI chat page");
4194
4266
  log.step("src/app/api/protected/ai/chat/ \u2014 streaming chat endpoint");
4195
4267
  log.blank();
4196
4268
  log.warn("Next steps:");
4197
4269
  log.step('Set the provider in appConfig.services.ai.provider ("openai" or "anthropic")');
4198
4270
  log.step("Set OPENAI_API_KEY or ANTHROPIC_API_KEY in .env");
4199
- log.step("Use getAIProvider() from server code for chat/streaming");
4200
- log.step("Use useChat() hook for client-side chat interfaces");
4201
4271
  log.blank();
4202
4272
  } catch (error) {
4203
4273
  await ctx.rollback();
@@ -4588,11 +4658,281 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
4588
4658
  }
4589
4659
  `;
4590
4660
  }
4661
+ function chatMessagesComponent() {
4662
+ return `${STAMP9}
4663
+ 'use client';
4664
+
4665
+ import { useEffect, useRef } from 'react';
4666
+ import type { ChatMessage } from '../types';
4667
+ import { Spinner } from '@mars-stack/ui';
4668
+
4669
+ interface ChatMessagesProps {
4670
+ messages: ChatMessage[];
4671
+ isLoading: boolean;
4672
+ }
4673
+
4674
+ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
4675
+ const bottomRef = useRef<HTMLDivElement>(null);
4676
+
4677
+ useEffect(() => {
4678
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
4679
+ }, [messages]);
4680
+
4681
+ if (messages.length === 0) {
4682
+ return (
4683
+ <div className="flex flex-1 items-center justify-center">
4684
+ <div className="text-center">
4685
+ <svg
4686
+ className="mx-auto h-10 w-10 text-text-muted"
4687
+ fill="none"
4688
+ viewBox="0 0 24 24"
4689
+ strokeWidth={1}
4690
+ stroke="currentColor"
4691
+ >
4692
+ <path
4693
+ strokeLinecap="round"
4694
+ strokeLinejoin="round"
4695
+ d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z"
4696
+ />
4697
+ </svg>
4698
+ <p className="mt-3 text-sm text-text-secondary">Start a conversation</p>
4699
+ <p className="mt-1 text-xs text-text-muted">Send a message to begin chatting with the AI assistant.</p>
4700
+ </div>
4701
+ </div>
4702
+ );
4703
+ }
4704
+
4705
+ return (
4706
+ <div className="flex-1 overflow-y-auto px-4 py-6 space-y-4">
4707
+ {messages.map((message, index) => (
4708
+ <div
4709
+ key={index}
4710
+ className={\`flex \${message.role === 'user' ? 'justify-end' : 'justify-start'}\`}
4711
+ >
4712
+ <div
4713
+ className={\`max-w-[80%] rounded-2xl px-4 py-2.5 \${
4714
+ message.role === 'user'
4715
+ ? 'bg-brand-primary text-text-on-brand'
4716
+ : 'bg-surface-card border border-border-default text-text-primary'
4717
+ }\`}
4718
+ >
4719
+ {message.content ? (
4720
+ <p className="text-sm whitespace-pre-wrap">{message.content}</p>
4721
+ ) : isLoading && index === messages.length - 1 ? (
4722
+ <div className="flex items-center gap-1.5 py-1">
4723
+ <Spinner size="sm" />
4724
+ <span className="text-xs text-text-muted">Thinking...</span>
4725
+ </div>
4726
+ ) : null}
4727
+ </div>
4728
+ </div>
4729
+ ))}
4730
+ <div ref={bottomRef} />
4731
+ </div>
4732
+ );
4733
+ }
4734
+ `;
4735
+ }
4736
+ function chatInputComponent() {
4737
+ return `${STAMP9}
4738
+ 'use client';
4739
+
4740
+ import { useState, useCallback, useRef, useEffect } from 'react';
4741
+ import { Button } from '@mars-stack/ui';
4742
+
4743
+ interface ChatInputProps {
4744
+ onSend: (content: string) => void;
4745
+ disabled?: boolean;
4746
+ placeholder?: string;
4747
+ }
4748
+
4749
+ function SendIcon() {
4750
+ return (
4751
+ <svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
4752
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
4753
+ </svg>
4754
+ );
4755
+ }
4756
+
4757
+ export function ChatInput({ onSend, disabled = false, placeholder = 'Type a message...' }: ChatInputProps) {
4758
+ const [input, setInput] = useState('');
4759
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
4760
+
4761
+ const handleSubmit = useCallback(() => {
4762
+ const trimmed = input.trim();
4763
+ if (!trimmed || disabled) return;
4764
+
4765
+ onSend(trimmed);
4766
+ setInput('');
4767
+
4768
+ requestAnimationFrame(() => {
4769
+ if (textareaRef.current) {
4770
+ textareaRef.current.style.height = 'auto';
4771
+ }
4772
+ });
4773
+ }, [input, disabled, onSend]);
4774
+
4775
+ const handleKeyDown = useCallback(
4776
+ (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
4777
+ if (event.key === 'Enter' && !event.shiftKey) {
4778
+ event.preventDefault();
4779
+ handleSubmit();
4780
+ }
4781
+ },
4782
+ [handleSubmit],
4783
+ );
4784
+
4785
+ useEffect(() => {
4786
+ const textarea = textareaRef.current;
4787
+ if (!textarea) return;
4788
+
4789
+ textarea.style.height = 'auto';
4790
+ textarea.style.height = \`\${Math.min(textarea.scrollHeight, 160)}px\`;
4791
+ }, [input]);
4792
+
4793
+ return (
4794
+ <div className="border-t border-border-default bg-surface-card p-4">
4795
+ <div className="flex items-end gap-2">
4796
+ <textarea
4797
+ ref={textareaRef}
4798
+ value={input}
4799
+ onChange={(e) => setInput(e.target.value)}
4800
+ onKeyDown={handleKeyDown}
4801
+ placeholder={placeholder}
4802
+ disabled={disabled}
4803
+ rows={1}
4804
+ className="flex-1 resize-none rounded-xl border border-border-input bg-surface-input px-4 py-2.5 text-sm text-text-primary placeholder:text-text-muted focus:border-border-focus focus:outline-none focus:ring-2 focus:ring-ring-focus disabled:opacity-50"
4805
+ />
4806
+ <Button
4807
+ variant="primary"
4808
+ size="md"
4809
+ onClick={handleSubmit}
4810
+ disabled={disabled || !input.trim()}
4811
+ aria-label="Send message"
4812
+ >
4813
+ <SendIcon />
4814
+ </Button>
4815
+ </div>
4816
+ <p className="mt-1.5 text-xs text-text-muted">
4817
+ Press Enter to send, Shift+Enter for a new line.
4818
+ </p>
4819
+ </div>
4820
+ );
4821
+ }
4822
+ `;
4823
+ }
4824
+ function chatComponent() {
4825
+ return `${STAMP9}
4826
+ 'use client';
4827
+
4828
+ import { useChat } from '../hooks/use-chat';
4829
+ import { ChatMessages } from './ChatMessages';
4830
+ import { ChatInput } from './ChatInput';
4831
+
4832
+ interface ChatProps {
4833
+ systemPrompt?: string;
4834
+ placeholder?: string;
4835
+ className?: string;
4836
+ }
4837
+
4838
+ export function Chat({ placeholder, className }: ChatProps) {
4839
+ const { messages, isLoading, error, send, reset } = useChat();
4840
+
4841
+ return (
4842
+ <div className={\`flex flex-col h-full \${className ?? ''}\`}>
4843
+ {error && (
4844
+ <div className="mx-4 mt-4 rounded-lg border border-border-default bg-error-muted p-3">
4845
+ <div className="flex items-center justify-between gap-2">
4846
+ <p className="text-sm text-text-error">{error}</p>
4847
+ <button
4848
+ type="button"
4849
+ onClick={reset}
4850
+ className="text-xs font-medium text-text-link hover:text-text-link-hover"
4851
+ >
4852
+ Reset
4853
+ </button>
4854
+ </div>
4855
+ </div>
4856
+ )}
4857
+
4858
+ <ChatMessages messages={messages} isLoading={isLoading} />
4859
+
4860
+ <ChatInput
4861
+ onSend={send}
4862
+ disabled={isLoading}
4863
+ placeholder={placeholder}
4864
+ />
4865
+ </div>
4866
+ );
4867
+ }
4868
+ `;
4869
+ }
4870
+ function componentBarrel() {
4871
+ return `${STAMP9}
4872
+ export { Chat } from './Chat';
4873
+ export { ChatMessages } from './ChatMessages';
4874
+ export { ChatInput } from './ChatInput';
4875
+ `;
4876
+ }
4877
+ async function wireProtectedNav4(projectRoot, ctx) {
4878
+ const layoutPath = path16.join(
4879
+ projectRoot,
4880
+ "src",
4881
+ "app",
4882
+ "(protected)",
4883
+ "layout.tsx"
4884
+ );
4885
+ if (!await fs16.pathExists(layoutPath)) return;
4886
+ let content = await fs16.readFile(layoutPath, "utf-8");
4887
+ if (content.includes("routes.ai") || content.includes("label: 'AI'")) return;
4888
+ await ctx.trackModifiedFile(layoutPath);
4889
+ const insertion = ` if (appConfig.features.ai) {
4890
+ items.push({ label: 'AI', href: routes.ai });
4891
+ }
4892
+ `;
4893
+ if (content.includes("function buildNavItems()") && content.includes("return items;")) {
4894
+ const returnMarker = " return items;";
4895
+ const insertPos = content.lastIndexOf(returnMarker);
4896
+ if (insertPos !== -1) {
4897
+ content = content.slice(0, insertPos) + insertion + content.slice(insertPos);
4898
+ }
4899
+ } else if (content.includes("NAV_ITEMS")) {
4900
+ content = insertImportAfterDirectives(
4901
+ content,
4902
+ "// AI nav link added by mars generate ai\n"
4903
+ );
4904
+ }
4905
+ await fs16.writeFile(layoutPath, content);
4906
+ }
4907
+ function aiPage() {
4908
+ return `${STAMP9}
4909
+ 'use client';
4910
+
4911
+ import { Chat } from '@/features/ai/components';
4912
+
4913
+ export default function AIPage() {
4914
+ return (
4915
+ <div className="flex h-[calc(100vh-8rem)] flex-col">
4916
+ <div className="mb-4">
4917
+ <h1 className="text-2xl font-bold text-text-primary">AI Assistant</h1>
4918
+ <p className="mt-1 text-text-secondary">Chat with the AI assistant.</p>
4919
+ </div>
4920
+
4921
+ <div className="flex-1 overflow-hidden rounded-xl border border-border-default bg-surface-card">
4922
+ <Chat placeholder="Ask me anything..." />
4923
+ </div>
4924
+ </div>
4925
+ );
4926
+ }
4927
+ `;
4928
+ }
4591
4929
  function barrelExports2() {
4592
4930
  return `${STAMP9}
4593
4931
  export type { ChatMessage, ChatParams, ChatResponse, AIProvider } from './types';
4594
- export { chatSchema, type ChatInput } from './validation/schemas';
4932
+ export { chatSchema } from './validation/schemas';
4933
+ export type { ChatInput as ChatFormInput } from './validation/schemas';
4595
4934
  export { useChat } from './hooks/use-chat';
4935
+ export { Chat, ChatMessages, ChatInput } from './components';
4596
4936
  `;
4597
4937
  }
4598
4938
  function chatRoute() {
@@ -4636,9 +4976,11 @@ var GENERATOR_VERSION9, STAMP9;
4636
4976
  var init_ai = __esm({
4637
4977
  "src/generators/features/ai.ts"() {
4638
4978
  "use strict";
4979
+ init_client_file_patch();
4639
4980
  init_logger();
4640
4981
  init_rollback();
4641
4982
  init_dependencies();
4983
+ init_routes();
4642
4984
  GENERATOR_VERSION9 = "0.1.0";
4643
4985
  STAMP9 = `// @mars-generated ai@${GENERATOR_VERSION9}`;
4644
4986
  }
@@ -6609,10 +6951,12 @@ var FEATURE_DIRECTORY_MAP = {
6609
6951
  "src/features/billing",
6610
6952
  "src/app/api/protected/billing",
6611
6953
  "src/app/api/webhooks/stripe",
6954
+ "src/app/(protected)/settings/billing",
6612
6955
  "prisma/schema/subscription.prisma"
6613
6956
  ],
6614
6957
  fileUpload: [
6615
6958
  "src/features/uploads",
6959
+ "src/app/(protected)/files",
6616
6960
  "src/app/api/protected/files",
6617
6961
  "prisma/schema/file.prisma"
6618
6962
  ]