@0dai-dev/cli 4.3.5 → 4.3.7

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 (79) hide show
  1. package/README.md +12 -11
  2. package/bin/0dai.js +214 -40
  3. package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
  4. package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
  5. package/lib/ai/registry/mcp-catalog.json +98 -0
  6. package/lib/commands/auth.js +55 -1
  7. package/lib/commands/compliance.js +1 -1
  8. package/lib/commands/detect.js +10 -4
  9. package/lib/commands/doctor.js +545 -26
  10. package/lib/commands/experience.js +40 -5
  11. package/lib/commands/export.js +73 -0
  12. package/lib/commands/feedback.js +157 -15
  13. package/lib/commands/gh.js +26 -0
  14. package/lib/commands/graph.js +9 -4
  15. package/lib/commands/heatmap.js +1 -1
  16. package/lib/commands/init.js +222 -30
  17. package/lib/commands/mcp.js +129 -21
  18. package/lib/commands/models.js +138 -41
  19. package/lib/commands/provider.js +30 -59
  20. package/lib/commands/quota.js +1 -1
  21. package/lib/commands/receipt.js +1 -1
  22. package/lib/commands/run.js +18 -7
  23. package/lib/commands/runner.js +31 -1
  24. package/lib/commands/status.js +44 -11
  25. package/lib/commands/swarm.js +130 -12
  26. package/lib/commands/trust.js +286 -0
  27. package/lib/commands/update.js +184 -38
  28. package/lib/commands/usage.js +1 -1
  29. package/lib/commands/validate.js +32 -3
  30. package/lib/commands/vault.js +46 -9
  31. package/lib/python/__init__.py +0 -0
  32. package/lib/python/agent_quotas.py +525 -0
  33. package/lib/python/anomaly_alert.py +397 -0
  34. package/lib/python/anti_pattern_detector.py +799 -0
  35. package/lib/python/auth.py +443 -0
  36. package/lib/python/capi_profile_guard.py +477 -0
  37. package/lib/python/compliance_report.py +581 -0
  38. package/lib/python/drift_detector.py +388 -0
  39. package/lib/python/experience_pipeline.py +1130 -0
  40. package/lib/python/graph.py +19 -0
  41. package/lib/python/graph_core.py +293 -0
  42. package/lib/python/graph_io.py +179 -0
  43. package/lib/python/graph_legacy.py +2052 -0
  44. package/lib/python/graph_legacy_helpers.py +221 -0
  45. package/lib/python/graph_outcomes_core.py +85 -0
  46. package/lib/python/graph_queries.py +171 -0
  47. package/lib/python/graph_slice.py +198 -0
  48. package/lib/python/graph_slicer.py +576 -0
  49. package/lib/python/graph_slicer_cli.py +60 -0
  50. package/lib/python/graph_validation.py +64 -0
  51. package/lib/python/heatmap.py +934 -0
  52. package/lib/python/json_utils.py +193 -0
  53. package/lib/python/mcp_exposure_check.py +247 -0
  54. package/lib/python/model_router.py +1434 -0
  55. package/lib/python/project_manager.py +621 -0
  56. package/lib/python/provider_profiles.py +1618 -0
  57. package/lib/python/provider_registry.py +1211 -0
  58. package/lib/python/provider_registry_cli.py +125 -0
  59. package/lib/python/receipt_png.py +727 -0
  60. package/lib/python/structural_memory.py +325 -0
  61. package/lib/python/swarm_cost.py +177 -0
  62. package/lib/python/usage_ledger.py +569 -0
  63. package/lib/scripts/mcp_tier_config.py +240 -0
  64. package/lib/shared.js +97 -14
  65. package/lib/tui/index.mjs +35174 -0
  66. package/lib/utils/activation_telemetry.js +230 -11
  67. package/lib/utils/constants.js +7 -1
  68. package/lib/utils/export-bundler.js +285 -0
  69. package/lib/utils/identity.js +198 -1
  70. package/lib/utils/mcp-auth.js +81 -15
  71. package/lib/utils/plan.js +1 -1
  72. package/lib/vault/index.js +19 -3
  73. package/lib/vault/storage.js +21 -2
  74. package/lib/wizard.js +5 -2
  75. package/package.json +9 -3
  76. package/scripts/build-python-bundle.js +106 -0
  77. package/scripts/build-tui.js +14 -1
  78. package/scripts/harvest_experience.py +523 -0
  79. package/scripts/postinstall.js +15 -9
