@invarn/cibuild 1.4.1 → 1.4.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.
package/dist/src/cli.js CHANGED
@@ -1,439 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { resolve, extname } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import { existsSync, readdirSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, appendFileSync, statSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import { execSync } from "node:child_process";
3
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
7
4
  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
5
  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
6
  import "./yaml/steps/index.js"; // Initialize step registry
18
7
  import { handleBuildCommand } from "./commands/build.js";
19
8
  import { handleEditCommand } from "./commands/edit.js";
20
9
  import { handleSecretsUploadCommand } from "./commands/secrets-upload.js";
21
10
  import { handleSecretsSyncWorkflowCommand } from "./commands/secrets-sync-workflow.js";
22
11
  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/KMM: 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
- // KMM: Gradle project with an iosApp/ directory or shared/ multiplatform module
39
- // Must check before plain Android since KMM also has Gradle files
40
- if (hasAndroidIndicator) {
41
- const kmmIndicators = ["iosApp", "shared", "composeApp"];
42
- const hasKmmIndicator = kmmIndicators.some((d) => {
43
- const fullPath = resolve(dir, d);
44
- try {
45
- return existsSync(fullPath) && statSync(fullPath).isDirectory();
46
- }
47
- catch {
48
- return false;
49
- }
50
- });
51
- if (hasKmmIndicator) {
52
- return "kmm";
53
- }
54
- return "android";
55
- }
56
- // iOS: must have a .xcodeproj or .xcworkspace directory, or a Podfile
57
- const iosFileIndicators = ["Podfile"];
58
- const hasIosFile = iosFileIndicators.some((f) => existsSync(resolve(dir, f)));
59
- if (!hasIosFile) {
60
- // Check for .xcodeproj / .xcworkspace directories
61
- try {
62
- const entries = readdirSync(dir);
63
- const hasXcodeDir = entries.some((e) => e.endsWith(".xcodeproj") || e.endsWith(".xcworkspace"));
64
- if (hasXcodeDir) {
65
- return "ios";
66
- }
67
- }
68
- catch {
69
- // If we can't read the directory, fall through
70
- }
71
- }
72
- else {
73
- return "ios";
74
- }
75
- return null;
76
- }
77
- /**
78
- * Ensures CI Build runtime files are listed in .gitignore.
79
- * Safe to call multiple times — only appends entries that are missing.
80
- */
81
- function ensureCiBuildGitignoreEntries(cwd) {
82
- const gitignorePath = resolve(cwd, ".gitignore");
83
- const gitignoreEntries = [".cibuild-secrets.json", ".ci/.envstore.json", ".ci/keys/", "build/"];
84
- if (existsSync(gitignorePath)) {
85
- const contents = readFileSync(gitignorePath, "utf-8");
86
- const lines = contents.split("\n").map((l) => l.trim());
87
- const toAdd = gitignoreEntries.filter((e) => !lines.includes(e));
88
- if (toAdd.length > 0) {
89
- appendFileSync(gitignorePath, `\n${toAdd.join("\n")}\n`);
90
- for (const entry of toAdd) {
91
- console.log(`✅ Added ${entry} to .gitignore`);
92
- }
93
- }
94
- }
95
- else {
96
- writeFileSync(gitignorePath, `${gitignoreEntries.join("\n")}\n`);
97
- console.log(`✅ Created .gitignore with CI Build entries`);
98
- }
99
- }
100
- async function handleInitCommand(opts = {}) {
101
- const cwd = process.cwd();
102
- // Verify we are inside a mobile project root before doing anything
103
- const projectType = detectMobileProjectRoot(cwd);
104
- if (!projectType) {
105
- console.error("Error: Not a mobile project root.");
106
- console.error("ci init must be run from the root folder of an Android, iOS, or KMM project.");
107
- console.error("\nAndroid/KMM projects must contain one of:");
108
- console.error(" build.gradle, build.gradle.kts, settings.gradle, settings.gradle.kts, gradlew");
109
- console.error("\nKMM projects additionally need: iosApp/, shared/, or composeApp/ directory");
110
- console.error("\niOS projects must contain one of:");
111
- console.error(" *.xcodeproj, *.xcworkspace, Podfile");
112
- process.exit(1);
113
- }
114
- // Bail out early if .ci already exists and has content
115
- const ciDir = resolve(cwd, ".ci");
116
- if (existsSync(ciDir)) {
117
- const entries = readdirSync(ciDir);
118
- if (entries.length > 0) {
119
- console.error("Already initialised. Run 'ci run' to execute your pipeline.");
120
- process.exit(1);
121
- }
122
- }
123
- const projectTypeLabel = projectType === "kmm" ? "KMM (Kotlin Multiplatform)" : projectType === "android" ? "Android" : "iOS";
124
- console.log(`\nDetected project type: ${projectTypeLabel}`);
125
- console.log(`
126
- ╔══════════════════════════════════════════════════════════════╗
127
- ║ CI Build - Dependency Check ║
128
- ╚══════════════════════════════════════════════════════════════╝
129
- `);
130
- const commonDependencies = [
131
- {
132
- name: "Git",
133
- command: "git --version",
134
- required: true,
135
- installInstructions: "Download from https://git-scm.com/"
136
- }
137
- ];
138
- const androidDependencies = [
139
- {
140
- name: "Java (JDK)",
141
- command: "java -version 2>&1 | head -1",
142
- required: true,
143
- installInstructions: "Download from https://adoptium.net/ or install via brew: brew install --cask temurin"
144
- },
145
- {
146
- name: "Android SDK",
147
- customCheck: () => {
148
- const fromEnv = process.env.ANDROID_HOME;
149
- if (fromEnv && existsSync(fromEnv)) {
150
- return { found: true, message: `${fromEnv} (via ANDROID_HOME)` };
151
- }
152
- const commonPaths = [
153
- `${homedir()}/Library/Android/sdk`, // macOS
154
- `${homedir()}/Android/Sdk`, // Linux
155
- "/usr/local/android-sdk",
156
- ];
157
- const detected = commonPaths.find((p) => existsSync(p));
158
- if (detected) {
159
- return { found: true, message: `${detected} (auto-detected)` };
160
- }
161
- return { found: false, message: "Android SDK not found" };
162
- },
163
- required: true,
164
- installInstructions: "Install Android Studio from https://developer.android.com/studio or set ANDROID_HOME manually"
165
- }
166
- ];
167
- const iosDependencies = [
168
- {
169
- name: "Xcode",
170
- command: "xcodebuild -version 2>&1 | head -1",
171
- required: true,
172
- installInstructions: "Install Xcode from the Mac App Store"
173
- },
174
- {
175
- name: "CocoaPods",
176
- command: "pod --version",
177
- required: false,
178
- installInstructions: "Run: sudo gem install cocoapods"
179
- }
180
- ];
181
- const dependencies = [
182
- ...commonDependencies,
183
- ...(projectType === "android" ? androidDependencies : projectType === "kmm" ? [...androidDependencies, ...iosDependencies] : iosDependencies),
184
- ];
185
- let allRequired = true;
186
- let missingOptional = false;
187
- for (const dep of dependencies) {
188
- if (dep.customCheck) {
189
- const result = dep.customCheck();
190
- if (result.found) {
191
- console.log(`✅ ${dep.name.padEnd(20)} ${result.message}`);
192
- }
193
- else if (dep.required) {
194
- console.log(`❌ ${dep.name.padEnd(20)} Not found (REQUIRED)`);
195
- console.log(` Install: ${dep.installInstructions}`);
196
- allRequired = false;
197
- }
198
- else {
199
- console.log(`⚠️ ${dep.name.padEnd(20)} Not found (optional)`);
200
- console.log(` Install: ${dep.installInstructions}`);
201
- missingOptional = true;
202
- }
203
- continue;
204
- }
205
- try {
206
- const output = execSync(dep.command, {
207
- encoding: "utf-8",
208
- stdio: ["pipe", "pipe", "pipe"],
209
- shell: "/bin/sh",
210
- }).trim();
211
- console.log(`✅ ${dep.name.padEnd(20)} ${output}`);
212
- }
213
- catch {
214
- if (dep.required) {
215
- console.log(`❌ ${dep.name.padEnd(20)} Not found (REQUIRED)`);
216
- console.log(` Install: ${dep.installInstructions}`);
217
- allRequired = false;
218
- }
219
- else {
220
- console.log(`⚠️ ${dep.name.padEnd(20)} Not found (optional)`);
221
- console.log(` Install: ${dep.installInstructions}`);
222
- missingOptional = true;
223
- }
224
- }
225
- }
226
- console.log();
227
- if (allRequired && !missingOptional) {
228
- console.log("✨ All dependencies are installed and ready!");
229
- }
230
- else if (allRequired) {
231
- console.log("✅ All required dependencies are installed!");
232
- console.log("⚠️ Some optional dependencies are missing but you can still use CI Build.");
233
- }
234
- else {
235
- console.log("❌ Please install the missing required dependencies above.");
236
- process.exit(1);
237
- }
238
- console.log();
239
- // Determine action: non-interactive flags take priority over interactive prompts
240
- let action;
241
- if (opts.importPath) {
242
- action = "import";
243
- }
244
- else if (opts.create) {
245
- action = "create";
246
- }
247
- else {
248
- // Interactive mode — ask the user
249
- const response = await prompts({
250
- type: "select",
251
- name: "action",
252
- message: "How would you like to set up your pipeline?",
253
- choices: [
254
- { title: "Import an existing YAML pipeline", value: "import" },
255
- { title: "Create a new pipeline", value: "create" },
256
- ],
257
- });
258
- action = response.action;
259
- }
260
- if (!action) {
261
- console.log("\nCancelled.");
262
- process.exit(0);
263
- }
264
- // Add CI Build runtime files to .gitignore (both create and import flows)
265
- ensureCiBuildGitignoreEntries(cwd);
266
- if (action === "create") {
267
- await handleBuildCommand(detectMobileProjectRoot, { createPipelinesDir: true, nonInteractive: true });
268
- process.exit(0);
269
- }
270
- // Import flow
271
- let resolvedYaml;
272
- if (opts.importPath) {
273
- // Non-interactive: validate the provided path
274
- const trimmed = opts.importPath.trim();
275
- if (!existsSync(trimmed)) {
276
- console.error(`Error: File not found: ${trimmed}`);
277
- process.exit(1);
278
- }
279
- const ext = trimmed.split(".").pop()?.toLowerCase();
280
- if (ext !== "yml" && ext !== "yaml") {
281
- console.error("Error: File must be a .yml or .yaml file");
282
- process.exit(1);
283
- }
284
- resolvedYaml = trimmed;
285
- }
286
- else {
287
- // Interactive: ask for the path
288
- const { yamlPath } = await prompts({
289
- type: "text",
290
- name: "yamlPath",
291
- message: "Enter the absolute path to your YAML pipeline file:",
292
- validate: (value) => {
293
- if (!value.trim())
294
- return "Path is required";
295
- if (!existsSync(value.trim()))
296
- return `File not found: ${value.trim()}`;
297
- const ext = value.trim().split(".").pop()?.toLowerCase();
298
- if (ext !== "yml" && ext !== "yaml")
299
- return "File must be a .yml or .yaml file";
300
- return true;
301
- },
302
- });
303
- if (!yamlPath) {
304
- console.log("\nCancelled.");
305
- process.exit(0);
306
- }
307
- resolvedYaml = yamlPath.trim();
308
- }
309
- const fileName = resolvedYaml.split("/").pop();
310
- const pipelinesDir = resolve(ciDir, "pipelines");
311
- const destPath = resolve(pipelinesDir, fileName);
312
- mkdirSync(pipelinesDir, { recursive: true });
313
- copyFileSync(resolvedYaml, destPath);
314
- console.log(`\n✅ Created .ci/pipelines/`);
315
- console.log(`✅ Copied pipeline: ${fileName}`);
316
- // Generate GitHub Actions workflow — detect platform from imported YAML if possible
317
- let importedPlatform = null;
318
- try {
319
- const yamlPipeline = loadYAMLPipeline(destPath);
320
- const meta = yamlPipeline.meta?.["cibuild.io"];
321
- if (meta?.platform === "ios" || meta?.platform === "android" || meta?.platform === "kmm") {
322
- importedPlatform = meta.platform === "kmm" ? "ios" : meta.platform;
323
- }
324
- }
325
- catch { /* ignore parse errors — default to macos-latest */ }
326
- generateGitHubActionsWorkflow({ platform: importedPlatform, cwd });
327
- const hasSecrets = existsSync(resolve(cwd, ".cibuild-secrets.json"));
328
- const hasWorkflow = existsSync(resolve(cwd, ".github", "workflows", "ci.yml"));
329
- console.log("\nNext steps:");
330
- console.log(" ci run .ci/pipelines/cibuild.yml -w primary # Run locally");
331
- if (hasWorkflow) {
332
- if (hasSecrets) {
333
- console.log(" ci secrets upload # Upload secrets to GitHub");
334
- }
335
- console.log(" git add . && git push # Push to GitHub - CI runs automatically");
336
- }
337
- console.log("");
338
- }
339
- /**
340
- * Shows an interactive workflow picker
341
- * @param workflows Available workflow names
342
- * @param pipelinePath Pipeline file path
343
- * @returns Selected workflow name or undefined if cancelled
344
- */
345
- async function promptForWorkflow(workflows) {
346
- if (workflows.length === 0) {
347
- console.error("Error: No workflows found in pipeline file");
348
- return undefined;
349
- }
350
- if (workflows.length === 1) {
351
- // Only one workflow, use it automatically
352
- console.log(`Using workflow: ${workflows[0]}\n`);
353
- return workflows[0];
354
- }
355
- // Multiple workflows, show interactive picker
356
- const response = await prompts({
357
- type: 'select',
358
- name: 'workflow',
359
- message: 'Select a workflow to run:',
360
- choices: workflows.map((name) => ({ title: name, value: name })),
361
- initial: 0,
362
- });
363
- if (!response.workflow) {
364
- // User cancelled (Ctrl+C)
365
- console.log('\nWorkflow selection cancelled');
366
- return undefined;
367
- }
368
- console.log(); // Add blank line after selection
369
- return response.workflow;
370
- }
371
- /**
372
- * Runs pre-execution validation, separates issues into user-fillable (missing env vars)
373
- * vs hard-blocking (missing commands, bad config), and collects all missing values
374
- * upfront in a single form-like pass before execution begins.
375
- *
376
- * @returns true if it's safe to proceed, false if the user cancelled or hard errors exist
377
- */
378
- async function promptForMissingVariables(yamlPipeline, workflowName, config, yamlFilePath) {
379
- console.log(`\n🔍 Running pre-execution validation...`);
380
- const validator = new StepValidator(yamlPipeline, workflowName, config, yamlFilePath);
381
- const result = await validator.validateWorkflow();
382
- const errorIssues = result.issues.filter((i) => i.requirement.severity === "error" && i.result && !i.result.passed);
383
- const fillable = errorIssues.filter((i) => i.requirement.timing === "pre-execution" &&
384
- (i.requirement.category === "environment" || i.requirement.category === "input"));
385
- const blocking = errorIssues.filter((i) => !(i.requirement.timing === "pre-execution" &&
386
- (i.requirement.category === "environment" || i.requirement.category === "input")));
387
- // Hard errors that the user can't fix interactively (missing commands, etc.)
388
- if (blocking.length > 0) {
389
- console.log(formatValidationResult(result));
390
- return false;
391
- }
392
- // Warnings and info only — nothing missing
393
- if (fillable.length === 0) {
394
- if (result.counts.warnings > 0 || result.counts.info > 0) {
395
- console.log(formatValidationResult(result));
396
- }
397
- else {
398
- console.log("✅ Pre-execution validation passed\n");
399
- }
400
- return true;
401
- }
402
- // Show all missing variables upfront
403
- console.log(`\n${fillable.length} required value(s) missing — provide them to continue:\n`);
404
- for (const issue of fillable) {
405
- const stepInfo = issue.stepName ? ` needed by: ${issue.stepName}` : "";
406
- console.log(` • ${issue.requirement.name.padEnd(30)}${stepInfo}`);
407
- }
408
- console.log();
409
- const handler = new MissingEnvHandler({
410
- interactive: true,
411
- workflow: workflowName,
412
- });
413
- for (let i = 0; i < fillable.length; i++) {
414
- const issue = fillable[i];
415
- const error = new MissingEnvironmentVariableError(issue.requirement.name, issue.stepName, issue.requirement.hint);
416
- // Patch the counter into the box header label
417
- const original = console.log;
418
- const label = `(${i + 1} of ${fillable.length})`;
419
- let patched = false;
420
- console.log = (...args) => {
421
- if (!patched && typeof args[0] === "string" && args[0].includes("MISSING REQUIRED")) {
422
- original(`║ MISSING REQUIRED ENVIRONMENT VARIABLE ${label.padEnd(28)}║`);
423
- patched = true;
424
- return;
425
- }
426
- original(...args);
427
- };
428
- const handleResult = await handler.handleMissingVariable(error);
429
- console.log = original;
430
- if (handleResult.cancelled) {
431
- return false;
432
- }
433
- }
434
- handler.close();
435
- return true;
436
- }
12
+ import { handleInitCommand } from "./commands/init.js";
13
+ import { handleValidateCommand } from "./commands/validate.js";
14
+ import { handleRunCommand } from "./commands/run.js";
15
+ import { handleDetectProjectCommand } from "./commands/detect-project.js";
16
+ import { handleDetectPlatformCommand } from "./commands/detect-platform.js";
17
+ import { detectMobileProjectRoot } from "./shared/detect-project.js";
437
18
  async function main() {
438
19
  const args = process.argv.slice(2);
439
20
  if (args[0] === "--version" || args[0] === "-v") {
@@ -505,9 +86,8 @@ Examples:
505
86
  }
506
87
  // Handle detect-project command
507
88
  if (args[0] === "detect-project") {
508
- const projectType = detectMobileProjectRoot(process.cwd());
509
- console.log(projectType ?? "unknown");
510
- process.exit(projectType ? 0 : 1);
89
+ await handleDetectProjectCommand();
90
+ return;
511
91
  }
512
92
  // Handle init command
513
93
  if (args[0] === "init") {
@@ -745,267 +325,65 @@ Examples:
745
325
  process.exit(1);
746
326
  }
747
327
  }
