odoro 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.
Files changed (59) hide show
  1. package/LICENSE +12 -0
  2. package/client.d.ts +84 -0
  3. package/dist/build-SNTGJH2J.js +4 -0
  4. package/dist/chunk-2YDI5NKV.js +55 -0
  5. package/dist/chunk-G2WW7N4C.js +1872 -0
  6. package/dist/chunk-G5QXVBYT.js +1139 -0
  7. package/dist/chunk-JMEHF3KN.js +35 -0
  8. package/dist/chunk-PEMYUK2D.js +64 -0
  9. package/dist/chunk-T42X2NJN.js +98 -0
  10. package/dist/chunk-T6RLHSCW.js +123 -0
  11. package/dist/chunk-UGRSODHU.js +154 -0
  12. package/dist/cli.d.ts +40 -0
  13. package/dist/cli.js +236 -0
  14. package/dist/commands-FVAHVVVN.js +793 -0
  15. package/dist/commands-V44Y5G4F.js +245 -0
  16. package/dist/create-SH2M722Y.js +288 -0
  17. package/dist/index.d.ts +354 -0
  18. package/dist/index.js +7 -0
  19. package/dist/package-AUINPBEX.js +62 -0
  20. package/dist/preview-LOAO5Y6V.js +3 -0
  21. package/dist/registry/index.d.ts +316 -0
  22. package/dist/registry/index.js +2 -0
  23. package/dist/server-MZ76LPAG.js +3 -0
  24. package/package.json +57 -0
  25. package/templates/react-ts/README.md +52 -0
  26. package/templates/react-ts/_gitignore +8 -0
  27. package/templates/react-ts/index.html +13 -0
  28. package/templates/react-ts/odoro.config.ts +10 -0
  29. package/templates/react-ts/package.json +23 -0
  30. package/templates/react-ts/public/favicon.svg +4 -0
  31. package/templates/react-ts/src/App.tsx +66 -0
  32. package/templates/react-ts/src/main.tsx +18 -0
  33. package/templates/react-ts/src/odoro-env.d.ts +1 -0
  34. package/templates/react-ts/src/routes/About.tsx +19 -0
  35. package/templates/react-ts/src/routes/Home.tsx +80 -0
  36. package/templates/react-ts/src/routes/NotFound.tsx +14 -0
  37. package/templates/react-ts/src/styles.css +13 -0
  38. package/templates/react-ts/tsconfig.json +25 -0
  39. package/templates/react-ts-server/Dockerfile +51 -0
  40. package/templates/react-ts-server/README.md +87 -0
  41. package/templates/react-ts-server/_dockerignore +8 -0
  42. package/templates/react-ts-server/_env.example +78 -0
  43. package/templates/react-ts-server/_gitignore +8 -0
  44. package/templates/react-ts-server/client/index.html +13 -0
  45. package/templates/react-ts-server/client/public/favicon.svg +4 -0
  46. package/templates/react-ts-server/client/src/App.tsx +66 -0
  47. package/templates/react-ts-server/client/src/main.tsx +18 -0
  48. package/templates/react-ts-server/client/src/odoro-env.d.ts +1 -0
  49. package/templates/react-ts-server/client/src/routes/About.tsx +19 -0
  50. package/templates/react-ts-server/client/src/routes/Home.tsx +148 -0
  51. package/templates/react-ts-server/client/src/routes/NotFound.tsx +14 -0
  52. package/templates/react-ts-server/client/src/styles.css +13 -0
  53. package/templates/react-ts-server/odoro.config.ts +21 -0
  54. package/templates/react-ts-server/package.json +33 -0
  55. package/templates/react-ts-server/scripts/dev.mjs +74 -0
  56. package/templates/react-ts-server/server/src/main.ts +131 -0
  57. package/templates/react-ts-server/server/src/modules/health/index.ts +144 -0
  58. package/templates/react-ts-server/server/tsconfig.json +23 -0
  59. package/templates/react-ts-server/tsconfig.json +27 -0
