@cocreate/cli 1.54.1 → 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/execute.js CHANGED
@@ -8,73 +8,116 @@ const { getConfig } = require("./getConfig");
8
8
  const { color } = require("./fonts");
9
9
 
10
10
  module.exports = async function execute(command, config) {
11
- let failed = [];
12
- let [filename, ...args] = command.replaceAll("'", '"').trim().split(" ");
11
+ let failed = [];
12
+ let [filename, ...args] = command.replaceAll("'", '"').trim().split(" ");
13
13
 
14
- let type;
14
+ let type;
15
15
 
16
- if (filename.endsWith(".js")) {
17
- type = filename.slice(0, -3);
18
- } else {
19
- type = filename;
20
- filename += ".js";
21
- }
16
+ if (filename.endsWith(".js")) {
17
+ type = filename.slice(0, -3);
18
+ } else {
19
+ type = filename;
20
+ filename += ".js";
21
+ }
22
22
 
23
- let predefined = path.resolve(__dirname, "commands", filename);
24
- let isPredefined = fs.existsSync(predefined);
25
- let repositories = [];
23
+ let predefined = path.resolve(__dirname, "commands", filename);
24
+ let isPredefined = fs.existsSync(predefined);
25
+ let repositories = [];
26
26
 
27
- for (let repo of config.repositories || []) {
28
- try {
29
- if (
30
- repo.exclude &&
31
- (repo.exclude.includes(type) || repo.exclude.includes(filename))
32
- ) {
33
- continue;
34
- }
27
+ // --- CONSOLIDATE REPOSITORIES ---
28
+ // Merge both modules and projects from the new configuration structure
29
+ let targetRepos = [];
30
+
31
+ if (config.modules) {
32
+ Object.entries(config.modules).forEach(([name, repo]) => {
33
+ // Ensure it's a valid object and has a path
34
+ if (typeof repo === 'object' && repo !== null && repo.path) {
35
+ targetRepos.push({ name, ...repo });
36
+ }
37
+ });
38
+ }
39
+
40
+ if (config.projects) {
41
+ Object.entries(config.projects).forEach(([name, repo]) => {
42
+ if (typeof repo === 'object' && repo !== null && repo.path) {
43
+ targetRepos.push({ name, ...repo });
44
+ }
45
+ });
46
+ }
35
47
 
36
- const packageJsonPath = path.resolve(repo.path, "package.json");
37
- const packageObj = require(packageJsonPath);
38
- repo.entry = packageObj.main;
48
+ // Backward compatibility for transitioning configs
49
+ if (config.repositories && Array.isArray(config.repositories)) {
50
+ targetRepos = targetRepos.concat(config.repositories);
51
+ }
39
52
 
40
- if (isPredefined) {
41
- repositories.push(repo);
42
- continue;
43
- }
53
+ for (let repo of targetRepos) {
54
+ try {
55
+ if (
56
+ repo.exclude &&
57
+ (repo.exclude.includes(type) || repo.exclude.includes(filename))
58
+ ) {
59
+ continue;
60
+ }
44
61
 
45
- console.log(color.green + `${repo.name}: ` + color.reset, command);
46
- let exitCode;
47
- if (config.hideMessage) {
48
- const { error } = await exec(command, {
49
- cwd: repo.absolutePath
50
- });
62
+ const packageJsonPath = path.resolve(repo.path, "package.json");
63
+
64
+ // Try to load package.json if it exists
65
+ if (fs.existsSync(packageJsonPath)) {
66
+ try {
67
+ const packageObj = require(packageJsonPath);
68
+ repo.entry = packageObj.main;
69
+ } catch (err) {
70
+ // Log error but continue
71
+ console.error("error reading packageObj:", err.message);
72
+ }
73
+ }
51
74
 
52
- if (error) exitCode = 1;
53
- } else {
54
- exitCode = await spawn(type, args, {
55
- cwd: repo.absolutePath,
56
- shell: true,
57
- stdio: "inherit"
58
- });
59
- }
75
+ if (isPredefined) {
76
+ repositories.push(repo);
77
+ continue;
78
+ }
60
79
 
61
- if (exitCode !== 0) {
62
- repo.error = "command failed: " + command;
63
- failed.push(repo);
64
- }
65
- } catch (err) {
66
- console.error(
67
- color.red +
68
- `an error occured executing command in ${repo.name} repository` +
69
- color.reset,
70
- err.message
71
- );
72
- }
73
- }
80
+ // For non-predefined commands, skip if package.json doesn't exist
81
+ if (!fs.existsSync(packageJsonPath)) {
82
+ continue;
83
+ }
74
84
 
75
- if (isPredefined) {
76
- failed = await require(predefined)(repositories, args);
77
- }
85
+ console.log(color.green + `${repo.name || repo.path}: ` + color.reset, command);
86
+ let exitCode = 0;
87
+ if (config.hideMessage) {
88
+ try {
89
+ await exec(command, {
90
+ cwd: repo.absolutePath || path.resolve(repo.path)
91
+ });
92
+ } catch (error) {
93
+ exitCode = 1;
94
+ }
95
+ } else {
96
+ // Use spawn with shell to stream output in real-time
97
+ exitCode = await spawn(type, args, {
98
+ cwd: repo.absolutePath || path.resolve(repo.path),
99
+ shell: true,
100
+ stdio: "inherit"
101
+ });
102
+ }
78
103
 
79
- return failed;
80
- };
104
+ if (exitCode !== 0) {
105
+ repo.error = "command failed: " + command;
106
+ failed.push(repo);
107
+ }
108
+ } catch (err) {
109
+ console.error(
110
+ color.red +
111
+ `an error occured executing command in ${repo.name || repo.path} repository` +
112
+ color.reset,
113
+ err.message
114
+ );
115
+ }
116
+ }
117
+
118
+ if (isPredefined) {
119
+ failed = await require(predefined)(repositories, args);
120
+ }
121
+
122
+ return failed;
123
+ };
package/src/getConfig.js CHANGED
@@ -1,32 +1,92 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const { pathToFileURL } = require("url");
3
4
 
