adelie-ai 0.3.5 → 0.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.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <a href="https://www.npmjs.com/package/adelie-ai"><img src="https://img.shields.io/npm/v/adelie-ai?style=flat-square&logo=npm&color=CB3837" alt="npm version" /></a>
14
14
  <img src="https://img.shields.io/badge/python-3.10+-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python" />
15
15
  <img src="https://img.shields.io/badge/LLM-Gemini%20│%20Ollama-FF6F00?style=flat-square" alt="LLM" />
16
- <img src="https://img.shields.io/badge/tests-748%20passing-2EA043?style=flat-square" alt="Tests" />
16
+ <img src="https://img.shields.io/badge/tests-750%20passing-2EA043?style=flat-square" alt="Tests" />
17
17
  <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License" /></a>
18
18
  </p>
19
19
 
@@ -35,7 +35,7 @@
35
35
  Adelie is an autonomous AI orchestrator that plans, codes, reviews, tests, deploys, and evolves software projects through a coordinated multi-agent loop. It ships as a single CLI (`npm install -g adelie-ai`) and requires only an LLM provider — no cloud backend, no account.
36
36
 
37
37
  ```
38
- (o_ Adelie v0.3.0
38
+ (o_ Adelie v0.3.5
39
39
  //\ gemini · gemini-2.5-pro
40
40
  V_/_ Phase: mid_1 | 🛡️3 📡🟢 🧠12/5
41
41
  ```
@@ -12,4 +12,4 @@ def _get_version() -> str:
12
12
  except Exception:
13
13
  pass
14
14
  return "0.0.0"
15
- __version__ = "0.3.5"
15
+ __version__ = "0.3.7"
@@ -267,178 +267,229 @@ def _get_scaffolding_need() -> str:
267
267
  if not project_root.exists():
268
268
  return ""
269
269
 
270
- # Gather existing files for detection
271
- existing = set()
272
- for f in project_root.iterdir():
273
- if f.is_file():
274
- existing.add(f.name.lower())
275
- src_dir = project_root / "src"
276
- if src_dir.exists():
277
- for f in src_dir.iterdir():
278
- if f.is_file():
279
- existing.add(f"src/{f.name}".lower())
280
-
281
- # Define scaffolding checks per project type
282
270
  checks: list[dict] = []
283
271
 
