create-shiftapi 0.0.1

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/README.md ADDED
@@ -0,0 +1,69 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/fcjr/shiftapi/main/assets/logo.svg" alt="ShiftAPI Logo">
3
+ </p>
4
+
5
+ # create-shiftapi
6
+
7
+ Scaffold a new [ShiftAPI](https://github.com/fcjr/shiftapi) fullstack app — Go server + typed TypeScript frontend.
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ npm create shiftapi
13
+ ```
14
+
15
+ Or with pnpm / yarn:
16
+
17
+ ```bash
18
+ pnpm create shiftapi
19
+ yarn create shiftapi
20
+ ```
21
+
22
+ You can also pass the project name directly:
23
+
24
+ ```bash
25
+ npm create shiftapi my-app
26
+ ```
27
+
28
+ ## What You Get
29
+
30
+ ```
31
+ my-app/
32
+ cmd/my-app/main.go # Go entry point with graceful shutdown
33
+ internal/server/server.go # API routes and handlers
34
+ go.mod
35
+ .env # PORT config
36
+ .gitignore
37
+ package.json # Monorepo root with workspaces
38
+ apps/web/
39
+ package.json # React or Svelte frontend
40
+ vite.config.ts # ShiftAPI vite plugin configured
41
+ tsconfig.json
42
+ index.html
43
+ src/
44
+ main.tsx (or .ts) # App entry
45
+ App.tsx (or .svelte) # Demo component with typed API calls
46
+ ```
47
+
48
+ ## Prompts
49
+
50
+ | Prompt | Default |
51
+ |---|---|
52
+ | Project name | `my-app` |
53
+ | Framework | React / Svelte |
54
+ | Directory | `./<project-name>` |
55
+ | Go module path | `github.com/<gh-user>/<project-name>` if logged into `gh`, otherwise `<project-name>` |
56
+ | Server port | `8080` |
57
+
58
+ ## After Scaffolding
59
+
60
+ ```bash
61
+ cd my-app
62
+ go mod tidy
63
+ npm install
64
+ npm run dev
65
+ ```
66
+
67
+ This starts the Go server and Vite dev server together. The frontend gets fully typed API clients generated from your Go handlers — edit a struct in Go, get instant type errors in TypeScript.
68
+
69
+ API docs are served at `http://localhost:8080/docs`.
package/dist/index.js ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs2 from "fs";
5
+ import os from "os";
6
+ import * as p from "@clack/prompts";
7
+ import path2 from "path";
8
+
9
+ // src/scaffold.ts
10
+ import { execFile } from "child_process";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ var renameFiles = {
15
+ _gitignore: ".gitignore"
16
+ };
17
+ var pkgDir = path.resolve(fileURLToPath(import.meta.url), "../..");
18
+ var templatesDir = path.join(pkgDir, "templates");
19
+ var pkgVersion = JSON.parse(
20
+ fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8")
21
+ ).version;
22
+ function replaceplaceholders(content, opts) {
23
+ return content.replaceAll("{{name}}", opts.name).replaceAll("{{modulePath}}", opts.modulePath).replaceAll("{{port}}", opts.port).replaceAll("{{version}}", pkgVersion);
24
+ }
25
+ function renamePath(filePath, opts) {
26
+ return filePath.replaceAll("__name__", opts.name);
27
+ }
28
+ function copyDir(srcDir, destDir, opts) {
29
+ fs.mkdirSync(destDir, { recursive: true });
30
+ for (const entry of fs.readdirSync(srcDir)) {
31
+ const srcPath = path.join(srcDir, entry);
32
+ const destName = renameFiles[entry] ?? entry;
33
+ const destPath = path.join(destDir, renamePath(destName, opts));
34
+ const stat = fs.statSync(srcPath);
35
+ if (stat.isDirectory()) {
36
+ copyDir(srcPath, destPath, opts);
37
+ } else {
38
+ const content = fs.readFileSync(srcPath, "utf-8");
39
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
40
+ fs.writeFileSync(destPath, replaceplaceholders(content, opts));
41
+ }
42
+ }
43
+ }
44
+ function gitInit(cwd) {
45
+ return new Promise((resolve, reject) => {
46
+ execFile("git", ["init"], { cwd }, (err) => {
47
+ if (err) {
48
+ reject(err);
49
+ return;
50
+ }
51
+ resolve();
52
+ });
53
+ });
54
+ }
55
+ async function scaffold(opts) {
56
+ copyDir(path.join(templatesDir, "base"), opts.targetDir, opts);
57
+ copyDir(path.join(templatesDir, opts.framework), opts.targetDir, opts);
58
+ await gitInit(opts.targetDir);
59
+ }
60
+
61
+ // src/index.ts
62
+ function expandHome(filepath) {
63
+ if (filepath === "~" || filepath.startsWith("~/")) {
64
+ return path2.join(os.homedir(), filepath.slice(1));
65
+ }
66
+ return filepath;
67
+ }
68
+ function getGitHubUser() {
69
+ const configDir = expandHome(
70
+ process.env.GH_CONFIG_DIR ?? process.env.XDG_CONFIG_HOME ? path2.join(expandHome(process.env.XDG_CONFIG_HOME), "gh") : path2.join(os.homedir(), ".config", "gh")
71
+ );
72
+ try {
73
+ const hosts = fs2.readFileSync(path2.join(configDir, "hosts.yml"), "utf-8");
74
+ const match = hosts.match(/github\.com:\s[\s\S]*?^\s+user:\s+(.+)$/m);
75
+ return match?.[1]?.trim() || null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+ async function main() {
81
+ const positionalName = process.argv[2];
82
+ const ghUser = getGitHubUser();
83
+ p.intro("create-shiftapi");
84
+ const project = await p.group(
85
+ {
86
+ name: () => positionalName ? Promise.resolve(positionalName) : p.text({
87
+ message: "Project name",
88
+ placeholder: "my-app",
89
+ defaultValue: "my-app"
90
+ }),
91
+ framework: () => p.select({
92
+ message: "Framework",
93
+ options: [
94
+ { label: "React", value: "react" },
95
+ { label: "Svelte", value: "svelte" }
96
+ ]
97
+ }),
98
+ directory: ({ results }) => p.text({
99
+ message: "Directory",
100
+ placeholder: `./${results.name}`,
101
+ defaultValue: `./${results.name}`
102
+ }),
103
+ module: ({ results }) => p.text({
104
+ message: "Go module path",
105
+ placeholder: ghUser ? `github.com/${ghUser}/${results.name}` : results.name,
106
+ defaultValue: ghUser ? `github.com/${ghUser}/${results.name}` : results.name
107
+ }),
108
+ port: () => p.text({
109
+ message: "Server port",
110
+ placeholder: "8080",
111
+ defaultValue: "8080"
112
+ })
113
+ },
114
+ {
115
+ onCancel: () => {
116
+ p.cancel("Cancelled.");
117
+ process.exit(1);
118
+ }
119
+ }
120
+ );
121
+ const targetDir = path2.resolve(process.cwd(), expandHome(project.directory));
122
+ if (fs2.existsSync(targetDir)) {
123
+ p.cancel(`${targetDir} already exists.`);
124
+ process.exit(1);
125
+ }
126
+ const s = p.spinner();
127
+ s.start("Scaffolding project");
128
+ await scaffold({
129
+ name: project.name,
130
+ modulePath: project.module,
131
+ port: project.port,
132
+ framework: project.framework,
133
+ targetDir
134
+ });
135
+ s.stop("Project scaffolded");
136
+ const relDir = path2.relative(process.cwd(), targetDir) || ".";
137
+ p.note(
138
+ [`cd ${relDir}`, "go mod tidy", "npm install", "npm run dev"].join("\n"),
139
+ "Next steps"
140
+ );
141
+ p.outro("Happy hacking!");
142
+ }
143
+ main().catch((err) => {
144
+ console.error(err);
145
+ process.exit(1);
146
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "create-shiftapi",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a new ShiftAPI fullstack app",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-shiftapi": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm",
15
+ "test": "vitest run"
16
+ },
17
+ "dependencies": {
18
+ "@clack/prompts": "^1.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^25.2.3",
22
+ "tsup": "^8.0.0",
23
+ "typescript": "^5.5.0",
24
+ "vitest": "^2.0.0"
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ PORT={{port}}
@@ -0,0 +1,32 @@
1
+ # {{name}}
2
+
3
+ Built with [ShiftAPI](https://github.com/fcjr/shiftapi) — Go server + typed TypeScript frontend.
4
+
5
+ ## Getting Started
6
+
7
+ ```bash
8
+ go mod tidy
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ This starts the Go server and Vite dev server together. The frontend gets fully typed API clients generated from your Go handlers.
14
+
15
+ - API docs: http://localhost:{{port}}/docs
16
+ - Frontend: http://localhost:5173
17
+
18
+ ## Project Structure
19
+
20
+ ```
21
+ cmd/{{name}}/main.go # Go entry point
22
+ internal/server/server.go # API routes and handlers
23
+ go.mod
24
+ .env # Environment variables (PORT)
25
+ apps/web/ # Frontend (Vite + TypeScript)
26
+ ```
27
+
28
+ ## Scripts
29
+
30
+ ```bash
31
+ npm run dev # Start Go server + Vite dev server
32
+ ```
@@ -0,0 +1,22 @@
1
+ # Go
2
+ *.exe
3
+ *.exe~
4
+ *.dll
5
+ *.so
6
+ *.dylib
7
+ *.test
8
+ *.out
9
+
10
+ # Node
11
+ node_modules/
12
+ dist/
13
+ .turbo/
14
+
15
+ # Env
16
+ .env
17
+
18
+ # IDE
19
+ .DS_Store
20
+ *.log
21
+ .vscode/
22
+ .idea/
@@ -0,0 +1,41 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "io"
7
+ "log/slog"
8
+ "os"
9
+ "os/signal"
10
+
11
+ "github.com/joho/godotenv"
12
+
13
+ "{{modulePath}}/internal/server"
14
+ )
15
+
16
+ func main() {
17
+ ctx := context.Background()
18
+
19
+ if err := godotenv.Load(); err != nil {
20
+ slog.Warn("error loading .env file, skipping", "error", err)
21
+ }
22
+
23
+ if err := run(ctx, os.Args, os.Getenv, os.Stdin, os.Stdout, os.Stderr); err != nil {
24
+ fmt.Fprintf(os.Stderr, "%v\n", err)
25
+ os.Exit(1)
26
+ }
27
+ }
28
+
29
+ func run(ctx context.Context, args []string, getenv func(string) string, stdin io.Reader, stdout, stderr io.Writer) error {
30
+ ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
31
+ defer cancel()
32
+
33
+ port := getenv("PORT")
34
+ if port == "" {
35
+ port = "{{port}}"
36
+ }
37
+
38
+ slog.Info("starting server", "port", port)
39
+ // docs at http://localhost:{{port}}/docs
40
+ return server.ListenAndServe(ctx, ":"+port)
41
+ }
@@ -0,0 +1,5 @@
1
+ module {{modulePath}}
2
+
3
+ go 1.24.0
4
+
5
+ require github.com/fcjr/shiftapi v{{version}}
@@ -0,0 +1,51 @@
1
+ package server
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+
7
+ "github.com/fcjr/shiftapi"
8
+ )
9
+
10
+ type EchoRequest struct {
11
+ Message string `json:"message" validate:"required"`
12
+ }
13
+
14
+ type EchoResponse struct {
15
+ Message string `json:"message"`
16
+ }
17
+
18
+ func echo(r *http.Request, body *EchoRequest) (*EchoResponse, error) {
19
+ return &EchoResponse{Message: body.Message}, nil
20
+ }
21
+
22
+ type Status struct {
23
+ OK bool `json:"ok"`
24
+ }
25
+
26
+ func health(r *http.Request) (*Status, error) {
27
+ return &Status{OK: true}, nil
28
+ }
29
+
30
+ func ListenAndServe(ctx context.Context, addr string) error {
31
+ api := shiftapi.New(shiftapi.WithInfo(shiftapi.Info{
32
+ Title: "{{name}}",
33
+ }))
34
+
35
+ shiftapi.Post(api, "/echo", echo,
36
+ shiftapi.WithRouteInfo(shiftapi.RouteInfo{
37
+ Summary: "Echo a message",
38
+ Description: "Returns the message you send",
39
+ Tags: []string{"echo"},
40
+ }),
41
+ )
42
+
43
+ shiftapi.Get(api, "/health", health,
44
+ shiftapi.WithRouteInfo(shiftapi.RouteInfo{
45
+ Summary: "Health check",
46
+ Tags: []string{"health"},
47
+ }),
48
+ )
49
+
50
+ return shiftapi.ListenAndServe(addr, api)
51
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "npm run dev --workspace apps/web"
6
+ },
7
+ "workspaces": [
8
+ "apps/*"
9
+ ]
10
+ }
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@{{name}}/web",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc --noEmit && vite build",
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "dependencies": {
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "@shiftapi/vite-plugin": "^{{version}}",
16
+ "@types/react": "^19.0.0",
17
+ "@types/react-dom": "^19.0.0",
18
+ "@vitejs/plugin-react": "^4.0.0",
19
+ "typescript": "^5.5.0",
20
+ "vite": "^6.0.0"
21
+ }
22
+ }
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { FormEvent } from "react";
3
+ import { client } from "@shiftapi/client";
4
+
5
+ export default function App() {
6
+ const [output, setOutput] = useState("");
7
+
8
+ useEffect(() => {
9
+ client.GET("/health").then(({ error }) => {
10
+ if (error) {
11
+ setOutput(`Health check failed: ${error.message}`);
12
+ } else {
13
+ setOutput("Health check passed. Try sending a message.");
14
+ }
15
+ });
16
+ }, []);
17
+
18
+ async function handleSubmit(e: FormEvent<HTMLFormElement>) {
19
+ e.preventDefault();
20
+ const formData = new FormData(e.currentTarget);
21
+ const message = (formData.get("message") as string).trim();
22
+ if (!message) return;
23
+ setOutput("Loading...");
24
+ const { data, error } = await client.POST("/echo", { body: { message } });
25
+ if (error) {
26
+ setOutput(`Error: ${error.message}`);
27
+ } else {
28
+ setOutput(`Echo: ${data.message}`);
29
+ }
30
+ }
31
+
32
+ return (
33
+ <div>
34
+ <h1>{{name}}</h1>
35
+ <form onSubmit={handleSubmit}>
36
+ <input type="text" name="message" placeholder="Enter a message" />
37
+ <button type="submit">Send</button>
38
+ </form>
39
+ <pre>{output}</pre>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App";
4
+
5
+ createRoot(document.getElementById("root")!).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>,
9
+ );
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "jsx": "react-jsx",
10
+ "paths": {
11
+ "@shiftapi/client": [
12
+ "./node_modules/.shiftapi/client.d.ts"
13
+ ]
14
+ }
15
+ },
16
+ "include": [
17
+ "src"
18
+ ]
19
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import shiftapi from "@shiftapi/vite-plugin";
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ react(),
8
+ shiftapi({
9
+ server: "./cmd/{{name}}",
10
+ goRoot: "../..",
11
+ }),
12
+ ],
13
+ });
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@{{name}}/web",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "tsc --noEmit && vite build",
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "dependencies": {
11
+ "svelte": "^5.0.0"
12
+ },
13
+ "devDependencies": {
14
+ "@shiftapi/vite-plugin": "^{{version}}",
15
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
16
+ "typescript": "^5.5.0",
17
+ "vite": "^6.0.0"
18
+ }
19
+ }
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { client } from "@shiftapi/client";
3
+
4
+ let output = $state("");
5
+
6
+ $effect(() => {
7
+ client.GET("/health").then(({ error }) => {
8
+ if (error) {
9
+ output = `Health check failed: ${error.message}`;
10
+ } else {
11
+ output = "Health check passed. Try sending a message.";
12
+ }
13
+ });
14
+ });
15
+
16
+ async function handleSubmit(e: SubmitEvent) {
17
+ e.preventDefault();
18
+ const formData = new FormData(e.currentTarget as HTMLFormElement);
19
+ const message = (formData.get("message") as string).trim();
20
+ if (!message) return;
21
+ output = "Loading...";
22
+ const { data, error } = await client.POST("/echo", { body: { message } });
23
+ if (error) {
24
+ output = `Error: ${error.message}`;
25
+ } else {
26
+ output = `Echo: ${data.message}`;
27
+ }
28
+ }
29
+ </script>
30
+
31
+ <div>
32
+ <h1>{{name}}</h1>
33
+ <form onsubmit={handleSubmit}>
34
+ <input type="text" name="message" placeholder="Enter a message" />
35
+ <button type="submit">Send</button>
36
+ </form>
37
+ <pre>{output}</pre>
38
+ </div>
@@ -0,0 +1,4 @@
1
+ import { mount } from "svelte";
2
+ import App from "./App.svelte";
3
+
4
+ mount(App, { target: document.getElementById("app")! });
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "paths": {
10
+ "@shiftapi/client": [
11
+ "./node_modules/.shiftapi/client.d.ts"
12
+ ]
13
+ }
14
+ },
15
+ "include": [
16
+ "src"
17
+ ]
18
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vite";
2
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
3
+ import shiftapi from "@shiftapi/vite-plugin";
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ svelte(),
8
+ shiftapi({
9
+ server: "./cmd/{{name}}",
10
+ goRoot: "../..",
11
+ }),
12
+ ],
13
+ });