@cocreate/cli 1.54.2 → 1.56.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/src/coc.js CHANGED
@@ -7,6 +7,7 @@ const execute = require("./execute");
7
7
  const addMeta = require("./addMeta");
8
8
  const { color } = require("./fonts");
9
9
  const { getConfig } = require("./getConfig");
10
+ const fsp = require('fs').promises;
10
11
 
11
12
  // Configuration object for storing options
12
13
  let config = {};
@@ -16,140 +17,208 @@ const argv = process.argv.slice(2);
16
17
 
17
18
  // Define available command-line options
18
19
  const availableOptions = {
19
- "--self": { key: "self", type: "boolean" },
20
- "-s": { key: "self", type: "boolean" },
21
- "--directory": { key: "directory", type: "string" },
22
- "-d": { key: "directory", type: "string" },
23
- "--config": { key: "config", type: "string" },
24
- "-c": { key: "config", type: "string" }
20
+ "--self": { key: "self", type: "boolean" },
21
+ "-s": { key: "self", type: "boolean" },
22
+ "--directory": { key: "directory", type: "string" },
23
+ "-d": { key: "directory", type: "string" },
24
+ "--config": { key: "config", type: "string" },
25
+ "-c": { key: "config", type: "string" }
25
26
  };
26
27
 
27
28
  const options = {};
28
29
  const commandParts = [];