5
+ /**
6
+ * Dynamically loads and returns the configuration file.
7
+ * Handles:
8
+ * - CommonJS (module.exports = { ... })
9
+ * - ESM Default Export (export default { ... })
10
+ * - ESM Named Export Object (export const config = { ... })
11
+ * - ESM Individual Named Exports (export const repositories = [...])
12
+ *
13
+ * @param {string} directory - The starting directory to search from.
14
+ * @param {string} filename - The target filename to append to the config filepath.
15
+ * @returns {Promise<Object|null>} The parsed config object, or null if not found.
16
+ */
4
17
  async function getConfig(directory, filename = "") {
5
- let configPath = findClosestConfig(directory, "CoCreate.config.js");
6
- if (configPath) {
7
- let config = require(configPath);
8
- config.configPath = configPath;
9
- config.filePath = path.resolve(directory, filename);
10
- return config;
11
- } else {
12
- console.log("No CoCreate.config file found in parent directories.");
13
- }
18
+ let configPath = findClosestConfig(directory, "CoCreate.config.js");
19
+ if (configPath) {
20
+ try {
21
+ // Convert path to file:// URL to make dynamic imports bulletproof across OS environments (especially Windows)
22
+ const configUrl = pathToFileURL(configPath).href;
23
+
24
+ // Dynamically import the module
25
+ const modulePayload = await import(configUrl);
26
+
27
+ let rawConfig;
28
+
29
+ if (modulePayload.default !== undefined) {
30
+ // 1. Handles 'export default { ... }' or 'module.exports = { ... }'
31
+ rawConfig = modulePayload.default;
32
+ } else if (modulePayload.config !== undefined && typeof modulePayload.config === 'object') {
33
+ // 2. Handles 'export const config = { ... }' or 'exports.config = { ... }'
34
+ rawConfig = modulePayload.config;
35
+ } else {
36
+ // 3. Handles individual exports like 'export const repositories = [...]'
37
+ rawConfig = modulePayload;
38
+ }
39
+
40
+ // Dynamic imports return read-only namespace objects;
41
+ // shallow cloning allows us to safely append configPath and filePath.
42
+ let config = {};
43
+ if (rawConfig && typeof rawConfig === 'object') {
44
+ config = { ...rawConfig };
45
+ } else {
46
+ config = rawConfig;
47
+ }
48
+
49
+ config.configPath = configPath;
50
+ config.filePath = path.resolve(directory, filename);
51
+ return config;
52
+ } catch (error) {
53
+ console.error(`Error loading configuration at ${configPath}:`, error);
54
+ throw error;
55
+ }
56
+ } else {
57
+ console.log("No CoCreate.config file found in parent directories.");
58
+ return null;
59
+ }
14
60
  }
15
61
 
