@iamsaroj/replicax 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +394 -0
  3. package/dist/index.js +1562 -0
  4. package/package.json +69 -0
package/dist/index.js ADDED
@@ -0,0 +1,1562 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import { Command } from "commander";
6
+
7
+ // src/utils/errors.ts
8
+ var ReplicaxError = class extends Error {
9
+ /** Optional follow-up lines shown under the main message as hints. */
10
+ hints;
11
+ constructor(message, hints = []) {
12
+ super(message);
13
+ this.name = "ReplicaxError";
14
+ this.hints = hints;
15
+ }
16
+ };
17
+
18
+ // src/utils/logger.ts
19
+ import pc from "picocolors";
20
+ var verbose = false;
21
+ function setVerbose(value) {
22
+ verbose = value;
23
+ }
24
+ function isVerbose() {
25
+ return verbose;
26
+ }
27
+ function write(line) {
28
+ process.stderr.write(line + "\n");
29
+ }
30
+ var logger = {
31
+ /** Plain informational line. */
32
+ info(message) {
33
+ write(`${pc.blue("\u2139")} ${message}`);
34
+ },
35
+ success(message) {
36
+ write(`${pc.green("\u2714")} ${message}`);
37
+ },
38
+ warn(message) {
39
+ write(`${pc.yellow("\u26A0")} ${pc.yellow(message)}`);
40
+ },
41
+ error(message) {
42
+ write(`${pc.red("\u2716")} ${pc.red(message)}`);
43
+ },
44
+ /** A nested detail line, only shown in verbose mode. */
45
+ detail(message) {
46
+ if (verbose) write(` ${pc.dim(message)}`);
47
+ },
48
+ /** Always-shown dim hint, e.g. follow-up suggestions. */
49
+ hint(message) {
50
+ write(` ${pc.dim(message)}`);
51
+ },
52
+ /** A blank separator line. */
53
+ newline() {
54
+ write("");
55
+ },
56
+ /** Write a raw line to stdout (for results meant to be piped/captured). */
57
+ out(line) {
58
+ process.stdout.write(line + "\n");
59
+ }
60
+ };
61
+
62
+ // src/commands/init.ts
63
+ import path6 from "path";
64
+ import fs5 from "fs-extra";
65
+ import ora from "ora";
66
+ import { confirm } from "@inquirer/prompts";
67
+
68
+ // src/constants.ts
69
+ var REPLICAX_DIR = ".replicax";
70
+ var IGNORE_FILE = ".replicaxignore";
71
+ var REPLICAX_VERSION = "2.0.0";
72
+ var PROFILE_FILES = {
73
+ profile: "profile.json",
74
+ tooling: "tooling.json",
75
+ structure: "structure.json",
76
+ metadata: "metadata.json",
77
+ checksum: "checksum.json"
78
+ };
79
+ var SCAN_PRUNE_GLOBS = [
80
+ "**/node_modules/**",
81
+ "**/.git/**",
82
+ "**/dist/**",
83
+ "**/build/**",
84
+ "**/out/**",
85
+ "**/coverage/**",
86
+ "**/.next/**",
87
+ "**/.nuxt/**",
88
+ "**/.svelte-kit/**",
89
+ "**/.turbo/**",
90
+ "**/.cache/**",
91
+ "**/.vercel/**",
92
+ "**/.output/**",
93
+ "**/.parcel-cache/**",
94
+ `**/${REPLICAX_DIR}/**`,
95
+ "**/.husky/_/**",
96
+ "**/.vscode/**",
97
+ "**/.idea/**",
98
+ "**/.vs/**",
99
+ "**/.fleet/**",
100
+ "**/.zed/**"
101
+ ];
102
+ var DEFAULT_IGNORE_PATTERNS = [
103
+ "node_modules/",
104
+ ".git/",
105
+ "dist/",
106
+ "build/",
107
+ "out/",
108
+ "coverage/",
109
+ ".next/",
110
+ ".nuxt/",
111
+ ".svelte-kit/",
112
+ ".turbo/",
113
+ ".cache/",
114
+ ".vercel/",
115
+ ".output/",
116
+ ".parcel-cache/",
117
+ `${REPLICAX_DIR}/`,
118
+ ".husky/_/",
119
+ ".vscode/",
120
+ ".idea/",
121
+ ".vs/",
122
+ ".fleet/",
123
+ ".zed/"
124
+ ];
125
+ var SECRET_GUARD_GLOBS = [
126
+ "**/.env",
127
+ "**/.env.*",
128
+ "**/*.pem",
129
+ "**/*.key",
130
+ "**/*.p12",
131
+ "**/*.pfx",
132
+ "**/*.cert",
133
+ "**/*.crt",
134
+ "**/*.keystore",
135
+ "**/*.jks",
136
+ "**/id_rsa*",
137
+ "**/id_dsa*",
138
+ "**/id_ecdsa*",
139
+ "**/id_ed25519*",
140
+ "**/.netrc",
141
+ "**/secrets.*",
142
+ "**/*.secret",
143
+ "**/*.secrets"
144
+ ];
145
+ var DEFAULT_IGNORE_FILE_CONTENTS = `# .replicaxignore \u2014 control what ReplicaX exports into a profile.
146
+ # Uses .gitignore syntax. Matched files are excluded from the profile,
147
+ # though ReplicaX may still scan them to infer project metadata.
148
+
149
+ # Business logic & application source (structure is kept, contents are not)
150
+ src/features/**
151
+ src/services/**
152
+ src/api/**
153
+ src/**/*.ts
154
+ src/**/*.tsx
155
+ src/**/*.js
156
+ src/**/*.jsx
157
+
158
+ # Secrets (also enforced unconditionally by ReplicaX)
159
+ .env
160
+ .env.*
161
+ *.pem
162
+ *.key
163
+
164
+ # Dependencies & build output
165
+ node_modules/
166
+ dist/
167
+ build/
168
+ coverage/
169
+ .next/
170
+ .nuxt/
171
+
172
+ # Logs
173
+ *.log
174
+ npm-debug.log*
175
+ yarn-debug.log*
176
+ yarn-error.log*
177
+ `;
178
+
179
+ // src/core/scanner.ts
180
+ import path4 from "path";
181
+ import fs3 from "fs-extra";
182
+ import fg from "fast-glob";
183
+
184
+ // src/config/supported-files.ts
185
+ var CONFIG_CATEGORIES = [
186
+ {
187
+ id: "typescript",
188
+ label: "Language & Type Checking",
189
+ patterns: ["tsconfig.json", "tsconfig.*.json", "jsconfig.json"]
190
+ },
191
+ {
192
+ id: "prettier",
193
+ label: "Formatting",
194
+ patterns: [".prettierrc", ".prettierrc.*", "prettier.config.*", ".prettierignore"]
195
+ },
196
+ {
197
+ id: "eslint",
198
+ label: "Linting",
199
+ patterns: ["eslint.config.*", ".eslintrc", ".eslintrc.*", ".eslintignore"]
200
+ },
201
+ {
202
+ id: "build",
203
+ label: "Build Tools",
204
+ patterns: [
205
+ "vite.config.*",
206
+ "webpack.config.*",
207
+ "rollup.config.*",
208
+ "esbuild.config.*",
209
+ "turbo.json"
210
+ ]
211
+ },
212
+ {
213
+ id: "styling",
214
+ label: "Styling",
215
+ patterns: ["tailwind.config.*", "postcss.config.*"]
216
+ },
217
+ {
218
+ id: "package",
219
+ label: "Package Management & Monorepos",
220
+ patterns: [
221
+ // package.json is handled specially (curated template), not here.
222
+ "pnpm-workspace.yaml",
223
+ "nx.json",
224
+ "lerna.json",
225
+ ".npmrc",
226
+ ".nvmrc",
227
+ ".node-version"
228
+ ]
229
+ },
230
+ {
231
+ id: "docker",
232
+ label: "Docker",
233
+ patterns: [
234
+ "Dockerfile",
235
+ "Dockerfile.*",
236
+ "docker-compose.yml",
237
+ "docker-compose.yaml",
238
+ "docker-compose.*.yml",
239
+ "docker-compose.*",
240
+ "compose.yml",
241
+ "compose.yaml",
242
+ ".dockerignore"
243
+ ]
244
+ },
245
+ {
246
+ id: "git",
247
+ label: "Git",
248
+ patterns: [".gitignore", ".gitattributes", ".gitmessage"]
249
+ },
250
+ {
251
+ id: "editor",
252
+ label: "Editor",
253
+ // Only the portable, cross-editor `.editorconfig` is captured. IDE-specific
254
+ // folders (`.vscode/`, `.idea/`, …) are intentionally excluded — see the IDE
255
+ // entries in DEFAULT_IGNORE_PATTERNS / SCAN_PRUNE_GLOBS.
256
+ patterns: [".editorconfig"]
257
+ },
258
+ {
259
+ id: "testing",
260
+ label: "Testing",
261
+ patterns: [
262
+ "vitest.config.*",
263
+ "vitest.workspace.*",
264
+ "jest.config.*",
265
+ "jest.setup.*",
266
+ "playwright.config.*",
267
+ "cypress.config.*"
268
+ ]
269
+ },
270
+ {
271
+ id: "cicd",
272
+ label: "CI/CD",
273
+ patterns: [
274
+ ".github/workflows/*.yml",
275
+ ".github/workflows/*.yaml",
276
+ ".gitlab-ci.yml",
277
+ ".circleci/config.yml",
278
+ "Jenkinsfile",
279
+ "azure-pipelines.yml"
280
+ ]
281
+ },
282
+ {
283
+ id: "husky",
284
+ label: "Git Hooks",
285
+ patterns: [".husky/*"]
286
+ },
287
+ {
288
+ id: "misc",
289
+ label: "Miscellaneous Tooling",
290
+ patterns: [
291
+ "commitlint.config.*",
292
+ "lint-staged.config.*",
293
+ ".lintstagedrc",
294
+ ".lintstagedrc.*",
295
+ "release.config.*",
296
+ ".releaserc",
297
+ ".releaserc.*",
298
+ "knip.config.*",
299
+ "knip.json",
300
+ "renovate.json",
301
+ ".czrc"
302
+ ]
303
+ }
304
+ ];
305
+ var ALL_CONFIG_PATTERNS = CONFIG_CATEGORIES.flatMap((c) => c.patterns);
306
+ var CATEGORY_BY_ID = new Map(CONFIG_CATEGORIES.map((c) => [c.id, c]));
307
+
308
+ // src/utils/paths.ts
309
+ import path from "path";
310
+ function toPosix(p) {
311
+ return p.replace(/\\/g, "/");
312
+ }
313
+ function relPosix(root, target) {
314
+ return toPosix(path.relative(root, target));
315
+ }
316
+ function detectVariant(filePath) {
317
+ const ext = path.extname(filePath).toLowerCase();
318
+ switch (ext) {
319
+ case ".ts":
320
+ case ".cts":
321
+ case ".mts":
322
+ return ext === ".cts" ? "cjs" : ext === ".mts" ? "mjs" : "ts";
323
+ case ".js":
324
+ return "js";
325
+ case ".mjs":
326
+ return "mjs";
327
+ case ".cjs":
328
+ return "cjs";
329
+ case ".json":
330
+ return "json";
331
+ case ".yml":
332
+ case ".yaml":
333
+ return "yaml";
334
+ default:
335
+ return "other";
336
+ }
337
+ }
338
+ function safeJoinable(relPath) {
339
+ const normalized = toPosix(relPath).replace(/^\.\//, "");
340
+ if (normalized.length === 0 || path.isAbsolute(normalized) || normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || normalized.split("/").some((seg) => seg === "..")) {
341
+ return null;
342
+ }
343
+ return normalized;
344
+ }
345
+
346
+ // src/core/ignore-engine.ts
347
+ import path2 from "path";
348
+ import fs from "fs-extra";
349
+ import ignore from "ignore";
350
+ var IgnoreEngine = class _IgnoreEngine {
351
+ ig;
352
+ secrets;
353
+ userPatterns;
354
+ constructor(userPatterns = []) {
355
+ this.userPatterns = userPatterns.filter((line) => {
356
+ const t = line.trim();
357
+ return t.length > 0 && !t.startsWith("#");
358
+ });
359
+ this.ig = ignore().add(DEFAULT_IGNORE_PATTERNS).add(this.userPatterns);
360
+ this.secrets = ignore().add(SECRET_GUARD_GLOBS);
361
+ }
362
+ /** Build an engine from a project's `.replicaxignore`, if present. */
363
+ static async fromProject(root) {
364
+ const file = path2.join(root, IGNORE_FILE);
365
+ if (await fs.pathExists(file)) {
366
+ const content = await fs.readFile(file, "utf8");
367
+ return new _IgnoreEngine(content.split(/\r?\n/));
368
+ }
369
+ return new _IgnoreEngine([]);
370
+ }
371
+ /** Whether a path is excluded by default or user ignore rules. */
372
+ isIgnored(relPosixPath) {
373
+ if (!relPosixPath || relPosixPath === ".") return false;
374
+ return this.ig.ignores(relPosixPath);
375
+ }
376
+ /** Whether a path is a protected secret that must never be captured. */
377
+ isSecret(relPosixPath) {
378
+ if (!relPosixPath || relPosixPath === ".") return false;
379
+ return this.secrets.ignores(relPosixPath);
380
+ }
381
+ };
382
+
383
+ // src/core/detect.ts
384
+ import path3 from "path";
385
+ import fs2 from "fs-extra";
386
+ async function readPackageJson(root) {
387
+ const file = path3.join(root, "package.json");
388
+ if (!await fs2.pathExists(file)) return null;
389
+ try {
390
+ return await fs2.readJson(file);
391
+ } catch {
392
+ return null;
393
+ }
394
+ }
395
+ function allDeps(pkg) {
396
+ return { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
397
+ }
398
+ async function detectPackageManager(root, pkg) {
399
+ const field = pkg?.packageManager;
400
+ if (typeof field === "string") {
401
+ const name = field.split("@")[0]?.trim().toLowerCase();
402
+ if (name === "pnpm" || name === "yarn" || name === "npm" || name === "bun") return name;
403
+ }
404
+ const lockfiles = [
405
+ ["pnpm-lock.yaml", "pnpm"],
406
+ ["bun.lockb", "bun"],
407
+ ["bun.lock", "bun"],
408
+ ["yarn.lock", "yarn"],
409
+ ["package-lock.json", "npm"],
410
+ ["npm-shrinkwrap.json", "npm"]
411
+ ];
412
+ for (const [file, manager] of lockfiles) {
413
+ if (await fs2.pathExists(path3.join(root, file))) return manager;
414
+ }
415
+ return pkg ? "npm" : "unknown";
416
+ }
417
+ async function detectNodeVersion(root, pkg) {
418
+ for (const file of [".nvmrc", ".node-version"]) {
419
+ const full = path3.join(root, file);
420
+ if (await fs2.pathExists(full)) {
421
+ const value = (await fs2.readFile(full, "utf8")).trim();
422
+ if (value) return value;
423
+ }
424
+ }
425
+ const enginesNode = pkg?.engines?.node;
426
+ if (enginesNode) return enginesNode;
427
+ const major = process.versions.node.split(".")[0];
428
+ return `${major}.x`;
429
+ }
430
+ async function detectLanguage(root, pkg) {
431
+ const deps = allDeps(pkg);
432
+ if ("typescript" in deps) return "typescript";
433
+ for (const file of ["tsconfig.json", "tsconfig.base.json", "jsconfig.json"]) {
434
+ if (await fs2.pathExists(path3.join(root, file))) {
435
+ return file === "jsconfig.json" ? "javascript" : "typescript";
436
+ }
437
+ }
438
+ return "javascript";
439
+ }
440
+ function detectFramework(pkg) {
441
+ const deps = allDeps(pkg);
442
+ const has = (name) => name in deps;
443
+ const checks = [
444
+ [has("next"), "next"],
445
+ [has("nuxt") || has("nuxt3"), "nuxt"],
446
+ [has("@remix-run/react"), "remix"],
447
+ [has("astro"), "astro"],
448
+ [has("@angular/core"), "angular"],
449
+ [has("@sveltejs/kit"), "sveltekit"],
450
+ [has("@nestjs/core"), "nestjs"],
451
+ [has("expo"), "expo"],
452
+ [has("react-native"), "react-native"],
453
+ [has("vue"), "vue"],
454
+ [has("svelte"), "svelte"],
455
+ [has("solid-js"), "solid"],
456
+ [has("react"), "react"],
457
+ [has("@fastify/fastify") || has("fastify"), "fastify"],
458
+ [has("koa"), "koa"],
459
+ [has("express"), "express"]
460
+ ];
461
+ for (const [matched, name] of checks) {
462
+ if (matched) return name;
463
+ }
464
+ return pkg ? "node" : "unknown";
465
+ }
466
+ async function detectMetadata(root, pkg) {
467
+ const [packageManager, nodeVersion, language] = await Promise.all([
468
+ detectPackageManager(root, pkg),
469
+ detectNodeVersion(root, pkg),
470
+ detectLanguage(root, pkg)
471
+ ]);
472
+ return {
473
+ nodeVersion,
474
+ packageManager,
475
+ framework: detectFramework(pkg),
476
+ language,
477
+ platform: process.platform
478
+ };
479
+ }
480
+
481
+ // src/core/package-template.ts
482
+ var PASSTHROUGH_CONFIG_KEYS = [
483
+ "lint-staged",
484
+ "nano-staged",
485
+ "prettier",
486
+ "eslintConfig",
487
+ "commitlint",
488
+ "release",
489
+ "husky",
490
+ "browserslist",
491
+ "c8",
492
+ "jest"
493
+ ];
494
+ function nonEmptyRecord(value) {
495
+ if (!value || typeof value !== "object") return void 0;
496
+ const entries = Object.entries(value).filter(
497
+ ([, v]) => typeof v === "string"
498
+ );
499
+ return entries.length ? Object.fromEntries(entries) : void 0;
500
+ }
501
+ function buildPackageTemplate(pkg) {
502
+ if (!pkg) return void 0;
503
+ const template = {};
504
+ if (typeof pkg.type === "string") template.type = pkg.type;
505
+ if (typeof pkg.packageManager === "string") template.packageManager = pkg.packageManager;
506
+ const scripts = nonEmptyRecord(pkg.scripts);
507
+ if (scripts) template.scripts = scripts;
508
+ const devDependencies = nonEmptyRecord(pkg.devDependencies);
509
+ if (devDependencies) template.devDependencies = devDependencies;
510
+ const engines = nonEmptyRecord(pkg.engines);
511
+ if (engines) template.engines = engines;
512
+ const config = {};
513
+ for (const key of PASSTHROUGH_CONFIG_KEYS) {
514
+ if (key in pkg && pkg[key] !== void 0) config[key] = pkg[key];
515
+ }
516
+ if (Object.keys(config).length) template.config = config;
517
+ return template;
518
+ }
519
+ function stableStringify(value) {
520
+ return JSON.stringify(sortDeep(value));
521
+ }
522
+ function sortDeep(value) {
523
+ if (Array.isArray(value)) return value.map(sortDeep);
524
+ if (value && typeof value === "object") {
525
+ const sorted = {};
526
+ for (const key of Object.keys(value).sort()) {
527
+ sorted[key] = sortDeep(value[key]);
528
+ }
529
+ return sorted;
530
+ }
531
+ return value;
532
+ }
533
+ function canonicalPackageJson(template) {
534
+ return stableStringify(template);
535
+ }
536
+ function renderPackageJson(template, projectName) {
537
+ const ordered = {
538
+ name: projectName,
539
+ version: "0.1.0",
540
+ private: true
541
+ };
542
+ if (template.type) ordered.type = template.type;
543
+ if (template.packageManager) ordered.packageManager = template.packageManager;
544
+ if (template.engines) ordered.engines = template.engines;
545
+ if (template.scripts) ordered.scripts = template.scripts;
546
+ for (const [key, value] of Object.entries(template.config ?? {})) {
547
+ ordered[key] = value;
548
+ }
549
+ if (template.devDependencies) ordered.devDependencies = template.devDependencies;
550
+ return JSON.stringify(ordered, null, 2) + "\n";
551
+ }
552
+
553
+ // src/core/scanner.ts
554
+ var FG_BASE_OPTIONS = {
555
+ dot: true,
556
+ followSymbolicLinks: false,
557
+ suppressErrors: true,
558
+ ignore: SCAN_PRUNE_GLOBS
559
+ };
560
+ function sanitizeNpmrc(content) {
561
+ const lines = content.split(/\r?\n/);
562
+ const sensitive = /(_auth(token)?|_password|:_secret|:always-auth=)/i;
563
+ const assignment = /^\s*[^#;=\s]*(token|password|secret|api[-_]?key)\s*=/i;
564
+ const kept = lines.filter((line) => {
565
+ const trimmed = line.trim();
566
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) return true;
567
+ return !sensitive.test(trimmed) && !assignment.test(trimmed);
568
+ });
569
+ return kept.join("\n");
570
+ }
571
+ async function scanToolingFiles(root, ignore2) {
572
+ const categoryOf = /* @__PURE__ */ new Map();
573
+ for (const category of CONFIG_CATEGORIES) {
574
+ const found = await fg(category.patterns, {
575
+ cwd: root,
576
+ onlyFiles: true,
577
+ unique: true,
578
+ ...FG_BASE_OPTIONS
579
+ });
580
+ for (const rel of found) {
581
+ const norm = toPosix(rel);
582
+ if (!categoryOf.has(norm)) categoryOf.set(norm, category.id);
583
+ }
584
+ }
585
+ const files = [];
586
+ const skippedSecrets = [];
587
+ for (const rel of [...categoryOf.keys()].sort()) {
588
+ if (rel === "package.json") continue;
589
+ if (ignore2.isSecret(rel)) {
590
+ skippedSecrets.push(rel);
591
+ logger.detail(`skipped (secret guard): ${rel}`);
592
+ continue;
593
+ }
594
+ if (ignore2.isIgnored(rel)) {
595
+ logger.detail(`skipped (.replicaxignore): ${rel}`);
596
+ continue;
597
+ }
598
+ const abs = path4.join(root, rel);
599
+ let stat;
600
+ try {
601
+ stat = await fs3.stat(abs);
602
+ } catch {
603
+ continue;
604
+ }
605
+ if (!stat.isFile()) continue;
606
+ let content = await fs3.readFile(abs, "utf8");
607
+ if (path4.basename(rel) === ".npmrc") content = sanitizeNpmrc(content);
608
+ files.push({
609
+ path: rel,
610
+ category: categoryOf.get(rel) ?? "misc",
611
+ variant: detectVariant(rel),
612
+ encoding: "utf8",
613
+ content,
614
+ bytes: Buffer.byteLength(content, "utf8")
615
+ });
616
+ logger.detail(`captured: ${rel}`);
617
+ }
618
+ return { files, skippedSecrets };
619
+ }
620
+ async function scanStructure(root, ignore2) {
621
+ const dirs = await fg("**", {
622
+ cwd: root,
623
+ onlyDirectories: true,
624
+ unique: true,
625
+ ...FG_BASE_OPTIONS
626
+ });
627
+ const directories = dirs.map(toPosix).filter((d) => d.length > 0 && d !== ".").filter((d) => !ignore2.isIgnored(d)).sort();
628
+ return {
629
+ root: path4.basename(path4.resolve(root)) || "project",
630
+ directories
631
+ };
632
+ }
633
+ async function scanProject(root) {
634
+ const resolved = path4.resolve(root);
635
+ if (!await fs3.pathExists(resolved)) {
636
+ throw new Error(`Directory does not exist: ${resolved}`);
637
+ }
638
+ const ignore2 = await IgnoreEngine.fromProject(resolved);
639
+ const pkg = await readPackageJson(resolved);
640
+ const [{ files, skippedSecrets }, structure, metadata] = await Promise.all([
641
+ scanToolingFiles(resolved, ignore2),
642
+ scanStructure(resolved, ignore2),
643
+ detectMetadata(resolved, pkg)
644
+ ]);
645
+ const tooling = {
646
+ files,
647
+ packageJson: buildPackageTemplate(pkg)
648
+ };
649
+ return { tooling, structure, metadata, pkg, skippedSecrets };
650
+ }
651
+
652
+ // src/core/checksum.ts
653
+ import { createHash } from "crypto";
654
+ var PACKAGE_JSON_KEY = "package.json";
655
+ function sha256(content) {
656
+ return createHash("sha256").update(content, "utf8").digest("hex");
657
+ }
658
+ function computeChecksum(tooling) {
659
+ const files = {};
660
+ for (const file of tooling.files) {
661
+ files[file.path] = sha256(file.content);
662
+ }
663
+ if (tooling.packageJson) {
664
+ files[PACKAGE_JSON_KEY] = sha256(canonicalPackageJson(tooling.packageJson));
665
+ }
666
+ return { algorithm: "sha256", files };
667
+ }
668
+ function verifyChecksum(tooling, stored) {
669
+ const current = computeChecksum(tooling);
670
+ const mismatches = [];
671
+ for (const [key, hash] of Object.entries(stored.files)) {
672
+ const actual = current.files[key];
673
+ if (actual === void 0) {
674
+ mismatches.push({ path: key, reason: "missing" });
675
+ } else if (actual !== hash) {
676
+ mismatches.push({ path: key, reason: "altered" });
677
+ }
678
+ }
679
+ for (const key of Object.keys(current.files)) {
680
+ if (!(key in stored.files)) {
681
+ mismatches.push({ path: key, reason: "unexpected" });
682
+ }
683
+ }
684
+ return mismatches;
685
+ }
686
+
687
+ // src/core/profile-generator.ts
688
+ function buildBundle(args) {
689
+ const now = (/* @__PURE__ */ new Date()).toISOString();
690
+ const profile = args.existing ? {
691
+ ...args.existing,
692
+ name: args.name,
693
+ description: args.description ?? args.existing.description,
694
+ replicaxVersion: REPLICAX_VERSION,
695
+ updatedAt: now
696
+ } : {
697
+ name: args.name,
698
+ version: "1.0.0",
699
+ createdAt: now,
700
+ replicaxVersion: REPLICAX_VERSION,
701
+ ...args.description ? { description: args.description } : {}
702
+ };
703
+ return {
704
+ profile,
705
+ tooling: args.tooling,
706
+ structure: args.structure,
707
+ metadata: args.metadata,
708
+ checksum: computeChecksum(args.tooling)
709
+ };
710
+ }
711
+
712
+ // src/core/profile-store.ts
713
+ import path5 from "path";
714
+ import fs4 from "fs-extra";
715
+
716
+ // src/schema.ts
717
+ import { z } from "zod";
718
+ var ProfileSchema = z.object({
719
+ name: z.string().min(1),
720
+ version: z.string().min(1),
721
+ createdAt: z.string().min(1),
722
+ updatedAt: z.string().optional(),
723
+ replicaxVersion: z.string().min(1),
724
+ description: z.string().optional()
725
+ });
726
+ var FileVariantSchema = z.enum(["ts", "js", "mjs", "cjs", "json", "yaml", "other"]);
727
+ var ToolingFileSchema = z.object({
728
+ /** POSIX-style path relative to the project root. */
729
+ path: z.string().min(1),
730
+ /** High-level grouping, e.g. "typescript", "eslint", "docker". */
731
+ category: z.string().min(1),
732
+ /** Detected file flavour, used purely for display/inspection. */
733
+ variant: FileVariantSchema,
734
+ /** Text encoding of {@link content}. */
735
+ encoding: z.enum(["utf8", "base64"]),
736
+ /** Verbatim file contents. */
737
+ content: z.string(),
738
+ /** Original size in bytes. */
739
+ bytes: z.number().int().nonnegative()
740
+ });
741
+ var PackageTemplateSchema = z.object({
742
+ type: z.string().optional(),
743
+ scripts: z.record(z.string(), z.string()).optional(),
744
+ devDependencies: z.record(z.string(), z.string()).optional(),
745
+ engines: z.record(z.string(), z.string()).optional(),
746
+ packageManager: z.string().optional(),
747
+ /** Pass-through config blocks that legitimately live in package.json. */
748
+ config: z.record(z.string(), z.unknown()).optional()
749
+ });
750
+ var ToolingSchema = z.object({
751
+ files: z.array(ToolingFileSchema),
752
+ packageJson: PackageTemplateSchema.optional()
753
+ });
754
+ var StructureSchema = z.object({
755
+ root: z.string(),
756
+ directories: z.array(z.string())
757
+ });
758
+ var MetadataSchema = z.object({
759
+ nodeVersion: z.string(),
760
+ packageManager: z.enum(["npm", "yarn", "pnpm", "bun", "unknown"]),
761
+ framework: z.string(),
762
+ language: z.enum(["typescript", "javascript"]),
763
+ platform: z.string()
764
+ });
765
+ var ChecksumSchema = z.object({
766
+ algorithm: z.literal("sha256"),
767
+ files: z.record(z.string(), z.string())
768
+ });
769
+
770
+ // src/core/profile-store.ts
771
+ function profileDir(root) {
772
+ return path5.join(path5.resolve(root), REPLICAX_DIR);
773
+ }
774
+ async function profileExists(dir) {
775
+ return fs4.pathExists(path5.join(dir, PROFILE_FILES.profile));
776
+ }
777
+ async function resolveProfileDir(input) {
778
+ const resolved = path5.resolve(input);
779
+ if (!await fs4.pathExists(resolved)) {
780
+ throw new ReplicaxError(`Profile path not found: ${input}`);
781
+ }
782
+ if (await profileExists(resolved)) return resolved;
783
+ const nested = path5.join(resolved, REPLICAX_DIR);
784
+ if (await profileExists(nested)) return nested;
785
+ throw new ReplicaxError(`No ReplicaX profile found at: ${input}`, [
786
+ `Looked for ${PROFILE_FILES.profile} in ${resolved} and ${nested}.`,
787
+ "Run `replicax init` in the source project first."
788
+ ]);
789
+ }
790
+ async function saveBundle(dir, bundle) {
791
+ await fs4.ensureDir(dir);
792
+ await Promise.all([
793
+ fs4.writeJson(path5.join(dir, PROFILE_FILES.profile), bundle.profile, { spaces: 2 }),
794
+ fs4.writeJson(path5.join(dir, PROFILE_FILES.tooling), bundle.tooling, { spaces: 2 }),
795
+ fs4.writeJson(path5.join(dir, PROFILE_FILES.structure), bundle.structure, { spaces: 2 }),
796
+ fs4.writeJson(path5.join(dir, PROFILE_FILES.metadata), bundle.metadata, { spaces: 2 }),
797
+ fs4.writeJson(path5.join(dir, PROFILE_FILES.checksum), bundle.checksum, { spaces: 2 })
798
+ ]);
799
+ }
800
+ async function readAndParse(dir, file, schema) {
801
+ const full = path5.join(dir, file);
802
+ if (!await fs4.pathExists(full)) {
803
+ throw new ReplicaxError(`Profile is missing ${file}`, [`Expected at ${full}.`]);
804
+ }
805
+ let raw;
806
+ try {
807
+ raw = await fs4.readJson(full);
808
+ } catch {
809
+ throw new ReplicaxError(`Profile file ${file} is not valid JSON`, [`Path: ${full}`]);
810
+ }
811
+ const result = schema.safeParse(raw);
812
+ if (!result.success) {
813
+ const issues = result.error.issues.slice(0, 5).map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`);
814
+ throw new ReplicaxError(`Profile file ${file} failed validation`, issues);
815
+ }
816
+ return result.data;
817
+ }
818
+ async function loadBundle(dir) {
819
+ if (!await profileExists(dir)) {
820
+ throw new ReplicaxError(`No ReplicaX profile found in ${dir}`, [
821
+ "Run `replicax init` to create one."
822
+ ]);
823
+ }
824
+ const [profile, tooling, structure, metadata, checksum] = await Promise.all([
825
+ readAndParse(dir, PROFILE_FILES.profile, ProfileSchema),
826
+ readAndParse(dir, PROFILE_FILES.tooling, ToolingSchema),
827
+ readAndParse(dir, PROFILE_FILES.structure, StructureSchema),
828
+ readAndParse(dir, PROFILE_FILES.metadata, MetadataSchema),
829
+ readAndParse(dir, PROFILE_FILES.checksum, ChecksumSchema)
830
+ ]);
831
+ return { profile, tooling, structure, metadata, checksum };
832
+ }
833
+
834
+ // src/commands/report.ts
835
+ function toolingByCategory(tooling) {
836
+ const counts = /* @__PURE__ */ new Map();
837
+ for (const file of tooling.files) {
838
+ counts.set(file.category, (counts.get(file.category) ?? 0) + 1);
839
+ }
840
+ if (tooling.packageJson) {
841
+ counts.set("package", (counts.get("package") ?? 0) + 1);
842
+ }
843
+ return [...counts.entries()].map(([id, n]) => [CATEGORY_BY_ID.get(id)?.label ?? id, n]).sort((a, b) => a[0].localeCompare(b[0]));
844
+ }
845
+ function printScanSummary(bundle) {
846
+ const { metadata, tooling, structure } = bundle;
847
+ logger.newline();
848
+ logger.info(pc.bold("Captured setup"));
849
+ logger.hint(`language ${metadata.language}`);
850
+ logger.hint(`framework ${metadata.framework}`);
851
+ logger.hint(`packageManager ${metadata.packageManager}`);
852
+ logger.hint(`nodeVersion ${metadata.nodeVersion}`);
853
+ logger.newline();
854
+ logger.info(pc.bold(`Tooling (${tooling.files.length + (tooling.packageJson ? 1 : 0)} files)`));
855
+ for (const [label, count] of toolingByCategory(tooling)) {
856
+ logger.hint(`${label.padEnd(32)} ${count}`);
857
+ }
858
+ logger.newline();
859
+ logger.info(pc.bold(`Structure (${structure.directories.length} directories)`));
860
+ }
861
+ function reportSkippedSecrets(skipped) {
862
+ if (skipped.length === 0) return;
863
+ logger.warn(`Excluded ${skipped.length} protected file(s) from the profile:`);
864
+ for (const file of skipped) logger.hint(file);
865
+ }
866
+
867
+ // src/utils/tree.ts
868
+ function emptyNode(name) {
869
+ return { name, children: /* @__PURE__ */ new Map() };
870
+ }
871
+ function renderTree(directories, rootLabel = ".") {
872
+ const root = emptyNode(rootLabel);
873
+ for (const dir of directories) {
874
+ let cursor = root;
875
+ for (const segment of dir.split("/")) {
876
+ if (!segment) continue;
877
+ let child = cursor.children.get(segment);
878
+ if (!child) {
879
+ child = emptyNode(segment);
880
+ cursor.children.set(segment, child);
881
+ }
882
+ cursor = child;
883
+ }
884
+ }
885
+ const lines = [root.name + "/"];
886
+ const walk = (node, prefix) => {
887
+ const children = [...node.children.values()].sort((a, b) => a.name.localeCompare(b.name));
888
+ children.forEach((child, index) => {
889
+ const last = index === children.length - 1;
890
+ lines.push(`${prefix}${last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${child.name}/`);
891
+ walk(child, `${prefix}${last ? " " : "\u2502 "}`);
892
+ });
893
+ };
894
+ walk(root, "");
895
+ return lines.join("\n");
896
+ }
897
+
898
+ // src/commands/init.ts
899
+ async function initCommand(options) {
900
+ if (options.verbose) setVerbose(true);
901
+ const root = process.cwd();
902
+ const dir = profileDir(root);
903
+ const alreadyExists = await profileExists(dir);
904
+ const spinner = ora({ text: "Scanning project\u2026", isEnabled: !options.verbose }).start();
905
+ const scan = await scanProject(root);
906
+ spinner.succeed(
907
+ `Scanned ${scan.tooling.files.length} config file(s) and ${scan.structure.directories.length} director(ies)`
908
+ );
909
+ const name = options.name ?? path6.basename(path6.resolve(root)) ?? "project";
910
+ const bundle = buildBundle({
911
+ name,
912
+ tooling: scan.tooling,
913
+ structure: scan.structure,
914
+ metadata: scan.metadata
915
+ });
916
+ reportSkippedSecrets(scan.skippedSecrets);
917
+ printScanSummary(bundle);
918
+ logger.out(renderTree(bundle.structure.directories, bundle.structure.root));
919
+ if (options.dryRun) {
920
+ logger.newline();
921
+ logger.info("Dry run \u2014 no files were written.");
922
+ return;
923
+ }
924
+ if (alreadyExists) {
925
+ logger.warn("A ReplicaX profile already exists here and will be replaced.");
926
+ }
927
+ await saveBundle(dir, bundle);
928
+ await maybeWriteIgnoreFile(root);
929
+ logger.newline();
930
+ logger.success(`Profile "${name}" written to ${relPosix(root, dir)}/`);
931
+ logger.hint("Create a project from it with: replicax create <project-name>");
932
+ }
933
+ async function maybeWriteIgnoreFile(root) {
934
+ const file = path6.join(root, IGNORE_FILE);
935
+ if (await fs5.pathExists(file)) return;
936
+ const create = process.stdin.isTTY ? await confirm({
937
+ message: `Create a starter ${IGNORE_FILE} to control what gets exported?`,
938
+ default: true
939
+ }) : false;
940
+ if (create) {
941
+ await fs5.writeFile(file, DEFAULT_IGNORE_FILE_CONTENTS, "utf8");
942
+ logger.success(`Wrote ${IGNORE_FILE}`);
943
+ }
944
+ }
945
+
946
+ // src/commands/create.ts
947
+ import path8 from "path";
948
+ import fs7 from "fs-extra";
949
+
950
+ // src/core/conflict-resolver.ts
951
+ import { select } from "@inquirer/prompts";
952
+ var ConflictResolver = class {
953
+ constructor(policy) {
954
+ this.policy = policy;
955
+ this.interactive = Boolean(process.stdin.isTTY);
956
+ }
957
+ policy;
958
+ blanket = null;
959
+ interactive;
960
+ async resolve(relPath) {
961
+ if (this.policy === "overwrite") return "overwrite";
962
+ if (this.policy === "skip") return "skip";
963
+ if (this.blanket) return this.blanket;
964
+ if (!this.interactive) {
965
+ logger.warn(`${relPath} exists; skipping (non-interactive shell, use --force to overwrite).`);
966
+ return "skip";
967
+ }
968
+ const answer = await select({
969
+ message: `${relPath} already exists. What should ReplicaX do?`,
970
+ choices: [
971
+ { name: "Skip this file", value: "skip" },
972
+ { name: "Overwrite this file", value: "overwrite" },
973
+ { name: "Skip all remaining conflicts", value: "skip-all" },
974
+ { name: "Overwrite all remaining conflicts", value: "overwrite-all" }
975
+ ]
976
+ });
977
+ if (answer === "overwrite-all") {
978
+ this.blanket = "overwrite";
979
+ return "overwrite";
980
+ }
981
+ if (answer === "skip-all") {
982
+ this.blanket = "skip";
983
+ return "skip";
984
+ }
985
+ return answer;
986
+ }
987
+ };
988
+
989
+ // src/core/project-generator.ts
990
+ import path7 from "path";
991
+ import fs6 from "fs-extra";
992
+ async function generateProject(options) {
993
+ const { bundle, targetDir, projectName, dryRun, conflict } = options;
994
+ const result = {
995
+ entries: [],
996
+ dirsCreated: 0,
997
+ filesWritten: 0,
998
+ filesSkipped: 0,
999
+ unsafeSkipped: []
1000
+ };
1001
+ if (!dryRun) await fs6.ensureDir(targetDir);
1002
+ for (const dir of bundle.structure.directories) {
1003
+ const safe = safeJoinable(dir);
1004
+ if (!safe) {
1005
+ result.unsafeSkipped.push(dir);
1006
+ continue;
1007
+ }
1008
+ const full = path7.join(targetDir, safe);
1009
+ const existed = await fs6.pathExists(full);
1010
+ if (!dryRun) await fs6.ensureDir(full);
1011
+ if (!existed) result.dirsCreated += 1;
1012
+ result.entries.push({ kind: "dir", path: safe, action: existed ? "skip" : "create" });
1013
+ }
1014
+ if (bundle.tooling.packageJson) {
1015
+ await writeFile(
1016
+ "package.json",
1017
+ renderPackageJson(bundle.tooling.packageJson, projectName),
1018
+ options,
1019
+ result
1020
+ );
1021
+ }
1022
+ for (const file of bundle.tooling.files) {
1023
+ await writeFile(file.path, file.content, options, result);
1024
+ }
1025
+ return result;
1026
+ }
1027
+ async function writeFile(relPath, content, options, result) {
1028
+ const safe = safeJoinable(relPath);
1029
+ if (!safe) {
1030
+ result.unsafeSkipped.push(relPath);
1031
+ logger.warn(`Refusing to write unsafe path from profile: ${relPath}`);
1032
+ return;
1033
+ }
1034
+ const full = path7.join(options.targetDir, safe);
1035
+ const exists = await fs6.pathExists(full);
1036
+ let action2 = exists ? "overwrite" : "create";
1037
+ if (exists) {
1038
+ const decision = await options.conflict.resolve(safe);
1039
+ if (decision === "skip") {
1040
+ result.filesSkipped += 1;
1041
+ result.entries.push({ kind: "file", path: safe, action: "skip" });
1042
+ logger.detail(`skip: ${safe}`);
1043
+ return;
1044
+ }
1045
+ action2 = "overwrite";
1046
+ }
1047
+ if (!options.dryRun) {
1048
+ await fs6.ensureDir(path7.dirname(full));
1049
+ await fs6.writeFile(full, content, "utf8");
1050
+ }
1051
+ result.filesWritten += 1;
1052
+ result.entries.push({ kind: "file", path: safe, action: action2 });
1053
+ logger.detail(`${action2}: ${safe}`);
1054
+ }
1055
+
1056
+ // src/core/installer.ts
1057
+ import { spawn } from "child_process";
1058
+ var COMMANDS = {
1059
+ npm: ["npm", "install"],
1060
+ pnpm: ["pnpm", "install"],
1061
+ yarn: ["yarn"],
1062
+ bun: ["bun", "install"]
1063
+ };
1064
+ function installDependencies(cwd, manager) {
1065
+ if (manager === "unknown") return Promise.resolve(false);
1066
+ const [command, ...args] = COMMANDS[manager];
1067
+ return new Promise((resolve) => {
1068
+ const child = spawn(command, args, {
1069
+ cwd,
1070
+ stdio: "inherit",
1071
+ // npm/pnpm/yarn are .cmd shims on Windows; a shell resolves them.
1072
+ shell: process.platform === "win32"
1073
+ });
1074
+ child.on("error", () => resolve(false));
1075
+ child.on("close", (code) => resolve(code === 0));
1076
+ });
1077
+ }
1078
+
1079
+ // src/commands/create.ts
1080
+ async function createCommand(projectName, options) {
1081
+ if (options.verbose) setVerbose(true);
1082
+ if (!projectName || projectName.trim().length === 0) {
1083
+ throw new ReplicaxError("A project name is required: replicax create <project-name>");
1084
+ }
1085
+ const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
1086
+ if (!await profileExists(dir)) {
1087
+ throw new ReplicaxError("No ReplicaX profile found.", [
1088
+ "Run `replicax init` in a source project first,",
1089
+ "or point at one with `replicax create <name> --profile <path>`."
1090
+ ]);
1091
+ }
1092
+ const bundle = await loadBundle(dir);
1093
+ const mismatches = verifyChecksum(bundle.tooling, bundle.checksum);
1094
+ if (mismatches.length > 0) {
1095
+ logger.warn(`Profile integrity check found ${mismatches.length} issue(s); continuing anyway.`);
1096
+ logger.hint("Run `replicax validate` for details.");
1097
+ }
1098
+ const targetDir = path8.resolve(process.cwd(), projectName);
1099
+ const leafName = path8.basename(targetDir);
1100
+ if (path8.resolve(process.cwd()) === targetDir) {
1101
+ throw new ReplicaxError("Refusing to scaffold into the current directory.", [
1102
+ "Pass a new project name, e.g. `replicax create my-app`."
1103
+ ]);
1104
+ }
1105
+ const policy = options.force ? "overwrite" : "prompt";
1106
+ const conflict = new ConflictResolver(policy);
1107
+ logger.info(
1108
+ `Creating ${pc.bold(leafName)} from profile ${pc.bold(bundle.profile.name)}${options.dryRun ? pc.dim(" (dry run)") : ""}`
1109
+ );
1110
+ const result = await generateProject({
1111
+ bundle,
1112
+ targetDir,
1113
+ projectName: leafName,
1114
+ dryRun: Boolean(options.dryRun),
1115
+ conflict
1116
+ });
1117
+ if (result.unsafeSkipped.length > 0) {
1118
+ logger.warn(`Skipped ${result.unsafeSkipped.length} unsafe path(s) in the profile.`);
1119
+ }
1120
+ logger.newline();
1121
+ logger.success(
1122
+ `${result.dirsCreated} director(ies) and ${result.filesWritten} file(s) ${options.dryRun ? "would be written" : "written"}` + (result.filesSkipped ? `, ${result.filesSkipped} skipped` : "")
1123
+ );
1124
+ if (options.dryRun) {
1125
+ logger.newline();
1126
+ logger.info("Dry run \u2014 no files were written.");
1127
+ return;
1128
+ }
1129
+ logger.hint(`Location: ${relPosix(process.cwd(), targetDir)}/`);
1130
+ await maybeInstall(
1131
+ bundle.metadata.packageManager,
1132
+ targetDir,
1133
+ options,
1134
+ Boolean(bundle.tooling.packageJson)
1135
+ );
1136
+ logger.newline();
1137
+ logger.success(`Project ${pc.bold(leafName)} is ready.`);
1138
+ }
1139
+ async function maybeInstall(manager, targetDir, options, hasPackageJson) {
1140
+ if (options.skipInstall) {
1141
+ logger.hint("Skipped dependency install (--skip-install).");
1142
+ return;
1143
+ }
1144
+ if (!hasPackageJson) return;
1145
+ if (manager === "unknown") {
1146
+ logger.hint("No package manager detected; run your install command manually.");
1147
+ return;
1148
+ }
1149
+ const pkgPath = path8.join(targetDir, "package.json");
1150
+ const pkg = await fs7.readJson(pkgPath).catch(() => null);
1151
+ if (!pkg?.devDependencies || Object.keys(pkg.devDependencies).length === 0) {
1152
+ logger.hint("No dependencies to install.");
1153
+ return;
1154
+ }
1155
+ logger.newline();
1156
+ logger.info(`Installing dependencies with ${manager}\u2026`);
1157
+ const ok = await installDependencies(targetDir, manager);
1158
+ if (ok) logger.success("Dependencies installed.");
1159
+ else logger.warn("Dependency install did not complete; run it manually.");
1160
+ }
1161
+
1162
+ // src/commands/sync.ts
1163
+ import ora2 from "ora";
1164
+
1165
+ // src/core/diff.ts
1166
+ function diffChecksums(prev, next) {
1167
+ const added = [];
1168
+ const removed = [];
1169
+ const changed = [];
1170
+ let packageJsonChanged = false;
1171
+ const keys = /* @__PURE__ */ new Set([...Object.keys(prev.files), ...Object.keys(next.files)]);
1172
+ for (const key of keys) {
1173
+ const before = prev.files[key];
1174
+ const after = next.files[key];
1175
+ if (key === PACKAGE_JSON_KEY) {
1176
+ if (before !== after) packageJsonChanged = true;
1177
+ continue;
1178
+ }
1179
+ if (before === void 0) added.push(key);
1180
+ else if (after === void 0) removed.push(key);
1181
+ else if (before !== after) changed.push(key);
1182
+ }
1183
+ return {
1184
+ files: { added: added.sort(), removed: removed.sort(), changed: changed.sort() },
1185
+ packageJsonChanged
1186
+ };
1187
+ }
1188
+ function diffStructure(prev, next) {
1189
+ const before = new Set(prev.directories);
1190
+ const after = new Set(next.directories);
1191
+ const added = next.directories.filter((d) => !before.has(d));
1192
+ const removed = prev.directories.filter((d) => !after.has(d));
1193
+ return { added, removed };
1194
+ }
1195
+ function diffMetadata(prev, next) {
1196
+ const fields = [
1197
+ "nodeVersion",
1198
+ "packageManager",
1199
+ "framework",
1200
+ "language",
1201
+ "platform"
1202
+ ];
1203
+ const changes = [];
1204
+ for (const field of fields) {
1205
+ if (prev[field] !== next[field]) {
1206
+ changes.push({ field, from: String(prev[field]), to: String(next[field]) });
1207
+ }
1208
+ }
1209
+ return changes;
1210
+ }
1211
+ function diffBundles(prev, next) {
1212
+ const checksums = diffChecksums(prev.checksum, next.checksum);
1213
+ return {
1214
+ files: checksums.files,
1215
+ packageJsonChanged: checksums.packageJsonChanged,
1216
+ directories: diffStructure(prev.structure, next.structure),
1217
+ metadataChanges: diffMetadata(prev.metadata, next.metadata)
1218
+ };
1219
+ }
1220
+ function hasChanges(diff) {
1221
+ return diff.files.added.length > 0 || diff.files.removed.length > 0 || diff.files.changed.length > 0 || diff.directories.added.length > 0 || diff.directories.removed.length > 0 || diff.packageJsonChanged || diff.metadataChanges.length > 0;
1222
+ }
1223
+
1224
+ // src/commands/sync.ts
1225
+ async function syncCommand(options) {
1226
+ if (options.verbose) setVerbose(true);
1227
+ const root = process.cwd();
1228
+ const dir = profileDir(root);
1229
+ if (!await profileExists(dir)) {
1230
+ throw new ReplicaxError("No ReplicaX profile to sync.", ["Run `replicax init` first."]);
1231
+ }
1232
+ const existing = await loadBundle(dir);
1233
+ const spinner = ora2({ text: "Re-scanning project\u2026", isEnabled: !options.verbose }).start();
1234
+ const scan = await scanProject(root);
1235
+ spinner.succeed("Re-scan complete");
1236
+ const next = buildBundle({
1237
+ name: existing.profile.name,
1238
+ description: existing.profile.description,
1239
+ tooling: scan.tooling,
1240
+ structure: scan.structure,
1241
+ metadata: scan.metadata,
1242
+ existing: existing.profile
1243
+ });
1244
+ const diff = diffBundles(existing, next);
1245
+ if (!hasChanges(diff) && !options.force) {
1246
+ logger.success("Profile is already up to date.");
1247
+ return;
1248
+ }
1249
+ reportSkippedSecrets(scan.skippedSecrets);
1250
+ printDiff(diff, Boolean(options.diff));
1251
+ await saveBundle(dir, next);
1252
+ logger.newline();
1253
+ logger.success("Profile updated.");
1254
+ }
1255
+ function printDiff(diff, detailed) {
1256
+ const { files, directories, metadataChanges, packageJsonChanged } = diff;
1257
+ logger.newline();
1258
+ logger.info(pc.bold("Changes since last sync"));
1259
+ logger.hint(
1260
+ `files ${pc.green(`+${files.added.length}`)} ${pc.yellow(`~${files.changed.length}`)} ${pc.red(`-${files.removed.length}`)}`
1261
+ );
1262
+ logger.hint(
1263
+ `dirs ${pc.green(`+${directories.added.length}`)} ${pc.red(`-${directories.removed.length}`)}`
1264
+ );
1265
+ if (packageJsonChanged) logger.hint("package.json template changed");
1266
+ if (metadataChanges.length) {
1267
+ for (const change of metadataChanges) {
1268
+ logger.hint(`metadata ${change.field}: ${change.from} \u2192 ${change.to}`);
1269
+ }
1270
+ }
1271
+ if (!detailed) return;
1272
+ logger.newline();
1273
+ printList("added files", files.added, pc.green("+"));
1274
+ printList("changed files", files.changed, pc.yellow("~"));
1275
+ printList("removed files", files.removed, pc.red("-"));
1276
+ printList("added directories", directories.added, pc.green("+"));
1277
+ printList("removed directories", directories.removed, pc.red("-"));
1278
+ }
1279
+ function printList(title, items, marker) {
1280
+ if (items.length === 0) return;
1281
+ logger.info(pc.bold(title));
1282
+ for (const item of items) logger.hint(`${marker} ${item}`);
1283
+ }
1284
+
1285
+ // src/commands/inspect.ts
1286
+ import Table from "cli-table3";
1287
+ var SECTIONS = ["profile", "tooling", "structure", "metadata"];
1288
+ async function inspectCommand(options) {
1289
+ const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
1290
+ if (!await profileExists(dir)) {
1291
+ throw new ReplicaxError("No ReplicaX profile found.", ["Run `replicax init` first."]);
1292
+ }
1293
+ if (options.section && !SECTIONS.includes(options.section)) {
1294
+ throw new ReplicaxError(`Unknown section "${options.section}".`, [
1295
+ `Valid sections: ${SECTIONS.join(", ")}.`
1296
+ ]);
1297
+ }
1298
+ const bundle = await loadBundle(dir);
1299
+ const section = options.section;
1300
+ if (options.json) {
1301
+ const payload = section ? { [section]: bundle[section] } : bundle;
1302
+ logger.out(JSON.stringify(payload, null, 2));
1303
+ return;
1304
+ }
1305
+ if (!section || section === "profile") printProfile(bundle);
1306
+ if (!section || section === "metadata") printMetadata(bundle);
1307
+ if (!section || section === "tooling") printTooling(bundle);
1308
+ if (!section || section === "structure") printStructure(bundle);
1309
+ }
1310
+ function printProfile(bundle) {
1311
+ const p = bundle.profile;
1312
+ logger.out(pc.bold("Profile"));
1313
+ logger.out(` name ${p.name}`);
1314
+ logger.out(` version ${p.version}`);
1315
+ if (p.description) logger.out(` description ${p.description}`);
1316
+ logger.out(` createdAt ${p.createdAt}`);
1317
+ if (p.updatedAt) logger.out(` updatedAt ${p.updatedAt}`);
1318
+ logger.out(` replicaxVersion ${p.replicaxVersion}`);
1319
+ logger.out("");
1320
+ }
1321
+ function printMetadata(bundle) {
1322
+ const m = bundle.metadata;
1323
+ logger.out(pc.bold("Metadata"));
1324
+ logger.out(` language ${m.language}`);
1325
+ logger.out(` framework ${m.framework}`);
1326
+ logger.out(` packageManager ${m.packageManager}`);
1327
+ logger.out(` nodeVersion ${m.nodeVersion}`);
1328
+ logger.out(` platform ${m.platform}`);
1329
+ logger.out("");
1330
+ }
1331
+ function printTooling(bundle) {
1332
+ const { tooling } = bundle;
1333
+ const total = tooling.files.length + (tooling.packageJson ? 1 : 0);
1334
+ logger.out(pc.bold(`Tooling (${total} file(s))`));
1335
+ const table = new Table({
1336
+ head: ["Category", "File", "Variant", "Size"],
1337
+ style: { head: ["cyan"], border: ["dim"] }
1338
+ });
1339
+ if (tooling.packageJson) {
1340
+ table.push(["Package Management & Monorepos", "package.json", "json", "template"]);
1341
+ }
1342
+ for (const file of [...tooling.files].sort((a, b) => a.path.localeCompare(b.path))) {
1343
+ table.push([
1344
+ CATEGORY_BY_ID.get(file.category)?.label ?? file.category,
1345
+ file.path,
1346
+ file.variant,
1347
+ formatBytes(file.bytes)
1348
+ ]);
1349
+ }
1350
+ logger.out(table.toString());
1351
+ logger.out("");
1352
+ }
1353
+ function printStructure(bundle) {
1354
+ const { structure } = bundle;
1355
+ logger.out(pc.bold(`Structure (${structure.directories.length} director(ies))`));
1356
+ logger.out(renderTree(structure.directories, structure.root));
1357
+ logger.out("");
1358
+ }
1359
+ function formatBytes(bytes) {
1360
+ if (bytes < 1024) return `${bytes} B`;
1361
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1362
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1363
+ }
1364
+
1365
+ // src/commands/validate.ts
1366
+ async function validateCommand(options) {
1367
+ const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
1368
+ if (!await profileExists(dir)) {
1369
+ throw new ReplicaxError("No ReplicaX profile found.", ["Run `replicax init` first."]);
1370
+ }
1371
+ const bundle = await loadBundle(dir);
1372
+ logger.success("Schema validation passed (profile, tooling, structure, metadata, checksum).");
1373
+ const issues = [];
1374
+ const secretGuard = new IgnoreEngine();
1375
+ const mismatches = verifyChecksum(bundle.tooling, bundle.checksum);
1376
+ for (const m of mismatches) {
1377
+ issues.push(`checksum ${m.reason}: ${m.path}`);
1378
+ }
1379
+ for (const file of bundle.tooling.files) {
1380
+ if (safeJoinable(file.path) === null) issues.push(`unsafe file path: ${file.path}`);
1381
+ if (secretGuard.isSecret(file.path)) issues.push(`secret leaked into profile: ${file.path}`);
1382
+ }
1383
+ for (const dirPath of bundle.structure.directories) {
1384
+ if (safeJoinable(dirPath) === null) issues.push(`unsafe directory path: ${dirPath}`);
1385
+ }
1386
+ if (issues.length === 0) {
1387
+ logger.success("Integrity checks passed \u2014 checksums match and no unsafe paths.");
1388
+ logger.newline();
1389
+ logger.success(pc.bold(`Profile "${bundle.profile.name}" is valid.`));
1390
+ return;
1391
+ }
1392
+ logger.newline();
1393
+ logger.error(`Found ${issues.length} issue(s):`);
1394
+ for (const issue of issues) logger.hint(issue);
1395
+ throw new ReplicaxError("Profile validation failed.", [
1396
+ "Re-run `replicax sync` to regenerate from the current project."
1397
+ ]);
1398
+ }
1399
+
1400
+ // src/commands/export.ts
1401
+ import path10 from "path";
1402
+ import fs9 from "fs-extra";
1403
+ import ora3 from "ora";
1404
+
1405
+ // src/core/archive.ts
1406
+ import os from "os";
1407
+ import path9 from "path";
1408
+ import fs8 from "fs-extra";
1409
+ import { create as tarCreate, extract as tarExtract } from "tar";
1410
+ async function exportProfile(profileDirectory, outPath) {
1411
+ const resolvedOut = path9.resolve(outPath);
1412
+ await fs8.ensureDir(path9.dirname(resolvedOut));
1413
+ const parent = path9.dirname(profileDirectory);
1414
+ const base = path9.basename(profileDirectory);
1415
+ await tarCreate(
1416
+ {
1417
+ gzip: true,
1418
+ file: resolvedOut,
1419
+ cwd: parent,
1420
+ // tar strips leading "/" and ".." by default, so extraction stays scoped.
1421
+ portable: true
1422
+ },
1423
+ [base]
1424
+ );
1425
+ }
1426
+ async function extractToTemp(archivePath) {
1427
+ const resolved = path9.resolve(archivePath);
1428
+ if (!await fs8.pathExists(resolved)) {
1429
+ throw new Error(`Archive not found: ${archivePath}`);
1430
+ }
1431
+ const tmp = await fs8.mkdtemp(path9.join(os.tmpdir(), "replicax-import-"));
1432
+ await tarExtract({ file: resolved, cwd: tmp, strip: 0 });
1433
+ return tmp;
1434
+ }
1435
+ async function findProfileRoot(dir) {
1436
+ const hasProfile = async (d) => fs8.pathExists(path9.join(d, PROFILE_FILES.profile));
1437
+ if (await hasProfile(dir)) return dir;
1438
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1439
+ for (const entry of entries) {
1440
+ if (entry.isDirectory()) {
1441
+ const candidate = path9.join(dir, entry.name);
1442
+ if (await hasProfile(candidate)) return candidate;
1443
+ }
1444
+ }
1445
+ return null;
1446
+ }
1447
+
1448
+ // src/commands/export.ts
1449
+ function slug(name) {
1450
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "profile";
1451
+ }
1452
+ async function exportCommand(options) {
1453
+ const dir = options.profile ? await resolveProfileDir(options.profile) : profileDir(process.cwd());
1454
+ if (!await profileExists(dir)) {
1455
+ throw new ReplicaxError("No ReplicaX profile found to export.", ["Run `replicax init` first."]);
1456
+ }
1457
+ const bundle = await loadBundle(dir);
1458
+ const outPath = path10.resolve(options.out ?? `${slug(bundle.profile.name)}.replicax.tar.gz`);
1459
+ const spinner = ora3({ text: "Packaging profile\u2026" }).start();
1460
+ await exportProfile(dir, outPath);
1461
+ spinner.stop();
1462
+ const { size } = await fs9.stat(outPath);
1463
+ logger.success(
1464
+ `Exported "${bundle.profile.name}" \u2192 ${path10.relative(process.cwd(), outPath)} (${formatBytes2(size)})`
1465
+ );
1466
+ logger.hint("Share it, then `replicax import <file>` elsewhere.");
1467
+ }
1468
+ function formatBytes2(bytes) {
1469
+ if (bytes < 1024) return `${bytes} B`;
1470
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1471
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1472
+ }
1473
+
1474
+ // src/commands/import.ts
1475
+ import fs10 from "fs-extra";
1476
+ import ora4 from "ora";
1477
+ import { confirm as confirm2 } from "@inquirer/prompts";
1478
+ async function importCommand(archivePath, options) {
1479
+ if (!archivePath) {
1480
+ throw new ReplicaxError("An archive path is required: replicax import <file>");
1481
+ }
1482
+ const spinner = ora4({ text: "Extracting archive\u2026" }).start();
1483
+ const tmp = await extractToTemp(archivePath);
1484
+ try {
1485
+ const source = await findProfileRoot(tmp);
1486
+ if (!source) {
1487
+ spinner.fail("No profile found in archive");
1488
+ throw new ReplicaxError("The archive does not contain a ReplicaX profile.");
1489
+ }
1490
+ const bundle = await loadBundle(source);
1491
+ spinner.succeed(`Validated profile "${bundle.profile.name}"`);
1492
+ const dest = profileDir(process.cwd());
1493
+ if (await profileExists(dest)) {
1494
+ const overwrite = options.force || (process.stdin.isTTY ? await confirm2({
1495
+ message: "A profile already exists here. Overwrite it?",
1496
+ default: false
1497
+ }) : false);
1498
+ if (!overwrite) {
1499
+ throw new ReplicaxError("A profile already exists.", [
1500
+ "Re-run with --force to overwrite it."
1501
+ ]);
1502
+ }
1503
+ await fs10.remove(dest);
1504
+ }
1505
+ await saveBundle(dest, bundle);
1506
+ logger.newline();
1507
+ logger.success(
1508
+ `Imported "${pc.bold(bundle.profile.name)}" into ${relPosix(process.cwd(), dest)}/`
1509
+ );
1510
+ logger.hint("Create a project with: replicax create <project-name>");
1511
+ } finally {
1512
+ await fs10.remove(tmp).catch(() => void 0);
1513
+ }
1514
+ }
1515
+
1516
+ // src/index.ts
1517
+ function packageVersion() {
1518
+ try {
1519
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
1520
+ return JSON.parse(raw).version ?? "0.0.0";
1521
+ } catch {
1522
+ return "0.0.0";
1523
+ }
1524
+ }
1525
+ function action(fn) {
1526
+ return async (...args) => {
1527
+ try {
1528
+ await fn(...args);
1529
+ } catch (err) {
1530
+ handleError(err);
1531
+ }
1532
+ };
1533
+ }
1534
+ function handleError(err) {
1535
+ if (err instanceof ReplicaxError) {
1536
+ logger.error(err.message);
1537
+ for (const hint of err.hints) logger.hint(hint);
1538
+ } else if (err instanceof Error && err.name === "ExitPromptError") {
1539
+ logger.newline();
1540
+ logger.warn("Cancelled.");
1541
+ } else if (err instanceof Error) {
1542
+ logger.error(err.message);
1543
+ if (isVerbose() && err.stack) logger.hint(err.stack);
1544
+ } else {
1545
+ logger.error(String(err));
1546
+ }
1547
+ process.exitCode = 1;
1548
+ }
1549
+ var program = new Command();
1550
+ program.name("replicax").description("Copy the setup, not the code.").version(packageVersion(), "-v, --version", "Print the ReplicaX version").showHelpAfterError("(run `replicax --help` for usage)");
1551
+ program.command("init").description("Scan the current project and create a ReplicaX profile in .replicax/").option("--name <name>", "Name the profile").option("--dry-run", "Preview what would be captured without writing").option("--verbose", "Show every detected file").action(action(initCommand));
1552
+ program.command("create").argument("<project-name>", "Directory/name for the new project").description("Create a new project from a profile").option("--profile <path>", "Use a profile from a custom path").option("--skip-install", "Do not run the package manager install step").option("--dry-run", "Preview the output without writing").option("--force", "Overwrite conflicting files without prompting").option("--verbose", "Show every written file").action(action(createCommand));
1553
+ program.command("sync").description("Update the profile from the current project state").option("--diff", "Show a detailed list of what changed").option("--force", "Rewrite the profile even if nothing changed").option("--verbose", "Show every detected file").action(action(syncCommand));
1554
+ program.command("inspect").description("Display captured configuration and structure").option("--json", "Output as JSON").option("--section <section>", "Inspect one section: profile|tooling|structure|metadata").option("--profile <path>", "Inspect a profile at a custom path").action(action(inspectCommand));
1555
+ program.command("validate").description("Check profile schema and integrity").option("--profile <path>", "Validate a profile at a custom path").action(action(validateCommand));
1556
+ program.command("export").description("Export the profile as a portable .tar.gz archive").option("--out <file>", "Output archive path").option("--profile <path>", "Export a profile from a custom path").action(action(exportCommand));
1557
+ program.command("import").argument("<archive>", "Path to a .tar.gz profile archive").description("Import a portable profile archive into .replicax/").option("--force", "Overwrite an existing profile").action(action(importCommand));
1558
+ if (process.argv.slice(2).length === 0) {
1559
+ program.outputHelp();
1560
+ process.exit(0);
1561
+ }
1562
+ program.parseAsync(process.argv).catch(handleError);