@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/cli.cjs +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +56 -678
- package/dist/src/commands/detect-platform.d.ts +13 -0
- package/dist/src/commands/detect-platform.d.ts.map +1 -0
- package/dist/src/commands/detect-platform.js +51 -0
- package/dist/src/commands/detect-project.d.ts +7 -0
- package/dist/src/commands/detect-project.d.ts.map +1 -0
- package/dist/src/commands/detect-project.js +12 -0
- package/dist/src/commands/index.d.ts +26 -0
- package/dist/src/commands/index.d.ts.map +1 -0
- package/dist/src/commands/index.js +22 -0
- package/dist/src/commands/init.d.ts +13 -0
- package/dist/src/commands/init.d.ts.map +1 -0
- package/dist/src/commands/init.js +262 -0
- package/dist/src/commands/run.d.ts +22 -0
- package/dist/src/commands/run.d.ts.map +1 -0
- package/dist/src/commands/run.js +131 -0
- package/dist/src/commands/validate.d.ts +10 -0
- package/dist/src/commands/validate.d.ts.map +1 -0
- package/dist/src/commands/validate.js +46 -0
- package/dist/src/shared/detect-project.d.ts +11 -0
- package/dist/src/shared/detect-project.d.ts.map +1 -0
- package/dist/src/shared/detect-project.js +53 -0
- package/dist/src/shared/gitignore.d.ts +6 -0
- package/dist/src/shared/gitignore.d.ts.map +1 -0
- package/dist/src/shared/gitignore.js +26 -0
- package/dist/src/shared/prompts.d.ts +18 -0
- package/dist/src/shared/prompts.d.ts.map +1 -0
- package/dist/src/shared/prompts.js +96 -0
- package/package.json +7 -3
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 {
|
|
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 {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
869
|
-
//
|
|
870
|
-
const
|
|
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 (
|
|
873
|
-
pipelinePath = resolve(process.cwd(), args[
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|