astro-users 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.
Files changed (2) hide show
  1. package/dist/index.js +488 -0
  2. package/package.json +45 -0
package/dist/index.js ADDED
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import pc3 from "picocolors";
5
+
6
+ // src/commands/add.ts
7
+ import { existsSync as existsSync3 } from "fs";
8
+ import { readFile as readFile3 } from "fs/promises";
9
+ import { join as join3 } from "path";
10
+ import * as p from "@clack/prompts";
11
+ import pc from "picocolors";
12
+
13
+ // src/registry.ts
14
+ import { existsSync } from "fs";
15
+ import { readFile } from "fs/promises";
16
+ import { dirname, isAbsolute, join, resolve } from "path";
17
+ import { fileURLToPath } from "url";
18
+ var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/alex-boop-chasey/astro-users/main/registry";
19
+ function resolveRegistry(override) {
20
+ const raw = override ?? process.env.ASTRO_USERS_REGISTRY;
21
+ if (raw) {
22
+ if (/^https?:\/\//.test(raw)) return { kind: "remote", base: raw.replace(/\/$/, "") };
23
+ return { kind: "local", base: resolve(raw) };
24
+ }
25
+ const local = findLocalRegistry();
26
+ if (local) return { kind: "local", base: local };
27
+ return { kind: "remote", base: DEFAULT_REGISTRY_URL };
28
+ }
29
+ function findLocalRegistry() {
30
+ const starts = [dirname(fileURLToPath(import.meta.url)), process.cwd()];
31
+ for (const start of starts) {
32
+ let dir = start;
33
+ for (let i = 0; i < 8; i++) {
34
+ const candidate = join(dir, "registry");
35
+ if (existsSync(join(candidate, "schema.json"))) return candidate;
36
+ const parent = dirname(dir);
37
+ if (parent === dir) break;
38
+ dir = parent;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ async function readSourceText(source, relPath) {
44
+ if (source.kind === "local") {
45
+ return readFile(join(source.base, relPath), "utf8");
46
+ }
47
+ const url = `${source.base}/${relPath.split("/").map(encodeURIComponent).join("/")}`;
48
+ const res = await fetch(url);
49
+ if (!res.ok) throw new Error(`Registry fetch failed (${res.status}) for ${url}`);
50
+ return res.text();
51
+ }
52
+ async function loadManifest(source, name) {
53
+ let text2;
54
+ try {
55
+ text2 = await readSourceText(source, `${name}/registry.json`);
56
+ } catch {
57
+ throw new Error(`Component "${name}" was not found in the registry (${source.base}).`);
58
+ }
59
+ let manifest;
60
+ try {
61
+ manifest = JSON.parse(text2);
62
+ } catch (err) {
63
+ throw new Error(`Component "${name}" has an invalid manifest: ${err.message}`);
64
+ }
65
+ if (!manifest.name || !Array.isArray(manifest.files)) {
66
+ throw new Error(`Component "${name}" has an incomplete manifest.`);
67
+ }
68
+ return manifest;
69
+ }
70
+ function readComponentFile(source, manifest, file) {
71
+ return readSourceText(source, `${manifest.name}/files/${file.path}`);
72
+ }
73
+ async function resolveInstallPlan(source, name) {
74
+ const seen = /* @__PURE__ */ new Map();
75
+ const order = [];
76
+ async function visit(slug, trail) {
77
+ if (seen.has(slug)) return;
78
+ if (trail.includes(slug)) {
79
+ throw new Error(`Circular dependency: ${[...trail, slug].join(" -> ")}`);
80
+ }
81
+ const manifest = await loadManifest(source, slug);
82
+ seen.set(slug, manifest);
83
+ for (const req of manifest.requires ?? []) {
84
+ await visit(req, [...trail, slug]);
85
+ }
86
+ order.push(manifest);
87
+ }
88
+ await visit(name, []);
89
+ return order;
90
+ }
91
+
92
+ // src/utils.ts
93
+ import { existsSync as existsSync2 } from "fs";
94
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
95
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
96
+ async function detectProject(cwd) {
97
+ const root = resolve2(cwd);
98
+ const pkgPath = join2(root, "package.json");
99
+ if (!existsSync2(pkgPath)) {
100
+ throw new Error(
101
+ `No package.json found in ${root}. Run this inside an Astro project, or pass --cwd <path>.`
102
+ );
103
+ }
104
+ let hasAstro = false;
105
+ try {
106
+ const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
107
+ hasAstro = Boolean(pkg.dependencies?.astro ?? pkg.devDependencies?.astro);
108
+ } catch {
109
+ }
110
+ return { root, hasAstro };
111
+ }
112
+ async function writeComponentFile(root, target, content, force) {
113
+ const abs = join2(root, target);
114
+ if (existsSync2(abs)) {
115
+ const existing = await readFile2(abs, "utf8");
116
+ if (existing === content) return { target, outcome: "unchanged" };
117
+ if (!force) {
118
+ const altAbs = `${abs}.astro-users-new`;
119
+ await mkdir(dirname2(altAbs), { recursive: true });
120
+ await writeFile(altAbs, content, "utf8");
121
+ return { target, outcome: "conflict", altPath: `${target}.astro-users-new` };
122
+ }
123
+ await writeFile(abs, content, "utf8");
124
+ return { target, outcome: "overwritten" };
125
+ }
126
+ await mkdir(dirname2(abs), { recursive: true });
127
+ await writeFile(abs, content, "utf8");
128
+ return { target, outcome: "created" };
129
+ }
130
+ async function mergeEnv(root, entries) {
131
+ const envPath = join2(root, ".env");
132
+ let current = "";
133
+ if (existsSync2(envPath)) current = await readFile2(envPath, "utf8");
134
+ const added = [];
135
+ const skipped = [];
136
+ const lines = [];
137
+ for (const entry of entries) {
138
+ const present = new RegExp(`^\\s*${escapeRegExp(entry.name)}\\s*=`, "m").test(current);
139
+ if (present) {
140
+ skipped.push(entry.name);
141
+ continue;
142
+ }
143
+ if (entry.comment) lines.push(`# ${entry.comment}`);
144
+ lines.push(`${entry.name}=${entry.value}`);
145
+ added.push(entry.name);
146
+ }
147
+ if (added.length > 0) {
148
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
149
+ const block = `${prefix}
150
+ # --- Added by Astro Users ---
151
+ ${lines.join("\n")}
152
+ `;
153
+ await writeFile(envPath, current + block, "utf8");
154
+ }
155
+ return { added, skipped };
156
+ }
157
+ async function checkAstroConfig(root) {
158
+ const candidates = ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.mts"];
159
+ for (const name of candidates) {
160
+ const p3 = join2(root, name);
161
+ if (!existsSync2(p3)) continue;
162
+ const src = await readFile2(p3, "utf8");
163
+ return {
164
+ found: true,
165
+ configPath: name,
166
+ isServer: /output\s*:\s*['"](server|hybrid)['"]/.test(src),
167
+ hasAdapter: /adapter\s*:/.test(src)
168
+ };
169
+ }
170
+ return { found: false, isServer: false, hasAdapter: false };
171
+ }
172
+ function escapeRegExp(s) {
173
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
174
+ }
175
+
176
+ // src/commands/add.ts
177
+ async function runAdd(names, opts) {
178
+ p.intro(pc.bgMagenta(pc.black(" Astro Users ")));
179
+ if (names.length === 0) {
180
+ p.log.error("Specify at least one component, e.g. " + pc.cyan("astro-users add auth"));
181
+ p.outro(pc.red("Nothing to install."));
182
+ return 1;
183
+ }
184
+ const project = await detectProject(opts.cwd);
185
+ if (!project.hasAstro) {
186
+ p.log.warn(
187
+ `${pc.dim(project.root)}
188
+ No ${pc.cyan("astro")} dependency detected \u2014 installing anyway, but this may not be an Astro project.`
189
+ );
190
+ }
191
+ const source = resolveRegistry(opts.registry);
192
+ p.log.info(`Registry: ${pc.dim(`${source.kind} \xB7 ${source.base}`)}`);
193
+ const manifests = [];
194
+ const seen = /* @__PURE__ */ new Set();
195
+ const spin = p.spinner();
196
+ spin.start("Resolving components");
197
+ try {
198
+ for (const name of names) {
199
+ const plan = await resolveInstallPlan(source, name);
200
+ for (const m of plan) {
201
+ if (!seen.has(m.name)) {
202
+ seen.add(m.name);
203
+ manifests.push(m);
204
+ }
205
+ }
206
+ }
207
+ } catch (err) {
208
+ spin.stop("Resolution failed", 1);
209
+ p.log.error(err.message);
210
+ p.outro(pc.red("Install aborted."));
211
+ return 1;
212
+ }
213
+ spin.stop(`Resolved ${manifests.length} component${manifests.length === 1 ? "" : "s"}`);
214
+ const pro = manifests.filter((m) => m.tier === "pro");
215
+ if (pro.length > 0) {
216
+ p.log.error(
217
+ `${pro.map((m) => pc.cyan(m.name)).join(", ")} ${pro.length === 1 ? "is a Pro component" : "are Pro components"} and require a license key.
218
+ Pro delivery is not enabled yet.`
219
+ );
220
+ p.outro(pc.red("Install aborted."));
221
+ return 1;
222
+ }
223
+ const npmDeps = {};
224
+ for (const m of manifests) Object.assign(npmDeps, m.npmDependencies ?? {});
225
+ const planLines = [];
226
+ for (const m of manifests) {
227
+ planLines.push(pc.bold(`${m.title} ${pc.dim(`v${m.version} \xB7 ${m.tier}`)}`));
228
+ for (const f of m.files) planLines.push(` ${pc.green("+")} ${f.target}`);
229
+ }
230
+ const depNames = Object.keys(npmDeps);
231
+ if (depNames.length > 0) {
232
+ planLines.push(pc.bold("npm dependencies"));
233
+ for (const [n, v] of Object.entries(npmDeps)) planLines.push(` ${pc.yellow("\u2022")} ${n}@${v}`);
234
+ }
235
+ p.note(planLines.join("\n"), "Install plan");
236
+ if (!opts.yes) {
237
+ const go = await p.confirm({ message: `Install into ${pc.cyan(project.root)}?` });
238
+ if (p.isCancel(go) || !go) {
239
+ p.cancel("Cancelled.");
240
+ return 1;
241
+ }
242
+ }
243
+ const results = [];
244
+ const write = p.spinner();
245
+ write.start("Writing files");
246
+ for (const m of manifests) {
247
+ for (const f of m.files) {
248
+ const content = await readComponentFile(source, m, f);
249
+ results.push(await writeComponentFile(project.root, f.target, content, opts.force));
250
+ }
251
+ }
252
+ write.stop("Files written");
253
+ renderFileResults(results);
254
+ await handleEnv(project.root, manifests, opts, source);
255
+ const needsServer = manifests.some((m) => m.astro?.output === "server" || m.astro?.output === "hybrid");
256
+ const needsAdapter = manifests.some((m) => m.astro?.adapterRequired);
257
+ if (needsServer || needsAdapter) {
258
+ const cfg = await checkAstroConfig(project.root);
259
+ const warns = [];
260
+ if (!cfg.found) warns.push("No astro.config.* found \u2014 add one with an SSR adapter.");
261
+ else {
262
+ if (needsServer && !cfg.isServer)
263
+ warns.push(`Set ${pc.cyan("output: 'server'")} in ${cfg.configPath}.`);
264
+ if (needsAdapter && !cfg.hasAdapter)
265
+ warns.push(`Add an SSR ${pc.cyan("adapter")} (e.g. @astrojs/cloudflare) in ${cfg.configPath}.`);
266
+ }
267
+ if (warns.length > 0) p.log.warn("Astro config:\n " + warns.join("\n "));
268
+ }
269
+ if (depNames.length > 0) {
270
+ const pm = detectPackageManager(project.root);
271
+ p.log.info(
272
+ `Install dependencies:
273
+ ${pc.cyan(`${pm} ${pm === "npm" ? "install" : "add"} ${depNames.join(" ")}`)}`
274
+ );
275
+ }
276
+ const notes = manifests.flatMap((m) => (m.postInstall ?? []).map((line) => `\u2022 ${line}`));
277
+ if (notes.length > 0) p.note(notes.join("\n"), "Next steps");
278
+ p.outro(pc.green(`Installed ${manifests.map((m) => m.name).join(", ")}. Happy building.`));
279
+ return 0;
280
+ }
281
+ function renderFileResults(results) {
282
+ const symbol = {
283
+ created: pc.green("created"),
284
+ overwritten: pc.yellow("overwritten"),
285
+ unchanged: pc.dim("unchanged"),
286
+ conflict: pc.red("conflict")
287
+ };
288
+ const lines = results.map((r) => {
289
+ const base = `${symbol[r.outcome]} ${r.target}`;
290
+ if (r.outcome === "conflict") {
291
+ return `${base}
292
+ ${pc.dim(`\u2192 kept your file; wrote ${r.altPath} for you to compare`)}`;
293
+ }
294
+ return base;
295
+ });
296
+ p.log.message(lines.join("\n"));
297
+ if (results.some((r) => r.outcome === "conflict")) {
298
+ p.log.warn(
299
+ "Some files already existed and were left untouched. Review the .astro-users-new files and merge manually, or re-run with --force to overwrite."
300
+ );
301
+ }
302
+ }
303
+ async function handleEnv(root, manifests, opts, _source) {
304
+ const entries = /* @__PURE__ */ new Map();
305
+ for (const m of manifests) for (const e of m.env ?? []) if (!entries.has(e.name)) entries.set(e.name, e);
306
+ if (entries.size === 0) return;
307
+ const envPath = join3(root, ".env");
308
+ const currentEnv = existsSync3(envPath) ? await readFile3(envPath, "utf8") : "";
309
+ const missing = [...entries.values()].filter(
310
+ (e) => !new RegExp(`^\\s*${e.name}\\s*=`, "m").test(currentEnv)
311
+ );
312
+ if (missing.length === 0) {
313
+ p.log.info(pc.dim("All required environment variables already present in .env."));
314
+ return;
315
+ }
316
+ const values = [];
317
+ if (opts.yes) {
318
+ for (const e of missing) values.push({ name: e.name, value: "", comment: e.prompt });
319
+ } else {
320
+ for (const e of missing) {
321
+ const answer = await p.text({
322
+ message: `${pc.cyan(e.name)}${e.required ? "" : pc.dim(" (optional)")}${e.prompt ? pc.dim(` \u2014 ${e.prompt}`) : ""}`,
323
+ placeholder: e.example ?? (e.secret ? "\u2022\u2022\u2022\u2022 (kept in .env, git-ignored)" : ""),
324
+ defaultValue: ""
325
+ });
326
+ if (p.isCancel(answer)) {
327
+ p.cancel("Cancelled.");
328
+ process.exit(1);
329
+ }
330
+ values.push({ name: e.name, value: answer ?? "", comment: e.prompt });
331
+ }
332
+ }
333
+ const merged = await mergeEnv(root, values);
334
+ if (merged.added.length > 0)
335
+ p.log.success(`Wrote ${merged.added.length} variable(s) to .env: ${merged.added.join(", ")}`);
336
+ if (merged.skipped.length > 0) p.log.info(pc.dim(`Left existing: ${merged.skipped.join(", ")}`));
337
+ }
338
+ function detectPackageManager(root) {
339
+ if (existsSync3(join3(root, "pnpm-lock.yaml"))) return "pnpm";
340
+ if (existsSync3(join3(root, "yarn.lock"))) return "yarn";
341
+ return "npm";
342
+ }
343
+
344
+ // src/commands/list.ts
345
+ import { readdir } from "fs/promises";
346
+ import { existsSync as existsSync4 } from "fs";
347
+ import { join as join4 } from "path";
348
+ import * as p2 from "@clack/prompts";
349
+ import pc2 from "picocolors";
350
+ async function runList(registry) {
351
+ p2.intro(pc2.bgMagenta(pc2.black(" Astro Users ")));
352
+ const source = resolveRegistry(registry);
353
+ p2.log.info(`Registry: ${pc2.dim(`${source.kind} \xB7 ${source.base}`)}`);
354
+ let slugs = [];
355
+ try {
356
+ if (source.kind === "local") {
357
+ const entries = await readdir(source.base, { withFileTypes: true });
358
+ slugs = entries.filter((e) => e.isDirectory() && existsSync4(join4(source.base, e.name, "registry.json"))).map((e) => e.name);
359
+ } else {
360
+ const res = await fetch(`${source.base}/index.json`);
361
+ if (!res.ok) throw new Error(`Registry index unavailable (${res.status}).`);
362
+ const index = await res.json();
363
+ slugs = index.components ?? [];
364
+ }
365
+ } catch (err) {
366
+ p2.log.error(err.message);
367
+ p2.outro(pc2.red("Could not list components."));
368
+ return 1;
369
+ }
370
+ const manifests = [];
371
+ for (const slug of slugs.sort()) {
372
+ try {
373
+ manifests.push(await loadManifest(source, slug));
374
+ } catch {
375
+ }
376
+ }
377
+ if (manifests.length === 0) {
378
+ p2.outro(pc2.yellow("No components found."));
379
+ return 0;
380
+ }
381
+ const lines = manifests.map((m) => {
382
+ const tag = m.tier === "pro" ? pc2.yellow("[pro] ") : pc2.green("[free]");
383
+ return `${tag} ${pc2.bold(m.name.padEnd(12))} ${pc2.dim(m.description.split(".")[0] + ".")}`;
384
+ });
385
+ p2.note(lines.join("\n"), `${manifests.length} component(s)`);
386
+ p2.outro(`Install with ${pc2.cyan("astro-users add <name>")}`);
387
+ return 0;
388
+ }
389
+
390
+ // src/index.ts
391
+ function parseArgs(argv) {
392
+ const out = {
393
+ positionals: [],
394
+ cwd: process.cwd(),
395
+ force: false,
396
+ yes: false,
397
+ help: false,
398
+ version: false
399
+ };
400
+ for (let i = 0; i < argv.length; i++) {
401
+ const arg = argv[i];
402
+ switch (arg) {
403
+ case "--cwd":
404
+ out.cwd = argv[++i] ?? process.cwd();
405
+ break;
406
+ case "--registry":
407
+ out.registry = argv[++i];
408
+ break;
409
+ case "--force":
410
+ case "-f":
411
+ out.force = true;
412
+ break;
413
+ case "--yes":
414
+ case "-y":
415
+ out.yes = true;
416
+ break;
417
+ case "--help":
418
+ case "-h":
419
+ out.help = true;
420
+ break;
421
+ case "--version":
422
+ case "-v":
423
+ out.version = true;
424
+ break;
425
+ default:
426
+ if (arg.startsWith("-")) break;
427
+ if (!out.command) out.command = arg;
428
+ else out.positionals.push(arg);
429
+ }
430
+ }
431
+ return out;
432
+ }
433
+ var HELP = `
434
+ ${pc3.bold("astro-users")} \u2014 install Astro Users components into your Astro project.
435
+
436
+ ${pc3.bold("Usage")}
437
+ astro-users <command> [options]
438
+
439
+ ${pc3.bold("Commands")}
440
+ add <name...> Copy component(s) into your src/ (with .env + config guidance)
441
+ list List available components in the registry
442
+ help Show this help
443
+
444
+ ${pc3.bold("Options")}
445
+ --cwd <path> Target project directory (default: current directory)
446
+ --registry <ref> Registry URL or local path (default: local dev / hosted)
447
+ -y, --yes Non-interactive; skip prompts and confirmations
448
+ -f, --force Overwrite existing files instead of writing *.astro-users-new
449
+ -h, --help Show help
450
+ -v, --version Show version
451
+
452
+ ${pc3.bold("Examples")}
453
+ ${pc3.dim("$")} astro-users list
454
+ ${pc3.dim("$")} astro-users add auth
455
+ ${pc3.dim("$")} astro-users add auth --cwd ./my-site
456
+ `;
457
+ async function main() {
458
+ const args = parseArgs(process.argv.slice(2));
459
+ if (args.version) {
460
+ console.log("astro-users 0.1.0");
461
+ return 0;
462
+ }
463
+ if (args.help || !args.command || args.command === "help") {
464
+ console.log(HELP);
465
+ return 0;
466
+ }
467
+ switch (args.command) {
468
+ case "add":
469
+ return runAdd(args.positionals, {
470
+ cwd: args.cwd,
471
+ registry: args.registry,
472
+ force: args.force,
473
+ yes: args.yes
474
+ });
475
+ case "list":
476
+ case "ls":
477
+ return runList(args.registry);
478
+ default:
479
+ console.error(pc3.red(`Unknown command: ${args.command}`));
480
+ console.log(HELP);
481
+ return 1;
482
+ }
483
+ }
484
+ main().then((code) => process.exit(code ?? 0)).catch((err) => {
485
+ console.error(pc3.red(`
486
+ Unexpected error: ${err.message}`));
487
+ process.exit(1);
488
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "astro-users",
3
+ "version": "0.1.0",
4
+ "description": "Install Astro Users components into your Astro project — copied into your src/, fully yours to edit.",
5
+ "type": "module",
6
+ "bin": {
7
+ "astro-users": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/alex-boop-chasey/astro-users.git"
15
+ },
16
+ "homepage": "https://github.com/alex-boop-chasey/astro-users#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/alex-boop-chasey/astro-users/issues"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsup --watch",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "keywords": [
29
+ "astro",
30
+ "astro-users",
31
+ "auth",
32
+ "cli",
33
+ "scaffold"
34
+ ],
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "@clack/prompts": "^0.7.0",
38
+ "picocolors": "^1.0.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.14.0",
42
+ "tsup": "^8.2.4",
43
+ "typescript": "^5.5.4"
44
+ }
45
+ }