284
- has_tsx = any(f.suffix in (".tsx", ".jsx") for f in project_root.rglob("*") if f.is_file())
285
- has_ts = any(f.suffix == ".ts" for f in project_root.rglob("*") if f.is_file())
286
- has_pkg = "package.json" in existing
287
-
288
- # ── Framework-aware scaffolding checks ────────────────────────────
289
- framework = _detect_framework(project_root)
290
-
291
- if framework == "nextjs":
292
- # Next.js needs: package.json, next.config.*, src/app/layout.tsx or pages/
293
- entry_files = {
294
- "package.json": "Node.js dependencies and scripts (npm run dev, npm run build)",
295
- }
296
- # Check for app router or pages router
297
- has_app_router = (
298
- (src_dir / "app" / "layout.tsx").exists()
299
- or (src_dir / "app" / "layout.ts").exists()
300
- or (src_dir / "app" / "layout.jsx").exists()
301
- or (project_root / "app" / "layout.tsx").exists()
302
- )
303
- has_pages_router = (
304
- (src_dir / "pages").exists()
305
- or (project_root / "pages").exists()
306
- )
307
- if not has_app_router and not has_pages_router:
308
- entry_files["src/app/layout.tsx"] = "Next.js App Router root layout"
309
- entry_files["src/app/page.tsx"] = "Next.js App Router home page"
310
-
311
- for fname, desc in entry_files.items():
312
- path = project_root / fname
313
- if not path.exists():
314
- checks.append({"file": fname, "desc": desc})
315
-
316
- elif framework == "nuxt":
317
- entry_files = {
318
- "package.json": "Node.js dependencies and scripts",
319
- }
320
- has_app_vue = (project_root / "app.vue").exists()
321
- has_pages = (project_root / "pages").exists()
322
- if not has_app_vue and not has_pages:
323
- entry_files["app.vue"] = "Nuxt root App component"
324
-
325
- for fname, desc in entry_files.items():
326
- path = project_root / fname
327
- if not path.exists():
328
- checks.append({"file": fname, "desc": desc})
329
-
330
- elif framework == "sveltekit":
331
- entry_files = {
332
- "package.json": "Node.js dependencies and scripts",
333
- }
334
- has_routes = (src_dir / "routes").exists()
335
- if not has_routes:
336
- entry_files["src/routes/+page.svelte"] = "SvelteKit root page"
337
-
338
- for fname, desc in entry_files.items():
339
- path = project_root / fname
340
- if not path.exists():
341
- checks.append({"file": fname, "desc": desc})
342
-
343
- elif framework in ("remix", "angular"):
344
- # Minimal checks — just package.json
345
- if not (project_root / "package.json").exists():
346
- checks.append({"file": "package.json", "desc": "Node.js dependencies and scripts"})
347
-
348
- elif has_tsx or has_ts or has_pkg:
349
- # Default: Vite / vanilla React project (original behavior)
350
- entry_files = {
351
- "index.html": "Vite entry point — must reference src/main.tsx",
352
- "package.json": "Node.js dependencies and scripts (npm run build, npm run dev)",
353
- "tsconfig.json": "TypeScript compiler configuration",
354
- }
355
- # Check src/main.tsx or src/main.ts
356
- has_main = (
357
- (src_dir / "main.tsx").exists()
358
- or (src_dir / "main.ts").exists()
359
- or (src_dir / "main.jsx").exists()
360
- or (src_dir / "main.js").exists()
361
- or (src_dir / "index.tsx").exists()
362
- or (src_dir / "index.ts").exists()
363
- )
364
- if not has_main:
365
- entry_files["src/main.tsx"] = "React root render — ReactDOM.createRoot + App import"
366
-
367
- has_vite_cfg = (
368
- (project_root / "vite.config.ts").exists()
369
- or (project_root / "vite.config.js").exists()
370
- )
371
- if not has_vite_cfg:
372
- entry_files["vite.config.ts"] = "Vite build configuration with React plugin"
373
-
374
- for fname, desc in entry_files.items():
375
- path = project_root / fname
376
- if not path.exists():
377
- checks.append({"file": fname, "desc": desc})
378
-
379
- # Python project detection
380
- has_py = any(f.suffix == ".py" for f in project_root.rglob("*") if f.is_file())
381
- if has_py and not (has_tsx or has_ts or has_pkg):
382
- py_entries = {
383
- "requirements.txt": "Python dependencies",
384
- }
385
- for fname, desc in py_entries.items():
386
- if not (project_root / fname).exists():
387
- checks.append({"file": fname, "desc": desc})
388
-
389
- # ── tsconfig.json deep validation ─────────────────────────────────
390
- tsconfig_path = project_root / "tsconfig.json"
391
- if tsconfig_path.exists():
272
+ # Detect workspaces
273
+ workspaces = []
274
+ pkg_path = project_root / "package.json"
275
+ if pkg_path.exists():
392
276
  try:
393
- # Strip comments (tsconfig allows // comments)
394
- raw = tsconfig_path.read_text(encoding="utf-8")
395
- # Remove single-line comments
396
- import re as _re
397
- cleaned = _re.sub(r'//.*$', '', raw, flags=_re.MULTILINE)
398
- tsconfig = json.loads(cleaned)
399
-
400
- # Check "references" — e.g. [{"path": "./tsconfig.node.json"}]
401
- for ref in tsconfig.get("references", []):
402
- ref_path = ref.get("path", "")
403
- if ref_path:
404
- ref_file = project_root / ref_path
405
- # If path is a directory, tsconfig.json is implied
406
- if not ref_file.exists() and not ref_file.with_suffix(".json").exists():
407
- checks.append({
408
- "file": ref_path,
409
- "desc": f"Referenced by tsconfig.json — must exist or build fails (TS6053)",
410
- })
277
+ pkg = json.loads(pkg_path.read_text(encoding="utf-8"))
278
+ workspaces = pkg.get("workspaces", [])
279
+ if isinstance(workspaces, dict):
280
+ workspaces = workspaces.get("packages", [])
281
+ except Exception:
282
+ pass
411
283
 
