@adia-ai/adia-ui-factory 0.2.2 → 0.2.4

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 (95) hide show
  1. package/.claude-plugin/plugin.json +12 -3
  2. package/.mcp.json +1 -1
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +24 -1
  5. package/bin/adia-scaffold +93 -13
  6. package/package.json +1 -1
  7. package/references/shell-embed.md +10 -0
  8. package/references/spa-architecture.md +7 -0
  9. package/skills/adia-ui-kit/CHANGELOG.md +41 -0
  10. package/skills/adia-ui-kit/SKILL.md +442 -0
  11. package/skills/adia-ui-kit/assets/figma-make/guidelines/Guidelines.md +87 -0
  12. package/skills/adia-ui-kit/assets/figma-make/guidelines/components.md +142 -0
  13. package/skills/adia-ui-kit/assets/figma-make/guidelines/styles.md +118 -0
  14. package/skills/adia-ui-kit/assets/figma-make/guidelines/tokens.md +120 -0
  15. package/skills/adia-ui-kit/assets/team-scaffold/archive-README.md +30 -0
  16. package/skills/adia-ui-kit/assets/team-scaffold/drafts-README.md +57 -0
  17. package/skills/adia-ui-kit/assets/team-scaffold/inbox-README.md +76 -0
  18. package/skills/adia-ui-kit/assets/team-scaffold/outbox-README.md +46 -0
  19. package/skills/adia-ui-kit/assets/team-scaffold/team-root.README.md +51 -0
  20. package/skills/adia-ui-kit/assets/team-scaffold/tickets-README.md +208 -0
  21. package/skills/adia-ui-kit/assets/team-scaffold/tickets-REGISTRY.md +81 -0
  22. package/skills/adia-ui-kit/assets/templates/AGENTS.template.md +128 -0
  23. package/skills/adia-ui-kit/assets/templates/CLAUDE.template.md +1 -0
  24. package/skills/adia-ui-kit/assets/templates/FEEDBACK-OMISSIONS-DEFENSE.template.md +150 -0
  25. package/skills/adia-ui-kit/assets/templates/FEEDBACK.template.md +213 -0
  26. package/skills/adia-ui-kit/assets/templates/RESPONSE.template.md +114 -0
  27. package/skills/adia-ui-kit/assets/templates/figma-make-guidelines.md +90 -0
  28. package/skills/adia-ui-kit/evals/adversarial-content-trust.json +83 -0
  29. package/skills/adia-ui-kit/evals/evals.json +55 -0
  30. package/skills/adia-ui-kit/evals/routing-corpus.json +121 -0
  31. package/skills/adia-ui-kit/evals/teach-routing-cases.json +92 -0
  32. package/skills/adia-ui-kit/references/capabilities.md +245 -0
  33. package/skills/adia-ui-kit/references/catalog-lookup.md +186 -0
  34. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v1.md +8 -0
  35. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v2.md +3585 -0
  36. package/skills/adia-ui-kit/references/changelog-archive/CHANGELOG-v3.md +1899 -0
  37. package/skills/adia-ui-kit/references/common-gotchas-consumer.md +522 -0
  38. package/skills/adia-ui-kit/references/component-literacy.md +291 -0
  39. package/skills/adia-ui-kit/references/composition.md +245 -0
  40. package/skills/adia-ui-kit/references/correction-loop.md +462 -0
  41. package/skills/adia-ui-kit/references/css-policy.md +343 -0
  42. package/skills/adia-ui-kit/references/decision-model.md +158 -0
  43. package/skills/adia-ui-kit/references/diagnostic-output.md +57 -0
  44. package/skills/adia-ui-kit/references/docs-catalog.md +239 -0
  45. package/skills/adia-ui-kit/references/exclusions.md +225 -0
  46. package/skills/adia-ui-kit/references/feedback-authoring.md +466 -0
  47. package/skills/adia-ui-kit/references/figma-make.md +244 -0
  48. package/skills/adia-ui-kit/references/flows-auth.md +307 -0
  49. package/skills/adia-ui-kit/references/flows-content.md +206 -0
  50. package/skills/adia-ui-kit/references/flows-data.md +348 -0
  51. package/skills/adia-ui-kit/references/flows-interaction.md +181 -0
  52. package/skills/adia-ui-kit/references/flows-shells.md +336 -0
  53. package/skills/adia-ui-kit/references/gap-detection.md +266 -0
  54. package/skills/adia-ui-kit/references/harness.md +284 -0
  55. package/skills/adia-ui-kit/references/loading-protocol.md +406 -0
  56. package/skills/adia-ui-kit/references/local-link.md +198 -0
  57. package/skills/adia-ui-kit/references/mcp.md +401 -0
  58. package/skills/adia-ui-kit/references/migration.md +116 -0
  59. package/skills/adia-ui-kit/references/mission.md +194 -0
  60. package/skills/adia-ui-kit/references/mode-routing.md +298 -0
  61. package/skills/adia-ui-kit/references/patterns-agent.md +223 -0
  62. package/skills/adia-ui-kit/references/patterns-data.md +1065 -0
  63. package/skills/adia-ui-kit/references/patterns-forms.md +223 -0
  64. package/skills/adia-ui-kit/references/patterns-layout.md +326 -0
  65. package/skills/adia-ui-kit/references/patterns-recipes.md +1416 -0
  66. package/skills/adia-ui-kit/references/patterns-shells.md +1097 -0
  67. package/skills/adia-ui-kit/references/patterns-traits.md +259 -0
  68. package/skills/adia-ui-kit/references/patterns-wiring.md +339 -0
  69. package/skills/adia-ui-kit/references/pev-rationale.md +137 -0
  70. package/skills/adia-ui-kit/references/reasoning-gate.md +146 -0
  71. package/skills/adia-ui-kit/references/reasoning-methodologies.md +112 -0
  72. package/skills/adia-ui-kit/references/recon.md +192 -0
  73. package/skills/adia-ui-kit/references/rendering-model.md +431 -0
  74. package/skills/adia-ui-kit/references/response-authoring.md +398 -0
  75. package/skills/adia-ui-kit/references/session-log-schema.md +203 -0
  76. package/skills/adia-ui-kit/references/session-log-template.md +118 -0
  77. package/skills/adia-ui-kit/references/setup.md +652 -0
  78. package/skills/adia-ui-kit/references/skill-hygiene.md +135 -0
  79. package/skills/adia-ui-kit/references/spec-to-ui-reasoning.md +394 -0
  80. package/skills/adia-ui-kit/references/state.md +229 -0
  81. package/skills/adia-ui-kit/references/teach-protocol.md +421 -0
  82. package/skills/adia-ui-kit/references/theme.md +142 -0
  83. package/skills/adia-ui-kit/references/version-check.md +69 -0
  84. package/skills/adia-ui-kit/scripts/audit-decision-trace.mjs +198 -0
  85. package/skills/adia-ui-kit/scripts/audit-kit-roster.mjs +366 -0
  86. package/skills/adia-ui-kit/scripts/changelog-archive.mjs +126 -0
  87. package/skills/adia-ui-kit/scripts/check-adia-versions.py +404 -0
  88. package/skills/adia-ui-kit/scripts/run-skill-evals.mjs +358 -0
  89. package/skills/adia-ui-kit/scripts/validate-session-log.mjs +242 -0
  90. package/skills/adia-ui-kit/skill.json +102 -0
  91. package/skills/adia-ui-migration/CHANGELOG.md +47 -0
  92. package/skills/adia-ui-migration/SKILL.md +381 -0
  93. package/skills/adia-ui-migration/skill.json +29 -0
  94. package/skills/adia-ui-spa/SKILL.md +1 -1
  95. package/skills/adia-ui-verify/SKILL.md +2 -2
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ check-adia-versions.py — verify your @adia-ai/* install is sane.
4
+
5
+ Run this from any consumer project (not just the monorepo). Walks three
6
+ sources and reports drift:
7
+
8
+ 1. Installed (node_modules/@adia-ai/<pkg>/package.json)
9
+ 2. Declared (your package.json `dependencies` + `devDependencies`)
10
+ 3. Latest published (npm view @adia-ai/<pkg> version — needs network)
11
+
12
+ Exits 0 when everything lines up. Exits 1 on drift. Exits 2 on
13
+ unrecoverable error (missing node_modules, malformed package.json).
14
+
15
+ Usage
16
+ -----
17
+ ./check-adia-versions.py # full check (network)
18
+ ./check-adia-versions.py --offline # skip npm view, lockstep only
19
+ ./check-adia-versions.py --json # machine-readable output
20
+ ./check-adia-versions.py --root . # explicit consumer root
21
+
22
+ Why this exists
23
+ ---------------
24
+ The 9 `@adia-ai/*` packages ship in lockstep — one stale install desyncs
25
+ the whole bundle. Plus the skill documents fixes that only apply at
26
+ specific versions (e.g. F-014/F-015: v0.6.7 ships broken absolute-path
27
+ imports in `@adia-ai/web-modules`, fix lands in v0.6.8). Without a
28
+ verification step, agents trust their installed snapshot blindly and
29
+ ship advice that's wrong for the user's actual install.
30
+
31
+ Output legend
32
+ -------------
33
+ ✓ ok installed == declared, and matches latest (or --offline)
34
+ 🔗 linked declared as `file:...`, `link:...`, or `workspace:*` —
35
+ local development link, not an npm version (informational)
36
+ ⚠ stale installed < latest published — `npm update` recommended
37
+ ⚠ split lockstep broken — packages disagree on version
38
+ ⚠ undeclared installed in node_modules but missing from package.json
39
+ ⚠ uninstalled declared but missing from node_modules — `npm install`
40
+ ✗ ahead installed > latest published — local source build /
41
+ pre-publish state (informational, not an error in this
42
+ monorepo where you may dev against unpublished cuts)
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import json
49
+ import re
50
+ import subprocess
51
+ import sys
52
+ from dataclasses import dataclass, field
53
+ from pathlib import Path
54
+ from typing import Optional
55
+
56
+
57
+ PACKAGE_NAMES = [
58
+ "@adia-ai/web-components",
59
+ "@adia-ai/web-modules",
60
+ "@adia-ai/llm",
61
+ "@adia-ai/a2ui-runtime",
62
+ "@adia-ai/a2ui-compose",
63
+ "@adia-ai/a2ui-corpus",
64
+ "@adia-ai/a2ui-mcp",
65
+ "@adia-ai/a2ui-retrieval",
66
+ "@adia-ai/a2ui-validator",
67
+ ]
68
+
69
+
70
+ @dataclass
71
+ class PackageStatus:
72
+ name: str
73
+ installed: Optional[str] = None
74
+ declared: Optional[str] = None # raw range from package.json
75
+ declared_normalized: Optional[str] = None # version stripped of ^~
76
+ latest: Optional[str] = None
77
+ symlinked: bool = False # node_modules/<name> is a symlink
78
+ issues: list[str] = field(default_factory=list)
79
+
80
+
81
+ def read_json(path: Path) -> Optional[dict]:
82
+ try:
83
+ return json.loads(path.read_text())
84
+ except (FileNotFoundError, json.JSONDecodeError):
85
+ return None
86
+
87
+
88
+ def is_link_spec(spec: Optional[str]) -> bool:
89
+ """Detect local-development link specs (file:, link:, workspace:)."""
90
+ if not spec:
91
+ return False
92
+ return spec.startswith(("file:", "link:", "workspace:", "portal:"))
93
+
94
+
95
+ def normalize_range(spec: str) -> str:
96
+ """Strip leading ^ ~ >= etc. from a semver range to get a version."""
97
+ if not spec:
98
+ return ""
99
+ # Link specs aren't semver — leave them alone for display
100
+ if is_link_spec(spec):
101
+ return spec
102
+ m = re.match(r"^[\^~><=\s]*([\d]+\.[\d]+\.[\d]+(?:-[\w.]+)?)", spec)
103
+ return m.group(1) if m else spec
104
+
105
+
106
+ def parse_semver(v: Optional[str]) -> tuple[int, int, int]:
107
+ if not v:
108
+ return (0, 0, 0)
109
+ m = re.match(r"^(\d+)\.(\d+)\.(\d+)", v)
110
+ if not m:
111
+ return (0, 0, 0)
112
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
113
+
114
+
115
+ def get_installed(root: Path, name: str) -> Optional[str]:
116
+ pkg = root / "node_modules" / name / "package.json"
117
+ data = read_json(pkg)
118
+ return data.get("version") if data else None
119
+
120
+
121
+ def is_symlinked(root: Path, name: str) -> bool:
122
+ """Check whether node_modules/<name> is a symlink (linked) vs a real dir."""
123
+ pkg_dir = root / "node_modules" / name
124
+ try:
125
+ return pkg_dir.is_symlink()
126
+ except OSError:
127
+ return False
128
+
129
+
130
+ def get_declared(consumer_pkg: dict, name: str) -> Optional[str]:
131
+ for field_name in ("dependencies", "devDependencies", "peerDependencies"):
132
+ spec = (consumer_pkg.get(field_name) or {}).get(name)
133
+ if spec:
134
+ return spec
135
+ return None
136
+
137
+
138
+ def get_latest(name: str, timeout: int = 10) -> Optional[str]:
139
+ """Query npm registry for the dist-tag latest version. Network call."""
140
+ try:
141
+ result = subprocess.run(
142
+ ["npm", "view", name, "version"],
143
+ capture_output=True, text=True, timeout=timeout,
144
+ )
145
+ if result.returncode == 0:
146
+ return result.stdout.strip() or None
147
+ except (subprocess.TimeoutExpired, FileNotFoundError):
148
+ pass
149
+ return None
150
+
151
+
152
+ def check_lockstep(statuses: list[PackageStatus]) -> Optional[str]:
153
+ """Return the consensus installed version, or None if split."""
154
+ versions = {s.installed for s in statuses if s.installed}
155
+ if len(versions) == 0:
156
+ return None
157
+ if len(versions) == 1:
158
+ return versions.pop()
159
+ return None # split
160
+
161
+
162
+ def run_audit(root: Path, offline: bool) -> tuple[list[PackageStatus], dict]:
163
+ consumer_pkg = read_json(root / "package.json") or {}
164
+
165
+ # Monorepo-root detection: if this package.json declares "workspaces",
166
+ # we're inside the @adia-ai/* source monorepo, not a consumer project.
167
+ # In that case, "undeclared" is expected (the packages are workspaces,
168
+ # not external deps) — suppress the noise.
169
+ is_monorepo_root = bool(consumer_pkg.get("workspaces"))
170
+
171
+ # "undeclared" classification — only flag when it likely reflects a
172
+ # mistake. In a consumer project, sub-packages like @adia-ai/llm,
173
+ # a2ui-runtime, a2ui-mcp are commonly transitive deps pulled in by
174
+ # @adia-ai/web-components or web-modules; "undeclared" there is noise.
175
+ # We only flag undeclared when:
176
+ # (a) we are NOT in the monorepo root (workspaces detected), AND
177
+ # (b) the package is one of the user-facing entry points
178
+ # (web-components, web-modules) AND it's installed.
179
+ # This catches the real failure mode (someone deleted the dep but
180
+ # node_modules still has it = phantom install) without spamming on
181
+ # legitimate transitive deps.
182
+ USER_FACING = {"@adia-ai/web-components", "@adia-ai/web-modules"}
183
+
184
+ statuses: list[PackageStatus] = []
185
+ for name in PACKAGE_NAMES:
186
+ s = PackageStatus(name=name)
187
+ s.installed = get_installed(root, name)
188
+ s.declared = get_declared(consumer_pkg, name)
189
+ s.declared_normalized = normalize_range(s.declared) if s.declared else None
190
+ s.symlinked = is_symlinked(root, name)
191
+ if not offline:
192
+ s.latest = get_latest(name)
193
+
194
+ declared_is_link = is_link_spec(s.declared)
195
+
196
+ # Per-package classification
197
+ if s.declared and not s.installed:
198
+ s.issues.append("uninstalled")
199
+ if (s.installed and not s.declared
200
+ and not is_monorepo_root
201
+ and name in USER_FACING):
202
+ s.issues.append("undeclared")
203
+ # Link-spec packages get a "linked" marker; suppress version-drift
204
+ # warnings since semver comparison doesn't apply to file:/link:/workspace:.
205
+ if declared_is_link:
206
+ s.issues.append("linked")
207
+ # Pitfall warning: declared as link but installed as real dir
208
+ # (npm sometimes copies instead of linking)
209
+ if s.installed and not s.symlinked:
210
+ s.issues.append("link-copied-not-symlinked")
211
+ elif s.installed and s.latest:
212
+ inst = parse_semver(s.installed)
213
+ late = parse_semver(s.latest)
214
+ if inst < late:
215
+ s.issues.append("stale")
216
+ elif inst > late:
217
+ s.issues.append("ahead")
218
+
219
+ statuses.append(s)
220
+
221
+ consensus = check_lockstep(statuses)
222
+ summary = {
223
+ "consumer_root": str(root.resolve()),
224
+ "consensus_installed": consensus,
225
+ "split_lockstep": consensus is None and any(s.installed for s in statuses),
226
+ "offline": offline,
227
+ "is_monorepo_root": is_monorepo_root,
228
+ }
229
+ return statuses, summary
230
+
231
+
232
+ def render_text(statuses: list[PackageStatus], summary: dict) -> int:
233
+ """Human-readable output. Returns process exit code."""
234
+ print(f"@adia-ai install audit — {summary['consumer_root']}")
235
+ if summary.get("is_monorepo_root"):
236
+ print("(monorepo-root mode — workspaces detected, 'undeclared' suppressed)")
237
+ print("─" * 78)
238
+
239
+ any_issues = False
240
+ consensus_latest = None
241
+ for s in statuses:
242
+ if s.latest:
243
+ consensus_latest = s.latest
244
+ break # first one with a latest is enough for the header line
245
+
246
+ if consensus_latest:
247
+ print(f"Latest published (npm view): {consensus_latest}")
248
+ if summary["consensus_installed"]:
249
+ print(f"Consensus installed: {summary['consensus_installed']}")
250
+ elif summary["split_lockstep"]:
251
+ print("Consensus installed: ⚠ SPLIT — packages disagree")
252
+ any_issues = True
253
+ print()
254
+
255
+ # Per-package table
256
+ name_width = max(len(s.name) for s in statuses) + 2
257
+ print(f" {'package'.ljust(name_width)} {'installed':<10} {'declared':<26} {'latest':<10} status")
258
+ print(f" {'-' * name_width} {'-' * 10} {'-' * 26} {'-' * 10} {'-' * 24}")
259
+
260
+ for s in statuses:
261
+ installed = s.installed or "—"
262
+ declared = s.declared or "—"
263
+ # Truncate long file: specs for table readability
264
+ if declared and len(declared) > 26:
265
+ declared = declared[:23] + "..."
266
+ latest = s.latest or ("(offline)" if summary["offline"] else "—")
267
+
268
+ if not s.issues:
269
+ status = "✓ ok"
270
+ else:
271
+ status_parts = []
272
+ for i in s.issues:
273
+ if i == "linked":
274
+ status_parts.append("🔗 linked")
275
+ elif i == "ahead":
276
+ status_parts.append("ⓘ ahead")
277
+ elif i == "link-copied-not-symlinked":
278
+ status_parts.append("⚠ copied-not-linked")
279
+ else:
280
+ status_parts.append(f"⚠ {i}")
281
+ status = " · ".join(status_parts)
282
+ # Only count as a real failure if it's not purely informational
283
+ real_issues = [i for i in s.issues
284
+ if i not in ("ahead", "linked")]
285
+ if real_issues:
286
+ any_issues = True
287
+
288
+ print(f" {s.name.ljust(name_width)} {installed:<10} {declared:<26} {latest:<10} {status}")
289
+
290
+ print()
291
+
292
+ # Diagnostics
293
+ if summary["split_lockstep"]:
294
+ print("✗ Lockstep broken — the 9 @adia-ai/* packages must share a version.")
295
+ print(" Fix: `npm install @adia-ai/web-components@<v> @adia-ai/web-modules@<v> ...`")
296
+ print(" or delete node_modules + package-lock.json and `npm install`.")
297
+ print()
298
+
299
+ stale_pkgs = [s for s in statuses if "stale" in s.issues]
300
+ if stale_pkgs:
301
+ print(f"⚠ Stale: {len(stale_pkgs)} package(s) behind latest. Suggested:")
302
+ print(f" npm install {' '.join(f'{s.name}@latest' for s in stale_pkgs)}")
303
+ print()
304
+
305
+ ahead_pkgs = [s for s in statuses if "ahead" in s.issues]
306
+ if ahead_pkgs:
307
+ print(f"ⓘ Ahead: {len(ahead_pkgs)} package(s) installed past latest published.")
308
+ print(" This is normal in the monorepo (pre-publish dev cut) or if you")
309
+ print(" installed from a local tarball / git tag. Not an error.")
310
+ print()
311
+
312
+ linked_pkgs = [s for s in statuses if "linked" in s.issues]
313
+ if linked_pkgs:
314
+ all_linked = len(linked_pkgs) == sum(1 for s in statuses if s.declared)
315
+ print(f"🔗 Linked: {len(linked_pkgs)} package(s) declared as local "
316
+ f"dev link ({'all declared' if all_linked else 'subset'}).")
317
+ print(" Version comparison disabled for these — they resolve to source")
318
+ print(" on disk, not an npm-published version. The 'installed' column")
319
+ print(" shows the version from the linked source's package.json.")
320
+ if not all_linked and sum(1 for s in statuses if s.declared) > len(linked_pkgs):
321
+ print()
322
+ print(" ⚠ Mixed link + npm declarations risk split-lockstep when the")
323
+ print(" linked source's transitive deps disagree with what npm")
324
+ print(" resolved. Recommended: link ALL 9 @adia-ai/* packages, or")
325
+ print(" link NONE. See §LocalLink in the adia-ui-kit skill.")
326
+ print()
327
+
328
+ copied_pkgs = [s for s in statuses if "link-copied-not-symlinked" in s.issues]
329
+ if copied_pkgs:
330
+ print(f"⚠ Link copied, not symlinked: {len(copied_pkgs)} package(s).")
331
+ print(" npm sometimes copies file: deps instead of symlinking — edits")
332
+ print(" to source won't be reflected. Fix:")
333
+ print(" rm -rf node_modules/@adia-ai")
334
+ print(" npm install --install-links=false")
335
+ print(" Then re-run this script to confirm symlinks are in place.")
336
+ print()
337
+
338
+ uninstalled = [s for s in statuses if "uninstalled" in s.issues]
339
+ if uninstalled:
340
+ print(f"⚠ Declared but uninstalled: {len(uninstalled)} package(s).")
341
+ print(" Fix: `npm install`")
342
+ print()
343
+
344
+ if not any_issues:
345
+ print("✓ All clean — lockstep aligned, declarations match installs, "
346
+ "and (if online) installs match latest published.")
347
+ return 0
348
+
349
+ return 1
350
+
351
+
352
+ def render_json(statuses: list[PackageStatus], summary: dict) -> int:
353
+ payload = {
354
+ "summary": summary,
355
+ "packages": [
356
+ {
357
+ "name": s.name,
358
+ "installed": s.installed,
359
+ "declared": s.declared,
360
+ "declared_normalized": s.declared_normalized,
361
+ "latest": s.latest,
362
+ "symlinked": s.symlinked,
363
+ "issues": s.issues,
364
+ }
365
+ for s in statuses
366
+ ],
367
+ }
368
+ print(json.dumps(payload, indent=2))
369
+ # 'ahead' and 'linked' are informational, not failures
370
+ real_issues = summary["split_lockstep"] or any(
371
+ i for s in statuses for i in s.issues if i not in ("ahead", "linked")
372
+ )
373
+ return 1 if real_issues else 0
374
+
375
+
376
+ def main(argv: Optional[list[str]] = None) -> int:
377
+ parser = argparse.ArgumentParser(
378
+ description="Verify @adia-ai/* install lockstep + freshness.",
379
+ )
380
+ parser.add_argument("--root", default=".",
381
+ help="Consumer project root (default: cwd).")
382
+ parser.add_argument("--offline", action="store_true",
383
+ help="Skip npm view (no network).")
384
+ parser.add_argument("--json", action="store_true",
385
+ help="Machine-readable JSON output.")
386
+ args = parser.parse_args(argv)
387
+
388
+ root = Path(args.root).resolve()
389
+ if not (root / "package.json").exists():
390
+ print(f"✗ No package.json at {root}", file=sys.stderr)
391
+ return 2
392
+ if not (root / "node_modules").exists():
393
+ print(f"⚠ No node_modules at {root} — run `npm install` first", file=sys.stderr)
394
+ return 2
395
+
396
+ statuses, summary = run_audit(root, args.offline)
397
+
398
+ if args.json:
399
+ return render_json(statuses, summary)
400
+ return render_text(statuses, summary)
401
+
402
+
403
+ if __name__ == "__main__":
404
+ sys.exit(main())