@invarn/cibuild 1.3.15 → 1.3.17

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 (242) hide show
  1. package/dist/cli.cjs +1 -1
  2. package/dist/src/cli.d.ts +3 -0
  3. package/dist/src/cli.d.ts.map +1 -0
  4. package/dist/src/cli.js +987 -0
  5. package/dist/src/commands/android-scanner.d.ts +32 -0
  6. package/dist/src/commands/android-scanner.d.ts.map +1 -0
  7. package/dist/src/commands/android-scanner.js +667 -0
  8. package/dist/src/commands/build.d.ts +5 -0
  9. package/dist/src/commands/build.d.ts.map +1 -0
  10. package/dist/src/commands/build.js +1096 -0
  11. package/dist/src/commands/edit.d.ts +3 -0
  12. package/dist/src/commands/edit.d.ts.map +1 -0
  13. package/dist/src/commands/edit.js +651 -0
  14. package/dist/src/commands/file-secret-collector.d.ts +37 -0
  15. package/dist/src/commands/file-secret-collector.d.ts.map +1 -0
  16. package/dist/src/commands/file-secret-collector.js +199 -0
  17. package/dist/src/commands/github-workflow.d.ts +5 -0
  18. package/dist/src/commands/github-workflow.d.ts.map +1 -0
  19. package/dist/src/commands/github-workflow.js +45 -0
  20. package/dist/src/commands/ios-scanner.d.ts +27 -0
  21. package/dist/src/commands/ios-scanner.d.ts.map +1 -0
  22. package/dist/src/commands/ios-scanner.js +337 -0
  23. package/dist/src/commands/reset.d.ts +7 -0
  24. package/dist/src/commands/reset.d.ts.map +1 -0
  25. package/dist/src/commands/reset.js +81 -0
  26. package/dist/src/commands/secrets-sync-workflow.d.ts +15 -0
  27. package/dist/src/commands/secrets-sync-workflow.d.ts.map +1 -0
  28. package/dist/src/commands/secrets-sync-workflow.js +255 -0
  29. package/dist/src/commands/secrets-upload.d.ts +21 -0
  30. package/dist/src/commands/secrets-upload.d.ts.map +1 -0
  31. package/dist/src/commands/secrets-upload.js +177 -0
  32. package/dist/src/commands/secrets-upload.test.d.ts +5 -0
  33. package/dist/src/commands/secrets-upload.test.d.ts.map +1 -0
  34. package/dist/src/commands/secrets-upload.test.js +60 -0
  35. package/dist/src/config.d.ts +3 -0
  36. package/dist/src/config.d.ts.map +1 -0
  37. package/dist/src/config.js +46 -0
  38. package/dist/src/envman/cli.d.ts +21 -0
  39. package/dist/src/envman/cli.d.ts.map +1 -0
  40. package/dist/src/envman/cli.js +240 -0
  41. package/dist/src/envman/envman.d.ts +83 -0
  42. package/dist/src/envman/envman.d.ts.map +1 -0
  43. package/dist/src/envman/envman.js +361 -0
  44. package/dist/src/envman/envman.test.d.ts +5 -0
  45. package/dist/src/envman/envman.test.d.ts.map +1 -0
  46. package/dist/src/envman/envman.test.js +236 -0
  47. package/dist/src/envman/index.d.ts +23 -0
  48. package/dist/src/envman/index.d.ts.map +1 -0
  49. package/dist/src/envman/index.js +23 -0
  50. package/dist/src/envman/types.d.ts +55 -0
  51. package/dist/src/envman/types.d.ts.map +1 -0
  52. package/dist/src/envman/types.js +12 -0
  53. package/dist/src/lib.d.ts +27 -0
  54. package/dist/src/lib.d.ts.map +1 -0
  55. package/dist/src/lib.js +32 -0
  56. package/dist/src/pipeline.d.ts +3 -0
  57. package/dist/src/pipeline.d.ts.map +1 -0
  58. package/dist/src/pipeline.js +57 -0
  59. package/dist/src/runner.d.ts +17 -0
  60. package/dist/src/runner.d.ts.map +1 -0
  61. package/dist/src/runner.js +234 -0
  62. package/dist/src/types.d.ts +57 -0
  63. package/dist/src/types.d.ts.map +1 -0
  64. package/dist/src/types.js +2 -0
  65. package/dist/src/yaml/bitrise-compat.d.ts +65 -0
  66. package/dist/src/yaml/bitrise-compat.d.ts.map +1 -0
  67. package/dist/src/yaml/bitrise-compat.js +206 -0
  68. package/dist/src/yaml/bitrise-compat.test.d.ts +5 -0
  69. package/dist/src/yaml/bitrise-compat.test.d.ts.map +1 -0
  70. package/dist/src/yaml/bitrise-compat.test.js +347 -0
  71. package/dist/src/yaml/converter.d.ts +33 -0
  72. package/dist/src/yaml/converter.d.ts.map +1 -0
  73. package/dist/src/yaml/converter.js +222 -0
  74. package/dist/src/yaml/converter.test.d.ts +5 -0
  75. package/dist/src/yaml/converter.test.d.ts.map +1 -0
  76. package/dist/src/yaml/converter.test.js +348 -0
  77. package/dist/src/yaml/e2e.test.d.ts +6 -0
  78. package/dist/src/yaml/e2e.test.d.ts.map +1 -0
  79. package/dist/src/yaml/e2e.test.js +446 -0
  80. package/dist/src/yaml/env-resolver.d.ts +120 -0
  81. package/dist/src/yaml/env-resolver.d.ts.map +1 -0
  82. package/dist/src/yaml/env-resolver.js +405 -0
  83. package/dist/src/yaml/env-resolver.test.d.ts +5 -0
  84. package/dist/src/yaml/env-resolver.test.d.ts.map +1 -0
  85. package/dist/src/yaml/env-resolver.test.js +502 -0
  86. package/dist/src/yaml/interactive-prompts.d.ts +71 -0
  87. package/dist/src/yaml/interactive-prompts.d.ts.map +1 -0
  88. package/dist/src/yaml/interactive-prompts.js +258 -0
  89. package/dist/src/yaml/missing-env-handler.d.ts +45 -0
  90. package/dist/src/yaml/missing-env-handler.d.ts.map +1 -0
  91. package/dist/src/yaml/missing-env-handler.js +64 -0
  92. package/dist/src/yaml/parser.d.ts +33 -0
  93. package/dist/src/yaml/parser.d.ts.map +1 -0
  94. package/dist/src/yaml/parser.js +145 -0
  95. package/dist/src/yaml/pipeline-with-secrets.d.ts +25 -0
  96. package/dist/src/yaml/pipeline-with-secrets.d.ts.map +1 -0
  97. package/dist/src/yaml/pipeline-with-secrets.js +76 -0
  98. package/dist/src/yaml/platform-detector.d.ts +83 -0
  99. package/dist/src/yaml/platform-detector.d.ts.map +1 -0
  100. package/dist/src/yaml/platform-detector.js +188 -0
  101. package/dist/src/yaml/platform-detector.test.d.ts +5 -0
  102. package/dist/src/yaml/platform-detector.test.d.ts.map +1 -0
  103. package/dist/src/yaml/platform-detector.test.js +414 -0
  104. package/dist/src/yaml/preflight-validation.d.ts +40 -0
  105. package/dist/src/yaml/preflight-validation.d.ts.map +1 -0
  106. package/dist/src/yaml/preflight-validation.js +152 -0
  107. package/dist/src/yaml/secrets-manager.d.ts +77 -0
  108. package/dist/src/yaml/secrets-manager.d.ts.map +1 -0
  109. package/dist/src/yaml/secrets-manager.js +219 -0
  110. package/dist/src/yaml/step-validator.d.ts +54 -0
  111. package/dist/src/yaml/step-validator.d.ts.map +1 -0
  112. package/dist/src/yaml/step-validator.js +403 -0
  113. package/dist/src/yaml/steps/android-sign.d.ts +35 -0
  114. package/dist/src/yaml/steps/android-sign.d.ts.map +1 -0
  115. package/dist/src/yaml/steps/android-sign.js +147 -0
  116. package/dist/src/yaml/steps/android-version.d.ts +26 -0
  117. package/dist/src/yaml/steps/android-version.d.ts.map +1 -0
  118. package/dist/src/yaml/steps/android-version.js +128 -0
  119. package/dist/src/yaml/steps/android-version.test.d.ts +5 -0
  120. package/dist/src/yaml/steps/android-version.test.d.ts.map +1 -0
  121. package/dist/src/yaml/steps/android-version.test.js +196 -0
  122. package/dist/src/yaml/steps/android.d.ts +95 -0
  123. package/dist/src/yaml/steps/android.d.ts.map +1 -0
  124. package/dist/src/yaml/steps/android.js +916 -0
  125. package/dist/src/yaml/steps/app-store-deploy.d.ts +48 -0
  126. package/dist/src/yaml/steps/app-store-deploy.d.ts.map +1 -0
  127. package/dist/src/yaml/steps/app-store-deploy.js +162 -0
  128. package/dist/src/yaml/steps/base.d.ts +238 -0
  129. package/dist/src/yaml/steps/base.d.ts.map +1 -0
  130. package/dist/src/yaml/steps/base.js +345 -0
  131. package/dist/src/yaml/steps/bitrise-android-tools.d.ts +26 -0
  132. package/dist/src/yaml/steps/bitrise-android-tools.d.ts.map +1 -0
  133. package/dist/src/yaml/steps/bitrise-android-tools.js +198 -0
  134. package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts +5 -0
  135. package/dist/src/yaml/steps/bitrise-android-tools.test.d.ts.map +1 -0
  136. package/dist/src/yaml/steps/bitrise-android-tools.test.js +280 -0
  137. package/dist/src/yaml/steps/bitrise-apk-info.d.ts +22 -0
  138. package/dist/src/yaml/steps/bitrise-apk-info.d.ts.map +1 -0
  139. package/dist/src/yaml/steps/bitrise-apk-info.js +144 -0
  140. package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts +5 -0
  141. package/dist/src/yaml/steps/bitrise-apk-info.test.d.ts.map +1 -0
  142. package/dist/src/yaml/steps/bitrise-apk-info.test.js +331 -0
  143. package/dist/src/yaml/steps/bitrise-slack.d.ts +49 -0
  144. package/dist/src/yaml/steps/bitrise-slack.d.ts.map +1 -0
  145. package/dist/src/yaml/steps/bitrise-slack.js +280 -0
  146. package/dist/src/yaml/steps/bitrise-slack.test.d.ts +5 -0
  147. package/dist/src/yaml/steps/bitrise-slack.test.d.ts.map +1 -0
  148. package/dist/src/yaml/steps/bitrise-slack.test.js +484 -0
  149. package/dist/src/yaml/steps/bitrise-ssh.d.ts +27 -0
  150. package/dist/src/yaml/steps/bitrise-ssh.d.ts.map +1 -0
  151. package/dist/src/yaml/steps/bitrise-ssh.js +134 -0
  152. package/dist/src/yaml/steps/bitrise-ssh.test.d.ts +5 -0
  153. package/dist/src/yaml/steps/bitrise-ssh.test.d.ts.map +1 -0
  154. package/dist/src/yaml/steps/bitrise-ssh.test.js +205 -0
  155. package/dist/src/yaml/steps/cache.d.ts +52 -0
  156. package/dist/src/yaml/steps/cache.d.ts.map +1 -0
  157. package/dist/src/yaml/steps/cache.js +351 -0
  158. package/dist/src/yaml/steps/fastlane.d.ts +27 -0
  159. package/dist/src/yaml/steps/fastlane.d.ts.map +1 -0
  160. package/dist/src/yaml/steps/fastlane.js +79 -0
  161. package/dist/src/yaml/steps/file.d.ts +27 -0
  162. package/dist/src/yaml/steps/file.d.ts.map +1 -0
  163. package/dist/src/yaml/steps/file.js +35 -0
  164. package/dist/src/yaml/steps/flutter.d.ts +63 -0
  165. package/dist/src/yaml/steps/flutter.d.ts.map +1 -0
  166. package/dist/src/yaml/steps/flutter.js +215 -0
  167. package/dist/src/yaml/steps/git-clone.d.ts +26 -0
  168. package/dist/src/yaml/steps/git-clone.d.ts.map +1 -0
  169. package/dist/src/yaml/steps/git-clone.js +111 -0
  170. package/dist/src/yaml/steps/google-play-deploy.d.ts +37 -0
  171. package/dist/src/yaml/steps/google-play-deploy.d.ts.map +1 -0
  172. package/dist/src/yaml/steps/google-play-deploy.js +193 -0
  173. package/dist/src/yaml/steps/google-play-deploy.test.d.ts +5 -0
  174. package/dist/src/yaml/steps/google-play-deploy.test.d.ts.map +1 -0
  175. package/dist/src/yaml/steps/google-play-deploy.test.js +310 -0
  176. package/dist/src/yaml/steps/index.d.ts +10 -0
  177. package/dist/src/yaml/steps/index.d.ts.map +1 -0
  178. package/dist/src/yaml/steps/index.js +1361 -0
  179. package/dist/src/yaml/steps/ios-deps.d.ts +43 -0
  180. package/dist/src/yaml/steps/ios-deps.d.ts.map +1 -0
  181. package/dist/src/yaml/steps/ios-deps.js +141 -0
  182. package/dist/src/yaml/steps/ios-deps.test.d.ts +5 -0
  183. package/dist/src/yaml/steps/ios-deps.test.d.ts.map +1 -0
  184. package/dist/src/yaml/steps/ios-deps.test.js +90 -0
  185. package/dist/src/yaml/steps/ios-signing.d.ts +31 -0
  186. package/dist/src/yaml/steps/ios-signing.d.ts.map +1 -0
  187. package/dist/src/yaml/steps/ios-signing.js +144 -0
  188. package/dist/src/yaml/steps/ios-version.d.ts +47 -0
  189. package/dist/src/yaml/steps/ios-version.d.ts.map +1 -0
  190. package/dist/src/yaml/steps/ios-version.js +151 -0
  191. package/dist/src/yaml/steps/linting.d.ts +47 -0
  192. package/dist/src/yaml/steps/linting.d.ts.map +1 -0
  193. package/dist/src/yaml/steps/linting.js +148 -0
  194. package/dist/src/yaml/steps/phase2.test.d.ts +6 -0
  195. package/dist/src/yaml/steps/phase2.test.d.ts.map +1 -0
  196. package/dist/src/yaml/steps/phase2.test.js +197 -0
  197. package/dist/src/yaml/steps/phase3.test.d.ts +5 -0
  198. package/dist/src/yaml/steps/phase3.test.d.ts.map +1 -0
  199. package/dist/src/yaml/steps/phase3.test.js +144 -0
  200. package/dist/src/yaml/steps/phase4.test.d.ts +5 -0
  201. package/dist/src/yaml/steps/phase4.test.d.ts.map +1 -0
  202. package/dist/src/yaml/steps/phase4.test.js +166 -0
  203. package/dist/src/yaml/steps/phase5.test.d.ts +6 -0
  204. package/dist/src/yaml/steps/phase5.test.d.ts.map +1 -0
  205. package/dist/src/yaml/steps/phase5.test.js +263 -0
  206. package/dist/src/yaml/steps/registry.d.ts +88 -0
  207. package/dist/src/yaml/steps/registry.d.ts.map +1 -0
  208. package/dist/src/yaml/steps/registry.js +125 -0
  209. package/dist/src/yaml/steps/registry.test.d.ts +5 -0
  210. package/dist/src/yaml/steps/registry.test.d.ts.map +1 -0
  211. package/dist/src/yaml/steps/registry.test.js +235 -0
  212. package/dist/src/yaml/steps/release.d.ts +50 -0
  213. package/dist/src/yaml/steps/release.d.ts.map +1 -0
  214. package/dist/src/yaml/steps/release.js +154 -0
  215. package/dist/src/yaml/steps/script.d.ts +23 -0
  216. package/dist/src/yaml/steps/script.d.ts.map +1 -0
  217. package/dist/src/yaml/steps/script.js +63 -0
  218. package/dist/src/yaml/steps/spec-validation.test.d.ts +6 -0
  219. package/dist/src/yaml/steps/spec-validation.test.d.ts.map +1 -0
  220. package/dist/src/yaml/steps/spec-validation.test.js +130 -0
  221. package/dist/src/yaml/steps/steps.test.d.ts +6 -0
  222. package/dist/src/yaml/steps/steps.test.d.ts.map +1 -0
  223. package/dist/src/yaml/steps/steps.test.js +474 -0
  224. package/dist/src/yaml/steps/test-config.d.ts +3 -0
  225. package/dist/src/yaml/steps/test-config.d.ts.map +1 -0
  226. package/dist/src/yaml/steps/test-config.js +16 -0
  227. package/dist/src/yaml/steps/xcode-new.test.d.ts +5 -0
  228. package/dist/src/yaml/steps/xcode-new.test.d.ts.map +1 -0
  229. package/dist/src/yaml/steps/xcode-new.test.js +211 -0
  230. package/dist/src/yaml/steps/xcode.d.ts +222 -0
  231. package/dist/src/yaml/steps/xcode.d.ts.map +1 -0
  232. package/dist/src/yaml/steps/xcode.js +999 -0
  233. package/dist/src/yaml/types.d.ts +68 -0
  234. package/dist/src/yaml/types.d.ts.map +1 -0
  235. package/dist/src/yaml/types.js +5 -0
  236. package/dist/src/yaml/validation-types.d.ts +96 -0
  237. package/dist/src/yaml/validation-types.d.ts.map +1 -0
  238. package/dist/src/yaml/validation-types.js +8 -0
  239. package/dist/src/yaml/yaml-updater.d.ts +24 -0
  240. package/dist/src/yaml/yaml-updater.d.ts.map +1 -0
  241. package/dist/src/yaml/yaml-updater.js +128 -0
  242. package/package.json +16 -4
