onemore-design 1.0.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/SKILL.md +180 -0
  3. package/bin/onemore.js +23 -0
  4. package/data/audit/hig-checklist.csv +39 -0
  5. package/data/components/content.csv +17 -0
  6. package/data/components/controls.csv +21 -0
  7. package/data/components/feedback.csv +17 -0
  8. package/data/components/input.csv +11 -0
  9. package/data/components/navigation.csv +15 -0
  10. package/data/foundations/colors.csv +38 -0
  11. package/data/foundations/corners.csv +13 -0
  12. package/data/foundations/elevation.csv +17 -0
  13. package/data/foundations/spacing.csv +21 -0
  14. package/data/foundations/typography.csv +26 -0
  15. package/data/patterns/animation.csv +24 -0
  16. package/data/patterns/gestures.csv +11 -0
  17. package/data/patterns/interaction.csv +16 -0
  18. package/data/patterns/layout.csv +20 -0
  19. package/data/platforms/ios.csv +21 -0
  20. package/data/platforms/macos.csv +16 -0
  21. package/data/platforms/visionos.csv +11 -0
  22. package/data/platforms/watchos.csv +11 -0
  23. package/data/platforms/web-apple.csv +21 -0
  24. package/data/reasoning/apple-reasoning.csv +16 -0
  25. package/data/stacks/astro.csv +21 -0
  26. package/data/stacks/flutter.csv +29 -0
  27. package/data/stacks/html-tailwind.csv +26 -0
  28. package/data/stacks/nativewind.csv +26 -0
  29. package/data/stacks/nextjs.csv +26 -0
  30. package/data/stacks/nuxtjs.csv +21 -0
  31. package/data/stacks/react-native.csv +26 -0
  32. package/data/stacks/react.csv +26 -0
  33. package/data/stacks/shadcn.csv +25 -0
  34. package/data/stacks/svelte.csv +21 -0
  35. package/data/stacks/swiftui.csv +31 -0
  36. package/data/stacks/uikit.csv +21 -0
  37. package/data/stacks/vue.csv +21 -0
  38. package/package.json +51 -0
  39. package/scripts/__init__.py +0 -0
  40. package/scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  41. package/scripts/__pycache__/core.cpython-314.pyc +0 -0
  42. package/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
  43. package/scripts/__pycache__/exporter.cpython-314.pyc +0 -0
  44. package/scripts/__pycache__/platforms.cpython-314.pyc +0 -0
  45. package/scripts/__pycache__/redesign.cpython-314.pyc +0 -0
  46. package/scripts/core.py +242 -0
  47. package/scripts/design_system.py +291 -0
  48. package/scripts/exporter.py +717 -0
  49. package/scripts/platforms.py +309 -0
  50. package/scripts/redesign.py +758 -0
  51. package/scripts/search.py +426 -0
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env python3
2
+ """OneMore CLI — Apple HIG design intelligence search."""
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ # Allow running as a script from any working directory
9
+ _SCRIPTS_DIR = Path(__file__).parent
10
+ _PROJECT_ROOT = _SCRIPTS_DIR.parent
11
+ if str(_PROJECT_ROOT) not in sys.path:
12
+ sys.path.insert(0, str(_PROJECT_ROOT))
13
+
14
+ from scripts.core import (
15
+ CSV_CONFIG,
16
+ PLATFORM_CONFIG,
17
+ STACK_CONFIG,
18
+ search,
19
+ search_platform,
20
+ search_stack,
21
+ )
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Output Formatting
25
+ # ---------------------------------------------------------------------------
26
+
27
+ _TRUNCATE = 200
28
+
29
+
30
+ def _truncate(value: str, limit: int = _TRUNCATE) -> str:
31
+ if len(value) <= limit:
32
+ return value
33
+ return value[:limit] + "…"
34
+
35
+
36
+ def format_output(
37
+ results: list[dict[str, str]],
38
+ *,
39
+ query: str = "",
40
+ domain: str | None = None,
41
+ stack: str | None = None,
42
+ platform: str | None = None,
43
+ fmt: str = "ascii",
44
+ ) -> str:
45
+ """Render results as a human-readable string."""
46
+ lines: list[str] = []
47
+
48
+ # --- Header ---
49
+ scope_parts: list[str] = []
50
+ if domain:
51
+ scope_parts.append(f"domain={domain}")
52
+ if stack:
53
+ scope_parts.append(f"stack={stack}")
54
+ if platform:
55
+ scope_parts.append(f"platform={platform}")
56
+ scope = ", ".join(scope_parts) if scope_parts else "auto"
57
+
58
+ if fmt == "markdown":
59
+ lines.append(f"## OneMore Search")
60
+ if query:
61
+ lines.append(f"**Query:** {query} ")
62
+ lines.append(f"**Scope:** {scope} ")
63
+ lines.append(f"**Results:** {len(results)}")
64
+ lines.append("")
65
+ else:
66
+ separator = "=" * 60
67
+ lines.append(separator)
68
+ lines.append(" OneMore")
69
+ if query:
70
+ lines.append(f" Query : {query}")
71
+ lines.append(f" Scope : {scope}")
72
+ lines.append(f" Results : {len(results)}")
73
+ lines.append(separator)
74
+
75
+ if not results:
76
+ lines.append("")
77
+ lines.append(" No results found.")
78
+ hints: list[str] = []
79
+ if domain and domain not in CSV_CONFIG:
80
+ hints.append(f" '{domain}' is not a known domain. Valid domains: {', '.join(sorted(CSV_CONFIG))}")
81
+ if stack and stack not in STACK_CONFIG:
82
+ hints.append(f" '{stack}' is not a known stack. Valid stacks: {', '.join(sorted(STACK_CONFIG))}")
83
+ if platform and platform not in PLATFORM_CONFIG:
84
+ hints.append(f" '{platform}' is not a known platform. Valid platforms: {', '.join(sorted(PLATFORM_CONFIG))}")
85
+ if not hints:
86
+ hints.append(" Try broadening your query or checking a different domain.")
87
+ lines.extend(hints)
88
+ lines.append("")
89
+ return "\n".join(lines)
90
+
91
+ # --- Results ---
92
+ for idx, row in enumerate(results, start=1):
93
+ if fmt == "markdown":
94
+ lines.append(f"### Result {idx}")
95
+ for key, value in row.items():
96
+ lines.append(f"- **{key}**: {_truncate(str(value))}")
97
+ lines.append("")
98
+ else:
99
+ lines.append(f"\n [{idx}]")
100
+ for key, value in row.items():
101
+ lines.append(f" {key:<20} {_truncate(str(value))}")
102
+
103
+ if fmt != "markdown":
104
+ lines.append("")
105
+
106
+ return "\n".join(lines)
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Argument Parser
111
+ # ---------------------------------------------------------------------------
112
+
113
+ def build_parser() -> argparse.ArgumentParser:
114
+ parser = argparse.ArgumentParser(
115
+ prog="onemore",
116
+ description="Search Apple HIG design intelligence from the command line.",
117
+ formatter_class=argparse.RawDescriptionHelpFormatter,
118
+ epilog="\n".join([
119
+ "Examples:",
120
+ " onemore 'blue accent color'",
121
+ " onemore 'button' --domain controls",
122
+ " onemore 'navigation' --platform ios",
123
+ " onemore 'button' --stack swiftui",
124
+ " onemore 'button' --json",
125
+ " onemore --design-system --project-name MyApp",
126
+ ]),
127
+ )
128
+
129
+ parser.add_argument(
130
+ "query",
131
+ nargs="?",
132
+ default="",
133
+ help="Search query text",
134
+ )
135
+ parser.add_argument(
136
+ "-d", "--domain",
137
+ choices=list(CSV_CONFIG.keys()),
138
+ metavar="DOMAIN",
139
+ help=f"Force a specific domain. Choices: {', '.join(sorted(CSV_CONFIG))}",
140
+ )
141
+ parser.add_argument(
142
+ "-s", "--stack",
143
+ choices=list(STACK_CONFIG.keys()),
144
+ metavar="STACK",
145
+ help=f"Stack-specific search. Choices: {', '.join(sorted(STACK_CONFIG))}",
146
+ )
147
+ parser.add_argument(
148
+ "-p", "--platform",
149
+ choices=list(PLATFORM_CONFIG.keys()),
150
+ metavar="PLATFORM",
151
+ help=f"Platform-specific search. Choices: {', '.join(sorted(PLATFORM_CONFIG))}",
152
+ )
153
+ parser.add_argument(
154
+ "-n", "--max-results",
155
+ type=int,
156
+ default=5,
157
+ metavar="N",
158
+ help="Maximum number of results to return (default: 5)",
159
+ )
160
+ parser.add_argument(
161
+ "--json",
162
+ action="store_true",
163
+ dest="json_output",
164
+ help="Output results as JSON",
165
+ )
166
+ parser.add_argument(
167
+ "--design-system",
168
+ action="store_true",
169
+ help="Generate a full design system (stub)",
170
+ )
171
+ parser.add_argument(
172
+ "--project-name",
173
+ default="MyApp",
174
+ metavar="NAME",
175
+ help="Project name for design system generation (default: MyApp)",
176
+ )
177
+ parser.add_argument(
178
+ "-f", "--format",
179
+ choices=["ascii", "markdown"],
180
+ default="ascii",
181
+ help="Output format: ascii or markdown (default: ascii)",
182
+ )
183
+ parser.add_argument(
184
+ "--persist",
185
+ action="store_true",
186
+ help="Save design system output to files",
187
+ )
188
+ parser.add_argument(
189
+ "--page",
190
+ metavar="PAGE",
191
+ help="Page-specific override for design system generation",
192
+ )
193
+
194
+ return parser
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # Main
199
+ # ---------------------------------------------------------------------------
200
+
201
+ def _handle_init(args: list[str]) -> int:
202
+ """Handle the 'init' subcommand for multi-platform distribution."""
203
+ from scripts.platforms import init_platform, list_platforms, detect_platforms, auto_init, PLATFORMS
204
+
205
+ if "--list" in args:
206
+ platforms = list_platforms()
207
+ print(f"{'Platform':<16} {'Name':<18} {'Type':<10} {'Status':<12} Path")
208
+ print("-" * 80)
209
+ for p in platforms:
210
+ status = "installed" if p["installed"] else "-"
211
+ print(f"{p['platform']:<16} {p['name']:<18} {p['type']:<10} {status:<12} {p['path']}")
212
+ return 0
213
+
214
+ # Find --ai value
215
+ ai_value = None
216
+ for i, arg in enumerate(args):
217
+ if arg == "--ai" and i + 1 < len(args):
218
+ ai_value = args[i + 1]
219
+ break
220
+
221
+ if not ai_value:
222
+ # Auto-detect mode: no --ai flag given
223
+ print("OneMore — Auto-Detecting AI Platforms...\n")
224
+
225
+ detected_list = detect_platforms()
226
+ for p in detected_list:
227
+ mark = "✓" if p["detected"] else "✗"
228
+ reason = p["reason"] if p["detected"] else "not detected"
229
+ print(f" {mark} {p['name']:<16} {reason}")
230
+
231
+ detected_count = sum(1 for p in detected_list if p["detected"])
232
+ if detected_count == 0:
233
+ print("\nNo AI platforms detected. Use --ai <platform> to install manually.")
234
+ print(f"Available platforms: {', '.join(sorted(PLATFORMS.keys()))}")
235
+ return 0
236
+
237
+ print(f"\nInstalling OneMore for {detected_count} detected platform(s)...\n")
238
+
239
+ result = auto_init()
240
+ installed_names = {p["platform"] for p in result["installed"]}
241
+ skipped_names = {p["platform"] for p in result["skipped"]}
242
+
243
+ for p in detected_list:
244
+ if not p["detected"]:
245
+ continue
246
+ platform_key = p["platform"]
247
+ config = PLATFORMS[platform_key]
248
+ install_path = config["path"]
249
+ if platform_key in installed_names:
250
+ print(f" ✓ {p['name']:<16} → {install_path}")
251
+ elif platform_key in skipped_names:
252
+ print(f" ✓ {p['name']:<16} → {install_path} (already installed)")
253
+
254
+ newly_installed = len(result["installed"])
255
+ already_installed = len(result["skipped"])
256
+ total = newly_installed + already_installed
257
+ if already_installed > 0:
258
+ print(f"\nDone! OneMore installed for {total} platform(s) ({already_installed} already installed).")
259
+ else:
260
+ print(f"\nDone! OneMore installed for {total} platform(s).")
261
+ return 0
262
+
263
+ # Determine which platforms to init
264
+ if ai_value == "all":
265
+ targets = [name for name, cfg in PLATFORMS.items() if cfg["type"] == "project"]
266
+ else:
267
+ targets = [t.strip() for t in ai_value.split(",")]
268
+
269
+ for platform_name in targets:
270
+ result = init_platform(platform_name)
271
+ if "error" in result:
272
+ print(f" ERROR {platform_name}: {result['error']}")
273
+ else:
274
+ print(f" {result['status'].upper():<18} {result['platform']:<18} {result.get('path', '')}")
275
+
276
+ return 0
277
+
278
+
279
+ def _handle_export(args: list[str]) -> int:
280
+ """Handle the 'export' subcommand for design token code generation."""
281
+ from scripts.exporter import EXPORT_FORMATS, export_tokens
282
+
283
+ if "--list" in args:
284
+ print(f"{'Format':<16} {'Output File':<24}")
285
+ print("-" * 40)
286
+ for name, cfg in EXPORT_FORMATS.items():
287
+ print(f"{name:<16} {cfg['filename']:<24}")
288
+ return 0
289
+
290
+ # Parse --format and --output
291
+ fmt_value = None
292
+ output_value = None
293
+ for i, arg in enumerate(args):
294
+ if arg == "--format" and i + 1 < len(args):
295
+ fmt_value = args[i + 1]
296
+ elif arg == "--output" and i + 1 < len(args):
297
+ output_value = args[i + 1]
298
+
299
+ if not fmt_value:
300
+ print("Usage: onemore export --format <format|all> [--output <dir>]")
301
+ print(" onemore export --list")
302
+ print(f"\nAvailable formats: {', '.join(EXPORT_FORMATS.keys())}")
303
+ return 1
304
+
305
+ if fmt_value == "all":
306
+ targets = list(EXPORT_FORMATS.keys())
307
+ else:
308
+ targets = [fmt_value]
309
+
310
+ for target in targets:
311
+ result = export_tokens(target, output_dir=output_value)
312
+ if "error" in result:
313
+ print(f" ERROR {result['error']}")
314
+ else:
315
+ print(f" {result['status'].upper():<12} {result['format']:<16} -> {result['file']}")
316
+
317
+ return 0
318
+
319
+
320
+ def _handle_redesign(args: list[str]) -> int:
321
+ """Handle the 'redesign' subcommand for HIG violation scanning."""
322
+ from scripts.redesign import cli_main
323
+ return cli_main(args)
324
+
325
+
326
+ def main() -> int:
327
+ # Handle subcommands before argparse
328
+ raw_args = sys.argv[1:]
329
+ if raw_args and raw_args[0] == "export":
330
+ return _handle_export(raw_args[1:])
331
+ if raw_args and raw_args[0] == "init":
332
+ return _handle_init(raw_args[1:])
333
+ if raw_args and raw_args[0] == "redesign":
334
+ return _handle_redesign(raw_args[1:])
335
+
336
+ # Use parse_known_args to allow unknown domain/stack/platform values
337
+ # without crashing, then validate manually so we can surface helpful messages.
338
+ parser = build_parser()
339
+
340
+ # Patch: temporarily widen domain/stack/platform to accept any string so
341
+ # unknown values are handled gracefully rather than argparse error-exiting.
342
+ _patched_parser = argparse.ArgumentParser(
343
+ prog="onemore",
344
+ description=parser.description,
345
+ formatter_class=parser.formatter_class,
346
+ epilog=parser.epilog,
347
+ add_help=True,
348
+ )
349
+ _patched_parser.add_argument("query", nargs="?", default="")
350
+ _patched_parser.add_argument("-d", "--domain", default=None, metavar="DOMAIN")
351
+ _patched_parser.add_argument("-s", "--stack", default=None, metavar="STACK")
352
+ _patched_parser.add_argument("-p", "--platform", default=None, metavar="PLATFORM")
353
+ _patched_parser.add_argument("-n", "--max-results", type=int, default=5, metavar="N")
354
+ _patched_parser.add_argument("--json", action="store_true", dest="json_output")
355
+ _patched_parser.add_argument("--design-system", action="store_true")
356
+ _patched_parser.add_argument("--project-name", default="MyApp", metavar="NAME")
357
+ _patched_parser.add_argument("-f", "--format", choices=["ascii", "markdown"], default="ascii")
358
+ _patched_parser.add_argument("--persist", action="store_true")
359
+ _patched_parser.add_argument("--page", default=None, metavar="PAGE")
360
+
361
+ # Show proper help from the nicer parser if --help requested
362
+ if "-h" in raw_args or "--help" in raw_args:
363
+ parser.print_help()
364
+ return 0
365
+
366
+ args = _patched_parser.parse_args(raw_args)
367
+
368
+ query: str = args.query or ""
369
+ domain: str | None = args.domain
370
+ stack: str | None = args.stack
371
+ platform: str | None = args.platform
372
+ max_results: int = args.max_results
373
+ json_output: bool = args.json_output
374
+ fmt: str = args.format
375
+
376
+ # ------------------------------------------------------------------
377
+ # Priority 1: --design-system
378
+ # ------------------------------------------------------------------
379
+ if args.design_system:
380
+ try:
381
+ from scripts.design_system import DesignSystemGenerator # type: ignore
382
+ generator = DesignSystemGenerator(project_name=args.project_name)
383
+ output = generator.generate(fmt=fmt, persist=args.persist, page=args.page)
384
+ print(output)
385
+ except ImportError:
386
+ print(
387
+ f"Design system generator for '{args.project_name}' is not yet implemented.\n"
388
+ "Run without --design-system to search the design token database."
389
+ )
390
+ return 0
391
+
392
+ # ------------------------------------------------------------------
393
+ # Priority 2: --stack
394
+ # ------------------------------------------------------------------
395
+ if stack:
396
+ results = search_stack(query, stack, max_results=max_results)
397
+ if json_output:
398
+ print(json.dumps(results, ensure_ascii=False, indent=2))
399
+ else:
400
+ print(format_output(results, query=query, stack=stack, fmt=fmt))
401
+ return 0
402
+
403
+ # ------------------------------------------------------------------
404
+ # Priority 3: --platform (without --domain)
405
+ # ------------------------------------------------------------------
406
+ if platform and not domain:
407
+ results = search_platform(query, platform, max_results=max_results)
408
+ if json_output:
409
+ print(json.dumps(results, ensure_ascii=False, indent=2))
410
+ else:
411
+ print(format_output(results, query=query, platform=platform, fmt=fmt))
412
+ return 0
413
+
414
+ # ------------------------------------------------------------------
415
+ # Default: search() with optional domain and platform filter
416
+ # ------------------------------------------------------------------
417
+ results = search(query, domain=domain, max_results=max_results, platform=platform)
418
+ if json_output:
419
+ print(json.dumps(results, ensure_ascii=False, indent=2))
420
+ else:
421
+ print(format_output(results, query=query, domain=domain, platform=platform, fmt=fmt))
422
+ return 0
423
+
424
+
425
+ if __name__ == "__main__":
426
+ sys.exit(main())