@raven.js/cli 0.0.1
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/index.ts +795 -0
- package/package.json +44 -0
- package/registry.json +29 -0
- package/scripts/generate-registry.ts +96 -0
package/index.ts
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { cac } from "cac";
|
|
4
|
+
import { mkdir, rm, readdir, stat, chmod, rename } from "node:fs/promises";
|
|
5
|
+
import { join, dirname, resolve, isAbsolute } from "path";
|
|
6
|
+
import { cwd } from "process";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
import { spinner as makeSpinner, log } from "@clack/prompts";
|
|
9
|
+
import { parse, stringify } from "yaml";
|
|
10
|
+
|
|
11
|
+
// @ts-ignore -- registry.json should generated dynamically
|
|
12
|
+
import registryPath from "./registry.json" with { type: "file" };
|
|
13
|
+
const GITHUB_REPO = "myWsq/RavenJS";
|
|
14
|
+
const GITHUB_RAW_URL = `https://raw.githubusercontent.com/${GITHUB_REPO}`;
|
|
15
|
+
const DEFAULT_ROOT = "raven";
|
|
16
|
+
|
|
17
|
+
interface CLIOptions {
|
|
18
|
+
verbose?: boolean;
|
|
19
|
+
root?: string;
|
|
20
|
+
source?: string;
|
|
21
|
+
prerelease?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RegistryModule {
|
|
25
|
+
files: string[];
|
|
26
|
+
fileMapping?: Record<string, string>;
|
|
27
|
+
dependencies?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RegistryAi {
|
|
31
|
+
claude: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Registry {
|
|
35
|
+
version: string;
|
|
36
|
+
modules: Record<string, RegistryModule>;
|
|
37
|
+
ai: RegistryAi;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const registry = (await Bun.file(registryPath as unknown as string).json()) as Registry;
|
|
41
|
+
|
|
42
|
+
function getRoot(options: CLIOptions): string {
|
|
43
|
+
return options.root || process.env.RAVEN_ROOT || DEFAULT_ROOT;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getSource(options: CLIOptions): string | undefined {
|
|
47
|
+
return options.source || process.env.RAVEN_SOURCE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveSourcePath(source?: string): string | undefined {
|
|
51
|
+
if (!source || source === "github") return undefined;
|
|
52
|
+
return isAbsolute(source) ? source : resolve(cwd(), source);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function verboseLog(message: string, options?: CLIOptions) {
|
|
56
|
+
if (options?.verbose) {
|
|
57
|
+
console.log(message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function error(message: string): never {
|
|
62
|
+
// Use stderr for programmatic consumption (e.g. tests, piping). @clack/prompts
|
|
63
|
+
// log.error writes to stdout which breaks stderr-based assertions.
|
|
64
|
+
console.error(message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function info(message: string) {
|
|
69
|
+
log.info(message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function success(message: string) {
|
|
73
|
+
log.success(message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printSectionHeader(title: string) {
|
|
77
|
+
log.step(title);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printListItem(item: string) {
|
|
81
|
+
log.message(item, { symbol: pc.dim("-") });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function ensureDir(path: string) {
|
|
85
|
+
try {
|
|
86
|
+
await mkdir(path, { recursive: true });
|
|
87
|
+
} catch (e: any) {
|
|
88
|
+
if (e.code !== "EEXIST") throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function isDirEmpty(path: string): Promise<boolean> {
|
|
93
|
+
try {
|
|
94
|
+
const entries = await readdir(path);
|
|
95
|
+
return entries.length === 0;
|
|
96
|
+
} catch (e: any) {
|
|
97
|
+
if (e.code === "ENOENT") return true;
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
103
|
+
try {
|
|
104
|
+
await stat(path);
|
|
105
|
+
return true;
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
if (e.code === "ENOENT") return false;
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getModuleNames(): string[] {
|
|
113
|
+
return Object.keys(registry.modules);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// === SECTION: Self-Update ===
|
|
117
|
+
|
|
118
|
+
import { gt as semverGt } from "semver";
|
|
119
|
+
|
|
120
|
+
const GITHUB_API_RELEASES = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
|
|
121
|
+
const INSTALL_DIR = `${process.env.HOME || process.env.USERPROFILE || ""}/.local/bin`;
|
|
122
|
+
const INSTALL_PATH = `${INSTALL_DIR}/raven`;
|
|
123
|
+
|
|
124
|
+
function detectOs(): "linux" | "darwin" {
|
|
125
|
+
switch (process.platform) {
|
|
126
|
+
case "linux":
|
|
127
|
+
return "linux";
|
|
128
|
+
case "darwin":
|
|
129
|
+
return "darwin";
|
|
130
|
+
default:
|
|
131
|
+
error(`unsupported OS: ${process.platform}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function detectArch(): "x64" | "arm64" {
|
|
136
|
+
switch (process.arch) {
|
|
137
|
+
case "x64":
|
|
138
|
+
return "x64";
|
|
139
|
+
case "arm64":
|
|
140
|
+
return "arm64";
|
|
141
|
+
default:
|
|
142
|
+
error(`unsupported architecture: ${process.arch}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function getLatestVersion(includePrerelease = false): Promise<string> {
|
|
147
|
+
const response = await fetch(GITHUB_API_RELEASES);
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
error("failed to get latest version");
|
|
150
|
+
}
|
|
151
|
+
const releases = (await response.json()) as { tag_name: string }[];
|
|
152
|
+
const tag = includePrerelease
|
|
153
|
+
? releases[0]?.tag_name
|
|
154
|
+
: releases.map((r) => r.tag_name).find((t) => !t.includes("-"));
|
|
155
|
+
if (!tag) {
|
|
156
|
+
error("failed to get latest version");
|
|
157
|
+
}
|
|
158
|
+
return tag.replace(/^v/, "");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function downloadBinary(
|
|
162
|
+
version: string,
|
|
163
|
+
os: string,
|
|
164
|
+
arch: string,
|
|
165
|
+
): Promise<ArrayBuffer> {
|
|
166
|
+
const url = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/raven-${version}-${os}-${arch}`;
|
|
167
|
+
const response = await fetch(url);
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`download failed (${response.status}). Please check if version v${version} exists and supports ${os}-${arch}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return response.arrayBuffer();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isLocalBinInPath(): boolean {
|
|
177
|
+
const pathEnv = process.env.PATH || "";
|
|
178
|
+
return pathEnv.split(":").includes(INSTALL_DIR);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function downloadFile(url: string, destPath: string): Promise<void> {
|
|
182
|
+
const response = await fetch(url);
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new Error(`Failed to download ${url}: ${response.status}`);
|
|
185
|
+
}
|
|
186
|
+
const content = await response.text();
|
|
187
|
+
await ensureDir(dirname(destPath));
|
|
188
|
+
await Bun.write(destPath, content);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function copyLocalFile(srcPath: string, destPath: string): Promise<void> {
|
|
192
|
+
const exists = await Bun.file(srcPath).exists();
|
|
193
|
+
if (!exists) {
|
|
194
|
+
throw new Error(`Missing local file: ${srcPath}`);
|
|
195
|
+
}
|
|
196
|
+
await ensureDir(dirname(destPath));
|
|
197
|
+
await Bun.write(destPath, Bun.file(srcPath));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function downloadModule(
|
|
201
|
+
moduleName: string,
|
|
202
|
+
version: string,
|
|
203
|
+
destDir: string,
|
|
204
|
+
options?: CLIOptions,
|
|
205
|
+
targetSubdir?: string,
|
|
206
|
+
): Promise<string[]> {
|
|
207
|
+
const module = registry.modules[moduleName];
|
|
208
|
+
if (!module) {
|
|
209
|
+
throw new Error(`Module ${moduleName} not found in registry`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sourcePath = resolveSourcePath(getSource(options || {}));
|
|
213
|
+
if (sourcePath) {
|
|
214
|
+
verboseLog(`Using local source: ${sourcePath}`, options);
|
|
215
|
+
}
|
|
216
|
+
verboseLog(`Downloading ${moduleName} files...`, options);
|
|
217
|
+
|
|
218
|
+
const modifiedFiles: string[] = [];
|
|
219
|
+
const downloads = module.files.map(async (file: string) => {
|
|
220
|
+
let destPath: string;
|
|
221
|
+
|
|
222
|
+
if (module.fileMapping && module.fileMapping[file]) {
|
|
223
|
+
destPath = join(destDir, module.fileMapping[file]);
|
|
224
|
+
} else if (targetSubdir) {
|
|
225
|
+
destPath = join(destDir, targetSubdir, file);
|
|
226
|
+
} else {
|
|
227
|
+
destPath = join(destDir, moduleName, file);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
verboseLog(` Downloading ${file}...`, options);
|
|
231
|
+
|
|
232
|
+
if (sourcePath) {
|
|
233
|
+
const primaryPath = join(sourcePath, "modules", moduleName, file);
|
|
234
|
+
const fallbackPath = join(sourcePath, moduleName, file);
|
|
235
|
+
let sourceFile: string;
|
|
236
|
+
if (await Bun.file(primaryPath).exists()) {
|
|
237
|
+
sourceFile = primaryPath;
|
|
238
|
+
} else if (await Bun.file(fallbackPath).exists()) {
|
|
239
|
+
sourceFile = fallbackPath;
|
|
240
|
+
} else {
|
|
241
|
+
throw new Error(`Missing local file: ${primaryPath}`);
|
|
242
|
+
}
|
|
243
|
+
await copyLocalFile(sourceFile, destPath);
|
|
244
|
+
} else {
|
|
245
|
+
const url = `${GITHUB_RAW_URL}/v${version}/modules/${moduleName}/${file}`;
|
|
246
|
+
await downloadFile(url, destPath);
|
|
247
|
+
}
|
|
248
|
+
modifiedFiles.push(destPath);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await Promise.all(downloads);
|
|
252
|
+
return modifiedFiles;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function downloadAiResources(
|
|
256
|
+
version: string,
|
|
257
|
+
destDir: string,
|
|
258
|
+
options?: CLIOptions,
|
|
259
|
+
): Promise<string[]> {
|
|
260
|
+
const ai = registry.ai;
|
|
261
|
+
if (!ai?.claude) {
|
|
262
|
+
throw new Error("AI resources not found in registry");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const mapping = ai.claude;
|
|
266
|
+
const entries = Object.entries(mapping);
|
|
267
|
+
|
|
268
|
+
const sourcePath = resolveSourcePath(getSource(options || {}));
|
|
269
|
+
if (sourcePath) {
|
|
270
|
+
verboseLog(`Using local source: ${sourcePath}`, options);
|
|
271
|
+
}
|
|
272
|
+
verboseLog("Downloading AI resources...", options);
|
|
273
|
+
|
|
274
|
+
const modifiedFiles: string[] = [];
|
|
275
|
+
const downloads = entries.map(async ([file, destRel]) => {
|
|
276
|
+
const destPath = join(destDir, destRel);
|
|
277
|
+
verboseLog(` Downloading ${file}...`, options);
|
|
278
|
+
|
|
279
|
+
if (sourcePath) {
|
|
280
|
+
const sourceFile = join(sourcePath, "packages", "ai", file);
|
|
281
|
+
await copyLocalFile(sourceFile, destPath);
|
|
282
|
+
} else {
|
|
283
|
+
const url = `${GITHUB_RAW_URL}/v${version}/packages/ai/${file}`;
|
|
284
|
+
await downloadFile(url, destPath);
|
|
285
|
+
}
|
|
286
|
+
modifiedFiles.push(destPath);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await Promise.all(downloads);
|
|
290
|
+
return modifiedFiles;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function cmdInit(options: CLIOptions) {
|
|
294
|
+
const targetDir = cwd();
|
|
295
|
+
|
|
296
|
+
verboseLog(`Initializing RavenJS AI resources in ${targetDir}`, options);
|
|
297
|
+
|
|
298
|
+
const dotClaudeDir = join(targetDir, ".claude");
|
|
299
|
+
|
|
300
|
+
// Check if .claude already exists with content
|
|
301
|
+
if (await pathExists(dotClaudeDir)) {
|
|
302
|
+
const empty = await isDirEmpty(dotClaudeDir);
|
|
303
|
+
if (!empty) {
|
|
304
|
+
error(
|
|
305
|
+
`AI resources already initialized at .claude/. Use 'raven update' to update.`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const version = registry.version;
|
|
311
|
+
const modifiedFiles: string[] = [];
|
|
312
|
+
|
|
313
|
+
if (options?.verbose) {
|
|
314
|
+
verboseLog(`Initializing RavenJS AI resources in ${targetDir}`, options);
|
|
315
|
+
await ensureDir(dotClaudeDir);
|
|
316
|
+
const aiFiles = await downloadAiResources(version, targetDir, options);
|
|
317
|
+
modifiedFiles.push(...aiFiles);
|
|
318
|
+
} else {
|
|
319
|
+
const s = makeSpinner();
|
|
320
|
+
s.start("Initializing RavenJS AI resources...");
|
|
321
|
+
try {
|
|
322
|
+
await ensureDir(dotClaudeDir);
|
|
323
|
+
const aiFiles = await downloadAiResources(version, targetDir, options);
|
|
324
|
+
modifiedFiles.push(...aiFiles);
|
|
325
|
+
s.stop("Initializing RavenJS AI resources...");
|
|
326
|
+
} catch (e: any) {
|
|
327
|
+
s.stop("Initialization failed");
|
|
328
|
+
error(e.message);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
success("RavenJS AI resources initialized successfully!");
|
|
333
|
+
|
|
334
|
+
printSectionHeader("Modified Files");
|
|
335
|
+
for (const file of modifiedFiles) {
|
|
336
|
+
printListItem(file);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
interface RavenYamlConfig {
|
|
341
|
+
version: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function loadRavenYaml(ravenDir: string): Promise<string> {
|
|
345
|
+
const yamlPath = join(ravenDir, "raven.yaml");
|
|
346
|
+
try {
|
|
347
|
+
const content = await Bun.file(yamlPath).text();
|
|
348
|
+
const config = parse(content) as RavenYamlConfig;
|
|
349
|
+
if (!config?.version) {
|
|
350
|
+
throw new Error("Invalid raven.yaml: version field is missing");
|
|
351
|
+
}
|
|
352
|
+
return config.version;
|
|
353
|
+
} catch (e: any) {
|
|
354
|
+
throw new Error(`Failed to load raven.yaml: ${e.message}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function createRavenYaml(destDir: string, version: string) {
|
|
359
|
+
const content = stringify({ version });
|
|
360
|
+
await Bun.write(join(destDir, "raven.yaml"), content);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function cmdInstall(options: CLIOptions) {
|
|
364
|
+
const targetDir = cwd();
|
|
365
|
+
const root = getRoot(options);
|
|
366
|
+
|
|
367
|
+
verboseLog(`Installing RavenJS in ${targetDir}`, options);
|
|
368
|
+
|
|
369
|
+
const ravenDir = join(targetDir, root);
|
|
370
|
+
|
|
371
|
+
if (await pathExists(ravenDir)) {
|
|
372
|
+
const empty = await isDirEmpty(ravenDir);
|
|
373
|
+
if (!empty) {
|
|
374
|
+
error(
|
|
375
|
+
`RavenJS is already installed at ${root}/. Use 'raven update' to update.`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const version = registry.version;
|
|
381
|
+
const modifiedFiles: string[] = [];
|
|
382
|
+
|
|
383
|
+
if (options?.verbose) {
|
|
384
|
+
verboseLog(`Installing RavenJS in ${targetDir}`, options);
|
|
385
|
+
await ensureDir(join(targetDir, root));
|
|
386
|
+
const coreFiles = await downloadModule(
|
|
387
|
+
"core",
|
|
388
|
+
version,
|
|
389
|
+
join(targetDir, root),
|
|
390
|
+
options,
|
|
391
|
+
);
|
|
392
|
+
modifiedFiles.push(...coreFiles);
|
|
393
|
+
} else {
|
|
394
|
+
const s = makeSpinner();
|
|
395
|
+
s.start("Installing RavenJS...");
|
|
396
|
+
try {
|
|
397
|
+
await ensureDir(join(targetDir, root));
|
|
398
|
+
const coreFiles = await downloadModule(
|
|
399
|
+
"core",
|
|
400
|
+
version,
|
|
401
|
+
join(targetDir, root),
|
|
402
|
+
options,
|
|
403
|
+
);
|
|
404
|
+
modifiedFiles.push(...coreFiles);
|
|
405
|
+
s.stop("Installing RavenJS...");
|
|
406
|
+
} catch (e: any) {
|
|
407
|
+
s.stop("Installation failed");
|
|
408
|
+
error(e.message);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
await createRavenYaml(join(targetDir, root), version);
|
|
413
|
+
modifiedFiles.push(join(targetDir, root, "raven.yaml"));
|
|
414
|
+
|
|
415
|
+
success("RavenJS installed successfully!");
|
|
416
|
+
|
|
417
|
+
printSectionHeader("Modified Files");
|
|
418
|
+
for (const file of modifiedFiles) {
|
|
419
|
+
printListItem(file);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const coreModule = registry.modules["core"];
|
|
423
|
+
if (
|
|
424
|
+
coreModule?.dependencies &&
|
|
425
|
+
Object.keys(coreModule.dependencies).length > 0
|
|
426
|
+
) {
|
|
427
|
+
printSectionHeader("Required Dependencies");
|
|
428
|
+
for (const [pkg, ver] of Object.entries(coreModule.dependencies)) {
|
|
429
|
+
printListItem(`${pkg}@${ver}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function cmdAdd(moduleName: string, options: CLIOptions) {
|
|
435
|
+
if (!moduleName) {
|
|
436
|
+
error(
|
|
437
|
+
`Please specify a module to add. Available: ${getModuleNames().join(", ")}`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const available = getModuleNames();
|
|
442
|
+
|
|
443
|
+
if (!available.includes(moduleName)) {
|
|
444
|
+
info(`Available modules: ${available.join(", ")}`);
|
|
445
|
+
error(`Unknown module: ${moduleName}`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const targetDir = cwd();
|
|
450
|
+
const root = getRoot(options);
|
|
451
|
+
const ravenDir = join(targetDir, root);
|
|
452
|
+
|
|
453
|
+
if (!(await pathExists(ravenDir))) {
|
|
454
|
+
error(`RavenJS not installed at ${root}/. Run 'raven install' first.`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let version: string;
|
|
458
|
+
try {
|
|
459
|
+
version = await loadRavenYaml(ravenDir);
|
|
460
|
+
} catch (e: any) {
|
|
461
|
+
error(e.message);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let modifiedFiles: string[];
|
|
466
|
+
if (options?.verbose) {
|
|
467
|
+
verboseLog(`Adding ${moduleName}...`, options);
|
|
468
|
+
modifiedFiles = await downloadModule(moduleName, version, ravenDir, options);
|
|
469
|
+
} else {
|
|
470
|
+
const s = makeSpinner();
|
|
471
|
+
s.start(`Adding ${moduleName}...`);
|
|
472
|
+
try {
|
|
473
|
+
modifiedFiles = await downloadModule(
|
|
474
|
+
moduleName,
|
|
475
|
+
version,
|
|
476
|
+
ravenDir,
|
|
477
|
+
options,
|
|
478
|
+
);
|
|
479
|
+
s.stop(`Adding ${moduleName}...`);
|
|
480
|
+
} catch (e: any) {
|
|
481
|
+
s.stop("Add failed");
|
|
482
|
+
error(e.message);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
success(`${moduleName} added successfully!`);
|
|
487
|
+
|
|
488
|
+
printSectionHeader("Modified Files");
|
|
489
|
+
for (const file of modifiedFiles) {
|
|
490
|
+
printListItem(file);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const module = registry.modules[moduleName];
|
|
494
|
+
if (module?.dependencies && Object.keys(module.dependencies).length > 0) {
|
|
495
|
+
printSectionHeader("Required Dependencies");
|
|
496
|
+
for (const [pkg, ver] of Object.entries(module.dependencies)) {
|
|
497
|
+
printListItem(`${pkg}@${ver}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
log.message(
|
|
502
|
+
`The module has been added to ${root}/${moduleName}\nSee ${root}/${moduleName}/README.md for usage.`,
|
|
503
|
+
{ symbol: pc.dim("~") },
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function cmdUpdate(options: CLIOptions) {
|
|
508
|
+
const targetDir = cwd();
|
|
509
|
+
const root = getRoot(options);
|
|
510
|
+
const ravenDir = join(targetDir, root);
|
|
511
|
+
const dotClaudeDir = join(targetDir, ".claude");
|
|
512
|
+
|
|
513
|
+
// Check if at least one of raven/ or .claude/ exists
|
|
514
|
+
const ravenExists = await pathExists(ravenDir);
|
|
515
|
+
const dotClaudeExists = await pathExists(dotClaudeDir);
|
|
516
|
+
|
|
517
|
+
if (!ravenExists && !dotClaudeExists) {
|
|
518
|
+
error(`RavenJS not installed. Run 'raven install' or 'raven init' first.`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let version: string = registry.version;
|
|
522
|
+
if (ravenExists) {
|
|
523
|
+
try {
|
|
524
|
+
version = await loadRavenYaml(ravenDir);
|
|
525
|
+
} catch (e: any) {
|
|
526
|
+
error(e.message);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const modifiedFiles: string[] = [];
|
|
532
|
+
const allDependencies: Record<string, string> = {};
|
|
533
|
+
|
|
534
|
+
if (options?.verbose) {
|
|
535
|
+
try {
|
|
536
|
+
info(`Updating RavenJS in ${targetDir}...`);
|
|
537
|
+
|
|
538
|
+
// Update framework modules if raven/ exists
|
|
539
|
+
if (ravenExists) {
|
|
540
|
+
const availableModules = getModuleNames();
|
|
541
|
+
for (const moduleName of availableModules) {
|
|
542
|
+
const moduleDir = join(ravenDir, moduleName);
|
|
543
|
+
if (await Bun.file(moduleDir).exists()) {
|
|
544
|
+
await rm(moduleDir, { recursive: true, force: true });
|
|
545
|
+
}
|
|
546
|
+
const files = await downloadModule(
|
|
547
|
+
moduleName,
|
|
548
|
+
version,
|
|
549
|
+
ravenDir,
|
|
550
|
+
options,
|
|
551
|
+
);
|
|
552
|
+
modifiedFiles.push(...files);
|
|
553
|
+
|
|
554
|
+
const module = registry.modules[moduleName];
|
|
555
|
+
if (module?.dependencies) {
|
|
556
|
+
for (const [pkg, ver] of Object.entries(module.dependencies)) {
|
|
557
|
+
allDependencies[pkg] = ver;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Update AI resources if .claude/ exists
|
|
564
|
+
if (dotClaudeExists) {
|
|
565
|
+
verboseLog("Updating AI resources...", options);
|
|
566
|
+
const aiFiles = await downloadAiResources(version, targetDir, options);
|
|
567
|
+
modifiedFiles.push(...aiFiles);
|
|
568
|
+
}
|
|
569
|
+
} catch (e: unknown) {
|
|
570
|
+
error(e instanceof Error ? e.message : String(e));
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
const s = makeSpinner();
|
|
574
|
+
s.start("Updating RavenJS...");
|
|
575
|
+
try {
|
|
576
|
+
// Update framework modules if raven/ exists
|
|
577
|
+
if (ravenExists) {
|
|
578
|
+
const availableModules = getModuleNames();
|
|
579
|
+
for (const moduleName of availableModules) {
|
|
580
|
+
const moduleDir = join(ravenDir, moduleName);
|
|
581
|
+
if (await Bun.file(moduleDir).exists()) {
|
|
582
|
+
await rm(moduleDir, { recursive: true, force: true });
|
|
583
|
+
}
|
|
584
|
+
const files = await downloadModule(moduleName, version, ravenDir, options);
|
|
585
|
+
modifiedFiles.push(...files);
|
|
586
|
+
|
|
587
|
+
const module = registry.modules[moduleName];
|
|
588
|
+
if (module?.dependencies) {
|
|
589
|
+
for (const [pkg, ver] of Object.entries(module.dependencies)) {
|
|
590
|
+
allDependencies[pkg] = ver;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Update AI resources if .claude/ exists
|
|
597
|
+
if (dotClaudeExists) {
|
|
598
|
+
const aiFiles = await downloadAiResources(version, targetDir, options);
|
|
599
|
+
modifiedFiles.push(...aiFiles);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
s.stop("Updating RavenJS...");
|
|
603
|
+
} catch (e: any) {
|
|
604
|
+
s.stop("Update failed");
|
|
605
|
+
error(e.message);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (ravenExists) {
|
|
610
|
+
await createRavenYaml(ravenDir, version);
|
|
611
|
+
modifiedFiles.push(join(ravenDir, "raven.yaml"));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
success("RavenJS updated successfully!");
|
|
615
|
+
|
|
616
|
+
printSectionHeader("Modified Files");
|
|
617
|
+
for (const file of modifiedFiles) {
|
|
618
|
+
printListItem(file);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (Object.keys(allDependencies).length > 0) {
|
|
622
|
+
printSectionHeader("Required Dependencies");
|
|
623
|
+
for (const [pkg, ver] of Object.entries(allDependencies)) {
|
|
624
|
+
printListItem(`${pkg}@${ver}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// === SECTION: Status ===
|
|
630
|
+
|
|
631
|
+
interface StatusResult {
|
|
632
|
+
core: { installed: boolean };
|
|
633
|
+
modules: string[];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function getStatus(options: CLIOptions): Promise<StatusResult> {
|
|
637
|
+
const targetDir = cwd();
|
|
638
|
+
const root = getRoot(options);
|
|
639
|
+
const ravenDir = join(targetDir, root);
|
|
640
|
+
|
|
641
|
+
let coreInstalled = false;
|
|
642
|
+
const installedModules: string[] = [];
|
|
643
|
+
if (await pathExists(ravenDir)) {
|
|
644
|
+
const coreDir = join(ravenDir, "core");
|
|
645
|
+
coreInstalled =
|
|
646
|
+
(await pathExists(coreDir)) && !(await isDirEmpty(coreDir));
|
|
647
|
+
|
|
648
|
+
const knownModules = getModuleNames();
|
|
649
|
+
const entries = await readdir(ravenDir, { withFileTypes: true });
|
|
650
|
+
for (const entry of entries) {
|
|
651
|
+
if (
|
|
652
|
+
entry.isDirectory() &&
|
|
653
|
+
entry.name !== "core" &&
|
|
654
|
+
knownModules.includes(entry.name)
|
|
655
|
+
) {
|
|
656
|
+
const modDir = join(ravenDir, entry.name);
|
|
657
|
+
if (!(await isDirEmpty(modDir))) {
|
|
658
|
+
installedModules.push(entry.name);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
installedModules.sort();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
core: { installed: coreInstalled },
|
|
667
|
+
modules: installedModules,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
interface StatusCLIOptions extends CLIOptions {
|
|
672
|
+
json?: boolean;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function cmdStatus(options: StatusCLIOptions) {
|
|
676
|
+
const status = await getStatus(options);
|
|
677
|
+
if (options.json) {
|
|
678
|
+
console.log(JSON.stringify(status));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
printSectionHeader("RavenJS Status");
|
|
682
|
+
printListItem(`core: ${status.core.installed ? "installed" : "not installed"}`);
|
|
683
|
+
printListItem(
|
|
684
|
+
`modules: ${status.modules.length > 0 ? status.modules.join(", ") : "none"}`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function cmdSelfUpdate(options: CLIOptions) {
|
|
689
|
+
info("Checking for updates...");
|
|
690
|
+
|
|
691
|
+
const os = detectOs();
|
|
692
|
+
const arch = detectArch();
|
|
693
|
+
|
|
694
|
+
let latestVersion: string;
|
|
695
|
+
try {
|
|
696
|
+
latestVersion = (await getLatestVersion(options.prerelease)).replace(/^v/, "");
|
|
697
|
+
} catch (e: unknown) {
|
|
698
|
+
error(e instanceof Error ? e.message : "failed to get latest version");
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const currentVersion = (registry.version || "").replace(/^v/, "");
|
|
702
|
+
|
|
703
|
+
let shouldUpdate: boolean;
|
|
704
|
+
try {
|
|
705
|
+
shouldUpdate = semverGt(latestVersion, currentVersion);
|
|
706
|
+
} catch {
|
|
707
|
+
shouldUpdate = false;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (!shouldUpdate) {
|
|
711
|
+
success("Already up to date");
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
info(`detected system: ${os}-${arch}`);
|
|
716
|
+
info(`installing version: v${latestVersion}`);
|
|
717
|
+
|
|
718
|
+
let buffer: ArrayBuffer;
|
|
719
|
+
try {
|
|
720
|
+
buffer = await downloadBinary(latestVersion, os, arch);
|
|
721
|
+
} catch (e: unknown) {
|
|
722
|
+
error(
|
|
723
|
+
e instanceof Error
|
|
724
|
+
? e.message
|
|
725
|
+
: `download failed. Please check if version v${latestVersion} exists and supports ${os}-${arch}`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!buffer || buffer.byteLength === 0) {
|
|
730
|
+
error(
|
|
731
|
+
`download failed - file is empty. Please check if version v${latestVersion} exists and supports ${os}-${arch}`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
await ensureDir(INSTALL_DIR);
|
|
736
|
+
const tempPath = `${INSTALL_PATH}.${process.pid}.tmp`;
|
|
737
|
+
try {
|
|
738
|
+
await Bun.write(tempPath, buffer);
|
|
739
|
+
await chmod(tempPath, 0o755);
|
|
740
|
+
await rename(tempPath, INSTALL_PATH);
|
|
741
|
+
} finally {
|
|
742
|
+
try {
|
|
743
|
+
await rm(tempPath, { force: true });
|
|
744
|
+
} catch {
|
|
745
|
+
/* ignore cleanup (file may already be renamed away) */
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
success(`installed successfully to: ${INSTALL_PATH}`);
|
|
750
|
+
|
|
751
|
+
if (!isLocalBinInPath()) {
|
|
752
|
+
log.warn(`${INSTALL_DIR} is not in your PATH`);
|
|
753
|
+
log.warn("add it to your shell config (e.g., ~/.bashrc, ~/.zshrc):");
|
|
754
|
+
log.message(`export PATH="$HOME/.local/bin:$PATH"`, { symbol: pc.dim("~") });
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
log.message("done! run 'raven --help' to get started", { symbol: pc.dim("~") });
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const cli = cac("raven");
|
|
761
|
+
|
|
762
|
+
cli.version(registry.version).help();
|
|
763
|
+
|
|
764
|
+
cli
|
|
765
|
+
.option("--root <dir>", "RavenJS root directory (default: raven)")
|
|
766
|
+
.option("--source <path>", "Local module source path (default: github)")
|
|
767
|
+
.option("--verbose, -v", "Verbose output");
|
|
768
|
+
|
|
769
|
+
cli
|
|
770
|
+
.command("init", "Initialize RavenJS AI resources")
|
|
771
|
+
.action((options) => cmdInit(options as CLIOptions));
|
|
772
|
+
|
|
773
|
+
cli
|
|
774
|
+
.command("install", "Install RavenJS into the current project")
|
|
775
|
+
.action((options) => cmdInstall(options as CLIOptions));
|
|
776
|
+
|
|
777
|
+
cli
|
|
778
|
+
.command("add <module>", "Add a module (e.g., jtd-validator)")
|
|
779
|
+
.action((module, options) => cmdAdd(module, options as CLIOptions));
|
|
780
|
+
|
|
781
|
+
cli
|
|
782
|
+
.command("update", "Update RavenJS to latest version")
|
|
783
|
+
.action((options) => cmdUpdate(options as CLIOptions));
|
|
784
|
+
|
|
785
|
+
cli
|
|
786
|
+
.command("status", "Show RavenJS installation status (core, modules)")
|
|
787
|
+
.option("--json", "Output as JSON for programmatic use")
|
|
788
|
+
.action((options) => cmdStatus(options as StatusCLIOptions));
|
|
789
|
+
|
|
790
|
+
cli
|
|
791
|
+
.command("self-update", "Update RavenJS CLI to latest version")
|
|
792
|
+
.option("--prerelease", "Include prerelease versions when checking for updates")
|
|
793
|
+
.action((options) => cmdSelfUpdate(options as CLIOptions));
|
|
794
|
+
|
|
795
|
+
cli.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raven.js/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI tool for RavenJS framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"raven": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.ts"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.ts",
|
|
15
|
+
"registry.json",
|
|
16
|
+
"scripts/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"prepublishOnly": "bun run scripts/generate-registry.ts 0.0.1",
|
|
20
|
+
"prebuild": "bun run scripts/generate-registry.ts",
|
|
21
|
+
"build": "bun build ./index.ts --compile --minify --outfile raven"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@clack/prompts": "^1.0.1",
|
|
25
|
+
"cac": "^6.7.14",
|
|
26
|
+
"picocolors": "^1.1.1",
|
|
27
|
+
"semver": "^7.7.4",
|
|
28
|
+
"yaml": "^2.6.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"bun": ">=1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"ravenjs",
|
|
35
|
+
"cli",
|
|
36
|
+
"framework",
|
|
37
|
+
"typescript"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/myWsq/RavenJS.git"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT"
|
|
44
|
+
}
|
package/registry.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.0.1",
|
|
3
|
+
"modules": {
|
|
4
|
+
"core": {
|
|
5
|
+
"files": [
|
|
6
|
+
"index.ts",
|
|
7
|
+
"main.ts",
|
|
8
|
+
"README.md"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": {}
|
|
11
|
+
},
|
|
12
|
+
"jtd-validator": {
|
|
13
|
+
"files": [
|
|
14
|
+
"index.ts",
|
|
15
|
+
"main.ts",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"ajv": "^8.18.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"ai": {
|
|
24
|
+
"claude": {
|
|
25
|
+
"add/skill.md": ".claude/skills/raven-add/SKILL.md",
|
|
26
|
+
"install/skill.md": ".claude/skills/raven-install/SKILL.md"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
const ROOT_DIR = join(import.meta.dir, "..", "..", "..");
|
|
5
|
+
const MODULES_DIR = join(ROOT_DIR, "modules");
|
|
6
|
+
const AI_PACKAGE_DIR = join(ROOT_DIR, "packages", "ai");
|
|
7
|
+
const OUTPUT_DIR = join(ROOT_DIR, "packages", "cli");
|
|
8
|
+
const OUTPUT_FILE = join(OUTPUT_DIR, "registry.json");
|
|
9
|
+
|
|
10
|
+
interface ModuleInfo {
|
|
11
|
+
files: string[];
|
|
12
|
+
dependencies: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Registry {
|
|
16
|
+
version: string;
|
|
17
|
+
modules: Record<string, ModuleInfo>;
|
|
18
|
+
ai: { claude: Record<string, string> };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function scanModules(): Promise<Record<string, ModuleInfo>> {
|
|
22
|
+
const modules: Record<string, ModuleInfo> = {};
|
|
23
|
+
|
|
24
|
+
const entries = await readdir(MODULES_DIR, { withFileTypes: true });
|
|
25
|
+
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!entry.isDirectory()) continue;
|
|
28
|
+
|
|
29
|
+
const moduleDir = join(MODULES_DIR, entry.name);
|
|
30
|
+
const packageJsonPath = join(moduleDir, "package.json");
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
34
|
+
const pkg = JSON.parse(content);
|
|
35
|
+
|
|
36
|
+
if (!pkg.files) {
|
|
37
|
+
console.warn(`Warning: ${entry.name} has no files field, skipping`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
modules[entry.name] = {
|
|
42
|
+
files: pkg.files,
|
|
43
|
+
dependencies: pkg.dependencies || {},
|
|
44
|
+
};
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.warn(`Warning: Could not read package.json for ${entry.name}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return modules;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function scanAi(): Promise<{ claude: Record<string, string> }> {
|
|
54
|
+
const packageJsonPath = join(AI_PACKAGE_DIR, "package.json");
|
|
55
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
56
|
+
const pkg = JSON.parse(content);
|
|
57
|
+
|
|
58
|
+
const claude = pkg.claude;
|
|
59
|
+
if (!claude || typeof claude !== "object") {
|
|
60
|
+
throw new Error("packages/ai/package.json must have a 'claude' mapping");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { claude };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function generateRegistry(): Promise<void> {
|
|
67
|
+
const args = process.argv.slice(2);
|
|
68
|
+
const argVersion = args[0];
|
|
69
|
+
const envVersion =
|
|
70
|
+
process.env.RAVEN_VERSION ||
|
|
71
|
+
process.env.RELEASE_VERSION ||
|
|
72
|
+
process.env.CLI_VERSION;
|
|
73
|
+
const version = argVersion || envVersion;
|
|
74
|
+
|
|
75
|
+
if (!version) {
|
|
76
|
+
console.error("Error: Version argument required");
|
|
77
|
+
console.error("Usage: bun run scripts/generate-registry.ts <version>");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const [modules, ai] = await Promise.all([scanModules(), scanAi()]);
|
|
82
|
+
|
|
83
|
+
const registry: Registry = {
|
|
84
|
+
version,
|
|
85
|
+
modules,
|
|
86
|
+
ai,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await writeFile(OUTPUT_FILE, JSON.stringify(registry, null, 2));
|
|
90
|
+
console.log(`Registry generated at ${OUTPUT_FILE}`);
|
|
91
|
+
console.log(`Version: ${version}`);
|
|
92
|
+
console.log(`Modules: ${Object.keys(modules).join(", ")}`);
|
|
93
|
+
console.log(`AI: ${Object.keys(ai.claude).length} files (claude)`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
generateRegistry().catch(console.error);
|