@siteping/cli 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +640 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/doctor.ts
|
|
7
|
+
import * as p from "@clack/prompts";
|
|
8
|
+
async function doctorCommand(options) {
|
|
9
|
+
p.intro("siteping \u2014 Diagnostic r\xE9seau");
|
|
10
|
+
const url = options.url ?? await p.text({
|
|
11
|
+
message: "URL du serveur de d\xE9veloppement",
|
|
12
|
+
placeholder: "http://localhost:3000",
|
|
13
|
+
defaultValue: "http://localhost:3000"
|
|
14
|
+
});
|
|
15
|
+
if (p.isCancel(url)) {
|
|
16
|
+
p.cancel("Annul\xE9.");
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
const endpoint = options.endpoint ?? await p.text({
|
|
20
|
+
message: "Chemin de l'endpoint API",
|
|
21
|
+
placeholder: "/api/siteping",
|
|
22
|
+
defaultValue: "/api/siteping"
|
|
23
|
+
});
|
|
24
|
+
if (p.isCancel(endpoint)) {
|
|
25
|
+
p.cancel("Annul\xE9.");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
const fullUrl = `${url}${endpoint}?projectName=__siteping_health_check__`;
|
|
29
|
+
const spinner2 = p.spinner();
|
|
30
|
+
spinner2.start(`Test de connexion \xE0 ${url}${endpoint}`);
|
|
31
|
+
try {
|
|
32
|
+
const start = performance.now();
|
|
33
|
+
const response = await fetch(fullUrl);
|
|
34
|
+
const elapsed = Math.round(performance.now() - start);
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
spinner2.stop(`Connexion r\xE9ussie (${elapsed}ms)`);
|
|
38
|
+
if (data && typeof data.total === "number") {
|
|
39
|
+
p.log.success(`API fonctionnelle \u2014 ${data.total} feedback(s) trouv\xE9(s)`);
|
|
40
|
+
} else {
|
|
41
|
+
p.log.warn("R\xE9ponse inattendue \u2014 v\xE9rifiez que l'endpoint utilise createSitepingHandler()");
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
spinner2.stop(`Erreur HTTP ${response.status} (${elapsed}ms)`);
|
|
45
|
+
const text2 = await response.text().catch(() => "");
|
|
46
|
+
p.log.error(`Le serveur a r\xE9pondu : ${response.status} ${response.statusText}`);
|
|
47
|
+
if (text2) p.log.info(text2.slice(0, 200));
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
spinner2.stop("Connexion \xE9chou\xE9e");
|
|
51
|
+
if (error instanceof TypeError && String(error).includes("fetch")) {
|
|
52
|
+
p.log.error("Impossible de se connecter \u2014 le serveur est-il lanc\xE9 ?");
|
|
53
|
+
p.log.info(`V\xE9rifiez que ${url} est accessible`);
|
|
54
|
+
} else {
|
|
55
|
+
p.log.error(`Erreur : ${error instanceof Error ? error.message : String(error)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
p.outro("Diagnostic termin\xE9");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/commands/init.ts
|
|
62
|
+
import { existsSync as existsSync3 } from "fs";
|
|
63
|
+
import { join as join2 } from "path";
|
|
64
|
+
import * as p2 from "@clack/prompts";
|
|
65
|
+
|
|
66
|
+
// src/generators/prisma.ts
|
|
67
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
68
|
+
import { getSchema, printSchema } from "@mrleebo/prisma-ast";
|
|
69
|
+
|
|
70
|
+
// ../core/src/schema.ts
|
|
71
|
+
var SITEPING_MODELS = {
|
|
72
|
+
SitepingFeedback: {
|
|
73
|
+
fields: {
|
|
74
|
+
id: { type: "String", isId: true, default: "cuid()" },
|
|
75
|
+
projectName: { type: "String" },
|
|
76
|
+
type: { type: "String" },
|
|
77
|
+
message: { type: "String" },
|
|
78
|
+
status: { type: "String", default: '"open"' },
|
|
79
|
+
url: { type: "String" },
|
|
80
|
+
viewport: { type: "String" },
|
|
81
|
+
userAgent: { type: "String" },
|
|
82
|
+
authorName: { type: "String" },
|
|
83
|
+
authorEmail: { type: "String" },
|
|
84
|
+
clientId: { type: "String", isUnique: true },
|
|
85
|
+
resolvedAt: { type: "DateTime", optional: true },
|
|
86
|
+
createdAt: { type: "DateTime", default: "now()" },
|
|
87
|
+
annotations: {
|
|
88
|
+
type: "SitepingAnnotation",
|
|
89
|
+
relation: { kind: "1-to-many", model: "SitepingAnnotation" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
SitepingAnnotation: {
|
|
94
|
+
fields: {
|
|
95
|
+
id: { type: "String", isId: true, default: "cuid()" },
|
|
96
|
+
feedbackId: { type: "String" },
|
|
97
|
+
feedback: {
|
|
98
|
+
type: "SitepingFeedback",
|
|
99
|
+
relation: {
|
|
100
|
+
kind: "many-to-1",
|
|
101
|
+
model: "SitepingFeedback",
|
|
102
|
+
fields: ["feedbackId"],
|
|
103
|
+
references: ["id"],
|
|
104
|
+
onDelete: "Cascade"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
cssSelector: { type: "String" },
|
|
108
|
+
xpath: { type: "String" },
|
|
109
|
+
textSnippet: { type: "String" },
|
|
110
|
+
elementTag: { type: "String" },
|
|
111
|
+
elementId: { type: "String", optional: true },
|
|
112
|
+
textPrefix: { type: "String" },
|
|
113
|
+
textSuffix: { type: "String" },
|
|
114
|
+
fingerprint: { type: "String" },
|
|
115
|
+
neighborText: { type: "String" },
|
|
116
|
+
xPct: { type: "Float" },
|
|
117
|
+
yPct: { type: "Float" },
|
|
118
|
+
wPct: { type: "Float" },
|
|
119
|
+
hPct: { type: "Float" },
|
|
120
|
+
scrollX: { type: "Float" },
|
|
121
|
+
scrollY: { type: "Float" },
|
|
122
|
+
viewportW: { type: "Int" },
|
|
123
|
+
viewportH: { type: "Int" },
|
|
124
|
+
devicePixelRatio: { type: "Float", default: "1" },
|
|
125
|
+
createdAt: { type: "DateTime", default: "now()" }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/generators/prisma.ts
|
|
131
|
+
var DEFAULT_SCHEMA_PATH = "prisma/schema.prisma";
|
|
132
|
+
function syncPrismaModels(schemaPath = DEFAULT_SCHEMA_PATH) {
|
|
133
|
+
if (!existsSync(schemaPath)) {
|
|
134
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
135
|
+
}
|
|
136
|
+
const source = readFileSync(schemaPath, "utf-8");
|
|
137
|
+
const schema = getSchema(source);
|
|
138
|
+
const existingModelsMap = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const item of schema.list) {
|
|
140
|
+
if (item.type === "model") {
|
|
141
|
+
existingModelsMap.set(item.name, item);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const addedModels = [];
|
|
145
|
+
const changes = [];
|
|
146
|
+
for (const [modelName, modelDef] of Object.entries(SITEPING_MODELS)) {
|
|
147
|
+
const existingModel = existingModelsMap.get(modelName);
|
|
148
|
+
if (!existingModel) {
|
|
149
|
+
const model = { type: "model", name: modelName, properties: [] };
|
|
150
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
151
|
+
model.properties.push(buildField(fieldName, fieldDef));
|
|
152
|
+
}
|
|
153
|
+
schema.list.push(model);
|
|
154
|
+
addedModels.push(modelName);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const existingFields = /* @__PURE__ */ new Map();
|
|
158
|
+
existingModel.properties.forEach((prop, idx) => {
|
|
159
|
+
if (prop.type === "field") {
|
|
160
|
+
existingFields.set(prop.name, { field: prop, index: idx });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const fieldsToAdd = [];
|
|
164
|
+
const fieldsToUpdate = [];
|
|
165
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
166
|
+
const expected = buildField(fieldName, fieldDef);
|
|
167
|
+
const existing = existingFields.get(fieldName);
|
|
168
|
+
if (!existing) {
|
|
169
|
+
fieldsToAdd.push(expected);
|
|
170
|
+
changes.push({
|
|
171
|
+
model: modelName,
|
|
172
|
+
field: fieldName,
|
|
173
|
+
action: "added",
|
|
174
|
+
detail: formatFieldSignature(fieldDef)
|
|
175
|
+
});
|
|
176
|
+
} else if (!fieldsMatch(existing.field, expected)) {
|
|
177
|
+
fieldsToUpdate.push({ index: existing.index, field: expected });
|
|
178
|
+
changes.push({
|
|
179
|
+
model: modelName,
|
|
180
|
+
field: fieldName,
|
|
181
|
+
action: "updated",
|
|
182
|
+
detail: describeChange(existing.field, expected)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const { index, field } of fieldsToUpdate) {
|
|
187
|
+
existingModel.properties[index] = field;
|
|
188
|
+
}
|
|
189
|
+
if (fieldsToAdd.length > 0) {
|
|
190
|
+
const createdAtIdx = existingModel.properties.findIndex(
|
|
191
|
+
(p5) => p5.type === "field" && p5.name === "createdAt"
|
|
192
|
+
);
|
|
193
|
+
if (createdAtIdx >= 0) {
|
|
194
|
+
existingModel.properties.splice(createdAtIdx, 0, ...fieldsToAdd);
|
|
195
|
+
} else {
|
|
196
|
+
existingModel.properties.push(...fieldsToAdd);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (addedModels.length > 0 || changes.length > 0) {
|
|
201
|
+
const output = printSchema(schema);
|
|
202
|
+
writeFileSync(schemaPath, output, "utf-8");
|
|
203
|
+
}
|
|
204
|
+
return { schemaPath, addedModels, changes };
|
|
205
|
+
}
|
|
206
|
+
function fieldsMatch(existing, expected) {
|
|
207
|
+
if (existing.fieldType !== expected.fieldType) return false;
|
|
208
|
+
if ((existing.optional ?? false) !== (expected.optional ?? false)) return false;
|
|
209
|
+
if ((existing.array ?? false) !== (expected.array ?? false)) return false;
|
|
210
|
+
const existingAttrs = (existing.attributes ?? []).map((a) => a.name).sort();
|
|
211
|
+
const expectedAttrs = (expected.attributes ?? []).map((a) => a.name).sort();
|
|
212
|
+
if (existingAttrs.length !== expectedAttrs.length) return false;
|
|
213
|
+
return existingAttrs.every((name, i) => name === expectedAttrs[i]);
|
|
214
|
+
}
|
|
215
|
+
function describeChange(existing, expected) {
|
|
216
|
+
const parts = [];
|
|
217
|
+
if (existing.fieldType !== expected.fieldType) {
|
|
218
|
+
parts.push(`${existing.fieldType} \u2192 ${expected.fieldType}`);
|
|
219
|
+
}
|
|
220
|
+
if ((existing.optional ?? false) !== (expected.optional ?? false)) {
|
|
221
|
+
parts.push(expected.optional ? "required \u2192 optional" : "optional \u2192 required");
|
|
222
|
+
}
|
|
223
|
+
const existingAttrs = new Set((existing.attributes ?? []).map((a) => a.name));
|
|
224
|
+
const expectedAttrs = new Set((expected.attributes ?? []).map((a) => a.name));
|
|
225
|
+
for (const attr of expectedAttrs) {
|
|
226
|
+
if (!existingAttrs.has(attr)) parts.push(`+@${attr}`);
|
|
227
|
+
}
|
|
228
|
+
for (const attr of existingAttrs) {
|
|
229
|
+
if (!expectedAttrs.has(attr)) parts.push(`-@${attr}`);
|
|
230
|
+
}
|
|
231
|
+
return parts.join(", ") || "attributes changed";
|
|
232
|
+
}
|
|
233
|
+
function formatFieldSignature(def) {
|
|
234
|
+
let sig = def.type;
|
|
235
|
+
if (def.optional) sig += "?";
|
|
236
|
+
return sig;
|
|
237
|
+
}
|
|
238
|
+
function buildField(name, def) {
|
|
239
|
+
const field = {
|
|
240
|
+
type: "field",
|
|
241
|
+
name,
|
|
242
|
+
fieldType: def.relation ? def.relation.model : def.type,
|
|
243
|
+
optional: def.optional ?? false,
|
|
244
|
+
array: def.relation?.kind === "1-to-many",
|
|
245
|
+
attributes: []
|
|
246
|
+
};
|
|
247
|
+
if (def.isId) {
|
|
248
|
+
field.attributes.push({ type: "attribute", name: "id", kind: "field" });
|
|
249
|
+
if (def.default) {
|
|
250
|
+
field.attributes.push({
|
|
251
|
+
type: "attribute",
|
|
252
|
+
name: "default",
|
|
253
|
+
kind: "field",
|
|
254
|
+
args: [
|
|
255
|
+
{
|
|
256
|
+
type: "attributeArgument",
|
|
257
|
+
value: { type: "function", name: def.default.replace("()", ""), params: [] }
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
} else if (def.default && !def.relation) {
|
|
263
|
+
const isFunction = def.default.endsWith("()");
|
|
264
|
+
field.attributes.push({
|
|
265
|
+
type: "attribute",
|
|
266
|
+
name: "default",
|
|
267
|
+
kind: "field",
|
|
268
|
+
args: [
|
|
269
|
+
{
|
|
270
|
+
type: "attributeArgument",
|
|
271
|
+
value: isFunction ? { type: "function", name: def.default.replace("()", ""), params: [] } : def.default
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (def.isUnique) {
|
|
277
|
+
field.attributes.push({ type: "attribute", name: "unique", kind: "field" });
|
|
278
|
+
}
|
|
279
|
+
if (def.relation?.kind === "many-to-1") {
|
|
280
|
+
const args = [];
|
|
281
|
+
if (def.relation.fields) {
|
|
282
|
+
args.push({
|
|
283
|
+
type: "attributeArgument",
|
|
284
|
+
value: { type: "keyValue", key: "fields", value: { type: "array", args: def.relation.fields } }
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (def.relation.references) {
|
|
288
|
+
args.push({
|
|
289
|
+
type: "attributeArgument",
|
|
290
|
+
value: { type: "keyValue", key: "references", value: { type: "array", args: def.relation.references } }
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (def.relation.onDelete) {
|
|
294
|
+
args.push({
|
|
295
|
+
type: "attributeArgument",
|
|
296
|
+
value: { type: "keyValue", key: "onDelete", value: def.relation.onDelete }
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
field.attributes.push({
|
|
300
|
+
type: "attribute",
|
|
301
|
+
name: "relation",
|
|
302
|
+
kind: "field",
|
|
303
|
+
args
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return field;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/generators/route.ts
|
|
310
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
311
|
+
import { dirname, join } from "path";
|
|
312
|
+
var ROUTE_TEMPLATE = `import { createSitepingHandler } from "@siteping/adapter-prisma";
|
|
313
|
+
import { prisma } from "@/lib/prisma";
|
|
314
|
+
|
|
315
|
+
export const { GET, POST, PATCH, DELETE } = createSitepingHandler({ prisma });
|
|
316
|
+
`;
|
|
317
|
+
function generateRoute(basePath = process.cwd()) {
|
|
318
|
+
const appDir = existsSync2(join(basePath, "src", "app")) ? join(basePath, "src", "app") : join(basePath, "app");
|
|
319
|
+
if (!existsSync2(appDir)) {
|
|
320
|
+
throw new Error(`Could not find app/ directory. Are you in a Next.js App Router project?`);
|
|
321
|
+
}
|
|
322
|
+
const routePath = join(appDir, "api", "siteping", "route.ts");
|
|
323
|
+
if (existsSync2(routePath)) {
|
|
324
|
+
return { created: false, path: routePath };
|
|
325
|
+
}
|
|
326
|
+
mkdirSync(dirname(routePath), { recursive: true });
|
|
327
|
+
writeFileSync2(routePath, ROUTE_TEMPLATE, "utf-8");
|
|
328
|
+
return { created: true, path: routePath };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/commands/init.ts
|
|
332
|
+
async function initCommand() {
|
|
333
|
+
p2.intro("siteping \u2014 Configuration");
|
|
334
|
+
const cwd = process.cwd();
|
|
335
|
+
const schemaPath = findPrismaSchema(cwd);
|
|
336
|
+
if (schemaPath) {
|
|
337
|
+
p2.log.info(`Schema Prisma trouv\xE9 : ${schemaPath}`);
|
|
338
|
+
const shouldSync = await p2.confirm({
|
|
339
|
+
message: "Synchroniser les mod\xE8les Siteping dans le schema Prisma ?"
|
|
340
|
+
});
|
|
341
|
+
if (p2.isCancel(shouldSync)) {
|
|
342
|
+
p2.cancel("Annul\xE9.");
|
|
343
|
+
process.exit(0);
|
|
344
|
+
}
|
|
345
|
+
if (shouldSync) {
|
|
346
|
+
try {
|
|
347
|
+
const { addedModels, changes } = syncPrismaModels(schemaPath);
|
|
348
|
+
if (addedModels.length > 0) {
|
|
349
|
+
p2.log.success(`Mod\xE8les cr\xE9\xE9s : ${addedModels.join(", ")}`);
|
|
350
|
+
}
|
|
351
|
+
for (const change of changes) {
|
|
352
|
+
if (change.action === "added") {
|
|
353
|
+
p2.log.success(`${change.model}.${change.field} \u2014 ajout\xE9 (${change.detail})`);
|
|
354
|
+
} else {
|
|
355
|
+
p2.log.success(`${change.model}.${change.field} \u2014 mis \xE0 jour (${change.detail})`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (addedModels.length === 0 && changes.length === 0) {
|
|
359
|
+
p2.log.info("Le schema est d\xE9j\xE0 \xE0 jour.");
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
p2.log.error(`Erreur : ${error instanceof Error ? error.message : String(error)}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
p2.log.warn("Aucun fichier schema.prisma trouv\xE9. Vous devrez ajouter les mod\xE8les manuellement.");
|
|
367
|
+
p2.log.info("Consultez la documentation : https://github.com/neosianexus/siteping#prisma-schema");
|
|
368
|
+
}
|
|
369
|
+
const shouldRoute = await p2.confirm({
|
|
370
|
+
message: "G\xE9n\xE9rer la route API Next.js App Router ?"
|
|
371
|
+
});
|
|
372
|
+
if (p2.isCancel(shouldRoute)) {
|
|
373
|
+
p2.cancel("Annul\xE9.");
|
|
374
|
+
process.exit(0);
|
|
375
|
+
}
|
|
376
|
+
if (shouldRoute) {
|
|
377
|
+
try {
|
|
378
|
+
const { created, path } = generateRoute(cwd);
|
|
379
|
+
if (created) {
|
|
380
|
+
p2.log.success(`Route cr\xE9\xE9e : ${path}`);
|
|
381
|
+
} else {
|
|
382
|
+
p2.log.info(`La route existe d\xE9j\xE0 : ${path}`);
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
p2.log.error(`Erreur : ${error instanceof Error ? error.message : String(error)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
p2.note(
|
|
389
|
+
[
|
|
390
|
+
"1. Ex\xE9cutez : npx prisma db push",
|
|
391
|
+
"2. Ajoutez le widget dans votre layout :",
|
|
392
|
+
"",
|
|
393
|
+
' import { initSiteping } from "@siteping/widget"',
|
|
394
|
+
"",
|
|
395
|
+
" initSiteping({",
|
|
396
|
+
' endpoint: "/api/siteping",',
|
|
397
|
+
' projectName: "mon-projet",',
|
|
398
|
+
" })"
|
|
399
|
+
].join("\n"),
|
|
400
|
+
"Prochaines \xE9tapes"
|
|
401
|
+
);
|
|
402
|
+
p2.outro("Configuration termin\xE9e !");
|
|
403
|
+
}
|
|
404
|
+
function findPrismaSchema(cwd) {
|
|
405
|
+
const candidates = [
|
|
406
|
+
join2(cwd, "prisma", "schema.prisma"),
|
|
407
|
+
join2(cwd, "schema.prisma"),
|
|
408
|
+
join2(cwd, "prisma", "schema", "schema.prisma")
|
|
409
|
+
];
|
|
410
|
+
return candidates.find((p5) => existsSync3(p5)) ?? null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/commands/status.ts
|
|
414
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
415
|
+
import { join as join3, relative } from "path";
|
|
416
|
+
import * as p3 from "@clack/prompts";
|
|
417
|
+
import { getSchema as getSchema2 } from "@mrleebo/prisma-ast";
|
|
418
|
+
function findPrismaSchema2(cwd) {
|
|
419
|
+
const candidates = [
|
|
420
|
+
join3(cwd, "prisma", "schema.prisma"),
|
|
421
|
+
join3(cwd, "schema.prisma"),
|
|
422
|
+
join3(cwd, "prisma", "schema", "schema.prisma")
|
|
423
|
+
];
|
|
424
|
+
return candidates.find((c) => existsSync4(c)) ?? null;
|
|
425
|
+
}
|
|
426
|
+
function findApiRoute(cwd) {
|
|
427
|
+
const candidates = [
|
|
428
|
+
join3(cwd, "app", "api", "siteping", "route.ts"),
|
|
429
|
+
join3(cwd, "src", "app", "api", "siteping", "route.ts")
|
|
430
|
+
];
|
|
431
|
+
return candidates.find((c) => existsSync4(c)) ?? null;
|
|
432
|
+
}
|
|
433
|
+
function readPackageJson(cwd) {
|
|
434
|
+
const pkgPath = join3(cwd, "package.json");
|
|
435
|
+
if (!existsSync4(pkgPath)) return null;
|
|
436
|
+
try {
|
|
437
|
+
return JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function findWidgetUsage(cwd) {
|
|
443
|
+
const searchDirs = [join3(cwd, "src"), join3(cwd, "app"), join3(cwd, "pages")];
|
|
444
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
445
|
+
const patterns = ["initSiteping", "@siteping/widget"];
|
|
446
|
+
for (const dir of searchDirs) {
|
|
447
|
+
if (!existsSync4(dir)) continue;
|
|
448
|
+
const match = searchInDir(dir, extensions, patterns);
|
|
449
|
+
if (match) return match;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function searchInDir(dir, extensions, patterns) {
|
|
454
|
+
let entries;
|
|
455
|
+
try {
|
|
456
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
const name = entry.name;
|
|
462
|
+
const fullPath = join3(dir, name);
|
|
463
|
+
if (entry.isDirectory()) {
|
|
464
|
+
if (name === "node_modules" || name === ".next") continue;
|
|
465
|
+
const match = searchInDir(fullPath, extensions, patterns);
|
|
466
|
+
if (match) return match;
|
|
467
|
+
} else if (extensions.some((ext) => name.endsWith(ext))) {
|
|
468
|
+
try {
|
|
469
|
+
const content = readFileSync2(fullPath, "utf-8");
|
|
470
|
+
if (patterns.some((pat) => content.includes(pat))) return fullPath;
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
function checkSchema(schemaPath) {
|
|
478
|
+
if (!schemaPath || !existsSync4(schemaPath)) {
|
|
479
|
+
return { found: false, path: null, missingModels: [], missingFields: [], outdatedFields: [] };
|
|
480
|
+
}
|
|
481
|
+
const source = readFileSync2(schemaPath, "utf-8");
|
|
482
|
+
const schema = getSchema2(source);
|
|
483
|
+
const existingModels = /* @__PURE__ */ new Map();
|
|
484
|
+
for (const item of schema.list) {
|
|
485
|
+
if (item.type === "model") {
|
|
486
|
+
existingModels.set(item.name, item);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const missingModels = [];
|
|
490
|
+
const missingFields = [];
|
|
491
|
+
const outdatedFields = [];
|
|
492
|
+
for (const [modelName, modelDef] of Object.entries(SITEPING_MODELS)) {
|
|
493
|
+
const model = existingModels.get(modelName);
|
|
494
|
+
if (!model) {
|
|
495
|
+
missingModels.push(modelName);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const existingFields = /* @__PURE__ */ new Map();
|
|
499
|
+
for (const prop of model.properties) {
|
|
500
|
+
if (prop.type === "field") {
|
|
501
|
+
existingFields.set(prop.name, prop);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
|
|
505
|
+
const existing = existingFields.get(fieldName);
|
|
506
|
+
if (!existing) {
|
|
507
|
+
missingFields.push(`${modelName}.${fieldName}`);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const expectedType = fieldDef.relation ? fieldDef.relation.model : fieldDef.type;
|
|
511
|
+
const expectedOptional = fieldDef.optional ?? false;
|
|
512
|
+
const expectedArray = fieldDef.relation?.kind === "1-to-many";
|
|
513
|
+
const typeMatch = existing.fieldType === expectedType;
|
|
514
|
+
const optionalMatch = (existing.optional ?? false) === expectedOptional;
|
|
515
|
+
const arrayMatch = (existing.array ?? false) === expectedArray;
|
|
516
|
+
if (!typeMatch || !optionalMatch || !arrayMatch) {
|
|
517
|
+
outdatedFields.push(`${modelName}.${fieldName}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { found: true, path: schemaPath, missingModels, missingFields, outdatedFields };
|
|
522
|
+
}
|
|
523
|
+
function pad(label, width) {
|
|
524
|
+
return label + " ".repeat(Math.max(1, width - label.length));
|
|
525
|
+
}
|
|
526
|
+
function statusCommand(options) {
|
|
527
|
+
const cwd = process.cwd();
|
|
528
|
+
p3.intro("siteping \u2014 Diagnostic");
|
|
529
|
+
const schemaPath = options.schema ?? findPrismaSchema2(cwd);
|
|
530
|
+
const schemaResult = checkSchema(schemaPath);
|
|
531
|
+
if (!schemaResult.found) {
|
|
532
|
+
p3.log.error(`${pad("Prisma schema", 25)}\u2717 Non trouv\xE9`);
|
|
533
|
+
} else {
|
|
534
|
+
const issues = [
|
|
535
|
+
...schemaResult.missingModels.map((m) => `mod\xE8le ${m}`),
|
|
536
|
+
...schemaResult.missingFields,
|
|
537
|
+
...schemaResult.outdatedFields
|
|
538
|
+
];
|
|
539
|
+
if (issues.length === 0) {
|
|
540
|
+
p3.log.success(`${pad("Prisma schema", 25)}\u2713 \xC0 jour`);
|
|
541
|
+
} else {
|
|
542
|
+
const missingCount = schemaResult.missingModels.length + schemaResult.missingFields.length;
|
|
543
|
+
const outdatedCount = schemaResult.outdatedFields.length;
|
|
544
|
+
const parts = [];
|
|
545
|
+
if (missingCount > 0)
|
|
546
|
+
parts.push(`${missingCount} champ${missingCount > 1 ? "s" : ""} manquant${missingCount > 1 ? "s" : ""}`);
|
|
547
|
+
if (outdatedCount > 0)
|
|
548
|
+
parts.push(`${outdatedCount} champ${outdatedCount > 1 ? "s" : ""} obsol\xE8te${outdatedCount > 1 ? "s" : ""}`);
|
|
549
|
+
p3.log.warn(`${pad("Prisma schema", 25)}\u26A0 ${parts.join(", ")} (${issues.join(", ")})`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const routePath = findApiRoute(cwd);
|
|
553
|
+
if (routePath) {
|
|
554
|
+
p3.log.success(`${pad("Route API", 25)}\u2713 ${relative(cwd, routePath)}`);
|
|
555
|
+
} else {
|
|
556
|
+
p3.log.error(`${pad("Route API", 25)}\u2717 Non trouv\xE9e`);
|
|
557
|
+
}
|
|
558
|
+
const pkg = readPackageJson(cwd);
|
|
559
|
+
if (pkg) {
|
|
560
|
+
const deps = pkg.dependencies ?? {};
|
|
561
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
562
|
+
const version = deps["@siteping/widget"] ?? devDeps["@siteping/widget"];
|
|
563
|
+
if (version) {
|
|
564
|
+
p3.log.success(`${pad("Package", 25)}\u2713 @siteping/widget@${version}`);
|
|
565
|
+
} else {
|
|
566
|
+
p3.log.error(`${pad("Package", 25)}\u2717 @siteping/widget non trouv\xE9 dans package.json`);
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
p3.log.error(`${pad("Package", 25)}\u2717 package.json non trouv\xE9`);
|
|
570
|
+
}
|
|
571
|
+
const widgetFile = findWidgetUsage(cwd);
|
|
572
|
+
if (widgetFile) {
|
|
573
|
+
p3.log.success(`${pad("Widget int\xE9gr\xE9", 25)}\u2713 trouv\xE9 dans ${relative(cwd, widgetFile)}`);
|
|
574
|
+
} else {
|
|
575
|
+
p3.log.warn(`${pad("Widget int\xE9gr\xE9", 25)}\u26A0 initSiteping non trouv\xE9 dans les sources`);
|
|
576
|
+
}
|
|
577
|
+
const hasError = !schemaResult.found || !routePath || !pkg || pkg && !(pkg.dependencies?.["@siteping/widget"] ?? pkg.devDependencies?.["@siteping/widget"]);
|
|
578
|
+
const hasWarning = schemaResult.missingModels.length > 0 || schemaResult.missingFields.length > 0 || schemaResult.outdatedFields.length > 0 || !widgetFile;
|
|
579
|
+
if (hasError) {
|
|
580
|
+
p3.outro("Des \xE9l\xE9ments sont manquants \u2014 lancez `siteping init` pour configurer.");
|
|
581
|
+
} else if (hasWarning) {
|
|
582
|
+
p3.outro("Quelques ajustements n\xE9cessaires \u2014 lancez `siteping sync` pour mettre \xE0 jour.");
|
|
583
|
+
} else {
|
|
584
|
+
p3.outro("Tout est configur\xE9 !");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/commands/sync.ts
|
|
589
|
+
import { existsSync as existsSync5 } from "fs";
|
|
590
|
+
import { join as join4 } from "path";
|
|
591
|
+
import * as p4 from "@clack/prompts";
|
|
592
|
+
function syncCommand(options) {
|
|
593
|
+
const cwd = process.cwd();
|
|
594
|
+
const schemaPath = options.schema ?? findPrismaSchema3(cwd);
|
|
595
|
+
if (!schemaPath) {
|
|
596
|
+
p4.log.error("Aucun fichier schema.prisma trouv\xE9.");
|
|
597
|
+
p4.log.info("Sp\xE9cifiez le chemin avec --schema <path>");
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
if (!existsSync5(schemaPath)) {
|
|
601
|
+
p4.log.error(`Fichier introuvable : ${schemaPath}`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
const { addedModels, changes } = syncPrismaModels(schemaPath);
|
|
606
|
+
if (addedModels.length === 0 && changes.length === 0) {
|
|
607
|
+
p4.log.info("\u2713 Le schema est d\xE9j\xE0 \xE0 jour.");
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (addedModels.length > 0) {
|
|
611
|
+
p4.log.success(`Mod\xE8les cr\xE9\xE9s : ${addedModels.join(", ")}`);
|
|
612
|
+
}
|
|
613
|
+
for (const change of changes) {
|
|
614
|
+
const icon = change.action === "added" ? "+" : "~";
|
|
615
|
+
p4.log.success(
|
|
616
|
+
`${icon} ${change.model}.${change.field} \u2014 ${change.action === "added" ? "ajout\xE9" : "mis \xE0 jour"} (${change.detail})`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
p4.log.info("N'oubliez pas : npx prisma db push");
|
|
620
|
+
} catch (error) {
|
|
621
|
+
p4.log.error(`Erreur : ${error instanceof Error ? error.message : String(error)}`);
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function findPrismaSchema3(cwd) {
|
|
626
|
+
const candidates = [
|
|
627
|
+
join4(cwd, "prisma", "schema.prisma"),
|
|
628
|
+
join4(cwd, "schema.prisma"),
|
|
629
|
+
join4(cwd, "prisma", "schema", "schema.prisma")
|
|
630
|
+
];
|
|
631
|
+
return candidates.find((p5) => existsSync5(p5)) ?? null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/index.ts
|
|
635
|
+
var program = new Command().name("siteping").description("CLI pour configurer @siteping/*").version("0.3.0");
|
|
636
|
+
program.command("init").description("Configure le schema Prisma et la route API dans votre projet").action(initCommand);
|
|
637
|
+
program.command("sync").description("Synchronise le schema Prisma (non-interactif, CI-friendly)").option("--schema <path>", "Chemin vers le fichier schema.prisma").action(syncCommand);
|
|
638
|
+
program.command("status").description("Diagnostic complet de l'int\xE9gration Siteping").option("--schema <path>", "Chemin vers le fichier schema.prisma").action(statusCommand);
|
|
639
|
+
program.command("doctor").description("Test de connexion \xE0 l'API Siteping").option("--url <url>", "URL du serveur (d\xE9faut: http://localhost:3000)").option("--endpoint <path>", "Chemin de l'endpoint (d\xE9faut: /api/siteping)").action(doctorCommand);
|
|
640
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@siteping/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "CLI pour configurer Siteping dans votre projet",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"siteping": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"check": "tsc --noEmit",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"siteping",
|
|
19
|
+
"cli",
|
|
20
|
+
"prisma",
|
|
21
|
+
"setup"
|
|
22
|
+
],
|
|
23
|
+
"author": "neosianexus",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"homepage": "https://github.com/NeosiaNexus/siteping/tree/main/packages/cli",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/NeosiaNexus/siteping.git",
|
|
29
|
+
"directory": "packages/cli"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"commander": "^13.0.0",
|
|
33
|
+
"@clack/prompts": "^0.9.0",
|
|
34
|
+
"@mrleebo/prisma-ast": "^0.12.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@siteping/core": "workspace:*"
|
|
38
|
+
}
|
|
39
|
+
}
|