@dafish/gogo-meta 1.1.0 → 1.2.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.d.ts CHANGED
@@ -112,6 +112,7 @@ declare function listCommands(metaConfig: MetaConfig): Array<{
112
112
  name: string;
113
113
  command: ResolvedCommand;
114
114
  }>;
115
+ declare function addToGitignore(metaDir: string, entry: string): Promise<boolean>;
115
116
 
116
117
  declare function execute(command: string, options: ExecutorOptions): Promise<ExecutorResult>;
117
118
  declare function executeSync(command: string, options: ExecutorOptions): ExecutorResult;
@@ -164,4 +165,39 @@ declare function summary(results: {
164
165
  }): void;
165
166
  declare function formatDuration(ms: number): string;
166
167
 
167
- export { type CommandConfig, type CommandConfigObject, CommandConfigObjectSchema, CommandConfigSchema, type CommandHandler, ConfigError, type ExecutorOptions, type ExecutorResult, type FilterOptions, LOOPRC_FILE, type LoopContext, type LoopOptions, type LoopRc, LoopRcSchema, type LoopResult, META_FILE, type MetaConfig, MetaConfigSchema, type ProjectInfo, type ResolvedCommand, addProject, applyFilters, bold, commandOutput, createDefaultConfig, createFilterOptions, dim, error, execute, executeStreaming, executeSync, fileExists, filterFromLoopRc, findFileUp, formatDuration, getCommand, getExitCode, getMetaDir, getProjectPaths, getProjectUrl, hasFailures, header, info, listCommands, loop, normalizeCommand, parseFilterList, parseFilterPattern, projectStatus, readLoopRc, readMetaConfig, removeProject, success, summary, symbols, warning, writeMetaConfig };
168
+ /**
169
+ * Extracts the SSH host from a git URL.
170
+ * Supports formats like:
171
+ * - git@github.com:user/repo.git
172
+ * - ssh://git@github.com/user/repo.git
173
+ * - git@gitlab.example.com:2222:user/repo.git (custom port)
174
+ *
175
+ * Returns null for non-SSH URLs (https://, file://, etc.)
176
+ */
177
+ declare function extractSshHost(url: string): string | null;
178
+ /**
179
+ * Extracts unique SSH hosts from a list of repository URLs.
180
+ */
181
+ declare function extractUniqueSshHosts(urls: string[]): string[];
182
+ /**
183
+ * Checks if a host is already in the known_hosts file.
184
+ */
185
+ declare function isHostKnown(host: string): Promise<boolean>;
186
+ /**
187
+ * Adds a host's SSH key to known_hosts using ssh-keyscan.
188
+ * Returns true if successful, false otherwise.
189
+ */
190
+ declare function addHostKey(host: string): boolean;
191
+ /**
192
+ * Ensures all SSH hosts for the given repository URLs are in known_hosts.
193
+ * Prompts or automatically adds missing host keys.
194
+ *
195
+ * @param urls - Array of git repository URLs
196
+ * @returns Object with arrays of added and failed hosts
197
+ */
198
+ declare function ensureSshHostsKnown(urls: string[]): Promise<{
199
+ added: string[];
200
+ failed: string[];
201
+ }>;
202
+
203
+ export { type CommandConfig, type CommandConfigObject, CommandConfigObjectSchema, CommandConfigSchema, type CommandHandler, ConfigError, type ExecutorOptions, type ExecutorResult, type FilterOptions, LOOPRC_FILE, type LoopContext, type LoopOptions, type LoopRc, LoopRcSchema, type LoopResult, META_FILE, type MetaConfig, MetaConfigSchema, type ProjectInfo, type ResolvedCommand, addHostKey, addProject, addToGitignore, applyFilters, bold, commandOutput, createDefaultConfig, createFilterOptions, dim, ensureSshHostsKnown, error, execute, executeStreaming, executeSync, extractSshHost, extractUniqueSshHosts, fileExists, filterFromLoopRc, findFileUp, formatDuration, getCommand, getExitCode, getMetaDir, getProjectPaths, getProjectUrl, hasFailures, header, info, isHostKnown, listCommands, loop, normalizeCommand, parseFilterList, parseFilterPattern, projectStatus, readLoopRc, readMetaConfig, removeProject, success, summary, symbols, warning, writeMetaConfig };
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { mkdir, appendFile, access, writeFile, readFile, unlink, symlink } from 'fs/promises';
4
4
  import { dirname, join, basename } from 'path';
5
5
  import pc from 'picocolors';
6
+ import { homedir } from 'os';
6
7
  import { Command } from 'commander';
