create-nscope-app 1.0.0

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.
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ import { cp, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { stdin as input, stdout as output } from "node:process";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const TEMPLATES_DIR = join(__dirname, "..", "templates");
10
+
11
+ const TEMPLATE_CHOICES = ["vite", "next"];
12
+
13
+ function parseArgs(argv) {
14
+ const result = {
15
+ projectName: "",
16
+ template: "",
17
+ apiKey: "",
18
+ baseUrl: "http://localhost:3333",
19
+ redirectUri: "http://localhost:5173",
20
+ help: false,
21
+ };
22
+
23
+ for (let index = 0; index < argv.length; index += 1) {
24
+ const arg = argv[index];
25
+
26
+ if (arg === "--help" || arg === "-h") {
27
+ result.help = true;
28
+ continue;
29
+ }
30
+
31
+ if (arg === "--template" || arg === "-t") {
32
+ result.template = argv[index + 1] ?? "";
33
+ index += 1;
34
+ continue;
35
+ }
36
+
37
+ if (arg === "--api-key") {
38
+ result.apiKey = argv[index + 1] ?? "";
39
+ index += 1;
40
+ continue;
41
+ }
42
+
43
+ if (arg === "--base-url") {
44
+ result.baseUrl = argv[index + 1] ?? result.baseUrl;
45
+ index += 1;
46
+ continue;
47
+ }
48
+
49
+ if (arg === "--redirect-uri") {
50
+ result.redirectUri = argv[index + 1] ?? result.redirectUri;
51
+ index += 1;
52
+ continue;
53
+ }
54
+
55
+ if (!arg.startsWith("-") && !result.projectName) {
56
+ result.projectName = arg;
57
+ }
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ function printHelp() {
64
+ console.log(`
65
+ create-nscope-app — scaffold a Nscope client app
66
+
67
+ Usage:
68
+ npx create-nscope-app <project-name> [options]
69
+
70
+ Options:
71
+ -t, --template <vite|next> Template (default: vite)
72
+ --api-key <key> Nscope API key (pk_live_...)
73
+ --base-url <url> API base URL (default: http://localhost:3333)
74
+ --redirect-uri <url> OAuth return URL (default: http://localhost:5173)
75
+ -h, --help Show this help
76
+
77
+ Examples:
78
+ npx create-nscope-app my-saas --template vite
79
+ npx create-nscope-app my-saas -t next --api-key pk_live_xxx
80
+ `);
81
+ }
82
+
83
+ async function prompt(question, defaultValue = "") {
84
+ const rl = createInterface({ input, output });
85
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
86
+ const answer = (await rl.question(`${question}${suffix}: `)).trim();
87
+ rl.close();
88
+ return answer || defaultValue;
89
+ }
90
+
91
+ async function copyDir(source, destination, replacements) {
92
+ await mkdir(destination, { recursive: true });
93
+ const entries = await readdir(source, { withFileTypes: true });
94
+
95
+ for (const entry of entries) {
96
+ const sourcePath = join(source, entry.name);
97
+ const destPath = join(destination, entry.name);
98
+
99
+ if (entry.isDirectory()) {
100
+ await copyDir(sourcePath, destPath, replacements);
101
+ continue;
102
+ }
103
+
104
+ let content = await readFile(sourcePath, "utf8");
105
+
106
+ for (const [key, value] of Object.entries(replacements)) {
107
+ content = content.replaceAll(`{{${key}}}`, value);
108
+ }
109
+
110
+ await writeFile(destPath, content, "utf8");
111
+ }
112
+ }
113
+
114
+ async function main() {
115
+ const args = parseArgs(process.argv.slice(2));
116
+
117
+ if (args.help) {
118
+ printHelp();
119
+ return;
120
+ }
121
+
122
+ const projectName =
123
+ args.projectName || (await prompt("Project name", "my-nscope-app"));
124
+ const template =
125
+ args.template ||
126
+ (await prompt(`Template (${TEMPLATE_CHOICES.join("|")})`, "vite"));
127
+
128
+ if (!TEMPLATE_CHOICES.includes(template)) {
129
+ console.error(`Invalid template "${template}". Use: ${TEMPLATE_CHOICES.join(", ")}`);
130
+ process.exit(1);
131
+ }
132
+
133
+ const apiKey =
134
+ args.apiKey || (await prompt("API key (pk_live_...)", "pk_live_YOUR_KEY"));
135
+ const baseUrl =
136
+ args.baseUrl || (await prompt("API base URL", "http://localhost:3333"));
137
+ const redirectUri =
138
+ args.redirectUri ||
139
+ (await prompt(
140
+ "Redirect URI",
141
+ template === "next" ? "http://localhost:3000" : "http://localhost:5173",
142
+ ));
143
+
144
+ const targetDir = resolve(process.cwd(), projectName);
145
+ const templateDir = join(TEMPLATES_DIR, template);
146
+
147
+ try {
148
+ await stat(templateDir);
149
+ } catch {
150
+ console.error(`Template not found: ${template}`);
151
+ process.exit(1);
152
+ }
153
+
154
+ try {
155
+ await stat(targetDir);
156
+ console.error(`Directory already exists: ${targetDir}`);
157
+ process.exit(1);
158
+ } catch {
159
+ // ok
160
+ }
161
+
162
+ const replacements = {
163
+ PROJECT_NAME: projectName,
164
+ API_KEY: apiKey,
165
+ BASE_URL: baseUrl,
166
+ REDIRECT_URI: redirectUri,
167
+ };
168
+
169
+ await copyDir(templateDir, targetDir, replacements);
170
+
171
+ console.log(`
172
+ ✓ Created ${projectName} (${template})
173
+
174
+ Next steps:
175
+ cd ${projectName}
176
+ pnpm install
177
+ pnpm dev
178
+
179
+ Docs: https://github.com/tratondigital/nscope-app — /docs/sdk
180
+ `);
181
+ }
182
+
183
+ main().catch((error) => {
184
+ console.error(error instanceof Error ? error.message : error);
185
+ process.exit(1);
186
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "create-nscope-app",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a Nscope client app (Vite or Next.js)",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-nscope-app": "./bin/create-nscope-app.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates"
12
+ ],
13
+ "keywords": [
14
+ "nscope",
15
+ "auth",
16
+ "scaffold",
17
+ "vite",
18
+ "next"
19
+ ],
20
+ "license": "ISC",
21
+ "author": "Traton Digital"
22
+ }
@@ -0,0 +1,3 @@
1
+ NEXT_PUBLIC_NSCOPE_API_KEY={{API_KEY}}
2
+ NEXT_PUBLIC_NSCOPE_BASE_URL={{BASE_URL}}
3
+ NEXT_PUBLIC_NSCOPE_REDIRECT_URI={{REDIRECT_URI}}
@@ -0,0 +1,21 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Starter Next.js App Router with [@nscope/react](https://www.npmjs.com/package/@nscope/react).
4
+
5
+ ## Setup
6
+
7
+ 1. Copy `.env.local.example` to `.env.local` and fill in your API key.
8
+ 2. Add `{{REDIRECT_URI}}` to **allowed domains** and as default return URL in Nscope.
9
+ 3. Run:
10
+
11
+ ```bash
12
+ pnpm install
13
+ pnpm dev
14
+ ```
15
+
16
+ Open [http://localhost:3000](http://localhost:3000).
17
+
18
+ ## Next steps
19
+
20
+ - Wrap protected pages with `<GuardedRoute>`
21
+ - See `/docs/sdk` in your Nscope dashboard
@@ -0,0 +1,30 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #18181b;
5
+ background: #fafafa;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ .page {
17
+ max-width: 640px;
18
+ margin: 4rem auto;
19
+ padding: 0 1rem;
20
+ }
21
+
22
+ button {
23
+ border: none;
24
+ border-radius: 8px;
25
+ background: #18181b;
26
+ color: white;
27
+ padding: 0.65rem 1rem;
28
+ font: inherit;
29
+ cursor: pointer;
30
+ }
@@ -0,0 +1,22 @@
1
+ import type { Metadata } from "next";
2
+ import { Providers } from "@/components/providers";
3
+ import { HomePage } from "@/components/home-page";
4
+ import "./globals.css";
5
+
6
+ export const metadata: Metadata = {
7
+ title: "{{PROJECT_NAME}}",
8
+ };
9
+
10
+ export default function RootLayout({
11
+ children,
12
+ }: Readonly<{
13
+ children: React.ReactNode;
14
+ }>) {
15
+ return (
16
+ <html lang="en">
17
+ <body>
18
+ <Providers>{children}</Providers>
19
+ </body>
20
+ </html>
21
+ );
22
+ }
@@ -0,0 +1,5 @@
1
+ import { HomePage } from "@/components/home-page";
2
+
3
+ export default function Page() {
4
+ return <HomePage />;
5
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { Can, useNscope } from "@nscope/react";
4
+
5
+ export function HomePage() {
6
+ const { user, isAuthenticated, isLoading, login, logout } = useNscope();
7
+
8
+ if (isLoading) {
9
+ return <main className="page">Loading…</main>;
10
+ }
11
+
12
+ return (
13
+ <main className="page">
14
+ <h1>{{PROJECT_NAME}}</h1>
15
+
16
+ {isAuthenticated && user ? (
17
+ <>
18
+ <p>Signed in as {user.name ?? user.email}</p>
19
+ <Can subject="users" action="LIST">
20
+ <p>You can list users.</p>
21
+ </Can>
22
+ <button type="button" onClick={() => logout()}>
23
+ Sign out
24
+ </button>
25
+ </>
26
+ ) : (
27
+ <button
28
+ type="button"
29
+ onClick={() =>
30
+ login({
31
+ redirectUri: process.env.NEXT_PUBLIC_NSCOPE_REDIRECT_URI,
32
+ })
33
+ }
34
+ >
35
+ Sign in with Nscope
36
+ </button>
37
+ )}
38
+ </main>
39
+ );
40
+ }
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { NscopeProvider } from "@nscope/react";
4
+ import { nscope } from "@/lib/nscope";
5
+
6
+ export function Providers({ children }: { children: React.ReactNode }) {
7
+ return <NscopeProvider config={nscope}>{children}</NscopeProvider>;
8
+ }
@@ -0,0 +1,10 @@
1
+ import { SDK } from "@nscope/sdk";
2
+
3
+ export const nscope = SDK.init({
4
+ baseUrl: process.env.NEXT_PUBLIC_NSCOPE_BASE_URL!,
5
+ apiKey: process.env.NEXT_PUBLIC_NSCOPE_API_KEY!,
6
+ auth: {
7
+ autoHandleCallback: true,
8
+ storage: "localStorage",
9
+ },
10
+ });
@@ -0,0 +1,2 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {};
4
+
5
+ export default nextConfig;
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "@nscope/react": "^1.0.0",
12
+ "@nscope/sdk": "^1.3.0",
13
+ "next": "^15.3.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "typescript": "^5.9.0"
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": {
18
+ "@/*": ["./*"]
19
+ }
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -0,0 +1,3 @@
1
+ VITE_NSCOPE_API_KEY={{API_KEY}}
2
+ VITE_NSCOPE_BASE_URL={{BASE_URL}}
3
+ VITE_NSCOPE_REDIRECT_URI={{REDIRECT_URI}}
@@ -0,0 +1,20 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Starter Vite + React with [@nscope/react](https://www.npmjs.com/package/@nscope/react).
4
+
5
+ ## Setup
6
+
7
+ 1. Copy `.env.example` to `.env` and fill in your API key from the Nscope dashboard.
8
+ 2. Add `{{REDIRECT_URI}}` to **allowed domains** and set it as the default return URL in project settings.
9
+ 3. Run:
10
+
11
+ ```bash
12
+ pnpm install
13
+ pnpm dev
14
+ ```
15
+
16
+ ## Next steps
17
+
18
+ - Customize login redirect in `src/App.tsx`
19
+ - Protect routes with `<GuardedRoute>` from `@nscope/react`
20
+ - Read the SDK docs in your Nscope panel at `/docs/sdk`
@@ -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>{{PROJECT_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,24 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@nscope/react": "^1.0.0",
13
+ "@nscope/sdk": "^1.3.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "@vitejs/plugin-react": "^4.4.0",
21
+ "typescript": "^5.9.0",
22
+ "vite": "^6.3.0"
23
+ }
24
+ }
@@ -0,0 +1,31 @@
1
+ import { Can, useNscope } from "@nscope/react";
2
+
3
+ export default function App() {
4
+ const { user, isAuthenticated, isLoading, login, logout } = useNscope();
5
+
6
+ if (isLoading) {
7
+ return <main className="page">Loading…</main>;
8
+ }
9
+
10
+ return (
11
+ <main className="page">
12
+ <h1>{{PROJECT_NAME}}</h1>
13
+
14
+ {isAuthenticated && user ? (
15
+ <>
16
+ <p>Signed in as {user.name ?? user.email}</p>
17
+ <Can subject="users" action="LIST">
18
+ <p>You can list users.</p>
19
+ </Can>
20
+ <button type="button" onClick={() => logout()}>
21
+ Sign out
22
+ </button>
23
+ </>
24
+ ) : (
25
+ <button type="button" onClick={() => login({ redirectUri: import.meta.env.VITE_NSCOPE_REDIRECT_URI })}>
26
+ Sign in with Nscope
27
+ </button>
28
+ )}
29
+ </main>
30
+ );
31
+ }
@@ -0,0 +1,30 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #18181b;
5
+ background: #fafafa;
6
+ }
7
+
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ .page {
17
+ max-width: 640px;
18
+ margin: 4rem auto;
19
+ padding: 0 1rem;
20
+ }
21
+
22
+ button {
23
+ border: none;
24
+ border-radius: 8px;
25
+ background: #18181b;
26
+ color: white;
27
+ padding: 0.65rem 1rem;
28
+ font: inherit;
29
+ cursor: pointer;
30
+ }
@@ -0,0 +1,14 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { NscopeProvider } from "@nscope/react";
4
+ import App from "./App";
5
+ import { nscope } from "./nscope";
6
+ import "./index.css";
7
+
8
+ createRoot(document.getElementById("root")!).render(
9
+ <StrictMode>
10
+ <NscopeProvider config={nscope}>
11
+ <App />
12
+ </NscopeProvider>
13
+ </StrictMode>,
14
+ );
@@ -0,0 +1,10 @@
1
+ import { SDK } from "@nscope/sdk";
2
+
3
+ export const nscope = SDK.init({
4
+ baseUrl: import.meta.env.VITE_NSCOPE_BASE_URL,
5
+ apiKey: import.meta.env.VITE_NSCOPE_API_KEY,
6
+ auth: {
7
+ autoHandleCallback: true,
8
+ storage: "localStorage",
9
+ },
10
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true
19
+ },
20
+ "include": ["src"]
21
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ });