diff --git a/qqq-dev-tools/pom.xml b/qqq-dev-tools/pom.xml
new file mode 100644
index 00000000..32acc4cf
--- /dev/null
+++ b/qqq-dev-tools/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ com.kingsrook.qqq
+ qqq-dev-tools
+ 1.0.0-SNAPSHOT
+ jar
+
+ QQQ Dev Tools
+ Tools for developers of QQQ (is that the framework or applications or qbits or what?)
+
+
+ 17
+ 17
+ UTF-8
+ 5.10.0
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+ 17
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+
+
+
\ No newline at end of file
diff --git a/qqq-dev-tools/src/main/java/com/kingsrook/qqq/devtools/CreateNewQBit.java b/qqq-dev-tools/src/main/java/com/kingsrook/qqq/devtools/CreateNewQBit.java
new file mode 100644
index 00000000..9d5a7194
--- /dev/null
+++ b/qqq-dev-tools/src/main/java/com/kingsrook/qqq/devtools/CreateNewQBit.java
@@ -0,0 +1,339 @@
+package com.kingsrook.qqq.devtools;
+
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+
+/*******************************************************************************
+ ** todo picocli this project and class
+ *******************************************************************************/
+public class CreateNewQBit
+{
+ private String name;
+ private String root;
+
+ private static ExecutorService executorService = null;
+
+ private static String SED = "/opt/homebrew/bin/gsed";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void main(String[] args)
+ {
+ args = new String[] { "/Users/dkelkhoff/git/kingsrook/qbits", "webhooks" };
+
+ if(args.length < 2)
+ {
+ System.out.println("Usage: java CreateNewQBit root-dir qbit-name");
+ System.exit(1);
+ }
+
+ CreateNewQBit instance = new CreateNewQBit();
+ instance.root = args[0];
+ instance.name = args[1];
+ System.exit(instance.run());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public int run()
+ {
+ try
+ {
+ String wordsName = makeWordsName(name);
+ wordsName = stripQBitPrefix(wordsName);
+ String dashName = makeDashName(wordsName);
+ String packageName = makePackageName(wordsName);
+ String className = makeClassName(wordsName);
+ String varName = makeVarName(wordsName);
+
+ if(!new File(root).exists())
+ {
+ System.err.println("ERROR: Root directory [" + root + "] does not exist.");
+ return (1);
+ }
+
+ File template = new File(root + File.separator + "TEMPLATE");
+ if(!template.exists())
+ {
+ System.err.println("ERROR: Template directory [TEMPLATE] does not exist under [" + root + "].");
+ return (1);
+ }
+
+ File dir = new File(root + File.separator + "qbit-" + dashName);
+ if(dir.exists())
+ {
+ System.err.println("ERROR: Directory [" + dashName + "] already exists under [" + root + "].");
+ return (1);
+ }
+
+ System.out.println("Creating qbit-" + dashName + ":");
+ System.out.printf("%13s %s\n", "packgaename:", packageName);
+ System.out.printf("%13s %s\n", "ClassName:", className);
+ System.out.printf("%13s %s\n", "varName:", varName);
+ System.out.println();
+
+ System.out.println("Copying template...");
+ ProcessResult cpResult = run(new ProcessBuilder("cp", "-rv", template.getAbsolutePath(), dir.getAbsolutePath()));
+ System.out.print(cpResult.stdout());
+ System.out.println();
+
+ System.out.println("Renaming files...");
+ renameFiles(dir, packageName, className);
+ System.out.println();
+
+ System.out.println("Updating file contents...");
+ replacePlaceholders(dir, dashName, packageName, className, varName);
+ System.out.println();
+
+ System.out.println("Init'ing git repo...");
+ run(new ProcessBuilder("git", "init").directory(dir));
+ System.out.println();
+
+ // git remote add origin https://github.com/Kingsrook/${name}.git ?
+ // echo https://app.circleci.com/projects/project-dashboard/github/Kingsrook after initial push
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ return 1;
+ }
+ return 0;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ void renameFiles(File dir, String packageName, String className) throws Exception
+ {
+ String srcPath = dir.getAbsolutePath() + "/src/main/java/com/kingsrook/qbits";
+ String packagePath = packageName.replace('.', '/');
+ System.out.print(run(new ProcessBuilder("mv", "-v", srcPath + "/todo/TodoQBitConfig.java", srcPath + "/todo/" + className + "QBitConfig.java")).stdout());
+ System.out.print(run(new ProcessBuilder("mv", "-v", srcPath + "/todo/TodoQBitProducer.java", srcPath + "/todo/" + className + "QBitProducer.java")).stdout());
+ System.out.print(run(new ProcessBuilder("mv", "-v", srcPath + "/todo", srcPath + "/" + packagePath)).stdout());
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static void replacePlaceholders(File dir, String dashName, String packageName, String className, String varName) throws Exception
+ {
+ for(File file : dir.listFiles())
+ {
+ if(file.isDirectory())
+ {
+ replacePlaceholders(file, dashName, packageName, className, varName);
+ continue;
+ }
+
+ System.out.println("Replacing placeholders in: " + file.getAbsolutePath());
+ replaceOne("dashName", dashName, file);
+ replaceOne("packageName", packageName, file);
+ replaceOne("className", className, file);
+ replaceOne("varName", varName, file);
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static void replaceOne(String from, String to, File file) throws Exception
+ {
+ run(new ProcessBuilder(SED, "s/\\${" + from + "}/" + to + "/g", "-i", file.getAbsolutePath()));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public record ProcessResult(Integer exitCode, String stdout, String stderr)
+ {
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ public boolean hasStdout()
+ {
+ return stdout != null && !stdout.isEmpty();
+ }
+
+
+
+ /***************************************************************************
+ *
+ ***************************************************************************/
+ public boolean hasStderr()
+ {
+ return stderr != null && !stderr.isEmpty();
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public static ProcessResult run(ProcessBuilder builder) throws Exception
+ {
+ StringBuilder stdout = new StringBuilder();
+ StringBuilder stderr = new StringBuilder();
+
+ Process process = builder.start();
+ Future> stdoutFuture = getExecutorService().submit(new StreamGobbler(process.getInputStream(), stdout::append));
+ Future> stderrFuture = getExecutorService().submit(new StreamGobbler(process.getErrorStream(), stderr::append));
+
+ int exitCode = process.waitFor();
+ stdoutFuture.get();
+ stderrFuture.get();
+
+ return (new ProcessResult(exitCode, stdout.toString(), stderr.toString()));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private static class StreamGobbler implements Runnable
+ {
+ private InputStream inputStream;
+ private Consumer consumer;
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ public StreamGobbler(InputStream inputStream, Consumer consumer)
+ {
+ this.inputStream = inputStream;
+ this.consumer = consumer;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ @Override
+ public void run()
+ {
+ new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(s -> consumer.accept(s + System.lineSeparator()));
+ }
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private static ExecutorService getExecutorService()
+ {
+ if(executorService == null)
+ {
+ executorService = Executors.newCachedThreadPool();
+ }
+ return (executorService);
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ private String makeWordsName(String s)
+ {
+ if(s.contains("-"))
+ {
+ return (s.toLowerCase().replace('-', ' '));
+ }
+
+ if(s.matches(".*[A-Z].*"))
+ {
+ return s.replaceAll("([A-Z])", "$1'").toLowerCase().trim();
+ }
+
+ return s;
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static String stripQBitPrefix(String s)
+ {
+ return (s.replaceFirst("^qbit(s) ", ""));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static String makeDashName(String s)
+ {
+ return (s.replace(' ', '-'));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static String makePackageName(String s)
+ {
+ return (s.replace(" ", ""));
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static String makeClassName(String s)
+ {
+ StringBuilder rs = new StringBuilder();
+ String[] words = s.split(" ");
+ for(String word : words)
+ {
+ rs.append(word.substring(0, 1).toUpperCase());
+ if(word.length() > 1)
+ {
+ rs.append(word.substring(1));
+ }
+ }
+ return rs.toString();
+ }
+
+
+
+ /***************************************************************************
+ **
+ ***************************************************************************/
+ static String makeVarName(String s)
+ {
+ String className = makeClassName(s);
+ return className.substring(0, 1).toLowerCase() + (className.length() == 1 ? "" : className.substring(1));
+ }
+}