7
8
  import { readFileSync } from 'fs';
8
9
  import { fileURLToPath } from 'url';
@@ -329,6 +330,23 @@ function listCommands(metaConfig) {
329
330
  command: normalizeCommand(config)
330
331
  }));
331
332
  }
333
+ async function addToGitignore(metaDir, entry) {
334
+ const gitignorePath = join(metaDir, ".gitignore");
335
+ if (await fileExists(gitignorePath)) {
336
+ const content = await readFile(gitignorePath, "utf-8");
337
+ const lines = content.split("\n").map((line) => line.trim());
338
+ if (lines.includes(entry)) {
339
+ return false;
340
+ }
341
+ const suffix = content.endsWith("\n") ? "" : "\n";
342
+ await appendFile(gitignorePath, `${suffix}${entry}
343
+ `);
344
+ } else {
345
+ await writeFile(gitignorePath, `${entry}
346
+ `, "utf-8");
347
+ }
348
+ return true;
349
+ }
332
350
 
333
351
  // src/core/index.ts
334
352
  init_executor();
@@ -550,6 +568,79 @@ function hasFailures(results) {
550
568
  function getExitCode(results) {
551
569
  return hasFailures(results) ? 1 : 0;
552
570
  }
571
+
572
+ // src/core/ssh.ts
573
+ init_executor();
574
+ function extractSshHost(url) {
575
+ if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
576
+ return null;
577
+ }
578
+ const sshMatch = url.match(/^ssh:\/\/[^@]+@([^/:]+)/);
579
+ if (sshMatch) {
580
+ return sshMatch[1] ?? null;
581
+ }
582
+ const gitMatch = url.match(/^[^@]+@([^:]+):/);
583
+ if (gitMatch) {
584
+ return gitMatch[1] ?? null;
585
+ }
586
+ return null;
587
+ }
588
+ function extractUniqueSshHosts(urls) {
589
+ const hosts = /* @__PURE__ */ new Set();
590
+ for (const url of urls) {
591
+ const host = extractSshHost(url);
592
+ if (host) {
593
+ hosts.add(host);
594
+ }
595
+ }
596
+ return Array.from(hosts);
597
+ }
598
+ async function isHostKnown(host) {
599
+ const knownHostsPath = join(homedir(), ".ssh", "known_hosts");
600
+ try {
601
+ const content = await readFile(knownHostsPath, "utf-8");
602
+ const hostPatterns = [
603
+ new RegExp(`^${escapeRegex(host)}[,\\s]`, "m"),
604
+ new RegExp(`^\\[${escapeRegex(host)}\\]:\\d+[,\\s]`, "m")
605
+ ];
606
+ return hostPatterns.some((pattern) => pattern.test(content));
607
+ } catch {
608
+ return false;
609
+ }
610
+ }
611
+ function escapeRegex(str) {
612
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
613
+ }
614
+ function addHostKey(host) {
615
+ const knownHostsPath = join(homedir(), ".ssh", "known_hosts");
616
+ const result = executeSync(`ssh-keyscan -H "${host}" >> "${knownHostsPath}" 2>/dev/null`, {
617
+ cwd: process.cwd()
618
+ });
619
+ return result.exitCode === 0;
620
+ }
621
+ async function ensureSshHostsKnown(urls) {
622
+ const hosts = extractUniqueSshHosts(urls);
623
+ const added = [];
624
+ const failed = [];
625
+ if (hosts.length === 0) {
626
+ return { added, failed };
627
+ }
628
+ for (const host of hosts) {
629
+ const known = await isHostKnown(host);
630
+ if (!known) {
631
+ info(`Adding SSH host key for ${host}...`);
632
+ const success2 = addHostKey(host);
633
+ if (success2) {
634
+ success(`Added host key for ${host}`);
635
+ added.push(host);
636
+ } else {
637
+ error(`Failed to add host key for ${host}`);
638
+ failed.push(host);
639
+ }
640
+ }
641
+ }
642
+ return { added, failed };
643
+ }
553
644
  async function initCommand(options = {}) {
554
645
  const cwd = process.cwd();
555
646
  const metaPath = join(cwd, META_FILE);
@@ -672,6 +763,7 @@ async function cloneCommand(url, options = {}) {
672
763
  if (await fileExists(targetDir)) {
673
764
  throw new Error(`Directory "${repoName}" already exists`);
674
765
  }
766
+ await ensureSshHostsKnown([url]);
675
767
  info(`Cloning meta repository: ${url}`);
676
768
  const cloneResult = await execute(`git clone "${url}" "${repoName}"`, { cwd });
677
769
  if (cloneResult.exitCode !== 0) {
@@ -692,6 +784,13 @@ async function cloneCommand(url, options = {}) {
692
784
  info("No child repositories defined in .gogo");
693
785
  return;
694
786
  }
787
+ const urls = projects.map(([, url2]) => url2);
788
+ const { failed: failedHosts } = await ensureSshHostsKnown(urls);
789
+ if (failedHosts.length > 0) {
790
+ warning(
791
+ `Could not verify SSH host keys for: ${failedHosts.join(", ")}. Clone may fail.`
792
+ );
793
+ }
695
794
  info(`Cloning ${projects.length} child repositories...`);
696
795
  let successCount = 0;
697
796
  let failCount = 0;
@@ -751,6 +850,13 @@ async function updateCommand(options = {}) {
751
850
  success("All repositories are already cloned");
752
851
  return;
753
852
  }
853
+ const urls = missing.map(([, url]) => url);
854
+ const { failed: failedHosts } = await ensureSshHostsKnown(urls);
855
+ if (failedHosts.length > 0) {
856
+ warning(
857
+ `Could not verify SSH host keys for: ${failedHosts.join(", ")}. Clone may fail.`
858
+ );
859
+ }
754
860
  info(`Cloning ${missing.length} missing repositories...`);
755
861
  let successCount = 0;
756
862
  let failCount = 0;
@@ -1043,7 +1149,11 @@ async function importCommand(folder, url, options = {}) {
1043
1149
  const config2 = await readMetaConfig(metaDir);
1044
1150
  const updatedConfig2 = addProject(config2, folder, url);
1045
1151
  await writeMetaConfig(metaDir, updatedConfig2);
1152
+ const added2 = await addToGitignore(metaDir, folder);
1046
1153
  success(`Registered project "${folder}" (not cloned)`);
1154
+ if (added2) {
1155
+ info(`Added ${folder} to .gitignore`);
1156
+ }
1047
1157
  info(`Run "gogo git update" to clone missing projects`);
1048
1158
  return;
1049
1159
  }
@@ -1061,11 +1171,8 @@ async function importCommand(folder, url, options = {}) {
1061
1171
  await writeMetaConfig(metaDir, updatedConfig);
1062
1172
  success(`Imported project "${folder}"`);
1063
1173
  }
1064
- const gitignorePath = join(metaDir, ".gitignore");
1065
- if (await fileExists(gitignorePath)) {
1066
- await appendFile(gitignorePath, `
1067
- ${folder}
1068
- `);
1174
+ const added = await addToGitignore(metaDir, folder);
1175
+ if (added) {
1069
1176
  info(`Added ${folder} to .gitignore`);
1070
1177
  }
1071
1178
  }
@@ -1304,6 +1411,6 @@ async function main() {
1304
1411
  }
1305
1412
  main();
1306
1413
 
1307
- export { CommandConfigObjectSchema, CommandConfigSchema, ConfigError, LOOPRC_FILE, LoopRcSchema, META_FILE, MetaConfigSchema, addProject, applyFilters, bold, commandOutput, createDefaultConfig, createFilterOptions, createProgram, dim, error, execute, executeStreaming, executeSync, fileExists, filterFromLoopRc, findFileUp, formatDuration, getCommand, getExitCode, getMetaDir, getProjectPaths, getProjectUrl, hasFailures, header, info, listCommands, loop, normalizeCommand, parseFilterList, parseFilterPattern, projectStatus, readLoopRc, readMetaConfig, removeProject, success, summary, symbols, warning, writeMetaConfig };
1414
+ export { CommandConfigObjectSchema, CommandConfigSchema, ConfigError, LOOPRC_FILE, LoopRcSchema, META_FILE, MetaConfigSchema, addHostKey, addProject, addToGitignore, applyFilters, bold, commandOutput, createDefaultConfig, createFilterOptions, createProgram, dim, ensureSshHostsKnown, error, execute, executeStreaming, executeSync, extractSshHost, extractUniqueSshHosts, fileExists, filterFromLoopRc, findFileUp, formatDuration, getCommand, getExitCode, getMetaDir, getProjectPaths, getProjectUrl, hasFailures, header, info, isHostKnown, listCommands, loop, normalizeCommand, parseFilterList, parseFilterPattern, projectStatus, readLoopRc, readMetaConfig, removeProject, success, summary, symbols, warning, writeMetaConfig };
1308
1415
  //# sourceMappingURL=index.js.map
1309
1416
  //# sourceMappingURL=index.js.map