62
+ /**
63
+ * Climbs up the directory tree to find the closest target configuration file.
64
+ * Safely handles file system root termination to avoid infinite loops.
65
+ *
66
+ * @param {string} directory - Starting directory path.
67
+ * @param {string} filename - Target file to locate.
68
+ * @returns {string|null} Resolved absolute path to the file, or null if not found.
69
+ */
16
70
  function findClosestConfig(directory, filename) {
17
- let currentDir = directory;
71
+ let currentDir = path.resolve(directory);
18
72
 
19
- while (currentDir !== "/" && currentDir !== ".") {
20
- let configFile = path.join(currentDir, filename);
73
+ while (true) {
74
+ let configFile = path.join(currentDir, filename);
21
75
 
22
- if (fs.existsSync(configFile)) {
23
- return configFile;
24
- }
76
+ if (fs.existsSync(configFile)) {
77
+ return configFile;
78
+ }
25
79
 
26
- currentDir = path.dirname(currentDir);
27
- }
80
+ let parentDir = path.dirname(currentDir);
81
+
82
+ // Safety Break: Stop climbing if we've reached the system root (e.g. '/' on Linux/Mac, 'C:\' on Windows)
83
+ if (parentDir === currentDir) {
84
+ break;
85
+ }
86
+ currentDir = parentDir;
87
+ }
28
88
 
29
- return null;
89
+ return null;
30
90
  }
31
91
 
