@mars-stack/cli 5.0.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8543,6 +8543,217 @@ function upgradeCommand(program2) {
8543
8543
  });
8544
8544
  }
8545
8545
 
8546
+ // src/commands/template.ts
8547
+ init_logger();
8548
+ import fs30 from "fs-extra";
8549
+ import path30 from "path";
8550
+ import yaml from "js-yaml";
8551
+ var FEATURE_GATE_DIRS = {
8552
+ admin: path30.join("src", "features", "admin"),
8553
+ billing: path30.join("src", "features", "billing"),
8554
+ uploads: path30.join("src", "features", "uploads")
8555
+ };
8556
+ var STATUS_ICONS = {
8557
+ unchanged: "\u2713",
8558
+ updated: "\u2191",
8559
+ new: "+",
8560
+ skipped: "\u2194"
8561
+ };
8562
+ function listFilesRecursively(dir) {
8563
+ const files = [];
8564
+ if (!fs30.existsSync(dir)) return files;
8565
+ const entries = fs30.readdirSync(dir, { withFileTypes: true });
8566
+ for (const entry of entries) {
8567
+ const fullPath = path30.join(dir, entry.name);
8568
+ if (entry.isDirectory()) {
8569
+ files.push(...listFilesRecursively(fullPath));
8570
+ } else if (entry.isFile()) {
8571
+ files.push(fullPath);
8572
+ }
8573
+ }
8574
+ return files;
8575
+ }
8576
+ function expandManifestEntry(templateDir, entry) {
8577
+ const isGlob = entry.path.endsWith("/**");
8578
+ if (isGlob) {
8579
+ const baseDir = entry.path.slice(0, -3);
8580
+ const templateBase = path30.join(templateDir, ...baseDir.split("/"));
8581
+ const files = listFilesRecursively(templateBase);
8582
+ return files.map(
8583
+ (f) => path30.relative(templateDir, f).split(path30.sep).join("/")
8584
+ );
8585
+ }
8586
+ return [entry.path];
8587
+ }
8588
+ function readManifest(manifestPath) {
8589
+ const raw = fs30.readFileSync(manifestPath, "utf8");
8590
+ const doc = yaml.load(raw);
8591
+ if (!Array.isArray(doc)) {
8592
+ throw new Error("Invalid sync manifest format: expected a YAML array.");
8593
+ }
8594
+ return doc;
8595
+ }
8596
+ async function runTemplateSync(options = {}) {
8597
+ log.title("MARS Template Sync");
8598
+ const projectDir = process.cwd();
8599
+ const scaffoldPath = path30.join(projectDir, ".mars", "scaffold.json");
8600
+ if (!await fs30.pathExists(scaffoldPath)) {
8601
+ log.error(
8602
+ "Not a Mars scaffold project. Run `mars create` to scaffold a new project."
8603
+ );
8604
+ process.exitCode = 1;
8605
+ return [];
8606
+ }
8607
+ let fingerprint = null;
8608
+ try {
8609
+ fingerprint = await fs30.readJson(scaffoldPath);
8610
+ } catch {
8611
+ log.warn(
8612
+ "Could not read .mars/scaffold.json \u2014 continuing without checksum comparison."
8613
+ );
8614
+ }
8615
+ const templateDir = getTemplatePath();
8616
+ const manifestPath = path30.join(templateDir, ".mars", "sync-manifest.yaml");
8617
+ if (!await fs30.pathExists(manifestPath)) {
8618
+ log.error(
8619
+ "Sync manifest not found in template. Update @mars-stack/cli to the latest version."
8620
+ );
8621
+ process.exitCode = 1;
8622
+ return [];
8623
+ }
8624
+ let entries;
8625
+ try {
8626
+ entries = readManifest(manifestPath);
8627
+ } catch (error) {
8628
+ log.error(
8629
+ error instanceof Error ? error.message : "Failed to parse sync manifest."
8630
+ );
8631
+ process.exitCode = 1;
8632
+ return [];
8633
+ }
8634
+ const syncCategory = options.only ?? "plumbing";
8635
+ const results = [];
8636
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
8637
+ const backupDir = path30.join(projectDir, ".mars", "backups", timestamp);
8638
+ for (const entry of entries) {
8639
+ if (entry.category === "excluded") continue;
8640
+ if (entry.feature_gate) {
8641
+ const gateDir = FEATURE_GATE_DIRS[entry.feature_gate];
8642
+ if (gateDir && !await fs30.pathExists(path30.join(projectDir, gateDir))) {
8643
+ continue;
8644
+ }
8645
+ }
8646
+ const shouldSync = entry.category === syncCategory || options.force && entry.category === "user-owned" && syncCategory === "plumbing";
8647
+ const filePaths = expandManifestEntry(templateDir, entry);
8648
+ for (const relativePath of filePaths) {
8649
+ const templateFile = path30.join(templateDir, ...relativePath.split("/"));
8650
+ const projectFile = path30.join(projectDir, ...relativePath.split("/"));
8651
+ if (!shouldSync) {
8652
+ results.push({
8653
+ relativePath,
8654
+ status: "skipped",
8655
+ category: entry.category,
8656
+ reason: entry.category
8657
+ });
8658
+ continue;
8659
+ }
8660
+ if (!await fs30.pathExists(templateFile)) continue;
8661
+ const templateStat = await fs30.stat(templateFile);
8662
+ if (!templateStat.isFile()) continue;
8663
+ const projectExists = await fs30.pathExists(projectFile);
8664
+ if (!projectExists) {
8665
+ if (!options.dryRun) {
8666
+ await fs30.ensureDir(path30.dirname(projectFile));
8667
+ await fs30.copy(templateFile, projectFile);
8668
+ }
8669
+ results.push({
8670
+ relativePath,
8671
+ status: "new",
8672
+ category: entry.category
8673
+ });
8674
+ continue;
8675
+ }
8676
+ const templateChecksum = await checksumFile(templateFile);
8677
+ const projectChecksum = await checksumFile(projectFile);
8678
+ if (templateChecksum === projectChecksum) {
8679
+ results.push({
8680
+ relativePath,
8681
+ status: "unchanged",
8682
+ category: entry.category
8683
+ });
8684
+ continue;
8685
+ }
8686
+ const hasLocalEdits = fingerprint?.checksums?.[relativePath] ? fingerprint.checksums[relativePath] !== projectChecksum : true;
8687
+ if (!options.dryRun) {
8688
+ const backupPath = path30.join(backupDir, ...relativePath.split("/"));
8689
+ await fs30.ensureDir(path30.dirname(backupPath));
8690
+ await fs30.copy(projectFile, backupPath);
8691
+ await fs30.copy(templateFile, projectFile);
8692
+ }
8693
+ results.push({
8694
+ relativePath,
8695
+ status: "updated",
8696
+ category: entry.category,
8697
+ backedUp: true,
8698
+ reason: hasLocalEdits ? "you have local edits \u2014 backup created" : "backup created"
8699
+ });
8700
+ }
8701
+ }
8702
+ if (options.dryRun) {
8703
+ log.step("mars template sync --dry-run");
8704
+ log.blank();
8705
+ }
8706
+ for (const result of results) {
8707
+ const icon = STATUS_ICONS[result.status];
8708
+ let label;
8709
+ switch (result.status) {
8710
+ case "unchanged":
8711
+ label = "unchanged";
8712
+ break;
8713
+ case "updated":
8714
+ label = result.reason ? `will update (${result.reason})` : "will update";
8715
+ break;
8716
+ case "new":
8717
+ label = "new file";
8718
+ break;
8719
+ case "skipped":
8720
+ label = `skipped (${result.reason ?? result.category})`;
8721
+ break;
8722
+ }
8723
+ log.info(`${icon} ${result.relativePath} ${label}`);
8724
+ }
8725
+ log.blank();
8726
+ const updated = results.filter((r) => r.status === "updated").length;
8727
+ const added = results.filter((r) => r.status === "new").length;
8728
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
8729
+ const skipped = results.filter((r) => r.status === "skipped").length;
8730
+ if (options.dryRun) {
8731
+ log.info(
8732
+ `Planned: ${updated} to update, ${added} new, ${unchanged} unchanged, ${skipped} skipped`
8733
+ );
8734
+ log.info("Run without --dry-run to apply changes.");
8735
+ } else {
8736
+ log.success(
8737
+ `${updated + added} files updated, ${unchanged} unchanged, ${skipped} skipped`
8738
+ );
8739
+ if (updated > 0) {
8740
+ log.info(`Backups saved to .mars/backups/${timestamp}/`);
8741
+ }
8742
+ }
8743
+ return results;
8744
+ }
8745
+ function templateCommand(program2) {
8746
+ const template = program2.command("template").description("Template management commands");
8747
+ template.command("sync").description(
8748
+ "Sync plumbing files from the upstream Mars template into your project"
8749
+ ).option("--dry-run", "Show planned changes without writing any files").option("--force", "Also overwrite user-owned files (with backup)").option(
8750
+ "--only <category>",
8751
+ "Limit sync to a specific category (default: plumbing)"
8752
+ ).action(async (syncOptions) => {
8753
+ await runTemplateSync(syncOptions);
8754
+ });
8755
+ }
8756
+
8546
8757
  // src/index.ts
8547
8758
  init_blog();
8548
8759
  init_dark_mode();
@@ -8629,6 +8840,7 @@ program.command("doctor").description("Check your development environment").opti
8629
8840
  await doctorCommand(options);
8630
8841
  });
8631
8842
  upgradeCommand(program);
8843
+ templateCommand(program);
8632
8844
  program.command("telemetry").description("Manage anonymous usage telemetry").argument("<action>", "enable or disable").action((action) => {
8633
8845
  if (action === "enable") {
8634
8846
  enableTelemetry();