748
- // Check for validate command or --validate-only flag
328
+ // Resolve --pipeline / -p flag OR a positional path (ci <cmd> <path>).
329
+ const resolvePipelinePath = (usage) => {
330
+ const flagIdx = args.findIndex((a) => a === "--pipeline" || a === "-p");
331
+ if (flagIdx !== -1 && args[flagIdx + 1]) {
332
+ return resolve(process.cwd(), args[flagIdx + 1]);
333
+ }
334
+ if (args[1] && !args[1].startsWith("-")) {
335
+ return resolve(process.cwd(), args[1]);
336
+ }
337
+ console.error("Error: Pipeline path is required");
338
+ for (const line of usage)
339
+ console.error(line);
340
+ process.exit(1);
341
+ };
342
+ const resolveWorkflowFlag = () => {
343
+ const idx = args.findIndex((a) => a === "--workflow" || a === "-w");
344
+ return idx !== -1 && args[idx + 1] ? args[idx + 1] : undefined;
345
+ };
346
+ // Handle validate command (and legacy `ci run <path> --validate-only`).
749
347
  const isValidateCommand = args[0] === "validate";
750
348
  const hasValidateOnlyFlag = args.includes("--validate-only");
751
349
  if (isValidateCommand || (args[0] === "run" && hasValidateOnlyFlag)) {
752
- // Support both: ci validate <path> and ci validate --pipeline <path>
753
- const pipelineIndex = args.findIndex((arg) => arg === "--pipeline" || arg === "-p");
754
- let pipelinePath;
755
- if (pipelineIndex !== -1 && args[pipelineIndex + 1]) {
756
- pipelinePath = resolve(process.cwd(), args[pipelineIndex + 1]);
757
- }
758
- else if (args[1] && !args[1].startsWith("-")) {
759
- // Second argument is the path (e.g., ci validate <path>)
760
- pipelinePath = resolve(process.cwd(), args[1]);
761
- }
762
- else {
763
- console.error("Error: Pipeline path is required");
764
- console.error("Usage: ci validate <path> [--workflow <name>]");
765
- console.error(" or: ci validate --pipeline <path> [--workflow <name>]");
766
- process.exit(1);
767
- }
768
- const workflowIndex = args.findIndex((arg) => arg === "--workflow" || arg === "-w");
769
- const workflowName = workflowIndex !== -1 && args[workflowIndex + 1]
770
- ? args[workflowIndex + 1]
771
- : undefined;
772
- if (!existsSync(pipelinePath)) {
773
- console.error(`Error: Pipeline file not found: ${pipelinePath}`);
774
- process.exit(1);
775
- }
776
- const fileExt = extname(pipelinePath).toLowerCase();
777
- const isYAML = fileExt === '.yml' || fileExt === '.yaml';
778
- if (!isYAML) {
779
- console.error("Error: validate only works with YAML pipeline files (.yml, .yaml)");
780
- process.exit(1);
781
- }
782
- try {
783
- const config = loadConfig();
784
- const yamlPipeline = loadYAMLPipeline(pipelinePath);
785
- // Prompt for workflow if not specified
786
- let selectedWorkflow = workflowName;
787
- if (!selectedWorkflow) {
788
- const workflows = Object.keys(yamlPipeline.workflows);
789
- selectedWorkflow = await promptForWorkflow(workflows);
790
- if (!selectedWorkflow) {
791
- process.exit(1);
792
- }
793
- }
794
- console.log(`🔍 Validating workflow: ${selectedWorkflow}`);
795
- console.log(` Pipeline: ${pipelinePath}\n`);
796
- const validator = new StepValidator(yamlPipeline, selectedWorkflow, config, pipelinePath);
797
- const result = await validator.validateWorkflow();
798
- console.log(formatValidationResult(result));
799
- process.exit(result.valid ? 0 : 1);
800
- }
801
- catch (error) {
802
- console.error("\n✗ Validation failed:");
803
- console.error(error instanceof Error ? error.message : String(error));
804
- process.exit(1);
805
- }
350
+ const pipelinePath = resolvePipelinePath([
351
+ "Usage: ci validate <path> [--workflow <name>]",
352
+ " or: ci validate --pipeline <path> [--workflow <name>]",
353
+ ]);
354
+ await handleValidateCommand({ pipelinePath, workflow: resolveWorkflowFlag() });
355
+ return;
806
356
  }
