mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-27 02:52:22 +03:00
Added JSON config via CLI for Wasp AI.
This commit is contained in:
parent
005c17cef6
commit
a5323a2e05
@ -38,14 +38,17 @@ export const startGeneratingNewApp: StartGeneratingNewApp<
|
||||
unconsumedStdout: "",
|
||||
};
|
||||
|
||||
// { auth: 'UsernameAndPassword', primaryColor: string }
|
||||
const projectConfig = {}
|
||||
|
||||
const stdoutMutex = new Mutex();
|
||||
let waspCliProcess = null;
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
waspCliProcess = spawn("wasp", ["new-ai", args.appName, args.appDesc]);
|
||||
waspCliProcess = spawn("wasp", ["new-ai", args.appName, args.appDesc, JSON.stringify(projectConfig)]);
|
||||
} else {
|
||||
// NOTE: In dev when we use `wasp-cli`, we want to make sure that if this app is run via `wasp` that its datadir env var does not propagate,
|
||||
// so we reset it here. This is problem only if you run app with `wasp` and let it call `wasp-cli` here.
|
||||
waspCliProcess = spawn("wasp-cli", ["new-ai", args.appName, args.appDesc], {
|
||||
waspCliProcess = spawn("wasp-cli", ["new-ai", args.appName, args.appDesc, JSON.stringify(projectConfig)], {
|
||||
env: { ...process.env, waspc_datadir: undefined },
|
||||
});
|
||||
}
|
||||
|
@ -47,11 +47,11 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
|
||||
("new" : newArgs) -> Command.Call.New newArgs
|
||||
-- new-ai / new-ai:stdout is meant to be called and consumed programatically (e.g. by our Wasp AI
|
||||
-- web app), while new-ai:disk is useful for us for testing.
|
||||
[newAiCmd, projectName, appDescription]
|
||||
[newAiCmd, projectName, appDescription, projectConfigJson]
|
||||
| newAiCmd `elem` ["new-ai", "new-ai:stdout"] ->
|
||||
Command.Call.NewAiToStdout projectName appDescription
|
||||
Command.Call.NewAiToStdout projectName appDescription projectConfigJson
|
||||
| newAiCmd == "new-ai:disk" ->
|
||||
Command.Call.NewAiToDisk projectName appDescription
|
||||
Command.Call.NewAiToDisk projectName appDescription projectConfigJson
|
||||
["start"] -> Command.Call.Start
|
||||
["start", "db"] -> Command.Call.StartDb
|
||||
["clean"] -> Command.Call.Clean
|
||||
@ -86,10 +86,10 @@ main = withUtf8 . (`E.catch` handleInternalErrors) $ do
|
||||
|
||||
case commandCall of
|
||||
Command.Call.New newArgs -> runCommand $ createNewProject newArgs
|
||||
Command.Call.NewAiToStdout projectName appDescription ->
|
||||
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveToStdout projectName appDescription
|
||||
Command.Call.NewAiToDisk projectName appDescription ->
|
||||
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveOnDisk projectName appDescription
|
||||
Command.Call.NewAiToStdout projectName appDescription projectConfigJson ->
|
||||
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveToStdout projectName appDescription projectConfigJson
|
||||
Command.Call.NewAiToDisk projectName appDescription projectConfigJson ->
|
||||
runCommand $ Command.CreateNewProject.AI.createNewProjectNonInteractiveOnDisk projectName appDescription projectConfigJson
|
||||
Command.Call.Start -> runCommand start
|
||||
Command.Call.StartDb -> runCommand Command.Start.Db.start
|
||||
Command.Call.Clean -> runCommand clean
|
||||
|
@ -2,8 +2,8 @@ module Wasp.Cli.Command.Call where
|
||||
|
||||
data Call
|
||||
= New Arguments
|
||||
| NewAiToStdout String String -- projectName, appDescription
|
||||
| NewAiToDisk String String -- projectName, appDescription
|
||||
| NewAiToStdout String String String -- projectName, appDescription, projectConfigJson
|
||||
| NewAiToDisk String String String -- projectName, appDescription, projectConfigJson
|
||||
| Start
|
||||
| StartDb
|
||||
| Clean
|
||||
|
@ -7,6 +7,7 @@ where
|
||||
|
||||
import Control.Arrow ()
|
||||
import Control.Monad.Except (MonadError (throwError), MonadIO (liftIO))
|
||||
import Data.Function ((&))
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.IO as T.IO
|
||||
import StrongPath (Abs, Dir, Path', fromAbsDir)
|
||||
@ -17,34 +18,44 @@ import System.FilePath (takeDirectory)
|
||||
import System.IO (hFlush, stdout)
|
||||
import qualified Wasp.AI.CodeAgent as CA
|
||||
import qualified Wasp.AI.GenerateNewProject as GNP
|
||||
import Wasp.AI.GenerateNewProject.Common (AuthProvider (..), NewProjectDetails (..))
|
||||
import Wasp.AI.GenerateNewProject.Common (NewProjectConfig, NewProjectDetails (..), emptyNewProjectConfig)
|
||||
import Wasp.AI.OpenAI (OpenAIApiKey)
|
||||
import Wasp.Cli.Command (Command, CommandError (CommandError))
|
||||
import Wasp.Cli.Command.CreateNewProject.ProjectDescription (NewProjectAppName (..), obtainAvailableProjectDirPath, parseWaspProjectNameIntoAppName)
|
||||
import Wasp.Cli.Command.CreateNewProject.StarterTemplates (readWaspProjectSkeletonFiles)
|
||||
import Wasp.Cli.Common (WaspProjectDir)
|
||||
import qualified Wasp.Cli.Interactive as Interactive
|
||||
import qualified Wasp.Util.Aeson as Utils.Aeson
|
||||
|
||||
createNewProjectInteractiveOnDisk :: Path' Abs (Dir WaspProjectDir) -> NewProjectAppName -> Command ()
|
||||
createNewProjectInteractiveOnDisk waspProjectDir appName = do
|
||||
openAIApiKey <- getOpenAIApiKey
|
||||
appDescription <- liftIO $ Interactive.askForRequiredInput "Describe your app in a couple of sentences"
|
||||
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription
|
||||
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription emptyNewProjectConfig
|
||||
|
||||
createNewProjectNonInteractiveOnDisk :: String -> String -> Command ()
|
||||
createNewProjectNonInteractiveOnDisk projectName appDescription = do
|
||||
createNewProjectNonInteractiveOnDisk :: String -> String -> String -> Command ()
|
||||
createNewProjectNonInteractiveOnDisk projectName appDescription projectConfigJson = do
|
||||
appName <- case parseWaspProjectNameIntoAppName projectName of
|
||||
Right appName -> pure appName
|
||||
Left err -> throwError $ CommandError "Invalid project name" err
|
||||
projectConfig <-
|
||||
Utils.Aeson.decodeFromString projectConfigJson
|
||||
& either (throwError . CommandError "Invalid project config" . ("Failed to parse JSON: " <>)) pure
|
||||
waspProjectDir <- obtainAvailableProjectDirPath projectName
|
||||
openAIApiKey <- getOpenAIApiKey
|
||||
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription
|
||||
liftIO $ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription projectConfig
|
||||
|
||||
createNewProjectOnDisk :: OpenAIApiKey -> Path' Abs (Dir WaspProjectDir) -> NewProjectAppName -> String -> IO ()
|
||||
createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription = do
|
||||
createNewProjectOnDisk ::
|
||||
OpenAIApiKey ->
|
||||
Path' Abs (Dir WaspProjectDir) ->
|
||||
NewProjectAppName ->
|
||||
String ->
|
||||
NewProjectConfig ->
|
||||
IO ()
|
||||
createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription projectConfig = do
|
||||
createDirectory $ fromAbsDir waspProjectDir
|
||||
setCurrentDirectory $ fromAbsDir waspProjectDir
|
||||
generateNewProject codeAgentConfig appName appDescription
|
||||
generateNewProject codeAgentConfig appName appDescription projectConfig
|
||||
where
|
||||
codeAgentConfig =
|
||||
CA.CodeAgentConfig
|
||||
@ -65,14 +76,18 @@ createNewProjectOnDisk openAIApiKey waspProjectDir appName appDescription = do
|
||||
|
||||
-- | Instead of writing files to disk, it will write files (and logs) to the stdout,
|
||||
-- with delimiters that make it easy to programmaticaly parse the output.
|
||||
createNewProjectNonInteractiveToStdout :: String -> String -> Command ()
|
||||
createNewProjectNonInteractiveToStdout projectName appDescription = do
|
||||
createNewProjectNonInteractiveToStdout :: String -> String -> String -> Command ()
|
||||
createNewProjectNonInteractiveToStdout projectName appDescription projectConfigJsonStr = do
|
||||
openAIApiKey <- getOpenAIApiKey
|
||||
|
||||
appName <- case parseWaspProjectNameIntoAppName projectName of
|
||||
Right appName -> pure appName
|
||||
Left err -> throwError $ CommandError "Invalid project name" err
|
||||
|
||||
projectConfig <-
|
||||
Utils.Aeson.decodeFromString projectConfigJsonStr
|
||||
& either (throwError . CommandError "Invalid project config" . ("Failed to parse JSON: " <>)) pure
|
||||
|
||||
let codeAgentConfig =
|
||||
CA.CodeAgentConfig
|
||||
{ CA._openAIApiKey = openAIApiKey,
|
||||
@ -80,7 +95,7 @@ createNewProjectNonInteractiveToStdout projectName appDescription = do
|
||||
CA._writeLog = writeLogToStdoutWithDelimiters
|
||||
}
|
||||
|
||||
liftIO $ generateNewProject codeAgentConfig appName appDescription
|
||||
liftIO $ generateNewProject codeAgentConfig appName appDescription projectConfig
|
||||
where
|
||||
writeFileToStdoutWithDelimiters path content =
|
||||
writeToStdoutWithDelimiters "WRITE FILE" [T.pack path, content]
|
||||
@ -100,11 +115,11 @@ createNewProjectNonInteractiveToStdout projectName appDescription = do
|
||||
"===/ WASP AI: " <> title <> " ===="
|
||||
]
|
||||
|
||||
generateNewProject :: CA.CodeAgentConfig -> NewProjectAppName -> String -> IO ()
|
||||
generateNewProject codeAgentConfig (NewProjectAppName appName) appDescription = do
|
||||
generateNewProject :: CA.CodeAgentConfig -> NewProjectAppName -> String -> NewProjectConfig -> IO ()
|
||||
generateNewProject codeAgentConfig (NewProjectAppName appName) appDescription projectConfig = do
|
||||
waspProjectSkeletonFiles <- readWaspProjectSkeletonFiles
|
||||
CA.runCodeAgent codeAgentConfig $ do
|
||||
GNP.generateNewProject (newProjectDetails appName appDescription) waspProjectSkeletonFiles
|
||||
GNP.generateNewProject (newProjectDetails projectConfig appName appDescription) waspProjectSkeletonFiles
|
||||
|
||||
getOpenAIApiKey :: Command OpenAIApiKey
|
||||
getOpenAIApiKey =
|
||||
@ -123,10 +138,10 @@ getOpenAIApiKey =
|
||||
"to .bash_profile or .profile, restart your shell, and you should be good to go."
|
||||
]
|
||||
|
||||
newProjectDetails :: String -> String -> NewProjectDetails
|
||||
newProjectDetails webAppName webAppDescription =
|
||||
newProjectDetails :: NewProjectConfig -> String -> String -> NewProjectDetails
|
||||
newProjectDetails projectConfig webAppName webAppDescription =
|
||||
NewProjectDetails
|
||||
{ _projectAppName = webAppName,
|
||||
_projectDescription = webAppDescription,
|
||||
_projectAuth = UsernameAndPassword
|
||||
_projectConfig = projectConfig
|
||||
}
|
||||
|
@ -1,14 +1,18 @@
|
||||
module Wasp.AI.GenerateNewProject.Common
|
||||
( NewProjectDetails (..),
|
||||
File,
|
||||
NewProjectConfig (..),
|
||||
AuthProvider (..),
|
||||
File,
|
||||
getProjectAuth,
|
||||
getProjectPrimaryColor,
|
||||
emptyNewProjectConfig,
|
||||
queryChatGPTForJSON,
|
||||
defaultChatGPTParams,
|
||||
writeToWaspFileEnd,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson (FromJSON)
|
||||
import Data.Aeson (FromJSON, withObject, withText, (.:?))
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
@ -21,14 +25,46 @@ import Wasp.Util (naiveTrimJSON, textToLazyBS)
|
||||
data NewProjectDetails = NewProjectDetails
|
||||
{ _projectAppName :: !String,
|
||||
_projectDescription :: !String,
|
||||
_projectAuth :: !AuthProvider
|
||||
_projectConfig :: NewProjectConfig
|
||||
}
|
||||
|
||||
-- TODO: Make these relative to WaspProjectDir, via StrongPath?
|
||||
type File = (FilePath, Text)
|
||||
data NewProjectConfig = NewProjectConfig
|
||||
{ projectAuth :: !(Maybe AuthProvider),
|
||||
-- CSS acceptable string for color.
|
||||
projectPrimaryColor :: !(Maybe String)
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
instance Aeson.FromJSON NewProjectConfig where
|
||||
parseJSON = withObject "NewProjectConfig" $ \obj -> do
|
||||
auth <- obj .:? "auth"
|
||||
primaryColor <- obj .:? "primaryColor"
|
||||
return (NewProjectConfig {projectAuth = auth, projectPrimaryColor = primaryColor})
|
||||
|
||||
emptyNewProjectConfig :: NewProjectConfig
|
||||
emptyNewProjectConfig =
|
||||
NewProjectConfig
|
||||
{ projectAuth = Nothing,
|
||||
projectPrimaryColor = Nothing
|
||||
}
|
||||
|
||||
getProjectAuth :: NewProjectDetails -> AuthProvider
|
||||
getProjectAuth = fromMaybe UsernameAndPassword . projectAuth . _projectConfig
|
||||
|
||||
getProjectPrimaryColor :: NewProjectDetails -> String
|
||||
getProjectPrimaryColor = fromMaybe "#fc0" . projectPrimaryColor . _projectConfig
|
||||
|
||||
-- TODO: Support more methods.
|
||||
data AuthProvider = UsernameAndPassword
|
||||
deriving (Show)
|
||||
|
||||
instance Aeson.FromJSON AuthProvider where
|
||||
parseJSON = withText "AuthProvider" $ \case
|
||||
"UsernameAndPassword" -> return UsernameAndPassword
|
||||
_ -> fail "invalid auth provider"
|
||||
|
||||
-- TODO: Make these relative to WaspProjectDir, via StrongPath?
|
||||
type File = (FilePath, Text)
|
||||
|
||||
queryChatGPTForJSON :: FromJSON a => ChatGPTParams -> [ChatMessage] -> CodeAgent a
|
||||
queryChatGPTForJSON chatGPTParams = doQueryForJSON 0
|
||||
|
@ -11,7 +11,7 @@ import StrongPath (File', Path, Rel)
|
||||
import qualified StrongPath as SP
|
||||
import StrongPath.Types (System)
|
||||
import Wasp.AI.CodeAgent (CodeAgent, writeNewFile)
|
||||
import Wasp.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..))
|
||||
import Wasp.AI.GenerateNewProject.Common (AuthProvider (..), File, NewProjectDetails (..), getProjectAuth)
|
||||
import Wasp.AI.GenerateNewProject.Plan (PlanRule)
|
||||
import Wasp.Project (WaspProjectDir)
|
||||
import qualified Wasp.SemanticVersion as SV
|
||||
@ -28,7 +28,7 @@ generateAndWriteProjectSkeletonAndPresetFiles newProjectDetails waspProjectSkele
|
||||
let (waspFile@(waspFilePath, _), planRules) = generateBaseWaspFile newProjectDetails
|
||||
writeNewFile waspFile
|
||||
|
||||
case _projectAuth newProjectDetails of
|
||||
case getProjectAuth newProjectDetails of
|
||||
UsernameAndPassword -> do
|
||||
writeNewFile generateLoginJsPage
|
||||
writeNewFile generateSignupJsPage
|
||||
@ -50,7 +50,7 @@ generateBaseWaspFile newProjectDetails = ((path, content), planRules)
|
||||
appName = T.pack $ _projectAppName newProjectDetails
|
||||
appTitle = appName
|
||||
waspVersionRange = T.pack . show $ SV.backwardsCompatibleWith Wasp.Version.waspVersion
|
||||
(appAuth, authPlanRules) = case _projectAuth newProjectDetails of
|
||||
(appAuth, authPlanRules) = case getProjectAuth newProjectDetails of
|
||||
UsernameAndPassword ->
|
||||
( [trimming|
|
||||
auth: {
|
||||
|
@ -1,13 +1,18 @@
|
||||
module Wasp.Util.Aeson
|
||||
( encodeToText,
|
||||
decodeFromString,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson (ToJSON)
|
||||
import Data.Aeson (FromJSON, ToJSON, eitherDecode)
|
||||
import Data.Aeson.Text (encodeToTextBuilder)
|
||||
import qualified Data.ByteString.Lazy.UTF8 as BS
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Lazy (toStrict)
|
||||
import Data.Text.Lazy.Builder (toLazyText)
|
||||
|
||||
encodeToText :: ToJSON a => a -> Text
|
||||
encodeToText = toStrict . toLazyText . encodeToTextBuilder
|
||||
|
||||
decodeFromString :: FromJSON a => String -> Either String a
|
||||
decodeFromString = eitherDecode . BS.fromString
|
||||
|
Loading…
Reference in New Issue
Block a user