lokuma-cli 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.
- package/README.md +64 -0
- package/assets/search.py +221 -0
- package/dist/index.js +202 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# lokuma-cli
|
|
2
|
+
|
|
3
|
+
Install [Lokuma](https://lokuma.io) design intelligence skill for AI coding assistants.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx lokuma-cli init --ai claude
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Supported AI Assistants
|
|
12
|
+
|
|
13
|
+
| Flag | Assistant |
|
|
14
|
+
|------|-----------|
|
|
15
|
+
| `--ai claude` | Claude Code |
|
|
16
|
+
| `--ai cursor` | Cursor |
|
|
17
|
+
| `--ai windsurf` | Windsurf |
|
|
18
|
+
| `--ai copilot` | GitHub Copilot |
|
|
19
|
+
| `--ai kiro` | Kiro |
|
|
20
|
+
| `--ai roocode` | Roo Code |
|
|
21
|
+
| `--ai codex` | Codex CLI |
|
|
22
|
+
| `--ai qoder` | Qoder |
|
|
23
|
+
| `--ai gemini` | Gemini CLI |
|
|
24
|
+
| `--ai trae` | Trae |
|
|
25
|
+
| `--ai opencode` | OpenCode |
|
|
26
|
+
| `--ai continue` | Continue |
|
|
27
|
+
| `--ai codebuddy` | CodeBuddy |
|
|
28
|
+
| `--ai droid` | Droid (Factory) |
|
|
29
|
+
| `--ai all` | All assistants |
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
After installing, set your API key:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export LOKUMA_API_KEY=lokuma_your_key_here
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Get your API key at [lokuma.io](https://lokuma.io).
|
|
40
|
+
|
|
41
|
+
## Options
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
lokuma init [options]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
-a, --ai <type> AI assistant type
|
|
48
|
+
-f, --force Overwrite existing files
|
|
49
|
+
-h, --help Show help
|
|
50
|
+
-V, --version Show version
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## How It Works
|
|
54
|
+
|
|
55
|
+
`lokuma-cli` installs two files into your project:
|
|
56
|
+
|
|
57
|
+
1. **SKILL.md** — tells your AI assistant how to use Lokuma
|
|
58
|
+
2. **search.py** — a lightweight cloud client that calls the Lokuma API
|
|
59
|
+
|
|
60
|
+
No local data files. No Python dependencies. Just set your API key and go.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
package/assets/search.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Lokuma Search - Cloud Client
|
|
5
|
+
Calls the Lokuma cloud API to generate design intelligence.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python search.py "<query>" [--domain <domain>] [--max-results 3]
|
|
9
|
+
python search.py "<query>" --stack <stack>
|
|
10
|
+
python search.py "<query>" --design-system [-p "Project Name"] [-f ascii|markdown|json]
|
|
11
|
+
|
|
12
|
+
Configuration:
|
|
13
|
+
Set LOKUMA_API_KEY environment variable (required):
|
|
14
|
+
export LOKUMA_API_KEY=lokuma_your_key_here
|
|
15
|
+
|
|
16
|
+
Optional overrides:
|
|
17
|
+
LOKUMA_API_URL=https://custom.endpoint.com (default: official API)
|
|
18
|
+
|
|
19
|
+
Domains: style, color, chart, landing, product, ux, typography, google-fonts,
|
|
20
|
+
icons, react, web
|
|
21
|
+
Stacks: react-native
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
import io
|
|
29
|
+
import urllib.request
|
|
30
|
+
import urllib.error
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
# Force UTF-8 output on Windows
|
|
34
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
|
35
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
36
|
+
if sys.stderr.encoding and sys.stderr.encoding.lower() != "utf-8":
|
|
37
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ─────────────────────────────────────────────
|
|
41
|
+
# Config
|
|
42
|
+
# ─────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
API_BASE = os.environ.get("LOKUMA_API_URL", "https://api.lokuma.io").rstrip("/")
|
|
45
|
+
API_VERSION = "v1"
|
|
46
|
+
_BASE = f"{API_BASE}/{API_VERSION}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_api_key() -> str:
|
|
50
|
+
key = os.environ.get("LOKUMA_API_KEY", "").strip()
|
|
51
|
+
if not key:
|
|
52
|
+
print(
|
|
53
|
+
"Error: LOKUMA_API_KEY is not set.\n"
|
|
54
|
+
"Set it with: export LOKUMA_API_KEY=lokuma_your_key_here",
|
|
55
|
+
file=sys.stderr,
|
|
56
|
+
)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
return key
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ─────────────────────────────────────────────
|
|
62
|
+
# HTTP client (stdlib only, zero deps)
|
|
63
|
+
# ─────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
def _post(endpoint: str, payload: dict) -> dict:
|
|
66
|
+
"""POST JSON payload to API endpoint, return parsed response."""
|
|
67
|
+
api_key = _get_api_key()
|
|
68
|
+
url = f"{_BASE}/{endpoint}"
|
|
69
|
+
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
70
|
+
|
|
71
|
+
req = urllib.request.Request(
|
|
72
|
+
url,
|
|
73
|
+
data=data,
|
|
74
|
+
headers={
|
|
75
|
+
"Content-Type": "application/json",
|
|
76
|
+
"X-API-Key": api_key,
|
|
77
|
+
"User-Agent": "lokuma-skill/1.0",
|
|
78
|
+
},
|
|
79
|
+
method="POST",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
84
|
+
body = resp.read().decode("utf-8")
|
|
85
|
+
return json.loads(body)
|
|
86
|
+
except urllib.error.HTTPError as e:
|
|
87
|
+
body = e.read().decode("utf-8", errors="replace")
|
|
88
|
+
try:
|
|
89
|
+
err = json.loads(body)
|
|
90
|
+
msg = err.get("error", body)
|
|
91
|
+
except Exception:
|
|
92
|
+
msg = body
|
|
93
|
+
if e.code == 401 or e.code == 403:
|
|
94
|
+
print(f"Error: Invalid or expired API key (HTTP {e.code})", file=sys.stderr)
|
|
95
|
+
else:
|
|
96
|
+
print(f"Error: API returned HTTP {e.code}: {msg}", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
except urllib.error.URLError as e:
|
|
99
|
+
print(f"Error: Could not reach API at {url}: {e.reason}", file=sys.stderr)
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
except json.JSONDecodeError as e:
|
|
102
|
+
print(f"Error: Invalid JSON response from API: {e}", file=sys.stderr)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ─────────────────────────────────────────────
|
|
107
|
+
# API wrappers
|
|
108
|
+
# ─────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def search(query: str, domain: Optional[str] = None, max_results: int = 3) -> dict:
|
|
111
|
+
payload = {"query": query, "max_results": max_results}
|
|
112
|
+
if domain:
|
|
113
|
+
payload["domain"] = domain
|
|
114
|
+
return _post("search", payload)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def search_stack(query: str, stack: str, max_results: int = 3) -> dict:
|
|
118
|
+
return _post("search/stack", {"query": query, "stack": stack, "max_results": max_results})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def design_system(query: str, project_name: Optional[str] = None, fmt: str = "ascii") -> dict:
|
|
122
|
+
payload = {"query": query, "format": fmt}
|
|
123
|
+
if project_name:
|
|
124
|
+
payload["project_name"] = project_name
|
|
125
|
+
return _post("design-system", payload)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ─────────────────────────────────────────────
|
|
129
|
+
# Output formatting (mirrors original search.py)
|
|
130
|
+
# ─────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def format_search_output(result: dict) -> str:
|
|
133
|
+
if "error" in result:
|
|
134
|
+
return f"Error: {result['error']}"
|
|
135
|
+
|
|
136
|
+
output = []
|
|
137
|
+
if result.get("stack"):
|
|
138
|
+
output.append("## Lokuma Stack Guidelines")
|
|
139
|
+
output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}")
|
|
140
|
+
else:
|
|
141
|
+
output.append("## Lokuma Search Results")
|
|
142
|
+
output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
|
|
143
|
+
output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n")
|
|
144
|
+
|
|
145
|
+
for i, row in enumerate(result.get("results", []), 1):
|
|
146
|
+
output.append(f"### Result {i}")
|
|
147
|
+
for key, value in row.items():
|
|
148
|
+
value_str = str(value)
|
|
149
|
+
if len(value_str) > 300:
|
|
150
|
+
value_str = value_str[:300] + "..."
|
|
151
|
+
output.append(f"- **{key}:** {value_str}")
|
|
152
|
+
output.append("")
|
|
153
|
+
|
|
154
|
+
return "\n".join(output)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ─────────────────────────────────────────────
|
|
158
|
+
# CLI
|
|
159
|
+
# ─────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
VALID_DOMAINS = [
|
|
162
|
+
"style", "color", "chart", "landing", "product", "ux",
|
|
163
|
+
"typography", "google-fonts", "icons", "react", "web",
|
|
164
|
+
]
|
|
165
|
+
VALID_STACKS = ["react-native"]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main():
|
|
169
|
+
parser = argparse.ArgumentParser(
|
|
170
|
+
description="Lokuma Cloud Search",
|
|
171
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
172
|
+
epilog=__doc__,
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument("query", help="Search query")
|
|
175
|
+
parser.add_argument("--domain", "-d", choices=VALID_DOMAINS, help="Search domain")
|
|
176
|
+
parser.add_argument("--stack", "-s", choices=VALID_STACKS, help="Stack-specific search")
|
|
177
|
+
parser.add_argument("--max-results", "-n", type=int, default=3, help="Max results (default: 3)")
|
|
178
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
179
|
+
# Design system flags
|
|
180
|
+
parser.add_argument("--design-system", "-ds", action="store_true",
|
|
181
|
+
help="Generate complete design system")
|
|
182
|
+
parser.add_argument("--project-name", "-p", type=str, default=None,
|
|
183
|
+
help="Project name for design system header")
|
|
184
|
+
parser.add_argument("--format", "-f", choices=["ascii", "markdown", "json"],
|
|
185
|
+
default="ascii", help="Output format for design system")
|
|
186
|
+
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
|
|
189
|
+
# ── Design system ──
|
|
190
|
+
if args.design_system:
|
|
191
|
+
fmt = args.format
|
|
192
|
+
result = design_system(args.query, args.project_name, fmt)
|
|
193
|
+
if args.json:
|
|
194
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
195
|
+
elif fmt == "json":
|
|
196
|
+
# Pretty-print the nested design_system dict
|
|
197
|
+
ds = result.get("design_system", result)
|
|
198
|
+
print(json.dumps(ds, indent=2, ensure_ascii=False))
|
|
199
|
+
else:
|
|
200
|
+
print(result.get("output", ""))
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# ── Stack search ──
|
|
204
|
+
if args.stack:
|
|
205
|
+
result = search_stack(args.query, args.stack, args.max_results)
|
|
206
|
+
if args.json:
|
|
207
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
208
|
+
else:
|
|
209
|
+
print(format_search_output(result))
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
# ── Domain search ──
|
|
213
|
+
result = search(args.query, args.domain, args.max_results)
|
|
214
|
+
if args.json:
|
|
215
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
216
|
+
else:
|
|
217
|
+
print(format_search_output(result))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
6
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/commands/init.ts
|
|
10
|
+
import { mkdir, writeFile, access } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import prompts from "prompts";
|
|
17
|
+
|
|
18
|
+
// src/types/index.ts
|
|
19
|
+
var AI_TYPES = [
|
|
20
|
+
"claude",
|
|
21
|
+
"cursor",
|
|
22
|
+
"windsurf",
|
|
23
|
+
"copilot",
|
|
24
|
+
"kiro",
|
|
25
|
+
"roocode",
|
|
26
|
+
"codex",
|
|
27
|
+
"qoder",
|
|
28
|
+
"gemini",
|
|
29
|
+
"trae",
|
|
30
|
+
"opencode",
|
|
31
|
+
"continue",
|
|
32
|
+
"codebuddy",
|
|
33
|
+
"droid",
|
|
34
|
+
"all"
|
|
35
|
+
];
|
|
36
|
+
var AI_DISPLAY_NAMES = {
|
|
37
|
+
claude: "Claude Code",
|
|
38
|
+
cursor: "Cursor",
|
|
39
|
+
windsurf: "Windsurf",
|
|
40
|
+
copilot: "GitHub Copilot",
|
|
41
|
+
kiro: "Kiro",
|
|
42
|
+
roocode: "Roo Code",
|
|
43
|
+
codex: "Codex CLI",
|
|
44
|
+
qoder: "Qoder",
|
|
45
|
+
gemini: "Gemini CLI",
|
|
46
|
+
trae: "Trae",
|
|
47
|
+
opencode: "OpenCode",
|
|
48
|
+
continue: "Continue",
|
|
49
|
+
codebuddy: "CodeBuddy",
|
|
50
|
+
droid: "Droid (Factory)",
|
|
51
|
+
all: "All assistants"
|
|
52
|
+
};
|
|
53
|
+
var AI_SKILL_PATHS = {
|
|
54
|
+
claude: { root: ".claude", skillMd: ".claude/skills/lokuma/SKILL.md", scriptPy: ".claude/skills/lokuma/scripts/search.py" },
|
|
55
|
+
cursor: { root: ".cursor", skillMd: ".cursor/rules/lokuma.mdc", scriptPy: ".cursor/rules/lokuma/scripts/search.py" },
|
|
56
|
+
windsurf: { root: ".windsurf", skillMd: ".windsurf/rules/lokuma.md", scriptPy: ".windsurf/rules/lokuma/scripts/search.py" },
|
|
57
|
+
copilot: { root: ".github", skillMd: ".github/instructions/lokuma.instructions.md", scriptPy: ".github/instructions/lokuma/scripts/search.py" },
|
|
58
|
+
kiro: { root: ".kiro", skillMd: ".kiro/steering/lokuma.md", scriptPy: ".kiro/steering/lokuma/scripts/search.py" },
|
|
59
|
+
roocode: { root: ".roo", skillMd: ".roo/rules/lokuma.md", scriptPy: ".roo/rules/lokuma/scripts/search.py" },
|
|
60
|
+
codex: { root: ".codex", skillMd: ".codex/instructions.md", scriptPy: ".codex/lokuma/scripts/search.py" },
|
|
61
|
+
qoder: { root: ".qoder", skillMd: ".qoder/rules/lokuma.md", scriptPy: ".qoder/rules/lokuma/scripts/search.py" },
|
|
62
|
+
gemini: { root: ".gemini", skillMd: ".gemini/GEMINI.md", scriptPy: ".gemini/lokuma/scripts/search.py" },
|
|
63
|
+
trae: { root: ".trae", skillMd: ".trae/rules/lokuma.md", scriptPy: ".trae/rules/lokuma/scripts/search.py" },
|
|
64
|
+
opencode: { root: ".opencode", skillMd: ".opencode/OPENCODE.md", scriptPy: ".opencode/lokuma/scripts/search.py" },
|
|
65
|
+
continue: { root: ".continue", skillMd: ".continue/lokuma/SKILL.md", scriptPy: ".continue/lokuma/scripts/search.py" },
|
|
66
|
+
codebuddy: { root: ".codebuddy", skillMd: ".codebuddy/lokuma/SKILL.md", scriptPy: ".codebuddy/lokuma/scripts/search.py" },
|
|
67
|
+
droid: { root: ".factory", skillMd: ".factory/skills/lokuma/SKILL.md", scriptPy: ".factory/skills/lokuma/scripts/search.py" }
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/commands/init.ts
|
|
71
|
+
import { createRequire } from "node:module";
|
|
72
|
+
var _require = createRequire(import.meta.url);
|
|
73
|
+
var _pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
74
|
+
var SEARCH_PY = readFileSync(join(_pkgDir, "assets", "search.py"), "utf-8");
|
|
75
|
+
function buildSkillMd(scriptPath) {
|
|
76
|
+
return `---
|
|
77
|
+
name: lokuma
|
|
78
|
+
description: Lokuma design intelligence. 67 UI styles, 161 color palettes, 57 font pairings, 25 chart types, 99 UX guidelines. Triggers: build, design, create, implement UI/UX. Projects: landing page, dashboard, SaaS, e-commerce, portfolio, mobile app. Styles: glassmorphism, minimalism, brutalism, neumorphism, bento grid, dark mode.
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# Lokuma - Design Intelligence
|
|
82
|
+
|
|
83
|
+
## Setup
|
|
84
|
+
|
|
85
|
+
Set your API key (required):
|
|
86
|
+
\`\`\`bash
|
|
87
|
+
export LOKUMA_API_KEY=lokuma_your_key_here
|
|
88
|
+
\`\`\`
|
|
89
|
+
|
|
90
|
+
Get your API key at https://lokuma.io
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
### Generate Design System (start here)
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
python3 ${scriptPath} "<product description>" --design-system [-p "Project Name"]
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
\`\`\`bash
|
|
101
|
+
python3 ${scriptPath} "beauty spa wellness" --design-system -p "Serenity Spa"
|
|
102
|
+
\`\`\`
|
|
103
|
+
|
|
104
|
+
### Domain Search
|
|
105
|
+
\`\`\`bash
|
|
106
|
+
python3 ${scriptPath} "<keyword>" --domain <domain>
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
Domains: \`style\` \`color\` \`typography\` \`chart\` \`landing\` \`product\` \`ux\` \`icons\`
|
|
110
|
+
|
|
111
|
+
### Stack Guidelines
|
|
112
|
+
\`\`\`bash
|
|
113
|
+
python3 ${scriptPath} "<keyword>" --stack react-native
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Workflow
|
|
117
|
+
|
|
118
|
+
1. **New project** \u2192 \`--design-system\` first to get full design system
|
|
119
|
+
2. **Component** \u2192 \`--domain style\` or \`--domain ux\`
|
|
120
|
+
3. **Colors/fonts** \u2192 \`--domain color\` or \`--domain typography\`
|
|
121
|
+
4. **Review** \u2192 \`--domain ux\` for best practices
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
async function exists(path) {
|
|
125
|
+
try {
|
|
126
|
+
await access(path);
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function initCommand(options) {
|
|
133
|
+
console.log(chalk.bold("\n Lokuma Design Intelligence Installer\n"));
|
|
134
|
+
let aiType = options.ai;
|
|
135
|
+
if (!aiType) {
|
|
136
|
+
const res = await prompts({
|
|
137
|
+
type: "select",
|
|
138
|
+
name: "aiType",
|
|
139
|
+
message: "Select your AI coding assistant:",
|
|
140
|
+
choices: AI_TYPES.map((t) => ({ title: AI_DISPLAY_NAMES[t], value: t }))
|
|
141
|
+
});
|
|
142
|
+
if (!res.aiType) {
|
|
143
|
+
console.log(chalk.yellow(" Cancelled."));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
aiType = res.aiType;
|
|
147
|
+
}
|
|
148
|
+
const targets = aiType === "all" ? AI_TYPES.filter((t) => t !== "all") : [aiType];
|
|
149
|
+
const spinner = ora("Installing\u2026").start();
|
|
150
|
+
const installed = [];
|
|
151
|
+
try {
|
|
152
|
+
for (const t of targets) {
|
|
153
|
+
const paths = AI_SKILL_PATHS[t];
|
|
154
|
+
if (!options.force && await exists(paths.skillMd)) {
|
|
155
|
+
spinner.warn(`${AI_DISPLAY_NAMES[t]}: already installed (use --force to overwrite)`);
|
|
156
|
+
spinner.start();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
await mkdir(dirname(paths.skillMd), { recursive: true });
|
|
160
|
+
await mkdir(dirname(paths.scriptPy), { recursive: true });
|
|
161
|
+
await writeFile(paths.skillMd, buildSkillMd(paths.scriptPy), "utf-8");
|
|
162
|
+
await writeFile(paths.scriptPy, SEARCH_PY, "utf-8");
|
|
163
|
+
installed.push(AI_DISPLAY_NAMES[t]);
|
|
164
|
+
}
|
|
165
|
+
spinner.succeed("Done!");
|
|
166
|
+
if (installed.length > 0) {
|
|
167
|
+
console.log();
|
|
168
|
+
installed.forEach((name) => console.log(chalk.green(` \u2713 ${name}`)));
|
|
169
|
+
}
|
|
170
|
+
console.log();
|
|
171
|
+
console.log(chalk.bold(" Next steps:"));
|
|
172
|
+
console.log(chalk.dim(" 1. Set your API key:"));
|
|
173
|
+
console.log(chalk.cyan(" export LOKUMA_API_KEY=lokuma_your_key_here"));
|
|
174
|
+
console.log(chalk.dim(" 2. Get a key \u2192 https://lokuma.io"));
|
|
175
|
+
console.log(chalk.dim(" 3. Restart your AI assistant"));
|
|
176
|
+
console.log(chalk.dim(' 4. Try: "Build a landing page for a SaaS product"'));
|
|
177
|
+
console.log();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
spinner.fail("Installation failed");
|
|
180
|
+
if (err instanceof Error) console.error(chalk.red(` ${err.message}`));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/index.ts
|
|
186
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
187
|
+
var __dirname = dirname2(__filename);
|
|
188
|
+
var pkg = JSON.parse(readFileSync2(join2(__dirname, "../package.json"), "utf-8"));
|
|
189
|
+
var program = new Command();
|
|
190
|
+
program.name("lokuma").description("Install Lokuma design intelligence skill for AI coding assistants").version(pkg.version);
|
|
191
|
+
program.command("init").description("Install Lokuma skill to current project").option("-a, --ai <type>", `AI assistant type (${AI_TYPES.join(", ")})`).option("-f, --force", "Overwrite existing files").action(async (options) => {
|
|
192
|
+
if (options.ai && !AI_TYPES.includes(options.ai)) {
|
|
193
|
+
console.error(`Invalid AI type: ${options.ai}`);
|
|
194
|
+
console.error(`Valid types: ${AI_TYPES.join(", ")}`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
await initCommand({
|
|
198
|
+
ai: options.ai,
|
|
199
|
+
force: options.force
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lokuma-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to install Lokuma design intelligence skill for AI coding assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lokuma": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"assets"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --format=esm --external:commander --external:chalk --external:ora --external:prompts",
|
|
15
|
+
"dev": "node --loader ts-node/esm src/index.ts",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"lokuma",
|
|
20
|
+
"ui",
|
|
21
|
+
"ux",
|
|
22
|
+
"design",
|
|
23
|
+
"claude",
|
|
24
|
+
"cursor",
|
|
25
|
+
"windsurf",
|
|
26
|
+
"copilot",
|
|
27
|
+
"kiro",
|
|
28
|
+
"codex",
|
|
29
|
+
"ai",
|
|
30
|
+
"skill"
|
|
31
|
+
],
|
|
32
|
+
"author": "s1non",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"chalk": "^5.3.0",
|
|
36
|
+
"commander": "^12.1.0",
|
|
37
|
+
"ora": "^8.1.1",
|
|
38
|
+
"prompts": "^2.4.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/bun": "^1.1.14",
|
|
42
|
+
"@types/node": "^22.10.1",
|
|
43
|
+
"@types/prompts": "^2.4.9",
|
|
44
|
+
"esbuild": "^0.27.4",
|
|
45
|
+
"typescript": "^5.7.2"
|
|
46
|
+
}
|
|
47
|
+
}
|