@@ -0,0 +1,793 @@
1
+ #!/usr/bin/env node
2
+ import { guessAlias, defaultAliases } from './chunk-T6RLHSCW.js';
3
+ import { object, string, _minLength, array, _default, record, literal, safeParse, resolveGraph, describeProblem, parseMeta } from './chunk-G2WW7N4C.js';
4
+ import { colors, info, warn, success, error } from './chunk-JMEHF3KN.js';
5
+ import { readFile, writeFile, mkdir, rm, rename } from 'fs/promises';
6
+ import { join, isAbsolute, resolve, dirname, posix } from 'path';
7
+ import * as prompts from '@clack/prompts';
8
+ import { createHash } from 'crypto';
9
+
10
+ var CONFIG_FILE = "odoro.json";
11
+ var trackedFileSchema = object({
12
+ /** Chemin dans le projet, relatif a sa racine, en barres obliques. */
13
+ path: string().check(_minLength(1)),
14
+ /** Empreinte du contenu **tel qu'il a ete livre**. */
15
+ hash: string().check(_minLength(1))
16
+ });
17
+ var installedSchema = object({
18
+ /** Date d'installation, en ISO 8601. */
19
+ installedAt: string().check(_minLength(1)),
20
+ /** Fichiers ecrits par cette entree. */
21
+ files: array(trackedFileSchema)
22
+ });
23
+ var aliasesSchema = object({
24
+ /**
25
+ * Prefixe d'import employe dans le code ecrit, sans barre finale.
26
+ * Par exemple `@/components/odoro`.
27
+ */
28
+ import: string().check(_minLength(1)),
29
+ /**
30
+ * Dossier correspondant sur le disque, relatif a la racine du projet.
31
+ * Par exemple `src/components/odoro`.
32
+ */
33
+ directory: string().check(_minLength(1))
34
+ });
35
+ var projectSchema = object({
36
+ /** Version du format, pour que la CLI sache si elle sait lire. */
37
+ version: _default(literal(1), 1),
38
+ /** Adresse du registre : une URL, ou un chemin local pour le developper. */
39
+ registry: string().check(_minLength(1)),
40
+ /** Ou ecrire, et sous quel prefixe importer. */
41
+ aliases: aliasesSchema,
42
+ /** Ce qui a ete installe, indexe par identifiant de registre. */
43
+ installed: _default(record(string(), installedSchema), {})
44
+ });
45
+ function fingerprint(content) {
46
+ const normalised = content.replaceAll("\r\n", "\n");
47
+ return createHash("sha256").update(normalised, "utf8").digest("hex").slice(0, 16);
48
+ }
49
+ function configPath(root) {
50
+ return join(root, CONFIG_FILE);
51
+ }
52
+ async function loadProject(root) {
53
+ let raw;
54
+ try {
55
+ raw = await readFile(configPath(root), "utf8");
56
+ } catch {
57
+ return { ok: false, reason: "absent", problems: [] };
58
+ }
59
+ let value;
60
+ try {
61
+ value = JSON.parse(raw);
62
+ } catch (error2) {
63
+ return {
64
+ ok: false,
65
+ reason: "invalide",
66
+ problems: [`JSON illisible \u2014 ${error2.message}`]
67
+ };
68
+ }
69
+ const parsed = safeParse(projectSchema, value);
70
+ if (!parsed.success) {
71
+ return {
72
+ ok: false,
73
+ reason: "invalide",
74
+ problems: parsed.error.issues.map(
75
+ (issue) => `${issue.path.join(".") || CONFIG_FILE} : ${issue.message}`
76
+ )
77
+ };
78
+ }
79
+ return { ok: true, config: parsed.data };
80
+ }
81
+ async function saveProject(root, config) {
82
+ const installed = Object.fromEntries(
83
+ Object.entries(config.installed).sort(([a], [b]) => a.localeCompare(b))
84
+ );
85
+ const ordered = { ...config, installed };
86
+ await writeFile(configPath(root), `${JSON.stringify(ordered, null, 2)}
87
+ `, "utf8");
88
+ }
89
+
90
+ // src/add/rewrite.ts
91
+ var REGISTRY_TOKEN = "@registre";
92
+ function rewriteImports(source, importPrefix) {
93
+ const prefix = importPrefix.replace(/\/$/, "");
94
+ return source.split(`${REGISTRY_TOKEN}/`).join(`${prefix}/`);
95
+ }
96
+ function usedTokens(source) {
97
+ const pattern = new RegExp(`${REGISTRY_TOKEN}/([\\w./-]+)`, "g");
98
+ const found = /* @__PURE__ */ new Set();
99
+ for (const match of source.matchAll(pattern)) {
100
+ const path = match[1];
101
+ if (path !== void 0) found.add(path);
102
+ }
103
+ return [...found].sort();
104
+ }
105
+
106
+ // src/add/inspect.ts
107
+ var STATE_LABEL = {
108
+ "a-jour": "a jour",
109
+ retouche: "retouche localement",
110
+ "mise-a-jour": "une mise a jour existe",
111
+ divergence: "retouche localement, et le registre a change",
112
+ absent: "absent du projet",
113
+ inconnu: "inconnu du registre"
114
+ };
115
+ function verdict(local, deliveredHash, upstream) {
116
+ if (local === null) return "absent";
117
+ if (upstream === null) return "inconnu";
118
+ const localChanged = fingerprint(local) !== deliveredHash;
119
+ const upstreamChanged = fingerprint(upstream) !== deliveredHash;
120
+ if (!localChanged && !upstreamChanged) return "a-jour";
121
+ if (localChanged && !upstreamChanged) return "retouche";
122
+ if (!localChanged && upstreamChanged) return "mise-a-jour";
123
+ return "divergence";
124
+ }
125
+ async function inspectEntry(root, config, id, upstream) {
126
+ const installed = config.installed[id];
127
+ if (installed === void 0) {
128
+ return { id, files: [], orphan: upstream === null, upstream };
129
+ }
130
+ const served = /* @__PURE__ */ new Map();
131
+ if (upstream !== null) {
132
+ for (const file of upstream.files) {
133
+ const source = upstream.sources[file.path];
134
+ if (source !== void 0) {
135
+ served.set(
136
+ join(config.aliases.directory, file.target).replaceAll("\\", "/"),
137
+ rewriteImports(source, config.aliases.import)
138
+ );
139
+ }
140
+ }
141
+ }
142
+ const files = [];
143
+ for (const tracked of installed.files) {
144
+ let local = null;
145
+ try {
146
+ local = await readFile(join(root, tracked.path), "utf8");
147
+ } catch {
148
+ local = null;
149
+ }
150
+ const upstreamSource = served.get(tracked.path.replaceAll("\\", "/")) ?? null;
151
+ files.push({
152
+ path: tracked.path,
153
+ state: verdict(local, tracked.hash, upstreamSource),
154
+ local,
155
+ upstream: upstreamSource
156
+ });
157
+ }
158
+ return { id, files, orphan: upstream === null, upstream };
159
+ }
160
+ async function inspectAll(root, config, registry) {
161
+ const reports = [];
162
+ for (const id of Object.keys(config.installed).sort()) {
163
+ const fetched = await registry.entry(id);
164
+ reports.push(await inspectEntry(root, config, id, fetched.ok ? fetched.value : null));
165
+ }
166
+ return reports;
167
+ }
168
+ function previewChanges(local, upstream, limit = 8) {
169
+ const localLines = local.replaceAll("\r\n", "\n").split("\n");
170
+ const upstreamLines = upstream.replaceAll("\r\n", "\n").split("\n");
171
+ const localSet = new Set(localLines);
172
+ const upstreamSet = new Set(upstreamLines);
173
+ return {
174
+ added: upstreamLines.filter((line) => line.trim() !== "" && !localSet.has(line)).slice(0, limit),
175
+ removed: localLines.filter((line) => line.trim() !== "" && !upstreamSet.has(line)).slice(0, limit)
176
+ };
177
+ }
178
+ function isRemote(location) {
179
+ return /^https?:\/\//.test(location);
180
+ }
181
+ async function readDocument(location, relativePath) {
182
+ if (isRemote(location)) {
183
+ const url = `${location.replace(/\/$/, "")}/${relativePath}`;
184
+ let response;
185
+ try {
186
+ response = await fetch(url);
187
+ } catch (error2) {
188
+ return {
189
+ ok: false,
190
+ problems: [`${url} : injoignable \u2014 ${error2.message}`]
191
+ };
192
+ }
193
+ if (!response.ok) {
194
+ return { ok: false, problems: [`${url} : reponse ${String(response.status)}.`] };
195
+ }
196
+ try {
197
+ return { ok: true, value: await response.json() };
198
+ } catch {
199
+ return { ok: false, problems: [`${url} : la reponse n'est pas du JSON.`] };
200
+ }
201
+ }
202
+ const file = join(location, relativePath);
203
+ let raw;
204
+ try {
205
+ raw = await readFile(file, "utf8");
206
+ } catch {
207
+ return { ok: false, problems: [`${file} : introuvable.`] };
208
+ }
209
+ try {
210
+ return { ok: true, value: JSON.parse(raw) };
211
+ } catch (error2) {
212
+ return {
213
+ ok: false,
214
+ problems: [`${file} : JSON illisible \u2014 ${error2.message}`]
215
+ };
216
+ }
217
+ }
218
+ function asIndex(value, origin) {
219
+ const candidate = value;
220
+ if (candidate.version !== 1) {
221
+ return {
222
+ ok: false,
223
+ problems: [
224
+ `${origin} : version de format ${String(candidate.version)} non reconnue. Mettez la CLI a jour.`
225
+ ]
226
+ };
227
+ }
228
+ if (!Array.isArray(candidate.entries)) {
229
+ return {
230
+ ok: false,
231
+ problems: [`${origin} : l'index ne contient aucune liste d'entrees.`]
232
+ };
233
+ }
234
+ return { ok: true, value: candidate };
235
+ }
236
+ function asEntry(value, id) {
237
+ const parsed = parseMeta(value, id);
238
+ if (!parsed.ok) return { ok: false, problems: parsed.problems };
239
+ const sources = value.sources;
240
+ if (typeof sources !== "object" || sources === null) {
241
+ return { ok: false, problems: [`${id} : l'entree ne porte aucun code source.`] };
242
+ }
243
+ const record2 = sources;
244
+ const problems = [];
245
+ for (const file of parsed.meta.files) {
246
+ if (typeof record2[file.path] !== "string") {
247
+ problems.push(`${id} : le fichier annonce "${file.path}" est absent de la reponse.`);
248
+ }
249
+ }
250
+ if (problems.length > 0) return { ok: false, problems };
251
+ return {
252
+ ok: true,
253
+ value: { ...parsed.meta, id, sources: record2 }
254
+ };
255
+ }
256
+ function openRegistry(location, root) {
257
+ const resolved = isRemote(location) ? location : isAbsolute(location) ? location : resolve(root, location);
258
+ return {
259
+ location: resolved,
260
+ async index() {
261
+ const document = await readDocument(resolved, "index.json");
262
+ if (!document.ok) return document;
263
+ return asIndex(document.value, `${resolved}/index.json`);
264
+ },
265
+ async entry(id) {
266
+ const document = await readDocument(resolved, `${id}.json`);
267
+ if (!document.ok) return document;
268
+ return asEntry(document.value, id);
269
+ }
270
+ };
271
+ }
272
+ function indexById(index) {
273
+ return new Map(index.entries.map((entry) => [entry.id, entry]));
274
+ }
275
+ async function planWrite(root, path, content, owner) {
276
+ let existing = null;
277
+ try {
278
+ existing = await readFile(join(root, path), "utf8");
279
+ } catch {
280
+ existing = null;
281
+ }
282
+ const action = existing === null ? "creation" : existing.replaceAll("\r\n", "\n") === content.replaceAll("\r\n", "\n") ? "inchange" : "remplacement";
283
+ return { path, content, action, owner };
284
+ }
285
+ var PENDING = ".odoro-en-cours";
286
+ async function applyPlan(root, plan) {
287
+ const todo = plan.filter((entry) => entry.action !== "inchange");
288
+ const skipped = plan.filter((entry) => entry.action === "inchange").map((e) => e.path);
289
+ const pending = [];
290
+ const previous = /* @__PURE__ */ new Map();
291
+ try {
292
+ for (const entry of todo) {
293
+ const target = join(root, entry.path);
294
+ await mkdir(dirname(target), { recursive: true });
295
+ if (entry.action === "remplacement") {
296
+ previous.set(entry.path, await readFile(target, "utf8"));
297
+ } else {
298
+ previous.set(entry.path, null);
299
+ }
300
+ const temporary = `${target}${PENDING}`;
301
+ await writeFile(temporary, entry.content, "utf8");
302
+ pending.push(temporary);
303
+ }
304
+ } catch (cause) {
305
+ await Promise.all(pending.map((file) => rm(file, { force: true })));
306
+ throw cause;
307
+ }
308
+ const placed = [];
309
+ try {
310
+ for (const entry of todo) {
311
+ const target = join(root, entry.path);
312
+ await rename(`${target}${PENDING}`, target);
313
+ placed.push(entry.path);
314
+ }
315
+ } catch (cause) {
316
+ await restore(root, placed, previous);
317
+ await Promise.all(pending.map((file) => rm(file, { force: true })));
318
+ throw cause;
319
+ }
320
+ return { written: todo.map((entry) => entry.path), skipped };
321
+ }
322
+ async function restore(root, placed, previous) {
323
+ for (const path of placed) {
324
+ const target = join(root, path);
325
+ const before = previous.get(path);
326
+ try {
327
+ if (before === null || before === void 0) {
328
+ await rm(target, { force: true });
329
+ } else {
330
+ await writeFile(target, before, "utf8");
331
+ }
332
+ } catch {
333
+ }
334
+ }
335
+ }
336
+
337
+ // src/add/install.ts
338
+ function suggest(unknown, known) {
339
+ const needle = unknown.toLowerCase();
340
+ const tail = needle.split("/").pop() ?? needle;
341
+ return known.filter((id) => {
342
+ const candidate = id.toLowerCase();
343
+ return candidate.includes(tail) || (candidate.split("/").pop() ?? "").includes(tail);
344
+ }).slice(0, 4);
345
+ }
346
+ async function prepareInstall(registry, requested) {
347
+ const index = await registry.index();
348
+ if (!index.ok) return { ok: false, problems: index.problems };
349
+ const catalogue = indexById(index.value);
350
+ const known = [...catalogue.keys()];
351
+ const ids = [];
352
+ const problems = [];
353
+ for (const asked of requested) {
354
+ if (catalogue.has(asked)) {
355
+ ids.push(asked);
356
+ continue;
357
+ }
358
+ const matches = known.filter((id) => (id.split("/").pop() ?? "") === asked);
359
+ if (matches.length === 1 && matches[0] !== void 0) {
360
+ ids.push(matches[0]);
361
+ continue;
362
+ }
363
+ if (matches.length > 1) {
364
+ problems.push(
365
+ `"${asked}" est ambigu : ${matches.join(", ")}. Precisez la categorie.`
366
+ );
367
+ continue;
368
+ }
369
+ const near = suggest(asked, known);
370
+ problems.push(
371
+ near.length > 0 ? `"${asked}" est introuvable. Vouliez-vous dire ${near.join(", ")} ?` : `"${asked}" est introuvable. "odoro list" donne le catalogue.`
372
+ );
373
+ }
374
+ if (problems.length > 0) return { ok: false, problems };
375
+ const resolvable = new Map(
376
+ [...catalogue].map(([id, entry]) => [
377
+ id,
378
+ { id, registryDependencies: entry.registryDependencies }
379
+ ])
380
+ );
381
+ const graph = resolveGraph(ids, resolvable);
382
+ if (!graph.ok) return { ok: false, problems: graph.problems.map(describeProblem) };
383
+ const entries = [];
384
+ for (const id of graph.graph.order) {
385
+ const entry = await registry.entry(id);
386
+ if (!entry.ok) return { ok: false, problems: entry.problems };
387
+ entries.push(entry.value);
388
+ }
389
+ return { ok: true, entries, implied: graph.graph.implied };
390
+ }
391
+ function targetPath(config, target) {
392
+ return posix.join(config.aliases.directory, target);
393
+ }
394
+ async function planInstall(root, config, entries) {
395
+ const plan = [];
396
+ for (const entry of entries) {
397
+ for (const file of entry.files) {
398
+ const source = entry.sources[file.path];
399
+ if (source === void 0) continue;
400
+ plan.push(
401
+ await planWrite(
402
+ root,
403
+ targetPath(config, file.target),
404
+ rewriteImports(source, config.aliases.import),
405
+ entry.id
406
+ )
407
+ );
408
+ }
409
+ }
410
+ return plan;
411
+ }
412
+ function recordInstall(previous, entries, plan, now) {
413
+ const installed = { ...previous };
414
+ for (const entry of entries) {
415
+ installed[entry.id] = {
416
+ installedAt: now.toISOString(),
417
+ files: plan.filter((write) => write.owner === entry.id).map((write) => ({ path: write.path, hash: fingerprint(write.content) }))
418
+ };
419
+ }
420
+ return installed;
421
+ }
422
+
423
+ // src/add/weight.ts
424
+ var BACKEND_WEIGHT = {
425
+ /** Scene 3D minimale : geometrie, materiau standard, lumiere directionnelle. */
426
+ three: 130,
427
+ /** Rendu plein ecran : contexte, programme, triangle. */
428
+ ogl: 13
429
+ };
430
+ function weighEntries(entries) {
431
+ const byBackend = /* @__PURE__ */ new Map();
432
+ for (const entry of entries) {
433
+ const backend = entry.perf.backend;
434
+ if (backend === false) continue;
435
+ const list = byBackend.get(backend) ?? [];
436
+ list.push(entry.id);
437
+ byBackend.set(backend, list);
438
+ }
439
+ const warnings = [];
440
+ for (const [backend, ids] of byBackend) {
441
+ const kilobytes = BACKEND_WEIGHT[backend];
442
+ const which = ids.length === 1 ? ids[0] : `${String(ids.length)} composants`;
443
+ warnings.push({
444
+ backend,
445
+ kilobytes,
446
+ entries: ids,
447
+ message: backend === "three" ? `${String(which)} charge une scene 3D : environ ${String(kilobytes)} Ko compresses au premier affichage. Le backend leger en demande ${String(BACKEND_WEIGHT.ogl)}, si un effet plein ecran suffit.` : `${String(which)} charge le backend leger : environ ${String(kilobytes)} Ko compresses.`
448
+ });
449
+ }
450
+ return warnings.sort((a, b) => b.kilobytes - a.kilobytes);
451
+ }
452
+ function requiredPackages(entries) {
453
+ const packages = /* @__PURE__ */ new Set();
454
+ for (const entry of entries) {
455
+ for (const dependency of entry.dependencies) packages.add(dependency);
456
+ if (entry.engine.gl !== false || entry.engine.gsap.length > 0) {
457
+ packages.add("@odoro-cli/engine");
458
+ }
459
+ }
460
+ return [...packages].sort();
461
+ }
462
+
463
+ // src/add/commands.ts
464
+ var DEFAULT_REGISTRY = "https://register.odoro.dev";
465
+ function canAsk(options) {
466
+ return options.yes !== true && process.stdin.isTTY === true;
467
+ }
468
+ function needsConfirmation(what) {
469
+ error(
470
+ `${what} Relancez avec ${colors.cyan("--yes")} pour l'accepter sans question.`
471
+ );
472
+ return 1;
473
+ }
474
+ function fail(title, problems) {
475
+ error(title);
476
+ for (const problem of problems) console.error(` ${colors.dim("\xB7")} ${problem}`);
477
+ return 1;
478
+ }
479
+ async function requireProject(root) {
480
+ const loaded = await loadProject(root);
481
+ if (loaded.ok) return loaded.config;
482
+ if (loaded.reason === "absent") {
483
+ error(`Aucun ${CONFIG_FILE} ici. Lancez ${colors.cyan("odoro init")} d'abord.`);
484
+ return 1;
485
+ }
486
+ return fail(`${CONFIG_FILE} est illisible :`, loaded.problems);
487
+ }
488
+ async function initCommand(options) {
489
+ const { root } = options;
490
+ const existing = await loadProject(root);
491
+ if (existing.ok && options.yes !== true) {
492
+ if (!canAsk(options)) {
493
+ return needsConfirmation(`${CONFIG_FILE} existe deja et serait remplace.`);
494
+ }
495
+ const replace = await prompts.confirm({
496
+ message: `${CONFIG_FILE} existe deja. Le remplacer ?`,
497
+ initialValue: false
498
+ });
499
+ if (prompts.isCancel(replace) || !replace) {
500
+ info("Rien n a ete change.");
501
+ return 0;
502
+ }
503
+ }
504
+ const guess = await guessAlias(root);
505
+ const suggested = defaultAliases(guess);
506
+ if (guess === null) {
507
+ warn(
508
+ `Aucun alias trouve dans tsconfig.json : les imports seront ecrits en ${colors.cyan(suggested.import)}.`
509
+ );
510
+ } else {
511
+ info(
512
+ `Alias trouve dans tsconfig.json : ${colors.cyan(`${guess.prefix}/*`)} vers ${colors.cyan(`${guess.directory}/`)}.`
513
+ );
514
+ }
515
+ let aliases = suggested;
516
+ let registry = options.registry ?? DEFAULT_REGISTRY;
517
+ if (canAsk(options)) {
518
+ const directory = await prompts.text({
519
+ message: "Ou ecrire les composants ?",
520
+ initialValue: suggested.directory
521
+ });
522
+ if (prompts.isCancel(directory)) return 0;
523
+ const importPrefix = await prompts.text({
524
+ message: "Sous quel prefixe les importer ?",
525
+ initialValue: suggested.import
526
+ });
527
+ if (prompts.isCancel(importPrefix)) return 0;
528
+ const source = await prompts.text({ message: "Registre ?", initialValue: registry });
529
+ if (prompts.isCancel(source)) return 0;
530
+ aliases = { directory, import: importPrefix };
531
+ registry = source;
532
+ }
533
+ const config = { version: 1, registry, aliases, installed: {} };
534
+ await saveProject(root, config);
535
+ success(`${CONFIG_FILE} ecrit.`);
536
+ console.log(` ${colors.dim("destination")} ${aliases.directory}/`);
537
+ console.log(` ${colors.dim("imports")} ${aliases.import}/\u2026`);
538
+ console.log(` ${colors.dim("registre")} ${registry}`);
539
+ console.log(`
540
+ ${colors.cyan("odoro list")} donne le catalogue.
541
+ `);
542
+ return 0;
543
+ }
544
+ var ACTION_MARK = {
545
+ creation: colors.green("+"),
546
+ remplacement: colors.yellow("~"),
547
+ inchange: colors.dim("=")
548
+ };
549
+ async function addCommand(options, requested) {
550
+ const { root } = options;
551
+ const config = await requireProject(root);
552
+ if (typeof config === "number") return config;
553
+ if (requested.length === 0) {
554
+ error('Rien a installer. Donnez un nom, ou "odoro list" pour le catalogue.');
555
+ return 1;
556
+ }
557
+ const registry = openRegistry(options.registry ?? config.registry, root);
558
+ const prepared = await prepareInstall(registry, requested);
559
+ if (!prepared.ok) return fail("Installation impossible :", prepared.problems);
560
+ const { entries, implied } = prepared;
561
+ if (implied.length > 0) {
562
+ info(`Dependances ajoutees : ${implied.map((id) => colors.cyan(id)).join(", ")}`);
563
+ }
564
+ for (const warning of weighEntries(entries)) warn(warning.message);
565
+ const missing = await missingPackages(root, requiredPackages(entries));
566
+ if (missing.length > 0) {
567
+ warn(
568
+ `A installer ensuite : ${missing.map((name) => colors.cyan(name)).join(" ")}`
569
+ );
570
+ }
571
+ const plan = await planInstall(root, config, entries);
572
+ const replacing = plan.filter((write) => write.action === "remplacement");
573
+ console.log("");
574
+ for (const write of plan) {
575
+ console.log(` ${ACTION_MARK[write.action]} ${write.path}`);
576
+ }
577
+ console.log("");
578
+ if (replacing.length > 0 && options.yes !== true) {
579
+ if (!canAsk(options)) {
580
+ return needsConfirmation(
581
+ `${String(replacing.length)} fichier(s) existant(s) seraient remplaces.`
582
+ );
583
+ }
584
+ const go = await prompts.confirm({
585
+ message: `${String(replacing.length)} fichier(s) existant(s) seront remplaces. Continuer ?`,
586
+ initialValue: false
587
+ });
588
+ if (prompts.isCancel(go) || !go) {
589
+ info("Rien n a ete ecrit.");
590
+ return 0;
591
+ }
592
+ }
593
+ try {
594
+ const report = await applyPlan(root, plan);
595
+ const updated = {
596
+ ...config,
597
+ installed: recordInstall(config.installed, entries, plan, /* @__PURE__ */ new Date())
598
+ };
599
+ await saveProject(root, updated);
600
+ const written = String(report.written.length);
601
+ const skipped = report.skipped.length > 0 ? `, ${String(report.skipped.length)} inchange(s)` : "";
602
+ success(`${written} fichier(s) ecrit(s)${skipped}.`);
603
+ } catch (cause) {
604
+ error("Ecriture interrompue : le projet est inchange.", cause);
605
+ return 1;
606
+ }
607
+ warnUndeclared(entries);
608
+ return 0;
609
+ }
610
+ function warnUndeclared(entries) {
611
+ const targets = new Set(
612
+ entries.flatMap((entry) => entry.files.map((file) => file.target))
613
+ );
614
+ for (const entry of entries) {
615
+ for (const source of Object.values(entry.sources)) {
616
+ for (const token of usedTokens(source)) {
617
+ const matched = [...targets].some((target) => target.startsWith(token));
618
+ if (!matched) {
619
+ warn(
620
+ `${entry.id} importe ${colors.cyan(token)} sans l'avoir declare : signalez-le au registre.`
621
+ );
622
+ }
623
+ }
624
+ }
625
+ }
626
+ }
627
+ async function missingPackages(root, required) {
628
+ if (required.length === 0) return [];
629
+ let manifest;
630
+ try {
631
+ manifest = JSON.parse(
632
+ await readFile(join(root, "package.json"), "utf8")
633
+ );
634
+ } catch {
635
+ return [...required];
636
+ }
637
+ const present = /* @__PURE__ */ new Set([
638
+ ...Object.keys(manifest.dependencies ?? {}),
639
+ ...Object.keys(manifest.devDependencies ?? {})
640
+ ]);
641
+ return required.filter((name) => !present.has(name));
642
+ }
643
+ async function listCommand(options) {
644
+ const { root } = options;
645
+ const loaded = await loadProject(root);
646
+ const location = options.registry ?? (loaded.ok ? loaded.config.registry : DEFAULT_REGISTRY);
647
+ const installed = loaded.ok ? new Set(Object.keys(loaded.config.installed)) : /* @__PURE__ */ new Set();
648
+ const registry = openRegistry(location, root);
649
+ const index = await registry.index();
650
+ if (!index.ok) return fail("Catalogue illisible :", index.problems);
651
+ const byCategory = /* @__PURE__ */ new Map();
652
+ for (const entry of index.value.entries) {
653
+ byCategory.set(entry.category, [...byCategory.get(entry.category) ?? [], entry]);
654
+ }
655
+ console.log(`
656
+ ${colors.dim(registry.location)}
657
+ `);
658
+ for (const [category, entries] of [...byCategory].sort(
659
+ ([a], [b]) => a.localeCompare(b)
660
+ )) {
661
+ console.log(` ${colors.bold(category)}`);
662
+ for (const entry of entries) {
663
+ const mark = installed.has(entry.id) ? colors.green("\u2713") : " ";
664
+ const cost = entry.backend === false ? "" : colors.yellow(` (${entry.backend}, ${entry.tier})`);
665
+ console.log(
666
+ ` ${mark} ${colors.cyan(entry.name.padEnd(22))} ${entry.description}${cost}`
667
+ );
668
+ }
669
+ console.log("");
670
+ }
671
+ const total = String(index.value.entries.length);
672
+ const marked = installed.size > 0 ? `, ${colors.green("\u2713")} = deja installe` : "";
673
+ console.log(` ${colors.dim(`${total} entree(s)${marked}`)}
674
+ `);
675
+ return 0;
676
+ }
677
+ function stateColor(state) {
678
+ const label = STATE_LABEL[state];
679
+ if (state === "a-jour") return colors.green(label);
680
+ if (state === "retouche") return colors.cyan(label);
681
+ if (state === "absent" || state === "divergence") return colors.red(label);
682
+ return colors.yellow(label);
683
+ }
684
+ async function diffCommand(options) {
685
+ const { root } = options;
686
+ const config = await requireProject(root);
687
+ if (typeof config === "number") return config;
688
+ const ids = Object.keys(config.installed);
689
+ if (ids.length === 0) {
690
+ info("Aucun composant installe.");
691
+ return 0;
692
+ }
693
+ const registry = openRegistry(options.registry ?? config.registry, root);
694
+ const reports = await inspectAll(root, config, registry);
695
+ let interesting = 0;
696
+ console.log("");
697
+ for (const report of reports) {
698
+ if (report.orphan) {
699
+ console.log(` ${colors.cyan(report.id)} ${colors.dim("\u2014 absent du registre")}`);
700
+ interesting += 1;
701
+ continue;
702
+ }
703
+ for (const file of report.files) {
704
+ if (file.state === "a-jour") continue;
705
+ interesting += 1;
706
+ console.log(` ${colors.cyan(report.id)} ${colors.dim(file.path)}`);
707
+ console.log(` ${stateColor(file.state)}`);
708
+ if (file.local !== null && file.upstream !== null && file.state !== "retouche") {
709
+ const changes = previewChanges(file.local, file.upstream);
710
+ for (const line of changes.removed)
711
+ console.log(` ${colors.red(`- ${line.trim()}`)}`);
712
+ for (const line of changes.added)
713
+ console.log(` ${colors.green(`+ ${line.trim()}`)}`);
714
+ }
715
+ console.log("");
716
+ }
717
+ }
718
+ if (interesting === 0) {
719
+ success(`${String(ids.length)} entree(s) installee(s), toutes a jour.`);
720
+ return 0;
721
+ }
722
+ console.log(
723
+ ` ${colors.dim(`odoro add <nom> r\xE9\xE9crit une entr\xE9e depuis le registre. Vos retouches seraient perdues.`)}
724
+ `
725
+ );
726
+ return 0;
727
+ }
728
+ async function doctorCommand(options) {
729
+ const { root } = options;
730
+ const troubles = [];
731
+ const notes = [];
732
+ const loaded = await loadProject(root);
733
+ if (!loaded.ok) {
734
+ return loaded.reason === "absent" ? fail("Diagnostic impossible :", [`aucun ${CONFIG_FILE}. Lancez "odoro init".`]) : fail(`${CONFIG_FILE} est illisible :`, loaded.problems);
735
+ }
736
+ const config = loaded.config;
737
+ notes.push(`registre : ${config.registry}`);
738
+ if (!isRemote(config.registry)) {
739
+ notes.push("registre local : les autres machines de l equipe ne le verront pas.");
740
+ }
741
+ const { access } = await import('fs/promises');
742
+ try {
743
+ await access(join(root, config.aliases.directory));
744
+ } catch {
745
+ if (Object.keys(config.installed).length > 0) {
746
+ troubles.push(
747
+ `${config.aliases.directory}/ n'existe pas, alors que des entrees y sont notees.`
748
+ );
749
+ }
750
+ }
751
+ const registry = openRegistry(options.registry ?? config.registry, root);
752
+ const reports = await inspectAll(root, config, registry);
753
+ let retouched = 0;
754
+ for (const report of reports) {
755
+ if (report.orphan) {
756
+ notes.push(`${report.id} n'est plus servi par le registre.`);
757
+ continue;
758
+ }
759
+ for (const file of report.files) {
760
+ if (file.state === "absent") {
761
+ troubles.push(`${report.id} : ${file.path} est note comme installe mais absent.`);
762
+ }
763
+ if (file.state === "retouche") retouched += 1;
764
+ if (file.state === "mise-a-jour" || file.state === "divergence") {
765
+ notes.push(`${report.id} : ${STATE_LABEL[file.state]} (${file.path}).`);
766
+ }
767
+ }
768
+ }
769
+ if (retouched > 0) {
770
+ notes.push(
771
+ `${String(retouched)} fichier(s) retouche(s) localement : "odoro add" les reecrirait.`
772
+ );
773
+ }
774
+ const served = reports.map((report) => report.upstream).filter((entry) => entry !== null);
775
+ for (const name of await missingPackages(root, requiredPackages(served))) {
776
+ troubles.push(
777
+ `${name} est requis par un composant installe mais absent du package.json.`
778
+ );
779
+ }
780
+ console.log("");
781
+ for (const note of notes) console.log(` ${colors.dim("\xB7")} ${note}`);
782
+ if (notes.length > 0) console.log("");
783
+ if (troubles.length === 0) {
784
+ success("Rien a signaler.");
785
+ return 0;
786
+ }
787
+ for (const trouble of troubles) console.error(` ${colors.red("\xB7")} ${trouble}`);
788
+ console.log("");
789
+ error(`${String(troubles.length)} probleme(s).`);
790
+ return 1;
791
+ }
792
+
793
+ export { DEFAULT_REGISTRY, addCommand, diffCommand, doctorCommand, initCommand, listCommand };