@asiyst/cli 1.0.8 → 1.1.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.
package/dist/index.js CHANGED
@@ -1,42 +1,414 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/ui/output.ts
4
- var ok = (label, detail = "") => console.log(` \u2713 ${label}${detail ? ` (${detail})` : ""}`);
5
- var fail = (label, detail = "") => console.log(` \u2717 ${label}${detail ? `: ${detail}` : ""}`);
6
- function projectChecks(project) {
7
- project.packageJson ? ok("Project detected", typeof project.packageJson.name === "string" ? project.packageJson.name : project.cwd) : fail("Project not detected", "package.json is missing");
8
- ok("Framework detected", project.framework);
9
- ok("Language detected", project.language);
10
- ok("Node.js detected", process.version);
11
- ok("Package manager detected", project.packageManager);
12
- project.sdkVersion ? ok("@asiyst/sdk detected", project.sdkVersion) : fail("@asiyst/sdk is not installed", "npm install @asiyst/sdk");
13
- }
14
- function homeStatus(project, connection = "Not connected") {
15
- console.log("\n PROJECT STATUS");
16
- console.log(` Project ${typeof project.packageJson?.name === "string" ? project.packageJson.name : "Not detected"}`);
17
- console.log(` Framework ${project.framework}`);
18
- console.log(` Language ${project.language}`);
19
- console.log(` Node.js ${process.version}`);
20
- console.log(` Package Manager ${project.packageManager}`);
21
- console.log(` @asiyst/sdk ${project.sdkVersion || "Not installed"}`);
22
- console.log("\n ASIYST STATUS");
23
- console.log(` Connection ${connection}`);
24
- console.log(` Avatar Not configured`);
25
- console.log(` AI Provider Not configured`);
26
- console.log(` Project ID ${project.config.projectId || "\u2014"}`);
27
- if (connection === "Not connected") console.log("\n Run 'connect' to link this project.");
28
- }
29
- function printVerification(results) {
30
- console.log("\n Asiyst Installation Verification\n");
31
- for (const result of results) result.ok ? ok(result.name, result.detail) : fail(result.name, result.detail);
32
- const success = results.length > 0 && results.every((result) => result.ok);
33
- if (success) console.log("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n ASIYST IS READY \u2713");
34
- return success;
3
+ // src/config/api.ts
4
+ var PRODUCTION_API_ORIGIN = "https://nqhxpgsjofzqudyqkqib.supabase.co/functions/v1/api";
5
+ var CLI_API_BASE_URL = PRODUCTION_API_ORIGIN;
6
+ var VERIFY_KEY_PATH = "/auth/api-key/verify";
7
+ var VERIFY_KEY_URL = `${CLI_API_BASE_URL}${VERIFY_KEY_PATH}`;
8
+ var ASIYST_WEB_URL = "https://asiyst.com";
9
+ var ASIIYST_WEB_URL = ASIYST_WEB_URL;
10
+ var REQUEST_TIMEOUT_MS = 15e3;
11
+ function readEnv(env, ...keys) {
12
+ for (const key of keys) {
13
+ const value = env[key]?.trim();
14
+ if (value) return value;
15
+ }
16
+ return void 0;
17
+ }
18
+ function isLocalUrl(value) {
19
+ try {
20
+ const url = new URL(value);
21
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
22
+ } catch {
23
+ return /localhost|127\.0\.0\.1|::1/i.test(value);
24
+ }
25
+ }
26
+ function resolveApiBaseUrl(env = process.env) {
27
+ const explicit = readEnv(env, "ASIYST_API_URL", "ASIIYST_API_URL");
28
+ const development = readEnv(env, "ASIYST_API_MODE", "ASIIYST_API_MODE") === "development";
29
+ if (explicit) {
30
+ if (isLocalUrl(explicit)) {
31
+ return CLI_API_BASE_URL;
32
+ }
33
+ return explicit.replace(/\/+$/, "");
34
+ }
35
+ if (development) {
36
+ return CLI_API_BASE_URL;
37
+ }
38
+ return CLI_API_BASE_URL;
39
+ }
40
+ function isDebugEnabled(env = process.env) {
41
+ return readEnv(env, "ASIYST_DEBUG", "ASIIYST_DEBUG") === "1" || readEnv(env, "ASIYST_DEBUG", "ASIIYST_DEBUG") === "true";
42
+ }
43
+
44
+ // src/config/ids.ts
45
+ var PUBLIC_IDENTIFIER_PATTERN = /^(?=.{1,128}$)[A-Za-z0-9_][A-Za-z0-9_-]*$/;
46
+ var USER_ID_PATTERN = /^[A-Za-z0-9_]{24}$/;
47
+ var AVATAR_ID_PATTERN = /^[A-Za-z0-9]{10}$/;
48
+ function isValidPublicIdentifier(value) {
49
+ if (typeof value !== "string") return false;
50
+ const trimmed = value.trim();
51
+ if (!trimmed || trimmed.length > 128) return false;
52
+ if (trimmed === "undefined" || trimmed === "null") return false;
53
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)) {
54
+ return false;
55
+ }
56
+ return PUBLIC_IDENTIFIER_PATTERN.test(trimmed);
57
+ }
58
+ function isValidUserId(value) {
59
+ return typeof value === "string" && value.trim() !== "undefined" && value.trim() !== "null" && USER_ID_PATTERN.test(value.trim());
60
+ }
61
+ function isValidAvatarId(value) {
62
+ return typeof value === "string" && value.trim() !== "undefined" && value.trim() !== "null" && AVATAR_ID_PATTERN.test(value.trim());
63
+ }
64
+ function parseProjectIdArgument(argv = []) {
65
+ const length = argv.length;
66
+ for (let index = 0; index < length; index += 1) {
67
+ const entry = argv[index];
68
+ if (entry === "--project-id" || entry === "--projectId") {
69
+ const next = argv[index + 1];
70
+ return typeof next === "string" ? next : "";
71
+ }
72
+ if (entry.startsWith("--project-id=") || entry.startsWith("--projectId=")) {
73
+ return entry.slice(entry.indexOf("=") + 1);
74
+ }
75
+ }
76
+ return void 0;
77
+ }
78
+
79
+ // src/api/errors.ts
80
+ var ApiError = class extends Error {
81
+ constructor(message, status, code = "UNKNOWN") {
82
+ super(message);
83
+ this.status = status;
84
+ this.code = code;
85
+ this.name = "ApiError";
86
+ }
87
+ status;
88
+ code;
89
+ };
90
+ function errorCodeFromStatus(status, bodyCode) {
91
+ if (bodyCode === "INVALID_API_KEY") return "INVALID_API_KEY";
92
+ if (bodyCode === "API_KEY_REVOKED") return "API_KEY_REVOKED";
93
+ if (bodyCode === "FORBIDDEN") return "FORBIDDEN";
94
+ if (bodyCode === "PROJECT_NOT_FOUND") return "PROJECT_NOT_FOUND";
95
+ if (bodyCode === "AVATAR_NOT_FOUND") return "AVATAR_NOT_FOUND";
96
+ if (bodyCode === "AVATAR_ALREADY_IMPORTED") return "AVATAR_ALREADY_IMPORTED";
97
+ if (bodyCode === "RATE_LIMITED") return "RATE_LIMITED";
98
+ if (bodyCode === "INTERNAL_ERROR") return "INTERNAL_ERROR";
99
+ if (status === 401) return "INVALID_API_KEY";
100
+ if (status === 403) return "FORBIDDEN";
101
+ if (status === 404) return "NOT_FOUND";
102
+ if (status === 409) return "CONFLICT";
103
+ if (status === 400 || status === 422) return "INVALID_REQUEST";
104
+ if (status === 429) return "RATE_LIMITED";
105
+ if (status >= 500) return "INTERNAL_ERROR";
106
+ return "UNKNOWN";
107
+ }
108
+ function friendlyApiMessage(error, endpointUrl) {
109
+ if (error.code === "INVALID_API_KEY") return "\u2717 Invalid API key.";
110
+ if (error.code === "API_KEY_REVOKED") {
111
+ return "\u2717 This API key has been revoked.\nCreate a new key from:\nhttps://asiyst.com";
112
+ }
113
+ if (error.code === "FORBIDDEN") {
114
+ return "\u2717 This API key does not have permission to access this resource.";
115
+ }
116
+ if (error.code === "NOT_FOUND") {
117
+ return endpointUrl ? `\u2717 Asiyst API endpoint was not found.
118
+ Verify that the CLI is using:
119
+ ${endpointUrl}` : "\u2717 Asiyst API endpoint was not found.";
120
+ }
121
+ if (error.code === "TIMEOUT") return "Connection to Asiyst timed out.";
122
+ if (error.code === "NETWORK") return "\u2717 Unable to reach Asiyst API.";
123
+ if (error.code === "MALFORMED_RESPONSE") return "Received an unexpected response from Asiyst.";
124
+ if (error.code === "RATE_LIMITED") return "\u2717 Too many requests. Try again shortly.";
125
+ if (error.code === "PROJECT_NOT_FOUND") return "\u2717 Authorized project was not found.";
126
+ if (error.code === "CONFLICT") return "\u2717 The request conflicts with the current project state.";
127
+ if (error.code === "INVALID_REQUEST") return "\u2717 The request was invalid.";
128
+ if (error.code === "AVATAR_NOT_FOUND") return "\u2717 Avatar was not found.";
129
+ if (error.code === "AVATAR_ALREADY_IMPORTED") return "\u2717 This avatar is already imported into the project.";
130
+ if (error.code === "INTERNAL_ERROR") return "\u2717 Asiyst is temporarily unavailable.";
131
+ return "\u2717 Asiyst request failed.";
132
+ }
133
+
134
+ // src/api/auth.ts
135
+ function asRecord(value) {
136
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
137
+ }
138
+ function bearerToken(apiKey) {
139
+ const trimmed = apiKey.trim();
140
+ return trimmed.toLowerCase().startsWith("bearer ") ? trimmed.slice(7).trim() : trimmed;
141
+ }
142
+ function projectFromBody(body) {
143
+ const nested = asRecord(body.project) ?? asRecord(body.data);
144
+ const nestedProject = nested ? asRecord(nested.project) ?? nested : void 0;
145
+ return nestedProject ?? body;
146
+ }
147
+ function parseVerifyKeyResponse(value, apiKey) {
148
+ const body = asRecord(value);
149
+ if (!body) throw new ApiError("Received an unexpected response from Asiyst.", 200, "MALFORMED_RESPONSE");
150
+ if (body.valid === false) {
151
+ const code = typeof body.code === "string" ? body.code : void 0;
152
+ if (code === "API_KEY_REVOKED" || body.revoked === true) {
153
+ throw new ApiError("This API key has been revoked.", 401, "API_KEY_REVOKED");
154
+ }
155
+ throw new ApiError("The Asiyst API key was rejected.", 401, "INVALID_API_KEY");
156
+ }
157
+ if (body.valid !== void 0 && body.valid !== true) {
158
+ throw new ApiError("Received an unexpected response from Asiyst.", 200, "MALFORMED_RESPONSE");
159
+ }
160
+ const project = projectFromBody(body);
161
+ if (!project) throw new ApiError("Received an unexpected response from Asiyst.", 200, "MALFORMED_RESPONSE");
162
+ const projectIdCandidate = project.projectId ?? project.project_id ?? project.projectID ?? project.id;
163
+ const projectId = typeof projectIdCandidate === "string" && isValidPublicIdentifier(projectIdCandidate) ? projectIdCandidate.trim() : void 0;
164
+ if (!projectId) {
165
+ throw new ApiError("Received an unexpected response from Asiyst.", 200, "MALFORMED_RESPONSE");
166
+ }
167
+ const website = project.website ?? project.websiteUrl ?? project.url ?? project.domain ?? project.website_url;
168
+ const projectName = project.projectName ?? project.name ?? project.title;
169
+ const publicKey = project.publicKey ?? project.public_key ?? project.publishableKey;
170
+ const userId = project.userId ?? project.user_id ?? project.asiystUserId ?? project.asiyst_user_id ?? body.userId ?? body.user_id ?? body.asiystUserId ?? body.asiyst_user_id;
171
+ return {
172
+ projectId,
173
+ projectName: typeof projectName === "string" ? projectName : void 0,
174
+ website: typeof website === "string" ? website : void 0,
175
+ publicKey: typeof publicKey === "string" ? publicKey : void 0,
176
+ apiKey,
177
+ userId: typeof userId === "string" && isValidPublicIdentifier(userId) ? userId : void 0
178
+ };
179
+ }
180
+ async function verifyApiKey(api, apiKey, expected) {
181
+ const token = bearerToken(apiKey);
182
+ if (!token) throw new ApiError("Invalid API key.", 401, "INVALID_API_KEY");
183
+ const value = await api.request(VERIFY_KEY_PATH, {
184
+ method: "POST",
185
+ headers: {
186
+ Authorization: "Bearer " + token,
187
+ "X-Asiyst-API-Key": token
188
+ },
189
+ body: JSON.stringify({
190
+ apiKey: token,
191
+ ...expected?.userId ? { userId: expected.userId } : {},
192
+ ...expected?.projectId ? { projectId: expected.projectId } : {}
193
+ })
194
+ });
195
+ return parseVerifyKeyResponse(value, token);
196
+ }
197
+
198
+ // src/browser/open.ts
199
+ import { execFile } from "child_process";
200
+ import { promisify } from "util";
201
+ var execFileAsync = promisify(execFile);
202
+ async function openBrowser(url) {
203
+ const command = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
204
+ const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
205
+ try {
206
+ await execFileAsync(command, args);
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ // src/config/credentials.ts
214
+ import { execFile as execFile2 } from "child_process";
215
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
216
+ import { homedir } from "os";
217
+ import { join, resolve } from "path";
218
+ import { promisify as promisify2 } from "util";
219
+ var execFileAsync2 = promisify2(execFile2);
220
+ function configDir() {
221
+ if (process.platform === "win32") return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "asiyst");
222
+ if (process.platform === "darwin") return join(homedir(), "Library", "Application Support", "asiyst");
223
+ return join(homedir(), ".config", "asiyst");
224
+ }
225
+ function storePath() {
226
+ return join(configDir(), "credentials.json");
227
+ }
228
+ function accountFor(cwd) {
229
+ return resolve(cwd);
230
+ }
231
+ function readStore() {
232
+ const path = storePath();
233
+ if (!existsSync(path)) return {};
234
+ try {
235
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
236
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
237
+ return parsed;
238
+ } catch {
239
+ return {};
240
+ }
241
+ }
242
+ function writeStore(data) {
243
+ mkdirSync(configDir(), { recursive: true, mode: 448 });
244
+ writeFileSync(storePath(), JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
245
+ }
246
+ async function dpapiProtect(plaintext) {
247
+ if (process.platform !== "win32") return void 0;
248
+ const script = [
249
+ "Add-Type -AssemblyName System.Security",
250
+ "$bytes = [System.Text.Encoding]::UTF8.GetBytes($env:ASIYST_DPAPI_PAYLOAD)",
251
+ "$protected = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
252
+ "[Convert]::ToBase64String($protected)"
253
+ ].join("; ");
254
+ try {
255
+ const { stdout: stdout3 } = await execFileAsync2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
256
+ env: { ...process.env, ASIYST_DPAPI_PAYLOAD: plaintext },
257
+ windowsHide: true
258
+ });
259
+ const value = stdout3.trim();
260
+ return value || void 0;
261
+ } catch {
262
+ return void 0;
263
+ }
264
+ }
265
+ async function dpapiUnprotect(payload) {
266
+ if (process.platform !== "win32") return void 0;
267
+ const script = [
268
+ "Add-Type -AssemblyName System.Security",
269
+ "$protected = [Convert]::FromBase64String($env:ASIYST_DPAPI_PAYLOAD)",
270
+ "$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($protected, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)",
271
+ "[System.Text.Encoding]::UTF8.GetString($bytes)"
272
+ ].join("; ");
273
+ try {
274
+ const { stdout: stdout3 } = await execFileAsync2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
275
+ env: { ...process.env, ASIYST_DPAPI_PAYLOAD: payload },
276
+ windowsHide: true
277
+ });
278
+ const value = stdout3.trim();
279
+ return value || void 0;
280
+ } catch {
281
+ return void 0;
282
+ }
283
+ }
284
+ function windowsBlobPath() {
285
+ return join(configDir(), "credentials.dpapi");
286
+ }
287
+ async function saveConnection(cwd, connection) {
288
+ const account = accountFor(cwd);
289
+ const next = { ...readStore(), [account]: connection };
290
+ if (process.platform === "win32") {
291
+ const protectedBlob = await dpapiProtect(JSON.stringify(next));
292
+ if (protectedBlob) {
293
+ mkdirSync(configDir(), { recursive: true, mode: 448 });
294
+ writeFileSync(windowsBlobPath(), protectedBlob, { encoding: "utf8", mode: 384 });
295
+ if (existsSync(storePath())) unlinkSync(storePath());
296
+ return;
297
+ }
298
+ }
299
+ writeStore(next);
300
+ }
301
+ async function loadConnection(cwd) {
302
+ const account = accountFor(cwd);
303
+ if (process.platform === "win32" && existsSync(windowsBlobPath())) {
304
+ const decrypted = await dpapiUnprotect(readFileSync(windowsBlobPath(), "utf8"));
305
+ if (decrypted) {
306
+ try {
307
+ const parsed = JSON.parse(decrypted);
308
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
309
+ const entry2 = parsed[account];
310
+ if (entry2 && typeof entry2.apiKey === "string" && typeof entry2.projectId === "string") return entry2;
311
+ }
312
+ } catch {
313
+ return void 0;
314
+ }
315
+ }
316
+ }
317
+ const entry = readStore()[account];
318
+ if (entry && typeof entry.apiKey === "string" && typeof entry.projectId === "string") return entry;
319
+ return void 0;
320
+ }
321
+ async function clearConnection(cwd) {
322
+ const account = accountFor(cwd);
323
+ if (process.platform === "win32" && existsSync(windowsBlobPath())) {
324
+ const decrypted = await dpapiUnprotect(readFileSync(windowsBlobPath(), "utf8"));
325
+ let next2 = {};
326
+ if (decrypted) {
327
+ try {
328
+ const parsed = JSON.parse(decrypted);
329
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) next2 = { ...parsed };
330
+ } catch {
331
+ next2 = {};
332
+ }
333
+ }
334
+ delete next2[account];
335
+ if (Object.keys(next2).length === 0) {
336
+ unlinkSync(windowsBlobPath());
337
+ return;
338
+ }
339
+ const protectedBlob = await dpapiProtect(JSON.stringify(next2));
340
+ if (protectedBlob) {
341
+ writeFileSync(windowsBlobPath(), protectedBlob, { encoding: "utf8", mode: 384 });
342
+ return;
343
+ }
344
+ }
345
+ const next = readStore();
346
+ delete next[account];
347
+ if (Object.keys(next).length === 0 && existsSync(storePath())) unlinkSync(storePath());
348
+ else writeStore(next);
349
+ }
350
+
351
+ // src/config/project.ts
352
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
353
+ import { join as join2, resolve as resolve2 } from "path";
354
+ var PROJECT_CONFIG_DIR = ".asiyst";
355
+ var PROJECT_CONFIG_FILE = "config.json";
356
+ function projectConfigPath(cwd) {
357
+ return join2(resolve2(cwd), PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE);
358
+ }
359
+ function readProjectMetadata(cwd) {
360
+ const path = projectConfigPath(cwd);
361
+ if (!existsSync2(path)) return void 0;
362
+ try {
363
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
364
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
365
+ const body = parsed;
366
+ if (typeof body.apiKey === "string") delete body.apiKey;
367
+ return {
368
+ projectId: typeof body.projectId === "string" ? body.projectId : void 0,
369
+ projectName: typeof body.projectName === "string" ? body.projectName : void 0,
370
+ website: typeof body.website === "string" ? body.website : void 0,
371
+ publicKey: typeof body.publicKey === "string" ? body.publicKey : void 0,
372
+ userId: typeof body.userId === "string" ? body.userId : void 0,
373
+ avatarId: typeof body.avatarId === "string" ? body.avatarId : void 0,
374
+ connected: body.connected === true
375
+ };
376
+ } catch {
377
+ return void 0;
378
+ }
379
+ }
380
+ function writeProjectMetadata(cwd, connection) {
381
+ const dir = join2(resolve2(cwd), PROJECT_CONFIG_DIR);
382
+ mkdirSync2(dir, { recursive: true });
383
+ const existing = readProjectMetadata(cwd) ?? {};
384
+ const next = {
385
+ ...existing,
386
+ projectId: connection.projectId,
387
+ projectName: connection.projectName,
388
+ website: connection.website,
389
+ publicKey: connection.publicKey,
390
+ userId: connection.userId,
391
+ avatarId: connection.avatarId,
392
+ connected: true
393
+ };
394
+ writeFileSync2(projectConfigPath(cwd), `${JSON.stringify(next, null, 2)}
395
+ `, { encoding: "utf8" });
396
+ }
397
+ function clearProjectMetadata(cwd) {
398
+ const path = projectConfigPath(cwd);
399
+ if (!existsSync2(path)) return;
400
+ const existing = readProjectMetadata(cwd) ?? {};
401
+ const next = {
402
+ ...existing,
403
+ connected: false
404
+ };
405
+ writeFileSync2(path, `${JSON.stringify(next, null, 2)}
406
+ `, { encoding: "utf8" });
35
407
  }
