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 +307 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +28 -0
- package/dist/chunk-AUVNDYJL.js +26 -0
- package/dist/chunk-E6WOLYO3.js +289 -0
- package/dist/cli-PUXM5F6I.js +1083 -0
- package/dist/server-R2ZGYYBT.js +954 -0
- package/package.json +37 -0
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
|
+
};
|