@perceo/perceo 0.2.0 → 0.3.1

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,197 @@
1
+ // src/commands/login.ts
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import ora from "ora";
5
+ import fs2 from "fs/promises";
6
+ import path2 from "path";
7
+ import http from "http";
8
+ import { createInterface } from "readline";
9
+
10
+ // src/auth.ts
11
+ import fs from "fs/promises";
12
+ import path from "path";
13
+ import os from "os";
14
+ import { createClient } from "@supabase/supabase-js";
15
+ import { getSupabaseAnonKey } from "@perceo/supabase";
16
+ var AUTH_FILE = "auth.json";
17
+ function getGlobalConfigDir() {
18
+ const xdg = process.env.XDG_CONFIG_HOME;
19
+ if (xdg) {
20
+ return path.join(xdg, "perceo");
21
+ }
22
+ return path.join(os.homedir(), ".perceo");
23
+ }
24
+ function getAuthPath(scope, projectDir) {
25
+ if (scope === "project") {
26
+ if (!projectDir) {
27
+ throw new Error("projectDir is required for project-scoped auth");
28
+ }
29
+ return path.join(path.resolve(projectDir), ".perceo", AUTH_FILE);
30
+ }
31
+ return path.join(getGlobalConfigDir(), AUTH_FILE);
32
+ }
33
+ async function fileExists(p) {
34
+ try {
35
+ await fs.access(p);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ async function getStoredAuth(scope, projectDir) {
42
+ const authPath = getAuthPath(scope, projectDir);
43
+ if (!await fileExists(authPath)) return null;
44
+ try {
45
+ const raw = await fs.readFile(authPath, "utf8");
46
+ const data = JSON.parse(raw);
47
+ if (!data.access_token || !data.refresh_token || data.scope !== scope) return null;
48
+ return data;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ async function setStoredAuth(auth, projectDir) {
54
+ const scope = auth.scope;
55
+ const authPath = getAuthPath(scope, projectDir);
56
+ const dir = path.dirname(authPath);
57
+ await fs.mkdir(dir, { recursive: true });
58
+ await fs.writeFile(authPath, JSON.stringify(auth, null, 2) + "\n", "utf8");
59
+ }
60
+ async function clearStoredAuth(scope, projectDir) {
61
+ const authPath = getAuthPath(scope, projectDir);
62
+ if (await fileExists(authPath)) {
63
+ await fs.unlink(authPath);
64
+ }
65
+ }
66
+ async function isLoggedIn(projectDir = process.cwd()) {
67
+ const projectAuth = await getStoredAuth("project", projectDir);
68
+ if (projectAuth?.access_token) return true;
69
+ const globalAuth = await getStoredAuth("global");
70
+ return !!globalAuth?.access_token;
71
+ }
72
+ async function getEffectiveAuth(projectDir = process.cwd()) {
73
+ const projectAuth = await getStoredAuth("project", projectDir);
74
+ if (projectAuth?.access_token) return projectAuth;
75
+ return getStoredAuth("global");
76
+ }
77
+
78
+ // src/commands/login.ts
79
+ import { createSupabaseAuthClient, sendMagicLink, sessionFromRedirectUrl } from "@perceo/supabase";
80
+ function question(rl, prompt) {
81
+ return new Promise((resolve) => {
82
+ rl.question(prompt, (answer) => resolve((answer || "").trim()));
83
+ });
84
+ }
85
+ function startCallbackServer(port) {
86
+ return new Promise((resolve, reject) => {
87
+ const server = http.createServer((req, res) => {
88
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
89
+ if (url.pathname === "/capture" && url.searchParams.has("url")) {
90
+ const redirectUrl = url.searchParams.get("url") ?? "";
91
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
92
+ res.end(
93
+ `<!DOCTYPE html><html><head><title>Perceo login</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><p>Login successful. You can close this window and return to the terminal.</p></body></html>`
94
+ );
95
+ server.close();
96
+ resolve(redirectUrl);
97
+ return;
98
+ }
99
+ if (url.pathname === "/callback" || url.pathname === "/") {
100
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
101
+ res.end(
102
+ `<!DOCTYPE html><html><head><title>Perceo login</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><p>Completing login...</p><script>
103
+ var u = window.location.href;
104
+ fetch('/capture?url=' + encodeURIComponent(u)).then(function() { document.body.innerHTML = '<p>Login successful. You can close this window.</p>'; });
105
+ </script></body></html>`
106
+ );
107
+ return;
108
+ }
109
+ res.writeHead(404);
110
+ res.end("Not found");
111
+ });
112
+ server.listen(port, "127.0.0.1", () => {
113
+ }).on("error", (err) => {
114
+ server.close();
115
+ reject(err);
116
+ });
117
+ });
118
+ }
119
+ async function findPort(base) {
120
+ const net = await import("net");
121
+ return new Promise((resolve) => {
122
+ const server = net.createServer();
123
+ server.listen(base, "127.0.0.1", () => {
124
+ const address = server.address();
125
+ const port = address && typeof address !== "string" ? address.port : base;
126
+ server.close(() => resolve(port));
127
+ });
128
+ server.on("error", () => resolve(findPort(base + 1)));
129
+ });
130
+ }
131
+ var loginCommand = new Command("login").description("Log in to Perceo using Supabase Auth (required before init)").option("-s, --scope <scope>", "Where to store the login: 'project' (this repo) or 'global' (all projects)", "global").option("-d, --dir <directory>", "Project directory (for project scope)", process.cwd()).action(async (options) => {
132
+ const scope = options.scope?.toLowerCase() === "project" ? "project" : "global";
133
+ const projectDir = path2.resolve(options.dir || process.cwd());
134
+ if (scope === "project") {
135
+ const perceoDir = path2.join(projectDir, ".perceo");
136
+ try {
137
+ await fs2.mkdir(perceoDir, { recursive: true });
138
+ } catch (e) {
139
+ console.error(chalk.red("Could not create .perceo directory: " + (e instanceof Error ? e.message : String(e))));
140
+ process.exit(1);
141
+ }
142
+ }
143
+ const existing = await getStoredAuth(scope, scope === "project" ? projectDir : void 0);
144
+ if (existing?.access_token) {
145
+ console.log(chalk.yellow(`Already logged in (${scope} scope). Use \`perceo logout --scope ${scope}\` to log out first.`));
146
+ return;
147
+ }
148
+ let email = process.env.PERCEO_LOGIN_EMAIL;
149
+ if (!email) {
150
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
151
+ email = await question(rl, "Email for magic link: ");
152
+ rl.close();
153
+ }
154
+ if (!email) {
155
+ console.error(chalk.red("Email is required. Set PERCEO_LOGIN_EMAIL or run the command interactively."));
156
+ process.exit(1);
157
+ }
158
+ const spinner = ora("Starting login...").start();
159
+ try {
160
+ const supabase = createSupabaseAuthClient();
161
+ const port = await findPort(38473);
162
+ const redirectUrl = `http://127.0.0.1:${port}/callback`;
163
+ spinner.text = "Sending magic link to your email...";
164
+ const { error: sendError } = await sendMagicLink(supabase, email, redirectUrl);
165
+ if (sendError) {
166
+ spinner.fail("Failed to send magic link");
167
+ console.error(chalk.red(sendError.message));
168
+ process.exit(1);
169
+ }
170
+ spinner.succeed("Magic link sent! Check your email.");
171
+ console.log(chalk.cyan(`Click the link in the email. It will open your browser and complete login (redirect: ${redirectUrl}).`));
172
+ console.log(chalk.gray("Waiting for you to complete the link..."));
173
+ const redirectReceivedUrl = await startCallbackServer(port);
174
+ const session = await sessionFromRedirectUrl(supabase, redirectReceivedUrl);
175
+ const toStore = {
176
+ ...session,
177
+ scope
178
+ };
179
+ await setStoredAuth(toStore, scope === "project" ? projectDir : void 0);
180
+ const where = scope === "project" ? path2.relative(process.cwd(), getAuthPath(scope, projectDir)) : getAuthPath(scope);
181
+ console.log(chalk.green("\nLogged in successfully."));
182
+ console.log(chalk.gray(`Auth saved to: ${where}`));
183
+ console.log(chalk.gray(`Scope: ${scope}. You can now run \`perceo init\`.`));
184
+ } catch (error) {
185
+ spinner.fail("Login failed");
186
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
187
+ process.exit(1);
188
+ }
189
+ });
190
+
191
+ export {
192
+ getStoredAuth,
193
+ clearStoredAuth,
194
+ isLoggedIn,
195
+ getEffectiveAuth,
196
+ loginCommand
197
+ };