@@ -66,7 +66,20 @@ esbuild
66
66
  logLevel: "warning",
67
67
  minify: false,
68
68
  sourcemap: false,
69
- banner: { js: "// @generated do not edit. Source: lib/tui/src/" },
69
+ // ESM output for node: define require/__filename/__dirname so esbuild's
70
+ // __require shim resolves real node builtins instead of throwing
71
+ // "Dynamic require of \"assert\" is not supported" at import time (#4359).
72
+ banner: {
73
+ js: [
74
+ "// @generated — do not edit. Source: lib/tui/src/",
75
+ "import { createRequire as __odaiCreateRequire } from 'node:module';",
76
+ "import { fileURLToPath as __odaiFileURLToPath } from 'node:url';",
77
+ "import { dirname as __odaiDirname } from 'node:path';",
78
+ "const require = __odaiCreateRequire(import.meta.url);",
79
+ "const __filename = __odaiFileURLToPath(import.meta.url);",
80
+ "const __dirname = __odaiDirname(__filename);",
81
+ ].join("\n"),
82
+ },
70
83
  })
71
84
  .then(() => {
72
85
  console.log("[tui-build] wrote " + path.relative(ROOT, OUT));
@@ -0,0 +1,523 @@
1
+ #!/usr/bin/env python3
2
+ """0dai Harvest — transform experience events into scored candidates.
3
+
4
+ Reads fallback events from ai/experience/events/*.json and *.jsonl,
5
+ groups by (tool, task_type) patterns, and creates candidate markdown files
6
+ in ai/experience/candidates/ for review and potential promotion.
7
+
8
+ Usage:
9
+ python3 scripts/harvest_experience.py [--target <path>] [--explain]
10
+ 0dai harvest --target . [--explain]
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import pathlib
17
+ import time
18
+ from collections import defaultdict
19
+
20
+ REQUIRED_EVENT_FIELDS = (
21
+ "schema",
22
+ "event_id",
23
+ "timestamp",
24
+ "event_type",
25
+ "tool",
26
+ "task_type",
27
+ "summary",
28
+ "paths",
29
+ "ci_passed",
30
+ "source",
31
+ )
32
+ STRING_FIELDS = (
33
+ "event_id",
34
+ "timestamp",
35
+ "event_type",
36
+ "tool",
37
+ "task_type",
38
+ "summary",
39
+ "source",
40
+ )
41
+ IGNORED_EVENT_FILENAMES = {"example.jsonl"}
42
+ HIGH_CONFIDENCE_EVENTS = 3
43
+ MEDIUM_CONFIDENCE_EVENTS = 2
44
+ SUCCESS_RESULTS = {"success", "partial", "passed", "ok", "done"}
45
+
46
+
47
+ def _rel(target: pathlib.Path, path: pathlib.Path) -> str:
48
+ try:
49
+ return str(path.relative_to(target))
50
+ except ValueError:
51
+ return str(path)
52
+
53
+
54
+ def _diagnostic(
55
+ path: str,
56
+ status: str,
57
+ reason: str,
58
+ *,
59
+ category: str = "discovery",
60
+ ) -> dict:
61
+ return {
62
+ "path": path,
63
+ "status": status,
64
+ "reason": reason,
65
+ "category": category,
66
+ }
67
+
68
+
69
+ def _validate_event(event: object) -> list[str]:
70
+ """Return deterministic fallback-contract validation issues."""
71
+ if not isinstance(event, dict):
72
+ return ["event must be a JSON object"]
73
+
74
+ issues: list[str] = []
75
+ missing = [
76
+ field
77
+ for field in REQUIRED_EVENT_FIELDS
78
+ if field not in event or event.get(field) in (None, "")
79
+ ]
80
+ for field in missing:
81
+ issues.append(f"missing required field: {field}")
82
+
83
+ if "schema" not in missing and event.get("schema") != 1:
84
+ issues.append("schema must be 1")
85
+
86
+ for field in STRING_FIELDS:
87
+ if field not in missing and not isinstance(event.get(field), str):
88
+ issues.append(f"{field} must be a string")
89
+
90
+ if "paths" not in missing:
91
+ paths = event.get("paths")
92
+ if not isinstance(paths, list) or any(not isinstance(item, str) for item in paths):
93
+ issues.append("paths must be an array of strings")
94
+
95
+ if "ci_passed" not in missing and not isinstance(event.get("ci_passed"), bool):
96
+ issues.append("ci_passed must be a boolean")
97
+
98
+ return issues
99
+
100
+
101
+ def _string_or_empty(value: object) -> str:
102
+ if value is None:
103
+ return ""
104
+ text = str(value).strip()
105
+ return text
106
+
107
+
108
+ def _first_string(*values: object) -> str:
109
+ for value in values:
110
+ text = _string_or_empty(value)
111
+ if text:
112
+ return text
113
+ return ""
114
+
115
+
116
+ def _string_list(value: object) -> list[str]:
117
+ if isinstance(value, str):
118
+ return [value] if value else []
119
+ if not isinstance(value, list):
120
+ return []
121
+ return [item for item in value if isinstance(item, str) and item]
122
+
123
+
124
+ def _legacy_ci_passed(event: dict, task: dict, context: dict, quality: dict) -> bool:
125
+ for value in (
126
+ event.get("ci_passed"),
127
+ context.get("tests_passed"),
128
+ task.get("tests_passed"),
129
+ quality.get("acceptance_criteria_met"),
130
+ ):
131
+ if isinstance(value, bool):
132
+ return value
133
+ result = _first_string(task.get("result"), event.get("result"), event.get("status"))
134
+ return result.lower() in SUCCESS_RESULTS
135
+
136
+
137
+ def _looks_like_structured_experience(event: dict) -> bool:
138
+ if "schema" in event:
139
+ return False
140
+ task = event.get("task")
141
+ if isinstance(task, dict) and (
142
+ "agent" in event
143
+ or "model" in event
144
+ or "project_id" in event
145
+ or "session_id" in event
146
+ or "context" in event
147
+ or "quality" in event
148
+ ):
149
+ return True
150
+ return False
151
+
152
+
153
+ def _normalize_structured_experience(event: object) -> tuple[object, str | None]:
154
+ """Adapt experience_pipeline JSONL records to the harvest fallback schema.
155
+
156
+ `experience_pipeline.py` writes structured events with `agent` and nested
157
+ `task` blocks. Older harvest versions treated those as invalid fallback
158
+ records because they do not carry the MCP fallback fields directly.
159
+ """
160
+ if not isinstance(event, dict) or not _looks_like_structured_experience(event):
161
+ return event, None
162
+
163
+ task = event.get("task") if isinstance(event.get("task"), dict) else {}
164
+ context = event.get("context") if isinstance(event.get("context"), dict) else {}
165
+ quality = event.get("quality") if isinstance(event.get("quality"), dict) else {}
166
+
167
+ event_type = _first_string(event.get("event_type"), "task_completed")
168
+ tool = _first_string(event.get("tool"), event.get("agent"), task.get("agent"), "unknown")
169
+ task_type = _first_string(
170
+ event.get("task_type"),
171
+ task.get("task_type"),
172
+ task.get("type"),
173
+ "unknown",
174
+ )
175
+ summary = _first_string(
176
+ event.get("summary"),
177
+ task.get("goal"),
178
+ event.get("goal"),
179
+ event.get("title"),
180
+ f"{event_type} event from {tool}",
181
+ )
182
+ paths = (
183
+ _string_list(event.get("paths"))
184
+ or _string_list(task.get("paths"))
185
+ or _string_list(task.get("files"))
186
+ or _string_list(context.get("paths"))
187
+ or _string_list(context.get("files"))
188
+ )
189
+
190
+ normalized = {
191
+ "schema": 1,
192
+ "event_id": _first_string(event.get("event_id"), event.get("id")),
193
+ "timestamp": _first_string(event.get("timestamp"), event.get("ts")),
194
+ "event_type": event_type,
195
+ "tool": tool,
196
+ "task_type": task_type,
197
+ "summary": summary,
198
+ "paths": paths,
199
+ "ci_passed": _legacy_ci_passed(event, task, context, quality),
200
+ "source": _first_string(event.get("source"), "experience-pipeline-compat"),
201
+ }
202
+ return normalized, "compatible structured experience event (normalized to harvest schema)"
203
+
204
+
205
+ def _append_event_or_diagnostic(
206
+ event: object,
207
+ rel_path: str,
208
+ events: list[dict],
209
+ diagnostics: list[dict],
210
+ ) -> None:
211
+ normalized, compatibility_reason = _normalize_structured_experience(event)
212
+ issues = _validate_event(normalized)
213
+ if issues:
214
+ diagnostics.append(
215
+ _diagnostic(
216
+ rel_path,
217
+ "skipped",
218
+ "; ".join(issues),
219
+ category="validation",
220
+ )
221
+ )
222
+ return
223
+
224
+ events.append(normalized)
225
+ diagnostics.append(
226
+ _diagnostic(
227
+ rel_path,
228
+ "kept",
229
+ compatibility_reason or "valid experience event",
230
+ category="compatibility" if compatibility_reason else "discovery",
231
+ )
232
+ )
233
+
234
+
235
+ def _discover_events(target: pathlib.Path) -> tuple[list[dict], list[dict], int]:
236
+ """Walk ai/experience/events + ai/experience/outbox; return valid events plus explain records.
237
+
238
+ Note: `0dai task run` writes events to `ai/experience/outbox/` while
239
+ `record_experience` MCP tool writes to `ai/experience/events/`. Both are
240
+ legitimate event sources; harvest scans both so the smoke-test pipeline
241
+ (init → task run → harvest → candidate) works end-to-end. Mirrors the
242
+ pattern in `scripts/score_experience.py:86` which reads both directories.
243
+ """
244
+ experience_root = target / "ai" / "experience"
245
+ event_dirs = [
246
+ experience_root / "events",
247
+ experience_root / "outbox",
248
+ ]
249
+ existing_dirs = [d for d in event_dirs if d.is_dir()]
250
+ if not existing_dirs:
251
+ return (
252
+ [],
253
+ [
254
+ _diagnostic(
255
+ "ai/experience/events/",
256
+ "skipped",
257
+ "directory missing (and outbox/ also missing)",
258
+ )
259
+ ],
260
+ 0,
261
+ )
262
+
263
+ events: list[dict] = []
264
+ diagnostics: list[dict] = []
265
+ walked: set[str] = set()
266
+
267
+ iter_paths = sorted(p for d in existing_dirs for p in d.iterdir())
268
+ for path in iter_paths:
269
+ rel_path = _rel(target, path)
270
+ walked.add(rel_path)
271
+
272
+ if path.name in IGNORED_EVENT_FILENAMES:
273
+ diagnostics.append(
274
+ _diagnostic(rel_path, "skipped", "template example file")
275
+ )
276
+ continue
277
+ if path.is_dir():
278
+ diagnostics.append(_diagnostic(rel_path, "skipped", "directory"))
279
+ continue
280
+ if path.suffix not in {".json", ".jsonl"}:
281
+ diagnostics.append(
282
+ _diagnostic(rel_path, "skipped", "unsupported extension")
283
+ )
284
+ continue
285
+
286
+ try:
287
+ raw = path.read_text("utf-8")
288
+ except OSError as exc:
289
+ diagnostics.append(
290
+ _diagnostic(rel_path, "skipped", f"unreadable: {exc}", category="io")
291
+ )
292
+ continue
293
+
294
+ if path.suffix == ".json":
295
+ try:
296
+ event = json.loads(raw)
297
+ except json.JSONDecodeError as exc:
298
+ diagnostics.append(
299
+ _diagnostic(
300
+ rel_path,
301
+ "skipped",
302
+ f"invalid JSON: {exc.msg}",
303
+ category="parse",
304
+ )
305
+ )
306
+ continue
307
+ _append_event_or_diagnostic(event, rel_path, events, diagnostics)
308
+ continue
309
+
310
+ nonempty_lines = 0
311
+ for idx, raw_line in enumerate(raw.splitlines(), start=1):
312
+ line = raw_line.strip()
313
+ if not line:
314
+ continue
315
+ nonempty_lines += 1
316
+ line_path = f"{rel_path}:{idx}"
317
+ try:
318
+ event = json.loads(line)
319
+ except json.JSONDecodeError as exc:
320
+ diagnostics.append(
321
+ _diagnostic(
322
+ line_path,
323
+ "skipped",
324
+ f"invalid JSON: {exc.msg}",
325
+ category="parse",
326
+ )
327
+ )
328
+ continue
329
+ _append_event_or_diagnostic(event, line_path, events, diagnostics)
330
+ if nonempty_lines == 0:
331
+ diagnostics.append(_diagnostic(rel_path, "skipped", "empty JSONL file"))
332
+
333
+ return events, diagnostics, len(walked)
334
+
335
+
336
+ def _load_events(target: pathlib.Path) -> list[dict]:
337
+ """Load all valid experience events (JSON files + JSONL lines)."""
338
+ events, _diagnostics, _walked = _discover_events(target)
339
+ return events
340
+
341
+
342
+ def _group_events(events: list[dict]) -> dict[str, list[dict]]:
343
+ """Group events by (tool, task_type) pattern for candidate creation."""
344
+ groups: dict[str, list[dict]] = defaultdict(list)
345
+ for event in events:
346
+ tool = event.get("tool", "unknown")
347
+ task_type = event.get("task_type", "unknown")
348
+ key = f"{tool}-{task_type}"
349
+ groups[key].append(event)
350
+ return dict(groups)
351
+
352
+
353
+ def _create_candidate(
354
+ key: str, events: list[dict], candidates_dir: pathlib.Path, *, dry_run: bool = False
355
+ ) -> pathlib.Path | None:
356
+ """Create a candidate markdown file from grouped events."""
357
+ if len(events) < 1:
358
+ return None
359
+
360
+ tool = events[0].get("tool", "unknown")
361
+ task_type = events[0].get("task_type", "unknown")
362
+ summaries = [e.get("summary", "") for e in events if e.get("summary")]
363
+ paths_seen: set[str] = set()
364
+ for e in events:
365
+ for p in e.get("paths", []):
366
+ paths_seen.add(p)
367
+ ci_passed = sum(1 for e in events if e.get("ci_passed", False))
368
+ event_ids = [e.get("event_id", "") for e in events if e.get("event_id")]
369
+
370
+ candidate_id = f"candidate.{key}"
371
+ filename = f"{candidate_id}.md"
372
+ dest = candidates_dir / filename
373
+
374
+ # Don't overwrite existing candidates — append new sources
375
+ if dest.is_file():
376
+ return None
377
+
378
+ today = time.strftime("%Y-%m-%d", time.gmtime())
379
+ scope_paths = sorted(paths_seen)[:10]
380
+
381
+ confidence = (
382
+ "high"
383
+ if len(events) >= HIGH_CONFIDENCE_EVENTS
384
+ else "medium" if len(events) >= MEDIUM_CONFIDENCE_EVENTS else "low"
385
+ )
386
+
387
+ content = f"""---
388
+ id: {candidate_id}
389
+ type: skill
390
+ status: candidate
391
+ tool: {tool}
392
+ task_type: {task_type}
393
+ confidence: {confidence}
394
+ sources: [{', '.join(event_ids[:10])}]
395
+ scope_paths: {json.dumps(scope_paths)}
396
+ created: {today}
397
+ ---
398
+
399
+ # Candidate: {tool} — {task_type}
400
+
401
+ ## Pattern
402
+
403
+ Agent `{tool}` performed `{task_type}` tasks {len(events)} time(s).
404
+ CI passed: {ci_passed}/{len(events)}.
405
+
406
+ ## Evidence
407
+
408
+ {chr(10).join(f'- {s[:200]}' for s in summaries[:5])}
409
+
410
+ ## Affected Files
411
+
412
+ {chr(10).join(f'- `{p}`' for p in scope_paths[:10])}
413
+ """
414
+
415
+ if not dry_run:
416
+ dest.parent.mkdir(parents=True, exist_ok=True)
417
+ dest.write_text(content, "utf-8")
418
+ return dest
419
+
420
+
421
+ def harvest(target: pathlib.Path, *, dry_run: bool = False) -> dict:
422
+ """Main harvest: events → candidates."""
423
+ events, diagnostics, files_walked = _discover_events(target)
424
+ events_rejected = _count_rejections(diagnostics)
425
+ if not events:
426
+ return {
427
+ "harvested": 0,
428
+ "events_read": 0,
429
+ "events_rejected": events_rejected,
430
+ "files_walked": files_walked,
431
+ "candidates_created": [],
432
+ "diagnostics": diagnostics,
433
+ }
434
+
435
+ groups = _group_events(events)
436
+ candidates_dir = target / "ai" / "experience" / "candidates"
437
+ created: list[str] = []
438
+
439
+ for key, group_events in groups.items():
440
+ path = _create_candidate(key, group_events, candidates_dir, dry_run=dry_run)
441
+ if path:
442
+ created.append(str(path.relative_to(target)))
443
+
444
+ return {
445
+ "harvested": len(created),
446
+ "events_read": len(events),
447
+ "events_rejected": events_rejected,
448
+ "files_walked": files_walked,
449
+ "groups": len(groups),
450
+ "candidates_created": created,
451
+ "diagnostics": diagnostics,
452
+ }
453
+
454
+
455
+ def _count_rejections(diagnostics: list[dict]) -> int:
456
+ return sum(1 for item in diagnostics if item.get("category") in {"parse", "validation", "io"})
457
+
458
+
459
+ def _print_explain(diagnostics: list[dict]) -> None:
460
+ for item in diagnostics:
461
+ print(
462
+ f"[harvest:explain] {item['path']} "
463
+ f"{item['status']}: {item['reason']}"
464
+ )
465
+
466
+
467
+ def _print_rejections(diagnostics: list[dict], *, limit: int = 5) -> None:
468
+ rejected = [
469
+ item
470
+ for item in diagnostics
471
+ if item.get("category") in {"parse", "validation", "io"}
472
+ ]
473
+ if not rejected:
474
+ return
475
+ print(f"[harvest] skipped {len(rejected)} invalid event(s):")
476
+ for item in rejected[:limit]:
477
+ print(f" {item['path']}: {item['reason']}")
478
+ if len(rejected) > limit:
479
+ print(" run with --explain for the full file list")
480
+
481
+
482
+ def main(argv: list[str] | None = None) -> int:
483
+ parser = argparse.ArgumentParser(description="Harvest fallback experience events.")
484
+ parser.add_argument("--target", default=".")
485
+ parser.add_argument(
486
+ "--explain",
487
+ action="store_true",
488
+ help="List every walked file/line and why it was kept or skipped.",
489
+ )
490
+ parser.add_argument("--dry-run", action="store_true", help="Report candidates without writing them.")
491
+ parser.add_argument("--json", action="store_true", help="Emit machine-readable harvest results.")
492
+ args = parser.parse_args(argv)
493
+
494
+ target = pathlib.Path(args.target).resolve()
495
+ result = harvest(target, dry_run=args.dry_run)
496
+
497
+ if args.json:
498
+ print(json.dumps(result, indent=2, ensure_ascii=False))
499
+ return 0
500
+
501
+ if args.explain:
502
+ _print_explain(result["diagnostics"])
503
+
504
+ _print_rejections(result["diagnostics"])
505
+
506
+ if result["harvested"] == 0:
507
+ if result["events_read"] == 0:
508
+ print("[harvest] no valid events found in ai/experience/events/")
509
+ print(" Record events with: 0dai MCP tool record_experience")
510
+ else:
511
+ print(f"[harvest] {result['events_read']} event(s) read, no new candidates to create")
512
+ print(" Existing candidates already cover these patterns")
513
+ else:
514
+ for path in result["candidates_created"]:
515
+ prefix = "[harvest] would create" if args.dry_run else "[harvest] created"
516
+ print(f"{prefix} {path}")
517
+ print(f"\n{result['harvested']} candidate(s) created from {result['events_read']} event(s)")
518
+ print("Next: run 'python3 scripts/score_experience.py' to score and promote")
519
+ return 0
520
+
521
+
522
+ if __name__ == "__main__":
523
+ raise SystemExit(main())
@@ -12,17 +12,23 @@ try {
12
12
 
13
13
  const pkg = require("../package.json");
14
14
  const v = pkg.version || "?";
15
+ const innerWidth = 41;
16
+ const horizontal = "\u2500".repeat(innerWidth);
17
+ const boxLine = (text = "") => {
18
+ const clipped = String(text).slice(0, innerWidth);
19
+ return " \u2502" + clipped.padEnd(innerWidth) + "\u2502";
20
+ };
15
21
 
16
22
  console.log("");
17
- console.log(" \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
18
- console.log(" \u2502 0dai installed successfully! v" + v.padEnd(10) + "\u2502");
19
- console.log(" \u2502 \u2502");
20
- console.log(" \u2502 Get started: \u2502");
21
- console.log(" \u2502 cd your-project \u2502");
22
- console.log(" \u2502 0dai init \u2502");
23
- console.log(" \u2502 \u2502");
24
- console.log(" \u2502 Docs: https://0dai.dev/docs \u2502");
25
- console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
23
+ console.log(" \u250c" + horizontal + "\u2510");
24
+ console.log(boxLine(" 0dai installed successfully! v" + v));
25
+ console.log(boxLine());
26
+ console.log(boxLine(" Get started:"));
27
+ console.log(boxLine(" cd your-project"));
28
+ console.log(boxLine(" 0dai init"));
29
+ console.log(boxLine());
30
+ console.log(boxLine(" Docs: https://0dai.dev/docs"));
31
+ console.log(" \u2514" + horizontal + "\u2518");
26
32
  console.log("");
27
33
  } catch (_) {
28
34
  // Never fail