412
- # Check "extends" e.g. "./tsconfig.node.json"
413
- extends = tsconfig.get("extends", "")
414
- if extends and not extends.startswith("@"):
415
- ext_file = project_root / extends
416
- if not ext_file.exists():
417
- checks.append({
418
- "file": extends,
419
- "desc": f"Extended by tsconfig.json — must exist or build fails",
420
- })
421
-
422
- # Check "compilerOptions.types" → need @types/* in package.json
423
- types_list = tsconfig.get("compilerOptions", {}).get("types", [])
424
- if types_list and has_pkg:
425
- try:
426
- pkg = json.loads((project_root / "package.json").read_text(encoding="utf-8"))
427
- all_deps = set(pkg.get("dependencies", {}).keys())
428
- all_deps |= set(pkg.get("devDependencies", {}).keys())
284
+ # If no explicit workspaces, check common folders
285
+ if not workspaces:
286
+ for folder in ["client", "server", "frontend", "backend"]:
287
+ if (project_root / folder).exists() and (project_root / folder).is_dir():
288
+ workspaces.append(folder)
289
+
290
+ # We will perform check on each workspace root, or on the project_root if no workspaces
291
+ targets = []
292
+ if workspaces:
293
+ for ws in workspaces:
294
+ # simple wildcards resolution
295
+ if "*" in ws or "?" in ws:
296
+ import glob
297
+ matched = glob.glob(str(project_root / ws))
298
+ for m in matched:
299
+ targets.append(Path(m))
300
+ else:
301
+ ws_path = project_root / ws
302
+ if ws_path.exists() and ws_path.is_dir():
303
+ targets.append(ws_path)
304
+
305
+ if not targets:
306
+ targets = [project_root]
307
+
308
+ for target in targets:
309
+ # Resolve path relative to project_root to report
310
+ rel_prefix = ""
311
+ if target != project_root:
312
+ rel_prefix = f"{target.relative_to(project_root).as_posix()}/"
313
+
314
+ existing = set()
315
+ for f in target.iterdir():
316
+ if f.is_file():
317
+ existing.add(f.name.lower())
318
+ src_dir = target / "src"
319
+ if src_dir.exists() and src_dir.is_dir():
320
+ for f in src_dir.iterdir():
321
+ if f.is_file():
322
+ existing.add(f"src/{f.name}".lower())
323
+
324
+ # Check project type for this target
325
+ has_tsx = any(f.suffix in (".tsx", ".jsx") for f in target.rglob("*") if f.is_file())
326
+ has_ts = any(f.suffix == ".ts" for f in target.rglob("*") if f.is_file())
327
+ has_pkg = "package.json" in existing
328
+
329
+ framework = _detect_framework(target)
330
+
331
+ # If it is a server-only workspace in a monorepo, only check package.json and tsconfig.json
332
+ is_server_ws = target != project_root and any(x in target.name.lower() for x in ["server", "backend", "api"])
333
+
334
+ if is_server_ws:
335
+ server_files = {
336
+ "package.json": "Node.js dependencies and scripts",
337
+ }
338
+ if has_ts:
339
+ server_files["tsconfig.json"] = "TypeScript compiler configuration"
340
+ # Check for src/index.ts or src/main.ts
341
+ has_server_main = (
342
+ (src_dir / "index.ts").exists()
343
+ or (src_dir / "main.ts").exists()
344
+ or (src_dir / "server.ts").exists()
345
+ )
346
+ if not has_server_main:
347
+ server_files["src/index.ts"] = "Server entry point"
348
+
349
+ for fname, desc in server_files.items():
350
+ path = target / fname
351
+ if not path.exists():
352
+ checks.append({"file": f"{rel_prefix}{fname}", "desc": desc})
353
+
354
+ elif framework == "nextjs":
355
+ entry_files = {
356
+ "package.json": "Node.js dependencies and scripts (npm run dev, npm run build)",
357
+ }
358
+ has_app_router = (
359
+ (src_dir / "app" / "layout.tsx").exists()
360
+ or (src_dir / "app" / "layout.ts").exists()
361
+ or (src_dir / "app" / "layout.jsx").exists()
362
+ or (target / "app" / "layout.tsx").exists()
363
+ )
364
+ has_pages_router = (
365
+ (src_dir / "pages").exists()
366
+ or (target / "pages").exists()
367
+ )
368
+ if not has_app_router and not has_pages_router:
369
+ entry_files["src/app/layout.tsx"] = "Next.js App Router root layout"
370
+ entry_files["src/app/page.tsx"] = "Next.js App Router home page"
371
+
372
+ for fname, desc in entry_files.items():
373
+ path = target / fname
374
+ if not path.exists():
375
+ checks.append({"file": f"{rel_prefix}{fname}", "desc": desc})
376
+
377
+ elif framework == "nuxt":
378
+ entry_files = {
379
+ "package.json": "Node.js dependencies and scripts",
380
+ }
381
+ has_app_vue = (target / "app.vue").exists()
382
+ has_pages = (target / "pages").exists()
383
+ if not has_app_vue and not has_pages:
384
+ entry_files["app.vue"] = "Nuxt root App component"
385
+
386
+ for fname, desc in entry_files.items():
387
+ path = target / fname
388
+ if not path.exists():
389
+ checks.append({"file": f"{rel_prefix}{fname}", "desc": desc})
390
+
391
+ elif framework == "sveltekit":
392
+ entry_files = {
393
+ "package.json": "Node.js dependencies and scripts",
394
+ }
395
+ has_routes = (src_dir / "routes").exists()
396
+ if not has_routes:
397
+ entry_files["src/routes/+page.svelte"] = "SvelteKit root page"
398
+
399
+ for fname, desc in entry_files.items():
400
+ path = target / fname
401
+ if not path.exists():
402
+ checks.append({"file": f"{rel_prefix}{fname}", "desc": desc})
403
+
404
+ elif framework in ("remix", "angular"):
405
+ if not (target / "package.json").exists():
406
+ checks.append({"file": f"{rel_prefix}package.json", "desc": "Node.js dependencies and scripts"})
407
+
408
+ elif has_tsx or has_ts or has_pkg:
409
+ entry_files = {
410
+ "index.html": "Vite entry point — must reference src/main.tsx",
411
+ "package.json": "Node.js dependencies and scripts (npm run build, npm run dev)",
412
+ "tsconfig.json": "TypeScript compiler configuration",
413
+ }
414
+ has_main = (
415
+ (src_dir / "main.tsx").exists()
416
+ or (src_dir / "main.ts").exists()
417
+ or (src_dir / "main.jsx").exists()
418
+ or (src_dir / "main.js").exists()
419
+ or (src_dir / "index.tsx").exists()
420
+ or (src_dir / "index.ts").exists()
421
+ )
422
+ if not has_main:
423
+ entry_files["src/main.tsx"] = "React root render — ReactDOM.createRoot + App import"
429
424
 