32
- module.exports = { getConfig, findClosestConfig };
92
+ module.exports = { getConfig, findClosestConfig };
package/src/getOS.js ADDED
@@ -0,0 +1,22 @@
1
+ const os = require("os");
2
+
3
+ /**
4
+ * Detects the active operating system and provides helpful boolean flags
5
+ * and platform metadata.
6
+ *
7
+ * @returns {{ platform: string, isWindows: boolean, isMac: boolean, isLinux: boolean, type: string, release: string }}
8
+ */
9
+ function getOS() {
10
+ const platform = process.platform;
11
+
12
+ return {
13
+ platform,
14
+ isWindows: platform === "win32",
15
+ isMac: platform === "darwin",
16
+ isLinux: platform === "linux",
17
+ type: os.type(), // 'Windows_NT', 'Darwin', 'Linux'
18
+ release: os.release() // OS release version string
19
+ };
20
+ }
21
+
22
+ module.exports = getOS;
@@ -0,0 +1,46 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Detects the active package manager based strictly on workspace lockfiles,
6
+ * distinguishing between Yarn Classic and Yarn Berry by analyzing yarn.lock directly.
7
+ *
8
+ * @returns {"bun"|"pnpm"|"yarn-berry"|"yarn-classic"|"npm"}
9
+ */
10
+ function getPackageManager() {
11
+ const cwd = process.cwd();
12
+
13
+ // Bun lockfile checks
14
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) {
15
+ return "bun";
16
+ }
17
+
18
+ // pnpm lockfile check
19
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
20
+ return "pnpm";
21
+ }
22
+
23
+ // Yarn lockfile check
24
+ const yarnLockPath = path.join(cwd, "yarn.lock");
25
+ if (fs.existsSync(yarnLockPath)) {
26
+ try {
27
+ const content = fs.readFileSync(yarnLockPath, "utf8");
28
+ // Yarn Berry lockfiles contain a "__metadata:" block at the top
29
+ if (content.includes("__metadata:")) {
30
+ return "yarn-berry";
31
+ }
32
+ } catch (e) {
33
+ // Safe fallback to classic if the file cannot be read
34
+ }
35
+ return "yarn-classic";
36
+ }
37
+
38
+ // npm lockfile check
39
+ if (fs.existsSync(path.join(cwd, "package-lock.json"))) {
40
+ return "npm";
41
+ }
42
+
43
+ return "npm"; // Default fallback
44
+ }
45
+
46
+ module.exports = getPackageManager;
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const readline = require("readline");
6
+ const util = require("node:util");
7
+ const exec = util.promisify(require("node:child_process").exec);
8
+
9
+ const getPackageManager = require("../getPackageManager");
10
+ const getOS = require("../getOS");
11
+
12
+ /**
13
+ * Prompts user for yes/no input
14
+ */
15
+ function promptUser(question) {
16
+ return new Promise((resolve) => {
17
+ const rl = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout
20
+ });
21
+
22
+ rl.question(question, (answer) => {
23
+ rl.close();
24
+ resolve(answer.toLowerCase() === "yes" || answer.toLowerCase() === "y");
25
+ });
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Attempts to link the CLI globally
31
+ */
32
+ async function linkGlobally() {
33
+ try {
34
+ // Resolve target OS properties and correct package manager binary name
35
+ const { isWindows, platform } = getOS();
36
+ const activePackageManager = getPackageManager();
37
+
38
+ // Standardize binary lookup name (fallback yarn-berry / yarn-classic to yarn executable)
39
+ const packageManager = (activePackageManager === "yarn-classic" || activePackageManager === "yarn-berry")
40
+ ? "yarn"
41
+ : activePackageManager;
42
+
43
+ const linkCommand = `${packageManager} link`;
44
+ const cliPath = path.resolve(__dirname, "..");
45
+
46
+ // First, ask user if they want to install globally
47
+ const wantGlobal = await promptUser(
48
+ `[CoCreate CLI] Would you like to install @cocreate/cli globally? (yes/no): `
49
+ );
50
+
51
+ if (!wantGlobal) {
52
+ console.log(
53
+ `[CoCreate CLI] Skipped global installation. You can install later by running: ${linkCommand}`
54
+ );
55
+ return;
56
+ }
57
+
58
+ console.log(`[CoCreate CLI] Attempting global link using ${packageManager}...`);
59
+
60
+ try {
61
+ await exec(linkCommand, {
62
+ cwd: cliPath
63
+ });
64
+ console.log(`[CoCreate CLI] Successfully linked globally via ${packageManager}`);
65
+ } catch (error) {
66
+ // Check if it's a permission error
67
+ if (error.message && error.message.includes("EACCES")) {
68
+ const useSudo = platform === "linux" || platform === "darwin";
69
+
70
+ if (useSudo) {
71
+ // On Linux/macOS, offer to try with sudo
72
+ const answer = await promptUser(
73
+ `[CoCreate CLI] Permission denied. Try with sudo? (yes/no): `
74
+ );
75
+
76
+ if (answer) {
77
+ try {
78
+ const sudoCommand = `sudo ${linkCommand}`;
79
+ console.log(
80
+ `[CoCreate CLI] Attempting with sudo...`
81
+ );
82
+ await exec(sudoCommand, {
83
+ cwd: cliPath,
84
+ stdio: "inherit"
85
+ });
86
+ console.log(
87
+ `[CoCreate CLI] Successfully linked globally with sudo`
88
+ );
89
+ } catch (sudoError) {
90
+ console.warn(
91
+ `[CoCreate CLI] Failed with sudo. Run this manually:`
92
+ );
93
+ console.warn(` sudo ${linkCommand}`);
94
+ }
95
+ } else {
96
+ console.warn(
97
+ `[CoCreate CLI] Skipped. To link globally later, run:`
98
+ );
99
+ console.warn(` ${linkCommand}`);
100
+ }
101
+ } else if (isWindows) {
102
+ // Windows
103
+ console.warn(
104
+ `[CoCreate CLI] Permission denied. Please run Command Prompt as Administrator and retry npm install`
105
+ );
106
+ } else {
107
+ // Other platforms
108
+ console.warn(
109
+ `[CoCreate CLI] Permission denied. You may need elevated privileges to link globally.`
110
+ );
111
+ console.warn(` Run: ${linkCommand}`);
112
+ }
113
+ } else {
114
+ // Other errors
115
+ console.warn(
116
+ `[CoCreate CLI] Failed to auto-link globally. Run this manually:`
117
+ );
118
+ console.warn(` ${linkCommand}`);
119
+ }
120
+ }
121
+ } catch (error) {
122
+ console.warn("[CoCreate CLI] Postinstall linking encountered an issue.");
123
+ }
124
+ }
125
+
126
+ // Run the linking process
127
+ linkGlobally().catch(() => {
128
+ // Silently fail - don't break npm install
129
+ });
@@ -1,32 +0,0 @@
1
- let glob = require("glob");
2
- let fs = require("fs");
3
- const path = require("path");
4
-
5
- function globUpdater(er, files) {
6
- if (er) console.log(files, "glob resolving issue");
7
- else files.forEach((filename) => update(filename));
8
- }
9
-
10
- function update(Path) {
11
- let fileContent = `# ignore
12
- node_modules
13
- dist
14
- .npmrc
15
-
16
- `;
17
- if (fs.existsSync(Path)) fs.unlinkSync(Path);
18
- fs.writeFileSync(Path, fileContent);
19
- }
20
-
21
- // glob("../CoCreate-modules/CoCreate-action/.gitignore", globUpdater)
22
- // glob("./.gitignore", globUpdater)
23
- // glob("../CoCreate-adminUI/.gitignore", globUpdater)
24
- glob("../CoCreate-modules/*/.gitignore", globUpdater);
25
- glob("../CoCreate-apps/*/.gitignore", globUpdater);
26
- glob("../CoCreate-plugins/*/.gitignore", globUpdater);
27
- // glob("../CoCreate-website/.gitignore", globUpdater)
28
- // glob("../CoCreate-website-template/.gitignore", globUpdater)
29
- glob("../CoCreateCSS/.gitignore", globUpdater);
30
- // glob("../CoCreateJS/.gitignore", globUpdater)
31
-
32
- console.log("finished");