loor-cli 0.1.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 ADDED
@@ -0,0 +1,307 @@
1
+ # loor CLI
2
+
3
+ CLI scaffolding tool for loor monorepo projects.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g loor
9
+ # or
10
+ pnpm add -g loor
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ### `loor init [name]`
16
+
17
+ Create a new loor monorepo project.
18
+
19
+ ```bash
20
+ loor init my-project
21
+ ```
22
+
23
+ #### Arguments
24
+
25
+ | Argument | Required | Description |
26
+ |----------|----------|-------------|
27
+ | `name` | No | Project name. If omitted, prompted interactively. |
28
+
29
+ #### Options
30
+
31
+ | Option | Description | Example |
32
+ |--------|-------------|---------|
33
+ | `--apps <apps>` | App types with optional names (comma-separated). Format: `type` or `type:name`. | `--apps api:backend,web-client:dashboard` |
34
+ | `--packages <packages>` | Optional packages to include (comma-separated). | `--packages mediaKit,notification` |
35
+ | `--no-packages` | Skip optional packages. Only required packages are installed. | |
36
+
37
+ #### Usage Examples
38
+
39
+ **Fully interactive** (prompts for everything):
40
+ ```bash
41
+ loor init
42
+ ```
43
+
44
+ **Non-interactive** (no prompts):
45
+ ```bash
46
+ loor init my-saas \
47
+ --apps api:backend,web-client:dashboard \
48
+ --packages mediaKit,notification
49
+ ```
50
+
51
+ **Partial — only skip app selection:**
52
+ ```bash
53
+ loor init my-saas --apps api,web-client
54
+ # App names default to "api" and "admin"
55
+ # Packages will be prompted interactively
56
+ ```
57
+
58
+ **Partial — only skip package selection:**
59
+ ```bash
60
+ loor init my-saas --no-packages
61
+ # App types and names will be prompted interactively
62
+ # No optional packages installed (only required ones)
63
+ ```
64
+
65
+ **Custom app names:**
66
+ ```bash
67
+ loor init my-saas --apps api:my-api,web-client:admin,web-client:portal
68
+ ```
69
+
70
+ #### App Types
71
+
72
+ | Type | Description | Default Name |
73
+ |------|-------------|--------------|
74
+ | `api` | Express.js + MongoDB REST API server | `api` |
75
+ | `web-client` | React + Vite admin panel / frontend | `admin` |
76
+
77
+ Multiple apps of the same type are allowed (e.g., two `web-client` apps with different names).
78
+
79
+ #### Required Packages
80
+
81
+ These packages are always installed based on selected app types — they cannot be deselected:
82
+
83
+ **API apps:** `auth`, `base`, `expressRouteKit`, `logger`, `eslintConfig`, `typescriptConfig`
84
+
85
+ **Web Client apps:** `auth`, `base`, `storage`, `ui`, `i18n`, `routerProvider`, `commandMenu`, `apiClient`, `eslintConfig`, `typescriptConfig`
86
+
87
+ #### What It Does
88
+
89
+ 1. Creates project directory with root config files (`turbo.json`, `pnpm-workspace.yaml`, `tsconfig.json`, etc.)
90
+ 2. Generates `package.json` programmatically
91
+ 3. Copies selected app templates from the reference apps
92
+ 4. Copies all packages, then removes artifacts of unselected packages (subtractive approach)
93
+ 5. Resolves transitive package dependencies automatically
94
+ 6. Replaces `@loor/` scope with `@<projectName>/` across the project
95
+
96
+ ---
97
+
98
+ ### `loor add app <type> [name]`
99
+
100
+ Add a new app to an existing project.
101
+
102
+ ```bash
103
+ loor add app api backend
104
+ loor add app web-client dashboard
105
+ ```
106
+
107
+ #### Arguments
108
+
109
+ | Argument | Required | Description |
110
+ |----------|----------|-------------|
111
+ | `type` | Yes | App type: `api` or `web-client` |
112
+ | `name` | No | App name. Defaults to `api` for API, `admin` for web-client. |
113
+
114
+ #### What It Does
115
+
116
+ 1. Copies reference app template to `apps/<name>/`
117
+ 2. Installs required packages for the app type (if missing)
118
+ 3. Removes artifacts of non-installed packages from the new app
119
+
120
+ ---
121
+
122
+ ### `loor add package <name>`
123
+
124
+ Add a package to an existing project.
125
+
126
+ ```bash
127
+ loor add package mediaKit
128
+ loor add package notification
129
+ ```
130
+
131
+ #### Arguments
132
+
133
+ | Argument | Required | Description |
134
+ |----------|----------|-------------|
135
+ | `name` | Yes | Package name from the registry |
136
+
137
+ #### What It Does
138
+
139
+ 1. Resolves transitive dependencies automatically
140
+ 2. Copies package source to `packages/<name>/`
141
+ 3. Generates infra adapters in API apps (`src/infra/`)
142
+ 4. Generates client setup files in client apps
143
+ 5. Adds env vars to `.env.example`
144
+ 6. Merges npm dependencies into app `package.json` files
145
+ 7. Injects config annotation blocks
146
+
147
+ ---
148
+
149
+ ### `loor list`
150
+
151
+ List all available packages with their descriptions, categories, and compatibility.
152
+
153
+ ```bash
154
+ loor list
155
+ ```
156
+
157
+ Packages are organized by category: **core**, **server**, **client**, **config**.
158
+
159
+ ---
160
+
161
+ ### `loor update`
162
+
163
+ Force-refresh the package registry from the remote source.
164
+
165
+ ```bash
166
+ loor update
167
+ ```
168
+
169
+ The registry is cached locally (~/.loor/registry/) with a 24-hour TTL. This command forces a re-download.
170
+
171
+ ---
172
+
173
+ ### `loor config`
174
+
175
+ Configure the CLI registry source (local development or remote production).
176
+
177
+ ```bash
178
+ # Show current config
179
+ loor config
180
+
181
+ # Use local repo as source (for CLI development)
182
+ loor config --local
183
+ loor config --local /path/to/loor
184
+
185
+ # Switch back to remote
186
+ loor config --remote
187
+ ```
188
+
189
+ #### Options
190
+
191
+ | Option | Description |
192
+ |--------|-------------|
193
+ | `--local [path]` | Use a local repo as scaffold source. Defaults to current directory. |
194
+ | `--remote` | Use the remote GitHub registry (production). |
195
+
196
+ The `LOOR_LOCAL` environment variable overrides any persisted config.
197
+
198
+ ---
199
+
200
+ ### `loor ai init`
201
+
202
+ Generate AI context files for the project.
203
+
204
+ ```bash
205
+ loor ai init
206
+ ```
207
+
208
+ #### What It Does
209
+
210
+ 1. Generates `CLAUDE.md` with project-specific context (apps, packages, conventions, routes, infra)
211
+ 2. Generates `.mcp.json` to register the loor MCP server
212
+
213
+ ---
214
+
215
+ ## Global Options
216
+
217
+ | Option | Description |
218
+ |--------|-------------|
219
+ | `--debug` | Enable debug mode with detailed error output |
220
+ | `--version` | Show CLI version |
221
+ | `--help` | Show help for any command |
222
+
223
+ ```bash
224
+ loor --debug init my-project
225
+ loor init --help
226
+ loor add app --help
227
+ ```
228
+
229
+ ---
230
+
231
+ ## MCP Server
232
+
233
+ The CLI includes a built-in MCP (Model Context Protocol) server for AI integrations.
234
+
235
+ ```bash
236
+ loor --mcp
237
+ ```
238
+
239
+ This starts a stdio-based MCP server that exposes the following tools:
240
+
241
+ | Tool | Description |
242
+ |------|-------------|
243
+ | `list_packages` | List available packages with optional category/compatibility filters |
244
+ | `get_package_guide` | Get detailed guide for a specific package (patterns, infra, env vars) |
245
+ | `get_conventions` | Get project conventions (architecture, routes, features) |
246
+ | `how_to` | Step-by-step guides for common tasks |
247
+ | `cli_usage` | Complete CLI command reference and usage examples |
248
+
249
+ The MCP server is auto-registered via `loor ai init`.
250
+
251
+ ---
252
+
253
+ ## Environment Variables
254
+
255
+ | Variable | Description | Default |
256
+ |----------|-------------|---------|
257
+ | `LOOR_LOCAL` | Path to local loor repo (overrides config) | — |
258
+ | `LOOR_REPO` | GitHub repository | `ismailyagci/loor` |
259
+ | `LOOR_BRANCH` | Git branch to use | `master` |
260
+
261
+ ---
262
+
263
+ ## Workflow Examples
264
+
265
+ ### Create a full-stack project (interactive)
266
+
267
+ ```bash
268
+ loor init my-app
269
+ # Select: api + web-client
270
+ # Name api: api
271
+ # Name web-client: admin
272
+ # Select packages: mediaKit, notification
273
+ cd my-app && pnpm install && pnpm dev
274
+ ```
275
+
276
+ ### Create a full-stack project (non-interactive / CI)
277
+
278
+ ```bash
279
+ loor init my-app \
280
+ --apps api,web-client:admin \
281
+ --packages mediaKit,notification
282
+ cd my-app && pnpm install && pnpm dev
283
+ ```
284
+
285
+ ### Add a second frontend to an existing project
286
+
287
+ ```bash
288
+ cd my-app
289
+ loor add app web-client portal
290
+ pnpm install
291
+ ```
292
+
293
+ ### Add a package to an existing project
294
+
295
+ ```bash
296
+ cd my-app
297
+ loor add package mediaKit
298
+ pnpm install
299
+ ```
300
+
301
+ ### Set up AI tooling
302
+
303
+ ```bash
304
+ cd my-app
305
+ loor ai init
306
+ # CLAUDE.md and .mcp.json are generated
307
+ ```
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/bin.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ enableDebug,
4
+ isDebug,
5
+ log
6
+ } from "./chunk-AUVNDYJL.js";
7
+
8
+ // src/bin.ts
9
+ if (process.argv.includes("--debug")) enableDebug();
10
+ try {
11
+ if (process.argv.includes("--mcp")) {
12
+ const { startMcpServer } = await import("./server-R2ZGYYBT.js");
13
+ await startMcpServer();
14
+ } else {
15
+ const { cli } = await import("./cli-PUXM5F6I.js");
16
+ await cli.parseAsync(process.argv);
17
+ }
18
+ } catch (err) {
19
+ if (isDebug()) {
20
+ console.error("");
21
+ log.error("Command failed with error:");
22
+ console.error(err instanceof Error ? err.stack : err);
23
+ } else {
24
+ log.error(err instanceof Error ? err.message : String(err));
25
+ log.dim(" Run with --debug for full error details.");
26
+ }
27
+ process.exit(1);
28
+ }
@@ -0,0 +1,26 @@
1
+ // src/utils/logger.ts
2
+ import chalk from "chalk";
3
+ var debugEnabled = false;
4
+ function enableDebug() {
5
+ debugEnabled = true;
6
+ }
7
+ function isDebug() {
8
+ return debugEnabled;
9
+ }
10
+ var log = {
11
+ info: (msg) => console.log(chalk.blue("info") + " " + msg),
12
+ success: (msg) => console.log(chalk.green("done") + " " + msg),
13
+ warn: (msg) => console.log(chalk.yellow("warn") + " " + msg),
14
+ error: (msg) => console.log(chalk.red("error") + " " + msg),
15
+ step: (msg) => console.log(chalk.cyan(">>") + " " + msg),
16
+ dim: (msg) => console.log(chalk.dim(msg)),
17
+ debug: (msg) => {
18
+ if (debugEnabled) console.log(chalk.magenta("debug") + " " + msg);
19
+ }
20
+ };
21
+
22
+ export {
23
+ enableDebug,
24
+ isDebug,
25
+ log
26
+ };
@@ -0,0 +1,289 @@
1
+ import {
2
+ log
3
+ } from "./chunk-AUVNDYJL.js";
4
+
5
+ // src/registry/packageRegistry.ts
6
+ import path from "path";
7
+ import fs from "fs-extra";
8
+ var PackageRegistry = class {
9
+ packages = /* @__PURE__ */ new Map();
10
+ packagesDir;
11
+ constructor(packagesDir) {
12
+ this.packagesDir = packagesDir;
13
+ }
14
+ async load() {
15
+ const registryJsonPath = path.join(this.packagesDir, "..", "registry.json");
16
+ if (await fs.pathExists(registryJsonPath)) {
17
+ const manifests = await fs.readJson(registryJsonPath);
18
+ for (const m of manifests) {
19
+ this.packages.set(m.name, m);
20
+ }
21
+ return;
22
+ }
23
+ const entries = await fs.readdir(this.packagesDir, { withFileTypes: true });
24
+ for (const entry of entries) {
25
+ if (!entry.isDirectory()) continue;
26
+ if (entry.name === "cli") continue;
27
+ const manifestPath = path.join(this.packagesDir, entry.name, "loor.json");
28
+ if (await fs.pathExists(manifestPath)) {
29
+ const manifest = await fs.readJson(manifestPath);
30
+ this.packages.set(manifest.name, manifest);
31
+ }
32
+ }
33
+ }
34
+ get(name) {
35
+ return this.packages.get(name);
36
+ }
37
+ getAll() {
38
+ return Array.from(this.packages.values());
39
+ }
40
+ getByCategory(category) {
41
+ return this.getAll().filter((pkg) => pkg.category === category);
42
+ }
43
+ getCompatible(appType) {
44
+ return this.getAll().filter((pkg) => pkg.compatibility.includes(appType));
45
+ }
46
+ getPackagesDir() {
47
+ return this.packagesDir;
48
+ }
49
+ };
50
+
51
+ // src/utils/fs.ts
52
+ import path3 from "path";
53
+ import os2 from "os";
54
+ import fs3 from "fs-extra";
55
+
56
+ // src/utils/config.ts
57
+ import path2 from "path";
58
+ import os from "os";
59
+ import fs2 from "fs-extra";
60
+ var CONFIG_DIR = path2.join(os.homedir(), ".loor");
61
+ var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
62
+ var DEFAULT_CONFIG = { source: "remote" };
63
+ async function readConfig() {
64
+ if (!await fs2.pathExists(CONFIG_PATH)) return DEFAULT_CONFIG;
65
+ try {
66
+ return await fs2.readJson(CONFIG_PATH);
67
+ } catch {
68
+ return DEFAULT_CONFIG;
69
+ }
70
+ }
71
+ async function writeConfig(config) {
72
+ await fs2.ensureDir(CONFIG_DIR);
73
+ await fs2.writeJson(CONFIG_PATH, config, { spaces: 2 });
74
+ }
75
+
76
+ // src/utils/fs.ts
77
+ async function getRegistryDir() {
78
+ if (process.env.LOOR_LOCAL) return process.env.LOOR_LOCAL;
79
+ const config = await readConfig();
80
+ if (config.source === "local" && config.localPath) return config.localPath;
81
+ return path3.join(os2.homedir(), ".loor", "registry");
82
+ }
83
+ async function getPackagesDir() {
84
+ return path3.join(await getRegistryDir(), "packages");
85
+ }
86
+ async function getAppsDir() {
87
+ return path3.join(await getRegistryDir(), "apps");
88
+ }
89
+ async function detectProject(dir) {
90
+ return fs3.pathExists(path3.join(dir, "turbo.json"));
91
+ }
92
+ async function removeBetweenAnnotations(filePath, packageName) {
93
+ if (!await fs3.pathExists(filePath)) return;
94
+ const content = await fs3.readFile(filePath, "utf-8");
95
+ const lines = content.split("\n");
96
+ const result = [];
97
+ let skipping = false;
98
+ const startPatterns = [
99
+ `// @loor:package(${packageName})`,
100
+ `# @loor:package(${packageName})`
101
+ ];
102
+ const endPatterns = [
103
+ `// @loor:end(${packageName})`,
104
+ `# @loor:end(${packageName})`
105
+ ];
106
+ for (const line of lines) {
107
+ const trimmed = line.trim();
108
+ if (startPatterns.some((p) => trimmed === p)) {
109
+ skipping = true;
110
+ continue;
111
+ }
112
+ if (endPatterns.some((p) => trimmed === p)) {
113
+ skipping = false;
114
+ continue;
115
+ }
116
+ if (!skipping) {
117
+ result.push(line);
118
+ }
119
+ }
120
+ await fs3.writeFile(filePath, result.join("\n"), "utf-8");
121
+ }
122
+ async function addAnnotatedBlock(filePath, packageName, content, commentPrefix = "//") {
123
+ if (!await fs3.pathExists(filePath)) return;
124
+ const fileContent = await fs3.readFile(filePath, "utf-8");
125
+ const startMarker = `${commentPrefix} @loor:package(${packageName})`;
126
+ const endMarker = `${commentPrefix} @loor:end(${packageName})`;
127
+ if (fileContent.includes(startMarker)) return;
128
+ const block = `
129
+ ${startMarker}
130
+ ${content}
131
+ ${endMarker}`;
132
+ if (filePath.endsWith(".ts")) {
133
+ const lastClose = fileContent.lastIndexOf("};");
134
+ if (lastClose !== -1) {
135
+ const before = fileContent.slice(0, lastClose);
136
+ const after = fileContent.slice(lastClose);
137
+ await fs3.writeFile(filePath, before + block + "\n" + after, "utf-8");
138
+ return;
139
+ }
140
+ }
141
+ const envBlock = `
142
+ ${startMarker}
143
+ ${content}
144
+ ${endMarker}
145
+ `;
146
+ await fs3.writeFile(filePath, fileContent + envBlock, "utf-8");
147
+ }
148
+ async function replaceInFile(filePath, search, replace) {
149
+ if (!await fs3.pathExists(filePath)) return;
150
+ const content = await fs3.readFile(filePath, "utf-8");
151
+ const updated = content.split(search).join(replace);
152
+ await fs3.writeFile(filePath, updated, "utf-8");
153
+ }
154
+ async function removeDependencies(packageJsonPath, depNames) {
155
+ if (!await fs3.pathExists(packageJsonPath)) return;
156
+ const pkg = await fs3.readJson(packageJsonPath);
157
+ for (const name of depNames) {
158
+ if (pkg.dependencies) delete pkg.dependencies[name];
159
+ if (pkg.devDependencies) delete pkg.devDependencies[name];
160
+ }
161
+ await fs3.writeJson(packageJsonPath, pkg, { spaces: 2 });
162
+ }
163
+ async function mergeDependencies(packageJsonPath, deps, type = "dependencies") {
164
+ if (!await fs3.pathExists(packageJsonPath)) return;
165
+ const pkg = await fs3.readJson(packageJsonPath);
166
+ pkg[type] = { ...pkg[type], ...deps };
167
+ pkg[type] = Object.fromEntries(
168
+ Object.entries(pkg[type]).sort(([a], [b]) => a.localeCompare(b))
169
+ );
170
+ await fs3.writeJson(packageJsonPath, pkg, { spaces: 2 });
171
+ }
172
+
173
+ // src/utils/cache.ts
174
+ import path4 from "path";
175
+ import os3 from "os";
176
+ import fs4 from "fs-extra";
177
+ import { execSync } from "child_process";
178
+ var CACHE_DIR = path4.join(os3.homedir(), ".loor");
179
+ var REGISTRY_DIR = path4.join(CACHE_DIR, "registry");
180
+ var META_PATH = path4.join(CACHE_DIR, "meta.json");
181
+ var DEFAULT_REPO = "ismailyagci/loor";
182
+ var DEFAULT_BRANCH = "master";
183
+ var CACHE_TTL = 24 * 60 * 60 * 1e3;
184
+ var GITHUB_TOKEN = "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";
185
+ function getRepo() {
186
+ return process.env.LOOR_REPO || DEFAULT_REPO;
187
+ }
188
+ function getBranch() {
189
+ return process.env.LOOR_BRANCH || DEFAULT_BRANCH;
190
+ }
191
+ async function readMeta() {
192
+ if (!await fs4.pathExists(META_PATH)) return null;
193
+ try {
194
+ return await fs4.readJson(META_PATH);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ async function writeMeta(meta) {
200
+ await fs4.ensureDir(CACHE_DIR);
201
+ await fs4.writeJson(META_PATH, meta, { spaces: 2 });
202
+ }
203
+ function isCacheValid(meta) {
204
+ const repo = getRepo();
205
+ const branch = getBranch();
206
+ if (meta.repo !== repo || meta.branch !== branch) return false;
207
+ return Date.now() - meta.lastUpdated < CACHE_TTL;
208
+ }
209
+ async function ensureCache(opts) {
210
+ const envPath = process.env.LOOR_LOCAL;
211
+ if (envPath) {
212
+ if (!await fs4.pathExists(envPath)) {
213
+ throw new Error(`LOOR_LOCAL path does not exist: ${envPath}`);
214
+ }
215
+ log.info(`Using local registry: ${envPath}`);
216
+ return envPath;
217
+ }
218
+ const config = await readConfig();
219
+ if (config.source === "local" && config.localPath) {
220
+ if (!await fs4.pathExists(config.localPath)) {
221
+ throw new Error(`Configured local path does not exist: ${config.localPath}
222
+ Run "loor config --remote" to switch back.`);
223
+ }
224
+ log.info(`Using local registry: ${config.localPath}`);
225
+ return config.localPath;
226
+ }
227
+ const meta = await readMeta();
228
+ if (!opts?.force && meta && isCacheValid(meta) && await fs4.pathExists(REGISTRY_DIR)) {
229
+ return REGISTRY_DIR;
230
+ }
231
+ const repo = getRepo();
232
+ const branch = getBranch();
233
+ const tarballUrl = `https://api.github.com/repos/${repo}/tarball/${branch}`;
234
+ log.info(`Fetching registry from ${repo}@${branch}...`);
235
+ try {
236
+ await downloadAndExtract(tarballUrl, REGISTRY_DIR);
237
+ } catch (err) {
238
+ if (meta && await fs4.pathExists(REGISTRY_DIR)) {
239
+ log.warn("Failed to update registry, using cached version.");
240
+ return REGISTRY_DIR;
241
+ }
242
+ throw new Error(
243
+ `Failed to download registry from ${tarballUrl}. Check your internet connection and ensure the repo exists.
244
+ ${err instanceof Error ? err.message : String(err)}`
245
+ );
246
+ }
247
+ await writeMeta({ lastUpdated: Date.now(), repo, branch });
248
+ log.success("Registry updated.");
249
+ return REGISTRY_DIR;
250
+ }
251
+ async function downloadAndExtract(url, destDir) {
252
+ await fs4.ensureDir(CACHE_DIR);
253
+ const tmpFile = path4.join(CACHE_DIR, "registry.tar.gz");
254
+ try {
255
+ const headers = { Accept: "application/vnd.github+json" };
256
+ if (GITHUB_TOKEN) {
257
+ headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`;
258
+ }
259
+ const response = await fetch(url, { headers, redirect: "follow" });
260
+ if (!response.ok) {
261
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
262
+ }
263
+ const buffer = Buffer.from(await response.arrayBuffer());
264
+ await fs4.writeFile(tmpFile, buffer);
265
+ await fs4.remove(destDir);
266
+ await fs4.ensureDir(destDir);
267
+ execSync(`tar -xzf "${tmpFile}" --strip-components=1 -C "${destDir}"`, {
268
+ stdio: "ignore"
269
+ });
270
+ } finally {
271
+ await fs4.remove(tmpFile);
272
+ }
273
+ }
274
+
275
+ export {
276
+ PackageRegistry,
277
+ readConfig,
278
+ writeConfig,
279
+ getRegistryDir,
280
+ getPackagesDir,
281
+ getAppsDir,
282
+ detectProject,
283
+ removeBetweenAnnotations,
284
+ addAnnotatedBlock,
285
+ replaceInFile,
286
+ removeDependencies,
287
+ mergeDependencies,
288
+ ensureCache
289
+ };