create-githat-app 1.0.9 → 1.0.11
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 +13 -18
- package/dist/cli.js +265 -180
- package/package.json +1 -1
- package/templates/base/.env.local.example.hbs +20 -0
- package/templates/base/README.md.hbs +31 -52
- package/templates/fullstack/apps-api-express/package.json.hbs +1 -1
- package/templates/fullstack/apps-api-fastify/package.json.hbs +1 -1
- package/templates/fullstack/apps-api-hono/package.json.hbs +1 -1
- package/templates/fullstack/apps-web-nextjs/app/layout.tsx.hbs +5 -0
- package/templates/fullstack/apps-web-nextjs/package.json.hbs +1 -1
- package/templates/nextjs/.github/workflows/deploy.yml.hbs +107 -0
- package/templates/nextjs/app/layout.tsx.hbs +5 -0
- package/templates/nextjs/next.config.ts.hbs +3 -1
- package/templates/react-vite/src/App.tsx.hbs +16 -9
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
Scaffold a production-ready app with a fully-managed backend — auth, teams, orgs, API keys,
|
|
17
|
+
Scaffold a production-ready app with a fully-managed backend — auth, teams, orgs, API keys, and AI agent identity. **No backend to deploy.**
|
|
18
18
|
|
|
19
|
-
[GitHat](https://githat.io) is your backend. When you run `create-githat-app`, your generated project connects to GitHat's hosted platform at `api.githat.io`. User accounts, organizations, teams, API keys,
|
|
19
|
+
[GitHat](https://githat.io) is your backend. When you run `create-githat-app`, your generated project connects to GitHat's hosted platform at `api.githat.io`. User accounts, organizations, teams, API keys, and AI agents are all stored and managed by GitHat. You write frontend code only.
|
|
20
20
|
|
|
21
21
|
## Install & Launch
|
|
22
22
|
|
|
@@ -75,7 +75,6 @@ The CLI asks you a series of questions:
|
|
|
75
75
|
│ ◻ Forgot password
|
|
76
76
|
│ ◻ Email verification
|
|
77
77
|
│ ◻ Organization management
|
|
78
|
-
│ ◻ MCP server identity
|
|
79
78
|
│ ◻ AI agent identity
|
|
80
79
|
│
|
|
81
80
|
◆ Database?
|
|
@@ -146,11 +145,11 @@ npx create-githat-app --version
|
|
|
146
145
|
|
|
147
146
|
A production-ready project connected to GitHat's hosted backend:
|
|
148
147
|
|
|
149
|
-
- **Fully-managed backend** — `api.githat.io` handles auth, orgs, teams, API keys, agents
|
|
148
|
+
- **Fully-managed backend** — `api.githat.io` handles auth, orgs, teams, API keys, and agents
|
|
150
149
|
- **Auth pages** — sign-in, sign-up, forgot password, email verification
|
|
151
150
|
- **Protected dashboard** — sidebar navigation, org switcher, user button
|
|
152
151
|
- **`@githat/nextjs` SDK** — `<GitHatProvider>`, `<ProtectedRoute>`, `useAuth()`
|
|
153
|
-
- **`githat/` platform folder** — typed API client for
|
|
152
|
+
- **`githat/` platform folder** — typed API client for the full REST API
|
|
154
153
|
- **Dark theme** — zinc/purple design system out of the box
|
|
155
154
|
- **Database ready** — Prisma or Drizzle for your app's own data (optional)
|
|
156
155
|
- **Tailwind CSS 4** — utility-first styling (optional)
|
|
@@ -168,7 +167,6 @@ githat/
|
|
|
168
167
|
auth.ts # Login, register, forgot password, verify email
|
|
169
168
|
orgs.ts # Create/update orgs, invite/remove members
|
|
170
169
|
users.ts # List orgs, switch org
|
|
171
|
-
mcp.ts # Register/verify MCP servers
|
|
172
170
|
agents.ts # Register/verify AI agent wallets
|
|
173
171
|
auth/
|
|
174
172
|
guard.tsx # Role-based route protection component
|
|
@@ -179,7 +177,6 @@ githat/
|
|
|
179
177
|
apps.tsx # App + API key management
|
|
180
178
|
members.tsx # Invite members, manage roles, remove
|
|
181
179
|
settings.tsx # Edit org name, brand color, save
|
|
182
|
-
mcp-servers.tsx # Register MCP servers, verify, remove
|
|
183
180
|
agents.tsx # Register AI agent wallets, verify, remove
|
|
184
181
|
```
|
|
185
182
|
|
|
@@ -193,23 +190,21 @@ You don't deploy or maintain a backend. GitHat's hosted platform handles:
|
|
|
193
190
|
- **Organizations** — create, switch, branding, custom domains
|
|
194
191
|
- **Team management** — invite members by email, assign roles (owner/admin/member), remove
|
|
195
192
|
- **API key management** — publishable + secret keys per app, rotation
|
|
196
|
-
- **MCP server registration** — domain verification via DNS TXT, OAuth2 credentials
|
|
197
193
|
- **AI agent registration** — Ethereum wallet verification, challenge-response tokens
|
|
198
194
|
- **Email delivery** — verification emails, invitations, password resets (via AWS SES)
|
|
199
|
-
- **Database** — users, orgs, teams, apps, agents
|
|
200
|
-
- **Public verification** — anyone can verify an agent
|
|
195
|
+
- **Database** — users, orgs, teams, apps, and agents (DynamoDB, managed by GitHat)
|
|
196
|
+
- **Public verification** — anyone can verify an agent at `githat.io/verify/`
|
|
201
197
|
|
|
202
198
|
Your data lives in GitHat's infrastructure. You write frontend code, GitHat handles the rest.
|
|
203
199
|
|
|
204
|
-
##
|
|
200
|
+
## Two Identity Types
|
|
205
201
|
|
|
206
|
-
GitHat supports
|
|
202
|
+
GitHat supports two types of identity in a single platform:
|
|
207
203
|
|
|
208
|
-
| Type
|
|
209
|
-
|
|
|
210
|
-
| **Humans**
|
|
211
|
-
| **
|
|
212
|
-
| **AI Agents** | Ethereum wallet signatures | AI Agents |
|
|
204
|
+
| Type | Auth Method | Dashboard Page |
|
|
205
|
+
| ------------- | -------------------------- | -------------- |
|
|
206
|
+
| **Humans** | Email + password | Members |
|
|
207
|
+
| **AI Agents** | Ethereum wallet signatures | AI Agents |
|
|
213
208
|
|
|
214
209
|
## Project Structure
|
|
215
210
|
|
|
@@ -281,7 +276,7 @@ VITE_GITHAT_API_URL=https://api.githat.io
|
|
|
281
276
|
|
|
282
277
|
- [GitHat Quick Start](https://githat.io/docs/quickstart) — Get running in 5 minutes
|
|
283
278
|
- [SDK Reference](https://githat.io/docs/sdk) — `@githat/nextjs` hooks, components, and server utils
|
|
284
|
-
- [API Reference](https://githat.io/docs/api) —
|
|
279
|
+
- [API Reference](https://githat.io/docs/api) — Full REST API reference
|
|
285
280
|
- [CLI Reference](https://githat.io/docs/cli) — Flags, prompts, and templates
|
|
286
281
|
- [Next.js Guide](https://githat.io/docs/nextjs) — Auth in Next.js 14-16+
|
|
287
282
|
- [React Guide](https://githat.io/docs/react) — Auth in any React app
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createRequire } from 'module'; const require = createRequire(import.met
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { Command as Command7 } from "commander";
|
|
5
|
-
import * as
|
|
6
|
-
import
|
|
5
|
+
import * as p12 from "@clack/prompts";
|
|
6
|
+
import chalk10 from "chalk";
|
|
7
7
|
|
|
8
8
|
// src/utils/ascii.ts
|
|
9
9
|
import figlet from "figlet";
|
|
@@ -11,7 +11,7 @@ import gradient from "gradient-string";
|
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
|
|
13
13
|
// src/constants.ts
|
|
14
|
-
var VERSION = "1.0.
|
|
14
|
+
var VERSION = "1.0.11";
|
|
15
15
|
var DEFAULT_API_URL = "https://api.githat.io";
|
|
16
16
|
var DASHBOARD_URL = "https://githat.io/dashboard/apps";
|
|
17
17
|
var BRAND_COLORS = ["#7c3aed", "#6366f1", "#8b5cf6"];
|
|
@@ -21,7 +21,7 @@ var DEPS = {
|
|
|
21
21
|
next: "^16.0.0",
|
|
22
22
|
react: "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0",
|
|
24
|
-
"@githat/nextjs": "^0.
|
|
24
|
+
"@githat/nextjs": "^0.5.0"
|
|
25
25
|
},
|
|
26
26
|
devDependencies: {
|
|
27
27
|
typescript: "^5.9.0",
|
|
@@ -35,7 +35,7 @@ var DEPS = {
|
|
|
35
35
|
react: "^19.0.0",
|
|
36
36
|
"react-dom": "^19.0.0",
|
|
37
37
|
"react-router-dom": "^7.0.0",
|
|
38
|
-
"@githat/nextjs": "^0.
|
|
38
|
+
"@githat/nextjs": "^0.5.0"
|
|
39
39
|
},
|
|
40
40
|
devDependencies: {
|
|
41
41
|
vite: "^7.0.0",
|
|
@@ -657,11 +657,11 @@ function answersToContext(answers) {
|
|
|
657
657
|
}
|
|
658
658
|
|
|
659
659
|
// src/scaffold/index.ts
|
|
660
|
-
import
|
|
661
|
-
import
|
|
660
|
+
import fs4 from "fs-extra";
|
|
661
|
+
import path4 from "path";
|
|
662
662
|
import { execSync as execSync3 } from "child_process";
|
|
663
|
-
import * as
|
|
664
|
-
import
|
|
663
|
+
import * as p10 from "@clack/prompts";
|
|
664
|
+
import chalk3 from "chalk";
|
|
665
665
|
|
|
666
666
|
// src/utils/template-engine.ts
|
|
667
667
|
import Handlebars from "handlebars";
|
|
@@ -809,27 +809,109 @@ function initGit(cwd) {
|
|
|
809
809
|
}
|
|
810
810
|
}
|
|
811
811
|
|
|
812
|
+
// src/utils/register-app.ts
|
|
813
|
+
import fs3 from "fs-extra";
|
|
814
|
+
import path3 from "path";
|
|
815
|
+
import os from "os";
|
|
816
|
+
import * as p9 from "@clack/prompts";
|
|
817
|
+
import chalk2 from "chalk";
|
|
818
|
+
var CREDENTIALS_PATH = path3.join(os.homedir(), ".githat", "credentials.json");
|
|
819
|
+
function readToken() {
|
|
820
|
+
if (!fs3.existsSync(CREDENTIALS_PATH)) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const creds = fs3.readJsonSync(CREDENTIALS_PATH);
|
|
825
|
+
return creds.token || null;
|
|
826
|
+
} catch {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function registerApp(appName, projectRoot) {
|
|
831
|
+
const token = readToken();
|
|
832
|
+
if (!token) {
|
|
833
|
+
p9.log.warn(
|
|
834
|
+
chalk2.yellow(
|
|
835
|
+
`GitHat credentials not found at ${CREDENTIALS_PATH}.
|
|
836
|
+
Run ${chalk2.cyan("githat login")} then re-run scaffolding, or paste your publishable key
|
|
837
|
+
from ${chalk2.cyan("https://githat.io/dashboard/apps")} into .env.local manually.`
|
|
838
|
+
)
|
|
839
|
+
);
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
p9.log.step("Registering app on GitHat...");
|
|
843
|
+
let registeredApp;
|
|
844
|
+
try {
|
|
845
|
+
const response = await fetch(`${DEFAULT_API_URL}/apps`, {
|
|
846
|
+
method: "POST",
|
|
847
|
+
headers: {
|
|
848
|
+
"Content-Type": "application/json",
|
|
849
|
+
Authorization: `Bearer ${token}`
|
|
850
|
+
},
|
|
851
|
+
body: JSON.stringify({
|
|
852
|
+
name: appName,
|
|
853
|
+
redirect_uris: ["http://localhost:3000/callback"]
|
|
854
|
+
})
|
|
855
|
+
});
|
|
856
|
+
if (!response.ok) {
|
|
857
|
+
const body = await response.text().catch(() => "");
|
|
858
|
+
throw new Error(`HTTP ${response.status}: ${body}`);
|
|
859
|
+
}
|
|
860
|
+
registeredApp = await response.json();
|
|
861
|
+
} catch (err) {
|
|
862
|
+
p9.log.warn(
|
|
863
|
+
chalk2.yellow(
|
|
864
|
+
`Could not register app on GitHat: ${err.message}
|
|
865
|
+
The scaffold was written successfully. Once the API is available,
|
|
866
|
+
register manually at ${chalk2.cyan("https://githat.io/dashboard/apps")} and paste the
|
|
867
|
+
publishable key into ${chalk2.cyan(".env.local")}.`
|
|
868
|
+
)
|
|
869
|
+
);
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const publishableKey = registeredApp.publishable_key;
|
|
873
|
+
const envLocalPath = path3.join(projectRoot, ".env.local");
|
|
874
|
+
try {
|
|
875
|
+
let envContent = fs3.existsSync(envLocalPath) ? fs3.readFileSync(envLocalPath, "utf-8") : "";
|
|
876
|
+
const keyLine = `NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=${publishableKey}`;
|
|
877
|
+
if (envContent.includes("NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=")) {
|
|
878
|
+
envContent = envContent.replace(
|
|
879
|
+
/NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=.*/,
|
|
880
|
+
keyLine
|
|
881
|
+
);
|
|
882
|
+
} else {
|
|
883
|
+
envContent += (envContent.endsWith("\n") || envContent === "" ? "" : "\n") + keyLine + "\n";
|
|
884
|
+
}
|
|
885
|
+
fs3.ensureDirSync(path3.dirname(envLocalPath));
|
|
886
|
+
fs3.writeFileSync(envLocalPath, envContent, "utf-8");
|
|
887
|
+
} catch (err) {
|
|
888
|
+
p9.log.warn(chalk2.yellow(`Registered app but could not write .env.local: ${err.message}`));
|
|
889
|
+
}
|
|
890
|
+
p9.log.success(chalk2.green(`Registered "${appName}" on GitHat. Publishable key written to .env.local.`));
|
|
891
|
+
return publishableKey;
|
|
892
|
+
}
|
|
893
|
+
|
|
812
894
|
// src/scaffold/index.ts
|
|
813
895
|
async function scaffold(context, options) {
|
|
814
|
-
const root =
|
|
815
|
-
if (
|
|
816
|
-
|
|
896
|
+
const root = path4.resolve(process.cwd(), context.projectName);
|
|
897
|
+
if (fs4.existsSync(root)) {
|
|
898
|
+
p10.cancel(`Directory "${context.projectName}" already exists.`);
|
|
817
899
|
process.exit(1);
|
|
818
900
|
}
|
|
819
901
|
const isFullstack = context.projectType === "fullstack";
|
|
820
902
|
await withSpinner("Creating project structure...", async () => {
|
|
821
|
-
|
|
903
|
+
fs4.ensureDirSync(root);
|
|
822
904
|
const templatesRoot = getTemplatesRoot();
|
|
823
905
|
if (isFullstack) {
|
|
824
906
|
scaffoldFullstack(templatesRoot, root, context);
|
|
825
907
|
} else {
|
|
826
|
-
const frameworkDir =
|
|
827
|
-
if (!
|
|
908
|
+
const frameworkDir = path4.join(templatesRoot, context.framework);
|
|
909
|
+
if (!fs4.existsSync(frameworkDir)) {
|
|
828
910
|
throw new Error(`Templates not found at ${frameworkDir}. This is a bug \u2014 please report it.`);
|
|
829
911
|
}
|
|
830
912
|
renderTemplateDirectory(frameworkDir, root, context);
|
|
831
|
-
const baseDir =
|
|
832
|
-
if (
|
|
913
|
+
const baseDir = path4.join(templatesRoot, "base");
|
|
914
|
+
if (fs4.existsSync(baseDir)) {
|
|
833
915
|
renderTemplateDirectory(baseDir, root, context);
|
|
834
916
|
}
|
|
835
917
|
}
|
|
@@ -840,6 +922,9 @@ async function scaffold(context, options) {
|
|
|
840
922
|
writeJson(root, "package.json", pkg);
|
|
841
923
|
}, "package.json generated");
|
|
842
924
|
}
|
|
925
|
+
if (!isFullstack && context.framework === "nextjs") {
|
|
926
|
+
await registerApp(context.projectName, root);
|
|
927
|
+
}
|
|
843
928
|
if (options.initGit) {
|
|
844
929
|
const gitSpinner = createSpinner("Initializing git repository...");
|
|
845
930
|
gitSpinner.start();
|
|
@@ -860,69 +945,69 @@ async function scaffold(context, options) {
|
|
|
860
945
|
} catch (err) {
|
|
861
946
|
const msg = err.message || "";
|
|
862
947
|
if (msg.includes("TIMEOUT")) {
|
|
863
|
-
|
|
948
|
+
p10.log.warn(`Install timed out. Run ${chalk3.cyan(installCmd)} manually.`);
|
|
864
949
|
} else {
|
|
865
|
-
|
|
950
|
+
p10.log.warn(`Could not auto-install. Run ${chalk3.cyan(installCmd)} manually.`);
|
|
866
951
|
}
|
|
867
952
|
}
|
|
868
953
|
},
|
|
869
954
|
"Dependencies installed"
|
|
870
955
|
);
|
|
871
956
|
}
|
|
872
|
-
|
|
957
|
+
p10.outro("Setup complete!");
|
|
873
958
|
displaySuccess(context.projectName, context.packageManager, context.framework, !!context.publishableKey, isFullstack);
|
|
874
959
|
if (!options.skipPrompts) {
|
|
875
|
-
const starPrompt = await
|
|
960
|
+
const starPrompt = await p10.confirm({
|
|
876
961
|
message: "Star GitHat on GitHub? (helps us grow!)",
|
|
877
962
|
initialValue: false
|
|
878
963
|
});
|
|
879
|
-
if (!
|
|
964
|
+
if (!p10.isCancel(starPrompt) && starPrompt) {
|
|
880
965
|
try {
|
|
881
966
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
882
967
|
execSync3(`${cmd} "https://github.com/GitHat-IO/githat"`, { stdio: "ignore" });
|
|
883
968
|
} catch {
|
|
884
|
-
|
|
969
|
+
p10.log.info("Visit https://github.com/GitHat-IO/githat to star us!");
|
|
885
970
|
}
|
|
886
971
|
}
|
|
887
972
|
}
|
|
888
973
|
}
|
|
889
974
|
function scaffoldFullstack(templatesRoot, root, context) {
|
|
890
|
-
const fullstackDir =
|
|
891
|
-
const rootDir =
|
|
892
|
-
if (
|
|
975
|
+
const fullstackDir = path4.join(templatesRoot, "fullstack");
|
|
976
|
+
const rootDir = path4.join(fullstackDir, "root");
|
|
977
|
+
if (fs4.existsSync(rootDir)) {
|
|
893
978
|
renderTemplateDirectory(rootDir, root, context);
|
|
894
979
|
}
|
|
895
|
-
const appsDir =
|
|
896
|
-
|
|
897
|
-
const webDir =
|
|
898
|
-
|
|
899
|
-
const webTemplateDir =
|
|
900
|
-
if (
|
|
980
|
+
const appsDir = path4.join(root, "apps");
|
|
981
|
+
fs4.ensureDirSync(appsDir);
|
|
982
|
+
const webDir = path4.join(appsDir, "web");
|
|
983
|
+
fs4.ensureDirSync(webDir);
|
|
984
|
+
const webTemplateDir = path4.join(fullstackDir, `apps-web-${context.framework}`);
|
|
985
|
+
if (fs4.existsSync(webTemplateDir)) {
|
|
901
986
|
renderTemplateDirectory(webTemplateDir, webDir, context);
|
|
902
987
|
} else {
|
|
903
988
|
throw new Error(`Web app templates not found at ${webTemplateDir}. This is a bug \u2014 please report it.`);
|
|
904
989
|
}
|
|
905
|
-
const apiDir =
|
|
906
|
-
|
|
990
|
+
const apiDir = path4.join(appsDir, "api");
|
|
991
|
+
fs4.ensureDirSync(apiDir);
|
|
907
992
|
const backendFramework = context.backendFramework || "hono";
|
|
908
|
-
const apiTemplateDir =
|
|
909
|
-
if (
|
|
993
|
+
const apiTemplateDir = path4.join(fullstackDir, `apps-api-${backendFramework}`);
|
|
994
|
+
if (fs4.existsSync(apiTemplateDir)) {
|
|
910
995
|
renderTemplateDirectory(apiTemplateDir, apiDir, context);
|
|
911
996
|
} else {
|
|
912
997
|
throw new Error(`API templates not found at ${apiTemplateDir}. This is a bug \u2014 please report it.`);
|
|
913
998
|
}
|
|
914
|
-
const packagesDir =
|
|
915
|
-
|
|
916
|
-
|
|
999
|
+
const packagesDir = path4.join(root, "packages");
|
|
1000
|
+
fs4.ensureDirSync(packagesDir);
|
|
1001
|
+
fs4.writeFileSync(path4.join(packagesDir, ".gitkeep"), "");
|
|
917
1002
|
}
|
|
918
1003
|
|
|
919
1004
|
// src/commands/skills/index.ts
|
|
920
1005
|
import { Command as Command6 } from "commander";
|
|
921
|
-
import
|
|
1006
|
+
import chalk9 from "chalk";
|
|
922
1007
|
|
|
923
1008
|
// src/commands/skills/search.ts
|
|
924
1009
|
import { Command } from "commander";
|
|
925
|
-
import
|
|
1010
|
+
import chalk4 from "chalk";
|
|
926
1011
|
|
|
927
1012
|
// src/commands/skills/api.ts
|
|
928
1013
|
async function fetchApi(endpoint, options = {}) {
|
|
@@ -970,99 +1055,99 @@ async function getDownloadUrl(slug, version) {
|
|
|
970
1055
|
// src/commands/skills/search.ts
|
|
971
1056
|
function formatSkill(skill) {
|
|
972
1057
|
const typeColors = {
|
|
973
|
-
template:
|
|
974
|
-
integration:
|
|
975
|
-
ui:
|
|
976
|
-
ai:
|
|
977
|
-
workflow:
|
|
1058
|
+
template: chalk4.blue,
|
|
1059
|
+
integration: chalk4.green,
|
|
1060
|
+
ui: chalk4.magenta,
|
|
1061
|
+
ai: chalk4.yellow,
|
|
1062
|
+
workflow: chalk4.cyan
|
|
978
1063
|
};
|
|
979
|
-
const typeColor = typeColors[skill.type] ||
|
|
1064
|
+
const typeColor = typeColors[skill.type] || chalk4.white;
|
|
980
1065
|
return [
|
|
981
|
-
`${
|
|
1066
|
+
`${chalk4.bold(skill.name)} ${chalk4.dim(`@${skill.latestVersion}`)}`,
|
|
982
1067
|
` ${skill.description}`,
|
|
983
1068
|
` ${typeColor(skill.type)} \xB7 \u2B07 ${skill.downloads} \xB7 \u2B50 ${skill.stars} \xB7 by ${skill.authorName}`,
|
|
984
|
-
` ${
|
|
1069
|
+
` ${chalk4.dim(`githat skills install ${skill.slug}`)}`
|
|
985
1070
|
].join("\n");
|
|
986
1071
|
}
|
|
987
1072
|
var searchCommand = new Command("search").description("Search skills by keyword").argument("<query>", "Search query").option("-t, --type <type>", "Filter by type (template, integration, ui, ai, workflow)").action(async (query, options) => {
|
|
988
1073
|
try {
|
|
989
|
-
console.log(
|
|
1074
|
+
console.log(chalk4.dim(`
|
|
990
1075
|
Searching for "${query}"...
|
|
991
1076
|
`));
|
|
992
1077
|
const result = await searchSkills(query, options.type);
|
|
993
1078
|
if (result.skills.length === 0) {
|
|
994
|
-
console.log(
|
|
995
|
-
console.log(
|
|
996
|
-
console.log(
|
|
1079
|
+
console.log(chalk4.yellow("No skills found matching your query."));
|
|
1080
|
+
console.log(chalk4.dim("\nTry a different search term or browse all skills:"));
|
|
1081
|
+
console.log(chalk4.dim(" githat skills list"));
|
|
997
1082
|
return;
|
|
998
1083
|
}
|
|
999
|
-
console.log(
|
|
1084
|
+
console.log(chalk4.cyan(`Found ${result.skills.length} skill(s):
|
|
1000
1085
|
`));
|
|
1001
1086
|
for (const skill of result.skills) {
|
|
1002
1087
|
console.log(formatSkill(skill));
|
|
1003
1088
|
console.log("");
|
|
1004
1089
|
}
|
|
1005
1090
|
} catch (err) {
|
|
1006
|
-
console.error(
|
|
1091
|
+
console.error(chalk4.red(`Error: ${err.message}`));
|
|
1007
1092
|
process.exit(1);
|
|
1008
1093
|
}
|
|
1009
1094
|
});
|
|
1010
1095
|
|
|
1011
1096
|
// src/commands/skills/list.ts
|
|
1012
1097
|
import { Command as Command2 } from "commander";
|
|
1013
|
-
import
|
|
1098
|
+
import chalk5 from "chalk";
|
|
1014
1099
|
function formatSkillCompact(skill) {
|
|
1015
1100
|
const typeColors = {
|
|
1016
|
-
template:
|
|
1017
|
-
integration:
|
|
1018
|
-
ui:
|
|
1019
|
-
ai:
|
|
1020
|
-
workflow:
|
|
1101
|
+
template: chalk5.blue,
|
|
1102
|
+
integration: chalk5.green,
|
|
1103
|
+
ui: chalk5.magenta,
|
|
1104
|
+
ai: chalk5.yellow,
|
|
1105
|
+
workflow: chalk5.cyan
|
|
1021
1106
|
};
|
|
1022
|
-
const typeColor = typeColors[skill.type] ||
|
|
1023
|
-
const name =
|
|
1107
|
+
const typeColor = typeColors[skill.type] || chalk5.white;
|
|
1108
|
+
const name = chalk5.bold(skill.name.padEnd(25));
|
|
1024
1109
|
const type = typeColor(skill.type.padEnd(12));
|
|
1025
1110
|
const stats = `\u2B07 ${String(skill.downloads).padStart(5)} \u2B50 ${String(skill.stars).padStart(4)}`;
|
|
1026
1111
|
const desc = skill.description.length > 40 ? skill.description.substring(0, 37) + "..." : skill.description;
|
|
1027
|
-
return `${name} ${type} ${stats} ${
|
|
1112
|
+
return `${name} ${type} ${stats} ${chalk5.dim(desc)}`;
|
|
1028
1113
|
}
|
|
1029
1114
|
var listCommand = new Command2("list").description("List available skills").option("-t, --type <type>", "Filter by type (template, integration, ui, ai, workflow)").option("-l, --limit <n>", "Number of results (default: 25)", "25").action(async (options) => {
|
|
1030
1115
|
try {
|
|
1031
1116
|
const limit = parseInt(options.limit, 10);
|
|
1032
|
-
console.log(
|
|
1117
|
+
console.log(chalk5.dim("\nFetching skills...\n"));
|
|
1033
1118
|
const result = await listSkills({ type: options.type, limit });
|
|
1034
1119
|
if (result.skills.length === 0) {
|
|
1035
|
-
console.log(
|
|
1120
|
+
console.log(chalk5.yellow("No skills found."));
|
|
1036
1121
|
if (options.type) {
|
|
1037
|
-
console.log(
|
|
1122
|
+
console.log(chalk5.dim(`
|
|
1038
1123
|
Try without the type filter:`));
|
|
1039
|
-
console.log(
|
|
1124
|
+
console.log(chalk5.dim(" githat skills list"));
|
|
1040
1125
|
}
|
|
1041
1126
|
return;
|
|
1042
1127
|
}
|
|
1043
1128
|
const header = `${"NAME".padEnd(25)} ${"TYPE".padEnd(12)} ${"DOWNLOADS".padStart(10)} DESCRIPTION`;
|
|
1044
|
-
console.log(
|
|
1045
|
-
console.log(
|
|
1129
|
+
console.log(chalk5.dim(header));
|
|
1130
|
+
console.log(chalk5.dim("\u2500".repeat(80)));
|
|
1046
1131
|
for (const skill of result.skills) {
|
|
1047
1132
|
console.log(formatSkillCompact(skill));
|
|
1048
1133
|
}
|
|
1049
|
-
console.log(
|
|
1050
|
-
console.log(
|
|
1134
|
+
console.log(chalk5.dim("\u2500".repeat(80)));
|
|
1135
|
+
console.log(chalk5.dim(`Showing ${result.skills.length} skill(s)`));
|
|
1051
1136
|
if (result.nextCursor) {
|
|
1052
|
-
console.log(
|
|
1137
|
+
console.log(chalk5.dim("\nMore results available. Use --limit to see more."));
|
|
1053
1138
|
}
|
|
1054
|
-
console.log(
|
|
1139
|
+
console.log(chalk5.dim("\nTo install: githat skills install <name>"));
|
|
1055
1140
|
} catch (err) {
|
|
1056
|
-
console.error(
|
|
1141
|
+
console.error(chalk5.red(`Error: ${err.message}`));
|
|
1057
1142
|
process.exit(1);
|
|
1058
1143
|
}
|
|
1059
1144
|
});
|
|
1060
1145
|
|
|
1061
1146
|
// src/commands/skills/install.ts
|
|
1062
1147
|
import { Command as Command3 } from "commander";
|
|
1063
|
-
import
|
|
1064
|
-
import * as
|
|
1065
|
-
import * as
|
|
1148
|
+
import chalk6 from "chalk";
|
|
1149
|
+
import * as fs5 from "fs";
|
|
1150
|
+
import * as path5 from "path";
|
|
1066
1151
|
import { pipeline } from "stream/promises";
|
|
1067
1152
|
import { createWriteStream, mkdirSync } from "fs";
|
|
1068
1153
|
import { Extract } from "unzipper";
|
|
@@ -1071,20 +1156,20 @@ async function downloadAndExtract(url, destDir) {
|
|
|
1071
1156
|
if (!response.ok) {
|
|
1072
1157
|
throw new Error(`Download failed: ${response.statusText}`);
|
|
1073
1158
|
}
|
|
1074
|
-
const tempZip =
|
|
1159
|
+
const tempZip = path5.join(destDir, ".skill-download.zip");
|
|
1075
1160
|
const fileStream = createWriteStream(tempZip);
|
|
1076
1161
|
await pipeline(response.body, fileStream);
|
|
1077
1162
|
await new Promise((resolve4, reject) => {
|
|
1078
|
-
|
|
1163
|
+
fs5.createReadStream(tempZip).pipe(Extract({ path: destDir })).on("close", resolve4).on("error", reject);
|
|
1079
1164
|
});
|
|
1080
|
-
|
|
1165
|
+
fs5.unlinkSync(tempZip);
|
|
1081
1166
|
}
|
|
1082
1167
|
function updateGithatLock(projectDir, skill) {
|
|
1083
|
-
const lockPath =
|
|
1168
|
+
const lockPath = path5.join(projectDir, "githat.lock");
|
|
1084
1169
|
let lock = {};
|
|
1085
|
-
if (
|
|
1170
|
+
if (fs5.existsSync(lockPath)) {
|
|
1086
1171
|
try {
|
|
1087
|
-
lock = JSON.parse(
|
|
1172
|
+
lock = JSON.parse(fs5.readFileSync(lockPath, "utf-8"));
|
|
1088
1173
|
} catch {
|
|
1089
1174
|
}
|
|
1090
1175
|
}
|
|
@@ -1092,17 +1177,17 @@ function updateGithatLock(projectDir, skill) {
|
|
|
1092
1177
|
version: skill.version,
|
|
1093
1178
|
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1094
1179
|
};
|
|
1095
|
-
|
|
1180
|
+
fs5.writeFileSync(lockPath, JSON.stringify(lock, null, 2));
|
|
1096
1181
|
}
|
|
1097
1182
|
function updateEnvExample(projectDir, manifest) {
|
|
1098
1183
|
if (!manifest.requires?.env?.length) return;
|
|
1099
|
-
const envPath =
|
|
1100
|
-
const envExamplePath =
|
|
1184
|
+
const envPath = path5.join(projectDir, ".env.local");
|
|
1185
|
+
const envExamplePath = path5.join(projectDir, ".env.example");
|
|
1101
1186
|
let envContent = "";
|
|
1102
|
-
if (
|
|
1103
|
-
envContent =
|
|
1104
|
-
} else if (
|
|
1105
|
-
envContent =
|
|
1187
|
+
if (fs5.existsSync(envPath)) {
|
|
1188
|
+
envContent = fs5.readFileSync(envPath, "utf-8");
|
|
1189
|
+
} else if (fs5.existsSync(envExamplePath)) {
|
|
1190
|
+
envContent = fs5.readFileSync(envExamplePath, "utf-8");
|
|
1106
1191
|
}
|
|
1107
1192
|
const existingVars = new Set(
|
|
1108
1193
|
envContent.split("\n").filter((line) => line.includes("=")).map((line) => line.split("=")[0].trim())
|
|
@@ -1118,10 +1203,10 @@ function updateEnvExample(projectDir, manifest) {
|
|
|
1118
1203
|
# Added by skill install
|
|
1119
1204
|
${newVars.join("\n")}
|
|
1120
1205
|
`;
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1206
|
+
if (fs5.existsSync(envPath)) {
|
|
1207
|
+
fs5.appendFileSync(envPath, addition);
|
|
1123
1208
|
} else {
|
|
1124
|
-
|
|
1209
|
+
fs5.writeFileSync(envPath, `# Environment variables
|
|
1125
1210
|
${newVars.join("\n")}
|
|
1126
1211
|
`);
|
|
1127
1212
|
}
|
|
@@ -1129,112 +1214,112 @@ ${newVars.join("\n")}
|
|
|
1129
1214
|
}
|
|
1130
1215
|
var installCommand = new Command3("install").description("Install a skill to your project").argument("<slug>", "Skill slug (e.g., stripe-billing)").option("-v, --version <version>", "Specific version to install").option("-d, --dir <dir>", "Project directory (default: current directory)").action(async (slug, options) => {
|
|
1131
1216
|
try {
|
|
1132
|
-
const projectDir = options.dir ?
|
|
1133
|
-
const packageJsonPath =
|
|
1134
|
-
if (!
|
|
1135
|
-
console.error(
|
|
1217
|
+
const projectDir = options.dir ? path5.resolve(options.dir) : process.cwd();
|
|
1218
|
+
const packageJsonPath = path5.join(projectDir, "package.json");
|
|
1219
|
+
if (!fs5.existsSync(packageJsonPath)) {
|
|
1220
|
+
console.error(chalk6.red("Error: No package.json found. Are you in a project directory?"));
|
|
1136
1221
|
process.exit(1);
|
|
1137
1222
|
}
|
|
1138
|
-
console.log(
|
|
1223
|
+
console.log(chalk6.dim(`
|
|
1139
1224
|
Fetching skill info for "${slug}"...
|
|
1140
1225
|
`));
|
|
1141
1226
|
const skill = await getSkill(slug);
|
|
1142
|
-
console.log(
|
|
1143
|
-
console.log(
|
|
1144
|
-
console.log(
|
|
1227
|
+
console.log(chalk6.cyan(`\u{1F4E6} ${skill.name}`));
|
|
1228
|
+
console.log(chalk6.dim(` ${skill.description}`));
|
|
1229
|
+
console.log(chalk6.dim(` Type: ${skill.type} \xB7 Author: ${skill.authorName}
|
|
1145
1230
|
`));
|
|
1146
1231
|
const download = await getDownloadUrl(slug, options.version);
|
|
1147
1232
|
const version = download.version.version;
|
|
1148
|
-
console.log(
|
|
1149
|
-
const skillDir =
|
|
1233
|
+
console.log(chalk6.dim(`Downloading ${skill.name}@${version}...`));
|
|
1234
|
+
const skillDir = path5.join(projectDir, "githat", "skills", slug);
|
|
1150
1235
|
mkdirSync(skillDir, { recursive: true });
|
|
1151
1236
|
await downloadAndExtract(download.downloadUrl, skillDir);
|
|
1152
|
-
console.log(
|
|
1153
|
-
const manifestPath =
|
|
1154
|
-
if (
|
|
1155
|
-
const manifest = JSON.parse(
|
|
1237
|
+
console.log(chalk6.green(`\u2713 Downloaded to ${path5.relative(projectDir, skillDir)}`));
|
|
1238
|
+
const manifestPath = path5.join(skillDir, "githat-skill.json");
|
|
1239
|
+
if (fs5.existsSync(manifestPath)) {
|
|
1240
|
+
const manifest = JSON.parse(fs5.readFileSync(manifestPath, "utf-8"));
|
|
1156
1241
|
updateEnvExample(projectDir, manifest);
|
|
1157
1242
|
if (manifest.requires?.env?.length) {
|
|
1158
|
-
console.log(
|
|
1243
|
+
console.log(chalk6.yellow(`
|
|
1159
1244
|
\u26A0 Required environment variables:`));
|
|
1160
1245
|
for (const envVar of manifest.requires.env) {
|
|
1161
|
-
console.log(
|
|
1246
|
+
console.log(chalk6.dim(` ${envVar}`));
|
|
1162
1247
|
}
|
|
1163
|
-
console.log(
|
|
1248
|
+
console.log(chalk6.dim(`
|
|
1164
1249
|
Add these to your .env.local file`));
|
|
1165
1250
|
}
|
|
1166
1251
|
if (manifest.install?.dependencies) {
|
|
1167
1252
|
const deps = Object.entries(manifest.install.dependencies).map(([name, ver]) => `${name}@${ver}`).join(" ");
|
|
1168
|
-
console.log(
|
|
1253
|
+
console.log(chalk6.yellow(`
|
|
1169
1254
|
\u26A0 Install npm dependencies:`));
|
|
1170
|
-
console.log(
|
|
1255
|
+
console.log(chalk6.dim(` npm install ${deps}`));
|
|
1171
1256
|
}
|
|
1172
1257
|
}
|
|
1173
1258
|
updateGithatLock(projectDir, { slug, version });
|
|
1174
|
-
console.log(
|
|
1259
|
+
console.log(chalk6.green(`
|
|
1175
1260
|
\u2705 Successfully installed ${skill.name}@${version}
|
|
1176
1261
|
`));
|
|
1177
|
-
console.log(
|
|
1178
|
-
console.log(
|
|
1179
|
-
console.log(
|
|
1180
|
-
console.log(
|
|
1262
|
+
console.log(chalk6.dim("Next steps:"));
|
|
1263
|
+
console.log(chalk6.dim(` 1. Check githat/skills/${slug}/README.md for usage`));
|
|
1264
|
+
console.log(chalk6.dim(" 2. Add required environment variables to .env.local"));
|
|
1265
|
+
console.log(chalk6.dim(" 3. Import and use the skill in your code"));
|
|
1181
1266
|
} catch (err) {
|
|
1182
|
-
console.error(
|
|
1267
|
+
console.error(chalk6.red(`Error: ${err.message}`));
|
|
1183
1268
|
process.exit(1);
|
|
1184
1269
|
}
|
|
1185
1270
|
});
|
|
1186
1271
|
|
|
1187
1272
|
// src/commands/skills/installed.ts
|
|
1188
1273
|
import { Command as Command4 } from "commander";
|
|
1189
|
-
import
|
|
1190
|
-
import * as
|
|
1191
|
-
import * as
|
|
1274
|
+
import chalk7 from "chalk";
|
|
1275
|
+
import * as fs6 from "fs";
|
|
1276
|
+
import * as path6 from "path";
|
|
1192
1277
|
var installedCommand = new Command4("installed").alias("ls").description("List installed skills in current project").option("-d, --dir <dir>", "Project directory (default: current directory)").action(async (options) => {
|
|
1193
1278
|
try {
|
|
1194
|
-
const projectDir = options.dir ?
|
|
1195
|
-
const lockPath =
|
|
1196
|
-
if (!
|
|
1197
|
-
console.log(
|
|
1198
|
-
console.log(
|
|
1199
|
-
console.log(
|
|
1279
|
+
const projectDir = options.dir ? path6.resolve(options.dir) : process.cwd();
|
|
1280
|
+
const lockPath = path6.join(projectDir, "githat.lock");
|
|
1281
|
+
if (!fs6.existsSync(lockPath)) {
|
|
1282
|
+
console.log(chalk7.yellow("\nNo skills installed in this project."));
|
|
1283
|
+
console.log(chalk7.dim("\nTo install a skill:"));
|
|
1284
|
+
console.log(chalk7.dim(" githat skills install <slug>"));
|
|
1200
1285
|
return;
|
|
1201
1286
|
}
|
|
1202
1287
|
let lock;
|
|
1203
1288
|
try {
|
|
1204
|
-
lock = JSON.parse(
|
|
1289
|
+
lock = JSON.parse(fs6.readFileSync(lockPath, "utf-8"));
|
|
1205
1290
|
} catch {
|
|
1206
|
-
console.error(
|
|
1291
|
+
console.error(chalk7.red("Error: Invalid githat.lock file"));
|
|
1207
1292
|
process.exit(1);
|
|
1208
1293
|
}
|
|
1209
1294
|
const entries = Object.entries(lock);
|
|
1210
1295
|
if (entries.length === 0) {
|
|
1211
|
-
console.log(
|
|
1296
|
+
console.log(chalk7.yellow("\nNo skills installed in this project."));
|
|
1212
1297
|
return;
|
|
1213
1298
|
}
|
|
1214
|
-
console.log(
|
|
1299
|
+
console.log(chalk7.cyan(`
|
|
1215
1300
|
\u{1F4E6} Installed skills (${entries.length}):
|
|
1216
1301
|
`));
|
|
1217
|
-
console.log(
|
|
1218
|
-
console.log(
|
|
1302
|
+
console.log(chalk7.dim(`${"SKILL".padEnd(30)} ${"VERSION".padEnd(12)} INSTALLED`));
|
|
1303
|
+
console.log(chalk7.dim("\u2500".repeat(60)));
|
|
1219
1304
|
for (const [slug, entry] of entries) {
|
|
1220
1305
|
const date = new Date(entry.installedAt).toLocaleDateString();
|
|
1221
|
-
console.log(`${
|
|
1306
|
+
console.log(`${chalk7.bold(slug.padEnd(30))} ${entry.version.padEnd(12)} ${chalk7.dim(date)}`);
|
|
1222
1307
|
}
|
|
1223
|
-
console.log(
|
|
1224
|
-
console.log(
|
|
1225
|
-
console.log(
|
|
1308
|
+
console.log(chalk7.dim("\u2500".repeat(60)));
|
|
1309
|
+
console.log(chalk7.dim("\nTo update a skill:"));
|
|
1310
|
+
console.log(chalk7.dim(" githat skills install <slug> --version <new-version>"));
|
|
1226
1311
|
} catch (err) {
|
|
1227
|
-
console.error(
|
|
1312
|
+
console.error(chalk7.red(`Error: ${err.message}`));
|
|
1228
1313
|
process.exit(1);
|
|
1229
1314
|
}
|
|
1230
1315
|
});
|
|
1231
1316
|
|
|
1232
1317
|
// src/commands/skills/init.ts
|
|
1233
1318
|
import { Command as Command5 } from "commander";
|
|
1234
|
-
import
|
|
1235
|
-
import * as
|
|
1236
|
-
import * as
|
|
1237
|
-
import * as
|
|
1319
|
+
import chalk8 from "chalk";
|
|
1320
|
+
import * as fs7 from "fs";
|
|
1321
|
+
import * as path7 from "path";
|
|
1322
|
+
import * as p11 from "@clack/prompts";
|
|
1238
1323
|
var SKILL_TYPES = ["template", "integration", "ui", "ai", "workflow"];
|
|
1239
1324
|
function generateReadme(manifest) {
|
|
1240
1325
|
return `# ${manifest.name}
|
|
@@ -1366,23 +1451,23 @@ function toPascalCase(str) {
|
|
|
1366
1451
|
var initCommand = new Command5("init").description("Initialize a new skill package").argument("<name>", "Skill name (slug format: lowercase-with-hyphens)").option("-t, --type <type>", "Skill type (template, integration, ui, ai, workflow)").option("-d, --dir <dir>", "Parent directory (default: current directory)").action(async (name, options) => {
|
|
1367
1452
|
try {
|
|
1368
1453
|
if (!/^[a-z][a-z0-9-]{1,62}[a-z0-9]$/.test(name)) {
|
|
1369
|
-
console.error(
|
|
1454
|
+
console.error(chalk8.red("Error: Name must be lowercase alphanumeric with hyphens (2-64 chars, start with letter)"));
|
|
1370
1455
|
process.exit(1);
|
|
1371
1456
|
}
|
|
1372
|
-
const parentDir = options.dir ?
|
|
1373
|
-
const skillDir =
|
|
1374
|
-
if (
|
|
1375
|
-
console.error(
|
|
1457
|
+
const parentDir = options.dir ? path7.resolve(options.dir) : process.cwd();
|
|
1458
|
+
const skillDir = path7.join(parentDir, name);
|
|
1459
|
+
if (fs7.existsSync(skillDir)) {
|
|
1460
|
+
console.error(chalk8.red(`Error: Directory "${name}" already exists`));
|
|
1376
1461
|
process.exit(1);
|
|
1377
1462
|
}
|
|
1378
|
-
console.log(
|
|
1463
|
+
console.log(chalk8.cyan(`
|
|
1379
1464
|
\u{1F4E6} Initializing skill: ${name}
|
|
1380
1465
|
`));
|
|
1381
1466
|
let type;
|
|
1382
1467
|
if (options.type && SKILL_TYPES.includes(options.type)) {
|
|
1383
1468
|
type = options.type;
|
|
1384
1469
|
} else {
|
|
1385
|
-
const result = await
|
|
1470
|
+
const result = await p11.select({
|
|
1386
1471
|
message: "What type of skill are you creating?",
|
|
1387
1472
|
options: [
|
|
1388
1473
|
{ value: "integration", label: "Integration", hint: "Connect to external services (Stripe, SendGrid, etc.)" },
|
|
@@ -1392,19 +1477,19 @@ var initCommand = new Command5("init").description("Initialize a new skill packa
|
|
|
1392
1477
|
{ value: "workflow", label: "Workflow", hint: "Automation recipes" }
|
|
1393
1478
|
]
|
|
1394
1479
|
});
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
1480
|
+
if (p11.isCancel(result)) {
|
|
1481
|
+
p11.cancel("Operation cancelled");
|
|
1397
1482
|
process.exit(0);
|
|
1398
1483
|
}
|
|
1399
1484
|
type = result;
|
|
1400
1485
|
}
|
|
1401
|
-
const description = await
|
|
1486
|
+
const description = await p11.text({
|
|
1402
1487
|
message: "Short description:",
|
|
1403
1488
|
placeholder: `A ${type} skill for...`,
|
|
1404
1489
|
validate: (v) => v.length < 10 ? "Description must be at least 10 characters" : void 0
|
|
1405
1490
|
});
|
|
1406
|
-
if (
|
|
1407
|
-
|
|
1491
|
+
if (p11.isCancel(description)) {
|
|
1492
|
+
p11.cancel("Operation cancelled");
|
|
1408
1493
|
process.exit(0);
|
|
1409
1494
|
}
|
|
1410
1495
|
const manifest = {
|
|
@@ -1429,22 +1514,22 @@ var initCommand = new Command5("init").description("Initialize a new skill packa
|
|
|
1429
1514
|
},
|
|
1430
1515
|
keywords: [type]
|
|
1431
1516
|
};
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1517
|
+
fs7.mkdirSync(skillDir, { recursive: true });
|
|
1518
|
+
fs7.mkdirSync(path7.join(skillDir, "src"), { recursive: true });
|
|
1519
|
+
fs7.writeFileSync(
|
|
1520
|
+
path7.join(skillDir, "githat-skill.json"),
|
|
1436
1521
|
JSON.stringify(manifest, null, 2)
|
|
1437
1522
|
);
|
|
1438
|
-
|
|
1439
|
-
|
|
1523
|
+
fs7.writeFileSync(
|
|
1524
|
+
path7.join(skillDir, "README.md"),
|
|
1440
1525
|
generateReadme(manifest)
|
|
1441
1526
|
);
|
|
1442
|
-
|
|
1443
|
-
|
|
1527
|
+
fs7.writeFileSync(
|
|
1528
|
+
path7.join(skillDir, "src", "index.ts"),
|
|
1444
1529
|
generateIndexFile(manifest)
|
|
1445
1530
|
);
|
|
1446
|
-
|
|
1447
|
-
|
|
1531
|
+
fs7.writeFileSync(
|
|
1532
|
+
path7.join(skillDir, ".gitignore"),
|
|
1448
1533
|
`node_modules/
|
|
1449
1534
|
dist/
|
|
1450
1535
|
.env
|
|
@@ -1452,21 +1537,21 @@ dist/
|
|
|
1452
1537
|
*.log
|
|
1453
1538
|
`
|
|
1454
1539
|
);
|
|
1455
|
-
console.log(
|
|
1540
|
+
console.log(chalk8.green(`
|
|
1456
1541
|
\u2705 Created skill at ${skillDir}
|
|
1457
1542
|
`));
|
|
1458
|
-
console.log(
|
|
1459
|
-
console.log(
|
|
1460
|
-
console.log(
|
|
1461
|
-
console.log(
|
|
1462
|
-
console.log(
|
|
1463
|
-
console.log(
|
|
1464
|
-
console.log(
|
|
1465
|
-
console.log(
|
|
1466
|
-
console.log(
|
|
1467
|
-
console.log(
|
|
1543
|
+
console.log(chalk8.dim("Files created:"));
|
|
1544
|
+
console.log(chalk8.dim(` githat-skill.json - Skill manifest`));
|
|
1545
|
+
console.log(chalk8.dim(` README.md - Documentation`));
|
|
1546
|
+
console.log(chalk8.dim(` src/index.ts - Main entry point`));
|
|
1547
|
+
console.log(chalk8.dim(` .gitignore - Git ignore rules`));
|
|
1548
|
+
console.log(chalk8.dim("\nNext steps:"));
|
|
1549
|
+
console.log(chalk8.dim(` 1. cd ${name}`));
|
|
1550
|
+
console.log(chalk8.dim(` 2. Edit githat-skill.json with your details`));
|
|
1551
|
+
console.log(chalk8.dim(` 3. Implement your skill in src/index.ts`));
|
|
1552
|
+
console.log(chalk8.dim(` 4. Publish: githat skills publish .`));
|
|
1468
1553
|
} catch (err) {
|
|
1469
|
-
console.error(
|
|
1554
|
+
console.error(chalk8.red(`Error: ${err.message}`));
|
|
1470
1555
|
process.exit(1);
|
|
1471
1556
|
}
|
|
1472
1557
|
});
|
|
@@ -1474,7 +1559,7 @@ dist/
|
|
|
1474
1559
|
// src/commands/skills/index.ts
|
|
1475
1560
|
var skillsCommand = new Command6("skills").description("Manage GitHat skills marketplace").addCommand(searchCommand).addCommand(listCommand).addCommand(installCommand).addCommand(installedCommand).addCommand(initCommand);
|
|
1476
1561
|
skillsCommand.action(() => {
|
|
1477
|
-
console.log(
|
|
1562
|
+
console.log(chalk9.cyan("\n\u{1F4E6} GitHat Skills Marketplace\n"));
|
|
1478
1563
|
console.log("Commands:");
|
|
1479
1564
|
console.log(" search <query> Search skills by keyword");
|
|
1480
1565
|
console.log(" list List skills (filterable by type)");
|
|
@@ -1497,7 +1582,7 @@ program.command("create [project-name]", { isDefault: true }).description("Scaff
|
|
|
1497
1582
|
displayBanner();
|
|
1498
1583
|
const typescript = opts.js ? false : opts.ts ? true : void 0;
|
|
1499
1584
|
if (opts.yes && !projectName) {
|
|
1500
|
-
|
|
1585
|
+
p12.cancel(chalk10.red("Project name is required when using --yes flag"));
|
|
1501
1586
|
process.exit(1);
|
|
1502
1587
|
}
|
|
1503
1588
|
const answers = await runPrompts({
|
|
@@ -1515,7 +1600,7 @@ program.command("create [project-name]", { isDefault: true }).description("Scaff
|
|
|
1515
1600
|
skipPrompts: opts.yes
|
|
1516
1601
|
});
|
|
1517
1602
|
} catch (err) {
|
|
1518
|
-
|
|
1603
|
+
p12.cancel(chalk10.red(err.message || "Something went wrong."));
|
|
1519
1604
|
process.exit(1);
|
|
1520
1605
|
}
|
|
1521
1606
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{{#ifEquals framework "nextjs"}}
|
|
2
|
+
# GitHat — publishable key for this app.
|
|
3
|
+
# Get yours at https://githat.io/dashboard/apps
|
|
4
|
+
NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY=pk_live_your_key_here
|
|
5
|
+
|
|
6
|
+
# GitHat API base URL (leave as-is unless using a self-hosted deployment)
|
|
7
|
+
NEXT_PUBLIC_GITHAT_API_URL={{apiUrl}}
|
|
8
|
+
{{else}}
|
|
9
|
+
# GitHat — publishable key for this app.
|
|
10
|
+
# Get yours at https://githat.io/dashboard/apps
|
|
11
|
+
VITE_GITHAT_PUBLISHABLE_KEY=pk_live_your_key_here
|
|
12
|
+
|
|
13
|
+
# GitHat API base URL (leave as-is unless using a self-hosted deployment)
|
|
14
|
+
VITE_GITHAT_API_URL={{apiUrl}}
|
|
15
|
+
{{/ifEquals}}
|
|
16
|
+
{{#if useDatabase}}
|
|
17
|
+
|
|
18
|
+
# Database
|
|
19
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/{{projectName}}"
|
|
20
|
+
{{/if}}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
# {{projectName}}
|
|
2
2
|
|
|
3
|
-
Built with [GitHat](https://githat.io) —
|
|
3
|
+
Built with [GitHat](https://githat.io) — auth, teams, orgs, API keys, MCP verification, and AI agent identity are handled by GitHat's hosted platform.
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
7
|
-
1.
|
|
7
|
+
1. Copy the env file and fill in your GitHat key:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
cp .env.local.example .env.local
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
2.
|
|
13
|
+
2. Install dependencies:
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
{{#ifEquals
|
|
15
|
+
```bash
|
|
16
|
+
{{#ifEquals packageManager "yarn"}}yarn{{else}}{{packageManager}} install{{/ifEquals}}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Get your key at [githat.io/dashboard/apps](https://githat.io/dashboard/apps).
|
|
20
|
-
|
|
21
19
|
3. Start the dev server:
|
|
22
20
|
|
|
23
21
|
```bash
|
|
@@ -26,36 +24,11 @@ Get your key at [githat.io/dashboard/apps](https://githat.io/dashboard/apps).
|
|
|
26
24
|
|
|
27
25
|
4. Open [http://localhost:{{#ifEquals framework "nextjs"}}3000{{else}}5173{{/ifEquals}}](http://localhost:{{#ifEquals framework "nextjs"}}3000{{else}}5173{{/ifEquals}})
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
All API calls go to `api.githat.io`. Your data is stored in GitHat's database. **No backend to deploy.**
|
|
32
|
-
|
|
33
|
-
GitHat handles:
|
|
34
|
-
|
|
35
|
-
- User auth (sign-up, sign-in, email verification, password reset)
|
|
36
|
-
- Organizations (create, switch, branding)
|
|
37
|
-
- Team management (invite members, assign roles)
|
|
38
|
-
- API key management (publishable + secret keys)
|
|
39
|
-
{{#if includeMcpModule}}- MCP server registration and domain verification
|
|
40
|
-
{{/if}}{{#if includeAgentModule}}- AI agent registration and wallet verification
|
|
41
|
-
{{/if}}- Email delivery (verification, invitations, password resets)
|
|
42
|
-
|
|
43
|
-
The `githat/` folder in your project is a typed API client that talks to `api.githat.io`. You write frontend code, GitHat handles the rest.
|
|
44
|
-
|
|
45
|
-
Manage everything at [githat.io/dashboard](https://githat.io/dashboard).
|
|
46
|
-
|
|
47
|
-
## Pages
|
|
48
|
-
|
|
49
|
-
- `/sign-in` — Sign in
|
|
50
|
-
- `/sign-up` — Create account
|
|
51
|
-
{{#if includeForgotPassword}}- `/forgot-password` — Password recovery
|
|
52
|
-
{{/if}}{{#if includeDashboard}}- `/dashboard` — Protected dashboard
|
|
53
|
-
{{/if}}{{#if includeOrgManagement}}- `/dashboard/members` — Invite members, manage roles
|
|
54
|
-
{{/if}}
|
|
27
|
+
Your `NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY` is required — get it at [githat.io/dashboard/apps](https://githat.io/dashboard/apps) or re-run `create-githat-app` after running `githat login` to have it auto-registered.
|
|
55
28
|
|
|
56
29
|
## GitHat SDK
|
|
57
30
|
|
|
58
|
-
This project uses [`@githat/nextjs`](https://www.npmjs.com/package/@githat/nextjs)
|
|
31
|
+
This project uses [`@githat/nextjs`](https://www.npmjs.com/package/@githat/nextjs):
|
|
59
32
|
|
|
60
33
|
```{{#if typescript}}tsx{{else}}jsx{{/if}}
|
|
61
34
|
import { useAuth, UserButton, OrgSwitcher } from '@githat/nextjs';
|
|
@@ -63,32 +36,38 @@ import { useAuth, UserButton, OrgSwitcher } from '@githat/nextjs';
|
|
|
63
36
|
const { user, org, signIn, signUp, signOut } = useAuth();
|
|
64
37
|
```
|
|
65
38
|
|
|
66
|
-
## Deploy
|
|
39
|
+
## Deploy to EC2 via GitHub Actions
|
|
67
40
|
|
|
68
|
-
|
|
41
|
+
The scaffolded `.github/workflows/deploy.yml` deploys the Next.js standalone bundle to an EC2 instance on every push to `main`.
|
|
69
42
|
|
|
70
|
-
|
|
71
|
-
npx vercel
|
|
72
|
-
```
|
|
43
|
+
**Required GitHub repo secrets:**
|
|
73
44
|
|
|
74
|
-
|
|
75
|
-
|
|
45
|
+
| Secret | Description |
|
|
46
|
+
|--------|-------------|
|
|
47
|
+
| `SSH_PRIVATE_KEY` | Private key whose public key is in the EC2 instance's `~/.ssh/authorized_keys` |
|
|
48
|
+
| `EC2_HOST` | `ubuntu@<ec2-ip-or-hostname>` |
|
|
49
|
+
| `PROD_ENV` | Full contents of your production `.env` file (set via `gh secret set PROD_ENV < .env.local`) |
|
|
50
|
+
| `GITHAT_PUBLISHABLE_KEY` | Your GitHat publishable key (baked into the client bundle at build time) |
|
|
76
51
|
|
|
77
|
-
|
|
78
|
-
npx vercel
|
|
79
|
-
```
|
|
52
|
+
**One-time EC2 setup:**
|
|
80
53
|
|
|
81
|
-
|
|
54
|
+
1. Create a systemd service at `/etc/systemd/system/{{projectName}}.service` that runs `node /opt/{{projectName}}/server.js` with `PORT=3000`.
|
|
55
|
+
2. Create `/opt/{{projectName}}/` owned by your deploy user.
|
|
56
|
+
3. Enable the service: `sudo systemctl enable {{projectName}}`.
|
|
82
57
|
|
|
83
|
-
|
|
84
|
-
npx netlify deploy
|
|
85
|
-
```
|
|
58
|
+
After that, every `git push origin main` triggers a build + rsync + restart. The workflow smoke-tests `/api/health` before reporting success.
|
|
86
59
|
|
|
87
|
-
|
|
88
|
-
|
|
60
|
+
## Pages
|
|
61
|
+
|
|
62
|
+
- `/sign-in` — Sign in
|
|
63
|
+
- `/sign-up` — Create account
|
|
64
|
+
{{#if includeForgotPassword}}- `/forgot-password` — Password recovery
|
|
65
|
+
{{/if}}{{#if includeDashboard}}- `/dashboard` — Protected dashboard
|
|
66
|
+
{{/if}}{{#if includeOrgManagement}}- `/dashboard/members` — Invite members, manage roles
|
|
67
|
+
{{/if}}
|
|
89
68
|
|
|
90
69
|
## Learn More
|
|
91
70
|
|
|
92
71
|
- [GitHat Documentation](https://githat.io/docs)
|
|
93
72
|
- [SDK Reference](https://www.npmjs.com/package/@githat/nextjs)
|
|
94
|
-
- [API Reference
|
|
73
|
+
- [API Reference](https://githat.io/docs/api)
|
|
@@ -20,6 +20,11 @@ export default function RootLayout({ children }{{#if typescript}}: { children: R
|
|
|
20
20
|
}}>
|
|
21
21
|
{children}
|
|
22
22
|
</GitHatProvider>
|
|
23
|
+
<footer style=\{{ textAlign: 'center', padding: '1rem 0', fontSize: '0.75rem', color: '#52525b' }}>
|
|
24
|
+
<a href="https://githat.io" target="_blank" rel="noopener noreferrer" style=\{{ color: '#7c3aed', textDecoration: 'none' }}>
|
|
25
|
+
Powered by GitHat
|
|
26
|
+
</a>
|
|
27
|
+
</footer>
|
|
23
28
|
</body>
|
|
24
29
|
</html>
|
|
25
30
|
);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
name: Deploy to EC2
|
|
2
|
+
|
|
3
|
+
# Auto-deploy every push to main. Also supports manual runs from the
|
|
4
|
+
# Actions tab (workflow_dispatch) so you can re-ship without a dummy commit.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
# Only one deploy at a time. Queue rather than race if a second push
|
|
11
|
+
# lands mid-deploy.
|
|
12
|
+
concurrency:
|
|
13
|
+
group: deploy-ec2
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build-and-deploy:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
# Skip if commit message starts with "docs:" unless it includes [deploy].
|
|
20
|
+
if: $\{{ !startsWith(github.event.head_commit.message, 'docs:') || contains(github.event.head_commit.message, '[deploy]') }}
|
|
21
|
+
timeout-minutes: 12
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Setup Node 20
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: "20"
|
|
30
|
+
cache: "npm"
|
|
31
|
+
|
|
32
|
+
- name: Install deps
|
|
33
|
+
run: npm ci
|
|
34
|
+
|
|
35
|
+
- name: Type-check
|
|
36
|
+
run: npx tsc --noEmit
|
|
37
|
+
|
|
38
|
+
- name: Compute BUILD_ID
|
|
39
|
+
id: build_id
|
|
40
|
+
run: |
|
|
41
|
+
BUILD_ID=$(git rev-parse --short HEAD || echo "${GITHUB_SHA:0:7}")
|
|
42
|
+
echo "BUILD_ID=$BUILD_ID" >> "$GITHUB_OUTPUT"
|
|
43
|
+
echo "Resolved BUILD_ID=$BUILD_ID"
|
|
44
|
+
|
|
45
|
+
- name: Build (Next.js standalone)
|
|
46
|
+
run: npm run build
|
|
47
|
+
env:
|
|
48
|
+
# NEXT_PUBLIC_* vars are baked into the client bundle at build time.
|
|
49
|
+
# The real value comes from the GITHAT_PUBLISHABLE_KEY repo secret.
|
|
50
|
+
NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY: $\{{ secrets.GITHAT_PUBLISHABLE_KEY }}
|
|
51
|
+
BUILD_ID: $\{{ steps.build_id.outputs.BUILD_ID }}
|
|
52
|
+
|
|
53
|
+
- name: Load SSH key
|
|
54
|
+
run: |
|
|
55
|
+
mkdir -p ~/.ssh
|
|
56
|
+
echo "$\{{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
|
57
|
+
chmod 600 ~/.ssh/deploy_key
|
|
58
|
+
ssh-keyscan -H "${EC2_HOST#*@}" >> ~/.ssh/known_hosts
|
|
59
|
+
env:
|
|
60
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
61
|
+
|
|
62
|
+
- name: Sync production .env
|
|
63
|
+
env:
|
|
64
|
+
PROD_ENV: $\{{ secrets.PROD_ENV }}
|
|
65
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
66
|
+
run: |
|
|
67
|
+
if [ -z "$PROD_ENV" ]; then
|
|
68
|
+
echo "PROD_ENV secret is empty — skipping env sync."
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
SSH_OPTS="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes"
|
|
72
|
+
printf '%s' "$PROD_ENV" > /tmp/prod.env
|
|
73
|
+
rsync -az -e "ssh $SSH_OPTS" /tmp/prod.env "$EC2_HOST:/opt/{{projectName}}/.env"
|
|
74
|
+
shred -u /tmp/prod.env 2>/dev/null || rm -f /tmp/prod.env
|
|
75
|
+
ssh $SSH_OPTS "$EC2_HOST" \
|
|
76
|
+
"sudo chgrp {{projectName}} /opt/{{projectName}}/.env && sudo chmod 640 /opt/{{projectName}}/.env"
|
|
77
|
+
|
|
78
|
+
- name: Deploy to EC2
|
|
79
|
+
env:
|
|
80
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
81
|
+
run: |
|
|
82
|
+
SSH_OPTS="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes"
|
|
83
|
+
|
|
84
|
+
rsync -az --delete -e "ssh $SSH_OPTS" \
|
|
85
|
+
--exclude .env \
|
|
86
|
+
.next/standalone/ "$EC2_HOST:/opt/{{projectName}}/"
|
|
87
|
+
|
|
88
|
+
rsync -az -e "ssh $SSH_OPTS" \
|
|
89
|
+
.next/static/ "$EC2_HOST:/opt/{{projectName}}/.next/static/"
|
|
90
|
+
|
|
91
|
+
rsync -az -e "ssh $SSH_OPTS" \
|
|
92
|
+
public/ "$EC2_HOST:/opt/{{projectName}}/public/"
|
|
93
|
+
|
|
94
|
+
ssh $SSH_OPTS "$EC2_HOST" "
|
|
95
|
+
sudo systemctl restart {{projectName}}
|
|
96
|
+
sleep 5
|
|
97
|
+
sudo systemctl is-active --quiet {{projectName}} || { sudo journalctl -u {{projectName}} -n 30 --no-pager; exit 1; }
|
|
98
|
+
echo \"deployed build \$(cat /opt/{{projectName}}/.next/BUILD_ID)\"
|
|
99
|
+
"
|
|
100
|
+
|
|
101
|
+
- name: Smoke-test the deploy
|
|
102
|
+
env:
|
|
103
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
104
|
+
run: |
|
|
105
|
+
SSH_OPTS="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes"
|
|
106
|
+
ssh $SSH_OPTS "$EC2_HOST" \
|
|
107
|
+
"curl --fail --silent --show-error --max-time 10 http://127.0.0.1:3000/api/health > /dev/null && echo OK"
|
|
@@ -27,6 +27,11 @@ export default function RootLayout({ children }{{#if typescript}}: { children: R
|
|
|
27
27
|
}}>
|
|
28
28
|
{children}
|
|
29
29
|
</GitHatProvider>
|
|
30
|
+
<footer style=\{{ textAlign: 'center', padding: '1rem 0', fontSize: '0.75rem', color: '#52525b' }}>
|
|
31
|
+
<a href="https://githat.io" target="_blank" rel="noopener noreferrer" style=\{{ color: '#7c3aed', textDecoration: 'none' }}>
|
|
32
|
+
Powered by GitHat
|
|
33
|
+
</a>
|
|
34
|
+
</footer>
|
|
30
35
|
</body>
|
|
31
36
|
</html>
|
|
32
37
|
);
|
|
@@ -27,20 +27,27 @@ function NotFound() {
|
|
|
27
27
|
|
|
28
28
|
export default function App() {
|
|
29
29
|
return (
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
<>
|
|
31
|
+
<Routes>
|
|
32
|
+
<Route path="/" element={<Home />} />
|
|
33
|
+
<Route path="/sign-in" element={<SignIn />} />
|
|
34
|
+
<Route path="/sign-up" element={<SignUp />} />
|
|
34
35
|
{{#if includeForgotPassword}}
|
|
35
|
-
|
|
36
|
+
<Route path="/forgot-password" element={<ForgotPassword />} />
|
|
36
37
|
{{/if}}
|
|
37
38
|
{{#if includeEmailVerification}}
|
|
38
|
-
|
|
39
|
+
<Route path="/verify-email" element={<VerifyEmail />} />
|
|
39
40
|
{{/if}}
|
|
40
41
|
{{#if includeDashboard}}
|
|
41
|
-
|
|
42
|
+
<Route path="/dashboard/*" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
|
|
42
43
|
{{/if}}
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
<Route path="*" element={<NotFound />} />
|
|
45
|
+
</Routes>
|
|
46
|
+
<footer style=\{{ textAlign: 'center', padding: '1rem 0', fontSize: '0.75rem', color: '#52525b' }}>
|
|
47
|
+
<a href="https://githat.io" target="_blank" rel="noopener noreferrer" style=\{{ color: '#7c3aed', textDecoration: 'none' }}>
|
|
48
|
+
Powered by GitHat
|
|
49
|
+
</a>
|
|
50
|
+
</footer>
|
|
51
|
+
</>
|
|
45
52
|
);
|
|
46
53
|
}
|