430
- for type_name in types_list:
431
- types_pkg = f"@types/{type_name}"
432
- if types_pkg not in all_deps:
425
+ has_vite_cfg = (
426
+ (target / "vite.config.ts").exists()
427
+ or (target / "vite.config.js").exists()
428
+ )
429
+ if not has_vite_cfg:
430
+ entry_files["vite.config.ts"] = "Vite build configuration with React plugin"
431
+
432
+ for fname, desc in entry_files.items():
433
+ path = target / fname
434
+ if not path.exists():
435
+ checks.append({"file": f"{rel_prefix}{fname}", "desc": desc})
436
+
437
+ # Python project detection
438
+ has_py = any(f.suffix == ".py" for f in target.rglob("*") if f.is_file())
439
+ if has_py and not (has_tsx or has_ts or has_pkg):
440
+ py_entries = {
441
+ "requirements.txt": "Python dependencies",
442
+ }
443
+ for fname, desc in py_entries.items():
444
+ if not (target / fname).exists():
445
+ checks.append({"file": f"{rel_prefix}{fname}", "desc": desc})
446
+
447
+ # tsconfig.json deep validation
448
+ tsconfig_path = target / "tsconfig.json"
449
+ if tsconfig_path.exists():
450
+ try:
451
+ raw = tsconfig_path.read_text(encoding="utf-8")
452
+ import re as _re
453
+ cleaned = _re.sub(r'//.*$', '', raw, flags=_re.MULTILINE)
454
+ tsconfig = json.loads(cleaned)
455
+
456
+ for ref in tsconfig.get("references", []):
457
+ ref_path = ref.get("path", "")
458
+ if ref_path:
459
+ ref_file = target / ref_path
460
+ if not ref_file.exists() and not ref_file.with_suffix(".json").exists():
433
461
  checks.append({
434
- "file": f"package.json (add {types_pkg})",
435
- "desc": f"tsconfig requires types '{type_name}' but {types_pkg} not in dependencies (TS2688)",
462
+ "file": f"{rel_prefix}{ref_path}",
463
+ "desc": f"Referenced by tsconfig.json must exist or build fails (TS6053)",
436
464
  })
