create-githat-app 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +83 -29
- package/package.json +1 -1
- package/templates/fullstack/apps-web-nextjs/app/(auth)/reset-password/page.tsx.hbs +30 -2
- package/templates/fullstack/apps-web-nextjs/app/dashboard/layout.tsx.hbs +10 -4
- package/templates/fullstack/apps-web-nextjs/app/dashboard/page.tsx.hbs +2 -2
- package/templates/fullstack/root/package.json.hbs +1 -1
- package/templates/nextjs/app/(auth)/reset-password/page.tsx.hbs +10 -2
package/dist/cli.js
CHANGED
|
@@ -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.6";
|
|
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"];
|
|
@@ -406,15 +406,77 @@ function openBrowser(url) {
|
|
|
406
406
|
} catch {
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
|
+
function sleep(ms) {
|
|
410
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
411
|
+
}
|
|
412
|
+
async function deviceAuthFlow() {
|
|
413
|
+
const spinner2 = p5.spinner();
|
|
414
|
+
try {
|
|
415
|
+
spinner2.start("Requesting device code...");
|
|
416
|
+
const codeRes = await fetch(`${DEFAULT_API_URL}/auth/device/code`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: { "Content-Type": "application/json" },
|
|
419
|
+
body: JSON.stringify({ client_name: "create-githat-app" })
|
|
420
|
+
});
|
|
421
|
+
if (!codeRes.ok) {
|
|
422
|
+
spinner2.stop("Failed to get device code");
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const codeData = await codeRes.json();
|
|
426
|
+
spinner2.stop("Device code generated");
|
|
427
|
+
p5.note(
|
|
428
|
+
`Code: ${codeData.user_code}
|
|
429
|
+
|
|
430
|
+
Opening browser to complete sign-in...
|
|
431
|
+
If it doesn't open, visit: ${codeData.verification_uri_complete}`,
|
|
432
|
+
"Authorize Device"
|
|
433
|
+
);
|
|
434
|
+
openBrowser(codeData.verification_uri_complete);
|
|
435
|
+
spinner2.start("Waiting for browser authorization...");
|
|
436
|
+
const expiresAt = Date.now() + codeData.expires_in * 1e3;
|
|
437
|
+
const pollInterval = (codeData.interval || 5) * 1e3;
|
|
438
|
+
while (Date.now() < expiresAt) {
|
|
439
|
+
await sleep(pollInterval);
|
|
440
|
+
const tokenRes = await fetch(`${DEFAULT_API_URL}/auth/device/token`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: { "Content-Type": "application/json" },
|
|
443
|
+
body: JSON.stringify({ device_code: codeData.device_code })
|
|
444
|
+
});
|
|
445
|
+
const tokenData = await tokenRes.json();
|
|
446
|
+
if (tokenData.error === "authorization_pending") {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (tokenData.error === "expired_token") {
|
|
450
|
+
spinner2.stop("Device code expired");
|
|
451
|
+
p5.log.error("Authorization timed out. Please try again.");
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
if (tokenData.publishable_key) {
|
|
455
|
+
spinner2.stop("Authorized!");
|
|
456
|
+
p5.log.success(`Connected to ${tokenData.app_name || "your app"} (${tokenData.org_name || "your org"})`);
|
|
457
|
+
return tokenData.publishable_key;
|
|
458
|
+
}
|
|
459
|
+
spinner2.stop("Authorization failed");
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
spinner2.stop("Timed out");
|
|
463
|
+
p5.log.error("Authorization timed out. Please try again.");
|
|
464
|
+
return null;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
spinner2.stop("Connection error");
|
|
467
|
+
p5.log.error("Failed to connect to GitHat API. Check your internet connection.");
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
409
471
|
async function promptGitHat(existingKey) {
|
|
410
472
|
let publishableKey = existingKey || "";
|
|
411
473
|
if (!publishableKey) {
|
|
412
474
|
const connectChoice = await p5.select({
|
|
413
475
|
message: "Connect to GitHat",
|
|
414
476
|
options: [
|
|
415
|
-
{ value: "
|
|
416
|
-
{ value: "
|
|
417
|
-
{ value: "
|
|
477
|
+
{ value: "skip", label: "Skip for now", hint: "auth works on localhost \u2014 add key later" },
|
|
478
|
+
{ value: "browser", label: "Sign in with browser", hint: "opens githat.io to authorize" },
|
|
479
|
+
{ value: "paste", label: "I have a key", hint: "paste your pk_live_... key" }
|
|
418
480
|
]
|
|
419
481
|
});
|
|
420
482
|
if (p5.isCancel(connectChoice)) {
|
|
@@ -422,19 +484,10 @@ async function promptGitHat(existingKey) {
|
|
|
422
484
|
process.exit(0);
|
|
423
485
|
}
|
|
424
486
|
if (connectChoice === "browser") {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const pastedKey = await p5.text({
|
|
429
|
-
message: "Paste your publishable key",
|
|
430
|
-
placeholder: "pk_live_...",
|
|
431
|
-
validate: validatePublishableKey
|
|
432
|
-
});
|
|
433
|
-
if (p5.isCancel(pastedKey)) {
|
|
434
|
-
p5.cancel("Setup cancelled.");
|
|
435
|
-
process.exit(0);
|
|
487
|
+
const key = await deviceAuthFlow();
|
|
488
|
+
if (key) {
|
|
489
|
+
publishableKey = key;
|
|
436
490
|
}
|
|
437
|
-
publishableKey = pastedKey || "";
|
|
438
491
|
} else if (connectChoice === "paste") {
|
|
439
492
|
const pastedKey = await p5.text({
|
|
440
493
|
message: "Publishable key",
|
|
@@ -446,9 +499,6 @@ async function promptGitHat(existingKey) {
|
|
|
446
499
|
process.exit(0);
|
|
447
500
|
}
|
|
448
501
|
publishableKey = pastedKey || "";
|
|
449
|
-
} else if (connectChoice === "skip") {
|
|
450
|
-
p5.log.info("Auth works on localhost without a key (CORS bypass for development).");
|
|
451
|
-
p5.log.info("Sign up at githat.io \u2014 a publishable key is auto-created for you.");
|
|
452
502
|
}
|
|
453
503
|
}
|
|
454
504
|
const authFeatures = await p5.multiselect({
|
|
@@ -528,13 +578,15 @@ async function promptFinalize() {
|
|
|
528
578
|
function toDisplayName(projectName) {
|
|
529
579
|
return projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
530
580
|
}
|
|
531
|
-
function getDefaults(projectName, publishableKey, typescript) {
|
|
581
|
+
function getDefaults(projectName, publishableKey, typescript, fullstack, backendFramework) {
|
|
532
582
|
const displayName = toDisplayName(projectName);
|
|
583
|
+
const projectType = fullstack ? "fullstack" : "frontend";
|
|
533
584
|
return {
|
|
534
585
|
projectName,
|
|
535
586
|
businessName: displayName,
|
|
536
587
|
description: `${displayName} \u2014 Built with GitHat`,
|
|
537
|
-
projectType
|
|
588
|
+
projectType,
|
|
589
|
+
backendFramework: fullstack ? backendFramework || "hono" : void 0,
|
|
538
590
|
framework: "nextjs",
|
|
539
591
|
typescript: typescript ?? true,
|
|
540
592
|
packageManager: detectPackageManager(),
|
|
@@ -544,7 +596,7 @@ function getDefaults(projectName, publishableKey, typescript) {
|
|
|
544
596
|
databaseChoice: "none",
|
|
545
597
|
useTailwind: true,
|
|
546
598
|
includeDashboard: true,
|
|
547
|
-
includeGithatFolder:
|
|
599
|
+
includeGithatFolder: projectType === "frontend",
|
|
548
600
|
initGit: true,
|
|
549
601
|
installDeps: true
|
|
550
602
|
};
|
|
@@ -552,7 +604,7 @@ function getDefaults(projectName, publishableKey, typescript) {
|
|
|
552
604
|
async function runPrompts(args) {
|
|
553
605
|
if (args.yes && args.initialName) {
|
|
554
606
|
p8.log.info("Using defaults (--yes flag)");
|
|
555
|
-
return getDefaults(args.initialName, args.publishableKey, args.typescript);
|
|
607
|
+
return getDefaults(args.initialName, args.publishableKey, args.typescript, args.fullstack, args.backendFramework);
|
|
556
608
|
}
|
|
557
609
|
p8.intro("Let's set up your GitHat app");
|
|
558
610
|
sectionHeader("Project");
|
|
@@ -725,14 +777,14 @@ function createSpinner(text4) {
|
|
|
725
777
|
return ora({ text: text4, color: "magenta" });
|
|
726
778
|
}
|
|
727
779
|
async function withSpinner(text4, fn, successText) {
|
|
728
|
-
const
|
|
729
|
-
|
|
780
|
+
const spinner2 = createSpinner(text4);
|
|
781
|
+
spinner2.start();
|
|
730
782
|
try {
|
|
731
783
|
const result = await fn();
|
|
732
|
-
|
|
784
|
+
spinner2.succeed(successText || text4);
|
|
733
785
|
return result;
|
|
734
786
|
} catch (err) {
|
|
735
|
-
|
|
787
|
+
spinner2.fail(text4);
|
|
736
788
|
throw err;
|
|
737
789
|
}
|
|
738
790
|
}
|
|
@@ -1436,7 +1488,7 @@ skillsCommand.action(() => {
|
|
|
1436
1488
|
// src/cli.ts
|
|
1437
1489
|
var program = new Command7();
|
|
1438
1490
|
program.name("githat").description("GitHat CLI - Scaffold apps and manage skills").version(VERSION);
|
|
1439
|
-
program.command("create [project-name]", { isDefault: true }).description("Scaffold a new GitHat app").option("--key <key>", "GitHat publishable key (pk_live_...)").option("--ts", "Use TypeScript (default)").option("--js", "Use JavaScript").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, opts) => {
|
|
1491
|
+
program.command("create [project-name]", { isDefault: true }).description("Scaffold a new GitHat app").option("--key <key>", "GitHat publishable key (pk_live_...)").option("--ts", "Use TypeScript (default)").option("--js", "Use JavaScript").option("--fullstack", "Create fullstack project (Turborepo)").option("--backend <framework>", "Backend framework (hono, express, fastify)").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, opts) => {
|
|
1440
1492
|
try {
|
|
1441
1493
|
displayBanner();
|
|
1442
1494
|
const typescript = opts.js ? false : opts.ts ? true : void 0;
|
|
@@ -1448,7 +1500,9 @@ program.command("create [project-name]", { isDefault: true }).description("Scaff
|
|
|
1448
1500
|
initialName: projectName,
|
|
1449
1501
|
publishableKey: opts.key,
|
|
1450
1502
|
typescript,
|
|
1451
|
-
yes: opts.yes
|
|
1503
|
+
yes: opts.yes,
|
|
1504
|
+
fullstack: opts.fullstack,
|
|
1505
|
+
backendFramework: opts.backend
|
|
1452
1506
|
});
|
|
1453
1507
|
const context = answersToContext(answers);
|
|
1454
1508
|
await scaffold(context, {
|
package/package.json
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
{{#if includeForgotPassword}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { Suspense } from 'react';
|
|
5
|
+
import { useSearchParams } from 'next/navigation';
|
|
2
6
|
import { ResetPasswordForm } from '@githat/nextjs';
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
function ResetPasswordContent() {
|
|
9
|
+
const searchParams = useSearchParams();
|
|
10
|
+
const token = searchParams.get('token');
|
|
11
|
+
|
|
12
|
+
if (!token) {
|
|
13
|
+
return (
|
|
14
|
+
<main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
|
|
15
|
+
<div {{#if useTailwind}}className="text-center"{{else}}style=\{{ textAlign: 'center' }}{{/if}}>
|
|
16
|
+
<h1 {{#if useTailwind}}className="text-xl font-semibold text-white mb-2"{{else}}style=\{{ fontSize: '1.25rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}{{/if}}>Invalid reset link</h1>
|
|
17
|
+
<p {{#if useTailwind}}className="text-zinc-400"{{else}}style=\{{ color: '#a1a1aa' }}{{/if}}>
|
|
18
|
+
<a href="/forgot-password" {{#if useTailwind}}className="text-violet-500 hover:underline"{{else}}style=\{{ color: '#7c3aed' }}{{/if}}>Request a new one</a>
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
</main>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
return (
|
|
6
26
|
<main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
|
|
7
|
-
<ResetPasswordForm signInUrl="/sign-in" />
|
|
27
|
+
<ResetPasswordForm token={token} signInUrl="/sign-in" />
|
|
8
28
|
</main>
|
|
9
29
|
);
|
|
10
30
|
}
|
|
31
|
+
|
|
32
|
+
export default function ResetPasswordPage() {
|
|
33
|
+
return (
|
|
34
|
+
<Suspense fallback={<div {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b] text-zinc-400"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b', color: '#a1a1aa' }}{{/if}}>Loading...</div>}>
|
|
35
|
+
<ResetPasswordContent />
|
|
36
|
+
</Suspense>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
11
39
|
{{/if}}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
{{#if includeDashboard}}
|
|
2
|
-
import {
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
3
|
+
import { cookies } from 'next/headers';
|
|
4
|
+
|
|
5
|
+
export default async function DashboardLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
|
|
6
|
+
const cookieStore = await cookies();
|
|
7
|
+
const token = cookieStore.get('githat_access')?.value;
|
|
8
|
+
|
|
9
|
+
if (!token) {
|
|
10
|
+
redirect('/sign-in');
|
|
11
|
+
}
|
|
3
12
|
|
|
4
|
-
async function DashboardLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
|
|
5
13
|
return <>{children}</>;
|
|
6
14
|
}
|
|
7
|
-
|
|
8
|
-
export default withAuth(DashboardLayout);
|
|
9
15
|
{{/if}}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{{#if includeDashboard}}
|
|
2
2
|
'use client';
|
|
3
|
-
import {
|
|
3
|
+
import { useAuth, UserButton } from '@githat/nextjs';
|
|
4
4
|
|
|
5
5
|
export default function DashboardPage() {
|
|
6
|
-
const { user, org } =
|
|
6
|
+
const { user, org } = useAuth();
|
|
7
7
|
|
|
8
8
|
return (
|
|
9
9
|
<div {{#if useTailwind}}className="p-8"{{else}}style=\{{ padding: '2rem' }}{{/if}}>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{{#if includeForgotPassword}}
|
|
2
2
|
'use client';
|
|
3
3
|
|
|
4
|
-
import { useState } from 'react';
|
|
4
|
+
import { Suspense, useState } from 'react';
|
|
5
5
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
6
6
|
{{#if includeGithatFolder}}
|
|
7
7
|
import { authApi } from '../../../githat/api/auth{{#unless typescript}}.js{{/unless}}';
|
|
8
8
|
{{/if}}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
function ResetPasswordContent() {
|
|
11
11
|
const searchParams = useSearchParams();
|
|
12
12
|
const router = useRouter();
|
|
13
13
|
const token = searchParams.get('token');
|
|
@@ -95,4 +95,12 @@ export default function ResetPasswordPage() {
|
|
|
95
95
|
</main>
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
export default function ResetPasswordPage() {
|
|
100
|
+
return (
|
|
101
|
+
<Suspense fallback={<div {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b] text-zinc-400"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b', color: '#a1a1aa' }}{{/if}}>Loading...</div>}>
|
|
102
|
+
<ResetPasswordContent />
|
|
103
|
+
</Suspense>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
98
106
|
{{/if}}
|