36
408
 
37
409
  // src/detection/project.ts
38
- import { existsSync, readFileSync } from "fs";
39
- import { resolve } from "path";
410
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
411
+ import { resolve as resolve3 } from "path";
40
412
  function dependencyVersion(pkg) {
41
413
  const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
42
414
  for (const section of sections) {
@@ -48,30 +420,37 @@ function dependencyVersion(pkg) {
48
420
  }
49
421
  return void 0;
50
422
  }
423
+ function hasDependency(deps, name) {
424
+ return typeof deps[name] === "string";
425
+ }
51
426
  function detectFramework(cwd, pkg) {
52
427
  const deps = Object.assign({}, pkg?.dependencies, pkg?.devDependencies);
53
- if (typeof deps.next === "string") return "Next.js";
54
- if (typeof deps.react === "string") return "React";
55
- if (typeof deps.vite === "string") return "Vite";
56
- if (typeof deps.vue === "string") return "Vue";
57
- if (existsSync(resolve(cwd, "vite.config.ts")) || existsSync(resolve(cwd, "vite.config.js"))) return "Vite";
58
- if (existsSync(resolve(cwd, "next.config.js")) || existsSync(resolve(cwd, "next.config.mjs"))) return "Next.js";
59
- return "Vanilla JavaScript/TypeScript";
428
+ if (hasDependency(deps, "next") || existsSync3(resolve3(cwd, "next.config.js")) || existsSync3(resolve3(cwd, "next.config.mjs")) || existsSync3(resolve3(cwd, "next.config.ts"))) {
429
+ return "Next.js";
430
+ }
431
+ if (hasDependency(deps, "nuxt")) return "Nuxt";
432
+ if (hasDependency(deps, "vue") || existsSync3(resolve3(cwd, "vue.config.js"))) return "Vue";
433
+ if (hasDependency(deps, "react")) return "React";
434
+ if (hasDependency(deps, "vite") || existsSync3(resolve3(cwd, "vite.config.ts")) || existsSync3(resolve3(cwd, "vite.config.js")) || existsSync3(resolve3(cwd, "vite.config.mjs"))) {
435
+ return "Vite";
436
+ }
437
+ if (pkg) return existsSync3(resolve3(cwd, "tsconfig.json")) ? "Vanilla TypeScript" : "Vanilla JavaScript";
438
+ return "Unknown";
60
439
  }
61
440
  function detectProject(cwd = process.cwd()) {
62
- const path = resolve(cwd, "package.json");
441
+ const path = resolve3(cwd, "package.json");
63
442
  let packageJson = null;
64
- if (existsSync(path)) {
443
+ if (existsSync3(path)) {
65
444
  try {
66
- const value = JSON.parse(readFileSync(path, "utf8"));
445
+ const value = JSON.parse(readFileSync3(path, "utf8"));
67
446
  if (value && typeof value === "object" && !Array.isArray(value)) packageJson = value;
68
447
  } catch {
69
448
  packageJson = null;
70
449
  }
71
450
  }
72
451
  const env = process.env;
73
- const packageManager = existsSync(resolve(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync(resolve(cwd, "yarn.lock")) ? "yarn" : existsSync(resolve(cwd, "bun.lockb")) || existsSync(resolve(cwd, "bun.lock")) ? "bun" : "npm";
74
- const sourceFiles = ["tsconfig.json", "src", "app"].some((entry) => existsSync(resolve(cwd, entry)));
452
+ const packageManager = existsSync3(resolve3(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync3(resolve3(cwd, "yarn.lock")) ? "yarn" : existsSync3(resolve3(cwd, "bun.lockb")) || existsSync3(resolve3(cwd, "bun.lock")) ? "bun" : "npm";
453
+ const sourceFiles = ["tsconfig.json", "src", "app"].some((entry) => existsSync3(resolve3(cwd, entry)));
75
454
  return {
76
455
  cwd,
77
456
  packageJson,
@@ -86,263 +465,298 @@ function detectProject(cwd = process.cwd()) {
86
465
  };
87
466
  }
88
467
 
89
- // src/config/api.ts
90
- var CLI_API_BASE_URL = "https://asiyst.com/api/v1";
91
- var ASIIYST_WEB_URL = "https://asiyst.com";
468
+ // src/ui/output.ts
469
+ var ok = (label, detail = "") => console.log(`\u2713 ${label}${detail ? ` (${detail})` : ""}`);
470
+ var fail = (label, detail = "") => console.log(`\u2717 ${label}${detail ? `: ${detail}` : ""}`);
471
+ function projectChecks(project) {
472
+ project.packageJson ? ok("Project detected", typeof project.packageJson.name === "string" ? project.packageJson.name : project.cwd) : fail("Project not detected", "package.json is missing");
473
+ if (project.framework === "Unknown") fail("Framework detected", "No supported project type was identified.");
474
+ else ok("Framework detected", project.framework);
475
+ ok("Language detected", project.language);
476
+ ok("Node.js detected", process.version);
477
+ ok("Package manager detected", project.packageManager);
478
+ project.sdkVersion ? ok("@asiyst/sdk detected", project.sdkVersion) : console.log("@asiyst/sdk is not installed. You can connect the project now and install the SDK later.");
479
+ }
480
+ function homeStatus() {
481
+ console.log("\nConnect your project to Asiyst to view stats.\n");
482
+ }
92
483
 
93
- // src/api/client.ts
94
- var ApiError = class extends Error {
95
- constructor(message, status) {
96
- super(message);
97
- this.status = status;
98
- this.name = "ApiError";
99
- }
100
- status;
101
- };
102
- var ApiClient = class {
103
- constructor(baseUrl = process.env.ASIIYST_API_URL || CLI_API_BASE_URL, fetcher = fetch) {
104
- this.fetcher = fetcher;
105
- this.baseUrl = baseUrl.replace(/\/+$/, "");
484
+ // src/ui/selector.ts
485
+ import { stdin, stdout } from "process";
486
+ import { clearLine, cursorTo, emitKeypressEvents, moveCursor } from "readline";
487
+ var HIDE_CURSOR = "\x1B[?25l";
488
+ var SHOW_CURSOR = "\x1B[?25h";
489
+ function moveSelection(active, optionCount, direction) {
490
+ if (optionCount === 0) return 0;
491
+ return direction === "up" ? Math.max(0, active - 1) : Math.min(optionCount - 1, active + 1);
492
+ }
493
+ function clearSelectorFrame(output, previousLineCount) {
494
+ if (previousLineCount <= 0) return;
495
+ cursorTo(output, 0);
496
+ for (let i = 0; i < previousLineCount - 1; i += 1) {
497
+ moveCursor(output, 0, -1);
106
498
  }
107
- fetcher;
108
- baseUrl;
109
- async request(path, init) {
110
- let response;
111
- try {
112
- response = await this.fetcher(`${this.baseUrl}${path}`, {
113
- ...init,
114
- headers: { Accept: "application/json", "Content-Type": "application/json", ...init?.headers }
115
- });
116
- } catch (error) {
117
- throw new ApiError(`Unable to reach Asiyst API at ${this.baseUrl}${path}. Check your internet connection, DNS, TLS/HTTPS, or whether the service is unavailable.`);
118
- }
119
- const body = await response.json().catch(() => void 0);
120
- if (!response.ok) {
121
- const detail = response.status === 401 ? "Authentication is required." : response.status === 403 ? "The request is not authorized." : response.status === 404 ? "The requested resource was not found." : response.status === 409 ? "The request conflicts with the current project state." : response.status === 422 ? "The request data was invalid." : response.status === 429 ? "Too many requests; try again shortly." : response.status >= 500 ? "Asiyst is temporarily unavailable." : "The request was rejected.";
122
- throw new ApiError(`Asiyst API returned HTTP ${response.status}. ${detail}`, response.status);
123
- }
124
- if (body === void 0) {
125
- throw new ApiError(`Asiyst API returned an invalid JSON response (HTTP ${response.status}).`, response.status);
499
+ for (let line = 0; line < previousLineCount; line += 1) {
500
+ cursorTo(output, 0);
501
+ clearLine(output, 0);
502
+ if (line < previousLineCount - 1) {
503
+ moveCursor(output, 0, 1);
126
504
  }
127
- return body;
128
505
  }
129
- health() {
130
- return this.request("/health").then((body) => {
131
- if (!body || typeof body !== "object" || Array.isArray(body)) {
132
- throw new ApiError("Asiyst API health endpoint returned an invalid response.");
133
- }
134
- return body;
135
- });
136
- }
137
- createSession() {
138
- return this.request("/cli/sessions", { method: "POST", body: JSON.stringify({}) }).then((value) => {
139
- if (!value || typeof value !== "object") throw new ApiError("Asiyst returned an invalid session response");
140
- const session = value;
141
- if (typeof session.sessionId !== "string" || typeof session.connectUrl !== "string" || typeof session.expiresAt !== "string") {
142
- throw new ApiError("Asiyst returned an invalid session response");
143
- }
144
- const url = new URL(session.connectUrl);
145
- if (url.protocol !== "https:") {
146
- throw new ApiError("Asiyst returned an insecure connection URL");
147
- }
148
- return { sessionId: session.sessionId, connectUrl: url.toString(), expiresAt: session.expiresAt };
149
- });
150
- }
151
- sessionStatus(sessionId) {
152
- return this.request(`/cli/sessions/${encodeURIComponent(sessionId)}/status`);
506
+ cursorTo(output, 0);
507
+ for (let i = 0; i < previousLineCount - 1; i += 1) {
508
+ moveCursor(output, 0, -1);
153
509
  }
154
- projectInfo(projectId) {
155
- return this.request(`/cli/projects/${encodeURIComponent(projectId)}`);
156
- }
157
- verify(projectId, publicKey, domain) {
158
- return this.request("/cli/verification", {
159
- method: "POST",
160
- body: JSON.stringify({ projectId, publicKey, domain })
161
- }).then((value) => {
162
- if (!Array.isArray(value) || !value.every((item) => item && typeof item === "object" && typeof item.name === "string" && typeof item.ok === "boolean")) {
163
- throw new ApiError("Asiyst returned an invalid verification response");
164
- }
165
- return value;
166
- });
510
+ }
511
+ function renderSelectorFrame(output, previousLineCount, lines) {
512
+ if (lines.length === 0) return 0;
513
+ if (previousLineCount > 0) {
514
+ cursorTo(output, 0);
515
+ for (let i = 0; i < previousLineCount - 1; i += 1) {
516
+ moveCursor(output, 0, -1);
517
+ }
167
518
  }
168
- };
169
-
170
- // src/config/local.ts
171
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
172
- import { resolve as resolve2 } from "path";
173
- function configWarning(cwd) {
174
- const envPath = resolve2(cwd, ".env");
175
- if (existsSync2(envPath)) {
176
- const ignored = existsSync2(resolve2(cwd, ".gitignore")) && readFileSync2(resolve2(cwd, ".gitignore"), "utf8").split(/\r?\n/).some((line) => line.trim() === ".env" || line.trim() === ".env.*");
177
- if (!ignored) return "Your .env file is not clearly ignored by .gitignore; do not commit public or private configuration accidentally.";
519
+ for (let i = 0; i < lines.length; i += 1) {
520
+ cursorTo(output, 0);
521
+ clearLine(output, 0);
522
+ output.write(lines[i]);
523
+ if (i < lines.length - 1) {
524
+ output.write("\n");
525
+ }
178
526
  }
179
- return void 0;
527
+ return lines.length;
180
528
  }
181
-
182
- // src/browser/open.ts
183
- import { execFile } from "child_process";
184
- import { promisify } from "util";
185
- var execFileAsync = promisify(execFile);
186
- async function openBrowser(url) {
187
- const command = process.platform === "win32" ? "rundll32.exe" : process.platform === "darwin" ? "open" : "xdg-open";
188
- const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
189
- try {
190
- await execFileAsync(command, args);
191
- return true;
192
- } catch {
193
- return false;
194
- }
529
+ function restoreTerminal(wasRaw) {
530
+ stdout.write(SHOW_CURSOR);
531
+ if (stdin.isTTY) stdin.setRawMode?.(wasRaw);
195
532
  }
196
-
197
- // src/ui/selector.ts
198
- import { stdin, stdout } from "process";
199
- import { emitKeypressEvents } from "readline";
200
- var escape = "\x1B[";
201
- var width = () => Math.max(40, (stdout.columns || 80) - 2);
202
- var fit = (value) => value.length > width() ? `${value.slice(0, width() - 1)}\u2026` : value;
203
- function selectOption(title, options, prompt = "> ") {
204
- if (!stdin.isTTY || !stdout.isTTY) return Promise.resolve({ type: "cancelled", input: "" });
205
- return new Promise((resolve4) => {
206
- let input = "";
533
+ function selectOption(title, options) {
534
+ if (!stdin.isTTY || !stdout.isTTY) return Promise.resolve({ type: "cancelled" });
535
+ return new Promise((resolve5) => {
207
536
  let active = 0;
208
- let visible = true;
209
537
  let renderedLines = 0;
210
538
  let settled = false;
211
- const filtered = () => {
212
- const query = input.trim().toLowerCase();
213
- return query ? options.filter((option) => option.label.toLowerCase().includes(query) || String(option.value).toLowerCase().includes(query)) : options;
214
- };
215
- const clear = () => {
216
- if (renderedLines === 0) return;
217
- stdout.write(`${escape}${renderedLines}A`);
218
- for (let line = 0; line < renderedLines; line += 1) stdout.write(`${escape}2K${line < renderedLines - 1 ? `${escape}1B` : ""}`);
219
- stdout.write(`${escape}${renderedLines}A`);
220
- };
221
- const render = () => {
222
- clear();
223
- const matches = filtered();
224
- if (active >= matches.length) active = Math.max(0, matches.length - 1);
225
- const lines = [`${title}`, `${prompt}${fit(input)}`];
226
- if (visible) {
227
- if (matches.length === 0) lines.push(" No matching commands.");
228
- else lines.push(...matches.map((option, index) => `${index === active ? "\u276F" : " "} ${fit(option.label)}`));
229
- lines.push("\u2191\u2193 Navigate Enter Select Esc Cancel");
230
- }
231
- stdout.write(`${lines.join("\n")}
232
- `);
233
- renderedLines = lines.length;
234
- };
235
- const finish = (result) => {
539
+ const wasRaw = Boolean(stdin.isRaw);
540
+ const enabled = options.filter((option) => !option.disabled);
541
+ const finish = (result, confirmation) => {
236
542
  if (settled) return;
237
543
  settled = true;
238
544
  stdin.off("keypress", onKeypress);
239
- clear();
240
- stdin.setRawMode?.(false);
545
+ clearSelectorFrame(stdout, renderedLines);
546
+ restoreTerminal(wasRaw);
241
547
  stdin.pause();
242
- stdout.write("\n");
243
- resolve4(result);
548
+ if (confirmation) stdout.write(`${confirmation}
549
+ `);
550
+ resolve5(result);
551
+ };
552
+ const render = () => {
553
+ const lines = [title, "", ...enabled.map((option, index) => `${index === active ? "\u276F" : " "} ${option.label}`)];
554
+ renderedLines = renderSelectorFrame(stdout, renderedLines, lines);
244
555
  };
245
- const onKeypress = (value, key) => {
246
- if (key.ctrl && key.name === "c") return finish({ type: "cancelled", input });
247
- if (key.ctrl && key.name === "d") return finish({ type: "exit", input });
248
- if (key.name === "escape") {
249
- return finish({ type: "cancelled", input });
556
+ const onKeypress = (_value, key) => {
557
+ if (!key) return;
558
+ if (key.ctrl && key.name === "c") {
559
+ clearSelectorFrame(stdout, renderedLines);
560
+ restoreTerminal(wasRaw);
561
+ stdin.pause();
562
+ stdout.write("\n");
563
+ process.exit(130);
250
564
  }
565
+ if (key.name === "escape") return finish({ type: "cancelled" });
251
566
  if (key.name === "up" || key.name === "down") {
252
- const matches = filtered();
253
- if (matches.length > 0) active = key.name === "up" ? (active + matches.length - 1) % matches.length : (active + 1) % matches.length;
254
- visible = true;
567
+ active = moveSelection(active, enabled.length, key.name);
255
568
  return render();
256
569
  }
257
570
  if (key.name === "return" || key.name === "enter") {
258
- const matches = filtered();
259
- const selected = matches[active];
260
- if (selected) return finish({ type: "selected", value: selected.value, input });
261
- return finish({ type: "cancelled", input });
262
- }
263
- if (key.name === "tab") {
264
- const matches = filtered();
265
- if (matches.length === 1) input = matches[0].value;
266
- else if (matches.length > 1) active = (active + 1) % matches.length;
267
- visible = true;
268
- return render();
269
- }
270
- if (key.name === "backspace") {
271
- input = input.slice(0, -1);
272
- visible = true;
273
- return render();
274
- }
275
- const sequence = key.sequence || value;
276
- if (/^[\x20-\x7e]+$/.test(sequence)) {
277
- input += sequence;
278
- active = 0;
279
- visible = true;
280
- return render();
571
+ const selected = enabled[active];
572
+ if (!selected) return finish({ type: "cancelled" });
573
+ return finish({ type: "selected", value: selected.value }, `\u2713 ${selected.label} selected.`);
281
574
  }
282
575
  };
283
576
  emitKeypressEvents(stdin);
284
577
  stdin.setRawMode?.(true);
285
578
  stdin.resume();
579
+ stdout.write(HIDE_CURSOR);
286
580
  stdin.on("keypress", onKeypress);
287
581
  render();
288
582
  });
289
583
  }
290
584
 
291
- // src/commands/shared.ts
292
- async function connect(api = new ApiClient()) {
293
- await api.health();
294
- const session = await api.createSession();
295
- console.log(`Opening Asiyst...
296
- ${session.connectUrl}`);
297
- if (!await openBrowser(session.connectUrl)) console.log("Automatic browser opening failed; open the URL above.");
298
- console.log("\u25CF Waiting for authentication\n\u25CF Waiting for project selection\n\u25CF Waiting for connection");
299
- const expires = Date.parse(session.expiresAt);
300
- for (; ; ) {
301
- const result = await api.sessionStatus(session.sessionId);
302
- if (result.state === "completed") return { session, project: result.project };
303
- if (result.state === "expired") throw new Error("Connection session expired.");
304
- if (result.state === "cancelled") throw new Error("Connection session was cancelled.");
305
- if (Number.isFinite(expires) && Date.now() >= expires) throw new Error("Connection session expired.");
306
- await new Promise((resolve4) => setTimeout(resolve4, 2e3));
307
- }
585
+ // src/ui/secret.ts
586
+ import { stdin as stdin2, stdout as stdout2 } from "process";
587
+ import { emitKeypressEvents as emitKeypressEvents2 } from "readline";
588
+ async function readSecret(prompt) {
589
+ if (!stdin2.isTTY) return void 0;
590
+ return new Promise((resolve5) => {
591
+ let value = "";
592
+ let settled = false;
593
+ const wasRaw = Boolean(stdin2.isRaw);
594
+ stdout2.write(prompt);
595
+ const finish = (result) => {
596
+ if (settled) return;
597
+ settled = true;
598
+ stdin2.off("keypress", onKeypress);
599
+ if (stdin2.isTTY) stdin2.setRawMode?.(wasRaw);
600
+ stdin2.pause();
601
+ stdout2.write("\n");
602
+ resolve5(result);
603
+ };
604
+ const onKeypress = (chunk, key) => {
605
+ if (!key) return;
606
+ if (key.ctrl && key.name === "c") {
607
+ finish(void 0);
608
+ process.exit(130);
609
+ }
610
+ if (key.name === "escape") return finish(void 0);
611
+ if (key.name === "return" || key.name === "enter") return finish(value.trim());
612
+ if (key.name === "backspace") {
613
+ value = value.slice(0, -1);
614
+ return;
615
+ }
616
+ const sequence = key.sequence || chunk;
617
+ if (sequence && /^[\x20-\x7e]+$/.test(sequence)) value += sequence;
618
+ };
619
+ emitKeypressEvents2(stdin2);
620
+ stdin2.setRawMode?.(true);
621
+ stdin2.resume();
622
+ stdin2.on("keypress", onKeypress);
623
+ });
308
624
  }
309
- async function confirm(question) {
310
- if (!process.stdin.isTTY || !process.stdout.isTTY) return true;
311
- const result = await selectOption(question, [
312
- { label: "Yes", value: true },
313
- { label: "No", value: false }
314
- ]);
315
- return result.type === "selected" && result.value;
625
+ async function readInput(prompt) {
626
+ if (!stdin2.isTTY) return void 0;
627
+ return new Promise((resolve5) => {
628
+ let value = "";
629
+ const onData = (chunk) => {
630
+ value += String(chunk);
631
+ const newline = value.search(/[\r\n]/);
632
+ if (newline >= 0) {
633
+ stdin2.off("data", onData);
634
+ resolve5(value.slice(0, newline).trim());
635
+ }
636
+ };
637
+ stdout2.write(prompt);
638
+ stdin2.resume();
639
+ stdin2.on("data", onData);
640
+ });
316
641
  }
317
642
 
318
643
  // src/config/trust.ts
319
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
320
- import { homedir } from "os";
321
- import { join, resolve as resolve3 } from "path";
322
- var trustFile = join(homedir(), ".config", "asiyst", "trusted-folders.json");
644
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
645
+ import { homedir as homedir2 } from "os";
646
+ import { join as join3, resolve as resolve4 } from "path";
647
+ var trustFile = join3(homedir2(), ".config", "asiyst", "trusted-folders.json");
323
648
  function readTrusted() {
324
- if (!existsSync3(trustFile)) return [];
649
+ if (!existsSync4(trustFile)) return [];
325
650
  try {
326
- const parsed = JSON.parse(readFileSync3(trustFile, "utf8"));
651
+ const parsed = JSON.parse(readFileSync4(trustFile, "utf8"));
327
652
  return Array.isArray(parsed) && parsed.every((item) => typeof item === "string") ? parsed : [];
328
653
  } catch {
329
654
  return [];
330
655
  }
331
656
  }
332
657
  function isTrusted(cwd) {
333
- return readTrusted().includes(resolve3(cwd));
658
+ return readTrusted().includes(resolve4(cwd));
334
659
  }
335
660
  function trustFolder(cwd) {
336
- const folder = resolve3(cwd);
661
+ const folder = resolve4(cwd);
337
662
  const trusted = readTrusted();
338
663
  if (!trusted.includes(folder)) trusted.push(folder);
339
- mkdirSync(join(homedir(), ".config", "asiyst"), { recursive: true, mode: 448 });
340
- writeFileSync(trustFile, JSON.stringify(trusted, null, 2), { encoding: "utf8", mode: 384 });
664
+ mkdirSync3(join3(homedir2(), ".config", "asiyst"), { recursive: true, mode: 448 });
665
+ writeFileSync3(trustFile, JSON.stringify(trusted, null, 2), { encoding: "utf8", mode: 384 });
341
666
  }
342
667
  function revokeTrust(cwd) {
343
- const folder = resolve3(cwd);
668
+ const folder = resolve4(cwd);
344
669
  const trusted = readTrusted().filter((item) => item !== folder);
345
- if (existsSync3(trustFile)) writeFileSync(trustFile, JSON.stringify(trusted, null, 2), { encoding: "utf8", mode: 384 });
670
+ if (existsSync4(trustFile)) writeFileSync3(trustFile, JSON.stringify(trusted, null, 2), { encoding: "utf8", mode: 384 });
671
+ }
672
+
673
+ // src/api/client.ts
674
+ function asRecord2(value) {
675
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
676
+ }
677
+ function bodyErrorCode(body) {
678
+ const record = asRecord2(body);
679
+ const code = record?.code ?? record?.errorCode ?? asRecord2(record?.error)?.code;
680
+ return typeof code === "string" ? code : void 0;
681
+ }
682
+ function isAbortError(error) {
683
+ return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
684
+ }
685
+ var ApiClient = class {
686
+ constructor(baseUrl = resolveApiBaseUrl(), fetcher = fetch) {
687
+ this.fetcher = fetcher;
688
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
689
+ }
690
+ fetcher;
691
+ baseUrl;
692
+ async request(path, init) {
693
+ const url = `${this.baseUrl}${path}`;
694
+ const controller = new AbortController();
695
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
696
+ const headers = new Headers(init?.headers);
697
+ headers.set("Accept", "application/json");
698
+ if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
699
+ let response;
700
+ try {
701
+ response = await this.fetcher(url, {
702
+ ...init,
703
+ headers,
704
+ signal: init?.signal || controller.signal
705
+ });
706
+ } catch (error) {
707
+ if (isAbortError(error)) {
708
+ throw new ApiError("Connection to Asiyst timed out.", void 0, "TIMEOUT");
709
+ }
710
+ throw new ApiError("Unable to reach Asiyst API.", void 0, "NETWORK");
711
+ } finally {
712
+ clearTimeout(timeout);
713
+ }
714
+ const rawText = await response.text();
715
+ let body;
716
+ if (rawText) {
717
+ try {
718
+ body = JSON.parse(rawText);
719
+ } catch {
720
+ body = void 0;
721
+ }
722
+ }
723
+ if (!response.ok) {
724
+ const code = errorCodeFromStatus(response.status, bodyErrorCode(body));
725
+ if (code === "API_KEY_REVOKED" || bodyErrorCode(body) === "API_KEY_REVOKED") {
726
+ throw new ApiError("This API key has been revoked.", response.status, "API_KEY_REVOKED");
727
+ }
728
+ throw new ApiError(`Asiyst API returned HTTP ${response.status}.`, response.status, code);
729
+ }
730
+ if (rawText && body === void 0) {
731
+ if (isDebugEnabled()) {
732
+ throw new ApiError("Asiyst API returned an invalid JSON response.", response.status, "MALFORMED_RESPONSE");
733
+ }
734
+ throw new ApiError("Received an unexpected response from Asiyst.", response.status, "MALFORMED_RESPONSE");
735
+ }
736
+ return body === void 0 ? {} : body;
737
+ }
738
+ async health() {
739
+ try {
740
+ const body = await this.request("/health");
741
+ const record = asRecord2(body);
742
+ if (record) return record;
743
+ } catch {
744
+ }
745
+ throw new ApiError("Received an unexpected response from Asiyst.", 200, "MALFORMED_RESPONSE");
746
+ }
747
+ };
748
+
749
+ // src/commands/shared.ts
750
+ async function confirm(question) {
751
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
752
+ const result = await selectOption(question, [
753
+ { label: "Yes", value: true },
754
+ { label: "No", value: false }
755
+ ]);
756
+ return result.type === "selected" && result.value;
757
+ }
758
+ function createApiClient() {
759
+ return new ApiClient();
346
760
  }
347
761
 
348
762
  // src/commands/trust.ts
@@ -368,63 +782,280 @@ function revokeTrustCommand(cwd = process.cwd()) {
368
782
  console.log("\u2713 Folder trust revoked.");
369
783
  }
370
784
 
371
- // src/commands/init.ts
372
- async function initCommand(cwd = process.cwd(), api = new ApiClient()) {
785
+ // src/commands/connect.ts
786
+ async function retryOrCancel(message) {
787
+ console.log(message);
788
+ const result = await selectOption("Options:", [
789
+ { label: "Retry", value: true },
790
+ { label: "Cancel", value: false }
791
+ ]);
792
+ return result.type === "selected" && result.value;
793
+ }
794
+ function printApiFailure(error) {
795
+ if (error instanceof ApiError) {
796
+ return friendlyApiMessage(error, error.code === "NOT_FOUND" ? VERIFY_KEY_URL : void 0);
797
+ }
798
+ if (isDebugEnabled() && error instanceof Error) return `\u2717 ${error.message}`;
799
+ return "\u2717 Unable to reach Asiyst API.";
800
+ }
801
+ async function promptForProjectId() {
802
+ const value = await readInput("Enter your Project ID: ");
803
+ if (value === void 0) {
804
+ console.log("Connection cancelled.");
805
+ return void 0;
806
+ }
807
+ const trimmed = value.trim();
808
+ if (!trimmed) {
809
+ console.log("Project ID is required. Use: asiyst connect --project-id <PROJECT_ID>");
810
+ return void 0;
811
+ }
812
+ return trimmed;
813
+ }
814
+ async function promptForUserId() {
815
+ const value = await readInput("Enter your Asiyst User ID: ");
816
+ if (value === void 0) {
817
+ console.log("Connection cancelled.");
818
+ return void 0;
819
+ }
820
+ if (!isValidUserId(value)) {
821
+ console.log("Invalid User ID. It must be a 24-character public Asiyst account ID.");
822
+ return void 0;
823
+ }
824
+ return value.trim();
825
+ }
826
+ async function connectCommand(cwd = process.cwd(), api = createApiClient(), cliProjectId) {
827
+ console.log(`Project folder:
828
+ ${cwd}`);
373
829
  if (!await ensureTrusted(cwd)) return;
374
830
  const project = detectProject(cwd);
375
- if (!project.packageJson) throw new Error("No package.json found. Run this command from your website project.");
376
- console.log(`Detected ${project.framework} project.`);
377
- if (!project.sdkVersion) {
378
- console.log("\n@asiyst/sdk is not installed.\nRun: npm install @asiyst/sdk");
831
+ projectChecks(project);
832
+ if (project.framework === "Unknown") {
833
+ console.log("No supported framework was detected in this folder.");
834
+ }
835
+ const shouldConnect = await selectOption("Connect this project to Asiyst?", [
836
+ { label: "Yes", value: true },
837
+ { label: "No", value: false }
838
+ ]);
839
+ if (shouldConnect.type !== "selected" || !shouldConnect.value) {
840
+ console.log("Connection cancelled.");
379
841
  return;
380
842
  }
381
- if (!await confirm("Connect this project to Asiyst?")) return;
382
- const result = await connect(api);
383
- ok("Asiyst account connected");
384
- ok("Project connected", result.project?.projectName);
385
- if (result.project?.projectId) {
386
- console.log(`
387
- Project ID: ${result.project.projectId}
388
- Public Key: ${result.project.publicKey || project.config.publicKey || "Use the public key from the Asiyst project settings."}`);
843
+ const projectIdArg = cliProjectId ?? parseProjectIdArgument(process.argv.slice(2));
844
+ if (projectIdArg === "") {
845
+ console.log("Project ID is required. Use: asiyst connect --project-id <PROJECT_ID>");
846
+ return;
847
+ }
848
+ let projectId = typeof projectIdArg === "string" ? projectIdArg.trim() : "";
849
+ if (!projectId) {
850
+ projectId = await promptForProjectId() || "";
851
+ }
852
+ if (!projectId) {
853
+ console.log("Project ID is required. Use: asiyst connect --project-id <PROJECT_ID>");
854
+ return;
855
+ }
856
+ if (!isValidPublicIdentifier(projectId)) {
857
+ console.log("Invalid Project ID. Use a public project ID such as K8mP2xQ7_vL4N9cR5T1zB6Y3");
858
+ return;
859
+ }
860
+ const userId = await promptForUserId();
861
+ if (!userId) return;
862
+ console.log("Opening Asiyst...");
863
+ if (!await openBrowser(ASIIYST_WEB_URL)) {
864
+ console.log(`Open this URL manually:
865
+ ${ASIIYST_WEB_URL}`);
866
+ }
867
+ console.log("Register or log in, complete onboarding, create or select a project, then create an API key.");
868
+ for (; ; ) {
869
+ const apiKey = await readSecret("Paste your Asiyst API key: ");
870
+ if (apiKey === void 0) {
871
+ console.log("Connection cancelled.");
872
+ return;
873
+ }
874
+ if (!apiKey) {
875
+ fail("Invalid API key.");
876
+ continue;
877
+ }
878
+ try {
879
+ const connected = await verifyApiKey(api, apiKey, { userId, projectId });
880
+ if (connected.projectId && connected.projectId !== projectId) {
881
+ console.log("The provided Project ID does not match the authenticated project.");
882
+ return;
883
+ }
884
+ const finalConnection = { ...connected, projectId, userId };
885
+ ok("API key verified.");
886
+ ok("Project ID verified.");
887
+ await saveConnection(cwd, finalConnection);
888
+ writeProjectMetadata(cwd, finalConnection);
889
+ ok("Project connected successfully.");
890
+ console.log(`
891
+ Project ID:
892
+ ${projectId}`);
893
+ if (connected.projectName) console.log(`
894
+ Project:
895
+ ${connected.projectName}`);
896
+ if (connected.website) console.log(`
897
+ Website:
898
+ ${connected.website}`);
899
+ return;
900
+ } catch (error) {
901
+ const message = printApiFailure(error);
902
+ const code = error instanceof ApiError ? error.code : "NETWORK";
903
+ if (code === "INVALID_API_KEY") {
904
+ console.log(message);
905
+ continue;
906
+ }
907
+ if (code === "API_KEY_REVOKED" || code === "FORBIDDEN" || code === "NOT_FOUND" || code === "MALFORMED_RESPONSE") {
908
+ console.log(message);
909
+ return;
910
+ }
911
+ if (!await retryOrCancel(message)) {
912
+ console.log("Connection cancelled.");
913
+ return;
914
+ }
915
+ }
389
916
  }
390
- const warning = configWarning(cwd);
391
- if (warning) console.log(`
392
- Warning: ${warning}`);
393
- console.log('\nUse the public SDK API:\n\nimport { Asiyst } from "@asiyst/sdk";\n\nawait Asiyst.init({ projectId: "PROJECT_ID", publicKey: "PUBLIC_KEY" });');
394
- console.log("\nRun `npx asiyst verify` after starting your website.");
395
917
  }
396
918
 
397
919
  // src/commands/login.ts
398
- async function loginCommand(api = new ApiClient()) {
399
- await connect(api);
400
- console.log("\u2713 Already connected to Asiyst.");
920
+ async function loginCommand() {
921
+ await connectCommand(process.cwd(), createApiClient());
922
+ }
923
+
924
+ // src/commands/disconnect.ts
925
+ async function disconnectCommand(cwd = process.cwd()) {
926
+ if (!await confirm("Disconnect this project?")) {
927
+ console.log("Disconnect cancelled.");
928
+ return;
929
+ }
930
+ await clearConnection(cwd);
931
+ clearProjectMetadata(cwd);
932
+ console.log("\u2713 Local connection information removed.");
933
+ console.log("The cloud project and API key were not changed.");
401
934
  }
402
935
 
403
936
  // src/commands/logout.ts
404
- function logoutCommand() {
405
- console.log("\u2713 Local CLI session information removed (the CLI does not store passwords or private credentials).");
937
+ async function logoutCommand() {
938
+ await disconnectCommand();
406
939
  }
407
940
 
408
941
  // src/commands/status.ts
409
- async function statusCommand(cwd = process.cwd(), api = new ApiClient()) {
410
- const project = detectProject(cwd);
411
- console.log(`Project: ${typeof project.packageJson?.name === "string" ? project.packageJson.name : "Unknown"}
412
- SDK: ${project.sdkVersion || "Not connected"}`);
413
- if (!project.config.projectId || !project.config.publicKey) {
414
- console.log("\nProject: Not connected\nSDK: Not connected\nAvatar: Not configured\n\nStatistics:\nConnect your website to show stats.");
942
+ async function statusCommand(cwd = process.cwd(), api = createApiClient()) {
943
+ const stored = await loadConnection(cwd);
944
+ if (!stored?.apiKey) {
945
+ const metadata = readProjectMetadata(cwd);
946
+ if (metadata?.connected && metadata.projectId) {
947
+ console.log("\u2713 Connected");
948
+ console.log(`Project:
949
+ ${metadata.projectId}`);
950
+ if (metadata.userId) console.log(`User ID:
951
+ ${metadata.userId}`);
952
+ return;
953
+ }
954
+ console.log("Connect your website to show stats.");
415
955
  return;
416
956
  }
417
- const info = await api.projectInfo(project.config.projectId);
418
- console.log(`
419
- Project: ${info.connectionStatus || "Connected"}
420
- SDK: ${project.sdkVersion}
421
- Avatar: ${info.avatarStatus || "Not configured"}`);
422
- console.log("\nStatistics:");
423
- console.log(info.lastSdkConnection ? `Last SDK heartbeat: ${info.lastSdkConnection}` : "No SDK heartbeat recorded.");
957
+ if (!stored.userId || !stored.projectId) {
958
+ console.log("\u2717 Connection is missing a User ID or Project ID. Please run:\nasiyst connect");
959
+ return;
960
+ }
961
+ try {
962
+ const connected = await verifyApiKey(api, stored.apiKey, {
963
+ userId: stored.userId,
964
+ projectId: stored.projectId
965
+ });
966
+ const projectId = stored.projectId || connected.projectId;
967
+ const userId = stored.userId || connected.userId;
968
+ console.log("\u2713 Connected");
969
+ if (userId) console.log(`
970
+ User ID:
971
+ ${userId}`);
972
+ console.log(`
973
+ Project ID:
974
+ ${projectId || connected.projectName || "Unknown"}`);
975
+ console.log("\nAPI Key:\n\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022");
976
+ if (stored.avatarId) console.log(`
977
+ Avatar:
978
+ ${stored.avatarId}`);
979
+ if (connected.website) console.log(`
980
+ Website:
981
+ ${connected.website}`);
982
+ } catch (error) {
983
+ if (error instanceof ApiError && error.code === "API_KEY_REVOKED") {
984
+ console.log("\u2717 API key revoked.");
985
+ console.log("Create a new key from:\nhttps://asiyst.com");
986
+ return;
987
+ }
988
+ if (error instanceof ApiError && (error.code === "INVALID_API_KEY" || error.code === "FORBIDDEN")) {
989
+ console.log("\u2717 Connection invalid.");
990
+ return;
991
+ }
992
+ console.log("\u2717 Connection invalid.");
993
+ }
994
+ }
995
+
996
+ // src/api/projects.ts
997
+ async function verifyInstallation(api, projectId, publicKey, domain) {
998
+ const value = await api.request("/cli/verification", {
999
+ method: "POST",
1000
+ body: JSON.stringify({ projectId, publicKey, domain })
1001
+ });
1002
+ if (!Array.isArray(value) || !value.every((item) => item && typeof item === "object" && typeof item.name === "string" && typeof item.ok === "boolean")) {
1003
+ throw new ApiError("Received an unexpected response from Asiyst.", 200, "MALFORMED_RESPONSE");
1004
+ }
1005
+ return value;
1006
+ }
1007
+ function requireIdentifier(value, name, valid) {
1008
+ const trimmed = value.trim();
1009
+ if (!valid(trimmed)) throw new ApiError(`Invalid ${name}.`, 400, "INVALID_REQUEST");
1010
+ return trimmed;
1011
+ }
1012
+ function parseAvatarImportResponse(value, projectId, userId, avatarId) {
1013
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1014
+ throw new ApiError("Received an unexpected response from Asiyst.", 200, "MALFORMED_RESPONSE");
1015
+ }
1016
+ const body = value;
1017
+ const imported = body.imported === true || body.success === true || body.status === "imported";
1018
+ if (!imported) {
1019
+ const code = typeof body.code === "string" ? body.code : "IMPORT_FAILED";
1020
+ const supportedCode = ["AVATAR_NOT_FOUND", "AVATAR_ALREADY_IMPORTED", "FORBIDDEN", "PROJECT_NOT_FOUND", "CONFLICT"].includes(code) ? code : "IMPORT_FAILED";
1021
+ throw new ApiError(typeof body.message === "string" ? body.message : "Avatar import was not completed.", 200, supportedCode);
1022
+ }
1023
+ const returnedAvatarId = body.avatarId ?? body.avatar_id;
1024
+ if (returnedAvatarId !== void 0 && returnedAvatarId !== avatarId) {
1025
+ throw new ApiError("The API returned a different avatar than requested.", 200, "AVATAR_MISMATCH");
1026
+ }
1027
+ return {
1028
+ imported: true,
1029
+ avatarId,
1030
+ projectId,
1031
+ userId,
1032
+ avatarName: typeof body.avatarName === "string" ? body.avatarName : void 0
1033
+ };
1034
+ }
1035
+ async function importAvatar(api, input) {
1036
+ const userId = requireIdentifier(input.userId, "User ID", isValidUserId);
1037
+ const projectId = requireIdentifier(input.projectId, "Project ID", isValidPublicIdentifier);
1038
+ const avatarId = requireIdentifier(input.avatarId, "Avatar ID", isValidAvatarId);
1039
+ const apiKey = input.apiKey.trim();
1040
+ if (!apiKey) throw new ApiError("An API key is required.", 401, "INVALID_API_KEY");
1041
+ const value = await api.request(`/cli/projects/${encodeURIComponent(projectId)}/avatars/import`, {
1042
+ method: "POST",
1043
+ headers: {
1044
+ Authorization: `Bearer ${apiKey}`,
1045
+ "X-Asiyst-API-Key": apiKey
1046
+ },
1047
+ body: JSON.stringify({ userId, projectId, avatarId })
1048
+ });
1049
+ return parseAvatarImportResponse(value, projectId, userId, avatarId);
424
1050
  }
425
1051
 
426
1052
  // src/commands/verify.ts
427
- async function verifyCommand(cwd = process.cwd(), api = new ApiClient()) {
1053
+ function printVerificationSafe(results) {
1054
+ console.log("\nAsiyst Installation Verification\n");
1055
+ for (const result of results) result.ok ? ok(result.name, result.detail) : fail(result.name, result.detail);
1056
+ return results.length > 0 && results.every((result) => result.ok);
1057
+ }
1058
+ async function verifyCommand(cwd = process.cwd()) {
428
1059
  const project = detectProject(cwd);
429
1060
  if (!project.packageJson) {
430
1061
  fail("Project detected", "package.json is missing");
@@ -432,105 +1063,180 @@ async function verifyCommand(cwd = process.cwd(), api = new ApiClient()) {
432
1063
  return;
433
1064
  }
434
1065
  ok("Project detected");
435
- if (!project.sdkVersion) {
436
- fail("SDK installed", "npm install @asiyst/sdk");
437
- process.exitCode = 1;
438
- return;
439
- }
440
- ok("SDK installed", project.sdkVersion);
441
- if (!project.config.projectId || !project.config.publicKey) {
442
- fail("Configuration found", "set ASIIYST_PROJECT_ID and ASIIYST_PUBLIC_KEY");
1066
+ const stored = await loadConnection(cwd);
1067
+ if (!stored) {
1068
+ fail("Configuration found", "run asiyst connect");
443
1069
  process.exitCode = 1;
444
1070
  return;
445
1071
  }
446
1072
  ok("Configuration found");
447
1073
  try {
448
- const results = await api.verify(project.config.projectId, project.config.publicKey, process.env.ASIIYST_DOMAIN);
449
- if (!printVerification(results)) process.exitCode = 1;
1074
+ const results = await verifyInstallation(createApiClient(), stored.projectId, stored.publicKey || "", process.env.ASIIYST_DOMAIN);
1075
+ if (!printVerificationSafe(results)) process.exitCode = 1;
450
1076
  } catch (error) {
451
1077
  fail("API reachable", error instanceof Error ? error.message : "verification failed");
452
1078
  process.exitCode = 1;
453
1079
  }
454
1080
  }
455
1081
 
1082
+ // src/config/version.ts
1083
+ import { readFileSync as readFileSync5 } from "fs";
1084
+ import { fileURLToPath } from "url";
1085
+ function readCurrentVersion() {
1086
+ if ("1.1.1") return "1.1.1";
1087
+ try {
1088
+ const packageJson = JSON.parse(readFileSync5(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
1089
+ if (packageJson && typeof packageJson === "object" && typeof packageJson.version === "string") {
1090
+ return packageJson.version;
1091
+ }
1092
+ } catch {
1093
+ }
1094
+ try {
1095
+ const packageJson = JSON.parse(readFileSync5(fileURLToPath(new URL("../../package.json", import.meta.url)), "utf8"));
1096
+ if (packageJson && typeof packageJson === "object" && typeof packageJson.version === "string" && packageJson.name === "@asiyst/cli") {
1097
+ return packageJson.version;
1098
+ }
1099
+ } catch {
1100
+ }
1101
+ return "0.0.0";
1102
+ }
1103
+
456
1104
  // src/commands/doctor.ts
457
- async function doctorCommand(cwd = process.cwd(), api = new ApiClient()) {
458
- const project = detectProject(cwd);
1105
+ async function doctorCommand(cwd = process.cwd(), api = createApiClient()) {
1106
+ ok("CLI version", readCurrentVersion());
1107
+ ok("Executable", process.argv[1] || "unknown");
1108
+ ok("API base", api.baseUrl || CLI_API_BASE_URL);
1109
+ ok("Verify endpoint", VERIFY_KEY_URL);
459
1110
  ok("System", process.platform);
460
1111
  ok("Node.js", process.version);
461
- ok("npm", "available through npm");
1112
+ const project = detectProject(cwd);
462
1113
  project.packageJson ? ok("Project", cwd) : fail("Project", "package.json is missing");
463
1114
  project.sdkVersion ? ok("SDK", project.sdkVersion) : fail("SDK", "not installed");
464
- project.config.projectId && project.config.publicKey ? ok("Configuration") : fail("Configuration", "public project values are missing");
1115
+ const stored = await loadConnection(cwd);
1116
+ stored ? ok("Local credential", "present") : fail("Local credential", "not connected");
465
1117
  try {
466
1118
  await api.health();
467
- ok("Asiyst API", "HTTPS health endpoint reachable");
1119
+ ok("Asiyst API", "health endpoint reachable");
468
1120
  } catch (error) {
469
1121
  fail("Asiyst API", error instanceof Error ? error.message : "unreachable");
470
1122
  }
471
- if (project.config.projectId && project.config.publicKey) {
472
- try {
473
- await api.projectInfo(project.config.projectId);
474
- ok("Project connection");
475
- } catch (error) {
476
- fail("Project connection", error instanceof Error ? error.message : "unavailable");
477
- }
478
- }
479
1123
  }
480
1124
 
481
1125
  // src/commands/dashboard.ts
482
1126
  async function dashboardCommand() {
483
- const configured = process.env.ASIIYST_DASHBOARD_URL || `${ASIIYST_WEB_URL}/dashboard`;
484
- const parsed = new URL(configured);
485
- if (parsed.protocol !== "https:") throw new Error("Configured dashboard URL must use HTTPS.");
486
- const url = parsed.toString();
487
- if (!await openBrowser(url)) console.log(`Open ${url}`);
1127
+ const url = ASIIYST_WEB_URL;
1128
+ if (!await openBrowser(url)) console.log(`Open this URL manually:
1129
+ ${url}`);
488
1130
  else console.log(`Opening ${url}`);
489
1131
  }
490
1132
 
491
1133
  // src/commands/avatar.ts
492
- async function avatarCommand(cwd = process.cwd(), api = new ApiClient()) {
493
- const project = detectProject(cwd);
494
- if (!project.config.projectId) {
495
- console.log("Avatar\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nNo avatar configured.\n\nCreate your avatar in the Asiyst dashboard.");
496
- const url = `${ASIIYST_WEB_URL}/dashboard`;
1134
+ function parseAvatarId(argv) {
1135
+ const index = argv.findIndex((value) => value === "--avatar-id");
1136
+ if (index >= 0) return argv[index + 1];
1137
+ const inline = argv.find((value) => value.startsWith("--avatar-id="));
1138
+ return inline?.slice("--avatar-id=".length);
1139
+ }
1140
+ async function avatarImportCommand(cwd = process.cwd(), api = createApiClient(), suppliedAvatarId) {
1141
+ const stored = await loadConnection(cwd);
1142
+ if (!stored?.apiKey || !stored.userId || !stored.projectId) {
1143
+ console.log("No project is connected. Please run:\nasiyst connect");
1144
+ return;
1145
+ }
1146
+ const avatarId = suppliedAvatarId ?? await readInput("Enter your Avatar ID: ");
1147
+ if (!avatarId || !isValidAvatarId(avatarId)) {
1148
+ console.log("Invalid Avatar ID. It must contain exactly 10 alphanumeric characters.");
1149
+ return;
1150
+ }
1151
+ try {
1152
+ const result = await importAvatar(api, {
1153
+ userId: stored.userId,
1154
+ projectId: stored.projectId,
1155
+ apiKey: stored.apiKey,
1156
+ avatarId
1157
+ });
1158
+ await saveConnection(cwd, { ...stored, avatarId: result.avatarId });
1159
+ console.log(`\u2713 Avatar ${result.avatarId} imported into project ${result.projectId}.`);
1160
+ } catch (error) {
1161
+ if (error instanceof ApiError) {
1162
+ if (error.code === "FORBIDDEN") console.log("\u2717 You do not have permission to import an avatar into this project.");
1163
+ else if (error.code === "NOT_FOUND") console.log("\u2717 Project or avatar was not found.");
1164
+ else if (error.code === "CONFLICT" || error.code === "AVATAR_ALREADY_IMPORTED") console.log("\u2717 This avatar is already imported into the project.");
1165
+ else console.log(`\u2717 ${error.message}`);
1166
+ return;
1167
+ }
1168
+ console.log("\u2717 Unable to import the avatar.");
1169
+ }
1170
+ }
1171
+ async function avatarCommand(cwd = process.cwd(), argv = process.argv.slice(2)) {
1172
+ if (argv[0] === "import") {
1173
+ await avatarImportCommand(cwd, createApiClient(), parseAvatarId(argv));
1174
+ return;
1175
+ }
1176
+ const stored = await loadConnection(cwd);
1177
+ if (!stored) {
1178
+ console.log("Connect your project to Asiyst to view avatar information.");
1179
+ const url = ASIIYST_WEB_URL;
497
1180
  if (await openBrowser(url)) console.log(`Opening ${url}`);
498
- else console.log(`Open ${url}`);
1181
+ else console.log(`Open this URL manually:
1182
+ ${url}`);
499
1183
  return;
500
1184
  }
501
- const info = await api.projectInfo(project.config.projectId);
502
- console.log(`Avatar
503
- \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
504
- Status ${info.avatarStatus || "Not configured"}
505
-
506
- Configure your avatar in the Asiyst dashboard.`);
1185
+ console.log(`Avatar configuration is managed at ${ASIIYST_WEB_URL}.`);
1186
+ if (!await openBrowser(ASIIYST_WEB_URL)) console.log(`Open this URL manually:
1187
+ ${ASIIYST_WEB_URL}`);
507
1188
  }
508
1189
 
509
- // src/update/check.ts
510
- import { execFileSync } from "child_process";
511
- import { readFileSync as readFileSync5 } from "fs";
1190
+ // src/commands/interactive.ts
1191
+ function interactiveHelp() {
1192
+ console.log(`
1193
+ Asiyst CLI
512
1194
 
513
- // src/config/version.ts
514
- import { readFileSync as readFileSync4 } from "fs";
515
- import { fileURLToPath } from "url";
516
- function readCurrentVersion() {
517
- for (const packageUrl of [
518
- new URL("../package.json", import.meta.url),
519
- new URL("../../package.json", import.meta.url),
520
- new URL("../../../package.json", import.meta.url)
521
- ]) {
522
- try {
523
- const packageJson = JSON.parse(readFileSync4(fileURLToPath(packageUrl), "utf8"));
524
- if (packageJson && typeof packageJson === "object" && typeof packageJson.version === "string") {
525
- return packageJson.version;
526
- }
527
- } catch {
1195
+ Usage: asiyst [command]
1196
+
1197
+ Commands
1198
+ connect Connect this project to Asiyst
1199
+ avatar import Import an avatar into the connected project
1200
+ status Show connection status
1201
+ health Check Asiyst API connectivity
1202
+ disconnect Remove local connection information
1203
+ help Show available commands
1204
+ version Show CLI version
1205
+
1206
+ Also available
1207
+ trust Trust this project folder
1208
+ revoke-trust Revoke folder trust
1209
+ doctor Run diagnostics
1210
+ update Check for CLI updates
1211
+
1212
+ Examples
1213
+ asiyst connect
1214
+ asiyst status
1215
+ asiyst disconnect
1216
+ `);
1217
+ }
1218
+ async function interactiveHome() {
1219
+ homeStatus();
1220
+ for (; ; ) {
1221
+ const result = await selectOption("What would you like to do?", [
1222
+ { label: "Connect", value: "connect" },
1223
+ { label: "Status", value: "status" },
1224
+ { label: "Help", value: "help" },
1225
+ { label: "Exit", value: "exit" }
1226
+ ]);
1227
+ if (result.type !== "selected" || result.value === "exit") {
1228
+ break;
528
1229
  }
1230
+ if (result.value === "connect") await connectCommand();
1231
+ else if (result.value === "status") await statusCommand();
1232
+ else if (result.value === "help") interactiveHelp();
1233
+ else if (result.value === "version") console.log(readCurrentVersion());
529
1234
  }
530
- return "0.2.0";
531
1235
  }
532
1236
 
533
1237
  // src/update/check.ts
1238
+ import { execFileSync } from "child_process";
1239
+ import { readFileSync as readFileSync6 } from "fs";
534
1240
  var PACKAGE_NAME = "@asiyst/cli";
535
1241
  var REGISTRY_URL = "https://registry.npmjs.org/%40asiyst%2Fcli/latest";
536
1242
  var TIMEOUT_MS = 800;
@@ -602,7 +1308,7 @@ function detectInstallKind(executablePath = process.argv[1] ?? "", env = process
602
1308
  }
603
1309
  function installedVersion(packageRoot) {
604
1310
  try {
605
- const value = JSON.parse(readFileSync5(`${packageRoot}/package.json`, "utf8"));
1311
+ const value = JSON.parse(readFileSync6(`${packageRoot}/package.json`, "utf8"));
606
1312
  return value && typeof value === "object" && typeof value.version === "string" ? value.version : void 0;
607
1313
  } catch {
608
1314
  return void 0;
@@ -663,102 +1369,65 @@ ${error instanceof Error ? error.message : "Installation could not be verified."
663
1369
  }
664
1370
  }
665
1371
 
666
- // src/commands/interactive.ts
667
- function resolveInput(value) {
668
- const input = value.trim().toLowerCase();
669
- if (/^(connect|connect my website|connect this website|connect website|init)/.test(input)) return "connect";
670
- if (/^(status|check status|check .*installation)/.test(input)) return "status";
671
- if (/diagnos|doctor/.test(input)) return "diagnostics";
672
- if (/dashboard/.test(input)) return "dashboard";
673
- if (/avatar|configure my avatar|create avatar/.test(input)) return "avatar";
674
- if (/update|upgrade/.test(input)) return "update";
675
- if (/^(help|\?)$/.test(input)) return "help";
676
- if (/^(version|--version)$/.test(input)) return "version";
677
- return input;
678
- }
679
- function interactiveHelp() {
680
- console.log("\nASIYST CLI\n\nUsage: asiyst [command]\n\nCommands\n connect Connect this project to Asiyst\n status Show project and Asiyst status\n diagnostics Run diagnostics\n dashboard Open Asiyst dashboard\n avatar Configure avatar\n update Check for and install CLI updates\n help Show available commands\n version Show CLI version\n trust Trust this project folder\n revoke-trust Revoke folder trust\n logout Remove local CLI session information\n\nExamples\n asiyst connect\n asiyst status\n asiyst update");
681
- }
682
- async function interactiveHome() {
683
- homeStatus(detectProject());
684
- console.log("\nWelcome to Asiyst CLI.\nUse UP/DOWN to select a command, or type a command.\n");
685
- const suggestions = [
686
- { label: "Connect this project", value: "connect" },
687
- { label: "Check project status", value: "status" },
688
- { label: "Run diagnostics", value: "diagnostics" },
689
- { label: "Open dashboard", value: "dashboard" },
690
- { label: "Configure avatar", value: "avatar" },
691
- { label: "Update CLI", value: "update" },
692
- { label: "Help", value: "help" },
693
- { label: "Exit", value: "exit" }
694
- ];
695
- for (; ; ) {
696
- const result = await selectOption("What would you like to do?", suggestions, "\u203A ");
697
- if (result.type === "exit") break;
698
- if (result.type !== "selected") continue;
699
- const command = resolveInput(result.input || String(result.value));
700
- if (command === "exit" || command === "quit") {
701
- const confirmed = await selectOption("Exit Asiyst?", [
702
- { label: "Yes", value: true },
703
- { label: "No", value: false }
704
- ]);
705
- if (confirmed.type === "selected") {
706
- if (confirmed.value) break;
707
- }
708
- continue;
709
- }
710
- if (command === "connect") await initCommand();
711
- else if (command === "status") await statusCommand();
712
- else if (command === "diagnostics") await doctorCommand();
713
- else if (command === "dashboard") await dashboardCommand();
714
- else if (command === "avatar") await avatarCommand();
715
- else if (command === "update") await updateCommand();
716
- else if (command === "logout") logoutCommand();
717
- else if (command === "trust") await trustCommand();
718
- else if (command === "revoke-trust") revokeTrustCommand();
719
- else if (command === "help") interactiveHelp();
720
- else if (command === "version") console.log(readCurrentVersion());
721
- else console.log(`I don't recognize that command: ${result.input}
722
- Run 'asiyst help' for available commands.`);
1372
+ // src/commands/health.ts
1373
+ async function healthCommand(api = createApiClient()) {
1374
+ try {
1375
+ await api.health();
1376
+ console.log("\u2713 Asiyst API is reachable.");
1377
+ } catch {
1378
+ console.log("\u2717 Unable to reach Asiyst API.");
723
1379
  }
724
1380
  }
725
1381
 
726
1382
  // src/index.ts
727
1383
  import { fileURLToPath as fileURLToPath2 } from "url";
1384
+ import { realpathSync } from "fs";
728
1385
  var CLI_VERSION = readCurrentVersion();
729
1386
  function help() {
730
1387
  interactiveHelp();
731
1388
  }
732
1389
  async function main(argv = process.argv.slice(2)) {
733
1390
  const command = argv[0] || "";
1391
+ const projectId = parseProjectIdArgument(argv);
734
1392
  if (command !== "update" && !argv.includes("--version") && !argv.includes("-v") && !argv.includes("--help") && !argv.includes("-h")) {
735
1393
  await notifyIfUpdateAvailable();
736
1394
  }
737
1395
  if (command === "--help" || command === "-h") return help();
738
1396
  if (command === "--version" || command === "-v") return console.log(CLI_VERSION);
739
- if (command === "init" || command === "connect") return initCommand();
1397
+ if (command === "init") return connectCommand(process.cwd(), void 0, projectId);
1398
+ if (command === "connect") return connectCommand(process.cwd(), void 0, projectId);
740
1399
  if (command === "login") return loginCommand();
741
1400
  if (command === "logout") return logoutCommand();
1401
+ if (command === "disconnect") return disconnectCommand();
742
1402
  if (command === "status") return statusCommand();
743
1403
  if (command === "verify") return verifyCommand();
1404
+ if (command === "health") return healthCommand();
744
1405
  if (command === "doctor") return doctorCommand();
745
1406
  if (command === "diagnostics") return doctorCommand();
746
1407
  if (command === "dashboard") return dashboardCommand();
747
- if (command === "avatar") return avatarCommand();
1408
+ if (command === "avatar") return avatarCommand(process.cwd(), argv.slice(1));
748
1409
  if (command === "trust") return trustCommand();
749
1410
  if (command === "revoke-trust") return revokeTrustCommand();
750
1411
  if (command === "update") return updateCommand();
751
1412
  if (command === "help") return help();
752
1413
  if (command === "version") return console.log(CLI_VERSION);
753
1414
  if (!process.stdin.isTTY) {
754
- const project = detectProject();
755
- projectChecks(project);
756
- console.log("\nRun `npx @asiyst/cli connect` to connect this project, or `npx @asiyst/cli --help` for commands.");
1415
+ help();
757
1416
  return;
758
1417
  }
759
1418
  return interactiveHome();
760
1419
  }
761
- if (process.argv[1] && fileURLToPath2(import.meta.url) === process.argv[1]) {
1420
+ function isDirectExecution() {
1421
+ if (!process.argv[1]) return false;
1422
+ try {
1423
+ const entryPath = realpathSync(fileURLToPath2(import.meta.url));
1424
+ const execPath = realpathSync(process.argv[1]);
1425
+ return entryPath.toLowerCase() === execPath.toLowerCase();
1426
+ } catch {
1427
+ return true;
1428
+ }
1429
+ }
1430
+ if (isDirectExecution()) {
762
1431
  main().catch((error) => {
763
1432
  console.error(`
764
1433
  Error: ${error instanceof Error ? error.message : "command failed"}`);
@@ -767,6 +1436,7 @@ Error: ${error instanceof Error ? error.message : "command failed"}`);
767
1436
  }
768
1437
  export {
769
1438
  CLI_VERSION,
1439
+ connectCommand as initCommand,
770
1440
  main
771
1441
  };
772
1442
  //# sourceMappingURL=index.js.map