@expressots/cli 3.0.0 → 4.0.0-preview.2

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.
Files changed (180) hide show
  1. package/README.md +41 -95
  2. package/bin/cicd/cli.d.ts +6 -0
  3. package/bin/cicd/cli.js +126 -0
  4. package/bin/cicd/form.d.ts +29 -0
  5. package/bin/cicd/form.js +345 -0
  6. package/bin/cicd/generators/azure-devops.d.ts +2 -0
  7. package/bin/cicd/generators/azure-devops.js +370 -0
  8. package/bin/cicd/generators/bitbucket.d.ts +2 -0
  9. package/bin/cicd/generators/bitbucket.js +217 -0
  10. package/bin/cicd/generators/circleci.d.ts +2 -0
  11. package/bin/cicd/generators/circleci.js +274 -0
  12. package/bin/cicd/generators/github-actions.d.ts +14 -0
  13. package/bin/cicd/generators/github-actions.js +426 -0
  14. package/bin/cicd/generators/gitlab-ci.d.ts +2 -0
  15. package/bin/cicd/generators/gitlab-ci.js +237 -0
  16. package/bin/cicd/generators/index.d.ts +6 -0
  17. package/bin/cicd/generators/index.js +15 -0
  18. package/bin/cicd/generators/jenkins.d.ts +2 -0
  19. package/bin/cicd/generators/jenkins.js +248 -0
  20. package/bin/cicd/generators/template-loader.d.ts +17 -0
  21. package/bin/cicd/generators/template-loader.js +128 -0
  22. package/bin/cicd/index.d.ts +1 -0
  23. package/bin/cicd/index.js +5 -0
  24. package/bin/cli.d.ts +1 -1
  25. package/bin/cli.js +18 -3
  26. package/bin/commands/project.commands.d.ts +19 -6
  27. package/bin/commands/project.commands.js +390 -61
  28. package/bin/config/index.d.ts +5 -0
  29. package/bin/config/index.js +10 -0
  30. package/bin/config/manager.d.ts +98 -0
  31. package/bin/config/manager.js +222 -0
  32. package/bin/containerize/analyzers/bootstrap-analyzer.d.ts +46 -0
  33. package/bin/containerize/analyzers/bootstrap-analyzer.js +187 -0
  34. package/bin/containerize/analyzers/project-analyzer.d.ts +20 -0
  35. package/bin/containerize/analyzers/project-analyzer.js +150 -0
  36. package/bin/containerize/cli.d.ts +4 -0
  37. package/bin/containerize/cli.js +113 -0
  38. package/bin/containerize/form.d.ts +15 -0
  39. package/bin/containerize/form.js +154 -0
  40. package/bin/containerize/generators/ci-generator.d.ts +31 -0
  41. package/bin/containerize/generators/ci-generator.js +936 -0
  42. package/bin/containerize/generators/docker-compose-generator.d.ts +8 -0
  43. package/bin/containerize/generators/docker-compose-generator.js +186 -0
  44. package/bin/containerize/generators/dockerfile-generator.d.ts +8 -0
  45. package/bin/containerize/generators/dockerfile-generator.js +635 -0
  46. package/bin/containerize/generators/kubernetes-generator.d.ts +8 -0
  47. package/bin/containerize/generators/kubernetes-generator.js +133 -0
  48. package/bin/containerize/generators/template-loader.d.ts +36 -0
  49. package/bin/containerize/generators/template-loader.js +129 -0
  50. package/bin/containerize/index.d.ts +4 -0
  51. package/bin/containerize/index.js +13 -0
  52. package/bin/containerize/presets/preset-registry.d.ts +20 -0
  53. package/bin/containerize/presets/preset-registry.js +102 -0
  54. package/bin/costs/cli.d.ts +5 -0
  55. package/bin/costs/cli.js +183 -0
  56. package/bin/costs/form.d.ts +44 -0
  57. package/bin/costs/form.js +412 -0
  58. package/bin/costs/index.d.ts +4 -0
  59. package/bin/costs/index.js +25 -0
  60. package/bin/costs/pricing-manager.d.ts +84 -0
  61. package/bin/costs/pricing-manager.js +342 -0
  62. package/bin/costs/providers/index.d.ts +32 -0
  63. package/bin/costs/providers/index.js +153 -0
  64. package/bin/costs/sources/api-source.d.ts +10 -0
  65. package/bin/costs/sources/api-source.js +32 -0
  66. package/bin/costs/sources/index.d.ts +6 -0
  67. package/bin/costs/sources/index.js +15 -0
  68. package/bin/costs/sources/local-json-source.d.ts +23 -0
  69. package/bin/costs/sources/local-json-source.js +59 -0
  70. package/bin/costs/sources/remote-json-source.d.ts +11 -0
  71. package/bin/costs/sources/remote-json-source.js +53 -0
  72. package/bin/costs/types.d.ts +53 -0
  73. package/bin/costs/types.js +5 -0
  74. package/bin/dev/cli.d.ts +4 -0
  75. package/bin/dev/cli.js +134 -0
  76. package/bin/dev/form.d.ts +36 -0
  77. package/bin/dev/form.js +254 -0
  78. package/bin/dev/index.d.ts +1 -0
  79. package/bin/dev/index.js +5 -0
  80. package/bin/generate/cli.js +29 -2
  81. package/bin/generate/form.d.ts +5 -1
  82. package/bin/generate/form.js +3 -3
  83. package/bin/generate/templates/nonopinionated/config.tpl +12 -0
  84. package/bin/generate/templates/nonopinionated/event.tpl +10 -0
  85. package/bin/generate/templates/nonopinionated/guard.tpl +18 -0
  86. package/bin/generate/templates/nonopinionated/handler.tpl +12 -0
  87. package/bin/generate/templates/nonopinionated/interceptor.tpl +27 -0
  88. package/bin/generate/templates/opinionated/config.tpl +47 -0
  89. package/bin/generate/templates/opinionated/entity.tpl +1 -8
  90. package/bin/generate/templates/opinionated/event.tpl +15 -0
  91. package/bin/generate/templates/opinionated/guard.tpl +41 -0
  92. package/bin/generate/templates/opinionated/handler.tpl +23 -0
  93. package/bin/generate/templates/opinionated/interceptor.tpl +50 -0
  94. package/bin/generate/utils/command-utils.d.ts +7 -3
  95. package/bin/generate/utils/command-utils.js +95 -31
  96. package/bin/generate/utils/nonopininated-cmd.d.ts +10 -1
  97. package/bin/generate/utils/nonopininated-cmd.js +100 -1
  98. package/bin/generate/utils/opinionated-cmd.d.ts +10 -1
  99. package/bin/generate/utils/opinionated-cmd.js +112 -7
  100. package/bin/generate/utils/string-utils.d.ts +6 -0
  101. package/bin/generate/utils/string-utils.js +13 -1
  102. package/bin/help/form.js +11 -3
  103. package/bin/migrate/analyzers/platform-detector.d.ts +14 -0
  104. package/bin/migrate/analyzers/platform-detector.js +116 -0
  105. package/bin/migrate/cli.d.ts +6 -0
  106. package/bin/migrate/cli.js +96 -0
  107. package/bin/migrate/form.d.ts +25 -0
  108. package/bin/migrate/form.js +347 -0
  109. package/bin/migrate/generators/compose-to-k8s.d.ts +2 -0
  110. package/bin/migrate/generators/compose-to-k8s.js +324 -0
  111. package/bin/migrate/generators/compose-to-railway.d.ts +2 -0
  112. package/bin/migrate/generators/compose-to-railway.js +138 -0
  113. package/bin/migrate/generators/compose-to-render.d.ts +2 -0
  114. package/bin/migrate/generators/compose-to-render.js +148 -0
  115. package/bin/migrate/generators/generic-migration.d.ts +9 -0
  116. package/bin/migrate/generators/generic-migration.js +221 -0
  117. package/bin/migrate/generators/heroku-to-fly.d.ts +2 -0
  118. package/bin/migrate/generators/heroku-to-fly.js +291 -0
  119. package/bin/migrate/generators/heroku-to-railway.d.ts +2 -0
  120. package/bin/migrate/generators/heroku-to-railway.js +283 -0
  121. package/bin/migrate/generators/heroku-to-render.d.ts +2 -0
  122. package/bin/migrate/generators/heroku-to-render.js +148 -0
  123. package/bin/migrate/generators/index.d.ts +7 -0
  124. package/bin/migrate/generators/index.js +17 -0
  125. package/bin/migrate/generators/template-loader.d.ts +21 -0
  126. package/bin/migrate/generators/template-loader.js +59 -0
  127. package/bin/migrate/index.d.ts +1 -0
  128. package/bin/migrate/index.js +5 -0
  129. package/bin/new/cli.js +21 -6
  130. package/bin/new/form.d.ts +25 -4
  131. package/bin/new/form.js +285 -70
  132. package/bin/profile/analyzers/dockerfile-analyzer.d.ts +27 -0
  133. package/bin/profile/analyzers/dockerfile-analyzer.js +122 -0
  134. package/bin/profile/analyzers/image-analyzer.d.ts +19 -0
  135. package/bin/profile/analyzers/image-analyzer.js +85 -0
  136. package/bin/profile/cli.d.ts +4 -0
  137. package/bin/profile/cli.js +92 -0
  138. package/bin/profile/form.d.ts +56 -0
  139. package/bin/profile/form.js +400 -0
  140. package/bin/profile/index.d.ts +1 -0
  141. package/bin/profile/index.js +5 -0
  142. package/bin/profile/optimizers/index.d.ts +19 -0
  143. package/bin/profile/optimizers/index.js +137 -0
  144. package/bin/providers/add/form.d.ts +1 -1
  145. package/bin/providers/add/form.js +27 -6
  146. package/bin/providers/create/form.js +2 -1
  147. package/bin/scripts/form.js +27 -5
  148. package/bin/studio/cli.d.ts +15 -0
  149. package/bin/studio/cli.js +166 -0
  150. package/bin/studio/index.d.ts +5 -0
  151. package/bin/studio/index.js +9 -0
  152. package/bin/templates/cache.d.ts +54 -0
  153. package/bin/templates/cache.js +180 -0
  154. package/bin/templates/cli.d.ts +8 -0
  155. package/bin/templates/cli.js +292 -0
  156. package/bin/templates/fetcher.d.ts +49 -0
  157. package/bin/templates/fetcher.js +208 -0
  158. package/bin/templates/index.d.ts +11 -0
  159. package/bin/templates/index.js +37 -0
  160. package/bin/templates/manager.d.ts +116 -0
  161. package/bin/templates/manager.js +323 -0
  162. package/bin/templates/renderer.d.ts +49 -0
  163. package/bin/templates/renderer.js +204 -0
  164. package/bin/templates/types.d.ts +51 -0
  165. package/bin/templates/types.js +5 -0
  166. package/bin/utils/add-module-to-container.d.ts +2 -2
  167. package/bin/utils/add-module-to-container.js +15 -5
  168. package/bin/utils/cli-ui.d.ts +30 -3
  169. package/bin/utils/cli-ui.js +95 -13
  170. package/bin/utils/index.d.ts +4 -0
  171. package/bin/utils/index.js +4 -0
  172. package/bin/utils/input-validation.d.ts +50 -0
  173. package/bin/utils/input-validation.js +143 -0
  174. package/bin/utils/package-manager-commands.d.ts +24 -0
  175. package/bin/utils/package-manager-commands.js +50 -0
  176. package/bin/utils/safe-spawn.d.ts +35 -0
  177. package/bin/utils/safe-spawn.js +51 -0
  178. package/bin/utils/update-tsconfig-paths.d.ts +35 -0
  179. package/bin/utils/update-tsconfig-paths.js +286 -0
  180. package/package.json +154 -154
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyOptimizations = exports.generateOptimizations = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ /**
9
+ * Generate optimization recommendations based on Dockerfile analysis
10
+ */
11
+ function generateOptimizations(analysis) {
12
+ const optimizations = [];
13
+ // Multi-stage build
14
+ if (!analysis.hasMultiStage) {
15
+ optimizations.push({
16
+ id: "OPT001",
17
+ priority: "high",
18
+ category: "Size",
19
+ title: "Use multi-stage build",
20
+ description: "Multi-stage builds significantly reduce final image size by separating build and runtime dependencies.",
21
+ impact: "Can reduce image size by 50-80%",
22
+ autoFixable: false,
23
+ });
24
+ }
25
+ // Non-root user
26
+ if (!analysis.hasNonRootUser) {
27
+ optimizations.push({
28
+ id: "OPT002",
29
+ priority: "high",
30
+ category: "Security",
31
+ title: "Run as non-root user",
32
+ description: "Running containers as root is a security risk. Create and use a non-root user.",
33
+ impact: "Reduces attack surface if container is compromised",
34
+ autoFixable: false,
35
+ });
36
+ }
37
+ // Health check
38
+ if (!analysis.hasHealthCheck) {
39
+ optimizations.push({
40
+ id: "OPT003",
41
+ priority: "medium",
42
+ category: "Reliability",
43
+ title: "Add HEALTHCHECK instruction",
44
+ description: "Health checks allow orchestrators to detect and restart unhealthy containers.",
45
+ impact: "Faster failure detection, better uptime",
46
+ autoFixable: true,
47
+ fix: (content) => {
48
+ // Add health check before CMD
49
+ const cmdMatch = content.match(/^CMD\s+.*/m);
50
+ if (cmdMatch) {
51
+ const healthCheck = `HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \\
52
+ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
53
+
54
+ `;
55
+ return content.replace(cmdMatch[0], healthCheck + cmdMatch[0]);
56
+ }
57
+ return content;
58
+ },
59
+ });
60
+ }
61
+ // npm ci instead of npm install
62
+ if (analysis.hasNpmInstallWithoutCi) {
63
+ optimizations.push({
64
+ id: "OPT004",
65
+ priority: "low",
66
+ category: "Best Practice",
67
+ title: "Use npm ci instead of npm install",
68
+ description: "npm ci provides faster, more reliable, and reproducible builds.",
69
+ impact: "Faster builds, reproducible dependencies",
70
+ autoFixable: true,
71
+ fix: (content) => {
72
+ return content.replace(/npm install(?!\s+[-\w])/g, "npm ci");
73
+ },
74
+ });
75
+ }
76
+ // .dockerignore
77
+ if (!analysis.hasDockerignore) {
78
+ optimizations.push({
79
+ id: "OPT005",
80
+ priority: "medium",
81
+ category: "Size",
82
+ title: "Add .dockerignore file",
83
+ description: "Exclude unnecessary files from the build context to speed up builds and reduce image size.",
84
+ impact: "Faster builds, smaller images",
85
+ autoFixable: false,
86
+ });
87
+ }
88
+ // Combine RUN commands
89
+ if (analysis.layers > 10) {
90
+ optimizations.push({
91
+ id: "OPT006",
92
+ priority: "low",
93
+ category: "Size",
94
+ title: "Combine RUN commands",
95
+ description: `You have ${analysis.layers} RUN layers. Consider combining related commands to reduce layers.`,
96
+ impact: "Slightly smaller image, faster pulls",
97
+ autoFixable: false,
98
+ });
99
+ }
100
+ // Check base image
101
+ if (analysis.baseImage &&
102
+ !analysis.baseImage.includes("alpine") &&
103
+ !analysis.baseImage.includes("slim")) {
104
+ optimizations.push({
105
+ id: "OPT007",
106
+ priority: "medium",
107
+ category: "Size",
108
+ title: "Consider using Alpine or slim base image",
109
+ description: `Current base image: ${analysis.baseImage}. Alpine images are typically 5-10x smaller.`,
110
+ impact: "Significantly smaller image size",
111
+ autoFixable: false,
112
+ });
113
+ }
114
+ return optimizations;
115
+ }
116
+ exports.generateOptimizations = generateOptimizations;
117
+ /**
118
+ * Apply auto-fixable optimizations to Dockerfile
119
+ */
120
+ async function applyOptimizations(dockerfilePath, optimizations) {
121
+ let content = fs_1.default.readFileSync(dockerfilePath, "utf-8");
122
+ let appliedCount = 0;
123
+ for (const opt of optimizations) {
124
+ if (opt.autoFixable && opt.fix) {
125
+ const newContent = opt.fix(content);
126
+ if (newContent !== content) {
127
+ content = newContent;
128
+ appliedCount++;
129
+ }
130
+ }
131
+ }
132
+ if (appliedCount > 0) {
133
+ fs_1.default.writeFileSync(dockerfilePath, content, "utf-8");
134
+ }
135
+ return appliedCount;
136
+ }
137
+ exports.applyOptimizations = applyOptimizations;
@@ -1,2 +1,2 @@
1
- export declare function addProvider(packageName: string, version?: string, isDevDependency?: boolean): Promise<void>;
1
+ export declare function addProvider(packageName: string, version?: string | false, isDevDependency?: boolean): Promise<void>;
2
2
  export declare function removeProvider(packageName: string): Promise<void>;