807
357
  if (args[0] === "detect-platform") {
808
- // Support both: ci detect-platform <path> and ci detect-platform --pipeline <path>
809
- const pipelineIndex = args.findIndex((arg) => arg === "--pipeline" || arg === "-p");
810
- let pipelinePath;
811
- if (pipelineIndex !== -1 && args[pipelineIndex + 1]) {
812
- pipelinePath = resolve(process.cwd(), args[pipelineIndex + 1]);
813
- }
814
- else if (args[1] && !args[1].startsWith("-")) {
815
- // Second argument is the path (e.g., ci detect-platform <path>)
816
- pipelinePath = resolve(process.cwd(), args[1]);
817
- }
818
- else {
819
- console.error("Error: Pipeline path is required");
820
- console.error("Usage: ci detect-platform <path> [--workflow <name>]");
821
- console.error(" or: ci detect-platform --pipeline <path> [--workflow <name>]");
822
- process.exit(1);
823
- }
824
- // Check for --workflow parameter (optional)
825
- const workflowIndex = args.findIndex((arg) => arg === "--workflow" || arg === "-w");
826
- const workflowName = workflowIndex !== -1 && args[workflowIndex + 1]
827
- ? args[workflowIndex + 1]
828
- : undefined;
829
- if (!existsSync(pipelinePath)) {
830
- console.error(`Error: Pipeline file not found: ${pipelinePath}`);
831
- process.exit(1);
832
- }
833
- // Only works with YAML files
834
- const fileExt = extname(pipelinePath).toLowerCase();
835
- const isYAML = fileExt === '.yml' || fileExt === '.yaml';
836
- if (!isYAML) {
837
- console.error("Error: detect-platform only works with YAML pipeline files (.yml, .yaml)");
838
- process.exit(1);
839
- }
840
- try {
841
- const yamlPipeline = loadYAMLPipeline(pipelinePath);
842
- // Prompt for workflow if not specified
843
- let selectedWorkflow = workflowName;
844
- if (!selectedWorkflow) {
845
- const workflows = Object.keys(yamlPipeline.workflows);
846
- selectedWorkflow = await promptForWorkflow(workflows);
847
- if (!selectedWorkflow) {
848
- process.exit(1);
849
- }
850
- }
851
- const platformInfo = detectPlatformInfo(yamlPipeline, selectedWorkflow);
852
- console.log(`\nPlatform Detection Results:`);
853
- console.log(`─────────────────────────────`);
854
- console.log(`Pipeline: ${pipelinePath}`);
855
- console.log(`Workflow: ${selectedWorkflow}`);
856
- console.log(`Platform: ${platformInfo.platform}`);
857
- console.log(`Stack: ${platformInfo.stack || 'N/A'}`);
858
- console.log(`Machine Type: ${platformInfo.machineType || 'N/A'}`);
859
- console.log();
860
- process.exit(0);
861
- }
862
- catch (error) {
863
- console.error("\n✗ Platform detection failed:");
864
- console.error(error instanceof Error ? error.message : String(error));
865
- process.exit(1);
866
- }
358
+ const pipelinePath = resolvePipelinePath([
359
+ "Usage: ci detect-platform <path> [--workflow <name>]",
360
+ " or: ci detect-platform --pipeline <path> [--workflow <name>]",
361
+ ]);
362
+ await handleDetectPlatformCommand({ pipelinePath, workflow: resolveWorkflowFlag() });
363
+ return;
867
364
  }
