@dafish/gogo-meta 1.2.0 → 1.4.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/cli.js CHANGED
@@ -3,7 +3,8 @@ import { Command } from 'commander';
3
3
  import { readFileSync } from 'fs';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { dirname, join, basename } from 'path';
6
- import { mkdir, appendFile, access, writeFile, readFile, unlink, symlink } from 'fs/promises';
6
+ import { unlink, mkdir, appendFile, access, writeFile, readFile, symlink } from 'fs/promises';
7
+ import { stringify, parse } from 'yaml';
7
8
  import { z } from 'zod';
8
9
  import pc from 'picocolors';
9
10
  import { homedir } from 'os';
@@ -208,6 +209,7 @@ var LoopRcSchema = z.object({
208
209
  // src/core/config.ts
209
210
  var META_FILE = ".gogo";
210
211
  var LOOPRC_FILE = ".looprc";
212
+ var META_FILE_CANDIDATES = [".gogo", ".gogo.yaml", ".gogo.yml"];
211
213
  var ConfigError = class extends Error {
212
214
  constructor(message, path) {
213
215
  super(message);
@@ -215,6 +217,21 @@ var ConfigError = class extends Error {
215
217
  this.name = "ConfigError";
216
218
  }
217
219
  };
220
+ function detectFormat(filePath) {
221
+ if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
222
+ return "yaml";
223
+ }
224
+ return "json";
225
+ }
226
+ function filenameForFormat(format) {
227
+ return format === "yaml" ? ".gogo.yaml" : ".gogo";
228
+ }
229
+ function parseContent(content, format) {
230
+ return format === "yaml" ? parse(content) : JSON.parse(content);
231
+ }
232
+ function serializeContent(data, format) {
233
+ return format === "yaml" ? stringify(data, { indent: 2 }) : JSON.stringify(data, null, 2) + "\n";
234
+ }
218
235
  async function fileExists(path) {
219
236
  try {
220
237
  await access(path);
@@ -237,31 +254,53 @@ async function findFileUp(filename, startDir) {
237
254
  currentDir = parentDir;
238
255
  }
239
256
  }
257
+ async function findMetaFileUp(startDir) {
258
+ let currentDir = startDir;
259
+ while (true) {
260
+ for (const candidate of META_FILE_CANDIDATES) {
261
+ const filePath = join(currentDir, candidate);
262
+ if (await fileExists(filePath)) {
263
+ return filePath;
264
+ }
265
+ }
266
+ const parentDir = dirname(currentDir);
267
+ if (parentDir === currentDir) {
268
+ return null;
269
+ }
270
+ currentDir = parentDir;
271
+ }
272
+ }
240
273
  async function readMetaConfig(cwd) {
241
- const metaPath = await findFileUp(META_FILE, cwd);
274
+ const metaPath = await findMetaFileUp(cwd);
242
275
  if (!metaPath) {
243
276
  throw new ConfigError(
244
277
  `No ${META_FILE} file found. Run 'gogo init' to create one, or navigate to a directory with a ${META_FILE} file.`
245
278
  );
246
279
  }
280
+ const format = detectFormat(metaPath);
247
281
  try {
248
282
  const content = await readFile(metaPath, "utf-8");
249
- const parsed = JSON.parse(content);
250
- return MetaConfigSchema.parse(parsed);
283
+ const parsed = parseContent(content, format);
284
+ const config = MetaConfigSchema.parse(parsed);
285
+ return { config, format, metaDir: dirname(metaPath) };
251
286
  } catch (error2) {
252
287
  if (error2 instanceof SyntaxError) {
253
- throw new ConfigError(`Invalid JSON in ${META_FILE} file`, metaPath);
288
+ throw new ConfigError(`Invalid JSON in config file`, metaPath);
289
+ }
290
+ if (error2 instanceof Error && error2.name === "YAMLParseError") {
291
+ throw new ConfigError(`Invalid YAML in config file`, metaPath);
254
292
  }
255
293
  if (error2 instanceof Error && error2.name === "ZodError") {
256
- throw new ConfigError(`Invalid ${META_FILE} file structure: ${error2.message}`, metaPath);
294
+ throw new ConfigError(`Invalid config file structure: ${error2.message}`, metaPath);
257
295
  }
258
296
  throw error2;
259
297
  }
260
298
  }
261
- async function writeMetaConfig(cwd, config) {
262
- const metaPath = join(cwd, META_FILE);
299
+ async function writeMetaConfig(cwd, config, format = "json") {
300
+ const filename = filenameForFormat(format);
301
+ const metaPath = join(cwd, filename);
263
302
  const validated = MetaConfigSchema.parse(config);
264
- const content = JSON.stringify(validated, null, 2) + "\n";
303
+ const content = serializeContent(validated, format);
265
304
  await writeFile(metaPath, content, "utf-8");
266
305
  }
267
306
  async function readLoopRc(cwd) {
@@ -278,7 +317,7 @@ async function readLoopRc(cwd) {
278
317
  }
279
318
  }
280
319
  function getMetaDir(cwd) {
281
- return findFileUp(META_FILE, cwd).then((path) => path ? dirname(path) : null);
320
+ return findMetaFileUp(cwd).then((path) => path ? dirname(path) : null);
282
321
  }
283
322
  function createDefaultConfig() {
284
323
  return {
@@ -398,22 +437,36 @@ function summary(results) {
398
437
  // src/commands/init.ts
399
438
  async function initCommand(options = {}) {
400
439
  const cwd = process.cwd();
401
- const metaPath = join(cwd, META_FILE);
402
- if (await fileExists(metaPath)) {
440
+ const format = options.format === "yaml" ? "yaml" : "json";
441
+ const existingFiles = [];
442
+ for (const candidate of META_FILE_CANDIDATES) {
443
+ const candidatePath = join(cwd, candidate);
444
+ if (await fileExists(candidatePath)) {
445
+ existingFiles.push(candidate);
446
+ }
447
+ }
448
+ if (existingFiles.length > 0) {
403
449
  if (!options.force) {
404
450
  throw new Error(
405
- `${META_FILE} file already exists. Use --force to overwrite.`
451
+ `${existingFiles[0]} file already exists. Use --force to overwrite.`
406
452
  );
407
453
  }
408
- warning(`Overwriting existing ${META_FILE} file`);
454
+ for (const candidate of existingFiles) {
455
+ await unlink(join(cwd, candidate));
456
+ }
457
+ warning(`Overwriting existing config file`);
409
458
  }
410
459
  const config = createDefaultConfig();
411
- await writeMetaConfig(cwd, config);
412
- success(`Created ${META_FILE} file in ${cwd}`);
460
+ await writeMetaConfig(cwd, config, format);
461
+ const filename = filenameForFormat(format);
462
+ success(`Created ${filename} file in ${cwd}`);
413
463
  info("Add projects with: gogo project import <folder> <repo-url>");
414
464
  }
415
465
  function registerInitCommand(program) {
416
- program.command("init").description("Initialize a new gogo-meta repository").option("-f, --force", "Overwrite existing .gogo file").action(async (options) => {
466
+ program.command("init").description("Initialize a new gogo-meta repository").option("-f, --force", "Overwrite existing config file").option("--format <format>", "Config file format (json or yaml)", "json").action(async (options) => {
467
+ if (options.format && !["json", "yaml"].includes(options.format)) {
468
+ throw new Error(`Invalid format "${options.format}". Use "json" or "yaml".`);
469
+ }
417
470
  await initCommand(options);
418
471
  });
419
472
  }
@@ -571,7 +624,7 @@ async function execCommand(command, options = {}) {
571
624
  if (!metaDir) {
572
625
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
573
626
  }
574
- const config = await readMetaConfig(cwd);
627
+ const { config } = await readMetaConfig(cwd);
575
628
  const filterOptions = createFilterOptions(options);
576
629
  info(`Executing: ${bold(command)}`);
577
630
  const results = await loop(command, { config, metaDir }, {
@@ -613,7 +666,7 @@ async function runCommand(name, options = {}) {
613
666
  if (!metaDir) {
614
667
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
615
668
  }
616
- const config = await readMetaConfig(cwd);
669
+ const { config } = await readMetaConfig(cwd);
617
670
  if (options.list || name === void 0) {
618
671
  formatCommandList(listCommands(config));
619
672
  return;
@@ -749,12 +802,12 @@ async function cloneCommand(url, options = {}) {
749
802
  return;
750
803
  }
751
804
  success(`Cloned meta repository to ${repoName}`);
752
- const metaPath = join(targetDir, ".gogo");
753
- if (!await fileExists(metaPath)) {
754
- warning("No .gogo file found in cloned repository");
805
+ const metaPath = await findMetaFileUp(targetDir);
806
+ if (!metaPath) {
807
+ warning("No config file found in cloned repository");
755
808
  return;
756
809
  }
757
- const config = await readMetaConfig(targetDir);
810
+ const { config } = await readMetaConfig(targetDir);
758
811
  const projects = Object.entries(config.projects);
759
812
  if (projects.length === 0) {
760
813
  info("No child repositories defined in .gogo");
@@ -804,7 +857,7 @@ async function updateCommand(options = {}) {
804
857
  if (!metaDir) {
805
858
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
806
859
  }
807
- const config = await readMetaConfig(cwd);
860
+ const { config } = await readMetaConfig(cwd);
808
861
  const filterOptions = createFilterOptions(options);
809
862
  let projectEntries = Object.entries(config.projects);
810
863
  const projectPaths = projectEntries.map(([path]) => path);
@@ -884,7 +937,7 @@ async function statusCommand(options = {}) {
884
937
  if (!metaDir) {
885
938
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
886
939
  }
887
- const config = await readMetaConfig(cwd);
940
+ const { config } = await readMetaConfig(cwd);
888
941
  const filterOptions = createFilterOptions(options);
889
942
  info("Checking git status across repositories...");
890
943
  const results = await loop("git status --short --branch", { config, metaDir }, {
@@ -904,7 +957,7 @@ async function pullCommand(options = {}) {
904
957
  if (!metaDir) {
905
958
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
906
959
  }
907
- const config = await readMetaConfig(cwd);
960
+ const { config } = await readMetaConfig(cwd);
908
961
  const filterOptions = createFilterOptions(options);
909
962
  info("Pulling changes across repositories...");
910
963
  const results = await loop("git pull", { config, metaDir }, {
@@ -925,7 +978,7 @@ async function pushCommand(options = {}) {
925
978
  if (!metaDir) {
926
979
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
927
980
  }
928
- const config = await readMetaConfig(cwd);
981
+ const { config } = await readMetaConfig(cwd);
929
982
  const filterOptions = createFilterOptions(options);
930
983
  info("Pushing changes across repositories...");
931
984
  const results = await loop("git push", { config, metaDir }, {
@@ -945,7 +998,7 @@ async function branchCommand(name, options = {}) {
945
998
  if (!metaDir) {
946
999
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
947
1000
  }
948
- const config = await readMetaConfig(cwd);
1001
+ const { config } = await readMetaConfig(cwd);
949
1002
  const filterOptions = createFilterOptions(options);
950
1003
  let command;
951
1004
  let actionMessage;
@@ -979,7 +1032,7 @@ async function checkoutCommand(branch, options = {}) {
979
1032
  if (!metaDir) {
980
1033
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
981
1034
  }
982
- const config = await readMetaConfig(cwd);
1035
+ const { config } = await readMetaConfig(cwd);
983
1036
  const filterOptions = createFilterOptions(options);
984
1037
  const flag = options.create ? "-b " : "";
985
1038
  const command = `git checkout ${flag}"${branch}"`;
@@ -1002,7 +1055,7 @@ async function commitCommand(options) {
1002
1055
  if (!metaDir) {
1003
1056
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
1004
1057
  }
1005
- const config = await readMetaConfig(cwd);
1058
+ const { config } = await readMetaConfig(cwd);
1006
1059
  const filterOptions = createFilterOptions(options);
1007
1060
  const escapedMessage = options.message.replace(/"/g, '\\"');
1008
1061
  const command = `git commit -m "${escapedMessage}"`;
@@ -1068,9 +1121,9 @@ async function createCommand(folder, url) {
1068
1121
  if (remoteResult.exitCode !== 0) {
1069
1122
  throw new Error(`Failed to add remote: ${remoteResult.stderr}`);
1070
1123
  }
1071
- const config = await readMetaConfig(metaDir);
1124
+ const { config, format } = await readMetaConfig(metaDir);
1072
1125
  const updatedConfig = addProject(config, folder, url);
1073
- await writeMetaConfig(metaDir, updatedConfig);
1126
+ await writeMetaConfig(metaDir, updatedConfig, format);
1074
1127
  const gitignorePath = join(metaDir, ".gitignore");
1075
1128
  if (await fileExists(gitignorePath)) {
1076
1129
  await appendFile(gitignorePath, `
@@ -1112,9 +1165,9 @@ async function importCommand(folder, url, options = {}) {
1112
1165
  info(`Existing: ${existingUrl}`);
1113
1166
  info(`Provided: ${url}`);
1114
1167
  }
1115
- const config = await readMetaConfig(metaDir);
1168
+ const { config, format } = await readMetaConfig(metaDir);
1116
1169
  const updatedConfig = addProject(config, folder, finalUrl);
1117
- await writeMetaConfig(metaDir, updatedConfig);
1170
+ await writeMetaConfig(metaDir, updatedConfig, format);
1118
1171
  success(`Imported existing project "${folder}"`);
1119
1172
  info(`Repository URL: ${finalUrl}`);
1120
1173
  } else {
@@ -1122,9 +1175,9 @@ async function importCommand(folder, url, options = {}) {
1122
1175
  throw new Error("URL is required when importing a non-existent project");
1123
1176
  }
1124
1177
  if (options.noClone) {
1125
- const config2 = await readMetaConfig(metaDir);
1178
+ const { config: config2, format: format2 } = await readMetaConfig(metaDir);
1126
1179
  const updatedConfig2 = addProject(config2, folder, url);
1127
- await writeMetaConfig(metaDir, updatedConfig2);
1180
+ await writeMetaConfig(metaDir, updatedConfig2, format2);
1128
1181
  const added2 = await addToGitignore(metaDir, folder);
1129
1182
  success(`Registered project "${folder}" (not cloned)`);
1130
1183
  if (added2) {
@@ -1142,9 +1195,9 @@ async function importCommand(folder, url, options = {}) {
1142
1195
  if (cloneResult.exitCode !== 0) {
1143
1196
  throw new Error(`Failed to clone repository: ${cloneResult.stderr}`);
1144
1197
  }
1145
- const config = await readMetaConfig(metaDir);
1198
+ const { config, format } = await readMetaConfig(metaDir);
1146
1199
  const updatedConfig = addProject(config, folder, url);
1147
- await writeMetaConfig(metaDir, updatedConfig);
1200
+ await writeMetaConfig(metaDir, updatedConfig, format);
1148
1201
  success(`Imported project "${folder}"`);
1149
1202
  }
1150
1203
  const added = await addToGitignore(metaDir, folder);
@@ -1171,7 +1224,7 @@ async function installCommand(variant, options = {}) {
1171
1224
  if (!metaDir) {
1172
1225
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
1173
1226
  }
1174
- const config = await readMetaConfig(cwd);
1227
+ const { config } = await readMetaConfig(cwd);
1175
1228
  const filterOptions = createFilterOptions(options);
1176
1229
  const command = `npm ${variant}`;
1177
1230
  info(`Running "${command}" across repositories...`);
@@ -1217,7 +1270,7 @@ async function linkCommand(options = {}) {
1217
1270
  if (!metaDir) {
1218
1271
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
1219
1272
  }
1220
- const config = await readMetaConfig(cwd);
1273
+ const { config } = await readMetaConfig(cwd);
1221
1274
  const filterOptions = createFilterOptions(options);
1222
1275
  let projectPaths = Object.keys(config.projects);
1223
1276
  projectPaths = applyFilters(projectPaths, filterOptions);
@@ -1295,7 +1348,7 @@ async function runCommand2(script, options = {}) {
1295
1348
  if (!metaDir) {
1296
1349
  throw new Error('Not in a gogo-meta repository. Run "gogo init" first.');
1297
1350
  }
1298
- const config = await readMetaConfig(cwd);
1351
+ const { config } = await readMetaConfig(cwd);
1299
1352
  const filterOptions = createFilterOptions(options);
1300
1353
  info(`Running "npm run ${script}" across repositories...`);
1301
1354
  const command = options.ifPresent ? async (dir, _projectPath) => {