@drmhse/authos-cli 0.1.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.
package/dist/bin.js ADDED
@@ -0,0 +1,1170 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var commander = require('commander');
5
+ var path2 = require('path');
6
+ var prompts2 = require('prompts');
7
+ var child_process = require('child_process');
8
+ var fs = require('fs');
9
+ var pc = require('picocolors');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
32
+ var prompts2__default = /*#__PURE__*/_interopDefault(prompts2);
33
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
34
+ var pc__default = /*#__PURE__*/_interopDefault(pc);
35
+
36
+ function detectFramework(cwd) {
37
+ const packageJsonPath = path2__namespace.join(cwd, "package.json");
38
+ if (!fs__namespace.existsSync(packageJsonPath)) {
39
+ return "unknown";
40
+ }
41
+ try {
42
+ const content = fs__namespace.readFileSync(packageJsonPath, "utf8");
43
+ const pkg = JSON.parse(content);
44
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
45
+ if (deps["nuxt"] || deps["nuxt3"]) {
46
+ return "nuxt";
47
+ }
48
+ if (deps["next"]) {
49
+ return "next";
50
+ }
51
+ if (deps["vue"]) {
52
+ return "vue";
53
+ }
54
+ if (deps["react"]) {
55
+ return "react";
56
+ }
57
+ return "unknown";
58
+ } catch {
59
+ return "unknown";
60
+ }
61
+ }
62
+ function getAdapterPackage(framework) {
63
+ switch (framework) {
64
+ case "react":
65
+ case "next":
66
+ return "@drmhse/authos-react";
67
+ case "vue":
68
+ case "nuxt":
69
+ return "@drmhse/authos-vue";
70
+ default:
71
+ return null;
72
+ }
73
+ }
74
+ function writeFile(filePath, content) {
75
+ const dir = path2__namespace.dirname(filePath);
76
+ if (!fs__namespace.existsSync(dir)) {
77
+ fs__namespace.mkdirSync(dir, { recursive: true });
78
+ }
79
+ fs__namespace.writeFileSync(filePath, content, "utf8");
80
+ }
81
+ function fileExists(filePath) {
82
+ return fs__namespace.existsSync(filePath);
83
+ }
84
+ function appendToFile(filePath, content) {
85
+ if (fs__namespace.existsSync(filePath)) {
86
+ const existing = fs__namespace.readFileSync(filePath, "utf8");
87
+ if (!existing.includes(content.trim())) {
88
+ fs__namespace.appendFileSync(filePath, "\n" + content);
89
+ }
90
+ } else {
91
+ writeFile(filePath, content);
92
+ }
93
+ }
94
+ function findComponentsDir(cwd) {
95
+ const candidates = [
96
+ "src/components",
97
+ "components",
98
+ "app/components",
99
+ "src/app/components"
100
+ ];
101
+ for (const candidate of candidates) {
102
+ const fullPath = path2__namespace.join(cwd, candidate);
103
+ if (fs__namespace.existsSync(fullPath)) {
104
+ return fullPath;
105
+ }
106
+ }
107
+ return path2__namespace.join(cwd, "src", "components");
108
+ }
109
+ function getFileExtension(framework) {
110
+ switch (framework) {
111
+ case "vue":
112
+ case "nuxt":
113
+ return ".vue";
114
+ case "react":
115
+ case "next":
116
+ default:
117
+ return ".tsx";
118
+ }
119
+ }
120
+ var log = {
121
+ info: (msg) => console.log(pc__default.default.blue("i"), msg),
122
+ success: (msg) => console.log(pc__default.default.green("\u2713"), msg),
123
+ warn: (msg) => console.log(pc__default.default.yellow("!"), msg),
124
+ error: (msg) => console.log(pc__default.default.red("\u2717"), msg),
125
+ step: (msg) => console.log(pc__default.default.cyan("\u2192"), msg)
126
+ };
127
+ function getFrameworkName(framework) {
128
+ switch (framework) {
129
+ case "react":
130
+ return "React";
131
+ case "vue":
132
+ return "Vue";
133
+ case "next":
134
+ return "Next.js";
135
+ case "nuxt":
136
+ return "Nuxt";
137
+ default:
138
+ return "Unknown";
139
+ }
140
+ }
141
+
142
+ // src/commands/init.ts
143
+ async function initCommand(options = {}) {
144
+ const cwd = options.cwd || process.cwd();
145
+ log.info("Initializing AuthOS...\n");
146
+ let framework = detectFramework(cwd);
147
+ if (framework === "unknown") {
148
+ log.warn("Could not detect project framework from package.json.");
149
+ const response = await prompts2__default.default({
150
+ type: "select",
151
+ name: "framework",
152
+ message: "Select your framework:",
153
+ choices: [
154
+ { title: "React", value: "react" },
155
+ { title: "Next.js", value: "next" },
156
+ { title: "Vue", value: "vue" },
157
+ { title: "Nuxt", value: "nuxt" }
158
+ ]
159
+ });
160
+ if (!response.framework) {
161
+ log.error("Initialization cancelled.");
162
+ process.exit(1);
163
+ }
164
+ framework = response.framework;
165
+ } else {
166
+ log.success(`Detected ${getFrameworkName(framework)} project`);
167
+ }
168
+ const adapterPackage = getAdapterPackage(framework);
169
+ if (!adapterPackage) {
170
+ log.error("Unsupported framework.");
171
+ process.exit(1);
172
+ }
173
+ const urlResponse = await prompts2__default.default({
174
+ type: "text",
175
+ name: "baseUrl",
176
+ message: "Enter your AuthOS base URL:",
177
+ initial: "https://auth.example.com",
178
+ validate: (value) => {
179
+ try {
180
+ new URL(value);
181
+ return true;
182
+ } catch {
183
+ return "Please enter a valid URL";
184
+ }
185
+ }
186
+ });
187
+ if (!urlResponse.baseUrl) {
188
+ log.error("Initialization cancelled.");
189
+ process.exit(1);
190
+ }
191
+ const baseUrl = urlResponse.baseUrl;
192
+ if (!options.skipInstall) {
193
+ log.step(`Installing ${adapterPackage}...`);
194
+ try {
195
+ const packageManager = detectPackageManager(cwd);
196
+ const installCmd = getInstallCommand(packageManager, adapterPackage);
197
+ child_process.execSync(installCmd, { cwd, stdio: "inherit" });
198
+ log.success(`Installed ${adapterPackage}`);
199
+ } catch (error) {
200
+ log.error(`Failed to install ${adapterPackage}`);
201
+ log.info(`You can install it manually: npm install ${adapterPackage}`);
202
+ }
203
+ }
204
+ const envPath = path2__namespace.join(cwd, ".env");
205
+ const envLocalPath = path2__namespace.join(cwd, ".env.local");
206
+ const envContent = `AUTHOS_BASE_URL=${baseUrl}
207
+ `;
208
+ const targetEnvPath = framework === "next" || framework === "nuxt" ? envLocalPath : envPath;
209
+ if (fileExists(targetEnvPath)) {
210
+ appendToFile(targetEnvPath, envContent);
211
+ log.success(`Updated ${path2__namespace.basename(targetEnvPath)}`);
212
+ } else {
213
+ appendToFile(targetEnvPath, envContent);
214
+ log.success(`Created ${path2__namespace.basename(targetEnvPath)}`);
215
+ }
216
+ console.log("\n");
217
+ log.success("AuthOS initialized successfully!\n");
218
+ console.log("Next steps:\n");
219
+ if (framework === "react" || framework === "next") {
220
+ console.log(" 1. Wrap your app with AuthOSProvider:\n");
221
+ console.log(' import { AuthOSProvider } from "@drmhse/authos-react";');
222
+ console.log("");
223
+ console.log(" <AuthOSProvider baseURL={process.env.AUTHOS_BASE_URL}>");
224
+ console.log(" <App />");
225
+ console.log(" </AuthOSProvider>\n");
226
+ } else if (framework === "vue" || framework === "nuxt") {
227
+ console.log(" 1. Install the Vue plugin:\n");
228
+ console.log(' import { createAuthOS } from "@drmhse/authos-vue";');
229
+ console.log("");
230
+ console.log(" app.use(createAuthOS({");
231
+ console.log(" baseURL: import.meta.env.VITE_AUTHOS_BASE_URL,");
232
+ console.log(" }));\n");
233
+ }
234
+ console.log(" 2. Add authentication components:");
235
+ console.log(" npx authos add login-form");
236
+ console.log(" npx authos add user-profile\n");
237
+ console.log(" 3. Check out the docs: https://authos.dev/docs\n");
238
+ }
239
+ function detectPackageManager(cwd) {
240
+ if (fileExists(path2__namespace.join(cwd, "bun.lockb"))) {
241
+ return "bun";
242
+ }
243
+ if (fileExists(path2__namespace.join(cwd, "pnpm-lock.yaml"))) {
244
+ return "pnpm";
245
+ }
246
+ if (fileExists(path2__namespace.join(cwd, "yarn.lock"))) {
247
+ return "yarn";
248
+ }
249
+ return "npm";
250
+ }
251
+ function getInstallCommand(pm, pkg) {
252
+ switch (pm) {
253
+ case "yarn":
254
+ return `yarn add ${pkg}`;
255
+ case "pnpm":
256
+ return `pnpm add ${pkg}`;
257
+ case "bun":
258
+ return `bun add ${pkg}`;
259
+ default:
260
+ return `npm install ${pkg}`;
261
+ }
262
+ }
263
+
264
+ // src/templates/registry.ts
265
+ var loginFormReact = `"use client";
266
+
267
+ import { useState } from "react";
268
+ import { useAuthOS } from "@drmhse/authos-react";
269
+ import { AuthErrorCodes } from "@drmhse/sso-sdk";
270
+
271
+ interface LoginFormProps {
272
+ onSuccess?: () => void;
273
+ redirectTo?: string;
274
+ }
275
+
276
+ export function LoginForm({ onSuccess, redirectTo }: LoginFormProps) {
277
+ const { client } = useAuthOS();
278
+ const [email, setEmail] = useState("");
279
+ const [password, setPassword] = useState("");
280
+ const [error, setError] = useState<string | null>(null);
281
+ const [loading, setLoading] = useState(false);
282
+ const [mfaRequired, setMfaRequired] = useState(false);
283
+ const [mfaCode, setMfaCode] = useState("");
284
+ const [mfaToken, setMfaToken] = useState<string | null>(null);
285
+
286
+ const handleSubmit = async (e: React.FormEvent) => {
287
+ e.preventDefault();
288
+ setError(null);
289
+ setLoading(true);
290
+
291
+ try {
292
+ const response = await client.auth.login({ email, password });
293
+
294
+ if (response.mfa_required) {
295
+ setMfaRequired(true);
296
+ setMfaToken(response.mfa_token || null);
297
+ } else {
298
+ onSuccess?.();
299
+ if (redirectTo) {
300
+ window.location.href = redirectTo;
301
+ }
302
+ }
303
+ } catch (err: unknown) {
304
+ if (err && typeof err === "object" && "errorCode" in err) {
305
+ const apiError = err as { errorCode: string; message: string };
306
+ if (apiError.errorCode === AuthErrorCodes.MFA_REQUIRED) {
307
+ setMfaRequired(true);
308
+ } else {
309
+ setError(apiError.message || "Login failed");
310
+ }
311
+ } else {
312
+ setError("An unexpected error occurred");
313
+ }
314
+ } finally {
315
+ setLoading(false);
316
+ }
317
+ };
318
+
319
+ const handleMfaSubmit = async (e: React.FormEvent) => {
320
+ e.preventDefault();
321
+ setError(null);
322
+ setLoading(true);
323
+
324
+ try {
325
+ await client.auth.verifyMfa({ code: mfaCode, mfa_token: mfaToken! });
326
+ onSuccess?.();
327
+ if (redirectTo) {
328
+ window.location.href = redirectTo;
329
+ }
330
+ } catch (err: unknown) {
331
+ if (err && typeof err === "object" && "message" in err) {
332
+ setError((err as { message: string }).message || "Invalid MFA code");
333
+ } else {
334
+ setError("Invalid MFA code");
335
+ }
336
+ } finally {
337
+ setLoading(false);
338
+ }
339
+ };
340
+
341
+ if (mfaRequired) {
342
+ return (
343
+ <form onSubmit={handleMfaSubmit} className="space-y-4 w-full max-w-sm">
344
+ <div className="text-center mb-6">
345
+ <h2 className="text-xl font-semibold">Two-Factor Authentication</h2>
346
+ <p className="text-gray-600 text-sm mt-1">
347
+ Enter the code from your authenticator app
348
+ </p>
349
+ </div>
350
+
351
+ {error && (
352
+ <div className="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm">
353
+ {error}
354
+ </div>
355
+ )}
356
+
357
+ <div>
358
+ <label htmlFor="mfa-code" className="block text-sm font-medium mb-1">
359
+ Verification Code
360
+ </label>
361
+ <input
362
+ id="mfa-code"
363
+ type="text"
364
+ inputMode="numeric"
365
+ autoComplete="one-time-code"
366
+ value={mfaCode}
367
+ onChange={(e) => setMfaCode(e.target.value)}
368
+ className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
369
+ placeholder="000000"
370
+ maxLength={6}
371
+ required
372
+ />
373
+ </div>
374
+
375
+ <button
376
+ type="submit"
377
+ disabled={loading}
378
+ className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
379
+ >
380
+ {loading ? "Verifying..." : "Verify"}
381
+ </button>
382
+
383
+ <button
384
+ type="button"
385
+ onClick={() => setMfaRequired(false)}
386
+ className="w-full text-gray-600 text-sm hover:underline"
387
+ >
388
+ Back to login
389
+ </button>
390
+ </form>
391
+ );
392
+ }
393
+
394
+ return (
395
+ <form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
396
+ <div className="text-center mb-6">
397
+ <h2 className="text-xl font-semibold">Sign In</h2>
398
+ <p className="text-gray-600 text-sm mt-1">
399
+ Enter your credentials to continue
400
+ </p>
401
+ </div>
402
+
403
+ {error && (
404
+ <div className="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm">
405
+ {error}
406
+ </div>
407
+ )}
408
+
409
+ <div>
410
+ <label htmlFor="email" className="block text-sm font-medium mb-1">
411
+ Email
412
+ </label>
413
+ <input
414
+ id="email"
415
+ type="email"
416
+ autoComplete="email"
417
+ value={email}
418
+ onChange={(e) => setEmail(e.target.value)}
419
+ className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
420
+ placeholder="you@example.com"
421
+ required
422
+ />
423
+ </div>
424
+
425
+ <div>
426
+ <label htmlFor="password" className="block text-sm font-medium mb-1">
427
+ Password
428
+ </label>
429
+ <input
430
+ id="password"
431
+ type="password"
432
+ autoComplete="current-password"
433
+ value={password}
434
+ onChange={(e) => setPassword(e.target.value)}
435
+ className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
436
+ placeholder="Your password"
437
+ required
438
+ />
439
+ </div>
440
+
441
+ <button
442
+ type="submit"
443
+ disabled={loading}
444
+ className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
445
+ >
446
+ {loading ? "Signing in..." : "Sign In"}
447
+ </button>
448
+ </form>
449
+ );
450
+ }
451
+ `;
452
+ var loginFormVue = `<script setup lang="ts">
453
+ import { ref } from "vue";
454
+ import { useAuthOS } from "@drmhse/authos-vue";
455
+ import { AuthErrorCodes } from "@drmhse/sso-sdk";
456
+
457
+ interface Props {
458
+ redirectTo?: string;
459
+ }
460
+
461
+ const props = defineProps<Props>();
462
+ const emit = defineEmits<{
463
+ success: [];
464
+ }>();
465
+
466
+ const { client } = useAuthOS();
467
+
468
+ const email = ref("");
469
+ const password = ref("");
470
+ const error = ref<string | null>(null);
471
+ const loading = ref(false);
472
+ const mfaRequired = ref(false);
473
+ const mfaCode = ref("");
474
+ const mfaToken = ref<string | null>(null);
475
+
476
+ async function handleSubmit() {
477
+ error.value = null;
478
+ loading.value = true;
479
+
480
+ try {
481
+ const response = await client.auth.login({
482
+ email: email.value,
483
+ password: password.value,
484
+ });
485
+
486
+ if (response.mfa_required) {
487
+ mfaRequired.value = true;
488
+ mfaToken.value = response.mfa_token || null;
489
+ } else {
490
+ emit("success");
491
+ if (props.redirectTo) {
492
+ window.location.href = props.redirectTo;
493
+ }
494
+ }
495
+ } catch (err: unknown) {
496
+ if (err && typeof err === "object" && "errorCode" in err) {
497
+ const apiError = err as { errorCode: string; message: string };
498
+ if (apiError.errorCode === AuthErrorCodes.MFA_REQUIRED) {
499
+ mfaRequired.value = true;
500
+ } else {
501
+ error.value = apiError.message || "Login failed";
502
+ }
503
+ } else {
504
+ error.value = "An unexpected error occurred";
505
+ }
506
+ } finally {
507
+ loading.value = false;
508
+ }
509
+ }
510
+
511
+ async function handleMfaSubmit() {
512
+ error.value = null;
513
+ loading.value = true;
514
+
515
+ try {
516
+ await client.auth.verifyMfa({
517
+ code: mfaCode.value,
518
+ mfa_token: mfaToken.value!,
519
+ });
520
+ emit("success");
521
+ if (props.redirectTo) {
522
+ window.location.href = props.redirectTo;
523
+ }
524
+ } catch (err: unknown) {
525
+ if (err && typeof err === "object" && "message" in err) {
526
+ error.value = (err as { message: string }).message || "Invalid MFA code";
527
+ } else {
528
+ error.value = "Invalid MFA code";
529
+ }
530
+ } finally {
531
+ loading.value = false;
532
+ }
533
+ }
534
+
535
+ function backToLogin() {
536
+ mfaRequired.value = false;
537
+ mfaCode.value = "";
538
+ error.value = null;
539
+ }
540
+ </script>
541
+
542
+ <template>
543
+ <form
544
+ v-if="mfaRequired"
545
+ @submit.prevent="handleMfaSubmit"
546
+ class="space-y-4 w-full max-w-sm"
547
+ >
548
+ <div class="text-center mb-6">
549
+ <h2 class="text-xl font-semibold">Two-Factor Authentication</h2>
550
+ <p class="text-gray-600 text-sm mt-1">
551
+ Enter the code from your authenticator app
552
+ </p>
553
+ </div>
554
+
555
+ <div
556
+ v-if="error"
557
+ class="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm"
558
+ >
559
+ {{ error }}
560
+ </div>
561
+
562
+ <div>
563
+ <label for="mfa-code" class="block text-sm font-medium mb-1">
564
+ Verification Code
565
+ </label>
566
+ <input
567
+ id="mfa-code"
568
+ v-model="mfaCode"
569
+ type="text"
570
+ inputmode="numeric"
571
+ autocomplete="one-time-code"
572
+ class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
573
+ placeholder="000000"
574
+ maxlength="6"
575
+ required
576
+ />
577
+ </div>
578
+
579
+ <button
580
+ type="submit"
581
+ :disabled="loading"
582
+ class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
583
+ >
584
+ {{ loading ? "Verifying..." : "Verify" }}
585
+ </button>
586
+
587
+ <button
588
+ type="button"
589
+ @click="backToLogin"
590
+ class="w-full text-gray-600 text-sm hover:underline"
591
+ >
592
+ Back to login
593
+ </button>
594
+ </form>
595
+
596
+ <form v-else @submit.prevent="handleSubmit" class="space-y-4 w-full max-w-sm">
597
+ <div class="text-center mb-6">
598
+ <h2 class="text-xl font-semibold">Sign In</h2>
599
+ <p class="text-gray-600 text-sm mt-1">
600
+ Enter your credentials to continue
601
+ </p>
602
+ </div>
603
+
604
+ <div
605
+ v-if="error"
606
+ class="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm"
607
+ >
608
+ {{ error }}
609
+ </div>
610
+
611
+ <div>
612
+ <label for="email" class="block text-sm font-medium mb-1"> Email </label>
613
+ <input
614
+ id="email"
615
+ v-model="email"
616
+ type="email"
617
+ autocomplete="email"
618
+ class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
619
+ placeholder="you@example.com"
620
+ required
621
+ />
622
+ </div>
623
+
624
+ <div>
625
+ <label for="password" class="block text-sm font-medium mb-1">
626
+ Password
627
+ </label>
628
+ <input
629
+ id="password"
630
+ v-model="password"
631
+ type="password"
632
+ autocomplete="current-password"
633
+ class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
634
+ placeholder="Your password"
635
+ required
636
+ />
637
+ </div>
638
+
639
+ <button
640
+ type="submit"
641
+ :disabled="loading"
642
+ class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
643
+ >
644
+ {{ loading ? "Signing in..." : "Sign In" }}
645
+ </button>
646
+ </form>
647
+ </template>
648
+ `;
649
+ var orgSwitcherReact = `"use client";
650
+
651
+ import { useState, useRef, useEffect } from "react";
652
+ import { useOrganization, useUser } from "@drmhse/authos-react";
653
+
654
+ export function OrganizationSwitcher() {
655
+ const { organization, organizations, switchOrganization, loading } = useOrganization();
656
+ const { user } = useUser();
657
+ const [open, setOpen] = useState(false);
658
+ const ref = useRef<HTMLDivElement>(null);
659
+
660
+ useEffect(() => {
661
+ function handleClickOutside(event: MouseEvent) {
662
+ if (ref.current && !ref.current.contains(event.target as Node)) {
663
+ setOpen(false);
664
+ }
665
+ }
666
+
667
+ document.addEventListener("mousedown", handleClickOutside);
668
+ return () => document.removeEventListener("mousedown", handleClickOutside);
669
+ }, []);
670
+
671
+ if (!user || organizations.length === 0) {
672
+ return null;
673
+ }
674
+
675
+ const handleSwitch = async (slug: string) => {
676
+ await switchOrganization(slug);
677
+ setOpen(false);
678
+ };
679
+
680
+ return (
681
+ <div ref={ref} className="relative">
682
+ <button
683
+ onClick={() => setOpen(!open)}
684
+ disabled={loading}
685
+ className="flex items-center gap-2 px-3 py-2 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50"
686
+ >
687
+ <span className="font-medium">
688
+ {organization?.name || "Select Organization"}
689
+ </span>
690
+ <svg
691
+ className={\`w-4 h-4 transition-transform \${open ? "rotate-180" : ""}\`}
692
+ fill="none"
693
+ stroke="currentColor"
694
+ viewBox="0 0 24 24"
695
+ >
696
+ <path
697
+ strokeLinecap="round"
698
+ strokeLinejoin="round"
699
+ strokeWidth={2}
700
+ d="M19 9l-7 7-7-7"
701
+ />
702
+ </svg>
703
+ </button>
704
+
705
+ {open && (
706
+ <div className="absolute right-0 mt-2 w-56 bg-white border rounded-md shadow-lg z-50">
707
+ <div className="py-1">
708
+ {organizations.map((org) => (
709
+ <button
710
+ key={org.slug}
711
+ onClick={() => handleSwitch(org.slug)}
712
+ className={\`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center justify-between \${
713
+ org.slug === organization?.slug ? "bg-blue-50" : ""
714
+ }\`}
715
+ >
716
+ <span>{org.name}</span>
717
+ {org.slug === organization?.slug && (
718
+ <svg
719
+ className="w-4 h-4 text-blue-600"
720
+ fill="currentColor"
721
+ viewBox="0 0 20 20"
722
+ >
723
+ <path
724
+ fillRule="evenodd"
725
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
726
+ clipRule="evenodd"
727
+ />
728
+ </svg>
729
+ )}
730
+ </button>
731
+ ))}
732
+ </div>
733
+ </div>
734
+ )}
735
+ </div>
736
+ );
737
+ }
738
+ `;
739
+ var orgSwitcherVue = `<script setup lang="ts">
740
+ import { ref, onMounted, onUnmounted } from "vue";
741
+ import { useOrganization, useUser } from "@drmhse/authos-vue";
742
+
743
+ const { organization, organizations, switchOrganization, loading } = useOrganization();
744
+ const { user } = useUser();
745
+
746
+ const open = ref(false);
747
+ const dropdownRef = ref<HTMLDivElement | null>(null);
748
+
749
+ function handleClickOutside(event: MouseEvent) {
750
+ if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
751
+ open.value = false;
752
+ }
753
+ }
754
+
755
+ onMounted(() => {
756
+ document.addEventListener("mousedown", handleClickOutside);
757
+ });
758
+
759
+ onUnmounted(() => {
760
+ document.removeEventListener("mousedown", handleClickOutside);
761
+ });
762
+
763
+ async function handleSwitch(slug: string) {
764
+ await switchOrganization(slug);
765
+ open.value = false;
766
+ }
767
+ </script>
768
+
769
+ <template>
770
+ <div v-if="user && organizations.length > 0" ref="dropdownRef" class="relative">
771
+ <button
772
+ @click="open = !open"
773
+ :disabled="loading"
774
+ class="flex items-center gap-2 px-3 py-2 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50"
775
+ >
776
+ <span class="font-medium">
777
+ {{ organization?.name || "Select Organization" }}
778
+ </span>
779
+ <svg
780
+ :class="['w-4 h-4 transition-transform', open ? 'rotate-180' : '']"
781
+ fill="none"
782
+ stroke="currentColor"
783
+ viewBox="0 0 24 24"
784
+ >
785
+ <path
786
+ stroke-linecap="round"
787
+ stroke-linejoin="round"
788
+ stroke-width="2"
789
+ d="M19 9l-7 7-7-7"
790
+ />
791
+ </svg>
792
+ </button>
793
+
794
+ <div
795
+ v-if="open"
796
+ class="absolute right-0 mt-2 w-56 bg-white border rounded-md shadow-lg z-50"
797
+ >
798
+ <div class="py-1">
799
+ <button
800
+ v-for="org in organizations"
801
+ :key="org.slug"
802
+ @click="handleSwitch(org.slug)"
803
+ :class="[
804
+ 'w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center justify-between',
805
+ org.slug === organization?.slug ? 'bg-blue-50' : '',
806
+ ]"
807
+ >
808
+ <span>{{ org.name }}</span>
809
+ <svg
810
+ v-if="org.slug === organization?.slug"
811
+ class="w-4 h-4 text-blue-600"
812
+ fill="currentColor"
813
+ viewBox="0 0 20 20"
814
+ >
815
+ <path
816
+ fill-rule="evenodd"
817
+ d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
818
+ clip-rule="evenodd"
819
+ />
820
+ </svg>
821
+ </button>
822
+ </div>
823
+ </div>
824
+ </div>
825
+ </template>
826
+ `;
827
+ var userProfileReact = `"use client";
828
+
829
+ import { useState, useRef, useEffect } from "react";
830
+ import { useUser, useAuthOS } from "@drmhse/authos-react";
831
+
832
+ interface UserProfileProps {
833
+ onSignOut?: () => void;
834
+ }
835
+
836
+ export function UserProfile({ onSignOut }: UserProfileProps) {
837
+ const { user } = useUser();
838
+ const { client } = useAuthOS();
839
+ const [open, setOpen] = useState(false);
840
+ const ref = useRef<HTMLDivElement>(null);
841
+
842
+ useEffect(() => {
843
+ function handleClickOutside(event: MouseEvent) {
844
+ if (ref.current && !ref.current.contains(event.target as Node)) {
845
+ setOpen(false);
846
+ }
847
+ }
848
+
849
+ document.addEventListener("mousedown", handleClickOutside);
850
+ return () => document.removeEventListener("mousedown", handleClickOutside);
851
+ }, []);
852
+
853
+ if (!user) {
854
+ return null;
855
+ }
856
+
857
+ const handleSignOut = async () => {
858
+ await client.auth.logout();
859
+ onSignOut?.();
860
+ setOpen(false);
861
+ };
862
+
863
+ const initials = user.name
864
+ ? user.name
865
+ .split(" ")
866
+ .map((n) => n[0])
867
+ .join("")
868
+ .toUpperCase()
869
+ .slice(0, 2)
870
+ : user.email[0].toUpperCase();
871
+
872
+ return (
873
+ <div ref={ref} className="relative">
874
+ <button
875
+ onClick={() => setOpen(!open)}
876
+ className="flex items-center gap-2 p-1 rounded-full hover:bg-gray-100"
877
+ >
878
+ {user.avatar_url ? (
879
+ <img
880
+ src={user.avatar_url}
881
+ alt={user.name || user.email}
882
+ className="w-8 h-8 rounded-full object-cover"
883
+ />
884
+ ) : (
885
+ <div className="w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center text-sm font-medium">
886
+ {initials}
887
+ </div>
888
+ )}
889
+ </button>
890
+
891
+ {open && (
892
+ <div className="absolute right-0 mt-2 w-64 bg-white border rounded-md shadow-lg z-50">
893
+ <div className="p-4 border-b">
894
+ <div className="font-medium">{user.name || "User"}</div>
895
+ <div className="text-sm text-gray-600">{user.email}</div>
896
+ </div>
897
+ <div className="py-1">
898
+ <button
899
+ onClick={handleSignOut}
900
+ className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
901
+ >
902
+ Sign out
903
+ </button>
904
+ </div>
905
+ </div>
906
+ )}
907
+ </div>
908
+ );
909
+ }
910
+ `;
911
+ var userProfileVue = `<script setup lang="ts">
912
+ import { ref, computed, onMounted, onUnmounted } from "vue";
913
+ import { useUser, useAuthOS } from "@drmhse/authos-vue";
914
+
915
+ const emit = defineEmits<{
916
+ signOut: [];
917
+ }>();
918
+
919
+ const { user } = useUser();
920
+ const { client } = useAuthOS();
921
+
922
+ const open = ref(false);
923
+ const dropdownRef = ref<HTMLDivElement | null>(null);
924
+
925
+ const initials = computed(() => {
926
+ if (!user.value) return "";
927
+ if (user.value.name) {
928
+ return user.value.name
929
+ .split(" ")
930
+ .map((n) => n[0])
931
+ .join("")
932
+ .toUpperCase()
933
+ .slice(0, 2);
934
+ }
935
+ return user.value.email[0].toUpperCase();
936
+ });
937
+
938
+ function handleClickOutside(event: MouseEvent) {
939
+ if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
940
+ open.value = false;
941
+ }
942
+ }
943
+
944
+ onMounted(() => {
945
+ document.addEventListener("mousedown", handleClickOutside);
946
+ });
947
+
948
+ onUnmounted(() => {
949
+ document.removeEventListener("mousedown", handleClickOutside);
950
+ });
951
+
952
+ async function handleSignOut() {
953
+ await client.auth.logout();
954
+ emit("signOut");
955
+ open.value = false;
956
+ }
957
+ </script>
958
+
959
+ <template>
960
+ <div v-if="user" ref="dropdownRef" class="relative">
961
+ <button
962
+ @click="open = !open"
963
+ class="flex items-center gap-2 p-1 rounded-full hover:bg-gray-100"
964
+ >
965
+ <img
966
+ v-if="user.avatar_url"
967
+ :src="user.avatar_url"
968
+ :alt="user.name || user.email"
969
+ class="w-8 h-8 rounded-full object-cover"
970
+ />
971
+ <div
972
+ v-else
973
+ class="w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center text-sm font-medium"
974
+ >
975
+ {{ initials }}
976
+ </div>
977
+ </button>
978
+
979
+ <div
980
+ v-if="open"
981
+ class="absolute right-0 mt-2 w-64 bg-white border rounded-md shadow-lg z-50"
982
+ >
983
+ <div class="p-4 border-b">
984
+ <div class="font-medium">{{ user.name || "User" }}</div>
985
+ <div class="text-sm text-gray-600">{{ user.email }}</div>
986
+ </div>
987
+ <div class="py-1">
988
+ <button
989
+ @click="handleSignOut"
990
+ class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
991
+ >
992
+ Sign out
993
+ </button>
994
+ </div>
995
+ </div>
996
+ </div>
997
+ </template>
998
+ `;
999
+ var templates = {
1000
+ "login-form": {
1001
+ name: "Login Form",
1002
+ description: "A styled login form with email/password and MFA support",
1003
+ files: [
1004
+ {
1005
+ name: "LoginForm",
1006
+ content: {
1007
+ react: loginFormReact,
1008
+ next: loginFormReact,
1009
+ vue: loginFormVue,
1010
+ nuxt: loginFormVue,
1011
+ unknown: loginFormReact
1012
+ }
1013
+ }
1014
+ ]
1015
+ },
1016
+ "org-switcher": {
1017
+ name: "Organization Switcher",
1018
+ description: "A dropdown component for switching between organizations",
1019
+ files: [
1020
+ {
1021
+ name: "OrganizationSwitcher",
1022
+ content: {
1023
+ react: orgSwitcherReact,
1024
+ next: orgSwitcherReact,
1025
+ vue: orgSwitcherVue,
1026
+ nuxt: orgSwitcherVue,
1027
+ unknown: orgSwitcherReact
1028
+ }
1029
+ }
1030
+ ]
1031
+ },
1032
+ "user-profile": {
1033
+ name: "User Profile",
1034
+ description: "A user avatar button with dropdown menu and sign out",
1035
+ files: [
1036
+ {
1037
+ name: "UserProfile",
1038
+ content: {
1039
+ react: userProfileReact,
1040
+ next: userProfileReact,
1041
+ vue: userProfileVue,
1042
+ nuxt: userProfileVue,
1043
+ unknown: userProfileReact
1044
+ }
1045
+ }
1046
+ ]
1047
+ }
1048
+ };
1049
+ function getAvailableTemplates() {
1050
+ return Object.keys(templates);
1051
+ }
1052
+ function getTemplate(name) {
1053
+ return templates[name] || null;
1054
+ }
1055
+
1056
+ // src/commands/add.ts
1057
+ async function addCommand(templateName, options = {}) {
1058
+ const cwd = options.cwd || process.cwd();
1059
+ let framework = detectFramework(cwd);
1060
+ if (framework === "unknown") {
1061
+ log.warn("Could not detect project framework from package.json.");
1062
+ const response = await prompts2__default.default({
1063
+ type: "select",
1064
+ name: "framework",
1065
+ message: "Select your framework:",
1066
+ choices: [
1067
+ { title: "React", value: "react" },
1068
+ { title: "Next.js", value: "next" },
1069
+ { title: "Vue", value: "vue" },
1070
+ { title: "Nuxt", value: "nuxt" }
1071
+ ]
1072
+ });
1073
+ if (!response.framework) {
1074
+ log.error("Command cancelled.");
1075
+ process.exit(1);
1076
+ }
1077
+ framework = response.framework;
1078
+ } else {
1079
+ log.info(`Detected ${getFrameworkName(framework)} project`);
1080
+ }
1081
+ if (!templateName) {
1082
+ const availableTemplates = getAvailableTemplates();
1083
+ const choices = availableTemplates.map((name) => {
1084
+ const template2 = getTemplate(name);
1085
+ return {
1086
+ title: template2.name,
1087
+ description: template2.description,
1088
+ value: name
1089
+ };
1090
+ });
1091
+ const response = await prompts2__default.default({
1092
+ type: "select",
1093
+ name: "template",
1094
+ message: "Select a component to add:",
1095
+ choices
1096
+ });
1097
+ if (!response.template) {
1098
+ log.error("Command cancelled.");
1099
+ process.exit(1);
1100
+ }
1101
+ templateName = response.template;
1102
+ }
1103
+ const template = getTemplate(templateName);
1104
+ if (!template) {
1105
+ log.error(`Unknown template: ${templateName}`);
1106
+ console.log("\nAvailable templates:");
1107
+ for (const name of getAvailableTemplates()) {
1108
+ const t = getTemplate(name);
1109
+ console.log(` - ${name}: ${t.description}`);
1110
+ }
1111
+ process.exit(1);
1112
+ }
1113
+ const componentsDir = findComponentsDir(cwd);
1114
+ const extension = getFileExtension(framework);
1115
+ log.info(`Adding ${template.name}...`);
1116
+ for (const file of template.files) {
1117
+ const fileName = `${file.name}${extension}`;
1118
+ const filePath = path2__namespace.join(componentsDir, fileName);
1119
+ if (fileExists(filePath) && !options.force) {
1120
+ const response = await prompts2__default.default({
1121
+ type: "confirm",
1122
+ name: "overwrite",
1123
+ message: `${fileName} already exists. Overwrite?`,
1124
+ initial: false
1125
+ });
1126
+ if (!response.overwrite) {
1127
+ log.warn(`Skipped ${fileName}`);
1128
+ continue;
1129
+ }
1130
+ }
1131
+ const content = file.content[framework];
1132
+ writeFile(filePath, content);
1133
+ log.success(`Created ${path2__namespace.relative(cwd, filePath)}`);
1134
+ }
1135
+ console.log("\n");
1136
+ log.success(`Added ${template.name} component!`);
1137
+ console.log("\nUsage:\n");
1138
+ if (framework === "react" || framework === "next") {
1139
+ console.log(` import { ${template.files[0].name} } from "@/components/${template.files[0].name}";`);
1140
+ console.log("");
1141
+ console.log(` <${template.files[0].name} />`);
1142
+ } else {
1143
+ console.log(` <script setup>`);
1144
+ console.log(` import ${template.files[0].name} from "@/components/${template.files[0].name}.vue";`);
1145
+ console.log(` </script>`);
1146
+ console.log("");
1147
+ console.log(` <${template.files[0].name} />`);
1148
+ }
1149
+ console.log("\n");
1150
+ }
1151
+ var program = new commander.Command();
1152
+ program.name("authos").description("CLI for scaffolding AuthOS integrations").version("0.1.0");
1153
+ program.command("init").description("Initialize AuthOS in your project").option("--skip-install", "Skip package installation").action(async (options) => {
1154
+ await initCommand({ skipInstall: options.skipInstall });
1155
+ });
1156
+ program.command("add [template]").description("Add a component template to your project").option("-f, --force", "Overwrite existing files without prompting").action(async (template, options) => {
1157
+ await addCommand(template, { force: options.force });
1158
+ });
1159
+ program.command("list").description("List available component templates").action(() => {
1160
+ console.log(pc__default.default.bold("\nAvailable templates:\n"));
1161
+ for (const name of getAvailableTemplates()) {
1162
+ const template = getTemplate(name);
1163
+ console.log(` ${pc__default.default.cyan(name)}`);
1164
+ console.log(` ${pc__default.default.dim(template.description)}
1165
+ `);
1166
+ }
1167
+ console.log(`Run ${pc__default.default.cyan("authos add <template>")} to add a component.
1168
+ `);
1169
+ });
1170
+ program.parse();