437
- except Exception:
438
- pass
439
465
 
440
- except Exception:
441
- pass # tsconfig parse error — skip validation
466
+ extends = tsconfig.get("extends", "")
467
+ if extends and not extends.startswith("@"):
468
+ ext_file = target / extends
469
+ if not ext_file.exists():
470
+ checks.append({
471
+ "file": f"{rel_prefix}{extends}",
472
+ "desc": f"Extended by tsconfig.json — must exist or build fails",
473
+ })
474
+
475
+ types_list = tsconfig.get("compilerOptions", {}).get("types", [])
476
+ if types_list and has_pkg:
477
+ try:
478
+ pkg = json.loads((target / "package.json").read_text(encoding="utf-8"))
479
+ all_deps = set(pkg.get("dependencies", {}).keys())
480
+ all_deps |= set(pkg.get("devDependencies", {}).keys())
481
+
482
+ for type_name in types_list:
483
+ types_pkg = f"@types/{type_name}"
484
+ if types_pkg not in all_deps:
485
+ checks.append({
486
+ "file": f"{rel_prefix}package.json (add {types_pkg})",
487
+ "desc": f"tsconfig requires types '{type_name}' but {types_pkg} not in dependencies (TS2688)",
488
+ })
489
+ except Exception:
490
+ pass
491
+ except Exception:
492
+ pass
442
493
 
443
494
  if not checks:
444
495
  return ""
package/adelie/cli.py CHANGED
@@ -87,6 +87,8 @@ def main() -> None:
87
87
  )
88
88
  parser.add_argument("-v", "--version", action="version",
89
89
  version=f"adelie {__version__}")
90
+ parser.add_argument("--update", action="store_true",
91
+ help="Check npm registry and update Adelie to the latest version")
90
92
  subparsers = parser.add_subparsers(dest="command", help="Available commands")
91
93
 
92
94
  # ── help ──────────────────────────────────────────────────────────────────
@@ -279,6 +281,17 @@ def main() -> None:
279
281
  # ── Splash screen (no command) ────────────────────────────────────────────
280
282
  args = parser.parse_args()
281
283
 
284
+ if getattr(args, "update", False):
285
+ from adelie.updater import check_for_update, do_update
286
+ console.print("[cyan]🔍 Checking for updates...[/cyan]")
287
+ up = check_for_update(__version__, timeout=4.0)
288
+ if up:
289
+ console.print(f"[bold yellow]🐧 A new version has been found! (v{up['current']} → v{up['latest']})[/bold yellow]")
290
+ sys.exit(do_update())
291
+ else:
292
+ console.print(f"[green]✅ You are already running the latest version! (v{__version__})[/green]")
293
+ sys.exit(0)
294
+
282
295
  if not args.command:
283
296
  art = Text(_PENGUIN, no_wrap=True)
284
297
  info = (
@@ -289,6 +302,16 @@ def main() -> None:
289
302
  f" [dim]adelie <command> --help[/dim]\n"
290
303
  )
291
304
  console.print(Columns([Padding(art, (0, 1)), info]))