@@ -5,10 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.removeProvider = exports.addProvider = void 0;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
- const node_child_process_1 = require("node:child_process");
9
8
  const node_fs_1 = __importDefault(require("node:fs"));
10
9
  const node_process_1 = require("node:process");
11
10
  const cli_ui_1 = require("../../utils/cli-ui");
11
+ const input_validation_1 = require("../../utils/input-validation");
12
+ const safe_spawn_1 = require("../../utils/safe-spawn");
12
13
  const PACKAGE_MANAGERS = {
13
14
  npm: {
14
15
  install: "install",
@@ -38,11 +39,14 @@ function detectPackageManager() {
38
39
  }
39
40
  async function execProcess({ command, args, directory, }) {
40
41
  return new Promise((resolve, reject) => {
41
- const isWindows = process.platform === "win32";
42
- const execCommand = isWindows ? `${command}.cmd` : command;
43
- const processRunner = (0, node_child_process_1.spawn)(execCommand, args, {
42
+ // `safeSpawn` (cross-spawn) resolves the Windows `.cmd` shim and
43
+ // applies cmd.exe-aware escaping for every argv entry. Combined
44
+ // with the `isValidPackageName` / `isValidVersion` guards on the
45
+ // caller side, this prevents command injection via the package
46
+ // name or version specifier (which can legitimately contain
47
+ // `>`, `<`, `|`, etc. in a semver range).
48
+ const processRunner = (0, safe_spawn_1.safeSpawn)(command, args, {
44
49
  cwd: directory,
45
- shell: true,
46
50
  });
47
51
  console.log(chalk_1.default.bold.blue(`Executing: ${command} ${args.join(" ")}`));
48
52
  console.log(chalk_1.default.yellow("-------------------------------------------------"));
@@ -66,6 +70,20 @@ async function execProcess({ command, args, directory, }) {
66
70
  });
67
71
  }
68
72
  async function addProvider(packageName, version, isDevDependency = false) {
73
+ if (!(0, input_validation_1.isValidPackageName)(packageName)) {
74
+ (0, cli_ui_1.printError)(`Invalid package name: ${JSON.stringify(packageName)}`, "add-package");
75
+ return;
76
+ }
77
+ // yargs assigns `false` for the version flag when the user omits
78
+ // it (see add/cli.ts). Treat that and "latest" as "no suffix".
79
+ let versionSuffix = "";
80
+ if (typeof version === "string" && version !== "latest") {
81
+ if (!(0, input_validation_1.isValidVersion)(version)) {
82
+ (0, cli_ui_1.printError)(`Invalid version specifier: ${JSON.stringify(version)}`, "add-package");
83
+ return;
84
+ }
85
+ versionSuffix = `@${version}`;
86
+ }
69
87
  const packageManager = detectPackageManager();
70
88
  if (!packageManager) {
71
89
  (0, cli_ui_1.printError)("No package manager found in the project", "add-package");
@@ -75,7 +93,6 @@ async function addProvider(packageName, version, isDevDependency = false) {
75
93
  const command = isDevDependency
76
94
  ? pkgManagerConfig.addDev
77
95
  : pkgManagerConfig.install;
78
- const versionSuffix = version && version !== "latest" ? `@${version}` : "";
79
96
  console.log(`${isDevDependency ? "Adding devDependency" : "Installing"} ${packageName}...`);
80
97
  await execProcess({
81
98
  command: packageManager,
@@ -85,6 +102,10 @@ async function addProvider(packageName, version, isDevDependency = false) {
85
102
  }
86
103
  exports.addProvider = addProvider;
87
104
  async function removeProvider(packageName) {
105
+ if (!(0, input_validation_1.isValidPackageName)(packageName)) {
106
+ (0, cli_ui_1.printError)(`Invalid package name: ${JSON.stringify(packageName)}`, "remove-package");
107
+ return;
108
+ }
88
109
  const packageManager = detectPackageManager();
89
110
  if (!packageManager) {
90
111
  (0, cli_ui_1.printError)("No package manager found in the project", "remove-package");
@@ -39,7 +39,8 @@ const createExternalProvider = async (provider) => {
39
39
  ]);
40
40
  }
41
41
  try {
42
- const emitter = (0, degit_1.default)(`expressots/templates/provider`);
42
+ // Pinned to the v4.0.0 GA tag, same policy as `expressots new`.
43
+ const emitter = (0, degit_1.default)(`expressots/templates/provider#v4.0.0-preview.1`);
43
44
  await emitter.clone(providerInfo.providerName);
44
45
  (0, change_package_info_1.changePackageName)({
45
46
  directory: providerInfo.providerName,
@@ -4,19 +4,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.scriptsForm = void 0;
7
- const child_process_1 = require("child_process");
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const inquirer_1 = __importDefault(require("inquirer"));
10
9
  const path_1 = __importDefault(require("path"));
11
10
  const cli_ui_1 = require("../utils/cli-ui");
11
+ const input_validation_1 = require("../utils/input-validation");
12
+ const safe_spawn_1 = require("../utils/safe-spawn");
12
13
  const cwd = process.cwd();
13
14
  const packageJsonPath = path_1.default.join(cwd, "package.json");
14
15
  function readPackageJson() {
16
+ let raw;
15
17
  try {
16
- return JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8"));
18
+ raw = fs_1.default.readFileSync(packageJsonPath, "utf8");
17
19
  }
18
20
  catch (e) {
19
- (0, cli_ui_1.printError)(`Error reading package.json`, "scripts-command");
21
+ (0, cli_ui_1.printError)(`Error reading package.json: ${e.message}`, "scripts-command");
22
+ process.exit(1);
23
+ }
24
+ try {
25
+ return JSON.parse(raw);
26
+ }
27
+ catch (e) {
28
+ (0, cli_ui_1.printError)(`package.json is not valid JSON: ${e.message}`, "scripts-command");
20
29
  process.exit(1);
21
30
  }
22
31
  }
@@ -51,14 +60,27 @@ async function promptUserToSelectScripts(scripts) {
51
60
  }
52
61
  function executeScripts(scripts, selectedScripts, runner) {
53
62
  selectedScripts.forEach((script) => {
63
+ if (!(0, input_validation_1.isValidScriptName)(script)) {
64
+ (0, cli_ui_1.printWarning)(`Skipping script with invalid name: ${JSON.stringify(script)}`, "scripts-command");
65
+ return;
66
+ }
54
67
  console.log(`Running ${script}...`);
55
68
  try {
56
- const command = `${runner} run ${script}`;
69
+ // `safeSpawnSync` (cross-spawn) handles Windows `.cmd` shim
70
+ // resolution and cmd.exe-aware argv escaping. Script names
71
+ // are also constrained to a strict alphanumeric/dash/dot
72
+ // charset by `isValidScriptName` above for defense in depth.
57
73
  const options = {
58
74
  stdio: "inherit",
59
75
  env: { ...process.env },
60
76
  };
61
- (0, child_process_1.execSync)(command, options);
77
+ const result = (0, safe_spawn_1.safeSpawnSync)(runner, ["run", script], options);
78
+ if (result.error) {
79
+ throw result.error;
80
+ }
81
+ if (typeof result.status === "number" && result.status !== 0) {
82
+ throw new Error(`exited with code ${result.status}`);
83
+ }
62
84
  }
63
85
  catch (e) {
64
86
  (0, cli_ui_1.printWarning)(`Command ${script} cancelled or failed - ${e}`, "scripts-command");
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Studio command CLI implementation
3
+ */
4
+ import type { CommandModule } from "yargs";
5
+ interface StudioYargsOptions {
6
+ port: number;
7
+ "agent-port": number;
8
+ "no-browser": boolean;
9
+ src: string;
10
+ }
11
+ /**
12
+ * Studio command definition
13
+ */
14
+ export declare function studioCommand(): CommandModule<object, StudioYargsOptions>;
15
+ export {};
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ /**
3
+ * Studio command CLI implementation
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.studioCommand = void 0;
10
+ const chalk_1 = __importDefault(require("chalk"));
11
+ const ora_1 = __importDefault(require("ora"));
12
+ const fs_1 = require("fs");
13
+ const path_1 = require("path");
14
+ const safe_spawn_1 = require("../utils/safe-spawn");
15
+ /**
16
+ * Check if @expressots/studio is installed
17
+ */
18
+ function isStudioInstalled() {
19
+ try {
20
+ // Check if the package exists in node_modules
21
+ const studioPath = (0, path_1.resolve)(process.cwd(), "node_modules/@expressots/studio");
22
+ return (0, fs_1.existsSync)(studioPath);
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ /**
29
+ * Install @expressots/studio as a dev dependency
30
+ */
31
+ async function installStudio() {
32
+ const spinner = (0, ora_1.default)("Installing @expressots/studio...").start();
33
+ try {
34
+ const hasYarn = (0, fs_1.existsSync)((0, path_1.resolve)(process.cwd(), "yarn.lock"));
35
+ const hasPnpm = (0, fs_1.existsSync)((0, path_1.resolve)(process.cwd(), "pnpm-lock.yaml"));
36
+ let pkgManager;
37
+ let args;
38
+ if (hasPnpm) {
39
+ pkgManager = "pnpm";
40
+ args = ["add", "-D", "@expressots/studio"];
41
+ }
42
+ else if (hasYarn) {
43
+ pkgManager = "yarn";
44
+ args = ["add", "-D", "@expressots/studio"];
45
+ }
46
+ else {
47
+ pkgManager = "npm";
48
+ args = ["install", "-D", "@expressots/studio"];
49
+ }
50
+ // `safeSpawnSync` (cross-spawn) resolves the Windows `.cmd` shim
51
+ // and properly escapes argv for cmd.exe. The argv here is a
52
+ // fixed list of literal strings, so it is safe by construction.
53
+ const result = (0, safe_spawn_1.safeSpawnSync)(pkgManager, args, {
54
+ stdio: "pipe",
55
+ });
56
+ if (result.error)
57
+ throw result.error;
58
+ if (typeof result.status === "number" && result.status !== 0) {
59
+ throw new Error(`exited with code ${result.status}`);
60
+ }
61
+ spinner.succeed(chalk_1.default.green("@expressots/studio installed successfully"));
62
+ return true;
63
+ }
64
+ catch (error) {
65
+ spinner.fail(chalk_1.default.red("Failed to install @expressots/studio"));
66
+ console.error(chalk_1.default.yellow("\nPlease install manually: npm install -D @expressots/studio"));
67
+ return false;
68
+ }
69
+ }
70
+ /**
71
+ * Launch the Studio
72
+ */
73
+ async function launchStudio(options) {
74
+ const args = ["start"];
75
+ if (options.port) {
76
+ args.push("--port", String(options.port));
77
+ }
78
+ if (options.agentPort) {
79
+ args.push("--agent-port", String(options.agentPort));
80
+ }
81
+ if (options.noBrowser) {
82
+ args.push("--no-browser");
83
+ }
84
+ if (options.src) {
85
+ args.push("--src", options.src);
86
+ }
87
+ const isWindows = process.platform === "win32";
88
+ const studioBinName = isWindows
89
+ ? "expressots-studio.cmd"
90
+ : "expressots-studio";
91
+ const studioPath = (0, path_1.resolve)(process.cwd(), "node_modules/.bin", studioBinName);
92
+ // `safeSpawn` (cross-spawn) handles Windows `.cmd` shim invocation
93
+ // and per-arg cmd.exe escaping, so user-controlled flags like
94
+ // `--src` or `--port` cannot break out into shell metacharacters
95
+ // even if the project path itself contains spaces.
96
+ const child = (0, safe_spawn_1.safeSpawn)(studioPath, args, {
97
+ stdio: "inherit",
98
+ cwd: process.cwd(),
99
+ });
100
+ child.on("error", (error) => {
101
+ console.error(chalk_1.default.red("Failed to start Studio:"), error.message);
102
+ process.exit(1);
103
+ });
104
+ child.on("exit", (code) => {
105
+ process.exit(code ?? 0);
106
+ });
107
+ }
108
+ /**
109
+ * Studio command handler
110
+ */
111
+ async function studioHandler(options) {
112
+ // Check if Studio is installed
113
+ if (!isStudioInstalled()) {
114
+ console.log(chalk_1.default.yellow("📦 @expressots/studio is not installed in this project."));
115
+ console.log("");
116
+ // Try to install
117
+ const installed = await installStudio();
118
+ if (!installed) {
119
+ process.exit(1);
120
+ }
121
+ console.log("");
122
+ }
123
+ // Launch Studio
124
+ await launchStudio(options);
125
+ }
126
+ /**
127
+ * Studio command definition
128
+ */
129
+ function studioCommand() {
130
+ return {
131
+ command: "studio",
132
+ describe: "Launch ExpressoTS Studio - Developer Experience Platform",
133
+ builder: (yargs) => yargs
134
+ .option("port", {
135
+ alias: "p",
136
+ type: "number",
137
+ default: 3333,
138
+ description: "UI port",
139
+ })
140
+ .option("agent-port", {
141
+ alias: "a",
142
+ type: "number",
143
+ default: 3334,
144
+ description: "Agent WebSocket port",
145
+ })
146
+ .option("no-browser", {
147
+ type: "boolean",
148
+ default: false,
149
+ description: "Do not open browser automatically",
150
+ })
151
+ .option("src", {
152
+ type: "string",
153
+ default: "./src",
154
+ description: "Source directory to scan",
155
+ }),
156
+ handler: async (argv) => {
157
+ await studioHandler({
158
+ port: argv.port,
159
+ agentPort: argv["agent-port"],
160
+ noBrowser: argv["no-browser"],
161
+ src: argv.src,
162
+ });
163
+ },
164
+ };
165
+ }
166
+ exports.studioCommand = studioCommand;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ExpressoTS Studio CLI integration
3
+ * Launches the local Studio development environment
4
+ */
5
+ export { studioCommand } from "./cli";
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ /**
3
+ * ExpressoTS Studio CLI integration
4
+ * Launches the local Studio development environment
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.studioCommand = void 0;
8
+ var cli_1 = require("./cli");
9
+ Object.defineProperty(exports, "studioCommand", { enumerable: true, get: function () { return cli_1.studioCommand; } });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Template cache management
3
+ * Stores templates locally for offline access and performance
4
+ */
5
+ import type { CacheConfig } from "./types";
6
+ export declare class TemplateCache {
7
+ private config;
8
+ constructor(config?: Partial<CacheConfig>);
9
+ /**
10
+ * Ensure cache directory exists
11
+ */
12
+ private ensureCacheDir;
13
+ /**
14
+ * Generate cache key from template identifier
15
+ */
16
+ private getCacheKey;
17
+ /**
18
+ * Get cache file path
19
+ */
20
+ private getCachePath;
21
+ /**
22
+ * Check if cache entry is valid (not expired)
23
+ */
24
+ private isValid;
25
+ /**
26
+ * Get cached template content
27
+ */
28
+ get<T>(category: string, platform: string, variant?: string): T | null;
29
+ /**
30
+ * Set cached template content
31
+ */
32
+ set<T>(category: string, platform: string, data: T, variant?: string, ttl?: number): void;
33
+ /**
34
+ * Delete cached template
35
+ */
36
+ delete(category: string, platform: string, variant?: string): void;
37
+ /**
38
+ * Clear all cached templates
39
+ */
40
+ clear(): void;
41
+ /**
42
+ * Get cache statistics
43
+ */
44
+ getStats(): {
45
+ files: number;
46
+ totalSize: number;
47
+ oldestEntry: Date | null;
48
+ };
49
+ /**
50
+ * Get cache directory path
51
+ */
52
+ getCacheDirectory(): string;
53
+ }
54
+ export declare function getTemplateCache(config?: Partial<CacheConfig>): TemplateCache;