868
- else if (args[0] === "run") {
869
- // Support both: ci run <path> and ci run --pipeline <path>
870
- const pipelineIndex = args.findIndex((arg) => arg === "--pipeline" || arg === "-p");
365
+ if (args[0] === "run") {
366
+ // Path is optional for `ci run` handler discovers from .ci/pipelines/.
367
+ const flagIdx = args.findIndex((a) => a === "--pipeline" || a === "-p");
871
368
  let pipelinePath;
872
- if (pipelineIndex !== -1 && args[pipelineIndex + 1]) {
873
- pipelinePath = resolve(process.cwd(), args[pipelineIndex + 1]);
369
+ if (flagIdx !== -1 && args[flagIdx + 1]) {
370
+ pipelinePath = resolve(process.cwd(), args[flagIdx + 1]);
874
371
  }
875
372
  else if (args[1] && !args[1].startsWith("-")) {
876
- // Second argument is the path (e.g., ci run <path>)
877
373
  pipelinePath = resolve(process.cwd(), args[1]);
878
374
  }
879
- else {
880
- // No path given — check for .ci/pipelines/
881
- const pipelinesDir = resolve(process.cwd(), ".ci", "pipelines");
882
- if (!existsSync(pipelinesDir)) {
883
- console.error("Project not initialised. Run 'ci init' first.");
884
- process.exit(1);
885
- }
886
- const yamlFiles = readdirSync(pipelinesDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
887
- if (yamlFiles.length === 0) {
888
- console.error("No pipelines found in .ci/pipelines/. Run 'ci init' to import one.");
889
- process.exit(1);
890
- }
891
- if (yamlFiles.length === 1) {
892
- pipelinePath = resolve(pipelinesDir, yamlFiles[0]);
893
- console.log(`Using pipeline: ${yamlFiles[0]}`);
894
- }
895
- else {
896
- const { selected } = await prompts({
897
- type: "select",
898
- name: "selected",
899
- message: "Select a pipeline to run:",
900
- choices: yamlFiles.map((f) => ({ title: f, value: resolve(pipelinesDir, f) })),
901
- });
902
- if (!selected) {
903
- console.log("Cancelled.");
904
- process.exit(0);
905
- }
906
- pipelinePath = selected;
907
- }
908
- }
909
- // Check for --workflow parameter (optional, only used for YAML)
910
- const workflowIndex = args.findIndex((arg) => arg === "--workflow" || arg === "-w");
911
- const workflowName = workflowIndex !== -1 && args[workflowIndex + 1]
912
- ? args[workflowIndex + 1]
913
- : undefined;
914
- // Check for --production, --skip-validation, and --local flags
915
- const isProduction = args.includes("--production");
916
- const skipValidation = args.includes("--skip-validation");
917
- const isLocal = args.includes("--local");
918
- if (!existsSync(pipelinePath)) {
919
- console.error(`Error: Pipeline file not found: ${pipelinePath}`);
920
- process.exit(1);
921
- }
922
- try {
923
- const config = loadConfig();
924
- config.local = isLocal;
925
- const runner = new PipelineRunner(config);
926
- // Detect file type based on extension
927
- const fileExt = extname(pipelinePath).toLowerCase();
928
- const isYAML = fileExt === '.yml' || fileExt === '.yaml';
929
- const isJavaScriptModule = fileExt === '.ts' || fileExt === '.js';
930
- let pipeline;
931
- if (isYAML) {
932
- // Load YAML pipeline
933
- console.log(`Loading YAML pipeline: ${pipelinePath}`);
934
- const yamlPipeline = loadYAMLPipeline(pipelinePath);
935
- // Prompt for workflow if not specified
936
- let selectedWorkflow = workflowName;
937
- if (!selectedWorkflow) {
938
- const workflows = Object.keys(yamlPipeline.workflows);
939
- selectedWorkflow = await promptForWorkflow(workflows);
940
- if (!selectedWorkflow) {
941
- process.exit(1);
942
- }
943
- }
944
- else {
945
- console.log(`Using workflow: ${workflowName}`);
946
- }
947
- // Run pre-execution validation and collect missing variables (unless skipped)
948
- if (skipValidation) {
949
- console.log(`\n⏭️ Skipping validation (--skip-validation flag set)\n`);
950
- }
951
- else {
952
- const canProceed = await promptForMissingVariables(yamlPipeline, selectedWorkflow, config, pipelinePath);
953
- if (!canProceed) {
954
- console.error("\n✗ Pipeline cannot run. Fix the errors above before continuing.");
955
- process.exit(1);
956
- }
957
- }
958
- // Check if production mode
959
- if (isProduction) {
960
- console.log("═".repeat(60));
961
- console.log("🚀 PRODUCTION MODE");
962
- console.log("═".repeat(60));
963
- console.log("\nValidation passed. Pipeline would be dispatched to remote runner.");
964
- console.log("(Remote runner integration not yet implemented)\n");
965
- console.log("For now, use without --production flag to run locally.\n");
966
- process.exit(0);
967
- }
968
- // Convert and run locally (development mode)
969
- const conversionResult = await convertYAMLWithSecrets(yamlPipeline, {
970
- config,
971
- workflowName: selectedWorkflow,
972
- yamlFilePath: pipelinePath,
973
- interactive: true,
974
- });
975
- pipeline = conversionResult.pipeline;
976
- // Run pipeline with warnings
977
- await runner.runPipeline(pipeline, conversionResult.warnings, conversionResult.skippedSteps);
978
- }
979
- else if (isJavaScriptModule) {
980
- // Load TypeScript/JavaScript pipeline module
981
- console.log(`Loading pipeline: ${pipelinePath}`);
982
- const pipelineUrl = pathToFileURL(pipelinePath).href;
983
- const pipelineModule = await import(pipelineUrl);
984
- pipeline = pipelineModule.default;
985
- if (!pipeline || typeof pipeline !== "object") {
986
- throw new Error("Pipeline file must export a default PipelineDef object");
987
- }
988
- // Run pipeline without warnings (TypeScript pipelines don't have warnings)
989
- await runner.runPipeline(pipeline);
990
- }
991
- else {
992
- throw new Error(`Unsupported pipeline file format: ${fileExt}\n` +
993
- `Supported formats: .yml, .yaml, .ts, .js`);
994
- }
995
- console.log("\n✓ All steps completed successfully!\n");
996
- process.exit(0);
997
- }
998
- catch (error) {
999
- console.error("\n✗ Pipeline failed:");
1000
- console.error(error instanceof Error ? error.message : String(error));
1001
- process.exit(1);
1002
- }
1003
- }
1004
- else {
1005
- console.error(`Unknown command: ${args[0]}`);
1006
- console.error("Run 'ci --help' for usage information");
1007
- process.exit(1);
375
+ await handleRunCommand({
376
+ pipelinePath,
377
+ workflow: resolveWorkflowFlag(),
378
+ production: args.includes("--production"),
379
+ skipValidation: args.includes("--skip-validation"),
380
+ local: args.includes("--local"),
381
+ });
382
+ return;
1008
383
  }
384
+ console.error(`Unknown command: ${args[0]}`);
385
+ console.error("Run 'ci --help' for usage information");
386
+ process.exit(1);
1009
387
  }
1010
388
  main();
1011
389
  //# sourceMappingURL=cli.js.map