@@ -0,0 +1,987 @@
1
+ #!/usr/bin/env node
2
+ import { resolve, extname } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { existsSync, readdirSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, appendFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { execSync } from "node:child_process";
7
+ import prompts from "prompts";
8
+ import { PipelineRunner } from "./runner.js";
9
+ import { loadConfig } from "./config.js";
10
+ import { loadYAMLPipeline } from "./yaml/parser.js";
11
+ import { convertYAMLWithSecrets } from "./yaml/pipeline-with-secrets.js";
12
+ import { detectPlatformInfo } from "./yaml/platform-detector.js";
13
+ import { StepValidator, formatValidationResult } from "./yaml/step-validator.js";
14
+ import { SecretsManager } from "./yaml/secrets-manager.js";
15
+ import { MissingEnvHandler } from "./yaml/missing-env-handler.js";
16
+ import { MissingEnvironmentVariableError } from "./yaml/env-resolver.js";
17
+ import "./yaml/steps/index.js"; // Initialize step registry
18
+ import { handleBuildCommand } from "./commands/build.js";
19
+ import { handleEditCommand } from "./commands/edit.js";
20
+ import { handleSecretsUploadCommand } from "./commands/secrets-upload.js";
21
+ import { handleSecretsSyncWorkflowCommand } from "./commands/secrets-sync-workflow.js";
22
+ import { handleResetCommand } from "./commands/reset.js";
23
+ import { generateGitHubActionsWorkflow } from "./commands/github-workflow.js";
24
+ /**
25
+ * Detects whether the current directory is the root of an Android or iOS project.
26
+ * Returns the detected project type, or null if neither is found.
27
+ */
28
+ function detectMobileProjectRoot(dir) {
29
+ // Android: must have build.gradle or build.gradle.kts at root
30
+ const androidIndicators = [
31
+ "build.gradle",
32
+ "build.gradle.kts",
33
+ "settings.gradle",
34
+ "settings.gradle.kts",
35
+ "gradlew",
36
+ ];
37
+ const hasAndroidIndicator = androidIndicators.some((f) => existsSync(resolve(dir, f)));
38
+ if (hasAndroidIndicator) {
39
+ return "android";
40
+ }
41
+ // iOS: must have a .xcodeproj or .xcworkspace directory, or a Podfile
42
+ const iosFileIndicators = ["Podfile"];
43
+ const hasIosFile = iosFileIndicators.some((f) => existsSync(resolve(dir, f)));
44
+ if (!hasIosFile) {
45
+ // Check for .xcodeproj / .xcworkspace directories
46
+ try {
47
+ const entries = readdirSync(dir);
48
+ const hasXcodeDir = entries.some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
49
+ if (hasXcodeDir) {
50
+ return "ios";
51
+ }
52
+ }
53
+ catch {
54
+ // If we can't read the directory, fall through
55
+ }
56
+ }
57
+ else {
58
+ return "ios";
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * Ensures CI Build runtime files are listed in .gitignore.
64
+ * Safe to call multiple times — only appends entries that are missing.
65
+ */
66
+ function ensureCiBuildGitignoreEntries(cwd) {
67
+ const gitignorePath = resolve(cwd, ".gitignore");
68
+ const gitignoreEntries = [".cibuild-secrets.json", ".ci/.envstore.json", ".ci/keys/", "build/"];
69
+ if (existsSync(gitignorePath)) {
70
+ const contents = readFileSync(gitignorePath, "utf-8");
71
+ const lines = contents.split("\n").map((l) => l.trim());
72
+ const toAdd = gitignoreEntries.filter((e) => !lines.includes(e));
73
+ if (toAdd.length > 0) {
74
+ appendFileSync(gitignorePath, `\n${toAdd.join("\n")}\n`);
75
+ for (const entry of toAdd) {
76
+ console.log(`✅ Added ${entry} to .gitignore`);
77
+ }
78
+ }
79
+ }
80
+ else {
81
+ writeFileSync(gitignorePath, `${gitignoreEntries.join("\n")}\n`);
82
+ console.log(`✅ Created .gitignore with CI Build entries`);
83
+ }
84
+ }
85
+ async function handleInitCommand(opts = {}) {
86
+ const cwd = process.cwd();
87
+ // Verify we are inside a mobile project root before doing anything
88
+ const projectType = detectMobileProjectRoot(cwd);
89
+ if (!projectType) {
90
+ console.error("Error: Not a mobile project root.");
91
+ console.error("ci init must be run from the root folder of an Android or iOS project.");
92
+ console.error("\nAndroid projects must contain one of:");
93
+ console.error(" build.gradle, build.gradle.kts, settings.gradle, settings.gradle.kts, gradlew");
94
+ console.error("\niOS projects must contain one of:");
95
+ console.error(" *.xcodeproj, *.xcworkspace, Podfile");
96
+ process.exit(1);
97
+ }
98
+ // Bail out early if .ci already exists and has content
99
+ const ciDir = resolve(cwd, ".ci");
100
+ if (existsSync(ciDir)) {
101
+ const entries = readdirSync(ciDir);
102
+ if (entries.length > 0) {
103
+ console.error("Already initialised. Run 'ci run' to execute your pipeline.");
104
+ process.exit(1);
105
+ }
106
+ }
107
+ console.log(`\nDetected project type: ${projectType === "android" ? "Android" : "iOS"}`);
108
+ console.log(`
109
+ ╔══════════════════════════════════════════════════════════════╗
110
+ ║ CI Build - Dependency Check ║
111
+ ╚══════════════════════════════════════════════════════════════╝
112
+ `);
113
+ const commonDependencies = [
114
+ {
115
+ name: "Git",
116
+ command: "git --version",
117
+ required: true,
118
+ installInstructions: "Download from https://git-scm.com/"
119
+ }
120
+ ];
121
+ const androidDependencies = [
122
+ {
123
+ name: "Java (JDK)",
124
+ command: "java -version 2>&1 | head -1",
125
+ required: true,
126
+ installInstructions: "Download from https://adoptium.net/ or install via brew: brew install --cask temurin"
127
+ },
128
+ {
129
+ name: "Android SDK",
130
+ customCheck: () => {
131
+ const fromEnv = process.env.ANDROID_HOME;
132
+ if (fromEnv && existsSync(fromEnv)) {
133
+ return { found: true, message: `${fromEnv} (via ANDROID_HOME)` };
134
+ }
135
+ const commonPaths = [
136
+ `${homedir()}/Library/Android/sdk`, // macOS
137
+ `${homedir()}/Android/Sdk`, // Linux
138
+ "/usr/local/android-sdk",
139
+ ];
140
+ const detected = commonPaths.find((p) => existsSync(p));
141
+ if (detected) {
142
+ return { found: true, message: `${detected} (auto-detected)` };
143
+ }
144
+ return { found: false, message: "Android SDK not found" };
145
+ },
146
+ required: true,
147
+ installInstructions: "Install Android Studio from https://developer.android.com/studio or set ANDROID_HOME manually"
148
+ }
149
+ ];
150
+ const iosDependencies = [
151
+ {
152
+ name: "Xcode",
153
+ command: "xcodebuild -version 2>&1 | head -1",
154
+ required: true,
155
+ installInstructions: "Install Xcode from the Mac App Store"
156
+ },
157
+ {
158
+ name: "CocoaPods",
159
+ command: "pod --version",
160
+ required: false,
161
+ installInstructions: "Run: sudo gem install cocoapods"
162
+ }
163
+ ];
164
+ const dependencies = [
165
+ ...commonDependencies,
166
+ ...(projectType === "android" ? androidDependencies : iosDependencies),
167
+ ];
168
+ let allRequired = true;
169
+ let missingOptional = false;
170
+ for (const dep of dependencies) {
171
+ if (dep.customCheck) {
172
+ const result = dep.customCheck();
173
+ if (result.found) {
174
+ console.log(`✅ ${dep.name.padEnd(20)} ${result.message}`);
175
+ }
176
+ else if (dep.required) {
177
+ console.log(`❌ ${dep.name.padEnd(20)} Not found (REQUIRED)`);
178
+ console.log(` Install: ${dep.installInstructions}`);
179
+ allRequired = false;
180
+ }
181
+ else {
182
+ console.log(`⚠️ ${dep.name.padEnd(20)} Not found (optional)`);
183
+ console.log(` Install: ${dep.installInstructions}`);
184
+ missingOptional = true;
185
+ }
186
+ continue;
187
+ }
188
+ try {
189
+ const output = execSync(dep.command, {
190
+ encoding: "utf-8",
191
+ stdio: ["pipe", "pipe", "pipe"],
192
+ shell: "/bin/sh",
193
+ }).trim();
194
+ console.log(`✅ ${dep.name.padEnd(20)} ${output}`);
195
+ }
196
+ catch {
197
+ if (dep.required) {
198
+ console.log(`❌ ${dep.name.padEnd(20)} Not found (REQUIRED)`);
199
+ console.log(` Install: ${dep.installInstructions}`);
200
+ allRequired = false;
201
+ }
202
+ else {
203
+ console.log(`⚠️ ${dep.name.padEnd(20)} Not found (optional)`);
204
+ console.log(` Install: ${dep.installInstructions}`);
205
+ missingOptional = true;
206
+ }
207
+ }
208
+ }
209
+ console.log();
210
+ if (allRequired && !missingOptional) {
211
+ console.log("✨ All dependencies are installed and ready!");
212
+ }
213
+ else if (allRequired) {
214
+ console.log("✅ All required dependencies are installed!");
215
+ console.log("⚠️ Some optional dependencies are missing but you can still use CI Build.");
216
+ }
217
+ else {
218
+ console.log("❌ Please install the missing required dependencies above.");
219
+ process.exit(1);
220
+ }
221
+ console.log();
222
+ // Determine action: non-interactive flags take priority over interactive prompts
223
+ let action;
224
+ if (opts.importPath) {
225
+ action = "import";
226
+ }
227
+ else if (opts.create) {
228
+ action = "create";
229
+ }
230
+ else {
231
+ // Interactive mode — ask the user
232
+ const response = await prompts({
233
+ type: "select",
234
+ name: "action",
235
+ message: "How would you like to set up your pipeline?",
236
+ choices: [
237
+ { title: "Import an existing YAML pipeline", value: "import" },
238
+ { title: "Create a new pipeline", value: "create" },
239
+ ],
240
+ });
241
+ action = response.action;
242
+ }
243
+ if (!action) {
244
+ console.log("\nCancelled.");
245
+ process.exit(0);
246
+ }
247
+ // Add CI Build runtime files to .gitignore (both create and import flows)
248
+ ensureCiBuildGitignoreEntries(cwd);
249
+ if (action === "create") {
250
+ await handleBuildCommand(detectMobileProjectRoot, { createPipelinesDir: true, nonInteractive: true });
251
+ process.exit(0);
252
+ }
253
+ // Import flow
254
+ let resolvedYaml;
255
+ if (opts.importPath) {
256
+ // Non-interactive: validate the provided path
257
+ const trimmed = opts.importPath.trim();
258
+ if (!existsSync(trimmed)) {
259
+ console.error(`Error: File not found: ${trimmed}`);
260
+ process.exit(1);
261
+ }
262
+ const ext = trimmed.split(".").pop()?.toLowerCase();
263
+ if (ext !== "yml" && ext !== "yaml") {
264
+ console.error("Error: File must be a .yml or .yaml file");
265
+ process.exit(1);
266
+ }
267
+ resolvedYaml = trimmed;
268
+ }
269
+ else {
270
+ // Interactive: ask for the path
271
+ const { yamlPath } = await prompts({
272
+ type: "text",
273
+ name: "yamlPath",
274
+ message: "Enter the absolute path to your YAML pipeline file:",
275
+ validate: (value) => {
276
+ if (!value.trim())
277
+ return "Path is required";
278
+ if (!existsSync(value.trim()))
279
+ return `File not found: ${value.trim()}`;
280
+ const ext = value.trim().split(".").pop()?.toLowerCase();
281
+ if (ext !== "yml" && ext !== "yaml")
282
+ return "File must be a .yml or .yaml file";
283
+ return true;
284
+ },
285
+ });
286
+ if (!yamlPath) {
287
+ console.log("\nCancelled.");
288
+ process.exit(0);
289
+ }
290
+ resolvedYaml = yamlPath.trim();
291
+ }
292
+ const fileName = resolvedYaml.split("/").pop();
293
+ const pipelinesDir = resolve(ciDir, "pipelines");
294
+ const destPath = resolve(pipelinesDir, fileName);
295
+ mkdirSync(pipelinesDir, { recursive: true });
296
+ copyFileSync(resolvedYaml, destPath);
297
+ console.log(`\n✅ Created .ci/pipelines/`);
298
+ console.log(`✅ Copied pipeline: ${fileName}`);
299
+ // Generate GitHub Actions workflow — detect platform from imported YAML if possible
300
+ let importedPlatform = null;
301
+ try {
302
+ const yamlPipeline = loadYAMLPipeline(destPath);
303
+ const meta = yamlPipeline.meta?.["cibuild.io"];
304
+ if (meta?.platform === "ios" || meta?.platform === "android") {
305
+ importedPlatform = meta.platform;
306
+ }
307
+ }
308
+ catch { /* ignore parse errors — default to macos-latest */ }
309
+ generateGitHubActionsWorkflow({ platform: importedPlatform, cwd });
310
+ const hasSecrets = existsSync(resolve(cwd, ".cibuild-secrets.json"));
311
+ const hasWorkflow = existsSync(resolve(cwd, ".github", "workflows", "ci.yml"));
312
+ console.log("\nNext steps:");
313
+ console.log(" ci run .ci/pipelines/cibuild.yml -w primary # Run locally");
314
+ if (hasWorkflow) {
315
+ if (hasSecrets) {
316
+ console.log(" ci secrets upload # Upload secrets to GitHub");
317
+ }
318
+ console.log(" git add . && git push # Push to GitHub - CI runs automatically");
319
+ }
320
+ console.log("");
321
+ }
322
+ /**
323
+ * Shows an interactive workflow picker
324
+ * @param workflows Available workflow names
325
+ * @param pipelinePath Pipeline file path
326
+ * @returns Selected workflow name or undefined if cancelled
327
+ */
328
+ async function promptForWorkflow(workflows) {
329
+ if (workflows.length === 0) {
330
+ console.error("Error: No workflows found in pipeline file");
331
+ return undefined;
332
+ }
333
+ if (workflows.length === 1) {
334
+ // Only one workflow, use it automatically
335
+ console.log(`Using workflow: ${workflows[0]}\n`);
336
+ return workflows[0];
337
+ }
338
+ // Multiple workflows, show interactive picker
339
+ const response = await prompts({
340
+ type: 'select',
341
+ name: 'workflow',
342
+ message: 'Select a workflow to run:',
343
+ choices: workflows.map((name) => ({ title: name, value: name })),
344
+ initial: 0,
345
+ });
346
+ if (!response.workflow) {
347
+ // User cancelled (Ctrl+C)
348
+ console.log('\nWorkflow selection cancelled');
349
+ return undefined;
350
+ }
351
+ console.log(); // Add blank line after selection
352
+ return response.workflow;
353
+ }
354
+ /**
355
+ * Runs pre-execution validation, separates issues into user-fillable (missing env vars)
356
+ * vs hard-blocking (missing commands, bad config), and collects all missing values
357
+ * upfront in a single form-like pass before execution begins.
358
+ *
359
+ * @returns true if it's safe to proceed, false if the user cancelled or hard errors exist
360
+ */
361
+ async function promptForMissingVariables(yamlPipeline, workflowName, config, yamlFilePath) {
362
+ console.log(`\n🔍 Running pre-execution validation...`);
363
+ const validator = new StepValidator(yamlPipeline, workflowName, config, yamlFilePath);
364
+ const result = await validator.validateWorkflow();
365
+ const errorIssues = result.issues.filter((i) => i.requirement.severity === "error" && i.result && !i.result.passed);
366
+ const fillable = errorIssues.filter((i) => i.requirement.timing === "pre-execution" &&
367
+ (i.requirement.category === "environment" || i.requirement.category === "input"));
368
+ const blocking = errorIssues.filter((i) => !(i.requirement.timing === "pre-execution" &&
369
+ (i.requirement.category === "environment" || i.requirement.category === "input")));
370
+ // Hard errors that the user can't fix interactively (missing commands, etc.)
371
+ if (blocking.length > 0) {
372
+ console.log(formatValidationResult(result));
373
+ return false;
374
+ }
375
+ // Warnings and info only — nothing missing
376
+ if (fillable.length === 0) {
377
+ if (result.counts.warnings > 0 || result.counts.info > 0) {
378
+ console.log(formatValidationResult(result));
379
+ }
380
+ else {
381
+ console.log("✅ Pre-execution validation passed\n");
382
+ }
383
+ return true;
384
+ }
385
+ // Show all missing variables upfront
386
+ console.log(`\n${fillable.length} required value(s) missing — provide them to continue:\n`);
387
+ for (const issue of fillable) {
388
+ const stepInfo = issue.stepName ? ` needed by: ${issue.stepName}` : "";
389
+ console.log(` • ${issue.requirement.name.padEnd(30)}${stepInfo}`);
390
+ }
391
+ console.log();
392
+ const handler = new MissingEnvHandler({
393
+ interactive: true,
394
+ workflow: workflowName,
395
+ });
396
+ for (let i = 0; i < fillable.length; i++) {
397
+ const issue = fillable[i];
398
+ const error = new MissingEnvironmentVariableError(issue.requirement.name, issue.stepName, issue.requirement.hint);
399
+ // Patch the counter into the box header label
400
+ const original = console.log;
401
+ const label = `(${i + 1} of ${fillable.length})`;
402
+ let patched = false;
403
+ console.log = (...args) => {
404
+ if (!patched && typeof args[0] === "string" && args[0].includes("MISSING REQUIRED")) {
405
+ original(`║ MISSING REQUIRED ENVIRONMENT VARIABLE ${label.padEnd(28)}║`);
406
+ patched = true;
407
+ return;
408
+ }
409
+ original(...args);
410
+ };
411
+ const handleResult = await handler.handleMissingVariable(error);
412
+ console.log = original;
413
+ if (handleResult.cancelled) {
414
+ return false;
415
+ }
416
+ }
417
+ handler.close();
418
+ return true;
419
+ }
420
+ async function main() {
421
+ const args = process.argv.slice(2);
422
+ if (args[0] === "--version" || args[0] === "-v") {
423
+ console.log(CIBUILD_VERSION);
424
+ process.exit(0);
425
+ }
426
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
427
+ console.log(`
428
+ CI Build - Lightweight CI/CD Pipeline Runner
429
+
430
+ Usage:
431
+ ci init Check dependencies + set up pipeline (interactive)
432
+ ci init --create Check dependencies + create pipeline (non-interactive)
433
+ ci init --import <path> Check dependencies + import YAML pipeline (non-interactive)
434
+ ci build Generate a standard pipeline for the current project
435
+ ci run <path> [-w <name>] Run locally (development mode)
436
+ ci run <path> [-w <name>] --production Run on remote runner (production)
437
+ ci run <path> [-w <name>] --validate-only Validate only, don't execute
438
+ ci run <path> [-w <name>] --skip-validation Skip validation, run with interactive prompts
439
+ ci validate <path> [-w <name>] Validate pipeline (alias for --validate-only)
440
+ ci detect-platform <path> [-w <name>] Detect platform from YAML pipeline
441
+ ci reset [--force] Remove all cibuild files and folders
442
+ ci edit <path> [-w <name>] View pipeline and edit step inputs
443
+ ci secrets add <var_name> <path> [-w <name>] Add a secret (prompted interactively)
444
+ ci secrets add <var_name> <path> --file <file> [-w <name>] Add a secret from a file
445
+ ci secrets upload [--env <name>] [--repo <owner/repo>] [--dry-run] Upload secrets to GitHub environment
446
+ ci --version Show version
447
+ ci --help Show this help
448
+
449
+ Options:
450
+ --workflow, -w <name> Workflow name (YAML only, defaults to first workflow)
451
+ --production Execute on remote runner after validation (vs local)
452
+ --validate-only Run validation only, don't execute pipeline
453
+ --skip-validation Skip pre-execution validation (for development)
454
+ --help, -h Show this help message
455
+
456
+ Supported Formats:
457
+ .ts TypeScript pipeline files
458
+ .yml, .yaml YAML pipeline files
459
+
460
+ Examples:
461
+ ci init # Interactive setup
462
+ ci init --create # Non-interactive: create pipeline
463
+ ci init --import pipeline.yml # Non-interactive: import YAML
464
+ ci build # Generate standard pipeline
465
+ ci run examples/android-build.yml -w pull-request-java-17
466
+ ci run build.pipeline.ts
467
+ ci run ios-build.yml
468
+ ci run android.yml --workflow qa
469
+ ci run multi-platform.yaml -w ios-build
470
+ ci run android.yml --production # Run on remote runner
471
+ ci validate ios-build.yml # Validate only
472
+ ci detect-platform ios-build.yml
473
+ ci secrets add SLACK_CHANNEL bitrise.yml # Add secret (prompted)
474
+ ci secrets add GOOGLE_SERVICES_JSON bitrise.yml --file app/google-services.json
475
+ ci detect-platform multi-platform.yaml -w android
476
+ ci edit android-build.yml # View + edit step inputs
477
+ ci edit android-build.yml -w debug # Edit specific workflow
478
+ `);
479
+ process.exit(0);
480
+ }
481
+ // Handle envman subcommand — delegates to envman CLI
482
+ if (args[0] === "envman") {
483
+ // Re-write process.argv so envman sees its own args
484
+ process.argv = [process.argv[0], "envman", ...args.slice(1)];
485
+ await import("./envman/cli.js");
486
+ return;
487
+ }
488
+ // Handle init command
489
+ if (args[0] === "init") {
490
+ const importIdx = args.indexOf("--import");
491
+ const importPath = importIdx !== -1 ? args[importIdx + 1] : undefined;
492
+ const createFlag = args.includes("--create");
493
+ await handleInitCommand({ importPath, create: createFlag });
494
+ process.exit(0);
495
+ }
496
+ // Handle reset command
497
+ if (args[0] === "reset") {
498
+ const force = args.includes("--force") || args.includes("-f");
499
+ await handleResetCommand({ force });
500
+ process.exit(0);
501
+ }
502
+ // Handle build command
503
+ if (args[0] === "build") {
504
+ await handleBuildCommand(detectMobileProjectRoot);
505
+ process.exit(0);
506
+ }
507
+ // Handle secrets command
508
+ if (args[0] === "secrets") {
509
+ const subCommand = args[1];
510
+ if (subCommand === "upload") {
511
+ const envFlagIdx = args.findIndex((a) => a === '--env' || a === '-e');
512
+ const envName = envFlagIdx !== -1 ? args[envFlagIdx + 1] : 'cibuild';
513
+ const repoFlagIdx = args.findIndex((a) => a === '--repo' || a === '-r');
514
+ const repo = repoFlagIdx !== -1 ? args[repoFlagIdx + 1] : undefined;
515
+ const dryRun = args.includes('--dry-run');
516
+ await handleSecretsUploadCommand({ envName, repo, dryRun });
517
+ process.exit(0);
518
+ }
519
+ if (subCommand === "sync-workflow") {
520
+ const pathFlagIdx = args.findIndex((a) => a === '--path' || a === '-p');
521
+ let workflowPath = pathFlagIdx !== -1 ? args[pathFlagIdx + 1] : undefined;
522
+ if (!workflowPath) {
523
+ // Auto-discover workflow files
524
+ const ghWorkflowDir = resolve(process.cwd(), '.github', 'workflows');
525
+ const discoveredFiles = [];
526
+ if (existsSync(ghWorkflowDir)) {
527
+ try {
528
+ const entries = readdirSync(ghWorkflowDir);
529
+ for (const entry of entries) {
530
+ if (entry.endsWith('.yml') || entry.endsWith('.yaml')) {
531
+ discoveredFiles.push(`.github/workflows/${entry}`);
532
+ }
533
+ }
534
+ }
535
+ catch { /* ignore read errors */ }
536
+ }
537
+ const choices = discoveredFiles.map((f) => ({
538
+ title: f,
539
+ value: f,
540
+ }));
541
+ choices.push({ title: 'Enter path manually…', value: '__manual__' });
542
+ if (choices.length === 1) {
543
+ // Only the manual option — no files found
544
+ console.log('No workflow files found in .github/workflows/\n');
545
+ }
546
+ const { selected } = await prompts({
547
+ type: 'select',
548
+ name: 'selected',
549
+ message: 'Select a workflow file to sync secrets into:',
550
+ choices,
551
+ });
552
+ if (!selected) {
553
+ process.exit(0);
554
+ }
555
+ if (selected === '__manual__') {
556
+ const { manualPath } = await prompts({
557
+ type: 'text',
558
+ name: 'manualPath',
559
+ message: 'Enter the path to the workflow YAML file:',
560
+ validate: (v) => v.trim() ? true : 'Path is required',
561
+ });
562
+ if (!manualPath) {
563
+ process.exit(0);
564
+ }
565
+ workflowPath = manualPath;
566
+ }
567
+ else {
568
+ workflowPath = selected;
569
+ }
570
+ }
571
+ const dryRun = args.includes('--dry-run');
572
+ await handleSecretsSyncWorkflowCommand({ workflowPath: resolve(process.cwd(), workflowPath), dryRun });
573
+ process.exit(0);
574
+ }
575
+ if (subCommand !== "add") {
576
+ console.error("Error: Unknown secrets subcommand. Available: add, upload, sync-workflow");
577
+ console.error("Usage: ci secrets add <var_name> <pipeline_path> [--file <file>] [--workflow <name>]");
578
+ console.error(" ci secrets upload [--env <name>] [--repo <owner/repo>] [--dry-run]");
579
+ console.error(" ci secrets sync-workflow --path <workflow.yml> [--dry-run]");
580
+ process.exit(1);
581
+ }
582
+ // ci secrets add <var_name> <pipeline_path> [--file <file>] [--workflow <name>]
583
+ const varName = args[2];
584
+ const pipelinePath = args[3];
585
+ if (!varName || !pipelinePath) {
586
+ console.error("Error: Variable name and pipeline path are required");
587
+ console.error("Usage: ci secrets add <var_name> <pipeline_path> [--file <file>] [-w <workflow>]");
588
+ console.error("\nExamples:");
589
+ console.error(" ci secrets add SLACK_CHANNEL bitrise.yml # prompted interactively");
590
+ console.error(" ci secrets add GOOGLE_SERVICES_JSON bitrise.yml --file google-services.json # read from file");
591
+ process.exit(1);
592
+ }
593
+ const resolvedPath = resolve(process.cwd(), pipelinePath);
594
+ if (!existsSync(resolvedPath)) {
595
+ console.error(`Error: Pipeline file not found: ${resolvedPath}`);
596
+ process.exit(1);
597
+ }
598
+ // Only works with YAML files
599
+ const fileExt = extname(resolvedPath).toLowerCase();
600
+ const isYAML = fileExt === '.yml' || fileExt === '.yaml';
601
+ if (!isYAML) {
602
+ console.error("Error: secrets command only works with YAML pipeline files (.yml, .yaml)");
603
+ process.exit(1);
604
+ }
605
+ // Parse --file / -f flag
606
+ const fileFlagIdx = args.findIndex((a) => a === '--file' || a === '-f');
607
+ const secretFilePath = fileFlagIdx !== -1 ? args[fileFlagIdx + 1] : undefined;
608
+ // Parse -w / --workflow flag
609
+ const workflowFlagIdx = args.findIndex((a) => a === '-w' || a === '--workflow');
610
+ const workflowArg = workflowFlagIdx !== -1 ? args[workflowFlagIdx + 1] : undefined;
611
+ try {
612
+ const secretsManager = new SecretsManager();
613
+ console.log('\n╔═══════════════════════════════════════════════════════════════════╗');
614
+ console.log('║ ADD SECRET ║');
615
+ console.log('╚═══════════════════════════════════════════════════════════════════╝\n');
616
+ console.log(`Variable: ${varName}`);
617
+ console.log(`Location: ${secretsManager.getSecretsFilePath()}`);
618
+ // Check if this variable already exists
619
+ const existingSecretId = secretsManager.getSecretIdByName(varName);
620
+ if (existingSecretId) {
621
+ console.log(`\nℹ️ This variable already exists in your secrets file.`);
622
+ const confirmResponse = await prompts({
623
+ type: 'confirm',
624
+ name: 'value',
625
+ message: 'Update existing value?',
626
+ initial: true,
627
+ });
628
+ if (!confirmResponse.value) {
629
+ console.log('\n❌ Cancelled');
630
+ process.exit(0);
631
+ }
632
+ }
633
+ let secretValue;
634
+ if (secretFilePath) {
635
+ // Read value from file
636
+ const resolvedFilePath = resolve(process.cwd(), secretFilePath);
637
+ if (!existsSync(resolvedFilePath)) {
638
+ console.error(`\nError: File not found: ${resolvedFilePath}`);
639
+ process.exit(1);
640
+ }
641
+ secretValue = readFileSync(resolvedFilePath, 'utf-8');
642
+ console.log(`\nReading value from: ${secretFilePath}`);
643
+ }
644
+ else {
645
+ // Collect value interactively via readline (handles multiline paste cleanly)
646
+ const { createInterface } = await import('node:readline');
647
+ console.log(`\n Enter value for ${varName}`);
648
+ console.log(' Paste content, then type . on its own line and press Enter:\n');
649
+ if (process.stdin.isTTY) {
650
+ try {
651
+ process.stdin.setRawMode(false);
652
+ }
653
+ catch { /* ignore */ }
654
+ }
655
+ process.stdin.resume();
656
+ secretValue = await new Promise((res) => {
657
+ const rl = createInterface({ input: process.stdin, terminal: false });
658
+ const lines = [];
659
+ rl.on('line', (line) => {
660
+ if (line.trim() === '.') {
661
+ rl.close();
662
+ return;
663
+ }
664
+ lines.push(line);
665
+ });
666
+ rl.once('close', () => { process.stdin.pause(); res(lines.join('\n')); });
667
+ rl.on('SIGINT', () => { rl.close(); res(''); });
668
+ });
669
+ if (!secretValue.trim()) {
670
+ console.log('\n❌ Cancelled');
671
+ process.exit(0);
672
+ }
673
+ }
674
+ secretsManager.storeSecret(varName, secretValue, workflowArg);
675
+ console.log(`\n✅ Secret stored: ${varName}${workflowArg ? ` (workflow: ${workflowArg})` : ' (global)'}`);
676
+ console.log(` Location: ${secretsManager.getSecretsFilePath()}`);
677
+ console.log(`\n💡 This value will be loaded automatically when you run the pipeline.\n`);
678
+ process.exit(0);
679
+ }
680
+ catch (error) {
681
+ console.error("\n✗ Failed to add secret:");
682
+ console.error(error instanceof Error ? error.message : String(error));
683
+ process.exit(1);
684
+ }
685
+ }
686
+ // Handle edit command
687
+ if (args[0] === "edit") {
688
+ const pipelineIndex = args.findIndex((arg) => arg === "--pipeline" || arg === "-p");
689
+ let pipelinePath;
690
+ if (pipelineIndex !== -1 && args[pipelineIndex + 1]) {
691
+ pipelinePath = resolve(process.cwd(), args[pipelineIndex + 1]);
692
+ }
693
+ else if (args[1] && !args[1].startsWith("-")) {
694
+ pipelinePath = resolve(process.cwd(), args[1]);
695
+ }
696
+ else {
697
+ console.error("Error: Pipeline path is required");
698
+ console.error("Usage: ci edit <path> [-w <name>]");
699
+ process.exit(1);
700
+ }
701
+ const workflowIndex = args.findIndex((arg) => arg === "--workflow" || arg === "-w");
702
+ const workflowName = workflowIndex !== -1 && args[workflowIndex + 1]
703
+ ? args[workflowIndex + 1]
704
+ : undefined;
705
+ if (!existsSync(pipelinePath)) {
706
+ console.error(`Error: Pipeline file not found: ${pipelinePath}`);
707
+ process.exit(1);
708
+ }
709
+ const fileExt = extname(pipelinePath).toLowerCase();
710
+ if (fileExt !== ".yml" && fileExt !== ".yaml") {
711
+ console.error("Error: edit only works with YAML pipeline files (.yml, .yaml)");
712
+ process.exit(1);
713
+ }
714
+ try {
715
+ await handleEditCommand(pipelinePath, workflowName);
716
+ process.exit(0);
717
+ }
718
+ catch (error) {
719
+ console.error("\n✗ Edit failed:");
720
+ console.error(error instanceof Error ? error.message : String(error));
721
+ process.exit(1);
722
+ }
723
+ }
724
+ // Check for validate command or --validate-only flag
725
+ const isValidateCommand = args[0] === "validate";
726
+ const hasValidateOnlyFlag = args.includes("--validate-only");
727
+ if (isValidateCommand || (args[0] === "run" && hasValidateOnlyFlag)) {
728
+ // Support both: ci validate <path> and ci validate --pipeline <path>
729
+ const pipelineIndex = args.findIndex((arg) => arg === "--pipeline" || arg === "-p");
730
+ let pipelinePath;
731
+ if (pipelineIndex !== -1 && args[pipelineIndex + 1]) {
732
+ pipelinePath = resolve(process.cwd(), args[pipelineIndex + 1]);
733
+ }
734
+ else if (args[1] && !args[1].startsWith("-")) {
735
+ // Second argument is the path (e.g., ci validate <path>)
736
+ pipelinePath = resolve(process.cwd(), args[1]);
737
+ }
738
+ else {
739
+ console.error("Error: Pipeline path is required");
740
+ console.error("Usage: ci validate <path> [--workflow <name>]");
741
+ console.error(" or: ci validate --pipeline <path> [--workflow <name>]");
742
+ process.exit(1);
743
+ }
744
+ const workflowIndex = args.findIndex((arg) => arg === "--workflow" || arg === "-w");
745
+ const workflowName = workflowIndex !== -1 && args[workflowIndex + 1]
746
+ ? args[workflowIndex + 1]
747
+ : undefined;
748
+ if (!existsSync(pipelinePath)) {
749
+ console.error(`Error: Pipeline file not found: ${pipelinePath}`);
750
+ process.exit(1);
751
+ }
752
+ const fileExt = extname(pipelinePath).toLowerCase();
753
+ const isYAML = fileExt === '.yml' || fileExt === '.yaml';
754
+ if (!isYAML) {
755
+ console.error("Error: validate only works with YAML pipeline files (.yml, .yaml)");
756
+ process.exit(1);
757
+ }
758
+ try {
759
+ const config = loadConfig();
760
+ const yamlPipeline = loadYAMLPipeline(pipelinePath);
761
+ // Prompt for workflow if not specified
762
+ let selectedWorkflow = workflowName;
763
+ if (!selectedWorkflow) {
764
+ const workflows = Object.keys(yamlPipeline.workflows);
765
+ selectedWorkflow = await promptForWorkflow(workflows);
766
+ if (!selectedWorkflow) {
767
+ process.exit(1);
768
+ }
769
+ }
770
+ console.log(`🔍 Validating workflow: ${selectedWorkflow}`);
771
+ console.log(` Pipeline: ${pipelinePath}\n`);
772
+ const validator = new StepValidator(yamlPipeline, selectedWorkflow, config, pipelinePath);
773
+ const result = await validator.validateWorkflow();
774
+ console.log(formatValidationResult(result));
775
+ process.exit(result.valid ? 0 : 1);
776
+ }
777
+ catch (error) {
778
+ console.error("\n✗ Validation failed:");
779
+ console.error(error instanceof Error ? error.message : String(error));
780
+ process.exit(1);
781
+ }
782
+ }
783
+ if (args[0] === "detect-platform") {
784
+ // Support both: ci detect-platform <path> and ci detect-platform --pipeline <path>
785
+ const pipelineIndex = args.findIndex((arg) => arg === "--pipeline" || arg === "-p");
786
+ let pipelinePath;
787
+ if (pipelineIndex !== -1 && args[pipelineIndex + 1]) {
788
+ pipelinePath = resolve(process.cwd(), args[pipelineIndex + 1]);
789
+ }
790
+ else if (args[1] && !args[1].startsWith("-")) {
791
+ // Second argument is the path (e.g., ci detect-platform <path>)
792
+ pipelinePath = resolve(process.cwd(), args[1]);
793
+ }
794
+ else {
795
+ console.error("Error: Pipeline path is required");
796
+ console.error("Usage: ci detect-platform <path> [--workflow <name>]");
797
+ console.error(" or: ci detect-platform --pipeline <path> [--workflow <name>]");
798
+ process.exit(1);
799
+ }
800
+ // Check for --workflow parameter (optional)
801
+ const workflowIndex = args.findIndex((arg) => arg === "--workflow" || arg === "-w");
802
+ const workflowName = workflowIndex !== -1 && args[workflowIndex + 1]
803
+ ? args[workflowIndex + 1]
804
+ : undefined;
805
+ if (!existsSync(pipelinePath)) {
806
+ console.error(`Error: Pipeline file not found: ${pipelinePath}`);
807
+ process.exit(1);
808
+ }
809
+ // Only works with YAML files
810
+ const fileExt = extname(pipelinePath).toLowerCase();
811
+ const isYAML = fileExt === '.yml' || fileExt === '.yaml';
812
+ if (!isYAML) {
813
+ console.error("Error: detect-platform only works with YAML pipeline files (.yml, .yaml)");
814
+ process.exit(1);
815
+ }
816
+ try {
817
+ const yamlPipeline = loadYAMLPipeline(pipelinePath);
818
+ // Prompt for workflow if not specified
819
+ let selectedWorkflow = workflowName;
820
+ if (!selectedWorkflow) {
821
+ const workflows = Object.keys(yamlPipeline.workflows);
822
+ selectedWorkflow = await promptForWorkflow(workflows);
823
+ if (!selectedWorkflow) {
824
+ process.exit(1);
825
+ }
826
+ }
827
+ const platformInfo = detectPlatformInfo(yamlPipeline, selectedWorkflow);
828
+ console.log(`\nPlatform Detection Results:`);
829
+ console.log(`─────────────────────────────`);
830
+ console.log(`Pipeline: ${pipelinePath}`);
831
+ console.log(`Workflow: ${selectedWorkflow}`);
832
+ console.log(`Platform: ${platformInfo.platform}`);
833
+ console.log(`Stack: ${platformInfo.stack || 'N/A'}`);
834
+ console.log(`Machine Type: ${platformInfo.machineType || 'N/A'}`);
835
+ console.log();
836
+ process.exit(0);
837
+ }
838
+ catch (error) {
839
+ console.error("\n✗ Platform detection failed:");
840
+ console.error(error instanceof Error ? error.message : String(error));
841
+ process.exit(1);
842
+ }
843
+ }
844
+ else if (args[0] === "run") {
845
+ // Support both: ci run <path> and ci run --pipeline <path>
846
+ const pipelineIndex = args.findIndex((arg) => arg === "--pipeline" || arg === "-p");
847
+ let pipelinePath;
848
+ if (pipelineIndex !== -1 && args[pipelineIndex + 1]) {
849
+ pipelinePath = resolve(process.cwd(), args[pipelineIndex + 1]);
850
+ }
851
+ else if (args[1] && !args[1].startsWith("-")) {
852
+ // Second argument is the path (e.g., ci run <path>)
853
+ pipelinePath = resolve(process.cwd(), args[1]);
854
+ }
855
+ else {
856
+ // No path given — check for .ci/pipelines/
857
+ const pipelinesDir = resolve(process.cwd(), ".ci", "pipelines");
858
+ if (!existsSync(pipelinesDir)) {
859
+ console.error("Project not initialised. Run 'ci init' first.");
860
+ process.exit(1);
861
+ }
862
+ const yamlFiles = readdirSync(pipelinesDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
863
+ if (yamlFiles.length === 0) {
864
+ console.error("No pipelines found in .ci/pipelines/. Run 'ci init' to import one.");
865
+ process.exit(1);
866
+ }
867
+ if (yamlFiles.length === 1) {
868
+ pipelinePath = resolve(pipelinesDir, yamlFiles[0]);
869
+ console.log(`Using pipeline: ${yamlFiles[0]}`);
870
+ }
871
+ else {
872
+ const { selected } = await prompts({
873
+ type: "select",
874
+ name: "selected",
875
+ message: "Select a pipeline to run:",
876
+ choices: yamlFiles.map((f) => ({ title: f, value: resolve(pipelinesDir, f) })),
877
+ });
878
+ if (!selected) {
879
+ console.log("Cancelled.");
880
+ process.exit(0);
881
+ }
882
+ pipelinePath = selected;
883
+ }
884
+ }
885
+ // Check for --workflow parameter (optional, only used for YAML)
886
+ const workflowIndex = args.findIndex((arg) => arg === "--workflow" || arg === "-w");
887
+ const workflowName = workflowIndex !== -1 && args[workflowIndex + 1]
888
+ ? args[workflowIndex + 1]
889
+ : undefined;
890
+ // Check for --production, --skip-validation, and --local flags
891
+ const isProduction = args.includes("--production");
892
+ const skipValidation = args.includes("--skip-validation");
893
+ const isLocal = args.includes("--local");
894
+ if (!existsSync(pipelinePath)) {
895
+ console.error(`Error: Pipeline file not found: ${pipelinePath}`);
896
+ process.exit(1);
897
+ }
898
+ try {
899
+ const config = loadConfig();
900
+ config.local = isLocal;
901
+ const runner = new PipelineRunner(config);
902
+ // Detect file type based on extension
903
+ const fileExt = extname(pipelinePath).toLowerCase();
904
+ const isYAML = fileExt === '.yml' || fileExt === '.yaml';
905
+ const isJavaScriptModule = fileExt === '.ts' || fileExt === '.js';
906
+ let pipeline;
907
+ if (isYAML) {
908
+ // Load YAML pipeline
909
+ console.log(`Loading YAML pipeline: ${pipelinePath}`);
910
+ const yamlPipeline = loadYAMLPipeline(pipelinePath);
911
+ // Prompt for workflow if not specified
912
+ let selectedWorkflow = workflowName;
913
+ if (!selectedWorkflow) {
914
+ const workflows = Object.keys(yamlPipeline.workflows);
915
+ selectedWorkflow = await promptForWorkflow(workflows);
916
+ if (!selectedWorkflow) {
917
+ process.exit(1);
918
+ }
919
+ }
920
+ else {
921
+ console.log(`Using workflow: ${workflowName}`);
922
+ }
923
+ // Run pre-execution validation and collect missing variables (unless skipped)
924
+ if (skipValidation) {
925
+ console.log(`\n⏭️ Skipping validation (--skip-validation flag set)\n`);
926
+ }
927
+ else {
928
+ const canProceed = await promptForMissingVariables(yamlPipeline, selectedWorkflow, config, pipelinePath);
929
+ if (!canProceed) {
930
+ console.error("\n✗ Pipeline cannot run. Fix the errors above before continuing.");
931
+ process.exit(1);
932
+ }
933
+ }
934
+ // Check if production mode
935
+ if (isProduction) {
936
+ console.log("═".repeat(60));
937
+ console.log("🚀 PRODUCTION MODE");
938
+ console.log("═".repeat(60));
939
+ console.log("\nValidation passed. Pipeline would be dispatched to remote runner.");
940
+ console.log("(Remote runner integration not yet implemented)\n");
941
+ console.log("For now, use without --production flag to run locally.\n");
942
+ process.exit(0);
943
+ }
944
+ // Convert and run locally (development mode)
945
+ const conversionResult = await convertYAMLWithSecrets(yamlPipeline, {
946
+ config,
947
+ workflowName: selectedWorkflow,
948
+ yamlFilePath: pipelinePath,
949
+ interactive: true,
950
+ });
951
+ pipeline = conversionResult.pipeline;
952
+ // Run pipeline with warnings
953
+ await runner.runPipeline(pipeline, conversionResult.warnings, conversionResult.skippedSteps);
954
+ }
955
+ else if (isJavaScriptModule) {
956
+ // Load TypeScript/JavaScript pipeline module
957
+ console.log(`Loading pipeline: ${pipelinePath}`);
958
+ const pipelineUrl = pathToFileURL(pipelinePath).href;
959
+ const pipelineModule = await import(pipelineUrl);
960
+ pipeline = pipelineModule.default;
961
+ if (!pipeline || typeof pipeline !== "object") {
962
+ throw new Error("Pipeline file must export a default PipelineDef object");
963
+ }
964
+ // Run pipeline without warnings (TypeScript pipelines don't have warnings)
965
+ await runner.runPipeline(pipeline);
966
+ }
967
+ else {
968
+ throw new Error(`Unsupported pipeline file format: ${fileExt}\n` +
969
+ `Supported formats: .yml, .yaml, .ts, .js`);
970
+ }
971
+ console.log("\n✓ All steps completed successfully!\n");
972
+ process.exit(0);
973
+ }
974
+ catch (error) {
975
+ console.error("\n✗ Pipeline failed:");
976
+ console.error(error instanceof Error ? error.message : String(error));
977
+ process.exit(1);
978
+ }
979
+ }
980
+ else {
981
+ console.error(`Unknown command: ${args[0]}`);
982
+ console.error("Run 'ci --help' for usage information");
983
+ process.exit(1);
984
+ }
985
+ }
986
+ main();
987
+ //# sourceMappingURL=cli.js.map