305
+
306
+ # Check for updates with a fast 1.0s timeout to avoid blocking startup
307
+ try:
308
+ from adelie.updater import check_for_update, format_update_notice
309
+ up = check_for_update(__version__, timeout=1.0)
310
+ if up:
311
+ console.print(format_update_notice(up["current"], up["latest"]))
312
+ except Exception:
313
+ pass
314
+
292
315
  parser.print_help()
293
316
  return
294
317
 
@@ -229,6 +229,17 @@ class AdelieApp:
229
229
  workspace=str(cfg.PROJECT_ROOT),
230
230
  )
231
231
 
232
+ # Start async update check in the background
233
+ try:
234
+ from adelie import __version__
235
+ from adelie.updater import check_for_update_async, format_update_notice
236
+ def _update_callback(result):
237
+ if result:
238
+ self._real_console.print(format_update_notice(result["current"], result["latest"]))
239
+ check_for_update_async(__version__, _update_callback)
240
+ except Exception:
241
+ pass
242
+
232
243
  # Start dashboard server FIRST (so _setup_logger can reference it)
233
244
  self._start_dashboard(cfg)
234
245
 
package/adelie/updater.py CHANGED
@@ -62,11 +62,11 @@ def format_update_notice(current: str, latest: str) -> str:
62
62
  """Return a cute, Rich-markup update notification string."""
63
63
  return (
64
64
  f"\n"
65
- f" [bold yellow]꼬르르... 🐧 버전이 나왔어요![/bold yellow]\n"
65
+ f" [bold yellow]Brrr... 🐧 A new version is available![/bold yellow]\n"
66
66
  f" [dim]v{current}[/dim] → [bold cyan]v{latest}[/bold cyan] "
67
- f"[dim](최신 Adelie 펭귄이 기다리고 있어요)[/dim]\n"
67
+ f"[dim](A fresh Adelie penguin is waiting for you)[/dim]\n"
68
68
  f"\n"
69
- f" [bold]adelie --update[/bold] [dim]로 업데이트할 있어요 ✨[/dim]\n"
69
+ f" Run [bold]adelie --update[/bold] to update now ✨\n"
70
70
  )
71
71
 
72
72
 
@@ -80,7 +80,7 @@ def do_update() -> int:
80
80
  from rich.console import Console
81
81
 
82
82
  console = Console()
83
- console.print("\n[bold cyan]🐧 Adelie 업데이트 중...[/bold cyan]")
83
+ console.print("\n[bold cyan]🐧 Updating Adelie...[/bold cyan]")
84
84
  console.print("[dim]npm install -g adelie-ai@latest[/dim]\n")
85
85
 
86
86
  try:
@@ -89,13 +89,13 @@ def do_update() -> int:
89
89
  check=False,
90
90
  )
91
91
  if result.returncode == 0:
92
- console.print("\n[bold green]✅ 업데이트 완료! 가장 귀여운 버전이 됐어요 🐧✨[/bold green]")
93
- console.print("[dim]다시 실행: adelie --version[/dim]\n")
92
+ console.print("\n[bold green]✅ Update complete! You are now running the cutest version 🐧✨[/bold green]")
93
+ console.print("[dim]Check version: adelie --version[/dim]\n")
94
94
  else:
95
- console.print("\n[bold red]❌ 업데이트 실패했어요.[/bold red]")
96
- console.print("[dim]직접 실행해보세요: npm install -g adelie-ai@latest[/dim]\n")
95
+ console.print("\n[bold red]❌ Update failed.[/bold red]")
96
+ console.print("[dim]Try running: npm install -g adelie-ai@latest[/dim]\n")
97
97
  return result.returncode
98
98
  except FileNotFoundError:
99
- console.print("[bold red]❌ npm 찾을 없어요.[/bold red]")
100
- console.print("[dim]Node.js 설치한 다시 시도해주세요: https://nodejs.org[/dim]\n")
99
+ console.print("[bold red]❌ npm command not found.[/bold red]")
100
+ console.print("[dim]Please install Node.js and try again: https://nodejs.org[/dim]\n")
101
101
  return 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adelie-ai",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Adelie — Self-Communicating Autonomous AI Loop CLI",
5
5
  "bin": {
6
6
  "adelie": "bin/adelie.js"