29
30
  for (let i = 0; i < argv.length; i++) {
30
- let option = availableOptions[argv[i]];
31
- if (option) {
32
- if (option.type === "boolean") {
33
- options[option.key] = true;
34
- } else if (option.type === "string") {
35
- options[option.key] = argv[i + 1];
36
- argv.splice(i + 1, 1);
37
- }
38
- } else {
39
- if (argv[i].match(/[\s'"]/)) {
40
- commandParts.push(`'${argv[i].replace(/'/g, "\\'")}'`);
41
- } else {
42
- commandParts.push(argv[i]);
43
- }
44
- }
31
+ let option = availableOptions[argv[i]];
32
+ if (option) {
33
+ if (option.type === "boolean") {
34
+ options[option.key] = true;
35
+ } else if (option.type === "string") {
36
+ options[option.key] = argv[i + 1];
37
+ argv.splice(i + 1, 1);
38
+ }
39
+ } else {
40
+ if (argv[i].match(/[\s'"]/)) {
41
+ commandParts.push(`'${argv[i].replace(/'/g, "\\'")}'`);
42
+ } else {
43
+ commandParts.push(argv[i]);
44
+ }
45
+ }
45
46
  }
46
47
 
47
- command = commandParts.join(" ");
48
+ let command = commandParts.join(" ");
48
49
 
49
50
  /**
50
51
  * Main function to execute commands across repositories.
51
52
  * @param {Object} config - The configuration object.
52
- * @param {Array} [repos=null] - List of repositories to process.
53
- * @param {string} [directory=null] - The directory path of the configuration.
53
+ * @param {Object} options - CLI options/flags.
54
54
  */
55
55
  async function main(config = {}, options) {
56
- let directory = options["directory"] || process.cwd();
57
- if (!config.repositories) {
58
- let configString = options["config"];
59
- if (configString) {
60
- try {
61
- config = JSON.parse(configString);
62
- console.warn(
63
- `${color.yellow}using supplied JSON configuration${color.reset}`
64
- );
65
- } catch (error) {
66
- directory = configString;
67
- config = await getConfig(directory);
68
- }
69
- } else {
70
- config = await getConfig(directory);
71
- }
72
-
73
- if (!config.configPath) {
74
- const package = await getConfig(directory, "package.json");
75
- if (package.configPath) {
76
- config.configPath = package.configPath;
77
- const repoUrl =
78
- package.repository && package.repository.url.substring(12);
79
- config.repositories = [
80
- {
81
- path: package.configPath,
82
- repo: repoUrl,
83
- entry: package.main
84
- }
85
- ];
86
- } else {
87
- console.error(
88
- `${color.red}config file cannot be found${color.reset}`
89
- );
90
- process.exit(1);
91
- }
92
- }
93
-
94
- directory = path.dirname(config.configPath);
95
- console.warn(
96
- `${color.yellow}using config ${config.configPath} ${color.reset}`
97
- );
98
- }
99
-
100
- config.hideMessage = false;
101
-
102
- // Add metadata to repositories if any are present
103
- if (config.repositories && config.repositories.length) {
104
- config.repositories = await addMeta(config.repositories, [], directory);
105
- }
106
-
107
- // Execute the command across repositories
108
- const failed = await execute(command, config);
109
-
110
- // Handle any failed command executions
111
- if (failed && failed.length > 0) {
112
- console.log(
113
- color.red +
114
- " **************** failures **************** " +
115
- color.reset
116
- );
117
- for (const failure of failed) {
118
- console.log(
119
- color.red + `${failure.name}: ${failure.error}` + color.reset
120
- );
121
- }
122
-
123
- // Prompt user to retry failed commands
124
- await promptRetry(failed, config, options);
125
- }
56
+ let directory = options["directory"] || process.cwd();
57
+
58
+ // Check if any repository targets exist in the configuration
59
+ if (!config.repositories && !config.modules && !config.projects) {
60
+ let configString = options["config"];
61
+ if (configString) {
62
+ try {
63
+ config = JSON.parse(configString);
64
+ console.warn(
65
+ `${color.yellow}using supplied JSON configuration${color.reset}`
66
+ );
67
+ } catch (error) {
68
+ directory = configString;
69
+ config = await getConfig(directory);
70
+ }
71
+ } else {
72
+ config = await getConfig(directory);
73
+ }
74
+
75
+ if (!config.configPath) {
76
+ const packageJson = await getConfig(directory, "package.json");
77
+ if (packageJson.configPath) {
78
+ config.configPath = packageJson.configPath;
79
+ const repoUrl =
80
+ packageJson.repository && packageJson.repository.url.substring(12);
81
+
82
+ // Fallback to old format if reading strictly from an old package.json pointer
83
+ config.repositories = [
84
+ {
85
+ path: packageJson.configPath,
86
+ repo: repoUrl,
87
+ entry: packageJson.main
88
+ }
89
+ ];
90
+ } else {
91
+ console.warn(
92
+ `${color.yellow}CoCreate.config.js not found. Generating a new one...${color.reset}`
93
+ );
94
+
95
+ const templatePath = path.resolve(__dirname, './config.template.js');
96
+ let templateConfig;
97
+ try {
98
+ templateConfig = require(templatePath);
99
+ } catch (e) {
100
+ console.error(`${color.red}Error loading config.template.js:`, e, color.reset);
101
+ process.exit(1);
102
+ }
103
+
104
+ // Customize the template with repository information from package.json
105
+ const currentRepoPath = '.'; // Current directory where config is generated
106
+ const repoUrl = packageJson.repository && packageJson.repository.url ? packageJson.repository.url.substring(packageJson.repository.url.indexOf('://') + 3) : '';
107
+ const projectName = packageJson.name || 'current';
108
+
109
+ // Use the new projects format for auto-generation
110
+ templateConfig.projects = {
111
+ [projectName]: {
112
+ path: currentRepoPath,
113
+ repo: repoUrl,
114
+ exclude: ["git"]
115
+ }
116
+ };
117
+
118
+ // Set initial organization_id and key from environment or placeholders
119
+ templateConfig.organization_id = process.env.organization_id || "YOUR_ORGANIZATION_ID";
120
+ templateConfig.key = process.env.key || "YOUR_API_KEY";
121
+
122
+ // Resolve host domain if available from environment variable HOST_DOMAIN
123
+ if (process.env.host_domain) {
124
+ templateConfig.host = templateConfig.host || { $branch: {} };
125
+ templateConfig.host.$branch = templateConfig.host.$branch || {};
126
+ templateConfig.host.$branch.master = `wss://${process.env.host_domain}`;
127
+ templateConfig.host.$branch.dev = `wss://dev.${process.env.host_domain}`;
128
+ templateConfig.host.$branch.test = `wss://test.${process.env.host_domain}`;
129
+ }
130
+
131
+ const newConfigPath = path.join(directory, 'CoCreate.config.js');
132
+ const configContent = `module.exports = ${JSON.stringify(templateConfig, null, 2)};`;
133
+
134
+ await fsp.writeFile(newConfigPath, configContent);
135
+
136
+ console.log(`${color.green}Generated CoCreate.config.js at ${newConfigPath}${color.reset}`);
137
+
138
+ // Use the newly generated template config
139
+ config = templateConfig;
140
+ config.configPath = newConfigPath; // Ensure configPath is set to the new file
141
+ }
142
+ }
143
+ }
144
+
145
+ directory = path.dirname(config.configPath);
146
+ console.warn(
147
+ `${color.yellow}using config ${config.configPath} ${color.reset}`
148
+ );
149
+
150
+ config.hideMessage = false;
151
+
152
+ // Add metadata to traditional repositories
153
+ if (config.repositories && config.repositories.length) {
154
+ config.repositories = await addMeta(config.repositories, [], directory);
155
+ }
156
+
157
+ // Add metadata to modules
158
+ if (config.modules) {
159
+ const moduleRepos = Object.values(config.modules).filter(r => typeof r === 'object' && r !== null && r.path);
160
+ if (moduleRepos.length) {
161
+ await addMeta(moduleRepos, [], directory);
162
+ }
163
+ }
164
+
165
+ // Add metadata to projects
166
+ if (config.projects) {
167
+ const projectRepos = Object.values(config.projects).filter(r => typeof r === 'object' && r !== null && r.path);
168
+ if (projectRepos.length) {
169
+ await addMeta(projectRepos, [], directory);
170
+ }
171
+ }
172
+
173
+ // Execute the command across targeted repositories/modules/projects
174
+ const failed = await execute(command, config);
175
+
176
+ // Handle any failed command executions
177
+ if (failed && failed.length > 0) {
178
+ console.log(
179
+ color.red +
180
+ "\n **************** failures **************** " +
181
+ color.reset
182
+ );
183
+ for (const failure of failed) {
184
+ console.log(
185
+ color.red + `${failure.name || failure.path}: ${failure.error}` + color.reset
186
+ );
187
+ }
188
+
189
+ // Prompt user to retry failed commands
190
+ await promptRetry(failed, config, options);
191
+ }
126
192
  }
127
193
 
128
194
  /**
129
195
  * Prompt the user to retry failed commands.
130
196
  * @param {Array} failed - List of failed commands.
131
197
  * @param {Object} config - Configuration object.
132
- * @param {string} directory - Path of the configuration directory.
198
+ * @param {Object} options - CLI options.
133
199
  */
134
200
  async function promptRetry(failed, config, options) {
135
- const rl = readline.createInterface({
136
- input: process.stdin,
137
- output: process.stdout
138
- });
139
-
140
- rl.question(
141
- "Do you want to retry the failed commands? (yes/no): ",
142
- async (answer) => {
143
- rl.close();
144
- if (answer.toLowerCase() === "yes") {
145
- config.repositories = failed;
146
- await main(config, options);
147
- } else {
148
- process.exit(0);
149
- }
150
- }
151
- );
201
+ const rl = readline.createInterface({
202
+ input: process.stdin,
203
+ output: process.stdout
204
+ });
205
+
206
+ rl.question(
207
+ "Do you want to retry the failed commands? (yes/no): ",
208
+ async (answer) => {
209
+ rl.close();
210
+ if (answer.toLowerCase() === "yes" || answer.toLowerCase() === "y") {
211
+ // Ensure we only retry the failed ones by clearing the existing modules/projects
212
+ config.repositories = failed;
213
+ delete config.modules;
214
+ delete config.projects;
215
+ await main(config, options);
216
+ } else {
217
+ process.exit(0);
218
+ }
219
+ }
220
+ );
152
221
  }
153
222
 
154
223
  // Call the main function with initial configuration
155
- main(config, options);
224
+ main(config, options);
@@ -1,32 +1,81 @@
1
1
  const spawn = require("../spawn");
2
2
  const path = require("path");
3
- let fs = require("fs");
3
+ const fs = require("fs");
4
4
 
5
+ /**
6
+ * CLI-compatible clone command that resolves paths dynamically,
7
+ * clones missing repositories, and silently skips already cloned ones.
8
+ */
5
9
  module.exports = async function gitClone(repos, args) {
6
- const failed = [];
7
- const cwdPath = path.resolve(process.cwd());
8
-
9
- for (let i = 0; i < repos.length; i++) {
10
- // TODO: Check if path exist and if git.config or package.json exist continue
11
- if (cwdPath !== repos[i].absolutePath) {
12
- if (!fs.existsSync(repos[i].directory))
13
- fs.mkdirSync(repos[i].directory);
14
-
15
- if (
16
- !repos[i].repo.startsWith("http://") &&
17
- !repos[i].repo.startsWith("https://")
18
- )
19
- repos[i].repo = "https://" + repos[i].repo;
20
-
21
- let exitCode = await spawn("git", ["clone", `${repos[i].repo}`], {
22
- stdio: "inherit",
23
- cwd: repos[i].directory
24
- });
25
- if (exitCode !== 0) {
26
- failed.push({ name: repos[i].name, error: `cloning failed` });
27
- }
28
- }
29
- }
30
-
31
- return failed;
32
- };
10
+ const failed = [];
11
+ const cwdPath = path.resolve(process.cwd());
12
+
13
+ for (let i = 0; i < repos.length; i++) {
14
+ const repo = repos[i];
15
+ if (!repo) continue;
16
+
17
+ const rawPath = repo.path || repo.absolutePath || repo.directory;
18
+ if (!rawPath) {
19
+ failed.push({
20
+ name: repo.name || `Repo #${i}`,
21
+ error: "No path property defined in the configuration"
22
+ });
23
+ continue;
24
+ }
25
+
26
+ repo.absolutePath = repo.absolutePath || path.resolve(cwdPath, rawPath);
27
+ repo.directory = repo.directory || path.dirname(repo.absolutePath);
28
+ repo.name = repo.name || path.basename(repo.absolutePath);
29
+
30
+ if (cwdPath === repo.absolutePath) {
31
+ continue;
32
+ }
33
+
34
+ const relativeRepoPath = path.relative(cwdPath, repo.absolutePath);
35
+
36
+ // Clean skip logging using path
37
+ if (fs.existsSync(repo.absolutePath)) {
38
+ console.log(`Already exists: ${relativeRepoPath} (Skipped)`);
39
+ continue;
40
+ }
41
+
42
+ if (!fs.existsSync(repo.directory)) {
43
+ try {
44
+ fs.mkdirSync(repo.directory, { recursive: true });
45
+ } catch (err) {
46
+ failed.push({
47
+ name: repo.name,
48
+ error: `Failed to create directory ${repo.directory}: ${err.message}`
49
+ });
50
+ continue;
51
+ }
52
+ }
53
+
54
+ if (!repo.repo) {
55
+ failed.push({
56
+ name: repo.name,
57
+ error: "No git repository URL provided in config"
58
+ });
59
+ continue;
60
+ }
61
+
62
+ let gitUrl = repo.repo;
63
+ if (!gitUrl.startsWith("http://") && !gitUrl.startsWith("https://")) {
64
+ gitUrl = "https://" + gitUrl;
65
+ }
66
+
67
+ // Clean success logging after process completes successfully
68
+ let exitCode = await spawn("git", ["clone", gitUrl], {
69
+ stdio: "inherit",
70
+ cwd: repo.directory
71
+ });
72
+
73
+ if (exitCode !== 0) {
74
+ failed.push({ name: repo.name, error: `cloning failed` });
75
+ } else {
76
+ console.log(`Successfully cloned: ${relativeRepoPath}`);
77
+ }
78
+ }
79
+
80
+ return failed;
81
+ };
@@ -0,0 +1,70 @@
1
+ const file = require("@cocreate/file");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const { getConfig } = require("../getConfig");
5
+
6
+ module.exports = async function del(directory, args) {
7
+ if (args && !Array.isArray(args)) args = [args];
8
+
9
+ directory = process.cwd();
10
+
11
+ // Process targets
12
+ if (!args || !args.length) {
13
+ const CoCreateConfig = await getConfig(directory);
14
+ if (CoCreateConfig && CoCreateConfig.configPath) {
15
+ const projectRoot = path.dirname(CoCreateConfig.configPath);
16
+ let rel = path.relative(projectRoot, process.cwd()).replace(/\\/g, '/');
17
+
18
+ // In the DB, 'src' is the root, so we must remove 'src' from the relative path
19
+ if (rel === 'src') {
20
+ rel = '';
21
+ } else if (rel.startsWith('src/')) {
22
+ rel = rel.substring(4);
23
+ } else if (rel === 'src') {
24
+ rel = '';
25
+ }
26
+
27
+ let target = '/' + rel.replace(/^\/+/, '');
28
+
29
+ await confirmAndDelete(CoCreateConfig, target);
30
+ } else {
31
+ console.log("Failed to read or parse CoCreate.config.js.");
32
+ }
33
+ } else {
34
+ // When explicit args are given, resolve config but do NOT change CWD
35
+ const CoCreateConfig = await getConfig(directory);
36
+ if (!CoCreateConfig || !CoCreateConfig.configPath) {
37
+ console.log("Failed to read or parse CoCreate.config.js for authentication.");
38
+ return;
39
+ }
40
+
41
+ for (let arg of args) {
42
+ let targetPath = String(arg).replace(/\\/g, '/');
43
+
44
+ // Normalize to DB path format
45
+ if (!targetPath.startsWith('/')) {
46
+ targetPath = '/' + targetPath.replace(/^\/+/,'');
47
+ }
48
+
49
+ await confirmAndDelete(CoCreateConfig, targetPath);
50
+ }
51
+ }
52
+
53
+ async function confirmAndDelete(CoCreateConfig, target) {
54
+ try {
55
+ if (typeof file.delete !== 'function') {
56
+ console.error('Delete not supported: @cocreate/file exposes no delete API.');
57
+ return;
58
+ }
59
+
60
+ const res = await file.delete(
61
+ CoCreateConfig,
62
+ CoCreateConfig.configPath,
63
+ target
64
+ );
65
+ console.log('Deleted:', (res && res.length) ? res.length : res, 'items for', target);
66
+ } catch (err) {
67
+ console.error('Delete failed for', target, err);
68
+ }
69
+ }
70
+ };
@@ -0,0 +1,72 @@
1
+ const file = require("@cocreate/file");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const { getConfig } = require("../getConfig");
5
+
6
+ module.exports = async function download(directory, args) {
7
+ if (args && !Array.isArray(args)) args = [args];
8
+
9
+ directory = process.cwd();
10
+
11
+ // Process targets
12
+ if (!args || !args.length) {
13
+ const CoCreateConfig = await getConfig(directory);
14
+ if (CoCreateConfig && CoCreateConfig.configPath) {
15
+ const projectRoot = path.dirname(CoCreateConfig.configPath);
16
+ let rel = path.relative(projectRoot, process.cwd()).replace(/\\/g, '/');
17
+
18
+ // In the DB, 'src' is the root, so we must remove 'src' from the relative path
19
+ if (rel === 'src') {
20
+ rel = '';
21
+ } else if (rel.startsWith('src/')) {
22
+ rel = rel.substring(4);
23
+ } else if (rel === 'src') {
24
+ rel = '';
25
+ }
26
+
27
+ let target = '/' + rel.replace(/^\/+/, '');
28
+
29
+ await fetchAndWrite(CoCreateConfig, target);
30
+ } else {
31
+ console.log("Failed to read or parse CoCreate.config.js.");
32
+ }
33
+ } else {
34
+ // When an explicit argument is given, resolve config but do NOT change CWD
35
+ // so that the files are downloaded into the folder output was initiated from.
36
+ const CoCreateConfig = await getConfig(directory);
37
+ if (!CoCreateConfig || !CoCreateConfig.configPath) {
38
+ console.log("Failed to read or parse CoCreate.config.js for authentication.");
39
+ return;
40
+ }
41
+
42
+ for (let arg of args) {
43
+ let targetPath = String(arg).replace(/\\/g, '/');
44
+
45
+ // Just use the requested string directly as the DB path
46
+ if (!targetPath.startsWith('/')) {
47
+ targetPath = '/' + targetPath.replace(/^\/+/, '');
48
+ }
49
+
50
+ await fetchAndWrite(CoCreateConfig, targetPath);
51
+ }
52
+ }
53
+
54
+ async function fetchAndWrite(CoCreateConfig, target) {
55
+ try {
56
+ if (typeof file.download !== 'function') {
57
+ console.error('Download not supported: @cocreate/file exposes no download API.');
58
+ return;
59
+ }
60
+
61
+ const downloaded = await file.download(
62
+ CoCreateConfig,
63
+ CoCreateConfig.configPath,
64
+ target
65
+ );
66
+ console.log('Downloaded:', downloaded && downloaded.length ? downloaded.length : 0, 'files for', target);
67
+ } catch (err) {
68
+ console.error('Download failed for', target, err);
69
+ }
70
+ }
71
+ };
72
+
@@ -0,0 +1,57 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+
4
+ /**
5
+ * Updates .gitignore files across multiple repositories with descriptive path logging.
6
+ */
7
+ module.exports = async function updateGitignore(repos, args) {
8
+ const failed = [];
9
+ const cwd = process.cwd();
10
+ const templatePath = path.join(cwd, ".gitignore");
11
+
12
+ if (!fs.existsSync(templatePath)) {
13
+ throw new Error(`No .gitignore template found in: ${cwd}`);
14
+ }
15
+
16
+ let templateContent;
17
+ try {
18
+ templateContent = fs.readFileSync(templatePath, "utf8");
19
+ } catch (error) {
20
+ throw new Error(`Failed to read the template file: ${error.message}`);
21
+ }
22
+
23
+ for (let i = 0; i < repos.length; i++) {
24
+ const repo = repos[i];
25
+ if (!repo) continue;
26
+
27
+ try {
28
+ const rawPath = repo.absolutePath || repo.directory || repo.path;
29
+ if (!rawPath) continue;
30
+
31
+ const resolvedPath = path.resolve(cwd, rawPath);
32
+
33
+ if (!fs.existsSync(resolvedPath)) continue;
34
+ if (path.resolve(resolvedPath) === path.resolve(cwd)) continue;
35
+
36
+ const gitignorePath = path.join(resolvedPath, ".gitignore");
37
+
38
+ if (fs.existsSync(gitignorePath)) {
39
+ fs.unlinkSync(gitignorePath);
40
+ }
41
+
42
+ fs.writeFileSync(gitignorePath, templateContent, "utf8");
43
+
44
+ // Log the clean relative path from where the command was executed
45
+ const relativeLogPath = path.relative(cwd, gitignorePath);
46
+ console.log(`Successfully updated: ${relativeLogPath}`);
47
+
48
+ } catch (error) {
49
+ failed.push({
50
+ name: repo.name || `Repo #${i}`,
51
+ error: error.message
52
+ });
53
+ }
54
+ }
55
+
56
+ return failed;
57
+ };
@@ -4,15 +4,12 @@ module.exports = async function (repos, args) {
4
4
  let failed = [];
5
5
  try {
6
6
  let cloneFailed = await require("./clone.js")(repos, args);
7
- if (cloneFailed) failed.push(cloneFailed);
7
+ if (cloneFailed && cloneFailed.length) failed.push(...cloneFailed);
8
8
 
9
9
  repos = await addMeta(repos, failed);
10
10
 
11
- let symlinkFailed = await require("./symlink.js")(repos, args);
12
- if (symlinkFailed) failed.push(symlinkFailed);
13
-
14
11
  let linkFailed = await require("./link.js")(repos, args);
15
- if (linkFailed) failed.push(linkFailed);
12
+ if (linkFailed && linkFailed.length) failed.push(...linkFailed);
16
13
  } catch (err) {
17
14
  console.error(err);
18
15
  failed.push({ name: "general", error: err.message });