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