@ansh-ui/cli 0.3.1 → 0.4.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.
package/dist/cli.js CHANGED
@@ -1,9 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
3
3
  import { parseArgs } from 'util';
4
- import { join, dirname, resolve } from 'path';
4
+ import { join, dirname, resolve, relative } from 'path';
5
5
  import { parseColor, generatePalette, rgbToOklch, toOklchString, toHex, createGradient, createScale, checkContrast, harmony, effects, behaviours } from '@ansh-ui/core';
6
+ import { createInterface } from 'readline';
6
7
 
8
+ var CONFIG_FILE = "ansh.config.json";
9
+ function readConfig(root) {
10
+ const path = join(root, CONFIG_FILE);
11
+ if (!existsSync(path)) return null;
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
14
+ return parsed && typeof parsed === "object" ? parsed : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ // src/project.ts
7
21
  function findRoot(cwd) {
8
22
  let current = resolve(cwd);
9
23
  for (let depth = 0; depth < 10; depth++) {
@@ -31,11 +45,12 @@ function detectProject(cwd = process.cwd()) {
31
45
  const framework = deps.nuxt ? "nuxt" : deps.next ? "next" : deps.vue ? "vue" : deps.react ? "react" : "unknown";
32
46
  const typescript = Boolean(deps.typescript) || existsSync(join(root, "tsconfig.json"));
33
47
  const packageManager = existsSync(join(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")) ? "bun" : existsSync(join(root, "yarn.lock")) ? "yarn" : "npm";
48
+ const config = readConfig(root);
34
49
  return {
35
50
  root,
36
51
  framework,
37
52
  typescript,
38
- componentsDir: detectComponentsDir(root, framework),
53
+ componentsDir: config?.componentsDir ?? detectComponentsDir(root, framework),
39
54
  alias: detectAlias(root),
40
55
  packageManager,
41
56
  installed: Boolean(
@@ -66,10 +81,8 @@ function detectAlias(root) {
66
81
  return null;
67
82
  }
68
83
  var KNOWN_INCONSISTENCIES = [
69
- {
70
- title: "License key prefix is still `aur_`",
71
- detail: "A leftover from the Aurelis \u2192 Ansh rename (packages/license/src/public-key.ts). Changing it without a dual-prefix verification path would invalidate already-issued keys \u2014 needs a maintainer decision, not a drive-by fix. See STATUS.md \xA77."
72
- }
84
+ // Empty since the licence key prefix moved to `ansh_` (with `aur_` still accepted). Add an entry
85
+ // here when STATUS.md §7 gains one.
73
86
  ];
74
87
  function isAnshRepoRoot(root) {
75
88
  const pkg = readJson(join(root, "package.json"));
@@ -78,6 +91,16 @@ function isAnshRepoRoot(root) {
78
91
  function detectKnownInconsistencies(root) {
79
92
  return isAnshRepoRoot(root) ? KNOWN_INCONSISTENCIES : [];
80
93
  }
94
+ function packagesFor(framework) {
95
+ switch (framework) {
96
+ case "nuxt":
97
+ return ["@ansh-ui/nuxt"];
98
+ case "vue":
99
+ return ["@ansh-ui/vue", "@ansh-ui/tokens"];
100
+ default:
101
+ return ["@ansh-ui/react", "@ansh-ui/tokens"];
102
+ }
103
+ }
81
104
  function installCommand(info, packages) {
82
105
  const list = packages.join(" ");
83
106
  switch (info.packageManager) {
@@ -184,15 +207,7 @@ function addComponent(options) {
184
207
  return { path, contents, written: true, missingDependencies };
185
208
  }
186
209
  function missingFor(project) {
187
- if (project.installed) return [];
188
- switch (project.framework) {
189
- case "nuxt":
190
- return ["@ansh-ui/nuxt"];
191
- case "vue":
192
- return ["@ansh-ui/vue", "@ansh-ui/tokens"];
193
- default:
194
- return ["@ansh-ui/react", "@ansh-ui/tokens"];
195
- }
210
+ return project.installed ? [] : packagesFor(project.framework);
196
211
  }
197
212
  function literal(value) {
198
213
  if (typeof value === "string") return JSON.stringify(value);
@@ -394,6 +409,242 @@ ${accentLines}
394
409
  }
395
410
  return { css, unsafeOnLight, unsafeOnDark, nearestStep };
396
411
  }
412
+ function initProject(options = {}) {
413
+ const project = detectProject(options.cwd);
414
+ if (project.framework === "unknown") {
415
+ throw new Error(
416
+ "Could not detect a React, Next, Vue or Nuxt project. Run this from your project root, or install the framework first."
417
+ );
418
+ }
419
+ const configPath = join(project.root, CONFIG_FILE);
420
+ if (existsSync(configPath) && !options.force) {
421
+ throw new Error(`${CONFIG_FILE} already exists. Pass --force to overwrite it.`);
422
+ }
423
+ const themeRelative = options.brand ? join(stylesDir(project), "ansh-theme.css") : null;
424
+ const themePath = themeRelative ? join(project.root, themeRelative) : null;
425
+ if (themePath && existsSync(themePath) && !options.force) {
426
+ throw new Error(`${themePath} already exists. Pass --force to overwrite it.`);
427
+ }
428
+ const config = {
429
+ framework: project.framework,
430
+ typescript: project.typescript,
431
+ componentsDir: options.componentsDir ?? project.componentsDir,
432
+ alias: project.alias,
433
+ theme: themeRelative
434
+ };
435
+ const written = [];
436
+ if (!options.dryRun) {
437
+ if (options.brand && themePath) {
438
+ generateTheme({ color: options.brand, out: themePath });
439
+ written.push(themePath);
440
+ }
441
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
442
+ `, "utf8");
443
+ written.push(configPath);
444
+ } else if (options.brand) {
445
+ generateTheme({ color: options.brand });
446
+ }
447
+ return {
448
+ project,
449
+ config,
450
+ written,
451
+ install: project.installed ? null : installCommand(project, packagesFor(project.framework)),
452
+ setup: setupFor(project, themeRelative)
453
+ };
454
+ }
455
+ function stylesDir(project) {
456
+ if (project.framework === "nuxt") return "assets/css";
457
+ return existsSync(join(project.root, "src")) ? "src/styles" : "styles";
458
+ }
459
+ function setupFor(project, theme) {
460
+ if (project.framework === "nuxt") {
461
+ const css = ["'@ansh-ui/vue/styles.css'", ...theme ? [`'~/${theme}'`] : []].join(", ");
462
+ return {
463
+ file: "nuxt.config.ts",
464
+ code: `export default defineNuxtConfig({
465
+ modules: ['@ansh-ui/nuxt'],
466
+ css: [${css}],
467
+ })`
468
+ };
469
+ }
470
+ const binding = project.framework === "vue" ? "vue" : "react";
471
+ const entry = project.framework === "next" ? existsSync(join(project.root, "src/app")) ? "src/app/layout.tsx" : "app/layout.tsx" : project.framework === "vue" ? `src/main.${project.typescript ? "ts" : "js"}` : `src/main.${project.typescript ? "tsx" : "jsx"}`;
472
+ const themeImport = theme ? `
473
+ import '${themeImportPath(project, entry, theme)}'` : "";
474
+ return {
475
+ file: entry,
476
+ code: `import '@ansh-ui/tokens/ansh.css'
477
+ import '@ansh-ui/${binding}/styles.css'${themeImport}`
478
+ };
479
+ }
480
+ function themeImportPath(project, entry, theme) {
481
+ const path = relative(join(project.root, entry, ".."), join(project.root, theme));
482
+ return path.startsWith(".") ? path : `./${path}`;
483
+ }
484
+ var PROTOCOL_VERSION = "2025-06-18";
485
+ var str = (value) => typeof value === "string" ? value : void 0;
486
+ var TOOLS = [
487
+ {
488
+ name: "ansh_list",
489
+ description: "List Ansh canvas effects and DOM behaviours with their component names and option counts.",
490
+ inputSchema: {
491
+ type: "object",
492
+ properties: { kind: { type: "string", enum: ["effect", "behaviour"] } }
493
+ },
494
+ run: (args) => {
495
+ const kind = args.kind === "effect" || args.kind === "behaviour" ? args.kind : void 0;
496
+ const entries = listCatalogue(kind).map(({ name, kind: kind2, component, options }) => ({
497
+ name,
498
+ kind: kind2,
499
+ component,
500
+ options: options.length
501
+ }));
502
+ return JSON.stringify(entries, null, 2);
503
+ }
504
+ },
505
+ {
506
+ name: "ansh_info",
507
+ description: "Every option of one Ansh effect or behaviour, with its default value. Accepts the catalogue name (aurora) or component name (AuroraBackground).",
508
+ inputSchema: {
509
+ type: "object",
510
+ properties: { name: { type: "string" } },
511
+ required: ["name"]
512
+ },
513
+ run: (args) => {
514
+ const entry = findEntry(str(args.name) ?? "");
515
+ if (!entry) throw new Error(`Unknown component "${String(args.name)}". Call ansh_list.`);
516
+ return JSON.stringify(entry, null, 2);
517
+ }
518
+ },
519
+ {
520
+ name: "ansh_setup",
521
+ description: "How to set up Ansh in the current project: detected framework, install command, and the imports or nuxt.config lines to add. Pass write: true to also create ansh.config.json (and a theme file when brand is given).",
522
+ inputSchema: {
523
+ type: "object",
524
+ properties: {
525
+ brand: { type: "string", description: "Brand colour for a generated theme, e.g. #0275f4" },
526
+ write: { type: "boolean", default: false }
527
+ }
528
+ },
529
+ run: (args, cwd) => {
530
+ const result = initProject({ cwd, brand: str(args.brand), dryRun: args.write !== true });
531
+ return JSON.stringify(
532
+ {
533
+ framework: result.config.framework,
534
+ install: result.install,
535
+ setupFile: result.setup.file,
536
+ setupCode: result.setup.code,
537
+ config: result.config,
538
+ written: result.written
539
+ },
540
+ null,
541
+ 2
542
+ );
543
+ }
544
+ },
545
+ {
546
+ name: "ansh_add",
547
+ description: "Generate a typed, project-local wrapper component around an Ansh effect or behaviour, with the project\u2019s defaults. Returns the file path and contents; pass write: true to write the file.",
548
+ inputSchema: {
549
+ type: "object",
550
+ properties: {
551
+ name: { type: "string" },
552
+ as: { type: "string", description: "Local component name, e.g. HeroBackground" },
553
+ write: { type: "boolean", default: false }
554
+ },
555
+ required: ["name"]
556
+ },
557
+ run: (args, cwd) => {
558
+ const result = addComponent({
559
+ cwd,
560
+ name: str(args.name) ?? "",
561
+ as: str(args.as),
562
+ dryRun: args.write !== true
563
+ });
564
+ return JSON.stringify(result, null, 2);
565
+ }
566
+ },
567
+ {
568
+ name: "ansh_theme",
569
+ description: "Generate an Ansh token override stylesheet from a brand colour, with WCAG contrast notes for each step.",
570
+ inputSchema: {
571
+ type: "object",
572
+ properties: { color: { type: "string" }, withAccent: { type: "boolean", default: false } },
573
+ required: ["color"]
574
+ },
575
+ run: (args) => {
576
+ const report = generateTheme({
577
+ color: str(args.color) ?? "",
578
+ withAccent: args.withAccent === true
579
+ });
580
+ return JSON.stringify(report, null, 2);
581
+ }
582
+ }
583
+ ];
584
+ function callTool(params, cwd) {
585
+ const tool = TOOLS.find((t) => t.name === params?.name);
586
+ if (!tool)
587
+ return {
588
+ content: [{ type: "text", text: `Unknown tool "${String(params?.name)}".` }],
589
+ isError: true
590
+ };
591
+ const args = params?.arguments ?? {};
592
+ try {
593
+ return { content: [{ type: "text", text: tool.run(args, cwd) }] };
594
+ } catch (error) {
595
+ const text = error instanceof Error ? error.message : String(error);
596
+ return { content: [{ type: "text", text }], isError: true };
597
+ }
598
+ }
599
+ function handleMessage(message, options) {
600
+ const cwd = options.cwd ?? process.cwd();
601
+ const reply = (result) => ({ jsonrpc: "2.0", id: message.id ?? null, result });
602
+ if (message.id === void 0) return null;
603
+ switch (message.method) {
604
+ case "initialize":
605
+ return reply({
606
+ protocolVersion: str(message.params?.protocolVersion) ?? PROTOCOL_VERSION,
607
+ capabilities: { tools: {} },
608
+ serverInfo: { name: "ansh", version: options.version },
609
+ instructions: "Ansh is a UI and animation library for React, Next.js, Vue and Nuxt. Call ansh_setup before writing Ansh code in a project, and ansh_info for an effect\u2019s exact options."
610
+ });
611
+ case "ping":
612
+ return reply({});
613
+ case "tools/list":
614
+ return reply({
615
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({
616
+ name,
617
+ description,
618
+ inputSchema
619
+ }))
620
+ });
621
+ case "tools/call":
622
+ return reply(callTool(message.params, cwd));
623
+ default:
624
+ return {
625
+ jsonrpc: "2.0",
626
+ id: message.id,
627
+ error: { code: -32601, message: `Method not found: ${message.method}` }
628
+ };
629
+ }
630
+ }
631
+ function serveMcp(version, input = process.stdin, output = process.stdout) {
632
+ const send = (payload) => output.write(`${JSON.stringify(payload)}
633
+ `);
634
+ createInterface({ input }).on("line", (line) => {
635
+ if (!line.trim()) return;
636
+ let message;
637
+ try {
638
+ message = JSON.parse(line);
639
+ } catch {
640
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
641
+ return;
642
+ }
643
+ const response = handleMessage(message, { version });
644
+ if (response) send(response);
645
+ });
646
+ console.error(`ansh mcp v${version} ready on stdio`);
647
+ }
397
648
 
398
649
  // src/cli.ts
399
650
  var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
@@ -413,14 +664,19 @@ ${bold("USAGE")}
413
664
  ansh <command> [options]
414
665
 
415
666
  ${bold("COMMANDS")}
667
+ ${cyan("init")} [--brand <color>] Set up Ansh in this project
416
668
  ${cyan("list")} [--effects|--behaviours] List the component catalogue
417
669
  ${cyan("info")} <name> Show a component's full option surface
418
670
  ${cyan("add")} <name> [--as <Name>] Generate a local wrapper component
419
671
  ${cyan("theme")} <color> [--out <file>] Generate a token override stylesheet
420
672
  ${cyan("palette")} [colors...] Generate or inspect a palette
421
673
  ${cyan("doctor")} Report the detected project setup
674
+ ${cyan("mcp")} Run as an MCP server for AI coding agents
422
675
 
423
676
  ${bold("EXAMPLES")}
677
+ ${dim("# Set up a project, with a theme from your brand colour")}
678
+ ansh init --brand '#0275f4'
679
+
424
680
  ${dim("# See what is available")}
425
681
  ansh list
426
682
 
@@ -442,6 +698,41 @@ function fail(message, hint) {
442
698
  if (hint) console.error(`${dim("hint")} ${hint}`);
443
699
  process.exit(1);
444
700
  }
701
+ function commandInit(argv) {
702
+ const { values } = parseArgs({
703
+ args: argv,
704
+ options: {
705
+ brand: { type: "string" },
706
+ dir: { type: "string" },
707
+ force: { type: "boolean", default: false },
708
+ "dry-run": { type: "boolean", default: false }
709
+ }
710
+ });
711
+ try {
712
+ const result = initProject({
713
+ brand: values.brand,
714
+ componentsDir: values.dir,
715
+ force: values.force,
716
+ dryRun: values["dry-run"]
717
+ });
718
+ console.log(`
719
+ ${bold("ansh init")} ${dim(`${result.config.framework} project detected`)}
720
+ `);
721
+ for (const path of result.written) console.log(`${green("created")} ${path}`);
722
+ if (result.install) console.log(`
723
+ ${yellow("1")} Install:
724
+ ${result.install}`);
725
+ console.log(`
726
+ ${yellow(result.install ? "2" : "1")} Add to ${result.setup.file}:
727
+ `);
728
+ console.log(result.setup.code.replace(/^/gm, " "));
729
+ console.log(`
730
+ ${dim(`Then try: ansh add aurora --as HeroBackground`)}
731
+ `);
732
+ } catch (error) {
733
+ fail(error instanceof Error ? error.message : String(error));
734
+ }
735
+ }
445
736
  function commandList(argv) {
446
737
  const { values } = parseArgs({
447
738
  args: argv,
@@ -645,9 +936,8 @@ ${bold("PROJECT")}
645
936
  console.log(` import alias ${project.alias ?? dim("none")}`);
646
937
  console.log(` ansh ${project.installed ? green("installed") : yellow("not installed")}`);
647
938
  if (!project.installed) {
648
- const packages = project.framework === "nuxt" ? ["@ansh-ui/nuxt"] : project.framework === "vue" ? ["@ansh-ui/vue", "@ansh-ui/tokens"] : ["@ansh-ui/react", "@ansh-ui/tokens"];
649
939
  console.log(`
650
- ${yellow("next")} ${installCommand(project, packages)}
940
+ ${yellow("next")} ${installCommand(project, packagesFor(project.framework))}
651
941
  `);
652
942
  } else {
653
943
  console.log("");
@@ -673,6 +963,8 @@ function swatch(hex) {
673
963
  function main() {
674
964
  const [command, ...rest] = process.argv.slice(2);
675
965
  switch (command) {
966
+ case "init":
967
+ return commandInit(rest);
676
968
  case "list":
677
969
  case "ls":
678
970
  return commandList(rest);
@@ -687,6 +979,8 @@ function main() {
687
979
  return commandPalette(rest);
688
980
  case "doctor":
689
981
  return commandDoctor();
982
+ case "mcp":
983
+ return serveMcp(VERSION);
690
984
  case "--version":
691
985
  case "-v":
692
986
  console.log(VERSION);