@citeme/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/README.md +263 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1239 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/login.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
import chalk2 from "chalk";
|
|
9
|
+
|
|
10
|
+
// src/services/api.ts
|
|
11
|
+
import axios from "axios";
|
|
12
|
+
|
|
13
|
+
// src/utils/config.ts
|
|
14
|
+
import Conf from "conf";
|
|
15
|
+
var config = new Conf({
|
|
16
|
+
projectName: "citeme",
|
|
17
|
+
schema: {
|
|
18
|
+
apiKey: {
|
|
19
|
+
type: "string"
|
|
20
|
+
},
|
|
21
|
+
apiUrl: {
|
|
22
|
+
type: "string",
|
|
23
|
+
default: "https://citeme.io"
|
|
24
|
+
},
|
|
25
|
+
email: {
|
|
26
|
+
type: "string"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
function getApiKey() {
|
|
31
|
+
return config.get("apiKey");
|
|
32
|
+
}
|
|
33
|
+
function setApiKey(apiKey) {
|
|
34
|
+
config.set("apiKey", apiKey);
|
|
35
|
+
}
|
|
36
|
+
function getApiUrl() {
|
|
37
|
+
return config.get("apiUrl") || "https://citeme.io";
|
|
38
|
+
}
|
|
39
|
+
function getEmail() {
|
|
40
|
+
return config.get("email");
|
|
41
|
+
}
|
|
42
|
+
function setEmail(email) {
|
|
43
|
+
config.set("email", email);
|
|
44
|
+
}
|
|
45
|
+
function clearConfig() {
|
|
46
|
+
config.clear();
|
|
47
|
+
}
|
|
48
|
+
function isAuthenticated() {
|
|
49
|
+
return !!config.get("apiKey");
|
|
50
|
+
}
|
|
51
|
+
function getConfigPath() {
|
|
52
|
+
return config.path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/services/api.ts
|
|
56
|
+
var ApiService = class {
|
|
57
|
+
client;
|
|
58
|
+
constructor() {
|
|
59
|
+
this.client = axios.create({
|
|
60
|
+
baseURL: getApiUrl(),
|
|
61
|
+
timeout: 3e4,
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"User-Agent": "@citeme/cli"
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
this.client.interceptors.request.use((config2) => {
|
|
68
|
+
const apiKey = getApiKey();
|
|
69
|
+
if (apiKey) {
|
|
70
|
+
config2.headers.Authorization = `Bearer ${apiKey}`;
|
|
71
|
+
}
|
|
72
|
+
return config2;
|
|
73
|
+
});
|
|
74
|
+
this.client.interceptors.response.use(
|
|
75
|
+
(response) => response,
|
|
76
|
+
(error) => {
|
|
77
|
+
if (error.response?.status === 401) {
|
|
78
|
+
throw new ApiError("Authentication failed. Please run `citeme login` first.", 401);
|
|
79
|
+
}
|
|
80
|
+
if (error.response?.status === 403) {
|
|
81
|
+
throw new ApiError("Access denied. Your API key may have expired.", 403);
|
|
82
|
+
}
|
|
83
|
+
if (error.response?.status === 429) {
|
|
84
|
+
throw new ApiError("Rate limit exceeded. Please try again later.", 429);
|
|
85
|
+
}
|
|
86
|
+
throw new ApiError(
|
|
87
|
+
error.response?.data?.message || error.message,
|
|
88
|
+
error.response?.status
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
async authenticate(email, password2) {
|
|
94
|
+
const response = await this.client.post("/api/v1/cli/auth", {
|
|
95
|
+
email,
|
|
96
|
+
password: password2
|
|
97
|
+
});
|
|
98
|
+
return response.data;
|
|
99
|
+
}
|
|
100
|
+
async audit(request) {
|
|
101
|
+
const response = await this.client.post("/api/v1/cli/audit", request);
|
|
102
|
+
return response.data;
|
|
103
|
+
}
|
|
104
|
+
async getApprovedPatches(auditId) {
|
|
105
|
+
const params = auditId ? { auditId } : {};
|
|
106
|
+
const response = await this.client.get("/api/v1/cli/patches", { params });
|
|
107
|
+
return response.data.patches;
|
|
108
|
+
}
|
|
109
|
+
async confirmPatchApplied(patchId) {
|
|
110
|
+
await this.client.post(`/api/v1/cli/patches/${patchId}/applied`);
|
|
111
|
+
}
|
|
112
|
+
async validateApiKey() {
|
|
113
|
+
try {
|
|
114
|
+
const response = await this.client.get("/api/v1/cli/validate");
|
|
115
|
+
return response.data;
|
|
116
|
+
} catch {
|
|
117
|
+
return { valid: false };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async getLatestAudit() {
|
|
121
|
+
try {
|
|
122
|
+
const response = await this.client.get("/api/v1/cli/audits/latest");
|
|
123
|
+
return response.data.audit;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async generateSuggestions(request) {
|
|
129
|
+
const response = await this.client.post(
|
|
130
|
+
"/api/v1/cli/suggestions/generate",
|
|
131
|
+
request
|
|
132
|
+
);
|
|
133
|
+
return response.data.suggestions;
|
|
134
|
+
}
|
|
135
|
+
async listSuggestions(options) {
|
|
136
|
+
const response = await this.client.get(
|
|
137
|
+
"/api/v1/cli/suggestions",
|
|
138
|
+
{ params: options }
|
|
139
|
+
);
|
|
140
|
+
return response.data.suggestions;
|
|
141
|
+
}
|
|
142
|
+
async getSuggestion(suggestionId) {
|
|
143
|
+
const response = await this.client.get(
|
|
144
|
+
`/api/v1/cli/suggestions/${suggestionId}`
|
|
145
|
+
);
|
|
146
|
+
return response.data;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var ApiError = class extends Error {
|
|
150
|
+
constructor(message, statusCode) {
|
|
151
|
+
super(message);
|
|
152
|
+
this.statusCode = statusCode;
|
|
153
|
+
this.name = "ApiError";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
var api = new ApiService();
|
|
157
|
+
|
|
158
|
+
// src/utils/display.ts
|
|
159
|
+
import chalk from "chalk";
|
|
160
|
+
var PULSE_METER_WIDTH = 50;
|
|
161
|
+
function renderPulseMeter(score, label) {
|
|
162
|
+
const normalizedScore = Math.max(0, Math.min(100, score));
|
|
163
|
+
const filledWidth = Math.round(normalizedScore / 100 * PULSE_METER_WIDTH);
|
|
164
|
+
const emptyWidth = PULSE_METER_WIDTH - filledWidth;
|
|
165
|
+
const color = getScoreColor(normalizedScore);
|
|
166
|
+
const filled = color("\u2588".repeat(filledWidth));
|
|
167
|
+
const empty = chalk.gray("\u2591".repeat(emptyWidth));
|
|
168
|
+
const lines = [
|
|
169
|
+
"",
|
|
170
|
+
chalk.bold(" GEO Score"),
|
|
171
|
+
` \u250C${"\u2500".repeat(PULSE_METER_WIDTH + 2)}\u2510`,
|
|
172
|
+
` \u2502 ${filled}${empty} \u2502`,
|
|
173
|
+
` \u2514${"\u2500".repeat(PULSE_METER_WIDTH + 2)}\u2518`,
|
|
174
|
+
` ${color.bold(`${normalizedScore}/100`)}${label ? chalk.gray(` - ${label}`) : ""}`,
|
|
175
|
+
""
|
|
176
|
+
];
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
function getScoreColor(score) {
|
|
180
|
+
if (score >= 80) return chalk.green;
|
|
181
|
+
if (score >= 60) return chalk.yellow;
|
|
182
|
+
if (score >= 40) return chalk.hex("#FFA500");
|
|
183
|
+
return chalk.red;
|
|
184
|
+
}
|
|
185
|
+
function getScoreLabel(score) {
|
|
186
|
+
if (score >= 80) return "Excellent";
|
|
187
|
+
if (score >= 60) return "Good";
|
|
188
|
+
if (score >= 40) return "Needs Improvement";
|
|
189
|
+
if (score >= 20) return "Poor";
|
|
190
|
+
return "Critical";
|
|
191
|
+
}
|
|
192
|
+
function renderBox(title, content, width = 60) {
|
|
193
|
+
const lines = [];
|
|
194
|
+
const innerWidth = width - 4;
|
|
195
|
+
lines.push(`\u256D${"\u2500".repeat(width - 2)}\u256E`);
|
|
196
|
+
lines.push(`\u2502 ${chalk.bold(title.padEnd(innerWidth))} \u2502`);
|
|
197
|
+
lines.push(`\u251C${"\u2500".repeat(width - 2)}\u2524`);
|
|
198
|
+
for (const line of content) {
|
|
199
|
+
const truncated = line.length > innerWidth ? line.slice(0, innerWidth - 3) + "..." : line;
|
|
200
|
+
lines.push(`\u2502 ${truncated.padEnd(innerWidth)} \u2502`);
|
|
201
|
+
}
|
|
202
|
+
lines.push(`\u2570${"\u2500".repeat(width - 2)}\u256F`);
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
function renderError(message) {
|
|
206
|
+
return chalk.red(`\u2716 ${message}`);
|
|
207
|
+
}
|
|
208
|
+
function renderSuccess(message) {
|
|
209
|
+
return chalk.green(`\u2714 ${message}`);
|
|
210
|
+
}
|
|
211
|
+
function renderWarning(message) {
|
|
212
|
+
return chalk.yellow(`\u26A0 ${message}`);
|
|
213
|
+
}
|
|
214
|
+
function renderInfo(message) {
|
|
215
|
+
return chalk.blue(`\u2139 ${message}`);
|
|
216
|
+
}
|
|
217
|
+
function renderIssuesList(issues) {
|
|
218
|
+
if (issues.length === 0) {
|
|
219
|
+
return chalk.green("\n \u2714 No technical issues found\n");
|
|
220
|
+
}
|
|
221
|
+
const grouped = issues.reduce((acc, issue) => {
|
|
222
|
+
if (!acc[issue.category]) acc[issue.category] = [];
|
|
223
|
+
acc[issue.category].push(issue);
|
|
224
|
+
return acc;
|
|
225
|
+
}, {});
|
|
226
|
+
const lines = [""];
|
|
227
|
+
for (const [category, categoryIssues] of Object.entries(grouped)) {
|
|
228
|
+
lines.push(chalk.bold(` ${category}`));
|
|
229
|
+
for (const issue of categoryIssues) {
|
|
230
|
+
const icon = issue.type === "error" ? chalk.red("\u2716") : issue.type === "warning" ? chalk.yellow("\u26A0") : chalk.blue("\u2139");
|
|
231
|
+
const location = issue.file ? chalk.gray(` (${issue.file}${issue.line ? `:${issue.line}` : ""})`) : "";
|
|
232
|
+
lines.push(` ${icon} ${issue.message}${location}`);
|
|
233
|
+
}
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
return lines.join("\n");
|
|
237
|
+
}
|
|
238
|
+
function renderHeader() {
|
|
239
|
+
const logo = `
|
|
240
|
+
${chalk.hex("#8B5CF6")("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
|
|
241
|
+
${chalk.hex("#8B5CF6")("\u2551")} ${chalk.bold.hex("#8B5CF6")("CiteMe CLI")} ${chalk.gray("- GEO Optimization Tool")} ${chalk.hex("#8B5CF6")("\u2551")}
|
|
242
|
+
${chalk.hex("#8B5CF6")("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
|
|
243
|
+
`;
|
|
244
|
+
return logo;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/commands/login.ts
|
|
248
|
+
async function loginCommand() {
|
|
249
|
+
console.log(renderHeader());
|
|
250
|
+
if (isAuthenticated()) {
|
|
251
|
+
const email = getEmail();
|
|
252
|
+
const shouldContinue = await p.confirm({
|
|
253
|
+
message: `You are already logged in as ${chalk2.cyan(email)}. Do you want to log in with a different account?`
|
|
254
|
+
});
|
|
255
|
+
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
|
256
|
+
p.outro(chalk2.gray("Login cancelled."));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
clearConfig();
|
|
260
|
+
}
|
|
261
|
+
p.intro(chalk2.hex("#8B5CF6")("Login to CiteMe"));
|
|
262
|
+
const credentials = await p.group(
|
|
263
|
+
{
|
|
264
|
+
email: () => p.text({
|
|
265
|
+
message: "Enter your email:",
|
|
266
|
+
placeholder: "you@example.com",
|
|
267
|
+
validate: (value) => {
|
|
268
|
+
if (!value) return "Email is required";
|
|
269
|
+
if (!value.includes("@")) return "Please enter a valid email";
|
|
270
|
+
}
|
|
271
|
+
}),
|
|
272
|
+
password: () => p.password({
|
|
273
|
+
message: "Enter your password:",
|
|
274
|
+
validate: (value) => {
|
|
275
|
+
if (!value) return "Password is required";
|
|
276
|
+
if (value.length < 6) return "Password must be at least 6 characters";
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
onCancel: () => {
|
|
282
|
+
p.cancel("Login cancelled.");
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
const spinner2 = p.spinner();
|
|
288
|
+
spinner2.start("Authenticating...");
|
|
289
|
+
try {
|
|
290
|
+
const response = await api.authenticate(credentials.email, credentials.password);
|
|
291
|
+
setApiKey(response.apiKey);
|
|
292
|
+
setEmail(response.email);
|
|
293
|
+
spinner2.stop(renderSuccess("Authentication successful!"));
|
|
294
|
+
p.note(
|
|
295
|
+
[
|
|
296
|
+
`Logged in as: ${chalk2.cyan(response.email)}`,
|
|
297
|
+
`Config saved to: ${chalk2.gray(getConfigPath())}`,
|
|
298
|
+
"",
|
|
299
|
+
`Run ${chalk2.cyan("citeme audit")} to analyze your project.`
|
|
300
|
+
].join("\n"),
|
|
301
|
+
"Success"
|
|
302
|
+
);
|
|
303
|
+
p.outro(chalk2.green("You are now authenticated!"));
|
|
304
|
+
} catch (error) {
|
|
305
|
+
spinner2.stop(renderError("Authentication failed"));
|
|
306
|
+
if (error instanceof ApiError) {
|
|
307
|
+
if (error.statusCode === 401) {
|
|
308
|
+
p.log.error("Invalid email or password. Please try again.");
|
|
309
|
+
} else if (error.statusCode === 404) {
|
|
310
|
+
p.log.error("No account found with this email. Please sign up at https://citeme.io");
|
|
311
|
+
} else {
|
|
312
|
+
p.log.error(error.message);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
p.log.error("An unexpected error occurred. Please check your internet connection.");
|
|
316
|
+
}
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function logoutCommand() {
|
|
321
|
+
console.log(renderHeader());
|
|
322
|
+
if (!isAuthenticated()) {
|
|
323
|
+
p.outro(chalk2.yellow("You are not currently logged in."));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const email = getEmail();
|
|
327
|
+
const confirm3 = await p.confirm({
|
|
328
|
+
message: `Are you sure you want to log out from ${chalk2.cyan(email)}?`
|
|
329
|
+
});
|
|
330
|
+
if (p.isCancel(confirm3) || !confirm3) {
|
|
331
|
+
p.outro(chalk2.gray("Logout cancelled."));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
clearConfig();
|
|
335
|
+
p.outro(chalk2.green("Successfully logged out."));
|
|
336
|
+
}
|
|
337
|
+
async function whoamiCommand() {
|
|
338
|
+
console.log(renderHeader());
|
|
339
|
+
if (!isAuthenticated()) {
|
|
340
|
+
p.log.warn("Not logged in. Run `citeme login` to authenticate.");
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const spinner2 = p.spinner();
|
|
344
|
+
spinner2.start("Validating session...");
|
|
345
|
+
try {
|
|
346
|
+
const { valid, email } = await api.validateApiKey();
|
|
347
|
+
if (valid) {
|
|
348
|
+
spinner2.stop(renderSuccess("Session valid"));
|
|
349
|
+
p.log.info(`Logged in as: ${chalk2.cyan(email || getEmail())}`);
|
|
350
|
+
p.log.info(`Config location: ${chalk2.gray(getConfigPath())}`);
|
|
351
|
+
} else {
|
|
352
|
+
spinner2.stop(renderError("Session expired"));
|
|
353
|
+
p.log.warn("Your session has expired. Please run `citeme login` again.");
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
spinner2.stop(renderError("Could not validate session"));
|
|
357
|
+
p.log.error("Failed to connect to CiteMe. Please check your internet connection.");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// src/commands/audit.ts
|
|
362
|
+
import * as p2 from "@clack/prompts";
|
|
363
|
+
import chalk3 from "chalk";
|
|
364
|
+
import ora from "ora";
|
|
365
|
+
|
|
366
|
+
// src/services/scanner.ts
|
|
367
|
+
import fg from "fast-glob";
|
|
368
|
+
import { readFile } from "fs/promises";
|
|
369
|
+
import { extname, relative } from "path";
|
|
370
|
+
var CONTENT_EXTENSIONS = [".md", ".mdx"];
|
|
371
|
+
var STRUCTURE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
372
|
+
var IGNORED_DIRS = ["node_modules", ".next", "dist", ".git", "coverage", "build", "out"];
|
|
373
|
+
async function scanDirectory(options = {}) {
|
|
374
|
+
const { cwd = process.cwd(), includeContent = false, maxFiles = 100 } = options;
|
|
375
|
+
const ignorePatterns = IGNORED_DIRS.map((dir) => `**/${dir}/**`);
|
|
376
|
+
const contentPattern = `**/*{${CONTENT_EXTENSIONS.join(",")}}`;
|
|
377
|
+
const structurePattern = `**/*{${STRUCTURE_EXTENSIONS.join(",")}}`;
|
|
378
|
+
const [contentFiles, structureFiles] = await Promise.all([
|
|
379
|
+
fg(contentPattern, { cwd, ignore: ignorePatterns, absolute: true }),
|
|
380
|
+
fg(structurePattern, { cwd, ignore: ignorePatterns, absolute: true })
|
|
381
|
+
]);
|
|
382
|
+
const allFiles = [...contentFiles, ...structureFiles];
|
|
383
|
+
const limitedFiles = allFiles.slice(0, maxFiles);
|
|
384
|
+
const skipped = allFiles.length - limitedFiles.length;
|
|
385
|
+
const filesMetadata = await Promise.all(
|
|
386
|
+
limitedFiles.map(async (filePath) => {
|
|
387
|
+
const ext = extname(filePath);
|
|
388
|
+
const isContent = CONTENT_EXTENSIONS.includes(ext);
|
|
389
|
+
const relativePath = relative(cwd, filePath);
|
|
390
|
+
const metadata = {
|
|
391
|
+
path: relativePath,
|
|
392
|
+
type: isContent ? "content" : "structure"
|
|
393
|
+
};
|
|
394
|
+
if (includeContent || isContent) {
|
|
395
|
+
const content = await readFile(filePath, "utf-8");
|
|
396
|
+
metadata.content = content;
|
|
397
|
+
if (!isContent) {
|
|
398
|
+
metadata.meta = extractMeta(content);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return metadata;
|
|
402
|
+
})
|
|
403
|
+
);
|
|
404
|
+
return {
|
|
405
|
+
files: filesMetadata,
|
|
406
|
+
contentFiles: contentFiles.length,
|
|
407
|
+
structureFiles: structureFiles.length,
|
|
408
|
+
skipped
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function extractMeta(content) {
|
|
412
|
+
const meta = {};
|
|
413
|
+
const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i) || content.match(/title:\s*["']([^"']+)["']/);
|
|
414
|
+
if (titleMatch) {
|
|
415
|
+
meta.title = titleMatch[1];
|
|
416
|
+
}
|
|
417
|
+
const descMatch = content.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i) || content.match(/<meta[^>]*content=["']([^"']+)["'][^>]*name=["']description["']/i) || content.match(/description:\s*["']([^"']+)["']/);
|
|
418
|
+
if (descMatch) {
|
|
419
|
+
meta.description = descMatch[1];
|
|
420
|
+
}
|
|
421
|
+
const h1Matches = content.match(/<h1[^>]*>([^<]+)<\/h1>/gi);
|
|
422
|
+
if (h1Matches) {
|
|
423
|
+
meta.h1 = h1Matches.map((h1) => {
|
|
424
|
+
const innerMatch = h1.match(/>([^<]+)</);
|
|
425
|
+
return innerMatch ? innerMatch[1] : "";
|
|
426
|
+
}).filter(Boolean);
|
|
427
|
+
}
|
|
428
|
+
meta.hasJsonLd = /<script[^>]*type=["']application\/ld\+json["'][^>]*>/i.test(content);
|
|
429
|
+
const imgMatches = content.match(/<img[^>]+>/gi) || [];
|
|
430
|
+
meta.imagesWithoutAlt = imgMatches.filter(
|
|
431
|
+
(img) => !img.includes("alt=") || img.includes('alt=""') || img.includes("alt=''")
|
|
432
|
+
).length;
|
|
433
|
+
return meta;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/commands/audit.ts
|
|
437
|
+
async function auditCommand(options) {
|
|
438
|
+
if (!options.json) {
|
|
439
|
+
console.log(renderHeader());
|
|
440
|
+
}
|
|
441
|
+
if (!isAuthenticated()) {
|
|
442
|
+
if (options.json) {
|
|
443
|
+
console.log(JSON.stringify({ error: "Not authenticated" }));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
p2.log.error("You must be logged in to run an audit.");
|
|
447
|
+
p2.log.info(`Run ${chalk3.cyan("citeme login")} first.`);
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
450
|
+
if (!options.json) {
|
|
451
|
+
p2.intro(chalk3.hex("#8B5CF6")("GEO Audit"));
|
|
452
|
+
}
|
|
453
|
+
const scanSpinner = ora({
|
|
454
|
+
text: "Scanning project files...",
|
|
455
|
+
color: "magenta"
|
|
456
|
+
}).start();
|
|
457
|
+
let scanResult;
|
|
458
|
+
try {
|
|
459
|
+
scanResult = await scanDirectory({
|
|
460
|
+
includeContent: true,
|
|
461
|
+
maxFiles: 100
|
|
462
|
+
});
|
|
463
|
+
scanSpinner.succeed(
|
|
464
|
+
`Found ${chalk3.cyan(scanResult.contentFiles)} content files and ${chalk3.cyan(scanResult.structureFiles)} structure files`
|
|
465
|
+
);
|
|
466
|
+
if (scanResult.skipped > 0) {
|
|
467
|
+
console.log(renderWarning(`Skipped ${scanResult.skipped} files (limit: 100)`));
|
|
468
|
+
}
|
|
469
|
+
} catch (error) {
|
|
470
|
+
scanSpinner.fail("Failed to scan project");
|
|
471
|
+
if (options.json) {
|
|
472
|
+
console.log(JSON.stringify({ error: "Scan failed" }));
|
|
473
|
+
}
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
if (scanResult.files.length === 0) {
|
|
477
|
+
if (options.json) {
|
|
478
|
+
console.log(JSON.stringify({ error: "No files found" }));
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
p2.log.warn("No content or structure files found in this directory.");
|
|
482
|
+
p2.log.info("Make sure you are in a project with .md, .mdx, .tsx, or .jsx files.");
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
const auditSpinner = ora({
|
|
486
|
+
text: "Analyzing with CiteMe AI...",
|
|
487
|
+
color: "magenta"
|
|
488
|
+
}).start();
|
|
489
|
+
try {
|
|
490
|
+
const result = await api.audit({
|
|
491
|
+
files: scanResult.files,
|
|
492
|
+
projectUrl: options.url
|
|
493
|
+
});
|
|
494
|
+
auditSpinner.stop();
|
|
495
|
+
if (options.json) {
|
|
496
|
+
console.log(JSON.stringify(result, null, 2));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
console.log("\n");
|
|
500
|
+
console.log(renderPulseMeter(result.score, getScoreLabel(result.score)));
|
|
501
|
+
const technicalScoreColor = getScoreColor(result.technicalScore);
|
|
502
|
+
console.log(
|
|
503
|
+
chalk3.gray(" Technical Score: ") + technicalScoreColor.bold(`${result.technicalScore}/100`)
|
|
504
|
+
);
|
|
505
|
+
console.log("");
|
|
506
|
+
if (result.issues.length > 0) {
|
|
507
|
+
const issueBox = renderBox(
|
|
508
|
+
`Technical Issues (${result.issues.length})`,
|
|
509
|
+
result.issues.slice(0, 10).map((issue) => {
|
|
510
|
+
const icon = issue.type === "error" ? "\u2716" : issue.type === "warning" ? "\u26A0" : "\u2139";
|
|
511
|
+
const color = issue.type === "error" ? chalk3.red : issue.type === "warning" ? chalk3.yellow : chalk3.blue;
|
|
512
|
+
return `${color(icon)} ${issue.message}`;
|
|
513
|
+
})
|
|
514
|
+
);
|
|
515
|
+
console.log(issueBox);
|
|
516
|
+
if (result.issues.length > 10) {
|
|
517
|
+
console.log(chalk3.gray(` ... and ${result.issues.length - 10} more issues
|
|
518
|
+
`));
|
|
519
|
+
}
|
|
520
|
+
} else {
|
|
521
|
+
console.log(renderSuccess("No critical technical issues found!"));
|
|
522
|
+
console.log("");
|
|
523
|
+
}
|
|
524
|
+
if (result.suggestions.length > 0) {
|
|
525
|
+
console.log(
|
|
526
|
+
chalk3.bold(` ${chalk3.hex("#8B5CF6")(result.suggestions.length)} technical suggestions available`)
|
|
527
|
+
);
|
|
528
|
+
console.log(chalk3.gray(` Run ${chalk3.cyan("citeme apply")} to review and apply them.
|
|
529
|
+
`));
|
|
530
|
+
}
|
|
531
|
+
p2.note(
|
|
532
|
+
[
|
|
533
|
+
`Audit ID: ${chalk3.gray(result.auditId)}`,
|
|
534
|
+
`Files analyzed: ${chalk3.cyan(scanResult.files.length)}`,
|
|
535
|
+
`Issues found: ${result.issues.length > 0 ? chalk3.yellow(result.issues.length) : chalk3.green("0")}`,
|
|
536
|
+
`Suggestions: ${chalk3.hex("#8B5CF6")(result.suggestions.length)}`
|
|
537
|
+
].join("\n"),
|
|
538
|
+
"Summary"
|
|
539
|
+
);
|
|
540
|
+
if (options.verbose) {
|
|
541
|
+
console.log("\n" + chalk3.bold.underline("Detailed Issues:\n"));
|
|
542
|
+
console.log(renderIssuesList(result.issues));
|
|
543
|
+
}
|
|
544
|
+
p2.outro(
|
|
545
|
+
result.score >= 60 ? chalk3.green("Audit complete!") : chalk3.yellow("Audit complete. Consider applying suggestions to improve your score.")
|
|
546
|
+
);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
auditSpinner.fail("Audit failed");
|
|
549
|
+
if (error instanceof ApiError) {
|
|
550
|
+
if (options.json) {
|
|
551
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
552
|
+
} else {
|
|
553
|
+
p2.log.error(error.message);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
if (options.json) {
|
|
557
|
+
console.log(JSON.stringify({ error: "Unknown error" }));
|
|
558
|
+
} else {
|
|
559
|
+
p2.log.error("An unexpected error occurred during the audit.");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/commands/apply.ts
|
|
567
|
+
import * as p3 from "@clack/prompts";
|
|
568
|
+
import chalk5 from "chalk";
|
|
569
|
+
import ora2 from "ora";
|
|
570
|
+
|
|
571
|
+
// src/services/patcher.ts
|
|
572
|
+
import { readFile as readFile2, writeFile, copyFile, unlink, access } from "fs/promises";
|
|
573
|
+
import { constants } from "fs";
|
|
574
|
+
import { resolve } from "path";
|
|
575
|
+
import { createPatch, structuredPatch } from "diff";
|
|
576
|
+
import chalk4 from "chalk";
|
|
577
|
+
async function fileExists(filePath) {
|
|
578
|
+
try {
|
|
579
|
+
await access(filePath, constants.F_OK);
|
|
580
|
+
return true;
|
|
581
|
+
} catch {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
async function createBackup(filePath) {
|
|
586
|
+
const absolutePath = resolve(process.cwd(), filePath);
|
|
587
|
+
const backupPath = `${absolutePath}.bak`;
|
|
588
|
+
let finalBackupPath = backupPath;
|
|
589
|
+
if (await fileExists(backupPath)) {
|
|
590
|
+
finalBackupPath = `${absolutePath}.${Date.now()}.bak`;
|
|
591
|
+
}
|
|
592
|
+
await copyFile(absolutePath, finalBackupPath);
|
|
593
|
+
return finalBackupPath;
|
|
594
|
+
}
|
|
595
|
+
async function restoreBackup(backupPath, originalPath) {
|
|
596
|
+
await copyFile(backupPath, originalPath);
|
|
597
|
+
await unlink(backupPath);
|
|
598
|
+
}
|
|
599
|
+
async function removeBackup(backupPath) {
|
|
600
|
+
try {
|
|
601
|
+
await unlink(backupPath);
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function applyPatch(filePath, original, patched) {
|
|
606
|
+
const absolutePath = resolve(process.cwd(), filePath);
|
|
607
|
+
try {
|
|
608
|
+
if (!await fileExists(absolutePath)) {
|
|
609
|
+
return {
|
|
610
|
+
success: false,
|
|
611
|
+
error: `File not found: ${filePath}`
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const currentContent = await readFile2(absolutePath, "utf-8");
|
|
615
|
+
const normalizedCurrent = normalizeWhitespace(currentContent);
|
|
616
|
+
const normalizedOriginal = normalizeWhitespace(original);
|
|
617
|
+
if (!normalizedCurrent.includes(normalizedOriginal)) {
|
|
618
|
+
return {
|
|
619
|
+
success: false,
|
|
620
|
+
error: "File content has changed since the patch was generated. Please run a new audit."
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const backupPath = await createBackup(absolutePath);
|
|
624
|
+
try {
|
|
625
|
+
const newContent = currentContent.replace(original, patched);
|
|
626
|
+
await writeFile(absolutePath, newContent, "utf-8");
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
backupPath
|
|
630
|
+
};
|
|
631
|
+
} catch (writeError) {
|
|
632
|
+
await restoreBackup(backupPath, absolutePath);
|
|
633
|
+
return {
|
|
634
|
+
success: false,
|
|
635
|
+
error: `Failed to write file: ${writeError.message}`
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
} catch (error) {
|
|
639
|
+
return {
|
|
640
|
+
success: false,
|
|
641
|
+
error: `Error applying patch: ${error.message}`
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function normalizeWhitespace(str) {
|
|
646
|
+
return str.replace(/\s+/g, " ").trim();
|
|
647
|
+
}
|
|
648
|
+
function generateDiff(original, patched, filePath) {
|
|
649
|
+
const patch = structuredPatch(filePath, filePath, original, patched, "", "");
|
|
650
|
+
const lines = [];
|
|
651
|
+
lines.push(chalk4.bold(`--- ${filePath}`));
|
|
652
|
+
lines.push(chalk4.bold(`+++ ${filePath}`));
|
|
653
|
+
for (const hunk of patch.hunks) {
|
|
654
|
+
lines.push(chalk4.cyan(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`));
|
|
655
|
+
for (const line of hunk.lines) {
|
|
656
|
+
if (line.startsWith("+")) {
|
|
657
|
+
lines.push(chalk4.green(line));
|
|
658
|
+
} else if (line.startsWith("-")) {
|
|
659
|
+
lines.push(chalk4.red(line));
|
|
660
|
+
} else {
|
|
661
|
+
lines.push(chalk4.gray(line));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return lines.join("\n");
|
|
666
|
+
}
|
|
667
|
+
function getDiffStats(original, patched) {
|
|
668
|
+
const patch = structuredPatch("file", "file", original, patched, "", "");
|
|
669
|
+
let additions = 0;
|
|
670
|
+
let deletions = 0;
|
|
671
|
+
for (const hunk of patch.hunks) {
|
|
672
|
+
for (const line of hunk.lines) {
|
|
673
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
674
|
+
additions++;
|
|
675
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
676
|
+
deletions++;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { additions, deletions };
|
|
681
|
+
}
|
|
682
|
+
function formatDiffStats(additions, deletions) {
|
|
683
|
+
const add = additions > 0 ? chalk4.green(`+${additions}`) : chalk4.gray("+0");
|
|
684
|
+
const del = deletions > 0 ? chalk4.red(`-${deletions}`) : chalk4.gray("-0");
|
|
685
|
+
return `${add} ${del}`;
|
|
686
|
+
}
|
|
687
|
+
async function previewPatch(filePath, original, patched) {
|
|
688
|
+
const header = [
|
|
689
|
+
"",
|
|
690
|
+
chalk4.bold.underline(`File: ${filePath}`),
|
|
691
|
+
""
|
|
692
|
+
].join("\n");
|
|
693
|
+
const diff = generateDiff(original, patched, filePath);
|
|
694
|
+
const stats = getDiffStats(original, patched);
|
|
695
|
+
const statsLine = `
|
|
696
|
+
${formatDiffStats(stats.additions, stats.deletions)} changes
|
|
697
|
+
`;
|
|
698
|
+
return header + diff + statsLine;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/commands/apply.ts
|
|
702
|
+
async function applyCommand(options) {
|
|
703
|
+
console.log(renderHeader());
|
|
704
|
+
if (!isAuthenticated()) {
|
|
705
|
+
p3.log.error("You must be logged in to apply patches.");
|
|
706
|
+
p3.log.info(`Run ${chalk5.cyan("citeme login")} first.`);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
p3.intro(chalk5.hex("#8B5CF6")("Apply Technical Suggestions"));
|
|
710
|
+
const fetchSpinner = ora2({
|
|
711
|
+
text: "Fetching approved technical patches...",
|
|
712
|
+
color: "magenta"
|
|
713
|
+
}).start();
|
|
714
|
+
let patches;
|
|
715
|
+
try {
|
|
716
|
+
patches = await api.getApprovedPatches(options.auditId);
|
|
717
|
+
fetchSpinner.stop();
|
|
718
|
+
if (patches.length === 0) {
|
|
719
|
+
console.log(renderInfo("No technical patches available to apply."));
|
|
720
|
+
p3.log.info("Run `citeme audit` first to generate suggestions.");
|
|
721
|
+
p3.outro(chalk5.gray("Nothing to apply."));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
console.log(renderSuccess(`Found ${patches.length} technical patches`));
|
|
725
|
+
} catch (error) {
|
|
726
|
+
fetchSpinner.fail("Failed to fetch patches");
|
|
727
|
+
if (error instanceof ApiError) {
|
|
728
|
+
p3.log.error(error.message);
|
|
729
|
+
} else {
|
|
730
|
+
p3.log.error("An unexpected error occurred.");
|
|
731
|
+
}
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
if (options.dryRun) {
|
|
735
|
+
console.log(renderWarning("Dry run mode - no changes will be made\n"));
|
|
736
|
+
for (const patch of patches) {
|
|
737
|
+
const preview = await previewPatch(patch.file, patch.original, patch.patched);
|
|
738
|
+
console.log(preview);
|
|
739
|
+
console.log(chalk5.gray(`Description: ${patch.description}`));
|
|
740
|
+
console.log("");
|
|
741
|
+
}
|
|
742
|
+
p3.outro(chalk5.green(`Dry run complete. ${patches.length} patches would be applied.`));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (options.all) {
|
|
746
|
+
const result2 = await applyAllPatches(patches);
|
|
747
|
+
displayResults(result2);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const result = await interactiveApply(patches);
|
|
751
|
+
displayResults(result);
|
|
752
|
+
}
|
|
753
|
+
async function interactiveApply(patches) {
|
|
754
|
+
const result = {
|
|
755
|
+
applied: 0,
|
|
756
|
+
skipped: 0,
|
|
757
|
+
failed: 0,
|
|
758
|
+
backups: /* @__PURE__ */ new Map()
|
|
759
|
+
};
|
|
760
|
+
for (let i = 0; i < patches.length; i++) {
|
|
761
|
+
const patch = patches[i];
|
|
762
|
+
const stats = getDiffStats(patch.original, patch.patched);
|
|
763
|
+
console.log("\n" + chalk5.bold(`[${i + 1}/${patches.length}] ${patch.file}`));
|
|
764
|
+
console.log(chalk5.gray(patch.description));
|
|
765
|
+
console.log(formatDiffStats(stats.additions, stats.deletions));
|
|
766
|
+
const action = await p3.select({
|
|
767
|
+
message: "What would you like to do?",
|
|
768
|
+
options: [
|
|
769
|
+
{ value: "apply", label: "Apply this change", hint: "y" },
|
|
770
|
+
{ value: "skip", label: "Skip this change", hint: "n" },
|
|
771
|
+
{ value: "diff", label: "Show full diff", hint: "d" },
|
|
772
|
+
{ value: "quit", label: "Quit and rollback all changes", hint: "q" }
|
|
773
|
+
]
|
|
774
|
+
});
|
|
775
|
+
if (p3.isCancel(action) || action === "quit") {
|
|
776
|
+
console.log(renderWarning("Aborting..."));
|
|
777
|
+
if (result.backups.size > 0) {
|
|
778
|
+
console.log(chalk5.yellow("Rolling back applied changes..."));
|
|
779
|
+
for (const [file, backupPath] of result.backups) {
|
|
780
|
+
await restoreBackup(backupPath, file);
|
|
781
|
+
console.log(chalk5.gray(` Restored: ${file}`));
|
|
782
|
+
}
|
|
783
|
+
result.applied = 0;
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
if (action === "diff") {
|
|
788
|
+
const preview = await previewPatch(patch.file, patch.original, patch.patched);
|
|
789
|
+
console.log(preview);
|
|
790
|
+
const confirmAfterDiff = await p3.confirm({
|
|
791
|
+
message: "Apply this change?"
|
|
792
|
+
});
|
|
793
|
+
if (p3.isCancel(confirmAfterDiff) || !confirmAfterDiff) {
|
|
794
|
+
result.skipped++;
|
|
795
|
+
console.log(chalk5.gray("Skipped"));
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
} else if (action === "skip") {
|
|
799
|
+
result.skipped++;
|
|
800
|
+
console.log(chalk5.gray("Skipped"));
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const applySpinner = ora2({
|
|
804
|
+
text: "Applying patch...",
|
|
805
|
+
color: "magenta"
|
|
806
|
+
}).start();
|
|
807
|
+
const patchResult = await applyPatch(patch.file, patch.original, patch.patched);
|
|
808
|
+
if (patchResult.success) {
|
|
809
|
+
applySpinner.succeed(chalk5.green("Applied"));
|
|
810
|
+
result.applied++;
|
|
811
|
+
if (patchResult.backupPath) {
|
|
812
|
+
result.backups.set(patch.file, patchResult.backupPath);
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
await api.confirmPatchApplied(patch.id);
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
applySpinner.fail(chalk5.red("Failed"));
|
|
820
|
+
console.log(renderError(patchResult.error || "Unknown error"));
|
|
821
|
+
result.failed++;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (result.applied > 0 && result.failed === 0) {
|
|
825
|
+
const cleanup = await p3.confirm({
|
|
826
|
+
message: `Remove ${result.backups.size} backup files?`,
|
|
827
|
+
initialValue: true
|
|
828
|
+
});
|
|
829
|
+
if (cleanup && !p3.isCancel(cleanup)) {
|
|
830
|
+
for (const backupPath of result.backups.values()) {
|
|
831
|
+
await removeBackup(backupPath);
|
|
832
|
+
}
|
|
833
|
+
console.log(chalk5.gray("Backup files removed."));
|
|
834
|
+
} else {
|
|
835
|
+
console.log(chalk5.gray("Backup files kept. They will be named *.bak"));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return result;
|
|
839
|
+
}
|
|
840
|
+
async function applyAllPatches(patches) {
|
|
841
|
+
const result = {
|
|
842
|
+
applied: 0,
|
|
843
|
+
skipped: 0,
|
|
844
|
+
failed: 0,
|
|
845
|
+
backups: /* @__PURE__ */ new Map()
|
|
846
|
+
};
|
|
847
|
+
console.log(renderWarning("Applying all patches without confirmation...\n"));
|
|
848
|
+
for (const patch of patches) {
|
|
849
|
+
const spinner2 = ora2({
|
|
850
|
+
text: `Applying: ${patch.file}`,
|
|
851
|
+
color: "magenta"
|
|
852
|
+
}).start();
|
|
853
|
+
const patchResult = await applyPatch(patch.file, patch.original, patch.patched);
|
|
854
|
+
if (patchResult.success) {
|
|
855
|
+
spinner2.succeed(chalk5.green(patch.file));
|
|
856
|
+
result.applied++;
|
|
857
|
+
if (patchResult.backupPath) {
|
|
858
|
+
result.backups.set(patch.file, patchResult.backupPath);
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
await api.confirmPatchApplied(patch.id);
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
spinner2.fail(chalk5.red(`${patch.file}: ${patchResult.error}`));
|
|
866
|
+
result.failed++;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
871
|
+
function displayResults(result) {
|
|
872
|
+
console.log("\n");
|
|
873
|
+
const lines = [
|
|
874
|
+
`${chalk5.green("Applied")}: ${result.applied}`,
|
|
875
|
+
`${chalk5.gray("Skipped")}: ${result.skipped}`,
|
|
876
|
+
`${chalk5.red("Failed")}: ${result.failed}`
|
|
877
|
+
];
|
|
878
|
+
if (result.backups.size > 0) {
|
|
879
|
+
lines.push("");
|
|
880
|
+
lines.push(`Backup files: ${result.backups.size}`);
|
|
881
|
+
}
|
|
882
|
+
p3.note(lines.join("\n"), "Results");
|
|
883
|
+
if (result.failed > 0) {
|
|
884
|
+
p3.outro(chalk5.yellow("Some patches failed to apply. Check the errors above."));
|
|
885
|
+
} else if (result.applied > 0) {
|
|
886
|
+
p3.outro(chalk5.green("All patches applied successfully!"));
|
|
887
|
+
} else {
|
|
888
|
+
p3.outro(chalk5.gray("No patches were applied."));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/commands/suggestions.ts
|
|
893
|
+
import * as p4 from "@clack/prompts";
|
|
894
|
+
import chalk6 from "chalk";
|
|
895
|
+
import ora3 from "ora";
|
|
896
|
+
async function suggestionsCommand() {
|
|
897
|
+
console.log(renderHeader());
|
|
898
|
+
console.log(`
|
|
899
|
+
${chalk6.bold("Suggestions Commands:")}
|
|
900
|
+
|
|
901
|
+
${chalk6.cyan("citeme suggestions generate")} Generate new suggestions from audit
|
|
902
|
+
${chalk6.cyan("citeme suggestions list")} List all suggestions
|
|
903
|
+
${chalk6.cyan("citeme suggestions view <id>")} View a specific suggestion
|
|
904
|
+
|
|
905
|
+
Run ${chalk6.gray("citeme suggestions <command> --help")} for more info.
|
|
906
|
+
`);
|
|
907
|
+
}
|
|
908
|
+
async function generateSuggestionsCommand(options) {
|
|
909
|
+
console.log(renderHeader());
|
|
910
|
+
if (!isAuthenticated()) {
|
|
911
|
+
p4.log.error("You must be logged in to generate suggestions.");
|
|
912
|
+
p4.log.info(`Run ${chalk6.cyan("citeme login")} first.`);
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
p4.intro(chalk6.hex("#8B5CF6")("Generate Suggestions"));
|
|
916
|
+
let auditId = options.auditId;
|
|
917
|
+
if (!auditId) {
|
|
918
|
+
const spinner2 = ora3({
|
|
919
|
+
text: "Fetching latest audit...",
|
|
920
|
+
color: "magenta"
|
|
921
|
+
}).start();
|
|
922
|
+
try {
|
|
923
|
+
const latestAudit = await api.getLatestAudit();
|
|
924
|
+
if (!latestAudit) {
|
|
925
|
+
spinner2.fail("No audit found");
|
|
926
|
+
p4.log.warn("No audit found. Run `citeme audit` first.");
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
auditId = latestAudit.id;
|
|
930
|
+
spinner2.succeed(`Using audit ${chalk6.gray(auditId.slice(0, 8))}... (score: ${latestAudit.score})`);
|
|
931
|
+
} catch (error) {
|
|
932
|
+
spinner2.fail("Failed to fetch audit");
|
|
933
|
+
if (error instanceof ApiError) {
|
|
934
|
+
p4.log.error(error.message);
|
|
935
|
+
}
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const types = options.types ? options.types.split(",").map((t) => t.trim().toUpperCase()) : ["LINKEDIN_POST", "BLOG_ARTICLE", "WEBSITE_OPTIMIZATION"];
|
|
940
|
+
const count = options.count || 3;
|
|
941
|
+
const generateSpinner = ora3({
|
|
942
|
+
text: "Generating suggestions with AI...",
|
|
943
|
+
color: "magenta"
|
|
944
|
+
}).start();
|
|
945
|
+
try {
|
|
946
|
+
const suggestions2 = await api.generateSuggestions({
|
|
947
|
+
auditId,
|
|
948
|
+
types,
|
|
949
|
+
count
|
|
950
|
+
});
|
|
951
|
+
generateSpinner.succeed(`Generated ${chalk6.cyan(suggestions2.length)} suggestions`);
|
|
952
|
+
console.log("");
|
|
953
|
+
for (const suggestion of suggestions2) {
|
|
954
|
+
const typeColor = getTypeColor(suggestion.type);
|
|
955
|
+
const statusIcon = getStatusIcon(suggestion.status);
|
|
956
|
+
console.log(
|
|
957
|
+
` ${statusIcon} ${typeColor(suggestion.type.padEnd(20))} ${chalk6.bold(suggestion.title.slice(0, 50))}${suggestion.title.length > 50 ? "..." : ""}`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
console.log("");
|
|
961
|
+
p4.note(
|
|
962
|
+
[
|
|
963
|
+
`Total generated: ${chalk6.cyan(suggestions2.length)}`,
|
|
964
|
+
"",
|
|
965
|
+
`View details: ${chalk6.gray("citeme suggestions view <id>")}`,
|
|
966
|
+
`List all: ${chalk6.gray("citeme suggestions list")}`
|
|
967
|
+
].join("\n"),
|
|
968
|
+
"Summary"
|
|
969
|
+
);
|
|
970
|
+
p4.outro(chalk6.green("Suggestions generated successfully!"));
|
|
971
|
+
} catch (error) {
|
|
972
|
+
generateSpinner.fail("Failed to generate suggestions");
|
|
973
|
+
if (error instanceof ApiError) {
|
|
974
|
+
p4.log.error(error.message);
|
|
975
|
+
} else {
|
|
976
|
+
p4.log.error("An unexpected error occurred.");
|
|
977
|
+
}
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async function listSuggestionsCommand(options) {
|
|
982
|
+
if (!options.json) {
|
|
983
|
+
console.log(renderHeader());
|
|
984
|
+
}
|
|
985
|
+
if (!isAuthenticated()) {
|
|
986
|
+
if (options.json) {
|
|
987
|
+
console.log(JSON.stringify({ error: "Not authenticated" }));
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
p4.log.error("You must be logged in to list suggestions.");
|
|
991
|
+
p4.log.info(`Run ${chalk6.cyan("citeme login")} first.`);
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
if (!options.json) {
|
|
995
|
+
p4.intro(chalk6.hex("#8B5CF6")("Suggestions"));
|
|
996
|
+
}
|
|
997
|
+
const spinner2 = ora3({
|
|
998
|
+
text: "Fetching suggestions...",
|
|
999
|
+
color: "magenta"
|
|
1000
|
+
}).start();
|
|
1001
|
+
try {
|
|
1002
|
+
const suggestions2 = await api.listSuggestions({
|
|
1003
|
+
status: options.status,
|
|
1004
|
+
type: options.type,
|
|
1005
|
+
limit: options.limit || 20
|
|
1006
|
+
});
|
|
1007
|
+
spinner2.stop();
|
|
1008
|
+
if (options.json) {
|
|
1009
|
+
console.log(JSON.stringify(suggestions2, null, 2));
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (suggestions2.length === 0) {
|
|
1013
|
+
console.log(renderWarning("No suggestions found."));
|
|
1014
|
+
p4.log.info("Run `citeme suggestions generate` to create new suggestions.");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
console.log("");
|
|
1018
|
+
const grouped = suggestions2.reduce((acc, s) => {
|
|
1019
|
+
if (!acc[s.type]) acc[s.type] = [];
|
|
1020
|
+
acc[s.type].push(s);
|
|
1021
|
+
return acc;
|
|
1022
|
+
}, {});
|
|
1023
|
+
for (const [type, typeSuggestions] of Object.entries(grouped)) {
|
|
1024
|
+
const typeColor = getTypeColor(type);
|
|
1025
|
+
console.log(chalk6.bold(` ${typeColor(type)} (${typeSuggestions.length})`));
|
|
1026
|
+
console.log("");
|
|
1027
|
+
for (const suggestion of typeSuggestions) {
|
|
1028
|
+
const statusIcon = getStatusIcon(suggestion.status);
|
|
1029
|
+
const id = chalk6.gray(`[${suggestion.id.slice(0, 8)}]`);
|
|
1030
|
+
console.log(` ${statusIcon} ${id} ${suggestion.title.slice(0, 60)}${suggestion.title.length > 60 ? "..." : ""}`);
|
|
1031
|
+
if (suggestion.reasoning) {
|
|
1032
|
+
console.log(chalk6.gray(` ${suggestion.reasoning.slice(0, 70)}...`));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
console.log("");
|
|
1036
|
+
}
|
|
1037
|
+
const stats = {
|
|
1038
|
+
total: suggestions2.length,
|
|
1039
|
+
pending: suggestions2.filter((s) => s.status === "PENDING").length,
|
|
1040
|
+
approved: suggestions2.filter((s) => s.status === "APPROVED").length,
|
|
1041
|
+
published: suggestions2.filter((s) => s.status === "PUBLISHED").length
|
|
1042
|
+
};
|
|
1043
|
+
p4.note(
|
|
1044
|
+
[
|
|
1045
|
+
`Total: ${chalk6.cyan(stats.total)}`,
|
|
1046
|
+
`Pending: ${chalk6.yellow(stats.pending)}`,
|
|
1047
|
+
`Approved: ${chalk6.blue(stats.approved)}`,
|
|
1048
|
+
`Published: ${chalk6.green(stats.published)}`
|
|
1049
|
+
].join(" | "),
|
|
1050
|
+
"Statistics"
|
|
1051
|
+
);
|
|
1052
|
+
p4.outro(chalk6.gray("Use `citeme suggestions view <id>` to see full content."));
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
spinner2.fail("Failed to fetch suggestions");
|
|
1055
|
+
if (error instanceof ApiError) {
|
|
1056
|
+
if (options.json) {
|
|
1057
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
1058
|
+
} else {
|
|
1059
|
+
p4.log.error(error.message);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function viewSuggestionCommand(suggestionId) {
|
|
1066
|
+
console.log(renderHeader());
|
|
1067
|
+
if (!isAuthenticated()) {
|
|
1068
|
+
p4.log.error("You must be logged in to view suggestions.");
|
|
1069
|
+
p4.log.info(`Run ${chalk6.cyan("citeme login")} first.`);
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
if (!suggestionId) {
|
|
1073
|
+
p4.log.error("Suggestion ID is required.");
|
|
1074
|
+
p4.log.info("Usage: citeme suggestions view <id>");
|
|
1075
|
+
process.exit(1);
|
|
1076
|
+
}
|
|
1077
|
+
const spinner2 = ora3({
|
|
1078
|
+
text: "Fetching suggestion...",
|
|
1079
|
+
color: "magenta"
|
|
1080
|
+
}).start();
|
|
1081
|
+
try {
|
|
1082
|
+
const suggestion = await api.getSuggestion(suggestionId);
|
|
1083
|
+
spinner2.stop();
|
|
1084
|
+
const typeColor = getTypeColor(suggestion.type);
|
|
1085
|
+
const statusIcon = getStatusIcon(suggestion.status);
|
|
1086
|
+
console.log("");
|
|
1087
|
+
console.log(chalk6.bold.underline(` ${suggestion.title}`));
|
|
1088
|
+
console.log("");
|
|
1089
|
+
console.log(` ${typeColor(suggestion.type)} ${statusIcon} ${suggestion.status}`);
|
|
1090
|
+
console.log(chalk6.gray(` ID: ${suggestion.id}`));
|
|
1091
|
+
console.log("");
|
|
1092
|
+
console.log(chalk6.bold(" Content:"));
|
|
1093
|
+
console.log(chalk6.gray(" " + "\u2500".repeat(60)));
|
|
1094
|
+
const contentLines = suggestion.content.split("\n");
|
|
1095
|
+
for (const line of contentLines) {
|
|
1096
|
+
console.log(` ${line}`);
|
|
1097
|
+
}
|
|
1098
|
+
console.log(chalk6.gray(" " + "\u2500".repeat(60)));
|
|
1099
|
+
console.log("");
|
|
1100
|
+
if (suggestion.reasoning) {
|
|
1101
|
+
console.log(chalk6.bold(" Why this suggestion:"));
|
|
1102
|
+
console.log(chalk6.italic.gray(` ${suggestion.reasoning}`));
|
|
1103
|
+
console.log("");
|
|
1104
|
+
}
|
|
1105
|
+
p4.note(
|
|
1106
|
+
[
|
|
1107
|
+
`Copy to clipboard: ${chalk6.gray("citeme suggestions copy " + suggestionId.slice(0, 8))}`,
|
|
1108
|
+
`Approve: ${chalk6.gray("citeme suggestions approve " + suggestionId.slice(0, 8))}`
|
|
1109
|
+
].join("\n"),
|
|
1110
|
+
"Actions"
|
|
1111
|
+
);
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
spinner2.fail("Failed to fetch suggestion");
|
|
1114
|
+
if (error instanceof ApiError) {
|
|
1115
|
+
if (error.statusCode === 404) {
|
|
1116
|
+
p4.log.error("Suggestion not found. Check the ID and try again.");
|
|
1117
|
+
} else {
|
|
1118
|
+
p4.log.error(error.message);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function getTypeColor(type) {
|
|
1125
|
+
switch (type) {
|
|
1126
|
+
case "LINKEDIN_POST":
|
|
1127
|
+
return chalk6.hex("#0A66C2");
|
|
1128
|
+
// LinkedIn blue
|
|
1129
|
+
case "BLOG_ARTICLE":
|
|
1130
|
+
return chalk6.hex("#FF6B6B");
|
|
1131
|
+
// Red
|
|
1132
|
+
case "TWEET":
|
|
1133
|
+
case "TWITTER_POST":
|
|
1134
|
+
return chalk6.hex("#1DA1F2");
|
|
1135
|
+
// Twitter blue
|
|
1136
|
+
case "WEBSITE_OPTIMIZATION":
|
|
1137
|
+
case "TECHNICAL":
|
|
1138
|
+
return chalk6.hex("#8B5CF6");
|
|
1139
|
+
// Violet
|
|
1140
|
+
default:
|
|
1141
|
+
return chalk6.gray;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function getStatusIcon(status) {
|
|
1145
|
+
switch (status) {
|
|
1146
|
+
case "PENDING":
|
|
1147
|
+
return chalk6.yellow("\u25CB");
|
|
1148
|
+
case "APPROVED":
|
|
1149
|
+
return chalk6.blue("\u25C9");
|
|
1150
|
+
case "PUBLISHED":
|
|
1151
|
+
return chalk6.green("\u2714");
|
|
1152
|
+
case "REJECTED":
|
|
1153
|
+
return chalk6.red("\u2716");
|
|
1154
|
+
case "EDITED":
|
|
1155
|
+
return chalk6.cyan("\u270E");
|
|
1156
|
+
default:
|
|
1157
|
+
return chalk6.gray("\u25CB");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/index.ts
|
|
1162
|
+
import chalk7 from "chalk";
|
|
1163
|
+
var program = new Command();
|
|
1164
|
+
program.name("citeme").description("CiteMe CLI - Audit your GEO score and apply optimizations").version("0.1.0", "-v, --version", "Display CLI version").helpOption("-h, --help", "Display help for command");
|
|
1165
|
+
program.command("login").description("Authenticate with your CiteMe account").action(loginCommand);
|
|
1166
|
+
program.command("logout").description("Log out and remove stored credentials").action(logoutCommand);
|
|
1167
|
+
program.command("whoami").description("Display current user information").action(whoamiCommand);
|
|
1168
|
+
program.command("audit").description("Analyze your project for GEO optimization opportunities").option("-u, --url <url>", "Project URL for enhanced analysis").option("-v, --verbose", "Show detailed output").option("--json", "Output results as JSON").action((options) => auditCommand(options));
|
|
1169
|
+
program.command("apply").description("Apply approved technical suggestions to your code").option("-a, --audit-id <id>", "Apply patches from a specific audit").option("--all", "Apply all patches without confirmation").option("--dry-run", "Preview changes without applying them").action((options) => applyCommand(options));
|
|
1170
|
+
var suggestions = program.command("suggestions").description("Manage content suggestions").action(suggestionsCommand);
|
|
1171
|
+
suggestions.command("generate").description("Generate new suggestions based on audit results").option("-a, --audit-id <id>", "Use a specific audit").option("-t, --types <types>", "Suggestion types (comma-separated): LINKEDIN_POST,BLOG_ARTICLE,WEBSITE_OPTIMIZATION").option("-c, --count <number>", "Number of suggestions per type", "3").action((options) => generateSuggestionsCommand({
|
|
1172
|
+
...options,
|
|
1173
|
+
count: parseInt(options.count, 10)
|
|
1174
|
+
}));
|
|
1175
|
+
suggestions.command("list").description("List all suggestions").option("-s, --status <status>", "Filter by status: PENDING,APPROVED,PUBLISHED,REJECTED").option("-t, --type <type>", "Filter by type: LINKEDIN_POST,BLOG_ARTICLE,WEBSITE_OPTIMIZATION").option("-l, --limit <number>", "Maximum number of suggestions", "20").option("--json", "Output as JSON").action((options) => listSuggestionsCommand({
|
|
1176
|
+
...options,
|
|
1177
|
+
limit: parseInt(options.limit, 10)
|
|
1178
|
+
}));
|
|
1179
|
+
suggestions.command("view <id>").description("View a specific suggestion").action((id) => viewSuggestionCommand(id));
|
|
1180
|
+
program.command("status").description("Show CLI status and configuration").action(() => {
|
|
1181
|
+
console.log("\n" + chalk7.bold("CiteMe CLI Status") + "\n");
|
|
1182
|
+
if (isAuthenticated()) {
|
|
1183
|
+
console.log(chalk7.green("\u2714") + " Authenticated as: " + chalk7.cyan(getEmail()));
|
|
1184
|
+
} else {
|
|
1185
|
+
console.log(chalk7.yellow("\u25CB") + " Not authenticated");
|
|
1186
|
+
}
|
|
1187
|
+
console.log(chalk7.gray(" Config: ") + getConfigPath());
|
|
1188
|
+
console.log("");
|
|
1189
|
+
});
|
|
1190
|
+
program.command("config").description("Show configuration file path").action(() => {
|
|
1191
|
+
console.log(getConfigPath());
|
|
1192
|
+
});
|
|
1193
|
+
program.command("help [command]").description("Display help for a command").action((command) => {
|
|
1194
|
+
if (command) {
|
|
1195
|
+
const cmd = program.commands.find((c) => c.name() === command);
|
|
1196
|
+
if (cmd) {
|
|
1197
|
+
cmd.outputHelp();
|
|
1198
|
+
} else {
|
|
1199
|
+
console.error(chalk7.red(`Unknown command: ${command}`));
|
|
1200
|
+
console.log(`Run ${chalk7.cyan("citeme help")} for available commands.`);
|
|
1201
|
+
}
|
|
1202
|
+
} else {
|
|
1203
|
+
program.outputHelp();
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
program.command("version").description("Display CLI version").action(() => {
|
|
1207
|
+
console.log(`${chalk7.hex("#8B5CF6").bold("CiteMe CLI")} v${program.version()}`);
|
|
1208
|
+
});
|
|
1209
|
+
program.on("command:*", () => {
|
|
1210
|
+
console.error(chalk7.red(`Unknown command: ${program.args.join(" ")}`));
|
|
1211
|
+
console.log(`Run ${chalk7.cyan("citeme --help")} for usage information.`);
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
});
|
|
1214
|
+
program.parse(process.argv);
|
|
1215
|
+
if (!process.argv.slice(2).length) {
|
|
1216
|
+
console.log(`
|
|
1217
|
+
${chalk7.hex("#8B5CF6").bold("CiteMe CLI")} - GEO Optimization Tool
|
|
1218
|
+
|
|
1219
|
+
${chalk7.bold("Quick Start:")}
|
|
1220
|
+
${chalk7.cyan("citeme login")} Authenticate with your account
|
|
1221
|
+
${chalk7.cyan("citeme audit")} Analyze your project
|
|
1222
|
+
${chalk7.cyan("citeme suggestions generate")} Generate content suggestions
|
|
1223
|
+
${chalk7.cyan("citeme apply")} Apply technical suggestions
|
|
1224
|
+
|
|
1225
|
+
${chalk7.bold("Commands:")}
|
|
1226
|
+
login Log in to your CiteMe account
|
|
1227
|
+
logout Log out and clear credentials
|
|
1228
|
+
whoami Show current user
|
|
1229
|
+
audit Run GEO audit on current directory
|
|
1230
|
+
suggestions generate Generate suggestions from audit
|
|
1231
|
+
suggestions list List all suggestions
|
|
1232
|
+
suggestions view <id> View suggestion details
|
|
1233
|
+
apply Apply approved technical patches
|
|
1234
|
+
status Show CLI status
|
|
1235
|
+
config Show config file path
|
|
1236
|
+
|
|
1237
|
+
${chalk7.gray("Run `citeme <command> --help` for more information.")}
|
|
1238
|
+
`);
|
|
1239
|
+
}
|